ethers.js/packages/providers/lib.esm/fallback-provider.js

528 lines
21 KiB
JavaScript
Raw Permalink Normal View History

2019-05-14 18:48:48 -04:00
"use strict";
2020-01-18 21:48:12 -05:00
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { Provider } from "@ethersproject/abstract-provider";
import { BigNumber } from "@ethersproject/bignumber";
2020-05-03 21:11:18 -04:00
import { isHexString } from "@ethersproject/bytes";
import { deepCopy, defineReadOnly, shallowCopy } from "@ethersproject/properties";
import { shuffled } from "@ethersproject/random";
import { poll } from "@ethersproject/web";
import { BaseProvider } from "./base-provider";
import { Logger } from "@ethersproject/logger";
import { version } from "./_version";
const logger = new Logger(version);
2019-05-14 18:48:48 -04:00
function now() { return (new Date()).getTime(); }
2020-01-18 21:48:12 -05:00
// Returns to network as long as all agree, or null if any is null.
// Throws an error if any two networks do not match.
2019-05-14 18:48:48 -04:00
function checkNetworks(networks) {
2020-01-18 21:48:12 -05:00
let result = null;
for (let i = 0; i < networks.length; i++) {
const network = networks[i];
// Null! We do not know our network; bail.
2019-05-14 18:48:48 -04:00
if (network == null) {
2020-01-18 21:48:12 -05:00
return null;
2019-05-14 18:48:48 -04:00
}
2020-01-18 21:48:12 -05:00
if (result) {
// Make sure the network matches the previous networks
if (!(result.name === network.name && result.chainId === network.chainId &&
((result.ensAddress === network.ensAddress) || (result.ensAddress == null && network.ensAddress == null)))) {
logger.throwArgumentError("provider mismatch", "networks", networks);
}
2019-05-14 18:48:48 -04:00
}
2020-01-18 21:48:12 -05:00
else {
result = network;
2019-05-14 18:48:48 -04:00
}
2020-01-18 21:48:12 -05:00
}
2019-05-14 18:48:48 -04:00
return result;
}
2020-04-23 23:35:39 -04:00
function median(values, maxDelta) {
2020-01-18 21:48:12 -05:00
values = values.slice().sort();
const middle = Math.floor(values.length / 2);
// Odd length; take the middle
if (values.length % 2) {
return values[middle];
}
// Even length; take the average of the two middle
const a = values[middle - 1], b = values[middle];
2020-04-23 23:35:39 -04:00
if (maxDelta != null && Math.abs(a - b) > maxDelta) {
return null;
}
2020-01-18 21:48:12 -05:00
return (a + b) / 2;
}
function serialize(value) {
if (value === null) {
2020-02-04 01:06:47 -05:00
return "null";
2020-01-18 21:48:12 -05:00
}
else if (typeof (value) === "number" || typeof (value) === "boolean") {
return JSON.stringify(value);
}
else if (typeof (value) === "string") {
return value;
2019-05-14 18:48:48 -04:00
}
2020-01-18 21:48:12 -05:00
else if (BigNumber.isBigNumber(value)) {
return value.toString();
2019-05-14 18:48:48 -04:00
}
2020-01-18 21:48:12 -05:00
else if (Array.isArray(value)) {
return JSON.stringify(value.map((i) => serialize(i)));
}
else if (typeof (value) === "object") {
const keys = Object.keys(value);
2019-05-14 18:48:48 -04:00
keys.sort();
return "{" + keys.map((key) => {
2020-01-18 21:48:12 -05:00
let v = value[key];
if (typeof (v) === "function") {
v = "[function]";
2019-05-14 18:48:48 -04:00
}
2019-08-21 01:52:13 -04:00
else {
2020-01-18 21:48:12 -05:00
v = serialize(v);
2019-08-21 01:52:13 -04:00
}
2020-01-18 21:48:12 -05:00
return JSON.stringify(key) + ":" + v;
2019-08-21 01:52:13 -04:00
}).join(",") + "}";
2019-05-14 18:48:48 -04:00
}
2020-01-18 21:48:12 -05:00
throw new Error("unknown value type: " + typeof (value));
2019-05-14 18:48:48 -04:00
}
2020-01-18 21:48:12 -05:00
// Next request ID to use for emitting debug info
let nextRid = 1;
2020-01-18 21:48:12 -05:00
;
function stall(duration) {
2020-05-03 17:53:58 -04:00
let cancel = null;
let timer = null;
let promise = (new Promise((resolve) => {
cancel = function () {
if (timer) {
clearTimeout(timer);
timer = null;
}
resolve();
};
timer = setTimeout(cancel, duration);
}));
const wait = (func) => {
promise = promise.then(func);
return promise;
};
function getPromise() {
return promise;
}
return { cancel, getPromise, wait };
2020-01-18 21:48:12 -05:00
}
;
function exposeDebugConfig(config, now) {
const result = {
provider: config.provider,
weight: config.weight
};
if (config.start) {
result.start = config.start;
}
if (now) {
result.duration = (now - config.start);
}
if (config.done) {
if (config.error) {
result.error = config.error;
2019-05-14 18:48:48 -04:00
}
else {
2020-01-18 21:48:12 -05:00
result.result = config.result || null;
}
}
return result;
}
function normalizedTally(normalize, quorum) {
return function (configs) {
// Count the votes for each result
const tally = {};
configs.forEach((c) => {
const value = normalize(c.result);
if (!tally[value]) {
tally[value] = { count: 0, result: c.result };
}
tally[value].count++;
});
// Check for a quorum on any given result
const keys = Object.keys(tally);
for (let i = 0; i < keys.length; i++) {
const check = tally[keys[i]];
if (check.count >= quorum) {
return check.result;
}
}
// No quroum
return undefined;
};
}
function getProcessFunc(provider, method, params) {
let normalize = serialize;
switch (method) {
case "getBlockNumber":
// Return the median value, unless there is (median + 1) is also
// present, in which case that is probably true and the median
// is going to be stale soon. In the event of a malicious node,
// the lie will be true soon enough.
return function (configs) {
const values = configs.map((c) => c.result);
// Get the median block number
2020-04-23 23:35:39 -04:00
let blockNumber = median(configs.map((c) => c.result), 2);
if (blockNumber == null) {
return undefined;
}
blockNumber = Math.ceil(blockNumber);
2020-01-18 21:48:12 -05:00
// If the next block height is present, its prolly safe to use
if (values.indexOf(blockNumber + 1) >= 0) {
blockNumber++;
2019-05-14 18:48:48 -04:00
}
2020-01-18 21:48:12 -05:00
// Don't ever roll back the blockNumber
if (blockNumber >= provider._highestBlockNumber) {
provider._highestBlockNumber = blockNumber;
}
return provider._highestBlockNumber;
};
case "getGasPrice":
// Return the middle (round index up) value, similar to median
// but do not average even entries and choose the higher.
// Malicious actors must compromise 50% of the nodes to lie.
return function (configs) {
const values = configs.map((c) => c.result);
values.sort();
return values[Math.floor(values.length / 2)];
};
case "getEtherPrice":
// Returns the median price. Malicious actors must compromise at
// least 50% of the nodes to lie (in a meaningful way).
return function (configs) {
return median(configs.map((c) => c.result));
};
// No additional normalizing required; serialize is enough
case "getBalance":
case "getTransactionCount":
case "getCode":
case "getStorageAt":
case "call":
case "estimateGas":
case "getLogs":
break;
// We drop the confirmations from transactions as it is approximate
case "getTransaction":
case "getTransactionReceipt":
normalize = function (tx) {
2020-02-04 01:06:47 -05:00
if (tx == null) {
return null;
}
2020-01-18 21:48:12 -05:00
tx = shallowCopy(tx);
tx.confirmations = -1;
return serialize(tx);
};
break;
// We drop the confirmations from transactions as it is approximate
case "getBlock":
// We drop the confirmations from transactions as it is approximate
if (params.includeTransactions) {
normalize = function (block) {
2020-02-04 01:06:47 -05:00
if (block == null) {
return null;
}
2020-01-18 21:48:12 -05:00
block = shallowCopy(block);
block.transactions = block.transactions.map((tx) => {
tx = shallowCopy(tx);
tx.confirmations = -1;
return tx;
});
return serialize(block);
};
}
2020-02-04 01:06:47 -05:00
else {
normalize = function (block) {
if (block == null) {
return null;
}
return serialize(block);
};
}
2020-01-18 21:48:12 -05:00
break;
default:
throw new Error("unknown method: " + method);
}
// Return the result if and only if the expected quorum is
// satisfied and agreed upon for the final result.
return normalizedTally(normalize, provider.quorum);
}
2020-05-03 21:11:18 -04:00
// If we are doing a blockTag query, we need to make sure the backend is
// caught up to the FallbackProvider, before sending a request to it.
2020-05-04 23:01:04 -04:00
function waitForSync(config, blockNumber) {
2020-05-03 21:11:18 -04:00
return __awaiter(this, void 0, void 0, function* () {
2020-05-04 23:01:04 -04:00
const provider = (config.provider);
2020-05-03 21:11:18 -04:00
if ((provider.blockNumber != null && provider.blockNumber >= blockNumber) || blockNumber === -1) {
return provider;
}
return poll(() => {
2020-05-04 23:01:04 -04:00
return new Promise((resolve, reject) => {
setTimeout(function () {
// We are synced
if (provider.blockNumber >= blockNumber) {
2020-05-21 00:07:41 -04:00
return resolve(provider);
2020-05-04 23:01:04 -04:00
}
// We're done; just quit
if (config.cancelled) {
return resolve(null);
}
// Try again, next block
return resolve(undefined);
}, 0);
2020-05-03 21:11:18 -04:00
});
2020-05-04 23:01:04 -04:00
}, { oncePoll: provider });
2020-05-03 21:11:18 -04:00
});
}
2020-05-04 23:01:04 -04:00
function getRunner(config, currentBlockNumber, method, params) {
2020-05-03 21:11:18 -04:00
return __awaiter(this, void 0, void 0, function* () {
2020-05-04 23:01:04 -04:00
let provider = config.provider;
2020-05-03 21:11:18 -04:00
switch (method) {
case "getBlockNumber":
case "getGasPrice":
return provider[method]();
case "getEtherPrice":
if (provider.getEtherPrice) {
return provider.getEtherPrice();
}
break;
case "getBalance":
case "getTransactionCount":
case "getCode":
if (params.blockTag && isHexString(params.blockTag)) {
2020-05-04 23:01:04 -04:00
provider = yield waitForSync(config, currentBlockNumber);
2020-05-03 21:11:18 -04:00
}
return provider[method](params.address, params.blockTag || "latest");
case "getStorageAt":
if (params.blockTag && isHexString(params.blockTag)) {
2020-05-04 23:01:04 -04:00
provider = yield waitForSync(config, currentBlockNumber);
2020-05-03 21:11:18 -04:00
}
return provider.getStorageAt(params.address, params.position, params.blockTag || "latest");
case "getBlock":
if (params.blockTag && isHexString(params.blockTag)) {
2020-05-04 23:01:04 -04:00
provider = yield waitForSync(config, currentBlockNumber);
2020-05-03 21:11:18 -04:00
}
return provider[(params.includeTransactions ? "getBlockWithTransactions" : "getBlock")](params.blockTag || params.blockHash);
case "call":
case "estimateGas":
if (params.blockTag && isHexString(params.blockTag)) {
2020-05-04 23:01:04 -04:00
provider = yield waitForSync(config, currentBlockNumber);
2020-05-03 21:11:18 -04:00
}
return provider[method](params.transaction);
case "getTransaction":
case "getTransactionReceipt":
return provider[method](params.transactionHash);
case "getLogs": {
let filter = params.filter;
if ((filter.fromBlock && isHexString(filter.fromBlock)) || (filter.toBlock && isHexString(filter.toBlock))) {
2020-05-04 23:01:04 -04:00
provider = yield waitForSync(config, currentBlockNumber);
2020-05-03 21:11:18 -04:00
}
return provider.getLogs(filter);
2020-01-18 21:48:12 -05:00
}
2020-05-03 21:11:18 -04:00
}
return logger.throwError("unknown method error", Logger.errors.UNKNOWN_ERROR, {
method: method,
params: params
});
2020-01-18 21:48:12 -05:00
});
}
export class FallbackProvider extends BaseProvider {
constructor(providers, quorum) {
logger.checkNew(new.target, FallbackProvider);
if (providers.length === 0) {
logger.throwArgumentError("missing providers", "providers", providers);
2019-05-14 18:48:48 -04:00
}
2020-01-18 21:48:12 -05:00
const providerConfigs = providers.map((configOrProvider, index) => {
if (Provider.isProvider(configOrProvider)) {
return Object.freeze({ provider: configOrProvider, weight: 1, stallTimeout: 750, priority: 1 });
}
const config = shallowCopy(configOrProvider);
if (config.priority == null) {
config.priority = 1;
}
if (config.stallTimeout == null) {
config.stallTimeout = 750;
}
if (config.weight == null) {
config.weight = 1;
}
const weight = config.weight;
if (weight % 1 || weight > 512 || weight < 1) {
logger.throwArgumentError("invalid weight; must be integer in [1, 512]", `providers[${index}].weight`, weight);
}
return Object.freeze(config);
});
const total = providerConfigs.reduce((accum, c) => (accum + c.weight), 0);
2019-05-14 18:48:48 -04:00
if (quorum == null) {
quorum = total / 2;
}
2020-01-18 21:48:12 -05:00
else if (quorum > total) {
logger.throwArgumentError("quorum will always fail; larger than total weight", "quorum", quorum);
2019-05-14 18:48:48 -04:00
}
2020-05-12 23:31:51 -04:00
// Are all providers' networks are known
let networkOrReady = checkNetworks(providerConfigs.map((c) => (c.provider).network));
// Not all networks are known; we must stall
if (networkOrReady == null) {
networkOrReady = new Promise((resolve, reject) => {
setTimeout(() => {
this.detectNetwork().then(resolve, reject);
}, 0);
});
2019-05-14 18:48:48 -04:00
}
2020-05-12 23:31:51 -04:00
super(networkOrReady);
2019-05-14 18:48:48 -04:00
// Preserve a copy, so we do not get mutated
2020-01-18 21:48:12 -05:00
defineReadOnly(this, "providerConfigs", Object.freeze(providerConfigs));
defineReadOnly(this, "quorum", quorum);
2020-01-18 21:48:12 -05:00
this._highestBlockNumber = -1;
}
2020-05-03 17:53:58 -04:00
detectNetwork() {
return __awaiter(this, void 0, void 0, function* () {
const networks = yield Promise.all(this.providerConfigs.map((c) => c.provider.getNetwork()));
return checkNetworks(networks);
});
}
perform(method, params) {
2020-01-18 21:48:12 -05:00
return __awaiter(this, void 0, void 0, function* () {
// Sending transactions is special; always broadcast it to all backends
if (method === "sendTransaction") {
2020-05-03 17:53:58 -04:00
const results = yield Promise.all(this.providerConfigs.map((c) => {
2020-01-18 21:48:12 -05:00
return c.provider.sendTransaction(params.signedTransaction).then((result) => {
return result.hash;
}, (error) => {
return error;
2019-08-02 02:10:58 -04:00
});
2020-05-03 17:53:58 -04:00
}));
// Any success is good enough (other errors are likely "already seen" errors
for (let i = 0; i < results.length; i++) {
const result = results[i];
if (typeof (result) === "string") {
return result;
2020-01-18 21:48:12 -05:00
}
2020-05-03 17:53:58 -04:00
}
// They were all an error; pick the first error
throw results[0];
2020-01-18 21:48:12 -05:00
}
2020-05-03 21:11:18 -04:00
// We need to make sure we are in sync with our backends, so we need
// to know this before we can make a lot of calls
if (this._highestBlockNumber === -1 && method !== "getBlockNumber") {
yield this.getBlockNumber();
}
2020-01-18 21:48:12 -05:00
const processFunc = getProcessFunc(this, method, params);
// Shuffle the providers and then sort them by their priority; we
// shallowCopy them since we will store the result in them too
2020-05-03 17:53:58 -04:00
const configs = shuffled(this.providerConfigs.map(shallowCopy));
2020-01-18 21:48:12 -05:00
configs.sort((a, b) => (a.priority - b.priority));
2020-05-03 21:11:18 -04:00
const currentBlockNumber = this._highestBlockNumber;
2020-01-18 21:48:12 -05:00
let i = 0;
2020-04-23 23:35:39 -04:00
let first = true;
2020-01-18 21:48:12 -05:00
while (true) {
const t0 = now();
// Compute the inflight weight (exclude anything past)
let inflightWeight = configs.filter((c) => (c.runner && ((t0 - c.start) < c.stallTimeout)))
.reduce((accum, c) => (accum + c.weight), 0);
// Start running enough to meet quorum
while (inflightWeight < this.quorum && i < configs.length) {
const config = configs[i++];
const rid = nextRid++;
config.start = now();
2020-05-03 17:53:58 -04:00
config.staller = stall(config.stallTimeout);
config.staller.wait(() => { config.staller = null; });
2020-05-04 23:01:04 -04:00
config.runner = getRunner(config, currentBlockNumber, method, params).then((result) => {
2020-01-18 21:48:12 -05:00
config.done = true;
config.result = result;
if (this.listenerCount("debug")) {
this.emit("debug", {
action: "request",
rid: rid,
backend: exposeDebugConfig(config, now()),
request: { method: method, params: deepCopy(params) },
provider: this
});
}
}, (error) => {
2020-01-18 21:48:12 -05:00
config.done = true;
config.error = error;
if (this.listenerCount("debug")) {
this.emit("debug", {
action: "request",
rid: rid,
backend: exposeDebugConfig(config, now()),
request: { method: method, params: deepCopy(params) },
provider: this
});
}
});
if (this.listenerCount("debug")) {
this.emit("debug", {
2020-01-18 21:48:12 -05:00
action: "request",
2019-08-02 02:10:58 -04:00
rid: rid,
2020-01-18 21:48:12 -05:00
backend: exposeDebugConfig(config, null),
request: { method: method, params: deepCopy(params) },
2020-01-18 21:48:12 -05:00
provider: this
2019-08-02 02:10:58 -04:00
});
2019-05-14 18:48:48 -04:00
}
2020-01-18 21:48:12 -05:00
inflightWeight += config.weight;
2019-05-14 18:48:48 -04:00
}
2020-01-18 21:48:12 -05:00
// Wait for anything meaningful to finish or stall out
const waiting = [];
configs.forEach((c) => {
if (c.done || !c.runner) {
2019-05-14 18:48:48 -04:00
return;
}
2020-01-18 21:48:12 -05:00
waiting.push(c.runner);
if (c.staller) {
2020-05-03 17:53:58 -04:00
waiting.push(c.staller.getPromise());
2019-05-14 18:48:48 -04:00
}
});
2020-01-18 21:48:12 -05:00
if (waiting.length) {
yield Promise.race(waiting);
2019-05-14 18:48:48 -04:00
}
2020-01-18 21:48:12 -05:00
// Check the quorum and process the results; the process function
// may additionally decide the quorum is not met
const results = configs.filter((c) => (c.done && c.error == null));
if (results.length >= this.quorum) {
const result = processFunc(results);
2020-02-04 01:06:47 -05:00
if (result !== undefined) {
2020-05-03 17:53:58 -04:00
// Shut down any stallers
2020-05-04 23:01:04 -04:00
configs.forEach(c => {
if (c.staller) {
c.staller.cancel();
}
c.cancelled = true;
});
2020-01-18 21:48:12 -05:00
return result;
}
2020-04-23 23:35:39 -04:00
if (!first) {
2020-05-03 17:53:58 -04:00
yield stall(100).getPromise();
2020-04-23 23:35:39 -04:00
}
first = false;
2020-01-18 21:48:12 -05:00
}
// All configs have run to completion; we will never get more data
if (configs.filter((c) => !c.done).length === 0) {
break;
}
}
2020-05-03 17:53:58 -04:00
// Shut down any stallers; shouldn't be any
2020-05-04 23:01:04 -04:00
configs.forEach(c => {
if (c.staller) {
c.staller.cancel();
}
c.cancelled = true;
});
2020-01-18 21:48:12 -05:00
return logger.throwError("failed to meet quorum", Logger.errors.SERVER_ERROR, {
method: method,
params: params,
2020-02-04 01:06:47 -05:00
//results: configs.map((c) => c.result),
2020-01-18 21:48:12 -05:00
//errors: configs.map((c) => c.error),
2020-02-04 01:06:47 -05:00
results: configs.map((c) => exposeDebugConfig(c)),
2020-01-18 21:48:12 -05:00
provider: this
});
2019-05-14 18:48:48 -04:00
});
}
}
2020-07-13 08:03:56 -04:00
//# sourceMappingURL=fallback-provider.js.map