new transactions API: withdrawal
This commit is contained in:
parent
857a2b9dca
commit
6206b418ff
@ -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> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
setTimeout(() => resolve(), milliSeconds);
|
||||
|
@ -375,10 +375,10 @@ export async function getHistory(
|
||||
return;
|
||||
}
|
||||
let reserveCreationDetail: ReserveCreationDetail;
|
||||
if (reserve.bankWithdrawStatusUrl) {
|
||||
if (reserve.bankInfo) {
|
||||
reserveCreationDetail = {
|
||||
type: ReserveType.TalerBankWithdraw,
|
||||
bankUrl: reserve.bankWithdrawStatusUrl,
|
||||
bankUrl: reserve.bankInfo.statusUrl,
|
||||
};
|
||||
} else {
|
||||
reserveCreationDetail = {
|
||||
|
@ -150,7 +150,7 @@ async function gatherReservePending(
|
||||
): Promise<void> {
|
||||
// FIXME: this should be optimized by using an index for "onlyDue==true".
|
||||
await tx.iter(Stores.reserves).forEach((reserve) => {
|
||||
const reserveType = reserve.bankWithdrawStatusUrl
|
||||
const reserveType = reserve.bankInfo
|
||||
? ReserveType.TalerBankWithdraw
|
||||
: ReserveType.Manual;
|
||||
if (!reserve.retryInfo.active) {
|
||||
|
@ -108,7 +108,14 @@ export async function createReserve(
|
||||
senderWire: req.senderWire,
|
||||
timestampConfirmed: 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,
|
||||
reserveStatus,
|
||||
lastSuccessfulStatusQuery: undefined,
|
||||
@ -173,10 +180,10 @@ export async function createReserve(
|
||||
],
|
||||
async (tx) => {
|
||||
// Check if we have already created a reserve for that bankWithdrawStatusUrl
|
||||
if (reserveRecord.bankWithdrawStatusUrl) {
|
||||
if (reserveRecord.bankInfo?.statusUrl) {
|
||||
const bwi = await tx.get(
|
||||
Stores.bankWithdrawUris,
|
||||
reserveRecord.bankWithdrawStatusUrl,
|
||||
reserveRecord.bankInfo.statusUrl,
|
||||
);
|
||||
if (bwi) {
|
||||
const otherReserve = await tx.get(Stores.reserves, bwi.reservePub);
|
||||
@ -192,7 +199,7 @@ export async function createReserve(
|
||||
}
|
||||
await tx.put(Stores.bankWithdrawUris, {
|
||||
reservePub: reserveRecord.reservePub,
|
||||
talerWithdrawUri: reserveRecord.bankWithdrawStatusUrl,
|
||||
talerWithdrawUri: reserveRecord.bankInfo.statusUrl,
|
||||
});
|
||||
}
|
||||
await tx.put(Stores.currencies, cr);
|
||||
@ -279,7 +286,7 @@ async function registerReserveWithBank(
|
||||
default:
|
||||
return;
|
||||
}
|
||||
const bankStatusUrl = reserve.bankWithdrawStatusUrl;
|
||||
const bankStatusUrl = reserve.bankInfo?.statusUrl;
|
||||
if (!bankStatusUrl) {
|
||||
return;
|
||||
}
|
||||
@ -333,7 +340,7 @@ async function processReserveBankStatusImpl(
|
||||
default:
|
||||
return;
|
||||
}
|
||||
const bankStatusUrl = reserve.bankWithdrawStatusUrl;
|
||||
const bankStatusUrl = reserve.bankInfo?.statusUrl;
|
||||
if (!bankStatusUrl) {
|
||||
return;
|
||||
}
|
||||
@ -382,7 +389,9 @@ async function processReserveBankStatusImpl(
|
||||
default:
|
||||
return;
|
||||
}
|
||||
r.bankWithdrawConfirmUrl = status.confirm_transfer_url;
|
||||
if (r.bankInfo) {
|
||||
r.bankInfo.confirmUrl = status.confirm_transfer_url;
|
||||
}
|
||||
return r;
|
||||
});
|
||||
await incrementReserveRetry(ws, reservePub, undefined);
|
||||
@ -673,35 +682,7 @@ async function depleteReserve(
|
||||
|
||||
logger.trace("selected denominations");
|
||||
|
||||
const withdrawalGroupId = encodeCrock(randomBytes(32));
|
||||
|
||||
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(
|
||||
const newWithdrawalGroup = await ws.db.runWithWriteTransaction(
|
||||
[
|
||||
Stores.withdrawalGroups,
|
||||
Stores.reserves,
|
||||
@ -748,20 +729,55 @@ async function depleteReserve(
|
||||
}
|
||||
newReserve.reserveStatus = ReserveRecordStatus.DORMANT;
|
||||
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.reserveHistory, newHist);
|
||||
await tx.put(Stores.withdrawalGroups, withdrawalRecord);
|
||||
return true;
|
||||
return withdrawalRecord;
|
||||
},
|
||||
);
|
||||
|
||||
if (success) {
|
||||
if (newWithdrawalGroup) {
|
||||
console.log("processing new withdraw group");
|
||||
ws.notify({
|
||||
type: NotificationType.WithdrawGroupCreated,
|
||||
withdrawalGroupId: withdrawalGroupId,
|
||||
withdrawalGroupId: newWithdrawalGroup.withdrawalGroupId,
|
||||
});
|
||||
await processWithdrawGroup(ws, withdrawalGroupId);
|
||||
await processWithdrawGroup(ws, newWithdrawalGroup.withdrawalGroupId);
|
||||
} else {
|
||||
console.trace("withdraw session already existed");
|
||||
}
|
||||
|
130
src/operations/transactions.ts
Normal file
130
src/operations/transactions.ts
Normal 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 };
|
||||
}
|
@ -273,13 +273,17 @@ export interface ReserveRecord {
|
||||
*/
|
||||
exchangeWire: string;
|
||||
|
||||
bankWithdrawStatusUrl?: string;
|
||||
|
||||
/**
|
||||
* URL that the bank gave us to redirect the customer
|
||||
* to in order to confirm a withdrawal.
|
||||
* Extra state for when this is a withdrawal involving
|
||||
* a Taler-integrated bank.
|
||||
*/
|
||||
bankWithdrawConfirmUrl?: string;
|
||||
bankInfo?: {
|
||||
statusUrl: string;
|
||||
confirmUrl?: string;
|
||||
amount: AmountJson;
|
||||
bankWithdrawalGroupId: string;
|
||||
withdrawalStarted: boolean;
|
||||
};
|
||||
|
||||
reserveStatus: ReserveRecordStatus;
|
||||
|
||||
|
208
src/types/transactions.ts
Normal file
208
src/types/transactions.ts
Normal 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;
|
||||
}
|
@ -112,6 +112,8 @@ import {
|
||||
import { durationMin, Duration } from "./util/time";
|
||||
import { processRecoupGroup } from "./operations/recoup";
|
||||
import { OperationFailedAndReportedError } from "./operations/errors";
|
||||
import { TransactionsRequest, TransactionsResponse } from "./types/transactions";
|
||||
import { getTransactions } from "./operations/transactions";
|
||||
|
||||
const builtinCurrencies: CurrencyRecord[] = [
|
||||
{
|
||||
@ -815,4 +817,8 @@ export class Wallet {
|
||||
}
|
||||
return coinsJson;
|
||||
}
|
||||
|
||||
async getTransactions(request: TransactionsRequest): Promise<TransactionsResponse> {
|
||||
return getTransactions(this.ws, request);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user