From ef0acf06bfb7820a21c4719dba0d659f600be3c7 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 2 Apr 2020 20:33:01 +0530 Subject: model reserve history in the exchange, improve reserve handling logic --- src/util/reserveHistoryUtil.ts | 384 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 src/util/reserveHistoryUtil.ts (limited to 'src/util/reserveHistoryUtil.ts') diff --git a/src/util/reserveHistoryUtil.ts b/src/util/reserveHistoryUtil.ts new file mode 100644 index 000000000..95f58449e --- /dev/null +++ b/src/util/reserveHistoryUtil.ts @@ -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 + */ + +/** + * 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 + */ + +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, + }; +} -- cgit v1.2.3