Added CCIP support to provider.call (#2478).
This commit is contained in:
parent
33a029e457
commit
ae23bb76a9
@ -34,6 +34,7 @@ export type TransactionRequest = {
|
|||||||
maxFeePerGas?: BigNumberish;
|
maxFeePerGas?: BigNumberish;
|
||||||
|
|
||||||
customData?: Record<string, any>;
|
customData?: Record<string, any>;
|
||||||
|
ccipReadEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TransactionResponse extends Transaction {
|
export interface TransactionResponse extends Transaction {
|
||||||
|
@ -10,7 +10,7 @@ import { version } from "./_version";
|
|||||||
const logger = new Logger(version);
|
const logger = new Logger(version);
|
||||||
|
|
||||||
const allowedTransactionKeys: Array<string> = [
|
const allowedTransactionKeys: Array<string> = [
|
||||||
"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 = [
|
const forwardErrors = [
|
||||||
|
@ -23,6 +23,7 @@ export interface Overrides {
|
|||||||
type?: number;
|
type?: number;
|
||||||
accessList?: AccessListish;
|
accessList?: AccessListish;
|
||||||
customData?: Record<string, any>;
|
customData?: Record<string, any>;
|
||||||
|
ccipReadEnabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface PayableOverrides extends Overrides {
|
export interface PayableOverrides extends Overrides {
|
||||||
@ -58,6 +59,7 @@ export interface PopulatedTransaction {
|
|||||||
maxPriorityFeePerGas?: BigNumber;
|
maxPriorityFeePerGas?: BigNumber;
|
||||||
|
|
||||||
customData?: Record<string, any>;
|
customData?: Record<string, any>;
|
||||||
|
ccipReadEnabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EventFilter = {
|
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,
|
chainId: true, data: true, from: true, gasLimit: true, gasPrice:true, nonce: true, to: true, value: true,
|
||||||
type: true, accessList: true,
|
type: true, accessList: true,
|
||||||
maxFeePerGas: true, maxPriorityFeePerGas: true,
|
maxFeePerGas: true, maxPriorityFeePerGas: true,
|
||||||
customData: true
|
customData: true,
|
||||||
|
ccipReadEnabled: true
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveName(resolver: Signer | Provider, nameOrPromise: string | Promise<string>): Promise<string> {
|
async function resolveName(resolver: Signer | Provider, nameOrPromise: string | Promise<string>): Promise<string> {
|
||||||
@ -274,6 +277,10 @@ async function populateTransaction(contract: Contract, fragment: FunctionFragmen
|
|||||||
tx.customData = shallowCopy(ro.customData);
|
tx.customData = shallowCopy(ro.customData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ro.ccipReadEnabled) {
|
||||||
|
tx.ccipReadEnabled = !!ro.ccipReadEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
// Remove the overrides
|
// Remove the overrides
|
||||||
delete overrides.nonce;
|
delete overrides.nonce;
|
||||||
delete overrides.gasLimit;
|
delete overrides.gasLimit;
|
||||||
@ -288,6 +295,7 @@ async function populateTransaction(contract: Contract, fragment: FunctionFragmen
|
|||||||
delete overrides.maxPriorityFeePerGas;
|
delete overrides.maxPriorityFeePerGas;
|
||||||
|
|
||||||
delete overrides.customData;
|
delete overrides.customData;
|
||||||
|
delete overrides.ccipReadEnabled;
|
||||||
|
|
||||||
// Make sure there are no stray overrides, which may indicate a
|
// Make sure there are no stray overrides, which may indicate a
|
||||||
// typo or using an unsupported key.
|
// typo or using an unsupported key.
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
} from "@ethersproject/abstract-provider";
|
} from "@ethersproject/abstract-provider";
|
||||||
import { Base58 } from "@ethersproject/basex";
|
import { Base58 } from "@ethersproject/basex";
|
||||||
import { BigNumber, BigNumberish } from "@ethersproject/bignumber";
|
import { BigNumber, BigNumberish } from "@ethersproject/bignumber";
|
||||||
import { arrayify, concat, hexConcat, hexDataLength, hexDataSlice, hexlify, hexValue, hexZeroPad, isHexString } from "@ethersproject/bytes";
|
import { arrayify, BytesLike, concat, hexConcat, hexDataLength, hexDataSlice, hexlify, hexValue, hexZeroPad, isHexString } from "@ethersproject/bytes";
|
||||||
import { HashZero } from "@ethersproject/constants";
|
import { HashZero } from "@ethersproject/constants";
|
||||||
import { dnsEncode, namehash } from "@ethersproject/hash";
|
import { dnsEncode, namehash } from "@ethersproject/hash";
|
||||||
import { getNetwork, Network, Networkish } from "@ethersproject/networks";
|
import { getNetwork, Network, Networkish } from "@ethersproject/networks";
|
||||||
@ -24,6 +24,8 @@ const logger = new Logger(version);
|
|||||||
|
|
||||||
import { Formatter } from "./formatter";
|
import { Formatter } from "./formatter";
|
||||||
|
|
||||||
|
const MAX_CCIP_REDIRECTS = 10;
|
||||||
|
|
||||||
//////////////////////////////
|
//////////////////////////////
|
||||||
// Event Serializeing
|
// Event Serializeing
|
||||||
|
|
||||||
@ -250,18 +252,19 @@ const matchers = [
|
|||||||
new RegExp("^eip155:[0-9]+/(erc[0-9]+):(.*)$", "i"),
|
new RegExp("^eip155:[0-9]+/(erc[0-9]+):(.*)$", "i"),
|
||||||
];
|
];
|
||||||
|
|
||||||
function _parseString(result: string): null | string {
|
function _parseString(result: string, start: number): null | string {
|
||||||
try {
|
try {
|
||||||
return toUtf8String(_parseBytes(result));
|
return toUtf8String(_parseBytes(result, start));
|
||||||
} catch(error) { }
|
} catch(error) { }
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _parseBytes(result: string): null | string {
|
function _parseBytes(result: string, start: number): null | string {
|
||||||
if (result === "0x") { return null; }
|
if (result === "0x") { return null; }
|
||||||
|
|
||||||
const offset = BigNumber.from(hexDataSlice(result, 0, 32)).toNumber();
|
const offset = BigNumber.from(hexDataSlice(result, start, start + 32)).toNumber();
|
||||||
const length = BigNumber.from(hexDataSlice(result, offset, offset + 32)).toNumber();
|
const length = BigNumber.from(hexDataSlice(result, offset, offset + 32)).toNumber();
|
||||||
|
|
||||||
return hexDataSlice(result, offset + 32, offset + 32 + length);
|
return hexDataSlice(result, offset + 32, offset + 32 + length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,6 +298,32 @@ function bytesPad(value: Uint8Array): Uint8Array {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ABI Encodes a series of (bytes, bytes, ...)
|
||||||
|
function encodeBytes(datas: Array<BytesLike>) {
|
||||||
|
const result: Array<Uint8Array> = [ ];
|
||||||
|
|
||||||
|
let byteCount = 0;
|
||||||
|
|
||||||
|
// Add place-holders for pointers as we add items
|
||||||
|
for (let i = 0; i < datas.length; i++) {
|
||||||
|
result.push(null);
|
||||||
|
byteCount += 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < datas.length; i++) {
|
||||||
|
const data = arrayify(datas[i]);
|
||||||
|
|
||||||
|
// Update the bytes offset
|
||||||
|
result[i] = numPad(byteCount);
|
||||||
|
|
||||||
|
// The length and padded value of data
|
||||||
|
result.push(numPad(data.length));
|
||||||
|
result.push(bytesPad(data));
|
||||||
|
byteCount += 32 + Math.ceil(data.length / 32) * 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hexConcat(result);
|
||||||
|
}
|
||||||
|
|
||||||
export class Resolver implements EnsResolver {
|
export class Resolver implements EnsResolver {
|
||||||
readonly provider: BaseProvider;
|
readonly provider: BaseProvider;
|
||||||
@ -346,41 +375,13 @@ export class Resolver implements EnsResolver {
|
|||||||
if (await this.supportsWildcard()) {
|
if (await this.supportsWildcard()) {
|
||||||
parseBytes = true;
|
parseBytes = true;
|
||||||
|
|
||||||
const p0 = arrayify(dnsEncode(this.name));
|
|
||||||
const p1 = arrayify(tx.data);
|
|
||||||
|
|
||||||
// selector("resolve(bytes,bytes)")
|
// selector("resolve(bytes,bytes)")
|
||||||
const bytes: Array<string | Uint8Array> = [ "0x9061b923" ];
|
tx.data = hexConcat([ "0x9061b923", encodeBytes([ dnsEncode(this.name), tx.data ]) ]);
|
||||||
let byteCount = 0;
|
|
||||||
|
|
||||||
// Place-holder pointer to p0
|
|
||||||
const placeHolder0 = bytes.length;
|
|
||||||
bytes.push("0x");
|
|
||||||
byteCount += 32;
|
|
||||||
|
|
||||||
// Place-holder pointer to p1
|
|
||||||
const placeHolder1 = bytes.length;
|
|
||||||
bytes.push("0x");
|
|
||||||
byteCount += 32;
|
|
||||||
|
|
||||||
// The length and padded value of p0
|
|
||||||
bytes[placeHolder0] = numPad(byteCount);
|
|
||||||
bytes.push(numPad(p0.length));
|
|
||||||
bytes.push(bytesPad(p0));
|
|
||||||
byteCount += 32 + Math.ceil(p0.length / 32) * 32;
|
|
||||||
|
|
||||||
// The length and padded value of p0
|
|
||||||
bytes[placeHolder1] = numPad(byteCount);
|
|
||||||
bytes.push(numPad(p1.length));
|
|
||||||
bytes.push(bytesPad(p1));
|
|
||||||
byteCount += 32 + Math.ceil(p1.length / 32) * 32;
|
|
||||||
|
|
||||||
tx.data = hexConcat(bytes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let result = await this.provider.call(tx);
|
let result = await this.provider.call(tx);
|
||||||
if (parseBytes) { result = _parseBytes(result); }
|
if (parseBytes) { result = _parseBytes(result, 0); }
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === Logger.errors.CALL_EXCEPTION) { return null; }
|
if (error.code === Logger.errors.CALL_EXCEPTION) { return null; }
|
||||||
@ -390,7 +391,7 @@ export class Resolver implements EnsResolver {
|
|||||||
|
|
||||||
async _fetchBytes(selector: string, parameters?: string): Promise<null | string> {
|
async _fetchBytes(selector: string, parameters?: string): Promise<null | string> {
|
||||||
const result = await this._fetch(selector, parameters);
|
const result = await this._fetch(selector, parameters);
|
||||||
if (result != null) { return _parseBytes(result); }
|
if (result != null) { return _parseBytes(result, 0); }
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -561,7 +562,7 @@ export class Resolver implements EnsResolver {
|
|||||||
data: hexConcat([ selector, tokenId ])
|
data: hexConcat([ selector, tokenId ])
|
||||||
};
|
};
|
||||||
|
|
||||||
let metadataUrl = _parseString(await this.provider.call(tx))
|
let metadataUrl = _parseString(await this.provider.call(tx), 0);
|
||||||
if (metadataUrl == null) { return null; }
|
if (metadataUrl == null) { return null; }
|
||||||
linkage.push({ type: "metadata-url-base", content: metadataUrl });
|
linkage.push({ type: "metadata-url-base", content: metadataUrl });
|
||||||
|
|
||||||
@ -700,6 +701,8 @@ export class BaseProvider extends Provider implements EnsProvider {
|
|||||||
|
|
||||||
readonly anyNetwork: boolean;
|
readonly anyNetwork: boolean;
|
||||||
|
|
||||||
|
disableCcipRead: boolean;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ready
|
* ready
|
||||||
@ -721,6 +724,8 @@ export class BaseProvider extends Provider implements EnsProvider {
|
|||||||
|
|
||||||
this._emitted = { block: -2 };
|
this._emitted = { block: -2 };
|
||||||
|
|
||||||
|
this.disableCcipRead = false;
|
||||||
|
|
||||||
this.formatter = new.target.getFormatter();
|
this.formatter = new.target.getFormatter();
|
||||||
|
|
||||||
// If network is any, this Provider allows the underlying
|
// If network is any, this Provider allows the underlying
|
||||||
@ -822,6 +827,46 @@ export class BaseProvider extends Provider implements EnsProvider {
|
|||||||
return getNetwork((network == null) ? "homestead": network);
|
return getNetwork((network == null) ? "homestead": network);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async ccipReadFetch(tx: Transaction, calldata: string, urls: Array<string>): Promise<null | string> {
|
||||||
|
if (this.disableCcipRead || urls.length === 0) { return null; }
|
||||||
|
|
||||||
|
const sender = (tx.from || "0x0000000000000000000000000000000000000000").toLowerCase();
|
||||||
|
const data = calldata.toLowerCase();
|
||||||
|
|
||||||
|
const errorMessages: Array<string> = [ ];
|
||||||
|
|
||||||
|
for (let i = 0; i < urls.length; i++) {
|
||||||
|
const url = urls[i];
|
||||||
|
|
||||||
|
// URL expansion
|
||||||
|
const href = url.replace("{sender}", sender).replace("{data}", data);
|
||||||
|
|
||||||
|
// If no {data} is present, use POST; otherwise GET
|
||||||
|
const json: string | null = (url.indexOf("{data}") >= 0) ? null: JSON.stringify({ data, sender });
|
||||||
|
|
||||||
|
const result = await fetchJson({ url: href, errorPassThrough: true }, json, (value, response) => {
|
||||||
|
value.status = response.statusCode;
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.data) { return result.data; }
|
||||||
|
|
||||||
|
const errorMessage = (result.message || "unknown error");
|
||||||
|
|
||||||
|
// 4xx indicates the result is not present; stop
|
||||||
|
if (result.status >= 400 && result.status < 500) {
|
||||||
|
return logger.throwError(`response not found during CCIP fetch: ${ errorMessage }`, Logger.errors.SERVER_ERROR, { url, errorMessage });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5xx indicates server issue; try the next url
|
||||||
|
errorMessages.push(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return logger.throwError(`error encountered during CCIP fetch: ${ errorMessages.map((m) => JSON.stringify(m)).join(", ") }`, Logger.errors.SERVER_ERROR, {
|
||||||
|
urls, errorMessages
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Fetches the blockNumber, but will reuse any result that is less
|
// Fetches the blockNumber, but will reuse any result that is less
|
||||||
// than maxAge old or has been requested since the last request
|
// than maxAge old or has been requested since the last request
|
||||||
async _getInternalBlockNumber(maxAge: number): Promise<number> {
|
async _getInternalBlockNumber(maxAge: number): Promise<number> {
|
||||||
@ -1512,22 +1557,105 @@ export class BaseProvider extends Provider implements EnsProvider {
|
|||||||
return this.formatter.filter(await resolveProperties(result));
|
return this.formatter.filter(await resolveProperties(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
async call(transaction: Deferrable<TransactionRequest>, blockTag?: BlockTag | Promise<BlockTag>): Promise<string> {
|
async _call(transaction: TransactionRequest, blockTag: BlockTag, attempt: number): Promise<string> {
|
||||||
await this.getNetwork();
|
if (attempt >= MAX_CCIP_REDIRECTS) {
|
||||||
const params = await resolveProperties({
|
logger.throwError("CCIP read exceeded maximum redirections", Logger.errors.SERVER_ERROR, {
|
||||||
transaction: this._getTransactionRequest(transaction),
|
redirects: attempt, transaction
|
||||||
blockTag: this._getBlockTag(blockTag)
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const txSender = transaction.to;
|
||||||
|
|
||||||
|
const result = await this.perform("call", { transaction, blockTag });
|
||||||
|
|
||||||
|
// CCIP Read request via OffchainLookup(address,string[],bytes,bytes4,bytes)
|
||||||
|
if (attempt >= 0 && blockTag === "latest" && txSender != null && result.substring(0, 10) === "0x556f1830" && (hexDataLength(result) % 32 === 4)) {
|
||||||
|
try {
|
||||||
|
const data = hexDataSlice(result, 4);
|
||||||
|
|
||||||
|
// Check the sender of the OffchainLookup matches the transaction
|
||||||
|
const sender = hexDataSlice(data, 0, 32);
|
||||||
|
if (!BigNumber.from(sender).eq(txSender)) {
|
||||||
|
logger.throwError("CCIP Read sender did not match", Logger.errors.CALL_EXCEPTION, {
|
||||||
|
name: "OffchainLookup",
|
||||||
|
signature: "OffchainLookup(address,string[],bytes,bytes4,bytes)",
|
||||||
|
transaction, data: result
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the URLs from the response
|
||||||
|
const urls: Array<string> = [];
|
||||||
|
const urlsOffset = BigNumber.from(hexDataSlice(data, 32, 64)).toNumber();
|
||||||
|
const urlsLength = BigNumber.from(hexDataSlice(data, urlsOffset, urlsOffset + 32)).toNumber();
|
||||||
|
const urlsData = hexDataSlice(data, urlsOffset + 32);
|
||||||
|
for (let u = 0; u < urlsLength; u++) {
|
||||||
|
const url = _parseString(urlsData, u * 32);
|
||||||
|
if (url == null) {
|
||||||
|
logger.throwError("CCIP Read contained corrupt URL string", Logger.errors.CALL_EXCEPTION, {
|
||||||
|
name: "OffchainLookup",
|
||||||
|
signature: "OffchainLookup(address,string[],bytes,bytes4,bytes)",
|
||||||
|
transaction, data: result
|
||||||
|
});
|
||||||
|
}
|
||||||
|
urls.push(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the CCIP calldata to forward
|
||||||
|
const calldata = _parseBytes(data, 64);
|
||||||
|
|
||||||
|
// Get the callbackSelector (bytes4)
|
||||||
|
if (!BigNumber.from(hexDataSlice(data, 100, 128)).isZero()) {
|
||||||
|
logger.throwError("CCIP Read callback selector included junk", Logger.errors.CALL_EXCEPTION, {
|
||||||
|
name: "OffchainLookup",
|
||||||
|
signature: "OffchainLookup(address,string[],bytes,bytes4,bytes)",
|
||||||
|
transaction, data: result
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const callbackSelector = hexDataSlice(data, 96, 100);
|
||||||
|
|
||||||
|
// Get the extra data to send back to the contract as context
|
||||||
|
const extraData = _parseBytes(data, 128);
|
||||||
|
|
||||||
|
const ccipResult = await this.ccipReadFetch(<Transaction>transaction, calldata, urls);
|
||||||
|
if (ccipResult == null) {
|
||||||
|
logger.throwError("CCIP Read disabled or provided no URLs", Logger.errors.CALL_EXCEPTION, {
|
||||||
|
name: "OffchainLookup",
|
||||||
|
signature: "OffchainLookup(address,string[],bytes,bytes4,bytes)",
|
||||||
|
transaction, data: result
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tx = {
|
||||||
|
to: txSender,
|
||||||
|
data: hexConcat([ callbackSelector, encodeBytes([ ccipResult, extraData ]) ])
|
||||||
|
};
|
||||||
|
|
||||||
|
return this._call(tx, blockTag, attempt + 1);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === Logger.errors.SERVER_ERROR) { throw error; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const result = await this.perform("call", params);
|
|
||||||
try {
|
try {
|
||||||
return hexlify(result);
|
return hexlify(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return logger.throwError("bad result from backend", Logger.errors.SERVER_ERROR, {
|
return logger.throwError("bad result from backend", Logger.errors.SERVER_ERROR, {
|
||||||
method: "call",
|
method: "call",
|
||||||
params, result, error
|
params: { transaction, blockTag }, result, error
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async call(transaction: Deferrable<TransactionRequest>, blockTag?: BlockTag | Promise<BlockTag>): Promise<string> {
|
||||||
|
await this.getNetwork();
|
||||||
|
const resolved = await resolveProperties({
|
||||||
|
transaction: this._getTransactionRequest(transaction),
|
||||||
|
blockTag: this._getBlockTag(blockTag),
|
||||||
|
ccipReadEnabled: Promise.resolve(transaction.ccipReadEnabled)
|
||||||
|
});
|
||||||
|
return this._call(resolved.transaction, resolved.blockTag, resolved.ccipReadEnabled ? 0: -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
async estimateGas(transaction: Deferrable<TransactionRequest>): Promise<BigNumber> {
|
async estimateGas(transaction: Deferrable<TransactionRequest>): Promise<BigNumber> {
|
||||||
@ -1843,7 +1971,7 @@ export class BaseProvider extends Provider implements EnsProvider {
|
|||||||
const name = _parseString(await this.call({
|
const name = _parseString(await this.call({
|
||||||
to: resolverAddr,
|
to: resolverAddr,
|
||||||
data: ("0x691f3431" + namehash(node).substring(2))
|
data: ("0x691f3431" + namehash(node).substring(2))
|
||||||
}));
|
}), 0);
|
||||||
|
|
||||||
const addr = await this.resolveName(name);
|
const addr = await this.resolveName(name);
|
||||||
if (addr != address) { return null; }
|
if (addr != address) { return null; }
|
||||||
@ -1877,7 +2005,7 @@ export class BaseProvider extends Provider implements EnsProvider {
|
|||||||
const name = _parseString(await this.call({
|
const name = _parseString(await this.call({
|
||||||
to: resolverAddress,
|
to: resolverAddress,
|
||||||
data: ("0x691f3431" + namehash(node).substring(2))
|
data: ("0x691f3431" + namehash(node).substring(2))
|
||||||
}));
|
}), 0);
|
||||||
resolver = await this.getResolver(name);
|
resolver = await this.getResolver(name);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code !== Logger.errors.CALL_EXCEPTION) { throw error; }
|
if (error.code !== Logger.errors.CALL_EXCEPTION) { throw error; }
|
||||||
|
@ -1393,3 +1393,155 @@ describe("Resolve ENS avatar", function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Test EIP-2544 ENS wildcards", function() {
|
||||||
|
const provider = <ethers.providers.BaseProvider>(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");
|
||||||
|
(<ethers.providers.BaseProvider>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);
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
|
@ -50,6 +50,7 @@ export type ConnectionInfo = {
|
|||||||
throttleCallback?: (attempt: number, url: string) => Promise<boolean>,
|
throttleCallback?: (attempt: number, url: string) => Promise<boolean>,
|
||||||
|
|
||||||
skipFetchSetup?: boolean;
|
skipFetchSetup?: boolean;
|
||||||
|
errorPassThrough?: boolean;
|
||||||
|
|
||||||
timeout?: number,
|
timeout?: number,
|
||||||
};
|
};
|
||||||
@ -98,6 +99,8 @@ export function _fetchData<T = Uint8Array>(connection: string | ConnectionInfo,
|
|||||||
logger.assertArgument((throttleSlotInterval > 0 && (throttleSlotInterval % 1) === 0),
|
logger.assertArgument((throttleSlotInterval > 0 && (throttleSlotInterval % 1) === 0),
|
||||||
"invalid connection throttle slot interval", "connection.throttleSlotInterval", throttleSlotInterval);
|
"invalid connection throttle slot interval", "connection.throttleSlotInterval", throttleSlotInterval);
|
||||||
|
|
||||||
|
const errorPassThrough = ((typeof(connection) === "object") ? !!(connection.errorPassThrough): false);
|
||||||
|
|
||||||
const headers: { [key: string]: Header } = { };
|
const headers: { [key: string]: Header } = { };
|
||||||
|
|
||||||
let url: string = null;
|
let url: string = null;
|
||||||
@ -288,8 +291,7 @@ export function _fetchData<T = Uint8Array>(connection: string | ConnectionInfo,
|
|||||||
|
|
||||||
if (allow304 && response.statusCode === 304) {
|
if (allow304 && response.statusCode === 304) {
|
||||||
body = null;
|
body = null;
|
||||||
|
} else if (!errorPassThrough && (response.statusCode < 200 || response.statusCode >= 300)) {
|
||||||
} else if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
||||||
runningTimeout.cancel();
|
runningTimeout.cancel();
|
||||||
logger.throwError("bad response", Logger.errors.SERVER_ERROR, {
|
logger.throwError("bad response", Logger.errors.SERVER_ERROR, {
|
||||||
status: response.statusCode,
|
status: response.statusCode,
|
||||||
|
Loading…
Reference in New Issue
Block a user