@arkade-os/sdk 0.4.22 → 0.4.24

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 (105) hide show
  1. package/README.md +116 -13
  2. package/dist/cjs/contracts/arkcontract.js +2 -1
  3. package/dist/cjs/contracts/contractManager.js +29 -4
  4. package/dist/cjs/contracts/contractWatcher.js +9 -3
  5. package/dist/cjs/contracts/handlers/default.js +3 -2
  6. package/dist/cjs/contracts/handlers/delegate.js +3 -2
  7. package/dist/cjs/contracts/handlers/helpers.js +2 -58
  8. package/dist/cjs/contracts/handlers/vhtlc.js +7 -6
  9. package/dist/cjs/contracts/vtxoOwnership.js +60 -0
  10. package/dist/cjs/identity/descriptor.js +75 -4
  11. package/dist/cjs/identity/hdCapableIdentity.js +2 -0
  12. package/dist/cjs/identity/seedIdentity.js +225 -103
  13. package/dist/cjs/identity/serialize.js +5 -0
  14. package/dist/cjs/identity/staticDescriptorProvider.js +1 -1
  15. package/dist/cjs/index.js +12 -3
  16. package/dist/cjs/providers/electrum.js +285 -79
  17. package/dist/cjs/providers/expoIndexer.js +1 -1
  18. package/dist/cjs/providers/indexer.js +2 -2
  19. package/dist/cjs/providers/onchain.js +9 -3
  20. package/dist/cjs/repositories/migrations/walletRepositoryImpl.js +6 -2
  21. package/dist/cjs/repositories/realm/walletRepository.js +2 -2
  22. package/dist/cjs/repositories/serialization.js +34 -1
  23. package/dist/cjs/repositories/sqlite/walletRepository.js +4 -2
  24. package/dist/cjs/script/address.js +2 -1
  25. package/dist/cjs/script/base.js +12 -47
  26. package/dist/cjs/script/tapscript.js +97 -73
  27. package/dist/cjs/utils/timelock.js +59 -0
  28. package/dist/cjs/utils/transactionHistory.js +4 -4
  29. package/dist/cjs/utils/unknownFields.js +2 -39
  30. package/dist/cjs/wallet/asset-manager.js +18 -18
  31. package/dist/cjs/wallet/asset.js +10 -8
  32. package/dist/cjs/wallet/delegator.js +2 -2
  33. package/dist/cjs/wallet/hdDescriptorProvider.js +159 -0
  34. package/dist/cjs/wallet/index.js +5 -1
  35. package/dist/cjs/wallet/onchain.js +2 -1
  36. package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +60 -10
  37. package/dist/cjs/wallet/serviceWorker/wallet.js +5 -4
  38. package/dist/cjs/wallet/unroll.js +79 -67
  39. package/dist/cjs/wallet/validation.js +2 -3
  40. package/dist/cjs/wallet/wallet.js +91 -22
  41. package/dist/cjs/worker/expo/processors/contractPollProcessor.js +7 -2
  42. package/dist/esm/contracts/arkcontract.js +2 -1
  43. package/dist/esm/contracts/contractManager.js +29 -4
  44. package/dist/esm/contracts/contractWatcher.js +9 -3
  45. package/dist/esm/contracts/handlers/default.js +2 -1
  46. package/dist/esm/contracts/handlers/delegate.js +2 -1
  47. package/dist/esm/contracts/handlers/helpers.js +1 -22
  48. package/dist/esm/contracts/handlers/vhtlc.js +2 -1
  49. package/dist/esm/contracts/vtxoOwnership.js +53 -0
  50. package/dist/esm/identity/descriptor.js +74 -5
  51. package/dist/esm/identity/hdCapableIdentity.js +1 -0
  52. package/dist/esm/identity/seedIdentity.js +225 -103
  53. package/dist/esm/identity/serialize.js +5 -0
  54. package/dist/esm/identity/staticDescriptorProvider.js +1 -1
  55. package/dist/esm/index.js +7 -4
  56. package/dist/esm/providers/electrum.js +284 -78
  57. package/dist/esm/providers/expoIndexer.js +1 -1
  58. package/dist/esm/providers/indexer.js +2 -2
  59. package/dist/esm/providers/onchain.js +9 -3
  60. package/dist/esm/repositories/migrations/walletRepositoryImpl.js +6 -2
  61. package/dist/esm/repositories/realm/walletRepository.js +3 -3
  62. package/dist/esm/repositories/serialization.js +27 -0
  63. package/dist/esm/repositories/sqlite/walletRepository.js +5 -3
  64. package/dist/esm/script/address.js +2 -1
  65. package/dist/esm/script/base.js +12 -14
  66. package/dist/esm/script/tapscript.js +97 -40
  67. package/dist/esm/utils/timelock.js +22 -0
  68. package/dist/esm/utils/transactionHistory.js +4 -4
  69. package/dist/esm/utils/unknownFields.js +2 -6
  70. package/dist/esm/wallet/asset-manager.js +18 -18
  71. package/dist/esm/wallet/asset.js +10 -8
  72. package/dist/esm/wallet/delegator.js +2 -2
  73. package/dist/esm/wallet/hdDescriptorProvider.js +155 -0
  74. package/dist/esm/wallet/index.js +4 -0
  75. package/dist/esm/wallet/onchain.js +2 -1
  76. package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +60 -10
  77. package/dist/esm/wallet/serviceWorker/wallet.js +5 -4
  78. package/dist/esm/wallet/unroll.js +78 -67
  79. package/dist/esm/wallet/validation.js +2 -3
  80. package/dist/esm/wallet/wallet.js +88 -20
  81. package/dist/esm/worker/expo/processors/contractPollProcessor.js +7 -2
  82. package/dist/types/contracts/arkcontract.d.ts +1 -1
  83. package/dist/types/contracts/handlers/helpers.d.ts +0 -9
  84. package/dist/types/contracts/vtxoOwnership.d.ts +25 -0
  85. package/dist/types/identity/descriptor.d.ts +26 -0
  86. package/dist/types/identity/descriptorProvider.d.ts +11 -4
  87. package/dist/types/identity/hdCapableIdentity.d.ts +44 -0
  88. package/dist/types/identity/index.d.ts +1 -0
  89. package/dist/types/identity/seedIdentity.d.ts +113 -29
  90. package/dist/types/identity/serialize.d.ts +12 -0
  91. package/dist/types/identity/staticDescriptorProvider.d.ts +1 -1
  92. package/dist/types/index.d.ts +6 -3
  93. package/dist/types/providers/electrum.d.ts +115 -15
  94. package/dist/types/providers/onchain.d.ts +6 -0
  95. package/dist/types/repositories/serialization.d.ts +26 -2
  96. package/dist/types/script/address.d.ts +1 -1
  97. package/dist/types/script/tapscript.d.ts +4 -0
  98. package/dist/types/utils/timelock.d.ts +9 -0
  99. package/dist/types/wallet/hdDescriptorProvider.d.ts +93 -0
  100. package/dist/types/wallet/index.d.ts +19 -10
  101. package/dist/types/wallet/onchain.d.ts +1 -1
  102. package/dist/types/wallet/serviceWorker/wallet.d.ts +1 -1
  103. package/dist/types/wallet/unroll.d.ts +10 -0
  104. package/dist/types/wallet/wallet.d.ts +4 -1
  105. package/package.json +1 -1
