fix session IDs, fix proposal download concurrency bug
This commit is contained in:
parent
6415564b92
commit
f05788f59d
@ -1052,6 +1052,15 @@ export interface PurchaseRecord {
|
|||||||
*/
|
*/
|
||||||
lastSessionId: string | undefined;
|
lastSessionId: string | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set for the first payment, or on re-plays.
|
||||||
|
*/
|
||||||
|
paymentSubmitPending: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do we need to query the merchant for the refund status
|
||||||
|
* of the payment?
|
||||||
|
*/
|
||||||
refundStatusRequested: boolean;
|
refundStatusRequested: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -333,11 +333,19 @@ async function recordConfirmPay(
|
|||||||
proposal: ProposalRecord,
|
proposal: ProposalRecord,
|
||||||
payCoinInfo: PayCoinInfo,
|
payCoinInfo: PayCoinInfo,
|
||||||
chosenExchange: string,
|
chosenExchange: string,
|
||||||
|
sessionIdOverride: string | undefined,
|
||||||
): Promise<PurchaseRecord> {
|
): Promise<PurchaseRecord> {
|
||||||
const d = proposal.download;
|
const d = proposal.download;
|
||||||
if (!d) {
|
if (!d) {
|
||||||
throw Error("proposal is in invalid state");
|
throw Error("proposal is in invalid state");
|
||||||
}
|
}
|
||||||
|
let sessionId;
|
||||||
|
if (sessionIdOverride) {
|
||||||
|
sessionId = sessionIdOverride;
|
||||||
|
} else {
|
||||||
|
sessionId = proposal.downloadSessionId;
|
||||||
|
}
|
||||||
|
logger.trace(`recording payment with session ID ${sessionId}`);
|
||||||
const payReq: PayReq = {
|
const payReq: PayReq = {
|
||||||
coins: payCoinInfo.sigs,
|
coins: payCoinInfo.sigs,
|
||||||
merchant_pub: d.contractTerms.merchant_pub,
|
merchant_pub: d.contractTerms.merchant_pub,
|
||||||
@ -349,7 +357,7 @@ async function recordConfirmPay(
|
|||||||
abortRequested: false,
|
abortRequested: false,
|
||||||
contractTerms: d.contractTerms,
|
contractTerms: d.contractTerms,
|
||||||
contractTermsHash: d.contractTermsHash,
|
contractTermsHash: d.contractTermsHash,
|
||||||
lastSessionId: undefined,
|
lastSessionId: sessionId,
|
||||||
merchantSig: d.merchantSig,
|
merchantSig: d.merchantSig,
|
||||||
payReq,
|
payReq,
|
||||||
refundsDone: {},
|
refundsDone: {},
|
||||||
@ -366,6 +374,7 @@ async function recordConfirmPay(
|
|||||||
refundApplyRetryInfo: initRetryInfo(),
|
refundApplyRetryInfo: initRetryInfo(),
|
||||||
firstSuccessfulPayTimestamp: undefined,
|
firstSuccessfulPayTimestamp: undefined,
|
||||||
autoRefundDeadline: undefined,
|
autoRefundDeadline: undefined,
|
||||||
|
paymentSubmitPending: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
await runWithWriteTransaction(
|
await runWithWriteTransaction(
|
||||||
@ -562,13 +571,12 @@ async function resetDownloadProposalRetry(
|
|||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
) {
|
) {
|
||||||
await oneShotMutate(ws.db, Stores.proposals, proposalId, (x) => {
|
await oneShotMutate(ws.db, Stores.proposals, proposalId, x => {
|
||||||
if (x.retryInfo.active) {
|
if (x.retryInfo.active) {
|
||||||
x.retryInfo = initRetryInfo();
|
x.retryInfo = initRetryInfo();
|
||||||
}
|
}
|
||||||
return x;
|
return x;
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processDownloadProposalImpl(
|
async function processDownloadProposalImpl(
|
||||||
@ -667,7 +675,7 @@ async function startDownloadProposal(
|
|||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
merchantBaseUrl: string,
|
merchantBaseUrl: string,
|
||||||
orderId: string,
|
orderId: string,
|
||||||
sessionId?: string,
|
sessionId: string | undefined,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const oldProposal = await oneShotGetIndexed(
|
const oldProposal = await oneShotGetIndexed(
|
||||||
ws.db,
|
ws.db,
|
||||||
@ -694,9 +702,21 @@ async function startDownloadProposal(
|
|||||||
repurchaseProposalId: undefined,
|
repurchaseProposalId: undefined,
|
||||||
retryInfo: initRetryInfo(),
|
retryInfo: initRetryInfo(),
|
||||||
lastError: undefined,
|
lastError: undefined,
|
||||||
|
downloadSessionId: sessionId,
|
||||||
};
|
};
|
||||||
|
|
||||||
await oneShotPut(ws.db, Stores.proposals, proposalRecord);
|
await runWithWriteTransaction(ws.db, [Stores.proposals], async (tx) => {
|
||||||
|
const existingRecord = await tx.getIndexed(Stores.proposals.urlAndOrderIdIndex, [
|
||||||
|
merchantBaseUrl,
|
||||||
|
orderId,
|
||||||
|
]);
|
||||||
|
if (existingRecord) {
|
||||||
|
// Created concurrently
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await tx.put(Stores.proposals, proposalRecord);
|
||||||
|
});
|
||||||
|
|
||||||
await processDownloadProposal(ws, proposalId);
|
await processDownloadProposal(ws, proposalId);
|
||||||
return proposalId;
|
return proposalId;
|
||||||
}
|
}
|
||||||
@ -704,7 +724,6 @@ async function startDownloadProposal(
|
|||||||
export async function submitPay(
|
export async function submitPay(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
sessionId: string | undefined,
|
|
||||||
): Promise<ConfirmPayResult> {
|
): Promise<ConfirmPayResult> {
|
||||||
const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
|
const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
|
||||||
if (!purchase) {
|
if (!purchase) {
|
||||||
@ -713,9 +732,12 @@ export async function submitPay(
|
|||||||
if (purchase.abortRequested) {
|
if (purchase.abortRequested) {
|
||||||
throw Error("not submitting payment for aborted purchase");
|
throw Error("not submitting payment for aborted purchase");
|
||||||
}
|
}
|
||||||
|
const sessionId = purchase.lastSessionId;
|
||||||
let resp;
|
let resp;
|
||||||
const payReq = { ...purchase.payReq, session_id: sessionId };
|
const payReq = { ...purchase.payReq, session_id: sessionId };
|
||||||
|
|
||||||
|
console.log("paying with session ID", sessionId);
|
||||||
|
|
||||||
const payUrl = new URL("pay", purchase.contractTerms.merchant_base_url).href;
|
const payUrl = new URL("pay", purchase.contractTerms.merchant_base_url).href;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -729,7 +751,7 @@ export async function submitPay(
|
|||||||
throw Error(`unexpected status (${resp.status}) for /pay`);
|
throw Error(`unexpected status (${resp.status}) for /pay`);
|
||||||
}
|
}
|
||||||
const merchantResp = await resp.json();
|
const merchantResp = await resp.json();
|
||||||
console.log("got success from pay URL");
|
console.log("got success from pay URL", merchantResp);
|
||||||
|
|
||||||
const merchantPub = purchase.contractTerms.merchant_pub;
|
const merchantPub = purchase.contractTerms.merchant_pub;
|
||||||
const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
|
const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
|
||||||
@ -744,6 +766,7 @@ export async function submitPay(
|
|||||||
}
|
}
|
||||||
const isFirst = purchase.firstSuccessfulPayTimestamp === undefined;
|
const isFirst = purchase.firstSuccessfulPayTimestamp === undefined;
|
||||||
purchase.firstSuccessfulPayTimestamp = getTimestampNow();
|
purchase.firstSuccessfulPayTimestamp = getTimestampNow();
|
||||||
|
purchase.paymentSubmitPending = false;
|
||||||
purchase.lastPayError = undefined;
|
purchase.lastPayError = undefined;
|
||||||
purchase.payRetryInfo = initRetryInfo(false);
|
purchase.payRetryInfo = initRetryInfo(false);
|
||||||
if (isFirst) {
|
if (isFirst) {
|
||||||
@ -916,7 +939,7 @@ export async function preparePay(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (uriResult.sessionId) {
|
if (uriResult.sessionId) {
|
||||||
await submitPay(ws, proposalId, uriResult.sessionId);
|
await submitPay(ws, proposalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -985,13 +1008,25 @@ export async function confirmPay(
|
|||||||
throw Error("proposal is in invalid state");
|
throw Error("proposal is in invalid state");
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionId = sessionIdOverride || proposal.downloadSessionId;
|
|
||||||
|
|
||||||
let purchase = await oneShotGet(ws.db, Stores.purchases, d.contractTermsHash);
|
let purchase = await oneShotGet(ws.db, Stores.purchases, d.contractTermsHash);
|
||||||
|
|
||||||
if (purchase) {
|
if (purchase) {
|
||||||
return submitPay(ws, proposalId, sessionId);
|
if (
|
||||||
|
sessionIdOverride !== undefined &&
|
||||||
|
sessionIdOverride != purchase.lastSessionId
|
||||||
|
) {
|
||||||
|
logger.trace(`changing session ID to ${sessionIdOverride}`);
|
||||||
|
await oneShotMutate(ws.db, Stores.purchases, purchase.proposalId, x => {
|
||||||
|
x.lastSessionId = sessionIdOverride;
|
||||||
|
x.paymentSubmitPending = true;
|
||||||
|
return x;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
logger.trace("confirmPay: submitting payment for existing purchase");
|
||||||
|
return submitPay(ws, proposalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.trace("confirmPay: purchase record does not exist yet");
|
||||||
|
|
||||||
const contractAmount = Amounts.parseOrThrow(d.contractTerms.amount);
|
const contractAmount = Amounts.parseOrThrow(d.contractTerms.amount);
|
||||||
|
|
||||||
@ -1029,17 +1064,25 @@ export async function confirmPay(
|
|||||||
cds,
|
cds,
|
||||||
totalAmount,
|
totalAmount,
|
||||||
);
|
);
|
||||||
purchase = await recordConfirmPay(ws, proposal, payCoinInfo, exchangeUrl);
|
purchase = await recordConfirmPay(
|
||||||
|
ws,
|
||||||
|
proposal,
|
||||||
|
payCoinInfo,
|
||||||
|
exchangeUrl,
|
||||||
|
sessionIdOverride,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
purchase = await recordConfirmPay(
|
purchase = await recordConfirmPay(
|
||||||
ws,
|
ws,
|
||||||
sd.proposal,
|
sd.proposal,
|
||||||
sd.payCoinInfo,
|
sd.payCoinInfo,
|
||||||
sd.exchangeUrl,
|
sd.exchangeUrl,
|
||||||
|
sessionIdOverride,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return submitPay(ws, proposalId, sessionId);
|
logger.trace("confirmPay: submitting payment after creating purchase record");
|
||||||
|
return submitPay(ws, proposalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFullRefundFees(
|
export async function getFullRefundFees(
|
||||||
@ -1249,7 +1292,7 @@ async function resetPurchasePayRetry(
|
|||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
) {
|
) {
|
||||||
await oneShotMutate(ws.db, Stores.purchases, proposalId, (x) => {
|
await oneShotMutate(ws.db, Stores.purchases, proposalId, x => {
|
||||||
if (x.payRetryInfo.active) {
|
if (x.payRetryInfo.active) {
|
||||||
x.payRetryInfo = initRetryInfo();
|
x.payRetryInfo = initRetryInfo();
|
||||||
}
|
}
|
||||||
@ -1269,11 +1312,11 @@ async function processPurchasePayImpl(
|
|||||||
if (!purchase) {
|
if (!purchase) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logger.trace(`processing purchase pay ${proposalId}`);
|
if (!purchase.paymentSubmitPending) {
|
||||||
if (purchase.firstSuccessfulPayTimestamp) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await submitPay(ws, proposalId, purchase.lastSessionId);
|
logger.trace(`processing purchase pay ${proposalId}`);
|
||||||
|
await submitPay(ws, proposalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processPurchaseQueryRefund(
|
export async function processPurchaseQueryRefund(
|
||||||
@ -1289,8 +1332,10 @@ export async function processPurchaseQueryRefund(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resetPurchaseQueryRefundRetry(
|
||||||
async function resetPurchaseQueryRefundRetry(ws: InternalWalletState, proposalId: string) {
|
ws: InternalWalletState,
|
||||||
|
proposalId: string,
|
||||||
|
) {
|
||||||
await oneShotMutate(ws.db, Stores.purchases, proposalId, x => {
|
await oneShotMutate(ws.db, Stores.purchases, proposalId, x => {
|
||||||
if (x.refundStatusRetryInfo.active) {
|
if (x.refundStatusRetryInfo.active) {
|
||||||
x.refundStatusRetryInfo = initRetryInfo();
|
x.refundStatusRetryInfo = initRetryInfo();
|
||||||
@ -1349,7 +1394,10 @@ export async function processPurchaseApplyRefund(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resetPurchaseApplyRefundRetry(ws: InternalWalletState, proposalId: string) {
|
async function resetPurchaseApplyRefundRetry(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
proposalId: string,
|
||||||
|
) {
|
||||||
await oneShotMutate(ws.db, Stores.purchases, proposalId, x => {
|
await oneShotMutate(ws.db, Stores.purchases, proposalId, x => {
|
||||||
if (x.refundApplyRetryInfo.active) {
|
if (x.refundApplyRetryInfo.active) {
|
||||||
x.refundApplyRetryInfo = initRetryInfo();
|
x.refundApplyRetryInfo = initRetryInfo();
|
||||||
|
@ -360,7 +360,7 @@ async function gatherPurchasePending(
|
|||||||
onlyDue: boolean = false,
|
onlyDue: boolean = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await tx.iter(Stores.purchases).forEach(pr => {
|
await tx.iter(Stores.purchases).forEach(pr => {
|
||||||
if (!pr.firstSuccessfulPayTimestamp) {
|
if (pr.paymentSubmitPending) {
|
||||||
resp.nextRetryDelay = updateRetryDelay(
|
resp.nextRetryDelay = updateRetryDelay(
|
||||||
resp.nextRetryDelay,
|
resp.nextRetryDelay,
|
||||||
now,
|
now,
|
||||||
|
Loading…
Reference in New Issue
Block a user