summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations/refresh.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/operations/refresh.ts')
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts987
1 files changed, 0 insertions, 987 deletions
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts
deleted file mode 100644
index 144514e1c..000000000
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ /dev/null
@@ -1,987 +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 { encodeCrock, getRandomBytes } from "@gnu-taler/taler-util";
-import {
- CoinRecord,
- CoinSourceType,
- CoinStatus,
- DenominationRecord,
- RefreshCoinStatus,
- RefreshGroupRecord,
- WalletStoresV1,
-} from "../db.js";
-import {
- codecForExchangeMeltResponse,
- codecForExchangeRevealResponse,
- CoinPublicKey,
- fnutil,
- NotificationType,
- RefreshGroupId,
- RefreshPlanchetInfo,
- RefreshReason,
- stringifyTimestamp,
- TalerErrorDetails,
- timestampToIsoString,
-} from "@gnu-taler/taler-util";
-import { AmountJson, Amounts } from "@gnu-taler/taler-util";
-import { amountToPretty } from "@gnu-taler/taler-util";
-import {
- HttpResponseStatus,
- readSuccessResponseJsonOrThrow,
- readUnexpectedResponseDetails,
-} from "../util/http.js";
-import { checkDbInvariant } from "../util/invariants.js";
-import { Logger } from "@gnu-taler/taler-util";
-import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js";
-import {
- Duration,
- durationFromSpec,
- durationMul,
- getTimestampNow,
- isTimestampExpired,
- Timestamp,
- timestampAddDuration,
- timestampDifference,
- timestampMin,
- URL,
-} from "@gnu-taler/taler-util";
-import { guardOperationException } from "../errors.js";
-import { updateExchangeFromUrl } from "./exchanges.js";
-import { EXCHANGE_COINS_LOCK, InternalWalletState } from "../common.js";
-import {
- isWithdrawableDenom,
- selectWithdrawalDenominations,
-} from "./withdraw.js";
-import { RefreshNewDenomInfo } from "../crypto/cryptoTypes.js";
-import { GetReadWriteAccess } from "../util/query.js";
-
-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 = selectWithdrawalDenominations(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;
-}
-
-function updateGroupStatus(rg: RefreshGroupRecord): void {
- let allDone = fnutil.all(
- rg.statusPerCoin,
- (x) => x === RefreshCoinStatus.Finished || x === RefreshCoinStatus.Frozen,
- );
- let anyFrozen = fnutil.any(
- rg.statusPerCoin,
- (x) => x === RefreshCoinStatus.Frozen,
- );
- if (allDone) {
- if (anyFrozen) {
- rg.frozen = true;
- rg.retryInfo = initRetryInfo();
- } else {
- rg.timestampFinished = getTimestampNow();
- rg.retryInfo = initRetryInfo();
- }
- }
-}
-
-/**
- * Create a refresh session for one particular coin 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 d = await ws.db
- .mktx((x) => ({
- refreshGroups: x.refreshGroups,
- coins: x.coins,
- }))
- .runReadWrite(async (tx) => {
- const refreshGroup = await tx.refreshGroups.get(refreshGroupId);
- if (!refreshGroup) {
- return;
- }
- if (
- refreshGroup.statusPerCoin[coinIndex] === RefreshCoinStatus.Finished
- ) {
- return;
- }
- const existingRefreshSession =
- refreshGroup.refreshSessionPerCoin[coinIndex];
- if (existingRefreshSession) {
- return;
- }
- const oldCoinPub = refreshGroup.oldCoinPubs[coinIndex];
- const coin = await tx.coins.get(oldCoinPub);
- if (!coin) {
- throw Error("Can't refresh, coin not found");
- }
- return { refreshGroup, coin };
- });
-
- if (!d) {
- return;
- }
-
- const { refreshGroup, coin } = d;
-
- const { exchange } = await updateExchangeFromUrl(ws, coin.exchangeBaseUrl);
- if (!exchange) {
- throw Error("db inconsistent: exchange of coin not found");
- }
-
- // FIXME: use helper functions from withdraw.ts
- // to update and filter withdrawable denoms.
-
- const { availableAmount, availableDenoms } = await ws.db
- .mktx((x) => ({
- denominations: x.denominations,
- }))
- .runReadOnly(async (tx) => {
- const oldDenom = await tx.denominations.get([
- exchange.baseUrl,
- coin.denomPubHash,
- ]);
-
- if (!oldDenom) {
- throw Error("db inconsistent: denomination for coin not found");
- }
-
- // FIXME: use an index here, based on the withdrawal expiration time.
- const availableDenoms: DenominationRecord[] = await tx.denominations.indexes.byExchangeBaseUrl
- .iter(exchange.baseUrl)
- .toArray();
-
- const availableAmount = Amounts.sub(
- refreshGroup.inputPerCoin[coinIndex],
- oldDenom.feeRefresh,
- ).amount;
- return { availableAmount, availableDenoms };
- });
-
- const newCoinDenoms = selectWithdrawalDenominations(
- availableAmount,
- availableDenoms,
- );
-
- if (logger.shouldLogTrace()) {
- logger.trace(`printing selected denominations for refresh`);
- logger.trace(`current time: ${stringifyTimestamp(getTimestampNow())}`);
- for (const denom of newCoinDenoms.selectedDenoms) {
- logger.trace(`denom ${denom.denom}, count ${denom.count}`);
- logger.trace(
- `withdrawal expiration ${stringifyTimestamp(
- denom.denom.stampExpireWithdraw,
- )}`,
- );
- }
- }
-
- if (newCoinDenoms.selectedDenoms.length === 0) {
- logger.trace(
- `not refreshing, available amount ${amountToPretty(
- availableAmount,
- )} too small`,
- );
- await ws.db
- .mktx((x) => ({
- coins: x.coins,
- refreshGroups: x.refreshGroups,
- }))
- .runReadWrite(async (tx) => {
- const rg = await tx.refreshGroups.get(refreshGroupId);
- if (!rg) {
- return;
- }
- rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Finished;
- updateGroupStatus(rg);
-
- await tx.refreshGroups.put(rg);
- });
- ws.notify({ type: NotificationType.RefreshUnwarranted });
- return;
- }
-
- const sessionSecretSeed = encodeCrock(getRandomBytes(64));
-
- // Store refresh session for this coin in the database.
- await ws.db
- .mktx((x) => ({
- refreshGroups: x.refreshGroups,
- coins: x.coins,
- }))
- .runReadWrite(async (tx) => {
- const rg = await tx.refreshGroups.get(refreshGroupId);
- if (!rg) {
- return;
- }
- if (rg.refreshSessionPerCoin[coinIndex]) {
- return;
- }
- rg.refreshSessionPerCoin[coinIndex] = {
- norevealIndex: undefined,
- sessionSecretSeed: sessionSecretSeed,
- newDenoms: newCoinDenoms.selectedDenoms.map((x) => ({
- count: x.count,
- denomPubHash: x.denom.denomPubHash,
- })),
- amountRefreshOutput: newCoinDenoms.totalCoinValue,
- };
- await tx.refreshGroups.put(rg);
- });
- logger.info(
- `created refresh session for coin #${coinIndex} in ${refreshGroupId}`,
- );
- ws.notify({ type: NotificationType.RefreshStarted });
-}
-
-function getRefreshRequestTimeout(rg: RefreshGroupRecord): Duration {
- return { d_ms: 5000 };
-}
-
-async function refreshMelt(
- ws: InternalWalletState,
- refreshGroupId: string,
- coinIndex: number,
-): Promise<void> {
- const d = await ws.db
- .mktx((x) => ({
- refreshGroups: x.refreshGroups,
- coins: x.coins,
- denominations: x.denominations,
- }))
- .runReadWrite(async (tx) => {
- const refreshGroup = await tx.refreshGroups.get(refreshGroupId);
- if (!refreshGroup) {
- return;
- }
- const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];
- if (!refreshSession) {
- return;
- }
- if (refreshSession.norevealIndex !== undefined) {
- return;
- }
-
- const oldCoin = await tx.coins.get(refreshGroup.oldCoinPubs[coinIndex]);
- checkDbInvariant(!!oldCoin, "melt coin doesn't exist");
- const oldDenom = await tx.denominations.get([
- oldCoin.exchangeBaseUrl,
- oldCoin.denomPubHash,
- ]);
- checkDbInvariant(
- !!oldDenom,
- "denomination for melted coin doesn't exist",
- );
-
- const newCoinDenoms: RefreshNewDenomInfo[] = [];
-
- for (const dh of refreshSession.newDenoms) {
- const newDenom = await tx.denominations.get([
- oldCoin.exchangeBaseUrl,
- dh.denomPubHash,
- ]);
- checkDbInvariant(
- !!newDenom,
- "new denomination for refresh not in database",
- );
- newCoinDenoms.push({
- count: dh.count,
- denomPub: newDenom.denomPub,
- feeWithdraw: newDenom.feeWithdraw,
- value: newDenom.value,
- });
- }
- return { newCoinDenoms, oldCoin, oldDenom, refreshGroup, refreshSession };
- });
-
- if (!d) {
- return;
- }
-
- const { newCoinDenoms, oldCoin, oldDenom, refreshGroup, refreshSession } = d;
-
- const derived = await ws.cryptoApi.deriveRefreshSession({
- kappa: 3,
- meltCoinDenomPubHash: oldCoin.denomPubHash,
- meltCoinPriv: oldCoin.coinPriv,
- meltCoinPub: oldCoin.coinPub,
- feeRefresh: oldDenom.feeRefresh,
- newCoinDenoms,
- sessionSecretSeed: refreshSession.sessionSecretSeed,
- });
-
- const reqUrl = new URL(
- `coins/${oldCoin.coinPub}/melt`,
- oldCoin.exchangeBaseUrl,
- );
- const meltReq = {
- coin_pub: oldCoin.coinPub,
- confirm_sig: derived.confirmSig,
- denom_pub_hash: oldCoin.denomPubHash,
- denom_sig: oldCoin.denomSig,
- rc: derived.hash,
- value_with_fee: Amounts.stringify(derived.meltValueWithFee),
- };
- logger.trace(`melt request for coin:`, meltReq);
-
- const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], async () => {
- return await ws.http.postJson(reqUrl.href, meltReq, {
- timeout: getRefreshRequestTimeout(refreshGroup),
- });
- });
-
- if (resp.status === HttpResponseStatus.NotFound) {
- const errDetails = await readUnexpectedResponseDetails(resp);
- await ws.db
- .mktx((x) => ({
- refreshGroups: x.refreshGroups,
- }))
- .runReadWrite(async (tx) => {
- const rg = await tx.refreshGroups.get(refreshGroupId);
- if (!rg) {
- return;
- }
- if (rg.timestampFinished) {
- return;
- }
- if (rg.statusPerCoin[coinIndex] !== RefreshCoinStatus.Pending) {
- return;
- }
- rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Frozen;
- rg.lastErrorPerCoin[coinIndex] = errDetails;
- updateGroupStatus(rg);
- await tx.refreshGroups.put(rg);
- });
- return;
- }
-
- const meltResponse = await readSuccessResponseJsonOrThrow(
- resp,
- codecForExchangeMeltResponse(),
- );
-
- const norevealIndex = meltResponse.noreveal_index;
-
- refreshSession.norevealIndex = norevealIndex;
-
- await ws.db
- .mktx((x) => ({
- refreshGroups: x.refreshGroups,
- }))
- .runReadWrite(async (tx) => {
- const rg = await tx.refreshGroups.get(refreshGroupId);
- if (!rg) {
- return;
- }
- if (rg.timestampFinished) {
- return;
- }
- const rs = rg.refreshSessionPerCoin[coinIndex];
- if (!rs) {
- return;
- }
- if (rs.norevealIndex !== undefined) {
- return;
- }
- rs.norevealIndex = norevealIndex;
- await tx.refreshGroups.put(rg);
- });
-
- ws.notify({
- type: NotificationType.RefreshMelted,
- });
-}
-
-async function refreshReveal(
- ws: InternalWalletState,
- refreshGroupId: string,
- coinIndex: number,
-): Promise<void> {
- const d = await ws.db
- .mktx((x) => ({
- refreshGroups: x.refreshGroups,
- coins: x.coins,
- denominations: x.denominations,
- }))
- .runReadOnly(async (tx) => {
- const refreshGroup = await tx.refreshGroups.get(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 oldCoin = await tx.coins.get(refreshGroup.oldCoinPubs[coinIndex]);
- checkDbInvariant(!!oldCoin, "melt coin doesn't exist");
- const oldDenom = await tx.denominations.get([
- oldCoin.exchangeBaseUrl,
- oldCoin.denomPubHash,
- ]);
- checkDbInvariant(
- !!oldDenom,
- "denomination for melted coin doesn't exist",
- );
-
- const newCoinDenoms: RefreshNewDenomInfo[] = [];
-
- for (const dh of refreshSession.newDenoms) {
- const newDenom = await tx.denominations.get([
- oldCoin.exchangeBaseUrl,
- dh.denomPubHash,
- ]);
- checkDbInvariant(
- !!newDenom,
- "new denomination for refresh not in database",
- );
- newCoinDenoms.push({
- count: dh.count,
- denomPub: newDenom.denomPub,
- feeWithdraw: newDenom.feeWithdraw,
- value: newDenom.value,
- });
- }
- return {
- oldCoin,
- oldDenom,
- newCoinDenoms,
- refreshSession,
- refreshGroup,
- norevealIndex,
- };
- });
-
- if (!d) {
- return;
- }
-
- const {
- oldCoin,
- oldDenom,
- newCoinDenoms,
- refreshSession,
- refreshGroup,
- norevealIndex,
- } = d;
-
- const derived = await ws.cryptoApi.deriveRefreshSession({
- kappa: 3,
- meltCoinDenomPubHash: oldCoin.denomPubHash,
- meltCoinPriv: oldCoin.coinPriv,
- meltCoinPub: oldCoin.coinPub,
- feeRefresh: oldDenom.feeRefresh,
- newCoinDenoms,
- sessionSecretSeed: refreshSession.sessionSecretSeed,
- });
-
- const privs = Array.from(derived.transferPrivs);
- privs.splice(norevealIndex, 1);
-
- const planchets = derived.planchetsForGammas[norevealIndex];
- if (!planchets) {
- throw Error("refresh index error");
- }
-
- const evs = planchets.map((x: RefreshPlanchetInfo) => x.coinEv);
- const newDenomsFlat: string[] = [];
- const linkSigs: string[] = [];
-
- for (let i = 0; i < refreshSession.newDenoms.length; i++) {
- const dsel = refreshSession.newDenoms[i];
- for (let j = 0; j < dsel.count; j++) {
- const newCoinIndex = linkSigs.length;
- const linkSig = await ws.cryptoApi.signCoinLink(
- oldCoin.coinPriv,
- dsel.denomPubHash,
- oldCoin.coinPub,
- derived.transferPubs[norevealIndex],
- planchets[newCoinIndex].coinEv,
- );
- linkSigs.push(linkSig);
- newDenomsFlat.push(dsel.denomPubHash);
- }
- }
-
- const req = {
- coin_evs: evs,
- new_denoms_h: newDenomsFlat,
- rc: derived.hash,
- transfer_privs: privs,
- transfer_pub: derived.transferPubs[norevealIndex],
- link_sigs: linkSigs,
- };
-
- const reqUrl = new URL(
- `refreshes/${derived.hash}/reveal`,
- oldCoin.exchangeBaseUrl,
- );
-
- const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], async () => {
- return await ws.http.postJson(reqUrl.href, req, {
- timeout: getRefreshRequestTimeout(refreshGroup),
- });
- });
-
- const reveal = await readSuccessResponseJsonOrThrow(
- resp,
- codecForExchangeRevealResponse(),
- );
-
- const coins: CoinRecord[] = [];
-
- for (let i = 0; i < refreshSession.newDenoms.length; i++) {
- for (let j = 0; j < refreshSession.newDenoms[i].count; j++) {
- const newCoinIndex = coins.length;
- // FIXME: Look up in earlier transaction!
- const denom = await ws.db
- .mktx((x) => ({
- denominations: x.denominations,
- }))
- .runReadOnly(async (tx) => {
- return tx.denominations.get([
- oldCoin.exchangeBaseUrl,
- refreshSession.newDenoms[i].denomPubHash,
- ]);
- });
- if (!denom) {
- console.error("denom not found");
- continue;
- }
- const pc = derived.planchetsForGammas[norevealIndex][newCoinIndex];
- const denomSig = await ws.cryptoApi.rsaUnblind(
- reveal.ev_sigs[newCoinIndex].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: oldCoin.exchangeBaseUrl,
- status: CoinStatus.Fresh,
- coinSource: {
- type: CoinSourceType.Refresh,
- oldCoinPub: refreshGroup.oldCoinPubs[coinIndex],
- },
- suspended: false,
- coinEvHash: pc.coinEv,
- };
-
- coins.push(coin);
- }
- }
-
- await ws.db
- .mktx((x) => ({
- coins: x.coins,
- refreshGroups: x.refreshGroups,
- }))
- .runReadWrite(async (tx) => {
- const rg = await tx.refreshGroups.get(refreshGroupId);
- if (!rg) {
- logger.warn("no refresh session found");
- return;
- }
- const rs = rg.refreshSessionPerCoin[coinIndex];
- if (!rs) {
- return;
- }
- rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Finished;
- updateGroupStatus(rg);
- for (const coin of coins) {
- await tx.coins.put(coin);
- }
- await tx.refreshGroups.put(rg);
- });
- logger.trace("refresh finished (end of reveal)");
- ws.notify({
- type: NotificationType.RefreshRevealed,
- });
-}
-
-async function incrementRefreshRetry(
- ws: InternalWalletState,
- refreshGroupId: string,
- err: TalerErrorDetails | undefined,
-): Promise<void> {
- await ws.db
- .mktx((x) => ({
- refreshGroups: x.refreshGroups,
- }))
- .runReadWrite(async (tx) => {
- const r = await tx.refreshGroups.get(refreshGroupId);
- if (!r) {
- return;
- }
- if (!r.retryInfo) {
- return;
- }
- r.retryInfo.retryCounter++;
- updateRetryInfoTimeout(r.retryInfo);
- r.lastError = err;
- await tx.refreshGroups.put(r);
- });
- if (err) {
- ws.notify({ type: NotificationType.RefreshOperationError, error: err });
- }
-}
-
-/**
- * Actually process a refresh group that has been created.
- */
-export async function processRefreshGroup(
- ws: InternalWalletState,
- refreshGroupId: string,
- forceNow = false,
-): Promise<void> {
- await ws.memoProcessRefresh.memo(refreshGroupId, async () => {
- const onOpErr = (e: TalerErrorDetails): Promise<void> =>
- incrementRefreshRetry(ws, refreshGroupId, e);
- return await guardOperationException(
- async () => await processRefreshGroupImpl(ws, refreshGroupId, forceNow),
- onOpErr,
- );
- });
-}
-
-async function resetRefreshGroupRetry(
- ws: InternalWalletState,
- refreshGroupId: string,
-): Promise<void> {
- await ws.db
- .mktx((x) => ({
- refreshGroups: x.refreshGroups,
- }))
- .runReadWrite(async (tx) => {
- const x = await tx.refreshGroups.get(refreshGroupId);
- if (x) {
- x.retryInfo = initRetryInfo();
- await tx.refreshGroups.put(x);
- }
- });
-}
-
-async function processRefreshGroupImpl(
- ws: InternalWalletState,
- refreshGroupId: string,
- forceNow: boolean,
-): Promise<void> {
- if (forceNow) {
- await resetRefreshGroupRetry(ws, refreshGroupId);
- }
- const refreshGroup = await ws.db
- .mktx((x) => ({
- refreshGroups: x.refreshGroups,
- }))
- .runReadOnly(async (tx) => {
- return tx.refreshGroups.get(refreshGroupId);
- });
- if (!refreshGroup) {
- return;
- }
- if (refreshGroup.timestampFinished) {
- return;
- }
- // Process refresh sessions of the group in parallel.
- 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
- .mktx((x) => ({ refreshGroups: x.refreshGroups }))
- .runReadOnly(async (tx) => {
- return tx.refreshGroups.get(refreshGroupId);
- });
- if (!refreshGroup) {
- return;
- }
- if (refreshGroup.statusPerCoin[coinIndex] === RefreshCoinStatus.Finished) {
- return;
- }
- if (!refreshGroup.refreshSessionPerCoin[coinIndex]) {
- await refreshCreateSession(ws, refreshGroupId, coinIndex);
- refreshGroup = await ws.db
- .mktx((x) => ({ refreshGroups: x.refreshGroups }))
- .runReadOnly(async (tx) => {
- return tx.refreshGroups.get(refreshGroupId);
- });
- if (!refreshGroup) {
- return;
- }
- }
- const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];
- if (!refreshSession) {
- if (refreshGroup.statusPerCoin[coinIndex] !== RefreshCoinStatus.Finished) {
- 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.
- *
- * Refreshes the remaining amount on the coin, effectively capturing the remaining
- * value in the refresh group.
- *
- * The caller must ensure that
- * the remaining amount was updated correctly before the coin was deposited or
- * credited.
- *
- * The caller must also ensure that the coins that should be refreshed exist
- * in the current database transaction.
- */
-export async function createRefreshGroup(
- ws: InternalWalletState,
- tx: GetReadWriteAccess<{
- denominations: typeof WalletStoresV1.denominations;
- coins: typeof WalletStoresV1.coins;
- refreshGroups: typeof WalletStoresV1.refreshGroups;
- }>,
- oldCoinPubs: CoinPublicKey[],
- reason: RefreshReason,
-): Promise<RefreshGroupId> {
- const refreshGroupId = encodeCrock(getRandomBytes(32));
-
- const inputPerCoin: AmountJson[] = [];
- const estimatedOutputPerCoin: AmountJson[] = [];
-
- const denomsPerExchange: Record<string, DenominationRecord[]> = {};
-
- const getDenoms = async (
- exchangeBaseUrl: string,
- ): Promise<DenominationRecord[]> => {
- if (denomsPerExchange[exchangeBaseUrl]) {
- return denomsPerExchange[exchangeBaseUrl];
- }
- const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
- .iter(exchangeBaseUrl)
- .filter((x) => {
- return isWithdrawableDenom(x);
- });
- denomsPerExchange[exchangeBaseUrl] = allDenoms;
- return allDenoms;
- };
-
- for (const ocp of oldCoinPubs) {
- const coin = await tx.coins.get(ocp.coinPub);
- checkDbInvariant(!!coin, "coin must be in database");
- const denom = await tx.denominations.get([
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- ]);
- checkDbInvariant(
- !!denom,
- "denomination for existing coin must be in database",
- );
- const refreshAmount = coin.currentAmount;
- inputPerCoin.push(refreshAmount);
- coin.currentAmount = Amounts.getZero(refreshAmount.currency);
- coin.status = CoinStatus.Dormant;
- await tx.coins.put(coin);
- const denoms = await getDenoms(coin.exchangeBaseUrl);
- const cost = getTotalRefreshCost(denoms, denom, refreshAmount);
- const output = Amounts.sub(refreshAmount, cost).amount;
- estimatedOutputPerCoin.push(output);
- }
-
- const refreshGroup: RefreshGroupRecord = {
- timestampFinished: undefined,
- statusPerCoin: oldCoinPubs.map(() => RefreshCoinStatus.Pending),
- lastError: undefined,
- lastErrorPerCoin: {},
- oldCoinPubs: oldCoinPubs.map((x) => x.coinPub),
- reason,
- refreshGroupId,
- refreshSessionPerCoin: oldCoinPubs.map(() => undefined),
- retryInfo: initRetryInfo(),
- inputPerCoin,
- estimatedOutputPerCoin,
- timestampCreated: getTimestampNow(),
- };
-
- if (oldCoinPubs.length == 0) {
- logger.warn("created refresh group with zero coins");
- refreshGroup.timestampFinished = getTimestampNow();
- }
-
- await tx.refreshGroups.put(refreshGroup);
-
- logger.trace(`created refresh group ${refreshGroupId}`);
-
- processRefreshGroup(ws, refreshGroupId).catch((e) => {
- logger.warn(`processing refresh group ${refreshGroupId} failed`);
- });
-
- return {
- refreshGroupId,
- };
-}
-
-/**
- * Timestamp after which the wallet would do the next check for an auto-refresh.
- */
-function getAutoRefreshCheckThreshold(d: DenominationRecord): Timestamp {
- const delta = timestampDifference(
- d.stampExpireWithdraw,
- d.stampExpireDeposit,
- );
- const deltaDiv = durationMul(delta, 0.75);
- return timestampAddDuration(d.stampExpireWithdraw, deltaDiv);
-}
-
-/**
- * Timestamp after which the wallet would do an auto-refresh.
- */
-function getAutoRefreshExecuteThreshold(d: DenominationRecord): Timestamp {
- const delta = timestampDifference(
- d.stampExpireWithdraw,
- d.stampExpireDeposit,
- );
- const deltaDiv = durationMul(delta, 0.5);
- return timestampAddDuration(d.stampExpireWithdraw, deltaDiv);
-}
-
-export async function autoRefresh(
- ws: InternalWalletState,
- exchangeBaseUrl: string,
-): Promise<void> {
- logger.info(`doing auto-refresh check for '${exchangeBaseUrl}'`);
- await updateExchangeFromUrl(ws, exchangeBaseUrl, undefined, true);
- let minCheckThreshold = timestampAddDuration(
- getTimestampNow(),
- durationFromSpec({ days: 1 }),
- );
- await ws.db
- .mktx((x) => ({
- coins: x.coins,
- denominations: x.denominations,
- refreshGroups: x.refreshGroups,
- exchanges: x.exchanges,
- }))
- .runReadWrite(async (tx) => {
- const exchange = await tx.exchanges.get(exchangeBaseUrl);
- if (!exchange) {
- return;
- }
- const coins = await tx.coins.indexes.byBaseUrl
- .iter(exchangeBaseUrl)
- .toArray();
- const refreshCoins: CoinPublicKey[] = [];
- for (const coin of coins) {
- if (coin.status !== CoinStatus.Fresh) {
- continue;
- }
- if (coin.suspended) {
- continue;
- }
- const denom = await tx.denominations.get([
- exchangeBaseUrl,
- coin.denomPubHash,
- ]);
- if (!denom) {
- logger.warn("denomination not in database");
- continue;
- }
- const executeThreshold = getAutoRefreshExecuteThreshold(denom);
- if (isTimestampExpired(executeThreshold)) {
- refreshCoins.push(coin);
- } else {
- const checkThreshold = getAutoRefreshCheckThreshold(denom);
- minCheckThreshold = timestampMin(minCheckThreshold, checkThreshold);
- }
- }
- if (refreshCoins.length > 0) {
- const res = await createRefreshGroup(
- ws,
- tx,
- refreshCoins,
- RefreshReason.Scheduled,
- );
- logger.info(
- `created refresh group for auto-refresh (${res.refreshGroupId})`,
- );
- }
- logger.info(
- `current wallet time: ${timestampToIsoString(getTimestampNow())}`,
- );
- logger.info(
- `next refresh check at ${timestampToIsoString(minCheckThreshold)}`,
- );
- exchange.nextRefreshCheck = minCheckThreshold;
- await tx.exchanges.put(exchange);
- });
-}