@leofcoin/chain 1.10.20 → 1.10.22

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.
@@ -1,6 +1,6 @@
1
- import { F as FormatInterface } from './node-browser-Ha4Lvntj.js';
1
+ import { F as FormatInterface } from './node-browser-BMfelxXV.js';
2
2
  import './browser-crypto-CH0ua22L.js';
3
- import './connection-monitor-D-krNiRH.js';
3
+ import './connection-monitor-DMGAhcC0.js';
4
4
  import './index-CvKt4UDE.js';
5
5
  import './sha2-BKcEJzgd.js';
6
6
 
@@ -1,5 +1,5 @@
1
1
  import { F as FormatInterface$2, T as TransactionMessage, C as ContractMessage, n as createHash$1 } from './browser-crypto-CH0ua22L.js';
2
- import { B as BlockMessage, a as BWMessage, V as ValidatorMessage, S as StateMessage, L as LastBlockMessage, D as DEFAULT_NODE_OPTIONS, g as guardPeerMessages } from './connection-monitor-D-krNiRH.js';
2
+ import { B as BlockMessage, b as BWMessage, V as ValidatorMessage, S as StateMessage, L as LastBlockMessage, D as DEFAULT_NODE_OPTIONS, g as guardPeerMessages, c as guardProviderDiscovery } from './connection-monitor-DMGAhcC0.js';
3
3
  import './index-CvKt4UDE.js';
4
4
 
5
5
  var proto = {};
@@ -8510,7 +8510,7 @@ class Identity {
8510
8510
  this.selectedAccount = new TextDecoder().decode(selected);
8511
8511
  }
