@leofcoin/chain 1.10.22 → 1.11.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.
Files changed (28) hide show
  1. package/README.md +74 -0
  2. package/exports/browser/beacon-wire.js +2 -2
  3. package/exports/browser/{browser-CCfR9KOM-UO9mzy-j.js → browser-CCfR9KOM-CNWKLusu.js} +3 -3
  4. package/exports/browser/{browser-crypto-CH0ua22L.js → browser-crypto-B_UJguhk.js} +453 -35
  5. package/exports/browser/chain.js +1234 -885
  6. package/exports/browser/{client-DbZBfpG6-CYDyUGUC.js → client-DbZBfpG6-C1cXgvfr.js} +4 -4
  7. package/exports/browser/{connection-monitor-DMGAhcC0.js → connection-monitor-BzbeYGEt.js} +46 -26
  8. package/exports/browser/{index-dTQVgkpf-C6SJ0ZYW.js → index-dTQVgkpf-CkoAiRNX.js} +3 -3
  9. package/exports/browser/{index-dxD17Fom.js → index-phv5KvaE.js} +7 -7
  10. package/exports/browser/{messages-DZYCxoZI-D6kIiLKF.js → messages-DZYCxoZI-B472zldo.js} +3 -3
  11. package/exports/browser/{node-browser-BMfelxXV.js → node-browser-BmyPBkL5.js} +36 -8
  12. package/exports/browser/node-browser.js +3 -3
  13. package/exports/browser/workers/block-worker.js +8 -9
  14. package/exports/browser/workers/machine-worker.js +141 -1195
  15. package/exports/{workers/worker-BDTWOCzS-COrAszCt.js → browser/workers/proposal.proto-8zgMfutf-B3So_tnS.js} +910 -398
  16. package/exports/chain.js +248 -960
  17. package/exports/{connection-monitor-h2L8vsS7.js → connection-monitor-BdFLRh-J.js} +33 -3
  18. package/exports/connection-monitor.js +1 -1
  19. package/exports/{constants-Dy9i9S_o.js → constants-BQ6f-_OT.js} +1 -1
  20. package/exports/hash-format-state.js +3 -3
  21. package/exports/machine.js +550 -0
  22. package/exports/node.js +33 -2
  23. package/exports/state-checkpoint-BiQ1EXzo.js +110 -0
  24. package/exports/state-checkpoint.js +3 -0
  25. package/exports/workers/block-worker.js +8 -9
  26. package/exports/workers/machine-worker.js +141 -1195
  27. package/exports/{browser/workers/worker-BDTWOCzS-COrAszCt.js → workers/proposal.proto-8zgMfutf-B3So_tnS.js} +910 -398
  28. package/package.json +7 -5
@@ -1,11 +1,20 @@
1
- import { F as FormatInterface, p as proto, a as proto$1, b as proto$2, T as TransactionMessage, j as jsonStringifyBigInt, c as jsonParseBigInt, C as ContractMessage, d as Codec } from './browser-crypto-CH0ua22L.js';
2
- import { P as Protocol, B as BlockMessage, L as LastBlockMessage, a as PROTOCOL_VERSION, R as REACHED_ONE_ZERO_ZERO, C as ConnectionMonitor, b as BWMessage, S as StateMessage } from './connection-monitor-DMGAhcC0.js';
3
- import { M as MAX_TRANSACTION_BYTES, a as MAX_BLOCK_TRANSACTION_BYTES, b as MAX_BLOCK_TRANSACTIONS, c as MultiWallet, d as createTransactionHash, e as calculateFee, f as createContractMessage, s as signTransaction, r as randombytes, g as distributeTransactionFee, h as supportsTransactionFees, i as supportsMonetaryPolicy, v as validateBlockResourceLimits, j as calculateMonetaryPolicy, k as distributeAmount, l as aggregateValidatorFees, m as MONETARY_POLICY_AUTHORITY, n as contractFactoryMessage, o as nativeTokenMessage, p as validatorsMessage, q as nameServiceMessage, t as parseUnits, u as formatUnits } from './index-dxD17Fom.js';
1
+ import { C as Codec, B as BinaryFormat, p as proto, a as proto$1, b as proto$2, T as TransactionMessage, j as jsonParseBigInt, c as jsonStringifyBigInt, d as ContractMessage, e as decodeWire$1, f as encodeWire$1 } from './browser-crypto-B_UJguhk.js';
2
+ import { P as Protocol, B as BlockMessage, L as LastBlockMessage, a as PROTOCOL_VERSION, R as REACHED_ONE_ZERO_ZERO, C as ConnectionMonitor, b as BWMessage, S as StateMessage } from './connection-monitor-BzbeYGEt.js';
4
3
  import { f as fromBase58 } from './index-CvKt4UDE.js';
4
+ import { M as MAX_TRANSACTION_BYTES, a as MAX_BLOCK_TRANSACTION_BYTES, b as MAX_BLOCK_TRANSACTIONS, c as MultiWallet, d as createTransactionHash, e as calculateFee, f as createContractMessage, s as signTransaction, r as randombytes, g as supportsTransactionFees, h as supportsMonetaryPolicy, v as validateBlockResourceLimits, i as aggregateValidatorFees, j as distributeAmount, k as MONETARY_POLICY_AUTHORITY, l as calculateMonetaryPolicy, m as contractFactoryMessage, n as nativeTokenMessage, o as validatorsMessage, p as nameServiceMessage, q as parseUnits, t as formatUnits, u as distributeTransactionFee } from './index-phv5KvaE.js';
5
5
  import { g as getDefaultExportFromCjs } from './_commonjsHelpers-B85MJLTf.js';
6
6
  import { q as quorumThreshold } from './quorum-S_qdgiAY.js';
7
7
  import './sha2-BKcEJzgd.js';
8
8
 
9
+ const decodeResponsePayload = async (result) => {
10
+ if (!(result instanceof Uint8Array)) throw new TypeError("Peer response must be Uint8Array");
11
+ const codec = new Codec(result);
12
+ if (codec.name !== "peernet-response") return result;
13
+ const ResponseMessage = globalThis.peernet?.protos?.["peernet-response"];
14
+ if (!ResponseMessage) throw new Error("peernet-response codec is not registered");
15
+ return new ResponseMessage(result).decoded.response;
16
+ };
17
+
9
18
  const getTargets = () => Array.from([]);
