diff options
author | Florian Dold <florian@dold.me> | 2024-03-25 20:31:56 +0100 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2024-03-25 20:31:56 +0100 |
commit | bd70ccfddfb9f993a5951a31be5bdc982fe1a58f (patch) | |
tree | bc631a948ec7ce5e52619c850622e8f91df4d7e7 /packages/taler-wallet-core/src | |
parent | 31b7ce31a3d34149d2397f999a86c14100bd72ac (diff) | |
download | wallet-core-bd70ccfddfb9f993a5951a31be5bdc982fe1a58f.tar.xz |
wallet-core: re-denomination of withdrawal groups
Diffstat (limited to 'packages/taler-wallet-core/src')
-rw-r--r-- | packages/taler-wallet-core/src/db.ts | 1 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/denomSelection.ts | 24 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/transactions.ts | 13 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/withdraw.ts | 208 |
4 files changed, 225 insertions, 21 deletions
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index b59efe034..8b7aede57 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -699,6 +699,7 @@ export enum PlanchetStatus { Pending = 0x0100_0000, KycRequired = 0x0100_0001, WithdrawalDone = 0x0500_000, + AbortedReplaced = 0x0503_0001, } /** diff --git a/packages/taler-wallet-core/src/denomSelection.ts b/packages/taler-wallet-core/src/denomSelection.ts index 12f8f8971..dd5ec60d8 100644 --- a/packages/taler-wallet-core/src/denomSelection.ts +++ b/packages/taler-wallet-core/src/denomSelection.ts @@ -58,6 +58,12 @@ export function selectWithdrawalDenominations( denoms = denoms.filter((d) => isWithdrawableDenom(d, denomselAllowLate)); denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value)); + if (logger.shouldLogTrace()) { + logger.trace( + `selecting withdrawal denoms for ${Amounts.stringify(amountAvailable)}`, + ); + } + for (const d of denoms) { const cost = Amounts.add(d.value, d.fees.feeWithdraw).amount; const res = Amounts.divmod(remaining, cost); @@ -78,19 +84,23 @@ export function selectWithdrawalDenominations( }); } + if (logger.shouldLogTrace()) { + logger.trace( + `denom_pub_hash=${ + d.denomPubHash + }, count=${count}, val=${Amounts.stringify( + d.value, + )}, wdfee=${Amounts.stringify(d.fees.feeWithdraw)}`, + ); + } + if (Amounts.isZero(remaining)) { break; } } if (logger.shouldLogTrace()) { - logger.trace( - `selected withdrawal denoms for ${Amounts.stringify(totalCoinValue)}`, - ); - for (const sd of selectedDenoms) { - logger.trace(`denom_pub_hash=${sd.denomPubHash}, count=${sd.count}`); - } - logger.trace("(end of withdrawal denom list)"); + logger.trace("(end of denom selection)"); } return { diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts index e2e188e74..0e3f4a3fb 100644 --- a/packages/taler-wallet-core/src/transactions.ts +++ b/packages/taler-wallet-core/src/transactions.ts @@ -132,7 +132,7 @@ import { computeTipTransactionActions, RewardTransactionContext, } from "./reward.js"; -import type { InternalWalletState, WalletExecutionContext } from "./wallet.js"; +import type { WalletExecutionContext } from "./wallet.js"; import { augmentPaytoUrisForWithdrawal, computeWithdrawalTransactionActions, @@ -1487,9 +1487,6 @@ export async function getTransactions( x.txState.major === TransactionMajorState.Aborting || x.txState.major === TransactionMajorState.Dialog; - const txPending = transactions.filter((x) => isPending(x)); - const txNotPending = transactions.filter((x) => !isPending(x)); - let sortSign: number; if (transactionsRequest?.sort == "descending") { sortSign = -1; @@ -1510,6 +1507,14 @@ export async function getTransactions( return sortSign * tsCmp; }; + if (transactionsRequest?.sort === "stable-ascending") { + transactions.sort(txCmp); + return { transactions }; + } + + const txPending = transactions.filter((x) => isPending(x)); + const txNotPending = transactions.filter((x) => !isPending(x)); + txPending.sort(txCmp); txNotPending.sort(txCmp); diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts index 9132d2b09..625d5dca4 100644 --- a/packages/taler-wallet-core/src/withdraw.ts +++ b/packages/taler-wallet-core/src/withdraw.ts @@ -27,6 +27,7 @@ import { AcceptManualWithdrawalResult, AcceptWithdrawalResponse, AgeRestriction, + Amount, AmountJson, AmountLike, AmountString, @@ -37,6 +38,7 @@ import { CoinStatus, CurrencySpecification, DenomKeyType, + DenomSelItem, DenomSelectionState, Duration, ExchangeBatchWithdrawRequest, @@ -92,6 +94,7 @@ import { HttpResponse, readSuccessResponseJsonOrErrorCode, readSuccessResponseJsonOrThrow, + readTalerErrorResponse, throwUnexpectedRequestError, } from "@gnu-taler/taler-util/http"; import { @@ -665,15 +668,22 @@ async function processPlanchetGenerate( return; } let ci = 0; + let isSkipped = false; let maybeDenomPubHash: string | undefined; for (let di = 0; di < withdrawalGroup.denomsSel.selectedDenoms.length; di++) { const d = withdrawalGroup.denomsSel.selectedDenoms[di]; if (coinIdx >= ci && coinIdx < ci + d.count) { maybeDenomPubHash = d.denomPubHash; + if (coinIdx >= ci + d.count - (d.skip ?? 0)) { + isSkipped = true; + } break; } ci += d.count; } + if (isSkipped) { + return; + } if (!maybeDenomPubHash) { throw Error("invariant violated"); } @@ -938,6 +948,9 @@ async function processPlanchetExchangeBatchRequest( logger.warn("processPlanchet: planchet already withdrawn"); continue; } + if (planchet.planchetStatus === PlanchetStatus.AbortedReplaced) { + continue; + } const denom = await getDenomInfo( wex, tx, @@ -968,10 +981,11 @@ async function processPlanchetExchangeBatchRequest( }; } - async function storeCoinError(e: any, coinIdx: number): Promise<void> { - const errDetail = getErrorDetailFromException(e); - logger.trace("withdrawal request failed", e); - logger.trace(String(e)); + async function storeCoinError( + errDetail: TalerErrorDetail, + coinIdx: number, + ): Promise<void> { + logger.trace(`withdrawal request failed: ${j2s(errDetail)}`); await wex.db.runReadWriteTx(["planchets"], async (tx) => { let planchet = await tx.planchets.indexes.byGroupAndIndex.get([ withdrawalGroup.withdrawalGroupId, @@ -1006,6 +1020,15 @@ async function processPlanchetExchangeBatchRequest( coinIdxs: [], }; } + if (resp.status === HttpStatusCode.Gone) { + const e = await readTalerErrorResponse(resp); + // FIXME: Store in place of the planchet that is actually affected! + await storeCoinError(e, requestCoinIdxs[0]); + return { + batchResp: { ev_sigs: [] }, + coinIdxs: [], + }; + } const r = await readSuccessResponseJsonOrThrow( resp, codecForExchangeWithdrawBatchResponse(), @@ -1015,7 +1038,10 @@ async function processPlanchetExchangeBatchRequest( batchResp: r, }; } catch (e) { - await storeCoinError(e, requestCoinIdxs[0]); + const errDetail = getErrorDetailFromException(e); + // We don't know which coin is affected, so we store the error + // with the first coin of the batch. + await storeCoinError(errDetail, requestCoinIdxs[0]); return { batchResp: { ev_sigs: [] }, coinIdxs: [], @@ -1488,6 +1514,128 @@ async function processWithdrawalGroupPendingKyc( return TaskRunResult.backoff(); } +async function redenominateWithdrawal( + wex: WalletExecutionContext, + withdrawalGroupId: string, +): Promise<void> { + logger.trace(`redenominating withdrawal group ${withdrawalGroupId}`); + await wex.db.runReadWriteTx( + ["withdrawalGroups", "planchets", "denominations"], + async (tx) => { + const wg = await tx.withdrawalGroups.get(withdrawalGroupId); + if (!wg) { + return; + } + const currency = Amounts.currencyOf(wg.denomsSel.totalWithdrawCost); + const exchangeBaseUrl = wg.exchangeBaseUrl; + + const candidates = await getCandidateWithdrawalDenomsTx( + wex, + tx, + exchangeBaseUrl, + currency, + ); + + const oldSel = wg.denomsSel; + + if (logger.shouldLogTrace()) { + logger.trace(`old denom sel: ${j2s(oldSel)}`); + } + + let zero = Amount.zeroOfCurrency(currency); + let amountRemaining = zero; + let prevTotalCoinValue = zero; + let prevTotalWithdrawalCost = zero; + let prevDenoms: DenomSelItem[] = []; + let coinIndex = 0; + for (let i = 0; i < oldSel.selectedDenoms.length; i++) { + const sel = wg.denomsSel.selectedDenoms[i]; + const denom = await tx.denominations.get([ + exchangeBaseUrl, + sel.denomPubHash, + ]); + if (!denom) { + throw Error("denom in use but not not found"); + } + // FIXME: Also check planchet if there was a different error or planchet already withdrawn + let denomOkay = isWithdrawableDenom( + denom, + wex.ws.config.testing.denomselAllowLate, + ); + const numCoins = sel.count - (sel.skip ?? 0); + const denomValue = Amount.from(denom.value).mult(numCoins); + const denomFeeWithdraw = Amount.from(denom.fees.feeWithdraw).mult( + numCoins, + ); + if (denomOkay) { + prevTotalCoinValue = prevTotalCoinValue.add(denomValue); + prevTotalWithdrawalCost = prevTotalWithdrawalCost.add( + denomValue, + denomFeeWithdraw, + ); + prevDenoms.push({ + count: sel.count, + denomPubHash: sel.denomPubHash, + skip: sel.skip, + }); + } else { + amountRemaining = amountRemaining.add(denomValue, denomFeeWithdraw); + prevDenoms.push({ + count: sel.count, + denomPubHash: sel.denomPubHash, + skip: (sel.skip ?? 0) + numCoins, + }); + + for (let j = 0; j < sel.count; j++) { + const ci = coinIndex + j; + const p = await tx.planchets.indexes.byGroupAndIndex.get([ + withdrawalGroupId, + ci, + ]); + if (!p) { + // Maybe planchet wasn't yet generated. + // No problem! + logger.info( + `not aborting planchet #${coinIndex}, planchet not found`, + ); + continue; + } + logger.info(`aborting planchet #${coinIndex}`); + p.planchetStatus = PlanchetStatus.AbortedReplaced; + await tx.planchets.put(p); + } + } + + coinIndex += sel.count; + } + + const newSel = selectWithdrawalDenominations( + amountRemaining.toJson(), + candidates, + ); + + if (logger.shouldLogTrace()) { + logger.trace(`new denom sel: ${j2s(newSel)}`); + } + + const mergedSel: DenomSelectionState = { + selectedDenoms: [...prevDenoms, ...newSel.selectedDenoms], + totalCoinValue: zero + .add(prevTotalCoinValue, newSel.totalCoinValue) + .toString(), + totalWithdrawCost: zero + .add(prevTotalWithdrawalCost, newSel.totalWithdrawCost) + .toString(), + }; + wg.denomsSel = mergedSel; + if (logger.shouldLogTrace()) { + logger.trace(`merged denom sel: ${j2s(mergedSel)}`); + } + await tx.withdrawalGroups.put(wg); + }, + ); +} + async function processWithdrawalGroupPendingReady( wex: WalletExecutionContext, withdrawalGroup: WithdrawalGroupRecord, @@ -1498,6 +1646,8 @@ async function processWithdrawalGroupPendingReady( withdrawalGroupId, }); + const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl; + await fetchFreshExchange(wex, withdrawalGroup.exchangeBaseUrl); if (withdrawalGroup.denomsSel.selectedDenoms.length === 0) { @@ -1576,9 +1726,41 @@ async function processWithdrawalGroupPendingReady( await Promise.all(work); } - let numFinished = 0; + let redenomRequired = false; + + await wex.db.runReadOnlyTx(["planchets"], async (tx) => { + const planchets = + await tx.planchets.indexes.byGroup.getAll(withdrawalGroupId); + for (const p of planchets) { + if (p.planchetStatus !== PlanchetStatus.Pending) { + continue; + } + if (!p.lastError) { + continue; + } + switch (p.lastError.code) { + case TalerErrorCode.EXCHANGE_GENERIC_DENOMINATION_EXPIRED: + case TalerErrorCode.EXCHANGE_GENERIC_DENOMINATION_REVOKED: + redenomRequired = true; + return; + } + } + }); + + if (redenomRequired) { + logger.warn(`withdrawal ${withdrawalGroupId} requires redenomination`); + await fetchFreshExchange(wex, exchangeBaseUrl, { + forceUpdate: true, + }); + await updateWithdrawalDenoms(wex, exchangeBaseUrl); + await redenominateWithdrawal(wex, withdrawalGroupId); + return TaskRunResult.backoff(); + } + const errorsPerCoin: Record<number, TalerErrorDetail> = {}; let numPlanchetErrors = 0; + let numActive = 0; + let numDone = 0; const maxReportedErrors = 5; const res = await wex.db.runReadWriteTx( @@ -1592,8 +1774,14 @@ async function processWithdrawalGroupPendingReady( await tx.planchets.indexes.byGroup .iter(withdrawalGroupId) .forEach((x) => { - if (x.planchetStatus === PlanchetStatus.WithdrawalDone) { - numFinished++; + switch (x.planchetStatus) { + case PlanchetStatus.KycRequired: + case PlanchetStatus.Pending: + numActive++; + break; + case PlanchetStatus.WithdrawalDone: + numDone++; + break; } if (x.lastError) { numPlanchetErrors++; @@ -1603,8 +1791,8 @@ async function processWithdrawalGroupPendingReady( } }); const oldTxState = computeWithdrawalTransactionStatus(wg); - logger.info(`now withdrawn ${numFinished} of ${numTotalCoins} coins`); - if (wg.timestampFinish === undefined && numFinished === numTotalCoins) { + logger.info(`now withdrawn ${numDone} of ${numTotalCoins} coins`); + if (wg.timestampFinish === undefined && numActive === 0) { wg.timestampFinish = timestampPreciseToDb(TalerPreciseTimestamp.now()); wg.status = WithdrawalGroupStatus.Done; await makeCoinsVisible(wex, tx, transactionId); |