aboutsummaryrefslogtreecommitdiff
path: root/src/operations
diff options
context:
space:
mode:
Diffstat (limited to 'src/operations')
-rw-r--r--src/operations/balance.ts47
-rw-r--r--src/operations/history.ts2
-rw-r--r--src/operations/pay.ts20
-rw-r--r--src/operations/pending.ts71
-rw-r--r--src/operations/refresh.ts364
-rw-r--r--src/operations/reserves.ts4
-rw-r--r--src/operations/tip.ts1
7 files changed, 290 insertions, 219 deletions
diff --git a/src/operations/balance.ts b/src/operations/balance.ts
index f5a51abec..15d8e52fa 100644
--- a/src/operations/balance.ts
+++ b/src/operations/balance.ts
@@ -74,7 +74,7 @@ export async function getBalances(
};
await ws.db.runWithReadTransaction(
- [Stores.coins, Stores.refresh, Stores.reserves, Stores.purchases, Stores.withdrawalSession],
+ [Stores.coins, Stores.refreshGroups, Stores.reserves, Stores.purchases, Stores.withdrawalSession],
async tx => {
await tx.iter(Stores.coins).forEach(c => {
if (c.suspended) {
@@ -83,39 +83,30 @@ export async function getBalances(
if (c.status === CoinStatus.Fresh) {
addTo(balanceStore, "available", c.currentAmount, c.exchangeBaseUrl);
}
- if (c.status === CoinStatus.Dirty) {
- addTo(
- balanceStore,
- "pendingIncoming",
- c.currentAmount,
- c.exchangeBaseUrl,
- );
- addTo(
- balanceStore,
- "pendingIncomingDirty",
- c.currentAmount,
- c.exchangeBaseUrl,
- );
- }
});
- await tx.iter(Stores.refresh).forEach(r => {
+ await tx.iter(Stores.refreshGroups).forEach(r => {
// Don't count finished refreshes, since the refresh already resulted
// in coins being added to the wallet.
if (r.finishedTimestamp) {
return;
}
- addTo(
- balanceStore,
- "pendingIncoming",
- r.valueOutput,
- r.exchangeBaseUrl,
- );
- addTo(
- balanceStore,
- "pendingIncomingRefresh",
- r.valueOutput,
- r.exchangeBaseUrl,
- );
+ for (let i = 0; i < r.oldCoinPubs.length; i++) {
+ const session = r.refreshSessionPerCoin[i];
+ if (session) {
+ addTo(
+ balanceStore,
+ "pendingIncoming",
+ session.valueOutput,
+ session.exchangeBaseUrl,
+ );
+ addTo(
+ balanceStore,
+ "pendingIncomingRefresh",
+ session.valueOutput,
+ session.exchangeBaseUrl,
+ );
+ }
+ }
});
await tx.iter(Stores.withdrawalSession).forEach(wds => {
diff --git a/src/operations/history.ts b/src/operations/history.ts
index 64f5b21cc..8b225ea07 100644
--- a/src/operations/history.ts
+++ b/src/operations/history.ts
@@ -45,7 +45,7 @@ export async function getHistory(
Stores.exchanges,
Stores.proposals,
Stores.purchases,
- Stores.refresh,
+ Stores.refreshGroups,
Stores.reserves,
Stores.tips,
Stores.withdrawalSession,
diff --git a/src/operations/pay.ts b/src/operations/pay.ts
index 27f0e4404..ccb55305d 100644
--- a/src/operations/pay.ts
+++ b/src/operations/pay.ts
@@ -34,6 +34,7 @@ import {
PreparePayResult,
ConfirmPayResult,
OperationError,
+ RefreshReason,
} from "../types/walletTypes";
import {
Database
@@ -65,7 +66,7 @@ import {
parseRefundUri,
getOrderDownloadUrl,
} from "../util/taleruri";
-import { getTotalRefreshCost, refresh } from "./refresh";
+import { getTotalRefreshCost, createRefreshGroup } from "./refresh";
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
import { guardOperationException } from "./errors";
import { assertUnreachable } from "../util/assertUnreachable";
@@ -782,26 +783,21 @@ export async function submitPay(
console.error("coin not found");
throw Error("coin used in payment not found");
}
- c.status = CoinStatus.Dirty;
+ c.status = CoinStatus.Dormant;
modifiedCoins.push(c);
}
await ws.db.runWithWriteTransaction(
- [Stores.coins, Stores.purchases],
+ [Stores.coins, Stores.purchases, Stores.refreshGroups],
async tx => {
for (let c of modifiedCoins) {
await tx.put(Stores.coins, c);
}
+ await createRefreshGroup(tx, modifiedCoins.map((x) => ({ coinPub: x.coinPub })), RefreshReason.Pay);
await tx.put(Stores.purchases, purchase);
},
);
- for (const c of purchase.payReq.coins) {
- refresh(ws, c.coin_pub).catch(e => {
- console.log("error in refreshing after payment:", e);
- });
- }
-
const nextUrl = getNextUrl(purchase.contractTerms);
ws.cachedNextUrl[purchase.contractTerms.fulfillment_url] = {
nextUrl,
@@ -1433,7 +1429,7 @@ async function processPurchaseApplyRefundImpl(
let allRefundsProcessed = false;
await ws.db.runWithWriteTransaction(
- [Stores.purchases, Stores.coins],
+ [Stores.purchases, Stores.coins, Stores.refreshGroups],
async tx => {
const p = await tx.get(Stores.purchases, proposalId);
if (!p) {
@@ -1456,10 +1452,11 @@ async function processPurchaseApplyRefundImpl(
}
const refundAmount = Amounts.parseOrThrow(perm.refund_amount);
const refundFee = Amounts.parseOrThrow(perm.refund_fee);
- c.status = CoinStatus.Dirty;
+ c.status = CoinStatus.Dormant;
c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
await tx.put(Stores.coins, c);
+ await createRefreshGroup(tx, [{ coinPub: perm.coin_pub }], RefreshReason.Refund);
},
);
if (allRefundsProcessed) {
@@ -1467,7 +1464,6 @@ async function processPurchaseApplyRefundImpl(
type: NotificationType.RefundFinished,
});
}
- await refresh(ws, perm.coin_pub);
}
ws.notify({
diff --git a/src/operations/pending.ts b/src/operations/pending.ts
index 13859c64b..27892df06 100644
--- a/src/operations/pending.ts
+++ b/src/operations/pending.ts
@@ -31,7 +31,7 @@ import {
CoinStatus,
ProposalStatus,
} from "../types/dbTypes";
-import { PendingOperationsResponse } from "../types/pending";
+import { PendingOperationsResponse, PendingOperationType } from "../types/pending";
function updateRetryDelay(
oldDelay: Duration,
@@ -59,7 +59,7 @@ async function gatherExchangePending(
case ExchangeUpdateStatus.FINISHED:
if (e.lastError) {
resp.pendingOperations.push({
- type: "bug",
+ type: PendingOperationType.Bug,
givesLifeness: false,
message:
"Exchange record is in FINISHED state but has lastError set",
@@ -70,7 +70,7 @@ async function gatherExchangePending(
}
if (!e.details) {
resp.pendingOperations.push({
- type: "bug",
+ type: PendingOperationType.Bug,
givesLifeness: false,
message:
"Exchange record does not have details, but no update in progress.",
@@ -81,7 +81,7 @@ async function gatherExchangePending(
}
if (!e.wireInfo) {
resp.pendingOperations.push({
- type: "bug",
+ type: PendingOperationType.Bug,
givesLifeness: false,
message:
"Exchange record does not have wire info, but no update in progress.",
@@ -93,7 +93,7 @@ async function gatherExchangePending(
break;
case ExchangeUpdateStatus.FETCH_KEYS:
resp.pendingOperations.push({
- type: "exchange-update",
+ type: PendingOperationType.ExchangeUpdate,
givesLifeness: false,
stage: "fetch-keys",
exchangeBaseUrl: e.baseUrl,
@@ -103,7 +103,7 @@ async function gatherExchangePending(
break;
case ExchangeUpdateStatus.FETCH_WIRE:
resp.pendingOperations.push({
- type: "exchange-update",
+ type: PendingOperationType.ExchangeUpdate,
givesLifeness: false,
stage: "fetch-wire",
exchangeBaseUrl: e.baseUrl,
@@ -113,7 +113,7 @@ async function gatherExchangePending(
break;
default:
resp.pendingOperations.push({
- type: "bug",
+ type: PendingOperationType.Bug,
givesLifeness: false,
message: "Unknown exchangeUpdateStatus",
details: {
@@ -147,7 +147,7 @@ async function gatherReservePending(
break;
}
resp.pendingOperations.push({
- type: "reserve",
+ type: PendingOperationType.Reserve,
givesLifeness: false,
stage: reserve.reserveStatus,
timestampCreated: reserve.created,
@@ -169,7 +169,7 @@ async function gatherReservePending(
return;
}
resp.pendingOperations.push({
- type: "reserve",
+ type: PendingOperationType.Reserve,
givesLifeness: true,
stage: reserve.reserveStatus,
timestampCreated: reserve.created,
@@ -180,7 +180,7 @@ async function gatherReservePending(
break;
default:
resp.pendingOperations.push({
- type: "bug",
+ type: PendingOperationType.Bug,
givesLifeness: false,
message: "Unknown reserve record status",
details: {
@@ -199,7 +199,7 @@ async function gatherRefreshPending(
resp: PendingOperationsResponse,
onlyDue: boolean = false,
): Promise<void> {
- await tx.iter(Stores.refresh).forEach(r => {
+ await tx.iter(Stores.refreshGroups).forEach(r => {
if (r.finishedTimestamp) {
return;
}
@@ -211,43 +211,15 @@ async function gatherRefreshPending(
if (onlyDue && r.retryInfo.nextRetry.t_ms > now.t_ms) {
return;
}
- let refreshStatus: string;
- if (r.norevealIndex === undefined) {
- refreshStatus = "melt";
- } else {
- refreshStatus = "reveal";
- }
resp.pendingOperations.push({
- type: "refresh",
+ type: PendingOperationType.Refresh,
givesLifeness: true,
- oldCoinPub: r.meltCoinPub,
- refreshStatus,
- refreshOutputSize: r.newDenoms.length,
- refreshSessionId: r.refreshSessionId,
+ refreshGroupId: r.refreshGroupId,
});
});
}
-async function gatherCoinsPending(
- tx: TransactionHandle,
- now: Timestamp,
- resp: PendingOperationsResponse,
- onlyDue: boolean = false,
-): Promise<void> {
- // Refreshing dirty coins is always due.
- await tx.iter(Stores.coins).forEach(coin => {
- if (coin.status == CoinStatus.Dirty) {
- resp.nextRetryDelay = { d_ms: 0 };
- resp.pendingOperations.push({
- givesLifeness: true,
- type: "dirty-coin",
- coinPub: coin.coinPub,
- });
- }
- });
-}
-
async function gatherWithdrawalPending(
tx: TransactionHandle,
now: Timestamp,
@@ -272,7 +244,7 @@ async function gatherWithdrawalPending(
);
const numCoinsTotal = wsr.withdrawn.length;
resp.pendingOperations.push({
- type: "withdraw",
+ type: PendingOperationType.Withdraw,
givesLifeness: true,
numCoinsTotal,
numCoinsWithdrawn,
@@ -294,7 +266,7 @@ async function gatherProposalPending(
return;
}
resp.pendingOperations.push({
- type: "proposal-choice",
+ type: PendingOperationType.ProposalChoice,
givesLifeness: false,
merchantBaseUrl: proposal.download!!.contractTerms.merchant_base_url,
proposalId: proposal.proposalId,
@@ -310,7 +282,7 @@ async function gatherProposalPending(
return;
}
resp.pendingOperations.push({
- type: "proposal-download",
+ type: PendingOperationType.ProposalDownload,
givesLifeness: true,
merchantBaseUrl: proposal.merchantBaseUrl,
orderId: proposal.orderId,
@@ -343,7 +315,7 @@ async function gatherTipPending(
}
if (tip.accepted) {
resp.pendingOperations.push({
- type: "tip",
+ type: PendingOperationType.TipPickup,
givesLifeness: true,
merchantBaseUrl: tip.merchantBaseUrl,
tipId: tip.tipId,
@@ -368,7 +340,7 @@ async function gatherPurchasePending(
);
if (!onlyDue || pr.payRetryInfo.nextRetry.t_ms <= now.t_ms) {
resp.pendingOperations.push({
- type: "pay",
+ type: PendingOperationType.Pay,
givesLifeness: true,
isReplay: false,
proposalId: pr.proposalId,
@@ -385,7 +357,7 @@ async function gatherPurchasePending(
);
if (!onlyDue || pr.refundStatusRetryInfo.nextRetry.t_ms <= now.t_ms) {
resp.pendingOperations.push({
- type: "refund-query",
+ type: PendingOperationType.RefundQuery,
givesLifeness: true,
proposalId: pr.proposalId,
retryInfo: pr.refundStatusRetryInfo,
@@ -403,7 +375,7 @@ async function gatherPurchasePending(
);
if (!onlyDue || pr.refundApplyRetryInfo.nextRetry.t_ms <= now.t_ms) {
resp.pendingOperations.push({
- type: "refund-apply",
+ type: PendingOperationType.RefundApply,
numRefundsDone,
numRefundsPending,
givesLifeness: true,
@@ -429,7 +401,7 @@ export async function getPendingOperations(
[
Stores.exchanges,
Stores.reserves,
- Stores.refresh,
+ Stores.refreshGroups,
Stores.coins,
Stores.withdrawalSession,
Stores.proposals,
@@ -440,7 +412,6 @@ export async function getPendingOperations(
await gatherExchangePending(tx, now, resp, onlyDue);
await gatherReservePending(tx, now, resp, onlyDue);
await gatherRefreshPending(tx, now, resp, onlyDue);
- await gatherCoinsPending(tx, now, resp, onlyDue);
await gatherWithdrawalPending(tx, now, resp, onlyDue);
await gatherProposalPending(tx, now, resp, onlyDue);
await gatherTipPending(tx, now, resp, onlyDue);
diff --git a/src/operations/refresh.ts b/src/operations/refresh.ts
index 4ffc3ea60..be23a5bb0 100644
--- a/src/operations/refresh.ts
+++ b/src/operations/refresh.ts
@@ -25,16 +25,24 @@ import {
RefreshSessionRecord,
initRetryInfo,
updateRetryInfoTimeout,
+ RefreshGroupRecord,
} from "../types/dbTypes";
import { amountToPretty } from "../util/helpers";
-import { Database } from "../util/query";
+import { Database, TransactionHandle } from "../util/query";
import { InternalWalletState } from "./state";
import { Logger } from "../util/logging";
import { getWithdrawDenomList } from "./withdraw";
import { updateExchangeFromUrl } from "./exchanges";
-import { getTimestampNow, OperationError } from "../types/walletTypes";
+import {
+ getTimestampNow,
+ OperationError,
+ CoinPublicKey,
+ RefreshReason,
+ RefreshGroupId,
+} from "../types/walletTypes";
import { guardOperationException } from "./errors";
import { NotificationType } from "../types/notifications";
+import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
const logger = new Logger("refresh.ts");
@@ -71,11 +79,130 @@ export function getTotalRefreshCost(
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.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;
+ await tx.put(Stores.refreshGroups, rg);
+ },
+ );
+ ws.notify({ type: NotificationType.RefreshRefused });
+ 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.valueWithFee);
+ 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,
- refreshSessionId: string,
+ refreshGroupId: string,
+ coinIndex: number,
): Promise<void> {
- const refreshSession = await ws.db.get(Stores.refresh, refreshSessionId);
+ const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
+ if (!refreshGroup) {
+ return;
+ }
+ const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];
if (!refreshSession) {
return;
}
@@ -122,7 +249,11 @@ async function refreshMelt(
refreshSession.norevealIndex = norevealIndex;
- await ws.db.mutate(Stores.refresh, refreshSessionId, rs => {
+ await ws.db.mutate(Stores.refreshGroups, refreshGroupId, rg => {
+ const rs = rg.refreshSessionPerCoin[coinIndex];
+ if (!rs) {
+ return;
+ }
if (rs.norevealIndex !== undefined) {
return;
}
@@ -130,7 +261,7 @@ async function refreshMelt(
return;
}
rs.norevealIndex = norevealIndex;
- return rs;
+ return rg;
});
ws.notify({
@@ -140,9 +271,14 @@ async function refreshMelt(
async function refreshReveal(
ws: InternalWalletState,
- refreshSessionId: string,
+ refreshGroupId: string,
+ coinIndex: number,
): Promise<void> {
- const refreshSession = await ws.db.get(Stores.refresh, refreshSessionId);
+ const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
+ if (!refreshGroup) {
+ return;
+ }
+ const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];
if (!refreshSession) {
return;
}
@@ -253,23 +389,38 @@ async function refreshReveal(
}
await ws.db.runWithWriteTransaction(
- [Stores.coins, Stores.refresh],
+ [Stores.coins, Stores.refreshGroups],
async tx => {
- const rs = await tx.get(Stores.refresh, refreshSessionId);
- if (!rs) {
+ 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();
- rs.retryInfo = initRetryInfo(false);
+ rg.finishedPerCoin[coinIndex] = true;
+ let allDone = true;
+ for (const f of rg.finishedPerCoin) {
+ if (!f) {
+ allDone = false;
+ break;
+ }
+ }
+ if (allDone) {
+ rg.finishedTimestamp = getTimestampNow();
+ rg.retryInfo = initRetryInfo(false);
+ }
for (let coin of coins) {
await tx.put(Stores.coins, coin);
}
- await tx.put(Stores.refresh, rs);
+ await tx.put(Stores.refreshGroups, rg);
},
);
console.log("refresh finished (end of reveal)");
@@ -280,11 +431,11 @@ async function refreshReveal(
async function incrementRefreshRetry(
ws: InternalWalletState,
- refreshSessionId: string,
+ refreshGroupId: string,
err: OperationError | undefined,
): Promise<void> {
- await ws.db.runWithWriteTransaction([Stores.refresh], async tx => {
- const r = await tx.get(Stores.refresh, refreshSessionId);
+ await ws.db.runWithWriteTransaction([Stores.refreshGroups], async tx => {
+ const r = await tx.get(Stores.refreshGroups, refreshGroupId);
if (!r) {
return;
}
@@ -294,31 +445,31 @@ async function incrementRefreshRetry(
r.retryInfo.retryCounter++;
updateRetryInfoTimeout(r.retryInfo);
r.lastError = err;
- await tx.put(Stores.refresh, r);
+ await tx.put(Stores.refreshGroups, r);
});
ws.notify({ type: NotificationType.RefreshOperationError });
}
-export async function processRefreshSession(
+export async function processRefreshGroup(
ws: InternalWalletState,
- refreshSessionId: string,
+ refreshGroupId: string,
forceNow: boolean = false,
-) {
- return ws.memoProcessRefresh.memo(refreshSessionId, async () => {
+): Promise<void> {
+ await ws.memoProcessRefresh.memo(refreshGroupId, async () => {
const onOpErr = (e: OperationError) =>
- incrementRefreshRetry(ws, refreshSessionId, e);
- return guardOperationException(
- () => processRefreshSessionImpl(ws, refreshSessionId, forceNow),
+ incrementRefreshRetry(ws, refreshGroupId, e);
+ return await guardOperationException(
+ async () => await processRefreshGroupImpl(ws, refreshGroupId, forceNow),
onOpErr,
);
});
}
-async function resetRefreshSessionRetry(
+async function resetRefreshGroupRetry(
ws: InternalWalletState,
refreshSessionId: string,
) {
- await ws.db.mutate(Stores.refresh, refreshSessionId, x => {
+ await ws.db.mutate(Stores.refreshGroups, refreshSessionId, x => {
if (x.retryInfo.active) {
x.retryInfo = initRetryInfo();
}
@@ -326,124 +477,87 @@ async function resetRefreshSessionRetry(
});
}
-async function processRefreshSessionImpl(
+async function processRefreshGroupImpl(
ws: InternalWalletState,
- refreshSessionId: string,
+ refreshGroupId: string,
forceNow: boolean,
) {
if (forceNow) {
- await resetRefreshSessionRetry(ws, refreshSessionId);
+ await resetRefreshGroupRetry(ws, refreshGroupId);
}
- const refreshSession = await ws.db.get(Stores.refresh, refreshSessionId);
- if (!refreshSession) {
+ const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
+ if (!refreshGroup) {
return;
}
- if (refreshSession.finishedTimestamp) {
+ if (refreshGroup.finishedTimestamp) {
return;
}
- if (typeof refreshSession.norevealIndex !== "number") {
- await refreshMelt(ws, refreshSession.refreshSessionId);
- }
- await refreshReveal(ws, refreshSession.refreshSessionId);
+ const ps = refreshGroup.oldCoinPubs.map((x, i) =>
+ processRefreshSession(ws, refreshGroupId, i),
+ );
+ await Promise.all(ps);
logger.trace("refresh finished");
}
-export async function refresh(
+async function processRefreshSession(
ws: InternalWalletState,
- oldCoinPub: string,
- force: boolean = false,
-): Promise<void> {
- const coin = await ws.db.get(Stores.coins, oldCoinPub);
- if (!coin) {
- console.warn("can't refresh, coin not in database");
+ refreshGroupId: string,
+ coinIndex: number,
+) {
+ logger.trace(`processing refresh session for coin ${coinIndex} of group ${refreshGroupId}`);
+ let refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
+ if (!refreshGroup) {
return;
}
- switch (coin.status) {
- case CoinStatus.Dirty:
- break;
- case CoinStatus.Dormant:
- return;
- case CoinStatus.Fresh:
- if (!force) {
- return;
- }
- break;
- }
-
- const exchange = await updateExchangeFromUrl(ws, coin.exchangeBaseUrl);
- if (!exchange) {
- throw Error("db inconsistent: exchange of coin not found");
+ if (refreshGroup.finishedPerCoin[coinIndex]) {
+ return;
}
-
- const oldDenom = await ws.db.get(Stores.denominations, [
- exchange.baseUrl,
- coin.denomPub,
- ]);
-
- if (!oldDenom) {
- throw Error("db inconsistent: denomination for coin not found");
+ if (!refreshGroup.refreshSessionPerCoin[coinIndex]) {
+ await refreshCreateSession(ws, refreshGroupId, coinIndex);
+ refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
+ if (!refreshGroup) {
+ return;
+ }
}
-
- 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.mutate(Stores.coins, oldCoinPub, x => {
- if (x.status != coin.status) {
- // Concurrent modification?
- return;
- }
- x.status = CoinStatus.Dormant;
- return x;
- });
- ws.notify({ type: NotificationType.RefreshRefused });
+ 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);
+}
- 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.refresh, Stores.coins],
- async tx => {
- const c = await tx.get(Stores.coins, coin.coinPub);
- if (!c) {
- return;
- }
- if (c.status !== CoinStatus.Dirty) {
- return;
- }
- const r = Amounts.sub(c.currentAmount, refreshSession.valueWithFee);
- if (r.saturated) {
- console.log("can't refresh coin, no amount left");
- return;
- }
- c.currentAmount = r.amount;
- c.status = CoinStatus.Dormant;
- await tx.put(Stores.refresh, refreshSession);
- await tx.put(Stores.coins, c);
- },
- );
- logger.info(`created refresh session ${refreshSession.refreshSessionId}`);
- ws.notify({ type: NotificationType.RefreshStarted });
+/**
+ * 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 = {
+ finishedTimestamp: undefined,
+ finishedPerCoin: oldCoinPubs.map(x => false),
+ lastError: undefined,
+ lastErrorPerCoin: oldCoinPubs.map(x => undefined),
+ oldCoinPubs: oldCoinPubs.map(x => x.coinPub),
+ reason,
+ refreshGroupId,
+ refreshSessionPerCoin: oldCoinPubs.map(x => undefined),
+ retryInfo: initRetryInfo(),
+ };
- await processRefreshSession(ws, refreshSession.refreshSessionId);
+ await tx.put(Stores.refreshGroups, refreshGroup);
+ return {
+ refreshGroupId,
+ };
}
diff --git a/src/operations/reserves.ts b/src/operations/reserves.ts
index 215d5ba7d..559d3ab08 100644
--- a/src/operations/reserves.ts
+++ b/src/operations/reserves.ts
@@ -458,10 +458,10 @@ async function processReserveImpl(
break;
case ReserveRecordStatus.REGISTERING_BANK:
await processReserveBankStatus(ws, reservePub);
- return processReserveImpl(ws, reservePub, true);
+ return await processReserveImpl(ws, reservePub, true);
case ReserveRecordStatus.QUERYING_STATUS:
await updateReserve(ws, reservePub);
- return processReserveImpl(ws, reservePub, true);
+ return await processReserveImpl(ws, reservePub, true);
case ReserveRecordStatus.WITHDRAWING:
await depleteReserve(ws, reservePub);
break;
diff --git a/src/operations/tip.ts b/src/operations/tip.ts
index f723374f9..f9953b513 100644
--- a/src/operations/tip.ts
+++ b/src/operations/tip.ts
@@ -15,7 +15,6 @@
*/
-import { Database } from "../util/query";
import { InternalWalletState } from "./state";
import { parseTipUri } from "../util/taleruri";
import { TipStatus, getTimestampNow, OperationError } from "../types/walletTypes";