2019-12-02 00:42:40 +01:00
|
|
|
/*
|
|
|
|
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 { AmountJson } from "../util/amounts";
|
|
|
|
import * as Amounts from "../util/amounts";
|
|
|
|
import {
|
|
|
|
DenominationRecord,
|
|
|
|
Stores,
|
|
|
|
CoinStatus,
|
|
|
|
RefreshPlanchetRecord,
|
|
|
|
CoinRecord,
|
|
|
|
RefreshSessionRecord,
|
2019-12-05 19:38:19 +01:00
|
|
|
initRetryInfo,
|
|
|
|
updateRetryInfoTimeout,
|
2019-12-15 16:59:00 +01:00
|
|
|
RefreshGroupRecord,
|
2020-03-11 20:14:28 +01:00
|
|
|
CoinSourceType,
|
2019-12-12 20:53:15 +01:00
|
|
|
} from "../types/dbTypes";
|
2019-12-02 00:42:40 +01:00
|
|
|
import { amountToPretty } from "../util/helpers";
|
2019-12-15 16:59:00 +01:00
|
|
|
import { Database, TransactionHandle } from "../util/query";
|
2019-12-02 00:42:40 +01:00
|
|
|
import { InternalWalletState } from "./state";
|
|
|
|
import { Logger } from "../util/logging";
|
|
|
|
import { getWithdrawDenomList } from "./withdraw";
|
|
|
|
import { updateExchangeFromUrl } from "./exchanges";
|
2019-12-15 16:59:00 +01:00
|
|
|
import {
|
|
|
|
OperationError,
|
|
|
|
CoinPublicKey,
|
|
|
|
RefreshReason,
|
|
|
|
RefreshGroupId,
|
|
|
|
} from "../types/walletTypes";
|
2019-12-05 19:38:19 +01:00
|
|
|
import { guardOperationException } from "./errors";
|
2019-12-12 20:53:15 +01:00
|
|
|
import { NotificationType } from "../types/notifications";
|
2019-12-15 16:59:00 +01:00
|
|
|
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
|
2019-12-19 20:42:49 +01:00
|
|
|
import { getTimestampNow } from "../util/time";
|
2019-12-02 00:42:40 +01:00
|
|
|
|
|
|
|
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),
|
2020-03-30 12:39:32 +02:00
|
|
|
...withdrawDenoms.map((d) => d.value),
|
2019-12-02 00:42:40 +01:00
|
|
|
).amount;
|
|
|
|
const totalCost = Amounts.sub(amountLeft, resultingAmount).amount;
|
|
|
|
logger.trace(
|
|
|
|
"total refresh cost for",
|
|
|
|
amountToPretty(amountLeft),
|
|
|
|
"is",
|
|
|
|
amountToPretty(totalCost),
|
|
|
|
);
|
|
|
|
return totalCost;
|
|
|
|
}
|
|
|
|
|
2019-12-15 16:59:00 +01:00
|
|
|
/**
|
|
|
|
* 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.length === 0) {
|
|
|
|
logger.trace(
|
|
|
|
`not refreshing, available amount ${amountToPretty(
|
|
|
|
availableAmount,
|
|
|
|
)} too small`,
|
|
|
|
);
|
|
|
|
await ws.db.runWithWriteTransaction(
|
|
|
|
[Stores.coins, Stores.refreshGroups],
|
2020-03-30 12:39:32 +02:00
|
|
|
async (tx) => {
|
2019-12-15 16:59:00 +01:00
|
|
|
const rg = await tx.get(Stores.refreshGroups, refreshGroupId);
|
|
|
|
if (!rg) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
rg.finishedPerCoin[coinIndex] = true;
|
2019-12-16 21:10:57 +01:00
|
|
|
let allDone = true;
|
|
|
|
for (const f of rg.finishedPerCoin) {
|
|
|
|
if (!f) {
|
|
|
|
allDone = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (allDone) {
|
|
|
|
rg.timestampFinished = getTimestampNow();
|
|
|
|
rg.retryInfo = initRetryInfo(false);
|
|
|
|
}
|
2019-12-15 16:59:00 +01:00
|
|
|
await tx.put(Stores.refreshGroups, rg);
|
|
|
|
},
|
|
|
|
);
|
2019-12-16 21:10:57 +01:00
|
|
|
ws.notify({ type: NotificationType.RefreshUnwarranted });
|
2019-12-15 16:59:00 +01:00
|
|
|
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],
|
2020-03-30 12:39:32 +02:00
|
|
|
async (tx) => {
|
2019-12-15 16:59:00 +01:00
|
|
|
const c = await tx.get(Stores.coins, coin.coinPub);
|
|
|
|
if (!c) {
|
|
|
|
throw Error("coin not found, but marked for refresh");
|
|
|
|
}
|
2019-12-16 16:20:45 +01:00
|
|
|
const r = Amounts.sub(c.currentAmount, refreshSession.amountRefreshInput);
|
2019-12-15 16:59:00 +01:00
|
|
|
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 });
|
|
|
|
}
|
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
async function refreshMelt(
|
|
|
|
ws: InternalWalletState,
|
2019-12-15 16:59:00 +01:00
|
|
|
refreshGroupId: string,
|
|
|
|
coinIndex: number,
|
2019-12-02 00:42:40 +01:00
|
|
|
): Promise<void> {
|
2019-12-15 16:59:00 +01:00
|
|
|
const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
|
|
|
|
if (!refreshGroup) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];
|
2019-12-02 00:42:40 +01:00
|
|
|
if (!refreshSession) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (refreshSession.norevealIndex !== undefined) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-12-12 22:39:45 +01:00
|
|
|
const coin = await ws.db.get(Stores.coins, refreshSession.meltCoinPub);
|
2019-12-02 00:42:40 +01:00
|
|
|
|
|
|
|
if (!coin) {
|
|
|
|
console.error("can't melt coin, it does not exist");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-03-09 12:07:46 +01:00
|
|
|
const reqUrl = new URL(
|
|
|
|
`coins/${coin.coinPub}/melt`,
|
|
|
|
refreshSession.exchangeBaseUrl,
|
|
|
|
);
|
2019-12-02 00:42:40 +01:00
|
|
|
const meltReq = {
|
|
|
|
coin_pub: coin.coinPub,
|
|
|
|
confirm_sig: refreshSession.confirmSig,
|
|
|
|
denom_pub_hash: coin.denomPubHash,
|
|
|
|
denom_sig: coin.denomSig,
|
|
|
|
rc: refreshSession.hash,
|
2020-01-17 22:25:33 +01:00
|
|
|
value_with_fee: Amounts.toString(refreshSession.amountRefreshInput),
|
2019-12-02 00:42:40 +01:00
|
|
|
};
|
2020-01-19 20:41:51 +01:00
|
|
|
logger.trace(`melt request for coin:`, meltReq);
|
2019-12-02 00:42:40 +01:00
|
|
|
const resp = await ws.http.postJson(reqUrl.href, meltReq);
|
2019-12-09 13:29:11 +01:00
|
|
|
if (resp.status !== 200) {
|
2020-01-17 22:25:33 +01:00
|
|
|
console.log(`got status ${resp.status} for refresh/melt`);
|
|
|
|
try {
|
|
|
|
const respJson = await resp.json();
|
2020-03-09 12:07:46 +01:00
|
|
|
console.log(
|
|
|
|
`body of refresh/melt error response:`,
|
|
|
|
JSON.stringify(respJson, undefined, 2),
|
|
|
|
);
|
2020-01-17 22:25:33 +01:00
|
|
|
} catch (e) {
|
|
|
|
console.log(`body of refresh/melt error response is not JSON`);
|
|
|
|
}
|
2019-12-09 13:29:11 +01:00
|
|
|
throw Error(`unexpected status code ${resp.status} for refresh/melt`);
|
|
|
|
}
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2019-12-09 13:29:11 +01:00
|
|
|
const respJson = await resp.json();
|
|
|
|
|
|
|
|
logger.trace("melt response:", respJson);
|
2019-12-02 00:42:40 +01:00
|
|
|
|
|
|
|
if (resp.status !== 200) {
|
2019-12-09 13:29:11 +01:00
|
|
|
console.error(respJson);
|
2019-12-02 00:42:40 +01:00
|
|
|
throw Error("refresh failed");
|
|
|
|
}
|
|
|
|
|
|
|
|
const norevealIndex = respJson.noreveal_index;
|
|
|
|
|
|
|
|
if (typeof norevealIndex !== "number") {
|
|
|
|
throw Error("invalid response");
|
|
|
|
}
|
|
|
|
|
|
|
|
refreshSession.norevealIndex = norevealIndex;
|
|
|
|
|
2020-03-30 12:39:32 +02:00
|
|
|
await ws.db.mutate(Stores.refreshGroups, refreshGroupId, (rg) => {
|
2019-12-15 16:59:00 +01:00
|
|
|
const rs = rg.refreshSessionPerCoin[coinIndex];
|
|
|
|
if (!rs) {
|
|
|
|
return;
|
|
|
|
}
|
2019-12-02 00:42:40 +01:00
|
|
|
if (rs.norevealIndex !== undefined) {
|
|
|
|
return;
|
|
|
|
}
|
2019-12-05 19:38:19 +01:00
|
|
|
if (rs.finishedTimestamp) {
|
2019-12-02 00:42:40 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
rs.norevealIndex = norevealIndex;
|
2019-12-15 16:59:00 +01:00
|
|
|
return rg;
|
2019-12-02 00:42:40 +01:00
|
|
|
});
|
|
|
|
|
2019-12-05 19:38:19 +01:00
|
|
|
ws.notify({
|
|
|
|
type: NotificationType.RefreshMelted,
|
|
|
|
});
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async function refreshReveal(
|
|
|
|
ws: InternalWalletState,
|
2019-12-15 16:59:00 +01:00
|
|
|
refreshGroupId: string,
|
|
|
|
coinIndex: number,
|
2019-12-02 00:42:40 +01:00
|
|
|
): Promise<void> {
|
2019-12-15 16:59:00 +01:00
|
|
|
const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
|
|
|
|
if (!refreshGroup) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];
|
2019-12-02 00:42:40 +01:00
|
|
|
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");
|
|
|
|
}
|
|
|
|
|
2019-12-12 22:39:45 +01:00
|
|
|
const meltCoinRecord = await ws.db.get(
|
2019-12-02 00:42:40 +01:00
|
|
|
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,
|
|
|
|
};
|
|
|
|
|
2020-03-09 12:07:46 +01:00
|
|
|
const reqUrl = new URL(
|
|
|
|
`refreshes/${refreshSession.hash}/reveal`,
|
|
|
|
refreshSession.exchangeBaseUrl,
|
|
|
|
);
|
2019-12-02 00:42:40 +01:00
|
|
|
logger.trace("reveal request:", req);
|
|
|
|
|
|
|
|
let resp;
|
|
|
|
try {
|
|
|
|
resp = await ws.http.postJson(reqUrl.href, req);
|
|
|
|
} catch (e) {
|
|
|
|
console.error("got error during /refresh/reveal request");
|
|
|
|
console.error(e);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.trace("session:", refreshSession);
|
|
|
|
logger.trace("reveal response:", resp);
|
|
|
|
|
|
|
|
if (resp.status !== 200) {
|
|
|
|
console.error("error: /refresh/reveal returned status " + resp.status);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-12-09 13:29:11 +01:00
|
|
|
const respJson = await resp.json();
|
2019-12-02 00:42:40 +01:00
|
|
|
|
|
|
|
if (!respJson.ev_sigs || !Array.isArray(respJson.ev_sigs)) {
|
|
|
|
console.error("/refresh/reveal did not contain ev_sigs");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const coins: CoinRecord[] = [];
|
|
|
|
|
|
|
|
for (let i = 0; i < respJson.ev_sigs.length; i++) {
|
2019-12-12 22:39:45 +01:00
|
|
|
const denom = await ws.db.get(Stores.denominations, [
|
2019-12-02 00:42:40 +01:00
|
|
|
refreshSession.exchangeBaseUrl,
|
|
|
|
refreshSession.newDenoms[i],
|
|
|
|
]);
|
|
|
|
if (!denom) {
|
|
|
|
console.error("denom not found");
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
const pc =
|
|
|
|
refreshSession.planchetsForGammas[refreshSession.norevealIndex!][i];
|
|
|
|
const denomSig = await ws.cryptoApi.rsaUnblind(
|
|
|
|
respJson.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,
|
2020-03-11 20:14:28 +01:00
|
|
|
coinSource: {
|
|
|
|
type: CoinSourceType.Refresh,
|
|
|
|
oldCoinPub: refreshSession.meltCoinPub,
|
2020-03-24 10:55:04 +01:00
|
|
|
},
|
|
|
|
suspended: false,
|
2019-12-02 00:42:40 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
coins.push(coin);
|
|
|
|
}
|
|
|
|
|
2019-12-12 22:39:45 +01:00
|
|
|
await ws.db.runWithWriteTransaction(
|
2019-12-15 16:59:00 +01:00
|
|
|
[Stores.coins, Stores.refreshGroups],
|
2020-03-30 12:39:32 +02:00
|
|
|
async (tx) => {
|
2019-12-15 16:59:00 +01:00
|
|
|
const rg = await tx.get(Stores.refreshGroups, refreshGroupId);
|
|
|
|
if (!rg) {
|
2019-12-05 19:38:19 +01:00
|
|
|
console.log("no refresh session found");
|
2019-12-02 00:42:40 +01:00
|
|
|
return;
|
|
|
|
}
|
2019-12-15 16:59:00 +01:00
|
|
|
const rs = rg.refreshSessionPerCoin[coinIndex];
|
|
|
|
if (!rs) {
|
|
|
|
return;
|
|
|
|
}
|
2019-12-05 19:38:19 +01:00
|
|
|
if (rs.finishedTimestamp) {
|
|
|
|
console.log("refresh session already finished");
|
2019-12-02 00:42:40 +01:00
|
|
|
return;
|
|
|
|
}
|
2019-12-05 19:38:19 +01:00
|
|
|
rs.finishedTimestamp = getTimestampNow();
|
2019-12-15 16:59:00 +01:00
|
|
|
rg.finishedPerCoin[coinIndex] = true;
|
|
|
|
let allDone = true;
|
|
|
|
for (const f of rg.finishedPerCoin) {
|
|
|
|
if (!f) {
|
|
|
|
allDone = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (allDone) {
|
2019-12-16 16:20:45 +01:00
|
|
|
rg.timestampFinished = getTimestampNow();
|
2019-12-15 16:59:00 +01:00
|
|
|
rg.retryInfo = initRetryInfo(false);
|
|
|
|
}
|
2019-12-02 00:42:40 +01:00
|
|
|
for (let coin of coins) {
|
|
|
|
await tx.put(Stores.coins, coin);
|
|
|
|
}
|
2019-12-15 16:59:00 +01:00
|
|
|
await tx.put(Stores.refreshGroups, rg);
|
2019-12-02 00:42:40 +01:00
|
|
|
},
|
|
|
|
);
|
2019-12-05 19:38:19 +01:00
|
|
|
console.log("refresh finished (end of reveal)");
|
|
|
|
ws.notify({
|
|
|
|
type: NotificationType.RefreshRevealed,
|
|
|
|
});
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
2019-12-05 19:38:19 +01:00
|
|
|
async function incrementRefreshRetry(
|
|
|
|
ws: InternalWalletState,
|
2019-12-15 16:59:00 +01:00
|
|
|
refreshGroupId: string,
|
2019-12-05 19:38:19 +01:00
|
|
|
err: OperationError | undefined,
|
|
|
|
): Promise<void> {
|
2020-03-30 12:39:32 +02:00
|
|
|
await ws.db.runWithWriteTransaction([Stores.refreshGroups], async (tx) => {
|
2019-12-15 16:59:00 +01:00
|
|
|
const r = await tx.get(Stores.refreshGroups, refreshGroupId);
|
2019-12-05 19:38:19 +01:00
|
|
|
if (!r) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!r.retryInfo) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
r.retryInfo.retryCounter++;
|
|
|
|
updateRetryInfoTimeout(r.retryInfo);
|
|
|
|
r.lastError = err;
|
2019-12-15 16:59:00 +01:00
|
|
|
await tx.put(Stores.refreshGroups, r);
|
2019-12-05 19:38:19 +01:00
|
|
|
});
|
2019-12-06 02:52:16 +01:00
|
|
|
ws.notify({ type: NotificationType.RefreshOperationError });
|
2019-12-05 19:38:19 +01:00
|
|
|
}
|
|
|
|
|
2019-12-15 16:59:00 +01:00
|
|
|
export async function processRefreshGroup(
|
2019-12-02 00:42:40 +01:00
|
|
|
ws: InternalWalletState,
|
2019-12-15 16:59:00 +01:00
|
|
|
refreshGroupId: string,
|
2019-12-07 22:02:11 +01:00
|
|
|
forceNow: boolean = false,
|
2019-12-15 16:59:00 +01:00
|
|
|
): Promise<void> {
|
|
|
|
await ws.memoProcessRefresh.memo(refreshGroupId, async () => {
|
2019-12-05 19:38:19 +01:00
|
|
|
const onOpErr = (e: OperationError) =>
|
2019-12-15 16:59:00 +01:00
|
|
|
incrementRefreshRetry(ws, refreshGroupId, e);
|
|
|
|
return await guardOperationException(
|
|
|
|
async () => await processRefreshGroupImpl(ws, refreshGroupId, forceNow),
|
2019-12-05 19:38:19 +01:00
|
|
|
onOpErr,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-12-15 16:59:00 +01:00
|
|
|
async function resetRefreshGroupRetry(
|
2019-12-07 22:02:11 +01:00
|
|
|
ws: InternalWalletState,
|
|
|
|
refreshSessionId: string,
|
|
|
|
) {
|
2020-03-30 12:39:32 +02:00
|
|
|
await ws.db.mutate(Stores.refreshGroups, refreshSessionId, (x) => {
|
2019-12-07 22:02:11 +01:00
|
|
|
if (x.retryInfo.active) {
|
|
|
|
x.retryInfo = initRetryInfo();
|
|
|
|
}
|
|
|
|
return x;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-12-15 16:59:00 +01:00
|
|
|
async function processRefreshGroupImpl(
|
2019-12-05 19:38:19 +01:00
|
|
|
ws: InternalWalletState,
|
2019-12-15 16:59:00 +01:00
|
|
|
refreshGroupId: string,
|
2019-12-07 22:02:11 +01:00
|
|
|
forceNow: boolean,
|
2019-12-02 00:42:40 +01:00
|
|
|
) {
|
2019-12-07 22:02:11 +01:00
|
|
|
if (forceNow) {
|
2019-12-15 16:59:00 +01:00
|
|
|
await resetRefreshGroupRetry(ws, refreshGroupId);
|
2019-12-07 22:02:11 +01:00
|
|
|
}
|
2019-12-15 16:59:00 +01:00
|
|
|
const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
|
|
|
|
if (!refreshGroup) {
|
2019-12-02 00:42:40 +01:00
|
|
|
return;
|
|
|
|
}
|
2019-12-16 16:20:45 +01:00
|
|
|
if (refreshGroup.timestampFinished) {
|
2019-12-02 00:42:40 +01:00
|
|
|
return;
|
|
|
|
}
|
2019-12-15 16:59:00 +01:00
|
|
|
const ps = refreshGroup.oldCoinPubs.map((x, i) =>
|
|
|
|
processRefreshSession(ws, refreshGroupId, i),
|
|
|
|
);
|
|
|
|
await Promise.all(ps);
|
2019-12-02 00:42:40 +01:00
|
|
|
logger.trace("refresh finished");
|
|
|
|
}
|
|
|
|
|
2019-12-15 16:59:00 +01:00
|
|
|
async function processRefreshSession(
|
2019-12-02 00:42:40 +01:00
|
|
|
ws: InternalWalletState,
|
2019-12-15 16:59:00 +01:00
|
|
|
refreshGroupId: string,
|
|
|
|
coinIndex: number,
|
|
|
|
) {
|
2020-03-09 12:07:46 +01:00
|
|
|
logger.trace(
|
|
|
|
`processing refresh session for coin ${coinIndex} of group ${refreshGroupId}`,
|
|
|
|
);
|
2019-12-15 16:59:00 +01:00
|
|
|
let refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
|
|
|
|
if (!refreshGroup) {
|
2019-12-02 00:42:40 +01:00
|
|
|
return;
|
|
|
|
}
|
2019-12-15 16:59:00 +01:00
|
|
|
if (refreshGroup.finishedPerCoin[coinIndex]) {
|
|
|
|
return;
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
2019-12-15 16:59:00 +01:00
|
|
|
if (!refreshGroup.refreshSessionPerCoin[coinIndex]) {
|
|
|
|
await refreshCreateSession(ws, refreshGroupId, coinIndex);
|
|
|
|
refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
|
|
|
|
if (!refreshGroup) {
|
|
|
|
return;
|
|
|
|
}
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
2019-12-15 16:59:00 +01:00
|
|
|
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",
|
|
|
|
);
|
|
|
|
}
|
2019-12-02 00:42:40 +01:00
|
|
|
return;
|
|
|
|
}
|
2019-12-15 16:59:00 +01:00
|
|
|
if (refreshSession.norevealIndex === undefined) {
|
|
|
|
await refreshMelt(ws, refreshGroupId, coinIndex);
|
|
|
|
}
|
|
|
|
await refreshReveal(ws, refreshGroupId, coinIndex);
|
|
|
|
}
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2019-12-15 16:59:00 +01:00
|
|
|
/**
|
|
|
|
* Create a refresh group for a list of coins.
|
|
|
|
*/
|
|
|
|
export async function createRefreshGroup(
|
|
|
|
tx: TransactionHandle,
|
|
|
|
oldCoinPubs: CoinPublicKey[],
|
|
|
|
reason: RefreshReason,
|
|
|
|
): Promise<RefreshGroupId> {
|
|
|
|
const refreshGroupId = encodeCrock(getRandomBytes(32));
|
|
|
|
|
|
|
|
const refreshGroup: RefreshGroupRecord = {
|
2019-12-16 16:20:45 +01:00
|
|
|
timestampFinished: undefined,
|
2020-03-30 12:39:32 +02:00
|
|
|
finishedPerCoin: oldCoinPubs.map((x) => false),
|
2019-12-15 16:59:00 +01:00
|
|
|
lastError: undefined,
|
2019-12-16 12:53:22 +01:00
|
|
|
lastErrorPerCoin: {},
|
2020-03-30 12:39:32 +02:00
|
|
|
oldCoinPubs: oldCoinPubs.map((x) => x.coinPub),
|
2019-12-15 16:59:00 +01:00
|
|
|
reason,
|
|
|
|
refreshGroupId,
|
2020-03-30 12:39:32 +02:00
|
|
|
refreshSessionPerCoin: oldCoinPubs.map((x) => undefined),
|
2019-12-15 16:59:00 +01:00
|
|
|
retryInfo: initRetryInfo(),
|
|
|
|
};
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2019-12-15 16:59:00 +01:00
|
|
|
await tx.put(Stores.refreshGroups, refreshGroup);
|
|
|
|
return {
|
|
|
|
refreshGroupId,
|
|
|
|
};
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|