All files / ethers.js/src.ts/_tests test-providers-fallback.ts

96.27% Statements 155/161
84.37% Branches 27/32
87.5% Functions 7/8
96.27% Lines 155/161

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 1621x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 11x 11x 11x 1x 1x 1x 1x 1x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x     12x 1x 1x 1x 1x 1x 1x 3x 3x 3x 3x 10x 26x 10x 10x 10x 4x 4x   10x 3x 3x 3x 3x 3x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x   1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 3x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x     2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 1x 1x 1x 1x 1x  
import assert from "assert";
 
import {
    isError, makeError,
 
    AbstractProvider, FallbackProvider, Network,
    ZeroAddress
} 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.ok(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.ok(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.ok(isError(error, "INSUFFICIENT_FUNDS"));
            return true;
        });
    });
});
 
describe("Test Inflight Quorum", function() {
    // Fires the %%actions%% as providers which will delay before returning,
    // and returns an array of arrays, where each sub-array indicates which
    // providers were inflight at once.
    async function test(actions: Array<{ delay: number, stallTimeout: number, priority: number, weight: number }>, quorum: number): Promise<Array<Array<number>>> {
        const inflights: Array<Array<number>> = [ [ ] ];
 
        const configs = actions.map(({ delay, stallTimeout, priority, weight }, index) => ({
            provider: new MockProvider(async (r) => {
                if (r.method === "getBlockNumber") { return 1; }
                if (r.method === "getBalance") {
                    // Add this as inflight
                    let last = inflights.pop();
                    if (last == null) { throw new Error("no elements"); }
                    inflights.push(last);
                    last = last.slice();
                    last.push(index);
                    inflights.push(last);
 
                    // Do the thing
                    await stall(delay);
 
                    // Remove as inflight
                    last = inflights.pop();
                    if (last == null) { throw new Error("no elements"); }
                    inflights.push(last);
                    last = last.filter((v) => (v !== index));
                    inflights.push(last);
 
                    return 0;
                }
                console.log(r);
                throw new Error(`unhandled method: ${ r.method }`);
            }),
            stallTimeout, priority, weight
        }));
 
        const provider = new FallbackProvider(configs, network, {
            cacheTimeout: -1, pollingInterval: 100,
            quorum
        });
        await provider.getBalance(ZeroAddress);
 
        return inflights;
    }
 
    // See: #4298
    it("applies weights against inflight requests", async function() {
        this.timeout(2000);
 
        const inflights = await test([
            { delay: 50, stallTimeout: 1000, priority: 1, weight: 2 },
            { delay: 50, stallTimeout: 1000, priority: 1, weight: 2 },
        ], 2);
 
        // Make sure there is never more than 1 inflight provider at once
        for (const running of inflights) {
            assert.ok(running.length <= 1, `too many inflight requests: ${ JSON.stringify(inflights) }`);
        }
    });
 
    // @TODO: add lots more tests, checking on priority, weight and stall
    //        configurations
});