@arkade-os/sdk 0.4.22 → 0.4.23

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 (74) hide show
  1. package/README.md +95 -12
  2. package/dist/cjs/contracts/arkcontract.js +2 -1
  3. package/dist/cjs/identity/descriptor.js +75 -4
  4. package/dist/cjs/identity/hdCapableIdentity.js +2 -0
  5. package/dist/cjs/identity/seedIdentity.js +225 -103
  6. package/dist/cjs/identity/serialize.js +5 -0
  7. package/dist/cjs/identity/staticDescriptorProvider.js +1 -1
  8. package/dist/cjs/index.js +12 -3
  9. package/dist/cjs/providers/electrum.js +285 -79
  10. package/dist/cjs/providers/expoIndexer.js +1 -1
  11. package/dist/cjs/providers/indexer.js +2 -2
  12. package/dist/cjs/providers/onchain.js +9 -3
  13. package/dist/cjs/repositories/migrations/walletRepositoryImpl.js +6 -2
  14. package/dist/cjs/repositories/realm/walletRepository.js +2 -2
  15. package/dist/cjs/repositories/serialization.js +34 -1
  16. package/dist/cjs/repositories/sqlite/walletRepository.js +4 -2
  17. package/dist/cjs/script/address.js +2 -1
  18. package/dist/cjs/utils/transactionHistory.js +4 -4
  19. package/dist/cjs/wallet/asset-manager.js +18 -18
  20. package/dist/cjs/wallet/asset.js +10 -8
  21. package/dist/cjs/wallet/delegator.js +2 -2
  22. package/dist/cjs/wallet/hdDescriptorProvider.js +159 -0
  23. package/dist/cjs/wallet/index.js +5 -1
  24. package/dist/cjs/wallet/onchain.js +2 -1
  25. package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +1 -1
  26. package/dist/cjs/wallet/serviceWorker/wallet.js +5 -4
  27. package/dist/cjs/wallet/validation.js +2 -3
  28. package/dist/cjs/wallet/wallet.js +13 -14
  29. package/dist/esm/contracts/arkcontract.js +2 -1
  30. package/dist/esm/identity/descriptor.js +74 -5
  31. package/dist/esm/identity/hdCapableIdentity.js +1 -0
  32. package/dist/esm/identity/seedIdentity.js +225 -103
  33. package/dist/esm/identity/serialize.js +5 -0
  34. package/dist/esm/identity/staticDescriptorProvider.js +1 -1
  35. package/dist/esm/index.js +7 -4
  36. package/dist/esm/providers/electrum.js +284 -78
  37. package/dist/esm/providers/expoIndexer.js +1 -1
  38. package/dist/esm/providers/indexer.js +2 -2
  39. package/dist/esm/providers/onchain.js +9 -3
  40. package/dist/esm/repositories/migrations/walletRepositoryImpl.js +6 -2
  41. package/dist/esm/repositories/realm/walletRepository.js +3 -3
  42. package/dist/esm/repositories/serialization.js +27 -0
  43. package/dist/esm/repositories/sqlite/walletRepository.js +5 -3
  44. package/dist/esm/script/address.js +2 -1
  45. package/dist/esm/utils/transactionHistory.js +4 -4
  46. package/dist/esm/wallet/asset-manager.js +18 -18
  47. package/dist/esm/wallet/asset.js +10 -8
  48. package/dist/esm/wallet/delegator.js +2 -2
  49. package/dist/esm/wallet/hdDescriptorProvider.js +155 -0
  50. package/dist/esm/wallet/index.js +4 -0
  51. package/dist/esm/wallet/onchain.js +2 -1
  52. package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +1 -1
  53. package/dist/esm/wallet/serviceWorker/wallet.js +5 -4
  54. package/dist/esm/wallet/validation.js +2 -3
  55. package/dist/esm/wallet/wallet.js +12 -14
  56. package/dist/types/contracts/arkcontract.d.ts +1 -1
  57. package/dist/types/identity/descriptor.d.ts +26 -0
  58. package/dist/types/identity/descriptorProvider.d.ts +11 -4
  59. package/dist/types/identity/hdCapableIdentity.d.ts +44 -0
  60. package/dist/types/identity/index.d.ts +1 -0
  61. package/dist/types/identity/seedIdentity.d.ts +113 -29
  62. package/dist/types/identity/serialize.d.ts +12 -0
  63. package/dist/types/identity/staticDescriptorProvider.d.ts +1 -1
  64. package/dist/types/index.d.ts +6 -3
  65. package/dist/types/providers/electrum.d.ts +115 -15
  66. package/dist/types/providers/onchain.d.ts +6 -0
  67. package/dist/types/repositories/serialization.d.ts +26 -2
  68. package/dist/types/script/address.d.ts +1 -1
  69. package/dist/types/wallet/hdDescriptorProvider.d.ts +93 -0
  70. package/dist/types/wallet/index.d.ts +19 -10
  71. package/dist/types/wallet/onchain.d.ts +1 -1
  72. package/dist/types/wallet/serviceWorker/wallet.d.ts +1 -1
  73. package/dist/types/wallet/wallet.d.ts +4 -1
  74. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { serializeVtxo, serializeUtxo, deserializeVtxo, deserializeUtxo, } from '../serialization.js';
