From fd4741019619386d17b93215bceacaa8222fc047 Mon Sep 17 00:00:00 2001 From: Igor Artamonov Date: Mon, 23 Dec 2024 20:08:19 +0000 Subject: [PATCH] problem: UI is too slow just after app launch if user has a very active wallet, resulting in too many state updates for the UI solution: buffer changes (history, balances, and allowance) and update only with large chunks on data --- .../services/src/services/AllowanceService.ts | 62 +++++-- .../services/src/services/BuffereHandler.ts | 76 ++++++++ .../src/services/TransactionService.ts | 127 +++++++------ .../src/services/balance/BalanceListener.ts | 2 +- .../src/services/balance/BalanceService.ts | 170 ++++++++++++------ packages/store/src/accounts/actions.ts | 2 +- packages/store/src/accounts/reducer.ts | 13 +- packages/store/src/accounts/types.ts | 2 +- packages/store/src/allowances/actions.ts | 3 +- packages/store/src/allowances/reducer.ts | 109 +++++------ packages/store/src/allowances/types.ts | 6 +- packages/store/src/tokens/actions.ts | 2 +- packages/store/src/tokens/reducer.ts | 25 +-- packages/store/src/tokens/types.ts | 2 +- packages/store/src/txhistory/actions.ts | 5 +- packages/store/src/txhistory/reducer.ts | 48 +++-- packages/store/src/txhistory/types.ts | 8 +- 17 files changed, 422 insertions(+), 240 deletions(-) create mode 100644 packages/services/src/services/BuffereHandler.ts diff --git a/packages/services/src/services/AllowanceService.ts b/packages/services/src/services/AllowanceService.ts index 0b9c46dc2..0a5569c62 100644 --- a/packages/services/src/services/AllowanceService.ts +++ b/packages/services/src/services/AllowanceService.ts @@ -1,4 +1,4 @@ -import { Publisher, token as TokenApi } from '@emeraldpay/api'; +import {address as AddressApi, Publisher, token as TokenApi} from '@emeraldpay/api'; import { EntryId } from '@emeraldpay/emerald-vault-core'; import { extractWalletId } from '@emeraldpay/emerald-vault-core/lib/types'; import { @@ -18,6 +18,7 @@ import { EmeraldApiAccess } from '../emerald-client/ApiAccess'; import { BalanceService } from './balance/BalanceService'; import { Service } from './ServiceManager'; import {isBlockchainId} from "@emeraldwallet/core"; +import {BufferedHandler} from "./BuffereHandler"; interface Subscription { address: string; @@ -29,6 +30,20 @@ function isEqual(first: Subscription, second: Subscription): boolean { return first.address === second.address && first.blockchain === second.blockchain && first.entryId === second.entryId; } +// from packages/store/src/allowances/types.ts +type Allowance = { + address: string; + allowance: string; + available: string; + blockchain: BlockchainCode; + contractAddress: string; + ownerAddress: string; + spenderAddress: string; + timestamp: number; + ownerControl?: AddressApi.AddressControl; + spenderControl?: AddressApi.AddressControl; +} + type AllowanceHandler = (allowance: TokenApi.AddressAllowanceAmount) => void; const log = Logger.forCategory('AllowanceService'); @@ -49,6 +64,8 @@ export class AllowanceService implements Service { private subscribers: Map> = new Map(); private subscriptions: Subscription[] = []; + private buffer: BufferedHandler; + constructor( ipcMain: IpcMain, apiAccess: EmeraldApiAccess, @@ -66,6 +83,9 @@ export class AllowanceService implements Service { this.tokens = settings.getTokens(); this.tokenRegistry = new TokenRegistry(this.tokens); + this.buffer = new BufferedHandler(this.submitAllowances()); + this.buffer.start(); + ipcMain.handle(IpcCommands.ALLOWANCE_SET_TOKENS, (event, tokens) => { this.tokens = tokens.filter((token: TokenData) => isBlockchainId(token.blockchain)); this.tokenRegistry = new TokenRegistry(this.tokens); @@ -179,26 +199,30 @@ export class AllowanceService implements Service { this.apiAccess.addressClient.describe({ blockchain, address: ownerAddress }), this.apiAccess.addressClient.describe({ blockchain, address: spenderAddress }), ]).then(([{ control: ownerControl }, { control: spenderControl }]) => - this.webContents.send(IpcCommands.STORE_DISPATCH, { - type: 'WALLET/ALLOWANCE/SET_ALLOWANCE', - payload: { - allowance: { - address, - allowance, - available, - contractAddress, - ownerAddress, - ownerControl, - spenderAddress, - spenderControl, - blockchain: blockchainCode, - timestamp: Date.now(), - }, - tokens: this.tokens, - }, - }), + this.buffer.onData({ + address, + allowance, + available, + contractAddress, + ownerAddress, + ownerControl, + spenderAddress, + spenderControl, + blockchain: blockchainCode, + timestamp: Date.now(), + }) ); }); }; } + + private submitAllowances() { + return async (values: Allowance[]) => { + this.webContents.send(IpcCommands.STORE_DISPATCH, { + type: 'WALLET/ALLOWANCE/SET_ALLOWANCE', + allowances: values, + tokens: this.tokens, + }); + }; + } } diff --git a/packages/services/src/services/BuffereHandler.ts b/packages/services/src/services/BuffereHandler.ts new file mode 100644 index 000000000..d2ab31043 --- /dev/null +++ b/packages/services/src/services/BuffereHandler.ts @@ -0,0 +1,76 @@ +/** + * A handler that buffers incoming data and flushes it to the handler when the buffer is full or a certain time has passed. + */ +export class BufferedHandler { + + /** + * The maximum size of the buffer before it is flushed. + * @private + */ + private readonly limitSize: number = 100; + + /** + * The maximum time in milliseconds before the buffer is flushed. + * @private + */ + private readonly limitTimeMs: number = 250; + /** + * The handler that is called when the buffer is flushed. + * @private + */ + private readonly handler: (values: T[]) => Promise; + + private buffer: T[] = []; + private closed = false; + private lastFlush = Date.now(); + + constructor(handler: (values: T[]) => Promise, limitSize?: number, limitTimeMs?: number) { + this.handler = handler; + if (limitSize != null) { + this.limitSize = limitSize; + } + if (limitTimeMs != null) { + this.limitTimeMs = limitTimeMs; + } + } + + start() { + this.scheduleNext(); + } + + private scheduleNext() { + setTimeout(() => { + if (this.lastFlush + this.limitTimeMs >= Date.now()) { + this.flush(); + } + if (!this.closed) { + this.scheduleNext(); + } + }, this.limitTimeMs - (Date.now() - this.lastFlush)); + } + + accept(): (tx: T) => void { + return (tx) => this.onData(tx); + } + + onData(tx: T): void { + this.buffer.push(tx); + if (this.buffer.length >= this.limitSize) { + this.flush(); + } + } + + flush(): void { + if (this.buffer.length > 0) { + this.handler(this.buffer) + .catch((e) => console.error('Error while handling buffer', e)); + } + this.buffer = []; + this.lastFlush = Date.now(); + } + + close() { + this.flush(); + this.closed = true; + } +} diff --git a/packages/services/src/services/TransactionService.ts b/packages/services/src/services/TransactionService.ts index 698087b2a..0cb0119f5 100644 --- a/packages/services/src/services/TransactionService.ts +++ b/packages/services/src/services/TransactionService.ts @@ -14,12 +14,13 @@ import { PersistentStateManager } from '@emeraldwallet/persistent-state'; import { IpcMain, WebContents } from 'electron'; import { EmeraldApiAccess } from '../emerald-client/ApiAccess'; import { Service } from './ServiceManager'; +import {BufferedHandler} from "./BuffereHandler"; const { ChangeType: ApiType, Direction: ApiDirection } = TransactionApi; const { ChangeType: StateType, Direction: StateDirection, State, Status } = PersistentState; type EntryIdentifier = { entryId: string; blockchain: number; identifier: string }; -type TransactionHandler = (tx: TransactionApi.AddressTransaction) => void; +type TransactionHandler = (tx: TransactionApi.AddressTransaction[]) => Promise; const log = Logger.forCategory('TransactionService'); @@ -118,17 +119,19 @@ export class TransactionService implements Service { this.persistentState.txhistory .query({ state: State.SUBMITTED }) - .then(({ items: transactions }) => - Promise.all( - transactions.map(({ blockchain: txBlockchain, txId }) => - this.persistentState.txhistory.remove(txBlockchain, txId).then(() => - this.webContents.send(IpcCommands.STORE_DISPATCH, { - type: 'WALLET/HISTORY/REMOVE_STORED_TX', - txId, - }), + .then(({ items: transactions }) => { + let removePromise = Promise.all( + transactions.map(({blockchain: txBlockchain, txId}) => + this.persistentState.txhistory.remove(txBlockchain, txId) ), - ), - ), + ); + removePromise.then(() => { + this.webContents.send(IpcCommands.STORE_DISPATCH, { + type: 'WALLET/HISTORY/REMOVE_STORED_TX', + txIds: transactions.map((tx) => tx.txId), + }) + }) + }, ) .then(() => entryIdentifiers.forEach(({ blockchain, entryId, identifier }) => @@ -157,6 +160,8 @@ export class TransactionService implements Service { private subscribe(identifier: string, blockchain: number, entryId: string): void { const blockchainCode = blockchainIdToCode(blockchain); + if (blockchain != 0) return; + this.persistentState.txhistory .getCursor(identifier) .then((cursor) => { @@ -172,29 +177,32 @@ export class TransactionService implements Service { }; const handler = this.processTransaction(identifier, blockchain, entryId); + const buffer = new BufferedHandler(handler); + buffer.start(); this.apiAccess.transactionClient .getTransactions(request) - .onData(handler) - .onError((error) => - log.error( - `Error while getting transactions for ${identifier} on ${blockchainCode},`, - `restart after ${this.restartTimeout} seconds...`, - error, - ), - ); + .onData(buffer.accept()) + .onError((error) => { + log.error( + `Error while getting transactions for ${identifier} on ${blockchainCode},`, + `restart after ${this.restartTimeout} seconds...`, + error, + ); + buffer.flush(); + }); const subscriber = this.apiAccess.transactionClient .subscribeTransactions(request) - .onData(handler) + .onData(buffer.accept()) .onError((error) => log.error(`Error while subscribing for ${identifier} on ${blockchainCode}`, error)) .finally(() => { log.info( `Subscription for ${identifier} on ${blockchainCode} is closed,`, `restart after ${this.restartTimeout} seconds...`, ); - setTimeout(() => this.subscribe(identifier, blockchain, entryId), this.restartTimeout * 1000); + buffer.flush(); }); this.subscribers.set(identifier, subscriber); @@ -205,22 +213,25 @@ export class TransactionService implements Service { private processTransaction(identifier: string, blockchain: number, entryId: string): TransactionHandler { const blockchainCode = blockchainIdToCode(blockchain); - return (tx) => { - log.info(`Receive transaction ${tx.txId} for ${identifier} on ${blockchainCode}...`); + return async (txs: TransactionApi.AddressTransaction[]) => { + let removed = txs.filter((tx) => tx.removed); + let added = txs.filter((tx) => !tx.removed); - if (tx.removed) { + log.info(`Receive transactions ${txs.length} (+${added.length}, -${removed.length}) for ${identifier} on ${blockchainCode}...`); + + removed.forEach((tx) => { this.persistentState.txhistory .remove(blockchain, tx.txId) - .then(() => - this.webContents.send(IpcCommands.STORE_DISPATCH, { - type: 'WALLET/HISTORY/REMOVE_STORED_TX', - txId: tx.txId, - }), - ) .catch((error) => log.error(`Error while removing transaction for ${identifier} on ${blockchainCode} from state`, error), ); - } else { + }); + this.webContents.send(IpcCommands.STORE_DISPATCH, { + type: 'WALLET/HISTORY/REMOVE_STORED_TX', + txIds: removed.map((tx) => tx.txId), + }); + + let mergedPromise = added.map((tx) => { let confirmation: PersistentState.TransactionConfirmation | null = null; const now = new Date(); @@ -257,38 +268,42 @@ export class TransactionService implements Service { txId: tx.txId, }; - this.persistentState.txhistory + return this.persistentState.txhistory .submit({ ...confirmation, ...transaction }) .then((merged) => { if (tx.cursor != null && tx.cursor.length > 0) { - this.persistentState.txhistory + return this.persistentState.txhistory .setCursor(identifier, tx.cursor) .catch((error) => log.error(`Error while set cursor ${tx.cursor} for ${identifier} on ${blockchainCode}`, error), - ); + ) + .then(() => merged); + } else { + return Promise.resolve(merged) } - - const walletId = EntryIdOp.of(entryId).extractWalletId(); - - this.persistentState.txmeta - .get(blockchainCode, merged.txId) - .then((meta) => - this.webContents.send(IpcCommands.STORE_DISPATCH, { - meta, - walletId, - type: 'WALLET/HISTORY/UPDATE_STORED_TX', - tokens: this.tokens, - transaction: merged, - }), - ) - .catch((error) => - log.error(`Error while getting transaction meta for ${identifier} on ${blockchainCode}`, error), - ); }) - .catch((error) => - log.error(`Error while submitting transaction data for ${identifier} on ${blockchainCode}`, error), - ); - } - }; + }); + + const merged = await Promise.all(mergedPromise); + const meta = await Promise.all( + merged.map((tx) => + this.persistentState.txmeta + .get(blockchainCode, tx.txId) + .catch((error) => { + log.error(`Error while getting transaction meta for ${identifier} on ${blockchainCode}`, error); + return null; + }) + )); + + const walletId = EntryIdOp.of(entryId).extractWalletId(); + this.webContents.send(IpcCommands.STORE_DISPATCH, { + transactions: merged.map((tx, index) => { + return { meta: meta[index], transaction: tx }; + }), + type: 'WALLET/HISTORY/UPDATE_STORED_TX', + tokens: this.tokens, + walletId, + }); + } } } diff --git a/packages/services/src/services/balance/BalanceListener.ts b/packages/services/src/services/balance/BalanceListener.ts index d51e02fd6..787e606e6 100644 --- a/packages/services/src/services/balance/BalanceListener.ts +++ b/packages/services/src/services/balance/BalanceListener.ts @@ -2,7 +2,7 @@ import { AddressBalance, AnyAsset, BalanceRequest, Publisher, Utxo } from '@emer import { BlockchainClient } from '@emeraldpay/api-node'; import { BlockchainCode, EthereumAddress, Logger, blockchainCodeToId, isBitcoin } from '@emeraldwallet/core'; -interface AddressEvent { +export interface AddressEvent { address: string; balance: string; asset: AnyAsset; diff --git a/packages/services/src/services/balance/BalanceService.ts b/packages/services/src/services/balance/BalanceService.ts index 632c5def4..c8b2e412b 100644 --- a/packages/services/src/services/balance/BalanceService.ts +++ b/packages/services/src/services/balance/BalanceService.ts @@ -1,4 +1,4 @@ -import { isAsset } from '@emeraldpay/api'; +import {AnyAsset, isAsset, Utxo} from '@emeraldpay/api'; import { EntryId } from '@emeraldpay/emerald-vault-core'; import { BlockchainCode, @@ -8,14 +8,16 @@ import { SettingsManager, TokenRegistry, amountFactory, - blockchainCodeToId, + blockchainCodeToId, CoinTicker, InputUtxo, } from '@emeraldwallet/core'; import { PersistentStateManager } from '@emeraldwallet/persistent-state'; import { IpcMain, WebContents } from 'electron'; import { EmeraldApiAccess } from '../..'; import { PriceService } from '../price/PriceService'; import { Service } from '../ServiceManager'; -import { BalanceListener } from './BalanceListener'; +import {AddressEvent, BalanceListener} from './BalanceListener'; +import {BigAmount, CreateAmount} from "@emeraldpay/bigamount"; +import {BufferedHandler} from "../BuffereHandler"; interface Subscription { address: string; @@ -24,6 +26,25 @@ interface Subscription { entryId: EntryId; } +// same as in packages/store/src/accounts/types.ts +interface AccountBalance { + address: string; + entryId: EntryId; + balance: string; + utxo?: InputUtxo[]; +} +// same as in packages/store/src/tokens/types.ts +interface TokenBalance { + address: string; + blockchain: BlockchainCode; + balance: { + decimals: number; + symbol: string; + unitsValue: string; + }; + contractAddress: string; +} + function isEqual(first: Subscription, second: Subscription): boolean { return ( first.address === second.address && @@ -48,6 +69,9 @@ export class BalanceService implements Service { private subscribers: Map = new Map(); private subscriptions: Subscription[] = []; + private nativeBuffer: BufferedHandler; + private erc20Buffer: BufferedHandler; + constructor( ipcMain: IpcMain, apiAccess: EmeraldApiAccess, @@ -63,6 +87,11 @@ export class BalanceService implements Service { this.tokenRegistry = new TokenRegistry(settings.getTokens()); this.webContents = webContents; + this.nativeBuffer = new BufferedHandler(this.submitManyAccountBalances()); + this.nativeBuffer.start(); + this.erc20Buffer = new BufferedHandler(this.submitManyTokenBalances()); + this.erc20Buffer.start(); + ipcMain.handle(IpcCommands.BALANCE_SET_TOKENS, (event, tokens) => { this.tokenRegistry = new TokenRegistry(tokens); @@ -118,9 +147,18 @@ export class BalanceService implements Service { const factory = amountFactory(blockchain); - subscriber.subscribe(blockchain, address, asset, ({ balance, utxo, address: eventAddress, asset: eventAsset }) => { + subscriber.subscribe(blockchain, address, asset, this.createHandler(blockchain, factory, entryId)); + + const id = `${entryId}:${address}:${asset}`; + + this.subscribers.get(id)?.stop(); + this.subscribers.set(id, subscriber); + } + + private createHandler(blockchain: BlockchainCode, factory: CreateAmount, entryId: string): (event: AddressEvent) => void { + return ({balance, utxo, address: eventAddress, asset: eventAsset}) => { const { - params: { coinTicker }, + params: {coinTicker}, } = Blockchains[blockchain]; this.persistentState.balances @@ -129,61 +167,81 @@ export class BalanceService implements Service { amount: balance, asset: isAsset(eventAsset) ? coinTicker : eventAsset.contractAddress, blockchain: blockchainCodeToId(blockchain), - utxo: utxo?.map(({ txid, vout, value: amount }) => ({ amount, txid, vout })), + utxo: utxo?.map(({txid, vout, value: amount}) => ({amount, txid, vout})), }) .then(() => { - if (isAsset(eventAsset)) { - const amount = factory(balance); - - if (amount.isPositive()) { - this.priceService.addFrom(coinTicker); - } - - this.webContents.send(IpcCommands.STORE_DISPATCH, { - type: 'ACCOUNT/SET_BALANCE', - payload: { - entryId, - address: eventAddress, - balance: amount.encode(), - utxo: utxo?.map((utxo) => ({ - address: eventAddress, - txid: utxo.txid, - value: factory(utxo.value).encode(), - vout: utxo.vout, - })), - }, - }); - } else { - const { contractAddress } = eventAsset; - - if (this.tokenRegistry.hasAddress(blockchain, contractAddress)) { - const token = this.tokenRegistry.byAddress(blockchain, contractAddress); - - if (token.pinned || token.getAmount(balance).isPositive()) { - this.priceService.addFrom(contractAddress, blockchain); - } - - this.webContents.send(IpcCommands.STORE_DISPATCH, { - type: 'TOKENS/SET_TOKEN_BALANCE', - payload: { - blockchain, - address: eventAddress, - balance: { - decimals: token.decimals, - symbol: token.symbol, - unitsValue: balance, - }, - contractAddress: token.address, - }, - }); - } - } + this.sendAsset(entryId, eventAsset, blockchain, balance, utxo, eventAddress, factory, coinTicker); }); - }); + }; + } - const id = `${entryId}:${address}:${asset}`; + private sendAsset(entryId: string, eventAsset: AnyAsset, blockchain: BlockchainCode, balance: string, utxo: Utxo[] | undefined, eventAddress: string, + factory: CreateAmount, coinTicker: CoinTicker): void { + if (isAsset(eventAsset)) { + this.sendNativeAsset(entryId, eventAsset, blockchain, balance, utxo, eventAddress, factory, coinTicker); + } else { + this.sendERC20Asset(entryId, eventAsset, blockchain, balance, eventAddress); + } + } - this.subscribers.get(id)?.stop(); - this.subscribers.set(id, subscriber); + private sendNativeAsset(entryId: string, eventAsset: AnyAsset, blockchain: BlockchainCode, balance: string, utxo: Utxo[] | undefined, eventAddress: string, factory: CreateAmount, coinTicker: CoinTicker): void { + const amount = factory(balance); + + if (amount.isPositive()) { + this.priceService.addFrom(coinTicker); + } + + this.nativeBuffer.onData({ + entryId, + address: eventAddress, + balance: amount.encode(), + utxo: utxo?.map((utxo) => ({ + address: eventAddress, + txid: utxo.txid, + value: factory(utxo.value).encode(), + vout: utxo.vout, + })), + }); + } + + private sendERC20Asset(entryId: string, eventAsset: AnyAsset, blockchain: BlockchainCode, balance: string, eventAddress: string): void { + // @ts-ignore + const {contractAddress} = eventAsset; + + if (this.tokenRegistry.hasAddress(blockchain, contractAddress)) { + const token = this.tokenRegistry.byAddress(blockchain, contractAddress); + + if (token.pinned || token.getAmount(balance).isPositive()) { + this.priceService.addFrom(contractAddress, blockchain); + } + this.erc20Buffer.onData({ + blockchain, + address: eventAddress, + balance: { + decimals: token.decimals, + symbol: token.symbol, + unitsValue: balance, + }, + contractAddress: token.address, + }); + } + } + + private submitManyAccountBalances(): (values: AccountBalance[]) => Promise { + return async (values: AccountBalance[]) => { + this.webContents.send(IpcCommands.STORE_DISPATCH, { + type: 'ACCOUNT/SET_BALANCE', + payload: values, + }); + }; + } + + private submitManyTokenBalances(): (values: TokenBalance[]) => Promise { + return async (values: TokenBalance[]) => { + this.webContents.send(IpcCommands.STORE_DISPATCH, { + type: 'TOKENS/SET_TOKEN_BALANCE', + payload: values, + }); + }; } } diff --git a/packages/store/src/accounts/actions.ts b/packages/store/src/accounts/actions.ts index 676cc1036..8328f39e9 100644 --- a/packages/store/src/accounts/actions.ts +++ b/packages/store/src/accounts/actions.ts @@ -64,7 +64,7 @@ export function initState( export function setBalanceAction(balance: AccountBalance): SetBalanceAction { return { type: ActionTypes.SET_BALANCE, - payload: balance, + payload: [balance], }; } diff --git a/packages/store/src/accounts/reducer.ts b/packages/store/src/accounts/reducer.ts index 4046012bf..c2c95dc18 100644 --- a/packages/store/src/accounts/reducer.ts +++ b/packages/store/src/accounts/reducer.ts @@ -2,6 +2,7 @@ import { EntryId, Uuid, Wallet } from '@emeraldpay/emerald-vault-core'; import { amountFactory, blockchainIdToCode } from '@emeraldwallet/core'; import produce from 'immer'; import { + AccountBalance, AccountDetails, AccountsAction, AccountsState, @@ -119,9 +120,15 @@ function updateAccountDetails( } function onSetBalance(state: AccountsState, action: SetBalanceAction): AccountsState { - const { address, balance, entryId, utxo } = action.payload; - - return updateAccountDetails(state, address, entryId, (account) => ({ ...account, balance, utxo })); + let updated: AccountsState = state; + for (const balance of action.payload) { + updated = updateAccountDetails(updated, balance.address, balance.entryId, (account) => ({ + ...account, + balance: balance.balance, + utxo: balance.utxo, + })); + } + return updated; } function onWalletsLoaded(state: AccountsState, action: WalletsLoadedAction): AccountsState { diff --git a/packages/store/src/accounts/types.ts b/packages/store/src/accounts/types.ts index 09221d404..6554042a2 100644 --- a/packages/store/src/accounts/types.ts +++ b/packages/store/src/accounts/types.ts @@ -121,7 +121,7 @@ export interface PendingBalanceAction { export interface SetBalanceAction { type: ActionTypes.SET_BALANCE; - payload: AccountBalance; + payload: AccountBalance[]; } export interface SetLoadingAction { diff --git a/packages/store/src/allowances/actions.ts b/packages/store/src/allowances/actions.ts index ce08c04f8..e0fef4ffa 100644 --- a/packages/store/src/allowances/actions.ts +++ b/packages/store/src/allowances/actions.ts @@ -23,6 +23,7 @@ export function initAddressAllowance(allowance: AllowanceRaw): Dispatched 0) { - const index = allowances.findIndex((item) => item.spenderAddress.toLowerCase() === spenderAddress.toLowerCase()); + if (allowances.length > 0) { + const index = allowances.findIndex((item) => item.spenderAddress.toLowerCase() === spenderAddress.toLowerCase()); - if (index > -1) { - allowances[index] = newAllowance; + if (index > -1) { + allowances[index] = newAllowance; + } else { + allowances.push(newAllowance); + } } else { allowances.push(newAllowance); } - } else { - allowances.push(newAllowance); - } - return produce(state, (draft) => { - draft[blockchain] = { - ...draft[blockchain], - [allowanceAddress]: { - ...draft[blockchain]?.[allowanceAddress], - [type]: { - ...draft[blockchain]?.[allowanceAddress]?.[type], - [allowanceContractAddress]: allowances, + updatedState = produce(updatedState, (draft) => { + draft[blockchain] = { + ...draft[blockchain], + [allowanceAddress]: { + ...draft[blockchain]?.[allowanceAddress], + [type]: { + ...draft[blockchain]?.[allowanceAddress]?.[type], + [allowanceContractAddress]: allowances, + }, }, - }, - }; - }); + }; + }); + } } - return state; + return updatedState; } function initAllowance(state: AllowanceState, { payload }: InitAllowanceAction): AllowanceState { @@ -102,7 +105,7 @@ function initAllowance(state: AllowanceState, { payload }: InitAllowanceAction): ); if (allowance == null) { - return setAllowance(state, { payload, type: ActionTypes.SET_ALLOWANCE }); + return setAllowance(state, { allowances: [payload.allowance], tokens: payload.tokens, type: ActionTypes.SET_ALLOWANCE }); } return state; diff --git a/packages/store/src/allowances/types.ts b/packages/store/src/allowances/types.ts index ad9e8c1f6..a67d3f366 100644 --- a/packages/store/src/allowances/types.ts +++ b/packages/store/src/allowances/types.ts @@ -93,10 +93,8 @@ export interface RemoveAllowanceAction { export interface SetAllowanceAction { type: ActionTypes.SET_ALLOWANCE; - payload: { - allowance: AllowanceCommon; - tokens: TokenData[]; - }; + allowances: AllowanceCommon[]; + tokens: TokenData[]; } export type AllowanceAction = InitAllowanceAction | RemoveAllowanceAction | SetAllowanceAction; diff --git a/packages/store/src/tokens/actions.ts b/packages/store/src/tokens/actions.ts index ba35884da..9fadec528 100644 --- a/packages/store/src/tokens/actions.ts +++ b/packages/store/src/tokens/actions.ts @@ -23,6 +23,6 @@ export function setTokenBalance( ): SetTokenBalanceAction { return { type: ActionTypes.SET_TOKEN_BALANCE, - payload: { address, blockchain, balance, contractAddress }, + payload: [{ address, blockchain, balance, contractAddress }], }; } diff --git a/packages/store/src/tokens/reducer.ts b/packages/store/src/tokens/reducer.ts index 0901b43b3..2d3075d6d 100644 --- a/packages/store/src/tokens/reducer.ts +++ b/packages/store/src/tokens/reducer.ts @@ -40,19 +40,22 @@ function onInitState(state: TokensState, { payload: { balances, tokens } }: Init } function onSetTokenBalance(state: TokensState, action: SetTokenBalanceAction): TokensState { - const { address, blockchain, balance, contractAddress } = action.payload; + let updated = state; + for (const { address, balance, blockchain, contractAddress } of action.payload) { + const balanceAddress = address.toLowerCase(); - const balanceAddress = address.toLowerCase(); + updated = produce(updated, (draft) => { + draft.balances[blockchain] = { + ...draft.balances[blockchain], + [balanceAddress]: { + ...draft.balances[blockchain]?.[balanceAddress], + [contractAddress.toLowerCase()]: { ...balance }, + }, + }; + }); + } - return produce(state, (draft) => { - draft.balances[blockchain] = { - ...draft.balances[blockchain], - [balanceAddress]: { - ...draft.balances[blockchain]?.[balanceAddress], - [contractAddress.toLowerCase()]: { ...balance }, - }, - }; - }); + return updated; } export function reducer(state: TokensState = INITIAL_STATE, action: TokensAction): TokensState { diff --git a/packages/store/src/tokens/types.ts b/packages/store/src/tokens/types.ts index a927ab7a3..21ecde3c9 100644 --- a/packages/store/src/tokens/types.ts +++ b/packages/store/src/tokens/types.ts @@ -52,7 +52,7 @@ export interface SetTokenBalanceAction { blockchain: BlockchainCode; balance: TokenBalance; contractAddress: string; - }; + }[]; } export type TokensAction = InitTokenStateAction | SetTokenBalanceAction; diff --git a/packages/store/src/txhistory/actions.ts b/packages/store/src/txhistory/actions.ts index a3488a8fa..e2a4f41dc 100644 --- a/packages/store/src/txhistory/actions.ts +++ b/packages/store/src/txhistory/actions.ts @@ -77,7 +77,7 @@ export function loadTransactions(walletId: Uuid, initial: boolean, limit = 20): export function removeTransaction(txId: string): RemoveStoredTxAction { return { - txId, + txIds: [txId], type: ActionTypes.REMOVE_STORED_TX, }; } @@ -93,9 +93,8 @@ export function updateTransaction( } = getState(); dispatch({ - meta, + transactions: [{ meta, transaction }], tokens, - transaction, walletId, type: ActionTypes.UPDATE_STORED_TX, }); diff --git a/packages/store/src/txhistory/reducer.ts b/packages/store/src/txhistory/reducer.ts index 9f40c19e2..a1c3be6fa 100644 --- a/packages/store/src/txhistory/reducer.ts +++ b/packages/store/src/txhistory/reducer.ts @@ -32,45 +32,41 @@ function onSetLastTxId(state: HistoryState, { txId }: LastTxIdAction): HistorySt return { ...state, lastTxId: txId }; } -function onRemoveStoreTransaction(state: HistoryState, { txId }: RemoveStoredTxAction): HistoryState { - return { ...state, transactions: state.transactions.filter((tx) => tx.txId !== txId) }; +function onRemoveStoreTransaction(state: HistoryState, { txIds }: RemoveStoredTxAction): HistoryState { + return { ...state, transactions: state.transactions + .filter((tx) => tx.txId !in txIds) }; } function onUpdateStoreTransaction( state: HistoryState, - { meta, tokens, transaction, walletId }: UpdateStoredTxAction, + { tokens, transactions, walletId }: UpdateStoredTxAction, ): HistoryState { + const tokenRegistry = new TokenRegistry(tokens.filter((token) => isBlockchainId(token.blockchain))); + const updatedTransactions = []; if (state.walletId === walletId) { - const tokenRegistry = new TokenRegistry(tokens.filter((token) => isBlockchainId(token.blockchain))); - - const storedTransaction = new StoredTransaction(tokenRegistry, transaction, meta); - const storedTransactions = [...state.transactions]; - - if (storedTransactions.length === 0) { - return { - ...state, - transactions: [storedTransaction], - }; + for (const prevTransaction of state.transactions) { + const update = transactions.find((tx) => tx.transaction.txId === prevTransaction.txId); + if (update) { + const storedTransaction = new StoredTransaction(tokenRegistry, update.transaction, update.meta); + if ((prevTransaction.version ?? 0) > (storedTransaction.version ?? 0)) { + updatedTransactions.push(prevTransaction); + } else { + updatedTransactions.push(storedTransaction); + } + } else { + updatedTransactions.push(prevTransaction); + } } - const index = storedTransactions.findIndex((tx) => tx.txId === transaction.txId); - - if (index > -1) { - if ((storedTransactions[index].version ?? 0) > (storedTransaction.version ?? 0)) { - return state; + for (const tx of transactions) { + if (!state.transactions.find((prev) => prev.txId === tx.transaction.txId)) { + updatedTransactions.push(new StoredTransaction(tokenRegistry, tx.transaction, tx.meta)); } - - storedTransactions[index] = storedTransaction; - - return { - ...state, - transactions: storedTransactions, - }; } return { ...state, - transactions: [storedTransaction, ...state.transactions].sort((first, second) => { + transactions: updatedTransactions.sort((first, second) => { const { confirmTimestamp: firstConfirmTimestamp } = first; const { confirmTimestamp: secondConfirmTimestamp } = second; diff --git a/packages/store/src/txhistory/types.ts b/packages/store/src/txhistory/types.ts index ea451272e..9fe2d6845 100644 --- a/packages/store/src/txhistory/types.ts +++ b/packages/store/src/txhistory/types.ts @@ -143,14 +143,16 @@ export interface LoadStoredTxsAction { export interface RemoveStoredTxAction { type: ActionTypes.REMOVE_STORED_TX; - txId: string; + txIds: string[]; } export interface UpdateStoredTxAction { type: ActionTypes.UPDATE_STORED_TX; - meta: PersistentState.TxMetaItem | null; + transactions: { + transaction: PersistentState.Transaction; + meta: PersistentState.TxMetaItem | null; + }[] tokens: TokenData[]; - transaction: PersistentState.Transaction; walletId: Uuid; }