wallet-core/packages/taler-wallet-core/src/operations/transactions.ts

1016 lines
33 KiB
TypeScript
Raw Normal View History

2020-05-12 10:38:58 +02:00
/*
This file is part of GNU Taler
(C) 2019 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Imports.
*/
import {
2022-03-18 15:32:41 +01:00
AbsoluteTime,
addPaytoQueryParams, AmountJson, Amounts,
constructPayPullUri,
constructPayPushUri,
Logger,
2022-09-14 13:37:33 +02:00
OrderShortInfo, PaymentStatus,
RefundInfoShort,
TalerProtocolTimestamp,
Transaction,
TransactionByIdRequest,
TransactionRefund,
TransactionsRequest,
TransactionsResponse,
TransactionType,
WithdrawalDetails,
2022-09-14 13:37:33 +02:00
WithdrawalType
} from "@gnu-taler/taler-util";
import {
AbortStatus,
DepositGroupRecord,
ExchangeDetailsRecord,
OperationRetryRecord,
PeerPullPaymentIncomingRecord,
PeerPushPaymentInitiationRecord,
PurchaseRecord,
RefundState,
TipRecord,
WalletRefundItem,
WithdrawalGroupRecord,
2022-09-14 13:37:33 +02:00
WithdrawalRecordType
2021-03-17 17:56:37 +01:00
} from "../db.js";
2022-09-14 13:37:33 +02:00
import { InternalWalletState } from "../internal-wallet-state.js";
import { RetryTags } from "../util/retries.js";
import { processDepositGroup } from "./deposits.js";
import { getExchangeDetails } from "./exchanges.js";
2021-06-14 19:37:35 +02:00
import { processPurchasePay } from "./pay.js";
import { processRefreshGroup } from "./refresh.js";
import { applyRefundFromPurchaseId } from "./refund.js";
import { processTip } from "./tip.js";
2022-08-09 15:00:45 +02:00
import { processWithdrawalGroup } from "./withdraw.js";
2020-05-12 10:38:58 +02:00
const logger = new Logger("taler-wallet-core:transactions.ts");
2022-05-14 23:09:33 +02:00
export enum TombstoneTag {
DeleteWithdrawalGroup = "delete-withdrawal-group",
DeleteReserve = "delete-reserve",
DeletePayment = "delete-payment",
DeleteTip = "delete-tip",
DeleteRefreshGroup = "delete-refresh-group",
DeleteDepositGroup = "delete-deposit-group",
DeleteRefund = "delete-refund",
DeletePeerPullDebit = "delete-peer-pull-debit",
DeletePeerPushDebit = "delete-peer-push-debit",
2022-05-14 23:09:33 +02:00
}
2020-05-12 10:38:58 +02:00
/**
* Create an event ID from the type and the primary key for the event.
*/
2021-05-21 11:47:11 +02:00
export function makeEventId(
type: TransactionType | TombstoneTag,
...args: string[]
): string {
return type + ":" + args.map((x) => encodeURIComponent(x)).join(":");
2020-05-12 10:38:58 +02:00
}
2020-05-15 13:28:15 +02:00
function shouldSkipCurrency(
transactionsRequest: TransactionsRequest | undefined,
currency: string,
): boolean {
if (!transactionsRequest?.currency) {
return false;
}
return transactionsRequest.currency.toLowerCase() !== currency.toLowerCase();
}
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.
*/
const txOrder: { [t in TransactionType]: number } = {
[TransactionType.Withdrawal]: 1,
[TransactionType.Tip]: 2,
[TransactionType.Payment]: 3,
[TransactionType.PeerPullCredit]: 4,
[TransactionType.PeerPullDebit]: 5,
[TransactionType.PeerPushCredit]: 6,
[TransactionType.PeerPushDebit]: 7,
[TransactionType.Refund]: 8,
[TransactionType.Deposit]: 9,
[TransactionType.Refresh]: 10,
[TransactionType.Tip]: 11,
};
export async function getTransactionById(
ws: InternalWalletState,
req: TransactionByIdRequest,
): Promise<Transaction> {
const [typeStr, ...rest] = req.transactionId.split(":");
const type = typeStr as TransactionType;
if (
type === TransactionType.Withdrawal ||
type === TransactionType.PeerPullCredit ||
type === TransactionType.PeerPushCredit
) {
const withdrawalGroupId = rest[0];
return await ws.db
.mktx((x) => [x.withdrawalGroups, x.exchangeDetails, x.exchanges, x.operationRetries])
.runReadWrite(async (tx) => {
const withdrawalGroupRecord = await tx.withdrawalGroups.get(
withdrawalGroupId,
);
if (!withdrawalGroupRecord) throw Error("not found")
const opId = RetryTags.forWithdrawal(withdrawalGroupRecord);
const ort = await tx.operationRetries.get(opId);
if (withdrawalGroupRecord.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated) {
return buildTransactionForBankIntegratedWithdraw(withdrawalGroupRecord, ort);
}
if (withdrawalGroupRecord.wgInfo.withdrawalType === WithdrawalRecordType.PeerPullCredit) {
return buildTransactionForPullPaymentCredit(withdrawalGroupRecord, ort);
}
if (withdrawalGroupRecord.wgInfo.withdrawalType === WithdrawalRecordType.PeerPushCredit) {
return buildTransactionForPushPaymentCredit(withdrawalGroupRecord, ort);
}
const exchangeDetails = await getExchangeDetails(tx, withdrawalGroupRecord.exchangeBaseUrl,);
if (!exchangeDetails) throw Error('not exchange details')
return buildTransactionForManualWithdraw(withdrawalGroupRecord, exchangeDetails, ort);
});
} else if (type === TransactionType.Payment) {
const proposalId = rest[0];
return await ws.db
.mktx((x) => [x.purchases, x.tombstones, x.operationRetries])
.runReadWrite(async (tx) => {
const purchase = await tx.purchases.get(proposalId);
if (!purchase) throw Error("not found")
const filteredRefunds = await Promise.all(Object.values(purchase.refunds).map(async r => {
const t = await tx.tombstones.get(makeEventId(
TombstoneTag.DeleteRefund,
purchase.proposalId,
`${r.executionTime.t_s}`,
))
if (!t) return r
return undefined
}));
const cleanRefunds = filteredRefunds.filter((x): x is WalletRefundItem => !!x);
const contractData = purchase.download.contractData;
const refunds = mergeRefundByExecutionTime(cleanRefunds, Amounts.getZero(contractData.amount.currency));
const payOpId = RetryTags.forPay(purchase);
const refundQueryOpId = RetryTags.forRefundQuery(purchase);
const payRetryRecord = await tx.operationRetries.get(payOpId);
const refundQueryRetryRecord = await tx.operationRetries.get(
refundQueryOpId,
);
const err = payRetryRecord !== undefined ? payRetryRecord : refundQueryRetryRecord
return buildTransactionForPurchase(purchase, refunds, err);
});
} else if (type === TransactionType.Refresh) {
const refreshGroupId = rest[0];
throw Error(`no tx for refresh`);
} else if (type === TransactionType.Tip) {
const tipId = rest[0];
return await ws.db
.mktx((x) => [x.tips, x.operationRetries])
.runReadWrite(async (tx) => {
const tipRecord = await tx.tips.get(tipId);
if (!tipRecord) throw Error("not found")
const retries = await tx.operationRetries.get(RetryTags.forTipPickup(tipRecord));
return buildTransactionForTip(tipRecord, retries)
});
} else if (type === TransactionType.Deposit) {
const depositGroupId = rest[0];
return await ws.db
.mktx((x) => [x.depositGroups, x.operationRetries])
.runReadWrite(async (tx) => {
const depositRecord = await tx.depositGroups.get(depositGroupId);
if (!depositRecord) throw Error("not found")
const retries = await tx.operationRetries.get(RetryTags.forDeposit(depositRecord));
return buildTransactionForDeposit(depositRecord, retries)
});
} else if (type === TransactionType.Refund) {
const proposalId = rest[0];
const executionTimeStr = rest[1];
return await ws.db
.mktx((x) => [x.operationRetries, x.purchases, x.tombstones])
.runReadWrite(async (tx) => {
const purchase = await tx.purchases.get(proposalId);
if (!purchase) throw Error("not found")
const theRefund = Object.values(purchase.refunds).find(r => `${r.executionTime.t_s}` === executionTimeStr)
if (!theRefund) throw Error("not found")
const t = await tx.tombstones.get(makeEventId(
TombstoneTag.DeleteRefund,
purchase.proposalId,
executionTimeStr,
))
if (t) throw Error("deleted")
const contractData = purchase.download.contractData;
const refunds = mergeRefundByExecutionTime([theRefund], Amounts.getZero(contractData.amount.currency))
const refundQueryOpId = RetryTags.forRefundQuery(purchase);
const refundQueryRetryRecord = await tx.operationRetries.get(
refundQueryOpId,
);
return buildTransactionForRefund(purchase, refunds[0], refundQueryRetryRecord);
});
} else if (type === TransactionType.PeerPullDebit) {
const peerPullPaymentIncomingId = rest[0];
return await ws.db
.mktx((x) => [x.peerPullPaymentIncoming])
.runReadWrite(async (tx) => {
const debit = await tx.peerPullPaymentIncoming.get(
peerPullPaymentIncomingId,
);
if (!debit) throw Error("not found");
return buildTransactionForPullPaymentDebit(debit)
});
} else if (type === TransactionType.PeerPushDebit) {
const pursePub = rest[0];
return await ws.db
.mktx((x) => [x.peerPushPaymentInitiations])
.runReadWrite(async (tx) => {
const debit = await tx.peerPushPaymentInitiations.get(pursePub);
if (!debit) throw Error("not found");
return buildTransactionForPushPaymentDebit(debit)
});
} else {
const unknownTxType: never = type;
throw Error(`can't delete a '${unknownTxType}' transaction`);
}
}
function buildTransactionForPushPaymentDebit(pi: PeerPushPaymentInitiationRecord, ort?: OperationRetryRecord): Transaction {
return {
type: TransactionType.PeerPushDebit,
amountEffective: pi.amount,
amountRaw: pi.amount,
exchangeBaseUrl: pi.exchangeBaseUrl,
info: {
expiration: pi.contractTerms.purse_expiration,
summary: pi.contractTerms.summary,
},
frozen: false,
pending: !pi.purseCreated,
timestamp: pi.timestampCreated,
talerUri: constructPayPushUri({
exchangeBaseUrl: pi.exchangeBaseUrl,
contractPriv: pi.contractPriv,
}),
transactionId: makeEventId(
TransactionType.PeerPushDebit,
pi.pursePub,
),
...(ort?.lastError ? { error: ort.lastError } : {}),
};
}
function buildTransactionForPullPaymentDebit(pi: PeerPullPaymentIncomingRecord, ort?: OperationRetryRecord): Transaction {
return {
type: TransactionType.PeerPullDebit,
amountEffective: Amounts.stringify(pi.contractTerms.amount),
amountRaw: Amounts.stringify(pi.contractTerms.amount),
exchangeBaseUrl: pi.exchangeBaseUrl,
frozen: false,
pending: false,
info: {
expiration: pi.contractTerms.purse_expiration,
summary: pi.contractTerms.summary,
},
timestamp: pi.timestampCreated,
transactionId: makeEventId(
TransactionType.PeerPullDebit,
pi.peerPullPaymentIncomingId,
),
...(ort?.lastError ? { error: ort.lastError } : {}),
}
}
function buildTransactionForPullPaymentCredit(wsr: WithdrawalGroupRecord, ort?: OperationRetryRecord): Transaction {
if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPullCredit) throw Error("")
return {
type: TransactionType.PeerPullCredit,
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
amountRaw: Amounts.stringify(wsr.rawWithdrawalAmount),
exchangeBaseUrl: wsr.exchangeBaseUrl,
pending: !wsr.timestampFinish,
timestamp: wsr.timestampStart,
info: {
expiration: wsr.wgInfo.contractTerms.purse_expiration,
summary: wsr.wgInfo.contractTerms.summary,
},
talerUri: constructPayPullUri({
exchangeBaseUrl: wsr.exchangeBaseUrl,
contractPriv: wsr.wgInfo.contractPriv,
}),
transactionId: makeEventId(
TransactionType.PeerPullCredit,
wsr.withdrawalGroupId,
),
frozen: false,
...(ort?.lastError ? { error: ort.lastError } : {}),
}
}
function buildTransactionForPushPaymentCredit(wsr: WithdrawalGroupRecord, ort?: OperationRetryRecord): Transaction {
if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPushCredit) throw Error("")
return {
type: TransactionType.PeerPushCredit,
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
amountRaw: Amounts.stringify(wsr.rawWithdrawalAmount),
exchangeBaseUrl: wsr.exchangeBaseUrl,
info: {
expiration: wsr.wgInfo.contractTerms.purse_expiration,
summary: wsr.wgInfo.contractTerms.summary,
},
pending: !wsr.timestampFinish,
timestamp: wsr.timestampStart,
transactionId: makeEventId(
TransactionType.PeerPushCredit,
wsr.withdrawalGroupId,
),
frozen: false,
...(ort?.lastError ? { error: ort.lastError } : {}),
}
}
function buildTransactionForBankIntegratedWithdraw(wsr: WithdrawalGroupRecord, ort?: OperationRetryRecord): Transaction {
if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) throw Error("")
return {
type: TransactionType.Withdrawal,
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
amountRaw: Amounts.stringify(wsr.rawWithdrawalAmount),
withdrawalDetails: {
type: WithdrawalType.TalerBankIntegrationApi,
confirmed: wsr.wgInfo.bankInfo.timestampBankConfirmed
? true
: false,
reservePub: wsr.reservePub,
bankConfirmationUrl: wsr.wgInfo.bankInfo.confirmUrl,
},
exchangeBaseUrl: wsr.exchangeBaseUrl,
pending: !wsr.timestampFinish,
timestamp: wsr.timestampStart,
transactionId: makeEventId(
TransactionType.Withdrawal,
wsr.withdrawalGroupId,
),
frozen: false,
...(ort?.lastError ? { error: ort.lastError } : {}),
}
}
function buildTransactionForManualWithdraw(wsr: WithdrawalGroupRecord, exchangeDetails: ExchangeDetailsRecord, ort?: OperationRetryRecord): Transaction {
if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.BankManual) throw Error("")
return {
type: TransactionType.Withdrawal,
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
amountRaw: Amounts.stringify(wsr.rawWithdrawalAmount),
withdrawalDetails: {
type: WithdrawalType.ManualTransfer,
reservePub: wsr.reservePub,
exchangePaytoUris:
exchangeDetails.wireInfo?.accounts.map(
(x) => addPaytoQueryParams(x.payto_uri, { subject: wsr.reservePub }),
) ?? [],
},
exchangeBaseUrl: wsr.exchangeBaseUrl,
pending: !wsr.timestampFinish,
timestamp: wsr.timestampStart,
transactionId: makeEventId(
TransactionType.Withdrawal,
wsr.withdrawalGroupId,
),
frozen: false,
...(ort?.lastError ? { error: ort.lastError } : {}),
}
}
function buildTransactionForDeposit(dg: DepositGroupRecord, ort?: OperationRetryRecord): Transaction {
return {
type: TransactionType.Deposit,
amountRaw: Amounts.stringify(dg.effectiveDepositAmount),
amountEffective: Amounts.stringify(dg.totalPayCost),
pending: !dg.timestampFinished,
frozen: false,
timestamp: dg.timestampCreated,
targetPaytoUri: dg.wire.payto_uri,
transactionId: makeEventId(
TransactionType.Deposit,
dg.depositGroupId,
),
depositGroupId: dg.depositGroupId,
...(ort?.lastError ? { error: ort.lastError } : {}),
}
}
function buildTransactionForTip(tipRecord: TipRecord, ort?: OperationRetryRecord): Transaction {
if (!tipRecord.acceptedTimestamp) throw Error("")
return {
type: TransactionType.Tip,
amountEffective: Amounts.stringify(tipRecord.tipAmountEffective),
amountRaw: Amounts.stringify(tipRecord.tipAmountRaw),
pending: !tipRecord.pickedUpTimestamp,
frozen: false,
timestamp: tipRecord.acceptedTimestamp,
transactionId: makeEventId(
TransactionType.Tip,
tipRecord.walletTipId,
),
merchantBaseUrl: tipRecord.merchantBaseUrl,
...(ort?.lastError ? { error: ort.lastError } : {}),
}
}
/**
* For a set of refund with the same executionTime.
*
*/
interface MergedRefundInfo {
executionTime: TalerProtocolTimestamp;
amountAppliedRaw: AmountJson;
amountAppliedEffective: AmountJson;
firstTimestamp: TalerProtocolTimestamp;
}
function mergeRefundByExecutionTime(rs: WalletRefundItem[], zero: AmountJson): MergedRefundInfo[] {
const refundByExecTime = rs.reduce((prev, refund) => {
const key = `${refund.executionTime.t_s}`;
//refunds counts if applied
const effective = refund.type === RefundState.Applied ? Amounts.sub(
refund.refundAmount,
refund.refundFee,
refund.totalRefreshCostBound,
).amount : zero
const raw = refund.type === RefundState.Applied ? refund.refundAmount : zero
const v = prev.get(key)
if (!v) {
prev.set(key, {
executionTime: refund.executionTime,
amountAppliedEffective: effective,
amountAppliedRaw: raw,
firstTimestamp: refund.obtainedTime
})
} else {
//v.executionTime is the same
v.amountAppliedEffective = Amounts.add(v.amountAppliedEffective, effective).amount;
v.amountAppliedRaw = Amounts.add(v.amountAppliedRaw).amount
v.firstTimestamp = TalerProtocolTimestamp.min(v.firstTimestamp, refund.obtainedTime);
}
return prev
}, {} as Map<string, MergedRefundInfo>);
return Array.from(refundByExecTime.values());
}
function buildTransactionForRefund(purchaseRecord: PurchaseRecord, refundInfo: MergedRefundInfo, ort?: OperationRetryRecord): Transaction {
const contractData = purchaseRecord.download.contractData;
const info: OrderShortInfo = {
merchant: contractData.merchant,
orderId: contractData.orderId,
products: contractData.products,
summary: contractData.summary,
summary_i18n: contractData.summaryI18n,
contractTermsHash: contractData.contractTermsHash,
};
if (contractData.fulfillmentUrl !== "") {
info.fulfillmentUrl = contractData.fulfillmentUrl;
}
return {
type: TransactionType.Refund,
info,
refundedTransactionId: makeEventId(
TransactionType.Payment,
purchaseRecord.proposalId,
),
transactionId: makeEventId(
TransactionType.Refund,
purchaseRecord.proposalId,
`${refundInfo.executionTime.t_s}`,
),
timestamp: refundInfo.firstTimestamp,
amountEffective: Amounts.stringify(refundInfo.amountAppliedEffective),
amountRaw: Amounts.stringify(refundInfo.amountAppliedRaw),
refundPending:
purchaseRecord.refundAwaiting === undefined
? undefined
: Amounts.stringify(purchaseRecord.refundAwaiting),
pending: false,
frozen: false,
...(ort?.lastError ? { error: ort.lastError } : {}),
}
}
function buildTransactionForPurchase(purchaseRecord: PurchaseRecord, refundsInfo: MergedRefundInfo[], ort?: OperationRetryRecord): Transaction {
const contractData = purchaseRecord.download.contractData;
const zero = Amounts.getZero(contractData.amount.currency)
const info: OrderShortInfo = {
merchant: contractData.merchant,
orderId: contractData.orderId,
products: contractData.products,
summary: contractData.summary,
summary_i18n: contractData.summaryI18n,
contractTermsHash: contractData.contractTermsHash,
};
if (contractData.fulfillmentUrl !== "") {
info.fulfillmentUrl = contractData.fulfillmentUrl;
}
const totalRefund = refundsInfo.reduce((prev, cur) => {
return {
raw: Amounts.add(prev.raw, cur.amountAppliedRaw).amount,
effective: Amounts.add(prev.effective, cur.amountAppliedEffective).amount,
}
}, {
raw: zero, effective: zero
} as { raw: AmountJson, effective: AmountJson })
const refunds: RefundInfoShort[] = refundsInfo.map(r => ({
amountEffective: Amounts.stringify(r.amountAppliedEffective),
amountRaw: Amounts.stringify(r.amountAppliedRaw),
timestamp: r.executionTime,
transactionId: makeEventId(
TransactionType.Refund,
purchaseRecord.proposalId,
`${r.executionTime.t_s}`
),
}))
return {
type: TransactionType.Payment,
amountRaw: Amounts.stringify(contractData.amount),
amountEffective: Amounts.stringify(purchaseRecord.totalPayCost),
totalRefundRaw: Amounts.stringify(totalRefund.raw),
totalRefundEffective: Amounts.stringify(totalRefund.effective),
refundPending:
purchaseRecord.refundAwaiting === undefined
? undefined
: Amounts.stringify(purchaseRecord.refundAwaiting),
status: purchaseRecord.timestampFirstSuccessfulPay
? PaymentStatus.Paid
: PaymentStatus.Accepted,
pending:
!purchaseRecord.timestampFirstSuccessfulPay &&
purchaseRecord.abortStatus === AbortStatus.None,
refunds,
timestamp: purchaseRecord.timestampAccept,
transactionId: makeEventId(
TransactionType.Payment,
purchaseRecord.proposalId,
),
proposalId: purchaseRecord.proposalId,
info,
frozen: purchaseRecord.payFrozen ?? false,
...(ort?.lastError ? { error: ort.lastError } : {}),
}
}
2020-05-12 10:38:58 +02:00
/**
2021-04-27 23:42:25 +02:00
* Retrieve the full event history for this wallet.
2020-05-12 10:38:58 +02:00
*/
export async function getTransactions(
ws: InternalWalletState,
transactionsRequest?: TransactionsRequest,
): Promise<TransactionsResponse> {
const transactions: Transaction[] = [];
2021-06-09 15:14:17 +02:00
await ws.db
.mktx((x) => [
x.coins,
x.denominations,
x.depositGroups,
x.exchangeDetails,
x.exchanges,
x.operationRetries,
x.peerPullPaymentIncoming,
x.peerPushPaymentInitiations,
x.planchets,
x.proposals,
x.purchases,
x.recoupGroups,
x.tips,
x.tombstones,
x.withdrawalGroups,
])
.runReadOnly(async (tx) => {
tx.peerPushPaymentInitiations.iter().forEachAsync(async (pi) => {
const amount = Amounts.parseOrThrow(pi.amount);
if (shouldSkipCurrency(transactionsRequest, amount.currency)) {
return;
}
if (shouldSkipSearch(transactionsRequest, [])) {
return;
}
transactions.push(buildTransactionForPushPaymentDebit(pi));
});
2020-05-15 13:28:15 +02:00
tx.peerPullPaymentIncoming.iter().forEachAsync(async (pi) => {
const amount = Amounts.parseOrThrow(pi.contractTerms.amount);
if (shouldSkipCurrency(transactionsRequest, amount.currency)) {
return;
}
if (shouldSkipSearch(transactionsRequest, [])) {
return;
}
if (!pi.accepted) {
return;
}
transactions.push(buildTransactionForPullPaymentDebit(pi));
});
tx.withdrawalGroups.iter().forEachAsync(async (wsr) => {
if (
shouldSkipCurrency(
transactionsRequest,
wsr.rawWithdrawalAmount.currency,
)
) {
return;
}
if (shouldSkipSearch(transactionsRequest, [])) {
return;
}
2022-09-05 18:12:30 +02:00
const opId = RetryTags.forWithdrawal(wsr);
const ort = await tx.operationRetries.get(opId);
if (wsr.wgInfo.withdrawalType === WithdrawalRecordType.PeerPullCredit) {
transactions.push(buildTransactionForPullPaymentCredit(wsr, ort));
return;
} else if (
wsr.wgInfo.withdrawalType === WithdrawalRecordType.PeerPushCredit
) {
transactions.push(buildTransactionForPushPaymentCredit(wsr, ort));
return;
} else if (
wsr.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated
) {
transactions.push(buildTransactionForBankIntegratedWithdraw(wsr, ort));
} else {
const exchangeDetails = await getExchangeDetails(
tx,
wsr.exchangeBaseUrl,
);
if (!exchangeDetails) {
// FIXME: report somehow
2021-06-09 15:14:17 +02:00
return;
}
transactions.push(buildTransactionForManualWithdraw(wsr, exchangeDetails, ort));
}
});
tx.depositGroups.iter().forEachAsync(async (dg) => {
const amount = Amounts.parseOrThrow(dg.contractTermsRaw.amount);
if (shouldSkipCurrency(transactionsRequest, amount.currency)) {
return;
}
2022-09-05 18:12:30 +02:00
const opId = RetryTags.forDeposit(dg);
const retryRecord = await tx.operationRetries.get(opId);
transactions.push(buildTransactionForDeposit(dg, retryRecord));
});
tx.purchases.iter().forEachAsync(async (pr) => {
if (
shouldSkipCurrency(
transactionsRequest,
pr.download.contractData.amount.currency,
)
) {
return;
}
const contractData = pr.download.contractData;
if (shouldSkipSearch(transactionsRequest, [contractData.summary])) {
return;
}
const proposal = await tx.proposals.get(pr.proposalId);
if (!proposal) {
return;
}
const filteredRefunds = await Promise.all(Object.values(pr.refunds).map(async r => {
const t = await tx.tombstones.get(makeEventId(
TombstoneTag.DeleteRefund,
pr.proposalId,
`${r.executionTime.t_s}`,
))
if (!t) return r
return undefined
}));
const cleanRefunds = filteredRefunds.filter((x): x is WalletRefundItem => !!x);
const refunds = mergeRefundByExecutionTime(cleanRefunds, Amounts.getZero(contractData.amount.currency));
refunds.forEach(async (refundInfo) => {
const refundQueryOpId = RetryTags.forRefundQuery(pr);
const refundQueryRetryRecord = await tx.operationRetries.get(
refundQueryOpId,
);
2022-05-14 23:09:33 +02:00
transactions.push(
buildTransactionForRefund(pr, refundInfo, refundQueryRetryRecord)
)
})
2022-09-05 18:12:30 +02:00
const payOpId = RetryTags.forPay(pr);
const refundQueryOpId = RetryTags.forRefundQuery(pr);
const payRetryRecord = await tx.operationRetries.get(payOpId);
const refundQueryRetryRecord = await tx.operationRetries.get(
refundQueryOpId,
);
const err = payRetryRecord !== undefined ? payRetryRecord : refundQueryRetryRecord
transactions.push(buildTransactionForPurchase(pr, refunds, err));
});
tx.tips.iter().forEachAsync(async (tipRecord) => {
if (
shouldSkipCurrency(
transactionsRequest,
tipRecord.tipAmountRaw.currency,
)
) {
return;
}
if (!tipRecord.acceptedTimestamp) {
return;
}
2022-09-05 18:12:30 +02:00
const opId = RetryTags.forTipPickup(tipRecord);
const retryRecord = await tx.operationRetries.get(opId);
transactions.push(buildTransactionForTip(tipRecord, retryRecord));
});
});
2020-05-12 10:38:58 +02:00
2020-05-15 20:22:58 +02:00
const txPending = transactions.filter((x) => x.pending);
const txNotPending = transactions.filter((x) => !x.pending);
const txCmp = (h1: Transaction, h2: Transaction) => {
const tsCmp = AbsoluteTime.cmp(
2022-03-18 15:32:41 +01:00
AbsoluteTime.fromTimestamp(h1.timestamp),
AbsoluteTime.fromTimestamp(h2.timestamp),
);
if (tsCmp === 0) {
return Math.sign(txOrder[h1.type] - txOrder[h2.type]);
}
return tsCmp;
};
txPending.sort(txCmp);
txNotPending.sort(txCmp);
2020-05-12 10:38:58 +02:00
return { transactions: [...txNotPending, ...txPending] };
2020-05-12 10:38:58 +02:00
}
2021-06-14 19:37:35 +02:00
/**
* Immediately retry the underlying operation
* of a transaction.
*/
export async function retryTransaction(
ws: InternalWalletState,
transactionId: string,
): Promise<void> {
logger.info(`retrying transaction ${transactionId}`);
2021-06-14 19:37:35 +02:00
const [type, ...rest] = transactionId.split(":");
switch (type) {
2022-05-14 23:09:33 +02:00
case TransactionType.Deposit: {
2021-06-14 19:37:35 +02:00
const depositGroupId = rest[0];
2022-03-28 23:59:16 +02:00
processDepositGroup(ws, depositGroupId, {
forceNow: true,
});
2021-06-14 19:37:35 +02:00
break;
2022-05-14 23:09:33 +02:00
}
case TransactionType.Withdrawal: {
2021-06-14 19:37:35 +02:00
const withdrawalGroupId = rest[0];
2022-08-09 15:00:45 +02:00
await processWithdrawalGroup(ws, withdrawalGroupId, { forceNow: true });
2021-06-14 19:37:35 +02:00
break;
2022-05-14 23:09:33 +02:00
}
case TransactionType.Payment: {
const proposalId = rest[0];
2022-03-29 13:50:45 +02:00
await processPurchasePay(ws, proposalId, { forceNow: true });
2021-06-14 19:37:35 +02:00
break;
2022-05-14 23:09:33 +02:00
}
case TransactionType.Tip: {
2021-06-14 19:37:35 +02:00
const walletTipId = rest[0];
2022-03-29 13:50:45 +02:00
await processTip(ws, walletTipId, { forceNow: true });
2021-06-14 19:37:35 +02:00
break;
2022-05-14 23:09:33 +02:00
}
case TransactionType.Refresh: {
2021-06-14 19:37:35 +02:00
const refreshGroupId = rest[0];
2022-03-29 13:50:45 +02:00
await processRefreshGroup(ws, refreshGroupId, { forceNow: true });
2021-06-14 19:37:35 +02:00
break;
2022-05-14 23:09:33 +02:00
}
2021-06-14 19:37:35 +02:00
default:
break;
}
}
/**
2021-05-21 13:32:49 +02:00
* Permanently delete a transaction based on the transaction ID.
*/
export async function deleteTransaction(
ws: InternalWalletState,
transactionId: string,
): Promise<void> {
const [typeStr, ...rest] = transactionId.split(":");
const type = typeStr as TransactionType;
2022-09-05 18:12:30 +02:00
if (
type === TransactionType.Withdrawal ||
type === TransactionType.PeerPullCredit ||
type === TransactionType.PeerPushCredit
) {
const withdrawalGroupId = rest[0];
2021-06-09 15:14:17 +02:00
await ws.db
.mktx((x) => [x.withdrawalGroups, x.tombstones])
2021-06-09 15:14:17 +02:00
.runReadWrite(async (tx) => {
const withdrawalGroupRecord = await tx.withdrawalGroups.get(
withdrawalGroupId,
);
if (withdrawalGroupRecord) {
2021-06-09 15:14:17 +02:00
await tx.withdrawalGroups.delete(withdrawalGroupId);
await tx.tombstones.put({
2021-05-20 17:11:44 +02:00
id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId,
});
return;
}
2021-06-09 15:14:17 +02:00
});
2021-05-20 17:11:44 +02:00
} else if (type === TransactionType.Payment) {
const proposalId = rest[0];
2021-06-09 15:14:17 +02:00
await ws.db
.mktx((x) => [x.proposals, x.purchases, x.tombstones])
2021-06-09 15:14:17 +02:00
.runReadWrite(async (tx) => {
2021-05-20 17:11:44 +02:00
let found = false;
2021-06-09 15:14:17 +02:00
const proposal = await tx.proposals.get(proposalId);
2021-05-20 17:11:44 +02:00
if (proposal) {
found = true;
2021-06-09 15:14:17 +02:00
await tx.proposals.delete(proposalId);
2021-05-20 17:11:44 +02:00
}
2021-06-09 15:14:17 +02:00
const purchase = await tx.purchases.get(proposalId);
2021-05-20 17:11:44 +02:00
if (purchase) {
found = true;
await tx.purchases.delete(proposalId);
2021-05-20 17:11:44 +02:00
}
if (found) {
2021-06-09 15:14:17 +02:00
await tx.tombstones.put({
2021-05-20 17:11:44 +02:00
id: TombstoneTag.DeletePayment + ":" + proposalId,
});
}
2021-06-09 15:14:17 +02:00
});
2021-05-20 17:11:44 +02:00
} else if (type === TransactionType.Refresh) {
const refreshGroupId = rest[0];
2021-06-09 15:14:17 +02:00
await ws.db
.mktx((x) => [x.refreshGroups, x.tombstones])
2021-06-09 15:14:17 +02:00
.runReadWrite(async (tx) => {
const rg = await tx.refreshGroups.get(refreshGroupId);
2021-05-20 17:11:44 +02:00
if (rg) {
2021-06-09 15:14:17 +02:00
await tx.refreshGroups.delete(refreshGroupId);
await tx.tombstones.put({
2021-05-20 17:11:44 +02:00
id: TombstoneTag.DeleteRefreshGroup + ":" + refreshGroupId,
});
}
2021-06-09 15:14:17 +02:00
});
2021-05-20 17:11:44 +02:00
} else if (type === TransactionType.Tip) {
const tipId = rest[0];
2021-06-09 15:14:17 +02:00
await ws.db
.mktx((x) => [x.tips, x.tombstones])
2021-06-09 15:14:17 +02:00
.runReadWrite(async (tx) => {
const tipRecord = await tx.tips.get(tipId);
2021-05-20 17:11:44 +02:00
if (tipRecord) {
2021-06-09 15:14:17 +02:00
await tx.tips.delete(tipId);
await tx.tombstones.put({
2021-05-20 17:11:44 +02:00
id: TombstoneTag.DeleteTip + ":" + tipId,
});
}
2021-06-09 15:14:17 +02:00
});
2021-05-20 17:11:44 +02:00
} else if (type === TransactionType.Deposit) {
const depositGroupId = rest[0];
2021-06-09 15:14:17 +02:00
await ws.db
.mktx((x) => [x.depositGroups, x.tombstones])
2021-06-09 15:14:17 +02:00
.runReadWrite(async (tx) => {
const tipRecord = await tx.depositGroups.get(depositGroupId);
2021-05-20 17:11:44 +02:00
if (tipRecord) {
2021-06-09 15:14:17 +02:00
await tx.depositGroups.delete(depositGroupId);
await tx.tombstones.put({
2021-05-20 17:11:44 +02:00
id: TombstoneTag.DeleteDepositGroup + ":" + depositGroupId,
});
}
2021-06-09 15:14:17 +02:00
});
} else if (type === TransactionType.Refund) {
const proposalId = rest[0];
const executionTimeStr = rest[1];
2021-06-09 15:14:17 +02:00
await ws.db
.mktx((x) => [x.proposals, x.purchases, x.tombstones])
2021-06-09 15:14:17 +02:00
.runReadWrite(async (tx) => {
const purchase = await tx.purchases.get(proposalId);
if (purchase) {
// This should just influence the history view,
// but won't delete any actual refund information.
2021-06-09 15:14:17 +02:00
await tx.tombstones.put({
id: makeEventId(
TombstoneTag.DeleteRefund,
proposalId,
executionTimeStr,
),
});
}
2021-06-09 15:14:17 +02:00
});
} else if (type === TransactionType.PeerPullDebit) {
const peerPullPaymentIncomingId = rest[0];
await ws.db
.mktx((x) => [x.peerPullPaymentIncoming, x.tombstones])
.runReadWrite(async (tx) => {
2022-09-05 18:12:30 +02:00
const debit = await tx.peerPullPaymentIncoming.get(
peerPullPaymentIncomingId,
);
if (debit) {
await tx.peerPullPaymentIncoming.delete(peerPullPaymentIncomingId);
await tx.tombstones.put({
id: makeEventId(
TombstoneTag.DeletePeerPullDebit,
peerPullPaymentIncomingId,
),
});
}
});
} else if (type === TransactionType.PeerPushDebit) {
const pursePub = rest[0];
await ws.db
.mktx((x) => [x.peerPushPaymentInitiations, x.tombstones])
.runReadWrite(async (tx) => {
const debit = await tx.peerPushPaymentInitiations.get(pursePub);
if (debit) {
await tx.peerPushPaymentInitiations.delete(pursePub);
await tx.tombstones.put({
2022-09-05 18:12:30 +02:00
id: makeEventId(TombstoneTag.DeletePeerPushDebit, pursePub),
});
}
});
} else {
const unknownTxType: never = type;
throw Error(`can't delete a '${unknownTxType}' transaction`);
}
}