Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion packages/xrpl/src/client/partialPayment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 6 additions & 1 deletion packages/xrpl/src/sugar/autofill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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',
)
Expand Down
25 changes: 25 additions & 0 deletions packages/xrpl/test/client/autofill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down