@hwlt/era-connect 0.1.0 → 0.3.0

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/verify.js CHANGED
@@ -1,11 +1,15 @@
1
- import { A as utf8Encode, D as hexToBytes, E as equalBytes, T as concatBytes, Z as EraSdkError, k as utf8Decode, w as bytesToHex } from "./shared-B8Wc7ViU.js";
1
+ import { A as utf8Encode, D as hexToBytes, E as equalBytes, P as parsePath, T as concatBytes, Z as EraSdkError, k as utf8Decode, w as bytesToHex } from "./shared-B8Wc7ViU.js";
2
+ import { a as cardanoSoftDerivePath } from "./derive-C4JdycUh.js";
3
+ import { n as parseWitnessSet } from "./cardano-yuOygqwp.js";
2
4
  import { n as EvmDataType, r as foldRecoveryId } from "./evm-kJb3-oiR.js";
5
+ import { n as TonDataType } from "./ton-5Uvi5ifg.js";
3
6
  import { a as firstVarint, i as firstBytes, o as readFields, r as splitSignedTronTx } from "./messages-17Oup0-Z.js";
4
7
  import { secp256k1 } from "@noble/curves/secp256k1";
5
8
  import { sha256 } from "@noble/hashes/sha2";
6
9
  import { keccak_256 } from "@noble/hashes/sha3";
7
10
  import { createBase58check } from "@scure/base";
8
11
  import { ed25519 } from "@noble/curves/ed25519";
12
+ import { blake2b } from "@noble/hashes/blake2b";
9
13
  //#region src/verify/psbt-reader.ts
