Skip to content
Open
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
23411ac
feat: support for custom definitions on xrpl methods: sign, encode, e…
elmurci Apr 21, 2024
9249824
chore: tests
elmurci Apr 23, 2024
b529149
docs(history): xrpl history update
elmurci Apr 23, 2024
a618fb9
chore: maintain linebreak
elmurci Apr 23, 2024
b283fc3
chore: keep source.fixAll.eslint value
elmurci Apr 23, 2024
4689b02
chore: typo
elmurci Apr 23, 2024
59c2074
Update packages/xrpl/src/models/transactions/transaction.ts
elmurci May 7, 2024
6089035
Update packages/xrpl/src/models/transactions/transaction.ts
elmurci Aug 19, 2024
7021e63
Support custom definitions for `client.submit()`, `client.submitAndWa…
tequdev Aug 23, 2024
02a8503
fix some lint error
tequdev Aug 23, 2024
3153659
Merge pull request #1 from tequdev/xrpl_binary_codec_custom_definitio…
elmurci Aug 25, 2024
b047f04
feat: addresses https://github.com/XRPLF/xrpl.js/pull/2683#discussion…
elmurci Aug 26, 2024
eeb9f43
Merge branch 'main' into xrpl_binary_codec_custom_definition_support
elmurci Aug 26, 2024
2900ab3
Update packages/xrpl/src/client/index.ts
mvadari Sep 5, 2024
2c60a1e
fix: address lint errors
elmurci Sep 13, 2024
071beb5
docs: update history on xrpl package
elmurci Nov 21, 2024
04446ff
chore: typo validateTxAgainstCustomDefintions
elmurci Nov 21, 2024
2452093
chore: remove getDefinitins function, https://github.com/XRPLF/xrpl.j…
elmurci Jan 9, 2025
4731f60
docs: add information about the new definition parameter, https://git…
elmurci Jan 9, 2025
5db85fe
docs: typo, https://github.com/XRPLF/xrpl.js/pull/2683#discussion_r18…
elmurci Jan 9, 2025
006a3cb
Merge remote-tracking branch 'upstream/main' into xrpl_binary_codec_c…
tequdev Mar 23, 2026
62a765d
Merge remote-tracking branch 'upstream/main' into xrpl_binary_codec_c…
tequdev Mar 23, 2026
0373902
update HISTORY.md
tequdev Mar 23, 2026
137f82e
fix lint error
tequdev Mar 23, 2026
6ae5337
fix test
tequdev Mar 23, 2026
3168faa
Merge remote-tracking branch 'upstream/main' into xrpl_binary_codec_c…
tequdev Apr 23, 2026
caa5fdf
Fix HISTORY.md
tequdev Apr 23, 2026
0360c4e
add customDefinition to RawTransaction validate
tequdev Apr 23, 2026
b5aff79
Update packages/xrpl/src/client/index.ts
tequdev Apr 23, 2026
777a8e2
pass definitions to submitRequest(), add `getDefinitions()`
tequdev Apr 23, 2026
7f5b146
Merge branch 'main' into xrpl_binary_codec_custom_definition_support
tequdev Apr 24, 2026
fd0465e
Merge branch 'main' into xrpl_binary_codec_custom_definition_support
tequdev May 5, 2026
f579a9f
Merge branch 'main' into xrpl_binary_codec_custom_definition_support
tequdev May 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions packages/ripple-binary-codec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ Functions to encode/decode to/from the ripple [binary serialization format](http
```


### decode(binary: string): object
Decode a hex-string into a transaction object.
### decode(binary: string, definitions?: XrplDefinitionsBase): object
Decode a hex-string into a transaction object. Optionally, a custom definition may be provided and utilized during the decoding process
```js
> api.decode('1100612200000000240000000125000000072D0000000055DF530FB14C5304852F20080B0A8EEF3A6BDD044F41F4EBBD68B8B321145FE4FF6240000002540BE4008114D0F5430B66E06498D4CEEC816C7B3337F9982337')
{
Expand All @@ -26,8 +26,8 @@ Decode a hex-string into a transaction object.
}
```

