@arkade-os/sdk 0.4.19 → 0.4.21
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/dist/cjs/contracts/contractWatcher.js +33 -3
- package/dist/cjs/contracts/handlers/default.js +10 -3
- package/dist/cjs/contracts/handlers/helpers.js +47 -5
- package/dist/cjs/contracts/handlers/vhtlc.js +4 -2
- package/dist/cjs/identity/descriptor.js +98 -0
- package/dist/cjs/identity/descriptorProvider.js +2 -0
- package/dist/cjs/identity/index.js +15 -1
- package/dist/cjs/identity/seedIdentity.js +91 -6
- package/dist/cjs/identity/serialize.js +166 -0
- package/dist/cjs/identity/staticDescriptorProvider.js +65 -0
- package/dist/cjs/index.js +6 -3
- package/dist/cjs/providers/ark.js +71 -46
- package/dist/cjs/providers/electrum.js +663 -0
- package/dist/cjs/providers/indexer.js +60 -43
- package/dist/cjs/providers/utils.js +62 -12
- package/dist/cjs/wallet/ramps.js +1 -1
- package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +10 -0
- package/dist/cjs/wallet/serviceWorker/wallet.js +137 -91
- package/dist/cjs/wallet/vtxo-manager.js +56 -8
- package/dist/cjs/wallet/wallet.js +130 -156
- package/dist/cjs/worker/messageBus.js +200 -56
- package/dist/esm/contracts/contractWatcher.js +33 -3
- package/dist/esm/contracts/handlers/default.js +10 -3
- package/dist/esm/contracts/handlers/helpers.js +47 -5
- package/dist/esm/contracts/handlers/vhtlc.js +4 -2
- package/dist/esm/identity/descriptor.js +92 -0
- package/dist/esm/identity/descriptorProvider.js +1 -0
- package/dist/esm/identity/index.js +6 -1
- package/dist/esm/identity/seedIdentity.js +89 -6
- package/dist/esm/identity/serialize.js +159 -0
- package/dist/esm/identity/staticDescriptorProvider.js +61 -0
- package/dist/esm/index.js +2 -1
- package/dist/esm/providers/ark.js +72 -47
- package/dist/esm/providers/electrum.js +658 -0
- package/dist/esm/providers/indexer.js +61 -44
- package/dist/esm/providers/utils.js +61 -12
- package/dist/esm/wallet/ramps.js +1 -1
- package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +10 -0
- package/dist/esm/wallet/serviceWorker/wallet.js +137 -91
- package/dist/esm/wallet/vtxo-manager.js +56 -8
- package/dist/esm/wallet/wallet.js +130 -156
- package/dist/esm/worker/messageBus.js +201 -57
- package/dist/types/contracts/contractWatcher.d.ts +3 -0
- package/dist/types/contracts/handlers/default.d.ts +1 -1
- package/dist/types/contracts/handlers/helpers.d.ts +1 -1
- package/dist/types/contracts/types.d.ts +11 -3
- package/dist/types/identity/descriptor.d.ts +35 -0
- package/dist/types/identity/descriptorProvider.d.ts +28 -0
- package/dist/types/identity/index.d.ts +7 -1
- package/dist/types/identity/seedIdentity.d.ts +41 -4
- package/dist/types/identity/serialize.d.ts +84 -0
- package/dist/types/identity/staticDescriptorProvider.d.ts +18 -0
- package/dist/types/index.d.ts +4 -2
- package/dist/types/providers/electrum.d.ts +212 -0
- package/dist/types/providers/utils.d.ts +10 -5
- package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +11 -2
- package/dist/types/wallet/serviceWorker/wallet.d.ts +27 -10
- package/dist/types/wallet/vtxo-manager.d.ts +2 -0
- package/dist/types/wallet/wallet.d.ts +7 -6
- package/dist/types/worker/messageBus.d.ts +68 -8
- package/package.json +3 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ContractWatcher = void 0;
|
|
4
|
+
const utils_1 = require("../providers/utils");
|
|
4
5
|
/**
|
|
5
6
|
* Watches multiple contracts for virtual output state changes with resilient connection handling.
|
|
6
7
|
*
|
|
@@ -253,13 +254,18 @@ class ContractWatcher {
|
|
|
253
254
|
}
|
|
254
255
|
/**
|
|
255
256
|
* Connect to the subscription.
|
|
257
|
+
*
|
|
258
|
+
* @param skipUpdate - Skip the leading `updateSubscription` call when
|
|
259
|
+
* the caller has already established `subscriptionId`.
|
|
256
260
|
*/
|
|
257
|
-
async connect() {
|
|
261
|
+
async connect(skipUpdate = false) {
|
|
258
262
|
if (!this.isWatching)
|
|
259
263
|
return;
|
|
260
264
|
this.connectionState = "connecting";
|
|
261
265
|
try {
|
|
262
|
-
|
|
266
|
+
if (!skipUpdate) {
|
|
267
|
+
await this.updateSubscription();
|
|
268
|
+
}
|
|
263
269
|
// Poll immediately after connection to sync state
|
|
264
270
|
await this.pollAllContracts();
|
|
265
271
|
this.connectionState = "connected";
|
|
@@ -270,7 +276,12 @@ class ContractWatcher {
|
|
|
270
276
|
// indefinitely and block the caller.
|
|
271
277
|
// Error management must be implemented to ensure the connection
|
|
272
278
|
// is restored and events are fired.
|
|
273
|
-
|
|
279
|
+
if ((0, utils_1.isEventSourceError)(e)) {
|
|
280
|
+
console.debug("ContractWatcher subscription disconnected; reconnecting");
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
console.error(e);
|
|
284
|
+
}
|
|
274
285
|
this.connectionState = "disconnected";
|
|
275
286
|
this.eventCallback?.({
|
|
276
287
|
type: "connection_reset",
|
|
@@ -385,11 +396,30 @@ class ContractWatcher {
|
|
|
385
396
|
}
|
|
386
397
|
}
|
|
387
398
|
async tryUpdateSubscription() {
|
|
399
|
+
const hadSubscription = this.subscriptionId !== undefined;
|
|
388
400
|
try {
|
|
389
401
|
await this.updateSubscription();
|
|
390
402
|
}
|
|
391
403
|
catch (error) {
|
|
392
404
|
// nothing, the connection will be retried later
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
// Cold start: `startWatching` may have run with zero scripts,
|
|
408
|
+
// leaving `listenLoop` parked behind the reconnect timer. Kick
|
|
409
|
+
// `connect` now so streaming resumes without waiting on the
|
|
410
|
+
// backoff. `skipUpdate` avoids re-issuing `subscribeForScripts`.
|
|
411
|
+
const justGotSubscription = !hadSubscription && this.subscriptionId !== undefined;
|
|
412
|
+
const listenerParked = this.connectionState === "disconnected" ||
|
|
413
|
+
this.connectionState === "reconnecting";
|
|
414
|
+
if (this.isWatching && justGotSubscription && listenerParked) {
|
|
415
|
+
if (this.reconnectTimeoutId) {
|
|
416
|
+
clearTimeout(this.reconnectTimeoutId);
|
|
417
|
+
this.reconnectTimeoutId = undefined;
|
|
418
|
+
}
|
|
419
|
+
this.reconnectAttempts = 0;
|
|
420
|
+
this.connect(true).catch((error) => {
|
|
421
|
+
console.warn("ContractWatcher cold-start connect failed:", error);
|
|
422
|
+
});
|
|
393
423
|
}
|
|
394
424
|
}
|
|
395
425
|
/**
|
|
@@ -4,8 +4,15 @@ exports.DefaultContractHandler = void 0;
|
|
|
4
4
|
const base_1 = require("@scure/base");
|
|
5
5
|
const default_1 = require("../../script/default");
|
|
6
6
|
const helpers_1 = require("./helpers");
|
|
7
|
+
const descriptor_1 = require("../../identity/descriptor");
|
|
7
8
|
/**
|
|
8
|
-
*
|
|
9
|
+
* Extract pubkey bytes from a descriptor or hex string.
|
|
10
|
+
*/
|
|
11
|
+
function extractPubKeyBytes(value) {
|
|
12
|
+
return base_1.hex.decode((0, descriptor_1.extractPubKey)((0, descriptor_1.normalizeToDescriptor)(value)));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Handler for default wallet VTXOs.
|
|
9
16
|
*
|
|
10
17
|
* Default contracts use the standard forfeit + exit tapscript:
|
|
11
18
|
* - forfeit: (Alice + Server) multisig for collaborative spending
|
|
@@ -29,8 +36,8 @@ exports.DefaultContractHandler = {
|
|
|
29
36
|
? (0, helpers_1.sequenceToTimelock)(Number(params.csvTimelock))
|
|
30
37
|
: default_1.DefaultVtxo.Script.DEFAULT_TIMELOCK;
|
|
31
38
|
return {
|
|
32
|
-
pubKey:
|
|
33
|
-
serverPubKey:
|
|
39
|
+
pubKey: extractPubKeyBytes(params.pubKey),
|
|
40
|
+
serverPubKey: extractPubKeyBytes(params.serverPubKey),
|
|
34
41
|
csvTimelock,
|
|
35
42
|
};
|
|
36
43
|
},
|
|
@@ -39,6 +39,23 @@ exports.resolveRole = resolveRole;
|
|
|
39
39
|
exports.isCltvSatisfied = isCltvSatisfied;
|
|
40
40
|
exports.isCsvSpendable = isCsvSpendable;
|
|
41
41
|
const bip68 = __importStar(require("bip68"));
|
|
42
|
+
const descriptor_1 = require("../../identity/descriptor");
|
|
43
|
+
/**
|
|
44
|
+
* Extract raw hex pubkey from a value that may be a descriptor or raw hex.
|
|
45
|
+
* Returns undefined for HD descriptors or unparseable values so role
|
|
46
|
+
* resolution stays best-effort and never throws.
|
|
47
|
+
*/
|
|
48
|
+
function extractRawPubKey(value) {
|
|
49
|
+
if (!(0, descriptor_1.isDescriptor)(value)) {
|
|
50
|
+
return value.toLowerCase();
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
return (0, descriptor_1.extractPubKey)(value).toLowerCase();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
42
59
|
/**
|
|
43
60
|
* Convert RelativeTimelock to BIP68 sequence number.
|
|
44
61
|
*/
|
|
@@ -61,21 +78,46 @@ function sequenceToTimelock(sequence) {
|
|
|
61
78
|
throw new Error(`Invalid BIP68 sequence: ${sequence}`);
|
|
62
79
|
}
|
|
63
80
|
/**
|
|
64
|
-
* Resolve wallet's role from explicit role or by matching pubkey.
|
|
81
|
+
* Resolve wallet's role from explicit role or by matching descriptor/pubkey.
|
|
65
82
|
*/
|
|
66
83
|
function resolveRole(contract, context) {
|
|
67
84
|
// Explicit role takes precedence
|
|
68
85
|
if (context.role === "sender" || context.role === "receiver") {
|
|
69
86
|
return context.role;
|
|
70
87
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
88
|
+
const senderKey = contract.params.sender
|
|
89
|
+
? extractRawPubKey(contract.params.sender)
|
|
90
|
+
: undefined;
|
|
91
|
+
const receiverKey = contract.params.receiver
|
|
92
|
+
? extractRawPubKey(contract.params.receiver)
|
|
93
|
+
: undefined;
|
|
94
|
+
const matchRole = (rawWalletKey) => {
|
|
95
|
+
if (!rawWalletKey)
|
|
96
|
+
return undefined;
|
|
97
|
+
if (senderKey && rawWalletKey === senderKey) {
|
|
74
98
|
return "sender";
|
|
75
99
|
}
|
|
76
|
-
if (
|
|
100
|
+
if (receiverKey && rawWalletKey === receiverKey) {
|
|
77
101
|
return "receiver";
|
|
78
102
|
}
|
|
103
|
+
return undefined;
|
|
104
|
+
};
|
|
105
|
+
// Try the preferred descriptor first. If it cannot be resolved
|
|
106
|
+
// (for example an HD descriptor without derivation support), fall back
|
|
107
|
+
// to walletPubKey for backward compatibility.
|
|
108
|
+
if (context.walletDescriptor) {
|
|
109
|
+
const walletDescriptorKey = extractRawPubKey(context.walletDescriptor);
|
|
110
|
+
const matchedRole = matchRole(walletDescriptorKey);
|
|
111
|
+
if (matchedRole) {
|
|
112
|
+
return matchedRole;
|
|
113
|
+
}
|
|
114
|
+
if (!walletDescriptorKey && context.walletPubKey) {
|
|
115
|
+
return matchRole(extractRawPubKey(context.walletPubKey));
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
if (context.walletPubKey) {
|
|
120
|
+
return matchRole(extractRawPubKey(context.walletPubKey));
|
|
79
121
|
}
|
|
80
122
|
return undefined;
|
|
81
123
|
}
|
|
@@ -52,7 +52,8 @@ exports.VHTLCContractHandler = {
|
|
|
52
52
|
/**
|
|
53
53
|
* Select spending path based on context.
|
|
54
54
|
*
|
|
55
|
-
* Role is determined from `context.role` or by matching
|
|
55
|
+
* Role is determined from `context.role` or by matching
|
|
56
|
+
* `context.walletDescriptor` (preferred) / `context.walletPubKey`
|
|
56
57
|
* against sender/receiver in contract params.
|
|
57
58
|
*/
|
|
58
59
|
selectPath(script, contract, context) {
|
|
@@ -101,7 +102,8 @@ exports.VHTLCContractHandler = {
|
|
|
101
102
|
/**
|
|
102
103
|
* Get all possible spending paths (no timelock checks).
|
|
103
104
|
*
|
|
104
|
-
* Role is determined from `context.role` or by matching
|
|
105
|
+
* Role is determined from `context.role` or by matching
|
|
106
|
+
* `context.walletDescriptor` (preferred) / `context.walletPubKey`
|
|
105
107
|
* against sender/receiver in contract params.
|
|
106
108
|
*/
|
|
107
109
|
getAllSpendingPaths(script, contract, context) {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isDescriptor = isDescriptor;
|
|
4
|
+
exports.normalizeToDescriptor = normalizeToDescriptor;
|
|
5
|
+
exports.extractPubKey = extractPubKey;
|
|
6
|
+
exports.parseHDDescriptor = parseHDDescriptor;
|
|
7
|
+
const descriptors_scure_1 = require("@bitcoinerlab/descriptors-scure");
|
|
8
|
+
const base_1 = require("@scure/base");
|
|
9
|
+
function inferNetwork(descriptor) {
|
|
10
|
+
return descriptor.includes("tpub") ? descriptors_scure_1.networks.testnet : descriptors_scure_1.networks.bitcoin;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Check if a string is a descriptor of the shape `tr(...)`.
|
|
14
|
+
*
|
|
15
|
+
* This is a shape check only — it does not validate the inner key material.
|
|
16
|
+
* Use {@link expand} (via {@link extractPubKey} / {@link parseHDDescriptor})
|
|
17
|
+
* for full parsing. The guard rejects empty bodies and missing/trailing
|
|
18
|
+
* parentheses so callers can safely branch on descriptor vs. raw pubkey.
|
|
19
|
+
*/
|
|
20
|
+
function isDescriptor(value) {
|
|
21
|
+
if (typeof value !== "string")
|
|
22
|
+
return false;
|
|
23
|
+
if (!value.startsWith("tr(") || !value.endsWith(")"))
|
|
24
|
+
return false;
|
|
25
|
+
// body length > 0 after stripping "tr(" and ")"
|
|
26
|
+
return value.length > "tr()".length;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Normalize a value to descriptor format.
|
|
30
|
+
* If already a descriptor, return as-is. If hex pubkey, wrap as tr(pubkey).
|
|
31
|
+
* Throws when the value is empty or not a string so we never produce
|
|
32
|
+
* malformed descriptors like `tr()` that downstream parsers would reject.
|
|
33
|
+
*/
|
|
34
|
+
function normalizeToDescriptor(value) {
|
|
35
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
36
|
+
throw new Error("normalizeToDescriptor: expected a non-empty string value");
|
|
37
|
+
}
|
|
38
|
+
if (isDescriptor(value)) {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
return `tr(${value})`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Extract the public key from a simple descriptor.
|
|
45
|
+
* For simple descriptors (tr(pubkey)), extracts the pubkey using the library.
|
|
46
|
+
* For HD descriptors, throws — use DescriptorProvider to derive the key.
|
|
47
|
+
*/
|
|
48
|
+
function extractPubKey(descriptor) {
|
|
49
|
+
if (!isDescriptor(descriptor)) {
|
|
50
|
+
return descriptor;
|
|
51
|
+
}
|
|
52
|
+
const network = inferNetwork(descriptor);
|
|
53
|
+
const expansion = (0, descriptors_scure_1.expand)({ descriptor, network });
|
|
54
|
+
if (!expansion.expansionMap) {
|
|
55
|
+
throw new Error("Cannot extract pubkey from descriptor: expansion failed.");
|
|
56
|
+
}
|
|
57
|
+
const key = expansion.expansionMap["@0"];
|
|
58
|
+
// HD descriptors (have a bip32 key) require DescriptorProvider for derivation
|
|
59
|
+
if (key?.bip32) {
|
|
60
|
+
throw new Error("Cannot extract pubkey from HD descriptor without derivation. " +
|
|
61
|
+
"Use DescriptorProvider to derive the key from the xpub.");
|
|
62
|
+
}
|
|
63
|
+
if (!key?.pubkey) {
|
|
64
|
+
throw new Error("Cannot extract pubkey from descriptor: no key found.");
|
|
65
|
+
}
|
|
66
|
+
return base_1.hex.encode(key.pubkey);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Parse an HD descriptor into its components.
|
|
70
|
+
* HD descriptors have the format: tr([fingerprint/path']xpub/derivation)
|
|
71
|
+
* Returns null if the descriptor is not in HD format.
|
|
72
|
+
*/
|
|
73
|
+
function parseHDDescriptor(descriptor) {
|
|
74
|
+
if (!isDescriptor(descriptor)) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
let expansion;
|
|
78
|
+
try {
|
|
79
|
+
const network = inferNetwork(descriptor);
|
|
80
|
+
expansion = (0, descriptors_scure_1.expand)({ descriptor, network });
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const key = expansion.expansionMap?.["@0"];
|
|
86
|
+
if (!key?.masterFingerprint ||
|
|
87
|
+
!key.originPath ||
|
|
88
|
+
!key.keyPath ||
|
|
89
|
+
!key.bip32) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
fingerprint: base_1.hex.encode(key.masterFingerprint),
|
|
94
|
+
basePath: key.originPath.replace(/^\//, ""),
|
|
95
|
+
xpub: key.bip32.toBase58(),
|
|
96
|
+
derivationPath: key.keyPath.replace(/^\//, ""),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -14,6 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.StaticDescriptorProvider = exports.parseHDDescriptor = exports.extractPubKey = exports.normalizeToDescriptor = exports.isDescriptor = exports.ReadonlyDescriptorIdentity = exports.MnemonicIdentity = exports.SeedIdentity = void 0;
|
|
17
18
|
exports.isBatchSignable = isBatchSignable;
|
|
18
19
|
/** Type guard for identities that support batch signing. */
|
|
19
20
|
function isBatchSignable(identity) {
|
|
@@ -21,4 +22,17 @@ function isBatchSignable(identity) {
|
|
|
21
22
|
typeof identity.signMultiple === "function");
|
|
22
23
|
}
|
|
23
24
|
__exportStar(require("./singleKey"), exports);
|
|
24
|
-
|
|
25
|
+
var seedIdentity_1 = require("./seedIdentity");
|
|
26
|
+
Object.defineProperty(exports, "SeedIdentity", { enumerable: true, get: function () { return seedIdentity_1.SeedIdentity; } });
|
|
27
|
+
Object.defineProperty(exports, "MnemonicIdentity", { enumerable: true, get: function () { return seedIdentity_1.MnemonicIdentity; } });
|
|
28
|
+
Object.defineProperty(exports, "ReadonlyDescriptorIdentity", { enumerable: true, get: function () { return seedIdentity_1.ReadonlyDescriptorIdentity; } });
|
|
29
|
+
__exportStar(require("./serialize"), exports);
|
|
30
|
+
// Descriptor utilities
|
|
31
|
+
var descriptor_1 = require("./descriptor");
|
|
32
|
+
Object.defineProperty(exports, "isDescriptor", { enumerable: true, get: function () { return descriptor_1.isDescriptor; } });
|
|
33
|
+
Object.defineProperty(exports, "normalizeToDescriptor", { enumerable: true, get: function () { return descriptor_1.normalizeToDescriptor; } });
|
|
34
|
+
Object.defineProperty(exports, "extractPubKey", { enumerable: true, get: function () { return descriptor_1.extractPubKey; } });
|
|
35
|
+
Object.defineProperty(exports, "parseHDDescriptor", { enumerable: true, get: function () { return descriptor_1.parseHDDescriptor; } });
|
|
36
|
+
// Static descriptor provider (wrapper for legacy Identity)
|
|
37
|
+
var staticDescriptorProvider_1 = require("./staticDescriptorProvider");
|
|
38
|
+
Object.defineProperty(exports, "StaticDescriptorProvider", { enumerable: true, get: function () { return staticDescriptorProvider_1.StaticDescriptorProvider; } });
|
|
@@ -1,14 +1,30 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ReadonlyDescriptorIdentity = exports.MnemonicIdentity = exports.SeedIdentity = void 0;
|
|
4
|
+
exports.serializeSeedOwnedSigningIdentity = serializeSeedOwnedSigningIdentity;
|
|
5
|
+
exports.serializeSeedOwnedReadonlyIdentity = serializeSeedOwnedReadonlyIdentity;
|
|
4
6
|
const bip39_1 = require("@scure/bip39");
|
|
5
7
|
const english_js_1 = require("@scure/bip39/wordlists/english.js");
|
|
6
8
|
const utils_js_1 = require("@scure/btc-signer/utils.js");
|
|
7
9
|
const btc_signer_1 = require("@scure/btc-signer");
|
|
10
|
+
const base_1 = require("@scure/base");
|
|
8
11
|
const signingSession_1 = require("../tree/signingSession");
|
|
9
12
|
const secp256k1_1 = require("@noble/secp256k1");
|
|
10
13
|
const descriptors_scure_1 = require("@bitcoinerlab/descriptors-scure");
|
|
11
14
|
const ALL_SIGHASH = Object.values(btc_signer_1.SigHash).filter((x) => typeof x === "number");
|
|
15
|
+
/**
|
|
16
|
+
* Secret-bearing state for seed-backed identities, held off the public
|
|
17
|
+
* instance surface. Accessed only by the SDK-internal serializer helpers
|
|
18
|
+
* below; application code cannot read these via ordinary field access.
|
|
19
|
+
*
|
|
20
|
+
* Using a module-private WeakMap (rather than `private` / `protected`)
|
|
21
|
+
* matters because TypeScript visibility is a compile-time boundary only:
|
|
22
|
+
* JavaScript consumers could still read public fields. A WeakMap removes
|
|
23
|
+
* that enumeration path entirely.
|
|
24
|
+
*/
|
|
25
|
+
const seedBytes = new WeakMap();
|
|
26
|
+
const mnemonicMeta = new WeakMap();
|
|
27
|
+
// ── Helpers ──────────────────────────────────────────────────────
|
|
12
28
|
/**
|
|
13
29
|
* Detects the network from a descriptor string by checking for tpub (testnet)
|
|
14
30
|
* vs xpub (mainnet) key prefix.
|
|
@@ -40,14 +56,14 @@ function buildDescriptor(seed, isMainnet) {
|
|
|
40
56
|
*
|
|
41
57
|
* This is the recommended identity type for most applications. It uses
|
|
42
58
|
* standard BIP86 (Taproot) derivation by default and stores an output
|
|
43
|
-
* descriptor for interoperability with other wallets.
|
|
44
|
-
* format is HD-ready, allowing future support for multiple addresses
|
|
45
|
-
* and change derivation.
|
|
59
|
+
* descriptor for interoperability with other wallets.
|
|
46
60
|
*
|
|
47
61
|
* Prefer this (or @see MnemonicIdentity) over `SingleKey` for new
|
|
48
62
|
* integrations — `SingleKey` exists for backward compatibility with
|
|
49
63
|
* raw nsec-style keys.
|
|
50
64
|
*
|
|
65
|
+
* For descriptor-based signing, wrap with {@link StaticDescriptorProvider}.
|
|
66
|
+
*
|
|
51
67
|
* @example
|
|
52
68
|
* ```typescript
|
|
53
69
|
* const seed = mnemonicToSeedSync(mnemonic);
|
|
@@ -67,7 +83,11 @@ class SeedIdentity {
|
|
|
67
83
|
if (seed.length !== 64) {
|
|
68
84
|
throw new Error("Seed must be 64 bytes");
|
|
69
85
|
}
|
|
70
|
-
|
|
86
|
+
// Defensive copy: `derivedKey` and `descriptor` are computed eagerly
|
|
87
|
+
// from the bytes we're about to stash, so a later mutation of the
|
|
88
|
+
// caller's buffer must not drift the serialized `seed` out of sync
|
|
89
|
+
// with the live identity state.
|
|
90
|
+
seedBytes.set(this, new Uint8Array(seed));
|
|
71
91
|
this.descriptor = descriptor;
|
|
72
92
|
const network = detectNetwork(descriptor);
|
|
73
93
|
// Parse and validate the descriptor using the library
|
|
@@ -173,8 +193,9 @@ exports.SeedIdentity = SeedIdentity;
|
|
|
173
193
|
* ```
|
|
174
194
|
*/
|
|
175
195
|
class MnemonicIdentity extends SeedIdentity {
|
|
176
|
-
constructor(seed, descriptor) {
|
|
196
|
+
constructor(seed, descriptor, mnemonic, passphrase) {
|
|
177
197
|
super(seed, descriptor);
|
|
198
|
+
mnemonicMeta.set(this, { mnemonic, passphrase });
|
|
178
199
|
}
|
|
179
200
|
/**
|
|
180
201
|
* Creates a MnemonicIdentity from a BIP39 mnemonic phrase.
|
|
@@ -194,7 +215,7 @@ class MnemonicIdentity extends SeedIdentity {
|
|
|
194
215
|
const descriptor = hasDescriptor(opts)
|
|
195
216
|
? opts.descriptor
|
|
196
217
|
: buildDescriptor(seed, opts.isMainnet ?? true);
|
|
197
|
-
return new MnemonicIdentity(seed, descriptor);
|
|
218
|
+
return new MnemonicIdentity(seed, descriptor, phrase, passphrase);
|
|
198
219
|
}
|
|
199
220
|
}
|
|
200
221
|
exports.MnemonicIdentity = MnemonicIdentity;
|
|
@@ -252,3 +273,67 @@ class ReadonlyDescriptorIdentity {
|
|
|
252
273
|
}
|
|
253
274
|
}
|
|
254
275
|
exports.ReadonlyDescriptorIdentity = ReadonlyDescriptorIdentity;
|
|
276
|
+
/**
|
|
277
|
+
* Serialize a seed-backed signing identity into a
|
|
278
|
+
* {@link SerializedSigningIdentity} envelope without exposing the
|
|
279
|
+
* underlying secret material on the public instance surface.
|
|
280
|
+
*
|
|
281
|
+
* Called by {@link serializeSigningIdentity}; application code should
|
|
282
|
+
* prefer that public dispatcher instead of calling this directly. This
|
|
283
|
+
* helper is deliberately kept out of the `src/identity` barrel so it is
|
|
284
|
+
* not part of the package's public export surface.
|
|
285
|
+
*
|
|
286
|
+
* Secret-surface trade-off: the resulting envelope carries master-seed
|
|
287
|
+
* material — the BIP39 mnemonic (+ optional passphrase) for
|
|
288
|
+
* `MnemonicIdentity` or the raw 64-byte seed for `SeedIdentity`. A party
|
|
289
|
+
* that reads this envelope can derive any key under the HD tree, not
|
|
290
|
+
* just the key currently in use. The pre-change `SingleKey` flow only
|
|
291
|
+
* shipped one derived private key and therefore had a smaller blast
|
|
292
|
+
* radius. This is an intentional design trade to preserve class and
|
|
293
|
+
* descriptor identity across the page / service-worker boundary; the
|
|
294
|
+
* page already holds the same material so that it can re-initialize a
|
|
295
|
+
* killed worker. Transport is same-origin `postMessage` only. See the
|
|
296
|
+
* threat-model note in `src/worker/browser/README.md`.
|
|
297
|
+
*
|
|
298
|
+
* @internal
|
|
299
|
+
*/
|
|
300
|
+
function serializeSeedOwnedSigningIdentity(identity) {
|
|
301
|
+
if (identity instanceof MnemonicIdentity) {
|
|
302
|
+
const meta = mnemonicMeta.get(identity);
|
|
303
|
+
if (!meta) {
|
|
304
|
+
throw new Error("MnemonicIdentity is missing internal secret state; was it constructed via MnemonicIdentity.fromMnemonic()?");
|
|
305
|
+
}
|
|
306
|
+
const envelope = {
|
|
307
|
+
type: "mnemonic",
|
|
308
|
+
mnemonic: meta.mnemonic,
|
|
309
|
+
descriptor: identity.descriptor,
|
|
310
|
+
};
|
|
311
|
+
if (meta.passphrase !== undefined) {
|
|
312
|
+
envelope.passphrase = meta.passphrase;
|
|
313
|
+
}
|
|
314
|
+
return envelope;
|
|
315
|
+
}
|
|
316
|
+
const seed = seedBytes.get(identity);
|
|
317
|
+
if (!seed) {
|
|
318
|
+
throw new Error("SeedIdentity is missing internal secret state; was it constructed via SeedIdentity.fromSeed() or the class constructor?");
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
type: "seed",
|
|
322
|
+
seed: base_1.hex.encode(seed),
|
|
323
|
+
descriptor: identity.descriptor,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Downgrade a seed-backed or descriptor-backed identity into a readonly
|
|
328
|
+
* descriptor envelope. Always produces a descriptor-only shape — secret
|
|
329
|
+
* material never crosses this path, even if the input is a signing
|
|
330
|
+
* identity.
|
|
331
|
+
*
|
|
332
|
+
* Deliberately kept out of the `src/identity` barrel; consumers should go
|
|
333
|
+
* through {@link serializeReadonlyIdentity}.
|
|
334
|
+
*
|
|
335
|
+
* @internal
|
|
336
|
+
*/
|
|
337
|
+
function serializeSeedOwnedReadonlyIdentity(identity) {
|
|
338
|
+
return { type: "readonly-descriptor", descriptor: identity.descriptor };
|
|
339
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isSigningSerialized = isSigningSerialized;
|
|
4
|
+
exports.serializeSigningIdentity = serializeSigningIdentity;
|
|
5
|
+
exports.serializeReadonlyIdentity = serializeReadonlyIdentity;
|
|
6
|
+
exports.hydrateIdentity = hydrateIdentity;
|
|
7
|
+
exports.normalizeSerializedIdentity = normalizeSerializedIdentity;
|
|
8
|
+
const base_1 = require("@scure/base");
|
|
9
|
+
const singleKey_1 = require("./singleKey");
|
|
10
|
+
const seedIdentity_1 = require("./seedIdentity");
|
|
11
|
+
/** Type guard — true for signing envelopes, false for readonly envelopes. */
|
|
12
|
+
function isSigningSerialized(s) {
|
|
13
|
+
return (s.type === "single-key" || s.type === "seed" || s.type === "mnemonic");
|
|
14
|
+
}
|
|
15
|
+
function hasToHex(identity) {
|
|
16
|
+
return typeof identity.toHex === "function";
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Serialize a signing identity into a structured-clone safe envelope for
|
|
20
|
+
* transport across the service-worker boundary.
|
|
21
|
+
*
|
|
22
|
+
* Supports SDK-owned signing identities directly. For custom identities, a
|
|
23
|
+
* duck-typed `toHex()` fallback preserves compatibility with existing
|
|
24
|
+
* `SingleKey`-like implementations.
|
|
25
|
+
*/
|
|
26
|
+
function serializeSigningIdentity(identity) {
|
|
27
|
+
// Seed-backed identities (including MnemonicIdentity, which extends
|
|
28
|
+
// SeedIdentity) delegate to the colocated helper so secret material
|
|
29
|
+
// stays behind the WeakMap-backed internal state in seedIdentity.ts.
|
|
30
|
+
if (identity instanceof seedIdentity_1.SeedIdentity) {
|
|
31
|
+
return (0, seedIdentity_1.serializeSeedOwnedSigningIdentity)(identity);
|
|
32
|
+
}
|
|
33
|
+
if (identity instanceof singleKey_1.SingleKey) {
|
|
34
|
+
return { type: "single-key", privateKey: identity.toHex() };
|
|
35
|
+
}
|
|
36
|
+
if (hasToHex(identity)) {
|
|
37
|
+
return { type: "single-key", privateKey: identity.toHex() };
|
|
38
|
+
}
|
|
39
|
+
throw new Error("Unsupported signing identity: cannot serialize for service-worker transport");
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Serialize a readonly identity into a structured-clone safe envelope.
|
|
43
|
+
*
|
|
44
|
+
* Works for any `ReadonlyIdentity` via `compressedPublicKey()`. When called
|
|
45
|
+
* with a signing identity, produces a readonly envelope (never ships signing
|
|
46
|
+
* material) — callers that need to preserve signing capability across the
|
|
47
|
+
* boundary must use {@link serializeSigningIdentity}.
|
|
48
|
+
*/
|
|
49
|
+
async function serializeReadonlyIdentity(identity) {
|
|
50
|
+
if (identity instanceof seedIdentity_1.SeedIdentity ||
|
|
51
|
+
identity instanceof seedIdentity_1.ReadonlyDescriptorIdentity) {
|
|
52
|
+
return (0, seedIdentity_1.serializeSeedOwnedReadonlyIdentity)(identity);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
type: "readonly-single-key",
|
|
56
|
+
publicKey: base_1.hex.encode(await identity.compressedPublicKey()),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Rehydrate a serialized identity envelope back into an identity instance.
|
|
61
|
+
* The return type is the union of signing and readonly; use
|
|
62
|
+
* {@link isSigningSerialized} on the envelope before hydration if the caller
|
|
63
|
+
* needs to know which side it ends up on.
|
|
64
|
+
*/
|
|
65
|
+
function hydrateIdentity(s) {
|
|
66
|
+
switch (s.type) {
|
|
67
|
+
case "single-key":
|
|
68
|
+
return singleKey_1.SingleKey.fromHex(s.privateKey);
|
|
69
|
+
case "readonly-single-key":
|
|
70
|
+
return singleKey_1.ReadonlySingleKey.fromPublicKey(base_1.hex.decode(s.publicKey));
|
|
71
|
+
case "seed":
|
|
72
|
+
return seedIdentity_1.SeedIdentity.fromSeed(base_1.hex.decode(s.seed), {
|
|
73
|
+
descriptor: s.descriptor,
|
|
74
|
+
});
|
|
75
|
+
case "mnemonic":
|
|
76
|
+
return seedIdentity_1.MnemonicIdentity.fromMnemonic(s.mnemonic, {
|
|
77
|
+
descriptor: s.descriptor,
|
|
78
|
+
passphrase: s.passphrase,
|
|
79
|
+
});
|
|
80
|
+
case "readonly-descriptor":
|
|
81
|
+
return seedIdentity_1.ReadonlyDescriptorIdentity.fromDescriptor(s.descriptor);
|
|
82
|
+
default:
|
|
83
|
+
// Belt-and-suspenders: `normalizeSerializedIdentity` already
|
|
84
|
+
// rejects unknown `type` values at the wire boundary. Without
|
|
85
|
+
// this throw, an unknown type would fall through and return
|
|
86
|
+
// undefined, which callers would then cast to Identity and
|
|
87
|
+
// crash downstream with an opaque error.
|
|
88
|
+
throw new Error(`Unknown serialized identity type: ${String(s.type)}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
let warnedLegacyShape = false;
|
|
92
|
+
/**
|
|
93
|
+
* Accept either a modern {@link SerializedIdentity} envelope or a legacy
|
|
94
|
+
* `{ privateKey }` / `{ publicKey }` shape and normalize to a
|
|
95
|
+
* {@link SerializedIdentity}. Emits a one-time deprecation warning when a
|
|
96
|
+
* legacy shape is seen.
|
|
97
|
+
*
|
|
98
|
+
* Intended for the worker-side boundary; new page builds always emit tagged
|
|
99
|
+
* envelopes via {@link serializeSigningIdentity} /
|
|
100
|
+
* {@link serializeReadonlyIdentity}.
|
|
101
|
+
*/
|
|
102
|
+
function normalizeSerializedIdentity(shape) {
|
|
103
|
+
if ("type" in shape) {
|
|
104
|
+
assertValidSerializedIdentity(shape);
|
|
105
|
+
return shape;
|
|
106
|
+
}
|
|
107
|
+
if (!warnedLegacyShape) {
|
|
108
|
+
warnedLegacyShape = true;
|
|
109
|
+
console.warn("[ts-sdk] Received legacy serialized identity shape " +
|
|
110
|
+
"(privateKey/publicKey). Upgrade the page build to the latest " +
|
|
111
|
+
"@arkade-os/sdk — this compatibility path will be removed in " +
|
|
112
|
+
"the next major.");
|
|
113
|
+
}
|
|
114
|
+
if ("privateKey" in shape && typeof shape.privateKey === "string") {
|
|
115
|
+
return { type: "single-key", privateKey: shape.privateKey };
|
|
116
|
+
}
|
|
117
|
+
if ("publicKey" in shape && typeof shape.publicKey === "string") {
|
|
118
|
+
return { type: "readonly-single-key", publicKey: shape.publicKey };
|
|
119
|
+
}
|
|
120
|
+
throw new Error("Unrecognized serialized identity shape");
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Runtime-validate that a tagged envelope carries the fields its variant
|
|
124
|
+
* requires. The SDK's own serializer produces well-formed envelopes; this
|
|
125
|
+
* guard exists so a malformed message (older SDK version mismatch,
|
|
126
|
+
* hand-built config, etc.) fails loudly at the wire boundary rather than
|
|
127
|
+
* with an opaque `"Cannot read properties of undefined"` deep inside a
|
|
128
|
+
* hydrator.
|
|
129
|
+
*/
|
|
130
|
+
function assertValidSerializedIdentity(s) {
|
|
131
|
+
const kind = s.type;
|
|
132
|
+
const bad = (field, expected) => {
|
|
133
|
+
throw new Error(`Malformed serialized identity ({ type: ${JSON.stringify(kind)} }): ` +
|
|
134
|
+
`missing or invalid "${field}" (expected ${expected})`);
|
|
135
|
+
};
|
|
136
|
+
const asStr = (key) => {
|
|
137
|
+
const v = s[key];
|
|
138
|
+
return typeof v === "string" ? v : bad(key, "string");
|
|
139
|
+
};
|
|
140
|
+
switch (kind) {
|
|
141
|
+
case "single-key":
|
|
142
|
+
asStr("privateKey");
|
|
143
|
+
return;
|
|
144
|
+
case "readonly-single-key":
|
|
145
|
+
asStr("publicKey");
|
|
146
|
+
return;
|
|
147
|
+
case "seed":
|
|
148
|
+
asStr("seed");
|
|
149
|
+
asStr("descriptor");
|
|
150
|
+
return;
|
|
151
|
+
case "mnemonic": {
|
|
152
|
+
asStr("mnemonic");
|
|
153
|
+
asStr("descriptor");
|
|
154
|
+
const passphrase = s.passphrase;
|
|
155
|
+
if (passphrase !== undefined && typeof passphrase !== "string") {
|
|
156
|
+
bad("passphrase", "string | undefined");
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
case "readonly-descriptor":
|
|
161
|
+
asStr("descriptor");
|
|
162
|
+
return;
|
|
163
|
+
default:
|
|
164
|
+
throw new Error(`Unknown serialized identity type: ${String(kind)}`);
|
|
165
|
+
}
|
|
166
|
+
}
|