10
14
  const MAGIC = [
11
15
  112,
@@ -21,7 +25,7 @@ const PsbtInputType = {
21
25
  taprootKeySpendSignature: 19,
22
26
  taprootScriptSpendSignature: 20
23
27
  };
24
- function err(message) {
28
+ function err$1(message) {
25
29
  return new EraSdkError("malformed-reply", `psbt: ${message}`);
26
30
  }
27
31
  var Reader = class {
@@ -34,7 +38,7 @@ var Reader = class {
34
38
  }
35
39
  u8() {
36
40
  const b = this.bytes[this.offset];
37
- if (b === void 0) throw err("truncated");
41
+ if (b === void 0) throw err$1("truncated");
38
42
  this.offset += 1;
39
43
  return b;
40
44
  }
@@ -56,12 +60,12 @@ var Reader = class {
56
60
  }
57
61
  let value = 0n;
58
62
  for (let i = 0; i < width; i++) value |= BigInt(this.u8()) << BigInt(8 * i);
59
- if (value < minimum) throw err("non-minimal compact-size encoding");
60
- if (value > BigInt(Number.MAX_SAFE_INTEGER)) throw err("length exceeds safe range");
63
+ if (value < minimum) throw err$1("non-minimal compact-size encoding");
64
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) throw err$1("length exceeds safe range");
61
65
  return Number(value);
62
66
  }
63
67
  take(length) {
64
- if (length > this.remaining) throw err("length exceeds input");
68
+ if (length > this.remaining) throw err$1("length exceeds input");
65
69
  const out = this.bytes.slice(this.offset, this.offset + length);
66
70
  this.offset += length;
67
71
  return out;
@@ -77,7 +81,7 @@ function readMap(reader) {
77
81
  const key = reader.take(keyLength);
78
82
  const value = reader.take(reader.compactSize());
79
83
  const keyId = Array.from(key).map((b) => b.toString(16).padStart(2, "0")).join("");
80
- if (seen.has(keyId)) throw err("duplicate key within one map");
84
+ if (seen.has(keyId)) throw err$1("duplicate key within one map");
81
85
  seen.add(keyId);
82
86
  entries.push({
83
87
  keyType: key[0],
@@ -91,7 +95,7 @@ function countTxInputsOutputs(tx) {
91
95
  const reader = new Reader(tx);
92
96
  reader.take(4);
93
97
  const inputs = reader.compactSize();
94
- if (inputs === 0) throw err("unsigned transaction has zero inputs (or carries witness data)");
98
+ if (inputs === 0) throw err$1("unsigned transaction has zero inputs (or carries witness data)");
95
99
  for (let i = 0; i < inputs; i++) {
96
100
  reader.take(36);
97
101
  reader.take(reader.compactSize());
@@ -103,7 +107,7 @@ function countTxInputsOutputs(tx) {
103
107
  reader.take(reader.compactSize());
104
108
  }
105
109
  reader.take(4);
106
- if (reader.remaining !== 0) throw err("trailing bytes after the unsigned transaction");
110
+ if (reader.remaining !== 0) throw err$1("trailing bytes after the unsigned transaction");
107
111
  return {
108
112
  inputs,
109
113
  outputs
@@ -111,25 +115,25 @@ function countTxInputsOutputs(tx) {
111
115
  }
112
116
  function parsePsbt(bytes) {
113
117
  const reader = new Reader(bytes);
114
- for (const expected of MAGIC) if (reader.u8() !== expected) throw err("bad magic");
118
+ for (const expected of MAGIC) if (reader.u8() !== expected) throw err$1("bad magic");
115
119
  const globalMap = readMap(reader);
116
120
  let unsignedTx = null;
117
121
  let version = 0;
118
122
  for (const entry of globalMap) {
119
123
  if (entry.keyType === 0 && entry.keyData.length === 0) unsignedTx = entry.value;
120
124
  if (entry.keyType === 251 && entry.keyData.length === 0) {
121
- if (entry.value.length !== 4) throw err("bad version field");
125
+ if (entry.value.length !== 4) throw err$1("bad version field");
122
126
  version = (entry.value[0] | entry.value[1] << 8 | entry.value[2] << 16 | entry.value[3] << 24) >>> 0;
123
127
  }
124
128
  }
125
- if (unsignedTx === null) throw err("no global unsigned transaction — not a PSBT v0");
126
- if (version !== 0) throw err(`unsupported PSBT version ${version}`);
129
+ if (unsignedTx === null) throw err$1("no global unsigned transaction — not a PSBT v0");
130
+ if (version !== 0) throw err$1(`unsupported PSBT version ${version}`);
127
131
  const counts = countTxInputsOutputs(unsignedTx);
128
132
  const inputs = [];
129
133
  for (let i = 0; i < counts.inputs; i++) inputs.push(readMap(reader));
130
134
  const outputs = [];
131
135
  for (let i = 0; i < counts.outputs; i++) outputs.push(readMap(reader));
132
- if (reader.remaining !== 0) throw err("trailing bytes after the output maps");
136
+ if (reader.remaining !== 0) throw err$1("trailing bytes after the output maps");
133
137
  return {
134
138
  unsignedTx,
135
139
  version,
@@ -245,6 +249,150 @@ function message(e) {
245
249
  return e instanceof EraSdkError || e instanceof Error ? e.message : String(e);
246
250
  }
247
251
  //#endregion
252
+ //#region src/verify/cardano.ts
253
+ /**
254
+ * Recompute the digest the device signs — BLAKE2b-256 of the ENCODED FIRST
255
+ * ELEMENT of the transaction CBOR array (the tx body) — and verify every
256
+ * `[vkey, signature]` pair against it. With `account` + `signerPaths`, the
257
+ * vkeys are additionally required to be exactly the soft-derived children of
258
+ * YOUR linked account at the request's own paths.
259
+ */
260
+ function verifyCardanoSignature(args) {
261
+ let witnesses;
262
+ try {
263
+ witnesses = args.witnesses ?? (args.witnessSet ? parseWitnessSet(args.witnessSet) : []);
264
+ } catch (e) {
265
+ return failed(`witness set is not readable: ${e.message}`);
266
+ }
267
+ if (witnesses.length === 0) return failed("no witnesses to verify");
268
+ let digest;
269
+ try {
270
+ digest = blake2b(firstArrayItemBytes(args.signData), { dkLen: 32 });
271
+ } catch (e) {
272
+ return failed(`signData is not a readable transaction array: ${e.message}`);
273
+ }
274
+ for (const witness of witnesses) {
275
+ let ok;
276
+ try {
277
+ ok = ed25519.verify(witness.signature, digest, witness.vkey);
278
+ } catch (e) {
279
+ return failed(`Cardano signature could not be checked: ${e.message}`);
280
+ }
281
+ if (!ok) return failed("a witness signature does not verify against its own vkey");
282
+ }
283
+ if (args.account && args.signerPaths && args.signerPaths.length > 0) {
284
+ const accountLevels = parsePath(args.account.accountPath);
285
+ const expected = /* @__PURE__ */ new Map();
286
+ for (const path of new Set(args.signerPaths)) {
287
+ const levels = parsePath(path);
288
+ if (levels.length !== accountLevels.length + 2 || !accountLevels.every((l, i) => levels[i].index === l.index && levels[i].hardened === l.hardened) || levels.slice(accountLevels.length).some((l) => l.hardened)) return failed(`signer path ${path} does not extend the account path with two soft components`);
289
+ const tail = levels.slice(accountLevels.length).map((l) => l.index);
290
+ const vkey = cardanoSoftDerivePath(args.account.publicKey, args.account.chainCode, tail);
291
+ expected.set(bytesToHex(vkey), path);
292
+ }
293
+ for (const [vkeyHex, path] of expected) if (!witnesses.some((w) => bytesToHex(w.vkey) === vkeyHex)) return failed(`no witness for the requested signer path ${path}`);
294
+ for (const witness of witnesses) if (!expected.has(bytesToHex(witness.vkey))) return failed("the witness set carries a key your request did not ask for");
295
+ }
296
+ return verified;
297
+ }
298
+ function firstArrayItemBytes(bytes) {
299
+ const top = bytes[0];
300
+ if (top === void 0) throw new Error("empty input");
301
+ if (top >> 5 !== 4) throw new Error("not a CBOR array");
302
+ let start;
303
+ if ((top & 31) === 31) start = 1;
304
+ else {
305
+ const head = readHead(bytes, 0);
306
+ if (head.value === 0n) throw new Error("transaction array is empty");
307
+ start = head.next;
308
+ }
309
+ const end = skipItem(bytes, start, 0);
310
+ return bytes.slice(start, end);
311
+ }
312
+ function readHead(bytes, offset) {
313
+ const initial = bytes[offset];
314
+ if (initial === void 0) throw new Error("truncated");
315
+ const info = initial & 31;
316
+ if (info < 24) return {
317
+ value: BigInt(info),
318
+ next: offset + 1
319
+ };
320
+ if (info === 31) return {
321
+ value: -1n,
322
+ next: offset + 1
323
+ };
324
+ let width;
325
+ if (info === 24) width = 1;
326
+ else if (info === 25) width = 2;
327
+ else if (info === 26) width = 4;
328
+ else if (info === 27) width = 8;
329
+ else throw new Error("reserved length encoding");
330
+ let value = 0n;
331
+ for (let i = 0; i < width; i++) {
332
+ const b = bytes[offset + 1 + i];
333
+ if (b === void 0) throw new Error("truncated");
334
+ value = value << 8n | BigInt(b);
335
+ }
336
+ return {
337
+ value,
338
+ next: offset + 1 + width
339
+ };
340
+ }
341
+ /** Returns the offset just past the item starting at `offset`. */
342
+ function skipItem(bytes, offset, depth) {
343
+ if (depth > 32) throw new Error("nesting too deep");
344
+ const initial = bytes[offset];
345
+ if (initial === void 0) throw new Error("truncated");
346
+ const major = initial >> 5;
347
+ const head = readHead(bytes, offset);
348
+ switch (major) {
349
+ case 0:
350
+ case 1:
351
+ if (head.value === -1n) throw new Error("malformed integer");
352
+ return head.next;
353
+ case 2:
354
+ case 3: {
355
+ if (head.value === -1n) {
356
+ let pos = head.next;
357
+ while (bytes[pos] !== 255) {
358
+ const chunk = readHead(bytes, pos);
359
+ if (chunk.value < 0n) throw new Error("malformed chunk");
360
+ pos = chunk.next + Number(chunk.value);
361
+ if (pos > bytes.length) throw new Error("truncated string");
362
+ }
363
+ return pos + 1;
364
+ }
365
+ const end = head.next + Number(head.value);
366
+ if (end > bytes.length) throw new Error("truncated string");
367
+ return end;
368
+ }
369
+ case 4:
370
+ case 5: {
371
+ const perEntry = major === 5 ? 2 : 1;
372
+ if (head.value === -1n) {
373
+ let pos = head.next;
374
+ while (bytes[pos] !== 255) for (let i = 0; i < perEntry; i++) pos = skipItem(bytes, pos, depth + 1);
375
+ return pos + 1;
376
+ }
377
+ let pos = head.next;
378
+ const count = Number(head.value) * perEntry;
379
+ if (count > 1e6) throw new Error("container too large");
380
+ for (let i = 0; i < count; i++) pos = skipItem(bytes, pos, depth + 1);
381
+ return pos;
382
+ }
383
+ case 6:
384
+ if (head.value === -1n) throw new Error("malformed tag");
385
+ return skipItem(bytes, head.next, depth + 1);
386
+ case 7:
387
+ if ((initial & 31) === 31) throw new Error("unexpected break");
388
+ if ((initial & 31) === 25) return offset + 3;
389
+ if ((initial & 31) === 26) return offset + 5;
390
+ if ((initial & 31) === 27) return offset + 9;
391
+ return head.next;
392
+ default: throw new Error("unreachable");
393
+ }
394
+ }
395
+ //#endregion
248
396
  //#region src/verify/evm.ts
249
397
  /** EIP-191: 0x19 || "Ethereum Signed Message:\n" || len(message). */
250
398
  function personalSignDigest(message) {
@@ -300,6 +448,144 @@ function verifySolanaSignature(args) {
300
448
  return ok ? verified : failed("the signature does not belong to this account");
301
449
  }
302
450
  //#endregion
451
+ //#region src/verify/ton-boc.ts
452
+ /**
453
+ * Minimal TON Bag-of-Cells reader + cell representation hash — exactly the
454
+ * subset the device's signer implements: generic BoC, ordinary level-0 cells,
455
+ * first root. The transaction digest the device signs IS the root cell's
456
+ * representation hash, so this must agree with the firmware bit for bit.
457
+ *
458
+ * Hardened for scanned/untrusted input: size caps, bounds checks, and
459
+ * forward-only references (the standard BoC topological order; a backward
460
+ * reference would make the single-pass hash read an uncomputed child).
461
+ */
462
+ const BOC_MAGIC = 3052313714;
463
+ const MAX_CELLS = 256;
464
+ const MAX_CELL_DATA_BYTES = 128;
465
+ const MAX_REFS = 4;
466
+ function err(message) {
467
+ return new EraSdkError("malformed-reply", `ton boc: ${message}`);
468
+ }
469
+ /** Representation hash of the ROOT cell of a BoC — the bytes TON signs. */
470
+ function bocRootHash(boc) {
471
+ if (boc.length < 10) throw err("too short");
472
+ if ((boc[0] << 24 | boc[1] << 16 | boc[2] << 8 | boc[3]) >>> 0 !== BOC_MAGIC) throw err("not a generic BoC");
473
+ const flags = boc[4];
474
+ const hasIdx = flags >> 7 & 1;
475
+ const refSize = flags & 7;
476
+ const offSize = boc[5];
477
+ if (refSize === 0 || refSize > 4) throw err("bad ref size");
478
+ if (offSize === 0 || offSize > 8) throw err("bad offset size");
479
+ let pos = 6;
480
+ const readInt = (byteLen) => {
481
+ let value = 0;
482
+ for (let i = 0; i < byteLen; i++) {
483
+ if (pos >= boc.length) throw err("truncated header");
484
+ value = value * 256 + boc[pos++];
485
+ }
486
+ if (!Number.isSafeInteger(value)) throw err("header value out of range");
487
+ return value;
488
+ };
489
+ const cellCount = readInt(refSize);
490
+ const rootCount = readInt(refSize);
491
+ readInt(refSize);
492
+ readInt(offSize);
493
+ if (rootCount === 0) throw err("no roots");
494
+ if (cellCount === 0 || cellCount > MAX_CELLS) throw err("cell count out of range");
495
+ const rootIndex = readInt(refSize);
496
+ for (let i = 1; i < rootCount; i++) readInt(refSize);
497
+ if (rootIndex >= cellCount) throw err("root index out of range");
498
+ if (hasIdx) pos += cellCount * offSize;
499
+ const cells = [];
500
+ for (let i = 0; i < cellCount; i++) {
501
+ if (pos + 2 > boc.length) throw err("truncated cell");
502
+ const d1 = boc[pos++];
503
+ const d2 = boc[pos++];
504
+ const refCount = d1 & 7;
505
+ if (refCount > MAX_REFS) throw err("too many references");
506
+ const dataByteLen = d2 + 1 >> 1;
507
+ const incomplete = (d2 & 1) === 1;
508
+ if (dataByteLen > MAX_CELL_DATA_BYTES) throw err("cell data too large");
509
+ if (pos + dataByteLen > boc.length) throw err("truncated cell data");
510
+ const data = boc.slice(pos, pos + dataByteLen);
511
+ pos += dataByteLen;
512
+ let dataBits;
513
+ if (incomplete && dataByteLen > 0) {
514
+ const last = data[dataByteLen - 1];
515
+ if (last === 0) dataBits = (dataByteLen - 1) * 8;
516
+ else {
517
+ let trailingZeros = 0;
518
+ for (let b = 0; b < 8; b++) {
519
+ if (last & 1 << b) break;
520
+ trailingZeros++;
521
+ }
522
+ dataBits = dataByteLen * 8 - 1 - trailingZeros;
523
+ }
524
+ } else dataBits = dataByteLen * 8;
525
+ const refs = [];
526
+ for (let r = 0; r < refCount; r++) {
527
+ const ref = readInt(refSize);
528
+ if (ref >= cellCount) throw err("reference out of range");
529
+ if (ref <= i) throw err("non-topological cell reference");
530
+ refs.push(ref);
531
+ }
532
+ cells.push({
533
+ dataBits,
534
+ data,
535
+ refs,
536
+ depth: 0,
537
+ hash: /* @__PURE__ */ new Uint8Array(0)
538
+ });
539
+ }
540
+ for (let i = cellCount - 1; i >= 0; i--) {
541
+ const cell = cells[i];
542
+ let maxChildDepth = -1;
543
+ for (const r of cell.refs) maxChildDepth = Math.max(maxChildDepth, cells[r].depth);
544
+ cell.depth = cell.refs.length === 0 ? 0 : maxChildDepth + 1;
545
+ const dataBytes = cell.dataBits + 7 >> 3;
546
+ const incomplete = cell.dataBits % 8 !== 0;
547
+ const repr = [cell.refs.length, dataBytes * 2 - (incomplete ? 1 : 0)];
548
+ for (let b = 0; b < dataBytes; b++) repr.push(cell.data[b] ?? 0);
549
+ if (incomplete && dataBytes > 0) {
550
+ const shift = 7 - cell.dataBits % 8;
551
+ const last = repr.length - 1;
552
+ repr[last] = (repr[last] | 1 << shift) & 255 << shift & 255;
553
+ }
554
+ for (const r of cell.refs) repr.push(cells[r].depth >> 8 & 255, cells[r].depth & 255);
555
+ let bytes = new Uint8Array(repr);
556
+ for (const r of cell.refs) bytes = concatBytes(bytes, cells[r].hash);
557
+ cell.hash = sha256(bytes);
558
+ }
559
+ return cells[rootIndex].hash;
560
+ }
561
+ //#endregion
562
+ //#region src/verify/ton.ts
563
+ /**
564
+ * Recompute the exact digest the device signs — the BoC ROOT CELL's
565
+ * representation hash for a transaction, or the TON Connect proof digest
566
+ * `sha256(0xFFFF || "ton-connect" || sha256(payload))` — and verify the
567
+ * Ed25519 signature against the linked key.
568
+ */
569
+ function verifyTonSignature(args) {
570
+ if (args.signature.length !== 64) return failed("signature must be 64 bytes");
571
+ if (args.publicKey.length !== 32) return failed("public key must be 32 bytes");
572
+ let digest;
573
+ if (args.dataType === TonDataType.tonProof) digest = sha256(concatBytes(new Uint8Array([255, 255]), utf8Encode("ton-connect"), sha256(args.signData)));
574
+ else if (args.dataType === TonDataType.transaction) try {
575
+ digest = bocRootHash(args.signData);
576
+ } catch (e) {
577
+ return failed(`signData is not a readable BoC: ${e.message}`);
578
+ }
579
+ else return failed(`unknown dataType ${args.dataType}`);
580
+ let ok;
581
+ try {
582
+ ok = ed25519.verify(args.signature, digest, args.publicKey);
583
+ } catch (e) {
584
+ return failed(`TON signature could not be checked: ${e.message}`);
585
+ }
586
+ return ok ? verified : failed("the signature does not belong to this account");
587
+ }
588
+ //#endregion
303
589
  //#region src/verify/tron.ts
304
590
  const base58check = createBase58check(sha256);
305
591
  /**
@@ -433,6 +719,6 @@ function compareWindow(repliedBytes, latestBlock) {
433
719
  return verified;
434
720
  }
435
721
  //#endregion
436
- export { PsbtInputType, parsePsbt, verifyBtcMessageHeader, verifyEvmSignature, verifySignedPsbt, verifySolanaSignature, verifyTronSignature };
722
+ export { PsbtInputType, bocRootHash, parsePsbt, verifyBtcMessageHeader, verifyCardanoSignature, verifyEvmSignature, verifySignedPsbt, verifySolanaSignature, verifyTonSignature, verifyTronSignature };
437
723
 
438
724
  //# sourceMappingURL=verify.js.map