### encode(json: object): string
Encode a transaction object into a hex-string. Note that encode filters out fields with undefined values.
### encode(json: object, definitions?: XrplDefinitionsBase): string
Encode a transaction object into a hex-string. Note that encode filters out fields with undefined values. Optionally, a custom definition may be provided and utilized during the encoding process
```js
> api.encode({
LedgerEntryType: 'AccountRoot',
Expand All @@ -37,12 +37,12 @@ Encode a transaction object into a hex-string. Note that encode filters out fiel
OwnerCount: 0,
PreviousTxnID: 'DF530FB14C5304852F20080B0A8EEF3A6BDD044F41F4EBBD68B8B321145FE4FF',
Balance: '10000000000',
Account: 'rLs1MzkFWCxTbuAHgjeTZK4fcCDDnf2KRv'
Account: 'rLs1MzkFWCxTbuAHgjeTZK4fcCDDnf2KRv'
})
'1100612200000000240000000125000000072D0000000055DF530FB14C5304852F20080B0A8EEF3A6BDD044F41F4EBBD68B8B321145FE4FF6240000002540BE4008114D0F5430B66E06498D4CEEC816C7B3337F9982337'
```

#### X-Address Compatibility
#### X-Address Compatibility
* ripple-binary-codec handles X-addresses by looking for a few specific files (Account/SourceTag, Destination/DestinationTag).
* If other fields (in the future) must to support X-addresses with tags, this library will need to be updated.
* When decoding rippled binary, the output will always output classic address + tag, with no X-addresses. X-address support only applies when encoding to binary.
Expand All @@ -54,25 +54,25 @@ Encode a transaction object into a hex-string. Note that encode filters out fiel
* When _decoding_, if a currency code is three uppercase letters or numbers (`/^[A-Z0-9]{3}$/`), then it will be decoded into that string. For example,`0000000000000000000000004142430000000000` decodes as `ABC`.
* When decoding, if a currency code is does not match the regex, then it is not considered to be an ISO 4217 or pseudo-ISO currency. ripple-binary-codec will return a 160-bit hex-string (40 hex characters). For example, `0000000000000000000000006142430000000000` (`aBC`) decodes as `0000000000000000000000006142430000000000` because it contains a lowercase letter.

### encodeForSigning(json: object): string
### encodeForSigning(json: object, definitions?: XrplDefinitionsBase): string

Encode the transaction object for signing.
Encode the transaction object for signing. Optionally, a custom definition may be provided and utilized during the encoding process

### encodeForSigningClaim(json: object): string

Encode the transaction object for payment channel claim.

### encodeForMultisigning(json: object, signer: string): string
### encodeForMultisigning(json: object, signer: string, definitions?: XrplDefinitionsBase): string

Encode the transaction object for multi-signing.
Encode the transaction object for multi-signing. Optionally, a custom definition may be provided and utilized during the encoding process

### encodeQuality(value: string): string
```js
> api.encodeQuality('195796912.5171664')
'5D06F4C3362FE1D0'
```

