@ledgerhq/coin-xrp 7.5.1 → 7.6.0-nightly.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +25 -0
- package/lib/api/index.d.ts.map +1 -1
- package/lib/api/index.integ.test.js +8 -0
- package/lib/api/index.integ.test.js.map +1 -1
- package/lib/api/index.js +5 -0
- package/lib/api/index.js.map +1 -1
- package/lib/api/index.test.js +10 -2
- package/lib/api/index.test.js.map +1 -1
- package/lib/logic/combine.d.ts.map +1 -1
- package/lib/logic/combine.js +16 -4
- package/lib/logic/combine.js.map +1 -1
- package/lib/logic/craftRawTransaction.d.ts +3 -0
- package/lib/logic/craftRawTransaction.d.ts.map +1 -0
- package/lib/logic/craftRawTransaction.js +70 -0
- package/lib/logic/craftRawTransaction.js.map +1 -0
- package/lib/logic/craftRawTransaction.test.d.ts +2 -0
- package/lib/logic/craftRawTransaction.test.d.ts.map +1 -0
- package/lib/logic/craftRawTransaction.test.js +304 -0
- package/lib/logic/craftRawTransaction.test.js.map +1 -0
- package/lib/logic/index.d.ts +1 -0
- package/lib/logic/index.d.ts.map +1 -1
- package/lib/logic/index.js +3 -1
- package/lib/logic/index.js.map +1 -1
- package/lib/logic/utils.d.ts +6 -0
- package/lib/logic/utils.d.ts.map +1 -1
- package/lib/logic/utils.js +18 -0
- package/lib/logic/utils.js.map +1 -1
- package/lib/logic/utils.test.js +62 -0
- package/lib/logic/utils.test.js.map +1 -1
- package/lib/logic/validateIntent.test.js +8 -0
- package/lib/logic/validateIntent.test.js.map +1 -1
- package/lib/types/model.d.ts +7 -0
- package/lib/types/model.d.ts.map +1 -1
- package/lib-es/api/index.d.ts.map +1 -1
- package/lib-es/api/index.integ.test.js +8 -0
- package/lib-es/api/index.integ.test.js.map +1 -1
- package/lib-es/api/index.js +6 -1
- package/lib-es/api/index.js.map +1 -1
- package/lib-es/api/index.test.js +10 -2
- package/lib-es/api/index.test.js.map +1 -1
- package/lib-es/logic/combine.d.ts.map +1 -1
- package/lib-es/logic/combine.js +16 -4
- package/lib-es/logic/combine.js.map +1 -1
- package/lib-es/logic/craftRawTransaction.d.ts +3 -0
- package/lib-es/logic/craftRawTransaction.d.ts.map +1 -0
- package/lib-es/logic/craftRawTransaction.js +67 -0
- package/lib-es/logic/craftRawTransaction.js.map +1 -0
- package/lib-es/logic/craftRawTransaction.test.d.ts +2 -0
- package/lib-es/logic/craftRawTransaction.test.d.ts.map +1 -0
- package/lib-es/logic/craftRawTransaction.test.js +302 -0
- package/lib-es/logic/craftRawTransaction.test.js.map +1 -0
- package/lib-es/logic/index.d.ts +1 -0
- package/lib-es/logic/index.d.ts.map +1 -1
- package/lib-es/logic/index.js +1 -0
- package/lib-es/logic/index.js.map +1 -1
- package/lib-es/logic/utils.d.ts +6 -0
- package/lib-es/logic/utils.d.ts.map +1 -1
- package/lib-es/logic/utils.js +18 -1
- package/lib-es/logic/utils.js.map +1 -1
- package/lib-es/logic/utils.test.js +63 -1
- package/lib-es/logic/utils.test.js.map +1 -1
- package/lib-es/logic/validateIntent.test.js +8 -0
- package/lib-es/logic/validateIntent.test.js.map +1 -1
- package/lib-es/types/model.d.ts +7 -0
- package/lib-es/types/model.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/api/index.integ.test.ts +8 -0
- package/src/api/index.test.ts +21 -10
- package/src/api/index.ts +6 -0
- package/src/logic/combine.ts +26 -6
- package/src/logic/craftRawTransaction.test.ts +362 -0
- package/src/logic/craftRawTransaction.ts +91 -0
- package/src/logic/index.ts +1 -0
- package/src/logic/utils.test.ts +71 -1
- package/src/logic/utils.ts +20 -1
- package/src/logic/validateIntent.test.ts +8 -0
- package/src/types/model.ts +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ledgerhq/coin-xrp",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.6.0-nightly.1",
|
|
4
4
|
"description": "Ledger XRP Coin integration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Ledger",
|
|
@@ -103,12 +103,12 @@
|
|
|
103
103
|
"invariant": "^2.2.4",
|
|
104
104
|
"ripple-address-codec": "^5.0.0",
|
|
105
105
|
"ripple-binary-codec": "^1.3.0",
|
|
106
|
-
"@ledgerhq/coin-framework": "^6.
|
|
107
|
-
"@ledgerhq/cryptoassets": "^13.
|
|
108
|
-
"@ledgerhq/devices": "8.6.1",
|
|
106
|
+
"@ledgerhq/coin-framework": "^6.7.0-nightly.1",
|
|
107
|
+
"@ledgerhq/cryptoassets": "^13.31.0-nightly.1",
|
|
109
108
|
"@ledgerhq/errors": "^6.26.0",
|
|
110
|
-
"@ledgerhq/
|
|
111
|
-
"@ledgerhq/
|
|
109
|
+
"@ledgerhq/devices": "8.6.1",
|
|
110
|
+
"@ledgerhq/live-network": "^2.0.20-nightly.0",
|
|
111
|
+
"@ledgerhq/types-live": "^6.87.0-nightly.1",
|
|
112
112
|
"@ledgerhq/logs": "^6.13.0"
|
|
113
113
|
},
|
|
114
114
|
"devDependencies": {
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
"ts-jest": "^29.1.1",
|
|
123
123
|
"typescript": "^5.4.5",
|
|
124
124
|
"@ledgerhq/disable-network-setup": "^0.0.0",
|
|
125
|
-
"@ledgerhq/types-cryptoassets": "^7.
|
|
125
|
+
"@ledgerhq/types-cryptoassets": "^7.29.0-nightly.0"
|
|
126
126
|
},
|
|
127
127
|
"scripts": {
|
|
128
128
|
"clean": "rimraf lib lib-es",
|
|
@@ -15,6 +15,7 @@ describe("Xrp Api (testnet)", () => {
|
|
|
15
15
|
|
|
16
16
|
// When
|
|
17
17
|
const result = await api.estimateFees({
|
|
18
|
+
intentType: "transaction",
|
|
18
19
|
asset: { type: "native" },
|
|
19
20
|
type: "send",
|
|
20
21
|
sender: SENDER,
|
|
@@ -125,6 +126,7 @@ describe("Xrp Api (testnet)", () => {
|
|
|
125
126
|
it("returns a raw transaction", async () => {
|
|
126
127
|
// When
|
|
127
128
|
const { transaction: result } = await api.craftTransaction({
|
|
129
|
+
intentType: "transaction",
|
|
128
130
|
asset: { type: "native" },
|
|
129
131
|
type: "send",
|
|
130
132
|
sender: SENDER,
|
|
@@ -141,6 +143,7 @@ describe("Xrp Api (testnet)", () => {
|
|
|
141
143
|
|
|
142
144
|
it("should use default fees when user does not provide them for crafting a transaction", async () => {
|
|
143
145
|
const { transaction: result } = await api.craftTransaction({
|
|
146
|
+
intentType: "transaction",
|
|
144
147
|
asset: { type: "native" },
|
|
145
148
|
type: "send",
|
|
146
149
|
sender: SENDER,
|
|
@@ -161,6 +164,7 @@ describe("Xrp Api (testnet)", () => {
|
|
|
161
164
|
const customFees = 99n;
|
|
162
165
|
const { transaction: result } = await api.craftTransaction(
|
|
163
166
|
{
|
|
167
|
+
intentType: "transaction",
|
|
164
168
|
asset: { type: "native" },
|
|
165
169
|
type: "send",
|
|
166
170
|
sender: SENDER,
|
|
@@ -192,6 +196,7 @@ describe("Xrp Api (mainnet)", () => {
|
|
|
192
196
|
|
|
193
197
|
// When
|
|
194
198
|
const result = await api.estimateFees({
|
|
199
|
+
intentType: "transaction",
|
|
195
200
|
asset: { type: "native" },
|
|
196
201
|
type: "send",
|
|
197
202
|
sender: SENDER,
|
|
@@ -289,6 +294,7 @@ describe("Xrp Api (mainnet)", () => {
|
|
|
289
294
|
|
|
290
295
|
it("returns a raw transaction", async () => {
|
|
291
296
|
const { transaction: result } = await api.craftTransaction({
|
|
297
|
+
intentType: "transaction",
|
|
292
298
|
asset: { type: "native" },
|
|
293
299
|
type: "send",
|
|
294
300
|
sender: SENDER,
|
|
@@ -304,6 +310,7 @@ describe("Xrp Api (mainnet)", () => {
|
|
|
304
310
|
|
|
305
311
|
it("should use default fees when user does not provide them for crafting a transaction", async () => {
|
|
306
312
|
const { transaction: result } = await api.craftTransaction({
|
|
313
|
+
intentType: "transaction",
|
|
307
314
|
asset: { type: "native" },
|
|
308
315
|
type: "send",
|
|
309
316
|
sender: SENDER,
|
|
@@ -324,6 +331,7 @@ describe("Xrp Api (mainnet)", () => {
|
|
|
324
331
|
const customFees = 99n;
|
|
325
332
|
const { transaction: result } = await api.craftTransaction(
|
|
326
333
|
{
|
|
334
|
+
intentType: "transaction",
|
|
327
335
|
asset: { type: "native" },
|
|
328
336
|
type: "send",
|
|
329
337
|
sender: SENDER,
|
package/src/api/index.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Operation,
|
|
1
|
+
import { Operation, SendTransactionIntent } from "@ledgerhq/coin-framework/api/types";
|
|
2
2
|
import * as LogicFunctions from "../logic";
|
|
3
3
|
import { GetTransactionsOptions } from "../network";
|
|
4
4
|
import { NetworkInfo, XrpMapMemo } from "../types";
|
|
@@ -280,9 +280,12 @@ describe("Testing craftTransaction function", () => {
|
|
|
280
280
|
|
|
281
281
|
it("should use custom user fees when user provides it for crafting a transaction", async () => {
|
|
282
282
|
const customFees = 99n;
|
|
283
|
-
await api.craftTransaction(
|
|
284
|
-
|
|
285
|
-
|
|
283
|
+
await api.craftTransaction(
|
|
284
|
+
{ intentType: "transaction", sender: "foo" } as SendTransactionIntent<XrpMapMemo>,
|
|
285
|
+
{
|
|
286
|
+
value: customFees,
|
|
287
|
+
},
|
|
288
|
+
);
|
|
286
289
|
|
|
287
290
|
expect(logicCraftTransactionSpy).toHaveBeenCalledWith(
|
|
288
291
|
expect.any(Object),
|
|
@@ -294,7 +297,10 @@ describe("Testing craftTransaction function", () => {
|
|
|
294
297
|
});
|
|
295
298
|
|
|
296
299
|
it("should use default fees when user does not provide them for crafting a transaction", async () => {
|
|
297
|
-
await api.craftTransaction({
|
|
300
|
+
await api.craftTransaction({
|
|
301
|
+
intentType: "transaction",
|
|
302
|
+
sender: "foo",
|
|
303
|
+
} as SendTransactionIntent<XrpMapMemo>);
|
|
298
304
|
|
|
299
305
|
expect(logicCraftTransactionSpy).toHaveBeenCalledWith(
|
|
300
306
|
expect.any(Object),
|
|
@@ -307,9 +313,10 @@ describe("Testing craftTransaction function", () => {
|
|
|
307
313
|
|
|
308
314
|
it("should pass signing pub key when user provides it for crafting a transaction", async () => {
|
|
309
315
|
await api.craftTransaction({
|
|
316
|
+
intentType: "transaction",
|
|
310
317
|
sender: "foo",
|
|
311
318
|
senderPublicKey: "bar",
|
|
312
|
-
} as
|
|
319
|
+
} as SendTransactionIntent<XrpMapMemo>);
|
|
313
320
|
|
|
314
321
|
expect(logicCraftTransactionSpy).toHaveBeenCalledWith(
|
|
315
322
|
expect.any(Object),
|
|
@@ -320,12 +327,13 @@ describe("Testing craftTransaction function", () => {
|
|
|
320
327
|
|
|
321
328
|
it("should pass memos when user provides it for crafting a transaction", async () => {
|
|
322
329
|
await api.craftTransaction({
|
|
330
|
+
intentType: "transaction",
|
|
323
331
|
sender: "foo",
|
|
324
332
|
memo: {
|
|
325
333
|
type: "map",
|
|
326
334
|
memos: new Map([["memos", ["testdata"]]]),
|
|
327
335
|
},
|
|
328
|
-
} as
|
|
336
|
+
} as SendTransactionIntent<XrpMapMemo>);
|
|
329
337
|
|
|
330
338
|
expect(logicCraftTransactionSpy).toHaveBeenCalledWith(
|
|
331
339
|
expect.any(Object),
|
|
@@ -340,8 +348,9 @@ describe("Testing craftTransaction function", () => {
|
|
|
340
348
|
|
|
341
349
|
it("should not pass memos when user does not provide it for crafting a transaction", async () => {
|
|
342
350
|
await api.craftTransaction({
|
|
351
|
+
intentType: "transaction",
|
|
343
352
|
sender: "foo",
|
|
344
|
-
} as
|
|
353
|
+
} as SendTransactionIntent<XrpMapMemo>);
|
|
345
354
|
|
|
346
355
|
expect(logicCraftTransactionSpy).toHaveBeenCalledWith(
|
|
347
356
|
expect.any(Object),
|
|
@@ -354,12 +363,13 @@ describe("Testing craftTransaction function", () => {
|
|
|
354
363
|
|
|
355
364
|
it("should not pass memos when user provides an empty memo list it for crafting a transaction", async () => {
|
|
356
365
|
await api.craftTransaction({
|
|
366
|
+
intentType: "transaction",
|
|
357
367
|
sender: "foo",
|
|
358
368
|
memo: {
|
|
359
369
|
type: "map",
|
|
360
370
|
memos: new Map(),
|
|
361
371
|
},
|
|
362
|
-
} as
|
|
372
|
+
} as SendTransactionIntent<XrpMapMemo>);
|
|
363
373
|
|
|
364
374
|
expect(logicCraftTransactionSpy).toHaveBeenCalledWith(
|
|
365
375
|
expect.any(Object),
|
|
@@ -372,12 +382,13 @@ describe("Testing craftTransaction function", () => {
|
|
|
372
382
|
|
|
373
383
|
it("should pass destination tag when user provides it for crafting a transaction", async () => {
|
|
374
384
|
await api.craftTransaction({
|
|
385
|
+
intentType: "transaction",
|
|
375
386
|
sender: "foo",
|
|
376
387
|
memo: {
|
|
377
388
|
type: "map",
|
|
378
389
|
memos: new Map([["destinationTag", "1337"]]),
|
|
379
390
|
},
|
|
380
|
-
} as
|
|
391
|
+
} as SendTransactionIntent<XrpMapMemo>);
|
|
381
392
|
|
|
382
393
|
expect(logicCraftTransactionSpy).toHaveBeenCalledWith(
|
|
383
394
|
expect.any(Object),
|
package/src/api/index.ts
CHANGED
|
@@ -12,12 +12,14 @@ import {
|
|
|
12
12
|
TransactionIntent,
|
|
13
13
|
CraftedTransaction,
|
|
14
14
|
} from "@ledgerhq/coin-framework/api/index";
|
|
15
|
+
import { isSendTransactionIntent } from "@ledgerhq/coin-framework/utils";
|
|
15
16
|
import { log } from "@ledgerhq/logs";
|
|
16
17
|
import coinConfig, { type XrpConfig } from "../config";
|
|
17
18
|
import {
|
|
18
19
|
broadcast,
|
|
19
20
|
combine,
|
|
20
21
|
craftTransaction,
|
|
22
|
+
craftRawTransaction,
|
|
21
23
|
estimateFees,
|
|
22
24
|
getBalance,
|
|
23
25
|
getAccountInfo,
|
|
@@ -36,6 +38,7 @@ export function createApi(config: XrpConfig): Api<XrpMapMemo> {
|
|
|
36
38
|
broadcast,
|
|
37
39
|
combine,
|
|
38
40
|
craftTransaction: craft,
|
|
41
|
+
craftRawTransaction,
|
|
39
42
|
estimateFees: estimate,
|
|
40
43
|
getBalance,
|
|
41
44
|
lastBlock,
|
|
@@ -64,6 +67,9 @@ async function craft(
|
|
|
64
67
|
transactionIntent: TransactionIntent<XrpMapMemo>,
|
|
65
68
|
customFees?: FeeEstimation,
|
|
66
69
|
): Promise<CraftedTransaction> {
|
|
70
|
+
if (isSendTransactionIntent(transactionIntent) === false) {
|
|
71
|
+
throw new Error("Only transaction intentType is supported");
|
|
72
|
+
}
|
|
67
73
|
const nextSequenceNumber = await getNextValidSequence(transactionIntent.sender);
|
|
68
74
|
const estimatedFees = customFees?.value ?? (await estimateFees()).fees;
|
|
69
75
|
|
package/src/logic/combine.ts
CHANGED
|
@@ -1,18 +1,38 @@
|
|
|
1
1
|
import { decode, encode } from "ripple-binary-codec";
|
|
2
2
|
import { JsonObject } from "ripple-binary-codec/dist/types/serialized-type";
|
|
3
|
+
import { SignerEntry } from "../types";
|
|
3
4
|
|
|
4
5
|
type XRPTransaction = JsonObject & {
|
|
5
|
-
|
|
6
|
+
Signers?: Array<SignerEntry>;
|
|
7
|
+
TxnSignature?: string;
|
|
6
8
|
SigningPubKey?: string;
|
|
7
9
|
};
|
|
8
10
|
|
|
9
11
|
export function combine(transaction: string, signature: string, publicKey?: string): string {
|
|
10
|
-
const xrplTransaction:
|
|
11
|
-
let transactionWithSignature: XRPTransaction = { ...xrplTransaction, TxnSignature: signature };
|
|
12
|
+
const xrplTransaction: XRPTransaction = decode(transaction);
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
// Multi sign transactions have an empty SigningPubKey
|
|
15
|
+
// https://xrpl.org/docs/concepts/accounts/multi-signing#sending-multi-signed-transactions
|
|
16
|
+
// https://xrpl.org/docs/tutorials/how-tos/manage-account-settings/send-a-multi-signed-transaction
|
|
17
|
+
if (xrplTransaction.SigningPubKey === "") {
|
|
18
|
+
// Find the signer that still needs a signature. If a publicKey is provided, match it;
|
|
19
|
+
// otherwise take the first signer without a TxnSignature.
|
|
20
|
+
const signerEntry = xrplTransaction.Signers?.find(
|
|
21
|
+
s => s.Signer.TxnSignature === "" && (!publicKey || s.Signer.SigningPubKey === publicKey),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
if (signerEntry) {
|
|
25
|
+
signerEntry.Signer.TxnSignature = signature;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return encode(xrplTransaction);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
xrplTransaction.TxnSignature = signature;
|
|
32
|
+
|
|
33
|
+
if (publicKey && !xrplTransaction.SigningPubKey) {
|
|
34
|
+
xrplTransaction.SigningPubKey = publicKey;
|
|
15
35
|
}
|
|
16
36
|
|
|
17
|
-
return encode(
|
|
37
|
+
return encode(xrplTransaction);
|
|
18
38
|
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { craftRawTransaction } from "./craftRawTransaction";
|
|
2
|
+
import { encode, decode } from "ripple-binary-codec";
|
|
3
|
+
import { SignerEntry } from "../types";
|
|
4
|
+
|
|
5
|
+
// --- Mocks ---
|
|
6
|
+
const mockEstimateFees = jest.fn();
|
|
7
|
+
const mockGetLedgerIndex = jest.fn();
|
|
8
|
+
|
|
9
|
+
jest.mock("./estimateFees", () => ({
|
|
10
|
+
estimateFees: () => mockEstimateFees(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
jest.mock("../network", () => ({
|
|
14
|
+
getLedgerIndex: () => mockGetLedgerIndex(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Mock ripple-address-codec globally so ordering logic in sortSignersByNumericAddress uses deterministic data.
|
|
18
|
+
const decodeAccountIDMock = jest.fn((account: string) => {
|
|
19
|
+
const bytes = new Uint8Array(20).fill(0);
|
|
20
|
+
const ordering: Record<string, number> = { alpha: 1, beta: 2, gamma: 3, delta: 4 };
|
|
21
|
+
if (ordering[account]) bytes[19] = ordering[account];
|
|
22
|
+
return bytes;
|
|
23
|
+
});
|
|
24
|
+
jest.mock("ripple-address-codec", () => ({
|
|
25
|
+
decodeAccountID: (addr: string) => decodeAccountIDMock(addr),
|
|
26
|
+
isValidClassicAddress: () => true,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
describe("craftRawTransaction", () => {
|
|
30
|
+
const sender = "rPDf6SQStnNmw1knCu1ei7h6BcDAEUUqn5";
|
|
31
|
+
const destination = "rJe1St1G6BWMFmdrrcT7NdD3XT1NxTMEWN";
|
|
32
|
+
// XRPL Ed25519 public keys must be 33 bytes: 0xED prefix + 32 bytes payload (66 hex chars total)
|
|
33
|
+
// Using a deterministic dummy key so ripple-binary-codec encode/decode roundtrips without alteration.
|
|
34
|
+
const publicKey = "ED0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543211"; // 66 hex chars
|
|
35
|
+
// Pre-set key used in the test that ensures existing SigningPubKey is preserved.
|
|
36
|
+
const alreadySetPublicKey = "EDFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"; // 66 hex chars
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
mockEstimateFees.mockReset();
|
|
40
|
+
mockGetLedgerIndex.mockReset();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Helper to safely extract Signers array without relying on type assertions elsewhere
|
|
44
|
+
function getSigners(obj: unknown): SignerEntry[] | undefined {
|
|
45
|
+
if (!obj || typeof obj !== "object") return undefined;
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
47
|
+
const potential = (obj as { Signers?: unknown }).Signers;
|
|
48
|
+
if (Array.isArray(potential)) {
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
50
|
+
return potential as SignerEntry[];
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
it("fills missing Fee, Sequence, LastLedgerSequence & SigningPubKey for a standard payment", async () => {
|
|
56
|
+
// Given: base transaction missing common autofill fields
|
|
57
|
+
const baseTx = {
|
|
58
|
+
TransactionType: "Payment",
|
|
59
|
+
Account: sender,
|
|
60
|
+
Amount: "1000",
|
|
61
|
+
Destination: destination,
|
|
62
|
+
} as const;
|
|
63
|
+
const serialized = encode(baseTx);
|
|
64
|
+
|
|
65
|
+
mockEstimateFees.mockResolvedValue({ fees: BigInt(123) });
|
|
66
|
+
mockGetLedgerIndex.mockResolvedValue(1000);
|
|
67
|
+
|
|
68
|
+
// When
|
|
69
|
+
const result = await craftRawTransaction(serialized, sender, publicKey, 10);
|
|
70
|
+
|
|
71
|
+
// Then
|
|
72
|
+
const decoded = decode(result.transaction);
|
|
73
|
+
expect(decoded).toMatchObject({
|
|
74
|
+
TransactionType: "Payment",
|
|
75
|
+
Account: sender,
|
|
76
|
+
Amount: "1000",
|
|
77
|
+
Destination: destination,
|
|
78
|
+
Fee: "123",
|
|
79
|
+
Sequence: 10,
|
|
80
|
+
LastLedgerSequence: 1020, // 1000 + 20 offset
|
|
81
|
+
SigningPubKey: publicKey,
|
|
82
|
+
});
|
|
83
|
+
expect(mockEstimateFees).toHaveBeenCalledTimes(1);
|
|
84
|
+
expect(mockGetLedgerIndex).toHaveBeenCalledTimes(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("autofills only missing Fee & LastLedgerSequence when Sequence provided", async () => {
|
|
88
|
+
// Given: Sequence present; Fee, LastLedgerSequence & SigningPubKey missing
|
|
89
|
+
const baseTx = {
|
|
90
|
+
TransactionType: "Payment",
|
|
91
|
+
Account: sender,
|
|
92
|
+
Amount: "1234",
|
|
93
|
+
Destination: destination,
|
|
94
|
+
Sequence: 77,
|
|
95
|
+
} as const;
|
|
96
|
+
const serialized = encode(baseTx);
|
|
97
|
+
|
|
98
|
+
mockEstimateFees.mockResolvedValue({ fees: BigInt(456) });
|
|
99
|
+
mockGetLedgerIndex.mockResolvedValue(2000);
|
|
100
|
+
|
|
101
|
+
// When: pass a different sequence param to ensure existing Sequence is not overwritten
|
|
102
|
+
const result = await craftRawTransaction(serialized, sender, publicKey, 999);
|
|
103
|
+
|
|
104
|
+
// Then
|
|
105
|
+
const decoded = decode(result.transaction);
|
|
106
|
+
expect(decoded.Sequence).toBe(77); // preserved
|
|
107
|
+
expect(decoded.Fee).toBe("456"); // autofilled
|
|
108
|
+
expect(decoded.LastLedgerSequence).toBe(2020); // 2000 + 20
|
|
109
|
+
expect(decoded.SigningPubKey).toBe(publicKey); // autofilled
|
|
110
|
+
expect(mockEstimateFees).toHaveBeenCalledTimes(1);
|
|
111
|
+
expect(mockGetLedgerIndex).toHaveBeenCalledTimes(1);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("does not overwrite provided Fee / Sequence / LastLedgerSequence / SigningPubKey", async () => {
|
|
115
|
+
// Given
|
|
116
|
+
const baseTx = {
|
|
117
|
+
TransactionType: "Payment",
|
|
118
|
+
Account: sender,
|
|
119
|
+
Amount: "2500",
|
|
120
|
+
Destination: destination,
|
|
121
|
+
Fee: "999",
|
|
122
|
+
Sequence: 42,
|
|
123
|
+
LastLedgerSequence: 5000,
|
|
124
|
+
SigningPubKey: alreadySetPublicKey,
|
|
125
|
+
} as const;
|
|
126
|
+
const serialized = encode(baseTx);
|
|
127
|
+
|
|
128
|
+
// Mocks would be used if fields missing; ensure they are ignored
|
|
129
|
+
mockEstimateFees.mockResolvedValue({ fees: BigInt(1) });
|
|
130
|
+
mockGetLedgerIndex.mockResolvedValue(1);
|
|
131
|
+
|
|
132
|
+
// When
|
|
133
|
+
const result = await craftRawTransaction(serialized, sender, publicKey, 999);
|
|
134
|
+
|
|
135
|
+
// Then
|
|
136
|
+
const decoded = decode(result.transaction);
|
|
137
|
+
expect(decoded.Fee).toBe("999");
|
|
138
|
+
expect(decoded.Sequence).toBe(42);
|
|
139
|
+
expect(decoded.LastLedgerSequence).toBe(5000);
|
|
140
|
+
expect(decoded.SigningPubKey).toBe(alreadySetPublicKey);
|
|
141
|
+
expect(mockEstimateFees).not.toHaveBeenCalled();
|
|
142
|
+
expect(mockGetLedgerIndex).not.toHaveBeenCalled();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("sets Sequence to 0 when TicketSequence is provided and Sequence missing", async () => {
|
|
146
|
+
// Given
|
|
147
|
+
const baseTx = {
|
|
148
|
+
TransactionType: "Payment",
|
|
149
|
+
Account: sender,
|
|
150
|
+
Amount: "1",
|
|
151
|
+
Destination: destination,
|
|
152
|
+
TicketSequence: 555,
|
|
153
|
+
Fee: "10",
|
|
154
|
+
// Sequence intentionally omitted
|
|
155
|
+
} as const;
|
|
156
|
+
const serialized = encode(baseTx);
|
|
157
|
+
|
|
158
|
+
mockGetLedgerIndex.mockResolvedValue(10);
|
|
159
|
+
|
|
160
|
+
// When
|
|
161
|
+
const result = await craftRawTransaction(serialized, sender, publicKey, 77);
|
|
162
|
+
|
|
163
|
+
// Then
|
|
164
|
+
const decoded = decode(result.transaction);
|
|
165
|
+
expect(decoded.TicketSequence).toBe(555);
|
|
166
|
+
expect(decoded.Sequence).toBe(0);
|
|
167
|
+
expect(decoded.LastLedgerSequence).toBe(30); // 10 + 20
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("forces Sequence to 0 when TicketSequence present even if non-zero Sequence provided", async () => {
|
|
171
|
+
// Given: TicketSequence provided AND a non-zero Sequence already set (we enforce spec by overriding)
|
|
172
|
+
const baseTx = {
|
|
173
|
+
TransactionType: "Payment",
|
|
174
|
+
Account: sender,
|
|
175
|
+
Amount: "123",
|
|
176
|
+
Destination: destination,
|
|
177
|
+
TicketSequence: 777,
|
|
178
|
+
Sequence: 55, // non-zero pre-set
|
|
179
|
+
Fee: "10",
|
|
180
|
+
} as const;
|
|
181
|
+
const serialized = encode(baseTx);
|
|
182
|
+
|
|
183
|
+
mockGetLedgerIndex.mockResolvedValue(111);
|
|
184
|
+
|
|
185
|
+
// When
|
|
186
|
+
const result = await craftRawTransaction(serialized, sender, publicKey, 9999);
|
|
187
|
+
|
|
188
|
+
// Then
|
|
189
|
+
const decoded = decode(result.transaction);
|
|
190
|
+
expect(decoded.Sequence).toBe(0); // overridden to comply with spec
|
|
191
|
+
expect(decoded.TicketSequence).toBe(777);
|
|
192
|
+
expect(decoded.LastLedgerSequence).toBe(131); // 111 + 20
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("throws when sender does not match Account for standard transaction", async () => {
|
|
196
|
+
// Given
|
|
197
|
+
const baseTx = {
|
|
198
|
+
TransactionType: "Payment",
|
|
199
|
+
Account: sender,
|
|
200
|
+
Amount: "10",
|
|
201
|
+
Destination: destination,
|
|
202
|
+
} as const;
|
|
203
|
+
const serialized = encode(baseTx);
|
|
204
|
+
|
|
205
|
+
// When & Then
|
|
206
|
+
await expect(craftRawTransaction(serialized, "rOTHERADDRESS", publicKey, 1)).rejects.toThrow(
|
|
207
|
+
"Sender address does not match the transaction account",
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("multi-sign", () => {
|
|
212
|
+
it("adds a Signers array when absent (single existing signer)", async () => {
|
|
213
|
+
// Given: multi-sign transaction (SigningPubKey empty) with required Fee & Sequence
|
|
214
|
+
const multi = {
|
|
215
|
+
TransactionType: "Payment",
|
|
216
|
+
Account: sender, // Account can differ from signer in multi-sign, we keep same for simplicity
|
|
217
|
+
Amount: "100",
|
|
218
|
+
Destination: destination,
|
|
219
|
+
SigningPubKey: "", // signals multi-sign
|
|
220
|
+
Fee: "500",
|
|
221
|
+
Sequence: 9,
|
|
222
|
+
} as const;
|
|
223
|
+
const serialized = encode(multi);
|
|
224
|
+
|
|
225
|
+
// When
|
|
226
|
+
const result = await craftRawTransaction(serialized, sender, publicKey, 9);
|
|
227
|
+
|
|
228
|
+
// Then
|
|
229
|
+
const decoded = decode(result.transaction);
|
|
230
|
+
const signers = getSigners(decoded);
|
|
231
|
+
expect(signers?.length).toBe(1);
|
|
232
|
+
if (signers && signers[0]) {
|
|
233
|
+
expect(signers[0]).toMatchObject({
|
|
234
|
+
Signer: { Account: sender, SigningPubKey: publicKey, TxnSignature: "" },
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
// No autofill of LastLedgerSequence in multi-sign path (not required for this assertion)
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("does not add LastLedgerSequence for multi-sign when absent", async () => {
|
|
241
|
+
// Given: multi-sign tx w/o LastLedgerSequence
|
|
242
|
+
const multi = {
|
|
243
|
+
TransactionType: "Payment",
|
|
244
|
+
Account: sender,
|
|
245
|
+
Amount: "50",
|
|
246
|
+
Destination: destination,
|
|
247
|
+
SigningPubKey: "", // multi-sign
|
|
248
|
+
Fee: "12",
|
|
249
|
+
Sequence: 2,
|
|
250
|
+
} as const;
|
|
251
|
+
const serialized = encode(multi);
|
|
252
|
+
mockGetLedgerIndex.mockResolvedValue(500); // should NOT be used
|
|
253
|
+
|
|
254
|
+
// When
|
|
255
|
+
const result = await craftRawTransaction(serialized, sender, publicKey, 2);
|
|
256
|
+
|
|
257
|
+
// Then
|
|
258
|
+
const decoded = decode(result.transaction);
|
|
259
|
+
expect(decoded.LastLedgerSequence).toBeUndefined();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("appends signer and sorts existing Signers numerically", async () => {
|
|
263
|
+
// Given existing unsorted signers
|
|
264
|
+
const existing: SignerEntry[] = [
|
|
265
|
+
{
|
|
266
|
+
Signer: {
|
|
267
|
+
Account: "rPDf6SQStnNmw1knCu1ei7h6BcDAEUUqn5",
|
|
268
|
+
SigningPubKey: "K3",
|
|
269
|
+
TxnSignature: "",
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
Signer: {
|
|
274
|
+
Account: "rJe1St1G6BWMFmdrrcT7NdD3XT1NxTMEWN",
|
|
275
|
+
SigningPubKey: "K1",
|
|
276
|
+
TxnSignature: "",
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
Signer: {
|
|
281
|
+
Account: "rDKsbvy9uaNpPtvVFraJyNGfjvTw8xivgK",
|
|
282
|
+
SigningPubKey: "K4",
|
|
283
|
+
TxnSignature: "",
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
];
|
|
287
|
+
const multi: {
|
|
288
|
+
TransactionType: string;
|
|
289
|
+
Account: string;
|
|
290
|
+
Amount: string;
|
|
291
|
+
Destination: string;
|
|
292
|
+
SigningPubKey: string;
|
|
293
|
+
Fee: string;
|
|
294
|
+
Sequence: number;
|
|
295
|
+
Signers: SignerEntry[];
|
|
296
|
+
} = {
|
|
297
|
+
TransactionType: "Payment",
|
|
298
|
+
Account: sender,
|
|
299
|
+
Amount: "200",
|
|
300
|
+
Destination: destination,
|
|
301
|
+
SigningPubKey: "",
|
|
302
|
+
Fee: "700",
|
|
303
|
+
Sequence: 3,
|
|
304
|
+
Signers: existing,
|
|
305
|
+
};
|
|
306
|
+
const serialized = encode(multi);
|
|
307
|
+
|
|
308
|
+
// When
|
|
309
|
+
const result = await craftRawTransaction(
|
|
310
|
+
serialized,
|
|
311
|
+
"r94uo44ukDHWVjYDJLZJYwDdZjo1F2QYgq",
|
|
312
|
+
publicKey,
|
|
313
|
+
3,
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// Then
|
|
317
|
+
const decoded = decode(result.transaction);
|
|
318
|
+
const signers = getSigners(decoded) ?? [];
|
|
319
|
+
const accounts = signers.map(s => s.Signer.Account);
|
|
320
|
+
expect(accounts).toEqual([
|
|
321
|
+
"rPDf6SQStnNmw1knCu1ei7h6BcDAEUUqn5",
|
|
322
|
+
"rJe1St1G6BWMFmdrrcT7NdD3XT1NxTMEWN",
|
|
323
|
+
"rDKsbvy9uaNpPtvVFraJyNGfjvTw8xivgK",
|
|
324
|
+
"r94uo44ukDHWVjYDJLZJYwDdZjo1F2QYgq",
|
|
325
|
+
]);
|
|
326
|
+
const betaEntry = signers.find(
|
|
327
|
+
s => s.Signer.Account === "r94uo44ukDHWVjYDJLZJYwDdZjo1F2QYgq",
|
|
328
|
+
);
|
|
329
|
+
expect(betaEntry?.Signer.SigningPubKey).toBe(publicKey);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("throws when Fee missing for multi-sign", async () => {
|
|
333
|
+
const multi = {
|
|
334
|
+
TransactionType: "Payment",
|
|
335
|
+
Account: sender,
|
|
336
|
+
Amount: "10",
|
|
337
|
+
Destination: destination,
|
|
338
|
+
SigningPubKey: "",
|
|
339
|
+
Sequence: 1,
|
|
340
|
+
} as const;
|
|
341
|
+
const serialized = encode(multi);
|
|
342
|
+
await expect(craftRawTransaction(serialized, sender, publicKey, 1)).rejects.toThrow(
|
|
343
|
+
"Fee is required for multi sign transactions",
|
|
344
|
+
);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("throws when Sequence missing for multi-sign", async () => {
|
|
348
|
+
const multi = {
|
|
349
|
+
TransactionType: "Payment",
|
|
350
|
+
Account: sender,
|
|
351
|
+
Amount: "10",
|
|
352
|
+
Destination: destination,
|
|
353
|
+
SigningPubKey: "",
|
|
354
|
+
Fee: "1",
|
|
355
|
+
} as const;
|
|
356
|
+
const serialized = encode(multi);
|
|
357
|
+
await expect(craftRawTransaction(serialized, sender, publicKey, 1)).rejects.toThrow(
|
|
358
|
+
"Sequence is required for multi sign transactions",
|
|
359
|
+
);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
});
|