taler-typescript-core

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

commit 17ca6918ed01006a296f99f03f6bc3db39398a87
parent 7e1468a1a07d3c5f5a441fb12ee16b7487c9b840
Author: Florian Dold <florian@dold.me>
Date:   Tue, 30 Jul 2024 20:44:42 +0200

wallet-core: update materialized transactions

Diffstat:
Mpackages/taler-util/src/http-client/exchange.ts | 2++
Mpackages/taler-wallet-core/src/common.ts | 1+
Mpackages/taler-wallet-core/src/deposits.ts | 154+++++++++++++++++++++++++++++++++++--------------------------------------------
Mpackages/taler-wallet-core/src/exchanges.ts | 104++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mpackages/taler-wallet-core/src/pay-merchant.ts | 237+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mpackages/taler-wallet-core/src/pay-peer-pull-credit.ts | 108++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Mpackages/taler-wallet-core/src/pay-peer-pull-debit.ts | 126++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mpackages/taler-wallet-core/src/pay-peer-push-credit.ts | 111+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mpackages/taler-wallet-core/src/pay-peer-push-debit.ts | 136+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mpackages/taler-wallet-core/src/recoup.ts | 86++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mpackages/taler-wallet-core/src/refresh.ts | 65+++++++++++++++++++++++++++++++++++++++++++----------------------
Mpackages/taler-wallet-core/src/withdraw.ts | 3---
12 files changed, 758 insertions(+), 375 deletions(-)

