edge-core-js
Version:
Edge account & wallet management library
847 lines (732 loc) • 25.1 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 { 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
}