@@ -0,0 +1,155 @@
1
+ import { expand, networks } from "@bitcoinerlab/descriptors-scure";
2
+ import { isMainnetDescriptor } from '../identity/descriptor.js';
3
+ import { updateWalletState } from '../utils/syncCursors.js';
4
+ /** Settings key under {@link WalletState.settings} where HD state lives. */
5
+ const HD_SETTINGS_KEY = "hd";
6
+ /**
7
+ * HD-wallet {@link DescriptorProvider} that allocates a fresh signing
8
+ * descriptor on every call. The provider holds no notion of "current" — it
9
+ * is a pure rotating allocator. The question of "which descriptor is the
10
+ * wallet currently bound to?" is answered by querying the contract
11
+ * repository for active contracts, not by asking this provider.
12
+ *
13
+ * State is persisted under `WalletRepository.getWalletState().settings.hd` so
14
+ * that no storage-schema migration is required when switching a wallet from
15
+ * single-key to HD. The provider is backed by an {@link HDCapableIdentity},
16
+ * which carries the wildcard account descriptor template (for derivation)
17
+ * and the signing primitives.
18
+ *
19
+ * The read-modify-write of the persisted index runs inside the shared per-
20
+ * repo `updateWalletState` mutex, so two `getNextSigningDescriptor` callers
21
+ * — including those driving separate `HDDescriptorProvider` instances on
22
+ * the same repo — can never observe the same index.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const provider = await HDDescriptorProvider.create(identity, walletRepo);
27
+ * const descriptor = await provider.getNextSigningDescriptor();
28
+ * // descriptor: tr([fp/86'/0'/0']xpub/0/0)
29
+ * const next = await provider.getNextSigningDescriptor();
30
+ * // next: tr([fp/86'/0'/0']xpub/0/1)
31
+ * ```
32
+ */
33
+ export class HDDescriptorProvider {
34
+ constructor(identity, walletRepository) {
35
+ this.identity = identity;
36
+ this.walletRepository = walletRepository;
37
+ }
38
+ /**
39
+ * Construct an HDDescriptorProvider. No I/O is performed here;
40
+ * persisted state is read lazily on the first call to
41
+ * `getNextSigningDescriptor`. A descriptor-mismatch error surfaces on
42
+ * first use rather than at boot.
43
+ */
44
+ static async create(identity, walletRepository) {
45
+ return new HDDescriptorProvider(identity, walletRepository);
46
+ }
47
+ /**
48
+ * Allocate the next descriptor and return it. The first call on a fresh
49
+ * wallet returns descriptor at index 0; subsequent calls return 1, 2, 3,
50
+ * ... in order. Each call is atomic with respect to other rotations on
51
+ * the same repo: two concurrent callers can never observe the same
52
+ * index.
53
+ */
54
+ async getNextSigningDescriptor() {
55
+ return this.mutate((settings) => {
56
+ const next = settings.lastIndexUsed === undefined
57
+ ? 0
58
+ : settings.lastIndexUsed + 1;
59
+ settings.lastIndexUsed = next;
60
+ return this.materializeAt(next);
61
+ });
62
+ }
63
+ /**
64
+ * Returns true when the given descriptor is derivable from this wallet's
65
+ * seed. Delegates to the underlying identity, which handles both HD and
66
+ * simple `tr(pubkey)` descriptors.
67
+ */
68
+ isOurs(descriptor) {
69
+ return this.identity.isOurs(descriptor);
70
+ }
71
+ /**
72
+ * Signs each request with the key derived from its descriptor. Delegates
73
+ * to the identity's signing primitives — the identity, not the provider,
74
+ * holds the seed.
75
+ */
76
+ async signWithDescriptor(requests) {
77
+ return this.identity.signWithDescriptor(requests);
78
+ }
79
+ /** Signs a message using the key derived from `descriptor`. */
80
+ async signMessageWithDescriptor(descriptor, message, signatureType = "schnorr") {
81
+ return this.identity.signMessageWithDescriptor(descriptor, message, signatureType);
82
+ }
83
+ // ── internals ────────────────────────────────────────────────────
84
+ /**
85
+ * Substitute the wildcard in the identity's account-descriptor template
86
+ * with a concrete index, going through the descriptors-scure parser
87
+ * rather than ad-hoc string substitution. The parser's `expand({ index })`
88
+ * call validates that the input is a ranged template AND produces a
89
+ * canonical materialized key expression at the given index.
90
+ */
91
+ materializeAt(index) {
92
+ const descriptor = this.identity.descriptor;
93
+ const network = isMainnetDescriptor(descriptor)
94
+ ? networks.bitcoin
95
+ : networks.testnet;
96
+ const expansion = expand({ descriptor, network, index });
97
+ const keyInfo = expansion.expansionMap?.["@0"];
98
+ if (!keyInfo?.keyExpression) {
99
+ throw new Error(`HDDescriptorProvider: cannot materialize descriptor at index ${index}`);
100
+ }
101
+ return `tr(${keyInfo.keyExpression})`;
102
+ }
103
+ /**
104
+ * Run the read-modify-write of HD settings inside the shared per-repo
105
+ * wallet-state mutex. The closure receives a freshly-validated settings
106
+ * snapshot, mutates it, and returns whatever value the caller wants to
107
+ * surface; the mutated settings are then persisted as part of the same
108
+ * atomic update.
109
+ *
110
+ * Doing the read inside the lock is what prevents two providers (or two
111
+ * concurrent callers on the same provider) from racing on a stale index.
112
+ */
113
+ async mutate(fn) {
114
+ let result;
115
+ await updateWalletState(this.walletRepository, (state) => {
116
+ const settings = this.parseSettings(state);
117
+ result = fn(settings);
118
+ return {
119
+ ...state,
120
+ settings: {
121
+ ...(state.settings ?? {}),
122
+ [HD_SETTINGS_KEY]: settings,
123
+ },
124
+ };
125
+ });
126
+ return result;
127
+ }
128
+ /**
129
+ * Validate the persisted HD settings (or initialize a fresh record when
130
+ * absent) and return a clone safe for the caller to mutate.
131
+ *
132
+ * The cast to `HDWalletSettings` trusts storage; a corrupted or
133
+ * partially-migrated repo could otherwise produce `NaN` descriptors.
134
+ * Fail loud rather than silently derive garbage.
135
+ */
136
+ parseSettings(state) {
137
+ const stored = state.settings?.[HD_SETTINGS_KEY];
138
+ const expected = this.identity.descriptor;
139
+ if (!stored) {
140
+ return { descriptor: expected };
141
+ }
142
+ if (stored.descriptor !== expected) {
143
+ throw new Error(`HD descriptor mismatch: stored "${stored.descriptor}", expected "${expected}". ` +
144
+ `Refusing to reuse HD state from a different identity.`);
145
+ }
146
+ if (stored.lastIndexUsed !== undefined &&
147
+ (typeof stored.lastIndexUsed !== "number" ||
148
+ !Number.isInteger(stored.lastIndexUsed) ||
149
+ stored.lastIndexUsed < 0)) {
150
+ throw new Error(`Corrupt HD settings: lastIndexUsed is not a non-negative integer (got ${String(stored.lastIndexUsed)}).`);
151
+ }
152
+ // Shallow clone so the closure may mutate without aliasing the repo's copy.
153
+ return { ...stored };
154
+ }
155
+ }
@@ -1,3 +1,7 @@
1
+ /** Defaults */
2
+ export const DEFAULT_ARKADE_SERVER_URL = "https://arkade.computer";
3
+ export const DEFAULT_ARKADE_HRP = "ark";
4
+ export const DEFAULT_NETWORK_NAME = "bitcoin";
1
5
  /** Wallet transaction direction. */
