@buildonspark/spark-sdk 0.2.7 → 0.2.9

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 (88) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/bare/index.cjs +1683 -1901
  3. package/dist/bare/index.d.cts +242 -292
  4. package/dist/bare/index.d.ts +242 -292
  5. package/dist/bare/index.js +1604 -1829
  6. package/dist/{chunk-7LY7PJQL.js → chunk-23BBEC25.js} +14 -5
  7. package/dist/{chunk-R5VUHUJR.js → chunk-5Y7YILMA.js} +4153 -3728
  8. package/dist/{chunk-GIDAHHDB.js → chunk-6CMNEDBK.js} +217 -9
  9. package/dist/{chunk-J24LM4RO.js → chunk-76SYPHOC.js} +1 -1
  10. package/dist/{chunk-2HD3USKS.js → chunk-A5M55UR3.js} +0 -24
  11. package/dist/{client-BmnZ1xDg.d.cts → client-B9CAWKWz.d.cts} +1 -1
  12. package/dist/{client-DmjOifnt.d.ts → client-Dd3QnxQu.d.ts} +1 -1
  13. package/dist/debug.cjs +1680 -1957
  14. package/dist/debug.d.cts +13 -8
  15. package/dist/debug.d.ts +13 -8
  16. package/dist/debug.js +6 -8
  17. package/dist/graphql/objects/index.d.cts +3 -3
  18. package/dist/graphql/objects/index.d.ts +3 -3
  19. package/dist/index.cjs +1729 -1948
  20. package/dist/index.d.cts +18 -6
  21. package/dist/index.d.ts +18 -6
  22. package/dist/index.js +17 -8
  23. package/dist/index.node.cjs +1729 -1948
  24. package/dist/index.node.d.cts +7 -6
  25. package/dist/index.node.d.ts +7 -6
  26. package/dist/index.node.js +22 -6
  27. package/dist/native/index.cjs +1723 -1949
  28. package/dist/native/index.d.cts +80 -125
  29. package/dist/native/index.d.ts +80 -125
  30. package/dist/native/index.js +1652 -1884
  31. package/dist/proto/spark.cjs +0 -24
  32. package/dist/proto/spark.d.cts +1 -1
  33. package/dist/proto/spark.d.ts +1 -1
  34. package/dist/proto/spark.js +1 -1
  35. package/dist/proto/spark_token.cjs +221 -8
  36. package/dist/proto/spark_token.d.cts +25 -2
  37. package/dist/proto/spark_token.d.ts +25 -2
  38. package/dist/proto/spark_token.js +12 -2
  39. package/dist/{spark-B305mDNB.d.cts → spark-CtGJPkx4.d.cts} +3 -31
  40. package/dist/{spark-B305mDNB.d.ts → spark-CtGJPkx4.d.ts} +3 -31
  41. package/dist/{spark-wallet-BdwARy70.d.cts → spark-wallet-Cp3yv6cK.d.ts} +40 -31
  42. package/dist/{spark-wallet-enp968Uc.d.ts → spark-wallet-yc2KhsVY.d.cts} +40 -31
  43. package/dist/{spark-wallet.node-CtpJlYBs.d.cts → spark-wallet.node-D0Qw5Wb4.d.cts} +1 -1
  44. package/dist/{spark-wallet.node-DqWcsNb6.d.ts → spark-wallet.node-D4IovOHu.d.ts} +1 -1
  45. package/dist/tests/test-utils.cjs +483 -1120
  46. package/dist/tests/test-utils.d.cts +9 -5
  47. package/dist/tests/test-utils.d.ts +9 -5
  48. package/dist/tests/test-utils.js +5 -6
  49. package/dist/{token-transactions-3-pVToE0.d.cts → token-transactions-0nmR9mQO.d.ts} +17 -12
  50. package/dist/{token-transactions-84Hp0hGz.d.ts → token-transactions-CwhlOgIP.d.cts} +17 -12
  51. package/dist/types/index.cjs +0 -24
  52. package/dist/types/index.d.cts +2 -2
  53. package/dist/types/index.d.ts +2 -2
  54. package/dist/types/index.js +2 -2
  55. package/dist/{xchain-address-BtuJEbzG.d.cts → xchain-address-BPwpnmuY.d.ts} +9 -3
  56. package/dist/{xchain-address-Q1BrcwID.d.ts → xchain-address-CNQEwLjR.d.cts} +9 -3
  57. package/package.json +1 -1
  58. package/src/constants.ts +7 -1
  59. package/src/debug.ts +1 -1
  60. package/src/proto/spark.ts +2 -48
  61. package/src/proto/spark_token.ts +255 -7
  62. package/src/services/token-transactions.ts +92 -44
  63. package/src/services/transfer.ts +20 -17
  64. package/src/services/wallet-config.ts +2 -0
  65. package/src/signer/signer.react-native.ts +0 -2
  66. package/src/spark-wallet/spark-wallet.browser.ts +9 -8
  67. package/src/spark-wallet/spark-wallet.node.ts +8 -4
  68. package/src/spark-wallet/spark-wallet.ts +427 -229
  69. package/src/tests/address.test.ts +87 -1
  70. package/src/tests/integration/retry.test.ts +78 -0
  71. package/src/tests/integration/ssp/static-deposit-validation.test.ts +1 -1
  72. package/src/tests/integration/transfer.test.ts +285 -1
  73. package/src/tests/integration/wallet.test.ts +160 -0
  74. package/src/tests/{tokens.test.ts → token-hashing.test.ts} +150 -162
  75. package/src/tests/token-outputs.test.ts +194 -0
  76. package/src/tests/utils/spark-testing-wallet.ts +16 -8
  77. package/src/utils/address.ts +152 -11
  78. package/src/utils/invoice-hashing.test.ts +235 -0
  79. package/src/utils/invoice-hashing.ts +227 -0
  80. package/src/utils/mempool.ts +6 -0
  81. package/src/utils/retry.ts +116 -0
  82. package/src/utils/token-hashing.ts +566 -0
  83. package/src/utils/token-transactions.ts +9 -5
  84. package/dist/chunk-7N6R7G3E.js +0 -7
  85. package/dist/spark-wallet.browser-BYlprQpX.d.ts +0 -12
  86. package/dist/spark-wallet.browser-CVI2Ss3u.d.cts +0 -12
  87. package/src/services/tree-creation.ts +0 -893
  88. package/src/tests/integration/tree-creation.test.ts +0 -46