10
19
  const isEnabled = (target) => {
11
20
  return false;
@@ -42,7 +51,7 @@ const formatBytes = (bytes, decimals = 2) => {
42
51
  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${byteFormats[i]}`;
43
52
  };
44
53
 
45
- class PrevoteMessage extends FormatInterface {
54
+ class PrevoteMessage extends BinaryFormat {
46
55
  get messageName() {
47
56
  return "PrevoteMessage";
48
57
  }
@@ -54,7 +63,7 @@ class PrevoteMessage extends FormatInterface {
54
63
  }
55
64
  }
56
65
 
57
- class PrecommitMessage extends FormatInterface {
66
+ class PrecommitMessage extends BinaryFormat {
58
67
  get messageName() {
59
68
  return "PrecommitMessage";
60
69
  }
@@ -66,7 +75,7 @@ class PrecommitMessage extends FormatInterface {
66
75
  }
67
76
  }
68
77
 
69
- class ProposalMessage extends FormatInterface {
78
+ class ProposalMessage extends BinaryFormat {
70
79
  get messageName() {
71
80
  return "ProposalMessage";
72
81
  }
@@ -78,10 +87,10 @@ class ProposalMessage extends FormatInterface {
78
87
  }
79
88
  }
80
89
 
81
- var contractFactory$1 = "B3OGRUBSBGSASFXWDZUI6AYITY76PYQZS4O5UDWVLSW56X5CSWTQGTGQHUGU";
82
- var nativeToken$1 = "B3OGRUBSA5V74HIRRP6YPMZYDZ7YM3SR3MQX63XSYEZIZ7TWHBPDWRWVA2XA";
83
- var nameService$1 = "B3OGRUBSBXX7EPFWHGZQGGSWBMMK7ATOSDICOW5CCFP25OMZBTRMHYGM4KFF";
84
- var validators$1 = "B3OGRUBSBXLCYRTXG5SRL6XNINEJNAI7CLJBKSRCXY24GMFU5ZY7TXHAGJGT";
90
+ var contractFactory$1 = "B3OGRUBSARKAMWHZLZZAL5Y4SRUJOE4RIMQX4SELNLMTQVPBGFQXEF5RPPHV";
91
+ var nativeToken$1 = "B3OGRUBSBSWHYGNYT4O57EK7Q5ZNXYFR23CCS4EUQPQ34RU57V6ROQNLYIVN";
92
+ var nameService$1 = "B3OGRUBSB23VEDKVPQM5DYREJVI2XYMSQ44ZN3NQG445AZMASFMOUU4P7HLM";
93
+ var validators$1 = "B3OGRUBSBT6UD4G6V3KP7Z6G2DLMMOV5D5IFIZH2I3KD2CK6VFD3CYY2NCKV";
85
94
  var addresses$1 = {
86
95
  contractFactory: contractFactory$1,
87
96
  nativeToken: nativeToken$1,
@@ -2915,26 +2924,46 @@ function requireSemver () {
2915
2924
  var semverExports = requireSemver();
2916
2925
  var semver = /*@__PURE__*/getDefaultExportFromCjs(semverExports);
2917
2926
 
2918
- const waitForCanonicalTransaction = (pubsub, transactionPool, hash) => new Promise((resolve, reject) => {
2919
- const completionTopic = `transaction.completed.${hash}`;
2920
- const cleanup = () => {
2921
- pubsub.unsubscribe("block-processed", blockProcessed);
2922
- pubsub.unsubscribe(completionTopic, transactionCompleted);
2927
+ const createCanonicalTransactionWaiter = (pubsub, transactionPool, hash) => {
2928
+ let cleanup = () => {
2923
2929
  };
2924
- const blockProcessed = (block) => {
2925
- if (!Array.isArray(block?.transactions) || !block.transactions.includes(hash)) return;
2926
- cleanup();
2927
- resolve(hash);
2930
+ let cancel = (_reason) => {
2928
2931
  };
2929
- const transactionCompleted = async (result) => {
2930
- if (result?.status === "fulfilled") return;
2931
- await transactionPool.delete(hash);
2932
- cleanup();
2933
- reject({ hash, error: result?.error });
2934
- };
2935
- pubsub.subscribe("block-processed", blockProcessed);
2936
- pubsub.subscribe(completionTopic, transactionCompleted);
2937
- });
2932
+ const wait = new Promise((resolve, reject) => {
2933
+ const completionTopic = `transaction.completed.${hash}`;
2934
+ let settled = false;
2935
+ cancel = (reason) => {
2936
+ if (settled) return;
2937
+ settled = true;
2938
+ reject({ hash, error: reason });
2939
+ };
2940
+ cleanup = () => {
2941
+ pubsub.unsubscribe("block-processed", blockProcessed);
2942
+ pubsub.unsubscribe(completionTopic, transactionCompleted);
2943
+ };
2944
+ const blockProcessed = (block) => {
2945
+ if (settled || !Array.isArray(block?.transactions) || !block.transactions.includes(hash)) return;
2946
+ settled = true;
2947
+ resolve(hash);
2948
+ };
2949
+ const transactionCompleted = async (result) => {
2950
+ if (settled || result?.status === "fulfilled") return;
2951
+ settled = true;
2952
+ try {
2953
+ await transactionPool.delete(hash);
2954
+ reject({ hash, error: result?.error });
2955
+ } catch (error) {
2956
+ reject({ hash, error, transactionError: result?.error });
2957
+ }
2958
+ };
2959
+ pubsub.subscribe("block-processed", blockProcessed);
2960
+ pubsub.subscribe(completionTopic, transactionCompleted);
2961
+ });
2962
+ const finalized = wait.finally(() => cleanup());
2963
+ void finalized.catch(() => {
2964
+ });
2965
+ return { wait: finalized, cancel };
2966
+ };
2938
2967
 
2939
2968
  class Transaction extends Protocol {
2940
2969
  #pendingNonces = /* @__PURE__ */ new Map();
@@ -3135,9 +3164,10 @@ class Transaction extends Protocol {
3135
3164
  await this.validateTransactionSignature(message);
3136
3165
  await this.validateNonce(message.decoded.from, message.decoded.nonce);
3137
3166
  const hash = await message.hash();
3167
+ const fee = await calculateFee(message.decoded);
3168
+ const waiter = createCanonicalTransactionWaiter(pubsub, transactionPoolStore, hash);
3138
3169
  try {
3139
3170
  let data;
3140
- const wait = waitForCanonicalTransaction(pubsub, transactionPoolStore, hash);
3141
3171
  await globalThis.transactionPoolStore.put(hash, message.encoded);
3142
3172
  this.addPendingNonce(message.decoded.from, message.decoded.nonce);
3143
3173
  try {
@@ -3145,12 +3175,16 @@ class Transaction extends Protocol {
3145
3175
  } catch (publishError) {
3146
3176
  console.warn("peernet publish failed: add-transaction", publishError?.message ?? publishError);
3147
3177
  }
3148
- const fee = await calculateFee(message.decoded);
3149
- return { hash, data, fee, wait, message };
3178
+ return { hash, data, fee, wait: waiter.wait, message };
3150
3179
  } catch (error) {
3151
- console.log("remo");
3152
- await transactionPoolStore.delete(hash);
3153
- this.removePendingNonce(message.decoded.from, message.decoded.nonce);
3180
+ waiter.cancel(error);
3181
+ try {
3182
+ await transactionPoolStore.delete(hash);
3183
+ } catch (cleanupError) {
3184
+ console.warn("failed to clean up rejected transaction:", cleanupError);
3185
+ } finally {
3186
+ this.removePendingNonce(message.decoded.from, message.decoded.nonce);
3187
+ }
3154
3188
  throw error;
3155
3189
  }
3156
3190
  }
@@ -3478,120 +3512,541 @@ class Contract extends Transaction {
3478
3512
  }
3479
3513
 
3480
3514
  class EasyWorker {
3481
- #messageEvent = 'message';
3482
- #errorEvent = 'error';
3483
- #isBrowser = false;
3484
- #isWorker = false;
3485
- #messageHandler = null;
3486
- #errorHandler = null;
3487
- worker;
3488
- get isWorker() {
3489
- return this.#isWorker;
3490
- }
3491
- constructor(url, options) {
3492
- return this.#init(url, options);
3493
- }
3494
- #init(url, options = {}) {
3495
- if (url) {
3496
- if (globalThis.Worker) {
3497
- this.#isBrowser = true;
3498
- this.worker = new Worker(url, { ...options });
3499
- }
3500
- else {
3501
- return new Promise((resolve, reject) => {
3502
- import('child_process').then(({ fork }) => {
3503
- const child = fork(url, ['easy-worker-child'], options);
3504
- child.once('error', reject);
3505
- this.worker = child;
3506
- resolve(this);
3507
- }).catch(reject);
3508
- });
3509
- }
3510
- }
3511
- else {
3512
- this.#isWorker = true;
3513
- if (globalThis.process?.argv[2] === 'easy-worker-child') {
3514
- this.worker = process;
3515
- }
3516
- else {
3517
- this.#isBrowser = true;
3518
- this.worker = globalThis;
3519
- }
3520
- }
3521
- return this;
3515
+ #messageEvent = "message";
3516
+ #errorEvent = "error";
3517
+ #isBrowser = false;
3518
+ #isWorker = false;
3519
+ #messageHandler = null;
3520
+ #errorHandler = null;
3521
+ worker;
3522
+ get isWorker() {
3523
+ return this.#isWorker;
3524
+ }
3525
+ constructor(url, options) {
3526
+ return this.#init(url, options);
3527
+ }
3528
+ #init(url, options = {}) {
3529
+ if (url) {
3530
+ if (globalThis.Worker) {
3531
+ this.#isBrowser = true;
3532
+ this.worker = new Worker(url, { ...options });
3533
+ } else {
3534
+ return new Promise((resolve, reject) => {
3535
+ import('child_process').then(({ fork }) => {
3536
+ const child = fork(url, ["easy-worker-child"], options);
3537
+ child.once("error", reject);
3538
+ this.worker = child;
3539
+ resolve(this);
3540
+ }).catch(reject);
3541
+ });
3542
+ }
3543
+ } else {
3544
+ this.#isWorker = true;
3545
+ if (globalThis.process?.argv[2] === "easy-worker-child") {
3546
+ this.worker = process;
3547
+ } else {
3548
+ this.#isBrowser = true;
3549
+ this.worker = globalThis;
3550
+ }
3522
3551
  }
3523
- onmessage(fn) {
3524
- if (this.#isBrowser) {
3525
- this.worker.onmessage = ({ data }) => fn(data);
3526
- }
3527
- else {
3528
- if (this.#messageHandler)
3529
- this.worker.off(this.#messageEvent, this.#messageHandler);
3530
- this.#messageHandler = fn;
3531
- this.worker.on(this.#messageEvent, fn);
3532
- }
3552
+ return this;
3553
+ }
3554
+ onmessage(fn) {
3555
+ if (this.#isBrowser) {
3556
+ this.worker.onmessage = ({ data }) => fn(data);
3557
+ } else {
3558
+ if (this.#messageHandler)
3559
+ this.worker.off(this.#messageEvent, this.#messageHandler);
3560
+ this.#messageHandler = fn;
3561
+ this.worker.on(this.#messageEvent, fn);
3533
3562
  }
3534
- postMessage(message) {
3535
- if (this.#isBrowser)
3536
- this.worker.postMessage(message);
3537
- else
3538
- this.worker.send(message);
3563
+ }
3564
+ postMessage(message) {
3565
+ if (this.#isBrowser)
3566
+ this.worker.postMessage(message);
3567
+ else
3568
+ this.worker.send(message);
3569
+ }
3570
+ terminate() {
3571
+ if (this.#isBrowser)
3572
+ this.worker.terminate();
3573
+ else
3574
+ this.worker.kill();
3575
+ }
3576
+ onerror(fn) {
3577
+ if (this.#isBrowser) {
3578
+ this.worker.onerror = fn;
3579
+ } else {
3580
+ if (this.#errorHandler)
3581
+ this.worker.off(this.#errorEvent, this.#errorHandler);
3582
+ this.#errorHandler = fn;
3583
+ this.worker.on(this.#errorEvent, fn);
3539
3584
  }
3540
- terminate() {
3541
- if (this.#isBrowser)
3542
- this.worker.terminate();
3543
- else
3544
- this.worker.kill();
3585
+ }
3586
+ /**
3587
+ *
3588
+ * @param {*} data
3589
+ * @returns {Promise} resolves result onmessage & rejects on error
3590
+ */
3591
+ once(data) {
3592
+ return new Promise((resolve, reject) => {
3593
+ if (this.#isBrowser) {
3594
+ const handleMessage = ({ data: result }) => {
3595
+ this.worker.onerror = null;
3596
+ resolve(result);
3597
+ this.terminate();
3598
+ };
3599
+ const handleError = (error) => {
3600
+ this.worker.onmessage = null;
3601
+ reject(error);
3602
+ this.terminate();
3603
+ };
3604
+ this.worker.onmessage = handleMessage;
3605
+ this.worker.onerror = handleError;
3606
+ } else {
3607
+ const handleMessage = (result) => {
3608
+ this.worker.off(this.#errorEvent, handleError);
3609
+ resolve(result);
3610
+ this.terminate();
3611
+ };
3612
+ const handleError = (error) => {
3613
+ this.worker.off(this.#messageEvent, handleMessage);
3614
+ reject(error);
3615
+ this.terminate();
3616
+ };
3617
+ this.worker.once(this.#messageEvent, handleMessage);
3618
+ this.worker.once(this.#errorEvent, handleError);
3619
+ }
3620
+ this.postMessage(data);
3621
+ });
3622
+ }
3623
+ }
3624
+ const MAX_BIGINT = (1n << 256n) - 1n;
3625
+ function size(value) {
3626
+ if (!Number.isSafeInteger(value) || value < 0)
3627
+ throw new RangeError("Expected an unsigned safe integer");
3628
+ }
3629
+ function bytes(value) {
3630
+ if (!(value instanceof Uint8Array))
3631
+ throw new TypeError("Expected Uint8Array");
3632
+ }
3633
+ function varUintLength(value) {
3634
+ size(value);
3635
+ let length = 1;
3636
+ while (value >= 128) {
3637
+ value = Math.floor(value / 128);
3638
+ length++;
3639
+ }
3640
+ return length;
3641
+ }
3642
+ function varBigIntLength(value) {
3643
+ if (typeof value !== "bigint")
3644
+ throw new TypeError("Expected BigInt");
3645
+ if (value < 0n || value > MAX_BIGINT)
3646
+ throw new RangeError("Expected an unsigned 256-bit integer");
3647
+ let length = 1;
3648
+ while (value >= 0x100000000000000n) {
3649
+ value >>= 56n;
3650
+ length += 8;
3651
+ }
3652
+ while (value >= 128n) {
3653
+ value >>= 7n;
3654
+ length++;
3655
+ }
3656
+ return length;
3657
+ }
3658
+ class BinaryReader {
3659
+ #buffer;
3660
+ #position = 0;
3661
+ constructor(buffer) {
3662
+ bytes(buffer);
3663
+ this.#buffer = buffer;
3664
+ }
3665
+ get position() {
3666
+ return this.#position;
3667
+ }
3668
+ get remaining() {
3669
+ return this.#buffer.length - this.#position;
3670
+ }
3671
+ readVarUint(canonical = true) {
3672
+ let result = 0;
3673
+ let multiplier = 1;
3674
+ for (let index = 0; index < 8; index++) {
3675
+ const cursor = this.#position + index;
3676
+ if (cursor >= this.#buffer.length)
3677
+ throw new RangeError("Truncated varint");
3678
+ const byte = this.#buffer[cursor];
3679
+ const payload = byte & 127;
3680
+ if (index === 7 && payload > Math.floor((Number.MAX_SAFE_INTEGER - result) / multiplier))
3681
+ throw new RangeError("Varint exceeds safe integer range");
3682
+ result += payload * multiplier;
3683
+ if (byte < 128) {
3684
+ if (canonical && index > 0 && payload === 0)
3685
+ throw new RangeError("Non-canonical varint");
3686
+ this.#position = cursor + 1;
3687
+ return result;
3688
+ }
3689
+ multiplier *= 128;
3690
+ }
3691
+ throw new RangeError("Varint exceeds safe integer range");
3692
+ }
3693
+ readVarBigInt(canonical = true) {
3694
+ let result = 0n;
3695
+ for (let index = 0; index < 37; index++) {
3696
+ const cursor = this.#position + index;
3697
+ if (cursor >= this.#buffer.length)
3698
+ throw new RangeError("Truncated varint");
3699
+ const byte = this.#buffer[cursor];
3700
+ result |= BigInt(byte & 127) << BigInt(index * 7);
3701
+ if (result > MAX_BIGINT)
3702
+ throw new RangeError("Varint exceeds 256 bits");
3703
+ if (byte < 128) {
3704
+ if (canonical && index > 0 && byte === 0)
3705
+ throw new RangeError("Non-canonical varint");
3706
+ this.#position = cursor + 1;
3707
+ return result;
3708
+ }
3709
+ }
3710
+ throw new RangeError("Varint exceeds 256 bits");
3711
+ }
3712
+ readBytes(length) {
3713
+ size(length);
3714
+ if (length > this.remaining)
3715
+ throw new RangeError("Truncated binary field");
3716
+ const start = this.#position;
3717
+ this.#position += length;
3718
+ return this.#buffer.subarray(start, this.#position);
3719
+ }
3720
+ readFrame(canonical = true) {
3721
+ const start = this.#position;
3722
+ try {
3723
+ return this.readBytes(this.readVarUint(canonical));
3724
+ } catch (error) {
3725
+ this.#position = start;
3726
+ throw error;
3545
3727
  }
3546
- onerror(fn) {
3547
- if (this.#isBrowser) {
3548
- this.worker.onerror = fn;
3728
+ }
3729
+ }
3730
+ const MAGIC = new Uint8Array([76, 70, 67, 1]);
3731
+ const utf8 = new TextEncoder();
3732
+ const text$1 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
3733
+ const defaults = { maxBytes: 16 * 1024 * 1024, maxDepth: 64, maxNodes: 2e5 };
3734
+ const BIGINT_LIMB = 1n << 49n;
3735
+ const BIGINT_MASK = BIGINT_LIMB - 1n;
3736
+ const limitsFor = (options) => {
3737
+ const limits = { ...defaults, ...options };
3738
+ for (const value of Object.values(limits))
3739
+ if (!Number.isSafeInteger(value) || value < 1)
3740
+ throw new RangeError("Invalid binary limits");
3741
+ return limits;
3742
+ };
3743
+ function stringLength(value, maximum) {
3744
+ if (value.length > maximum)
3745
+ throw new RangeError("Invalid binary string");
3746
+ let length = value.length;
3747
+ for (let index = 0; index < value.length; index++) {
3748
+ const code = value.charCodeAt(index);
3749
+ if (code < 128)
3750
+ continue;
3751
+ if (code < 2048) {
3752
+ length++;
3753
+ continue;
3754
+ }
3755
+ if (code >= 55296 && code <= 56319) {
3756
+ const low = value.charCodeAt(++index);
3757
+ if (!(low >= 56320 && low <= 57343))
3758
+ throw new RangeError("Invalid binary string");
3759
+ length += 2;
3760
+ } else {
3761
+ if (code >= 56320 && code <= 57343)
3762
+ throw new RangeError("Invalid binary string");
3763
+ length += 2;
3764
+ }
3765
+ }
3766
+ return length;
3767
+ }
3768
+ function encodeValue(value, options = {}) {
3769
+ return encodeValueWithPrefix(value, EMPTY, options);
3770
+ }
3771
+ const EMPTY = new Uint8Array();
3772
+ function encodeValueWithPrefix(value, prefix, options = {}) {
3773
+ if (!(prefix instanceof Uint8Array))
3774
+ throw new TypeError("Expected Uint8Array prefix");
3775
+ const limits = limitsFor(options);
3776
+ const plan = [];
3777
+ const ancestors = /* @__PURE__ */ new Set();
3778
+ let size2 = MAGIC.length;
3779
+ let nodes = 0;
3780
+ const reserve = (length) => {
3781
+ size2 += length;
3782
+ if (size2 > limits.maxBytes)
3783
+ throw new RangeError("Binary value exceeds byte limit");
3784
+ };
3785
+ const string = (value2, tag) => {
3786
+ const length = stringLength(value2, limits.maxBytes);
3787
+ reserve((tag === 7 ? 1 : 0) + varUintLength(length) + length);
3788
+ plan.push(tag, value2, length);
3789
+ };
3790
+ const visit = (value2, depth) => {
3791
+ if (++nodes > limits.maxNodes || depth > limits.maxDepth)
3792
+ throw new RangeError("Binary value exceeds complexity limit");
3793
+ if (value2 === void 0 || value2 === null || typeof value2 === "boolean") {
3794
+ reserve(1);
3795
+ plan.push(value2 === void 0 ? 0 : value2 === null ? 1 : value2 === false ? 2 : 3);
3796
+ return;
3797
+ }
3798
+ if (typeof value2 === "number") {
3799
+ if (!Number.isFinite(value2))
3800
+ throw new TypeError("Binary numbers must be finite");
3801
+ reserve(9);
3802
+ plan.push(4, value2);
3803
+ return;
3804
+ }
3805
+ if (typeof value2 === "bigint") {
3806
+ const magnitude = value2 < 0n ? -value2 : value2;
3807
+ reserve(1 + varBigIntLength(magnitude));
3808
+ plan.push(value2 < 0n ? 6 : 5, magnitude);
3809
+ return;
3810
+ }
3811
+ if (typeof value2 === "string")
3812
+ return string(value2, 7);
3813
+ if (value2 instanceof Uint8Array) {
3814
+ reserve(1 + varUintLength(value2.length) + value2.length);
3815
+ plan.push(8, value2, value2.length);
3816
+ return;
3817
+ }
3818
+ if (typeof value2 !== "object")
3819
+ throw new TypeError("Unsupported binary value");
3820
+ if (ancestors.has(value2))
3821
+ throw new TypeError("Cyclic binary value");
3822
+ ancestors.add(value2);
3823
+ try {
3824
+ if (Array.isArray(value2)) {
3825
+ if (value2.length > limits.maxNodes - nodes)
3826
+ throw new RangeError("Binary array exceeds node limit");
3827
+ if (Object.keys(value2).length !== value2.length || Object.getOwnPropertySymbols(value2).length)
3828
+ throw new TypeError("Binary arrays cannot have extra properties or holes");
3829
+ reserve(1 + varUintLength(value2.length));
3830
+ plan.push(9, value2.length);
3831
+ for (let index = 0; index < value2.length; index++) {
3832
+ const entry = Object.getOwnPropertyDescriptor(value2, String(index));
3833
+ if (!entry || !("value" in entry))
3834
+ throw new TypeError("Sparse arrays and accessors are not binary values");
3835
+ visit(entry.value, depth + 1);
3549
3836
  }
3550
- else {
3551
- if (this.#errorHandler)
3552
- this.worker.off(this.#errorEvent, this.#errorHandler);
3553
- this.#errorHandler = fn;
3554
- this.worker.on(this.#errorEvent, fn);
3837
+ } else {
3838
+ const prototype = Object.getPrototypeOf(value2);
3839
+ if (prototype !== Object.prototype && prototype !== null)
3840
+ throw new TypeError("Binary maps must be plain objects");
3841
+ const keys = Object.keys(value2).sort();
3842
+ if (Object.getOwnPropertySymbols(value2).length)
3843
+ throw new TypeError("Symbol keys are not binary values");
3844
+ if (keys.length > limits.maxNodes - nodes)
3845
+ throw new RangeError("Binary map exceeds node limit");
3846
+ reserve(1 + varUintLength(keys.length));
3847
+ plan.push(10, keys.length);
3848
+ for (const key of keys) {
3849
+ const entry = Object.getOwnPropertyDescriptor(value2, key);
3850
+ if (!("value" in entry))
3851
+ throw new TypeError("Accessors are not binary values");
3852
+ string(key, 11);
3853
+ visit(entry.value, depth + 1);
3555
3854
  }
3855
+ }
3856
+ } finally {
3857
+ ancestors.delete(value2);
3556
3858
  }
3557
- /**
3558
- *
3559
- * @param {*} data
3560
- * @returns {Promise} resolves result onmessage & rejects on error
3561
- */
3562
- once(data) {
3563
- return new Promise((resolve, reject) => {
3564
- if (this.#isBrowser) {
3565
- const handleMessage = ({ data: result }) => {
3566
- this.worker.onerror = null;
3567
- resolve(result);
3568
- this.terminate();
3569
- };
3570
- const handleError = (error) => {
3571
- this.worker.onmessage = null;
3572
- reject(error);
3573
- this.terminate();
3574
- };
3575
- this.worker.onmessage = handleMessage;
3576
- this.worker.onerror = handleError;
3577
- }
3578
- else {
3579
- const handleMessage = (result) => {
3580
- this.worker.off(this.#errorEvent, handleError);
3581
- resolve(result);
3582
- this.terminate();
3583
- };
3584
- const handleError = (error) => {
3585
- this.worker.off(this.#messageEvent, handleMessage);
3586
- reject(error);
3587
- this.terminate();
3588
- };
3589
- this.worker.once(this.#messageEvent, handleMessage);
3590
- this.worker.once(this.#errorEvent, handleError);
3591
- }
3592
- this.postMessage(data);
3593
- });
3859
+ };
3860
+ visit(value, 0);
3861
+ const capacity = prefix.length + size2;
3862
+ if (!Number.isSafeInteger(capacity))
3863
+ throw new RangeError("Binary output is too large");
3864
+ const output = new Uint8Array(capacity);
3865
+ output.set(prefix);
3866
+ output.set(MAGIC, prefix.length);
3867
+ let position = prefix.length + MAGIC.length;
3868
+ let view;
3869
+ const uint = (value2) => {
3870
+ while (value2 >= 128) {
3871
+ output[position++] = value2 % 128 | 128;
3872
+ value2 = Math.floor(value2 / 128);
3873
+ }
3874
+ output[position++] = value2;
3875
+ };
3876
+ for (let cursor = 0; cursor < plan.length; ) {
3877
+ const tag = plan[cursor++];
3878
+ if (tag !== 11)
3879
+ output[position++] = tag;
3880
+ if (tag < 4)
3881
+ continue;
3882
+ if (tag === 4) {
3883
+ view ??= new DataView(output.buffer);
3884
+ view.setFloat64(position, plan[cursor++], false);
3885
+ position += 8;
3886
+ } else if (tag === 5 || tag === 6) {
3887
+ let value2 = plan[cursor++];
3888
+ while (value2 >= BIGINT_LIMB) {
3889
+ let limb = Number(value2 & BIGINT_MASK);
3890
+ for (let index = 0; index < 7; index++) {
3891
+ output[position++] = limb % 128 | 128;
3892
+ limb = Math.floor(limb / 128);
3893
+ }
3894
+ value2 >>= 49n;
3895
+ }
3896
+ uint(Number(value2));
3897
+ } else if (tag === 7 || tag === 11) {
3898
+ const value2 = plan[cursor++];
3899
+ const length = plan[cursor++];
3900
+ uint(length);
3901
+ if (length === value2.length) {
3902
+ for (let index = 0; index < length; index++)
3903
+ output[position++] = value2.charCodeAt(index);
3904
+ } else {
3905
+ const result = utf8.encodeInto(value2, output.subarray(position, position + length));
3906
+ if (result.read !== value2.length || result.written !== length)
3907
+ throw new Error("UTF-8 planning mismatch");
3908
+ position += length;
3909
+ }
3910
+ } else if (tag === 8) {
3911
+ const value2 = plan[cursor++];
3912
+ const length = plan[cursor++];
3913
+ if (value2.length !== length)
3914
+ throw new RangeError("Byte input changed while encoding");
3915
+ uint(length);
3916
+ output.set(value2, position);
3917
+ position += length;
3918
+ } else {
3919
+ uint(plan[cursor++]);
3920
+ }
3921
+ }
3922
+ if (position !== capacity)
3923
+ throw new Error("Binary planning mismatch");
3924
+ return output;
3925
+ }
3926
+ function decodeValue(input, options = {}) {
3927
+ const limits = limitsFor(options);
3928
+ if (!(input instanceof Uint8Array))
3929
+ throw new TypeError("Wire payload must be Uint8Array");
3930
+ if (input.length > limits.maxBytes)
3931
+ throw new RangeError("Binary value exceeds byte limit");
3932
+ const reader = new BinaryReader(input);
3933
+ if (reader.readBytes(MAGIC.length).some((byte, index) => byte !== MAGIC[index]))
3934
+ throw new TypeError("Unsupported binary wire version");
3935
+ let nodes = 0;
3936
+ const string = () => text$1.decode(reader.readFrame());
3937
+ const read = (depth) => {
3938
+ if (++nodes > limits.maxNodes || depth > limits.maxDepth)
3939
+ throw new RangeError("Binary value exceeds complexity limit");
3940
+ const tag = reader.readBytes(1)[0];
3941
+ if (tag === 0)
3942
+ return void 0;
3943
+ if (tag === 1)
3944
+ return null;
3945
+ if (tag === 2)
3946
+ return false;
3947
+ if (tag === 3)
3948
+ return true;
3949
+ if (tag === 4) {
3950
+ const bytes2 = reader.readBytes(8);
3951
+ const value2 = new DataView(bytes2.buffer, bytes2.byteOffset, 8).getFloat64(0, false);
3952
+ if (!Number.isFinite(value2))
3953
+ throw new TypeError("Binary numbers must be finite");
3954
+ return value2;
3955
+ }
3956
+ if (tag === 5 || tag === 6) {
3957
+ const value2 = reader.readVarBigInt();
3958
+ if (tag === 6 && value2 === 0n)
3959
+ throw new TypeError("Non-canonical negative zero");
3960
+ return tag === 6 ? -value2 : value2;
3961
+ }
3962
+ if (tag === 7)
3963
+ return string();
3964
+ if (tag === 8)
3965
+ return new Uint8Array(reader.readFrame());
3966
+ if (tag === 9 || tag === 10) {
3967
+ const count = reader.readVarUint();
3968
+ if (count > limits.maxNodes - nodes || count > reader.remaining)
3969
+ throw new RangeError("Invalid binary collection size");
3970
+ if (tag === 9)
3971
+ return Array.from({ length: count }, () => read(depth + 1));
3972
+ const result = {};
3973
+ let previous;
3974
+ for (let index = 0; index < count; index++) {
3975
+ const key = string();
3976
+ if (previous !== void 0 && key <= previous)
3977
+ throw new TypeError("Non-canonical binary map keys");
3978
+ previous = key;
3979
+ Object.defineProperty(result, key, { value: read(depth + 1), enumerable: true, writable: true, configurable: true });
3980
+ }
3981
+ return result;
3982
+ }
3983
+ throw new TypeError("Unknown binary value tag");
3984
+ };
3985
+ const value = read(0);
3986
+ if (reader.remaining)
3987
+ throw new TypeError("Trailing binary data");
3988
+ return value;
3989
+ }
3990
+ const encodeWire = (channel, value, limits) => encodeValue([channel, value], limits);
3991
+ function decodeWire(channel, bytes2, limits) {
3992
+ const envelope = decodeValue(bytes2, limits);
3993
+ if (!Array.isArray(envelope) || envelope.length !== 2 || envelope[0] !== channel)
3994
+ throw new TypeError("Wrong binary message channel");
3995
+ return envelope[1];
3996
+ }
3997
+ class BinaryWorker {
3998
+ #transport;
3999
+ #errorHandler;
4000
+ constructor(url, options = {}) {
4001
+ const transport = new EasyWorker(url, { ...options, serialization: "advanced" });
4002
+ if (transport && typeof transport.then === "function")
4003
+ return transport.then((value) => {
4004
+ this.#transport = value;
4005
+ this.worker = value.worker;
4006
+ return this;
4007
+ });
4008
+ this.#transport = transport;
4009
+ this.worker = transport.worker;
4010
+ }
4011
+ postMessage(value) {
4012
+ this.#transport.postMessage(encodeWire("worker", value));
4013
+ }
4014
+ onmessage(handler) {
4015
+ this.#transport.onmessage((bytes2) => {
4016
+ let value;
4017
+ try {
4018
+ value = decodeWire("worker", bytes2);
4019
+ } catch (error) {
4020
+ if (this.#errorHandler)
4021
+ this.#errorHandler(error);
4022
+ else
4023
+ throw error;
4024
+ return;
4025
+ }
4026
+ handler(value);
4027
+ });
4028
+ }
4029
+ onerror(handler) {
4030
+ this.#errorHandler = handler;
4031
+ this.#transport.onerror(handler);
4032
+ }
4033
+ terminate() {
4034
+ return this.#transport.terminate();
4035
+ }
4036
+ async once(value) {
4037
+ let timer;
4038
+ try {
4039
+ return await new Promise((resolve, reject) => {
4040
+ timer = setTimeout(() => reject(new Error("Binary worker request timed out")), 1e4);
4041
+ this.onerror(reject);
4042
+ this.onmessage(resolve);
4043
+ this.postMessage(value);
4044
+ });
4045
+ } finally {
4046
+ clearTimeout(timer);
4047
+ this.terminate();
3594
4048
  }
4049
+ }
3595
4050
  }
3596
4051
 
3597
4052
  class LeofcoinError extends Error {
@@ -3613,90 +4068,412 @@ class ExecutionError extends LeofcoinError {
3613
4068
  class ContractDeploymentError extends LeofcoinError {
3614
4069
  name = "ContractDeploymentError";
3615
4070
  }
3616
- const isResolveError = (error) => error.name === "ResolveError";
3617
- const isExecutionError = (error) => error.name === "ExecutionError";
3618
4071
 
3619
- const debug$2 = createDebugger("leofcoin/machine");
3620
- class Machine {
3621
- constructor(blocks) {
3622
- this.states = {
3623
- states: {},
3624
- lastBlock: {
3625
- index: -1,
3626
- hash: ""
3627
- },
3628
- accounts: {},
3629
- info: {
3630
- nativeCalls: BigInt(0),
3631
- nativeMints: BigInt(0),
3632
- nativeBurns: BigInt(0),
3633
- nativeTransfers: BigInt(0),
3634
- totalBurnAmount: BigInt(0),
3635
- totalMintAmount: BigInt(0),
3636
- totalTransferAmount: BigInt(0),
3637
- totalTransactions: BigInt(0),
3638
- totalBlocks: BigInt(0)
3639
- }
3640
- };
3641
- this.wantList = [];
3642
- this.ready = new Promise((resolve) => {
3643
- this.readyResolve = resolve;
3644
- });
3645
- this.init(blocks);
4072
+ const CHAIN_ID_SCHEMA = "leofcoin:chain-identity:v1";
4073
+ const GENESIS_CONTRACT_NAMES = ["contractFactory", "nativeToken", "nameService", "validators"];
4074
+ const assertChainId = (value) => {
4075
+ if (typeof value !== "string" || !/^lfc:[0-9a-f]{64}$/.test(value))
4076
+ throw new Error("Invalid chain ID; expected lfc: followed by 64 lowercase hex characters");
4077
+ return value;
4078
+ };
4079
+ const createChainIdentity = async (network, contracts) => {
4080
+ if (typeof network !== "string" || !/^[a-z0-9][a-z0-9:-]{0,79}$/.test(network))
4081
+ throw new Error("Invalid genesis network namespace");
4082
+ const genesisContracts = {};
4083
+ for (const name of GENESIS_CONTRACT_NAMES) {
4084
+ const hash = contracts?.[name];
4085
+ if (typeof hash !== "string" || !/^[A-Z2-7]{32,128}$/.test(hash))
4086
+ throw new Error(`Invalid genesis contract hash: ${name}`);
4087
+ genesisContracts[name] = hash;
3646
4088
  }
3647
- init(blocks) {
3648
- return this.#init(blocks);
4089
+ const bytes = new TextEncoder().encode(JSON.stringify({ schema: CHAIN_ID_SCHEMA, network, genesisContracts }));
4090
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
4091
+ const chainId = "lfc:" + [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
4092
+ return { schema: CHAIN_ID_SCHEMA, chainId, network, genesisContracts };
4093
+ };
4094
+ const verifyChainIdentity = async (value) => {
4095
+ const identity = value;
4096
+ if (!identity || identity.schema !== CHAIN_ID_SCHEMA)
4097
+ throw new Error("Unsupported chain identity response");
4098
+ const verified = await createChainIdentity(identity.network, identity.genesisContracts);
4099
+ if (verified.chainId !== assertChainId(identity.chainId))
4100
+ throw new Error("Chain ID does not match its genesis contracts");
4101
+ return verified;
4102
+ };
4103
+ const findChainPresence = async (chainId, peerIds, requestIdentity, timeoutMs = 3e3) => {
4104
+ assertChainId(chainId);
4105
+ const peers = [...new Set(peerIds)].slice(0, 16);
4106
+ const matches = [];
4107
+ let responded = 0;
4108
+ let unavailable = 0;
4109
+ await Promise.all(peers.map(async (peerId) => {
4110
+ let timer;
4111
+ try {
4112
+ const result = await Promise.race([
4113
+ Promise.resolve().then(() => requestIdentity(peerId)),
4114
+ new Promise((_, reject) => {
4115
+ timer = setTimeout(() => reject(new Error("Chain identity request timed out")), timeoutMs);
4116
+ })
4117
+ ]);
4118
+ const identity = await verifyChainIdentity(result);
4119
+ responded++;
4120
+ if (identity.chainId === chainId)
4121
+ matches.push(peerId);
4122
+ } catch {
4123
+ unavailable++;
4124
+ } finally {
4125
+ clearTimeout(timer);
4126
+ }
4127
+ }));
4128
+ return {
4129
+ chainId,
4130
+ status: matches.length ? "found" : !peers.length || unavailable ? "unknown" : "not-observed",
4131
+ checkedPeers: peers.length,
4132
+ respondedPeers: responded,
4133
+ unavailablePeers: unavailable,
4134
+ matchingPeers: matches.sort(),
4135
+ scope: "reachable peers only; offline or undiscovered chains may still exist"
4136
+ };
4137
+ };
4138
+
4139
+ const validateChainLink = (localTip, incoming) => {
4140
+ const localHash = localTip?.hash || "0x0";
4141
+ const localIndex = Number(localTip?.index ?? -1);
4142
+ const hasCanonicalTip = localHash !== "0x0";
4143
+ if (hasCanonicalTip && incoming.index < localIndex) return "stale";
4144
+ if (hasCanonicalTip && incoming.index === localIndex) {
4145
+ if (incoming.hash === localHash) return "duplicate";
4146
+ throw new Error(`Block conflict detected at index ${incoming.index}`);
3649
4147
  }
3650
- async #onmessage(data) {
3651
- switch (data.type) {
3652
- case "transactionLoaded": {
3653
- const { from, nonce, hash } = data.result;
3654
- await transactionPoolStore.has(hash) && await transactionPoolStore.delete(hash);
3655
- try {
3656
- const _nonce = await accountsStore.get(from);
3657
- if (nonce > _nonce) await accountsStore.put(from, nonce);
3658
- } catch (error) {
3659
- await accountsStore.put(from, nonce);
3660
- }
3661
- break;
3662
- }
3663
- case "contractError": {
3664
- console.warn(data.error);
3665
- console.warn(`contract error: ${data.hash}`);
3666
- await contractStore.delete(data.hash);
3667
- break;
3668
- }
3669
- case "initError": {
3670
- console.error(`init error: ${data.message}`);
3671
- break;
3672
- }
3673
- case "executeError": {
3674
- pubsub.publish(data.id, { error: data.message });
3675
- break;
3676
- }
3677
- case "addLoadedBlockError":
3678
- case "collectFeeError":
3679
- case "settleRewardsError": {
3680
- pubsub.publish(data.id, { error: new Error(data.message) });
3681
- break;
3682
- }
3683
- case "debug": {
3684
- if (data.message.includes("loaded transactions for block:")) {
3685
- pubsub.publish("block-loaded", data.message.replace("loaded transactions for block: ", "").split(" @")[0]);
4148
+ const expectedIndex = hasCanonicalTip ? localIndex + 1 : 0;
4149
+ if (incoming.index !== expectedIndex) {
4150
+ throw new Error(`Unexpected block index ${incoming.index}; expected ${expectedIndex}`);
4151
+ }
4152
+ if (incoming.previousHash !== localHash) {
4153
+ throw new Error(
4154
+ `previousHash mismatch at index ${incoming.index}: expected ${localHash}, got ${incoming.previousHash}`
4155
+ );
4156
+ }
4157
+ return "append";
4158
+ };
4159
+
4160
+ const CHECKPOINT_KEYS = ["checkpoint:0", "checkpoint:1"];
4161
+ const counters = [
4162
+ "nativeCalls",
4163
+ "nativeMints",
4164
+ "nativeBurns",
4165
+ "nativeTransfers",
4166
+ "totalBurnAmount",
4167
+ "totalMintAmount",
4168
+ "totalTransferAmount",
4169
+ "totalTransactions",
4170
+ "totalBlocks"
4171
+ ];
4172
+ const record = (value) => value && typeof value === "object" && !Array.isArray(value);
4173
+ const text = (value) => typeof value === "string" ? value : new TextDecoder("utf-8", { fatal: true }).decode(value);
4174
+ const digest = async (value) => [...new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value)))].map((byte) => byte.toString(16).padStart(2, "0")).join("");
4175
+ function validateCheckpoint(snapshot, chainId, requiredContracts) {
4176
+ if (!record(snapshot) || snapshot.chainId !== chainId || !record(snapshot.states) || !record(snapshot.accounts) || !record(snapshot.info))
4177
+ throw new Error("Invalid checkpoint or chain identity");
4178
+ const tip = snapshot.lastBlock;
4179
+ const index = Number(tip?.index);
4180
+ if (!["bigint", "number"].includes(typeof tip?.index) || !Number.isSafeInteger(index) || index < -1 || (index === -1 ? tip.hash !== "0x0" : typeof tip.hash !== "string" || !tip.hash || tip.hash === "0x0"))
4181
+ throw new Error("Invalid checkpoint tip");
4182
+ for (const contract of requiredContracts) if (!record(snapshot.states[contract])) throw new Error(`Incomplete checkpoint contract: ${contract}`);
4183
+ for (const [address, nonce] of Object.entries(snapshot.accounts))
4184
+ if (!address || typeof nonce !== "string" || !/^(0|[1-9]\d*)$/.test(nonce)) throw new Error("Invalid checkpoint nonce");
4185
+ for (const counter of counters)
4186
+ if (typeof snapshot.info[counter] !== "bigint" || snapshot.info[counter] < 0n) throw new Error(`Invalid checkpoint counter: ${counter}`);
4187
+ if (snapshot.info.totalBlocks !== BigInt(index) + 1n) throw new Error("Checkpoint block count does not match tip");
4188
+ return snapshot;
4189
+ }
4190
+ async function writeCheckpoint(store, snapshot, generation, requiredContracts) {
4191
+ if (!Number.isSafeInteger(generation) || generation < 0) throw new Error("Invalid checkpoint generation");
4192
+ validateCheckpoint(snapshot, snapshot.chainId, requiredContracts);
4193
+ const payload = JSON.stringify({ generation, snapshot }, jsonStringifyBigInt);
4194
+ await store.put(CHECKPOINT_KEYS[generation % 2], new TextEncoder().encode(JSON.stringify({ schema: 1, payload, checksum: await digest(payload) })));
4195
+ }
4196
+ async function readCheckpoint(store, chainId, requiredContracts) {
4197
+ const valid = [];
4198
+ let present = 0;
4199
+ const keys = new Set(await store.keys());
4200
+ for (const key of CHECKPOINT_KEYS) {
4201
+ if (!keys.has(key)) continue;
4202
+ present++;
4203
+ const bytes = await store.get(key);
4204
+ try {
4205
+ const envelope = JSON.parse(text(bytes));
4206
+ if (envelope.schema !== 1 || typeof envelope.payload !== "string" || envelope.checksum !== await digest(envelope.payload)) throw new Error("Invalid checkpoint checksum");
4207
+ const result = JSON.parse(envelope.payload, jsonParseBigInt);
4208
+ if (!Number.isSafeInteger(result.generation) || result.generation < 0 || result.generation % 2 !== CHECKPOINT_KEYS.indexOf(key)) throw new Error("Invalid checkpoint generation");
4209
+ validateCheckpoint(result.snapshot, chainId, requiredContracts);
4210
+ valid.push(result);
4211
+ } catch {
4212
+ }
4213
+ }
4214
+ if (present && !valid.length) throw new Error("No valid state checkpoint remains; preserve this data root and rebuild in a separate root");
4215
+ return valid.sort((left, right) => right.generation - left.generation)[0];
4216
+ }
4217
+ async function recoveryHistory(hash, readBlock) {
4218
+ const reversed = [];
4219
+ const seen = /* @__PURE__ */ new Set();
4220
+ while (hash !== "0x0") {
4221
+ if (!hash || seen.has(hash)) throw new Error("Invalid or cyclic recovery ancestry");
4222
+ seen.add(hash);
4223
+ const bytes = await readBlock(hash);
4224
+ if (!bytes) throw new Error(`Recovery block unavailable: ${hash}`);
4225
+ const message = new BlockMessage(bytes);
4226
+ if (await message.hash() !== hash) throw new Error(`Recovery block hash mismatch: ${hash}`);
4227
+ reversed.push({ ...message.decoded, hash, loaded: false });
4228
+ hash = message.decoded.previousHash;
4229
+ }
4230
+ const history = reversed.reverse();
4231
+ let tip = { index: -1, hash: "0x0" };
4232
+ for (const block of history) {
4233
+ const index = Number(block.index);
4234
+ if (!Number.isSafeInteger(index) || index < 0) throw new Error("Invalid recovery block index");
4235
+ if (validateChainLink(tip, { index, hash: block.hash, previousHash: block.previousHash }) !== "append") throw new Error("Invalid recovery chain link");
4236
+ tip = { index, hash: block.hash };
4237
+ }
4238
+ return history;
4239
+ }
4240
+ async function restoreAccountNonces(store, accounts) {
4241
+ await store.clear();
4242
+ for (const [address, nonce] of Object.entries(accounts)) await store.put(address, String(nonce));
4243
+ }
4244
+
4245
+ class ReadOnlyMachine {
4246
+ constructor(workerPath, readContract) {
4247
+ this.workerPath = workerPath;
4248
+ this.readContract = readContract;
4249
+ }
4250
+ #worker;
4251
+ #pending = /* @__PURE__ */ new Map();
4252
+ #tail = Promise.resolve();
4253
+ #queued = 0;
4254
+ #seed;
4255
+ #cache = /* @__PURE__ */ new Map();
4256
+ #cacheBytes = 0;
4257
+ #closed = false;
4258
+ read(seed, contract, method, params = [], sender = "leofcoin:read-only") {
4259
+ if (this.#closed) return Promise.reject(new Error("Read-only machine is closed"));
4260
+ if (typeof contract !== "string" || !contract || typeof method !== "string" || !method || ["constructor", "__proto__", "prototype"].includes(method) || !Array.isArray(params))
4261
+ return Promise.reject(new Error("Invalid read-only contract query"));
4262
+ if (this.#queued >= 32) return Promise.reject(new Error("Read-only query queue is full"));
4263
+ let key;
4264
+ try {
4265
+ key = JSON.stringify({ contract, method, params, sender }, jsonStringifyBigInt);
4266
+ if (new TextEncoder().encode(key).byteLength > 65536) throw new Error("Read-only query is too large");
4267
+ params = structuredClone(params);
4268
+ } catch (error) {
4269
+ return Promise.reject(error);
4270
+ }
4271
+ this.#queued++;
4272
+ const result = this.#tail.then(async () => {
4273
+ if (this.#closed) throw new Error("Read-only machine is closed");
4274
+ if (this.#seed !== seed) {
4275
+ this.#seed = seed;
4276
+ this.#cache.clear();
4277
+ this.#cacheBytes = 0;
4278
+ }
4279
+ if (this.#cache.has(key)) return structuredClone(this.#cache.get(key).value);
4280
+ try {
4281
+ await this.#start();
4282
+ const initialized = await this.#request("init", {
4283
+ readOnly: true,
4284
+ fromState: true,
4285
+ blocks: [],
4286
+ peerid: "leofcoin:read-only",
4287
+ lastBlock: seed.lastBlock,
4288
+ state: seed.states,
4289
+ info: seed.info
4290
+ });
4291
+ if (initialized !== true) throw new Error("Worker does not support isolated contract queries");
4292
+ const value = await this.#request("read", { contract, method, params, sender });
4293
+ const bytes = new TextEncoder().encode(JSON.stringify(value, jsonStringifyBigInt) ?? "").byteLength;
4294
+ if (bytes > 4 * 1024 * 1024) throw new Error("Read-only result is too large");
4295
+ while (this.#cache.size && (this.#cache.size >= 64 || this.#cacheBytes + bytes > 4 * 1024 * 1024)) {
4296
+ const oldest = this.#cache.keys().next().value;
4297
+ this.#cacheBytes -= this.#cache.get(oldest).bytes;
4298
+ this.#cache.delete(oldest);
4299
+ }
4300
+ this.#cache.set(key, { value: structuredClone(value), bytes });
4301
+ this.#cacheBytes += bytes;
4302
+ return value;
4303
+ } catch (error) {
4304
+ this.#stop();
4305
+ throw error;
4306
+ }
4307
+ });
4308
+ this.#tail = result.then(() => void 0, () => void 0);
4309
+ return result.finally(() => {
4310
+ this.#queued--;
4311
+ });
4312
+ }
4313
+ async #start() {
4314
+ if (this.#worker) return;
4315
+ const worker = await new BinaryWorker(this.workerPath, { serialization: "advanced", type: "module" });
4316
+ if (this.#closed) {
4317
+ worker.terminate();
4318
+ throw new Error("Read-only machine is closed");
4319
+ }
4320
+ this.#worker = worker;
4321
+ worker.onmessage(async (message) => {
4322
+ if (this.#worker !== worker) return;
4323
+ if (message.type === "ask") {
4324
+ try {
4325
+ const input = message.question === "peers" ? [] : message.question === "contract" ? await this.readContract(message.input) : void 0;
4326
+ if (this.#worker === worker) worker.postMessage({ id: message.id, input });
4327
+ } catch {
4328
+ if (this.#worker === worker) worker.postMessage({ id: message.id, input: void 0 });
3686
4329
  }
4330
+ return;
4331
+ }
4332
+ const pending = this.#pending.get(message.id);
4333
+ if (!pending) return;
4334
+ if (message.type !== "response" && !["initError", "readError"].includes(message.type)) return;
4335
+ clearTimeout(pending.timer);
4336
+ this.#pending.delete(message.id);
4337
+ if (message.type === "response") pending.resolve(message.value);
4338
+ else pending.reject(new Error(message.message || "Read-only worker failed"));
4339
+ });
4340
+ worker.onerror(() => {
4341
+ if (this.#worker === worker) this.#stop();
4342
+ });
4343
+ worker.worker?.once?.("exit", () => {
4344
+ if (this.#worker === worker) this.#stop();
4345
+ });
4346
+ }
4347
+ #request(type, input) {
4348
+ return new Promise((resolve, reject) => {
4349
+ const id = crypto.randomUUID();
4350
+ const timer = setTimeout(() => this.#stop(new Error("Read-only query timed out")), 1e4);
4351
+ this.#pending.set(id, { resolve, reject, timer });
4352
+ try {
4353
+ this.#worker.postMessage({ id, type, input });
4354
+ } catch (error) {
4355
+ this.#stop(error);
4356
+ }
4357
+ });
4358
+ }
4359
+ #stop(error = new Error("Read-only worker stopped")) {
4360
+ const worker = this.#worker;
4361
+ this.#worker = void 0;
4362
+ for (const pending of this.#pending.values()) {
4363
+ clearTimeout(pending.timer);
4364
+ pending.reject(error);
4365
+ }
4366
+ this.#pending.clear();
4367
+ worker?.terminate();
4368
+ }
4369
+ close() {
4370
+ this.#closed = true;
4371
+ this.#cache.clear();
4372
+ this.#cacheBytes = 0;
4373
+ this.#seed = void 0;
4374
+ this.#stop();
4375
+ }
4376
+ }
4377
+
4378
+ const genesisContracts = {
4379
+ contractFactory: addresses.contractFactory,
4380
+ nativeToken: addresses.nativeToken,
4381
+ nameService: addresses.nameService,
4382
+ validators: addresses.validators
4383
+ };
4384
+ const debug$2 = createDebugger("leofcoin/machine");
4385
+ class Machine {
4386
+ constructor(blocks = [], checkpoint) {
4387
+ this.states = {
4388
+ states: {},
4389
+ lastBlock: {
4390
+ index: -1,
4391
+ hash: "0x0"
4392
+ },
4393
+ accounts: {},
4394
+ info: {
4395
+ nativeCalls: BigInt(0),
4396
+ nativeMints: BigInt(0),
4397
+ nativeBurns: BigInt(0),
4398
+ nativeTransfers: BigInt(0),
4399
+ totalBurnAmount: BigInt(0),
4400
+ totalMintAmount: BigInt(0),
4401
+ totalTransferAmount: BigInt(0),
4402
+ totalTransactions: BigInt(0),
4403
+ totalBlocks: BigInt(0)
4404
+ }
4405
+ };
4406
+ this.wantList = [];
4407
+ this.ready = new Promise((resolve, reject) => {
4408
+ this.readyResolve = resolve;
4409
+ this.readyReject = reject;
4410
+ });
4411
+ this.#checkpointGeneration = -1;
4412
+ this.#closed = false;
4413
+ this.init(blocks, checkpoint).then(() => this.readyResolve(this), (error) => {
4414
+ this.close();
4415
+ this.readyReject(error);
4416
+ });
4417
+ }
4418
+ #checkpointGeneration;
4419
+ #chainId;
4420
+ #reader;
4421
+ #contractReader;
4422
+ #readSnapshot;
4423
+ #workerPath;
4424
+ #closed;
4425
+ close() {
4426
+ this.#closed = true;
4427
+ this.#reader?.close();
4428
+ this.#contractReader?.close();
4429
+ this.worker?.terminate();
4430
+ }
4431
+ async init(blocks, checkpoint) {
4432
+ return this.#init(blocks, checkpoint);
4433
+ }
4434
+ async #onmessage(data) {
4435
+ if (this.#closed) return;
4436
+ switch (data.type) {
4437
+ case "contractError": {
4438
+ console.warn(data.message ?? data.error);
4439
+ console.warn(`contract error: ${data.hash}`);
3687
4440
  break;
3688
4441
  }
3689
- case "error": {
3690
- console.error(data.message);
4442
+ case "initError": {
4443
+ pubsub.publish(data.id, { error: new Error(`Machine initialization failed: ${data.message}`) });
4444
+ break;
4445
+ }
4446
+ case "executeError": {
4447
+ pubsub.publish(data.id, { error: data.message });
4448
+ break;
4449
+ }
4450
+ case "addLoadedBlockError":
4451
+ case "runContractError":
4452
+ case "collectFeeError":
4453
+ case "settleRewardsError": {
4454
+ pubsub.publish(data.id, { error: new Error(data.message) });
3691
4455
  break;
3692
4456
  }
3693
- case "machine-ready": {
3694
- this.readyResolve(this);
3695
- pubsub.publish("machine.ready", true);
4457
+ case "snapshotError": {
4458
+ pubsub.publish(data.id, { error: new Error(data.message) });
4459
+ break;
4460
+ }
4461
+ case "getError": {
4462
+ pubsub.publish(data.id, { error: new Error(data.message) });
4463
+ break;
4464
+ }
4465
+ case "debug": {
4466
+ if (data.message.includes("loaded transactions for block:")) {
4467
+ pubsub.publish("block-loaded", data.message.replace("loaded transactions for block: ", "").split(" @")[0]);
4468
+ }
4469
+ break;
4470
+ }
4471
+ case "error": {
4472
+ console.error(data.message);
3696
4473
  break;
3697
4474
  }
3698
4475
  case "response": {
3699
- pubsub.publish(data.id, data.value || false);
4476
+ pubsub.publish(data.id, data.value ?? false);
3700
4477
  break;
3701
4478
  }
3702
4479
  case "addToWantList": {
@@ -3704,6 +4481,16 @@ class Machine {
3704
4481
  break;
3705
4482
  }
3706
4483
  case "ask": {
4484
+ if (data.question === "staticCall") {
4485
+ try {
4486
+ const { snapshot, contract, method, params, sender } = data.input;
4487
+ const input = await this.#contractReader.read(snapshot, contract, method, params, sender);
4488
+ if (!this.#closed) this.worker.postMessage({ id: data.id, input });
4489
+ } catch (error) {
4490
+ if (!this.#closed) this.worker.postMessage({ id: data.id, error: error instanceof Error ? error.message : String(error) });
4491
+ }
4492
+ break;
4493
+ }
3707
4494
  if (data.question === "contract" || data.question === "transaction") {
3708
4495
  try {
3709
4496
  const input = await peernet.get(data.input, data.question);
@@ -3731,164 +4518,61 @@ class Machine {
3731
4518
  }
3732
4519
  async updateState(strict = false) {
3733
4520
  try {
3734
- const lastBlock = strict ? await this.getCanonicalTip() : await this.lastBlock;
3735
- const lastBlockIndex = Number(lastBlock.index);
3736
- const statesLastBlockIndex = Number(this.states.lastBlock.index);
3737
- if (lastBlockIndex > statesLastBlockIndex || lastBlock.hash !== this.states.lastBlock.hash && Number(lastBlockIndex) === 0) {
3738
- const blocks = (await this.blocks).slice(statesLastBlockIndex);
3739
- let transactions = [];
3740
- for (const block of blocks) {
3741
- if (block?.transactions) transactions = [...transactions, ...block.transactions];
3742
- }
3743
- transactions = await Promise.all(
3744
- transactions.map(async (transaction) => new TransactionMessage(await transactionStore.get(transaction)))
3745
- );
3746
- const contractsToGet = transactions.reduce((set, current) => {
3747
- const contract = current.decoded.to;
3748
- if (!set.includes(contract)) set.push(contract);
3749
- return set;
3750
- }, []);
3751
- const state = {};
3752
- if (!contractsToGet.includes(addresses.contractFactory)) contractsToGet.push(addresses.contractFactory);
3753
- if (!contractsToGet.includes(addresses.nativeToken)) contractsToGet.push(addresses.nativeToken);
3754
- if (!contractsToGet.includes(addresses.nameService)) contractsToGet.push(addresses.nameService);
3755
- if (!contractsToGet.includes(addresses.validators)) contractsToGet.push(addresses.validators);
3756
- await Promise.all(
3757
- contractsToGet.map(async (contract) => {
3758
- const value = await this.#askWorker("get", { contract, method: "state", params: [] });
3759
- state[contract] = value;
3760
- })
3761
- );
3762
- let accounts = {};
3763
- try {
3764
- const promises = [];
3765
- for (const address of await accountsStore.keys()) {
3766
- promises.push(
3767
- (async () => {
3768
- accounts[address] = await accountsStore.get(address);
3769
- })()
3770
- );
3771
- }
3772
- await Promise.all(promises);
3773
- } catch (error) {
3774
- const promises = [];
3775
- for (const transaction of transactions.reverse()) {
3776
- const { from, to, amount, nonce } = transaction.decoded.data;
3777
- if (!accounts[from]) {
3778
- accounts[from] = nonce;
3779
- promises.push(accountsStore.put(from, nonce));
3780
- }
3781
- }
3782
- await Promise.all(promises);
3783
- }
3784
- const sortedStringify = (obj, replacer) => {
3785
- const sorted = {};
3786
- const keys = Object.keys(obj).sort();
3787
- for (const key of keys) {
3788
- sorted[key] = obj[key];
3789
- }
3790
- return JSON.stringify(sorted, replacer);
3791
- };
3792
- const tasks = [
3793
- stateStore.put("lastBlock", sortedStringify(lastBlock, jsonStringifyBigInt)),
3794
- stateStore.put("states", sortedStringify(state, jsonStringifyBigInt)),
3795
- stateStore.put("accounts", sortedStringify(accounts, jsonStringifyBigInt)),
3796
- stateStore.put(
3797
- "info",
3798
- sortedStringify(
3799
- {
3800
- nativeBurnAmount: await this.totalBurnAmount,
3801
- nativeBurns: await this.nativeBurns,
3802
- nativeCalls: await this.nativeCalls,
3803
- nativeMints: await this.nativeMints,
3804
- nativeTransfers: await this.nativeTransfers,
3805
- totalBlocks: await blockStore.length,
3806
- totalBurnAmount: await this.totalBurnAmount,
3807
- totalMintAmount: await this.totalMintAmount,
3808
- totalTransactions: await this.totalTransactions,
3809
- totalTransferAmount: await this.totalTransferAmount
3810
- },
3811
- jsonStringifyBigInt
3812
- )
3813
- )
3814
- // accountsStore.clear()
3815
- ];
3816
- await Promise.all(tasks);
4521
+ const snapshot = await this.#askWorker("snapshot");
4522
+ snapshot.chainId = this.#chainId;
4523
+ const accounts = {};
4524
+ for (const address of await accountsStore.keys()) {
4525
+ const raw = await accountsStore.get(address);
4526
+ accounts[address] = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
3817
4527
  }
4528
+ snapshot.accounts = accounts;
4529
+ const generation = this.#checkpointGeneration + 1;
4530
+ await writeCheckpoint(stateStore, snapshot, generation, Object.values(genesisContracts));
4531
+ this.#checkpointGeneration = generation;
4532
+ this.states = snapshot;
4533
+ this.#readSnapshot = structuredClone(snapshot);
3818
4534
  } catch (error) {
3819
- console.error(error);
3820
4535
  if (strict) throw error;
4536
+ console.error(error);
4537
+ throw error;
3821
4538
  }
3822
4539
  }
3823
- async #init(blocks) {
3824
- return new Promise(async (resolve) => {
3825
- const machineReady = async () => {
3826
- pubsub.unsubscribe("machine.ready", machineReady);
3827
- await this.updateState();
3828
- resolve(this);
3829
- };
3830
- pubsub.subscribe("machine.ready", machineReady);
3831
- let pre;
3832
- try {
3833
- const importee = await import('url');
3834
- const url = importee.default;
3835
- if (url) pre = url.fileURLToPath(new URL(".", import.meta.url));
3836
- } catch {
3837
- pre = "./";
3838
- }
3839
- this.worker = await new EasyWorker(pre + "workers/machine-worker.js", {
3840
- serialization: "advanced",
3841
- type: "module"
3842
- });
3843
- this.worker.onmessage(this.#onmessage.bind(this));
3844
- let rawLastBlock;
3845
- let rawStates;
3846
- let rawAccounts;
3847
- let rawInfo;
3848
- if (await stateStore.has("lastBlock")) {
3849
- const decoder = new TextDecoder();
3850
- const decode = (param) => decoder.decode(param);
3851
- rawLastBlock = decode(await stateStore.get("lastBlock"));
3852
- this.states.lastBlock = JSON.parse(rawLastBlock, jsonParseBigInt);
3853
- try {
3854
- rawStates = decode(await stateStore.get("states"));
3855
- rawAccounts = decode(await stateStore.get("accounts"));
3856
- rawInfo = decode(await stateStore.get("info"));
3857
- this.states.states = JSON.parse(rawStates, jsonParseBigInt);
3858
- this.states.accounts = JSON.parse(rawAccounts, jsonParseBigInt);
3859
- this.states.info = JSON.parse(rawInfo, jsonParseBigInt);
3860
- } catch (error) {
3861
- console.error(error);
3862
- this.states.accounts = {};
3863
- this.states.info = {
3864
- nativeCalls: BigInt(0),
3865
- nativeMints: BigInt(0),
3866
- nativeBurns: BigInt(0),
3867
- nativeTransfers: BigInt(0),
3868
- totalBurnAmount: BigInt(0),
3869
- totalMintAmount: BigInt(0),
3870
- totalTransferAmount: BigInt(0),
3871
- totalTransactions: BigInt(0),
3872
- totalBlocks: BigInt(0)
3873
- };
3874
- rawInfo = JSON.stringify(this.states.info, jsonStringifyBigInt);
3875
- await stateStore.put("info", new TextEncoder().encode(rawInfo));
3876
- }
3877
- }
3878
- const message = {
3879
- type: "init",
3880
- input: {
3881
- blocks,
3882
- fromState: this.states.lastBlock?.index >= 0,
3883
- lastBlock: rawLastBlock,
3884
- state: rawStates,
3885
- info: rawInfo,
3886
- // @ts-ignore
3887
- peerid: peernet.peerId
3888
- }
3889
- };
3890
- this.worker.postMessage(message);
4540
+ async #init(blocks, checkpoint) {
4541
+ const identity = await createChainIdentity(globalThis.peernet?.network || "leofcoin:peach", addresses);
4542
+ this.#chainId = identity.chainId;
4543
+ if (checkpoint) {
4544
+ this.#checkpointGeneration = checkpoint.generation;
4545
+ this.states = checkpoint.snapshot;
4546
+ }
4547
+ let pre = "./";
4548
+ try {
4549
+ const { fileURLToPath } = await import('url');
4550
+ pre = fileURLToPath(new URL(".", import.meta.url));
4551
+ } catch {
4552
+ }
4553
+ this.#workerPath = pre + "workers/machine-worker.js";
4554
+ const readContract = (hash) => globalThis.contractStore.get(hash);
4555
+ this.#reader = new ReadOnlyMachine(this.#workerPath, readContract);
4556
+ this.#contractReader = new ReadOnlyMachine(this.#workerPath, readContract);
4557
+ this.worker = await new BinaryWorker(this.#workerPath, {
4558
+ serialization: "advanced",
4559
+ type: "module"
4560
+ });
4561
+ if (this.#closed) {
4562
+ this.worker.terminate();
4563
+ throw new Error("State machine is closed");
4564
+ }
4565
+ this.worker.onmessage(this.#onmessage.bind(this));
4566
+ const initialized = await this.#askWorker("init", {
4567
+ blocks,
4568
+ fromState: Boolean(checkpoint),
4569
+ lastBlock: checkpoint?.snapshot.lastBlock,
4570
+ state: checkpoint?.snapshot.states,
4571
+ info: checkpoint?.snapshot.info,
4572
+ peerid: peernet.peerId
3891
4573
  });
4574
+ if (initialized !== true) throw new Error("Worker does not support checkpoint initialization; deploy matching chain and worker builds");
4575
+ this.#readSnapshot = structuredClone(checkpoint?.snapshot ?? await this.#askWorker("snapshot"));
3892
4576
  }
3893
4577
  async #runContract(contractMessage) {
3894
4578
  const hash = await contractMessage.hash();
@@ -3919,6 +4603,7 @@ class Machine {
3919
4603
  * @returns Promise<message>
3920
4604
  */
3921
4605
  async execute(contract, method, parameters, sender, execution) {
4606
+ if (this.#closed) throw new Error("State machine is closed");
3922
4607
  try {
3923
4608
  if (contract === contractFactory && method === "registerContract") {
3924
4609
  if (await this.has(parameters[0])) throw new Error(`duplicate contract @${parameters[0]}`);
@@ -3975,28 +4660,8 @@ ${error.message}`);
3975
4660
  return this.#askWorker("settleRewards", { rewards });
3976
4661
  }
