diff --git a/packages/xrpl/HISTORY.md b/packages/xrpl/HISTORY.md index 7fa719827e..841603e5cf 100644 --- a/packages/xrpl/HISTORY.md +++ b/packages/xrpl/HISTORY.md @@ -5,7 +5,8 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr ## Unreleased ### Fixed -* Fix `AccountRoot` ledger object to correctly parse `FirstNFTokenSequence` field. +* Fix `AccountRoot` ledger object to correctly parse `FirstNFTokenSequence` field +* Fail faster on `tem` errors with `submitAndWait` ## 4.3.0 (2025-6-09) @@ -20,7 +21,7 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr * `TransactionStream` model includes `hash` field in APIv2 * `TransactionStream` model includes `close_time_iso` field only for APIv2 * Adds `MPTCurrency` type -* Better faucet support +* Improve faucet support * Improve multisign fee calculations ## 4.2.0 (2025-2-13) diff --git a/packages/xrpl/src/client/index.ts b/packages/xrpl/src/client/index.ts index 9e6b589fc9..4838fa64d2 100644 --- a/packages/xrpl/src/client/index.ts +++ b/packages/xrpl/src/client/index.ts @@ -699,6 +699,41 @@ class Client extends EventEmitter { return Promise.all(promises).then(() => tx) } + /** + * Simulates an unsigned transaction. + * Steps performed on a transaction: + * 1. Autofill. + * 2. Sign & Encode. + * 3. Submit. + * + * @category Core + * + * @param transaction - A transaction to autofill, sign & encode, and submit. + * @param opts - (Optional) Options used to sign and submit a transaction. + * @param opts.binary - If true, return the metadata in a binary encoding. + * + * @returns A promise that contains SimulateResponse. + * @throws RippledError if the simulate request fails. + */ + + public async simulate( + transaction: SubmittableTransaction | string, + opts?: { + // If true, return the binary-encoded representation of the results. + binary?: Binary + }, + ): Promise< + Binary extends true ? SimulateBinaryResponse : SimulateJsonResponse + > { + // send request + const binary = opts?.binary ?? false + const request: SimulateRequest = + typeof transaction === 'string' + ? { command: 'simulate', tx_blob: transaction, binary } + : { command: 'simulate', tx_json: transaction, binary } + return this.request(request) + } + /** * Submits a signed/unsigned transaction. * Steps performed on a transaction: @@ -748,41 +783,6 @@ class Client extends EventEmitter { return submitRequest(this, signedTx, opts?.failHard) } - /** - * Simulates an unsigned transaction. - * Steps performed on a transaction: - * 1. Autofill. - * 2. Sign & Encode. - * 3. Submit. - * - * @category Core - * - * @param transaction - A transaction to autofill, sign & encode, and submit. - * @param opts - (Optional) Options used to sign and submit a transaction. - * @param opts.binary - If true, return the metadata in a binary encoding. - * - * @returns A promise that contains SimulateResponse. - * @throws RippledError if the simulate request fails. - */ - - public async simulate( - transaction: SubmittableTransaction | string, - opts?: { - // If true, return the binary-encoded representation of the results. - binary?: Binary - }, - ): Promise< - Binary extends true ? SimulateBinaryResponse : SimulateJsonResponse - > { - // send request - const binary = opts?.binary ?? false - const request: SimulateRequest = - typeof transaction === 'string' - ? { command: 'simulate', tx_blob: transaction, binary } - : { command: 'simulate', tx_json: transaction, binary } - return this.request(request) - } - /** * Asynchronously submits a transaction and verifies that it has been included in a * validated ledger (or has errored/will not be included for some reason). @@ -823,7 +823,7 @@ class Client extends EventEmitter { * Under the hood, `submit` will call `client.autofill` by default, and because we've passed in a `Wallet` it * Will also sign the transaction for us before submitting the signed transaction binary blob to the ledger. * - * This is similar to `submitAndWait` which does all of the above, but also waits to see if the transaction has been validated. + * This is similar to `submit`, which does all of the above, but also waits to see if the transaction has been validated. * @param transaction - A transaction to autofill, sign & encode, and submit. * @param opts - (Optional) Options used to sign and submit a transaction. * @param opts.autofill - If true, autofill a transaction. @@ -864,6 +864,12 @@ class Client extends EventEmitter { const response = await submitRequest(this, signedTx, opts?.failHard) + if (response.result.engine_result.startsWith('tem')) { + throw new XrplError( + `Transaction failed, ${response.result.engine_result}: ${response.result.engine_result_message}`, + ) + } + const txHash = hashes.hashSignedTx(signedTx) return waitForFinalTransactionOutcome( this, diff --git a/packages/xrpl/test/client/submitAndWait.test.ts b/packages/xrpl/test/client/submitAndWait.test.ts new file mode 100644 index 0000000000..8138c64f2e --- /dev/null +++ b/packages/xrpl/test/client/submitAndWait.test.ts @@ -0,0 +1,44 @@ +import { XrplError } from '../../src' +import { Transaction } from '../../src/models/transactions' +import rippled from '../fixtures/rippled' +import { + setupClient, + teardownClient, + type XrplTestContext, +} from '../setupClient' +import { assertRejects } from '../testUtils' + +describe('client.submitAndWait', function () { + let testContext: XrplTestContext + + beforeEach(async () => { + testContext = await setupClient() + }) + afterEach(async () => teardownClient(testContext)) + + const signedTransaction: Transaction = { + TransactionType: 'Payment', + Sequence: 1, + LastLedgerSequence: 12312, + Amount: '20000000', + Fee: '12', + SigningPubKey: + '030E58CDD076E798C84755590AAF6237CA8FAE821070A59F648B517A30DC6F589D', + TxnSignature: + '3045022100B3D311371EDAB371CD8F2B661A04B800B61D4B132E09B7B0712D3B2F11B1758302203906B44C4A150311D74FF6A35B146763C0B5B40AC30BD815113F058AA17B3E63', + Account: 'rhvh5SrgBL5V8oeV9EpDuVszeJSSCEkbPc', + Destination: 'rQ3PTWGLCbPz8ZCicV5tCX3xuymojTng5r', + } + + it('should exit early with a tem error', async function () { + const signedTx = { ...signedTransaction } + + testContext.mockRippled!.addResponse('submit', rippled.submit.temError) + + await assertRejects( + testContext.client.submitAndWait(signedTx), + XrplError, + 'Transaction failed, temMALFORMED: Malformed transaction.', + ) + }) +}) diff --git a/packages/xrpl/test/fixtures/rippled/index.ts b/packages/xrpl/test/fixtures/rippled/index.ts index f8b4382d19..e7e0976c9b 100644 --- a/packages/xrpl/test/fixtures/rippled/index.ts +++ b/packages/xrpl/test/fixtures/rippled/index.ts @@ -23,6 +23,7 @@ import peerStatusStream from './streams/peerStatusChange.json' import transactionStream from './streams/transaction.json' import validationStream from './streams/validation.json' import successSubmit from './submit.json' +import temErrorSubmit from './submitTemError.json' import successSubscribe from './subscribe.json' import errorSubscribe from './subscribeError.json' import transaction_entry from './transactionEntry.json' @@ -36,6 +37,7 @@ import unsubscribe from './unsubscribe.json' const submit = { success: successSubmit, + temError: temErrorSubmit, } const ledger = { diff --git a/packages/xrpl/test/fixtures/rippled/submitTemError.json b/packages/xrpl/test/fixtures/rippled/submitTemError.json new file mode 100644 index 0000000000..452d81d017 --- /dev/null +++ b/packages/xrpl/test/fixtures/rippled/submitTemError.json @@ -0,0 +1,28 @@ +{ + "id": 0, + "status": "success", + "type": "response", + "result": { + "success": false, + "engine_result": "temMALFORMED", + "engine_result_code": -299, + "engine_result_message": "Malformed transaction.", + "tx_blob": "1200002280000000240000016861D4838D7EA4C6800000000000000000000000000055534400000000004B4E9C06F24296074F7BC48F92A97916C6DC5EA9684000000000002710732103AB40A0490F9B7ED8DF29D246BF2D6269820A0EE7742ACDD457BEA7C7D0931EDB7446304402200E5C2DD81FDF0BE9AB2A8D797885ED49E804DBF28E806604D878756410CA98B102203349581946B0DDA06B36B35DBC20EDA27552C1F167BCF5C6ECFF49C6A46F858081144B4E9C06F24296074F7BC48F92A97916C6DC5EA983143E9D4A2B8AA0780F682D136F7A56D6724EF53754", + "tx_json": { + "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Amount": { + "currency": "USD", + "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "value": "1" + }, + "Destination": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + "Fee": "10000", + "Flags": 2147483648, + "Sequence": 360, + "SigningPubKey": "03AB40A0490F9B7ED8DF29D246BF2D6269820A0EE7742ACDD457BEA7C7D0931EDB", + "TransactionType": "Payment", + "TxnSignature": "304402200E5C2DD81FDF0BE9AB2A8D797885ED49E804DBF28E806604D878756410CA98B102203349581946B0DDA06B36B35DBC20EDA27552C1F167BCF5C6ECFF49C6A46F8580", + "hash": "4D5D90890F8D49519E4151938601EF3D0B30B16CD6A519D9C99102C9FA77F7E0" + } + } +}