Skip to content

Commit 2687d1e

Browse files
committed
fix: widen transactions type to include hash strings, normalize v1 in hashTxTree
- Export LedgerTransactionExpanded and LedgerTransactionExpandedV1 types from Ledger.ts - Move transactions field from BaseLedger to Ledger/LedgerV1 with version-specific types - Normalize v1 wrapped transactions in hashTxTree for cross-version compatibility - Add tests for LedgerTransactionExpanded type handling - Fix lint errors (prettier, max-lines-per-function, type assertions)
1 parent ea2e4e7 commit 2687d1e

File tree

7 files changed

+183
-39
lines changed

7 files changed

+183
-39
lines changed

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

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,19 +54,30 @@ interface BaseLedger {
5454
total_coins: string
5555
/** Hash of the transaction information included in this ledger, as hex. */
5656
transaction_hash: string
57-
/**
58-
* Transactions applied in this ledger version. By default, members are the
59-
* transactions' identifying Hash strings. If the request specified expand as
60-
* true, members are full representations of the transactions instead, in
61-
* either JSON or binary depending on whether the request specified binary
62-
* as true.
63-
*/
64-
transactions?: Array<
65-
Transaction & {
66-
hash: string
67-
metaData?: TransactionMetadata
68-
}
69-
>
57+
}
58+
59+
/**
60+
* Expanded transaction format in API version 2.
61+
* Transactions are returned as flat objects with the transaction fields
62+
* directly on the object, plus `hash` and `metaData`.
63+
*/
64+
export type LedgerTransactionExpanded = Transaction & {
65+
hash: string
66+
metaData?: TransactionMetadata
67+
}
68+
69+
/**
70+
* Expanded transaction format in API version 1.
71+
* Transactions are wrapped in an object with `tx_json` and `meta` fields.
72+
*/
73+
export interface LedgerTransactionExpandedV1 {
74+
tx_json: Transaction
75+
meta: TransactionMetadata
76+
hash: string
77+
validated: boolean
78+
ledger_index: number
79+
close_time_iso: string
80+
ledger_hash: string
7081
}
7182

7283
/**
@@ -80,6 +91,12 @@ export interface Ledger extends BaseLedger {
8091
* The ledger index of the ledger. Represented as a number.
8192
*/
8293
ledger_index: number
94+
/**
95+
* Transactions applied in this ledger version. When expanded, members are
96+
* full representations of the transactions as flat objects with the
97+
* transaction fields directly on the object, plus `hash` and `metaData`.
98+
*/
99+
transactions?: Array<string | LedgerTransactionExpanded>
83100
}
84101

85102
/**
@@ -95,6 +112,12 @@ export interface LedgerV1 extends BaseLedger {
95112
* integer; some display it as a number.
96113
*/
97114
ledger_index: string
115+
/**
116+
* Transactions applied in this ledger version. When expanded, members are
117+
* full representations of the transactions wrapped in objects with
118+
* `tx_json` and `meta` fields.
119+
*/
120+
transactions?: Array<string | LedgerTransactionExpandedV1>
98121
}
99122

