diff options
Diffstat (limited to 'src/wallet-impl/reserves.ts')
-rw-r--r-- | src/wallet-impl/reserves.ts | 630 |
1 files changed, 0 insertions, 630 deletions
diff --git a/src/wallet-impl/reserves.ts b/src/wallet-impl/reserves.ts deleted file mode 100644 index 504cf10f0..000000000 --- a/src/wallet-impl/reserves.ts +++ /dev/null @@ -1,630 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2019 GNUnet e.V. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -import { - CreateReserveRequest, - CreateReserveResponse, - getTimestampNow, - ConfirmReserveRequest, - OperationError, - NotificationType, -} from "../walletTypes"; -import { canonicalizeBaseUrl } from "../util/helpers"; -import { InternalWalletState } from "./state"; -import { - ReserveRecordStatus, - ReserveRecord, - CurrencyRecord, - Stores, - WithdrawalSessionRecord, - initRetryInfo, - updateRetryInfoTimeout, -} from "../dbTypes"; -import { - oneShotMutate, - oneShotPut, - oneShotGet, - runWithWriteTransaction, - TransactionAbort, -} from "../util/query"; -import { Logger } from "../util/logging"; -import * as Amounts from "../util/amounts"; -import { updateExchangeFromUrl, getExchangeTrust } from "./exchanges"; -import { WithdrawOperationStatusResponse, ReserveStatus } from "../talerTypes"; -import { assertUnreachable } from "../util/assertUnreachable"; -import { encodeCrock } from "../crypto/talerCrypto"; -import { randomBytes } from "../crypto/primitives/nacl-fast"; -import { - getVerifiedWithdrawDenomList, - processWithdrawSession, -} from "./withdraw"; -import { guardOperationException, OperationFailedAndReportedError } from "./errors"; - -const logger = new Logger("reserves.ts"); - -/** - * Create a reserve, but do not flag it as confirmed yet. - * - * Adds the corresponding exchange as a trusted exchange if it is neither - * audited nor trusted already. - */ -export async function createReserve( - ws: InternalWalletState, - req: CreateReserveRequest, -): Promise<CreateReserveResponse> { - const keypair = await ws.cryptoApi.createEddsaKeypair(); - const now = getTimestampNow(); - const canonExchange = canonicalizeBaseUrl(req.exchange); - - let reserveStatus; - if (req.bankWithdrawStatusUrl) { - reserveStatus = ReserveRecordStatus.REGISTERING_BANK; - } else { - reserveStatus = ReserveRecordStatus.UNCONFIRMED; - } - - const currency = req.amount.currency; - - const reserveRecord: ReserveRecord = { - created: now, - withdrawAllocatedAmount: Amounts.getZero(currency), - withdrawCompletedAmount: Amounts.getZero(currency), - withdrawRemainingAmount: Amounts.getZero(currency), - exchangeBaseUrl: canonExchange, - hasPayback: false, - initiallyRequestedAmount: req.amount, - reservePriv: keypair.priv, - reservePub: keypair.pub, - senderWire: req.senderWire, - timestampConfirmed: undefined, - timestampReserveInfoPosted: undefined, - bankWithdrawStatusUrl: req.bankWithdrawStatusUrl, - exchangeWire: req.exchangeWire, - reserveStatus, - lastSuccessfulStatusQuery: undefined, - retryInfo: initRetryInfo(), - lastError: undefined, - }; - - const senderWire = req.senderWire; - if (senderWire) { - const rec = { - paytoUri: senderWire, - }; - await oneShotPut(ws.db, Stores.senderWires, rec); - } - - const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange); - const exchangeDetails = exchangeInfo.details; - if (!exchangeDetails) { - console.log(exchangeDetails); - throw Error("exchange not updated"); - } - const { isAudited, isTrusted } = await getExchangeTrust(ws, exchangeInfo); - let currencyRecord = await oneShotGet( - ws.db, - Stores.currencies, - exchangeDetails.currency, - ); - if (!currencyRecord) { - currencyRecord = { - auditors: [], - exchanges: [], - fractionalDigits: 2, - name: exchangeDetails.currency, - }; - } - - if (!isAudited && !isTrusted) { - currencyRecord.exchanges.push({ - baseUrl: req.exchange, - exchangePub: exchangeDetails.masterPublicKey, - }); - } - - const cr: CurrencyRecord = currencyRecord; - - const resp = await runWithWriteTransaction( - ws.db, - [Stores.currencies, Stores.reserves, Stores.bankWithdrawUris], - async tx => { - // Check if we have already created a reserve for that bankWithdrawStatusUrl - if (reserveRecord.bankWithdrawStatusUrl) { - const bwi = await tx.get( - Stores.bankWithdrawUris, - reserveRecord.bankWithdrawStatusUrl, - ); - if (bwi) { - const otherReserve = await tx.get(Stores.reserves, bwi.reservePub); - if (otherReserve) { - logger.trace( - "returning existing reserve for bankWithdrawStatusUri", - ); - return { - exchange: otherReserve.exchangeBaseUrl, - reservePub: otherReserve.reservePub, - }; - } - } - await tx.put(Stores.bankWithdrawUris, { - reservePub: reserveRecord.reservePub, - talerWithdrawUri: reserveRecord.bankWithdrawStatusUrl, - }); - } - await tx.put(Stores.currencies, cr); - await tx.put(Stores.reserves, reserveRecord); - const r: CreateReserveResponse = { - exchange: canonExchange, - reservePub: keypair.pub, - }; - return r; - }, - ); - - ws.notify({ type: NotificationType.ReserveCreated }); - - // Asynchronously process the reserve, but return - // to the caller already. - processReserve(ws, resp.reservePub, true).catch(e => { - console.error("Processing reserve failed:", e); - }); - - return resp; -} - -/** - * First fetch information requred to withdraw from the reserve, - * then deplete the reserve, withdrawing coins until it is empty. - * - * The returned promise resolves once the reserve is set to the - * state DORMANT. - */ -export async function processReserve( - ws: InternalWalletState, - reservePub: string, - forceNow: boolean = false, -): Promise<void> { - return ws.memoProcessReserve.memo(reservePub, async () => { - const onOpError = (err: OperationError) => - incrementReserveRetry(ws, reservePub, err); - await guardOperationException( - () => processReserveImpl(ws, reservePub, forceNow), - onOpError, - ); - }); -} - - -async function registerReserveWithBank( - ws: InternalWalletState, - reservePub: string, -): Promise<void> { - let reserve = await oneShotGet(ws.db, Stores.reserves, reservePub); - switch (reserve?.reserveStatus) { - case ReserveRecordStatus.WAIT_CONFIRM_BANK: - case ReserveRecordStatus.REGISTERING_BANK: - break; - default: - return; - } - const bankStatusUrl = reserve.bankWithdrawStatusUrl; - if (!bankStatusUrl) { - return; - } - console.log("making selection"); - if (reserve.timestampReserveInfoPosted) { - throw Error("bank claims that reserve info selection is not done"); - } - const bankResp = await ws.http.postJson(bankStatusUrl, { - reserve_pub: reservePub, - selected_exchange: reserve.exchangeWire, - }); - console.log("got response", bankResp); - await oneShotMutate(ws.db, Stores.reserves, reservePub, r => { - switch (r.reserveStatus) { - case ReserveRecordStatus.REGISTERING_BANK: - case ReserveRecordStatus.WAIT_CONFIRM_BANK: - break; - default: - return; - } - r.timestampReserveInfoPosted = getTimestampNow(); - r.reserveStatus = ReserveRecordStatus.WAIT_CONFIRM_BANK; - r.retryInfo = initRetryInfo(); - return r; - }); - ws.notify( { type: NotificationType.Wildcard }); - return processReserveBankStatus(ws, reservePub); -} - -export async function processReserveBankStatus( - ws: InternalWalletState, - reservePub: string, -): Promise<void> { - const onOpError = (err: OperationError) => - incrementReserveRetry(ws, reservePub, err); - await guardOperationException( - () => processReserveBankStatusImpl(ws, reservePub), - onOpError, - ); -} - -async function processReserveBankStatusImpl( - ws: InternalWalletState, - reservePub: string, -): Promise<void> { - let reserve = await oneShotGet(ws.db, Stores.reserves, reservePub); - switch (reserve?.reserveStatus) { - case ReserveRecordStatus.WAIT_CONFIRM_BANK: - case ReserveRecordStatus.REGISTERING_BANK: - break; - default: - return; - } - const bankStatusUrl = reserve.bankWithdrawStatusUrl; - if (!bankStatusUrl) { - return; - } - - let status: WithdrawOperationStatusResponse; - try { - const statusResp = await ws.http.get(bankStatusUrl); - if (statusResp.status !== 200) { - throw Error(`unexpected status ${statusResp.status} for bank status query`); - } - status = WithdrawOperationStatusResponse.checked(await statusResp.json()); - } catch (e) { - throw e; - } - - ws.notify( { type: NotificationType.Wildcard }); - - if (status.selection_done) { - if (reserve.reserveStatus === ReserveRecordStatus.REGISTERING_BANK) { - await registerReserveWithBank(ws, reservePub); - return await processReserveBankStatus(ws, reservePub); - } - } else { - await registerReserveWithBank(ws, reservePub); - return await processReserveBankStatus(ws, reservePub); - } - - if (status.transfer_done) { - await oneShotMutate(ws.db, Stores.reserves, reservePub, r => { - switch (r.reserveStatus) { - case ReserveRecordStatus.REGISTERING_BANK: - case ReserveRecordStatus.WAIT_CONFIRM_BANK: - break; - default: - return; - } - const now = getTimestampNow(); - r.timestampConfirmed = now; - r.reserveStatus = ReserveRecordStatus.QUERYING_STATUS; - r.retryInfo = initRetryInfo(); - return r; - }); - await processReserveImpl(ws, reservePub, true); - } else { - await oneShotMutate(ws.db, Stores.reserves, reservePub, r => { - switch (r.reserveStatus) { - case ReserveRecordStatus.WAIT_CONFIRM_BANK: - break; - default: - return; - } - r.bankWithdrawConfirmUrl = status.confirm_transfer_url; - return r; - }); - await incrementReserveRetry(ws, reservePub, undefined); - } - ws.notify( { type: NotificationType.Wildcard }); -} - -async function incrementReserveRetry( - ws: InternalWalletState, - reservePub: string, - err: OperationError | undefined, -): Promise<void> { - await runWithWriteTransaction(ws.db, [Stores.reserves], async tx => { - const r = await tx.get(Stores.reserves, reservePub); - if (!r) { - return; - } - if (!r.retryInfo) { - return; - } - r.retryInfo.retryCounter++; - updateRetryInfoTimeout(r.retryInfo); - r.lastError = err; - await tx.put(Stores.reserves, r); - }); - ws.notify({ type: NotificationType.ReserveOperationError }); -} - -/** - * Update the information about a reserve that is stored in the wallet - * by quering the reserve's exchange. - */ -async function updateReserve( - ws: InternalWalletState, - reservePub: string, -): Promise<void> { - const reserve = await oneShotGet(ws.db, Stores.reserves, reservePub); - if (!reserve) { - throw Error("reserve not in db"); - } - - if (reserve.timestampConfirmed === undefined) { - throw Error("reserve not confirmed yet"); - } - - if (reserve.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) { - return; - } - - const reqUrl = new URL("reserve/status", reserve.exchangeBaseUrl); - reqUrl.searchParams.set("reserve_pub", reservePub); - let resp; - try { - resp = await ws.http.get(reqUrl.href); - if (resp.status === 404) { - const m = "The exchange does not know about this reserve (yet)."; - await incrementReserveRetry(ws, reservePub, undefined); - return; - } - if (resp.status !== 200) { - throw Error(`unexpected status code ${resp.status} for reserve/status`) - } - } catch (e) { - const m = e.message; - await incrementReserveRetry(ws, reservePub, { - type: "network", - details: {}, - message: m, - }); - throw new OperationFailedAndReportedError(m); - } - const reserveInfo = ReserveStatus.checked(await resp.json()); - const balance = Amounts.parseOrThrow(reserveInfo.balance); - await oneShotMutate(ws.db, Stores.reserves, reserve.reservePub, r => { - if (r.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) { - return; - } - - // FIXME: check / compare history! - if (!r.lastSuccessfulStatusQuery) { - // FIXME: check if this matches initial expectations - r.withdrawRemainingAmount = balance; - } else { - const expectedBalance = Amounts.sub( - r.withdrawAllocatedAmount, - r.withdrawCompletedAmount, - ); - const cmp = Amounts.cmp(balance, expectedBalance.amount); - if (cmp == 0) { - // Nothing changed. - return; - } - if (cmp > 0) { - const extra = Amounts.sub(balance, expectedBalance.amount).amount; - r.withdrawRemainingAmount = Amounts.add( - r.withdrawRemainingAmount, - extra, - ).amount; - } else { - // We're missing some money. - } - } - r.lastSuccessfulStatusQuery = getTimestampNow(); - r.reserveStatus = ReserveRecordStatus.WITHDRAWING; - r.retryInfo = initRetryInfo(); - return r; - }); - ws.notify( { type: NotificationType.ReserveUpdated }); -} - -async function processReserveImpl( - ws: InternalWalletState, - reservePub: string, - forceNow: boolean = false, -): Promise<void> { - const reserve = await oneShotGet(ws.db, Stores.reserves, reservePub); - if (!reserve) { - console.log("not processing reserve: reserve does not exist"); - return; - } - if (!forceNow) { - const now = getTimestampNow(); - if (reserve.retryInfo.nextRetry.t_ms > now.t_ms) { - logger.trace("processReserve retry not due yet"); - return; - } - } - logger.trace( - `Processing reserve ${reservePub} with status ${reserve.reserveStatus}`, - ); - switch (reserve.reserveStatus) { - case ReserveRecordStatus.UNCONFIRMED: - // nothing to do - break; - case ReserveRecordStatus.REGISTERING_BANK: - await processReserveBankStatus(ws, reservePub); - return processReserveImpl(ws, reservePub, true); - case ReserveRecordStatus.QUERYING_STATUS: - await updateReserve(ws, reservePub); - return processReserveImpl(ws, reservePub, true); - case ReserveRecordStatus.WITHDRAWING: - await depleteReserve(ws, reservePub); - break; - case ReserveRecordStatus.DORMANT: - // nothing to do - break; - case ReserveRecordStatus.WAIT_CONFIRM_BANK: - await processReserveBankStatus(ws, reservePub); - break; - default: - console.warn("unknown reserve record status:", reserve.reserveStatus); - assertUnreachable(reserve.reserveStatus); - break; - } -} - -export async function confirmReserve( - ws: InternalWalletState, - req: ConfirmReserveRequest, -): Promise<void> { - const now = getTimestampNow(); - await oneShotMutate(ws.db, Stores.reserves, req.reservePub, reserve => { - if (reserve.reserveStatus !== ReserveRecordStatus.UNCONFIRMED) { - return; - } - reserve.timestampConfirmed = now; - reserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS; - reserve.retryInfo = initRetryInfo(); - return reserve; - }); - - ws.notify({ type: NotificationType.ReserveUpdated }); - - processReserve(ws, req.reservePub, true).catch(e => { - console.log("processing reserve failed:", e); - }); -} - -/** - * Withdraw coins from a reserve until it is empty. - * - * When finished, marks the reserve as depleted by setting - * the depleted timestamp. - */ -async function depleteReserve( - ws: InternalWalletState, - reservePub: string, -): Promise<void> { - const reserve = await oneShotGet(ws.db, Stores.reserves, reservePub); - if (!reserve) { - return; - } - if (reserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) { - return; - } - logger.trace(`depleting reserve ${reservePub}`); - - const withdrawAmount = reserve.withdrawRemainingAmount; - - logger.trace(`getting denom list`); - - const denomsForWithdraw = await getVerifiedWithdrawDenomList( - ws, - reserve.exchangeBaseUrl, - withdrawAmount, - ); - logger.trace(`got denom list`); - if (denomsForWithdraw.length === 0) { - const m = `Unable to withdraw from reserve, no denominations are available to withdraw.`; - await incrementReserveRetry(ws, reserve.reservePub, { - type: "internal", - message: m, - details: {}, - }); - console.log(m); - throw new OperationFailedAndReportedError(m); - } - - logger.trace("selected denominations"); - - const withdrawalSessionId = encodeCrock(randomBytes(32)); - - const totalCoinValue = Amounts.sum(denomsForWithdraw.map(x => x.value)) - .amount; - - const withdrawalRecord: WithdrawalSessionRecord = { - withdrawSessionId: withdrawalSessionId, - exchangeBaseUrl: reserve.exchangeBaseUrl, - source: { - type: "reserve", - reservePub: reserve.reservePub, - }, - rawWithdrawalAmount: withdrawAmount, - startTimestamp: getTimestampNow(), - denoms: denomsForWithdraw.map(x => x.denomPub), - withdrawn: denomsForWithdraw.map(x => false), - planchets: denomsForWithdraw.map(x => undefined), - totalCoinValue, - retryInfo: initRetryInfo(), - lastCoinErrors: denomsForWithdraw.map(x => undefined), - lastError: undefined, - }; - - const totalCoinWithdrawFee = Amounts.sum( - denomsForWithdraw.map(x => x.feeWithdraw), - ).amount; - const totalWithdrawAmount = Amounts.add(totalCoinValue, totalCoinWithdrawFee) - .amount; - - function mutateReserve(r: ReserveRecord): ReserveRecord { - const remaining = Amounts.sub( - r.withdrawRemainingAmount, - totalWithdrawAmount, - ); - if (remaining.saturated) { - console.error("can't create planchets, saturated"); - throw TransactionAbort; - } - const allocated = Amounts.add( - r.withdrawAllocatedAmount, - totalWithdrawAmount, - ); - if (allocated.saturated) { - console.error("can't create planchets, saturated"); - throw TransactionAbort; - } - r.withdrawRemainingAmount = remaining.amount; - r.withdrawAllocatedAmount = allocated.amount; - r.reserveStatus = ReserveRecordStatus.DORMANT; - r.retryInfo = initRetryInfo(false); - return r; - } - - const success = await runWithWriteTransaction( - ws.db, - [Stores.withdrawalSession, Stores.reserves], - async tx => { - const myReserve = await tx.get(Stores.reserves, reservePub); - if (!myReserve) { - return false; - } - if (myReserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) { - return false; - } - await tx.mutate(Stores.reserves, reserve.reservePub, mutateReserve); - await tx.put(Stores.withdrawalSession, withdrawalRecord); - return true; - }, - ); - - if (success) { - console.log("processing new withdraw session"); - ws.notify({ - type: NotificationType.WithdrawSessionCreated, - withdrawSessionId: withdrawalSessionId, - }); - await processWithdrawSession(ws, withdrawalSessionId); - } else { - console.trace("withdraw session already existed"); - } -} |