summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2020-05-12 14:08:58 +0530
committerFlorian Dold <florian.dold@gmail.com>2020-05-12 14:09:10 +0530
commit6206b418ff88a238762a18e7b6eeaceafc5de294 (patch)
treec4d947770ccf50e362abf083953f55140046fc2a
parent857a2b9dcaf64d4298027644f8e6716fa22db941 (diff)
downloadwallet-core-6206b418ff88a238762a18e7b6eeaceafc5de294.tar.gz
wallet-core-6206b418ff88a238762a18e7b6eeaceafc5de294.tar.bz2
wallet-core-6206b418ff88a238762a18e7b6eeaceafc5de294.zip
new transactions API: withdrawal
-rw-r--r--src/headless/taler-wallet-cli.ts9
-rw-r--r--src/operations/history.ts4
-rw-r--r--src/operations/pending.ts2
-rw-r--r--src/operations/reserves.ts96
-rw-r--r--src/operations/transactions.ts130
-rw-r--r--src/types/dbTypes.ts14
-rw-r--r--src/types/transactions.ts208
-rw-r--r--src/wallet.ts6
8 files changed, 421 insertions, 48 deletions
diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts
index 483a9e7ce..3e9d993d8 100644
--- a/src/headless/taler-wallet-cli.ts
+++ b/src/headless/taler-wallet-cli.ts
@@ -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);
diff --git a/src/operations/history.ts b/src/operations/history.ts
index 1271c56ef..4e43596f0 100644
--- a/src/operations/history.ts
+++ b/src/operations/history.ts
@@ -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 = {
diff --git a/src/operations/pending.ts b/src/operations/pending.ts
index 14072633c..c793f5f0a 100644
--- a/src/operations/pending.ts
+++ b/src/operations/pending.ts
@@ -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) {
diff --git a/src/operations/reserves.ts b/src/operations/reserves.ts
index 2bbb085d5..347f6e894 100644
--- a/src/operations/reserves.ts
+++ b/src/operations/reserves.ts
@@ -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");
}
diff --git a/src/operations/transactions.ts b/src/operations/transactions.ts
new file mode 100644
index 000000000..8333b66c6
--- /dev/null
+++ b/src/operations/transactions.ts
@@ -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 };
+}
diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts
index 4cf19a56e..07c59d4d3 100644
--- a/src/types/dbTypes.ts
+++ b/src/types/dbTypes.ts
@@ -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;
diff --git a/src/types/transactions.ts b/src/types/transactions.ts
new file mode 100644
index 000000000..d2f0f6cbc
--- /dev/null
+++ b/src/types/transactions.ts
@@ -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;
+} \ No newline at end of file
diff --git a/src/wallet.ts b/src/wallet.ts
index 3558e102b..2d63e2298 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -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);
+ }
}