UNPKG

edge-core-js

Version:

Edge account & wallet management library

245 lines (209 loc) 7.22 kB
function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }import { asMaybe, asObject, asString } from 'cleaners' import { makeJsonFile } from '../../util/file-helpers' import { getCurrencyTools, maybeFindCurrencyPluginId } from '../plugins/plugins-selectors' import { getStorageWalletDisklet } from '../storage/storage-selectors' import { asCustomTokensFile, asGuiSettingsFile } from './account-cleaners' const customTokensFile = makeJsonFile(asCustomTokensFile) const guiSettingsFile = makeJsonFile(asGuiSettingsFile) const CUSTOM_TOKENS_FILE = 'CustomTokens.json' const GUI_SETTINGS_FILE = 'Settings.json' /** * The `networkLocation` field is untyped, * but many currency plugins will put a contract address in there. */ const asMaybeContractLocation = asMaybe( asObject({ contractAddress: asString }) ) /** * We need to validate the token before we can add it. * * If the plugin has a `getTokenId` method, just use that. * * Otherwise, we need to call `EdgeCurrencyEngine.addCustomToken` * to validate the contract address, and then guess the tokenId from that. */ export async function getTokenId( ai, pluginId, token ) { // The normal code path: const tools = await getCurrencyTools(ai, pluginId) if (tools.getTokenId != null) { return await tools.getTokenId(token) } // Find an engine (any engine) to validate our token: const engine = findEngine(ai, pluginId) if (engine == null) { throw new Error( 'A wallet must exist before adding tokens to a legacy currency plugin' ) } if (engine.addCustomToken == null) { throw new Error(`${pluginId} doesn't support tokens`) } // Validate the token: const tokenInfo = makeTokenInfo(token) if (tokenInfo == null) { throw new Error( 'A token must have a contract address to be added to a legacy currency plugin' ) } await engine.addCustomToken({ ...tokenInfo, ...token }) return contractToTokenId(tokenInfo.contractAddress) } function contractToTokenId(contractAddress) { return contractAddress.toLowerCase().replace(/^0x/, '') } function upgradeMetaTokens(metaTokens) { const out = {} for (const metaToken of metaTokens) { const { contractAddress } = metaToken if (contractAddress == null) continue out[contractToTokenId(contractAddress)] = { currencyCode: metaToken.currencyCode, denominations: metaToken.denominations, displayName: metaToken.currencyName, networkLocation: { contractAddress: metaToken.contractAddress } } } return out } export function makeMetaToken(token) { const { currencyCode, displayName, denominations, networkLocation } = token const cleanLocation = asMaybeContractLocation(networkLocation) return { currencyCode, currencyName: displayName, denominations, contractAddress: _optionalChain([cleanLocation, 'optionalAccess', _ => _.contractAddress]) } } export function makeMetaTokens(tokens = {}) { const out = [] for (const tokenId of Object.keys(tokens)) { out.push(makeMetaToken(tokens[tokenId])) } return out } export function makeTokenInfo(token) { const { currencyCode, displayName, denominations, networkLocation } = token const cleanLocation = asMaybeContractLocation(networkLocation) if (cleanLocation == null) return return { currencyCode, currencyName: displayName, multiplier: denominations[0].multiplier, contractAddress: cleanLocation.contractAddress } } export async function loadBuiltinTokens( ai, accountId ) { const { dispatch, state } = ai.props // Load builtin tokens: await Promise.all( Object.keys(state.plugins.currency).map(async pluginId => { const plugin = state.plugins.currency[pluginId] const tokens = plugin.getBuiltinTokens == null ? upgradeMetaTokens(_nullishCoalesce(plugin.currencyInfo.metaTokens, () => ( []))) : await plugin.getBuiltinTokens() dispatch({ type: 'ACCOUNT_BUILTIN_TOKENS_LOADED', payload: { accountId, pluginId, tokens } }) }) ) } export function findEngine( ai, pluginId ) { for (const walletId of Object.keys(ai.props.state.currency.wallets)) { const walletOutput = ai.props.output.currency.wallets[walletId] if ( _optionalChain([walletOutput, 'optionalAccess', _2 => _2.engine]) != null && ai.props.state.currency.wallets[walletId].pluginId === pluginId ) { return walletOutput.engine } } } async function loadGuiTokens( ai, accountId ) { const { state } = ai.props const { accountWalletInfo } = state.accounts[accountId] const disklet = getStorageWalletDisklet(state, accountWalletInfo.id) const file = await guiSettingsFile.load(disklet, GUI_SETTINGS_FILE) if (file == null) return {} const out = {} for (const guiToken of file.customTokens) { if (!guiToken.isVisible) continue // Find the plugin: const pluginId = maybeFindCurrencyPluginId( state.plugins.currency, guiToken.walletType ) if (pluginId == null) continue if (out[pluginId] == null) out[pluginId] = {} // Add it to the list: const tokenId = contractToTokenId(guiToken.contractAddress) out[pluginId][tokenId] = { currencyCode: guiToken.currencyCode, denominations: guiToken.denominations, displayName: guiToken.currencyName, networkLocation: { contractAddress: guiToken.contractAddress } } } return out } export async function loadCustomTokens( ai, accountId ) { const { dispatch, state } = ai.props const { accountWalletInfo } = state.accounts[accountId] const disklet = getStorageWalletDisklet(state, accountWalletInfo.id) // Load the file: const file = await customTokensFile.load(disklet, CUSTOM_TOKENS_FILE) if (file != null) { const { customTokens } = file dispatch({ type: 'ACCOUNT_CUSTOM_TOKENS_LOADED', payload: { accountId, customTokens } }) } else { // Fall back on the legacy file: const customTokens = await loadGuiTokens(ai, accountId) dispatch({ type: 'ACCOUNT_CUSTOM_TOKENS_LOADED', payload: { accountId, customTokens } }) } } export async function saveCustomTokens( ai, accountId ) { const { state } = ai.props const { accountWalletInfo } = state.accounts[accountId] const disklet = getStorageWalletDisklet(state, accountWalletInfo.id) const { customTokens } = ai.props.state.accounts[accountId] // Refresh the file: const file = await customTokensFile.load(disklet, CUSTOM_TOKENS_FILE) await customTokensFile.save(disklet, CUSTOM_TOKENS_FILE, { ...file, customTokens }) }