Skip to content

Commit ce5ca31

Browse files
authored
feat: add support for the simulate RPC (XLS-69d) (#2867)
1 parent 991a1d2 commit ce5ca31

File tree

9 files changed

+240
-5
lines changed

9 files changed

+240
-5
lines changed

packages/xrpl/HISTORY.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr
1010
### Changed
1111
* Deprecated `setTransactionFlagsToNumber`. Start using convertTxFlagsToNumber instead
1212

13+
### Added
14+
* Support for the `simulate` RPC ([XLS-69](https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0069-simulate))
15+
1316
## 4.1.0 (2024-12-23)
1417

1518
### Added

packages/xrpl/src/client/index.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,13 @@ import type {
4040
MarkerRequest,
4141
MarkerResponse,
4242
SubmitResponse,
43+
SimulateRequest,
4344
} from '../models/methods'
4445
import type { BookOffer, BookOfferCurrency } from '../models/methods/bookOffers'
46+
import {
47+
SimulateBinaryResponse,
48+
SimulateJsonResponse,
49+
} from '../models/methods/simulate'
4550
import type {
4651
EventTypes,
4752
OnEventToListenerMap,
@@ -764,6 +769,41 @@ class Client extends EventEmitter<EventTypes> {
764769
return submitRequest(this, signedTx, opts?.failHard)
765770
}
766771

772+
/**
773+
* Simulates an unsigned transaction.
774+
* Steps performed on a transaction:
775+
* 1. Autofill.
776+
* 2. Sign & Encode.
777+
* 3. Submit.
778+
*
779+
* @category Core
780+
*
781+
* @param transaction - A transaction to autofill, sign & encode, and submit.
782+
* @param opts - (Optional) Options used to sign and submit a transaction.
783+
* @param opts.binary - If true, return the metadata in a binary encoding.
784+
*
785+
* @returns A promise that contains SimulateResponse.
786+
* @throws RippledError if the simulate request fails.
787+
*/
788+
789+
public async simulate<Binary extends boolean = false>(
790+
transaction: SubmittableTransaction | string,
791+
opts?: {
792+
// If true, return the binary-encoded representation of the results.
793+
binary?: Binary
794+
},
795+
): Promise<
796+
Binary extends true ? SimulateBinaryResponse : SimulateJsonResponse
797+
> {
798+
// send request
799+
const binary = opts?.binary ?? false
800+
const request: SimulateRequest =
801+
typeof transaction === 'string'
802+
? { command: 'simulate', tx_blob: transaction, binary }
803+
: { command: 'simulate', tx_json: transaction, binary }
804+
return this.request(request)
805+
}
806+
767807
/**
768808
* Asynchronously submits a transaction and verifies that it has been included in a
769809
* validated ledger (or has errored/will not be included for some reason).

packages/xrpl/src/models/methods/index.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,14 @@ import {
148148
StateAccountingFinal,
149149
} from './serverInfo'
150150
import { ServerStateRequest, ServerStateResponse } from './serverState'
151+
import {
152+
SimulateBinaryRequest,
153+
SimulateBinaryResponse,
154+
SimulateJsonRequest,
155+
SimulateJsonResponse,
156+
SimulateRequest,
157+
SimulateResponse,
158+
} from './simulate'
151159
import { SubmitRequest, SubmitResponse } from './submit'
152160
import {
153161
SubmitMultisignedRequest,
@@ -203,6 +211,7 @@ type Request =
203211
| LedgerDataRequest
204212
| LedgerEntryRequest
205213
// transaction methods
214+
| SimulateRequest
206215
| SubmitRequest
207216
| SubmitMultisignedRequest
208217
| TransactionEntryRequest
@@ -261,6 +270,7 @@ type Response<Version extends APIVersion = typeof DEFAULT_API_VERSION> =
261270
| LedgerDataResponse
262271
| LedgerEntryResponse
263272
// transaction methods
273+
| SimulateResponse
264274
| SubmitResponse
265275
| SubmitMultisignedVersionResponseMap<Version>
266276
| TransactionEntryResponse
@@ -398,6 +408,12 @@ export type RequestResponseMap<
398408
? LedgerDataResponse
399409
: T extends LedgerEntryRequest
400410
? LedgerEntryResponse
411+
: T extends SimulateBinaryRequest
412+
? SimulateBinaryResponse
413+
: T extends SimulateJsonRequest
414+
? SimulateJsonResponse
415+
: T extends SimulateRequest
416+
? SimulateJsonResponse
401417
: T extends SubmitRequest
402418
? SubmitResponse
403419
: T extends SubmitMultisignedRequest
@@ -544,6 +560,8 @@ export {
544560
LedgerEntryRequest,
545561
LedgerEntryResponse,
546562
// transaction methods with types
563+
SimulateRequest,
564+
SimulateResponse,
547565
SubmitRequest,
548566
SubmitResponse,
549567
SubmitMultisignedRequest,

packages/xrpl/src/models/methods/ledger.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,13 +203,13 @@ export interface LedgerQueueData {
203203
}
204204

205205
export interface LedgerBinary
206-
extends Omit<Omit<Ledger, 'transactions'>, 'accountState'> {
206+
extends Omit<Ledger, 'transactions' | 'accountState'> {
207207
accountState?: string[]
208208
transactions?: string[]
209209
}
210210

211211
export interface LedgerBinaryV1
212-
extends Omit<Omit<LedgerV1, 'transactions'>, 'accountState'> {
212+
extends Omit<LedgerV1, 'transactions' | 'accountState'> {
213213
accountState?: string[]
214214
transactions?: string[]
215215
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import {
2+
BaseTransaction,
3+
Transaction,
4+
TransactionMetadata,
5+
} from '../transactions'
6+
7+
import { BaseRequest, BaseResponse } from './baseMethod'
8+
9+
/**
10+
* The `simulate` method simulates a transaction without submitting it to the network.
11+
* Returns a {@link SimulateResponse}.
12+
*
13+
* @category Requests
14+
*/
15+
export type SimulateRequest = BaseRequest & {
16+
command: 'simulate'
17+
18+
binary?: boolean
19+
} & (
20+
| {
21+
tx_blob: string
22+
tx_json?: never
23+
}
24+
| {
25+
tx_json: Transaction
26+
tx_blob?: never
27+
}
28+
)
29+
30+
export type SimulateBinaryRequest = SimulateRequest & {
31+
binary: true
32+
}
33+
34+
export type SimulateJsonRequest = SimulateRequest & {
35+
binary?: false
36+
}
37+
38+
/**
39+
* Response expected from an {@link SimulateRequest}.
40+
*
41+
* @category Responses
42+
*/
43+
export type SimulateResponse = SimulateJsonResponse | SimulateBinaryResponse
44+
45+
export interface SimulateBinaryResponse extends BaseResponse {
46+
result: {
47+
applied: false
48+
49+
engine_result: string
50+
51+
engine_result_code: number
52+
53+
engine_result_message: string
54+
55+
tx_blob: string
56+
57+
meta_blob: string
58+
59+
/**
60+
* The ledger index of the ledger version that was used to generate this
61+
* response.
62+
*/
63+
ledger_index: number
64+
}
65+
}
66+
67+
export interface SimulateJsonResponse<T extends BaseTransaction = Transaction>
68+
extends BaseResponse {
69+
result: {
70+
applied: false
71+
72+
engine_result: string
73+
74+
engine_result_code: number
75+
76+
engine_result_message: string
77+
78+
/**
79+
* The ledger index of the ledger version that was used to generate this
80+
* response.
81+
*/
82+
ledger_index: number
83+
84+
tx_json: T
85+
86+
meta?: TransactionMetadata<T>
87+
}
88+
}

packages/xrpl/src/sugar/submit.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { decode, encode } from 'ripple-binary-codec'
2-
31
import type {
42
Client,
53
SubmitRequest,
@@ -12,6 +10,7 @@ import { ValidationError, XrplError } from '../errors'
1210
import { Signer } from '../models/common'
1311
import { TxResponse } from '../models/methods'
1412
import { BaseTransaction } from '../models/transactions/common'
13+
import { decode, encode } from '../utils'
1514

1615
/** Approximate time for a ledger to close, in milliseconds */
1716
const LEDGER_CLOSE_TIME = 1000
@@ -52,7 +51,7 @@ export async function submitRequest(
5251
failHard = false,
5352
): Promise<SubmitResponse> {
5453
if (!isSigned(signedTransaction)) {
55-
throw new ValidationError('Transaction must be signed')
54+
throw new ValidationError('Transaction must be signed.')
5655
}
5756

5857
const signedTxEncoded =

packages/xrpl/test/integration/requests/serverInfo.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ describe('server_info (rippled)', function () {
125125
'build_version',
126126
'node_size',
127127
'initial_sync_duration_us',
128+
'git',
128129
]
129130
assert.deepEqual(
130131
omit(response.result.info, removeKeys),

packages/xrpl/test/integration/requests/serverState.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ describe('server_state', function () {
116116
'node_size',
117117
'initial_sync_duration_us',
118118
'ports',
119+
'git',
119120
]
120121
assert.deepEqual(
121122
omit(response.result.state, removeKeys),
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { assert } from 'chai'
2+
3+
import { AccountSet, SimulateRequest } from '../../../src'
4+
import { SimulateBinaryRequest } from '../../../src/models/methods/simulate'
5+
import serverUrl from '../serverUrl'
6+
import {
7+
setupClient,
8+
teardownClient,
9+
type XrplIntegrationTestContext,
10+
} from '../setup'
11+
12+
// how long before each test case times out
13+
const TIMEOUT = 20000
14+
15+
describe('simulate', function () {
16+
let testContext: XrplIntegrationTestContext
17+
18+
beforeEach(async () => {
19+
testContext = await setupClient(serverUrl)
20+
})
21+
afterEach(async () => teardownClient(testContext))
22+
23+
it(
24+
'json',
25+
async () => {
26+
const simulateRequest: SimulateRequest = {
27+
command: 'simulate',
28+
tx_json: {
29+
TransactionType: 'AccountSet',
30+
Account: testContext.wallet.address,
31+
NFTokenMinter: testContext.wallet.address,
32+
},
33+
}
34+
const simulateResponse = await testContext.client.request(simulateRequest)
35+
36+
assert.equal(simulateResponse.type, 'response')
37+
assert.typeOf(simulateResponse.result.meta, 'object')
38+
assert.typeOf(simulateResponse.result.tx_json, 'object')
39+
assert.equal(simulateResponse.result.engine_result, 'tesSUCCESS')
40+
assert.isFalse(simulateResponse.result.applied)
41+
},
42+
TIMEOUT,
43+
)
44+
45+
it(
46+
'binary',
47+
async () => {
48+
const simulateRequest: SimulateBinaryRequest = {
49+
command: 'simulate',
50+
tx_json: {
51+
TransactionType: 'AccountSet',
52+
Account: testContext.wallet.address,
53+
},
54+
binary: true,
55+
}
56+
const simulateResponse = await testContext.client.request(simulateRequest)
57+
58+
assert.equal(simulateResponse.type, 'response')
59+
assert.typeOf(simulateResponse.result.meta_blob, 'string')
60+
assert.typeOf(simulateResponse.result.tx_blob, 'string')
61+
assert.equal(simulateResponse.result.engine_result, 'tesSUCCESS')
62+
assert.isFalse(simulateResponse.result.applied)
63+
},
64+
TIMEOUT,
65+
)
66+
67+
it(
68+
'sugar',
69+
async () => {
70+
const tx: AccountSet = {
71+
TransactionType: 'AccountSet',
72+
Account: testContext.wallet.address,
73+
NFTokenMinter: testContext.wallet.address,
74+
}
75+
const simulateResponse = await testContext.client.simulate(tx)
76+
77+
assert.equal(simulateResponse.type, 'response')
78+
assert.typeOf(simulateResponse.result.meta, 'object')
79+
assert.typeOf(simulateResponse.result.tx_json, 'object')
80+
assert.equal(simulateResponse.result.engine_result, 'tesSUCCESS')
81+
assert.isFalse(simulateResponse.result.applied)
82+
},
83+
TIMEOUT,
84+
)
85+
})

0 commit comments

Comments
 (0)