diff options
author | Florian Dold <florian.dold@gmail.com> | 2020-04-02 20:33:01 +0530 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2020-04-02 20:33:01 +0530 |
commit | ef0acf06bfb7820a21c4719dba0d659f600be3c7 (patch) | |
tree | 216d34722240f682e5ee58632947fa75e9b4fcee /src/operations | |
parent | 1728e5011e16f226c6e3675aa196872af39f6c4e (diff) | |
download | wallet-core-ef0acf06bfb7820a21c4719dba0d659f600be3c7.tar.xz |
model reserve history in the exchange, improve reserve handling logic
Diffstat (limited to 'src/operations')
-rw-r--r-- | src/operations/balance.ts | 4 | ||||
-rw-r--r-- | src/operations/exchanges.ts | 2 | ||||
-rw-r--r-- | src/operations/history.ts | 46 | ||||
-rw-r--r-- | src/operations/pending.ts | 7 | ||||
-rw-r--r-- | src/operations/recoup.ts | 4 | ||||
-rw-r--r-- | src/operations/refresh.ts | 5 | ||||
-rw-r--r-- | src/operations/refund.ts | 6 | ||||
-rw-r--r-- | src/operations/reserves.ts | 235 | ||||
-rw-r--r-- | src/operations/tip.ts | 22 | ||||
-rw-r--r-- | src/operations/withdraw.ts | 202 |
10 files changed, 230 insertions, 303 deletions
diff --git a/src/operations/balance.ts b/src/operations/balance.ts index 8858e8b43..7c2d0e3fe 100644 --- a/src/operations/balance.ts +++ b/src/operations/balance.ts @@ -106,7 +106,7 @@ export async function getBalancesInsideTransaction( } }); - await tx.iter(Stores.withdrawalSession).forEach((wds) => { + await tx.iter(Stores.withdrawalGroups).forEach((wds) => { let w = wds.totalCoinValue; for (let i = 0; i < wds.planchets.length; i++) { if (wds.withdrawn[i]) { @@ -150,7 +150,7 @@ export async function getBalances( Stores.refreshGroups, Stores.reserves, Stores.purchases, - Stores.withdrawalSession, + Stores.withdrawalGroups, ], async (tx) => { return getBalancesInsideTransaction(ws, tx); diff --git a/src/operations/exchanges.ts b/src/operations/exchanges.ts index 3aaf77468..b9806bb62 100644 --- a/src/operations/exchanges.ts +++ b/src/operations/exchanges.ts @@ -53,7 +53,7 @@ async function denominationRecordFromKeys( exchangeBaseUrl: string, denomIn: Denomination, ): Promise<DenominationRecord> { - const denomPubHash = await ws.cryptoApi.hashDenomPub(denomIn.denom_pub); + const denomPubHash = await ws.cryptoApi.hashEncoded(denomIn.denom_pub); const d: DenominationRecord = { denomPub: denomIn.denom_pub, denomPubHash, diff --git a/src/operations/history.ts b/src/operations/history.ts index 1b4172526..848739334 100644 --- a/src/operations/history.ts +++ b/src/operations/history.ts @@ -26,7 +26,7 @@ import { PlanchetRecord, CoinRecord, } from "../types/dbTypes"; -import * as Amounts from "../util/amounts"; +import { Amounts } from "../util/amounts"; import { AmountJson } from "../util/amounts"; import { HistoryQuery, @@ -42,6 +42,7 @@ import { import { assertUnreachable } from "../util/assertUnreachable"; import { TransactionHandle, Store } from "../util/query"; import { timestampCmp } from "../util/time"; +import { summarizeReserveHistory } from "../util/reserveHistoryUtil"; /** * Create an event ID from the type and the primary key for the event. @@ -58,7 +59,7 @@ function getOrderShortInfo( return undefined; } return { - amount: Amounts.toString(download.contractData.amount), + amount: Amounts.stringify(download.contractData.amount), fulfillmentUrl: download.contractData.fulfillmentUrl, orderId: download.contractData.orderId, merchantBaseUrl: download.contractData.merchantBaseUrl, @@ -176,7 +177,7 @@ export async function getHistory( Stores.refreshGroups, Stores.reserves, Stores.tips, - Stores.withdrawalSession, + Stores.withdrawalGroups, Stores.payEvents, Stores.refundEvents, Stores.reserveUpdatedEvents, @@ -208,7 +209,7 @@ export async function getHistory( }); }); - tx.iter(Stores.withdrawalSession).forEach((wsr) => { + tx.iter(Stores.withdrawalGroups).forEach((wsr) => { if (wsr.timestampFinish) { const cs: PlanchetRecord[] = []; wsr.planchets.forEach((x) => { @@ -221,7 +222,7 @@ export async function getHistory( if (historyQuery?.extraDebug) { verboseDetails = { coins: cs.map((x) => ({ - value: Amounts.toString(x.coinValue), + value: Amounts.stringify(x.coinValue), denomPub: x.denomPub, })), }; @@ -229,13 +230,13 @@ export async function getHistory( history.push({ type: HistoryEventType.Withdrawn, - withdrawSessionId: wsr.withdrawSessionId, + withdrawalGroupId: wsr.withdrawalGroupId, eventId: makeEventId( HistoryEventType.Withdrawn, - wsr.withdrawSessionId, + wsr.withdrawalGroupId, ), - amountWithdrawnEffective: Amounts.toString(wsr.totalCoinValue), - amountWithdrawnRaw: Amounts.toString(wsr.rawWithdrawalAmount), + amountWithdrawnEffective: Amounts.stringify(wsr.totalCoinValue), + amountWithdrawnRaw: Amounts.stringify(wsr.rawWithdrawalAmount), exchangeBaseUrl: wsr.exchangeBaseUrl, timestamp: wsr.timestampFinish, withdrawalSource: wsr.source, @@ -283,7 +284,7 @@ export async function getHistory( coins.push({ contribution: x.contribution, denomPub: c.denomPub, - value: Amounts.toString(d.value), + value: Amounts.stringify(d.value), }); } verboseDetails = { coins }; @@ -301,7 +302,7 @@ export async function getHistory( sessionId: pe.sessionId, timestamp: pe.timestamp, numCoins: purchase.payReq.coins.length, - amountPaidWithFees: Amounts.toString(amountPaidWithFees), + amountPaidWithFees: Amounts.stringify(amountPaidWithFees), verboseDetails, }); }); @@ -364,7 +365,7 @@ export async function getHistory( } outputCoins.push({ denomPub: d.denomPub, - value: Amounts.toString(d.value), + value: Amounts.stringify(d.value), }); } } @@ -378,8 +379,8 @@ export async function getHistory( eventId: makeEventId(HistoryEventType.Refreshed, rg.refreshGroupId), timestamp: rg.timestampFinished, refreshReason: rg.reason, - amountRefreshedEffective: Amounts.toString(amountRefreshedEffective), - amountRefreshedRaw: Amounts.toString(amountRefreshedRaw), + amountRefreshedEffective: Amounts.stringify(amountRefreshedEffective), + amountRefreshedRaw: Amounts.stringify(amountRefreshedRaw), numInputCoins, numOutputCoins, numRefreshedInputCoins, @@ -403,21 +404,22 @@ export async function getHistory( type: ReserveType.Manual, }; } + const s = summarizeReserveHistory(reserve.reserveTransactions, reserve.currency); history.push({ type: HistoryEventType.ReserveBalanceUpdated, eventId: makeEventId( HistoryEventType.ReserveBalanceUpdated, ru.reserveUpdateId, ), - amountExpected: ru.amountExpected, - amountReserveBalance: ru.amountReserveBalance, timestamp: ru.timestamp, - newHistoryTransactions: ru.newHistoryTransactions, reserveShortInfo: { exchangeBaseUrl: reserve.exchangeBaseUrl, reserveCreationDetail, reservePub: reserve.reservePub, }, + reserveAwaitedAmount: Amounts.stringify(s.awaitedReserveAmount), + reserveBalance: Amounts.stringify(s.computedReserveBalance), + reserveUnclaimedAmount: Amounts.stringify(s.unclaimedReserveAmount), }); }); @@ -428,7 +430,7 @@ export async function getHistory( eventId: makeEventId(HistoryEventType.TipAccepted, tip.tipId), timestamp: tip.acceptedTimestamp, tipId: tip.tipId, - tipAmountRaw: Amounts.toString(tip.amount), + tipAmountRaw: Amounts.stringify(tip.amount), }); } }); @@ -488,9 +490,9 @@ export async function getHistory( refundGroupId: re.refundGroupId, orderShortInfo, timestamp: re.timestamp, - amountRefundedEffective: Amounts.toString(amountRefundedEffective), - amountRefundedRaw: Amounts.toString(amountRefundedRaw), - amountRefundedInvalid: Amounts.toString(amountRefundedInvalid), + amountRefundedEffective: Amounts.stringify(amountRefundedEffective), + amountRefundedRaw: Amounts.stringify(amountRefundedRaw), + amountRefundedInvalid: Amounts.stringify(amountRefundedInvalid), }); }); @@ -499,7 +501,7 @@ export async function getHistory( let verboseDetails: any = undefined; if (historyQuery?.extraDebug) { verboseDetails = { - oldAmountPerCoin: rg.oldAmountPerCoin.map(Amounts.toString), + oldAmountPerCoin: rg.oldAmountPerCoin.map(Amounts.stringify), }; } diff --git a/src/operations/pending.ts b/src/operations/pending.ts index adf47b151..b0bb9a7c3 100644 --- a/src/operations/pending.ts +++ b/src/operations/pending.ts @@ -243,7 +243,7 @@ async function gatherWithdrawalPending( resp: PendingOperationsResponse, onlyDue: boolean = false, ): Promise<void> { - await tx.iter(Stores.withdrawalSession).forEach((wsr) => { + await tx.iter(Stores.withdrawalGroups).forEach((wsr) => { if (wsr.timestampFinish) { return; } @@ -266,7 +266,8 @@ async function gatherWithdrawalPending( numCoinsTotal, numCoinsWithdrawn, source: wsr.source, - withdrawSessionId: wsr.withdrawSessionId, + withdrawalGroupId: wsr.withdrawalGroupId, + lastError: wsr.lastError, }); }); } @@ -444,7 +445,7 @@ export async function getPendingOperations( Stores.reserves, Stores.refreshGroups, Stores.coins, - Stores.withdrawalSession, + Stores.withdrawalGroups, Stores.proposals, Stores.tips, Stores.purchases, diff --git a/src/operations/recoup.ts b/src/operations/recoup.ts index 4c6eaf3b6..e13ae3c1f 100644 --- a/src/operations/recoup.ts +++ b/src/operations/recoup.ts @@ -42,7 +42,7 @@ import { codecForRecoupConfirmation } from "../types/talerTypes"; import { NotificationType } from "../types/notifications"; import { forceQueryReserve } from "./reserves"; -import * as Amounts from "../util/amounts"; +import { Amounts } from "../util/amounts"; import { createRefreshGroup, processRefreshGroup } from "./refresh"; import { RefreshReason, OperationError } from "../types/walletTypes"; import { TransactionHandle } from "../util/query"; @@ -266,7 +266,7 @@ async function recoupRefreshCoin( ).amount; console.log( "recoup: setting old coin amount to", - Amounts.toString(oldCoin.currentAmount), + Amounts.stringify(oldCoin.currentAmount), ); recoupGroup.scheduleRefreshCoins.push(oldCoin.coinPub); await tx.put(Stores.coins, revokedCoin); diff --git a/src/operations/refresh.ts b/src/operations/refresh.ts index 5628263ec..be4f5c5af 100644 --- a/src/operations/refresh.ts +++ b/src/operations/refresh.ts @@ -14,8 +14,7 @@ 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 { Amounts, AmountJson } from "../util/amounts"; import { DenominationRecord, Stores, @@ -239,7 +238,7 @@ async function refreshMelt( denom_pub_hash: coin.denomPubHash, denom_sig: coin.denomSig, rc: refreshSession.hash, - value_with_fee: Amounts.toString(refreshSession.amountRefreshInput), + value_with_fee: Amounts.stringify(refreshSession.amountRefreshInput), }; logger.trace(`melt request for coin:`, meltReq); const resp = await ws.http.postJson(reqUrl.href, meltReq); diff --git a/src/operations/refund.ts b/src/operations/refund.ts index 7552fc11c..f0fec4065 100644 --- a/src/operations/refund.ts +++ b/src/operations/refund.ts @@ -41,7 +41,7 @@ import { import { NotificationType } from "../types/notifications"; import { parseRefundUri } from "../util/taleruri"; import { createRefreshGroup, getTotalRefreshCost } from "./refresh"; -import * as Amounts from "../util/amounts"; +import { Amounts } from "../util/amounts"; import { MerchantRefundPermission, MerchantRefundResponse, @@ -476,7 +476,7 @@ async function processPurchaseApplyRefundImpl( `commiting refund ${perm.merchant_sig} to coin ${c.coinPub}`, ); logger.trace( - `coin amount before is ${Amounts.toString(c.currentAmount)}`, + `coin amount before is ${Amounts.stringify(c.currentAmount)}`, ); logger.trace(`refund amount (via merchant) is ${perm.refund_amount}`); logger.trace(`refund fee (via merchant) is ${perm.refund_fee}`); @@ -486,7 +486,7 @@ async function processPurchaseApplyRefundImpl( c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount; c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount; logger.trace( - `coin amount after is ${Amounts.toString(c.currentAmount)}`, + `coin amount after is ${Amounts.stringify(c.currentAmount)}`, ); await tx.put(Stores.coins, c); }; diff --git a/src/operations/reserves.ts b/src/operations/reserves.ts index 5cf189d3b..2ef902ef2 100644 --- a/src/operations/reserves.ts +++ b/src/operations/reserves.ts @@ -28,14 +28,17 @@ import { ReserveRecord, CurrencyRecord, Stores, - WithdrawalSessionRecord, + WithdrawalGroupRecord, initRetryInfo, updateRetryInfoTimeout, ReserveUpdatedEventRecord, + WalletReserveHistoryItemType, + DenominationRecord, + PlanchetRecord, + WithdrawalSourceType, } from "../types/dbTypes"; -import { TransactionAbort } from "../util/query"; import { Logger } from "../util/logging"; -import * as Amounts from "../util/amounts"; +import { Amounts } from "../util/amounts"; import { updateExchangeFromUrl, getExchangeTrust, @@ -50,7 +53,7 @@ import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto"; import { randomBytes } from "../crypto/primitives/nacl-fast"; import { getVerifiedWithdrawDenomList, - processWithdrawSession, + processWithdrawGroup, getBankWithdrawalInfo, } from "./withdraw"; import { @@ -61,6 +64,10 @@ import { import { NotificationType } from "../types/notifications"; import { codecForReserveStatus } from "../types/ReserveStatus"; import { getTimestampNow } from "../util/time"; +import { + reconcileReserveHistory, + summarizeReserveHistory, +} from "../util/reserveHistoryUtil"; const logger = new Logger("reserves.ts"); @@ -98,11 +105,7 @@ export async function createReserve( const reserveRecord: ReserveRecord = { timestampCreated: now, - amountWithdrawAllocated: Amounts.getZero(currency), - amountWithdrawCompleted: Amounts.getZero(currency), - amountWithdrawRemaining: Amounts.getZero(currency), exchangeBaseUrl: canonExchange, - amountInitiallyRequested: req.amount, reservePriv: keypair.priv, reservePub: keypair.pub, senderWire: req.senderWire, @@ -115,8 +118,14 @@ export async function createReserve( retryInfo: initRetryInfo(), lastError: undefined, reserveTransactions: [], + currency: req.amount.currency, }; + reserveRecord.reserveTransactions.push({ + type: WalletReserveHistoryItemType.Credit, + expectedAmount: req.amount, + }); + const senderWire = req.senderWire; if (senderWire) { const rec = { @@ -460,6 +469,7 @@ async function updateReserve( const respJson = await resp.json(); const reserveInfo = codecForReserveStatus().decode(respJson); const balance = Amounts.parseOrThrow(reserveInfo.balance); + const currency = balance.currency; await ws.db.runWithWriteTransaction( [Stores.reserves, Stores.reserveUpdatedEvents], async (tx) => { @@ -477,60 +487,41 @@ async function updateReserve( const reserveUpdateId = encodeCrock(getRandomBytes(32)); - // FIXME: check / compare history! - if (!r.lastSuccessfulStatusQuery) { - // FIXME: check if this matches initial expectations - r.amountWithdrawRemaining = balance; + const reconciled = reconcileReserveHistory( + r.reserveTransactions, + reserveInfo.history, + ); + + console.log("reconciled history:", JSON.stringify(reconciled, undefined, 2)); + + const summary = summarizeReserveHistory( + reconciled.updatedLocalHistory, + currency, + ); + console.log("summary", summary); + + if ( + reconciled.newAddedItems.length + reconciled.newMatchedItems.length != + 0 + ) { const reserveUpdate: ReserveUpdatedEventRecord = { reservePub: r.reservePub, timestamp: getTimestampNow(), - amountReserveBalance: Amounts.toString(balance), - amountExpected: Amounts.toString(reserve.amountInitiallyRequested), + amountReserveBalance: Amounts.stringify(balance), + amountExpected: Amounts.stringify(summary.awaitedReserveAmount), newHistoryTransactions, reserveUpdateId, }; await tx.put(Stores.reserveUpdatedEvents, reserveUpdate); r.reserveStatus = ReserveRecordStatus.WITHDRAWING; + r.retryInfo = initRetryInfo(); } else { - const expectedBalance = Amounts.add( - r.amountWithdrawRemaining, - Amounts.sub(r.amountWithdrawAllocated, r.amountWithdrawCompleted) - .amount, - ); - const cmp = Amounts.cmp(balance, expectedBalance.amount); - if (cmp == 0) { - // Nothing changed, go back to sleep! - r.reserveStatus = ReserveRecordStatus.DORMANT; - } else if (cmp > 0) { - const extra = Amounts.sub(balance, expectedBalance.amount).amount; - r.amountWithdrawRemaining = Amounts.add( - r.amountWithdrawRemaining, - extra, - ).amount; - r.reserveStatus = ReserveRecordStatus.WITHDRAWING; - } else { - // We're missing some money. - r.reserveStatus = ReserveRecordStatus.DORMANT; - } - if (r.reserveStatus !== ReserveRecordStatus.DORMANT) { - const reserveUpdate: ReserveUpdatedEventRecord = { - reservePub: r.reservePub, - timestamp: getTimestampNow(), - amountReserveBalance: Amounts.toString(balance), - amountExpected: Amounts.toString(expectedBalance.amount), - newHistoryTransactions, - reserveUpdateId, - }; - await tx.put(Stores.reserveUpdatedEvents, reserveUpdate); - } - } - r.lastSuccessfulStatusQuery = getTimestampNow(); - if (r.reserveStatus == ReserveRecordStatus.DORMANT) { + r.reserveStatus = ReserveRecordStatus.DORMANT; r.retryInfo = initRetryInfo(false); - } else { - r.retryInfo = initRetryInfo(); } - r.reserveTransactions = reserveInfo.history; + r.lastSuccessfulStatusQuery = getTimestampNow(); + r.reserveTransactions = reconciled.updatedLocalHistory; + r.lastError = undefined; await tx.put(Stores.reserves, r); }, ); @@ -607,6 +598,33 @@ export async function confirmReserve( }); } +async function makePlanchet( + ws: InternalWalletState, + reserve: ReserveRecord, + denom: DenominationRecord, +): Promise<PlanchetRecord> { + const r = await ws.cryptoApi.createPlanchet({ + denomPub: denom.denomPub, + feeWithdraw: denom.feeWithdraw, + reservePriv: reserve.reservePriv, + reservePub: reserve.reservePub, + value: denom.value, + }); + return { + blindingKey: r.blindingKey, + coinEv: r.coinEv, + coinPriv: r.coinPriv, + coinPub: r.coinPub, + coinValue: r.coinValue, + denomPub: r.denomPub, + denomPubHash: r.denomPubHash, + isFromTip: false, + reservePub: r.reservePub, + withdrawSig: r.withdrawSig, + coinEvHash: r.coinEvHash, + }; +} + /** * Withdraw coins from a reserve until it is empty. * @@ -626,7 +644,12 @@ async function depleteReserve( } logger.trace(`depleting reserve ${reservePub}`); - const withdrawAmount = reserve.amountWithdrawRemaining; + const summary = summarizeReserveHistory( + reserve.reserveTransactions, + reserve.currency, + ); + + const withdrawAmount = summary.unclaimedReserveAmount; logger.trace(`getting denom list`); @@ -637,36 +660,47 @@ async function depleteReserve( ); logger.trace(`got denom list`); if (denomsForWithdraw.length === 0) { - const m = `Unable to withdraw from reserve, no denominations are available to withdraw.`; - const opErr = { - type: "internal", - message: m, - details: {}, - }; - await incrementReserveRetry(ws, reserve.reservePub, opErr); - console.log(m); - throw new OperationFailedAndReportedError(opErr); + // Only complain about inability to withdraw if we + // didn't withdraw before. + if (Amounts.isZero(summary.withdrawnAmount)) { + const m = `Unable to withdraw from reserve, no denominations are available to withdraw.`; + const opErr = { + type: "internal", + message: m, + details: {}, + }; + await incrementReserveRetry(ws, reserve.reservePub, opErr); + console.log(m); + throw new OperationFailedAndReportedError(opErr); + } + return; } logger.trace("selected denominations"); - const withdrawalSessionId = encodeCrock(randomBytes(32)); + const withdrawalGroupId = encodeCrock(randomBytes(32)); const totalCoinValue = Amounts.sum(denomsForWithdraw.map((x) => x.value)) .amount; - const withdrawalRecord: WithdrawalSessionRecord = { - withdrawSessionId: withdrawalSessionId, + const planchets: PlanchetRecord[] = []; + for (const d of denomsForWithdraw) { + const p = await makePlanchet(ws, reserve, d); + planchets.push(p); + } + + const withdrawalRecord: WithdrawalGroupRecord = { + withdrawalGroupId: withdrawalGroupId, exchangeBaseUrl: reserve.exchangeBaseUrl, source: { - type: "reserve", + type: WithdrawalSourceType.Reserve, reservePub: reserve.reservePub, }, rawWithdrawalAmount: withdrawAmount, timestampStart: getTimestampNow(), denoms: denomsForWithdraw.map((x) => x.denomPub), withdrawn: denomsForWithdraw.map((x) => false), - planchets: denomsForWithdraw.map((x) => undefined), + planchets, totalCoinValue, retryInfo: initRetryInfo(), lastErrorPerCoin: {}, @@ -679,53 +713,54 @@ async function depleteReserve( const totalWithdrawAmount = Amounts.add(totalCoinValue, totalCoinWithdrawFee) .amount; - function mutateReserve(r: ReserveRecord): ReserveRecord { - const remaining = Amounts.sub( - r.amountWithdrawRemaining, - totalWithdrawAmount, - ); - if (remaining.saturated) { - console.error("can't create planchets, saturated"); - throw TransactionAbort; - } - const allocated = Amounts.add( - r.amountWithdrawAllocated, - totalWithdrawAmount, - ); - if (allocated.saturated) { - console.error("can't create planchets, saturated"); - throw TransactionAbort; - } - r.amountWithdrawRemaining = remaining.amount; - r.amountWithdrawAllocated = allocated.amount; - r.reserveStatus = ReserveRecordStatus.DORMANT; - r.retryInfo = initRetryInfo(false); - return r; - } - const success = await ws.db.runWithWriteTransaction( - [Stores.withdrawalSession, Stores.reserves], + [Stores.withdrawalGroups, Stores.reserves], async (tx) => { - const myReserve = await tx.get(Stores.reserves, reservePub); - if (!myReserve) { + const newReserve = await tx.get(Stores.reserves, reservePub); + if (!newReserve) { return false; } - if (myReserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) { + if (newReserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) { return false; } - await tx.mutate(Stores.reserves, reserve.reservePub, mutateReserve); - await tx.put(Stores.withdrawalSession, withdrawalRecord); + const newSummary = summarizeReserveHistory( + newReserve.reserveTransactions, + newReserve.currency, + ); + if ( + Amounts.cmp(newSummary.unclaimedReserveAmount, totalWithdrawAmount) < 0 + ) { + // Something must have happened concurrently! + logger.error( + "aborting withdrawal session, likely concurrent withdrawal happened", + ); + return false; + } + for (let i = 0; i < planchets.length; i++) { + const amt = Amounts.add( + denomsForWithdraw[i].value, + denomsForWithdraw[i].feeWithdraw, + ).amount; + newReserve.reserveTransactions.push({ + type: WalletReserveHistoryItemType.Withdraw, + expectedAmount: amt, + }); + } + newReserve.reserveStatus = ReserveRecordStatus.DORMANT; + newReserve.retryInfo = initRetryInfo(false); + await tx.put(Stores.reserves, newReserve); + await tx.put(Stores.withdrawalGroups, withdrawalRecord); return true; }, ); if (success) { - console.log("processing new withdraw session"); + console.log("processing new withdraw group"); ws.notify({ - type: NotificationType.WithdrawSessionCreated, - withdrawSessionId: withdrawalSessionId, + type: NotificationType.WithdrawGroupCreated, + withdrawalGroupId: withdrawalGroupId, }); - await processWithdrawSession(ws, withdrawalSessionId); + await processWithdrawGroup(ws, withdrawalGroupId); } else { console.trace("withdraw session already existed"); } diff --git a/src/operations/tip.ts b/src/operations/tip.ts index 3636dd247..d3c98d288 100644 --- a/src/operations/tip.ts +++ b/src/operations/tip.ts @@ -28,14 +28,15 @@ import * as Amounts from "../util/amounts"; import { Stores, PlanchetRecord, - WithdrawalSessionRecord, + WithdrawalGroupRecord, initRetryInfo, updateRetryInfoTimeout, + WithdrawalSourceType, } from "../types/dbTypes"; import { getExchangeWithdrawalInfo, getVerifiedWithdrawDenomList, - processWithdrawSession, + processWithdrawGroup, } from "./withdraw"; import { updateExchangeFromUrl } from "./exchanges"; import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto"; @@ -246,8 +247,10 @@ async function processTipImpl( const planchets: PlanchetRecord[] = []; + for (let i = 0; i < tipRecord.planchets.length; i++) { const tipPlanchet = tipRecord.planchets[i]; + const coinEvHash = await ws.cryptoApi.hashEncoded(tipPlanchet.coinEv); const planchet: PlanchetRecord = { blindingKey: tipPlanchet.blindingKey, coinEv: tipPlanchet.coinEv, @@ -259,22 +262,23 @@ async function processTipImpl( reservePub: response.reserve_pub, withdrawSig: response.reserve_sigs[i].reserve_sig, isFromTip: true, + coinEvHash, }; planchets.push(planchet); } - const withdrawalSessionId = encodeCrock(getRandomBytes(32)); + const withdrawalGroupId = encodeCrock(getRandomBytes(32)); - const withdrawalSession: WithdrawalSessionRecord = { + const withdrawalGroup: WithdrawalGroupRecord = { denoms: planchets.map((x) => x.denomPub), exchangeBaseUrl: tipRecord.exchangeUrl, planchets: planchets, source: { - type: "tip", + type: WithdrawalSourceType.Tip, tipId: tipRecord.tipId, }, timestampStart: getTimestampNow(), - withdrawSessionId: withdrawalSessionId, + withdrawalGroupId: withdrawalGroupId, rawWithdrawalAmount: tipRecord.amount, withdrawn: planchets.map((x) => false), totalCoinValue: Amounts.sum(planchets.map((p) => p.coinValue)).amount, @@ -285,7 +289,7 @@ async function processTipImpl( }; await ws.db.runWithWriteTransaction( - [Stores.tips, Stores.withdrawalSession], + [Stores.tips, Stores.withdrawalGroups], async (tx) => { const tr = await tx.get(Stores.tips, tipId); if (!tr) { @@ -298,11 +302,11 @@ async function processTipImpl( tr.retryInfo = initRetryInfo(false); await tx.put(Stores.tips, tr); - await tx.put(Stores.withdrawalSession, withdrawalSession); + await tx.put(Stores.withdrawalGroups, withdrawalGroup); }, ); - await processWithdrawSession(ws, withdrawalSessionId); + await processWithdrawGroup(ws, withdrawalGroupId); return; } diff --git a/src/operations/withdraw.ts b/src/operations/withdraw.ts index 4d8af9fc0..48d70db20 100644 --- a/src/operations/withdraw.ts +++ b/src/operations/withdraw.ts @@ -52,6 +52,7 @@ import { timestampCmp, timestampSubtractDuraction, } from "../util/time"; +import { summarizeReserveHistory, ReserveHistorySummary } from "../util/reserveHistoryUtil"; const logger = new Logger("withdraw.ts"); @@ -158,29 +159,29 @@ async function getPossibleDenoms( */ async function processPlanchet( ws: InternalWalletState, - withdrawalSessionId: string, + withdrawalGroupId: string, coinIdx: number, ): Promise<void> { - const withdrawalSession = await ws.db.get( - Stores.withdrawalSession, - withdrawalSessionId, + const withdrawalGroup = await ws.db.get( + Stores.withdrawalGroups, + withdrawalGroupId, ); - if (!withdrawalSession) { + if (!withdrawalGroup) { return; } - if (withdrawalSession.withdrawn[coinIdx]) { + if (withdrawalGroup.withdrawn[coinIdx]) { return; } - if (withdrawalSession.source.type === "reserve") { + if (withdrawalGroup.source.type === "reserve") { } - const planchet = withdrawalSession.planchets[coinIdx]; + const planchet = withdrawalGroup.planchets[coinIdx]; if (!planchet) { console.log("processPlanchet: planchet not found"); return; } const exchange = await ws.db.get( Stores.exchanges, - withdrawalSession.exchangeBaseUrl, + withdrawalGroup.exchangeBaseUrl, ); if (!exchange) { console.error("db inconsistent: exchange for planchet not found"); @@ -188,7 +189,7 @@ async function processPlanchet( } const denom = await ws.db.get(Stores.denominations, [ - withdrawalSession.exchangeBaseUrl, + withdrawalGroup.exchangeBaseUrl, planchet.denomPub, ]); @@ -232,24 +233,24 @@ async function processPlanchet( denomPub: planchet.denomPub, denomPubHash: planchet.denomPubHash, denomSig, - exchangeBaseUrl: withdrawalSession.exchangeBaseUrl, + exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl, status: CoinStatus.Fresh, coinSource: { type: CoinSourceType.Withdraw, coinIndex: coinIdx, reservePub: planchet.reservePub, - withdrawSessionId: withdrawalSessionId, + withdrawalGroupId: withdrawalGroupId, }, suspended: false, }; - let withdrawSessionFinished = false; - let reserveDepleted = false; + let withdrawalGroupFinished = false; + let summary: ReserveHistorySummary | undefined = undefined; const success = await ws.db.runWithWriteTransaction( - [Stores.coins, Stores.withdrawalSession, Stores.reserves], + [Stores.coins, Stores.withdrawalGroups, Stores.reserves], async (tx) => { - const ws = await tx.get(Stores.withdrawalSession, withdrawalSessionId); + const ws = await tx.get(Stores.withdrawalGroups, withdrawalGroupId); if (!ws) { return false; } @@ -269,23 +270,13 @@ async function processPlanchet( ws.timestampFinish = getTimestampNow(); ws.lastError = undefined; ws.retryInfo = initRetryInfo(false); - withdrawSessionFinished = true; + withdrawalGroupFinished = true; } - await tx.put(Stores.withdrawalSession, ws); + await tx.put(Stores.withdrawalGroups, ws); if (!planchet.isFromTip) { const r = await tx.get(Stores.reserves, planchet.reservePub); if (r) { - r.amountWithdrawCompleted = Amounts.add( - r.amountWithdrawCompleted, - Amounts.add(denom.value, denom.feeWithdraw).amount, - ).amount; - if ( - Amounts.cmp(r.amountWithdrawCompleted, r.amountWithdrawAllocated) == - 0 - ) { - reserveDepleted = true; - } - await tx.put(Stores.reserves, r); + summary = summarizeReserveHistory(r.reserveTransactions, r.currency); } } await tx.add(Stores.coins, coin); @@ -299,17 +290,10 @@ async function processPlanchet( }); } - if (withdrawSessionFinished) { + if (withdrawalGroupFinished) { ws.notify({ - type: NotificationType.WithdrawSessionFinished, - withdrawSessionId: withdrawalSessionId, - }); - } - - if (reserveDepleted && withdrawalSession.source.type === "reserve") { - ws.notify({ - type: NotificationType.ReserveDepleted, - reservePub: withdrawalSession.source.reservePub, + type: NotificationType.WithdrawGroupFinished, + withdrawalSource: withdrawalGroup.source, }); } } @@ -383,113 +367,15 @@ export async function getVerifiedWithdrawDenomList( return selectedDenoms; } -async function makePlanchet( - ws: InternalWalletState, - withdrawalSessionId: string, - coinIndex: number, -): Promise<void> { - const withdrawalSession = await ws.db.get( - Stores.withdrawalSession, - withdrawalSessionId, - ); - if (!withdrawalSession) { - return; - } - const src = withdrawalSession.source; - if (src.type !== "reserve") { - throw Error("invalid state"); - } - const reserve = await ws.db.get(Stores.reserves, src.reservePub); - if (!reserve) { - return; - } - const denom = await ws.db.get(Stores.denominations, [ - withdrawalSession.exchangeBaseUrl, - withdrawalSession.denoms[coinIndex], - ]); - if (!denom) { - return; - } - const r = await ws.cryptoApi.createPlanchet({ - denomPub: denom.denomPub, - feeWithdraw: denom.feeWithdraw, - reservePriv: reserve.reservePriv, - reservePub: reserve.reservePub, - value: denom.value, - }); - const newPlanchet: PlanchetRecord = { - blindingKey: r.blindingKey, - coinEv: r.coinEv, - coinPriv: r.coinPriv, - coinPub: r.coinPub, - coinValue: r.coinValue, - denomPub: r.denomPub, - denomPubHash: r.denomPubHash, - isFromTip: false, - reservePub: r.reservePub, - withdrawSig: r.withdrawSig, - }; - await ws.db.runWithWriteTransaction( - [Stores.withdrawalSession], - async (tx) => { - const myWs = await tx.get(Stores.withdrawalSession, withdrawalSessionId); - if (!myWs) { - return; - } - if (myWs.planchets[coinIndex]) { - return; - } - myWs.planchets[coinIndex] = newPlanchet; - await tx.put(Stores.withdrawalSession, myWs); - }, - ); -} - -async function processWithdrawCoin( - ws: InternalWalletState, - withdrawalSessionId: string, - coinIndex: number, -) { - logger.trace("starting withdraw for coin", coinIndex); - const withdrawalSession = await ws.db.get( - Stores.withdrawalSession, - withdrawalSessionId, - ); - if (!withdrawalSession) { - console.log("ws doesn't exist"); - return; - } - - const planchet = withdrawalSession.planchets[coinIndex]; - - if (planchet) { - const coin = await ws.db.get(Stores.coins, planchet.coinPub); - - if (coin) { - console.log("coin already exists"); - return; - } - } - - if (!withdrawalSession.planchets[coinIndex]) { - const key = `${withdrawalSessionId}-${coinIndex}`; - await ws.memoMakePlanchet.memo(key, async () => { - logger.trace("creating planchet for coin", coinIndex); - return makePlanchet(ws, withdrawalSessionId, coinIndex); - }); - } - await processPlanchet(ws, withdrawalSessionId, coinIndex); -} - async function incrementWithdrawalRetry( ws: InternalWalletState, - withdrawalSessionId: string, + withdrawalGroupId: string, err: OperationError | undefined, ): Promise<void> { await ws.db.runWithWriteTransaction( - [Stores.withdrawalSession], + [Stores.withdrawalGroups], async (tx) => { - const wsr = await tx.get(Stores.withdrawalSession, withdrawalSessionId); + const wsr = await tx.get(Stores.withdrawalGroups, withdrawalGroupId); if (!wsr) { return; } @@ -499,30 +385,30 @@ async function incrementWithdrawalRetry( wsr.retryInfo.retryCounter++; updateRetryInfoTimeout(wsr.retryInfo); wsr.lastError = err; - await tx.put(Stores.withdrawalSession, wsr); + await tx.put(Stores.withdrawalGroups, wsr); }, ); ws.notify({ type: NotificationType.WithdrawOperationError }); } -export async function processWithdrawSession( +export async function processWithdrawGroup( ws: InternalWalletState, - withdrawalSessionId: string, + withdrawalGroupId: string, forceNow: boolean = false, ): Promise<void> { const onOpErr = (e: OperationError) => - incrementWithdrawalRetry(ws, withdrawalSessionId, e); + incrementWithdrawalRetry(ws, withdrawalGroupId, e); await guardOperationException( - () => processWithdrawSessionImpl(ws, withdrawalSessionId, forceNow), + () => processWithdrawGroupImpl(ws, withdrawalGroupId, forceNow), onOpErr, ); } -async function resetWithdrawSessionRetry( +async function resetWithdrawalGroupRetry( ws: InternalWalletState, - withdrawalSessionId: string, + withdrawalGroupId: string, ) { - await ws.db.mutate(Stores.withdrawalSession, withdrawalSessionId, (x) => { + await ws.db.mutate(Stores.withdrawalGroups, withdrawalGroupId, (x) => { if (x.retryInfo.active) { x.retryInfo = initRetryInfo(); } @@ -530,26 +416,26 @@ async function resetWithdrawSessionRetry( }); } -async function processWithdrawSessionImpl( +async function processWithdrawGroupImpl( ws: InternalWalletState, - withdrawalSessionId: string, + withdrawalGroupId: string, forceNow: boolean, ): Promise<void> { - logger.trace("processing withdraw session", withdrawalSessionId); + logger.trace("processing withdraw group", withdrawalGroupId); if (forceNow) { - await resetWithdrawSessionRetry(ws, withdrawalSessionId); + await resetWithdrawalGroupRetry(ws, withdrawalGroupId); } - const withdrawalSession = await ws.db.get( - Stores.withdrawalSession, - withdrawalSessionId, + const withdrawalGroup = await ws.db.get( + Stores.withdrawalGroups, + withdrawalGroupId, ); - if (!withdrawalSession) { + if (!withdrawalGroup) { logger.trace("withdraw session doesn't exist"); return; } - const ps = withdrawalSession.denoms.map((d, i) => - processWithdrawCoin(ws, withdrawalSessionId, i), + const ps = withdrawalGroup.denoms.map((d, i) => + processPlanchet(ws, withdrawalGroupId, i), ); await Promise.all(ps); return; |