UNPKG

18.3 kBJavaScriptView Raw
1"use strict";
2var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3 function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4 return new (P || (P = Promise))(function (resolve, reject) {
5 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7 function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8 step((generator = generator.apply(thisArg, _arguments || [])).next());
9 });
10};
11import { hexlify, hexValue, isHexString } from "@ethersproject/bytes";
12import { deepCopy, defineReadOnly } from "@ethersproject/properties";
13import { accessListify } from "@ethersproject/transactions";
14import { fetchJson } from "@ethersproject/web";
15import { showThrottleMessage } from "./formatter";
16import { Logger } from "@ethersproject/logger";
17import { version } from "./_version";
18const logger = new Logger(version);
19import { BaseProvider } from "./base-provider";
20// The transaction has already been sanitized by the calls in Provider
21function getTransactionPostData(transaction) {
22 const result = {};
23 for (let key in transaction) {
24 if (transaction[key] == null) {
25 continue;
26 }
27 let value = transaction[key];
28 if (key === "type" && value === 0) {
29 continue;
30 }
31 // Quantity-types require no leading zero, unless 0
32 if ({ type: true, gasLimit: true, gasPrice: true, maxFeePerGs: true, maxPriorityFeePerGas: true, nonce: true, value: true }[key]) {
33 value = hexValue(hexlify(value));
34 }
35 else if (key === "accessList") {
36 value = "[" + accessListify(value).map((set) => {
37 return `{address:"${set.address}",storageKeys:["${set.storageKeys.join('","')}"]}`;
38 }).join(",") + "]";
39 }
40 else {
41 value = hexlify(value);
42 }
43 result[key] = value;
44 }
45 return result;
46}
47function getResult(result) {
48 // getLogs, getHistory have weird success responses
49 if (result.status == 0 && (result.message === "No records found" || result.message === "No transactions found")) {
50 return result.result;
51 }
52 if (result.status != 1 || result.message != "OK") {
53 const error = new Error("invalid response");
54 error.result = JSON.stringify(result);
55 if ((result.result || "").toLowerCase().indexOf("rate limit") >= 0) {
56 error.throttleRetry = true;
57 }
58 throw error;
59 }
60 return result.result;
61}
62function getJsonResult(result) {
63 // This response indicates we are being throttled
64 if (result && result.status == 0 && result.message == "NOTOK" && (result.result || "").toLowerCase().indexOf("rate limit") >= 0) {
65 const error = new Error("throttled response");
66 error.result = JSON.stringify(result);
67 error.throttleRetry = true;
68 throw error;
69 }
70 if (result.jsonrpc != "2.0") {
71 // @TODO: not any
72 const error = new Error("invalid response");
73 error.result = JSON.stringify(result);
74 throw error;
75 }
76 if (result.error) {
77 // @TODO: not any
78 const error = new Error(result.error.message || "unknown error");
79 if (result.error.code) {
80 error.code = result.error.code;
81 }
82 if (result.error.data) {
83 error.data = result.error.data;
84 }
85 throw error;
86 }
87 return result.result;
88}
89// The blockTag was normalized as a string by the Provider pre-perform operations
90function checkLogTag(blockTag) {
91 if (blockTag === "pending") {
92 throw new Error("pending not supported");
93 }
94 if (blockTag === "latest") {
95 return blockTag;
96 }
97 return parseInt(blockTag.substring(2), 16);
98}
99const defaultApiKey = "9D13ZE7XSBTJ94N9BNJ2MA33VMAY2YPIRB";
100function checkError(method, error, transaction) {
101 // Undo the "convenience" some nodes are attempting to prevent backwards
102 // incompatibility; maybe for v6 consider forwarding reverts as errors
103 if (method === "call" && error.code === Logger.errors.SERVER_ERROR) {
104 const e = error.error;
105 // Etherscan keeps changing their string
106 if (e && (e.message.match(/reverted/i) || e.message.match(/VM execution error/i))) {
107 // Etherscan prefixes the data like "Reverted 0x1234"
108 let data = e.data;
109 if (data) {
110 data = "0x" + data.replace(/^.*0x/i, "");
111 }
112 if (isHexString(data)) {
113 return data;
114 }
115 logger.throwError("missing revert data in call exception", Logger.errors.CALL_EXCEPTION, {
116 error, data: "0x"
117 });
118 }
119 }
120 // Get the message from any nested error structure
121 let message = error.message;
122 if (error.code === Logger.errors.SERVER_ERROR) {
123 if (error.error && typeof (error.error.message) === "string") {
124 message = error.error.message;
125 }
126 else if (typeof (error.body) === "string") {
127 message = error.body;
128 }
129 else if (typeof (error.responseText) === "string") {
130 message = error.responseText;
131 }
132 }
133 message = (message || "").toLowerCase();
134 // "Insufficient funds. The account you tried to send transaction from does not have enough funds. Required 21464000000000 and got: 0"
135 if (message.match(/insufficient funds/)) {
136 logger.throwError("insufficient funds for intrinsic transaction cost", Logger.errors.INSUFFICIENT_FUNDS, {
137 error, method, transaction
138 });
139 }
140 // "Transaction with the same hash was already imported."
141 if (message.match(/same hash was already imported|transaction nonce is too low|nonce too low/)) {
142 logger.throwError("nonce has already been used", Logger.errors.NONCE_EXPIRED, {
143 error, method, transaction
144 });
145 }
146 // "Transaction gas price is too low. There is another transaction with same nonce in the queue. Try increasing the gas price or incrementing the nonce."
147 if (message.match(/another transaction with same nonce/)) {
148 logger.throwError("replacement fee too low", Logger.errors.REPLACEMENT_UNDERPRICED, {
149 error, method, transaction
150 });
151 }
152 if (message.match(/execution failed due to an exception|execution reverted/)) {
153 logger.throwError("cannot estimate gas; transaction may fail or may require manual gas limit", Logger.errors.UNPREDICTABLE_GAS_LIMIT, {
154 error, method, transaction
155 });
156 }
157 throw error;
158}
159export class EtherscanProvider extends BaseProvider {
160 constructor(network, apiKey) {
161 super(network);
162 defineReadOnly(this, "baseUrl", this.getBaseUrl());
163 defineReadOnly(this, "apiKey", apiKey || defaultApiKey);
164 }
165 getBaseUrl() {
166 switch (this.network ? this.network.name : "invalid") {
167 case "homestead":
168 return "https:/\/api.etherscan.io";
169 case "ropsten":
170 return "https:/\/api-ropsten.etherscan.io";
171 case "rinkeby":
172 return "https:/\/api-rinkeby.etherscan.io";
173 case "kovan":
174 return "https:/\/api-kovan.etherscan.io";
175 case "goerli":
176 return "https:/\/api-goerli.etherscan.io";
177 case "optimism":
178 return "https:/\/api-optimistic.etherscan.io";
179 default:
180 }
181 return logger.throwArgumentError("unsupported network", "network", this.network.name);
182 }
183 getUrl(module, params) {
184 const query = Object.keys(params).reduce((accum, key) => {
185 const value = params[key];
186 if (value != null) {
187 accum += `&${key}=${value}`;
188 }
189 return accum;
190 }, "");
191 const apiKey = ((this.apiKey) ? `&apikey=${this.apiKey}` : "");
192 return `${this.baseUrl}/api?module=${module}${query}${apiKey}`;
193 }
194 getPostUrl() {
195 return `${this.baseUrl}/api`;
196 }
197 getPostData(module, params) {
198 params.module = module;
199 params.apikey = this.apiKey;
200 return params;
201 }
202 fetch(module, params, post) {
203 return __awaiter(this, void 0, void 0, function* () {
204 const url = (post ? this.getPostUrl() : this.getUrl(module, params));
205 const payload = (post ? this.getPostData(module, params) : null);
206 const procFunc = (module === "proxy") ? getJsonResult : getResult;
207 this.emit("debug", {
208 action: "request",
209 request: url,
210 provider: this
211 });
212 const connection = {
213 url: url,
214 throttleSlotInterval: 1000,
215 throttleCallback: (attempt, url) => {
216 if (this.isCommunityResource()) {
217 showThrottleMessage();
218 }
219 return Promise.resolve(true);
220 }
221 };
222 let payloadStr = null;
223 if (payload) {
224 connection.headers = { "content-type": "application/x-www-form-urlencoded; charset=UTF-8" };
225 payloadStr = Object.keys(payload).map((key) => {
226 return `${key}=${payload[key]}`;
227 }).join("&");
228 }
229 const result = yield fetchJson(connection, payloadStr, procFunc || getJsonResult);
230 this.emit("debug", {
231 action: "response",
232 request: url,
233 response: deepCopy(result),
234 provider: this
235 });
236 return result;
237 });
238 }
239 detectNetwork() {
240 return __awaiter(this, void 0, void 0, function* () {
241 return this.network;
242 });
243 }
244 perform(method, params) {
245 const _super = Object.create(null, {
246 perform: { get: () => super.perform }
247 });
248 return __awaiter(this, void 0, void 0, function* () {
249 switch (method) {
250 case "getBlockNumber":
251 return this.fetch("proxy", { action: "eth_blockNumber" });
252 case "getGasPrice":
253 return this.fetch("proxy", { action: "eth_gasPrice" });
254 case "getBalance":
255 // Returns base-10 result
256 return this.fetch("account", {
257 action: "balance",
258 address: params.address,
259 tag: params.blockTag
260 });
261 case "getTransactionCount":
262 return this.fetch("proxy", {
263 action: "eth_getTransactionCount",
264 address: params.address,
265 tag: params.blockTag
266 });
267 case "getCode":
268 return this.fetch("proxy", {
269 action: "eth_getCode",
270 address: params.address,
271 tag: params.blockTag
272 });
273 case "getStorageAt":
274 return this.fetch("proxy", {
275 action: "eth_getStorageAt",
276 address: params.address,
277 position: params.position,
278 tag: params.blockTag
279 });
280 case "sendTransaction":
281 return this.fetch("proxy", {
282 action: "eth_sendRawTransaction",
283 hex: params.signedTransaction
284 }, true).catch((error) => {
285 return checkError("sendTransaction", error, params.signedTransaction);
286 });
287 case "getBlock":
288 if (params.blockTag) {
289 return this.fetch("proxy", {
290 action: "eth_getBlockByNumber",
291 tag: params.blockTag,
292 boolean: (params.includeTransactions ? "true" : "false")
293 });
294 }
295 throw new Error("getBlock by blockHash not implemented");
296 case "getTransaction":
297 return this.fetch("proxy", {
298 action: "eth_getTransactionByHash",
299 txhash: params.transactionHash
300 });
301 case "getTransactionReceipt":
302 return this.fetch("proxy", {
303 action: "eth_getTransactionReceipt",
304 txhash: params.transactionHash
305 });
306 case "call": {
307 if (params.blockTag !== "latest") {
308 throw new Error("EtherscanProvider does not support blockTag for call");
309 }
310 const postData = getTransactionPostData(params.transaction);
311 postData.module = "proxy";
312 postData.action = "eth_call";
313 try {
314 return yield this.fetch("proxy", postData, true);
315 }
316 catch (error) {
317 return checkError("call", error, params.transaction);
318 }
319 }
320 case "estimateGas": {
321 const postData = getTransactionPostData(params.transaction);
322 postData.module = "proxy";
323 postData.action = "eth_estimateGas";
324 try {
325 return yield this.fetch("proxy", postData, true);
326 }
327 catch (error) {
328 return checkError("estimateGas", error, params.transaction);
329 }
330 }
331 case "getLogs": {
332 const args = { action: "getLogs" };
333 if (params.filter.fromBlock) {
334 args.fromBlock = checkLogTag(params.filter.fromBlock);
335 }
336 if (params.filter.toBlock) {
337 args.toBlock = checkLogTag(params.filter.toBlock);
338 }
339 if (params.filter.address) {
340 args.address = params.filter.address;
341 }
342 // @TODO: We can handle slightly more complicated logs using the logs API
343 if (params.filter.topics && params.filter.topics.length > 0) {
344 if (params.filter.topics.length > 1) {
345 logger.throwError("unsupported topic count", Logger.errors.UNSUPPORTED_OPERATION, { topics: params.filter.topics });
346 }
347 if (params.filter.topics.length === 1) {
348 const topic0 = params.filter.topics[0];
349 if (typeof (topic0) !== "string" || topic0.length !== 66) {
350 logger.throwError("unsupported topic format", Logger.errors.UNSUPPORTED_OPERATION, { topic0: topic0 });
351 }
352 args.topic0 = topic0;
353 }
354 }
355 const logs = yield this.fetch("logs", args);
356 // Cache txHash => blockHash
357 let blocks = {};
358 // Add any missing blockHash to the logs
359 for (let i = 0; i < logs.length; i++) {
360 const log = logs[i];
361 if (log.blockHash != null) {
362 continue;
363 }
364 if (blocks[log.blockNumber] == null) {
365 const block = yield this.getBlock(log.blockNumber);
366 if (block) {
367 blocks[log.blockNumber] = block.hash;
368 }
369 }
370 log.blockHash = blocks[log.blockNumber];
371 }
372 return logs;
373 }
374 case "getEtherPrice":
375 if (this.network.name !== "homestead") {
376 return 0.0;
377 }
378 return parseFloat((yield this.fetch("stats", { action: "ethprice" })).ethusd);
379 default:
380 break;
381 }
382 return _super.perform.call(this, method, params);
383 });
384 }
385 // Note: The `page` page parameter only allows pagination within the
386 // 10,000 window available without a page and offset parameter
387 // Error: Result window is too large, PageNo x Offset size must
388 // be less than or equal to 10000
389 getHistory(addressOrName, startBlock, endBlock) {
390 return __awaiter(this, void 0, void 0, function* () {
391 const params = {
392 action: "txlist",
393 address: (yield this.resolveName(addressOrName)),
394 startblock: ((startBlock == null) ? 0 : startBlock),
395 endblock: ((endBlock == null) ? 99999999 : endBlock),
396 sort: "asc"
397 };
398 const result = yield this.fetch("account", params);
399 return result.map((tx) => {
400 ["contractAddress", "to"].forEach(function (key) {
401 if (tx[key] == "") {
402 delete tx[key];
403 }
404 });
405 if (tx.creates == null && tx.contractAddress != null) {
406 tx.creates = tx.contractAddress;
407 }
408 const item = this.formatter.transactionResponse(tx);
409 if (tx.timeStamp) {
410 item.timestamp = parseInt(tx.timeStamp);
411 }
412 return item;
413 });
414 });
415 }
416 isCommunityResource() {
417 return (this.apiKey === defaultApiKey);
418 }
419}
420//# sourceMappingURL=etherscan-provider.js.map
\No newline at end of file