new transactions API: withdrawal

This commit is contained in:
Florian Dold 2020-05-12 14:08:58 +05:30
parent 857a2b9dca
commit 6206b418ff
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
8 changed files with 421 additions and 48 deletions

View File

@ -231,6 +231,15 @@ walletCli
}); });
}); });
walletCli
.subcommand("", "transactions", { help: "Show transactions." })
.action(async (args) => {
await withWallet(args, async (wallet) => {
const pending = await wallet.getTransactions({});
console.log(JSON.stringify(pending, undefined, 2));
});
});
async function asyncSleep(milliSeconds: number): Promise<void> { async function asyncSleep(milliSeconds: number): Promise<void> {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
setTimeout(() => resolve(), milliSeconds); setTimeout(() => resolve(), milliSeconds);

View File

@ -375,10 +375,10 @@ export async function getHistory(
return; return;
} }
let reserveCreationDetail: ReserveCreationDetail; let reserveCreationDetail: ReserveCreationDetail;
if (reserve.bankWithdrawStatusUrl) { if (reserve.bankInfo) {
reserveCreationDetail = { reserveCreationDetail = {
type: ReserveType.TalerBankWithdraw, type: ReserveType.TalerBankWithdraw,
bankUrl: reserve.bankWithdrawStatusUrl, bankUrl: reserve.bankInfo.statusUrl,
}; };
} else { } else {
reserveCreationDetail = { reserveCreationDetail = {

View File

@ -150,7 +150,7 @@ async function gatherReservePending(
): Promise<void> { ): Promise<void> {
// FIXME: this should be optimized by using an index for "onlyDue==true". // FIXME: this should be optimized by using an index for "onlyDue==true".
await tx.iter(Stores.reserves).forEach((reserve) => { await tx.iter(Stores.reserves).forEach((reserve) => {
const reserveType = reserve.bankWithdrawStatusUrl const reserveType = reserve.bankInfo
? ReserveType.TalerBankWithdraw ? ReserveType.TalerBankWithdraw
: ReserveType.Manual; : ReserveType.Manual;
if (!reserve.retryInfo.active) { if (!reserve.retryInfo.active) {

View File

@ -108,7 +108,14 @@ export async function createReserve(
senderWire: req.senderWire, senderWire: req.senderWire,
timestampConfirmed: undefined, timestampConfirmed: undefined,
timestampReserveInfoPosted: undefined, timestampReserveInfoPosted: undefined,
bankWithdrawStatusUrl: req.bankWithdrawStatusUrl, bankInfo: req.bankWithdrawStatusUrl
? {
statusUrl: req.bankWithdrawStatusUrl,
amount: req.amount,
bankWithdrawalGroupId: encodeCrock(getRandomBytes(32)),
withdrawalStarted: false,
}
: undefined,
exchangeWire: req.exchangeWire, exchangeWire: req.exchangeWire,
reserveStatus, reserveStatus,
lastSuccessfulStatusQuery: undefined, lastSuccessfulStatusQuery: undefined,
@ -173,10 +180,10 @@ export async function createReserve(
], ],
async (tx) => { async (tx) => {
// Check if we have already created a reserve for that bankWithdrawStatusUrl // Check if we have already created a reserve for that bankWithdrawStatusUrl
if (reserveRecord.bankWithdrawStatusUrl) { if (reserveRecord.bankInfo?.statusUrl) {
const bwi = await tx.get( const bwi = await tx.get(
Stores.bankWithdrawUris, Stores.bankWithdrawUris,
reserveRecord.bankWithdrawStatusUrl, reserveRecord.bankInfo.statusUrl,
); );
if (bwi) { if (bwi) {
const otherReserve = await tx.get(Stores.reserves, bwi.reservePub); const otherReserve = await tx.get(Stores.reserves, bwi.reservePub);
@ -192,7 +199,7 @@ export async function createReserve(
} }
await tx.put(Stores.bankWithdrawUris, { await tx.put(Stores.bankWithdrawUris, {
reservePub: reserveRecord.reservePub, reservePub: reserveRecord.reservePub,
talerWithdrawUri: reserveRecord.bankWithdrawStatusUrl, talerWithdrawUri: reserveRecord.bankInfo.statusUrl,
}); });
} }
await tx.put(Stores.currencies, cr); await tx.put(Stores.currencies, cr);
@ -279,7 +286,7 @@ async function registerReserveWithBank(
default: default:
return; return;
} }
const bankStatusUrl = reserve.bankWithdrawStatusUrl; const bankStatusUrl = reserve.bankInfo?.statusUrl;
if (!bankStatusUrl) { if (!bankStatusUrl) {
return; return;
} }
@ -333,7 +340,7 @@ async function processReserveBankStatusImpl(
default: default:
return; return;
} }
const bankStatusUrl = reserve.bankWithdrawStatusUrl; const bankStatusUrl = reserve.bankInfo?.statusUrl;
if (!bankStatusUrl) { if (!bankStatusUrl) {
return; return;
} }
@ -382,7 +389,9 @@ async function processReserveBankStatusImpl(
default: default:
return; return;
} }
r.bankWithdrawConfirmUrl = status.confirm_transfer_url; if (r.bankInfo) {
r.bankInfo.confirmUrl = status.confirm_transfer_url;
}
return r; return r;
}); });
await incrementReserveRetry(ws, reservePub, undefined); await incrementReserveRetry(ws, reservePub, undefined);
@ -673,35 +682,7 @@ async function depleteReserve(
logger.trace("selected denominations"); logger.trace("selected denominations");
const withdrawalGroupId = encodeCrock(randomBytes(32)); const newWithdrawalGroup = await ws.db.runWithWriteTransaction(
logger.trace("created plachets");
const withdrawalRecord: WithdrawalGroupRecord = {
withdrawalGroupId: withdrawalGroupId,
exchangeBaseUrl: reserve.exchangeBaseUrl,
source: {
type: WithdrawalSourceType.Reserve,
reservePub: reserve.reservePub,
},
rawWithdrawalAmount: withdrawAmount,
timestampStart: getTimestampNow(),
retryInfo: initRetryInfo(),
lastErrorPerCoin: {},
lastError: undefined,
denomsSel: {
totalCoinValue: denomsForWithdraw.totalCoinValue,
totalWithdrawCost: denomsForWithdraw.totalWithdrawCost,
selectedDenoms: denomsForWithdraw.selectedDenoms.map((x) => {
return {
count: x.count,
denomPubHash: x.denom.denomPubHash,
};
}),
},
};
const success = await ws.db.runWithWriteTransaction(
[ [
Stores.withdrawalGroups, Stores.withdrawalGroups,
Stores.reserves, Stores.reserves,
@ -748,20 +729,55 @@ async function depleteReserve(
} }
newReserve.reserveStatus = ReserveRecordStatus.DORMANT; newReserve.reserveStatus = ReserveRecordStatus.DORMANT;
newReserve.retryInfo = initRetryInfo(false); newReserve.retryInfo = initRetryInfo(false);
let withdrawalGroupId: string;
const bankInfo = newReserve.bankInfo;
if (bankInfo && !bankInfo.withdrawalStarted) {
withdrawalGroupId = bankInfo.bankWithdrawalGroupId;
bankInfo.withdrawalStarted = true;
} else {
withdrawalGroupId = encodeCrock(randomBytes(32));
}
const withdrawalRecord: WithdrawalGroupRecord = {
withdrawalGroupId: withdrawalGroupId,
exchangeBaseUrl: newReserve.exchangeBaseUrl,
source: {
type: WithdrawalSourceType.Reserve,
reservePub: newReserve.reservePub,
},
rawWithdrawalAmount: withdrawAmount,
timestampStart: getTimestampNow(),
retryInfo: initRetryInfo(),
lastErrorPerCoin: {},
lastError: undefined,
denomsSel: {
totalCoinValue: denomsForWithdraw.totalCoinValue,
totalWithdrawCost: denomsForWithdraw.totalWithdrawCost,
selectedDenoms: denomsForWithdraw.selectedDenoms.map((x) => {
return {
count: x.count,
denomPubHash: x.denom.denomPubHash,
};
}),
},
};
await tx.put(Stores.reserves, newReserve); await tx.put(Stores.reserves, newReserve);
await tx.put(Stores.reserveHistory, newHist); await tx.put(Stores.reserveHistory, newHist);
await tx.put(Stores.withdrawalGroups, withdrawalRecord); await tx.put(Stores.withdrawalGroups, withdrawalRecord);
return true; return withdrawalRecord;
}, },
); );
if (success) { if (newWithdrawalGroup) {
console.log("processing new withdraw group"); console.log("processing new withdraw group");
ws.notify({ ws.notify({
type: NotificationType.WithdrawGroupCreated, type: NotificationType.WithdrawGroupCreated,
withdrawalGroupId: withdrawalGroupId, withdrawalGroupId: newWithdrawalGroup.withdrawalGroupId,
}); });
await processWithdrawGroup(ws, withdrawalGroupId); await processWithdrawGroup(ws, newWithdrawalGroup.withdrawalGroupId);
} else { } else {
console.trace("withdraw session already existed"); console.trace("withdraw session already existed");
} }

View File

@ -0,0 +1,130 @@
/*
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";
import { Stores, ProposalRecord, ReserveRecordStatus } from "../types/dbTypes";
import { Amounts } from "../util/amounts";
import { timestampCmp } from "../util/time";
import {
TransactionsRequest,
TransactionsResponse,
Transaction,
TransactionType,
} from "../types/transactions";
import { OrderShortInfo } from "../types/history";
/**
* 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(";");
}
function getOrderShortInfo(
proposal: ProposalRecord,
): OrderShortInfo | undefined {
const download = proposal.download;
if (!download) {
return undefined;
}
return {
amount: Amounts.stringify(download.contractData.amount),
fulfillmentUrl: download.contractData.fulfillmentUrl,
orderId: download.contractData.orderId,
merchantBaseUrl: download.contractData.merchantBaseUrl,
proposalId: proposal.proposalId,
summary: download.contractData.summary,
};
}
/**
* 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,
Stores.proposals,
Stores.purchases,
Stores.refreshGroups,
Stores.reserves,
Stores.reserveHistory,
Stores.tips,
Stores.withdrawalGroups,
Stores.payEvents,
Stores.planchets,
Stores.refundEvents,
Stores.reserveUpdatedEvents,
Stores.recoupGroups,
],
async (tx) => {
tx.iter(Stores.withdrawalGroups).forEach((wsr) => {
if (wsr.timestampFinish) {
transactions.push({
type: TransactionType.Withdrawal,
amountEffective: Amounts.stringify(wsr.denomsSel.totalWithdrawCost),
amountRaw: Amounts.stringify(wsr.denomsSel.totalCoinValue),
confirmed: true,
exchangeBaseUrl: wsr.exchangeBaseUrl,
pending: !wsr.timestampFinish,
timestamp: wsr.timestampStart,
transactionId: makeEventId(
TransactionType.Withdrawal,
wsr.withdrawalGroupId,
),
});
}
});
tx.iter(Stores.reserves).forEach((r) => {
if (r.reserveStatus !== ReserveRecordStatus.WAIT_CONFIRM_BANK) {
return;
}
if (!r.bankInfo) {
return;
}
transactions.push({
type: TransactionType.Withdrawal,
confirmed: false,
amountRaw: Amounts.stringify(r.bankInfo.amount),
amountEffective: undefined,
exchangeBaseUrl: undefined,
pending: true,
timestamp: r.timestampCreated,
bankConfirmationUrl: r.bankInfo.confirmUrl,
transactionId: makeEventId(
TransactionType.Withdrawal,
r.bankInfo.bankWithdrawalGroupId,
),
});
});
},
);
transactions.sort((h1, h2) => timestampCmp(h1.timestamp, h2.timestamp));
return { transactions };
}

View File

@ -273,13 +273,17 @@ export interface ReserveRecord {
*/ */
exchangeWire: string; exchangeWire: string;
bankWithdrawStatusUrl?: string;
/** /**
* URL that the bank gave us to redirect the customer * Extra state for when this is a withdrawal involving
* to in order to confirm a withdrawal. * a Taler-integrated bank.
*/ */
bankWithdrawConfirmUrl?: string; bankInfo?: {
statusUrl: string;
confirmUrl?: string;
amount: AmountJson;
bankWithdrawalGroupId: string;
withdrawalStarted: boolean;
};
reserveStatus: ReserveRecordStatus; reserveStatus: ReserveRecordStatus;

208
src/types/transactions.ts Normal file
View File

@ -0,0 +1,208 @@
/*
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/>
*/
/**
* Type and schema definitions for the wallet's transaction list.
*/
/**
* Imports.
*/
import { Timestamp } from "../util/time";
import { AmountString } from "./talerTypes";
export interface TransactionsRequest {
/**
* return only transactions in the given currency
*/
currency?: string;
/**
* if present, results will be limited to transactions related to the given search string
*/
search?: string;
}
export interface TransactionsResponse {
// a list of past and pending transactions sorted by pending, timestamp and transactionId.
// In case two events are both pending and have the same timestamp,
// they are sorted by the transactionId
// (lexically ascending and locale-independent comparison).
transactions: Transaction[];
}
export interface TransactionCommon {
// opaque unique ID for the transaction, used as a starting point for paginating queries
// and for invoking actions on the transaction (e.g. deleting/hiding it from the history)
transactionId: string;
// the type of the transaction; different types might provide additional information
type: TransactionType;
// main timestamp of the transaction
timestamp: Timestamp;
// true if the transaction is still pending, false otherwise
// If a transaction is not longer pending, its timestamp will be updated,
// but its transactionId will remain unchanged
pending: boolean;
// Raw amount of the transaction (exclusive of fees or other extra costs)
amountRaw: AmountString;
// Amount added or removed from the wallet's balance (including all fees and other costs)
amountEffective?: AmountString;
}
export type Transaction = (
TransactionWithdrawal |
TransactionPayment |
TransactionRefund |
TransactionTip |
TransactionRefresh
)
export const enum TransactionType {
Withdrawal = "withdrawal",
Payment = "payment",
Refund = "refund",
Refresh = "refresh",
Tip = "tip",
}
// This should only be used for actual withdrawals
// and not for tips that have their own transactions type.
interface TransactionWithdrawal extends TransactionCommon {
type: TransactionType.Withdrawal;
/**
* Exchange of the withdrawal.
*/
exchangeBaseUrl?: string;
// true if the bank has confirmed the withdrawal, false if not.
// An unconfirmed withdrawal usually requires user-input and should be highlighted in the UI.
// See also bankConfirmationUrl below.
confirmed: boolean;
// If the withdrawal is unconfirmed, this can include a URL for user initiated confirmation.
bankConfirmationUrl?: string;
// Amount that has been subtracted from the reserve's balance for this withdrawal.
amountRaw: AmountString;
/**
* Amount that actually was (or will be) added to the wallet's balance.
* Only present if an exchange has already been selected.
*/
amountEffective?: AmountString;
}
interface TransactionPayment extends TransactionCommon {
type: TransactionType.Payment;
// Additional information about the payment.
info: TransactionInfo;
// true if the payment failed, false otherwise.
// Note that failed payments with zero effective amount will not be returned by the API.
failed: boolean;
// Amount that must be paid for the contract
amountRaw: AmountString;
// Amount that was paid, including deposit, wire and refresh fees.
amountEffective: AmountString;
}
interface TransactionInfo {
// Order ID, uniquely identifies the order within a merchant instance
orderId: string;
// More information about the merchant
merchant: any;
// Summary of the order, given by the merchant
summary: string;
// Map from IETF BCP 47 language tags to localized summaries
summary_i18n?: { [lang_tag: string]: string };
// List of products that are part of the order
products: any[];
// URL of the fulfillment, given by the merchant
fulfillmentUrl: string;
}
interface TransactionRefund extends TransactionCommon {
type: TransactionType.Refund;
// ID for the transaction that is refunded
refundedTransactionId: string;
// Additional information about the refunded payment
info: TransactionInfo;
// Part of the refund that couldn't be applied because the refund permissions were expired
amountInvalid: AmountString;
// Amount that has been refunded by the merchant
amountRaw: AmountString;
// Amount will be added to the wallet's balance after fees and refreshing
amountEffective: AmountString;
}
interface TransactionTip extends TransactionCommon {
type: TransactionType.Tip;
// true if the user still needs to accept/decline this tip
waiting: boolean;
// true if the user has accepted this top, false otherwise
accepted: boolean;
// Exchange that the tip will be (or was) withdrawn from
exchangeBaseUrl: string;
// More information about the merchant that sent the tip
merchant: any;
// Raw amount of the tip, without extra fees that apply
amountRaw: AmountString;
// Amount will be (or was) added to the wallet's balance after fees and refreshing
amountEffective: AmountString;
}
// A transaction shown for refreshes that are not associated to other transactions
// such as a refresh necessary before coin expiration.
// It should only be returned by the API if the effective amount is different from zero.
interface TransactionRefresh extends TransactionCommon {
type: TransactionType.Refresh;
// Exchange that the coins are refreshed with
exchangeBaseUrl: string;
// Raw amount that is refreshed
amountRaw: AmountString;
// Amount that will be paid as fees for the refresh
amountEffective: AmountString;
}

View File

@ -112,6 +112,8 @@ import {
import { durationMin, Duration } from "./util/time"; import { durationMin, Duration } from "./util/time";
import { processRecoupGroup } from "./operations/recoup"; import { processRecoupGroup } from "./operations/recoup";
import { OperationFailedAndReportedError } from "./operations/errors"; import { OperationFailedAndReportedError } from "./operations/errors";
import { TransactionsRequest, TransactionsResponse } from "./types/transactions";
import { getTransactions } from "./operations/transactions";
const builtinCurrencies: CurrencyRecord[] = [ const builtinCurrencies: CurrencyRecord[] = [
{ {
@ -815,4 +817,8 @@ export class Wallet {
} }
return coinsJson; return coinsJson;
} }
async getTransactions(request: TransactionsRequest): Promise<TransactionsResponse> {
return getTransactions(this.ws, request);
}
} }