1 | "use strict";
|
2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
4 | return new (P || (P = Promise))(function (resolve, reject) {
|
5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
8 | step((generator = generator.apply(thisArg, _arguments || [])).next());
|
9 | });
|
10 | };
|
11 | import { Provider } from "@ethersproject/abstract-provider";
|
12 | import { BigNumber } from "@ethersproject/bignumber";
|
13 | import { isHexString } from "@ethersproject/bytes";
|
14 | import { deepCopy, defineReadOnly, shallowCopy } from "@ethersproject/properties";
|
15 | import { shuffled } from "@ethersproject/random";
|
16 | import { poll } from "@ethersproject/web";
|
17 | import { BaseProvider } from "./base-provider";
|
18 | import { isCommunityResource } from "./formatter";
|
19 | import { Logger } from "@ethersproject/logger";
|
20 | import { version } from "./_version";
|
21 | const logger = new Logger(version);
|
22 | function now() { return (new Date()).getTime(); }
|
23 |
|
24 |
|
25 | function checkNetworks(networks) {
|
26 | let result = null;
|
27 | for (let i = 0; i < networks.length; i++) {
|
28 | const network = networks[i];
|
29 |
|
30 | if (network == null) {
|
31 | return null;
|
32 | }
|
33 | if (result) {
|
34 |
|
35 | if (!(result.name === network.name && result.chainId === network.chainId &&
|
36 | ((result.ensAddress === network.ensAddress) || (result.ensAddress == null && network.ensAddress == null)))) {
|
37 | logger.throwArgumentError("provider mismatch", "networks", networks);
|
38 | }
|
39 | }
|
40 | else {
|
41 | result = network;
|
42 | }
|
43 | }
|
44 | return result;
|
45 | }
|
46 | function median(values, maxDelta) {
|
47 | values = values.slice().sort();
|
48 | const middle = Math.floor(values.length / 2);
|
49 |
|
50 | if (values.length % 2) {
|
51 | return values[middle];
|
52 | }
|
53 |
|
54 | const a = values[middle - 1], b = values[middle];
|
55 | if (maxDelta != null && Math.abs(a - b) > maxDelta) {
|
56 | return null;
|
57 | }
|
58 | return (a + b) / 2;
|
59 | }
|
60 | function serialize(value) {
|
61 | if (value === null) {
|
62 | return "null";
|
63 | }
|
64 | else if (typeof (value) === "number" || typeof (value) === "boolean") {
|
65 | return JSON.stringify(value);
|
66 | }
|
67 | else if (typeof (value) === "string") {
|
68 | return value;
|
69 | }
|
70 | else if (BigNumber.isBigNumber(value)) {
|
71 | return value.toString();
|
72 | }
|
73 | else if (Array.isArray(value)) {
|
74 | return JSON.stringify(value.map((i) => serialize(i)));
|
75 | }
|
76 | else if (typeof (value) === "object") {
|
77 | const keys = Object.keys(value);
|
78 | keys.sort();
|
79 | return "{" + keys.map((key) => {
|
80 | let v = value[key];
|
81 | if (typeof (v) === "function") {
|
82 | v = "[function]";
|
83 | }
|
84 | else {
|
85 | v = serialize(v);
|
86 | }
|
87 | return JSON.stringify(key) + ":" + v;
|
88 | }).join(",") + "}";
|
89 | }
|
90 | throw new Error("unknown value type: " + typeof (value));
|
91 | }
|
92 |
|
93 | let nextRid = 1;
|
94 | ;
|
95 | function stall(duration) {
|
96 | let cancel = null;
|
97 | let timer = null;
|
98 | let promise = (new Promise((resolve) => {
|
99 | cancel = function () {
|
100 | if (timer) {
|
101 | clearTimeout(timer);
|
102 | timer = null;
|
103 | }
|
104 | resolve();
|
105 | };
|
106 | timer = setTimeout(cancel, duration);
|
107 | }));
|
108 | const wait = (func) => {
|
109 | promise = promise.then(func);
|
110 | return promise;
|
111 | };
|
112 | function getPromise() {
|
113 | return promise;
|
114 | }
|
115 | return { cancel, getPromise, wait };
|
116 | }
|
117 | const ForwardErrors = [
|
118 | Logger.errors.CALL_EXCEPTION,
|
119 | Logger.errors.INSUFFICIENT_FUNDS,
|
120 | Logger.errors.NONCE_EXPIRED,
|
121 | Logger.errors.REPLACEMENT_UNDERPRICED,
|
122 | Logger.errors.UNPREDICTABLE_GAS_LIMIT
|
123 | ];
|
124 | const ForwardProperties = [
|
125 | "address",
|
126 | "args",
|
127 | "errorArgs",
|
128 | "errorSignature",
|
129 | "method",
|
130 | "transaction",
|
131 | ];
|
132 | ;
|
133 | function exposeDebugConfig(config, now) {
|
134 | const result = {
|
135 | weight: config.weight
|
136 | };
|
137 | Object.defineProperty(result, "provider", { get: () => config.provider });
|
138 | if (config.start) {
|
139 | result.start = config.start;
|
140 | }
|
141 | if (now) {
|
142 | result.duration = (now - config.start);
|
143 | }
|
144 | if (config.done) {
|
145 | if (config.error) {
|
146 | result.error = config.error;
|
147 | }
|
148 | else {
|
149 | result.result = config.result || null;
|
150 | }
|
151 | }
|
152 | return result;
|
153 | }
|
154 | function normalizedTally(normalize, quorum) {
|
155 | return function (configs) {
|
156 |
|
157 | const tally = {};
|
158 | configs.forEach((c) => {
|
159 | const value = normalize(c.result);
|
160 | if (!tally[value]) {
|
161 | tally[value] = { count: 0, result: c.result };
|
162 | }
|
163 | tally[value].count++;
|
164 | });
|
165 |
|
166 | const keys = Object.keys(tally);
|
167 | for (let i = 0; i < keys.length; i++) {
|
168 | const check = tally[keys[i]];
|
169 | if (check.count >= quorum) {
|
170 | return check.result;
|
171 | }
|
172 | }
|
173 |
|
174 | return undefined;
|
175 | };
|
176 | }
|
177 | function getProcessFunc(provider, method, params) {
|
178 | let normalize = serialize;
|
179 | switch (method) {
|
180 | case "getBlockNumber":
|
181 |
|
182 |
|
183 |
|
184 |
|
185 | return function (configs) {
|
186 | const values = configs.map((c) => c.result);
|
187 |
|
188 | let blockNumber = median(configs.map((c) => c.result), 2);
|
189 | if (blockNumber == null) {
|
190 | return undefined;
|
191 | }
|
192 | blockNumber = Math.ceil(blockNumber);
|
193 |
|
194 | if (values.indexOf(blockNumber + 1) >= 0) {
|
195 | blockNumber++;
|
196 | }
|
197 |
|
198 | if (blockNumber >= provider._highestBlockNumber) {
|
199 | provider._highestBlockNumber = blockNumber;
|
200 | }
|
201 | return provider._highestBlockNumber;
|
202 | };
|
203 | case "getGasPrice":
|
204 |
|
205 |
|
206 |
|
207 | return function (configs) {
|
208 | const values = configs.map((c) => c.result);
|
209 | values.sort();
|
210 | return values[Math.floor(values.length / 2)];
|
211 | };
|
212 | case "getEtherPrice":
|
213 |
|
214 |
|
215 | return function (configs) {
|
216 | return median(configs.map((c) => c.result));
|
217 | };
|
218 |
|
219 | case "getBalance":
|
220 | case "getTransactionCount":
|
221 | case "getCode":
|
222 | case "getStorageAt":
|
223 | case "call":
|
224 | case "estimateGas":
|
225 | case "getLogs":
|
226 | break;
|
227 |
|
228 | case "getTransaction":
|
229 | case "getTransactionReceipt":
|
230 | normalize = function (tx) {
|
231 | if (tx == null) {
|
232 | return null;
|
233 | }
|
234 | tx = shallowCopy(tx);
|
235 | tx.confirmations = -1;
|
236 | return serialize(tx);
|
237 | };
|
238 | break;
|
239 |
|
240 | case "getBlock":
|
241 |
|
242 | if (params.includeTransactions) {
|
243 | normalize = function (block) {
|
244 | if (block == null) {
|
245 | return null;
|
246 | }
|
247 | block = shallowCopy(block);
|
248 | block.transactions = block.transactions.map((tx) => {
|
249 | tx = shallowCopy(tx);
|
250 | tx.confirmations = -1;
|
251 | return tx;
|
252 | });
|
253 | return serialize(block);
|
254 | };
|
255 | }
|
256 | else {
|
257 | normalize = function (block) {
|
258 | if (block == null) {
|
259 | return null;
|
260 | }
|
261 | return serialize(block);
|
262 | };
|
263 | }
|
264 | break;
|
265 | default:
|
266 | throw new Error("unknown method: " + method);
|
267 | }
|
268 |
|
269 |
|
270 | return normalizedTally(normalize, provider.quorum);
|
271 | }
|
272 |
|
273 |
|
274 | function waitForSync(config, blockNumber) {
|
275 | return __awaiter(this, void 0, void 0, function* () {
|
276 | const provider = (config.provider);
|
277 | if ((provider.blockNumber != null && provider.blockNumber >= blockNumber) || blockNumber === -1) {
|
278 | return provider;
|
279 | }
|
280 | return poll(() => {
|
281 | return new Promise((resolve, reject) => {
|
282 | setTimeout(function () {
|
283 |
|
284 | if (provider.blockNumber >= blockNumber) {
|
285 | return resolve(provider);
|
286 | }
|
287 |
|
288 | if (config.cancelled) {
|
289 | return resolve(null);
|
290 | }
|
291 |
|
292 | return resolve(undefined);
|
293 | }, 0);
|
294 | });
|
295 | }, { oncePoll: provider });
|
296 | });
|
297 | }
|
298 | function getRunner(config, currentBlockNumber, method, params) {
|
299 | return __awaiter(this, void 0, void 0, function* () {
|
300 | let provider = config.provider;
|
301 | switch (method) {
|
302 | case "getBlockNumber":
|
303 | case "getGasPrice":
|
304 | return provider[method]();
|
305 | case "getEtherPrice":
|
306 | if (provider.getEtherPrice) {
|
307 | return provider.getEtherPrice();
|
308 | }
|
309 | break;
|
310 | case "getBalance":
|
311 | case "getTransactionCount":
|
312 | case "getCode":
|
313 | if (params.blockTag && isHexString(params.blockTag)) {
|
314 | provider = yield waitForSync(config, currentBlockNumber);
|
315 | }
|
316 | return provider[method](params.address, params.blockTag || "latest");
|
317 | case "getStorageAt":
|
318 | if (params.blockTag && isHexString(params.blockTag)) {
|
319 | provider = yield waitForSync(config, currentBlockNumber);
|
320 | }
|
321 | return provider.getStorageAt(params.address, params.position, params.blockTag || "latest");
|
322 | case "getBlock":
|
323 | if (params.blockTag && isHexString(params.blockTag)) {
|
324 | provider = yield waitForSync(config, currentBlockNumber);
|
325 | }
|
326 | return provider[(params.includeTransactions ? "getBlockWithTransactions" : "getBlock")](params.blockTag || params.blockHash);
|
327 | case "call":
|
328 | case "estimateGas":
|
329 | if (params.blockTag && isHexString(params.blockTag)) {
|
330 | provider = yield waitForSync(config, currentBlockNumber);
|
331 | }
|
332 | return provider[method](params.transaction);
|
333 | case "getTransaction":
|
334 | case "getTransactionReceipt":
|
335 | return provider[method](params.transactionHash);
|
336 | case "getLogs": {
|
337 | let filter = params.filter;
|
338 | if ((filter.fromBlock && isHexString(filter.fromBlock)) || (filter.toBlock && isHexString(filter.toBlock))) {
|
339 | provider = yield waitForSync(config, currentBlockNumber);
|
340 | }
|
341 | return provider.getLogs(filter);
|
342 | }
|
343 | }
|
344 | return logger.throwError("unknown method error", Logger.errors.UNKNOWN_ERROR, {
|
345 | method: method,
|
346 | params: params
|
347 | });
|
348 | });
|
349 | }
|
350 | export class FallbackProvider extends BaseProvider {
|
351 | constructor(providers, quorum) {
|
352 | if (providers.length === 0) {
|
353 | logger.throwArgumentError("missing providers", "providers", providers);
|
354 | }
|
355 | const providerConfigs = providers.map((configOrProvider, index) => {
|
356 | if (Provider.isProvider(configOrProvider)) {
|
357 | const stallTimeout = isCommunityResource(configOrProvider) ? 2000 : 750;
|
358 | const priority = 1;
|
359 | return Object.freeze({ provider: configOrProvider, weight: 1, stallTimeout, priority });
|
360 | }
|
361 | const config = shallowCopy(configOrProvider);
|
362 | if (config.priority == null) {
|
363 | config.priority = 1;
|
364 | }
|
365 | if (config.stallTimeout == null) {
|
366 | config.stallTimeout = isCommunityResource(configOrProvider) ? 2000 : 750;
|
367 | }
|
368 | if (config.weight == null) {
|
369 | config.weight = 1;
|
370 | }
|
371 | const weight = config.weight;
|
372 | if (weight % 1 || weight > 512 || weight < 1) {
|
373 | logger.throwArgumentError("invalid weight; must be integer in [1, 512]", `providers[${index}].weight`, weight);
|
374 | }
|
375 | return Object.freeze(config);
|
376 | });
|
377 | const total = providerConfigs.reduce((accum, c) => (accum + c.weight), 0);
|
378 | if (quorum == null) {
|
379 | quorum = total / 2;
|
380 | }
|
381 | else if (quorum > total) {
|
382 | logger.throwArgumentError("quorum will always fail; larger than total weight", "quorum", quorum);
|
383 | }
|
384 |
|
385 | let networkOrReady = checkNetworks(providerConfigs.map((c) => (c.provider).network));
|
386 |
|
387 | if (networkOrReady == null) {
|
388 | networkOrReady = new Promise((resolve, reject) => {
|
389 | setTimeout(() => {
|
390 | this.detectNetwork().then(resolve, reject);
|
391 | }, 0);
|
392 | });
|
393 | }
|
394 | super(networkOrReady);
|
395 |
|
396 | defineReadOnly(this, "providerConfigs", Object.freeze(providerConfigs));
|
397 | defineReadOnly(this, "quorum", quorum);
|
398 | this._highestBlockNumber = -1;
|
399 | }
|
400 | detectNetwork() {
|
401 | return __awaiter(this, void 0, void 0, function* () {
|
402 | const networks = yield Promise.all(this.providerConfigs.map((c) => c.provider.getNetwork()));
|
403 | return checkNetworks(networks);
|
404 | });
|
405 | }
|
406 | perform(method, params) {
|
407 | return __awaiter(this, void 0, void 0, function* () {
|
408 |
|
409 | if (method === "sendTransaction") {
|
410 | const results = yield Promise.all(this.providerConfigs.map((c) => {
|
411 | return c.provider.sendTransaction(params.signedTransaction).then((result) => {
|
412 | return result.hash;
|
413 | }, (error) => {
|
414 | return error;
|
415 | });
|
416 | }));
|
417 |
|
418 | for (let i = 0; i < results.length; i++) {
|
419 | const result = results[i];
|
420 | if (typeof (result) === "string") {
|
421 | return result;
|
422 | }
|
423 | }
|
424 |
|
425 | throw results[0];
|
426 | }
|
427 |
|
428 |
|
429 | if (this._highestBlockNumber === -1 && method !== "getBlockNumber") {
|
430 | yield this.getBlockNumber();
|
431 | }
|
432 | const processFunc = getProcessFunc(this, method, params);
|
433 |
|
434 |
|
435 | const configs = shuffled(this.providerConfigs.map(shallowCopy));
|
436 | configs.sort((a, b) => (a.priority - b.priority));
|
437 | const currentBlockNumber = this._highestBlockNumber;
|
438 | let i = 0;
|
439 | let first = true;
|
440 | while (true) {
|
441 | const t0 = now();
|
442 |
|
443 | let inflightWeight = configs.filter((c) => (c.runner && ((t0 - c.start) < c.stallTimeout)))
|
444 | .reduce((accum, c) => (accum + c.weight), 0);
|
445 |
|
446 | while (inflightWeight < this.quorum && i < configs.length) {
|
447 | const config = configs[i++];
|
448 | const rid = nextRid++;
|
449 | config.start = now();
|
450 | config.staller = stall(config.stallTimeout);
|
451 | config.staller.wait(() => { config.staller = null; });
|
452 | config.runner = getRunner(config, currentBlockNumber, method, params).then((result) => {
|
453 | config.done = true;
|
454 | config.result = result;
|
455 | if (this.listenerCount("debug")) {
|
456 | this.emit("debug", {
|
457 | action: "request",
|
458 | rid: rid,
|
459 | backend: exposeDebugConfig(config, now()),
|
460 | request: { method: method, params: deepCopy(params) },
|
461 | provider: this
|
462 | });
|
463 | }
|
464 | }, (error) => {
|
465 | config.done = true;
|
466 | config.error = error;
|
467 | if (this.listenerCount("debug")) {
|
468 | this.emit("debug", {
|
469 | action: "request",
|
470 | rid: rid,
|
471 | backend: exposeDebugConfig(config, now()),
|
472 | request: { method: method, params: deepCopy(params) },
|
473 | provider: this
|
474 | });
|
475 | }
|
476 | });
|
477 | if (this.listenerCount("debug")) {
|
478 | this.emit("debug", {
|
479 | action: "request",
|
480 | rid: rid,
|
481 | backend: exposeDebugConfig(config, null),
|
482 | request: { method: method, params: deepCopy(params) },
|
483 | provider: this
|
484 | });
|
485 | }
|
486 | inflightWeight += config.weight;
|
487 | }
|
488 |
|
489 | const waiting = [];
|
490 | configs.forEach((c) => {
|
491 | if (c.done || !c.runner) {
|
492 | return;
|
493 | }
|
494 | waiting.push(c.runner);
|
495 | if (c.staller) {
|
496 | waiting.push(c.staller.getPromise());
|
497 | }
|
498 | });
|
499 | if (waiting.length) {
|
500 | yield Promise.race(waiting);
|
501 | }
|
502 |
|
503 |
|
504 | const results = configs.filter((c) => (c.done && c.error == null));
|
505 | if (results.length >= this.quorum) {
|
506 | const result = processFunc(results);
|
507 | if (result !== undefined) {
|
508 |
|
509 | configs.forEach(c => {
|
510 | if (c.staller) {
|
511 | c.staller.cancel();
|
512 | }
|
513 | c.cancelled = true;
|
514 | });
|
515 | return result;
|
516 | }
|
517 | if (!first) {
|
518 | yield stall(100).getPromise();
|
519 | }
|
520 | first = false;
|
521 | }
|
522 |
|
523 | const errors = configs.reduce((accum, c) => {
|
524 | if (!c.done || c.error == null) {
|
525 | return accum;
|
526 | }
|
527 | const code = (c.error).code;
|
528 | if (ForwardErrors.indexOf(code) >= 0) {
|
529 | if (!accum[code]) {
|
530 | accum[code] = { error: c.error, weight: 0 };
|
531 | }
|
532 | accum[code].weight += c.weight;
|
533 | }
|
534 | return accum;
|
535 | }, ({}));
|
536 | Object.keys(errors).forEach((errorCode) => {
|
537 | const tally = errors[errorCode];
|
538 | if (tally.weight < this.quorum) {
|
539 | return;
|
540 | }
|
541 |
|
542 | configs.forEach(c => {
|
543 | if (c.staller) {
|
544 | c.staller.cancel();
|
545 | }
|
546 | c.cancelled = true;
|
547 | });
|
548 | const e = (tally.error);
|
549 | const props = {};
|
550 | ForwardProperties.forEach((name) => {
|
551 | if (e[name] == null) {
|
552 | return;
|
553 | }
|
554 | props[name] = e[name];
|
555 | });
|
556 | logger.throwError(e.reason || e.message, errorCode, props);
|
557 | });
|
558 |
|
559 | if (configs.filter((c) => !c.done).length === 0) {
|
560 | break;
|
561 | }
|
562 | }
|
563 |
|
564 | configs.forEach(c => {
|
565 | if (c.staller) {
|
566 | c.staller.cancel();
|
567 | }
|
568 | c.cancelled = true;
|
569 | });
|
570 | return logger.throwError("failed to meet quorum", Logger.errors.SERVER_ERROR, {
|
571 | method: method,
|
572 | params: params,
|
573 |
|
574 |
|
575 | results: configs.map((c) => exposeDebugConfig(c)),
|
576 | provider: this
|
577 | });
|
578 | });
|
579 | }
|
580 | }
|
581 |
|
\ | No newline at end of file |