summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-01-19 14:35:00 +0100
committerFlorian Dold <florian@dold.me>2024-01-19 14:35:00 +0100
commit2b7fbbaaa561d2ff821bab432a6ad839af291660 (patch)
tree71a24375c3052670868fc17e46f0ee713759a48b
parente9c6b105d18f142cf4d4b203e734513df1e5021c (diff)
downloadwallet-core-dev/dold/bug-7836.tar.gz
wallet-core-dev/dold/bug-7836.tar.bz2
wallet-core-dev/dold/bug-7836.zip
-rw-r--r--packages/taler-harness/src/integrationtests/test-refund-gone.ts5
-rw-r--r--packages/taler-wallet-core/src/db.ts10
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts3
-rw-r--r--packages/taler-wallet-core/src/operations/pay-merchant.ts172
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts5
-rw-r--r--packages/taler-wallet-core/src/util/coinSelection.ts3
6 files changed, 118 insertions, 80 deletions
diff --git a/packages/taler-harness/src/integrationtests/test-refund-gone.ts b/packages/taler-harness/src/integrationtests/test-refund-gone.ts
index d50919934..24d5d7ddd 100644
--- a/packages/taler-harness/src/integrationtests/test-refund-gone.ts
+++ b/packages/taler-harness/src/integrationtests/test-refund-gone.ts
@@ -32,7 +32,8 @@ import {
} from "../harness/helpers.js";
/**
- * Run test for basic, bank-integrated withdrawal.
+ * Test wallet behavior when a refund expires before the wallet
+ * can claim it.
*/
export async function runRefundGoneTest(t: GlobalTestState) {
// Set up test environment
@@ -102,7 +103,7 @@ export async function runRefundGoneTest(t: GlobalTestState) {
await applyTimeTravelV2(
Duration.toMilliseconds(Duration.fromSpec({ hours: 1 })),
- { exchange, walletClient: walletClient },
+ { exchange, merchant, walletClient: walletClient },
);
await exchange.runAggregatorOnce();
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 73739c19c..f16600f5d 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -2217,6 +2217,7 @@ export enum RefundGroupStatus {
Done = 0x0500_0000,
Failed = 0x0501_0000,
Aborted = 0x0503_0000,
+ Expired = 0x0502_0000,
}
/**
@@ -2641,6 +2642,7 @@ export const WalletStoresV1 = {
"coinPub",
"rtxid",
]),
+ // FIXME: Why is this a list of index keys? Confusing!
byRefundGroupId: describeIndex("byRefundGroupId", ["refundGroupId"]),
},
),
@@ -2663,6 +2665,14 @@ export type WalletDbReadWriteTransaction<
Stores extends StoreNames<typeof WalletStoresV1> & string,
> = DbReadWriteTransaction<typeof WalletStoresV1, Stores>;
+export type WalletDbReadWriteTransactionArr<
+ StoresArr extends Array<StoreNames<typeof WalletStoresV1>>,
+> = DbReadWriteTransactionArr<typeof WalletStoresV1, StoresArr>;
+
+export type WalletDbReadOnlyTransactionArr<
+ StoresArr extends Array<StoreNames<typeof WalletStoresV1>>,
+> = DbReadOnlyTransactionArr<typeof WalletStoresV1, StoresArr>;
+
/**
* An applied migration.
*/
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts
index f983a7c4d..67404665c 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -89,6 +89,7 @@ import {
ExchangeEntryDbUpdateStatus,
PendingTaskType,
WalletDbReadWriteTransaction,
+ WalletDbReadWriteTransactionArr,
createTimeline,
isWithdrawableDenom,
selectBestForOverlappingDenominations,
@@ -421,7 +422,7 @@ async function validateGlobalFees(
* if the DB transaction succeeds.
*/
export async function addPresetExchangeEntry(
- tx: WalletDbReadWriteTransaction<"exchanges">,
+ tx: WalletDbReadWriteTransactionArr<["exchanges"]>,
exchangeBaseUrl: string,
currencyHint?: string,
): Promise<{ notification?: WalletNotification }> {
diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts
index 50b73acb7..f6bbe5b9f 100644
--- a/packages/taler-wallet-core/src/operations/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts
@@ -111,6 +111,7 @@ import {
timestampPreciseToDb,
timestampProtocolFromDb,
timestampProtocolToDb,
+ WalletDbReadWriteTransactionArr,
} from "../index.js";
import {
EXCHANGE_COINS_LOCK,
@@ -2006,8 +2007,8 @@ async function processPurchasePay(
) {
// Do this in the background, as it might take some time
handleInsufficientFunds(ws, proposalId, err).catch(async (e) => {
- console.log("handling insufficient funds failed");
- console.log(`${e.toString()}`);
+ logger.error("handling insufficient funds failed");
+ logger.error(`${e.toString()}`);
});
// FIXME: Should we really consider this to be pending?
@@ -2853,6 +2854,55 @@ export async function startQueryRefund(
ws.workAvailable.trigger();
}
+async function computeRefreshRequest(
+ ws: InternalWalletState,
+ tx: WalletDbReadWriteTransactionArr<["coins", "denominations"]>,
+ items: RefundItemRecord[],
+): Promise<CoinRefreshRequest[]> {
+ const refreshCoins: CoinRefreshRequest[] = [];
+ for (const item of items) {
+ const coin = await tx.coins.get(item.coinPub);
+ if (!coin) {
+ throw Error("coin not found");
+ }
+ const denomInfo = await ws.getDenomInfo(
+ ws,
+ tx,
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ );
+ if (!denomInfo) {
+ throw Error("denom not found");
+ }
+ if (item.status === RefundItemStatus.Done) {
+ const refundedAmount = Amounts.sub(
+ item.refundAmount,
+ denomInfo.feeRefund,
+ ).amount;
+ refreshCoins.push({
+ amount: Amounts.stringify(refundedAmount),
+ coinPub: item.coinPub,
+ });
+ }
+ }
+ return refreshCoins;
+}
+
+/**
+ * Compute the refund item status based on the merchant's response.
+ */
+function getItemStatus(rf: MerchantCoinRefundStatus): RefundItemStatus {
+ if (rf.type === "success") {
+ return RefundItemStatus.Done;
+ } else {
+ if (rf.exchange_status >= 500 && rf.exchange_status <= 599) {
+ return RefundItemStatus.Pending;
+ } else {
+ return RefundItemStatus.Failed;
+ }
+ }
+}
+
/**
* Store refunds, possibly creating a new refund group.
*/
@@ -2875,59 +2925,19 @@ async function storeRefunds(
const download = await expectProposalDownload(ws, purchase);
const currency = Amounts.currencyOf(download.contractData.amount);
- const getItemStatus = (rf: MerchantCoinRefundStatus) => {
- if (rf.type === "success") {
- return RefundItemStatus.Done;
- } else {
- if (rf.exchange_status >= 500 && rf.exchange_status <= 599) {
- return RefundItemStatus.Pending;
- } else {
- return RefundItemStatus.Failed;
- }
- }
- };
-
- const result = await ws.db
- .mktx((x) => [
- x.purchases,
- x.refundGroups,
- x.refundItems,
- x.coins,
- x.denominations,
- x.coinAvailability,
- x.refreshGroups,
- ])
- .runReadWrite(async (tx) => {
- const computeRefreshRequest = async (items: RefundItemRecord[]) => {
- const refreshCoins: CoinRefreshRequest[] = [];
- for (const item of items) {
- const coin = await tx.coins.get(item.coinPub);
- if (!coin) {
- throw Error("coin not found");
- }
- const denomInfo = await ws.getDenomInfo(
- ws,
- tx,
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- );
- if (!denomInfo) {
- throw Error("denom not found");
- }
- if (item.status === RefundItemStatus.Done) {
- const refundedAmount = Amounts.sub(
- item.refundAmount,
- denomInfo.feeRefund,
- ).amount;
- refreshCoins.push({
- amount: Amounts.stringify(refundedAmount),
- coinPub: item.coinPub,
- });
- }
- }
- return refreshCoins;
- };
-
+ const result = await ws.db.runReadWriteTx(
+ [
+ "coins",
+ "denominations",
+ "purchases",
+ "refundItems",
+ "refundGroups",
+ "denominations",
+ "coins",
+ "coinAvailability",
+ "refreshGroups",
+ ],
+ async (tx) => {
const myPurchase = await tx.purchases.get(purchase.proposalId);
if (!myPurchase) {
logger.warn("purchase group not found anymore");
@@ -3008,7 +3018,11 @@ async function storeRefunds(
// we can compute the raw/effective amounts.
if (newGroup) {
const amountsRaw = newGroupRefunds.map((x) => x.refundAmount);
- const refreshCoins = await computeRefreshRequest(newGroupRefunds);
+ const refreshCoins = await computeRefreshRequest(
+ ws,
+ tx,
+ newGroupRefunds,
+ );
const outInfo = await calculateRefreshOutput(
ws,
tx,
@@ -3028,35 +3042,40 @@ async function storeRefunds(
myPurchase.proposalId,
);
- logger.info(
- `refund groups for proposal ${myPurchase.proposalId}: ${j2s(
- refundGroups,
- )}`,
- );
-
for (const refundGroup of refundGroups) {
- if (refundGroup.status === RefundGroupStatus.Aborted) {
- continue;
- }
- if (refundGroup.status === RefundGroupStatus.Done) {
- continue;
+ switch (refundGroup.status) {
+ case RefundGroupStatus.Aborted:
+ case RefundGroupStatus.Expired:
+ case RefundGroupStatus.Failed:
+ case RefundGroupStatus.Done:
+ continue;
+ case RefundGroupStatus.Pending:
+ break;
+ default:
+ assertUnreachable(refundGroup.status);
}
- const items = await tx.refundItems.indexes.byRefundGroupId.getAll(
+ const items = await tx.refundItems.indexes.byRefundGroupId.getAll([
refundGroup.refundGroupId,
- );
+ ]);
let numPending = 0;
+ let numFailed = 0;
for (const item of items) {
if (item.status === RefundItemStatus.Pending) {
numPending++;
}
+ if (item.status === RefundItemStatus.Failed) {
+ numFailed++;
+ }
}
- logger.info(`refund items pending for refund group: ${numPending}`);
if (numPending === 0) {
- logger.info("refund group is done!");
// We're done for this refund group!
- refundGroup.status = RefundGroupStatus.Done;
+ if (numFailed === 0) {
+ refundGroup.status = RefundGroupStatus.Done;
+ } else {
+ refundGroup.status = RefundGroupStatus.Failed;
+ }
await tx.refundGroups.put(refundGroup);
- const refreshCoins = await computeRefreshRequest(items);
+ const refreshCoins = await computeRefreshRequest(ws, tx, items);
await createRefreshGroup(
ws,
tx,
@@ -3085,7 +3104,8 @@ async function storeRefunds(
newTxState,
},
};
- });
+ },
+ );
if (!result) {
return TaskRunResult.finished();
@@ -3120,5 +3140,9 @@ export function computeRefundTransactionState(
return {
major: TransactionMajorState.Pending,
};
+ case RefundGroupStatus.Expired:
+ return {
+ major: TransactionMajorState.Expired,
+ };
}
}
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts
index fc2508cd3..d49c9a1cf 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -84,6 +84,7 @@ import {
timestampPreciseToDb,
timestampProtocolFromDb,
WalletDbReadWriteTransaction,
+ WalletDbReadWriteTransactionArr,
} from "../index.js";
import {
EXCHANGE_COINS_LOCK,
@@ -1244,8 +1245,8 @@ async function applyRefresh(
*/
export async function createRefreshGroup(
ws: InternalWalletState,
- tx: WalletDbReadWriteTransaction<
- "denominations" | "coins" | "refreshGroups" | "coinAvailability"
+ tx: WalletDbReadWriteTransactionArr<
+ ["denominations", "coins", "refreshGroups", "coinAvailability"]
>,
currency: string,
oldCoinPubs: CoinRefreshRequest[],
diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts b/packages/taler-wallet-core/src/util/coinSelection.ts
index 9b29cee26..d75450a64 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.ts
@@ -60,6 +60,7 @@ import {
getExchangeWireDetailsInTx,
isWithdrawableDenom,
WalletDbReadOnlyTransaction,
+ WalletDbReadOnlyTransactionArr,
} from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import {
@@ -914,7 +915,7 @@ export interface PeerCoinSelectionRequest {
*/
async function selectPayPeerCandidatesForExchange(
ws: InternalWalletState,
- tx: WalletDbReadOnlyTransaction<"coinAvailability" | "denominations">,
+ tx: WalletDbReadOnlyTransactionArr<["coinAvailability", "denominations"]>,
exchangeBaseUrl: string,
): Promise<AvailableDenom[]> {
const denoms: AvailableDenom[] = [];