3977
4662
  get(contract, method, parameters) {
3978
- return new Promise((resolve, reject) => {
3979
- const id = randombytes(20).toString();
3980
- const timeout = setTimeout(() => {
3981
- pubsub.unsubscribe(id, onmessage);
3982
- reject(new Error(`Machine.get timeout for ${contract}.${method}`));
3983
- }, 3e4);
3984
- const onmessage = (message) => {
3985
- clearTimeout(timeout);
3986
- pubsub.unsubscribe(id, onmessage);
3987
- resolve(message);
3988
- };
3989
- pubsub.subscribe(id, onmessage);
3990
- this.worker.postMessage({
3991
- type: "get",
3992
- id,
3993
- input: {
3994
- contract,
3995
- method,
3996
- params: parameters
3997
- }
3998
- });
3999
- });
4663
+ if (!this.#readSnapshot) return Promise.reject(new Error("Contract read state is not initialized"));
4664
+ return this.#reader.read(this.#readSnapshot, contract, method, parameters ?? []);
4000
4665
  }
4001
4666
  async has(address) {
4002
4667
  return new Promise((resolve, reject) => {
@@ -4022,6 +4687,7 @@ ${error.message}`);
4022
4687
  });
4023
4688
  }
4024
4689
  #askWorker(type, input) {
4690
+ if (this.#closed) return Promise.reject(new Error("State machine is closed"));
4025
4691
  return new Promise((resolve, reject) => {
4026
4692
  const id = randombytes(20).toString("hex");
4027
4693
  const timeout = setTimeout(() => {
@@ -4079,19 +4745,13 @@ ${error.message}`);
4079
4745
  return this.getBlocks();
4080
4746
  }
4081
4747
  get lastBlock() {
4082
- return this.#askWorker("lastBlock").catch((error) => {
4083
- debug$2("lastBlock fallback after worker timeout:", error?.message ?? error);
4084
- return this.states.lastBlock;
4085
- });
4748
+ return Promise.resolve(this.states.lastBlock);
4086
4749
  }