### decodeQuality(value: string): string
### decodeQuality(value: string): string
```js
> api.decodeQuality('5D06F4C3362FE1D0')
'195796912.5171664'
Expand Down
2 changes: 2 additions & 0 deletions packages/xrpl/HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr

### Added
* Add new fields to `ServerDefinitionsResponse`: `ACCOUNT_SET_FLAGS`, `LEDGER_ENTRY_FLAGS`, `LEDGER_ENTRY_FORMATS`, `TRANSACTION_FLAGS`, and `TRANSACTION_FORMATS`, reflecting new sections returned by `server_definitions` in rippled.
* Adds support for Custom Definitions to `client.submit()` and `client.submitAndWait()`
* Custom definitions support for `util.encode`, `util.decode`, `util.encodeForSigning` and `Wallet.sign`.

### Fixed
* Fix event listener accumulation bug where `'connected'` event handlers would fire multiple times after each reconnection. The fix cleans up stale listeners from previous reconnect attempts to prevent duplicate event emissions on flaky connections with multiple sequential reconnect attempts.
Expand Down
16 changes: 11 additions & 5 deletions packages/xrpl/src/Wallet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { wordlist } from '@scure/bip39/wordlists/english.js'
import { bytesToHex } from '@xrplf/isomorphic/utils'
import BigNumber from 'bignumber.js'
import { classicAddressToXAddress, encodeSeed } from 'ripple-address-codec'
import { encode } from 'ripple-binary-codec'
import { encode, XrplDefinitionsBase } from 'ripple-binary-codec'
import { deriveAddress, deriveKeypair, generateSeed } from 'ripple-keypairs'

import ECDSA from '../ECDSA'
Expand Down Expand Up @@ -356,15 +356,17 @@ export class Wallet {
* @param transaction - A transaction to be signed offline.
* @param multisign - Specify true/false to use multisign or actual address (classic/x-address) to make multisign tx request.
* The actual address is only needed in the case of regular key usage.
* @param definitions Custom rippled types to use instead of the default. Used for sidechains and amendments.
* @returns A signed transaction.
* @throws ValidationError if the transaction is already signed or does not encode/decode to same result.
* @throws XrplError if the issued currency being signed is XRP ignoring case.
*/
// eslint-disable-next-line max-lines-per-function -- introduced more checks to support both string and boolean inputs.
// eslint-disable-next-line max-lines-per-function, max-params -- introduced more checks to support string and boolean inputs.
public sign(
this: Wallet,
transaction: Transaction,
multisign?: boolean | string,
definitions?: XrplDefinitionsBase,
): {
tx_blob: string
hash: string
Expand Down Expand Up @@ -395,7 +397,7 @@ export class Wallet {
* This will throw a more clear error for JS users if the supplied transaction has incorrect formatting
*/
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- validate does not accept Transaction type
validate(tx as unknown as Record<string, unknown>)
validate(tx as unknown as Record<string, unknown>, definitions)
if (hasFlag(tx, GlobalFlags.tfInnerBatchTxn, 'tfInnerBatchTxn')) {
throw new ValidationError('Cannot sign a Batch inner transaction.')
}
Expand All @@ -411,6 +413,7 @@ export class Wallet {
txToSignAndEncode,
this.privateKey,
multisignAddress,
definitions,
),
}
txToSignAndEncode.Signers = [{ Signer: signer }]
Expand All @@ -419,13 +422,16 @@ export class Wallet {
txToSignAndEncode.TxnSignature = computeSignature(
txToSignAndEncode,
this.privateKey,
undefined,
definitions,
)
}

const serialized = encode(txToSignAndEncode)
const serialized = encode(txToSignAndEncode, definitions)

return {
tx_blob: serialized,
hash: hashSignedTx(serialized),
hash: hashSignedTx(serialized, definitions),
}
}

Expand Down
20 changes: 16 additions & 4 deletions packages/xrpl/src/Wallet/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
encode,
encodeForMultisigning,
encodeForSigning,
XrplDefinitionsBase,
} from 'ripple-binary-codec'
import { sign } from 'ripple-keypairs'

Expand Down Expand Up @@ -61,20 +62,25 @@ export function addressToBigNumber(address: string): BigNumber {
* Decodes a transaction or transaction blob into a Transaction object.
*
* @param txOrBlob - A Transaction object or a hex string representing a transaction blob.
* @param definitions - Custom rippled types to use instead of the default. Used for sidechains and amendments.
* @returns A Transaction object.
* @throws If the input is not a valid Transaction or transaction blob.
*/
export function getDecodedTransaction(
txOrBlob: Transaction | string,
definitions?: XrplDefinitionsBase,
): Transaction {
if (typeof txOrBlob === 'object') {
// We need this to handle X-addresses in multisigning
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We are casting here to get strong typing
return decode(encode(txOrBlob)) as unknown as Transaction
return decode(
encode(txOrBlob, definitions),
definitions,
) as unknown as Transaction
}

// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We are casting here to get strong typing
return decode(txOrBlob) as unknown as Transaction
return decode(txOrBlob, definitions) as unknown as Transaction
}

/**
Expand All @@ -84,19 +90,25 @@ export function getDecodedTransaction(
* @param privateKey - A key to sign the transaction with.
* @param signAs - Multisign only. An account address to include in the Signer field.
* Can be either a classic address or an XAddress.
* @param definitions Custom rippled types to use instead of the default. Used for sidechains and amendments.
* @returns A signed transaction in the proper format.
*/
// eslint-disable-next-line max-params -- Needs 4 params
export function computeSignature(
tx: Transaction,
privateKey: string,
signAs?: string,
definitions?: XrplDefinitionsBase,
): string {
if (signAs) {
const classicAddress = isValidXAddress(signAs)
? xAddressToClassicAddress(signAs).classicAddress
: signAs

return sign(encodeForMultisigning(tx, classicAddress), privateKey)
return sign(
encodeForMultisigning(tx, classicAddress, definitions),
privateKey,
)
}
return sign(encodeForSigning(tx), privateKey)
return sign(encodeForSigning(tx, definitions), privateKey)
}
28 changes: 25 additions & 3 deletions packages/xrpl/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

/* eslint-disable max-lines -- Client is a large file w/ lots of imports/exports */
import { EventEmitter } from 'eventemitter3'
import { XrplDefinitions, XrplDefinitionsBase } from 'ripple-binary-codec'
import type { DefinitionsData } from 'ripple-binary-codec/dist/enums/xrpl-definitions-base'

import {
RippledError,
Expand Down Expand Up @@ -225,6 +227,12 @@ class Client extends EventEmitter<EventTypes> {
*/
public buildVersion: string | undefined

/**
* Custom rippled types to use instead of the default. Used for sidechains and amendments.
*
*/
public definitions: XrplDefinitionsBase | undefined

Comment on lines +230 to +235

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Client.definitions is never wired into ClientOptions.

The new public field is declared but there is no way to set it at construction — users must mutate client.definitions = ... after new Client(...). Consider exposing it through ClientOptions (e.g., options.definitions) and assigning it in the constructor, mirroring feeCushion/maxFeeXRP. This also makes the behavior documentable and consistent with how the property is threaded into submit/submitAndWait.

Suggested change
 export interface ClientOptions extends ConnectionUserOptions {
   ...
   timeout?: number
+  /**
+   * Custom rippled transaction type definitions for sidechains/amendments.
+   */
+  definitions?: XrplDefinitionsBase
 }
   public definitions: XrplDefinitionsBase | undefined
   ...
   public constructor(server: string, options: ClientOptions = {}) {
     ...
     this.feeCushion = options.feeCushion ?? DEFAULT_FEE_CUSHION
     this.maxFeeXRP = options.maxFeeXRP ?? DEFAULT_MAX_FEE_XRP
+    this.definitions = options.definitions
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/xrpl/src/client/index.ts` around lines 229 - 234, Client.definitions
is declared but not exposed via ClientOptions; add an optional definitions?:
XrplDefinitionsBase to the ClientOptions type/interface and assign
this.options.definitions (or options.definitions) to this.definitions in the
Client constructor, following the pattern used for feeCushion/maxFeeXRP, and
ensure any internal uses (e.g., submit and submitAndWait) read from
this.definitions rather than only from mutated instances so the value is
available immediately after new Client(...).

