Skip to content

Commit 32a128f

Browse files
authored
fix: improve Batch inner transaction typing (#3035)
1 parent 201e126 commit 32a128f

File tree

10 files changed

+104
-92
lines changed

10 files changed

+104
-92
lines changed

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v18
1+
v22

packages/xrpl/HISTORY.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr
2525
* Fail faster on `tem` errors with `submitAndWait`
2626
* Improved type-checking in models
2727
* Fix issue with some transactions that would crash in validation
28+
* Improve typing of `Batch` inner transactions
2829

29-
## 4.3.0 (2025-6-09)
30+
## 4.3.0 (2025-06-09)
3031

3132
### Added
3233
* Support for `NFTokenMintOffer` (XLS-52)

packages/xrpl/src/models/transactions/batch.ts

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import {
77
GlobalFlags,
88
GlobalFlagsInterface,
99
isArray,
10+
isNull,
1011
isRecord,
1112
isString,
13+
isValue,
1214
validateBaseTransaction,
1315
validateOptionalField,
1416
validateRequiredField,
@@ -40,18 +42,6 @@ export interface BatchFlagsInterface extends GlobalFlagsInterface {
4042
tfIndependent?: boolean
4143
}
4244

43-
export type BatchInnerTransaction = SubmittableTransaction & {
44-
Fee?: '0'
45-
46-
SigningPubKey?: ''
47-
48-
TxnSignature?: never
49-
50-
Signers?: never
51-
52-
LastLedgerSequence?: never
53-
}
54-
5545
export interface BatchSigner {
5646
BatchSigner: {
5747
Account: string
@@ -73,10 +63,48 @@ export interface Batch extends BaseTransaction {
7363
BatchSigners?: BatchSigner[]
7464

7565
RawTransactions: Array<{
76-
RawTransaction: BatchInnerTransaction
66+
RawTransaction: SubmittableTransaction
7767
}>
7868
}
7969

70+
function validateBatchInnerTransaction(
71+
tx: Record<string, unknown>,
72+
index: number,
73+
): void {
74+
if (tx.TransactionType === 'Batch') {
75+
throw new ValidationError(
76+
`Batch: RawTransactions[${index}] is a Batch transaction. Cannot nest Batch transactions.`,
77+
)
78+
}
79+
80+
// Check for the `tfInnerBatchTxn` flag in the inner transactions
81+
if (!hasFlag(tx, GlobalFlags.tfInnerBatchTxn, 'tfInnerBatchTxn')) {
82+
throw new ValidationError(
83+
`Batch: RawTransactions[${index}] must contain the \`tfInnerBatchTxn\` flag.`,
84+
)
85+
}
86+
validateOptionalField(tx, 'Fee', isValue('0'), {
87+
paramName: `RawTransactions[${index}].RawTransaction.Fee`,
88+
txType: 'Batch',
89+
})
90+
validateOptionalField(tx, 'SigningPubKey', isValue(''), {
91+
paramName: `RawTransactions[${index}].RawTransaction.SigningPubKey`,
92+
txType: 'Batch',
93+
})
94+
validateOptionalField(tx, 'TxnSignature', isNull, {
95+
paramName: `RawTransactions[${index}].RawTransaction.TxnSignature`,
96+
txType: 'Batch',
97+
})
98+
validateOptionalField(tx, 'Signers', isNull, {
99+
paramName: `RawTransactions[${index}].RawTransaction.Signers`,
100+
txType: 'Batch',
101+
})
102+
validateOptionalField(tx, 'LastLedgerSequence', isNull, {
103+
paramName: `RawTransactions[${index}].RawTransaction.LastLedgerSequence`,
104+
txType: 'Batch',
105+
})
106+
}
107+
80108
/**
81109
* Verify the form and type of a Batch at runtime.
82110
*
@@ -101,18 +129,7 @@ export function validateBatch(tx: Record<string, unknown>): void {
101129
})
102130

103131
const rawTx = rawTxObj.RawTransaction
104-
if (rawTx.TransactionType === 'Batch') {
105-
throw new ValidationError(
106-
`Batch: RawTransactions[${index}] is a Batch transaction. Cannot nest Batch transactions.`,
107-
)
108-
}
109-
110-
// Check for the `tfInnerBatchTxn` flag in the inner transactions
111-
if (!hasFlag(rawTx, GlobalFlags.tfInnerBatchTxn, 'tfInnerBatchTxn')) {
112-
throw new ValidationError(
113-
`Batch: RawTransactions[${index}] must contain the \`tfInnerBatchTxn\` flag.`,
114-
)
115-
}
132+
validateBatchInnerTransaction(rawTx, index)
116133

117134
// Full validation of each `RawTransaction` object is done in `validate` to avoid dependency cycles
118135
})
@@ -132,19 +149,19 @@ export function validateBatch(tx: Record<string, unknown>): void {
132149

133150
const signer = signerRecord.BatchSigner
134151
validateRequiredField(signer, 'Account', isString, {
135-
paramName: `BatchSigners[${index}].Account`,
152+
paramName: `BatchSigners[${index}].BatchSigner.Account`,
136153
txType: 'Batch',
137154
})
138155
validateOptionalField(signer, 'SigningPubKey', isString, {
139-
paramName: `BatchSigners[${index}].SigningPubKey`,
156+
paramName: `BatchSigners[${index}].BatchSigner.SigningPubKey`,
140157
txType: 'Batch',
141158
})
142159
validateOptionalField(signer, 'TxnSignature', isString, {
143-
paramName: `BatchSigners[${index}].TxnSignature`,
160+
paramName: `BatchSigners[${index}].BatchSigner.TxnSignature`,
144161
txType: 'Batch',
145162
})
146163
validateOptionalField(signer, 'Signers', isArray, {
147-
paramName: `BatchSigners[${index}].Signers`,
164+
paramName: `BatchSigners[${index}].BatchSigner.Signers`,
148165
txType: 'Batch',
149166
})
150167
})

packages/xrpl/src/models/transactions/common.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,28 @@ export function isNumber(num: unknown): num is number {
152152
return typeof num === 'number'
153153
}
154154

155+
/**
156+
* Verify the form and type of a null value at runtime.
157+
*
158+
* @param inp - The value to check the form and type of.
159+
* @returns Whether the value is properly formed.
160+
*/
161+
export function isNull(inp: unknown): inp is null {
162+
return inp == null
163+
}
164+
165+
/**
166+
* Verify that a certain field has a certain exact value at runtime.
167+
*
168+
* @param value The value to compare against.
169+
* @returns Whether the number is properly formed and within the bounds.
170+
*/
171+
export function isValue<V>(value: V): (inp: unknown) => inp is V {
172+
// eslint-disable-next-line func-style -- returning a function
173+
const isValueInternal = (inp: unknown): inp is V => inp === value
174+
return isValueInternal
175+
}
176+
155177
/**
156178
* Checks whether the given value is a valid XRPL number string.
157179
* Accepts integer, decimal, or scientific notation strings.

packages/xrpl/src/models/transactions/delegateSet.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
} from './common'
1010

1111
const PERMISSIONS_MAX_LENGTH = 10
12-
const NON_DELEGATABLE_TRANSACTIONS = new Set([
12+
const NON_DELEGABLE_TRANSACTIONS = new Set([
1313
'AccountSet',
1414
'SetRegularKey',
1515
'SignerListSet',
@@ -97,7 +97,7 @@ export function validateDelegateSet(tx: Record<string, unknown>): void {
9797
if (typeof permissionValue !== 'string') {
9898
throw new ValidationError('DelegateSet: PermissionValue must be a string')
9999
}
100-
if (NON_DELEGATABLE_TRANSACTIONS.has(permissionValue)) {
100+
if (NON_DELEGABLE_TRANSACTIONS.has(permissionValue)) {
101101
throw new ValidationError(
102102
`DelegateSet: PermissionValue contains a non-delegatable transaction ${permissionValue}`,
103103
)

packages/xrpl/src/sugar/autofill.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -458,28 +458,24 @@ export async function autofillBatchTxn(
458458

459459
if (txn.Fee == null) {
460460
txn.Fee = '0'
461-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- needed for JS checks
462461
} else if (txn.Fee !== '0') {
463462
throw new XrplError('Must have `Fee of "0" in inner Batch transaction.')
464463
}
465464

466465
if (txn.SigningPubKey == null) {
467466
txn.SigningPubKey = ''
468-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- needed for JS checks
469467
} else if (txn.SigningPubKey !== '') {
470468
throw new XrplError(
471469
'Must have `SigningPubKey` of "" in inner Batch transaction.',
472470
)
473471
}
474472

475-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- needed for JS checks
476473
if (txn.TxnSignature != null) {
477474
throw new XrplError(
478475
'Must not have `TxnSignature` in inner Batch transaction.',
479476
)
480477
}
481478

482-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- needed for JS checks
483479
if (txn.Signers != null) {
484480
throw new XrplError('Must not have `Signers` in inner Batch transaction.')
485481
}

packages/xrpl/test/integration/transactions/batch.test.ts

Lines changed: 24 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Batch, Wallet } from '../../../src'
1+
import { Batch, Payment, Wallet } from '../../../src'
22
import { BatchFlags } from '../../../src/models/transactions/batch'
33
import { signMultiBatch } from '../../../src/Wallet/batchSigner'
44
import serverUrl from '../serverUrl'
@@ -49,30 +49,22 @@ describe('Batch', function () {
4949
it(
5050
'base',
5151
async () => {
52+
const payment: Payment = {
53+
TransactionType: 'Payment',
54+
Flags: 0x40000000,
55+
Account: testContext.wallet.classicAddress,
56+
Destination: destination.classicAddress,
57+
Amount: '10000000',
58+
}
5259
const tx: Batch = {
5360
TransactionType: 'Batch',
5461
Account: testContext.wallet.classicAddress,
5562
Flags: BatchFlags.tfAllOrNothing,
56-
RawTransactions: [
57-
{
58-
RawTransaction: {
59-
TransactionType: 'Payment',
60-
Flags: 0x40000000,
61-
Account: testContext.wallet.classicAddress,
62-
Destination: destination.classicAddress,
63-
Amount: '10000000',
64-
},
65-
},
66-
{
67-
RawTransaction: {
68-
TransactionType: 'Payment',
69-
Flags: 0x40000000,
70-
Account: testContext.wallet.classicAddress,
71-
Destination: destination.classicAddress,
72-
Amount: '10000000',
73-
},
74-
},
75-
],
63+
RawTransactions: [payment, { ...payment }, { ...payment }].map(
64+
(rawTx) => ({
65+
RawTransaction: rawTx,
66+
}),
67+
),
7668
}
7769
const autofilled = await testContext.client.autofill(tx)
7870
await testBatchTransaction(autofilled, testContext.wallet)
@@ -83,30 +75,21 @@ describe('Batch', function () {
8375
it(
8476
'batch multisign',
8577
async () => {
78+
const payment: Payment = {
79+
TransactionType: 'Payment',
80+
Flags: 0x40000000,
81+
Account: testContext.wallet.classicAddress,
82+
Destination: destination.classicAddress,
83+
Amount: '10000000',
84+
}
85+
const payment2: Payment = { ...payment, Account: wallet2.classicAddress }
8686
const tx: Batch = {
8787
TransactionType: 'Batch',
8888
Account: testContext.wallet.classicAddress,
8989
Flags: BatchFlags.tfAllOrNothing,
90-
RawTransactions: [
91-
{
92-
RawTransaction: {
93-
TransactionType: 'Payment',
94-
Flags: 0x40000000,
95-
Account: testContext.wallet.classicAddress,
96-
Destination: destination.classicAddress,
97-
Amount: '10000000',
98-
},
99-
},
100-
{
101-
RawTransaction: {
102-
TransactionType: 'Payment',
103-
Flags: 0x40000000,
104-
Account: wallet2.classicAddress,
105-
Destination: destination.classicAddress,
106-
Amount: '10000000',
107-
},
108-
},
109-
],
90+
RawTransactions: [payment, payment2].map((rawTx) => ({
91+
RawTransaction: rawTx,
92+
})),
11093
}
11194
const autofilled = await testContext.client.autofill(tx, 1)
11295
signMultiBatch(wallet2, autofilled)

packages/xrpl/test/models/batch.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ describe('Batch', function () {
5050
Fee: '0',
5151
Flags: 0x40000000,
5252
NetworkID: 21336,
53-
Sequence: 0,
53+
Sequence: 1,
5454
SigningPubKey: '',
5555
TransactionType: 'Payment',
5656
},
@@ -90,7 +90,7 @@ describe('Batch', function () {
9090
Fee: '0',
9191
Flags: 0x40000000,
9292
NetworkID: 21336,
93-
Sequence: 0,
93+
Sequence: 1,
9494
SigningPubKey: '',
9595
TransactionType: 'Payment',
9696
},

packages/xrpl/test/utils/hashes.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import { encode } from 'ripple-binary-codec'
77
import {
88
EnableAmendment,
99
OfferCreate,
10+
Payment,
1011
Transaction,
1112
ValidationError,
1213
} from '../../src'
13-
import { BatchInnerTransaction } from '../../src/models/transactions/batch'
1414
import {
1515
hashStateTree,
1616
hashTxTree,
@@ -214,7 +214,7 @@ describe('Hashes', function () {
214214
})
215215

216216
it('hashSignedTx - batch transaction', function () {
217-
const transaction: BatchInnerTransaction = {
217+
const transaction: Payment = {
218218
Account: 'rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK',
219219
Amount: '1000000',
220220
Destination: 'rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp',

packages/xrpl/test/wallet/batchSigner.test.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,11 @@ import {
55
decode,
66
ECDSA,
77
encode,
8+
SubmittableTransaction,
89
ValidationError,
910
Wallet,
1011
} from '../../src'
11-
import {
12-
BatchFlags,
13-
BatchInnerTransaction,
14-
BatchSigner,
15-
} from '../../src/models/transactions/batch'
12+
import { BatchFlags, BatchSigner } from '../../src/models/transactions/batch'
1613
import {
1714
combineBatchSigners,
1815
signMultiBatch,
@@ -46,10 +43,6 @@ const nonBatchTx = {
4643
Amount: '1000',
4744
}
4845

49-
interface RawTransaction {
50-
RawTransaction: BatchInnerTransaction
51-
}
52-
5346
describe('Wallet batch operations', function () {
5447
describe('signMultiBatch', function () {
5548
let transaction: Batch
@@ -291,7 +284,7 @@ describe('Wallet batch operations', function () {
291284

292285
it('removes signer for Batch submitter', function () {
293286
// add a third inner transaction from the transaction submitter
294-
const rawTx3: RawTransaction = {
287+
const rawTx3: { RawTransaction: SubmittableTransaction } = {
295288
RawTransaction: {
296289
Account: 'rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp',
297290
Amount: '1000000',

0 commit comments

Comments
 (0)