@leofcoin/chain 1.10.19 → 1.10.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,4 +6,21 @@
6
6
 
7
7
  - reachedOneZeroZero
8
8
 
9
+ ## Pending transaction propagation
10
+
11
+ Transactions enter the local pending pool before canonical confirmation. Peers
12
+ relay newly accepted transaction bytes using the existing `add-transaction`
13
+ topic; duplicate and already-confirmed entries are not relayed again.
14
+
15
+ On compatible peer reconnection, pending pools are reconciled even at equal
16
+ block heights. Missing bodies are fetched from `transactionPool`, checked against
17
+ their advertised hash, and admitted through the normal signature-validation and
18
+ pending-nonce path. Unavailable or invalid entries are logged without preventing
19
+ recovery of the remaining entries.
20
+
21
+ Pool admission and pubsub publication are not validator acknowledgements.
22
+ `sendTransaction().wait` resolves only after canonical block inclusion. Deploy
23
+ the updated chain runtime to intermediate nodes and validators as well as the
24
+ browser to enable relay and reconnect recovery throughout the network.
25
+
9
26
  <a rel="license" href="http://creativecommons.org/licenses/by-nc/4.0/"><img alt="Creative Commons Licence" style="border-width:0" src="https://i.creativecommons.org/l/by-nc/4.0/88x31.png" /></a><br />This work is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by-nc/4.0/">Creative Commons Attribution-NonCommercial 4.0 International License</a>.
@@ -1,4 +1,4 @@
1
- import { g as getDefaultExportFromCjs } from './node-browser-BNe-en1l.js';
1
+ import { g as getDefaultExportFromCjs } from './node-browser-Ha4Lvntj.js';
2
2
  import './browser-crypto-CH0ua22L.js';
3
3
  import './index-CvKt4UDE.js';
4
4
  import './sha2-BKcEJzgd.js';
