provider-etherscan.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. "use strict";
  2. /**
  3. * [[link-etherscan]] provides a third-party service for connecting to
  4. * various blockchains over a combination of JSON-RPC and custom API
  5. * endpoints.
  6. *
  7. * **Supported Networks**
  8. *
  9. * - Ethereum Mainnet (``mainnet``)
  10. * - Goerli Testnet (``goerli``)
  11. * - Sepolia Testnet (``sepolia``)
  12. * - Holesky Testnet (``holesky``)
  13. * - Arbitrum (``arbitrum``)
  14. * - Arbitrum Goerli Testnet (``arbitrum-goerli``)
  15. * - Base (``base``)
  16. * - Base Sepolia Testnet (``base-sepolia``)
  17. * - BNB Smart Chain Mainnet (``bnb``)
  18. * - BNB Smart Chain Testnet (``bnbt``)
  19. * - Optimism (``optimism``)
  20. * - Optimism Goerli Testnet (``optimism-goerli``)
  21. * - Polygon (``matic``)
  22. * - Polygon Mumbai Testnet (``matic-mumbai``)
  23. * - Polygon Amoy Testnet (``matic-amoy``)
  24. *
  25. * @_subsection api/providers/thirdparty:Etherscan [providers-etherscan]
  26. */
  27. Object.defineProperty(exports, "__esModule", { value: true });
  28. exports.EtherscanProvider = exports.EtherscanPlugin = void 0;
  29. const index_js_1 = require("../abi/index.js");
  30. const index_js_2 = require("../contract/index.js");
  31. const index_js_3 = require("../transaction/index.js");
  32. const index_js_4 = require("../utils/index.js");
  33. const abstract_provider_js_1 = require("./abstract-provider.js");
  34. const network_js_1 = require("./network.js");
  35. const plugins_network_js_1 = require("./plugins-network.js");
  36. const community_js_1 = require("./community.js");
  37. const THROTTLE = 2000;
  38. function isPromise(value) {
  39. return (value && typeof (value.then) === "function");
  40. }
  41. const EtherscanPluginId = "org.ethers.plugins.provider.Etherscan";
  42. /**
  43. * A Network can include an **EtherscanPlugin** to provide
  44. * a custom base URL.
  45. *
  46. * @_docloc: api/providers/thirdparty:Etherscan
  47. */
  48. class EtherscanPlugin extends plugins_network_js_1.NetworkPlugin {
  49. /**
  50. * The Etherscan API base URL.
  51. */
  52. baseUrl;
  53. /**
  54. * Creates a new **EtherscanProvider** which will use
  55. * %%baseUrl%%.
  56. */
  57. constructor(baseUrl) {
  58. super(EtherscanPluginId);
  59. (0, index_js_4.defineProperties)(this, { baseUrl });
  60. }
  61. clone() {
  62. return new EtherscanPlugin(this.baseUrl);
  63. }
  64. }
  65. exports.EtherscanPlugin = EtherscanPlugin;
  66. const skipKeys = ["enableCcipRead"];
  67. let nextId = 1;
  68. /**
  69. * The **EtherscanBaseProvider** is the super-class of
  70. * [[EtherscanProvider]], which should generally be used instead.
  71. *
  72. * Since the **EtherscanProvider** includes additional code for
  73. * [[Contract]] access, in //rare cases// that contracts are not
  74. * used, this class can reduce code size.
  75. *
  76. * @_docloc: api/providers/thirdparty:Etherscan
  77. */
  78. class EtherscanProvider extends abstract_provider_js_1.AbstractProvider {
  79. /**
  80. * The connected network.
  81. */
  82. network;
  83. /**
  84. * The API key or null if using the community provided bandwidth.
  85. */
  86. apiKey;
  87. #plugin;
  88. /**
  89. * Creates a new **EtherscanBaseProvider**.
  90. */
  91. constructor(_network, _apiKey) {
  92. const apiKey = (_apiKey != null) ? _apiKey : null;
  93. super();
  94. const network = network_js_1.Network.from(_network);
  95. this.#plugin = network.getPlugin(EtherscanPluginId);
  96. (0, index_js_4.defineProperties)(this, { apiKey, network });
  97. // Test that the network is supported by Etherscan
  98. this.getBaseUrl();
  99. }
  100. /**
  101. * Returns the base URL.
  102. *
  103. * If an [[EtherscanPlugin]] is configured on the
  104. * [[EtherscanBaseProvider_network]], returns the plugin's
  105. * baseUrl.
  106. */
  107. getBaseUrl() {
  108. if (this.#plugin) {
  109. return this.#plugin.baseUrl;
  110. }
  111. switch (this.network.name) {
  112. case "mainnet":
  113. return "https:/\/api.etherscan.io";
  114. case "goerli":
  115. return "https:/\/api-goerli.etherscan.io";
  116. case "sepolia":
  117. return "https:/\/api-sepolia.etherscan.io";
  118. case "holesky":
  119. return "https:/\/api-holesky.etherscan.io";
  120. case "arbitrum":
  121. return "https:/\/api.arbiscan.io";
  122. case "arbitrum-goerli":
  123. return "https:/\/api-goerli.arbiscan.io";
  124. case "base":
  125. return "https:/\/api.basescan.org";
  126. case "base-sepolia":
  127. return "https:/\/api-sepolia.basescan.org";
  128. case "bnb":
  129. return "https:/\/api.bscscan.com";
  130. case "bnbt":
  131. return "https:/\/api-testnet.bscscan.com";
  132. case "matic":
  133. return "https:/\/api.polygonscan.com";
  134. case "matic-amoy":
  135. return "https:/\/api-amoy.polygonscan.com";
  136. case "matic-mumbai":
  137. return "https:/\/api-testnet.polygonscan.com";
  138. case "optimism":
  139. return "https:/\/api-optimistic.etherscan.io";
  140. case "optimism-goerli":
  141. return "https:/\/api-goerli-optimistic.etherscan.io";
  142. default:
  143. }
  144. (0, index_js_4.assertArgument)(false, "unsupported network", "network", this.network);
  145. }
  146. /**
  147. * Returns the URL for the %%module%% and %%params%%.
  148. */
  149. getUrl(module, params) {
  150. const query = Object.keys(params).reduce((accum, key) => {
  151. const value = params[key];
  152. if (value != null) {
  153. accum += `&${key}=${value}`;
  154. }
  155. return accum;
  156. }, "");
  157. const apiKey = ((this.apiKey) ? `&apikey=${this.apiKey}` : "");
  158. return `${this.getBaseUrl()}/api?module=${module}${query}${apiKey}`;
  159. }
  160. /**
  161. * Returns the URL for using POST requests.
  162. */
  163. getPostUrl() {
  164. return `${this.getBaseUrl()}/api`;
  165. }
  166. /**
  167. * Returns the parameters for using POST requests.
  168. */
  169. getPostData(module, params) {
  170. params.module = module;
  171. params.apikey = this.apiKey;
  172. return params;
  173. }
  174. async detectNetwork() {
  175. return this.network;
  176. }
  177. /**
  178. * Resolves to the result of calling %%module%% with %%params%%.
  179. *
  180. * If %%post%%, the request is made as a POST request.
  181. */
  182. async fetch(module, params, post) {
  183. const id = nextId++;
  184. const url = (post ? this.getPostUrl() : this.getUrl(module, params));
  185. const payload = (post ? this.getPostData(module, params) : null);
  186. this.emit("debug", { action: "sendRequest", id, url, payload: payload });
  187. const request = new index_js_4.FetchRequest(url);
  188. request.setThrottleParams({ slotInterval: 1000 });
  189. request.retryFunc = (req, resp, attempt) => {
  190. if (this.isCommunityResource()) {
  191. (0, community_js_1.showThrottleMessage)("Etherscan");
  192. }
  193. return Promise.resolve(true);
  194. };
  195. request.processFunc = async (request, response) => {
  196. const result = response.hasBody() ? JSON.parse((0, index_js_4.toUtf8String)(response.body)) : {};
  197. const throttle = ((typeof (result.result) === "string") ? result.result : "").toLowerCase().indexOf("rate limit") >= 0;
  198. if (module === "proxy") {
  199. // This JSON response indicates we are being throttled
  200. if (result && result.status == 0 && result.message == "NOTOK" && throttle) {
  201. this.emit("debug", { action: "receiveError", id, reason: "proxy-NOTOK", error: result });
  202. response.throwThrottleError(result.result, THROTTLE);
  203. }
  204. }
  205. else {
  206. if (throttle) {
  207. this.emit("debug", { action: "receiveError", id, reason: "null result", error: result.result });
  208. response.throwThrottleError(result.result, THROTTLE);
  209. }
  210. }
  211. return response;
  212. };
  213. if (payload) {
  214. request.setHeader("content-type", "application/x-www-form-urlencoded; charset=UTF-8");
  215. request.body = Object.keys(payload).map((k) => `${k}=${payload[k]}`).join("&");
  216. }
  217. const response = await request.send();
  218. try {
  219. response.assertOk();
  220. }
  221. catch (error) {
  222. this.emit("debug", { action: "receiveError", id, error, reason: "assertOk" });
  223. (0, index_js_4.assert)(false, "response error", "SERVER_ERROR", { request, response });
  224. }
  225. if (!response.hasBody()) {
  226. this.emit("debug", { action: "receiveError", id, error: "missing body", reason: "null body" });
  227. (0, index_js_4.assert)(false, "missing response", "SERVER_ERROR", { request, response });
  228. }
  229. const result = JSON.parse((0, index_js_4.toUtf8String)(response.body));
  230. if (module === "proxy") {
  231. if (result.jsonrpc != "2.0") {
  232. this.emit("debug", { action: "receiveError", id, result, reason: "invalid JSON-RPC" });
  233. (0, index_js_4.assert)(false, "invalid JSON-RPC response (missing jsonrpc='2.0')", "SERVER_ERROR", { request, response, info: { result } });
  234. }
  235. if (result.error) {
  236. this.emit("debug", { action: "receiveError", id, result, reason: "JSON-RPC error" });
  237. (0, index_js_4.assert)(false, "error response", "SERVER_ERROR", { request, response, info: { result } });
  238. }
  239. this.emit("debug", { action: "receiveRequest", id, result });
  240. return result.result;
  241. }
  242. else {
  243. // getLogs, getHistory have weird success responses
  244. if (result.status == 0 && (result.message === "No records found" || result.message === "No transactions found")) {
  245. this.emit("debug", { action: "receiveRequest", id, result });
  246. return result.result;
  247. }
  248. if (result.status != 1 || (typeof (result.message) === "string" && !result.message.match(/^OK/))) {
  249. this.emit("debug", { action: "receiveError", id, result });
  250. (0, index_js_4.assert)(false, "error response", "SERVER_ERROR", { request, response, info: { result } });
  251. }
  252. this.emit("debug", { action: "receiveRequest", id, result });
  253. return result.result;
  254. }
  255. }
  256. /**
  257. * Returns %%transaction%% normalized for the Etherscan API.
  258. */
  259. _getTransactionPostData(transaction) {
  260. const result = {};
  261. for (let key in transaction) {
  262. if (skipKeys.indexOf(key) >= 0) {
  263. continue;
  264. }
  265. if (transaction[key] == null) {
  266. continue;
  267. }
  268. let value = transaction[key];
  269. if (key === "type" && value === 0) {
  270. continue;
  271. }
  272. if (key === "blockTag" && value === "latest") {
  273. continue;
  274. }
  275. // Quantity-types require no leading zero, unless 0
  276. if ({ type: true, gasLimit: true, gasPrice: true, maxFeePerGs: true, maxPriorityFeePerGas: true, nonce: true, value: true }[key]) {
  277. value = (0, index_js_4.toQuantity)(value);
  278. }
  279. else if (key === "accessList") {
  280. value = "[" + (0, index_js_3.accessListify)(value).map((set) => {
  281. return `{address:"${set.address}",storageKeys:["${set.storageKeys.join('","')}"]}`;
  282. }).join(",") + "]";
  283. }
  284. else if (key === "blobVersionedHashes") {
  285. if (value.length === 0) {
  286. continue;
  287. }
  288. // @TODO: update this once the API supports blobs
  289. (0, index_js_4.assert)(false, "Etherscan API does not support blobVersionedHashes", "UNSUPPORTED_OPERATION", {
  290. operation: "_getTransactionPostData",
  291. info: { transaction }
  292. });
  293. }
  294. else {
  295. value = (0, index_js_4.hexlify)(value);
  296. }
  297. result[key] = value;
  298. }
  299. return result;
  300. }
  301. /**
  302. * Throws the normalized Etherscan error.
  303. */
  304. _checkError(req, error, transaction) {
  305. // Pull any message out if, possible
  306. let message = "";
  307. if ((0, index_js_4.isError)(error, "SERVER_ERROR")) {
  308. // Check for an error emitted by a proxy call
  309. try {
  310. message = error.info.result.error.message;
  311. }
  312. catch (e) { }
  313. if (!message) {
  314. try {
  315. message = error.info.message;
  316. }
  317. catch (e) { }
  318. }
  319. }
  320. if (req.method === "estimateGas") {
  321. if (!message.match(/revert/i) && message.match(/insufficient funds/i)) {
  322. (0, index_js_4.assert)(false, "insufficient funds", "INSUFFICIENT_FUNDS", {
  323. transaction: req.transaction
  324. });
  325. }
  326. }
  327. if (req.method === "call" || req.method === "estimateGas") {
  328. if (message.match(/execution reverted/i)) {
  329. let data = "";
  330. try {
  331. data = error.info.result.error.data;
  332. }
  333. catch (error) { }
  334. const e = index_js_1.AbiCoder.getBuiltinCallException(req.method, req.transaction, data);
  335. e.info = { request: req, error };
  336. throw e;
  337. }
  338. }
  339. if (message) {
  340. if (req.method === "broadcastTransaction") {
  341. const transaction = index_js_3.Transaction.from(req.signedTransaction);
  342. if (message.match(/replacement/i) && message.match(/underpriced/i)) {
  343. (0, index_js_4.assert)(false, "replacement fee too low", "REPLACEMENT_UNDERPRICED", {
  344. transaction
  345. });
  346. }
  347. if (message.match(/insufficient funds/)) {
  348. (0, index_js_4.assert)(false, "insufficient funds for intrinsic transaction cost", "INSUFFICIENT_FUNDS", {
  349. transaction
  350. });
  351. }
  352. if (message.match(/same hash was already imported|transaction nonce is too low|nonce too low/)) {
  353. (0, index_js_4.assert)(false, "nonce has already been used", "NONCE_EXPIRED", {
  354. transaction
  355. });
  356. }
  357. }
  358. }
  359. // Something we could not process
  360. throw error;
  361. }
  362. async _detectNetwork() {
  363. return this.network;
  364. }
  365. async _perform(req) {
  366. switch (req.method) {
  367. case "chainId":
  368. return this.network.chainId;
  369. case "getBlockNumber":
  370. return this.fetch("proxy", { action: "eth_blockNumber" });
  371. case "getGasPrice":
  372. return this.fetch("proxy", { action: "eth_gasPrice" });
  373. case "getPriorityFee":
  374. // This is temporary until Etherscan completes support
  375. if (this.network.name === "mainnet") {
  376. return "1000000000";
  377. }
  378. else if (this.network.name === "optimism") {
  379. return "1000000";
  380. }
  381. else {
  382. throw new Error("fallback onto the AbstractProvider default");
  383. }
  384. /* Working with Etherscan to get this added:
  385. try {
  386. const test = await this.fetch("proxy", {
  387. action: "eth_maxPriorityFeePerGas"
  388. });
  389. console.log(test);
  390. return test;
  391. } catch (e) {
  392. console.log("DEBUG", e);
  393. throw e;
  394. }
  395. */
  396. /* This might be safe; but due to rounding neither myself
  397. or Etherscan are necessarily comfortable with this. :)
  398. try {
  399. const result = await this.fetch("gastracker", { action: "gasoracle" });
  400. console.log(result);
  401. const gasPrice = parseUnits(result.SafeGasPrice, "gwei");
  402. const baseFee = parseUnits(result.suggestBaseFee, "gwei");
  403. const priorityFee = gasPrice - baseFee;
  404. if (priorityFee < 0) { throw new Error("negative priority fee; defer to abstract provider default"); }
  405. return priorityFee;
  406. } catch (error) {
  407. console.log("DEBUG", error);
  408. throw error;
  409. }
  410. */
  411. case "getBalance":
  412. // Returns base-10 result
  413. return this.fetch("account", {
  414. action: "balance",
  415. address: req.address,
  416. tag: req.blockTag
  417. });
  418. case "getTransactionCount":
  419. return this.fetch("proxy", {
  420. action: "eth_getTransactionCount",
  421. address: req.address,
  422. tag: req.blockTag
  423. });
  424. case "getCode":
  425. return this.fetch("proxy", {
  426. action: "eth_getCode",
  427. address: req.address,
  428. tag: req.blockTag
  429. });
  430. case "getStorage":
  431. return this.fetch("proxy", {
  432. action: "eth_getStorageAt",
  433. address: req.address,
  434. position: req.position,
  435. tag: req.blockTag
  436. });
  437. case "broadcastTransaction":
  438. return this.fetch("proxy", {
  439. action: "eth_sendRawTransaction",
  440. hex: req.signedTransaction
  441. }, true).catch((error) => {
  442. return this._checkError(req, error, req.signedTransaction);
  443. });
  444. case "getBlock":
  445. if ("blockTag" in req) {
  446. return this.fetch("proxy", {
  447. action: "eth_getBlockByNumber",
  448. tag: req.blockTag,
  449. boolean: (req.includeTransactions ? "true" : "false")
  450. });
  451. }
  452. (0, index_js_4.assert)(false, "getBlock by blockHash not supported by Etherscan", "UNSUPPORTED_OPERATION", {
  453. operation: "getBlock(blockHash)"
  454. });
  455. case "getTransaction":
  456. return this.fetch("proxy", {
  457. action: "eth_getTransactionByHash",
  458. txhash: req.hash
  459. });
  460. case "getTransactionReceipt":
  461. return this.fetch("proxy", {
  462. action: "eth_getTransactionReceipt",
  463. txhash: req.hash
  464. });
  465. case "call": {
  466. if (req.blockTag !== "latest") {
  467. throw new Error("EtherscanProvider does not support blockTag for call");
  468. }
  469. const postData = this._getTransactionPostData(req.transaction);
  470. postData.module = "proxy";
  471. postData.action = "eth_call";
  472. try {
  473. return await this.fetch("proxy", postData, true);
  474. }
  475. catch (error) {
  476. return this._checkError(req, error, req.transaction);
  477. }
  478. }
  479. case "estimateGas": {
  480. const postData = this._getTransactionPostData(req.transaction);
  481. postData.module = "proxy";
  482. postData.action = "eth_estimateGas";
  483. try {
  484. return await this.fetch("proxy", postData, true);
  485. }
  486. catch (error) {
  487. return this._checkError(req, error, req.transaction);
  488. }
  489. }
  490. /*
  491. case "getLogs": {
  492. // Needs to complain if more than one address is passed in
  493. const args: Record<string, any> = { action: "getLogs" }
  494. if (params.filter.fromBlock) {
  495. args.fromBlock = checkLogTag(params.filter.fromBlock);
  496. }
  497. if (params.filter.toBlock) {
  498. args.toBlock = checkLogTag(params.filter.toBlock);
  499. }
  500. if (params.filter.address) {
  501. args.address = params.filter.address;
  502. }
  503. // @TODO: We can handle slightly more complicated logs using the logs API
  504. if (params.filter.topics && params.filter.topics.length > 0) {
  505. if (params.filter.topics.length > 1) {
  506. logger.throwError("unsupported topic count", Logger.Errors.UNSUPPORTED_OPERATION, { topics: params.filter.topics });
  507. }
  508. if (params.filter.topics.length === 1) {
  509. const topic0 = params.filter.topics[0];
  510. if (typeof(topic0) !== "string" || topic0.length !== 66) {
  511. logger.throwError("unsupported topic format", Logger.Errors.UNSUPPORTED_OPERATION, { topic0: topic0 });
  512. }
  513. args.topic0 = topic0;
  514. }
  515. }
  516. const logs: Array<any> = await this.fetch("logs", args);
  517. // Cache txHash => blockHash
  518. let blocks: { [tag: string]: string } = {};
  519. // Add any missing blockHash to the logs
  520. for (let i = 0; i < logs.length; i++) {
  521. const log = logs[i];
  522. if (log.blockHash != null) { continue; }
  523. if (blocks[log.blockNumber] == null) {
  524. const block = await this.getBlock(log.blockNumber);
  525. if (block) {
  526. blocks[log.blockNumber] = block.hash;
  527. }
  528. }
  529. log.blockHash = blocks[log.blockNumber];
  530. }
  531. return logs;
  532. }
  533. */
  534. default:
  535. break;
  536. }
  537. return super._perform(req);
  538. }
  539. async getNetwork() {
  540. return this.network;
  541. }
  542. /**
  543. * Resolves to the current price of ether.
  544. *
  545. * This returns ``0`` on any network other than ``mainnet``.
  546. */
  547. async getEtherPrice() {
  548. if (this.network.name !== "mainnet") {
  549. return 0.0;
  550. }
  551. return parseFloat((await this.fetch("stats", { action: "ethprice" })).ethusd);
  552. }
  553. /**
  554. * Resolves to a [Contract]] for %%address%%, using the
  555. * Etherscan API to retreive the Contract ABI.
  556. */
  557. async getContract(_address) {
  558. let address = this._getAddress(_address);
  559. if (isPromise(address)) {
  560. address = await address;
  561. }
  562. try {
  563. const resp = await this.fetch("contract", {
  564. action: "getabi", address
  565. });
  566. const abi = JSON.parse(resp);
  567. return new index_js_2.Contract(address, abi, this);
  568. }
  569. catch (error) {
  570. return null;
  571. }
  572. }
  573. isCommunityResource() {
  574. return (this.apiKey == null);
  575. }
  576. }
  577. exports.EtherscanProvider = EtherscanProvider;
  578. //# sourceMappingURL=provider-etherscan.js.map