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