commit 2aaa990e7e22eec09eab370f4aeb6e1e1c45903f
parent 582960d48fff593c9067d9fae2a3bb22f8ee322c
Author: Florian Dold <florian@dold.me>
Date: Tue, 1 Apr 2025 23:00:52 +0200
wallet-core: fix bad feature interaction in merchant payments
Due to overloading of the "shared" flag, repurchase detection would go
wrong for shared orders.
Diffstat:
3 files changed, 56 insertions(+), 37 deletions(-)
diff --git a/packages/taler-harness/src/integrationtests/test-payment-share.ts b/packages/taler-harness/src/integrationtests/test-payment-share.ts
@@ -23,6 +23,8 @@ import {
PreparePayResultType,
succeedOrThrow,
TalerMerchantInstanceHttpClient,
+ TransactionMajorState,
+ TransactionMinorState,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import {
@@ -283,21 +285,25 @@ export async function runPaymentShareTest(t: GlobalTestState) {
t.assertAmountEquals(bal2.balances[0].available, "TESTKUDOS:14.23");
t.logStep("wait-for-payment");
- // secondWallet.waitForNotificationCond(n =>
- // n.type === NotificationType.TransactionStateTransition &&
- // n.transactionId === claimSecondWallet.transactionId
- // )
- // Claim the order with the first wallet
- const claimSecondWalletAgain = await secondWallet.call(
- WalletApiOperation.PreparePayForUri,
- { talerPayUri: order.uri },
- );
+ // Now the second wallet should realize that the first wallet indeed
+ // paid.
- t.assertTrue(
- claimSecondWalletAgain.status === PreparePayResultType.AlreadyConfirmed,
- );
- t.assertTrue(claimSecondWalletAgain.paid);
+ // Manually trigger checking the status. In the future,
+ // this might happen automatically via long-polling
+ // in the dialog state.
+
+ await secondWallet.call(WalletApiOperation.PreparePayForUri, {
+ talerPayUri: order.uri,
+ });
+
+ await secondWallet.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: claimSecondWallet.transactionId,
+ txState: {
+ major: TransactionMajorState.Failed,
+ minor: TransactionMinorState.PaidByOther,
+ },
+ });
}
t.logStep("second-case-done");
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
@@ -1348,11 +1348,19 @@ export interface PurchaseRecord {
posConfirmation: string | undefined;
/**
+ * This purchase was shared with another wallet
+ * that is now supposed to finish the payment.
+ */
+ shared: boolean;
+
+ /**
* This purchase was created by reading
* a payment share or the wallet
* the nonce public by a payment share
+ *
+ * Defaults to false.
*/
- shared: boolean;
+ createdFromShared?: boolean;
/**
* When was the purchase record created?
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -74,6 +74,7 @@ import {
RefreshReason,
RefundInfoShort,
RefundPaymentInfo,
+ ScopeInfo,
SelectedProspectiveCoin,
SharePaymentResult,
StartRefundQueryForUriResponse,
@@ -205,7 +206,7 @@ export class PayMerchantTransactionContext implements TransactionContext {
return;
}
if (!purchaseRec.download) {
- // Transaction is not reportable yet
+ // Not ready yet.
await tx.transactionsMeta.delete(this.transactionId);
return;
}
@@ -213,7 +214,7 @@ export class PayMerchantTransactionContext implements TransactionContext {
transactionId: this.transactionId,
status: purchaseRec.purchaseStatus,
timestamp: purchaseRec.timestamp,
- currency: purchaseRec.download?.currency,
+ currency: purchaseRec.download?.currency ?? "UNKNOWN",
exchanges: purchaseRec.exchanges ?? [],
});
}
@@ -270,29 +271,32 @@ export class PayMerchantTransactionContext implements TransactionContext {
}),
}));
- const timestamp = purchaseRec.timestampAccept;
- if (!timestamp) {
- return undefined;
- }
- if (!purchaseRec.payInfo) {
- return undefined;
- }
-
const txState = computePayMerchantTransactionState(purchaseRec);
- return {
- type: TransactionType.Payment,
- txState,
- scopes: await getScopeForAllCoins(
+
+ let scopes: ScopeInfo[];
+ let amountEffective: AmountString;
+
+ if (!purchaseRec.payInfo) {
+ scopes = [];
+ amountEffective = Amounts.stringify(zero);
+ } else {
+ scopes = await getScopeForAllCoins(
tx,
!purchaseRec.payInfo.payCoinSelection
? []
: purchaseRec.payInfo.payCoinSelection.coinPubs,
- ),
+ );
+ amountEffective = isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(zero)
+ : Amounts.stringify(purchaseRec.payInfo.totalPayCost);
+ }
+ return {
+ type: TransactionType.Payment,
+ txState,
+ scopes,
txActions: computePayMerchantTransactionActions(purchaseRec),
amountRaw: Amounts.stringify(contractData.amount),
- amountEffective: isUnsuccessfulTransaction(txState)
- ? Amounts.stringify(zero)
- : Amounts.stringify(purchaseRec.payInfo.totalPayCost),
+ amountEffective,
totalRefundRaw: Amounts.stringify(zero), // FIXME!
totalRefundEffective: Amounts.stringify(zero), // FIXME!
refundPending:
@@ -301,7 +305,7 @@ export class PayMerchantTransactionContext implements TransactionContext {
: Amounts.stringify(purchaseRec.refundAmountAwaiting),
refunds,
posConfirmation: purchaseRec.posConfirmation,
- timestamp: timestampPreciseFromDb(timestamp),
+ timestamp: timestampPreciseFromDb(purchaseRec.timestamp),
transactionId: constructTransactionIdentifier({
tag: TransactionType.Payment,
proposalId: purchaseRec.proposalId,
@@ -1220,7 +1224,7 @@ async function createOrReusePurchase(
PurchaseStatus[oldProposal.purchaseStatus]
}) for order ${orderId} at ${merchantBaseUrl}`,
);
- if (oldProposal.purchaseStatus === PurchaseStatus.DialogShared) {
+ if (oldProposal.shared || oldProposal.createdFromShared) {
const download = await expectProposalDownload(wex, oldProposal);
const paid = await checkIfOrderIsAlreadyPaid(
wex,
@@ -1263,9 +1267,9 @@ async function createOrReusePurchase(
}
let noncePair: EddsaKeyPairStrings;
- let shared = false;
+ let createdFromShared = false;
if (noncePriv) {
- shared = true;
+ createdFromShared = true;
noncePair = {
priv: noncePriv,
pub: (await wex.cryptoApi.eddsaGetPublic({ priv: noncePriv })).pub,
@@ -1303,7 +1307,8 @@ async function createOrReusePurchase(
timestampLastRefundStatus: undefined,
pendingRemovedCoinPubs: undefined,
posConfirmation: undefined,
- shared: shared,
+ shared: false,
+ createdFromShared,
};
const ctx = new PayMerchantTransactionContext(wex, proposalId);