From 39fed49654d5de960b6d85898562857655d107f2 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com> Date: Thu, 27 Jun 2024 09:46:49 -0700 Subject: [PATCH] Aliter: Implement DeliverMax alias in Payment transactions, through autofill method (#2689) Co-authored-by: Omar Khan --- packages/xrpl/src/client/index.ts | 38 ++++++++- packages/xrpl/test/client/autofill.test.ts | 65 +++++++++++++++- .../integration/transactions/payment.test.ts | 78 ++++++++++++++++++- 3 files changed, 172 insertions(+), 9 deletions(-) diff --git a/packages/xrpl/src/client/index.ts b/packages/xrpl/src/client/index.ts index 5d2c881592..1e6b89288c 100644 --- a/packages/xrpl/src/client/index.ts +++ b/packages/xrpl/src/client/index.ts @@ -226,7 +226,7 @@ class Client extends EventEmitter { * const client = new Client('wss://s.altnet.rippletest.net:51233') * ``` */ - // eslint-disable-next-line max-lines-per-function -- okay because we have to set up all the connection handlers + /* eslint-disable max-lines-per-function -- the constructor requires more lines to implement the logic */ public constructor(server: string, options: ClientOptions = {}) { super() if (typeof server !== 'string' || !/wss?(?:\+unix)?:\/\//u.exec(server)) { @@ -290,6 +290,7 @@ class Client extends EventEmitter { this.emit('path_find', path) }) } + /* eslint-enable max-lines-per-function */ /** * Get the url that the client is connected to. @@ -638,7 +639,10 @@ class Client extends EventEmitter { * @param signersCount - The expected number of signers for this transaction. * Only used for multisigned transactions. * @returns The autofilled transaction. + * @throws ValidationError If Amount and DeliverMax fields are not identical in a Payment Transaction */ + + // eslint-disable-next-line complexity -- handling Payment transaction API v2 requires more logic public async autofill( transaction: T, signersCount?: number, @@ -646,7 +650,6 @@ class Client extends EventEmitter { const tx = { ...transaction } setValidAddresses(tx) - setTransactionFlagsToNumber(tx) const promises: Array> = [] @@ -666,6 +669,34 @@ class Client extends EventEmitter { promises.push(checkAccountDeleteBlockers(this, tx)) } + // 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 + if (tx.TransactionType === 'Payment' && tx.DeliverMax != null) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- This is a valid null check for Amount + if (tx.Amount == null) { + // If only DeliverMax is provided, use it to populate the Amount field + // 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-unsafe-assignment -- DeliverMax is a known RPC-level property + tx.Amount = tx.DeliverMax + } + + // 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) { + return Promise.reject( + new ValidationError( + 'PaymentTransaction: Amount and DeliverMax fields must be identical when both are provided', + ), + ) + } + + // 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 + delete tx.DeliverMax + } + return Promise.all(promises).then(() => tx) } @@ -909,7 +940,7 @@ class Client extends EventEmitter { * @param options.limit - Limit number of balances to return. * @returns An array of XRP/non-XRP balances for the given account. */ - // eslint-disable-next-line max-lines-per-function -- Longer definition is required for end users to see the definition. + /* eslint-disable max-lines-per-function -- getBalances requires more lines to implement logic */ public async getBalances( address: string, options: { @@ -957,6 +988,7 @@ class Client extends EventEmitter { ) return balances.slice(0, options.limit) } + /* eslint-enable max-lines-per-function */ /** * Fetch orderbook (buy/sell orders) between two currency pairs. This checks both sides of the orderbook diff --git a/packages/xrpl/test/client/autofill.test.ts b/packages/xrpl/test/client/autofill.test.ts index 9f6ee5296d..8c2d9b5ec8 100644 --- a/packages/xrpl/test/client/autofill.test.ts +++ b/packages/xrpl/test/client/autofill.test.ts @@ -7,6 +7,7 @@ import { Payment, Transaction, } from '../../src' +import { ValidationError } from '../../src/errors' import rippled from '../fixtures/rippled' import { setupClient, @@ -22,6 +23,8 @@ const LastLedgerSequence = 2908734 describe('client.autofill', function () { let testContext: XrplTestContext + const AMOUNT = '1234' + let paymentTx: Payment async function setupMockRippledVersionAndID( buildVersion: string, @@ -40,10 +43,68 @@ describe('client.autofill', function () { await testContext.client.connect() } - beforeEach(async () => { + beforeAll(async () => { testContext = await setupClient() }) - afterEach(async () => teardownClient(testContext)) + afterAll(async () => teardownClient(testContext)) + + beforeEach(async () => { + paymentTx = { + TransactionType: 'Payment', + Account: 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo', + Amount: AMOUNT, + Destination: 'rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy', + DestinationTag: 1, + Fee: '12', + Flags: 2147483648, + LastLedgerSequence: 65953073, + Sequence: 65923914, + SigningPubKey: + '02F9E33F16DF9507705EC954E3F94EB5F10D1FC4A354606DBE6297DBB1096FE654', + TxnSignature: + '3045022100E3FAE0EDEC3D6A8FF6D81BC9CF8288A61B7EEDE8071E90FF9314CB4621058D10022043545CF631706D700CEE65A1DB83EFDD185413808292D9D90F14D87D3DC2D8CB', + InvoiceID: + '6F1DFD1D0FE8A32E40E1F2C05CF1C15545BAB56B617F9C6C2D63A6B704BEF59B', + Paths: [ + [{ currency: 'BTC', issuer: 'r9vbV3EHvXWjSkeQ6CAcYVPGeq7TuiXY2X' }], + ], + SendMax: '100000000', + } + }) + + it('Validate Payment transaction API v2: Payment Transaction: Specify Only Amount field', async function () { + const txResult = await testContext.client.autofill(paymentTx) + + assert.strictEqual(txResult.Amount, AMOUNT) + }) + + it('Validate Payment transaction API v2: Payment Transaction: Specify Only DeliverMax field', async function () { + // @ts-expect-error -- DeliverMax is a non-protocol, RPC level field in Payment transactions + paymentTx.DeliverMax = paymentTx.Amount + // @ts-expect-error -- DeliverMax is a non-protocol, RPC level field in Payment transactions + delete paymentTx.Amount + const txResult = await testContext.client.autofill(paymentTx) + + assert.strictEqual(txResult.Amount, AMOUNT) + }) + + it('Validate Payment transaction API v2: Payment Transaction: identical DeliverMax and Amount fields', async function () { + // @ts-expect-error -- DeliverMax is a non-protocol, RPC level field in Payment transactions + paymentTx.DeliverMax = paymentTx.Amount + + const txResult = await testContext.client.autofill(paymentTx) + + assert.strictEqual(txResult.Amount, 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' + paymentTx.Amount = '1234' + + await assertRejects(testContext.client.autofill(paymentTx), ValidationError) + }) it('should not autofill if fields are present', async function () { const tx: Transaction = { diff --git a/packages/xrpl/test/integration/transactions/payment.test.ts b/packages/xrpl/test/integration/transactions/payment.test.ts index 333c6278cc..391ab317b2 100644 --- a/packages/xrpl/test/integration/transactions/payment.test.ts +++ b/packages/xrpl/test/integration/transactions/payment.test.ts @@ -1,4 +1,6 @@ -import { Payment } from '../../../src' +import { assert } from 'chai' + +import { Payment, Wallet } from '../../../src' import serverUrl from '../serverUrl' import { setupClient, @@ -12,24 +14,92 @@ const TIMEOUT = 20000 describe('Payment', function () { let testContext: XrplIntegrationTestContext + let paymentTx: Payment + const AMOUNT = '10000000' + // This wallet is used for DeliverMax related tests + let senderWallet: Wallet beforeEach(async () => { + // this payment transaction JSON needs to be refreshed before every test. + // Because, we tinker with Amount and DeliverMax fields in the API v2 tests + paymentTx = { + TransactionType: 'Payment', + Account: senderWallet.classicAddress, + Amount: AMOUNT, + Destination: 'rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy', + } + }) + + beforeAll(async () => { testContext = await setupClient(serverUrl) + senderWallet = await generateFundedWallet(testContext.client) }) - afterEach(async () => teardownClient(testContext)) + afterAll(async () => teardownClient(testContext)) it( 'base', async () => { - const wallet2 = await generateFundedWallet(testContext.client) const tx: Payment = { TransactionType: 'Payment', Account: testContext.wallet.classicAddress, - Destination: wallet2.classicAddress, + Destination: senderWallet.classicAddress, Amount: '1000', } await testTransaction(testContext.client, tx, testContext.wallet) }, TIMEOUT, ) + + it( + 'Validate Payment transaction API v2: Payment Transaction: Specify Only Amount field', + async () => { + const result = await testTransaction( + testContext.client, + paymentTx, + senderWallet, + ) + + assert.equal(result.result.engine_result_code, 0) + assert.equal((result.result.tx_json as Payment).Amount, AMOUNT) + }, + TIMEOUT, + ) + + it( + 'Validate Payment transaction API v2: Payment Transaction: Specify Only DeliverMax field', + async () => { + // @ts-expect-error -- DeliverMax is a non-protocol, RPC level field in Payment transactions + paymentTx.DeliverMax = paymentTx.Amount + // @ts-expect-error -- DeliverMax is a non-protocol, RPC level field in Payment transactions + delete paymentTx.Amount + + const result = await testTransaction( + testContext.client, + paymentTx, + senderWallet, + ) + + assert.equal(result.result.engine_result_code, 0) + assert.equal((result.result.tx_json as Payment).Amount, AMOUNT) + }, + TIMEOUT, + ) + + it( + 'Validate Payment transaction API v2: Payment Transaction: identical DeliverMax and Amount fields', + async () => { + // @ts-expect-error -- DeliverMax is a non-protocol, RPC level field in Payment transactions + paymentTx.DeliverMax = paymentTx.Amount + + const result = await testTransaction( + testContext.client, + paymentTx, + senderWallet, + ) + + assert.equal(result.result.engine_result_code, 0) + assert.equal((result.result.tx_json as Payment).Amount, AMOUNT) + }, + TIMEOUT, + ) })