tests: added initial provider tests.

This commit is contained in:
Richard Moore 2022-09-15 22:20:48 -04:00
parent 74f7967be6
commit d7c6252521
5 changed files with 439 additions and 0 deletions

View File

@ -0,0 +1,116 @@
import {
AlchemyProvider,
AnkrProvider,
CloudflareProvider,
EtherscanProvider,
InfuraProvider,
// PocketProvider,
FallbackProvider,
} from "../index.js";
import type { AbstractProvider } from "../index.js";
interface ProviderCreator {
name: string;
networks: Array<string>;
create: (network: string) => null | AbstractProvider;
};
const ethNetworks = [ "default", "homestead", "rinkeby", "ropsten", "goerli" ];
//const maticNetworks = [ "matic", "maticmum" ];
const ProviderCreators: Array<ProviderCreator> = [
{
name: "AlchemyProvider",
networks: ethNetworks,
create: function(network: string) {
return new AlchemyProvider(network, "YrPw6SWb20vJDRFkhWq8aKnTQ8JRNRHM");
}
},
{
name: "AnkrProvider",
networks: ethNetworks.concat([ "matic", "arbitrum" ]),
create: function(network: string) {
return new AnkrProvider(network);
}
},
{
name: "CloudflareProvider",
networks: [ "default", "homestead" ],
create: function(network: string) {
return new CloudflareProvider(network);
}
},
{
name: "EtherscanProvider",
networks: ethNetworks,
create: function(network: string) {
return new EtherscanProvider(network, "FPFGK6JSW2UHJJ2666FG93KP7WC999MNW7");
}
},
{
name: "InfuraProvider",
networks: ethNetworks,
create: function(network: string) {
return new InfuraProvider(network, "49a0efa3aaee4fd99797bfa94d8ce2f1");
}
},
/*
{
name: "PocketProvider",
networks: ethNetworks,
create: function(network: string) {
const apiKeys: Record<string, string> = {
homestead: "6004bcd10040261633ade990",
ropsten: "6004bd4d0040261633ade991",
rinkeby: "6004bda20040261633ade994",
goerli: "6004bd860040261633ade992",
};
return new PocketProvider(network, apiKeys[network]);
}
},
*/
{
name: "FallbackProvider",
networks: ethNetworks,
create: function(network: string) {
const providers: Array<AbstractProvider> = [];
for (const creator of ProviderCreators) {
if (creator.name === "FallbackProvider") { continue; }
if (creator.networks.indexOf(network) >= 0) {
const provider = creator.create(network);
if (provider) { providers.push(provider); }
}
}
return new FallbackProvider(providers);
}
},
];
export const providerNames = Object.freeze(ProviderCreators.map((c) => (c.name)));
function getCreator(provider: string): null | ProviderCreator {
const creators = ProviderCreators.filter((c) => (c.name === provider));
if (creators.length === 1) { return creators[0]; }
return null;
}
export function getProviderNetworks(provider: string): Array<string> {
const creator = getCreator(provider);
if (creator) { return creator.networks; }
return [ ];
}
export function getProvider(provider: string, network: string): null | AbstractProvider {
const creator = getCreator(provider);
if (creator) { return creator.create(network); }
return null;
}
export function connect(network: string): AbstractProvider {
const provider = getProvider("InfuraProvider", network);
if (provider == null) { throw new Error(`could not connect to ${ network }`); }
return provider;
}

View File

@ -0,0 +1,20 @@
import assert from "assert";
import { connect } from "./create-provider.js";
describe("Test EIP-2544 ENS wildcards", function() {
const provider = connect("ropsten");
it("Resolves recursively", async function() {
const resolver = await provider.getResolver("ricmoose.hatch.eth");
assert.ok(resolver, "failed to get resolver");
assert.equal(resolver.address, "0x8fc4C380c5d539aE631daF3Ca9182b40FB21D1ae", "address");
assert.equal(await resolver.supportsWildcard(), true, "supportsWildcard()");
// Test pass-through avatar
assert.equal(await resolver.getAvatar(), "https:/\/static.ricmoo.com/uploads/profile-06cb9c3031c9.jpg", "getAvatar()");
assert.equal(await resolver.getAddress(), "0x4FaBE0A3a4DDd9968A7b4565184Ad0eFA7BE5411", "getAddress()");
});
});

View File

