From 75c5c59316a428fbebe2448d9d79a70689565657 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 16 Jul 2020 14:44:59 +0530 Subject: report manual withdrawals properly in transaction list --- src/operations/reserves.ts | 31 ++++++------ src/operations/transactions.ts | 110 ++++++++++++++++++++++++++++++----------- src/operations/withdraw.ts | 4 +- src/types/dbTypes.ts | 27 ++++++++-- src/types/transactions.ts | 44 ++++++++++++++--- src/types/walletTypes.ts | 2 +- src/wallet.ts | 5 +- src/webex/messages.ts | 2 +- src/webex/pages/withdraw.tsx | 6 +-- src/webex/wxApi.ts | 4 +- 10 files changed, 167 insertions(+), 68 deletions(-) diff --git a/src/operations/reserves.ts b/src/operations/reserves.ts index 2761dfaf9..ff20ce9ba 100644 --- a/src/operations/reserves.ts +++ b/src/operations/reserves.ts @@ -53,7 +53,6 @@ import { processWithdrawGroup, getBankWithdrawalInfo, denomSelectionInfoToState, - getWithdrawDenomList, } from "./withdraw"; import { guardOperationException, @@ -106,22 +105,25 @@ export async function createReserve( let bankInfo: ReserveBankInfo | undefined; if (req.bankWithdrawStatusUrl) { - const denomSelInfo = await selectWithdrawalDenoms( - ws, - canonExchange, - req.amount, - ); - const denomSel = denomSelectionInfoToState(denomSelInfo); bankInfo = { statusUrl: req.bankWithdrawStatusUrl, - amount: req.amount, - bankWithdrawalGroupId: encodeCrock(getRandomBytes(32)), - withdrawalStarted: false, - denomSel, }; } + const initialWithdrawalGroupId = encodeCrock(getRandomBytes(32)); + + const denomSelInfo = await selectWithdrawalDenoms( + ws, + canonExchange, + req.amount, + ); + const initialDenomSel = denomSelectionInfoToState(denomSelInfo); + const reserveRecord: ReserveRecord = { + instructedAmount: req.amount, + initialWithdrawalGroupId, + initialDenomSel, + initialWithdrawalStarted: false, timestampCreated: now, exchangeBaseUrl: canonExchange, reservePriv: keypair.priv, @@ -750,10 +752,9 @@ async function depleteReserve( let withdrawalGroupId: string; - const bankInfo = newReserve.bankInfo; - if (bankInfo && !bankInfo.withdrawalStarted) { - withdrawalGroupId = bankInfo.bankWithdrawalGroupId; - bankInfo.withdrawalStarted = true; + if (!newReserve.initialWithdrawalStarted) { + withdrawalGroupId = newReserve.initialWithdrawalGroupId; + newReserve.initialWithdrawalStarted = true; } else { withdrawalGroupId = encodeCrock(randomBytes(32)); } diff --git a/src/operations/transactions.ts b/src/operations/transactions.ts index 9a3d48bb3..f2845cb18 100644 --- a/src/operations/transactions.ts +++ b/src/operations/transactions.ts @@ -32,7 +32,10 @@ import { Transaction, TransactionType, PaymentStatus, + WithdrawalType, + WithdrawalDetails, } from "../types/transactions"; +import { WithdrawalDetailsResponse } from "../types/walletTypes"; /** * Create an event ID from the type and the primary key for the event. @@ -156,6 +159,7 @@ export async function getTransactions( Stores.reserveUpdatedEvents, Stores.recoupGroups, ], + // Report withdrawals that are currently in progress. async (tx) => { tx.iter(Stores.withdrawalGroups).forEachAsync(async (wsr) => { if ( @@ -171,34 +175,62 @@ export async function getTransactions( return; } - let amountRaw: AmountJson | undefined = undefined; - - if (wsr.source.type === WithdrawalSourceType.Reserve) { - const r = await tx.get(Stores.reserves, wsr.source.reservePub); - if (r?.bankInfo?.amount) { - amountRaw = r.bankInfo.amount; + switch (wsr.source.type) { + 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) { + withdrawalDetails = { + type: WithdrawalType.TalerBankIntegrationApi, + confirmed: true, + bankConfirmationUrl: r.bankInfo.confirmUrl, + }; + } else { + const exchange = await tx.get(Stores.exchanges, r.exchangeBaseUrl); + if (!exchange) { + // FIXME: report somehow + break; + } + withdrawalDetails = { + type: WithdrawalType.ManualTransfer, + reservePublicKey: r.reservePub, + exchangePaytoUris: exchange.wireInfo?.accounts.map((x) => x.payto_uri) ?? [], + }; + } + 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, + ), + }); } + break; + default: + // Tips are reported via their own event + break; } - if (!amountRaw) { - amountRaw = wsr.denomsSel.totalWithdrawCost; - } - - transactions.push({ - type: TransactionType.Withdrawal, - amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), - amountRaw: Amounts.stringify(amountRaw), - confirmed: true, - exchangeBaseUrl: wsr.exchangeBaseUrl, - pending: !wsr.timestampFinish, - timestamp: wsr.timestampStart, - transactionId: makeEventId( - TransactionType.Withdrawal, - wsr.withdrawalGroupId, - ), - }); }); - tx.iter(Stores.reserves).forEach((r) => { + // 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) => { if (shouldSkipCurrency(transactionsRequest, r.currency)) { return; } @@ -213,23 +245,41 @@ export async function getTransactions( default: return; } - if (!r.bankInfo) { + if (r.initialWithdrawalStarted) { return; } + let withdrawalDetails: WithdrawalDetails; + if (r.bankInfo) { + withdrawalDetails = { + type: WithdrawalType.TalerBankIntegrationApi, + confirmed: false, + bankConfirmationUrl: r.bankInfo.confirmUrl, + } + } else { + const exchange = await tx.get(Stores.exchanges, r.exchangeBaseUrl); + if (!exchange) { + // FIXME: report somehow + return; + } + withdrawalDetails = { + type: WithdrawalType.ManualTransfer, + reservePublicKey: r.reservePub, + exchangePaytoUris: exchange.wireInfo?.accounts.map((x) => x.payto_uri) ?? [], + }; + } transactions.push({ type: TransactionType.Withdrawal, - confirmed: false, - amountRaw: Amounts.stringify(r.bankInfo.amount), + amountRaw: Amounts.stringify(r.instructedAmount), amountEffective: Amounts.stringify( - r.bankInfo.denomSel.totalCoinValue, + r.initialDenomSel.totalCoinValue, ), exchangeBaseUrl: r.exchangeBaseUrl, pending: true, timestamp: r.timestampCreated, - bankConfirmationUrl: r.bankInfo.confirmUrl, + withdrawalDetails: withdrawalDetails, transactionId: makeEventId( TransactionType.Withdrawal, - r.bankInfo.bankWithdrawalGroupId, + r.initialWithdrawalGroupId, ), }); }); diff --git a/src/operations/withdraw.ts b/src/operations/withdraw.ts index 284743415..fd850f140 100644 --- a/src/operations/withdraw.ts +++ b/src/operations/withdraw.ts @@ -32,7 +32,7 @@ import { import { BankWithdrawDetails, ExchangeWithdrawDetails, - WithdrawDetails, + WithdrawalDetailsResponse, OperationError, } from "../types/walletTypes"; import { @@ -708,7 +708,7 @@ export async function getWithdrawDetailsForUri( ws: InternalWalletState, talerWithdrawUri: string, maybeSelectedExchange?: string, -): Promise { +): Promise { const info = await getBankWithdrawalInfo(ws, talerWithdrawUri); let rci: ExchangeWithdrawDetails | undefined = undefined; if (maybeSelectedExchange) { diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts index 6693e22a2..55f16f40b 100644 --- a/src/types/dbTypes.ts +++ b/src/types/dbTypes.ts @@ -221,10 +221,6 @@ export interface ReserveHistoryRecord { export interface ReserveBankInfo { statusUrl: string; confirmUrl?: string; - amount: AmountJson; - bankWithdrawalGroupId: string; - withdrawalStarted: boolean; - denomSel: DenomSelectionState; } /** @@ -285,12 +281,28 @@ export interface ReserveRecord { */ exchangeWire: string; + /** + * Amount that was sent by the user to fund the reserve. + */ + instructedAmount: AmountJson; + /** * Extra state for when this is a withdrawal involving * a Taler-integrated bank. */ bankInfo?: ReserveBankInfo; + initialWithdrawalGroupId: string; + + /** + * Did we start the first withdrawal for this reserve? + * + * We only report a pending withdrawal for the reserve before + * the first withdrawal has started. + */ + initialWithdrawalStarted: boolean; + initialDenomSel: DenomSelectionState; + reserveStatus: ReserveRecordStatus; /** @@ -1436,6 +1448,13 @@ export interface DenomSelectionState { }[]; } +/** + * Group of withdrawal operations that need to be executed. + * (Either for a normal withdrawal or from a tip.) + * + * The withdrawal group record is only created after we know + * the coin selection we want to withdraw. + */ export interface WithdrawalGroupRecord { withdrawalGroupId: string; diff --git a/src/types/transactions.ts b/src/types/transactions.ts index 6ed9a52d4..aa618cd4e 100644 --- a/src/types/transactions.ts +++ b/src/types/transactions.ts @@ -105,18 +105,35 @@ export const enum TransactionType { 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; +export const enum WithdrawalType { + TalerBankIntegrationApi = "taler-bank-integration-api", + ManualTransfer = "manual-transfer", +} + +export type WithdrawalDetails = + | WithdrawalDetailsForManualTransfer + | WithdrawalDetailsForTalerBankIntegrationApi; + +interface WithdrawalDetailsForManualTransfer { + type: WithdrawalType.ManualTransfer; /** - * Exchange of the withdrawal. + * Public key of the reserve that needs to be funded + * manually. + */ + reservePublicKey: string; + + /** + * Payto URIs that the exchange supports. */ - exchangeBaseUrl?: string; + exchangePaytoUris: string[]; +} + +interface WithdrawalDetailsForTalerBankIntegrationApi { + type: WithdrawalType.TalerBankIntegrationApi; /** - * true if the bank has confirmed the withdrawal, false if not. + * Set to 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. */ @@ -127,6 +144,17 @@ interface TransactionWithdrawal extends TransactionCommon { * initiated confirmation. */ bankConfirmationUrl?: string; +} + +// 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; /** * Amount that got subtracted from the reserve balance. @@ -137,6 +165,8 @@ interface TransactionWithdrawal extends TransactionCommon { * Amount that actually was (or will be) added to the wallet's balance. */ amountEffective: AmountString; + + withdrawalDetails: WithdrawalDetails; } export const enum PaymentStatus { diff --git a/src/types/walletTypes.ts b/src/types/walletTypes.ts index 4b6d867a2..74f2428dd 100644 --- a/src/types/walletTypes.ts +++ b/src/types/walletTypes.ts @@ -146,7 +146,7 @@ export interface ExchangeWithdrawDetails { walletVersion: string; } -export interface WithdrawDetails { +export interface WithdrawalDetailsResponse { bankWithdrawDetails: BankWithdrawDetails; exchangeWithdrawDetails: ExchangeWithdrawDetails | undefined; } diff --git a/src/wallet.ts b/src/wallet.ts index e04c849d5..737704fd6 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -64,10 +64,9 @@ import { TipStatus, WalletBalance, PreparePayResult, - WithdrawDetails, + WithdrawalDetailsResponse, AcceptWithdrawalResponse, PurchaseDetails, - ExchangeWithdrawDetails as ExchangeWithdrawalDetails, RefreshReason, ExchangeListItem, ExchangesListRespose, @@ -477,7 +476,7 @@ export class Wallet { async getWithdrawDetailsForUri( talerWithdrawUri: string, maybeSelectedExchange?: string, - ): Promise { + ): Promise { return getWithdrawDetailsForUri( this.ws, talerWithdrawUri, diff --git a/src/webex/messages.ts b/src/webex/messages.ts index 8120d4f94..5cf2fefdb 100644 --- a/src/webex/messages.ts +++ b/src/webex/messages.ts @@ -146,7 +146,7 @@ export interface MessageMap { talerWithdrawUri: string; maybeSelectedExchange: string | undefined; }; - response: walletTypes.WithdrawDetails; + response: walletTypes.WithdrawalDetailsResponse; }; "accept-withdrawal": { request: { talerWithdrawUri: string; selectedExchange: string }; diff --git a/src/webex/pages/withdraw.tsx b/src/webex/pages/withdraw.tsx index d8ac3c455..c4e4ebbb9 100644 --- a/src/webex/pages/withdraw.tsx +++ b/src/webex/pages/withdraw.tsx @@ -23,7 +23,7 @@ import * as i18n from "../i18n"; -import { WithdrawDetails } from "../../types/walletTypes"; +import { WithdrawalDetailsResponse } from "../../types/walletTypes"; import { WithdrawDetailView, renderAmount } from "../renderHtml"; @@ -35,7 +35,7 @@ import { } from "../wxApi"; function WithdrawalDialog(props: { talerWithdrawUri: string }): JSX.Element { - const [details, setDetails] = useState(); + const [details, setDetails] = useState(); const [selectedExchange, setSelectedExchange] = useState< string | undefined >(); @@ -56,7 +56,7 @@ function WithdrawalDialog(props: { talerWithdrawUri: string }): JSX.Element { useEffect(() => { const fetchData = async (): Promise => { console.log("getting from", talerWithdrawUri); - let d: WithdrawDetails | undefined = undefined; + let d: WithdrawalDetailsResponse | undefined = undefined; try { d = await getWithdrawDetails(talerWithdrawUri, selectedExchange); } catch (e) { diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts index 0901005b5..47e73ca4c 100644 --- a/src/webex/wxApi.ts +++ b/src/webex/wxApi.ts @@ -38,7 +38,7 @@ import { WalletBalance, PurchaseDetails, WalletDiagnostics, - WithdrawDetails, + WithdrawalDetailsResponse, PreparePayResult, AcceptWithdrawalResponse, ExtendedPermissionsResponse, @@ -283,7 +283,7 @@ export function benchmarkCrypto(repetitions: number): Promise { export function getWithdrawDetails( talerWithdrawUri: string, maybeSelectedExchange: string | undefined, -): Promise { +): Promise { return callBackend("get-withdraw-details", { talerWithdrawUri, maybeSelectedExchange, -- cgit v1.2.3