diff --git a/src/db.ts b/src/db.ts index efc3b78ad..098767b55 100644 --- a/src/db.ts +++ b/src/db.ts @@ -7,7 +7,7 @@ import { openDatabase, Database, Store, Index } from "./util/query"; * with each major change. When incrementing the major version, * the wallet should import data from the previous version. */ -const TALER_DB_NAME = "taler-walletdb-v2"; +const TALER_DB_NAME = "taler-walletdb-v3"; /** * Current database minor version, should be incremented diff --git a/src/operations/balance.ts b/src/operations/balance.ts index b5c1ec79e..6f9135028 100644 --- a/src/operations/balance.ts +++ b/src/operations/balance.ts @@ -145,7 +145,7 @@ export async function getBalances( ): Promise { logger.trace("starting to compute balance"); - return await ws.db.runWithReadTransaction( + const wbal = await ws.db.runWithReadTransaction( [ Stores.coins, Stores.refreshGroups, @@ -157,4 +157,8 @@ export async function getBalances( return getBalancesInsideTransaction(ws, tx); }, ); + + logger.trace("finished computing wallet balance"); + + return wbal; } diff --git a/src/operations/history.ts b/src/operations/history.ts index 669a6cf85..1271c56ef 100644 --- a/src/operations/history.ts +++ b/src/operations/history.ts @@ -172,6 +172,7 @@ export async function getHistory( Stores.purchases, Stores.refreshGroups, Stores.reserves, + Stores.reserveHistory, Stores.tips, Stores.withdrawalGroups, Stores.payEvents, @@ -384,8 +385,12 @@ export async function getHistory( type: ReserveType.Manual, }; } + const hist = await tx.get(Stores.reserveHistory, reserve.reservePub); + if (!hist) { + throw Error("inconsistent database"); + } const s = summarizeReserveHistory( - reserve.reserveTransactions, + hist.reserveTransactions, reserve.currency, ); history.push({ diff --git a/src/operations/reserves.ts b/src/operations/reserves.ts index a3c6d56a4..2bbb085d5 100644 --- a/src/operations/reserves.ts +++ b/src/operations/reserves.ts @@ -33,8 +33,8 @@ import { updateRetryInfoTimeout, ReserveUpdatedEventRecord, WalletReserveHistoryItemType, - PlanchetRecord, WithdrawalSourceType, + ReserveHistoryRecord, } from "../types/dbTypes"; import { Logger } from "../util/logging"; import { Amounts } from "../util/amounts"; @@ -114,11 +114,15 @@ export async function createReserve( lastSuccessfulStatusQuery: undefined, retryInfo: initRetryInfo(), lastError: undefined, - reserveTransactions: [], currency: req.amount.currency, }; - reserveRecord.reserveTransactions.push({ + const reserveHistoryRecord: ReserveHistoryRecord = { + reservePub: keypair.pub, + reserveTransactions: [], + }; + + reserveHistoryRecord.reserveTransactions.push({ type: WalletReserveHistoryItemType.Credit, expectedAmount: req.amount, }); @@ -161,7 +165,12 @@ export async function createReserve( const cr: CurrencyRecord = currencyRecord; const resp = await ws.db.runWithWriteTransaction( - [Stores.currencies, Stores.reserves, Stores.bankWithdrawUris], + [ + Stores.currencies, + Stores.reserves, + Stores.reserveHistory, + Stores.bankWithdrawUris, + ], async (tx) => { // Check if we have already created a reserve for that bankWithdrawStatusUrl if (reserveRecord.bankWithdrawStatusUrl) { @@ -188,6 +197,7 @@ export async function createReserve( } await tx.put(Stores.currencies, cr); await tx.put(Stores.reserves, reserveRecord); + await tx.put(Stores.reserveHistory, reserveHistoryRecord); const r: CreateReserveResponse = { exchange: canonExchange, reservePub: keypair.pub, @@ -462,7 +472,7 @@ async function updateReserve( const balance = Amounts.parseOrThrow(reserveInfo.balance); const currency = balance.currency; await ws.db.runWithWriteTransaction( - [Stores.reserves, Stores.reserveUpdatedEvents], + [Stores.reserves, Stores.reserveUpdatedEvents, Stores.reserveHistory], async (tx) => { const r = await tx.get(Stores.reserves, reservePub); if (!r) { @@ -472,14 +482,19 @@ async function updateReserve( return; } + const hist = await tx.get(Stores.reserveHistory, reservePub); + if (!hist) { + throw Error("inconsistent database"); + } + const newHistoryTransactions = reserveInfo.history.slice( - r.reserveTransactions.length, + hist.reserveTransactions.length, ); const reserveUpdateId = encodeCrock(getRandomBytes(32)); const reconciled = reconcileReserveHistory( - r.reserveTransactions, + hist.reserveTransactions, reserveInfo.history, ); @@ -514,9 +529,10 @@ async function updateReserve( r.retryInfo = initRetryInfo(false); } r.lastSuccessfulStatusQuery = getTimestampNow(); - r.reserveTransactions = reconciled.updatedLocalHistory; + hist.reserveTransactions = reconciled.updatedLocalHistory; r.lastError = undefined; await tx.put(Stores.reserves, r); + await tx.put(Stores.reserveHistory, hist); }, ); ws.notify({ type: NotificationType.ReserveUpdated }); @@ -602,17 +618,29 @@ async function depleteReserve( ws: InternalWalletState, reservePub: string, ): Promise { - const reserve = await ws.db.get(Stores.reserves, reservePub); + let reserve: ReserveRecord | undefined; + let hist: ReserveHistoryRecord | undefined; + await ws.db.runWithReadTransaction( + [Stores.reserves, Stores.reserveHistory], + async (tx) => { + reserve = await tx.get(Stores.reserves, reservePub); + hist = await tx.get(Stores.reserveHistory, reservePub); + }, + ); + if (!reserve) { return; } + if (!hist) { + throw Error("inconsistent database"); + } if (reserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) { return; } logger.trace(`depleting reserve ${reservePub}`); const summary = summarizeReserveHistory( - reserve.reserveTransactions, + hist.reserveTransactions, reserve.currency, ); @@ -674,7 +702,12 @@ async function depleteReserve( }; const success = await ws.db.runWithWriteTransaction( - [Stores.withdrawalGroups, Stores.reserves, Stores.planchets], + [ + Stores.withdrawalGroups, + Stores.reserves, + Stores.reserveHistory, + Stores.planchets, + ], async (tx) => { const newReserve = await tx.get(Stores.reserves, reservePub); if (!newReserve) { @@ -683,8 +716,12 @@ async function depleteReserve( if (newReserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) { return false; } + const newHist = await tx.get(Stores.reserveHistory, reservePub); + if (!newHist) { + throw Error("inconsistent database"); + } const newSummary = summarizeReserveHistory( - newReserve.reserveTransactions, + newHist.reserveTransactions, newReserve.currency, ); if ( @@ -703,7 +740,7 @@ async function depleteReserve( const sd = denomsForWithdraw.selectedDenoms[i]; for (let j = 0; j < sd.count; j++) { const amt = Amounts.add(sd.denom.value, sd.denom.feeWithdraw).amount; - newReserve.reserveTransactions.push({ + newHist.reserveTransactions.push({ type: WalletReserveHistoryItemType.Withdraw, expectedAmount: amt, }); @@ -712,6 +749,7 @@ async function depleteReserve( newReserve.reserveStatus = ReserveRecordStatus.DORMANT; newReserve.retryInfo = initRetryInfo(false); await tx.put(Stores.reserves, newReserve); + await tx.put(Stores.reserveHistory, newHist); await tx.put(Stores.withdrawalGroups, withdrawalRecord); return true; }, diff --git a/src/operations/withdraw.ts b/src/operations/withdraw.ts index 36b9f1c0f..e1c4ed57c 100644 --- a/src/operations/withdraw.ts +++ b/src/operations/withdraw.ts @@ -564,7 +564,7 @@ async function processWithdrawGroupImpl( // Withdraw coins in batches. // The batch size is relatively large - await processInBatches(genWork(), 50); + await processInBatches(genWork(), 10); } export async function getExchangeWithdrawalInfo( diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts index 26fcca9e8..4cf19a56e 100644 --- a/src/types/dbTypes.ts +++ b/src/types/dbTypes.ts @@ -210,6 +210,11 @@ export type WalletReserveHistoryItem = | WalletReserveHistoryRecoupItem | WalletReserveHistoryClosingItem; +export interface ReserveHistoryRecord { + reservePub: string; + reserveTransactions: WalletReserveHistoryItem[]; +} + /** * A reserve record as stored in the wallet's database. */ @@ -295,8 +300,6 @@ export interface ReserveRecord { * (either talking to the bank or the exchange). */ lastError: OperationError | undefined; - - reserveTransactions: WalletReserveHistoryItem[]; } /** @@ -1639,6 +1642,12 @@ export namespace Stores { } } + class ReserveHistoryStore extends Store { + constructor() { + super("reserveHistory", { keyPath: "reservePub" }); + } + } + class TipsStore extends Store { constructor() { super("tips", { keyPath: "tipId" }); @@ -1725,6 +1734,7 @@ export namespace Stores { keyPath: "recoupGroupId", }); export const reserves = new ReservesStore(); + export const reserveHistory = new ReserveHistoryStore(); export const purchases = new PurchasesStore(); export const tips = new TipsStore(); export const senderWires = new SenderWiresStore(); diff --git a/src/webex/pages/popup.tsx b/src/webex/pages/popup.tsx index 450aae4ce..4d3c65b26 100644 --- a/src/webex/pages/popup.tsx +++ b/src/webex/pages/popup.tsx @@ -177,6 +177,7 @@ class WalletBalanceView extends React.Component { private gotError = false; private canceler: (() => void) | undefined = undefined; private unmount = false; + private updateBalanceRunning = false; componentWillMount(): void { this.canceler = wxApi.onUpdateNotification(() => this.updateBalance()); @@ -192,6 +193,10 @@ class WalletBalanceView extends React.Component { } async updateBalance(): Promise { + if (this.updateBalanceRunning) { + return; + } + this.updateBalanceRunning = true; let balance: WalletBalance; try { balance = await wxApi.getBalance(); @@ -203,6 +208,8 @@ class WalletBalanceView extends React.Component { console.error("could not retrieve balances", e); this.setState({}); return; + } finally { + this.updateBalanceRunning = false; } if (this.unmount) { return;