diff --git a/CHANGELOG.md b/CHANGELOG.md index c66ea6f76..ecbfa97f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ ## Unreleased +- added: `EdgeCurrencyCodeOptions.tokenId`. This upgrades `getBalance`, `getNumTransactions`, and `getReceiveAddress`. +- added: `EdgeCurrencyEngineCallbacks.onTokenBalanceChanged`, which is thew new balance-update callback. +- added: `EdgeCurrencyWallet.balanceMap`, which reports balances by tokenId. +- added: `EdgeParsedUri.tokenId` +- added: `EdgeTokenId` type definition. +- added: `EdgeTransaction.tokenId`. +- added: Allow deleting metadata fields by passing `null` to `saveTxMetadata`. +- deprecated: `EdgeCurrencyEngineCallbacks.onBalanceChanged`. Use `onTokenBalanceChanged` instead. +- deprecated: `EdgeParsedUri.currencyCode`. Use `tokenId` instead. +- deprecated: `EdgeTransaction.currencyCode`. Use `tokenId` instead. + ## 1.13.1 (2023-12-06) - added: Extra cors servers to distribute load diff --git a/src/core/actions.ts b/src/core/actions.ts index 0916c3f09..0d8442dd2 100644 --- a/src/core/actions.ts +++ b/src/core/actions.ts @@ -7,6 +7,7 @@ import { EdgeRateHint, EdgeStakingStatus, EdgeToken, + EdgeTokenId, EdgeTokenMap, EdgeTransaction, EdgeWalletInfo, @@ -155,7 +156,7 @@ export type RootAction = type: 'CURRENCY_ENGINE_CHANGED_BALANCE' payload: { balance: string - currencyCode: string + tokenId: EdgeTokenId walletId: string } } @@ -213,7 +214,7 @@ export type RootAction = type: 'CURRENCY_ENGINE_GOT_TXS' payload: { walletId: string - currencyCode: string + tokenId: EdgeTokenId } } | { diff --git a/src/core/currency/currency-selectors.ts b/src/core/currency/currency-selectors.ts index 42de86f08..1fca70fd8 100644 --- a/src/core/currency/currency-selectors.ts +++ b/src/core/currency/currency-selectors.ts @@ -1,37 +1,23 @@ import { - EdgeCurrencyPlugin, + EdgeCurrencyInfo, EdgeCurrencyWallet, EdgeTokenMap } from '../../types/types' import { ApiInput, RootProps } from '../root-pixie' -const getFromTokenMap = ( - tokenMap: EdgeTokenMap, - currencyCode: string -): string | undefined => { - for (const tokenId of Object.keys(tokenMap)) { - const token = tokenMap[tokenId] - for (const denomination of token.denominations) { - if (denomination.name === currencyCode) { - return denomination.multiplier - } - } - } -} - export function getCurrencyMultiplier( - plugin: EdgeCurrencyPlugin, - allTokens: EdgeTokenMap = {}, + currencyInfo: EdgeCurrencyInfo, + allTokens: EdgeTokenMap, currencyCode: string ): string { - const info = plugin.currencyInfo - for (const denomination of info.denominations) { + for (const denomination of currencyInfo.denominations) { if (denomination.name === currencyCode) { return denomination.multiplier } } - for (const token of info.metaTokens) { + for (const tokenId of Object.keys(allTokens)) { + const token = allTokens[tokenId] for (const denomination of token.denominations) { if (denomination.name === currencyCode) { return denomination.multiplier @@ -39,8 +25,6 @@ export function getCurrencyMultiplier( } } - const multiplier = getFromTokenMap(allTokens, currencyCode) - if (multiplier != null) return multiplier return '1' } diff --git a/src/core/currency/wallet/currency-wallet-api.ts b/src/core/currency/wallet/currency-wallet-api.ts index 7f79f57e9..f79bed743 100644 --- a/src/core/currency/wallet/currency-wallet-api.ts +++ b/src/core/currency/wallet/currency-wallet-api.ts @@ -9,6 +9,7 @@ import { } from '../../../client-side' import { upgradeCurrencyCode } from '../../../types/type-helpers' import { + EdgeBalanceMap, EdgeBalances, EdgeCurrencyCodeOptions, EdgeCurrencyConfig, @@ -21,7 +22,7 @@ import { EdgeGetReceiveAddressOptions, EdgeGetTransactionsOptions, EdgeMemoRules, - EdgeMetadata, + EdgeMetadataChange, EdgeParsedUri, EdgePaymentProtocolInfo, EdgeReceiveAddress, @@ -30,21 +31,16 @@ import { EdgeSpendTarget, EdgeStakingStatus, EdgeStreamTransactionOptions, + EdgeTokenId, EdgeTransaction, EdgeWalletInfo } from '../../../types/types' -import { mergeDeeply } from '../../../util/util' 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 { - asEdgeTxSwap, - packMetadata, - TransactionFile, - unpackMetadata -} from './currency-wallet-cleaners' +import { asEdgeTxSwap, TransactionFile } from './currency-wallet-cleaners' import { dateFilter, searchStringFilter } from './currency-wallet-export' import { loadTxFiles, @@ -55,6 +51,7 @@ import { } from './currency-wallet-files' import { CurrencyWalletInput } from './currency-wallet-pixie' import { MergedTransaction } from './currency-wallet-reducer' +import { mergeMetadata, upgradeMetadata } from './metadata' import { upgradeMemos } from './upgrade-memos' const fakeMetadata = { @@ -143,9 +140,8 @@ export function makeCurrencyWalletApi( denominatedAmount: string, currencyCode: string ): Promise { - const plugin = input.props.state.plugins.currency[pluginId] const multiplier = getCurrencyMultiplier( - plugin, + plugin.currencyInfo, input.props.state.accounts[accountId].allTokens[pluginId], currencyCode ) @@ -155,9 +151,8 @@ export function makeCurrencyWalletApi( nativeAmount: string, currencyCode: string ): Promise { - const plugin = input.props.state.plugins.currency[pluginId] const multiplier = getCurrencyMultiplier( - plugin, + plugin.currencyInfo, input.props.state.accounts[accountId].allTokens[pluginId], currencyCode ) @@ -172,6 +167,9 @@ export function makeCurrencyWalletApi( get balances(): EdgeBalances { return input.props.walletState.balances }, + get balanceMap(): EdgeBalanceMap { + return input.props.walletState.balanceMap + }, get blockHeight(): number { const { skipBlockHeight } = input.props.state return skipBlockHeight ? 0 : input.props.walletState.height @@ -223,7 +221,14 @@ export function makeCurrencyWalletApi( async getNumTransactions( opts: EdgeCurrencyCodeOptions = {} ): Promise { - return engine.getNumTransactions(opts) + const { currencyCode, tokenId } = upgradeCurrencyCode({ + allTokens: input.props.state.accounts[accountId].allTokens[pluginId], + currencyInfo: plugin.currencyInfo, + currencyCode: opts.currencyCode, + tokenId: opts.tokenId + }) + + return engine.getNumTransactions({ currencyCode, tokenId }) }, async $internalStreamTransactions( @@ -235,7 +240,7 @@ export function makeCurrencyWalletApi( beforeDate, firstBatchSize = batchSize, searchString, - tokenId, + tokenId = null, unfilteredStart } = opts const { currencyCode } = @@ -245,14 +250,14 @@ export function makeCurrencyWalletApi( // Load transactions from the engine if necessary: let state = input.props.walletState - if (!state.gotTxs[currencyCode]) { + if (!state.gotTxs.has(tokenId)) { const txs = await engine.getTransactions({ currencyCode }) fakeCallbacks.onTransactionsChanged(txs) input.props.dispatch({ type: 'CURRENCY_ENGINE_GOT_TXS', payload: { walletId: input.props.walletId, - currencyCode + tokenId } }) state = input.props.walletState @@ -298,14 +303,13 @@ export function makeCurrencyWalletApi( // Filter transactions based on the currency code: if ( tx == null || - (tx.nativeAmount[currencyCode] == null && - tx.networkFee[currencyCode] == null) + !(tx.nativeAmount.has(tokenId) || tx.networkFee.has(tokenId)) ) { continue } // Filter transactions based on search criteria: - const edgeTx = combineTxWithFile(input, tx, file, currencyCode) + const edgeTx = combineTxWithFile(input, tx, file, tokenId) if (!searchStringFilter(ai, edgeTx, searchString)) continue if (!dateFilter(edgeTx, afterDate, beforeDate)) continue @@ -337,7 +341,8 @@ export function makeCurrencyWalletApi( const { tokenId } = upgradeCurrencyCode({ allTokens: input.props.state.accounts[accountId].allTokens[pluginId], currencyInfo: plugin.currencyInfo, - currencyCode + currencyCode, + tokenId: opts.tokenId }) const stream = await out.$internalStreamTransactions({ @@ -370,7 +375,18 @@ export function makeCurrencyWalletApi( async getReceiveAddress( opts: EdgeGetReceiveAddressOptions = {} ): Promise { - const freshAddress = await engine.getFreshAddress(opts) + const { currencyCode, tokenId } = upgradeCurrencyCode({ + allTokens: input.props.state.accounts[accountId].allTokens[pluginId], + currencyInfo: plugin.currencyInfo, + currencyCode: opts.currencyCode, + tokenId: opts.tokenId + }) + + const freshAddress = await engine.getFreshAddress({ + forceIndex: opts.forceIndex, + currencyCode, + tokenId + }) const receiveAddress: EdgeReceiveAddress = { ...freshAddress, nativeAmount: '0', @@ -404,8 +420,16 @@ export function makeCurrencyWalletApi( return await engine.getMaxSpendable(spendInfo, { privateKeys }) } - const { currencyCode, networkFeeOption, customNetworkFee } = spendInfo - const balance = engine.getBalance({ currencyCode }) + + // Figure out which asset this is: + const { networkFeeOption, customNetworkFee } = spendInfo + const { currencyCode, tokenId } = upgradeCurrencyCode({ + allTokens: input.props.state.accounts[accountId].allTokens[pluginId], + currencyInfo: plugin.currencyInfo, + currencyCode: spendInfo.currencyCode, + tokenId: spendInfo.tokenId + }) + const balance = engine.getBalance({ currencyCode, tokenId }) // Copy all the spend targets, setting the amounts to 0 // but keeping all other information so we can get accurate fees: @@ -431,6 +455,7 @@ export function makeCurrencyWalletApi( .makeSpend( { currencyCode, + tokenId, spendTargets, networkFeeOption, customNetworkFee @@ -544,19 +569,22 @@ export function makeCurrencyWalletApi( await engine.saveTx(tx) fakeCallbacks.onTransactionsChanged([tx]) }, + async saveTxMetadata( txid: string, currencyCode: string, - metadata: EdgeMetadata + metadata: EdgeMetadataChange ): Promise { + upgradeMetadata(input, metadata) await setCurrencyWalletTxMetadata( input, txid, currencyCode, - packMetadata(metadata, input.props.walletState.fiat), + metadata, fakeCallbacks ) }, + async signMessage( message: string, opts: EdgeSignMessageOptions = {} @@ -612,13 +640,23 @@ export function makeCurrencyWalletApi( ) }, async parseUri(uri: string, currencyCode?: string): Promise { - return await tools.parseUri( + 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: parsedUri.currencyCode ?? currencyCode + }) + parsedUri.tokenId = tokenId + } + return parsedUri }, // Generic: @@ -633,11 +671,14 @@ export function combineTxWithFile( input: CurrencyWalletInput, tx: MergedTransaction, file: TransactionFile | undefined, - currencyCode: string + tokenId: EdgeTokenId ): EdgeTransaction { const walletId = input.props.walletId - const walletCurrency = input.props.walletState.currencyInfo.currencyCode - const walletFiat = input.props.walletState.fiat + 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: EdgeTransaction = { @@ -649,12 +690,13 @@ export function combineTxWithFile( isSend: tx.isSend, memos: tx.memos, metadata: {}, - nativeAmount: tx.nativeAmount[currencyCode] ?? '0', - networkFee: tx.networkFee[currencyCode] ?? '0', + nativeAmount: tx.nativeAmount.get(tokenId) ?? '0', + networkFee: tx.networkFee.get(tokenId) ?? '0', otherParams: { ...tx.otherParams }, ourReceiveAddresses: tx.ourReceiveAddresses, - parentNetworkFee: tx.networkFee[walletCurrency], + parentNetworkFee: tx.networkFee.get(null) ?? '0', signedTx: tx.signedTx, + tokenId, txid: tx.txid, walletId } @@ -663,16 +705,17 @@ export function combineTxWithFile( if (file != null) { if (file.creationDate < out.date) out.date = file.creationDate - const merged = mergeDeeply( - file.currencies[walletCurrency], - file.currencies[currencyCode] + const metadata = mergeMetadata( + file.tokens.get(null)?.metadata ?? + file.currencies.get(walletCurrency)?.metadata ?? + {}, + file.tokens.get(tokenId)?.metadata ?? + file.currencies.get(currencyCode)?.metadata ?? + {} ) - if (merged.metadata != null) { - out.metadata = { - ...out.metadata, - ...unpackMetadata(merged.metadata, walletFiat) - } - } + metadata.amountFiat = + metadata.exchangeAmount?.[input.props.walletState.fiat] + out.metadata = metadata if (file.feeRateRequested != null) { if (typeof file.feeRateRequested === 'string') { diff --git a/src/core/currency/wallet/currency-wallet-callbacks.ts b/src/core/currency/wallet/currency-wallet-callbacks.ts index 0e92e8eaf..b399cd439 100644 --- a/src/core/currency/wallet/currency-wallet-callbacks.ts +++ b/src/core/currency/wallet/currency-wallet-callbacks.ts @@ -3,9 +3,11 @@ import { asMaybe } from 'cleaners' import { isPixieShutdownError } from 'redux-pixies' import { emit } from 'yaob' +import { upgradeCurrencyCode } from '../../../types/type-helpers' import { EdgeCurrencyEngineCallbacks, EdgeStakingStatus, + EdgeTokenId, EdgeTransaction } from '../../../types/types' import { compare } from '../../../util/compare' @@ -108,7 +110,7 @@ export function makeCurrencyWalletCallbacks( } ) - return { + const out: EdgeCurrencyEngineCallbacks = { onAddressesChecked(ratio: number) { pushUpdate({ id: walletId, @@ -169,22 +171,39 @@ export function makeCurrencyWalletCallbacks( }, onBalanceChanged(currencyCode: string, balance: string) { + const { accountId, currencyInfo, pluginId } = input.props.walletState + const allTokens = + input.props.state.accounts[accountId].allTokens[pluginId] + + if (currencyCode === currencyInfo.currencyCode) { + out.onTokenBalanceChanged(null, balance) + } else { + const { tokenId } = upgradeCurrencyCode({ + allTokens, + currencyInfo, + currencyCode + }) + if (tokenId != null) out.onTokenBalanceChanged(tokenId, balance) + } + }, + + onTokenBalanceChanged(tokenId: EdgeTokenId, balance: string) { const clean = asMaybe(asIntegerString)(balance) if (clean == null) { input.props.onError( new Error( - `Plugin sent bogus balance for ${currencyCode}: "${balance}"` + `Plugin sent bogus balance for ${String(tokenId)}: "${balance}"` ) ) return } pushUpdate({ - id: `${walletId}==${currencyCode}`, - action: 'onBalanceChanged', + id: `${walletId}==${tokenId}`, + action: 'onTokenBalanceChanged', updateFunc: () => { input.props.dispatch({ type: 'CURRENCY_ENGINE_CHANGED_BALANCE', - payload: { balance: clean, currencyCode, walletId } + payload: { balance: clean, tokenId, walletId } }) } }) @@ -225,7 +244,7 @@ export function makeCurrencyWalletCallbacks( input, reduxTx, files[txidHash], - reduxTx.currencyCode + null ) // Dispatch event to update the redux transaction object @@ -260,6 +279,10 @@ export function makeCurrencyWalletCallbacks( }, onTransactionsChanged(txs: EdgeTransaction[]) { + const { accountId, currencyInfo, pluginId } = input.props.walletState + const allTokens = + input.props.state.accounts[accountId].allTokens[pluginId] + // Sanity-check incoming transactions: if (txs == null) return for (const tx of txs) { @@ -278,12 +301,19 @@ export function makeCurrencyWalletCallbacks( } if (tx.isSend == null) tx.isSend = lt(tx.nativeAmount, '0') if (tx.memos == null) tx.memos = [] + if (tx.tokenId === undefined) { + const { tokenId } = upgradeCurrencyCode({ + allTokens, + currencyInfo, + currencyCode: tx.currencyCode + }) + tx.tokenId = tokenId ?? null + } } // Grab stuff from redux: const { state } = input.props const { fileNames, txs: reduxTxs } = input.props.walletState - const defaultCurrency = input.props.walletState.currencyInfo.currencyCode const txidHashes: TxidHashes = {} const changed: EdgeTransaction[] = [] @@ -307,7 +337,7 @@ export function makeCurrencyWalletCallbacks( } // Verify that something has changed: - const reduxTx = mergeTx(tx, defaultCurrency, reduxTxs[txid]) + const reduxTx = mergeTx(tx, reduxTxs[txid]) if (compare(reduxTx, reduxTxs[txid]) && tx.metadata == null) continue // Ensure the transaction has metadata: @@ -325,7 +355,7 @@ export function makeCurrencyWalletCallbacks( input, reduxTx, files[txidHash], - tx.currencyCode + tx.tokenId ) if (isNew) created.push(combinedTx) else if (files[txidHash] != null) changed.push(combinedTx) @@ -352,6 +382,7 @@ export function makeCurrencyWalletCallbacks( }, onTxidsChanged() {} } + return out } /** diff --git a/src/core/currency/wallet/currency-wallet-cleaners.ts b/src/core/currency/wallet/currency-wallet-cleaners.ts index 9e79a7ba9..7d060c4a4 100644 --- a/src/core/currency/wallet/currency-wallet-cleaners.ts +++ b/src/core/currency/wallet/currency-wallet-cleaners.ts @@ -11,19 +11,15 @@ import { Cleaner } from 'cleaners' -import { EdgeMetadata, EdgeTxSwap } from '../../../types/types' +import { EdgeMetadata, EdgeTokenId, EdgeTxSwap } from '../../../types/types' +import { asMap, asTokenIdMap } from '../../../util/asMap' import { asJsonObject } from '../../../util/file-helpers' +import { asEdgeMetadata } from './metadata' -/** - * The on-disk metadata format, - * which has a mandatory `exchangeAmount` table and no `amountFiat`. - */ -export interface DiskMetadata { - bizId?: number - category?: string - exchangeAmount: { [fiatCurrencyCode: string]: number } - name?: string - notes?: string +interface TransactionAsset { + metadata: EdgeMetadata + nativeAmount?: string + providerFeeSent?: string } /** @@ -33,13 +29,9 @@ export interface TransactionFile { txid: string internal: boolean creationDate: number - currencies: { - [currencyCode: string]: { - metadata: DiskMetadata - nativeAmount?: string - providerFeeSent?: string - } - } + currencies: Map + tokens: Map + deviceDescription?: string feeRateRequested?: 'high' | 'standard' | 'low' | object feeRateUsed?: object @@ -111,42 +103,6 @@ interface LegacyMapFile { // building-block cleaners // --------------------------------------------------------------------- -/** - * Turns user-provided metadata into its on-disk format. - */ -export function packMetadata( - raw: EdgeMetadata, - walletFiat: string -): DiskMetadata { - const clean = asDiskMetadata(raw) - - if (typeof raw.amountFiat === 'number') { - clean.exchangeAmount[walletFiat] = raw.amountFiat - } - - return clean -} - -/** - * Turns on-disk metadata into the user-facing format. - */ -export function unpackMetadata( - raw: DiskMetadata, - walletFiat: string -): EdgeMetadata { - const clean = asDiskMetadata(raw) - const { exchangeAmount } = clean - - // Delete corrupt amounts that exceed the Javascript number range: - for (const currency of Object.keys(exchangeAmount)) { - if (String(exchangeAmount[currency]).includes('e')) { - delete exchangeAmount[currency] - } - } - - return { ...clean, amountFiat: exchangeAmount[walletFiat] } -} - const asFeeRate: Cleaner<'high' | 'standard' | 'low'> = asValue( 'high', 'standard', @@ -173,14 +129,6 @@ export const asEdgeTxSwap = asObject({ refundAddress: asOptional(asString) }) -const asDiskMetadata = asObject({ - bizId: asOptional(asNumber), - category: asOptional(asString), - exchangeAmount: asOptional(asObject(asNumber), () => ({})), - name: asOptional(asString), - notes: asOptional(asString) -}) - export function asIntegerString(raw: unknown): string { const clean = asString(raw) if (!/^\d+$/.test(clean)) { @@ -211,17 +159,18 @@ export const asTokensFile = asObject({ detectedTokenIds: asArray(asString) }) +const asTransactionAsset = asObject({ + metadata: asEdgeMetadata, + nativeAmount: asOptional(asString), + providerFeeSent: asOptional(asString) +}) + export const asTransactionFile = asObject({ txid: asString, internal: asBoolean, creationDate: asNumber, - currencies: asObject( - asObject({ - metadata: asDiskMetadata, - nativeAmount: asOptional(asString), - providerFeeSent: asOptional(asString) - }) - ), + currencies: asMap(asTransactionAsset), + tokens: asOptional(asTokenIdMap(asTransactionAsset), () => new Map()), deviceDescription: asOptional(asString), feeRateRequested: asOptional(asEither(asFeeRate, asJsonObject)), feeRateUsed: asOptional(asJsonObject), diff --git a/src/core/currency/wallet/currency-wallet-files.ts b/src/core/currency/wallet/currency-wallet-files.ts index 44eba6535..d286132d6 100644 --- a/src/core/currency/wallet/currency-wallet-files.ts +++ b/src/core/currency/wallet/currency-wallet-files.ts @@ -1,12 +1,13 @@ import { number as currencyFromNumber } from 'currency-codes' import { Disklet, justFiles, navigateDisklet } from 'disklet' +import { upgradeCurrencyCode } from '../../../types/type-helpers' import { EdgeCurrencyEngineCallbacks, + EdgeMetadataChange, EdgeTransaction } from '../../../types/types' import { makeJsonFile } from '../../../util/file-helpers' -import { mergeDeeply } from '../../../util/util' import { fetchAppIdInfo } from '../../account/lobby-api' import { toApiInput } from '../../root-pixie' import { RootState } from '../../root-reducer' @@ -25,14 +26,13 @@ import { asTransactionFile, asWalletFiatFile, asWalletNameFile, - DiskMetadata, LegacyTransactionFile, - packMetadata, TransactionFile } from './currency-wallet-cleaners' import { CurrencyWalletInput } from './currency-wallet-pixie' import { TxFileNames } from './currency-wallet-reducer' import { currencyCodesToTokenIds } from './enabled-tokens' +import { mergeMetadata, upgradeMetadata } from './metadata' const CURRENCY_FILE = 'Currency.json' const LEGACY_MAP_FILE = 'fixedLegacyFileNames.json' @@ -76,13 +76,14 @@ function fixLegacyFile( ): TransactionFile { const out: TransactionFile = { creationDate: file.state.creationDate, - currencies: {}, + currencies: new Map(), + tokens: new Map(), internal: file.state.internal, txid: file.state.malleableTxId } const exchangeAmount: { [currencyCode: string]: number } = {} exchangeAmount[walletFiat] = file.meta.amountCurrency - out.currencies[walletCurrency] = { + out.currencies.set(walletCurrency, { metadata: { bizId: file.meta.bizId, category: file.meta.category, @@ -91,7 +92,7 @@ function fixLegacyFile( notes: file.meta.notes }, providerFeeSent: file.meta.amountFeeAirBitzSatoshi.toFixed() - } + }) return out } @@ -257,9 +258,8 @@ export async function loadTxFiles( input: CurrencyWalletInput, txIdHashes: string[] ): Promise<{ [txidHash: string]: TransactionFile }> { - const { walletId } = input.props + const { dispatch, walletId } = input.props const disklet = getStorageWalletDisklet(input.props.state, walletId) - const { dispatch } = input.props const walletCurrency = input.props.walletState.currencyInfo.currencyCode const fileNames = input.props.walletState.fileNames const walletFiat = input.props.walletState.fiat @@ -415,60 +415,68 @@ export async function setCurrencyWalletTxMetadata( input: CurrencyWalletInput, txid: string, currencyCode: string, - metadata: DiskMetadata, + metadataChange: EdgeMetadataChange, fakeCallbacks: EdgeCurrencyEngineCallbacks ): Promise { const { dispatch, state, walletId } = input.props const disklet = getStorageWalletDisklet(state, walletId) + // Upgrade the currency code: + const { accountId, currencyInfo, pluginId } = input.props.walletState + const allTokens = input.props.state.accounts[accountId].allTokens[pluginId] + const { tokenId = null } = upgradeCurrencyCode({ + allTokens, + currencyCode, + currencyInfo + }) + // Find the tx: const tx = input.props.walletState.txs[txid] if (tx == null) { throw new Error(`Setting metatdata for missing tx ${txid}`) } + // Find the old file: const files = input.props.walletState.files - // Get the txidHash for this txid - let oldTxidHash = '' - for (const hash of Object.keys(files)) { - if (files[hash].txid === txid) { - oldTxidHash = hash - break - } + const oldTxidHash = Object.keys(files).find(hash => files[hash].txid === txid) + const oldFile = oldTxidHash != null ? files[oldTxidHash] : undefined + + // Set up the new file: + const { creationDate = Math.min(tx.date, Date.now() / 1000) } = oldFile ?? {} + const newFile: TransactionFile = { + ...oldFile, + creationDate, + currencies: new Map(oldFile?.currencies ?? []), + internal: true, + tokens: new Map(oldFile?.tokens ?? []), + txid } - // Load the old file: - const oldFile = input.props.walletState.files[oldTxidHash] - const creationDate = - oldFile == null - ? Math.min(tx.date, Date.now() / 1000) - : oldFile.creationDate + // Migrate the asset data from currencyCode to tokenId: + const assetData = { + metadata: {}, + ...newFile.currencies.get(currencyCode), + ...newFile.tokens.get(tokenId) + } + newFile.tokens.set(tokenId, assetData) + newFile.currencies.delete(currencyCode) - // Set up the new file: + // Make the change: + assetData.metadata = mergeMetadata(assetData.metadata ?? {}, metadataChange) + + // Save the new file: const { fileName, txidHash } = getTxFileName( state, walletId, creationDate, txid ) - const newFile: TransactionFile = { - txid, - internal: false, - creationDate, - currencies: {} - } - newFile.currencies[currencyCode] = { - metadata - } - const json = mergeDeeply(oldFile, newFile) - - // Save the new file: dispatch({ type: 'CURRENCY_WALLET_FILE_CHANGED', - payload: { creationDate, fileName, json, txid, txidHash, walletId } + payload: { creationDate, fileName, json: newFile, txid, txidHash, walletId } }) - await transactionFile.save(disklet, 'transaction/' + fileName, json) - const callbackTx = combineTxWithFile(input, tx, json, currencyCode) + await transactionFile.save(disklet, 'transaction/' + fileName, newFile) + const callbackTx = combineTxWithFile(input, tx, newFile, tokenId) fakeCallbacks.onTransactionsChanged([callbackTx]) } @@ -479,31 +487,24 @@ export async function setupNewTxMetadata( input: CurrencyWalletInput, tx: EdgeTransaction ): Promise { - const { dispatch, walletState, state, walletId } = input.props - const { fiat = 'iso:USD' } = walletState - const { currencyCode, spendTargets, swapData, txid } = tx + const { dispatch, state, walletId } = input.props + const { spendTargets, swapData, tokenId, txid } = tx const disklet = getStorageWalletDisklet(state, walletId) const creationDate = Date.now() / 1000 - - // Calculate the exchange rate: - const nativeAmount = tx.nativeAmount - - // Set up metadata: - const metadata: DiskMetadata = - tx.metadata != null - ? packMetadata(tx.metadata, fiat) - : { exchangeAmount: {} } + const { metadata = {}, nativeAmount } = tx + upgradeMetadata(input, metadata) // Basic file template: const json: TransactionFile = { txid, internal: true, creationDate, - currencies: {}, + currencies: new Map(), + tokens: new Map(), swap: swapData } - json.currencies[currencyCode] = { metadata, nativeAmount } + json.tokens.set(tokenId, { metadata, nativeAmount }) // Set up the fee metadata: if (tx.networkFeeOption != null) { diff --git a/src/core/currency/wallet/currency-wallet-pixie.ts b/src/core/currency/wallet/currency-wallet-pixie.ts index 024160685..ac4dc900b 100644 --- a/src/core/currency/wallet/currency-wallet-pixie.ts +++ b/src/core/currency/wallet/currency-wallet-pixie.ts @@ -132,12 +132,12 @@ export const walletPixie: TamePixie = combinePixies({ // Grab initial state: const balance = asMaybe(asIntegerString)( - engine.getBalance({ currencyCode }) + engine.getBalance({ currencyCode, tokenId: undefined }) ) if (balance != null) { input.props.dispatch({ type: 'CURRENCY_ENGINE_CHANGED_BALANCE', - payload: { balance, currencyCode, walletId } + payload: { balance, tokenId: null, walletId } }) } const height = engine.getBlockHeight() diff --git a/src/core/currency/wallet/currency-wallet-reducer.ts b/src/core/currency/wallet/currency-wallet-reducer.ts index d3abc938a..f73ead719 100644 --- a/src/core/currency/wallet/currency-wallet-reducer.ts +++ b/src/core/currency/wallet/currency-wallet-reducer.ts @@ -2,10 +2,12 @@ import { lt } from 'biggystring' import { buildReducer, filterReducer, memoizeReducer } from 'redux-keto' import { + EdgeBalanceMap, EdgeBalances, EdgeCurrencyInfo, EdgeMemo, EdgeStakingStatus, + EdgeTokenId, EdgeTransaction, EdgeTxAction, EdgeWalletInfo, @@ -43,7 +45,6 @@ export interface MergedTransaction { action?: EdgeTxAction blockHeight: number confirmations: EdgeTransaction['confirmations'] - currencyCode: string date: number isSend: boolean memos: EdgeMemo[] @@ -51,9 +52,8 @@ export interface MergedTransaction { ourReceiveAddresses: string[] signedTx: string txid: string - - nativeAmount: { [currencyCode: string]: string } - networkFee: { [currencyCode: string]: string } + nativeAmount: EdgeBalanceMap + networkFee: EdgeBalanceMap } export interface CurrencyWalletState { @@ -63,6 +63,7 @@ export interface CurrencyWalletState { readonly paused: boolean readonly allEnabledTokenIds: string[] + readonly balanceMap: EdgeBalanceMap readonly balances: EdgeBalances readonly currencyInfo: EdgeCurrencyInfo readonly detectedTokenIds: string[] @@ -73,7 +74,7 @@ export interface CurrencyWalletState { readonly fiatLoaded: boolean readonly fileNames: TxFileNames readonly files: TxFileJsons - readonly gotTxs: { [currencyCode: string]: boolean } + readonly gotTxs: Set readonly height: number readonly name: string | null readonly nameLoaded: boolean @@ -253,15 +254,33 @@ const currencyWalletInner = buildReducer< return state }, - balances(state = {}, action): EdgeBalances { + balanceMap(state = new Map(), action): Map { if (action.type === 'CURRENCY_ENGINE_CHANGED_BALANCE') { - const out = { ...state } - out[action.payload.currencyCode] = action.payload.balance + const { balance, tokenId } = action.payload + const out = new Map(state) + out.set(tokenId, balance) return out } return state }, + balances: memoizeReducer( + next => next.self.balanceMap, + next => next.self.currencyInfo, + next => + next.root.accounts[next.self.accountId].allTokens[next.self.pluginId], + (balanceMap, currencyInfo, allTokens) => { + const out: EdgeBalances = {} + for (const tokenId of balanceMap.keys()) { + const balance = balanceMap.get(tokenId) + const { currencyCode } = + tokenId == null ? currencyInfo : allTokens[tokenId] + if (balance != null) out[currencyCode] = balance + } + return out + } + ), + height(state = 0, action): number { return action.type === 'CURRENCY_ENGINE_CHANGED_HEIGHT' ? action.payload.height @@ -323,10 +342,9 @@ const currencyWalletInner = buildReducer< } case 'CURRENCY_ENGINE_CHANGED_TXS': { const { txs } = action.payload - const defaultCurrency = next.self.currencyInfo.currencyCode const out = { ...state } for (const tx of txs) { - out[tx.txid] = mergeTx(tx, defaultCurrency, out[tx.txid]) + out[tx.txid] = mergeTx(tx, out[tx.txid]) } return out } @@ -348,14 +366,16 @@ const currencyWalletInner = buildReducer< return state }, - gotTxs(state = {}, action): { [currencyCode: string]: boolean } { + gotTxs(state = new Set(), action): Set { switch (action.type) { case 'CURRENCY_ENGINE_GOT_TXS': { - const { currencyCode } = action.payload - return { ...state, [currencyCode]: true } + const { tokenId } = action.payload + const out = new Set(state) + out.add(tokenId) + return out } case 'CURRENCY_ENGINE_CLEARED': - return {} + return new Set() default: return state } @@ -403,58 +423,33 @@ export const currencyWalletReducer = filterReducer< : { type: 'UPDATE_NEXT' } }) -const defaultTx: MergedTransaction = { - blockHeight: 0, - confirmations: 'unconfirmed', - currencyCode: '', - date: 0, - isSend: false, - memos: [], - ourReceiveAddresses: [], - signedTx: '', - txid: '', - nativeAmount: {}, - networkFee: {} -} - /** * Merges a new incoming transaction with an existing transaction. */ export function mergeTx( tx: EdgeTransaction, - defaultCurrency: string, - oldTx: MergedTransaction = defaultTx + oldTx: MergedTransaction | undefined ): MergedTransaction { - const { - action, - currencyCode = defaultCurrency, - isSend = lt(tx.nativeAmount, '0'), - memos - } = tx - - const out = { - action, + const { isSend = lt(tx.nativeAmount, '0'), tokenId = null } = tx + + const out: MergedTransaction = { + action: tx.action, blockHeight: tx.blockHeight, confirmations: tx.confirmations ?? 'unconfirmed', - currencyCode, date: tx.date, - memos, + isSend, + memos: tx.memos, + nativeAmount: new Map(oldTx?.nativeAmount ?? []), + networkFee: new Map(oldTx?.networkFee ?? []), otherParams: tx.otherParams, ourReceiveAddresses: tx.ourReceiveAddresses, signedTx: tx.signedTx, - isSend, - txid: tx.txid, - - nativeAmount: { ...oldTx.nativeAmount }, - networkFee: { ...oldTx.networkFee } + txid: tx.txid } - - out.nativeAmount[currencyCode] = tx.nativeAmount - out.networkFee[currencyCode] = - tx.networkFee != null ? tx.networkFee.toString() : '0' - + out.nativeAmount.set(tokenId, tx.nativeAmount) + out.networkFee.set(tokenId, tx.networkFee ?? '0') if (tx.parentNetworkFee != null) { - out.networkFee[defaultCurrency] = String(tx.parentNetworkFee) + out.networkFee.set(null, String(tx.parentNetworkFee)) } return out diff --git a/src/core/currency/wallet/metadata.ts b/src/core/currency/wallet/metadata.ts new file mode 100644 index 000000000..2f86319e0 --- /dev/null +++ b/src/core/currency/wallet/metadata.ts @@ -0,0 +1,66 @@ +import { asNumber, asObject, asOptional, asString, Cleaner } from 'cleaners' + +import { EdgeMetadata, EdgeMetadataChange } from '../../../types/types' +import { CurrencyWalletInput } from './currency-wallet-pixie' + +export const asEdgeMetadata: Cleaner = raw => { + const clean = asDiskMetadata(raw) + const { exchangeAmount = {} } = clean + + // Delete corrupt amounts that exceed the Javascript number range: + for (const fiat of Object.keys(clean)) { + if (String(exchangeAmount[fiat]).includes('e')) { + delete exchangeAmount[fiat] + } + } + + return clean +} + +export function mergeMetadata( + under: EdgeMetadata, + over: EdgeMetadata | EdgeMetadataChange +): EdgeMetadata { + const out: EdgeMetadata = { exchangeAmount: {} } + const { exchangeAmount = {} } = out + + // Merge the fiat amounts: + const underAmounts = under.exchangeAmount ?? {} + const overAmounts = over.exchangeAmount ?? {} + for (const fiat of Object.keys(underAmounts)) { + if (overAmounts[fiat] !== null) exchangeAmount[fiat] = underAmounts[fiat] + } + for (const fiat of Object.keys(overAmounts)) { + const amount = overAmounts[fiat] + if (amount != null) exchangeAmount[fiat] = amount + } + + // Merge simple fields: + if (over.bizId !== null) out.bizId = over.bizId ?? under.bizId + if (over.category !== null) out.category = over.category ?? under.category + if (over.name !== null) out.name = over.name ?? under.name + if (over.notes !== null) out.notes = over.notes ?? under.notes + + return out +} + +export function upgradeMetadata( + input: CurrencyWalletInput, + metadata: EdgeMetadata | EdgeMetadataChange +): void { + const { fiat = 'iso:USD' } = input.props.walletState + if (metadata.amountFiat != null) { + metadata.exchangeAmount = { + ...metadata.exchangeAmount, + [fiat]: metadata.amountFiat + } + } +} + +const asDiskMetadata = asObject({ + bizId: asOptional(asNumber), + category: asOptional(asString), + exchangeAmount: asOptional(asObject(asNumber)), + name: asOptional(asString), + notes: asOptional(asString) +}) diff --git a/src/types/error.ts b/src/types/error.ts index d9b053a8a..729181489 100644 --- a/src/types/error.ts +++ b/src/types/error.ts @@ -5,7 +5,7 @@ import { base64 } from 'rfc4648' import { asOtpErrorPayload, asPasswordErrorPayload } from './server-cleaners' import type { ChallengeErrorPayload } from './server-types' import { upgradeCurrencyCode } from './type-helpers' -import type { EdgeSwapInfo, EdgeSwapRequest } from './types' +import type { EdgeSwapInfo, EdgeSwapRequest, EdgeTokenId } from './types' /* * These are errors the core knows about. @@ -294,8 +294,8 @@ export class SwapCurrencyError extends Error { readonly pluginId: string readonly fromCurrency: string readonly toCurrency: string - readonly fromTokenId: string | undefined - readonly toTokenId: string | undefined + readonly fromTokenId: EdgeTokenId | undefined + readonly toTokenId: EdgeTokenId | undefined constructor( swapInfo: EdgeSwapInfo, @@ -342,9 +342,9 @@ export class SwapCurrencyError extends Error { this.name = 'SwapCurrencyError' this.pluginId = swapInfo.pluginId this.fromCurrency = from.currencyCode - this.fromTokenId = from.tokenId + this.fromTokenId = from.tokenId ?? null this.toCurrency = to.currencyCode - this.toTokenId = to.tokenId + this.toTokenId = to.tokenId ?? null } } } diff --git a/src/types/type-helpers.ts b/src/types/type-helpers.ts index df6ed4e8a..5bed8a865 100644 --- a/src/types/type-helpers.ts +++ b/src/types/type-helpers.ts @@ -1,4 +1,4 @@ -import type { EdgeCurrencyInfo, EdgeTokenMap } from './types' +import type { EdgeCurrencyInfo, EdgeTokenId, EdgeTokenMap } from './types' /** * Translates a currency code to a tokenId, @@ -8,14 +8,14 @@ export function upgradeCurrencyCode(opts: { allTokens: EdgeTokenMap currencyInfo: EdgeCurrencyInfo currencyCode?: string - tokenId?: string -}): { currencyCode: string; tokenId?: string } { + tokenId?: EdgeTokenId +}): { currencyCode: string; tokenId?: EdgeTokenId } { const { currencyInfo, allTokens } = opts // Find the tokenId: let tokenId = opts.tokenId if ( - tokenId == null && + tokenId === undefined && opts.currencyCode != null && opts.currencyCode !== currencyInfo.currencyCode ) { diff --git a/src/types/types.ts b/src/types/types.ts index 901b8b79c..2bdd00921 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -256,7 +256,7 @@ export interface EdgeMemo { export interface EdgeAssetAmount { pluginId: string - tokenId?: string + tokenId?: EdgeTokenId nativeAmount?: string } @@ -322,6 +322,12 @@ export interface EdgeToken { networkLocation: JsonObject | undefined } +/** + * A normal tokenId (chosen by the currency plugin), + * or `null` to indicate the parent currency (such as "ETH"). + */ +export type EdgeTokenId = string | null + export interface EdgeTokenMap { // Each currency plugin decides how to generate this ID, // such as by using the contract address: @@ -442,15 +448,36 @@ export interface EdgeMetadata { name?: string notes?: string - /** @deprecated Use exchangeAmount instead */ + /** + * @deprecated Use exchangeAmount instead. + * This is a copy of `exchangeAmount[wallet.fiatCurrencyCode]` + */ amountFiat?: number } +/** + * Like EdgeMetadata, but passing `null` will delete a saved value, + * while passing `undefined` will leave the value unchanged. + */ +export interface EdgeMetadataChange { + bizId?: number | null + category?: string | null + exchangeAmount?: { [fiatCurrencyCode: string]: number | null } + name?: string | null + notes?: string | null + + /** + * @deprecated Use exchangeAmount instead. + * This will be saved as `exchangeAmount[wallet.fiatCurrencyCode]` + */ + amountFiat?: number | null +} + // Would prefer a better name than EdgeNetworkFee2 but can't think of one export interface EdgeNetworkFee2 { readonly nativeAmount: string readonly currencyPluginId: string - readonly tokenId?: string + readonly tokenId?: EdgeTokenId } export interface EdgeTxSwap { @@ -488,11 +515,14 @@ export type EdgeConfirmationState = | number export interface EdgeTransaction { + /** + * The asset used to query this transaction. + * The amounts and metadata will reflect the chosen asset. + */ + tokenId: EdgeTokenId + // Amounts: - currencyCode: string nativeAmount: string - - // Fees: networkFee: string parentNetworkFee?: string @@ -539,6 +569,9 @@ export interface EdgeTransaction { metadata?: EdgeMetadata walletId: string otherParams?: JsonObject + + /** @deprecated Use tokenId instead */ + currencyCode: string } export interface EdgeSpendTarget { @@ -563,7 +596,7 @@ export interface EdgePaymentProtocolInfo { export interface EdgeSpendInfo { // Basic information: - tokenId?: string + tokenId?: EdgeTokenId privateKeys?: string[] spendTargets: EdgeSpendTarget[] memos?: EdgeMemo[] @@ -656,7 +689,6 @@ export interface EdgeParsedUri { bitidKycRequest?: string // Experimental bitidPaymentAddress?: string // Experimental bitIDURI?: string - currencyCode?: string expireDate?: Date legacyAddress?: string metadata?: EdgeMetadata @@ -668,8 +700,12 @@ export interface EdgeParsedUri { returnUri?: string segwitAddress?: string token?: EdgeMetaToken + tokenId?: EdgeTokenId uniqueIdentifier?: string // Ripple payment id walletConnect?: WalletConnect + + /** @deprecated Use tokenId instead */ + currencyCode?: string } export interface EdgeEncodeUri { @@ -683,6 +719,9 @@ export interface EdgeEncodeUri { // options ------------------------------------------------------------- export interface EdgeCurrencyCodeOptions { + tokenId?: EdgeTokenId + + /** @deprecated Use `tokenId` instead. */ currencyCode?: string } @@ -707,10 +746,13 @@ export interface EdgeGetTransactionsOptions { startEntries?: number // Filtering: - currencyCode?: string startDate?: Date endDate?: Date searchString?: string + tokenId?: EdgeTokenId + + /** @deprecated Use tokenId instead */ + currencyCode?: string } export interface EdgeStreamTransactionOptions { @@ -736,7 +778,7 @@ export interface EdgeStreamTransactionOptions { searchString?: string /** The token to query, or undefined for the main currency */ - tokenId?: string + tokenId?: EdgeTokenId } export type EdgeGetReceiveAddressOptions = EdgeCurrencyCodeOptions & { @@ -774,12 +816,12 @@ export interface EdgeSignMessageOptions { export interface EdgeCurrencyEngineCallbacks { readonly onAddressChanged: () => void readonly onAddressesChecked: (progressRatio: number) => void - readonly onBalanceChanged: ( - currencyCode: string, - nativeBalance: string - ) => void readonly onNewTokens: (tokenIds: string[]) => void readonly onStakingStatusChanged: (status: EdgeStakingStatus) => void + readonly onTokenBalanceChanged: ( + tokenId: EdgeTokenId, + balance: string + ) => void readonly onTransactionsChanged: (transactions: EdgeTransaction[]) => void readonly onTxidsChanged: (txids: EdgeTxidMap) => void readonly onUnactivatedTokenIdsChanged: (unactivatedTokenIds: string[]) => void @@ -787,6 +829,12 @@ export interface EdgeCurrencyEngineCallbacks { /** @deprecated onTransactionsChanged handles confirmation changes */ readonly onBlockHeightChanged: (blockHeight: number) => void + + /** @deprecated Use onTokenBalanceChanged instead */ + readonly onBalanceChanged: ( + currencyCode: string, + nativeBalance: string + ) => void } export interface EdgeCurrencyEngineOptions { @@ -970,6 +1018,8 @@ export interface EdgeBalances { [currencyCode: string]: string } +export type EdgeBalanceMap = Map + export type EdgeReceiveAddress = EdgeFreshAddress & { metadata: EdgeMetadata nativeAmount: string @@ -994,7 +1044,7 @@ export interface EdgeGetActivationAssetsResults { assetOptions: Array<{ paymentWalletId?: string // If walletId is present, use MUST activate with this wallet currencyPluginId: string - tokenId?: string + tokenId?: EdgeTokenId }> } @@ -1062,6 +1112,7 @@ export interface EdgeCurrencyWallet { ) => Promise // Chain state: + readonly balanceMap: EdgeBalanceMap readonly balances: EdgeBalances readonly blockHeight: number readonly syncRatio: number @@ -1113,7 +1164,7 @@ export interface EdgeCurrencyWallet { readonly saveTxMetadata: ( txid: string, currencyCode: string, - metadata: EdgeMetadata + metadata: EdgeMetadataChange ) => Promise readonly signTx: (tx: EdgeTransaction) => Promise readonly sweepPrivateKeys: ( @@ -1174,8 +1225,8 @@ export interface EdgeSwapRequest { toWallet: EdgeCurrencyWallet // What? - fromTokenId?: string - toTokenId?: string + fromTokenId?: EdgeTokenId + toTokenId?: EdgeTokenId // How much? nativeAmount: string diff --git a/src/util/asMap.ts b/src/util/asMap.ts new file mode 100644 index 000000000..15d466612 --- /dev/null +++ b/src/util/asMap.ts @@ -0,0 +1,54 @@ +import { asCodec, asObject, Cleaner } from 'cleaners' + +import { EdgeTokenId } from '../browser' + +/** + * Reads a JSON-style object into a JavaScript `Map` object with string keys. + */ +export function asMap(cleaner: Cleaner): Cleaner> { + const asJsonObject = asObject(cleaner) + + return asCodec( + raw => { + const clean = asJsonObject(raw) + const out = new Map() + for (const key of Object.keys(clean)) out.set(key, clean[key]) + return out + }, + clean => { + const out: { [key: string]: T } = {} + clean.forEach((value, key) => { + out[key] = value + }) + return asJsonObject(out) + } + ) +} + +/** + * Reads a JSON-style object into a JavaScript `Map` object + * with EdgeTokenId keys. + */ +export function asTokenIdMap( + cleaner: Cleaner +): Cleaner> { + const asJsonObject = asObject(cleaner) + + return asCodec( + raw => { + const clean = asJsonObject(raw) + const out = new Map() + for (const key of Object.keys(clean)) { + out.set(key === '' ? null : key, clean[key]) + } + return out + }, + clean => { + const out: { [key: string]: T } = {} + clean.forEach((value, key) => { + out[key == null ? '' : key] = value + }) + return asJsonObject(out) + } + ) +} diff --git a/src/util/util.ts b/src/util/util.ts index 6d830ddb8..a93de3c90 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -3,31 +3,8 @@ * Purrs quietly when pet. */ export function softCat(...lists: Array): T[] { - const flowHack: any = lists.filter(list => list != null) - return [].concat(...flowHack) -} - -/** - * Merges several Javascript objects deeply, - * preferring the items from later objects. - */ -export function mergeDeeply(...objects: any[]): any { - const out: any = {} - - for (const o of objects) { - if (o == null) continue - - for (const key of Object.keys(o)) { - if (o[key] == null) continue - - out[key] = - out[key] != null && typeof o[key] === 'object' - ? mergeDeeply(out[key], o[key]) - : o[key] - } - } - - return out + const out: T[] = [] + return out.concat(...lists.filter((list): list is T[] => list != null)) } /** diff --git a/test/core/currency/currency.test.ts b/test/core/currency/currency.test.ts index bb0937d0c..736ca7968 100644 --- a/test/core/currency/currency.test.ts +++ b/test/core/currency/currency.test.ts @@ -5,12 +5,16 @@ import { getCurrencyMultiplier } from '../../../src/core/currency/currency-selec import { fakeCurrencyPlugin } from '../../fake/fake-currency-plugin' describe('currency selectors', function () { - it('find currency multiplier', function () { - expect(getCurrencyMultiplier(fakeCurrencyPlugin, {}, 'SMALL')).equals('10') - expect(getCurrencyMultiplier(fakeCurrencyPlugin, {}, 'FAKE')).equals('100') - expect(getCurrencyMultiplier(fakeCurrencyPlugin, {}, 'TOKEN')).equals( - '1000' - ) - expect(getCurrencyMultiplier(fakeCurrencyPlugin, {}, '-error-')).equals('1') + it('find currency multiplier', async function () { + const { currencyInfo } = fakeCurrencyPlugin + const tokens = + fakeCurrencyPlugin.getBuiltinTokens != null + ? await fakeCurrencyPlugin.getBuiltinTokens() + : {} + + expect(getCurrencyMultiplier(currencyInfo, tokens, 'SMALL')).equals('10') + expect(getCurrencyMultiplier(currencyInfo, tokens, 'FAKE')).equals('100') + expect(getCurrencyMultiplier(currencyInfo, tokens, 'TOKEN')).equals('1000') + expect(getCurrencyMultiplier(currencyInfo, tokens, '-error-')).equals('1') }) }) diff --git a/test/core/currency/wallet/currency-wallet.test.ts b/test/core/currency/wallet/currency-wallet.test.ts index e32185ab8..4f857a5ff 100644 --- a/test/core/currency/wallet/currency-wallet.test.ts +++ b/test/core/currency/wallet/currency-wallet.test.ts @@ -9,6 +9,7 @@ import { EdgeCurrencyConfig, EdgeCurrencyWallet, EdgeMetadata, + EdgeMetadataChange, EdgeToken, EdgeTransaction, EdgeTxSwap, @@ -92,8 +93,8 @@ describe('currency wallets', function () { wallet.on('transactionsChanged', txs => { log('changed', txs.map(tx => tx.txid).join(' ')) }) - wallet.watch('balances', balances => { - log('balances', balances) + wallet.watch('balanceMap', balanceMap => { + log('balanceMap', Array.from(balanceMap.entries())) }) wallet.watch('blockHeight', blockHeight => { log('blockHeight', blockHeight) @@ -107,13 +108,21 @@ describe('currency wallets', function () { // Test property watchers: log.assert() + expect(Array.from(wallet.balanceMap.entries())).to.deep.equal([ + [null, '0'], + ['badf00d5', '0'] + ]) expect(wallet.balances).to.deep.equal({ FAKE: '0', TOKEN: '0' }) expect(wallet.stakingStatus).deep.equals({ stakedAmounts: [{ nativeAmount: '0' }] }) await config.changeUserSettings({ tokenBalance: 30 }) - await log.waitFor(1).assert('balances { FAKE: "0", TOKEN: "30" }') + await log.waitFor(1).assert(`balanceMap [[null, "0"], ["badf00d5", "30"]]`) + expect(Array.from(wallet.balanceMap.entries())).to.deep.equal([ + [null, '0'], + ['badf00d5', '30'] + ]) expect(wallet.balances).to.deep.equal({ FAKE: '0', TOKEN: '30' }) await config.changeUserSettings({ blockHeight: 200 }) @@ -125,7 +134,13 @@ describe('currency wallets', function () { expect(wallet.syncRatio).to.equal(0.123456789) await config.changeUserSettings({ balance: 1234567890 }) - await log.waitFor(1).assert('balances { FAKE: "1234567890", TOKEN: "30" }') + await log + .waitFor(1) + .assert(`balanceMap [[null, "1234567890"], ["badf00d5", "30"]]`) + expect(Array.from(wallet.balanceMap.entries())).to.deep.equal([ + [null, '1234567890'], + ['badf00d5', '30'] + ]) expect(wallet.balances).to.deep.equal({ FAKE: '1234567890', TOKEN: '30' }) await config.changeUserSettings({ stakedBalance: 543 }) @@ -194,13 +209,12 @@ describe('currency wallets', function () { const { config } = await makeFakeCurrencyWallet() expect(config.builtinTokens).deep.equals({ - f98103e9217f099208569d295c1b276f1821348636c268c854bb2a086e0037cd: { + badf00d5: { currencyCode: 'TOKEN', displayName: 'Fake Token', denominations: [{ multiplier: '1000', name: 'TOKEN' }], networkLocation: { - contractAddress: - '0XF98103E9217F099208569D295C1B276F1821348636C268C854BB2A086E0037CD' + contractAddress: '0xBADF00D5' } } }) @@ -239,8 +253,7 @@ describe('currency wallets', function () { it('enables tokens', async function () { const log = makeAssertLog() const { wallet } = await makeFakeCurrencyWallet() - const tokenId = - 'f98103e9217f099208569d295c1b276f1821348636c268c854bb2a086e0037cd' + const tokenId = 'badf00d5' wallet.watch('enabledTokenIds', ids => log(ids.join(', '))) expect(wallet.enabledTokenIds).deep.equals([]) @@ -257,8 +270,7 @@ describe('currency wallets', function () { it('supports always-enabled tokens', async function () { const log = makeAssertLog() const { config } = await makeFakeCurrencyWallet() - const tokenId = - 'f98103e9217f099208569d295c1b276f1821348636c268c854bb2a086e0037cd' + const tokenId = 'badf00d5' config.watch('alwaysEnabledTokenIds', ids => log(ids.join(', '))) expect(config.alwaysEnabledTokenIds).deep.equals([]) @@ -515,6 +527,51 @@ describe('currency wallets', function () { }) }) + it('can delete metadata', async function () { + const { wallet, config } = await makeFakeCurrencyWallet() + + const metadata: EdgeMetadata = { + bizId: 1234, + name: 'me', + amountFiat: 0.75, + category: 'expense:Foot Massage', + notes: 'Hello World' + } + + const updateMetadata: EdgeMetadataChange = { + bizId: 1234, + name: 'me', + amountFiat: 0.75, + category: null, + notes: null + } + + const newMetadata = { ...updateMetadata } + newMetadata.category = undefined + newMetadata.notes = undefined + + await config.changeUserSettings({ + txs: { a: { nativeAmount: '25', metadata } } + }) + + const txs = await wallet.getTransactions({}) + expect(txs.length).equals(1) + expect(txs[0].nativeAmount).equals('25') + expect(txs[0].metadata).deep.equals({ + exchangeAmount: { 'iso:USD': 0.75 }, + ...metadata + }) + + await wallet.saveTxMetadata('a', 'FAKE', updateMetadata) + const txs2 = await wallet.getTransactions({}) + expect(txs2.length).equals(1) + expect(txs2[0].nativeAmount).equals('25') + expect(txs2[0].metadata).deep.equals({ + exchangeAmount: { 'iso:USD': 0.75 }, + ...newMetadata + }) + }) + it('can be paused and un-paused', async function () { const { wallet, context } = await makeFakeCurrencyWallet(true) const isEngineRunning = async (): Promise => { @@ -581,10 +638,6 @@ describe('currency wallets', function () { async function addDemoTransactions( currencyConfig: EdgeCurrencyConfig ): Promise { - await currencyConfig.changeUserSettings({ - txs: walletTxs - }) - const tokenId = await currencyConfig.addCustomToken({ currencyCode: 'BTC', denominations: [], @@ -593,6 +646,11 @@ async function addDemoTransactions( contractAddress: 'madeupcontract' } }) + + await currencyConfig.changeUserSettings({ + txs: walletTxs + }) + return tokenId } diff --git a/test/fake/fake-currency-plugin.ts b/test/fake/fake-currency-plugin.ts index 46ecafb3b..ff09c5234 100644 --- a/test/fake/fake-currency-plugin.ts +++ b/test/fake/fake-currency-plugin.ts @@ -22,10 +22,22 @@ import { EdgeWalletInfo, InsufficientFundsError } from '../../src/index' +import { upgradeCurrencyCode } from '../../src/types/type-helpers' import { compare } from '../../src/util/compare' const GENESIS_BLOCK = 1231006505000 +const fakeTokens: EdgeTokenMap = { + badf00d5: { + currencyCode: 'TOKEN', + denominations: [{ multiplier: '1000', name: 'TOKEN' }], + displayName: 'Fake Token', + networkLocation: { + contractAddress: '0xBADF00D5' + } + } +} + const fakeCurrencyInfo: EdgeCurrencyInfo = { currencyCode: 'FAKE', displayName: 'Fake Coin', @@ -43,15 +55,7 @@ const fakeCurrencyInfo: EdgeCurrencyInfo = { // Deprecated: defaultSettings: {}, - metaTokens: [ - { - currencyCode: 'TOKEN', - currencyName: 'Fake Token', - denominations: [{ multiplier: '1000', name: 'TOKEN' }], - contractAddress: - '0XF98103E9217F099208569D295C1B276F1821348636C268C854BB2A086E0037CD' - } - ], + metaTokens: [], memoType: 'text' } @@ -81,6 +85,7 @@ class FakeCurrencyEngine implements EdgeCurrencyEngine { private readonly callbacks: EdgeCurrencyEngineCallbacks private running: boolean private readonly state: State + private allTokens: EdgeTokenMap = fakeTokens constructor(walletInfo: EdgeWalletInfo, opts: EdgeCurrencyEngineOptions) { this.walletId = walletInfo.id @@ -101,11 +106,11 @@ class FakeCurrencyEngine implements EdgeCurrencyEngine { private updateState(settings: Partial): void { const state = this.state const { - onAddressesChecked = nop, - onBalanceChanged = nop, - onBlockHeightChanged = nop, - onStakingStatusChanged = nop, - onTransactionsChanged = nop + onAddressesChecked, + onTokenBalanceChanged, + onBlockHeightChanged, + onStakingStatusChanged, + onTransactionsChanged } = this.callbacks // Address callback: @@ -117,7 +122,7 @@ class FakeCurrencyEngine implements EdgeCurrencyEngine { // Balance callback: if (settings.balance != null) { state.balance = settings.balance - onBalanceChanged('FAKE', state.balance.toString()) + onTokenBalanceChanged(null, state.balance.toString()) } // Staking status callback: @@ -131,7 +136,7 @@ class FakeCurrencyEngine implements EdgeCurrencyEngine { // Token balance callback: if (settings.tokenBalance != null) { state.tokenBalance = settings.tokenBalance - onBalanceChanged('TOKEN', state.tokenBalance.toString()) + onTokenBalanceChanged('badf00d5', state.tokenBalance.toString()) } // Block height callback: @@ -144,9 +149,24 @@ class FakeCurrencyEngine implements EdgeCurrencyEngine { if (settings.txs != null) { const changes: EdgeTransaction[] = [] for (const txid of Object.keys(settings.txs)) { - const newTx = { - ...blankTx, - ...settings.txs[txid], + const incoming: Partial = settings.txs[txid] + const { tokenId = null } = upgradeCurrencyCode({ + allTokens: this.allTokens, + currencyCode: incoming.currencyCode, + currencyInfo: fakeCurrencyInfo + }) + const newTx: EdgeTransaction = { + blockHeight: 0, + currencyCode: 'FAKE', + date: GENESIS_BLOCK, + isSend: false, + memos: [], + nativeAmount: '0', + networkFee: '0', + ourReceiveAddresses: [], + signedTx: '', + tokenId, + ...incoming, txid, walletId: this.walletId } @@ -193,15 +213,11 @@ class FakeCurrencyEngine implements EdgeCurrencyEngine { } getBalance(opts: EdgeCurrencyCodeOptions): string { - const { currencyCode = 'FAKE' } = opts - switch (currencyCode) { - case 'FAKE': - return this.state.balance.toString() - case 'TOKEN': - return this.state.tokenBalance.toString() - default: - throw new Error('Unknown currency') - } + const { tokenId = null } = opts + if (tokenId == null) return this.state.balance.toString() + if (tokenId === 'badf00d5') this.state.tokenBalance.toString() + if (this.allTokens[tokenId] != null) return '0' + throw new Error('Unknown currency') } getNumTransactions(opts: EdgeCurrencyCodeOptions): number { @@ -218,6 +234,7 @@ class FakeCurrencyEngine implements EdgeCurrencyEngine { // Tokens: changeCustomTokens(tokens: EdgeTokenMap): Promise { + this.allTokens = { ...tokens, ...fakeTokens } return Promise.resolve() } @@ -247,7 +264,9 @@ class FakeCurrencyEngine implements EdgeCurrencyEngine { // Spending: makeSpend(spendInfo: EdgeSpendInfo): Promise { - const { currencyCode = 'FAKE', spendTargets } = spendInfo + const { memos = [], spendTargets, tokenId = null } = spendInfo + const { currencyCode } = + tokenId == null ? fakeCurrencyInfo : this.allTokens[tokenId] // Check the spend targets: let total = '0' @@ -258,7 +277,7 @@ class FakeCurrencyEngine implements EdgeCurrencyEngine { } // Check the balances: - if (lt(this.getBalance({ currencyCode }), total)) { + if (lt(this.getBalance({ tokenId }), total)) { return Promise.reject(new InsufficientFundsError()) } @@ -269,13 +288,14 @@ class FakeCurrencyEngine implements EdgeCurrencyEngine { date: GENESIS_BLOCK, feeRateUsed: { fakePrice: 0 }, isSend: false, - memos: [], + memos, nativeAmount: total, networkFee: '0', otherParams: {}, ourReceiveAddresses: [], signedTx: '', txid: 'spend', + tokenId, walletId: this.walletId }) } @@ -351,6 +371,10 @@ class FakeCurrencyTools implements EdgeCurrencyTools { export const fakeCurrencyPlugin: EdgeCurrencyPlugin = { currencyInfo: fakeCurrencyInfo, + getBuiltinTokens(): Promise { + return Promise.resolve(fakeTokens) + }, + makeCurrencyEngine( walletInfo: EdgeWalletInfo, opts: EdgeCurrencyEngineOptions @@ -366,19 +390,3 @@ export const fakeCurrencyPlugin: EdgeCurrencyPlugin = { const asNetworkLocation = asObject({ contractAddress: asString }) - -function nop(...args: unknown[]): void {} - -const blankTx: EdgeTransaction = { - blockHeight: 0, - currencyCode: 'FAKE', - date: GENESIS_BLOCK, - isSend: false, - memos: [], - nativeAmount: '0', - networkFee: '0', - ourReceiveAddresses: [], - signedTx: '', - txid: '', - walletId: '' -} diff --git a/test/fake/fake-transactions.ts b/test/fake/fake-transactions.ts index 284df6f35..9053870b3 100644 --- a/test/fake/fake-transactions.ts +++ b/test/fake/fake-transactions.ts @@ -1,4 +1,6 @@ -export const walletTxs = { +import { EdgeTransaction } from '../../types' + +export const walletTxs: { [txid: string]: Partial } = { a: { blockHeight: 669865, date: 1612885126, @@ -270,7 +272,9 @@ export const walletTxs = { { currencyCode: 'BTC', nativeAmount: '347450', - publicAddress: '3LydNJToQaDnsPf2uQ32AH8jgdRTo2mgdL' + publicAddress: '3LydNJToQaDnsPf2uQ32AH8jgdRTo2mgdL', + memo: undefined, + uniqueIdentifier: undefined } ], swapData: { @@ -320,7 +324,9 @@ export const walletTxs = { { currencyCode: 'BTC', nativeAmount: '147680', - publicAddress: '3FNhvvv5xQ3JVKiNyo7Py9wMZ15GGgBUFv' + publicAddress: '3FNhvvv5xQ3JVKiNyo7Py9wMZ15GGgBUFv', + memo: undefined, + uniqueIdentifier: undefined } ], swapData: { @@ -370,7 +376,9 @@ export const walletTxs = { { currencyCode: 'BTC', nativeAmount: '59270', - publicAddress: '3LgBrHJApTLyoD2PTaGNCP5YgrBtnyNVtA' + publicAddress: '3LgBrHJApTLyoD2PTaGNCP5YgrBtnyNVtA', + memo: undefined, + uniqueIdentifier: undefined } ], swapData: { diff --git a/test/util/asMap.test.ts b/test/util/asMap.test.ts new file mode 100644 index 000000000..42c47d66d --- /dev/null +++ b/test/util/asMap.test.ts @@ -0,0 +1,35 @@ +import { expect } from 'chai' +import { asDate, uncleaner } from 'cleaners' +import { describe, it } from 'mocha' + +import { asMap } from '../../src/util/asMap' + +describe('asMap', function () { + const asDates = asMap(asDate) + + it('cleans JSON data', function () { + const clean = asDates({ + btc: '2009-01-03', + usa: '1776-07-04' + }) + + expect(Array.from(clean.entries())).deep.equals([ + ['btc', new Date('2009-01-03')], + ['usa', new Date('1776-07-04')] + ]) + }) + + it('restores JSON data', function () { + const wasDates = uncleaner(asDates) + + const clean = new Map([ + ['btc', new Date('2009-01-03')], + ['usa', new Date('1776-07-04')] + ]) + + expect(wasDates(clean)).deep.equals({ + btc: '2009-01-03T00:00:00.000Z', + usa: '1776-07-04T00:00:00.000Z' + }) + }) +}) diff --git a/test/util/util.test.ts b/test/util/util.test.ts deleted file mode 100644 index 05eff5a6e..000000000 --- a/test/util/util.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { expect } from 'chai' -import { describe, it } from 'mocha' - -import { mergeDeeply } from '../../src/util/util' - -describe('utilities', function () { - it('mergeDeeply', function () { - const a = { - x: 1, - y: { a: -1, c: 4 } - } - const b = { - y: { a: 2, b: 3 }, - z: 5 - } - - expect(mergeDeeply(a, b)).deep.equals({ - x: 1, - y: { a: 2, b: 3, c: 4 }, - z: 5 - }) - }) -})