4087
4750
  getCanonicalTip() {
4088
4751
  return this.#askWorker("lastBlock");
4089
4752
  }
4090
4753
  get lastBlockHeight() {
4091
- return this.#askWorker("lastBlockHeight").catch((error) => {
4092
- debug$2("lastBlockHeight fallback after worker timeout:", error?.message ?? error);
4093
- return Number(this.states.lastBlock?.index ?? 0);
4094
- });
4754
+ return Promise.resolve(Number(this.states.lastBlock.index));
4095
4755
  }
4096
4756
  getBlocks(from, to) {
4097
4757
  return this.#askWorker("blocks", { from, to });
@@ -4102,7 +4762,7 @@ ${error.message}`);
4102
4762
  async addLoadedBlock(block) {
4103
4763
  debug$2(`adding loaded block: ${block.index}@${block.hash}`);
4104
4764
  if (block.decoded) block = { ...block.decoded, hash: await block.hash() };
4105
- return this.#askWorker("addLoadedBlock", JSON.stringify(block, jsonStringifyBigInt));
4765
+ return this.#askWorker("addLoadedBlock", block);
4106
4766
  }
4107
4767
  async latestTransactions() {
4108
4768
  return this.#askWorker("latestTransactions");
@@ -4150,91 +4810,19 @@ class Jobber {
4150
4810
  }
4151
4811
  }
4152
4812
 
4153
- const MAX_RESPONSE_DEPTH$1 = 3;
4154
- const asLastBlockMessage = async (blockInput, claimedHash) => {
4155
- const block = new BlockMessage(blockInput);
4156
- const hash = await block.hash();
4157
- if (claimedHash && hash !== claimedHash) {
4158
- throw new Error(`lastBlock hash mismatch: expected ${claimedHash}, got ${hash}`);
4159
- }
4160
- return new LastBlockMessage({ hash, index: block.decoded.index });
4161
- };
4162
- const resolveLegacyHash = async (hash) => {
4163
- const blockData = await globalThis.peernet?.get?.(hash, "block");
4164
- if (!blockData) throw new Error(`unable to resolve lastBlock hash: ${hash}`);
4165
- return asLastBlockMessage(blockData, hash);
4166
- };
4167
- const resolveLastBlockMessage = async (result, depth = 0) => {
4168
- if (depth > MAX_RESPONSE_DEPTH$1) throw new Error("lastBlock response nesting exceeds limit");
4169
- if (result instanceof Uint8Array) {
4170
- let codec;
4171
- try {
4172
- codec = new Codec(result);
4173
- } catch {
4174
- }
4175
- if (codec) {
4176
- if (codec.name === "peernet-response") {
4177
- const ResponseMessage = globalThis.peernet?.protos?.["peernet-response"];
4178
- if (!ResponseMessage) throw new Error("peernet-response codec is not registered");
4179
- const wrapped = new ResponseMessage(result);
4180
- if (wrapped?.decoded?.response === void 0) throw new Error("peernet-response has no response payload");
4181
- return resolveLastBlockMessage(wrapped.decoded.response, depth + 1);
4182
- }
4183
- if (codec.name === "last-block-message") return new LastBlockMessage(result);
4184
- if (codec.name === "block-message") return asLastBlockMessage(result);
4185
- }
4186
- const candidate = new TextDecoder().decode(result);
4187
- if (candidate) return resolveLegacyHash(candidate);
4188
- }
4189
- if (typeof result === "string") return resolveLegacyHash(result);
4190
- if (result && typeof result === "object") {
4191
- const response = result.decoded?.response ?? result.response;
4192
- if (response !== void 0) return resolveLastBlockMessage(response, depth + 1);
4193
- if ("hash" in result && "index" in result) return new LastBlockMessage(result);
4194
- if ("previousHash" in result && "index" in result) return asLastBlockMessage(result);
4195
- }
4196
- throw new Error(`invalid lastBlock payload: ${typeof result}`);
4197
- };
4198
-
4199
- const MAX_RESPONSE_DEPTH = 3;
4200
- const decodeResponsePayload = async (result, depth = 0) => {
4201
- if (depth > MAX_RESPONSE_DEPTH) throw new Error("peernet response nesting exceeds limit");
4202
- if (result instanceof Uint8Array) {
4203
- let codec;
4204
- try {
4205
- codec = new Codec(result);
4206
- } catch {
4207
- }
4208
- if (codec?.name === "peernet-response") {
4209
- const ResponseMessage = globalThis.peernet?.protos?.["peernet-response"];
4210
- if (!ResponseMessage) throw new Error("peernet-response codec is not registered");
4211
- const wrapped = new ResponseMessage(result);
4212
- return decodeResponsePayload(wrapped.decoded.response, depth + 1);
4213
- }
4214
- try {
4215
- return JSON.parse(new TextDecoder().decode(result));
4216
- } catch {
4217
- return result;
4218
- }
4219
- }
4220
- if (result && typeof result === "object") {
4221
- const response = result.decoded?.response ?? result.response;
4222
- if (response !== void 0) return decodeResponsePayload(response, depth + 1);
4223
- }
4224
- if (typeof result === "string") {
4225
- try {
4226
- return JSON.parse(result);
4227
- } catch {
4228
- return result;
4229
- }
4230
- }
4231
- return result;
4813
+ const resolveLastBlockMessage = async (result) => {
4814
+ if (!(result instanceof Uint8Array) || new Codec(result).name !== "last-block-message")
4815
+ throw new TypeError("lastBlock response must be a binary last-block message");
4816
+ return new LastBlockMessage(result);
4232
4817
  };
4233
4818
 
4234
4819
  class CanonicalStateQueue {
4235
4820
  #tail = Promise.resolve();
4236
4821
  #failed = false;
4237
4822
  #failure;
4823
+ get failed() {
4824
+ return this.#failed;
4825
+ }
4238
4826
  run(operation) {
4239
4827
  const result = this.#tail.then(async () => {
4240
4828
  if (this.#failed) {
@@ -4260,27 +4848,6 @@ class CanonicalStateQueue {
4260
4848
  }
4261
4849
  }
4262
4850
 
4263
- const validateChainLink = (localTip, incoming) => {
4264
- const localHash = localTip?.hash || "0x0";
4265
- const localIndex = Number(localTip?.index ?? -1);
4266
- const hasCanonicalTip = localHash !== "0x0";
4267
- if (hasCanonicalTip && incoming.index < localIndex) return "stale";
4268
- if (hasCanonicalTip && incoming.index === localIndex) {
4269
- if (incoming.hash === localHash) return "duplicate";
4270
- throw new Error(`Block conflict detected at index ${incoming.index}`);
4271
- }
4272
- const expectedIndex = hasCanonicalTip ? localIndex + 1 : 0;
4273
- if (incoming.index !== expectedIndex) {
4274
- throw new Error(`Unexpected block index ${incoming.index}; expected ${expectedIndex}`);
4275
- }
4276
- if (incoming.previousHash !== localHash) {
4277
- throw new Error(
4278
- `previousHash mismatch at index ${incoming.index}: expected ${localHash}, got ${incoming.previousHash}`
4279
- );
4280
- }
4281
- return "append";
4282
- };
4283
-
4284
4851
  const resolveTransactionReference = async (expectedHash, transactionData) => {
4285
4852
  const transaction = transactionData instanceof TransactionMessage ? transactionData : new TransactionMessage(transactionData);
4286
4853
  const actualHash = await transaction.hash();
@@ -4290,6 +4857,41 @@ const resolveTransactionReference = async (expectedHash, transactionData) => {
4290
4857
  return transaction;
4291
4858
  };
4292
4859
 
4860
+ function publishObserverEvent(bus, topic, payload, onError = (topic2, error) => console.warn(`[chain observer] ${topic2}:`, error)) {
4861
+ const report = (error) => {
4862
+ try {
4863
+ onError(topic, error);
4864
+ } catch {
4865
+ }
4866
+ };
4867
+ const invoke = (callback) => {
4868
+ try {
4869
+ const result = callback();
4870
+ if (result && typeof result.then === "function") Promise.resolve(result).catch(report);
4871
+ } catch (error) {
4872
+ report(error);
4873
+ }
4874
+ };
4875
+ try {
4876
+ const value = structuredClone(payload);
4877
+ if (!(bus.subscribers instanceof Map)) {
4878
+ invoke(() => bus.publish(topic, value));
4879
+ return;
4880
+ }
4881
+ let subscriber = bus.subscribers.get(topic);
4882
+ if (!subscriber) {
4883
+ subscriber = { handlers: [], value: void 0 };
4884
+ bus.subscribers.set(topic, subscriber);
4885
+ }
4886
+ const oldValue = subscriber.value;
4887
+ if (!bus.verbose && oldValue === value) return;
4888
+ subscriber.value = value;
4889
+ for (const handler of [...subscriber.handlers]) invoke(() => handler.bound(value, oldValue));
4890
+ } catch (error) {
4891
+ report(error);
4892
+ }
4893
+ }
4894
+
4293
4895
  const debug$1 = createDebugger("leofcoin/state");
4294
4896
  class State extends Contract {
4295
4897
  constructor(config) {
@@ -4323,15 +4925,6 @@ class State extends Contract {
4323
4925
  response: { blocks: await globalThis.blockStore.keys() }
4324
4926
  });
4325
4927
  };
4326
- this.#loadBlockTransactions = (transactions) => Promise.all(
4327
- transactions.map(async (transaction) => new TransactionMessage(await peernet.get(transaction, "transaction")))
4328
- );
4329
- this.#getLastTransactions = async () => {
4330
- let lastTransactions = (await Promise.all(
4331
- (await this.blocks).filter((block) => block.loaded).slice(-24).map((block) => this.#loadBlockTransactions(block.transactions))
4332
- )).reduce((all, transactions) => [...all, ...transactions], []);
4333
- return Promise.all(lastTransactions.map((transaction) => transaction.hash()));
4334
- };
4335
4928
  }
4336
4929
  #resolveErrored;
4337
4930
  #lastResolvedTime;
@@ -4349,7 +4942,13 @@ class State extends Contract {
4349
4942
  #machine;
4350
4943
  #canonicalStateQueue;
4351
4944
  withCanonicalState(operation) {
4352
- return this.#canonicalStateQueue.run(operation);
4945
+ return this.#canonicalStateQueue.run(operation).catch((error) => {
4946
+ if (this.#canonicalStateQueue.failed) {
4947
+ this.#loaded = false;
4948
+ this.#syncState = "errored";
4949
+ }
4950
+ throw error;
4951
+ });
4353
4952
  }
4354
4953
  canonicalTip() {
4355
4954
  return this.#machine ? this.#machine.getCanonicalTip() : Promise.resolve({ index: -1, hash: "0x0", previousHash: "0x0" });
@@ -4449,91 +5048,79 @@ class State extends Contract {
4449
5048
  #lastBlockHandler;
4450
5049
  #knownBlocksHandler;
4451
5050
  async init() {
4452
- debug$1("State init start");
4453
5051
  this.jobber = new Jobber(this.resolveTimeout);
4454
5052
  await globalThis.peernet.addRequestHandler("lastBlock", this.#lastBlockHandler);
4455
5053
  await globalThis.peernet.addRequestHandler("knownBlocks", this.#knownBlocksHandler);
4456
5054
  await globalThis.peernet.addRequestHandler("chainState", this.#chainStateHandler);
4457
- let localBlockHash;
4458
- let blockMessage;
4459
- let localBlock;
4460
- console.log("State init before try-catch");
4461
5055
  try {
4462
- const rawBlock = await globalThis.chainStore.has("lastBlock");
4463
- console.log("State init after has lastBlock check");
4464
- if (rawBlock) {
4465
- console.log("State init after has lastBlock found");
4466
- console.log(rawBlock);
4467
- localBlockHash = new TextDecoder().decode(await globalThis.chainStore.get("lastBlock"));
4468
- console.log(localBlockHash);
4469
- if (localBlockHash !== "0x0") {
4470
- blockMessage = await globalThis.blockStore.get(localBlockHash);
4471
- console.log(blockMessage);
4472
- blockMessage = await new BlockMessage(blockMessage);
4473
- localBlock = { ...blockMessage.decoded, hash: localBlockHash };
4474
- }
4475
- console.log("State init after localBlock set");
4476
- } else {
4477
- localBlock = { index: 0, hash: "0x0", previousHash: "0x0" };
5056
+ const identity = await createChainIdentity(globalThis.peernet?.network || "leofcoin:peach", addresses);
5057
+ const required = [addresses.contractFactory, addresses.nativeToken, addresses.nameService, addresses.validators];
5058
+ const checkpoint = await readCheckpoint(globalThis.stateStore, identity.chainId, required);
5059
+ const checkpointTip = checkpoint?.snapshot.lastBlock ?? { index: -1, hash: "0x0" };
5060
+ const marker = await globalThis.chainStore.has("lastBlock") ? new TextDecoder().decode(await globalThis.chainStore.get("lastBlock")) : "0x0";
5061
+ const readBlock = async (hash) => await globalThis.blockStore.has(hash) ? globalThis.blockStore.get(hash) : globalThis.peernet.get(hash, "block");
5062
+ let history = await recoveryHistory(checkpointTip.hash, readBlock);
5063
+ if (marker !== "0x0" && marker !== checkpointTip.hash && !history.some((block) => block.hash === marker)) {
5064
+ const candidate = await recoveryHistory(marker, readBlock);
5065
+ if (checkpointTip.hash !== "0x0" && candidate[Number(checkpointTip.index)]?.hash !== checkpointTip.hash)
5066
+ throw new Error("Persisted chain marker conflicts with state checkpoint");
5067
+ history = candidate;
4478
5068
  }
4479
- } catch {
4480
- console.log("e");
4481
- console.log("State init middle");
4482
- localBlock = { index: 0, hash: "0x0", previousHash: "0x0" };
4483
- }
4484
- console.log("State init middle");
4485
- try {
4486
- console.log("fetching known blocks from blockStore");
4487
- this.knownBlocks = await globalThis.blockStore.keys();
4488
- } catch (error) {
4489
- console.log("no local known blocks found");
4490
- }
4491
- try {
4492
- if (localBlock?.hash && localBlock.hash !== "0x0") {
4493
- try {
4494
- if (!await globalThis.stateStore.has("lastBlock")) {
4495
- await this.resolveBlocks();
4496
- }
4497
- } catch {
4498
- await this.resolveBlocks();
5069
+ if (!checkpoint && await globalThis.stateStore.has("lastBlock")) {
5070
+ const legacy = JSON.parse(new TextDecoder().decode(await globalThis.stateStore.get("lastBlock")));
5071
+ if (legacy.hash && legacy.hash !== "0x0" && legacy.hash !== history.at(-1)?.hash) {
5072
+ const candidate = await recoveryHistory(legacy.hash, readBlock);
5073
+ const common = Math.min(history.length, candidate.length);
5074
+ if (common && history[common - 1].hash !== candidate[common - 1].hash)
5075
+ throw new Error("Legacy snapshot conflicts with canonical chain marker");
5076
+ if (candidate.length > history.length) history = candidate;
4499
5077
  }
4500
- } else {
4501
- await this.resolveBlocks();
4502
5078
  }
4503
- const machine = new Machine(this.#blocks);
4504
- console.log(machine);
5079
+ if (await globalThis.chainStore.has("recoveryTarget")) {
5080
+ const target = new TextDecoder().decode(await globalThis.chainStore.get("recoveryTarget"));
5081
+ const candidate = await recoveryHistory(target, readBlock);
5082
+ const common = Math.min(history.length, candidate.length);
5083
+ if (common && history[common - 1].hash !== candidate[common - 1].hash)
5084
+ throw new Error("Recovery target conflicts with canonical checkpoint history");
5085
+ if (candidate.length > history.length) history = candidate;
5086
+ }
5087
+ this.#blocks = history;
5088
+ this.#blockHashMap = new Map(history.map((block) => [block.hash, block.index]));
5089
+ this.knownBlocks = await globalThis.blockStore.keys();
5090
+ if (!checkpoint && !history.length && this.knownBlocks.length)
5091
+ throw new Error("Local blocks exist without a canonical recovery anchor; preserve this root and rebuild in a separate root");
5092
+ const prefix = history.slice(0, Number(checkpointTip.index) + 1).map((block) => ({ ...block, loaded: true }));
5093
+ if (history.length > prefix.length) await globalThis.chainStore.put("recoveryTarget", history.at(-1).hash);
5094
+ const machine = new Machine(prefix, checkpoint);
4505
5095
  await machine.ready;
4506
5096
  this.#machine = machine;
4507
- const lastBlock = await this.#machine.lastBlock;
4508
- if (lastBlock.hash !== "0x0") {
4509
- await this.updateState(new BlockMessage(lastBlock));
4510
- }
5097
+ await restoreAccountNonces(globalThis.accountsStore, checkpoint?.snapshot.accounts ?? {});
5098
+ for (const block of prefix) this.#blocks[Number(block.index)].loaded = true;
5099
+ await this.withCanonicalState((transition) => this.#loadBlocks(history.slice(prefix.length), transition));
5100
+ if (!checkpoint && !history.length) await this.#machine.updateState(true);
5101
+ const tip = await this.canonicalTip();
5102
+ await globalThis.chainStore.put("lastBlock", tip.hash);
5103
+ await globalThis.chainStore.delete("recoveryTarget");
4511
5104
  this.#loaded = true;
4512
5105
  this.#chainState = "loaded";
4513
5106
  } catch (error) {
4514
- console.log("e");
4515
- if (isResolveError(error)) {
4516
- console.error(error);
4517
- }
4518
- console.log(error);
5107
+ this.#loaded = false;
5108
+ this.#chainState = "loading";
5109
+ this.#syncState = "errored";
5110
+ throw error;
4519
5111
  }
4520
5112
  }
4521
5113
  async updateState(message) {
4522
- try {
4523
- const hash = await message.hash();
4524
- await globalThis.chainStore.put("lastBlock", hash);
4525
- globalThis.pubsub.publish("lastBlock", message.encoded);
4526
- if (!this.#machine) {
4527
- const machine = new Machine(this.#blocks);
4528
- console.log(machine);
4529
- await machine.ready;
4530
- this.#machine = machine;
4531
- }
4532
- await this.#machine.updateState(true);
4533
- } catch (error) {
4534
- console.error(error);
4535
- throw error;
4536
- }
5114
+ if (!this.#machine) throw new Error("Cannot checkpoint an uninitialized state machine");
5115
+ const hash = await message.hash();
5116
+ const tip = await this.canonicalTip();
5117
+ if (tip.hash !== hash || Number(tip.index) !== Number(message.decoded.index))
5118
+ throw new Error("Checkpoint target does not match canonical worker tip");
5119
+ await this.#machine.updateState(true);
5120
+ await globalThis.chainStore.put("lastBlock", hash);
5121
+ if (await globalThis.chainStore.has("recoveryTarget") && new TextDecoder().decode(await globalThis.chainStore.get("recoveryTarget")) === hash)
5122
+ await globalThis.chainStore.delete("recoveryTarget");
5123
+ publishObserverEvent(globalThis.pubsub, "lastBlock", message.encoded);
4537
5124
  }
4538
5125
  getLatestBlock() {
4539
5126
  return this.#getLatestBlock();
@@ -4591,7 +5178,7 @@ class State extends Contract {
4591
5178
  this.#blocks[index] = { hash, ...block.decoded };
4592
5179
  this.#blockHashMap.set(hash, index);
4593
5180
  debug$1(`resolved block: ${hash} @${index} ${formatBytes(size)}`);
4594
- globalThis.pubsub.publish("block-resolved", { hash, index });
5181
+ publishObserverEvent(globalThis.pubsub, "block-resolved", { hash, index });
4595
5182
  this.#lastResolved = this.#blocks[index];
4596
5183
  this.#lastResolvedTime = Date.now();
4597
5184
  hash = block.decoded.previousHash;
@@ -4728,7 +5315,7 @@ class State extends Contract {
4728
5315
  const localIndex = localBlock ? Number(localBlock.index) : -1;
4729
5316
  const remoteIndex = Number(lastBlock.index);
4730
5317
  const remoteBlockHash = lastBlock.hash;
4731
- globalThis.pubsub.publish("sync-progress", {
5318
+ publishObserverEvent(globalThis.pubsub, "sync-progress", {
4732
5319
  phase: "syncing",
4733
5320
  startIndex: localIndex,
4734
5321
  localIndex,
@@ -4764,6 +5351,7 @@ class State extends Contract {
4764
5351
  if (localStateHash !== remoteBlockHash) {
4765
5352
  debug$1(`Resolving remote block: ${remoteBlockHash} @${remoteIndex} (differs from local state)`);
4766
5353
  await this.resolveBlock(remoteBlockHash);
5354
+ await globalThis.chainStore.put("recoveryTarget", remoteBlockHash);
4767
5355
  const blocksSynced = remoteIndex - localIndex;
4768
5356
  debug$1(`Resolved ${blocksSynced} new block(s)`);
4769
5357
  const blocks = this.#blocks;
@@ -4783,7 +5371,7 @@ class State extends Contract {
4783
5371
  } else {
4784
5372
  debug$1(`Block already in local state. Remote hash: ${remoteBlockHash} matches local state`);
4785
5373
  }
4786
- globalThis.pubsub.publish("sync-progress", {
5374
+ publishObserverEvent(globalThis.pubsub, "sync-progress", {
4787
5375
  phase: "synced",
4788
5376
  startIndex: localIndex,
4789
5377
  localIndex: remoteIndex,
@@ -4871,112 +5459,23 @@ class State extends Contract {
4871
5459
  }
4872
5460
  return latest;
4873
5461
  }
4874
- #loadBlockTransactions;
4875
- #getLastTransactions;
4876
- // todo throw error
4877
- async #_executeTransaction(transaction, validators, feesEnabled, burnBasisPoints, blockTimestamp) {
4878
- try {
4879
- const hash = await transaction.hash();
4880
- if (feesEnabled) {
4881
- const fee = BigInt(await calculateFee(transaction.decoded));
4882
- const { payments, burned } = distributeTransactionFee(fee, hash, validators, burnBasisPoints);
4883
- await this.#machine.collectFee(transaction.decoded.from, payments, burned);
4884
- }
4885
- await this.#machine.execute(
4886
- transaction.decoded.to,
4887
- transaction.decoded.method,
4888
- transaction.decoded.params,
4889
- transaction.decoded.from,
4890
- { seed: hash, timestamp: Number(blockTimestamp) }
4891
- );
4892
- } catch (error) {
4893
- console.log(error);
4894
- await globalThis.transactionPoolStore.delete(await transaction.hash());
4895
- console.log("removing invalid transaction");
4896
- if (isExecutionError(error)) {
4897
- console.log(error);
4898
- }
4899
- console.log(error);
4900
- throw error;
4901
- }
5462
+ async applyRecoveredBlock(block, transition) {
5463
+ throw new Error("Canonical block application is not implemented");
4902
5464
  }
4903
- /**
4904
- *
4905
- * @param {Block[]} blocks
4906
- */
4907
5465
  async #loadBlocks(blocks, transition) {
4908
5466
  this.#chainState = "loading";
4909
- if (blocks.some((block) => !block)) {
4910
- throw new Error("missing block data during load; chain resolution incomplete");
4911
- }
4912
- const poolTransactionKeys = new Set(await globalThis.transactionPoolStore.keys());
4913
- debug$1(`pool transactions: ${poolTransactionKeys.size}`);
4914
- debug$1(`loading ${blocks.length} blocks`);
4915
5467
  for (const block of blocks) {
4916
- if (block && !block.loaded) {
4917
- try {
4918
- const link = validateChainLink(await this.canonicalTip(), {
4919
- index: Number(block.index),
4920
- hash: block.hash,
4921
- previousHash: String(block.previousHash)
4922
- });
4923
- if (link !== "append") {
4924
- this.#blocks[block.index].loaded = true;
4925
- debug$1(`Ignoring ${link} loaded block ${block.hash} at height ${block.index}`);
4926
- continue;
4927
- }
4928
- debug$1(`loading block: ${Number(block.index)} ${block.hash}`);
4929
- let transactions = await this.#loadBlockTransactions(block.transactions || []);
4930
- const validators = block.validators.map(({ address }) => address);
4931
- debug$1(`loading transactions: ${transactions.length} for block ${block.index}`);
4932
- transition.markMutating();
4933
- for (const transaction of transactions) {
4934
- const hash = await transaction.hash();
4935
- if (poolTransactionKeys.has(hash)) await globalThis.transactionPoolStore.delete(hash);
4936
- }
4937
- transactions = transactions.sort((a, b) => {
4938
- if (a.decoded.priority !== b.decoded.priority) return a.decoded.priority ? -1 : 1;
4939
- const left = BigInt(a.decoded.nonce);
4940
- const right = BigInt(b.decoded.nonce);
4941
- return left < right ? -1 : left > right ? 1 : 0;
4942
- });
4943
- debug$1(`executing ${transactions.length} transactions for block ${block.index}`);
4944
- const feesEnabled = supportsTransactionFees(block.protocolVersion);
4945
- const monetaryPolicyEnabled = supportsMonetaryPolicy(block.protocolVersion);
4946
- if (monetaryPolicyEnabled) await validateBlockResourceLimits(transactions);
4947
- const policy = monetaryPolicyEnabled ? calculateMonetaryPolicy(
4948
- BigInt(await this.#machine.get(nativeToken, "totalSupply")),
4949
- BigInt(await this.#machine.get(nativeToken, "targetSupply"))
4950
- ) : { subsidy: 0n, burnBasisPoints: 1000n };
4951
- for (const transaction of transactions) {
4952
- await this.#_executeTransaction(
4953
- transaction,
4954
- validators,
4955
- feesEnabled,
4956
- policy.burnBasisPoints,
4957
- block.timestamp
4958
- );
4959
- }
4960
- if (monetaryPolicyEnabled && policy.subsidy > 0n) {
4961
- await this.#machine.settleRewards([
4962
- ...distributeAmount(policy.subsidy, validators, Number(block.index) % validators.length)
4963
- ]);
4964
- }
4965
- debug$1(`executed transactions for block ${block.index}`);
4966
- const registered = await this.#machine.addLoadedBlock({ ...block, loaded: true });
4967
- if (registered !== true) throw new Error(`Loaded block registration failed after execution: ${block.hash}`);
4968
- this.#blocks[block.index].loaded = true;
4969
- if (Number(block.index) === 0) this.#loaded = true;
4970
- debug$1(`loaded block: ${block.hash} @${Number(block.index)}`);
4971
- globalThis.pubsub.publish("block-loaded", { ...block });
4972
- } catch (error) {
4973
- console.error(error);
4974
- for (const transaction of block.transactions) {
4975
- this.wantList.push(transaction);
4976
- }
4977
- throw error;
4978
- }
4979
- }
5468
+ if (!block) throw new Error("Missing block data during recovery");
5469
+ if (block.loaded) continue;
5470
+ const { hash, loaded, ...header } = block;
5471
+ const message = new BlockMessage(header);
5472
+ if (await message.hash() !== block.hash) throw new Error("Recovery block hash changed before application");
5473
+ await this.applyRecoveredBlock(message, transition);
5474
+ const tip = await this.canonicalTip();
5475
+ if (tip.hash !== block.hash || Number(tip.index) !== Number(block.index))
5476
+ throw new Error("Recovery did not apply the expected canonical block");
5477
+ block.loaded = true;
5478
+ publishObserverEvent(globalThis.pubsub, "block-loaded", { ...block });
4980
5479
  }
4981
5480
  this.#chainState = "loaded";
4982
5481
  return true;
@@ -5036,18 +5535,16 @@ class State extends Contract {
5036
5535
  return this.syncChain(latest);
5037
5536
  }
5038
5537
  async triggerLoad() {
5039
- if (this.#blocks?.length > 0) {
5040
- const machine = new Machine(this.#blocks);
5041
- console.log(machine);
5042
- await machine.ready;
5043
- this.#machine = machine;
5044
- }
5538
+ if (!this.#machine) throw new Error("Restart the node to initialize recovery");
5539
+ const tip = await this.canonicalTip();
5540
+ return this.withCanonicalState((transition) => this.#loadBlocks(this.#blocks.slice(Number(tip.index) + 1), transition));
5045
5541
  }
5046
5542
  }
5047
5543
 
5048
5544
  class VersionControl extends State {
5049
5545
  constructor(config) {
5050
5546
  super(config);
5547
+ this.version = PROTOCOL_VERSION;
5051
5548
  }
5052
5549
  #currentVersion = PROTOCOL_VERSION;
5053
5550
  #reachedOneZeroZero = REACHED_ONE_ZERO_ZERO;
@@ -5191,80 +5688,13 @@ const assertHashFormatState = async (chainStore, stateStore) => {
5191
5688
  const parsed = /^(\d+)\.(\d+)\.\d+$/.exec(version);
5192
5689
  const current = PROTOCOL_VERSION.split(".");
5193
5690
  if (parsed && parsed[1] === current[0] && parsed[2] === current[1]) return;
5194
- throw new Error(`stored protocol ${version} is incompatible with canonical hash protocol ${PROTOCOL_VERSION}; back up existing data and use a separate migrated data root`);
5691
+ throw new Error(`stored protocol ${version} is incompatible with canonical hash protocol ${PROTOCOL_VERSION}; explicit ledger migration or a testnet reset is required (preserve the wallet identity)`);
5195
5692
  }
5196
- if (await chainStore.has("lastBlock") || await stateStore?.has("lastBlock")) {
5693
+ if (await chainStore.has("lastBlock") || await chainStore.has("recoveryTarget") || await stateStore?.has("lastBlock") || await stateStore?.has("checkpoint:0") || await stateStore?.has("checkpoint:1")) {
5197
5694
  throw new Error("existing chainstate has no protocol version; explicit canonical hash migration required");
5198
5695
  }
5199
5696
  };
5200
5697
 
5201
- const CHAIN_ID_SCHEMA = "leofcoin:chain-identity:v1";
5202
- const GENESIS_CONTRACT_NAMES = ["contractFactory", "nativeToken", "nameService", "validators"];
5203
- const assertChainId = (value) => {
5204
- if (typeof value !== "string" || !/^lfc:[0-9a-f]{64}$/.test(value))
5205
- throw new Error("Invalid chain ID; expected lfc: followed by 64 lowercase hex characters");
5206
- return value;
5207
- };
5208
- const createChainIdentity = async (network, contracts) => {
5209
- if (typeof network !== "string" || !/^[a-z0-9][a-z0-9:-]{0,79}$/.test(network))
5210
- throw new Error("Invalid genesis network namespace");
5211
- const genesisContracts = {};
5212
- for (const name of GENESIS_CONTRACT_NAMES) {
5213
- const hash = contracts?.[name];
5214
- if (typeof hash !== "string" || !/^[A-Z2-7]{32,128}$/.test(hash))
5215
- throw new Error(`Invalid genesis contract hash: ${name}`);
5216
- genesisContracts[name] = hash;
5217
- }
5218
- const bytes = new TextEncoder().encode(JSON.stringify({ schema: CHAIN_ID_SCHEMA, network, genesisContracts }));
5219
- const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
5220
- const chainId = "lfc:" + [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
5221
- return { schema: CHAIN_ID_SCHEMA, chainId, network, genesisContracts };
5222
- };
5223
- const verifyChainIdentity = async (value) => {
5224
- const identity = value;
5225
- if (!identity || identity.schema !== CHAIN_ID_SCHEMA)
5226
- throw new Error("Unsupported chain identity response");
5227
- const verified = await createChainIdentity(identity.network, identity.genesisContracts);
5228
- if (verified.chainId !== assertChainId(identity.chainId))
5229
- throw new Error("Chain ID does not match its genesis contracts");
5230
- return verified;
5231
- };
5232
- const findChainPresence = async (chainId, peerIds, requestIdentity, timeoutMs = 3e3) => {
5233
- assertChainId(chainId);
5234
- const peers = [...new Set(peerIds)].slice(0, 16);
5235
- const matches = [];
5236
- let responded = 0;
5237
- let unavailable = 0;
5238
- await Promise.all(peers.map(async (peerId) => {
5239
- let timer;
5240
- try {
5241
- const result = await Promise.race([
5242
- Promise.resolve().then(() => requestIdentity(peerId)),
5243
- new Promise((_, reject) => {
5244
- timer = setTimeout(() => reject(new Error("Chain identity request timed out")), timeoutMs);
5245
- })
5246
- ]);
5247
- const identity = await verifyChainIdentity(result);
5248
- responded++;
5249
- if (identity.chainId === chainId)
5250
- matches.push(peerId);
5251
- } catch {
5252
- unavailable++;
5253
- } finally {
5254
- clearTimeout(timer);
5255
- }
5256
- }));
5257
- return {
5258
- chainId,
5259
- status: matches.length ? "found" : !peers.length || unavailable ? "unknown" : "not-observed",
5260
- checkedPeers: peers.length,
5261
- respondedPeers: responded,
5262
- unavailablePeers: unavailable,
5263
- matchingPeers: matches.sort(),
5264
- scope: "reachable peers only; offline or undiscovered chains may still exist"
5265
- };
5266
- };
5267
-
5268
5698
  const debug = createDebugger("leofcoin/chain");
5269
5699
  class Chain extends VersionControl {
5270
5700
  constructor(config) {
@@ -5864,10 +6294,10 @@ class Chain extends VersionControl {
5864
6294
  });
5865
6295
  });
5866
6296
  globalThis.peernet.subscribe("add-block", this.#addBlock.bind(this));
5867
- globalThis.peernet.subscribe("invalid-transaction", this.#invalidTransaction.bind(this));
6297
+ globalThis.peernet.subscribe("invalid-transaction", (bytes) => this.#invalidTransaction(decodeWire$1("invalid-transaction", bytes)));
5868
6298
  globalThis.peernet.subscribe("send-transaction", this.#sendTransaction.bind(this));
5869
6299
  globalThis.peernet.subscribe("add-transaction", this.#addTransaction.bind(this));
5870
- globalThis.peernet.subscribe("validator:timeout", this.#validatorTimeout.bind(this));
6300
+ globalThis.peernet.subscribe("validator:timeout", (bytes) => this.#validatorTimeout(decodeWire$1("validator:timeout", bytes)));
5871
6301
  globalThis.peernet.subscribe("consensus:propose", this.#handleProposal.bind(this));
5872
6302
  globalThis.peernet.subscribe("consensus:prevote", this.#handlePrevote.bind(this));
5873
6303
  globalThis.peernet.subscribe("consensus:precommit", this.#handlePrecommit.bind(this));
@@ -5913,52 +6343,7 @@ class Chain extends VersionControl {
5913
6343
  let response = await peer.request(node.encoded);
5914
6344
  console.log(`raw response for request ${request}:`, response);
5915
6345
  if (response === void 0 || response === null) return response;
5916
- const normalizeResponse = async (payload) => {
5917
- if (payload === void 0 || payload === null) return payload;
5918
- if (payload instanceof Uint8Array || typeof Buffer !== "undefined" && Buffer.isBuffer(payload)) {
5919
- let bytes = payload instanceof Uint8Array ? payload : new Uint8Array(payload);
5920
- for (let i = 0; i < 3; i += 1) {
5921
- try {
5922
- const codec = new Codec(bytes);
5923
- if (codec.name !== "peernet-response") break;
5924
- const wrapped = await new globalThis.peernet.protos["peernet-response"](bytes);
5925
- if (wrapped?.decoded?.response === void 0) break;
5926
- const next = wrapped.decoded.response;
5927
- if (next instanceof Uint8Array || typeof Buffer !== "undefined" && Buffer.isBuffer(next)) {
5928
- bytes = next instanceof Uint8Array ? next : new Uint8Array(next);
5929
- continue;
5930
- }
5931
- return normalizeResponse(next);
5932
- } catch {
5933
- break;
5934
- }
5935
- }
5936
- try {
5937
- return JSON.parse(new TextDecoder().decode(bytes));
5938
- } catch {
5939
- return bytes;
5940
- }
5941
- }
5942
- if (payload && typeof payload === "object") {
5943
- const objectPayload = payload;
5944
- const decoded = objectPayload.decoded;
5945
- if ("decoded" in objectPayload && decoded && "response" in decoded) {
5946
- return normalizeResponse(decoded.response);
5947
- }
5948
- if ("response" in objectPayload) {
5949
- return normalizeResponse(objectPayload.response);
5950
- }
5951
- }
5952
- if (typeof payload === "string") {
5953
- try {
5954
- return JSON.parse(payload);
5955
- } catch {
5956
- return payload;
5957
- }
5958
- }
5959
- return payload;
5960
- };
5961
- return await normalizeResponse(response);
6346
+ return decodeResponsePayload(response);
5962
6347
  } catch (error) {
5963
6348
  const peerId = peer?.peerId || peer?.id || peer?.address || "unknown";
5964
6349
  debug(`peernet request failed: ${request} -> ${peerId}:`, error?.message ?? error);
@@ -5966,60 +6351,11 @@ class Chain extends VersionControl {
5966
6351
  }
5967
6352
  }
5968
6353
  async #decodeKnownBlocksResponse(response) {
5969
- if (!response) return null;
5970
- if (Array.isArray(response)) {
5971
- return { blocks: response };
5972
- }
5973
- if (Array.isArray(response.blocks)) {
5974
- return { blocks: response.blocks };
5975
- }
5976
- if (response.response && Array.isArray(response.response.blocks)) {
5977
- return { blocks: response.response.blocks };
5978
- }
5979
- if (typeof response === "string") {
5980
- try {
5981
- const parsed = JSON.parse(response);
5982
- if (Array.isArray(parsed)) return { blocks: parsed };
5983
- if (parsed && Array.isArray(parsed.blocks)) return { blocks: parsed.blocks };
5984
- if (parsed?.response && Array.isArray(parsed.response.blocks)) return { blocks: parsed.response.blocks };
5985
- } catch {
5986
- return null;
5987
- }
5988
- return null;
5989
- }
5990
- if (!(response instanceof Uint8Array)) return null;
5991
- let payload = response;
5992
- for (let i = 0; i < 3; i++) {
5993
- if (!(payload instanceof Uint8Array)) break;
5994
- try {
5995
- const nestedResponse = await new globalThis.peernet.protos["peernet-response"](payload);
5996
- payload = nestedResponse?.decoded?.response;
5997
- } catch {
5998
- break;
5999
- }
6000
- }
6001
- if (Array.isArray(payload)) return { blocks: payload };
6002
- if (payload && Array.isArray(payload.blocks)) return { blocks: payload.blocks };
6003
- if (payload?.response && Array.isArray(payload.response.blocks)) return { blocks: payload.response.blocks };
6004
- try {
6005
- const decoded = new TextDecoder().decode(response);
6006
- const parsed = JSON.parse(decoded);
6007
- if (Array.isArray(parsed)) return { blocks: parsed };
6008
- if (parsed && Array.isArray(parsed.blocks)) return { blocks: parsed.blocks };
6009
- if (parsed?.response && Array.isArray(parsed.response.blocks)) return { blocks: parsed.response.blocks };
6010
- } catch {
6011
- return null;
6012
- }
6013
- return null;
6354
+ if (!Array.isArray(response) || response.length > 1e5 || response.some((hash) => typeof hash !== "string")) return null;
6355
+ return { blocks: response };
6014
6356
  }
6015
6357
  async getPeerTransactionPool(peer) {
6016
6358
  let transactionsInPool = await this.#makeRequest(peer, "transactionPool");
6017
- if (transactionsInPool instanceof Uint8Array) {
6018
- transactionsInPool = new TextDecoder().decode(transactionsInPool);
6019
- }
6020
- if (typeof transactionsInPool === "string") {
6021
- transactionsInPool = JSON.parse(transactionsInPool);
6022
- }
6023
6359
  if (!Array.isArray(transactionsInPool) || transactionsInPool.some((hash) => typeof hash !== "string")) {
6024
6360
  throw new Error("transactionPool response must be an array of transaction hashes");
6025
6361
  }
@@ -6157,7 +6493,6 @@ class Chain extends VersionControl {
6157
6493
  debug(
6158
6494
  `sync finished with peer ${peerId}: state=${this.syncState} localNow=${(await this.lastBlock)?.index ?? -1}`
6159
6495
  );
6160
- this.machine.states.info = stateInfo;
6161
6496
  } catch (error) {
6162
6497
  const peerName = peer?.peerId || peer?.id || peer?.address || peerId || "unknown";
6163
6498
  debug(`stateInfo/syncChain failed: ${peerName}:`, error?.message ?? error);
@@ -6195,7 +6530,7 @@ class Chain extends VersionControl {
6195
6530
  } catch (error) {
6196
6531
  await transactionPoolStore.delete(hash);
6197
6532
  try {
6198
- globalThis.peernet.publish("invalid-transaction", hash);
6533
+ globalThis.peernet.publish("invalid-transaction", encodeWire$1("invalid-transaction", hash));
6199
6534
  } catch (publishError) {
6200
6535
  debug("peernet publish failed: invalid-transaction", publishError?.message ?? publishError);
6201
6536
  }
@@ -6206,7 +6541,13 @@ class Chain extends VersionControl {
6206
6541
  async #addBlock(block) {
6207
6542
  return this.withCanonicalState((transition) => this.#applyBlock(block, transition));
6208
6543
  }
6209
- async #applyBlock(block, transition) {
6544
+ applyRecoveredBlock(block, transition) {
6545
+ return this.#applyBlock(block, transition, false);
6546
+ }
6547
+ #recordValidatorSnapshot(index) {
6548
+ return this.machine.execute(addresses.validators, "recordValidatorSnapshot", [index], globalThis.peernet.selectedAccount);
6549
+ }
6550
+ async #applyBlock(block, transition, schedule = true) {
6210
6551
  const receivedEncoded = block instanceof BlockMessage ? block.encoded : block;
6211
6552
  const blockMessage = await new BlockMessage(block);
6212
6553
  const hash = await blockMessage.hash();
@@ -6240,13 +6581,14 @@ class Chain extends VersionControl {
6240
6581
  console.log(`[chain] \u2705 Block data integrity verified: ${hash}`);
6241
6582
  await this.#validateBlockValidators(blockMessage);
6242
6583
  const transactions = await this.#resolveBlockTransactions(blockMessage);
6584
+ await globalThis.blockStore.put(hash, blockMessage.encoded);
6585
+ if (!await globalThis.chainStore.has("recoveryTarget")) await globalThis.chainStore.put("recoveryTarget", hash);
6243
6586
  transition.markMutating();
6244
6587
  await Promise.all(
6245
6588
  blockMessage.decoded.transactions.map(async (transactionHash) => {
6246
6589
  if (await transactionPoolStore.has(transactionHash)) await transactionPoolStore.delete(transactionHash);
6247
6590
  })
6248
6591
  );
6249
- await globalThis.blockStore.put(hash, blockMessage.encoded);
6250
6592
  this.#blocks[blockIndex] = {
6251
6593
  hash,
6252
6594
  ...blockMessage.decoded
@@ -6280,9 +6622,6 @@ class Chain extends VersionControl {
6280
6622
  }
6281
6623
  const noncesByAddress = {};
6282
6624
  for (let transaction of transactions) {
6283
- globalThis.pubsub.publish("transaction-processed", transaction.encoded);
6284
- if (transaction.decoded.to === globalThis.peernet.selectedAccount)
6285
- globalThis.pubsub.publish("account-transaction-processed", transaction.encoded);
6286
6625
  if (!noncesByAddress[transaction.decoded.from] || noncesByAddress?.[transaction.decoded.from] < transaction.decoded.nonce) {
6287
6626
  noncesByAddress[transaction.decoded.from] = transaction.decoded.nonce;
6288
6627
  }
@@ -6300,14 +6639,19 @@ class Chain extends VersionControl {
6300
6639
  const registered = await this.machine.addLoadedBlock({ ...blockMessage.decoded, loaded: true, hash });
6301
6640
  if (registered !== true) throw new Error(`Block registration failed after execution: ${hash}`);
6302
6641
  try {
6303
- await this.call(addresses.validators, "recordValidatorSnapshot", [blockMessage.decoded.index]);
6642
+ await this.#recordValidatorSnapshot(blockMessage.decoded.index);
6304
6643
  } catch (snapshotError) {
6305
6644
  debug(`failed to record validator snapshot: ${snapshotError?.message ?? snapshotError}`);
6306
6645
  }
6307
6646
  await this.updateState(blockMessage);
6308
6647
  debug(`added block: ${hash}`);
6309
- globalThis.pubsub.publish("block-processed", blockMessage.decoded);
6310
- await this.#handleEpochBoundary(blockIndex);
6648
+ for (const transaction of transactions) {
6649
+ publishObserverEvent(globalThis.pubsub, "transaction-processed", transaction.encoded);
6650
+ if (transaction.decoded.to === globalThis.peernet.selectedAccount)
6651
+ publishObserverEvent(globalThis.pubsub, "account-transaction-processed", transaction.encoded);
6652
+ }
6653
+ publishObserverEvent(globalThis.pubsub, "block-processed", blockMessage.decoded);
6654
+ if (schedule) await this.#handleEpochBoundary(blockIndex);
6311
6655
  }
6312
6656
  async participate(address) {
6313
6657
  try {
@@ -6355,15 +6699,14 @@ class Chain extends VersionControl {
6355
6699
  async #handleTransaction(transaction, latestTransactions, block, feePayments = { payments: [], burned: 0n }) {
6356
6700
  await this.validateTransactionSignature(transaction);
6357
6701
  const hash = await transaction.hash();
6358
- const doubleTransactions = [];
6359
- if (latestTransactions.includes(hash) || await transactionStore.has(hash)) {
6360
- doubleTransactions.push(hash);
6361
- }
6362
- if (doubleTransactions.length > 0) {
6363
- await globalThis.transactionPoolStore.delete(hash);
6364
- await globalThis.peernet.publish("invalid-transaction", hash);
6365
- return;
6366
- }
6702
+ if (latestTransactions.includes(hash)) throw new Error(`Duplicate canonical transaction: ${hash}`);
6703
+ const nonce = BigInt(transaction.decoded.nonce);
6704
+ let committed = 0n;
6705
+ if (await globalThis.accountsStore.has(transaction.decoded.from)) {
6706
+ const raw = await globalThis.accountsStore.get(transaction.decoded.from);
6707
+ committed = BigInt(typeof raw === "string" ? raw : new TextDecoder().decode(raw));
6708
+ }
6709
+ if (nonce <= committed) throw new Error(`Transaction nonce already committed: ${hash}`);
6367
6710
  try {
6368
6711
  const result = await this.#executeTransaction({
6369
6712
  ...transaction.decoded,
@@ -6380,7 +6723,7 @@ class Chain extends VersionControl {
6380
6723
  console.log("vvvvvv");
6381
6724
  console.log({ e });
6382
6725
  console.log(hash);
6383
- peernet.publish("invalid-transaction", hash);
6726
+ peernet.publish("invalid-transaction", encodeWire$1("invalid-transaction", hash));
6384
6727
  await globalThis.transactionPoolStore.delete(e.hash);
6385
6728
  if (block) throw e;
6386
6729
  }
@@ -6545,7 +6888,7 @@ class Chain extends VersionControl {
6545
6888
  } catch (e) {
6546
6889
  if (hash) {
6547
6890
  try {
6548
- await globalThis.peernet.publish("invalid-transaction", hash);
6891
+ await globalThis.peernet.publish("invalid-transaction", encodeWire$1("invalid-transaction", hash));
6549
6892
  } catch (publishError) {
6550
6893
  debug("peernet publish failed: invalid-transaction", publishError?.message ?? publishError);
6551
6894
  }
@@ -6559,7 +6902,8 @@ class Chain extends VersionControl {
6559
6902
  * error is thrown on error so undefined data doesn't mean there is an error...
6560
6903
  **/
6561
6904
  async sendTransaction(transaction) {
6562
- const transactionMessage = await new TransactionMessage({ ...transaction });
6905
+ const { hash: ignoredHash, ...payload } = transaction instanceof TransactionMessage ? transaction.decoded : transaction;
6906
+ const transactionMessage = await new TransactionMessage(payload);
6563
6907
  const event = await super.sendTransaction(transactionMessage);
6564
6908
  await this.#sendTransaction(transactionMessage.encoded);
6565
6909
  try {
@@ -6599,7 +6943,7 @@ class Chain extends VersionControl {
6599
6943
  * @returns
6600
6944
  */
6601
6945
  internalCall(sender, contract, method, parameters) {
6602
- return this.machine.execute(contract, method, parameters, sender);
6946
+ throw new Error("Direct sender impersonation is disabled; submit a signed transaction with sendTransaction()");
6603
6947
  }
6604
6948
  /**
6605
6949
  *
@@ -6608,18 +6952,23 @@ class Chain extends VersionControl {
6608
6952
  * @param {Array} parameters
6609
6953
  * @returns
6610
6954
  */
6611
- call(contract, method, parameters) {
6612
- return this.machine.execute(contract, method, parameters, globalThis.peernet.selectedAccount);
6955
+ async call(transaction, ...legacy) {
6956
+ const signed = transaction instanceof TransactionMessage ? transaction.decoded : transaction;
6957
+ if (legacy.length || !signed || typeof signed !== "object" || typeof signed.signature !== "string" || !signed.signature)
6958
+ throw new Error("call() requires a signed transaction object; use staticCall() for reads or sendTransaction() for writes");
6959
+ return this.sendTransaction(structuredClone(signed));
6613
6960
  }
6614
6961
  staticCall(contract, method, parameters) {
6615
6962
  return this.machine.get(contract, method, parameters);
6616
6963
  }
6617
- mint(to, amount) {
6618
- return this.call(addresses.nativeToken, "mint", [to, amount]);
6964
+ mint(...args) {
6965
+ throw new Error("Native minting is restricted to the monetary protocol");
6619
6966
  }
6620
- transfer(from, to, amount) {
6621
- if (from !== globalThis.peernet.selectedAccount) throw new Error("transfer sender must be the selected account");
6622
- return this.call(addresses.nativeToken, "transfer", [to, amount]);
6967
+ async transfer(transaction, ...legacy) {
6968
+ const signed = transaction instanceof TransactionMessage ? transaction.decoded : transaction;
6969
+ if (legacy.length || signed?.to !== addresses.nativeToken || signed?.method !== "transfer")
6970
+ throw new Error("transfer() requires a signed native-token transfer transaction");
6971
+ return this.call(signed);
6623
6972
  }
6624
6973
  balanceOf(address) {
6625
6974
  return this.staticCall(addresses.nativeToken, "balanceOf", [address]);
@@ -6646,7 +6995,7 @@ class Chain extends VersionControl {
6646
6995
  * @example chain.lookup('myCoolContractName') // qmqsfddfdgfg...
6647
6996
  */
6648
6997
  lookup(name) {
6649
- return this.call(addresses.nameService, "lookup", [name]);
6998
+ return this.staticCall(addresses.nameService, "lookup", [name]);
6650
6999
  }
6651
7000
  #monitorPeerConnections() {
6652
7001
  setInterval(() => {