taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit ba68d7f49503aa48f7f8e33cf22634de1a79097f
parent f80bfee758403381bc0b8549406fe32c105f8101
Author: Florian Dold <florian@dold.me>
Date:   Wed, 17 Jul 2024 15:45:34 +0200

wallet-core: refactor generation of transaction details

Diffstat:
Mpackages/taler-wallet-core/src/common.ts | 5+++++
Mpackages/taler-wallet-core/src/deposits.ts | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/exchanges.ts | 50+++++++++++++++++++++++++++++++++++++++++---------
Mpackages/taler-wallet-core/src/pay-merchant.ts | 168+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/pay-peer-pull-credit.ts | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/pay-peer-pull-debit.ts | 47+++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/pay-peer-push-credit.ts | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/pay-peer-push-debit.ts | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/recoup.ts | 41+++++++++++++++++++++++++++--------------
Mpackages/taler-wallet-core/src/refresh.ts | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/transactions.ts | 945++++++-------------------------------------------------------------------------
11 files changed, 767 insertions(+), 896 deletions(-)

diff --git a/packages/taler-wallet-core/src/common.ts b/packages/taler-wallet-core/src/common.ts @@ -38,6 +38,7 @@ import { TalerPreciseTimestamp, TalerProtocolTimestamp, TombstoneIdStr, + Transaction, TransactionIdStr, WalletNotification, assertUnreachable, @@ -61,6 +62,7 @@ import { PurchaseRecord, RecoupGroupRecord, RefreshGroupRecord, + WalletDbAllStoresReadOnlyTransaction, WalletDbReadWriteTransaction, WithdrawalGroupRecord, timestampPreciseToDb, @@ -795,6 +797,9 @@ export interface TransactionContext { resumeTransaction(): Promise<void>; failTransaction(): Promise<void>; deleteTransaction(): Promise<void>; + lookupFullTransaction( + tx: WalletDbAllStoresReadOnlyTransaction, + ): Promise<Transaction | undefined>; } declare const __taskIdStr: unique symbol; diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts @@ -31,6 +31,7 @@ import { CreateDepositGroupRequest, CreateDepositGroupResponse, DepositGroupFees, + DepositTransactionTrackingState, Duration, ExchangeBatchDepositRequest, ExchangeHandle, @@ -48,6 +49,7 @@ import { TalerPreciseTimestamp, TalerProtocolTimestamp, TrackTransaction, + Transaction, TransactionAction, TransactionIdStr, TransactionMajorState, @@ -91,7 +93,10 @@ import { DepositTrackingInfo, KycPendingInfo, RefreshOperationStatus, + WalletDbAllStoresReadOnlyTransaction, + timestampPreciseFromDb, timestampPreciseToDb, + timestampProtocolFromDb, timestampProtocolToDb, } from "./db.js"; import { getExchangeWireDetailsInTx } from "./exchanges.js"; @@ -107,6 +112,7 @@ import { } from "./refresh.js"; import { constructTransactionIdentifier, + isUnsuccessfulTransaction, notifyTransition, parseTransactionIdentifier, } from "./transactions.js"; @@ -135,6 +141,78 @@ export class DepositTransactionContext implements TransactionContext { }); } + /** + * Get the full transaction details for the transaction. + * + * Returns undefined if the transaction is in a state where we do not have a + * transaction item (e.g. if it was deleted). + */ + async lookupFullTransaction( + tx: WalletDbAllStoresReadOnlyTransaction, + ): Promise<Transaction | undefined> { + const dg = await tx.depositGroups.get(this.depositGroupId); + if (!dg) { + return undefined; + } + const ort = await tx.operationRetries.get(this.taskId); + + 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 deleteTransaction(): Promise<void> { const depositGroupId = this.depositGroupId; const ws = this.wex; diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts @@ -69,6 +69,8 @@ import { TalerPreciseTimestamp, TalerProtocolDuration, TalerProtocolTimestamp, + Transaction, + TransactionAction, TransactionIdStr, TransactionMajorState, TransactionState, @@ -125,6 +127,7 @@ import { ExchangeEntryDbRecordStatus, ExchangeEntryDbUpdateStatus, ExchangeEntryRecord, + WalletDbAllStoresReadOnlyTransaction, WalletDbHelpers, WalletDbReadOnlyTransaction, WalletDbReadWriteTransaction, @@ -2062,23 +2065,38 @@ export function computeDenomLossTransactionStatus( } export class DenomLossTransactionContext implements TransactionContext { + transactionId: TransactionIdStr; + + constructor( + private wex: WalletExecutionContext, + public denomLossEventId: string, + ) { + this.transactionId = constructTransactionIdentifier({ + tag: TransactionType.DenomLoss, + denomLossEventId, + }); + } + get taskId(): TaskIdStr | undefined { return undefined; } - transactionId: TransactionIdStr; abortTransaction(): Promise<void> { throw new Error("Method not implemented."); } + suspendTransaction(): Promise<void> { throw new Error("Method not implemented."); } + resumeTransaction(): Promise<void> { throw new Error("Method not implemented."); } + failTransaction(): Promise<void> { throw new Error("Method not implemented."); } + async deleteTransaction(): Promise<void> { const transitionInfo = await this.wex.db.runReadWriteTx( { storeNames: ["denomLossEvents"] }, @@ -2100,14 +2118,28 @@ export class DenomLossTransactionContext implements TransactionContext { notifyTransition(this.wex, this.transactionId, transitionInfo); } - constructor( - private wex: WalletExecutionContext, - public denomLossEventId: string, - ) { - this.transactionId = constructTransactionIdentifier({ - tag: TransactionType.DenomLoss, - denomLossEventId, - }); + async lookupFullTransaction( + tx: WalletDbAllStoresReadOnlyTransaction, + ): Promise<Transaction | undefined> { + const rec = await tx.denomLossEvents.get(this.denomLossEventId); + if (!rec) { + return undefined; + } + 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, + }; } } diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts @@ -63,6 +63,7 @@ import { MerchantPayResponse, MerchantUsingTemplateDetails, NotificationType, + OrderShortInfo, parsePayTemplateUri, parsePayUri, parseTalerUri, @@ -71,6 +72,8 @@ import { PreparePayTemplateRequest, randomBytes, RefreshReason, + RefundInfoShort, + RefundPaymentInfo, SelectedProspectiveCoin, SharePaymentResult, StartRefundQueryForUriResponse, @@ -84,6 +87,7 @@ import { TalerPreciseTimestamp, TalerProtocolViolationError, TalerUriAction, + Transaction, TransactionAction, TransactionIdStr, TransactionMajorState, @@ -111,6 +115,7 @@ import { constructTaskIdentifier, PendingTaskType, spendCoins, + TaskIdentifiers, TaskIdStr, TaskRunResult, TaskRunResultType, @@ -130,9 +135,11 @@ import { RefundItemRecord, RefundItemStatus, RefundReason, + timestampPreciseFromDb, timestampPreciseToDb, timestampProtocolFromDb, timestampProtocolToDb, + WalletDbAllStoresReadOnlyTransaction, WalletDbReadOnlyTransaction, WalletDbReadWriteTransaction, WalletStoresV1, @@ -145,6 +152,7 @@ import { } from "./refresh.js"; import { constructTransactionIdentifier, + isUnsuccessfulTransaction, notifyTransition, parseTransactionIdentifier, } from "./transactions.js"; @@ -177,6 +185,97 @@ export class PayMerchantTransactionContext implements TransactionContext { }); } + async lookupFullTransaction( + tx: WalletDbAllStoresReadOnlyTransaction, + ): Promise<Transaction | undefined> { + const proposalId = this.proposalId; + const purchaseRec = await tx.purchases.get(proposalId); + if (!purchaseRec) throw Error("not found"); + const download = await expectProposalDownloadInTx( + this.wex, + tx, + purchaseRec, + ); + const contractData = download.contractData; + const payOpId = TaskIdentifiers.forPay(purchaseRec); + const payRetryRec = await tx.operationRetries.get(payOpId); + + const refundsInfo = await tx.refundGroups.indexes.byProposalId.getAll( + purchaseRec.proposalId, + ); + + 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 = purchaseRec.timestampAccept; + checkDbInvariant( + !!timestamp, + `purchase ${purchaseRec.orderId} without accepted time`, + ); + checkDbInvariant( + !!purchaseRec.payInfo, + `purchase ${purchaseRec.orderId} without payinfo`, + ); + + const txState = computePayMerchantTransactionState(purchaseRec); + return { + type: TransactionType.Payment, + txState, + txActions: computePayMerchantTransactionActions(purchaseRec), + amountRaw: Amounts.stringify(contractData.amount), + amountEffective: isUnsuccessfulTransaction(txState) + ? Amounts.stringify(zero) + : Amounts.stringify(purchaseRec.payInfo.totalPayCost), + totalRefundRaw: Amounts.stringify(zero), // FIXME! + totalRefundEffective: Amounts.stringify(zero), // FIXME! + refundPending: + purchaseRec.refundAmountAwaiting === undefined + ? undefined + : Amounts.stringify(purchaseRec.refundAmountAwaiting), + refunds, + posConfirmation: purchaseRec.posConfirmation, + timestamp: timestampPreciseFromDb(timestamp), + transactionId: constructTransactionIdentifier({ + tag: TransactionType.Payment, + proposalId: purchaseRec.proposalId, + }), + proposalId: purchaseRec.proposalId, + info, + refundQueryActive: + purchaseRec.purchaseStatus === PurchaseStatus.PendingQueryingRefund, + ...(payRetryRec?.lastError ? { error: payRetryRec.lastError } : {}), + }; + } + /** * Transition a payment transition. */ @@ -420,6 +519,7 @@ export class PayMerchantTransactionContext implements TransactionContext { export class RefundTransactionContext implements TransactionContext { public transactionId: TransactionIdStr; public taskId: TaskIdStr | undefined = undefined; + constructor( public wex: WalletExecutionContext, public refundGroupId: string, @@ -430,6 +530,50 @@ export class RefundTransactionContext implements TransactionContext { }); } + async lookupFullTransaction( + tx: WalletDbAllStoresReadOnlyTransaction, + ): Promise<Transaction | undefined> { + const refundRecord = await tx.refundGroups.get(this.refundGroupId); + if (!refundRecord) { + throw Error("not found"); + } + const maybeContractData = await lookupMaybeContractData( + tx, + refundRecord?.proposalId, + ); + + 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, + }; + } + async deleteTransaction(): Promise<void> { const { wex, refundGroupId, transactionId } = this; await wex.db.runReadWriteTx( @@ -463,6 +607,30 @@ export class RefundTransactionContext implements TransactionContext { } } +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; +} + /** * Compute the total cost of a payment to the customer. * diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts @@ -31,6 +31,7 @@ import { TalerPreciseTimestamp, TalerProtocolTimestamp, TalerUriAction, + Transaction, TransactionAction, TransactionIdStr, TransactionMajorState, @@ -47,6 +48,7 @@ import { getRandomBytes, j2s, makeErrorDetail, + stringifyPayPullUri, stringifyTalerUri, talerPaytoFromExchangeReserve, } from "@gnu-taler/taler-util"; @@ -54,6 +56,7 @@ import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; import { PendingTaskType, TaskIdStr, + TaskIdentifiers, TaskRunResult, TaskRunResultType, TombstoneTag, @@ -65,8 +68,11 @@ import { import { KycPendingInfo, KycUserType, + OperationRetryRecord, PeerPullCreditRecord, PeerPullPaymentCreditStatus, + WalletDbAllStoresReadOnlyTransaction, + WithdrawalGroupRecord, WithdrawalGroupStatus, WithdrawalRecordType, timestampOptionalPreciseFromDb, @@ -80,6 +86,7 @@ import { } from "./pay-peer-common.js"; import { constructTransactionIdentifier, + isUnsuccessfulTransaction, notifyTransition, } from "./transactions.js"; import { WalletExecutionContext } from "./wallet.js"; @@ -109,6 +116,121 @@ export class PeerPullCreditTransactionContext implements TransactionContext { }); } + /** + * Get the full transaction details for the transaction. + * + * Returns undefined if the transaction is in a state where we do not have a + * transaction item (e.g. if it was deleted). + */ + async lookupFullTransaction( + tx: WalletDbAllStoresReadOnlyTransaction, + ): Promise<Transaction | undefined> { + const pullCredit = await tx.peerPullCredit.get(this.pursePub); + if (!pullCredit) { + return undefined; + } + const ct = await tx.contractTerms.get(pullCredit.contractTermsHash); + checkDbInvariant(!!ct, `no contract terms for p2p push ${this.pursePub}`); + + const peerContractTerms = ct.contractTermsRaw; + + let wsr: WithdrawalGroupRecord | undefined = undefined; + let wsrOrt: OperationRetryRecord | undefined = undefined; + if (pullCredit.withdrawalGroupId) { + wsr = await tx.withdrawalGroups.get(pullCredit.withdrawalGroupId); + if (wsr) { + const withdrawalOpId = TaskIdentifiers.forWithdrawal(wsr); + wsrOrt = await tx.operationRetries.get(withdrawalOpId); + } + } + const pullCreditOpId = + TaskIdentifiers.forPeerPullPaymentInitiation(pullCredit); + let pullCreditOrt = await tx.operationRetries.get(pullCreditOpId); + + 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 } : {}), + }; + } + async deleteTransaction(): Promise<void> { const { wex: ws, pursePub } = this; await ws.db.runReadWriteTx( diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts @@ -43,6 +43,7 @@ import { TalerErrorCode, TalerPreciseTimestamp, TalerProtocolViolationError, + Transaction, TransactionAction, TransactionIdStr, TransactionMajorState, @@ -50,6 +51,7 @@ import { TransactionState, TransactionType, assertUnreachable, + checkDbInvariant, checkLogicInvariant, codecForAny, codecForExchangeGetContractResponse, @@ -81,7 +83,9 @@ import { PeerPullDebitRecordStatus, PeerPullPaymentIncomingRecord, RefreshOperationStatus, + WalletDbAllStoresReadOnlyTransaction, WalletStoresV1, + timestampPreciseFromDb, timestampPreciseToDb, } from "./db.js"; import { @@ -93,6 +97,7 @@ import { DbReadWriteTransaction, StoreNames } from "./query.js"; import { createRefreshGroup } from "./refresh.js"; import { constructTransactionIdentifier, + isUnsuccessfulTransaction, notifyTransition, parseTransactionIdentifier, } from "./transactions.js"; @@ -122,6 +127,48 @@ export class PeerPullDebitTransactionContext implements TransactionContext { this.peerPullDebitId = peerPullDebitId; } + /** + * Get the full transaction details for the transaction. + * + * Returns undefined if the transaction is in a state where we do not have a + * transaction item (e.g. if it was deleted). + */ + async lookupFullTransaction( + tx: WalletDbAllStoresReadOnlyTransaction, + ): Promise<Transaction | undefined> { + const pi = await tx.peerPullDebit.get(this.peerPullDebitId); + if (!pi) { + return undefined; + } + const ort = await tx.operationRetries.get(this.taskId); + const txState = computePeerPullDebitTransactionState(pi); + const ctRec = await tx.contractTerms.get(pi.contractTermsHash); + checkDbInvariant(!!ctRec, `no contract terms for ${this.transactionId}`); + const contractTerms = ctRec.contractTermsRaw; + 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 } : {}), + }; + } + async deleteTransaction(): Promise<void> { const transactionId = this.transactionId; const ws = this.wex; diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts @@ -29,6 +29,7 @@ import { PreparePeerPushCreditResponse, TalerErrorCode, TalerPreciseTimestamp, + Transaction, TransactionAction, TransactionIdStr, TransactionMajorState, @@ -56,6 +57,7 @@ import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; import { PendingTaskType, TaskIdStr, + TaskIdentifiers, TaskRunResult, TaskRunResultType, TombstoneTag, @@ -66,8 +68,11 @@ import { import { KycPendingInfo, KycUserType, + OperationRetryRecord, PeerPushCreditStatus, PeerPushPaymentIncomingRecord, + WalletDbAllStoresReadOnlyTransaction, + WithdrawalGroupRecord, WithdrawalGroupStatus, WithdrawalRecordType, timestampPreciseFromDb, @@ -81,6 +86,7 @@ import { import { TransitionInfo, constructTransactionIdentifier, + isUnsuccessfulTransaction, notifyTransition, parseTransactionIdentifier, } from "./transactions.js"; @@ -113,6 +119,99 @@ export class PeerPushCreditTransactionContext implements TransactionContext { }); } + /** + * Get the full transaction details for the transaction. + * + * Returns undefined if the transaction is in a state where we do not have a + * transaction item (e.g. if it was deleted). + */ + async lookupFullTransaction( + tx: WalletDbAllStoresReadOnlyTransaction, + ): Promise<Transaction | undefined> { + const pushInc = await tx.peerPushCredit.get(this.peerPushCreditId); + if (!pushInc) { + return undefined; + } + + let wg: WithdrawalGroupRecord | undefined = undefined; + let wgRetryRecord: OperationRetryRecord | undefined = undefined; + if (pushInc.withdrawalGroupId) { + wg = await tx.withdrawalGroups.get(pushInc.withdrawalGroupId); + if (wg) { + const withdrawalOpId = TaskIdentifiers.forWithdrawal(wg); + wgRetryRecord = await tx.operationRetries.get(withdrawalOpId); + } + } + const pushIncOpId = TaskIdentifiers.forPeerPushCredit(pushInc); + const pushRetryRecord = await tx.operationRetries.get(pushIncOpId); + + const ct = await tx.contractTerms.get(pushInc.contractTermsHash); + + if (!ct) { + throw Error("contract terms for P2P payment not found"); + } + + const peerContractTerms = ct.contractTermsRaw; + + 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, + ...(wgRetryRecord?.lastError ? { error: wgRetryRecord.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, + }), + ...(pushRetryRecord?.lastError + ? { error: pushRetryRecord.lastError } + : {}), + }; + } + async deleteTransaction(): Promise<void> { const { wex, peerPushCreditId } = this; await wex.db.runReadWriteTx( diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts @@ -33,6 +33,7 @@ import { TalerPreciseTimestamp, TalerProtocolTimestamp, TalerProtocolViolationError, + Transaction, TransactionAction, TransactionIdStr, TransactionMajorState, @@ -45,6 +46,7 @@ import { encodeCrock, getRandomBytes, j2s, + stringifyPayPushUri, } from "@gnu-taler/taler-util"; import { HttpResponse, @@ -71,6 +73,8 @@ import { PeerPushDebitRecord, PeerPushDebitStatus, RefreshOperationStatus, + WalletDbAllStoresReadOnlyTransaction, + timestampPreciseFromDb, timestampPreciseToDb, timestampProtocolFromDb, timestampProtocolToDb, @@ -84,6 +88,7 @@ import { import { createRefreshGroup, waitRefreshFinal } from "./refresh.js"; import { constructTransactionIdentifier, + isUnsuccessfulTransaction, notifyTransition, } from "./transactions.js"; import { WalletExecutionContext } from "./wallet.js"; @@ -108,6 +113,59 @@ export class PeerPushDebitTransactionContext implements TransactionContext { }); } + /** + * Get the full transaction details for the transaction. + * + * Returns undefined if the transaction is in a state where we do not have a + * transaction item (e.g. if it was deleted). + */ + async lookupFullTransaction( + tx: WalletDbAllStoresReadOnlyTransaction, + ): Promise<Transaction | undefined> { + const pushDebitRec = await tx.peerPushDebit.get(this.pursePub); + if (!pushDebitRec) { + return undefined; + } + const retryRec = await tx.operationRetries.get(this.taskId); + + const ctRec = await tx.contractTerms.get(pushDebitRec.contractTermsHash); + checkDbInvariant(!!ctRec, `no contract terms for p2p push ${this.pursePub}`); + + const contractTerms = ctRec.contractTermsRaw; + + let talerUri: string | undefined = undefined; + switch (pushDebitRec.status) { + case PeerPushDebitStatus.PendingReady: + case PeerPushDebitStatus.SuspendedReady: + talerUri = stringifyPayPushUri({ + exchangeBaseUrl: pushDebitRec.exchangeBaseUrl, + contractPriv: pushDebitRec.contractPriv, + }); + } + const txState = computePeerPushDebitTransactionState(pushDebitRec); + return { + type: TransactionType.PeerPushDebit, + txState, + txActions: computePeerPushDebitTransactionActions(pushDebitRec), + amountEffective: isUnsuccessfulTransaction(txState) + ? Amounts.stringify(Amounts.zeroOfAmount(pushDebitRec.totalCost)) + : pushDebitRec.totalCost, + amountRaw: pushDebitRec.amount, + exchangeBaseUrl: pushDebitRec.exchangeBaseUrl, + info: { + expiration: contractTerms.purse_expiration, + summary: contractTerms.summary, + }, + timestamp: timestampPreciseFromDb(pushDebitRec.timestampCreated), + talerUri, + transactionId: constructTransactionIdentifier({ + tag: TransactionType.PeerPushDebit, + pursePub: pushDebitRec.pursePub, + }), + ...(retryRec?.lastError ? { error: retryRec.lastError } : {}), + }; + } + async deleteTransaction(): Promise<void> { const { wex, pursePub, transactionId } = this; await wex.db.runReadWriteTx( diff --git a/packages/taler-wallet-core/src/recoup.ts b/packages/taler-wallet-core/src/recoup.ts @@ -30,6 +30,7 @@ import { Logger, RefreshReason, TalerPreciseTimestamp, + Transaction, TransactionIdStr, TransactionType, URL, @@ -54,6 +55,7 @@ import { RecoupGroupRecord, RecoupOperationStatus, RefreshCoinSource, + WalletDbAllStoresReadOnlyTransaction, WalletDbReadWriteTransaction, WithdrawCoinSource, WithdrawalGroupStatus, @@ -432,36 +434,47 @@ export async function processRecoupGroup( } export class RecoupTransactionContext implements TransactionContext { + public transactionId: TransactionIdStr; + public taskId: TaskIdStr; + + constructor( + public wex: WalletExecutionContext, + private recoupGroupId: string, + ) { + this.transactionId = constructTransactionIdentifier({ + tag: TransactionType.Recoup, + recoupGroupId, + }); + this.taskId = constructTaskIdentifier({ + tag: PendingTaskType.Recoup, + recoupGroupId, + }); + } + abortTransaction(): Promise<void> { throw new Error("Method not implemented."); } + suspendTransaction(): Promise<void> { throw new Error("Method not implemented."); } + resumeTransaction(): Promise<void> { throw new Error("Method not implemented."); } + failTransaction(): Promise<void> { throw new Error("Method not implemented."); } + deleteTransaction(): Promise<void> { throw new Error("Method not implemented."); } - public transactionId: TransactionIdStr; - public taskId: TaskIdStr; - constructor( - public wex: WalletExecutionContext, - private recoupGroupId: string, - ) { - this.transactionId = constructTransactionIdentifier({ - tag: TransactionType.Recoup, - recoupGroupId, - }); - this.taskId = constructTaskIdentifier({ - tag: PendingTaskType.Recoup, - recoupGroupId, - }); + lookupFullTransaction( + tx: WalletDbAllStoresReadOnlyTransaction, + ): Promise<Transaction | undefined> { + throw new Error("Method not implemented."); } } diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts @@ -57,6 +57,7 @@ import { TalerErrorCode, TalerErrorDetail, TalerPreciseTimestamp, + Transaction, TransactionAction, TransactionIdStr, TransactionMajorState, @@ -101,7 +102,9 @@ import { RefreshGroupRecord, RefreshOperationStatus, RefreshSessionRecord, + timestampPreciseFromDb, timestampPreciseToDb, + WalletDbAllStoresReadOnlyTransaction, WalletDbReadOnlyTransaction, WalletDbReadWriteTransaction, WalletDbStoresArr, @@ -109,6 +112,7 @@ import { import { selectWithdrawalDenominations } from "./denomSelection.js"; import { constructTransactionIdentifier, + isUnsuccessfulTransaction, notifyTransition, TransitionInfo, } from "./transactions.js"; @@ -157,6 +161,52 @@ export class RefreshTransactionContext implements TransactionContext { } /** + * Get the full transaction details for the transaction. + * + * Returns undefined if the transaction is in a state where we do not have a + * transaction item (e.g. if it was deleted). + */ + async lookupFullTransaction( + tx: WalletDbAllStoresReadOnlyTransaction, + ): Promise<Transaction | undefined> { + const refreshGroupRecord = await tx.refreshGroups.get(this.refreshGroupId); + if (!refreshGroupRecord) { + return undefined; + } + const ort = await tx.operationRetries.get(this.taskId); + 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 } : {}), + }; + } + + /** * Transition a withdrawal transaction. * Extra object stores may be accessed during the transition. */ diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts @@ -22,22 +22,11 @@ import { AbsoluteTime, Amounts, assertUnreachable, - checkDbInvariant, - DepositTransactionTrackingState, j2s, Logger, NotificationType, - OrderShortInfo, - PeerContractTerms, - RefundInfoShort, - RefundPaymentInfo, ScopeType, - stringifyPayPullUri, - stringifyPayPushUri, - TalerErrorCode, - TalerPreciseTimestamp, Transaction, - TransactionAction, TransactionByIdRequest, TransactionIdStr, TransactionMajorState, @@ -47,9 +36,7 @@ import { TransactionState, TransactionType, TransactionWithdrawal, - WalletContractData, WithdrawalTransactionByURIRequest, - WithdrawalType, } from "@gnu-taler/taler-util"; import { constructTaskIdentifier, @@ -60,81 +47,36 @@ import { } 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, } from "./db.js"; +import { DepositTransactionContext } from "./deposits.js"; +import { DenomLossTransactionContext } from "./exchanges.js"; import { - computeDepositTransactionActions, - computeDepositTransactionStatus, - DepositTransactionContext, -} from "./deposits.js"; -import { - computeDenomLossTransactionStatus, - DenomLossTransactionContext, - ExchangeWireDetails, -} 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"); @@ -222,474 +164,27 @@ export async function getTransactionById( switch (parsedTx.tag) { case TransactionType.InternalWithdrawal: - case TransactionType.Withdrawal: { - const ctx = new WithdrawTransactionContext( - wex, - parsedTx.withdrawalGroupId, - ); - const res = await wex.db.runAllStoresReadOnlyTx({}, async (tx) => { - return await ctx.lookupFullTransaction(tx); - }); - if (!res) { - throw Error("not found"); - } - return res; - } - - 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); - }, + 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; } - 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, - ); - }, - ); - } - } -} - -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 } : {}), - }; } export function isUnsuccessfulTransaction(state: TransactionState): boolean { @@ -702,259 +197,6 @@ export function isUnsuccessfulTransaction(state: TransactionState): boolean { ); } -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, @@ -1005,11 +247,11 @@ export async function getTransactions( 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), - ); + const ctx = new PeerPushDebitTransactionContext(wex, pi.pursePub); + const txDetails = await ctx.lookupFullTransaction(tx); + if (txDetails) { + transactions.push(txDetails); + } }); await iterRecordsForPeerPullDebit(tx, filter, async (pi) => { @@ -1031,17 +273,11 @@ export async function getTransactions( return; } - const contractTermsRec = await tx.contractTerms.get(pi.contractTermsHash); - if (!contractTermsRec) { - return; + const ctx = new PeerPullDebitTransactionContext(wex, pi.peerPullDebitId); + const txDetails = await ctx.lookupFullTransaction(tx); + if (txDetails) { + transactions.push(txDetails); } - - transactions.push( - buildTransactionForPullPaymentDebit( - pi, - contractTermsRec.contractTermsRaw, - ), - ); }); await iterRecordsForPeerPushCredit(tx, filter, async (pi) => { @@ -1061,29 +297,15 @@ export async function getTransactions( // 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, - ), + + const ctx = new PeerPushCreditTransactionContext( + wex, + pi.peerPushCreditId, ); + const txDetails = await ctx.lookupFullTransaction(tx); + if (txDetails) { + transactions.push(txDetails); + } }); await iterRecordsForPeerPullCredit(tx, filter, async (pi) => { @@ -1095,29 +317,12 @@ export async function getTransactions( 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 ctx = new PeerPullCreditTransactionContext(wex, pi.pursePub); + const txDetails = await ctx.lookupFullTransaction(tx); + if (txDetails) { + transactions.push(txDetails); } - 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) => { @@ -1141,11 +346,12 @@ export async function getTransactions( if (shouldSkipCurrency(transactionsRequest, currency, exchangesInTx)) { return; } - const contractData = await lookupMaybeContractData( - tx, - refundGroup.proposalId, - ); - transactions.push(buildTransactionForRefund(refundGroup, contractData)); + + const ctx = new RefundTransactionContext(wex, refundGroup.refundGroupId); + const txDetails = await ctx.lookupFullTransaction(tx); + if (txDetails) { + transactions.push(txDetails); + } }); await iterRecordsForRefresh(tx, filter, async (rg) => { @@ -1166,8 +372,11 @@ export async function getTransactions( } } if (required) { - const ort = await tx.operationRetries.get(opId); - transactions.push(buildTransactionForRefresh(rg, ort)); + const ctx = new RefreshTransactionContext(wex, rg.refreshGroupId); + const txDetails = await ctx.lookupFullTransaction(tx); + if (txDetails) { + transactions.push(txDetails); + } } }); @@ -1232,7 +441,11 @@ export async function getTransactions( ) { return; } - transactions.push(buildTransactionForDenomLoss(rec)); + const ctx = new DenomLossTransactionContext(wex, rec.denomLossEventId); + const txDetails = await ctx.lookupFullTransaction(tx); + if (txDetails) { + transactions.push(txDetails); + } }); await iterRecordsForDeposit(tx, filter, async (dg) => { @@ -1245,10 +458,12 @@ export async function getTransactions( ) { return; } - const opId = TaskIdentifiers.forDeposit(dg); - const retryRecord = await tx.operationRetries.get(opId); - transactions.push(buildTransactionForDeposit(dg, retryRecord)); + const ctx = new DepositTransactionContext(wex, dg.depositGroupId); + const txDetails = await ctx.lookupFullTransaction(tx); + if (txDetails) { + transactions.push(txDetails); + } }); await iterRecordsForPurchase(tx, filter, async (purchase) => { @@ -1291,27 +506,11 @@ export async function getTransactions( return; } - const contractData = extractContractData( - contractTermsRecord?.contractTermsRaw, - download.contractTermsHash, - download.contractTermsMerchantSig, - ); - - const payOpId = TaskIdentifiers.forPay(purchase); - const payRetryRecord = await tx.operationRetries.get(payOpId); - - const refunds = await tx.refundGroups.indexes.byProposalId.getAll( - purchase.proposalId, - ); - - transactions.push( - buildTransactionForPurchase( - purchase, - contractData, - refunds, - payRetryRecord, - ), - ); + const ctx = new PayMerchantTransactionContext(wex, purchase.proposalId); + const txDetails = await ctx.lookupFullTransaction(tx); + if (txDetails) { + transactions.push(txDetails); + } }); });