quantumcoin 6.14.2 → 6.14.5
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/CHANGELOG.md +442 -442
- package/FUNDING.json +10 -10
- package/LICENSE.md +21 -21
- package/README.md +132 -142
- package/SECURITY.md +34 -34
- package/dist/README.md +22 -22
- package/dist/quantumcoin.js +1127 -1324
- package/dist/quantumcoin.js.map +1 -1
- package/dist/quantumcoin.min.js +1 -1
- package/dist/quantumcoin.umd.js +1128 -1327
- package/dist/quantumcoin.umd.js.map +1 -1
- package/dist/quantumcoin.umd.min.js +1 -1
- package/dist/wordlists-extra.js +1 -1
- package/dist/wordlists-extra.js.map +1 -1
- package/dist/wordlists-extra.min.js +1 -1
- package/lib.commonjs/README.md +16 -16
- package/lib.commonjs/_version.js +1 -1
- package/lib.commonjs/crypto/signature.d.ts +3 -76
- package/lib.commonjs/crypto/signature.d.ts.map +1 -1
- package/lib.commonjs/crypto/signature.js +15 -199
- package/lib.commonjs/crypto/signature.js.map +1 -1
- package/lib.commonjs/crypto/signing-key.d.ts +1 -1
- package/lib.commonjs/crypto/signing-key.d.ts.map +1 -1
- package/lib.commonjs/crypto/signing-key.js +19 -10
- package/lib.commonjs/crypto/signing-key.js.map +1 -1
- package/lib.commonjs/package.json +12 -12
- package/lib.commonjs/providers/provider-jsonrpc.d.ts +0 -1
- package/lib.commonjs/providers/provider-jsonrpc.d.ts.map +1 -1
- package/lib.commonjs/providers/provider-jsonrpc.js +0 -1
- package/lib.commonjs/providers/provider-jsonrpc.js.map +1 -1
- package/lib.commonjs/quantumcoin.d.ts +2 -0
- package/lib.commonjs/quantumcoin.d.ts.map +1 -1
- package/lib.commonjs/quantumcoin.js +11 -5
- package/lib.commonjs/quantumcoin.js.map +1 -1
- package/lib.commonjs/transaction/address.d.ts.map +1 -1
- package/lib.commonjs/transaction/address.js +8 -3
- package/lib.commonjs/transaction/address.js.map +1 -1
- package/lib.commonjs/transaction/transaction.d.ts.map +1 -1
- package/lib.commonjs/transaction/transaction.js +7 -40
- package/lib.commonjs/transaction/transaction.js.map +1 -1
- package/lib.commonjs/wallet/json-keystore.d.ts.map +1 -1
- package/lib.commonjs/wallet/json-keystore.js +7 -7
- package/lib.commonjs/wallet/json-keystore.js.map +1 -1
- package/lib.commonjs/wallet/wallet.d.ts.map +1 -1
- package/lib.commonjs/wallet/wallet.js +2 -2
- package/lib.commonjs/wallet/wallet.js.map +1 -1
- package/lib.esm/README.md +16 -16
- package/lib.esm/_version.js +1 -1
- package/lib.esm/crypto/signature.d.ts +3 -76
- package/lib.esm/crypto/signature.d.ts.map +1 -1
- package/lib.esm/crypto/signature.js +16 -202
- package/lib.esm/crypto/signature.js.map +1 -1
- package/lib.esm/crypto/signing-key.d.ts +1 -1
- package/lib.esm/crypto/signing-key.d.ts.map +1 -1
- package/lib.esm/crypto/signing-key.js +20 -9
- package/lib.esm/crypto/signing-key.js.map +1 -1
- package/lib.esm/package.json +12 -12
- package/lib.esm/providers/provider-jsonrpc.d.ts +0 -1
- package/lib.esm/providers/provider-jsonrpc.d.ts.map +1 -1
- package/lib.esm/providers/provider-jsonrpc.js +0 -1
- package/lib.esm/providers/provider-jsonrpc.js.map +1 -1
- package/lib.esm/quantumcoin.d.ts +2 -0
- package/lib.esm/quantumcoin.d.ts.map +1 -1
- package/lib.esm/quantumcoin.js +6 -0
- package/lib.esm/quantumcoin.js.map +1 -1
- package/lib.esm/transaction/address.d.ts.map +1 -1
- package/lib.esm/transaction/address.js +8 -2
- package/lib.esm/transaction/address.js.map +1 -1
- package/lib.esm/transaction/transaction.d.ts.map +1 -1
- package/lib.esm/transaction/transaction.js +7 -40
- package/lib.esm/transaction/transaction.js.map +1 -1
- package/lib.esm/wallet/json-keystore.d.ts.map +1 -1
- package/lib.esm/wallet/json-keystore.js +11 -5
- package/lib.esm/wallet/json-keystore.js.map +1 -1
- package/lib.esm/wallet/wallet.d.ts.map +1 -1
- package/lib.esm/wallet/wallet.js +3 -1
- package/lib.esm/wallet/wallet.js.map +1 -1
- package/package.json +6 -5
- package/rollup.config.mjs +50 -50
- package/src.ts/_version.ts +1 -1
- package/src.ts/abi/abi-coder.ts +237 -237
- package/src.ts/abi/bytes32.ts +45 -45
- package/src.ts/abi/coders/abstract-coder.ts +541 -541
- package/src.ts/abi/coders/address.ts +36 -36
- package/src.ts/abi/coders/anonymous.ts +29 -29
- package/src.ts/abi/coders/array.ts +199 -199
- package/src.ts/abi/coders/boolean.ts +27 -27
- package/src.ts/abi/coders/bytes.ts +43 -43
- package/src.ts/abi/coders/fixed-bytes.ts +37 -37
- package/src.ts/abi/coders/null.ts +28 -28
- package/src.ts/abi/coders/number.ts +63 -63
- package/src.ts/abi/coders/string.ts +29 -29
- package/src.ts/abi/coders/tuple.ts +69 -69
- package/src.ts/abi/fragments.ts +1617 -1617
- package/src.ts/abi/index.ts +41 -41
- package/src.ts/abi/interface.ts +1271 -1271
- package/src.ts/abi/typed.ts +796 -796
- package/src.ts/address/address.ts +148 -148
- package/src.ts/address/checks.ts +123 -123
- package/src.ts/address/contract-address.ts +80 -80
- package/src.ts/address/index.ts +57 -57
- package/src.ts/constants/addresses.ts +8 -8
- package/src.ts/constants/hashes.ts +7 -7
- package/src.ts/constants/index.ts +16 -16
- package/src.ts/constants/numbers.ts +35 -35
- package/src.ts/constants/strings.ts +16 -16
- package/src.ts/contract/contract.ts +1120 -1120
- package/src.ts/contract/factory.ts +143 -143
- package/src.ts/contract/index.ts +31 -31
- package/src.ts/contract/types.ts +236 -236
- package/src.ts/contract/wrappers.ts +225 -225
- package/src.ts/crypto/crypto-browser.ts +64 -64
- package/src.ts/crypto/crypto.ts +4 -4
- package/src.ts/crypto/hmac.ts +51 -51
- package/src.ts/crypto/index.ts +59 -59
- package/src.ts/crypto/keccak.ts +54 -54
- package/src.ts/crypto/pbkdf2.ts +55 -55
- package/src.ts/crypto/random.ts +36 -36
- package/src.ts/crypto/ripemd160.ts +43 -43
- package/src.ts/crypto/scrypt.ts +114 -114
- package/src.ts/crypto/sha2.ts +78 -78
- package/src.ts/crypto/signature.ts +145 -349
- package/src.ts/crypto/signing-key.ts +126 -118
- package/src.ts/hash/authorization.ts +38 -38
- package/src.ts/hash/id.ts +17 -17
- package/src.ts/hash/index.ts +18 -18
- package/src.ts/hash/message.ts +51 -51
- package/src.ts/hash/namehash.ts +101 -101
- package/src.ts/hash/solidity.ts +117 -117
- package/src.ts/hash/typed-data.ts +658 -658
- package/src.ts/index.ts +12 -12
- package/src.ts/providers/abstract-provider.ts +1761 -1761
- package/src.ts/providers/abstract-signer.ts +314 -314
- package/src.ts/providers/community.ts +49 -49
- package/src.ts/providers/contracts.ts +42 -42
- package/src.ts/providers/default-provider.ts +96 -96
- package/src.ts/providers/ens-resolver.ts +606 -606
- package/src.ts/providers/format.ts +320 -320
- package/src.ts/providers/formatting.ts +418 -418
- package/src.ts/providers/index.ts +125 -125
- package/src.ts/providers/network.ts +327 -327
- package/src.ts/providers/pagination.ts +8 -8
- package/src.ts/providers/plugin-fallback.ts +35 -35
- package/src.ts/providers/plugins-network.ts +281 -281
- package/src.ts/providers/provider-browser.ts +334 -334
- package/src.ts/providers/provider-fallback.ts +801 -801
- package/src.ts/providers/provider-ipcsocket-browser.ts +3 -3
- package/src.ts/providers/provider-ipcsocket.ts +81 -81
- package/src.ts/providers/provider-jsonrpc.ts +1334 -1335
- package/src.ts/providers/provider-socket.ts +352 -352
- package/src.ts/providers/provider-websocket.ts +103 -103
- package/src.ts/providers/provider.ts +2136 -2136
- package/src.ts/providers/signer-noncemanager.ts +98 -98
- package/src.ts/providers/signer.ts +166 -166
- package/src.ts/providers/subscriber-connection.ts +74 -74
- package/src.ts/providers/subscriber-filterid.ts +199 -199
- package/src.ts/providers/subscriber-polling.ts +321 -321
- package/src.ts/providers/ws-browser.ts +11 -11
- package/src.ts/providers/ws.ts +3 -3
- package/src.ts/quantumcoin.ts +219 -211
- package/src.ts/thirdparty.d.ts +16 -16
- package/src.ts/transaction/accesslist.ts +43 -43
- package/src.ts/transaction/address.ts +35 -31
- package/src.ts/transaction/authorization.ts +14 -14
- package/src.ts/transaction/index.ts +51 -51
- package/src.ts/transaction/transaction.ts +1349 -1379
- package/src.ts/utils/base58.ts +73 -73
- package/src.ts/utils/base64-browser.ts +25 -25
- package/src.ts/utils/base64.ts +56 -56
- package/src.ts/utils/data.ts +199 -199
- package/src.ts/utils/errors.ts +793 -793
- package/src.ts/utils/events.ts +105 -105
- package/src.ts/utils/fetch.ts +970 -970
- package/src.ts/utils/fixednumber.ts +643 -643
- package/src.ts/utils/geturl-browser.ts +81 -81
- package/src.ts/utils/geturl.ts +134 -134
- package/src.ts/utils/index.ts +95 -95
- package/src.ts/utils/maths.ts +240 -240
- package/src.ts/utils/properties.ts +60 -60
- package/src.ts/utils/rlp-decode.ts +104 -104
- package/src.ts/utils/rlp-encode.ts +64 -64
- package/src.ts/utils/rlp.ts +20 -20
- package/src.ts/utils/units.ts +91 -91
- package/src.ts/utils/utf8.ts +325 -325
- package/src.ts/utils/uuid.ts +36 -36
- package/src.ts/wallet/base-wallet.ts +160 -160
- package/src.ts/wallet/index.ts +32 -32
- package/src.ts/wallet/json-keystore.ts +108 -106
- package/src.ts/wallet/utils.ts +147 -147
- package/src.ts/wallet/wallet.ts +138 -139
- package/src.ts/wordlists/bit-reader.ts +35 -35
- package/src.ts/wordlists/decode-owl.ts +58 -58
- package/src.ts/wordlists/decode-owla.ts +33 -33
- package/src.ts/wordlists/generation/encode-latin.ts +370 -370
- package/src.ts/wordlists/index.ts +26 -26
- package/src.ts/wordlists/lang-cz.ts +33 -33
- package/src.ts/wordlists/lang-en.ts +33 -33
- package/src.ts/wordlists/lang-es.ts +35 -35
- package/src.ts/wordlists/lang-fr.ts +34 -34
- package/src.ts/wordlists/lang-it.ts +33 -33
- package/src.ts/wordlists/lang-ja.ts +181 -181
- package/src.ts/wordlists/lang-ko.ts +104 -104
- package/src.ts/wordlists/lang-pt.ts +34 -34
- package/src.ts/wordlists/lang-zh.ts +112 -112
- package/src.ts/wordlists/wordlist-owl.ts +77 -77
- package/src.ts/wordlists/wordlist-owla.ts +41 -41
- package/src.ts/wordlists/wordlist.ts +59 -59
- package/src.ts/wordlists/wordlists-browser.ts +8 -8
- package/src.ts/wordlists/wordlists-extra.ts +9 -9
- package/src.ts/wordlists/wordlists.ts +38 -38
- package/dist/quantumcoin.min.js'.gz' +0 -0
- package/dist/quantumcoin.umd.min.js'.gz' +0 -0
- package/dist/wordlists-extra.min.js'.gz' +0 -0
- package/lib.commonjs/providers/provider-alchemy.d.ts +0 -50
- package/lib.commonjs/providers/provider-alchemy.d.ts.map +0 -1
- package/lib.commonjs/providers/provider-alchemy.js +0 -151
- package/lib.commonjs/providers/provider-alchemy.js.map +0 -1
- package/lib.commonjs/providers/provider-ankr.d.ts +0 -61
- package/lib.commonjs/providers/provider-ankr.d.ts.map +0 -1
- package/lib.commonjs/providers/provider-ankr.js +0 -137
- package/lib.commonjs/providers/provider-ankr.js.map +0 -1
- package/lib.commonjs/providers/provider-blockscout.d.ts +0 -59
- package/lib.commonjs/providers/provider-blockscout.d.ts.map +0 -1
- package/lib.commonjs/providers/provider-blockscout.js +0 -145
- package/lib.commonjs/providers/provider-blockscout.js.map +0 -1
- package/lib.commonjs/providers/provider-chainstack.d.ts +0 -46
- package/lib.commonjs/providers/provider-chainstack.d.ts.map +0 -1
- package/lib.commonjs/providers/provider-chainstack.js +0 -102
- package/lib.commonjs/providers/provider-chainstack.js.map +0 -1
- package/lib.commonjs/providers/provider-cloudflare.d.ts +0 -14
- package/lib.commonjs/providers/provider-cloudflare.d.ts.map +0 -1
- package/lib.commonjs/providers/provider-cloudflare.js +0 -26
- package/lib.commonjs/providers/provider-cloudflare.js.map +0 -1
- package/lib.commonjs/providers/provider-etherscan.d.ts +0 -147
- package/lib.commonjs/providers/provider-etherscan.d.ts.map +0 -1
- package/lib.commonjs/providers/provider-etherscan.js +0 -587
- package/lib.commonjs/providers/provider-etherscan.js.map +0 -1
- package/lib.commonjs/providers/provider-infura.d.ts +0 -101
- package/lib.commonjs/providers/provider-infura.d.ts.map +0 -1
- package/lib.commonjs/providers/provider-infura.js +0 -206
- package/lib.commonjs/providers/provider-infura.js.map +0 -1
- package/lib.commonjs/providers/provider-pocket.d.ts +0 -54
- package/lib.commonjs/providers/provider-pocket.d.ts.map +0 -1
- package/lib.commonjs/providers/provider-pocket.js +0 -109
- package/lib.commonjs/providers/provider-pocket.js.map +0 -1
- package/lib.commonjs/providers/provider-quicknode.d.ts +0 -59
- package/lib.commonjs/providers/provider-quicknode.d.ts.map +0 -1
- package/lib.commonjs/providers/provider-quicknode.js +0 -163
- package/lib.commonjs/providers/provider-quicknode.js.map +0 -1
- package/lib.commonjs/wallet/hdwallet.d.ts +0 -248
- package/lib.commonjs/wallet/hdwallet.d.ts.map +0 -1
- package/lib.commonjs/wallet/hdwallet.js +0 -505
- package/lib.commonjs/wallet/hdwallet.js.map +0 -1
- package/lib.commonjs/wallet/json-crowdsale.d.ts +0 -27
- package/lib.commonjs/wallet/json-crowdsale.d.ts.map +0 -1
- package/lib.commonjs/wallet/json-crowdsale.js +0 -60
- package/lib.commonjs/wallet/json-crowdsale.js.map +0 -1
- package/lib.commonjs/wallet/mnemonic.d.ts +0 -65
- package/lib.commonjs/wallet/mnemonic.d.ts.map +0 -1
- package/lib.commonjs/wallet/mnemonic.js +0 -169
- package/lib.commonjs/wallet/mnemonic.js.map +0 -1
- package/lib.commonjs/wallet/seedwallet.d.ts +0 -4
- package/lib.commonjs/wallet/seedwallet.d.ts.map +0 -1
- package/lib.commonjs/wallet/seedwallet.js +0 -8
- package/lib.commonjs/wallet/seedwallet.js.map +0 -1
- package/lib.esm/providers/provider-alchemy.d.ts +0 -50
- package/lib.esm/providers/provider-alchemy.d.ts.map +0 -1
- package/lib.esm/providers/provider-alchemy.js +0 -147
- package/lib.esm/providers/provider-alchemy.js.map +0 -1
- package/lib.esm/providers/provider-ankr.d.ts +0 -61
- package/lib.esm/providers/provider-ankr.d.ts.map +0 -1
- package/lib.esm/providers/provider-ankr.js +0 -133
- package/lib.esm/providers/provider-ankr.js.map +0 -1
- package/lib.esm/providers/provider-blockscout.d.ts +0 -59
- package/lib.esm/providers/provider-blockscout.d.ts.map +0 -1
- package/lib.esm/providers/provider-blockscout.js +0 -141
- package/lib.esm/providers/provider-blockscout.js.map +0 -1
- package/lib.esm/providers/provider-chainstack.d.ts +0 -46
- package/lib.esm/providers/provider-chainstack.d.ts.map +0 -1
- package/lib.esm/providers/provider-chainstack.js +0 -98
- package/lib.esm/providers/provider-chainstack.js.map +0 -1
- package/lib.esm/providers/provider-cloudflare.d.ts +0 -14
- package/lib.esm/providers/provider-cloudflare.d.ts.map +0 -1
- package/lib.esm/providers/provider-cloudflare.js +0 -22
- package/lib.esm/providers/provider-cloudflare.js.map +0 -1
- package/lib.esm/providers/provider-etherscan.d.ts +0 -147
- package/lib.esm/providers/provider-etherscan.d.ts.map +0 -1
- package/lib.esm/providers/provider-etherscan.js +0 -584
- package/lib.esm/providers/provider-etherscan.js.map +0 -1
- package/lib.esm/providers/provider-infura.d.ts +0 -101
- package/lib.esm/providers/provider-infura.d.ts.map +0 -1
- package/lib.esm/providers/provider-infura.js +0 -201
- package/lib.esm/providers/provider-infura.js.map +0 -1
- package/lib.esm/providers/provider-pocket.d.ts +0 -54
- package/lib.esm/providers/provider-pocket.d.ts.map +0 -1
- package/lib.esm/providers/provider-pocket.js +0 -105
- package/lib.esm/providers/provider-pocket.js.map +0 -1
- package/lib.esm/providers/provider-quicknode.d.ts +0 -59
- package/lib.esm/providers/provider-quicknode.d.ts.map +0 -1
- package/lib.esm/providers/provider-quicknode.js +0 -159
- package/lib.esm/providers/provider-quicknode.js.map +0 -1
- package/lib.esm/wallet/hdwallet.d.ts +0 -248
- package/lib.esm/wallet/hdwallet.d.ts.map +0 -1
- package/lib.esm/wallet/hdwallet.js +0 -498
- package/lib.esm/wallet/hdwallet.js.map +0 -1
- package/lib.esm/wallet/json-crowdsale.d.ts +0 -27
- package/lib.esm/wallet/json-crowdsale.d.ts.map +0 -1
- package/lib.esm/wallet/json-crowdsale.js +0 -55
- package/lib.esm/wallet/json-crowdsale.js.map +0 -1
- package/lib.esm/wallet/mnemonic.d.ts +0 -65
- package/lib.esm/wallet/mnemonic.d.ts.map +0 -1
- package/lib.esm/wallet/mnemonic.js +0 -165
- package/lib.esm/wallet/mnemonic.js.map +0 -1
- package/lib.esm/wallet/seedwallet.d.ts +0 -4
- package/lib.esm/wallet/seedwallet.d.ts.map +0 -1
- package/lib.esm/wallet/seedwallet.js +0 -4
- package/lib.esm/wallet/seedwallet.js.map +0 -1
|
@@ -1,801 +1,801 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A **FallbackProvider** provides resilience, security and performance
|
|
3
|
-
* in a way that is customizable and configurable.
|
|
4
|
-
*
|
|
5
|
-
* @_section: api/providers/fallback-provider:Fallback Provider [about-fallback-provider]
|
|
6
|
-
*/
|
|
7
|
-
import {
|
|
8
|
-
assert, assertArgument, getBigInt, getNumber, isError
|
|
9
|
-
} from "../utils/index.js";
|
|
10
|
-
|
|
11
|
-
import { AbstractProvider } from "./abstract-provider.js";
|
|
12
|
-
import { Network } from "./network.js"
|
|
13
|
-
|
|
14
|
-
import type { PerformActionRequest } from "./abstract-provider.js";
|
|
15
|
-
import type { Networkish } from "./network.js"
|
|
16
|
-
|
|
17
|
-
const BN_1 = BigInt("1");
|
|
18
|
-
const BN_2 = BigInt("2");
|
|
19
|
-
|
|
20
|
-
function shuffle<T = any>(array: Array<T>): void {
|
|
21
|
-
for (let i = array.length - 1; i > 0; i--) {
|
|
22
|
-
const j = Math.floor(Math.random() * (i + 1));
|
|
23
|
-
const tmp = array[i];
|
|
24
|
-
array[i] = array[j];
|
|
25
|
-
array[j] = tmp;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function stall(duration: number): Promise<void> {
|
|
30
|
-
return new Promise((resolve) => { setTimeout(resolve, duration); });
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function getTime(): number { return (new Date()).getTime(); }
|
|
34
|
-
|
|
35
|
-
function stringify(value: any): string {
|
|
36
|
-
return JSON.stringify(value, (key, value) => {
|
|
37
|
-
if (typeof(value) === "bigint") {
|
|
38
|
-
return { type: "bigint", value: value.toString() };
|
|
39
|
-
}
|
|
40
|
-
return value;
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* A configuration entry for how to use a [[Provider]].
|
|
46
|
-
*/
|
|
47
|
-
export interface FallbackProviderConfig {
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* The provider.
|
|
51
|
-
*/
|
|
52
|
-
provider: AbstractProvider;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* The amount of time to wait before kicking off the next provider.
|
|
56
|
-
*
|
|
57
|
-
* Any providers that have not responded can still respond and be
|
|
58
|
-
* counted, but this ensures new providers start.
|
|
59
|
-
*/
|
|
60
|
-
stallTimeout?: number;
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* The priority. Lower priority providers are dispatched first.
|
|
64
|
-
*/
|
|
65
|
-
priority?: number;
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* The amount of weight a provider is given against the quorum.
|
|
69
|
-
*/
|
|
70
|
-
weight?: number;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const defaultConfig = { stallTimeout: 400, priority: 1, weight: 1 };
|
|
74
|
-
|
|
75
|
-
// We track a bunch of extra stuff that might help debug problems or
|
|
76
|
-
// optimize infrastructure later on.
|
|
77
|
-
/**
|
|
78
|
-
* The statistics and state maintained for a [[Provider]].
|
|
79
|
-
*/
|
|
80
|
-
export interface FallbackProviderState extends Required<FallbackProviderConfig> {
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* The most recent blockNumber this provider has reported (-2 if none).
|
|
84
|
-
*/
|
|
85
|
-
blockNumber: number;
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* The number of total requests ever sent to this provider.
|
|
89
|
-
*/
|
|
90
|
-
requests: number;
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* The number of responses that errored.
|
|
94
|
-
*/
|
|
95
|
-
errorResponses: number;
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* The number of responses that occured after the result resolved.
|
|
99
|
-
*/
|
|
100
|
-
lateResponses: number;
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* How many times syncing was required to catch up the expected block.
|
|
104
|
-
*/
|
|
105
|
-
outOfSync: number;
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* The number of requests which reported unsupported operation.
|
|
109
|
-
*/
|
|
110
|
-
unsupportedEvents: number;
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* A rolling average (5% current duration) for response time.
|
|
114
|
-
*/
|
|
115
|
-
rollingDuration: number;
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* The ratio of quorum-agreed results to total.
|
|
119
|
-
*/
|
|
120
|
-
score: number;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
interface Config extends FallbackProviderState {
|
|
124
|
-
_updateNumber: null | Promise<any>;
|
|
125
|
-
_network: null | Network;
|
|
126
|
-
_totalTime: number;
|
|
127
|
-
_lastFatalError: null | Error;
|
|
128
|
-
_lastFatalErrorTimestamp: number;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const defaultState = {
|
|
132
|
-
blockNumber: -2, requests: 0, lateResponses: 0, errorResponses: 0,
|
|
133
|
-
outOfSync: -1, unsupportedEvents: 0, rollingDuration: 0, score: 0,
|
|
134
|
-
_network: null, _updateNumber: null, _totalTime: 0,
|
|
135
|
-
_lastFatalError: null, _lastFatalErrorTimestamp: 0
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
async function waitForSync(config: Config, blockNumber: number): Promise<void> {
|
|
140
|
-
while (config.blockNumber < 0 || config.blockNumber < blockNumber) {
|
|
141
|
-
if (!config._updateNumber) {
|
|
142
|
-
config._updateNumber = (async () => {
|
|
143
|
-
try {
|
|
144
|
-
const blockNumber = await config.provider.getBlockNumber();
|
|
145
|
-
if (blockNumber > config.blockNumber) {
|
|
146
|
-
config.blockNumber = blockNumber;
|
|
147
|
-
}
|
|
148
|
-
} catch (error: any) {
|
|
149
|
-
config.blockNumber = -2;
|
|
150
|
-
config._lastFatalError = error;
|
|
151
|
-
config._lastFatalErrorTimestamp = getTime();
|
|
152
|
-
}
|
|
153
|
-
config._updateNumber = null;
|
|
154
|
-
})();
|
|
155
|
-
}
|
|
156
|
-
await config._updateNumber;
|
|
157
|
-
config.outOfSync++;
|
|
158
|
-
if (config._lastFatalError) { break; }
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Additional options to configure a [[FallbackProvider]].
|
|
164
|
-
*/
|
|
165
|
-
export type FallbackProviderOptions = {
|
|
166
|
-
// How many providers must agree on a value before reporting
|
|
167
|
-
// back the response
|
|
168
|
-
quorum?: number;
|
|
169
|
-
|
|
170
|
-
// How many providers must have reported the same event
|
|
171
|
-
// for it to be emitted (currently unimplmented)
|
|
172
|
-
eventQuorum?: number;
|
|
173
|
-
|
|
174
|
-
// How many providers to dispatch each event to simultaneously.
|
|
175
|
-
// Set this to 0 to use getLog polling, which implies eventQuorum
|
|
176
|
-
// is equal to quorum. (currently unimplemented)
|
|
177
|
-
eventWorkers?: number;
|
|
178
|
-
|
|
179
|
-
cacheTimeout?: number;
|
|
180
|
-
|
|
181
|
-
pollingInterval?: number;
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
type RunnerResult = { result: any } | { error: Error };
|
|
185
|
-
|
|
186
|
-
type RunnerState = {
|
|
187
|
-
config: Config;
|
|
188
|
-
staller: null | Promise<void>;
|
|
189
|
-
didBump: boolean;
|
|
190
|
-
perform: null | Promise<any>;
|
|
191
|
-
result: null | RunnerResult;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function _normalize(value: any): string {
|
|
195
|
-
if (value == null) { return "null"; }
|
|
196
|
-
|
|
197
|
-
if (Array.isArray(value)) {
|
|
198
|
-
return "[" + (value.map(_normalize)).join(",") + "]";
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (typeof(value) === "object" && typeof(value.toJSON) === "function") {
|
|
202
|
-
return _normalize(value.toJSON());
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
switch (typeof(value)) {
|
|
206
|
-
case "boolean": case "symbol":
|
|
207
|
-
return value.toString();
|
|
208
|
-
case "bigint": case "number":
|
|
209
|
-
return BigInt(value).toString();
|
|
210
|
-
case "string":
|
|
211
|
-
return JSON.stringify(value);
|
|
212
|
-
case "object": {
|
|
213
|
-
const keys = Object.keys(value);
|
|
214
|
-
keys.sort();
|
|
215
|
-
return "{" + keys.map((k) => `${ JSON.stringify(k) }:${ _normalize(value[k]) }`).join(",") + "}";
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
console.log("Could not serialize", value);
|
|
220
|
-
throw new Error("Hmm...");
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function normalizeResult(method: string, value: RunnerResult): { tag: string, value: any } {
|
|
224
|
-
|
|
225
|
-
if ("error" in value) {
|
|
226
|
-
const error = value.error;
|
|
227
|
-
|
|
228
|
-
let tag: string;
|
|
229
|
-
if (isError(error, "CALL_EXCEPTION")) {
|
|
230
|
-
tag = _normalize(Object.assign({ }, error, {
|
|
231
|
-
shortMessage: undefined, reason: undefined, info: undefined
|
|
232
|
-
}));
|
|
233
|
-
} else {
|
|
234
|
-
tag = _normalize(error)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return { tag, value: error };
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const result = value.result;
|
|
241
|
-
return { tag: _normalize(result), value: result };
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
type TallyResult = {
|
|
245
|
-
tag: string;
|
|
246
|
-
value: any;
|
|
247
|
-
weight: number;
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
// This strategy picks the highest weight result, as long as the weight is
|
|
251
|
-
// equal to or greater than quorum
|
|
252
|
-
function checkQuorum(quorum: number, results: Array<TallyResult>): any | Error {
|
|
253
|
-
const tally: Map<string, { value: any, weight: number }> = new Map();
|
|
254
|
-
for (const { value, tag, weight } of results) {
|
|
255
|
-
const t = tally.get(tag) || { value, weight: 0 };
|
|
256
|
-
t.weight += weight;
|
|
257
|
-
tally.set(tag, t);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
let best: null | { value: any, weight: number } = null;
|
|
261
|
-
for (const r of tally.values()) {
|
|
262
|
-
if (r.weight >= quorum && (!best || r.weight > best.weight)) {
|
|
263
|
-
best = r;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (best) { return best.value; }
|
|
268
|
-
|
|
269
|
-
return undefined;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function getMedian(quorum: number, results: Array<TallyResult>): undefined | bigint | Error {
|
|
273
|
-
let resultWeight = 0;
|
|
274
|
-
|
|
275
|
-
const errorMap: Map<string, { weight: number, value: Error }> = new Map();
|
|
276
|
-
let bestError: null | { weight: number, value: Error } = null;
|
|
277
|
-
|
|
278
|
-
const values: Array<bigint> = [ ];
|
|
279
|
-
for (const { value, tag, weight } of results) {
|
|
280
|
-
if (value instanceof Error) {
|
|
281
|
-
const e = errorMap.get(tag) || { value, weight: 0 };
|
|
282
|
-
e.weight += weight;
|
|
283
|
-
errorMap.set(tag, e);
|
|
284
|
-
|
|
285
|
-
if (bestError == null || e.weight > bestError.weight) { bestError = e; }
|
|
286
|
-
} else {
|
|
287
|
-
values.push(BigInt(value));
|
|
288
|
-
resultWeight += weight;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (resultWeight < quorum) {
|
|
293
|
-
// We have quorum for an error
|
|
294
|
-
if (bestError && bestError.weight >= quorum) { return bestError.value; }
|
|
295
|
-
|
|
296
|
-
// We do not have quorum for a result
|
|
297
|
-
return undefined;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Get the sorted values
|
|
301
|
-
values.sort((a, b) => ((a < b) ? -1: (b > a) ? 1: 0));
|
|
302
|
-
|
|
303
|
-
const mid = Math.floor(values.length / 2);
|
|
304
|
-
|
|
305
|
-
// Odd-length; take the middle value
|
|
306
|
-
if (values.length % 2) { return values[mid]; }
|
|
307
|
-
|
|
308
|
-
// Even length; take the ceiling of the mean of the center two values
|
|
309
|
-
return (values[mid - 1] + values[mid] + BN_1) / BN_2;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function getAnyResult(quorum: number, results: Array<TallyResult>): undefined | any | Error {
|
|
313
|
-
// If any value or error meets quorum, that is our preferred result
|
|
314
|
-
const result = checkQuorum(quorum, results);
|
|
315
|
-
if (result !== undefined) { return result; }
|
|
316
|
-
|
|
317
|
-
// Otherwise, do we have any result?
|
|
318
|
-
for (const r of results) {
|
|
319
|
-
if (r.value) { return r.value; }
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Nope!
|
|
323
|
-
return undefined;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function getFuzzyMode(quorum: number, results: Array<TallyResult>): undefined | number {
|
|
327
|
-
if (quorum === 1) { return getNumber(<bigint>getMedian(quorum, results), "%internal"); }
|
|
328
|
-
|
|
329
|
-
const tally: Map<number, { result: number, weight: number }> = new Map();
|
|
330
|
-
const add = (result: number, weight: number) => {
|
|
331
|
-
const t = tally.get(result) || { result, weight: 0 };
|
|
332
|
-
t.weight += weight;
|
|
333
|
-
tally.set(result, t);
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
for (const { weight, value } of results) {
|
|
337
|
-
const r = getNumber(value);
|
|
338
|
-
add(r - 1, weight);
|
|
339
|
-
add(r, weight);
|
|
340
|
-
add(r + 1, weight);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
let bestWeight = 0;
|
|
344
|
-
let bestResult: undefined | number = undefined;
|
|
345
|
-
|
|
346
|
-
for (const { weight, result } of tally.values()) {
|
|
347
|
-
// Use this result, if this result meets quorum and has either:
|
|
348
|
-
// - a better weight
|
|
349
|
-
// - or equal weight, but the result is larger
|
|
350
|
-
if (weight >= quorum && (weight > bestWeight || (bestResult != null && weight === bestWeight && result > bestResult))) {
|
|
351
|
-
bestWeight = weight;
|
|
352
|
-
bestResult = result;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
return bestResult;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* A **FallbackProvider** manages several [[Providers]] providing
|
|
361
|
-
* resilience by switching between slow or misbehaving nodes, security
|
|
362
|
-
* by requiring multiple backends to aggree and performance by allowing
|
|
363
|
-
* faster backends to respond earlier.
|
|
364
|
-
*
|
|
365
|
-
*/
|
|
366
|
-
export class FallbackProvider extends AbstractProvider {
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* The number of backends that must agree on a value before it is
|
|
370
|
-
* accpeted.
|
|
371
|
-
*/
|
|
372
|
-
readonly quorum: number;
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* @_ignore:
|
|
376
|
-
*/
|
|
377
|
-
readonly eventQuorum: number;
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* @_ignore:
|
|
381
|
-
*/
|
|
382
|
-
readonly eventWorkers: number;
|
|
383
|
-
|
|
384
|
-
readonly #configs: Array<Config>;
|
|
385
|
-
|
|
386
|
-
#height: number;
|
|
387
|
-
#initialSyncPromise: null | Promise<void>;
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Creates a new **FallbackProvider** with %%providers%% connected to
|
|
391
|
-
* %%network%%.
|
|
392
|
-
*
|
|
393
|
-
* If a [[Provider]] is included in %%providers%%, defaults are used
|
|
394
|
-
* for the configuration.
|
|
395
|
-
*/
|
|
396
|
-
constructor(providers: Array<AbstractProvider | FallbackProviderConfig>, network?: Networkish, options?: FallbackProviderOptions) {
|
|
397
|
-
super(network, options);
|
|
398
|
-
|
|
399
|
-
this.#configs = providers.map((p) => {
|
|
400
|
-
if (p instanceof AbstractProvider) {
|
|
401
|
-
return Object.assign({ provider: p }, defaultConfig, defaultState );
|
|
402
|
-
} else {
|
|
403
|
-
return Object.assign({ }, defaultConfig, p, defaultState );
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
this.#height = -2;
|
|
408
|
-
this.#initialSyncPromise = null;
|
|
409
|
-
|
|
410
|
-
if (options && options.quorum != null) {
|
|
411
|
-
this.quorum = options.quorum;
|
|
412
|
-
} else {
|
|
413
|
-
this.quorum = Math.ceil(this.#configs.reduce((accum, config) => {
|
|
414
|
-
accum += config.weight;
|
|
415
|
-
return accum;
|
|
416
|
-
}, 0) / 2);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
this.eventQuorum = 1;
|
|
420
|
-
this.eventWorkers = 1;
|
|
421
|
-
|
|
422
|
-
assertArgument(this.quorum <= this.#configs.reduce((a, c) => (a + c.weight), 0),
|
|
423
|
-
"quorum exceed provider weight", "quorum", this.quorum);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
get providerConfigs(): Array<FallbackProviderState> {
|
|
427
|
-
return this.#configs.map((c) => {
|
|
428
|
-
const result: any = Object.assign({ }, c);
|
|
429
|
-
for (const key in result) {
|
|
430
|
-
if (key[0] === "_") { delete result[key]; }
|
|
431
|
-
}
|
|
432
|
-
return result;
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
async _detectNetwork(): Promise<Network> {
|
|
437
|
-
return Network.from(getBigInt(await this._perform({ method: "chainId" })));
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// @TODO: Add support to select providers to be the event subscriber
|
|
441
|
-
//_getSubscriber(sub: Subscription): Subscriber {
|
|
442
|
-
// throw new Error("@TODO");
|
|
443
|
-
//}
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* Transforms a %%req%% into the correct method call on %%provider%%.
|
|
447
|
-
*/
|
|
448
|
-
async _translatePerform(provider: AbstractProvider, req: PerformActionRequest): Promise<any> {
|
|
449
|
-
switch (req.method) {
|
|
450
|
-
case "broadcastTransaction":
|
|
451
|
-
return await provider.broadcastTransaction(req.signedTransaction);
|
|
452
|
-
case "call":
|
|
453
|
-
return await provider.call(Object.assign({ }, req.transaction, { blockTag: req.blockTag }));
|
|
454
|
-
case "chainId":
|
|
455
|
-
return (await provider.getNetwork()).chainId;
|
|
456
|
-
case "estimateGas":
|
|
457
|
-
return await provider.estimateGas(req.transaction);
|
|
458
|
-
case "getBalance":
|
|
459
|
-
return await provider.getBalance(req.address, req.blockTag);
|
|
460
|
-
case "getBlock": {
|
|
461
|
-
const block = ("blockHash" in req) ? req.blockHash: req.blockTag;
|
|
462
|
-
return await provider.getBlock(block, req.includeTransactions);
|
|
463
|
-
}
|
|
464
|
-
case "getBlockNumber":
|
|
465
|
-
return await provider.getBlockNumber();
|
|
466
|
-
case "getCode":
|
|
467
|
-
return await provider.getCode(req.address, req.blockTag);
|
|
468
|
-
case "getGasPrice":
|
|
469
|
-
return (await provider.getFeeData()).gasPrice;
|
|
470
|
-
case "getPriorityFee":
|
|
471
|
-
return (await provider.getFeeData()).maxPriorityFeePerGas;
|
|
472
|
-
case "getLogs":
|
|
473
|
-
return await provider.getLogs(req.filter);
|
|
474
|
-
case "getStorage":
|
|
475
|
-
return await provider.getStorage(req.address, req.position, req.blockTag);
|
|
476
|
-
case "getTransaction":
|
|
477
|
-
return await provider.getTransaction(req.hash);
|
|
478
|
-
case "getTransactionCount":
|
|
479
|
-
return await provider.getTransactionCount(req.address, req.blockTag);
|
|
480
|
-
case "getTransactionReceipt":
|
|
481
|
-
return await provider.getTransactionReceipt(req.hash);
|
|
482
|
-
case "getTransactionResult":
|
|
483
|
-
return await provider.getTransactionResult(req.hash);
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// Grab the next (random) config that is not already part of
|
|
488
|
-
// the running set
|
|
489
|
-
#getNextConfig(running: Set<RunnerState>): null | Config {
|
|
490
|
-
// @TODO: Maybe do a check here to favour (heavily) providers that
|
|
491
|
-
// do not require waitForSync and disfavour providers that
|
|
492
|
-
// seem down-ish or are behaving slowly
|
|
493
|
-
|
|
494
|
-
const configs = Array.from(running).map((r) => r.config)
|
|
495
|
-
|
|
496
|
-
// Shuffle the states, sorted by priority
|
|
497
|
-
const allConfigs = this.#configs.slice();
|
|
498
|
-
shuffle(allConfigs);
|
|
499
|
-
allConfigs.sort((a, b) => (a.priority - b.priority));
|
|
500
|
-
|
|
501
|
-
for (const config of allConfigs) {
|
|
502
|
-
if (config._lastFatalError) { continue; }
|
|
503
|
-
if (configs.indexOf(config) === -1) { return config; }
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
return null;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// Adds a new runner (if available) to running.
|
|
510
|
-
#addRunner(running: Set<RunnerState>, req: PerformActionRequest): null | RunnerState {
|
|
511
|
-
const config = this.#getNextConfig(running);
|
|
512
|
-
|
|
513
|
-
// No runners available
|
|
514
|
-
if (config == null) { return null; }
|
|
515
|
-
|
|
516
|
-
// Create a new runner
|
|
517
|
-
const runner: RunnerState = {
|
|
518
|
-
config, result: null, didBump: false,
|
|
519
|
-
perform: null, staller: null
|
|
520
|
-
};
|
|
521
|
-
|
|
522
|
-
const now = getTime();
|
|
523
|
-
|
|
524
|
-
// Start performing this operation
|
|
525
|
-
runner.perform = (async () => {
|
|
526
|
-
try {
|
|
527
|
-
config.requests++;
|
|
528
|
-
const result = await this._translatePerform(config.provider, req);
|
|
529
|
-
runner.result = { result };
|
|
530
|
-
} catch (error: any) {
|
|
531
|
-
config.errorResponses++;
|
|
532
|
-
runner.result = { error };
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
const dt = (getTime() - now);
|
|
536
|
-
config._totalTime += dt;
|
|
537
|
-
|
|
538
|
-
config.rollingDuration = 0.95 * config.rollingDuration + 0.05 * dt;
|
|
539
|
-
|
|
540
|
-
runner.perform = null;
|
|
541
|
-
})();
|
|
542
|
-
|
|
543
|
-
// Start a staller; when this times out, it's time to force
|
|
544
|
-
// kicking off another runner because we are taking too long
|
|
545
|
-
runner.staller = (async () => {
|
|
546
|
-
await stall(config.stallTimeout);
|
|
547
|
-
runner.staller = null;
|
|
548
|
-
})();
|
|
549
|
-
|
|
550
|
-
running.add(runner);
|
|
551
|
-
return runner;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// Initializes the blockNumber and network for each runner and
|
|
555
|
-
// blocks until initialized
|
|
556
|
-
async #initialSync(): Promise<void> {
|
|
557
|
-
let initialSync = this.#initialSyncPromise;
|
|
558
|
-
if (!initialSync) {
|
|
559
|
-
const promises: Array<Promise<any>> = [ ];
|
|
560
|
-
this.#configs.forEach((config) => {
|
|
561
|
-
promises.push((async () => {
|
|
562
|
-
await waitForSync(config, 0);
|
|
563
|
-
if (!config._lastFatalError) {
|
|
564
|
-
config._network = await config.provider.getNetwork();
|
|
565
|
-
}
|
|
566
|
-
})());
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
this.#initialSyncPromise = initialSync = (async () => {
|
|
570
|
-
// Wait for all providers to have a block number and network
|
|
571
|
-
await Promise.all(promises);
|
|
572
|
-
|
|
573
|
-
// Check all the networks match
|
|
574
|
-
let chainId: null | bigint = null;
|
|
575
|
-
for (const config of this.#configs) {
|
|
576
|
-
if (config._lastFatalError) { continue; }
|
|
577
|
-
const network = <Network>(config._network);
|
|
578
|
-
if (chainId == null) {
|
|
579
|
-
chainId = network.chainId;
|
|
580
|
-
} else if (network.chainId !== chainId) {
|
|
581
|
-
assert(false, "cannot mix providers on different networks", "UNSUPPORTED_OPERATION", {
|
|
582
|
-
operation: "new FallbackProvider"
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
})();
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
await initialSync
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
async #checkQuorum(running: Set<RunnerState>, req: PerformActionRequest): Promise<any> {
|
|
594
|
-
// Get all the result objects
|
|
595
|
-
const results: Array<TallyResult> = [ ];
|
|
596
|
-
for (const runner of running) {
|
|
597
|
-
if (runner.result != null) {
|
|
598
|
-
const { tag, value } = normalizeResult(req.method, runner.result);
|
|
599
|
-
results.push({ tag, value, weight: runner.config.weight });
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// Are there enough results to event meet quorum?
|
|
604
|
-
if (results.reduce((a, r) => (a + r.weight), 0) < this.quorum) {
|
|
605
|
-
return undefined;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
switch (req.method) {
|
|
609
|
-
case "getBlockNumber": {
|
|
610
|
-
// We need to get the bootstrap block height
|
|
611
|
-
if (this.#height === -2) {
|
|
612
|
-
this.#height = Math.ceil(getNumber(<bigint>getMedian(this.quorum, this.#configs.filter((c) => (!c._lastFatalError)).map((c) => ({
|
|
613
|
-
value: c.blockNumber,
|
|
614
|
-
tag: getNumber(c.blockNumber).toString(),
|
|
615
|
-
weight: c.weight
|
|
616
|
-
})))));
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
// Find the mode across all the providers, allowing for
|
|
620
|
-
// a little drift between block heights
|
|
621
|
-
const mode = getFuzzyMode(this.quorum, results);
|
|
622
|
-
if (mode === undefined) { return undefined; }
|
|
623
|
-
if (mode > this.#height) { this.#height = mode; }
|
|
624
|
-
return this.#height;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
case "getGasPrice":
|
|
628
|
-
case "getPriorityFee":
|
|
629
|
-
case "estimateGas":
|
|
630
|
-
return getMedian(this.quorum, results);
|
|
631
|
-
|
|
632
|
-
case "getBlock":
|
|
633
|
-
// Pending blocks are in the mempool and already
|
|
634
|
-
// quite untrustworthy; just grab anything
|
|
635
|
-
if ("blockTag" in req && req.blockTag === "pending") {
|
|
636
|
-
return getAnyResult(this.quorum, results);
|
|
637
|
-
}
|
|
638
|
-
return checkQuorum(this.quorum, results);
|
|
639
|
-
|
|
640
|
-
case "call":
|
|
641
|
-
case "chainId":
|
|
642
|
-
case "getBalance":
|
|
643
|
-
case "getTransactionCount":
|
|
644
|
-
case "getCode":
|
|
645
|
-
case "getStorage":
|
|
646
|
-
case "getTransaction":
|
|
647
|
-
case "getTransactionReceipt":
|
|
648
|
-
case "getLogs":
|
|
649
|
-
return checkQuorum(this.quorum, results);
|
|
650
|
-
|
|
651
|
-
case "broadcastTransaction":
|
|
652
|
-
return getAnyResult(this.quorum, results);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
assert(false, "unsupported method", "UNSUPPORTED_OPERATION", {
|
|
656
|
-
operation: `_perform(${ stringify((<any>req).method) })`
|
|
657
|
-
});
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
async #waitForQuorum(running: Set<RunnerState>, req: PerformActionRequest): Promise<any> {
|
|
661
|
-
if (running.size === 0) { throw new Error("no runners?!"); }
|
|
662
|
-
|
|
663
|
-
// Any promises that are interesting to watch for; an expired stall
|
|
664
|
-
// or a successful perform
|
|
665
|
-
const interesting: Array<Promise<void>> = [ ];
|
|
666
|
-
|
|
667
|
-
let newRunners = 0;
|
|
668
|
-
for (const runner of running) {
|
|
669
|
-
|
|
670
|
-
// No responses, yet; keep an eye on it
|
|
671
|
-
if (runner.perform) {
|
|
672
|
-
interesting.push(runner.perform);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// Still stalling...
|
|
676
|
-
if (runner.staller) {
|
|
677
|
-
interesting.push(runner.staller);
|
|
678
|
-
continue;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// This runner has already triggered another runner
|
|
682
|
-
if (runner.didBump) { continue; }
|
|
683
|
-
|
|
684
|
-
// Got a response (result or error) or stalled; kick off another runner
|
|
685
|
-
runner.didBump = true;
|
|
686
|
-
newRunners++;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// Check if we have reached quorum on a result (or error)
|
|
690
|
-
const value = await this.#checkQuorum(running, req);
|
|
691
|
-
if (value !== undefined) {
|
|
692
|
-
if (value instanceof Error) { throw value; }
|
|
693
|
-
return value;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
// Add any new runners, because a staller timed out or a result
|
|
697
|
-
// or error response came in.
|
|
698
|
-
for (let i = 0; i < newRunners; i++) {
|
|
699
|
-
this.#addRunner(running, req);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// All providers have returned, and we have no result
|
|
703
|
-
|
|
704
|
-
assert(interesting.length > 0, "quorum not met", "SERVER_ERROR", {
|
|
705
|
-
request: "%sub-requests",
|
|
706
|
-
info: { request: req, results: Array.from(running).map((r) => stringify(r.result)) }
|
|
707
|
-
});
|
|
708
|
-
|
|
709
|
-
// Wait for someone to either complete its perform or stall out
|
|
710
|
-
await Promise.race(interesting);
|
|
711
|
-
|
|
712
|
-
// This is recursive, but at worst case the depth is 2x the
|
|
713
|
-
// number of providers (each has a perform and a staller)
|
|
714
|
-
return await this.#waitForQuorum(running, req);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
async _perform<T = any>(req: PerformActionRequest): Promise<T> {
|
|
718
|
-
// Broadcasting a transaction is rare (ish) and already incurs
|
|
719
|
-
// a cost on the user, so spamming is safe-ish. Just send it to
|
|
720
|
-
// every backend.
|
|
721
|
-
if (req.method === "broadcastTransaction") {
|
|
722
|
-
// Once any broadcast provides a positive result, use it. No
|
|
723
|
-
// need to wait for anyone else
|
|
724
|
-
const results: Array<null | TallyResult> = this.#configs.map((c) => null);
|
|
725
|
-
const broadcasts = this.#configs.map(async ({ provider, weight }, index) => {
|
|
726
|
-
try {
|
|
727
|
-
const result = await provider._perform(req);
|
|
728
|
-
results[index] = Object.assign(normalizeResult(req.method, { result }), { weight });
|
|
729
|
-
} catch (error: any) {
|
|
730
|
-
results[index] = Object.assign(normalizeResult(req.method, { error }), { weight });
|
|
731
|
-
}
|
|
732
|
-
});
|
|
733
|
-
|
|
734
|
-
// As each promise finishes...
|
|
735
|
-
while (true) {
|
|
736
|
-
// Check for a valid broadcast result
|
|
737
|
-
const done = <Array<any>>results.filter((r) => (r != null));
|
|
738
|
-
for (const { value } of done) {
|
|
739
|
-
if (!(value instanceof Error)) { return value; }
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
// Check for a legit broadcast error (one which we cannot
|
|
743
|
-
// recover from; some nodes may return the following red
|
|
744
|
-
// herring events:
|
|
745
|
-
// - alredy seend (UNKNOWN_ERROR)
|
|
746
|
-
// - NONCE_EXPIRED
|
|
747
|
-
// - REPLACEMENT_UNDERPRICED
|
|
748
|
-
const result = checkQuorum(this.quorum, <Array<any>>results.filter((r) => (r != null)));
|
|
749
|
-
if (isError(result, "INSUFFICIENT_FUNDS")) {
|
|
750
|
-
throw result;
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
// Kick off the next provider (if any)
|
|
754
|
-
const waiting = broadcasts.filter((b, i) => (results[i] == null));
|
|
755
|
-
if (waiting.length === 0) { break; }
|
|
756
|
-
await Promise.race(waiting);
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
// Use standard quorum results; any result was returned above,
|
|
760
|
-
// so this will find any error that met quorum if any
|
|
761
|
-
const result = getAnyResult(this.quorum, <Array<any>>results);
|
|
762
|
-
assert(result !== undefined, "problem multi-broadcasting", "SERVER_ERROR", {
|
|
763
|
-
request: "%sub-requests",
|
|
764
|
-
info: { request: req, results: results.map(stringify) }
|
|
765
|
-
})
|
|
766
|
-
if (result instanceof Error) { throw result; }
|
|
767
|
-
return result;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
await this.#initialSync();
|
|
771
|
-
|
|
772
|
-
// Bootstrap enough runners to meet quorum
|
|
773
|
-
const running: Set<RunnerState> = new Set();
|
|
774
|
-
let inflightQuorum = 0;
|
|
775
|
-
while (true) {
|
|
776
|
-
const runner = this.#addRunner(running, req);
|
|
777
|
-
if (runner == null) { break; }
|
|
778
|
-
inflightQuorum += runner.config.weight;
|
|
779
|
-
if (inflightQuorum >= this.quorum) { break; }
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
const result = await this.#waitForQuorum(running, req);
|
|
783
|
-
|
|
784
|
-
// Track requests sent to a provider that are still
|
|
785
|
-
// outstanding after quorum has been otherwise found
|
|
786
|
-
for (const runner of running) {
|
|
787
|
-
if (runner.perform && runner.result == null) {
|
|
788
|
-
runner.config.lateResponses++;
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
return result;
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
async destroy(): Promise<void> {
|
|
796
|
-
for (const { provider } of this.#configs) {
|
|
797
|
-
provider.destroy();
|
|
798
|
-
}
|
|
799
|
-
super.destroy();
|
|
800
|
-
}
|
|
801
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* A **FallbackProvider** provides resilience, security and performance
|
|
3
|
+
* in a way that is customizable and configurable.
|
|
4
|
+
*
|
|
5
|
+
* @_section: api/providers/fallback-provider:Fallback Provider [about-fallback-provider]
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
assert, assertArgument, getBigInt, getNumber, isError
|
|
9
|
+
} from "../utils/index.js";
|
|
10
|
+
|
|
11
|
+
import { AbstractProvider } from "./abstract-provider.js";
|
|
12
|
+
import { Network } from "./network.js"
|
|
13
|
+
|
|
14
|
+
import type { PerformActionRequest } from "./abstract-provider.js";
|
|
15
|
+
import type { Networkish } from "./network.js"
|
|
16
|
+
|
|
17
|
+
const BN_1 = BigInt("1");
|
|
18
|
+
const BN_2 = BigInt("2");
|
|
19
|
+
|
|
20
|
+
function shuffle<T = any>(array: Array<T>): void {
|
|
21
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
22
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
23
|
+
const tmp = array[i];
|
|
24
|
+
array[i] = array[j];
|
|
25
|
+
array[j] = tmp;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function stall(duration: number): Promise<void> {
|
|
30
|
+
return new Promise((resolve) => { setTimeout(resolve, duration); });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getTime(): number { return (new Date()).getTime(); }
|
|
34
|
+
|
|
35
|
+
function stringify(value: any): string {
|
|
36
|
+
return JSON.stringify(value, (key, value) => {
|
|
37
|
+
if (typeof(value) === "bigint") {
|
|
38
|
+
return { type: "bigint", value: value.toString() };
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A configuration entry for how to use a [[Provider]].
|
|
46
|
+
*/
|
|
47
|
+
export interface FallbackProviderConfig {
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The provider.
|
|
51
|
+
*/
|
|
52
|
+
provider: AbstractProvider;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The amount of time to wait before kicking off the next provider.
|
|
56
|
+
*
|
|
57
|
+
* Any providers that have not responded can still respond and be
|
|
58
|
+
* counted, but this ensures new providers start.
|
|
59
|
+
*/
|
|
60
|
+
stallTimeout?: number;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The priority. Lower priority providers are dispatched first.
|
|
64
|
+
*/
|
|
65
|
+
priority?: number;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The amount of weight a provider is given against the quorum.
|
|
69
|
+
*/
|
|
70
|
+
weight?: number;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const defaultConfig = { stallTimeout: 400, priority: 1, weight: 1 };
|
|
74
|
+
|
|
75
|
+
// We track a bunch of extra stuff that might help debug problems or
|
|
76
|
+
// optimize infrastructure later on.
|
|
77
|
+
/**
|
|
78
|
+
* The statistics and state maintained for a [[Provider]].
|
|
79
|
+
*/
|
|
80
|
+
export interface FallbackProviderState extends Required<FallbackProviderConfig> {
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The most recent blockNumber this provider has reported (-2 if none).
|
|
84
|
+
*/
|
|
85
|
+
blockNumber: number;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The number of total requests ever sent to this provider.
|
|
89
|
+
*/
|
|
90
|
+
requests: number;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The number of responses that errored.
|
|
94
|
+
*/
|
|
95
|
+
errorResponses: number;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The number of responses that occured after the result resolved.
|
|
99
|
+
*/
|
|
100
|
+
lateResponses: number;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* How many times syncing was required to catch up the expected block.
|
|
104
|
+
*/
|
|
105
|
+
outOfSync: number;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The number of requests which reported unsupported operation.
|
|
109
|
+
*/
|
|
110
|
+
unsupportedEvents: number;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A rolling average (5% current duration) for response time.
|
|
114
|
+
*/
|
|
115
|
+
rollingDuration: number;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The ratio of quorum-agreed results to total.
|
|
119
|
+
*/
|
|
120
|
+
score: number;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface Config extends FallbackProviderState {
|
|
124
|
+
_updateNumber: null | Promise<any>;
|
|
125
|
+
_network: null | Network;
|
|
126
|
+
_totalTime: number;
|
|
127
|
+
_lastFatalError: null | Error;
|
|
128
|
+
_lastFatalErrorTimestamp: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const defaultState = {
|
|
132
|
+
blockNumber: -2, requests: 0, lateResponses: 0, errorResponses: 0,
|
|
133
|
+
outOfSync: -1, unsupportedEvents: 0, rollingDuration: 0, score: 0,
|
|
134
|
+
_network: null, _updateNumber: null, _totalTime: 0,
|
|
135
|
+
_lastFatalError: null, _lastFatalErrorTimestamp: 0
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async function waitForSync(config: Config, blockNumber: number): Promise<void> {
|
|
140
|
+
while (config.blockNumber < 0 || config.blockNumber < blockNumber) {
|
|
141
|
+
if (!config._updateNumber) {
|
|
142
|
+
config._updateNumber = (async () => {
|
|
143
|
+
try {
|
|
144
|
+
const blockNumber = await config.provider.getBlockNumber();
|
|
145
|
+
if (blockNumber > config.blockNumber) {
|
|
146
|
+
config.blockNumber = blockNumber;
|
|
147
|
+
}
|
|
148
|
+
} catch (error: any) {
|
|
149
|
+
config.blockNumber = -2;
|
|
150
|
+
config._lastFatalError = error;
|
|
151
|
+
config._lastFatalErrorTimestamp = getTime();
|
|
152
|
+
}
|
|
153
|
+
config._updateNumber = null;
|
|
154
|
+
})();
|
|
155
|
+
}
|
|
156
|
+
await config._updateNumber;
|
|
157
|
+
config.outOfSync++;
|
|
158
|
+
if (config._lastFatalError) { break; }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Additional options to configure a [[FallbackProvider]].
|
|
164
|
+
*/
|
|
165
|
+
export type FallbackProviderOptions = {
|
|
166
|
+
// How many providers must agree on a value before reporting
|
|
167
|
+
// back the response
|
|
168
|
+
quorum?: number;
|
|
169
|
+
|
|
170
|
+
// How many providers must have reported the same event
|
|
171
|
+
// for it to be emitted (currently unimplmented)
|
|
172
|
+
eventQuorum?: number;
|
|
173
|
+
|
|
174
|
+
// How many providers to dispatch each event to simultaneously.
|
|
175
|
+
// Set this to 0 to use getLog polling, which implies eventQuorum
|
|
176
|
+
// is equal to quorum. (currently unimplemented)
|
|
177
|
+
eventWorkers?: number;
|
|
178
|
+
|
|
179
|
+
cacheTimeout?: number;
|
|
180
|
+
|
|
181
|
+
pollingInterval?: number;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
type RunnerResult = { result: any } | { error: Error };
|
|
185
|
+
|
|
186
|
+
type RunnerState = {
|
|
187
|
+
config: Config;
|
|
188
|
+
staller: null | Promise<void>;
|
|
189
|
+
didBump: boolean;
|
|
190
|
+
perform: null | Promise<any>;
|
|
191
|
+
result: null | RunnerResult;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function _normalize(value: any): string {
|
|
195
|
+
if (value == null) { return "null"; }
|
|
196
|
+
|
|
197
|
+
if (Array.isArray(value)) {
|
|
198
|
+
return "[" + (value.map(_normalize)).join(",") + "]";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (typeof(value) === "object" && typeof(value.toJSON) === "function") {
|
|
202
|
+
return _normalize(value.toJSON());
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
switch (typeof(value)) {
|
|
206
|
+
case "boolean": case "symbol":
|
|
207
|
+
return value.toString();
|
|
208
|
+
case "bigint": case "number":
|
|
209
|
+
return BigInt(value).toString();
|
|
210
|
+
case "string":
|
|
211
|
+
return JSON.stringify(value);
|
|
212
|
+
case "object": {
|
|
213
|
+
const keys = Object.keys(value);
|
|
214
|
+
keys.sort();
|
|
215
|
+
return "{" + keys.map((k) => `${ JSON.stringify(k) }:${ _normalize(value[k]) }`).join(",") + "}";
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
console.log("Could not serialize", value);
|
|
220
|
+
throw new Error("Hmm...");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function normalizeResult(method: string, value: RunnerResult): { tag: string, value: any } {
|
|
224
|
+
|
|
225
|
+
if ("error" in value) {
|
|
226
|
+
const error = value.error;
|
|
227
|
+
|
|
228
|
+
let tag: string;
|
|
229
|
+
if (isError(error, "CALL_EXCEPTION")) {
|
|
230
|
+
tag = _normalize(Object.assign({ }, error, {
|
|
231
|
+
shortMessage: undefined, reason: undefined, info: undefined
|
|
232
|
+
}));
|
|
233
|
+
} else {
|
|
234
|
+
tag = _normalize(error)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return { tag, value: error };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const result = value.result;
|
|
241
|
+
return { tag: _normalize(result), value: result };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
type TallyResult = {
|
|
245
|
+
tag: string;
|
|
246
|
+
value: any;
|
|
247
|
+
weight: number;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// This strategy picks the highest weight result, as long as the weight is
|
|
251
|
+
// equal to or greater than quorum
|
|
252
|
+
function checkQuorum(quorum: number, results: Array<TallyResult>): any | Error {
|
|
253
|
+
const tally: Map<string, { value: any, weight: number }> = new Map();
|
|
254
|
+
for (const { value, tag, weight } of results) {
|
|
255
|
+
const t = tally.get(tag) || { value, weight: 0 };
|
|
256
|
+
t.weight += weight;
|
|
257
|
+
tally.set(tag, t);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let best: null | { value: any, weight: number } = null;
|
|
261
|
+
for (const r of tally.values()) {
|
|
262
|
+
if (r.weight >= quorum && (!best || r.weight > best.weight)) {
|
|
263
|
+
best = r;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (best) { return best.value; }
|
|
268
|
+
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function getMedian(quorum: number, results: Array<TallyResult>): undefined | bigint | Error {
|
|
273
|
+
let resultWeight = 0;
|
|
274
|
+
|
|
275
|
+
const errorMap: Map<string, { weight: number, value: Error }> = new Map();
|
|
276
|
+
let bestError: null | { weight: number, value: Error } = null;
|
|
277
|
+
|
|
278
|
+
const values: Array<bigint> = [ ];
|
|
279
|
+
for (const { value, tag, weight } of results) {
|
|
280
|
+
if (value instanceof Error) {
|
|
281
|
+
const e = errorMap.get(tag) || { value, weight: 0 };
|
|
282
|
+
e.weight += weight;
|
|
283
|
+
errorMap.set(tag, e);
|
|
284
|
+
|
|
285
|
+
if (bestError == null || e.weight > bestError.weight) { bestError = e; }
|
|
286
|
+
} else {
|
|
287
|
+
values.push(BigInt(value));
|
|
288
|
+
resultWeight += weight;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (resultWeight < quorum) {
|
|
293
|
+
// We have quorum for an error
|
|
294
|
+
if (bestError && bestError.weight >= quorum) { return bestError.value; }
|
|
295
|
+
|
|
296
|
+
// We do not have quorum for a result
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Get the sorted values
|
|
301
|
+
values.sort((a, b) => ((a < b) ? -1: (b > a) ? 1: 0));
|
|
302
|
+
|
|
303
|
+
const mid = Math.floor(values.length / 2);
|
|
304
|
+
|
|
305
|
+
// Odd-length; take the middle value
|
|
306
|
+
if (values.length % 2) { return values[mid]; }
|
|
307
|
+
|
|
308
|
+
// Even length; take the ceiling of the mean of the center two values
|
|
309
|
+
return (values[mid - 1] + values[mid] + BN_1) / BN_2;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function getAnyResult(quorum: number, results: Array<TallyResult>): undefined | any | Error {
|
|
313
|
+
// If any value or error meets quorum, that is our preferred result
|
|
314
|
+
const result = checkQuorum(quorum, results);
|
|
315
|
+
if (result !== undefined) { return result; }
|
|
316
|
+
|
|
317
|
+
// Otherwise, do we have any result?
|
|
318
|
+
for (const r of results) {
|
|
319
|
+
if (r.value) { return r.value; }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Nope!
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function getFuzzyMode(quorum: number, results: Array<TallyResult>): undefined | number {
|
|
327
|
+
if (quorum === 1) { return getNumber(<bigint>getMedian(quorum, results), "%internal"); }
|
|
328
|
+
|
|
329
|
+
const tally: Map<number, { result: number, weight: number }> = new Map();
|
|
330
|
+
const add = (result: number, weight: number) => {
|
|
331
|
+
const t = tally.get(result) || { result, weight: 0 };
|
|
332
|
+
t.weight += weight;
|
|
333
|
+
tally.set(result, t);
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
for (const { weight, value } of results) {
|
|
337
|
+
const r = getNumber(value);
|
|
338
|
+
add(r - 1, weight);
|
|
339
|
+
add(r, weight);
|
|
340
|
+
add(r + 1, weight);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let bestWeight = 0;
|
|
344
|
+
let bestResult: undefined | number = undefined;
|
|
345
|
+
|
|
346
|
+
for (const { weight, result } of tally.values()) {
|
|
347
|
+
// Use this result, if this result meets quorum and has either:
|
|
348
|
+
// - a better weight
|
|
349
|
+
// - or equal weight, but the result is larger
|
|
350
|
+
if (weight >= quorum && (weight > bestWeight || (bestResult != null && weight === bestWeight && result > bestResult))) {
|
|
351
|
+
bestWeight = weight;
|
|
352
|
+
bestResult = result;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return bestResult;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* A **FallbackProvider** manages several [[Providers]] providing
|
|
361
|
+
* resilience by switching between slow or misbehaving nodes, security
|
|
362
|
+
* by requiring multiple backends to aggree and performance by allowing
|
|
363
|
+
* faster backends to respond earlier.
|
|
364
|
+
*
|
|
365
|
+
*/
|
|
366
|
+
export class FallbackProvider extends AbstractProvider {
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* The number of backends that must agree on a value before it is
|
|
370
|
+
* accpeted.
|
|
371
|
+
*/
|
|
372
|
+
readonly quorum: number;
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* @_ignore:
|
|
376
|
+
*/
|
|
377
|
+
readonly eventQuorum: number;
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* @_ignore:
|
|
381
|
+
*/
|
|
382
|
+
readonly eventWorkers: number;
|
|
383
|
+
|
|
384
|
+
readonly #configs: Array<Config>;
|
|
385
|
+
|
|
386
|
+
#height: number;
|
|
387
|
+
#initialSyncPromise: null | Promise<void>;
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Creates a new **FallbackProvider** with %%providers%% connected to
|
|
391
|
+
* %%network%%.
|
|
392
|
+
*
|
|
393
|
+
* If a [[Provider]] is included in %%providers%%, defaults are used
|
|
394
|
+
* for the configuration.
|
|
395
|
+
*/
|
|
396
|
+
constructor(providers: Array<AbstractProvider | FallbackProviderConfig>, network?: Networkish, options?: FallbackProviderOptions) {
|
|
397
|
+
super(network, options);
|
|
398
|
+
|
|
399
|
+
this.#configs = providers.map((p) => {
|
|
400
|
+
if (p instanceof AbstractProvider) {
|
|
401
|
+
return Object.assign({ provider: p }, defaultConfig, defaultState );
|
|
402
|
+
} else {
|
|
403
|
+
return Object.assign({ }, defaultConfig, p, defaultState );
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
this.#height = -2;
|
|
408
|
+
this.#initialSyncPromise = null;
|
|
409
|
+
|
|
410
|
+
if (options && options.quorum != null) {
|
|
411
|
+
this.quorum = options.quorum;
|
|
412
|
+
} else {
|
|
413
|
+
this.quorum = Math.ceil(this.#configs.reduce((accum, config) => {
|
|
414
|
+
accum += config.weight;
|
|
415
|
+
return accum;
|
|
416
|
+
}, 0) / 2);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
this.eventQuorum = 1;
|
|
420
|
+
this.eventWorkers = 1;
|
|
421
|
+
|
|
422
|
+
assertArgument(this.quorum <= this.#configs.reduce((a, c) => (a + c.weight), 0),
|
|
423
|
+
"quorum exceed provider weight", "quorum", this.quorum);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
get providerConfigs(): Array<FallbackProviderState> {
|
|
427
|
+
return this.#configs.map((c) => {
|
|
428
|
+
const result: any = Object.assign({ }, c);
|
|
429
|
+
for (const key in result) {
|
|
430
|
+
if (key[0] === "_") { delete result[key]; }
|
|
431
|
+
}
|
|
432
|
+
return result;
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async _detectNetwork(): Promise<Network> {
|
|
437
|
+
return Network.from(getBigInt(await this._perform({ method: "chainId" })));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// @TODO: Add support to select providers to be the event subscriber
|
|
441
|
+
//_getSubscriber(sub: Subscription): Subscriber {
|
|
442
|
+
// throw new Error("@TODO");
|
|
443
|
+
//}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Transforms a %%req%% into the correct method call on %%provider%%.
|
|
447
|
+
*/
|
|
448
|
+
async _translatePerform(provider: AbstractProvider, req: PerformActionRequest): Promise<any> {
|
|
449
|
+
switch (req.method) {
|
|
450
|
+
case "broadcastTransaction":
|
|
451
|
+
return await provider.broadcastTransaction(req.signedTransaction);
|
|
452
|
+
case "call":
|
|
453
|
+
return await provider.call(Object.assign({ }, req.transaction, { blockTag: req.blockTag }));
|
|
454
|
+
case "chainId":
|
|
455
|
+
return (await provider.getNetwork()).chainId;
|
|
456
|
+
case "estimateGas":
|
|
457
|
+
return await provider.estimateGas(req.transaction);
|
|
458
|
+
case "getBalance":
|
|
459
|
+
return await provider.getBalance(req.address, req.blockTag);
|
|
460
|
+
case "getBlock": {
|
|
461
|
+
const block = ("blockHash" in req) ? req.blockHash: req.blockTag;
|
|
462
|
+
return await provider.getBlock(block, req.includeTransactions);
|
|
463
|
+
}
|
|
464
|
+
case "getBlockNumber":
|
|
465
|
+
return await provider.getBlockNumber();
|
|
466
|
+
case "getCode":
|
|
467
|
+
return await provider.getCode(req.address, req.blockTag);
|
|
468
|
+
case "getGasPrice":
|
|
469
|
+
return (await provider.getFeeData()).gasPrice;
|
|
470
|
+
case "getPriorityFee":
|
|
471
|
+
return (await provider.getFeeData()).maxPriorityFeePerGas;
|
|
472
|
+
case "getLogs":
|
|
473
|
+
return await provider.getLogs(req.filter);
|
|
474
|
+
case "getStorage":
|
|
475
|
+
return await provider.getStorage(req.address, req.position, req.blockTag);
|
|
476
|
+
case "getTransaction":
|
|
477
|
+
return await provider.getTransaction(req.hash);
|
|
478
|
+
case "getTransactionCount":
|
|
479
|
+
return await provider.getTransactionCount(req.address, req.blockTag);
|
|
480
|
+
case "getTransactionReceipt":
|
|
481
|
+
return await provider.getTransactionReceipt(req.hash);
|
|
482
|
+
case "getTransactionResult":
|
|
483
|
+
return await provider.getTransactionResult(req.hash);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Grab the next (random) config that is not already part of
|
|
488
|
+
// the running set
|
|
489
|
+
#getNextConfig(running: Set<RunnerState>): null | Config {
|
|
490
|
+
// @TODO: Maybe do a check here to favour (heavily) providers that
|
|
491
|
+
// do not require waitForSync and disfavour providers that
|
|
492
|
+
// seem down-ish or are behaving slowly
|
|
493
|
+
|
|
494
|
+
const configs = Array.from(running).map((r) => r.config)
|
|
495
|
+
|
|
496
|
+
// Shuffle the states, sorted by priority
|
|
497
|
+
const allConfigs = this.#configs.slice();
|
|
498
|
+
shuffle(allConfigs);
|
|
499
|
+
allConfigs.sort((a, b) => (a.priority - b.priority));
|
|
500
|
+
|
|
501
|
+
for (const config of allConfigs) {
|
|
502
|
+
if (config._lastFatalError) { continue; }
|
|
503
|
+
if (configs.indexOf(config) === -1) { return config; }
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Adds a new runner (if available) to running.
|
|
510
|
+
#addRunner(running: Set<RunnerState>, req: PerformActionRequest): null | RunnerState {
|
|
511
|
+
const config = this.#getNextConfig(running);
|
|
512
|
+
|
|
513
|
+
// No runners available
|
|
514
|
+
if (config == null) { return null; }
|
|
515
|
+
|
|
516
|
+
// Create a new runner
|
|
517
|
+
const runner: RunnerState = {
|
|
518
|
+
config, result: null, didBump: false,
|
|
519
|
+
perform: null, staller: null
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const now = getTime();
|
|
523
|
+
|
|
524
|
+
// Start performing this operation
|
|
525
|
+
runner.perform = (async () => {
|
|
526
|
+
try {
|
|
527
|
+
config.requests++;
|
|
528
|
+
const result = await this._translatePerform(config.provider, req);
|
|
529
|
+
runner.result = { result };
|
|
530
|
+
} catch (error: any) {
|
|
531
|
+
config.errorResponses++;
|
|
532
|
+
runner.result = { error };
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const dt = (getTime() - now);
|
|
536
|
+
config._totalTime += dt;
|
|
537
|
+
|
|
538
|
+
config.rollingDuration = 0.95 * config.rollingDuration + 0.05 * dt;
|
|
539
|
+
|
|
540
|
+
runner.perform = null;
|
|
541
|
+
})();
|
|
542
|
+
|
|
543
|
+
// Start a staller; when this times out, it's time to force
|
|
544
|
+
// kicking off another runner because we are taking too long
|
|
545
|
+
runner.staller = (async () => {
|
|
546
|
+
await stall(config.stallTimeout);
|
|
547
|
+
runner.staller = null;
|
|
548
|
+
})();
|
|
549
|
+
|
|
550
|
+
running.add(runner);
|
|
551
|
+
return runner;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Initializes the blockNumber and network for each runner and
|
|
555
|
+
// blocks until initialized
|
|
556
|
+
async #initialSync(): Promise<void> {
|
|
557
|
+
let initialSync = this.#initialSyncPromise;
|
|
558
|
+
if (!initialSync) {
|
|
559
|
+
const promises: Array<Promise<any>> = [ ];
|
|
560
|
+
this.#configs.forEach((config) => {
|
|
561
|
+
promises.push((async () => {
|
|
562
|
+
await waitForSync(config, 0);
|
|
563
|
+
if (!config._lastFatalError) {
|
|
564
|
+
config._network = await config.provider.getNetwork();
|
|
565
|
+
}
|
|
566
|
+
})());
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
this.#initialSyncPromise = initialSync = (async () => {
|
|
570
|
+
// Wait for all providers to have a block number and network
|
|
571
|
+
await Promise.all(promises);
|
|
572
|
+
|
|
573
|
+
// Check all the networks match
|
|
574
|
+
let chainId: null | bigint = null;
|
|
575
|
+
for (const config of this.#configs) {
|
|
576
|
+
if (config._lastFatalError) { continue; }
|
|
577
|
+
const network = <Network>(config._network);
|
|
578
|
+
if (chainId == null) {
|
|
579
|
+
chainId = network.chainId;
|
|
580
|
+
} else if (network.chainId !== chainId) {
|
|
581
|
+
assert(false, "cannot mix providers on different networks", "UNSUPPORTED_OPERATION", {
|
|
582
|
+
operation: "new FallbackProvider"
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
})();
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
await initialSync
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
async #checkQuorum(running: Set<RunnerState>, req: PerformActionRequest): Promise<any> {
|
|
594
|
+
// Get all the result objects
|
|
595
|
+
const results: Array<TallyResult> = [ ];
|
|
596
|
+
for (const runner of running) {
|
|
597
|
+
if (runner.result != null) {
|
|
598
|
+
const { tag, value } = normalizeResult(req.method, runner.result);
|
|
599
|
+
results.push({ tag, value, weight: runner.config.weight });
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Are there enough results to event meet quorum?
|
|
604
|
+
if (results.reduce((a, r) => (a + r.weight), 0) < this.quorum) {
|
|
605
|
+
return undefined;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
switch (req.method) {
|
|
609
|
+
case "getBlockNumber": {
|
|
610
|
+
// We need to get the bootstrap block height
|
|
611
|
+
if (this.#height === -2) {
|
|
612
|
+
this.#height = Math.ceil(getNumber(<bigint>getMedian(this.quorum, this.#configs.filter((c) => (!c._lastFatalError)).map((c) => ({
|
|
613
|
+
value: c.blockNumber,
|
|
614
|
+
tag: getNumber(c.blockNumber).toString(),
|
|
615
|
+
weight: c.weight
|
|
616
|
+
})))));
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Find the mode across all the providers, allowing for
|
|
620
|
+
// a little drift between block heights
|
|
621
|
+
const mode = getFuzzyMode(this.quorum, results);
|
|
622
|
+
if (mode === undefined) { return undefined; }
|
|
623
|
+
if (mode > this.#height) { this.#height = mode; }
|
|
624
|
+
return this.#height;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
case "getGasPrice":
|
|
628
|
+
case "getPriorityFee":
|
|
629
|
+
case "estimateGas":
|
|
630
|
+
return getMedian(this.quorum, results);
|
|
631
|
+
|
|
632
|
+
case "getBlock":
|
|
633
|
+
// Pending blocks are in the mempool and already
|
|
634
|
+
// quite untrustworthy; just grab anything
|
|
635
|
+
if ("blockTag" in req && req.blockTag === "pending") {
|
|
636
|
+
return getAnyResult(this.quorum, results);
|
|
637
|
+
}
|
|
638
|
+
return checkQuorum(this.quorum, results);
|
|
639
|
+
|
|
640
|
+
case "call":
|
|
641
|
+
case "chainId":
|
|
642
|
+
case "getBalance":
|
|
643
|
+
case "getTransactionCount":
|
|
644
|
+
case "getCode":
|
|
645
|
+
case "getStorage":
|
|
646
|
+
case "getTransaction":
|
|
647
|
+
case "getTransactionReceipt":
|
|
648
|
+
case "getLogs":
|
|
649
|
+
return checkQuorum(this.quorum, results);
|
|
650
|
+
|
|
651
|
+
case "broadcastTransaction":
|
|
652
|
+
return getAnyResult(this.quorum, results);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
assert(false, "unsupported method", "UNSUPPORTED_OPERATION", {
|
|
656
|
+
operation: `_perform(${ stringify((<any>req).method) })`
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
async #waitForQuorum(running: Set<RunnerState>, req: PerformActionRequest): Promise<any> {
|
|
661
|
+
if (running.size === 0) { throw new Error("no runners?!"); }
|
|
662
|
+
|
|
663
|
+
// Any promises that are interesting to watch for; an expired stall
|
|
664
|
+
// or a successful perform
|
|
665
|
+
const interesting: Array<Promise<void>> = [ ];
|
|
666
|
+
|
|
667
|
+
let newRunners = 0;
|
|
668
|
+
for (const runner of running) {
|
|
669
|
+
|
|
670
|
+
// No responses, yet; keep an eye on it
|
|
671
|
+
if (runner.perform) {
|
|
672
|
+
interesting.push(runner.perform);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Still stalling...
|
|
676
|
+
if (runner.staller) {
|
|
677
|
+
interesting.push(runner.staller);
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// This runner has already triggered another runner
|
|
682
|
+
if (runner.didBump) { continue; }
|
|
683
|
+
|
|
684
|
+
// Got a response (result or error) or stalled; kick off another runner
|
|
685
|
+
runner.didBump = true;
|
|
686
|
+
newRunners++;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Check if we have reached quorum on a result (or error)
|
|
690
|
+
const value = await this.#checkQuorum(running, req);
|
|
691
|
+
if (value !== undefined) {
|
|
692
|
+
if (value instanceof Error) { throw value; }
|
|
693
|
+
return value;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Add any new runners, because a staller timed out or a result
|
|
697
|
+
// or error response came in.
|
|
698
|
+
for (let i = 0; i < newRunners; i++) {
|
|
699
|
+
this.#addRunner(running, req);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// All providers have returned, and we have no result
|
|
703
|
+
|
|
704
|
+
assert(interesting.length > 0, "quorum not met", "SERVER_ERROR", {
|
|
705
|
+
request: "%sub-requests",
|
|
706
|
+
info: { request: req, results: Array.from(running).map((r) => stringify(r.result)) }
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
// Wait for someone to either complete its perform or stall out
|
|
710
|
+
await Promise.race(interesting);
|
|
711
|
+
|
|
712
|
+
// This is recursive, but at worst case the depth is 2x the
|
|
713
|
+
// number of providers (each has a perform and a staller)
|
|
714
|
+
return await this.#waitForQuorum(running, req);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
async _perform<T = any>(req: PerformActionRequest): Promise<T> {
|
|
718
|
+
// Broadcasting a transaction is rare (ish) and already incurs
|
|
719
|
+
// a cost on the user, so spamming is safe-ish. Just send it to
|
|
720
|
+
// every backend.
|
|
721
|
+
if (req.method === "broadcastTransaction") {
|
|
722
|
+
// Once any broadcast provides a positive result, use it. No
|
|
723
|
+
// need to wait for anyone else
|
|
724
|
+
const results: Array<null | TallyResult> = this.#configs.map((c) => null);
|
|
725
|
+
const broadcasts = this.#configs.map(async ({ provider, weight }, index) => {
|
|
726
|
+
try {
|
|
727
|
+
const result = await provider._perform(req);
|
|
728
|
+
results[index] = Object.assign(normalizeResult(req.method, { result }), { weight });
|
|
729
|
+
} catch (error: any) {
|
|
730
|
+
results[index] = Object.assign(normalizeResult(req.method, { error }), { weight });
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
// As each promise finishes...
|
|
735
|
+
while (true) {
|
|
736
|
+
// Check for a valid broadcast result
|
|
737
|
+
const done = <Array<any>>results.filter((r) => (r != null));
|
|
738
|
+
for (const { value } of done) {
|
|
739
|
+
if (!(value instanceof Error)) { return value; }
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Check for a legit broadcast error (one which we cannot
|
|
743
|
+
// recover from; some nodes may return the following red
|
|
744
|
+
// herring events:
|
|
745
|
+
// - alredy seend (UNKNOWN_ERROR)
|
|
746
|
+
// - NONCE_EXPIRED
|
|
747
|
+
// - REPLACEMENT_UNDERPRICED
|
|
748
|
+
const result = checkQuorum(this.quorum, <Array<any>>results.filter((r) => (r != null)));
|
|
749
|
+
if (isError(result, "INSUFFICIENT_FUNDS")) {
|
|
750
|
+
throw result;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Kick off the next provider (if any)
|
|
754
|
+
const waiting = broadcasts.filter((b, i) => (results[i] == null));
|
|
755
|
+
if (waiting.length === 0) { break; }
|
|
756
|
+
await Promise.race(waiting);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Use standard quorum results; any result was returned above,
|
|
760
|
+
// so this will find any error that met quorum if any
|
|
761
|
+
const result = getAnyResult(this.quorum, <Array<any>>results);
|
|
762
|
+
assert(result !== undefined, "problem multi-broadcasting", "SERVER_ERROR", {
|
|
763
|
+
request: "%sub-requests",
|
|
764
|
+
info: { request: req, results: results.map(stringify) }
|
|
765
|
+
})
|
|
766
|
+
if (result instanceof Error) { throw result; }
|
|
767
|
+
return result;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
await this.#initialSync();
|
|
771
|
+
|
|
772
|
+
// Bootstrap enough runners to meet quorum
|
|
773
|
+
const running: Set<RunnerState> = new Set();
|
|
774
|
+
let inflightQuorum = 0;
|
|
775
|
+
while (true) {
|
|
776
|
+
const runner = this.#addRunner(running, req);
|
|
777
|
+
if (runner == null) { break; }
|
|
778
|
+
inflightQuorum += runner.config.weight;
|
|
779
|
+
if (inflightQuorum >= this.quorum) { break; }
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const result = await this.#waitForQuorum(running, req);
|
|
783
|
+
|
|
784
|
+
// Track requests sent to a provider that are still
|
|
785
|
+
// outstanding after quorum has been otherwise found
|
|
786
|
+
for (const runner of running) {
|
|
787
|
+
if (runner.perform && runner.result == null) {
|
|
788
|
+
runner.config.lateResponses++;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return result;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async destroy(): Promise<void> {
|
|
796
|
+
for (const { provider } of this.#configs) {
|
|
797
|
+
provider.destroy();
|
|
798
|
+
}
|
|
799
|
+
super.destroy();
|
|
800
|
+
}
|
|
801
|
+
}
|