UNPKG

@xchainjs/xchain-evm

Version:
1,283 lines (1,267 loc) 64 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var xchainCrypto = require('@xchainjs/xchain-crypto'); var ethers = require('ethers'); var BigNumber = require('bignumber.js'); var AppEth = require('@ledgerhq/hw-app-eth'); var xchainClient = require('@xchainjs/xchain-client'); var xchainUtil = require('@xchainjs/xchain-util'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var BigNumber__default = /*#__PURE__*/_interopDefault(BigNumber); var AppEth__default = /*#__PURE__*/_interopDefault(AppEth); /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __awaiter(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()); }); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; /** * Abstraction of EVM Account */ class Signer { constructor({ provider, derivationPath }) { this.provider = provider; if (derivationPath.endsWith('/')) { derivationPath = derivationPath.slice(0, -1); } this.derivationPath = derivationPath; } /** * Get the full derivation path based on the wallet index. * @param {number} walletIndex The HD wallet index * @returns {string} The full derivation path */ getFullDerivationPath(walletIndex) { return `${this.derivationPath}/${walletIndex}`; } /** * Get the provider the signer is using to be connected with the blockchain. * @returns {Provider} The provider the signer is using */ getProvider() { return this.provider; } } /** * Signer which operates with an EVM account thanks to the seed phrase */ class KeystoreSigner extends Signer { constructor(params) { super(params); const mnemonic = ethers.Mnemonic.fromPhrase(params.phrase); if (params.derivationPath.endsWith('/')) { params.derivationPath = params.derivationPath.slice(0, -1); } this.hdNode = ethers.HDNodeWallet.fromMnemonic(mnemonic, params.derivationPath); this.phrase = params.phrase; } /** * Validates the given Ethereum address. * * @param {Address} address - The address to validate. * @returns {boolean} `true` if the address is valid, `false` otherwise. */ setPhrase(phrase, walletIndex = 0) { if (this.phrase !== phrase) { if (!xchainCrypto.validatePhrase(phrase)) { throw new Error('Invalid phrase'); } this.phrase = phrase; const mnemonic = ethers.Mnemonic.fromPhrase(phrase); this.hdNode = ethers.HDNodeWallet.fromMnemonic(mnemonic, this.derivationPath); } return this.getAddress(walletIndex); } /** * Purges the client, resetting it to initial state. * * @returns {void} */ purge() { this.hdNode = undefined; this.phrase = undefined; } /** * @deprecated Use getAddressAsync instead. This function will eventually be removed. */ getAddress(walletIndex = 0) { if (walletIndex < 0) { throw new Error('Index must be greater than or equal to zero'); } if (!this.hdNode) { throw new Error('HDNode is not defined. Make sure phrase has been provided.'); } const derived = this.hdNode.deriveChild(walletIndex); return derived.address.toLowerCase(); } /** * Get the current address asynchronously. * * @param {number} walletIndex The current address. * @returns {Address} The current address. * @throws {Error} Thrown if HDNode is not defined, indicating that a phrase is needed to derive an address. * @throws {Error} Thrown if wallet index < 0. */ getAddressAsync() { return __awaiter(this, arguments, void 0, function* (walletIndex = 0) { return this.getAddress(walletIndex); }); } /** * Retrieves the Ethereum wallet interface. * * @param {number} walletIndex - The index of the HD wallet (optional). * @returns {Wallet} The current Ethereum wallet interface. * @throws Error - Thrown if the HDNode is not defined, indicating that a phrase is needed to create a wallet and derive an address. * Note: A phrase is needed to create a wallet and to derive an address from it. */ getWallet(walletIndex = 0) { if (!this.hdNode) { throw new Error('HDNode is not defined. Make sure phrase has been provided.'); } const derived = ethers.HDNodeWallet.fromExtendedKey(this.hdNode.extendedKey).deriveChild(walletIndex); derived.connect(this.getProvider()); return derived; } /** * Sign an EVM transaction with Ledger * * @param {SignTransferParams} params Sign transfer params * @param {string} SignTransferParams.sender Fee option (optional) * @param {ethers.Transaction} SignTransferParams.tx Fee option (optional) * @returns {string} The raw signed transaction. */ signTransfer(_a) { return __awaiter(this, arguments, void 0, function* ({ walletIndex, tx }) { // Get the signer const signer = this.getWallet(walletIndex); // Send the transaction and return the hash return signer.signTransaction(tx); }); } /** * Sign an EVM approve transaction with Ledger * * @param {SignTransferParams} params Sign transfer params * @param {string} SignTransferParams.sender The sender address * @param {ethers.Transaction} SignTransferParams.tx Approve transaction to sign * @returns {string} The raw signed transaction. */ signApprove(_a) { return __awaiter(this, arguments, void 0, function* ({ walletIndex, tx }) { return this.getWallet().signTransaction({ from: yield this.getAddressAsync(walletIndex), to: tx.to, value: tx.value, data: tx.data, nonce: tx.nonce ? new BigNumber__default.default(tx.nonce).toNumber() : undefined, gasPrice: tx.gasPrice, gasLimit: tx.gasLimit, chainId: tx.chainId, }); }); } } /** * Signer which operates with an EVM account through the Ledger device */ class LedgerSigner extends Signer { constructor(params) { super(params); this.app = new AppEth__default.default(params.transport); } /** * Get ethereum ledger app * @returns {AppEth} Ethereum ledger app */ getApp() { return this.app; } /** * Purge signer */ purge() { } /** * Get the current address. * @param {number} index The index of the address. Default 0 * @param {boolean} verify True to check the address against the Ledger device, otherwise false * @returns {Address} The address corresponding to the index provided * @returns */ getAddressAsync() { return __awaiter(this, arguments, void 0, function* (index = 0, verify = false) { if (index < 0) throw Error('Index must be greater than or equal to zero'); const app = this.getApp(); const result = yield app.getAddress(this.getFullDerivationPath(index), verify); return result.address; }); } /** * Sign an EVM transaction with Ledger * * @param {SignTransferParams} params Sign transfer params * @param {string} SignTransferParams.sender Fee option (optional) * @param {Transaction} SignTransferParams.tx Fee option (optional) * @returns {string} The raw signed transaction. */ signTransfer(_a) { return __awaiter(this, arguments, void 0, function* ({ walletIndex, tx }) { // const unsignedTx = ethers.utils.serializeTransaction(tx).substring(2) const unsignedTx = tx.unsignedSerialized.substring(2); const resolution = yield AppEth.ledgerService.resolveTransaction(unsignedTx, {}, { externalPlugins: true, erc20: true }); const ethApp = yield this.getApp(); const signatureData = yield ethApp.signTransaction(this.getFullDerivationPath(walletIndex), unsignedTx, resolution); tx.signature = { v: Number(BigInt(signatureData.v)), r: `0x${signatureData.r}`, s: `0x${signatureData.s}`, }; const rawSignedTx = tx.serialized; return rawSignedTx; }); } /** * Sign an EVM approve transaction with Ledger * * @param {SignTransferParams} params Sign transfer params * @param {string} SignTransferParams.sender The sender address * @param {ethers.Transaction} SignTransferParams.tx Approve transaction to sign * @returns {string} The raw signed transaction. */ signApprove(_a) { return __awaiter(this, arguments, void 0, function* ({ walletIndex, tx }) { const unsignedTx = tx.unsignedSerialized.substring(2); const resolution = yield AppEth.ledgerService.resolveTransaction(unsignedTx, {}, { externalPlugins: true, erc20: true }); const ethApp = yield this.getApp(); const signatureData = yield ethApp.signTransaction(this.getFullDerivationPath(walletIndex), unsignedTx, resolution); tx.signature = { v: Number(BigInt(signatureData.v)), r: `0x${signatureData.r}`, s: `0x${signatureData.s}`, }; const rawSignedTx = tx.serialized; return rawSignedTx; }); } } var erc20ABI = [ { inputs: [ ], stateMutability: "nonpayable", type: "constructor" }, { anonymous: false, inputs: [ { indexed: true, internalType: "address", name: "owner", type: "address" }, { indexed: true, internalType: "address", name: "spender", type: "address" }, { indexed: false, internalType: "uint256", name: "value", type: "uint256" } ], name: "Approval", type: "event" }, { anonymous: false, inputs: [ { indexed: true, internalType: "address", name: "from", type: "address" }, { indexed: true, internalType: "address", name: "to", type: "address" }, { indexed: false, internalType: "uint256", name: "value", type: "uint256" } ], name: "Transfer", type: "event" }, { inputs: [ { internalType: "address", name: "", type: "address" }, { internalType: "address", name: "", type: "address" } ], name: "allowance", outputs: [ { internalType: "uint256", name: "", type: "uint256" } ], stateMutability: "view", type: "function" }, { inputs: [ { internalType: "address", name: "spender", type: "address" }, { internalType: "uint256", name: "value", type: "uint256" } ], name: "approve", outputs: [ { internalType: "bool", name: "success", type: "bool" } ], stateMutability: "nonpayable", type: "function" }, { inputs: [ { internalType: "address", name: "", type: "address" } ], name: "balanceOf", outputs: [ { internalType: "uint256", name: "", type: "uint256" } ], stateMutability: "view", type: "function" }, { inputs: [ ], name: "decimals", outputs: [ { internalType: "uint256", name: "", type: "uint256" } ], stateMutability: "view", type: "function" }, { inputs: [ ], name: "name", outputs: [ { internalType: "string", name: "", type: "string" } ], stateMutability: "view", type: "function" }, { inputs: [ ], name: "symbol", outputs: [ { internalType: "string", name: "", type: "string" } ], stateMutability: "view", type: "function" }, { inputs: [ ], name: "totalSupply", outputs: [ { internalType: "uint256", name: "", type: "uint256" } ], stateMutability: "view", type: "function" }, { inputs: [ { internalType: "address", name: "to", type: "address" }, { internalType: "uint256", name: "value", type: "uint256" } ], name: "transfer", outputs: [ { internalType: "bool", name: "success", type: "bool" } ], stateMutability: "nonpayable", type: "function" }, { inputs: [ { internalType: "address", name: "from", type: "address" }, { internalType: "address", name: "to", type: "address" }, { internalType: "uint256", name: "value", type: "uint256" } ], name: "transferFrom", outputs: [ { internalType: "bool", name: "success", type: "bool" } ], stateMutability: "nonpayable", type: "function" } ]; /** * Maximum approval amount possible, set to 2^256 - 1. */ const MAX_APPROVAL = new BigNumber__default.default(2).pow(256).minus(1); /** * Validate the given address. * * @param {Address} address The address to validate. * @returns {boolean} `true` if the address is valid, otherwise `false`. */ const validateAddress = (address) => { try { ethers.getAddress(address); return true; } catch (error) { return false; } }; /** * Get token address from asset. * * @param {TokenAsset} asset The asset to extract the token address from. * @returns {Address|null} The token address if found, otherwise `null`. */ const getTokenAddress = (asset) => { try { // strip 0X only - 0x is still valid return ethers.getAddress(asset.symbol.slice(asset.ticker.length + 1).replace(/^0X/, '')); } catch (err) { return null; } }; /** * Calculate fees by multiplying gas price and gas limit. * * @returns {Fees} The calculated fee. */ const getFee = ({ gasPrice, gasLimit, decimals, }) => xchainUtil.baseAmount(gasPrice.amount().multipliedBy(gasLimit.toString()), decimals); /** * Get address prefix based on the network. * * @returns {string} The address prefix based on the network ('0x'). */ const getPrefix = () => '0x'; /** * Returns approval amount. If amount is not set or zero, returns `MAX_APPROVAL`. * * @param {BaseAmount} amount The amount to check. * @returns {ethers.BigNumber} The approval amount. */ const getApprovalAmount = (amount) => amount && amount.gt(xchainUtil.baseAmount(0, amount.decimal)) ? new BigNumber__default.default(amount.amount().toFixed()) : MAX_APPROVAL; /** * Estimate gas required for calling a contract function. * * @param {object} params Parameters for estimating gas. * @param {Provider} provider Provider to interact with the contract. * @param {Address} contractAddress The contract address. * @param {ContractInterface} abi The contract ABI json. * @param {string} funcName The function to be called. * @param {unknown[]} funcParams The parameters of the function. * @returns {BigNumber} The estimated gas required for the function call. */ const estimateCall = (_a) => __awaiter(void 0, [_a], void 0, function* ({ provider, contractAddress, abi, funcName, funcParams = [], }) { const contract = new ethers.Contract(contractAddress, abi, provider); const estiamtion = yield contract.getFunction(funcName).estimateGas(...funcParams); return yield new BigNumber__default.default(estiamtion.toString()); }); /** * Calls a contract function. * * @param {Provider} provider The provider to interact with the contract. * @param {Signer} signer The signer of the transaction (optional - needed for sending transactions only). * @param {Address} contractAddress The contract address. * @param {ContractInterface} abi The contract ABI json. * @param {string} funcName The function to be called. * @param {unknown[]} funcParams (optional) The parameters of the function. * @returns {Promise<T>} The result of the contract function call. */ const call = (_a) => __awaiter(void 0, [_a], void 0, function* ({ provider, signer, contractAddress, abi, funcName, funcParams = [], }) { let contract = new ethers.Contract(contractAddress, abi, provider); if (signer) { // For sending transactions, a signer is needed contract = contract.connect(signer); } // eslint-disable-next-line @typescript-eslint/no-explicit-any return contract[funcName](...funcParams); }); /** * Estimate gas for calling `approve`. * * @param {Provider} provider The provider to interact with the contract. * @param {Address} contractAddress The contract address. * @param {Address} spenderAddress The spender address. * @param {Address} fromAddress The address a transaction is sent from. * @param {ContractInterface} abi The contract ABI json. * @param {BaseAmount} amount (optional) The amount of token. By default, it will be unlimited token allowance. * @returns {Promise<ethers.BigNumber>} The estimated gas. */ function estimateApprove(_a) { return __awaiter(this, arguments, void 0, function* ({ provider, contractAddress, spenderAddress, fromAddress, abi, amount, }) { const txAmount = getApprovalAmount(amount); return yield estimateCall({ provider, contractAddress, abi, funcName: 'approve', funcParams: [spenderAddress, BigInt(txAmount.toFixed(0)), { from: fromAddress }], }); }); } /** * Check allowance. * * @param {Provider} provider The provider to interact with the contract. * @param {Address} contractAddress The contract (ERC20 token) address. * @param {Address} spenderAddress The spender address (router). * @param {Address} fromAddress The address a transaction is sent from. * @param {BaseAmount} amount The amount to check if it's allowed to spend or not (optional). * @param {number} walletIndex (optional) HD wallet index * @returns {Promise<boolean>} `true` if the spender is allowed to spend the specified amount, `false` otherwise. */ function isApproved(_a) { return __awaiter(this, arguments, void 0, function* ({ provider, contractAddress, spenderAddress, fromAddress, amount, }) { var _b; const txAmount = new BigNumber__default.default((_b = amount === null || amount === void 0 ? void 0 : amount.amount().toFixed()) !== null && _b !== void 0 ? _b : 1); const contract = new ethers.Contract(contractAddress, erc20ABI, provider); const allowanceResponse = yield contract.allowance(fromAddress, spenderAddress); const allowance = new BigNumber__default.default(allowanceResponse.toString()); return txAmount.lte(allowance); }); } /** * Removes `0x` or `0X` from the beginning of the address string. * * @param {Address} addr The address to remove the `0x` or `0X` prefix from. * @returns {string} The address without the `0x` or `0X` prefix. */ const strip0x = (addr) => addr.replace(/^0(x|X)/, ''); /** * Get the chain identifier the provider is connected with * @param {Provider} provider Provider * @returns {number} the chain identifier the provider is connected with */ const getNetworkId = (provider) => __awaiter(void 0, void 0, void 0, function* () { const network = yield provider.getNetwork(); return Number(network.chainId); }); /** * Custom EVM client class. */ class Client extends xchainClient.BaseXChainClient { /** * Constructor for the EVM client. * @param {EVMClientParams} params - Parameters for configuring the EVM client. */ constructor({ chain, gasAsset, gasAssetDecimals, defaults, network = xchainClient.Network.Mainnet, feeBounds, providers, rootDerivationPaths, explorerProviders, dataProviders, signer, }) { super(chain, { network, rootDerivationPaths, feeBounds }); this.config = { chain, gasAsset, gasAssetDecimals, defaults, network, feeBounds, providers, rootDerivationPaths, explorerProviders, dataProviders, }; this.signer = signer; this.defaults = defaults; this.cachedNetworkId = new xchainUtil.CachedValue(() => getNetworkId(this.getProvider())); } /** * Retrieves the Ethereum Provider interface. * @returns {Provider} The current Ethereum Provider interface. */ getProvider() { return this.config.providers[this.network]; } /** * Retrieves the explorer URL based on the current network. * @returns {string} The explorer URL for Ethereum based on the current network. */ getExplorerUrl() { return this.config.explorerProviders[this.network].getExplorerUrl(); } /** * Retrieves asset information. * @returns {AssetInfo} Asset information containing the asset and its decimal places. */ getAssetInfo() { const assetInfo = { asset: this.config.gasAsset, decimal: this.config.gasAssetDecimals, }; return assetInfo; } /** * Retrieves the explorer URL for a given address. * @param {Address} address - The address to retrieve the explorer URL for. * @returns {string} The explorer URL for the given address. */ getExplorerAddressUrl(address) { return this.config.explorerProviders[this.network].getExplorerAddressUrl(address); } /** * Retrieves the explorer URL for a given transaction ID. * @param {string} txID - The transaction ID to retrieve the explorer URL for. * @returns {string} The explorer URL for the given transaction ID. */ getExplorerTxUrl(txID) { return this.config.explorerProviders[this.network].getExplorerTxUrl(txID); } /** * Sets or updates the current network. * @param {Network} network - The network to set or update. * @returns {void} * @throws {"Network must be provided"} Thrown if the network has not been set before. */ setNetwork(network) { super.setNetwork(network); this.cachedNetworkId = new xchainUtil.CachedValue(() => getNetworkId(this.getProvider())); } /** * @throws {Error} Method not implement */ getAddress() { throw Error('Sync method not supported for Ledger'); } getAddressAsync(walletIndex, verify = false) { return this.getSigner().getAddressAsync(walletIndex, verify); } /** * Validate the given address. * * @param {Address} address * @returns {boolean} `true` or `false` */ validateAddress(address) { return validateAddress(address); } /** * Retrieves the balance of a given address. * @param {Address} address - The address to retrieve the balance for. * @param {Asset[]} assets - Assets to retrieve the balance for (optional). * @returns {Promise<Balance[]>} An array containing the balance of the address. * @throws {"Invalid asset"} Thrown when the provided asset is invalid. */ getBalance(address, assets) { return __awaiter(this, void 0, void 0, function* () { return yield this.roundRobinGetBalance(address, assets); }); } /** * Retrieves the transaction history of a given address with pagination options. * @param {TxHistoryParams} params - Options to get transaction history (optional). * @returns {Promise<TxsPage>} The transaction history. */ getTransactions(params) { return __awaiter(this, void 0, void 0, function* () { const filteredParams = { address: (params === null || params === void 0 ? void 0 : params.address) || (yield this.getAddressAsync()), offset: params === null || params === void 0 ? void 0 : params.offset, limit: params === null || params === void 0 ? void 0 : params.limit, startTime: params === null || params === void 0 ? void 0 : params.startTime, asset: params === null || params === void 0 ? void 0 : params.asset, }; return yield this.roundRobinGetTransactions(filteredParams); }); } /** * Retrieves the transaction details of a given transaction ID. * @param {string} txId - The transaction ID. * @param {string} assetAddress - The asset address (optional). * @returns {Promise<Tx>} The transaction details of the given transaction ID. * @throws {"Need to provide valid txId"} Thrown if the provided transaction ID is invalid. */ getTransactionData(txId, assetAddress) { return __awaiter(this, void 0, void 0, function* () { return yield this.roundRobinGetTransactionData(txId, assetAddress); }); } /** * Estimates the gas required for calling a contract function. * @param {Address} contractAddress The contract address. * @param {ContractInterface} abi The contract ABI json. * @param {string} funcName The function to be called. * @param {any[]} funcParams The parameters of the function. * @param {number} walletIndex (optional) HD wallet index * @param {EstimateCallParams} params - Parameters for estimating gas. * @returns {BigNumber} The estimated gas required for the contract function call. */ estimateCall(_a) { return __awaiter(this, arguments, void 0, function* ({ contractAddress, abi, funcName, funcParams = [] }) { return estimateCall({ provider: this.getProvider(), contractAddress, abi, funcName, funcParams, }); }); } /** * Check allowance. * * @param {Address} contractAddress The contract address. * @param {Address} spenderAddress The spender address. * @param {BaseAmount} amount The amount to check if it's allowed to spend or not (optional). * @param {number} walletIndex (optional) HD wallet index * @param {IsApprovedParams} params - Parameters for checking allowance. * @returns {boolean} `true` if the allowance is approved, `false` otherwise. */ isApproved(_a) { return __awaiter(this, arguments, void 0, function* ({ contractAddress, spenderAddress, amount, walletIndex }) { const allowance = yield isApproved({ provider: this.getProvider(), amount, spenderAddress, contractAddress, fromAddress: yield this.getAddressAsync(walletIndex), }); return allowance; }); } /** * Estimates the gas required for approving an allowance. * * @param {EstimateApproveParams} params - Parameters for estimating gas. * @param {Address} contractAddress The contract address. * @param {Address} spenderAddress The spender address. * @param {Address} fromAddress The address the approve transaction is sent from. * @param {BaseAmount} amount The amount of token. By default, it will be unlimited token allowance. (optional) * * @returns {BigNumber} The estimated gas required for the approval. */ estimateApprove(_a) { return __awaiter(this, arguments, void 0, function* ({ fromAddress, contractAddress, spenderAddress, amount, }) { return yield estimateApprove({ provider: this.getProvider(), contractAddress, spenderAddress, fromAddress, abi: erc20ABI, amount, }); }); } /** * Broadcasts a transaction. * @param {string} txHex - The transaction in hexadecimal format. * @returns {Promise<TxHash>} The transaction hash. */ broadcastTx(txHex) { return __awaiter(this, void 0, void 0, function* () { const provider = this.config.providers[this.network]; if (!provider.broadcastTransaction) { throw new Error('Provider does not support sendTransaction'); } const resp = yield provider.broadcastTransaction(txHex); return resp.hash; }); } /** * Estimates gas prices (average, fast, fastest) for a transaction. * @param {Protocol} protocol The protocol to use for estimating gas prices. * @returns {GasPrices} The gas prices (average, fast, fastest) in `Wei` (`BaseAmount`) */ estimateGasPrices(protocol) { return __awaiter(this, void 0, void 0, function* () { if (!protocol) { try { // Attempt to fetch gas prices via round-robin from multiple providers const feeRates = yield this.roundRobinGetFeeRates(); return { [xchainClient.FeeOption.Average]: xchainUtil.baseAmount(feeRates.average, this.config.gasAssetDecimals), [xchainClient.FeeOption.Fast]: xchainUtil.baseAmount(feeRates.fast, this.config.gasAssetDecimals), [xchainClient.FeeOption.Fastest]: xchainUtil.baseAmount(feeRates.fastest, this.config.gasAssetDecimals), }; } catch (error) { console.warn(`Can not round robin over GetFeeRates: ${error}`); } try { // If round-robin fails, fetch gas price from the primary provider const feeData = yield this.getProvider().getFeeData(); if (!feeData.gasPrice) { throw new Error('Gas price is null'); } const gasPrice = new BigNumber__default.default(feeData.gasPrice.toString()); // Adjust gas prices for different fee options return { [xchainClient.FeeOption.Average]: xchainUtil.baseAmount(gasPrice.toString(), this.config.gasAssetDecimals), [xchainClient.FeeOption.Fast]: xchainUtil.baseAmount(gasPrice.multipliedBy(1.5).toString(), this.config.gasAssetDecimals), [xchainClient.FeeOption.Fastest]: xchainUtil.baseAmount(gasPrice.multipliedBy(2).toString(), this.config.gasAssetDecimals), }; } catch (error) { console.warn(`Can not get gasPrice from provider: ${error}`); } } // If primary provider fails or fallback to THORChain protocol, fetch gas prices from THORChain if (!protocol || protocol === xchainClient.Protocol.THORCHAIN) { try { // Fetch fee rates from THORChain and convert to BaseAmount // Note: `rates` are in `gwei` // @see https://gitlab.com/thorchain/thornode/-/blob/develop/x/thorchain/querier.go#L416-420 // To have all values in `BaseAmount`, they needs to be converted into `wei` (1 gwei = 1,000,000,000 wei = 1e9) const ratesInGwei = xchainClient.standardFeeRates(yield this.getFeeRateFromThorchain()); return { [xchainClient.FeeOption.Average]: xchainUtil.baseAmount(ratesInGwei[xchainClient.FeeOption.Average] * Math.pow(10, 9), this.config.gasAssetDecimals), [xchainClient.FeeOption.Fast]: xchainUtil.baseAmount(ratesInGwei[xchainClient.FeeOption.Fast] * Math.pow(10, 9), this.config.gasAssetDecimals), [xchainClient.FeeOption.Fastest]: xchainUtil.baseAmount(ratesInGwei[xchainClient.FeeOption.Fastest] * Math.pow(10, 9), this.config.gasAssetDecimals), }; } catch (error) { console.warn(error); } } // Default fee rates if everything else fails const defaultRatesInGwei = xchainClient.standardFeeRates(this.defaults[this.network].gasPrice.toNumber()); return { [xchainClient.FeeOption.Average]: xchainUtil.baseAmount(defaultRatesInGwei[xchainClient.FeeOption.Average], this.config.gasAssetDecimals), [xchainClient.FeeOption.Fast]: xchainUtil.baseAmount(defaultRatesInGwei[xchainClient.FeeOption.Fast], this.config.gasAssetDecimals), [xchainClient.FeeOption.Fastest]: xchainUtil.baseAmount(defaultRatesInGwei[xchainClient.FeeOption.Fastest], this.config.gasAssetDecimals), }; }); } /** * Estimates gas limit for a transaction. * * @param {TxParams} params The transaction and fees options. * @returns {BaseAmount} The estimated gas limit. * @throws Error Thrown if address could not be parsed from the given ERC20 asset. */ estimateGasLimit(_a) { return __awaiter(this, arguments, void 0, function* ({ asset, recipient, amount, memo, from, isMemoEncoded, }) { const txAmount = BigInt(amount.amount().toFixed()); const theAsset = asset !== null && asset !== void 0 ? asset : this.config.gasAsset; let gasEstimate; if (!this.isGasAsset(theAsset)) { // ERC20 gas estimate const assetAddress = getTokenAddress(theAsset); if (!assetAddress) throw Error(`Can't get address from asset ${xchainUtil.assetToString(theAsset)}`); const contract = new ethers.Contract(assetAddress, erc20ABI, this.getProvider()); const address = from || (yield this.getAddressAsync()); // eslint-disable-next-line @typescript-eslint/no-explicit-any const gasEstimateResponse = yield contract.getFunction('transfer').estimateGas(recipient, txAmount, { from: address, }); gasEstimate = new BigNumber__default.default(gasEstimateResponse.toString()); } else { // ETH gas estimate let stringEncodedMemo; if (memo) { stringEncodedMemo = ethers.hexlify(ethers.toUtf8Bytes(memo)); } const parsedMemo = memo ? (isMemoEncoded ? memo : stringEncodedMemo) : undefined; const transactionRequest = { from: from || (yield this.getAddressAsync()), to: recipient, value: txAmount, data: parsedMemo, }; const gasEstimation = yield this.getProvider().estimateGas(transactionRequest); gasEstimate = new BigNumber__default.default(gasEstimation.toString()); } return gasEstimate; }); } /** * Checks if the given asset matches the gas asset. * * @param {Asset} asset - The asset to check. * @returns {boolean} True if the asset matches the gas asset, false otherwise. */ isGasAsset(asset) { return xchainUtil.eqAsset(this.config.gasAsset, asset); } /** * Estimates gas prices/limits (average, fast, fastest) and fees for a transaction. * * @param {TxParams} params The transaction parameters. * @returns {FeesWithGasPricesAndLimits} The estimated gas prices/limits and fees. */ estimateFeesWithGasPricesAndLimits(params) { return __awaiter(this, void 0, void 0, function* () { // Gas prices estimation const gasPrices = yield this.estimateGasPrices(); const decimals = this.config.gasAssetDecimals; const { fast: fastGP, fastest: fastestGP, average: averageGP } = gasPrices; // Gas limits estimation const gasLimit = yield this.estimateGasLimit({ asset: params.asset, amount: params.amount, recipient: params.recipient, memo: params.memo, }); // Calculate fees return { gasPrices, fees: { type: xchainClient.FeeType.PerByte, average: getFee({ gasPrice: averageGP, gasLimit, decimals }), fast: getFee({ gasPrice: fastGP, gasLimit, decimals }), fastest: getFee({ gasPrice: fastestGP, gasLimit, decimals }), }, gasLimit, }; }); } /** * Wait until tx is confirmed * @param {string} hash - tx's hash */ awaitTxConfirmed(hash) { return __awaiter(this, void 0, void 0, function* () { yield this.getProvider().waitForTransaction(hash); }); } getFees(params) { return __awaiter(this, void 0, void 0, function* () { if (!params) throw new Error('Params need to be passed'); const { fees } = yield this.estimateFeesWithGasPricesAndLimits(params); return fees; }); } /** * Retrieves the balance of an address by round-robin querying multiple data providers. * * @param {Address} address - The address to query the balance for. * @param {Asset[]} [assets] - Optional list of assets to query the balance for. * @returns {Promise<Balance[]>} The balance information for the address. * @throws Error Thrown if no provider is able to retrieve the balance. */ roundRobinGetBalance(address, assets) { return __awaiter(this, void 0, void 0, function* () { for (const provider of this.config.dataProviders) { try { const prov = provider[this.network]; if (prov) return yield prov.getBalance(address, assets); } catch (error) { console.warn(error); } } throw Error('no provider able to get balance'); }); } /** * Retrieves transaction data by round-robin querying multiple data providers. * * @param {string} txId - The transaction ID. * @param {string} [assetAddress] - Optional asset address. * @returns {Promise<Tx>} The transaction data. * @throws Error Thrown if no provider is able to retrieve the transaction data. */ roundRobinGetTransactionData(txId, assetAddress) { return __awaiter(this, void 0, void 0, function* () { for (const provider of this.config.dataProviders) { try { const prov = provider[this.network]; if (prov) return yield prov.getTransactionData(txId, assetAddress); } catch (error) { console.warn(error); } } throw Error('no provider able to GetTransactionData'); }); } /** * Retrieves transaction history by round-robin querying multiple data providers. * * @param {TxHistoryParams} params - The transaction history parameters. * @returns {Promise<TxsPage>} The transaction history. * @throws Error Thrown if no provider is able to retrieve the transaction history. */ roundRobinGetTransactions(params) { return __awaiter(this, void 0, void 0, function* () { for (const provider of this.config.dataProviders) { try { const prov = provider[this.network]; if (prov) return yield prov.getTransactions(params); } catch (error) { console.warn(error); } } throw Error('no provider able to GetTransactions'); }); } /** * Retrieves fee rates by round-robin querying multiple data providers. * * @returns {Promise<FeeRates>} The fee rates. * @throws Error Thrown if no provider is able to retrieve the fee rates. */ roundRobinGetFeeRates() { return __awaiter(this, void 0, void 0, function* () { for (const provider of this.config.dataProviders) { try { const prov = provider[this.network]; if (prov) return yield prov.getFeeRates(); } catch (error) { console.warn(error); } } throw Error('No provider available to getFeeRates'); }); } /** * Prepares a transaction for transfer. * * @param {TxParams&Address&FeeOption&BaseAmount&BigNumber} params - The transfer options. * @returns {Promise<PreparedTx>} The raw unsigned transaction. * @throws Error Thrown if the provided asset chain does not match the client's chain, or if any of the addresses are invalid. */ prepareTx(_a) { return __awaiter(this, arguments, void 0, function* ({ sender, asset = this.config.gasAsset, memo, amount, recipient, isMemoEncoded = false, }) { if (asset.chain !== this.chain) throw Error(`This client can only prepare transactions on chain: ${this.chain}. Bad asset: ${asset.chain}`); if (!this.validateAddress(sender)) throw Error('Invalid sender address'); if (!this.validateAddress(recipient)) throw Error('Invalid recipient address'); const nonce = yield this.getProvider().getTransactionCount(sender); if (this.isGasAsset(asset)) { let stringEncodedMemo; if (memo) { stringEncodedMemo = ethers.toUtf8Bytes(memo); } const parsedMemo = memo ? (isMemoEncoded ? memo : stringEncodedMemo) : undefined; const tx = new ethers.Transaction(); tx.chainId = yield this.cachedNetworkId.getValue(); tx.to = recipient; tx.value = amount.amount().toFixed(); tx.nonce = nonce; if (parsedMemo) { tx.data = parsedMemo; } return { rawUnsignedTx: tx.unsignedSerialized, }; } else { const assetAddress = getTokenAddress(asset); if (!assetAddress) throw Error(`Can't parse address from asset ${xchainUtil.assetToString(asset)}`); const contract = new ethers.Contract(assetAddress, erc20ABI, this.getProvider()); const amountToTransfer = BigInt(amount.amount().toFixed()); const unsignedTx = yield contract.getFunction('transfer').populateTransaction(recipient, amountToTransfer); unsignedTx.chainId = BigInt(yield this.cachedNetworkId.getValue()); unsignedTx.nonce = nonce; const tx = ethers.Transaction.from(unsignedTx); return { rawUnsignedTx: tx.unsignedSerialized, }; } }); } /** * Prepares an approval transaction. * * @param {ApproveParams&Address&FeeOption&BaseAmount&BigNumber} params - The approval options. * @returns {Promise<PreparedTx>} The raw unsigned transaction. * @throws Error Thrown if any of the addresses are invalid. */ prepareApprove(_a) { return __awaiter(this, arguments, void 0, function* ({ contractAddress, spenderAddress, amount, sender, }) { if (!this.validateAddress(contractAddress)) throw Error('Invalid contractAddress address'); if (!this.validateAddress(spenderAddress)) throw Error('Invalid spenderAddress address'); if (!this.validateAddress(sender)) throw Error('Invalid sender address'); const contract = new ethers.Contract(contractAddress, erc20ABI, this.getProvider()); const valueToApprove = getApprovalAmount(amount); const unsignedTx = yield contract .getFunction('approve') .populateTransaction(spenderAddress, BigInt(valueToApprove.toFixed())); const nonce = yield this.getProvider().getTransactionCount(sender); unsignedTx.chainId = BigInt(yield this.cachedNetworkId.getValue()); unsignedTx.nonce = nonce; const tx = ethers.Transaction.from(unsignedTx); return { rawUnsignedTx: tx.unsignedSerialized, }; }); } /** * Call a contract function. * @param {signer} Signer (optional) The address a transaction is send from. If not set, signer will be defined based on `walletIndex` * @param {Address} contractAddress The contract address. * @param {number} walletIndex (optional) HD wallet index * @param {ContractInterface} abi The contract ABI json. * @param {string} funcName The function to be called. * @param {unknown[]} funcParams (optional) The parameters of the function. * @param {CallParams} params - Parameters for calling the contract function. * @returns {T} The result of the contract function call.. */ call(_a) { return __awaiter(this, arguments, void 0, function* ({ contractAddress, abi, funcName, funcParams = [], signer }) { return call({ provider: this.getProvider(), signer, contractAddress, abi, funcName, funcParams }); }); } /** * Transfers ETH or ERC20 token * * Note: A given `feeOption` wins over `gasPrice` and `gasLimit` * * @param {TxParams} params The transfer options. * @param {feeOption} FeeOption Fee option (optional) * @param {gasPrice} BaseAmount Gas price (optional) * @param {maxFeePerGas} BaseAmount Optional. Following EIP-1559, maximum fee per gas. Parameter not compatible with gasPrice * @param {maxPriorityFeePerGas} BaseAmount Optional. Following EIP-1559, maximum priority fee per gas. Parameter not compatible with gasPrice * @param {gasLimit} BigNumber Gas limit (optional) * @throws Error Thrown if address of given `Asset` could not be parsed * @throws {Error} Error thrown if not compatible fee parameters are provided * @returns {TxHash} The transaction hash. */ transfer(_a) { return __awaiter(this, arguments, void 0, function* ({ walletIndex = 0, asset = this.getAssetInfo().asset, memo, amount, recipient, feeOption = xchainClient.FeeOption.Fast, gasPrice, maxFeePerGas, maxPriorityFeePerGas, gasLimit, isMemoEncoded, }) { // Check for compatibility between gasPrice and EIP 1559 parameters if (gasPrice && (maxFeePerGas || maxPriorityFeePerGas)) { throw new Error('gasPrice is not compatible with EIP 1559 (maxFeePerGas and maxPriorityFeePerGas) params'); } // Initialize fee data object const feeData = { maxFeePerGas: null, maxPriorityFeePerGas: null, gasPrice: null, }; const feeInfo = yield this.getProvider().getFeeData(); // If EIP 1559 parameters are provided, use them; otherwise, estimate gas price if (maxFeePerGas || maxPriorityFeePerGas) { // Get fee info from the provider const block = yield this.getProvider().getBlock('latest'); // Set max fee per gas if (maxFeePerGas) { // Set max priority fee per gas feeData.maxFeePerGas = BigInt(maxFeePerGas.amount().toFixed()); } else if (maxPriorityFeePerGas && (block === null || block === void 0 ? void 0 : block.baseFeePerGas)) { const baseFee = block.baseFeePerGas; const maxPriority = BigInt(maxPriorityFeePerGas.amount().toFixed()); feeData.maxFeePerGas = baseFee * BigInt(2) + maxPriority; } feeData.maxPriorityFeePerGas = maxPriorityFeePerGas ? BigInt(maxPriorityFeePerGas.amount().toFixed()) : feeInfo.maxPriorityFeePerGas; } else { const txGasPrice = gasPrice ? // Estimate gas price based on