1
+ import { serializeVtxo, serializeUtxo, deserializeVtxo, deserializeUtxo, serializeAssets, deserializeAssets, } from '../serialization.js';
2
2
  import { scriptFromArkAddress } from '../scriptFromAddress.js';
3
3
  /**
4
4
  * SQLite-based implementation of WalletRepository.
@@ -299,7 +299,9 @@ export class SQLiteWalletRepository {
299
299
  tx.amount,
300
300
  tx.settled ? 1 : 0,
301
301
  tx.createdAt,
302
- tx.assets ? JSON.stringify(tx.assets) : null,
302
+ tx.assets
303
+ ? JSON.stringify(serializeAssets(tx.assets))
304
+ : null,
303
305
  ]);
304
306
  }
305
307
  }
@@ -406,7 +408,7 @@ function txRowToDomain(row) {
406
408
  createdAt: row.created_at,
407
409
  };
408
410
  if (row.assets_json) {
409
- tx.assets = JSON.parse(row.assets_json);
411
+ tx.assets = deserializeAssets(JSON.parse(row.assets_json));
410
412
  }
411
413
  return tx;
412
414
  }
@@ -1,5 +1,6 @@
1
1
  import { bech32m } from "@scure/base";
2
2
  import { Script } from "@scure/btc-signer/script.js";
3
+ import { DEFAULT_ARKADE_HRP } from '../wallet/index.js';
3
4
  /**
4
5
  * ArkAddress allows creating and decoding bech32m-encoded Arkade addresses.
5
6
  *
@@ -40,7 +41,7 @@ export class ArkAddress {
40
41
  * @defaultValue `version = 0`
41
42
  * @throws Error if either public key is not 32 bytes long
42
43
  */
