@leofcoin/chain 1.10.21 → 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 (29) hide show
  1. package/README.md +95 -0
  2. package/exports/browser/beacon-wire.js +2 -2
  3. package/exports/browser/{browser-CCfR9KOM-BGMMnVsR.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 +1275 -996
  6. package/exports/browser/client-DbZBfpG6-C1cXgvfr.js +5943 -0
  7. package/exports/browser/{connection-monitor-D-krNiRH.js → connection-monitor-BzbeYGEt.js} +153 -24
  8. package/exports/browser/{index-dTQVgkpf-DOL2rIjp.js → index-dTQVgkpf-CkoAiRNX.js} +3 -3
  9. package/exports/browser/{index-DCbLvMKL.js → index-phv5KvaE.js} +238 -234
  10. package/exports/browser/{messages-DCePyqKC-BSvJxDWZ.js → messages-DZYCxoZI-B472zldo.js} +3 -3
  11. package/exports/browser/{node-browser-Ha4Lvntj.js → node-browser-BmyPBkL5.js} +55 -11
  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 +296 -1078
  17. package/exports/connection-monitor-BdFLRh-J.js +465 -0
  18. package/exports/connection-monitor.js +2 -324
  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 +35 -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 +9 -7
  29. package/exports/browser/client-DKKzPSlb-C_1ug6ux.js +0 -2401
@@ -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 { B as BlockMessage, L as LastBlockMessage, P as PROTOCOL_VERSION, R as REACHED_ONE_ZERO_ZERO, C as ConnectionMonitor, a as BWMessage, S as StateMessage } from './connection-monitor-D-krNiRH.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-DCbLvMKL.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,49 +2924,46 @@ function requireSemver () {
2915
2924
  var semverExports = requireSemver();
2916
2925
  var semver = /*@__PURE__*/getDefaultExportFromCjs(semverExports);
2917
2926
 
2918
- const limit = 1800;
2919
- const transactionLimit = 2500;
2920
- const requestTimeout = 3e4;
2921
- const syncTimeout = 3e4;
2922
- class Protocol {
2923
- constructor(config) {
2924
- this.resolveTimeout = 1e4;
2925
- if (config?.resolveTimeout) this.resolveTimeout = config.resolveTimeout;
2926
- }
2927
- get limit() {
2928
- return limit;
2929
- }
2930
- get transactionLimit() {
2931
- return transactionLimit;
2932
- }
2933
- get requestTimeout() {
2934
- return requestTimeout;
2935
- }
2936
- get syncTimeout() {
2937
- return syncTimeout;
2938
- }
2939
- }
2940
-
2941
- const waitForCanonicalTransaction = (pubsub, transactionPool, hash) => new Promise((resolve, reject) => {
2942
- const completionTopic = `transaction.completed.${hash}`;
2943
- const cleanup = () => {
2944
- pubsub.unsubscribe("block-processed", blockProcessed);
2945
- pubsub.unsubscribe(completionTopic, transactionCompleted);
2927
+ const createCanonicalTransactionWaiter = (pubsub, transactionPool, hash) => {
2928
+ let cleanup = () => {
2946
2929
  };
2947
- const blockProcessed = (block) => {
2948
- if (!Array.isArray(block?.transactions) || !block.transactions.includes(hash)) return;
2949
- cleanup();
2950
- resolve(hash);
2930
+ let cancel = (_reason) => {
2951
2931
  };
2952
- const transactionCompleted = async (result) => {
2953
- if (result?.status === "fulfilled") return;
2954
- await transactionPool.delete(hash);
2955
- cleanup();
2956
- reject({ hash, error: result?.error });
2957
- };
2958
- pubsub.subscribe("block-processed", blockProcessed);
2959
- pubsub.subscribe(completionTopic, transactionCompleted);
2960
- });
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
+ };
2961
2967
 
2962
2968
  class Transaction extends Protocol {
2963
2969
  #pendingNonces = /* @__PURE__ */ new Map();
@@ -3158,9 +3164,10 @@ class Transaction extends Protocol {
3158
3164
  await this.validateTransactionSignature(message);
3159
3165
  await this.validateNonce(message.decoded.from, message.decoded.nonce);
3160
3166
  const hash = await message.hash();
3167
+ const fee = await calculateFee(message.decoded);
3168
+ const waiter = createCanonicalTransactionWaiter(pubsub, transactionPoolStore, hash);
3161
3169
  try {
3162
3170
  let data;
3163
- const wait = waitForCanonicalTransaction(pubsub, transactionPoolStore, hash);
3164
3171
  await globalThis.transactionPoolStore.put(hash, message.encoded);
3165
3172
  this.addPendingNonce(message.decoded.from, message.decoded.nonce);
3166
3173
  try {
@@ -3168,12 +3175,16 @@ class Transaction extends Protocol {
3168
3175
  } catch (publishError) {
3169
3176
  console.warn("peernet publish failed: add-transaction", publishError?.message ?? publishError);
3170
3177
  }
3171
- const fee = await calculateFee(message.decoded);
3172
- return { hash, data, fee, wait, message };
3178
+ return { hash, data, fee, wait: waiter.wait, message };
3173
3179
  } catch (error) {
3174
- console.log("remo");
3175
- await transactionPoolStore.delete(hash);
3176
- 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
+ }
3177
3188
  throw error;
3178
3189
  }
3179
3190
  }
@@ -3501,120 +3512,541 @@ class Contract extends Transaction {
3501
3512
  }
3502
3513
 
3503
3514
  class EasyWorker {
3504
- #messageEvent = 'message';
3505
- #errorEvent = 'error';
3506
- #isBrowser = false;
3507
- #isWorker = false;
3508
- #messageHandler = null;
3509
- #errorHandler = null;
3510
- worker;
3511
- get isWorker() {
3512
- return this.#isWorker;
3513
- }
3514
- constructor(url, options) {
3515
- return this.#init(url, options);
3516
- }
3517
- #init(url, options = {}) {
3518
- if (url) {
3519
- if (globalThis.Worker) {
3520
- this.#isBrowser = true;
3521
- this.worker = new Worker(url, { ...options });
3522
- }
3523
- else {
3524
- return new Promise((resolve, reject) => {
3525
- import('child_process').then(({ fork }) => {
3526
- const child = fork(url, ['easy-worker-child'], options);
3527
- child.once('error', reject);
3528
- this.worker = child;
3529
- resolve(this);
3530
- }).catch(reject);
3531
- });
3532
- }
3533
- }
3534
- else {
3535
- this.#isWorker = true;
3536
- if (globalThis.process?.argv[2] === 'easy-worker-child') {
3537
- this.worker = process;
3538
- }
3539
- else {
3540
- this.#isBrowser = true;
3541
- this.worker = globalThis;
3542
- }
3543
- }
3544
- 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
+ }
3545
3551
  }
3546
- onmessage(fn) {
3547
- if (this.#isBrowser) {
3548
- this.worker.onmessage = ({ data }) => fn(data);
3549
- }
3550
- else {
3551
- if (this.#messageHandler)
3552
- this.worker.off(this.#messageEvent, this.#messageHandler);
3553
- this.#messageHandler = fn;
3554
- this.worker.on(this.#messageEvent, fn);
3555
- }
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);
3556
3562
  }
3557
- postMessage(message) {
3558
- if (this.#isBrowser)
3559
- this.worker.postMessage(message);
3560
- else
3561
- 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);
3562
3584
  }
3563
- terminate() {
3564
- if (this.#isBrowser)
3565
- this.worker.terminate();
3566
- else
3567
- 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;
3727
+ }
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;
3568
3804
  }
3569
- onerror(fn) {
3570
- if (this.#isBrowser) {
3571
- this.worker.onerror = fn;
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);
3836
+ }
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);
3572
3854
  }
3573
- else {
3574
- if (this.#errorHandler)
3575
- this.worker.off(this.#errorEvent, this.#errorHandler);
3576
- this.#errorHandler = fn;
3577
- this.worker.on(this.#errorEvent, fn);
3855
+ }
3856
+ } finally {
3857
+ ancestors.delete(value2);
3858
+ }
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);
3578
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;
3579
3982
  }
3580
- /**
3581
- *
3582
- * @param {*} data
3583
- * @returns {Promise} resolves result onmessage & rejects on error
3584
- */
3585
- once(data) {
3586
- return new Promise((resolve, reject) => {
3587
- if (this.#isBrowser) {
3588
- const handleMessage = ({ data: result }) => {
3589
- this.worker.onerror = null;
3590
- resolve(result);
3591
- this.terminate();
3592
- };
3593
- const handleError = (error) => {
3594
- this.worker.onmessage = null;
3595
- reject(error);
3596
- this.terminate();
3597
- };
3598
- this.worker.onmessage = handleMessage;
3599
- this.worker.onerror = handleError;
3600
- }
3601
- else {
3602
- const handleMessage = (result) => {
3603
- this.worker.off(this.#errorEvent, handleError);
3604
- resolve(result);
3605
- this.terminate();
3606
- };
3607
- const handleError = (error) => {
3608
- this.worker.off(this.#messageEvent, handleMessage);
3609
- reject(error);
3610
- this.terminate();
3611
- };
3612
- this.worker.once(this.#messageEvent, handleMessage);
3613
- this.worker.once(this.#errorEvent, handleError);
3614
- }
3615
- this.postMessage(data);
3616
- });
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();
3617
4048
  }
4049
+ }
3618
4050
  }
3619
4051
 
3620
4052
  class LeofcoinError extends Error {
@@ -3636,73 +4068,400 @@ class ExecutionError extends LeofcoinError {
3636
4068
  class ContractDeploymentError extends LeofcoinError {
3637
4069
  name = "ContractDeploymentError";
3638
4070
  }
3639
- const isResolveError = (error) => error.name === "ResolveError";
3640
- const isExecutionError = (error) => error.name === "ExecutionError";
3641
4071
 
3642
- const debug$2 = createDebugger("leofcoin/machine");
3643
- class Machine {
3644
- constructor(blocks) {
3645
- this.states = {
3646
- states: {},
3647
- lastBlock: {
3648
- index: -1,
3649
- hash: ""
3650
- },
3651
- accounts: {},
3652
- info: {
3653
- nativeCalls: BigInt(0),
3654
- nativeMints: BigInt(0),
3655
- nativeBurns: BigInt(0),
3656
- nativeTransfers: BigInt(0),
3657
- totalBurnAmount: BigInt(0),
3658
- totalMintAmount: BigInt(0),
3659
- totalTransferAmount: BigInt(0),
3660
- totalTransactions: BigInt(0),
3661
- totalBlocks: BigInt(0)
3662
- }
3663
- };
3664
- this.wantList = [];
3665
- this.ready = new Promise((resolve) => {
3666
- this.readyResolve = resolve;
3667
- });
3668
- 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;
3669
4088
  }
3670
- init(blocks) {
3671
- 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}`);
3672
4147
  }
3673
- async #onmessage(data) {
3674
- switch (data.type) {
3675
- case "transactionLoaded": {
3676
- const { from, nonce, hash } = data.result;
3677
- await transactionPoolStore.has(hash) && await transactionPoolStore.delete(hash);
3678
- try {
3679
- const _nonce = await accountsStore.get(from);
3680
- if (nonce > _nonce) await accountsStore.put(from, nonce);
3681
- } catch (error) {
3682
- await accountsStore.put(from, nonce);
3683
- }
3684
- break;
3685
- }
3686
- case "contractError": {
3687
- console.warn(data.error);
3688
- console.warn(`contract error: ${data.hash}`);
3689
- await contractStore.delete(data.hash);
3690
- break;
3691
- }
3692
- case "initError": {
3693
- console.error(`init error: ${data.message}`);
3694
- break;
3695
- }
3696
- case "executeError": {
3697
- pubsub.publish(data.id, { error: data.message });
3698
- break;
3699
- }
3700
- case "addLoadedBlockError":
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 });
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}`);
4440
+ break;
4441
+ }
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":
3701
4452
  case "collectFeeError":
3702
4453
  case "settleRewardsError": {
3703
4454
  pubsub.publish(data.id, { error: new Error(data.message) });
3704
4455
  break;
3705
4456
  }
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
+ }
3706
4465
  case "debug": {
3707
4466
  if (data.message.includes("loaded transactions for block:")) {
3708
4467
  pubsub.publish("block-loaded", data.message.replace("loaded transactions for block: ", "").split(" @")[0]);
@@ -3713,13 +4472,8 @@ class Machine {
3713
4472
  console.error(data.message);
3714
4473
  break;
3715
4474
  }
3716
- case "machine-ready": {
3717
- this.readyResolve(this);
3718
- pubsub.publish("machine.ready", true);
3719
- break;
3720
- }
3721
4475
  case "response": {
3722
- pubsub.publish(data.id, data.value || false);
4476
+ pubsub.publish(data.id, data.value ?? false);
3723
4477
  break;
3724
4478
  }
3725
4479
  case "addToWantList": {
@@ -3727,6 +4481,16 @@ class Machine {
3727
4481
  break;
3728
4482
  }
3729
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
+ }
3730
4494
  if (data.question === "contract" || data.question === "transaction") {
3731
4495
  try {
3732
4496
  const input = await peernet.get(data.input, data.question);
@@ -3754,164 +4518,61 @@ class Machine {
3754
4518
  }
3755
4519
  async updateState(strict = false) {
3756
4520
  try {
3757
- const lastBlock = strict ? await this.getCanonicalTip() : await this.lastBlock;
3758
- const lastBlockIndex = Number(lastBlock.index);
3759
- const statesLastBlockIndex = Number(this.states.lastBlock.index);
3760
- if (lastBlockIndex > statesLastBlockIndex || lastBlock.hash !== this.states.lastBlock.hash && Number(lastBlockIndex) === 0) {
3761
- const blocks = (await this.blocks).slice(statesLastBlockIndex);
3762
- let transactions = [];
3763
- for (const block of blocks) {
3764
- if (block?.transactions) transactions = [...transactions, ...block.transactions];
3765
- }
3766
- transactions = await Promise.all(
3767
- transactions.map(async (transaction) => new TransactionMessage(await transactionStore.get(transaction)))
3768
- );
3769
- const contractsToGet = transactions.reduce((set, current) => {
3770
- const contract = current.decoded.to;
3771
- if (!set.includes(contract)) set.push(contract);
3772
- return set;
3773
- }, []);
3774
- const state = {};
3775
- if (!contractsToGet.includes(addresses.contractFactory)) contractsToGet.push(addresses.contractFactory);
3776
- if (!contractsToGet.includes(addresses.nativeToken)) contractsToGet.push(addresses.nativeToken);
3777
- if (!contractsToGet.includes(addresses.nameService)) contractsToGet.push(addresses.nameService);
3778
- if (!contractsToGet.includes(addresses.validators)) contractsToGet.push(addresses.validators);
3779
- await Promise.all(
3780
- contractsToGet.map(async (contract) => {
3781
- const value = await this.#askWorker("get", { contract, method: "state", params: [] });
3782
- state[contract] = value;
3783
- })
3784
- );
3785
- let accounts = {};
3786
- try {
3787
- const promises = [];
3788
- for (const address of await accountsStore.keys()) {
3789
- promises.push(
3790
- (async () => {
3791
- accounts[address] = await accountsStore.get(address);
3792
- })()
3793
- );
3794
- }
3795
- await Promise.all(promises);
3796
- } catch (error) {
3797
- const promises = [];
3798
- for (const transaction of transactions.reverse()) {
3799
- const { from, to, amount, nonce } = transaction.decoded.data;
3800
- if (!accounts[from]) {
3801
- accounts[from] = nonce;
3802
- promises.push(accountsStore.put(from, nonce));
3803
- }
3804
- }
3805
- await Promise.all(promises);
3806
- }
3807
- const sortedStringify = (obj, replacer) => {
3808
- const sorted = {};
3809
- const keys = Object.keys(obj).sort();
3810
- for (const key of keys) {
3811
- sorted[key] = obj[key];
3812
- }
3813
- return JSON.stringify(sorted, replacer);
3814
- };
3815
- const tasks = [
3816
- stateStore.put("lastBlock", sortedStringify(lastBlock, jsonStringifyBigInt)),
3817
- stateStore.put("states", sortedStringify(state, jsonStringifyBigInt)),
3818
- stateStore.put("accounts", sortedStringify(accounts, jsonStringifyBigInt)),
3819
- stateStore.put(
3820
- "info",
3821
- sortedStringify(
3822
- {
3823
- nativeBurnAmount: await this.totalBurnAmount,
3824
- nativeBurns: await this.nativeBurns,
3825
- nativeCalls: await this.nativeCalls,
3826
- nativeMints: await this.nativeMints,
3827
- nativeTransfers: await this.nativeTransfers,
3828
- totalBlocks: await blockStore.length,
3829
- totalBurnAmount: await this.totalBurnAmount,
3830
- totalMintAmount: await this.totalMintAmount,
3831
- totalTransactions: await this.totalTransactions,
3832
- totalTransferAmount: await this.totalTransferAmount
3833
- },
3834
- jsonStringifyBigInt
3835
- )
3836
- )
3837
- // accountsStore.clear()
3838
- ];
3839
- 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);
3840
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);
3841
4534
  } catch (error) {
3842
- console.error(error);
3843
4535
  if (strict) throw error;
4536
+ console.error(error);
4537
+ throw error;
3844
4538
  }
3845
4539
  }
3846
- async #init(blocks) {
3847
- return new Promise(async (resolve) => {
3848
- const machineReady = async () => {
3849
- pubsub.unsubscribe("machine.ready", machineReady);
3850
- await this.updateState();
3851
- resolve(this);
3852
- };
3853
- pubsub.subscribe("machine.ready", machineReady);
3854
- let pre;
3855
- try {
3856
- const importee = await import('url');
3857
- const url = importee.default;
3858
- if (url) pre = url.fileURLToPath(new URL(".", import.meta.url));
3859
- } catch {
3860
- pre = "./";
3861
- }
3862
- this.worker = await new EasyWorker(pre + "workers/machine-worker.js", {
3863
- serialization: "advanced",
3864
- type: "module"
3865
- });
3866
- this.worker.onmessage(this.#onmessage.bind(this));
3867
- let rawLastBlock;
3868
- let rawStates;
3869
- let rawAccounts;
3870
- let rawInfo;
3871
- if (await stateStore.has("lastBlock")) {
3872
- const decoder = new TextDecoder();
3873
- const decode = (param) => decoder.decode(param);
3874
- rawLastBlock = decode(await stateStore.get("lastBlock"));
3875
- this.states.lastBlock = JSON.parse(rawLastBlock, jsonParseBigInt);
3876
- try {
3877
- rawStates = decode(await stateStore.get("states"));
3878
- rawAccounts = decode(await stateStore.get("accounts"));
3879
- rawInfo = decode(await stateStore.get("info"));
3880
- this.states.states = JSON.parse(rawStates, jsonParseBigInt);
3881
- this.states.accounts = JSON.parse(rawAccounts, jsonParseBigInt);
3882
- this.states.info = JSON.parse(rawInfo, jsonParseBigInt);
3883
- } catch (error) {
3884
- console.error(error);
3885
- this.states.accounts = {};
3886
- this.states.info = {
3887
- nativeCalls: BigInt(0),
3888
- nativeMints: BigInt(0),
3889
- nativeBurns: BigInt(0),
3890
- nativeTransfers: BigInt(0),
3891
- totalBurnAmount: BigInt(0),
3892
- totalMintAmount: BigInt(0),
3893
- totalTransferAmount: BigInt(0),
3894
- totalTransactions: BigInt(0),
3895
- totalBlocks: BigInt(0)
3896
- };
3897
- rawInfo = JSON.stringify(this.states.info, jsonStringifyBigInt);
3898
- await stateStore.put("info", new TextEncoder().encode(rawInfo));
3899
- }
3900
- }
3901
- const message = {
3902
- type: "init",
3903
- input: {
3904
- blocks,
3905
- fromState: this.states.lastBlock?.index >= 0,
3906
- lastBlock: rawLastBlock,
3907
- state: rawStates,
3908
- info: rawInfo,
3909
- // @ts-ignore
3910
- peerid: peernet.peerId
3911
- }
3912
- };
3913
- 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
3914
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"));
3915
4576
  }
3916
4577
  async #runContract(contractMessage) {
3917
4578
  const hash = await contractMessage.hash();
@@ -3942,6 +4603,7 @@ class Machine {
3942
4603
  * @returns Promise<message>
3943
4604
  */
3944
4605
  async execute(contract, method, parameters, sender, execution) {
4606
+ if (this.#closed) throw new Error("State machine is closed");
3945
4607
  try {
3946
4608
  if (contract === contractFactory && method === "registerContract") {
3947
4609
  if (await this.has(parameters[0])) throw new Error(`duplicate contract @${parameters[0]}`);
@@ -3998,28 +4660,8 @@ ${error.message}`);
3998
4660
  return this.#askWorker("settleRewards", { rewards });
3999
4661
  }
4000
4662
  get(contract, method, parameters) {
4001
- return new Promise((resolve, reject) => {
4002
- const id = randombytes(20).toString();
4003
- const timeout = setTimeout(() => {
4004
- pubsub.unsubscribe(id, onmessage);
4005
- reject(new Error(`Machine.get timeout for ${contract}.${method}`));
4006
- }, 3e4);
4007
- const onmessage = (message) => {
4008
- clearTimeout(timeout);
4009
- pubsub.unsubscribe(id, onmessage);
4010
- resolve(message);
4011
- };
4012
- pubsub.subscribe(id, onmessage);
4013
- this.worker.postMessage({
4014
- type: "get",
4015
- id,
4016
- input: {
4017
- contract,
4018
- method,
4019
- params: parameters
4020
- }
4021
- });
4022
- });
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 ?? []);
4023
4665
  }
4024
4666
  async has(address) {
4025
4667
  return new Promise((resolve, reject) => {
@@ -4045,6 +4687,7 @@ ${error.message}`);
4045
4687
  });
4046
4688
  }
4047
4689
  #askWorker(type, input) {
4690
+ if (this.#closed) return Promise.reject(new Error("State machine is closed"));
4048
4691
  return new Promise((resolve, reject) => {
4049
4692
  const id = randombytes(20).toString("hex");
4050
4693
  const timeout = setTimeout(() => {
@@ -4102,19 +4745,13 @@ ${error.message}`);
4102
4745
  return this.getBlocks();
4103
4746
  }
4104
4747
  get lastBlock() {
4105
- return this.#askWorker("lastBlock").catch((error) => {
4106
- debug$2("lastBlock fallback after worker timeout:", error?.message ?? error);
4107
- return this.states.lastBlock;
4108
- });
4748
+ return Promise.resolve(this.states.lastBlock);
4109
4749
  }
4110
4750
  getCanonicalTip() {
4111
4751
  return this.#askWorker("lastBlock");
4112
4752
  }
4113
4753
  get lastBlockHeight() {
4114
- return this.#askWorker("lastBlockHeight").catch((error) => {
4115
- debug$2("lastBlockHeight fallback after worker timeout:", error?.message ?? error);
4116
- return Number(this.states.lastBlock?.index ?? 0);
4117
- });
4754
+ return Promise.resolve(Number(this.states.lastBlock.index));
4118
4755
  }
4119
4756
  getBlocks(from, to) {
4120
4757
  return this.#askWorker("blocks", { from, to });
@@ -4125,7 +4762,7 @@ ${error.message}`);
4125
4762
  async addLoadedBlock(block) {
4126
4763
  debug$2(`adding loaded block: ${block.index}@${block.hash}`);
4127
4764
  if (block.decoded) block = { ...block.decoded, hash: await block.hash() };
4128
- return this.#askWorker("addLoadedBlock", JSON.stringify(block, jsonStringifyBigInt));
4765
+ return this.#askWorker("addLoadedBlock", block);
4129
4766
  }
4130
4767
  async latestTransactions() {
4131
4768
  return this.#askWorker("latestTransactions");
@@ -4173,91 +4810,19 @@ class Jobber {
4173
4810
  }
4174
4811
  }
4175
4812
 
4176
- const MAX_RESPONSE_DEPTH$1 = 3;
4177
- const asLastBlockMessage = async (blockInput, claimedHash) => {
4178
- const block = new BlockMessage(blockInput);
4179
- const hash = await block.hash();
4180
- if (claimedHash && hash !== claimedHash) {
4181
- throw new Error(`lastBlock hash mismatch: expected ${claimedHash}, got ${hash}`);
4182
- }
4183
- return new LastBlockMessage({ hash, index: block.decoded.index });
4184
- };
4185
- const resolveLegacyHash = async (hash) => {
4186
- const blockData = await globalThis.peernet?.get?.(hash, "block");
4187
- if (!blockData) throw new Error(`unable to resolve lastBlock hash: ${hash}`);
4188
- return asLastBlockMessage(blockData, hash);
4189
- };
4190
- const resolveLastBlockMessage = async (result, depth = 0) => {
4191
- if (depth > MAX_RESPONSE_DEPTH$1) throw new Error("lastBlock response nesting exceeds limit");
4192
- if (result instanceof Uint8Array) {
4193
- let codec;
4194
- try {
4195
- codec = new Codec(result);
4196
- } catch {
4197
- }
4198
- if (codec) {
4199
- if (codec.name === "peernet-response") {
4200
- const ResponseMessage = globalThis.peernet?.protos?.["peernet-response"];
4201
- if (!ResponseMessage) throw new Error("peernet-response codec is not registered");
4202
- const wrapped = new ResponseMessage(result);
4203
- if (wrapped?.decoded?.response === void 0) throw new Error("peernet-response has no response payload");
4204
- return resolveLastBlockMessage(wrapped.decoded.response, depth + 1);
4205
- }
4206
- if (codec.name === "last-block-message") return new LastBlockMessage(result);
4207
- if (codec.name === "block-message") return asLastBlockMessage(result);
4208
- }
4209
- const candidate = new TextDecoder().decode(result);
4210
- if (candidate) return resolveLegacyHash(candidate);
4211
- }
4212
- if (typeof result === "string") return resolveLegacyHash(result);
4213
- if (result && typeof result === "object") {
4214
- const response = result.decoded?.response ?? result.response;
4215
- if (response !== void 0) return resolveLastBlockMessage(response, depth + 1);
4216
- if ("hash" in result && "index" in result) return new LastBlockMessage(result);
4217
- if ("previousHash" in result && "index" in result) return asLastBlockMessage(result);
4218
- }
4219
- throw new Error(`invalid lastBlock payload: ${typeof result}`);
4220
- };
4221
-
4222
- const MAX_RESPONSE_DEPTH = 3;
4223
- const decodeResponsePayload = async (result, depth = 0) => {
4224
- if (depth > MAX_RESPONSE_DEPTH) throw new Error("peernet response nesting exceeds limit");
4225
- if (result instanceof Uint8Array) {
4226
- let codec;
4227
- try {
4228
- codec = new Codec(result);
4229
- } catch {
4230
- }
4231
- if (codec?.name === "peernet-response") {
4232
- const ResponseMessage = globalThis.peernet?.protos?.["peernet-response"];
4233
- if (!ResponseMessage) throw new Error("peernet-response codec is not registered");
4234
- const wrapped = new ResponseMessage(result);
4235
- return decodeResponsePayload(wrapped.decoded.response, depth + 1);
4236
- }
4237
- try {
4238
- return JSON.parse(new TextDecoder().decode(result));
4239
- } catch {
4240
- return result;
4241
- }
4242
- }
4243
- if (result && typeof result === "object") {
4244
- const response = result.decoded?.response ?? result.response;
4245
- if (response !== void 0) return decodeResponsePayload(response, depth + 1);
4246
- }
4247
- if (typeof result === "string") {
4248
- try {
4249
- return JSON.parse(result);
4250
- } catch {
4251
- return result;
4252
- }
4253
- }
4254
- 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);
4255
4817
  };
4256
4818
 
4257
4819
  class CanonicalStateQueue {
4258
4820
  #tail = Promise.resolve();
4259
4821
  #failed = false;
4260
4822
  #failure;
4823
+ get failed() {
4824
+ return this.#failed;
4825
+ }
4261
4826
  run(operation) {
4262
4827
  const result = this.#tail.then(async () => {
4263
4828
  if (this.#failed) {
@@ -4283,27 +4848,50 @@ class CanonicalStateQueue {
4283
4848
  }
4284
4849
  }
4285
4850
 
4286
- const validateChainLink = (localTip, incoming) => {
4287
- const localHash = localTip?.hash || "0x0";
4288
- const localIndex = Number(localTip?.index ?? -1);
4289
- const hasCanonicalTip = localHash !== "0x0";
4290
- if (hasCanonicalTip && incoming.index < localIndex) return "stale";
4291
- if (hasCanonicalTip && incoming.index === localIndex) {
4292
- if (incoming.hash === localHash) return "duplicate";
4293
- throw new Error(`Block conflict detected at index ${incoming.index}`);
4294
- }
4295
- const expectedIndex = hasCanonicalTip ? localIndex + 1 : 0;
4296
- if (incoming.index !== expectedIndex) {
4297
- throw new Error(`Unexpected block index ${incoming.index}; expected ${expectedIndex}`);
4298
- }
4299
- if (incoming.previousHash !== localHash) {
4300
- throw new Error(
4301
- `previousHash mismatch at index ${incoming.index}: expected ${localHash}, got ${incoming.previousHash}`
4302
- );
4851
+ const resolveTransactionReference = async (expectedHash, transactionData) => {
4852
+ const transaction = transactionData instanceof TransactionMessage ? transactionData : new TransactionMessage(transactionData);
4853
+ const actualHash = await transaction.hash();
4854
+ if (actualHash !== expectedHash) {
4855
+ throw new Error(`Transaction hash mismatch: expected ${expectedHash}, got ${actualHash}`);
4303
4856
  }
4304
- return "append";
4857
+ return transaction;
4305
4858
  };
4306
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
+
4307
4895
  const debug$1 = createDebugger("leofcoin/state");
4308
4896
  class State extends Contract {
4309
4897
  constructor(config) {
@@ -4320,7 +4908,7 @@ class State extends Contract {
4320
4908
  this.#totalSize = 0;
4321
4909
  this.#canonicalStateQueue = new CanonicalStateQueue();
4322
4910
  this.#loaded = false;
4323
- this.#resolvingHashes = /* @__PURE__ */ new Set();
4911
+ this.#resolvingHashes = /* @__PURE__ */ new Map();
4324
4912
  this._wantList = [];
4325
4913
  this.#chainStateHandler = () => {
4326
4914
  return new globalThis.peernet.protos["peernet-response"]({
@@ -4337,15 +4925,6 @@ class State extends Contract {
4337
4925
  response: { blocks: await globalThis.blockStore.keys() }
4338
4926
  });
4339
4927
  };
4340
- this.#loadBlockTransactions = (transactions) => Promise.all(
4341
- transactions.map(async (transaction) => new TransactionMessage(await peernet.get(transaction, "transaction")))
4342
- );
4343
- this.#getLastTransactions = async () => {
4344
- let lastTransactions = (await Promise.all(
4345
- (await this.blocks).filter((block) => block.loaded).slice(-24).map((block) => this.#loadBlockTransactions(block.transactions))
4346
- )).reduce((all, transactions) => [...all, ...transactions], []);
4347
- return Promise.all(lastTransactions.map((transaction) => transaction.hash()));
4348
- };
4349
4928
  }
4350
4929
  #resolveErrored;
4351
4930
  #lastResolvedTime;
@@ -4363,7 +4942,13 @@ class State extends Contract {
4363
4942
  #machine;
4364
4943
  #canonicalStateQueue;
4365
4944
  withCanonicalState(operation) {
4366
- 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
+ });
4367
4952
  }
4368
4953
  canonicalTip() {
4369
4954
  return this.#machine ? this.#machine.getCanonicalTip() : Promise.resolve({ index: -1, hash: "0x0", previousHash: "0x0" });
@@ -4463,91 +5048,79 @@ class State extends Contract {
4463
5048
  #lastBlockHandler;
4464
5049
  #knownBlocksHandler;
4465
5050
  async init() {
4466
- debug$1("State init start");
4467
5051
  this.jobber = new Jobber(this.resolveTimeout);
4468
5052
  await globalThis.peernet.addRequestHandler("lastBlock", this.#lastBlockHandler);
4469
5053
  await globalThis.peernet.addRequestHandler("knownBlocks", this.#knownBlocksHandler);
4470
5054
  await globalThis.peernet.addRequestHandler("chainState", this.#chainStateHandler);
4471
- let localBlockHash;
4472
- let blockMessage;
4473
- let localBlock;
4474
- console.log("State init before try-catch");
4475
5055
  try {
4476
- const rawBlock = await globalThis.chainStore.has("lastBlock");
4477
- console.log("State init after has lastBlock check");
4478
- if (rawBlock) {
4479
- console.log("State init after has lastBlock found");
4480
- console.log(rawBlock);
4481
- localBlockHash = new TextDecoder().decode(await globalThis.chainStore.get("lastBlock"));
4482
- console.log(localBlockHash);
4483
- if (localBlockHash !== "0x0") {
4484
- blockMessage = await globalThis.blockStore.get(localBlockHash);
4485
- console.log(blockMessage);
4486
- blockMessage = await new BlockMessage(blockMessage);
4487
- localBlock = { ...blockMessage.decoded, hash: localBlockHash };
4488
- }
4489
- console.log("State init after localBlock set");
4490
- } else {
4491
- 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;
4492
5068
  }
4493
- } catch {
4494
- console.log("e");
4495
- console.log("State init middle");
4496
- localBlock = { index: 0, hash: "0x0", previousHash: "0x0" };
4497
- }
4498
- console.log("State init middle");
4499
- try {
4500
- console.log("fetching known blocks from blockStore");
4501
- this.knownBlocks = await globalThis.blockStore.keys();
4502
- } catch (error) {
4503
- console.log("no local known blocks found");
4504
- }
4505
- try {
4506
- if (localBlock?.hash && localBlock.hash !== "0x0") {
4507
- try {
4508
- if (!await globalThis.stateStore.has("lastBlock")) {
4509
- await this.resolveBlocks();
4510
- }
4511
- } catch {
4512
- 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;
4513
5077
  }
4514
- } else {
4515
- await this.resolveBlocks();
4516
5078
  }
4517
- const machine = new Machine(this.#blocks);
4518
- 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);
4519
5095
  await machine.ready;
4520
5096
  this.#machine = machine;
4521
- const lastBlock = await this.#machine.lastBlock;
4522
- if (lastBlock.hash !== "0x0") {
4523
- await this.updateState(new BlockMessage(lastBlock));
4524
- }
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");
4525
5104
  this.#loaded = true;
4526
5105
  this.#chainState = "loaded";
4527
5106
  } catch (error) {
4528
- console.log("e");
4529
- if (isResolveError(error)) {
4530
- console.error(error);
4531
- }
4532
- console.log(error);
5107
+ this.#loaded = false;
5108
+ this.#chainState = "loading";
5109
+ this.#syncState = "errored";
5110
+ throw error;
4533
5111
  }
4534
5112
  }
4535
5113
  async updateState(message) {
4536
- try {
4537
- const hash = await message.hash();
4538
- await globalThis.chainStore.put("lastBlock", hash);
4539
- globalThis.pubsub.publish("lastBlock", message.encoded);
4540
- if (!this.#machine) {
4541
- const machine = new Machine(this.#blocks);
4542
- console.log(machine);
4543
- await machine.ready;
4544
- this.#machine = machine;
4545
- }
4546
- await this.#machine.updateState(true);
4547
- } catch (error) {
4548
- console.error(error);
4549
- throw error;
4550
- }
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);
4551
5124
  }
4552
5125
  getLatestBlock() {
4553
5126
  return this.#getLatestBlock();
@@ -4576,7 +5149,8 @@ class State extends Contract {
4576
5149
  if (!exists) {
4577
5150
  const data = await peernet.get(hash, "transaction");
4578
5151
  if (!data) throw new Error(`missing transaction data for ${hash}`);
4579
- await transactionStore.put(hash, data);
5152
+ const transaction = await resolveTransactionReference(hash, data);
5153
+ await transactionStore.put(hash, transaction.encoded);
4580
5154
  }
4581
5155
  const inPool = await transactionPoolStore.has(hash);
4582
5156
  if (inPool) await transactionPoolStore.delete(hash);
@@ -4584,84 +5158,61 @@ class State extends Contract {
4584
5158
  );
4585
5159
  }
4586
5160
  async #resolveBlock(hash) {
4587
- let index = this.#blockHashMap.get(hash);
4588
- let localHash = "0x0";
4589
- try {
4590
- const rawLocalHash = await globalThis.stateStore.get("lastBlock");
4591
- if (rawLocalHash) {
4592
- const decoded = JSON.parse(new TextDecoder().decode(rawLocalHash));
4593
- localHash = typeof decoded === "string" ? decoded : decoded?.hash ?? "0x0";
4594
- }
4595
- } catch (error) {
4596
- debug$1("no local state found");
4597
- }
4598
- if (this.#blocks[index]) {
4599
- const previousHash = this.#blocks[index].previousHash;
4600
- if (previousHash === localHash) return;
4601
- if (previousHash !== "0x0") {
4602
- return this.resolveBlock(previousHash);
4603
- } else {
4604
- return;
5161
+ const localHash = (await this.canonicalTip()).hash;
5162
+ const visited = /* @__PURE__ */ new Set();
5163
+ while (hash !== "0x0" && hash !== localHash) {
5164
+ if (visited.has(hash)) throw new ResolveError(`cyclic block ancestry at ${hash}`);
5165
+ visited.add(hash);
5166
+ const cachedIndex = this.#blockHashMap.get(hash);
5167
+ const cached = this.#blocks[cachedIndex];
5168
+ if (cached) {
5169
+ hash = cached.previousHash;
5170
+ continue;
4605
5171
  }
4606
- }
4607
- try {
4608
- const block = await this.getAndPutBlock(hash);
4609
- const promises = [];
4610
- if (block.decoded.previousHash !== "0x0" && block.decoded.previousHash !== localHash) {
4611
- promises.push(this.resolveBlock(block.decoded.previousHash));
5172
+ try {
5173
+ const block = await this.getAndPutBlock(hash);
5174
+ await this.#resolveTransactions(block.decoded.transactions);
5175
+ const index = block.decoded.index;
5176
+ const size = block.encoded.byteLength;
5177
+ this.#totalSize += size;
5178
+ this.#blocks[index] = { hash, ...block.decoded };
5179
+ this.#blockHashMap.set(hash, index);
5180
+ debug$1(`resolved block: ${hash} @${index} ${formatBytes(size)}`);
5181
+ publishObserverEvent(globalThis.pubsub, "block-resolved", { hash, index });
5182
+ this.#lastResolved = this.#blocks[index];
5183
+ this.#lastResolvedTime = Date.now();
5184
+ hash = block.decoded.previousHash;
5185
+ } catch (error) {
5186
+ throw new ResolveError(`block: ${hash}`, { cause: error });
4612
5187
  }
4613
- promises.push(this.#resolveTransactions(block.decoded.transactions));
4614
- await Promise.all(promises);
4615
- index = block.decoded.index;
4616
- const size = block.encoded.length > 0 ? block.encoded.length : block.encoded.byteLength;
4617
- this.#totalSize += size;
4618
- this.#blocks[index] = { hash, ...block.decoded };
4619
- this.#blockHashMap.set(hash, index);
4620
- debug$1(`resolved block: ${hash} @${index} ${formatBytes(size)}`);
4621
- globalThis.pubsub.publish("block-resolved", { hash, index });
4622
- this.#lastResolved = this.#blocks[index];
4623
- this.#lastResolvedTime = Date.now();
4624
- } catch (error) {
4625
- throw new ResolveError(`block: ${hash}@${index}`, { cause: error });
4626
5188
  }
4627
- return;
4628
5189
  }
4629
5190
  async resolveBlock(hash) {
4630
5191
  if (!hash) throw new Error(`expected hash, got: ${hash}`);
4631
5192
  if (hash === "0x0") return;
4632
- if (this.#resolvingHashes.has(hash)) return;
4633
- this.#resolvingHashes.add(hash);
4634
- const isEntering = this.#resolvingHashes.size === 1;
5193
+ const existing = this.#resolvingHashes.get(hash);
5194
+ if (existing) return existing;
4635
5195
  this.#resolving = true;
4636
- try {
4637
- if (isEntering) {
4638
- if (this.jobber.busy && this.jobber.destroy) await this.jobber.destroy();
4639
- await this.jobber.add(() => this.#resolveBlock(hash));
4640
- } else {
4641
- await this.#resolveBlock(hash);
4642
- }
4643
- try {
4644
- const lastBlockHash = await globalThis.stateStore.get("lastBlock");
4645
- if (lastBlockHash === hash) {
5196
+ const resolution = Promise.resolve().then(async () => {
5197
+ for (let attempt = 0; attempt < 3; attempt++) {
5198
+ try {
5199
+ await this.#resolveBlock(hash);
4646
5200
  this.#resolveErrored = false;
4647
5201
  return;
5202
+ } catch (error) {
5203
+ console.error(`block resolution attempt ${attempt + 1} failed for ${hash}`, error);
5204
+ if (attempt === 2) {
5205
+ if (!this.wantList.includes(hash)) this.wantList.push(hash);
5206
+ throw new ResolveError(`block: ${hash}`, { cause: error });
5207
+ }
4648
5208
  }
4649
- } catch (error) {
4650
- }
4651
- } catch (error) {
4652
- console.log({ error });
4653
- this.#resolveErrorCount += 1;
4654
- if (this.#resolveErrorCount < 3) {
4655
- this.#resolvingHashes.delete(hash);
4656
- return this.resolveBlock(hash);
4657
5209
  }
4658
- this.#resolveErrorCount = 0;
4659
- this.wantList.push(hash);
4660
- throw new ResolveError(`block: ${hash}`, { cause: error });
4661
- } finally {
5210
+ }).finally(() => {
4662
5211
  this.#resolvingHashes.delete(hash);
4663
5212
  if (this.#resolvingHashes.size === 0) this.#resolving = false;
4664
- }
5213
+ });
5214
+ this.#resolvingHashes.set(hash, resolution);
5215
+ return resolution;
4665
5216
  }
4666
5217
  async resolveBlocks() {
4667
5218
  if (this.#chainSyncing || this.#resolving) {
@@ -4764,7 +5315,7 @@ class State extends Contract {
4764
5315
  const localIndex = localBlock ? Number(localBlock.index) : -1;
4765
5316
  const remoteIndex = Number(lastBlock.index);
4766
5317
  const remoteBlockHash = lastBlock.hash;
4767
- globalThis.pubsub.publish("sync-progress", {
5318
+ publishObserverEvent(globalThis.pubsub, "sync-progress", {
4768
5319
  phase: "syncing",
4769
5320
  startIndex: localIndex,
4770
5321
  localIndex,
@@ -4798,26 +5349,9 @@ class State extends Contract {
4798
5349
  return;
4799
5350
  }
4800
5351
  if (localStateHash !== remoteBlockHash) {
4801
- if (this.wantList.length > 0) {
4802
- debug$1(`Fetching ${this.wantList.length} blocks before resolving`);
4803
- const getBatch = async (batch) => {
4804
- const blocks2 = await Promise.all(
4805
- batch.map(
4806
- (hash) => this.getAndPutBlock(hash).catch((e) => {
4807
- console.warn(`failed to fetch block ${hash}`, e);
4808
- })
4809
- )
4810
- );
4811
- const transactions = blocks2.filter((block) => Boolean(block)).flatMap((block) => block.decoded.transactions);
4812
- return this.#resolveTransactions(transactions);
4813
- };
4814
- for (let i = 0; i < this.wantList.length; i += 50) {
4815
- const batch = this.wantList.slice(i, i + 50);
4816
- await getBatch(batch);
4817
- }
4818
- }
4819
5352
  debug$1(`Resolving remote block: ${remoteBlockHash} @${remoteIndex} (differs from local state)`);
4820
5353
  await this.resolveBlock(remoteBlockHash);
5354
+ await globalThis.chainStore.put("recoveryTarget", remoteBlockHash);
4821
5355
  const blocksSynced = remoteIndex - localIndex;
4822
5356
  debug$1(`Resolved ${blocksSynced} new block(s)`);
4823
5357
  const blocks = this.#blocks;
@@ -4837,7 +5371,7 @@ class State extends Contract {
4837
5371
  } else {
4838
5372
  debug$1(`Block already in local state. Remote hash: ${remoteBlockHash} matches local state`);
4839
5373
  }
4840
- globalThis.pubsub.publish("sync-progress", {
5374
+ publishObserverEvent(globalThis.pubsub, "sync-progress", {
4841
5375
  phase: "synced",
4842
5376
  startIndex: localIndex,
4843
5377
  localIndex: remoteIndex,
@@ -4925,112 +5459,23 @@ class State extends Contract {
4925
5459
  }
4926
5460
  return latest;
4927
5461
  }
4928
- #loadBlockTransactions;
4929
- #getLastTransactions;
4930
- // todo throw error
4931
- async #_executeTransaction(transaction, validators, feesEnabled, burnBasisPoints, blockTimestamp) {
4932
- try {
4933
- const hash = await transaction.hash();
4934
- if (feesEnabled) {
4935
- const fee = BigInt(await calculateFee(transaction.decoded));
4936
- const { payments, burned } = distributeTransactionFee(fee, hash, validators, burnBasisPoints);
4937
- await this.#machine.collectFee(transaction.decoded.from, payments, burned);
4938
- }
4939
- await this.#machine.execute(
4940
- transaction.decoded.to,
4941
- transaction.decoded.method,
4942
- transaction.decoded.params,
4943
- transaction.decoded.from,
4944
- { seed: hash, timestamp: Number(blockTimestamp) }
4945
- );
4946
- } catch (error) {
4947
- console.log(error);
4948
- await globalThis.transactionPoolStore.delete(await transaction.hash());
4949
- console.log("removing invalid transaction");
4950
- if (isExecutionError(error)) {
4951
- console.log(error);
4952
- }
4953
- console.log(error);
4954
- throw error;
4955
- }
5462
+ async applyRecoveredBlock(block, transition) {
5463
+ throw new Error("Canonical block application is not implemented");
4956
5464
  }
4957
- /**
4958
- *
4959
- * @param {Block[]} blocks
4960
- */
4961
5465
  async #loadBlocks(blocks, transition) {
4962
5466
  this.#chainState = "loading";
4963
- if (blocks.some((block) => !block)) {
4964
- throw new Error("missing block data during load; chain resolution incomplete");
4965
- }
4966
- const poolTransactionKeys = new Set(await globalThis.transactionPoolStore.keys());
4967
- debug$1(`pool transactions: ${poolTransactionKeys.size}`);
4968
- debug$1(`loading ${blocks.length} blocks`);
4969
5467
  for (const block of blocks) {
4970
- if (block && !block.loaded) {
4971
- try {
4972
- const link = validateChainLink(await this.canonicalTip(), {
4973
- index: Number(block.index),
4974
- hash: block.hash,
4975
- previousHash: String(block.previousHash)
4976
- });
4977
- if (link !== "append") {
4978
- this.#blocks[block.index].loaded = true;
4979
- debug$1(`Ignoring ${link} loaded block ${block.hash} at height ${block.index}`);
4980
- continue;
4981
- }
4982
- debug$1(`loading block: ${Number(block.index)} ${block.hash}`);
4983
- let transactions = await this.#loadBlockTransactions(block.transactions || []);
4984
- const validators = block.validators.map(({ address }) => address);
4985
- debug$1(`loading transactions: ${transactions.length} for block ${block.index}`);
4986
- transition.markMutating();
4987
- for (const transaction of transactions) {
4988
- const hash = await transaction.hash();
4989
- if (poolTransactionKeys.has(hash)) await globalThis.transactionPoolStore.delete(hash);
4990
- }
4991
- transactions = transactions.sort((a, b) => {
4992
- if (a.decoded.priority !== b.decoded.priority) return a.decoded.priority ? -1 : 1;
4993
- const left = BigInt(a.decoded.nonce);
4994
- const right = BigInt(b.decoded.nonce);
4995
- return left < right ? -1 : left > right ? 1 : 0;
4996
- });
4997
- debug$1(`executing ${transactions.length} transactions for block ${block.index}`);
4998
- const feesEnabled = supportsTransactionFees(block.protocolVersion);
4999
- const monetaryPolicyEnabled = supportsMonetaryPolicy(block.protocolVersion);
5000
- if (monetaryPolicyEnabled) await validateBlockResourceLimits(transactions);
5001
- const policy = monetaryPolicyEnabled ? calculateMonetaryPolicy(
5002
- BigInt(await this.#machine.get(nativeToken, "totalSupply")),
5003
- BigInt(await this.#machine.get(nativeToken, "targetSupply"))
5004
- ) : { subsidy: 0n, burnBasisPoints: 1000n };
5005
- for (const transaction of transactions) {
5006
- await this.#_executeTransaction(
5007
- transaction,
5008
- validators,
5009
- feesEnabled,
5010
- policy.burnBasisPoints,
5011
- block.timestamp
5012
- );
5013
- }
5014
- if (monetaryPolicyEnabled && policy.subsidy > 0n) {
5015
- await this.#machine.settleRewards([
5016
- ...distributeAmount(policy.subsidy, validators, Number(block.index) % validators.length)
5017
- ]);
5018
- }
5019
- debug$1(`executed transactions for block ${block.index}`);
5020
- const registered = await this.#machine.addLoadedBlock({ ...block, loaded: true });
5021
- if (registered !== true) throw new Error(`Loaded block registration failed after execution: ${block.hash}`);
5022
- this.#blocks[block.index].loaded = true;
5023
- if (Number(block.index) === 0) this.#loaded = true;
5024
- debug$1(`loaded block: ${block.hash} @${Number(block.index)}`);
5025
- globalThis.pubsub.publish("block-loaded", { ...block });
5026
- } catch (error) {
5027
- console.error(error);
5028
- for (const transaction of block.transactions) {
5029
- this.wantList.push(transaction);
5030
- }
5031
- throw error;
5032
- }
5033
- }
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 });
5034
5479
  }
5035
5480
  this.#chainState = "loaded";
5036
5481
  return true;
@@ -5090,18 +5535,16 @@ class State extends Contract {
5090
5535
  return this.syncChain(latest);
5091
5536
  }
5092
5537
  async triggerLoad() {
5093
- if (this.#blocks?.length > 0) {
5094
- const machine = new Machine(this.#blocks);
5095
- console.log(machine);
5096
- await machine.ready;
5097
- this.#machine = machine;
5098
- }
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));
5099
5541
  }
5100
5542
  }
5101
5543
 
5102
5544
  class VersionControl extends State {
5103
5545
  constructor(config) {
5104
5546
  super(config);
5547
+ this.version = PROTOCOL_VERSION;
5105
5548
  }
5106
5549
  #currentVersion = PROTOCOL_VERSION;
5107
5550
  #reachedOneZeroZero = REACHED_ONE_ZERO_ZERO;
@@ -5171,15 +5614,6 @@ const validateBlockEconomics = (block, calculatedFees, validatorFees = /* @__PUR
5171
5614
  }
5172
5615
  };
5173
5616
 
5174
- const resolveTransactionReference = async (expectedHash, transactionData) => {
5175
- const transaction = transactionData instanceof TransactionMessage ? transactionData : new TransactionMessage(transactionData);
5176
- const actualHash = await transaction.hash();
5177
- if (actualHash !== expectedHash) {
5178
- throw new Error(`Transaction hash mismatch: expected ${expectedHash}, got ${actualHash}`);
5179
- }
5180
- return transaction;
5181
- };
5182
-
5183
5617
  const consensusSignableData = (validatorsAddress, type, message) => ({
5184
5618
  from: String(message.from),
5185
5619
  to: validatorsAddress,
@@ -5254,80 +5688,13 @@ const assertHashFormatState = async (chainStore, stateStore) => {
5254
5688
  const parsed = /^(\d+)\.(\d+)\.\d+$/.exec(version);
5255
5689
  const current = PROTOCOL_VERSION.split(".");
5256
5690
  if (parsed && parsed[1] === current[0] && parsed[2] === current[1]) return;
5257
- 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)`);
5258
5692
  }
5259
- 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")) {
5260
5694
  throw new Error("existing chainstate has no protocol version; explicit canonical hash migration required");
5261
5695
  }
5262
5696
  };
5263
5697
 
5264
- const CHAIN_ID_SCHEMA = "leofcoin:chain-identity:v1";
5265
- const GENESIS_CONTRACT_NAMES = ["contractFactory", "nativeToken", "nameService", "validators"];
5266
- const assertChainId = (value) => {
5267
- if (typeof value !== "string" || !/^lfc:[0-9a-f]{64}$/.test(value))
5268
- throw new Error("Invalid chain ID; expected lfc: followed by 64 lowercase hex characters");
5269
- return value;
5270
- };
5271
- const createChainIdentity = async (network, contracts) => {
5272
- if (typeof network !== "string" || !/^[a-z0-9][a-z0-9:-]{0,79}$/.test(network))
5273
- throw new Error("Invalid genesis network namespace");
5274
- const genesisContracts = {};
5275
- for (const name of GENESIS_CONTRACT_NAMES) {
5276
- const hash = contracts?.[name];
5277
- if (typeof hash !== "string" || !/^[A-Z2-7]{32,128}$/.test(hash))
5278
- throw new Error(`Invalid genesis contract hash: ${name}`);
5279
- genesisContracts[name] = hash;
5280
- }
5281
- const bytes = new TextEncoder().encode(JSON.stringify({ schema: CHAIN_ID_SCHEMA, network, genesisContracts }));
5282
- const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
5283
- const chainId = "lfc:" + [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
5284
- return { schema: CHAIN_ID_SCHEMA, chainId, network, genesisContracts };
5285
- };
5286
- const verifyChainIdentity = async (value) => {
5287
- const identity = value;
5288
- if (!identity || identity.schema !== CHAIN_ID_SCHEMA)
5289
- throw new Error("Unsupported chain identity response");
5290
- const verified = await createChainIdentity(identity.network, identity.genesisContracts);
5291
- if (verified.chainId !== assertChainId(identity.chainId))
5292
- throw new Error("Chain ID does not match its genesis contracts");
5293
- return verified;
5294
- };
5295
- const findChainPresence = async (chainId, peerIds, requestIdentity, timeoutMs = 3e3) => {
5296
- assertChainId(chainId);
5297
- const peers = [...new Set(peerIds)].slice(0, 16);
5298
- const matches = [];
5299
- let responded = 0;
5300
- let unavailable = 0;
5301
- await Promise.all(peers.map(async (peerId) => {
5302
- let timer;
5303
- try {
5304
- const result = await Promise.race([
5305
- Promise.resolve().then(() => requestIdentity(peerId)),
5306
- new Promise((_, reject) => {
5307
- timer = setTimeout(() => reject(new Error("Chain identity request timed out")), timeoutMs);
5308
- })
5309
- ]);
5310
- const identity = await verifyChainIdentity(result);
5311
- responded++;
5312
- if (identity.chainId === chainId)
5313
- matches.push(peerId);
5314
- } catch {
5315
- unavailable++;
5316
- } finally {
5317
- clearTimeout(timer);
5318
- }
5319
- }));
5320
- return {
5321
- chainId,
5322
- status: matches.length ? "found" : !peers.length || unavailable ? "unknown" : "not-observed",
5323
- checkedPeers: peers.length,
5324
- respondedPeers: responded,
5325
- unavailablePeers: unavailable,
5326
- matchingPeers: matches.sort(),
5327
- scope: "reachable peers only; offline or undiscovered chains may still exist"
5328
- };
5329
- };
5330
-
5331
5698
  const debug = createDebugger("leofcoin/chain");
5332
5699
  class Chain extends VersionControl {
5333
5700
  constructor(config) {
@@ -5927,10 +6294,10 @@ class Chain extends VersionControl {
5927
6294
  });
5928
6295
  });
5929
6296
  globalThis.peernet.subscribe("add-block", this.#addBlock.bind(this));
5930
- globalThis.peernet.subscribe("invalid-transaction", this.#invalidTransaction.bind(this));
6297
+ globalThis.peernet.subscribe("invalid-transaction", (bytes) => this.#invalidTransaction(decodeWire$1("invalid-transaction", bytes)));
5931
6298
  globalThis.peernet.subscribe("send-transaction", this.#sendTransaction.bind(this));
5932
6299
  globalThis.peernet.subscribe("add-transaction", this.#addTransaction.bind(this));
5933
- globalThis.peernet.subscribe("validator:timeout", this.#validatorTimeout.bind(this));
6300
+ globalThis.peernet.subscribe("validator:timeout", (bytes) => this.#validatorTimeout(decodeWire$1("validator:timeout", bytes)));
5934
6301
  globalThis.peernet.subscribe("consensus:propose", this.#handleProposal.bind(this));
5935
6302
  globalThis.peernet.subscribe("consensus:prevote", this.#handlePrevote.bind(this));
5936
6303
  globalThis.peernet.subscribe("consensus:precommit", this.#handlePrecommit.bind(this));
@@ -5976,52 +6343,7 @@ class Chain extends VersionControl {
5976
6343
  let response = await peer.request(node.encoded);
5977
6344
  console.log(`raw response for request ${request}:`, response);
5978
6345
  if (response === void 0 || response === null) return response;
5979
- const normalizeResponse = async (payload) => {
5980
- if (payload === void 0 || payload === null) return payload;
5981
- if (payload instanceof Uint8Array || typeof Buffer !== "undefined" && Buffer.isBuffer(payload)) {
5982
- let bytes = payload instanceof Uint8Array ? payload : new Uint8Array(payload);
5983
- for (let i = 0; i < 3; i += 1) {
5984
- try {
5985
- const codec = new Codec(bytes);
5986
- if (codec.name !== "peernet-response") break;
5987
- const wrapped = await new globalThis.peernet.protos["peernet-response"](bytes);
5988
- if (wrapped?.decoded?.response === void 0) break;
5989
- const next = wrapped.decoded.response;
5990
- if (next instanceof Uint8Array || typeof Buffer !== "undefined" && Buffer.isBuffer(next)) {
5991
- bytes = next instanceof Uint8Array ? next : new Uint8Array(next);
5992
- continue;
5993
- }
5994
- return normalizeResponse(next);
5995
- } catch {
5996
- break;
5997
- }
5998
- }
5999
- try {
6000
- return JSON.parse(new TextDecoder().decode(bytes));
6001
- } catch {
6002
- return bytes;
6003
- }
6004
- }
6005
- if (payload && typeof payload === "object") {
6006
- const objectPayload = payload;
6007
- const decoded = objectPayload.decoded;
6008
- if ("decoded" in objectPayload && decoded && "response" in decoded) {
6009
- return normalizeResponse(decoded.response);
6010
- }
6011
- if ("response" in objectPayload) {
6012
- return normalizeResponse(objectPayload.response);
6013
- }
6014
- }
6015
- if (typeof payload === "string") {
6016
- try {
6017
- return JSON.parse(payload);
6018
- } catch {
6019
- return payload;
6020
- }
6021
- }
6022
- return payload;
6023
- };
6024
- return await normalizeResponse(response);
6346
+ return decodeResponsePayload(response);
6025
6347
  } catch (error) {
6026
6348
  const peerId = peer?.peerId || peer?.id || peer?.address || "unknown";
6027
6349
  debug(`peernet request failed: ${request} -> ${peerId}:`, error?.message ?? error);
@@ -6029,60 +6351,11 @@ class Chain extends VersionControl {
6029
6351
  }
6030
6352
  }
6031
6353
  async #decodeKnownBlocksResponse(response) {
6032
- if (!response) return null;
6033
- if (Array.isArray(response)) {
6034
- return { blocks: response };
6035
- }
6036
- if (Array.isArray(response.blocks)) {
6037
- return { blocks: response.blocks };
6038
- }
6039
- if (response.response && Array.isArray(response.response.blocks)) {
6040
- return { blocks: response.response.blocks };
6041
- }
6042
- if (typeof response === "string") {
6043
- try {
6044
- const parsed = JSON.parse(response);
6045
- if (Array.isArray(parsed)) return { blocks: parsed };
6046
- if (parsed && Array.isArray(parsed.blocks)) return { blocks: parsed.blocks };
6047
- if (parsed?.response && Array.isArray(parsed.response.blocks)) return { blocks: parsed.response.blocks };
6048
- } catch {
6049
- return null;
6050
- }
6051
- return null;
6052
- }
6053
- if (!(response instanceof Uint8Array)) return null;
6054
- let payload = response;
6055
- for (let i = 0; i < 3; i++) {
6056
- if (!(payload instanceof Uint8Array)) break;
6057
- try {
6058
- const nestedResponse = await new globalThis.peernet.protos["peernet-response"](payload);
6059
- payload = nestedResponse?.decoded?.response;
6060
- } catch {
6061
- break;
6062
- }
6063
- }
6064
- if (Array.isArray(payload)) return { blocks: payload };
6065
- if (payload && Array.isArray(payload.blocks)) return { blocks: payload.blocks };
6066
- if (payload?.response && Array.isArray(payload.response.blocks)) return { blocks: payload.response.blocks };
6067
- try {
6068
- const decoded = new TextDecoder().decode(response);
6069
- const parsed = JSON.parse(decoded);
6070
- if (Array.isArray(parsed)) return { blocks: parsed };
6071
- if (parsed && Array.isArray(parsed.blocks)) return { blocks: parsed.blocks };
6072
- if (parsed?.response && Array.isArray(parsed.response.blocks)) return { blocks: parsed.response.blocks };
6073
- } catch {
6074
- return null;
6075
- }
6076
- return null;
6354
+ if (!Array.isArray(response) || response.length > 1e5 || response.some((hash) => typeof hash !== "string")) return null;
6355
+ return { blocks: response };
6077
6356
  }
6078
6357
  async getPeerTransactionPool(peer) {
6079
6358
  let transactionsInPool = await this.#makeRequest(peer, "transactionPool");
6080
- if (transactionsInPool instanceof Uint8Array) {
6081
- transactionsInPool = new TextDecoder().decode(transactionsInPool);
6082
- }
6083
- if (typeof transactionsInPool === "string") {
6084
- transactionsInPool = JSON.parse(transactionsInPool);
6085
- }
6086
6359
  if (!Array.isArray(transactionsInPool) || transactionsInPool.some((hash) => typeof hash !== "string")) {
6087
6360
  throw new Error("transactionPool response must be an array of transaction hashes");
6088
6361
  }
@@ -6207,13 +6480,6 @@ class Chain extends VersionControl {
6207
6480
  }
6208
6481
  }
6209
6482
  }
6210
- if (this.wantList.length > 0) {
6211
- const promises = await Promise.allSettled(this.wantList.map((hash) => peernet.get(hash, "block")));
6212
- for (let i = promises.length - 1; i >= 0; i -= 1) {
6213
- if (promises[i].status === "fulfilled") this.wantList.splice(i, 1);
6214
- }
6215
- if (this.wantList.length === 0) await this.triggerSync();
6216
- }
6217
6483
  try {
6218
6484
  let stateInfo = await this.#makeRequest(peer, "stateInfo");
6219
6485
  if (stateInfo instanceof Uint8Array) {
@@ -6227,7 +6493,6 @@ class Chain extends VersionControl {
6227
6493
  debug(
6228
6494
  `sync finished with peer ${peerId}: state=${this.syncState} localNow=${(await this.lastBlock)?.index ?? -1}`
6229
6495
  );
6230
- this.machine.states.info = stateInfo;
6231
6496
  } catch (error) {
6232
6497
  const peerName = peer?.peerId || peer?.id || peer?.address || peerId || "unknown";
6233
6498
  debug(`stateInfo/syncChain failed: ${peerName}:`, error?.message ?? error);
@@ -6265,7 +6530,7 @@ class Chain extends VersionControl {
6265
6530
  } catch (error) {
6266
6531
  await transactionPoolStore.delete(hash);
6267
6532
  try {
6268
- globalThis.peernet.publish("invalid-transaction", hash);
6533
+ globalThis.peernet.publish("invalid-transaction", encodeWire$1("invalid-transaction", hash));
6269
6534
  } catch (publishError) {
6270
6535
  debug("peernet publish failed: invalid-transaction", publishError?.message ?? publishError);
6271
6536
  }
@@ -6276,7 +6541,13 @@ class Chain extends VersionControl {
6276
6541
  async #addBlock(block) {
6277
6542
  return this.withCanonicalState((transition) => this.#applyBlock(block, transition));
6278
6543
  }
6279
- 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) {
6280
6551
  const receivedEncoded = block instanceof BlockMessage ? block.encoded : block;
6281
6552
  const blockMessage = await new BlockMessage(block);
6282
6553
  const hash = await blockMessage.hash();
@@ -6310,13 +6581,14 @@ class Chain extends VersionControl {
6310
6581
  console.log(`[chain] \u2705 Block data integrity verified: ${hash}`);
6311
6582
  await this.#validateBlockValidators(blockMessage);
6312
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);
6313
6586
  transition.markMutating();
6314
6587
  await Promise.all(
6315
6588
  blockMessage.decoded.transactions.map(async (transactionHash) => {
6316
6589
  if (await transactionPoolStore.has(transactionHash)) await transactionPoolStore.delete(transactionHash);
6317
6590
  })
6318
6591
  );
6319
- await globalThis.blockStore.put(hash, blockMessage.encoded);
6320
6592
  this.#blocks[blockIndex] = {
6321
6593
  hash,
6322
6594
  ...blockMessage.decoded
@@ -6350,9 +6622,6 @@ class Chain extends VersionControl {
6350
6622
  }
6351
6623
  const noncesByAddress = {};
6352
6624
  for (let transaction of transactions) {
6353
- globalThis.pubsub.publish("transaction-processed", transaction.encoded);
6354
- if (transaction.decoded.to === globalThis.peernet.selectedAccount)
6355
- globalThis.pubsub.publish("account-transaction-processed", transaction.encoded);
6356
6625
  if (!noncesByAddress[transaction.decoded.from] || noncesByAddress?.[transaction.decoded.from] < transaction.decoded.nonce) {
6357
6626
  noncesByAddress[transaction.decoded.from] = transaction.decoded.nonce;
6358
6627
  }
@@ -6370,14 +6639,19 @@ class Chain extends VersionControl {
6370
6639
  const registered = await this.machine.addLoadedBlock({ ...blockMessage.decoded, loaded: true, hash });
6371
6640
  if (registered !== true) throw new Error(`Block registration failed after execution: ${hash}`);
6372
6641
  try {
6373
- await this.call(addresses.validators, "recordValidatorSnapshot", [blockMessage.decoded.index]);
6642
+ await this.#recordValidatorSnapshot(blockMessage.decoded.index);
6374
6643
  } catch (snapshotError) {
6375
6644
  debug(`failed to record validator snapshot: ${snapshotError?.message ?? snapshotError}`);
6376
6645
  }
6377
6646
  await this.updateState(blockMessage);
6378
6647
  debug(`added block: ${hash}`);
6379
- globalThis.pubsub.publish("block-processed", blockMessage.decoded);
6380
- 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);
6381
6655
  }
6382
6656
  async participate(address) {
6383
6657
  try {
@@ -6425,15 +6699,14 @@ class Chain extends VersionControl {
6425
6699
  async #handleTransaction(transaction, latestTransactions, block, feePayments = { payments: [], burned: 0n }) {
6426
6700
  await this.validateTransactionSignature(transaction);
6427
6701
  const hash = await transaction.hash();
6428
- const doubleTransactions = [];
6429
- if (latestTransactions.includes(hash) || await transactionStore.has(hash)) {
6430
- doubleTransactions.push(hash);
6431
- }
6432
- if (doubleTransactions.length > 0) {
6433
- await globalThis.transactionPoolStore.delete(hash);
6434
- await globalThis.peernet.publish("invalid-transaction", hash);
6435
- return;
6436
- }
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}`);
6437
6710
  try {
6438
6711
  const result = await this.#executeTransaction({
6439
6712
  ...transaction.decoded,
@@ -6450,7 +6723,7 @@ class Chain extends VersionControl {
6450
6723
  console.log("vvvvvv");
6451
6724
  console.log({ e });
6452
6725
  console.log(hash);
6453
- peernet.publish("invalid-transaction", hash);
6726
+ peernet.publish("invalid-transaction", encodeWire$1("invalid-transaction", hash));
6454
6727
  await globalThis.transactionPoolStore.delete(e.hash);
6455
6728
  if (block) throw e;
6456
6729
  }
@@ -6615,7 +6888,7 @@ class Chain extends VersionControl {
6615
6888
  } catch (e) {
6616
6889
  if (hash) {
6617
6890
  try {
6618
- await globalThis.peernet.publish("invalid-transaction", hash);
6891
+ await globalThis.peernet.publish("invalid-transaction", encodeWire$1("invalid-transaction", hash));
6619
6892
  } catch (publishError) {
6620
6893
  debug("peernet publish failed: invalid-transaction", publishError?.message ?? publishError);
6621
6894
  }
@@ -6629,7 +6902,8 @@ class Chain extends VersionControl {
6629
6902
  * error is thrown on error so undefined data doesn't mean there is an error...
6630
6903
  **/
6631
6904
  async sendTransaction(transaction) {
6632
- const transactionMessage = await new TransactionMessage({ ...transaction });
6905
+ const { hash: ignoredHash, ...payload } = transaction instanceof TransactionMessage ? transaction.decoded : transaction;
6906
+ const transactionMessage = await new TransactionMessage(payload);
6633
6907
  const event = await super.sendTransaction(transactionMessage);
6634
6908
  await this.#sendTransaction(transactionMessage.encoded);
6635
6909
  try {
@@ -6669,7 +6943,7 @@ class Chain extends VersionControl {
6669
6943
  * @returns
6670
6944
  */
6671
6945
  internalCall(sender, contract, method, parameters) {
6672
- return this.machine.execute(contract, method, parameters, sender);
6946
+ throw new Error("Direct sender impersonation is disabled; submit a signed transaction with sendTransaction()");
6673
6947
  }
6674
6948
  /**
6675
6949
  *
@@ -6678,18 +6952,23 @@ class Chain extends VersionControl {
6678
6952
  * @param {Array} parameters
6679
6953
  * @returns
6680
6954
  */
6681
- call(contract, method, parameters) {
6682
- 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));
6683
6960
  }
6684
6961
  staticCall(contract, method, parameters) {
6685
6962
  return this.machine.get(contract, method, parameters);
6686
6963
  }
6687
- mint(to, amount) {
6688
- return this.call(addresses.nativeToken, "mint", [to, amount]);
6964
+ mint(...args) {
6965
+ throw new Error("Native minting is restricted to the monetary protocol");
6689
6966
  }
6690
- transfer(from, to, amount) {
6691
- if (from !== globalThis.peernet.selectedAccount) throw new Error("transfer sender must be the selected account");
6692
- 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);
6693
6972
  }
6694
6973
  balanceOf(address) {
6695
6974
  return this.staticCall(addresses.nativeToken, "balanceOf", [address]);
@@ -6716,7 +6995,7 @@ class Chain extends VersionControl {
6716
6995
  * @example chain.lookup('myCoolContractName') // qmqsfddfdgfg...
6717
6996
  */
6718
6997
  lookup(name) {
6719
- return this.call(addresses.nameService, "lookup", [name]);
6998
+ return this.staticCall(addresses.nameService, "lookup", [name]);
6720
6999
  }
6721
7000
  #monitorPeerConnections() {
6722
7001
  setInterval(() => {