wallet-core: allow inclusion of refreshes in transactions list

This commit is contained in:
Florian Dold 2023-02-14 13:02:59 +01:00
parent 97fac057c2
commit 55f868d5e8
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
7 changed files with 69 additions and 8 deletions

View File

@ -68,6 +68,11 @@ export interface TransactionsRequest {
* if present, results will be limited to transactions related to the given search string * if present, results will be limited to transactions related to the given search string
*/ */
search?: string; search?: string;
/**
* If true, include all refreshes in the transactions list.
*/
includeRefreshes?: boolean;
} }
export interface TransactionsResponse { export interface TransactionsResponse {
@ -514,11 +519,6 @@ export interface TransactionTip extends TransactionCommon {
export interface TransactionRefresh extends TransactionCommon { export interface TransactionRefresh extends TransactionCommon {
type: TransactionType.Refresh; type: TransactionType.Refresh;
/**
* Exchange that the coins are refreshed with
*/
exchangeBaseUrl: string;
refreshReason: RefreshReason; refreshReason: RefreshReason;
/** /**

View File

@ -381,7 +381,8 @@ walletCli
const transactionsCli = walletCli const transactionsCli = walletCli
.subcommand("transactions", "transactions", { help: "Manage transactions." }) .subcommand("transactions", "transactions", { help: "Manage transactions." })
.maybeOption("currency", ["--currency"], clk.STRING) .maybeOption("currency", ["--currency"], clk.STRING)
.maybeOption("search", ["--search"], clk.STRING); .maybeOption("search", ["--search"], clk.STRING)
.flag("includeRefreshes", ["--include-refreshes"]);
// Default action // Default action
transactionsCli.action(async (args) => { transactionsCli.action(async (args) => {
@ -391,6 +392,7 @@ transactionsCli.action(async (args) => {
{ {
currency: args.transactions.currency, currency: args.transactions.currency,
search: args.transactions.search, search: args.transactions.search,
includeRefreshes: args.transactions.includeRefreshes,
}, },
); );
console.log(JSON.stringify(pending, undefined, 2)); console.log(JSON.stringify(pending, undefined, 2));

View File

@ -56,6 +56,8 @@ import {
PeerPullPaymentIncomingStatus, PeerPullPaymentIncomingStatus,
TransactionStatus, TransactionStatus,
WithdrawalGroupStatus, WithdrawalGroupStatus,
RefreshGroupRecord,
RefreshOperationStatus,
} from "../db.js"; } from "../db.js";
import { InternalWalletState } from "../internal-wallet-state.js"; import { InternalWalletState } from "../internal-wallet-state.js";
import { checkDbInvariant } from "../util/invariants.js"; import { checkDbInvariant } from "../util/invariants.js";
@ -580,6 +582,45 @@ function buildTransactionForManualWithdraw(
}; };
} }
function buildTransactionForRefresh(
refreshGroupRecord: RefreshGroupRecord,
ort?: OperationRetryRecord,
): Transaction {
let extendedStatus: ExtendedStatus;
switch (refreshGroupRecord.operationStatus) {
case RefreshOperationStatus.Finished:
case RefreshOperationStatus.FinishedWithError:
extendedStatus = ExtendedStatus.Done;
break;
default:
extendedStatus = ExtendedStatus.Pending;
}
return {
type: TransactionType.Refresh,
refreshReason: refreshGroupRecord.reason,
amountEffective: Amounts.stringify(
Amounts.zeroOfCurrency(refreshGroupRecord.currency),
),
amountRaw: Amounts.stringify(
Amounts.zeroOfCurrency(refreshGroupRecord.currency),
),
extendedStatus:
refreshGroupRecord.operationStatus === RefreshOperationStatus.Finished ||
refreshGroupRecord.operationStatus ===
RefreshOperationStatus.FinishedWithError
? ExtendedStatus.Done
: ExtendedStatus.Pending,
pending: extendedStatus == ExtendedStatus.Pending,
timestamp: refreshGroupRecord.timestampCreated,
transactionId: makeTransactionId(
TransactionType.Refresh,
refreshGroupRecord.refreshGroupId,
),
frozen: false,
...(ort?.lastError ? { error: ort.lastError } : {}),
};
}
function buildTransactionForDeposit( function buildTransactionForDeposit(
dg: DepositGroupRecord, dg: DepositGroupRecord,
ort?: OperationRetryRecord, ort?: OperationRetryRecord,
@ -880,6 +921,7 @@ export async function getTransactions(
x.tips, x.tips,
x.tombstones, x.tombstones,
x.withdrawalGroups, x.withdrawalGroups,
x.refreshGroups,
]) ])
.runReadOnly(async (tx) => { .runReadOnly(async (tx) => {
tx.peerPushPaymentInitiations.iter().forEachAsync(async (pi) => { tx.peerPushPaymentInitiations.iter().forEachAsync(async (pi) => {
@ -916,6 +958,17 @@ export async function getTransactions(
transactions.push(buildTransactionForPullPaymentDebit(pi)); transactions.push(buildTransactionForPullPaymentDebit(pi));
}); });
if (transactionsRequest?.includeRefreshes) {
tx.refreshGroups.iter().forEachAsync(async (rg) => {
if (shouldSkipCurrency(transactionsRequest, rg.currency)) {
return;
}
const opId = RetryTags.forRefresh(rg);
const ort = await tx.operationRetries.get(opId);
transactions.push(buildTransactionForRefresh(rg, ort));
});
}
tx.withdrawalGroups.iter().forEachAsync(async (wsr) => { tx.withdrawalGroups.iter().forEachAsync(async (wsr) => {
if ( if (
shouldSkipCurrency( shouldSkipCurrency(

View File

@ -110,7 +110,7 @@ export function TransactionItem(props: { tx: Transaction }): VNode {
id={tx.transactionId} id={tx.transactionId}
amount={tx.amountEffective} amount={tx.amountEffective}
debitCreditIndicator={"credit"} debitCreditIndicator={"credit"}
title={new URL(tx.exchangeBaseUrl).hostname} title={"Refresh"}
timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)} timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
iconPath={"R"} iconPath={"R"}
// pending={tx.pending} // pending={tx.pending}

View File

@ -21,6 +21,7 @@
import { import {
PaymentStatus, PaymentStatus,
RefreshReason,
ScopeType, ScopeType,
TalerProtocolTimestamp, TalerProtocolTimestamp,
TransactionCommon, TransactionCommon,
@ -90,6 +91,7 @@ const exampleData = {
totalRefundRaw: "USD:0", totalRefundRaw: "USD:0",
proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0", proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
status: PaymentStatus.Accepted, status: PaymentStatus.Accepted,
refundQueryActive: false,
} as TransactionPayment, } as TransactionPayment,
deposit: { deposit: {
...commonTransaction(), ...commonTransaction(),
@ -101,6 +103,7 @@ const exampleData = {
...commonTransaction(), ...commonTransaction(),
type: TransactionType.Refresh, type: TransactionType.Refresh,
exchangeBaseUrl: "http://exchange.taler", exchangeBaseUrl: "http://exchange.taler",
refreshReason: RefreshReason.PayMerchant,
} as TransactionRefresh, } as TransactionRefresh,
tip: { tip: {
...commonTransaction(), ...commonTransaction(),

View File

@ -23,6 +23,7 @@ import {
AbsoluteTime, AbsoluteTime,
ExtendedStatus, ExtendedStatus,
PaymentStatus, PaymentStatus,
RefreshReason,
TalerProtocolTimestamp, TalerProtocolTimestamp,
TransactionCommon, TransactionCommon,
TransactionDeposit, TransactionDeposit,
@ -111,6 +112,7 @@ const exampleData = {
totalRefundRaw: "KUDOS:0", totalRefundRaw: "KUDOS:0",
proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0", proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
status: PaymentStatus.Accepted, status: PaymentStatus.Accepted,
refundQueryActive: false,
} as TransactionPayment, } as TransactionPayment,
deposit: { deposit: {
...commonTransaction, ...commonTransaction,
@ -125,6 +127,7 @@ const exampleData = {
...commonTransaction, ...commonTransaction,
type: TransactionType.Refresh, type: TransactionType.Refresh,
exchangeBaseUrl: "http://exchange.taler", exchangeBaseUrl: "http://exchange.taler",
refreshReason: RefreshReason.Manual,
} as TransactionRefresh, } as TransactionRefresh,
tip: { tip: {
...commonTransaction, ...commonTransaction,

View File

@ -729,7 +729,7 @@ export function TransactionView({
total={effective} total={effective}
kind="negative" kind="negative"
> >
{transaction.exchangeBaseUrl} {"Refresh"}
</Header> </Header>
<Part <Part
title={i18n.str`Details`} title={i18n.str`Details`}