@arkade-os/sdk 0.3.8 → 0.3.10

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 (41) hide show
  1. package/README.md +78 -1
  2. package/dist/cjs/identity/singleKey.js +33 -1
  3. package/dist/cjs/index.js +17 -2
  4. package/dist/cjs/intent/index.js +31 -2
  5. package/dist/cjs/providers/ark.js +15 -5
  6. package/dist/cjs/providers/indexer.js +2 -2
  7. package/dist/cjs/wallet/batch.js +183 -0
  8. package/dist/cjs/wallet/index.js +15 -0
  9. package/dist/cjs/wallet/serviceWorker/request.js +0 -2
  10. package/dist/cjs/wallet/serviceWorker/wallet.js +98 -34
  11. package/dist/cjs/wallet/serviceWorker/worker.js +163 -72
  12. package/dist/cjs/wallet/utils.js +2 -2
  13. package/dist/cjs/wallet/vtxo-manager.js +5 -0
  14. package/dist/cjs/wallet/wallet.js +358 -360
  15. package/dist/esm/identity/singleKey.js +31 -0
  16. package/dist/esm/index.js +12 -7
  17. package/dist/esm/intent/index.js +31 -2
  18. package/dist/esm/providers/ark.js +15 -5
  19. package/dist/esm/providers/indexer.js +2 -2
  20. package/dist/esm/wallet/batch.js +180 -0
  21. package/dist/esm/wallet/index.js +14 -0
  22. package/dist/esm/wallet/serviceWorker/request.js +0 -2
  23. package/dist/esm/wallet/serviceWorker/wallet.js +96 -33
  24. package/dist/esm/wallet/serviceWorker/worker.js +165 -74
  25. package/dist/esm/wallet/utils.js +2 -2
  26. package/dist/esm/wallet/vtxo-manager.js +6 -1
  27. package/dist/esm/wallet/wallet.js +359 -363
  28. package/dist/types/identity/index.d.ts +5 -3
  29. package/dist/types/identity/singleKey.d.ts +20 -1
  30. package/dist/types/index.d.ts +11 -8
  31. package/dist/types/intent/index.d.ts +19 -2
  32. package/dist/types/providers/ark.d.ts +9 -8
  33. package/dist/types/providers/indexer.d.ts +2 -2
  34. package/dist/types/wallet/batch.d.ts +87 -0
  35. package/dist/types/wallet/index.d.ts +76 -16
  36. package/dist/types/wallet/serviceWorker/request.d.ts +5 -1
  37. package/dist/types/wallet/serviceWorker/wallet.d.ts +46 -15
  38. package/dist/types/wallet/serviceWorker/worker.d.ts +6 -3
  39. package/dist/types/wallet/utils.d.ts +8 -3
  40. package/dist/types/wallet/wallet.d.ts +87 -36
  41. package/package.json +1 -1
@@ -33,7 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.Wallet = void 0;
36
+ exports.Wallet = exports.ReadonlyWallet = void 0;
37
+ exports.getSequence = getSequence;
37
38
  exports.waitForIncomingFunds = waitForIncomingFunds;
38
39
  const base_1 = require("@scure/base");
39
40
  const bip68 = __importStar(require("bip68"));
@@ -56,74 +57,40 @@ const vtxo_manager_1 = require("./vtxo-manager");
56
57
  const arknote_1 = require("../arknote");
57
58
  const intent_1 = require("../intent");
58
59
  const indexer_1 = require("../providers/indexer");
59
- const txTree_1 = require("../tree/txTree");
60
60
  const unknownFields_1 = require("../utils/unknownFields");
61
61
  const inMemory_1 = require("../storage/inMemory");
62
62
  const walletRepository_1 = require("../repositories/walletRepository");
63
63
  const contractRepository_1 = require("../repositories/contractRepository");
64
64
  const utils_1 = require("./utils");
65
65
  const errors_1 = require("../providers/errors");
66
+ const batch_1 = require("./batch");
66
67
  /**
67
- * Main wallet implementation for Bitcoin transactions with Ark protocol support.
68
- * The wallet does not store any data locally and relies on Ark and onchain
69
- * providers to fetch UTXOs and VTXOs.
70
- *
71
- * @example
72
- * ```typescript
73
- * // Create a wallet with URL configuration
74
- * const wallet = await Wallet.create({
75
- * identity: SingleKey.fromHex('your_private_key'),
76
- * arkServerUrl: 'https://ark.example.com',
77
- * esploraUrl: 'https://mempool.space/api'
78
- * });
79
- *
80
- * // Or with custom provider instances (e.g., for Expo/React Native)
81
- * const wallet = await Wallet.create({
82
- * identity: SingleKey.fromHex('your_private_key'),
83
- * arkProvider: new ExpoArkProvider('https://ark.example.com'),
84
- * indexerProvider: new ExpoIndexerProvider('https://ark.example.com'),
85
- * esploraUrl: 'https://mempool.space/api'
86
- * });
87
- *
88
- * // Get addresses
89
- * const arkAddress = await wallet.getAddress();
90
- * const boardingAddress = await wallet.getBoardingAddress();
91
- *
92
- * // Send bitcoin
93
- * const txid = await wallet.sendBitcoin({
94
- * address: 'tb1...',
95
- * amount: 50000
96
- * });
97
- * ```
68
+ * Type guard function to check if an identity has a toReadonly method.
98
69
  */
