diff options
Diffstat (limited to 'src/wallet-impl/withdraw.ts')
-rw-r--r-- | src/wallet-impl/withdraw.ts | 699 |
1 files changed, 0 insertions, 699 deletions
diff --git a/src/wallet-impl/withdraw.ts b/src/wallet-impl/withdraw.ts deleted file mode 100644 index d8b2b599c..000000000 --- a/src/wallet-impl/withdraw.ts +++ /dev/null @@ -1,699 +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 { AmountJson } from "../util/amounts"; -import { - DenominationRecord, - Stores, - DenominationStatus, - CoinStatus, - CoinRecord, - PlanchetRecord, - initRetryInfo, - updateRetryInfoTimeout, -} from "../dbTypes"; -import * as Amounts from "../util/amounts"; -import { - getTimestampNow, - AcceptWithdrawalResponse, - BankWithdrawDetails, - ExchangeWithdrawDetails, - WithdrawDetails, - OperationError, - NotificationType, -} from "../walletTypes"; -import { WithdrawOperationStatusResponse } from "../talerTypes"; -import { InternalWalletState } from "./state"; -import { parseWithdrawUri } from "../util/taleruri"; -import { Logger } from "../util/logging"; -import { - oneShotGet, - oneShotPut, - oneShotIterIndex, - oneShotGetIndexed, - runWithWriteTransaction, - oneShotMutate, -} from "../util/query"; -import { - updateExchangeFromUrl, - getExchangePaytoUri, - getExchangeTrust, -} from "./exchanges"; -import { createReserve, processReserveBankStatus } from "./reserves"; -import { WALLET_PROTOCOL_VERSION } from "../wallet"; - -import * as LibtoolVersion from "../util/libtoolVersion"; -import { guardOperationException } from "./errors"; - -const logger = new Logger("withdraw.ts"); - -function isWithdrawableDenom(d: DenominationRecord) { - const now = getTimestampNow(); - const started = now.t_ms >= d.stampStart.t_ms; - const stillOkay = d.stampExpireWithdraw.t_ms + 60 * 1000 > now.t_ms; - return started && stillOkay; -} - -/** - * Get a list of denominations (with repetitions possible) - * whose total value is as close as possible to the available - * amount, but never larger. - */ -export function getWithdrawDenomList( - amountAvailable: AmountJson, - denoms: DenominationRecord[], -): DenominationRecord[] { - let remaining = Amounts.copy(amountAvailable); - const ds: DenominationRecord[] = []; - - denoms = denoms.filter(isWithdrawableDenom); - denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value)); - - // This is an arbitrary number of coins - // we can withdraw in one go. It's not clear if this limit - // is useful ... - for (let i = 0; i < 1000; i++) { - let found = false; - for (const d of denoms) { - const cost = Amounts.add(d.value, d.feeWithdraw).amount; - if (Amounts.cmp(remaining, cost) < 0) { - continue; - } - found = true; - remaining = Amounts.sub(remaining, cost).amount; - ds.push(d); - break; - } - if (!found) { - break; - } - } - return ds; -} - -/** - * Get information about a withdrawal from - * a taler://withdraw URI by asking the bank. - */ -async function getBankWithdrawalInfo( - ws: InternalWalletState, - talerWithdrawUri: string, -): Promise<BankWithdrawDetails> { - const uriResult = parseWithdrawUri(talerWithdrawUri); - if (!uriResult) { - throw Error("can't parse URL"); - } - const resp = await ws.http.get(uriResult.statusUrl); - if (resp.status !== 200) { - throw Error(`unexpected status (${resp.status}) from bank for ${uriResult.statusUrl}`); - } - const respJson = await resp.json(); - console.log("resp:", respJson); - const status = WithdrawOperationStatusResponse.checked(respJson); - return { - amount: Amounts.parseOrThrow(status.amount), - confirmTransferUrl: status.confirm_transfer_url, - extractedStatusUrl: uriResult.statusUrl, - selectionDone: status.selection_done, - senderWire: status.sender_wire, - suggestedExchange: status.suggested_exchange, - transferDone: status.transfer_done, - wireTypes: status.wire_types, - }; -} - -export async function acceptWithdrawal( - ws: InternalWalletState, - talerWithdrawUri: string, - selectedExchange: string, -): Promise<AcceptWithdrawalResponse> { - const withdrawInfo = await getBankWithdrawalInfo(ws, talerWithdrawUri); - const exchangeWire = await getExchangePaytoUri( - ws, - selectedExchange, - withdrawInfo.wireTypes, - ); - const reserve = await createReserve(ws, { - amount: withdrawInfo.amount, - bankWithdrawStatusUrl: withdrawInfo.extractedStatusUrl, - exchange: selectedExchange, - senderWire: withdrawInfo.senderWire, - exchangeWire: exchangeWire, - }); - // We do this here, as the reserve should be registered before we return, - // so that we can redirect the user to the bank's status page. - await processReserveBankStatus(ws, reserve.reservePub); - console.log("acceptWithdrawal: returning"); - return { - reservePub: reserve.reservePub, - confirmTransferUrl: withdrawInfo.confirmTransferUrl, - }; -} - -async function getPossibleDenoms( - ws: InternalWalletState, - exchangeBaseUrl: string, -): Promise<DenominationRecord[]> { - return await oneShotIterIndex( - ws.db, - Stores.denominations.exchangeBaseUrlIndex, - exchangeBaseUrl, - ).filter(d => { - return ( - d.status === DenominationStatus.Unverified || - d.status === DenominationStatus.VerifiedGood - ); - }); -} - -/** - * Given a planchet, withdraw a coin from the exchange. - */ -async function processPlanchet( - ws: InternalWalletState, - withdrawalSessionId: string, - coinIdx: number, -): Promise<void> { - const withdrawalSession = await oneShotGet( - ws.db, - Stores.withdrawalSession, - withdrawalSessionId, - ); - if (!withdrawalSession) { - return; - } - if (withdrawalSession.withdrawn[coinIdx]) { - return; - } - if (withdrawalSession.source.type === "reserve") { - } - const planchet = withdrawalSession.planchets[coinIdx]; - if (!planchet) { - console.log("processPlanchet: planchet not found"); - return; - } - const exchange = await oneShotGet( - ws.db, - Stores.exchanges, - withdrawalSession.exchangeBaseUrl, - ); - if (!exchange) { - console.error("db inconsistent: exchange for planchet not found"); - return; - } - - const denom = await oneShotGet(ws.db, Stores.denominations, [ - withdrawalSession.exchangeBaseUrl, - planchet.denomPub, - ]); - - if (!denom) { - console.error("db inconsistent: denom for planchet not found"); - return; - } - - const wd: any = {}; - wd.denom_pub_hash = planchet.denomPubHash; - wd.reserve_pub = planchet.reservePub; - wd.reserve_sig = planchet.withdrawSig; - wd.coin_ev = planchet.coinEv; - const reqUrl = new URL("reserve/withdraw", exchange.baseUrl).href; - const resp = await ws.http.postJson(reqUrl, wd); - if (resp.status !== 200) { - throw Error(`unexpected status ${resp.status} for withdraw`); - } - - const r = await resp.json(); - - const denomSig = await ws.cryptoApi.rsaUnblind( - r.ev_sig, - planchet.blindingKey, - planchet.denomPub, - ); - - - const isValid = await ws.cryptoApi.rsaVerify(planchet.coinPub, denomSig, planchet.denomPub); - if (!isValid) { - throw Error("invalid RSA signature by the exchange"); - } - - const coin: CoinRecord = { - blindingKey: planchet.blindingKey, - coinPriv: planchet.coinPriv, - coinPub: planchet.coinPub, - currentAmount: planchet.coinValue, - denomPub: planchet.denomPub, - denomPubHash: planchet.denomPubHash, - denomSig, - exchangeBaseUrl: withdrawalSession.exchangeBaseUrl, - reservePub: planchet.reservePub, - status: CoinStatus.Fresh, - coinIndex: coinIdx, - withdrawSessionId: withdrawalSessionId, - }; - - let withdrawSessionFinished = false; - let reserveDepleted = false; - - const success = await runWithWriteTransaction( - ws.db, - [Stores.coins, Stores.withdrawalSession, Stores.reserves], - async tx => { - const ws = await tx.get(Stores.withdrawalSession, withdrawalSessionId); - if (!ws) { - return false; - } - if (ws.withdrawn[coinIdx]) { - // Already withdrawn - return false; - } - ws.withdrawn[coinIdx] = true; - ws.lastCoinErrors[coinIdx] = undefined; - let numDone = 0; - for (let i = 0; i < ws.withdrawn.length; i++) { - if (ws.withdrawn[i]) { - numDone++; - } - } - if (numDone === ws.denoms.length) { - ws.finishTimestamp = getTimestampNow(); - ws.lastError = undefined; - ws.retryInfo = initRetryInfo(false); - withdrawSessionFinished = true; - } - await tx.put(Stores.withdrawalSession, ws); - if (!planchet.isFromTip) { - const r = await tx.get(Stores.reserves, planchet.reservePub); - if (r) { - r.withdrawCompletedAmount = Amounts.add( - r.withdrawCompletedAmount, - Amounts.add(denom.value, denom.feeWithdraw).amount, - ).amount; - if (Amounts.cmp(r.withdrawCompletedAmount, r.withdrawAllocatedAmount) == 0) { - reserveDepleted = true; - } - await tx.put(Stores.reserves, r); - } - } - await tx.add(Stores.coins, coin); - return true; - }, - ); - - if (success) { - ws.notify( { - type: NotificationType.CoinWithdrawn, - } ); - } - - if (withdrawSessionFinished) { - ws.notify({ - type: NotificationType.WithdrawSessionFinished, - withdrawSessionId: withdrawalSessionId, - }); - } - - if (reserveDepleted && withdrawalSession.source.type === "reserve") { - ws.notify({ - type: NotificationType.ReserveDepleted, - reservePub: withdrawalSession.source.reservePub, - }); - } -} - -/** - * Get a list of denominations to withdraw from the given exchange for the - * given amount, making sure that all denominations' signatures are verified. - * - * Writes to the DB in order to record the result from verifying - * denominations. - */ -export async function getVerifiedWithdrawDenomList( - ws: InternalWalletState, - exchangeBaseUrl: string, - amount: AmountJson, -): Promise<DenominationRecord[]> { - const exchange = await oneShotGet(ws.db, Stores.exchanges, exchangeBaseUrl); - if (!exchange) { - console.log("exchange not found"); - throw Error(`exchange ${exchangeBaseUrl} not found`); - } - const exchangeDetails = exchange.details; - if (!exchangeDetails) { - console.log("exchange details not available"); - throw Error(`exchange ${exchangeBaseUrl} details not available`); - } - - console.log("getting possible denoms"); - - const possibleDenoms = await getPossibleDenoms(ws, exchange.baseUrl); - - console.log("got possible denoms"); - - let allValid = false; - - let selectedDenoms: DenominationRecord[]; - - do { - allValid = true; - const nextPossibleDenoms = []; - selectedDenoms = getWithdrawDenomList(amount, possibleDenoms); - console.log("got withdraw denom list"); - for (const denom of selectedDenoms || []) { - if (denom.status === DenominationStatus.Unverified) { - console.log( - "checking validity", - denom, - exchangeDetails.masterPublicKey, - ); - const valid = await ws.cryptoApi.isValidDenom( - denom, - exchangeDetails.masterPublicKey, - ); - console.log("done checking validity"); - if (!valid) { - denom.status = DenominationStatus.VerifiedBad; - allValid = false; - } else { - denom.status = DenominationStatus.VerifiedGood; - nextPossibleDenoms.push(denom); - } - await oneShotPut(ws.db, Stores.denominations, denom); - } else { - nextPossibleDenoms.push(denom); - } - } - } while (selectedDenoms.length > 0 && !allValid); - - console.log("returning denoms"); - - return selectedDenoms; -} - -async function makePlanchet( - ws: InternalWalletState, - withdrawalSessionId: string, - coinIndex: number, -): Promise<void> { - const withdrawalSession = await oneShotGet( - ws.db, - Stores.withdrawalSession, - withdrawalSessionId, - ); - if (!withdrawalSession) { - return; - } - const src = withdrawalSession.source; - if (src.type !== "reserve") { - throw Error("invalid state"); - } - const reserve = await oneShotGet(ws.db, Stores.reserves, src.reservePub); - if (!reserve) { - return; - } - const denom = await oneShotGet(ws.db, 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 runWithWriteTransaction(ws.db, [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 oneShotGet( - ws.db, - Stores.withdrawalSession, - withdrawalSessionId, - ); - if (!withdrawalSession) { - console.log("ws doesn't exist"); - return; - } - - const coin = await oneShotGetIndexed( - ws.db, - Stores.coins.byWithdrawalWithIdx, - [withdrawalSessionId, coinIndex], - ); - - 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, - err: OperationError | undefined, -): Promise<void> { - await runWithWriteTransaction(ws.db, [Stores.withdrawalSession], async tx => { - const wsr = await tx.get(Stores.withdrawalSession, withdrawalSessionId); - if (!wsr) { - return; - } - if (!wsr.retryInfo) { - return; - } - wsr.retryInfo.retryCounter++; - updateRetryInfoTimeout(wsr.retryInfo); - wsr.lastError = err; - await tx.put(Stores.withdrawalSession, wsr); - }); - ws.notify({ type: NotificationType.WithdrawOperationError }); -} - -export async function processWithdrawSession( - ws: InternalWalletState, - withdrawalSessionId: string, - forceNow: boolean = false, -): Promise<void> { - const onOpErr = (e: OperationError) => - incrementWithdrawalRetry(ws, withdrawalSessionId, e); - await guardOperationException( - () => processWithdrawSessionImpl(ws, withdrawalSessionId, forceNow), - onOpErr, - ); -} - -async function resetWithdrawSessionRetry( - ws: InternalWalletState, - withdrawalSessionId: string, -) { - await oneShotMutate(ws.db, Stores.withdrawalSession, withdrawalSessionId, (x) => { - if (x.retryInfo.active) { - x.retryInfo = initRetryInfo(); - } - return x; - }); -} - -async function processWithdrawSessionImpl( - ws: InternalWalletState, - withdrawalSessionId: string, - forceNow: boolean, -): Promise<void> { - logger.trace("processing withdraw session", withdrawalSessionId); - if (forceNow) { - await resetWithdrawSessionRetry(ws, withdrawalSessionId); - } - const withdrawalSession = await oneShotGet( - ws.db, - Stores.withdrawalSession, - withdrawalSessionId, - ); - if (!withdrawalSession) { - logger.trace("withdraw session doesn't exist"); - return; - } - - const ps = withdrawalSession.denoms.map((d, i) => - processWithdrawCoin(ws, withdrawalSessionId, i), - ); - await Promise.all(ps); - return; -} - -export async function getExchangeWithdrawalInfo( - ws: InternalWalletState, - baseUrl: string, - amount: AmountJson, -): Promise<ExchangeWithdrawDetails> { - const exchangeInfo = await updateExchangeFromUrl(ws, baseUrl); - const exchangeDetails = exchangeInfo.details; - if (!exchangeDetails) { - throw Error(`exchange ${exchangeInfo.baseUrl} details not available`); - } - const exchangeWireInfo = exchangeInfo.wireInfo; - if (!exchangeWireInfo) { - throw Error(`exchange ${exchangeInfo.baseUrl} wire details not available`); - } - - const selectedDenoms = await getVerifiedWithdrawDenomList( - ws, - baseUrl, - amount, - ); - let acc = Amounts.getZero(amount.currency); - for (const d of selectedDenoms) { - acc = Amounts.add(acc, d.feeWithdraw).amount; - } - const actualCoinCost = selectedDenoms - .map((d: DenominationRecord) => Amounts.add(d.value, d.feeWithdraw).amount) - .reduce((a, b) => Amounts.add(a, b).amount); - - const exchangeWireAccounts: string[] = []; - for (let account of exchangeWireInfo.accounts) { - exchangeWireAccounts.push(account.url); - } - - const { isTrusted, isAudited } = await getExchangeTrust(ws, exchangeInfo); - - let earliestDepositExpiration = selectedDenoms[0].stampExpireDeposit; - for (let i = 1; i < selectedDenoms.length; i++) { - const expireDeposit = selectedDenoms[i].stampExpireDeposit; - if (expireDeposit.t_ms < earliestDepositExpiration.t_ms) { - earliestDepositExpiration = expireDeposit; - } - } - - const possibleDenoms = await oneShotIterIndex( - ws.db, - Stores.denominations.exchangeBaseUrlIndex, - baseUrl, - ).filter(d => d.isOffered); - - const trustedAuditorPubs = []; - const currencyRecord = await oneShotGet( - ws.db, - Stores.currencies, - amount.currency, - ); - if (currencyRecord) { - trustedAuditorPubs.push(...currencyRecord.auditors.map(a => a.auditorPub)); - } - - let versionMatch; - if (exchangeDetails.protocolVersion) { - versionMatch = LibtoolVersion.compare( - WALLET_PROTOCOL_VERSION, - exchangeDetails.protocolVersion, - ); - - if ( - versionMatch && - !versionMatch.compatible && - versionMatch.currentCmp === -1 - ) { - console.warn( - `wallet version ${WALLET_PROTOCOL_VERSION} might be outdated ` + - `(exchange has ${exchangeDetails.protocolVersion}), checking for updates`, - ); - } - } - - let tosAccepted = false; - - if (exchangeInfo.termsOfServiceAcceptedTimestamp) { - if (exchangeInfo.termsOfServiceAcceptedEtag == exchangeInfo.termsOfServiceLastEtag) { - tosAccepted = true; - } - } - - const ret: ExchangeWithdrawDetails = { - earliestDepositExpiration, - exchangeInfo, - exchangeWireAccounts, - exchangeVersion: exchangeDetails.protocolVersion || "unknown", - isAudited, - isTrusted, - numOfferedDenoms: possibleDenoms.length, - overhead: Amounts.sub(amount, actualCoinCost).amount, - selectedDenoms, - trustedAuditorPubs, - versionMatch, - walletVersion: WALLET_PROTOCOL_VERSION, - wireFees: exchangeWireInfo, - withdrawFee: acc, - termsOfServiceAccepted: tosAccepted, - }; - return ret; -} - -export async function getWithdrawDetailsForUri( - ws: InternalWalletState, - talerWithdrawUri: string, - maybeSelectedExchange?: string, -): Promise<WithdrawDetails> { - const info = await getBankWithdrawalInfo(ws, talerWithdrawUri); - let rci: ExchangeWithdrawDetails | undefined = undefined; - if (maybeSelectedExchange) { - rci = await getExchangeWithdrawalInfo( - ws, - maybeSelectedExchange, - info.amount, - ); - } - return { - bankWithdrawDetails: info, - exchangeWithdrawDetails: rci, - }; -} |