/**
* API Version used by the server this client is connected to
*
Expand Down Expand Up @@ -642,6 +650,14 @@ class Client extends EventEmitter<EventTypes> {
return this.connection.isConnected()
}

public async getDefinitions(): Promise<void> {
const response = await this.request({
command: 'server_definitions',
})
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- response.result is the DefinitionsData
this.definitions = new XrplDefinitions(response.result as DefinitionsData)
}

/**
* Autofills fields in a transaction. This will set `Sequence`, `Fee`,
* `lastLedgerSequence` according to the current state of the server this Client
Expand Down Expand Up @@ -801,8 +817,11 @@ class Client extends EventEmitter<EventTypes> {
wallet?: Wallet
},
): Promise<SubmitResponse> {
const signedTx = await getSignedTx(this, transaction, opts)
return submitRequest(this, signedTx, opts?.failHard)
const signedTx = await getSignedTx(this, transaction, {
...opts,
definitions: this.definitions,
})
return submitRequest(this, signedTx, opts?.failHard, this.definitions)
}

/**
Expand Down Expand Up @@ -875,7 +894,10 @@ class Client extends EventEmitter<EventTypes> {
wallet?: Wallet
},
): Promise<TxResponse<T>> {
const signedTx = await getSignedTx(this, transaction, opts)
const signedTx = await getSignedTx(this, transaction, {
...opts,
definitions: this.definitions,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Severity: MEDIUM

submitAndWait passes this.definitions to getSignedTx here, but omits it from the subsequent submitRequest() and hashSignedTx() calls below. Compare with submit() which correctly passes this.definitions to submitRequest (line 824). This causes isSigned check, encoding, and hash computation to use wrong (default) definitions for custom types.
Helpful? Add 👍 / 👎

💡 Fix Suggestion

Suggestion: Pass this.definitions to both submitRequest and hashes.hashSignedTx in the submitAndWait method, matching the pattern used in submit():

  1. On line 909, change:
    const response = await submitRequest(this, signedTx, opts?.failHard)
    to:
    const response = await submitRequest(this, signedTx, opts?.failHard, this.definitions)

  2. On line 917, change:
    const txHash = hashes.hashSignedTx(signedTx)
    to:
    const txHash = hashes.hashSignedTx(signedTx, this.definitions)

This ensures that custom definitions are used for the isSigned check, encode call in submitRequest, and the decode/encode calls in hashSignedTx, consistent with how submit() already passes this.definitions on line 824.

})

const lastLedger = getLastLedgerSequence(signedTx)
if (lastLedger == null) {
Expand Down
6 changes: 4 additions & 2 deletions packages/xrpl/src/models/transactions/common.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable max-lines -- common utility file */
import { HEX_REGEX } from '@xrplf/isomorphic/utils'
import { isValidClassicAddress, isValidXAddress } from 'ripple-address-codec'
import { TRANSACTION_TYPES } from 'ripple-binary-codec'
import { DEFAULT_DEFINITIONS, XrplDefinitionsBase } from 'ripple-binary-codec'

