From fb6508de9d71600dbca59cb0e6a4c77e4f3f3ee5 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 16 Dec 2019 21:10:57 +0100 Subject: finish refresh correctly, display fees correctly --- src/headless/taler-wallet-cli.ts | 30 ++++++-- src/operations/history.ts | 69 ++++++++++++----- src/operations/pending.ts | 2 + src/operations/refresh.ts | 14 +++- src/operations/reserves.ts | 161 +++++++++++++++++++++------------------ src/types/history.ts | 10 +++ src/types/notifications.ts | 7 +- src/types/pending.ts | 2 + 8 files changed, 189 insertions(+), 106 deletions(-) (limited to 'src') diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts index bc83bac2f..610990ae4 100644 --- a/src/headless/taler-wallet-cli.ts +++ b/src/headless/taler-wallet-cli.ts @@ -1,17 +1,17 @@ /* - This file is part of TALER - (C) 2019 GNUnet e.V. + This file is part of GNU Taler + (C) 2019 Taler Systems S.A. - TALER is free software; you can redistribute it and/or modify it under the + 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. - TALER is distributed in the hope that it will be useful, but WITHOUT ANY + 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 - TALER; see the file COPYING. If not, see + GNU Taler; see the file COPYING. If not, see */ import os = require("os"); @@ -167,7 +167,10 @@ walletCli }); walletCli - .subcommand("", "history", { help: "Show wallet event history." }) + .subcommand("history", "history", { help: "Show wallet event history." }) + .flag("json", ["--json"], { + default: false, + }) .maybeOption("from", ["--from"], clk.STRING) .maybeOption("to", ["--to"], clk.STRING) .maybeOption("limit", ["--limit"], clk.STRING) @@ -175,7 +178,17 @@ walletCli .action(async args => { await withWallet(args, async wallet => { const history = await wallet.getHistory(); - console.log(JSON.stringify(history, undefined, 2)); + if (args.history.json) { + console.log(JSON.stringify(history, undefined, 2)); + } else { + for (const h of history.history) { + console.log( + `event at ${new Date(h.timestamp.t_ms).toISOString()} with type ${h.type}:`, + ); + console.log(JSON.stringify(h, undefined, 2)); + console.log(); + } + } }); }); @@ -231,7 +244,8 @@ walletCli case TalerUriType.TalerWithdraw: { const withdrawInfo = await wallet.getWithdrawDetailsForUri(uri); - const selectedExchange = withdrawInfo.bankWithdrawDetails.suggestedExchange; + const selectedExchange = + withdrawInfo.bankWithdrawDetails.suggestedExchange; if (!selectedExchange) { console.error("no suggested exchange!"); process.exit(1); diff --git a/src/operations/history.ts b/src/operations/history.ts index eec398f37..bb57a9c60 100644 --- a/src/operations/history.ts +++ b/src/operations/history.ts @@ -61,7 +61,6 @@ function getOrderShortInfo( }; } - async function collectProposalHistory( tx: TransactionHandle, history: HistoryEvent[], @@ -162,6 +161,7 @@ export async function getHistory( await ws.db.runWithReadTransaction( [ Stores.currencies, + Stores.coins, Stores.exchanges, Stores.exchangeUpdatedEvents, Stores.proposals, @@ -220,15 +220,22 @@ export async function getHistory( await collectProposalHistory(tx, history, historyQuery); - await tx.iter(Stores.payEvents).forEachAsync(async (pe) => { + await tx.iter(Stores.payEvents).forEachAsync(async pe => { const proposal = await tx.get(Stores.proposals, pe.proposalId); if (!proposal) { return; } + const purchase = await tx.get(Stores.purchases, pe.proposalId); + if (!purchase) { + return; + } const orderShortInfo = getOrderShortInfo(proposal); if (!orderShortInfo) { return; } + const amountPaidWithFees = Amounts.sum( + purchase.payReq.coins.map(x => Amounts.parseOrThrow(x.contribution)), + ).amount; history.push({ type: HistoryEventType.PaymentSent, eventId: makeEventId(HistoryEventType.PaymentSent, pe.proposalId), @@ -236,10 +243,12 @@ export async function getHistory( replay: pe.isReplay, sessionId: pe.sessionId, timestamp: pe.timestamp, + numCoins: purchase.payReq.coins.length, + amountPaidWithFees: Amounts.toString(amountPaidWithFees), }); }); - await tx.iter(Stores.refreshGroups).forEachAsync(async (rg) => { + await tx.iter(Stores.refreshGroups).forEachAsync(async rg => { if (!rg.timestampFinished) { return; } @@ -251,23 +260,26 @@ export async function getHistory( for (let i = 0; i < rg.refreshSessionPerCoin.length; i++) { const session = rg.refreshSessionPerCoin[i]; numInputCoins++; + const c = await tx.get(Stores.coins, rg.oldCoinPubs[i]); + if (!c) { + continue; + } if (session) { numRefreshedInputCoins++; amountsRaw.push(session.amountRefreshInput); + amountsRaw.push(c.currentAmount); amountsEffective.push(session.amountRefreshOutput); numOutputCoins += session.newDenoms.length; } else { - const c = await tx.get(Stores.coins, rg.oldCoinPubs[i]); - if (!c) { - continue; - } amountsRaw.push(c.currentAmount); } } let amountRefreshedRaw = Amounts.sum(amountsRaw).amount; let amountRefreshedEffective: AmountJson; if (amountsEffective.length == 0) { - amountRefreshedEffective = Amounts.getZero(amountRefreshedRaw.currency); + amountRefreshedEffective = Amounts.getZero( + amountRefreshedRaw.currency, + ); } else { amountRefreshedEffective = Amounts.sum(amountsEffective).amount; } @@ -285,7 +297,7 @@ export async function getHistory( }); }); - tx.iter(Stores.reserveUpdatedEvents).forEachAsync(async (ru) => { + tx.iter(Stores.reserveUpdatedEvents).forEachAsync(async ru => { const reserve = await tx.get(Stores.reserves, ru.reservePub); if (!reserve) { return; @@ -295,28 +307,31 @@ export async function getHistory( reserveCreationDetail = { type: ReserveType.TalerBankWithdraw, bankUrl: reserve.bankWithdrawStatusUrl, - } + }; } else { reserveCreationDetail = { type: ReserveType.Manual, - } + }; } history.push({ type: HistoryEventType.ReserveBalanceUpdated, - eventId: makeEventId(HistoryEventType.ReserveBalanceUpdated, ru.reserveUpdateId), + eventId: makeEventId( + HistoryEventType.ReserveBalanceUpdated, + ru.reserveUpdateId, + ), amountExpected: ru.amountExpected, amountReserveBalance: ru.amountReserveBalance, - timestamp: reserve.timestampCreated, + timestamp: ru.timestamp, newHistoryTransactions: ru.newHistoryTransactions, reserveShortInfo: { exchangeBaseUrl: reserve.exchangeBaseUrl, reserveCreationDetail, reservePub: reserve.reservePub, - } + }, }); }); - tx.iter(Stores.tips).forEach((tip) => { + tx.iter(Stores.tips).forEach(tip => { if (tip.acceptedTimestamp) { history.push({ type: HistoryEventType.TipAccepted, @@ -328,7 +343,7 @@ export async function getHistory( } }); - tx.iter(Stores.refundEvents).forEachAsync(async (re) => { + tx.iter(Stores.refundEvents).forEachAsync(async re => { const proposal = await tx.get(Stores.proposals, re.proposalId); if (!proposal) { return; @@ -341,7 +356,9 @@ export async function getHistory( if (!orderShortInfo) { return; } - const purchaseAmount = Amounts.parseOrThrow(purchase.contractTerms.amount); + const purchaseAmount = Amounts.parseOrThrow( + purchase.contractTerms.amount, + ); let amountRefundedRaw = Amounts.getZero(purchaseAmount.currency); let amountRefundedInvalid = Amounts.getZero(purchaseAmount.currency); let amountRefundedEffective = Amounts.getZero(purchaseAmount.currency); @@ -352,9 +369,16 @@ export async function getHistory( } const refundAmount = Amounts.parseOrThrow(r.perm.refund_amount); const refundFee = Amounts.parseOrThrow(r.perm.refund_fee); - amountRefundedRaw = Amounts.add(amountRefundedRaw, refundAmount).amount; - amountRefundedEffective = Amounts.add(amountRefundedEffective, refundAmount).amount; - amountRefundedEffective = Amounts.sub(amountRefundedEffective, refundFee).amount; + amountRefundedRaw = Amounts.add(amountRefundedRaw, refundAmount) + .amount; + amountRefundedEffective = Amounts.add( + amountRefundedEffective, + refundAmount, + ).amount; + amountRefundedEffective = Amounts.sub( + amountRefundedEffective, + refundFee, + ).amount; }); Object.keys(purchase.refundState.refundsFailed).forEach((x, i) => { const r = purchase.refundState.refundsFailed[x]; @@ -365,7 +389,10 @@ export async function getHistory( const refundFee = Amounts.parseOrThrow(r.perm.refund_fee); amountRefundedRaw = Amounts.add(amountRefundedRaw, ra).amount; amountRefundedInvalid = Amounts.add(amountRefundedInvalid, ra).amount; - amountRefundedEffective = Amounts.sub(amountRefundedEffective, refundFee).amount; + amountRefundedEffective = Amounts.sub( + amountRefundedEffective, + refundFee, + ).amount; }); history.push({ type: HistoryEventType.Refund, diff --git a/src/operations/pending.ts b/src/operations/pending.ts index ffa23f101..360180854 100644 --- a/src/operations/pending.ts +++ b/src/operations/pending.ts @@ -224,6 +224,8 @@ async function gatherRefreshPending( type: PendingOperationType.Refresh, givesLifeness: true, refreshGroupId: r.refreshGroupId, + finishedPerCoin: r.finishedPerCoin, + retryInfo: r.retryInfo, }); }); } diff --git a/src/operations/refresh.ts b/src/operations/refresh.ts index f602221af..8390cac54 100644 --- a/src/operations/refresh.ts +++ b/src/operations/refresh.ts @@ -144,10 +144,22 @@ async function refreshCreateSession( return; } rg.finishedPerCoin[coinIndex] = true; + rg.finishedPerCoin[coinIndex] = true; + let allDone = true; + for (const f of rg.finishedPerCoin) { + if (!f) { + allDone = false; + break; + } + } + if (allDone) { + rg.timestampFinished = getTimestampNow(); + rg.retryInfo = initRetryInfo(false); + } await tx.put(Stores.refreshGroups, rg); }, ); - ws.notify({ type: NotificationType.RefreshRefused }); + ws.notify({ type: NotificationType.RefreshUnwarranted }); return; } diff --git a/src/operations/reserves.ts b/src/operations/reserves.ts index 649bf75f2..7be927824 100644 --- a/src/operations/reserves.ts +++ b/src/operations/reserves.ts @@ -34,12 +34,14 @@ import { updateRetryInfoTimeout, ReserveUpdatedEventRecord, } from "../types/dbTypes"; -import { - TransactionAbort, -} from "../util/query"; +import { TransactionAbort } from "../util/query"; import { Logger } from "../util/logging"; import * as Amounts from "../util/amounts"; -import { updateExchangeFromUrl, getExchangeTrust, getExchangePaytoUri } from "./exchanges"; +import { + updateExchangeFromUrl, + getExchangeTrust, + getExchangePaytoUri, +} from "./exchanges"; import { WithdrawOperationStatusResponse } from "../types/talerTypes"; import { assertUnreachable } from "../util/assertUnreachable"; import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto"; @@ -49,7 +51,10 @@ import { processWithdrawSession, getBankWithdrawalInfo, } from "./withdraw"; -import { guardOperationException, OperationFailedAndReportedError } from "./errors"; +import { + guardOperationException, + OperationFailedAndReportedError, +} from "./errors"; import { NotificationType } from "../types/notifications"; import { codecForReserveStatus } from "../types/ReserveStatus"; @@ -206,7 +211,6 @@ export async function processReserve( }); } - async function registerReserveWithBank( ws: InternalWalletState, reservePub: string, @@ -231,7 +235,6 @@ async function registerReserveWithBank( reserve_pub: reservePub, selected_exchange: reserve.exchangeWire, }); - console.log("got response", bankResp); await ws.db.mutate(Stores.reserves, reservePub, r => { switch (r.reserveStatus) { case ReserveRecordStatus.REGISTERING_BANK: @@ -245,7 +248,7 @@ async function registerReserveWithBank( r.retryInfo = initRetryInfo(); return r; }); - ws.notify( { type: NotificationType.Wildcard }); + ws.notify({ type: NotificationType.Wildcard }); return processReserveBankStatus(ws, reservePub); } @@ -282,14 +285,16 @@ async function processReserveBankStatusImpl( try { const statusResp = await ws.http.get(bankStatusUrl); if (statusResp.status !== 200) { - throw Error(`unexpected status ${statusResp.status} for bank status query`); + throw Error( + `unexpected status ${statusResp.status} for bank status query`, + ); } status = WithdrawOperationStatusResponse.checked(await statusResp.json()); } catch (e) { throw e; } - ws.notify( { type: NotificationType.Wildcard }); + ws.notify({ type: NotificationType.Wildcard }); if (status.selection_done) { if (reserve.reserveStatus === ReserveRecordStatus.REGISTERING_BANK) { @@ -330,7 +335,7 @@ async function processReserveBankStatusImpl( }); await incrementReserveRetry(ws, reservePub, undefined); } - ws.notify( { type: NotificationType.Wildcard }); + ws.notify({ type: NotificationType.Wildcard }); } async function incrementReserveRetry( @@ -351,7 +356,12 @@ async function incrementReserveRetry( r.lastError = err; await tx.put(Stores.reserves, r); }); - ws.notify({ type: NotificationType.ReserveOperationError }); + if (err) { + ws.notify({ + type: NotificationType.ReserveOperationError, + operationError: err, + }); + } } /** @@ -386,7 +396,7 @@ async function updateReserve( return; } if (resp.status !== 200) { - throw Error(`unexpected status code ${resp.status} for reserve/status`) + throw Error(`unexpected status code ${resp.status} for reserve/status`); } } catch (e) { const m = e.message; @@ -400,68 +410,73 @@ async function updateReserve( const respJson = await resp.json(); const reserveInfo = codecForReserveStatus.decode(respJson); const balance = Amounts.parseOrThrow(reserveInfo.balance); - await ws.db.runWithWriteTransaction([Stores.reserves, Stores.reserveUpdatedEvents], async (tx) => { - const r = await tx.get(Stores.reserves, reservePub); - if (!r) { - return; - } - if (r.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) { - return; - } - - const newHistoryTransactions = reserveInfo.history.slice(r.reserveTransactions.length); - - const reserveUpdateId = encodeCrock(getRandomBytes(32)); - - // FIXME: check / compare history! - if (!r.lastSuccessfulStatusQuery) { - // FIXME: check if this matches initial expectations - r.amountWithdrawRemaining = balance; - const reserveUpdate: ReserveUpdatedEventRecord = { - reservePub: r.reservePub, - timestamp: getTimestampNow(), - amountReserveBalance: Amounts.toString(balance), - amountExpected: Amounts.toString(reserve.amountInitiallyRequested), - newHistoryTransactions, - reserveUpdateId, - }; - await tx.put(Stores.reserveUpdatedEvents, reserveUpdate); - } else { - const expectedBalance = Amounts.sub( - r.amountWithdrawAllocated, - r.amountWithdrawCompleted, - ); - const cmp = Amounts.cmp(balance, expectedBalance.amount); - if (cmp == 0) { - // Nothing changed. + await ws.db.runWithWriteTransaction( + [Stores.reserves, Stores.reserveUpdatedEvents], + async tx => { + const r = await tx.get(Stores.reserves, reservePub); + if (!r) { + return; + } + if (r.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) { return; } - if (cmp > 0) { - const extra = Amounts.sub(balance, expectedBalance.amount).amount; - r.amountWithdrawRemaining = Amounts.add( - r.amountWithdrawRemaining, - extra, - ).amount; + + const newHistoryTransactions = reserveInfo.history.slice( + r.reserveTransactions.length, + ); + + const reserveUpdateId = encodeCrock(getRandomBytes(32)); + + // FIXME: check / compare history! + if (!r.lastSuccessfulStatusQuery) { + // FIXME: check if this matches initial expectations + r.amountWithdrawRemaining = balance; + const reserveUpdate: ReserveUpdatedEventRecord = { + reservePub: r.reservePub, + timestamp: getTimestampNow(), + amountReserveBalance: Amounts.toString(balance), + amountExpected: Amounts.toString(reserve.amountInitiallyRequested), + newHistoryTransactions, + reserveUpdateId, + }; + await tx.put(Stores.reserveUpdatedEvents, reserveUpdate); } else { - // We're missing some money. + const expectedBalance = Amounts.sub( + r.amountWithdrawAllocated, + r.amountWithdrawCompleted, + ); + const cmp = Amounts.cmp(balance, expectedBalance.amount); + if (cmp == 0) { + // Nothing changed. + return; + } + if (cmp > 0) { + const extra = Amounts.sub(balance, expectedBalance.amount).amount; + r.amountWithdrawRemaining = Amounts.add( + r.amountWithdrawRemaining, + extra, + ).amount; + } else { + // We're missing some money. + } + const reserveUpdate: ReserveUpdatedEventRecord = { + reservePub: r.reservePub, + timestamp: getTimestampNow(), + amountReserveBalance: Amounts.toString(balance), + amountExpected: Amounts.toString(expectedBalance.amount), + newHistoryTransactions, + reserveUpdateId, + }; + await tx.put(Stores.reserveUpdatedEvents, reserveUpdate); } - const reserveUpdate: ReserveUpdatedEventRecord = { - reservePub: r.reservePub, - timestamp: getTimestampNow(), - amountReserveBalance: Amounts.toString(balance), - amountExpected: Amounts.toString(expectedBalance.amount), - newHistoryTransactions, - reserveUpdateId, - }; - await tx.put(Stores.reserveUpdatedEvents, reserveUpdate); - } - r.lastSuccessfulStatusQuery = getTimestampNow(); - r.reserveStatus = ReserveRecordStatus.WITHDRAWING; - r.retryInfo = initRetryInfo(); - r.reserveTransactions = reserveInfo.history; - await tx.put(Stores.reserves, r); - }); - ws.notify( { type: NotificationType.ReserveUpdated }); + r.lastSuccessfulStatusQuery = getTimestampNow(); + r.reserveStatus = ReserveRecordStatus.WITHDRAWING; + r.retryInfo = initRetryInfo(); + r.reserveTransactions = reserveInfo.history; + await tx.put(Stores.reserves, r); + }, + ); + ws.notify({ type: NotificationType.ReserveUpdated }); } async function processReserveImpl( @@ -655,8 +670,6 @@ async function depleteReserve( } } - - export async function createTalerWithdrawReserve( ws: InternalWalletState, talerWithdrawUri: string, @@ -683,4 +696,4 @@ export async function createTalerWithdrawReserve( reservePub: reserve.reservePub, confirmTransferUrl: withdrawInfo.confirmTransferUrl, }; -} \ No newline at end of file +} diff --git a/src/types/history.ts b/src/types/history.ts index 8b46276be..aa35ab962 100644 --- a/src/types/history.ts +++ b/src/types/history.ts @@ -485,6 +485,16 @@ export interface HistoryPaymentSent { */ replay: boolean; + /** + * Number of coins that were involved in the payment. + */ + numCoins: number; + + /** + * Amount that was paid, including deposit and wire fees. + */ + amountPaidWithFees: string; + /** * Session ID that the payment was (re-)submitted under. */ diff --git a/src/types/notifications.ts b/src/types/notifications.ts index c64d33bfb..30ede151c 100644 --- a/src/types/notifications.ts +++ b/src/types/notifications.ts @@ -1,3 +1,5 @@ +import { OperationError } from "./walletTypes"; + /* This file is part of GNU Taler (C) 2019 GNUnet e.V. @@ -29,7 +31,7 @@ export const enum NotificationType { RefreshRevealed = "refresh-revealed", RefreshMelted = "refresh-melted", RefreshStarted = "refresh-started", - RefreshRefused = "refresh-refused", + RefreshUnwarranted = "refresh-unwarranted", ReserveUpdated = "reserve-updated", ReserveConfirmed = "reserve-confirmed", ReserveDepleted = "reserve-depleted", @@ -100,7 +102,7 @@ export interface RefreshStartedNotification { } export interface RefreshRefusedNotification { - type: NotificationType.RefreshRefused; + type: NotificationType.RefreshUnwarranted; } export interface ReserveUpdatedNotification { @@ -170,6 +172,7 @@ export interface WithdrawOperationErrorNotification { export interface ReserveOperationErrorNotification { type: NotificationType.ReserveOperationError; + operationError: OperationError; } export interface ReserveCreatedNotification { diff --git a/src/types/pending.ts b/src/types/pending.ts index 53932e8f3..efb97f536 100644 --- a/src/types/pending.ts +++ b/src/types/pending.ts @@ -87,6 +87,8 @@ export interface PendingRefreshOperation { type: PendingOperationType.Refresh; lastError?: OperationError; refreshGroupId: string; + finishedPerCoin: boolean[]; + retryInfo: RetryInfo; } export interface PendingProposalDownloadOperation { -- cgit v1.2.3