@arkade-os/sdk 0.4.26 → 0.4.27

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 (47) hide show
  1. package/README.md +5 -25
  2. package/dist/cjs/contracts/contractManager.js +31 -11
  3. package/dist/cjs/contracts/contractWatcher.js +2 -2
  4. package/dist/cjs/identity/hdCapableIdentity.js +18 -0
  5. package/dist/cjs/identity/index.js +3 -1
  6. package/dist/cjs/identity/seedIdentity.js +16 -0
  7. package/dist/cjs/index.js +4 -2
  8. package/dist/cjs/wallet/delegator.js +10 -4
  9. package/dist/cjs/wallet/hdDescriptorProvider.js +29 -0
  10. package/dist/cjs/wallet/inputSignerRouter.js +98 -0
  11. package/dist/cjs/wallet/serviceWorker/wallet.js +1 -0
  12. package/dist/cjs/wallet/signingErrors.js +32 -0
  13. package/dist/cjs/wallet/unroll.js +5 -1
  14. package/dist/cjs/wallet/wallet.js +232 -86
  15. package/dist/cjs/wallet/walletReceiveRotator.js +547 -0
  16. package/dist/cjs/worker/messageBus.js +1 -0
  17. package/dist/esm/contracts/contractManager.js +31 -11
  18. package/dist/esm/contracts/contractWatcher.js +2 -2
  19. package/dist/esm/identity/hdCapableIdentity.js +17 -1
  20. package/dist/esm/identity/index.js +1 -0
  21. package/dist/esm/identity/seedIdentity.js +16 -0
  22. package/dist/esm/index.js +2 -2
  23. package/dist/esm/wallet/delegator.js +10 -4
  24. package/dist/esm/wallet/hdDescriptorProvider.js +29 -0
  25. package/dist/esm/wallet/inputSignerRouter.js +94 -0
  26. package/dist/esm/wallet/serviceWorker/wallet.js +1 -0
  27. package/dist/esm/wallet/signingErrors.js +27 -0
  28. package/dist/esm/wallet/unroll.js +5 -1
  29. package/dist/esm/wallet/wallet.js +231 -86
  30. package/dist/esm/wallet/walletReceiveRotator.js +540 -0
  31. package/dist/esm/worker/messageBus.js +1 -0
  32. package/dist/types/contracts/contractManager.d.ts +33 -3
  33. package/dist/types/contracts/types.d.ts +19 -2
  34. package/dist/types/identity/descriptorProvider.d.ts +7 -0
  35. package/dist/types/identity/hdCapableIdentity.d.ts +30 -3
  36. package/dist/types/identity/index.d.ts +1 -0
  37. package/dist/types/identity/seedIdentity.d.ts +16 -0
  38. package/dist/types/index.d.ts +6 -6
  39. package/dist/types/wallet/hdDescriptorProvider.d.ts +22 -1
  40. package/dist/types/wallet/index.d.ts +34 -0
  41. package/dist/types/wallet/inputSignerRouter.d.ts +35 -0
  42. package/dist/types/wallet/serviceWorker/wallet.d.ts +10 -0
  43. package/dist/types/wallet/signingErrors.d.ts +19 -0
  44. package/dist/types/wallet/wallet.d.ts +51 -2
  45. package/dist/types/wallet/walletReceiveRotator.d.ts +306 -0
  46. package/dist/types/worker/messageBus.d.ts +1 -0
  47. package/package.json +1 -1
