@buildonspark/spark-sdk 0.0.15 → 0.0.17

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 (136) hide show
  1. package/dist/services/config.d.ts +1 -0
  2. package/dist/services/config.js +4 -1
  3. package/dist/services/config.js.map +1 -1
  4. package/dist/services/wallet-config.d.ts +2 -0
  5. package/dist/services/wallet-config.js +2 -0
  6. package/dist/services/wallet-config.js.map +1 -1
  7. package/dist/signer/signer.js +4 -3
  8. package/dist/signer/signer.js.map +1 -1
  9. package/dist/spark-sdk.d.ts +4 -4
  10. package/dist/spark-sdk.js +26 -10
  11. package/dist/spark-sdk.js.map +1 -1
  12. package/package.json +4 -3
  13. package/src/examples/example.js +247 -0
  14. package/src/examples/example.ts +207 -0
  15. package/src/graphql/client.ts +282 -0
  16. package/src/graphql/mutations/CompleteCoopExit.ts +19 -0
  17. package/src/graphql/mutations/CompleteLeavesSwap.ts +17 -0
  18. package/src/graphql/mutations/RequestCoopExit.ts +20 -0
  19. package/src/graphql/mutations/RequestLightningReceive.ts +26 -0
  20. package/src/graphql/mutations/RequestLightningSend.ts +17 -0
  21. package/src/graphql/mutations/RequestSwapLeaves.ts +24 -0
  22. package/src/graphql/objects/BitcoinNetwork.ts +22 -0
  23. package/src/graphql/objects/CompleteCoopExitInput.ts +41 -0
  24. package/src/graphql/objects/CompleteCoopExitOutput.ts +45 -0
  25. package/src/graphql/objects/CompleteLeavesSwapInput.ts +45 -0
  26. package/src/graphql/objects/CompleteLeavesSwapOutput.ts +45 -0
  27. package/src/graphql/objects/CompleteSeedReleaseInput.ts +41 -0
  28. package/src/graphql/objects/CompleteSeedReleaseOutput.ts +43 -0
  29. package/src/graphql/objects/Connection.ts +90 -0
  30. package/src/graphql/objects/CoopExitFeeEstimateInput.ts +41 -0
  31. package/src/graphql/objects/CoopExitFeeEstimateOutput.ts +52 -0
  32. package/src/graphql/objects/CoopExitRequest.ts +118 -0
  33. package/src/graphql/objects/CurrencyAmount.ts +74 -0
  34. package/src/graphql/objects/CurrencyUnit.ts +32 -0
  35. package/src/graphql/objects/Entity.ts +202 -0
  36. package/src/graphql/objects/GetChallengeInput.ts +37 -0
  37. package/src/graphql/objects/GetChallengeOutput.ts +43 -0
  38. package/src/graphql/objects/Invoice.ts +83 -0
  39. package/src/graphql/objects/Leaf.ts +59 -0
  40. package/src/graphql/objects/LeavesSwapFeeEstimateInput.ts +37 -0
  41. package/src/graphql/objects/LeavesSwapFeeEstimateOutput.ts +52 -0
  42. package/src/graphql/objects/LeavesSwapRequest.ts +192 -0
  43. package/src/graphql/objects/LightningReceiveFeeEstimateInput.ts +41 -0
  44. package/src/graphql/objects/LightningReceiveFeeEstimateOutput.ts +52 -0
  45. package/src/graphql/objects/LightningReceiveRequest.ts +147 -0
  46. package/src/graphql/objects/LightningReceiveRequestStatus.ts +34 -0
  47. package/src/graphql/objects/LightningSendFeeEstimateInput.ts +37 -0
  48. package/src/graphql/objects/LightningSendFeeEstimateOutput.ts +52 -0
  49. package/src/graphql/objects/LightningSendRequest.ts +134 -0
  50. package/src/graphql/objects/LightningSendRequestStatus.ts +28 -0
  51. package/src/graphql/objects/NotifyReceiverTransferInput.ts +41 -0
  52. package/src/graphql/objects/PageInfo.ts +58 -0
  53. package/src/graphql/objects/Provider.ts +41 -0
  54. package/src/graphql/objects/RequestCoopExitInput.ts +41 -0
  55. package/src/graphql/objects/RequestCoopExitOutput.ts +45 -0
  56. package/src/graphql/objects/RequestLeavesSwapInput.ts +55 -0
  57. package/src/graphql/objects/RequestLeavesSwapOutput.ts +45 -0
  58. package/src/graphql/objects/RequestLightningReceiveInput.ts +58 -0
  59. package/src/graphql/objects/RequestLightningReceiveOutput.ts +45 -0
  60. package/src/graphql/objects/RequestLightningSendInput.ts +41 -0
  61. package/src/graphql/objects/RequestLightningSendOutput.ts +45 -0
  62. package/src/graphql/objects/SparkCoopExitRequestStatus.ts +20 -0
  63. package/src/graphql/objects/SparkLeavesSwapRequestStatus.ts +20 -0
  64. package/src/graphql/objects/SparkTransferToLeavesConnection.ts +79 -0
  65. package/src/graphql/objects/SparkWalletUser.ts +86 -0
  66. package/src/graphql/objects/StartSeedReleaseInput.ts +37 -0
  67. package/src/graphql/objects/SwapLeaf.ts +53 -0
  68. package/src/graphql/objects/Transfer.ts +98 -0
  69. package/src/graphql/objects/UserLeafInput.ts +28 -0
  70. package/src/graphql/objects/VerifyChallengeInput.ts +51 -0
  71. package/src/graphql/objects/VerifyChallengeOutput.ts +43 -0
  72. package/src/graphql/objects/WalletUserIdentityPublicKeyInput.ts +37 -0
  73. package/src/graphql/objects/WalletUserIdentityPublicKeyOutput.ts +43 -0
  74. package/src/graphql/objects/index.ts +67 -0
  75. package/src/graphql/queries/CoopExitFeeEstimate.ts +18 -0
  76. package/src/graphql/queries/CurrentUser.ts +10 -0
  77. package/src/graphql/queries/LightningReceiveFeeEstimate.ts +18 -0
  78. package/src/graphql/queries/LightningSendFeeEstimate.ts +16 -0
  79. package/src/proto/common.ts +431 -0
  80. package/src/proto/google/protobuf/descriptor.ts +6625 -0
  81. package/src/proto/google/protobuf/duration.ts +197 -0
  82. package/src/proto/google/protobuf/empty.ts +83 -0
  83. package/src/proto/google/protobuf/timestamp.ts +226 -0
  84. package/src/proto/mock.ts +151 -0
  85. package/src/proto/spark.ts +12727 -0
  86. package/src/proto/spark_authn.ts +673 -0
  87. package/src/proto/validate/validate.ts +6047 -0
  88. package/src/services/config.ts +75 -0
  89. package/src/services/connection.ts +264 -0
  90. package/src/services/coop-exit.ts +190 -0
  91. package/src/services/deposit.ts +327 -0
  92. package/src/services/lightning.ts +341 -0
  93. package/src/services/lrc20.ts +42 -0
  94. package/src/services/token-transactions.ts +499 -0
  95. package/src/services/transfer.ts +1188 -0
  96. package/src/services/tree-creation.ts +618 -0
  97. package/src/services/wallet-config.ts +143 -0
  98. package/src/signer/signer.ts +532 -0
  99. package/src/spark-sdk.ts +1665 -0
  100. package/src/tests/adaptor-signature.test.ts +64 -0
  101. package/src/tests/bitcoin.test.ts +122 -0
  102. package/src/tests/coop-exit.test.ts +233 -0
  103. package/src/tests/deposit.test.ts +98 -0
  104. package/src/tests/keys.test.ts +82 -0
  105. package/src/tests/lightning.test.ts +307 -0
  106. package/src/tests/secret-sharing.test.ts +63 -0
  107. package/src/tests/swap.test.ts +252 -0
  108. package/src/tests/test-util.ts +92 -0
  109. package/src/tests/tokens.test.ts +47 -0
  110. package/src/tests/transfer.test.ts +371 -0
  111. package/src/tests/tree-creation.test.ts +56 -0
  112. package/src/tests/utils/spark-testing-wallet.ts +37 -0
  113. package/src/tests/utils/test-faucet.ts +257 -0
  114. package/src/types/grpc.ts +8 -0
  115. package/src/types/index.ts +3 -0
  116. package/src/utils/adaptor-signature.ts +189 -0
  117. package/src/utils/bitcoin.ts +138 -0
  118. package/src/utils/crypto.ts +14 -0
  119. package/src/utils/index.ts +12 -0
  120. package/src/utils/keys.ts +92 -0
  121. package/src/utils/mempool.ts +42 -0
  122. package/src/utils/network.ts +70 -0
  123. package/src/utils/proof.ts +17 -0
  124. package/src/utils/response-validation.ts +26 -0
  125. package/src/utils/secret-sharing.ts +263 -0
  126. package/src/utils/signing.ts +96 -0
  127. package/src/utils/token-hashing.ts +163 -0
  128. package/src/utils/token-keyshares.ts +31 -0
  129. package/src/utils/token-transactions.ts +71 -0
  130. package/src/utils/transaction.ts +45 -0
  131. package/src/utils/wasm-wrapper.ts +57 -0
  132. package/src/utils/wasm.ts +154 -0
  133. package/src/wasm/spark_bindings.d.ts +208 -0
  134. package/src/wasm/spark_bindings.js +1161 -0
  135. package/src/wasm/spark_bindings_bg.wasm +0 -0
  136. package/src/wasm/spark_bindings_bg.wasm.d.ts +136 -0
