diff --git a/packages/xrpl/HISTORY.md b/packages/xrpl/HISTORY.md index 0f995f47a7..74c5cdfc0d 100644 --- a/packages/xrpl/HISTORY.md +++ b/packages/xrpl/HISTORY.md @@ -10,6 +10,9 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr ### Changed * Deprecated `setTransactionFlagsToNumber`. Start using convertTxFlagsToNumber instead +### Added +* Support for the `simulate` RPC + ## 4.1.0 (2024-12-23) ### Added diff --git a/packages/xrpl/src/client/index.ts b/packages/xrpl/src/client/index.ts index 0afaa29b72..91186d99bf 100644 --- a/packages/xrpl/src/client/index.ts +++ b/packages/xrpl/src/client/index.ts @@ -40,8 +40,13 @@ import type { MarkerRequest, MarkerResponse, SubmitResponse, + SimulateRequest, } from '../models/methods' import type { BookOffer, BookOfferCurrency } from '../models/methods/bookOffers' +import { + SimulateBinaryResponse, + SimulateJsonResponse, +} from '../models/methods/simulate' import type { EventTypes, OnEventToListenerMap, @@ -764,6 +769,41 @@ 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). diff --git a/packages/xrpl/src/models/methods/index.ts b/packages/xrpl/src/models/methods/index.ts index a0259167b2..e54c56141a 100644 --- a/packages/xrpl/src/models/methods/index.ts +++ b/packages/xrpl/src/models/methods/index.ts @@ -148,6 +148,14 @@ import { StateAccountingFinal, } from './serverInfo' import { ServerStateRequest, ServerStateResponse } from './serverState' +import { + SimulateBinaryRequest, + SimulateBinaryResponse, + SimulateJsonRequest, + SimulateJsonResponse, + SimulateRequest, + SimulateResponse, +} from './simulate' import { SubmitRequest, SubmitResponse } from './submit' import { SubmitMultisignedRequest, @@ -203,6 +211,7 @@ type Request = | LedgerDataRequest | LedgerEntryRequest // transaction methods + | SimulateRequest | SubmitRequest | SubmitMultisignedRequest | TransactionEntryRequest @@ -261,6 +270,7 @@ type Response = | LedgerDataResponse | LedgerEntryResponse // transaction methods + | SimulateResponse | SubmitResponse | SubmitMultisignedVersionResponseMap | TransactionEntryResponse @@ -398,6 +408,12 @@ export type RequestResponseMap< ? LedgerDataResponse : T extends LedgerEntryRequest ? LedgerEntryResponse + : T extends SimulateBinaryRequest + ? SimulateBinaryResponse + : T extends SimulateJsonRequest + ? SimulateJsonResponse + : T extends SimulateRequest + ? SimulateJsonResponse : T extends SubmitRequest ? SubmitResponse : T extends SubmitMultisignedRequest @@ -544,6 +560,8 @@ export { LedgerEntryRequest, LedgerEntryResponse, // transaction methods with types + SimulateRequest, + SimulateResponse, SubmitRequest, SubmitResponse, SubmitMultisignedRequest, diff --git a/packages/xrpl/src/models/methods/ledger.ts b/packages/xrpl/src/models/methods/ledger.ts index 07c72cc27e..a4f657786c 100644 --- a/packages/xrpl/src/models/methods/ledger.ts +++ b/packages/xrpl/src/models/methods/ledger.ts @@ -203,13 +203,13 @@ export interface LedgerQueueData { } export interface LedgerBinary - extends Omit, 'accountState'> { + extends Omit { accountState?: string[] transactions?: string[] } export interface LedgerBinaryV1 - extends Omit, 'accountState'> { + extends Omit { accountState?: string[] transactions?: string[] } diff --git a/packages/xrpl/src/models/methods/simulate.ts b/packages/xrpl/src/models/methods/simulate.ts new file mode 100644 index 0000000000..ed27212eb8 --- /dev/null +++ b/packages/xrpl/src/models/methods/simulate.ts @@ -0,0 +1,88 @@ +import { + BaseTransaction, + Transaction, + TransactionMetadata, +} from '../transactions' + +import { BaseRequest, BaseResponse } from './baseMethod' + +/** + * The `simulate` method simulates a transaction without submitting it to the network. + * Returns a {@link SimulateResponse}. + * + * @category Requests + */ +export type SimulateRequest = BaseRequest & { + command: 'simulate' + + binary?: boolean +} & ( + | { + tx_blob: string + tx_json?: never + } + | { + tx_json: Transaction + tx_blob?: never + } + ) + +export type SimulateBinaryRequest = SimulateRequest & { + binary: true +} + +export type SimulateJsonRequest = SimulateRequest & { + binary?: false +} + +/** + * Response expected from an {@link SimulateRequest}. + * + * @category Responses + */ +export type SimulateResponse = SimulateJsonResponse | SimulateBinaryResponse + +export interface SimulateBinaryResponse extends BaseResponse { + result: { + applied: false + + engine_result: string + + engine_result_code: number + + engine_result_message: string + + tx_blob: string + + meta_blob: string + + /** + * The ledger index of the ledger version that was used to generate this + * response. + */ + ledger_index: number + } +} + +export interface SimulateJsonResponse + extends BaseResponse { + result: { + applied: false + + engine_result: string + + engine_result_code: number + + engine_result_message: string + + /** + * The ledger index of the ledger version that was used to generate this + * response. + */ + ledger_index: number + + tx_json: T + + meta?: TransactionMetadata + } +} diff --git a/packages/xrpl/src/sugar/submit.ts b/packages/xrpl/src/sugar/submit.ts index 423a863cf8..b14f68d945 100644 --- a/packages/xrpl/src/sugar/submit.ts +++ b/packages/xrpl/src/sugar/submit.ts @@ -1,5 +1,3 @@ -import { decode, encode } from 'ripple-binary-codec' - import type { Client, SubmitRequest, @@ -12,6 +10,7 @@ import { ValidationError, XrplError } from '../errors' import { Signer } from '../models/common' import { TxResponse } from '../models/methods' import { BaseTransaction } from '../models/transactions/common' +import { decode, encode } from '../utils' /** Approximate time for a ledger to close, in milliseconds */ const LEDGER_CLOSE_TIME = 1000 @@ -52,7 +51,7 @@ export async function submitRequest( failHard = false, ): Promise { if (!isSigned(signedTransaction)) { - throw new ValidationError('Transaction must be signed') + throw new ValidationError('Transaction must be signed.') } const signedTxEncoded = diff --git a/packages/xrpl/test/integration/requests/simulate.test.ts b/packages/xrpl/test/integration/requests/simulate.test.ts new file mode 100644 index 0000000000..d6ea013996 --- /dev/null +++ b/packages/xrpl/test/integration/requests/simulate.test.ts @@ -0,0 +1,85 @@ +import { assert } from 'chai' + +import { AccountSet, SimulateRequest } from '../../../src' +import { SimulateBinaryRequest } from '../../../src/models/methods/simulate' +import serverUrl from '../serverUrl' +import { + setupClient, + teardownClient, + type XrplIntegrationTestContext, +} from '../setup' + +// how long before each test case times out +const TIMEOUT = 20000 + +describe('simulate', function () { + let testContext: XrplIntegrationTestContext + + beforeEach(async () => { + testContext = await setupClient(serverUrl) + }) + afterEach(async () => teardownClient(testContext)) + + it( + 'json', + async () => { + const simulateRequest: SimulateRequest = { + command: 'simulate', + tx_json: { + TransactionType: 'AccountSet', + Account: testContext.wallet.address, + NFTokenMinter: testContext.wallet.address, + }, + } + const simulateResponse = await testContext.client.request(simulateRequest) + + assert.equal(simulateResponse.type, 'response') + assert.typeOf(simulateResponse.result.meta, 'object') + assert.typeOf(simulateResponse.result.tx_json, 'object') + assert.equal(simulateResponse.result.engine_result, 'tesSUCCESS') + assert.isFalse(simulateResponse.result.applied) + }, + TIMEOUT, + ) + + it( + 'binary', + async () => { + const simulateRequest: SimulateBinaryRequest = { + command: 'simulate', + tx_json: { + TransactionType: 'AccountSet', + Account: testContext.wallet.address, + }, + binary: true, + } + const simulateResponse = await testContext.client.request(simulateRequest) + + assert.equal(simulateResponse.type, 'response') + assert.typeOf(simulateResponse.result.meta_blob, 'string') + assert.typeOf(simulateResponse.result.tx_blob, 'string') + assert.equal(simulateResponse.result.engine_result, 'tesSUCCESS') + assert.isFalse(simulateResponse.result.applied) + }, + TIMEOUT, + ) + + it( + 'sugar', + async () => { + const tx: AccountSet = { + TransactionType: 'AccountSet', + Account: testContext.wallet.address, + NFTokenMinter: testContext.wallet.address, + } + const simulateResponse = await testContext.client.simulate(tx) + + assert.equal(simulateResponse.type, 'response') + assert.typeOf(simulateResponse.result.meta, 'object') + assert.typeOf(simulateResponse.result.tx_json, 'object') + assert.equal(simulateResponse.result.engine_result, 'tesSUCCESS') + assert.isFalse(simulateResponse.result.applied) + }, + TIMEOUT, + ) +})