@@ -10,7 +10,17 @@ import {
10
10
  hexToBytes,
11
11
  numberToVarBytesBE,
12
12
  } from "@noble/curves/abstract/utils";
13
- import { encodeSparkAddress, decodeSparkAddress } from "../utils/address.js";
13
+ import {
14
+ encodeSparkAddress,
15
+ decodeSparkAddress,
16
+ getNetworkFromSparkAddress,
17
+ encodeSparkAddressWithSignature,
18
+ SparkAddressData,
19
+ bech32mDecode,
20
+ SparkAddressFormat,
21
+ } from "../utils/address.js";
22
+ import { SparkAddress } from "../proto/spark.js";
23
+ import { bech32m } from "@scure/base";
14
24
 
15
25
  describe("Spark Invoice Encode/Decode", () => {
16
26
  const testCases = [
@@ -213,3 +223,79 @@ describe("Spark Invoice Encode/Decode", () => {
213
223
  });
214
224
  });
215
225
  });
226
+
227
+ describe("getNetworkFromSparkAddress", () => {
228
+ test("REGTEST", () => {
229
+ const network = getNetworkFromSparkAddress(
230
+ "sprt1pgssx63fa5g6uyv450rajp5ndwy9laxzpsp9e37su58jddmcdsvhgm5n7y0ud6",
231
+ );
232
+ expect(network).toBe("REGTEST");
233
+ });
234
+ test("MAINNET", () => {
235
+ const network = getNetworkFromSparkAddress(
236
+ "sp1pgssxwh6hznfdc3c0cuqrhgttder539d52a0rqcf34amge69huh664gd2ew787",
237
+ );
238
+ expect(network).toBe("MAINNET");
239
+ });
240
+ });
241
+
242
+ describe("knownSparkAddress", () => {
243
+ test("known spark address decodes and encodes to the same address", () => {
244
+ const address =
245
+ "sprt1pgss8stv8nfkamyea7mtc8werley55anfnnpgtnglff0wmxwm52mkyk6zfeqsqgjzqqe3dvr6e48l2alnpagf7ny3vlj5pr5v4ehgv3pqwd7wxx3awkku9p3epk73na6hcf9220h8kue2tmlkqx8tcrfpsf5ywsvpzgd9px9qcgvpzy8ecp35fg2yq4r39r4njq3slgcul7laarh9sndex9uejz7vwrcrz4g7n4egvwt5yspvsdyped46sflczvrzh0jzksgqnvaqlk02cz4vkwjrkwuep9zsrz5vmjp7mqxq7762tfjczy07at2fvzd7cgk2sqsxrmqdxnpy464rmq2nzdqzpuhme";
246
+ const decoded = bech32mDecode(address as SparkAddressFormat);
247
+ const payload = SparkAddress.decode(bech32m.fromWords(decoded.words));
248
+
249
+ const { identityPublicKey, sparkInvoiceFields, signature } = payload;
250
+
251
+ const sparkAddressData: SparkAddressData = {
252
+ identityPublicKey: bytesToHex(identityPublicKey),
253
+ network: "REGTEST",
254
+ sparkInvoiceFields: sparkInvoiceFields,
255
+ };
256
+ const reEncoded = encodeSparkAddressWithSignature(
257
+ sparkAddressData,
258
+ signature,
259
+ );
260
+ expect(reEncoded).toBe(address);
261
+ });
262
+
263
+ test("known spark address decodes to expected fields", () => {
264
+ const address =
265
+ "sprt1pgss8stv8nfkamyea7mtc8werley55anfnnpgtnglff0wmxwm52mkyk6zfeqsqgjzqqe3dvr6e48l2alnpagf7ny3vl35fg2yq4r39r4njq3slgcul7laarh9sndex9uejz7vwrcrz4g7n4egvwt5yspvs4qgar9wd6ryggrn0n3350t44hpgvwgdh5vlw47zf2jnaeahx2j7lasp367q6gvzdpr5rqgjrfgf3gxzrqg3p7wqvdyped46sflczvrzh0jzksgqnvaqlk02cz4vkwjrkwuep9zsrz5vmjp7mqxq7762tfjczy07at2fvzd7cgk2sqsxrmqdxnpy464rmq2nzdqneal34";
266
+
267
+ const decoded = decodeSparkAddress(address, "REGTEST");
268
+
269
+ expect(decoded.network).toBe("REGTEST");
270
+ expect(decoded.identityPublicKey).toBe(
271
+ "03c16c3cd36eec99efb6bc1dd91ff24a53b34ce6142e68fa52f76ccedd15bb12da",
272
+ );
273
+
274
+ const f = decoded.sparkInvoiceFields!;
275
+ expect(f.version).toBe(1);
276
+ expect(f.id).toBe("0198b583-d66a-7fab-bf98-7a84fa648b3f");
277
+
278
+ expect(f.paymentType?.type).toBe("tokens");
279
+ expect(
280
+ f.paymentType && "tokenIdentifier" in f.paymentType
281
+ ? f.paymentType.tokenIdentifier
282
+ : undefined,
283
+ ).toBe("2a3894759c81187d18e7fdfef4772c26dc98bccc85e6387818aa8f4eb9431cba");
284
+ expect(
285
+ f.paymentType && "amount" in f.paymentType
286
+ ? f.paymentType.amount
287
+ : undefined,
288
+ ).toBe(100n);
289
+
290
+ expect(f.memo).toBe("test");
291
+ expect(f.senderPublicKey).toBe(
292
+ "039be718d1ebad6e1431c86de8cfbabe125529f73db9952f7fb00c75e0690c1342",
293
+ );
294
+
295
+ expect(f.expiryTime?.toISOString()).toBe("2025-08-17T00:57:52.969Z");
296
+
297
+ expect(decoded.signature).toBe(
298
+ "e5b5d413fc098315df215a0804d9d07ecf56055659d21d9dcc84a280c5466e41f6c0607bda52d32c088ff756a4b04df61165401030f6069a61257551ec0a989a",
299
+ );
300
+ });
301
+ });
@@ -0,0 +1,78 @@
1
+ import { describe, expect, it, jest } from "@jest/globals";
2
+ import { DEFAULT_RETRY_CONFIG, withRetry } from "../../utils/retry.js";
3
+
4
+ describe("Retry Test", () => {
5
+ beforeEach(() => {
6
+ jest.useFakeTimers();
7
+ });
8
+
9
+ afterEach(() => {
10
+ jest.useRealTimers();
11
+ });
12
+
13
+ it("should succeed on first attempt", async () => {
14
+ const operation = jest
15
+ .fn<() => Promise<string>>()
16
+ .mockResolvedValue("success");
17
+ const onRetry = jest.fn<() => Promise<void>>();
18
+ const onError = jest
19
+ .fn<() => string | undefined>()
20
+ .mockReturnValue(undefined);
21
+
22
+ const result = await withRetry<string>(operation, {
23
+ callbacks: {
24
+ onRetry,
25
+ onError,
26
+ },
27
+ });
28
+
29
+ expect(result).toBe("success");
30
+ expect(operation).toHaveBeenCalledTimes(1);
31
+ expect(onRetry).not.toHaveBeenCalled();
32
+ expect(onError).not.toHaveBeenCalled();
33
+ });
34
+
35
+ it("should retry on failure and then succeed", async () => {
36
+ const operation = jest
37
+ .fn<() => Promise<string>>()
38
+ .mockRejectedValueOnce(new Error("Network error"))
39
+ .mockRejectedValueOnce(new Error("Network error"))
40
+ .mockResolvedValue("success");
41
+
42
+ const onRetry = jest.fn<() => Promise<void>>();
43
+ const onError = jest
44
+ .fn<() => string | undefined>()
45
+ .mockReturnValue(undefined);
46
+
47
+ const promise = withRetry<string>(operation, {
48
+ callbacks: {
49
+ onRetry,
50
+ onError,
51
+ },
52
+ });
53
+
54
+ jest.runAllTimersAsync();
55
+
56
+ const result = await promise;
57
+
58
+ expect(result).toBe("success");
59
+ expect(operation).toHaveBeenCalledTimes(3);
60
+ expect(onRetry).toHaveBeenCalledTimes(2);
61
+ expect(onError).toHaveBeenCalledTimes(2);
62
+ }, 10000);
63
+
64
+ it("should fail after max attempts", async () => {
65
+ let operation = jest.fn<() => Promise<string>>();
66
+
67
+ for (let i = 0; i < DEFAULT_RETRY_CONFIG.maxAttempts; i++) {
68
+ operation = operation.mockRejectedValueOnce(new Error("Network error"));
69
+ }
70
+
71
+ const promise = withRetry<string>(operation);
72
+
73
+ jest.runAllTimersAsync();
74
+
75
+ await expect(promise).rejects.toThrow("Network error");
76
+ expect(operation).toHaveBeenCalledTimes(DEFAULT_RETRY_CONFIG.maxAttempts);
77
+ });
78
+ });
@@ -71,7 +71,7 @@ describe("SSP static deposit validation tests", () => {
71
71
  // Invalid transaction ID
72
72
  await expect(
73
73
  userWallet.getClaimStaticDepositQuote("invalid-txid", vout!),
74
- ).rejects.toThrow("Invalid transaction ID:");
74
+ ).rejects.toThrow(/InvalidInputException/);
75
75
 
76
76
  await new Promise((resolve) => setTimeout(resolve, 5000));
77
77
 
@@ -745,7 +745,7 @@ describe.each(walletTypes)(
745
745
  // "should not allow transfer from %s to %s network due to address validation",
746
746
  // async (sourceNetwork, targetNetwork) => {
747
747
  // const sourceOptions: ConfigOptions = {
748
- // network: sourceNetwork,
748
+ // network: sourceNetwork
749
749
  // };
750
750
  // const targetOptions: ConfigOptions = {
751
751
  // network: targetNetwork,
@@ -1036,4 +1036,288 @@ describe.each(walletTypes)("transfer v2", ({ name, Signer, createTree }) => {
1036
1036
  const newBalance = await sdk.getBalance();
1037
1037
  expect(newBalance.balance).toBe(1000n);
1038
1038
  });
1039
+
1040
+ it(`${name} - test transfer with retry`, async () => {
1041
+ const faucet = BitcoinFaucet.getInstance();
1042
+
1043
+ const { wallet: sdk } = await SparkWalletTesting.initialize({
1044
+ options: {
1045
+ network: "LOCAL",
1046
+ },
1047
+ signer: new Signer(),
1048
+ });
1049
+
1050
+ const depositResp = await sdk.getSingleUseDepositAddress();
1051
+ if (!depositResp) {
1052
+ throw new RPCError("Deposit address not found", {
1053
+ method: "getDepositAddress",
1054
+ });
1055
+ }
1056
+
1057
+ const signedTx = await faucet.sendToAddress(depositResp, 1_000n);
1058
+
1059
+ await sdk.claimDeposit(signedTx.id);
1060
+
1061
+ const balance = await sdk.getBalance();
1062
+ expect(balance.balance).toBe(1_000n);
1063
+
1064
+ const { wallet: sdk2 } = await SparkWalletTesting.initialize({
1065
+ options: {
1066
+ network: "LOCAL",
1067
+ },
1068
+ signer: new Signer(),
1069
+ });
1070
+
1071
+ await sdk.transfer({
1072
+ amountSats: 1000,
1073
+ receiverSparkAddress: await sdk2.getSparkAddress(),
1074
+ });
1075
+
1076
+ const pendingTransfers = await sdk2.queryPendingTransfers();
1077
+ expect(pendingTransfers.transfers.length).toBe(1);
1078
+ const transfer = pendingTransfers.transfers[0]!;
1079
+
1080
+ const originalClaimTransferCore = (sdk2 as any).claimTransferCore.bind(
1081
+ sdk2,
1082
+ );
1083
+ const claimTransferCoreSpy = jest
1084
+ .spyOn(sdk2 as any, "claimTransferCore")
1085
+ .mockRejectedValueOnce(new Error("Network error"))
1086
+ .mockImplementation(async (transfer) => {
1087
+ return await originalClaimTransferCore(transfer);
1088
+ });
1089
+
1090
+ await (sdk2 as any).claimTransfer({ transfer });
1091
+
1092
+ expect(claimTransferCoreSpy).toHaveBeenCalledTimes(2);
1093
+ expect((await sdk2.getBalance()).balance).toBe(1000n);
1094
+ });
1095
+
1096
+ it(`${name} - test claiming already claimed transfer`, async () => {
1097
+ const faucet = BitcoinFaucet.getInstance();
1098
+
1099
+ const { wallet: sdk } = await SparkWalletTesting.initialize({
1100
+ options: {
1101
+ network: "LOCAL",
1102
+ },
1103
+ signer: new Signer(),
1104
+ });
1105
+
1106
+ const depositResp = await sdk.getSingleUseDepositAddress();
1107
+
1108
+ if (!depositResp) {
1109
+ throw new RPCError("Deposit address not found", {
1110
+ method: "getDepositAddress",
1111
+ });
1112
+ }
1113
+
1114
+ const signedTx = await faucet.sendToAddress(depositResp, 1_000n);
1115
+
1116
+ await sdk.claimDeposit(signedTx.id);
1117
+
1118
+ const balance = await sdk.getBalance();
1119
+ expect(balance.balance).toBe(1_000n);
1120
+
1121
+ const { wallet: sdk2 } = await SparkWalletTesting.initialize({
1122
+ options: {
1123
+ network: "LOCAL",
1124
+ },
1125
+ signer: new Signer(),
1126
+ });
1127
+
1128
+ await sdk.transfer({
1129
+ amountSats: 1000,
1130
+ receiverSparkAddress: await sdk2.getSparkAddress(),
1131
+ });
1132
+
1133
+ const pendingTransfers = await sdk2.queryPendingTransfers();
1134
+ expect(pendingTransfers.transfers.length).toBe(1);
1135
+ const transfer = pendingTransfers.transfers[0]!;
1136
+
1137
+ await (sdk2 as any).claimTransfer({ transfer });
1138
+
1139
+ const claimTransferCoreSpy = jest.spyOn(sdk2 as any, "claimTransferCore");
1140
+
1141
+ const claim1 = await (sdk2 as any).claimTransfer({
1142
+ transfer: {
1143
+ ...transfer,
1144
+ status: TransferStatus.TRANSFER_STATUS_SENDER_KEY_TWEAKED,
1145
+ },
1146
+ });
1147
+ expect(claim1.length).toBe(0);
1148
+
1149
+ const claim2 = await (sdk2 as any).claimTransfer({
1150
+ transfer: {
1151
+ ...transfer,
1152
+ status: TransferStatus.TRANSFER_STATUS_RECEIVER_KEY_TWEAKED,
1153
+ },
1154
+ });
1155
+ expect(claim2.length).toBe(0);
1156
+
1157
+ const claim3 = await (sdk2 as any).claimTransfer({
1158
+ transfer,
1159
+ });
1160
+
1161
+ expect(claim3.length).toBe(0);
1162
+
1163
+ // Expect 3 because we call claimTransfer 3 times and we expect there to be 0 retries
1164
+ expect(claimTransferCoreSpy).toHaveBeenCalledTimes(3);
1165
+ });
1166
+
1167
+ it(`${name} - test querying updated transfer after error`, async () => {
1168
+ const faucet = BitcoinFaucet.getInstance();
1169
+
1170
+ const options: ConfigOptions = {
1171
+ network: "LOCAL",
1172
+ };
1173
+
1174
+ const { wallet: sdk } = await SparkWalletTesting.initialize({
1175
+ options,
1176
+ signer: new Signer(),
1177
+ });
1178
+
1179
+ const depositResp = await sdk.getSingleUseDepositAddress();
1180
+
1181
+ if (!depositResp) {
1182
+ throw new RPCError("Deposit address not found", {
1183
+ method: "getDepositAddress",
1184
+ });
1185
+ }
1186
+
1187
+ const signedTx = await faucet.sendToAddress(depositResp, 1_000n);
1188
+
1189
+ await sdk.claimDeposit(signedTx.id);
1190
+
1191
+ const balance = await sdk.getBalance();
1192
+ expect(balance.balance).toBe(1_000n);
1193
+
1194
+ const { wallet: sdk2 } = await SparkWalletTesting.initialize({
1195
+ options: {
1196
+ network: "LOCAL",
1197
+ },
1198
+ signer: new Signer(),
1199
+ });
1200
+
1201
+ const receiverConfigService = new WalletConfigService(
1202
+ options,
1203
+ sdk2.getSigner(),
1204
+ );
1205
+ const receiverConnectionManager = new ConnectionManager(
1206
+ receiverConfigService,
1207
+ );
1208
+ const receiverSigningService = new SigningService(receiverConfigService);
1209
+ const receiverTransferService = new TransferService(
1210
+ receiverConfigService,
1211
+ receiverConnectionManager,
1212
+ receiverSigningService,
1213
+ );
1214
+
1215
+ await sdk.transfer({
1216
+ amountSats: 1000,
1217
+ receiverSparkAddress: await sdk2.getSparkAddress(),
1218
+ });
1219
+
1220
+ const pendingTransfers = await sdk2.queryPendingTransfers();
1221
+ expect(pendingTransfers.transfers.length).toBe(1);
1222
+ const transfer = pendingTransfers.transfers[0]!;
1223
+
1224
+ const leaves: LeafKeyTweak[] = transfer.leaves.map((leaf) => ({
1225
+ leaf: {
1226
+ ...leaf.leaf!,
1227
+ refundTx: leaf.intermediateRefundTx,
1228
+ directRefundTx: leaf.intermediateDirectRefundTx,
1229
+ directFromCpfpRefundTx: leaf.intermediateDirectFromCpfpRefundTx,
1230
+ },
1231
+ keyDerivation: {
1232
+ type: KeyDerivationType.ECIES,
1233
+ path: leaf.secretCipher,
1234
+ },
1235
+ newKeyDerivation: {
1236
+ type: KeyDerivationType.LEAF,
1237
+ path: leaf.leaf!.id,
1238
+ },
1239
+ }));
1240
+
1241
+ await receiverTransferService.claimTransferTweakKeys(transfer, leaves);
1242
+
1243
+ const claimTransferCoreSpy = jest.spyOn(sdk2 as any, "claimTransferCore");
1244
+
1245
+ const res = await (sdk2 as any).claimTransfer({ transfer });
1246
+ expect(res.length).toBe(1);
1247
+
1248
+ expect(claimTransferCoreSpy).toHaveBeenCalledTimes(2);
1249
+ });
1250
+
1251
+ it(`${name} - transfer between two wallets that are using different coordinators`, async () => {
1252
+ const faucet = BitcoinFaucet.getInstance();
1253
+
1254
+ const localOperators = Object.values(getLocalSigningOperators());
1255
+ const { wallet: alice } = await SparkWalletTesting.initialize({
1256
+ options: {
1257
+ network: "LOCAL",
1258
+ coordinatorIdentifier: localOperators[0]!.identifier,
1259
+ },
1260
+ signer: new Signer(),
1261
+ });
1262
+ const depositResp = await alice.getSingleUseDepositAddress();
1263
+
1264
+ if (!depositResp) {
1265
+ throw new RPCError("Deposit address not found", {
1266
+ method: "getDepositAddress",
1267
+ });
1268
+ }
1269
+
1270
+ const signedTx = await faucet.sendToAddress(depositResp, 1_000n);
1271
+
1272
+ await faucet.mineBlocks(1);
1273
+
1274
+ await alice.claimDeposit(signedTx.id);
1275
+
1276
+ const balance = await alice.getBalance();
1277
+ expect(balance.balance).toBe(1_000n);
1278
+
1279
+ const options: ConfigOptions = {
1280
+ network: "LOCAL",
1281
+ coordinatorIdentifier: localOperators[1]!.identifier,
1282
+ };
1283
+ const { wallet: bob } = await SparkWalletTesting.initialize({
1284
+ options,
1285
+ signer: new Signer(),
1286
+ });
1287
+
1288
+ const bobConfigService = new WalletConfigService(options, bob.getSigner());
1289
+ const bobConnectionManager = new ConnectionManager(bobConfigService);
1290
+ const bobSigningService = new SigningService(bobConfigService);
1291
+
1292
+ const bobTransferService = new TransferService(
1293
+ bobConfigService,
1294
+ bobConnectionManager,
1295
+ bobSigningService,
1296
+ );
1297
+
1298
+ const sparkAddress = await bob.getSparkAddress();
1299
+
1300
+ await alice.transfer({
1301
+ amountSats: 1000,
1302
+ receiverSparkAddress: sparkAddress,
1303
+ });
1304
+
1305
+ const pendingTransfers = await bob.queryPendingTransfers();
1306
+ expect(pendingTransfers.transfers.length).toBe(1);
1307
+ const transfer = pendingTransfers.transfers[0]!;
1308
+
1309
+ const claimingNodes: LeafKeyTweak[] = transfer!.leaves.map((leaf) => ({
1310
+ leaf: leaf.leaf!,
1311
+ keyDerivation: {
1312
+ type: KeyDerivationType.ECIES,
1313
+ path: leaf.secretCipher,
1314
+ },
1315
+ newKeyDerivation: {
1316
+ type: KeyDerivationType.LEAF,
1317
+ path: leaf.leaf!.id,
1318
+ },
1319
+ }));
1320
+
1321
+ await bobTransferService.claimTransfer(transfer!, claimingNodes);
1322
+ });
1039
1323
  });
