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 { InternalWalletState } from "./state";
|
2020-08-10 13:18:38 +02:00
|
|
|
import {
|
|
|
|
Stores,
|
|
|
|
WithdrawalSourceType,
|
|
|
|
WalletRefundItem,
|
|
|
|
RefundState,
|
2020-08-20 11:04:56 +02:00
|
|
|
ReserveRecordStatus,
|
2020-08-10 13:18:38 +02:00
|
|
|
} from "../types/dbTypes";
|
2020-05-12 12:14:48 +02:00
|
|
|
import { Amounts, AmountJson } from "../util/amounts";
|
2020-09-04 08:34:11 +02:00
|
|
|
import { timestampCmp } from "../util/time";
|
2020-05-12 10:38:58 +02:00
|
|
|
import {
|
|
|
|
TransactionsRequest,
|
|
|
|
TransactionsResponse,
|
|
|
|
Transaction,
|
|
|
|
TransactionType,
|
2020-05-12 12:49:40 +02:00
|
|
|
PaymentStatus,
|
2020-07-16 11:14:59 +02:00
|
|
|
WithdrawalType,
|
|
|
|
WithdrawalDetails,
|
2020-08-24 16:09:09 +02:00
|
|
|
OrderShortInfo,
|
2020-05-12 10:38:58 +02:00
|
|
|
} from "../types/transactions";
|
2020-07-16 19:22:56 +02:00
|
|
|
import { getFundingPaytoUris } from "./reserves";
|
2020-05-12 10:38:58 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Create an event ID from the type and the primary key for the event.
|
|
|
|
*/
|
|
|
|
function makeEventId(type: TransactionType, ...args: string[]): string {
|
|
|
|
return type + ";" + args.map((x) => encodeURIComponent(x)).join(";");
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-05-12 10:38:58 +02:00
|
|
|
/**
|
|
|
|
* Retrive the full event history for this wallet.
|
|
|
|
*/
|
|
|
|
export async function getTransactions(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
transactionsRequest?: TransactionsRequest,
|
|
|
|
): Promise<TransactionsResponse> {
|
|
|
|
const transactions: Transaction[] = [];
|
|
|
|
|
|
|
|
await ws.db.runWithReadTransaction(
|
|
|
|
[
|
|
|
|
Stores.currencies,
|
|
|
|
Stores.coins,
|
|
|
|
Stores.denominations,
|
2020-07-17 19:34:38 +02:00
|
|
|
Stores.exchanges,
|
2020-05-12 10:38:58 +02:00
|
|
|
Stores.proposals,
|
|
|
|
Stores.purchases,
|
|
|
|
Stores.refreshGroups,
|
|
|
|
Stores.reserves,
|
|
|
|
Stores.reserveHistory,
|
|
|
|
Stores.tips,
|
|
|
|
Stores.withdrawalGroups,
|
|
|
|
Stores.payEvents,
|
|
|
|
Stores.planchets,
|
|
|
|
Stores.refundEvents,
|
|
|
|
Stores.reserveUpdatedEvents,
|
|
|
|
Stores.recoupGroups,
|
|
|
|
],
|
2020-07-16 11:14:59 +02:00
|
|
|
// Report withdrawals that are currently in progress.
|
2020-05-12 10:38:58 +02:00
|
|
|
async (tx) => {
|
2020-05-15 20:11:47 +02:00
|
|
|
tx.iter(Stores.withdrawalGroups).forEachAsync(async (wsr) => {
|
2020-05-12 12:14:48 +02:00
|
|
|
if (
|
2020-05-15 13:28:15 +02:00
|
|
|
shouldSkipCurrency(
|
|
|
|
transactionsRequest,
|
|
|
|
wsr.rawWithdrawalAmount.currency,
|
|
|
|
)
|
2020-05-12 12:14:48 +02:00
|
|
|
) {
|
|
|
|
return;
|
2020-05-12 10:38:58 +02:00
|
|
|
}
|
2020-05-15 13:28:15 +02:00
|
|
|
|
|
|
|
if (shouldSkipSearch(transactionsRequest, [])) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-07-16 11:14:59 +02:00
|
|
|
switch (wsr.source.type) {
|
2020-07-22 10:52:03 +02:00
|
|
|
case WithdrawalSourceType.Reserve:
|
|
|
|
{
|
|
|
|
const r = await tx.get(Stores.reserves, wsr.source.reservePub);
|
|
|
|
if (!r) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
let amountRaw: AmountJson | undefined = undefined;
|
|
|
|
if (wsr.withdrawalGroupId === r.initialWithdrawalGroupId) {
|
|
|
|
amountRaw = r.instructedAmount;
|
|
|
|
} else {
|
|
|
|
amountRaw = wsr.denomsSel.totalWithdrawCost;
|
|
|
|
}
|
|
|
|
let withdrawalDetails: WithdrawalDetails;
|
|
|
|
if (r.bankInfo) {
|
2020-07-16 11:14:59 +02:00
|
|
|
withdrawalDetails = {
|
|
|
|
type: WithdrawalType.TalerBankIntegrationApi,
|
|
|
|
confirmed: true,
|
|
|
|
bankConfirmationUrl: r.bankInfo.confirmUrl,
|
|
|
|
};
|
2020-07-22 10:52:03 +02:00
|
|
|
} else {
|
|
|
|
const exchange = await tx.get(
|
|
|
|
Stores.exchanges,
|
|
|
|
r.exchangeBaseUrl,
|
|
|
|
);
|
|
|
|
if (!exchange) {
|
|
|
|
// FIXME: report somehow
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
withdrawalDetails = {
|
|
|
|
type: WithdrawalType.ManualTransfer,
|
|
|
|
exchangePaytoUris:
|
|
|
|
exchange.wireInfo?.accounts.map((x) => x.payto_uri) ?? [],
|
|
|
|
};
|
2020-07-16 11:14:59 +02:00
|
|
|
}
|
2020-07-22 10:52:03 +02:00
|
|
|
transactions.push({
|
|
|
|
type: TransactionType.Withdrawal,
|
|
|
|
amountEffective: Amounts.stringify(
|
|
|
|
wsr.denomsSel.totalCoinValue,
|
|
|
|
),
|
|
|
|
amountRaw: Amounts.stringify(amountRaw),
|
|
|
|
withdrawalDetails,
|
|
|
|
exchangeBaseUrl: wsr.exchangeBaseUrl,
|
|
|
|
pending: !wsr.timestampFinish,
|
|
|
|
timestamp: wsr.timestampStart,
|
|
|
|
transactionId: makeEventId(
|
|
|
|
TransactionType.Withdrawal,
|
|
|
|
wsr.withdrawalGroupId,
|
|
|
|
),
|
2020-09-01 15:37:14 +02:00
|
|
|
...(wsr.lastError ? { error: wsr.lastError } : {}),
|
2020-07-22 10:52:03 +02:00
|
|
|
});
|
2020-07-16 11:14:59 +02:00
|
|
|
}
|
2020-07-22 10:52:03 +02:00
|
|
|
break;
|
2020-07-16 11:14:59 +02:00
|
|
|
default:
|
|
|
|
// Tips are reported via their own event
|
|
|
|
break;
|
2020-05-15 20:11:47 +02:00
|
|
|
}
|
2020-05-12 10:38:58 +02:00
|
|
|
});
|
|
|
|
|
2020-07-16 11:14:59 +02:00
|
|
|
// Report pending withdrawals based on reserves that
|
|
|
|
// were created, but where the actual withdrawal group has
|
|
|
|
// not started yet.
|
|
|
|
tx.iter(Stores.reserves).forEachAsync(async (r) => {
|
2020-05-15 13:28:15 +02:00
|
|
|
if (shouldSkipCurrency(transactionsRequest, r.currency)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (shouldSkipSearch(transactionsRequest, [])) {
|
2020-05-12 12:14:48 +02:00
|
|
|
return;
|
|
|
|
}
|
2020-07-16 11:14:59 +02:00
|
|
|
if (r.initialWithdrawalStarted) {
|
2020-05-12 10:38:58 +02:00
|
|
|
return;
|
|
|
|
}
|
2020-08-20 11:04:56 +02:00
|
|
|
if (r.reserveStatus === ReserveRecordStatus.BANK_ABORTED) {
|
|
|
|
return;
|
|
|
|
}
|
2020-07-16 11:14:59 +02:00
|
|
|
let withdrawalDetails: WithdrawalDetails;
|
|
|
|
if (r.bankInfo) {
|
|
|
|
withdrawalDetails = {
|
|
|
|
type: WithdrawalType.TalerBankIntegrationApi,
|
|
|
|
confirmed: false,
|
|
|
|
bankConfirmationUrl: r.bankInfo.confirmUrl,
|
2020-07-22 10:52:03 +02:00
|
|
|
};
|
2020-07-16 11:14:59 +02:00
|
|
|
} else {
|
|
|
|
withdrawalDetails = {
|
|
|
|
type: WithdrawalType.ManualTransfer,
|
2020-07-16 19:22:56 +02:00
|
|
|
exchangePaytoUris: await getFundingPaytoUris(tx, r.reservePub),
|
2020-07-16 11:14:59 +02:00
|
|
|
};
|
|
|
|
}
|
2020-05-12 10:38:58 +02:00
|
|
|
transactions.push({
|
|
|
|
type: TransactionType.Withdrawal,
|
2020-07-16 11:14:59 +02:00
|
|
|
amountRaw: Amounts.stringify(r.instructedAmount),
|
2020-07-22 10:52:03 +02:00
|
|
|
amountEffective: Amounts.stringify(r.initialDenomSel.totalCoinValue),
|
2020-05-15 12:33:52 +02:00
|
|
|
exchangeBaseUrl: r.exchangeBaseUrl,
|
2020-05-12 10:38:58 +02:00
|
|
|
pending: true,
|
|
|
|
timestamp: r.timestampCreated,
|
2020-07-16 11:14:59 +02:00
|
|
|
withdrawalDetails: withdrawalDetails,
|
2020-05-12 10:38:58 +02:00
|
|
|
transactionId: makeEventId(
|
|
|
|
TransactionType.Withdrawal,
|
2020-07-16 11:14:59 +02:00
|
|
|
r.initialWithdrawalGroupId,
|
2020-05-12 10:38:58 +02:00
|
|
|
),
|
2020-09-01 16:03:06 +02:00
|
|
|
...(r.lastError ? { error: r.lastError } : {}),
|
2020-05-12 10:38:58 +02:00
|
|
|
});
|
|
|
|
});
|
2020-05-12 12:14:48 +02:00
|
|
|
|
|
|
|
tx.iter(Stores.purchases).forEachAsync(async (pr) => {
|
|
|
|
if (
|
2020-05-15 13:28:15 +02:00
|
|
|
shouldSkipCurrency(
|
|
|
|
transactionsRequest,
|
|
|
|
pr.contractData.amount.currency,
|
|
|
|
)
|
2020-05-12 12:14:48 +02:00
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
2020-05-15 13:28:15 +02:00
|
|
|
if (shouldSkipSearch(transactionsRequest, [pr.contractData.summary])) {
|
|
|
|
return;
|
|
|
|
}
|
2020-05-12 12:14:48 +02:00
|
|
|
const proposal = await tx.get(Stores.proposals, pr.proposalId);
|
|
|
|
if (!proposal) {
|
|
|
|
return;
|
|
|
|
}
|
2020-08-24 16:09:09 +02:00
|
|
|
const info: OrderShortInfo = {
|
2020-08-10 13:18:38 +02:00
|
|
|
merchant: pr.contractData.merchant,
|
|
|
|
orderId: pr.contractData.orderId,
|
|
|
|
products: pr.contractData.products,
|
|
|
|
summary: pr.contractData.summary,
|
|
|
|
summary_i18n: pr.contractData.summaryI18n,
|
2020-08-21 17:26:25 +02:00
|
|
|
contractTermsHash: pr.contractData.contractTermsHash,
|
2020-08-10 13:18:38 +02:00
|
|
|
};
|
2020-08-24 16:30:15 +02:00
|
|
|
if (pr.contractData.fulfillmentUrl !== "") {
|
|
|
|
info.fulfillmentUrl = pr.contractData.fulfillmentUrl;
|
|
|
|
}
|
2020-08-10 13:18:38 +02:00
|
|
|
const paymentTransactionId = makeEventId(
|
|
|
|
TransactionType.Payment,
|
|
|
|
pr.proposalId,
|
|
|
|
);
|
2020-09-01 16:03:06 +02:00
|
|
|
const err = pr.lastPayError ?? pr.lastRefundStatusError;
|
2020-05-12 12:14:48 +02:00
|
|
|
transactions.push({
|
|
|
|
type: TransactionType.Payment,
|
|
|
|
amountRaw: Amounts.stringify(pr.contractData.amount),
|
2020-05-12 12:34:28 +02:00
|
|
|
amountEffective: Amounts.stringify(pr.payCostInfo.totalCost),
|
2020-05-12 12:49:40 +02:00
|
|
|
status: pr.timestampFirstSuccessfulPay
|
|
|
|
? PaymentStatus.Paid
|
|
|
|
: PaymentStatus.Accepted,
|
2020-05-12 12:14:48 +02:00
|
|
|
pending: !pr.timestampFirstSuccessfulPay,
|
|
|
|
timestamp: pr.timestampAccept,
|
2020-08-10 13:18:38 +02:00
|
|
|
transactionId: paymentTransactionId,
|
|
|
|
info: info,
|
2020-09-01 16:03:06 +02:00
|
|
|
...(err ? { error: err } : {}),
|
2020-08-10 13:18:38 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
const refundGroupKeys = new Set<string>();
|
|
|
|
|
|
|
|
for (const rk of Object.keys(pr.refunds)) {
|
|
|
|
const refund = pr.refunds[rk];
|
|
|
|
const groupKey = `${refund.executionTime.t_ms}`;
|
|
|
|
refundGroupKeys.add(groupKey);
|
|
|
|
}
|
|
|
|
|
|
|
|
refundGroupKeys.forEach((groupKey: string) => {
|
|
|
|
const refundTransactionId = makeEventId(
|
2020-08-20 08:29:06 +02:00
|
|
|
TransactionType.Refund,
|
2020-08-10 13:18:38 +02:00
|
|
|
pr.proposalId,
|
|
|
|
groupKey,
|
|
|
|
);
|
|
|
|
let r0: WalletRefundItem | undefined;
|
2020-09-04 08:34:11 +02:00
|
|
|
let amountRaw = Amounts.getZero(pr.contractData.amount.currency);
|
|
|
|
let amountEffective = Amounts.getZero(
|
2020-08-10 13:18:38 +02:00
|
|
|
pr.contractData.amount.currency,
|
|
|
|
);
|
|
|
|
for (const rk of Object.keys(pr.refunds)) {
|
|
|
|
const refund = pr.refunds[rk];
|
2020-09-01 17:07:50 +02:00
|
|
|
const myGroupKey = `${refund.executionTime.t_ms}`;
|
|
|
|
if (myGroupKey !== groupKey) {
|
|
|
|
continue;
|
|
|
|
}
|
2020-08-10 13:18:38 +02:00
|
|
|
if (!r0) {
|
|
|
|
r0 = refund;
|
|
|
|
}
|
2020-09-01 17:07:50 +02:00
|
|
|
|
2020-08-10 13:18:38 +02:00
|
|
|
if (refund.type === RefundState.Applied) {
|
2020-09-04 08:34:11 +02:00
|
|
|
amountRaw = Amounts.add(amountRaw, refund.refundAmount).amount;
|
2020-09-01 17:07:50 +02:00
|
|
|
amountEffective = Amounts.add(
|
|
|
|
amountEffective,
|
2020-08-10 13:18:38 +02:00
|
|
|
Amounts.sub(
|
|
|
|
refund.refundAmount,
|
|
|
|
refund.refundFee,
|
|
|
|
refund.totalRefreshCostBound,
|
|
|
|
).amount,
|
|
|
|
).amount;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!r0) {
|
|
|
|
throw Error("invariant violated");
|
|
|
|
}
|
|
|
|
transactions.push({
|
|
|
|
type: TransactionType.Refund,
|
|
|
|
info,
|
|
|
|
refundedTransactionId: paymentTransactionId,
|
|
|
|
transactionId: refundTransactionId,
|
2020-08-14 12:23:50 +02:00
|
|
|
timestamp: r0.obtainedTime,
|
2020-08-10 13:18:38 +02:00
|
|
|
amountEffective: Amounts.stringify(amountEffective),
|
|
|
|
amountRaw: Amounts.stringify(amountRaw),
|
|
|
|
pending: false,
|
|
|
|
});
|
2020-05-12 12:14:48 +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);
|
|
|
|
|
|
|
|
txPending.sort((h1, h2) => timestampCmp(h1.timestamp, h2.timestamp));
|
|
|
|
txNotPending.sort((h1, h2) => timestampCmp(h1.timestamp, h2.timestamp));
|
2020-05-12 10:38:58 +02:00
|
|
|
|
2020-05-15 20:22:58 +02:00
|
|
|
return { transactions: [...txPending, ...txNotPending] };
|
2020-05-12 10:38:58 +02:00
|
|
|
}
|