wallet-core: more DB and refund fixes

This commit is contained in:
Florian Dold 2022-10-08 23:45:49 +02:00
parent 3897bd4f01
commit 8ac5080607
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
6 changed files with 80 additions and 78 deletions

View File

@ -1026,7 +1026,7 @@ export interface WalletContractData {
deliveryLocation: Location | undefined; deliveryLocation: Location | undefined;
} }
export enum ProposalStatus { export enum PurchaseStatus {
/** /**
* Not downloaded yet. * Not downloaded yet.
*/ */
@ -1150,7 +1150,7 @@ export interface PurchaseRecord {
*/ */
repurchaseProposalId: string | undefined; repurchaseProposalId: string | undefined;
status: ProposalStatus; purchaseStatus: PurchaseStatus;
/** /**
* Private key for the nonce. * Private key for the nonce.
@ -1934,7 +1934,7 @@ export const WalletStoresV1 = {
"purchases", "purchases",
describeContents<PurchaseRecord>({ keyPath: "proposalId" }), describeContents<PurchaseRecord>({ keyPath: "proposalId" }),
{ {
byStatus: describeIndex("byStatus", "operationStatus"), byStatus: describeIndex("byStatus", "purchaseStatus"),
byFulfillmentUrl: describeIndex( byFulfillmentUrl: describeIndex(
"byFulfillmentUrl", "byFulfillmentUrl",
"download.contractData.fulfillmentUrl", "download.contractData.fulfillmentUrl",

View File

@ -65,7 +65,7 @@ import {
CoinSourceType, CoinSourceType,
CoinStatus, CoinStatus,
DenominationRecord, DenominationRecord,
ProposalStatus, PurchaseStatus,
RefreshCoinStatus, RefreshCoinStatus,
RefundState, RefundState,
WALLET_BACKUP_STATE_KEY, WALLET_BACKUP_STATE_KEY,
@ -382,21 +382,21 @@ export async function exportBackup(
} }
let propStatus: BackupProposalStatus; let propStatus: BackupProposalStatus;
switch (purch.status) { switch (purch.purchaseStatus) {
case ProposalStatus.Paid: case PurchaseStatus.Paid:
propStatus = BackupProposalStatus.Paid; propStatus = BackupProposalStatus.Paid;
return; return;
case ProposalStatus.DownloadingProposal: case PurchaseStatus.DownloadingProposal:
case ProposalStatus.Proposed: case PurchaseStatus.Proposed:
propStatus = BackupProposalStatus.Proposed; propStatus = BackupProposalStatus.Proposed;
break; break;
case ProposalStatus.ProposalDownloadFailed: case PurchaseStatus.ProposalDownloadFailed:
propStatus = BackupProposalStatus.PermanentlyFailed; propStatus = BackupProposalStatus.PermanentlyFailed;
break; break;
case ProposalStatus.ProposalRefused: case PurchaseStatus.ProposalRefused:
propStatus = BackupProposalStatus.Refused; propStatus = BackupProposalStatus.Refused;
break; break;
case ProposalStatus.RepurchaseDetected: case PurchaseStatus.RepurchaseDetected:
propStatus = BackupProposalStatus.Repurchase; propStatus = BackupProposalStatus.Repurchase;
break; break;
default: default:

View File

@ -46,7 +46,7 @@ import {
DenomSelectionState, DenomSelectionState,
OperationStatus, OperationStatus,
ProposalDownload, ProposalDownload,
ProposalStatus, PurchaseStatus,
PurchasePayInfo, PurchasePayInfo,
RefreshCoinStatus, RefreshCoinStatus,
RefreshSessionRecord, RefreshSessionRecord,
@ -564,10 +564,10 @@ export async function importBackup(
const existingPurchase = await tx.purchases.get( const existingPurchase = await tx.purchases.get(
backupPurchase.proposal_id, backupPurchase.proposal_id,
); );
let proposalStatus: ProposalStatus; let proposalStatus: PurchaseStatus;
switch (backupPurchase.proposal_status) { switch (backupPurchase.proposal_status) {
case BackupProposalStatus.Paid: case BackupProposalStatus.Paid:
proposalStatus = ProposalStatus.Paid; proposalStatus = PurchaseStatus.Paid;
break; break;
default: default:
throw Error(); throw Error();
@ -703,7 +703,7 @@ export async function importBackup(
payInfo, payInfo,
refundAmountAwaiting: undefined, refundAmountAwaiting: undefined,
repurchaseProposalId: backupPurchase.repurchase_proposal_id, repurchaseProposalId: backupPurchase.repurchase_proposal_id,
status: proposalStatus, purchaseStatus: proposalStatus,
timestamp: backupPurchase.timestamp_proposed, timestamp: backupPurchase.timestamp_proposed,
}); });
} }

View File

@ -81,7 +81,7 @@ import {
CoinStatus, CoinStatus,
DenominationRecord, DenominationRecord,
ProposalDownload, ProposalDownload,
ProposalStatus, PurchaseStatus,
PurchaseRecord, PurchaseRecord,
RefundReason, RefundReason,
RefundState, RefundState,
@ -230,7 +230,7 @@ async function failProposalPermanently(
if (!p) { if (!p) {
return; return;
} }
p.status = ProposalStatus.ProposalDownloadFailed; p.purchaseStatus = PurchaseStatus.ProposalDownloadFailed;
await tx.purchases.put(p); await tx.purchases.put(p);
}); });
} }
@ -329,7 +329,7 @@ export async function processDownloadProposal(
}; };
} }
if (proposal.status != ProposalStatus.DownloadingProposal) { if (proposal.purchaseStatus != PurchaseStatus.DownloadingProposal) {
return { return {
type: OperationAttemptResultType.Finished, type: OperationAttemptResultType.Finished,
result: undefined, result: undefined,
@ -500,7 +500,7 @@ export async function processDownloadProposal(
if (!p) { if (!p) {
return; return;
} }
if (p.status !== ProposalStatus.DownloadingProposal) { if (p.purchaseStatus !== PurchaseStatus.DownloadingProposal) {
return; return;
} }
p.download = { p.download = {
@ -516,13 +516,13 @@ export async function processDownloadProposal(
await tx.purchases.indexes.byFulfillmentUrl.get(fulfillmentUrl); await tx.purchases.indexes.byFulfillmentUrl.get(fulfillmentUrl);
if (differentPurchase) { if (differentPurchase) {
logger.warn("repurchase detected"); logger.warn("repurchase detected");
p.status = ProposalStatus.RepurchaseDetected; p.purchaseStatus = PurchaseStatus.RepurchaseDetected;
p.repurchaseProposalId = differentPurchase.proposalId; p.repurchaseProposalId = differentPurchase.proposalId;
await tx.purchases.put(p); await tx.purchases.put(p);
return; return;
} }
} }
p.status = ProposalStatus.Proposed; p.purchaseStatus = PurchaseStatus.Proposed;
await tx.purchases.put(p); await tx.purchases.put(p);
}); });
@ -595,7 +595,7 @@ async function startDownloadProposal(
merchantBaseUrl, merchantBaseUrl,
orderId, orderId,
proposalId: proposalId, proposalId: proposalId,
status: ProposalStatus.DownloadingProposal, purchaseStatus: PurchaseStatus.DownloadingProposal,
repurchaseProposalId: undefined, repurchaseProposalId: undefined,
downloadSessionId: sessionId, downloadSessionId: sessionId,
autoRefundDeadline: undefined, autoRefundDeadline: undefined,
@ -649,8 +649,8 @@ async function storeFirstPaySuccess(
logger.warn("payment success already stored"); logger.warn("payment success already stored");
return; return;
} }
if (purchase.status === ProposalStatus.Paying) { if (purchase.purchaseStatus === PurchaseStatus.Paying) {
purchase.status = ProposalStatus.Paid; purchase.purchaseStatus = PurchaseStatus.Paid;
} }
purchase.timestampFirstSuccessfulPay = now; purchase.timestampFirstSuccessfulPay = now;
purchase.lastSessionId = sessionId; purchase.lastSessionId = sessionId;
@ -659,7 +659,7 @@ async function storeFirstPaySuccess(
if (protoAr) { if (protoAr) {
const ar = Duration.fromTalerProtocolDuration(protoAr); const ar = Duration.fromTalerProtocolDuration(protoAr);
logger.info("auto_refund present"); logger.info("auto_refund present");
purchase.status = ProposalStatus.QueryingAutoRefund; purchase.purchaseStatus = PurchaseStatus.QueryingAutoRefund;
purchase.autoRefundDeadline = AbsoluteTime.toTimestamp( purchase.autoRefundDeadline = AbsoluteTime.toTimestamp(
AbsoluteTime.addDuration(AbsoluteTime.now(), ar), AbsoluteTime.addDuration(AbsoluteTime.now(), ar),
); );
@ -686,8 +686,8 @@ async function storePayReplaySuccess(
if (isFirst) { if (isFirst) {
throw Error("invalid payment state"); throw Error("invalid payment state");
} }
if (purchase.status === ProposalStatus.Paying) { if (purchase.purchaseStatus === PurchaseStatus.Paying) {
purchase.status = ProposalStatus.Paid; purchase.purchaseStatus = PurchaseStatus.Paid;
} }
purchase.lastSessionId = sessionId; purchase.lastSessionId = sessionId;
await tx.purchases.put(purchase); await tx.purchases.put(purchase);
@ -1239,7 +1239,7 @@ export async function checkPaymentByProposalId(
if (!proposal) { if (!proposal) {
throw Error(`could not get proposal ${proposalId}`); throw Error(`could not get proposal ${proposalId}`);
} }
if (proposal.status === ProposalStatus.RepurchaseDetected) { if (proposal.purchaseStatus === PurchaseStatus.RepurchaseDetected) {
const existingProposalId = proposal.repurchaseProposalId; const existingProposalId = proposal.repurchaseProposalId;
if (!existingProposalId) { if (!existingProposalId) {
throw Error("invalid proposal state"); throw Error("invalid proposal state");
@ -1274,7 +1274,7 @@ export async function checkPaymentByProposalId(
return tx.purchases.get(proposalId); return tx.purchases.get(proposalId);
}); });
if (!purchase || purchase.status === ProposalStatus.Proposed) { if (!purchase || purchase.purchaseStatus === PurchaseStatus.Proposed) {
// If not already paid, check if we could pay for it. // If not already paid, check if we could pay for it.
const res = await selectPayCoinsNew(ws, { const res = await selectPayCoinsNew(ws, {
auditors: contractData.allowedAuditors, auditors: contractData.allowedAuditors,
@ -1315,7 +1315,7 @@ export async function checkPaymentByProposalId(
} }
if ( if (
purchase.status === ProposalStatus.Paid && purchase.purchaseStatus === PurchaseStatus.Paid &&
purchase.lastSessionId !== sessionId purchase.lastSessionId !== sessionId
) { ) {
logger.trace( logger.trace(
@ -1330,7 +1330,7 @@ export async function checkPaymentByProposalId(
return; return;
} }
p.lastSessionId = sessionId; p.lastSessionId = sessionId;
p.status = ProposalStatus.PayingReplay; p.purchaseStatus = PurchaseStatus.PayingReplay;
await tx.purchases.put(p); await tx.purchases.put(p);
}); });
const r = await processPurchasePay(ws, proposalId, { forceNow: true }); const r = await processPurchasePay(ws, proposalId, { forceNow: true });
@ -1361,9 +1361,9 @@ export async function checkPaymentByProposalId(
}; };
} else { } else {
const paid = const paid =
purchase.status === ProposalStatus.Paid || purchase.purchaseStatus === PurchaseStatus.Paid ||
purchase.status === ProposalStatus.QueryingRefund || purchase.purchaseStatus === PurchaseStatus.QueryingRefund ||
purchase.status === ProposalStatus.QueryingAutoRefund; purchase.purchaseStatus === PurchaseStatus.QueryingAutoRefund;
const download = await expectProposalDownload(purchase); const download = await expectProposalDownload(purchase);
return { return {
status: PreparePayResultType.AlreadyConfirmed, status: PreparePayResultType.AlreadyConfirmed,
@ -1615,6 +1615,9 @@ export async function confirmPay(
) { ) {
logger.trace(`changing session ID to ${sessionIdOverride}`); logger.trace(`changing session ID to ${sessionIdOverride}`);
purchase.lastSessionId = sessionIdOverride; purchase.lastSessionId = sessionIdOverride;
if (purchase.purchaseStatus === PurchaseStatus.Paid) {
purchase.purchaseStatus = PurchaseStatus.PayingReplay;
}
await tx.purchases.put(purchase); await tx.purchases.put(purchase);
} }
return purchase; return purchase;
@ -1688,8 +1691,8 @@ export async function confirmPay(
if (!p) { if (!p) {
return; return;
} }
switch (p.status) { switch (p.purchaseStatus) {
case ProposalStatus.Proposed: case PurchaseStatus.Proposed:
p.payInfo = { p.payInfo = {
payCoinSelection: coinSelection, payCoinSelection: coinSelection,
payCoinSelectionUid: encodeCrock(getRandomBytes(16)), payCoinSelectionUid: encodeCrock(getRandomBytes(16)),
@ -1698,7 +1701,7 @@ export async function confirmPay(
}; };
p.lastSessionId = sessionId; p.lastSessionId = sessionId;
p.timestampAccept = TalerProtocolTimestamp.now(); p.timestampAccept = TalerProtocolTimestamp.now();
p.status = ProposalStatus.Paying; p.purchaseStatus = PurchaseStatus.Paying;
await tx.purchases.put(p); await tx.purchases.put(p);
await spendCoins(ws, tx, { await spendCoins(ws, tx, {
allocationId: `proposal:${p.proposalId}`, allocationId: `proposal:${p.proposalId}`,
@ -1707,8 +1710,8 @@ export async function confirmPay(
refreshReason: RefreshReason.PayMerchant, refreshReason: RefreshReason.PayMerchant,
}); });
break; break;
case ProposalStatus.Paid: case PurchaseStatus.Paid:
case ProposalStatus.Paying: case PurchaseStatus.Paying:
default: default:
break; break;
} }
@ -1746,26 +1749,26 @@ export async function processPurchase(
}; };
} }
switch (purchase.status) { switch (purchase.purchaseStatus) {
case ProposalStatus.DownloadingProposal: case PurchaseStatus.DownloadingProposal:
return processDownloadProposal(ws, proposalId, options); return processDownloadProposal(ws, proposalId, options);
case ProposalStatus.Paying: case PurchaseStatus.Paying:
case ProposalStatus.PayingReplay: case PurchaseStatus.PayingReplay:
return processPurchasePay(ws, proposalId, options); return processPurchasePay(ws, proposalId, options);
case ProposalStatus.QueryingAutoRefund: case PurchaseStatus.QueryingRefund:
case ProposalStatus.QueryingAutoRefund: case PurchaseStatus.QueryingAutoRefund:
case ProposalStatus.AbortingWithRefund: case PurchaseStatus.AbortingWithRefund:
return processPurchaseQueryRefund(ws, proposalId, options); return processPurchaseQueryRefund(ws, proposalId, options);
case ProposalStatus.ProposalDownloadFailed: case PurchaseStatus.ProposalDownloadFailed:
case ProposalStatus.Paid: case PurchaseStatus.Paid:
case ProposalStatus.AbortingWithRefund: case PurchaseStatus.AbortingWithRefund:
case ProposalStatus.RepurchaseDetected: case PurchaseStatus.RepurchaseDetected:
return { return {
type: OperationAttemptResultType.Finished, type: OperationAttemptResultType.Finished,
result: undefined, result: undefined,
}; };
default: default:
throw Error(`unexpected purchase status (${purchase.status})`); throw Error(`unexpected purchase status (${purchase.purchaseStatus})`);
} }
} }
@ -1792,9 +1795,9 @@ export async function processPurchasePay(
}, },
}; };
} }
switch (purchase.status) { switch (purchase.purchaseStatus) {
case ProposalStatus.Paying: case PurchaseStatus.Paying:
case ProposalStatus.PayingReplay: case PurchaseStatus.PayingReplay:
break; break;
default: default:
return OperationAttemptResult.finishedEmpty(); return OperationAttemptResult.finishedEmpty();
@ -1870,7 +1873,7 @@ export async function processPurchasePay(
return; return;
} }
// FIXME: Should be some "PayPermanentlyFailed" and error info should be stored // FIXME: Should be some "PayPermanentlyFailed" and error info should be stored
purch.status = ProposalStatus.PaymentAbortFinished; purch.purchaseStatus = PurchaseStatus.PaymentAbortFinished;
await tx.purchases.put(purch); await tx.purchases.put(purch);
}); });
throw makePendingOperationFailedError( throw makePendingOperationFailedError(
@ -1975,10 +1978,10 @@ export async function refuseProposal(
logger.trace(`proposal ${proposalId} not found, won't refuse proposal`); logger.trace(`proposal ${proposalId} not found, won't refuse proposal`);
return false; return false;
} }
if (proposal.status !== ProposalStatus.Proposed) { if (proposal.purchaseStatus !== PurchaseStatus.Proposed) {
return false; return false;
} }
proposal.status = ProposalStatus.ProposalRefused; proposal.purchaseStatus = PurchaseStatus.ProposalRefused;
await tx.purchases.put(proposal); await tx.purchases.put(proposal);
return true; return true;
}); });
@ -2211,7 +2214,7 @@ async function storeFailedRefund(
rtransactionId: r.rtransaction_id, rtransactionId: r.rtransaction_id,
}; };
if (p.status === ProposalStatus.AbortingWithRefund) { if (p.purchaseStatus === PurchaseStatus.AbortingWithRefund) {
// Refund failed because the merchant didn't even try to deposit // Refund failed because the merchant didn't even try to deposit
// the coin yet, so we try to refresh. // the coin yet, so we try to refresh.
if (r.exchange_code === TalerErrorCode.EXCHANGE_REFUND_DEPOSIT_NOT_FOUND) { if (r.exchange_code === TalerErrorCode.EXCHANGE_REFUND_DEPOSIT_NOT_FOUND) {
@ -2346,9 +2349,9 @@ async function acceptRefunds(
if (queryDone) { if (queryDone) {
p.timestampLastRefundStatus = now; p.timestampLastRefundStatus = now;
if (p.status === ProposalStatus.AbortingWithRefund) { if (p.purchaseStatus === PurchaseStatus.AbortingWithRefund) {
p.status = ProposalStatus.PaymentAbortFinished; p.purchaseStatus = PurchaseStatus.PaymentAbortFinished;
} else if (p.status === ProposalStatus.QueryingAutoRefund) { } else if (p.purchaseStatus === PurchaseStatus.QueryingAutoRefund) {
const autoRefundDeadline = p.autoRefundDeadline; const autoRefundDeadline = p.autoRefundDeadline;
checkDbInvariant(!!autoRefundDeadline); checkDbInvariant(!!autoRefundDeadline);
if ( if (
@ -2356,10 +2359,10 @@ async function acceptRefunds(
AbsoluteTime.fromTimestamp(autoRefundDeadline), AbsoluteTime.fromTimestamp(autoRefundDeadline),
) )
) { ) {
p.status = ProposalStatus.Paid; p.purchaseStatus = PurchaseStatus.Paid;
} }
} else if (p.status === ProposalStatus.QueryingRefund) { } else if (p.purchaseStatus === PurchaseStatus.QueryingRefund) {
p.status = ProposalStatus.Paid; p.purchaseStatus = PurchaseStatus.Paid;
} }
logger.trace("refund query done"); logger.trace("refund query done");
} else { } else {
@ -2483,8 +2486,8 @@ export async function applyRefundFromPurchaseId(
logger.error("no purchase found for refund URL"); logger.error("no purchase found for refund URL");
return false; return false;
} }
if (p.status === ProposalStatus.Paid) { if (p.purchaseStatus === PurchaseStatus.Paid) {
p.status = ProposalStatus.QueryingRefund; p.purchaseStatus = PurchaseStatus.QueryingRefund;
} }
await tx.purchases.put(p); await tx.purchases.put(p);
return true; return true;
@ -2610,9 +2613,9 @@ export async function processPurchaseQueryRefund(
if ( if (
!( !(
purchase.status === ProposalStatus.QueryingAutoRefund || purchase.purchaseStatus === PurchaseStatus.QueryingAutoRefund ||
purchase.status === ProposalStatus.QueryingRefund || purchase.purchaseStatus === PurchaseStatus.QueryingRefund ||
purchase.status === ProposalStatus.AbortingWithRefund purchase.purchaseStatus === PurchaseStatus.AbortingWithRefund
) )
) { ) {
return OperationAttemptResult.finishedEmpty(); return OperationAttemptResult.finishedEmpty();
@ -2659,7 +2662,7 @@ export async function processPurchaseQueryRefund(
refundResponse.refunds, refundResponse.refunds,
RefundReason.NormalRefund, RefundReason.NormalRefund,
); );
} else if (purchase.status === ProposalStatus.AbortingWithRefund) { } else if (purchase.purchaseStatus === PurchaseStatus.AbortingWithRefund) {
const requestUrl = new URL( const requestUrl = new URL(
`orders/${download.contractData.orderId}/abort`, `orders/${download.contractData.orderId}/abort`,
download.contractData.merchantBaseUrl, download.contractData.merchantBaseUrl,
@ -2745,8 +2748,8 @@ export async function abortFailedPayWithRefund(
logger.warn(`tried to abort successful payment`); logger.warn(`tried to abort successful payment`);
return; return;
} }
if (purchase.status === ProposalStatus.Paying) { if (purchase.purchaseStatus === PurchaseStatus.Paying) {
purchase.status = ProposalStatus.AbortingWithRefund; purchase.purchaseStatus = PurchaseStatus.AbortingWithRefund;
} }
await tx.purchases.put(purchase); await tx.purchases.put(purchase);
}); });

View File

@ -22,7 +22,7 @@
* Imports. * Imports.
*/ */
import { import {
ProposalStatus, PurchaseStatus,
WalletStoresV1, WalletStoresV1,
BackupProviderStateTag, BackupProviderStateTag,
RefreshCoinStatus, RefreshCoinStatus,
@ -252,7 +252,6 @@ async function gatherPurchasePending(
now: AbsoluteTime, now: AbsoluteTime,
resp: PendingOperationsResponse, resp: PendingOperationsResponse,
): Promise<void> { ): Promise<void> {
// FIXME: Only iter purchases with some "active" flag!
const keyRange = GlobalIDB.KeyRange.bound( const keyRange = GlobalIDB.KeyRange.bound(
OperationStatusRange.ACTIVE_START, OperationStatusRange.ACTIVE_START,
OperationStatusRange.ACTIVE_END, OperationStatusRange.ACTIVE_END,
@ -268,7 +267,7 @@ async function gatherPurchasePending(
type: PendingTaskType.Purchase, type: PendingTaskType.Purchase,
...getPendingCommon(ws, opId, timestampDue), ...getPendingCommon(ws, opId, timestampDue),
givesLifeness: true, givesLifeness: true,
statusStr: ProposalStatus[pr.status], statusStr: PurchaseStatus[pr.purchaseStatus],
proposalId: pr.proposalId, proposalId: pr.proposalId,
retryInfo: retryRecord?.retryInfo, retryInfo: retryRecord?.retryInfo,
lastError: retryRecord?.lastError, lastError: retryRecord?.lastError,

View File

@ -41,7 +41,7 @@ import {
OperationRetryRecord, OperationRetryRecord,
PeerPullPaymentIncomingRecord, PeerPullPaymentIncomingRecord,
PeerPushPaymentInitiationRecord, PeerPushPaymentInitiationRecord,
ProposalStatus, PurchaseStatus,
PurchaseRecord, PurchaseRecord,
RefundState, RefundState,
TipRecord, TipRecord,
@ -679,7 +679,7 @@ async function buildTransactionForPurchase(
status: purchaseRecord.timestampFirstSuccessfulPay status: purchaseRecord.timestampFirstSuccessfulPay
? PaymentStatus.Paid ? PaymentStatus.Paid
: PaymentStatus.Accepted, : PaymentStatus.Accepted,
pending: purchaseRecord.status === ProposalStatus.Paying, pending: purchaseRecord.purchaseStatus === PurchaseStatus.Paying,
refunds, refunds,
timestamp, timestamp,
transactionId: makeEventId( transactionId: makeEventId(
@ -689,7 +689,7 @@ async function buildTransactionForPurchase(
proposalId: purchaseRecord.proposalId, proposalId: purchaseRecord.proposalId,
info, info,
frozen: frozen:
purchaseRecord.status === ProposalStatus.PaymentAbortFinished ?? false, purchaseRecord.purchaseStatus === PurchaseStatus.PaymentAbortFinished ?? false,
...(ort?.lastError ? { error: ort.lastError } : {}), ...(ort?.lastError ? { error: ort.lastError } : {}),
}; };
} }