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:
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> {