@@ -0,0 +1,1665 @@
1
+ import { bytesToHex, hexToBytes } from "@noble/curves/abstract/utils";
2
+ import { secp256k1 } from "@noble/curves/secp256k1";
3
+ import { Address, OutScript, Transaction } from "@scure/btc-signer";
4
+ import { TransactionInput } from "@scure/btc-signer/psbt";
5
+ import { sha256 } from "@scure/btc-signer/utils";
6
+ import { decode } from "light-bolt11-decoder";
7
+ import SspClient from "./graphql/client.js";
8
+ import {
9
+ BitcoinNetwork,
10
+ CoopExitFeeEstimateInput,
11
+ CoopExitFeeEstimateOutput,
12
+ LeavesSwapRequest,
13
+ LightningReceiveFeeEstimateInput,
14
+ LightningReceiveFeeEstimateOutput,
15
+ LightningSendFeeEstimateInput,
16
+ LightningSendFeeEstimateOutput,
17
+ UserLeafInput,
18
+ } from "./graphql/objects/index.js";
19
+ import {
20
+ DepositAddressQueryResult,
21
+ LeafWithPreviousTransactionData,
22
+ QueryAllTransfersResponse,
23
+ TokenTransactionWithStatus,
24
+ Transfer,
25
+ TransferStatus,
26
+ TreeNode,
27
+ } from "./proto/spark.js";
28
+ import { WalletConfigService } from "./services/config.js";
29
+ import { ConnectionManager } from "./services/connection.js";
30
+ import { CoopExitService } from "./services/coop-exit.js";
31
+ import { DepositService } from "./services/deposit.js";
32
+ import { LightningService } from "./services/lightning.js";
33
+ import { TokenTransactionService } from "./services/token-transactions.js";
34
+ import { LeafKeyTweak, TransferService } from "./services/transfer.js";
35
+ import { ConfigOptions } from "./services/wallet-config.js";
36
+
37
+ import { validateMnemonic } from "@scure/bip39";
38
+ import { wordlist } from "@scure/bip39/wordlists/english";
39
+ import { Mutex } from "async-mutex";
40
+ import bitcoin from "bitcoinjs-lib";
41
+ import {
42
+ DepositAddressTree,
43
+ TreeCreationService,
44
+ } from "./services/tree-creation.js";
45
+ import {
46
+ applyAdaptorToSignature,
47
+ generateAdaptorFromSignature,
48
+ generateSignatureFromExistingAdaptor,
49
+ } from "./utils/adaptor-signature.js";
50
+ import {
51
+ computeTaprootKeyNoScript,
52
+ getSigHashFromTx,
53
+ getTxFromRawTxBytes,
54
+ getTxFromRawTxHex,
55
+ getTxId,
56
+ } from "./utils/bitcoin.js";
57
+ import {
58
+ getNetwork,
59
+ LRC_WALLET_NETWORK,
60
+ LRC_WALLET_NETWORK_TYPE,
61
+ Network,
62
+ } from "./utils/network.js";
63
+ import {
64
+ calculateAvailableTokenAmount,
65
+ checkIfSelectedLeavesAreAvailable,
66
+ } from "./utils/token-transactions.js";
67
+ import { getNextTransactionSequence } from "./utils/transaction.js";
68
+ import { initWasm } from "./utils/wasm-wrapper.js";
69
+ import { InitOutput } from "./wasm/spark_bindings.js";
70
+
71
+ import { LRC20WalletApiConfig, LRCWallet } from "@buildonspark/lrc20-sdk";
72
+ import { broadcastL1Withdrawal } from "./services/lrc20.js";
73
+ import { SparkSigner } from "./signer/signer.js";
74
+ import { getMasterHDKeyFromSeed } from "./utils/index.js";
75
+
76
+ // Add this constant at the file level
77
+ const MAX_TOKEN_LEAVES = 100;
78
+
79
+ export type CreateLightningInvoiceParams = {
80
+ amountSats: number;
81
+ memo: string;
82
+ expirySeconds?: number;
83
+ };
84
+
85
+ export type PayLightningInvoiceParams = {
86
+ invoice: string;
87
+ };
88
+
89
+ export type TransferParams = {
90
+ amountSats: number;
91
+ receiverSparkAddress: string;
92
+ };
93
+
94
+ type DepositParams = {
95
+ signingPubKey: Uint8Array;
96
+ verifyingKey: Uint8Array;
97
+ depositTx: Transaction;
98
+ vout: number;
99
+ };
100
+
101
+ export type InitWalletResponse = {
102
+ mnemonic?: string | undefined;
103
+ };
104
+
105
+ export interface SparkWalletProps {
106
+ mnemonicOrSeed?: Uint8Array | string;
107
+ signer?: SparkSigner;
108
+ options?: ConfigOptions;
109
+ lrc20WalletApiConfig?: LRC20WalletApiConfig;
110
+ }
111
+
112
+ /**
113
+ * The SparkWallet class is the primary interface for interacting with the Spark network.
114
+ * It provides methods for creating and managing wallets, handling deposits, executing transfers,
115
+ * and interacting with the Lightning Network.
116
+ */
117
+ export class SparkWallet {
118
+ protected config: WalletConfigService;
119
+
120
+ protected connectionManager: ConnectionManager;
121
+ protected lrc20Wallet: LRCWallet | undefined;
122
+
123
+ private depositService: DepositService;
124
+ protected transferService: TransferService;
125
+ private treeCreationService: TreeCreationService;
126
+ private lightningService: LightningService;
127
+ private coopExitService: CoopExitService;
128
+ private tokenTransactionService: TokenTransactionService;
129
+
130
+ private claimTransferMutex = new Mutex();
131
+ private leavesMutex = new Mutex();
132
+ private optimizationInProgress = false;
133
+ private sspClient: SspClient | null = null;
134
+ private wasmModule: InitOutput | null = null;
135
+
136
+ protected leaves: TreeNode[] = [];
137
+ protected tokenLeaves: Map<string, LeafWithPreviousTransactionData[]> =
138
+ new Map();
139
+
140
+ protected constructor(options?: ConfigOptions, signer?: SparkSigner) {
141
+ this.config = new WalletConfigService(options, signer);
142
+ this.connectionManager = new ConnectionManager(this.config);
143
+ this.depositService = new DepositService(
144
+ this.config,
145
+ this.connectionManager,
146
+ );
147
+ this.transferService = new TransferService(
148
+ this.config,
149
+ this.connectionManager,
150
+ );
151
+ this.treeCreationService = new TreeCreationService(
152
+ this.config,
153
+ this.connectionManager,
154
+ );
155
+ this.tokenTransactionService = new TokenTransactionService(
156
+ this.config,
157
+ this.connectionManager,
158
+ );
159
+ this.lightningService = new LightningService(
160
+ this.config,
161
+ this.connectionManager,
162
+ );
163
+ this.coopExitService = new CoopExitService(
164
+ this.config,
165
+ this.connectionManager,
166
+ );
167
+ }
168
+
169
+ public static async create({
170
+ mnemonicOrSeed,
171
+ signer,
172
+ options,
173
+ lrc20WalletApiConfig,
174
+ }: SparkWalletProps) {
175
+ const wallet = new SparkWallet(options, signer);
176
+ const initResponse = await wallet.initWallet(
177
+ mnemonicOrSeed,
178
+ options?.enableLrc20,
179
+ lrc20WalletApiConfig,
180
+ );
181
+ return {
182
+ wallet,
183
+ ...initResponse,
184
+ };
185
+ }
186
+
187
+ private async initWasm() {
188
+ try {
189
+ this.wasmModule = await initWasm();
190
+ } catch (e) {
191
+ console.error("Failed to initialize Wasm module", e);
192
+ }
193
+ }
194
+
195
+ private async initializeWallet(identityPublicKey: string) {
196
+ this.sspClient = new SspClient(identityPublicKey);
197
+ await this.connectionManager.createClients();
198
+
199
+ await this.initWasm();
200
+ await this.syncWallet();
201
+ }
202
+
203
+ private async getLeaves(): Promise<TreeNode[]> {
204
+ const sparkClient = await this.connectionManager.createSparkClient(
205
+ this.config.getCoordinatorAddress(),
206
+ );
207
+ const leaves = await sparkClient.query_nodes({
208
+ source: {
209
+ $case: "ownerIdentityPubkey",
210
+ ownerIdentityPubkey: await this.config.signer.getIdentityPublicKey(),
211
+ },
212
+ includeParents: false,
213
+ network: this.config.getNetworkProto(),
214
+ });
215
+ return Object.entries(leaves.nodes)
216
+ .filter(([_, node]) => node.status === "AVAILABLE")
217
+ .map(([_, node]) => node);
218
+ }
219
+
220
+ private async selectLeaves(targetAmount: number): Promise<TreeNode[]> {
221
+ if (targetAmount <= 0) {
222
+ throw new Error("Target amount must be positive");
223
+ }
224
+
225
+ const leaves = await this.getLeaves();
226
+ if (leaves.length === 0) {
227
+ throw new Error("No owned leaves found");
228
+ }
229
+
230
+ leaves.sort((a, b) => b.value - a.value);
231
+
232
+ let amount = 0;
233
+ let nodes: TreeNode[] = [];
234
+ for (const leaf of leaves) {
235
+ if (targetAmount - amount >= leaf.value) {
236
+ amount += leaf.value;
237
+ nodes.push(leaf);
238
+ }
239
+ }
240
+
241
+ if (amount !== targetAmount) {
242
+ await this.requestLeavesSwap({ targetAmount });
243
+
244
+ amount = 0;
245
+ nodes = [];
246
+ const newLeaves = await this.getLeaves();
247
+ newLeaves.sort((a, b) => b.value - a.value);
248
+ for (const leaf of newLeaves) {
249
+ if (targetAmount - amount >= leaf.value) {
250
+ amount += leaf.value;
251
+ nodes.push(leaf);
252
+ }
253
+ }
254
+ }
255
+
256
+ return nodes;
257
+ }
258
+
259
+ private async selectLeavesForSwap(targetAmount: number) {
260
+ if (targetAmount == 0) {
261
+ throw new Error("Target amount needs to > 0");
262
+ }
263
+ const leaves = await this.getLeaves();
264
+ leaves.sort((a, b) => a.value - b.value);
265
+
266
+ let amount = 0;
267
+ const nodes: TreeNode[] = [];
268
+ for (const leaf of leaves) {
269
+ if (amount < targetAmount) {
270
+ amount += leaf.value;
271
+ nodes.push(leaf);
272
+ }
273
+ }
274
+
275
+ if (amount < targetAmount) {
276
+ throw new Error("Not enough leaves to swap for the target amount");
277
+ }
278
+
279
+ return nodes;
280
+ }
281
+
282
+ private areLeavesInefficient() {
283
+ const totalAmount = this.leaves.reduce((acc, leaf) => acc + leaf.value, 0);
284
+
285
+ if (this.leaves.length <= 1) {
286
+ return false;
287
+ }
288
+
289
+ const nextLowerPowerOfTwo = 31 - Math.clz32(totalAmount);
290
+
291
+ let remainingAmount = totalAmount;
292
+ let optimalLeavesLength = 0;
293
+
294
+ for (let i = nextLowerPowerOfTwo; i >= 0; i--) {
295
+ const denomination = 2 ** i;
296
+ while (remainingAmount >= denomination) {
297
+ remainingAmount -= denomination;
298
+ optimalLeavesLength++;
299
+ }
300
+ }
301
+
302
+ return this.leaves.length > optimalLeavesLength * 5;
303
+ }
304
+
305
+ private async optimizeLeaves() {
306
+ if (this.optimizationInProgress || !this.areLeavesInefficient()) {
307
+ return;
308
+ }
309
+
310
+ await this.withLeaves(async () => {
311
+ this.optimizationInProgress = true;
312
+ try {
313
+ if (this.leaves.length > 0) {
314
+ await this.requestLeavesSwap({ leaves: this.leaves });
315
+ }
316
+ this.leaves = await this.getLeaves();
317
+ } finally {
318
+ this.optimizationInProgress = false;
319
+ }
320
+ });
321
+ }
322
+
323
+ private async syncWallet() {
324
+ await Promise.all([this.claimTransfers(), this.syncTokenLeaves()]);
325
+ this.leaves = await this.getLeaves();
326
+ await this.config.signer.restoreSigningKeysFromLeafs(this.leaves);
327
+ await this.refreshTimelockNodes();
328
+
329
+ this.optimizeLeaves().catch((e) => {
330
+ console.error("Failed to optimize leaves", e);
331
+ });
332
+ }
333
+
334
+ private async withLeaves<T>(operation: () => Promise<T>): Promise<T> {
335
+ const release = await this.leavesMutex.acquire();
336
+ try {
337
+ return await operation();
338
+ } finally {
339
+ release();
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Gets the identity public key of the wallet.
345
+ *
346
+ * @returns {Promise<string>} The identity public key as a hex string.
347
+ */
348
+ public async getIdentityPublicKey(): Promise<string> {
349
+ return bytesToHex(await this.config.signer.getIdentityPublicKey());
350
+ }
351
+
352
+ /**
353
+ * Gets the Spark address of the wallet.
354
+ *
355
+ * @returns {Promise<string>} The Spark address as a hex string.
356
+ */
357
+ public async getSparkAddress(): Promise<string> {
358
+ return bytesToHex(await this.config.signer.getIdentityPublicKey());
359
+ }
360
+
361
+ /**
362
+ * Initializes the wallet using either a mnemonic phrase or a raw seed.
363
+ * initWallet will also claim any pending incoming lightning payment, spark transfer,
364
+ * or bitcoin deposit.
365
+ *
366
+ * @param {Uint8Array | string} [mnemonicOrSeed] - (Optional) Either:
367
+ * - A BIP-39 mnemonic phrase as string
368
+ * - A raw seed as Uint8Array or hex string
369
+ * If not provided, generates a new mnemonic and uses it to create a new wallet
370
+ *
371
+ * @returns {Promise<Object>} Object containing:
372
+ * - mnemonic: The mnemonic if one was generated (undefined for raw seed)
373
+ * - balance: The wallet's initial balance in satoshis
374
+ * - tokenBalance: Map of token balances and leaf counts
375
+ * @private
376
+ */
377
+ protected async initWallet(
378
+ mnemonicOrSeed?: Uint8Array | string,
379
+ enableLrc20?: boolean,
380
+ lrc20WalletApiConfig?: LRC20WalletApiConfig,
381
+ ): Promise<InitWalletResponse | undefined> {
382
+ const returnMnemonic = !mnemonicOrSeed;
383
+ let mnemonic: string | undefined;
384
+ if (!mnemonicOrSeed) {
385
+ mnemonic = await this.config.signer.generateMnemonic();
386
+ mnemonicOrSeed = mnemonic;
387
+ }
388
+
389
+ let seed: Uint8Array;
390
+ if (typeof mnemonicOrSeed !== "string") {
391
+ seed = mnemonicOrSeed;
392
+ } else {
393
+ if (validateMnemonic(mnemonicOrSeed, wordlist)) {
394
+ seed = await this.config.signer.mnemonicToSeed(mnemonicOrSeed);
395
+ } else {
396
+ seed = hexToBytes(mnemonicOrSeed);
397
+ }
398
+ }
399
+
400
+ await this.initWalletFromSeed(seed);
401
+
402
+ if (enableLrc20) {
403
+ const network = this.config.getNetwork();
404
+ // TODO: remove this once we move it back to the signer
405
+ const masterPrivateKey = getMasterHDKeyFromSeed(seed).privateKey!;
406
+ this.lrc20Wallet = new LRCWallet(
407
+ bytesToHex(masterPrivateKey),
408
+ LRC_WALLET_NETWORK[network],
409
+ LRC_WALLET_NETWORK_TYPE[network],
410
+ );
411
+ }
412
+
413
+ if (returnMnemonic) {
414
+ return {
415
+ mnemonic,
416
+ };
417
+ }
418
+
419
+ return;
420
+ }
421
+
422
+ /**
423
+ * Initializes a wallet from a seed.
424
+ *
425
+ * @param {Uint8Array | string} seed - The seed to initialize the wallet from
426
+ * @returns {Promise<string>} The identity public key
427
+ * @private
428
+ */
429
+ private async initWalletFromSeed(seed: Uint8Array | string) {
430
+ const identityPublicKey =
431
+ await this.config.signer.createSparkWalletFromSeed(
432
+ seed,
433
+ this.config.getNetwork(),
434
+ );
435
+ await this.initializeWallet(identityPublicKey);
436
+ return identityPublicKey;
437
+ }
438
+
439
+ /**
440
+ * Requests a swap of leaves to optimize wallet structure.
441
+ *
442
+ * @param {Object} params - Parameters for the leaves swap
443
+ * @param {number} [params.targetAmount] - Target amount for the swap
444
+ * @param {TreeNode[]} [params.leaves] - Specific leaves to swap
445
+ * @returns {Promise<Object>} The completed swap response
446
+ */
447
+ public async requestLeavesSwap({
448
+ targetAmount,
449
+ leaves,
450
+ }: {
451
+ targetAmount?: number;
452
+ leaves?: TreeNode[];
453
+ }) {
454
+ if (targetAmount && targetAmount <= 0) {
455
+ throw new Error("targetAmount must be positive");
456
+ }
457
+
458
+ await this.claimTransfers();
459
+
460
+ let leavesToSwap: TreeNode[];
461
+ if (targetAmount && leaves && leaves.length > 0) {
462
+ if (targetAmount < leaves.reduce((acc, leaf) => acc + leaf.value, 0)) {
463
+ throw new Error("targetAmount is less than the sum of leaves");
464
+ }
465
+ leavesToSwap = leaves;
466
+ } else if (targetAmount) {
467
+ leavesToSwap = await this.selectLeavesForSwap(targetAmount);
468
+ } else if (leaves && leaves.length > 0) {
469
+ leavesToSwap = leaves;
470
+ } else {
471
+ throw new Error("targetAmount or leaves must be provided");
472
+ }
473
+
474
+ const leafKeyTweaks = await Promise.all(
475
+ leavesToSwap.map(async (leaf) => ({
476
+ leaf,
477
+ signingPubKey: await this.config.signer.generatePublicKey(
478
+ sha256(leaf.id),
479
+ ),
480
+ newSigningPubKey: await this.config.signer.generatePublicKey(),
481
+ })),
482
+ );
483
+
484
+ const { transfer, signatureMap } =
485
+ await this.transferService.sendTransferSignRefund(
486
+ leafKeyTweaks,
487
+ await this.config.signer.getSspIdentityPublicKey(
488
+ this.config.getNetwork(),
489
+ ),
490
+ new Date(Date.now() + 2 * 60 * 1000),
491
+ );
492
+ try {
493
+ if (!transfer.leaves[0]?.leaf) {
494
+ throw new Error("Failed to get leaf");
495
+ }
496
+
497
+ const refundSignature = signatureMap.get(transfer.leaves[0].leaf.id);
498
+ if (!refundSignature) {
499
+ throw new Error("Failed to get refund signature");
500
+ }
501
+
502
+ const { adaptorPrivateKey, adaptorSignature } =
503
+ generateAdaptorFromSignature(refundSignature);
504
+
505
+ if (!transfer.leaves[0].leaf) {
506
+ throw new Error("Failed to get leaf");
507
+ }
508
+
509
+ const userLeaves: UserLeafInput[] = [];
510
+ userLeaves.push({
511
+ leaf_id: transfer.leaves[0].leaf.id,
512
+ raw_unsigned_refund_transaction: bytesToHex(
513
+ transfer.leaves[0].intermediateRefundTx,
514
+ ),
515
+ adaptor_added_signature: bytesToHex(adaptorSignature),
516
+ });
517
+
518
+ for (let i = 1; i < transfer.leaves.length; i++) {
519
+ const leaf = transfer.leaves[i];
520
+ if (!leaf?.leaf) {
521
+ throw new Error("Failed to get leaf");
522
+ }
523
+
524
+ const refundSignature = signatureMap.get(leaf.leaf.id);
525
+ if (!refundSignature) {
526
+ throw new Error("Failed to get refund signature");
527
+ }
528
+
529
+ const signature = generateSignatureFromExistingAdaptor(
530
+ refundSignature,
531
+ adaptorPrivateKey,
532
+ );
533
+
534
+ userLeaves.push({
535
+ leaf_id: leaf.leaf.id,
536
+ raw_unsigned_refund_transaction: bytesToHex(
537
+ leaf.intermediateRefundTx,
538
+ ),
539
+ adaptor_added_signature: bytesToHex(signature),
540
+ });
541
+ }
542
+
543
+ const adaptorPubkey = bytesToHex(
544
+ secp256k1.getPublicKey(adaptorPrivateKey),
545
+ );
546
+ let request: LeavesSwapRequest | null | undefined = null;
547
+ request = await this.sspClient?.requestLeaveSwap({
548
+ userLeaves,
549
+ adaptorPubkey,
550
+ targetAmountSats:
551
+ targetAmount ||
552
+ leavesToSwap.reduce((acc, leaf) => acc + leaf.value, 0),
553
+ totalAmountSats: leavesToSwap.reduce(
554
+ (acc, leaf) => acc + leaf.value,
555
+ 0,
556
+ ),
557
+ // TODO: Request fee from SSP
558
+ feeSats: 0,
559
+ });
560
+
561
+ if (!request) {
562
+ throw new Error("Failed to request leaves swap. No response returned.");
563
+ }
564
+
565
+ const sparkClient = await this.connectionManager.createSparkClient(
566
+ this.config.getCoordinatorAddress(),
567
+ );
568
+
569
+ const nodes = await sparkClient.query_nodes({
570
+ source: {
571
+ $case: "nodeIds",
572
+ nodeIds: {
573
+ nodeIds: request.swapLeaves.map((leaf) => leaf.leafId),
574
+ },
575
+ },
576
+ includeParents: false,
577
+ network: this.config.getNetworkProto(),
578
+ });
579
+
580
+ if (Object.values(nodes.nodes).length !== request.swapLeaves.length) {
581
+ throw new Error("Expected same number of nodes as swapLeaves");
582
+ }
583
+
584
+ for (const [nodeId, node] of Object.entries(nodes.nodes)) {
585
+ if (!node.nodeTx) {
586
+ throw new Error(`Node tx not found for leaf ${nodeId}`);
587
+ }
588
+
589
+ if (!node.verifyingPublicKey) {
590
+ throw new Error(`Node public key not found for leaf ${nodeId}`);
591
+ }
592
+
593
+ const leaf = request.swapLeaves.find((leaf) => leaf.leafId === nodeId);
594
+ if (!leaf) {
595
+ throw new Error(`Leaf not found for node ${nodeId}`);
596
+ }
597
+
598
+ // @ts-ignore - We do a null check above
599
+ const nodeTx = getTxFromRawTxBytes(node.nodeTx);
600
+ const refundTxBytes = hexToBytes(leaf.rawUnsignedRefundTransaction);
601
+ const refundTx = getTxFromRawTxBytes(refundTxBytes);
602
+ const sighash = getSigHashFromTx(refundTx, 0, nodeTx.getOutput(0));
603
+
604
+ const nodePublicKey = node.verifyingPublicKey;
605
+
606
+ const taprootKey = computeTaprootKeyNoScript(nodePublicKey.slice(1));
607
+ const adaptorSignatureBytes = hexToBytes(leaf.adaptorSignedSignature);
608
+ applyAdaptorToSignature(
609
+ taprootKey.slice(1),
610
+ sighash,
611
+ adaptorSignatureBytes,
612
+ adaptorPrivateKey,
613
+ );
614
+ }
615
+
616
+ await this.transferService.sendTransferTweakKey(
617
+ transfer,
618
+ leafKeyTweaks,
619
+ signatureMap,
620
+ );
621
+
622
+ const completeResponse = await this.sspClient?.completeLeaveSwap({
623
+ adaptorSecretKey: bytesToHex(adaptorPrivateKey),
624
+ userOutboundTransferExternalId: transfer.id,
625
+ leavesSwapRequestId: request.id,
626
+ });
627
+
628
+ if (!completeResponse) {
629
+ throw new Error("Failed to complete leaves swap");
630
+ }
631
+
632
+ await this.claimTransfers();
633
+
634
+ return completeResponse;
635
+ } catch (e) {
636
+ await this.cancelAllSenderInitiatedTransfers();
637
+ throw new Error(`Failed to request leaves swap: ${e}`);
638
+ }
639
+ }
640
+
641
+ /**
642
+ * Gets all transfers for the wallet.
643
+ *
644
+ * @param {number} [limit=20] - Maximum number of transfers to return
645
+ * @param {number} [offset=0] - Offset for pagination
646
+ * @returns {Promise<QueryAllTransfersResponse>} Response containing the list of transfers
647
+ */
648
+ public async getTransfers(
649
+ limit: number = 20,
650
+ offset: number = 0,
651
+ ): Promise<QueryAllTransfersResponse> {
652
+ return await this.transferService.queryAllTransfers(limit, offset);
653
+ }
654
+
655
+ /**
656
+ * Gets the current balance of the wallet.
657
+ * You can use the forceRefetch option to synchronize your wallet and claim any
658
+ * pending incoming lightning payment, spark transfer, or bitcoin deposit before returning the balance.
659
+ *
660
+ * @returns {Promise<Object>} Object containing:
661
+ * - balance: The wallet's current balance in satoshis
662
+ * - tokenBalances: Map of token balances and leaf counts
663
+ */
664
+ public async getBalance(): Promise<{
665
+ balance: bigint;
666
+ tokenBalances: Map<string, { balance: bigint }>;
667
+ }> {
668
+ this.leaves = await this.getLeaves();
669
+ await this.syncTokenLeaves();
670
+
671
+ const tokenBalances = new Map<string, { balance: bigint }>();
672
+
673
+ for (const [tokenPublicKey, leaves] of this.tokenLeaves.entries()) {
674
+ tokenBalances.set(tokenPublicKey, {
675
+ balance: calculateAvailableTokenAmount(leaves),
676
+ });
677
+ }
678
+
679
+ return {
680
+ balance: this.leaves.reduce((acc, leaf) => acc + BigInt(leaf.value), 0n),
681
+ tokenBalances,
682
+ };
683
+ }
684
+
685
+ // ***** Deposit Flow *****
686
+
687
+ /**
688
+ * Generates a new deposit address for receiving bitcoin funds.
689
+ * Note that this function returns a bitcoin address, not a spark address.
690
+ * For Layer 1 Bitcoin deposits, Spark generates Pay to Taproot (P2TR) addresses.
691
+ * These addresses start with "bc1p" and can be used to receive Bitcoin from any wallet.
692
+ *
693
+ * @returns {Promise<string>} A Bitcoin address for depositing funds
694
+ */
695
+ public async getDepositAddress(): Promise<string> {
696
+ return await this.generateDepositAddress();
697
+ }
698
+
699
+ /**
700
+ * Generates a deposit address for receiving funds.
701
+ *
702
+ * @returns {Promise<string>} A deposit address
703
+ * @private
704
+ */
705
+ private async generateDepositAddress(): Promise<string> {
706
+ const signingPubkey = await this.config.signer.getDepositSigningKey();
707
+ const address = await this.depositService!.generateDepositAddress({
708
+ signingPubkey,
709
+ });
710
+ if (!address.depositAddress) {
711
+ throw new Error("Failed to generate deposit address");
712
+ }
713
+ return address.depositAddress.address;
714
+ }
715
+
716
+ /**
717
+ * Finalizes a deposit to the wallet.
718
+ *
719
+ * @param {DepositParams} params - Parameters for finalizing the deposit
720
+ * @returns {Promise<TreeNode[] | undefined>} The nodes created from the deposit
721
+ * @private
722
+ */
723
+ private async finalizeDeposit({
724
+ signingPubKey,
725
+ verifyingKey,
726
+ depositTx,
727
+ vout,
728
+ }: DepositParams) {
729
+ const response = await this.depositService!.createTreeRoot({
730
+ signingPubKey,
731
+ verifyingKey,
732
+ depositTx,
733
+ vout,
734
+ });
735
+
736
+ return await this.transferDepositToSelf(response.nodes, signingPubKey);
737
+ }
738
+
739
+ /**
740
+ * Gets all unused deposit addresses for the wallet.
741
+ *
742
+ * @returns {Promise<string[]>} The unused deposit addresses
743
+ */
744
+ public async getUnusedDepositAddresses(): Promise<string[]> {
745
+ const sparkClient = await this.connectionManager.createSparkClient(
746
+ this.config.getCoordinatorAddress(),
747
+ );
748
+ return (
749
+ await sparkClient.query_unused_deposit_addresses({
750
+ identityPublicKey: await this.config.signer.getIdentityPublicKey(),
751
+ })
752
+ ).depositAddresses.map((addr) => addr.depositAddress);
753
+ }
754
+ /**
755
+ * Claims a deposit to the wallet.
756
+ *
757
+ * @param {string} txid - The transaction ID of the deposit
758
+ * @returns {Promise<TreeNode[] | undefined>} The nodes resulting from the deposit
759
+ */
760
+ public async claimDeposit(txid: string) {
761
+ const baseUrl =
762
+ this.config.getNetwork() === Network.REGTEST
763
+ ? "https://regtest-mempool.dev.dev.sparkinfra.net/api"
764
+ : "https://mempool.space/api";
765
+ const auth = btoa("spark-sdk:mCMk1JqlBNtetUNy");
766
+
767
+ const headers: Record<string, string> = {
768
+ "Content-Type": "application/json",
769
+ };
770
+
771
+ if (this.config.getNetwork() === Network.REGTEST) {
772
+ headers["Authorization"] = `Basic ${auth}`;
773
+ }
774
+
775
+ const response = await fetch(`${baseUrl}/tx/${txid}/hex`, {
776
+ headers,
777
+ });
778
+
779
+ const txHex = await response.text();
780
+ if (!/^[0-9A-Fa-f]+$/.test(txHex)) {
781
+ throw new Error("Transaction not found");
782
+ }
783
+ const depositTx = getTxFromRawTxHex(txHex);
784
+
785
+ const sparkClient = await this.connectionManager.createSparkClient(
786
+ this.config.getCoordinatorAddress(),
787
+ );
788
+
789
+ const unusedDepositAddresses: Map<string, DepositAddressQueryResult> =
790
+ new Map(
791
+ (
792
+ await sparkClient.query_unused_deposit_addresses({
793
+ identityPublicKey: await this.config.signer.getIdentityPublicKey(),
794
+ })
795
+ ).depositAddresses.map((addr) => [addr.depositAddress, addr]),
796
+ );
797
+
798
+ let depositAddress: DepositAddressQueryResult | undefined;
799
+ let vout = 0;
800
+ for (let i = 0; i < depositTx.outputsLength; i++) {
801
+ const output = depositTx.getOutput(i);
802
+ if (!output) {
803
+ continue;
804
+ }
805
+ const parsedScript = OutScript.decode(output.script!);
806
+ const address = Address(getNetwork(this.config.getNetwork())).encode(
807
+ parsedScript,
808
+ );
809
+ if (unusedDepositAddresses.has(address)) {
810
+ vout = i;
811
+ depositAddress = unusedDepositAddresses.get(address);
812
+ break;
813
+ }
814
+ }
815
+ if (!depositAddress) {
816
+ throw new Error("Deposit address not found");
817
+ }
818
+
819
+ const nodes = await this.finalizeDeposit({
820
+ signingPubKey: depositAddress.userSigningPublicKey,
821
+ verifyingKey: depositAddress.verifyingPublicKey,
822
+ depositTx,
823
+ vout,
824
+ });
825
+
826
+ return nodes;
827
+ }
828
+
829
+ /**
830
+ * Transfers deposit to self to claim ownership.
831
+ *
832
+ * @param {TreeNode[]} leaves - The leaves to transfer
833
+ * @param {Uint8Array} signingPubKey - The signing public key
834
+ * @returns {Promise<TreeNode[] | undefined>} The nodes resulting from the transfer
835
+ * @private
836
+ */
837
+ private async transferDepositToSelf(
838
+ leaves: TreeNode[],
839
+ signingPubKey: Uint8Array,
840
+ ): Promise<TreeNode[] | undefined> {
841
+ const leafKeyTweaks = await Promise.all(
842
+ leaves.map(async (leaf) => ({
843
+ leaf,
844
+ signingPubKey,
845
+ newSigningPubKey: await this.config.signer.generatePublicKey(),
846
+ })),
847
+ );
848
+
849
+ await this.transferService.sendTransfer(
850
+ leafKeyTweaks,
851
+ await this.config.signer.getIdentityPublicKey(),
852
+ );
853
+
854
+ const pendingTransfers = await this.transferService.queryPendingTransfers();
855
+ if (pendingTransfers.transfers.length > 0) {
856
+ // @ts-ignore - We check the length, so the first element is guaranteed to exist
857
+ return (await this.claimTransfer(pendingTransfers.transfers[0])).nodes;
858
+ }
859
+
860
+ return;
861
+ }
862
+ // ***** Transfer Flow *****
863
+
864
+ /**
865
+ * Sends a transfer to another Spark user.
866
+ *
867
+ * @param {TransferParams} params - Parameters for the transfer
868
+ * @param {string} params.receiverSparkAddress - The recipient's Spark address
869
+ * @param {number} params.amountSats - Amount to send in satoshis
870
+ * @returns {Promise<Transfer>} The completed transfer details
871
+ */
872
+ public async transfer({ amountSats, receiverSparkAddress }: TransferParams) {
873
+ return await this.withLeaves(async () => {
874
+ const leavesToSend = await this.selectLeaves(amountSats);
875
+
876
+ await this.refreshTimelockNodes();
877
+
878
+ const leafKeyTweaks = await Promise.all(
879
+ leavesToSend.map(async (leaf) => ({
880
+ leaf,
881
+ signingPubKey: await this.config.signer.generatePublicKey(
882
+ sha256(leaf.id),
883
+ ),
884
+ newSigningPubKey: await this.config.signer.generatePublicKey(),
885
+ })),
886
+ );
887
+
888
+ const transfer = await this.transferService.sendTransfer(
889
+ leafKeyTweaks,
890
+ hexToBytes(receiverSparkAddress),
891
+ );
892
+
893
+ const leavesToRemove = new Set(leavesToSend.map((leaf) => leaf.id));
894
+ this.leaves = this.leaves.filter((leaf) => !leavesToRemove.has(leaf.id));
895
+
896
+ return transfer;
897
+ });
898
+ }
899
+
900
+ /**
901
+ * Internal method to refresh timelock nodes.
902
+ *
903
+ * @param {string} nodeId - The optional ID of the node to refresh. If not provided, all nodes will be checked.
904
+ * @returns {Promise<void>}
905
+ * @private
906
+ */
907
+ private async refreshTimelockNodes(nodeId?: string) {
908
+ const nodesToRefresh: TreeNode[] = [];
909
+ const nodeIds: string[] = [];
910
+
911
+ if (nodeId) {
912
+ for (const node of this.leaves) {
913
+ if (node.id === nodeId) {
914
+ nodesToRefresh.push(node);
915
+ nodeIds.push(node.id);
916
+ break;
917
+ }
918
+ }
919
+ if (nodesToRefresh.length === 0) {
920
+ throw new Error(`node ${nodeId} not found`);
921
+ }
922
+ } else {
923
+ for (const node of this.leaves) {
924
+ const refundTx = getTxFromRawTxBytes(node.refundTx);
925
+ const nextSequence = getNextTransactionSequence(
926
+ refundTx.getInput(0).sequence,
927
+ );
928
+ const needRefresh = nextSequence <= 0;
929
+ if (needRefresh) {
930
+ nodesToRefresh.push(node);
931
+ nodeIds.push(node.id);
932
+ }
933
+ }
934
+ }
935
+
936
+ if (nodesToRefresh.length === 0) {
937
+ return;
938
+ }
939
+
940
+ const sparkClient = await this.connectionManager.createSparkClient(
941
+ this.config.getCoordinatorAddress(),
942
+ );
943
+
944
+ const nodesResp = await sparkClient.query_nodes({
945
+ source: {
946
+ $case: "nodeIds",
947
+ nodeIds: {
948
+ nodeIds,
949
+ },
950
+ },
951
+ includeParents: true,
952
+ network: this.config.getNetworkProto(),
953
+ });
954
+
955
+ const nodesMap = new Map<string, TreeNode>();
956
+ for (const node of Object.values(nodesResp.nodes)) {
957
+ nodesMap.set(node.id, node);
958
+ }
959
+
960
+ for (const node of nodesToRefresh) {
961
+ if (!node.parentNodeId) {
962
+ throw new Error(`node ${node.id} has no parent`);
963
+ }
964
+
965
+ const parentNode = nodesMap.get(node.parentNodeId);
966
+ if (!parentNode) {
967
+ throw new Error(`parent node ${node.parentNodeId} not found`);
968
+ }
969
+
970
+ const { nodes } = await this.transferService.refreshTimelockNodes(
971
+ [node],
972
+ parentNode,
973
+ await this.config.signer.generatePublicKey(sha256(node.id)),
974
+ );
975
+
976
+ if (nodes.length !== 1) {
977
+ throw new Error(`expected 1 node, got ${nodes.length}`);
978
+ }
979
+
980
+ const newNode = nodes[0];
981
+ if (!newNode) {
982
+ throw new Error("Failed to refresh timelock node");
983
+ }
984
+
985
+ this.leaves = this.leaves.filter((leaf) => leaf.id !== node.id);
986
+ this.leaves.push(newNode);
987
+ }
988
+ }
989
+
990
+ /**
991
+ * Gets all pending transfers.
992
+ *
993
+ * @returns {Promise<Transfer[]>} The pending transfers
994
+ */
995
+ public async getPendingTransfers() {
996
+ return (await this.transferService.queryPendingTransfers()).transfers;
997
+ }
998
+
999
+ /**
1000
+ * Claims a specific transfer.
1001
+ *
1002
+ * @param {Transfer} transfer - The transfer to claim
1003
+ * @returns {Promise<Object>} The claim result
1004
+ */
1005
+ public async claimTransfer(transfer: Transfer) {
1006
+ return await this.claimTransferMutex.runExclusive(async () => {
1007
+ const leafPubKeyMap =
1008
+ await this.transferService.verifyPendingTransfer(transfer);
1009
+
1010
+ let leavesToClaim: LeafKeyTweak[] = [];
1011
+
1012
+ for (const leaf of transfer.leaves) {
1013
+ if (leaf.leaf) {
1014
+ const leafPubKey = leafPubKeyMap.get(leaf.leaf.id);
1015
+ if (leafPubKey) {
1016
+ leavesToClaim.push({
1017
+ leaf: leaf.leaf,
1018
+ signingPubKey: leafPubKey,
1019
+ newSigningPubKey: await this.config.signer.generatePublicKey(
1020
+ sha256(leaf.leaf.id),
1021
+ ),
1022
+ });
1023
+ }
1024
+ }
1025
+ }
1026
+
1027
+ const response = await this.transferService.claimTransfer(
1028
+ transfer,
1029
+ leavesToClaim,
1030
+ );
1031
+
1032
+ this.leaves.push(...response.nodes);
1033
+ await this.refreshTimelockNodes();
1034
+
1035
+ return response.nodes;
1036
+ });
1037
+ }
1038
+
1039
+ /**
1040
+ * Claims all pending transfers.
1041
+ *
1042
+ * @returns {Promise<boolean>} True if any transfers were claimed
1043
+ */
1044
+ public async claimTransfers(): Promise<boolean> {
1045
+ const transfers = await this.transferService.queryPendingTransfers();
1046
+ let claimed = false;
1047
+ for (const transfer of transfers.transfers) {
1048
+ if (
1049
+ transfer.status !== TransferStatus.TRANSFER_STATUS_SENDER_KEY_TWEAKED &&
1050
+ transfer.status !==
1051
+ TransferStatus.TRANSFER_STATUS_RECEIVER_KEY_TWEAKED &&
1052
+ transfer.status !==
1053
+ TransferStatus.TRANSFER_STATUSR_RECEIVER_REFUND_SIGNED
1054
+ ) {
1055
+ continue;
1056
+ }
1057
+ await this.claimTransfer(transfer);
1058
+ claimed = true;
1059
+ }
1060
+ return claimed;
1061
+ }
1062
+
1063
+ /**
1064
+ * Cancels all sender-initiated transfers.
1065
+ *
1066
+ * @returns {Promise<void>}
1067
+ * @private
1068
+ */
1069
+ private async cancelAllSenderInitiatedTransfers() {
1070
+ for (const operator of Object.values(this.config.getSigningOperators())) {
1071
+ const transfers =
1072
+ await this.transferService.queryPendingTransfersBySender(
1073
+ operator.address,
1074
+ );
1075
+
1076
+ for (const transfer of transfers.transfers) {
1077
+ if (
1078
+ transfer.status === TransferStatus.TRANSFER_STATUS_SENDER_INITIATED
1079
+ ) {
1080
+ await this.transferService.cancelSendTransfer(
1081
+ transfer,
1082
+ operator.address,
1083
+ );
1084
+ }
1085
+ }
1086
+ }
1087
+ }
1088
+
1089
+ // ***** Lightning Flow *****
1090
+
1091
+ /**
1092
+ * Creates a Lightning invoice for receiving payments.
1093
+ *
1094
+ * @param {Object} params - Parameters for the lightning invoice
1095
+ * @param {number} params.amountSats - Amount in satoshis
1096
+ * @param {string} params.memo - Description for the invoice
1097
+ * @param {number} [params.expirySeconds] - Optional expiry time in seconds
1098
+ * @returns {Promise<string>} BOLT11 encoded invoice
1099
+ */
1100
+ public async createLightningInvoice({
1101
+ amountSats,
1102
+ memo,
1103
+ expirySeconds = 60 * 60 * 24 * 30,
1104
+ }: CreateLightningInvoiceParams) {
1105
+ if (!this.sspClient) {
1106
+ throw new Error("SSP client not initialized");
1107
+ }
1108
+
1109
+ const requestLightningInvoice = async (
1110
+ amountSats: number,
1111
+ paymentHash: Uint8Array,
1112
+ memo: string,
1113
+ ) => {
1114
+ const network = this.config.getNetwork();
1115
+ let bitcoinNetwork: BitcoinNetwork = BitcoinNetwork.REGTEST;
1116
+ if (network === Network.MAINNET) {
1117
+ bitcoinNetwork = BitcoinNetwork.MAINNET;
1118
+ } else if (network === Network.REGTEST) {
1119
+ bitcoinNetwork = BitcoinNetwork.REGTEST;
1120
+ }
1121
+
1122
+ const invoice = await this.sspClient!.requestLightningReceive({
1123
+ amountSats,
1124
+ network: bitcoinNetwork,
1125
+ paymentHash: bytesToHex(paymentHash),
1126
+ expirySecs: expirySeconds,
1127
+ memo,
1128
+ });
1129
+
1130
+ return invoice?.invoice.encodedEnvoice;
1131
+ };
1132
+
1133
+ return this.lightningService!.createLightningInvoice({
1134
+ amountSats,
1135
+ memo,
1136
+ invoiceCreator: requestLightningInvoice,
1137
+ });
1138
+ }
1139
+
1140
+ /**
1141
+ * Pays a Lightning invoice.
1142
+ *
1143
+ * @param {Object} params - Parameters for paying the invoice
1144
+ * @param {string} params.invoice - The BOLT11-encoded Lightning invoice to pay
1145
+ * @returns {Promise<LightningSendRequest>} The Lightning payment request details
1146
+ */
1147
+ public async payLightningInvoice({ invoice }: PayLightningInvoiceParams) {
1148
+ return await this.withLeaves(async () => {
1149
+ if (!this.sspClient) {
1150
+ throw new Error("SSP client not initialized");
1151
+ }
1152
+
1153
+ // TODO: Get fee
1154
+
1155
+ const decodedInvoice = decode(invoice);
1156
+ const amountSats =
1157
+ Number(
1158
+ decodedInvoice.sections.find((section) => section.name === "amount")
1159
+ ?.value,
1160
+ ) / 1000;
1161
+
1162
+ if (isNaN(amountSats) || amountSats <= 0) {
1163
+ throw new Error("Invalid amount");
1164
+ }
1165
+
1166
+ const paymentHash = decodedInvoice.sections.find(
1167
+ (section) => section.name === "payment_hash",
1168
+ )?.value;
1169
+
1170
+ if (!paymentHash) {
1171
+ throw new Error("No payment hash found in invoice");
1172
+ }
1173
+
1174
+ const leaves = await this.selectLeaves(amountSats);
1175
+
1176
+ await this.refreshTimelockNodes();
1177
+ const leavesToSend = await Promise.all(
1178
+ leaves.map(async (leaf) => ({
1179
+ leaf,
1180
+ signingPubKey: await this.config.signer.generatePublicKey(
1181
+ sha256(leaf.id),
1182
+ ),
1183
+ newSigningPubKey: await this.config.signer.generatePublicKey(),
1184
+ })),
1185
+ );
1186
+
1187
+ const swapResponse = await this.lightningService.swapNodesForPreimage({
1188
+ leaves: leavesToSend,
1189
+ receiverIdentityPubkey:
1190
+ await this.config.signer.getSspIdentityPublicKey(
1191
+ this.config.getNetwork(),
1192
+ ),
1193
+ paymentHash: hexToBytes(paymentHash),
1194
+ isInboundPayment: false,
1195
+ invoiceString: invoice,
1196
+ });
1197
+
1198
+ if (!swapResponse.transfer) {
1199
+ throw new Error("Failed to swap nodes for preimage");
1200
+ }
1201
+
1202
+ const transfer = await this.transferService.sendTransferTweakKey(
1203
+ swapResponse.transfer,
1204
+ leavesToSend,
1205
+ new Map(),
1206
+ );
1207
+
1208
+ const sspResponse = await this.sspClient.requestLightningSend({
1209
+ encodedInvoice: invoice,
1210
+ idempotencyKey: paymentHash,
1211
+ });
1212
+
1213
+ if (!sspResponse) {
1214
+ throw new Error("Failed to contact SSP");
1215
+ }
1216
+
1217
+ const leavesToRemove = new Set(leavesToSend.map((leaf) => leaf.leaf.id));
1218
+ this.leaves = this.leaves.filter((leaf) => !leavesToRemove.has(leaf.id));
1219
+
1220
+ return sspResponse;
1221
+ });
1222
+ }
1223
+
1224
+ /**
1225
+ * Gets fee estimate for receiving Lightning payments.
1226
+ *
1227
+ * @param {LightningReceiveFeeEstimateInput} params - Input parameters for fee estimation
1228
+ * @returns {Promise<LightningReceiveFeeEstimateOutput | null>} Fee estimate for receiving Lightning payments
1229
+ */
1230
+ public async getLightningReceiveFeeEstimate({
1231
+ amountSats,
1232
+ network,
1233
+ }: LightningReceiveFeeEstimateInput): Promise<LightningReceiveFeeEstimateOutput | null> {
1234
+ if (!this.sspClient) {
1235
+ throw new Error("SSP client not initialized");
1236
+ }
1237
+
1238
+ return await this.sspClient.getLightningReceiveFeeEstimate(
1239
+ amountSats,
1240
+ network,
1241
+ );
1242
+ }
1243
+
1244
+ /**
1245
+ * Gets fee estimate for sending Lightning payments.
1246
+ *
1247
+ * @param {LightningSendFeeEstimateInput} params - Input parameters for fee estimation
1248
+ * @returns {Promise<LightningSendFeeEstimateOutput | null>} Fee estimate for sending Lightning payments
1249
+ */
1250
+ public async getLightningSendFeeEstimate({
1251
+ encodedInvoice,
1252
+ }: LightningSendFeeEstimateInput): Promise<LightningSendFeeEstimateOutput | null> {
1253
+ if (!this.sspClient) {
1254
+ throw new Error("SSP client not initialized");
1255
+ }
1256
+
1257
+ return await this.sspClient.getLightningSendFeeEstimate(encodedInvoice);
1258
+ }
1259
+
1260
+ // ***** Tree Creation Flow *****
1261
+
1262
+ /**
1263
+ * Generates a deposit address for a tree.
1264
+ *
1265
+ * @param {number} vout - The vout index
1266
+ * @param {Uint8Array} parentSigningPubKey - The parent signing public key
1267
+ * @param {Transaction} [parentTx] - Optional parent transaction
1268
+ * @param {TreeNode} [parentNode] - Optional parent node
1269
+ * @returns {Promise<Object>} Deposit address information
1270
+ * @private
1271
+ */
1272
+ private async generateDepositAddressForTree(
1273
+ vout: number,
1274
+ parentSigningPubKey: Uint8Array,
1275
+ parentTx?: Transaction,
1276
+ parentNode?: TreeNode,
1277
+ ) {
1278
+ return await this.treeCreationService!.generateDepositAddressForTree(
1279
+ vout,
1280
+ parentSigningPubKey,
1281
+ parentTx,
1282
+ parentNode,
1283
+ );
1284
+ }
1285
+
1286
+ /**
1287
+ * Creates a tree structure.
1288
+ *
1289
+ * @param {number} vout - The vout index
1290
+ * @param {DepositAddressTree} root - The root of the tree
1291
+ * @param {boolean} createLeaves - Whether to create leaves
1292
+ * @param {Transaction} [parentTx] - Optional parent transaction
1293
+ * @param {TreeNode} [parentNode] - Optional parent node
1294
+ * @returns {Promise<Object>} The created tree
1295
+ * @private
1296
+ */
1297
+ private async createTree(
1298
+ vout: number,
1299
+ root: DepositAddressTree,
1300
+ createLeaves: boolean,
1301
+ parentTx?: Transaction,
1302
+ parentNode?: TreeNode,
1303
+ ) {
1304
+ return await this.treeCreationService!.createTree(
1305
+ vout,
1306
+ root,
1307
+ createLeaves,
1308
+ parentTx,
1309
+ parentNode,
1310
+ );
1311
+ }
1312
+
1313
+ // ***** Cooperative Exit Flow *****
1314
+
1315
+ /**
1316
+ * Initiates a withdrawal to move funds from the Spark network to an on-chain Bitcoin address.
1317
+ *
1318
+ * @param {Object} params - Parameters for the withdrawal
1319
+ * @param {string} params.onchainAddress - The Bitcoin address where the funds should be sent
1320
+ * @param {number} [params.targetAmountSats] - The amount in satoshis to withdraw. If not specified, attempts to withdraw all available funds
1321
+ * @returns {Promise<CoopExitRequest | null | undefined>} The withdrawal request details, or null/undefined if the request cannot be completed
1322
+ */
1323
+ public async withdraw({
1324
+ onchainAddress,
1325
+ targetAmountSats,
1326
+ }: {
1327
+ onchainAddress: string;
1328
+ targetAmountSats?: number;
1329
+ }) {
1330
+ return await this.withLeaves(async () => {
1331
+ return await this.coopExit(onchainAddress, targetAmountSats);
1332
+ });
1333
+ }
1334
+
1335
+ /**
1336
+ * Internal method to perform a cooperative exit (withdrawal).
1337
+ *
1338
+ * @param {string} onchainAddress - The Bitcoin address where the funds should be sent
1339
+ * @param {number} [targetAmountSats] - The amount in satoshis to withdraw
1340
+ * @returns {Promise<Object | null | undefined>} The exit request details
1341
+ * @private
1342
+ */
1343
+ private async coopExit(onchainAddress: string, targetAmountSats?: number) {
1344
+ let leavesToSend: TreeNode[] = [];
1345
+ if (targetAmountSats) {
1346
+ leavesToSend = await this.selectLeaves(targetAmountSats);
1347
+ } else {
1348
+ leavesToSend = this.leaves.map((leaf) => ({
1349
+ ...leaf,
1350
+ }));
1351
+ }
1352
+
1353
+ const leafKeyTweaks = await Promise.all(
1354
+ leavesToSend.map(async (leaf) => ({
1355
+ leaf,
1356
+ signingPubKey: await this.config.signer.generatePublicKey(
1357
+ sha256(leaf.id),
1358
+ ),
1359
+ newSigningPubKey: await this.config.signer.generatePublicKey(),
1360
+ })),
1361
+ );
1362
+
1363
+ const coopExitRequest = await this.sspClient?.requestCoopExit({
1364
+ leafExternalIds: leavesToSend.map((leaf) => leaf.id),
1365
+ withdrawalAddress: onchainAddress,
1366
+ });
1367
+
1368
+ if (!coopExitRequest?.rawConnectorTransaction) {
1369
+ throw new Error("Failed to request coop exit");
1370
+ }
1371
+
1372
+ const connectorTx = getTxFromRawTxHex(
1373
+ coopExitRequest.rawConnectorTransaction,
1374
+ );
1375
+
1376
+ const coopExitTxId = connectorTx.getInput(0).txid;
1377
+ const connectorTxId = getTxId(connectorTx);
1378
+
1379
+ if (!coopExitTxId) {
1380
+ throw new Error("Failed to get coop exit tx id");
1381
+ }
1382
+
1383
+ const connectorOutputs: TransactionInput[] = [];
1384
+ for (let i = 0; i < connectorTx.outputsLength - 1; i++) {
1385
+ connectorOutputs.push({
1386
+ txid: hexToBytes(connectorTxId),
1387
+ index: i,
1388
+ });
1389
+ }
1390
+
1391
+ const sspPubIdentityKey = await this.config.signer.getSspIdentityPublicKey(
1392
+ this.config.getNetwork(),
1393
+ );
1394
+
1395
+ const transfer = await this.coopExitService.getConnectorRefundSignatures({
1396
+ leaves: leafKeyTweaks,
1397
+ exitTxId: coopExitTxId,
1398
+ connectorOutputs,
1399
+ receiverPubKey: sspPubIdentityKey,
1400
+ });
1401
+
1402
+ const completeResponse = await this.sspClient?.completeCoopExit({
1403
+ userOutboundTransferExternalId: transfer.transfer.id,
1404
+ coopExitRequestId: coopExitRequest.id,
1405
+ });
1406
+
1407
+ return completeResponse;
1408
+ }
1409
+
1410
+ /**
1411
+ * Gets fee estimate for cooperative exit (on-chain withdrawal).
1412
+ *
1413
+ * @param {CoopExitFeeEstimateInput} params - Input parameters for fee estimation
1414
+ * @returns {Promise<CoopExitFeeEstimateOutput | null>} Fee estimate for the withdrawal
1415
+ */
1416
+ public async getCoopExitFeeEstimate({
1417
+ leafExternalIds,
1418
+ withdrawalAddress,
1419
+ }: CoopExitFeeEstimateInput): Promise<CoopExitFeeEstimateOutput | null> {
1420
+ if (!this.sspClient) {
1421
+ throw new Error("SSP client not initialized");
1422
+ }
1423
+
1424
+ return await this.sspClient.getCoopExitFeeEstimate({
1425
+ leafExternalIds,
1426
+ withdrawalAddress,
1427
+ });
1428
+ }
1429
+
1430
+ // ***** Token Flow *****
1431
+
1432
+ /**
1433
+ * Synchronizes token leaves for the wallet.
1434
+ *
1435
+ * @returns {Promise<void>}
1436
+ * @private
1437
+ */
1438
+ protected async syncTokenLeaves() {
1439
+ this.tokenLeaves.clear();
1440
+
1441
+ const trackedPublicKeys = await this.config.signer.getTrackedPublicKeys();
1442
+ const unsortedTokenLeaves =
1443
+ await this.tokenTransactionService.fetchOwnedTokenLeaves(
1444
+ [...trackedPublicKeys, await this.config.signer.getIdentityPublicKey()],
1445
+ [],
1446
+ );
1447
+
1448
+ // Group leaves by token key
1449
+ const groupedLeaves = new Map<string, LeafWithPreviousTransactionData[]>();
1450
+
1451
+ unsortedTokenLeaves.forEach((leaf) => {
1452
+ const tokenKey = bytesToHex(leaf.leaf!.tokenPublicKey!);
1453
+ const index = leaf.previousTransactionVout!;
1454
+
1455
+ if (!groupedLeaves.has(tokenKey)) {
1456
+ groupedLeaves.set(tokenKey, []);
1457
+ }
1458
+
1459
+ groupedLeaves.get(tokenKey)!.push({
1460
+ ...leaf,
1461
+ previousTransactionVout: index,
1462
+ });
1463
+ });
1464
+
1465
+ this.tokenLeaves = groupedLeaves;
1466
+ }
1467
+
1468
+ /**
1469
+ * Gets all token balances.
1470
+ *
1471
+ * @returns {Promise<Map<string, { balance: bigint }>>} Map of token balances and leaf counts
1472
+ * @private
1473
+ */
1474
+ private async getAllTokenBalances(): Promise<
1475
+ Map<
1476
+ string,
1477
+ {
1478
+ balance: bigint;
1479
+ }
1480
+ >
1481
+ > {
1482
+ await this.syncTokenLeaves();
1483
+
1484
+ const balances = new Map<string, { balance: bigint }>();
1485
+ for (const [tokenPublicKey, leaves] of this.tokenLeaves.entries()) {
1486
+ balances.set(tokenPublicKey, {
1487
+ balance: calculateAvailableTokenAmount(leaves),
1488
+ });
1489
+ }
1490
+ return balances;
1491
+ }
1492
+
1493
+ /**
1494
+ * Transfers tokens to another user.
1495
+ *
1496
+ * @param {Object} params - Parameters for the token transfer
1497
+ * @param {string} params.tokenPublicKey - The public key of the token to transfer
1498
+ * @param {bigint} params.tokenAmount - The amount of tokens to transfer
1499
+ * @param {string} params.receiverSparkAddress - The recipient's public key
1500
+ * @param {LeafWithPreviousTransactionData[]} [params.selectedLeaves] - Optional specific leaves to use for the transfer
1501
+ * @returns {Promise<string>} The transaction ID of the token transfer
1502
+ */
1503
+ public async transferTokens({
1504
+ tokenPublicKey,
1505
+ tokenAmount,
1506
+ receiverSparkAddress,
1507
+ selectedLeaves,
1508
+ }: {
1509
+ tokenPublicKey: string;
1510
+ tokenAmount: bigint;
1511
+ receiverSparkAddress: string;
1512
+ selectedLeaves?: LeafWithPreviousTransactionData[];
1513
+ }): Promise<string> {
1514
+ await this.syncTokenLeaves();
1515
+ if (!this.tokenLeaves.has(tokenPublicKey)) {
1516
+ throw new Error("No token leaves with the given tokenPublicKey");
1517
+ }
1518
+
1519
+ const tokenPublicKeyBytes = hexToBytes(tokenPublicKey);
1520
+ const receiverSparkAddressBytes = hexToBytes(receiverSparkAddress);
1521
+
1522
+ if (selectedLeaves) {
1523
+ if (
1524
+ !checkIfSelectedLeavesAreAvailable(
1525
+ selectedLeaves,
1526
+ this.tokenLeaves,
1527
+ tokenPublicKeyBytes,
1528
+ )
1529
+ ) {
1530
+ throw new Error("One or more selected leaves are not available");
1531
+ }
1532
+ } else {
1533
+ selectedLeaves = this.selectTokenLeaves(tokenPublicKey, tokenAmount);
1534
+ }
1535
+
1536
+ if (selectedLeaves!.length > MAX_TOKEN_LEAVES) {
1537
+ throw new Error("Too many leaves selected");
1538
+ }
1539
+
1540
+ const tokenTransaction =
1541
+ await this.tokenTransactionService.constructTransferTokenTransaction(
1542
+ selectedLeaves,
1543
+ receiverSparkAddressBytes,
1544
+ tokenPublicKeyBytes,
1545
+ tokenAmount,
1546
+ );
1547
+
1548
+ return await this.tokenTransactionService.broadcastTokenTransaction(
1549
+ tokenTransaction,
1550
+ selectedLeaves.map((leaf) => leaf.leaf!.ownerPublicKey),
1551
+ selectedLeaves.map((leaf) => leaf.leaf!.revocationPublicKey!),
1552
+ );
1553
+ }
1554
+
1555
+ public async getTokenTransactions(
1556
+ tokenPublicKeys: string[],
1557
+ tokenTransactionHashes?: string[],
1558
+ ): Promise<TokenTransactionWithStatus[]> {
1559
+ const sparkClient = await this.connectionManager.createSparkClient(
1560
+ this.config.getCoordinatorAddress(),
1561
+ );
1562
+
1563
+ let queryParams;
1564
+ if (tokenTransactionHashes?.length) {
1565
+ queryParams = {
1566
+ tokenPublicKeys: tokenPublicKeys?.map(hexToBytes)!,
1567
+ ownerPublicKeys: [hexToBytes(await this.getIdentityPublicKey())],
1568
+ tokenTransactionHashes: tokenTransactionHashes.map(hexToBytes),
1569
+ };
1570
+ } else {
1571
+ queryParams = {
1572
+ tokenPublicKeys: tokenPublicKeys?.map(hexToBytes)!,
1573
+ ownerPublicKeys: [hexToBytes(await this.getIdentityPublicKey())],
1574
+ };
1575
+ }
1576
+
1577
+ const response = await sparkClient.query_token_transactions(queryParams);
1578
+ return response.tokenTransactionsWithStatus;
1579
+ }
1580
+
1581
+ /**
1582
+ * Selects token leaves for a transfer.
1583
+ *
1584
+ * @param {string} tokenPublicKey - The public key of the token
1585
+ * @param {bigint} tokenAmount - The amount of tokens to select leaves for
1586
+ * @returns {LeafWithPreviousTransactionData[]} The selected leaves
1587
+ * @private
1588
+ */
1589
+ private selectTokenLeaves(
1590
+ tokenPublicKey: string,
1591
+ tokenAmount: bigint,
1592
+ ): LeafWithPreviousTransactionData[] {
1593
+ return this.tokenTransactionService.selectTokenLeaves(
1594
+ this.tokenLeaves.get(tokenPublicKey)!,
1595
+ tokenAmount,
1596
+ );
1597
+ }
1598
+
1599
+ public async withdrawTokens(
1600
+ tokenPublicKey: string,
1601
+ receiverPublicKey?: string,
1602
+ leafIds?: string[],
1603
+ ): Promise<{ txid: string } | undefined> {
1604
+ if (!this.lrc20Wallet) {
1605
+ throw new Error("LRC20 wallet not initialized");
1606
+ }
1607
+
1608
+ await this.syncTokenLeaves();
1609
+
1610
+ let leavesToExit = this.tokenLeaves.get(tokenPublicKey);
1611
+
1612
+ if (leavesToExit && leafIds) {
1613
+ leavesToExit = leavesToExit.filter(
1614
+ ({ leaf }) => leafIds.findIndex((leafId) => leafId == leaf!.id) != -1,
1615
+ );
1616
+ }
1617
+
1618
+ if (!leavesToExit) {
1619
+ throw new Error("No leaves to exit");
1620
+ }
1621
+
1622
+ if (!receiverPublicKey) {
1623
+ receiverPublicKey = await this.getIdentityPublicKey();
1624
+ }
1625
+
1626
+ try {
1627
+ return await broadcastL1Withdrawal(
1628
+ this.lrc20Wallet!,
1629
+ leavesToExit,
1630
+ receiverPublicKey,
1631
+ );
1632
+ } catch (err: any) {
1633
+ if (err.message === "Not enough UTXOs") {
1634
+ console.error(
1635
+ "Error: No L1 UTXOs available to cover exit fees. Please send sats to the address associated with your Wallet:",
1636
+ this.lrc20Wallet!.p2wpkhAddress,
1637
+ );
1638
+ } else {
1639
+ console.error("Unexpected error:", err);
1640
+ }
1641
+ return;
1642
+ }
1643
+ }
1644
+ }
1645
+
1646
+ /**
1647
+ * Utility function to determine the network from a Bitcoin address.
1648
+ *
1649
+ * @param {string} address - The Bitcoin address
1650
+ * @returns {BitcoinNetwork | null} The detected network or null if not detected
1651
+ */
1652
+ function getNetworkFromAddress(address: string) {
1653
+ try {
1654
+ const decoded = bitcoin.address.fromBech32(address);
1655
+ // HRP (human-readable part) determines the network
1656
+ if (decoded.prefix === "bc") {
1657
+ return BitcoinNetwork.MAINNET;
1658
+ } else if (decoded.prefix === "bcrt") {
1659
+ return BitcoinNetwork.REGTEST;
1660
+ }
1661
+ } catch (err) {
1662
+ throw new Error("Invalid Bitcoin address");
1663
+ }
1664
+ return null;
1665
+ }