summaryrefslogtreecommitdiff
path: root/src/operations/refresh.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/operations/refresh.ts')
-rw-r--r--src/operations/refresh.ts572
1 files changed, 0 insertions, 572 deletions
diff --git a/src/operations/refresh.ts b/src/operations/refresh.ts
deleted file mode 100644
index 74b032b91..000000000
--- a/src/operations/refresh.ts
+++ /dev/null
@@ -1,572 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- 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/>
- */
-
-import { Amounts, AmountJson } from "../util/amounts";
-import {
- DenominationRecord,
- Stores,
- CoinStatus,
- RefreshPlanchetRecord,
- CoinRecord,
- RefreshSessionRecord,
- initRetryInfo,
- updateRetryInfoTimeout,
- RefreshGroupRecord,
- CoinSourceType,
-} from "../types/dbTypes";
-import { amountToPretty } from "../util/helpers";
-import { TransactionHandle } from "../util/query";
-import { InternalWalletState } from "./state";
-import { Logger } from "../util/logging";
-import { getWithdrawDenomList } from "./withdraw";
-import { updateExchangeFromUrl } from "./exchanges";
-import {
- OperationErrorDetails,
- CoinPublicKey,
- RefreshReason,
- RefreshGroupId,
-} from "../types/walletTypes";
-import { guardOperationException } from "./errors";
-import { NotificationType } from "../types/notifications";
-import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
-import { getTimestampNow } from "../util/time";
-import { readSuccessResponseJsonOrThrow } from "../util/http";
-import {
- codecForExchangeMeltResponse,
- codecForExchangeRevealResponse,
-} from "../types/talerTypes";
-
-const logger = new Logger("refresh.ts");
-
-/**
- * Get the amount that we lose when refreshing a coin of the given denomination
- * with a certain amount left.
- *
- * If the amount left is zero, then the refresh cost
- * is also considered to be zero. If a refresh isn't possible (e.g. due to lack of
- * the right denominations), then the cost is the full amount left.
- *
- * Considers refresh fees, withdrawal fees after refresh and amounts too small
- * to refresh.
- */
-export function getTotalRefreshCost(
- denoms: DenominationRecord[],
- refreshedDenom: DenominationRecord,
- amountLeft: AmountJson,
-): AmountJson {
- const withdrawAmount = Amounts.sub(amountLeft, refreshedDenom.feeRefresh)
- .amount;
- const withdrawDenoms = getWithdrawDenomList(withdrawAmount, denoms);
- const resultingAmount = Amounts.add(
- Amounts.getZero(withdrawAmount.currency),
- ...withdrawDenoms.selectedDenoms.map(
- (d) => Amounts.mult(d.denom.value, d.count).amount,
- ),
- ).amount;
- const totalCost = Amounts.sub(amountLeft, resultingAmount).amount;
- logger.trace(
- `total refresh cost for ${amountToPretty(amountLeft)} is ${amountToPretty(
- totalCost,
- )}`,
- );
- return totalCost;
-}
-
-/**
- * Create a refresh session inside a refresh group.
- */
-async function refreshCreateSession(
- ws: InternalWalletState,
- refreshGroupId: string,
- coinIndex: number,
-): Promise<void> {
- logger.trace(
- `creating refresh session for coin ${coinIndex} in refresh group ${refreshGroupId}`,
- );
- const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
- if (!refreshGroup) {
- return;
- }
- if (refreshGroup.finishedPerCoin[coinIndex]) {
- return;
- }
- const existingRefreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];
- if (existingRefreshSession) {
- return;
- }
- const oldCoinPub = refreshGroup.oldCoinPubs[coinIndex];
- const coin = await ws.db.get(Stores.coins, oldCoinPub);
- if (!coin) {
- throw Error("Can't refresh, coin not found");
- }
-
- const exchange = await updateExchangeFromUrl(ws, coin.exchangeBaseUrl);
- if (!exchange) {
- throw Error("db inconsistent: exchange of coin not found");
- }
-
- const oldDenom = await ws.db.get(Stores.denominations, [
- exchange.baseUrl,
- coin.denomPub,
- ]);
-
- if (!oldDenom) {
- throw Error("db inconsistent: denomination for coin not found");
- }
-
- const availableDenoms: DenominationRecord[] = await ws.db
- .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl)
- .toArray();
-
- const availableAmount = Amounts.sub(coin.currentAmount, oldDenom.feeRefresh)
- .amount;
-
- const newCoinDenoms = getWithdrawDenomList(availableAmount, availableDenoms);
-
- if (newCoinDenoms.selectedDenoms.length === 0) {
- logger.trace(
- `not refreshing, available amount ${amountToPretty(
- availableAmount,
- )} too small`,
- );
- await ws.db.runWithWriteTransaction(
- [Stores.coins, Stores.refreshGroups],
- async (tx) => {
- const rg = await tx.get(Stores.refreshGroups, refreshGroupId);
- if (!rg) {
- return;
- }
- rg.finishedPerCoin[coinIndex] = true;
- let allDone = true;
- for (const f of rg.finishedPerCoin) {
- if (!f) {
- allDone = false;
- break;
- }
- }
- if (allDone) {
- rg.timestampFinished = getTimestampNow();
- rg.retryInfo = initRetryInfo(false);
- }
- await tx.put(Stores.refreshGroups, rg);
- },
- );
- ws.notify({ type: NotificationType.RefreshUnwarranted });
- return;
- }
-
- const refreshSession: RefreshSessionRecord = await ws.cryptoApi.createRefreshSession(
- exchange.baseUrl,
- 3,
- coin,
- newCoinDenoms,
- oldDenom.feeRefresh,
- );
-
- // Store refresh session and subtract refreshed amount from
- // coin in the same transaction.
- await ws.db.runWithWriteTransaction(
- [Stores.refreshGroups, Stores.coins],
- async (tx) => {
- const c = await tx.get(Stores.coins, coin.coinPub);
- if (!c) {
- throw Error("coin not found, but marked for refresh");
- }
- const r = Amounts.sub(c.currentAmount, refreshSession.amountRefreshInput);
- if (r.saturated) {
- console.log("can't refresh coin, no amount left");
- return;
- }
- c.currentAmount = r.amount;
- c.status = CoinStatus.Dormant;
- const rg = await tx.get(Stores.refreshGroups, refreshGroupId);
- if (!rg) {
- return;
- }
- if (rg.refreshSessionPerCoin[coinIndex]) {
- return;
- }
- rg.refreshSessionPerCoin[coinIndex] = refreshSession;
- await tx.put(Stores.refreshGroups, rg);
- await tx.put(Stores.coins, c);
- },
- );
- logger.info(
- `created refresh session for coin #${coinIndex} in ${refreshGroupId}`,
- );
- ws.notify({ type: NotificationType.RefreshStarted });
-}
-
-async function refreshMelt(
- ws: InternalWalletState,
- refreshGroupId: string,
- coinIndex: number,
-): Promise<void> {
- const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
- if (!refreshGroup) {
- return;
- }
- const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];
- if (!refreshSession) {
- return;
- }
- if (refreshSession.norevealIndex !== undefined) {
- return;
- }
-
- const coin = await ws.db.get(Stores.coins, refreshSession.meltCoinPub);
-
- if (!coin) {
- console.error("can't melt coin, it does not exist");
- return;
- }
-
- const reqUrl = new URL(
- `coins/${coin.coinPub}/melt`,
- refreshSession.exchangeBaseUrl,
- );
- const meltReq = {
- coin_pub: coin.coinPub,
- confirm_sig: refreshSession.confirmSig,
- denom_pub_hash: coin.denomPubHash,
- denom_sig: coin.denomSig,
- rc: refreshSession.hash,
- value_with_fee: Amounts.stringify(refreshSession.amountRefreshInput),
- };
- logger.trace(`melt request for coin:`, meltReq);
- const resp = await ws.http.postJson(reqUrl.href, meltReq);
- const meltResponse = await readSuccessResponseJsonOrThrow(
- resp,
- codecForExchangeMeltResponse(),
- );
-
- const norevealIndex = meltResponse.noreveal_index;
-
- refreshSession.norevealIndex = norevealIndex;
-
- await ws.db.mutate(Stores.refreshGroups, refreshGroupId, (rg) => {
- const rs = rg.refreshSessionPerCoin[coinIndex];
- if (!rs) {
- return;
- }
- if (rs.norevealIndex !== undefined) {
- return;
- }
- if (rs.finishedTimestamp) {
- return;
- }
- rs.norevealIndex = norevealIndex;
- return rg;
- });
-
- ws.notify({
- type: NotificationType.RefreshMelted,
- });
-}
-
-async function refreshReveal(
- ws: InternalWalletState,
- refreshGroupId: string,
- coinIndex: number,
-): Promise<void> {
- const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
- if (!refreshGroup) {
- return;
- }
- const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];
- if (!refreshSession) {
- return;
- }
- const norevealIndex = refreshSession.norevealIndex;
- if (norevealIndex === undefined) {
- throw Error("can't reveal without melting first");
- }
- const privs = Array.from(refreshSession.transferPrivs);
- privs.splice(norevealIndex, 1);
-
- const planchets = refreshSession.planchetsForGammas[norevealIndex];
- if (!planchets) {
- throw Error("refresh index error");
- }
-
- const meltCoinRecord = await ws.db.get(
- Stores.coins,
- refreshSession.meltCoinPub,
- );
- if (!meltCoinRecord) {
- throw Error("inconsistent database");
- }
-
- const evs = planchets.map((x: RefreshPlanchetRecord) => x.coinEv);
-
- const linkSigs: string[] = [];
- for (let i = 0; i < refreshSession.newDenoms.length; i++) {
- const linkSig = await ws.cryptoApi.signCoinLink(
- meltCoinRecord.coinPriv,
- refreshSession.newDenomHashes[i],
- refreshSession.meltCoinPub,
- refreshSession.transferPubs[norevealIndex],
- planchets[i].coinEv,
- );
- linkSigs.push(linkSig);
- }
-
- const req = {
- coin_evs: evs,
- new_denoms_h: refreshSession.newDenomHashes,
- rc: refreshSession.hash,
- transfer_privs: privs,
- transfer_pub: refreshSession.transferPubs[norevealIndex],
- link_sigs: linkSigs,
- };
-
- const reqUrl = new URL(
- `refreshes/${refreshSession.hash}/reveal`,
- refreshSession.exchangeBaseUrl,
- );
-
- const resp = await ws.http.postJson(reqUrl.href, req);
- const reveal = await readSuccessResponseJsonOrThrow(
- resp,
- codecForExchangeRevealResponse(),
- );
-
- const coins: CoinRecord[] = [];
-
- for (let i = 0; i < reveal.ev_sigs.length; i++) {
- const denom = await ws.db.get(Stores.denominations, [
- refreshSession.exchangeBaseUrl,
- refreshSession.newDenoms[i],
- ]);
- if (!denom) {
- console.error("denom not found");
- continue;
- }
- const pc = refreshSession.planchetsForGammas[norevealIndex][i];
- const denomSig = await ws.cryptoApi.rsaUnblind(
- reveal.ev_sigs[i].ev_sig,
- pc.blindingKey,
- denom.denomPub,
- );
- const coin: CoinRecord = {
- blindingKey: pc.blindingKey,
- coinPriv: pc.privateKey,
- coinPub: pc.publicKey,
- currentAmount: denom.value,
- denomPub: denom.denomPub,
- denomPubHash: denom.denomPubHash,
- denomSig,
- exchangeBaseUrl: refreshSession.exchangeBaseUrl,
- status: CoinStatus.Fresh,
- coinSource: {
- type: CoinSourceType.Refresh,
- oldCoinPub: refreshSession.meltCoinPub,
- },
- suspended: false,
- };
-
- coins.push(coin);
- }
-
- await ws.db.runWithWriteTransaction(
- [Stores.coins, Stores.refreshGroups],
- async (tx) => {
- const rg = await tx.get(Stores.refreshGroups, refreshGroupId);
- if (!rg) {
- console.log("no refresh session found");
- return;
- }
- const rs = rg.refreshSessionPerCoin[coinIndex];
- if (!rs) {
- return;
- }
- if (rs.finishedTimestamp) {
- console.log("refresh session already finished");
- return;
- }
- rs.finishedTimestamp = getTimestampNow();
- rg.finishedPerCoin[coinIndex] = true;
- let allDone = true;
- for (const f of rg.finishedPerCoin) {
- if (!f) {
- allDone = false;
- break;
- }
- }
- if (allDone) {
- rg.timestampFinished = getTimestampNow();
- rg.retryInfo = initRetryInfo(false);
- }
- for (const coin of coins) {
- await tx.put(Stores.coins, coin);
- }
- await tx.put(Stores.refreshGroups, rg);
- },
- );
- console.log("refresh finished (end of reveal)");
- ws.notify({
- type: NotificationType.RefreshRevealed,
- });
-}
-
-async function incrementRefreshRetry(
- ws: InternalWalletState,
- refreshGroupId: string,
- err: OperationErrorDetails | undefined,
-): Promise<void> {
- await ws.db.runWithWriteTransaction([Stores.refreshGroups], async (tx) => {
- const r = await tx.get(Stores.refreshGroups, refreshGroupId);
- if (!r) {
- return;
- }
- if (!r.retryInfo) {
- return;
- }
- r.retryInfo.retryCounter++;
- updateRetryInfoTimeout(r.retryInfo);
- r.lastError = err;
- await tx.put(Stores.refreshGroups, r);
- });
- if (err) {
- ws.notify({ type: NotificationType.RefreshOperationError, error: err });
- }
-}
-
-export async function processRefreshGroup(
- ws: InternalWalletState,
- refreshGroupId: string,
- forceNow = false,
-): Promise<void> {
- await ws.memoProcessRefresh.memo(refreshGroupId, async () => {
- const onOpErr = (e: OperationErrorDetails): Promise<void> =>
- incrementRefreshRetry(ws, refreshGroupId, e);
- return await guardOperationException(
- async () => await processRefreshGroupImpl(ws, refreshGroupId, forceNow),
- onOpErr,
- );
- });
-}
-
-async function resetRefreshGroupRetry(
- ws: InternalWalletState,
- refreshSessionId: string,
-): Promise<void> {
- await ws.db.mutate(Stores.refreshGroups, refreshSessionId, (x) => {
- if (x.retryInfo.active) {
- x.retryInfo = initRetryInfo();
- }
- return x;
- });
-}
-
-async function processRefreshGroupImpl(
- ws: InternalWalletState,
- refreshGroupId: string,
- forceNow: boolean,
-): Promise<void> {
- if (forceNow) {
- await resetRefreshGroupRetry(ws, refreshGroupId);
- }
- const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
- if (!refreshGroup) {
- return;
- }
- if (refreshGroup.timestampFinished) {
- return;
- }
- const ps = refreshGroup.oldCoinPubs.map((x, i) =>
- processRefreshSession(ws, refreshGroupId, i),
- );
- await Promise.all(ps);
- logger.trace("refresh finished");
-}
-
-async function processRefreshSession(
- ws: InternalWalletState,
- refreshGroupId: string,
- coinIndex: number,
-): Promise<void> {
- logger.trace(
- `processing refresh session for coin ${coinIndex} of group ${refreshGroupId}`,
- );
- let refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
- if (!refreshGroup) {
- return;
- }
- if (refreshGroup.finishedPerCoin[coinIndex]) {
- return;
- }
- if (!refreshGroup.refreshSessionPerCoin[coinIndex]) {
- await refreshCreateSession(ws, refreshGroupId, coinIndex);
- refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
- if (!refreshGroup) {
- return;
- }
- }
- const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];
- if (!refreshSession) {
- if (!refreshGroup.finishedPerCoin[coinIndex]) {
- throw Error(
- "BUG: refresh session was not created and coin not marked as finished",
- );
- }
- return;
- }
- if (refreshSession.norevealIndex === undefined) {
- await refreshMelt(ws, refreshGroupId, coinIndex);
- }
- await refreshReveal(ws, refreshGroupId, coinIndex);
-}
-
-/**
- * Create a refresh group for a list of coins.
- */
-export async function createRefreshGroup(
- ws: InternalWalletState,
- tx: TransactionHandle,
- oldCoinPubs: CoinPublicKey[],
- reason: RefreshReason,
-): Promise<RefreshGroupId> {
- const refreshGroupId = encodeCrock(getRandomBytes(32));
-
- const refreshGroup: RefreshGroupRecord = {
- timestampFinished: undefined,
- finishedPerCoin: oldCoinPubs.map((x) => false),
- lastError: undefined,
- lastErrorPerCoin: {},
- oldCoinPubs: oldCoinPubs.map((x) => x.coinPub),
- reason,
- refreshGroupId,
- refreshSessionPerCoin: oldCoinPubs.map((x) => undefined),
- retryInfo: initRetryInfo(),
- };
-
- await tx.put(Stores.refreshGroups, refreshGroup);
-
- const processAsync = async (): Promise<void> => {
- try {
- await processRefreshGroup(ws, refreshGroupId);
- } catch (e) {
- logger.trace(`Error during refresh: ${e}`)
- }
- };
-
- processAsync();
-
- return {
- refreshGroupId,
- };
-}