UNPKG

@thorwallet/xchain-ethereum

Version:
636 lines 28.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Client = void 0; const tslib_1 = require("tslib"); const ethers_1 = require("ethers"); const providers_1 = require("@ethersproject/providers"); const erc20_json_1 = tslib_1.__importDefault(require("./data/erc20.json")); const utils_1 = require("ethers/lib/utils"); const types_1 = require("./types"); const xchain_util_1 = require("@thorwallet/xchain-util"); const Crypto = tslib_1.__importStar(require("@thorwallet/xchain-crypto")); const ethplorerAPI = tslib_1.__importStar(require("./ethplorer-api")); const etherscanAPI = tslib_1.__importStar(require("./etherscan-api")); const utils_2 = require("./utils"); const hdnode_1 = require("./hdnode/hdnode"); const wallet_1 = require("./wallet/wallet"); const get_address_1 = require("./get-address"); /** * Custom Ethereum client */ class Client { /** * Constructor * @param {EthereumClientParams} params */ constructor({ network = 'testnet', ethplorerUrl = 'https://api.ethplorer.io', ethplorerApiKey = 'freekey', explorerUrl, rootDerivationPaths = { mainnet: `m/44'/60'/0'/0/`, testnet: `m/44'/60'/0'/0/`, // this is INCORRECT but makes the unit tests pass }, etherscanApiKey, infuraCreds, }) { this.providers = new Map(); /** * Purge client. * * @returns {void} */ this.purgeClient = () => tslib_1.__awaiter(this, void 0, void 0, function* () { this.hdNode = yield hdnode_1.HDNode.fromMnemonic(''); }); /** * Set/Update the explorer url. * * @param {string} url The explorer url. * @returns {void} */ this.setExplorerURL = (url) => { this.explorerUrl = url; }; /** * Get the current network. * * @returns {Network} The current network. (`mainnet` or `testnet`) */ this.getNetwork = () => { return utils_2.ethNetworkToXchains(this.network); }; /** * Get etherjs wallet interface. * * @returns {Wallet} The current etherjs wallet interface. * * @throws {"Phrase must be provided"} * Thrown if phrase has not been set before. A phrase is needed to create a wallet and to derive an address from it. */ this.getWallet = (index = 0) => tslib_1.__awaiter(this, void 0, void 0, function* () { const newHdNode = yield this.hdNode.derivePath(this.getFullDerivationPath(index)); return new wallet_1.Wallet(newHdNode).connect(this.getProvider()); }); this.setupProviders = () => { if (this.infuraCreds) { // Infura provider takes either a string of project id // or an object of id and secret const testnetProvider = this.infuraCreds.projectSecret ? new ethers_1.ethers.providers.InfuraProvider(types_1.Network.TEST, this.infuraCreds) : new ethers_1.ethers.providers.InfuraProvider(types_1.Network.TEST, this.infuraCreds.projectId); const mainnetProvider = this.infuraCreds.projectSecret ? new ethers_1.ethers.providers.InfuraProvider(types_1.Network.MAIN, this.infuraCreds) : new ethers_1.ethers.providers.InfuraProvider(types_1.Network.MAIN, this.infuraCreds.projectId); this.providers.set('testnet', testnetProvider); this.providers.set('mainnet', mainnetProvider); } else { this.providers.set('testnet', providers_1.getDefaultProvider(types_1.Network.TEST)); this.providers.set('mainnet', providers_1.getDefaultProvider(types_1.Network.MAIN)); } }; /** * Get etherjs Provider interface. * * @returns {Provider} The current etherjs Provider interface. */ this.getProvider = () => { const net = utils_2.ethNetworkToXchains(this.network); return this.providers.get(net) || providers_1.getDefaultProvider(net); }; /** * Get etherjs EtherscanProvider interface. * * @returns {EtherscanProvider} The current etherjs EtherscanProvider interface. */ this.getEtherscanProvider = () => { return new providers_1.EtherscanProvider(this.network, this.etherscanApiKey); }; /** * Get the explorer url. * * @returns {string} The explorer url for ethereum based on the current network. */ this.getExplorerUrl = () => { return this.getExplorerUrlByNetwork(this.getNetwork()); }; /** * Get the explorer url. * * @returns {ExplorerUrl} The explorer url (both mainnet and testnet) for ethereum. */ this.getDefaultExplorerURL = () => { return { testnet: 'https://ropsten.etherscan.io', mainnet: 'https://etherscan.io', }; }; /** * Get the explorer url. * * @param {Network} network * @returns {string} The explorer url for ethereum based on the network. */ this.getExplorerUrlByNetwork = (network) => { return this.explorerUrl[network]; }; /** * Get the explorer url for the given address. * * @param {Address} address * @returns {string} The explorer url for the given address. */ this.getExplorerAddressUrl = (address) => { return `${this.getExplorerUrl()}/address/${address}`; }; /** * Get the explorer url for the given transaction id. * * @param {string} txID * @returns {string} The explorer url for the given transaction id. */ this.getExplorerTxUrl = (txID) => { return `${this.getExplorerUrl()}/tx/${txID}`; }; /** * Set/update the current network. * * @param {Network} network `mainnet` or `testnet`. * @returns {void} * * @throws {"Network must be provided"} * Thrown if network has not been set before. */ this.setNetwork = (network) => { if (!network) { throw new Error('Network must be provided'); } else { this.network = utils_2.xchainNetworkToEths(network); } }; /** * Set/update a new phrase (Eg. If user wants to change wallet) * * @param {string} phrase A new phrase. * @returns {Address} The address from the given phrase * * @throws {"Invalid phrase"} * Thrown if the given phase is invalid. */ this.setPhrase = (phrase, walletIndex = 0) => tslib_1.__awaiter(this, void 0, void 0, function* () { if (!Crypto.validatePhrase(phrase)) { throw new Error('Invalid phrase'); } this.phrase = phrase; this.hdNode = yield hdnode_1.HDNode.fromMnemonic(phrase); return get_address_1.getAddress({ network: this.getNetwork(), phrase, index: walletIndex }); }); /** * Validate the given address. * * @param {Address} address * @returns {boolean} `true` or `false` */ this.validateAddress = (address) => { return utils_2.validateAddress(address); }; /** * Get transaction history of a given address with pagination options. * By default it will return the transaction history of the current wallet. * * @param {TxHistoryParams} params The options to get transaction history. (optional) * @returns {TxsPage} The transaction history. */ this.getTransactions = (params) => tslib_1.__awaiter(this, void 0, void 0, function* () { try { const assetAddress = params === null || params === void 0 ? void 0 : params.asset; const maxCount = 10000; let transations; const etherscan = this.getEtherscanProvider(); if (assetAddress) { transations = yield etherscanAPI.getTokenTransactionHistory({ baseUrl: etherscan.baseUrl, address: params === null || params === void 0 ? void 0 : params.address, assetAddress, page: 0, offset: maxCount, apiKey: etherscan.apiKey, }); } else { transations = yield etherscanAPI.getETHTransactionHistory({ baseUrl: etherscan.baseUrl, address: params === null || params === void 0 ? void 0 : params.address, page: 0, offset: maxCount, apiKey: etherscan.apiKey, }); } return { total: transations.length, txs: transations, }; } catch (error) { return Promise.reject(error); } }); /** * Get the transaction details of a given transaction id. * * @param {string} txId The transaction id. * @param {string} assetAddress The asset address. (optional) * @returns {Tx} The transaction details of the given transaction id. * * @throws {"Need to provide valid txId"} * Thrown if the given txId is invalid. */ this.getTransactionData = (txId, assetAddress) => tslib_1.__awaiter(this, void 0, void 0, function* () { var _a, _b; try { if (this.getNetwork() === 'mainnet') { // use ethplorerAPI for mainnet - ignore assetAddress const txInfo = yield ethplorerAPI.getTxInfo(this.ethplorerUrl, txId, this.ethplorerApiKey); if (txInfo.operations && txInfo.operations.length > 0) { const tx = utils_2.getTxFromEthplorerTokenOperation(txInfo.operations[0]); if (!tx) { throw new Error('Could not parse transaction data'); } return tx; } else { return utils_2.getTxFromEthplorerEthTransaction(txInfo); } } else { let tx; const etherscan = this.getEtherscanProvider(); const txInfo = yield etherscan.getTransaction(txId); if (txInfo) { if (assetAddress) { tx = (_a = (yield etherscanAPI.getTokenTransactionHistory({ baseUrl: etherscan.baseUrl, assetAddress, startblock: txInfo.blockNumber, endblock: txInfo.blockNumber, apiKey: etherscan.apiKey, })).filter((info) => info.hash === txId)[0]) !== null && _a !== void 0 ? _a : null; } else { tx = (_b = (yield etherscanAPI.getETHTransactionHistory({ baseUrl: etherscan.baseUrl, startblock: txInfo.blockNumber, endblock: txInfo.blockNumber, apiKey: etherscan.apiKey, address: txInfo.from, })).filter((info) => info.hash === txId)[0]) !== null && _b !== void 0 ? _b : null; } } if (!tx) { throw new Error('Could not get transaction history'); } return tx; } } catch (error) { return Promise.reject(error); } }); /** * Call a contract function. * @template T The result interface. * @param {Address} address The contract address. * @param {ContractInterface} abi The contract ABI json. * @param {string} func The function to be called. * @param {Array<any>} params The parameters of the function. * @returns {T} The result of the contract function call. * * @throws {"address must be provided"} * Thrown if the given contract address is empty. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any this.call = (walletIndex = 0, contractAddress, abi, func, params) => tslib_1.__awaiter(this, void 0, void 0, function* () { if (!contractAddress) { return Promise.reject(new Error('contractAddress must be provided')); } const contract = new ethers_1.ethers.Contract(contractAddress, abi, this.getProvider()).connect(yield this.getWallet(walletIndex)); return contract[func](...params); }); /** * Call a contract function. * @param {Address} address The contract address. * @param {ContractInterface} abi The contract ABI json. * @param {string} func The function to be called. * @param {Array<any>} params The parameters of the function. * @returns {BigNumber} The result of the contract function call. * * @throws {"address must be provided"} * Thrown if the given contract address is empty. */ this.estimateCall = (contractAddress, abi, func, // eslint-disable-next-line @typescript-eslint/no-explicit-any params) => tslib_1.__awaiter(this, void 0, void 0, function* () { if (!contractAddress) { return Promise.reject(new Error('contractAddress must be provided')); } const contract = new ethers_1.ethers.Contract(contractAddress, abi, this.getProvider()).connect(yield this.getWallet(0)); return contract.estimateGas[func](...params); }); /** * Check allowance. * * @param {Address} spender The spender address. * @param {Address} sender The sender address. * @param {BaseAmount} amount The amount of token. * @returns {boolean} `true` or `false`. */ this.isApproved = (spender, sender, amount) => tslib_1.__awaiter(this, void 0, void 0, function* () { try { const txAmount = ethers_1.BigNumber.from(amount.amount().toFixed()); const allowance = yield this.call(0, sender, erc20_json_1.default, 'allowance', [spender, spender]); return txAmount.lte(allowance); } catch (error) { return Promise.reject(error); } }); /** * Check allowance. * * @param {number} walletIndex which wallet to use to make the call * @param {Address} spender The spender index. * @param {Address} sender The sender address. * @param {feeOptionKey} FeeOptionKey Fee option (optional) * @param {BaseAmount} amount The amount of token. By default, it will be unlimited token allowance. (optional) * @returns {TransactionResponse} The transaction result. */ this.approve = ({ walletIndex = 0, spender, sender, feeOptionKey, amount, }) => tslib_1.__awaiter(this, void 0, void 0, function* () { const gasPrice = feeOptionKey && ethers_1.BigNumber.from((yield this.estimateGasPrices() .then((prices) => prices[feeOptionKey]) .catch(() => utils_2.getDefaultGasPrices()[feeOptionKey])) .amount() .toFixed()); const gasLimit = yield this.estimateApprove({ spender, sender, amount }).catch(() => undefined); try { const txAmount = amount ? ethers_1.BigNumber.from(amount.amount().toFixed()) : utils_2.MAX_APPROVAL; const txResult = yield this.call(walletIndex, sender, erc20_json_1.default, 'approve', [ spender, txAmount, { from: yield get_address_1.getAddress({ index: 0, network: this.getNetwork(), phrase: this.phrase, }), gasPrice, gasLimit, }, ]); return txResult; } catch (error) { return Promise.reject(error); } }); /** * Estimate gas limit of approve. * * @param {Address} spender The spender address. * @param {Address} sender The sender address. * @param {BaseAmount} amount The amount of token. By default, it will be unlimited token allowance. (optional) * @returns {BigNumber} The estimated gas limit. */ this.estimateApprove = ({ spender, sender, amount, }) => tslib_1.__awaiter(this, void 0, void 0, function* () { try { const txAmount = amount ? ethers_1.BigNumber.from(amount.amount().toFixed()) : utils_2.MAX_APPROVAL; const gasLimit = yield this.estimateCall(sender, erc20_json_1.default, 'approve', [ spender, txAmount, { from: yield get_address_1.getAddress({ index: 0, network: this.getNetwork(), phrase: this.phrase, }), }, ]); return gasLimit; } catch (error) { return Promise.reject(error); } }); /** * Transfer ETH. * * @param {TxParams} params The transfer options. * @param {feeOptionKey} FeeOptionKey Fee option (optional) * @param {gasPrice} BaseAmount Gas price (optional) * @param {gasLimit} BigNumber Gas limit (optional) * * A given `feeOptionKey` wins over `gasPrice` and `gasLimit` * * @returns {TxHash} The transaction hash. * * @throws {"Invalid asset address"} * Thrown if the given asset is invalid. */ this.transfer = ({ walletIndex = 0, asset, memo, amount, recipient, feeOptionKey, gasPrice, gasLimit, }) => tslib_1.__awaiter(this, void 0, void 0, function* () { try { const txAmount = ethers_1.BigNumber.from(amount.amount().toFixed()); let assetAddress; if (asset && xchain_util_1.assetToString(asset) !== xchain_util_1.assetToString(xchain_util_1.AssetETH)) { assetAddress = utils_2.getTokenAddress(asset); } const isETHAddress = assetAddress === utils_2.ETHAddress; // feeOptionKey const defaultGasLimit = isETHAddress ? utils_2.SIMPLE_GAS_COST : utils_2.BASE_TOKEN_GAS_COST; let overrides = { gasLimit: gasLimit || defaultGasLimit, gasPrice: gasPrice && ethers_1.BigNumber.from(gasPrice.amount().toFixed()), }; // override `overrides` if `feeOptionKey` is provided if (feeOptionKey) { const gasPrice = yield this.estimateGasPrices() .then((prices) => prices[feeOptionKey]) .catch(() => utils_2.getDefaultGasPrices()[feeOptionKey]); const gasLimit = yield this.estimateGasLimit({ asset, recipient, amount, memo }).catch(() => defaultGasLimit); overrides = { gasLimit, gasPrice: ethers_1.BigNumber.from(gasPrice.amount().toFixed()), }; } let txResult; if (assetAddress && !isETHAddress) { // Transfer ERC20 txResult = yield this.call(walletIndex, assetAddress, erc20_json_1.default, 'transfer', [ recipient, txAmount, Object.assign({}, overrides), ]); } else { // Transfer ETH const transactionRequest = Object.assign({ to: recipient, value: txAmount }, Object.assign(Object.assign({}, overrides), { data: memo ? utils_1.toUtf8Bytes(memo) : undefined })); txResult = yield (yield this.getWallet()).sendTransaction(transactionRequest); } return txResult.hash; } catch (error) { return Promise.reject(error); } }); /** * Estimate gas price. * @see https://etherscan.io/apis#gastracker * * @returns {GasPrices} The gas prices (average, fast, fastest) in `Wei` (`BaseAmount`) * * @throws {"Failed to estimate gas price"} Thrown if failed to estimate gas price. */ this.estimateGasPrices = () => tslib_1.__awaiter(this, void 0, void 0, function* () { var _c; try { const etherscan = this.getEtherscanProvider(); const response = yield etherscanAPI.getGasOracle(etherscan.baseUrl, etherscan.apiKey); // Convert result of gas prices: `Gwei` -> `Wei` const averageWei = utils_1.parseUnits(response.SafeGasPrice, 'gwei'); const fastWei = utils_1.parseUnits(response.ProposeGasPrice, 'gwei'); const fastestWei = utils_1.parseUnits(response.FastGasPrice, 'gwei'); return { average: xchain_util_1.baseAmount(averageWei.toString(), utils_2.ETH_DECIMAL), fast: xchain_util_1.baseAmount(fastWei.toString(), utils_2.ETH_DECIMAL), fastest: xchain_util_1.baseAmount(fastestWei.toString(), utils_2.ETH_DECIMAL), }; } catch (error) { return Promise.reject(new Error(`Failed to estimate gas price: ${(_c = error.msg) !== null && _c !== void 0 ? _c : error.toString()}`)); } }); /** * Estimate gas. * * @param {FeesParams} params The transaction options. * @returns {BaseAmount} The estimated gas fee. * * @throws {"Failed to estimate gas limit"} Thrown if failed to estimate gas limit. */ this.estimateGasLimit = ({ asset, recipient, amount, memo, from, }) => tslib_1.__awaiter(this, void 0, void 0, function* () { var _d; try { const txAmount = ethers_1.BigNumber.from(amount.amount().toFixed()); let assetAddress; if (asset && xchain_util_1.assetToString(asset) !== xchain_util_1.assetToString(xchain_util_1.AssetETH)) { assetAddress = utils_2.getTokenAddress(asset); } let estimate; if (assetAddress && assetAddress !== utils_2.ETHAddress) { // ERC20 gas estimate const contract = new ethers_1.ethers.Contract(assetAddress, erc20_json_1.default, this.getProvider()); estimate = yield contract.estimateGas.transfer(recipient, txAmount, { from: from || (yield get_address_1.getAddress({ index: 0, network: this.getNetwork(), phrase: this.phrase, })), }); } else { // ETH gas estimate const transactionRequest = { from: from || (yield get_address_1.getAddress({ index: 0, network: this.getNetwork(), phrase: this.phrase, })), to: recipient, value: txAmount, data: memo ? utils_1.toUtf8Bytes(memo) : undefined, }; estimate = yield this.getProvider().estimateGas(transactionRequest); } return estimate; } catch (error) { return Promise.reject(new Error(`Failed to estimate gas limit: ${(_d = error.msg) !== null && _d !== void 0 ? _d : error.toString()}`)); } }); /** * Estimate gas prices/limits (average, fast fastest). * * @param {FeesParams} params * @returns {FeesWithGasPricesAndLimits} The estimated gas prices/limits. * * @throws {"Failed to estimate fees, gas price, gas limit"} Thrown if failed to estimate fees, gas price, gas limit. */ this.estimateFeesWithGasPricesAndLimits = (params) => tslib_1.__awaiter(this, void 0, void 0, function* () { var _e; try { // gas prices const gasPrices = yield this.estimateGasPrices(); const { fast: fastGP, fastest: fastestGP, average: averageGP } = gasPrices; // gas limits const gasLimit = yield this.estimateGasLimit({ asset: params.asset, amount: params.amount, recipient: params.recipient, memo: params.memo, from: params.from, }); return { gasPrices, fees: { type: 'byte', average: utils_2.getFee({ gasPrice: averageGP, gasLimit }), fast: utils_2.getFee({ gasPrice: fastGP, gasLimit }), fastest: utils_2.getFee({ gasPrice: fastestGP, gasLimit }), }, gasLimit, }; } catch (error) { return Promise.reject(new Error(`Failed to estimate fees, gas price, gas limit: ${(_e = error.msg) !== null && _e !== void 0 ? _e : error.toString()}`)); } }); /** * Get fees. * * @param {FeesParams} params * @returns {Fees} The average/fast/fastest fees. * * @throws {"Failed to get fees"} Thrown if failed to get fees. */ this.getFees = (params) => tslib_1.__awaiter(this, void 0, void 0, function* () { var _f; if (!params) return Promise.reject('Params need to be passed'); try { const { fees } = yield this.estimateFeesWithGasPricesAndLimits(params); return fees; } catch (error) { return Promise.reject(new Error(`Failed to get fees: ${(_f = error.msg) !== null && _f !== void 0 ? _f : error.toString()}`)); } }); this.rootDerivationPaths = rootDerivationPaths; this.network = utils_2.xchainNetworkToEths(network); this.infuraCreds = infuraCreds; this.etherscanApiKey = etherscanApiKey; this.ethplorerUrl = ethplorerUrl; this.ethplorerApiKey = ethplorerApiKey; this.explorerUrl = explorerUrl || this.getDefaultExplorerURL(); this.setupProviders(); } /** * Get getFullDerivationPath * * @param {number} index the HD wallet index * @returns {string} The derivation path based on the network. */ getFullDerivationPath(index) { return this.rootDerivationPaths[this.getNetwork()] + `${index}`; } } exports.default = Client; exports.Client = Client; //# sourceMappingURL=client.js.map