@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.
- package/README.md +5 -25
- package/dist/cjs/contracts/contractManager.js +31 -11
- package/dist/cjs/contracts/contractWatcher.js +2 -2
- package/dist/cjs/identity/hdCapableIdentity.js +18 -0
- package/dist/cjs/identity/index.js +3 -1
- package/dist/cjs/identity/seedIdentity.js +16 -0
- package/dist/cjs/index.js +4 -2
- package/dist/cjs/wallet/delegator.js +10 -4
- package/dist/cjs/wallet/hdDescriptorProvider.js +29 -0
- package/dist/cjs/wallet/inputSignerRouter.js +98 -0
- package/dist/cjs/wallet/serviceWorker/wallet.js +1 -0
- package/dist/cjs/wallet/signingErrors.js +32 -0
- package/dist/cjs/wallet/unroll.js +5 -1
- package/dist/cjs/wallet/wallet.js +232 -86
- package/dist/cjs/wallet/walletReceiveRotator.js +547 -0
- package/dist/cjs/worker/messageBus.js +1 -0
- package/dist/esm/contracts/contractManager.js +31 -11
- package/dist/esm/contracts/contractWatcher.js +2 -2
- package/dist/esm/identity/hdCapableIdentity.js +17 -1
- package/dist/esm/identity/index.js +1 -0
- package/dist/esm/identity/seedIdentity.js +16 -0
- package/dist/esm/index.js +2 -2
- package/dist/esm/wallet/delegator.js +10 -4
- package/dist/esm/wallet/hdDescriptorProvider.js +29 -0
- package/dist/esm/wallet/inputSignerRouter.js +94 -0
- package/dist/esm/wallet/serviceWorker/wallet.js +1 -0
- package/dist/esm/wallet/signingErrors.js +27 -0
- package/dist/esm/wallet/unroll.js +5 -1
- package/dist/esm/wallet/wallet.js +231 -86
- package/dist/esm/wallet/walletReceiveRotator.js +540 -0
- package/dist/esm/worker/messageBus.js +1 -0
- package/dist/types/contracts/contractManager.d.ts +33 -3
- package/dist/types/contracts/types.d.ts +19 -2
- package/dist/types/identity/descriptorProvider.d.ts +7 -0
- package/dist/types/identity/hdCapableIdentity.d.ts +30 -3
- package/dist/types/identity/index.d.ts +1 -0
- package/dist/types/identity/seedIdentity.d.ts +16 -0
- package/dist/types/index.d.ts +6 -6
- package/dist/types/wallet/hdDescriptorProvider.d.ts +22 -1
- package/dist/types/wallet/index.d.ts +34 -0
- package/dist/types/wallet/inputSignerRouter.d.ts +35 -0
- package/dist/types/wallet/serviceWorker/wallet.d.ts +10 -0
- package/dist/types/wallet/signingErrors.d.ts +19 -0
- package/dist/types/wallet/wallet.d.ts +51 -2
- package/dist/types/wallet/walletReceiveRotator.d.ts +306 -0
- package/dist/types/worker/messageBus.d.ts +1 -0
- 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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
}
|