8512
8512
  else {
8513
- const importee = await import(/* webpackChunkName: "generate-account" */ './index-dTQVgkpf-DOL2rIjp.js');
8513
+ const importee = await import(/* webpackChunkName: "generate-account" */ './index-dTQVgkpf-C6SJ0ZYW.js');
8514
8514
  const { identity, accounts } = await importee.default(password, this.network);
8515
8515
  if (fresh) {
8516
8516
  await globalThis.accountStore.clear();
@@ -16102,7 +16102,7 @@ const blockchainCodecs = [
16102
16102
  },
16103
16103
  {
16104
16104
  name: 'raw-transaction-message',
16105
- codec: '0x772746d',
16105
+ codec: '0x72746d',
16106
16106
  hashAlg: 'keccak-256'
16107
16107
  },
16108
16108
  {
@@ -16147,6 +16147,21 @@ const blockchainCodecs = [
16147
16147
  }
16148
16148
  ];
16149
16149
  const consensusCodecs = [
16150
+ {
16151
+ name: 'beacon-activation-message',
16152
+ codec: '0x62616d',
16153
+ hashAlg: 'keccak-256'
16154
+ },
16155
+ {
16156
+ name: 'beacon-commitment-message',
16157
+ codec: '0x62636d',
16158
+ hashAlg: 'keccak-256'
16159
+ },
16160
+ {
16161
+ name: 'beacon-share-message',
16162
+ codec: '0x62736d',
16163
+ hashAlg: 'keccak-256'
16164
+ },
16150
16165
  {
16151
16166
  name: 'proposal-message',
16152
16167
  codec: '0x70726d',
@@ -16225,7 +16240,7 @@ const peernetCodecs = [
16225
16240
  },
16226
16241
  {
16227
16242
  name: 'peernet-ps',
16228
- codec: '707073',
16243
+ codec: '0x707073',
16229
16244
  hashAlg: 'keccak-256'
16230
16245
  },
16231
16246
  {
@@ -17927,7 +17942,7 @@ class Peernet {
17927
17942
  await getAddress();
17928
17943
  this.storePrefix = options.storePrefix;
17929
17944
  this.root = options.root;
17930
- const { RequestMessage, ResponseMessage, PeerMessage, PeerMessageResponse, PeernetMessage, DHTMessage, DHTMessageResponse, DataMessage, DataMessageResponse, PsMessage, ChatMessage, PeernetFile } = await import(/* webpackChunkName: "messages" */ './messages-DCePyqKC-BSvJxDWZ.js');
17945
+ const { RequestMessage, ResponseMessage, PeerMessage, PeerMessageResponse, PeernetMessage, DHTMessage, DHTMessageResponse, DataMessage, DataMessageResponse, PsMessage, ChatMessage, PeernetFile } = await import(/* webpackChunkName: "messages" */ './messages-DZYCxoZI-D6kIiLKF.js');
17931
17946
  /**
17932
17947
  * proto Object containing protos
17933
17948
  * @type {Object}
@@ -17973,7 +17988,7 @@ class Peernet {
17973
17988
  if (this.#starting || this.#started)
17974
17989
  return;
17975
17990
  this.#starting = true;
17976
- const importee = await import('./client-DKKzPSlb-C_1ug6ux.js');
17991
+ const importee = await import('./client-DbZBfpG6-CYDyUGUC.js');
17977
17992
  /**
17978
17993
  * @access public
17979
17994
  * @type {PeernetClient}
@@ -18776,6 +18791,7 @@ class Node {
18776
18791
  config = { ...DEFAULT_NODE_OPTIONS, ...config };
18777
18792
  this.#node = globalThis.Peernet ? await new globalThis.Peernet(config, password) : await new Peernet(config, password);
18778
18793
  guardPeerMessages(this.#node);
18794
+ guardProviderDiscovery(this.#node);
18779
18795
  await nodeConfig(config);
18780
18796
  globalThis.pubsub.subscribe("chain:ready", async () => {
18781
18797
  if (!this.#node.autoStart) {
@@ -18787,4 +18803,4 @@ class Node {
18787
18803
  }
18788
18804
  }
18789
18805
 
18790
- export { FormatInterface as F, LittlePubSub as L, MultiWallet as M, Node as N, base58$1 as b, createDebugger as c, deflate_1 as d, encrypt as e, getDefaultExportFromCjs as g, inflate_1 as i };
18806
+ export { FormatInterface as F, LittlePubSub as L, MultiWallet as M, Node as N, base58$1 as b, createDebugger as c, encrypt as e, getDefaultExportFromCjs as g };
@@ -1,5 +1,5 @@
1
- export { N as default } from './node-browser-Ha4Lvntj.js';
1
+ export { N as default } from './node-browser-BMfelxXV.js';
2
2
  import './browser-crypto-CH0ua22L.js';
3
- import './connection-monitor-D-krNiRH.js';
3
+ import './connection-monitor-DMGAhcC0.js';
4
4
  import './index-CvKt4UDE.js';
5
5
  import './sha2-BKcEJzgd.js';
package/exports/chain.js CHANGED
@@ -7,39 +7,16 @@ import { MAX_TRANSACTION_BYTES, MAX_BLOCK_TRANSACTION_BYTES, MAX_BLOCK_TRANSACTI
7
7
  import MultiWallet from '@leofcoin/multi-wallet';
8
8
  import { fromBase58 } from '@vandeurenglenn/typed-array-utils';
9
9
  import semver from 'semver';
10
+ import { P as Protocol, C as ConnectionMonitor } from './connection-monitor-h2L8vsS7.js';
10
11
  import { randombytes } from '@leofcoin/crypto';
11
12
  import EasyWorker from '@vandeurenglenn/easy-worker';
12
13
  import { ContractDeploymentError, ExecutionError, isResolveError, ResolveError, isExecutionError } from '@leofcoin/errors';
13
14
  import { P as PROTOCOL_VERSION, R as REACHED_ONE_ZERO_ZERO } from './constants-Dy9i9S_o.js';
14
- import ConnectionMonitor from './connection-monitor.js';
15
15
  import { q as quorumThreshold } from './quorum-S_qdgiAY.js';
16
16
  import { assertHashFormatState } from './hash-format-state.js';
17
17
  import { createChainIdentity, assertChainId, findChainPresence } from '@leofcoin/lib/chain-identity';
18
18
  import '@leofcoin/networks';
19
19
 
20
- const limit = 1800;
21
- const transactionLimit = 2500;
22
- const requestTimeout = 3e4;
23
- const syncTimeout = 3e4;
24
- class Protocol {
25
- constructor(config) {
26
- this.resolveTimeout = 1e4;
27
- if (config?.resolveTimeout) this.resolveTimeout = config.resolveTimeout;
28
- }
29
- get limit() {
30
- return limit;
31
- }
32
- get transactionLimit() {
33
- return transactionLimit;
34
- }
35
- get requestTimeout() {
36
- return requestTimeout;
37
- }
38
- get syncTimeout() {
39
- return syncTimeout;
40
- }
41
- }
42
-
43
20
  const waitForCanonicalTransaction = (pubsub, transactionPool, hash) => new Promise((resolve, reject) => {
44
21
  const completionTopic = `transaction.completed.${hash}`;
45
22
  const cleanup = () => {
@@ -266,7 +243,7 @@ class Transaction extends Protocol {
266
243
  await globalThis.transactionPoolStore.put(hash, message.encoded);
267
244
  this.addPendingNonce(message.decoded.from, message.decoded.nonce);
268
245
  try {
269
- peernet.publish("add-transaction", message.encoded);
246
+ await peernet.publish("add-transaction", message.encoded);
270
247
  } catch (publishError) {
271
248
  console.warn("peernet publish failed: add-transaction", publishError?.message ?? publishError);
272
249
  }
@@ -1267,6 +1244,15 @@ const validateChainLink = (localTip, incoming) => {
1267
1244
  return "append";
1268
1245
  };
1269
1246
 
1247
+ const resolveTransactionReference = async (expectedHash, transactionData) => {
1248
+ const transaction = transactionData instanceof TransactionMessage ? transactionData : new TransactionMessage(transactionData);
1249
+ const actualHash = await transaction.hash();
1250
+ if (actualHash !== expectedHash) {
1251
+ throw new Error(`Transaction hash mismatch: expected ${expectedHash}, got ${actualHash}`);
1252
+ }
1253
+ return transaction;
1254
+ };
1255
+
1270
1256
  const debug$1 = createDebugger("leofcoin/state");
1271
1257
  class State extends Contract {
1272
1258
  constructor(config) {
@@ -1283,7 +1269,7 @@ class State extends Contract {
1283
1269
  this.#totalSize = 0;
1284
1270
  this.#canonicalStateQueue = new CanonicalStateQueue();
1285
1271
  this.#loaded = false;
1286
- this.#resolvingHashes = /* @__PURE__ */ new Set();
1272
+ this.#resolvingHashes = /* @__PURE__ */ new Map();
1287
1273
  this._wantList = [];
1288
1274
  this.#chainStateHandler = () => {
1289
1275
  return new globalThis.peernet.protos["peernet-response"]({
@@ -1539,7 +1525,8 @@ class State extends Contract {
1539
1525
  if (!exists) {
1540
1526
  const data = await peernet.get(hash, "transaction");
1541
1527
  if (!data) throw new Error(`missing transaction data for ${hash}`);
1542
- await transactionStore.put(hash, data);
1528
+ const transaction = await resolveTransactionReference(hash, data);
1529
+ await transactionStore.put(hash, transaction.encoded);
1543
1530
  }
1544
1531
  const inPool = await transactionPoolStore.has(hash);
1545
1532
  if (inPool) await transactionPoolStore.delete(hash);
@@ -1547,84 +1534,61 @@ class State extends Contract {
1547
1534
  );
1548
1535
  }
1549
1536
  async #resolveBlock(hash) {
1550
- let index = this.#blockHashMap.get(hash);
1551
- let localHash = "0x0";
1552
- try {
1553
- const rawLocalHash = await globalThis.stateStore.get("lastBlock");
1554
- if (rawLocalHash) {
1555
- const decoded = JSON.parse(new TextDecoder().decode(rawLocalHash));
1556
- localHash = typeof decoded === "string" ? decoded : decoded?.hash ?? "0x0";
1537
+ const localHash = (await this.canonicalTip()).hash;
1538
+ const visited = /* @__PURE__ */ new Set();
1539
+ while (hash !== "0x0" && hash !== localHash) {
1540
+ if (visited.has(hash)) throw new ResolveError(`cyclic block ancestry at ${hash}`);
1541
+ visited.add(hash);
1542
+ const cachedIndex = this.#blockHashMap.get(hash);
1543
+ const cached = this.#blocks[cachedIndex];
1544
+ if (cached) {
1545
+ hash = cached.previousHash;
1546
+ continue;
1557
1547
  }
1558
- } catch (error) {
1559
- debug$1("no local state found");
1560
- }
1561
- if (this.#blocks[index]) {
1562
- const previousHash = this.#blocks[index].previousHash;
1563
- if (previousHash === localHash) return;
1564
- if (previousHash !== "0x0") {
1565
- return this.resolveBlock(previousHash);
1566
- } else {
1567
- return;
1548
+ try {
1549
+ const block = await this.getAndPutBlock(hash);
1550
+ await this.#resolveTransactions(block.decoded.transactions);
1551
+ const index = block.decoded.index;
1552
+ const size = block.encoded.byteLength;
1553
+ this.#totalSize += size;
1554
+ this.#blocks[index] = { hash, ...block.decoded };
1555
+ this.#blockHashMap.set(hash, index);
1556
+ debug$1(`resolved block: ${hash} @${index} ${formatBytes(size)}`);
1557
+ globalThis.pubsub.publish("block-resolved", { hash, index });
1558
+ this.#lastResolved = this.#blocks[index];
1559
+ this.#lastResolvedTime = Date.now();
1560
+ hash = block.decoded.previousHash;
1561
+ } catch (error) {
1562
+ throw new ResolveError(`block: ${hash}`, { cause: error });
1568
1563
  }
1569
1564
  }
1570
- try {
1571
- const block = await this.getAndPutBlock(hash);
1572
- const promises = [];
1573
- if (block.decoded.previousHash !== "0x0" && block.decoded.previousHash !== localHash) {
1574
- promises.push(this.resolveBlock(block.decoded.previousHash));
1575
- }
1576
- promises.push(this.#resolveTransactions(block.decoded.transactions));
1577
- await Promise.all(promises);
1578
- index = block.decoded.index;
1579
- const size = block.encoded.length > 0 ? block.encoded.length : block.encoded.byteLength;
1580
- this.#totalSize += size;
1581
- this.#blocks[index] = { hash, ...block.decoded };
1582
- this.#blockHashMap.set(hash, index);
1583
- debug$1(`resolved block: ${hash} @${index} ${formatBytes(size)}`);
1584
- globalThis.pubsub.publish("block-resolved", { hash, index });
1585
- this.#lastResolved = this.#blocks[index];
1586
- this.#lastResolvedTime = Date.now();
1587
- } catch (error) {
1588
- throw new ResolveError(`block: ${hash}@${index}`, { cause: error });
1589
- }
1590
- return;
1591
1565
  }
1592
1566
  async resolveBlock(hash) {
1593
1567
  if (!hash) throw new Error(`expected hash, got: ${hash}`);
1594
1568
  if (hash === "0x0") return;
1595
- if (this.#resolvingHashes.has(hash)) return;
1596
- this.#resolvingHashes.add(hash);
1597
- const isEntering = this.#resolvingHashes.size === 1;
1569
+ const existing = this.#resolvingHashes.get(hash);
1570
+ if (existing) return existing;
1598
1571
  this.#resolving = true;
1599
- try {
1600
- if (isEntering) {
1601
- if (this.jobber.busy && this.jobber.destroy) await this.jobber.destroy();
1602
- await this.jobber.add(() => this.#resolveBlock(hash));
1603
- } else {
1604
- await this.#resolveBlock(hash);
1605
- }
1606
- try {
1607
- const lastBlockHash = await globalThis.stateStore.get("lastBlock");
1608
- if (lastBlockHash === hash) {
1572
+ const resolution = Promise.resolve().then(async () => {
1573
+ for (let attempt = 0; attempt < 3; attempt++) {
1574
+ try {
1575
+ await this.#resolveBlock(hash);
1609
1576
  this.#resolveErrored = false;
1610
1577
  return;
1578
+ } catch (error) {
1579
+ console.error(`block resolution attempt ${attempt + 1} failed for ${hash}`, error);
1580
+ if (attempt === 2) {
1581
+ if (!this.wantList.includes(hash)) this.wantList.push(hash);
1582
+ throw new ResolveError(`block: ${hash}`, { cause: error });
1583
+ }
1611
1584
  }
1612
- } catch (error) {
1613
1585
  }
1614
- } catch (error) {
1615
- console.log({ error });
1616
- this.#resolveErrorCount += 1;
1617
- if (this.#resolveErrorCount < 3) {
1618
- this.#resolvingHashes.delete(hash);
1619
- return this.resolveBlock(hash);
1620
- }
1621
- this.#resolveErrorCount = 0;
1622
- this.wantList.push(hash);
1623
- throw new ResolveError(`block: ${hash}`, { cause: error });
1624
- } finally {
1586
+ }).finally(() => {
1625
1587
  this.#resolvingHashes.delete(hash);
1626
1588
  if (this.#resolvingHashes.size === 0) this.#resolving = false;
1627
- }
1589
+ });
1590
+ this.#resolvingHashes.set(hash, resolution);
1591
+ return resolution;
1628
1592
  }
1629
1593
  async resolveBlocks() {
1630
1594
  if (this.#chainSyncing || this.#resolving) {
@@ -1761,24 +1725,6 @@ class State extends Contract {
1761
1725
  return;
1762
1726
  }
1763
1727
  if (localStateHash !== remoteBlockHash) {
1764
- if (this.wantList.length > 0) {
1765
- debug$1(`Fetching ${this.wantList.length} blocks before resolving`);
1766
- const getBatch = async (batch) => {
1767
- const blocks2 = await Promise.all(
1768
- batch.map(
1769
- (hash) => this.getAndPutBlock(hash).catch((e) => {
1770
- console.warn(`failed to fetch block ${hash}`, e);
1771
- })
1772
- )
1773
- );
1774
- const transactions = blocks2.filter((block) => Boolean(block)).flatMap((block) => block.decoded.transactions);
1775
- return this.#resolveTransactions(transactions);
1776
- };
1777
- for (let i = 0; i < this.wantList.length; i += 50) {
1778
- const batch = this.wantList.slice(i, i + 50);
1779
- await getBatch(batch);
1780
- }
1781
- }
1782
1728
  debug$1(`Resolving remote block: ${remoteBlockHash} @${remoteIndex} (differs from local state)`);
1783
1729
  await this.resolveBlock(remoteBlockHash);
1784
1730
  const blocksSynced = remoteIndex - localIndex;
@@ -2134,15 +2080,6 @@ const validateBlockEconomics = (block, calculatedFees, validatorFees = /* @__PUR
2134
2080
  }
2135
2081
  };
2136
2082
 
2137
- const resolveTransactionReference = async (expectedHash, transactionData) => {
2138
- const transaction = transactionData instanceof TransactionMessage ? transactionData : new TransactionMessage(transactionData);
2139
- const actualHash = await transaction.hash();
2140
- if (actualHash !== expectedHash) {
2141
- throw new Error(`Transaction hash mismatch: expected ${expectedHash}, got ${actualHash}`);
2142
- }
2143
- return transaction;
2144
- };
2145
-
2146
2083
  const consensusSignableData = (validatorsAddress, type, message) => ({
2147
2084
  from: String(message.from),
2148
2085
  to: validatorsAddress,
@@ -2960,39 +2897,29 @@ class Chain extends VersionControl {
2960
2897
  }
2961
2898
  async getPeerTransactionPool(peer) {
2962
2899
  let transactionsInPool = await this.#makeRequest(peer, "transactionPool");
2963
- console.log("raw response for request transactionPool:", transactionsInPool);
2964
2900
  if (transactionsInPool instanceof Uint8Array) {
2965
- try {
2966
- transactionsInPool = JSON.parse(new TextDecoder().decode(transactionsInPool));
2967
- } catch {
2968
- debug("transactionPool response must be decoded array payload");
2969
- return [];
2970
- }
2901
+ transactionsInPool = new TextDecoder().decode(transactionsInPool);
2971
2902
  }
2972
2903
  if (typeof transactionsInPool === "string") {
2973
- try {
2974
- transactionsInPool = JSON.parse(transactionsInPool);
2975
- } catch {
2976
- return [];
2977
- }
2904
+ transactionsInPool = JSON.parse(transactionsInPool);
2978
2905
  }
2979
- if (!Array.isArray(transactionsInPool)) return [];
2980
- const localTransactions = new Set(await globalThis.transactionPoolStore.keys());
2981
- const transactionsToGet = [];
2982
- for (const key of transactionsInPool) {
2983
- if (!localTransactions.has(key)) {
2984
- let txData;
2985
- try {
2986
- txData = await globalThis.peernet.get(key, "transaction");
2987
- } catch (error) {
2988
- debug(`Failed to get transaction ${key}:`, error?.message ?? error);
2989
- }
2990
- if (txData !== void 0) {
2991
- transactionsToGet.push(transactionPoolStore.put(key, txData));
2992
- }
2906
+ if (!Array.isArray(transactionsInPool) || transactionsInPool.some((hash) => typeof hash !== "string")) {
2907
+ throw new Error("transactionPool response must be an array of transaction hashes");
2908
+ }
2909
+ const recovered = [];
2910
+ for (const hash of new Set(transactionsInPool)) {
2911
+ if (await globalThis.transactionPoolStore.has(hash)) continue;
2912
+ if (await globalThis.transactionStore.has(hash)) continue;
2913
+ try {
2914
+ const txData = await globalThis.peernet.get(hash, "transactionPool");
2915
+ if (txData === void 0) throw new Error("pending transaction body unavailable");
2916
+ const transaction = await resolveTransactionReference(hash, txData);
2917
+ recovered.push(await this.#sendTransaction(transaction));
2918
+ } catch (error) {
2919
+ debug(`Failed to recover transaction ${hash}:`, error?.message ?? error);
2993
2920
  }
2994
2921
  }
2995
- return Promise.all(transactionsToGet);
2922
+ return recovered;
2996
2923
  }
2997
2924
  async #peerConnected(peerId) {
2998
2925
  if (typeof peerId !== "string" || !peerId.trim() || peerId === "undefined") {
@@ -3060,6 +2987,14 @@ class Chain extends VersionControl {
3060
2987
  await this.#recordPeerFailure(peerId, `unreasonable lastBlock index: ${lastBlock.index}`);
3061
2988
  return;
3062
2989
  }
2990
+ setTimeout(async () => {
2991
+ try {
2992
+ await this.getPeerTransactionPool(peer);
2993
+ } catch (error) {
2994
+ const peerName = peer?.peerId || peer?.id || peer?.address || peerId || "unknown";
2995
+ debug(`transactionPool request failed: ${peerName}:`, error?.message ?? error);
2996
+ }
2997
+ }, 3e3);
3063
2998
  if (!lastBlock || !lastBlock.hash || lastBlock.hash === "0x0") {
3064
2999
  debug(`peer has no lastBlock: ${peerId}`);
3065
3000
  return;
@@ -3092,22 +3027,6 @@ class Chain extends VersionControl {
3092
3027
  }
3093
3028
  }
3094
3029
  }
3095
- if (this.wantList.length > 0) {
3096
- const promises = await Promise.allSettled(this.wantList.map((hash) => peernet.get(hash, "block")));
3097
- for (let i = promises.length - 1; i >= 0; i -= 1) {
3098
- if (promises[i].status === "fulfilled") this.wantList.splice(i, 1);
3099
- }
3100
- if (this.wantList.length === 0) await this.triggerSync();
3101
- }
3102
- setTimeout(async () => {
3103
- try {
3104
- const peerTransactionPool = higherThenCurrentLocal && await this.getPeerTransactionPool(peer) || [];
3105
- if (this.#participating && peerTransactionPool.length > 0) return this.#runEpoch();
3106
- } catch (error) {
3107
- const peerName = peer?.peerId || peer?.id || peer?.address || peerId || "unknown";
3108
- debug(`transactionPool request failed: ${peerName}:`, error?.message ?? error);
3109
- }
3110
- }, 3e3);
3111
3030
  try {
3112
3031
  let stateInfo = await this.#makeRequest(peer, "stateInfo");
3113
3032
  if (stateInfo instanceof Uint8Array) {
@@ -3484,18 +3403,32 @@ class Chain extends VersionControl {
3484
3403
  await this.validateTransactionSignature(transaction);
3485
3404
  let hash;
3486
3405
  try {
3406
+ hash = await transaction.hash();
3407
+ if (await globalThis.transactionStore.has(hash)) return hash;
3408
+ const alreadyPending = await globalThis.transactionPoolStore.has(hash);
3487
3409
  hash = await enqueueTransaction(transaction, {
3488
3410
  putToPool: (txHash, data) => globalThis.transactionPoolStore.put(txHash, data),
3489
3411
  hasInPool: (txHash) => globalThis.transactionPoolStore.has(txHash),
3490
3412
  hasInStore: (txHash) => globalThis.transactionStore.has(txHash),
3491
3413
  addPendingNonce: (address, nonce) => this.addPendingNonce(address, nonce)
3492
3414
  });
3493
- if (this.#participating && !this.#runningEpoch) this.#runEpoch();
3415
+ if (!alreadyPending) {
3416
+ try {
3417
+ await globalThis.peernet.publish("add-transaction", transaction.encoded);
3418
+ } catch (publishError) {
3419
+ debug("peernet publish failed: add-transaction relay", publishError?.message ?? publishError);
3420
+ }
3421
+ }
3422
+ if (this.#participating && !this.#runningEpoch) {
3423
+ void this.#runEpoch().catch((error) => {
3424
+ debug("transaction epoch scheduling failed:", error?.message ?? error);
3425
+ });
3426
+ }
3494
3427
  return hash;
3495
3428
  } catch (e) {
3496
3429
  if (hash) {
3497
3430
  try {
3498
- globalThis.peernet.publish("invalid-transaction", hash);
3431
+ await globalThis.peernet.publish("invalid-transaction", hash);
3499
3432
  } catch (publishError) {
3500
3433
  debug("peernet publish failed: invalid-transaction", publishError?.message ?? publishError);
3501
3434
  }
@@ -3513,7 +3446,7 @@ class Chain extends VersionControl {
3513
3446
  const event = await super.sendTransaction(transactionMessage);
3514
3447
  await this.#sendTransaction(transactionMessage.encoded);
3515
3448
  try {
3516
- globalThis.peernet.publish("send-transaction", transactionMessage.encoded);
3449
+ await globalThis.peernet.publish("send-transaction", transactionMessage.encoded);
3517
3450
  } catch (publishError) {
3518
3451
  debug("peernet publish failed: send-transaction", publishError?.message ?? publishError);
3519
3452
  }