taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Mpackages/taler-harness/src/integrationtests/test-payment-share.ts | 32+++++++++++++++++++-------------
Mpackages/taler-wallet-core/src/db.ts | 10+++++++++-
Mpackages/taler-wallet-core/src/pay-merchant.ts | 51++++++++++++++++++++++++++++-----------------------
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);