diff options
Diffstat (limited to 'packages/taler-wallet-core/src/withdraw.ts')
-rw-r--r-- | packages/taler-wallet-core/src/withdraw.ts | 208 |
1 files changed, 198 insertions, 10 deletions
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); |