diff --git a/src/core/currency/wallet/currency-wallet-api.ts b/src/core/currency/wallet/currency-wallet-api.ts index 8349ee503..fc9173f7b 100644 --- a/src/core/currency/wallet/currency-wallet-api.ts +++ b/src/core/currency/wallet/currency-wallet-api.ts @@ -35,7 +35,7 @@ import { EdgeTxAction, EdgeWalletInfo } from '../../../types/types' -import { mergeDeeply } from '../../../util/util' +import { mergeDeeply, mergeDeeplyNull } from '../../../util/util' import { makeMetaTokens } from '../../account/custom-tokens' import { toApiInput } from '../../root-pixie' import { makeStorageWalletApi } from '../../storage/storage-api' @@ -718,10 +718,11 @@ export function combineTxWithFile( if (file != null) { if (file.creationDate < out.date) out.date = file.creationDate - const merged: TransactionFile['currencies']['currencyCode'] = mergeDeeply( - file.currencies[walletCurrency], - file.currencies[currencyCode] - ) + const merged: TransactionFile['currencies']['currencyCode'] = + mergeDeeplyNull( + file.currencies[walletCurrency], + file.currencies[currencyCode] + ) if (merged.metadata != null) { out.metadata = { ...out.metadata, diff --git a/src/core/currency/wallet/currency-wallet-cleaners.ts b/src/core/currency/wallet/currency-wallet-cleaners.ts index 79d667853..ab681bb30 100644 --- a/src/core/currency/wallet/currency-wallet-cleaners.ts +++ b/src/core/currency/wallet/currency-wallet-cleaners.ts @@ -33,10 +33,10 @@ import { asJsonObject } from '../../../util/file-helpers' */ export interface DiskMetadata { bizId?: number - category?: string + category?: string | null exchangeAmount: { [fiatCurrencyCode: string]: number } - name?: string - notes?: string + name?: string | null + notes?: string | null } /** @@ -130,6 +130,16 @@ interface LegacyMapFile { // building-block cleaners // --------------------------------------------------------------------- +/** + * Like `asOptional`, but explicitly preserves `null`. + */ +function asNullable(cleaner: Cleaner): Cleaner { + return raw => { + if (raw === undefined) return undefined + if (raw === null) return null + return cleaner(raw) + } +} /** * Turns user-provided metadata into its on-disk format. */ @@ -194,10 +204,10 @@ export const asEdgeTxSwap = asObject({ const asDiskMetadata = asObject({ bizId: asOptional(asNumber), - category: asOptional(asString), + category: asNullable(asString), exchangeAmount: asOptional(asObject(asNumber), () => ({})), - name: asOptional(asString), - notes: asOptional(asString) + name: asNullable(asString), + notes: asNullable(asString) }) export function asIntegerString(raw: unknown): string { diff --git a/src/core/currency/wallet/currency-wallet-export.ts b/src/core/currency/wallet/currency-wallet-export.ts index 738ec2bf7..40d6592ad 100644 --- a/src/core/currency/wallet/currency-wallet-export.ts +++ b/src/core/currency/wallet/currency-wallet-export.ts @@ -51,18 +51,13 @@ export function searchStringFilter( if (checkNullTypeAndIndex(tx.nativeAmount)) return true if (tx.metadata != null) { - const { - category = '', - name = '', - notes = '', - exchangeAmount = {} - } = tx.metadata + const { category, name, notes, exchangeAmount = {} } = tx.metadata const txCurrencyWalletState = tx.walletId != null ? currencyState.wallets[tx.walletId] : undefined if ( - checkNullTypeAndIndex(category) || - checkNullTypeAndIndex(name) || - checkNullTypeAndIndex(notes) || + checkNullTypeAndIndex(category ?? '') || + checkNullTypeAndIndex(name ?? '') || + checkNullTypeAndIndex(notes ?? '') || (txCurrencyWalletState != null && checkNullTypeAndIndex(exchangeAmount[txCurrencyWalletState.fiat])) ) diff --git a/src/core/currency/wallet/currency-wallet-files.ts b/src/core/currency/wallet/currency-wallet-files.ts index 1e94774c0..b25b21a49 100644 --- a/src/core/currency/wallet/currency-wallet-files.ts +++ b/src/core/currency/wallet/currency-wallet-files.ts @@ -8,7 +8,7 @@ import { EdgeTxAction } from '../../../types/types' import { makeJsonFile } from '../../../util/file-helpers' -import { mergeDeeply } from '../../../util/util' +import { mergeDeeplyNull } from '../../../util/util' import { fetchAppIdInfo } from '../../account/lobby-api' import { toApiInput } from '../../root-pixie' import { RootState } from '../../root-reducer' @@ -479,7 +479,7 @@ export async function setCurrencyWalletTxMetadata( } } - const json = mergeDeeply(oldFile, newFile) + const json = mergeDeeplyNull(oldFile, newFile) // Save the new file: dispatch({ diff --git a/src/types/types.ts b/src/types/types.ts index d67fd4b43..63e8cd9f5 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -490,13 +490,13 @@ export interface EdgeCurrencyInfo { export interface EdgeMetadata { bizId?: number - category?: string + category?: string | null exchangeAmount?: { [fiatCurrencyCode: string]: number } - name?: string - notes?: string + name?: string | null + notes?: string | null /** @deprecated Use exchangeAmount instead */ - amountFiat?: number + amountFiat?: number | null } // Would prefer a better name than EdgeNetworkFee2 but can't think of one diff --git a/src/util/util.ts b/src/util/util.ts index 6d830ddb8..1d0774e60 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -30,6 +30,34 @@ export function mergeDeeply(...objects: any[]): any { return out } +/** + * Merges several Javascript objects deeply, + * preferring the items from later objects. Includes + * null as a valid to stomp on older data + */ +export function mergeDeeplyNull(...objects: any[]): any { + const out: any = {} + + for (const o of objects) { + if (o === undefined) continue + + for (const key of Object.keys(o)) { + if (o[key] === undefined) continue + if (o[key] === null) { + out[key] = null + continue + } + + out[key] = + out[key] !== undefined && typeof o[key] === 'object' + ? mergeDeeplyNull(out[key], o[key]) + : o[key] + } + } + + return out +} + /** * Like `Object.assign`, but makes the properties non-enumerable. */ diff --git a/test/core/currency/wallet/currency-wallet.test.ts b/test/core/currency/wallet/currency-wallet.test.ts index 9604e4f06..b74823bae 100644 --- a/test/core/currency/wallet/currency-wallet.test.ts +++ b/test/core/currency/wallet/currency-wallet.test.ts @@ -589,6 +589,46 @@ describe('currency wallets', function () { expect(txs[0].savedAction).deep.equals(savedAction) }) + 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 newMetadata: EdgeMetadata = { + bizId: 1234, + name: 'me', + amountFiat: 0.75, + category: null, + notes: null + } + + 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', newMetadata) + 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 => { diff --git a/test/util/util.test.ts b/test/util/util.test.ts index 05eff5a6e..e0ff08dcf 100644 --- a/test/util/util.test.ts +++ b/test/util/util.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai' import { describe, it } from 'mocha' -import { mergeDeeply } from '../../src/util/util' +import { mergeDeeply, mergeDeeplyNull } from '../../src/util/util' describe('utilities', function () { it('mergeDeeply', function () { @@ -20,4 +20,21 @@ describe('utilities', function () { z: 5 }) }) + it('mergeDeeplyNull', function () { + const a = { + x: 1, + y: { a: -1, c: 4 } + } + const b = { + x: null, + y: { a: 2, b: 3 }, + z: 5 + } + + expect(mergeDeeplyNull(a, b)).deep.equals({ + x: null, + y: { a: 2, b: 3, c: 4 }, + z: 5 + }) + }) })