| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584 | /** *  [[link-etherscan]] provides a third-party service for connecting to *  various blockchains over a combination of JSON-RPC and custom API *  endpoints. * *  **Supported Networks** * *  - Ethereum Mainnet (``mainnet``) *  - Goerli Testnet (``goerli``) *  - Sepolia Testnet (``sepolia``) *  - Holesky Testnet (``holesky``) *  - Arbitrum (``arbitrum``) *  - Arbitrum Goerli Testnet (``arbitrum-goerli``) *  - Base (``base``) *  - Base Sepolia Testnet (``base-sepolia``) *  - BNB Smart Chain Mainnet (``bnb``) *  - BNB Smart Chain Testnet (``bnbt``) *  - Optimism (``optimism``) *  - Optimism Goerli Testnet (``optimism-goerli``) *  - Polygon (``matic``) *  - Polygon Mumbai Testnet (``matic-mumbai``) *  - Polygon Amoy Testnet (``matic-amoy``) * *  @_subsection api/providers/thirdparty:Etherscan  [providers-etherscan] */import { AbiCoder } from "../abi/index.js";import { Contract } from "../contract/index.js";import { accessListify, Transaction } from "../transaction/index.js";import { defineProperties, hexlify, toQuantity, FetchRequest, assert, assertArgument, isError, //    parseUnits,toUtf8String } from "../utils/index.js";import { AbstractProvider } from "./abstract-provider.js";import { Network } from "./network.js";import { NetworkPlugin } from "./plugins-network.js";import { showThrottleMessage } from "./community.js";const THROTTLE = 2000;function isPromise(value) {    return (value && typeof (value.then) === "function");}const EtherscanPluginId = "org.ethers.plugins.provider.Etherscan";/** *  A Network can include an **EtherscanPlugin** to provide *  a custom base URL. * *  @_docloc: api/providers/thirdparty:Etherscan */export class EtherscanPlugin extends NetworkPlugin {    /**     *  The Etherscan API base URL.     */    baseUrl;    /**     *  Creates a new **EtherscanProvider** which will use     *  %%baseUrl%%.     */    constructor(baseUrl) {        super(EtherscanPluginId);        defineProperties(this, { baseUrl });    }    clone() {        return new EtherscanPlugin(this.baseUrl);    }}const skipKeys = ["enableCcipRead"];let nextId = 1;/** *  The **EtherscanBaseProvider** is the super-class of *  [[EtherscanProvider]], which should generally be used instead. * *  Since the **EtherscanProvider** includes additional code for *  [[Contract]] access, in //rare cases// that contracts are not *  used, this class can reduce code size. * *  @_docloc: api/providers/thirdparty:Etherscan */export class EtherscanProvider extends AbstractProvider {    /**     *  The connected network.     */    network;    /**     *  The API key or null if using the community provided bandwidth.     */    apiKey;    #plugin;    /**     *  Creates a new **EtherscanBaseProvider**.     */    constructor(_network, _apiKey) {        const apiKey = (_apiKey != null) ? _apiKey : null;        super();        const network = Network.from(_network);        this.#plugin = network.getPlugin(EtherscanPluginId);        defineProperties(this, { apiKey, network });        // Test that the network is supported by Etherscan        this.getBaseUrl();    }    /**     *  Returns the base URL.     *     *  If an [[EtherscanPlugin]] is configured on the     *  [[EtherscanBaseProvider_network]], returns the plugin's     *  baseUrl.     */    getBaseUrl() {        if (this.#plugin) {            return this.#plugin.baseUrl;        }        switch (this.network.name) {            case "mainnet":                return "https:/\/api.etherscan.io";            case "goerli":                return "https:/\/api-goerli.etherscan.io";            case "sepolia":                return "https:/\/api-sepolia.etherscan.io";            case "holesky":                return "https:/\/api-holesky.etherscan.io";            case "arbitrum":                return "https:/\/api.arbiscan.io";            case "arbitrum-goerli":                return "https:/\/api-goerli.arbiscan.io";            case "base":                return "https:/\/api.basescan.org";            case "base-sepolia":                return "https:/\/api-sepolia.basescan.org";            case "bnb":                return "https:/\/api.bscscan.com";            case "bnbt":                return "https:/\/api-testnet.bscscan.com";            case "matic":                return "https:/\/api.polygonscan.com";            case "matic-amoy":                return "https:/\/api-amoy.polygonscan.com";            case "matic-mumbai":                return "https:/\/api-testnet.polygonscan.com";            case "optimism":                return "https:/\/api-optimistic.etherscan.io";            case "optimism-goerli":                return "https:/\/api-goerli-optimistic.etherscan.io";            default:        }        assertArgument(false, "unsupported network", "network", this.network);    }    /**     *  Returns the URL for the %%module%% and %%params%%.     */    getUrl(module, params) {        const query = Object.keys(params).reduce((accum, key) => {            const value = params[key];            if (value != null) {                accum += `&${key}=${value}`;            }            return accum;        }, "");        const apiKey = ((this.apiKey) ? `&apikey=${this.apiKey}` : "");        return `${this.getBaseUrl()}/api?module=${module}${query}${apiKey}`;    }    /**     *  Returns the URL for using POST requests.     */    getPostUrl() {        return `${this.getBaseUrl()}/api`;    }    /**     *  Returns the parameters for using POST requests.     */    getPostData(module, params) {        params.module = module;        params.apikey = this.apiKey;        return params;    }    async detectNetwork() {        return this.network;    }    /**     *  Resolves to the result of calling %%module%% with %%params%%.     *     *  If %%post%%, the request is made as a POST request.     */    async fetch(module, params, post) {        const id = nextId++;        const url = (post ? this.getPostUrl() : this.getUrl(module, params));        const payload = (post ? this.getPostData(module, params) : null);        this.emit("debug", { action: "sendRequest", id, url, payload: payload });        const request = new FetchRequest(url);        request.setThrottleParams({ slotInterval: 1000 });        request.retryFunc = (req, resp, attempt) => {            if (this.isCommunityResource()) {                showThrottleMessage("Etherscan");            }            return Promise.resolve(true);        };        request.processFunc = async (request, response) => {            const result = response.hasBody() ? JSON.parse(toUtf8String(response.body)) : {};            const throttle = ((typeof (result.result) === "string") ? result.result : "").toLowerCase().indexOf("rate limit") >= 0;            if (module === "proxy") {                // This JSON response indicates we are being throttled                if (result && result.status == 0 && result.message == "NOTOK" && throttle) {                    this.emit("debug", { action: "receiveError", id, reason: "proxy-NOTOK", error: result });                    response.throwThrottleError(result.result, THROTTLE);                }            }            else {                if (throttle) {                    this.emit("debug", { action: "receiveError", id, reason: "null result", error: result.result });                    response.throwThrottleError(result.result, THROTTLE);                }            }            return response;        };        if (payload) {            request.setHeader("content-type", "application/x-www-form-urlencoded; charset=UTF-8");            request.body = Object.keys(payload).map((k) => `${k}=${payload[k]}`).join("&");        }        const response = await request.send();        try {            response.assertOk();        }        catch (error) {            this.emit("debug", { action: "receiveError", id, error, reason: "assertOk" });            assert(false, "response error", "SERVER_ERROR", { request, response });        }        if (!response.hasBody()) {            this.emit("debug", { action: "receiveError", id, error: "missing body", reason: "null body" });            assert(false, "missing response", "SERVER_ERROR", { request, response });        }        const result = JSON.parse(toUtf8String(response.body));        if (module === "proxy") {            if (result.jsonrpc != "2.0") {                this.emit("debug", { action: "receiveError", id, result, reason: "invalid JSON-RPC" });                assert(false, "invalid JSON-RPC response (missing jsonrpc='2.0')", "SERVER_ERROR", { request, response, info: { result } });            }            if (result.error) {                this.emit("debug", { action: "receiveError", id, result, reason: "JSON-RPC error" });                assert(false, "error response", "SERVER_ERROR", { request, response, info: { result } });            }            this.emit("debug", { action: "receiveRequest", id, result });            return result.result;        }        else {            // getLogs, getHistory have weird success responses            if (result.status == 0 && (result.message === "No records found" || result.message === "No transactions found")) {                this.emit("debug", { action: "receiveRequest", id, result });                return result.result;            }            if (result.status != 1 || (typeof (result.message) === "string" && !result.message.match(/^OK/))) {                this.emit("debug", { action: "receiveError", id, result });                assert(false, "error response", "SERVER_ERROR", { request, response, info: { result } });            }            this.emit("debug", { action: "receiveRequest", id, result });            return result.result;        }    }    /**     *  Returns %%transaction%% normalized for the Etherscan API.     */    _getTransactionPostData(transaction) {        const result = {};        for (let key in transaction) {            if (skipKeys.indexOf(key) >= 0) {                continue;            }            if (transaction[key] == null) {                continue;            }            let value = transaction[key];            if (key === "type" && value === 0) {                continue;            }            if (key === "blockTag" && value === "latest") {                continue;            }            // Quantity-types require no leading zero, unless 0            if ({ type: true, gasLimit: true, gasPrice: true, maxFeePerGs: true, maxPriorityFeePerGas: true, nonce: true, value: true }[key]) {                value = toQuantity(value);            }            else if (key === "accessList") {                value = "[" + accessListify(value).map((set) => {                    return `{address:"${set.address}",storageKeys:["${set.storageKeys.join('","')}"]}`;                }).join(",") + "]";            }            else if (key === "blobVersionedHashes") {                if (value.length === 0) {                    continue;                }                // @TODO: update this once the API supports blobs                assert(false, "Etherscan API does not support blobVersionedHashes", "UNSUPPORTED_OPERATION", {                    operation: "_getTransactionPostData",                    info: { transaction }                });            }            else {                value = hexlify(value);            }            result[key] = value;        }        return result;    }    /**     *  Throws the normalized Etherscan error.     */    _checkError(req, error, transaction) {        // Pull any message out if, possible        let message = "";        if (isError(error, "SERVER_ERROR")) {            // Check for an error emitted by a proxy call            try {                message = error.info.result.error.message;            }            catch (e) { }            if (!message) {                try {                    message = error.info.message;                }                catch (e) { }            }        }        if (req.method === "estimateGas") {            if (!message.match(/revert/i) && message.match(/insufficient funds/i)) {                assert(false, "insufficient funds", "INSUFFICIENT_FUNDS", {                    transaction: req.transaction                });            }        }        if (req.method === "call" || req.method === "estimateGas") {            if (message.match(/execution reverted/i)) {                let data = "";                try {                    data = error.info.result.error.data;                }                catch (error) { }                const e = AbiCoder.getBuiltinCallException(req.method, req.transaction, data);                e.info = { request: req, error };                throw e;            }        }        if (message) {            if (req.method === "broadcastTransaction") {                const transaction = Transaction.from(req.signedTransaction);                if (message.match(/replacement/i) && message.match(/underpriced/i)) {                    assert(false, "replacement fee too low", "REPLACEMENT_UNDERPRICED", {                        transaction                    });                }                if (message.match(/insufficient funds/)) {                    assert(false, "insufficient funds for intrinsic transaction cost", "INSUFFICIENT_FUNDS", {                        transaction                    });                }                if (message.match(/same hash was already imported|transaction nonce is too low|nonce too low/)) {                    assert(false, "nonce has already been used", "NONCE_EXPIRED", {                        transaction                    });                }            }        }        // Something we could not process        throw error;    }    async _detectNetwork() {        return this.network;    }    async _perform(req) {        switch (req.method) {            case "chainId":                return this.network.chainId;            case "getBlockNumber":                return this.fetch("proxy", { action: "eth_blockNumber" });            case "getGasPrice":                return this.fetch("proxy", { action: "eth_gasPrice" });            case "getPriorityFee":                // This is temporary until Etherscan completes support                if (this.network.name === "mainnet") {                    return "1000000000";                }                else if (this.network.name === "optimism") {                    return "1000000";                }                else {                    throw new Error("fallback onto the AbstractProvider default");                }            /* Working with Etherscan to get this added:            try {                const test = await this.fetch("proxy", {                    action: "eth_maxPriorityFeePerGas"                });                console.log(test);                return test;            } catch (e) {                console.log("DEBUG", e);                throw e;            }            */            /* This might be safe; but due to rounding neither myself               or Etherscan are necessarily comfortable with this. :)            try {                const result = await this.fetch("gastracker", { action: "gasoracle" });                console.log(result);                const gasPrice = parseUnits(result.SafeGasPrice, "gwei");                const baseFee = parseUnits(result.suggestBaseFee, "gwei");                const priorityFee = gasPrice - baseFee;                if (priorityFee < 0) { throw new Error("negative priority fee; defer to abstract provider default"); }                return priorityFee;            } catch (error) {                console.log("DEBUG", error);                throw error;            }            */            case "getBalance":                // Returns base-10 result                return this.fetch("account", {                    action: "balance",                    address: req.address,                    tag: req.blockTag                });            case "getTransactionCount":                return this.fetch("proxy", {                    action: "eth_getTransactionCount",                    address: req.address,                    tag: req.blockTag                });            case "getCode":                return this.fetch("proxy", {                    action: "eth_getCode",                    address: req.address,                    tag: req.blockTag                });            case "getStorage":                return this.fetch("proxy", {                    action: "eth_getStorageAt",                    address: req.address,                    position: req.position,                    tag: req.blockTag                });            case "broadcastTransaction":                return this.fetch("proxy", {                    action: "eth_sendRawTransaction",                    hex: req.signedTransaction                }, true).catch((error) => {                    return this._checkError(req, error, req.signedTransaction);                });            case "getBlock":                if ("blockTag" in req) {                    return this.fetch("proxy", {                        action: "eth_getBlockByNumber",                        tag: req.blockTag,                        boolean: (req.includeTransactions ? "true" : "false")                    });                }                assert(false, "getBlock by blockHash not supported by Etherscan", "UNSUPPORTED_OPERATION", {                    operation: "getBlock(blockHash)"                });            case "getTransaction":                return this.fetch("proxy", {                    action: "eth_getTransactionByHash",                    txhash: req.hash                });            case "getTransactionReceipt":                return this.fetch("proxy", {                    action: "eth_getTransactionReceipt",                    txhash: req.hash                });            case "call": {                if (req.blockTag !== "latest") {                    throw new Error("EtherscanProvider does not support blockTag for call");                }                const postData = this._getTransactionPostData(req.transaction);                postData.module = "proxy";                postData.action = "eth_call";                try {                    return await this.fetch("proxy", postData, true);                }                catch (error) {                    return this._checkError(req, error, req.transaction);                }            }            case "estimateGas": {                const postData = this._getTransactionPostData(req.transaction);                postData.module = "proxy";                postData.action = "eth_estimateGas";                try {                    return await this.fetch("proxy", postData, true);                }                catch (error) {                    return this._checkError(req, error, req.transaction);                }            }            /*                        case "getLogs": {                            // Needs to complain if more than one address is passed in                            const args: Record<string, any> = { action: "getLogs" }                                        if (params.filter.fromBlock) {                                args.fromBlock = checkLogTag(params.filter.fromBlock);                            }                                        if (params.filter.toBlock) {                                args.toBlock = checkLogTag(params.filter.toBlock);                            }                                        if (params.filter.address) {                                args.address = params.filter.address;                            }                                        // @TODO: We can handle slightly more complicated logs using the logs API                            if (params.filter.topics && params.filter.topics.length > 0) {                                if (params.filter.topics.length > 1) {                                    logger.throwError("unsupported topic count", Logger.Errors.UNSUPPORTED_OPERATION, { topics: params.filter.topics });                                }                                if (params.filter.topics.length === 1) {                                    const topic0 = params.filter.topics[0];                                    if (typeof(topic0) !== "string" || topic0.length !== 66) {                                        logger.throwError("unsupported topic format", Logger.Errors.UNSUPPORTED_OPERATION, { topic0: topic0 });                                    }                                    args.topic0 = topic0;                                }                            }                                        const logs: Array<any> = await this.fetch("logs", args);                                        // Cache txHash => blockHash                            let blocks: { [tag: string]: string } = {};                                        // Add any missing blockHash to the logs                            for (let i = 0; i < logs.length; i++) {                                const log = logs[i];                                if (log.blockHash != null) { continue; }                                if (blocks[log.blockNumber] == null) {                                    const block = await this.getBlock(log.blockNumber);                                    if (block) {                                        blocks[log.blockNumber] = block.hash;                                    }                                }                                            log.blockHash = blocks[log.blockNumber];                            }                                        return logs;                        }            */            default:                break;        }        return super._perform(req);    }    async getNetwork() {        return this.network;    }    /**     *  Resolves to the current price of ether.     *     *  This returns ``0`` on any network other than ``mainnet``.     */    async getEtherPrice() {        if (this.network.name !== "mainnet") {            return 0.0;        }        return parseFloat((await this.fetch("stats", { action: "ethprice" })).ethusd);    }    /**     *  Resolves to a [Contract]] for %%address%%, using the     *  Etherscan API to retreive the Contract ABI.     */    async getContract(_address) {        let address = this._getAddress(_address);        if (isPromise(address)) {            address = await address;        }        try {            const resp = await this.fetch("contract", {                action: "getabi", address            });            const abi = JSON.parse(resp);            return new Contract(address, abi, this);        }        catch (error) {            return null;        }    }    isCommunityResource() {        return (this.apiKey == null);    }}//# sourceMappingURL=provider-etherscan.js.map
 |