UNPKG

@dojima-wallet/connection

Version:

Initialise and connection for layer 1&2 blockchain

594 lines (551 loc) 15 kB
import { Balance, FeeType, Fees, Network, Tx, TxType } from "../client"; import { Address, Asset, AssetETH, BaseAmount, EthChain, assetAmount, assetFromString, assetToBase, assetToString, baseAmount, eqAsset, } from "@dojima-wallet/utils"; import { Signer, ethers, providers } from "ethers"; import { parseUnits } from "ethers/lib/utils"; import erc20ABI from "./data/erc20.json"; import { ETHTransactionInfo, EthNetwork, FeesWithGasPricesAndLimits, GasPrices, TokenBalance, TokenTransactionInfo, TransactionInfo, TransactionOperation, } from "./types"; import { ETH_DECIMAL } from "./const"; export const ETHPLORER_FREEKEY = "freekey"; // from https://github.com/MetaMask/metamask-extension/blob/ee205b893fe61dc4736efc576e0663189a9d23da/ui/app/pages/send/send.constants.js#L39 // and based on recommendations of https://ethgasstation.info/blog/gas-limit/ export const SIMPLE_GAS_COST: ethers.BigNumber = ethers.BigNumber.from(21000); export const BASE_TOKEN_GAS_COST: ethers.BigNumber = ethers.BigNumber.from(100000); // default gas price in gwei export const DEFAULT_GAS_PRICE = 50; export const ETHAddress = "0x0000000000000000000000000000000000000000"; export const MAX_APPROVAL: ethers.BigNumber = ethers.BigNumber.from(2) .pow(256) .sub(1); /** * Network -> EthNetwork * * @param {Network} network * @returns {EthNetwork} */ export const chainNetworkToEths = (network: Network): EthNetwork => { switch (network) { case Network.Mainnet: case Network.Stagenet: return EthNetwork.Main; case Network.Testnet: return EthNetwork.Test; } }; /** * EthNetwork -> Network * * @param {EthNetwork} network * @returns {Network} */ export const ethNetworkTochains = (network: EthNetwork): Network => { switch (network) { case EthNetwork.Main: return Network.Mainnet; case EthNetwork.Test: return Network.Testnet; } }; /** * Validate the given address. * * @param {Address} address * @returns {boolean} `true` or `false` */ export const validateAddress = (address: Address): boolean => { try { ethers.utils.getAddress(address); return true; } catch (error) { return false; } }; /** * Get token address from asset. * * @param {Asset} asset * @returns {Address|null} The token address. */ export const getTokenAddress = (asset: Asset): Address | null => { try { // strip 0X only - 0x is still valid return ethers.utils.getAddress( asset.symbol.slice(asset.ticker.length + 1).replace(/^0X/, "") ); } catch (err) { return null; } }; /** * Checks whether an `Asset` is `AssetETH` or not * * @param {Asset} asset * @returns {boolean} Result of check if an asset is ETH or not */ export const isEthAsset = (asset: Asset): boolean => eqAsset(AssetETH, asset); /** * Parses asset address from `Asset` * * @param {Asset} asset * @returns {Address|null} Asset address */ export const getAssetAddress = (asset: Asset): Address | null => { if (isEthAsset(asset)) return ETHAddress; return getTokenAddress(asset); }; /** * Check if the symbol is valid. * * @param {string|null|undefined} symbol * @returns {boolean} `true` or `false`. */ export const validateSymbol = (symbol?: string | null): boolean => symbol ? symbol.length >= 3 : false; /** * Get transactions from token tx * * @param {TokenTransactionInfo} tx * @returns {Tx|null} The parsed transaction. */ export const getTxFromTokenTransaction = ( tx: TokenTransactionInfo ): Tx | null => { const decimals = parseInt(tx.tokenDecimal) || ETH_DECIMAL; const symbol = tx.tokenSymbol; const address = tx.contractAddress; if (validateSymbol(symbol) && validateAddress(address)) { const tokenAsset = assetFromString( `${EthChain.ticker}.${symbol}-${address}` ); if (tokenAsset) { return { asset: tokenAsset, from: [ { from: tx.from, amount: baseAmount(tx.value, decimals), }, ], to: [ { to: tx.to, amount: baseAmount(tx.value, decimals), }, ], date: new Date(parseInt(tx.timeStamp) * 1000), type: TxType.Transfer, hash: tx.hash, }; } } return null; }; /** * Get transactions from ETH transaction * * @param {ETHTransactionInfo} tx * @returns {Tx} The parsed transaction. */ export const getTxFromEthTransaction = (tx: ETHTransactionInfo): Tx => { return { asset: AssetETH, from: [ { from: tx.from, amount: baseAmount(tx.value, ETH_DECIMAL), }, ], to: [ { to: tx.to, amount: baseAmount(tx.value, ETH_DECIMAL), }, ], date: new Date(parseInt(tx.timeStamp) * 1000), type: TxType.Transfer, hash: tx.hash, }; }; /** * Get transactions from operation * * @param {TransactionOperation} operation * @returns {Tx|null} The parsed transaction. */ export const getTxFromEthplorerTokenOperation = ( operation: TransactionOperation ): Tx | null => { const decimals = parseInt(operation.tokenInfo.decimals) || ETH_DECIMAL; const { symbol, address } = operation.tokenInfo; if (validateSymbol(symbol) && validateAddress(address)) { const tokenAsset = assetFromString( `${EthChain.ticker}.${symbol}-${address}` ); if (tokenAsset) { return { asset: tokenAsset, from: [ { from: operation.from, amount: baseAmount(operation.value, decimals), }, ], to: [ { to: operation.to, amount: baseAmount(operation.value, decimals), }, ], date: new Date(operation.timestamp * 1000), type: operation.type === "transfer" ? TxType.Transfer : TxType.Unknown, hash: operation.transactionHash, }; } } return null; }; /** * Get transactions from ETH transaction * * @param {TransactionInfo} txInfo * @returns {Tx} The parsed transaction. */ export const getTxFromEthplorerEthTransaction = ( txInfo: TransactionInfo ): Tx => { return { asset: AssetETH, from: [ { from: txInfo.from, amount: assetToBase(assetAmount(txInfo.value, ETH_DECIMAL)), }, ], to: [ { to: txInfo.to, amount: assetToBase(assetAmount(txInfo.value, ETH_DECIMAL)), }, ], date: new Date(txInfo.timestamp * 1000), type: TxType.Transfer, hash: txInfo.hash, }; }; /** * Calculate fees by multiplying . * * @returns {Fees} The default gas price. */ export const getFee = ({ gasPrice, gasLimit, }: { gasPrice: BaseAmount; gasLimit: ethers.BigNumber; }) => baseAmount(gasPrice.amount().multipliedBy(gasLimit.toString()), ETH_DECIMAL); export const estimateDefaultFeesWithGasPricesAndLimits = ( asset?: Asset ): FeesWithGasPricesAndLimits => { const gasPrices = { average: baseAmount( parseUnits(DEFAULT_GAS_PRICE.toString(), "gwei").toString(), ETH_DECIMAL ), fast: baseAmount( parseUnits((DEFAULT_GAS_PRICE * 2).toString(), "gwei").toString(), ETH_DECIMAL ), fastest: baseAmount( parseUnits((DEFAULT_GAS_PRICE * 3).toString(), "gwei").toString(), ETH_DECIMAL ), }; const { fast: fastGP, fastest: fastestGP, average: averageGP } = gasPrices; let assetAddress; if (asset && assetToString(asset) !== assetToString(AssetETH)) { assetAddress = getTokenAddress(asset); } let gasLimit; if (assetAddress && assetAddress !== ETHAddress) { gasLimit = ethers.BigNumber.from(BASE_TOKEN_GAS_COST); } else { gasLimit = ethers.BigNumber.from(SIMPLE_GAS_COST); } return { gasPrices, gasLimit, fees: { type: FeeType.PerByte, average: getFee({ gasPrice: averageGP, gasLimit }), fast: getFee({ gasPrice: fastGP, gasLimit }), fastest: getFee({ gasPrice: fastestGP, gasLimit }), }, }; }; /** * Get the default fees. * * @returns {Fees} The default gas price. */ export const getDefaultFees = (asset?: Asset): Fees => { const { fees } = estimateDefaultFeesWithGasPricesAndLimits(asset); return fees; }; /** * Get the default gas price. * * @returns {Fees} The default gas prices. */ export const getDefaultGasPrices = (asset?: Asset): GasPrices => { const { gasPrices } = estimateDefaultFeesWithGasPricesAndLimits(asset); return gasPrices; }; /** * Get address prefix based on the network. * * @returns {string} The address prefix based on the network. * **/ export const getPrefix = () => "0x"; /** * Filter self txs * * @returns {T[]} * **/ export const filterSelfTxs = < T extends { from: string; to: string; hash: string } >( txs: T[] ): T[] => { const filterTxs = txs.filter((tx) => tx.from !== tx.to); let selfTxs = txs.filter((tx) => tx.from === tx.to); while (selfTxs.length) { const selfTx = selfTxs[0]; filterTxs.push(selfTx); selfTxs = selfTxs.filter((tx) => tx.hash !== selfTx.hash); } return filterTxs; }; /** * Returns approval amount * * If given amount is not set or zero, `MAX_APPROVAL` amount is used */ export const getApprovalAmount = (amount?: BaseAmount): ethers.BigNumber => amount && amount.gt(baseAmount(0, amount.decimal)) ? ethers.BigNumber.from(amount.amount().toFixed()) : MAX_APPROVAL; /** * Call a contract function. * * @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 result of the contract function call. */ export const estimateCall = async ({ provider, contractAddress, abi, funcName, funcParams = [], }: { provider: providers.Provider; contractAddress: Address; abi: ethers.ContractInterface; funcName: string; funcParams?: unknown[]; }): Promise<ethers.BigNumber> => { const contract: ethers.Contract = new ethers.Contract( contractAddress, abi, provider ); return await contract.estimateGas[funcName](...funcParams); }; /** * Calls a contract function. * * @param {Provider} provider Provider to interact with the contract. * @param {signer} 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 {T} The result of the contract function call. */ export const call = async <T>({ provider, signer, contractAddress, abi, funcName, funcParams = [], }: { provider: providers.Provider; signer?: Signer; contractAddress: Address; abi: ethers.ContractInterface; funcName: string; funcParams?: unknown[]; }): Promise<T> => { let contract = new ethers.Contract(contractAddress, abi, provider); if (signer) { // For sending transactions a signer is needed contract = contract.connect(signer); } return contract[funcName](...funcParams); }; /** * Estimate gas for calling `approve`. * * @param {Provider} provider 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 {BaseAmount} amount (optional) The amount of token. By default, it will be unlimited token allowance. * * @returns {BigNumber} Estimated gas */ export const estimateApprove = async ({ provider, contractAddress, spenderAddress, fromAddress, abi, amount, }: { provider: providers.Provider; contractAddress: Address; spenderAddress: Address; fromAddress: Address; abi: ethers.ContractInterface; amount?: BaseAmount; }): Promise<ethers.BigNumber> => { const txAmount = getApprovalAmount(amount); return await estimateCall({ provider, contractAddress, abi, funcName: "approve", funcParams: [spenderAddress, txAmount, { from: fromAddress }], }); }; /** * Get Decimals * * @param {Asset} asset * @returns {Number} the decimal of a given asset * * @throws {"Invalid asset"} Thrown if the given asset is invalid */ export const getDecimal = async ( asset: Asset, provider: providers.Provider ): Promise<number> => { if (assetToString(asset) === assetToString(AssetETH)) return ETH_DECIMAL; const assetAddress = getTokenAddress(asset); if (!assetAddress) throw new Error(`Invalid asset ${assetToString(asset)}`); const contract: ethers.Contract = new ethers.Contract( assetAddress, erc20ABI, provider ); const decimal: ethers.BigNumberish = await contract.decimals(); return ethers.BigNumber.from(decimal).toNumber(); }; /** * Check allowance. * * @param {Provider} provider 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 {boolean} `true` or `false`. */ export const isApproved = async ({ provider, contractAddress, spenderAddress, fromAddress, amount, }: { provider: providers.Provider; contractAddress: Address; spenderAddress: Address; fromAddress: Address; amount?: BaseAmount; }): Promise<boolean> => { const txAmount = ethers.BigNumber.from(amount?.amount().toFixed() ?? 1); const contract: ethers.Contract = new ethers.Contract( contractAddress, erc20ABI, provider ); const allowance: ethers.BigNumberish = await contract.allowance( fromAddress, spenderAddress ); return txAmount.lte(allowance); }; /** * Get Token Balances * * @param {TokenBalance[]} tokenBalances * @returns {Balance[]} the parsed balances * */ export const getTokenBalances = (tokenBalances: TokenBalance[]): Balance[] => { return tokenBalances.reduce((acc, cur) => { const { symbol, address: tokenAddress } = cur.tokenInfo; if ( validateSymbol(symbol) && validateAddress(tokenAddress) && cur?.tokenInfo?.decimals !== undefined ) { const decimals = parseInt(cur.tokenInfo.decimals, 10); const tokenAsset = assetFromString( `${EthChain.ticker}.${symbol}-${ethers.utils.getAddress(tokenAddress)}` ); if (tokenAsset) { return [ ...acc, { asset: tokenAsset, amount: baseAmount(cur.balance, decimals), }, ]; } } return acc; }, [] as Balance[]); };