@ -0,0 +1,30 @@
import assert from "assert";
import { connect } from "./create-provider.js";
describe("Resolve ENS avatar", function() {
[
{ title: "data", name: "data-avatar.tests.eth", value: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAMAAACeL25MAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyVpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDYuMC1jMDAyIDc5LjE2NDQ4OCwgMjAyMC8wNy8xMC0yMjowNjo1MyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDIyLjAgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NUQ4NTEyNUIyOEIwMTFFQzg0NTBDNTU2RDk1NTA5NzgiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NUQ4NTEyNUMyOEIwMTFFQzg0NTBDNTU2RDk1NTA5NzgiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo1RDg1MTI1OTI4QjAxMUVDODQ1MEM1NTZEOTU1MDk3OCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo1RDg1MTI1QTI4QjAxMUVDODQ1MEM1NTZEOTU1MDk3OCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PkbM0uMAAAAGUExURQAA/wAAAHtivz4AAAAOSURBVHjaYmDABAABBgAAFAABaEkyYwAAAABJRU5ErkJggg==" },
{ title: "ipfs", name: "ipfs-avatar.tests.eth", value: "https:/\/gateway.ipfs.io/ipfs/QmQsQgpda6JAYkFoeVcj5iPbwV3xRcvaiXv3bhp1VuYUqw" },
{ title: "url", name: "url-avatar.tests.eth", value: "https:/\/ethers.org/static/logo.png" },
].forEach((test) => {
it(`Resolves avatar for ${ test.title }`, async function() {
this.timeout(60000);
const provider = connect("ropsten");
const avatar = await provider.getAvatar(test.name);
assert.equal(test.value, avatar, "avatar url");
});
});
[
{ title: "ERC-1155", name: "nick.eth", value: "https:/\/lh3.googleusercontent.com/hKHZTZSTmcznonu8I6xcVZio1IF76fq0XmcxnvUykC-FGuVJ75UPdLDlKJsfgVXH9wOSmkyHw0C39VAYtsGyxT7WNybjQ6s3fM3macE" },
{ title: "ERC-721", name: "brantly.eth", value: "https:/\/api.wrappedpunks.com/images/punks/2430.png" }
].forEach((test) => {
it(`Resolves avatar for ${ test.title }`, async function() {
this.timeout(60000);
const provider = connect("homestead");
const avatar = await provider.getAvatar(test.name);
assert.equal(avatar, test.value, "avatar url");
});
});
});

View File

@ -0,0 +1,179 @@
import assert from "assert";
import {
concat, dataLength,
keccak256,
toArray,
isCallException, isError
} from "../index.js";
import { connect } from "./create-provider.js";
describe("Test CCIP execution", function() {
// This matches the verify method in the Solidity contract against the
// processed data from the endpoint
const verify = function(sender: string, data: string, result: string): void {
const check = concat([
toArray(dataLength(sender)), sender,
toArray(dataLength(data)), data
]);
assert.equal(result, keccak256(check), "response is equal");
}
const address = "0xAe375B05A08204C809b3cA67C680765661998886";
const calldata = "0x1234";
it("testGet passes under normal operation", async function() {
this.timeout(60000);
const provider = connect("ropsten");
// testGet(bytes callData = "0x1234")
const tx = {
to: address, enableCcipRead: true,
data: "0xa5f3271e000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000"
}
const result = await provider.call(tx);
verify(address, calldata, result);
});
it("testGet should fail with CCIP not explicitly enabled by overrides", async function() {
this.timeout(60000);
const provider = connect("ropsten");
// testGet(bytes callData = "0x1234")
const tx = {
to: address,
data: "0xa5f3271e000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000"
}
await assert.rejects(async function() {
const result = await provider.call(tx);
console.log(result);
}, (error: unknown) => {
const offchainErrorData = "0x556f1830000000000000000000000000ae375b05a08204c809b3ca67c68076566199888600000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000140b1494be100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004068747470733a2f2f6574686572732e7269636d6f6f2e776f726b6572732e6465762f746573742d636369702d726561642f7b73656e6465727d2f7b646174617d00000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d4d79206578747261206461746100000000000000000000000000000000000000";
return (isCallException(error) && error.data === offchainErrorData);
});
});
it("testGet should fail with CCIP explicitly disabled on provider", async function() {
this.timeout(60000);
const provider = connect("ropsten");
provider.disableCcipRead = true;
// testGetFail(bytes callData = "0x1234")
const tx = {
to: address, enableCcipRead: true,
data: "0xa5f3271e000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000"
}
await assert.rejects(async function() {
const result = await provider.call(tx);
console.log(result);
}, (error: unknown) => {
const offchainErrorData = "0x556f1830000000000000000000000000ae375b05a08204c809b3ca67c68076566199888600000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000140b1494be100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004068747470733a2f2f6574686572732e7269636d6f6f2e776f726b6572732e6465762f746573742d636369702d726561642f7b73656e6465727d2f7b646174617d00000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d4d79206578747261206461746100000000000000000000000000000000000000";
return (isCallException(error) && error.data === offchainErrorData);
});
});
it("testGetFail should fail if all URLs 5xx", async function() {
this.timeout(60000);
const provider = connect("ropsten");
// testGetFail(bytes callData = "0x1234")
const tx = {
to: address, enableCcipRead: true,
data: "0x36f9cea6000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000"
}
await assert.rejects(async function() {
const result = await provider.call(tx);
console.log(result);
}, (error: unknown) => {
const infoJson = '{"urls":["https:/\/ethers.ricmoo.workers.dev/status/500/{sender}/{data}"],"errorMessages":["hello world"]}';
return (isError(error, "OFFCHAIN_FAULT") && error.reason === "500_SERVER_ERROR" &&
JSON.stringify(error.info) === infoJson);
});
});
it("testGetSenderFail should fail if sender does not match", async function() {
this.timeout(60000);
const provider = connect("ropsten");
// testGetSenderFail(bytes callData = "0x1234")
const tx = {
to: address, enableCcipRead: true,
data: "0x64bff6d1000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000",
}
await assert.rejects(async function() {
const result = await provider.call(tx);
console.log(result);
}, (error: unknown) => {
const errorArgsJson = '["0x0000000000000000000000000000000000000000",["https://ethers.ricmoo.workers.dev/test-ccip-read/{sender}/{data}"],"0x1234","0xb1494be1","0x4d792065787472612064617461"]';
const offchainErrorData = "0x556f1830000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000140b1494be100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004068747470733a2f2f6574686572732e7269636d6f6f2e776f726b6572732e6465762f746573742d636369702d726561642f7b73656e6465727d2f7b646174617d00000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d4d79206578747261206461746100000000000000000000000000000000000000";
return (isCallException(error) && error.data === offchainErrorData &&
error.errorSignature === "OffchainLookup(address,string[],bytes,bytes4,bytes)" &&
JSON.stringify(error.errorArgs) === errorArgsJson);
});
});
it("testGetMissing should fail if early URL 4xx", async function() {
this.timeout(60000);
const provider = connect("ropsten");
// testGetMissing(bytes callData = "0x1234")
const tx = {
to: address, enableCcipRead: true,
data: "0x4ece8d7d000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000"
}
await assert.rejects(async function() {
const result = await provider.call(tx);
console.log(result);
}, (error: unknown) => {
const infoJson = '{"url":"https:/\/ethers.ricmoo.workers.dev/status/404/{sender}/{data}","errorMessage":"hello world"}';
return (isError(error, "OFFCHAIN_FAULT") && error.reason === "404_MISSING_RESOURCE" &&
JSON.stringify(error.info || "") === infoJson);
});
});
it("testGetFallback passes if any URL returns correctly", async function() {
this.timeout(60000);
const provider = connect("ropsten");
// testGetFallback(bytes callData = "0x1234")
const tx = {
to: address, enableCcipRead: true,
data: "0xedf4a021000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000"
}
const result = await provider.call(tx);
verify(address, calldata, result);
});
it("testPost passes under normal operation", async function() {
this.timeout(60000);
const provider = connect("ropsten");
// testPost(bytes callData = "0x1234")
const tx = {
to: address, enableCcipRead: true,
data: "0x66cab49d000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000"
}
const result = await provider.call(tx);
verify(address, calldata, result);
});
})

View File

@ -30,3 +30,97 @@ export function log(context: any, text: string): void {
console.log(text);
}
}
async function stall(duration: number): Promise<void> {
return new Promise((resolve) => { setTimeout(resolve, duration); });
}
export interface MochaRunnable {
timeout: (value: number) => void;
skip: () => void;
}
const ATTEMPTS = 5;
export async function retryIt(name: string, func: (this: MochaRunnable) => Promise<void>): Promise<void> {
it(name, async function() {
this.timeout(ATTEMPTS * 5000);
for (let i = 0; i < ATTEMPTS; i++) {
try {
await func.call(this);
return;
} catch (error: any) {
if (error.message === "sync skip; aborting execution") {
// Skipping a test; let mocha handle it
throw error;
} else if (error.code === "ERR_ASSERTION") {
// Assertion error; let mocha scold us
throw error;
} else {
if (i === ATTEMPTS - 1) {
stats.pushRetry(i, name, error);
} else {
await stall(500 * (1 << i));
stats.pushRetry(i, name, null);
}
}
}
}
// All hope is lost.
throw new Error(`Failed after ${ ATTEMPTS } attempts; ${ name }`);
});
}
export interface StatSet {
name: string;
retries: Array<{ message: string, error: null | Error }>;
}
const _guard = { };
export class Stats {
#stats: Array<StatSet>;
constructor(guard: any) {
if (guard !== _guard) { throw new Error("private constructor"); }
this.#stats = [ ];
}
#currentStats(): StatSet {
if (this.#stats.length === 0) { throw new Error("no active stats"); }
return this.#stats[this.#stats.length - 1];
}
pushRetry(attempt: number, line: string, error: null | Error): void {
const { retries } = this.#currentStats();
if (attempt > 0) { retries.pop(); }
if (retries.length < 100) {
retries.push({
message: `${ attempt + 1 } failures: ${ line }`,
error
});
}
}
start(name: string): void {
this.#stats.push({ name, retries: [ ] });
}
end(context?: any): void {
let log = console.log.bind(console);
if (context && typeof(context._ethersLog) === "function") {
log = context._ethersLog;
}
const { name, retries } = this.#currentStats();
if (retries.length === 0) { return; }
log(`Warning: The following tests required retries (${ name })`);
retries.forEach(({ error, message }) => {
log(" " + message);
if (error) { log(error); }
});
}
}
export const stats = new Stats(_guard);