@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.
Files changed (81) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/bare/index.cjs +1608 -3635
  3. package/dist/bare/index.d.cts +27 -435
  4. package/dist/bare/index.d.ts +27 -435
  5. package/dist/bare/index.js +1608 -3634
  6. package/dist/{chunk-RU434ZAE.js → chunk-F3BFSHVR.js} +357 -391
  7. package/dist/{chunk-UYEB2VPG.js → chunk-IOIEBLMK.js} +7 -1
  8. package/dist/{chunk-EU3I7GFB.js → chunk-STB6WMU7.js} +1 -1
  9. package/dist/{chunk-JE3MXMPW.js → chunk-UTECVGQQ.js} +93 -202
  10. package/dist/{chunk-ZP6Z6DFX.js → chunk-YFVVYZCS.js} +37 -5
  11. package/dist/{client-D1dLzWu0.d.ts → client-C9kc4cog.d.cts} +9 -3
  12. package/dist/{client-CVn0R_eM.d.cts → client-eyjf4knu.d.ts} +9 -3
  13. package/dist/graphql/objects/index.cjs +7 -1
  14. package/dist/graphql/objects/index.d.cts +3 -3
  15. package/dist/graphql/objects/index.d.ts +3 -3
  16. package/dist/graphql/objects/index.js +1 -1
  17. package/dist/index.browser.d.ts +27 -435
  18. package/dist/index.browser.js +1613 -3639
  19. package/dist/index.node.cjs +1613 -3640
  20. package/dist/index.node.d.cts +7 -8
  21. package/dist/index.node.d.ts +7 -8
  22. package/dist/index.node.js +5 -7
  23. package/dist/native/index.react-native.cjs +1613 -3640
  24. package/dist/native/index.react-native.d.cts +27 -435
  25. package/dist/native/index.react-native.d.ts +27 -435
  26. package/dist/native/index.react-native.js +1613 -3639
  27. package/dist/proto/spark.cjs +93 -202
  28. package/dist/proto/spark.d.cts +1 -1
  29. package/dist/proto/spark.d.ts +1 -1
  30. package/dist/proto/spark.js +1 -1
  31. package/dist/proto/spark_token.cjs +36 -4
  32. package/dist/proto/spark_token.d.cts +4 -1
  33. package/dist/proto/spark_token.d.ts +4 -1
  34. package/dist/proto/spark_token.js +2 -2
  35. package/dist/{spark-2Fxnvl8K.d.cts → spark-d6w3BLGZ.d.cts} +10 -328
  36. package/dist/{spark-2Fxnvl8K.d.ts → spark-d6w3BLGZ.d.ts} +10 -328
  37. package/dist/{spark-wallet.node-DlhZiDgY.d.ts → spark-wallet.node-MReThHBY.d.ts} +6 -7
  38. package/dist/{spark-wallet.node-xKJXzAEd.d.cts → spark-wallet.node-eR0svGws.d.cts} +6 -7
  39. package/dist/tests/test-utils.cjs +409 -2429
  40. package/dist/tests/test-utils.d.cts +3 -3
  41. package/dist/tests/test-utils.d.ts +3 -3
  42. package/dist/tests/test-utils.js +5 -5
  43. package/dist/types/index.cjs +100 -203
  44. package/dist/types/index.d.cts +2 -2
  45. package/dist/types/index.d.ts +2 -2
  46. package/dist/types/index.js +3 -3
  47. package/package.json +3 -3
  48. package/src/graphql/client.ts +36 -1
  49. package/src/graphql/objects/LightningSendRequestStatus.ts +25 -13
  50. package/src/proto/common.ts +1 -1
  51. package/src/proto/google/protobuf/descriptor.ts +1 -1
  52. package/src/proto/google/protobuf/duration.ts +1 -1
  53. package/src/proto/google/protobuf/empty.ts +1 -1
  54. package/src/proto/google/protobuf/timestamp.ts +1 -1
  55. package/src/proto/mock.ts +1 -1
  56. package/src/proto/spark.ts +113 -446
  57. package/src/proto/spark_authn.ts +1 -1
  58. package/src/proto/spark_token.ts +41 -2
  59. package/src/proto/validate/validate.ts +1 -1
  60. package/src/services/connection/connection.ts +23 -60
  61. package/src/services/coop-exit.ts +3 -5
  62. package/src/services/deposit.ts +1 -1
  63. package/src/services/lightning.ts +1 -1
  64. package/src/services/signing.ts +5 -6
  65. package/src/services/transfer.ts +250 -240
  66. package/src/services/wallet-config.ts +22 -5
  67. package/src/spark-wallet/proto-descriptors.ts +1 -1
  68. package/src/spark-wallet/proto-reflection.ts +0 -2
  69. package/src/spark-wallet/spark-wallet.ts +2 -2
  70. package/src/spark_descriptors.pb +0 -0
  71. package/src/tests/bufbuild-reflection.test.ts +2 -3
  72. package/src/tests/integration/coop-exit.test.ts +6 -1
  73. package/src/tests/integration/htlc.test.ts +5 -0
  74. package/src/tests/integration/lightning.test.ts +24 -4
  75. package/src/tests/integration/time-sync.test.ts +18 -0
  76. package/src/tests/integration/transfer.test.ts +42 -7
  77. package/src/tests/ssp-client-retry.test.ts +161 -0
  78. package/src/tests/token-hashing.test.ts +92 -0
  79. package/src/utils/token-hashing.ts +4 -51
  80. package/src/utils/transaction.ts +1 -2
  81. 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: leaf.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: leaf.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: leaf.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: leaf.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: leaf.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: leaf.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: rootNode,
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: leaf.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: rootNode,
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: leaf.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: leaf.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
- type Keyed = { id: Uint8Array; attachment: InvoiceAttachment };
1122
- const keyed: Keyed[] = [];
1123
-
1124
- for (let i = 0; i < attachments.length; i++) {
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(
@@ -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