parent
93fb138809
commit
e2485b8ef9
95
src.ts/_tests/test-providers-fallback.ts
Normal file
95
src.ts/_tests/test-providers-fallback.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import assert from "assert";
|
||||||
|
|
||||||
|
import {
|
||||||
|
isError, makeError,
|
||||||
|
|
||||||
|
AbstractProvider, FallbackProvider, Network,
|
||||||
|
} from "../index.js";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
PerformActionRequest
|
||||||
|
} from "../index.js";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const network = Network.from("mainnet");
|
||||||
|
|
||||||
|
function stall(duration: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => { setTimeout(resolve, duration); });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type Performer = (req: PerformActionRequest) => Promise<any>;
|
||||||
|
|
||||||
|
export class MockProvider extends AbstractProvider {
|
||||||
|
readonly _perform: Performer;
|
||||||
|
|
||||||
|
constructor(perform: Performer) {
|
||||||
|
super(network, { cacheTimeout: -1 });
|
||||||
|
this._perform = perform;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _detectNetwork(): Promise<Network> { return network; }
|
||||||
|
|
||||||
|
async perform(req: PerformActionRequest): Promise<any> {
|
||||||
|
return await this._perform(req);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Test Fallback broadcast", function() {
|
||||||
|
|
||||||
|
const txHash = "0x33017397ef7c7943dee3b422aec52b0a210de58d73d49c1b3ce455970f01c83a";
|
||||||
|
|
||||||
|
async function test(actions: Array<{ timeout: number, error?: Error }>): Promise<any> {
|
||||||
|
// https://sepolia.etherscan.io/tx/0x33017397ef7c7943dee3b422aec52b0a210de58d73d49c1b3ce455970f01c83a
|
||||||
|
const tx = "0x02f87683aa36a7048459682f00845d899ef982520894b5bdaa442bb34f27e793861c456cd5bdc527ac8c89056bc75e2d6310000080c001a07503893743e94445b2361a444343757e6f59d52e19e9b3f65eb138d802eaa972a06e4e9bc10ff55474f9aac0a4c284733b4195cb7b273de5e7465ce75a168e0c38";
|
||||||
|
|
||||||
|
const providers: Array<MockProvider> = actions.map(({ timeout, error }) => {
|
||||||
|
return new MockProvider(async (r) => {
|
||||||
|
if (r.method === "getBlockNumber") { return 1; }
|
||||||
|
if (r.method === "broadcastTransaction") {
|
||||||
|
await stall(timeout);
|
||||||
|
if (error) { throw error; }
|
||||||
|
return txHash;
|
||||||
|
}
|
||||||
|
throw new Error(`unhandled method: ${ r.method }`);
|
||||||
|
});
|
||||||
|
});;
|
||||||
|
|
||||||
|
const provider = new FallbackProvider(providers);
|
||||||
|
return await provider.broadcastTransaction(tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
it("picks late non-failed broadcasts", async function() {
|
||||||
|
const result = await test([
|
||||||
|
{ timeout: 200, error: makeError("already seen", "UNKNOWN_ERROR") },
|
||||||
|
{ timeout: 4000, error: makeError("already seen", "UNKNOWN_ERROR") },
|
||||||
|
{ timeout: 400 },
|
||||||
|
]);
|
||||||
|
assert(result.hash === txHash, "result.hash === txHash");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("picks late non-failed broadcasts with quorum-met red-herrings", async function() {
|
||||||
|
const result = await test([
|
||||||
|
{ timeout: 200, error: makeError("bad nonce", "NONCE_EXPIRED") },
|
||||||
|
{ timeout: 400, error: makeError("bad nonce", "NONCE_EXPIRED") },
|
||||||
|
{ timeout: 1000 },
|
||||||
|
]);
|
||||||
|
assert(result.hash === txHash, "result.hash === txHash");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("insufficient funds short-circuit broadcast", async function() {
|
||||||
|
await assert.rejects(async function() {
|
||||||
|
const result = await test([
|
||||||
|
{ timeout: 200, error: makeError("is broke", "INSUFFICIENT_FUNDS") },
|
||||||
|
{ timeout: 400, error: makeError("is broke", "INSUFFICIENT_FUNDS") },
|
||||||
|
{ timeout: 800 },
|
||||||
|
{ timeout: 1000 },
|
||||||
|
]);
|
||||||
|
console.log(result);
|
||||||
|
}, function(error: unknown) {
|
||||||
|
assert(isError(error, "INSUFFICIENT_FUNDS"));
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -5,7 +5,7 @@
|
|||||||
* @_section: api/providers/fallback-provider:Fallback Provider [about-fallback-provider]
|
* @_section: api/providers/fallback-provider:Fallback Provider [about-fallback-provider]
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
getBigInt, getNumber, assert, assertArgument
|
assert, assertArgument, getBigInt, getNumber, isError
|
||||||
} from "../utils/index.js";
|
} from "../utils/index.js";
|
||||||
|
|
||||||
import { AbstractProvider } from "./abstract-provider.js";
|
import { AbstractProvider } from "./abstract-provider.js";
|
||||||
@ -707,16 +707,46 @@ export class FallbackProvider extends AbstractProvider {
|
|||||||
// a cost on the user, so spamming is safe-ish. Just send it to
|
// a cost on the user, so spamming is safe-ish. Just send it to
|
||||||
// every backend.
|
// every backend.
|
||||||
if (req.method === "broadcastTransaction") {
|
if (req.method === "broadcastTransaction") {
|
||||||
const results = await Promise.all(this.#configs.map(async ({ provider, weight }) => {
|
// Once any broadcast provides a positive result, use it. No
|
||||||
|
// need to wait for anyone else
|
||||||
|
const results: Array<null | TallyResult> = this.#configs.map((c) => null);
|
||||||
|
const broadcasts = this.#configs.map(async ({ provider, weight }, index) => {
|
||||||
try {
|
try {
|
||||||
const result = await provider._perform(req);
|
const result = await provider._perform(req);
|
||||||
return Object.assign(normalizeResult({ result }), { weight });
|
results[index] = Object.assign(normalizeResult({ result }), { weight });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return Object.assign(normalizeResult({ error }), { weight });
|
results[index] = Object.assign(normalizeResult({ error }), { weight });
|
||||||
}
|
}
|
||||||
}));
|
});
|
||||||
|
|
||||||
const result = getAnyResult(this.quorum, results);
|
// As each promise finishes...
|
||||||
|
while (true) {
|
||||||
|
// Check for a valid broadcast result
|
||||||
|
const done = <Array<any>>results.filter((r) => (r != null));
|
||||||
|
for (const { value } of done) {
|
||||||
|
if (!(value instanceof Error)) { return value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a legit broadcast error (one which we cannot
|
||||||
|
// recover from; some nodes may return the following red
|
||||||
|
// herring events:
|
||||||
|
// - alredy seend (UNKNOWN_ERROR)
|
||||||
|
// - NONCE_EXPIRED
|
||||||
|
// - REPLACEMENT_UNDERPRICED
|
||||||
|
const result = checkQuorum(this.quorum, <Array<any>>results.filter((r) => (r != null)));
|
||||||
|
if (isError(result, "INSUFFICIENT_FUNDS")) {
|
||||||
|
throw result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kick off the next provider (if any)
|
||||||
|
const waiting = broadcasts.filter((b, i) => (results[i] == null));
|
||||||
|
if (waiting.length === 0) { break; }
|
||||||
|
await Promise.race(waiting);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use standard quorum results; any result was returned above,
|
||||||
|
// so this will find any error that met quorum if any
|
||||||
|
const result = getAnyResult(this.quorum, <Array<any>>results);
|
||||||
assert(result !== undefined, "problem multi-broadcasting", "SERVER_ERROR", {
|
assert(result !== undefined, "problem multi-broadcasting", "SERVER_ERROR", {
|
||||||
request: "%sub-requests",
|
request: "%sub-requests",
|
||||||
info: { request: req, results: results.map(stringify) }
|
info: { request: req, results: results.map(stringify) }
|
||||||
|
Loading…
Reference in New Issue
Block a user