@@ -3164,7 +3164,7 @@ class Transaction extends Protocol {
3164
3164
  await globalThis.transactionPoolStore.put(hash, message.encoded);
3165
3165
  this.addPendingNonce(message.decoded.from, message.decoded.nonce);
3166
3166
  try {
3167
- peernet.publish("add-transaction", message.encoded);
3167
+ await peernet.publish("add-transaction", message.encoded);
3168
3168
  } catch (publishError) {
3169
3169
  console.warn("peernet publish failed: add-transaction", publishError?.message ?? publishError);
3170
3170
  }
@@ -6077,39 +6077,29 @@ class Chain extends VersionControl {
6077
6077
  }
6078
6078
  async getPeerTransactionPool(peer) {
6079
6079
  let transactionsInPool = await this.#makeRequest(peer, "transactionPool");
6080
- console.log("raw response for request transactionPool:", transactionsInPool);
6081
6080
  if (transactionsInPool instanceof Uint8Array) {
6082
- try {
6083
- transactionsInPool = JSON.parse(new TextDecoder().decode(transactionsInPool));
6084
- } catch {
6085
- debug("transactionPool response must be decoded array payload");
6086
- return [];
6087
- }
6081
+ transactionsInPool = new TextDecoder().decode(transactionsInPool);
6088
6082
  }
6089
6083
  if (typeof transactionsInPool === "string") {
6090
- try {
6091
- transactionsInPool = JSON.parse(transactionsInPool);
6092
- } catch {
6093
- return [];
6094
- }
6084
+ transactionsInPool = JSON.parse(transactionsInPool);
6095
6085
  }
6096
- if (!Array.isArray(transactionsInPool)) return [];
6097
- const localTransactions = new Set(await globalThis.transactionPoolStore.keys());
6098
- const transactionsToGet = [];
6099
- for (const key of transactionsInPool) {
6100
- if (!localTransactions.has(key)) {
6101
- let txData;
6102
- try {
6103
- txData = await globalThis.peernet.get(key, "transaction");
6104
- } catch (error) {
6105
- debug(`Failed to get transaction ${key}:`, error?.message ?? error);
6106
- }
6107
- if (txData !== void 0) {
6108
- transactionsToGet.push(transactionPoolStore.put(key, txData));
6109
- }
6086
+ if (!Array.isArray(transactionsInPool) || transactionsInPool.some((hash) => typeof hash !== "string")) {
6087
+ throw new Error("transactionPool response must be an array of transaction hashes");
6088
+ }
6089
+ const recovered = [];
6090
+ for (const hash of new Set(transactionsInPool)) {
6091
+ if (await globalThis.transactionPoolStore.has(hash)) continue;
6092
+ if (await globalThis.transactionStore.has(hash)) continue;
6093
+ try {
6094
+ const txData = await globalThis.peernet.get(hash, "transactionPool");
6095
+ if (txData === void 0) throw new Error("pending transaction body unavailable");
6096
+ const transaction = await resolveTransactionReference(hash, txData);
6097
+ recovered.push(await this.#sendTransaction(transaction));
6098
+ } catch (error) {
6099
+ debug(`Failed to recover transaction ${hash}:`, error?.message ?? error);
6110
6100
  }
6111
6101
  }
6112
- return Promise.all(transactionsToGet);
6102
+ return recovered;
6113
6103
  }
6114
6104
  async #peerConnected(peerId) {
6115
6105
  if (typeof peerId !== "string" || !peerId.trim() || peerId === "undefined") {
@@ -6177,6 +6167,14 @@ class Chain extends VersionControl {
6177
6167
  await this.#recordPeerFailure(peerId, `unreasonable lastBlock index: ${lastBlock.index}`);
6178
6168
  return;
6179
6169
  }
6170
+ setTimeout(async () => {
6171
+ try {
6172
+ await this.getPeerTransactionPool(peer);
6173
+ } catch (error) {
6174
+ const peerName = peer?.peerId || peer?.id || peer?.address || peerId || "unknown";
6175
+ debug(`transactionPool request failed: ${peerName}:`, error?.message ?? error);
6176
+ }
6177
+ }, 3e3);
6180
6178
  if (!lastBlock || !lastBlock.hash || lastBlock.hash === "0x0") {
6181
6179
  debug(`peer has no lastBlock: ${peerId}`);
6182
6180
  return;
@@ -6216,15 +6214,6 @@ class Chain extends VersionControl {
6216
6214
  }
6217
6215
  if (this.wantList.length === 0) await this.triggerSync();
6218
6216
  }
6219
- setTimeout(async () => {
6220
- try {
6221
- const peerTransactionPool = higherThenCurrentLocal && await this.getPeerTransactionPool(peer) || [];
6222
- if (this.#participating && peerTransactionPool.length > 0) return this.#runEpoch();
6223
- } catch (error) {
6224
- const peerName = peer?.peerId || peer?.id || peer?.address || peerId || "unknown";
6225
- debug(`transactionPool request failed: ${peerName}:`, error?.message ?? error);
6226
- }
6227
- }, 3e3);
6228
6217
  try {
6229
6218
  let stateInfo = await this.#makeRequest(peer, "stateInfo");
6230
6219
  if (stateInfo instanceof Uint8Array) {
@@ -6601,18 +6590,32 @@ class Chain extends VersionControl {
6601
6590
  await this.validateTransactionSignature(transaction);
6602
6591
  let hash;
6603
6592
  try {
6593
+ hash = await transaction.hash();
6594
+ if (await globalThis.transactionStore.has(hash)) return hash;
6595
+ const alreadyPending = await globalThis.transactionPoolStore.has(hash);
6604
6596
  hash = await enqueueTransaction(transaction, {
6605
6597
  putToPool: (txHash, data) => globalThis.transactionPoolStore.put(txHash, data),
6606
6598
  hasInPool: (txHash) => globalThis.transactionPoolStore.has(txHash),
6607
6599
  hasInStore: (txHash) => globalThis.transactionStore.has(txHash),
6608
6600
  addPendingNonce: (address, nonce) => this.addPendingNonce(address, nonce)
6609
6601
  });
6610
- if (this.#participating && !this.#runningEpoch) this.#runEpoch();
6602
+ if (!alreadyPending) {
6603
+ try {
6604
+ await globalThis.peernet.publish("add-transaction", transaction.encoded);
6605
+ } catch (publishError) {
6606
+ debug("peernet publish failed: add-transaction relay", publishError?.message ?? publishError);
6607
+ }
6608
+ }
6609
+ if (this.#participating && !this.#runningEpoch) {
6610
+ void this.#runEpoch().catch((error) => {
6611
+ debug("transaction epoch scheduling failed:", error?.message ?? error);
6612
+ });
6613
+ }
6611
6614
  return hash;
6612
6615
  } catch (e) {
6613
6616
  if (hash) {
6614
6617
  try {
6615
- globalThis.peernet.publish("invalid-transaction", hash);
6618
+ await globalThis.peernet.publish("invalid-transaction", hash);
6616
6619
  } catch (publishError) {
6617
6620
  debug("peernet publish failed: invalid-transaction", publishError?.message ?? publishError);
6618
6621
  }
@@ -6630,7 +6633,7 @@ class Chain extends VersionControl {
6630
6633
  const event = await super.sendTransaction(transactionMessage);
6631
6634
  await this.#sendTransaction(transactionMessage.encoded);
6632
6635
  try {
6633
- globalThis.peernet.publish("send-transaction", transactionMessage.encoded);
6636
+ await globalThis.peernet.publish("send-transaction", transactionMessage.encoded);
6634
6637
  } catch (publishError) {
6635
6638
  debug("peernet publish failed: send-transaction", publishError?.message ?? publishError);
6636
6639
  }
@@ -1,4 +1,4 @@
1
- import { i as inflate_1, c as createDebugger, d as deflate_1, L as LittlePubSub } from './node-browser-BNe-en1l.js';
1
+ import { i as inflate_1, c as createDebugger, d as deflate_1, L as LittlePubSub } from './node-browser-Ha4Lvntj.js';
2
2
  import './browser-crypto-CH0ua22L.js';
3
3
  import './connection-monitor-D-krNiRH.js';
4
4
  import './index-CvKt4UDE.js';
@@ -233,7 +233,7 @@ class SocketRequestClient {
233
233
  const init = async () => {
234
234
  // @ts-ignore
235
235
  if (!globalThis.WebSocket && !this.#experimentalWebsocket)
236
- globalThis.WebSocket = (await import('./browser-CCfR9KOM-CpStHgTj.js').then(function (n) { return n.b; })).default.w3cwebsocket;
236
+ globalThis.WebSocket = (await import('./browser-CCfR9KOM-BGMMnVsR.js').then(function (n) { return n.b; })).default.w3cwebsocket;
237
237
  const client = new WebSocket(this.#url, this.#protocol);
238
238
  if (this.#experimentalWebsocket) {
239
239
  client.addEventListener('error', this.onerror);
@@ -1,4 +1,4 @@
1
- import { M as MultiWallet, e as encrypt, b as base58$1 } from './node-browser-BNe-en1l.js';
1
+ import { M as MultiWallet, e as encrypt, b as base58$1 } from './node-browser-Ha4Lvntj.js';
2
2
  import './browser-crypto-CH0ua22L.js';
3
3
  import './connection-monitor-D-krNiRH.js';
4
4
  import './index-CvKt4UDE.js';
@@ -1,4 +1,4 @@
1
- import { F as FormatInterface } from './node-browser-BNe-en1l.js';
1
+ import { F as FormatInterface } from './node-browser-Ha4Lvntj.js';
2
2
  import './browser-crypto-CH0ua22L.js';
3
3
  import './connection-monitor-D-krNiRH.js';
4
4
  import './index-CvKt4UDE.js';
@@ -524,7 +524,7 @@ const concatAndDoubleHash = async (input) => {
524
524
  return new Uint8Array(await createDoubleHash(typedArrayConcat(input), 'SHA-256'));
525
525
  };
526
526
 
527
- const encode$2 = async (source, prefix = new TextEncoder().encode('00'), hex) => {
527
+ const encode$2$1 = async (source, prefix = new TextEncoder().encode('00'), hex) => {
528
528
  if (!(source instanceof Uint8Array) || !(prefix instanceof Uint8Array)) {
529
529
  throw new TypeError('Expected Uint8Array');
530
530
  }
@@ -568,10 +568,10 @@ const isBase58check = (string, hex) => {
568
568
  return false;
569
569
  }
570
570
  };
571
- const encodeHex$1 = (uint8Array, prefix = new TextEncoder().encode('00')) => encode$2(uint8Array, prefix, true);
571
+ const encodeHex$1 = (uint8Array, prefix = new TextEncoder().encode('00')) => encode$2$1(uint8Array, prefix, true);
572
572
  const decodeHex$1 = (string) => decode$3(string, true);
573
573
  const isBase58checkHex = (string) => isBase58check(string, true);
574
- var base58check = { encode: encode$2, decode: decode$3, encodeHex: encodeHex$1, decodeHex: decodeHex$1, isBase58check, isBase58checkHex };
574
+ var base58check = { encode: encode$2$1, decode: decode$3, encodeHex: encodeHex$1, decodeHex: decodeHex$1, isBase58check, isBase58checkHex };
575
575
 
576
576
  /** @leofcoin/binary: dependency-free unsigned framing, with no global byte counters. */
577
577
  const MAX_BIGINT = (1n << 256n) - 1n;
@@ -798,7 +798,7 @@ var index$3 = (input, prefix) => encodeFrames(input.map(asBytes));
798
798
 
799
799
  var index$2 = (typedArray, prefix) => decodeFrames(typedArray, 0, { canonical: false });
800
800
 
801
- const decode$2 = (multiWif, expectedVersion, expectedCodec) => {
801
+ const decode$2$1 = (multiWif, expectedVersion, expectedCodec) => {
802
802
  const decoded = base58$1.decode(multiWif);
803
803
  let [version, codec, privateKey] = index$2(decoded);
804
804
  version = Number(new TextDecoder().decode(version));
@@ -817,10 +817,10 @@ var index$1$1 = {
817
817
  privateKey
818
818
  ]));
819
819
  },
820
- decode: decode$2,
820
+ decode: decode$2$1,
821
821
  isMultiWif: (multiWif) => {
822
822
  try {
823
- const { version, codec, privateKey } = decode$2(multiWif);
823
+ const { version, codec, privateKey } = decode$2$1(multiWif);
824
824
  if (version === undefined)
825
825
  return false;
826
826
  if (codec === undefined)
@@ -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-BbdshWB0.js');
8513
+ const importee = await import(/* webpackChunkName: "generate-account" */ './index-dTQVgkpf-DOL2rIjp.js');
8514
8514
  const { identity, accounts } = await importee.default(password, this.network);
8515
8515
  if (fresh) {
8516
8516
  await globalThis.accountStore.clear();
@@ -8573,12 +8573,12 @@ class Identity {
8573
8573
  }
8574
8574
  }
8575
8575
 
8576
- const encode$1 = (string) => {
8576
+ const encode$2 = (string) => {
8577
8577
  if (typeof string === 'string')
8578
8578
  return new TextEncoder().encode(string);
8579
8579
  throw Error(`expected typeof String instead got ${string}`);
8580
8580
  };
8581
- const decode$1 = (uint8Array) => {
8581
+ const decode$2 = (uint8Array) => {
8582
8582
  if (uint8Array instanceof Uint8Array)
8583
8583
  return new TextDecoder().decode(uint8Array);
8584
8584
  throw Error(`expected typeof uint8Array instead got ${uint8Array}`);
@@ -8589,7 +8589,7 @@ class KeyPath {
8589
8589
  uint8Array;
8590
8590
  constructor(input) {
8591
8591
  if (typeof input === 'string') {
8592
- this.uint8Array = encode$1(input);
8592
+ this.uint8Array = encode$2(input);
8593
8593
  }
8594
8594
  else if (input instanceof Uint8Array) {
8595
8595
  this.uint8Array = input;
@@ -8605,7 +8605,7 @@ class KeyPath {
8605
8605
  return true;
8606
8606
  }
8607
8607
  toString() {
8608
- return decode$1(this.uint8Array);
8608
+ return decode$2(this.uint8Array);
8609
8609
  }
8610
8610
  /**
8611
8611
  * Returns the `list` representation of this path.
@@ -8625,7 +8625,7 @@ class KeyValue {
8625
8625
  uint8Array;
8626
8626
  constructor(input) {
8627
8627
  if (typeof input === 'string') {
8628
- this.uint8Array = encode$1(input);
8628
+ this.uint8Array = encode$2(input);
8629
8629
  }
8630
8630
  else if (input instanceof Uint8Array) {
8631
8631
  this.uint8Array = input;
@@ -8641,7 +8641,7 @@ class KeyValue {
8641
8641
  return true;
8642
8642
  }
8643
8643
  toString() {
8644
- return decode$1(this.uint8Array);
8644
+ return decode$2(this.uint8Array);
8645
8645
  }
8646
8646
  }
8647
8647
 
@@ -15734,23 +15734,23 @@ const getTokens = (proto) => {
15734
15734
  return tokens;
15735
15735
  };
15736
15736
 
15737
- const encoder = new TextEncoder();
15737
+ const encoder$1 = new TextEncoder();
15738
15738
  const toType = (data) => {
15739
15739
  if (data instanceof Uint8Array)
15740
15740
  return data;
15741
15741
  if (data instanceof ArrayBuffer)
15742
15742
  return new Uint8Array(data);
15743
15743
  if (typeof data === 'bigint')
15744
- return encoder.encode(data.toString());
15744
+ return encoder$1.encode(data.toString());
15745
15745
  if (typeof data === 'string')
15746
- return encoder.encode(data);
15746
+ return encoder$1.encode(data);
15747
15747
  if (typeof data === 'object')
15748
- return encoder.encode(JSON.stringify(data, jsonStringifyBigInt$1));
15748
+ return encoder$1.encode(JSON.stringify(data, jsonStringifyBigInt$1));
15749
15749
  if (typeof data === 'number' || typeof data === 'boolean')
15750
- return encoder.encode(data.toString());
15750
+ return encoder$1.encode(data.toString());
15751
15751
  throw new Error(`unsuported type ${typeof data || data}`);
15752
15752
  };
15753
- const encode = (proto, input, compress) => {
15753
+ const encode$1 = (proto, input, compress) => {
15754
15754
  const tokens = getTokens(proto);
15755
15755
  const set = [];
15756
15756
  for (let i = 0; i < tokens.length; i++) {
@@ -15807,8 +15807,8 @@ var index$1 = {
15807
15807
  toString: toString$1};
15808
15808
 
15809
15809
  const { toString } = index$1;
15810
- const decoder = new TextDecoder();
15811
- const decode = (proto, uint8Array, compressed) => {
15810
+ const decoder$1 = new TextDecoder();
15811
+ const decode$1 = (proto, uint8Array, compressed) => {
15812
15812
  if (compressed)
15813
15813
  uint8Array = pako.inflate(uint8Array);
15814
15814
  let deconcated = index$2(uint8Array);
@@ -15823,13 +15823,13 @@ const decode = (proto, uint8Array, compressed) => {
15823
15823
  else if (token.type === 'string')
15824
15824
  output[token.key] = toString(deconcated[i]);
15825
15825
  else if (token.type === 'boolean')
15826
- output[token.key] = decoder.decode(deconcated[i]) === 'true';
15826
+ output[token.key] = decoder$1.decode(deconcated[i]) === 'true';
15827
15827
  else if (token.type === 'number')
15828
- output[token.key] = Number(decoder.decode(deconcated[i]));
15828
+ output[token.key] = Number(decoder$1.decode(deconcated[i]));
15829
15829
  else if (token.type === 'bigint')
15830
- output[token.key] = BigInt(decoder.decode(deconcated[i]));
15830
+ output[token.key] = BigInt(decoder$1.decode(deconcated[i]));
15831
15831
  else if (token.type === 'object' || token.type === 'array')
15832
- output[token.key] = JSON.parse(decoder.decode(deconcated[i]), jsonParseBigInt$1);
15832
+ output[token.key] = JSON.parse(decoder$1.decode(deconcated[i]), jsonParseBigInt$1);
15833
15833
  if (token.optional) {
15834
15834
  if (!output[token.key] || output[token.key].length === 0)
15835
15835
  delete output[token.key];
@@ -15840,7 +15840,7 @@ const decode = (proto, uint8Array, compressed) => {
15840
15840
  return output;
15841
15841
  };
15842
15842
 
15843
- var index = { encode, decode };
15843
+ var index = { encode: encode$1, decode: decode$1 };
15844
15844
 
15845
15845
  const jsonStringifyBigInt = (key, value) => {
15846
15846
  if (typeof value === 'bigint')
@@ -17306,6 +17306,439 @@ class FileTransfer {
17306
17306
  }
17307
17307
  }
17308
17308
 
17309
+ const VERSION = 1;
17310
+ const MAX_PACKET_BYTES = 64_000;
17311
+ const MAX_ENVELOPE_BYTES = 32_000;
17312
+ const MAX_ARCHIVES = 500;
17313
+ const MAX_OUTBOX = 500;
17314
+ const MAX_AGE = 30 * 24 * 60 * 60 * 1000;
17315
+ const MAX_CLOCK_SKEW = 10 * 60 * 1000;
17316
+ const MAX_HOPS = 8;
17317
+ const encoder = new TextEncoder();
17318
+ const decoder = new TextDecoder();
17319
+ function isUnsubscribe(value) {
17320
+ return typeof value === 'function';
17321
+ }
17322
+ function encode(value) {
17323
+ return encoder.encode(JSON.stringify(value));
17324
+ }
17325
+ function decode(payload) {
17326
+ if (!(payload instanceof Uint8Array))
17327
+ throw new TypeError('Archive packets must be encoded bytes.');
17328
+ if (payload.byteLength > MAX_PACKET_BYTES)
17329
+ throw new RangeError('Archive packet exceeds the size limit.');
17330
+ return JSON.parse(decoder.decode(payload));
17331
+ }
17332
+ function randomId() {
17333
+ if (typeof crypto.randomUUID === 'function')
17334
+ return crypto.randomUUID();
17335
+ return Array.from(crypto.getRandomValues(new Uint8Array(16)), (byte) => byte.toString(16).padStart(2, '0')).join('');
17336
+ }
17337
+ function keyPrefix(network) {
17338
+ return `peernet:archive:v1:${encodeURIComponent(network)}:`;
17339
+ }
17340
+ function recordKey(network, kind, id) {
17341
+ return `${keyPrefix(network)}${kind}:${encodeURIComponent(id)}`;
17342
+ }
17343
+ function stateKey(network) {
17344
+ return `${keyPrefix(network)}state`;
17345
+ }
17346
+ async function storeGet(store, key) {
17347
+ if (store.has && !(await store.has(key)))
17348
+ return undefined;
17349
+ try {
17350
+ return await store.get(key);
17351
+ }
17352
+ catch (error) {
17353
+ if (error?.code === 'LEVEL_NOT_FOUND' ||
17354
+ error?.name === 'NotFoundError')
17355
+ return undefined;
17356
+ throw error;
17357
+ }
17358
+ }
17359
+ function validateEnvelope(envelope, network) {
17360
+ const envelopeNetwork = envelope?.network ||
17361
+ envelope?.senderCertificate?.network ||
17362
+ envelope?.recipientCertificate?.network;
17363
+ if (!envelope ||
17364
+ typeof envelope !== 'object' ||
17365
+ envelopeNetwork !== network ||
17366
+ typeof envelope.id !== 'string' ||
17367
+ !envelope.id ||
17368
+ typeof envelope.recipientAccount !== 'string' ||
17369
+ typeof envelope.recipientDeviceId !== 'string' ||
17370
+ typeof envelope.ciphertext !== 'string' ||
17371
+ !envelope.ciphertext ||
17372
+ typeof envelope.signature !== 'string' ||
17373
+ !Number.isFinite(envelope.timestamp) ||
17374
+ envelope.timestamp < Date.now() - MAX_AGE ||
17375
+ envelope.timestamp > Date.now() + MAX_CLOCK_SKEW)
17376
+ throw new Error('Only a valid, network-scoped encrypted message envelope can be archived.');
17377
+ if ('text' in envelope || 'plaintext' in envelope || 'message' in envelope)
17378
+ throw new Error('Plaintext message fields cannot be archived.');
17379
+ if (encode(envelope).byteLength > MAX_ENVELOPE_BYTES)
17380
+ throw new RangeError('Encrypted message envelope exceeds the archive size limit.');
17381
+ }
17382
+ function validatePacket(value, network) {
17383
+ if (!value || typeof value !== 'object' || Array.isArray(value))
17384
+ return;
17385
+ const packet = value;
17386
+ if (packet.version !== VERSION ||
17387
+ packet.network !== network ||
17388
+ typeof packet.id !== 'string' ||
17389
+ !packet.id ||
17390
+ typeof packet.origin !== 'string' ||
17391
+ !packet.origin ||
17392
+ !Number.isFinite(packet.timestamp) ||
17393
+ packet.timestamp < Date.now() - MAX_AGE ||
17394
+ packet.timestamp > Date.now() + MAX_CLOCK_SKEW ||
17395
+ !Number.isInteger(packet.hopsRemaining) ||
17396
+ packet.hopsRemaining < 0 ||
17397
+ packet.hopsRemaining > MAX_HOPS ||
17398
+ !['direct', 'inbox-request', 'archive-delivery', 'delivery-receipt'].includes(packet.type) ||
17399
+ !packet.payload ||
17400
+ typeof packet.payload !== 'object' ||
17401
+ Array.isArray(packet.payload))
17402
+ return;
17403
+ return packet;
17404
+ }
17405
+ class PeernetArchiveMode {
17406
+ #peernet;
17407
+ #store;
17408
+ #network;
17409
+ #topic;
17410
+ #verifyEnvelope;
17411
+ #verifyInboxRequest;
17412
+ #verifyReceipt;
17413
+ #handlers = new Set();
17414
+ #seen = new Map();
17415
+ #archives = new Set();
17416
+ #outbox = new Set();
17417
+ #enabled = false;
17418
+ #started = false;
17419
+ #unsubscribe;
17420
+ #retryTimer;
17421
+ #account = '';
17422
+ #deviceId = '';
17423
+ constructor(options) {
17424
+ if (!options?.peernet?.network || typeof options.peernet.publish !== 'function' ||
17425
+ typeof options.peernet.subscribe !== 'function')
17426
+ throw new Error('A Peernet instance is required for archive mode.');
17427
+ const store = options.store || globalThis.messageStore;
17428
+ if (!store || typeof store.get !== 'function' || typeof store.put !== 'function' ||
17429
+ typeof store.delete !== 'function' || typeof store.keys !== 'function')
17430
+ throw new Error('A persistent Peernet message store is required for archive mode.');
17431
+ if (typeof options.verifyEnvelope !== 'function' ||
17432
+ typeof options.verifyInboxRequest !== 'function' ||
17433
+ typeof options.verifyReceipt !== 'function')
17434
+ throw new Error('Archive mode requires envelope, inbox-request and receipt verification functions.');
17435
+ this.#peernet = options.peernet;
17436
+ this.#store = store;
17437
+ this.#network = options.peernet.network;
17438
+ this.#topic = `peernet:archive:v1:${this.#network}`;
17439
+ this.#verifyEnvelope = options.verifyEnvelope;
17440
+ this.#verifyInboxRequest = options.verifyInboxRequest;
17441
+ this.#verifyReceipt = options.verifyReceipt;
17442
+ }
17443
+ get network() {
17444
+ return this.#network;
17445
+ }
17446
+ get enabled() {
17447
+ return this.#enabled;
17448
+ }
17449
+ get archiveCount() {
17450
+ return this.#archives.size;
17451
+ }
17452
+ get pendingCount() {
17453
+ return this.#outbox.size;
17454
+ }
17455
+ onMessage(handler) {
17456
+ if (typeof handler !== 'function')
17457
+ throw new TypeError('An archive message handler is required.');
17458
+ this.#handlers.add(handler);
17459
+ return () => this.#handlers.delete(handler);
17460
+ }
17461
+ async start({ account, deviceId }) {
17462
+ if (this.#started)
17463
+ return;
17464
+ if (!account || !deviceId)
17465
+ throw new Error('A local account and chat device ID are required.');
17466
+ this.#account = account;
17467
+ this.#deviceId = deviceId;
17468
+ await this.#load();
17469
+ const receiver = (payload) => {
17470
+ this.#receive(payload).catch((error) => console.error('Peernet archive packet failed:', error));
17471
+ };
17472
+ const unsubscribe = await this.#peernet.subscribe(this.#topic, receiver);
17473
+ this.#unsubscribe = isUnsubscribe(unsubscribe)
17474
+ ? unsubscribe
17475
+ : typeof globalThis.globalSub?.unsubscribe === 'function'
17476
+ ? () => globalThis.globalSub.unsubscribe(this.#topic, receiver)
17477
+ : undefined;
17478
+ this.#started = true;
17479
+ this.#retryTimer = setInterval(() => {
17480
+ this.#retryOutbox().catch((error) => console.error('Peernet archive retry failed:', error));
17481
+ }, 30_000);
17482
+ try {
17483
+ for (const id of this.#outbox) {
17484
+ const bytes = await storeGet(this.#store, recordKey(this.#network, 'outbox', id));
17485
+ if (bytes)
17486
+ await this.#publish('direct', { envelope: JSON.parse(decoder.decode(bytes)) });
17487
+ }
17488
+ }
17489
+ catch (error) {
17490
+ this.stop();
17491
+ throw error;
17492
+ }
17493
+ }
17494
+ stop() {
17495
+ clearInterval(this.#retryTimer);
17496
+ this.#retryTimer = undefined;
17497
+ this.#unsubscribe?.();
17498
+ this.#unsubscribe = undefined;
17499
+ this.#started = false;
17500
+ this.#account = '';
17501
+ this.#deviceId = '';
17502
+ }
17503
+ async setEnabled(enabled) {
17504
+ if (typeof enabled !== 'boolean')
17505
+ throw new TypeError('Archive mode must be enabled or disabled explicitly.');
17506
+ if (!this.#started)
17507
+ await this.#load();
17508
+ this.#enabled = enabled;
17509
+ await this.#store.put(stateKey(this.#network), encode({ version: VERSION, enabled }));
17510
+ if (!enabled) {
17511
+ for (const id of this.#archives)
17512
+ await this.#store.delete(recordKey(this.#network, 'archive', id));
17513
+ this.#archives.clear();
17514
+ }
17515
+ }
17516
+ async send(envelope) {
17517
+ validateEnvelope(envelope, this.#network);
17518
+ if (!(await this.#verifyEnvelope(envelope)))
17519
+ throw new Error('Encrypted message signature verification failed.');
17520
+ if (this.#outbox.has(envelope.id))
17521
+ throw new Error('This encrypted message is already pending.');
17522
+ await this.#store.put(recordKey(this.#network, 'outbox', envelope.id), encode(envelope));
17523
+ this.#outbox.add(envelope.id);
17524
+ if (this.#outbox.size > MAX_OUTBOX)
17525
+ await this.#dropOldest('outbox');
17526
+ await this.#publish('direct', { envelope });
17527
+ }
17528
+ async requestInbox(request) {
17529
+ if (request.network !== this.#network ||
17530
+ request.account !== this.#account ||
17531
+ request.deviceId !== this.#deviceId ||
17532
+ !(await this.#validInboxRequest(request)))
17533
+ throw new Error('A valid signed inbox request for this Peernet network and device is required.');
17534
+ await this.#publish('inbox-request', { request });
17535
+ }
17536
+ async acknowledge(receipt) {
17537
+ if (receipt.network !== this.#network ||
17538
+ receipt.recipientAccount !== this.#account ||
17539
+ receipt.recipientDeviceId !== this.#deviceId ||
17540
+ typeof receipt.signature !== 'string' ||
17541
+ !Number.isFinite(receipt.timestamp) ||
17542
+ Math.abs(Date.now() - receipt.timestamp) > MAX_CLOCK_SKEW)
17543
+ throw new Error('Delivery receipt does not belong to this Peernet network and device.');
17544
+ await this.#publish('delivery-receipt', { receipt });
17545
+ }
17546
+ async #load() {
17547
+ const state = await storeGet(this.#store, stateKey(this.#network));
17548
+ if (state) {
17549
+ const saved = JSON.parse(decoder.decode(state));
17550
+ if (saved?.version !== VERSION || typeof saved.enabled !== 'boolean')
17551
+ throw new Error('Saved Peernet archive settings are invalid.');
17552
+ this.#enabled = saved.enabled;
17553
+ }
17554
+ const prefix = keyPrefix(this.#network);
17555
+ const keys = await this.#store.keys();
17556
+ for (const keyInput of keys) {
17557
+ const key = keyInput.toString().replace(/^\/+/, '');
17558
+ if (!key.startsWith(prefix))
17559
+ continue;
17560
+ const archiveMarker = `${prefix}archive:`;
17561
+ const outboxMarker = `${prefix}outbox:`;
17562
+ if (key.startsWith(archiveMarker))
17563
+ this.#archives.add(decodeURIComponent(key.slice(archiveMarker.length)));
17564
+ if (key.startsWith(outboxMarker))
17565
+ this.#outbox.add(decodeURIComponent(key.slice(outboxMarker.length)));
17566
+ }
17567
+ await this.#prune();
17568
+ }
17569
+ async #publish(type, payload, hopsRemaining = MAX_HOPS) {
17570
+ if (!this.#started)
17571
+ throw new Error('Peernet archive mode is not started.');
17572
+ const packet = {
17573
+ version: VERSION,
17574
+ id: randomId(),
17575
+ network: this.#network,
17576
+ origin: this.#peernet.id,
17577
+ timestamp: Date.now(),
17578
+ hopsRemaining,
17579
+ type,
17580
+ payload
17581
+ };
17582
+ const bytes = encode(packet);
17583
+ if (bytes.byteLength > MAX_PACKET_BYTES)
17584
+ throw new RangeError('Peernet archive packet exceeds its size limit.');
17585
+ this.#seen.set(packet.id, packet.timestamp);
17586
+ await this.#peernet.publish(this.#topic, bytes);
17587
+ }
17588
+ async #receive(payload) {
17589
+ const packet = validatePacket(decode(payload), this.#network);
17590
+ if (!packet || packet.origin === this.#peernet.id || this.#wasSeen(packet.id))
17591
+ return;
17592
+ this.#markSeen(packet.id);
17593
+ let forward = false;
17594
+ switch (packet.type) {
17595
+ case 'direct':
17596
+ case 'archive-delivery': {
17597
+ const envelope = packet.payload.envelope;
17598
+ try {
17599
+ validateEnvelope(envelope, this.#network);
17600
+ }
17601
+ catch {
17602
+ break;
17603
+ }
17604
+ if (!(await this.#verifyEnvelope(envelope)))
17605
+ break;
17606
+ forward = true;
17607
+ if (packet.type === 'direct' && this.#enabled)
17608
+ await this.#saveArchive(envelope);
17609
+ if (envelope.recipientAccount === this.#account && envelope.recipientDeviceId === this.#deviceId) {
17610
+ for (const handler of this.#handlers)
17611
+ handler({ type: 'message', envelope });
17612
+ }
17613
+ break;
17614
+ }
17615
+ case 'inbox-request': {
17616
+ const request = packet.payload.request;
17617
+ if (!(await this.#validInboxRequest(request)))
17618
+ break;
17619
+ forward = true;
17620
+ if (this.#enabled && !(request.account === this.#account && request.deviceId === this.#deviceId))
17621
+ await this.#deliverTo(request);
17622
+ break;
17623
+ }
17624
+ case 'delivery-receipt': {
17625
+ const receipt = packet.payload.receipt;
17626
+ forward = await this.#receiveReceipt(receipt);
17627
+ break;
17628
+ }
17629
+ }
17630
+ if (forward && packet.hopsRemaining > 0) {
17631
+ await this.#peernet.publish(this.#topic, encode({ ...packet, hopsRemaining: packet.hopsRemaining - 1 }));
17632
+ }
17633
+ }
17634
+ async #validInboxRequest(request) {
17635
+ return Boolean(request &&
17636
+ request.network === this.#network &&
17637
+ typeof request.account === 'string' &&
17638
+ typeof request.deviceId === 'string' &&
17639
+ Number.isFinite(request.timestamp) &&
17640
+ Math.abs(Date.now() - request.timestamp) <= MAX_CLOCK_SKEW &&
17641
+ typeof request.signature === 'string' &&
17642
+ await this.#verifyInboxRequest(request));
17643
+ }
17644
+ async #saveArchive(envelope) {
17645
+ if (this.#archives.has(envelope.id))
17646
+ return;
17647
+ await this.#store.put(recordKey(this.#network, 'archive', envelope.id), encode({
17648
+ envelope,
17649
+ storedAt: Date.now()
17650
+ }));
17651
+ this.#archives.add(envelope.id);
17652
+ if (this.#archives.size > MAX_ARCHIVES)
17653
+ await this.#dropOldest('archive');
17654
+ }
17655
+ async #deliverTo(request) {
17656
+ for (const id of this.#archives) {
17657
+ const bytes = await storeGet(this.#store, recordKey(this.#network, 'archive', id));
17658
+ if (!bytes) {
17659
+ this.#archives.delete(id);
17660
+ continue;
17661
+ }
17662
+ const record = JSON.parse(decoder.decode(bytes));
17663
+ const envelope = record?.envelope;
17664
+ if (envelope?.recipientAccount === request.account && envelope.recipientDeviceId === request.deviceId)
17665
+ await this.#publish('archive-delivery', { envelope });
17666
+ }
17667
+ }
17668
+ async #receiveReceipt(receipt) {
17669
+ if (!receipt || receipt.network !== this.#network ||
17670
+ typeof receipt.messageId !== 'string' ||
17671
+ !Number.isFinite(receipt.timestamp) ||
17672
+ Math.abs(Date.now() - receipt.timestamp) > MAX_CLOCK_SKEW)
17673
+ return false;
17674
+ const keys = ['archive', 'outbox'];
17675
+ let verified = false;
17676
+ for (const kind of keys) {
17677
+ const key = recordKey(this.#network, kind, receipt.messageId);
17678
+ const bytes = await storeGet(this.#store, key);
17679
+ if (!bytes)
17680
+ continue;
17681
+ const stored = JSON.parse(decoder.decode(bytes));
17682
+ const envelope = kind === 'archive' ? stored.envelope : stored;
17683
+ if (!(await this.#verifyReceipt(receipt, envelope)))
17684
+ continue;
17685
+ await this.#store.delete(key);
17686
+ (kind === 'archive' ? this.#archives : this.#outbox).delete(receipt.messageId);
17687
+ verified = true;
17688
+ }
17689
+ return verified;
17690
+ }
17691
+ async #retryOutbox() {
17692
+ if (!this.#started)
17693
+ return;
17694
+ await this.#prune();
17695
+ for (const id of this.#outbox) {
17696
+ const bytes = await storeGet(this.#store, recordKey(this.#network, 'outbox', id));
17697
+ if (bytes)
17698
+ await this.#publish('direct', { envelope: JSON.parse(decoder.decode(bytes)) });
17699
+ }
17700
+ }
17701
+ async #prune() {
17702
+ const cutoff = Date.now() - MAX_AGE;
17703
+ for (const [kind, ids] of [['archive', this.#archives], ['outbox', this.#outbox]]) {
17704
+ for (const id of ids) {
17705
+ const key = recordKey(this.#network, kind, id);
17706
+ const bytes = await storeGet(this.#store, key);
17707
+ if (!bytes) {
17708
+ ids.delete(id);
17709
+ continue;
17710
+ }
17711
+ const saved = JSON.parse(decoder.decode(bytes));
17712
+ const timestamp = kind === 'archive' ? saved.storedAt : saved.timestamp;
17713
+ if (!Number.isFinite(timestamp) || timestamp < cutoff) {
17714
+ await this.#store.delete(key);
17715
+ ids.delete(id);
17716
+ }
17717
+ }
17718
+ }
17719
+ }
17720
+ async #dropOldest(kind) {
17721
+ const ids = kind === 'archive' ? this.#archives : this.#outbox;
17722
+ const id = ids.values().next().value;
17723
+ if (!id)
17724
+ return;
17725
+ await this.#store.delete(recordKey(this.#network, kind, id));
17726
+ ids.delete(id);
17727
+ }
17728
+ #wasSeen(id) {
17729
+ const timestamp = this.#seen.get(id);
17730
+ return timestamp !== undefined && Date.now() - timestamp < MAX_AGE;
17731
+ }
17732
+ #markSeen(id) {
17733
+ this.#seen.set(id, Date.now());
17734
+ if (this.#seen.size > 4_000) {
17735
+ const oldestId = this.#seen.keys().next().value;
17736
+ if (oldestId !== undefined)
17737
+ this.#seen.delete(oldestId);
17738
+ }
17739
+ }
17740
+ }
17741
+
17309
17742
  const DEFAULT_CHUNK_SIZE = 256 * 1024;
17310
17743
  const DEFAULT_BLOCK_CHUNK_SIZE = 256 * 1024;
17311
17744
  const DEFAULT_BLOCK_CHUNK_THRESHOLD = 1024 * 1024;
@@ -17332,7 +17765,7 @@ class Peernet {
17332
17765
  root;
17333
17766
  identity;
17334
17767
  stores = [];
17335
- peerId;
17768
+ routingId;
17336
17769
  /**
17337
17770
  * @type {Object}
17338
17771
  * @property {Object} peer Instance of Peer
@@ -17367,6 +17800,7 @@ class Peernet {
17367
17800
  * @param {String} options.root - path to root directory
17368
17801
  * @param {String} options.version - path to root directory
17369
17802
  * @param {String} options.storePrefix - prefix for datatores (lfc)
17803
+ * @param {String} options.routingId - optional transport identity, separate from the wallet identity
17370
17804
  * @param {Object} options.transport - transport selection and fallback options passed to @netpeer/swarm
17371
17805
  *
17372
17806
  * @return {Promise} instance of Peernet
@@ -17379,6 +17813,9 @@ class Peernet {
17379
17813
  * @property {String} network - current network
17380
17814
  */
17381
17815
  this.network = options.network || 'leofcoin';
17816
+ if (options.routingId !== undefined && (typeof options.routingId !== 'string' || !options.routingId.trim()))
17817
+ throw new TypeError('routingId must be a non-empty string');
17818
+ this.routingId = options.routingId?.trim();
17382
17819
  this.autoStart = options.autoStart === undefined ? true : options.autoStart;
17383
17820
  this.stars = options.stars;
17384
17821
  this.transport = options.transport;
@@ -17410,6 +17847,9 @@ class Peernet {
17410
17847
  return this._init(options, password);
17411
17848
  }
17412
17849
  get id() {
17850
+ return this.routingId || this.identity.id;
17851
+ }
17852
+ get peerId() {
17413
17853
  return this.identity.id;
17414
17854
  }
17415
17855
  get selectedAccount() {
@@ -17431,6 +17871,9 @@ class Peernet {
17431
17871
  addCodec(codec) {
17432
17872
  return utils.addCodec(codec);
17433
17873
  }
17874
+ createArchiveMode(options) {
17875
+ return new PeernetArchiveMode({ ...options, peernet: this });
17876
+ }
17434
17877
  async addStore(name, prefix, root, isPrivate = true) {
17435
17878
  if (name === 'block' ||
17436
17879
  name === 'transaction' ||
@@ -17484,7 +17927,7 @@ class Peernet {
17484
17927
  await getAddress();
17485
17928
  this.storePrefix = options.storePrefix;
17486
17929
  this.root = options.root;
17487
- const { RequestMessage, ResponseMessage, PeerMessage, PeerMessageResponse, PeernetMessage, DHTMessage, DHTMessageResponse, DataMessage, DataMessageResponse, PsMessage, ChatMessage, PeernetFile } = await import(/* webpackChunkName: "messages" */ './messages-BGHaihSi-eHZVl0y9.js');
17930
+ const { RequestMessage, ResponseMessage, PeerMessage, PeerMessageResponse, PeernetMessage, DHTMessage, DHTMessageResponse, DataMessage, DataMessageResponse, PsMessage, ChatMessage, PeernetFile } = await import(/* webpackChunkName: "messages" */ './messages-DCePyqKC-BSvJxDWZ.js');
17488
17931
  /**
17489
17932
  * proto Object containing protos
17490
17933
  * @type {Object}
@@ -17517,7 +17960,6 @@ class Peernet {
17517
17960
  this.identity = new Identity(this.network);
17518
17961
  await this.identity.load(password, options.freshIdentity === true);
17519
17962
  this._peerHandler = new PeerDiscovery(this.id);
17520
- this.peerId = this.id;
17521
17963
  /**
17522
17964
  * converts data -> message -> proto
17523
17965
  * @see DataHandler
@@ -17531,7 +17973,7 @@ class Peernet {
17531
17973
  if (this.#starting || this.#started)
17532
17974
  return;
17533
17975
  this.#starting = true;
17534
- const importee = await import('./client-C_pBoi1g-_pfKzCvX.js');
17976
+ const importee = await import('./client-DKKzPSlb-C_1ug6ux.js');
17535
17977
  /**
17536
17978
  * @access public
17537
17979
  * @type {PeernetClient}
@@ -1,4 +1,4 @@
1
- export { N as default } from './node-browser-BNe-en1l.js';
1
+ export { N as default } from './node-browser-Ha4Lvntj.js';
2
2
  import './browser-crypto-CH0ua22L.js';
3
3
  import './connection-monitor-D-krNiRH.js';
4
4
  import './index-CvKt4UDE.js';
package/exports/chain.js CHANGED
@@ -266,7 +266,7 @@ class Transaction extends Protocol {
266
266
  await globalThis.transactionPoolStore.put(hash, message.encoded);
267
267
  this.addPendingNonce(message.decoded.from, message.decoded.nonce);
268
268
  try {
269
- peernet.publish("add-transaction", message.encoded);
269
+ await peernet.publish("add-transaction", message.encoded);
270
270
  } catch (publishError) {
271
271
  console.warn("peernet publish failed: add-transaction", publishError?.message ?? publishError);
272
272
  }
@@ -2960,39 +2960,29 @@ class Chain extends VersionControl {
2960
2960
  }
2961
2961
  async getPeerTransactionPool(peer) {
2962
2962
  let transactionsInPool = await this.#makeRequest(peer, "transactionPool");
2963
- console.log("raw response for request transactionPool:", transactionsInPool);
2964
2963
  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
- }
2964
+ transactionsInPool = new TextDecoder().decode(transactionsInPool);
2971
2965
  }
2972
2966
  if (typeof transactionsInPool === "string") {
2973
- try {
2974
- transactionsInPool = JSON.parse(transactionsInPool);
2975
- } catch {
2976
- return [];
2977
- }
2967
+ transactionsInPool = JSON.parse(transactionsInPool);
2978
2968
  }
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
- }
2969
+ if (!Array.isArray(transactionsInPool) || transactionsInPool.some((hash) => typeof hash !== "string")) {
2970
+ throw new Error("transactionPool response must be an array of transaction hashes");
2971
+ }
2972
+ const recovered = [];
2973
+ for (const hash of new Set(transactionsInPool)) {
2974
+ if (await globalThis.transactionPoolStore.has(hash)) continue;
2975
+ if (await globalThis.transactionStore.has(hash)) continue;
2976
+ try {
2977
+ const txData = await globalThis.peernet.get(hash, "transactionPool");
2978
+ if (txData === void 0) throw new Error("pending transaction body unavailable");
2979
+ const transaction = await resolveTransactionReference(hash, txData);
2980
+ recovered.push(await this.#sendTransaction(transaction));
2981
+ } catch (error) {
2982
+ debug(`Failed to recover transaction ${hash}:`, error?.message ?? error);
2993
2983
  }
2994
2984
  }
2995
- return Promise.all(transactionsToGet);
2985
+ return recovered;
2996
2986
  }
2997
2987
  async #peerConnected(peerId) {
2998
2988
  if (typeof peerId !== "string" || !peerId.trim() || peerId === "undefined") {
@@ -3060,6 +3050,14 @@ class Chain extends VersionControl {
3060
3050
  await this.#recordPeerFailure(peerId, `unreasonable lastBlock index: ${lastBlock.index}`);
3061
3051
  return;
3062
3052
  }
3053
+ setTimeout(async () => {
3054
+ try {
3055
+ await this.getPeerTransactionPool(peer);
3056
+ } catch (error) {
3057
+ const peerName = peer?.peerId || peer?.id || peer?.address || peerId || "unknown";
3058
+ debug(`transactionPool request failed: ${peerName}:`, error?.message ?? error);
3059
+ }
3060
+ }, 3e3);
3063
3061
  if (!lastBlock || !lastBlock.hash || lastBlock.hash === "0x0") {
3064
3062
  debug(`peer has no lastBlock: ${peerId}`);
3065
3063
  return;
@@ -3099,15 +3097,6 @@ class Chain extends VersionControl {
3099
3097
  }
3100
3098
  if (this.wantList.length === 0) await this.triggerSync();
3101
3099
  }
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
3100
  try {
3112
3101
  let stateInfo = await this.#makeRequest(peer, "stateInfo");
3113
3102
  if (stateInfo instanceof Uint8Array) {
@@ -3484,18 +3473,32 @@ class Chain extends VersionControl {
3484
3473
  await this.validateTransactionSignature(transaction);
3485
3474
  let hash;
3486
3475
  try {
3476
+ hash = await transaction.hash();
3477
+ if (await globalThis.transactionStore.has(hash)) return hash;
3478
+ const alreadyPending = await globalThis.transactionPoolStore.has(hash);
3487
3479
  hash = await enqueueTransaction(transaction, {
3488
3480
  putToPool: (txHash, data) => globalThis.transactionPoolStore.put(txHash, data),
3489
3481
  hasInPool: (txHash) => globalThis.transactionPoolStore.has(txHash),
3490
3482
  hasInStore: (txHash) => globalThis.transactionStore.has(txHash),
3491
3483
  addPendingNonce: (address, nonce) => this.addPendingNonce(address, nonce)
3492
3484
  });
3493
- if (this.#participating && !this.#runningEpoch) this.#runEpoch();
3485
+ if (!alreadyPending) {
3486
+ try {
3487
+ await globalThis.peernet.publish("add-transaction", transaction.encoded);
3488
+ } catch (publishError) {
3489
+ debug("peernet publish failed: add-transaction relay", publishError?.message ?? publishError);
3490
+ }
3491
+ }
3492
+ if (this.#participating && !this.#runningEpoch) {
3493
+ void this.#runEpoch().catch((error) => {
3494
+ debug("transaction epoch scheduling failed:", error?.message ?? error);
3495
+ });
3496
+ }
3494
3497
  return hash;
3495
3498
  } catch (e) {
3496
3499
  if (hash) {
3497
3500
  try {
3498
- globalThis.peernet.publish("invalid-transaction", hash);
3501
+ await globalThis.peernet.publish("invalid-transaction", hash);
3499
3502
  } catch (publishError) {
3500
3503
  debug("peernet publish failed: invalid-transaction", publishError?.message ?? publishError);
3501
3504
  }
@@ -3513,7 +3516,7 @@ class Chain extends VersionControl {
3513
3516
  const event = await super.sendTransaction(transactionMessage);
3514
3517
  await this.#sendTransaction(transactionMessage.encoded);
3515
3518
  try {
3516
- globalThis.peernet.publish("send-transaction", transactionMessage.encoded);
3519
+ await globalThis.peernet.publish("send-transaction", transactionMessage.encoded);
3517
3520
  } catch (publishError) {
3518
3521
  debug("peernet publish failed: send-transaction", publishError?.message ?? publishError);
3519
3522
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leofcoin/chain",
3
- "version": "1.10.19",
3
+ "version": "1.10.21",
4
4
  "description": "Official javascript implementation",
5
5
  "private": false,
6
6
  "exports": {
@@ -81,10 +81,10 @@
81
81
  "@rollup/plugin-commonjs": "^29.0.3",
82
82
  "@rollup/plugin-json": "^6.1.0",
83
83
  "@rollup/plugin-node-resolve": "^16.0.3",
84
- "@rollup/plugin-typescript": "^12.3.0",
84
+ "@rollup/plugin-typescript": "^12.3.1",
85
85
  "@types/semver": "^7.8.0",
86
86
  "@vandeurenglenn/debug": "^1.4.0",
87
- "rollup": "^4.64.12",
87
+ "rollup": "^4.65.0",
88
88
  "rollup-plugin-esbuild": "^6.2.1",
89
89
  "rollup-plugin-modify": "^3.0.0",
90
90
  "tape": "^5.10.3",
@@ -99,14 +99,14 @@
99
99
  "@leofcoin/contracts": "^0.1.27",
100
100
  "@leofcoin/crypto": "^0.2.41",
101
101
  "@leofcoin/errors": "^1.0.30",
102
- "@leofcoin/lib": "^1.3.15",
102
+ "@leofcoin/lib": "^1.3.16",
103
103
  "@leofcoin/messages": "^1.6.5",
104
104
  "@leofcoin/multi-wallet": "^3.1.11",
105
105
  "@leofcoin/networks": "^1.1.31",
106
- "@leofcoin/peernet": "^3.0.0",
106
+ "@leofcoin/peernet": "^3.1.0",
107
107
  "@leofcoin/storage": "^3.5.38",
108
108
  "@leofcoin/utils": "^1.1.45",
109
- "@leofcoin/workers": "^1.5.40",
109
+ "@leofcoin/workers": "^1.5.42",
110
110
  "@vandeurenglenn/base58": "^1.1.9",
111
111
  "@vandeurenglenn/easy-worker": "^1.0.3",
112
112
  "@vandeurenglenn/typed-array-utils": "^1.2.0",