@@ -180,6 +180,10 @@ export class SeedIdentity {
180
180
  * Returns true when `descriptor` is derived from this identity's seed.
181
181
  * HD descriptors match by account xpub; bare `tr(pubkey)` descriptors
182
182
  * match by raw pubkey. See {@link descriptorIsOurs}.
183
+ *
184
+ * @deprecated Prefer `DescriptorProvider.isOurs()` via
185
+ * `HDDescriptorProvider` for rotating HD wallets or
186
+ * `StaticDescriptorProvider` for legacy single-key wallets.
183
187
  */
184
188
  isOurs(descriptor) {
185
189
  return descriptorIsOurs(descriptor, this.descriptor, pubSchnorr(this.derivedKey));
@@ -187,6 +191,10 @@ export class SeedIdentity {
187
191
  /**
188
192
  * Signs each request with the key derived from its descriptor.
189
193
  * Each descriptor must share this identity's seed ({@link isOurs}).
194
+ *
195
+ * @deprecated Prefer `DescriptorProvider.signWithDescriptor()` via
196
+ * `HDDescriptorProvider` or `StaticDescriptorProvider`. Identities keep
197
+ * this method only as backing implementation for descriptor providers.
190
198
  */
191
199
  async signWithDescriptor(requests) {
192
200
  return requests.map((request) => {
@@ -199,6 +207,10 @@ export class SeedIdentity {
199
207
  }
200
208
  /**
201
209
  * Signs a message with the key derived from `descriptor`.
210
+ *
211
+ * @deprecated Prefer `DescriptorProvider.signMessageWithDescriptor()` via
212
+ * `HDDescriptorProvider` or `StaticDescriptorProvider`. Identities keep
213
+ * this method only as backing implementation for descriptor providers.
202
214
  */
203
215
  async signMessageWithDescriptor(descriptor, message, signatureType = "schnorr") {
204
216
  if (!this.isOurs(descriptor)) {
@@ -379,6 +391,10 @@ export class ReadonlyDescriptorIdentity {
379
391
  * HD descriptors match by account xpub; bare `tr(pubkey)` descriptors
380
392
  * fall back to comparing against the index-0 x-only pubkey. See
381
393
  * {@link descriptorIsOurs}.
394
+ *
395
+ * @deprecated Prefer `DescriptorProvider.isOurs()` via
396
+ * `HDDescriptorProvider` for rotating HD wallets or
397
+ * `StaticDescriptorProvider` for legacy single-key wallets.
382
398
  */
383
399
  isOurs(descriptor) {
384
400
  return descriptorIsOurs(descriptor, this.descriptor, this.indexZero.pubkey);
package/dist/esm/index.js CHANGED
@@ -10,7 +10,7 @@ import { MessageBus, } from "./worker/messageBus.js";
10
10
  import { VtxoScript, TapTreeCoder, getSequence, } from "./script/base.js";
11
11
  import { TxType, isSpendable, isSubdust, isRecoverable, isExpired, } from "./wallet/index.js";
12
12
  import { Batch } from "./wallet/batch.js";
13
- import { Wallet, ReadonlyWallet, waitForIncomingFunds, } from "./wallet/wallet.js";
13
+ import { Wallet, ReadonlyWallet, waitForIncomingFunds, DescriptorSigningProviderMissingError, MissingSigningDescriptorError, } from "./wallet/wallet.js";
14
14
  import { TxTree } from "./tree/txTree.js";
15
15
  import { Ramps } from "./wallet/ramps.js";
16
16
  import { HDDescriptorProvider } from "./wallet/hdDescriptorProvider.js";
@@ -80,7 +80,7 @@ TxTree,
80
80
  // Anchor
81
81
  P2A, Unroll, Transaction, TxWeightEstimator, timelockToSequence, sequenceToTimelock,
82
82
  // Errors
83
- ArkError, maybeArkError,
83
+ ArkError, maybeArkError, DescriptorSigningProviderMissingError, MissingSigningDescriptorError,
84
84
  // Batch session
85
85
  Batch, validateVtxoTxGraph, validateConnectorsTxGraph, buildForfeitTx, isRecoverable, isSpendable, isSubdust, isExpired, getSequence,
86
86
  // Contracts
@@ -25,10 +25,11 @@ export class DelegatorManagerImpl {
25
25
  // fetch server and delegator info once, shared across all groups
26
26
  const arkInfo = await this.arkInfoProvider.getInfo();
27
27
  const delegateInfo = await this.delegatorProvider.getDelegateInfo();
28
- // keep only vtxos that can be signed by the delegate
29
- const eligible = vtxos
30
- .filter((v) => findDelegateTapLeaf(v, delegateInfo.pubkey) !== undefined)
31
- .map((v) => v);
28
+ // keep only vtxos that can be signed by the delegate. The guard
29
+ // narrows ContractVtxo (with optional taproot fields) to the
30
+ // ExtendedVirtualCoin shape required by makeDelegateForfeitTx.
31
+ const eligible = vtxos.filter((v) => isAnnotated(v) &&
32
+ findDelegateTapLeaf(v, delegateInfo.pubkey) !== undefined);
32
33
  if (eligible.length === 0) {
33
34
  return { delegated: [], failed: [] };
34
35
  }
@@ -295,3 +296,8 @@ function findDelegateTapLeaf(vtxo, delegatePubkey) {
295
296
  return arkTapscript.params.pubkeys.map(hex.encode).includes(pk);
296
297
  });
297
298
  }
299
+ function isAnnotated(v) {
300
+ return (v.tapTree !== undefined &&
301
+ v.forfeitTapLeafScript !== undefined &&
302
+ v.intentTapLeafScript !== undefined);
303
+ }
@@ -1,6 +1,7 @@
1
1
  import { expand, networks } from "@bitcoinerlab/descriptors-scure";
2
2
  import { isMainnetDescriptor } from "../identity/descriptor.js";
3
3
  import { updateWalletState } from "../utils/syncCursors.js";
4
+ import { WalletReceiveRotator, } from "./walletReceiveRotator.js";
4
5
  /** Settings key under {@link WalletState.settings} where HD state lives. */
5
6
  const HD_SETTINGS_KEY = "hd";
6
7
  /**
@@ -60,6 +61,25 @@ export class HDDescriptorProvider {
60
61
  return this.materializeAt(next);
61
62
  });
62
63
  }
64
+ /**
65
+ * Re-derive the descriptor at the most recently allocated index
66
+ * WITHOUT advancing — i.e. read the same descriptor
67
+ * `getNextSigningDescriptor` last returned. Returns `undefined`
68
+ * when no descriptor has ever been allocated on this repo.
69
+ *
70
+ * Used by the boot path to keep the wallet's display address
71
+ * stable across restarts: when no tagged display contract exists
72
+ * (e.g. a fresh wallet that hasn't rotated yet, or a wallet whose
73
+ * baseline-only repo carries no rotation history), the boot should
74
+ * re-derive the existing index rather than burn a new one.
75
+ */
76
+ async getCurrentSigningDescriptor() {
77
+ const state = await this.walletRepository.getWalletState();
78
+ const settings = this.parseSettings(state ?? {});
79
+ if (settings.lastIndexUsed === undefined)
80
+ return undefined;
81
+ return this.materializeAt(settings.lastIndexUsed);
82
+ }
63
83
  /**
64
84
  * Returns true when the given descriptor is derivable from this wallet's
65
85
  * seed. Delegates to the underlying identity, which handles both HD and
@@ -80,6 +100,15 @@ export class HDDescriptorProvider {
80
100
  async signMessageWithDescriptor(descriptor, message, signatureType = "schnorr") {
81
101
  return this.identity.signMessageWithDescriptor(descriptor, message, signatureType);
82
102
  }
103
+ /**
104
+ * HD providers participate in receive rotation. The default
105
+ * factory boot (contract-repo lookup → allocate fresh descriptor)
106
+ * is exactly what we want, so this just delegates to
107
+ * {@link WalletReceiveRotator.defaultBoot}.
108
+ */
109
+ async createReceiveRotator(opts) {
110
+ return WalletReceiveRotator.defaultBoot(this, opts);
111
+ }
83
112
  // ── internals ────────────────────────────────────────────────────
84
113
  /**
85
114
  * Substitute the wildcard in the identity's account-descriptor template
@@ -0,0 +1,94 @@
1
+ import { hex } from "@scure/base";
2
+ import { DescriptorSigningProviderMissingError, MissingSigningDescriptorError, } from "./signingErrors.js";
3
+ const DESCRIPTOR_CAPABLE_CONTRACT_TYPES = new Set(["default", "delegate"]);
4
+ /**
5
+ * Routes PSBT inputs to the correct signer based on the owning contract.
6
+ * Inputs whose script matches a `default`/`delegate` contract with a
7
+ * non-baseline owner are sent to {@link DescriptorProvider}; everything
8
+ * else (baseline-owned contracts, non-default/non-delegate contracts,
9
+ * and the boarding script) is sent to {@link Identity}. Inputs with no
10
+ * matching contract and no boarding match are silently skipped, matching
11
+ * how the wallet historically handled cosigner/connector inputs.
12
+ */
13
+ export class InputSignerRouter {
14
+ constructor(deps) {
15
+ this.deps = deps;
16
+ }
17
+ async sign(tx, jobs) {
18
+ if (jobs.length === 0)
19
+ return tx;
20
+ const distinctScripts = Array.from(new Set(jobs.map((j) => hex.encode(j.lookupScript))));
21
+ const contracts = await this.deps.contractRepository.getContracts({
22
+ script: distinctScripts,
23
+ });
24
+ // Repo may yield duplicates if seeded oddly; keep the first one
25
+ // for each script to match the wallet's historical behaviour.
26
+ const scriptToContract = new Map();
27
+ for (const contract of contracts) {
28
+ if (!scriptToContract.has(contract.script)) {
29
+ scriptToContract.set(contract.script, contract);
30
+ }
31
+ }
32
+ const baselinePubKeyHex = hex.encode(await this.deps.identity.xOnlyPublicKey());
33
+ const boardingScriptHex = hex.encode(this.deps.boardingPkScript);
34
+ const identityIndexes = [];
35
+ const descriptorGroups = new Map();
36
+ for (const job of jobs) {
37
+ const scriptHex = hex.encode(job.lookupScript);
38
+ const contract = scriptToContract.get(scriptHex);
39
+ if (!contract) {
40
+ if (scriptHex === boardingScriptHex) {
41
+ identityIndexes.push(job.index);
42
+ }
43
+ continue;
44
+ }
45
+ if (!DESCRIPTOR_CAPABLE_CONTRACT_TYPES.has(contract.type)) {
46
+ identityIndexes.push(job.index);
47
+ continue;
48
+ }
49
+ // `baselinePubKeyHex` is freshly produced by `hex.encode`,
50
+ // so it is already lowercase. `contract.params.pubKey` is
51
+ // persisted data: a migration or custom repository adapter
52
+ // could legitimately store it uppercase, so canonicalize
53
+ // before comparing to match the legacy router behaviour.
54
+ const ownerPubKeyHex = contract.params.pubKey?.toLowerCase();
55
+ if (ownerPubKeyHex && ownerPubKeyHex === baselinePubKeyHex) {
56
+ identityIndexes.push(job.index);
57
+ continue;
58
+ }
59
+ const descriptor = contract.metadata?.signingDescriptor;
60
+ if (typeof descriptor !== "string" || descriptor.length === 0) {
61
+ throw new MissingSigningDescriptorError(contract.script, contract.type);
62
+ }
63
+ const bucket = descriptorGroups.get(descriptor);
64
+ if (bucket) {
65
+ bucket.push(job.index);
66
+ }
67
+ else {
68
+ descriptorGroups.set(descriptor, [job.index]);
69
+ }
70
+ }
71
+ let signed = tx;
72
+ if (identityIndexes.length > 0) {
73
+ signed = await this.deps.identity.sign(signed, identityIndexes);
74
+ }
75
+ if (descriptorGroups.size > 0) {
76
+ if (!this.deps.descriptorProvider) {
77
+ throw new DescriptorSigningProviderMissingError();
78
+ }
79
+ const sortedDescriptors = Array.from(descriptorGroups.keys()).sort();
80
+ for (const descriptor of sortedDescriptors) {
81
+ const indexes = descriptorGroups.get(descriptor);
82
+ const [next] = await this.deps.descriptorProvider.signWithDescriptor([
83
+ {
84
+ tx: signed,
85
+ descriptor,
86
+ inputIndexes: indexes,
87
+ },
88
+ ]);
89
+ signed = next;
90
+ }
91
+ }
92
+ return signed;
93
+ }
94
+ }
@@ -917,6 +917,7 @@ export class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
917
917
  indexerUrl: options.indexerUrl,
918
918
  esploraUrl: options.esploraUrl,
919
919
  settlementConfig: options.settlementConfig,
920
+ walletMode: options.walletMode,
920
921
  watcherConfig: options.watcherConfig,
921
922
  messageTimeouts,
922
923
  };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Thrown when a rotated contract (default or delegate) is missing the
3
+ * metadata.signingDescriptor required to route it to a descriptor-aware
4
+ * signer.
5
+ */
6
+ export class MissingSigningDescriptorError extends Error {
7
+ constructor(contractScript, contractType) {
8
+ super(`Cannot sign input for ${contractType} contract ${contractScript}: ` +
9
+ `metadata.signingDescriptor is missing. This wallet was rotated ` +
10
+ `on an earlier build that did not persist signing descriptors. ` +
11
+ `Manually set metadata.signingDescriptor on the contract record, ` +
12
+ `or restore from a pre-rotation snapshot.`);
13
+ this.contractScript = contractScript;
14
+ this.contractType = contractType;
15
+ this.name = "MissingSigningDescriptorError";
16
+ }
17
+ }
18
+ /**
19
+ * Thrown when an input needs descriptor-aware signing but no
20
+ * DescriptorProvider was wired into the wallet.
21
+ */
22
+ export class DescriptorSigningProviderMissingError extends Error {
23
+ constructor() {
24
+ super("Descriptor signing requested but no DescriptorProvider was wired into this wallet");
25
+ this.name = "DescriptorSigningProviderMissingError";
26
+ }
27
+ }
@@ -227,7 +227,11 @@ export async function prepareUnrollTransaction(wallet, vtxoTxIds, outputAddress)
227
227
  if (!feeRate || feeRate < Wallet.MIN_FEE_RATE) {
228
228
  feeRate = Wallet.MIN_FEE_RATE;
229
229
  }
230
- const feeAmount = txWeightEstimator.vsize().fee(BigInt(feeRate));
230
+ // Esplora returns a `number` and bitcoind regtest sometimes reports
231
+ // fractional sat/vB (e.g. 1.006). `BigInt(1.006)` throws RangeError
232
+ // — round up so we always pay AT LEAST the advertised rate and
233
+ // satisfy BigInt's integer requirement.
234
+ const feeAmount = txWeightEstimator.vsize().fee(BigInt(Math.ceil(feeRate)));
231
235
  if (feeAmount > totalAmount) {
232
236
  throw new Error("fee amount is greater than the total amount");
233
237
  }