1 | "use strict";
|
2 | var __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 | };
|
11 | import { hexlify, hexValue, isHexString } from "@ethersproject/bytes";
|
12 | import { deepCopy, defineReadOnly } from "@ethersproject/properties";
|
13 | import { accessListify } from "@ethersproject/transactions";
|
14 | import { fetchJson } from "@ethersproject/web";
|
15 | import { showThrottleMessage } from "./formatter";
|
16 | import { Logger } from "@ethersproject/logger";
|
17 | import { version } from "./_version";
|
18 | const logger = new Logger(version);
|
19 | import { BaseProvider } from "./base-provider";
|
20 |
|
21 | function 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 |
|
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 | }
|
47 | function getResult(result) {
|
48 |
|
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 | }
|
62 | function getJsonResult(result) {
|
63 |
|
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 |
|
72 | const error = new Error("invalid response");
|
73 | error.result = JSON.stringify(result);
|
74 | throw error;
|
75 | }
|
76 | if (result.error) {
|
77 |
|
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 |
|
90 | function 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 | }
|
99 | const defaultApiKey = "9D13ZE7XSBTJ94N9BNJ2MA33VMAY2YPIRB";
|
100 | function checkError(method, error, transaction) {
|
101 |
|
102 |
|
103 | if (method === "call" && error.code === Logger.errors.SERVER_ERROR) {
|
104 | const e = error.error;
|
105 |
|
106 | if (e && (e.message.match(/reverted/i) || e.message.match(/VM execution error/i))) {
|
107 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 | }
|
159 | export 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 |
|
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 |
|
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 |
|
357 | let blocks = {};
|
358 |
|
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 |
|
386 |
|
387 |
|
388 |
|
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 |
|
\ | No newline at end of file |