From ce7d0f3b38e497f30d3376896a7b9564cdedc831 Mon Sep 17 00:00:00 2001 From: Richard Moore Date: Fri, 4 Mar 2022 16:15:47 -0500 Subject: [PATCH] Added CCIP support to provider.call (#2478). --- packages/abstract-provider/src.ts/index.ts | 1 + packages/abstract-signer/src.ts/index.ts | 2 +- packages/contracts/src.ts/index.ts | 10 +- packages/tests/src.ts/test-providers.ts | 152 +++++++++++++++++++++ packages/web/src.ts/index.ts | 6 +- 5 files changed, 167 insertions(+), 4 deletions(-) diff --git a/packages/abstract-provider/src.ts/index.ts b/packages/abstract-provider/src.ts/index.ts index af3a806981..6e2472fdb0 100644 --- a/packages/abstract-provider/src.ts/index.ts +++ b/packages/abstract-provider/src.ts/index.ts @@ -34,6 +34,7 @@ export type TransactionRequest = { maxFeePerGas?: BigNumberish; customData?: Record; + ccipReadEnabled?: boolean; } export interface TransactionResponse extends Transaction { diff --git a/packages/abstract-signer/src.ts/index.ts b/packages/abstract-signer/src.ts/index.ts index 8ec850d899..f0e5d875e6 100644 --- a/packages/abstract-signer/src.ts/index.ts +++ b/packages/abstract-signer/src.ts/index.ts @@ -10,7 +10,7 @@ import { version } from "./_version"; const logger = new Logger(version); const allowedTransactionKeys: Array = [ - "accessList", "chainId", "customData", "data", "from", "gasLimit", "gasPrice", "maxFeePerGas", "maxPriorityFeePerGas", "nonce", "to", "type", "value" + "accessList", "ccipReadEnabled", "chainId", "customData", "data", "from", "gasLimit", "gasPrice", "maxFeePerGas", "maxPriorityFeePerGas", "nonce", "to", "type", "value" ]; const forwardErrors = [ diff --git a/packages/contracts/src.ts/index.ts b/packages/contracts/src.ts/index.ts index 2f96bae14a..40afbf84d5 100644 --- a/packages/contracts/src.ts/index.ts +++ b/packages/contracts/src.ts/index.ts @@ -23,6 +23,7 @@ export interface Overrides { type?: number; accessList?: AccessListish; customData?: Record; + ccipReadEnabled?: boolean; }; export interface PayableOverrides extends Overrides { @@ -58,6 +59,7 @@ export interface PopulatedTransaction { maxPriorityFeePerGas?: BigNumber; customData?: Record; + ccipReadEnabled?: boolean; }; export type EventFilter = { @@ -110,7 +112,8 @@ const allowedTransactionKeys: { [ key: string ]: boolean } = { chainId: true, data: true, from: true, gasLimit: true, gasPrice:true, nonce: true, to: true, value: true, type: true, accessList: true, maxFeePerGas: true, maxPriorityFeePerGas: true, - customData: true + customData: true, + ccipReadEnabled: true } async function resolveName(resolver: Signer | Provider, nameOrPromise: string | Promise): Promise { @@ -274,6 +277,10 @@ async function populateTransaction(contract: Contract, fragment: FunctionFragmen tx.customData = shallowCopy(ro.customData); } + if (ro.ccipReadEnabled) { + tx.ccipReadEnabled = !!ro.ccipReadEnabled; + } + // Remove the overrides delete overrides.nonce; delete overrides.gasLimit; @@ -288,6 +295,7 @@ async function populateTransaction(contract: Contract, fragment: FunctionFragmen delete overrides.maxPriorityFeePerGas; delete overrides.customData; + delete overrides.ccipReadEnabled; // Make sure there are no stray overrides, which may indicate a // typo or using an unsupported key. diff --git a/packages/tests/src.ts/test-providers.ts b/packages/tests/src.ts/test-providers.ts index e02ef015a5..d55af0f8cd 100644 --- a/packages/tests/src.ts/test-providers.ts +++ b/packages/tests/src.ts/test-providers.ts @@ -1393,3 +1393,155 @@ describe("Resolve ENS avatar", function() { }); }); }); + +describe("Test EIP-2544 ENS wildcards", function() { + const provider = (providerFunctions[0].create("ropsten")); + + it("Resolves recursively", async function() { + const resolver = await provider.getResolver("ricmoose.hatch.eth"); + assert.equal(resolver.address, "0x8fc4C380c5d539aE631daF3Ca9182b40FB21D1ae", "found the correct resolver"); + assert.equal(await resolver.supportsWildcard(), true, "supportsWildcard"); + assert.equal((await resolver.getAvatar()).url, "https://static.ricmoo.com/uploads/profile-06cb9c3031c9.jpg", "gets passed-through avatar"); + assert.equal(await resolver.getAddress(), "0x4FaBE0A3a4DDd9968A7b4565184Ad0eFA7BE5411", "gets resolved address"); + }); +}); + +describe("Test CCIP execution", function() { + const address = "0xAe375B05A08204C809b3cA67C680765661998886"; + const ABI = [ + //'error OffchainLookup(address sender, string[] urls, bytes callData, bytes4 callbackFunction, bytes extraData)', + 'function testGet(bytes callData) view returns (bytes32)', + 'function testGetFail(bytes callData) view returns (bytes32)', + 'function testGetSenderFail(bytes callData) view returns (bytes32)', + 'function testGetFallback(bytes callData) view returns (bytes32)', + 'function testGetMissing(bytes callData) view returns (bytes32)', + 'function testPost(bytes callData) view returns (bytes32)', + 'function verifyTest(bytes result, bytes extraData) pure returns (bytes32)' + ]; + + const provider = providerFunctions[0].create("ropsten"); + const contract = new ethers.Contract(address, ABI, provider); + + // 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 = ethers.utils.concat([ + ethers.utils.arrayify(ethers.utils.arrayify(sender).length), + sender, + ethers.utils.arrayify(ethers.utils.arrayify(data).length), + data + ]); + assert.equal(result, ethers.utils.keccak256(check), "response is equal"); + } + + it("testGet passes under normal operation", async function() { + this.timeout(60000); + const data = "0x1234"; + const result = await contract.testGet(data, { ccipReadEnabled: true }); + verify(ethers.constants.AddressZero, data, result); + }); + + it("testGet should fail with CCIP not explicitly enabled by overrides", async function() { + this.timeout(60000); + + try { + const data = "0x1234"; + const result = await contract.testGet(data); + console.log(result); + assert.fail("throw-failed"); + } catch (error: any) { + if (error.message === "throw-failed") { throw error; } + if (error.code !== "CALL_EXCEPTION") { + console.log(error); + assert.fail("failed"); + } + } + }); + + it("testGet should fail with CCIP explicitly disabled on provider", async function() { + this.timeout(60000); + + const provider = providerFunctions[0].create("ropsten"); + (provider).disableCcipRead = true; + const contract = new ethers.Contract(address, ABI, provider); + + try { + const data = "0x1234"; + const result = await contract.testGet(data, { ccipReadEnabled: true }); + console.log(result); + assert.fail("throw-failed"); + } catch (error: any) { + if (error.message === "throw-failed") { throw error; } + if (error.code !== "CALL_EXCEPTION") { + console.log(error); + assert.fail("failed"); + } + } + }); + + it("testGetFail should fail if all URLs 5xx", async function() { + this.timeout(60000); + + try { + const data = "0x1234"; + const result = await contract.testGetFail(data, { ccipReadEnabled: true }); + console.log(result); + assert.fail("throw-failed"); + } catch (error: any) { + if (error.message === "throw-failed") { throw error; } + if (error.code !== "SERVER_ERROR" || (error.errorMessages || []).pop() !== "hello world") { + console.log(error); + assert.fail("failed"); + } + } + }); + + it("testGetSenderFail should fail if sender does not match", async function() { + this.timeout(60000); + + try { + const data = "0x1234"; + const result = await contract.testGetSenderFail(data, { ccipReadEnabled: true }); + console.log(result); + assert.fail("throw-failed"); + } catch (error: any) { + if (error.message === "throw-failed") { throw error; } + if (error.code !== "CALL_EXCEPTION") { + console.log(error); + assert.fail("failed"); + } + } + }); + + it("testGetMissing should fail if early URL 4xx", async function() { + this.timeout(60000); + + try { + const data = "0x1234"; + const result = await contract.testGetMissing(data, { ccipReadEnabled: true }); + console.log(result); + assert.fail("throw-failed"); + } catch (error: any) { + if (error.message === "throw-failed") { throw error; } + if (error.code !== "SERVER_ERROR" || error.errorMessage !== "hello world") { + console.log(error); + assert.fail("failed"); + } + } + }); + + it("testGetFallback passes if any URL returns coorectly", async function() { + this.timeout(60000); + const data = "0x123456"; + const result = await contract.testGetFallback(data, { ccipReadEnabled: true }); + verify(ethers.constants.AddressZero, data, result); + }); + + it("testPost passes under normal operation", async function() { + this.timeout(60000); + const data = "0x1234"; + const result = await contract.testPost(data, { ccipReadEnabled: true }); + verify(ethers.constants.AddressZero, data, result); + }); + +}) diff --git a/packages/web/src.ts/index.ts b/packages/web/src.ts/index.ts index 92ac6ccbb6..c46cf9203c 100644 --- a/packages/web/src.ts/index.ts +++ b/packages/web/src.ts/index.ts @@ -50,6 +50,7 @@ export type ConnectionInfo = { throttleCallback?: (attempt: number, url: string) => Promise, skipFetchSetup?: boolean; + errorPassThrough?: boolean; timeout?: number, }; @@ -98,6 +99,8 @@ export function _fetchData(connection: string | ConnectionInfo, logger.assertArgument((throttleSlotInterval > 0 && (throttleSlotInterval % 1) === 0), "invalid connection throttle slot interval", "connection.throttleSlotInterval", throttleSlotInterval); + const errorPassThrough = ((typeof(connection) === "object") ? !!(connection.errorPassThrough): false); + const headers: { [key: string]: Header } = { }; let url: string = null; @@ -288,8 +291,7 @@ export function _fetchData(connection: string | ConnectionInfo, if (allow304 && response.statusCode === 304) { body = null; - - } else if (response.statusCode < 200 || response.statusCode >= 300) { + } else if (!errorPassThrough && (response.statusCode < 200 || response.statusCode >= 300)) { runningTimeout.cancel(); logger.throwError("bad response", Logger.errors.SERVER_ERROR, { status: response.statusCode,