diff --git a/CHANGELOG.md b/CHANGELOG.md index a92a5ab94..498f8a1dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- added: `EdgeCurrencyWallet.walletSettings` and `EdgeCurrencyWallet.changeWalletSettings`, plus matching engine plumbing. + ## 2.43.6 (2026-04-02) - fixed: Upgraded @nymproject/mix-fetch with promised reliability improvements. diff --git a/src/core/account/account-api.ts b/src/core/account/account-api.ts index 496bb3d4e..d97cc995c 100644 --- a/src/core/account/account-api.ts +++ b/src/core/account/account-api.ts @@ -25,11 +25,13 @@ import { EdgeSwapQuote, EdgeSwapRequest, EdgeSwapRequestOptions, + EdgeWalletInfo, EdgeWalletInfoFull, EdgeWalletStates } from '../../types/types' import { makeEdgeResult } from '../../util/edgeResult' import { base58 } from '../../util/encoding' +import { saveWalletSettings } from '../currency/wallet/currency-wallet-files' import { getPublicWalletInfo } from '../currency/wallet/currency-wallet-pixie' import { finishWalletCreation, @@ -55,13 +57,14 @@ import { import { changePin, checkPin2, deletePin } from '../login/pin2' import { changeRecovery, deleteRecovery } from '../login/recovery2' import { listSplittableWalletTypes, splitWalletInfo } from '../login/splitting' +import { asEdgeStorageKeys } from '../login/storage-keys' import { changeVoucherStatus } from '../login/vouchers' import { findCurrencyPluginId, getCurrencyTools } from '../plugins/plugins-selectors' import { ApiInput } from '../root-pixie' -import { makeLocalDisklet } from '../storage/repo' +import { makeLocalDisklet, makeRepoPaths } from '../storage/repo' import { makeStorageWalletApi } from '../storage/storage-api' import { fetchSwapQuotes } from '../swap/swap-api' import { changeWalletStates } from './account-files' @@ -72,6 +75,18 @@ import { makeLobbyApi } from './lobby-api' import { makeMemoryWalletInner } from './memory-wallet' import { CurrencyConfig, SwapConfig } from './plugin-api' +async function prewriteWalletSettings( + ai: ApiInput, + walletInfo: EdgeWalletInfo, + walletSettings: object +): Promise { + const { io } = ai.props + const storageKeys = asEdgeStorageKeys(walletInfo.keys) + const { disklet } = makeRepoPaths(io, storageKeys) + + await saveWalletSettings(disklet, walletSettings) +} + /** * Creates an unwrapped account API object around an account state object. */ @@ -642,6 +657,9 @@ export function makeAccountApi(ai: ApiInput, accountId: string): EdgeAccount { ai.props.log.breadcrumb('EdgeAccount.createCurrencyWallet', {}) const walletInfo = await makeCurrencyWalletKeys(ai, walletType, opts) + if (opts.walletSettings != null) { + await prewriteWalletSettings(ai, walletInfo, opts.walletSettings) + } const childKey = decryptChildKey(stashTree, sessionKey, login.loginId) await applyKit(ai, sessionKey, makeKeysKit(ai, childKey, [walletInfo])) return await finishWalletCreation(ai, accountId, walletInfo.id, opts) @@ -674,6 +692,14 @@ export function makeAccountApi(ai: ApiInput, accountId: string): EdgeAccount { ) ) + // Prewrite wallet settings before applyKit triggers the pixie: + for (let i = 0; i < walletInfos.length; i++) { + const { walletSettings } = createWallets[i] + if (walletSettings != null) { + await prewriteWalletSettings(ai, walletInfos[i], walletSettings) + } + } + // Store the keys on the server: const childKey = decryptChildKey(stashTree, sessionKey, login.loginId) await applyKit(ai, sessionKey, makeKeysKit(ai, childKey, walletInfos)) diff --git a/src/core/account/memory-wallet.ts b/src/core/account/memory-wallet.ts index 7bfb31f83..c1282ee37 100644 --- a/src/core/account/memory-wallet.ts +++ b/src/core/account/memory-wallet.ts @@ -28,7 +28,7 @@ export const makeMemoryWalletInner = async ( walletType: string, opts: EdgeCreateCurrencyWalletOptions = {} ): Promise => { - const { keys } = opts + const { keys, walletSettings = {} } = opts if (keys == null) throw new Error('No keys provided') const walletId = `memorywallet-${memoryWalletCount++}` @@ -113,6 +113,7 @@ export const makeMemoryWalletInner = async ( lightMode: true, log, userSettings: { ...(config.userSettings ?? {}) }, + walletSettings, walletLocalDisklet: makeMemoryDisklet(), walletLocalEncryptedDisklet: makeMemoryDisklet() }) diff --git a/src/core/actions.ts b/src/core/actions.ts index 59bdf34ca..dfb685e44 100644 --- a/src/core/actions.ts +++ b/src/core/actions.ts @@ -11,7 +11,8 @@ import { EdgeTokenMap, EdgeTransaction, EdgeWalletInfo, - EdgeWalletStates + EdgeWalletStates, + JsonObject } from '../types/types' import { SwapSettings } from './account/account-types' import { ClientInfo } from './context/client-file' @@ -359,6 +360,20 @@ export type RootAction = walletId: string } } + | { + type: 'CURRENCY_WALLET_CHANGED_WALLET_SETTINGS' + payload: { + walletId: string + walletSettings: JsonObject + } + } + | { + type: 'CURRENCY_WALLET_LOADED_WALLET_SETTINGS_FILE' + payload: { + walletId: string + walletSettings: JsonObject + } + } | { type: 'INFO_CACHE_FETCHED' payload: InfoCacheFile diff --git a/src/core/currency/wallet/currency-wallet-api.ts b/src/core/currency/wallet/currency-wallet-api.ts index a3da9d3f3..094d8ce1a 100644 --- a/src/core/currency/wallet/currency-wallet-api.ts +++ b/src/core/currency/wallet/currency-wallet-api.ts @@ -40,7 +40,8 @@ import { EdgeTokenId, EdgeTokenIdOptions, EdgeTransaction, - EdgeWalletInfo + EdgeWalletInfo, + JsonObject } from '../../../types/types' import { makeMetaTokens } from '../../account/custom-tokens' import { splitWalletInfo } from '../../login/splitting' @@ -63,6 +64,7 @@ import { loadTxFiles, renameCurrencyWallet, saveTxMetadataFile, + saveWalletSettingsFile, setCurrencyWalletFiat, setupNewTxMetadata, updateCurrencyWalletTxMetadata @@ -135,6 +137,9 @@ export function makeCurrencyWalletApi( get id(): string { return storageWalletApi.id }, + get imported(): boolean { + return walletInfo.imported === true + }, get localDisklet(): Disklet { return storageWalletApi.localDisklet }, @@ -193,6 +198,17 @@ export function makeCurrencyWalletApi( return div(nativeAmount, multiplier, multiplier.length) }, + // User settings for this wallet: + get walletSettings(): JsonObject { + return input.props.walletState.walletSettings + }, + async changeWalletSettings(settings: JsonObject): Promise { + if (input.props.walletState.currencyInfo.hasWalletSettings !== true) { + throw new Error('Wallet settings unsupported') + } + await saveWalletSettingsFile(input, settings) + }, + // Chain state: get balances(): EdgeBalances { return input.props.walletState.balances diff --git a/src/core/currency/wallet/currency-wallet-cleaners.ts b/src/core/currency/wallet/currency-wallet-cleaners.ts index c4890912b..4175d4e96 100644 --- a/src/core/currency/wallet/currency-wallet-cleaners.ts +++ b/src/core/currency/wallet/currency-wallet-cleaners.ts @@ -308,6 +308,10 @@ export const asTokensFile = asObject({ detectedTokenIds: asArray(asString) }) +export const asWalletSettingsFile = asObject({ + walletSettings: asOptional(asJsonObject, () => ({})) +}) + const asTransactionAsset = asObject({ assetAction: asOptional(asEdgeAssetAction), metadata: asEdgeMetadata, diff --git a/src/core/currency/wallet/currency-wallet-files.ts b/src/core/currency/wallet/currency-wallet-files.ts index e09605775..3c548787f 100644 --- a/src/core/currency/wallet/currency-wallet-files.ts +++ b/src/core/currency/wallet/currency-wallet-files.ts @@ -8,7 +8,8 @@ import { EdgeSubscribedAddress, EdgeTokenId, EdgeTransaction, - EdgeTxAction + EdgeTxAction, + JsonObject } from '../../../types/types' import { makeJsonFile } from '../../../util/file-helpers' import { fetchAppIdInfo } from '../../account/lobby-api' @@ -30,6 +31,7 @@ import { asTransactionFile, asWalletFiatFile, asWalletNameFile, + asWalletSettingsFile, LegacyTransactionFile, TransactionAsset, TransactionFile @@ -45,6 +47,7 @@ const LEGACY_TOKENS_FILE = 'EnabledTokens.json' const SEEN_TX_CHECKPOINT_FILE = 'seenTxCheckpoint.json' const TOKENS_FILE = 'Tokens.json' const WALLET_NAME_FILE = 'WalletName.json' +const WALLET_SETTINGS_FILE = 'WalletSettings.json' const legacyAddressFile = makeJsonFile(asLegacyAddressFile) const legacyMapFile = makeJsonFile(asLegacyMapFile) @@ -55,6 +58,7 @@ const tokensFile = makeJsonFile(asTokensFile) const transactionFile = makeJsonFile(asTransactionFile) const walletFiatFile = makeJsonFile(asWalletFiatFile) const walletNameFile = makeJsonFile(asWalletNameFile) +const walletSettingsFile = makeJsonFile(asWalletSettingsFile) /** * Updates the enabled tokens on a wallet. @@ -284,6 +288,52 @@ export async function loadTokensFile( }) } +/** + * Loads wallet-specific settings. + */ +export async function loadWalletSettingsFile( + input: CurrencyWalletInput +): Promise { + const { dispatch, state, walletId } = input.props + const disklet = getStorageWalletDisklet(state, walletId) + + const clean = await walletSettingsFile.load(disklet, WALLET_SETTINGS_FILE) + dispatch({ + type: 'CURRENCY_WALLET_LOADED_WALLET_SETTINGS_FILE', + payload: { + walletId, + walletSettings: clean?.walletSettings ?? {} + } + }) +} + +/** + * Persists wallet settings to disk. + */ +export async function saveWalletSettingsFile( + input: CurrencyWalletInput, + walletSettings: JsonObject +): Promise { + const { dispatch, state, walletId } = input.props + const disklet = getStorageWalletDisklet(state, walletId) + + await saveWalletSettings(disklet, walletSettings) + + dispatch({ + type: 'CURRENCY_WALLET_CHANGED_WALLET_SETTINGS', + payload: { walletId, walletSettings } + }) +} + +export async function saveWalletSettings( + disklet: Disklet, + walletSettings: JsonObject +): Promise { + await walletSettingsFile.save(disklet, WALLET_SETTINGS_FILE, { + walletSettings + }) +} + /** * Loads transaction metadata files. */ @@ -697,6 +747,10 @@ export async function reloadWalletFiles( if (changes.includes(TOKENS_FILE) || changes.includes(LEGACY_TOKENS_FILE)) { await loadTokensFile(input) } + const { hasWalletSettings = false } = input.props.walletState.currencyInfo + if (hasWalletSettings && changes.includes(WALLET_SETTINGS_FILE)) { + await loadWalletSettingsFile(input) + } if (changes.includes(CURRENCY_FILE)) { await loadFiatFile(input) } diff --git a/src/core/currency/wallet/currency-wallet-pixie.ts b/src/core/currency/wallet/currency-wallet-pixie.ts index 2b310532b..4871474fb 100644 --- a/src/core/currency/wallet/currency-wallet-pixie.ts +++ b/src/core/currency/wallet/currency-wallet-pixie.ts @@ -14,7 +14,8 @@ import { EdgeCurrencyTools, EdgeCurrencyWallet, EdgeTokenMap, - EdgeWalletInfo + EdgeWalletInfo, + JsonObject } from '../../../types/types' import { makeJsonFile } from '../../../util/file-helpers' import { makePeriodicTask, PeriodicTask } from '../../../util/periodic-task' @@ -45,9 +46,14 @@ import { loadSeenTxCheckpointFile, loadTokensFile, loadTxFileNames, + loadWalletSettingsFile, writeTokensFile } from './currency-wallet-files' -import { CurrencyWalletState, initialTokenIds } from './currency-wallet-reducer' +import { + CurrencyWalletState, + initialTokenIds, + initialWalletSettings +} from './currency-wallet-reducer' import { tokenIdsToCurrencyCodes, uniqueStrings } from './enabled-tokens' export interface CurrencyWalletOutput { @@ -118,6 +124,11 @@ export const walletPixie: TamePixie = combinePixies({ // so the engine can start in the right state: await loadTokensFile(input) + const { hasWalletSettings = false } = walletState.currencyInfo + if (hasWalletSettings) { + await loadWalletSettingsFile(input) + } + // Start the engine: const accountState = state.accounts[accountId] const engine = await plugin.makeCurrencyEngine(publicWalletInfo, { @@ -138,7 +149,8 @@ export const walletPixie: TamePixie = combinePixies({ // User settings: customTokens: accountState.customTokens[pluginId] ?? {}, enabledTokenIds: input.props.walletState.allEnabledTokenIds, - userSettings: accountState.userSettings[pluginId] ?? {} + userSettings: accountState.userSettings[pluginId] ?? {}, + walletSettings: input.props.walletState.walletSettings }) input.onOutput(engine) @@ -462,7 +474,8 @@ export const walletPixie: TamePixie = combinePixies({ watcher(input: CurrencyWalletInput) { let lastState: CurrencyWalletState | undefined - let lastSettings: object = {} + let lastUserSettings: object = {} + let lastWalletSettings: JsonObject = initialWalletSettings let lastTokens: EdgeTokenMap = {} let lastEnabledTokenIds: string[] = initialTokenIds @@ -480,11 +493,12 @@ export const walletPixie: TamePixie = combinePixies({ lastState = walletState // Update engine settings: - const userSettings = accountState.userSettings[pluginId] ?? lastSettings - if (lastSettings !== userSettings && engine != null) { + const userSettings = + accountState.userSettings[pluginId] ?? lastUserSettings + if (lastUserSettings !== userSettings && engine != null) { await engine.changeUserSettings(userSettings) } - lastSettings = userSettings + lastUserSettings = userSettings // Update the custom tokens: const customTokens = accountState.customTokens[pluginId] ?? lastTokens @@ -505,6 +519,26 @@ export const walletPixie: TamePixie = combinePixies({ } lastTokens = customTokens + // Update wallet-scoped settings: + const { hasWalletSettings = false } = walletState.currencyInfo + const { walletSettings } = walletState + const settingsChanged = lastWalletSettings !== walletSettings + + if ( + settingsChanged && + engine?.changeWalletSettings != null && + hasWalletSettings + ) { + input.props.log.warn( + `walletSettings applying: ${JSON.stringify(walletSettings)}` + ) + await engine.changeWalletSettings(walletSettings).catch(error => { + input.props.log.warn(`walletSettings error: ${String(error)}`) + input.props.onError(error) + }) + } + lastWalletSettings = walletSettings + // Update enabled tokens: const { allEnabledTokenIds } = walletState if (lastEnabledTokenIds !== allEnabledTokenIds && engine != null) { diff --git a/src/core/currency/wallet/currency-wallet-reducer.ts b/src/core/currency/wallet/currency-wallet-reducer.ts index b9912277a..1cf017ae8 100644 --- a/src/core/currency/wallet/currency-wallet-reducer.ts +++ b/src/core/currency/wallet/currency-wallet-reducer.ts @@ -13,7 +13,8 @@ import { EdgeTransaction, EdgeTxAction, EdgeWalletInfo, - EdgeWalletInfoFull + EdgeWalletInfoFull, + JsonObject } from '../../../types/types' import { compare } from '../../../util/compare' import { RootAction } from '../../actions' @@ -75,6 +76,7 @@ export interface CurrencyWalletState { readonly enabledTokenIds: string[] readonly tokenFileDirty: boolean readonly tokenFileLoaded: boolean + readonly walletSettings: JsonObject readonly engineFailure: Error | null readonly engineStarted: boolean readonly fiat: string @@ -118,6 +120,8 @@ export interface CurrencyWalletNext { readonly self: CurrencyWalletState } +export const initialWalletSettings: JsonObject = {} + // Used for detectedTokenIds & enabledTokenIds: export const initialTokenIds: string[] = [] @@ -250,6 +254,16 @@ const currencyWalletInner = buildReducer< } }, + walletSettings(state = initialWalletSettings, action): JsonObject { + switch (action.type) { + case 'CURRENCY_WALLET_LOADED_WALLET_SETTINGS_FILE': + case 'CURRENCY_WALLET_CHANGED_WALLET_SETTINGS': + return action.payload.walletSettings + default: + return state + } + }, + engineFailure(state = null, action): Error | null { if (action.type === 'CURRENCY_ENGINE_FAILED') { const { error } = action.payload diff --git a/src/core/login/splitting.ts b/src/core/login/splitting.ts index 1a0591372..0fac7ca63 100644 --- a/src/core/login/splitting.ts +++ b/src/core/login/splitting.ts @@ -172,18 +172,14 @@ export async function splitWalletInfo( // Restore anything that has simply been deleted: if (toRestore.length > 0) { const newStates: EdgeWalletStates = {} - let hasChanges = false for (const existingWalletInfo of toRestore) { - if (existingWalletInfo.archived || existingWalletInfo.deleted) { - hasChanges = true - newStates[existingWalletInfo.id] = { - archived: false, - deleted: false, - migratedFromWalletId: existingWalletInfo.migratedFromWalletId - } + newStates[existingWalletInfo.id] = { + archived: false, + deleted: false, + migratedFromWalletId: existingWalletInfo.migratedFromWalletId } } - if (hasChanges) await changeWalletStates(ai, accountId, newStates) + await changeWalletStates(ai, accountId, newStates) } // Add the keys to the login: diff --git a/src/types/types.ts b/src/types/types.ts index 3127ec66c..0d47cb191 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -543,6 +543,7 @@ export interface EdgeCurrencyInfo { unsafeMakeSpend?: boolean unsafeSyncNetwork?: boolean usesChangeServer?: boolean + hasWalletSettings?: boolean /** Show the total sync percentage with this many decimal digits */ syncDisplayPrecision?: number @@ -1043,10 +1044,12 @@ export interface EdgeCurrencyEngineOptions { customTokens: EdgeTokenMap enabledTokenIds: string[] userSettings: JsonObject | undefined + walletSettings: JsonObject } export interface EdgeCurrencyEngine { readonly changeUserSettings: (settings: JsonObject) => Promise + readonly changeWalletSettings?: (settings: JsonObject) => Promise /** * Starts any persistent resources the engine needs, such as WebSockets. @@ -1329,6 +1332,7 @@ export interface EdgeCurrencyWallet { readonly created: Date | undefined readonly disklet: Disklet readonly id: string + readonly imported: boolean readonly localDisklet: Disklet readonly publicWalletInfo: EdgeWalletInfo readonly sync: () => Promise @@ -1346,6 +1350,10 @@ export interface EdgeCurrencyWallet { readonly currencyConfig: EdgeCurrencyConfig // eslint-disable-line no-use-before-define readonly currencyInfo: EdgeCurrencyInfo + // User settings for this wallet: + readonly walletSettings: JsonObject + readonly changeWalletSettings: (settings: JsonObject) => Promise + // Chain state: readonly balanceMap: EdgeBalanceMap readonly balances: EdgeBalances @@ -1651,6 +1659,7 @@ export interface EdgeCreateCurrencyWalletOptions { enabledTokenIds?: string[] fiatCurrencyCode?: string name?: string + walletSettings?: JsonObject // Create a private key from some text: importText?: string