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