report manual withdrawals properly in transaction list

This commit is contained in:
Florian Dold 2020-07-16 14:44:59 +05:30
parent c6d80b0128
commit 75c5c59316
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
10 changed files with 167 additions and 68 deletions

View File

@ -53,7 +53,6 @@ import {
processWithdrawGroup, processWithdrawGroup,
getBankWithdrawalInfo, getBankWithdrawalInfo,
denomSelectionInfoToState, denomSelectionInfoToState,
getWithdrawDenomList,
} from "./withdraw"; } from "./withdraw";
import { import {
guardOperationException, guardOperationException,
@ -106,22 +105,25 @@ export async function createReserve(
let bankInfo: ReserveBankInfo | undefined; let bankInfo: ReserveBankInfo | undefined;
if (req.bankWithdrawStatusUrl) { if (req.bankWithdrawStatusUrl) {
bankInfo = {
statusUrl: req.bankWithdrawStatusUrl,
};
}
const initialWithdrawalGroupId = encodeCrock(getRandomBytes(32));
const denomSelInfo = await selectWithdrawalDenoms( const denomSelInfo = await selectWithdrawalDenoms(
ws, ws,
canonExchange, canonExchange,
req.amount, req.amount,
); );
const denomSel = denomSelectionInfoToState(denomSelInfo); const initialDenomSel = denomSelectionInfoToState(denomSelInfo);
bankInfo = {
statusUrl: req.bankWithdrawStatusUrl,
amount: req.amount,
bankWithdrawalGroupId: encodeCrock(getRandomBytes(32)),
withdrawalStarted: false,
denomSel,
};
}
const reserveRecord: ReserveRecord = { const reserveRecord: ReserveRecord = {
instructedAmount: req.amount,
initialWithdrawalGroupId,
initialDenomSel,
initialWithdrawalStarted: false,
timestampCreated: now, timestampCreated: now,
exchangeBaseUrl: canonExchange, exchangeBaseUrl: canonExchange,
reservePriv: keypair.priv, reservePriv: keypair.priv,
@ -750,10 +752,9 @@ async function depleteReserve(
let withdrawalGroupId: string; let withdrawalGroupId: string;
const bankInfo = newReserve.bankInfo; if (!newReserve.initialWithdrawalStarted) {
if (bankInfo && !bankInfo.withdrawalStarted) { withdrawalGroupId = newReserve.initialWithdrawalGroupId;
withdrawalGroupId = bankInfo.bankWithdrawalGroupId; newReserve.initialWithdrawalStarted = true;
bankInfo.withdrawalStarted = true;
} else { } else {
withdrawalGroupId = encodeCrock(randomBytes(32)); withdrawalGroupId = encodeCrock(randomBytes(32));
} }

View File

@ -32,7 +32,10 @@ import {
Transaction, Transaction,
TransactionType, TransactionType,
PaymentStatus, PaymentStatus,
WithdrawalType,
WithdrawalDetails,
} from "../types/transactions"; } from "../types/transactions";
import { WithdrawalDetailsResponse } from "../types/walletTypes";
/** /**
* Create an event ID from the type and the primary key for the event. * 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.reserveUpdatedEvents,
Stores.recoupGroups, Stores.recoupGroups,
], ],
// Report withdrawals that are currently in progress.
async (tx) => { async (tx) => {
tx.iter(Stores.withdrawalGroups).forEachAsync(async (wsr) => { tx.iter(Stores.withdrawalGroups).forEachAsync(async (wsr) => {
if ( if (
@ -171,23 +175,42 @@ export async function getTransactions(
return; return;
} }
let amountRaw: AmountJson | undefined = undefined; switch (wsr.source.type) {
case WithdrawalSourceType.Reserve: {
if (wsr.source.type === WithdrawalSourceType.Reserve) {
const r = await tx.get(Stores.reserves, wsr.source.reservePub); const r = await tx.get(Stores.reserves, wsr.source.reservePub);
if (r?.bankInfo?.amount) { if (!r) {
amountRaw = r.bankInfo.amount; break;
} }
} let amountRaw: AmountJson | undefined = undefined;
if (!amountRaw) { if (wsr.withdrawalGroupId === r.initialWithdrawalGroupId) {
amountRaw = r.instructedAmount;
} else {
amountRaw = wsr.denomsSel.totalWithdrawCost; 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({ transactions.push({
type: TransactionType.Withdrawal, type: TransactionType.Withdrawal,
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue), amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
amountRaw: Amounts.stringify(amountRaw), amountRaw: Amounts.stringify(amountRaw),
confirmed: true, withdrawalDetails,
exchangeBaseUrl: wsr.exchangeBaseUrl, exchangeBaseUrl: wsr.exchangeBaseUrl,
pending: !wsr.timestampFinish, pending: !wsr.timestampFinish,
timestamp: wsr.timestampStart, timestamp: wsr.timestampStart,
@ -196,9 +219,18 @@ export async function getTransactions(
wsr.withdrawalGroupId, wsr.withdrawalGroupId,
), ),
}); });
}
break;
default:
// Tips are reported via their own event
break;
}
}); });
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)) { if (shouldSkipCurrency(transactionsRequest, r.currency)) {
return; return;
} }
@ -213,23 +245,41 @@ export async function getTransactions(
default: default:
return; return;
} }
if (!r.bankInfo) { if (r.initialWithdrawalStarted) {
return; 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({ transactions.push({
type: TransactionType.Withdrawal, type: TransactionType.Withdrawal,
confirmed: false, amountRaw: Amounts.stringify(r.instructedAmount),
amountRaw: Amounts.stringify(r.bankInfo.amount),
amountEffective: Amounts.stringify( amountEffective: Amounts.stringify(
r.bankInfo.denomSel.totalCoinValue, r.initialDenomSel.totalCoinValue,
), ),
exchangeBaseUrl: r.exchangeBaseUrl, exchangeBaseUrl: r.exchangeBaseUrl,
pending: true, pending: true,
timestamp: r.timestampCreated, timestamp: r.timestampCreated,
bankConfirmationUrl: r.bankInfo.confirmUrl, withdrawalDetails: withdrawalDetails,
transactionId: makeEventId( transactionId: makeEventId(
TransactionType.Withdrawal, TransactionType.Withdrawal,
r.bankInfo.bankWithdrawalGroupId, r.initialWithdrawalGroupId,
), ),
}); });
}); });

View File

@ -32,7 +32,7 @@ import {
import { import {
BankWithdrawDetails, BankWithdrawDetails,
ExchangeWithdrawDetails, ExchangeWithdrawDetails,
WithdrawDetails, WithdrawalDetailsResponse,
OperationError, OperationError,
} from "../types/walletTypes"; } from "../types/walletTypes";
import { import {
@ -708,7 +708,7 @@ export async function getWithdrawDetailsForUri(
ws: InternalWalletState, ws: InternalWalletState,
talerWithdrawUri: string, talerWithdrawUri: string,
maybeSelectedExchange?: string, maybeSelectedExchange?: string,
): Promise<WithdrawDetails> { ): Promise<WithdrawalDetailsResponse> {
const info = await getBankWithdrawalInfo(ws, talerWithdrawUri); const info = await getBankWithdrawalInfo(ws, talerWithdrawUri);
let rci: ExchangeWithdrawDetails | undefined = undefined; let rci: ExchangeWithdrawDetails | undefined = undefined;
if (maybeSelectedExchange) { if (maybeSelectedExchange) {

View File

@ -221,10 +221,6 @@ export interface ReserveHistoryRecord {
export interface ReserveBankInfo { export interface ReserveBankInfo {
statusUrl: string; statusUrl: string;
confirmUrl?: string; confirmUrl?: string;
amount: AmountJson;
bankWithdrawalGroupId: string;
withdrawalStarted: boolean;
denomSel: DenomSelectionState;
} }
/** /**
@ -285,12 +281,28 @@ export interface ReserveRecord {
*/ */
exchangeWire: string; exchangeWire: string;
/**
* Amount that was sent by the user to fund the reserve.
*/
instructedAmount: AmountJson;
/** /**
* Extra state for when this is a withdrawal involving * Extra state for when this is a withdrawal involving
* a Taler-integrated bank. * a Taler-integrated bank.
*/ */
bankInfo?: ReserveBankInfo; 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; 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 { export interface WithdrawalGroupRecord {
withdrawalGroupId: string; withdrawalGroupId: string;

View File

@ -105,18 +105,35 @@ export const enum TransactionType {
Tip = "tip", Tip = "tip",
} }
// This should only be used for actual withdrawals export const enum WithdrawalType {
// and not for tips that have their own transactions type. TalerBankIntegrationApi = "taler-bank-integration-api",
interface TransactionWithdrawal extends TransactionCommon { ManualTransfer = "manual-transfer",
type: TransactionType.Withdrawal; }
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.
*/ */
exchangeBaseUrl?: string; reservePublicKey: string;
/** /**
* true if the bank has confirmed the withdrawal, false if not. * Payto URIs that the exchange supports.
*/
exchangePaytoUris: string[];
}
interface WithdrawalDetailsForTalerBankIntegrationApi {
type: WithdrawalType.TalerBankIntegrationApi;
/**
* 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. * An unconfirmed withdrawal usually requires user-input and should be highlighted in the UI.
* See also bankConfirmationUrl below. * See also bankConfirmationUrl below.
*/ */
@ -127,6 +144,17 @@ interface TransactionWithdrawal extends TransactionCommon {
* initiated confirmation. * initiated confirmation.
*/ */
bankConfirmationUrl?: string; 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. * 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. * Amount that actually was (or will be) added to the wallet's balance.
*/ */
amountEffective: AmountString; amountEffective: AmountString;
withdrawalDetails: WithdrawalDetails;
} }
export const enum PaymentStatus { export const enum PaymentStatus {

View File

@ -146,7 +146,7 @@ export interface ExchangeWithdrawDetails {
walletVersion: string; walletVersion: string;
} }
export interface WithdrawDetails { export interface WithdrawalDetailsResponse {
bankWithdrawDetails: BankWithdrawDetails; bankWithdrawDetails: BankWithdrawDetails;
exchangeWithdrawDetails: ExchangeWithdrawDetails | undefined; exchangeWithdrawDetails: ExchangeWithdrawDetails | undefined;
} }

View File

@ -64,10 +64,9 @@ import {
TipStatus, TipStatus,
WalletBalance, WalletBalance,
PreparePayResult, PreparePayResult,
WithdrawDetails, WithdrawalDetailsResponse,
AcceptWithdrawalResponse, AcceptWithdrawalResponse,
PurchaseDetails, PurchaseDetails,
ExchangeWithdrawDetails as ExchangeWithdrawalDetails,
RefreshReason, RefreshReason,
ExchangeListItem, ExchangeListItem,
ExchangesListRespose, ExchangesListRespose,
@ -477,7 +476,7 @@ export class Wallet {
async getWithdrawDetailsForUri( async getWithdrawDetailsForUri(
talerWithdrawUri: string, talerWithdrawUri: string,
maybeSelectedExchange?: string, maybeSelectedExchange?: string,
): Promise<WithdrawDetails> { ): Promise<WithdrawalDetailsResponse> {
return getWithdrawDetailsForUri( return getWithdrawDetailsForUri(
this.ws, this.ws,
talerWithdrawUri, talerWithdrawUri,

View File

@ -146,7 +146,7 @@ export interface MessageMap {
talerWithdrawUri: string; talerWithdrawUri: string;
maybeSelectedExchange: string | undefined; maybeSelectedExchange: string | undefined;
}; };
response: walletTypes.WithdrawDetails; response: walletTypes.WithdrawalDetailsResponse;
}; };
"accept-withdrawal": { "accept-withdrawal": {
request: { talerWithdrawUri: string; selectedExchange: string }; request: { talerWithdrawUri: string; selectedExchange: string };

View File

@ -23,7 +23,7 @@
import * as i18n from "../i18n"; import * as i18n from "../i18n";
import { WithdrawDetails } from "../../types/walletTypes"; import { WithdrawalDetailsResponse } from "../../types/walletTypes";
import { WithdrawDetailView, renderAmount } from "../renderHtml"; import { WithdrawDetailView, renderAmount } from "../renderHtml";
@ -35,7 +35,7 @@ import {
} from "../wxApi"; } from "../wxApi";
function WithdrawalDialog(props: { talerWithdrawUri: string }): JSX.Element { function WithdrawalDialog(props: { talerWithdrawUri: string }): JSX.Element {
const [details, setDetails] = useState<WithdrawDetails | undefined>(); const [details, setDetails] = useState<WithdrawalDetailsResponse | undefined>();
const [selectedExchange, setSelectedExchange] = useState< const [selectedExchange, setSelectedExchange] = useState<
string | undefined string | undefined
>(); >();
@ -56,7 +56,7 @@ function WithdrawalDialog(props: { talerWithdrawUri: string }): JSX.Element {
useEffect(() => { useEffect(() => {
const fetchData = async (): Promise<void> => { const fetchData = async (): Promise<void> => {
console.log("getting from", talerWithdrawUri); console.log("getting from", talerWithdrawUri);
let d: WithdrawDetails | undefined = undefined; let d: WithdrawalDetailsResponse | undefined = undefined;
try { try {
d = await getWithdrawDetails(talerWithdrawUri, selectedExchange); d = await getWithdrawDetails(talerWithdrawUri, selectedExchange);
} catch (e) { } catch (e) {

View File

@ -38,7 +38,7 @@ import {
WalletBalance, WalletBalance,
PurchaseDetails, PurchaseDetails,
WalletDiagnostics, WalletDiagnostics,
WithdrawDetails, WithdrawalDetailsResponse,
PreparePayResult, PreparePayResult,
AcceptWithdrawalResponse, AcceptWithdrawalResponse,
ExtendedPermissionsResponse, ExtendedPermissionsResponse,
@ -283,7 +283,7 @@ export function benchmarkCrypto(repetitions: number): Promise<BenchmarkResult> {
export function getWithdrawDetails( export function getWithdrawDetails(
talerWithdrawUri: string, talerWithdrawUri: string,
maybeSelectedExchange: string | undefined, maybeSelectedExchange: string | undefined,
): Promise<WithdrawDetails> { ): Promise<WithdrawalDetailsResponse> {
return callBackend("get-withdraw-details", { return callBackend("get-withdraw-details", {
talerWithdrawUri, talerWithdrawUri,
maybeSelectedExchange, maybeSelectedExchange,