summaryrefslogtreecommitdiff
path: root/src/operations/withdraw.ts
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2019-12-12 20:53:15 +0100
committerFlorian Dold <florian.dold@gmail.com>2019-12-12 20:53:15 +0100
commit74433c3e05734aa1194049fcbcaa92c70ce61c74 (patch)
treed30e79c9ac3fd5720de628f6a9764354ec69c648 /src/operations/withdraw.ts
parentcc137c87394ec34d2f54d69fe896dfdf3feec5ea (diff)
downloadwallet-core-74433c3e05734aa1194049fcbcaa92c70ce61c74.tar.gz
wallet-core-74433c3e05734aa1194049fcbcaa92c70ce61c74.tar.bz2
wallet-core-74433c3e05734aa1194049fcbcaa92c70ce61c74.zip
refactor: re-structure type definitions
Diffstat (limited to 'src/operations/withdraw.ts')
-rw-r--r--src/operations/withdraw.ts699
1 files changed, 699 insertions, 0 deletions
diff --git a/src/operations/withdraw.ts b/src/operations/withdraw.ts
new file mode 100644
index 000000000..4ecc321f8
--- /dev/null
+++ b/src/operations/withdraw.ts
@@ -0,0 +1,699 @@
+/*
+ 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 "../types/dbTypes";
+import * as Amounts from "../util/amounts";
+import {
+ getTimestampNow,
+ AcceptWithdrawalResponse,
+ BankWithdrawDetails,
+ ExchangeWithdrawDetails,
+ WithdrawDetails,
+ OperationError,
+} from "../types/walletTypes";
+import { WithdrawOperationStatusResponse } from "../types/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";
+import { NotificationType } from "../types/notifications";
+
+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,
+ };
+}