2
6
  export var TxType;
3
7
  (function (TxType) {
@@ -1,4 +1,5 @@
1
1
  import { p2tr } from "@scure/btc-signer";
2
+ import { DEFAULT_NETWORK_NAME } from './index.js';
2
3
  import { getNetwork } from '../networks.js';
3
4
  import { ESPLORA_URL, EsploraProvider, } from '../providers/onchain.js';
4
5
  import { findP2AOutput, P2A } from '../utils/anchor.js';
@@ -39,7 +40,7 @@ export class OnchainWallet {
39
40
  * @defaultValue `provider = new EsploraProvider('https://mempool.space/api')`
40
41
  * @throws Error if the configured identity cannot produce a valid x-only public key
41
42
  */
42
- static async create(identity, networkName, provider) {
43
+ static async create(identity, networkName = DEFAULT_NETWORK_NAME, provider) {
43
44
  const pubkey = await identity.xOnlyPublicKey();
44
45
  if (!pubkey) {
45
46
  throw new Error("Invalid configured public key");
@@ -2,6 +2,8 @@ import { RestIndexerProvider } from '../../providers/indexer.js';
2
2
  import { isExpired, isRecoverable, isSpendable, isSubdust, } from '../index.js';
3
3
  import { extendCoin } from '../utils.js';
4
4
  import { buildTransactionHistory } from '../../utils/transactionHistory.js';
5
+ import { filterVtxosForScript, warnAndFilterVtxosForScript, } from '../../contracts/vtxoOwnership.js';
6
+ import { scriptFromArkAddress } from '../../repositories/scriptFromAddress.js';
5
7
  export class WalletNotInitializedError extends Error {
6
8
  constructor() {
7
9
  super("Wallet handler not initialized");
@@ -508,7 +510,7 @@ export class WalletMessageHandler {
508
510
  for (const vtxo of spendableVtxos) {
509
511
  if (vtxo.assets) {
510
512
  for (const a of vtxo.assets) {
511
- const current = assetBalances.get(a.assetId) ?? 0;
513
+ const current = assetBalances.get(a.assetId) ?? 0n;
512
514
  assetBalances.set(a.assetId, current + a.amount);
513
515
  }
514
516
  }
@@ -581,11 +583,45 @@ export class WalletMessageHandler {
581
583
  const { newVtxos, spentVtxos } = funds;
582
584
  if (newVtxos.length + spentVtxos.length === 0)
583
585
  return;
584
- // save virtual outputs using unified repository
585
- await this.walletRepository?.saveVtxos(address, [
586
- ...newVtxos,
587
- ...spentVtxos,
588
- ]);
586
+ // Save virtual outputs using unified repository. The
587
+ // event may carry rows for several scripts (other
588
+ // contracts the wallet watches), so split by script and
589
+ // save each bucket under its own contract address rather
590
+ // than saving a mixed-script array under one address.
591
+ const byScript = new Map();
592
+ for (const v of [...newVtxos, ...spentVtxos]) {
593
+ if (!v.script) {
594
+ // Without a script we can't route the row to the
595
+ // right contract bucket; surface the drop instead
596
+ // of silently losing the VTXO.
597
+ console.warn(`WalletMessageHandler.notifyIncomingFunds: dropping VTXO without script ${v.txid}:${v.vout}`);
598
+ continue;
599
+ }
600
+ const arr = byScript.get(v.script) ?? [];
601
+ arr.push(v);
602
+ byScript.set(v.script, arr);
603
+ }
604
+ let walletScript;
605
+ try {
606
+ walletScript = scriptFromArkAddress(address);
607
+ }
608
+ catch {
609
+ walletScript = undefined;
610
+ }
611
+ const cm = await this.readonlyWallet.getContractManager();
612
+ const contracts = await cm.getContracts();
613
+ const addrByScript = new Map(contracts.map((c) => [c.script, c.address]));
614
+ for (const [script, vtxos] of byScript) {
615
+ const filtered = warnAndFilterVtxosForScript(vtxos, script, "WalletMessageHandler.notifyIncomingFunds");
616
+ if (filtered.length === 0)
617
+ continue;
618
+ const targetAddress = script === walletScript
619
+ ? address
620
+ : addrByScript.get(script);
621
+ if (!targetAddress)
622
+ continue;
623
+ await this.walletRepository?.saveVtxos(targetAddress, filtered);
624
+ }
589
625
  // notify all clients about the virtual output state update
590
626
  this.scheduleForNextTick(() => this.tagged({
591
627
  type: "VTXO_UPDATE",
@@ -797,17 +833,31 @@ export class WalletMessageHandler {
797
833
  }
798
834
  }
799
835
  };
800
- // Aggregate virtual outputs from all contract addresses
836
+ // Aggregate virtual outputs from all contract addresses. Address
837
+ // buckets may carry legacy duplicate rows from other contracts; gate
838
+ // each bucket by its owning contract script before deduplication so a
839
+ // wrong-script row never wins the txid:vout race.
801
840
  const manager = await this.readonlyWallet.getContractManager();
802
841
  const contracts = await manager.getContracts();
803
842
  for (const contract of contracts) {
804
843
  const vtxos = await this.walletRepository.getVtxos(contract.address);
805
- addVtxos(vtxos);
844
+ addVtxos(filterVtxosForScript(vtxos, contract.script));
806
845
  }
807
- // Also check the wallet's primary address
846
+ // Also check the wallet's primary address. Decode it to its script
847
+ // and apply the same script gate. Failing to decode the wallet's own
848
+ // address is a structural bug — surfacing the error is safer than
849
+ // silently dropping the primary bucket and zeroing the user's
850
+ // visible balance.
808
851
  const walletAddress = await this.readonlyWallet.getAddress();
852
+ let walletScript;
853
+ try {
854
+ walletScript = scriptFromArkAddress(walletAddress);
855
+ }
856
+ catch (e) {
857
+ throw new Error(`WalletMessageHandler.getVtxosFromRepo: failed to derive script from wallet address ${walletAddress}: ${e instanceof Error ? e.message : String(e)}`);
858
+ }
809
859
  const walletVtxos = await this.walletRepository.getVtxos(walletAddress);
810
- addVtxos(walletVtxos);
860
+ addVtxos(filterVtxosForScript(walletVtxos, walletScript));
811
861
  return allVtxos;
812
862
  }
813
863
  /**
@@ -5,6 +5,7 @@ import { IndexedDBContractRepository, IndexedDBWalletRepository, } from '../../r
5
5
  import { DEFAULT_MESSAGE_TAG, } from './wallet-message-handler.js';
6
6
  import { getRandomId } from '../utils.js';
7
7
  import { MESSAGE_BUS_NOT_INITIALIZED, ServiceWorkerTimeoutError, } from '../../worker/errors.js';
8
+ import { getArkadeServerUrl } from '../wallet.js';
8
9
  // Check by error message content instead of instanceof because postMessage uses the
9
10
  // structured clone algorithm which strips the prototype chain — the page
10
11
  // receives a plain Error, not the original MessageBusNotInitializedError.
@@ -211,7 +212,7 @@ export class ServiceWorkerReadonlyWallet {
211
212
  .then(hex.encode);
212
213
  const initWalletPayload = {
213
214
  key: { publicKey },
214
- arkServerUrl: options.arkServerUrl,
215
+ arkServerUrl: getArkadeServerUrl(options),
215
216
  arkServerPublicKey: options.arkServerPublicKey,
216
217
  delegatorUrl: options.delegatorUrl,
217
218
  };
@@ -226,7 +227,7 @@ export class ServiceWorkerReadonlyWallet {
226
227
  const busInitConfig = {
227
228
  wallet: serializedWallet,
228
229
  arkServer: {
229
- url: options.arkServerUrl,
230
+ url: getArkadeServerUrl(options),
230
231
  publicKey: options.arkServerPublicKey,
231
232
  },
232
233
  delegatorUrl: options.delegatorUrl,
@@ -884,7 +885,7 @@ export class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
884
885
  : null;
885
886
  const initWalletPayload = {
886
887
  key: legacyPrivateKey ? { privateKey: legacyPrivateKey } : {},
887
- arkServerUrl: options.arkServerUrl,
888
+ arkServerUrl: getArkadeServerUrl(options),
888
889
  arkServerPublicKey: options.arkServerPublicKey,
889
890
  delegatorUrl: options.delegatorUrl,
890
891
  };
@@ -899,7 +900,7 @@ export class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
899
900
  const busInitConfig = {
900
901
  wallet: serializedWallet,
901
902
  arkServer: {
902
- url: options.arkServerUrl,
903
+ url: getArkadeServerUrl(options),
903
904
  publicKey: options.arkServerPublicKey,
904
905
  },
905
906
  delegatorUrl: options.delegatorUrl,
@@ -1,6 +1,6 @@
1
1
  import { base64, hex } from "@scure/base";
2
2
  import { SigHash, TaprootControlBlock } from "@scure/btc-signer";
3
- import { timelockToSequence } from '../contracts/handlers/helpers.js';
3
+ import { timelockToSequence } from '../utils/timelock.js';
4
4
  import { ChainTxType } from '../providers/indexer.js';
5
5
  import { VtxoScript } from '../script/base.js';
6
6
  import { TxWeightEstimator } from '../utils/txSizeEstimator.js';
@@ -126,10 +126,12 @@ export var Unroll;
126
126
  // finalize Arkade transaction
127
127
  tx.finalize();
128
128
  }
129
+ const pkg = await this.bumper.bumpP2A(tx);
129
130
  return {
130
131
  type: StepType.UNROLL,
131
132
  tx,
132
- do: doUnroll(this.bumper, this.explorer, tx),
133
+ pkg,
134
+ do: doUnroll(this.explorer, pkg),
133
135
  };
134
136
  }
135
137
  /**
@@ -161,79 +163,88 @@ export var Unroll;
161
163
  * @returns the txid of the transaction spending the unrolled funds
162
164
  */
163
165
  async function completeUnroll(wallet, vtxoTxids, outputAddress) {
164
- const chainTip = await wallet.onchainProvider.getChainTip();
165
- let vtxos = await wallet.getVtxos({ withUnrolled: true });
166
- vtxos = vtxos.filter((vtxo) => vtxoTxids.includes(vtxo.txid));
167
- if (vtxos.length === 0) {
168
- throw new Error("No vtxos to complete unroll");
169
- }
170
- const inputs = [];
171
- let totalAmount = 0n;
172
- const txWeightEstimator = TxWeightEstimator.create();
173
- for (const vtxo of vtxos) {
174
- if (!vtxo.isUnrolled) {
175
- throw new Error(`Vtxo ${vtxo.txid}:${vtxo.vout} is not fully unrolled, use unroll first`);
176
- }
177
- const txStatus = await wallet.onchainProvider.getTxStatus(vtxo.txid);
178
- if (!txStatus.confirmed) {
179
- throw new Error(`tx ${vtxo.txid} is not confirmed`);
180
- }
181
- const exit = availableExitPath({ height: txStatus.blockHeight, time: txStatus.blockTime }, chainTip, vtxo);
182
- if (!exit) {
183
- throw new Error(`no available exit path found for vtxo ${vtxo.txid}:${vtxo.vout}`);
184
- }
185
- const spendingLeaf = VtxoScript.decode(vtxo.tapTree).findLeaf(hex.encode(exit.script));
186
- if (!spendingLeaf) {
187
- throw new Error(`spending leaf not found for vtxo ${vtxo.txid}:${vtxo.vout}`);
188
- }
189
- totalAmount += BigInt(vtxo.value);
190
- const sequence = timelockToSequence(exit.params.timelock);
191
- inputs.push({
192
- txid: vtxo.txid,
193
- index: vtxo.vout,
194
- tapLeafScript: [spendingLeaf],
195
- sequence,
196
- witnessUtxo: {
197
- amount: BigInt(vtxo.value),
198
- script: VtxoScript.decode(vtxo.tapTree).pkScript,
199
- },
200
- sighashType: SigHash.DEFAULT,
201
- });
202
- txWeightEstimator.addTapscriptInput(64, spendingLeaf[1].length, TaprootControlBlock.encode(spendingLeaf[0]).length);
203
- }
204
- const tx = new Transaction({ version: 2 });
205
- for (const input of inputs) {
206
- tx.addInput(input);
207
- }
208
- txWeightEstimator.addOutputAddress(outputAddress, wallet.network);
209
- let feeRate = await wallet.onchainProvider.getFeeRate();
210
- if (!feeRate || feeRate < Wallet.MIN_FEE_RATE) {
211
- feeRate = Wallet.MIN_FEE_RATE;
212
- }
213
- const feeAmount = txWeightEstimator.vsize().fee(BigInt(feeRate));
214
- if (feeAmount > totalAmount) {
215
- throw new Error("fee amount is greater than the total amount");
216
- }
217
- const sendAmount = totalAmount - feeAmount;
218
- if (sendAmount < BigInt(DUST_AMOUNT)) {
219
- throw new Error("send amount is less than dust amount");
220
- }
221
- tx.addOutputAddress(outputAddress, sendAmount);
222
- const signedTx = await wallet.identity.sign(tx);
223
- signedTx.finalize();
166
+ const signedTx = await prepareUnrollTransaction(wallet, vtxoTxids, outputAddress);
224
167
  await wallet.onchainProvider.broadcastTransaction(signedTx.hex);
225
168
  return signedTx.id;
226
169
  }
227
170
  Unroll.completeUnroll = completeUnroll;
228
171
  })(Unroll || (Unroll = {}));
172
+ /**
173
+ * Prepares the transaction that spends the CSV path to complete unrolling a VTXO.
174
+ * @param wallet the wallet owning the VTXO(s)
175
+ * @param vtxoTxIds the txids of the VTXO(s) to complete unroll
176
+ * @param outputAddress the address to send the unrolled funds to
177
+ * @throws if the VTXO(s) are not fully unrolled, if the txids are not found, if the tx is not confirmed, if no exit path is found or not available
178
+ * @returns the transaction spending the unrolled funds
179
+ */
180
+ export async function prepareUnrollTransaction(wallet, vtxoTxIds, outputAddress) {
181
+ const chainTip = await wallet.onchainProvider.getChainTip();
182
+ let vtxos = await wallet.getVtxos({ withUnrolled: true });
183
+ vtxos = vtxos.filter((vtxo) => vtxoTxIds.includes(vtxo.txid));
184
+ if (vtxos.length === 0) {
185
+ throw new Error("No vtxos to complete unroll");
186
+ }
187
+ const inputs = [];
188
+ let totalAmount = 0n;
189
+ const txWeightEstimator = TxWeightEstimator.create();
190
+ for (const vtxo of vtxos) {
191
+ if (!vtxo.isUnrolled) {
192
+ throw new Error(`Vtxo ${vtxo.txid}:${vtxo.vout} is not fully unrolled, use unroll first`);
193
+ }
194
+ const txStatus = await wallet.onchainProvider.getTxStatus(vtxo.txid);
195
+ if (!txStatus.confirmed) {
196
+ throw new Error(`tx ${vtxo.txid} is not confirmed`);
197
+ }
198
+ const exit = availableExitPath({ height: txStatus.blockHeight, time: txStatus.blockTime }, chainTip, vtxo);
199
+ if (!exit) {
200
+ throw new Error(`no available exit path found for vtxo ${vtxo.txid}:${vtxo.vout}`);
201
+ }
202
+ const spendingLeaf = VtxoScript.decode(vtxo.tapTree).findLeaf(hex.encode(exit.script));
203
+ if (!spendingLeaf) {
204
+ throw new Error(`spending leaf not found for vtxo ${vtxo.txid}:${vtxo.vout}`);
205
+ }
206
+ totalAmount += BigInt(vtxo.value);
207
+ const sequence = timelockToSequence(exit.params.timelock);
208
+ inputs.push({
209
+ txid: vtxo.txid,
210
+ index: vtxo.vout,
211
+ tapLeafScript: [spendingLeaf],
212
+ sequence,
213
+ witnessUtxo: {
214
+ amount: BigInt(vtxo.value),
215
+ script: VtxoScript.decode(vtxo.tapTree).pkScript,
216
+ },
217
+ sighashType: SigHash.DEFAULT,
218
+ });
219
+ txWeightEstimator.addTapscriptInput(64, spendingLeaf[1].length, TaprootControlBlock.encode(spendingLeaf[0]).length);
220
+ }
221
+ const tx = new Transaction({ version: 2 });
222
+ for (const input of inputs) {
223
+ tx.addInput(input);
224
+ }
225
+ txWeightEstimator.addOutputAddress(outputAddress, wallet.network);
226
+ let feeRate = await wallet.onchainProvider.getFeeRate();
227
+ if (!feeRate || feeRate < Wallet.MIN_FEE_RATE) {
228
+ feeRate = Wallet.MIN_FEE_RATE;
229
+ }
230
+ const feeAmount = txWeightEstimator.vsize().fee(BigInt(feeRate));
231
+ if (feeAmount > totalAmount) {
232
+ throw new Error("fee amount is greater than the total amount");
233
+ }
234
+ const sendAmount = totalAmount - feeAmount;
235
+ if (sendAmount < BigInt(DUST_AMOUNT)) {
236
+ throw new Error("send amount is less than dust amount");
237
+ }
238
+ tx.addOutputAddress(outputAddress, sendAmount, wallet.network);
239
+ const signedTx = await wallet.identity.sign(tx);
240
+ signedTx.finalize();
241
+ return signedTx;
242
+ }
229
243
  function sleep(ms) {
230
244
  return new Promise((resolve) => setTimeout(resolve, ms));
231
245
  }
232
- function doUnroll(bumper, onchainProvider, tx) {
233
- return async () => {
234
- const [parent, child] = await bumper.bumpP2A(tx);
235
- await onchainProvider.broadcastTransaction(parent, child);
236
- };
246
+ function doUnroll(onchainProvider, pkg) {
247
+ return () => onchainProvider.broadcastTransaction(...pkg).then(() => undefined);
237
248
  }
238
249
  function doWait(onchainProvider, txid) {
239
250
  return () => {
@@ -136,8 +136,7 @@ function validateAssetGroupOutput(packet, outputIndex, assetId, expectedAmount)
136
136
  if (!assetOutput) {
137
137
  throw ErrAssetOutputNotFound(assetId, outputIndex);
138
138
  }
139
- const expectedAmountBigInt = BigInt(expectedAmount);
140
- if (assetOutput.amount !== expectedAmountBigInt) {
141
- throw ErrInvalidAssetOutputAmount(assetOutput.amount, expectedAmountBigInt, assetId);
139
+ if (assetOutput.amount !== expectedAmount) {
140
+ throw ErrInvalidAssetOutputAmount(assetOutput.amount, expectedAmount, assetId);
142
141
  }
143
142
  }