Skip to content
5 changes: 3 additions & 2 deletions packages/xrpl/HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)
Expand Down
78 changes: 42 additions & 36 deletions packages/xrpl/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,41 @@ class Client extends EventEmitter<EventTypes> {
return Promise.all(promises).then(() => tx)
}

/**
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just moved, no code change

* 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)
}

/**
* Submits a signed/unsigned transaction.
* Steps performed on a transaction:
Expand Down Expand Up @@ -748,41 +783,6 @@ 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).
Expand Down Expand Up @@ -823,7 +823,7 @@ class Client extends EventEmitter<EventTypes> {
* 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.
Expand Down Expand Up @@ -864,6 +864,12 @@ class Client extends EventEmitter<EventTypes> {

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,
Expand Down
44 changes: 44 additions & 0 deletions packages/xrpl/test/client/submitAndWait.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { XrplError } from '../../src'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file (packages/xrpl/test/integration/submitAndWait.test.ts) is also relevant for adding this test case. Why did you create a new file?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's easier to test this sort of thing with a unit test where I can control the fixture as opposed to with an integration test.

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.',
)
})
})
2 changes: 2 additions & 0 deletions packages/xrpl/test/fixtures/rippled/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -36,6 +37,7 @@ import unsubscribe from './unsubscribe.json'

const submit = {
success: successSubmit,
temError: temErrorSubmit,
}

const ledger = {
Expand Down
28 changes: 28 additions & 0 deletions packages/xrpl/test/fixtures/rippled/submitTemError.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
Loading