edge-core-js
Version:
Edge account & wallet management library
245 lines (209 loc) • 7.22 kB
JavaScript
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
})
}