model reserve history in the exchange, improve reserve handling logic

This commit is contained in:
Florian Dold 2020-04-02 20:33:01 +05:30
parent 1728e5011e
commit ef0acf06bf
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
27 changed files with 1062 additions and 426 deletions

View File

@ -359,8 +359,8 @@ export class CryptoApi {
return this.doRpc<string>("hashString", 1, str); return this.doRpc<string>("hashString", 1, str);
} }
hashDenomPub(denomPub: string): Promise<string> { hashEncoded(encodedBytes: string): Promise<string> {
return this.doRpc<string>("hashDenomPub", 1, denomPub); return this.doRpc<string>("hashEncoded", 1, encodedBytes);
} }
isValidDenom(denom: DenominationRecord, masterPub: string): Promise<boolean> { isValidDenom(denom: DenominationRecord, masterPub: string): Promise<boolean> {

View File

@ -49,8 +49,7 @@ import {
PlanchetCreationRequest, PlanchetCreationRequest,
DepositInfo, DepositInfo,
} from "../../types/walletTypes"; } from "../../types/walletTypes";
import { AmountJson } from "../../util/amounts"; import { AmountJson, Amounts } from "../../util/amounts";
import * as Amounts from "../../util/amounts";
import * as timer from "../../util/timer"; import * as timer from "../../util/timer";
import { import {
encodeCrock, encodeCrock,
@ -199,6 +198,7 @@ export class CryptoImplementation {
denomPubHash: encodeCrock(denomPubHash), denomPubHash: encodeCrock(denomPubHash),
reservePub: encodeCrock(reservePub), reservePub: encodeCrock(reservePub),
withdrawSig: encodeCrock(sig), withdrawSig: encodeCrock(sig),
coinEvHash: encodeCrock(evHash),
}; };
return planchet; return planchet;
} }
@ -367,7 +367,7 @@ export class CryptoImplementation {
const s: CoinDepositPermission = { const s: CoinDepositPermission = {
coin_pub: depositInfo.coinPub, coin_pub: depositInfo.coinPub,
coin_sig: encodeCrock(coinSig), coin_sig: encodeCrock(coinSig),
contribution: Amounts.toString(depositInfo.spendAmount), contribution: Amounts.stringify(depositInfo.spendAmount),
denom_pub: depositInfo.denomPub, denom_pub: depositInfo.denomPub,
exchange_url: depositInfo.exchangeBaseUrl, exchange_url: depositInfo.exchangeBaseUrl,
ub_sig: depositInfo.denomSig, ub_sig: depositInfo.denomSig,
@ -491,10 +491,10 @@ export class CryptoImplementation {
} }
/** /**
* Hash a denomination public key. * Hash a crockford encoded value.
*/ */
hashDenomPub(denomPub: string): string { hashEncoded(encodedBytes: string): string {
return encodeCrock(hash(decodeCrock(denomPub))); return encodeCrock(hash(decodeCrock(encodedBytes)));
} }
signCoinLink( signCoinLink(

View File

@ -35,6 +35,7 @@ import { Database } from "../util/query";
import { NodeHttpLib } from "./NodeHttpLib"; import { NodeHttpLib } from "./NodeHttpLib";
import { Logger } from "../util/logging"; import { Logger } from "../util/logging";
import { SynchronousCryptoWorkerFactory } from "../crypto/workers/synchronousWorker"; import { SynchronousCryptoWorkerFactory } from "../crypto/workers/synchronousWorker";
import { WithdrawalSourceType } from "../types/dbTypes";
const logger = new Logger("helpers.ts"); const logger = new Logger("helpers.ts");
@ -165,8 +166,9 @@ export async function withdrawTestBalance(
}); });
myWallet.addNotificationListener((n) => { myWallet.addNotificationListener((n) => {
if ( if (
n.type === NotificationType.ReserveDepleted && n.type === NotificationType.WithdrawGroupFinished &&
n.reservePub === reservePub n.withdrawalSource.type === WithdrawalSourceType.Reserve &&
n.withdrawalSource.reservePub === reservePub
) { ) {
resolve(); resolve();
} }

View File

@ -22,9 +22,9 @@ import { getDefaultNodeWallet, withdrawTestBalance } from "./helpers";
import { MerchantBackendConnection } from "./merchant"; import { MerchantBackendConnection } from "./merchant";
import { Logger } from "../util/logging"; import { Logger } from "../util/logging";
import { NodeHttpLib } from "./NodeHttpLib"; import { NodeHttpLib } from "./NodeHttpLib";
import * as Amounts from "../util/amounts";
import { Wallet } from "../wallet"; import { Wallet } from "../wallet";
import { Configuration } from "../util/talerconfig"; import { Configuration } from "../util/talerconfig";
import { Amounts, AmountJson } from "../util/amounts";
const logger = new Logger("integrationtest.ts"); const logger = new Logger("integrationtest.ts");
@ -127,31 +127,31 @@ export async function runIntegrationTest(args: IntegrationTestArgs) {
await myWallet.runUntilDone(); await myWallet.runUntilDone();
console.log("withdrawing test balance for refund"); console.log("withdrawing test balance for refund");
const withdrawAmountTwo: Amounts.AmountJson = { const withdrawAmountTwo: AmountJson = {
currency, currency,
value: 18, value: 18,
fraction: 0, fraction: 0,
}; };
const spendAmountTwo: Amounts.AmountJson = { const spendAmountTwo: AmountJson = {
currency, currency,
value: 7, value: 7,
fraction: 0, fraction: 0,
}; };
const refundAmount: Amounts.AmountJson = { const refundAmount: AmountJson = {
currency, currency,
value: 6, value: 6,
fraction: 0, fraction: 0,
}; };
const spendAmountThree: Amounts.AmountJson = { const spendAmountThree: AmountJson = {
currency, currency,
value: 3, value: 3,
fraction: 0, fraction: 0,
}; };
await withdrawTestBalance( await withdrawTestBalance(
myWallet, myWallet,
Amounts.toString(withdrawAmountTwo), Amounts.stringify(withdrawAmountTwo),
args.bankBaseUrl, args.bankBaseUrl,
args.exchangeBaseUrl, args.exchangeBaseUrl,
); );
@ -162,14 +162,14 @@ export async function runIntegrationTest(args: IntegrationTestArgs) {
let { orderId: refundOrderId } = await makePayment( let { orderId: refundOrderId } = await makePayment(
myWallet, myWallet,
myMerchant, myMerchant,
Amounts.toString(spendAmountTwo), Amounts.stringify(spendAmountTwo),
"order that will be refunded", "order that will be refunded",
); );
const refundUri = await myMerchant.refund( const refundUri = await myMerchant.refund(
refundOrderId, refundOrderId,
"test refund", "test refund",
Amounts.toString(refundAmount), Amounts.stringify(refundAmount),
); );
console.log("refund URI", refundUri); console.log("refund URI", refundUri);
@ -182,7 +182,7 @@ export async function runIntegrationTest(args: IntegrationTestArgs) {
await makePayment( await makePayment(
myWallet, myWallet,
myMerchant, myMerchant,
Amounts.toString(spendAmountThree), Amounts.stringify(spendAmountThree),
"payment after refund", "payment after refund",
); );
@ -240,7 +240,7 @@ export async function runIntegrationTestBasic(cfg: Configuration) {
logger.info("withdrawing test balance"); logger.info("withdrawing test balance");
await withdrawTestBalance( await withdrawTestBalance(
myWallet, myWallet,
Amounts.toString(parsedWithdrawAmount), Amounts.stringify(parsedWithdrawAmount),
bankBaseUrl, bankBaseUrl,
exchangeBaseUrl, exchangeBaseUrl,
); );
@ -258,7 +258,7 @@ export async function runIntegrationTestBasic(cfg: Configuration) {
await makePayment( await makePayment(
myWallet, myWallet,
myMerchant, myMerchant,
Amounts.toString(parsedSpendAmount), Amounts.stringify(parsedSpendAmount),
"hello world", "hello world",
); );
@ -266,24 +266,24 @@ export async function runIntegrationTestBasic(cfg: Configuration) {
await myWallet.runUntilDone(); await myWallet.runUntilDone();
console.log("withdrawing test balance for refund"); console.log("withdrawing test balance for refund");
const withdrawAmountTwo: Amounts.AmountJson = { const withdrawAmountTwo: AmountJson = {
currency, currency,
value: 18, value: 18,
fraction: 0, fraction: 0,
}; };
const spendAmountTwo: Amounts.AmountJson = { const spendAmountTwo: AmountJson = {
currency, currency,
value: 7, value: 7,
fraction: 0, fraction: 0,
}; };
const refundAmount: Amounts.AmountJson = { const refundAmount: AmountJson = {
currency, currency,
value: 6, value: 6,
fraction: 0, fraction: 0,
}; };
const spendAmountThree: Amounts.AmountJson = { const spendAmountThree: AmountJson = {
currency, currency,
value: 3, value: 3,
fraction: 0, fraction: 0,
@ -291,7 +291,7 @@ export async function runIntegrationTestBasic(cfg: Configuration) {
await withdrawTestBalance( await withdrawTestBalance(
myWallet, myWallet,
Amounts.toString(withdrawAmountTwo), Amounts.stringify(withdrawAmountTwo),
bankBaseUrl, bankBaseUrl,
exchangeBaseUrl, exchangeBaseUrl,
); );
@ -302,14 +302,14 @@ export async function runIntegrationTestBasic(cfg: Configuration) {
let { orderId: refundOrderId } = await makePayment( let { orderId: refundOrderId } = await makePayment(
myWallet, myWallet,
myMerchant, myMerchant,
Amounts.toString(spendAmountTwo), Amounts.stringify(spendAmountTwo),
"order that will be refunded", "order that will be refunded",
); );
const refundUri = await myMerchant.refund( const refundUri = await myMerchant.refund(
refundOrderId, refundOrderId,
"test refund", "test refund",
Amounts.toString(refundAmount), Amounts.stringify(refundAmount),
); );
console.log("refund URI", refundUri); console.log("refund URI", refundUri);
@ -322,7 +322,7 @@ export async function runIntegrationTestBasic(cfg: Configuration) {
await makePayment( await makePayment(
myWallet, myWallet,
myMerchant, myMerchant,
Amounts.toString(spendAmountThree), Amounts.stringify(spendAmountThree),
"payment after refund", "payment after refund",
); );

View File

@ -24,7 +24,7 @@ import qrcodeGenerator = require("qrcode-generator");
import * as clk from "./clk"; import * as clk from "./clk";
import { BridgeIDBFactory, MemoryBackend } from "idb-bridge"; import { BridgeIDBFactory, MemoryBackend } from "idb-bridge";
import { Logger } from "../util/logging"; import { Logger } from "../util/logging";
import * as Amounts from "../util/amounts"; import { Amounts } from "../util/amounts";
import { decodeCrock } from "../crypto/talerCrypto"; import { decodeCrock } from "../crypto/talerCrypto";
import { OperationFailedAndReportedError } from "../operations/errors"; import { OperationFailedAndReportedError } from "../operations/errors";
import { Bank } from "./bank"; import { Bank } from "./bank";
@ -190,7 +190,7 @@ walletCli
} else { } else {
const currencies = Object.keys(balance.byCurrency).sort(); const currencies = Object.keys(balance.byCurrency).sort();
for (const c of currencies) { for (const c of currencies) {
console.log(Amounts.toString(balance.byCurrency[c].available)); console.log(Amounts.stringify(balance.byCurrency[c].available));
} }
} }
}); });
@ -356,6 +356,32 @@ advancedCli
fs.writeFileSync(1, decodeCrock(enc.trim())); fs.writeFileSync(1, decodeCrock(enc.trim()));
}); });
const reservesCli = advancedCli.subcommand("reserves", "reserves", {
help: "Manage reserves.",
});
reservesCli
.subcommand("list", "list", {
help: "List reserves.",
})
.action(async (args) => {
await withWallet(args, async (wallet) => {
const reserves = await wallet.getReserves();
console.log(JSON.stringify(reserves, undefined, 2));
});
});
reservesCli
.subcommand("update", "update", {
help: "Update reserve status via exchange.",
})
.requiredArgument("reservePub", clk.STRING)
.action(async (args) => {
await withWallet(args, async (wallet) => {
await wallet.updateReserve(args.update.reservePub);
});
});
advancedCli advancedCli
.subcommand("payPrepare", "pay-prepare", { .subcommand("payPrepare", "pay-prepare", {
help: "Claim an order but don't pay yet.", help: "Claim an order but don't pay yet.",
@ -464,7 +490,7 @@ advancedCli
console.log(` exchange ${coin.exchangeBaseUrl}`); console.log(` exchange ${coin.exchangeBaseUrl}`);
console.log(` denomPubHash ${coin.denomPubHash}`); console.log(` denomPubHash ${coin.denomPubHash}`);
console.log( console.log(
` remaining amount ${Amounts.toString(coin.currentAmount)}`, ` remaining amount ${Amounts.stringify(coin.currentAmount)}`,
); );
} }
}); });

View File

@ -106,7 +106,7 @@ export async function getBalancesInsideTransaction(
} }
}); });
await tx.iter(Stores.withdrawalSession).forEach((wds) => { await tx.iter(Stores.withdrawalGroups).forEach((wds) => {
let w = wds.totalCoinValue; let w = wds.totalCoinValue;
for (let i = 0; i < wds.planchets.length; i++) { for (let i = 0; i < wds.planchets.length; i++) {
if (wds.withdrawn[i]) { if (wds.withdrawn[i]) {
@ -150,7 +150,7 @@ export async function getBalances(
Stores.refreshGroups, Stores.refreshGroups,
Stores.reserves, Stores.reserves,
Stores.purchases, Stores.purchases,
Stores.withdrawalSession, Stores.withdrawalGroups,
], ],
async (tx) => { async (tx) => {
return getBalancesInsideTransaction(ws, tx); return getBalancesInsideTransaction(ws, tx);

View File

@ -53,7 +53,7 @@ async function denominationRecordFromKeys(
exchangeBaseUrl: string, exchangeBaseUrl: string,
denomIn: Denomination, denomIn: Denomination,
): Promise<DenominationRecord> { ): Promise<DenominationRecord> {
const denomPubHash = await ws.cryptoApi.hashDenomPub(denomIn.denom_pub); const denomPubHash = await ws.cryptoApi.hashEncoded(denomIn.denom_pub);
const d: DenominationRecord = { const d: DenominationRecord = {
denomPub: denomIn.denom_pub, denomPub: denomIn.denom_pub,
denomPubHash, denomPubHash,

View File

@ -26,7 +26,7 @@ import {
PlanchetRecord, PlanchetRecord,
CoinRecord, CoinRecord,
} from "../types/dbTypes"; } from "../types/dbTypes";
import * as Amounts from "../util/amounts"; import { Amounts } from "../util/amounts";
import { AmountJson } from "../util/amounts"; import { AmountJson } from "../util/amounts";
import { import {
HistoryQuery, HistoryQuery,
@ -42,6 +42,7 @@ import {
import { assertUnreachable } from "../util/assertUnreachable"; import { assertUnreachable } from "../util/assertUnreachable";
import { TransactionHandle, Store } from "../util/query"; import { TransactionHandle, Store } from "../util/query";
import { timestampCmp } from "../util/time"; import { timestampCmp } from "../util/time";
import { summarizeReserveHistory } from "../util/reserveHistoryUtil";
/** /**
* 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.
@ -58,7 +59,7 @@ function getOrderShortInfo(
return undefined; return undefined;
} }
return { return {
amount: Amounts.toString(download.contractData.amount), amount: Amounts.stringify(download.contractData.amount),
fulfillmentUrl: download.contractData.fulfillmentUrl, fulfillmentUrl: download.contractData.fulfillmentUrl,
orderId: download.contractData.orderId, orderId: download.contractData.orderId,
merchantBaseUrl: download.contractData.merchantBaseUrl, merchantBaseUrl: download.contractData.merchantBaseUrl,
@ -176,7 +177,7 @@ export async function getHistory(
Stores.refreshGroups, Stores.refreshGroups,
Stores.reserves, Stores.reserves,
Stores.tips, Stores.tips,
Stores.withdrawalSession, Stores.withdrawalGroups,
Stores.payEvents, Stores.payEvents,
Stores.refundEvents, Stores.refundEvents,
Stores.reserveUpdatedEvents, Stores.reserveUpdatedEvents,
@ -208,7 +209,7 @@ export async function getHistory(
}); });
}); });
tx.iter(Stores.withdrawalSession).forEach((wsr) => { tx.iter(Stores.withdrawalGroups).forEach((wsr) => {
if (wsr.timestampFinish) { if (wsr.timestampFinish) {
const cs: PlanchetRecord[] = []; const cs: PlanchetRecord[] = [];
wsr.planchets.forEach((x) => { wsr.planchets.forEach((x) => {
@ -221,7 +222,7 @@ export async function getHistory(
if (historyQuery?.extraDebug) { if (historyQuery?.extraDebug) {
verboseDetails = { verboseDetails = {
coins: cs.map((x) => ({ coins: cs.map((x) => ({
value: Amounts.toString(x.coinValue), value: Amounts.stringify(x.coinValue),
denomPub: x.denomPub, denomPub: x.denomPub,
})), })),
}; };
@ -229,13 +230,13 @@ export async function getHistory(
history.push({ history.push({
type: HistoryEventType.Withdrawn, type: HistoryEventType.Withdrawn,
withdrawSessionId: wsr.withdrawSessionId, withdrawalGroupId: wsr.withdrawalGroupId,
eventId: makeEventId( eventId: makeEventId(
HistoryEventType.Withdrawn, HistoryEventType.Withdrawn,
wsr.withdrawSessionId, wsr.withdrawalGroupId,
), ),
amountWithdrawnEffective: Amounts.toString(wsr.totalCoinValue), amountWithdrawnEffective: Amounts.stringify(wsr.totalCoinValue),
amountWithdrawnRaw: Amounts.toString(wsr.rawWithdrawalAmount), amountWithdrawnRaw: Amounts.stringify(wsr.rawWithdrawalAmount),
exchangeBaseUrl: wsr.exchangeBaseUrl, exchangeBaseUrl: wsr.exchangeBaseUrl,
timestamp: wsr.timestampFinish, timestamp: wsr.timestampFinish,
withdrawalSource: wsr.source, withdrawalSource: wsr.source,
@ -283,7 +284,7 @@ export async function getHistory(
coins.push({ coins.push({
contribution: x.contribution, contribution: x.contribution,
denomPub: c.denomPub, denomPub: c.denomPub,
value: Amounts.toString(d.value), value: Amounts.stringify(d.value),
}); });
} }
verboseDetails = { coins }; verboseDetails = { coins };
@ -301,7 +302,7 @@ export async function getHistory(
sessionId: pe.sessionId, sessionId: pe.sessionId,
timestamp: pe.timestamp, timestamp: pe.timestamp,
numCoins: purchase.payReq.coins.length, numCoins: purchase.payReq.coins.length,
amountPaidWithFees: Amounts.toString(amountPaidWithFees), amountPaidWithFees: Amounts.stringify(amountPaidWithFees),
verboseDetails, verboseDetails,
}); });
}); });
@ -364,7 +365,7 @@ export async function getHistory(
} }
outputCoins.push({ outputCoins.push({
denomPub: d.denomPub, denomPub: d.denomPub,
value: Amounts.toString(d.value), value: Amounts.stringify(d.value),
}); });
} }
} }
@ -378,8 +379,8 @@ export async function getHistory(
eventId: makeEventId(HistoryEventType.Refreshed, rg.refreshGroupId), eventId: makeEventId(HistoryEventType.Refreshed, rg.refreshGroupId),
timestamp: rg.timestampFinished, timestamp: rg.timestampFinished,
refreshReason: rg.reason, refreshReason: rg.reason,
amountRefreshedEffective: Amounts.toString(amountRefreshedEffective), amountRefreshedEffective: Amounts.stringify(amountRefreshedEffective),
amountRefreshedRaw: Amounts.toString(amountRefreshedRaw), amountRefreshedRaw: Amounts.stringify(amountRefreshedRaw),
numInputCoins, numInputCoins,
numOutputCoins, numOutputCoins,
numRefreshedInputCoins, numRefreshedInputCoins,
@ -403,21 +404,22 @@ export async function getHistory(
type: ReserveType.Manual, type: ReserveType.Manual,
}; };
} }
const s = summarizeReserveHistory(reserve.reserveTransactions, reserve.currency);
history.push({ history.push({
type: HistoryEventType.ReserveBalanceUpdated, type: HistoryEventType.ReserveBalanceUpdated,
eventId: makeEventId( eventId: makeEventId(
HistoryEventType.ReserveBalanceUpdated, HistoryEventType.ReserveBalanceUpdated,
ru.reserveUpdateId, ru.reserveUpdateId,
), ),
amountExpected: ru.amountExpected,
amountReserveBalance: ru.amountReserveBalance,
timestamp: ru.timestamp, timestamp: ru.timestamp,
newHistoryTransactions: ru.newHistoryTransactions,
reserveShortInfo: { reserveShortInfo: {
exchangeBaseUrl: reserve.exchangeBaseUrl, exchangeBaseUrl: reserve.exchangeBaseUrl,
reserveCreationDetail, reserveCreationDetail,
reservePub: reserve.reservePub, reservePub: reserve.reservePub,
}, },
reserveAwaitedAmount: Amounts.stringify(s.awaitedReserveAmount),
reserveBalance: Amounts.stringify(s.computedReserveBalance),
reserveUnclaimedAmount: Amounts.stringify(s.unclaimedReserveAmount),
}); });
}); });
@ -428,7 +430,7 @@ export async function getHistory(
eventId: makeEventId(HistoryEventType.TipAccepted, tip.tipId), eventId: makeEventId(HistoryEventType.TipAccepted, tip.tipId),
timestamp: tip.acceptedTimestamp, timestamp: tip.acceptedTimestamp,
tipId: tip.tipId, tipId: tip.tipId,
tipAmountRaw: Amounts.toString(tip.amount), tipAmountRaw: Amounts.stringify(tip.amount),
}); });
} }
}); });
@ -488,9 +490,9 @@ export async function getHistory(
refundGroupId: re.refundGroupId, refundGroupId: re.refundGroupId,
orderShortInfo, orderShortInfo,
timestamp: re.timestamp, timestamp: re.timestamp,
amountRefundedEffective: Amounts.toString(amountRefundedEffective), amountRefundedEffective: Amounts.stringify(amountRefundedEffective),
amountRefundedRaw: Amounts.toString(amountRefundedRaw), amountRefundedRaw: Amounts.stringify(amountRefundedRaw),
amountRefundedInvalid: Amounts.toString(amountRefundedInvalid), amountRefundedInvalid: Amounts.stringify(amountRefundedInvalid),
}); });
}); });
@ -499,7 +501,7 @@ export async function getHistory(
let verboseDetails: any = undefined; let verboseDetails: any = undefined;
if (historyQuery?.extraDebug) { if (historyQuery?.extraDebug) {
verboseDetails = { verboseDetails = {
oldAmountPerCoin: rg.oldAmountPerCoin.map(Amounts.toString), oldAmountPerCoin: rg.oldAmountPerCoin.map(Amounts.stringify),
}; };
} }

View File

@ -243,7 +243,7 @@ async function gatherWithdrawalPending(
resp: PendingOperationsResponse, resp: PendingOperationsResponse,
onlyDue: boolean = false, onlyDue: boolean = false,
): Promise<void> { ): Promise<void> {
await tx.iter(Stores.withdrawalSession).forEach((wsr) => { await tx.iter(Stores.withdrawalGroups).forEach((wsr) => {
if (wsr.timestampFinish) { if (wsr.timestampFinish) {
return; return;
} }
@ -266,7 +266,8 @@ async function gatherWithdrawalPending(
numCoinsTotal, numCoinsTotal,
numCoinsWithdrawn, numCoinsWithdrawn,
source: wsr.source, source: wsr.source,
withdrawSessionId: wsr.withdrawSessionId, withdrawalGroupId: wsr.withdrawalGroupId,
lastError: wsr.lastError,
}); });
}); });
} }
@ -444,7 +445,7 @@ export async function getPendingOperations(
Stores.reserves, Stores.reserves,
Stores.refreshGroups, Stores.refreshGroups,
Stores.coins, Stores.coins,
Stores.withdrawalSession, Stores.withdrawalGroups,
Stores.proposals, Stores.proposals,
Stores.tips, Stores.tips,
Stores.purchases, Stores.purchases,

View File

@ -42,7 +42,7 @@ import { codecForRecoupConfirmation } from "../types/talerTypes";
import { NotificationType } from "../types/notifications"; import { NotificationType } from "../types/notifications";
import { forceQueryReserve } from "./reserves"; import { forceQueryReserve } from "./reserves";
import * as Amounts from "../util/amounts"; import { Amounts } from "../util/amounts";
import { createRefreshGroup, processRefreshGroup } from "./refresh"; import { createRefreshGroup, processRefreshGroup } from "./refresh";
import { RefreshReason, OperationError } from "../types/walletTypes"; import { RefreshReason, OperationError } from "../types/walletTypes";
import { TransactionHandle } from "../util/query"; import { TransactionHandle } from "../util/query";
@ -266,7 +266,7 @@ async function recoupRefreshCoin(
).amount; ).amount;
console.log( console.log(
"recoup: setting old coin amount to", "recoup: setting old coin amount to",
Amounts.toString(oldCoin.currentAmount), Amounts.stringify(oldCoin.currentAmount),
); );
recoupGroup.scheduleRefreshCoins.push(oldCoin.coinPub); recoupGroup.scheduleRefreshCoins.push(oldCoin.coinPub);
await tx.put(Stores.coins, revokedCoin); await tx.put(Stores.coins, revokedCoin);

View File

@ -14,8 +14,7 @@
GNU 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 { AmountJson } from "../util/amounts"; import { Amounts, AmountJson } from "../util/amounts";
import * as Amounts from "../util/amounts";
import { import {
DenominationRecord, DenominationRecord,
Stores, Stores,
@ -239,7 +238,7 @@ async function refreshMelt(
denom_pub_hash: coin.denomPubHash, denom_pub_hash: coin.denomPubHash,
denom_sig: coin.denomSig, denom_sig: coin.denomSig,
rc: refreshSession.hash, rc: refreshSession.hash,
value_with_fee: Amounts.toString(refreshSession.amountRefreshInput), value_with_fee: Amounts.stringify(refreshSession.amountRefreshInput),
}; };
logger.trace(`melt request for coin:`, meltReq); logger.trace(`melt request for coin:`, meltReq);
const resp = await ws.http.postJson(reqUrl.href, meltReq); const resp = await ws.http.postJson(reqUrl.href, meltReq);

View File

@ -41,7 +41,7 @@ import {
import { NotificationType } from "../types/notifications"; import { NotificationType } from "../types/notifications";
import { parseRefundUri } from "../util/taleruri"; import { parseRefundUri } from "../util/taleruri";
import { createRefreshGroup, getTotalRefreshCost } from "./refresh"; import { createRefreshGroup, getTotalRefreshCost } from "./refresh";
import * as Amounts from "../util/amounts"; import { Amounts } from "../util/amounts";
import { import {
MerchantRefundPermission, MerchantRefundPermission,
MerchantRefundResponse, MerchantRefundResponse,
@ -476,7 +476,7 @@ async function processPurchaseApplyRefundImpl(
`commiting refund ${perm.merchant_sig} to coin ${c.coinPub}`, `commiting refund ${perm.merchant_sig} to coin ${c.coinPub}`,
); );
logger.trace( logger.trace(
`coin amount before is ${Amounts.toString(c.currentAmount)}`, `coin amount before is ${Amounts.stringify(c.currentAmount)}`,
); );
logger.trace(`refund amount (via merchant) is ${perm.refund_amount}`); logger.trace(`refund amount (via merchant) is ${perm.refund_amount}`);
logger.trace(`refund fee (via merchant) is ${perm.refund_fee}`); logger.trace(`refund fee (via merchant) is ${perm.refund_fee}`);
@ -486,7 +486,7 @@ async function processPurchaseApplyRefundImpl(
c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount; c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount; c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
logger.trace( logger.trace(
`coin amount after is ${Amounts.toString(c.currentAmount)}`, `coin amount after is ${Amounts.stringify(c.currentAmount)}`,
); );
await tx.put(Stores.coins, c); await tx.put(Stores.coins, c);
}; };

View File

@ -28,14 +28,17 @@ import {
ReserveRecord, ReserveRecord,
CurrencyRecord, CurrencyRecord,
Stores, Stores,
WithdrawalSessionRecord, WithdrawalGroupRecord,
initRetryInfo, initRetryInfo,
updateRetryInfoTimeout, updateRetryInfoTimeout,
ReserveUpdatedEventRecord, ReserveUpdatedEventRecord,
WalletReserveHistoryItemType,
DenominationRecord,
PlanchetRecord,
WithdrawalSourceType,
} from "../types/dbTypes"; } from "../types/dbTypes";
import { TransactionAbort } from "../util/query";
import { Logger } from "../util/logging"; import { Logger } from "../util/logging";
import * as Amounts from "../util/amounts"; import { Amounts } from "../util/amounts";
import { import {
updateExchangeFromUrl, updateExchangeFromUrl,
getExchangeTrust, getExchangeTrust,
@ -50,7 +53,7 @@ import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
import { randomBytes } from "../crypto/primitives/nacl-fast"; import { randomBytes } from "../crypto/primitives/nacl-fast";
import { import {
getVerifiedWithdrawDenomList, getVerifiedWithdrawDenomList,
processWithdrawSession, processWithdrawGroup,
getBankWithdrawalInfo, getBankWithdrawalInfo,
} from "./withdraw"; } from "./withdraw";
import { import {
@ -61,6 +64,10 @@ import {
import { NotificationType } from "../types/notifications"; import { NotificationType } from "../types/notifications";
import { codecForReserveStatus } from "../types/ReserveStatus"; import { codecForReserveStatus } from "../types/ReserveStatus";
import { getTimestampNow } from "../util/time"; import { getTimestampNow } from "../util/time";
import {
reconcileReserveHistory,
summarizeReserveHistory,
} from "../util/reserveHistoryUtil";
const logger = new Logger("reserves.ts"); const logger = new Logger("reserves.ts");
@ -98,11 +105,7 @@ export async function createReserve(
const reserveRecord: ReserveRecord = { const reserveRecord: ReserveRecord = {
timestampCreated: now, timestampCreated: now,
amountWithdrawAllocated: Amounts.getZero(currency),
amountWithdrawCompleted: Amounts.getZero(currency),
amountWithdrawRemaining: Amounts.getZero(currency),
exchangeBaseUrl: canonExchange, exchangeBaseUrl: canonExchange,
amountInitiallyRequested: req.amount,
reservePriv: keypair.priv, reservePriv: keypair.priv,
reservePub: keypair.pub, reservePub: keypair.pub,
senderWire: req.senderWire, senderWire: req.senderWire,
@ -115,8 +118,14 @@ export async function createReserve(
retryInfo: initRetryInfo(), retryInfo: initRetryInfo(),
lastError: undefined, lastError: undefined,
reserveTransactions: [], reserveTransactions: [],
currency: req.amount.currency,
}; };
reserveRecord.reserveTransactions.push({
type: WalletReserveHistoryItemType.Credit,
expectedAmount: req.amount,
});
const senderWire = req.senderWire; const senderWire = req.senderWire;
if (senderWire) { if (senderWire) {
const rec = { const rec = {
@ -460,6 +469,7 @@ async function updateReserve(
const respJson = await resp.json(); const respJson = await resp.json();
const reserveInfo = codecForReserveStatus().decode(respJson); const reserveInfo = codecForReserveStatus().decode(respJson);
const balance = Amounts.parseOrThrow(reserveInfo.balance); const balance = Amounts.parseOrThrow(reserveInfo.balance);
const currency = balance.currency;
await ws.db.runWithWriteTransaction( await ws.db.runWithWriteTransaction(
[Stores.reserves, Stores.reserveUpdatedEvents], [Stores.reserves, Stores.reserveUpdatedEvents],
async (tx) => { async (tx) => {
@ -477,60 +487,41 @@ async function updateReserve(
const reserveUpdateId = encodeCrock(getRandomBytes(32)); const reserveUpdateId = encodeCrock(getRandomBytes(32));
// FIXME: check / compare history! const reconciled = reconcileReserveHistory(
if (!r.lastSuccessfulStatusQuery) { r.reserveTransactions,
// FIXME: check if this matches initial expectations reserveInfo.history,
r.amountWithdrawRemaining = balance; );
console.log("reconciled history:", JSON.stringify(reconciled, undefined, 2));
const summary = summarizeReserveHistory(
reconciled.updatedLocalHistory,
currency,
);
console.log("summary", summary);
if (
reconciled.newAddedItems.length + reconciled.newMatchedItems.length !=
0
) {
const reserveUpdate: ReserveUpdatedEventRecord = { const reserveUpdate: ReserveUpdatedEventRecord = {
reservePub: r.reservePub, reservePub: r.reservePub,
timestamp: getTimestampNow(), timestamp: getTimestampNow(),
amountReserveBalance: Amounts.toString(balance), amountReserveBalance: Amounts.stringify(balance),
amountExpected: Amounts.toString(reserve.amountInitiallyRequested), amountExpected: Amounts.stringify(summary.awaitedReserveAmount),
newHistoryTransactions, newHistoryTransactions,
reserveUpdateId, reserveUpdateId,
}; };
await tx.put(Stores.reserveUpdatedEvents, reserveUpdate); await tx.put(Stores.reserveUpdatedEvents, reserveUpdate);
r.reserveStatus = ReserveRecordStatus.WITHDRAWING; r.reserveStatus = ReserveRecordStatus.WITHDRAWING;
r.retryInfo = initRetryInfo();
} else { } else {
const expectedBalance = Amounts.add( r.reserveStatus = ReserveRecordStatus.DORMANT;
r.amountWithdrawRemaining, r.retryInfo = initRetryInfo(false);
Amounts.sub(r.amountWithdrawAllocated, r.amountWithdrawCompleted)
.amount,
);
const cmp = Amounts.cmp(balance, expectedBalance.amount);
if (cmp == 0) {
// Nothing changed, go back to sleep!
r.reserveStatus = ReserveRecordStatus.DORMANT;
} else if (cmp > 0) {
const extra = Amounts.sub(balance, expectedBalance.amount).amount;
r.amountWithdrawRemaining = Amounts.add(
r.amountWithdrawRemaining,
extra,
).amount;
r.reserveStatus = ReserveRecordStatus.WITHDRAWING;
} else {
// We're missing some money.
r.reserveStatus = ReserveRecordStatus.DORMANT;
}
if (r.reserveStatus !== ReserveRecordStatus.DORMANT) {
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.lastSuccessfulStatusQuery = getTimestampNow();
if (r.reserveStatus == ReserveRecordStatus.DORMANT) { r.reserveTransactions = reconciled.updatedLocalHistory;
r.retryInfo = initRetryInfo(false); r.lastError = undefined;
} else {
r.retryInfo = initRetryInfo();
}
r.reserveTransactions = reserveInfo.history;
await tx.put(Stores.reserves, r); await tx.put(Stores.reserves, r);
}, },
); );
@ -607,6 +598,33 @@ export async function confirmReserve(
}); });
} }
async function makePlanchet(
ws: InternalWalletState,
reserve: ReserveRecord,
denom: DenominationRecord,
): Promise<PlanchetRecord> {
const r = await ws.cryptoApi.createPlanchet({
denomPub: denom.denomPub,
feeWithdraw: denom.feeWithdraw,
reservePriv: reserve.reservePriv,
reservePub: reserve.reservePub,
value: denom.value,
});
return {
blindingKey: r.blindingKey,
coinEv: r.coinEv,
coinPriv: r.coinPriv,
coinPub: r.coinPub,
coinValue: r.coinValue,
denomPub: r.denomPub,
denomPubHash: r.denomPubHash,
isFromTip: false,
reservePub: r.reservePub,
withdrawSig: r.withdrawSig,
coinEvHash: r.coinEvHash,
};
}
/** /**
* Withdraw coins from a reserve until it is empty. * Withdraw coins from a reserve until it is empty.
* *
@ -626,7 +644,12 @@ async function depleteReserve(
} }
logger.trace(`depleting reserve ${reservePub}`); logger.trace(`depleting reserve ${reservePub}`);
const withdrawAmount = reserve.amountWithdrawRemaining; const summary = summarizeReserveHistory(
reserve.reserveTransactions,
reserve.currency,
);
const withdrawAmount = summary.unclaimedReserveAmount;
logger.trace(`getting denom list`); logger.trace(`getting denom list`);
@ -637,36 +660,47 @@ async function depleteReserve(
); );
logger.trace(`got denom list`); logger.trace(`got denom list`);
if (denomsForWithdraw.length === 0) { if (denomsForWithdraw.length === 0) {
const m = `Unable to withdraw from reserve, no denominations are available to withdraw.`; // Only complain about inability to withdraw if we
const opErr = { // didn't withdraw before.
type: "internal", if (Amounts.isZero(summary.withdrawnAmount)) {
message: m, const m = `Unable to withdraw from reserve, no denominations are available to withdraw.`;
details: {}, const opErr = {
}; type: "internal",
await incrementReserveRetry(ws, reserve.reservePub, opErr); message: m,
console.log(m); details: {},
throw new OperationFailedAndReportedError(opErr); };
await incrementReserveRetry(ws, reserve.reservePub, opErr);
console.log(m);
throw new OperationFailedAndReportedError(opErr);
}
return;
} }
logger.trace("selected denominations"); logger.trace("selected denominations");
const withdrawalSessionId = encodeCrock(randomBytes(32)); const withdrawalGroupId = encodeCrock(randomBytes(32));
const totalCoinValue = Amounts.sum(denomsForWithdraw.map((x) => x.value)) const totalCoinValue = Amounts.sum(denomsForWithdraw.map((x) => x.value))
.amount; .amount;
const withdrawalRecord: WithdrawalSessionRecord = { const planchets: PlanchetRecord[] = [];
withdrawSessionId: withdrawalSessionId, for (const d of denomsForWithdraw) {
const p = await makePlanchet(ws, reserve, d);
planchets.push(p);
}
const withdrawalRecord: WithdrawalGroupRecord = {
withdrawalGroupId: withdrawalGroupId,
exchangeBaseUrl: reserve.exchangeBaseUrl, exchangeBaseUrl: reserve.exchangeBaseUrl,
source: { source: {
type: "reserve", type: WithdrawalSourceType.Reserve,
reservePub: reserve.reservePub, reservePub: reserve.reservePub,
}, },
rawWithdrawalAmount: withdrawAmount, rawWithdrawalAmount: withdrawAmount,
timestampStart: getTimestampNow(), timestampStart: getTimestampNow(),
denoms: denomsForWithdraw.map((x) => x.denomPub), denoms: denomsForWithdraw.map((x) => x.denomPub),
withdrawn: denomsForWithdraw.map((x) => false), withdrawn: denomsForWithdraw.map((x) => false),
planchets: denomsForWithdraw.map((x) => undefined), planchets,
totalCoinValue, totalCoinValue,
retryInfo: initRetryInfo(), retryInfo: initRetryInfo(),
lastErrorPerCoin: {}, lastErrorPerCoin: {},
@ -679,53 +713,54 @@ async function depleteReserve(
const totalWithdrawAmount = Amounts.add(totalCoinValue, totalCoinWithdrawFee) const totalWithdrawAmount = Amounts.add(totalCoinValue, totalCoinWithdrawFee)
.amount; .amount;
function mutateReserve(r: ReserveRecord): ReserveRecord {
const remaining = Amounts.sub(
r.amountWithdrawRemaining,
totalWithdrawAmount,
);
if (remaining.saturated) {
console.error("can't create planchets, saturated");
throw TransactionAbort;
}
const allocated = Amounts.add(
r.amountWithdrawAllocated,
totalWithdrawAmount,
);
if (allocated.saturated) {
console.error("can't create planchets, saturated");
throw TransactionAbort;
}
r.amountWithdrawRemaining = remaining.amount;
r.amountWithdrawAllocated = allocated.amount;
r.reserveStatus = ReserveRecordStatus.DORMANT;
r.retryInfo = initRetryInfo(false);
return r;
}
const success = await ws.db.runWithWriteTransaction( const success = await ws.db.runWithWriteTransaction(
[Stores.withdrawalSession, Stores.reserves], [Stores.withdrawalGroups, Stores.reserves],
async (tx) => { async (tx) => {
const myReserve = await tx.get(Stores.reserves, reservePub); const newReserve = await tx.get(Stores.reserves, reservePub);
if (!myReserve) { if (!newReserve) {
return false; return false;
} }
if (myReserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) { if (newReserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
return false; return false;
} }
await tx.mutate(Stores.reserves, reserve.reservePub, mutateReserve); const newSummary = summarizeReserveHistory(
await tx.put(Stores.withdrawalSession, withdrawalRecord); newReserve.reserveTransactions,
newReserve.currency,
);
if (
Amounts.cmp(newSummary.unclaimedReserveAmount, totalWithdrawAmount) < 0
) {
// Something must have happened concurrently!
logger.error(
"aborting withdrawal session, likely concurrent withdrawal happened",
);
return false;
}
for (let i = 0; i < planchets.length; i++) {
const amt = Amounts.add(
denomsForWithdraw[i].value,
denomsForWithdraw[i].feeWithdraw,
).amount;
newReserve.reserveTransactions.push({
type: WalletReserveHistoryItemType.Withdraw,
expectedAmount: amt,
});
}
newReserve.reserveStatus = ReserveRecordStatus.DORMANT;
newReserve.retryInfo = initRetryInfo(false);
await tx.put(Stores.reserves, newReserve);
await tx.put(Stores.withdrawalGroups, withdrawalRecord);
return true; return true;
}, },
); );
if (success) { if (success) {
console.log("processing new withdraw session"); console.log("processing new withdraw group");
ws.notify({ ws.notify({
type: NotificationType.WithdrawSessionCreated, type: NotificationType.WithdrawGroupCreated,
withdrawSessionId: withdrawalSessionId, withdrawalGroupId: withdrawalGroupId,
}); });
await processWithdrawSession(ws, withdrawalSessionId); await processWithdrawGroup(ws, withdrawalGroupId);
} else { } else {
console.trace("withdraw session already existed"); console.trace("withdraw session already existed");
} }

View File

@ -28,14 +28,15 @@ import * as Amounts from "../util/amounts";
import { import {
Stores, Stores,
PlanchetRecord, PlanchetRecord,
WithdrawalSessionRecord, WithdrawalGroupRecord,
initRetryInfo, initRetryInfo,
updateRetryInfoTimeout, updateRetryInfoTimeout,
WithdrawalSourceType,
} from "../types/dbTypes"; } from "../types/dbTypes";
import { import {
getExchangeWithdrawalInfo, getExchangeWithdrawalInfo,
getVerifiedWithdrawDenomList, getVerifiedWithdrawDenomList,
processWithdrawSession, processWithdrawGroup,
} from "./withdraw"; } from "./withdraw";
import { updateExchangeFromUrl } from "./exchanges"; import { updateExchangeFromUrl } from "./exchanges";
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto"; import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
@ -246,8 +247,10 @@ async function processTipImpl(
const planchets: PlanchetRecord[] = []; const planchets: PlanchetRecord[] = [];
for (let i = 0; i < tipRecord.planchets.length; i++) { for (let i = 0; i < tipRecord.planchets.length; i++) {
const tipPlanchet = tipRecord.planchets[i]; const tipPlanchet = tipRecord.planchets[i];
const coinEvHash = await ws.cryptoApi.hashEncoded(tipPlanchet.coinEv);
const planchet: PlanchetRecord = { const planchet: PlanchetRecord = {
blindingKey: tipPlanchet.blindingKey, blindingKey: tipPlanchet.blindingKey,
coinEv: tipPlanchet.coinEv, coinEv: tipPlanchet.coinEv,
@ -259,22 +262,23 @@ async function processTipImpl(
reservePub: response.reserve_pub, reservePub: response.reserve_pub,
withdrawSig: response.reserve_sigs[i].reserve_sig, withdrawSig: response.reserve_sigs[i].reserve_sig,
isFromTip: true, isFromTip: true,
coinEvHash,
}; };
planchets.push(planchet); planchets.push(planchet);
} }
const withdrawalSessionId = encodeCrock(getRandomBytes(32)); const withdrawalGroupId = encodeCrock(getRandomBytes(32));
const withdrawalSession: WithdrawalSessionRecord = { const withdrawalGroup: WithdrawalGroupRecord = {
denoms: planchets.map((x) => x.denomPub), denoms: planchets.map((x) => x.denomPub),
exchangeBaseUrl: tipRecord.exchangeUrl, exchangeBaseUrl: tipRecord.exchangeUrl,
planchets: planchets, planchets: planchets,
source: { source: {
type: "tip", type: WithdrawalSourceType.Tip,
tipId: tipRecord.tipId, tipId: tipRecord.tipId,
}, },
timestampStart: getTimestampNow(), timestampStart: getTimestampNow(),
withdrawSessionId: withdrawalSessionId, withdrawalGroupId: withdrawalGroupId,
rawWithdrawalAmount: tipRecord.amount, rawWithdrawalAmount: tipRecord.amount,
withdrawn: planchets.map((x) => false), withdrawn: planchets.map((x) => false),
totalCoinValue: Amounts.sum(planchets.map((p) => p.coinValue)).amount, totalCoinValue: Amounts.sum(planchets.map((p) => p.coinValue)).amount,
@ -285,7 +289,7 @@ async function processTipImpl(
}; };
await ws.db.runWithWriteTransaction( await ws.db.runWithWriteTransaction(
[Stores.tips, Stores.withdrawalSession], [Stores.tips, Stores.withdrawalGroups],
async (tx) => { async (tx) => {
const tr = await tx.get(Stores.tips, tipId); const tr = await tx.get(Stores.tips, tipId);
if (!tr) { if (!tr) {
@ -298,11 +302,11 @@ async function processTipImpl(
tr.retryInfo = initRetryInfo(false); tr.retryInfo = initRetryInfo(false);
await tx.put(Stores.tips, tr); await tx.put(Stores.tips, tr);
await tx.put(Stores.withdrawalSession, withdrawalSession); await tx.put(Stores.withdrawalGroups, withdrawalGroup);
}, },
); );
await processWithdrawSession(ws, withdrawalSessionId); await processWithdrawGroup(ws, withdrawalGroupId);
return; return;
} }

View File

@ -52,6 +52,7 @@ import {
timestampCmp, timestampCmp,
timestampSubtractDuraction, timestampSubtractDuraction,
} from "../util/time"; } from "../util/time";
import { summarizeReserveHistory, ReserveHistorySummary } from "../util/reserveHistoryUtil";
const logger = new Logger("withdraw.ts"); const logger = new Logger("withdraw.ts");
@ -158,29 +159,29 @@ async function getPossibleDenoms(
*/ */
async function processPlanchet( async function processPlanchet(
ws: InternalWalletState, ws: InternalWalletState,
withdrawalSessionId: string, withdrawalGroupId: string,
coinIdx: number, coinIdx: number,
): Promise<void> { ): Promise<void> {
const withdrawalSession = await ws.db.get( const withdrawalGroup = await ws.db.get(
Stores.withdrawalSession, Stores.withdrawalGroups,
withdrawalSessionId, withdrawalGroupId,
); );
if (!withdrawalSession) { if (!withdrawalGroup) {
return; return;
} }
if (withdrawalSession.withdrawn[coinIdx]) { if (withdrawalGroup.withdrawn[coinIdx]) {
return; return;
} }
if (withdrawalSession.source.type === "reserve") { if (withdrawalGroup.source.type === "reserve") {
} }
const planchet = withdrawalSession.planchets[coinIdx]; const planchet = withdrawalGroup.planchets[coinIdx];
if (!planchet) { if (!planchet) {
console.log("processPlanchet: planchet not found"); console.log("processPlanchet: planchet not found");
return; return;
} }
const exchange = await ws.db.get( const exchange = await ws.db.get(
Stores.exchanges, Stores.exchanges,
withdrawalSession.exchangeBaseUrl, withdrawalGroup.exchangeBaseUrl,
); );
if (!exchange) { if (!exchange) {
console.error("db inconsistent: exchange for planchet not found"); console.error("db inconsistent: exchange for planchet not found");
@ -188,7 +189,7 @@ async function processPlanchet(
} }
const denom = await ws.db.get(Stores.denominations, [ const denom = await ws.db.get(Stores.denominations, [
withdrawalSession.exchangeBaseUrl, withdrawalGroup.exchangeBaseUrl,
planchet.denomPub, planchet.denomPub,
]); ]);
@ -232,24 +233,24 @@ async function processPlanchet(
denomPub: planchet.denomPub, denomPub: planchet.denomPub,
denomPubHash: planchet.denomPubHash, denomPubHash: planchet.denomPubHash,
denomSig, denomSig,
exchangeBaseUrl: withdrawalSession.exchangeBaseUrl, exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl,
status: CoinStatus.Fresh, status: CoinStatus.Fresh,
coinSource: { coinSource: {
type: CoinSourceType.Withdraw, type: CoinSourceType.Withdraw,
coinIndex: coinIdx, coinIndex: coinIdx,
reservePub: planchet.reservePub, reservePub: planchet.reservePub,
withdrawSessionId: withdrawalSessionId, withdrawalGroupId: withdrawalGroupId,
}, },
suspended: false, suspended: false,
}; };
let withdrawSessionFinished = false; let withdrawalGroupFinished = false;
let reserveDepleted = false; let summary: ReserveHistorySummary | undefined = undefined;
const success = await ws.db.runWithWriteTransaction( const success = await ws.db.runWithWriteTransaction(
[Stores.coins, Stores.withdrawalSession, Stores.reserves], [Stores.coins, Stores.withdrawalGroups, Stores.reserves],
async (tx) => { async (tx) => {
const ws = await tx.get(Stores.withdrawalSession, withdrawalSessionId); const ws = await tx.get(Stores.withdrawalGroups, withdrawalGroupId);
if (!ws) { if (!ws) {
return false; return false;
} }
@ -269,23 +270,13 @@ async function processPlanchet(
ws.timestampFinish = getTimestampNow(); ws.timestampFinish = getTimestampNow();
ws.lastError = undefined; ws.lastError = undefined;
ws.retryInfo = initRetryInfo(false); ws.retryInfo = initRetryInfo(false);
withdrawSessionFinished = true; withdrawalGroupFinished = true;
} }
await tx.put(Stores.withdrawalSession, ws); await tx.put(Stores.withdrawalGroups, ws);
if (!planchet.isFromTip) { if (!planchet.isFromTip) {
const r = await tx.get(Stores.reserves, planchet.reservePub); const r = await tx.get(Stores.reserves, planchet.reservePub);
if (r) { if (r) {
r.amountWithdrawCompleted = Amounts.add( summary = summarizeReserveHistory(r.reserveTransactions, r.currency);
r.amountWithdrawCompleted,
Amounts.add(denom.value, denom.feeWithdraw).amount,
).amount;
if (
Amounts.cmp(r.amountWithdrawCompleted, r.amountWithdrawAllocated) ==
0
) {
reserveDepleted = true;
}
await tx.put(Stores.reserves, r);
} }
} }
await tx.add(Stores.coins, coin); await tx.add(Stores.coins, coin);
@ -299,17 +290,10 @@ async function processPlanchet(
}); });
} }
if (withdrawSessionFinished) { if (withdrawalGroupFinished) {
ws.notify({ ws.notify({
type: NotificationType.WithdrawSessionFinished, type: NotificationType.WithdrawGroupFinished,
withdrawSessionId: withdrawalSessionId, withdrawalSource: withdrawalGroup.source,
});
}
if (reserveDepleted && withdrawalSession.source.type === "reserve") {
ws.notify({
type: NotificationType.ReserveDepleted,
reservePub: withdrawalSession.source.reservePub,
}); });
} }
} }
@ -383,113 +367,15 @@ export async function getVerifiedWithdrawDenomList(
return selectedDenoms; return selectedDenoms;
} }
async function makePlanchet(
ws: InternalWalletState,
withdrawalSessionId: string,
coinIndex: number,
): Promise<void> {
const withdrawalSession = await ws.db.get(
Stores.withdrawalSession,
withdrawalSessionId,
);
if (!withdrawalSession) {
return;
}
const src = withdrawalSession.source;
if (src.type !== "reserve") {
throw Error("invalid state");
}
const reserve = await ws.db.get(Stores.reserves, src.reservePub);
if (!reserve) {
return;
}
const denom = await ws.db.get(Stores.denominations, [
withdrawalSession.exchangeBaseUrl,
withdrawalSession.denoms[coinIndex],
]);
if (!denom) {
return;
}
const r = await ws.cryptoApi.createPlanchet({
denomPub: denom.denomPub,
feeWithdraw: denom.feeWithdraw,
reservePriv: reserve.reservePriv,
reservePub: reserve.reservePub,
value: denom.value,
});
const newPlanchet: PlanchetRecord = {
blindingKey: r.blindingKey,
coinEv: r.coinEv,
coinPriv: r.coinPriv,
coinPub: r.coinPub,
coinValue: r.coinValue,
denomPub: r.denomPub,
denomPubHash: r.denomPubHash,
isFromTip: false,
reservePub: r.reservePub,
withdrawSig: r.withdrawSig,
};
await ws.db.runWithWriteTransaction(
[Stores.withdrawalSession],
async (tx) => {
const myWs = await tx.get(Stores.withdrawalSession, withdrawalSessionId);
if (!myWs) {
return;
}
if (myWs.planchets[coinIndex]) {
return;
}
myWs.planchets[coinIndex] = newPlanchet;
await tx.put(Stores.withdrawalSession, myWs);
},
);
}
async function processWithdrawCoin(
ws: InternalWalletState,
withdrawalSessionId: string,
coinIndex: number,
) {
logger.trace("starting withdraw for coin", coinIndex);
const withdrawalSession = await ws.db.get(
Stores.withdrawalSession,
withdrawalSessionId,
);
if (!withdrawalSession) {
console.log("ws doesn't exist");
return;
}
const planchet = withdrawalSession.planchets[coinIndex];
if (planchet) {
const coin = await ws.db.get(Stores.coins, planchet.coinPub);
if (coin) {
console.log("coin already exists");
return;
}
}
if (!withdrawalSession.planchets[coinIndex]) {
const key = `${withdrawalSessionId}-${coinIndex}`;
await ws.memoMakePlanchet.memo(key, async () => {
logger.trace("creating planchet for coin", coinIndex);
return makePlanchet(ws, withdrawalSessionId, coinIndex);
});
}
await processPlanchet(ws, withdrawalSessionId, coinIndex);
}
async function incrementWithdrawalRetry( async function incrementWithdrawalRetry(
ws: InternalWalletState, ws: InternalWalletState,
withdrawalSessionId: string, withdrawalGroupId: string,
err: OperationError | undefined, err: OperationError | undefined,
): Promise<void> { ): Promise<void> {
await ws.db.runWithWriteTransaction( await ws.db.runWithWriteTransaction(
[Stores.withdrawalSession], [Stores.withdrawalGroups],
async (tx) => { async (tx) => {
const wsr = await tx.get(Stores.withdrawalSession, withdrawalSessionId); const wsr = await tx.get(Stores.withdrawalGroups, withdrawalGroupId);
if (!wsr) { if (!wsr) {
return; return;
} }
@ -499,30 +385,30 @@ async function incrementWithdrawalRetry(
wsr.retryInfo.retryCounter++; wsr.retryInfo.retryCounter++;
updateRetryInfoTimeout(wsr.retryInfo); updateRetryInfoTimeout(wsr.retryInfo);
wsr.lastError = err; wsr.lastError = err;
await tx.put(Stores.withdrawalSession, wsr); await tx.put(Stores.withdrawalGroups, wsr);
}, },
); );
ws.notify({ type: NotificationType.WithdrawOperationError }); ws.notify({ type: NotificationType.WithdrawOperationError });
} }
export async function processWithdrawSession( export async function processWithdrawGroup(
ws: InternalWalletState, ws: InternalWalletState,
withdrawalSessionId: string, withdrawalGroupId: string,
forceNow: boolean = false, forceNow: boolean = false,
): Promise<void> { ): Promise<void> {
const onOpErr = (e: OperationError) => const onOpErr = (e: OperationError) =>
incrementWithdrawalRetry(ws, withdrawalSessionId, e); incrementWithdrawalRetry(ws, withdrawalGroupId, e);
await guardOperationException( await guardOperationException(
() => processWithdrawSessionImpl(ws, withdrawalSessionId, forceNow), () => processWithdrawGroupImpl(ws, withdrawalGroupId, forceNow),
onOpErr, onOpErr,
); );
} }
async function resetWithdrawSessionRetry( async function resetWithdrawalGroupRetry(
ws: InternalWalletState, ws: InternalWalletState,
withdrawalSessionId: string, withdrawalGroupId: string,
) { ) {
await ws.db.mutate(Stores.withdrawalSession, withdrawalSessionId, (x) => { await ws.db.mutate(Stores.withdrawalGroups, withdrawalGroupId, (x) => {
if (x.retryInfo.active) { if (x.retryInfo.active) {
x.retryInfo = initRetryInfo(); x.retryInfo = initRetryInfo();
} }
@ -530,26 +416,26 @@ async function resetWithdrawSessionRetry(
}); });
} }
async function processWithdrawSessionImpl( async function processWithdrawGroupImpl(
ws: InternalWalletState, ws: InternalWalletState,
withdrawalSessionId: string, withdrawalGroupId: string,
forceNow: boolean, forceNow: boolean,
): Promise<void> { ): Promise<void> {
logger.trace("processing withdraw session", withdrawalSessionId); logger.trace("processing withdraw group", withdrawalGroupId);
if (forceNow) { if (forceNow) {
await resetWithdrawSessionRetry(ws, withdrawalSessionId); await resetWithdrawalGroupRetry(ws, withdrawalGroupId);
} }
const withdrawalSession = await ws.db.get( const withdrawalGroup = await ws.db.get(
Stores.withdrawalSession, Stores.withdrawalGroups,
withdrawalSessionId, withdrawalGroupId,
); );
if (!withdrawalSession) { if (!withdrawalGroup) {
logger.trace("withdraw session doesn't exist"); logger.trace("withdraw session doesn't exist");
return; return;
} }
const ps = withdrawalSession.denoms.map((d, i) => const ps = withdrawalGroup.denoms.map((d, i) =>
processWithdrawCoin(ws, withdrawalSessionId, i), processPlanchet(ws, withdrawalGroupId, i),
); );
await Promise.all(ps); await Promise.all(ps);
return; return;

View File

@ -151,7 +151,7 @@ export interface WalletReserveHistoryCreditItem {
/** /**
* Amount we expect to see credited. * Amount we expect to see credited.
*/ */
expectedAmount?: string; expectedAmount?: AmountJson;
/** /**
* Item from the reserve transaction history that this * Item from the reserve transaction history that this
@ -161,7 +161,15 @@ export interface WalletReserveHistoryCreditItem {
} }
export interface WalletReserveHistoryWithdrawItem { export interface WalletReserveHistoryWithdrawItem {
expectedAmount?: string; expectedAmount?: AmountJson;
/**
* Hash of the blinded coin.
*
* When this value is set, it indicates that a withdrawal is active
* in the wallet for the
*/
expectedCoinEvHash?: string;
type: WalletReserveHistoryItemType.Withdraw; type: WalletReserveHistoryItemType.Withdraw;
@ -188,7 +196,7 @@ export interface WalletReserveHistoryRecoupItem {
/** /**
* Amount we expect to see recouped. * Amount we expect to see recouped.
*/ */
expectedAmount?: string; expectedAmount?: AmountJson;
/** /**
* Item from the reserve transaction history that this * Item from the reserve transaction history that this
@ -222,6 +230,11 @@ export interface ReserveRecord {
*/ */
exchangeBaseUrl: string; exchangeBaseUrl: string;
/**
* Currency of the reserve.
*/
currency: string;
/** /**
* Time when the reserve was created. * Time when the reserve was created.
*/ */
@ -237,34 +250,13 @@ export interface ReserveRecord {
timestampReserveInfoPosted: Timestamp | undefined; timestampReserveInfoPosted: Timestamp | undefined;
/** /**
* Time when the reserve was confirmed. * Time when the reserve was confirmed, either manually by the user
* or by the bank.
* *
* Set to 0 if not confirmed yet. * Set to undefined if not confirmed yet.
*/ */
timestampConfirmed: Timestamp | undefined; timestampConfirmed: Timestamp | undefined;
/**
* Amount that's still available for withdrawing
* from this reserve.
*/
amountWithdrawRemaining: AmountJson;
/**
* Amount allocated for withdrawing.
* The corresponding withdraw operation may or may not
* have been completed yet.
*/
amountWithdrawAllocated: AmountJson;
amountWithdrawCompleted: AmountJson;
/**
* Amount requested when the reserve was created.
* When a reserve is re-used (rare!) the current_amount can
* be higher than the requested_amount
*/
amountInitiallyRequested: AmountJson;
/** /**
* Wire information (as payto URI) for the bank account that * Wire information (as payto URI) for the bank account that
* transfered funds for this reserve. * transfered funds for this reserve.
@ -305,7 +297,7 @@ export interface ReserveRecord {
*/ */
lastError: OperationError | undefined; lastError: OperationError | undefined;
reserveTransactions: ReserveTransaction[]; reserveTransactions: WalletReserveHistoryItem[];
} }
/** /**
@ -627,6 +619,7 @@ export interface PlanchetRecord {
blindingKey: string; blindingKey: string;
withdrawSig: string; withdrawSig: string;
coinEv: string; coinEv: string;
coinEvHash: string;
coinValue: AmountJson; coinValue: AmountJson;
isFromTip: boolean; isFromTip: boolean;
} }
@ -675,7 +668,7 @@ export const enum CoinSourceType {
export interface WithdrawCoinSource { export interface WithdrawCoinSource {
type: CoinSourceType.Withdraw; type: CoinSourceType.Withdraw;
withdrawSessionId: string; withdrawalGroupId: string;
/** /**
* Index of the coin in the withdrawal session. * Index of the coin in the withdrawal session.
@ -1362,20 +1355,25 @@ export interface CoinsReturnRecord {
wire: any; wire: any;
} }
export const enum WithdrawalSourceType {
Tip = "tip",
Reserve = "reserve",
}
export interface WithdrawalSourceTip { export interface WithdrawalSourceTip {
type: "tip"; type: WithdrawalSourceType.Tip;
tipId: string; tipId: string;
} }
export interface WithdrawalSourceReserve { export interface WithdrawalSourceReserve {
type: "reserve"; type: WithdrawalSourceType.Reserve;
reservePub: string; reservePub: string;
} }
export type WithdrawalSource = WithdrawalSourceTip | WithdrawalSourceReserve; export type WithdrawalSource = WithdrawalSourceTip | WithdrawalSourceReserve;
export interface WithdrawalSessionRecord { export interface WithdrawalGroupRecord {
withdrawSessionId: string; withdrawalGroupId: string;
/** /**
* Withdrawal source. Fields that don't apply to the respective * Withdrawal source. Fields that don't apply to the respective
@ -1636,9 +1634,9 @@ export namespace Stores {
} }
} }
class WithdrawalSessionsStore extends Store<WithdrawalSessionRecord> { class WithdrawalGroupsStore extends Store<WithdrawalGroupRecord> {
constructor() { constructor() {
super("withdrawals", { keyPath: "withdrawSessionId" }); super("withdrawals", { keyPath: "withdrawalGroupId" });
} }
} }
@ -1697,7 +1695,7 @@ export namespace Stores {
export const purchases = new PurchasesStore(); export const purchases = new PurchasesStore();
export const tips = new TipsStore(); export const tips = new TipsStore();
export const senderWires = new SenderWiresStore(); export const senderWires = new SenderWiresStore();
export const withdrawalSession = new WithdrawalSessionsStore(); export const withdrawalGroups = new WithdrawalGroupsStore();
export const bankWithdrawUris = new BankWithdrawUrisStore(); export const bankWithdrawUris = new BankWithdrawUrisStore();
export const refundEvents = new RefundEventsStore(); export const refundEvents = new RefundEventsStore();
export const payEvents = new PayEventsStore(); export const payEvents = new PayEventsStore();

View File

@ -119,8 +119,6 @@ export interface HistoryReserveBalanceUpdatedEvent {
*/ */
timestamp: Timestamp; timestamp: Timestamp;
newHistoryTransactions: ReserveTransaction[];
/** /**
* Condensed information about the reserve. * Condensed information about the reserve.
*/ */
@ -129,13 +127,17 @@ export interface HistoryReserveBalanceUpdatedEvent {
/** /**
* Amount currently left in the reserve. * Amount currently left in the reserve.
*/ */
amountReserveBalance: string; reserveBalance: string;
/** /**
* Amount we expected to be in the reserve at that time, * Amount we still expect to be added to the reserve.
* considering ongoing withdrawals from that reserve.
*/ */
amountExpected: string; reserveAwaitedAmount: string;
/**
* Amount that hasn't been withdrawn yet.
*/
reserveUnclaimedAmount: string;
} }
/** /**
@ -612,7 +614,7 @@ export interface HistoryWithdrawnEvent {
* Unique identifier for the withdrawal session, can be used to * Unique identifier for the withdrawal session, can be used to
* query more detailed information from the wallet. * query more detailed information from the wallet.
*/ */
withdrawSessionId: string; withdrawalGroupId: string;
withdrawalSource: WithdrawalSource; withdrawalSource: WithdrawalSource;

View File

@ -1,4 +1,5 @@
import { OperationError } from "./walletTypes"; import { OperationError } from "./walletTypes";
import { WithdrawCoinSource, WithdrawalSource } from "./dbTypes";
/* /*
This file is part of GNU Taler This file is part of GNU Taler
@ -34,10 +35,9 @@ export const enum NotificationType {
RefreshUnwarranted = "refresh-unwarranted", RefreshUnwarranted = "refresh-unwarranted",
ReserveUpdated = "reserve-updated", ReserveUpdated = "reserve-updated",
ReserveConfirmed = "reserve-confirmed", ReserveConfirmed = "reserve-confirmed",
ReserveDepleted = "reserve-depleted",
ReserveCreated = "reserve-created", ReserveCreated = "reserve-created",
WithdrawSessionCreated = "withdraw-session-created", WithdrawGroupCreated = "withdraw-group-created",
WithdrawSessionFinished = "withdraw-session-finished", WithdrawGroupFinished = "withdraw-group-finished",
WaitingForRetry = "waiting-for-retry", WaitingForRetry = "waiting-for-retry",
RefundStarted = "refund-started", RefundStarted = "refund-started",
RefundQueried = "refund-queried", RefundQueried = "refund-queried",
@ -114,19 +114,14 @@ export interface ReserveConfirmedNotification {
type: NotificationType.ReserveConfirmed; type: NotificationType.ReserveConfirmed;
} }
export interface WithdrawSessionCreatedNotification { export interface WithdrawalGroupCreatedNotification {
type: NotificationType.WithdrawSessionCreated; type: NotificationType.WithdrawGroupCreated;
withdrawSessionId: string; withdrawalGroupId: string;
} }
export interface WithdrawSessionFinishedNotification { export interface WithdrawalGroupFinishedNotification {
type: NotificationType.WithdrawSessionFinished; type: NotificationType.WithdrawGroupFinished;
withdrawSessionId: string; withdrawalSource: WithdrawalSource;
}
export interface ReserveDepletedNotification {
type: NotificationType.ReserveDepleted;
reservePub: string;
} }
export interface WaitingForRetryNotification { export interface WaitingForRetryNotification {
@ -210,13 +205,12 @@ export type WalletNotification =
| ReserveUpdatedNotification | ReserveUpdatedNotification
| ReserveCreatedNotification | ReserveCreatedNotification
| ReserveConfirmedNotification | ReserveConfirmedNotification
| WithdrawSessionFinishedNotification | WithdrawalGroupFinishedNotification
| ReserveDepletedNotification
| WaitingForRetryNotification | WaitingForRetryNotification
| RefundStartedNotification | RefundStartedNotification
| RefundFinishedNotification | RefundFinishedNotification
| RefundQueriedNotification | RefundQueriedNotification
| WithdrawSessionCreatedNotification | WithdrawalGroupCreatedNotification
| CoinWithdrawnNotification | CoinWithdrawnNotification
| WildcardNotification | WildcardNotification
| RecoupOperationErrorNotification; | RecoupOperationErrorNotification;

View File

@ -214,7 +214,8 @@ export interface PendingRecoupOperation {
export interface PendingWithdrawOperation { export interface PendingWithdrawOperation {
type: PendingOperationType.Withdraw; type: PendingOperationType.Withdraw;
source: WithdrawalSource; source: WithdrawalSource;
withdrawSessionId: string; lastError: OperationError | undefined;
withdrawalGroupId: string;
numCoinsWithdrawn: number; numCoinsWithdrawn: number;
numCoinsTotal: number; numCoinsTotal: number;
} }

View File

@ -15,14 +15,14 @@
*/ */
import test from "ava"; import test from "ava";
import * as Amounts from "../util/amounts"; import { Amounts, AmountJson } from "../util/amounts";
import { ContractTerms, codecForContractTerms } from "./talerTypes"; import { codecForContractTerms } from "./talerTypes";
const amt = ( const amt = (
value: number, value: number,
fraction: number, fraction: number,
currency: string, currency: string,
): Amounts.AmountJson => ({ value, fraction, currency }); ): AmountJson => ({ value, fraction, currency });
test("amount addition (simple)", (t) => { test("amount addition (simple)", (t) => {
const a1 = amt(1, 0, "EUR"); const a1 = amt(1, 0, "EUR");
@ -118,13 +118,13 @@ test("amount parsing", (t) => {
}); });
test("amount stringification", (t) => { test("amount stringification", (t) => {
t.is(Amounts.toString(amt(0, 0, "TESTKUDOS")), "TESTKUDOS:0"); t.is(Amounts.stringify(amt(0, 0, "TESTKUDOS")), "TESTKUDOS:0");
t.is(Amounts.toString(amt(4, 94000000, "TESTKUDOS")), "TESTKUDOS:4.94"); t.is(Amounts.stringify(amt(4, 94000000, "TESTKUDOS")), "TESTKUDOS:4.94");
t.is(Amounts.toString(amt(0, 10000000, "TESTKUDOS")), "TESTKUDOS:0.1"); t.is(Amounts.stringify(amt(0, 10000000, "TESTKUDOS")), "TESTKUDOS:0.1");
t.is(Amounts.toString(amt(0, 1, "TESTKUDOS")), "TESTKUDOS:0.00000001"); t.is(Amounts.stringify(amt(0, 1, "TESTKUDOS")), "TESTKUDOS:0.00000001");
t.is(Amounts.toString(amt(5, 0, "TESTKUDOS")), "TESTKUDOS:5"); t.is(Amounts.stringify(amt(5, 0, "TESTKUDOS")), "TESTKUDOS:5");
// denormalized // denormalized
t.is(Amounts.toString(amt(1, 100000000, "TESTKUDOS")), "TESTKUDOS:2"); t.is(Amounts.stringify(amt(1, 100000000, "TESTKUDOS")), "TESTKUDOS:2");
t.pass(); t.pass();
}); });

View File

@ -427,6 +427,7 @@ export interface PlanchetCreationResult {
withdrawSig: string; withdrawSig: string;
coinEv: string; coinEv: string;
coinValue: AmountJson; coinValue: AmountJson;
coinEvHash: string;
} }
export interface PlanchetCreationRequest { export interface PlanchetCreationRequest {

View File

@ -299,7 +299,7 @@ export function fromFloat(floatVal: number, currency: string) {
* Convert to standard human-readable string representation that's * Convert to standard human-readable string representation that's
* also used in JSON formats. * also used in JSON formats.
*/ */
export function toString(a: AmountJson): string { export function stringify(a: AmountJson): string {
const av = a.value + Math.floor(a.fraction / fractionalBase); const av = a.value + Math.floor(a.fraction / fractionalBase);
const af = a.fraction % fractionalBase; const af = a.fraction % fractionalBase;
let s = av.toString(); let s = av.toString();
@ -322,7 +322,7 @@ export function toString(a: AmountJson): string {
/** /**
* Check if the argument is a valid amount in string form. * Check if the argument is a valid amount in string form.
*/ */
export function check(a: any): boolean { function check(a: any): boolean {
if (typeof a !== "string") { if (typeof a !== "string") {
return false; return false;
} }
@ -333,3 +333,19 @@ export function check(a: any): boolean {
return false; return false;
} }
} }
// Export all amount-related functions here for better IDE experience.
export const Amounts = {
stringify: stringify,
parse: parse,
parseOrThrow: parseOrThrow,
cmp: cmp,
add: add,
sum: sum,
sub: sub,
check: check,
getZero: getZero,
isZero: isZero,
maxAmountValue: maxAmountValue,
fromFloat: fromFloat,
};

View File

@ -0,0 +1,286 @@
/*
This file is part of GNU Taler
(C) 2020 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 test from "ava";
import {
reconcileReserveHistory,
summarizeReserveHistory,
} from "./reserveHistoryUtil";
import {
WalletReserveHistoryItem,
WalletReserveHistoryItemType,
} from "../types/dbTypes";
import {
ReserveTransaction,
ReserveTransactionType,
} from "../types/ReserveTransaction";
import { Amounts } from "./amounts";
test("basics", (t) => {
const r = reconcileReserveHistory([], []);
t.deepEqual(r.updatedLocalHistory, []);
});
test("unmatched credit", (t) => {
const localHistory: WalletReserveHistoryItem[] = [];
const remoteHistory: ReserveTransaction[] = [
{
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:100",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC01",
},
];
const r = reconcileReserveHistory(localHistory, remoteHistory);
const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
t.deepEqual(r.updatedLocalHistory.length, 1);
t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:100");
t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:100");
});
test("unmatched credit #2", (t) => {
const localHistory: WalletReserveHistoryItem[] = [];
const remoteHistory: ReserveTransaction[] = [
{
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:100",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC01",
},
{
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:50",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC02",
},
];
const r = reconcileReserveHistory(localHistory, remoteHistory);
const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
t.deepEqual(r.updatedLocalHistory.length, 2);
t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:150");
});
test("matched credit", (t) => {
const localHistory: WalletReserveHistoryItem[] = [
{
type: WalletReserveHistoryItemType.Credit,
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
matchedExchangeTransaction: {
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:100",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC01",
},
},
];
const remoteHistory: ReserveTransaction[] = [
{
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:100",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC01",
},
{
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:50",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC02",
},
];
const r = reconcileReserveHistory(localHistory, remoteHistory);
const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
t.deepEqual(r.updatedLocalHistory.length, 2);
t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:150");
});
test("fulfilling credit", (t) => {
const localHistory: WalletReserveHistoryItem[] = [
{
type: WalletReserveHistoryItemType.Credit,
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
},
];
const remoteHistory: ReserveTransaction[] = [
{
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:100",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC01",
},
{
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:50",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC02",
},
];
const r = reconcileReserveHistory(localHistory, remoteHistory);
const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
t.deepEqual(r.updatedLocalHistory.length, 2);
t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
});
test("unfulfilled credit", (t) => {
const localHistory: WalletReserveHistoryItem[] = [
{
type: WalletReserveHistoryItemType.Credit,
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
},
];
const remoteHistory: ReserveTransaction[] = [
{
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:100",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC01",
},
{
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:50",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC02",
},
];
const r = reconcileReserveHistory(localHistory, remoteHistory);
const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
t.deepEqual(r.updatedLocalHistory.length, 2);
t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
});
test("awaited credit", (t) => {
const localHistory: WalletReserveHistoryItem[] = [
{
type: WalletReserveHistoryItemType.Credit,
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:50"),
},
{
type: WalletReserveHistoryItemType.Credit,
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
},
];
const remoteHistory: ReserveTransaction[] = [
{
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:100",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC01",
},
];
const r = reconcileReserveHistory(localHistory, remoteHistory);
const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
t.deepEqual(r.updatedLocalHistory.length, 2);
t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:100");
t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:50");
t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:100");
});
test("withdrawal new match", (t) => {
const localHistory: WalletReserveHistoryItem[] = [
{
type: WalletReserveHistoryItemType.Credit,
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
matchedExchangeTransaction: {
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:100",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC01",
},
},
{
type: WalletReserveHistoryItemType.Withdraw,
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:5"),
},
];
const remoteHistory: ReserveTransaction[] = [
{
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:100",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC01",
},
{
type: ReserveTransactionType.Withdraw,
amount: "TESTKUDOS:5",
h_coin_envelope: "foobar",
h_denom_pub: "foobar",
reserve_sig: "foobar",
withdraw_fee: "TESTKUDOS:0.1",
},
];
const r = reconcileReserveHistory(localHistory, remoteHistory);
const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
console.log(r);
t.deepEqual(r.updatedLocalHistory.length, 2);
t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:95");
t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:95");
});
test("claimed but now arrived", (t) => {
const localHistory: WalletReserveHistoryItem[] = [
{
type: WalletReserveHistoryItemType.Credit,
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
matchedExchangeTransaction: {
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:100",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC01",
},
},
{
type: WalletReserveHistoryItemType.Withdraw,
expectedAmount: Amounts.parseOrThrow("TESTKUDOS:5"),
},
];
const remoteHistory: ReserveTransaction[] = [
{
type: ReserveTransactionType.Credit,
amount: "TESTKUDOS:100",
sender_account_url: "payto://void/",
timestamp: { t_ms: 42 },
wire_reference: "ABC01",
},
];
const r = reconcileReserveHistory(localHistory, remoteHistory);
const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
t.deepEqual(r.updatedLocalHistory.length, 2);
t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:100");
t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:95");
});

View File

@ -0,0 +1,384 @@
/*
This file is part of GNU Taler
(C) 2020 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 {
WalletReserveHistoryItem,
WalletReserveHistoryItemType,
} from "../types/dbTypes";
import {
ReserveTransaction,
ReserveTransactionType,
} from "../types/ReserveTransaction";
import * as Amounts from "../util/amounts";
import { timestampCmp } from "./time";
import { deepCopy } from "./helpers";
import { AmountString } from "../types/talerTypes";
import { AmountJson } from "../util/amounts";
/**
* Helpers for dealing with reserve histories.
*
* @author Florian Dold <dold@taler.net>
*/
export interface ReserveReconciliationResult {
/**
* The wallet's local history reconciled with the exchange's reserve history.
*/
updatedLocalHistory: WalletReserveHistoryItem[];
/**
* History items that were newly created, subset of the
* updatedLocalHistory items.
*/
newAddedItems: WalletReserveHistoryItem[];
/**
* History items that were newly matched, subset of the
* updatedLocalHistory items.
*/
newMatchedItems: WalletReserveHistoryItem[];
}
export interface ReserveHistorySummary {
/**
* Balance computed by the wallet, should match the balance
* computed by the reserve.
*/
computedReserveBalance: Amounts.AmountJson;
/**
* Reserve balance that is still available for withdrawal.
*/
unclaimedReserveAmount: Amounts.AmountJson;
/**
* Amount that we're still expecting to come into the reserve.
*/
awaitedReserveAmount: Amounts.AmountJson;
/**
* Amount withdrawn from the reserve so far. Only counts
* finished withdrawals, not withdrawals in progress.
*/
withdrawnAmount: Amounts.AmountJson;
}
export function isRemoteHistoryMatch(
t1: ReserveTransaction,
t2: ReserveTransaction,
): boolean {
switch (t1.type) {
case ReserveTransactionType.Closing: {
return t1.type === t2.type && t1.wtid == t2.wtid;
}
case ReserveTransactionType.Credit: {
return t1.type === t2.type && t1.wire_reference === t2.wire_reference;
}
case ReserveTransactionType.Recoup: {
return (
t1.type === t2.type &&
t1.coin_pub === t2.coin_pub &&
timestampCmp(t1.timestamp, t2.timestamp) === 0
);
}
case ReserveTransactionType.Withdraw: {
return t1.type === t2.type && t1.h_coin_envelope === t2.h_coin_envelope;
}
}
}
export function isLocalRemoteHistoryPreferredMatch(
t1: WalletReserveHistoryItem,
t2: ReserveTransaction,
): boolean {
switch (t1.type) {
case WalletReserveHistoryItemType.Credit: {
return (
t2.type === ReserveTransactionType.Credit &&
!!t1.expectedAmount &&
Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0
);
}
case WalletReserveHistoryItemType.Withdraw:
return (
t2.type === ReserveTransactionType.Withdraw &&
!!t1.expectedAmount &&
Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0
)
case WalletReserveHistoryItemType.Recoup: {
return (
t2.type === ReserveTransactionType.Recoup &&
!!t1.expectedAmount &&
Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0
);
}
}
return false;
}
export function isLocalRemoteHistoryAcceptableMatch(
t1: WalletReserveHistoryItem,
t2: ReserveTransaction,
): boolean {
switch (t1.type) {
case WalletReserveHistoryItemType.Closing:
throw Error("invariant violated");
case WalletReserveHistoryItemType.Credit:
return !t1.expectedAmount && t2.type == ReserveTransactionType.Credit;
case WalletReserveHistoryItemType.Recoup:
return !t1.expectedAmount && t2.type == ReserveTransactionType.Recoup;
case WalletReserveHistoryItemType.Withdraw:
return !t1.expectedAmount && t2.type == ReserveTransactionType.Withdraw;
}
}
export function summarizeReserveHistory(
localHistory: WalletReserveHistoryItem[],
currency: string,
): ReserveHistorySummary {
const posAmounts: AmountJson[] = [];
const negAmounts: AmountJson[] = [];
const expectedPosAmounts: AmountJson[] = [];
const expectedNegAmounts: AmountJson[] = [];
const withdrawnAmounts: AmountJson[] = [];
for (const item of localHistory) {
switch (item.type) {
case WalletReserveHistoryItemType.Credit:
if (item.matchedExchangeTransaction) {
posAmounts.push(
Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
);
} else if (item.expectedAmount) {
expectedPosAmounts.push(item.expectedAmount);
}
break;
case WalletReserveHistoryItemType.Recoup:
if (item.matchedExchangeTransaction) {
if (item.matchedExchangeTransaction) {
posAmounts.push(
Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
);
} else if (item.expectedAmount) {
expectedPosAmounts.push(item.expectedAmount);
} else {
throw Error("invariant failed");
}
}
break;
case WalletReserveHistoryItemType.Closing:
if (item.matchedExchangeTransaction) {
negAmounts.push(
Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
);
} else {
throw Error("invariant failed");
}
break;
case WalletReserveHistoryItemType.Withdraw:
if (item.matchedExchangeTransaction) {
negAmounts.push(
Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
);
withdrawnAmounts.push(Amounts.parseOrThrow(item.matchedExchangeTransaction.amount));
} else if (item.expectedAmount) {
expectedNegAmounts.push(item.expectedAmount);
} else {
throw Error("invariant failed");
}
break;
}
}
const z = Amounts.getZero(currency);
const computedBalance = Amounts.sub(
Amounts.add(z, ...posAmounts).amount,
...negAmounts,
).amount;
const unclaimedReserveAmount = Amounts.sub(
Amounts.add(z, ...posAmounts).amount,
...negAmounts,
...expectedNegAmounts,
).amount;
const awaitedReserveAmount = Amounts.sub(
Amounts.add(z, ...expectedPosAmounts).amount,
...expectedNegAmounts,
).amount;
const withdrawnAmount = Amounts.add(z, ...withdrawnAmounts).amount;
return {
computedReserveBalance: computedBalance,
unclaimedReserveAmount: unclaimedReserveAmount,
awaitedReserveAmount: awaitedReserveAmount,
withdrawnAmount,
};
}
export function reconcileReserveHistory(
localHistory: WalletReserveHistoryItem[],
remoteHistory: ReserveTransaction[],
): ReserveReconciliationResult {
const updatedLocalHistory: WalletReserveHistoryItem[] = deepCopy(
localHistory,
);
const newMatchedItems: WalletReserveHistoryItem[] = [];
const newAddedItems: WalletReserveHistoryItem[] = [];
const remoteMatched = remoteHistory.map(() => false);
const localMatched = localHistory.map(() => false);
// Take care of deposits
// First, see which pairs are already a definite match.
for (let remoteIndex = 0; remoteIndex < remoteHistory.length; remoteIndex++) {
const rhi = remoteHistory[remoteIndex];
for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
if (localMatched[localIndex]) {
continue;
}
const lhi = localHistory[localIndex];
if (!lhi.matchedExchangeTransaction) {
continue;
}
if (isRemoteHistoryMatch(rhi, lhi.matchedExchangeTransaction)) {
localMatched[localIndex] = true;
remoteMatched[remoteIndex] = true;
break;
}
}
}
// Check that all previously matched items are still matched
for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
if (localMatched[localIndex]) {
continue;
}
const lhi = localHistory[localIndex];
if (lhi.matchedExchangeTransaction) {
// Don't use for further matching
localMatched[localIndex] = true;
// FIXME: emit some error here!
throw Error("previously matched reserve history item now unmatched");
}
}
// Next, find out if there are any exact new matches between local and remote
// history items
for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
if (localMatched[localIndex]) {
continue;
}
const lhi = localHistory[localIndex];
for (
let remoteIndex = 0;
remoteIndex < remoteHistory.length;
remoteIndex++
) {
const rhi = remoteHistory[remoteIndex];
if (remoteMatched[remoteIndex]) {
continue;
}
if (isLocalRemoteHistoryPreferredMatch(lhi, rhi)) {
localMatched[localIndex] = true;
remoteMatched[remoteIndex] = true;
updatedLocalHistory[localIndex].matchedExchangeTransaction = rhi as any;
newMatchedItems.push(lhi);
break;
}
}
}
// Next, find out if there are any acceptable new matches between local and remote
// history items
for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
if (localMatched[localIndex]) {
continue;
}
const lhi = localHistory[localIndex];
for (
let remoteIndex = 0;
remoteIndex < remoteHistory.length;
remoteIndex++
) {
const rhi = remoteHistory[remoteIndex];
if (remoteMatched[remoteIndex]) {
continue;
}
if (isLocalRemoteHistoryAcceptableMatch(lhi, rhi)) {
localMatched[localIndex] = true;
remoteMatched[remoteIndex] = true;
updatedLocalHistory[localIndex].matchedExchangeTransaction = rhi as any;
newMatchedItems.push(lhi);
break;
}
}
}
// Finally we add new history items
for (let remoteIndex = 0; remoteIndex < remoteHistory.length; remoteIndex++) {
if (remoteMatched[remoteIndex]) {
continue;
}
const rhi = remoteHistory[remoteIndex];
let newItem: WalletReserveHistoryItem;
switch (rhi.type) {
case ReserveTransactionType.Closing: {
newItem = {
type: WalletReserveHistoryItemType.Closing,
matchedExchangeTransaction: rhi,
};
break;
}
case ReserveTransactionType.Credit: {
newItem = {
type: WalletReserveHistoryItemType.Credit,
matchedExchangeTransaction: rhi,
};
break;
}
case ReserveTransactionType.Recoup: {
newItem = {
type: WalletReserveHistoryItemType.Recoup,
matchedExchangeTransaction: rhi,
};
break;
}
case ReserveTransactionType.Withdraw: {
newItem = {
type: WalletReserveHistoryItemType.Withdraw,
matchedExchangeTransaction: rhi,
};
break;
}
}
updatedLocalHistory.push(newItem);
newAddedItems.push(newItem);
}
return {
updatedLocalHistory,
newAddedItems,
newMatchedItems,
};
}

View File

@ -26,8 +26,7 @@ import { CryptoWorkerFactory } from "./crypto/workers/cryptoApi";
import { HttpRequestLibrary } from "./util/http"; import { HttpRequestLibrary } from "./util/http";
import { Database } from "./util/query"; import { Database } from "./util/query";
import { AmountJson } from "./util/amounts"; import { Amounts, AmountJson } from "./util/amounts";
import * as Amounts from "./util/amounts";
import { import {
getWithdrawDetailsForUri, getWithdrawDetailsForUri,
@ -92,7 +91,7 @@ import {
import { InternalWalletState } from "./operations/state"; import { InternalWalletState } from "./operations/state";
import { createReserve, confirmReserve } from "./operations/reserves"; import { createReserve, confirmReserve } from "./operations/reserves";
import { processRefreshGroup, createRefreshGroup } from "./operations/refresh"; import { processRefreshGroup, createRefreshGroup } from "./operations/refresh";
import { processWithdrawSession } from "./operations/withdraw"; import { processWithdrawGroup } from "./operations/withdraw";
import { getHistory } from "./operations/history"; import { getHistory } from "./operations/history";
import { getPendingOperations } from "./operations/pending"; import { getPendingOperations } from "./operations/pending";
import { getBalances } from "./operations/balance"; import { getBalances } from "./operations/balance";
@ -193,9 +192,9 @@ export class Wallet {
await processReserve(this.ws, pending.reservePub, forceNow); await processReserve(this.ws, pending.reservePub, forceNow);
break; break;
case PendingOperationType.Withdraw: case PendingOperationType.Withdraw:
await processWithdrawSession( await processWithdrawGroup(
this.ws, this.ws,
pending.withdrawSessionId, pending.withdrawalGroupId,
forceNow, forceNow,
); );
break; break;
@ -574,10 +573,14 @@ export class Wallet {
await this.db.put(Stores.currencies, currencyRecord); await this.db.put(Stores.currencies, currencyRecord);
} }
async getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> { async getReserves(exchangeBaseUrl?: string): Promise<ReserveRecord[]> {
return await this.db if (exchangeBaseUrl) {
.iter(Stores.reserves) return await this.db
.filter((r) => r.exchangeBaseUrl === exchangeBaseUrl); .iter(Stores.reserves)
.filter((r) => r.exchangeBaseUrl === exchangeBaseUrl);
} else {
return await this.db.iter(Stores.reserves).toArray();
}
} }
async getCoinsForExchange(exchangeBaseUrl: string): Promise<CoinRecord[]> { async getCoinsForExchange(exchangeBaseUrl: string): Promise<CoinRecord[]> {
@ -807,8 +810,8 @@ export class Wallet {
let withdrawalReservePub: string | undefined; let withdrawalReservePub: string | undefined;
if (cs.type == CoinSourceType.Withdraw) { if (cs.type == CoinSourceType.Withdraw) {
const ws = await this.db.get( const ws = await this.db.get(
Stores.withdrawalSession, Stores.withdrawalGroups,
cs.withdrawSessionId, cs.withdrawalGroupId,
); );
if (!ws) { if (!ws) {
console.error("no withdrawal session found for coin"); console.error("no withdrawal session found for coin");
@ -822,10 +825,10 @@ export class Wallet {
coin_pub: c.coinPub, coin_pub: c.coinPub,
denom_pub: c.denomPub, denom_pub: c.denomPub,
denom_pub_hash: c.denomPubHash, denom_pub_hash: c.denomPubHash,
denom_value: Amounts.toString(denom.value), denom_value: Amounts.stringify(denom.value),
exchange_base_url: c.exchangeBaseUrl, exchange_base_url: c.exchangeBaseUrl,
refresh_parent_coin_pub: refreshParentCoinPub, refresh_parent_coin_pub: refreshParentCoinPub,
remaining_value: Amounts.toString(c.currentAmount), remaining_value: Amounts.stringify(c.currentAmount),
withdrawal_reserve_pub: withdrawalReservePub, withdrawal_reserve_pub: withdrawalReservePub,
coin_suspended: c.suspended, coin_suspended: c.suspended,
}); });

View File

@ -565,10 +565,6 @@ function formatHistoryItem(historyItem: HistoryEvent) {
<HistoryItem <HistoryItem
timestamp={historyItem.timestamp} timestamp={historyItem.timestamp}
small={i18n.str`Reserve balance updated`} small={i18n.str`Reserve balance updated`}
fees={amountDiff(
historyItem.amountExpected,
historyItem.amountReserveBalance,
)}
/> />
); );
} }

View File

@ -25,7 +25,7 @@
*/ */
import { AmountJson } from "../../util/amounts"; import { AmountJson } from "../../util/amounts";
import * as Amounts from "../../util/amounts"; import { Amounts } from "../../util/amounts";
import { SenderWireInfos, WalletBalance } from "../../types/walletTypes"; import { SenderWireInfos, WalletBalance } from "../../types/walletTypes";
@ -70,7 +70,7 @@ class ReturnSelectionItem extends React.Component<
); );
this.state = { this.state = {
currency: props.balance.byExchange[props.exchangeUrl].available.currency, currency: props.balance.byExchange[props.exchangeUrl].available.currency,
selectedValue: Amounts.toString( selectedValue: Amounts.stringify(
props.balance.byExchange[props.exchangeUrl].available, props.balance.byExchange[props.exchangeUrl].available,
), ),
selectedWire: "", selectedWire: "",