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.
|
|
|
|
*/
|
2021-11-19 18:46:32 +01:00
|
|
|
import {
|
2022-03-18 15:32:41 +01:00
|
|
|
AbsoluteTime,
|
2021-11-19 18:46:32 +01:00
|
|
|
AmountJson,
|
2021-12-13 11:28:15 +01:00
|
|
|
Amounts,
|
|
|
|
Logger,
|
|
|
|
OrderShortInfo,
|
|
|
|
PaymentStatus,
|
2022-05-29 06:23:15 +02:00
|
|
|
RefundInfoShort,
|
2021-12-13 11:28:15 +01:00
|
|
|
Transaction,
|
|
|
|
TransactionsRequest,
|
|
|
|
TransactionsResponse,
|
|
|
|
TransactionType,
|
|
|
|
WithdrawalDetails,
|
|
|
|
WithdrawalType,
|
2021-11-19 18:46:32 +01:00
|
|
|
} from "@gnu-taler/taler-util";
|
2022-03-23 13:11:36 +01:00
|
|
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
2020-08-10 13:18:38 +02:00
|
|
|
import {
|
2021-12-13 11:28:15 +01:00
|
|
|
AbortStatus,
|
|
|
|
RefundState,
|
|
|
|
ReserveRecordStatus,
|
|
|
|
WalletRefundItem,
|
2022-08-24 22:17:19 +02:00
|
|
|
WithdrawalRecordType,
|
2021-03-17 17:56:37 +01:00
|
|
|
} from "../db.js";
|
2021-11-19 18:46:32 +01:00
|
|
|
import { processDepositGroup } from "./deposits.js";
|
2021-06-02 13:23:51 +02:00
|
|
|
import { getExchangeDetails } from "./exchanges.js";
|
2021-06-14 19:37:35 +02:00
|
|
|
import { processPurchasePay } from "./pay.js";
|
|
|
|
import { processRefreshGroup } from "./refresh.js";
|
2021-11-19 18:46:32 +01:00
|
|
|
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
|
|
|
|
2021-12-13 11:28:15 +01: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",
|
|
|
|
}
|
|
|
|
|
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(
|
2021-05-20 19:03:49 +02:00
|
|
|
type: TransactionType | TombstoneTag,
|
|
|
|
...args: string[]
|
|
|
|
): string {
|
2021-05-20 16:24:41 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2022-06-01 10:47:46 +02:00
|
|
|
/**
|
|
|
|
* Fallback order of transactions that have the same timestamp.
|
|
|
|
*/
|
|
|
|
const txOrder: { [t in TransactionType]: number } = {
|
|
|
|
[TransactionType.Withdrawal]: 1,
|
|
|
|
[TransactionType.Tip]: 2,
|
|
|
|
[TransactionType.Payment]: 3,
|
2022-08-24 22:17:19 +02:00
|
|
|
[TransactionType.PeerPullCredit]: 4,
|
|
|
|
[TransactionType.PeerPullDebit]: 5,
|
|
|
|
[TransactionType.PeerPushCredit]: 6,
|
|
|
|
[TransactionType.PeerPushDebit]: 7,
|
|
|
|
[TransactionType.Refund]: 8,
|
|
|
|
[TransactionType.Deposit]: 9,
|
|
|
|
[TransactionType.Refresh]: 10,
|
|
|
|
[TransactionType.Tip]: 11,
|
2022-06-01 10:47:46 +02:00
|
|
|
};
|
|
|
|
|
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) => ({
|
|
|
|
coins: x.coins,
|
|
|
|
denominations: x.denominations,
|
|
|
|
exchanges: x.exchanges,
|
|
|
|
exchangeDetails: x.exchangeDetails,
|
|
|
|
proposals: x.proposals,
|
|
|
|
purchases: x.purchases,
|
|
|
|
refreshGroups: x.refreshGroups,
|
|
|
|
tips: x.tips,
|
|
|
|
withdrawalGroups: x.withdrawalGroups,
|
|
|
|
planchets: x.planchets,
|
|
|
|
recoupGroups: x.recoupGroups,
|
|
|
|
depositGroups: x.depositGroups,
|
|
|
|
tombstones: x.tombstones,
|
2022-08-24 22:17:19 +02:00
|
|
|
peerPushPaymentInitiations: x.peerPushPaymentInitiations,
|
|
|
|
peerPullPaymentIncoming: x.peerPullPaymentIncoming,
|
2021-06-09 15:14:17 +02:00
|
|
|
}))
|
2022-08-24 22:17:19 +02:00
|
|
|
.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({
|
|
|
|
type: TransactionType.PeerPushDebit,
|
|
|
|
amountEffective: pi.amount,
|
|
|
|
amountRaw: pi.amount,
|
|
|
|
exchangeBaseUrl: pi.exchangeBaseUrl,
|
|
|
|
frozen: false,
|
|
|
|
pending: !pi.purseCreated,
|
|
|
|
timestamp: pi.timestampCreated,
|
|
|
|
transactionId: makeEventId(
|
|
|
|
TransactionType.PeerPushDebit,
|
|
|
|
pi.pursePub,
|
|
|
|
),
|
|
|
|
});
|
|
|
|
});
|
2020-05-15 13:28:15 +02:00
|
|
|
|
2022-08-24 22:17:19 +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({
|
|
|
|
type: TransactionType.PeerPullDebit,
|
|
|
|
amountEffective: Amounts.stringify(amount),
|
|
|
|
amountRaw: Amounts.stringify(amount),
|
|
|
|
exchangeBaseUrl: pi.exchangeBaseUrl,
|
|
|
|
frozen: false,
|
|
|
|
pending: false,
|
|
|
|
timestamp: pi.timestampCreated,
|
|
|
|
transactionId: makeEventId(
|
|
|
|
TransactionType.PeerPullDebit,
|
|
|
|
pi.pursePub,
|
|
|
|
),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
tx.withdrawalGroups.iter().forEachAsync(async (wsr) => {
|
|
|
|
if (
|
|
|
|
shouldSkipCurrency(
|
|
|
|
transactionsRequest,
|
|
|
|
wsr.rawWithdrawalAmount.currency,
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
2021-11-19 18:46:32 +01:00
|
|
|
|
2022-08-24 22:17:19 +02:00
|
|
|
if (shouldSkipSearch(transactionsRequest, [])) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
let withdrawalDetails: WithdrawalDetails;
|
|
|
|
if (wsr.withdrawalType === WithdrawalRecordType.PeerPullCredit) {
|
2021-06-09 15:14:17 +02:00
|
|
|
transactions.push({
|
2022-08-24 22:17:19 +02:00
|
|
|
type: TransactionType.PeerPullCredit,
|
2021-06-09 15:14:17 +02:00
|
|
|
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
|
2022-08-09 15:00:45 +02:00
|
|
|
amountRaw: Amounts.stringify(wsr.rawWithdrawalAmount),
|
2021-06-09 15:14:17 +02:00
|
|
|
exchangeBaseUrl: wsr.exchangeBaseUrl,
|
|
|
|
pending: !wsr.timestampFinish,
|
|
|
|
timestamp: wsr.timestampStart,
|
|
|
|
transactionId: makeEventId(
|
2022-08-24 22:17:19 +02:00
|
|
|
TransactionType.PeerPullCredit,
|
2021-06-09 15:14:17 +02:00
|
|
|
wsr.withdrawalGroupId,
|
|
|
|
),
|
2021-08-24 15:08:34 +02:00
|
|
|
frozen: false,
|
2021-06-09 15:14:17 +02:00
|
|
|
...(wsr.lastError ? { error: wsr.lastError } : {}),
|
|
|
|
});
|
2022-08-24 22:17:19 +02:00
|
|
|
return;
|
|
|
|
} else if (wsr.withdrawalType === WithdrawalRecordType.PeerPushCredit) {
|
2021-06-09 15:14:17 +02:00
|
|
|
transactions.push({
|
2022-08-24 22:17:19 +02:00
|
|
|
type: TransactionType.PeerPushCredit,
|
|
|
|
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
|
|
|
|
amountRaw: Amounts.stringify(wsr.rawWithdrawalAmount),
|
|
|
|
exchangeBaseUrl: wsr.exchangeBaseUrl,
|
|
|
|
pending: !wsr.timestampFinish,
|
|
|
|
timestamp: wsr.timestampStart,
|
2021-06-09 15:14:17 +02:00
|
|
|
transactionId: makeEventId(
|
2022-08-24 22:17:19 +02:00
|
|
|
TransactionType.PeerPushCredit,
|
|
|
|
wsr.withdrawalGroupId,
|
2021-06-09 15:14:17 +02:00
|
|
|
),
|
2022-08-24 22:17:19 +02:00
|
|
|
frozen: false,
|
|
|
|
...(wsr.lastError ? { error: wsr.lastError } : {}),
|
2021-06-09 15:14:17 +02:00
|
|
|
});
|
2022-08-24 22:17:19 +02:00
|
|
|
return;
|
|
|
|
} else if (wsr.bankInfo) {
|
|
|
|
withdrawalDetails = {
|
|
|
|
type: WithdrawalType.TalerBankIntegrationApi,
|
|
|
|
confirmed: wsr.bankInfo.timestampBankConfirmed ? true : false,
|
|
|
|
reservePub: wsr.reservePub,
|
|
|
|
bankConfirmationUrl: wsr.bankInfo.confirmUrl,
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
const exchangeDetails = await getExchangeDetails(
|
|
|
|
tx,
|
|
|
|
wsr.exchangeBaseUrl,
|
|
|
|
);
|
|
|
|
if (!exchangeDetails) {
|
|
|
|
// FIXME: report somehow
|
2021-06-09 15:14:17 +02:00
|
|
|
return;
|
|
|
|
}
|
2022-08-24 22:17:19 +02:00
|
|
|
withdrawalDetails = {
|
|
|
|
type: WithdrawalType.ManualTransfer,
|
|
|
|
reservePub: wsr.reservePub,
|
|
|
|
exchangePaytoUris:
|
|
|
|
exchangeDetails.wireInfo?.accounts.map(
|
|
|
|
(x) => `${x.payto_uri}?subject=${wsr.reservePub}`,
|
|
|
|
) ?? [],
|
2021-06-09 15:14:17 +02:00
|
|
|
};
|
2022-08-24 22:17:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
transactions.push({
|
|
|
|
type: TransactionType.Withdrawal,
|
|
|
|
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
|
|
|
|
amountRaw: Amounts.stringify(wsr.rawWithdrawalAmount),
|
|
|
|
withdrawalDetails,
|
|
|
|
exchangeBaseUrl: wsr.exchangeBaseUrl,
|
|
|
|
pending: !wsr.timestampFinish,
|
|
|
|
timestamp: wsr.timestampStart,
|
|
|
|
transactionId: makeEventId(
|
|
|
|
TransactionType.Withdrawal,
|
|
|
|
wsr.withdrawalGroupId,
|
|
|
|
),
|
|
|
|
frozen: false,
|
|
|
|
...(wsr.lastError ? { error: wsr.lastError } : {}),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
tx.depositGroups.iter().forEachAsync(async (dg) => {
|
|
|
|
const amount = Amounts.parseOrThrow(dg.contractTermsRaw.amount);
|
|
|
|
if (shouldSkipCurrency(transactionsRequest, amount.currency)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
transactions.push({
|
|
|
|
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,
|
|
|
|
...(dg.lastError ? { error: dg.lastError } : {}),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
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 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 paymentTransactionId = makeEventId(
|
|
|
|
TransactionType.Payment,
|
|
|
|
pr.proposalId,
|
|
|
|
);
|
|
|
|
const refundGroupKeys = new Set<string>();
|
|
|
|
|
|
|
|
for (const rk of Object.keys(pr.refunds)) {
|
|
|
|
const refund = pr.refunds[rk];
|
|
|
|
const groupKey = `${refund.executionTime.t_s}`;
|
|
|
|
refundGroupKeys.add(groupKey);
|
|
|
|
}
|
|
|
|
|
|
|
|
let totalRefundRaw = Amounts.getZero(contractData.amount.currency);
|
|
|
|
let totalRefundEffective = Amounts.getZero(
|
|
|
|
contractData.amount.currency,
|
|
|
|
);
|
|
|
|
const refunds: RefundInfoShort[] = [];
|
|
|
|
|
|
|
|
for (const groupKey of refundGroupKeys.values()) {
|
|
|
|
const refundTombstoneId = makeEventId(
|
|
|
|
TombstoneTag.DeleteRefund,
|
|
|
|
pr.proposalId,
|
|
|
|
groupKey,
|
|
|
|
);
|
|
|
|
const tombstone = await tx.tombstones.get(refundTombstoneId);
|
|
|
|
if (tombstone) {
|
|
|
|
continue;
|
2021-06-09 15:14:17 +02:00
|
|
|
}
|
2022-08-24 22:17:19 +02:00
|
|
|
const refundTransactionId = makeEventId(
|
|
|
|
TransactionType.Refund,
|
2020-08-10 13:18:38 +02:00
|
|
|
pr.proposalId,
|
2022-08-24 22:17:19 +02:00
|
|
|
groupKey,
|
2020-08-10 13:18:38 +02:00
|
|
|
);
|
2022-08-24 22:17:19 +02:00
|
|
|
let r0: WalletRefundItem | undefined;
|
|
|
|
let amountRaw = Amounts.getZero(contractData.amount.currency);
|
|
|
|
let amountEffective = Amounts.getZero(contractData.amount.currency);
|
2020-08-10 13:18:38 +02:00
|
|
|
for (const rk of Object.keys(pr.refunds)) {
|
|
|
|
const refund = pr.refunds[rk];
|
2022-08-24 22:17:19 +02:00
|
|
|
const myGroupKey = `${refund.executionTime.t_s}`;
|
|
|
|
if (myGroupKey !== groupKey) {
|
2020-09-01 17:07:50 +02:00
|
|
|
continue;
|
|
|
|
}
|
2020-08-10 13:18:38 +02:00
|
|
|
if (!r0) {
|
2022-08-24 22:17:19 +02:00
|
|
|
r0 = refund;
|
2020-08-10 13:18:38 +02:00
|
|
|
}
|
2022-05-14 23:09:33 +02:00
|
|
|
|
2022-08-24 22:17:19 +02:00
|
|
|
if (refund.type === RefundState.Applied) {
|
|
|
|
amountRaw = Amounts.add(amountRaw, refund.refundAmount).amount;
|
|
|
|
amountEffective = Amounts.add(
|
|
|
|
amountEffective,
|
|
|
|
Amounts.sub(
|
|
|
|
refund.refundAmount,
|
|
|
|
refund.refundFee,
|
|
|
|
refund.totalRefreshCostBound,
|
|
|
|
).amount,
|
|
|
|
).amount;
|
|
|
|
|
|
|
|
refunds.push({
|
|
|
|
transactionId: refundTransactionId,
|
|
|
|
timestamp: r0.obtainedTime,
|
|
|
|
amountEffective: Amounts.stringify(amountEffective),
|
|
|
|
amountRaw: Amounts.stringify(amountRaw),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!r0) {
|
|
|
|
throw Error("invariant violated");
|
2021-06-09 15:14:17 +02:00
|
|
|
}
|
2022-05-14 23:09:33 +02:00
|
|
|
|
2022-08-24 22:17:19 +02:00
|
|
|
totalRefundRaw = Amounts.add(totalRefundRaw, amountRaw).amount;
|
|
|
|
totalRefundEffective = Amounts.add(
|
|
|
|
totalRefundEffective,
|
|
|
|
amountEffective,
|
|
|
|
).amount;
|
2022-05-14 23:09:33 +02:00
|
|
|
transactions.push({
|
2022-08-24 22:17:19 +02:00
|
|
|
type: TransactionType.Refund,
|
|
|
|
info,
|
|
|
|
refundedTransactionId: paymentTransactionId,
|
|
|
|
transactionId: refundTransactionId,
|
|
|
|
timestamp: r0.obtainedTime,
|
|
|
|
amountEffective: Amounts.stringify(amountEffective),
|
|
|
|
amountRaw: Amounts.stringify(amountRaw),
|
2022-06-01 10:47:46 +02:00
|
|
|
refundPending:
|
|
|
|
pr.refundAwaiting === undefined
|
|
|
|
? undefined
|
|
|
|
: Amounts.stringify(pr.refundAwaiting),
|
2022-08-24 22:17:19 +02:00
|
|
|
pending: false,
|
|
|
|
frozen: false,
|
2022-05-14 23:09:33 +02:00
|
|
|
});
|
2022-08-24 22:17:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const err = pr.lastPayError ?? pr.lastRefundStatusError;
|
|
|
|
transactions.push({
|
|
|
|
type: TransactionType.Payment,
|
|
|
|
amountRaw: Amounts.stringify(contractData.amount),
|
|
|
|
amountEffective: Amounts.stringify(pr.totalPayCost),
|
|
|
|
totalRefundRaw: Amounts.stringify(totalRefundRaw),
|
|
|
|
totalRefundEffective: Amounts.stringify(totalRefundEffective),
|
|
|
|
refundPending:
|
|
|
|
pr.refundAwaiting === undefined
|
|
|
|
? undefined
|
|
|
|
: Amounts.stringify(pr.refundAwaiting),
|
|
|
|
status: pr.timestampFirstSuccessfulPay
|
|
|
|
? PaymentStatus.Paid
|
|
|
|
: PaymentStatus.Accepted,
|
|
|
|
pending:
|
|
|
|
!pr.timestampFirstSuccessfulPay &&
|
|
|
|
pr.abortStatus === AbortStatus.None,
|
|
|
|
refunds,
|
|
|
|
timestamp: pr.timestampAccept,
|
|
|
|
transactionId: paymentTransactionId,
|
|
|
|
proposalId: pr.proposalId,
|
|
|
|
info,
|
|
|
|
frozen: pr.payFrozen ?? false,
|
|
|
|
...(err ? { error: err } : {}),
|
2021-06-09 15:14:17 +02:00
|
|
|
});
|
2022-08-24 22:17:19 +02:00
|
|
|
});
|
2020-09-01 17:07:50 +02:00
|
|
|
|
2022-08-24 22:17:19 +02:00
|
|
|
tx.tips.iter().forEachAsync(async (tipRecord) => {
|
|
|
|
if (
|
|
|
|
shouldSkipCurrency(
|
|
|
|
transactionsRequest,
|
|
|
|
tipRecord.tipAmountRaw.currency,
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!tipRecord.acceptedTimestamp) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
transactions.push({
|
|
|
|
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,
|
|
|
|
// merchant: {
|
|
|
|
// name: tipRecord.merchantBaseUrl,
|
|
|
|
// },
|
|
|
|
error: tipRecord.lastError,
|
2020-09-08 16:24:23 +02:00
|
|
|
});
|
2022-08-24 22:17:19 +02:00
|
|
|
});
|
|
|
|
});
|
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);
|
|
|
|
|
2022-06-01 10:47:46 +02:00
|
|
|
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),
|
2022-06-01 10:47:46 +02:00
|
|
|
);
|
|
|
|
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
|
|
|
|
2020-09-08 22:48:03 +02:00
|
|
|
return { transactions: [...txNotPending, ...txPending] };
|
2020-05-12 10:38:58 +02:00
|
|
|
}
|
2021-05-20 16:24:41 +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> {
|
2021-12-13 11:28:15 +01:00
|
|
|
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: {
|
2021-06-15 18:52:43 +02:00
|
|
|
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-20 16:24:41 +02:00
|
|
|
/**
|
2021-05-21 13:32:49 +02:00
|
|
|
* Permanently delete a transaction based on the transaction ID.
|
2021-05-20 16:24:41 +02:00
|
|
|
*/
|
|
|
|
export async function deleteTransaction(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
transactionId: string,
|
|
|
|
): Promise<void> {
|
|
|
|
const [type, ...rest] = transactionId.split(":");
|
|
|
|
|
|
|
|
if (type === TransactionType.Withdrawal) {
|
|
|
|
const withdrawalGroupId = rest[0];
|
2021-06-09 15:14:17 +02:00
|
|
|
await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
withdrawalGroups: x.withdrawalGroups,
|
|
|
|
tombstones: x.tombstones,
|
|
|
|
}))
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const withdrawalGroupRecord = await tx.withdrawalGroups.get(
|
2021-05-20 16:24:41 +02:00
|
|
|
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,
|
2021-05-20 16:24:41 +02:00
|
|
|
});
|
|
|
|
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) => ({
|
|
|
|
proposals: x.proposals,
|
|
|
|
purchases: x.purchases,
|
|
|
|
tombstones: x.tombstones,
|
|
|
|
}))
|
|
|
|
.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;
|
2021-08-23 22:28:36 +02:00
|
|
|
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) => ({
|
|
|
|
refreshGroups: x.refreshGroups,
|
|
|
|
tombstones: x.tombstones,
|
|
|
|
}))
|
|
|
|
.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) => ({
|
|
|
|
tips: x.tips,
|
|
|
|
tombstones: x.tombstones,
|
|
|
|
}))
|
|
|
|
.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) => ({
|
|
|
|
depositGroups: x.depositGroups,
|
|
|
|
tombstones: x.tombstones,
|
|
|
|
}))
|
|
|
|
.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-05-20 16:24:41 +02:00
|
|
|
});
|
|
|
|
}
|
2021-06-09 15:14:17 +02:00
|
|
|
});
|
2021-05-20 16:24:41 +02:00
|
|
|
} else if (type === TransactionType.Refund) {
|
2021-05-20 19:03:49 +02:00
|
|
|
const proposalId = rest[0];
|
|
|
|
const executionTimeStr = rest[1];
|
|
|
|
|
2021-06-09 15:14:17 +02:00
|
|
|
await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
proposals: x.proposals,
|
|
|
|
purchases: x.purchases,
|
|
|
|
tombstones: x.tombstones,
|
|
|
|
}))
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const purchase = await tx.purchases.get(proposalId);
|
2021-05-20 19:03:49 +02:00
|
|
|
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({
|
2021-05-20 19:03:49 +02:00
|
|
|
id: makeEventId(
|
|
|
|
TombstoneTag.DeleteRefund,
|
|
|
|
proposalId,
|
|
|
|
executionTimeStr,
|
|
|
|
),
|
|
|
|
});
|
|
|
|
}
|
2021-06-09 15:14:17 +02:00
|
|
|
});
|
2021-05-20 16:24:41 +02:00
|
|
|
} else {
|
|
|
|
throw Error(`can't delete a '${type}' transaction`);
|
|
|
|
}
|
|
|
|
}
|