import { ValidationError } from '../../errors'
import {
Expand Down Expand Up @@ -542,11 +542,13 @@ export interface BaseTransaction extends Record<string, unknown> {
* any time a transaction will be verified.
*
* @param common - An interface w/ common transaction fields.
* @param definitions - Custom rippled types to use instead of the default. Used for sidechains and amendments.
* @throws When the common param is malformed.
*/
// eslint-disable-next-line max-statements, max-lines-per-function -- lines required for validation
export function validateBaseTransaction(
common: unknown,
definitions: XrplDefinitionsBase = DEFAULT_DEFINITIONS,
): asserts common is BaseTransaction {
if (!isRecord(common)) {
throw new ValidationError(
Expand All @@ -562,7 +564,7 @@ export function validateBaseTransaction(
throw new ValidationError('BaseTransaction: TransactionType not string')
}

if (!TRANSACTION_TYPES.includes(common.TransactionType)) {
if (!definitions.transactionNames.includes(common.TransactionType)) {
throw new ValidationError(
`BaseTransaction: Unknown TransactionType ${common.TransactionType}`,
)
Expand Down
25 changes: 18 additions & 7 deletions packages/xrpl/src/models/transactions/transaction.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* eslint-disable max-lines -- need to work with a lot of transactions in a switch statement */
/* eslint-disable max-lines-per-function -- need to work with a lot of Tx verifications */

import { XrplDefinitionsBase } from 'ripple-binary-codec'

import { ValidationError } from '../../errors'
import { convertTxFlagsToNumber } from '../utils/flags'

Expand Down Expand Up @@ -252,14 +254,18 @@ export interface TransactionAndMetadata<
* Encode/decode and individual type validation.
*
* @param transaction - A Transaction.
* @param customDefinitions - Optional parameter to validate against a custom definition.
* @throws ValidationError When the Transaction is malformed.
* @category Utilities
*/
export function validate(transaction: Record<string, unknown>): void {
export function validate(
transaction: Record<string, unknown>,
customDefinitions?: XrplDefinitionsBase,
): void {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const tx = { ...transaction }

// should already be done in the tx-specific validation, but doesn't hurt to check again
validateBaseTransaction(tx)
validateBaseTransaction(tx, customDefinitions)

Object.keys(tx).forEach((key) => {
const standard_currency_code_len = 3
Expand Down Expand Up @@ -325,8 +331,11 @@ export function validate(transaction: Record<string, unknown>): void {
// @ts-expect-error -- already checked
// eslint-disable-next-line @typescript-eslint/no-unsafe-call -- already checked above
tx.RawTransactions.forEach((innerTx: Record<string, unknown>) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- already checked above
validate(innerTx.RawTransaction as Record<string, unknown>)
validate(
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- already checked above
innerTx.RawTransaction as Record<string, unknown>,
customDefinitions,
)
})
break

Expand Down Expand Up @@ -575,8 +584,10 @@ export function validate(transaction: Record<string, unknown>): void {
break

default:
throw new ValidationError(
`Invalid field TransactionType: ${tx.TransactionType}`,
)
if (!customDefinitions?.transactionNames.includes(tx.TransactionType)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Custom transaction types skip all field-level validation — only validateBaseTransaction runs. Document this limitation and consider a hook for registering field validators for custom types.

throw new ValidationError(
`Invalid field TransactionType: ${tx.TransactionType}`,
)
}
}
}
Loading
Loading