diff options
Diffstat (limited to 'src/operations/recoup.ts')
-rw-r--r-- | src/operations/recoup.ts | 411 |
1 files changed, 0 insertions, 411 deletions
diff --git a/src/operations/recoup.ts b/src/operations/recoup.ts deleted file mode 100644 index e5f14c6ee..000000000 --- a/src/operations/recoup.ts +++ /dev/null @@ -1,411 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2019-2020 Taler Systems SA - - 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/> - */ - -/** - * Implementation of the recoup operation, which allows to recover the - * value of coins held in a revoked denomination. - * - * @author Florian Dold <dold@taler.net> - */ - -/** - * Imports. - */ -import { InternalWalletState } from "./state"; -import { - Stores, - CoinStatus, - CoinSourceType, - CoinRecord, - WithdrawCoinSource, - RefreshCoinSource, - ReserveRecordStatus, - RecoupGroupRecord, - initRetryInfo, - updateRetryInfoTimeout, -} from "../types/dbTypes"; - -import { codecForRecoupConfirmation } from "../types/talerTypes"; -import { NotificationType } from "../types/notifications"; -import { forceQueryReserve } from "./reserves"; - -import { Amounts } from "../util/amounts"; -import { createRefreshGroup, processRefreshGroup } from "./refresh"; -import { RefreshReason, OperationErrorDetails } from "../types/walletTypes"; -import { TransactionHandle } from "../util/query"; -import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto"; -import { getTimestampNow } from "../util/time"; -import { guardOperationException } from "./errors"; -import { readSuccessResponseJsonOrThrow } from "../util/http"; - -async function incrementRecoupRetry( - ws: InternalWalletState, - recoupGroupId: string, - err: OperationErrorDetails | undefined, -): Promise<void> { - await ws.db.runWithWriteTransaction([Stores.recoupGroups], async (tx) => { - const r = await tx.get(Stores.recoupGroups, recoupGroupId); - if (!r) { - return; - } - if (!r.retryInfo) { - return; - } - r.retryInfo.retryCounter++; - updateRetryInfoTimeout(r.retryInfo); - r.lastError = err; - await tx.put(Stores.recoupGroups, r); - }); - if (err) { - ws.notify({ type: NotificationType.RecoupOperationError, error: err }); - } -} - -async function putGroupAsFinished( - ws: InternalWalletState, - tx: TransactionHandle, - recoupGroup: RecoupGroupRecord, - coinIdx: number, -): Promise<void> { - if (recoupGroup.timestampFinished) { - return; - } - recoupGroup.recoupFinishedPerCoin[coinIdx] = true; - let allFinished = true; - for (const b of recoupGroup.recoupFinishedPerCoin) { - if (!b) { - allFinished = false; - } - } - if (allFinished) { - recoupGroup.timestampFinished = getTimestampNow(); - recoupGroup.retryInfo = initRetryInfo(false); - recoupGroup.lastError = undefined; - if (recoupGroup.scheduleRefreshCoins.length > 0) { - const refreshGroupId = await createRefreshGroup( - ws, - tx, - recoupGroup.scheduleRefreshCoins.map((x) => ({ coinPub: x })), - RefreshReason.Recoup, - ); - processRefreshGroup(ws, refreshGroupId.refreshGroupId).then((e) => { - console.error("error while refreshing after recoup", e); - }); - } - } - await tx.put(Stores.recoupGroups, recoupGroup); -} - -async function recoupTipCoin( - ws: InternalWalletState, - recoupGroupId: string, - coinIdx: number, - coin: CoinRecord, -): Promise<void> { - // We can't really recoup a coin we got via tipping. - // Thus we just put the coin to sleep. - // FIXME: somehow report this to the user - await ws.db.runWithWriteTransaction([Stores.recoupGroups], async (tx) => { - const recoupGroup = await tx.get(Stores.recoupGroups, recoupGroupId); - if (!recoupGroup) { - return; - } - if (recoupGroup.recoupFinishedPerCoin[coinIdx]) { - return; - } - await putGroupAsFinished(ws, tx, recoupGroup, coinIdx); - }); -} - -async function recoupWithdrawCoin( - ws: InternalWalletState, - recoupGroupId: string, - coinIdx: number, - coin: CoinRecord, - cs: WithdrawCoinSource, -): Promise<void> { - const reservePub = cs.reservePub; - const reserve = await ws.db.get(Stores.reserves, reservePub); - if (!reserve) { - // FIXME: We should at least emit some pending operation / warning for this? - return; - } - - ws.notify({ - type: NotificationType.RecoupStarted, - }); - - const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin); - const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl); - const resp = await ws.http.postJson(reqUrl.href, recoupRequest); - const recoupConfirmation = await readSuccessResponseJsonOrThrow( - resp, - codecForRecoupConfirmation(), - ); - - if (recoupConfirmation.reserve_pub !== reservePub) { - throw Error(`Coin's reserve doesn't match reserve on recoup`); - } - - const exchange = await ws.db.get(Stores.exchanges, coin.exchangeBaseUrl); - if (!exchange) { - // FIXME: report inconsistency? - return; - } - const exchangeDetails = exchange.details; - if (!exchangeDetails) { - // FIXME: report inconsistency? - return; - } - - // FIXME: verify that our expectations about the amount match - - await ws.db.runWithWriteTransaction( - [Stores.coins, Stores.reserves, Stores.recoupGroups], - async (tx) => { - const recoupGroup = await tx.get(Stores.recoupGroups, recoupGroupId); - if (!recoupGroup) { - return; - } - if (recoupGroup.recoupFinishedPerCoin[coinIdx]) { - return; - } - const updatedCoin = await tx.get(Stores.coins, coin.coinPub); - if (!updatedCoin) { - return; - } - const updatedReserve = await tx.get(Stores.reserves, reserve.reservePub); - if (!updatedReserve) { - return; - } - updatedCoin.status = CoinStatus.Dormant; - const currency = updatedCoin.currentAmount.currency; - updatedCoin.currentAmount = Amounts.getZero(currency); - updatedReserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS; - await tx.put(Stores.coins, updatedCoin); - await tx.put(Stores.reserves, updatedReserve); - await putGroupAsFinished(ws, tx, recoupGroup, coinIdx); - }, - ); - - ws.notify({ - type: NotificationType.RecoupFinished, - }); - - forceQueryReserve(ws, reserve.reservePub).catch((e) => { - console.log("re-querying reserve after recoup failed:", e); - }); -} - -async function recoupRefreshCoin( - ws: InternalWalletState, - recoupGroupId: string, - coinIdx: number, - coin: CoinRecord, - cs: RefreshCoinSource, -): Promise<void> { - ws.notify({ - type: NotificationType.RecoupStarted, - }); - - const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin); - const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl); - console.log("making recoup request"); - - const resp = await ws.http.postJson(reqUrl.href, recoupRequest); - const recoupConfirmation = await readSuccessResponseJsonOrThrow( - resp, - codecForRecoupConfirmation(), - ); - - if (recoupConfirmation.old_coin_pub != cs.oldCoinPub) { - throw Error(`Coin's oldCoinPub doesn't match reserve on recoup`); - } - - const exchange = await ws.db.get(Stores.exchanges, coin.exchangeBaseUrl); - if (!exchange) { - // FIXME: report inconsistency? - return; - } - const exchangeDetails = exchange.details; - if (!exchangeDetails) { - // FIXME: report inconsistency? - return; - } - - await ws.db.runWithWriteTransaction( - [Stores.coins, Stores.reserves, Stores.recoupGroups, Stores.refreshGroups], - async (tx) => { - const recoupGroup = await tx.get(Stores.recoupGroups, recoupGroupId); - if (!recoupGroup) { - return; - } - if (recoupGroup.recoupFinishedPerCoin[coinIdx]) { - return; - } - const oldCoin = await tx.get(Stores.coins, cs.oldCoinPub); - const revokedCoin = await tx.get(Stores.coins, coin.coinPub); - if (!revokedCoin) { - return; - } - if (!oldCoin) { - return; - } - revokedCoin.status = CoinStatus.Dormant; - oldCoin.currentAmount = Amounts.add( - oldCoin.currentAmount, - recoupGroup.oldAmountPerCoin[coinIdx], - ).amount; - console.log( - "recoup: setting old coin amount to", - Amounts.stringify(oldCoin.currentAmount), - ); - recoupGroup.scheduleRefreshCoins.push(oldCoin.coinPub); - await tx.put(Stores.coins, revokedCoin); - await tx.put(Stores.coins, oldCoin); - await putGroupAsFinished(ws, tx, recoupGroup, coinIdx); - }, - ); -} - -async function resetRecoupGroupRetry( - ws: InternalWalletState, - recoupGroupId: string, -): Promise<void> { - await ws.db.mutate(Stores.recoupGroups, recoupGroupId, (x) => { - if (x.retryInfo.active) { - x.retryInfo = initRetryInfo(); - } - return x; - }); -} - -export async function processRecoupGroup( - ws: InternalWalletState, - recoupGroupId: string, - forceNow = false, -): Promise<void> { - await ws.memoProcessRecoup.memo(recoupGroupId, async () => { - const onOpErr = (e: OperationErrorDetails): Promise<void> => - incrementRecoupRetry(ws, recoupGroupId, e); - return await guardOperationException( - async () => await processRecoupGroupImpl(ws, recoupGroupId, forceNow), - onOpErr, - ); - }); -} - -async function processRecoupGroupImpl( - ws: InternalWalletState, - recoupGroupId: string, - forceNow = false, -): Promise<void> { - if (forceNow) { - await resetRecoupGroupRetry(ws, recoupGroupId); - } - console.log("in processRecoupGroupImpl"); - const recoupGroup = await ws.db.get(Stores.recoupGroups, recoupGroupId); - if (!recoupGroup) { - return; - } - console.log(recoupGroup); - if (recoupGroup.timestampFinished) { - console.log("recoup group finished"); - return; - } - const ps = recoupGroup.coinPubs.map((x, i) => - processRecoup(ws, recoupGroupId, i), - ); - await Promise.all(ps); -} - -export async function createRecoupGroup( - ws: InternalWalletState, - tx: TransactionHandle, - coinPubs: string[], -): Promise<string> { - const recoupGroupId = encodeCrock(getRandomBytes(32)); - - const recoupGroup: RecoupGroupRecord = { - recoupGroupId, - coinPubs: coinPubs, - lastError: undefined, - timestampFinished: undefined, - timestampStarted: getTimestampNow(), - retryInfo: initRetryInfo(), - recoupFinishedPerCoin: coinPubs.map(() => false), - // Will be populated later - oldAmountPerCoin: [], - scheduleRefreshCoins: [], - }; - - for (let coinIdx = 0; coinIdx < coinPubs.length; coinIdx++) { - const coinPub = coinPubs[coinIdx]; - const coin = await tx.get(Stores.coins, coinPub); - if (!coin) { - await putGroupAsFinished(ws, tx, recoupGroup, coinIdx); - continue; - } - if (Amounts.isZero(coin.currentAmount)) { - await putGroupAsFinished(ws, tx, recoupGroup, coinIdx); - continue; - } - recoupGroup.oldAmountPerCoin[coinIdx] = coin.currentAmount; - coin.currentAmount = Amounts.getZero(coin.currentAmount.currency); - await tx.put(Stores.coins, coin); - } - - await tx.put(Stores.recoupGroups, recoupGroup); - - return recoupGroupId; -} - -async function processRecoup( - ws: InternalWalletState, - recoupGroupId: string, - coinIdx: number, -): Promise<void> { - const recoupGroup = await ws.db.get(Stores.recoupGroups, recoupGroupId); - if (!recoupGroup) { - return; - } - if (recoupGroup.timestampFinished) { - return; - } - if (recoupGroup.recoupFinishedPerCoin[coinIdx]) { - return; - } - - const coinPub = recoupGroup.coinPubs[coinIdx]; - - const coin = await ws.db.get(Stores.coins, coinPub); - if (!coin) { - throw Error(`Coin ${coinPub} not found, can't request payback`); - } - - const cs = coin.coinSource; - - switch (cs.type) { - case CoinSourceType.Tip: - return recoupTipCoin(ws, recoupGroupId, coinIdx, coin); - case CoinSourceType.Refresh: - return recoupRefreshCoin(ws, recoupGroupId, coinIdx, coin, cs); - case CoinSourceType.Withdraw: - return recoupWithdrawCoin(ws, recoupGroupId, coinIdx, coin, cs); - default: - throw Error("unknown coin source type"); - } -} |