diff --git a/packages/taler-util/src/http-client/exchange.ts b/packages/taler-util/src/http-client/exchange.ts @@ -313,3 +313,5 @@ function buildDecisionSignature( officer_sig, }; } + + diff --git a/packages/taler-wallet-core/src/common.ts b/packages/taler-wallet-core/src/common.ts @@ -167,6 +167,7 @@ export async function spendCoins( "refreshGroups", "refreshSessions", "denominations", + "transactionsMeta", ] >, csi: CoinsSpendInfo, diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts @@ -57,7 +57,6 @@ import { TransactionState, TransactionType, URL, - WireFee, assertUnreachable, canonicalJson, checkDbInvariant, @@ -94,12 +93,13 @@ import { KycPendingInfo, RefreshOperationStatus, WalletDbAllStoresReadOnlyTransaction, + WalletDbReadWriteTransaction, timestampPreciseFromDb, timestampPreciseToDb, timestampProtocolFromDb, timestampProtocolToDb, } from "./db.js"; -import { getExchangeWireDetailsInTx } from "./exchanges.js"; +import { getExchangeWireDetailsInTx, getExchangeWireFee } from "./exchanges.js"; import { extractContractData, generateDepositPermissions, @@ -213,13 +213,33 @@ export class DepositTransactionContext implements TransactionContext { }; } + /** + * Update the metadata of the transaction in the database. + */ + async updateTransactionMeta( + tx: WalletDbReadWriteTransaction<["depositGroups", "transactionsMeta"]>, + ): Promise<void> { + const depositRec = await tx.depositGroups.get(this.depositGroupId); + if (!depositRec) { + await tx.transactionsMeta.delete(this.transactionId); + return; + } + await tx.transactionsMeta.put({ + transactionId: this.transactionId, + status: depositRec.operationStatus, + timestamp: depositRec.timestampCreated, + currency: depositRec.currency, + exchanges: Object.keys(depositRec.infoPerExchange ?? {}), + }); + } + async deleteTransaction(): Promise<void> { const depositGroupId = this.depositGroupId; const ws = this.wex; // FIXME: We should check first if we are in a final state // where deletion is allowed. await ws.db.runReadWriteTx( - { storeNames: ["depositGroups", "tombstones"] }, + { storeNames: ["depositGroups", "tombstones", "transactionsMeta"] }, async (tx) => { const tipRecord = await tx.depositGroups.get(depositGroupId); if (tipRecord) { @@ -227,6 +247,7 @@ export class DepositTransactionContext implements TransactionContext { await tx.tombstones.put({ id: TombstoneTag.DeleteDepositGroup + ":" + depositGroupId, }); + await this.updateTransactionMeta(tx); } }, ); @@ -236,7 +257,7 @@ export class DepositTransactionContext implements TransactionContext { async suspendTransaction(): Promise<void> { const { wex, depositGroupId, transactionId, taskId: retryTag } = this; const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["depositGroups"] }, + { storeNames: ["depositGroups", "transactionsMeta"] }, async (tx) => { const dg = await tx.depositGroups.get(depositGroupId); if (!dg) { @@ -266,6 +287,7 @@ export class DepositTransactionContext implements TransactionContext { } dg.operationStatus = newOpStatus; await tx.depositGroups.put(dg); + await this.updateTransactionMeta(tx); return { oldTxState: oldState, newTxState: computeDepositTransactionStatus(dg), @@ -279,7 +301,7 @@ export class DepositTransactionContext implements TransactionContext { async abortTransaction(): Promise<void> { const { wex, depositGroupId, transactionId, taskId: retryTag } = this; const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["depositGroups"] }, + { storeNames: ["depositGroups", "transactionsMeta"] }, async (tx) => { const dg = await tx.depositGroups.get(depositGroupId); if (!dg) { @@ -296,6 +318,7 @@ export class DepositTransactionContext implements TransactionContext { case DepositOperationStatus.SuspendedDeposit: { dg.operationStatus = DepositOperationStatus.Aborting; await tx.depositGroups.put(dg); + await this.updateTransactionMeta(tx); return { oldTxState: oldState, newTxState: computeDepositTransactionStatus(dg), @@ -317,7 +340,7 @@ export class DepositTransactionContext implements TransactionContext { async resumeTransaction(): Promise<void> { const { wex, depositGroupId, transactionId, taskId: retryTag } = this; const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["depositGroups"] }, + { storeNames: ["depositGroups", "transactionsMeta"] }, async (tx) => { const dg = await tx.depositGroups.get(depositGroupId); if (!dg) { @@ -347,6 +370,7 @@ export class DepositTransactionContext implements TransactionContext { } dg.operationStatus = newOpStatus; await tx.depositGroups.put(dg); + await this.updateTransactionMeta(tx); return { oldTxState: oldState, newTxState: computeDepositTransactionStatus(dg), @@ -360,7 +384,7 @@ export class DepositTransactionContext implements TransactionContext { async failTransaction(): Promise<void> { const { wex, depositGroupId, transactionId, taskId } = this; const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["depositGroups"] }, + { storeNames: ["depositGroups", "transactionsMeta"] }, async (tx) => { const dg = await tx.depositGroups.get(depositGroupId); if (!dg) { @@ -375,6 +399,7 @@ export class DepositTransactionContext implements TransactionContext { case DepositOperationStatus.Aborting: { dg.operationStatus = DepositOperationStatus.Failed; await tx.depositGroups.put(dg); + await this.updateTransactionMeta(tx); return { oldTxState: oldState, newTxState: computeDepositTransactionStatus(dg), @@ -594,6 +619,8 @@ async function refundDepositGroup( const currency = Amounts.currencyOf(depositGroup.totalPayCost); + const ctx = new DepositTransactionContext(wex, depositGroup.depositGroupId); + const res = await wex.db.runReadWriteTx( { storeNames: [ @@ -604,6 +631,7 @@ async function refundDepositGroup( "depositGroups", "refreshGroups", "refreshSessions", + "transactionsMeta", ], }, async (tx) => { @@ -635,6 +663,7 @@ async function refundDepositGroup( newDg.abortRefreshGroupId = refreshRes.refreshGroupId; } await tx.depositGroups.put(newDg); + await ctx.updateTransactionMeta(tx); return { refreshRes }; }, ); @@ -667,12 +696,9 @@ async function waitForRefreshOnDepositGroup( ): Promise<TaskRunResult> { const abortRefreshGroupId = depositGroup.abortRefreshGroupId; checkLogicInvariant(!!abortRefreshGroupId); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Deposit, - depositGroupId: depositGroup.depositGroupId, - }); + const ctx = new DepositTransactionContext(wex, depositGroup.depositGroupId); const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["depositGroups", "refreshGroups"] }, + { storeNames: ["depositGroups", "refreshGroups", "transactionsMeta"] }, async (tx) => { const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId); let newOpState: DepositOperationStatus | undefined; @@ -699,16 +725,17 @@ async function waitForRefreshOnDepositGroup( newDg.operationStatus = newOpState; const newTxState = computeDepositTransactionStatus(newDg); await tx.depositGroups.put(newDg); + await ctx.updateTransactionMeta(tx); return { oldTxState, newTxState }; } return undefined; }, ); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); wex.ws.notify({ type: NotificationType.BalanceChange, - hintTransactionId: transactionId, + hintTransactionId: ctx.transactionId, }); return TaskRunResult.backoff(); } @@ -762,6 +789,8 @@ async function processDepositGroupPendingKyc( }, ); + const ctx = new DepositTransactionContext(wex, depositGroupId); + if ( kycStatusRes.status === HttpStatusCode.Ok || //FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge @@ -769,7 +798,7 @@ async function processDepositGroupPendingKyc( kycStatusRes.status === HttpStatusCode.NoContent ) { const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["depositGroups"] }, + { storeNames: ["depositGroups", "transactionsMeta"] }, async (tx) => { const newDg = await tx.depositGroups.get(depositGroupId); if (!newDg) { @@ -782,6 +811,7 @@ async function processDepositGroupPendingKyc( newDg.operationStatus = DepositOperationStatus.PendingTrack; const newTxState = computeDepositTransactionStatus(newDg); await tx.depositGroups.put(newDg); + await ctx.updateTransactionMeta(tx); return { oldTxState, newTxState }; }, ); @@ -808,10 +838,7 @@ async function transitionToKycRequired( const { depositGroupId } = depositGroup; const userType = "individual"; - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Deposit, - depositGroupId, - }); + const ctx = new DepositTransactionContext(wex, depositGroupId); const url = new URL( `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`, @@ -828,7 +855,7 @@ async function transitionToKycRequired( const kycStatus = await kycStatusReq.json(); logger.info(`kyc status: ${j2s(kycStatus)}`); const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["depositGroups"] }, + { storeNames: ["depositGroups", "transactionsMeta"] }, async (tx) => { const dg = await tx.depositGroups.get(depositGroupId); if (!dg) { @@ -845,11 +872,12 @@ async function transitionToKycRequired( requirementRow: kycInfo.requirementRow, }; await tx.depositGroups.put(dg); + await ctx.updateTransactionMeta(tx); const newTxState = computeDepositTransactionStatus(dg); return { oldTxState, newTxState }; }, ); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); return TaskRunResult.finished(); } else { throw Error(`unexpected response from kyc-check (${kycStatusReq.status})`); @@ -873,6 +901,7 @@ async function processDepositGroupPendingTrack( ); } const { depositGroupId } = depositGroup; + const ctx = new DepositTransactionContext(wex, depositGroupId); for (let i = 0; i < statusPerCoin.length; i++) { const coinPub = payCoinSelection.coinPubs[i]; // FIXME: Make the URL part of the coin selection? @@ -954,7 +983,7 @@ async function processDepositGroupPendingTrack( if (updatedTxStatus !== undefined) { await wex.db.runReadWriteTx( - { storeNames: ["depositGroups"] }, + { storeNames: ["depositGroups", "transactionsMeta"] }, async (tx) => { const dg = await tx.depositGroups.get(depositGroupId); if (!dg) { @@ -982,6 +1011,7 @@ async function processDepositGroupPendingTrack( dg.trackingState[newWiredCoin.id] = newWiredCoin.value; } await tx.depositGroups.put(dg); + await ctx.updateTransactionMeta(tx); }, ); } @@ -990,7 +1020,7 @@ async function processDepositGroupPendingTrack( let allWired = true; const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["depositGroups"] }, + { storeNames: ["depositGroups", "transactionsMeta"] }, async (tx) => { const dg = await tx.depositGroups.get(depositGroupId); if (!dg) { @@ -1012,20 +1042,17 @@ async function processDepositGroupPendingTrack( ); dg.operationStatus = DepositOperationStatus.Finished; await tx.depositGroups.put(dg); + await ctx.updateTransactionMeta(tx); } const newTxState = computeDepositTransactionStatus(dg); return { oldTxState, newTxState }; }, ); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Deposit, - depositGroupId, - }); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); if (allWired) { wex.ws.notify({ type: NotificationType.BalanceChange, - hintTransactionId: transactionId, + hintTransactionId: ctx.transactionId, }); return TaskRunResult.finished(); } else { @@ -1057,10 +1084,7 @@ async function processDepositGroupPendingDeposit( "", ); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Deposit, - depositGroupId, - }); + const ctx = new DepositTransactionContext(wex, depositGroupId); // Check for cancellation before expensive operations. cancellationToken?.throwIfCancelled(); @@ -1081,6 +1105,7 @@ async function processDepositGroupPendingDeposit( "refreshGroups", "refreshSessions", "denominations", + "transactionsMeta", ], }, async (tx) => { @@ -1141,8 +1166,9 @@ async function processDepositGroupPendingDeposit( () => DepositElementStatus.DepositPending, ); await tx.depositGroups.put(dg); + await ctx.updateTransactionMeta(tx); await spendCoins(wex, tx, { - transactionId, + transactionId: ctx.transactionId, coinPubs: dg.payCoinSelection.coinPubs, contributions: dg.payCoinSelection.coinContributions.map((x) => Amounts.parseOrThrow(x), @@ -1222,7 +1248,7 @@ async function processDepositGroupPendingDeposit( ); await wex.db.runReadWriteTx( - { storeNames: ["depositGroups"] }, + { storeNames: ["depositGroups", "transactionsMeta"] }, async (tx) => { const dg = await tx.depositGroups.get(depositGroupId); if (!dg) { @@ -1239,12 +1265,13 @@ async function processDepositGroupPendingDeposit( await tx.depositGroups.put(dg); } } + await ctx.updateTransactionMeta(tx); }, ); } const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["depositGroups"] }, + { storeNames: ["depositGroups", "transactionsMeta"] }, async (tx) => { const dg = await tx.depositGroups.get(depositGroupId); if (!dg) { @@ -1253,12 +1280,13 @@ async function processDepositGroupPendingDeposit( const oldTxState = computeDepositTransactionStatus(dg); dg.operationStatus = DepositOperationStatus.PendingTrack; await tx.depositGroups.put(dg); + await ctx.updateTransactionMeta(tx); const newTxState = computeDepositTransactionStatus(dg); return { oldTxState, newTxState }; }, ); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); return TaskRunResult.progress(); } @@ -1298,54 +1326,6 @@ export async function processDepositGroup( return TaskRunResult.finished(); } -/** - * FIXME: Consider moving this to exchanges.ts. - */ -async function getExchangeWireFee( - wex: WalletExecutionContext, - wireType: string, - baseUrl: string, - time: TalerProtocolTimestamp, -): Promise<WireFee> { - const exchangeDetails = await wex.db.runReadOnlyTx( - { storeNames: ["exchangeDetails", "exchanges"] }, - async (tx) => { - const ex = await tx.exchanges.get(baseUrl); - if (!ex || !ex.detailsPointer) return undefined; - return await tx.exchangeDetails.indexes.byPointer.get([ - baseUrl, - ex.detailsPointer.currency, - ex.detailsPointer.masterPublicKey, - ]); - }, - ); - - if (!exchangeDetails) { - throw Error(`exchange missing: ${baseUrl}`); - } - - const fees = exchangeDetails.wireInfo.feesForType[wireType]; - if (!fees || fees.length === 0) { - throw Error( - `exchange ${baseUrl} doesn't have fees for wire type ${wireType}`, - ); - } - const fee = fees.find((x) => { - return AbsoluteTime.isBetween( - AbsoluteTime.fromProtocolTimestamp(time), - AbsoluteTime.fromProtocolTimestamp(x.startStamp), - AbsoluteTime.fromProtocolTimestamp(x.endStamp), - ); - }); - if (!fee) { - throw Error( - `exchange ${exchangeDetails.exchangeBaseUrl} doesn't have fees for wire type ${wireType} at ${time.t_s}`, - ); - } - - return fee; -} - async function trackDeposit( wex: WalletExecutionContext, depositGroup: DepositGroupRecord, @@ -1742,6 +1722,7 @@ export async function createDepositGroup( "recoupGroups", "refreshGroups", "refreshSessions", + "transactionsMeta", ], }, async (tx) => { @@ -1760,6 +1741,7 @@ export async function createDepositGroup( contractTermsRaw: contractTerms, h: contractTermsHash, }); + await ctx.updateTransactionMeta(tx); return computeDepositTransactionStatus(depositGroup); }, ); @@ -1790,7 +1772,7 @@ export async function createDepositGroup( * Get the amount that will be deposited on the users bank * account after depositing, not considering aggregation. */ -export async function getCounterpartyEffectiveDepositAmount( +async function getCounterpartyEffectiveDepositAmount( wex: WalletExecutionContext, wireType: string, pcs: SelectedProspectiveCoin[], diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts @@ -1537,6 +1537,7 @@ export async function updateExchangeFromUrlHandler( "coinAvailability", "denomLossEvents", "currencyInfo", + "transactionsMeta", ], }, async (tx) => { @@ -1804,6 +1805,7 @@ async function doAutoRefresh( "exchanges", "refreshGroups", "refreshSessions", + "transactionsMeta", ], }, async (tx) => { @@ -1873,7 +1875,13 @@ interface DenomLossResult { async function handleDenomLoss( wex: WalletExecutionContext, tx: WalletDbReadWriteTransaction< - ["coinAvailability", "denominations", "denomLossEvents", "coins"] + [ + "coinAvailability", + "denominations", + "denomLossEvents", + "coins", + "transactionsMeta", + ] >, currency: string, exchangeBaseUrl: string, @@ -1964,13 +1972,11 @@ async function handleDenomLoss( status: DenomLossStatus.Done, timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()), }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.DenomLoss, - denomLossEventId, - }); + const ctx = new DenomLossTransactionContext(wex, denomLossEventId); + await ctx.updateTransactionMeta(tx); result.notifications.push({ type: NotificationType.TransactionStateTransition, - transactionId, + transactionId: ctx.transactionId, oldTxState: { major: TransactionMajorState.None, }, @@ -1980,7 +1986,7 @@ async function handleDenomLoss( }); result.notifications.push({ type: NotificationType.BalanceChange, - hintTransactionId: transactionId, + hintTransactionId: ctx.transactionId, }); } @@ -1996,13 +2002,11 @@ async function handleDenomLoss( status: DenomLossStatus.Done, timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()), }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.DenomLoss, - denomLossEventId, - }); + const ctx = new DenomLossTransactionContext(wex, denomLossEventId); + await ctx.updateTransactionMeta(tx); result.notifications.push({ type: NotificationType.TransactionStateTransition, - transactionId, + transactionId: ctx.transactionId, oldTxState: { major: TransactionMajorState.None, }, @@ -2012,7 +2016,7 @@ async function handleDenomLoss( }); result.notifications.push({ type: NotificationType.BalanceChange, - hintTransactionId: transactionId, + hintTransactionId: ctx.transactionId, }); } @@ -2083,6 +2087,23 @@ export class DenomLossTransactionContext implements TransactionContext { return undefined; } + async updateTransactionMeta( + tx: WalletDbReadWriteTransaction<["denomLossEvents", "transactionsMeta"]>, + ): Promise<void> { + const denomLossRec = await tx.denomLossEvents.get(this.denomLossEventId); + if (!denomLossRec) { + await tx.transactionsMeta.delete(this.transactionId); + return; + } + await tx.transactionsMeta.put({ + transactionId: this.transactionId, + status: denomLossRec.status, + timestamp: denomLossRec.timestampCreated, + currency: denomLossRec.currency, + exchanges: [denomLossRec.exchangeBaseUrl], + }); + } + abortTransaction(): Promise<void> { throw new Error("Method not implemented."); } @@ -2148,7 +2169,14 @@ export class DenomLossTransactionContext implements TransactionContext { async function handleRecoup( wex: WalletExecutionContext, tx: WalletDbReadWriteTransaction< - ["denominations", "coins", "recoupGroups", "refreshGroups"] + [ + "denominations", + "coins", + "recoupGroups", + "refreshGroups", + "transactionsMeta", + "exchanges", + ] >, exchangeBaseUrl: string, recoup: Recoup[], @@ -2740,3 +2768,51 @@ export async function getExchangeResources( } return res; } + +/** + * Find the currently applicable wire fee for an exchange. + */ +export async function getExchangeWireFee( + wex: WalletExecutionContext, + wireType: string, + baseUrl: string, + time: TalerProtocolTimestamp, +): Promise<WireFee> { + const exchangeDetails = await wex.db.runReadOnlyTx( + { storeNames: ["exchangeDetails", "exchanges"] }, + async (tx) => { + const ex = await tx.exchanges.get(baseUrl); + if (!ex || !ex.detailsPointer) return undefined; + return await tx.exchangeDetails.indexes.byPointer.get([ + baseUrl, + ex.detailsPointer.currency, + ex.detailsPointer.masterPublicKey, + ]); + }, + ); + + if (!exchangeDetails) { + throw Error(`exchange missing: ${baseUrl}`); + } + + const fees = exchangeDetails.wireInfo.feesForType[wireType]; + if (!fees || fees.length === 0) { + throw Error( + `exchange ${baseUrl} doesn't have fees for wire type ${wireType}`, + ); + } + const fee = fees.find((x) => { + return AbsoluteTime.isBetween( + AbsoluteTime.fromProtocolTimestamp(time), + AbsoluteTime.fromProtocolTimestamp(x.startStamp), + AbsoluteTime.fromProtocolTimestamp(x.endStamp), + ); + }); + if (!fee) { + throw Error( + `exchange ${exchangeDetails.exchangeBaseUrl} doesn't have fees for wire type ${wireType} at ${time.t_s}`, + ); + } + + return fee; +} diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts @@ -185,6 +185,34 @@ export class PayMerchantTransactionContext implements TransactionContext { }); } + /** + * Function that updates the metadata of the transaction. + * + * Must be called each time the DB record for the transaction is updated. + */ + async updateTransactionMeta( + tx: WalletDbReadWriteTransaction<["purchases", "transactionsMeta"]>, + ): Promise<void> { + const purchaseRec = await tx.purchases.get(this.proposalId); + if (!purchaseRec) { + await tx.transactionsMeta.delete(this.transactionId); + return; + } + if (!purchaseRec.download) { + // Transaction is not reportable yet + await tx.transactionsMeta.delete(this.transactionId); + return; + } + await tx.transactionsMeta.put({ + transactionId: this.transactionId, + status: purchaseRec.purchaseStatus, + timestamp: purchaseRec.timestamp, + currency: purchaseRec.download?.currency, + // FIXME! + exchanges: [], + }); + } + async lookupFullTransaction( tx: WalletDbAllStoresReadOnlyTransaction, ): Promise<Transaction | undefined> { @@ -302,14 +330,14 @@ export class PayMerchantTransactionContext implements TransactionContext { rec: PurchaseRecord, tx: DbReadWriteTransaction< typeof WalletStoresV1, - ["purchases", ...StoreNameArray] + ["purchases", "transactionsMeta", ...StoreNameArray] >, ) => Promise<TransitionResultType>, ): Promise<void> { const ws = this.wex; const extraStores = opts.extraStores ?? []; const transitionInfo = await ws.db.runReadWriteTx( - { storeNames: ["purchases", ...extraStores] }, + { storeNames: ["purchases", "transactionsMeta", ...extraStores] }, async (tx) => { const purchaseRec = await tx.purchases.get(this.proposalId); if (!purchaseRec) { @@ -320,12 +348,22 @@ export class PayMerchantTransactionContext implements TransactionContext { switch (res) { case TransitionResultType.Transition: { await tx.purchases.put(purchaseRec); + await this.updateTransactionMeta(tx); const newTxState = computePayMerchantTransactionState(purchaseRec); return { oldTxState, newTxState, }; } + case TransitionResultType.Delete: + await tx.purchases.delete(this.proposalId); + await this.updateTransactionMeta(tx); + return { + oldTxState, + newTxState: { + major: TransactionMajorState.None, + }, + }; default: return undefined; } @@ -337,13 +375,14 @@ export class PayMerchantTransactionContext implements TransactionContext { async deleteTransaction(): Promise<void> { const { wex: ws, proposalId } = this; await ws.db.runReadWriteTx( - { storeNames: ["purchases", "tombstones"] }, + { storeNames: ["purchases", "tombstones", "transactionsMeta"] }, async (tx) => { let found = false; const purchase = await tx.purchases.get(proposalId); if (purchase) { found = true; await tx.purchases.delete(proposalId); + await this.updateTransactionMeta(tx); } if (found) { await tx.tombstones.put({ @@ -358,7 +397,7 @@ export class PayMerchantTransactionContext implements TransactionContext { const { wex, proposalId, transactionId } = this; wex.taskScheduler.stopShepherdTask(this.taskId); const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["purchases"] }, + { storeNames: ["purchases", "transactionsMeta"] }, async (tx) => { const purchase = await tx.purchases.get(proposalId); if (!purchase) { @@ -370,6 +409,7 @@ export class PayMerchantTransactionContext implements TransactionContext { return undefined; } await tx.purchases.put(purchase); + await this.updateTransactionMeta(tx); const newTxState = computePayMerchantTransactionState(purchase); return { oldTxState, newTxState }; }, @@ -390,6 +430,7 @@ export class PayMerchantTransactionContext implements TransactionContext { "purchases", "refreshGroups", "refreshSessions", + "transactionsMeta", ], }, async (tx) => { @@ -446,6 +487,7 @@ export class PayMerchantTransactionContext implements TransactionContext { return; } await tx.purchases.put(purchase); + await this.updateTransactionMeta(tx); const newTxState = computePayMerchantTransactionState(purchase); return { oldTxState, newTxState }; }, @@ -458,7 +500,7 @@ export class PayMerchantTransactionContext implements TransactionContext { async resumeTransaction(): Promise<void> { const { wex, proposalId, transactionId, taskId: retryTag } = this; const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["purchases"] }, + { storeNames: ["purchases", "transactionsMeta"] }, async (tx) => { const purchase = await tx.purchases.get(proposalId); if (!purchase) { @@ -470,6 +512,7 @@ export class PayMerchantTransactionContext implements TransactionContext { return undefined; } await tx.purchases.put(purchase); + await this.updateTransactionMeta(tx); const newTxState = computePayMerchantTransactionState(purchase); return { oldTxState, newTxState }; }, @@ -489,6 +532,7 @@ export class PayMerchantTransactionContext implements TransactionContext { "coinAvailability", "coins", "operationRetries", + "transactionsMeta", ], }, async (tx) => { @@ -507,6 +551,7 @@ export class PayMerchantTransactionContext implements TransactionContext { purchase.purchaseStatus = newState; await tx.purchases.put(purchase); } + await this.updateTransactionMeta(tx); const newTxState = computePayMerchantTransactionState(purchase); return { oldTxState, newTxState }; }, @@ -530,6 +575,29 @@ export class RefundTransactionContext implements TransactionContext { }); } + /** + * Function that updates the metadata of the transaction. + * + * Must be called each time the DB record for the transaction is updated. + */ + async updateTransactionMeta( + tx: WalletDbReadWriteTransaction<["refundGroups", "transactionsMeta"]>, + ): Promise<void> { + const refundRec = await tx.refundGroups.get(this.refundGroupId); + if (!refundRec) { + await tx.transactionsMeta.delete(this.transactionId); + return; + } + await tx.transactionsMeta.put({ + transactionId: this.transactionId, + status: refundRec.status, + timestamp: refundRec.timestampCreated, + currency: Amounts.currencyOf(refundRec.amountEffective), + // FIXME! + exchanges: [], + }); + } + async lookupFullTransaction( tx: WalletDbAllStoresReadOnlyTransaction, ): Promise<Transaction | undefined> { @@ -577,13 +645,14 @@ export class RefundTransactionContext implements TransactionContext { async deleteTransaction(): Promise<void> { const { wex, refundGroupId, transactionId } = this; await wex.db.runReadWriteTx( - { storeNames: ["refundGroups", "tombstones"] }, + { storeNames: ["refundGroups", "tombstones", "transactionsMeta"] }, async (tx) => { const refundRecord = await tx.refundGroups.get(refundGroupId); if (!refundRecord) { return; } await tx.refundGroups.delete(refundGroupId); + await this.updateTransactionMeta(tx); await tx.tombstones.put({ id: transactionId }); // FIXME: Also tombstone the refund items, so that they won't reappear. }, @@ -687,12 +756,9 @@ async function failProposalPermanently( proposalId: string, err: TalerErrorDetail, ): Promise<void> { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Payment, - proposalId, - }); + const ctx = new PayMerchantTransactionContext(wex, proposalId); const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["purchases"] }, + { storeNames: ["purchases", "transactionsMeta"] }, async (tx) => { const p = await tx.purchases.get(proposalId); if (!p) { @@ -703,10 +769,11 @@ async function failProposalPermanently( p.purchaseStatus = PurchaseStatus.FailedClaim; const newTxState = computePayMerchantTransactionState(p); await tx.purchases.put(p); + await ctx.updateTransactionMeta(tx); return { oldTxState, newTxState }; }, ); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); } function getPayRequestTimeout(purchase: PurchaseRecord): Duration { @@ -973,7 +1040,7 @@ async function processDownloadProposal( logger.trace(`extracted contract data: ${j2s(contractData)}`); const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["purchases", "contractTerms"] }, + { storeNames: ["purchases", "contractTerms", "transactionsMeta"] }, async (tx) => { const p = await tx.purchases.get(proposalId); if (!p) { @@ -1020,6 +1087,7 @@ async function processDownloadProposal( : PurchaseStatus.DialogProposed; await tx.purchases.put(p); } + await ctx.updateTransactionMeta(tx); const newTxState = computePayMerchantTransactionState(p); return { oldTxState, @@ -1087,8 +1155,12 @@ async function createOrReusePurchase( if (paid) { // if this transaction was shared and the order is paid then it // means that another wallet already paid the proposal + const ctx = new PayMerchantTransactionContext( + wex, + oldProposal.proposalId, + ); const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["purchases"] }, + { storeNames: ["purchases", "transactionsMeta"] }, async (tx) => { const p = await tx.purchases.get(oldProposal.proposalId); if (!p) { @@ -1099,6 +1171,7 @@ async function createOrReusePurchase( p.purchaseStatus = PurchaseStatus.FailedPaidByOther; const newTxState = computePayMerchantTransactionState(p); await tx.purchases.put(p); + await ctx.updateTransactionMeta(tx); return { oldTxState, newTxState }; }, ); @@ -1153,10 +1226,13 @@ async function createOrReusePurchase( shared: shared, }; + const ctx = new PayMerchantTransactionContext(wex, proposalId); + const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["purchases"] }, + { storeNames: ["purchases", "transactionsMeta"] }, async (tx) => { await tx.purchases.put(proposalRecord); + await ctx.updateTransactionMeta(tx); const oldTxState: TransactionState = { major: TransactionMajorState.None, }; @@ -1168,11 +1244,7 @@ async function createOrReusePurchase( }, ); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Payment, - proposalId, - }); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); return proposalId; } @@ -1182,13 +1254,10 @@ async function storeFirstPaySuccess( sessionId: string | undefined, payResponse: MerchantPayResponse, ): Promise<void> { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Payment, - proposalId, - }); + const ctx = new PayMerchantTransactionContext(wex, proposalId); const now = AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now()); const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["contractTerms", "purchases"] }, + { storeNames: ["contractTerms", "purchases", "transactionsMeta"] }, async (tx) => { const purchase = await tx.purchases.get(proposalId); @@ -1238,6 +1307,7 @@ async function storeFirstPaySuccess( ); } await tx.purchases.put(purchase); + await ctx.updateTransactionMeta(tx); const newTxState = computePayMerchantTransactionState(purchase); return { oldTxState, @@ -1245,7 +1315,7 @@ async function storeFirstPaySuccess( }; }, ); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); } async function storePayReplaySuccess( @@ -1253,12 +1323,9 @@ async function storePayReplaySuccess( proposalId: string, sessionId: string | undefined, ): Promise<void> { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Payment, - proposalId, - }); + const ctx = new PayMerchantTransactionContext(wex, proposalId); const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["purchases"] }, + { storeNames: ["purchases", "transactionsMeta"] }, async (tx) => { const purchase = await tx.purchases.get(proposalId); @@ -1279,11 +1346,12 @@ async function storePayReplaySuccess( } purchase.lastSessionId = sessionId; await tx.purchases.put(purchase); + await ctx.updateTransactionMeta(tx); const newTxState = computePayMerchantTransactionState(purchase); return { oldTxState, newTxState }; }, ); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); } /** @@ -1361,6 +1429,7 @@ async function handleInsufficientFunds( "purchases", "refreshGroups", "refreshSessions", + "transactionsMeta", ], }, async (tx) => { @@ -1419,6 +1488,7 @@ async function handleInsufficientFunds( }; payInfo.payCoinSelectionUid = encodeCrock(getRandomBytes(32)); await tx.purchases.put(p); + await ctx.updateTransactionMeta(tx); await spendCoins(wex, tx, { transactionId: ctx.transactionId, coinPubs: payInfo.payCoinSelection.coinPubs, @@ -1576,7 +1646,7 @@ async function checkPaymentByProposalId( ); logger.trace(`last: ${purchase.lastSessionId}, current: ${sessionId}`); const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["purchases"] }, + { storeNames: ["purchases", "transactionsMeta"] }, async (tx) => { const p = await tx.purchases.get(proposalId); if (!p) { @@ -1586,6 +1656,7 @@ async function checkPaymentByProposalId( p.lastSessionId = sessionId; p.purchaseStatus = PurchaseStatus.PendingPayingReplay; await tx.purchases.put(p); + await ctx.updateTransactionMeta(tx); const newTxState = computePayMerchantTransactionState(p); return { oldTxState, newTxState }; }, @@ -2068,6 +2139,7 @@ export async function confirmPay( throw Error("expected payment transaction ID"); } const proposalId = parsedTx.proposalId; + const ctx = new PayMerchantTransactionContext(wex, proposalId); logger.trace( `executing confirmPay with proposalId ${proposalId} and sessionIdOverride ${sessionIdOverride}`, ); @@ -2088,7 +2160,7 @@ export async function confirmPay( } const existingPurchase = await wex.db.runReadWriteTx( - { storeNames: ["purchases"] }, + { storeNames: ["purchases", "transactionsMeta"] }, async (tx) => { const purchase = await tx.purchases.get(proposalId); if ( @@ -2102,6 +2174,7 @@ export async function confirmPay( purchase.purchaseStatus = PurchaseStatus.PendingPayingReplay; } await tx.purchases.put(purchase); + await ctx.updateTransactionMeta(tx); } return purchase; }, @@ -2146,6 +2219,7 @@ export async function confirmPay( "purchases", "refreshGroups", "refreshSessions", + "transactionsMeta", ], }, async (tx) => { @@ -2217,6 +2291,7 @@ export async function confirmPay( p.timestampAccept = timestampPreciseToDb(TalerPreciseTimestamp.now()); p.purchaseStatus = PurchaseStatus.PendingPaying; await tx.purchases.put(p); + await ctx.updateTransactionMeta(tx); if (p.payInfo.payCoinSelection) { const sel = p.payInfo.payCoinSelection; await spendCoins(wex, tx, { @@ -2245,8 +2320,6 @@ export async function confirmPay( hintTransactionId: transactionId, }); - const ctx = new PayMerchantTransactionContext(wex, proposalId); - // In case we're sharing the payment and we're long-polling wex.taskScheduler.stopShepherdTask(ctx.taskId); @@ -2372,7 +2445,7 @@ async function processPurchasePay( if (paid) { const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["purchases"] }, + { storeNames: ["purchases", "transactionsMeta"] }, async (tx) => { const p = await tx.purchases.get(purchase.proposalId); if (!p) { @@ -2383,6 +2456,7 @@ async function processPurchasePay( p.purchaseStatus = PurchaseStatus.FailedPaidByOther; const newTxState = computePayMerchantTransactionState(p); await tx.purchases.put(p); + await ctx.updateTransactionMeta(tx); return { oldTxState, newTxState }; }, ); @@ -2453,6 +2527,7 @@ async function processPurchasePay( "purchases", "refreshGroups", "refreshSessions", + "transactionsMeta", ], }, async (tx) => { @@ -2478,7 +2553,7 @@ async function processPurchasePay( p.payInfo.payCoinSelectionUid = encodeCrock(getRandomBytes(16)); p.purchaseStatus = PurchaseStatus.PendingPaying; await tx.purchases.put(p); - + await ctx.updateTransactionMeta(tx); await spendCoins(wex, tx, { transactionId: ctx.transactionId, coinPubs: selectCoinsResult.coinSel.coins.map((x) => x.coinPub), @@ -2639,12 +2714,9 @@ export async function refuseProposal( wex: WalletExecutionContext, proposalId: string, ): Promise<void> { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Payment, - proposalId, - }); + const ctx = new PayMerchantTransactionContext(wex, proposalId); const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["purchases"] }, + { storeNames: ["purchases", "transactionsMeta"] }, async (tx) => { const proposal = await tx.purchases.get(proposalId); if (!proposal) { @@ -2661,11 +2733,12 @@ export async function refuseProposal( proposal.purchaseStatus = PurchaseStatus.AbortedProposalRefused; const newTxState = computePayMerchantTransactionState(proposal); await tx.purchases.put(proposal); + await ctx.updateTransactionMeta(tx); return { oldTxState, newTxState }; }, ); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); } const transitionSuspend: { @@ -2970,13 +3043,30 @@ export async function sharePayment( merchantBaseUrl: string, orderId: string, ): Promise<SharePaymentResult> { - const result = await wex.db.runReadWriteTx( - { storeNames: ["purchases"] }, + // First, translate the order ID into a proposal ID + const proposalId = await wex.db.runReadOnlyTx( + { + storeNames: ["purchases"], + }, async (tx) => { const p = await tx.purchases.indexes.byUrlAndOrderId.get([ merchantBaseUrl, orderId, ]); + return p?.proposalId; + }, + ); + + if (!proposalId) { + throw Error(`no proposal found for order id ${orderId}`); + } + + const ctx = new PayMerchantTransactionContext(wex, proposalId); + + const result = await wex.db.runReadWriteTx( + { storeNames: ["purchases", "transactionsMeta"] }, + async (tx) => { + const p = await tx.purchases.get(proposalId); if (!p) { logger.warn("purchase does not exist anymore"); return undefined; @@ -2995,6 +3085,8 @@ export async function sharePayment( await tx.purchases.put(p); } + await ctx.updateTransactionMeta(tx); + const newTxState = computePayMerchantTransactionState(p); return { @@ -3014,8 +3106,6 @@ export async function sharePayment( throw Error("This purchase can't be shared"); } - const ctx = new PayMerchantTransactionContext(wex, result.proposalId); - notifyTransition(wex, ctx.transactionId, result.transitionInfo); // schedule a task to watch for the status @@ -3082,11 +3172,12 @@ async function processPurchaseDialogShared( const proposalId = purchase.proposalId; logger.trace(`processing dialog-shared for proposal ${proposalId}`); const download = await expectProposalDownload(wex, purchase); - if (purchase.purchaseStatus !== PurchaseStatus.DialogShared) { return TaskRunResult.finished(); } + const ctx = new PayMerchantTransactionContext(wex, proposalId); + const paid = await checkIfOrderIsAlreadyPaid( wex, download.contractData, @@ -3094,7 +3185,7 @@ async function processPurchaseDialogShared( ); if (paid) { const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["purchases"] }, + { storeNames: ["purchases", "transactionsMeta"] }, async (tx) => { const p = await tx.purchases.get(purchase.proposalId); if (!p) { @@ -3105,6 +3196,7 @@ async function processPurchaseDialogShared( p.purchaseStatus = PurchaseStatus.FailedPaidByOther; const newTxState = computePayMerchantTransactionState(p); await tx.purchases.put(p); + await ctx.updateTransactionMeta(tx); return { oldTxState, newTxState }; }, ); @@ -3164,9 +3256,11 @@ async function processPurchaseAutoRefund( Amounts.cmp(download.contractData.amount, totalKnownRefund) === +1; const nothingMoreToRefund = !refundedIsLessThanPrice; + const ctx = new PayMerchantTransactionContext(wex, proposalId); + if (noAutoRefundOrExpired || nothingMoreToRefund) { const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["purchases"] }, + { storeNames: ["purchases", "transactionsMeta"] }, async (tx) => { const p = await tx.purchases.get(purchase.proposalId); if (!p) { @@ -3185,6 +3279,7 @@ async function processPurchaseAutoRefund( p.refundAmountAwaiting = undefined; const newTxState = computePayMerchantTransactionState(p); await tx.purchases.put(p); + await ctx.updateTransactionMeta(tx); return { oldTxState, newTxState }; }, ); @@ -3223,7 +3318,7 @@ async function processPurchaseAutoRefund( if (orderStatus.refund_pending) { const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["purchases"] }, + { storeNames: ["purchases", "transactionsMeta"] }, async (tx) => { const p = await tx.purchases.get(purchase.proposalId); if (!p) { @@ -3241,6 +3336,7 @@ async function processPurchaseAutoRefund( p.purchaseStatus = PurchaseStatus.PendingAcceptRefund; const newTxState = computePayMerchantTransactionState(p); await tx.purchases.put(p); + await ctx.updateTransactionMeta(tx); return { oldTxState, newTxState }; }, ); @@ -3370,14 +3466,11 @@ async function processPurchaseQueryRefund( codecForMerchantOrderStatusPaid(), ); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Payment, - proposalId, - }); + const ctx = new PayMerchantTransactionContext(wex, proposalId); if (!orderStatus.refund_pending) { const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["purchases"] }, + { storeNames: ["purchases", "transactionsMeta"] }, async (tx) => { const p = await tx.purchases.get(purchase.proposalId); if (!p) { @@ -3392,10 +3485,11 @@ async function processPurchaseQueryRefund( p.refundAmountAwaiting = undefined; const newTxState = computePayMerchantTransactionState(p); await tx.purchases.put(p); + await ctx.updateTransactionMeta(tx); return { oldTxState, newTxState }; }, ); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); return TaskRunResult.progress(); } else { const refundAwaiting = Amounts.sub( @@ -3404,7 +3498,7 @@ async function processPurchaseQueryRefund( ).amount; const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["purchases"] }, + { storeNames: ["purchases", "transactionsMeta"] }, async (tx) => { const p = await tx.purchases.get(purchase.proposalId); if (!p) { @@ -3419,10 +3513,11 @@ async function processPurchaseQueryRefund( p.purchaseStatus = PurchaseStatus.PendingAcceptRefund; const newTxState = computePayMerchantTransactionState(p); await tx.purchases.put(p); + await ctx.updateTransactionMeta(tx); return { oldTxState, newTxState }; }, ); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); return TaskRunResult.progress(); } } @@ -3503,7 +3598,7 @@ export async function startQueryRefund( ): Promise<void> { const ctx = new PayMerchantTransactionContext(wex, proposalId); const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["purchases"] }, + { storeNames: ["purchases", "transactionsMeta"] }, async (tx) => { const p = await tx.purchases.get(proposalId); if (!p) { @@ -3517,6 +3612,7 @@ export async function startQueryRefund( p.purchaseStatus = PurchaseStatus.PendingQueryingRefund; const newTxState = computePayMerchantTransactionState(p); await tx.purchases.put(p); + await ctx.updateTransactionMeta(tx); return { oldTxState, newTxState }; }, ); @@ -3584,10 +3680,7 @@ async function storeRefunds( ): Promise<TaskRunResult> { logger.info(`storing refunds: ${j2s(refunds)}`); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Payment, - proposalId: purchase.proposalId, - }); + const ctx = new PayMerchantTransactionContext(wex, purchase.proposalId); const newRefundGroupId = encodeCrock(randomBytes(32)); const now = TalerPreciseTimestamp.now(); @@ -3609,6 +3702,7 @@ async function storeRefunds( "refreshSessions", "refundGroups", "refundItems", + "transactionsMeta", ], }, async (tx) => { @@ -3709,7 +3803,12 @@ async function storeRefunds( newGroup.amountRaw = Amounts.stringify( Amounts.sumOrZero(currency, amountsRaw).amount, ); + const refundCtx = new RefundTransactionContext( + wex, + newGroup.refundGroupId, + ); await tx.refundGroups.put(newGroup); + await refundCtx.updateTransactionMeta(tx); } const refundGroups = await tx.refundGroups.indexes.byProposalId.getAll( @@ -3717,6 +3816,10 @@ async function storeRefunds( ); for (const refundGroup of refundGroups) { + const refundCtx = new RefundTransactionContext( + wex, + refundGroup.refundGroupId, + ); switch (refundGroup.status) { case RefundGroupStatus.Aborted: case RefundGroupStatus.Expired: @@ -3749,6 +3852,7 @@ async function storeRefunds( refundGroup.status = RefundGroupStatus.Failed; } await tx.refundGroups.put(refundGroup); + await refundCtx.updateTransactionMeta(tx); const refreshCoins = await computeRefreshRequest(wex, tx, items); await createRefreshGroup( wex, @@ -3788,6 +3892,7 @@ async function storeRefunds( myPurchase.refundAmountAwaiting = undefined; } await tx.purchases.put(myPurchase); + await ctx.updateTransactionMeta(tx); const newTxState = computePayMerchantTransactionState(myPurchase); return { @@ -3804,7 +3909,7 @@ async function storeRefunds( return TaskRunResult.finished(); } - notifyTransition(wex, transactionId, result.transitionInfo); + notifyTransition(wex, ctx.transactionId, result.transitionInfo); if (result.numPendingItemsTotal > 0) { return TaskRunResult.backoff(); diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts @@ -14,6 +14,9 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +/** + * Imports. + */ import { AbsoluteTime, Amounts, @@ -72,6 +75,7 @@ import { PeerPullCreditRecord, PeerPullPaymentCreditStatus, WalletDbAllStoresReadOnlyTransaction, + WalletDbReadWriteTransaction, WithdrawalGroupRecord, WithdrawalGroupStatus, WithdrawalRecordType, @@ -116,6 +120,23 @@ export class PeerPullCreditTransactionContext implements TransactionContext { }); } + async updateTransactionMeta( + tx: WalletDbReadWriteTransaction<["peerPullCredit", "transactionsMeta"]>, + ): Promise<void> { + const ppcRec = await tx.peerPullCredit.get(this.pursePub); + if (!ppcRec) { + await tx.transactionsMeta.delete(this.transactionId); + return; + } + await tx.transactionsMeta.put({ + transactionId: this.transactionId, + status: ppcRec.status, + timestamp: ppcRec.mergeTimestamp, + currency: Amounts.currencyOf(ppcRec.amount), + exchanges: [ppcRec.exchangeBaseUrl], + }); + } + /** * Get the full transaction details for the transaction. * @@ -234,7 +255,14 @@ export class PeerPullCreditTransactionContext implements TransactionContext { async deleteTransaction(): Promise<void> { const { wex: ws, pursePub } = this; await ws.db.runReadWriteTx( - { storeNames: ["withdrawalGroups", "peerPullCredit", "tombstones"] }, + { + storeNames: [ + "withdrawalGroups", + "peerPullCredit", + "tombstones", + "transactionsMeta", + ], + }, async (tx) => { const pullIni = await tx.peerPullCredit.get(pursePub); if (!pullIni) { @@ -252,6 +280,7 @@ export class PeerPullCreditTransactionContext implements TransactionContext { } } await tx.peerPullCredit.delete(pursePub); + await this.updateTransactionMeta(tx); await tx.tombstones.put({ id: TombstoneTag.DeletePeerPullCredit + ":" + pursePub, }); @@ -264,7 +293,7 @@ export class PeerPullCreditTransactionContext implements TransactionContext { async suspendTransaction(): Promise<void> { const { wex, pursePub, taskId: retryTag, transactionId } = this; const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPullCredit"] }, + { storeNames: ["peerPullCredit", "transactionsMeta"] }, async (tx) => { const pullCreditRec = await tx.peerPullCredit.get(pursePub); if (!pullCreditRec) { @@ -309,6 +338,7 @@ export class PeerPullCreditTransactionContext implements TransactionContext { const newTxState = computePeerPullCreditTransactionState(pullCreditRec); await tx.peerPullCredit.put(pullCreditRec); + await this.updateTransactionMeta(tx); return { oldTxState, newTxState, @@ -324,7 +354,7 @@ export class PeerPullCreditTransactionContext implements TransactionContext { async failTransaction(): Promise<void> { const { wex, pursePub, taskId: retryTag, transactionId } = this; const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPullCredit"] }, + { storeNames: ["peerPullCredit", "transactionsMeta"] }, async (tx) => { const pullCreditRec = await tx.peerPullCredit.get(pursePub); if (!pullCreditRec) { @@ -360,6 +390,7 @@ export class PeerPullCreditTransactionContext implements TransactionContext { const newTxState = computePeerPullCreditTransactionState(pullCreditRec); await tx.peerPullCredit.put(pullCreditRec); + await this.updateTransactionMeta(tx); return { oldTxState, newTxState, @@ -375,7 +406,7 @@ export class PeerPullCreditTransactionContext implements TransactionContext { async resumeTransaction(): Promise<void> { const { wex, pursePub, taskId: retryTag, transactionId } = this; const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPullCredit"] }, + { storeNames: ["peerPullCredit", "transactionsMeta"] }, async (tx) => { const pullCreditRec = await tx.peerPullCredit.get(pursePub); if (!pullCreditRec) { @@ -419,6 +450,7 @@ export class PeerPullCreditTransactionContext implements TransactionContext { const newTxState = computePeerPullCreditTransactionState(pullCreditRec); await tx.peerPullCredit.put(pullCreditRec); + await this.updateTransactionMeta(tx); return { oldTxState, newTxState, @@ -434,7 +466,7 @@ export class PeerPullCreditTransactionContext implements TransactionContext { async abortTransaction(): Promise<void> { const { wex, pursePub, taskId: retryTag, transactionId } = this; const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPullCredit"] }, + { storeNames: ["peerPullCredit", "transactionsMeta"] }, async (tx) => { const pullCreditRec = await tx.peerPullCredit.get(pursePub); if (!pullCreditRec) { @@ -473,6 +505,7 @@ export class PeerPullCreditTransactionContext implements TransactionContext { const newTxState = computePeerPullCreditTransactionState(pullCreditRec); await tx.peerPullCredit.put(pullCreditRec); + await this.updateTransactionMeta(tx); return { oldTxState, newTxState, @@ -507,10 +540,7 @@ async function queryPurseForPeerPullCredit( }); }, ); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullCredit, - pursePub: pullIni.pursePub, - }); + const ctx = new PeerPullCreditTransactionContext(wex, pullIni.pursePub); logger.info(`purse status code: HTTP ${resp.status}`); @@ -518,7 +548,7 @@ async function queryPurseForPeerPullCredit( case HttpStatusCode.Gone: { // Exchange says that purse doesn't exist anymore => expired! const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPullCredit"] }, + { storeNames: ["peerPullCredit", "transactionsMeta"] }, async (tx) => { const finPi = await tx.peerPullCredit.get(pullIni.pursePub); if (!finPi) { @@ -530,11 +560,12 @@ async function queryPurseForPeerPullCredit( finPi.status = PeerPullPaymentCreditStatus.Expired; } await tx.peerPullCredit.put(finPi); + await ctx.updateTransactionMeta(tx); const newTxState = computePeerPullCreditTransactionState(finPi); return { oldTxState, newTxState }; }, ); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); return TaskRunResult.backoff(); } case HttpStatusCode.NotFound: @@ -582,7 +613,7 @@ async function queryPurseForPeerPullCredit( }, }); const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPullCredit"] }, + { storeNames: ["peerPullCredit", "transactionsMeta"] }, async (tx) => { const finPi = await tx.peerPullCredit.get(pullIni.pursePub); if (!finPi) { @@ -594,11 +625,12 @@ async function queryPurseForPeerPullCredit( finPi.status = PeerPullPaymentCreditStatus.PendingWithdrawing; } await tx.peerPullCredit.put(finPi); + await ctx.updateTransactionMeta(tx); const newTxState = computePeerPullCreditTransactionState(finPi); return { oldTxState, newTxState }; }, ); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); return TaskRunResult.backoff(); } @@ -613,6 +645,7 @@ async function longpollKycStatus( tag: TransactionType.PeerPullCredit, pursePub, }); + const ctx = new PeerPullCreditTransactionContext(wex, pursePub); const url = new URL( `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`, exchangeUrl, @@ -636,7 +669,7 @@ async function longpollKycStatus( kycStatusRes.status === HttpStatusCode.NoContent ) { const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPullCredit"] }, + { storeNames: ["peerPullCredit", "transactionsMeta"] }, async (tx) => { const peerIni = await tx.peerPullCredit.get(pursePub); if (!peerIni) { @@ -651,6 +684,7 @@ async function longpollKycStatus( peerIni.status = PeerPullPaymentCreditStatus.PendingCreatePurse; const newTxState = computePeerPullCreditTransactionState(peerIni); await tx.peerPullCredit.put(peerIni); + await ctx.updateTransactionMeta(tx); return { oldTxState, newTxState }; }, ); @@ -668,10 +702,7 @@ async function processPeerPullCreditAbortingDeletePurse( peerPullIni: PeerPullCreditRecord, ): Promise<TaskRunResult> { const { pursePub, pursePriv } = peerPullIni; - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullCredit, - pursePub, - }); + const ctx = new PeerPullCreditTransactionContext(wex, peerPullIni.pursePub); const sigResp = await wex.cryptoApi.signDeletePurse({ pursePriv, @@ -694,6 +725,7 @@ async function processPeerPullCreditAbortingDeletePurse( "denominations", "coinAvailability", "coins", + "transactionsMeta", ], }, async (tx) => { @@ -707,6 +739,7 @@ async function processPeerPullCreditAbortingDeletePurse( const oldTxState = computePeerPullCreditTransactionState(ppiRec); ppiRec.status = PeerPullPaymentCreditStatus.Aborted; await tx.peerPullCredit.put(ppiRec); + await ctx.updateTransactionMeta(tx); const newTxState = computePeerPullCreditTransactionState(ppiRec); return { oldTxState, @@ -714,7 +747,7 @@ async function processPeerPullCreditAbortingDeletePurse( }; }, ); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); return TaskRunResult.backoff(); } @@ -727,14 +760,11 @@ async function handlePeerPullCreditWithdrawing( throw Error("invalid db state (withdrawing, but no withdrawal group ID"); } await waitWithdrawalFinal(wex, pullIni.withdrawalGroupId); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullCredit, - pursePub: pullIni.pursePub, - }); + const ctx = new PeerPullCreditTransactionContext(wex, pullIni.pursePub); const wgId = pullIni.withdrawalGroupId; let finished: boolean = false; const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPullCredit", "withdrawalGroups"] }, + { storeNames: ["peerPullCredit", "withdrawalGroups", "transactionsMeta"] }, async (tx) => { const ppi = await tx.peerPullCredit.get(pullIni.pursePub); if (!ppi) { @@ -759,6 +789,7 @@ async function handlePeerPullCreditWithdrawing( // FIXME: Also handle other final states! } await tx.peerPullCredit.put(ppi); + await ctx.updateTransactionMeta(tx); const newTxState = computePeerPullCreditTransactionState(ppi); return { oldTxState, @@ -766,7 +797,7 @@ async function handlePeerPullCreditWithdrawing( }; }, ); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); if (finished) { return TaskRunResult.finished(); } else { @@ -875,13 +906,10 @@ async function handlePeerPullCreditCreatePurse( logger.info(`reserve merge response: ${j2s(resp)}`); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullCredit, - pursePub: pullIni.pursePub, - }); + const ctx = new PeerPullCreditTransactionContext(wex, pullIni.pursePub); const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPullCredit"] }, + { storeNames: ["peerPullCredit", "transactionsMeta"] }, async (tx) => { const pi2 = await tx.peerPullCredit.get(pursePub); if (!pi2) { @@ -890,11 +918,12 @@ async function handlePeerPullCreditCreatePurse( const oldTxState = computePeerPullCreditTransactionState(pi2); pi2.status = PeerPullPaymentCreditStatus.PendingReady; await tx.peerPullCredit.put(pi2); + await ctx.updateTransactionMeta(tx); const newTxState = computePeerPullCreditTransactionState(pi2); return { oldTxState, newTxState }; }, ); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); return TaskRunResult.backoff(); } @@ -968,10 +997,7 @@ async function processPeerPullCreditKycRequired( peerIni: PeerPullCreditRecord, kycPending: WalletKycUuid, ): Promise<TaskRunResult> { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullCredit, - pursePub: peerIni.pursePub, - }); + const ctx = new PeerPullCreditTransactionContext(wex, peerIni.pursePub); const { pursePub } = peerIni; const userType = "individual"; @@ -998,7 +1024,7 @@ async function processPeerPullCreditKycRequired( const kycStatus = await kycStatusRes.json(); logger.info(`kyc status: ${j2s(kycStatus)}`); const { transitionInfo, result } = await wex.db.runReadWriteTx( - { storeNames: ["peerPullCredit"] }, + { storeNames: ["peerPullCredit", "transactionsMeta"] }, async (tx) => { const peerInc = await tx.peerPullCredit.get(pursePub); if (!peerInc) { @@ -1016,6 +1042,7 @@ async function processPeerPullCreditKycRequired( peerInc.status = PeerPullPaymentCreditStatus.PendingMergeKycRequired; const newTxState = computePeerPullCreditTransactionState(peerInc); await tx.peerPullCredit.put(peerInc); + await ctx.updateTransactionMeta(tx); // We'll remove this eventually! New clients should rely on the // kycUrl field of the transaction, not the error code. const res: TaskRunResult = { @@ -1033,7 +1060,7 @@ async function processPeerPullCreditKycRequired( }; }, ); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); return TaskRunResult.backoff(); } else { throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`); @@ -1221,8 +1248,10 @@ export async function initiatePeerPullPayment( const mergeTimestamp = TalerPreciseTimestamp.now(); + const ctx = new PeerPullCreditTransactionContext(wex, pursePair.pub); + const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPullCredit", "contractTerms"] }, + { storeNames: ["peerPullCredit", "contractTerms", "transactionsMeta"] }, async (tx) => { const ppi: PeerPullCreditRecord = { amount: req.partialContractTerms.amount, @@ -1242,6 +1271,7 @@ export async function initiatePeerPullPayment( estimatedAmountEffective: wi.withdrawalAmountEffective, }; await tx.peerPullCredit.put(ppi); + await ctx.updateTransactionMeta(tx); const oldTxState: TransactionState = { major: TransactionMajorState.None, }; @@ -1254,8 +1284,6 @@ export async function initiatePeerPullPayment( }, ); - const ctx = new PeerPullCreditTransactionContext(wex, pursePair.pub); - notifyTransition(wex, ctx.transactionId, transitionInfo); wex.taskScheduler.startShepherdTask(ctx.taskId); diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts @@ -84,6 +84,7 @@ import { PeerPullPaymentIncomingRecord, RefreshOperationStatus, WalletDbAllStoresReadOnlyTransaction, + WalletDbReadWriteTransaction, WalletStoresV1, timestampPreciseFromDb, timestampPreciseToDb, @@ -127,6 +128,23 @@ export class PeerPullDebitTransactionContext implements TransactionContext { this.peerPullDebitId = peerPullDebitId; } + async updateTransactionMeta( + tx: WalletDbReadWriteTransaction<["peerPullDebit", "transactionsMeta"]>, + ): Promise<void> { + const ppdRec = await tx.peerPullDebit.get(this.peerPullDebitId); + if (!ppdRec) { + await tx.transactionsMeta.delete(this.transactionId); + return; + } + await tx.transactionsMeta.put({ + transactionId: this.transactionId, + status: ppdRec.status, + timestamp: ppdRec.timestampCreated, + currency: Amounts.currencyOf(ppdRec.amount), + exchanges: [ppdRec.exchangeBaseUrl], + }); + } + /** * Get the full transaction details for the transaction. * @@ -174,11 +192,12 @@ export class PeerPullDebitTransactionContext implements TransactionContext { const ws = this.wex; const peerPullDebitId = this.peerPullDebitId; await ws.db.runReadWriteTx( - { storeNames: ["peerPullDebit", "tombstones"] }, + { storeNames: ["peerPullDebit", "tombstones", "transactionsMeta"] }, async (tx) => { const debit = await tx.peerPullDebit.get(peerPullDebitId); if (debit) { await tx.peerPullDebit.delete(peerPullDebitId); + await this.updateTransactionMeta(tx); await tx.tombstones.put({ id: transactionId }); } }, @@ -191,7 +210,7 @@ export class PeerPullDebitTransactionContext implements TransactionContext { const wex = this.wex; const peerPullDebitId = this.peerPullDebitId; const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPullDebit"] }, + { storeNames: ["peerPullDebit", "transactionsMeta"] }, async (tx) => { const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId); if (!pullDebitRec) { @@ -226,6 +245,7 @@ export class PeerPullDebitTransactionContext implements TransactionContext { pullDebitRec.status = newStatus; const newTxState = computePeerPullDebitTransactionState(pullDebitRec); await tx.peerPullDebit.put(pullDebitRec); + await this.updateTransactionMeta(tx); return { oldTxState, newTxState, @@ -349,14 +369,14 @@ export class PeerPullDebitTransactionContext implements TransactionContext { rec: PeerPullPaymentIncomingRecord, tx: DbReadWriteTransaction< typeof WalletStoresV1, - ["peerPullDebit", ...StoreNameArray] + ["peerPullDebit", "transactionsMeta", ...StoreNameArray] >, ) => Promise<TransitionResultType>, ): Promise<void> { const wex = this.wex; const extraStores = opts.extraStores ?? []; const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPullDebit", ...extraStores] }, + { storeNames: ["peerPullDebit", "transactionsMeta", ...extraStores] }, async (tx) => { const pi = await tx.peerPullDebit.get(this.peerPullDebitId); if (!pi) { @@ -367,12 +387,23 @@ export class PeerPullDebitTransactionContext implements TransactionContext { switch (res) { case TransitionResultType.Transition: { await tx.peerPullDebit.put(pi); + await this.updateTransactionMeta(tx); const newTxState = computePeerPullDebitTransactionState(pi); return { oldTxState, newTxState, }; } + case TransitionResultType.Delete: { + await tx.peerPullDebit.delete(this.peerPullDebitId); + await this.updateTransactionMeta(tx); + return { + oldTxState, + newTxState: { + major: TransactionMajorState.None, + }, + }; + } default: return undefined; } @@ -449,27 +480,31 @@ async function handlePurseCreationConflict( coinSelRes.result.coins, ); - await ws.db.runReadWriteTx({ storeNames: ["peerPullDebit"] }, async (tx) => { - const myPpi = await tx.peerPullDebit.get(peerPullInc.peerPullDebitId); - if (!myPpi) { - return; - } - switch (myPpi.status) { - case PeerPullDebitRecordStatus.PendingDeposit: - case PeerPullDebitRecordStatus.SuspendedDeposit: { - const sel = coinSelRes.result; - myPpi.coinSel = { - coinPubs: sel.coins.map((x) => x.coinPub), - contributions: sel.coins.map((x) => x.contribution), - totalCost: Amounts.stringify(totalAmount), - }; - break; - } - default: + await ws.db.runReadWriteTx( + { storeNames: ["peerPullDebit", "transactionsMeta"] }, + async (tx) => { + const myPpi = await tx.peerPullDebit.get(peerPullInc.peerPullDebitId); + if (!myPpi) { return; - } - await tx.peerPullDebit.put(myPpi); - }); + } + switch (myPpi.status) { + case PeerPullDebitRecordStatus.PendingDeposit: + case PeerPullDebitRecordStatus.SuspendedDeposit: { + const sel = coinSelRes.result; + myPpi.coinSel = { + coinPubs: sel.coins.map((x) => x.coinPub), + contributions: sel.coins.map((x) => x.contribution), + totalCost: Amounts.stringify(totalAmount), + }; + break; + } + default: + return; + } + await tx.peerPullDebit.put(myPpi); + await ctx.updateTransactionMeta(tx); + }, + ); return TaskRunResult.backoff(); } @@ -531,6 +566,7 @@ async function processPeerPullDebitPendingDeposit( "peerPullDebit", "refreshGroups", "refreshSessions", + "transactionsMeta", ], }, async (tx) => { @@ -558,6 +594,7 @@ async function processPeerPullDebitPendingDeposit( totalCost: Amounts.stringify(totalAmount), }; await tx.peerPullDebit.put(pi); + await ctx.updateTransactionMeta(tx); return true; }, ); @@ -653,12 +690,9 @@ async function processPeerPullDebitAbortingRefresh( const peerPullDebitId = peerPullInc.peerPullDebitId; const abortRefreshGroupId = peerPullInc.abortRefreshGroupId; checkLogicInvariant(!!abortRefreshGroupId); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullDebit, - peerPullDebitId, - }); + const ctx = new PeerPullDebitTransactionContext(wex, peerPullDebitId); const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPullDebit", "refreshGroups"] }, + { storeNames: ["peerPullDebit", "refreshGroups", "transactionsMeta"] }, async (tx) => { const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId); let newOpState: PeerPullDebitRecordStatus | undefined; @@ -685,12 +719,13 @@ async function processPeerPullDebitAbortingRefresh( newDg.status = newOpState; const newTxState = computePeerPullDebitTransactionState(newDg); await tx.peerPullDebit.put(newDg); + await ctx.updateTransactionMeta(tx); return { oldTxState, newTxState }; } return undefined; }, ); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); // FIXME: Shouldn't this be finished in some cases?! return TaskRunResult.backoff(); } @@ -793,6 +828,7 @@ export async function confirmPeerPullDebit( "peerPullDebit", "refreshGroups", "refreshSessions", + "transactionsMeta", ], }, async (tx) => { @@ -819,6 +855,7 @@ export async function confirmPeerPullDebit( }; } pi.status = PeerPullDebitRecordStatus.PendingDeposit; + await ctx.updateTransactionMeta(tx); await tx.peerPullDebit.put(pi); }, ); @@ -962,24 +999,27 @@ export async function preparePeerPullDebit( const totalAmount = await getTotalPeerPaymentCost(wex, coins); + const ctx = new PeerPullDebitTransactionContext(wex, peerPullDebitId); + await wex.db.runReadWriteTx( - { storeNames: ["peerPullDebit", "contractTerms"] }, + { storeNames: ["peerPullDebit", "contractTerms", "transactionsMeta"] }, async (tx) => { await tx.contractTerms.put({ h: contractTermsHash, contractTermsRaw: contractTerms, - }), - await tx.peerPullDebit.add({ - peerPullDebitId, - contractPriv: contractPriv, - exchangeBaseUrl: exchangeBaseUrl, - pursePub: pursePub, - timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()), - contractTermsHash, - amount: contractTerms.amount, - status: PeerPullDebitRecordStatus.DialogProposed, - totalCostEstimated: Amounts.stringify(totalAmount), - }); + }); + await tx.peerPullDebit.add({ + peerPullDebitId, + contractPriv: contractPriv, + exchangeBaseUrl: exchangeBaseUrl, + pursePub: pursePub, + timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()), + contractTermsHash, + amount: contractTerms.amount, + status: PeerPullDebitRecordStatus.DialogProposed, + totalCostEstimated: Amounts.stringify(totalAmount), + }); + await ctx.updateTransactionMeta(tx); }, ); diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts @@ -72,6 +72,7 @@ import { PeerPushCreditStatus, PeerPushPaymentIncomingRecord, WalletDbAllStoresReadOnlyTransaction, + WalletDbReadWriteTransaction, WithdrawalGroupRecord, WithdrawalGroupStatus, WithdrawalRecordType, @@ -119,6 +120,23 @@ export class PeerPushCreditTransactionContext implements TransactionContext { }); } + async updateTransactionMeta( + tx: WalletDbReadWriteTransaction<["peerPushCredit", "transactionsMeta"]>, + ): Promise<void> { + const ppdRec = await tx.peerPushCredit.get(this.peerPushCreditId); + if (!ppdRec) { + await tx.transactionsMeta.delete(this.transactionId); + return; + } + await tx.transactionsMeta.put({ + transactionId: this.transactionId, + status: ppdRec.status, + timestamp: ppdRec.timestamp, + currency: Amounts.currencyOf(ppdRec.estimatedAmountEffective), + exchanges: [ppdRec.exchangeBaseUrl], + }); + } + /** * Get the full transaction details for the transaction. * @@ -215,7 +233,14 @@ export class PeerPushCreditTransactionContext implements TransactionContext { async deleteTransaction(): Promise<void> { const { wex, peerPushCreditId } = this; await wex.db.runReadWriteTx( - { storeNames: ["withdrawalGroups", "peerPushCredit", "tombstones"] }, + { + storeNames: [ + "withdrawalGroups", + "peerPushCredit", + "tombstones", + "transactionsMeta", + ], + }, async (tx) => { const pushInc = await tx.peerPushCredit.get(peerPushCreditId); if (!pushInc) { @@ -233,6 +258,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext { } } await tx.peerPushCredit.delete(peerPushCreditId); + await this.updateTransactionMeta(tx); await tx.tombstones.put({ id: TombstoneTag.DeletePeerPushCredit + ":" + peerPushCreditId, }); @@ -244,7 +270,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext { async suspendTransaction(): Promise<void> { const { wex, peerPushCreditId, taskId: retryTag, transactionId } = this; const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPushCredit"] }, + { storeNames: ["peerPushCredit", "transactionsMeta"] }, async (tx) => { const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId); if (!pushCreditRec) { @@ -283,6 +309,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext { const newTxState = computePeerPushCreditTransactionState(pushCreditRec); await tx.peerPushCredit.put(pushCreditRec); + await this.updateTransactionMeta(tx); return { oldTxState, newTxState, @@ -298,7 +325,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext { async abortTransaction(): Promise<void> { const { wex, peerPushCreditId, taskId: retryTag, transactionId } = this; const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPushCredit"] }, + { storeNames: ["peerPushCredit", "transactionsMeta"] }, async (tx) => { const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId); if (!pushCreditRec) { @@ -340,6 +367,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext { const newTxState = computePeerPushCreditTransactionState(pushCreditRec); await tx.peerPushCredit.put(pushCreditRec); + await this.updateTransactionMeta(tx); return { oldTxState, newTxState, @@ -355,7 +383,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext { async resumeTransaction(): Promise<void> { const { wex, peerPushCreditId, taskId: retryTag, transactionId } = this; const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPushCredit"] }, + { storeNames: ["peerPushCredit", "transactionsMeta"] }, async (tx) => { const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId); if (!pushCreditRec) { @@ -393,6 +421,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext { const newTxState = computePeerPushCreditTransactionState(pushCreditRec); await tx.peerPushCredit.put(pushCreditRec); + await this.updateTransactionMeta(tx); return { oldTxState, newTxState, @@ -408,7 +437,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext { async failTransaction(): Promise<void> { const { wex, peerPushCreditId, taskId: retryTag, transactionId } = this; const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPushCredit"] }, + { storeNames: ["peerPushCredit", "transactionsMeta"] }, async (tx) => { const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId); if (!pushCreditRec) { @@ -441,6 +470,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext { const newTxState = computePeerPushCreditTransactionState(pushCreditRec); await tx.peerPushCredit.put(pushCreditRec); + await this.updateTransactionMeta(tx); return { oldTxState, newTxState, @@ -564,8 +594,10 @@ export async function preparePeerPushCredit( ); } + const ctx = new PeerPushCreditTransactionContext(wex, withdrawalGroupId); + const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["contractTerms", "peerPushCredit"] }, + { storeNames: ["contractTerms", "peerPushCredit", "transactionsMeta"] }, async (tx) => { const rec: PeerPushPaymentIncomingRecord = { peerPushCreditId, @@ -583,6 +615,7 @@ export async function preparePeerPushCredit( ), }; await tx.peerPushCredit.add(rec); + await ctx.updateTransactionMeta(tx); await tx.contractTerms.put({ h: contractTermsHash, contractTermsRaw: dec.contractTerms, @@ -629,10 +662,7 @@ async function longpollKycStatus( kycInfo: KycPendingInfo, userType: KycUserType, ): Promise<TaskRunResult> { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPushCredit, - peerPushCreditId, - }); + const ctx = new PeerPushCreditTransactionContext(wex, peerPushCreditId); const url = new URL( `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`, exchangeUrl, @@ -657,7 +687,7 @@ async function longpollKycStatus( kycStatusRes.status === HttpStatusCode.NoContent ) { const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPushCredit"] }, + { storeNames: ["peerPushCredit", "transactionsMeta"] }, async (tx) => { const peerInc = await tx.peerPushCredit.get(peerPushCreditId); if (!peerInc) { @@ -670,10 +700,11 @@ async function longpollKycStatus( peerInc.status = PeerPushCreditStatus.PendingMerge; const newTxState = computePeerPushCreditTransactionState(peerInc); await tx.peerPushCredit.put(peerInc); + await ctx.updateTransactionMeta(tx); return { oldTxState, newTxState }; }, ); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); return TaskRunResult.progress(); } else if (kycStatusRes.status === HttpStatusCode.Accepted) { // FIXME: Do we have to update the URL here? @@ -692,6 +723,10 @@ async function processPeerPushCreditKycRequired( tag: TransactionType.PeerPushCredit, peerPushCreditId: peerInc.peerPushCreditId, }); + const ctx = new PeerPushCreditTransactionContext( + wex, + peerInc.peerPushCreditId, + ); const { peerPushCreditId } = peerInc; const userType = "individual"; @@ -718,7 +753,7 @@ async function processPeerPushCreditKycRequired( const kycStatus = await kycStatusRes.json(); logger.info(`kyc status: ${j2s(kycStatus)}`); const { transitionInfo, result } = await wex.db.runReadWriteTx( - { storeNames: ["peerPushCredit"] }, + { storeNames: ["peerPushCredit", "transactionsMeta"] }, async (tx) => { const peerInc = await tx.peerPushCredit.get(peerPushCreditId); if (!peerInc) { @@ -736,6 +771,7 @@ async function processPeerPushCreditKycRequired( peerInc.status = PeerPushCreditStatus.PendingMergeKycRequired; const newTxState = computePeerPushCreditTransactionState(peerInc); await tx.peerPushCredit.put(peerInc); + await ctx.updateTransactionMeta(tx); // We'll remove this eventually! New clients should rely on the // kycUrl field of the transaction, not the error code. const res: TaskRunResult = { @@ -766,10 +802,7 @@ async function handlePendingMerge( contractTerms: PeerContractTerms, ): Promise<TaskRunResult> { const { peerPushCreditId } = peerInc; - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPushCredit, - peerPushCreditId, - }); + const ctx = new PeerPushCreditTransactionContext(wex, peerPushCreditId); const amount = Amounts.parseOrThrow(contractTerms.amount); @@ -855,6 +888,7 @@ async function handlePendingMerge( "reserves", "exchanges", "exchangeDetails", + "transactionsMeta", ], }, async (tx) => { @@ -880,6 +914,7 @@ async function handlePendingMerge( } } await tx.peerPushCredit.put(peerInc); + await ctx.updateTransactionMeta(tx); const newTxState = computePeerPushCreditTransactionState(peerInc); return { peerPushCreditTransition: { oldTxState, newTxState }, @@ -896,7 +931,7 @@ async function handlePendingMerge( withdrawalGroupPrep.transactionId, txRes?.wgCreateRes?.transitionInfo, ); - notifyTransition(wex, transactionId, txRes?.peerPushCreditTransition); + notifyTransition(wex, ctx.transactionId, txRes?.peerPushCreditTransition); return TaskRunResult.backoff(); } @@ -909,14 +944,14 @@ async function handlePendingWithdrawing( throw Error("invalid db state (withdrawing, but no withdrawal group ID"); } await waitWithdrawalFinal(wex, peerInc.withdrawalGroupId); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPushCredit, - peerPushCreditId: peerInc.peerPushCreditId, - }); + const ctx = new PeerPushCreditTransactionContext( + wex, + peerInc.peerPushCreditId, + ); const wgId = peerInc.withdrawalGroupId; let finished: boolean = false; const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPushCredit", "withdrawalGroups"] }, + { storeNames: ["peerPushCredit", "withdrawalGroups", "transactionsMeta"] }, async (tx) => { const ppi = await tx.peerPushCredit.get(peerInc.peerPushCreditId); if (!ppi) { @@ -941,6 +976,7 @@ async function handlePendingWithdrawing( // FIXME: Also handle other final states! } await tx.peerPushCredit.put(ppi); + await ctx.updateTransactionMeta(tx); const newTxState = computePeerPushCreditTransactionState(ppi); return { oldTxState, @@ -948,7 +984,7 @@ async function handlePendingWithdrawing( }; }, ); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); if (finished) { return TaskRunResult.finished(); } else { @@ -964,11 +1000,12 @@ export async function processPeerPushCredit( if (!wex.ws.networkAvailable) { return TaskRunResult.networkRequired(); } + const ctx = new PeerPushCreditTransactionContext(wex, peerPushCreditId); let peerInc: PeerPushPaymentIncomingRecord | undefined; let contractTerms: PeerContractTerms | undefined; await wex.db.runReadWriteTx( - { storeNames: ["contractTerms", "peerPushCredit"] }, + { storeNames: ["contractTerms", "peerPushCredit", "transactionsMeta"] }, async (tx) => { peerInc = await tx.peerPushCredit.get(peerPushCreditId); if (!peerInc) { @@ -979,6 +1016,7 @@ export async function processPeerPushCredit( contractTerms = ctRec.contractTermsRaw; } await tx.peerPushCredit.put(peerInc); + await ctx.updateTransactionMeta(tx); }, ); @@ -1026,8 +1064,6 @@ export async function confirmPeerPushCredit( wex: WalletExecutionContext, req: ConfirmPeerPushCreditRequest, ): Promise<AcceptPeerPushPaymentResponse> { - // PeerPushPaymentIncomingRecord | undefined; - let peerPushCreditId: string; const parsedTx = parseTransactionIdentifier(req.transactionId); if (!parsedTx) { throw Error("invalid transaction ID"); @@ -1035,14 +1071,17 @@ export async function confirmPeerPushCredit( if (parsedTx.tag !== TransactionType.PeerPushCredit) { throw Error("invalid transaction ID type"); } - peerPushCreditId = parsedTx.peerPushCreditId; + const ctx = new PeerPushCreditTransactionContext( + wex, + parsedTx.peerPushCreditId, + ); - logger.trace(`confirming peer-push-credit ${peerPushCreditId}`); + logger.trace(`confirming peer-push-credit ${ctx.peerPushCreditId}`); const peerInc = await wex.db.runReadWriteTx( - { storeNames: ["contractTerms", "peerPushCredit"] }, + { storeNames: ["contractTerms", "peerPushCredit", "transactionsMeta"] }, async (tx) => { - const rec = await tx.peerPushCredit.get(peerPushCreditId); + const rec = await tx.peerPushCredit.get(ctx.peerPushCreditId); if (!rec) { return; } @@ -1050,6 +1089,7 @@ export async function confirmPeerPushCredit( rec.status = PeerPushCreditStatus.PendingMerge; } await tx.peerPushCredit.put(rec); + await ctx.updateTransactionMeta(tx); return rec; }, ); @@ -1063,17 +1103,10 @@ export async function confirmPeerPushCredit( const exchange = await fetchFreshExchange(wex, peerInc.exchangeBaseUrl); requireExchangeTosAcceptedOrThrow(exchange); - const ctx = new PeerPushCreditTransactionContext(wex, peerPushCreditId); - wex.taskScheduler.startShepherdTask(ctx.taskId); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPushCredit, - peerPushCreditId, - }); - return { - transactionId, + transactionId: ctx.transactionId, }; } diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts @@ -74,6 +74,7 @@ import { PeerPushDebitStatus, RefreshOperationStatus, WalletDbAllStoresReadOnlyTransaction, + WalletDbReadWriteTransaction, timestampPreciseFromDb, timestampPreciseToDb, timestampProtocolFromDb, @@ -113,6 +114,23 @@ export class PeerPushDebitTransactionContext implements TransactionContext { }); } + async updateTransactionMeta( + tx: WalletDbReadWriteTransaction<["peerPushDebit", "transactionsMeta"]>, + ): Promise<void> { + const ppdRec = await tx.peerPushDebit.get(this.pursePub); + if (!ppdRec) { + await tx.transactionsMeta.delete(this.transactionId); + return; + } + await tx.transactionsMeta.put({ + transactionId: this.transactionId, + status: ppdRec.status, + timestamp: ppdRec.timestampCreated, + currency: Amounts.currencyOf(ppdRec.amount), + exchanges: [ppdRec.exchangeBaseUrl], + }); + } + /** * Get the full transaction details for the transaction. * @@ -129,7 +147,10 @@ export class PeerPushDebitTransactionContext implements TransactionContext { 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}`); + checkDbInvariant( + !!ctRec, + `no contract terms for p2p push ${this.pursePub}`, + ); const contractTerms = ctRec.contractTermsRaw; @@ -169,11 +190,12 @@ export class PeerPushDebitTransactionContext implements TransactionContext { async deleteTransaction(): Promise<void> { const { wex, pursePub, transactionId } = this; await wex.db.runReadWriteTx( - { storeNames: ["peerPushDebit", "tombstones"] }, + { storeNames: ["peerPushDebit", "tombstones", "transactionsMeta"] }, async (tx) => { const debit = await tx.peerPushDebit.get(pursePub); if (debit) { await tx.peerPushDebit.delete(pursePub); + await this.updateTransactionMeta(tx); await tx.tombstones.put({ id: transactionId }); } }, @@ -183,7 +205,7 @@ export class PeerPushDebitTransactionContext implements TransactionContext { async suspendTransaction(): Promise<void> { const { wex, pursePub, transactionId, taskId: retryTag } = this; const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPushDebit"] }, + { storeNames: ["peerPushDebit", "transactionsMeta"] }, async (tx) => { const pushDebitRec = await tx.peerPushDebit.get(pursePub); if (!pushDebitRec) { @@ -226,6 +248,7 @@ export class PeerPushDebitTransactionContext implements TransactionContext { pushDebitRec.status = newStatus; const newTxState = computePeerPushDebitTransactionState(pushDebitRec); await tx.peerPushDebit.put(pushDebitRec); + await this.updateTransactionMeta(tx); return { oldTxState, newTxState, @@ -241,7 +264,7 @@ export class PeerPushDebitTransactionContext implements TransactionContext { async abortTransaction(): Promise<void> { const { wex, pursePub, transactionId, taskId: retryTag } = this; const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPushDebit"] }, + { storeNames: ["peerPushDebit", "transactionsMeta"] }, async (tx) => { const pushDebitRec = await tx.peerPushDebit.get(pursePub); if (!pushDebitRec) { @@ -279,6 +302,7 @@ export class PeerPushDebitTransactionContext implements TransactionContext { pushDebitRec.status = newStatus; const newTxState = computePeerPushDebitTransactionState(pushDebitRec); await tx.peerPushDebit.put(pushDebitRec); + await this.updateTransactionMeta(tx); return { oldTxState, newTxState, @@ -295,7 +319,7 @@ export class PeerPushDebitTransactionContext implements TransactionContext { async resumeTransaction(): Promise<void> { const { wex, pursePub, transactionId, taskId: retryTag } = this; const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPushDebit"] }, + { storeNames: ["peerPushDebit", "transactionsMeta"] }, async (tx) => { const pushDebitRec = await tx.peerPushDebit.get(pursePub); if (!pushDebitRec) { @@ -338,6 +362,7 @@ export class PeerPushDebitTransactionContext implements TransactionContext { pushDebitRec.status = newStatus; const newTxState = computePeerPushDebitTransactionState(pushDebitRec); await tx.peerPushDebit.put(pushDebitRec); + await this.updateTransactionMeta(tx); return { oldTxState, newTxState, @@ -353,7 +378,7 @@ export class PeerPushDebitTransactionContext implements TransactionContext { async failTransaction(): Promise<void> { const { wex, pursePub, transactionId, taskId: retryTag } = this; const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPushDebit"] }, + { storeNames: ["peerPushDebit", "transactionsMeta"] }, async (tx) => { const pushDebitRec = await tx.peerPushDebit.get(pursePub); if (!pushDebitRec) { @@ -391,6 +416,7 @@ export class PeerPushDebitTransactionContext implements TransactionContext { pushDebitRec.status = newStatus; const newTxState = computePeerPushDebitTransactionState(pushDebitRec); await tx.peerPushDebit.put(pushDebitRec); + await this.updateTransactionMeta(tx); return { oldTxState, newTxState, @@ -516,26 +542,30 @@ async function handlePurseCreationConflict( assertUnreachable(coinSelRes); } - await wex.db.runReadWriteTx({ storeNames: ["peerPushDebit"] }, async (tx) => { - const myPpi = await tx.peerPushDebit.get(peerPushInitiation.pursePub); - if (!myPpi) { - return; - } - switch (myPpi.status) { - case PeerPushDebitStatus.PendingCreatePurse: - case PeerPushDebitStatus.SuspendedCreatePurse: { - const sel = coinSelRes.result; - myPpi.coinSel = { - coinPubs: sel.coins.map((x) => x.coinPub), - contributions: sel.coins.map((x) => x.contribution), - }; - break; - } - default: + await wex.db.runReadWriteTx( + { storeNames: ["peerPushDebit", "transactionsMeta"] }, + async (tx) => { + const myPpi = await tx.peerPushDebit.get(peerPushInitiation.pursePub); + if (!myPpi) { return; - } - await tx.peerPushDebit.put(myPpi); - }); + } + switch (myPpi.status) { + case PeerPushDebitStatus.PendingCreatePurse: + case PeerPushDebitStatus.SuspendedCreatePurse: { + const sel = coinSelRes.result; + myPpi.coinSel = { + coinPubs: sel.coins.map((x) => x.coinPub), + contributions: sel.coins.map((x) => x.contribution), + }; + break; + } + default: + return; + } + await tx.peerPushDebit.put(myPpi); + await ctx.updateTransactionMeta(tx); + }, + ); return TaskRunResult.progress(); } @@ -596,6 +626,7 @@ async function processPeerPushDebitCreateReserve( "peerPushDebit", "refreshGroups", "refreshSessions", + "transactionsMeta", ], }, async (tx) => { @@ -624,6 +655,7 @@ async function processPeerPushDebitCreateReserve( }); await tx.peerPushDebit.put(ppi); + await ctx.updateTransactionMeta(tx); return true; }, ); @@ -780,10 +812,7 @@ async function processPeerPushDebitAbortingDeletePurse( peerPushInitiation: PeerPushDebitRecord, ): Promise<TaskRunResult> { const { pursePub, pursePriv } = peerPushInitiation; - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPushDebit, - pursePub, - }); + const ctx = new PeerPushDebitTransactionContext(wex, pursePub); const sigResp = await wex.cryptoApi.signDeletePurse({ pursePriv, @@ -811,6 +840,7 @@ async function processPeerPushDebitAbortingDeletePurse( "peerPushDebit", "refreshGroups", "refreshSessions", + "transactionsMeta", ], }, async (tx) => { @@ -842,11 +872,12 @@ async function processPeerPushDebitAbortingDeletePurse( currency, coinPubs, RefreshReason.AbortPeerPushDebit, - transactionId, + ctx.transactionId, ); ppiRec.status = PeerPushDebitStatus.AbortingRefreshDeleted; ppiRec.abortRefreshGroupId = refresh.refreshGroupId; await tx.peerPushDebit.put(ppiRec); + await ctx.updateTransactionMeta(tx); const newTxState = computePeerPushDebitTransactionState(ppiRec); return { oldTxState, @@ -854,7 +885,7 @@ async function processPeerPushDebitAbortingDeletePurse( }; }, ); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); return TaskRunResult.backoff(); } @@ -870,12 +901,9 @@ async function transitionPeerPushDebitTransaction( pursePub: string, transitionSpec: SimpleTransition, ): Promise<void> { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPushDebit, - pursePub, - }); + const ctx = new PeerPushDebitTransactionContext(wex, pursePub); const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPushDebit"] }, + { storeNames: ["peerPushDebit", "transactionsMeta"] }, async (tx) => { const ppiRec = await tx.peerPushDebit.get(pursePub); if (!ppiRec) { @@ -887,6 +915,7 @@ async function transitionPeerPushDebitTransaction( const oldTxState = computePeerPushDebitTransactionState(ppiRec); ppiRec.status = transitionSpec.stTo; await tx.peerPushDebit.put(ppiRec); + await ctx.updateTransactionMeta(tx); const newTxState = computePeerPushDebitTransactionState(ppiRec); return { oldTxState, @@ -894,7 +923,7 @@ async function transitionPeerPushDebitTransaction( }; }, ); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); } async function processPeerPushDebitAbortingRefreshDeleted( @@ -904,15 +933,12 @@ async function processPeerPushDebitAbortingRefreshDeleted( const pursePub = peerPushInitiation.pursePub; const abortRefreshGroupId = peerPushInitiation.abortRefreshGroupId; checkLogicInvariant(!!abortRefreshGroupId); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPushDebit, - pursePub: peerPushInitiation.pursePub, - }); + const ctx = new PeerPushDebitTransactionContext(wex, pursePub); if (peerPushInitiation.abortRefreshGroupId) { await waitRefreshFinal(wex, peerPushInitiation.abortRefreshGroupId); } const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["refreshGroups", "peerPushDebit"] }, + { storeNames: ["refreshGroups", "peerPushDebit", "transactionsMeta"] }, async (tx) => { const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId); let newOpState: PeerPushDebitStatus | undefined; @@ -939,12 +965,13 @@ async function processPeerPushDebitAbortingRefreshDeleted( newDg.status = newOpState; const newTxState = computePeerPushDebitTransactionState(newDg); await tx.peerPushDebit.put(newDg); + await ctx.updateTransactionMeta(tx); return { oldTxState, newTxState }; } return undefined; }, ); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); // FIXME: Shouldn't this be finished in some cases?! return TaskRunResult.backoff(); } @@ -956,12 +983,9 @@ async function processPeerPushDebitAbortingRefreshExpired( const pursePub = peerPushInitiation.pursePub; const abortRefreshGroupId = peerPushInitiation.abortRefreshGroupId; checkLogicInvariant(!!abortRefreshGroupId); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPushDebit, - pursePub: peerPushInitiation.pursePub, - }); + const ctx = new PeerPushDebitTransactionContext(wex, pursePub); const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPushDebit", "refreshGroups"] }, + { storeNames: ["peerPushDebit", "refreshGroups", "transactionsMeta"] }, async (tx) => { const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId); let newOpState: PeerPushDebitStatus | undefined; @@ -988,12 +1012,13 @@ async function processPeerPushDebitAbortingRefreshExpired( newDg.status = newOpState; const newTxState = computePeerPushDebitTransactionState(newDg); await tx.peerPushDebit.put(newDg); + await ctx.updateTransactionMeta(tx); return { oldTxState, newTxState }; } return undefined; }, ); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); // FIXME: Shouldn't this be finished in some cases?! return TaskRunResult.backoff(); } @@ -1007,10 +1032,7 @@ async function processPeerPushDebitReady( ): Promise<TaskRunResult> { logger.trace("processing peer-push-debit pending(ready)"); const pursePub = peerPushInitiation.pursePub; - const transactionId = constructTaskIdentifier({ - tag: PendingTaskType.PeerPushDebit, - pursePub, - }); + const ctx = new PeerPushDebitTransactionContext(wex, pursePub); const mergeUrl = new URL( `purses/${pursePub}/merge`, peerPushInitiation.exchangeBaseUrl, @@ -1059,6 +1081,7 @@ async function processPeerPushDebitReady( "peerPushDebit", "refreshGroups", "refreshSessions", + "transactionsMeta", ], }, async (tx) => { @@ -1087,13 +1110,14 @@ async function processPeerPushDebitReady( currency, coinPubs, RefreshReason.AbortPeerPushDebit, - transactionId, + ctx.transactionId, ); ppiRec.abortRefreshGroupId = refresh.refreshGroupId; } ppiRec.status = PeerPushDebitStatus.AbortingRefreshExpired; await tx.peerPushDebit.put(ppiRec); + await ctx.updateTransactionMeta(tx); const newTxState = computePeerPushDebitTransactionState(ppiRec); return { oldTxState, @@ -1101,7 +1125,7 @@ async function processPeerPushDebitReady( }; }, ); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); return TaskRunResult.backoff(); } else { logger.warn(`unexpected HTTP status for purse: ${resp.status}`); @@ -1196,6 +1220,7 @@ export async function initiatePeerPushDebit( "peerPushDebit", "refreshGroups", "refreshSessions", + "transactionsMeta", ], }, async (tx) => { @@ -1262,6 +1287,7 @@ export async function initiatePeerPushDebit( } await tx.peerPushDebit.add(ppi); + await ctx.updateTransactionMeta(tx); await tx.contractTerms.put({ h: hContractTerms, diff --git a/packages/taler-wallet-core/src/recoup.ts b/packages/taler-wallet-core/src/recoup.ts @@ -76,11 +76,19 @@ export const logger = new Logger("operations/recoup.ts"); export async function putGroupAsFinished( wex: WalletExecutionContext, tx: WalletDbReadWriteTransaction< - ["recoupGroups", "denominations", "refreshGroups", "coins"] + [ + "recoupGroups", + "denominations", + "refreshGroups", + "coins", + "exchanges", + "transactionsMeta", + ] >, recoupGroup: RecoupGroupRecord, coinIdx: number, ): Promise<void> { + const ctx = new RecoupTransactionContext(wex, recoupGroup.recoupGroupId); logger.trace( `setting coin ${coinIdx} of ${recoupGroup.coinPubs.length} as finished`, ); @@ -89,6 +97,7 @@ export async function putGroupAsFinished( } recoupGroup.recoupFinishedPerCoin[coinIdx] = true; await tx.recoupGroups.put(recoupGroup); + await ctx.updateTransactionMeta(tx); } async function recoupRewardCoin( @@ -101,7 +110,16 @@ async function recoupRewardCoin( // Thus we just put the coin to sleep. // FIXME: somehow report this to the user await wex.db.runReadWriteTx( - { storeNames: ["recoupGroups", "denominations", "refreshGroups", "coins"] }, + { + storeNames: [ + "recoupGroups", + "denominations", + "refreshGroups", + "coins", + "exchanges", + "transactionsMeta", + ], + }, async (tx) => { const recoupGroup = await tx.recoupGroups.get(recoupGroupId); if (!recoupGroup) { @@ -170,7 +188,16 @@ async function recoupRefreshCoin( } await wex.db.runReadWriteTx( - { storeNames: ["coins", "denominations", "recoupGroups", "refreshGroups"] }, + { + storeNames: [ + "coins", + "denominations", + "recoupGroups", + "refreshGroups", + "transactionsMeta", + "exchanges", + ], + }, async (tx) => { const recoupGroup = await tx.recoupGroups.get(recoupGroupId); if (!recoupGroup) { @@ -274,7 +301,16 @@ export async function recoupWithdrawCoin( // FIXME: verify that our expectations about the amount match await wex.db.runReadWriteTx( - { storeNames: ["coins", "denominations", "recoupGroups", "refreshGroups"] }, + { + storeNames: [ + "coins", + "denominations", + "recoupGroups", + "refreshGroups", + "transactionsMeta", + "exchanges", + ], + }, async (tx) => { const recoupGroup = await tx.recoupGroups.get(recoupGroupId); if (!recoupGroup) { @@ -395,6 +431,8 @@ export async function processRecoupGroup( }); } + const ctx = new RecoupTransactionContext(wex, recoupGroupId); + await wex.db.runReadWriteTx( { storeNames: [ @@ -405,6 +443,8 @@ export async function processRecoupGroup( "recoupGroups", "refreshGroups", "refreshSessions", + "transactionsMeta", + "exchanges", ], }, async (tx) => { @@ -428,6 +468,7 @@ export async function processRecoupGroup( ); } await tx.recoupGroups.put(rg2); + await ctx.updateTransactionMeta(tx); }, ); return TaskRunResult.finished(); @@ -451,6 +492,30 @@ export class RecoupTransactionContext implements TransactionContext { }); } + async updateTransactionMeta( + tx: WalletDbReadWriteTransaction< + ["recoupGroups", "exchanges", "transactionsMeta"] + >, + ): Promise<void> { + const recoupRec = await tx.recoupGroups.get(this.recoupGroupId); + if (!recoupRec) { + await tx.transactionsMeta.delete(this.transactionId); + return; + } + const exch = await tx.exchanges.get(recoupRec.exchangeBaseUrl); + if (!exch || !exch.detailsPointer) { + await tx.transactionsMeta.delete(this.transactionId); + return; + } + await tx.transactionsMeta.put({ + transactionId: this.transactionId, + status: recoupRec.operationStatus, + timestamp: recoupRec.timestampStarted, + currency: exch.detailsPointer?.currency, + exchanges: [recoupRec.exchangeBaseUrl], + }); + } + abortTransaction(): Promise<void> { throw new Error("Method not implemented."); } @@ -481,13 +546,21 @@ export class RecoupTransactionContext implements TransactionContext { export async function createRecoupGroup( wex: WalletExecutionContext, tx: WalletDbReadWriteTransaction< - ["recoupGroups", "denominations", "refreshGroups", "coins"] + [ + "recoupGroups", + "denominations", + "refreshGroups", + "coins", + "exchanges", + "transactionsMeta", + ] >, exchangeBaseUrl: string, coinPubs: string[], ): Promise<string> { const recoupGroupId = encodeCrock(getRandomBytes(32)); + const ctx = new RecoupTransactionContext(wex, recoupGroupId); const recoupGroup: RecoupGroupRecord = { recoupGroupId, exchangeBaseUrl: exchangeBaseUrl, @@ -510,8 +583,7 @@ export async function createRecoupGroup( } await tx.recoupGroups.put(recoupGroup); - - const ctx = new RecoupTransactionContext(wex, recoupGroupId); + await ctx.updateTransactionMeta(tx); wex.taskScheduler.startShepherdTask(ctx.taskId); diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts @@ -125,23 +125,6 @@ import { getCandidateWithdrawalDenomsTx } from "./withdraw.js"; const logger = new Logger("refresh.ts"); -/** - * Update the materialized refresh transaction based - * on the refresh group record. - */ -async function updateRefreshTransaction( - ctx: RefreshTransactionContext, - tx: WalletDbReadWriteTransaction< - [ - "refreshGroups", - "transactionsMeta", - "operationRetries", - "exchanges", - "exchangeDetails", - ] - >, -): Promise<void> {} - export class RefreshTransactionContext implements TransactionContext { readonly transactionId: TransactionIdStr; readonly taskId: TaskIdStr; @@ -160,6 +143,23 @@ export class RefreshTransactionContext implements TransactionContext { }); } + async updateTransactionMeta( + tx: WalletDbReadWriteTransaction<["refreshGroups", "transactionsMeta"]>, + ): Promise<void> { + const rgRec = await tx.refreshGroups.get(this.refreshGroupId); + if (!rgRec) { + await tx.transactionsMeta.delete(this.transactionId); + return; + } + await tx.transactionsMeta.put({ + transactionId: this.transactionId, + status: rgRec.operationStatus, + timestamp: rgRec.timestampCreated, + currency: rgRec.currency, + exchanges: Object.keys(rgRec.infoPerExchange ?? {}), + }); + } + /** * Get the full transaction details for the transaction. * @@ -252,7 +252,7 @@ export class RefreshTransactionContext implements TransactionContext { switch (res.type) { case TransitionResultType.Transition: { await tx.refreshGroups.put(res.rec); - await updateRefreshTransaction(this, tx); + await this.updateTransactionMeta(tx); const newTxState = computeRefreshTransactionState(res.rec); return { oldTxState, @@ -261,7 +261,7 @@ export class RefreshTransactionContext implements TransactionContext { } case TransitionResultType.Delete: await tx.refreshGroups.delete(this.refreshGroupId); - await updateRefreshTransaction(this, tx); + await this.updateTransactionMeta(tx); return { oldTxState, newTxState: { @@ -822,6 +822,7 @@ async function handleRefreshMeltGone( "coins", "denominations", "coinAvailability", + "transactionsMeta", ], }, async (tx) => { @@ -846,6 +847,7 @@ async function handleRefreshMeltGone( refreshSession.lastError = errDetails; await destroyRefreshSession(ctx.wex, tx, rg, refreshSession); await tx.refreshGroups.put(rg); + await ctx.updateTransactionMeta(tx); await tx.refreshSessions.put(refreshSession); }, ); @@ -901,6 +903,7 @@ async function handleRefreshMeltConflict( "denominations", "coins", "coinAvailability", + "transactionsMeta", ], }, async (tx) => { @@ -927,6 +930,7 @@ async function handleRefreshMeltConflict( } refreshSession.lastError = errDetails; await tx.refreshGroups.put(rg); + await ctx.updateTransactionMeta(tx); await tx.refreshSessions.put(refreshSession); } else { // Try again with new denoms! @@ -971,6 +975,7 @@ async function handleRefreshMeltNotFound( "coins", "denominations", "coinAvailability", + "transactionsMeta", ], }, async (tx) => { @@ -995,6 +1000,7 @@ async function handleRefreshMeltNotFound( await destroyRefreshSession(ctx.wex, tx, rg, refreshSession); refreshSession.lastError = errDetails; await tx.refreshGroups.put(rg); + await ctx.updateTransactionMeta(tx); await tx.refreshSessions.put(refreshSession); }, ); @@ -1275,6 +1281,7 @@ async function refreshReveal( "coinAvailability", "refreshGroups", "refreshSessions", + "transactionsMeta", ], }, async (tx) => { @@ -1323,6 +1330,7 @@ async function refreshReveal( await tx.coinAvailability.put(car); } await tx.refreshGroups.put(rg); + await ctx.updateTransactionMeta(tx); }, ); logger.trace("refresh finished (end of reveal)"); @@ -1341,6 +1349,7 @@ async function handleRefreshRevealError( "coins", "denominations", "coinAvailability", + "transactionsMeta", ], }, async (tx) => { @@ -1366,6 +1375,7 @@ async function handleRefreshRevealError( await destroyRefreshSession(ctx.wex, tx, rg, refreshSession); await tx.refreshGroups.put(rg); await tx.refreshSessions.put(refreshSession); + await ctx.updateTransactionMeta(tx); }, ); } @@ -1438,7 +1448,14 @@ export async function processRefreshGroup( // status of the whole refresh group. const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["coins", "coinAvailability", "refreshGroups"] }, + { + storeNames: [ + "coins", + "coinAvailability", + "refreshGroups", + "transactionsMeta", + ], + }, async (tx) => { const rg = await tx.refreshGroups.get(refreshGroupId); if (!rg) { @@ -1474,6 +1491,7 @@ export async function processRefreshGroup( } await makeCoinsVisible(wex, tx, ctx.transactionId); await tx.refreshGroups.put(rg); + await ctx.updateTransactionMeta(tx); const newTxState = computeRefreshTransactionState(rg); return { oldTxState, @@ -1711,6 +1729,7 @@ export async function createRefreshGroup( "refreshGroups", "refreshSessions", "coinAvailability", + "transactionsMeta", ] >, currency: string, @@ -1758,14 +1777,15 @@ export async function createRefreshGroup( await initRefreshSession(wex, tx, refreshGroup, i); } + const ctx = new RefreshTransactionContext(wex, refreshGroupId); + await tx.refreshGroups.put(refreshGroup); + await ctx.updateTransactionMeta(tx); const newTxState = computeRefreshTransactionState(refreshGroup); logger.trace(`created refresh group ${refreshGroupId}`); - const ctx = new RefreshTransactionContext(wex, refreshGroupId); - // Shepherd the task. // If the current transaction fails to commit the refresh // group to the DB, the shepherd will give up. @@ -1866,6 +1886,7 @@ export async function forceRefresh( "denominations", "coins", "coinHistory", + "transactionsMeta", ], }, async (tx) => { diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts @@ -352,9 +352,6 @@ export class WithdrawTransactionContext implements TransactionContext { [ "withdrawalGroups", "transactionsMeta", - "operationRetries", - "exchanges", - "exchangeDetails", ] >, ): Promise<void> {