43
- constructor(serverPubKey, vtxoTaprootKey, hrp, version = 0) {
44
+ constructor(serverPubKey, vtxoTaprootKey, hrp = DEFAULT_ARKADE_HRP, version = 0) {
44
45
  this.serverPubKey = serverPubKey;
45
46
  this.vtxoTaprootKey = vtxoTaprootKey;
46
47
  this.hrp = hrp;
@@ -9,7 +9,7 @@ function collectAssets(vtxos) {
9
9
  for (const vtxo of vtxos) {
10
10
  if (vtxo.assets) {
11
11
  for (const a of vtxo.assets) {
12
- map.set(a.assetId, (map.get(a.assetId) ?? 0) + a.amount);
12
+ map.set(a.assetId, (map.get(a.assetId) ?? 0n) + a.amount);
13
13
  }
14
14
  }
15
15
  }
@@ -22,16 +22,16 @@ function subtractAssets(spent, change) {
22
22
  for (const vtxo of change) {
23
23
  if (vtxo.assets) {
24
24
  for (const a of vtxo.assets) {
25
- map.set(a.assetId, (map.get(a.assetId) ?? 0) + a.amount);
25
+ map.set(a.assetId, (map.get(a.assetId) ?? 0n) + a.amount);
26
26
  }
27
27
  }
28
28
  }
29
29
  for (const vtxo of spent) {
30
30
  if (vtxo.assets) {
31
31
  for (const a of vtxo.assets) {
32
- const current = map.get(a.assetId) ?? 0;
32
+ const current = map.get(a.assetId) ?? 0n;
33
33
  const remaining = current - a.amount;
34
- if (remaining !== 0) {
34
+ if (remaining !== 0n) {
35
35
  map.set(a.assetId, remaining);
36
36
  }
37
37
  else {
@@ -40,7 +40,7 @@ export class AssetManager extends ReadonlyAssetManager {
40
40
  * ```
41
41
  */
42
42
  async issue(params) {
43
- if (params.amount <= 0) {
43
+ if (params.amount <= 0n) {
44
44
  throw new Error(`Issue amount must be greater than 0, got ${params.amount}`);
45
45
  }
46
46
  const metadata = castMetadata(params.metadata);
@@ -60,12 +60,12 @@ export class AssetManager extends ReadonlyAssetManager {
60
60
  continue;
61
61
  for (const { assetId, amount } of coin.assets) {
62
62
  const existing = assetChanges.get(assetId) ?? 0n;
63
- assetChanges.set(assetId, existing + BigInt(amount));
63
+ assetChanges.set(assetId, existing + amount);
64
64
  }
65
65
  }
66
66
  const groups = [];
67
67
  // issued asset group
68
- const issuedAssetOutput = AssetOutput.create(0, BigInt(params.amount));
68
+ const issuedAssetOutput = AssetOutput.create(0, params.amount);
69
69
  const issuedAssetGroup = AssetGroup.create(null, controlAssetRef, [], [issuedAssetOutput], metadata);
70
70
  groups.push(issuedAssetGroup);
71
71
  // add asset groups for each asset change
@@ -78,7 +78,7 @@ export class AssetManager extends ReadonlyAssetManager {
78
78
  for (const asset of assets) {
79
79
  if (asset.assetId !== assetId)
80
80
  continue;
81
- changeInputs.push(AssetInput.create(inputIndex, BigInt(asset.amount)));
81
+ changeInputs.push(AssetInput.create(inputIndex, asset.amount));
82
82
  }
83
83
  }
84
84
  // add the change asset group
@@ -119,7 +119,7 @@ export class AssetManager extends ReadonlyAssetManager {
119
119
  * ```
120
120
  */
121
121
  async reissue(params) {
122
- if (params.amount <= 0) {
122
+ if (params.amount <= 0n) {
123
123
  throw new Error(`Reissuance amount must be greater than 0, got ${params.amount}`);
124
124
  }
125
125
  const { controlAssetId } = await this.getAssetDetails(params.assetId);
@@ -140,11 +140,11 @@ export class AssetManager extends ReadonlyAssetManager {
140
140
  continue;
141
141
  for (const { assetId, amount } of coin.assets) {
142
142
  if (assetId === params.assetId) {
143
- assetToReissueAmount += BigInt(amount);
143
+ assetToReissueAmount += amount;
144
144
  continue;
145
145
  }
146
146
  const existing = assetChanges.get(assetId) ?? 0n;
147
- assetChanges.set(assetId, existing + BigInt(amount));
147
+ assetChanges.set(assetId, existing + amount);
148
148
  }
149
149
  }
150
150
  // select at least dust amount
@@ -159,11 +159,11 @@ export class AssetManager extends ReadonlyAssetManager {
159
159
  continue;
160
160
  for (const { assetId, amount } of coin.assets) {
161
161
  if (assetId === params.assetId) {
162
- assetToReissueAmount += BigInt(amount);
162
+ assetToReissueAmount += amount;
163
163
  continue;
164
164
  }
165
165
  const existing = assetChanges.get(assetId) ?? 0n;
166
- assetChanges.set(assetId, existing + BigInt(amount));
166
+ assetChanges.set(assetId, existing + amount);
167
167
  }
168
168
  }
169
169
  selectedCoins = [...selectedCoins, ...additional.inputs];
@@ -176,11 +176,11 @@ export class AssetManager extends ReadonlyAssetManager {
176
176
  for (const asset of assets) {
177
177
  if (asset.assetId !== params.assetId)
178
178
  continue;
179
- reissueInputs.push(AssetInput.create(inputIndex, BigInt(asset.amount)));
179
+ reissueInputs.push(AssetInput.create(inputIndex, asset.amount));
180
180
  }
181
181
  }
182
182
  // the total output amount of the asset to reissue = new + (optional) selected amount
183
- const totalAssetAmount = assetToReissueAmount + BigInt(params.amount);
183
+ const totalAssetAmount = assetToReissueAmount + params.amount;
184
184
  const reissueAssetIdObj = AssetId.fromString(params.assetId);
185
185
  // create the reissuance asset group
186
186
  const reissueAssetGroup = AssetGroup.create(reissueAssetIdObj, null, reissueInputs, [AssetOutput.create(0, totalAssetAmount)], []);
@@ -192,7 +192,7 @@ export class AssetManager extends ReadonlyAssetManager {
192
192
  for (const asset of assets) {
193
193
  if (asset.assetId !== assetId)
194
194
  continue;
195
- changeInputs.push(AssetInput.create(inputIndex, BigInt(asset.amount)));
195
+ changeInputs.push(AssetInput.create(inputIndex, asset.amount));
196
196
  }
197
197
  }
198
198
  groups.push(AssetGroup.create(AssetId.fromString(assetId), null, changeInputs, [AssetOutput.create(0, amount)], []));
@@ -226,7 +226,7 @@ export class AssetManager extends ReadonlyAssetManager {
226
226
  * ```
227
227
  */
228
228
  async burn(params) {
229
- if (params.amount <= 0) {
229
+ if (params.amount <= 0n) {
230
230
  throw new Error(`Burn amount must be greater than 0, got ${params.amount}`);
231
231
  }
232
232
  const virtualCoins = await this.wallet.getVtxos({
@@ -234,7 +234,7 @@ export class AssetManager extends ReadonlyAssetManager {
234
234
  });
235
235
  const assetChanges = new Map();
236
236
  // select virtual outputs with the asset to burn
237
- const { selected: assetCoins } = selectCoinsWithAsset(virtualCoins, params.assetId, BigInt(params.amount));
237
+ const { selected: assetCoins } = selectCoinsWithAsset(virtualCoins, params.assetId, params.amount);
238
238
  const selectedCoins = [...assetCoins];
239
239
  let totalBtcSelected = 0;
240
240
  // add the selected coins to asset changes, including the asset to burn
@@ -244,11 +244,11 @@ export class AssetManager extends ReadonlyAssetManager {
244
244
  continue;
245
245
  for (const { assetId, amount } of coin.assets) {
246
246
  const existing = assetChanges.get(assetId) ?? 0n;
247
- assetChanges.set(assetId, existing + BigInt(amount));
247
+ assetChanges.set(assetId, existing + amount);
248
248
  }
249
249
  }
250
250
  // subtract the amount to burn from the asset change
251
- assetChanges.set(params.assetId, (assetChanges.get(params.assetId) ?? 0n) - BigInt(params.amount));
251
+ assetChanges.set(params.assetId, (assetChanges.get(params.assetId) ?? 0n) - params.amount);
252
252
  const minBtcNeeded = Number(this.wallet.dustAmount);
253
253
  // we need to ensure at least dust amount is selected
254
254
  // if not, select additional coins
@@ -262,7 +262,7 @@ export class AssetManager extends ReadonlyAssetManager {
262
262
  continue;
263
263
  for (const { assetId, amount } of coin.assets) {
264
264
  const existing = assetChanges.get(assetId) ?? 0n;
265
- assetChanges.set(assetId, existing + BigInt(amount));
265
+ assetChanges.set(assetId, existing + amount);
266
266
  }
267
267
  }
268
268
  selectedCoins.push(...additional.inputs);
@@ -276,7 +276,7 @@ export class AssetManager extends ReadonlyAssetManager {
276
276
  for (const asset of assets) {
277
277
  if (asset.assetId !== assetId)
278
278
  continue;
279
- changeInputs.push(AssetInput.create(inputIndex, BigInt(asset.amount)));
279
+ changeInputs.push(AssetInput.create(inputIndex, asset.amount));
280
280
  }
281
281
  }
282
282
  groups.push(AssetGroup.create(AssetId.fromString(assetId), null, changeInputs, amount > 0n ? [AssetOutput.create(0, amount)] : [], []));
@@ -15,7 +15,7 @@ export function createAssetPacket(assetInputs, receivers, changeReceiver) {
15
15
  const existing = inputsByAssetId.get(asset.assetId);
16
16
  inputsByAssetId.set(asset.assetId, [
17
17
  ...(existing ?? []),
18
- AssetInput.create(inputIndex, BigInt(asset.amount)),
18
+ AssetInput.create(inputIndex, asset.amount),
19
19
  ]);
20
20
  }
21
21
  }
@@ -29,7 +29,7 @@ export function createAssetPacket(assetInputs, receivers, changeReceiver) {
29
29
  const existing = outputsByAssetId.get(asset.assetId);
30
30
  outputsByAssetId.set(asset.assetId, [
31
31
  ...(existing ?? []),
32
- AssetOutput.create(outputIndex, BigInt(asset.amount)),
32
+ AssetOutput.create(outputIndex, asset.amount),
33
33
  ]);
34
34
  }
35
35
  }
@@ -41,7 +41,7 @@ export function createAssetPacket(assetInputs, receivers, changeReceiver) {
41
41
  const existing = outputsByAssetId.get(asset.assetId);
42
42
  outputsByAssetId.set(asset.assetId, [
43
43
  ...(existing ?? []),
44
- AssetOutput.create(outputIndex, BigInt(asset.amount)),
44
+ AssetOutput.create(outputIndex, asset.amount),
45
45
  ]);
46
46
  }
47
47
  }
@@ -69,9 +69,11 @@ export function selectCoinsWithAsset(coins, assetId, requiredAmount) {
69
69
  const coinsWithAsset = coins.filter((coin) => coin.assets?.some((a) => a.assetId === assetId));
70
70
  // sort by asset amount (smallest first for better selection)
71
71
  coinsWithAsset.sort((a, b) => {
72
- const amountA = a.assets?.find((asset) => asset.assetId === assetId)?.amount ?? 0;
73
- const amountB = b.assets?.find((asset) => asset.assetId === assetId)?.amount ?? 0;
74
- return amountA - amountB;
72
+ const amountA = a.assets?.find((asset) => asset.assetId === assetId)?.amount ?? 0n;
73
+ const amountB = b.assets?.find((asset) => asset.assetId === assetId)?.amount ?? 0n;
74
+ // Array.sort callback returns number; reduce the bigint diff to
75
+ // -1/0/1 (the only thing sort actually consults).
76
+ return amountA < amountB ? -1 : amountA > amountB ? 1 : 0;
75
77
  });
76
78
  const selected = [];
77
79
  let totalAssetAmount = 0n;
@@ -79,8 +81,8 @@ export function selectCoinsWithAsset(coins, assetId, requiredAmount) {
79
81
  if (totalAssetAmount >= requiredAmount)
80
82
  break;
81
83
  selected.push(coin);
82
- const assetAmount = coin.assets?.find((a) => a.assetId === assetId)?.amount ?? 0;
83
- totalAssetAmount += BigInt(assetAmount);
84
+ const assetAmount = coin.assets?.find((a) => a.assetId === assetId)?.amount ?? 0n;
85
+ totalAssetAmount += assetAmount;
84
86
  }
85
87
  if (totalAssetAmount < requiredAmount) {
86
88
  throw new Error(`Insufficient asset balance: have ${totalAssetAmount}, need ${requiredAmount}`);
@@ -240,12 +240,12 @@ async function makeSignedDelegateIntent(identity, coins, outputs, onchainOutputs
240
240
  for (const [, assets] of assetInputs) {
241
241
  for (const asset of assets) {
242
242
  const existing = allAssets.get(asset.assetId) ?? 0n;
243
- allAssets.set(asset.assetId, existing + BigInt(asset.amount));
243
+ allAssets.set(asset.assetId, existing + asset.amount);
244
244
  }
245
245
  }
246
246
  outputAssets = [];
247
247
  for (const [assetId, amount] of allAssets) {
248
- outputAssets.push({ assetId, amount: Number(amount) });
248
+ outputAssets.push({ assetId, amount });
249
249
  }
250
250
  }
251
251
  const recipients = outputs.map((output, i) => ({
@@ -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");
@@ -508,7 +508,7 @@ export class WalletMessageHandler {
508
508
  for (const vtxo of spendableVtxos) {
509
509
  if (vtxo.assets) {
510
510
  for (const a of vtxo.assets) {
511
- const current = assetBalances.get(a.assetId) ?? 0;
511
+ const current = assetBalances.get(a.assetId) ?? 0n;
512
512
  assetBalances.set(a.assetId, current + a.amount);
513
513
  }
514
514
  }
@@ -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,
@@ -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
  }
@@ -11,7 +11,7 @@ import { buildForfeitTx } from '../forfeit.js';
11
11
  import { validateConnectorsTxGraph, validateVtxoTxGraph, } from '../tree/validation.js';
12
12
  import { validateBatchRecipients } from './validation.js';
13
13
  import { isBatchSignable } from '../identity/index.js';
14
- import { isExpired, isRecoverable, isSpendable, isSubdust, TxType, } from './index.js';
14
+ import { DEFAULT_ARKADE_SERVER_URL, isExpired, isRecoverable, isSpendable, isSubdust, TxType, } from './index.js';
15
15
  import { createAssetPacket, selectCoinsWithAsset, selectedCoinsToAssetInputs, } from './asset.js';
16
16
  import { VtxoScript } from '../script/base.js';
17
17
  import { CSVMultisigTapscript } from '../script/tapscript.js';
@@ -34,6 +34,7 @@ import { ContractManager } from '../contracts/contractManager.js';
34
34
  import { contractHandlers } from '../contracts/handlers/index.js';
35
35
  import { timelockToSequence } from '../contracts/handlers/helpers.js';
36
36
  import { clearSyncCursor, updateWalletState } from '../utils/syncCursors.js';
37
+ export const getArkadeServerUrl = ({ arkServerUrl, }) => arkServerUrl || DEFAULT_ARKADE_SERVER_URL;
37
38
  // Historical unilateral exit delay for mainnet (~7 days in seconds).
38
39
  // Kept so existing wallets can still discover and spend VTXOs sent to the
39
40
  // legacy address after arkd starts advertising a different delay.
@@ -120,10 +121,7 @@ export class ReadonlyWallet {
120
121
  // Use provided arkProvider instance or create a new one from arkServerUrl
121
122
  const arkProvider = config.arkProvider ||
122
123
  (() => {
123
- if (!config.arkServerUrl) {
124
- throw new Error("Either arkProvider or arkServerUrl must be provided");
125
- }
126
- return new RestArkProvider(config.arkServerUrl);
124
+ return new RestArkProvider(getArkadeServerUrl(config));
127
125
  })();
128
126
  // Extract arkServerUrl from provider if not explicitly provided
129
127
  const arkServerUrl = config.arkServerUrl || arkProvider.serverUrl;
@@ -302,7 +300,7 @@ export class ReadonlyWallet {
302
300
  continue;
303
301
  if (vtxo.assets) {
304
302
  for (const a of vtxo.assets) {
305
- const current = assetBalances.get(a.assetId) ?? 0;
303
+ const current = assetBalances.get(a.assetId) ?? 0n;
306
304
  assetBalances.set(a.assetId, current + a.amount);
307
305
  }
308
306
  }
@@ -1123,12 +1121,12 @@ export class Wallet extends ReadonlyWallet {
1123
1121
  for (const [, assets] of assetInputs) {
1124
1122
  for (const asset of assets) {
1125
1123
  const existing = allAssets.get(asset.assetId) ?? 0n;
1126
- allAssets.set(asset.assetId, existing + BigInt(asset.amount));
1124
+ allAssets.set(asset.assetId, existing + asset.amount);
1127
1125
  }
1128
1126
  }
1129
1127
  outputAssets = [];
1130
1128
  for (const [assetId, amount] of allAssets) {
1131
- outputAssets.push({ assetId, amount: Number(amount) });
1129
+ outputAssets.push({ assetId, amount });
1132
1130
  }
1133
1131
  }
1134
1132
  const recipients = params.outputs.map((output, i) => ({
@@ -1563,7 +1561,7 @@ export class Wallet extends ReadonlyWallet {
1563
1561
  * const txid = await wallet.send({
1564
1562
  * address: 'ark1q...',
1565
1563
  * amount: 1000, // (optional, default to dust) btc amount to send to the output
1566
- * assets: [{ assetId: 'abc123...', amount: 50 }] // (optional) list of assets to send
1564
+ * assets: [{ assetId: 'abc123...', amount: 50n }] // (optional) list of assets to send
1567
1565
  * });
1568
1566
  * ```
1569
1567
  */
@@ -1594,7 +1592,7 @@ export class Wallet extends ReadonlyWallet {
1594
1592
  continue;
1595
1593
  }
1596
1594
  for (const receiverAsset of recipient.assets) {
1597
- let amountToSelect = BigInt(receiverAsset.amount);
1595
+ let amountToSelect = receiverAsset.amount;
1598
1596
  // check if existing change covers the needed amount
1599
1597
  const existingChange = assetChanges.get(receiverAsset.assetId) ?? 0n;
1600
1598
  if (existingChange >= amountToSelect) {
@@ -1621,7 +1619,7 @@ export class Wallet extends ReadonlyWallet {
1621
1619
  continue;
1622
1620
  }
1623
1621
  const existing = assetChanges.get(a.assetId) ?? 0n;
1624
- assetChanges.set(a.assetId, existing + BigInt(a.amount));
1622
+ assetChanges.set(a.assetId, existing + a.amount);
1625
1623
  }
1626
1624
  }
1627
1625
  }
@@ -1641,7 +1639,7 @@ export class Wallet extends ReadonlyWallet {
1641
1639
  if (coin.assets) {
1642
1640
  for (const asset of coin.assets) {
1643
1641
  const existing = assetChanges.get(asset.assetId) ?? 0n;
1644
- assetChanges.set(asset.assetId, existing + BigInt(asset.amount));
1642
+ assetChanges.set(asset.assetId, existing + asset.amount);
1645
1643
  }
1646
1644
  }
1647
1645
  }
@@ -1663,7 +1661,7 @@ export class Wallet extends ReadonlyWallet {
1663
1661
  if (coin.assets) {
1664
1662
  for (const asset of coin.assets) {
1665
1663
  const existing = assetChanges.get(asset.assetId) ?? 0n;
1666
- assetChanges.set(asset.assetId, existing + BigInt(asset.amount));
1664
+ assetChanges.set(asset.assetId, existing + asset.amount);
1667
1665
  }
1668
1666
  }
1669
1667
  }
@@ -1678,7 +1676,7 @@ export class Wallet extends ReadonlyWallet {
1678
1676
  const changeAssets = [];
1679
1677
  for (const [assetId, amount] of assetChanges) {
1680
1678
  if (amount > 0n) {
1681
- changeAssets.push({ assetId, amount: Number(amount) });
1679
+ changeAssets.push({ assetId, amount });
1682
1680
  }
1683
1681
  }
1684
1682
  changeIndex = outputs.length;
@@ -88,7 +88,7 @@ export declare function contractFromArkContract(encoded: string, options?: {
88
88
  * @param options - Additional options
89
89
  * @returns A complete Contract object
90
90
  */
91
- export declare function contractFromArkContractWithAddress(encoded: string, serverPubKey: Uint8Array, addressPrefix: string, options?: {
91
+ export declare function contractFromArkContractWithAddress(encoded: string, serverPubKey: Uint8Array, addressPrefix?: string, options?: {
92
92
  label?: string;
93
93
  state?: "active" | "inactive";
94
94
  metadata?: Record<string, unknown>;