UNPKG

edge-core-js

Version:

Edge account & wallet management library

847 lines (732 loc) 25.1 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 { abs, div, lt, mul } from 'biggystring' import { base64 } from 'rfc4648' import { bridgifyObject, emit, onMethod, watchMethod } from 'yaob' import { streamTransactions } from '../../../client-side' import { upgradeCurrencyCode, upgradeTxNetworkFees } from '../../../types/type-helpers' import { makeMetaTokens } from '../../account/custom-tokens' import { toApiInput } from '../../root-pixie' import { makeStorageWalletApi } from '../../storage/storage-api' import { getCurrencyMultiplier } from '../currency-selectors' import { makeCurrencyWalletCallbacks } from './currency-wallet-callbacks' import { asEdgeAssetAction, asEdgeTxAction, asEdgeTxSwap, } from './currency-wallet-cleaners' import { dateFilter, searchStringFilter } from './currency-wallet-export' import { loadTxFiles, renameCurrencyWallet, saveTxMetadataFile, setCurrencyWalletFiat, setupNewTxMetadata, updateCurrencyWalletTxMetadata } from './currency-wallet-files' import { uniqueStrings } from './enabled-tokens' import { getMaxSpendableInner } from './max-spend' import { mergeMetadata } from './metadata' import { upgradeMemos } from './upgrade-memos' const fakeMetadata = { bizId: 0, category: '', exchangeAmount: {}, name: '', notes: '' } // The EdgeTransaction.spendTargets type, but non-null: /** * Creates an `EdgeCurrencyWallet` API object. */ export function makeCurrencyWalletApi( input, engine, tools, publicWalletInfo ) { const ai = toApiInput(input) const { accountId, pluginId, walletInfo } = input.props.walletState const plugin = input.props.state.plugins.currency[pluginId] const { unsafeBroadcastTx = false, unsafeMakeSpend = false } = plugin.currencyInfo const storageWalletApi = makeStorageWalletApi(ai, walletInfo) const fakeCallbacks = makeCurrencyWalletCallbacks(input) const otherMethods = {} if (engine.otherMethods != null) { for (const name of Object.keys(engine.otherMethods)) { const method = engine.otherMethods[name] if (typeof method !== 'function') continue otherMethods[name] = method } } if (engine.otherMethodsWithKeys != null) { for (const name of Object.keys(engine.otherMethodsWithKeys)) { const method = engine.otherMethodsWithKeys[name] if (typeof method !== 'function') continue otherMethods[name] = (...args) => method(walletInfo.keys, ...args) } } bridgifyObject(otherMethods) const out = { on: onMethod, watch: watchMethod, // Data store: get created() { return walletInfo.created }, get disklet() { return storageWalletApi.disklet }, get id() { return storageWalletApi.id }, get localDisklet() { return storageWalletApi.localDisklet }, publicWalletInfo, async sync() { await storageWalletApi.sync() }, get type() { return storageWalletApi.type }, // Wallet name: get name() { return input.props.walletState.name }, async renameWallet(name) { await renameCurrencyWallet(input, name) }, // Fiat currency option: get fiatCurrencyCode() { return input.props.walletState.fiat }, async setFiatCurrencyCode(fiatCurrencyCode) { await setCurrencyWalletFiat(input, fiatCurrencyCode) }, // Currency info: get currencyConfig() { const { accountApi } = input.props.output.accounts[accountId] return accountApi.currencyConfig[pluginId] }, get currencyInfo() { return plugin.currencyInfo }, async denominationToNative( denominatedAmount, currencyCode ) { const multiplier = getCurrencyMultiplier( plugin.currencyInfo, input.props.state.accounts[accountId].allTokens[pluginId], currencyCode ) return mul(denominatedAmount, multiplier) }, async nativeToDenomination( nativeAmount, currencyCode ) { const multiplier = getCurrencyMultiplier( plugin.currencyInfo, input.props.state.accounts[accountId].allTokens[pluginId], currencyCode ) return div(nativeAmount, multiplier, multiplier.length) }, // Chain state: get balances() { return input.props.walletState.balances }, get balanceMap() { return input.props.walletState.balanceMap }, get blockHeight() { const { skipBlockHeight } = input.props.state return skipBlockHeight ? 0 : input.props.walletState.height }, get syncRatio() { return input.props.walletState.syncRatio }, get unactivatedTokenIds() { return input.props.walletState.unactivatedTokenIds }, // Running state: async changePaused(paused) { input.props.dispatch({ type: 'CURRENCY_WALLET_CHANGED_PAUSED', payload: { walletId: input.props.walletId, paused } }) }, get paused() { return input.props.walletState.paused }, // Tokens: async changeEnabledTokenIds(tokenIds) { const { dispatch, walletId, walletState } = input.props const { accountId, pluginId } = walletState const accountState = input.props.state.accounts[accountId] const allTokens = _nullishCoalesce(accountState.allTokens[pluginId], () => ( {})) const enabledTokenIds = uniqueStrings(tokenIds).filter( tokenId => allTokens[tokenId] != null ) const shortId = walletId.slice(0, 2) input.props.log.warn(`enabledTokenIds: ${shortId} changeEnabledTokenIds`) dispatch({ type: 'CURRENCY_WALLET_ENABLED_TOKENS_CHANGED', payload: { walletId, enabledTokenIds } }) }, get detectedTokenIds() { return input.props.walletState.detectedTokenIds }, get enabledTokenIds() { return input.props.walletState.enabledTokenIds }, // Transactions history: async getNumTransactions(opts) { const upgradedCurrency = upgradeCurrencyCode({ allTokens: input.props.state.accounts[accountId].allTokens[pluginId], currencyInfo: plugin.currencyInfo, tokenId: opts.tokenId }) return engine.getNumTransactions(upgradedCurrency) }, async $internalStreamTransactions( opts ) { const { afterDate, batchSize = 10, beforeDate, firstBatchSize = batchSize, searchString, spamThreshold = '0', tokenId = null } = opts const { currencyCode } = tokenId == null ? this.currencyInfo : this.currencyConfig.allTokens[tokenId] const upgradedCurrency = { currencyCode, tokenId } // Load transactions from the engine if necessary: let state = input.props.walletState if (!state.gotTxs.has(tokenId)) { const txs = await engine.getTransactions(upgradedCurrency) fakeCallbacks.onTransactionsChanged(txs) input.props.dispatch({ type: 'CURRENCY_ENGINE_GOT_TXS', payload: { walletId: input.props.walletId, tokenId } }) state = input.props.walletState } const { // All the files we have loaded from disk: files, // All the txid hashes we know about from either the engine or disk, // sorted using the lowest available date. // Some may not exist on disk, and some may not exist on chain: sortedTxidHashes, // Maps from txid hashes to original txids: txidHashes, // All the transactions we have from the engine: txs } = state let i = 0 let isFirst = true let lastFile = 0 return bridgifyObject({ async next() { const thisBatchSize = isFirst ? firstBatchSize : batchSize const out = [] while (i < sortedTxidHashes.length && out.length < thisBatchSize) { // Load a batch of files if we need that: if (i >= lastFile) { const missingTxIdHashes = sortedTxidHashes .slice(lastFile, lastFile + thisBatchSize) .filter(txidHash => files[txidHash] == null) const missingFiles = await loadTxFiles(input, missingTxIdHashes) Object.assign(files, missingFiles) lastFile = lastFile + thisBatchSize } const txidHash = sortedTxidHashes[i++] const file = files[txidHash] const txid = _nullishCoalesce(_optionalChain([file, 'optionalAccess', _ => _.txid]), () => ( _optionalChain([txidHashes, 'access', _2 => _2[txidHash], 'optionalAccess', _3 => _3.txid]))) if (txid == null) continue const tx = txs[txid] // Filter transactions with missing amounts (nativeAmount/networkFee) const nativeAmount = _optionalChain([tx, 'optionalAccess', _4 => _4.nativeAmount, 'access', _5 => _5.get, 'call', _6 => _6(tokenId)]) const networkFee = _optionalChain([tx, 'optionalAccess', _7 => _7.networkFee, 'access', _8 => _8.get, 'call', _9 => _9(tokenId)]) if (tx == null || nativeAmount == null || networkFee == null) { continue } // Filter transactions based on search criteria: const edgeTx = combineTxWithFile(input, tx, file, tokenId) upgradeTxNetworkFees(edgeTx) if (!searchStringFilter(ai, edgeTx, searchString)) continue if (!dateFilter(edgeTx, afterDate, beforeDate)) continue const isKnown = tx.isSend || edgeTx.assetAction != null || edgeTx.chainAction != null || edgeTx.chainAssetAction != null || edgeTx.savedAction != null if (!isKnown && lt(abs(nativeAmount), spamThreshold)) continue out.push(edgeTx) } isFirst = false return { done: out.length === 0, value: out } } }) }, async getTransactions( opts ) { const { endDate: beforeDate, startDate: afterDate, searchString, spamThreshold } = opts const upgradedCurrency = upgradeCurrencyCode({ allTokens: input.props.state.accounts[accountId].allTokens[pluginId], currencyInfo: plugin.currencyInfo, tokenId: opts.tokenId }) const stream = await out.$internalStreamTransactions({ ...upgradedCurrency, afterDate, beforeDate, searchString, spamThreshold }) // We have no length, so iterate to get everything: const txs = [] while (true) { const batch = await stream.next() if (batch.done) return txs txs.push(...batch.value) } }, streamTransactions, // Addresses: async getAddresses( opts ) { if (engine.getAddresses != null) { return await engine.getAddresses(opts) } else { const upgradedCurrency = upgradeCurrencyCode({ allTokens: input.props.state.accounts[accountId].allTokens[pluginId], currencyInfo: plugin.currencyInfo, tokenId: opts.tokenId }) const freshAddress = await engine.getFreshAddress({ ...upgradedCurrency, forceIndex: opts.forceIndex }) const { publicAddress, legacyAddress, segwitAddress, nativeBalance, legacyNativeBalance, segwitNativeBalance } = freshAddress const addresses = [ { addressType: 'publicAddress', publicAddress, nativeBalance } ] if (segwitAddress != null) { addresses.unshift({ addressType: 'segwitAddress', publicAddress: segwitAddress, nativeBalance: segwitNativeBalance }) } if (legacyAddress != null) { addresses.push({ addressType: 'legacyAddress', publicAddress: legacyAddress, nativeBalance: legacyNativeBalance }) } return addresses } }, async getReceiveAddress( opts ) { const addresses = await this.getAddresses(opts) if (addresses.length < 1) throw new Error('No addresses available') const primaryAddress = _nullishCoalesce(addresses.find(address => { return address.addressType === 'publicAddress' }), () => ( addresses[0])) const receiveAddress = { publicAddress: primaryAddress.publicAddress, nativeBalance: primaryAddress.nativeBalance, metadata: fakeMetadata, nativeAmount: '0' } const segwitAddress = addresses.find(address => { return address.addressType === 'segwitAddress' }) if (segwitAddress != null) { receiveAddress.segwitAddress = segwitAddress.publicAddress receiveAddress.segwitNativeBalance = segwitAddress.nativeBalance } const legacyAddress = addresses.find(address => { return address.addressType === 'legacyAddress' }) if (legacyAddress != null) { receiveAddress.legacyAddress = legacyAddress.publicAddress receiveAddress.legacyNativeBalance = legacyAddress.nativeBalance } return receiveAddress }, async lockReceiveAddress( receiveAddress ) { // TODO: Address metadata }, async saveReceiveAddress( receiveAddress ) { // TODO: Address metadata }, // Sending: async broadcastTx(tx) { // Only provide wallet info if currency requires it: const privateKeys = unsafeBroadcastTx ? walletInfo.keys : undefined return await engine.broadcastTx(tx, { privateKeys }) }, async getMaxSpendable(spendInfo) { return await getMaxSpendableInner( spendInfo, plugin, engine, input.props.state.accounts[accountId].allTokens[pluginId], walletInfo ) }, async getPaymentProtocolInfo( paymentProtocolUrl ) { if (engine.getPaymentProtocolInfo == null) { throw new Error( "'getPaymentProtocolInfo' is not implemented on wallets of this type" ) } return await engine.getPaymentProtocolInfo(paymentProtocolUrl) }, async makeSpend(spendInfo) { spendInfo = upgradeMemos(spendInfo, plugin.currencyInfo) const { assetAction, customNetworkFee, enableRbf, memos, metadata, networkFeeOption = 'standard', noUnconfirmed = false, otherParams, pendingTxs, rbfTxid, savedAction, skipChecks, spendTargets = [], swapData } = spendInfo // Figure out which asset this is: const upgradedCurrency = upgradeCurrencyCode({ allTokens: input.props.state.accounts[accountId].allTokens[pluginId], currencyInfo: plugin.currencyInfo, tokenId: spendInfo.tokenId }) // Check the spend targets: const cleanTargets = [] const savedTargets = [] for (const target of spendTargets) { const { memo, publicAddress, nativeAmount = '0', otherParams = {} } = target if (publicAddress == null) continue cleanTargets.push({ memo, nativeAmount, otherParams, publicAddress, uniqueIdentifier: memo }) savedTargets.push({ currencyCode: upgradedCurrency.currencyCode, memo, nativeAmount, publicAddress, uniqueIdentifier: memo }) } if (spendInfo.privateKeys != null) { throw new TypeError('Only sweepPrivateKeys takes private keys') } // Only provide wallet info if currency requires it: const privateKeys = unsafeMakeSpend ? walletInfo.keys : undefined const tx = await engine.makeSpend( { ...upgradedCurrency, customNetworkFee, enableRbf, memos, metadata, networkFeeOption, noUnconfirmed, otherParams, pendingTxs, rbfTxid, skipChecks, spendTargets: cleanTargets }, { privateKeys } ) upgradeTxNetworkFees(tx) tx.networkFeeOption = networkFeeOption tx.requestedCustomFee = customNetworkFee tx.spendTargets = savedTargets tx.currencyCode = upgradedCurrency.currencyCode tx.tokenId = upgradedCurrency.tokenId if (metadata != null) tx.metadata = metadata if (swapData != null) tx.swapData = asEdgeTxSwap(swapData) if (savedAction != null) tx.savedAction = asEdgeTxAction(savedAction) if (assetAction != null) tx.assetAction = asEdgeAssetAction(assetAction) if (input.props.state.login.deviceDescription != null) tx.deviceDescription = input.props.state.login.deviceDescription return tx }, async saveTx(transaction) { if (input.props.walletState.txs[transaction.txid] == null) { const { fileName, txFile } = await setupNewTxMetadata( input, transaction ) await saveTxMetadataFile(input, fileName, txFile) fakeCallbacks.onTransactions([{ isNew: true, transaction }]) } else { await updateCurrencyWalletTxMetadata( input, transaction.txid, transaction.tokenId, fakeCallbacks ) } await engine.saveTx(transaction) }, async saveTxAction(opts) { const { txid, tokenId, assetAction, savedAction } = opts await updateCurrencyWalletTxMetadata( input, txid, tokenId, fakeCallbacks, undefined, assetAction, savedAction ) }, async saveTxMetadata(opts) { const { txid, tokenId, metadata } = opts await updateCurrencyWalletTxMetadata( input, txid, tokenId, fakeCallbacks, metadata ) }, async signBytes( bytes, opts = {} ) { const privateKeys = walletInfo.keys if (engine.signBytes != null) { return await engine.signBytes(bytes, privateKeys, opts) } // Various plugins expect specific encodings for signing messages // (base16, base64, etc). // Do the conversion here temporarily if `signMessage` is implemented // while we migrate to `signBytes`. else if (pluginId === 'bitcoin' && engine.signMessage != null) { return await engine.signMessage( base64.stringify(bytes), privateKeys, opts ) } throw new Error(`${pluginId} doesn't support signBytes`) }, async signMessage( message, opts = {} ) { if (engine.signMessage == null) { throw new Error(`${pluginId} doesn't support signing messages`) } const privateKeys = walletInfo.keys return await engine.signMessage(message, privateKeys, opts) }, async signTx(tx) { const privateKeys = walletInfo.keys return await engine.signTx(tx, privateKeys) }, async sweepPrivateKeys(spendInfo) { if (engine.sweepPrivateKeys == null) { throw new Error('Sweeping this currency is not supported.') } return await engine.sweepPrivateKeys(spendInfo) }, // Accelerating: async accelerate(tx) { if (engine.accelerate == null) return null return await engine.accelerate(tx) }, // Staking: get stakingStatus() { return input.props.walletState.stakingStatus }, // Wallet management: async dumpData() { return await engine.dumpData() }, async resyncBlockchain() { const shortId = input.props.walletId.slice(0, 2) input.props.log.warn(`enabledTokenIds: ${shortId} resyncBlockchain`) ai.props.dispatch({ type: 'CURRENCY_ENGINE_CLEARED', payload: { walletId: input.props.walletId } }) await engine.resyncBlockchain() emit(out, 'transactionsRemoved', undefined) }, // URI handling: async encodeUri(options) { return await tools.encodeUri( options, makeMetaTokens( input.props.state.accounts[accountId].customTokens[pluginId] ) ) }, async parseUri(uri, currencyCode) { const parsedUri = await tools.parseUri( uri, currencyCode, makeMetaTokens( input.props.state.accounts[accountId].customTokens[pluginId] ) ) if (parsedUri.tokenId === undefined) { const { tokenId = null } = upgradeCurrencyCode({ allTokens: input.props.state.accounts[accountId].allTokens[pluginId], currencyInfo: plugin.currencyInfo, currencyCode: _nullishCoalesce(parsedUri.currencyCode, () => ( currencyCode)) }) parsedUri.tokenId = tokenId } return parsedUri }, // Generic: otherMethods } return bridgifyObject(out) } export function combineTxWithFile( input, tx, file, tokenId ) { const walletId = input.props.walletId const { accountId, currencyInfo, pluginId } = input.props.walletState const allTokens = input.props.state.accounts[accountId].allTokens[pluginId] const { currencyCode } = tokenId == null ? currencyInfo : allTokens[tokenId] const walletCurrency = currencyInfo.currencyCode // Copy the tx properties to the output: const out = { chainAction: tx.chainAction, chainAssetAction: tx.chainAssetAction.get(tokenId), blockHeight: tx.blockHeight, confirmations: tx.confirmations, currencyCode, feeRateUsed: tx.feeRateUsed, date: tx.date, isSend: tx.isSend, memos: tx.memos, metadata: {}, nativeAmount: _nullishCoalesce(tx.nativeAmount.get(tokenId), () => ( '0')), networkFee: _nullishCoalesce(tx.networkFee.get(tokenId), () => ( '0')), networkFees: [], otherParams: { ...tx.otherParams }, ourReceiveAddresses: tx.ourReceiveAddresses, parentNetworkFee: walletCurrency === currencyCode ? undefined : _nullishCoalesce(tx.networkFee.get(null), () => ( '0')), signedTx: tx.signedTx, tokenId, txid: tx.txid, walletId } // If we have a file, use it to override the defaults: if (file != null) { if (file.creationDate < out.date) out.date = file.creationDate out.metadata = mergeMetadata( _nullishCoalesce(_nullishCoalesce(_optionalChain([file, 'access', _10 => _10.tokens, 'access', _11 => _11.get, 'call', _12 => _12(null), 'optionalAccess', _13 => _13.metadata]), () => ( _optionalChain([file, 'access', _14 => _14.currencies, 'access', _15 => _15.get, 'call', _16 => _16(walletCurrency), 'optionalAccess', _17 => _17.metadata]))), () => ( {})), _nullishCoalesce(_nullishCoalesce(_optionalChain([file, 'access', _18 => _18.tokens, 'access', _19 => _19.get, 'call', _20 => _20(tokenId), 'optionalAccess', _21 => _21.metadata]), () => ( _optionalChain([file, 'access', _22 => _22.currencies, 'access', _23 => _23.get, 'call', _24 => _24(currencyCode), 'optionalAccess', _25 => _25.metadata]))), () => ( {})) ) if (file.feeRateRequested != null) { if (typeof file.feeRateRequested === 'string') { out.networkFeeOption = file.feeRateRequested } else { out.networkFeeOption = 'custom' out.requestedCustomFee = file.feeRateRequested } } if (out.feeRateUsed == null) { out.feeRateUsed = file.feeRateUsed } if (file.payees != null) { out.spendTargets = file.payees.map(payee => ({ currencyCode: payee.currency, memo: payee.tag, nativeAmount: payee.amount, publicAddress: payee.address, uniqueIdentifier: payee.tag })) } const assetAction = _optionalChain([file, 'access', _26 => _26.tokens, 'access', _27 => _27.get, 'call', _28 => _28(tokenId), 'optionalAccess', _29 => _29.assetAction]) if (assetAction != null) out.assetAction = assetAction if (file.savedAction != null) out.savedAction = file.savedAction if (file.swap != null) out.swapData = file.swap if (file.secret != null) out.txSecret = file.secret if (file.deviceDescription != null) out.deviceDescription = file.deviceDescription } return out }