2019-12-02 00:42:40 +01:00
|
|
|
/*
|
|
|
|
This file is part of GNU Taler
|
2020-03-13 09:21:03 +01:00
|
|
|
(C) 2019-2020 Taler Systems SA
|
2019-12-02 00:42:40 +01:00
|
|
|
|
|
|
|
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/>
|
|
|
|
*/
|
|
|
|
|
2020-03-11 20:14:28 +01:00
|
|
|
/**
|
|
|
|
* Implementation of the recoup operation, which allows to recover the
|
|
|
|
* value of coins held in a revoked denomination.
|
|
|
|
*
|
|
|
|
* @author Florian Dold <dold@taler.net>
|
|
|
|
*/
|
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
/**
|
|
|
|
* Imports.
|
|
|
|
*/
|
2021-06-02 13:23:51 +02:00
|
|
|
import {
|
|
|
|
Amounts,
|
|
|
|
codecForRecoupConfirmation,
|
|
|
|
getTimestampNow,
|
|
|
|
NotificationType,
|
|
|
|
RefreshReason,
|
|
|
|
TalerErrorDetails,
|
|
|
|
} from "@gnu-taler/taler-util";
|
2021-02-08 15:38:34 +01:00
|
|
|
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
|
2021-06-02 13:23:51 +02:00
|
|
|
import {
|
|
|
|
CoinRecord,
|
|
|
|
CoinSourceType,
|
|
|
|
CoinStatus,
|
|
|
|
RecoupGroupRecord,
|
|
|
|
RefreshCoinSource,
|
|
|
|
ReserveRecordStatus,
|
|
|
|
WithdrawCoinSource,
|
2021-06-09 15:14:17 +02:00
|
|
|
WalletStoresV1,
|
2021-06-02 13:23:51 +02:00
|
|
|
} from "../db.js";
|
2021-03-17 17:56:37 +01:00
|
|
|
|
2020-07-22 10:52:03 +02:00
|
|
|
import { readSuccessResponseJsonOrThrow } from "../util/http";
|
2021-06-08 20:58:13 +02:00
|
|
|
import { Logger } from "@gnu-taler/taler-util";
|
2020-09-08 16:59:47 +02:00
|
|
|
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries";
|
2021-02-08 15:38:34 +01:00
|
|
|
import { URL } from "../util/url";
|
|
|
|
import { guardOperationException } from "./errors";
|
|
|
|
import { createRefreshGroup, processRefreshGroup } from "./refresh";
|
|
|
|
import { getReserveRequestTimeout, processReserve } from "./reserves";
|
|
|
|
import { InternalWalletState } from "./state";
|
2021-06-09 15:14:17 +02:00
|
|
|
import { GetReadWriteAccess } from "../util/query.js";
|
2020-08-14 12:23:50 +02:00
|
|
|
|
|
|
|
const logger = new Logger("operations/recoup.ts");
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2020-03-11 20:14:28 +01:00
|
|
|
async function incrementRecoupRetry(
|
2019-12-02 00:42:40 +01:00
|
|
|
ws: InternalWalletState,
|
2020-03-11 20:14:28 +01:00
|
|
|
recoupGroupId: string,
|
2020-09-01 14:57:22 +02:00
|
|
|
err: TalerErrorDetails | undefined,
|
2019-12-02 00:42:40 +01:00
|
|
|
): Promise<void> {
|
2021-06-09 15:14:17 +02:00
|
|
|
await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
recoupGroups: x.recoupGroups,
|
|
|
|
}))
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const r = await tx.recoupGroups.get(recoupGroupId);
|
|
|
|
if (!r) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!r.retryInfo) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
r.retryInfo.retryCounter++;
|
|
|
|
updateRetryInfoTimeout(r.retryInfo);
|
|
|
|
r.lastError = err;
|
|
|
|
await tx.recoupGroups.put(r);
|
|
|
|
});
|
2020-07-22 10:52:03 +02:00
|
|
|
if (err) {
|
|
|
|
ws.notify({ type: NotificationType.RecoupOperationError, error: err });
|
|
|
|
}
|
2020-03-11 20:14:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async function putGroupAsFinished(
|
2020-03-27 19:07:02 +01:00
|
|
|
ws: InternalWalletState,
|
2021-06-09 15:14:17 +02:00
|
|
|
tx: GetReadWriteAccess<{
|
|
|
|
recoupGroups: typeof WalletStoresV1.recoupGroups;
|
|
|
|
denominations: typeof WalletStoresV1.denominations;
|
|
|
|
refreshGroups: typeof WalletStoresV1.refreshGroups;
|
|
|
|
coins: typeof WalletStoresV1.coins;
|
|
|
|
}>,
|
2020-03-11 20:14:28 +01:00
|
|
|
recoupGroup: RecoupGroupRecord,
|
|
|
|
coinIdx: number,
|
|
|
|
): Promise<void> {
|
2021-01-14 17:24:44 +01:00
|
|
|
logger.trace(
|
|
|
|
`setting coin ${coinIdx} of ${recoupGroup.coinPubs.length} as finished`,
|
|
|
|
);
|
2020-03-27 19:07:02 +01:00
|
|
|
if (recoupGroup.timestampFinished) {
|
|
|
|
return;
|
|
|
|
}
|
2020-03-11 20:14:28 +01:00
|
|
|
recoupGroup.recoupFinishedPerCoin[coinIdx] = true;
|
|
|
|
let allFinished = true;
|
|
|
|
for (const b of recoupGroup.recoupFinishedPerCoin) {
|
|
|
|
if (!b) {
|
|
|
|
allFinished = false;
|
|
|
|
}
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
2020-03-11 20:14:28 +01:00
|
|
|
if (allFinished) {
|
2021-01-14 17:24:44 +01:00
|
|
|
logger.trace("all recoups of recoup group are finished");
|
2020-03-11 20:14:28 +01:00
|
|
|
recoupGroup.timestampFinished = getTimestampNow();
|
|
|
|
recoupGroup.retryInfo = initRetryInfo(false);
|
|
|
|
recoupGroup.lastError = undefined;
|
2020-03-27 19:07:02 +01:00
|
|
|
if (recoupGroup.scheduleRefreshCoins.length > 0) {
|
|
|
|
const refreshGroupId = await createRefreshGroup(
|
2020-07-23 14:05:17 +02:00
|
|
|
ws,
|
2020-03-27 19:07:02 +01:00
|
|
|
tx,
|
|
|
|
recoupGroup.scheduleRefreshCoins.map((x) => ({ coinPub: x })),
|
|
|
|
RefreshReason.Recoup,
|
|
|
|
);
|
|
|
|
processRefreshGroup(ws, refreshGroupId.refreshGroupId).then((e) => {
|
|
|
|
console.error("error while refreshing after recoup", e);
|
|
|
|
});
|
|
|
|
}
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.recoupGroups.put(recoupGroup);
|
2020-03-11 20:14:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2021-06-09 15:14:17 +02:00
|
|
|
await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
recoupGroups: x.recoupGroups,
|
|
|
|
denominations: WalletStoresV1.denominations,
|
|
|
|
refreshGroups: WalletStoresV1.refreshGroups,
|
|
|
|
coins: WalletStoresV1.coins,
|
|
|
|
}))
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const recoupGroup = await tx.recoupGroups.get(recoupGroupId);
|
|
|
|
if (!recoupGroup) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
|
|
|
|
});
|
2020-03-11 20:14:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async function recoupWithdrawCoin(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
recoupGroupId: string,
|
|
|
|
coinIdx: number,
|
|
|
|
coin: CoinRecord,
|
|
|
|
cs: WithdrawCoinSource,
|
|
|
|
): Promise<void> {
|
|
|
|
const reservePub = cs.reservePub;
|
2021-06-09 15:14:17 +02:00
|
|
|
const reserve = await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
reserves: x.reserves,
|
|
|
|
}))
|
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
return tx.reserves.get(reservePub);
|
|
|
|
});
|
2019-12-02 00:42:40 +01:00
|
|
|
if (!reserve) {
|
2020-03-11 20:14:28 +01:00
|
|
|
// 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);
|
2020-08-20 12:57:20 +02:00
|
|
|
const resp = await ws.http.postJson(reqUrl.href, recoupRequest, {
|
|
|
|
timeout: getReserveRequestTimeout(reserve),
|
|
|
|
});
|
2020-07-22 10:52:03 +02:00
|
|
|
const recoupConfirmation = await readSuccessResponseJsonOrThrow(
|
|
|
|
resp,
|
|
|
|
codecForRecoupConfirmation(),
|
|
|
|
);
|
2020-03-11 20:14:28 +01:00
|
|
|
|
|
|
|
if (recoupConfirmation.reserve_pub !== reservePub) {
|
|
|
|
throw Error(`Coin's reserve doesn't match reserve on recoup`);
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
2020-03-11 20:14:28 +01:00
|
|
|
|
|
|
|
// FIXME: verify that our expectations about the amount match
|
|
|
|
|
2021-06-09 15:14:17 +02:00
|
|
|
await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
coins: x.coins,
|
|
|
|
denominations: x.denominations,
|
|
|
|
reserves: x.reserves,
|
|
|
|
recoupGroups: x.recoupGroups,
|
|
|
|
refreshGroups: x.refreshGroups,
|
|
|
|
}))
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const recoupGroup = await tx.recoupGroups.get(recoupGroupId);
|
2020-03-11 20:14:28 +01:00
|
|
|
if (!recoupGroup) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
|
|
|
|
return;
|
|
|
|
}
|
2021-06-09 15:14:17 +02:00
|
|
|
const updatedCoin = await tx.coins.get(coin.coinPub);
|
2020-03-11 20:14:28 +01:00
|
|
|
if (!updatedCoin) {
|
|
|
|
return;
|
|
|
|
}
|
2021-06-09 15:14:17 +02:00
|
|
|
const updatedReserve = await tx.reserves.get(reserve.reservePub);
|
2020-03-11 20:14:28 +01:00
|
|
|
if (!updatedReserve) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
updatedCoin.status = CoinStatus.Dormant;
|
|
|
|
const currency = updatedCoin.currentAmount.currency;
|
|
|
|
updatedCoin.currentAmount = Amounts.getZero(currency);
|
2020-09-03 23:40:36 +02:00
|
|
|
if (updatedReserve.reserveStatus === ReserveRecordStatus.DORMANT) {
|
|
|
|
updatedReserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
|
|
|
|
updatedReserve.retryInfo = initRetryInfo();
|
|
|
|
} else {
|
|
|
|
updatedReserve.requestedQuery = true;
|
|
|
|
updatedReserve.retryInfo = initRetryInfo();
|
|
|
|
}
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.coins.put(updatedCoin);
|
|
|
|
await tx.reserves.put(updatedReserve);
|
2020-03-27 19:07:02 +01:00
|
|
|
await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
|
2021-06-09 15:14:17 +02:00
|
|
|
});
|
2020-03-11 20:14:28 +01:00
|
|
|
|
2019-12-05 19:38:19 +01:00
|
|
|
ws.notify({
|
2020-03-11 20:14:28 +01:00
|
|
|
type: NotificationType.RecoupFinished,
|
2019-12-05 19:38:19 +01:00
|
|
|
});
|
2020-03-11 20:14:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
2021-01-14 17:24:44 +01:00
|
|
|
logger.trace(`making recoup request for ${coin.coinPub}`);
|
2020-07-22 10:52:03 +02:00
|
|
|
|
|
|
|
const resp = await ws.http.postJson(reqUrl.href, recoupRequest);
|
|
|
|
const recoupConfirmation = await readSuccessResponseJsonOrThrow(
|
|
|
|
resp,
|
|
|
|
codecForRecoupConfirmation(),
|
|
|
|
);
|
2020-03-11 20:14:28 +01:00
|
|
|
|
|
|
|
if (recoupConfirmation.old_coin_pub != cs.oldCoinPub) {
|
|
|
|
throw Error(`Coin's oldCoinPub doesn't match reserve on recoup`);
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
2020-03-11 20:14:28 +01:00
|
|
|
|
2021-06-09 15:14:17 +02:00
|
|
|
await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
coins: x.coins,
|
|
|
|
denominations: x.denominations,
|
|
|
|
reserves: x.reserves,
|
|
|
|
recoupGroups: x.recoupGroups,
|
|
|
|
refreshGroups: x.refreshGroups,
|
|
|
|
}))
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const recoupGroup = await tx.recoupGroups.get(recoupGroupId);
|
2020-03-11 20:14:28 +01:00
|
|
|
if (!recoupGroup) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
|
|
|
|
return;
|
|
|
|
}
|
2021-06-09 15:14:17 +02:00
|
|
|
const oldCoin = await tx.coins.get(cs.oldCoinPub);
|
|
|
|
const revokedCoin = await tx.coins.get(coin.coinPub);
|
2020-03-26 18:04:14 +01:00
|
|
|
if (!revokedCoin) {
|
2021-01-14 17:24:44 +01:00
|
|
|
logger.warn("revoked coin for recoup not found");
|
2020-03-11 20:14:28 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!oldCoin) {
|
2021-01-14 17:24:44 +01:00
|
|
|
logger.warn("refresh old coin for recoup not found");
|
2020-03-11 20:14:28 +01:00
|
|
|
return;
|
|
|
|
}
|
2020-03-26 18:04:14 +01:00
|
|
|
revokedCoin.status = CoinStatus.Dormant;
|
2020-03-11 20:14:28 +01:00
|
|
|
oldCoin.currentAmount = Amounts.add(
|
|
|
|
oldCoin.currentAmount,
|
2020-03-26 18:04:14 +01:00
|
|
|
recoupGroup.oldAmountPerCoin[coinIdx],
|
2020-03-11 20:14:28 +01:00
|
|
|
).amount;
|
2020-08-14 12:23:50 +02:00
|
|
|
logger.trace(
|
2020-03-26 18:04:14 +01:00
|
|
|
"recoup: setting old coin amount to",
|
2020-04-02 17:03:01 +02:00
|
|
|
Amounts.stringify(oldCoin.currentAmount),
|
2020-03-26 18:04:14 +01:00
|
|
|
);
|
2020-03-27 19:07:02 +01:00
|
|
|
recoupGroup.scheduleRefreshCoins.push(oldCoin.coinPub);
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.coins.put(revokedCoin);
|
|
|
|
await tx.coins.put(oldCoin);
|
2020-03-27 19:07:02 +01:00
|
|
|
await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
|
2021-06-09 15:14:17 +02:00
|
|
|
});
|
2020-03-11 20:14:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async function resetRecoupGroupRetry(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
recoupGroupId: string,
|
2020-04-07 10:07:32 +02:00
|
|
|
): Promise<void> {
|
2021-06-09 15:14:17 +02:00
|
|
|
await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
recoupGroups: x.recoupGroups,
|
|
|
|
}))
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const x = await tx.recoupGroups.get(recoupGroupId);
|
2021-06-11 11:15:08 +02:00
|
|
|
if (x) {
|
2021-06-09 15:14:17 +02:00
|
|
|
x.retryInfo = initRetryInfo();
|
|
|
|
await tx.recoupGroups.put(x);
|
|
|
|
}
|
|
|
|
});
|
2020-03-11 20:14:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export async function processRecoupGroup(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
recoupGroupId: string,
|
2020-04-06 17:45:41 +02:00
|
|
|
forceNow = false,
|
2020-03-11 20:14:28 +01:00
|
|
|
): Promise<void> {
|
|
|
|
await ws.memoProcessRecoup.memo(recoupGroupId, async () => {
|
2020-09-01 14:57:22 +02:00
|
|
|
const onOpErr = (e: TalerErrorDetails): Promise<void> =>
|
2020-03-11 20:14:28 +01:00
|
|
|
incrementRecoupRetry(ws, recoupGroupId, e);
|
|
|
|
return await guardOperationException(
|
|
|
|
async () => await processRecoupGroupImpl(ws, recoupGroupId, forceNow),
|
|
|
|
onOpErr,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async function processRecoupGroupImpl(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
recoupGroupId: string,
|
2020-04-06 17:45:41 +02:00
|
|
|
forceNow = false,
|
2020-03-11 20:14:28 +01:00
|
|
|
): Promise<void> {
|
|
|
|
if (forceNow) {
|
|
|
|
await resetRecoupGroupRetry(ws, recoupGroupId);
|
|
|
|
}
|
2021-06-09 15:14:17 +02:00
|
|
|
const recoupGroup = await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
recoupGroups: x.recoupGroups,
|
|
|
|
}))
|
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
return tx.recoupGroups.get(recoupGroupId);
|
|
|
|
});
|
2020-03-11 20:14:28 +01:00
|
|
|
if (!recoupGroup) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (recoupGroup.timestampFinished) {
|
2020-08-14 12:23:50 +02:00
|
|
|
logger.trace("recoup group finished");
|
2020-03-11 20:14:28 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
const ps = recoupGroup.coinPubs.map((x, i) =>
|
|
|
|
processRecoup(ws, recoupGroupId, i),
|
|
|
|
);
|
|
|
|
await Promise.all(ps);
|
2020-09-03 23:40:36 +02:00
|
|
|
|
|
|
|
const reserveSet = new Set<string>();
|
|
|
|
for (let i = 0; i < recoupGroup.coinPubs.length; i++) {
|
|
|
|
const coinPub = recoupGroup.coinPubs[i];
|
2021-06-09 15:14:17 +02:00
|
|
|
const coin = await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
coins: x.coins,
|
|
|
|
}))
|
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
return tx.coins.get(coinPub);
|
|
|
|
});
|
2020-09-03 23:40:36 +02:00
|
|
|
if (!coin) {
|
2021-06-09 15:14:17 +02:00
|
|
|
throw Error(`Coin ${coinPub} not found, can't request recoup`);
|
2020-09-03 23:40:36 +02:00
|
|
|
}
|
|
|
|
if (coin.coinSource.type === CoinSourceType.Withdraw) {
|
|
|
|
reserveSet.add(coin.coinSource.reservePub);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const r of reserveSet.values()) {
|
|
|
|
processReserve(ws, r).catch((e) => {
|
|
|
|
logger.error(`processing reserve ${r} after recoup failed`);
|
|
|
|
});
|
|
|
|
}
|
2020-03-11 20:14:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export async function createRecoupGroup(
|
|
|
|
ws: InternalWalletState,
|
2021-06-09 15:14:17 +02:00
|
|
|
tx: GetReadWriteAccess<{
|
|
|
|
recoupGroups: typeof WalletStoresV1.recoupGroups;
|
|
|
|
denominations: typeof WalletStoresV1.denominations;
|
|
|
|
refreshGroups: typeof WalletStoresV1.refreshGroups;
|
|
|
|
coins: typeof WalletStoresV1.coins;
|
|
|
|
}>,
|
2020-03-11 20:14:28 +01:00
|
|
|
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),
|
2020-03-26 18:04:14 +01:00
|
|
|
// Will be populated later
|
|
|
|
oldAmountPerCoin: [],
|
2020-03-27 19:07:02 +01:00
|
|
|
scheduleRefreshCoins: [],
|
2020-03-11 20:14:28 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
for (let coinIdx = 0; coinIdx < coinPubs.length; coinIdx++) {
|
|
|
|
const coinPub = coinPubs[coinIdx];
|
2021-06-09 15:14:17 +02:00
|
|
|
const coin = await tx.coins.get(coinPub);
|
2020-03-11 20:14:28 +01:00
|
|
|
if (!coin) {
|
2020-03-27 19:07:02 +01:00
|
|
|
await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
|
2020-03-11 20:14:28 +01:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (Amounts.isZero(coin.currentAmount)) {
|
2020-03-27 19:07:02 +01:00
|
|
|
await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
|
2020-03-11 20:14:28 +01:00
|
|
|
continue;
|
|
|
|
}
|
2020-03-26 18:04:14 +01:00
|
|
|
recoupGroup.oldAmountPerCoin[coinIdx] = coin.currentAmount;
|
2020-03-11 20:14:28 +01:00
|
|
|
coin.currentAmount = Amounts.getZero(coin.currentAmount.currency);
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.coins.put(coin);
|
2020-03-11 20:14:28 +01:00
|
|
|
}
|
|
|
|
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.recoupGroups.put(recoupGroup);
|
2020-03-11 20:14:28 +01:00
|
|
|
|
|
|
|
return recoupGroupId;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function processRecoup(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
recoupGroupId: string,
|
|
|
|
coinIdx: number,
|
|
|
|
): Promise<void> {
|
2021-06-09 15:14:17 +02:00
|
|
|
const coin = await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
recoupGroups: x.recoupGroups,
|
|
|
|
coins: x.coins,
|
|
|
|
}))
|
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
const recoupGroup = await tx.recoupGroups.get(recoupGroupId);
|
|
|
|
if (!recoupGroup) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (recoupGroup.timestampFinished) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
|
|
|
|
return;
|
|
|
|
}
|
2020-03-11 20:14:28 +01:00
|
|
|
|
2021-06-09 15:14:17 +02:00
|
|
|
const coinPub = recoupGroup.coinPubs[coinIdx];
|
|
|
|
|
|
|
|
const coin = await tx.coins.get(coinPub);
|
|
|
|
if (!coin) {
|
|
|
|
throw Error(`Coin ${coinPub} not found, can't request payback`);
|
|
|
|
}
|
|
|
|
return coin;
|
|
|
|
});
|
2020-03-11 20:14:28 +01:00
|
|
|
|
|
|
|
if (!coin) {
|
2021-06-09 15:14:17 +02:00
|
|
|
return;
|
2020-03-11 20:14:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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");
|
|
|
|
}
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|