commit 51d2d831b0a6831cc7f722b0f0358ee073e52635
parent a314508d92884f20890987f3161d5a245708e931
Author: Florian Dold <florian@dold.me>
Date: Wed, 16 Apr 2025 15:27:55 +0200
wallet-core: hide repurchase transactions by default, delete them when parent tx gets deleted
Diffstat:
5 files changed, 116 insertions(+), 13 deletions(-)
diff --git a/packages/taler-harness/src/integrationtests/test-repurchase.ts b/packages/taler-harness/src/integrationtests/test-repurchase.ts
@@ -19,11 +19,12 @@
*/
import {
ConfirmPayResultType,
+ j2s,
PreparePayResultType,
succeedOrThrow,
TalerCorebankApiClient,
- TalerMerchantApi,
TalerMerchantInstanceHttpClient,
+ TransactionType,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import {
@@ -51,12 +52,6 @@ export async function runRepurchaseTest(t: GlobalTestState) {
await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
- const order = {
- summary: "Buy me!",
- amount: "TESTKUDOS:5",
- fulfillment_url: "taler://fulfillment-success/thx",
- } satisfies TalerMerchantApi.Order;
-
const merchantClient = new TalerMerchantInstanceHttpClient(
merchant.makeInstanceBaseUrl(),
);
@@ -90,11 +85,14 @@ export async function runRepurchaseTest(t: GlobalTestState) {
preparePayOneResult.status === PreparePayResultType.PaymentPossible,
);
- const r2 = await walletClient.call(WalletApiOperation.ConfirmPay, {
- transactionId: preparePayOneResult.transactionId,
- });
+ const confirmPayResp = await walletClient.call(
+ WalletApiOperation.ConfirmPay,
+ {
+ transactionId: preparePayOneResult.transactionId,
+ },
+ );
- t.assertTrue(r2.type === ConfirmPayResultType.Done);
+ t.assertTrue(confirmPayResp.type === ConfirmPayResultType.Done);
const orderTwoResp = succeedOrThrow(
await merchantClient.createOrder(undefined, {
@@ -174,6 +172,58 @@ export async function runRepurchaseTest(t: GlobalTestState) {
t.assertTrue(
preparePayThreeResult.status === PreparePayResultType.AlreadyConfirmed,
);
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ {
+ const txnsNormal = await walletClient.call(
+ WalletApiOperation.GetTransactionsV2,
+ {},
+ );
+
+ console.log(j2s(txnsNormal));
+
+ t.assertDeepEqual(txnsNormal.transactions.length, 2);
+ }
+
+ // The list of all transactions should also include repurchases.
+ {
+ const txnsAll = await walletClient.call(
+ WalletApiOperation.GetTransactionsV2,
+ {
+ includeAll: true,
+ },
+ );
+
+ console.log(j2s(txnsAll));
+
+ const numPayments = txnsAll.transactions.filter(
+ (x) => x.type === TransactionType.Payment,
+ ).length;
+
+ t.assertDeepEqual(numPayments, 3);
+ }
+
+ // Test that deleting the original transaction also deletes the repurchase transaction.
+
+ await walletClient.call(WalletApiOperation.DeleteTransaction, {
+ transactionId: preparePayOneResult.transactionId,
+ });
+
+ {
+ const txnsAll = await walletClient.call(
+ WalletApiOperation.GetTransactionsV2,
+ {
+ includeAll: true,
+ },
+ );
+
+ const numPayments = txnsAll.transactions.filter(
+ (x) => x.type === TransactionType.Payment,
+ ).length;
+
+ t.assertDeepEqual(numPayments, 0);
+ }
}
runRepurchaseTest.suites = ["wallet"];
diff --git a/packages/taler-util/src/types-taler-wallet-transactions.ts b/packages/taler-util/src/types-taler-wallet-transactions.ts
@@ -121,6 +121,12 @@ export interface GetTransactionsV2Request {
includeRefreshes?: boolean;
/**
+ * If true, include transactions that would usually be filtered out.
+ * Implies includeRefreshes.
+ */
+ includeAll?: boolean;
+
+ /**
* Only return transactions before/after this offset.
*/
offsetTransactionId?: TransactionIdStr;
@@ -889,6 +895,7 @@ export const codecForGetTransactionsV2Request =
),
)
.property("includeRefreshes", codecOptional(codecForBoolean()))
+ .property("includeAll", codecOptional(codecForBoolean()))
.build("GetTransactionsV2Request");
export const codecForTransactionsRequest = (): Codec<TransactionsRequest> =>
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
@@ -1536,7 +1536,7 @@ export interface WithdrawalGroupRecord {
/**
* Delay to wait until the next withdrawal attempt.
- *
+ *
* @deprecated by https://bugs.gnunet.org/view.php?id=9694
*/
kycWithdrawalDelay?: TalerProtocolDuration;
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -417,6 +417,39 @@ export class PayMerchantTransactionContext implements TransactionContext {
if (!rec) {
return { notifs };
}
+ let relatedTransactions: PurchaseRecord[] = [];
+ // Automatically delete transactions that are a repurchase of this transaction,
+ // as they're typically hidden.
+ if (rec.download?.fulfillmentUrl) {
+ const otherTransactions =
+ await tx.purchases.indexes.byFulfillmentUrl.getAll(
+ rec.download.fulfillmentUrl,
+ );
+ for (const ot of otherTransactions) {
+ if (
+ ot.purchaseStatus === PurchaseStatus.DoneRepurchaseDetected &&
+ ot.repurchaseProposalId === rec.proposalId
+ ) {
+ relatedTransactions.push(ot);
+ }
+ }
+ }
+ for (const rt of relatedTransactions) {
+ const otherCtx = new PayMerchantTransactionContext(
+ this.wex,
+ rt.proposalId,
+ );
+ await tx.purchases.delete(rt.proposalId);
+ await otherCtx.updateTransactionMeta(tx);
+ notifs.push({
+ type: NotificationType.TransactionStateTransition,
+ transactionId: this.transactionId,
+ oldTxState: computePayMerchantTransactionState(rt),
+ newTxState: {
+ major: TransactionMajorState.Deleted,
+ },
+ });
+ }
const oldTxState = computePayMerchantTransactionState(rec);
await tx.purchases.delete(rec.proposalId);
await this.updateTransactionMeta(tx);
diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts
@@ -54,6 +54,7 @@ import {
OPERATION_STATUS_DONE_LAST,
OPERATION_STATUS_NONFINAL_FIRST,
OPERATION_STATUS_NONFINAL_LAST,
+ PurchaseStatus,
timestampPreciseToDb,
TransactionMetaRecord,
WalletDbAllStoresReadOnlyTransaction,
@@ -189,10 +190,22 @@ function checkFilterIncludes(
}
const parsedTx = parseTransactionIdentifier(mtx.transactionId);
- if (parsedTx?.tag === TransactionType.Refresh && !req?.includeRefreshes) {
+ if (
+ parsedTx?.tag === TransactionType.Refresh &&
+ !(req?.includeRefreshes || req?.includeAll)
+ ) {
return false;
}
+ if (!req?.includeAll) {
+ if (
+ parsedTx?.tag === TransactionType.Payment &&
+ mtx.status === PurchaseStatus.DoneRepurchaseDetected
+ ) {
+ return false;
+ }
+ }
+
let included: boolean;
const filter = req?.filterByState;