finish refresh correctly, display fees correctly

This commit is contained in:
Florian Dold 2019-12-16 21:10:57 +01:00
parent c2ee8fd9ab
commit fb6508de9d
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
8 changed files with 190 additions and 107 deletions

View File

@ -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 <http://www.gnu.org/licenses/>
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
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);

View File

@ -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,

View File

@ -224,6 +224,8 @@ async function gatherRefreshPending(
type: PendingOperationType.Refresh,
givesLifeness: true,
refreshGroupId: r.refreshGroupId,
finishedPerCoin: r.finishedPerCoin,
retryInfo: r.retryInfo,
});
});
}

View File

@ -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;
}

View File

@ -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 (cmp > 0) {
const extra = Amounts.sub(balance, expectedBalance.amount).amount;
r.amountWithdrawRemaining = Amounts.add(
r.amountWithdrawRemaining,
extra,
).amount;
} else {
// We're missing some money.
if (r.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) {
return;
}
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 });
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.
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);
}
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,
};
}
}

View File

@ -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.
*/

View File

@ -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 {

View File

@ -87,6 +87,8 @@ export interface PendingRefreshOperation {
type: PendingOperationType.Refresh;
lastError?: OperationError;
refreshGroupId: string;
finishedPerCoin: boolean[];
retryInfo: RetryInfo;
}
export interface PendingProposalDownloadOperation {