diff --git a/packages/xrpl/HISTORY.md b/packages/xrpl/HISTORY.md index db958dab05..d7a03ab4d6 100644 --- a/packages/xrpl/HISTORY.md +++ b/packages/xrpl/HISTORY.md @@ -6,9 +6,11 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr ### Added * Adds utility function `convertTxFlagsToNumber` +* New util `areAmountsEqual` to check if 2 amounts are strictly equal ### Changed * Deprecated `setTransactionFlagsToNumber`. Start using convertTxFlagsToNumber instead +* `autofill` function in client not validating amounts correctly ## 4.1.0 (2024-12-23) diff --git a/packages/xrpl/src/client/index.ts b/packages/xrpl/src/client/index.ts index 0afaa29b72..f6ca71df7a 100644 --- a/packages/xrpl/src/client/index.ts +++ b/packages/xrpl/src/client/index.ts @@ -47,6 +47,7 @@ import type { OnEventToListenerMap, } from '../models/methods/subscribe' import type { SubmittableTransaction } from '../models/transactions' +import { areAmountsEqual } from '../models/transactions/common' import { convertTxFlagsToNumber } from '../models/utils/flags' import { ensureClassicAddress, @@ -699,7 +700,7 @@ class Client extends EventEmitter { // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- ignore type-assertions on the DeliverMax property // @ts-expect-error -- DeliverMax property exists only at the RPC level, not at the protocol level // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- This is a valid null check for Amount - if (tx.Amount != null && tx.Amount !== tx.DeliverMax) { + if (tx.Amount != null && !areAmountsEqual(tx.Amount, tx.DeliverMax)) { return Promise.reject( new ValidationError( 'PaymentTransaction: Amount and DeliverMax fields must be identical when both are provided', diff --git a/packages/xrpl/src/models/transactions/common.ts b/packages/xrpl/src/models/transactions/common.ts index d82625355f..4cebfd0cdb 100644 --- a/packages/xrpl/src/models/transactions/common.ts +++ b/packages/xrpl/src/models/transactions/common.ts @@ -1,5 +1,6 @@ /* eslint-disable max-lines -- common utility file */ import { HEX_REGEX } from '@xrplf/isomorphic/utils' +import BigNumber from 'bignumber.js' import { isValidClassicAddress, isValidXAddress } from 'ripple-address-codec' import { TRANSACTION_TYPES } from 'ripple-binary-codec' @@ -191,6 +192,46 @@ export function isAmount(amount: unknown): amount is Amount { ) } +/** + * Check if two amounts are equal. + * + * @param amount1 - The first amount to compare. + * @param amount2 - The second amount to compare. + * @returns Whether the two amounts are equal. + * @throws When the amounts are not valid. + */ +export function areAmountsEqual(amount1: unknown, amount2: unknown): boolean { + const isAmount1Invalid = !isAmount(amount1) + if (isAmount1Invalid || !isAmount(amount2)) { + throw new ValidationError( + `Amount: invalid field. Expected Amount but received ${JSON.stringify( + isAmount1Invalid ? amount1 : amount2, + )}`, + ) + } + + if (isString(amount1) && isString(amount2)) { + return new BigNumber(amount1).eq(amount2) + } + + if (isIssuedCurrency(amount1) && isIssuedCurrency(amount2)) { + return ( + amount1.currency === amount2.currency && + amount1.issuer === amount2.issuer && + new BigNumber(amount1.value).eq(amount2.value) + ) + } + + if (isMPTAmount(amount1) && isMPTAmount(amount2)) { + return ( + amount1.mpt_issuance_id === amount2.mpt_issuance_id && + new BigNumber(amount1.value).eq(amount2.value) + ) + } + + return false +} + /** * Verify the form and type of an XChainBridge at runtime. * diff --git a/packages/xrpl/test/client/autofill.test.ts b/packages/xrpl/test/client/autofill.test.ts index 8c2d9b5ec8..2e154d26c3 100644 --- a/packages/xrpl/test/client/autofill.test.ts +++ b/packages/xrpl/test/client/autofill.test.ts @@ -6,6 +6,7 @@ import { EscrowFinish, Payment, Transaction, + IssuedCurrencyAmount, } from '../../src' import { ValidationError } from '../../src/errors' import rippled from '../fixtures/rippled' @@ -98,6 +99,25 @@ describe('client.autofill', function () { assert.strictEqual('DeliverMax' in txResult, false) }) + it('Validate Payment transaction API v2: Payment Transaction: identical DeliverMax and Amount fields using amount objects', async function () { + // @ts-expect-error -- DeliverMax is a non-protocol, RPC level field in Payment transactions + paymentTx.DeliverMax = { + currency: 'USD', + value: AMOUNT, + issuer: 'r9vbV3EHvXWjSkeQ6CAcYVPGeq7TuiXY2X', + } + paymentTx.Amount = { + currency: 'USD', + value: AMOUNT, + issuer: 'r9vbV3EHvXWjSkeQ6CAcYVPGeq7TuiXY2X', + } + + const txResult = await testContext.client.autofill(paymentTx) + + assert.strictEqual((txResult.Amount as IssuedCurrencyAmount).value, AMOUNT) + assert.strictEqual('DeliverMax' in txResult, false) + }) + it('Validate Payment transaction API v2: Payment Transaction: differing DeliverMax and Amount fields', async function () { // @ts-expect-error -- DeliverMax is a non-protocol, RPC level field in Payment transactions paymentTx.DeliverMax = '6789' @@ -106,6 +126,22 @@ describe('client.autofill', function () { await assertRejects(testContext.client.autofill(paymentTx), ValidationError) }) + it('Validate Payment transaction API v2: Payment Transaction: differing DeliverMax and Amount fields using objects', async function () { + // @ts-expect-error -- DeliverMax is a non-protocol, RPC level field in Payment transactions + paymentTx.DeliverMax = { + currency: 'USD', + value: '31415', + issuer: 'r9vbV3EHvXWjSkeQ6CAcYVPGeq7TuiXY2X', + } + paymentTx.Amount = { + currency: 'USD', + value: '27182', + issuer: 'r9vbV3EHvXWjSkeQ6CAcYVPGeq7TuiXY2X', + } + + await assertRejects(testContext.client.autofill(paymentTx), ValidationError) + }) + it('should not autofill if fields are present', async function () { const tx: Transaction = { TransactionType: 'DepositPreauth',