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
|