provider-etherscan.js 23 KB

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