diff options
Diffstat (limited to 'packages/taler-wallet-core/src/operations')
5 files changed, 78 insertions, 21 deletions
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<RefreshGroupId> { const refreshGroupId = encodeCrock(getRandomBytes(32)); + const inputPerCoin: AmountJson[] = []; + const estimatedOutputPerCoin: AmountJson[] = []; + + const denomsPerExchange: Record<string, DenominationRecord[]> = {}; + + const getDenoms = async ( + exchangeBaseUrl: string, + ): Promise<DenominationRecord[]> => { + 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( |