@@ -3,6 +3,18 @@ import { ConfigOptions } from "../../services/wallet-config.js";
3
3
  import { NetworkType } from "../../utils/network.js";
4
4
  import { walletTypes } from "../test-utils.js";
5
5
  import { SparkWalletTesting } from "../utils/spark-testing-wallet.js";
6
+ import { secp256k1, schnorr } from "@noble/curves/secp256k1";
7
+ import { bytesToHex } from "@noble/curves/abstract/utils";
8
+ import type { SparkSigner } from "../../signer/signer.js";
9
+ import type { Transaction } from "@scure/btc-signer";
10
+ import type {
11
+ AggregateFrostParams,
12
+ KeyDerivation,
13
+ SigningCommitmentWithOptionalNonce,
14
+ SignFrostParams,
15
+ SplitSecretWithProofsParams,
16
+ } from "../../signer/types.js";
17
+ import type { VerifiableSecretShare } from "../../utils/secret-sharing.js";
6
18
 
7
19
  describe.each(walletTypes)("wallet", ({ name, Signer }) => {
8
20
  it(`${name} - should initialize a wallet`, async () => {
@@ -79,3 +91,151 @@ describe.each(walletTypes)("wallet", ({ name, Signer }) => {
79
91
  }
80
92
  });
81
93
  });
94
+
95
+ class PreinitializedTestSigner implements SparkSigner {
96
+ private readonly identityPrivateKey: Uint8Array;
97
+ private readonly depositPrivateKey: Uint8Array;
98
+
99
+ constructor(params?: {
100
+ identityPrivateKey?: Uint8Array;
101
+ depositPrivateKey?: Uint8Array;
102
+ }) {
103
+ this.identityPrivateKey =
104
+ params?.identityPrivateKey ?? secp256k1.utils.randomPrivateKey();
105
+ this.depositPrivateKey =
106
+ params?.depositPrivateKey ?? secp256k1.utils.randomPrivateKey();
107
+ }
108
+
109
+ async getIdentityPublicKey(): Promise<Uint8Array> {
110
+ return secp256k1.getPublicKey(this.identityPrivateKey);
111
+ }
112
+ async getDepositSigningKey(): Promise<Uint8Array> {
113
+ return secp256k1.getPublicKey(this.depositPrivateKey);
114
+ }
115
+ async getStaticDepositSigningKey(_idx: number): Promise<Uint8Array> {
116
+ // Not used in this test; return a valid pubkey
117
+ return secp256k1.getPublicKey(secp256k1.utils.randomPrivateKey());
118
+ }
119
+ async getStaticDepositSecretKey(_idx: number): Promise<Uint8Array> {
120
+ // Not used in this test
121
+ return secp256k1.utils.randomPrivateKey();
122
+ }
123
+
124
+ async generateMnemonic(): Promise<string> {
125
+ throw new Error("Not implemented in PreinitializedTestSigner");
126
+ }
127
+ async mnemonicToSeed(_mnemonic: string): Promise<Uint8Array> {
128
+ throw new Error("Not implemented in PreinitializedTestSigner");
129
+ }
130
+ async createSparkWalletFromSeed(
131
+ _seed: Uint8Array | string,
132
+ _accountNumber?: number,
133
+ ): Promise<string> {
134
+ throw new Error("Not implemented in PreinitializedTestSigner");
135
+ }
136
+ async getPublicKeyFromDerivation(
137
+ _keyDerivation?: KeyDerivation,
138
+ ): Promise<Uint8Array> {
139
+ throw new Error("Not implemented in PreinitializedTestSigner");
140
+ }
141
+
142
+ async signSchnorrWithIdentityKey(message: Uint8Array): Promise<Uint8Array> {
143
+ return schnorr.sign(message, this.identityPrivateKey);
144
+ }
145
+
146
+ async subtractPrivateKeysGivenDerivationPaths(
147
+ _first: string,
148
+ _second: string,
149
+ ): Promise<Uint8Array> {
150
+ throw new Error("Not implemented in PreinitializedTestSigner");
151
+ }
152
+ async subtractAndSplitSecretWithProofsGivenDerivations(
153
+ _params: Omit<SplitSecretWithProofsParams, "secret"> & {
154
+ first: KeyDerivation;
155
+ second?: KeyDerivation | undefined;
156
+ },
157
+ ): Promise<VerifiableSecretShare[]> {
158
+ throw new Error("Not implemented in PreinitializedTestSigner");
159
+ }
160
+ async subtractSplitAndEncrypt(
161
+ _params: Omit<SplitSecretWithProofsParams, "secret"> & {
162
+ first: KeyDerivation;
163
+ second: KeyDerivation;
164
+ receiverPublicKey: Uint8Array;
165
+ },
166
+ ): Promise<{ shares: VerifiableSecretShare[]; secretCipher: Uint8Array }> {
167
+ throw new Error("Not implemented in PreinitializedTestSigner");
168
+ }
169
+ async splitSecretWithProofs(
170
+ _params: SplitSecretWithProofsParams,
171
+ ): Promise<VerifiableSecretShare[]> {
172
+ throw new Error("Not implemented in PreinitializedTestSigner");
173
+ }
174
+ async signFrost(_params: SignFrostParams): Promise<Uint8Array> {
175
+ throw new Error("Not implemented in PreinitializedTestSigner");
176
+ }
177
+ async aggregateFrost(_params: AggregateFrostParams): Promise<Uint8Array> {
178
+ throw new Error("Not implemented in PreinitializedTestSigner");
179
+ }
180
+
181
+ async signMessageWithIdentityKey(
182
+ message: Uint8Array,
183
+ compact?: boolean,
184
+ ): Promise<Uint8Array> {
185
+ const signature = secp256k1.sign(message, this.identityPrivateKey);
186
+ return compact ? signature.toCompactRawBytes() : signature.toDERRawBytes();
187
+ }
188
+ async validateMessageWithIdentityKey(
189
+ message: Uint8Array,
190
+ signature: Uint8Array,
191
+ ): Promise<boolean> {
192
+ return secp256k1.verify(
193
+ signature,
194
+ message,
195
+ secp256k1.getPublicKey(this.identityPrivateKey),
196
+ );
197
+ }
198
+
199
+ signTransactionIndex(
200
+ _tx: Transaction,
201
+ _index: number,
202
+ _publicKey: Uint8Array,
203
+ ): void {
204
+ // Not used in this test
205
+ return;
206
+ }
207
+
208
+ async decryptEcies(_ciphertext: Uint8Array): Promise<Uint8Array> {
209
+ throw new Error("Not implemented in PreinitializedTestSigner");
210
+ }
211
+
212
+ async getRandomSigningCommitment(): Promise<SigningCommitmentWithOptionalNonce> {
213
+ // Provide a structurally valid fake commitment
214
+ const binding = secp256k1.utils.randomPrivateKey();
215
+ const hiding = secp256k1.utils.randomPrivateKey();
216
+ return { commitment: { binding, hiding } };
217
+ }
218
+ }
219
+
220
+ it("PreinitializedTestSigner - should initialize a wallet without seed using pre-existing keys", async () => {
221
+ const identityPrivateKey = secp256k1.utils.randomPrivateKey();
222
+ const signer = new PreinitializedTestSigner({ identityPrivateKey });
223
+
224
+ const { wallet } = await SparkWalletTesting.initialize({
225
+ options: {
226
+ network: "LOCAL",
227
+ signerWithPreExistingKeys: true,
228
+ },
229
+ signer,
230
+ });
231
+
232
+ expect(wallet).toBeDefined();
233
+ const identityPubkeyHex = bytesToHex(
234
+ secp256k1.getPublicKey(identityPrivateKey),
235
+ );
236
+ const walletIdentityPubkey = await wallet.getIdentityPublicKey();
237
+ expect(walletIdentityPubkey).toEqual(identityPubkeyHex);
238
+
239
+ const sparkAddress = await wallet.getSparkAddress();
240
+ expect(sparkAddress).toBeDefined();
241
+ });