aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2023-01-11 17:12:08 +0100
committerFlorian Dold <florian@dold.me>2023-01-11 17:14:49 +0100
commit143a4fe4ac5b8724cf6e13a704e88daa99dd4202 (patch)
tree07b2fe6d993ae46838bc4072db8eba609bbb1801 /packages/taler-wallet-core/src/operations
parent5fc0cb7927e7a81a80129f6165b2027e72b89d33 (diff)
downloadwallet-core-143a4fe4ac5b8724cf6e13a704e88daa99dd4202.tar.xz
wallet-core: refresh when aborting payments
Diffstat (limited to 'packages/taler-wallet-core/src/operations')
-rw-r--r--packages/taler-wallet-core/src/operations/backup/export.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/pay-merchant.ts118
-rw-r--r--packages/taler-wallet-core/src/operations/transactions.ts68
4 files changed, 135 insertions, 55 deletions
diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts
index 965d51776..a66444485 100644
--- a/packages/taler-wallet-core/src/operations/backup/export.ts
+++ b/packages/taler-wallet-core/src/operations/backup/export.ts
@@ -89,7 +89,7 @@ export async function exportBackup(
x.config,
x.exchanges,
x.exchangeDetails,
- x.exchangeSignkeys,
+ x.exchangeSignKeys,
x.coins,
x.contractTerms,
x.denominations,
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts
index b6e2a9d73..67f77de77 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -675,7 +675,7 @@ export async function updateExchangeFromUrlHandler(
x.exchanges,
x.exchangeTos,
x.exchangeDetails,
- x.exchangeSignkeys,
+ x.exchangeSignKeys,
x.denominations,
x.coins,
x.refreshGroups,
diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts
index c8e3230b9..2f1f3375c 100644
--- a/packages/taler-wallet-core/src/operations/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts
@@ -125,6 +125,7 @@ import {
} from "../util/retries.js";
import {
makeTransactionId,
+ runOperationWithErrorReporting,
spendCoins,
storeOperationError,
storeOperationPending,
@@ -1135,9 +1136,9 @@ export function selectForced(
export type SelectPayCoinsResult =
| {
- type: "failure";
- insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails;
- }
+ type: "failure";
+ insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails;
+ }
| { type: "success"; coinSel: PayCoinSelection };
/**
@@ -1594,7 +1595,12 @@ export async function runPayForConfirmPay(
ws: InternalWalletState,
proposalId: string,
): Promise<ConfirmPayResult> {
- const res = await processPurchasePay(ws, proposalId, { forceNow: true });
+ logger.trace("processing proposal for confirmPay");
+ const opId = RetryTags.byPaymentProposalId(proposalId);
+ const res = await runOperationWithErrorReporting(ws, opId, async () => {
+ return await processPurchasePay(ws, proposalId, { forceNow: true });
+ });
+ logger.trace(`processPurchasePay response type ${res.type}`);
switch (res.type) {
case OperationAttemptResultType.Finished: {
const purchase = await ws.db
@@ -1623,9 +1629,10 @@ export async function runPayForConfirmPay(
const numRetry = opRetry?.retryInfo.retryCounter ?? 0;
if (
res.errorDetail.code ===
- TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR &&
+ TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR &&
numRetry < maxRetry
) {
+ logger.trace("hiding transient error from caller");
// Pretend the operation is pending instead of reporting
// an error, but only up to maxRetry attempts.
await storeOperationPending(
@@ -1638,20 +1645,11 @@ export async function runPayForConfirmPay(
transactionId: makeTransactionId(TransactionType.Payment, proposalId),
};
} else {
- // FIXME: allocate error code!
- await storeOperationError(
- ws,
- RetryTags.byPaymentProposalId(proposalId),
- res.errorDetail,
- );
throw Error("payment failed");
}
}
case OperationAttemptResultType.Pending:
- await storeOperationPending(
- ws,
- `${PendingTaskType.Purchase}:${proposalId}`,
- );
+ logger.trace("reporting pending as confirmPay response");
return {
type: ConfirmPayResultType.Pending,
transactionId: makeTransactionId(TransactionType.Payment, proposalId),
@@ -1968,29 +1966,12 @@ export async function processPurchasePay(
result: undefined,
};
}
+ }
- if (resp.status >= 400 && resp.status <= 499) {
- const errDetails = await readUnexpectedResponseDetails(resp);
- logger.warn(`server returned ${resp.status} response for /pay`);
- logger.warn(j2s(errDetails));
- await ws.db
- .mktx((x) => [x.purchases])
- .runReadWrite(async (tx) => {
- const purch = await tx.purchases.get(proposalId);
- if (!purch) {
- return;
- }
- // FIXME: Should be some "PayPermanentlyFailed" and error info should be stored
- purch.purchaseStatus = PurchaseStatus.PaymentAbortFinished;
- await tx.purchases.put(purch);
- });
- throw makePendingOperationFailedError(
- errDetails,
- TransactionType.Payment,
- proposalId,
- );
- }
-
+ if (resp.status >= 400 && resp.status <= 499) {
+ logger.trace("got generic 4xx from merchant");
+ const err = await readTalerErrorResponse(resp);
+ throwUnexpectedRequestError(resp, err);
}
const merchantResp = await readSuccessResponseJsonOrThrow(
@@ -2395,16 +2376,18 @@ async function acceptRefunds(
}
}
- const refreshCoinsPubs = Object.values(refreshCoinsMap);
- logger.info(`refreshCoinMap ${j2s(refreshCoinsMap)}`);
- if (refreshCoinsPubs.length > 0) {
- await createRefreshGroup(
- ws,
- tx,
- Amounts.currencyOf(refreshCoinsPubs[0].amount),
- refreshCoinsPubs,
- RefreshReason.Refund,
- );
+ if (reason === RefundReason.AbortRefund) {
+ const refreshCoinsPubs = Object.values(refreshCoinsMap);
+ logger.info(`refreshCoinMap ${j2s(refreshCoinsMap)}`);
+ if (refreshCoinsPubs.length > 0) {
+ await createRefreshGroup(
+ ws,
+ tx,
+ Amounts.currencyOf(refreshCoinsPubs[0].amount),
+ refreshCoinsPubs,
+ RefreshReason.Refund,
+ );
+ }
}
// Are we done with querying yet, or do we need to do another round
@@ -2808,12 +2791,21 @@ export async function processPurchaseQueryRefund(
return OperationAttemptResult.finishedEmpty();
}
-export async function abortFailedPayWithRefund(
+export async function abortPay(
ws: InternalWalletState,
proposalId: string,
+ cancelImmediately?: boolean,
): Promise<void> {
+ const opId = RetryTags.byPaymentProposalId(proposalId);
await ws.db
- .mktx((x) => [x.purchases])
+ .mktx((x) => [
+ x.purchases,
+ x.refreshGroups,
+ x.denominations,
+ x.coinAvailability,
+ x.coins,
+ x.operationRetries,
+ ])
.runReadWrite(async (tx) => {
const purchase = await tx.purchases.get(proposalId);
if (!purchase) {
@@ -2828,10 +2820,30 @@ export async function abortFailedPayWithRefund(
purchase.purchaseStatus = PurchaseStatus.AbortingWithRefund;
}
await tx.purchases.put(purchase);
+ await tx.operationRetries.delete(opId);
+ if (purchase.payInfo) {
+ const coinSel = purchase.payInfo.payCoinSelection;
+ const currency = Amounts.currencyOf(purchase.payInfo.totalPayCost);
+ const refreshCoins: CoinRefreshRequest[] = [];
+ for (let i = 0; i < coinSel.coinPubs.length; i++) {
+ refreshCoins.push({
+ amount: coinSel.coinContributions[i],
+ coinPub: coinSel.coinPubs[i],
+ });
+ }
+ await createRefreshGroup(
+ ws,
+ tx,
+ currency,
+ refreshCoins,
+ RefreshReason.AbortPay,
+ );
+ }
+ });
+
+ runOperationWithErrorReporting(ws, opId, async () => {
+ return await processPurchaseQueryRefund(ws, proposalId, {
+ forceNow: true,
});
- processPurchaseQueryRefund(ws, proposalId, {
- forceNow: true,
- }).catch((e) => {
- logger.trace(`error during refund processing after abort pay: ${e}`);
});
}
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts
index 3fb13d2a0..2570a4e5f 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -23,6 +23,7 @@ import {
Amounts,
constructPayPullUri,
constructPayPushUri,
+ ExtendedStatus,
Logger,
OrderShortInfo,
PaymentStatus,
@@ -66,6 +67,7 @@ import {
import { processDepositGroup } from "./deposits.js";
import { getExchangeDetails } from "./exchanges.js";
import {
+ abortPay,
expectProposalDownload,
extractContractData,
processPurchasePay,
@@ -352,6 +354,10 @@ function buildTransactionForPushPaymentDebit(
summary: contractTerms.summary,
},
frozen: false,
+ extendedStatus:
+ pi.status != PeerPushPaymentInitiationStatus.PurseCreated
+ ? ExtendedStatus.Pending
+ : ExtendedStatus.Done,
pending: pi.status != PeerPushPaymentInitiationStatus.PurseCreated,
timestamp: pi.timestampCreated,
talerUri: constructPayPushUri({
@@ -377,6 +383,7 @@ function buildTransactionForPullPaymentDebit(
exchangeBaseUrl: pi.exchangeBaseUrl,
frozen: false,
pending: false,
+ extendedStatus: ExtendedStatus.Done,
info: {
expiration: pi.contractTerms.purse_expiration,
summary: pi.contractTerms.summary,
@@ -401,6 +408,7 @@ function buildTransactionForPullPaymentCredit(
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
amountRaw: Amounts.stringify(wsr.instructedAmount),
exchangeBaseUrl: wsr.exchangeBaseUrl,
+ extendedStatus: ExtendedStatus.Done,
pending: !wsr.timestampFinish,
timestamp: wsr.timestampStart,
info: {
@@ -435,6 +443,9 @@ function buildTransactionForPushPaymentCredit(
expiration: wsr.wgInfo.contractTerms.purse_expiration,
summary: wsr.wgInfo.contractTerms.summary,
},
+ extendedStatus: wsr.timestampFinish
+ ? ExtendedStatus.Done
+ : ExtendedStatus.Pending,
pending: !wsr.timestampFinish,
timestamp: wsr.timestampStart,
transactionId: makeTransactionId(
@@ -464,6 +475,9 @@ function buildTransactionForBankIntegratedWithdraw(
bankConfirmationUrl: wsr.wgInfo.bankInfo.confirmUrl,
},
exchangeBaseUrl: wsr.exchangeBaseUrl,
+ extendedStatus: wsr.timestampFinish
+ ? ExtendedStatus.Done
+ : ExtendedStatus.Pending,
pending: !wsr.timestampFinish,
timestamp: wsr.timestampStart,
transactionId: makeTransactionId(
@@ -504,6 +518,9 @@ function buildTransactionForManualWithdraw(
exchangePaytoUris,
},
exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl,
+ extendedStatus: withdrawalGroup.timestampFinish
+ ? ExtendedStatus.Done
+ : ExtendedStatus.Pending,
pending: !withdrawalGroup.timestampFinish,
timestamp: withdrawalGroup.timestampStart,
transactionId: makeTransactionId(
@@ -523,6 +540,9 @@ function buildTransactionForDeposit(
type: TransactionType.Deposit,
amountRaw: Amounts.stringify(dg.effectiveDepositAmount),
amountEffective: Amounts.stringify(dg.totalPayCost),
+ extendedStatus: dg.timestampFinished
+ ? ExtendedStatus.Done
+ : ExtendedStatus.Pending,
pending: !dg.timestampFinished,
frozen: false,
timestamp: dg.timestampCreated,
@@ -547,6 +567,9 @@ function buildTransactionForTip(
type: TransactionType.Tip,
amountEffective: Amounts.stringify(tipRecord.tipAmountEffective),
amountRaw: Amounts.stringify(tipRecord.tipAmountRaw),
+ extendedStatus: tipRecord.pickedUpTimestamp
+ ? ExtendedStatus.Done
+ : ExtendedStatus.Pending,
pending: !tipRecord.pickedUpTimestamp,
frozen: false,
timestamp: tipRecord.acceptedTimestamp,
@@ -654,6 +677,7 @@ async function buildTransactionForRefund(
purchaseRecord.refundAmountAwaiting === undefined
? undefined
: Amounts.stringify(purchaseRecord.refundAmountAwaiting),
+ extendedStatus: ExtendedStatus.Done,
pending: false,
frozen: false,
...(ort?.lastError ? { error: ort.lastError } : {}),
@@ -710,6 +734,33 @@ async function buildTransactionForPurchase(
checkDbInvariant(!!timestamp);
checkDbInvariant(!!purchaseRecord.payInfo);
+
+ let status: ExtendedStatus;
+ switch (purchaseRecord.purchaseStatus) {
+ case PurchaseStatus.AbortingWithRefund:
+ status = ExtendedStatus.Aborting;
+ break;
+ case PurchaseStatus.Paid:
+ case PurchaseStatus.RepurchaseDetected:
+ status = ExtendedStatus.Done;
+ break;
+ case PurchaseStatus.DownloadingProposal:
+ case PurchaseStatus.QueryingRefund:
+ case PurchaseStatus.Proposed:
+ case PurchaseStatus.Paying:
+ status = ExtendedStatus.Pending;
+ break;
+ case PurchaseStatus.ProposalDownloadFailed:
+ status = ExtendedStatus.Failed;
+ break;
+ case PurchaseStatus.PaymentAbortFinished:
+ status = ExtendedStatus.Aborted;
+ break;
+ default:
+ // FIXME: Should we have some unknown status?
+ status = ExtendedStatus.Pending;
+ }
+
return {
type: TransactionType.Payment,
amountRaw: Amounts.stringify(contractData.amount),
@@ -723,6 +774,7 @@ async function buildTransactionForPurchase(
status: purchaseRecord.timestampFirstSuccessfulPay
? PaymentStatus.Paid
: PaymentStatus.Accepted,
+ extendedStatus: status,
pending: purchaseRecord.purchaseStatus === PurchaseStatus.Paying,
refunds,
timestamp,
@@ -1163,3 +1215,19 @@ export async function deleteTransaction(
throw Error(`can't delete a '${unknownTxType}' transaction`);
}
}
+
+export async function abortTransaction(
+ ws: InternalWalletState,
+ transactionId: string,
+ forceImmediateAbort?: boolean,
+): Promise<void> {
+ const { type, args: rest } = parseId("txn", transactionId);
+
+ if (type === TransactionType.Payment) {
+ const proposalId = rest[0];
+ await abortPay(ws, proposalId, forceImmediateAbort);
+ } else {
+ const unknownTxType: any = type;
+ throw Error(`can't abort a '${unknownTxType}' transaction`);
+ }
+}