diff options
Diffstat (limited to 'src/operations')
-rw-r--r-- | src/operations/balance.ts | 47 | ||||
-rw-r--r-- | src/operations/history.ts | 2 | ||||
-rw-r--r-- | src/operations/pay.ts | 20 | ||||
-rw-r--r-- | src/operations/pending.ts | 71 | ||||
-rw-r--r-- | src/operations/refresh.ts | 364 | ||||
-rw-r--r-- | src/operations/reserves.ts | 4 | ||||
-rw-r--r-- | src/operations/tip.ts | 1 |
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"; |