318 lines
14 KiB
JavaScript
318 lines
14 KiB
JavaScript
"use strict";
|
|
var __extends = (this && this.__extends) || (function () {
|
|
var extendStatics = function (d, b) {
|
|
extendStatics = Object.setPrototypeOf ||
|
|
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
|
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
|
|
return extendStatics(d, b);
|
|
};
|
|
return function (d, b) {
|
|
extendStatics(d, b);
|
|
function __() { this.constructor = d; }
|
|
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
|
};
|
|
})();
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
var random_1 = require("@ethersproject/random");
|
|
var properties_1 = require("@ethersproject/properties");
|
|
var logger_1 = require("@ethersproject/logger");
|
|
var _version_1 = require("./_version");
|
|
var logger = new logger_1.Logger(_version_1.version);
|
|
var base_provider_1 = require("./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) {
|
|
var result = true;
|
|
var check = null;
|
|
networks.forEach(function (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;
|
|
}
|
|
logger.throwArgumentError("provider mismatch", "networks", networks);
|
|
});
|
|
return result;
|
|
}
|
|
function serialize(result) {
|
|
if (Array.isArray(result)) {
|
|
return JSON.stringify(result.map(function (r) { return serialize(r); }));
|
|
}
|
|
else if (result === null) {
|
|
return "null";
|
|
}
|
|
else if (typeof (result) === "object") {
|
|
var keys = Object.keys(result);
|
|
keys.sort();
|
|
return "{" + keys.map(function (key) {
|
|
var value = result[key];
|
|
if (typeof (value) === "function") {
|
|
value = "function{}";
|
|
}
|
|
else {
|
|
value = serialize(value);
|
|
}
|
|
return JSON.stringify(key) + "=" + serialize(value);
|
|
}).join(",") + "}";
|
|
}
|
|
return JSON.stringify(result);
|
|
}
|
|
var nextRid = 1;
|
|
var FallbackProvider = /** @class */ (function (_super) {
|
|
__extends(FallbackProvider, _super);
|
|
function FallbackProvider(providers, quorum, weights) {
|
|
var _newTarget = this.constructor;
|
|
var _this = this;
|
|
logger.checkNew(_newTarget, FallbackProvider);
|
|
if (providers.length === 0) {
|
|
logger.throwArgumentError("missing providers", "providers", providers);
|
|
}
|
|
if (weights != null && weights.length !== providers.length) {
|
|
logger.throwArgumentError("too many weights", "weights", weights);
|
|
}
|
|
else if (!weights) {
|
|
weights = providers.map(function (p) { return 1; });
|
|
}
|
|
else {
|
|
weights.forEach(function (w) {
|
|
if (w % 1 || w > 512 || w < 1) {
|
|
logger.throwArgumentError("invalid weight; must be integer in [1, 512]", "weights", weights);
|
|
}
|
|
});
|
|
}
|
|
var total = weights.reduce(function (accum, w) { return (accum + w); });
|
|
if (quorum == null) {
|
|
quorum = total / 2;
|
|
}
|
|
else {
|
|
if (quorum > total) {
|
|
logger.throwArgumentError("quorum will always fail; larger than total weight", "quorum", quorum);
|
|
}
|
|
}
|
|
// All networks are ready, we can know the network for certain
|
|
var ready = checkNetworks(providers.map(function (p) { return p.network; }));
|
|
if (ready) {
|
|
_this = _super.call(this, providers[0].network) || this;
|
|
}
|
|
else {
|
|
// The network won't be known until all child providers know
|
|
var ready_1 = Promise.all(providers.map(function (p) { return p.getNetwork(); })).then(function (networks) {
|
|
if (!checkNetworks(networks)) {
|
|
logger.throwError("getNetwork returned null", logger_1.Logger.errors.UNKNOWN_ERROR);
|
|
}
|
|
return networks[0];
|
|
});
|
|
_this = _super.call(this, ready_1) || this;
|
|
}
|
|
// Preserve a copy, so we do not get mutated
|
|
properties_1.defineReadOnly(_this, "providers", Object.freeze(providers.slice()));
|
|
properties_1.defineReadOnly(_this, "quorum", quorum);
|
|
properties_1.defineReadOnly(_this, "weights", Object.freeze(weights.slice()));
|
|
return _this;
|
|
}
|
|
FallbackProvider.doPerform = function (provider, method, params) {
|
|
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":
|
|
return provider.sendTransaction(params.signedTransaction).then(function (result) {
|
|
return result.hash;
|
|
});
|
|
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_1.Logger.errors.UNKNOWN_ERROR, {
|
|
method: method,
|
|
params: params
|
|
});
|
|
};
|
|
FallbackProvider.prototype.perform = function (method, params) {
|
|
var _this = this;
|
|
var T0 = now();
|
|
var runners = (random_1.shuffled(this.providers)).map(function (provider, i) {
|
|
var weight = _this.weights[i];
|
|
var rid = nextRid++;
|
|
return {
|
|
run: function () {
|
|
var t0 = now();
|
|
var start = t0 - T0;
|
|
_this.emit("debug", {
|
|
action: "request",
|
|
rid: rid,
|
|
backend: { weight: weight, start: start, provider: provider },
|
|
request: { method: method, params: properties_1.deepCopy(params) },
|
|
provider: _this
|
|
});
|
|
return FallbackProvider.doPerform(provider, method, params).then(function (result) {
|
|
var duration = now() - t0;
|
|
_this.emit("debug", {
|
|
action: "response",
|
|
rid: rid,
|
|
backend: { weight: weight, start: start, duration: duration, provider: provider },
|
|
request: { method: method, params: properties_1.deepCopy(params) },
|
|
response: properties_1.deepCopy(result)
|
|
});
|
|
return { weight: weight, result: result };
|
|
}, function (error) {
|
|
var duration = now() - t0;
|
|
_this.emit("debug", {
|
|
action: "response",
|
|
rid: rid,
|
|
backend: { weight: weight, start: start, duration: duration, provider: provider },
|
|
request: { method: method, params: properties_1.deepCopy(params) },
|
|
error: error
|
|
});
|
|
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(function (r) { return r.run(); })).then(function (results) {
|
|
for (var i = 0; i < results.length; i++) {
|
|
var 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(function (resolve, reject) {
|
|
var firstError = null;
|
|
// How much weight is inflight
|
|
var inflightWeight = 0;
|
|
// All results, indexed by the serialized response.
|
|
var results = {};
|
|
var next = function () {
|
|
if (runners.length === 0) {
|
|
return;
|
|
}
|
|
var runner = runners.shift();
|
|
inflightWeight += runner.weight;
|
|
runner.run().then(function (result) {
|
|
if (results === null) {
|
|
return;
|
|
}
|
|
inflightWeight -= runner.weight;
|
|
if (result.error) {
|
|
if (firstError == null) {
|
|
firstError = result.error;
|
|
}
|
|
}
|
|
else {
|
|
var unique = serialize(result.result);
|
|
if (results[unique] == null) {
|
|
results[unique] = [];
|
|
}
|
|
results[unique].push(result);
|
|
// Do any results meet our quroum?
|
|
for (var u in results) {
|
|
var weight = results[u].reduce(function (accum, r) { return (accum + r.weight); }, 0);
|
|
if (weight >= _this.quorum) {
|
|
var result_1 = results[u][0].result;
|
|
_this.emit("debug", "quorum", -1, { weight: weight, result: result_1 });
|
|
resolve(result_1);
|
|
results = null;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
// Out of options; give up
|
|
if (runners.length === 0 && inflightWeight === 0) {
|
|
// @TODO: this might need some more thinking... Maybe only if half
|
|
// of the results contain non-error?
|
|
if (method === "getGasPrice") {
|
|
var values_1 = [];
|
|
Object.keys(results).forEach(function (key) {
|
|
results[key].forEach(function (result) {
|
|
if (!result.result) {
|
|
return;
|
|
}
|
|
values_1.push(result.result);
|
|
});
|
|
});
|
|
values_1.sort(function (a, b) {
|
|
if (a.lt(b)) {
|
|
return -1;
|
|
}
|
|
if (a.gt(b)) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
});
|
|
var index = parseInt(String(values_1.length / 2));
|
|
if (values_1.length % 2) {
|
|
resolve(values_1[index]);
|
|
return;
|
|
}
|
|
resolve(values_1[index - 1].add(values_1[index]).div(2));
|
|
return;
|
|
}
|
|
if (firstError === null) {
|
|
firstError = logger.makeError("failed to meet quorum", logger_1.Logger.errors.SERVER_ERROR, {
|
|
results: Object.keys(results).map(function (u) {
|
|
return {
|
|
method: method,
|
|
params: params,
|
|
result: u,
|
|
weight: results[u].reduce(function (accum, r) { return (accum + r.weight); }, 0)
|
|
};
|
|
})
|
|
});
|
|
}
|
|
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();
|
|
});
|
|
};
|
|
return FallbackProvider;
|
|
}(base_provider_1.BaseProvider));
|
|
exports.FallbackProvider = FallbackProvider;
|