summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-03-25 20:31:56 +0100
committerFlorian Dold <florian@dold.me>2024-03-25 20:31:56 +0100
commitbd70ccfddfb9f993a5951a31be5bdc982fe1a58f (patch)
treebc631a948ec7ce5e52619c850622e8f91df4d7e7 /packages/taler-wallet-core
parent31b7ce31a3d34149d2397f999a86c14100bd72ac (diff)
downloadwallet-core-bd70ccfddfb9f993a5951a31be5bdc982fe1a58f.tar.gz
wallet-core-bd70ccfddfb9f993a5951a31be5bdc982fe1a58f.tar.bz2
wallet-core-bd70ccfddfb9f993a5951a31be5bdc982fe1a58f.zip
wallet-core: re-denomination of withdrawal groups
Diffstat (limited to 'packages/taler-wallet-core')
-rw-r--r--packages/taler-wallet-core/src/db.ts1
-rw-r--r--packages/taler-wallet-core/src/denomSelection.ts24
-rw-r--r--packages/taler-wallet-core/src/transactions.ts13
-rw-r--r--packages/taler-wallet-core/src/withdraw.ts208
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);