aboutsummaryrefslogtreecommitdiff
path: root/src/wallet-impl/pay.ts
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2019-12-05 19:38:19 +0100
committerFlorian Dold <florian.dold@gmail.com>2019-12-05 19:38:19 +0100
commitf67d7f54f9d0fed97446898942e3dfee67ee2985 (patch)
tree2b81738025e8f61250ede10908cbf81071e16975 /src/wallet-impl/pay.ts
parent829acdd3d98f1014747f15ecb619b6fbaa06b640 (diff)
downloadwallet-core-f67d7f54f9d0fed97446898942e3dfee67ee2985.tar.xz
threads, retries and notifications WIP
Diffstat (limited to 'src/wallet-impl/pay.ts')
-rw-r--r--src/wallet-impl/pay.ts207
1 files changed, 173 insertions, 34 deletions
diff --git a/src/wallet-impl/pay.ts b/src/wallet-impl/pay.ts
index 9942139a6..9b2da9c7d 100644
--- a/src/wallet-impl/pay.ts
+++ b/src/wallet-impl/pay.ts
@@ -33,6 +33,8 @@ import {
getTimestampNow,
PreparePayResult,
ConfirmPayResult,
+ OperationError,
+ NotificationType,
} from "../walletTypes";
import {
oneShotIter,
@@ -51,12 +53,14 @@ import {
PurchaseRecord,
CoinRecord,
ProposalStatus,
+ initRetryInfo,
+ updateRetryInfoTimeout,
+ PurchaseStatus,
} from "../dbTypes";
import * as Amounts from "../util/amounts";
import {
amountToPretty,
strcmp,
- extractTalerStamp,
canonicalJson,
extractTalerStampOrThrow,
} from "../util/helpers";
@@ -65,6 +69,8 @@ import { InternalWalletState } from "./state";
import { parsePayUri, parseRefundUri } from "../util/taleruri";
import { getTotalRefreshCost, refresh } from "./refresh";
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
+import { guardOperationException } from "./errors";
+import { assertUnreachable } from "../util/assertUnreachable";
export interface SpeculativePayData {
payCoinInfo: PayCoinInfo;
@@ -344,9 +350,12 @@ async function recordConfirmPay(
payReq,
refundsDone: {},
refundsPending: {},
- timestamp: getTimestampNow(),
- timestamp_refund: undefined,
+ acceptTimestamp: getTimestampNow(),
+ lastRefundTimestamp: undefined,
proposalId: proposal.proposalId,
+ retryInfo: initRetryInfo(),
+ lastError: undefined,
+ status: PurchaseStatus.SubmitPay,
};
await runWithWriteTransaction(
@@ -365,8 +374,10 @@ async function recordConfirmPay(
},
);
- ws.badge.showNotification();
- ws.notifier.notify();
+ ws.notify({
+ type: NotificationType.ProposalAccepted,
+ proposalId: proposal.proposalId,
+ });
return t;
}
@@ -419,7 +430,7 @@ export async function abortFailedPayment(
}
const refundResponse = MerchantRefundResponse.checked(resp.responseJson);
- await acceptRefundResponse(ws, refundResponse);
+ await acceptRefundResponse(ws, purchase.proposalId, refundResponse);
await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
const p = await tx.get(Stores.purchases, proposalId);
@@ -431,10 +442,62 @@ export async function abortFailedPayment(
});
}
+async function incrementProposalRetry(
+ ws: InternalWalletState,
+ proposalId: string,
+ err: OperationError | undefined,
+): Promise<void> {
+ await runWithWriteTransaction(ws.db, [Stores.proposals], async tx => {
+ const pr = await tx.get(Stores.proposals, proposalId);
+ if (!pr) {
+ return;
+ }
+ if (!pr.retryInfo) {
+ return;
+ }
+ pr.retryInfo.retryCounter++;
+ updateRetryInfoTimeout(pr.retryInfo);
+ pr.lastError = err;
+ await tx.put(Stores.proposals, pr);
+ });
+}
+
+async function incrementPurchaseRetry(
+ ws: InternalWalletState,
+ proposalId: string,
+ err: OperationError | undefined,
+): Promise<void> {
+ await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
+ const pr = await tx.get(Stores.purchases, proposalId);
+ if (!pr) {
+ return;
+ }
+ if (!pr.retryInfo) {
+ return;
+ }
+ pr.retryInfo.retryCounter++;
+ updateRetryInfoTimeout(pr.retryInfo);
+ pr.lastError = err;
+ await tx.put(Stores.purchases, pr);
+ });
+}
+
export async function processDownloadProposal(
ws: InternalWalletState,
proposalId: string,
): Promise<void> {
+ const onOpErr = (err: OperationError) =>
+ incrementProposalRetry(ws, proposalId, err);
+ await guardOperationException(
+ () => processDownloadProposalImpl(ws, proposalId),
+ onOpErr,
+ );
+}
+
+async function processDownloadProposalImpl(
+ ws: InternalWalletState,
+ proposalId: string,
+): Promise<void> {
const proposal = await oneShotGet(ws.db, Stores.proposals, proposalId);
if (!proposal) {
return;
@@ -498,7 +561,10 @@ export async function processDownloadProposal(
},
);
- ws.notifier.notify();
+ ws.notify({
+ type: NotificationType.ProposalDownloaded,
+ proposalId: proposal.proposalId,
+ });
}
/**
@@ -536,6 +602,8 @@ async function startDownloadProposal(
proposalId: proposalId,
proposalStatus: ProposalStatus.DOWNLOADING,
repurchaseProposalId: undefined,
+ retryInfo: initRetryInfo(),
+ lastError: undefined,
};
await oneShotPut(ws.db, Stores.proposals, proposalRecord);
@@ -582,6 +650,7 @@ export async function submitPay(
throw Error("merchant payment signature invalid");
}
purchase.finished = true;
+ purchase.retryInfo = initRetryInfo(false);
const modifiedCoins: CoinRecord[] = [];
for (const pc of purchase.payReq.coins) {
const c = await oneShotGet(ws.db, Stores.coins, pc.coin_pub);
@@ -859,8 +928,6 @@ export async function confirmPay(
return submitPay(ws, proposalId, sessionId);
}
-
-
export async function getFullRefundFees(
ws: InternalWalletState,
refundPermissions: MerchantRefundPermission[],
@@ -914,15 +981,13 @@ export async function getFullRefundFees(
return feeAcc;
}
-async function submitRefunds(
+async function submitRefundsToExchange(
ws: InternalWalletState,
proposalId: string,
): Promise<void> {
const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
if (!purchase) {
- console.error(
- "not submitting refunds, payment not found:",
- );
+ console.error("not submitting refunds, payment not found:");
return;
}
const pendingKeys = Object.keys(purchase.refundsPending);
@@ -991,14 +1056,18 @@ async function submitRefunds(
refresh(ws, perm.coin_pub);
}
- ws.badge.showNotification();
- ws.notifier.notify();
+ ws.notify({
+ type: NotificationType.RefundsSubmitted,
+ proposalId,
+ });
}
-export async function acceptRefundResponse(
+
+async function acceptRefundResponse(
ws: InternalWalletState,
+ proposalId: string,
refundResponse: MerchantRefundResponse,
-): Promise<string> {
+): Promise<void> {
const refundPermissions = refundResponse.refund_permissions;
if (!refundPermissions.length) {
@@ -1015,7 +1084,8 @@ export async function acceptRefundResponse(
return;
}
- t.timestamp_refund = getTimestampNow();
+ t.lastRefundTimestamp = getTimestampNow();
+ t.status = PurchaseStatus.ProcessRefund;
for (const perm of refundPermissions) {
if (
@@ -1027,18 +1097,48 @@ export async function acceptRefundResponse(
}
return t;
}
+ // Add the refund permissions to the purchase within a DB transaction
+ await oneShotMutate(ws.db, Stores.purchases, proposalId, f);
+ await submitRefundsToExchange(ws, proposalId);
+}
- const hc = refundResponse.h_contract_terms;
- // Add the refund permissions to the purchase within a DB transaction
- await oneShotMutate(ws.db, Stores.purchases, hc, f);
- ws.notifier.notify();
+async function queryRefund(ws: InternalWalletState, proposalId: string): Promise<void> {
+ const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
+ if (purchase?.status !== PurchaseStatus.QueryRefund) {
+ return;
+ }
- await submitRefunds(ws, hc);
+ const refundUrl = new URL("refund", purchase.contractTerms.merchant_base_url).href
+ let resp;
+ try {
+ resp = await ws.http.get(refundUrl);
+ } catch (e) {
+ console.error("error downloading refund permission", e);
+ throw e;
+ }
- return hc;
+ const refundResponse = MerchantRefundResponse.checked(resp.responseJson);
+ await acceptRefundResponse(ws, proposalId, refundResponse);
}
+async function startRefundQuery(ws: InternalWalletState, proposalId: string): Promise<void> {
+ const success = await runWithWriteTransaction(ws.db, [Stores.purchases], async (tx) => {
+ const p = await tx.get(Stores.purchases, proposalId);
+ if (p?.status !== PurchaseStatus.Done) {
+ return false;
+ }
+ p.status = PurchaseStatus.QueryRefund;
+ return true;
+ });
+
+ if (!success) {
+ return;
+ }
+ await queryRefund(ws, proposalId);
+}
+
+
/**
* Accept a refund, return the contract hash for the contract
* that was involved in the refund.
@@ -1053,17 +1153,56 @@ export async function applyRefund(
throw Error("invalid refund URI");
}
- const refundUrl = parseResult.refundUrl;
+ const purchase = await oneShotGetIndexed(
+ ws.db,
+ Stores.purchases.orderIdIndex,
+ [parseResult.merchantBaseUrl, parseResult.orderId],
+ );
- logger.trace("processing refund");
- let resp;
- try {
- resp = await ws.http.get(refundUrl);
- } catch (e) {
- console.error("error downloading refund permission", e);
- throw e;
+ if (!purchase) {
+ throw Error("no purchase for the taler://refund/ URI was found");
}
- const refundResponse = MerchantRefundResponse.checked(resp.responseJson);
- return acceptRefundResponse(ws, refundResponse);
+ await startRefundQuery(ws, purchase.proposalId);
+
+ return purchase.contractTermsHash;
+}
+
+export async function processPurchase(
+ ws: InternalWalletState,
+ proposalId: string,
+): Promise<void> {
+ const onOpErr = (e: OperationError) =>
+ incrementPurchaseRetry(ws, proposalId, e);
+ await guardOperationException(
+ () => processPurchaseImpl(ws, proposalId),
+ onOpErr,
+ );
+}
+
+export async function processPurchaseImpl(
+ ws: InternalWalletState,
+ proposalId: string,
+): Promise<void> {
+ const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
+ if (!purchase) {
+ return;
+ }
+ switch (purchase.status) {
+ case PurchaseStatus.Done:
+ return;
+ case PurchaseStatus.Abort:
+ // FIXME
+ break;
+ case PurchaseStatus.SubmitPay:
+ break;
+ case PurchaseStatus.QueryRefund:
+ await queryRefund(ws, proposalId);
+ break;
+ case PurchaseStatus.ProcessRefund:
+ await submitRefundsToExchange(ws, proposalId);
+ break;
+ default:
+ throw assertUnreachable(purchase.status);
+ }
}