import log from 'loglevel'; import BigNumber from 'bignumber.js'; import contractMap from '@metamask/contract-metadata'; import * as util from './util'; import { conversionUtil, multiplyCurrencies } from './conversion-util'; import { formatCurrency } from './confirm-tx.util'; const casedContractMap = Object.keys(contractMap).reduce((acc, base) => { return { ...acc, [base.toLowerCase()]: contractMap[base], }; }, {}); const DEFAULT_SYMBOL = ''; const DEFAULT_DECIMALS = '0'; async function getSymbolFromContract(tokenAddress) { const token = util.getContractAtAddress(tokenAddress); try { const result = await token.symbol(); return result[0]; } catch (error) { log.warn( `symbol() call for token at address ${tokenAddress} resulted in error:`, error, ); return undefined; } } async function getDecimalsFromContract(tokenAddress) { const token = util.getContractAtAddress(tokenAddress); try { const result = await token.decimals(); const decimalsBN = result[0]; return decimalsBN?.toString(); } catch (error) { log.warn( `decimals() call for token at address ${tokenAddress} resulted in error:`, error, ); return undefined; } } function getContractMetadata(tokenAddress) { return tokenAddress && casedContractMap[tokenAddress.toLowerCase()]; } async function getSymbol(tokenAddress) { let symbol = await getSymbolFromContract(tokenAddress); if (!symbol) { const contractMetadataInfo = getContractMetadata(tokenAddress); if (contractMetadataInfo) { symbol = contractMetadataInfo.symbol; } } return symbol; } async function getDecimals(tokenAddress) { let decimals = await getDecimalsFromContract(tokenAddress); if (!decimals || decimals === '0') { const contractMetadataInfo = getContractMetadata(tokenAddress); if (contractMetadataInfo) { decimals = contractMetadataInfo.decimals; } } return decimals; } export async function fetchSymbolAndDecimals(tokenAddress) { let symbol, decimals; try { symbol = await getSymbol(tokenAddress); decimals = await getDecimals(tokenAddress); } catch (error) { log.warn( `symbol() and decimal() calls for token at address ${tokenAddress} resulted in error:`, error, ); } return { symbol: symbol || DEFAULT_SYMBOL, decimals: decimals || DEFAULT_DECIMALS, }; } export async function getSymbolAndDecimals(tokenAddress, existingTokens = []) { const existingToken = existingTokens.find( ({ address }) => tokenAddress === address, ); if (existingToken) { return { symbol: existingToken.symbol, decimals: existingToken.decimals, }; } let symbol, decimals; try { symbol = await getSymbol(tokenAddress); decimals = await getDecimals(tokenAddress); } catch (error) { log.warn( `symbol() and decimal() calls for token at address ${tokenAddress} resulted in error:`, error, ); } return { symbol: symbol || DEFAULT_SYMBOL, decimals: decimals || DEFAULT_DECIMALS, }; } export function tokenInfoGetter() { const tokens = {}; return async (address) => { if (tokens[address]) { return tokens[address]; } tokens[address] = await getSymbolAndDecimals(address); return tokens[address]; }; } export function calcTokenAmount(value, decimals) { const multiplier = Math.pow(10, Number(decimals || 0)); return new BigNumber(String(value)).div(multiplier); } export function calcTokenValue(value, decimals) { const multiplier = Math.pow(10, Number(decimals || 0)); return new BigNumber(String(value)).times(multiplier); } /** * Attempts to get the address parameter of the given token transaction data * (i.e. function call) per the Human Standard Token ABI, in the following * order: * - The '_to' parameter, if present * - The first parameter, if present * * @param {Object} tokenData - ethers Interface token data. * @returns {string | undefined} A lowercase address string. */ export function getTokenAddressParam(tokenData = {}) { const value = tokenData?.args?._to || tokenData?.args?.[0]; return value?.toString().toLowerCase(); } /** * Gets the '_value' parameter of the given token transaction data * (i.e function call) per the Human Standard Token ABI, if present. * * @param {Object} tokenData - ethers Interface token data. * @returns {string | undefined} A decimal string value. */ export function getTokenValueParam(tokenData = {}) { return tokenData?.args?._value?.toString(); } export function getTokenValue(tokenParams = []) { const valueData = tokenParams.find((param) => param.name === '_value'); return valueData && valueData.value; } /** * Get the token balance converted to fiat and optionally formatted for display * * @param {number} [contractExchangeRate] - The exchange rate between the current token and the native currency * @param {number} conversionRate - The exchange rate between the current fiat currency and the native currency * @param {string} currentCurrency - The currency code for the user's chosen fiat currency * @param {string} [tokenAmount] - The current token balance * @param {string} [tokenSymbol] - The token symbol * @param {boolean} [formatted] - Whether the return value should be formatted or not * @param {boolean} [hideCurrencySymbol] - excludes the currency symbol in the result if true * @returns {string|undefined} The token amount in the user's chosen fiat currency, optionally formatted and localize */ export function getTokenFiatAmount( contractExchangeRate, conversionRate, currentCurrency, tokenAmount, tokenSymbol, formatted = true, hideCurrencySymbol = false, ) { // If the conversionRate is 0 (i.e. unknown) or the contract exchange rate // is currently unknown, the fiat amount cannot be calculated so it is not // shown to the user if ( conversionRate <= 0 || !contractExchangeRate || tokenAmount === undefined ) { return undefined; } const currentTokenToFiatRate = multiplyCurrencies( contractExchangeRate, conversionRate, { multiplicandBase: 10, multiplierBase: 10, }, ); const currentTokenInFiat = conversionUtil(tokenAmount, { fromNumericBase: 'dec', fromCurrency: tokenSymbol, toCurrency: currentCurrency.toUpperCase(), numberOfDecimals: 2, conversionRate: currentTokenToFiatRate, }); let result; if (hideCurrencySymbol) { result = formatCurrency(currentTokenInFiat, currentCurrency); } else if (formatted) { result = `${formatCurrency( currentTokenInFiat, currentCurrency, )} ${currentCurrency.toUpperCase()}`; } else { result = currentTokenInFiat; } return result; }