aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2019-12-06 00:24:34 +0100
committerFlorian Dold <florian.dold@gmail.com>2019-12-06 00:24:34 +0100
commit65bccbd139c53a2baccec442a680373125488102 (patch)
tree216860ec3523af33091b8fb52193787034c667f8
parent7b54439fd62bd2a5e15b3068a8fbaffeb0a57468 (diff)
separate operations for pay, refund status query and refund submission
-rw-r--r--src/dbTypes.ts50
-rw-r--r--src/wallet-impl/balance.ts2
-rw-r--r--src/wallet-impl/exchanges.ts19
-rw-r--r--src/wallet-impl/history.ts4
-rw-r--r--src/wallet-impl/pay.ts418
-rw-r--r--src/wallet-impl/pending.ts66
-rw-r--r--src/wallet.ts14
-rw-r--r--src/walletTypes.ts20
8 files changed, 359 insertions, 234 deletions
diff --git a/src/dbTypes.ts b/src/dbTypes.ts
index 096c3f04e..e39d73672 100644
--- a/src/dbTypes.ts
+++ b/src/dbTypes.ts
@@ -970,18 +970,6 @@ export interface WireFee {
sig: string;
}
-export enum PurchaseStatus {
- /**
- * We're currently paying, either for the first
- * time or as a re-play potentially with a different
- * session ID.
- */
- SubmitPay = "submit-pay",
- QueryRefund = "query-refund",
- ProcessRefund = "process-refund",
- Abort = "abort",
- Dormant = "dormant",
-}
/**
* Record that stores status information about one purchase, starting from when
@@ -995,11 +983,6 @@ export interface PurchaseRecord {
proposalId: string;
/**
- * Status of this purchase.
- */
- status: PurchaseStatus;
-
- /**
* Hash of the contract terms.
*/
contractTermsHash: string;
@@ -1021,10 +1004,9 @@ export interface PurchaseRecord {
merchantSig: string;
/**
- * The purchase isn't active anymore, it's either successfully paid or
- * refunded/aborted.
+ * A successful payment has been made.
*/
- finished: boolean;
+ payFinished: boolean;
/**
* Pending refunds for the purchase.
@@ -1046,13 +1028,15 @@ export interface PurchaseRecord {
* When was the last refund made?
* Set to 0 if no refund was made on the purchase.
*/
- lastRefundTimestamp: Timestamp | undefined;
+ lastRefundStatusTimestamp: Timestamp | undefined;
/**
* Last session signature that we submitted to /pay (if any).
*/
lastSessionId: string | undefined;
+ refundStatusRequested: boolean;
+
/**
* An abort (with refund) was requested for this (incomplete!) purchase.
*/
@@ -1063,9 +1047,29 @@ export interface PurchaseRecord {
*/
abortDone: boolean;
- retryInfo: RetryInfo;
+ payRetryInfo: RetryInfo;
- lastError: OperationError | undefined;
+ lastPayError: OperationError | undefined;
+
+ /**
+ * Retry information for querying the refund status with the merchant.
+ */
+ refundStatusRetryInfo: RetryInfo;
+
+ /**
+ * Last error (or undefined) for querying the refund status with the merchant.
+ */
+ lastRefundStatusError: OperationError | undefined;
+
+ /**
+ * Retry information for querying the refund status with the merchant.
+ */
+ refundApplyRetryInfo: RetryInfo;
+
+ /**
+ * Last error (or undefined) for querying the refund status with the merchant.
+ */
+ lastRefundApplyError: OperationError | undefined;
}
/**
diff --git a/src/wallet-impl/balance.ts b/src/wallet-impl/balance.ts
index a1351014c..082e62563 100644
--- a/src/wallet-impl/balance.ts
+++ b/src/wallet-impl/balance.ts
@@ -138,7 +138,7 @@ export async function getBalances(
});
await tx.iter(Stores.purchases).forEach(t => {
- if (t.finished) {
+ if (t.payFinished) {
return;
}
for (const c of t.payReq.coins) {
diff --git a/src/wallet-impl/exchanges.ts b/src/wallet-impl/exchanges.ts
index b6a2f1c8a..3814971a3 100644
--- a/src/wallet-impl/exchanges.ts
+++ b/src/wallet-impl/exchanges.ts
@@ -44,7 +44,10 @@ import {
} from "../util/query";
import * as Amounts from "../util/amounts";
import { parsePaytoUri } from "../util/payto";
-import { OperationFailedAndReportedError } from "./errors";
+import {
+ OperationFailedAndReportedError,
+ guardOperationException,
+} from "./errors";
async function denominationRecordFromKeys(
ws: InternalWalletState,
@@ -307,12 +310,24 @@ async function updateExchangeWithWireInfo(
});
}
+export async function updateExchangeFromUrl(
+ ws: InternalWalletState,
+ baseUrl: string,
+ force: boolean = false,
+): Promise<ExchangeRecord> {
+ const onOpErr = (e: OperationError) => setExchangeError(ws, baseUrl, e);
+ return await guardOperationException(
+ () => updateExchangeFromUrlImpl(ws, baseUrl, force),
+ onOpErr,
+ );
+}
+
/**
* Update or add exchange DB entry by fetching the /keys and /wire information.
* Optionally link the reserve entry to the new or existing
* exchange entry in then DB.
*/
-export async function updateExchangeFromUrl(
+async function updateExchangeFromUrlImpl(
ws: InternalWalletState,
baseUrl: string,
force: boolean = false,
diff --git a/src/wallet-impl/history.ts b/src/wallet-impl/history.ts
index 5e93ab878..23887e895 100644
--- a/src/wallet-impl/history.ts
+++ b/src/wallet-impl/history.ts
@@ -82,7 +82,7 @@ export async function getHistory(
type: "pay",
explicit: false,
});
- if (p.lastRefundTimestamp) {
+ if (p.lastRefundStatusTimestamp) {
const contractAmount = Amounts.parseOrThrow(p.contractTerms.amount);
const amountsPending = Object.keys(p.refundsPending).map(x =>
Amounts.parseOrThrow(p.refundsPending[x].refund_amount),
@@ -103,7 +103,7 @@ export async function getHistory(
merchantName: p.contractTerms.merchant.name,
refundAmount: amount,
},
- timestamp: p.lastRefundTimestamp,
+ timestamp: p.lastRefundStatusTimestamp,
type: "refund",
explicit: false,
});
diff --git a/src/wallet-impl/pay.ts b/src/wallet-impl/pay.ts
index f07b0328c..cec1b6bc5 100644
--- a/src/wallet-impl/pay.ts
+++ b/src/wallet-impl/pay.ts
@@ -55,7 +55,6 @@ import {
ProposalStatus,
initRetryInfo,
updateRetryInfoTimeout,
- PurchaseStatus,
} from "../dbTypes";
import * as Amounts from "../util/amounts";
import {
@@ -344,18 +343,22 @@ async function recordConfirmPay(
abortRequested: false,
contractTerms: d.contractTerms,
contractTermsHash: d.contractTermsHash,
- finished: false,
+ payFinished: false,
lastSessionId: undefined,
merchantSig: d.merchantSig,
payReq,
refundsDone: {},
refundsPending: {},
acceptTimestamp: getTimestampNow(),
- lastRefundTimestamp: undefined,
+ lastRefundStatusTimestamp: undefined,
proposalId: proposal.proposalId,
- retryInfo: initRetryInfo(),
- lastError: undefined,
- status: PurchaseStatus.SubmitPay,
+ lastPayError: undefined,
+ lastRefundStatusError: undefined,
+ payRetryInfo: initRetryInfo(),
+ refundStatusRetryInfo: initRetryInfo(),
+ refundStatusRequested: false,
+ lastRefundApplyError: undefined,
+ refundApplyRetryInfo: initRetryInfo(),
};
await runWithWriteTransaction(
@@ -402,7 +405,7 @@ export async function abortFailedPayment(
if (!purchase) {
throw Error("Purchase not found, unable to abort with refund");
}
- if (purchase.finished) {
+ if (purchase.payFinished) {
throw Error("Purchase already finished, not aborting");
}
if (purchase.abortDone) {
@@ -464,23 +467,65 @@ async function incrementProposalRetry(
});
}
-async function incrementPurchaseRetry(
+async function incrementPurchasePayRetry(
ws: InternalWalletState,
proposalId: string,
err: OperationError | undefined,
): Promise<void> {
- console.log("incrementing purchase retry with error", err);
+ console.log("incrementing purchase pay retry with error", err);
await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
const pr = await tx.get(Stores.purchases, proposalId);
if (!pr) {
return;
}
- if (!pr.retryInfo) {
+ if (!pr.payRetryInfo) {
return;
}
- pr.retryInfo.retryCounter++;
- updateRetryInfoTimeout(pr.retryInfo);
- pr.lastError = err;
+ pr.payRetryInfo.retryCounter++;
+ updateRetryInfoTimeout(pr.payRetryInfo);
+ pr.lastPayError = err;
+ await tx.put(Stores.purchases, pr);
+ });
+}
+
+async function incrementPurchaseQueryRefundRetry(
+ ws: InternalWalletState,
+ proposalId: string,
+ err: OperationError | undefined,
+): Promise<void> {
+ console.log("incrementing purchase refund query retry with error", err);
+ await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
+ const pr = await tx.get(Stores.purchases, proposalId);
+ if (!pr) {
+ return;
+ }
+ if (!pr.refundStatusRetryInfo) {
+ return;
+ }
+ pr.refundStatusRetryInfo.retryCounter++;
+ updateRetryInfoTimeout(pr.refundStatusRetryInfo);
+ pr.lastRefundStatusError = err;
+ await tx.put(Stores.purchases, pr);
+ });
+}
+
+async function incrementPurchaseApplyRefundRetry(
+ ws: InternalWalletState,
+ proposalId: string,
+ err: OperationError | undefined,
+): Promise<void> {
+ console.log("incrementing purchase refund apply retry with error", err);
+ await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
+ const pr = await tx.get(Stores.purchases, proposalId);
+ if (!pr) {
+ return;
+ }
+ if (!pr.refundApplyRetryInfo) {
+ return;
+ }
+ pr.refundApplyRetryInfo.retryCounter++;
+ updateRetryInfoTimeout(pr.refundStatusRetryInfo);
+ pr.lastRefundApplyError = err;
await tx.put(Stores.purchases, pr);
});
}
@@ -652,10 +697,9 @@ export async function submitPay(
// FIXME: properly display error
throw Error("merchant payment signature invalid");
}
- purchase.finished = true;
- purchase.status = PurchaseStatus.Dormant;
- purchase.lastError = undefined;
- purchase.retryInfo = initRetryInfo(false);
+ purchase.payFinished = true;
+ purchase.lastPayError = undefined;
+ purchase.payRetryInfo = initRetryInfo(false);
const modifiedCoins: CoinRecord[] = [];
for (const pc of purchase.payReq.coins) {
const c = await oneShotGet(ws.db, Stores.coins, pc.coin_pub);
@@ -986,90 +1030,6 @@ export async function getFullRefundFees(
return feeAcc;
}
-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:");
- return;
- }
- const pendingKeys = Object.keys(purchase.refundsPending);
- if (pendingKeys.length === 0) {
- console.log("no pending refunds");
- return;
- }
- for (const pk of pendingKeys) {
- const perm = purchase.refundsPending[pk];
- const req: RefundRequest = {
- coin_pub: perm.coin_pub,
- h_contract_terms: purchase.contractTermsHash,
- merchant_pub: purchase.contractTerms.merchant_pub,
- merchant_sig: perm.merchant_sig,
- refund_amount: perm.refund_amount,
- refund_fee: perm.refund_fee,
- rtransaction_id: perm.rtransaction_id,
- };
- console.log("sending refund permission", perm);
- // FIXME: not correct once we support multiple exchanges per payment
- const exchangeUrl = purchase.payReq.coins[0].exchange_url;
- const reqUrl = new URL("refund", exchangeUrl);
- const resp = await ws.http.postJson(reqUrl.href, req);
- console.log("sent refund permission");
- if (resp.status !== 200) {
- console.error("refund failed", resp);
- continue;
- }
-
- let allRefundsProcessed = false;
-
- await runWithWriteTransaction(
- ws.db,
- [Stores.purchases, Stores.coins],
- async tx => {
- const p = await tx.get(Stores.purchases, proposalId);
- if (!p) {
- return;
- }
- if (p.refundsPending[pk]) {
- p.refundsDone[pk] = p.refundsPending[pk];
- delete p.refundsPending[pk];
- }
- if (Object.keys(p.refundsPending).length === 0) {
- p.retryInfo = initRetryInfo();
- p.lastError = undefined;
- p.status = PurchaseStatus.Dormant;
- allRefundsProcessed = true;
- }
- await tx.put(Stores.purchases, p);
- const c = await tx.get(Stores.coins, perm.coin_pub);
- if (!c) {
- console.warn("coin not found, can't apply refund");
- return;
- }
- const refundAmount = Amounts.parseOrThrow(perm.refund_amount);
- const refundFee = Amounts.parseOrThrow(perm.refund_fee);
- c.status = CoinStatus.Dirty;
- c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
- c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
- await tx.put(Stores.coins, c);
- },
- );
- if (allRefundsProcessed) {
- ws.notify({
- type: NotificationType.RefundFinished,
- })
- }
- await refresh(ws, perm.coin_pub);
- }
-
- ws.notify({
- type: NotificationType.RefundsSubmitted,
- proposalId,
- });
-}
-
async function acceptRefundResponse(
ws: InternalWalletState,
proposalId: string,
@@ -1082,60 +1042,45 @@ async function acceptRefundResponse(
throw Error("empty refund");
}
- /**
- * Add refund to purchase if not already added.
- */
- function f(t: PurchaseRecord | undefined): PurchaseRecord | undefined {
- if (!t) {
+
+ let numNewRefunds = 0;
+
+ await runWithWriteTransaction(ws.db, [Stores.purchases], async (tx) => {
+ const p = await tx.get(Stores.purchases, proposalId);
+ if (!p) {
console.error("purchase not found, not adding refunds");
return;
}
- t.lastRefundTimestamp = getTimestampNow();
- t.status = PurchaseStatus.ProcessRefund;
- t.lastError = undefined;
- t.retryInfo = initRetryInfo();
+ if (!p.refundStatusRequested) {
+ return;
+ }
+
+ p.lastRefundStatusTimestamp = getTimestampNow();
+ p.lastRefundStatusError = undefined;
+ p.refundStatusRetryInfo = initRetryInfo();
+ p.refundStatusRequested = false;
for (const perm of refundPermissions) {
if (
- !t.refundsPending[perm.merchant_sig] &&
- !t.refundsDone[perm.merchant_sig]
+ !p.refundsPending[perm.merchant_sig] &&
+ !p.refundsDone[perm.merchant_sig]
) {
- t.refundsPending[perm.merchant_sig] = perm;
+ p.refundsPending[perm.merchant_sig] = perm;
+ numNewRefunds++;
}
}
- 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);
-}
-async function queryRefund(
- ws: InternalWalletState,
- proposalId: string,
-): Promise<void> {
- const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
- if (purchase?.status !== PurchaseStatus.QueryRefund) {
- return;
- }
+ if (numNewRefunds) {
+ p.lastRefundApplyError = undefined;
+ p.refundApplyRetryInfo = initRetryInfo();
+ }
- const refundUrlObj = new URL(
- "refund",
- purchase.contractTerms.merchant_base_url,
- );
- refundUrlObj.searchParams.set("order_id", purchase.contractTerms.order_id);
- const refundUrl = refundUrlObj.href;
- let resp;
- try {
- resp = await ws.http.get(refundUrl);
- } catch (e) {
- console.error("error downloading refund permission", e);
- throw e;
+ await tx.put(Stores.purchases, p);
+ });
+ if (numNewRefunds > 0) {
+ await processPurchaseApplyRefund(ws, proposalId);
}
-
- const refundResponse = MerchantRefundResponse.checked(resp.responseJson);
- await acceptRefundResponse(ws, proposalId, refundResponse);
}
async function startRefundQuery(
@@ -1151,21 +1096,12 @@ async function startRefundQuery(
console.log("no purchase found for refund URL");
return false;
}
- if (p.status === PurchaseStatus.QueryRefund) {
- return true;
- }
- if (p.status === PurchaseStatus.ProcessRefund) {
- return true;
- }
- if (p.status !== PurchaseStatus.Dormant) {
- console.log(
- `can't apply refund, as payment isn't done (status ${p.status})`,
- );
- return false;
+ if (p.refundStatusRequested) {
+
}
- p.lastError = undefined;
- p.status = PurchaseStatus.QueryRefund;
- p.retryInfo = initRetryInfo();
+ p.refundStatusRequested = true;
+ p.lastRefundStatusError = undefined;
+ p.refundStatusRetryInfo = initRetryInfo();
await tx.put(Stores.purchases, p);
return true;
},
@@ -1175,7 +1111,7 @@ async function startRefundQuery(
return;
}
- await processPurchase(ws, proposalId);
+ await processPurchaseQueryRefund(ws, proposalId);
}
/**
@@ -1210,19 +1146,19 @@ export async function applyRefund(
return purchase.contractTermsHash;
}
-export async function processPurchase(
+export async function processPurchasePay(
ws: InternalWalletState,
proposalId: string,
): Promise<void> {
const onOpErr = (e: OperationError) =>
- incrementPurchaseRetry(ws, proposalId, e);
+ incrementPurchasePayRetry(ws, proposalId, e);
await guardOperationException(
- () => processPurchaseImpl(ws, proposalId),
+ () => processPurchasePayImpl(ws, proposalId),
onOpErr,
);
}
-async function processPurchaseImpl(
+async function processPurchasePayImpl(
ws: InternalWalletState,
proposalId: string,
): Promise<void> {
@@ -1230,24 +1166,146 @@ async function processPurchaseImpl(
if (!purchase) {
return;
}
- logger.trace(`processing purchase ${proposalId}`);
- switch (purchase.status) {
- case PurchaseStatus.Dormant:
- return;
- case PurchaseStatus.Abort:
- // FIXME
- break;
- case PurchaseStatus.SubmitPay:
- break;
- case PurchaseStatus.QueryRefund:
- await queryRefund(ws, proposalId);
- break;
- case PurchaseStatus.ProcessRefund:
- console.log("submitting refunds to exchange (toplvl)");
- await submitRefundsToExchange(ws, proposalId);
- console.log("after submitting refunds to exchange (toplvl)");
- break;
- default:
- throw assertUnreachable(purchase.status);
+ logger.trace(`processing purchase pay ${proposalId}`);
+ if (purchase.payFinished) {
+ return;
+ }
+ await submitPay(ws, proposalId, purchase.lastSessionId);
+}
+
+export async function processPurchaseQueryRefund(
+ ws: InternalWalletState,
+ proposalId: string,
+): Promise<void> {
+ const onOpErr = (e: OperationError) =>
+ incrementPurchaseQueryRefundRetry(ws, proposalId, e);
+ await guardOperationException(
+ () => processPurchaseQueryRefundImpl(ws, proposalId),
+ onOpErr,
+ );
+}
+
+async function processPurchaseQueryRefundImpl(
+ ws: InternalWalletState,
+ proposalId: string,
+): Promise<void> {
+ const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
+ if (!purchase) {
+ return;
+ }
+ if (!purchase.refundStatusRequested) {
+ return;
+ }
+
+ const refundUrlObj = new URL(
+ "refund",
+ purchase.contractTerms.merchant_base_url,
+ );
+ refundUrlObj.searchParams.set("order_id", purchase.contractTerms.order_id);
+ const refundUrl = refundUrlObj.href;
+ let resp;
+ try {
+ resp = await ws.http.get(refundUrl);
+ } catch (e) {
+ console.error("error downloading refund permission", e);
+ throw e;
+ }
+
+ const refundResponse = MerchantRefundResponse.checked(resp.responseJson);
+ await acceptRefundResponse(ws, proposalId, refundResponse);
+}
+
+export async function processPurchaseApplyRefund(
+ ws: InternalWalletState,
+ proposalId: string,
+): Promise<void> {
+ const onOpErr = (e: OperationError) =>
+ incrementPurchaseApplyRefundRetry(ws, proposalId, e);
+ await guardOperationException(
+ () => processPurchaseApplyRefundImpl(ws, proposalId),
+ onOpErr,
+ );
+}
+
+async function processPurchaseApplyRefundImpl(
+ 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:");
+ return;
+ }
+ const pendingKeys = Object.keys(purchase.refundsPending);
+ if (pendingKeys.length === 0) {
+ console.log("no pending refunds");
+ return;
+ }
+ for (const pk of pendingKeys) {
+ const perm = purchase.refundsPending[pk];
+ const req: RefundRequest = {
+ coin_pub: perm.coin_pub,
+ h_contract_terms: purchase.contractTermsHash,
+ merchant_pub: purchase.contractTerms.merchant_pub,
+ merchant_sig: perm.merchant_sig,
+ refund_amount: perm.refund_amount,
+ refund_fee: perm.refund_fee,
+ rtransaction_id: perm.rtransaction_id,
+ };
+ console.log("sending refund permission", perm);
+ // FIXME: not correct once we support multiple exchanges per payment
+ const exchangeUrl = purchase.payReq.coins[0].exchange_url;
+ const reqUrl = new URL("refund", exchangeUrl);
+ const resp = await ws.http.postJson(reqUrl.href, req);
+ console.log("sent refund permission");
+ if (resp.status !== 200) {
+ console.error("refund failed", resp);
+ continue;
+ }
+
+ let allRefundsProcessed = false;
+
+ await runWithWriteTransaction(
+ ws.db,
+ [Stores.purchases, Stores.coins],
+ async tx => {
+ const p = await tx.get(Stores.purchases, proposalId);
+ if (!p) {
+ return;
+ }
+ if (p.refundsPending[pk]) {
+ p.refundsDone[pk] = p.refundsPending[pk];
+ delete p.refundsPending[pk];
+ }
+ if (Object.keys(p.refundsPending).length === 0) {
+ p.refundStatusRetryInfo = initRetryInfo();
+ p.lastRefundStatusError = undefined;
+ allRefundsProcessed = true;
+ }
+ await tx.put(Stores.purchases, p);
+ const c = await tx.get(Stores.coins, perm.coin_pub);
+ if (!c) {
+ console.warn("coin not found, can't apply refund");
+ return;
+ }
+ const refundAmount = Amounts.parseOrThrow(perm.refund_amount);
+ const refundFee = Amounts.parseOrThrow(perm.refund_fee);
+ c.status = CoinStatus.Dirty;
+ c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
+ c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
+ await tx.put(Stores.coins, c);
+ },
+ );
+ if (allRefundsProcessed) {
+ ws.notify({
+ type: NotificationType.RefundFinished,
+ });
+ }
+ await refresh(ws, perm.coin_pub);
}
+
+ ws.notify({
+ type: NotificationType.RefundsSubmitted,
+ proposalId,
+ });
}
diff --git a/src/wallet-impl/pending.ts b/src/wallet-impl/pending.ts
index c86ed6959..169c7e291 100644
--- a/src/wallet-impl/pending.ts
+++ b/src/wallet-impl/pending.ts
@@ -32,9 +32,7 @@ import {
ReserveRecordStatus,
CoinStatus,
ProposalStatus,
- PurchaseStatus,
} from "../dbTypes";
-import { assertUnreachable } from "../util/assertUnreachable";
function updateRetryDelay(
oldDelay: Duration,
@@ -355,28 +353,54 @@ async function gatherPurchasePending(
onlyDue: boolean = false,
): Promise<void> {
await tx.iter(Stores.purchases).forEach((pr) => {
- if (pr.status === PurchaseStatus.Dormant) {
- return;
+ if (!pr.payFinished) {
+ resp.nextRetryDelay = updateRetryDelay(
+ resp.nextRetryDelay,
+ now,
+ pr.payRetryInfo.nextRetry,
+ );
+ resp.pendingOperations.push({
+ type: "pay",
+ givesLifeness: true,
+ isReplay: false,
+ proposalId: pr.proposalId,
+ retryInfo: pr.payRetryInfo,
+ lastError: pr.lastPayError,
+ });
}
- resp.nextRetryDelay = updateRetryDelay(
- resp.nextRetryDelay,
- now,
- pr.retryInfo.nextRetry,
- );
- if (onlyDue && pr.retryInfo.nextRetry.t_ms > now.t_ms) {
- return;
+ if (pr.refundStatusRequested) {
+ resp.nextRetryDelay = updateRetryDelay(
+ resp.nextRetryDelay,
+ now,
+ pr.refundStatusRetryInfo.nextRetry,
+ );
+ resp.pendingOperations.push({
+ type: "refund-query",
+ givesLifeness: true,
+ proposalId: pr.proposalId,
+ retryInfo: pr.refundStatusRetryInfo,
+ lastError: pr.lastRefundStatusError,
+ });
+ }
+ const numRefundsPending = Object.keys(pr.refundsPending).length;
+ if (numRefundsPending > 0) {
+ const numRefundsDone = Object.keys(pr.refundsDone).length;
+ resp.nextRetryDelay = updateRetryDelay(
+ resp.nextRetryDelay,
+ now,
+ pr.refundApplyRetryInfo.nextRetry,
+ );
+ resp.pendingOperations.push({
+ type: "refund-apply",
+ numRefundsDone,
+ numRefundsPending,
+ givesLifeness: true,
+ proposalId: pr.proposalId,
+ retryInfo: pr.refundApplyRetryInfo,
+ lastError: pr.lastRefundApplyError,
+ });
}
- resp.pendingOperations.push({
- type: "pay",
- givesLifeness: true,
- isReplay: false,
- proposalId: pr.proposalId,
- status: pr.status,
- retryInfo: pr.retryInfo,
- lastError: pr.lastError,
- });
});
-
}
export async function getPendingOperations(
diff --git a/src/wallet.ts b/src/wallet.ts
index 489bb2af8..276e3c371 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -49,7 +49,9 @@ import {
processDownloadProposal,
applyRefund,
getFullRefundFees,
- processPurchase,
+ processPurchasePay,
+ processPurchaseQueryRefund,
+ processPurchaseApplyRefund,
} from "./wallet-impl/pay";
import {
@@ -210,7 +212,13 @@ export class Wallet {
await processTip(this.ws, pending.tipId);
break;
case "pay":
- await processPurchase(this.ws, pending.proposalId);
+ await processPurchasePay(this.ws, pending.proposalId);
+ break;
+ case "refund-query":
+ await processPurchaseQueryRefund(this.ws, pending.proposalId);
+ break;
+ case "refund-apply":
+ await processPurchaseApplyRefund(this.ws, pending.proposalId);
break;
default:
assertUnreachable(pending);
@@ -710,7 +718,7 @@ export class Wallet {
const totalFees = totalRefundFees;
return {
contractTerms: purchase.contractTerms,
- hasRefund: purchase.lastRefundTimestamp !== undefined,
+ hasRefund: purchase.lastRefundStatusTimestamp !== undefined,
totalRefundAmount: totalRefundAmount,
totalRefundAndRefreshFees: totalFees,
};
diff --git a/src/walletTypes.ts b/src/walletTypes.ts
index 2413234eb..f27970330 100644
--- a/src/walletTypes.ts
+++ b/src/walletTypes.ts
@@ -37,7 +37,6 @@ import {
ExchangeWireInfo,
WithdrawalSource,
RetryInfo,
- PurchaseStatus,
} from "./dbTypes";
import { CoinPaySig, ContractTerms, PayReq } from "./talerTypes";
@@ -681,11 +680,26 @@ export interface PendingPayOperation {
type: "pay";
proposalId: string;
isReplay: boolean;
- status: PurchaseStatus;
retryInfo: RetryInfo,
lastError: OperationError | undefined;
}
+export interface PendingRefundQueryOperation {
+ type: "refund-query";
+ proposalId: string;
+ retryInfo: RetryInfo,
+ lastError: OperationError | undefined;
+}
+
+export interface PendingRefundApplyOperation {
+ type: "refund-apply";
+ proposalId: string;
+ retryInfo: RetryInfo,
+ lastError: OperationError | undefined;
+ numRefundsPending: number;
+ numRefundsDone: number;
+}
+
export interface PendingOperationInfoCommon {
type: string;
givesLifeness: boolean;
@@ -703,6 +717,8 @@ export type PendingOperationInfo = PendingOperationInfoCommon &
| PendingProposalDownloadOperation
| PendingProposalChoiceOperation
| PendingPayOperation
+ | PendingRefundQueryOperation
+ | PendingRefundApplyOperation
);
export interface PendingOperationsResponse {