From 38e6d519461cff32107b5eebfc217fd9276960db Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 1 Sep 2020 23:01:44 +0530 Subject: estimate refresh output, show correct(er) balance --- .../taler-wallet-core/src/operations/balance.ts | 5 +- packages/taler-wallet-core/src/operations/pay.ts | 8 ++- .../taler-wallet-core/src/operations/recoup.ts | 2 +- .../taler-wallet-core/src/operations/refresh.ts | 78 +++++++++++++++++----- .../taler-wallet-core/src/operations/withdraw.ts | 6 +- packages/taler-wallet-core/src/types/dbTypes.ts | 4 ++ packages/taler-wallet-core/src/util/amounts.ts | 7 +- packages/taler-wallet-core/src/wallet.ts | 2 +- 8 files changed, 88 insertions(+), 24 deletions(-) (limited to 'packages/taler-wallet-core') diff --git a/packages/taler-wallet-core/src/operations/balance.ts b/packages/taler-wallet-core/src/operations/balance.ts index 26f0aaeee..47ce5f482 100644 --- a/packages/taler-wallet-core/src/operations/balance.ts +++ b/packages/taler-wallet-core/src/operations/balance.ts @@ -90,7 +90,10 @@ export async function getBalancesInsideTransaction( const b = initBalance(session.amountRefreshOutput.currency); // We are always assuming the refresh will succeed, thus we // report the output as available balance. - b.available = Amounts.add(session.amountRefreshOutput).amount; + b.available = Amounts.add(b.available, session.amountRefreshOutput).amount; + } else { + const b = initBalance(r.inputPerCoin[i].currency); + b.available = Amounts.add(b.available, r.estimatedOutputPerCoin[i]).amount; } } }); diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts index 6981b44fc..ce71737d1 100644 --- a/packages/taler-wallet-core/src/operations/pay.ts +++ b/packages/taler-wallet-core/src/operations/pay.ts @@ -473,7 +473,13 @@ async function recordConfirmPay( }; await ws.db.runWithWriteTransaction( - [Stores.coins, Stores.purchases, Stores.proposals, Stores.refreshGroups], + [ + Stores.coins, + Stores.purchases, + Stores.proposals, + Stores.refreshGroups, + Stores.denominations, + ], async (tx) => { const p = await tx.get(Stores.proposals, proposal.proposalId); if (p) { diff --git a/packages/taler-wallet-core/src/operations/recoup.ts b/packages/taler-wallet-core/src/operations/recoup.ts index 7896fc275..0e4ce18d3 100644 --- a/packages/taler-wallet-core/src/operations/recoup.ts +++ b/packages/taler-wallet-core/src/operations/recoup.ts @@ -180,7 +180,7 @@ async function recoupWithdrawCoin( // FIXME: verify that our expectations about the amount match await ws.db.runWithWriteTransaction( - [Stores.coins, Stores.reserves, Stores.recoupGroups], + [Stores.coins, Stores.denominations, Stores.reserves, Stores.recoupGroups], async (tx) => { const recoupGroup = await tx.get(Stores.recoupGroups, recoupGroupId); if (!recoupGroup) { diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index d951cfeb8..770c67da6 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -31,7 +31,7 @@ import { amountToPretty } from "../util/helpers"; import { TransactionHandle } from "../util/query"; import { InternalWalletState, EXCHANGE_COINS_LOCK } from "./state"; import { Logger } from "../util/logging"; -import { getWithdrawDenomList } from "./withdraw"; +import { getWithdrawDenomList, isWithdrawableDenom } from "./withdraw"; import { updateExchangeFromUrl } from "./exchanges"; import { TalerErrorDetails, @@ -49,6 +49,7 @@ import { codecForExchangeRevealResponse, } from "../types/talerTypes"; import { URL } from "../util/url"; +import { checkDbInvariant } from "../util/invariants"; const logger = new Logger("refresh.ts"); @@ -132,8 +133,10 @@ async function refreshCreateSession( .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl) .toArray(); - const availableAmount = Amounts.sub(coin.currentAmount, oldDenom.feeRefresh) - .amount; + const availableAmount = Amounts.sub( + refreshGroup.inputPerCoin[coinIndex], + oldDenom.feeRefresh, + ).amount; const newCoinDenoms = getWithdrawDenomList(availableAmount, availableDenoms); @@ -177,22 +180,10 @@ async function refreshCreateSession( oldDenom.feeRefresh, ); - // Store refresh session and subtract refreshed amount from - // coin in the same transaction. + // Store refresh session for this coin in the database. await ws.db.runWithWriteTransaction( [Stores.refreshGroups, Stores.coins], async (tx) => { - const c = await tx.get(Stores.coins, coin.coinPub); - if (!c) { - throw Error("coin not found, but marked for refresh"); - } - const r = Amounts.sub(c.currentAmount, refreshSession.amountRefreshInput); - if (r.saturated) { - logger.warn("can't refresh coin, no amount left"); - return; - } - c.currentAmount = r.amount; - c.status = CoinStatus.Dormant; const rg = await tx.get(Stores.refreshGroups, refreshGroupId); if (!rg) { return; @@ -202,7 +193,6 @@ async function refreshCreateSession( } rg.refreshSessionPerCoin[coinIndex] = refreshSession; await tx.put(Stores.refreshGroups, rg); - await tx.put(Stores.coins, c); }, ); logger.info( @@ -552,6 +542,16 @@ async function processRefreshSession( /** * Create a refresh group for a list of coins. + * + * Refreshes the remaining amount on the coin, effectively capturing the remaining + * value in the refresh group. + * + * The caller must ensure that + * the remaining amount was updated correctly before the coin was deposited or + * credited. + * + * The caller must also ensure that the coins that should be refreshed exist + * in the current database transaction. */ export async function createRefreshGroup( ws: InternalWalletState, @@ -561,6 +561,48 @@ export async function createRefreshGroup( ): Promise { const refreshGroupId = encodeCrock(getRandomBytes(32)); + const inputPerCoin: AmountJson[] = []; + const estimatedOutputPerCoin: AmountJson[] = []; + + const denomsPerExchange: Record = {}; + + const getDenoms = async ( + exchangeBaseUrl: string, + ): Promise => { + if (denomsPerExchange[exchangeBaseUrl]) { + return denomsPerExchange[exchangeBaseUrl]; + } + const allDenoms = await tx + .iterIndexed(Stores.denominations.exchangeBaseUrlIndex, exchangeBaseUrl) + .filter((x) => { + return isWithdrawableDenom(x); + }); + denomsPerExchange[exchangeBaseUrl] = allDenoms; + return allDenoms; + }; + + for (const ocp of oldCoinPubs) { + const coin = await tx.get(Stores.coins, ocp.coinPub); + checkDbInvariant(!!coin, "coin must be in database"); + const denom = await tx.get(Stores.denominations, [ + coin.exchangeBaseUrl, + coin.denomPub, + ]); + checkDbInvariant( + !!denom, + "denomination for existing coin must be in database", + ); + const refreshAmount = coin.currentAmount; + inputPerCoin.push(refreshAmount); + coin.currentAmount = Amounts.getZero(refreshAmount.currency); + coin.status = CoinStatus.Dormant; + await tx.put(Stores.coins, coin); + const denoms = await getDenoms(coin.exchangeBaseUrl); + const cost = getTotalRefreshCost(denoms, denom, refreshAmount); + const output = Amounts.sub(refreshAmount, cost).amount; + estimatedOutputPerCoin.push(output); + } + const refreshGroup: RefreshGroupRecord = { timestampFinished: undefined, finishedPerCoin: oldCoinPubs.map((x) => false), @@ -571,6 +613,8 @@ export async function createRefreshGroup( refreshGroupId, refreshSessionPerCoin: oldCoinPubs.map((x) => undefined), retryInfo: initRetryInfo(), + inputPerCoin, + estimatedOutputPerCoin, }; if (oldCoinPubs.length == 0) { diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index c68f1521f..c719f7ab8 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -67,7 +67,11 @@ import { encodeCrock } from "../crypto/talerCrypto"; const logger = new Logger("withdraw.ts"); -function isWithdrawableDenom(d: DenominationRecord): boolean { +/** + * Check if a denom is withdrawable based on the expiration time + * and revocation state. + */ +export function isWithdrawableDenom(d: DenominationRecord): boolean { const now = getTimestampNow(); const started = timestampCmp(now, d.stampStart) >= 0; const lastPossibleWithdraw = timestampSubtractDuraction( diff --git a/packages/taler-wallet-core/src/types/dbTypes.ts b/packages/taler-wallet-core/src/types/dbTypes.ts index 148ec8c35..9ba07a0fd 100644 --- a/packages/taler-wallet-core/src/types/dbTypes.ts +++ b/packages/taler-wallet-core/src/types/dbTypes.ts @@ -1014,6 +1014,10 @@ export interface RefreshGroupRecord { refreshSessionPerCoin: (RefreshSessionRecord | undefined)[]; + inputPerCoin: AmountJson[]; + + estimatedOutputPerCoin: AmountJson[]; + /** * Flag for each coin whether refreshing finished. * If a coin can't be refreshed (remaining value too small), diff --git a/packages/taler-wallet-core/src/util/amounts.ts b/packages/taler-wallet-core/src/util/amounts.ts index 2f912cff4..533005b18 100644 --- a/packages/taler-wallet-core/src/util/amounts.ts +++ b/packages/taler-wallet-core/src/util/amounts.ts @@ -195,7 +195,9 @@ export function sub(a: AmountJson, ...rest: AmountJson[]): Result { * Compare two amounts. Returns 0 when equal, -1 when a < b * and +1 when a > b. Throws when currencies don't match. */ -export function cmp(a: AmountJson, b: AmountJson): -1 | 0 | 1 { +export function cmp(a: AmountLike, b: AmountLike): -1 | 0 | 1 { + a = jsonifyAmount(a); + b = jsonifyAmount(b); if (a.currency !== b.currency) { throw Error(`Mismatched currency: ${a.currency} and ${b.currency}`); } @@ -310,7 +312,8 @@ export function fromFloat(floatVal: number, currency: string): AmountJson { * Convert to standard human-readable string representation that's * also used in JSON formats. */ -export function stringify(a: AmountJson): string { +export function stringify(a: AmountLike): string { + a = jsonifyAmount(a); const av = a.value + Math.floor(a.fraction / fractionalBase); const af = a.fraction % fractionalBase; let s = av.toString(); diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index ccc4e9d2c..b448df648 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -571,7 +571,7 @@ export class Wallet { async refresh(oldCoinPub: string): Promise { try { const refreshGroupId = await this.db.runWithWriteTransaction( - [Stores.refreshGroups], + [Stores.refreshGroups, Stores.denominations, Stores.coins], async (tx) => { return await createRefreshGroup( this.ws, -- cgit v1.2.3