2019-05-15 01:25:46 +03:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
import { Network } from "@ethersproject/networks";
|
|
|
|
import { shuffled } from "@ethersproject/random";
|
2019-08-01 23:13:35 +03:00
|
|
|
import { deepCopy, defineReadOnly } from "@ethersproject/properties";
|
2019-08-22 23:51:35 +03:00
|
|
|
import { BigNumber } from "@ethersproject/bignumber";
|
2019-05-15 01:25:46 +03:00
|
|
|
|
2019-08-02 01:04:06 +03:00
|
|
|
import { Logger } from "@ethersproject/logger";
|
|
|
|
import { version } from "./_version";
|
|
|
|
const logger = new Logger(version);
|
|
|
|
|
2019-05-15 01:25:46 +03:00
|
|
|
import { BaseProvider } from "./base-provider";
|
|
|
|
|
|
|
|
function now() { return (new Date()).getTime(); }
|
|
|
|
|
|
|
|
// Returns:
|
|
|
|
// - true is all networks match
|
|
|
|
// - false if any network is null
|
|
|
|
// - throws if any 2 networks do not match
|
|
|
|
function checkNetworks(networks: Array<Network>): boolean {
|
|
|
|
let result = true;
|
|
|
|
|
|
|
|
let check: Network = null;
|
|
|
|
networks.forEach((network) => {
|
|
|
|
|
|
|
|
// Null
|
|
|
|
if (network == null) {
|
|
|
|
result = false;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Have nothing to compre to yet
|
|
|
|
if (check == null) {
|
|
|
|
check = network;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Matches!
|
|
|
|
if (check.name === network.name &&
|
|
|
|
check.chainId === network.chainId &&
|
|
|
|
((check.ensAddress === network.ensAddress) ||
|
|
|
|
(check.ensAddress == null && network.ensAddress == null))) { return; }
|
|
|
|
|
2019-08-02 01:04:06 +03:00
|
|
|
logger.throwArgumentError("provider mismatch", "networks", networks);
|
2019-05-15 01:25:46 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
type Result = {
|
|
|
|
result?: any;
|
|
|
|
error?: Error;
|
|
|
|
weight: number;
|
|
|
|
};
|
|
|
|
|
|
|
|
type Runner = {
|
|
|
|
run: () => Promise<Result>;
|
|
|
|
weight: number;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function serialize(result: any): string {
|
|
|
|
if (Array.isArray(result)) {
|
|
|
|
return JSON.stringify(result.map((r) => serialize(r)));
|
|
|
|
} else if (result === null) {
|
|
|
|
return "null";
|
|
|
|
} else if (typeof(result) === "object") {
|
|
|
|
let keys = Object.keys(result);
|
|
|
|
keys.sort();
|
2019-08-21 08:45:51 +03:00
|
|
|
return "{" + keys.map((key) => {
|
2019-05-15 01:25:46 +03:00
|
|
|
let value = result[key];
|
2019-08-21 08:45:51 +03:00
|
|
|
if (typeof(value) === "function") {
|
|
|
|
value = "function{}";
|
|
|
|
} else {
|
|
|
|
value = serialize(value);
|
|
|
|
}
|
|
|
|
return JSON.stringify(key) + "=" + serialize(value);
|
|
|
|
}).join(",") + "}";
|
2019-05-15 01:25:46 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return JSON.stringify(result);
|
|
|
|
}
|
|
|
|
|
|
|
|
let nextRid = 1;
|
|
|
|
|
|
|
|
export class FallbackProvider extends BaseProvider {
|
|
|
|
readonly providers: Array<BaseProvider>;
|
|
|
|
readonly weights: Array<number>;
|
|
|
|
readonly quorum: number;
|
|
|
|
|
|
|
|
constructor(providers: Array<BaseProvider>, quorum?: number, weights?: Array<number>) {
|
2019-08-02 01:04:06 +03:00
|
|
|
logger.checkNew(new.target, FallbackProvider);
|
2019-05-15 01:25:46 +03:00
|
|
|
|
|
|
|
if (providers.length === 0) {
|
2019-08-02 01:04:06 +03:00
|
|
|
logger.throwArgumentError("missing providers", "providers", providers);
|
2019-05-15 01:25:46 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (weights != null && weights.length !== providers.length) {
|
2019-08-02 01:04:06 +03:00
|
|
|
logger.throwArgumentError("too many weights", "weights", weights);
|
2019-05-15 01:25:46 +03:00
|
|
|
} else if (!weights) {
|
|
|
|
weights = providers.map((p) => 1);
|
|
|
|
} else {
|
|
|
|
weights.forEach((w) => {
|
|
|
|
if (w % 1 || w > 512 || w < 1) {
|
2019-08-02 01:04:06 +03:00
|
|
|
logger.throwArgumentError("invalid weight; must be integer in [1, 512]", "weights", weights);
|
2019-05-15 01:25:46 +03:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
let total = weights.reduce((accum, w) => (accum + w));
|
|
|
|
|
|
|
|
if (quorum == null) {
|
|
|
|
quorum = total / 2;
|
|
|
|
} else {
|
|
|
|
if (quorum > total) {
|
2019-08-02 01:04:06 +03:00
|
|
|
logger.throwArgumentError("quorum will always fail; larger than total weight", "quorum", quorum);
|
2019-05-15 01:25:46 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// All networks are ready, we can know the network for certain
|
|
|
|
let ready = checkNetworks(providers.map((p) => p.network));
|
|
|
|
if (ready) {
|
|
|
|
super(providers[0].network);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
// The network won't be known until all child providers know
|
|
|
|
let ready = Promise.all(providers.map((p) => p.getNetwork())).then((networks) => {
|
|
|
|
if (!checkNetworks(networks)) {
|
2019-08-02 01:04:06 +03:00
|
|
|
logger.throwError("getNetwork returned null", Logger.errors.UNKNOWN_ERROR)
|
2019-05-15 01:25:46 +03:00
|
|
|
}
|
|
|
|
return networks[0];
|
|
|
|
});
|
|
|
|
|
|
|
|
super(ready);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Preserve a copy, so we do not get mutated
|
|
|
|
defineReadOnly(this, "providers", Object.freeze(providers.slice()));
|
|
|
|
defineReadOnly(this, "quorum", quorum);
|
|
|
|
defineReadOnly(this, "weights", Object.freeze(weights.slice()));
|
|
|
|
}
|
|
|
|
|
2019-08-21 08:45:51 +03:00
|
|
|
static doPerform(provider: BaseProvider, method: string, params: { [ name: string ]: any }): Promise<any> {
|
|
|
|
switch (method) {
|
|
|
|
case "getBlockNumber":
|
|
|
|
case "getGasPrice":
|
|
|
|
case "getEtherPrice":
|
|
|
|
return provider[method]();
|
|
|
|
case "getBalance":
|
|
|
|
case "getTransactionCount":
|
|
|
|
case "getCode":
|
|
|
|
return provider[method](params.address, params.blockTag || "latest");
|
|
|
|
case "getStorageAt":
|
|
|
|
return provider.getStorageAt(params.address, params.position, params.blockTag || "latest");
|
|
|
|
case "sendTransaction":
|
2019-08-22 23:51:35 +03:00
|
|
|
return provider.sendTransaction(params.signedTransaction).then((result) => {
|
|
|
|
return result.hash;
|
|
|
|
});
|
2019-08-21 08:45:51 +03:00
|
|
|
case "getBlock":
|
|
|
|
return provider[(params.includeTransactions ? "getBlockWithTransactions": "getBlock")](params.blockTag || params.blockHash);
|
|
|
|
case "call":
|
|
|
|
case "estimateGas":
|
|
|
|
return provider[method](params.transaction);
|
|
|
|
case "getTransaction":
|
|
|
|
case "getTransactionReceipt":
|
|
|
|
return provider[method](params.transactionHash);
|
|
|
|
case "getLogs":
|
|
|
|
return provider.getLogs(params.filter);
|
|
|
|
}
|
|
|
|
return logger.throwError("unknown method error", Logger.errors.UNKNOWN_ERROR, {
|
|
|
|
method: method,
|
|
|
|
params: params
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-05-15 01:25:46 +03:00
|
|
|
perform(method: string, params: { [name: string]: any }): any {
|
|
|
|
let T0 = now();
|
|
|
|
let runners: Array<Runner> = (<Array<BaseProvider>>(shuffled(this.providers))).map((provider, i) => {
|
|
|
|
let weight = this.weights[i];
|
|
|
|
let rid = nextRid++;
|
|
|
|
return {
|
|
|
|
run: () => {
|
|
|
|
let t0 = now();
|
|
|
|
let start = t0 - T0;
|
2019-08-01 23:13:35 +03:00
|
|
|
this.emit("debug", {
|
|
|
|
action: "request",
|
|
|
|
rid: rid,
|
|
|
|
backend: { weight, start, provider },
|
|
|
|
request: { method: method, params: deepCopy(params) },
|
|
|
|
provider: this
|
|
|
|
});
|
2019-08-21 08:45:51 +03:00
|
|
|
return FallbackProvider.doPerform(provider, method, params).then((result) => {
|
2019-05-15 01:25:46 +03:00
|
|
|
let duration = now() - t0;
|
2019-08-01 23:13:35 +03:00
|
|
|
this.emit("debug", {
|
|
|
|
action: "response",
|
|
|
|
rid: rid,
|
|
|
|
backend: { weight, start, duration, provider },
|
|
|
|
request: { method: method, params: deepCopy(params) },
|
|
|
|
response: deepCopy(result)
|
|
|
|
});
|
2019-05-15 01:25:46 +03:00
|
|
|
return { weight: weight, result: result };
|
|
|
|
}, (error) => {
|
|
|
|
let duration = now() - t0;
|
2019-08-01 23:13:35 +03:00
|
|
|
this.emit("debug", {
|
|
|
|
action: "response",
|
|
|
|
rid: rid,
|
|
|
|
backend: { weight, start, duration, provider },
|
|
|
|
request: { method: method, params: deepCopy(params) },
|
|
|
|
error: error
|
|
|
|
});
|
2019-05-15 01:25:46 +03:00
|
|
|
return { weight: weight, error: error };
|
|
|
|
});
|
|
|
|
},
|
|
|
|
weight: weight
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Broadcast transactions to all backends, any that succeed is good enough
|
|
|
|
if (method === "sendTransaction") {
|
|
|
|
return Promise.all(runners.map((r) => r.run())).then((results) => {
|
|
|
|
for (let i = 0; i < results.length; i++) {
|
|
|
|
let result = results[i];
|
|
|
|
if (result.result) { return result.result; }
|
|
|
|
}
|
|
|
|
return Promise.reject(results[0].error);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise query backends (randomly) until we have a quorum agreement
|
|
|
|
// on the correct result
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
let firstError: Error = null;
|
|
|
|
|
|
|
|
// How much weight is inflight
|
|
|
|
let inflightWeight = 0;
|
|
|
|
|
|
|
|
// All results, indexed by the serialized response.
|
|
|
|
let results: { [ unique: string ]: Array<Result> } = { };
|
|
|
|
|
|
|
|
let next = () => {
|
|
|
|
if (runners.length === 0) { return; }
|
|
|
|
|
|
|
|
let runner = runners.shift();
|
|
|
|
inflightWeight += runner.weight;
|
|
|
|
|
|
|
|
runner.run().then((result) => {
|
|
|
|
if (results === null) { return; }
|
|
|
|
inflightWeight -= runner.weight;
|
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
if (firstError == null) { firstError = result.error; }
|
|
|
|
|
|
|
|
} else {
|
|
|
|
let unique = serialize(result.result);
|
|
|
|
if (results[unique] == null) { results[unique] = []; }
|
|
|
|
results[unique].push(result);
|
|
|
|
|
|
|
|
// Do any results meet our quroum?
|
|
|
|
for (let u in results) {
|
|
|
|
let weight = results[u].reduce((accum, r) => (accum + r.weight), 0);
|
|
|
|
if (weight >= this.quorum) {
|
|
|
|
let result = results[u][0].result;
|
|
|
|
this.emit("debug", "quorum", -1, { weight, result })
|
|
|
|
resolve(result);
|
|
|
|
results = null;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Out of options; give up
|
|
|
|
if (runners.length === 0 && inflightWeight === 0) {
|
2019-08-22 23:51:35 +03:00
|
|
|
|
|
|
|
// @TODO: this might need some more thinking... Maybe only if half
|
|
|
|
// of the results contain non-error?
|
|
|
|
if (method === "getGasPrice") {
|
|
|
|
const values: Array<BigNumber> = [ ];
|
|
|
|
Object.keys(results).forEach((key) => {
|
|
|
|
results[key].forEach((result) => {
|
|
|
|
if (!result.result) { return; }
|
|
|
|
values.push(result.result);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
values.sort((a, b) => {
|
|
|
|
if (a.lt(b)) { return -1; }
|
|
|
|
if (a.gt(b)) { return 1; }
|
|
|
|
return 0;
|
|
|
|
});
|
|
|
|
let index = parseInt(String(values.length / 2));
|
|
|
|
if (values.length % 2) {
|
|
|
|
resolve(values[index]);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
resolve(values[index - 1].add(values[index]).div(2));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-08-21 08:45:51 +03:00
|
|
|
if (firstError === null) {
|
|
|
|
firstError = logger.makeError("failed to meet quorum", Logger.errors.SERVER_ERROR, {
|
|
|
|
results: Object.keys(results).map((u) => {
|
2019-08-22 23:51:35 +03:00
|
|
|
return {
|
|
|
|
method: method,
|
|
|
|
params: params,
|
|
|
|
result: u,
|
|
|
|
weight: results[u].reduce((accum, r) => (accum + r.weight), 0)
|
|
|
|
};
|
2019-08-21 08:45:51 +03:00
|
|
|
})
|
|
|
|
});
|
|
|
|
}
|
2019-05-15 01:25:46 +03:00
|
|
|
reject(firstError);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Queue up the next round
|
|
|
|
setTimeout(next, 0);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Fire off requests until we could possibly meet quorum
|
|
|
|
if (inflightWeight < this.quorum) {
|
|
|
|
setTimeout(next, 0);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// bootstrap firing requests
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|