@buildonspark/spark-sdk 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/spark-sdk.js CHANGED
@@ -15,14 +15,21 @@ import { TransferService } from "./services/transfer.js";
15
15
  import { validateMnemonic } from "@scure/bip39";
16
16
  import { wordlist } from "@scure/bip39/wordlists/english";
17
17
  import { Mutex } from "async-mutex";
18
+ import bitcoin from "bitcoinjs-lib";
18
19
  import { TreeCreationService, } from "./services/tree-creation.js";
19
20
  import { applyAdaptorToSignature, generateAdaptorFromSignature, generateSignatureFromExistingAdaptor, } from "./utils/adaptor-signature.js";
20
21
  import { computeTaprootKeyNoScript, getSigHashFromTx, getTxFromRawTxBytes, getTxFromRawTxHex, getTxId, } from "./utils/bitcoin.js";
21
22
  import { Network } from "./utils/network.js";
22
23
  import { calculateAvailableTokenAmount, checkIfSelectedLeavesAreAvailable, } from "./utils/token-transactions.js";
24
+ import { getNextTransactionSequence } from "./utils/transaction.js";
23
25
  import { initWasm } from "./utils/wasm-wrapper.js";
24
26
  // Add this constant at the file level
25
27
  const MAX_TOKEN_LEAVES = 100;
