feat: add support for the simulate RPC (XLS-69d) (#2867)

This commit is contained in:
Mayukha Vadari
2025-02-07 10:38:52 -08:00
committed by GitHub
parent 991a1d29a4
commit ce5ca316ca
9 changed files with 240 additions and 5 deletions

View File

@@ -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 ([XLS-69](https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0069-simulate))
## 4.1.0 (2024-12-23)
### Added

View File

@@ -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<EventTypes> {
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<Binary extends boolean = false>(
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).

View File

@@ -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<Version extends APIVersion = typeof DEFAULT_API_VERSION> =
| LedgerDataResponse
| LedgerEntryResponse
// transaction methods
| SimulateResponse
| SubmitResponse
| SubmitMultisignedVersionResponseMap<Version>
| 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,

View File

@@ -203,13 +203,13 @@ export interface LedgerQueueData {
}
export interface LedgerBinary
extends Omit<Omit<Ledger, 'transactions'>, 'accountState'> {
extends Omit<Ledger, 'transactions' | 'accountState'> {
accountState?: string[]
transactions?: string[]
}
export interface LedgerBinaryV1
extends Omit<Omit<LedgerV1, 'transactions'>, 'accountState'> {
extends Omit<LedgerV1, 'transactions' | 'accountState'> {
accountState?: string[]
transactions?: string[]
}

View File

@@ -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<T extends BaseTransaction = Transaction>
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<T>
}
}

View File

@@ -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<SubmitResponse> {
if (!isSigned(signedTransaction)) {
throw new ValidationError('Transaction must be signed')
throw new ValidationError('Transaction must be signed.')
}
const signedTxEncoded =

View File

@@ -125,6 +125,7 @@ describe('server_info (rippled)', function () {
'build_version',
'node_size',
'initial_sync_duration_us',
'git',
]
assert.deepEqual(
omit(response.result.info, removeKeys),

View File

@@ -116,6 +116,7 @@ describe('server_state', function () {
'node_size',
'initial_sync_duration_us',
'ports',
'git',
]
assert.deepEqual(
omit(response.result.state, removeKeys),

View File

@@ -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,
)
})