diff --git a/packages/xrpl/src/client/partialPayment.ts b/packages/xrpl/src/client/partialPayment.ts index e039f76584..f46c1315eb 100644 --- a/packages/xrpl/src/client/partialPayment.ts +++ b/packages/xrpl/src/client/partialPayment.ts @@ -28,7 +28,16 @@ const WARN_PARTIAL_PAYMENT_CODE = 2001 /* eslint-disable complexity -- check different token types */ /* eslint-disable @typescript-eslint/consistent-type-assertions -- known currency type */ -function amountsEqual( +/** + * Returns true when two transaction amounts represent the same on-ledger value. + * + * Handles all three amount shapes (XRP string, IOU object, MPT object) and uses + * `BigNumber` for the numeric `value` field so `"1.0"` and `"1"` compare equal. + * Exported so callers outside this module (notably `handleDeliverMax` in + * `sugar/autofill.ts`) can avoid reference-equality bugs on IOU/MPT objects — + * see issue #3313. + */ +export function amountsEqual( amt1: Amount | MPTAmount, amt2: Amount | MPTAmount, ): boolean { diff --git a/packages/xrpl/src/sugar/autofill.ts b/packages/xrpl/src/sugar/autofill.ts index bb7809ac98..e19747058e 100644 --- a/packages/xrpl/src/sugar/autofill.ts +++ b/packages/xrpl/src/sugar/autofill.ts @@ -4,6 +4,7 @@ import BigNumber from 'bignumber.js' import { xAddressToClassicAddress, isValidXAddress } from 'ripple-address-codec' import { type Client } from '..' +import { amountsEqual } from '../client/partialPayment' import { ValidationError, XrplError } from '../errors' import { LoanBroker } from '../models/ledger' import { @@ -478,8 +479,12 @@ export function handleDeliverMax(tx: Payment): void { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, no-param-reassign -- needed here tx.Amount ??= tx.DeliverMax + // Use `amountsEqual` rather than `!==` so two distinct objects with identical + // currency/issuer/value fields don't trigger a spurious validation error. The previous + // reference-equality check rejected every IOU payment whose caller built `Amount` and + // `DeliverMax` as separate object literals (issue #3313). // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- needed here - if (tx.Amount != null && tx.Amount !== tx.DeliverMax) { + if (tx.Amount != null && !amountsEqual(tx.Amount, tx.DeliverMax)) { throw new ValidationError( 'PaymentTransaction: Amount and DeliverMax fields must be identical when both are provided', ) diff --git a/packages/xrpl/test/client/autofill.test.ts b/packages/xrpl/test/client/autofill.test.ts index b68b2ddd08..edc9f45477 100644 --- a/packages/xrpl/test/client/autofill.test.ts +++ b/packages/xrpl/test/client/autofill.test.ts @@ -105,6 +105,31 @@ describe('client.autofill', function () { await assertRejects(testContext.client.autofill(paymentTx), ValidationError) }) + it('Validate Payment transaction API v2: Payment Transaction: identical IOU DeliverMax and Amount as separate objects (#3313)', async function () { + // Regression test: the previous `tx.Amount !== tx.DeliverMax` used reference equality, + // which always returned true for two distinct IssuedCurrencyAmount object literals + // (different references, even with identical fields) — every IOU payment that set both + // fields as separate objects therefore tripped the validation throw. + const issuer = 'r9vbV3EHvXWjSkeQ6CAcYVPGeq7TuiXY2X' + paymentTx.Amount = { currency: 'USD', issuer, value: '100' } + paymentTx.DeliverMax = { currency: 'USD', issuer, value: '100' } + + const txResult = await testContext.client.autofill(paymentTx) + + assert.deepEqual(txResult.Amount, { currency: 'USD', issuer, value: '100' }) + assert.strictEqual('DeliverMax' in txResult, false) + }) + + it('Validate Payment transaction API v2: Payment Transaction: differing IOU DeliverMax and Amount values still rejected', async function () { + // Companion to the case above: ensure the fix didn't loosen validation. Identical + // currency/issuer but different `value` must still throw. + const issuer = 'r9vbV3EHvXWjSkeQ6CAcYVPGeq7TuiXY2X' + paymentTx.Amount = { currency: 'USD', issuer, value: '100' } + paymentTx.DeliverMax = { currency: 'USD', issuer, value: '200' } + + await assertRejects(testContext.client.autofill(paymentTx), ValidationError) + }) + it('should not autofill if fields are present', async function () { const tx: Transaction = { TransactionType: 'DepositPreauth',