@buildonspark/spark-sdk 0.5.0 → 0.5.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/CHANGELOG.md +26 -0
- package/dist/bare/index.cjs +1608 -3635
- package/dist/bare/index.d.cts +27 -435
- package/dist/bare/index.d.ts +27 -435
- package/dist/bare/index.js +1608 -3634
- package/dist/{chunk-RU434ZAE.js → chunk-F3BFSHVR.js} +357 -391
- package/dist/{chunk-UYEB2VPG.js → chunk-IOIEBLMK.js} +7 -1
- package/dist/{chunk-EU3I7GFB.js → chunk-STB6WMU7.js} +1 -1
- package/dist/{chunk-JE3MXMPW.js → chunk-UTECVGQQ.js} +93 -202
- package/dist/{chunk-ZP6Z6DFX.js → chunk-YFVVYZCS.js} +37 -5
- package/dist/{client-D1dLzWu0.d.ts → client-C9kc4cog.d.cts} +9 -3
- package/dist/{client-CVn0R_eM.d.cts → client-eyjf4knu.d.ts} +9 -3
- package/dist/graphql/objects/index.cjs +7 -1
- package/dist/graphql/objects/index.d.cts +3 -3
- package/dist/graphql/objects/index.d.ts +3 -3
- package/dist/graphql/objects/index.js +1 -1
- package/dist/index.browser.d.ts +27 -435
- package/dist/index.browser.js +1613 -3639
- package/dist/index.node.cjs +1613 -3640
- package/dist/index.node.d.cts +7 -8
- package/dist/index.node.d.ts +7 -8
- package/dist/index.node.js +5 -7
- package/dist/native/index.react-native.cjs +1613 -3640
- package/dist/native/index.react-native.d.cts +27 -435
- package/dist/native/index.react-native.d.ts +27 -435
- package/dist/native/index.react-native.js +1613 -3639
- package/dist/proto/spark.cjs +93 -202
- package/dist/proto/spark.d.cts +1 -1
- package/dist/proto/spark.d.ts +1 -1
- package/dist/proto/spark.js +1 -1
- package/dist/proto/spark_token.cjs +36 -4
- package/dist/proto/spark_token.d.cts +4 -1
- package/dist/proto/spark_token.d.ts +4 -1
- package/dist/proto/spark_token.js +2 -2
- package/dist/{spark-2Fxnvl8K.d.cts → spark-d6w3BLGZ.d.cts} +10 -328
- package/dist/{spark-2Fxnvl8K.d.ts → spark-d6w3BLGZ.d.ts} +10 -328
- package/dist/{spark-wallet.node-DlhZiDgY.d.ts → spark-wallet.node-MReThHBY.d.ts} +6 -7
- package/dist/{spark-wallet.node-xKJXzAEd.d.cts → spark-wallet.node-eR0svGws.d.cts} +6 -7
- package/dist/tests/test-utils.cjs +409 -2429
- package/dist/tests/test-utils.d.cts +3 -3
- package/dist/tests/test-utils.d.ts +3 -3
- package/dist/tests/test-utils.js +5 -5
- package/dist/types/index.cjs +100 -203
- package/dist/types/index.d.cts +2 -2
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.js +3 -3
- package/package.json +3 -3
- package/src/graphql/client.ts +36 -1
- package/src/graphql/objects/LightningSendRequestStatus.ts +25 -13
- package/src/proto/common.ts +1 -1
- package/src/proto/google/protobuf/descriptor.ts +1 -1
- package/src/proto/google/protobuf/duration.ts +1 -1
- package/src/proto/google/protobuf/empty.ts +1 -1
- package/src/proto/google/protobuf/timestamp.ts +1 -1
- package/src/proto/mock.ts +1 -1
- package/src/proto/spark.ts +113 -446
- package/src/proto/spark_authn.ts +1 -1
- package/src/proto/spark_token.ts +41 -2
- package/src/proto/validate/validate.ts +1 -1
- package/src/services/connection/connection.ts +23 -60
- package/src/services/coop-exit.ts +3 -5
- package/src/services/deposit.ts +1 -1
- package/src/services/lightning.ts +1 -1
- package/src/services/signing.ts +5 -6
- package/src/services/transfer.ts +250 -240
- package/src/services/wallet-config.ts +22 -5
- package/src/spark-wallet/proto-descriptors.ts +1 -1
- package/src/spark-wallet/proto-reflection.ts +0 -2
- package/src/spark-wallet/spark-wallet.ts +2 -2
- package/src/spark_descriptors.pb +0 -0
- package/src/tests/bufbuild-reflection.test.ts +2 -3
- package/src/tests/integration/coop-exit.test.ts +6 -1
- package/src/tests/integration/htlc.test.ts +5 -0
- package/src/tests/integration/lightning.test.ts +24 -4
- package/src/tests/integration/time-sync.test.ts +18 -0
- package/src/tests/integration/transfer.test.ts +42 -7
- package/src/tests/ssp-client-retry.test.ts +161 -0
- package/src/tests/token-hashing.test.ts +92 -0
- package/src/utils/token-hashing.ts +4 -51
- package/src/utils/transaction.ts +1 -2
- package/src/utils/unilateral-exit.ts +139 -142
|
@@ -237,7 +237,12 @@ describe.each(walletTypes)(
|
|
|
237
237
|
throw new Error("test: Leaf not found");
|
|
238
238
|
}
|
|
239
239
|
return {
|
|
240
|
-
leaf:
|
|
240
|
+
leaf: {
|
|
241
|
+
...leaf.leaf,
|
|
242
|
+
refundTx: leaf.intermediateRefundTx,
|
|
243
|
+
directRefundTx: leaf.intermediateDirectRefundTx,
|
|
244
|
+
directFromCpfpRefundTx: leaf.intermediateDirectFromCpfpRefundTx,
|
|
245
|
+
},
|
|
241
246
|
keyDerivation: {
|
|
242
247
|
type: KeyDerivationType.ECIES,
|
|
243
248
|
path: leaf.secretCipher,
|
|
@@ -356,7 +361,12 @@ describe.each(walletTypes)(
|
|
|
356
361
|
throw new Error("test: Leaf not found");
|
|
357
362
|
}
|
|
358
363
|
return {
|
|
359
|
-
leaf:
|
|
364
|
+
leaf: {
|
|
365
|
+
...leaf.leaf,
|
|
366
|
+
refundTx: leaf.intermediateRefundTx,
|
|
367
|
+
directRefundTx: leaf.intermediateDirectRefundTx,
|
|
368
|
+
directFromCpfpRefundTx: leaf.intermediateDirectFromCpfpRefundTx,
|
|
369
|
+
},
|
|
360
370
|
keyDerivation: {
|
|
361
371
|
type: KeyDerivationType.ECIES,
|
|
362
372
|
path: leaf.secretCipher,
|
|
@@ -474,7 +484,12 @@ describe.each(walletTypes)(
|
|
|
474
484
|
throw new Error("test: Leaf not found");
|
|
475
485
|
}
|
|
476
486
|
return {
|
|
477
|
-
leaf:
|
|
487
|
+
leaf: {
|
|
488
|
+
...leaf.leaf,
|
|
489
|
+
refundTx: leaf.intermediateRefundTx,
|
|
490
|
+
directRefundTx: leaf.intermediateDirectRefundTx,
|
|
491
|
+
directFromCpfpRefundTx: leaf.intermediateDirectFromCpfpRefundTx,
|
|
492
|
+
},
|
|
478
493
|
keyDerivation: {
|
|
479
494
|
type: KeyDerivationType.ECIES,
|
|
480
495
|
path: leaf.secretCipher,
|
|
@@ -598,7 +613,12 @@ describe.each(walletTypes)(
|
|
|
598
613
|
throw new Error("test: Leaf not found");
|
|
599
614
|
}
|
|
600
615
|
return {
|
|
601
|
-
leaf:
|
|
616
|
+
leaf: {
|
|
617
|
+
...leaf.leaf,
|
|
618
|
+
refundTx: leaf.intermediateRefundTx,
|
|
619
|
+
directRefundTx: leaf.intermediateDirectRefundTx,
|
|
620
|
+
directFromCpfpRefundTx: leaf.intermediateDirectFromCpfpRefundTx,
|
|
621
|
+
},
|
|
602
622
|
keyDerivation: {
|
|
603
623
|
type: KeyDerivationType.ECIES,
|
|
604
624
|
path: leaf.secretCipher,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it } from "@jest/globals";
|
|
2
|
+
import { SparkWalletTestingIntegration } from "../utils/spark-testing-wallet.js";
|
|
3
|
+
|
|
4
|
+
describe("Server time synchronization", () => {
|
|
5
|
+
it("should be synced after making a gRPC call", async () => {
|
|
6
|
+
const { wallet } = await SparkWalletTestingIntegration.initialize({
|
|
7
|
+
options: { network: "LOCAL" },
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const connectionManager = wallet.getConnectionManager();
|
|
11
|
+
|
|
12
|
+
await wallet.getSparkAddress();
|
|
13
|
+
|
|
14
|
+
expect(connectionManager.isTimeSynced()).toBe(true);
|
|
15
|
+
|
|
16
|
+
await wallet.cleanupConnections();
|
|
17
|
+
}, 30000);
|
|
18
|
+
});
|
|
@@ -100,7 +100,12 @@ describe.each(walletTypes)(
|
|
|
100
100
|
|
|
101
101
|
const claimingNodes: LeafKeyTweak[] = receiverTransfer!.leaves.map(
|
|
102
102
|
(leaf) => ({
|
|
103
|
-
leaf:
|
|
103
|
+
leaf: {
|
|
104
|
+
...leaf.leaf!,
|
|
105
|
+
refundTx: leaf.intermediateRefundTx,
|
|
106
|
+
directRefundTx: leaf.intermediateDirectRefundTx,
|
|
107
|
+
directFromCpfpRefundTx: leaf.intermediateDirectFromCpfpRefundTx,
|
|
108
|
+
},
|
|
104
109
|
keyDerivation: {
|
|
105
110
|
type: KeyDerivationType.ECIES,
|
|
106
111
|
path: leaf.secretCipher,
|
|
@@ -197,7 +202,12 @@ describe.each(walletTypes)(
|
|
|
197
202
|
|
|
198
203
|
const claimingNodes: LeafKeyTweak[] = receiverTransfer!.leaves.map(
|
|
199
204
|
(leaf) => ({
|
|
200
|
-
leaf:
|
|
205
|
+
leaf: {
|
|
206
|
+
...leaf.leaf!,
|
|
207
|
+
refundTx: leaf.intermediateRefundTx,
|
|
208
|
+
directRefundTx: leaf.intermediateDirectRefundTx,
|
|
209
|
+
directFromCpfpRefundTx: leaf.intermediateDirectFromCpfpRefundTx,
|
|
210
|
+
},
|
|
201
211
|
keyDerivation: {
|
|
202
212
|
type: KeyDerivationType.ECIES,
|
|
203
213
|
path: leaf.secretCipher,
|
|
@@ -342,7 +352,12 @@ describe.each(walletTypes)(
|
|
|
342
352
|
|
|
343
353
|
const claimingNodes: LeafKeyTweak[] = receiverTransfer!.leaves.map(
|
|
344
354
|
(leaf) => ({
|
|
345
|
-
leaf:
|
|
355
|
+
leaf: {
|
|
356
|
+
...rootNode,
|
|
357
|
+
refundTx: leaf.intermediateRefundTx,
|
|
358
|
+
directRefundTx: leaf.intermediateDirectRefundTx,
|
|
359
|
+
directFromCpfpRefundTx: leaf.intermediateDirectFromCpfpRefundTx,
|
|
360
|
+
},
|
|
346
361
|
keyDerivation: {
|
|
347
362
|
type: KeyDerivationType.ECIES,
|
|
348
363
|
path: receiverTransfer!.leaves[0]!.secretCipher,
|
|
@@ -691,7 +706,12 @@ describe.each(walletTypes)("transfer v2", ({ name, Signer, createTree }) => {
|
|
|
691
706
|
|
|
692
707
|
const claimingNodes: LeafKeyTweak[] = receiverTransfer!.leaves.map(
|
|
693
708
|
(leaf) => ({
|
|
694
|
-
leaf:
|
|
709
|
+
leaf: {
|
|
710
|
+
...leaf.leaf!,
|
|
711
|
+
refundTx: leaf.intermediateRefundTx,
|
|
712
|
+
directRefundTx: leaf.intermediateDirectRefundTx,
|
|
713
|
+
directFromCpfpRefundTx: leaf.intermediateDirectFromCpfpRefundTx,
|
|
714
|
+
},
|
|
695
715
|
keyDerivation: {
|
|
696
716
|
type: KeyDerivationType.ECIES,
|
|
697
717
|
path: leaf.secretCipher,
|
|
@@ -753,7 +773,12 @@ describe.each(walletTypes)("transfer v2", ({ name, Signer, createTree }) => {
|
|
|
753
773
|
|
|
754
774
|
const claimingNodes: LeafKeyTweak[] = receiverTransfer!.leaves.map(
|
|
755
775
|
(leaf) => ({
|
|
756
|
-
leaf:
|
|
776
|
+
leaf: {
|
|
777
|
+
...rootNode,
|
|
778
|
+
refundTx: leaf.intermediateRefundTx,
|
|
779
|
+
directRefundTx: leaf.intermediateDirectRefundTx,
|
|
780
|
+
directFromCpfpRefundTx: leaf.intermediateDirectFromCpfpRefundTx,
|
|
781
|
+
},
|
|
757
782
|
keyDerivation: {
|
|
758
783
|
type: KeyDerivationType.ECIES,
|
|
759
784
|
path: receiverTransfer!.leaves[0]!.secretCipher,
|
|
@@ -1049,7 +1074,12 @@ describe.each(walletTypes)("transfer v2", ({ name, Signer, createTree }) => {
|
|
|
1049
1074
|
const transfer = pendingTransfers.transfers[0]!;
|
|
1050
1075
|
|
|
1051
1076
|
const claimingNodes: LeafKeyTweak[] = transfer!.leaves.map((leaf) => ({
|
|
1052
|
-
leaf:
|
|
1077
|
+
leaf: {
|
|
1078
|
+
...leaf.leaf!,
|
|
1079
|
+
refundTx: leaf.intermediateRefundTx,
|
|
1080
|
+
directRefundTx: leaf.intermediateDirectRefundTx,
|
|
1081
|
+
directFromCpfpRefundTx: leaf.intermediateDirectFromCpfpRefundTx,
|
|
1082
|
+
},
|
|
1053
1083
|
keyDerivation: {
|
|
1054
1084
|
type: KeyDerivationType.ECIES,
|
|
1055
1085
|
path: leaf.secretCipher,
|
|
@@ -1112,7 +1142,12 @@ describe.each(walletTypes)("transfer v2", ({ name, Signer, createTree }) => {
|
|
|
1112
1142
|
const transfer = pendingTransfers.transfers[0]!;
|
|
1113
1143
|
|
|
1114
1144
|
const claimingNodes: LeafKeyTweak[] = transfer!.leaves.map((leaf) => ({
|
|
1115
|
-
leaf:
|
|
1145
|
+
leaf: {
|
|
1146
|
+
...leaf.leaf!,
|
|
1147
|
+
refundTx: leaf.intermediateRefundTx,
|
|
1148
|
+
directRefundTx: leaf.intermediateDirectRefundTx,
|
|
1149
|
+
directFromCpfpRefundTx: leaf.intermediateDirectFromCpfpRefundTx,
|
|
1150
|
+
},
|
|
1116
1151
|
keyDerivation: {
|
|
1117
1152
|
type: KeyDerivationType.ECIES,
|
|
1118
1153
|
path: leaf.secretCipher,
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { describe, expect, it, jest } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
// Re-create the retry logic here for testing (since it's not exported)
|
|
4
|
+
const RETRYABLE_STATUS_CODES = new Set([502, 503, 504]);
|
|
5
|
+
|
|
6
|
+
type FetchFn = (
|
|
7
|
+
input: RequestInfo | URL,
|
|
8
|
+
init?: RequestInit,
|
|
9
|
+
) => Promise<Response>;
|
|
10
|
+
|
|
11
|
+
function createRetryFetch(
|
|
12
|
+
baseFetch: FetchFn,
|
|
13
|
+
maxRetries: number = 5,
|
|
14
|
+
baseDelayMs: number = 1000,
|
|
15
|
+
): FetchFn {
|
|
16
|
+
return async (input, init) => {
|
|
17
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
18
|
+
const response = await baseFetch(input, init);
|
|
19
|
+
|
|
20
|
+
if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < maxRetries) {
|
|
21
|
+
const delay = Math.min(baseDelayMs * Math.pow(2, attempt), 10000);
|
|
22
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return response;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
throw new Error("Retry loop exited unexpectedly");
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("SspClient retry fetch", () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
jest.useFakeTimers();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
jest.useRealTimers();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should return response immediately on 200", async () => {
|
|
43
|
+
const mockFetch = jest
|
|
44
|
+
.fn<FetchFn>()
|
|
45
|
+
.mockResolvedValue(new Response("ok", { status: 200 }));
|
|
46
|
+
|
|
47
|
+
const retryFetch = createRetryFetch(mockFetch, 3, 100);
|
|
48
|
+
const response = await retryFetch("https://example.com", {});
|
|
49
|
+
|
|
50
|
+
expect(response.status).toBe(200);
|
|
51
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should retry on 502 and succeed", async () => {
|
|
55
|
+
const mockFetch = jest
|
|
56
|
+
.fn<FetchFn>()
|
|
57
|
+
.mockResolvedValueOnce(new Response("bad gateway", { status: 502 }))
|
|
58
|
+
.mockResolvedValueOnce(new Response("ok", { status: 200 }));
|
|
59
|
+
|
|
60
|
+
const retryFetch = createRetryFetch(mockFetch, 3, 100);
|
|
61
|
+
|
|
62
|
+
const promise = retryFetch("https://example.com", {});
|
|
63
|
+
await jest.runAllTimersAsync();
|
|
64
|
+
const response = await promise;
|
|
65
|
+
|
|
66
|
+
expect(response.status).toBe(200);
|
|
67
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should retry on 503 and succeed", async () => {
|
|
71
|
+
const mockFetch = jest
|
|
72
|
+
.fn<FetchFn>()
|
|
73
|
+
.mockResolvedValueOnce(
|
|
74
|
+
new Response("service unavailable", { status: 503 }),
|
|
75
|
+
)
|
|
76
|
+
.mockResolvedValueOnce(new Response("ok", { status: 200 }));
|
|
77
|
+
|
|
78
|
+
const retryFetch = createRetryFetch(mockFetch, 3, 100);
|
|
79
|
+
|
|
80
|
+
const promise = retryFetch("https://example.com", {});
|
|
81
|
+
await jest.runAllTimersAsync();
|
|
82
|
+
const response = await promise;
|
|
83
|
+
|
|
84
|
+
expect(response.status).toBe(200);
|
|
85
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should retry on 504 and succeed", async () => {
|
|
89
|
+
const mockFetch = jest
|
|
90
|
+
.fn<FetchFn>()
|
|
91
|
+
.mockResolvedValueOnce(new Response("gateway timeout", { status: 504 }))
|
|
92
|
+
.mockResolvedValueOnce(new Response("ok", { status: 200 }));
|
|
93
|
+
|
|
94
|
+
const retryFetch = createRetryFetch(mockFetch, 3, 100);
|
|
95
|
+
|
|
96
|
+
const promise = retryFetch("https://example.com", {});
|
|
97
|
+
await jest.runAllTimersAsync();
|
|
98
|
+
const response = await promise;
|
|
99
|
+
|
|
100
|
+
expect(response.status).toBe(200);
|
|
101
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should return 502 after max retries exhausted", async () => {
|
|
105
|
+
const mockFetch = jest
|
|
106
|
+
.fn<FetchFn>()
|
|
107
|
+
.mockResolvedValue(new Response("bad gateway", { status: 502 }));
|
|
108
|
+
|
|
109
|
+
const retryFetch = createRetryFetch(mockFetch, 3, 100);
|
|
110
|
+
|
|
111
|
+
const promise = retryFetch("https://example.com", {});
|
|
112
|
+
await jest.runAllTimersAsync();
|
|
113
|
+
const response = await promise;
|
|
114
|
+
|
|
115
|
+
// After maxRetries, it returns the last response
|
|
116
|
+
expect(response.status).toBe(502);
|
|
117
|
+
expect(mockFetch).toHaveBeenCalledTimes(4); // 1 initial + 3 retries
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should not retry on 400", async () => {
|
|
121
|
+
const mockFetch = jest
|
|
122
|
+
.fn<FetchFn>()
|
|
123
|
+
.mockResolvedValue(new Response("bad request", { status: 400 }));
|
|
124
|
+
|
|
125
|
+
const retryFetch = createRetryFetch(mockFetch, 3, 100);
|
|
126
|
+
const response = await retryFetch("https://example.com", {});
|
|
127
|
+
|
|
128
|
+
expect(response.status).toBe(400);
|
|
129
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should not retry on 500", async () => {
|
|
133
|
+
const mockFetch = jest
|
|
134
|
+
.fn<FetchFn>()
|
|
135
|
+
.mockResolvedValue(new Response("internal error", { status: 500 }));
|
|
136
|
+
|
|
137
|
+
const retryFetch = createRetryFetch(mockFetch, 3, 100);
|
|
138
|
+
const response = await retryFetch("https://example.com", {});
|
|
139
|
+
|
|
140
|
+
expect(response.status).toBe(500);
|
|
141
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should retry multiple times before succeeding", async () => {
|
|
145
|
+
const mockFetch = jest
|
|
146
|
+
.fn<FetchFn>()
|
|
147
|
+
.mockResolvedValueOnce(new Response("", { status: 504 }))
|
|
148
|
+
.mockResolvedValueOnce(new Response("", { status: 503 }))
|
|
149
|
+
.mockResolvedValueOnce(new Response("", { status: 502 }))
|
|
150
|
+
.mockResolvedValueOnce(new Response("ok", { status: 200 }));
|
|
151
|
+
|
|
152
|
+
const retryFetch = createRetryFetch(mockFetch, 3, 100);
|
|
153
|
+
|
|
154
|
+
const promise = retryFetch("https://example.com", {});
|
|
155
|
+
await jest.runAllTimersAsync();
|
|
156
|
+
const response = await promise;
|
|
157
|
+
|
|
158
|
+
expect(response.status).toBe(200);
|
|
159
|
+
expect(mockFetch).toHaveBeenCalledTimes(4);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -4,6 +4,7 @@ import { Network } from "../proto/spark.js";
|
|
|
4
4
|
import {
|
|
5
5
|
hashTokenTransactionV1,
|
|
6
6
|
hashTokenTransactionV2,
|
|
7
|
+
sortInvoiceAttachments,
|
|
7
8
|
} from "../utils/token-hashing.js";
|
|
8
9
|
|
|
9
10
|
// Test constants for consistent test data across all hash tests - matching Go test data
|
|
@@ -316,3 +317,94 @@ describe("Hash Token Transaction V2", () => {
|
|
|
316
317
|
]);
|
|
317
318
|
});
|
|
318
319
|
});
|
|
320
|
+
|
|
321
|
+
describe("sortInvoiceAttachments", () => {
|
|
322
|
+
it("should sort by bech32m string (lexicographic) not by UUID bytes", () => {
|
|
323
|
+
const attachments = [
|
|
324
|
+
{
|
|
325
|
+
sparkInvoice:
|
|
326
|
+
"sparkl1pgssx2r8ytpwc4exthzsg7ss7a7m69ty8p6s32j0rw65wmd38eamutyezf2ssqgjzqqe4mhekkjh9h58tq0k0522j0uj5zjfdemx76trv5szxvf6psygmq73eyrpps8zctwsyx39pgsy3dytxng6g6dmenc45enqtuc03ml2ryqn5wlxkgtkd4tnckaaj6cjq9h35spgdegtqzfc4uc8c5jcydzhrtv9dznjkwvv66835cxzr60zaczrapxfps6nlk7kqpa4xahmrm4yfm57jxu0l4326rnv4psuwr3ggk582jf9azn",
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
sparkInvoice:
|
|
330
|
+
"sparkl1pgssx2r8ytpwc4exthzsg7ss7a7m69ty8p6s32j0rw65wmd38eamutyezf2ssqgjzqqe4mhekknhnldmyjqdka8ajjdz5zjfdemx76trv5szxv36psygmq73eyrpps8thn0qyx39pgsy3dytxng6g6dmenc45enqtuc03ml2ryqn5wlxkgtkd4tnckaaj6cjq80p5s82syz0gr60at4yv2szssqyr9r8jlh57aa8jan0xskmkv2jqd93zty4vef3lc32ksxfq5c57hpw2j542dnjpjnya3lad2muqcqz26jcj2a9qkf",
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
sparkInvoice:
|
|
334
|
+
"sparkl1pgss9e7ld3nw57ejatjwq64xawwf9akm0yzn09ywfyj5wmr99t5fwrt8zftqsqgjzqqe4mhekk58qsvlkfqd7qlqthvz5zjfdemx76trv5szxve6psygmq73eyrppq8sl80qyx3xpgsy3dytxng6g6dmenc45enqtuc03ml2ryqn5wlxkgtkd4tnckaaj6cjqgq56xjqesr97xccw4u8qn8k68sddsk7rzcs5ctg27pqfu8v0mkfh350tkt4e3g8qr3qyqzcd99recq7ud6yhtvfhtj948a9944zz7q9xxrjhvgp58ut6",
|
|
335
|
+
},
|
|
336
|
+
];
|
|
337
|
+
|
|
338
|
+
const sorted = sortInvoiceAttachments(attachments);
|
|
339
|
+
|
|
340
|
+
expect(sorted).toEqual([
|
|
341
|
+
{
|
|
342
|
+
sparkInvoice:
|
|
343
|
+
"sparkl1pgss9e7ld3nw57ejatjwq64xawwf9akm0yzn09ywfyj5wmr99t5fwrt8zftqsqgjzqqe4mhekk58qsvlkfqd7qlqthvz5zjfdemx76trv5szxve6psygmq73eyrppq8sl80qyx3xpgsy3dytxng6g6dmenc45enqtuc03ml2ryqn5wlxkgtkd4tnckaaj6cjqgq56xjqesr97xccw4u8qn8k68sddsk7rzcs5ctg27pqfu8v0mkfh350tkt4e3g8qr3qyqzcd99recq7ud6yhtvfhtj948a9944zz7q9xxrjhvgp58ut6",
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
sparkInvoice:
|
|
347
|
+
"sparkl1pgssx2r8ytpwc4exthzsg7ss7a7m69ty8p6s32j0rw65wmd38eamutyezf2ssqgjzqqe4mhekkjh9h58tq0k0522j0uj5zjfdemx76trv5szxvf6psygmq73eyrpps8zctwsyx39pgsy3dytxng6g6dmenc45enqtuc03ml2ryqn5wlxkgtkd4tnckaaj6cjq9h35spgdegtqzfc4uc8c5jcydzhrtv9dznjkwvv66835cxzr60zaczrapxfps6nlk7kqpa4xahmrm4yfm57jxu0l4326rnv4psuwr3ggk582jf9azn",
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
sparkInvoice:
|
|
351
|
+
"sparkl1pgssx2r8ytpwc4exthzsg7ss7a7m69ty8p6s32j0rw65wmd38eamutyezf2ssqgjzqqe4mhekknhnldmyjqdka8ajjdz5zjfdemx76trv5szxv36psygmq73eyrpps8thn0qyx39pgsy3dytxng6g6dmenc45enqtuc03ml2ryqn5wlxkgtkd4tnckaaj6cjq80p5s82syz0gr60at4yv2szssqyr9r8jlh57aa8jan0xskmkv2jqd93zty4vef3lc32ksxfq5c57hpw2j542dnjpjnya3lad2muqcqz26jcj2a9qkf",
|
|
352
|
+
},
|
|
353
|
+
]);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("should verify sorted invoices are in correct order", () => {
|
|
357
|
+
const attachments = [
|
|
358
|
+
{
|
|
359
|
+
sparkInvoice:
|
|
360
|
+
"sparkl1pgssx2r8ytpwc4exthzsg7ss7a7m69ty8p6s32j0rw65wmd38eamutyezf2ssqgjzqqe4mhekkjh9h58tq0k0522j0uj5zjfdemx76trv5szxvf6psygmq73eyrpps8zctwsyx39pgsy3dytxng6g6dmenc45enqtuc03ml2ryqn5wlxkgtkd4tnckaaj6cjq9h35spgdegtqzfc4uc8c5jcydzhrtv9dznjkwvv66835cxzr60zaczrapxfps6nlk7kqpa4xahmrm4yfm57jxu0l4326rnv4psuwr3ggk582jf9azn",
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
sparkInvoice:
|
|
364
|
+
"sparkl1pgssx2r8ytpwc4exthzsg7ss7a7m69ty8p6s32j0rw65wmd38eamutyezf2ssqgjzqqe4mhekknhnldmyjqdka8ajjdz5zjfdemx76trv5szxv36psygmq73eyrpps8thn0qyx39pgsy3dytxng6g6dmenc45enqtuc03ml2ryqn5wlxkgtkd4tnckaaj6cjq80p5s82syz0gr60at4yv2szssqyr9r8jlh57aa8jan0xskmkv2jqd93zty4vef3lc32ksxfq5c57hpw2j542dnjpjnya3lad2muqcqz26jcj2a9qkf",
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
sparkInvoice:
|
|
368
|
+
"sparkl1pgss9e7ld3nw57ejatjwq64xawwf9akm0yzn09ywfyj5wmr99t5fwrt8zftqsqgjzqqe4mhekk58qsvlkfqd7qlqthvz5zjfdemx76trv5szxve6psygmq73eyrppq8sl80qyx3xpgsy3dytxng6g6dmenc45enqtuc03ml2ryqn5wlxkgtkd4tnckaaj6cjqgq56xjqesr97xccw4u8qn8k68sddsk7rzcs5ctg27pqfu8v0mkfh350tkt4e3g8qr3qyqzcd99recq7ud6yhtvfhtj948a9944zz7q9xxrjhvgp58ut6",
|
|
369
|
+
},
|
|
370
|
+
];
|
|
371
|
+
|
|
372
|
+
const sorted = sortInvoiceAttachments(attachments);
|
|
373
|
+
|
|
374
|
+
expect(sorted).toEqual([
|
|
375
|
+
{
|
|
376
|
+
sparkInvoice:
|
|
377
|
+
"sparkl1pgss9e7ld3nw57ejatjwq64xawwf9akm0yzn09ywfyj5wmr99t5fwrt8zftqsqgjzqqe4mhekk58qsvlkfqd7qlqthvz5zjfdemx76trv5szxve6psygmq73eyrppq8sl80qyx3xpgsy3dytxng6g6dmenc45enqtuc03ml2ryqn5wlxkgtkd4tnckaaj6cjqgq56xjqesr97xccw4u8qn8k68sddsk7rzcs5ctg27pqfu8v0mkfh350tkt4e3g8qr3qyqzcd99recq7ud6yhtvfhtj948a9944zz7q9xxrjhvgp58ut6",
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
sparkInvoice:
|
|
381
|
+
"sparkl1pgssx2r8ytpwc4exthzsg7ss7a7m69ty8p6s32j0rw65wmd38eamutyezf2ssqgjzqqe4mhekkjh9h58tq0k0522j0uj5zjfdemx76trv5szxvf6psygmq73eyrpps8zctwsyx39pgsy3dytxng6g6dmenc45enqtuc03ml2ryqn5wlxkgtkd4tnckaaj6cjq9h35spgdegtqzfc4uc8c5jcydzhrtv9dznjkwvv66835cxzr60zaczrapxfps6nlk7kqpa4xahmrm4yfm57jxu0l4326rnv4psuwr3ggk582jf9azn",
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
sparkInvoice:
|
|
385
|
+
"sparkl1pgssx2r8ytpwc4exthzsg7ss7a7m69ty8p6s32j0rw65wmd38eamutyezf2ssqgjzqqe4mhekknhnldmyjqdka8ajjdz5zjfdemx76trv5szxv36psygmq73eyrpps8thn0qyx39pgsy3dytxng6g6dmenc45enqtuc03ml2ryqn5wlxkgtkd4tnckaaj6cjq80p5s82syz0gr60at4yv2szssqyr9r8jlh57aa8jan0xskmkv2jqd93zty4vef3lc32ksxfq5c57hpw2j542dnjpjnya3lad2muqcqz26jcj2a9qkf",
|
|
386
|
+
},
|
|
387
|
+
]);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it("should return undefined for undefined input", () => {
|
|
391
|
+
expect(sortInvoiceAttachments(undefined)).toBeUndefined();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it("should return empty array for empty input", () => {
|
|
395
|
+
expect(sortInvoiceAttachments([])).toEqual([]);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("should handle single invoice", () => {
|
|
399
|
+
const testInvoice = TEST_INVOICE_ATTACHMENTS[0];
|
|
400
|
+
if (!testInvoice) throw new Error("Test invoice not found");
|
|
401
|
+
|
|
402
|
+
const single = [
|
|
403
|
+
{
|
|
404
|
+
sparkInvoice: testInvoice.sparkInvoice,
|
|
405
|
+
},
|
|
406
|
+
];
|
|
407
|
+
const sorted = sortInvoiceAttachments(single);
|
|
408
|
+
expect(sorted).toEqual(single);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
@@ -1118,58 +1118,11 @@ export function sortInvoiceAttachments(
|
|
|
1118
1118
|
return attachments;
|
|
1119
1119
|
}
|
|
1120
1120
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
const attachment = attachments[i];
|
|
1126
|
-
if (!attachment) {
|
|
1127
|
-
throw new SparkValidationError(
|
|
1128
|
-
`invoice attachment at index ${i} cannot be null`,
|
|
1129
|
-
{
|
|
1130
|
-
field: `invoiceAttachments[${i}]`,
|
|
1131
|
-
index: i,
|
|
1132
|
-
},
|
|
1133
|
-
);
|
|
1134
|
-
}
|
|
1135
|
-
const invoice = attachment.sparkInvoice;
|
|
1136
|
-
|
|
1137
|
-
let idBytes: Uint8Array | undefined;
|
|
1138
|
-
try {
|
|
1139
|
-
const decoded = bech32m.decode(invoice as any, 500);
|
|
1140
|
-
const payload = SparkAddress.decode(bech32m.fromWords(decoded.words));
|
|
1141
|
-
if (!payload.sparkInvoiceFields || !payload.sparkInvoiceFields.id) {
|
|
1142
|
-
throw new Error("missing spark invoice fields or id");
|
|
1143
|
-
}
|
|
1144
|
-
idBytes = payload.sparkInvoiceFields.id;
|
|
1145
|
-
} catch (err) {
|
|
1146
|
-
throw new SparkValidationError(`invalid invoice at ${i}`, {
|
|
1147
|
-
field: `invoiceAttachments[${i}].sparkInvoice`,
|
|
1148
|
-
index: i,
|
|
1149
|
-
value: invoice,
|
|
1150
|
-
error: err,
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
if (!idBytes || idBytes.length !== 16) {
|
|
1154
|
-
throw new SparkValidationError(`invalid invoice id at ${i}`, {
|
|
1155
|
-
field: `invoiceAttachments[${i}].sparkInvoice`,
|
|
1156
|
-
index: i,
|
|
1157
|
-
});
|
|
1158
|
-
}
|
|
1159
|
-
keyed.push({ id: idBytes, attachment });
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
// Sort by UUID bytes (lexicographically)
|
|
1163
|
-
keyed.sort((a, b) => {
|
|
1164
|
-
for (let j = 0; j < a.id.length && j < b.id.length; j++) {
|
|
1165
|
-
const av = a.id[j] as number;
|
|
1166
|
-
const bv = b.id[j] as number;
|
|
1167
|
-
if (av !== bv) return av - bv;
|
|
1168
|
-
}
|
|
1169
|
-
return a.id.length - b.id.length;
|
|
1121
|
+
return [...attachments].sort((a, b) => {
|
|
1122
|
+
const invoiceA = a.sparkInvoice;
|
|
1123
|
+
const invoiceB = b.sparkInvoice;
|
|
1124
|
+
return invoiceA < invoiceB ? -1 : invoiceA > invoiceB ? 1 : 0;
|
|
1170
1125
|
});
|
|
1171
|
-
|
|
1172
|
-
return keyed.map((k) => k.attachment);
|
|
1173
1126
|
}
|
|
1174
1127
|
|
|
1175
1128
|
export async function hashTokenTransactionV3(
|
package/src/utils/transaction.ts
CHANGED
|
@@ -292,8 +292,7 @@ interface RefundTxWithSequenceParams extends RefundTxParams {
|
|
|
292
292
|
enforceTimelocks?: boolean;
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
interface RefundTxWithSequenceAndConnectorOutputParams
|
|
296
|
-
extends RefundTxWithSequenceParams {
|
|
295
|
+
interface RefundTxWithSequenceAndConnectorOutputParams extends RefundTxWithSequenceParams {
|
|
297
296
|
connectorOutput: TransactionInput;
|
|
298
297
|
}
|
|
299
298
|
|