@qntx/spark 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,1024 @@
1
+ import { n as SparkError, r as SparkInputError, t as SparkCryptoError } from "./errors-5xLkExNL.mjs";
2
+ import { At as SecretShareSchema, B as parseTransaction, F as checkIfValidSequence, H as taprootScriptSpendSighash, It as SparkInvoiceFieldsSchema, K as PartialTokenTransactionSchema, Mt as SendLeafKeyTweaksSchema, Q as ChallengeSchema, R as getTimelockFromSequence, V as taprootKeySpendSighash, bt as ClaimPackageSchema, c as staticDepositStatementDigest, ct as uint64be, d as LIGHTNING_HTLC_SEQUENCE, et as hashSparkInvoice, ft as requireLength, g as lightningHtlcOutputScript, i as claimPackageDigest, jt as SendLeafKeyTweakSchema, l as transferPackageDigest, m as createSequenceLockScript, mt as sparkNetworkConfig, n as hashPartialTokenTransaction, nt as protoInvoiceToPlain, o as sendLeafSigningDigest, r as authnChallengeDigest, s as sspChallengeDigest, u as watchtowerRecoveryStatementDigest, vt as ClaimLeafKeyTweakSchema, yt as ClaimLeafKeyTweaksSchema, zt as TransferPackageSchema } from "./token-DmojVTWi.mjs";
3
+ import { i as lightningAddressMessageDigest, r as lightningAddressMessage } from "./lightning-address-CZrb8x4p.mjs";
4
+ import "./crypto.mjs";
5
+ import { bytesToHex, concatBytes, hexToBytes, utf8ToBytes } from "@noble/hashes/utils.js";
6
+ import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2";
7
+ import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
8
+ import { schnorr, secp256k1 } from "@noble/curves/secp256k1.js";
9
+ import { sha256 } from "@noble/hashes/sha2.js";
10
+ import { taprootTweakPrivKey } from "@scure/btc-signer/utils.js";
11
+ import { HDKey } from "@scure/bip32";
12
+ import { hmac } from "@noble/hashes/hmac.js";
13
+ const VSS_PROOF_LENGTH = 33;
14
+ const VSS_SHARE_LENGTH = 32;
15
+ const IDENTIFIER_PATTERN = /^[0-9a-f]{64}$/;
16
+ /**
17
+ * The local backend is synchronous; awaiting a resolved promise keeps every method async so callers
18
+ * always observe rejections, never synchronous throws.
19
+ */
20
+ const ready = Promise.resolve();
21
+ function unreachable(message) {
22
+ throw new SparkError(message);
23
+ }
24
+ function checkBatch(count, name) {
25
+ if (count > 256) throw new SparkInputError(`${name}: at most 256 items per call`);
26
+ }
27
+ /** Backend failures keep their ABI code; anything else becomes SparkCryptoError(8). */
28
+ function backendCall(call) {
29
+ try {
30
+ return call();
31
+ } catch (error) {
32
+ if (error instanceof SparkCryptoError) throw error;
33
+ const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "number" ? error.code : 8;
34
+ throw new SparkCryptoError(code, { cause: error });
35
+ }
36
+ }
37
+ function randomBytes(random, length) {
38
+ const bytes = random(length);
39
+ if (bytes.length !== length) throw new SparkError(`random: expected ${length} bytes, got ${bytes.length}`);
40
+ return bytes;
41
+ }
42
+ /**
43
+ * Draws `length` bytes of caller-supplied randomness for a single backend call, then wipes the
44
+ * seed. The seed is secret-equivalent material: it determines FROST nonces, the VSS polynomial, and
45
+ * the ECIES ephemeral key — knowing it undoes the secret the call protects.
46
+ */
47
+ function withRandom(random, length, use) {
48
+ const seed = randomBytes(random, length);
49
+ try {
50
+ return use(seed);
51
+ } finally {
52
+ seed.fill(0);
53
+ }
54
+ }
55
+ /** A fresh 32-byte scalar, retrying on the (negligible) chance the RNG output is invalid. */
56
+ function randomScalar(random) {
57
+ for (;;) {
58
+ const scalar = randomBytes(random, 32);
59
+ if (secp256k1.utils.isValidSecretKey(scalar)) return scalar;
60
+ scalar.fill(0);
61
+ }
62
+ }
63
+ function identifierBytes(identifier) {
64
+ if (!IDENTIFIER_PATTERN.test(identifier)) throw new SparkInputError("identifier: must be 64 lowercase hex characters");
65
+ return hexToBytes(identifier);
66
+ }
67
+ function commitmentKey(commitment) {
68
+ requireLength(commitment.hiding, 33, "commitment.hiding");
69
+ requireLength(commitment.binding, 33, "commitment.binding");
70
+ return bytesToHex(concatBytes(commitment.hiding, commitment.binding));
71
+ }
72
+ function checkOperators(operators) {
73
+ if (operators.length === 0) throw new SparkInputError("operators: must not be empty");
74
+ const ids = /* @__PURE__ */ new Set();
75
+ const identifiers = /* @__PURE__ */ new Set();
76
+ const publicKeys = /* @__PURE__ */ new Set();
77
+ for (const operator of operators) {
78
+ if (!Number.isSafeInteger(operator.id) || operator.id < 0) throw new SparkInputError("operator.id: must be a non-negative safe integer");
79
+ identifierBytes(operator.identifier);
80
+ requireLength(operator.identityPublicKey, 33, "operator.identityPublicKey");
81
+ const publicKey = bytesToHex(operator.identityPublicKey);
82
+ if (ids.has(operator.id) || identifiers.has(operator.identifier) || publicKeys.has(publicKey)) throw new SparkInputError("operators: id, identifier and identityPublicKey must each be unique");
83
+ ids.add(operator.id);
84
+ identifiers.add(operator.identifier);
85
+ publicKeys.add(publicKey);
86
+ }
87
+ }
88
+ /**
89
+ * Rejects a VSS threshold that is not a safe integer in `2..=operators.length` — the Rust VSS
90
+ * requires t ≥ 2, so accepting t = 1 here would only surface as a backend error. The wasm ABI
91
+ * otherwise coerces the JS number into an i32 (fractions truncate, values past 2^32 wrap) and
92
+ * silently splits at a different threshold than requested.
93
+ */
94
+ function checkThreshold(threshold, operators) {
95
+ if (!Number.isSafeInteger(threshold) || threshold < 2 || threshold > operators.length) throw new SparkInputError(`threshold: must be an integer between 2 and ${operators.length}`);
96
+ }
97
+ /** Splits the flat `t×33 ‖ n×32` backend output into per-share proofs and share bytes. */
98
+ function parseSplit(output, threshold, count) {
99
+ const proofsLength = threshold * VSS_PROOF_LENGTH;
100
+ if (output.length !== proofsLength + count * VSS_SHARE_LENGTH) throw new SparkCryptoError(8);
101
+ const proofs = [];
102
+ for (let i = 0; i < threshold; i++) proofs.push(output.slice(i * VSS_PROOF_LENGTH, (i + 1) * VSS_PROOF_LENGTH));
103
+ const shares = [];
104
+ for (let i = 0; i < count; i++) shares.push(output.slice(proofsLength + i * VSS_SHARE_LENGTH, proofsLength + (i + 1) * VSS_SHARE_LENGTH));
105
+ return {
106
+ proofs,
107
+ shares
108
+ };
109
+ }
110
+ /** The share for `operator` — share indexes run 1..n and operator id + 1 selects the share. */
111
+ function operatorShare(shares, operator, context) {
112
+ const share = shares[operator.id];
113
+ if (share === void 0) throw new SparkError(`${context}: no share for operator id ${operator.id}`);
114
+ return share;
115
+ }
116
+ //#endregion
117
+ //#region src/proto/gen/qntx_spark_ffi_pb.ts
118
+ /**
119
+ * Describes the file qntx_spark_ffi.proto.
120
+ */
121
+ const file_qntx_spark_ffi = /*@__PURE__*/ fileDesc("ChRxbnR4X3NwYXJrX2ZmaS5wcm90bxIOcW50eC5zcGFyay5mZmkiLQoKQ29tbWl0bWVudBIOCgZoaWRpbmcYASABKAwSDwoHYmluZGluZxgCIAEoDCJYChJPcGVyYXRvckNvbW1pdG1lbnQSEgoKaWRlbnRpZmllchgBIAEoDBIuCgpjb21taXRtZW50GAIgASgLMhoucW50eC5zcGFyay5mZmkuQ29tbWl0bWVudCLsAQoMRnJvc3RTaWduSm9iEg8KB21lc3NhZ2UYASABKAwSEgoKc2VjcmV0X2tleRgCIAEoDBIVCg12ZXJpZnlpbmdfa2V5GAMgASgMEg0KBW5vbmNlGAQgASgMEjMKD3VzZXJfY29tbWl0bWVudBgFIAEoCzIaLnFudHguc3BhcmsuZmZpLkNvbW1pdG1lbnQSQAoUb3BlcmF0b3JfY29tbWl0bWVudHMYBiADKAsyIi5xbnR4LnNwYXJrLmZmaS5PcGVyYXRvckNvbW1pdG1lbnQSGgoSYWRhcHRvcl9wdWJsaWNfa2V5GAcgASgMIj4KEEZyb3N0U2lnblJlcXVlc3QSKgoEam9icxgBIAMoCzIcLnFudHguc3BhcmsuZmZpLkZyb3N0U2lnbkpvYiItChFGcm9zdFNpZ25SZXNwb25zZRIYChBzaWduYXR1cmVfc2hhcmVzGAEgAygMIoABCg1PcGVyYXRvclNoYXJlEhIKCmlkZW50aWZpZXIYASABKAwSLgoKY29tbWl0bWVudBgCIAEoCzIaLnFudHguc3BhcmsuZmZpLkNvbW1pdG1lbnQSFwoPc2lnbmF0dXJlX3NoYXJlGAMgASgMEhIKCnB1YmxpY19rZXkYBCABKAwi9QEKEUZyb3N0QWdncmVnYXRlSm9iEg8KB21lc3NhZ2UYASABKAwSFQoNdmVyaWZ5aW5nX2tleRgCIAEoDBIzCg91c2VyX2NvbW1pdG1lbnQYAyABKAsyGi5xbnR4LnNwYXJrLmZmaS5Db21taXRtZW50EhcKD3VzZXJfcHVibGljX2tleRgEIAEoDBIcChR1c2VyX3NpZ25hdHVyZV9zaGFyZRgFIAEoDBIwCglvcGVyYXRvcnMYBiADKAsyHS5xbnR4LnNwYXJrLmZmaS5PcGVyYXRvclNoYXJlEhoKEmFkYXB0b3JfcHVibGljX2tleRgHIAEoDCJIChVGcm9zdEFnZ3JlZ2F0ZVJlcXVlc3QSLwoEam9icxgBIAMoCzIhLnFudHguc3BhcmsuZmZpLkZyb3N0QWdncmVnYXRlSm9iIiwKFkZyb3N0QWdncmVnYXRlUmVzcG9uc2USEgoKc2lnbmF0dXJlcxgBIAMoDGIGcHJvdG8z");
122
+ /**
123
+ * Describes the message qntx.spark.ffi.FrostSignJob.
124
+ * Use `create(FrostSignJobSchema)` to create a new message.
125
+ */
126
+ const FrostSignJobSchema = /*@__PURE__*/ messageDesc(file_qntx_spark_ffi, 2);
127
+ /**
128
+ * Describes the message qntx.spark.ffi.FrostSignRequest.
129
+ * Use `create(FrostSignRequestSchema)` to create a new message.
130
+ */
131
+ const FrostSignRequestSchema = /*@__PURE__*/ messageDesc(file_qntx_spark_ffi, 3);
132
+ /**
133
+ * Describes the message qntx.spark.ffi.FrostSignResponse.
134
+ * Use `create(FrostSignResponseSchema)` to create a new message.
135
+ */
136
+ const FrostSignResponseSchema = /*@__PURE__*/ messageDesc(file_qntx_spark_ffi, 4);
137
+ /**
138
+ * Describes the message qntx.spark.ffi.FrostAggregateJob.
139
+ * Use `create(FrostAggregateJobSchema)` to create a new message.
140
+ */
141
+ const FrostAggregateJobSchema = /*@__PURE__*/ messageDesc(file_qntx_spark_ffi, 6);
142
+ /**
143
+ * Describes the message qntx.spark.ffi.FrostAggregateRequest.
144
+ * Use `create(FrostAggregateRequestSchema)` to create a new message.
145
+ */
146
+ const FrostAggregateRequestSchema = /*@__PURE__*/ messageDesc(file_qntx_spark_ffi, 7);
147
+ /**
148
+ * Describes the message qntx.spark.ffi.FrostAggregateResponse.
149
+ * Use `create(FrostAggregateResponseSchema)` to create a new message.
150
+ */
151
+ const FrostAggregateResponseSchema = /*@__PURE__*/ messageDesc(file_qntx_spark_ffi, 8);
152
+ //#endregion
153
+ //#region src/signer/frost.ts
154
+ /**
155
+ * User-side FROST signature shares, one 32-byte share per job in order. Every job's commitment
156
+ * nonce is removed from the store before any other validation or the backend call — a batch that
157
+ * fails partway leaves no job's nonce outstanding, and reuse fails even after an error. Resolved
158
+ * private keys and consumed nonces are wiped.
159
+ */
160
+ function frostSignJobs(crypto, jobs, keys, nonces) {
161
+ checkBatch(jobs.length, "signFrost");
162
+ if (jobs.length === 0) return [];
163
+ const secretKeys = [];
164
+ const consumedNonces = [];
165
+ try {
166
+ const request = create(FrostSignRequestSchema, { jobs: [] });
167
+ const taken = jobs.map((job) => {
168
+ const nonce = nonces.take(job.commitment);
169
+ consumedNonces.push(nonce);
170
+ return {
171
+ job,
172
+ nonce
173
+ };
174
+ });
175
+ for (const { job, nonce } of taken) {
176
+ const secretKey = keys.resolve(job.key);
177
+ secretKeys.push(secretKey);
178
+ request.jobs.push(create(FrostSignJobSchema, {
179
+ message: taprootKeySpendSighash(job.target),
180
+ secretKey,
181
+ verifyingKey: job.verifyingKey,
182
+ nonce,
183
+ userCommitment: {
184
+ hiding: job.commitment.hiding,
185
+ binding: job.commitment.binding
186
+ },
187
+ operatorCommitments: job.operatorCommitments.map((operator) => ({
188
+ identifier: identifierBytes(operator.identifier),
189
+ commitment: {
190
+ hiding: operator.commitment.hiding,
191
+ binding: operator.commitment.binding
192
+ }
193
+ })),
194
+ adaptorPublicKey: job.adaptorPublicKey ?? /* @__PURE__ */ new Uint8Array()
195
+ }));
196
+ }
197
+ const requestBytes = toBinary(FrostSignRequestSchema, request);
198
+ let responseBytes;
199
+ try {
200
+ responseBytes = backendCall(() => crypto.frostSign(requestBytes));
201
+ } finally {
202
+ requestBytes.fill(0);
203
+ }
204
+ const response = fromBinary(FrostSignResponseSchema, responseBytes);
205
+ if (response.signatureShares.length !== jobs.length) throw new SparkCryptoError(8);
206
+ return response.signatureShares;
207
+ } finally {
208
+ for (const key of secretKeys) key.fill(0);
209
+ for (const nonce of consumedNonces) nonce.fill(0);
210
+ }
211
+ }
212
+ /**
213
+ * Aggregated 64-byte FROST (Schnorr) signatures, one per job in order. The user verifying share is
214
+ * derived from the resolved private key.
215
+ */
216
+ function frostAggregateJobs(crypto, jobs, keys) {
217
+ checkBatch(jobs.length, "aggregateFrost");
218
+ if (jobs.length === 0) return [];
219
+ const secretKeys = [];
220
+ try {
221
+ const request = create(FrostAggregateRequestSchema, { jobs: [] });
222
+ for (const job of jobs) {
223
+ const secretKey = keys.resolve(job.key);
224
+ secretKeys.push(secretKey);
225
+ request.jobs.push(create(FrostAggregateJobSchema, {
226
+ message: taprootKeySpendSighash(job.target),
227
+ verifyingKey: job.verifyingKey,
228
+ userCommitment: {
229
+ hiding: job.commitment.hiding,
230
+ binding: job.commitment.binding
231
+ },
232
+ userPublicKey: secp256k1.getPublicKey(secretKey, true),
233
+ userSignatureShare: job.signatureShare,
234
+ operators: job.operators.map((operator) => ({
235
+ identifier: identifierBytes(operator.identifier),
236
+ commitment: {
237
+ hiding: operator.commitment.hiding,
238
+ binding: operator.commitment.binding
239
+ },
240
+ signatureShare: operator.signatureShare,
241
+ publicKey: operator.publicKey
242
+ })),
243
+ adaptorPublicKey: job.adaptorPublicKey ?? /* @__PURE__ */ new Uint8Array()
244
+ }));
245
+ }
246
+ const responseBytes = backendCall(() => crypto.frostAggregate(toBinary(FrostAggregateRequestSchema, request)));
247
+ const response = fromBinary(FrostAggregateResponseSchema, responseBytes);
248
+ if (response.signatures.length !== jobs.length) throw new SparkCryptoError(8);
249
+ return response.signatures;
250
+ } finally {
251
+ for (const key of secretKeys) key.fill(0);
252
+ }
253
+ }
254
+ //#endregion
255
+ //#region src/signer/keys.ts
256
+ const HARDENED_OFFSET = 2147483648;
257
+ const SPARK_PURPOSE = "m/8797555'";
258
+ const DEFAULT_ACCOUNT = {
259
+ mainnet: 1,
260
+ regtest: 0
261
+ };
262
+ /**
263
+ * The 64-byte extended private key (private key ‖ chain code) at `m/8797555'/account'` — the input
264
+ * {@link createSparkSigner} expects. The default account matches the official SDK: mainnet 1,
265
+ * regtest 0.
266
+ */
267
+ function sparkAccountKey(seed, network, account) {
268
+ const resolvedAccount = account ?? DEFAULT_ACCOUNT[network];
269
+ if (!Number.isSafeInteger(resolvedAccount) || resolvedAccount < 0 || resolvedAccount >= HARDENED_OFFSET) throw new SparkInputError("account: must be a non-negative safe integer below 2^31");
270
+ let master;
271
+ try {
272
+ master = HDKey.fromMasterSeed(seed);
273
+ } catch (error) {
274
+ throw new SparkInputError("seed: invalid BIP-32 master seed", { cause: error });
275
+ }
276
+ const node = master.derive(`${SPARK_PURPOSE}/${resolvedAccount}'`);
277
+ if (node.privateKey === null || node.chainCode === null) throw new SparkInputError("seed: account derivation produced no private key");
278
+ const key = concatBytes(node.privateKey, node.chainCode);
279
+ master.wipePrivateData();
280
+ node.wipePrivateData();
281
+ return key;
282
+ }
283
+ function openKeys(accountKey, crypto) {
284
+ if (accountKey.length !== 64) throw new SparkInputError("accountKey: must be 64 bytes (private key ‖ chain code)");
285
+ const privateHalf = accountKey.slice(0, 32);
286
+ let root;
287
+ let identityNode;
288
+ let signingRoot;
289
+ let staticRoot;
290
+ try {
291
+ if (!secp256k1.utils.isValidSecretKey(privateHalf)) throw new SparkInputError("accountKey: private key half is not a valid secp256k1 scalar");
292
+ root = new HDKey({
293
+ privateKey: privateHalf,
294
+ chainCode: accountKey.slice(32)
295
+ });
296
+ identityNode = root.deriveChild(HARDENED_OFFSET);
297
+ signingRoot = root.deriveChild(2147483649);
298
+ staticRoot = root.deriveChild(2147483651);
299
+ } catch (error) {
300
+ privateHalf.fill(0);
301
+ throw error;
302
+ }
303
+ const identityPrivateKey = requiredPrivateKey(identityNode, "identity");
304
+ const identityPublicKey = secp256k1.getPublicKey(identityPrivateKey, true);
305
+ const identityPoint = secp256k1.Point.fromBytes(identityPublicKey);
306
+ const leafKeyCache = /* @__PURE__ */ new Map();
307
+ function leafPrivateKey(leafId) {
308
+ const cached = leafKeyCache.get(leafId);
309
+ if (cached !== void 0) return cached;
310
+ const hash = sha256(utf8ToBytes(leafId));
311
+ const index = new DataView(hash.buffer, hash.byteOffset, hash.byteLength).getUint32(0, false) % HARDENED_OFFSET;
312
+ const priv = requiredPrivateKey(signingRoot.deriveChild(HARDENED_OFFSET + index), "leaf key");
313
+ leafKeyCache.set(leafId, priv);
314
+ return priv;
315
+ }
316
+ function staticDepositPrivateKey(index) {
317
+ if (!Number.isSafeInteger(index) || index < 0 || index >= HARDENED_OFFSET) throw new SparkInputError("index: must be a non-negative safe integer below 2^31");
318
+ return requiredPrivateKey(staticRoot.deriveChild(HARDENED_OFFSET + index), "static deposit key");
319
+ }
320
+ function decryptReceivedKey(ciphertext) {
321
+ const plain = backendCall(() => crypto.eciesDecrypt(identityPrivateKey, ciphertext));
322
+ if (plain.length !== 32 || !secp256k1.utils.isValidSecretKey(plain)) {
323
+ plain.fill(0);
324
+ throw new SparkError("received key: decrypted payload is not a 32-byte secp256k1 private key");
325
+ }
326
+ return plain;
327
+ }
328
+ /** Resolves a key descriptor to a private-key copy the caller wipes after use. */
329
+ function resolve(key) {
330
+ if (key.type === "leaf") return new Uint8Array(leafPrivateKey(key.leafId));
331
+ if (key.type === "static-deposit") return staticDepositPrivateKey(key.index);
332
+ if (key.type === "received") return decryptReceivedKey(key.ciphertext);
333
+ return unreachable("unexpected key type");
334
+ }
335
+ return {
336
+ identityPrivateKey,
337
+ identityPublicKey,
338
+ identityPoint,
339
+ accountPrivate: privateHalf,
340
+ resolve,
341
+ staticDepositPrivateKey,
342
+ publicKeys(descriptors) {
343
+ return descriptors.map((descriptor) => {
344
+ const priv = resolve(descriptor);
345
+ try {
346
+ return secp256k1.getPublicKey(priv, true);
347
+ } finally {
348
+ priv.fill(0);
349
+ }
350
+ });
351
+ },
352
+ receivedPublicKeys(ciphertexts) {
353
+ return ciphertexts.map((ciphertext) => {
354
+ const priv = decryptReceivedKey(ciphertext);
355
+ try {
356
+ return secp256k1.getPublicKey(priv, true);
357
+ } finally {
358
+ priv.fill(0);
359
+ }
360
+ });
361
+ },
362
+ wipe() {
363
+ for (const priv of leafKeyCache.values()) priv.fill(0);
364
+ leafKeyCache.clear();
365
+ privateHalf.fill(0);
366
+ staticRoot.wipePrivateData();
367
+ signingRoot.wipePrivateData();
368
+ identityNode.wipePrivateData();
369
+ root.wipePrivateData();
370
+ }
371
+ };
372
+ }
373
+ function requiredPrivateKey(node, name) {
374
+ if (node.privateKey === null) throw new SparkError(`${name}: derivation produced no private key`);
375
+ return node.privateKey;
376
+ }
377
+ //#endregion
378
+ //#region src/signer/nonces.ts
379
+ const FROST_COMMITMENT_ITEM = 130;
380
+ const NONCE_LENGTH = 64;
381
+ const FROST_HEDGE_TAG = "spark/frost-hedge";
382
+ const MAX_OUTSTANDING = 1024;
383
+ function createNonceStore(crypto, accountPrivate, random) {
384
+ const hedgeKey = hmac(sha256, accountPrivate, utf8ToBytes(FROST_HEDGE_TAG));
385
+ const nonces = /* @__PURE__ */ new Map();
386
+ let hedgeCounter = 0n;
387
+ return {
388
+ commit(count) {
389
+ if (!Number.isSafeInteger(count) || count < 1 || count > 256) throw new SparkInputError(`newCommitments: count must be between 1 and 256`);
390
+ for (const [key, nonce] of nonces) {
391
+ if (nonces.size + count <= MAX_OUTSTANDING) break;
392
+ nonce.fill(0);
393
+ nonces.delete(key);
394
+ }
395
+ const hedge = hmac(sha256, hedgeKey, concatBytes(utf8ToBytes(FROST_HEDGE_TAG), uint64be(hedgeCounter)));
396
+ hedgeCounter += 1n;
397
+ let items;
398
+ try {
399
+ items = backendCall(() => withRandom(random, 32 * count, (seed) => crypto.frostCommit(hedge, seed)));
400
+ } finally {
401
+ hedge.fill(0);
402
+ }
403
+ if (items.length !== count * FROST_COMMITMENT_ITEM) {
404
+ items.fill(0);
405
+ throw new SparkCryptoError(8);
406
+ }
407
+ const commitments = [];
408
+ for (let i = 0; i < count; i++) {
409
+ const item = items.subarray(i * FROST_COMMITMENT_ITEM, (i + 1) * FROST_COMMITMENT_ITEM);
410
+ const nonce = item.slice(0, NONCE_LENGTH);
411
+ const hiding = item.slice(NONCE_LENGTH, 97);
412
+ const binding = item.slice(97, FROST_COMMITMENT_ITEM);
413
+ nonces.set(bytesToHex(concatBytes(hiding, binding)), nonce);
414
+ commitments.push({
415
+ hiding,
416
+ binding
417
+ });
418
+ }
419
+ items.fill(0);
420
+ return commitments;
421
+ },
422
+ take(commitment) {
423
+ const key = commitmentKey(commitment);
424
+ const nonce = nonces.get(key);
425
+ nonces.delete(key);
426
+ if (nonce === void 0) throw new SparkError("signFrost: unknown or already-consumed signing commitment");
427
+ return nonce;
428
+ },
429
+ wipe() {
430
+ hedgeKey.fill(0);
431
+ for (const nonce of nonces.values()) nonce.fill(0);
432
+ nonces.clear();
433
+ }
434
+ };
435
+ }
436
+ //#endregion
437
+ //#region src/signer/statements.ts
438
+ /**
439
+ * Computes the digest for a statement from its complete protocol content, plus the signature format
440
+ * upstream emits for that statement kind (DER ECDSA unless noted).
441
+ */
442
+ function statementDigest(statement, context) {
443
+ if (statement.kind === "so-challenge") {
444
+ const challenge = decode(ChallengeSchema, statement.challenge, "so challenge");
445
+ if (challenge.$unknown !== void 0) throw new SparkInputError("so challenge: unknown fields are not allowed");
446
+ if (!decodePoint(challenge.publicKey, "challenge.publicKey").equals(context.identityPoint)) throw new SparkInputError("so challenge: challenge targets a different public key than the signer identity");
447
+ return {
448
+ digest: authnChallengeDigest(challenge),
449
+ format: "der"
450
+ };
451
+ }
452
+ if (statement.kind === "ssp-challenge") return {
453
+ digest: sspChallengeDigest(statement.protectedChallenge, context.identityPublicKey),
454
+ format: "der"
455
+ };
456
+ if (statement.kind === "transfer-package") {
457
+ const pkg = decode(TransferPackageSchema, statement.package, "transfer package");
458
+ return {
459
+ digest: transferPackageDigest(statement.transferId, pkg),
460
+ format: "der"
461
+ };
462
+ }
463
+ if (statement.kind === "claim-package") {
464
+ const pkg = decode(ClaimPackageSchema, statement.package, "claim package");
465
+ return {
466
+ digest: claimPackageDigest(statement.transferId, pkg.keyTweakPackage),
467
+ format: "der"
468
+ };
469
+ }
470
+ if (statement.kind === "static-deposit-claim") return {
471
+ digest: staticDepositStatementDigest({
472
+ transactionId: bytesToHex(statement.txid),
473
+ outputIndex: statement.vout,
474
+ network: statement.network,
475
+ requestType: "fixed",
476
+ creditAmountSats: statement.creditAmountSats,
477
+ payload: statement.sspSignature
478
+ }),
479
+ format: "der"
480
+ };
481
+ if (statement.kind === "static-deposit-refund") return {
482
+ digest: staticDepositStatementDigest({
483
+ transactionId: bytesToHex(statement.txid),
484
+ outputIndex: statement.vout,
485
+ network: statement.network,
486
+ requestType: "refund",
487
+ creditAmountSats: statement.amount,
488
+ payload: taprootKeySpendSighash(statement.target)
489
+ }),
490
+ format: "der"
491
+ };
492
+ if (statement.kind === "watchtower-recovery") return {
493
+ digest: watchtowerRecoveryStatementDigest(statement.network, statement.leafId, taprootKeySpendSighash(statement.target)),
494
+ format: "der"
495
+ };
496
+ if (statement.kind === "spark-invoice") {
497
+ const proto = decode(SparkInvoiceFieldsSchema, statement.invoice, "spark invoice");
498
+ const invoice = protoInvoiceToPlain(proto);
499
+ return {
500
+ digest: hashSparkInvoice(invoice, context.identityPublicKey, context.network),
501
+ format: "schnorr"
502
+ };
503
+ }
504
+ if (statement.kind === "lightning-address") return {
505
+ digest: lightningAddressMessageDigest(lightningAddressMessage(statement)),
506
+ format: "der"
507
+ };
508
+ if (statement.kind === "token-transaction") {
509
+ const partial = decode(PartialTokenTransactionSchema, statement.partialTransaction, "token transaction");
510
+ return {
511
+ digest: hashPartialTokenTransaction(partial),
512
+ format: "schnorr"
513
+ };
514
+ }
515
+ return unreachable("unexpected statement kind");
516
+ }
517
+ /** Signs `digest` in `format`: low-S ECDSA (`der`/`compact`) or BIP-340 (`schnorr`). */
518
+ function signDigest(digest, format, secretKey, random) {
519
+ if (format === "schnorr") return schnorr.sign(digest, secretKey, randomBytes(random, 32));
520
+ return ecdsaSign(digest, secretKey, format);
521
+ }
522
+ function ecdsaSign(digest, secretKey, format) {
523
+ return secp256k1.sign(digest, secretKey, {
524
+ format,
525
+ prehash: false,
526
+ lowS: true
527
+ });
528
+ }
529
+ function decode(schema, bytes, name) {
530
+ try {
531
+ return fromBinary(schema, bytes);
532
+ } catch (error) {
533
+ throw new SparkInputError(`${name}: failed to decode protobuf`, { cause: error });
534
+ }
535
+ }
536
+ function decodePoint(bytes, name) {
537
+ try {
538
+ return secp256k1.Point.fromBytes(bytes);
539
+ } catch (error) {
540
+ throw new SparkInputError(`${name}: invalid secp256k1 public key`, { cause: error });
541
+ }
542
+ }
543
+ //#endregion
544
+ //#region src/signer/packages.ts
545
+ /**
546
+ * VSS-splits `tweak` across `operators` at `threshold`, returning the proofs, a per-operator share
547
+ * accessor and each operator's share public key (`share·G`). The backend output is registered in
548
+ * `secrets` so the caller's `finally` wipes it.
549
+ */
550
+ function splitTweak(tweak, operators, threshold, context, secrets, label) {
551
+ const { crypto, random } = context;
552
+ checkThreshold(threshold, operators);
553
+ const split = backendCall(() => withRandom(random, 32, (seed) => crypto.splitSecretWithProofs(tweak, threshold, operators.length, seed)));
554
+ secrets.push(split);
555
+ const { proofs, shares } = parseSplit(split, threshold, operators.length);
556
+ const shareFor = (operator) => operatorShare(shares, operator, label);
557
+ const pubkeySharesTweak = {};
558
+ for (const operator of operators) pubkeySharesTweak[operator.identifier] = secp256k1.getPublicKey(shareFor(operator), true);
559
+ return {
560
+ proofs,
561
+ shareFor,
562
+ pubkeySharesTweak
563
+ };
564
+ }
565
+ /**
566
+ * Builds one plaintext per operator and ECIES-seals it to that operator's identity key. Every
567
+ * plaintext is registered in `plaintexts` so the caller's `finally` wipes it.
568
+ */
569
+ function sealPerOperator(operators, plaintextFor, context, plaintexts) {
570
+ const { crypto, random } = context;
571
+ const operatorPackages = {};
572
+ for (const operator of operators) {
573
+ const plaintext = plaintextFor(operator);
574
+ plaintexts.push(plaintext);
575
+ operatorPackages[operator.identifier] = backendCall(() => withRandom(random, 32, (seed) => crypto.eciesEncrypt(operator.identityPublicKey, plaintext, seed)));
576
+ }
577
+ return operatorPackages;
578
+ }
579
+ /**
580
+ * Builds the sender transfer package: every leaf gets a fresh random recipient key, the
581
+ * operator-visible tweak is `old − new` (mod n) split by VSS, and each `SendLeafKeyTweaks`
582
+ * plaintext is ECIES-sealed to the operator it belongs to.
583
+ */
584
+ function prepareTransferPackage(request, context) {
585
+ const { crypto, keys, random, operators, threshold } = context;
586
+ if (request.leaves.length === 0) throw new SparkInputError("leaves: must not be empty");
587
+ const secrets = [];
588
+ const plaintexts = [];
589
+ try {
590
+ const perOperator = /* @__PURE__ */ new Map();
591
+ for (const operator of operators) perOperator.set(operator.identifier, []);
592
+ const leaves = [];
593
+ for (const leaf of request.leaves) {
594
+ requireLength(leaf.receiverIdentityPublicKey, 33, "receiverIdentityPublicKey");
595
+ const oldPrivateKey = keys.resolve(leaf.key);
596
+ const newPrivateKey = randomScalar(random);
597
+ secrets.push(oldPrivateKey, newPrivateKey);
598
+ const secretCipher = backendCall(() => withRandom(random, 32, (seed) => crypto.eciesEncrypt(leaf.receiverIdentityPublicKey, newPrivateKey, seed)));
599
+ const signature = ecdsaSign(sendLeafSigningDigest(leaf.leafId, request.transferId, secretCipher), keys.identityPrivateKey, "compact");
600
+ const tweak = secp256k1.Point.Fn.toBytes(secp256k1.Point.Fn.sub(secp256k1.Point.Fn.fromBytes(oldPrivateKey), secp256k1.Point.Fn.fromBytes(newPrivateKey)));
601
+ secrets.push(tweak);
602
+ const { proofs, shareFor, pubkeySharesTweak } = splitTweak(tweak, operators, threshold, context, secrets, "prepareTransfer");
603
+ for (const operator of operators) perOperator.get(operator.identifier)?.push(create(SendLeafKeyTweakSchema, {
604
+ leafId: leaf.leafId,
605
+ secretShareTweak: {
606
+ secretShare: shareFor(operator),
607
+ proofs
608
+ },
609
+ pubkeySharesTweak,
610
+ secretCipher,
611
+ sig: {
612
+ case: "signature",
613
+ value: signature
614
+ }
615
+ }));
616
+ leaves.push({
617
+ leafId: leaf.leafId,
618
+ newPublicKey: secp256k1.getPublicKey(newPrivateKey, true),
619
+ secretCipher,
620
+ signature
621
+ });
622
+ }
623
+ return {
624
+ operatorPackages: sealPerOperator(operators, (operator) => {
625
+ const tweaks = perOperator.get(operator.identifier);
626
+ if (tweaks === void 0) throw new SparkError("prepareTransfer: internal operator grouping failure");
627
+ return toBinary(SendLeafKeyTweaksSchema, create(SendLeafKeyTweaksSchema, { leavesToSend: tweaks }));
628
+ }, context, plaintexts),
629
+ leaves
630
+ };
631
+ } finally {
632
+ for (const secret of secrets) secret.fill(0);
633
+ for (const plaintext of plaintexts) plaintext.fill(0);
634
+ }
635
+ }
636
+ /**
637
+ * Builds the receiver claim package: each leaf's tweak is `received − leaf` (the sealed key minus
638
+ * the local `LEAF{leafId}` key), VSS-split and sealed per operator as `ClaimLeafKeyTweaks`.
639
+ */
640
+ function prepareClaimPackage(request, context) {
641
+ const { keys, operators, threshold } = context;
642
+ if (request.leaves.length === 0) throw new SparkInputError("leaves: must not be empty");
643
+ const secrets = [];
644
+ const plaintexts = [];
645
+ try {
646
+ const perOperator = /* @__PURE__ */ new Map();
647
+ for (const operator of operators) perOperator.set(operator.identifier, []);
648
+ for (const leaf of request.leaves) {
649
+ const receivedPrivateKey = keys.resolve({
650
+ type: "received",
651
+ ciphertext: leaf.secretCipher
652
+ });
653
+ const newPrivateKey = keys.resolve({
654
+ type: "leaf",
655
+ leafId: leaf.leafId
656
+ });
657
+ secrets.push(receivedPrivateKey, newPrivateKey);
658
+ const tweak = secp256k1.Point.Fn.toBytes(secp256k1.Point.Fn.sub(secp256k1.Point.Fn.fromBytes(receivedPrivateKey), secp256k1.Point.Fn.fromBytes(newPrivateKey)));
659
+ secrets.push(tweak);
660
+ const { proofs, shareFor, pubkeySharesTweak } = splitTweak(tweak, operators, threshold, context, secrets, "prepareClaim");
661
+ for (const operator of operators) perOperator.get(operator.identifier)?.push(create(ClaimLeafKeyTweakSchema, {
662
+ leafId: leaf.leafId,
663
+ secretShareTweak: {
664
+ secretShare: shareFor(operator),
665
+ proofs
666
+ },
667
+ pubkeySharesTweak
668
+ }));
669
+ }
670
+ return { operatorPackages: sealPerOperator(operators, (operator) => {
671
+ const tweaks = perOperator.get(operator.identifier);
672
+ if (tweaks === void 0) throw new SparkError("prepareClaim: internal operator grouping failure");
673
+ return toBinary(ClaimLeafKeyTweaksSchema, create(ClaimLeafKeyTweaksSchema, { leavesToReceive: tweaks }));
674
+ }, context, plaintexts) };
675
+ } finally {
676
+ for (const secret of secrets) secret.fill(0);
677
+ for (const plaintext of plaintexts) plaintext.fill(0);
678
+ }
679
+ }
680
+ /**
681
+ * Generates a preimage, VSS-splits it across `operators` at `threshold`, and seals one
682
+ * `SecretShare` plaintext per operator. Returns `sha256(preimage)` as the payment hash.
683
+ */
684
+ function prepareLightningReceivePackage(context) {
685
+ const { crypto, random, operators, threshold } = context;
686
+ const secrets = [];
687
+ const plaintexts = [];
688
+ try {
689
+ const preimage = randomScalar(random);
690
+ secrets.push(preimage);
691
+ const split = backendCall(() => withRandom(random, 32, (seed) => crypto.splitSecretWithProofs(preimage, threshold, operators.length, seed)));
692
+ secrets.push(split);
693
+ const { proofs, shares } = parseSplit(split, threshold, operators.length);
694
+ const operatorPackages = sealPerOperator(operators, (operator) => toBinary(SecretShareSchema, create(SecretShareSchema, {
695
+ secretShare: operatorShare(shares, operator, "prepareLightningReceive"),
696
+ proofs
697
+ })), context, plaintexts);
698
+ return {
699
+ paymentHash: sha256(preimage),
700
+ operatorPackages
701
+ };
702
+ } finally {
703
+ for (const secret of secrets) secret.fill(0);
704
+ for (const plaintext of plaintexts) plaintext.fill(0);
705
+ }
706
+ }
707
+ /**
708
+ * The static-deposit claim: ECIES-seals the child secret key to the SSP identity (the same scheme
709
+ * as transfer leaf secret ciphers) and signs the claim statement with the identity key. The
710
+ * plaintext key is wiped before returning.
711
+ */
712
+ function claimStaticDepositPackage(request, context) {
713
+ const { crypto, keys, network, random, sspIdentityPublicKey } = context;
714
+ const { digest } = statementDigest(request.statement, {
715
+ identityPoint: keys.identityPoint,
716
+ identityPublicKey: keys.identityPublicKey,
717
+ network
718
+ });
719
+ const secret = keys.staticDepositPrivateKey(request.index);
720
+ try {
721
+ return {
722
+ encryptedSecretKey: backendCall(() => withRandom(random, 32, (seed) => crypto.eciesEncrypt(sspIdentityPublicKey, secret, seed))),
723
+ signature: ecdsaSign(digest, keys.identityPrivateKey, "der")
724
+ };
725
+ } finally {
726
+ secret.fill(0);
727
+ }
728
+ }
729
+ //#endregion
730
+ //#region src/signer/local.ts
731
+ /**
732
+ * Creates the local Spark signer: the implementation of {@link SparkSigner} that holds the account
733
+ * extended private key and delegates curve-heavy FROST/ECIES/VSS work to a `SparkCrypto` backend.
734
+ * Throws `SparkInputError` on an ABI version mismatch or a malformed `accountKey`.
735
+ *
736
+ * The operator set, VSS threshold and SSP identity are bound here and default to the network's
737
+ * `sparkNetworkConfig`; every key-tweak share and the static-deposit secret is sealed to these keys
738
+ * and no request can retarget them. Pass explicit values only to point the signer at a different
739
+ * deployment (the local stack's operators/SSP), exactly like the client's `operators` and `ssp`
740
+ * options.
741
+ */
742
+ function createSparkSigner(options) {
743
+ const { crypto, network, random } = options;
744
+ if (crypto.abiVersion !== 1) throw new SparkInputError(`crypto.abiVersion: expected 1, got ${crypto.abiVersion}`);
745
+ if (network !== "mainnet" && network !== "regtest") throw new SparkInputError(`network: unsupported network ${String(network)}`);
746
+ const config = sparkNetworkConfig(network);
747
+ const operators = options.operators ?? config.operators;
748
+ const threshold = options.threshold ?? config.threshold;
749
+ const sspIdentityPublicKey = options.sspIdentityPublicKey ?? config.ssp.identityPublicKey;
750
+ checkOperators(operators);
751
+ checkThreshold(threshold, operators);
752
+ requireLength(sspIdentityPublicKey, 33, "sspIdentityPublicKey");
753
+ try {
754
+ secp256k1.Point.fromBytes(sspIdentityPublicKey);
755
+ } catch (error) {
756
+ throw new SparkInputError("sspIdentityPublicKey is not a valid point", { cause: error });
757
+ }
758
+ const keys = openKeys(options.accountKey, crypto);
759
+ const nonces = createNonceStore(crypto, keys.accountPrivate, random);
760
+ const statements = {
761
+ identityPoint: keys.identityPoint,
762
+ identityPublicKey: keys.identityPublicKey,
763
+ network
764
+ };
765
+ const packages = {
766
+ crypto,
767
+ keys,
768
+ network,
769
+ random,
770
+ operators,
771
+ threshold,
772
+ sspIdentityPublicKey
773
+ };
774
+ let disposed = false;
775
+ function ensureAlive() {
776
+ if (disposed) throw new SparkError("signer: disposed");
777
+ }
778
+ const identityPublicKey = async () => {
779
+ ensureAlive();
780
+ await ready;
781
+ ensureAlive();
782
+ return new Uint8Array(keys.identityPublicKey);
783
+ };
784
+ const publicKeys = async (descriptors) => {
785
+ ensureAlive();
786
+ await ready;
787
+ ensureAlive();
788
+ checkBatch(descriptors.length, "publicKeys");
789
+ return keys.publicKeys(descriptors);
790
+ };
791
+ const receivedPublicKeys = async (ciphertexts) => {
792
+ ensureAlive();
793
+ await ready;
794
+ ensureAlive();
795
+ checkBatch(ciphertexts.length, "receivedPublicKeys");
796
+ return keys.receivedPublicKeys(ciphertexts);
797
+ };
798
+ const newCommitments = async (count) => {
799
+ ensureAlive();
800
+ await ready;
801
+ ensureAlive();
802
+ return nonces.commit(count);
803
+ };
804
+ const signFrost = async (jobs) => {
805
+ ensureAlive();
806
+ await ready;
807
+ ensureAlive();
808
+ return frostSignJobs(crypto, jobs, keys, nonces);
809
+ };
810
+ const aggregateFrost = async (jobs) => {
811
+ ensureAlive();
812
+ await ready;
813
+ ensureAlive();
814
+ return frostAggregateJobs(crypto, jobs, keys);
815
+ };
816
+ const signIdentity = async (statement) => {
817
+ ensureAlive();
818
+ await ready;
819
+ ensureAlive();
820
+ const { digest, format } = statementDigest(statement, statements);
821
+ return signDigest(digest, format, keys.identityPrivateKey, random);
822
+ };
823
+ const signKeySpend = async (key, target) => {
824
+ ensureAlive();
825
+ await ready;
826
+ ensureAlive();
827
+ const secretKey = keys.resolve(key);
828
+ let tweaked;
829
+ try {
830
+ tweaked = taprootTweakPrivKey(secretKey);
831
+ return schnorr.sign(taprootKeySpendSighash(target), tweaked, randomBytes(random, 32));
832
+ } finally {
833
+ secretKey.fill(0);
834
+ tweaked?.fill(0);
835
+ }
836
+ };
837
+ const signHtlcTimeout = async (request) => {
838
+ ensureAlive();
839
+ await ready;
840
+ ensureAlive();
841
+ const tx = parseTransaction(request.transaction);
842
+ if (tx.inputsLength !== 1 || request.inputIndex !== 0) throw new SparkInputError("htlc timeout: only the single-input sender sweep is supported");
843
+ const { sequence } = tx.getInput(0);
844
+ if (sequence === void 0) throw new SparkInputError("htlc timeout: input has no sequence");
845
+ checkIfValidSequence(sequence);
846
+ if (getTimelockFromSequence(sequence) < 2160) throw new SparkInputError(`htlc timeout: input sequence ${String(sequence)} does not satisfy the ${LIGHTNING_HTLC_SEQUENCE}-block HTLC CSV`);
847
+ const sequenceLockScript = createSequenceLockScript(LIGHTNING_HTLC_SEQUENCE, keys.identityPublicKey);
848
+ return signDigest(taprootScriptSpendSighash({
849
+ transaction: request.transaction,
850
+ inputIndex: request.inputIndex,
851
+ prevouts: [{
852
+ script: lightningHtlcOutputScript(request.paymentHash, request.hashLockPublicKey, keys.identityPublicKey),
853
+ amount: request.prevoutAmountSats
854
+ }]
855
+ }, sequenceLockScript), "schnorr", keys.identityPrivateKey, random);
856
+ };
857
+ const prepareTransfer = async (request) => {
858
+ ensureAlive();
859
+ await ready;
860
+ ensureAlive();
861
+ return prepareTransferPackage(request, packages);
862
+ };
863
+ const prepareClaim = async (request) => {
864
+ ensureAlive();
865
+ await ready;
866
+ ensureAlive();
867
+ return prepareClaimPackage(request, packages);
868
+ };
869
+ const prepareLightningReceive = async () => {
870
+ ensureAlive();
871
+ await ready;
872
+ ensureAlive();
873
+ return prepareLightningReceivePackage(packages);
874
+ };
875
+ const claimStaticDeposit = async (request) => {
876
+ ensureAlive();
877
+ await ready;
878
+ ensureAlive();
879
+ return claimStaticDepositPackage(request, packages);
880
+ };
881
+ const dispose = () => {
882
+ if (disposed) return;
883
+ disposed = true;
884
+ nonces.wipe();
885
+ keys.wipe();
886
+ };
887
+ return {
888
+ identityPublicKey,
889
+ publicKeys,
890
+ receivedPublicKeys,
891
+ newCommitments,
892
+ signFrost,
893
+ aggregateFrost,
894
+ signIdentity,
895
+ signKeySpend,
896
+ signHtlcTimeout,
897
+ prepareTransfer,
898
+ prepareClaim,
899
+ prepareLightningReceive,
900
+ claimStaticDeposit,
901
+ dispose
902
+ };
903
+ }
904
+ //#endregion
905
+ //#region src/signer/lnurl-auth.ts
906
+ const PURPOSE_138 = "m/138'";
907
+ const HOST_LABEL = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
908
+ const SECRET_KEY_LENGTH = 32;
909
+ function pathElement(mac, offset) {
910
+ return new DataView(mac.buffer, mac.byteOffset, mac.byteLength).getUint32(offset);
911
+ }
912
+ function checkDomain(domain) {
913
+ if (domain !== domain.toLowerCase()) throw new SparkInputError("lnurl-auth domain: must be lowercase");
914
+ if (domain.length === 0 || domain.length > 253) throw new SparkInputError("lnurl-auth domain: invalid hostname length");
915
+ for (const label of domain.split(".")) if (!HOST_LABEL.test(label)) throw new SparkInputError("lnurl-auth domain: invalid hostname label");
916
+ }
917
+ function wipeNodes(nodes) {
918
+ for (const node of nodes) node.wipePrivateData();
919
+ }
920
+ function leafKeyOf(nodes) {
921
+ const leaf = nodes.at(-1);
922
+ if (leaf === void 0 || leaf.privateKey === null) throw new SparkError("lnurl-auth: linking-key derivation produced no private key");
923
+ return leaf.privateKey;
924
+ }
925
+ /**
926
+ * The 64-byte extended private key (private key ‖ chain code) at `m/138'` — the input
927
+ * {@link createLnurlAuthSigner} expects. The caller wipes the result when it is done holding the
928
+ * material.
929
+ */
930
+ function lnurlAuthRootKey(seed) {
931
+ let master;
932
+ try {
933
+ master = HDKey.fromMasterSeed(seed);
934
+ } catch (error) {
935
+ throw new SparkInputError("seed: invalid BIP-32 master seed", { cause: error });
936
+ }
937
+ const node = master.derive(PURPOSE_138);
938
+ if (node.privateKey === null || node.chainCode === null) throw new SparkInputError("seed: linking-key derivation produced no private key");
939
+ const key = concatBytes(node.privateKey, node.chainCode);
940
+ master.wipePrivateData();
941
+ node.wipePrivateData();
942
+ return key;
943
+ }
944
+ /** Creates the signer from a `m/138'` extended private key produced by {@link lnurlAuthRootKey}. */
945
+ function createLnurlAuthSigner(options) {
946
+ if (options.rootKey.length !== 64) throw new SparkInputError("rootKey: must be 64 bytes (private key ‖ chain code)");
947
+ const privateHalf = options.rootKey.slice(0, SECRET_KEY_LENGTH);
948
+ let root;
949
+ try {
950
+ if (!secp256k1.utils.isValidSecretKey(privateHalf)) throw new SparkInputError("rootKey: private key half is not a valid secp256k1 scalar");
951
+ root = new HDKey({
952
+ privateKey: privateHalf,
953
+ chainCode: options.rootKey.slice(SECRET_KEY_LENGTH)
954
+ });
955
+ } catch (error) {
956
+ privateHalf.fill(0);
957
+ throw error;
958
+ }
959
+ let disposed = false;
960
+ function ensureAlive() {
961
+ if (disposed) throw new SparkError("lnurl-auth signer: disposed");
962
+ }
963
+ function linkingNodes(domain) {
964
+ checkDomain(domain);
965
+ const hashing = root.deriveChild(0);
966
+ const nodes = [hashing];
967
+ try {
968
+ const hashingPrivate = hashing.privateKey;
969
+ if (hashingPrivate === null) throw new SparkInputError("lnurl-auth: hashing-key derivation produced no private key");
970
+ const mac = hmac(sha256, hashingPrivate, utf8ToBytes(domain));
971
+ let node = root;
972
+ try {
973
+ for (let i = 0; i < 4; i += 1) {
974
+ node = node.deriveChild(pathElement(mac, i * 4));
975
+ nodes.push(node);
976
+ }
977
+ return nodes;
978
+ } finally {
979
+ mac.fill(0);
980
+ }
981
+ } catch (error) {
982
+ wipeNodes(nodes);
983
+ throw error;
984
+ }
985
+ }
986
+ const linkingKey = async (domain) => {
987
+ ensureAlive();
988
+ await ready;
989
+ const nodes = linkingNodes(domain);
990
+ try {
991
+ return secp256k1.getPublicKey(leafKeyOf(nodes), true);
992
+ } finally {
993
+ wipeNodes(nodes);
994
+ }
995
+ };
996
+ const sign = async (request) => {
997
+ ensureAlive();
998
+ await ready;
999
+ if (request.k1.length !== 32) throw new SparkInputError("lnurl-auth k1: must be 32 bytes");
1000
+ const nodes = linkingNodes(request.domain);
1001
+ try {
1002
+ const privateKey = leafKeyOf(nodes);
1003
+ return {
1004
+ linkingKey: secp256k1.getPublicKey(privateKey, true),
1005
+ signature: ecdsaSign(request.k1, privateKey, "der")
1006
+ };
1007
+ } finally {
1008
+ wipeNodes(nodes);
1009
+ }
1010
+ };
1011
+ const dispose = () => {
1012
+ if (disposed) return;
1013
+ disposed = true;
1014
+ root.wipePrivateData();
1015
+ privateHalf.fill(0);
1016
+ };
1017
+ return {
1018
+ linkingKey,
1019
+ sign,
1020
+ dispose
1021
+ };
1022
+ }
1023
+ //#endregion
1024
+ export { createLnurlAuthSigner, createSparkSigner, lnurlAuthRootKey, sparkAccountKey };