diff options
Diffstat (limited to 'packages/taler-wallet-core/src/transactions.ts')
-rw-r--r-- | packages/taler-wallet-core/src/transactions.ts | 1744 |
1 files changed, 148 insertions, 1596 deletions
diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts index 0649f9ce2..3bf0f7d10 100644 --- a/packages/taler-wallet-core/src/transactions.ts +++ b/packages/taler-wallet-core/src/transactions.ts @@ -17,125 +17,50 @@ /** * Imports. */ -import { GlobalIDB } from "@gnu-taler/idb-bridge"; +import { GlobalIDB, IDBKeyRange } from "@gnu-taler/idb-bridge"; import { AbsoluteTime, Amounts, assertUnreachable, - checkDbInvariant, - DepositTransactionTrackingState, j2s, Logger, + makeTalerErrorDetail, NotificationType, - OrderShortInfo, - PeerContractTerms, - RefundInfoShort, - RefundPaymentInfo, ScopeType, - stringifyPayPullUri, - stringifyPayPushUri, TalerErrorCode, - TalerPreciseTimestamp, Transaction, - TransactionAction, TransactionByIdRequest, TransactionIdStr, TransactionMajorState, - TransactionRecordFilter, TransactionsRequest, TransactionsResponse, TransactionState, TransactionType, - TransactionWithdrawal, - WalletContractData, - WithdrawalTransactionByURIRequest, - WithdrawalType, } from "@gnu-taler/taler-util"; import { constructTaskIdentifier, PendingTaskType, - TaskIdentifiers, TaskIdStr, TransactionContext, } from "./common.js"; import { - DenomLossEventRecord, - DepositElementStatus, - DepositGroupRecord, OPERATION_STATUS_NONFINAL_FIRST, OPERATION_STATUS_NONFINAL_LAST, - OperationRetryRecord, - PeerPullCreditRecord, - PeerPullDebitRecordStatus, - PeerPullPaymentIncomingRecord, - PeerPushCreditStatus, - PeerPushDebitRecord, - PeerPushDebitStatus, - PeerPushPaymentIncomingRecord, - PurchaseRecord, - PurchaseStatus, - RefreshGroupRecord, - RefreshOperationStatus, - RefundGroupRecord, - timestampPreciseFromDb, - timestampProtocolFromDb, - WalletDbReadOnlyTransaction, - WithdrawalGroupRecord, - WithdrawalGroupStatus, - WithdrawalRecordType, + WalletDbAllStoresReadWriteTransaction, } from "./db.js"; +import { DepositTransactionContext } from "./deposits.js"; +import { DenomLossTransactionContext } from "./exchanges.js"; import { - computeDepositTransactionActions, - computeDepositTransactionStatus, - DepositTransactionContext, -} from "./deposits.js"; -import { - computeDenomLossTransactionStatus, - DenomLossTransactionContext, - ExchangeWireDetails, - getExchangeWireDetailsInTx, -} from "./exchanges.js"; -import { - computePayMerchantTransactionActions, - computePayMerchantTransactionState, - computeRefundTransactionState, - expectProposalDownloadInTx, - extractContractData, PayMerchantTransactionContext, RefundTransactionContext, } from "./pay-merchant.js"; -import { - computePeerPullCreditTransactionActions, - computePeerPullCreditTransactionState, - PeerPullCreditTransactionContext, -} from "./pay-peer-pull-credit.js"; -import { - computePeerPullDebitTransactionActions, - computePeerPullDebitTransactionState, - PeerPullDebitTransactionContext, -} from "./pay-peer-pull-debit.js"; -import { - computePeerPushCreditTransactionActions, - computePeerPushCreditTransactionState, - PeerPushCreditTransactionContext, -} from "./pay-peer-push-credit.js"; -import { - computePeerPushDebitTransactionActions, - computePeerPushDebitTransactionState, - PeerPushDebitTransactionContext, -} from "./pay-peer-push-debit.js"; -import { - computeRefreshTransactionActions, - computeRefreshTransactionState, - RefreshTransactionContext, -} from "./refresh.js"; +import { PeerPullCreditTransactionContext } from "./pay-peer-pull-credit.js"; +import { PeerPullDebitTransactionContext } from "./pay-peer-pull-debit.js"; +import { PeerPushCreditTransactionContext } from "./pay-peer-push-credit.js"; +import { PeerPushDebitTransactionContext } from "./pay-peer-push-debit.js"; +import { RefreshTransactionContext } from "./refresh.js"; import type { WalletExecutionContext } from "./wallet.js"; -import { - augmentPaytoUrisForWithdrawal, - computeWithdrawalTransactionActions, - computeWithdrawalTransactionStatus, - WithdrawTransactionContext, -} from "./withdraw.js"; +import { WithdrawTransactionContext } from "./withdraw.js"; const logger = new Logger("taler-wallet-core:transactions.ts"); @@ -177,22 +102,6 @@ function shouldSkipCurrency( return false; } -function shouldSkipSearch( - transactionsRequest: TransactionsRequest | undefined, - fields: string[], -): boolean { - if (!transactionsRequest?.search) { - return false; - } - const needle = transactionsRequest.search.trim(); - for (const f of fields) { - if (f.indexOf(needle) >= 0) { - return false; - } - } - return true; -} - /** * Fallback order of transactions that have the same timestamp. */ @@ -223,569 +132,29 @@ export async function getTransactionById( switch (parsedTx.tag) { case TransactionType.InternalWithdrawal: - case TransactionType.Withdrawal: { - const withdrawalGroupId = parsedTx.withdrawalGroupId; - return await wex.db.runReadWriteTx( - { - storeNames: [ - "withdrawalGroups", - "exchangeDetails", - "exchanges", - "operationRetries", - ], - }, - async (tx) => { - const withdrawalGroupRecord = - await tx.withdrawalGroups.get(withdrawalGroupId); - - if (!withdrawalGroupRecord) throw Error("not found"); - - const opId = TaskIdentifiers.forWithdrawal(withdrawalGroupRecord); - const ort = await tx.operationRetries.get(opId); - - const exchangeDetails = - withdrawalGroupRecord.exchangeBaseUrl === undefined - ? undefined - : await getExchangeWireDetailsInTx( - tx, - withdrawalGroupRecord.exchangeBaseUrl, - ); - // if (!exchangeDetails) throw Error("not exchange details"); - - if ( - withdrawalGroupRecord.wgInfo.withdrawalType === - WithdrawalRecordType.BankIntegrated - ) { - return buildTransactionForBankIntegratedWithdraw( - withdrawalGroupRecord, - exchangeDetails, - ort, - ); - } - checkDbInvariant( - exchangeDetails !== undefined, - "manual withdrawal without exchange", - ); - return buildTransactionForManualWithdraw( - withdrawalGroupRecord, - exchangeDetails, - ort, - ); - }, - ); - } - - case TransactionType.DenomLoss: { - const rec = await wex.db.runReadOnlyTx( - { storeNames: ["denomLossEvents"] }, - async (tx) => { - return tx.denomLossEvents.get(parsedTx.denomLossEventId); - }, - ); - if (!rec) { - throw Error("denom loss record not found"); - } - return buildTransactionForDenomLoss(rec); - } - + case TransactionType.Withdrawal: + case TransactionType.DenomLoss: case TransactionType.Recoup: - throw new Error("not yet supported"); - - case TransactionType.Payment: { - const proposalId = parsedTx.proposalId; - return await wex.db.runReadWriteTx( - { - storeNames: [ - "purchases", - "tombstones", - "operationRetries", - "contractTerms", - "refundGroups", - ], - }, - async (tx) => { - const purchase = await tx.purchases.get(proposalId); - if (!purchase) throw Error("not found"); - const download = await expectProposalDownloadInTx(wex, tx, purchase); - const contractData = download.contractData; - const payOpId = TaskIdentifiers.forPay(purchase); - const payRetryRecord = await tx.operationRetries.get(payOpId); - - const refunds = await tx.refundGroups.indexes.byProposalId.getAll( - purchase.proposalId, - ); - - return buildTransactionForPurchase( - purchase, - contractData, - refunds, - payRetryRecord, - ); - }, - ); - } - - case TransactionType.Refresh: { - // FIXME: We should return info about the refresh here!; - const refreshGroupId = parsedTx.refreshGroupId; - return await wex.db.runReadOnlyTx( - { storeNames: ["refreshGroups", "operationRetries"] }, - async (tx) => { - const refreshGroupRec = await tx.refreshGroups.get(refreshGroupId); - if (!refreshGroupRec) { - throw Error("not found"); - } - const retries = await tx.operationRetries.get( - TaskIdentifiers.forRefresh(refreshGroupRec), - ); - return buildTransactionForRefresh(refreshGroupRec, retries); - }, - ); - } - - case TransactionType.Deposit: { - const depositGroupId = parsedTx.depositGroupId; - return await wex.db.runReadWriteTx( - { storeNames: ["depositGroups", "operationRetries"] }, - async (tx) => { - const depositRecord = await tx.depositGroups.get(depositGroupId); - if (!depositRecord) throw Error("not found"); - - const retries = await tx.operationRetries.get( - TaskIdentifiers.forDeposit(depositRecord), - ); - return buildTransactionForDeposit(depositRecord, retries); - }, - ); - } - + case TransactionType.PeerPushDebit: + case TransactionType.PeerPushCredit: + case TransactionType.Refresh: + case TransactionType.PeerPullCredit: + case TransactionType.Payment: + case TransactionType.Deposit: + case TransactionType.PeerPullDebit: case TransactionType.Refund: { - return await wex.db.runReadOnlyTx( - { - storeNames: [ - "refundGroups", - "purchases", - "operationRetries", - "contractTerms", - ], - }, - async (tx) => { - const refundRecord = await tx.refundGroups.get( - parsedTx.refundGroupId, - ); - if (!refundRecord) { - throw Error("not found"); - } - const contractData = await lookupMaybeContractData( - tx, - refundRecord?.proposalId, - ); - return buildTransactionForRefund(refundRecord, contractData); - }, - ); - } - case TransactionType.PeerPullDebit: { - return await wex.db.runReadWriteTx( - { storeNames: ["peerPullDebit", "contractTerms"] }, - async (tx) => { - const debit = await tx.peerPullDebit.get(parsedTx.peerPullDebitId); - if (!debit) throw Error("not found"); - const contractTermsRec = await tx.contractTerms.get( - debit.contractTermsHash, - ); - if (!contractTermsRec) - throw Error("contract terms for peer-pull-debit not found"); - return buildTransactionForPullPaymentDebit( - debit, - contractTermsRec.contractTermsRaw, - ); - }, - ); - } - - case TransactionType.PeerPushDebit: { - return await wex.db.runReadWriteTx( - { storeNames: ["peerPushDebit", "contractTerms"] }, - async (tx) => { - const debit = await tx.peerPushDebit.get(parsedTx.pursePub); - if (!debit) throw Error("not found"); - const ct = await tx.contractTerms.get(debit.contractTermsHash); - checkDbInvariant( - !!ct, - `no contract terms for p2p push ${parsedTx.pursePub}`, - ); - return buildTransactionForPushPaymentDebit( - debit, - ct.contractTermsRaw, - ); - }, - ); - } - - case TransactionType.PeerPushCredit: { - const peerPushCreditId = parsedTx.peerPushCreditId; - return await wex.db.runReadWriteTx( - { - storeNames: [ - "peerPushCredit", - "contractTerms", - "withdrawalGroups", - "operationRetries", - ], - }, - async (tx) => { - const pushInc = await tx.peerPushCredit.get(peerPushCreditId); - if (!pushInc) throw Error("not found"); - const ct = await tx.contractTerms.get(pushInc.contractTermsHash); - checkDbInvariant( - !!ct, - `no contract terms for p2p push ${peerPushCreditId}`, - ); - - let wg: WithdrawalGroupRecord | undefined = undefined; - let wgOrt: OperationRetryRecord | undefined = undefined; - if (pushInc.withdrawalGroupId) { - wg = await tx.withdrawalGroups.get(pushInc.withdrawalGroupId); - if (wg) { - const withdrawalOpId = TaskIdentifiers.forWithdrawal(wg); - wgOrt = await tx.operationRetries.get(withdrawalOpId); - } - } - const pushIncOpId = TaskIdentifiers.forPeerPushCredit(pushInc); - const pushIncOrt = await tx.operationRetries.get(pushIncOpId); - - return buildTransactionForPeerPushCredit( - pushInc, - pushIncOrt, - ct.contractTermsRaw, - wg, - wgOrt, - ); - }, - ); - } - - case TransactionType.PeerPullCredit: { - const pursePub = parsedTx.pursePub; - return await wex.db.runReadWriteTx( - { - storeNames: [ - "peerPullCredit", - "contractTerms", - "withdrawalGroups", - "operationRetries", - ], - }, - async (tx) => { - const pushInc = await tx.peerPullCredit.get(pursePub); - if (!pushInc) throw Error("not found"); - const ct = await tx.contractTerms.get(pushInc.contractTermsHash); - checkDbInvariant(!!ct, `no contract terms for p2p push ${pursePub}`); - - let wg: WithdrawalGroupRecord | undefined = undefined; - let wgOrt: OperationRetryRecord | undefined = undefined; - if (pushInc.withdrawalGroupId) { - wg = await tx.withdrawalGroups.get(pushInc.withdrawalGroupId); - if (wg) { - const withdrawalOpId = TaskIdentifiers.forWithdrawal(wg); - wgOrt = await tx.operationRetries.get(withdrawalOpId); - } - } - const pushIncOpId = - TaskIdentifiers.forPeerPullPaymentInitiation(pushInc); - let pushIncOrt = await tx.operationRetries.get(pushIncOpId); - - return buildTransactionForPeerPullCredit( - pushInc, - pushIncOrt, - ct.contractTermsRaw, - wg, - wgOrt, - ); - }, + const ctx = await getContextForTransaction(wex, req.transactionId); + const txDetails = await wex.db.runAllStoresReadOnlyTx({}, async (tx) => + ctx.lookupFullTransaction(tx), ); + if (!txDetails) { + throw Error("transaction not found"); + } + return txDetails; } } } -function buildTransactionForPushPaymentDebit( - pi: PeerPushDebitRecord, - contractTerms: PeerContractTerms, - ort?: OperationRetryRecord, -): Transaction { - let talerUri: string | undefined = undefined; - switch (pi.status) { - case PeerPushDebitStatus.PendingReady: - case PeerPushDebitStatus.SuspendedReady: - talerUri = stringifyPayPushUri({ - exchangeBaseUrl: pi.exchangeBaseUrl, - contractPriv: pi.contractPriv, - }); - } - const txState = computePeerPushDebitTransactionState(pi); - return { - type: TransactionType.PeerPushDebit, - txState, - txActions: computePeerPushDebitTransactionActions(pi), - amountEffective: isUnsuccessfulTransaction(txState) - ? Amounts.stringify(Amounts.zeroOfAmount(pi.totalCost)) - : pi.totalCost, - amountRaw: pi.amount, - exchangeBaseUrl: pi.exchangeBaseUrl, - info: { - expiration: contractTerms.purse_expiration, - summary: contractTerms.summary, - }, - timestamp: timestampPreciseFromDb(pi.timestampCreated), - talerUri, - transactionId: constructTransactionIdentifier({ - tag: TransactionType.PeerPushDebit, - pursePub: pi.pursePub, - }), - ...(ort?.lastError ? { error: ort.lastError } : {}), - }; -} - -function buildTransactionForPullPaymentDebit( - pi: PeerPullPaymentIncomingRecord, - contractTerms: PeerContractTerms, - ort?: OperationRetryRecord, -): Transaction { - const txState = computePeerPullDebitTransactionState(pi); - return { - type: TransactionType.PeerPullDebit, - txState, - txActions: computePeerPullDebitTransactionActions(pi), - amountEffective: isUnsuccessfulTransaction(txState) - ? Amounts.stringify(Amounts.zeroOfAmount(pi.amount)) - : pi.coinSel?.totalCost - ? pi.coinSel?.totalCost - : Amounts.stringify(pi.amount), - amountRaw: Amounts.stringify(pi.amount), - exchangeBaseUrl: pi.exchangeBaseUrl, - info: { - expiration: contractTerms.purse_expiration, - summary: contractTerms.summary, - }, - timestamp: timestampPreciseFromDb(pi.timestampCreated), - transactionId: constructTransactionIdentifier({ - tag: TransactionType.PeerPullDebit, - peerPullDebitId: pi.peerPullDebitId, - }), - ...(ort?.lastError ? { error: ort.lastError } : {}), - }; -} - -function buildTransactionForPeerPullCredit( - pullCredit: PeerPullCreditRecord, - pullCreditOrt: OperationRetryRecord | undefined, - peerContractTerms: PeerContractTerms, - wsr: WithdrawalGroupRecord | undefined, - wsrOrt: OperationRetryRecord | undefined, -): Transaction { - if (wsr) { - if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPullCredit) { - throw Error(`Unexpected withdrawalType: ${wsr.wgInfo.withdrawalType}`); - } - /** - * FIXME: this should be handled in the withdrawal process. - * PeerPull withdrawal fails until reserve have funds but it is not - * an error from the user perspective. - */ - const silentWithdrawalErrorForInvoice = - wsrOrt?.lastError && - wsrOrt.lastError.code === - TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE && - Object.values(wsrOrt.lastError.errorsPerCoin ?? {}).every((e) => { - return ( - e.code === TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR && - e.httpStatusCode === 409 - ); - }); - const txState = computePeerPullCreditTransactionState(pullCredit); - checkDbInvariant(wsr.instructedAmount !== undefined, "wg uninitialized"); - checkDbInvariant(wsr.denomsSel !== undefined, "wg uninitialized"); - checkDbInvariant(wsr.exchangeBaseUrl !== undefined, "wg uninitialized"); - return { - type: TransactionType.PeerPullCredit, - txState, - txActions: computePeerPullCreditTransactionActions(pullCredit), - amountEffective: isUnsuccessfulTransaction(txState) - ? Amounts.stringify(Amounts.zeroOfAmount(wsr.instructedAmount)) - : Amounts.stringify(wsr.denomsSel.totalCoinValue), - amountRaw: Amounts.stringify(wsr.instructedAmount), - exchangeBaseUrl: wsr.exchangeBaseUrl, - timestamp: timestampPreciseFromDb(pullCredit.mergeTimestamp), - info: { - expiration: peerContractTerms.purse_expiration, - summary: peerContractTerms.summary, - }, - talerUri: stringifyPayPullUri({ - exchangeBaseUrl: wsr.exchangeBaseUrl, - contractPriv: wsr.wgInfo.contractPriv, - }), - transactionId: constructTransactionIdentifier({ - tag: TransactionType.PeerPullCredit, - pursePub: pullCredit.pursePub, - }), - kycUrl: pullCredit.kycUrl, - ...(wsrOrt?.lastError - ? { - error: silentWithdrawalErrorForInvoice - ? undefined - : wsrOrt.lastError, - } - : {}), - }; - } - - const txState = computePeerPullCreditTransactionState(pullCredit); - return { - type: TransactionType.PeerPullCredit, - txState, - txActions: computePeerPullCreditTransactionActions(pullCredit), - amountEffective: isUnsuccessfulTransaction(txState) - ? Amounts.stringify(Amounts.zeroOfAmount(peerContractTerms.amount)) - : Amounts.stringify(pullCredit.estimatedAmountEffective), - amountRaw: Amounts.stringify(peerContractTerms.amount), - exchangeBaseUrl: pullCredit.exchangeBaseUrl, - timestamp: timestampPreciseFromDb(pullCredit.mergeTimestamp), - info: { - expiration: peerContractTerms.purse_expiration, - summary: peerContractTerms.summary, - }, - talerUri: stringifyPayPullUri({ - exchangeBaseUrl: pullCredit.exchangeBaseUrl, - contractPriv: pullCredit.contractPriv, - }), - transactionId: constructTransactionIdentifier({ - tag: TransactionType.PeerPullCredit, - pursePub: pullCredit.pursePub, - }), - kycUrl: pullCredit.kycUrl, - ...(pullCreditOrt?.lastError ? { error: pullCreditOrt.lastError } : {}), - }; -} - -function buildTransactionForPeerPushCredit( - pushInc: PeerPushPaymentIncomingRecord, - pushOrt: OperationRetryRecord | undefined, - peerContractTerms: PeerContractTerms, - wg: WithdrawalGroupRecord | undefined, - wsrOrt: OperationRetryRecord | undefined, -): Transaction { - if (wg) { - if (wg.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPushCredit) { - throw Error("invalid withdrawal group type for push payment credit"); - } - checkDbInvariant(wg.instructedAmount !== undefined, "wg uninitialized"); - checkDbInvariant(wg.denomsSel !== undefined, "wg uninitialized"); - checkDbInvariant(wg.exchangeBaseUrl !== undefined, "wg uninitialized"); - - const txState = computePeerPushCreditTransactionState(pushInc); - return { - type: TransactionType.PeerPushCredit, - txState, - txActions: computePeerPushCreditTransactionActions(pushInc), - amountEffective: isUnsuccessfulTransaction(txState) - ? Amounts.stringify(Amounts.zeroOfAmount(wg.instructedAmount)) - : Amounts.stringify(wg.denomsSel.totalCoinValue), - amountRaw: Amounts.stringify(wg.instructedAmount), - exchangeBaseUrl: wg.exchangeBaseUrl, - info: { - expiration: peerContractTerms.purse_expiration, - summary: peerContractTerms.summary, - }, - timestamp: timestampPreciseFromDb(wg.timestampStart), - transactionId: constructTransactionIdentifier({ - tag: TransactionType.PeerPushCredit, - peerPushCreditId: pushInc.peerPushCreditId, - }), - kycUrl: pushInc.kycUrl, - ...(wsrOrt?.lastError ? { error: wsrOrt.lastError } : {}), - }; - } - - const txState = computePeerPushCreditTransactionState(pushInc); - return { - type: TransactionType.PeerPushCredit, - txState, - txActions: computePeerPushCreditTransactionActions(pushInc), - amountEffective: isUnsuccessfulTransaction(txState) - ? Amounts.stringify(Amounts.zeroOfAmount(peerContractTerms.amount)) - : // FIXME: This is wrong, needs to consider fees! - Amounts.stringify(peerContractTerms.amount), - amountRaw: Amounts.stringify(peerContractTerms.amount), - exchangeBaseUrl: pushInc.exchangeBaseUrl, - info: { - expiration: peerContractTerms.purse_expiration, - summary: peerContractTerms.summary, - }, - kycUrl: pushInc.kycUrl, - timestamp: timestampPreciseFromDb(pushInc.timestamp), - transactionId: constructTransactionIdentifier({ - tag: TransactionType.PeerPushCredit, - peerPushCreditId: pushInc.peerPushCreditId, - }), - ...(pushOrt?.lastError ? { error: pushOrt.lastError } : {}), - }; -} - -function buildTransactionForBankIntegratedWithdraw( - wg: WithdrawalGroupRecord, - exchangeDetails: ExchangeWireDetails | undefined, - ort?: OperationRetryRecord, -): TransactionWithdrawal { - if (wg.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) { - throw Error(""); - } - const instructedCurrency = - wg.instructedAmount === undefined - ? undefined - : Amounts.currencyOf(wg.instructedAmount); - const currency = wg.wgInfo.bankInfo.currency ?? instructedCurrency; - checkDbInvariant( - currency !== undefined, - "wg uninitialized (missing currency)", - ); - const txState = computeWithdrawalTransactionStatus(wg); - - const zero = Amounts.stringify(Amounts.zeroOfCurrency(currency)); - return { - type: TransactionType.Withdrawal, - txState, - txActions: computeWithdrawalTransactionActions(wg), - exchangeBaseUrl: wg.exchangeBaseUrl, - amountEffective: - isUnsuccessfulTransaction(txState) || !wg.denomsSel - ? zero - : Amounts.stringify(wg.denomsSel.totalCoinValue), - amountRaw: !wg.instructedAmount - ? zero - : Amounts.stringify(wg.instructedAmount), - withdrawalDetails: { - type: WithdrawalType.TalerBankIntegrationApi, - confirmed: wg.wgInfo.bankInfo.timestampBankConfirmed ? true : false, - exchangeCreditAccountDetails: wg.wgInfo.exchangeCreditAccounts, - reservePub: wg.reservePub, - bankConfirmationUrl: wg.wgInfo.bankInfo.externalConfirmation - ? undefined - : wg.wgInfo.bankInfo.confirmUrl, - externalConfirmation: wg.wgInfo.bankInfo.externalConfirmation, - reserveIsReady: - wg.status === WithdrawalGroupStatus.Done || - wg.status === WithdrawalGroupStatus.PendingReady, - }, - kycUrl: wg.kycUrl, - timestamp: timestampPreciseFromDb(wg.timestampStart), - transactionId: constructTransactionIdentifier({ - tag: TransactionType.Withdrawal, - withdrawalGroupId: wg.withdrawalGroupId, - }), - ...(ort?.lastError ? { error: ort.lastError } : {}), - }; -} - export function isUnsuccessfulTransaction(state: TransactionState): boolean { return ( state.major === TransactionMajorState.Aborted || @@ -796,365 +165,6 @@ export function isUnsuccessfulTransaction(state: TransactionState): boolean { ); } -function buildTransactionForManualWithdraw( - wg: WithdrawalGroupRecord, - exchangeDetails: ExchangeWireDetails, - ort?: OperationRetryRecord, -): TransactionWithdrawal { - if (wg.wgInfo.withdrawalType !== WithdrawalRecordType.BankManual) - throw Error(""); - - const plainPaytoUris = - exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? []; - - checkDbInvariant(wg.instructedAmount !== undefined, "wg uninitialized"); - checkDbInvariant(wg.denomsSel !== undefined, "wg uninitialized"); - checkDbInvariant(wg.exchangeBaseUrl !== undefined, "wg uninitialized"); - const exchangePaytoUris = augmentPaytoUrisForWithdrawal( - plainPaytoUris, - wg.reservePub, - wg.instructedAmount, - ); - - const txState = computeWithdrawalTransactionStatus(wg); - - return { - type: TransactionType.Withdrawal, - txState, - txActions: computeWithdrawalTransactionActions(wg), - amountEffective: isUnsuccessfulTransaction(txState) - ? Amounts.stringify(Amounts.zeroOfAmount(wg.instructedAmount)) - : Amounts.stringify(wg.denomsSel.totalCoinValue), - amountRaw: Amounts.stringify(wg.instructedAmount), - withdrawalDetails: { - type: WithdrawalType.ManualTransfer, - reservePub: wg.reservePub, - exchangePaytoUris, - exchangeCreditAccountDetails: wg.wgInfo.exchangeCreditAccounts, - reserveIsReady: - wg.status === WithdrawalGroupStatus.Done || - wg.status === WithdrawalGroupStatus.PendingReady, - }, - kycUrl: wg.kycUrl, - exchangeBaseUrl: wg.exchangeBaseUrl, - timestamp: timestampPreciseFromDb(wg.timestampStart), - transactionId: constructTransactionIdentifier({ - tag: TransactionType.Withdrawal, - withdrawalGroupId: wg.withdrawalGroupId, - }), - ...(ort?.lastError ? { error: ort.lastError } : {}), - }; -} - -function buildTransactionForRefund( - refundRecord: RefundGroupRecord, - maybeContractData: WalletContractData | undefined, -): Transaction { - let paymentInfo: RefundPaymentInfo | undefined = undefined; - - if (maybeContractData) { - paymentInfo = { - merchant: maybeContractData.merchant, - summary: maybeContractData.summary, - summary_i18n: maybeContractData.summaryI18n, - }; - } - - const txState = computeRefundTransactionState(refundRecord); - return { - type: TransactionType.Refund, - amountEffective: isUnsuccessfulTransaction(txState) - ? Amounts.stringify(Amounts.zeroOfAmount(refundRecord.amountEffective)) - : refundRecord.amountEffective, - amountRaw: refundRecord.amountRaw, - refundedTransactionId: constructTransactionIdentifier({ - tag: TransactionType.Payment, - proposalId: refundRecord.proposalId, - }), - timestamp: timestampPreciseFromDb(refundRecord.timestampCreated), - transactionId: constructTransactionIdentifier({ - tag: TransactionType.Refund, - refundGroupId: refundRecord.refundGroupId, - }), - txState, - txActions: [], - paymentInfo, - }; -} - -function buildTransactionForRefresh( - refreshGroupRecord: RefreshGroupRecord, - ort?: OperationRetryRecord, -): Transaction { - const inputAmount = Amounts.sumOrZero( - refreshGroupRecord.currency, - refreshGroupRecord.inputPerCoin, - ).amount; - const outputAmount = Amounts.sumOrZero( - refreshGroupRecord.currency, - refreshGroupRecord.expectedOutputPerCoin, - ).amount; - const txState = computeRefreshTransactionState(refreshGroupRecord); - return { - type: TransactionType.Refresh, - txState, - txActions: computeRefreshTransactionActions(refreshGroupRecord), - refreshReason: refreshGroupRecord.reason, - amountEffective: isUnsuccessfulTransaction(txState) - ? Amounts.stringify(Amounts.zeroOfAmount(inputAmount)) - : Amounts.stringify(Amounts.sub(outputAmount, inputAmount).amount), - amountRaw: Amounts.stringify( - Amounts.zeroOfCurrency(refreshGroupRecord.currency), - ), - refreshInputAmount: Amounts.stringify(inputAmount), - refreshOutputAmount: Amounts.stringify(outputAmount), - originatingTransactionId: refreshGroupRecord.originatingTransactionId, - timestamp: timestampPreciseFromDb(refreshGroupRecord.timestampCreated), - transactionId: constructTransactionIdentifier({ - tag: TransactionType.Refresh, - refreshGroupId: refreshGroupRecord.refreshGroupId, - }), - ...(ort?.lastError ? { error: ort.lastError } : {}), - }; -} - -function buildTransactionForDenomLoss(rec: DenomLossEventRecord): Transaction { - const txState = computeDenomLossTransactionStatus(rec); - return { - type: TransactionType.DenomLoss, - txState, - txActions: [TransactionAction.Delete], - amountRaw: Amounts.stringify(rec.amount), - amountEffective: Amounts.stringify(rec.amount), - timestamp: timestampPreciseFromDb(rec.timestampCreated), - transactionId: constructTransactionIdentifier({ - tag: TransactionType.DenomLoss, - denomLossEventId: rec.denomLossEventId, - }), - lossEventType: rec.eventType, - exchangeBaseUrl: rec.exchangeBaseUrl, - }; -} - -function buildTransactionForDeposit( - dg: DepositGroupRecord, - ort?: OperationRetryRecord, -): Transaction { - let deposited = true; - if (dg.statusPerCoin) { - for (const d of dg.statusPerCoin) { - if (d == DepositElementStatus.DepositPending) { - deposited = false; - } - } - } else { - deposited = false; - } - - const trackingState: DepositTransactionTrackingState[] = []; - - for (const ts of Object.values(dg.trackingState ?? {})) { - trackingState.push({ - amountRaw: ts.amountRaw, - timestampExecuted: timestampProtocolFromDb(ts.timestampExecuted), - wireFee: ts.wireFee, - wireTransferId: ts.wireTransferId, - }); - } - - let wireTransferProgress = 0; - if (dg.statusPerCoin) { - wireTransferProgress = - (100 * - dg.statusPerCoin.reduce( - (prev, cur) => prev + (cur === DepositElementStatus.Wired ? 1 : 0), - 0, - )) / - dg.statusPerCoin.length; - } - - const txState = computeDepositTransactionStatus(dg); - return { - type: TransactionType.Deposit, - txState, - txActions: computeDepositTransactionActions(dg), - amountRaw: Amounts.stringify(dg.counterpartyEffectiveDepositAmount), - amountEffective: isUnsuccessfulTransaction(txState) - ? Amounts.stringify(Amounts.zeroOfAmount(dg.totalPayCost)) - : Amounts.stringify(dg.totalPayCost), - timestamp: timestampPreciseFromDb(dg.timestampCreated), - targetPaytoUri: dg.wire.payto_uri, - wireTransferDeadline: timestampProtocolFromDb(dg.wireTransferDeadline), - transactionId: constructTransactionIdentifier({ - tag: TransactionType.Deposit, - depositGroupId: dg.depositGroupId, - }), - wireTransferProgress, - depositGroupId: dg.depositGroupId, - trackingState, - deposited, - ...(ort?.lastError ? { error: ort.lastError } : {}), - }; -} - -async function lookupMaybeContractData( - tx: WalletDbReadOnlyTransaction<["purchases", "contractTerms"]>, - proposalId: string, -): Promise<WalletContractData | undefined> { - let contractData: WalletContractData | undefined = undefined; - const purchaseTx = await tx.purchases.get(proposalId); - if (purchaseTx && purchaseTx.download) { - const download = purchaseTx.download; - const contractTermsRecord = await tx.contractTerms.get( - download.contractTermsHash, - ); - if (!contractTermsRecord) { - return; - } - contractData = extractContractData( - contractTermsRecord?.contractTermsRaw, - download.contractTermsHash, - download.contractTermsMerchantSig, - ); - } - - return contractData; -} - -function buildTransactionForPurchase( - purchaseRecord: PurchaseRecord, - contractData: WalletContractData, - refundsInfo: RefundGroupRecord[], - ort?: OperationRetryRecord, -): Transaction { - const zero = Amounts.zeroOfAmount(contractData.amount); - - const info: OrderShortInfo = { - merchant: { - name: contractData.merchant.name, - address: contractData.merchant.address, - email: contractData.merchant.email, - jurisdiction: contractData.merchant.jurisdiction, - website: contractData.merchant.website, - }, - orderId: contractData.orderId, - summary: contractData.summary, - summary_i18n: contractData.summaryI18n, - contractTermsHash: contractData.contractTermsHash, - }; - - if (contractData.fulfillmentUrl !== "") { - info.fulfillmentUrl = contractData.fulfillmentUrl; - } - - const refunds: RefundInfoShort[] = refundsInfo.map((r) => ({ - amountEffective: r.amountEffective, - amountRaw: r.amountRaw, - timestamp: TalerPreciseTimestamp.round( - timestampPreciseFromDb(r.timestampCreated), - ), - transactionId: constructTransactionIdentifier({ - tag: TransactionType.Refund, - refundGroupId: r.refundGroupId, - }), - })); - - const timestamp = purchaseRecord.timestampAccept; - checkDbInvariant( - !!timestamp, - `purchase ${purchaseRecord.orderId} without accepted time`, - ); - checkDbInvariant( - !!purchaseRecord.payInfo, - `purchase ${purchaseRecord.orderId} without payinfo`, - ); - - const txState = computePayMerchantTransactionState(purchaseRecord); - return { - type: TransactionType.Payment, - txState, - txActions: computePayMerchantTransactionActions(purchaseRecord), - amountRaw: Amounts.stringify(contractData.amount), - amountEffective: isUnsuccessfulTransaction(txState) - ? Amounts.stringify(zero) - : Amounts.stringify(purchaseRecord.payInfo.totalPayCost), - totalRefundRaw: Amounts.stringify(zero), // FIXME! - totalRefundEffective: Amounts.stringify(zero), // FIXME! - refundPending: - purchaseRecord.refundAmountAwaiting === undefined - ? undefined - : Amounts.stringify(purchaseRecord.refundAmountAwaiting), - refunds, - posConfirmation: purchaseRecord.posConfirmation, - timestamp: timestampPreciseFromDb(timestamp), - transactionId: constructTransactionIdentifier({ - tag: TransactionType.Payment, - proposalId: purchaseRecord.proposalId, - }), - proposalId: purchaseRecord.proposalId, - info, - refundQueryActive: - purchaseRecord.purchaseStatus === PurchaseStatus.PendingQueryingRefund, - ...(ort?.lastError ? { error: ort.lastError } : {}), - }; -} - -export async function getWithdrawalTransactionByUri( - wex: WalletExecutionContext, - request: WithdrawalTransactionByURIRequest, -): Promise<TransactionWithdrawal | undefined> { - return await wex.db.runReadWriteTx( - { - storeNames: [ - "withdrawalGroups", - "exchangeDetails", - "exchanges", - "operationRetries", - ], - }, - async (tx) => { - const withdrawalGroupRecord = - await tx.withdrawalGroups.indexes.byTalerWithdrawUri.get( - request.talerWithdrawUri, - ); - - if (!withdrawalGroupRecord) { - return undefined; - } - if (withdrawalGroupRecord.exchangeBaseUrl === undefined) { - // prepared and unconfirmed withdrawals are hidden - return undefined; - } - - const opId = TaskIdentifiers.forWithdrawal(withdrawalGroupRecord); - const ort = await tx.operationRetries.get(opId); - - const exchangeDetails = await getExchangeWireDetailsInTx( - tx, - withdrawalGroupRecord.exchangeBaseUrl, - ); - if (!exchangeDetails) throw Error("not exchange details"); - - if ( - withdrawalGroupRecord.wgInfo.withdrawalType === - WithdrawalRecordType.BankIntegrated - ) { - return buildTransactionForBankIntegratedWithdraw( - withdrawalGroupRecord, - exchangeDetails, - ort, - ); - } - - return buildTransactionForManualWithdraw( - withdrawalGroupRecord, - exchangeDetails, - ort, - ); - }, - ); -} - /** * Retrieve the full event history for this wallet. */ @@ -1164,404 +174,45 @@ export async function getTransactions( ): Promise<TransactionsResponse> { const transactions: Transaction[] = []; - const filter: TransactionRecordFilter = {}; - if (transactionsRequest?.filterByState) { - filter.onlyState = transactionsRequest.filterByState; - } - - await wex.db.runReadOnlyTx( - { - storeNames: [ - "coins", - "denominations", - "depositGroups", - "exchangeDetails", - "exchanges", - "operationRetries", - "peerPullDebit", - "peerPushDebit", - "peerPushCredit", - "peerPullCredit", - "planchets", - "purchases", - "contractTerms", - "recoupGroups", - "rewards", - "tombstones", - "withdrawalGroups", - "refreshGroups", - "refundGroups", - "denomLossEvents", - ], - }, - async (tx) => { - await iterRecordsForPeerPushDebit(tx, filter, async (pi) => { - const amount = Amounts.parseOrThrow(pi.amount); - const exchangesInTx = [pi.exchangeBaseUrl]; - if ( - shouldSkipCurrency( - transactionsRequest, - amount.currency, - exchangesInTx, - ) - ) { - return; - } - if (shouldSkipSearch(transactionsRequest, [])) { - return; - } - const ct = await tx.contractTerms.get(pi.contractTermsHash); - checkDbInvariant(!!ct, `no contract terms for p2p push ${pi.pursePub}`); - transactions.push( - buildTransactionForPushPaymentDebit(pi, ct.contractTermsRaw), - ); - }); - - await iterRecordsForPeerPullDebit(tx, filter, async (pi) => { - const amount = Amounts.parseOrThrow(pi.amount); - const exchangesInTx = [pi.exchangeBaseUrl]; - if ( - shouldSkipCurrency( - transactionsRequest, - amount.currency, - exchangesInTx, - ) - ) { - return; - } - if (shouldSkipSearch(transactionsRequest, [])) { - return; - } - if ( - pi.status !== PeerPullDebitRecordStatus.PendingDeposit && - pi.status !== PeerPullDebitRecordStatus.Done - ) { - // FIXME: Why?! - return; - } - - const contractTermsRec = await tx.contractTerms.get( - pi.contractTermsHash, - ); - if (!contractTermsRec) { - return; - } - - transactions.push( - buildTransactionForPullPaymentDebit( - pi, - contractTermsRec.contractTermsRaw, - ), - ); - }); - - await iterRecordsForPeerPushCredit(tx, filter, async (pi) => { - if (!pi.currency) { - // Legacy transaction - return; - } - const exchangesInTx = [pi.exchangeBaseUrl]; - if ( - shouldSkipCurrency(transactionsRequest, pi.currency, exchangesInTx) - ) { - return; - } - if (shouldSkipSearch(transactionsRequest, [])) { - return; - } - if (pi.status === PeerPushCreditStatus.DialogProposed) { - // We don't report proposed push credit transactions, user needs - // to scan URI again and confirm to see it. - return; - } - const ct = await tx.contractTerms.get(pi.contractTermsHash); - let wg: WithdrawalGroupRecord | undefined = undefined; - let wgOrt: OperationRetryRecord | undefined = undefined; - if (pi.withdrawalGroupId) { - wg = await tx.withdrawalGroups.get(pi.withdrawalGroupId); - if (wg) { - const withdrawalOpId = TaskIdentifiers.forWithdrawal(wg); - wgOrt = await tx.operationRetries.get(withdrawalOpId); - } - } - const pushIncOpId = TaskIdentifiers.forPeerPushCredit(pi); - const pushIncOrt = await tx.operationRetries.get(pushIncOpId); - - checkDbInvariant(!!ct, `no contract terms for p2p push ${pi.pursePub}`); - transactions.push( - buildTransactionForPeerPushCredit( - pi, - pushIncOrt, - ct.contractTermsRaw, - wg, - wgOrt, - ), - ); - }); - - await iterRecordsForPeerPullCredit(tx, filter, async (pi) => { - const currency = Amounts.currencyOf(pi.amount); - const exchangesInTx = [pi.exchangeBaseUrl]; - if (shouldSkipCurrency(transactionsRequest, currency, exchangesInTx)) { - return; - } - if (shouldSkipSearch(transactionsRequest, [])) { - return; - } - const ct = await tx.contractTerms.get(pi.contractTermsHash); - let wg: WithdrawalGroupRecord | undefined = undefined; - let wgOrt: OperationRetryRecord | undefined = undefined; - if (pi.withdrawalGroupId) { - wg = await tx.withdrawalGroups.get(pi.withdrawalGroupId); - if (wg) { - const withdrawalOpId = TaskIdentifiers.forWithdrawal(wg); - wgOrt = await tx.operationRetries.get(withdrawalOpId); - } - } - const pushIncOpId = TaskIdentifiers.forPeerPullPaymentInitiation(pi); - const pushIncOrt = await tx.operationRetries.get(pushIncOpId); - - checkDbInvariant(!!ct, `no contract terms for p2p push ${pi.pursePub}`); - transactions.push( - buildTransactionForPeerPullCredit( - pi, - pushIncOrt, - ct.contractTermsRaw, - wg, - wgOrt, - ), - ); - }); - - await iterRecordsForRefund(tx, filter, async (refundGroup) => { - const currency = Amounts.currencyOf(refundGroup.amountRaw); - - const exchangesInTx: string[] = []; - const p = await tx.purchases.get(refundGroup.proposalId); - if (!p || !p.payInfo || !p.payInfo.payCoinSelection) { - //refund with no payment - return; - } - - // FIXME: This is very slow, should become obsolete with materialized transactions. - for (const cp of p.payInfo.payCoinSelection.coinPubs) { - const c = await tx.coins.get(cp); - if (c?.exchangeBaseUrl) { - exchangesInTx.push(c.exchangeBaseUrl); - } - } - - if (shouldSkipCurrency(transactionsRequest, currency, exchangesInTx)) { - return; - } - const contractData = await lookupMaybeContractData( - tx, - refundGroup.proposalId, - ); - transactions.push(buildTransactionForRefund(refundGroup, contractData)); - }); - - await iterRecordsForRefresh(tx, filter, async (rg) => { - const exchangesInTx = rg.infoPerExchange - ? Object.keys(rg.infoPerExchange) - : []; - if ( - shouldSkipCurrency(transactionsRequest, rg.currency, exchangesInTx) - ) { - return; - } - let required = false; - const opId = TaskIdentifiers.forRefresh(rg); - if (transactionsRequest?.includeRefreshes) { - required = true; - } else if (rg.operationStatus !== RefreshOperationStatus.Finished) { - const ort = await tx.operationRetries.get(opId); - if (ort) { - required = true; - } - } - if (required) { - const ort = await tx.operationRetries.get(opId); - transactions.push(buildTransactionForRefresh(rg, ort)); - } - }); - - await iterRecordsForWithdrawal(tx, filter, async (wsr) => { - if ( - wsr.rawWithdrawalAmount === undefined || - wsr.exchangeBaseUrl == undefined - ) { - // skip prepared withdrawals which has not been confirmed - return; - } - const exchangesInTx = [wsr.exchangeBaseUrl]; - if ( - shouldSkipCurrency( - transactionsRequest, - Amounts.currencyOf(wsr.rawWithdrawalAmount), - exchangesInTx, - ) - ) { - return; - } - - if (shouldSkipSearch(transactionsRequest, [])) { - return; - } - - const opId = TaskIdentifiers.forWithdrawal(wsr); - const ort = await tx.operationRetries.get(opId); - - switch (wsr.wgInfo.withdrawalType) { - case WithdrawalRecordType.PeerPullCredit: - // Will be reported by the corresponding p2p transaction. - // FIXME: If this is an orphan withdrawal, still report it as a withdrawal! - // FIXME: Still report if requested with verbose option? - return; - case WithdrawalRecordType.PeerPushCredit: - // Will be reported by the corresponding p2p transaction. - // FIXME: If this is an orphan withdrawal, still report it as a withdrawal! - // FIXME: Still report if requested with verbose option? - return; - case WithdrawalRecordType.BankIntegrated: { - const exchangeDetails = await getExchangeWireDetailsInTx( - tx, - wsr.exchangeBaseUrl, - ); - if (!exchangeDetails) { - // FIXME: report somehow - return; - } - - transactions.push( - buildTransactionForBankIntegratedWithdraw( - wsr, - exchangeDetails, - ort, - ), - ); - return; - } - - case WithdrawalRecordType.BankManual: { - const exchangeDetails = await getExchangeWireDetailsInTx( - tx, - wsr.exchangeBaseUrl, - ); - if (!exchangeDetails) { - // FIXME: report somehow - return; - } - transactions.push( - buildTransactionForManualWithdraw(wsr, exchangeDetails, ort), - ); - return; - } - case WithdrawalRecordType.Recoup: - // FIXME: Do we also report a transaction here? - return; - } - }); - - await iterRecordsForDenomLoss(tx, filter, async (rec) => { - const amount = Amounts.parseOrThrow(rec.amount); - const exchangesInTx = [rec.exchangeBaseUrl]; - if ( - shouldSkipCurrency( - transactionsRequest, - amount.currency, - exchangesInTx, - ) - ) { - return; - } - transactions.push(buildTransactionForDenomLoss(rec)); - }); - - await iterRecordsForDeposit(tx, filter, async (dg) => { - const amount = Amounts.parseOrThrow(dg.amount); - const exchangesInTx = dg.infoPerExchange - ? Object.keys(dg.infoPerExchange) - : []; - if ( - shouldSkipCurrency( - transactionsRequest, - amount.currency, - exchangesInTx, - ) - ) { - return; - } - const opId = TaskIdentifiers.forDeposit(dg); - const retryRecord = await tx.operationRetries.get(opId); - - transactions.push(buildTransactionForDeposit(dg, retryRecord)); - }); - - await iterRecordsForPurchase(tx, filter, async (purchase) => { - const download = purchase.download; - if (!download) { - return; - } - if (!purchase.payInfo) { - return; - } - - const exchangesInTx: string[] = []; - for (const cp of purchase.payInfo.payCoinSelection?.coinPubs ?? []) { - const c = await tx.coins.get(cp); - if (c?.exchangeBaseUrl) { - exchangesInTx.push(c.exchangeBaseUrl); - } - } - - if ( - shouldSkipCurrency( - transactionsRequest, - download.currency, - exchangesInTx, - ) - ) { - return; - } - const contractTermsRecord = await tx.contractTerms.get( - download.contractTermsHash, - ); - if (!contractTermsRecord) { - return; - } - if ( - shouldSkipSearch(transactionsRequest, [ - contractTermsRecord?.contractTermsRaw?.summary || "", - ]) - ) { - return; - } + let keyRange: IDBKeyRange | undefined = undefined; - const contractData = extractContractData( - contractTermsRecord?.contractTermsRaw, - download.contractTermsHash, - download.contractTermsMerchantSig, - ); + if (transactionsRequest?.filterByState === "nonfinal") { + keyRange = GlobalIDB.KeyRange.bound( + OPERATION_STATUS_NONFINAL_FIRST, + OPERATION_STATUS_NONFINAL_LAST, + ); + } - const payOpId = TaskIdentifiers.forPay(purchase); - const payRetryRecord = await tx.operationRetries.get(payOpId); + await wex.db.runAllStoresReadOnlyTx({}, async (tx) => { + const allMetaTransactions = + await tx.transactionsMeta.indexes.byStatus.getAll(keyRange); + for (const metaTx of allMetaTransactions) { + if ( + shouldSkipCurrency( + transactionsRequest, + metaTx.currency, + metaTx.exchanges, + ) + ) { + continue; + } - const refunds = await tx.refundGroups.indexes.byProposalId.getAll( - purchase.proposalId, - ); + const parsedTx = parseTransactionIdentifier(metaTx.transactionId); + if ( + parsedTx?.tag === TransactionType.Refresh && + !transactionsRequest?.includeRefreshes + ) { + continue; + } - transactions.push( - buildTransactionForPurchase( - purchase, - contractData, - refunds, - payRetryRecord, - ), - ); - }); - }, - ); + const ctx = await getContextForTransaction(wex, metaTx.transactionId); + const txDetails = await ctx.lookupFullTransaction(tx); + if (!txDetails) { + continue; + } + transactions.push(txDetails); + } + }); // One-off checks, because of a bug where the wallet previously // did not migrate the DB correctly and caused these amounts @@ -1617,6 +268,73 @@ export async function getTransactions( return { transactions: [...txPending, ...txNotPending] }; } +/** + * Re-create materialized transactions from scratch. + * + * Used for migrations. + */ +export async function rematerializeTransactions( + wex: WalletExecutionContext, + tx: WalletDbAllStoresReadWriteTransaction, +): Promise<void> { + logger.info("re-materializing transactions"); + + const allTxMeta = await tx.transactionsMeta.getAll(); + for (const txMeta of allTxMeta) { + await tx.transactionsMeta.delete(txMeta.transactionId); + } + + await tx.peerPushDebit.iter().forEachAsync(async (x) => { + const ctx = new PeerPushDebitTransactionContext(wex, x.pursePub); + await ctx.updateTransactionMeta(tx); + }); + + await tx.peerPushCredit.iter().forEachAsync(async (x) => { + const ctx = new PeerPushCreditTransactionContext(wex, x.peerPushCreditId); + await ctx.updateTransactionMeta(tx); + }); + + await tx.peerPullCredit.iter().forEachAsync(async (x) => { + const ctx = new PeerPullCreditTransactionContext(wex, x.pursePub); + await ctx.updateTransactionMeta(tx); + }); + + await tx.peerPullDebit.iter().forEachAsync(async (x) => { + const ctx = new PeerPullDebitTransactionContext(wex, x.peerPullDebitId); + await ctx.updateTransactionMeta(tx); + }); + + await tx.refundGroups.iter().forEachAsync(async (x) => { + const ctx = new RefundTransactionContext(wex, x.refundGroupId); + await ctx.updateTransactionMeta(tx); + }); + + await tx.refreshGroups.iter().forEachAsync(async (x) => { + const ctx = new RefreshTransactionContext(wex, x.refreshGroupId); + await ctx.updateTransactionMeta(tx); + }); + + await tx.withdrawalGroups.iter().forEachAsync(async (x) => { + const ctx = new WithdrawTransactionContext(wex, x.withdrawalGroupId); + await ctx.updateTransactionMeta(tx); + }); + + await tx.denomLossEvents.iter().forEachAsync(async (x) => { + const ctx = new DenomLossTransactionContext(wex, x.denomLossEventId); + await ctx.updateTransactionMeta(tx); + }); + + await tx.depositGroups.iter().forEachAsync(async (x) => { + const ctx = new DepositTransactionContext(wex, x.depositGroupId); + await ctx.updateTransactionMeta(tx); + }); + + await tx.purchases.iter().forEachAsync(async (x) => { + const ctx = new PayMerchantTransactionContext(wex, x.proposalId); + await ctx.updateTransactionMeta(tx); + }); +} + export type ParsedTransactionIdentifier = | { tag: TransactionType.Deposit; depositGroupId: string } | { tag: TransactionType.Payment; proposalId: string } @@ -1882,7 +600,12 @@ export async function failTransaction( transactionId: string, ): Promise<void> { const ctx = await getContextForTransaction(wex, transactionId); - await ctx.failTransaction(); + await ctx.failTransaction( + makeTalerErrorDetail( + TalerErrorCode.WALLET_TRANSACTION_ABANDONED_BY_USER, + {}, + ), + ); } /** @@ -1915,7 +638,9 @@ export async function abortTransaction( transactionId: string, ): Promise<void> { const ctx = await getContextForTransaction(wex, transactionId); - await ctx.abortTransaction(); + await ctx.abortTransaction( + makeTalerErrorDetail(TalerErrorCode.WALLET_TRANSACTION_ABORTED_BY_USER, {}), + ); } export interface TransitionInfo { @@ -1946,189 +671,16 @@ export function notifyTransition( transactionId, experimentalUserData, }); - } -} - -/** - * Iterate refresh records based on a filter. - */ -async function iterRecordsForRefresh( - tx: WalletDbReadOnlyTransaction<["refreshGroups"]>, - filter: TransactionRecordFilter, - f: (r: RefreshGroupRecord) => Promise<void>, -): Promise<void> { - let refreshGroups: RefreshGroupRecord[]; - if (filter.onlyState === "nonfinal") { - const keyRange = GlobalIDB.KeyRange.bound( - RefreshOperationStatus.Pending, - RefreshOperationStatus.Suspended, - ); - refreshGroups = await tx.refreshGroups.indexes.byStatus.getAll(keyRange); - } else { - refreshGroups = await tx.refreshGroups.indexes.byStatus.getAll(); - } - - for (const r of refreshGroups) { - await f(r); - } -} - -async function iterRecordsForWithdrawal( - tx: WalletDbReadOnlyTransaction<["withdrawalGroups"]>, - filter: TransactionRecordFilter, - f: (r: WithdrawalGroupRecord) => Promise<void>, -): Promise<void> { - let withdrawalGroupRecords: WithdrawalGroupRecord[]; - if (filter.onlyState === "nonfinal") { - const keyRange = GlobalIDB.KeyRange.bound( - OPERATION_STATUS_NONFINAL_FIRST, - OPERATION_STATUS_NONFINAL_LAST, - ); - withdrawalGroupRecords = - await tx.withdrawalGroups.indexes.byStatus.getAll(keyRange); - } else { - withdrawalGroupRecords = - await tx.withdrawalGroups.indexes.byStatus.getAll(); - } - for (const wgr of withdrawalGroupRecords) { - await f(wgr); - } -} - -async function iterRecordsForDeposit( - tx: WalletDbReadOnlyTransaction<["depositGroups"]>, - filter: TransactionRecordFilter, - f: (r: DepositGroupRecord) => Promise<void>, -): Promise<void> { - let dgs: DepositGroupRecord[]; - if (filter.onlyState === "nonfinal") { - const keyRange = GlobalIDB.KeyRange.bound( - OPERATION_STATUS_NONFINAL_FIRST, - OPERATION_STATUS_NONFINAL_LAST, - ); - dgs = await tx.depositGroups.indexes.byStatus.getAll(keyRange); - } else { - dgs = await tx.depositGroups.indexes.byStatus.getAll(); - } - - for (const dg of dgs) { - await f(dg); - } -} - -async function iterRecordsForDenomLoss( - tx: WalletDbReadOnlyTransaction<["denomLossEvents"]>, - filter: TransactionRecordFilter, - f: (r: DenomLossEventRecord) => Promise<void>, -): Promise<void> { - let dgs: DenomLossEventRecord[]; - if (filter.onlyState === "nonfinal") { - const keyRange = GlobalIDB.KeyRange.bound( - OPERATION_STATUS_NONFINAL_FIRST, - OPERATION_STATUS_NONFINAL_LAST, - ); - dgs = await tx.denomLossEvents.indexes.byStatus.getAll(keyRange); - } else { - dgs = await tx.denomLossEvents.indexes.byStatus.getAll(); - } - - for (const dg of dgs) { - await f(dg); - } -} - -async function iterRecordsForRefund( - tx: WalletDbReadOnlyTransaction<["refundGroups"]>, - filter: TransactionRecordFilter, - f: (r: RefundGroupRecord) => Promise<void>, -): Promise<void> { - if (filter.onlyState === "nonfinal") { - const keyRange = GlobalIDB.KeyRange.bound( - OPERATION_STATUS_NONFINAL_FIRST, - OPERATION_STATUS_NONFINAL_LAST, - ); - await tx.refundGroups.indexes.byStatus.iter(keyRange).forEachAsync(f); - } else { - await tx.refundGroups.iter().forEachAsync(f); - } -} - -async function iterRecordsForPurchase( - tx: WalletDbReadOnlyTransaction<["purchases"]>, - filter: TransactionRecordFilter, - f: (r: PurchaseRecord) => Promise<void>, -): Promise<void> { - if (filter.onlyState === "nonfinal") { - const keyRange = GlobalIDB.KeyRange.bound( - OPERATION_STATUS_NONFINAL_FIRST, - OPERATION_STATUS_NONFINAL_LAST, - ); - await tx.purchases.indexes.byStatus.iter(keyRange).forEachAsync(f); - } else { - await tx.purchases.indexes.byStatus.iter().forEachAsync(f); - } -} - -async function iterRecordsForPeerPullCredit( - tx: WalletDbReadOnlyTransaction<["peerPullCredit"]>, - filter: TransactionRecordFilter, - f: (r: PeerPullCreditRecord) => Promise<void>, -): Promise<void> { - if (filter.onlyState === "nonfinal") { - const keyRange = GlobalIDB.KeyRange.bound( - OPERATION_STATUS_NONFINAL_FIRST, - OPERATION_STATUS_NONFINAL_LAST, - ); - await tx.peerPullCredit.indexes.byStatus.iter(keyRange).forEachAsync(f); - } else { - await tx.peerPullCredit.indexes.byStatus.iter().forEachAsync(f); - } -} - -async function iterRecordsForPeerPullDebit( - tx: WalletDbReadOnlyTransaction<["peerPullDebit"]>, - filter: TransactionRecordFilter, - f: (r: PeerPullPaymentIncomingRecord) => Promise<void>, -): Promise<void> { - if (filter.onlyState === "nonfinal") { - const keyRange = GlobalIDB.KeyRange.bound( - OPERATION_STATUS_NONFINAL_FIRST, - OPERATION_STATUS_NONFINAL_LAST, - ); - await tx.peerPullDebit.indexes.byStatus.iter(keyRange).forEachAsync(f); - } else { - await tx.peerPullDebit.indexes.byStatus.iter().forEachAsync(f); - } -} -async function iterRecordsForPeerPushDebit( - tx: WalletDbReadOnlyTransaction<["peerPushDebit"]>, - filter: TransactionRecordFilter, - f: (r: PeerPushDebitRecord) => Promise<void>, -): Promise<void> { - if (filter.onlyState === "nonfinal") { - const keyRange = GlobalIDB.KeyRange.bound( - OPERATION_STATUS_NONFINAL_FIRST, - OPERATION_STATUS_NONFINAL_LAST, - ); - await tx.peerPushDebit.indexes.byStatus.iter(keyRange).forEachAsync(f); - } else { - await tx.peerPushDebit.indexes.byStatus.iter().forEachAsync(f); - } -} - -async function iterRecordsForPeerPushCredit( - tx: WalletDbReadOnlyTransaction<["peerPushCredit"]>, - filter: TransactionRecordFilter, - f: (r: PeerPushPaymentIncomingRecord) => Promise<void>, -): Promise<void> { - if (filter.onlyState === "nonfinal") { - const keyRange = GlobalIDB.KeyRange.bound( - OPERATION_STATUS_NONFINAL_FIRST, - OPERATION_STATUS_NONFINAL_LAST, - ); - await tx.peerPushCredit.indexes.byStatus.iter(keyRange).forEachAsync(f); - } else { - await tx.peerPushCredit.indexes.byStatus.iter().forEachAsync(f); + // As a heuristic, we emit balance-change notifications + // whenever the major state changes. + // This sometimes emits more notifications than we need, + // but makes it much more unlikely that we miss any. + if (transitionInfo.newTxState.major !== transitionInfo.oldTxState.major) { + wex.ws.notify({ + type: NotificationType.BalanceChange, + hintTransactionId: transactionId, + }); + } } } |