@thorwallet/xchain-ethereum
Version:
Ethereum client for XChainJS
636 lines • 28.8 kB
JavaScript
"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