@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 +17 -0
- package/exports/browser/{browser-CCfR9KOM-CpStHgTj.js → browser-CCfR9KOM-BGMMnVsR.js} +1 -1
- package/exports/browser/chain.js +43 -40
- package/exports/browser/{client-C_pBoi1g-_pfKzCvX.js → client-DKKzPSlb-C_1ug6ux.js} +2 -2
- package/exports/browser/{index-dTQVgkpf-BbdshWB0.js → index-dTQVgkpf-DOL2rIjp.js} +1 -1
- package/exports/browser/{messages-BGHaihSi-eHZVl0y9.js → messages-DCePyqKC-BSvJxDWZ.js} +1 -1
- package/exports/browser/{node-browser-BNe-en1l.js → node-browser-Ha4Lvntj.js} +472 -30
- package/exports/browser/node-browser.js +1 -1
- package/exports/chain.js +43 -40
- package/package.json +6 -6
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>.
|
package/exports/browser/chain.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
6091
|
-
transactionsInPool = JSON.parse(transactionsInPool);
|
|
6092
|
-
} catch {
|
|
6093
|
-
return [];
|
|
6094
|
-
}
|
|
6084
|
+
transactionsInPool = JSON.parse(transactionsInPool);
|
|
6095
6085
|
}
|
|
6096
|
-
if (!Array.isArray(transactionsInPool))
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
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
|
|
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 (
|
|
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-
|
|
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-
|
|
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-
|
|
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';
|
|
@@ -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-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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-
|
|
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-
|
|
17976
|
+
const importee = await import('./client-DKKzPSlb-C_1ug6ux.js');
|
|
17535
17977
|
/**
|
|
17536
17978
|
* @access public
|
|
17537
17979
|
* @type {PeernetClient}
|
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
|
-
|
|
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
|
-
|
|
2974
|
-
transactionsInPool = JSON.parse(transactionsInPool);
|
|
2975
|
-
} catch {
|
|
2976
|
-
return [];
|
|
2977
|
-
}
|
|
2967
|
+
transactionsInPool = JSON.parse(transactionsInPool);
|
|
2978
2968
|
}
|
|
2979
|
-
if (!Array.isArray(transactionsInPool))
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
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
|
|
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 (
|
|
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.
|
|
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.
|
|
84
|
+
"@rollup/plugin-typescript": "^12.3.1",
|
|
85
85
|
"@types/semver": "^7.8.0",
|
|
86
86
|
"@vandeurenglenn/debug": "^1.4.0",
|
|
87
|
-
"rollup": "^4.
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|