@arkade-os/sdk 0.4.18 → 0.4.20

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 (60) hide show
  1. package/dist/cjs/contracts/contractWatcher.js +7 -1
  2. package/dist/cjs/contracts/handlers/default.js +10 -3
  3. package/dist/cjs/contracts/handlers/helpers.js +47 -5
  4. package/dist/cjs/contracts/handlers/vhtlc.js +4 -2
  5. package/dist/cjs/identity/descriptor.js +98 -0
  6. package/dist/cjs/identity/descriptorProvider.js +2 -0
  7. package/dist/cjs/identity/index.js +15 -1
  8. package/dist/cjs/identity/seedIdentity.js +91 -6
  9. package/dist/cjs/identity/serialize.js +166 -0
  10. package/dist/cjs/identity/staticDescriptorProvider.js +65 -0
  11. package/dist/cjs/index.js +6 -3
  12. package/dist/cjs/providers/ark.js +45 -34
  13. package/dist/cjs/providers/electrum.js +663 -0
  14. package/dist/cjs/providers/indexer.js +5 -1
  15. package/dist/cjs/providers/utils.js +4 -0
  16. package/dist/cjs/wallet/ramps.js +1 -1
  17. package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +10 -0
  18. package/dist/cjs/wallet/serviceWorker/wallet.js +137 -91
  19. package/dist/cjs/wallet/vtxo-manager.js +133 -17
  20. package/dist/cjs/wallet/wallet.js +80 -19
  21. package/dist/cjs/worker/messageBus.js +200 -56
  22. package/dist/esm/contracts/contractWatcher.js +7 -1
  23. package/dist/esm/contracts/handlers/default.js +10 -3
  24. package/dist/esm/contracts/handlers/helpers.js +47 -5
  25. package/dist/esm/contracts/handlers/vhtlc.js +4 -2
  26. package/dist/esm/identity/descriptor.js +92 -0
  27. package/dist/esm/identity/descriptorProvider.js +1 -0
  28. package/dist/esm/identity/index.js +6 -1
  29. package/dist/esm/identity/seedIdentity.js +89 -6
  30. package/dist/esm/identity/serialize.js +159 -0
  31. package/dist/esm/identity/staticDescriptorProvider.js +61 -0
  32. package/dist/esm/index.js +2 -1
  33. package/dist/esm/providers/ark.js +46 -35
  34. package/dist/esm/providers/electrum.js +658 -0
  35. package/dist/esm/providers/indexer.js +6 -2
  36. package/dist/esm/providers/utils.js +3 -0
  37. package/dist/esm/wallet/ramps.js +1 -1
  38. package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +10 -0
  39. package/dist/esm/wallet/serviceWorker/wallet.js +137 -91
  40. package/dist/esm/wallet/vtxo-manager.js +133 -17
  41. package/dist/esm/wallet/wallet.js +80 -19
  42. package/dist/esm/worker/messageBus.js +201 -57
  43. package/dist/types/contracts/handlers/default.d.ts +1 -1
  44. package/dist/types/contracts/handlers/helpers.d.ts +1 -1
  45. package/dist/types/contracts/types.d.ts +11 -3
  46. package/dist/types/identity/descriptor.d.ts +35 -0
  47. package/dist/types/identity/descriptorProvider.d.ts +28 -0
  48. package/dist/types/identity/index.d.ts +7 -1
  49. package/dist/types/identity/seedIdentity.d.ts +41 -4
  50. package/dist/types/identity/serialize.d.ts +84 -0
  51. package/dist/types/identity/staticDescriptorProvider.d.ts +18 -0
  52. package/dist/types/index.d.ts +4 -2
  53. package/dist/types/providers/electrum.d.ts +212 -0
  54. package/dist/types/providers/utils.d.ts +1 -0
  55. package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +11 -2
  56. package/dist/types/wallet/serviceWorker/wallet.d.ts +27 -10
  57. package/dist/types/wallet/vtxo-manager.d.ts +15 -0
  58. package/dist/types/wallet/wallet.d.ts +3 -0
  59. package/dist/types/worker/messageBus.d.ts +68 -8
  60. package/package.json +3 -2
