ethers.js/packages/providers/lib.esm/etherscan-provider.js

364 lines
16 KiB
JavaScript
Raw Permalink Normal View History

2019-05-14 18:48:48 -04:00
"use strict";
2019-11-20 18:57:38 +09:00
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { hexlify, hexValue } from "@ethersproject/bytes";
import { deepCopy, defineReadOnly } from "@ethersproject/properties";
import { fetchJson } from "@ethersproject/web";
2020-07-14 02:33:30 -04:00
import { showThrottleMessage } from "./formatter";
import { Logger } from "@ethersproject/logger";
import { version } from "./_version";
const logger = new Logger(version);
import { BaseProvider } from "./base-provider";
2019-05-14 18:48:48 -04:00
// The transaction has already been sanitized by the calls in Provider
function getTransactionString(transaction) {
2019-11-20 18:57:38 +09:00
const result = [];
for (let key in transaction) {
2019-05-14 18:48:48 -04:00
if (transaction[key] == null) {
continue;
}
let value = hexlify(transaction[key]);
2019-05-14 18:48:48 -04:00
if ({ gasLimit: true, gasPrice: true, nonce: true, value: true }[key]) {
value = hexValue(value);
2019-05-14 18:48:48 -04:00
}
result.push(key + "=" + value);
}
return result.join("&");
}
function getResult(result) {
// getLogs, getHistory have weird success responses
if (result.status == 0 && (result.message === "No records found" || result.message === "No transactions found")) {
return result.result;
}
if (result.status != 1 || result.message != "OK") {
2019-11-20 18:57:38 +09:00
const error = new Error("invalid response");
2019-05-14 18:48:48 -04:00
error.result = JSON.stringify(result);
2020-07-14 02:33:30 -04:00
if ((result.result || "").toLowerCase().indexOf("rate limit") >= 0) {
error.throttleRetry = true;
}
2019-05-14 18:48:48 -04:00
throw error;
}
return result.result;
}
function getJsonResult(result) {
2020-07-14 02:33:30 -04:00
// This response indicates we are being throttled
if (result && result.status == 0 && result.message == "NOTOK" && (result.result || "").toLowerCase().indexOf("rate limit") >= 0) {
const error = new Error("throttled response");
error.result = JSON.stringify(result);
error.throttleRetry = true;
throw error;
}
2019-05-14 18:48:48 -04:00
if (result.jsonrpc != "2.0") {
// @TODO: not any
2019-11-20 18:57:38 +09:00
const error = new Error("invalid response");
2019-05-14 18:48:48 -04:00
error.result = JSON.stringify(result);
throw error;
}
if (result.error) {
// @TODO: not any
2019-11-20 18:57:38 +09:00
const error = new Error(result.error.message || "unknown error");
2019-05-14 18:48:48 -04:00
if (result.error.code) {
error.code = result.error.code;
}
if (result.error.data) {
error.data = result.error.data;
}
throw error;
}
return result.result;
}
// The blockTag was normalized as a string by the Provider pre-perform operations
function checkLogTag(blockTag) {
if (blockTag === "pending") {
throw new Error("pending not supported");
}
if (blockTag === "latest") {
return blockTag;
}
return parseInt(blockTag.substring(2), 16);
}
2020-02-17 20:22:33 -05:00
const defaultApiKey = "9D13ZE7XSBTJ94N9BNJ2MA33VMAY2YPIRB";
export class EtherscanProvider extends BaseProvider {
constructor(network, apiKey) {
logger.checkNew(new.target, EtherscanProvider);
super(network);
let name = "invalid";
if (this.network) {
name = this.network.name;
2019-05-14 18:48:48 -04:00
}
let baseUrl = null;
2019-05-14 18:48:48 -04:00
switch (name) {
case "homestead":
baseUrl = "https://api.etherscan.io";
break;
case "ropsten":
baseUrl = "https://api-ropsten.etherscan.io";
break;
case "rinkeby":
baseUrl = "https://api-rinkeby.etherscan.io";
break;
case "kovan":
baseUrl = "https://api-kovan.etherscan.io";
break;
case "goerli":
baseUrl = "https://api-goerli.etherscan.io";
break;
default:
throw new Error("unsupported network");
}
defineReadOnly(this, "baseUrl", baseUrl);
2020-02-17 20:22:33 -05:00
defineReadOnly(this, "apiKey", apiKey || defaultApiKey);
2019-05-14 18:48:48 -04:00
}
2020-05-03 17:53:58 -04:00
detectNetwork() {
return __awaiter(this, void 0, void 0, function* () {
return this.network;
});
}
perform(method, params) {
2019-11-20 18:57:38 +09:00
const _super = Object.create(null, {
perform: { get: () => super.perform }
});
return __awaiter(this, void 0, void 0, function* () {
let url = this.baseUrl;
let apiKey = "";
if (this.apiKey) {
apiKey += "&apikey=" + this.apiKey;
}
const get = (url, procFunc) => __awaiter(this, void 0, void 0, function* () {
this.emit("debug", {
action: "request",
request: url,
provider: this
});
2020-07-14 02:33:30 -04:00
const connection = {
url: url,
2020-07-16 05:35:32 -04:00
throttleSlotInterval: 1000,
2020-07-14 02:33:30 -04:00
throttleCallback: (attempt, url) => {
if (this.apiKey === defaultApiKey) {
showThrottleMessage();
}
return Promise.resolve(true);
}
};
const result = yield fetchJson(connection, null, procFunc || getJsonResult);
this.emit("debug", {
2019-08-02 02:10:58 -04:00
action: "response",
2019-05-14 18:48:48 -04:00
request: url,
response: deepCopy(result),
provider: this
2019-05-14 18:48:48 -04:00
});
return result;
});
2019-11-20 18:57:38 +09:00
switch (method) {
case "getBlockNumber":
url += "/api?module=proxy&action=eth_blockNumber" + apiKey;
return get(url);
case "getGasPrice":
url += "/api?module=proxy&action=eth_gasPrice" + apiKey;
return get(url);
case "getBalance":
// Returns base-10 result
url += "/api?module=account&action=balance&address=" + params.address;
url += "&tag=" + params.blockTag + apiKey;
return get(url, getResult);
case "getTransactionCount":
url += "/api?module=proxy&action=eth_getTransactionCount&address=" + params.address;
url += "&tag=" + params.blockTag + apiKey;
return get(url);
case "getCode":
url += "/api?module=proxy&action=eth_getCode&address=" + params.address;
url += "&tag=" + params.blockTag + apiKey;
2020-07-14 02:33:30 -04:00
return get(url);
2019-11-20 18:57:38 +09:00
case "getStorageAt":
url += "/api?module=proxy&action=eth_getStorageAt&address=" + params.address;
url += "&position=" + params.position;
url += "&tag=" + params.blockTag + apiKey;
2020-07-14 02:33:30 -04:00
return get(url);
2019-11-20 18:57:38 +09:00
case "sendTransaction":
url += "/api?module=proxy&action=eth_sendRawTransaction&hex=" + params.signedTransaction;
url += apiKey;
return get(url).catch((error) => {
if (error.responseText) {
// "Insufficient funds. The account you tried to send transaction from does not have enough funds. Required 21464000000000 and got: 0"
if (error.responseText.toLowerCase().indexOf("insufficient funds") >= 0) {
logger.throwError("insufficient funds", Logger.errors.INSUFFICIENT_FUNDS, {});
}
// "Transaction with the same hash was already imported."
if (error.responseText.indexOf("same hash was already imported") >= 0) {
logger.throwError("nonce has already been used", Logger.errors.NONCE_EXPIRED, {});
}
// "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."
if (error.responseText.indexOf("another transaction with same nonce") >= 0) {
logger.throwError("replacement fee too low", Logger.errors.REPLACEMENT_UNDERPRICED, {});
}
2019-05-14 18:48:48 -04:00
}
2019-11-20 18:57:38 +09:00
throw error;
});
case "getBlock":
if (params.blockTag) {
url += "/api?module=proxy&action=eth_getBlockByNumber&tag=" + params.blockTag;
if (params.includeTransactions) {
url += "&boolean=true";
2019-05-14 18:48:48 -04:00
}
2019-11-20 18:57:38 +09:00
else {
url += "&boolean=false";
2019-05-14 18:48:48 -04:00
}
2019-11-20 18:57:38 +09:00
url += apiKey;
return get(url);
2019-05-14 18:48:48 -04:00
}
2020-04-23 23:35:39 -04:00
throw new Error("getBlock by blockHash not implemented");
2019-11-20 18:57:38 +09:00
case "getTransaction":
url += "/api?module=proxy&action=eth_getTransactionByHash&txhash=" + params.transactionHash;
url += apiKey;
return get(url);
case "getTransactionReceipt":
url += "/api?module=proxy&action=eth_getTransactionReceipt&txhash=" + params.transactionHash;
url += apiKey;
return get(url);
case "call": {
let transaction = getTransactionString(params.transaction);
if (transaction) {
transaction = "&" + transaction;
2019-05-14 18:48:48 -04:00
}
2019-11-20 18:57:38 +09:00
url += "/api?module=proxy&action=eth_call" + transaction;
//url += "&tag=" + params.blockTag + apiKey;
if (params.blockTag !== "latest") {
throw new Error("EtherscanProvider does not support blockTag for call");
2019-05-14 18:48:48 -04:00
}
url += apiKey;
return get(url);
}
2019-11-20 18:57:38 +09:00
case "estimateGas": {
let transaction = getTransactionString(params.transaction);
if (transaction) {
transaction = "&" + transaction;
}
url += "/api?module=proxy&action=eth_estimateGas&" + transaction;
url += apiKey;
return get(url);
2019-05-14 18:48:48 -04:00
}
2019-11-20 18:57:38 +09:00
case "getLogs": {
url += "/api?module=logs&action=getLogs";
2019-05-14 18:48:48 -04:00
if (params.filter.fromBlock) {
url += "&fromBlock=" + checkLogTag(params.filter.fromBlock);
}
if (params.filter.toBlock) {
url += "&toBlock=" + checkLogTag(params.filter.toBlock);
}
if (params.filter.address) {
url += "&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) {
2019-11-20 18:57:38 +09:00
logger.throwError("unsupported topic count", Logger.errors.UNSUPPORTED_OPERATION, { topics: params.filter.topics });
2019-05-14 18:48:48 -04:00
}
2019-11-20 18:57:38 +09:00
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 });
}
url += "&topic0=" + topic0;
2019-05-14 18:48:48 -04:00
}
}
2019-11-20 18:57:38 +09:00
url += apiKey;
const logs = yield get(url, getResult);
// Cache txHash => blockHash
let txs = {};
2019-11-20 18:57:38 +09:00
// 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 (txs[log.transactionHash] == null) {
const tx = yield this.getTransaction(log.transactionHash);
if (tx) {
txs[log.transactionHash] = tx.blockHash;
2019-05-14 18:48:48 -04:00
}
2019-11-20 18:57:38 +09:00
}
log.blockHash = txs[log.transactionHash];
}
return logs;
2019-05-14 18:48:48 -04:00
}
2019-11-20 18:57:38 +09:00
case "getEtherPrice":
if (this.network.name !== "homestead") {
return 0.0;
}
url += "/api?module=stats&action=ethprice";
url += apiKey;
2020-03-31 23:40:54 -04:00
return parseFloat((yield get(url, getResult)).ethusd);
2019-11-20 18:57:38 +09:00
default:
break;
}
return _super.perform.call(this, method, params);
});
}
2019-05-14 18:48:48 -04:00
// @TODO: Allow startBlock and endBlock to be Promises
getHistory(addressOrName, startBlock, endBlock) {
let url = this.baseUrl;
let apiKey = "";
2019-05-14 18:48:48 -04:00
if (this.apiKey) {
apiKey += "&apikey=" + this.apiKey;
}
if (startBlock == null) {
startBlock = 0;
}
if (endBlock == null) {
endBlock = 99999999;
}
return this.resolveName(addressOrName).then((address) => {
2019-05-14 18:48:48 -04:00
url += "/api?module=account&action=txlist&address=" + address;
url += "&startblock=" + startBlock;
url += "&endblock=" + endBlock;
url += "&sort=asc" + apiKey;
this.emit("debug", {
2019-08-02 02:10:58 -04:00
action: "request",
request: url,
provider: this
2019-08-02 02:10:58 -04:00
});
2020-07-14 02:33:30 -04:00
const connection = {
url: url,
2020-07-16 05:35:32 -04:00
throttleSlotInterval: 1000,
2020-07-14 02:33:30 -04:00
throttleCallback: (attempt, url) => {
if (this.apiKey === defaultApiKey) {
showThrottleMessage();
}
return Promise.resolve(true);
}
};
return fetchJson(connection, null, getResult).then((result) => {
this.emit("debug", {
2019-08-02 02:10:58 -04:00
action: "response",
2019-05-14 18:48:48 -04:00
request: url,
response: deepCopy(result),
provider: this
2019-05-14 18:48:48 -04:00
});
let output = [];
result.forEach((tx) => {
2019-05-14 18:48:48 -04:00
["contractAddress", "to"].forEach(function (key) {
if (tx[key] == "") {
delete tx[key];
}
});
if (tx.creates == null && tx.contractAddress != null) {
tx.creates = tx.contractAddress;
}
let item = this.formatter.transactionResponse(tx);
2019-05-14 18:48:48 -04:00
if (tx.timeStamp) {
item.timestamp = parseInt(tx.timeStamp);
}
output.push(item);
});
return output;
});
});
}
}
2020-07-13 08:03:56 -04:00
//# sourceMappingURL=etherscan-provider.js.map