You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1404 lines
50 KiB
1404 lines
50 KiB
/** |
|
* The available providers should suffice for most developers purposes, |
|
* but the [[AbstractProvider]] class has many features which enable |
|
* sub-classing it for specific purposes. |
|
* |
|
* @_section: api/providers/abstract-provider: Subclassing Provider [abstract-provider] |
|
*/ |
|
// @TODO |
|
// Event coalescence |
|
// When we register an event with an async value (e.g. address is a Signer |
|
// or ENS name), we need to add it immeidately for the Event API, but also |
|
// need time to resolve the address. Upon resolving the address, we need to |
|
// migrate the listener to the static event. We also need to maintain a map |
|
// of Signer/ENS name to address so we can sync respond to listenerCount. |
|
import { getAddress, resolveAddress } from "../address/index.js"; |
|
import { ZeroAddress } from "../constants/index.js"; |
|
import { Contract } from "../contract/index.js"; |
|
import { namehash } from "../hash/index.js"; |
|
import { Transaction } from "../transaction/index.js"; |
|
import { concat, dataLength, dataSlice, hexlify, isHexString, getBigInt, getBytes, getNumber, isCallException, isError, makeError, assert, assertArgument, FetchRequest, toBeArray, toQuantity, defineProperties, EventPayload, resolveProperties, toUtf8String } from "../utils/index.js"; |
|
import { EnsResolver } from "./ens-resolver.js"; |
|
import { formatBlock, formatLog, formatTransactionReceipt, formatTransactionResponse } from "./format.js"; |
|
import { Network } from "./network.js"; |
|
import { copyRequest, Block, FeeData, Log, TransactionReceipt, TransactionResponse } from "./provider.js"; |
|
import { PollingBlockSubscriber, PollingBlockTagSubscriber, PollingEventSubscriber, PollingOrphanSubscriber, PollingTransactionSubscriber } from "./subscriber-polling.js"; |
|
// Constants |
|
const BN_2 = BigInt(2); |
|
const MAX_CCIP_REDIRECTS = 10; |
|
function isPromise(value) { |
|
return (value && typeof (value.then) === "function"); |
|
} |
|
function getTag(prefix, value) { |
|
return prefix + ":" + JSON.stringify(value, (k, v) => { |
|
if (v == null) { |
|
return "null"; |
|
} |
|
if (typeof (v) === "bigint") { |
|
return `bigint:${v.toString()}`; |
|
} |
|
if (typeof (v) === "string") { |
|
return v.toLowerCase(); |
|
} |
|
// Sort object keys |
|
if (typeof (v) === "object" && !Array.isArray(v)) { |
|
const keys = Object.keys(v); |
|
keys.sort(); |
|
return keys.reduce((accum, key) => { |
|
accum[key] = v[key]; |
|
return accum; |
|
}, {}); |
|
} |
|
return v; |
|
}); |
|
} |
|
/** |
|
* An **UnmanagedSubscriber** is useful for events which do not require |
|
* any additional management, such as ``"debug"`` which only requires |
|
* emit in synchronous event loop triggered calls. |
|
*/ |
|
export class UnmanagedSubscriber { |
|
/** |
|
* The name fof the event. |
|
*/ |
|
name; |
|
/** |
|
* Create a new UnmanagedSubscriber with %%name%%. |
|
*/ |
|
constructor(name) { defineProperties(this, { name }); } |
|
start() { } |
|
stop() { } |
|
pause(dropWhilePaused) { } |
|
resume() { } |
|
} |
|
function copy(value) { |
|
return JSON.parse(JSON.stringify(value)); |
|
} |
|
function concisify(items) { |
|
items = Array.from((new Set(items)).values()); |
|
items.sort(); |
|
return items; |
|
} |
|
async function getSubscription(_event, provider) { |
|
if (_event == null) { |
|
throw new Error("invalid event"); |
|
} |
|
// Normalize topic array info an EventFilter |
|
if (Array.isArray(_event)) { |
|
_event = { topics: _event }; |
|
} |
|
if (typeof (_event) === "string") { |
|
switch (_event) { |
|
case "block": |
|
case "debug": |
|
case "error": |
|
case "finalized": |
|
case "network": |
|
case "pending": |
|
case "safe": { |
|
return { type: _event, tag: _event }; |
|
} |
|
} |
|
} |
|
if (isHexString(_event, 32)) { |
|
const hash = _event.toLowerCase(); |
|
return { type: "transaction", tag: getTag("tx", { hash }), hash }; |
|
} |
|
if (_event.orphan) { |
|
const event = _event; |
|
// @TODO: Should lowercase and whatnot things here instead of copy... |
|
return { type: "orphan", tag: getTag("orphan", event), filter: copy(event) }; |
|
} |
|
if ((_event.address || _event.topics)) { |
|
const event = _event; |
|
const filter = { |
|
topics: ((event.topics || []).map((t) => { |
|
if (t == null) { |
|
return null; |
|
} |
|
if (Array.isArray(t)) { |
|
return concisify(t.map((t) => t.toLowerCase())); |
|
} |
|
return t.toLowerCase(); |
|
})) |
|
}; |
|
if (event.address) { |
|
const addresses = []; |
|
const promises = []; |
|
const addAddress = (addr) => { |
|
if (isHexString(addr)) { |
|
addresses.push(addr); |
|
} |
|
else { |
|
promises.push((async () => { |
|
addresses.push(await resolveAddress(addr, provider)); |
|
})()); |
|
} |
|
}; |
|
if (Array.isArray(event.address)) { |
|
event.address.forEach(addAddress); |
|
} |
|
else { |
|
addAddress(event.address); |
|
} |
|
if (promises.length) { |
|
await Promise.all(promises); |
|
} |
|
filter.address = concisify(addresses.map((a) => a.toLowerCase())); |
|
} |
|
return { filter, tag: getTag("event", filter), type: "event" }; |
|
} |
|
assertArgument(false, "unknown ProviderEvent", "event", _event); |
|
} |
|
function getTime() { return (new Date()).getTime(); } |
|
const defaultOptions = { |
|
cacheTimeout: 250, |
|
pollingInterval: 4000 |
|
}; |
|
/** |
|
* An **AbstractProvider** provides a base class for other sub-classes to |
|
* implement the [[Provider]] API by normalizing input arguments and |
|
* formatting output results as well as tracking events for consistent |
|
* behaviour on an eventually-consistent network. |
|
*/ |
|
export class AbstractProvider { |
|
#subs; |
|
#plugins; |
|
// null=unpaused, true=paused+dropWhilePaused, false=paused |
|
#pausedState; |
|
#destroyed; |
|
#networkPromise; |
|
#anyNetwork; |
|
#performCache; |
|
// The most recent block number if running an event or -1 if no "block" event |
|
#lastBlockNumber; |
|
#nextTimer; |
|
#timers; |
|
#disableCcipRead; |
|
#options; |
|
/** |
|
* Create a new **AbstractProvider** connected to %%network%%, or |
|
* use the various network detection capabilities to discover the |
|
* [[Network]] if necessary. |
|
*/ |
|
constructor(_network, options) { |
|
this.#options = Object.assign({}, defaultOptions, options || {}); |
|
if (_network === "any") { |
|
this.#anyNetwork = true; |
|
this.#networkPromise = null; |
|
} |
|
else if (_network) { |
|
const network = Network.from(_network); |
|
this.#anyNetwork = false; |
|
this.#networkPromise = Promise.resolve(network); |
|
setTimeout(() => { this.emit("network", network, null); }, 0); |
|
} |
|
else { |
|
this.#anyNetwork = false; |
|
this.#networkPromise = null; |
|
} |
|
this.#lastBlockNumber = -1; |
|
this.#performCache = new Map(); |
|
this.#subs = new Map(); |
|
this.#plugins = new Map(); |
|
this.#pausedState = null; |
|
this.#destroyed = false; |
|
this.#nextTimer = 1; |
|
this.#timers = new Map(); |
|
this.#disableCcipRead = false; |
|
} |
|
get pollingInterval() { return this.#options.pollingInterval; } |
|
/** |
|
* Returns ``this``, to allow an **AbstractProvider** to implement |
|
* the [[ContractRunner]] interface. |
|
*/ |
|
get provider() { return this; } |
|
/** |
|
* Returns all the registered plug-ins. |
|
*/ |
|
get plugins() { |
|
return Array.from(this.#plugins.values()); |
|
} |
|
/** |
|
* Attach a new plug-in. |
|
*/ |
|
attachPlugin(plugin) { |
|
if (this.#plugins.get(plugin.name)) { |
|
throw new Error(`cannot replace existing plugin: ${plugin.name} `); |
|
} |
|
this.#plugins.set(plugin.name, plugin.connect(this)); |
|
return this; |
|
} |
|
/** |
|
* Get a plugin by name. |
|
*/ |
|
getPlugin(name) { |
|
return (this.#plugins.get(name)) || null; |
|
} |
|
/** |
|
* Prevent any CCIP-read operation, regardless of whether requested |
|
* in a [[call]] using ``enableCcipRead``. |
|
*/ |
|
get disableCcipRead() { return this.#disableCcipRead; } |
|
set disableCcipRead(value) { this.#disableCcipRead = !!value; } |
|
// Shares multiple identical requests made during the same 250ms |
|
async #perform(req) { |
|
const timeout = this.#options.cacheTimeout; |
|
// Caching disabled |
|
if (timeout < 0) { |
|
return await this._perform(req); |
|
} |
|
// Create a tag |
|
const tag = getTag(req.method, req); |
|
let perform = this.#performCache.get(tag); |
|
if (!perform) { |
|
perform = this._perform(req); |
|
this.#performCache.set(tag, perform); |
|
setTimeout(() => { |
|
if (this.#performCache.get(tag) === perform) { |
|
this.#performCache.delete(tag); |
|
} |
|
}, timeout); |
|
} |
|
return await perform; |
|
} |
|
/** |
|
* Resolves to the data for executing the CCIP-read operations. |
|
*/ |
|
async ccipReadFetch(tx, calldata, urls) { |
|
if (this.disableCcipRead || urls.length === 0 || tx.to == null) { |
|
return null; |
|
} |
|
const sender = tx.to.toLowerCase(); |
|
const data = calldata.toLowerCase(); |
|
const errorMessages = []; |
|
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; |
|
//}); |
|
const request = new FetchRequest(href); |
|
if (url.indexOf("{data}") === -1) { |
|
request.body = { data, sender }; |
|
} |
|
this.emit("debug", { action: "sendCcipReadFetchRequest", request, index: i, urls }); |
|
let errorMessage = "unknown error"; |
|
// Fetch the resource... |
|
let resp; |
|
try { |
|
resp = await request.send(); |
|
} |
|
catch (error) { |
|
// ...low-level fetch error (missing host, bad SSL, etc.), |
|
// so try next URL |
|
errorMessages.push(error.message); |
|
this.emit("debug", { action: "receiveCcipReadFetchError", request, result: { error } }); |
|
continue; |
|
} |
|
try { |
|
const result = resp.bodyJson; |
|
if (result.data) { |
|
this.emit("debug", { action: "receiveCcipReadFetchResult", request, result }); |
|
return result.data; |
|
} |
|
if (result.message) { |
|
errorMessage = result.message; |
|
} |
|
this.emit("debug", { action: "receiveCcipReadFetchError", request, result }); |
|
} |
|
catch (error) { } |
|
// 4xx indicates the result is not present; stop |
|
assert(resp.statusCode < 400 || resp.statusCode >= 500, `response not found during CCIP fetch: ${errorMessage}`, "OFFCHAIN_FAULT", { reason: "404_MISSING_RESOURCE", transaction: tx, info: { url, errorMessage } }); |
|
// 5xx indicates server issue; try the next url |
|
errorMessages.push(errorMessage); |
|
} |
|
assert(false, `error encountered during CCIP fetch: ${errorMessages.map((m) => JSON.stringify(m)).join(", ")}`, "OFFCHAIN_FAULT", { |
|
reason: "500_SERVER_ERROR", |
|
transaction: tx, info: { urls, errorMessages } |
|
}); |
|
} |
|
/** |
|
* Provides the opportunity for a sub-class to wrap a block before |
|
* returning it, to add additional properties or an alternate |
|
* sub-class of [[Block]]. |
|
*/ |
|
_wrapBlock(value, network) { |
|
return new Block(formatBlock(value), this); |
|
} |
|
/** |
|
* Provides the opportunity for a sub-class to wrap a log before |
|
* returning it, to add additional properties or an alternate |
|
* sub-class of [[Log]]. |
|
*/ |
|
_wrapLog(value, network) { |
|
return new Log(formatLog(value), this); |
|
} |
|
/** |
|
* Provides the opportunity for a sub-class to wrap a transaction |
|
* receipt before returning it, to add additional properties or an |
|
* alternate sub-class of [[TransactionReceipt]]. |
|
*/ |
|
_wrapTransactionReceipt(value, network) { |
|
return new TransactionReceipt(formatTransactionReceipt(value), this); |
|
} |
|
/** |
|
* Provides the opportunity for a sub-class to wrap a transaction |
|
* response before returning it, to add additional properties or an |
|
* alternate sub-class of [[TransactionResponse]]. |
|
*/ |
|
_wrapTransactionResponse(tx, network) { |
|
return new TransactionResponse(formatTransactionResponse(tx), this); |
|
} |
|
/** |
|
* Resolves to the Network, forcing a network detection using whatever |
|
* technique the sub-class requires. |
|
* |
|
* Sub-classes **must** override this. |
|
*/ |
|
_detectNetwork() { |
|
assert(false, "sub-classes must implement this", "UNSUPPORTED_OPERATION", { |
|
operation: "_detectNetwork" |
|
}); |
|
} |
|
/** |
|
* Sub-classes should use this to perform all built-in operations. All |
|
* methods sanitizes and normalizes the values passed into this. |
|
* |
|
* Sub-classes **must** override this. |
|
*/ |
|
async _perform(req) { |
|
assert(false, `unsupported method: ${req.method}`, "UNSUPPORTED_OPERATION", { |
|
operation: req.method, |
|
info: req |
|
}); |
|
} |
|
// State |
|
async getBlockNumber() { |
|
const blockNumber = getNumber(await this.#perform({ method: "getBlockNumber" }), "%response"); |
|
if (this.#lastBlockNumber >= 0) { |
|
this.#lastBlockNumber = blockNumber; |
|
} |
|
return blockNumber; |
|
} |
|
/** |
|
* Returns or resolves to the address for %%address%%, resolving ENS |
|
* names and [[Addressable]] objects and returning if already an |
|
* address. |
|
*/ |
|
_getAddress(address) { |
|
return resolveAddress(address, this); |
|
} |
|
/** |
|
* Returns or resolves to a valid block tag for %%blockTag%%, resolving |
|
* negative values and returning if already a valid block tag. |
|
*/ |
|
_getBlockTag(blockTag) { |
|
if (blockTag == null) { |
|
return "latest"; |
|
} |
|
switch (blockTag) { |
|
case "earliest": |
|
return "0x0"; |
|
case "finalized": |
|
case "latest": |
|
case "pending": |
|
case "safe": |
|
return blockTag; |
|
} |
|
if (isHexString(blockTag)) { |
|
if (isHexString(blockTag, 32)) { |
|
return blockTag; |
|
} |
|
return toQuantity(blockTag); |
|
} |
|
if (typeof (blockTag) === "bigint") { |
|
blockTag = getNumber(blockTag, "blockTag"); |
|
} |
|
if (typeof (blockTag) === "number") { |
|
if (blockTag >= 0) { |
|
return toQuantity(blockTag); |
|
} |
|
if (this.#lastBlockNumber >= 0) { |
|
return toQuantity(this.#lastBlockNumber + blockTag); |
|
} |
|
return this.getBlockNumber().then((b) => toQuantity(b + blockTag)); |
|
} |
|
assertArgument(false, "invalid blockTag", "blockTag", blockTag); |
|
} |
|
/** |
|
* Returns or resolves to a filter for %%filter%%, resolving any ENS |
|
* names or [[Addressable]] object and returning if already a valid |
|
* filter. |
|
*/ |
|
_getFilter(filter) { |
|
// Create a canonical representation of the topics |
|
const topics = (filter.topics || []).map((t) => { |
|
if (t == null) { |
|
return null; |
|
} |
|
if (Array.isArray(t)) { |
|
return concisify(t.map((t) => t.toLowerCase())); |
|
} |
|
return t.toLowerCase(); |
|
}); |
|
const blockHash = ("blockHash" in filter) ? filter.blockHash : undefined; |
|
const resolve = (_address, fromBlock, toBlock) => { |
|
let address = undefined; |
|
switch (_address.length) { |
|
case 0: break; |
|
case 1: |
|
address = _address[0]; |
|
break; |
|
default: |
|
_address.sort(); |
|
address = _address; |
|
} |
|
if (blockHash) { |
|
if (fromBlock != null || toBlock != null) { |
|
throw new Error("invalid filter"); |
|
} |
|
} |
|
const filter = {}; |
|
if (address) { |
|
filter.address = address; |
|
} |
|
if (topics.length) { |
|
filter.topics = topics; |
|
} |
|
if (fromBlock) { |
|
filter.fromBlock = fromBlock; |
|
} |
|
if (toBlock) { |
|
filter.toBlock = toBlock; |
|
} |
|
if (blockHash) { |
|
filter.blockHash = blockHash; |
|
} |
|
return filter; |
|
}; |
|
// Addresses could be async (ENS names or Addressables) |
|
let address = []; |
|
if (filter.address) { |
|
if (Array.isArray(filter.address)) { |
|
for (const addr of filter.address) { |
|
address.push(this._getAddress(addr)); |
|
} |
|
} |
|
else { |
|
address.push(this._getAddress(filter.address)); |
|
} |
|
} |
|
let fromBlock = undefined; |
|
if ("fromBlock" in filter) { |
|
fromBlock = this._getBlockTag(filter.fromBlock); |
|
} |
|
let toBlock = undefined; |
|
if ("toBlock" in filter) { |
|
toBlock = this._getBlockTag(filter.toBlock); |
|
} |
|
if (address.filter((a) => (typeof (a) !== "string")).length || |
|
(fromBlock != null && typeof (fromBlock) !== "string") || |
|
(toBlock != null && typeof (toBlock) !== "string")) { |
|
return Promise.all([Promise.all(address), fromBlock, toBlock]).then((result) => { |
|
return resolve(result[0], result[1], result[2]); |
|
}); |
|
} |
|
return resolve(address, fromBlock, toBlock); |
|
} |
|
/** |
|
* Returns or resolves to a transaction for %%request%%, resolving |
|
* any ENS names or [[Addressable]] and returning if already a valid |
|
* transaction. |
|
*/ |
|
_getTransactionRequest(_request) { |
|
const request = copyRequest(_request); |
|
const promises = []; |
|
["to", "from"].forEach((key) => { |
|
if (request[key] == null) { |
|
return; |
|
} |
|
const addr = resolveAddress(request[key], this); |
|
if (isPromise(addr)) { |
|
promises.push((async function () { request[key] = await addr; })()); |
|
} |
|
else { |
|
request[key] = addr; |
|
} |
|
}); |
|
if (request.blockTag != null) { |
|
const blockTag = this._getBlockTag(request.blockTag); |
|
if (isPromise(blockTag)) { |
|
promises.push((async function () { request.blockTag = await blockTag; })()); |
|
} |
|
else { |
|
request.blockTag = blockTag; |
|
} |
|
} |
|
if (promises.length) { |
|
return (async function () { |
|
await Promise.all(promises); |
|
return request; |
|
})(); |
|
} |
|
return request; |
|
} |
|
async getNetwork() { |
|
// No explicit network was set and this is our first time |
|
if (this.#networkPromise == null) { |
|
// Detect the current network (shared with all calls) |
|
const detectNetwork = (async () => { |
|
try { |
|
const network = await this._detectNetwork(); |
|
this.emit("network", network, null); |
|
return network; |
|
} |
|
catch (error) { |
|
if (this.#networkPromise === detectNetwork) { |
|
this.#networkPromise = null; |
|
} |
|
throw error; |
|
} |
|
})(); |
|
this.#networkPromise = detectNetwork; |
|
return (await detectNetwork).clone(); |
|
} |
|
const networkPromise = this.#networkPromise; |
|
const [expected, actual] = await Promise.all([ |
|
networkPromise, |
|
this._detectNetwork() // The actual connected network |
|
]); |
|
if (expected.chainId !== actual.chainId) { |
|
if (this.#anyNetwork) { |
|
// The "any" network can change, so notify listeners |
|
this.emit("network", actual, expected); |
|
// Update the network if something else hasn't already changed it |
|
if (this.#networkPromise === networkPromise) { |
|
this.#networkPromise = Promise.resolve(actual); |
|
} |
|
} |
|
else { |
|
// Otherwise, we do not allow changes to the underlying network |
|
assert(false, `network changed: ${expected.chainId} => ${actual.chainId} `, "NETWORK_ERROR", { |
|
event: "changed" |
|
}); |
|
} |
|
} |
|
return expected.clone(); |
|
} |
|
async getFeeData() { |
|
const network = await this.getNetwork(); |
|
const getFeeDataFunc = async () => { |
|
const { _block, gasPrice, priorityFee } = await resolveProperties({ |
|
_block: this.#getBlock("latest", false), |
|
gasPrice: ((async () => { |
|
try { |
|
const value = await this.#perform({ method: "getGasPrice" }); |
|
return getBigInt(value, "%response"); |
|
} |
|
catch (error) { } |
|
return null; |
|
})()), |
|
priorityFee: ((async () => { |
|
try { |
|
const value = await this.#perform({ method: "getPriorityFee" }); |
|
return getBigInt(value, "%response"); |
|
} |
|
catch (error) { } |
|
return null; |
|
})()) |
|
}); |
|
let maxFeePerGas = null; |
|
let maxPriorityFeePerGas = null; |
|
// These are the recommended EIP-1559 heuristics for fee data |
|
const block = this._wrapBlock(_block, network); |
|
if (block && block.baseFeePerGas) { |
|
maxPriorityFeePerGas = (priorityFee != null) ? priorityFee : BigInt("1000000000"); |
|
maxFeePerGas = (block.baseFeePerGas * BN_2) + maxPriorityFeePerGas; |
|
} |
|
return new FeeData(gasPrice, maxFeePerGas, maxPriorityFeePerGas); |
|
}; |
|
// Check for a FeeDataNetWorkPlugin |
|
const plugin = network.getPlugin("org.ethers.plugins.network.FetchUrlFeeDataPlugin"); |
|
if (plugin) { |
|
const req = new FetchRequest(plugin.url); |
|
const feeData = await plugin.processFunc(getFeeDataFunc, this, req); |
|
return new FeeData(feeData.gasPrice, feeData.maxFeePerGas, feeData.maxPriorityFeePerGas); |
|
} |
|
return await getFeeDataFunc(); |
|
} |
|
async estimateGas(_tx) { |
|
let tx = this._getTransactionRequest(_tx); |
|
if (isPromise(tx)) { |
|
tx = await tx; |
|
} |
|
return getBigInt(await this.#perform({ |
|
method: "estimateGas", transaction: tx |
|
}), "%response"); |
|
} |
|
async #call(tx, blockTag, attempt) { |
|
assert(attempt < MAX_CCIP_REDIRECTS, "CCIP read exceeded maximum redirections", "OFFCHAIN_FAULT", { |
|
reason: "TOO_MANY_REDIRECTS", |
|
transaction: Object.assign({}, tx, { blockTag, enableCcipRead: true }) |
|
}); |
|
// This came in as a PerformActionTransaction, so to/from are safe; we can cast |
|
const transaction = copyRequest(tx); |
|
try { |
|
return hexlify(await this._perform({ method: "call", transaction, blockTag })); |
|
} |
|
catch (error) { |
|
// CCIP Read OffchainLookup |
|
if (!this.disableCcipRead && isCallException(error) && error.data && attempt >= 0 && blockTag === "latest" && transaction.to != null && dataSlice(error.data, 0, 4) === "0x556f1830") { |
|
const data = error.data; |
|
const txSender = await resolveAddress(transaction.to, this); |
|
// Parse the CCIP Read Arguments |
|
let ccipArgs; |
|
try { |
|
ccipArgs = parseOffchainLookup(dataSlice(error.data, 4)); |
|
} |
|
catch (error) { |
|
assert(false, error.message, "OFFCHAIN_FAULT", { |
|
reason: "BAD_DATA", transaction, info: { data } |
|
}); |
|
} |
|
// Check the sender of the OffchainLookup matches the transaction |
|
assert(ccipArgs.sender.toLowerCase() === txSender.toLowerCase(), "CCIP Read sender mismatch", "CALL_EXCEPTION", { |
|
action: "call", |
|
data, |
|
reason: "OffchainLookup", |
|
transaction: transaction, |
|
invocation: null, |
|
revert: { |
|
signature: "OffchainLookup(address,string[],bytes,bytes4,bytes)", |
|
name: "OffchainLookup", |
|
args: ccipArgs.errorArgs |
|
} |
|
}); |
|
const ccipResult = await this.ccipReadFetch(transaction, ccipArgs.calldata, ccipArgs.urls); |
|
assert(ccipResult != null, "CCIP Read failed to fetch data", "OFFCHAIN_FAULT", { |
|
reason: "FETCH_FAILED", transaction, info: { data: error.data, errorArgs: ccipArgs.errorArgs } |
|
}); |
|
const tx = { |
|
to: txSender, |
|
data: concat([ccipArgs.selector, encodeBytes([ccipResult, ccipArgs.extraData])]) |
|
}; |
|
this.emit("debug", { action: "sendCcipReadCall", transaction: tx }); |
|
try { |
|
const result = await this.#call(tx, blockTag, attempt + 1); |
|
this.emit("debug", { action: "receiveCcipReadCallResult", transaction: Object.assign({}, tx), result }); |
|
return result; |
|
} |
|
catch (error) { |
|
this.emit("debug", { action: "receiveCcipReadCallError", transaction: Object.assign({}, tx), error }); |
|
throw error; |
|
} |
|
} |
|
throw error; |
|
} |
|
} |
|
async #checkNetwork(promise) { |
|
const { value } = await resolveProperties({ |
|
network: this.getNetwork(), |
|
value: promise |
|
}); |
|
return value; |
|
} |
|
async call(_tx) { |
|
const { tx, blockTag } = await resolveProperties({ |
|
tx: this._getTransactionRequest(_tx), |
|
blockTag: this._getBlockTag(_tx.blockTag) |
|
}); |
|
return await this.#checkNetwork(this.#call(tx, blockTag, _tx.enableCcipRead ? 0 : -1)); |
|
} |
|
// Account |
|
async #getAccountValue(request, _address, _blockTag) { |
|
let address = this._getAddress(_address); |
|
let blockTag = this._getBlockTag(_blockTag); |
|
if (typeof (address) !== "string" || typeof (blockTag) !== "string") { |
|
[address, blockTag] = await Promise.all([address, blockTag]); |
|
} |
|
return await this.#checkNetwork(this.#perform(Object.assign(request, { address, blockTag }))); |
|
} |
|
async getBalance(address, blockTag) { |
|
return getBigInt(await this.#getAccountValue({ method: "getBalance" }, address, blockTag), "%response"); |
|
} |
|
async getTransactionCount(address, blockTag) { |
|
return getNumber(await this.#getAccountValue({ method: "getTransactionCount" }, address, blockTag), "%response"); |
|
} |
|
async getCode(address, blockTag) { |
|
return hexlify(await this.#getAccountValue({ method: "getCode" }, address, blockTag)); |
|
} |
|
async getStorage(address, _position, blockTag) { |
|
const position = getBigInt(_position, "position"); |
|
return hexlify(await this.#getAccountValue({ method: "getStorage", position }, address, blockTag)); |
|
} |
|
// Write |
|
async broadcastTransaction(signedTx) { |
|
const { blockNumber, hash, network } = await resolveProperties({ |
|
blockNumber: this.getBlockNumber(), |
|
hash: this._perform({ |
|
method: "broadcastTransaction", |
|
signedTransaction: signedTx |
|
}), |
|
network: this.getNetwork() |
|
}); |
|
const tx = Transaction.from(signedTx); |
|
if (tx.hash !== hash) { |
|
throw new Error("@TODO: the returned hash did not match"); |
|
} |
|
return this._wrapTransactionResponse(tx, network).replaceableTransaction(blockNumber); |
|
} |
|
async #getBlock(block, includeTransactions) { |
|
// @TODO: Add CustomBlockPlugin check |
|
if (isHexString(block, 32)) { |
|
return await this.#perform({ |
|
method: "getBlock", blockHash: block, includeTransactions |
|
}); |
|
} |
|
let blockTag = this._getBlockTag(block); |
|
if (typeof (blockTag) !== "string") { |
|
blockTag = await blockTag; |
|
} |
|
return await this.#perform({ |
|
method: "getBlock", blockTag, includeTransactions |
|
}); |
|
} |
|
// Queries |
|
async getBlock(block, prefetchTxs) { |
|
const { network, params } = await resolveProperties({ |
|
network: this.getNetwork(), |
|
params: this.#getBlock(block, !!prefetchTxs) |
|
}); |
|
if (params == null) { |
|
return null; |
|
} |
|
return this._wrapBlock(params, network); |
|
} |
|
async getTransaction(hash) { |
|
const { network, params } = await resolveProperties({ |
|
network: this.getNetwork(), |
|
params: this.#perform({ method: "getTransaction", hash }) |
|
}); |
|
if (params == null) { |
|
return null; |
|
} |
|
return this._wrapTransactionResponse(params, network); |
|
} |
|
async getTransactionReceipt(hash) { |
|
const { network, params } = await resolveProperties({ |
|
network: this.getNetwork(), |
|
params: this.#perform({ method: "getTransactionReceipt", hash }) |
|
}); |
|
if (params == null) { |
|
return null; |
|
} |
|
// Some backends did not backfill the effectiveGasPrice into old transactions |
|
// in the receipt, so we look it up manually and inject it. |
|
if (params.gasPrice == null && params.effectiveGasPrice == null) { |
|
const tx = await this.#perform({ method: "getTransaction", hash }); |
|
if (tx == null) { |
|
throw new Error("report this; could not find tx or effectiveGasPrice"); |
|
} |
|
params.effectiveGasPrice = tx.gasPrice; |
|
} |
|
return this._wrapTransactionReceipt(params, network); |
|
} |
|
async getTransactionResult(hash) { |
|
const { result } = await resolveProperties({ |
|
network: this.getNetwork(), |
|
result: this.#perform({ method: "getTransactionResult", hash }) |
|
}); |
|
if (result == null) { |
|
return null; |
|
} |
|
return hexlify(result); |
|
} |
|
// Bloom-filter Queries |
|
async getLogs(_filter) { |
|
let filter = this._getFilter(_filter); |
|
if (isPromise(filter)) { |
|
filter = await filter; |
|
} |
|
const { network, params } = await resolveProperties({ |
|
network: this.getNetwork(), |
|
params: this.#perform({ method: "getLogs", filter }) |
|
}); |
|
return params.map((p) => this._wrapLog(p, network)); |
|
} |
|
// ENS |
|
_getProvider(chainId) { |
|
assert(false, "provider cannot connect to target network", "UNSUPPORTED_OPERATION", { |
|
operation: "_getProvider()" |
|
}); |
|
} |
|
async getResolver(name) { |
|
return await EnsResolver.fromName(this, name); |
|
} |
|
async getAvatar(name) { |
|
const resolver = await this.getResolver(name); |
|
if (resolver) { |
|
return await resolver.getAvatar(); |
|
} |
|
return null; |
|
} |
|
async resolveName(name) { |
|
const resolver = await this.getResolver(name); |
|
if (resolver) { |
|
return await resolver.getAddress(); |
|
} |
|
return null; |
|
} |
|
async lookupAddress(address) { |
|
address = getAddress(address); |
|
const node = namehash(address.substring(2).toLowerCase() + ".addr.reverse"); |
|
try { |
|
const ensAddr = await EnsResolver.getEnsAddress(this); |
|
const ensContract = new Contract(ensAddr, [ |
|
"function resolver(bytes32) view returns (address)" |
|
], this); |
|
const resolver = await ensContract.resolver(node); |
|
if (resolver == null || resolver === ZeroAddress) { |
|
return null; |
|
} |
|
const resolverContract = new Contract(resolver, [ |
|
"function name(bytes32) view returns (string)" |
|
], this); |
|
const name = await resolverContract.name(node); |
|
// Failed forward resolution |
|
const check = await this.resolveName(name); |
|
if (check !== address) { |
|
return null; |
|
} |
|
return name; |
|
} |
|
catch (error) { |
|
// No data was returned from the resolver |
|
if (isError(error, "BAD_DATA") && error.value === "0x") { |
|
return null; |
|
} |
|
// Something reerted |
|
if (isError(error, "CALL_EXCEPTION")) { |
|
return null; |
|
} |
|
throw error; |
|
} |
|
return null; |
|
} |
|
async waitForTransaction(hash, _confirms, timeout) { |
|
const confirms = (_confirms != null) ? _confirms : 1; |
|
if (confirms === 0) { |
|
return this.getTransactionReceipt(hash); |
|
} |
|
return new Promise(async (resolve, reject) => { |
|
let timer = null; |
|
const listener = (async (blockNumber) => { |
|
try { |
|
const receipt = await this.getTransactionReceipt(hash); |
|
if (receipt != null) { |
|
if (blockNumber - receipt.blockNumber + 1 >= confirms) { |
|
resolve(receipt); |
|
//this.off("block", listener); |
|
if (timer) { |
|
clearTimeout(timer); |
|
timer = null; |
|
} |
|
return; |
|
} |
|
} |
|
} |
|
catch (error) { |
|
console.log("EEE", error); |
|
} |
|
this.once("block", listener); |
|
}); |
|
if (timeout != null) { |
|
timer = setTimeout(() => { |
|
if (timer == null) { |
|
return; |
|
} |
|
timer = null; |
|
this.off("block", listener); |
|
reject(makeError("timeout", "TIMEOUT", { reason: "timeout" })); |
|
}, timeout); |
|
} |
|
listener(await this.getBlockNumber()); |
|
}); |
|
} |
|
async waitForBlock(blockTag) { |
|
assert(false, "not implemented yet", "NOT_IMPLEMENTED", { |
|
operation: "waitForBlock" |
|
}); |
|
} |
|
/** |
|
* Clear a timer created using the [[_setTimeout]] method. |
|
*/ |
|
_clearTimeout(timerId) { |
|
const timer = this.#timers.get(timerId); |
|
if (!timer) { |
|
return; |
|
} |
|
if (timer.timer) { |
|
clearTimeout(timer.timer); |
|
} |
|
this.#timers.delete(timerId); |
|
} |
|
/** |
|
* Create a timer that will execute %%func%% after at least %%timeout%% |
|
* (in ms). If %%timeout%% is unspecified, then %%func%% will execute |
|
* in the next event loop. |
|
* |
|
* [Pausing](AbstractProvider-paused) the provider will pause any |
|
* associated timers. |
|
*/ |
|
_setTimeout(_func, timeout) { |
|
if (timeout == null) { |
|
timeout = 0; |
|
} |
|
const timerId = this.#nextTimer++; |
|
const func = () => { |
|
this.#timers.delete(timerId); |
|
_func(); |
|
}; |
|
if (this.paused) { |
|
this.#timers.set(timerId, { timer: null, func, time: timeout }); |
|
} |
|
else { |
|
const timer = setTimeout(func, timeout); |
|
this.#timers.set(timerId, { timer, func, time: getTime() }); |
|
} |
|
return timerId; |
|
} |
|
/** |
|
* Perform %%func%% on each subscriber. |
|
*/ |
|
_forEachSubscriber(func) { |
|
for (const sub of this.#subs.values()) { |
|
func(sub.subscriber); |
|
} |
|
} |
|
/** |
|
* Sub-classes may override this to customize subscription |
|
* implementations. |
|
*/ |
|
_getSubscriber(sub) { |
|
switch (sub.type) { |
|
case "debug": |
|
case "error": |
|
case "network": |
|
return new UnmanagedSubscriber(sub.type); |
|
case "block": { |
|
const subscriber = new PollingBlockSubscriber(this); |
|
subscriber.pollingInterval = this.pollingInterval; |
|
return subscriber; |
|
} |
|
case "safe": |
|
case "finalized": |
|
return new PollingBlockTagSubscriber(this, sub.type); |
|
case "event": |
|
return new PollingEventSubscriber(this, sub.filter); |
|
case "transaction": |
|
return new PollingTransactionSubscriber(this, sub.hash); |
|
case "orphan": |
|
return new PollingOrphanSubscriber(this, sub.filter); |
|
} |
|
throw new Error(`unsupported event: ${sub.type}`); |
|
} |
|
/** |
|
* If a [[Subscriber]] fails and needs to replace itself, this |
|
* method may be used. |
|
* |
|
* For example, this is used for providers when using the |
|
* ``eth_getFilterChanges`` method, which can return null if state |
|
* filters are not supported by the backend, allowing the Subscriber |
|
* to swap in a [[PollingEventSubscriber]]. |
|
*/ |
|
_recoverSubscriber(oldSub, newSub) { |
|
for (const sub of this.#subs.values()) { |
|
if (sub.subscriber === oldSub) { |
|
if (sub.started) { |
|
sub.subscriber.stop(); |
|
} |
|
sub.subscriber = newSub; |
|
if (sub.started) { |
|
newSub.start(); |
|
} |
|
if (this.#pausedState != null) { |
|
newSub.pause(this.#pausedState); |
|
} |
|
break; |
|
} |
|
} |
|
} |
|
async #hasSub(event, emitArgs) { |
|
let sub = await getSubscription(event, this); |
|
// This is a log that is removing an existing log; we actually want |
|
// to emit an orphan event for the removed log |
|
if (sub.type === "event" && emitArgs && emitArgs.length > 0 && emitArgs[0].removed === true) { |
|
sub = await getSubscription({ orphan: "drop-log", log: emitArgs[0] }, this); |
|
} |
|
return this.#subs.get(sub.tag) || null; |
|
} |
|
async #getSub(event) { |
|
const subscription = await getSubscription(event, this); |
|
// Prevent tampering with our tag in any subclass' _getSubscriber |
|
const tag = subscription.tag; |
|
let sub = this.#subs.get(tag); |
|
if (!sub) { |
|
const subscriber = this._getSubscriber(subscription); |
|
const addressableMap = new WeakMap(); |
|
const nameMap = new Map(); |
|
sub = { subscriber, tag, addressableMap, nameMap, started: false, listeners: [] }; |
|
this.#subs.set(tag, sub); |
|
} |
|
return sub; |
|
} |
|
async on(event, listener) { |
|
const sub = await this.#getSub(event); |
|
sub.listeners.push({ listener, once: false }); |
|
if (!sub.started) { |
|
sub.subscriber.start(); |
|
sub.started = true; |
|
if (this.#pausedState != null) { |
|
sub.subscriber.pause(this.#pausedState); |
|
} |
|
} |
|
return this; |
|
} |
|
async once(event, listener) { |
|
const sub = await this.#getSub(event); |
|
sub.listeners.push({ listener, once: true }); |
|
if (!sub.started) { |
|
sub.subscriber.start(); |
|
sub.started = true; |
|
if (this.#pausedState != null) { |
|
sub.subscriber.pause(this.#pausedState); |
|
} |
|
} |
|
return this; |
|
} |
|
async emit(event, ...args) { |
|
const sub = await this.#hasSub(event, args); |
|
// If there is not subscription or if a recent emit removed |
|
// the last of them (which also deleted the sub) do nothing |
|
if (!sub || sub.listeners.length === 0) { |
|
return false; |
|
} |
|
; |
|
const count = sub.listeners.length; |
|
sub.listeners = sub.listeners.filter(({ listener, once }) => { |
|
const payload = new EventPayload(this, (once ? null : listener), event); |
|
try { |
|
listener.call(this, ...args, payload); |
|
} |
|
catch (error) { } |
|
return !once; |
|
}); |
|
if (sub.listeners.length === 0) { |
|
if (sub.started) { |
|
sub.subscriber.stop(); |
|
} |
|
this.#subs.delete(sub.tag); |
|
} |
|
return (count > 0); |
|
} |
|
async listenerCount(event) { |
|
if (event) { |
|
const sub = await this.#hasSub(event); |
|
if (!sub) { |
|
return 0; |
|
} |
|
return sub.listeners.length; |
|
} |
|
let total = 0; |
|
for (const { listeners } of this.#subs.values()) { |
|
total += listeners.length; |
|
} |
|
return total; |
|
} |
|
async listeners(event) { |
|
if (event) { |
|
const sub = await this.#hasSub(event); |
|
if (!sub) { |
|
return []; |
|
} |
|
return sub.listeners.map(({ listener }) => listener); |
|
} |
|
let result = []; |
|
for (const { listeners } of this.#subs.values()) { |
|
result = result.concat(listeners.map(({ listener }) => listener)); |
|
} |
|
return result; |
|
} |
|
async off(event, listener) { |
|
const sub = await this.#hasSub(event); |
|
if (!sub) { |
|
return this; |
|
} |
|
if (listener) { |
|
const index = sub.listeners.map(({ listener }) => listener).indexOf(listener); |
|
if (index >= 0) { |
|
sub.listeners.splice(index, 1); |
|
} |
|
} |
|
if (!listener || sub.listeners.length === 0) { |
|
if (sub.started) { |
|
sub.subscriber.stop(); |
|
} |
|
this.#subs.delete(sub.tag); |
|
} |
|
return this; |
|
} |
|
async removeAllListeners(event) { |
|
if (event) { |
|
const { tag, started, subscriber } = await this.#getSub(event); |
|
if (started) { |
|
subscriber.stop(); |
|
} |
|
this.#subs.delete(tag); |
|
} |
|
else { |
|
for (const [tag, { started, subscriber }] of this.#subs) { |
|
if (started) { |
|
subscriber.stop(); |
|
} |
|
this.#subs.delete(tag); |
|
} |
|
} |
|
return this; |
|
} |
|
// Alias for "on" |
|
async addListener(event, listener) { |
|
return await this.on(event, listener); |
|
} |
|
// Alias for "off" |
|
async removeListener(event, listener) { |
|
return this.off(event, listener); |
|
} |
|
/** |
|
* If this provider has been destroyed using the [[destroy]] method. |
|
* |
|
* Once destroyed, all resources are reclaimed, internal event loops |
|
* and timers are cleaned up and no further requests may be sent to |
|
* the provider. |
|
*/ |
|
get destroyed() { |
|
return this.#destroyed; |
|
} |
|
/** |
|
* Sub-classes may use this to shutdown any sockets or release their |
|
* resources and reject any pending requests. |
|
* |
|
* Sub-classes **must** call ``super.destroy()``. |
|
*/ |
|
destroy() { |
|
// Stop all listeners |
|
this.removeAllListeners(); |
|
// Shut down all tiemrs |
|
for (const timerId of this.#timers.keys()) { |
|
this._clearTimeout(timerId); |
|
} |
|
this.#destroyed = true; |
|
} |
|
/** |
|
* Whether the provider is currently paused. |
|
* |
|
* A paused provider will not emit any events, and generally should |
|
* not make any requests to the network, but that is up to sub-classes |
|
* to manage. |
|
* |
|
* Setting ``paused = true`` is identical to calling ``.pause(false)``, |
|
* which will buffer any events that occur while paused until the |
|
* provider is unpaused. |
|
*/ |
|
get paused() { return (this.#pausedState != null); } |
|
set paused(pause) { |
|
if (!!pause === this.paused) { |
|
return; |
|
} |
|
if (this.paused) { |
|
this.resume(); |
|
} |
|
else { |
|
this.pause(false); |
|
} |
|
} |
|
/** |
|
* Pause the provider. If %%dropWhilePaused%%, any events that occur |
|
* while paused are dropped, otherwise all events will be emitted once |
|
* the provider is unpaused. |
|
*/ |
|
pause(dropWhilePaused) { |
|
this.#lastBlockNumber = -1; |
|
if (this.#pausedState != null) { |
|
if (this.#pausedState == !!dropWhilePaused) { |
|
return; |
|
} |
|
assert(false, "cannot change pause type; resume first", "UNSUPPORTED_OPERATION", { |
|
operation: "pause" |
|
}); |
|
} |
|
this._forEachSubscriber((s) => s.pause(dropWhilePaused)); |
|
this.#pausedState = !!dropWhilePaused; |
|
for (const timer of this.#timers.values()) { |
|
// Clear the timer |
|
if (timer.timer) { |
|
clearTimeout(timer.timer); |
|
} |
|
// Remaining time needed for when we become unpaused |
|
timer.time = getTime() - timer.time; |
|
} |
|
} |
|
/** |
|
* Resume the provider. |
|
*/ |
|
resume() { |
|
if (this.#pausedState == null) { |
|
return; |
|
} |
|
this._forEachSubscriber((s) => s.resume()); |
|
this.#pausedState = null; |
|
for (const timer of this.#timers.values()) { |
|
// Remaining time when we were paused |
|
let timeout = timer.time; |
|
if (timeout < 0) { |
|
timeout = 0; |
|
} |
|
// Start time (in cause paused, so we con compute remaininf time) |
|
timer.time = getTime(); |
|
// Start the timer |
|
setTimeout(timer.func, timeout); |
|
} |
|
} |
|
} |
|
function _parseString(result, start) { |
|
try { |
|
const bytes = _parseBytes(result, start); |
|
if (bytes) { |
|
return toUtf8String(bytes); |
|
} |
|
} |
|
catch (error) { } |
|
return null; |
|
} |
|
function _parseBytes(result, start) { |
|
if (result === "0x") { |
|
return null; |
|
} |
|
try { |
|
const offset = getNumber(dataSlice(result, start, start + 32)); |
|
const length = getNumber(dataSlice(result, offset, offset + 32)); |
|
return dataSlice(result, offset + 32, offset + 32 + length); |
|
} |
|
catch (error) { } |
|
return null; |
|
} |
|
function numPad(value) { |
|
const result = toBeArray(value); |
|
if (result.length > 32) { |
|
throw new Error("internal; should not happen"); |
|
} |
|
const padded = new Uint8Array(32); |
|
padded.set(result, 32 - result.length); |
|
return padded; |
|
} |
|
function bytesPad(value) { |
|
if ((value.length % 32) === 0) { |
|
return value; |
|
} |
|
const result = new Uint8Array(Math.ceil(value.length / 32) * 32); |
|
result.set(value); |
|
return result; |
|
} |
|
const empty = new Uint8Array([]); |
|
// ABI Encodes a series of (bytes, bytes, ...) |
|
function encodeBytes(datas) { |
|
const result = []; |
|
let byteCount = 0; |
|
// Add place-holders for pointers as we add items |
|
for (let i = 0; i < datas.length; i++) { |
|
result.push(empty); |
|
byteCount += 32; |
|
} |
|
for (let i = 0; i < datas.length; i++) { |
|
const data = getBytes(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 concat(result); |
|
} |
|
const zeros = "0x0000000000000000000000000000000000000000000000000000000000000000"; |
|
function parseOffchainLookup(data) { |
|
const result = { |
|
sender: "", urls: [], calldata: "", selector: "", extraData: "", errorArgs: [] |
|
}; |
|
assert(dataLength(data) >= 5 * 32, "insufficient OffchainLookup data", "OFFCHAIN_FAULT", { |
|
reason: "insufficient OffchainLookup data" |
|
}); |
|
const sender = dataSlice(data, 0, 32); |
|
assert(dataSlice(sender, 0, 12) === dataSlice(zeros, 0, 12), "corrupt OffchainLookup sender", "OFFCHAIN_FAULT", { |
|
reason: "corrupt OffchainLookup sender" |
|
}); |
|
result.sender = dataSlice(sender, 12); |
|
// Read the URLs from the response |
|
try { |
|
const urls = []; |
|
const urlsOffset = getNumber(dataSlice(data, 32, 64)); |
|
const urlsLength = getNumber(dataSlice(data, urlsOffset, urlsOffset + 32)); |
|
const urlsData = dataSlice(data, urlsOffset + 32); |
|
for (let u = 0; u < urlsLength; u++) { |
|
const url = _parseString(urlsData, u * 32); |
|
if (url == null) { |
|
throw new Error("abort"); |
|
} |
|
urls.push(url); |
|
} |
|
result.urls = urls; |
|
} |
|
catch (error) { |
|
assert(false, "corrupt OffchainLookup urls", "OFFCHAIN_FAULT", { |
|
reason: "corrupt OffchainLookup urls" |
|
}); |
|
} |
|
// Get the CCIP calldata to forward |
|
try { |
|
const calldata = _parseBytes(data, 64); |
|
if (calldata == null) { |
|
throw new Error("abort"); |
|
} |
|
result.calldata = calldata; |
|
} |
|
catch (error) { |
|
assert(false, "corrupt OffchainLookup calldata", "OFFCHAIN_FAULT", { |
|
reason: "corrupt OffchainLookup calldata" |
|
}); |
|
} |
|
// Get the callbackSelector (bytes4) |
|
assert(dataSlice(data, 100, 128) === dataSlice(zeros, 0, 28), "corrupt OffchainLookup callbaackSelector", "OFFCHAIN_FAULT", { |
|
reason: "corrupt OffchainLookup callbaackSelector" |
|
}); |
|
result.selector = dataSlice(data, 96, 100); |
|
// Get the extra data to send back to the contract as context |
|
try { |
|
const extraData = _parseBytes(data, 128); |
|
if (extraData == null) { |
|
throw new Error("abort"); |
|
} |
|
result.extraData = extraData; |
|
} |
|
catch (error) { |
|
assert(false, "corrupt OffchainLookup extraData", "OFFCHAIN_FAULT", { |
|
reason: "corrupt OffchainLookup extraData" |
|
}); |
|
} |
|
result.errorArgs = "sender,urls,calldata,selector,extraData".split(/,/).map((k) => result[k]); |
|
return result; |
|
} |
|
//# sourceMappingURL=abstract-provider.js.map
|