@@ -1,4 +1,21 @@
1
1
  import * as bip68 from "bip68";
2
+ import { isDescriptor, extractPubKey } from '../../identity/descriptor.js';
3
+ /**
4
+ * Extract raw hex pubkey from a value that may be a descriptor or raw hex.
5
+ * Returns undefined for HD descriptors or unparseable values so role
6
+ * resolution stays best-effort and never throws.
7
+ */
8
+ function extractRawPubKey(value) {
9
+ if (!isDescriptor(value)) {
10
+ return value.toLowerCase();
11
+ }
12
+ try {
13
+ return extractPubKey(value).toLowerCase();
14
+ }
15
+ catch {
16
+ return undefined;
17
+ }
18
+ }
2
19
  /**
3
20
  * Convert RelativeTimelock to BIP68 sequence number.
4
21
  */
@@ -21,21 +38,46 @@ export function sequenceToTimelock(sequence) {
21
38
  throw new Error(`Invalid BIP68 sequence: ${sequence}`);
22
39
  }
23
40
  /**
24
- * Resolve wallet's role from explicit role or by matching pubkey.
41
+ * Resolve wallet's role from explicit role or by matching descriptor/pubkey.
25
42
  */
26
43
  export function resolveRole(contract, context) {
27
44
  // Explicit role takes precedence
28
45
  if (context.role === "sender" || context.role === "receiver") {
29
46
  return context.role;
30
47
  }
31
- // Try to match wallet pubkey against contract params
32
- if (context.walletPubKey) {
33
- if (context.walletPubKey === contract.params.sender) {
48
+ const senderKey = contract.params.sender
49
+ ? extractRawPubKey(contract.params.sender)
50
+ : undefined;
51
+ const receiverKey = contract.params.receiver
52
+ ? extractRawPubKey(contract.params.receiver)
53
+ : undefined;
54
+ const matchRole = (rawWalletKey) => {
55
+ if (!rawWalletKey)
56
+ return undefined;
57
+ if (senderKey && rawWalletKey === senderKey) {
34
58
  return "sender";
35
59
  }
36
- if (context.walletPubKey === contract.params.receiver) {
60
+ if (receiverKey && rawWalletKey === receiverKey) {
37
61
  return "receiver";
38
62
  }
63
+ return undefined;
64
+ };
65
+ // Try the preferred descriptor first. If it cannot be resolved
66
+ // (for example an HD descriptor without derivation support), fall back
67
+ // to walletPubKey for backward compatibility.
68
+ if (context.walletDescriptor) {
69
+ const walletDescriptorKey = extractRawPubKey(context.walletDescriptor);
70
+ const matchedRole = matchRole(walletDescriptorKey);
71
+ if (matchedRole) {
72
+ return matchedRole;
73
+ }
74
+ if (!walletDescriptorKey && context.walletPubKey) {
75
+ return matchRole(extractRawPubKey(context.walletPubKey));
76
+ }
77
+ return undefined;
78
+ }
79
+ if (context.walletPubKey) {
80
+ return matchRole(extractRawPubKey(context.walletPubKey));
39
81
  }
40
82
  return undefined;
41
83
  }
@@ -49,7 +49,8 @@ export const VHTLCContractHandler = {
49
49
  /**
50
50
  * Select spending path based on context.
51
51
  *
52
- * Role is determined from `context.role` or by matching `context.walletPubKey`
52
+ * Role is determined from `context.role` or by matching
53
+ * `context.walletDescriptor` (preferred) / `context.walletPubKey`
53
54
  * against sender/receiver in contract params.
54
55
  */
55
56
  selectPath(script, contract, context) {
@@ -98,7 +99,8 @@ export const VHTLCContractHandler = {
98
99
  /**
99
100
  * Get all possible spending paths (no timelock checks).
100
101
  *
101
- * Role is determined from `context.role` or by matching `context.walletPubKey`
102
+ * Role is determined from `context.role` or by matching
103
+ * `context.walletDescriptor` (preferred) / `context.walletPubKey`
102
104
  * against sender/receiver in contract params.
103
105
  */
104
106
  getAllSpendingPaths(script, contract, context) {
@@ -0,0 +1,92 @@
1
+ import { expand, networks, } from "@bitcoinerlab/descriptors-scure";
2
+ import { hex } from "@scure/base";
3
+ function inferNetwork(descriptor) {
4
+ return descriptor.includes("tpub") ? networks.testnet : networks.bitcoin;
5
+ }
6
+ /**
7
+ * Check if a string is a descriptor of the shape `tr(...)`.
8
+ *
9
+ * This is a shape check only — it does not validate the inner key material.
10
+ * Use {@link expand} (via {@link extractPubKey} / {@link parseHDDescriptor})
11
+ * for full parsing. The guard rejects empty bodies and missing/trailing
12
+ * parentheses so callers can safely branch on descriptor vs. raw pubkey.
13
+ */
14
+ export function isDescriptor(value) {
15
+ if (typeof value !== "string")
16
+ return false;
17
+ if (!value.startsWith("tr(") || !value.endsWith(")"))
18
+ return false;
19
+ // body length > 0 after stripping "tr(" and ")"
20
+ return value.length > "tr()".length;
21
+ }
22
+ /**
23
+ * Normalize a value to descriptor format.
24
+ * If already a descriptor, return as-is. If hex pubkey, wrap as tr(pubkey).
25
+ * Throws when the value is empty or not a string so we never produce
26
+ * malformed descriptors like `tr()` that downstream parsers would reject.
27
+ */
28
+ export function normalizeToDescriptor(value) {
29
+ if (typeof value !== "string" || value.length === 0) {
30
+ throw new Error("normalizeToDescriptor: expected a non-empty string value");
31
+ }
32
+ if (isDescriptor(value)) {
33
+ return value;
34
+ }
35
+ return `tr(${value})`;
36
+ }
37
+ /**
38
+ * Extract the public key from a simple descriptor.
39
+ * For simple descriptors (tr(pubkey)), extracts the pubkey using the library.
40
+ * For HD descriptors, throws — use DescriptorProvider to derive the key.
41
+ */
42
+ export function extractPubKey(descriptor) {
43
+ if (!isDescriptor(descriptor)) {
44
+ return descriptor;
45
+ }
46
+ const network = inferNetwork(descriptor);
47
+ const expansion = expand({ descriptor, network });
48
+ if (!expansion.expansionMap) {
49
+ throw new Error("Cannot extract pubkey from descriptor: expansion failed.");
50
+ }
51
+ const key = expansion.expansionMap["@0"];
52
+ // HD descriptors (have a bip32 key) require DescriptorProvider for derivation
53
+ if (key?.bip32) {
54
+ throw new Error("Cannot extract pubkey from HD descriptor without derivation. " +
55
+ "Use DescriptorProvider to derive the key from the xpub.");
56
+ }
57
+ if (!key?.pubkey) {
58
+ throw new Error("Cannot extract pubkey from descriptor: no key found.");
59
+ }
60
+ return hex.encode(key.pubkey);
61
+ }
62
+ /**
63
+ * Parse an HD descriptor into its components.
64
+ * HD descriptors have the format: tr([fingerprint/path']xpub/derivation)
65
+ * Returns null if the descriptor is not in HD format.
66
+ */
67
+ export function parseHDDescriptor(descriptor) {
68
+ if (!isDescriptor(descriptor)) {
69
+ return null;
70
+ }
71
+ let expansion;
72
+ try {
73
+ const network = inferNetwork(descriptor);
74
+ expansion = expand({ descriptor, network });
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ const key = expansion.expansionMap?.["@0"];
80
+ if (!key?.masterFingerprint ||
81
+ !key.originPath ||
82
+ !key.keyPath ||
83
+ !key.bip32) {
84
+ return null;
85
+ }
86
+ return {
87
+ fingerprint: hex.encode(key.masterFingerprint),
88
+ basePath: key.originPath.replace(/^\//, ""),
89
+ xpub: key.bip32.toBase58(),
90
+ derivationPath: key.keyPath.replace(/^\//, ""),
91
+ };
92
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -4,4 +4,9 @@ export function isBatchSignable(identity) {
4
4
  typeof identity.signMultiple === "function");
5
5
  }
6
6
  export * from './singleKey.js';
7
- export * from './seedIdentity.js';
7
+ export { SeedIdentity, MnemonicIdentity, ReadonlyDescriptorIdentity, } from './seedIdentity.js';
8
+ export * from './serialize.js';
9
+ // Descriptor utilities
10
+ export { isDescriptor, normalizeToDescriptor, extractPubKey, parseHDDescriptor, } from './descriptor.js';
11
+ // Static descriptor provider (wrapper for legacy Identity)
12
+ export { StaticDescriptorProvider } from './staticDescriptorProvider.js';
@@ -2,10 +2,24 @@ import { validateMnemonic, mnemonicToSeedSync } from "@scure/bip39";
2
2
  import { wordlist } from "@scure/bip39/wordlists/english.js";
3
3
  import { pubECDSA, pubSchnorr } from "@scure/btc-signer/utils.js";
4
4
  import { SigHash } from "@scure/btc-signer";
5
+ import { hex } from "@scure/base";
5
6
  import { TreeSignerSession } from '../tree/signingSession.js';
6
7
  import { schnorr, signAsync } from "@noble/secp256k1";
7
8
  import { HDKey, expand, networks, scriptExpressions, } from "@bitcoinerlab/descriptors-scure";
8
9
  const ALL_SIGHASH = Object.values(SigHash).filter((x) => typeof x === "number");
10
+ /**
11
+ * Secret-bearing state for seed-backed identities, held off the public
12
+ * instance surface. Accessed only by the SDK-internal serializer helpers
13
+ * below; application code cannot read these via ordinary field access.
14
+ *
15
+ * Using a module-private WeakMap (rather than `private` / `protected`)
16
+ * matters because TypeScript visibility is a compile-time boundary only:
17
+ * JavaScript consumers could still read public fields. A WeakMap removes
18
+ * that enumeration path entirely.
19
+ */
20
+ const seedBytes = new WeakMap();
21
+ const mnemonicMeta = new WeakMap();
22
+ // ── Helpers ──────────────────────────────────────────────────────
9
23
  /**
10
24
  * Detects the network from a descriptor string by checking for tpub (testnet)
11
25
  * vs xpub (mainnet) key prefix.
@@ -37,14 +51,14 @@ function buildDescriptor(seed, isMainnet) {
37
51
  *
38
52
  * This is the recommended identity type for most applications. It uses
39
53
  * standard BIP86 (Taproot) derivation by default and stores an output
40
- * descriptor for interoperability with other wallets. The descriptor
41
- * format is HD-ready, allowing future support for multiple addresses
42
- * and change derivation.
54
+ * descriptor for interoperability with other wallets.
43
55
  *
44
56
  * Prefer this (or @see MnemonicIdentity) over `SingleKey` for new
45
57
  * integrations — `SingleKey` exists for backward compatibility with
46
58
  * raw nsec-style keys.
47
59
  *
60
+ * For descriptor-based signing, wrap with {@link StaticDescriptorProvider}.
61
+ *
48
62
  * @example
49
63
  * ```typescript
50
64
  * const seed = mnemonicToSeedSync(mnemonic);
@@ -64,7 +78,11 @@ export class SeedIdentity {
64
78
  if (seed.length !== 64) {
65
79
  throw new Error("Seed must be 64 bytes");
66
80
  }
67
- this.seed = seed;
81
+ // Defensive copy: `derivedKey` and `descriptor` are computed eagerly
82
+ // from the bytes we're about to stash, so a later mutation of the
83
+ // caller's buffer must not drift the serialized `seed` out of sync
84
+ // with the live identity state.
85
+ seedBytes.set(this, new Uint8Array(seed));
68
86
  this.descriptor = descriptor;
69
87
  const network = detectNetwork(descriptor);
70
88
  // Parse and validate the descriptor using the library
@@ -169,8 +187,9 @@ export class SeedIdentity {
169
187
  * ```
170
188
  */
171
189
  export class MnemonicIdentity extends SeedIdentity {
172
- constructor(seed, descriptor) {
190
+ constructor(seed, descriptor, mnemonic, passphrase) {
173
191
  super(seed, descriptor);
192
+ mnemonicMeta.set(this, { mnemonic, passphrase });
174
193
  }
175
194
  /**
176
195
  * Creates a MnemonicIdentity from a BIP39 mnemonic phrase.
@@ -190,7 +209,7 @@ export class MnemonicIdentity extends SeedIdentity {
190
209
  const descriptor = hasDescriptor(opts)
191
210
  ? opts.descriptor
192
211
  : buildDescriptor(seed, opts.isMainnet ?? true);
193
- return new MnemonicIdentity(seed, descriptor);
212
+ return new MnemonicIdentity(seed, descriptor, phrase, passphrase);
194
213
  }
195
214
  }
196
215
  /**
@@ -246,3 +265,67 @@ export class ReadonlyDescriptorIdentity {
246
265
  return this.compressedPubKey;
247
266
  }
248
267
  }
268
+ /**
269
+ * Serialize a seed-backed signing identity into a
270
+ * {@link SerializedSigningIdentity} envelope without exposing the
271
+ * underlying secret material on the public instance surface.
272
+ *
273
+ * Called by {@link serializeSigningIdentity}; application code should
274
+ * prefer that public dispatcher instead of calling this directly. This
275
+ * helper is deliberately kept out of the `src/identity` barrel so it is
276
+ * not part of the package's public export surface.
277
+ *
278
+ * Secret-surface trade-off: the resulting envelope carries master-seed
279
+ * material — the BIP39 mnemonic (+ optional passphrase) for
280
+ * `MnemonicIdentity` or the raw 64-byte seed for `SeedIdentity`. A party
281
+ * that reads this envelope can derive any key under the HD tree, not
282
+ * just the key currently in use. The pre-change `SingleKey` flow only
283
+ * shipped one derived private key and therefore had a smaller blast
284
+ * radius. This is an intentional design trade to preserve class and
285
+ * descriptor identity across the page / service-worker boundary; the
286
+ * page already holds the same material so that it can re-initialize a
287
+ * killed worker. Transport is same-origin `postMessage` only. See the
288
+ * threat-model note in `src/worker/browser/README.md`.
289
+ *
290
+ * @internal
291
+ */
292
+ export function serializeSeedOwnedSigningIdentity(identity) {
293
+ if (identity instanceof MnemonicIdentity) {
294
+ const meta = mnemonicMeta.get(identity);
295
+ if (!meta) {
296
+ throw new Error("MnemonicIdentity is missing internal secret state; was it constructed via MnemonicIdentity.fromMnemonic()?");
297
+ }
298
+ const envelope = {
299
+ type: "mnemonic",
300
+ mnemonic: meta.mnemonic,
301
+ descriptor: identity.descriptor,
302
+ };
303
+ if (meta.passphrase !== undefined) {
304
+ envelope.passphrase = meta.passphrase;
305
+ }
306
+ return envelope;
307
+ }
308
+ const seed = seedBytes.get(identity);
309
+ if (!seed) {
310
+ throw new Error("SeedIdentity is missing internal secret state; was it constructed via SeedIdentity.fromSeed() or the class constructor?");
311
+ }
312
+ return {
313
+ type: "seed",
314
+ seed: hex.encode(seed),
315
+ descriptor: identity.descriptor,
316
+ };
317
+ }
318
+ /**
319
+ * Downgrade a seed-backed or descriptor-backed identity into a readonly
320
+ * descriptor envelope. Always produces a descriptor-only shape — secret
321
+ * material never crosses this path, even if the input is a signing
322
+ * identity.
323
+ *
324
+ * Deliberately kept out of the `src/identity` barrel; consumers should go
325
+ * through {@link serializeReadonlyIdentity}.
326
+ *
327
+ * @internal
328
+ */
329
+ export function serializeSeedOwnedReadonlyIdentity(identity) {
330
+ return { type: "readonly-descriptor", descriptor: identity.descriptor };
331
+ }
@@ -0,0 +1,159 @@
1
+ import { hex } from "@scure/base";
2
+ import { SingleKey, ReadonlySingleKey } from './singleKey.js';
3
+ import { SeedIdentity, MnemonicIdentity, ReadonlyDescriptorIdentity, serializeSeedOwnedSigningIdentity, serializeSeedOwnedReadonlyIdentity, } from './seedIdentity.js';
4
+ /** Type guard — true for signing envelopes, false for readonly envelopes. */
5
+ export function isSigningSerialized(s) {
6
+ return (s.type === "single-key" || s.type === "seed" || s.type === "mnemonic");
7
+ }
8
+ function hasToHex(identity) {
9
+ return typeof identity.toHex === "function";
10
+ }
11
+ /**
12
+ * Serialize a signing identity into a structured-clone safe envelope for
13
+ * transport across the service-worker boundary.
14
+ *
15
+ * Supports SDK-owned signing identities directly. For custom identities, a
16
+ * duck-typed `toHex()` fallback preserves compatibility with existing
17
+ * `SingleKey`-like implementations.
18
+ */
19
+ export function serializeSigningIdentity(identity) {
20
+ // Seed-backed identities (including MnemonicIdentity, which extends
21
+ // SeedIdentity) delegate to the colocated helper so secret material
22
+ // stays behind the WeakMap-backed internal state in seedIdentity.ts.
23
+ if (identity instanceof SeedIdentity) {
24
+ return serializeSeedOwnedSigningIdentity(identity);
25
+ }
26
+ if (identity instanceof SingleKey) {
27
+ return { type: "single-key", privateKey: identity.toHex() };
28
+ }
29
+ if (hasToHex(identity)) {
30
+ return { type: "single-key", privateKey: identity.toHex() };
31
+ }
32
+ throw new Error("Unsupported signing identity: cannot serialize for service-worker transport");
33
+ }
34
+ /**
35
+ * Serialize a readonly identity into a structured-clone safe envelope.
36
+ *
37
+ * Works for any `ReadonlyIdentity` via `compressedPublicKey()`. When called
38
+ * with a signing identity, produces a readonly envelope (never ships signing
39
+ * material) — callers that need to preserve signing capability across the
40
+ * boundary must use {@link serializeSigningIdentity}.
41
+ */
42
+ export async function serializeReadonlyIdentity(identity) {
43
+ if (identity instanceof SeedIdentity ||
44
+ identity instanceof ReadonlyDescriptorIdentity) {
45
+ return serializeSeedOwnedReadonlyIdentity(identity);
46
+ }
47
+ return {
48
+ type: "readonly-single-key",
49
+ publicKey: hex.encode(await identity.compressedPublicKey()),
50
+ };
51
+ }
52
+ /**
53
+ * Rehydrate a serialized identity envelope back into an identity instance.
54
+ * The return type is the union of signing and readonly; use
55
+ * {@link isSigningSerialized} on the envelope before hydration if the caller
56
+ * needs to know which side it ends up on.
57
+ */
58
+ export function hydrateIdentity(s) {
59
+ switch (s.type) {
60
+ case "single-key":
61
+ return SingleKey.fromHex(s.privateKey);
62
+ case "readonly-single-key":
63
+ return ReadonlySingleKey.fromPublicKey(hex.decode(s.publicKey));
64
+ case "seed":
65
+ return SeedIdentity.fromSeed(hex.decode(s.seed), {
66
+ descriptor: s.descriptor,
67
+ });
68
+ case "mnemonic":
69
+ return MnemonicIdentity.fromMnemonic(s.mnemonic, {
70
+ descriptor: s.descriptor,
71
+ passphrase: s.passphrase,
72
+ });
73
+ case "readonly-descriptor":
74
+ return ReadonlyDescriptorIdentity.fromDescriptor(s.descriptor);
75
+ default:
76
+ // Belt-and-suspenders: `normalizeSerializedIdentity` already
77
+ // rejects unknown `type` values at the wire boundary. Without
78
+ // this throw, an unknown type would fall through and return
79
+ // undefined, which callers would then cast to Identity and
80
+ // crash downstream with an opaque error.
81
+ throw new Error(`Unknown serialized identity type: ${String(s.type)}`);
82
+ }
83
+ }
84
+ let warnedLegacyShape = false;
85
+ /**
86
+ * Accept either a modern {@link SerializedIdentity} envelope or a legacy
87
+ * `{ privateKey }` / `{ publicKey }` shape and normalize to a
88
+ * {@link SerializedIdentity}. Emits a one-time deprecation warning when a
89
+ * legacy shape is seen.
90
+ *
91
+ * Intended for the worker-side boundary; new page builds always emit tagged
92
+ * envelopes via {@link serializeSigningIdentity} /
93
+ * {@link serializeReadonlyIdentity}.
94
+ */
95
+ export function normalizeSerializedIdentity(shape) {
96
+ if ("type" in shape) {
97
+ assertValidSerializedIdentity(shape);
98
+ return shape;
99
+ }
100
+ if (!warnedLegacyShape) {
101
+ warnedLegacyShape = true;
102
+ console.warn("[ts-sdk] Received legacy serialized identity shape " +
103
+ "(privateKey/publicKey). Upgrade the page build to the latest " +
104
+ "@arkade-os/sdk — this compatibility path will be removed in " +
105
+ "the next major.");
106
+ }
107
+ if ("privateKey" in shape && typeof shape.privateKey === "string") {
108
+ return { type: "single-key", privateKey: shape.privateKey };
109
+ }
110
+ if ("publicKey" in shape && typeof shape.publicKey === "string") {
111
+ return { type: "readonly-single-key", publicKey: shape.publicKey };
112
+ }
113
+ throw new Error("Unrecognized serialized identity shape");
114
+ }
115
+ /**
116
+ * Runtime-validate that a tagged envelope carries the fields its variant
117
+ * requires. The SDK's own serializer produces well-formed envelopes; this
118
+ * guard exists so a malformed message (older SDK version mismatch,
119
+ * hand-built config, etc.) fails loudly at the wire boundary rather than
120
+ * with an opaque `"Cannot read properties of undefined"` deep inside a
121
+ * hydrator.
122
+ */
123
+ function assertValidSerializedIdentity(s) {
124
+ const kind = s.type;
125
+ const bad = (field, expected) => {
126
+ throw new Error(`Malformed serialized identity ({ type: ${JSON.stringify(kind)} }): ` +
127
+ `missing or invalid "${field}" (expected ${expected})`);
128
+ };
129
+ const asStr = (key) => {
130
+ const v = s[key];
131
+ return typeof v === "string" ? v : bad(key, "string");
132
+ };
133
+ switch (kind) {
134
+ case "single-key":
135
+ asStr("privateKey");
136
+ return;
137
+ case "readonly-single-key":
138
+ asStr("publicKey");
139
+ return;
140
+ case "seed":
141
+ asStr("seed");
142
+ asStr("descriptor");
143
+ return;
144
+ case "mnemonic": {
145
+ asStr("mnemonic");
146
+ asStr("descriptor");
147
+ const passphrase = s.passphrase;
148
+ if (passphrase !== undefined && typeof passphrase !== "string") {
149
+ bad("passphrase", "string | undefined");
150
+ }
151
+ return;
152
+ }
153
+ case "readonly-descriptor":
154
+ asStr("descriptor");
155
+ return;
156
+ default:
157
+ throw new Error(`Unknown serialized identity type: ${String(kind)}`);
158
+ }
159
+ }
@@ -0,0 +1,61 @@
1
+ import { hex } from "@scure/base";
2
+ import { isBatchSignable } from './index.js';
3
+ import { normalizeToDescriptor, extractPubKey } from './descriptor.js';
4
+ /**
5
+ * Wraps a legacy Identity (single-key) as a DescriptorProvider.
6
+ * The descriptor is always a simple tr(pubkey) format.
7
+ */
8
+ export class StaticDescriptorProvider {
9
+ constructor(identity, pubKeyHex) {
10
+ this.identity = identity;
11
+ this.pubKeyHex = pubKeyHex;
12
+ this.descriptor = `tr(${pubKeyHex})`;
13
+ }
14
+ static async create(identity) {
15
+ const pubKey = await identity.xOnlyPublicKey();
16
+ return new StaticDescriptorProvider(identity, hex.encode(pubKey));
17
+ }
18
+ getSigningDescriptor() {
19
+ return this.descriptor;
20
+ }
21
+ isOurs(descriptor) {
22
+ try {
23
+ const normalized = normalizeToDescriptor(descriptor);
24
+ const pubKey = extractPubKey(normalized);
25
+ return pubKey.toLowerCase() === this.pubKeyHex.toLowerCase();
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ async signWithDescriptor(requests) {
32
+ for (const request of requests) {
33
+ if (!this.isOurs(request.descriptor)) {
34
+ throw new Error(`Descriptor ${request.descriptor} does not belong to this provider`);
35
+ }
36
+ }
37
+ // Use batch signing when the identity supports it (fewer confirmation popups)
38
+ if (isBatchSignable(this.identity)) {
39
+ const signed = await this.identity.signMultiple(requests.map((r) => ({
40
+ tx: r.tx,
41
+ inputIndexes: r.inputIndexes,
42
+ })));
43
+ if (signed.length !== requests.length) {
44
+ throw new Error(`signMultiple returned ${signed.length} transactions, expected ${requests.length}`);
45
+ }
46
+ return signed;
47
+ }
48
+ const results = [];
49
+ for (const request of requests) {
50
+ const signed = await this.identity.sign(request.tx, request.inputIndexes);
51
+ results.push(signed);
52
+ }
53
+ return results;
54
+ }
55
+ async signMessageWithDescriptor(descriptor, message, type = "schnorr") {
56
+ if (!this.isOurs(descriptor)) {
57
+ throw new Error(`Descriptor ${descriptor} does not belong to this provider`);
58
+ }
59
+ return this.identity.signMessage(message, type);
60
+ }
61
+ }
package/dist/esm/index.js CHANGED
@@ -18,6 +18,7 @@ import { ServiceWorkerWallet, ServiceWorkerReadonlyWallet, DEFAULT_MESSAGE_TIMEO
18
18
  import { OnchainWallet } from './wallet/onchain.js';
19
19
  import { setupServiceWorker } from './worker/browser/utils.js';
20
20
  import { ESPLORA_URL, EsploraProvider, } from './providers/onchain.js';
21
+ import { ElectrumOnchainProvider, WsElectrumChainSource, } from './providers/electrum.js';
21
22
  import { RestArkProvider, SettlementEventType, } from './providers/ark.js';
22
23
  import { RestDelegatorProvider, } from './providers/delegator.js';
23
24
  import { CLTVMultisigTapscript, ConditionCSVMultisigTapscript, ConditionMultisigTapscript, CSVMultisigTapscript, decodeTapscript, MultisigTapscript, } from './script/tapscript.js';
@@ -46,7 +47,7 @@ export {
46
47
  // Wallets
47
48
  Wallet, ReadonlyWallet, SingleKey, ReadonlySingleKey, SeedIdentity, MnemonicIdentity, ReadonlyDescriptorIdentity, isBatchSignable, OnchainWallet, Ramps, VtxoManager, DelegatorManagerImpl, RestDelegatorProvider,
48
49
  // Providers
49
- ESPLORA_URL, EsploraProvider, RestArkProvider, RestIndexerProvider,
50
+ ESPLORA_URL, EsploraProvider, ElectrumOnchainProvider, WsElectrumChainSource, RestArkProvider, RestIndexerProvider,
50
51
  // Script-related
51
52
  ArkAddress, DefaultVtxo, DelegateVtxo, VtxoScript, VHTLC,
52
53
  // Enums