28
+ /**
29
+ * The SparkWallet class is the primary interface for interacting with the Spark network.
30
+ * It provides methods for creating and managing wallets, handling deposits, executing transfers,
31
+ * and interacting with the Lightning Network.
32
+ */
26
33
  export class SparkWallet {
27
34
  config;
28
35
  connectionManager;
@@ -139,21 +146,49 @@ export class SparkWallet {
139
146
  this.leaves = await this.getLeaves();
140
147
  }
141
148
  async syncWallet() {
142
- await this.claimTransfers();
143
- await this.claimDeposits();
144
- await this.syncTokenLeaves();
149
+ await Promise.all([
150
+ this.claimTransfers(),
151
+ this.claimDeposits(),
152
+ this.syncTokenLeaves(),
153
+ ]);
145
154
  this.leaves = await this.getLeaves();
146
- await this.optimizeLeaves();
155
+ await this.#refreshTimelockNodes();
156
+ // await this.optimizeLeaves();
147
157
  }
148
158
  isInitialized() {
149
159
  return this.sspClient !== null && this.wasmModule !== null;
150
160
  }
161
+ /**
162
+ * Gets the identity public key of the wallet.
163
+ *
164
+ * @returns {Promise<string>} The identity public key as a hex string.
165
+ */
151
166
  async getIdentityPublicKey() {
152
167
  return bytesToHex(await this.config.signer.getIdentityPublicKey());
153
168
  }
169
+ /**
170
+ * Gets the Spark address of the wallet.
171
+ *
172
+ * @returns {Promise<string>} The Spark address as a hex string.
173
+ */
154
174
  async getSparkAddress() {
155
175
  return bytesToHex(await this.config.signer.getIdentityPublicKey());
156
176
  }
177
+ /**
178
+ * Initializes the wallet using either a mnemonic phrase or a raw seed.
179
+ * initWallet will also claim any pending incoming lightning payment, spark transfer,
180
+ * or bitcoin deposit.
181
+ *
182
+ * @param {Uint8Array | string} [mnemonicOrSeed] - (Optional) Either:
183
+ * - A BIP-39 mnemonic phrase as string
184
+ * - A raw seed as Uint8Array or hex string
185
+ * If not provided, generates a new mnemonic and uses it to create a new wallet
186
+ *
187
+ * @returns {Promise<Object>} Object containing:
188
+ * - mnemonic: The mnemonic if one was generated (undefined for raw seed)
189
+ * - balance: The wallet's initial balance in satoshis
190
+ * - tokenBalance: Map of token balances and leaf counts
191
+ */
157
192
  async initWallet(mnemonicOrSeed) {
158
193
  const returnMnemonic = !mnemonicOrSeed;
159
194
  if (!mnemonicOrSeed) {
@@ -185,15 +220,31 @@ export class SparkWallet {
185
220
  };
186
221
  }
187
222
  async initWalletFromMnemonic(mnemonic) {
188
- const identityPublicKey = await this.config.signer.createSparkWalletFromMnemonic(mnemonic);
223
+ const identityPublicKey = await this.config.signer.createSparkWalletFromMnemonic(mnemonic, this.config.getNetwork());
189
224
  await this.initializeWallet(identityPublicKey);
190
225
  return identityPublicKey;
191
226
  }
227
+ /**
228
+ * Initializes a wallet from a seed.
229
+ *
230
+ * @param {Uint8Array | string} seed - The seed to initialize the wallet from
231
+ * @returns {Promise<string>} The identity public key
232
+ * @private
233
+ */
192
234
  async initWalletFromSeed(seed) {
193
- const identityPublicKey = await this.config.signer.createSparkWalletFromSeed(seed);
235
+ const identityPublicKey = await this.config.signer.createSparkWalletFromSeed(seed, this.config.getNetwork());
194
236
  await this.initializeWallet(identityPublicKey);
195
237
  return identityPublicKey;
196
238
  }
239
+ /**
240
+ * Requests a swap of leaves to optimize wallet structure.
241
+ *
242
+ * @param {Object} params - Parameters for the leaves swap
243
+ * @param {number} [params.targetAmount] - Target amount for the swap
244
+ * @param {TreeNode[]} [params.leaves] - Specific leaves to swap
245
+ * @returns {Promise<Object>} The completed swap response
246
+ * @private
247
+ */
197
248
  async requestLeavesSwap({ targetAmount, leaves, }) {
198
249
  if (targetAmount && targetAmount <= 0) {
199
250
  throw new Error("targetAmount must be positive");
@@ -320,9 +371,26 @@ export class SparkWallet {
320
371
  throw new Error(`Failed to request leaves swap: ${e}`);
321
372
  }
322
373
  }
374
+ /**
375
+ * Gets all transfers for the wallet.
376
+ *
377
+ * @param {number} [limit=20] - Maximum number of transfers to return
378
+ * @param {number} [offset=0] - Offset for pagination
379
+ * @returns {Promise<QueryAllTransfersResponse>} Response containing the list of transfers
380
+ */
323
381
  async getAllTransfers(limit = 20, offset = 0) {
324
382
  return await this.transferService.queryAllTransfers(limit, offset);
325
383
  }
384
+ /**
385
+ * Gets the current balance of the wallet.
386
+ * You can use the forceRefetch option to synchronize your wallet and claim any
387
+ * pending incoming lightning payment, spark transfer, or bitcoin deposit before returning the balance.
388
+ *
389
+ * @param {boolean} [forceRefetch=false] - Synchronizes the wallet before returning the balance
390
+ * @returns {Promise<Object>} Object containing:
391
+ * - balance: The wallet's current balance in satoshis
392
+ * - tokenBalances: Map of token balances and leaf counts
393
+ */
326
394
  async getBalance(forceRefetch = false) {
327
395
  if (forceRefetch) {
328
396
  await Promise.all([
@@ -330,7 +398,6 @@ export class SparkWallet {
330
398
  this.claimDeposits(),
331
399
  this.syncTokenLeaves(),
332
400
  ]);
333
- await this.syncTokenLeaves();
334
401
  this.leaves = await this.getLeaves();
335
402
  }
336
403
  const tokenBalances = new Map();
@@ -347,9 +414,23 @@ export class SparkWallet {
347
414
  };
348
415
  }
349
416
  // ***** Deposit Flow *****
417
+ /**
418
+ * Generates a new deposit address for receiving bitcoin funds.
419
+ * Note that this function returns a bitcoin address, not a spark address.
420
+ * For Layer 1 Bitcoin deposits, Spark generates Pay to Taproot (P2TR) addresses.
421
+ * These addresses start with "bc1p" and can be used to receive Bitcoin from any wallet.
422
+ *
423
+ * @returns {Promise<string>} A Bitcoin address for depositing funds
424
+ */
350
425
  async getDepositAddress() {
351
426
  return await this.generateDepositAddress();
352
427
  }
428
+ /**
429
+ * Generates a deposit address for receiving funds.
430
+ *
431
+ * @returns {Promise<string>} A deposit address
432
+ * @private
433
+ */
353
434
  async generateDepositAddress() {
354
435
  const signingPubkey = await this.config.signer.getDepositSigningKey();
355
436
  const address = await this.depositService.generateDepositAddress({
@@ -360,6 +441,13 @@ export class SparkWallet {
360
441
  }
361
442
  return address.depositAddress.address;
362
443
  }
444
+ /**
445
+ * Finalizes a deposit to the wallet.
446
+ *
447
+ * @param {DepositParams} params - Parameters for finalizing the deposit
448
+ * @returns {Promise<TreeNode[] | undefined>} The nodes created from the deposit
449
+ * @private
450
+ */
363
451
  async finalizeDeposit({ signingPubKey, verifyingKey, depositTx, vout, }) {
364
452
  const response = await this.depositService.createTreeRoot({
365
453
  signingPubKey,
@@ -369,6 +457,12 @@ export class SparkWallet {
369
457
  });
370
458
  return await this.transferDepositToSelf(response.nodes, signingPubKey);
371
459
  }
460
+ /**
461
+ * Claims any pending deposits to the wallet.
462
+ *
463
+ * @returns {Promise<TreeNode[]>} The nodes created from the claimed deposits
464
+ * @private
465
+ */
372
466
  async claimDeposits() {
373
467
  const sparkClient = await this.connectionManager.createSparkClient(this.config.getCoordinatorAddress());
374
468
  const identityPublicKey = await this.config.signer.getIdentityPublicKey();
@@ -394,14 +488,27 @@ export class SparkWallet {
394
488
  }
395
489
  return depositNodes;
396
490
  }
491
+ /**
492
+ * Queries the mempool for transactions associated with an address.
493
+ *
494
+ * @param {string} address - The address to query
495
+ * @returns {Promise<{depositTx: Transaction, vout: number} | null>} Transaction details or null if none found
496
+ * @private
497
+ */
397
498
  async queryMempoolTxs(address) {
398
- const baseUrl = "https://regtest-mempool.dev.dev.sparkinfra.net/api";
499
+ const network = getNetworkFromAddress(address) || this.config.getNetwork();
500
+ const baseUrl = network === BitcoinNetwork.REGTEST
501
+ ? "https://regtest-mempool.dev.dev.sparkinfra.net/api"
502
+ : "https://mempool.space/docs/api/rest";
399
503
  const auth = btoa("spark-sdk:mCMk1JqlBNtetUNy");
504
+ const headers = {
505
+ "Content-Type": "application/json",
506
+ };
507
+ if (network === BitcoinNetwork.REGTEST) {
508
+ headers["Authorization"] = `Basic ${auth}`;
509
+ }
400
510
  const response = await fetch(`${baseUrl}/address/${address}/txs`, {
401
- headers: {
402
- Authorization: `Basic ${auth}`,
403
- "Content-Type": "application/json",
404
- },
511
+ headers,
405
512
  });
406
513
  const addressTxs = await response.json();
407
514
  if (addressTxs && addressTxs.length > 0) {
@@ -411,10 +518,7 @@ export class SparkWallet {
411
518
  return null;
412
519
  }
413
520
  const txResponse = await fetch(`${baseUrl}/tx/${latestTx.txid}/hex`, {
414
- headers: {
415
- Authorization: `Basic ${auth}`,
416
- "Content-Type": "application/json",
417
- },
521
+ headers,
418
522
  });
419
523
  const txHex = await txResponse.text();
420
524
  const depositTx = getTxFromRawTxHex(txHex);
@@ -425,6 +529,14 @@ export class SparkWallet {
425
529
  }
426
530
  return null;
427
531
  }
532
+ /**
533
+ * Transfers deposit to self to claim ownership.
534
+ *
535
+ * @param {TreeNode[]} leaves - The leaves to transfer
536
+ * @param {Uint8Array} signingPubKey - The signing public key
537
+ * @returns {Promise<TreeNode[] | undefined>} The nodes resulting from the transfer
538
+ * @private
539
+ */
428
540
  async transferDepositToSelf(leaves, signingPubKey) {
429
541
  const leafKeyTweaks = await Promise.all(leaves.map(async (leaf) => ({
430
542
  leaf,
@@ -440,12 +552,27 @@ export class SparkWallet {
440
552
  return;
441
553
  }
442
554
  // ***** Transfer Flow *****
555
+ /**
556
+ * Sends a transfer to another Spark user.
557
+ *
558
+ * @param {Object} params - Parameters for the transfer
559
+ * @param {string} params.receiverSparkAddress - The recipient's Spark address
560
+ * @param {number} params.amountSats - Amount to send in satoshis
561
+ * @returns {Promise<Transfer>} The completed transfer details
562
+ */
443
563
  async sendSparkTransfer({ receiverSparkAddress, amountSats, }) {
444
564
  return await this._sendTransfer({
445
565
  receiverPubKey: receiverSparkAddress,
446
566
  amount: amountSats,
447
567
  });
448
568
  }
569
+ /**
570
+ * Internal method to send a transfer.
571
+ *
572
+ * @param {SendTransferParams} params - Parameters for the transfer
573
+ * @returns {Promise<Transfer>} The completed transfer details
574
+ * @private
575
+ */
449
576
  async _sendTransfer({ amount, receiverPubKey, leaves, expiryTime = new Date(Date.now() + 10 * 60 * 1000), }) {
450
577
  return await this.sendTransferMutex.runExclusive(async () => {
451
578
  let leavesToSend = [];
@@ -460,6 +587,7 @@ export class SparkWallet {
460
587
  else {
461
588
  throw new Error("Must provide amount or leaves");
462
589
  }
590
+ await this.#refreshTimelockNodes();
463
591
  const leafKeyTweaks = await Promise.all(leavesToSend.map(async (leaf) => ({
464
592
  leaf,
465
593
  signingPubKey: await this.config.signer.generatePublicKey(sha256(leaf.id)),
@@ -471,6 +599,83 @@ export class SparkWallet {
471
599
  return transfer;
472
600
  });
473
601
  }
602
+ /**
603
+ * Internal method to refresh timelock nodes.
604
+ *
605
+ * @param {string} nodeId - The optional ID of the node to refresh. If not provided, all nodes will be checked.
606
+ * @returns {Promise<void>}
607
+ * @private
608
+ */
609
+ async #refreshTimelockNodes(nodeId) {
610
+ const nodesToRefresh = [];
611
+ const nodeIds = [];
612
+ if (nodeId) {
613
+ for (const node of this.leaves) {
614
+ if (node.id === nodeId) {
615
+ nodesToRefresh.push(node);
616
+ nodeIds.push(node.id);
617
+ break;
618
+ }
619
+ }
620
+ if (nodesToRefresh.length === 0) {
621
+ throw new Error(`node ${nodeId} not found`);
622
+ }
623
+ }
624
+ else {
625
+ for (const node of this.leaves) {
626
+ const refundTx = getTxFromRawTxBytes(node.refundTx);
627
+ const nextSequence = getNextTransactionSequence(refundTx.getInput(0).sequence);
628
+ const needRefresh = nextSequence <= 0;
629
+ if (needRefresh) {
630
+ nodesToRefresh.push(node);
631
+ nodeIds.push(node.id);
632
+ }
633
+ }
634
+ }
635
+ if (nodesToRefresh.length === 0) {
636
+ return;
637
+ }
638
+ const sparkClient = await this.connectionManager.createSparkClient(this.config.getCoordinatorAddress());
639
+ const nodesResp = await sparkClient.query_nodes({
640
+ source: {
641
+ $case: "nodeIds",
642
+ nodeIds: {
643
+ nodeIds,
644
+ },
645
+ },
646
+ includeParents: true,
647
+ });
648
+ const nodesMap = new Map();
649
+ for (const node of Object.values(nodesResp.nodes)) {
650
+ nodesMap.set(node.id, node);
651
+ }
652
+ for (const node of nodesToRefresh) {
653
+ if (!node.parentNodeId) {
654
+ throw new Error(`node ${node.id} has no parent`);
655
+ }
656
+ const parentNode = nodesMap.get(node.parentNodeId);
657
+ if (!parentNode) {
658
+ throw new Error(`parent node ${node.parentNodeId} not found`);
659
+ }
660
+ const { nodes } = await this.transferService.refreshTimelockNodes([node], parentNode, await this.config.signer.generatePublicKey(sha256(node.id)));
661
+ if (nodes.length !== 1) {
662
+ throw new Error(`expected 1 node, got ${nodes.length}`);
663
+ }
664
+ const newNode = nodes[0];
665
+ if (!newNode) {
666
+ throw new Error("Failed to refresh timelock node");
667
+ }
668
+ this.leaves = this.leaves.filter((leaf) => leaf.id !== node.id);
669
+ this.leaves.push(newNode);
670
+ }
671
+ }
672
+ /**
673
+ * Claims a specific transfer.
674
+ *
675
+ * @param {Transfer} transfer - The transfer to claim
676
+ * @returns {Promise<Object>} The claim result
677
+ * @private
678
+ */
474
679
  async claimTransfer(transfer) {
475
680
  return await this.claimTransferMutex.runExclusive(async () => {
476
681
  const leafPubKeyMap = await this.transferService.verifyPendingTransfer(transfer);
@@ -487,9 +692,18 @@ export class SparkWallet {
487
692
  }
488
693
  }
489
694
  }
490
- return await this.transferService.claimTransfer(transfer, leavesToClaim);
695
+ const response = await this.transferService.claimTransfer(transfer, leavesToClaim);
696
+ this.leaves.push(...response.nodes);
697
+ await this.#refreshTimelockNodes();
698
+ return response;
491
699
  });
492
700
  }
701
+ /**
702
+ * Claims all pending transfers.
703
+ *
704
+ * @returns {Promise<boolean>} True if any transfers were claimed
705
+ * @private
706
+ */
493
707
  async claimTransfers() {
494
708
  const transfers = await this.transferService.queryPendingTransfers();
495
709
  let claimed = false;
@@ -506,6 +720,12 @@ export class SparkWallet {
506
720
  }
507
721
  return claimed;
508
722
  }
723
+ /**
724
+ * Cancels all sender-initiated transfers.
725
+ *
726
+ * @returns {Promise<void>}
727
+ * @private
728
+ */
509
729
  async cancelAllSenderInitiatedTransfers() {
510
730
  const transfers = await this.transferService.queryPendingTransfersBySender();
511
731
  for (const transfer of transfers.transfers) {
@@ -515,6 +735,14 @@ export class SparkWallet {
515
735
  }
516
736
  }
517
737
  // ***** Lightning Flow *****
738
+ /**
739
+ * Creates a Lightning invoice for receiving payments.
740
+ *
741
+ * @param {Object} params - Parameters for the lightning invoice
742
+ * @param {number} params.amountSats - Amount in satoshis
743
+ * @param {string} params.memo - Description for the invoice
744
+ * @returns {Promise<string>} BOLT11 encoded invoice
745
+ */
518
746
  async createLightningInvoice({ amountSats, memo, }) {
519
747
  const expirySeconds = 60 * 60 * 24 * 30;
520
748
  if (!this.sspClient) {
@@ -544,6 +772,13 @@ export class SparkWallet {
544
772
  invoiceCreator: requestLightningInvoice,
545
773
  });
546
774
  }
775
+ /**
776
+ * Pays a Lightning invoice.
777
+ *
778
+ * @param {Object} params - Parameters for paying the invoice
779
+ * @param {string} params.invoice - The BOLT11-encoded Lightning invoice to pay
780
+ * @returns {Promise<LightningSendRequest>} The Lightning payment request details
781
+ */
547
782
  async payLightningInvoice({ invoice }) {
548
783
  if (!this.sspClient) {
549
784
  throw new Error("SSP client not initialized");
@@ -561,6 +796,7 @@ export class SparkWallet {
561
796
  }
562
797
  // fetch leaves for amount
563
798
  const leaves = await this.selectLeaves(amountSats);
799
+ await this.#refreshTimelockNodes();
564
800
  const leavesToSend = await Promise.all(leaves.map(async (leaf) => ({
565
801
  leaf,
566
802
  signingPubKey: await this.config.signer.generatePublicKey(sha256(leaf.id)),
@@ -588,12 +824,26 @@ export class SparkWallet {
588
824
  this.leaves = this.leaves.filter((leaf) => !leavesToRemove.has(leaf.id));
589
825
  return sspResponse;
590
826
  }
827
+ /**
828
+ * Gets fee estimate for receiving Lightning payments.
829
+ *
830
+ * @param {LightningReceiveFeeEstimateInput} params - Input parameters for fee estimation
831
+ * @returns {Promise<LightningReceiveFeeEstimateOutput | null>} Fee estimate for receiving Lightning payments
832
+ * @private
833
+ */
591
834
  async getLightningReceiveFeeEstimate({ amountSats, network, }) {
592
835
  if (!this.sspClient) {
593
836
  throw new Error("SSP client not initialized");
594
837
  }
595
838
  return await this.sspClient.getLightningReceiveFeeEstimate(amountSats, network);
596
839
  }
840
+ /**
841
+ * Gets fee estimate for sending Lightning payments.
842
+ *
843
+ * @param {LightningSendFeeEstimateInput} params - Input parameters for fee estimation
844
+ * @returns {Promise<LightningSendFeeEstimateOutput | null>} Fee estimate for sending Lightning payments
845
+ * @private
846
+ */
597
847
  async getLightningSendFeeEstimate({ encodedInvoice, }) {
598
848
  if (!this.sspClient) {
599
849
  throw new Error("SSP client not initialized");
@@ -601,16 +851,53 @@ export class SparkWallet {
601
851
  return await this.sspClient.getLightningSendFeeEstimate(encodedInvoice);
602
852
  }
603
853
  // ***** Tree Creation Flow *****
854
+ /**
855
+ * Generates a deposit address for a tree.
856
+ *
857
+ * @param {number} vout - The vout index
858
+ * @param {Uint8Array} parentSigningPubKey - The parent signing public key
859
+ * @param {Transaction} [parentTx] - Optional parent transaction
860
+ * @param {TreeNode} [parentNode] - Optional parent node
861
+ * @returns {Promise<Object>} Deposit address information
862
+ * @private
863
+ */
604
864
  async generateDepositAddressForTree(vout, parentSigningPubKey, parentTx, parentNode) {
605
865
  return await this.treeCreationService.generateDepositAddressForTree(vout, parentSigningPubKey, parentTx, parentNode);
606
866
  }
867
+ /**
868
+ * Creates a tree structure.
869
+ *
870
+ * @param {number} vout - The vout index
871
+ * @param {DepositAddressTree} root - The root of the tree
872
+ * @param {boolean} createLeaves - Whether to create leaves
873
+ * @param {Transaction} [parentTx] - Optional parent transaction
874
+ * @param {TreeNode} [parentNode] - Optional parent node
875
+ * @returns {Promise<Object>} The created tree
876
+ * @private
877
+ */
607
878
  async createTree(vout, root, createLeaves, parentTx, parentNode) {
608
879
  return await this.treeCreationService.createTree(vout, root, createLeaves, parentTx, parentNode);
609
880
  }
610
881
  // ***** Cooperative Exit Flow *****
882
+ /**
883
+ * Initiates a withdrawal to move funds from the Spark network to an on-chain Bitcoin address.
884
+ *
885
+ * @param {Object} params - Parameters for the withdrawal
886
+ * @param {string} params.onchainAddress - The Bitcoin address where the funds should be sent
887
+ * @param {number} [params.targetAmountSats] - The amount in satoshis to withdraw. If not specified, attempts to withdraw all available funds
888
+ * @returns {Promise<CoopExitRequest | null | undefined>} The withdrawal request details, or null/undefined if the request cannot be completed
889
+ */
611
890
  async withdraw({ onchainAddress, targetAmountSats, }) {
612
891
  return this.coopExit(onchainAddress, targetAmountSats);
613
892
  }
893
+ /**
894
+ * Internal method to perform a cooperative exit (withdrawal).
895
+ *
896
+ * @param {string} onchainAddress - The Bitcoin address where the funds should be sent
897
+ * @param {number} [targetAmountSats] - The amount in satoshis to withdraw
898
+ * @returns {Promise<Object | null | undefined>} The exit request details
899
+ * @private
900
+ */
614
901
  async coopExit(onchainAddress, targetAmountSats) {
615
902
  let leavesToSend = [];
616
903
  if (targetAmountSats) {
@@ -621,7 +908,6 @@ export class SparkWallet {
621
908
  ...leaf,
622
909
  }));
623
910
  }
624
- const pubkey = await this.config.signer.getIdentityPublicKey();
625
911
  const leafKeyTweaks = await Promise.all(leavesToSend.map(async (leaf) => ({
626
912
  leaf,
627
913
  signingPubKey: await this.config.signer.generatePublicKey(sha256(leaf.id)),
@@ -635,18 +921,22 @@ export class SparkWallet {
635
921
  throw new Error("Failed to request coop exit");
636
922
  }
637
923
  const connectorTx = getTxFromRawTxHex(coopExitRequest.rawConnectorTransaction);
638
- const coopExitTxId = getTxId(connectorTx);
924
+ const coopExitTxId = connectorTx.getInput(0).txid;
925
+ const connectorTxId = getTxId(connectorTx);
926
+ if (!coopExitTxId) {
927
+ throw new Error("Failed to get coop exit tx id");
928
+ }
639
929
  const connectorOutputs = [];
640
930
  for (let i = 0; i < connectorTx.outputsLength - 1; i++) {
641
931
  connectorOutputs.push({
642
- txid: hexToBytes(coopExitTxId),
932
+ txid: hexToBytes(connectorTxId),
643
933
  index: i,
644
934
  });
645
935
  }
646
936
  const sspPubIdentityKey = await this.config.signer.getSspIdentityPublicKey(this.config.getNetwork());
647
937
  const transfer = await this.coopExitService.getConnectorRefundSignatures({
648
938
  leaves: leafKeyTweaks,
649
- exitTxId: hexToBytes(coopExitTxId),
939
+ exitTxId: coopExitTxId,
650
940
  connectorOutputs,
651
941
  receiverPubKey: sspPubIdentityKey,
652
942
  });
@@ -656,6 +946,13 @@ export class SparkWallet {
656
946
  });
657
947
  return completeResponse;
658
948
  }
949
+ /**
950
+ * Gets fee estimate for cooperative exit (on-chain withdrawal).
951
+ *
952
+ * @param {CoopExitFeeEstimateInput} params - Input parameters for fee estimation
953
+ * @returns {Promise<CoopExitFeeEstimateOutput | null>} Fee estimate for the withdrawal
954
+ * @private
955
+ */
659
956
  async getCoopExitFeeEstimate({ leafExternalIds, withdrawalAddress, }) {
660
957
  if (!this.sspClient) {
661
958
  throw new Error("SSP client not initialized");
@@ -666,6 +963,12 @@ export class SparkWallet {
666
963
  });
667
964
  }
668
965
  // ***** Token Flow *****
966
+ /**
967
+ * Synchronizes token leaves for the wallet.
968
+ *
969
+ * @returns {Promise<void>}
970
+ * @private
971
+ */
669
972
  async syncTokenLeaves() {
670
973
  this.tokenLeaves.clear();
671
974
  const trackedPublicKeys = await this.config.signer.getTrackedPublicKeys();
@@ -685,6 +988,12 @@ export class SparkWallet {
685
988
  });
686
989
  this.tokenLeaves = groupedLeaves;
687
990
  }
991
+ /**
992
+ * Gets all token balances.
993
+ *
994
+ * @returns {Promise<Map<string, { balance: bigint, leafCount: number }>>} Map of token balances and leaf counts
995
+ * @private
996
+ */
688
997
  async getAllTokenBalances() {
689
998
  await this.syncTokenLeaves();
690
999
  const balances = new Map();
@@ -696,6 +1005,15 @@ export class SparkWallet {
696
1005
  }
697
1006
  return balances;
698
1007
  }
1008
+ /**
1009
+ * Transfers tokens to another user.
1010
+ *
1011
+ * @param {string} tokenPublicKey - The public key of the token to transfer
1012
+ * @param {bigint} tokenAmount - The amount of tokens to transfer
1013
+ * @param {string} recipientPublicKey - The recipient's public key
1014
+ * @param {LeafWithPreviousTransactionData[]} [selectedLeaves] - Optional specific leaves to use for the transfer
1015
+ * @returns {Promise<string>} The transaction ID of the token transfer
1016
+ */
699
1017
  async transferTokens(tokenPublicKey, tokenAmount, recipientPublicKey, selectedLeaves) {
700
1018
  await this.syncTokenLeaves();
701
1019
  if (!this.tokenLeaves.has(tokenPublicKey)) {
@@ -717,8 +1035,38 @@ export class SparkWallet {
717
1035
  const tokenTransaction = await this.tokenTransactionService.constructTransferTokenTransaction(selectedLeaves, recipientPublicKeyBytes, tokenPublicKeyBytes, tokenAmount);
718
1036
  return await this.tokenTransactionService.broadcastTokenTransaction(tokenTransaction, selectedLeaves.map((leaf) => leaf.leaf.ownerPublicKey), selectedLeaves.map((leaf) => leaf.leaf.revocationPublicKey));
719
1037
  }
1038
+ /**
1039
+ * Selects token leaves for a transfer.
1040
+ *
1041
+ * @param {string} tokenPublicKey - The public key of the token
1042
+ * @param {bigint} tokenAmount - The amount of tokens to select leaves for
1043
+ * @returns {LeafWithPreviousTransactionData[]} The selected leaves
1044
+ * @private
1045
+ */
720
1046
  selectTokenLeaves(tokenPublicKey, tokenAmount) {
721
1047
  return this.tokenTransactionService.selectTokenLeaves(this.tokenLeaves.get(tokenPublicKey), tokenAmount);
722
1048
  }
723
1049
  }
1050
+ /**
1051
+ * Utility function to determine the network from a Bitcoin address.
1052
+ *
1053
+ * @param {string} address - The Bitcoin address
1054
+ * @returns {BitcoinNetwork | null} The detected network or null if not detected
1055
+ */
1056
+ function getNetworkFromAddress(address) {
1057
+ try {
1058
+ const decoded = bitcoin.address.fromBech32(address);
1059
+ // HRP (human-readable part) determines the network
1060
+ if (decoded.prefix === "bc") {
1061
+ return BitcoinNetwork.MAINNET;
1062
+ }
1063
+ else if (decoded.prefix === "bcrt") {
1064
+ return BitcoinNetwork.REGTEST;
1065
+ }
1066
+ }
1067
+ catch (err) {
1068
+ throw new Error("Invalid Bitcoin address");
1069
+ }
1070
+ return null;
1071
+ }
724
1072
  //# sourceMappingURL=spark-sdk.js.map