From 5c26461247040c07c86291babf0c87631df638b5 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 9 Jun 2021 15:14:17 +0200 Subject: database access refactor --- .../src/operations/transactions.ts | 656 +++++++++++---------- 1 file changed, 337 insertions(+), 319 deletions(-) (limited to 'packages/taler-wallet-core/src/operations/transactions.ts') diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 42ed2d2ec..ecef3c2ce 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -19,7 +19,6 @@ */ import { InternalWalletState } from "./state"; import { - Stores, WalletRefundItem, RefundState, ReserveRecordStatus, @@ -85,296 +84,300 @@ export async function getTransactions( ): Promise { const transactions: Transaction[] = []; - await ws.db.runWithReadTransaction( - [ - Stores.coins, - Stores.denominations, - Stores.exchanges, - Stores.exchangeDetails, - Stores.proposals, - Stores.purchases, - Stores.refreshGroups, - Stores.reserves, - Stores.tips, - Stores.withdrawalGroups, - Stores.planchets, - Stores.recoupGroups, - Stores.depositGroups, - Stores.tombstones, - ], - // Report withdrawals that are currently in progress. - async (tx) => { - tx.iter(Stores.withdrawalGroups).forEachAsync(async (wsr) => { - if ( - shouldSkipCurrency( - transactionsRequest, - wsr.rawWithdrawalAmount.currency, - ) - ) { - return; - } + await ws.db + .mktx((x) => ({ + coins: x.coins, + denominations: x.denominations, + exchanges: x.exchanges, + exchangeDetails: x.exchangeDetails, + proposals: x.proposals, + purchases: x.purchases, + refreshGroups: x.refreshGroups, + reserves: x.reserves, + tips: x.tips, + withdrawalGroups: x.withdrawalGroups, + planchets: x.planchets, + recoupGroups: x.recoupGroups, + depositGroups: x.depositGroups, + tombstones: x.tombstones, + })) + .runReadOnly( + // Report withdrawals that are currently in progress. + async (tx) => { + tx.withdrawalGroups.iter().forEachAsync(async (wsr) => { + if ( + shouldSkipCurrency( + transactionsRequest, + wsr.rawWithdrawalAmount.currency, + ) + ) { + return; + } - if (shouldSkipSearch(transactionsRequest, [])) { - return; - } + if (shouldSkipSearch(transactionsRequest, [])) { + return; + } - const r = await tx.get(Stores.reserves, wsr.reservePub); - if (!r) { - return; - } - let amountRaw: AmountJson | undefined = undefined; - if (wsr.withdrawalGroupId === r.initialWithdrawalGroupId) { - amountRaw = r.instructedAmount; - } else { - amountRaw = wsr.denomsSel.totalWithdrawCost; - } - let withdrawalDetails: WithdrawalDetails; - if (r.bankInfo) { - withdrawalDetails = { - type: WithdrawalType.TalerBankIntegrationApi, - confirmed: true, - bankConfirmationUrl: r.bankInfo.confirmUrl, - }; - } else { - const exchangeDetails = await getExchangeDetails( - tx, - wsr.exchangeBaseUrl, - ); - if (!exchangeDetails) { - // FIXME: report somehow + const r = await tx.reserves.get(wsr.reservePub); + if (!r) { return; } - withdrawalDetails = { - type: WithdrawalType.ManualTransfer, - exchangePaytoUris: - exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? [], - }; - } - transactions.push({ - type: TransactionType.Withdrawal, - amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), - amountRaw: Amounts.stringify(amountRaw), - withdrawalDetails, - exchangeBaseUrl: wsr.exchangeBaseUrl, - pending: !wsr.timestampFinish, - timestamp: wsr.timestampStart, - transactionId: makeEventId( - TransactionType.Withdrawal, - wsr.withdrawalGroupId, - ), - ...(wsr.lastError ? { error: wsr.lastError } : {}), + let amountRaw: AmountJson | undefined = undefined; + if (wsr.withdrawalGroupId === r.initialWithdrawalGroupId) { + amountRaw = r.instructedAmount; + } else { + amountRaw = wsr.denomsSel.totalWithdrawCost; + } + let withdrawalDetails: WithdrawalDetails; + if (r.bankInfo) { + withdrawalDetails = { + type: WithdrawalType.TalerBankIntegrationApi, + confirmed: true, + bankConfirmationUrl: r.bankInfo.confirmUrl, + }; + } else { + const exchangeDetails = await getExchangeDetails( + tx, + wsr.exchangeBaseUrl, + ); + if (!exchangeDetails) { + // FIXME: report somehow + return; + } + withdrawalDetails = { + type: WithdrawalType.ManualTransfer, + exchangePaytoUris: + exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? + [], + }; + } + transactions.push({ + type: TransactionType.Withdrawal, + amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), + amountRaw: Amounts.stringify(amountRaw), + withdrawalDetails, + exchangeBaseUrl: wsr.exchangeBaseUrl, + pending: !wsr.timestampFinish, + timestamp: wsr.timestampStart, + transactionId: makeEventId( + TransactionType.Withdrawal, + wsr.withdrawalGroupId, + ), + ...(wsr.lastError ? { error: wsr.lastError } : {}), + }); }); - }); - // Report pending withdrawals based on reserves that - // were created, but where the actual withdrawal group has - // not started yet. - tx.iter(Stores.reserves).forEachAsync(async (r) => { - if (shouldSkipCurrency(transactionsRequest, r.currency)) { - return; - } - if (shouldSkipSearch(transactionsRequest, [])) { - return; - } - if (r.initialWithdrawalStarted) { - return; - } - if (r.reserveStatus === ReserveRecordStatus.BANK_ABORTED) { - return; - } - let withdrawalDetails: WithdrawalDetails; - if (r.bankInfo) { - withdrawalDetails = { - type: WithdrawalType.TalerBankIntegrationApi, - confirmed: false, - bankConfirmationUrl: r.bankInfo.confirmUrl, - }; - } else { - withdrawalDetails = { - type: WithdrawalType.ManualTransfer, - exchangePaytoUris: await getFundingPaytoUris(tx, r.reservePub), - }; - } - transactions.push({ - type: TransactionType.Withdrawal, - amountRaw: Amounts.stringify(r.instructedAmount), - amountEffective: Amounts.stringify(r.initialDenomSel.totalCoinValue), - exchangeBaseUrl: r.exchangeBaseUrl, - pending: true, - timestamp: r.timestampCreated, - withdrawalDetails: withdrawalDetails, - transactionId: makeEventId( - TransactionType.Withdrawal, - r.initialWithdrawalGroupId, - ), - ...(r.lastError ? { error: r.lastError } : {}), + // Report pending withdrawals based on reserves that + // were created, but where the actual withdrawal group has + // not started yet. + tx.reserves.iter().forEachAsync(async (r) => { + if (shouldSkipCurrency(transactionsRequest, r.currency)) { + return; + } + if (shouldSkipSearch(transactionsRequest, [])) { + return; + } + if (r.initialWithdrawalStarted) { + return; + } + if (r.reserveStatus === ReserveRecordStatus.BANK_ABORTED) { + return; + } + let withdrawalDetails: WithdrawalDetails; + if (r.bankInfo) { + withdrawalDetails = { + type: WithdrawalType.TalerBankIntegrationApi, + confirmed: false, + bankConfirmationUrl: r.bankInfo.confirmUrl, + }; + } else { + withdrawalDetails = { + type: WithdrawalType.ManualTransfer, + exchangePaytoUris: await getFundingPaytoUris(tx, r.reservePub), + }; + } + transactions.push({ + type: TransactionType.Withdrawal, + amountRaw: Amounts.stringify(r.instructedAmount), + amountEffective: Amounts.stringify( + r.initialDenomSel.totalCoinValue, + ), + exchangeBaseUrl: r.exchangeBaseUrl, + pending: true, + timestamp: r.timestampCreated, + withdrawalDetails: withdrawalDetails, + transactionId: makeEventId( + TransactionType.Withdrawal, + r.initialWithdrawalGroupId, + ), + ...(r.lastError ? { error: r.lastError } : {}), + }); }); - }); - tx.iter(Stores.depositGroups).forEachAsync(async (dg) => { - const amount = Amounts.parseOrThrow(dg.contractTermsRaw.amount); - if (shouldSkipCurrency(transactionsRequest, amount.currency)) { - return; - } - - transactions.push({ - type: TransactionType.Deposit, - amountRaw: Amounts.stringify(dg.effectiveDepositAmount), - amountEffective: Amounts.stringify(dg.totalPayCost), - pending: !dg.timestampFinished, - timestamp: dg.timestampCreated, - targetPaytoUri: dg.wire.payto_uri, - transactionId: makeEventId( - TransactionType.Deposit, - dg.depositGroupId, - ), - depositGroupId: dg.depositGroupId, - ...(dg.lastError ? { error: dg.lastError } : {}), - }); - }); + tx.depositGroups.iter().forEachAsync(async (dg) => { + const amount = Amounts.parseOrThrow(dg.contractTermsRaw.amount); + if (shouldSkipCurrency(transactionsRequest, amount.currency)) { + return; + } - tx.iter(Stores.purchases).forEachAsync(async (pr) => { - if ( - shouldSkipCurrency( - transactionsRequest, - pr.download.contractData.amount.currency, - ) - ) { - return; - } - const contractData = pr.download.contractData; - if (shouldSkipSearch(transactionsRequest, [contractData.summary])) { - return; - } - const proposal = await tx.get(Stores.proposals, pr.proposalId); - if (!proposal) { - return; - } - const info: OrderShortInfo = { - merchant: contractData.merchant, - orderId: contractData.orderId, - products: contractData.products, - summary: contractData.summary, - summary_i18n: contractData.summaryI18n, - contractTermsHash: contractData.contractTermsHash, - }; - if (contractData.fulfillmentUrl !== "") { - info.fulfillmentUrl = contractData.fulfillmentUrl; - } - const paymentTransactionId = makeEventId( - TransactionType.Payment, - pr.proposalId, - ); - const err = pr.lastPayError ?? pr.lastRefundStatusError; - transactions.push({ - type: TransactionType.Payment, - amountRaw: Amounts.stringify(contractData.amount), - amountEffective: Amounts.stringify(pr.totalPayCost), - status: pr.timestampFirstSuccessfulPay - ? PaymentStatus.Paid - : PaymentStatus.Accepted, - pending: - !pr.timestampFirstSuccessfulPay && - pr.abortStatus === AbortStatus.None, - timestamp: pr.timestampAccept, - transactionId: paymentTransactionId, - proposalId: pr.proposalId, - info: info, - ...(err ? { error: err } : {}), + transactions.push({ + type: TransactionType.Deposit, + amountRaw: Amounts.stringify(dg.effectiveDepositAmount), + amountEffective: Amounts.stringify(dg.totalPayCost), + pending: !dg.timestampFinished, + timestamp: dg.timestampCreated, + targetPaytoUri: dg.wire.payto_uri, + transactionId: makeEventId( + TransactionType.Deposit, + dg.depositGroupId, + ), + depositGroupId: dg.depositGroupId, + ...(dg.lastError ? { error: dg.lastError } : {}), + }); }); - const refundGroupKeys = new Set(); - - for (const rk of Object.keys(pr.refunds)) { - const refund = pr.refunds[rk]; - const groupKey = `${refund.executionTime.t_ms}`; - refundGroupKeys.add(groupKey); - } - - for (const groupKey of refundGroupKeys.values()) { - const refundTombstoneId = makeEventId( - TombstoneTag.DeleteRefund, - pr.proposalId, - groupKey, - ); - const tombstone = await tx.get(Stores.tombstones, refundTombstoneId); - if (tombstone) { - continue; + tx.purchases.iter().forEachAsync(async (pr) => { + if ( + shouldSkipCurrency( + transactionsRequest, + pr.download.contractData.amount.currency, + ) + ) { + return; } - const refundTransactionId = makeEventId( - TransactionType.Refund, + const contractData = pr.download.contractData; + if (shouldSkipSearch(transactionsRequest, [contractData.summary])) { + return; + } + const proposal = await tx.proposals.get(pr.proposalId); + if (!proposal) { + return; + } + const info: OrderShortInfo = { + merchant: contractData.merchant, + orderId: contractData.orderId, + products: contractData.products, + summary: contractData.summary, + summary_i18n: contractData.summaryI18n, + contractTermsHash: contractData.contractTermsHash, + }; + if (contractData.fulfillmentUrl !== "") { + info.fulfillmentUrl = contractData.fulfillmentUrl; + } + const paymentTransactionId = makeEventId( + TransactionType.Payment, pr.proposalId, - groupKey, ); - let r0: WalletRefundItem | undefined; - let amountRaw = Amounts.getZero(contractData.amount.currency); - let amountEffective = Amounts.getZero(contractData.amount.currency); + const err = pr.lastPayError ?? pr.lastRefundStatusError; + transactions.push({ + type: TransactionType.Payment, + amountRaw: Amounts.stringify(contractData.amount), + amountEffective: Amounts.stringify(pr.totalPayCost), + status: pr.timestampFirstSuccessfulPay + ? PaymentStatus.Paid + : PaymentStatus.Accepted, + pending: + !pr.timestampFirstSuccessfulPay && + pr.abortStatus === AbortStatus.None, + timestamp: pr.timestampAccept, + transactionId: paymentTransactionId, + proposalId: pr.proposalId, + info: info, + ...(err ? { error: err } : {}), + }); + + const refundGroupKeys = new Set(); + for (const rk of Object.keys(pr.refunds)) { const refund = pr.refunds[rk]; - const myGroupKey = `${refund.executionTime.t_ms}`; - if (myGroupKey !== groupKey) { + const groupKey = `${refund.executionTime.t_ms}`; + refundGroupKeys.add(groupKey); + } + + for (const groupKey of refundGroupKeys.values()) { + const refundTombstoneId = makeEventId( + TombstoneTag.DeleteRefund, + pr.proposalId, + groupKey, + ); + const tombstone = await tx.tombstones.get(refundTombstoneId); + if (tombstone) { continue; } + const refundTransactionId = makeEventId( + TransactionType.Refund, + pr.proposalId, + groupKey, + ); + let r0: WalletRefundItem | undefined; + let amountRaw = Amounts.getZero(contractData.amount.currency); + let amountEffective = Amounts.getZero(contractData.amount.currency); + for (const rk of Object.keys(pr.refunds)) { + const refund = pr.refunds[rk]; + const myGroupKey = `${refund.executionTime.t_ms}`; + if (myGroupKey !== groupKey) { + continue; + } + if (!r0) { + r0 = refund; + } + + if (refund.type === RefundState.Applied) { + amountRaw = Amounts.add(amountRaw, refund.refundAmount).amount; + amountEffective = Amounts.add( + amountEffective, + Amounts.sub( + refund.refundAmount, + refund.refundFee, + refund.totalRefreshCostBound, + ).amount, + ).amount; + } + } if (!r0) { - r0 = refund; + throw Error("invariant violated"); } + transactions.push({ + type: TransactionType.Refund, + info, + refundedTransactionId: paymentTransactionId, + transactionId: refundTransactionId, + timestamp: r0.obtainedTime, + amountEffective: Amounts.stringify(amountEffective), + amountRaw: Amounts.stringify(amountRaw), + pending: false, + }); + } + }); - if (refund.type === RefundState.Applied) { - amountRaw = Amounts.add(amountRaw, refund.refundAmount).amount; - amountEffective = Amounts.add( - amountEffective, - Amounts.sub( - refund.refundAmount, - refund.refundFee, - refund.totalRefreshCostBound, - ).amount, - ).amount; - } + tx.tips.iter().forEachAsync(async (tipRecord) => { + if ( + shouldSkipCurrency( + transactionsRequest, + tipRecord.tipAmountRaw.currency, + ) + ) { + return; } - if (!r0) { - throw Error("invariant violated"); + if (!tipRecord.acceptedTimestamp) { + return; } transactions.push({ - type: TransactionType.Refund, - info, - refundedTransactionId: paymentTransactionId, - transactionId: refundTransactionId, - timestamp: r0.obtainedTime, - amountEffective: Amounts.stringify(amountEffective), - amountRaw: Amounts.stringify(amountRaw), - pending: false, + type: TransactionType.Tip, + amountEffective: Amounts.stringify(tipRecord.tipAmountEffective), + amountRaw: Amounts.stringify(tipRecord.tipAmountRaw), + pending: !tipRecord.pickedUpTimestamp, + timestamp: tipRecord.acceptedTimestamp, + transactionId: makeEventId( + TransactionType.Tip, + tipRecord.walletTipId, + ), + merchantBaseUrl: tipRecord.merchantBaseUrl, + error: tipRecord.lastError, }); - } - }); - - tx.iter(Stores.tips).forEachAsync(async (tipRecord) => { - if ( - shouldSkipCurrency( - transactionsRequest, - tipRecord.tipAmountRaw.currency, - ) - ) { - return; - } - if (!tipRecord.acceptedTimestamp) { - return; - } - transactions.push({ - type: TransactionType.Tip, - amountEffective: Amounts.stringify(tipRecord.tipAmountEffective), - amountRaw: Amounts.stringify(tipRecord.tipAmountRaw), - pending: !tipRecord.pickedUpTimestamp, - timestamp: tipRecord.acceptedTimestamp, - transactionId: makeEventId( - TransactionType.Tip, - tipRecord.walletTipId, - ), - merchantBaseUrl: tipRecord.merchantBaseUrl, - error: tipRecord.lastError, }); - }); - }, - ); + }, + ); const txPending = transactions.filter((x) => x.pending); const txNotPending = transactions.filter((x) => !x.pending); @@ -406,110 +409,126 @@ export async function deleteTransaction( if (type === TransactionType.Withdrawal) { const withdrawalGroupId = rest[0]; - await ws.db.runWithWriteTransaction( - [Stores.withdrawalGroups, Stores.reserves, Stores.tombstones], - async (tx) => { - const withdrawalGroupRecord = await tx.get( - Stores.withdrawalGroups, + await ws.db + .mktx((x) => ({ + withdrawalGroups: x.withdrawalGroups, + reserves: x.reserves, + tombstones: x.tombstones, + })) + .runReadWrite(async (tx) => { + const withdrawalGroupRecord = await tx.withdrawalGroups.get( withdrawalGroupId, ); if (withdrawalGroupRecord) { - await tx.delete(Stores.withdrawalGroups, withdrawalGroupId); - await tx.put(Stores.tombstones, { + await tx.withdrawalGroups.delete(withdrawalGroupId); + await tx.tombstones.put({ id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId, }); return; } - const reserveRecord: ReserveRecord | undefined = await tx.getIndexed( - Stores.reserves.byInitialWithdrawalGroupId, + const reserveRecord: + | ReserveRecord + | undefined = await tx.reserves.indexes.byInitialWithdrawalGroupId.get( withdrawalGroupId, ); if (reserveRecord && !reserveRecord.initialWithdrawalStarted) { const reservePub = reserveRecord.reservePub; - await tx.delete(Stores.reserves, reservePub); - await tx.put(Stores.tombstones, { + await tx.reserves.delete(reservePub); + await tx.tombstones.put({ id: TombstoneTag.DeleteReserve + ":" + reservePub, }); } - }, - ); + }); } else if (type === TransactionType.Payment) { const proposalId = rest[0]; - await ws.db.runWithWriteTransaction( - [Stores.proposals, Stores.purchases, Stores.tombstones], - async (tx) => { + await ws.db + .mktx((x) => ({ + proposals: x.proposals, + purchases: x.purchases, + tombstones: x.tombstones, + })) + .runReadWrite(async (tx) => { let found = false; - const proposal = await tx.get(Stores.proposals, proposalId); + const proposal = await tx.proposals.get(proposalId); if (proposal) { found = true; - await tx.delete(Stores.proposals, proposalId); + await tx.proposals.delete(proposalId); } - const purchase = await tx.get(Stores.purchases, proposalId); + const purchase = await tx.purchases.get(proposalId); if (purchase) { found = true; - await tx.delete(Stores.proposals, proposalId); + await tx.proposals.delete(proposalId); } if (found) { - await tx.put(Stores.tombstones, { + await tx.tombstones.put({ id: TombstoneTag.DeletePayment + ":" + proposalId, }); } - }, - ); + }); } else if (type === TransactionType.Refresh) { const refreshGroupId = rest[0]; - await ws.db.runWithWriteTransaction( - [Stores.refreshGroups, Stores.tombstones], - async (tx) => { - const rg = await tx.get(Stores.refreshGroups, refreshGroupId); + await ws.db + .mktx((x) => ({ + refreshGroups: x.refreshGroups, + tombstones: x.tombstones, + })) + .runReadWrite(async (tx) => { + const rg = await tx.refreshGroups.get(refreshGroupId); if (rg) { - await tx.delete(Stores.refreshGroups, refreshGroupId); - await tx.put(Stores.tombstones, { + await tx.refreshGroups.delete(refreshGroupId); + await tx.tombstones.put({ id: TombstoneTag.DeleteRefreshGroup + ":" + refreshGroupId, }); } - }, - ); + }); } else if (type === TransactionType.Tip) { const tipId = rest[0]; - await ws.db.runWithWriteTransaction( - [Stores.tips, Stores.tombstones], - async (tx) => { - const tipRecord = await tx.get(Stores.tips, tipId); + await ws.db + .mktx((x) => ({ + tips: x.tips, + tombstones: x.tombstones, + })) + .runReadWrite(async (tx) => { + const tipRecord = await tx.tips.get(tipId); if (tipRecord) { - await tx.delete(Stores.tips, tipId); - await tx.put(Stores.tombstones, { + await tx.tips.delete(tipId); + await tx.tombstones.put({ id: TombstoneTag.DeleteTip + ":" + tipId, }); } - }, - ); + }); } else if (type === TransactionType.Deposit) { const depositGroupId = rest[0]; - await ws.db.runWithWriteTransaction( - [Stores.depositGroups, Stores.tombstones], - async (tx) => { - const tipRecord = await tx.get(Stores.depositGroups, depositGroupId); + await ws.db + .mktx((x) => ({ + depositGroups: x.depositGroups, + tombstones: x.tombstones, + })) + .runReadWrite(async (tx) => { + const tipRecord = await tx.depositGroups.get(depositGroupId); if (tipRecord) { - await tx.delete(Stores.depositGroups, depositGroupId); - await tx.put(Stores.tombstones, { + await tx.depositGroups.delete(depositGroupId); + await tx.tombstones.put({ id: TombstoneTag.DeleteDepositGroup + ":" + depositGroupId, }); } - }, - ); + }); } else if (type === TransactionType.Refund) { const proposalId = rest[0]; const executionTimeStr = rest[1]; - await ws.db.runWithWriteTransaction( - [Stores.proposals, Stores.purchases, Stores.tombstones], - async (tx) => { - const purchase = await tx.get(Stores.purchases, proposalId); + await ws.db + .mktx((x) => ({ + proposals: x.proposals, + purchases: x.purchases, + tombstones: x.tombstones, + })) + .runReadWrite(async (tx) => { + const purchase = await tx.purchases.get(proposalId); if (purchase) { // This should just influence the history view, // but won't delete any actual refund information. - await tx.put(Stores.tombstones, { + await tx.tombstones.put({ id: makeEventId( TombstoneTag.DeleteRefund, proposalId, @@ -517,8 +536,7 @@ export async function deleteTransaction( ), }); } - }, - ); + }); } else { throw Error(`can't delete a '${type}' transaction`); } -- cgit v1.2.3