99
- class Wallet {
100
- constructor(identity, network, networkName, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository, renewalConfig) {
70
+ function hasToReadonly(identity) {
71
+ return (typeof identity === "object" &&
72
+ identity !== null &&
73
+ "toReadonly" in identity &&
74
+ typeof identity.toReadonly === "function");
75
+ }
76
+ class ReadonlyWallet {
77
+ constructor(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository) {
101
78
  this.identity = identity;
102
79
  this.network = network;
103
- this.networkName = networkName;
104
80
  this.onchainProvider = onchainProvider;
105
- this.arkProvider = arkProvider;
106
81
  this.indexerProvider = indexerProvider;
107
82
  this.arkServerPublicKey = arkServerPublicKey;
108
83
  this.offchainTapscript = offchainTapscript;
109
84
  this.boardingTapscript = boardingTapscript;
110
- this.serverUnrollScript = serverUnrollScript;
111
- this.forfeitOutputScript = forfeitOutputScript;
112
- this.forfeitPubkey = forfeitPubkey;
113
85
  this.dustAmount = dustAmount;
114
86
  this.walletRepository = walletRepository;
115
87
  this.contractRepository = contractRepository;
116
- this.renewalConfig = {
117
- enabled: renewalConfig?.enabled ?? false,
118
- ...vtxo_manager_1.DEFAULT_RENEWAL_CONFIG,
119
- ...renewalConfig,
120
- };
121
88
  }
122
- static async create(config) {
123
- const pubkey = await config.identity.xOnlyPublicKey();
124
- if (!pubkey) {
125
- throw new Error("Invalid configured public key");
126
- }
89
+ /**
90
+ * Protected helper to set up shared wallet configuration.
91
+ * Extracts common logic used by both ReadonlyWallet.create() and Wallet.create().
92
+ */
93
+ static async setupWalletConfig(config, pubkey) {
127
94
  // Use provided arkProvider instance or create a new one from arkServerUrl
128
95
  const arkProvider = config.arkProvider ||
129
96
  (() => {
@@ -187,25 +154,32 @@ class Wallet {
187
154
  });
188
155
  // Save tapscripts
189
156
  const offchainTapscript = bareVtxoTapscript;
190
- // the serverUnrollScript is the one used to create output scripts of the checkpoint transactions
191
- let serverUnrollScript;
192
- try {
193
- const raw = base_1.hex.decode(info.checkpointTapscript);
194
- serverUnrollScript = tapscript_1.CSVMultisigTapscript.decode(raw);
195
- }
196
- catch (e) {
197
- throw new Error("Invalid checkpointTapscript from server");
198
- }
199
- // parse the server forfeit address
200
- // server is expecting funds to be sent to this address
201
- const forfeitPubkey = base_1.hex.decode(info.forfeitPubkey).slice(1);
202
- const forfeitAddress = (0, btc_signer_1.Address)(network).decode(info.forfeitAddress);
203
- const forfeitOutputScript = btc_signer_1.OutScript.encode(forfeitAddress);
204
157
  // Set up storage and repositories
205
158
  const storage = config.storage || new inMemory_1.InMemoryStorageAdapter();
206
159
  const walletRepository = new walletRepository_1.WalletRepositoryImpl(storage);
207
160
  const contractRepository = new contractRepository_1.ContractRepositoryImpl(storage);
208
- return new Wallet(config.identity, network, info.network, onchainProvider, arkProvider, indexerProvider, serverPubKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, info.dust, walletRepository, contractRepository, config.renewalConfig);
161
+ return {
162
+ arkProvider,
163
+ indexerProvider,
164
+ onchainProvider,
165
+ network,
166
+ networkName: info.network,
167
+ serverPubKey,
168
+ offchainTapscript,
169
+ boardingTapscript,
170
+ dustAmount: info.dust,
171
+ walletRepository,
172
+ contractRepository,
173
+ info,
174
+ };
175
+ }
176
+ static async create(config) {
177
+ const pubkey = await config.identity.xOnlyPublicKey();
178
+ if (!pubkey) {
179
+ throw new Error("Invalid configured public key");
180
+ }
181
+ const setup = await ReadonlyWallet.setupWalletConfig(config, pubkey);
182
+ return new ReadonlyWallet(config.identity, setup.network, setup.onchainProvider, setup.indexerProvider, setup.serverPubKey, setup.offchainTapscript, setup.boardingTapscript, setup.dustAmount, setup.walletRepository, setup.contractRepository);
209
183
  }
210
184
  get arkAddress() {
211
185
  return this.offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
@@ -280,7 +254,7 @@ class Wallet {
280
254
  let vtxos = allVtxos.filter(_1.isSpendable);
281
255
  // all recoverable vtxos are spendable by definition
282
256
  if (!filter.withRecoverable) {
283
- vtxos = vtxos.filter((vtxo) => !(0, _1.isRecoverable)(vtxo));
257
+ vtxos = vtxos.filter((vtxo) => !(0, _1.isRecoverable)(vtxo) && !(0, _1.isExpired)(vtxo));
284
258
  }
285
259
  if (filter.withUnrolled) {
286
260
  const spentVtxos = allVtxos.filter((vtxo) => !(0, _1.isSpendable)(vtxo));
@@ -289,9 +263,6 @@ class Wallet {
289
263
  return vtxos;
290
264
  }
291
265
  async getTransactionHistory() {
292
- if (!this.indexerProvider) {
293
- return [];
294
- }
295
266
  const response = await this.indexerProvider.getVtxos({
296
267
  scripts: [base_1.hex.encode(this.offchainTapscript.pkScript)],
297
268
  });
@@ -395,6 +366,179 @@ class Wallet {
395
366
  await this.walletRepository.saveUtxos(boardingAddress, utxos);
396
367
  return utxos;
397
368
  }
369
+ async notifyIncomingFunds(eventCallback) {
370
+ const arkAddress = await this.getAddress();
371
+ const boardingAddress = await this.getBoardingAddress();
372
+ let onchainStopFunc;
373
+ let indexerStopFunc;
374
+ if (this.onchainProvider && boardingAddress) {
375
+ const findVoutOnTx = (tx) => {
376
+ return tx.vout.findIndex((v) => v.scriptpubkey_address === boardingAddress);
377
+ };
378
+ onchainStopFunc = await this.onchainProvider.watchAddresses([boardingAddress], (txs) => {
379
+ // find all utxos belonging to our boarding address
380
+ const coins = txs
381
+ // filter txs where address is in output
382
+ .filter((tx) => findVoutOnTx(tx) !== -1)
383
+ // return utxo as Coin
384
+ .map((tx) => {
385
+ const { txid, status } = tx;
386
+ const vout = findVoutOnTx(tx);
387
+ const value = Number(tx.vout[vout].value);
388
+ return { txid, vout, value, status };
389
+ });
390
+ // and notify via callback
391
+ eventCallback({
392
+ type: "utxo",
393
+ coins,
394
+ });
395
+ });
396
+ }
397
+ if (this.indexerProvider && arkAddress) {
398
+ const offchainScript = this.offchainTapscript;
399
+ const subscriptionId = await this.indexerProvider.subscribeForScripts([
400
+ base_1.hex.encode(offchainScript.pkScript),
401
+ ]);
402
+ const abortController = new AbortController();
403
+ const subscription = this.indexerProvider.getSubscription(subscriptionId, abortController.signal);
404
+ indexerStopFunc = async () => {
405
+ abortController.abort();
406
+ await this.indexerProvider?.unsubscribeForScripts(subscriptionId);
407
+ };
408
+ // Handle subscription updates asynchronously without blocking
409
+ (async () => {
410
+ try {
411
+ for await (const update of subscription) {
412
+ if (update.newVtxos?.length > 0 ||
413
+ update.spentVtxos?.length > 0) {
414
+ eventCallback({
415
+ type: "vtxo",
416
+ newVtxos: update.newVtxos.map((vtxo) => (0, utils_1.extendVirtualCoin)(this, vtxo)),
417
+ spentVtxos: update.spentVtxos.map((vtxo) => (0, utils_1.extendVirtualCoin)(this, vtxo)),
418
+ });
419
+ }
420
+ }
421
+ }
422
+ catch (error) {
423
+ console.error("Subscription error:", error);
424
+ }
425
+ })();
426
+ }
427
+ const stopFunc = () => {
428
+ onchainStopFunc?.();
429
+ indexerStopFunc?.();
430
+ };
431
+ return stopFunc;
432
+ }
433
+ async fetchPendingTxs() {
434
+ // get non-swept VTXOs, rely on the indexer only in case DB doesn't have the right state
435
+ const scripts = [base_1.hex.encode(this.offchainTapscript.pkScript)];
436
+ let { vtxos } = await this.indexerProvider.getVtxos({
437
+ scripts,
438
+ });
439
+ return vtxos
440
+ .filter((vtxo) => vtxo.virtualStatus.state !== "swept" &&
441
+ vtxo.virtualStatus.state !== "settled" &&
442
+ vtxo.arkTxId !== undefined)
443
+ .map((_) => _.arkTxId);
444
+ }
445
+ }
446
+ exports.ReadonlyWallet = ReadonlyWallet;
447
+ /**
448
+ * Main wallet implementation for Bitcoin transactions with Ark protocol support.
449
+ * The wallet does not store any data locally and relies on Ark and onchain
450
+ * providers to fetch UTXOs and VTXOs.
451
+ *
452
+ * @example
453
+ * ```typescript
454
+ * // Create a wallet with URL configuration
455
+ * const wallet = await Wallet.create({
456
+ * identity: SingleKey.fromHex('your_private_key'),
457
+ * arkServerUrl: 'https://ark.example.com',
458
+ * esploraUrl: 'https://mempool.space/api'
459
+ * });
460
+ *
461
+ * // Or with custom provider instances (e.g., for Expo/React Native)
462
+ * const wallet = await Wallet.create({
463
+ * identity: SingleKey.fromHex('your_private_key'),
464
+ * arkProvider: new ExpoArkProvider('https://ark.example.com'),
465
+ * indexerProvider: new ExpoIndexerProvider('https://ark.example.com'),
466
+ * esploraUrl: 'https://mempool.space/api'
467
+ * });
468
+ *
469
+ * // Get addresses
470
+ * const arkAddress = await wallet.getAddress();
471
+ * const boardingAddress = await wallet.getBoardingAddress();
472
+ *
473
+ * // Send bitcoin
474
+ * const txid = await wallet.sendBitcoin({
475
+ * address: 'tb1...',
476
+ * amount: 50000
477
+ * });
478
+ * ```
479
+ */
480
+ class Wallet extends ReadonlyWallet {
481
+ constructor(identity, network, networkName, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository, renewalConfig) {
482
+ super(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository);
483
+ this.networkName = networkName;
484
+ this.arkProvider = arkProvider;
485
+ this.serverUnrollScript = serverUnrollScript;
486
+ this.forfeitOutputScript = forfeitOutputScript;
487
+ this.forfeitPubkey = forfeitPubkey;
488
+ this.identity = identity;
489
+ this.renewalConfig = {
490
+ enabled: renewalConfig?.enabled ?? false,
491
+ ...vtxo_manager_1.DEFAULT_RENEWAL_CONFIG,
492
+ ...renewalConfig,
493
+ };
494
+ }
495
+ static async create(config) {
496
+ const pubkey = await config.identity.xOnlyPublicKey();
497
+ if (!pubkey) {
498
+ throw new Error("Invalid configured public key");
499
+ }
500
+ const setup = await ReadonlyWallet.setupWalletConfig(config, pubkey);
501
+ // Compute Wallet-specific forfeit and unroll scripts
502
+ // the serverUnrollScript is the one used to create output scripts of the checkpoint transactions
503
+ let serverUnrollScript;
504
+ try {
505
+ const raw = base_1.hex.decode(setup.info.checkpointTapscript);
506
+ serverUnrollScript = tapscript_1.CSVMultisigTapscript.decode(raw);
507
+ }
508
+ catch (e) {
509
+ throw new Error("Invalid checkpointTapscript from server");
510
+ }
511
+ // parse the server forfeit address
512
+ // server is expecting funds to be sent to this address
513
+ const forfeitPubkey = base_1.hex.decode(setup.info.forfeitPubkey).slice(1);
514
+ const forfeitAddress = (0, btc_signer_1.Address)(setup.network).decode(setup.info.forfeitAddress);
515
+ const forfeitOutputScript = btc_signer_1.OutScript.encode(forfeitAddress);
516
+ return new Wallet(config.identity, setup.network, setup.networkName, setup.onchainProvider, setup.arkProvider, setup.indexerProvider, setup.serverPubKey, setup.offchainTapscript, setup.boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, setup.dustAmount, setup.walletRepository, setup.contractRepository, config.renewalConfig);
517
+ }
518
+ /**
519
+ * Convert this wallet to a readonly wallet.
520
+ *
521
+ * @returns A readonly wallet with the same configuration but readonly identity
522
+ * @example
523
+ * ```typescript
524
+ * const wallet = await Wallet.create({ identity: SingleKey.fromHex('...'), ... });
525
+ * const readonlyWallet = await wallet.toReadonly();
526
+ *
527
+ * // Can query balance and addresses
528
+ * const balance = await readonlyWallet.getBalance();
529
+ * const address = await readonlyWallet.getAddress();
530
+ *
531
+ * // But cannot send transactions (type error)
532
+ * // readonlyWallet.sendBitcoin(...); // TypeScript error
533
+ * ```
534
+ */
535
+ async toReadonly() {
536
+ // Check if the identity has a toReadonly method using type guard
537
+ const readonlyIdentity = hasToReadonly(this.identity)
538
+ ? await this.identity.toReadonly()
539
+ : this.identity; // Identity extends ReadonlyIdentity, so this is safe
540
+ return new ReadonlyWallet(readonlyIdentity, this.network, this.onchainProvider, this.indexerProvider, this.arkServerPublicKey, this.offchainTapscript, this.boardingTapscript, this.dustAmount, this.walletRepository, this.contractRepository);
541
+ }
398
542
  async sendBitcoin(params) {
399
543
  if (params.amount <= 0) {
400
544
  throw new Error("Amount must be positive");
@@ -406,7 +550,23 @@ class Wallet {
406
550
  const virtualCoins = await this.getVirtualCoins({
407
551
  withRecoverable: false,
408
552
  });
409
- const selected = selectVirtualCoins(virtualCoins, params.amount);
553
+ let selected;
554
+ if (params.selectedVtxos) {
555
+ const selectedVtxoSum = params.selectedVtxos
556
+ .map((v) => v.value)
557
+ .reduce((a, b) => a + b, 0);
558
+ if (selectedVtxoSum < params.amount) {
559
+ throw new Error("Selected VTXOs do not cover specified amount");
560
+ }
561
+ const changeAmount = selectedVtxoSum - params.amount;
562
+ selected = {
563
+ inputs: params.selectedVtxos,
564
+ changeAmount: BigInt(changeAmount),
565
+ };
566
+ }
567
+ else {
568
+ selected = selectVirtualCoins(virtualCoins, params.amount);
569
+ }
410
570
  const selectedLeaf = this.offchainTapscript.forfeit();
411
571
  if (!selectedLeaf) {
412
572
  throw new Error("Selected leaf not found");
@@ -481,7 +641,7 @@ class Wallet {
481
641
  vout: outputs.length - 1,
482
642
  createdAt: new Date(createdAt),
483
643
  forfeitTapLeafScript: this.offchainTapscript.forfeit(),
484
- intentTapLeafScript: this.offchainTapscript.exit(),
644
+ intentTapLeafScript: this.offchainTapscript.forfeit(),
485
645
  isUnrolled: false,
486
646
  isSpent: false,
487
647
  tapTree: this.offchainTapscript.encode(),
@@ -591,285 +751,31 @@ class Wallet {
591
751
  this.makeDeleteIntentSignature(params.inputs),
592
752
  ]);
593
753
  const intentId = await this.safeRegisterIntent(intent);
754
+ const topics = [
755
+ ...signingPublicKeys,
756
+ ...params.inputs.map((input) => `${input.txid}:${input.vout}`),
757
+ ];
758
+ const handler = this.createBatchHandler(intentId, params.inputs, session);
594
759
  const abortController = new AbortController();
595
- // listen to settlement events
596
760
  try {
597
- let step;
598
- const topics = [
599
- ...signingPublicKeys,
600
- ...params.inputs.map((input) => `${input.txid}:${input.vout}`),
601
- ];
602
- const settlementStream = this.arkProvider.getEventStream(abortController.signal, topics);
603
- // batchId, sweepTapTreeRoot and forfeitOutputScript are set once the BatchStarted event is received
604
- let batchId;
605
- let sweepTapTreeRoot;
606
- const vtxoChunks = [];
607
- const connectorsChunks = [];
608
- let vtxoGraph;
609
- let connectorsGraph;
610
- for await (const event of settlementStream) {
611
- if (eventCallback) {
612
- eventCallback(event);
613
- }
614
- switch (event.type) {
615
- // the settlement failed
616
- case ark_1.SettlementEventType.BatchFailed:
617
- throw new Error(event.reason);
618
- case ark_1.SettlementEventType.BatchStarted:
619
- if (step !== undefined) {
620
- continue;
621
- }
622
- const res = await this.handleBatchStartedEvent(event, intentId, this.forfeitPubkey, this.forfeitOutputScript);
623
- if (!res.skip) {
624
- step = event.type;
625
- sweepTapTreeRoot = res.sweepTapTreeRoot;
626
- batchId = res.roundId;
627
- if (!hasOffchainOutputs) {
628
- // if there are no offchain outputs, we don't have to handle musig2 tree signatures
629
- // we can directly advance to the finalization step
630
- step = ark_1.SettlementEventType.TreeNonces;
631
- }
632
- }
633
- break;
634
- case ark_1.SettlementEventType.TreeTx:
635
- if (step !== ark_1.SettlementEventType.BatchStarted &&
636
- step !== ark_1.SettlementEventType.TreeNonces) {
637
- continue;
638
- }
639
- // index 0 = vtxo tree
640
- if (event.batchIndex === 0) {
641
- vtxoChunks.push(event.chunk);
642
- // index 1 = connectors tree
643
- }
644
- else if (event.batchIndex === 1) {
645
- connectorsChunks.push(event.chunk);
646
- }
647
- else {
648
- throw new Error(`Invalid batch index: ${event.batchIndex}`);
649
- }
650
- break;
651
- case ark_1.SettlementEventType.TreeSignature:
652
- if (step !== ark_1.SettlementEventType.TreeNonces) {
653
- continue;
654
- }
655
- if (!hasOffchainOutputs) {
656
- continue;
657
- }
658
- if (!vtxoGraph) {
659
- throw new Error("Vtxo graph not set, something went wrong");
660
- }
661
- // index 0 = vtxo graph
662
- if (event.batchIndex === 0) {
663
- const tapKeySig = base_1.hex.decode(event.signature);
664
- vtxoGraph.update(event.txid, (tx) => {
665
- tx.updateInput(0, {
666
- tapKeySig,
667
- });
668
- });
669
- }
670
- break;
671
- // the server has started the signing process of the vtxo tree transactions
672
- // the server expects the partial musig2 nonces for each tx
673
- case ark_1.SettlementEventType.TreeSigningStarted:
674
- if (step !== ark_1.SettlementEventType.BatchStarted) {
675
- continue;
676
- }
677
- if (hasOffchainOutputs) {
678
- if (!session) {
679
- throw new Error("Signing session not set");
680
- }
681
- if (!sweepTapTreeRoot) {
682
- throw new Error("Sweep tap tree root not set");
683
- }
684
- if (vtxoChunks.length === 0) {
685
- throw new Error("unsigned vtxo graph not received");
686
- }
687
- vtxoGraph = txTree_1.TxTree.create(vtxoChunks);
688
- await this.handleSettlementSigningEvent(event, sweepTapTreeRoot, session, vtxoGraph);
689
- }
690
- step = event.type;
691
- break;
692
- // the musig2 nonces of the vtxo tree transactions are generated
693
- // the server expects now the partial musig2 signatures
694
- case ark_1.SettlementEventType.TreeNonces:
695
- if (step !== ark_1.SettlementEventType.TreeSigningStarted) {
696
- continue;
697
- }
698
- if (hasOffchainOutputs) {
699
- if (!session) {
700
- throw new Error("Signing session not set");
701
- }
702
- const signed = await this.handleSettlementTreeNoncesEvent(event, session);
703
- if (signed) {
704
- step = event.type;
705
- }
706
- break;
707
- }
708
- step = event.type;
709
- break;
710
- // the vtxo tree is signed, craft, sign and submit forfeit transactions
711
- // if any boarding utxos are involved, the settlement tx is also signed
712
- case ark_1.SettlementEventType.BatchFinalization:
713
- if (step !== ark_1.SettlementEventType.TreeNonces) {
714
- continue;
715
- }
716
- if (!this.forfeitOutputScript) {
717
- throw new Error("Forfeit output script not set");
718
- }
719
- if (connectorsChunks.length > 0) {
720
- connectorsGraph = txTree_1.TxTree.create(connectorsChunks);
721
- (0, validation_1.validateConnectorsTxGraph)(event.commitmentTx, connectorsGraph);
722
- }
723
- await this.handleSettlementFinalizationEvent(event, params.inputs, this.forfeitOutputScript, connectorsGraph);
724
- step = event.type;
725
- break;
726
- // the settlement is done, last event to be received
727
- case ark_1.SettlementEventType.BatchFinalized:
728
- if (step !== ark_1.SettlementEventType.BatchFinalization) {
729
- continue;
730
- }
731
- if (event.id === batchId) {
732
- abortController.abort();
733
- return event.commitmentTxid;
734
- }
735
- }
736
- }
761
+ const stream = this.arkProvider.getEventStream(abortController.signal, topics);
762
+ return await batch_1.Batch.join(stream, handler, {
763
+ abortController,
764
+ skipVtxoTreeSigning: !hasOffchainOutputs,
765
+ eventCallback: eventCallback
766
+ ? (event) => Promise.resolve(eventCallback(event))
767
+ : undefined,
768
+ });
737
769
  }
738
770
  catch (error) {
739
- // close the stream
740
- abortController.abort();
741
- try {
742
- // delete the intent to not be stuck in the queue
743
- await this.arkProvider.deleteIntent(deleteIntent);
744
- }
745
- catch (error) {
746
- console.error("failed to delete intent: ", error);
747
- }
771
+ // delete the intent to not be stuck in the queue
772
+ await this.arkProvider.deleteIntent(deleteIntent).catch(() => { });
748
773
  throw error;
749
774
  }
750
- throw new Error("Settlement failed");
751
- }
752
- async notifyIncomingFunds(eventCallback) {
753
- const arkAddress = await this.getAddress();
754
- const boardingAddress = await this.getBoardingAddress();
755
- let onchainStopFunc;
756
- let indexerStopFunc;
757
- if (this.onchainProvider && boardingAddress) {
758
- const findVoutOnTx = (tx) => {
759
- return tx.vout.findIndex((v) => v.scriptpubkey_address === boardingAddress);
760
- };
761
- onchainStopFunc = await this.onchainProvider.watchAddresses([boardingAddress], (txs) => {
762
- // find all utxos belonging to our boarding address
763
- const coins = txs
764
- // filter txs where address is in output
765
- .filter((tx) => findVoutOnTx(tx) !== -1)
766
- // return utxo as Coin
767
- .map((tx) => {
768
- const { txid, status } = tx;
769
- const vout = findVoutOnTx(tx);
770
- const value = Number(tx.vout[vout].value);
771
- return { txid, vout, value, status };
772
- });
773
- // and notify via callback
774
- eventCallback({
775
- type: "utxo",
776
- coins,
777
- });
778
- });
779
- }
780
- if (this.indexerProvider && arkAddress) {
781
- const offchainScript = this.offchainTapscript;
782
- const subscriptionId = await this.indexerProvider.subscribeForScripts([
783
- base_1.hex.encode(offchainScript.pkScript),
784
- ]);
785
- const abortController = new AbortController();
786
- const subscription = this.indexerProvider.getSubscription(subscriptionId, abortController.signal);
787
- indexerStopFunc = async () => {
788
- abortController.abort();
789
- await this.indexerProvider?.unsubscribeForScripts(subscriptionId);
790
- };
791
- // Handle subscription updates asynchronously without blocking
792
- (async () => {
793
- try {
794
- for await (const update of subscription) {
795
- if (update.newVtxos?.length > 0 ||
796
- update.spentVtxos?.length > 0) {
797
- eventCallback({
798
- type: "vtxo",
799
- newVtxos: update.newVtxos.map((vtxo) => (0, utils_1.extendVirtualCoin)(this, vtxo)),
800
- spentVtxos: update.spentVtxos.map((vtxo) => (0, utils_1.extendVirtualCoin)(this, vtxo)),
801
- });
802
- }
803
- }
804
- }
805
- catch (error) {
806
- console.error("Subscription error:", error);
807
- }
808
- })();
809
- }
810
- const stopFunc = () => {
811
- onchainStopFunc?.();
812
- indexerStopFunc?.();
813
- };
814
- return stopFunc;
815
- }
816
- async handleBatchStartedEvent(event, intentId, forfeitPubKey, forfeitOutputScript) {
817
- const utf8IntentId = new TextEncoder().encode(intentId);
818
- const intentIdHash = (0, utils_js_1.sha256)(utf8IntentId);
819
- const intentIdHashStr = base_1.hex.encode(intentIdHash);
820
- let skip = true;
821
- // check if our intent ID hash matches any in the event
822
- for (const idHash of event.intentIdHashes) {
823
- if (idHash === intentIdHashStr) {
824
- if (!this.arkProvider) {
825
- throw new Error("Ark provider not configured");
826
- }
827
- await this.arkProvider.confirmRegistration(intentId);
828
- skip = false;
829
- }
830
- }
831
- if (skip) {
832
- return { skip };
833
- }
834
- const sweepTapscript = tapscript_1.CSVMultisigTapscript.encode({
835
- timelock: {
836
- value: event.batchExpiry,
837
- type: event.batchExpiry >= 512n ? "seconds" : "blocks",
838
- },
839
- pubkeys: [forfeitPubKey],
840
- }).script;
841
- const sweepTapTreeRoot = (0, payment_js_1.tapLeafHash)(sweepTapscript);
842
- return {
843
- roundId: event.id,
844
- sweepTapTreeRoot,
845
- forfeitOutputScript,
846
- skip: false,
847
- };
848
- }
849
- // validates the vtxo tree, creates a signing session and generates the musig2 nonces
850
- async handleSettlementSigningEvent(event, sweepTapTreeRoot, session, vtxoGraph) {
851
- // validate the unsigned vtxo tree
852
- const commitmentTx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(event.unsignedCommitmentTx));
853
- (0, validation_1.validateVtxoTxGraph)(vtxoGraph, commitmentTx, sweepTapTreeRoot);
854
- // TODO check if our registered outputs are in the vtxo tree
855
- const sharedOutput = commitmentTx.getOutput(0);
856
- if (!sharedOutput?.amount) {
857
- throw new Error("Shared output not found");
775
+ finally {
776
+ // close the stream
777
+ abortController.abort();
858
778
  }
859
- session.init(vtxoGraph, sweepTapTreeRoot, sharedOutput.amount);
860
- const pubkey = base_1.hex.encode(await session.getPublicKey());
861
- const nonces = await session.getNonces();
862
- await this.arkProvider.submitTreeNonces(event.id, pubkey, nonces);
863
- }
864
- async handleSettlementTreeNoncesEvent(event, session) {
865
- const { hasAllNonces } = await session.aggregatedNonces(event.txid, event.nonces);
866
- // wait to receive and aggregate all nonces before sending signatures
867
- if (!hasAllNonces)
868
- return false;
869
- const signatures = await session.sign();
870
- const pubkey = base_1.hex.encode(await session.getPublicKey());
871
- await this.arkProvider.submitTreeSignatures(event.id, pubkey, signatures);
872
- return true;
873
779
  }
874
780
  async handleSettlementFinalizationEvent(event, inputs, forfeitOutputScript, connectorsGraph) {
875
781
  // the signed forfeits transactions to submit
@@ -959,9 +865,98 @@ class Wallet {
959
865
  : undefined);
960
866
  }
961
867
  }
868
+ /**
869
+ * @implements Batch.Handler interface.
870
+ * @param intentId - The intent ID.
871
+ * @param inputs - The inputs of the intent.
872
+ * @param session - The musig2 signing session, if not provided, the signing will be skipped.
873
+ */
874
+ createBatchHandler(intentId, inputs, session) {
875
+ let sweepTapTreeRoot;
876
+ return {
877
+ onBatchStarted: async (event) => {
878
+ const utf8IntentId = new TextEncoder().encode(intentId);
879
+ const intentIdHash = (0, utils_js_1.sha256)(utf8IntentId);
880
+ const intentIdHashStr = base_1.hex.encode(intentIdHash);
881
+ let skip = true;
882
+ // check if our intent ID hash matches any in the event
883
+ for (const idHash of event.intentIdHashes) {
884
+ if (idHash === intentIdHashStr) {
885
+ if (!this.arkProvider) {
886
+ throw new Error("Ark provider not configured");
887
+ }
888
+ await this.arkProvider.confirmRegistration(intentId);
889
+ skip = false;
890
+ }
891
+ }
892
+ if (skip) {
893
+ return { skip };
894
+ }
895
+ const sweepTapscript = tapscript_1.CSVMultisigTapscript.encode({
896
+ timelock: {
897
+ value: event.batchExpiry,
898
+ type: event.batchExpiry >= 512n ? "seconds" : "blocks",
899
+ },
900
+ pubkeys: [this.forfeitPubkey],
901
+ }).script;
902
+ sweepTapTreeRoot = (0, payment_js_1.tapLeafHash)(sweepTapscript);
903
+ return { skip: false };
904
+ },
905
+ onTreeSigningStarted: async (event, vtxoTree) => {
906
+ if (!session) {
907
+ return { skip: true };
908
+ }
909
+ if (!sweepTapTreeRoot) {
910
+ throw new Error("Sweep tap tree root not set");
911
+ }
912
+ const xOnlyPublicKeys = event.cosignersPublicKeys.map((k) => k.slice(2));
913
+ const signerPublicKey = await session.getPublicKey();
914
+ const xonlySignerPublicKey = signerPublicKey.subarray(1);
915
+ if (!xOnlyPublicKeys.includes(base_1.hex.encode(xonlySignerPublicKey))) {
916
+ // not a cosigner, skip the signing
917
+ return { skip: true };
918
+ }
919
+ // validate the unsigned vtxo tree
920
+ const commitmentTx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(event.unsignedCommitmentTx));
921
+ (0, validation_1.validateVtxoTxGraph)(vtxoTree, commitmentTx, sweepTapTreeRoot);
922
+ // TODO check if our registered outputs are in the vtxo tree
923
+ const sharedOutput = commitmentTx.getOutput(0);
924
+ if (!sharedOutput?.amount) {
925
+ throw new Error("Shared output not found");
926
+ }
927
+ await session.init(vtxoTree, sweepTapTreeRoot, sharedOutput.amount);
928
+ const pubkey = base_1.hex.encode(await session.getPublicKey());
929
+ const nonces = await session.getNonces();
930
+ await this.arkProvider.submitTreeNonces(event.id, pubkey, nonces);
931
+ return { skip: false };
932
+ },
933
+ onTreeNonces: async (event) => {
934
+ if (!session) {
935
+ return { fullySigned: true }; // Signing complete (no signing needed)
936
+ }
937
+ const { hasAllNonces } = await session.aggregatedNonces(event.txid, event.nonces);
938
+ // wait to receive and aggregate all nonces before sending signatures
939
+ if (!hasAllNonces)
940
+ return { fullySigned: false };
941
+ const signatures = await session.sign();
942
+ const pubkey = base_1.hex.encode(await session.getPublicKey());
943
+ await this.arkProvider.submitTreeSignatures(event.id, pubkey, signatures);
944
+ return { fullySigned: true };
945
+ },
946
+ onBatchFinalization: async (event, _, connectorTree) => {
947
+ if (!this.forfeitOutputScript) {
948
+ throw new Error("Forfeit output script not set");
949
+ }
950
+ if (connectorTree) {
951
+ (0, validation_1.validateConnectorsTxGraph)(event.commitmentTx, connectorTree);
952
+ }
953
+ await this.handleSettlementFinalizationEvent(event, inputs, this.forfeitOutputScript, connectorTree);
954
+ },
955
+ };
956
+ }
962
957
  async safeRegisterIntent(intent) {
963
958
  try {
964
- return this.arkProvider.registerIntent(intent);
959
+ return await this.arkProvider.registerIntent(intent);
965
960
  }
966
961
  catch (error) {
967
962
  // catch the "already registered by another intent" error
@@ -989,12 +984,11 @@ class Wallet {
989
984
  expire_at: 0,
990
985
  cosigners_public_keys: cosignerPubKeys,
991
986
  };
992
- const encodedMessage = JSON.stringify(message, null, 0);
993
- const proof = intent_1.Intent.create(encodedMessage, inputs, outputs);
987
+ const proof = intent_1.Intent.create(message, inputs, outputs);
994
988
  const signedProof = await this.identity.sign(proof);
995
989
  return {
996
990
  proof: base_1.base64.encode(signedProof.toPSBT()),
997
- message: encodedMessage,
991
+ message,
998
992
  };
999
993
  }
1000
994
  async makeDeleteIntentSignature(coins) {
@@ -1003,12 +997,11 @@ class Wallet {
1003
997
  type: "delete",
1004
998
  expire_at: 0,
1005
999
  };
1006
- const encodedMessage = JSON.stringify(message, null, 0);
1007
- const proof = intent_1.Intent.create(encodedMessage, inputs, []);
1000
+ const proof = intent_1.Intent.create(message, inputs, []);
1008
1001
  const signedProof = await this.identity.sign(proof);
1009
1002
  return {
1010
1003
  proof: base_1.base64.encode(signedProof.toPSBT()),
1011
- message: encodedMessage,
1004
+ message,
1012
1005
  };
1013
1006
  }
1014
1007
  async makeGetPendingTxIntentSignature(vtxos) {
@@ -1017,12 +1010,11 @@ class Wallet {
1017
1010
  type: "get-pending-tx",
1018
1011
  expire_at: 0,
1019
1012
  };
1020
- const encodedMessage = JSON.stringify(message, null, 0);
1021
- const proof = intent_1.Intent.create(encodedMessage, inputs, []);
1013
+ const proof = intent_1.Intent.create(message, inputs, []);
1022
1014
  const signedProof = await this.identity.sign(proof);
1023
1015
  return {
1024
1016
  proof: base_1.base64.encode(signedProof.toPSBT()),
1025
- message: encodedMessage,
1017
+ message,
1026
1018
  };
1027
1019
  }
1028
1020
  /**
@@ -1076,7 +1068,7 @@ class Wallet {
1076
1068
  const inputs = [];
1077
1069
  for (const input of coins) {
1078
1070
  const vtxoScript = base_2.VtxoScript.decode(input.tapTree);
1079
- const sequence = getSequence(input);
1071
+ const sequence = getSequence(input.intentTapLeafScript);
1080
1072
  const unknown = [unknownFields_1.VtxoTaprootTree.encode(input.tapTree)];
1081
1073
  if (input.extraWitness) {
1082
1074
  unknown.push(unknownFields_1.ConditionWitness.encode(input.extraWitness));
@@ -1098,15 +1090,21 @@ class Wallet {
1098
1090
  }
1099
1091
  exports.Wallet = Wallet;
1100
1092
  Wallet.MIN_FEE_RATE = 1; // sats/vbyte
1101
- function getSequence(coin) {
1093
+ function getSequence(tapLeafScript) {
1102
1094
  let sequence = undefined;
1103
1095
  try {
1104
- const scriptWithLeafVersion = coin.intentTapLeafScript[1];
1096
+ const scriptWithLeafVersion = tapLeafScript[1];
1105
1097
  const script = scriptWithLeafVersion.subarray(0, scriptWithLeafVersion.length - 1);
1106
- const params = tapscript_1.CSVMultisigTapscript.decode(script).params;
1107
- sequence = bip68.encode(params.timelock.type === "blocks"
1108
- ? { blocks: Number(params.timelock.value) }
1109
- : { seconds: Number(params.timelock.value) });
1098
+ try {
1099
+ const params = tapscript_1.CSVMultisigTapscript.decode(script).params;
1100
+ sequence = bip68.encode(params.timelock.type === "blocks"
1101
+ ? { blocks: Number(params.timelock.value) }
1102
+ : { seconds: Number(params.timelock.value) });
1103
+ }
1104
+ catch {
1105
+ const params = tapscript_1.CLTVMultisigTapscript.decode(script).params;
1106
+ sequence = Number(params.absoluteTimelock);
1107
+ }
1110
1108
  }
1111
1109
  catch { }
1112
1110
  return sequence;