100123
/**

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ import FeeSettings, {
1717
FeeSettingsPostAmendmentFields,
1818
FEE_SETTINGS_ID,
1919
} from './FeeSettings'
20-
import { Ledger, LedgerV1 } from './Ledger'
20+
import {
21+
Ledger,
22+
LedgerV1,
23+
LedgerTransactionExpanded,
24+
LedgerTransactionExpandedV1,
25+
} from './Ledger'
2126
import { LedgerEntry, LedgerEntryFilter } from './LedgerEntry'
2227
import LedgerHashes from './LedgerHashes'
2328
import Loan, { LoanFlags } from './Loan'
@@ -58,6 +63,8 @@ export {
5863
FeeSettingsPostAmendmentFields,
5964
Ledger,
6065
LedgerV1,
66+
LedgerTransactionExpanded,
67+
LedgerTransactionExpandedV1,
6168
LedgerEntryFilter,
6269
LedgerEntry,
6370
LedgerHashes,

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

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,8 @@ export interface LedgerRequestExpandedAccountsOnly extends LedgerRequest {
144144
*
145145
* @category Requests
146146
*/
147-
// eslint-disable-next-line max-len -- Disable for interface declaration.
148-
export interface LedgerRequestExpandedAccountsAndTransactions extends LedgerRequest {
147+
export interface LedgerRequestExpandedAccountsAndTransactions
148+
extends LedgerRequest {
149149
expand: true
150150
accounts: true
151151
transactions: true
@@ -202,18 +202,14 @@ export interface LedgerQueueData {
202202
max_spend_drops?: string
203203
}
204204

205-
export interface LedgerBinary extends Omit<
206-
Ledger,
207-
'transactions' | 'accountState'
208-
> {
205+
export interface LedgerBinary
206+
extends Omit<Ledger, 'transactions' | 'accountState'> {
209207
accountState?: string[]
210208
transactions?: string[]
211209
}
212210

213-
export interface LedgerBinaryV1 extends Omit<
214-
LedgerV1,
215-
'transactions' | 'accountState'
216-
> {
211+
export interface LedgerBinaryV1
212+
extends Omit<LedgerV1, 'transactions' | 'accountState'> {
217213
accountState?: string[]
218214
transactions?: string[]
219215
}

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,8 @@ export interface SimulateBinaryResponse extends BaseResponse {
6464
}
6565
}
6666

67-
export interface SimulateJsonResponse<
68-
T extends BaseTransaction = Transaction,
69-
> extends BaseResponse {
67+
export interface SimulateJsonResponse<T extends BaseTransaction = Transaction>
68+
extends BaseResponse {
7069
result: {
7170
applied: false
7271

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,8 @@ interface BaseTxResult<
9393
*
9494
* @category Responses
9595
*/
96-
export interface TxResponse<
97-
T extends BaseTransaction = Transaction,
98-
> extends BaseResponse {
96+
export interface TxResponse<T extends BaseTransaction = Transaction>
97+
extends BaseResponse {
9998
result: BaseTxResult<typeof RIPPLED_API_V2, T> & { tx_json: T }
10099
/**
101100
* If true, the server was able to search all of the specified ledger
@@ -111,9 +110,8 @@ export interface TxResponse<
111110
*
112111
* @category ResponsesV1
113112
*/
114-
export interface TxV1Response<
115-
T extends BaseTransaction = Transaction,
116-
> extends BaseResponse {
113+
export interface TxV1Response<T extends BaseTransaction = Transaction>
114+
extends BaseResponse {
117115
result: BaseTxResult<typeof RIPPLED_API_V1, T> & T
118116
/**
119117
* If true, the server was able to search all of the specified ledger

packages/xrpl/src/utils/hashes/hashLedger.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@ import { decode, encode } from 'ripple-binary-codec'
1010
import { ValidationError, XrplError } from '../../errors'
1111
import { APIVersion } from '../../models'
1212
import { LedgerEntry } from '../../models/ledger'
13-
import { LedgerVersionMap } from '../../models/ledger/Ledger'
14-
import { Transaction, TransactionMetadata } from '../../models/transactions'
13+
import {
14+
LedgerVersionMap,
15+
LedgerTransactionExpanded,
16+
LedgerTransactionExpandedV1,
17+
} from '../../models/ledger/Ledger'
18+
import { Transaction } from '../../models/transactions'
1519
import { GlobalFlags } from '../../models/transactions/common'
1620
import { hasFlag } from '../../models/utils'
1721

@@ -130,9 +134,7 @@ export function hashLedgerHeader(
130134
* @returns The root hash of the SHAMap.
131135
* @category Utilities
132136
*/
133-
export function hashTxTree(
134-
transactions: Array<Transaction & { metaData?: TransactionMetadata }>,
135-
): string {
137+
export function hashTxTree(transactions: LedgerTransactionExpanded[]): string {
136138
const shamap = new SHAMap()
137139
for (const txJSON of transactions) {
138140
const txBlobHex = encode(txJSON)
@@ -163,6 +165,18 @@ export function hashStateTree(entries: LedgerEntry[]): string {
163165
return shamap.hash
164166
}
165167

168+
/**
169+
* Normalize a v1 wrapped transaction to v2 flat format.
170+
*/
171+
function normalizeToV2(
172+
tx: LedgerTransactionExpanded | LedgerTransactionExpandedV1,
173+
): LedgerTransactionExpanded {
174+
if ('tx_json' in tx) {
175+
return { ...tx.tx_json, hash: tx.hash, metaData: tx.meta }
176+
}
177+
return tx
178+
}
179+
166180
function computeTransactionHash(
167181
ledger: LedgerVersionMap<APIVersion>,
168182
options: HashLedgerHeaderOptions,
@@ -177,7 +191,19 @@ function computeTransactionHash(
177191
throw new ValidationError('transactions is missing from the ledger')
178192
}
179193

180-
const transactionHash = hashTxTree(ledger.transactions)
194+
// Normalize transactions to the v2 flat format expected by hashTxTree.
195+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- ledger is a version union
196+
const txs = ledger.transactions as Array<
197+
string | LedgerTransactionExpanded | LedgerTransactionExpandedV1
198+
>
199+
const normalizedTransactions = txs
200+
.filter(
201+
(tx): tx is LedgerTransactionExpanded | LedgerTransactionExpandedV1 =>
202+
typeof tx !== 'string',
203+
)
204+
.map(normalizeToV2)
205+
206+
const transactionHash = hashTxTree(normalizedTransactions)
181207

182208
if (transaction_hash !== transactionHash) {
183209
throw new ValidationError(
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { assert } from 'chai'
2+
3+
import {
4+
Ledger,
5+
LedgerV1,
6+
LedgerTransactionExpanded,
7+
LedgerTransactionExpandedV1,
8+
} from '../../src/models/ledger/Ledger'
9+
10+
describe('LedgerTransactionExpanded types', function () {
11+
it('Ledger (v2) transactions use flat format with metaData', function () {
12+
const tx: LedgerTransactionExpanded = {
13+
Account: 'rPrioTXJgZJF8bpdXq2X73PcVPfvYqjVKd',
14+
Amount: '1000000',
15+
Destination: 'rsRy14FvipgqudiGmptJBhr1RtpsgfzKMM',
16+
TransactionType: 'Payment',
17+
Fee: '11',
18+
Sequence: 1,
19+
Flags: 0,
20+
SigningPubKey: '',
21+
TxnSignature: '',
22+
hash: '044314FE34236A262DA692789CE5B48CA1A3CEC078B1A4ECCD65F4B61A9EB0A7',
23+
metaData: {
24+
AffectedNodes: [],
25+
TransactionIndex: 0,
26+
TransactionResult: 'tesSUCCESS',
27+
},
28+
}
29+
30+
// Verify v2 expanded transactions are assignable to Ledger.transactions
31+
const ledgerV2: Pick<Ledger, 'transactions'> = {
32+
transactions: [tx],
33+
}
34+
35+
assert.isArray(ledgerV2.transactions)
36+
const firstTx = ledgerV2.transactions![0]
37+
assert.notEqual(typeof firstTx, 'string')
38+
if (typeof firstTx !== 'string') {
39+
assert.strictEqual(firstTx.hash, tx.hash)
40+
assert.isDefined(firstTx.metaData)
41+
}
42+
})
43+
44+
it('Ledger transactions can also be hash strings', function () {
45+
const ledgerV2: Pick<Ledger, 'transactions'> = {
46+
transactions: [
47+
'044314FE34236A262DA692789CE5B48CA1A3CEC078B1A4ECCD65F4B61A9EB0A7',
48+
],
49+
}
50+
51+
assert.isArray(ledgerV2.transactions)
52+
assert.strictEqual(typeof ledgerV2.transactions![0], 'string')
53+
})
54+
55+
it('LedgerV1 transactions use wrapped format with tx_json and meta', function () {
56+
const tx: LedgerTransactionExpandedV1 = {
57+
tx_json: {
58+
Account: 'rPrioTXJgZJF8bpdXq2X73PcVPfvYqjVKd',
59+
Amount: '1000000',
60+
Destination: 'rsRy14FvipgqudiGmptJBhr1RtpsgfzKMM',
61+
TransactionType: 'Payment',
62+
Fee: '11',
63+
Sequence: 1,
64+
Flags: 0,
65+
SigningPubKey: '',
66+
TxnSignature: '',
67+
},
68+
meta: {
69+
AffectedNodes: [],
70+
TransactionIndex: 0,
71+
TransactionResult: 'tesSUCCESS',
72+
},
73+
hash: '044314FE34236A262DA692789CE5B48CA1A3CEC078B1A4ECCD65F4B61A9EB0A7',
74+
validated: true,
75+
ledger_index: 93637993,
76+
close_time_iso: '2025-01-22T21:13:50Z',
77+
ledger_hash:
78+
'058FDA696458896EC515AC19C3EDC8CD0E163A3620CA6B314165E5BAED70846A',
79+
}
80+
81+
// Verify v1 expanded transactions are assignable to LedgerV1.transactions
82+
const ledgerV1: Pick<LedgerV1, 'transactions'> = {
83+
transactions: [tx],
84+
}
85+
86+
assert.isArray(ledgerV1.transactions)
87+
const firstTx = ledgerV1.transactions![0]
88+
assert.notEqual(typeof firstTx, 'string')
89+
if (typeof firstTx !== 'string') {
90+
assert.strictEqual(firstTx.hash, tx.hash)
91+
assert.isDefined(firstTx.tx_json)
92+
assert.isDefined(firstTx.meta)
93+
}
94+
})
95+
})

0 commit comments

Comments
 (0)