2019-12-02 00:42:40 +01:00
|
|
|
/*
|
|
|
|
This file is part of GNU Taler
|
2020-05-15 12:33:52 +02:00
|
|
|
(C) 2019-2020 Taler Systems SA
|
2019-12-02 00:42:40 +01:00
|
|
|
|
|
|
|
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/>
|
|
|
|
*/
|
|
|
|
|
2020-05-11 14:33:25 +02:00
|
|
|
import { AmountJson, Amounts } from "../util/amounts";
|
2019-12-02 00:42:40 +01:00
|
|
|
import {
|
|
|
|
DenominationRecord,
|
|
|
|
Stores,
|
|
|
|
DenominationStatus,
|
|
|
|
CoinStatus,
|
|
|
|
CoinRecord,
|
2019-12-05 19:38:19 +01:00
|
|
|
initRetryInfo,
|
|
|
|
updateRetryInfoTimeout,
|
2020-03-11 20:14:28 +01:00
|
|
|
CoinSourceType,
|
2020-05-11 14:33:25 +02:00
|
|
|
DenominationSelectionInfo,
|
2020-05-11 17:13:19 +02:00
|
|
|
PlanchetRecord,
|
|
|
|
WithdrawalSourceType,
|
2020-05-15 12:33:52 +02:00
|
|
|
DenomSelectionState,
|
2019-12-12 20:53:15 +01:00
|
|
|
} from "../types/dbTypes";
|
2019-12-02 00:42:40 +01:00
|
|
|
import {
|
2019-12-09 19:59:08 +01:00
|
|
|
BankWithdrawDetails,
|
|
|
|
ExchangeWithdrawDetails,
|
2020-07-16 11:14:59 +02:00
|
|
|
WithdrawalDetailsResponse,
|
2020-07-22 10:52:03 +02:00
|
|
|
OperationErrorDetails,
|
2019-12-12 20:53:15 +01:00
|
|
|
} from "../types/walletTypes";
|
2020-03-12 14:55:38 +01:00
|
|
|
import {
|
|
|
|
codecForWithdrawOperationStatusResponse,
|
|
|
|
codecForWithdrawResponse,
|
|
|
|
} from "../types/talerTypes";
|
2019-12-02 00:42:40 +01:00
|
|
|
import { InternalWalletState } from "./state";
|
|
|
|
import { parseWithdrawUri } from "../util/taleruri";
|
|
|
|
import { Logger } from "../util/logging";
|
2019-12-19 20:42:49 +01:00
|
|
|
import { updateExchangeFromUrl, getExchangeTrust } from "./exchanges";
|
2019-12-16 16:59:09 +01:00
|
|
|
import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions";
|
2019-12-02 00:42:40 +01:00
|
|
|
|
|
|
|
import * as LibtoolVersion from "../util/libtoolVersion";
|
2020-07-20 14:16:49 +02:00
|
|
|
import { guardOperationException } from "./errors";
|
2019-12-12 20:53:15 +01:00
|
|
|
import { NotificationType } from "../types/notifications";
|
2019-12-19 20:42:49 +01:00
|
|
|
import {
|
|
|
|
getTimestampNow,
|
|
|
|
getDurationRemaining,
|
|
|
|
timestampCmp,
|
|
|
|
timestampSubtractDuraction,
|
|
|
|
} from "../util/time";
|
2020-07-22 10:52:03 +02:00
|
|
|
import { readSuccessResponseJsonOrThrow } from "../util/http";
|
2019-12-02 00:42:40 +01:00
|
|
|
|
|
|
|
const logger = new Logger("withdraw.ts");
|
|
|
|
|
2020-04-07 10:07:32 +02:00
|
|
|
function isWithdrawableDenom(d: DenominationRecord): boolean {
|
2019-12-02 00:42:40 +01:00
|
|
|
const now = getTimestampNow();
|
2019-12-19 20:42:49 +01:00
|
|
|
const started = timestampCmp(now, d.stampStart) >= 0;
|
|
|
|
const lastPossibleWithdraw = timestampSubtractDuraction(
|
|
|
|
d.stampExpireWithdraw,
|
|
|
|
{ d_ms: 50 * 1000 },
|
|
|
|
);
|
|
|
|
const remaining = getDurationRemaining(lastPossibleWithdraw, now);
|
|
|
|
const stillOkay = remaining.d_ms !== 0;
|
2020-03-12 14:55:38 +01:00
|
|
|
return started && stillOkay && !d.isRevoked;
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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[],
|
2020-05-11 14:33:25 +02:00
|
|
|
): DenominationSelectionInfo {
|
2019-12-02 00:42:40 +01:00
|
|
|
let remaining = Amounts.copy(amountAvailable);
|
2020-05-11 14:33:25 +02:00
|
|
|
|
|
|
|
const selectedDenoms: {
|
|
|
|
count: number;
|
|
|
|
denom: DenominationRecord;
|
|
|
|
}[] = [];
|
|
|
|
|
|
|
|
let totalCoinValue = Amounts.getZero(amountAvailable.currency);
|
|
|
|
let totalWithdrawCost = Amounts.getZero(amountAvailable.currency);
|
2019-12-02 00:42:40 +01:00
|
|
|
|
|
|
|
denoms = denoms.filter(isWithdrawableDenom);
|
|
|
|
denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value));
|
|
|
|
|
2020-05-11 14:33:25 +02:00
|
|
|
for (const d of denoms) {
|
|
|
|
let count = 0;
|
|
|
|
const cost = Amounts.add(d.value, d.feeWithdraw).amount;
|
|
|
|
for (;;) {
|
2019-12-02 00:42:40 +01:00
|
|
|
if (Amounts.cmp(remaining, cost) < 0) {
|
2020-05-11 14:33:25 +02:00
|
|
|
break;
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
remaining = Amounts.sub(remaining, cost).amount;
|
2020-05-11 14:33:25 +02:00
|
|
|
count++;
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
2020-05-11 14:33:25 +02:00
|
|
|
if (count > 0) {
|
|
|
|
totalCoinValue = Amounts.add(
|
|
|
|
totalCoinValue,
|
|
|
|
Amounts.mult(d.value, count).amount,
|
|
|
|
).amount;
|
2020-05-12 21:50:40 +02:00
|
|
|
totalWithdrawCost = Amounts.add(
|
|
|
|
totalWithdrawCost,
|
|
|
|
Amounts.mult(cost, count).amount,
|
|
|
|
).amount;
|
2020-05-11 14:33:25 +02:00
|
|
|
selectedDenoms.push({
|
|
|
|
count,
|
|
|
|
denom: d,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Amounts.isZero(remaining)) {
|
2019-12-02 00:42:40 +01:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2020-05-11 14:33:25 +02:00
|
|
|
|
|
|
|
return {
|
|
|
|
selectedDenoms,
|
|
|
|
totalCoinValue,
|
|
|
|
totalWithdrawCost,
|
|
|
|
};
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get information about a withdrawal from
|
2019-12-09 19:59:08 +01:00
|
|
|
* a taler://withdraw URI by asking the bank.
|
2019-12-02 00:42:40 +01:00
|
|
|
*/
|
2019-12-16 16:59:09 +01:00
|
|
|
export async function getBankWithdrawalInfo(
|
2019-12-02 00:42:40 +01:00
|
|
|
ws: InternalWalletState,
|
|
|
|
talerWithdrawUri: string,
|
2019-12-09 19:59:08 +01:00
|
|
|
): Promise<BankWithdrawDetails> {
|
2019-12-02 00:42:40 +01:00
|
|
|
const uriResult = parseWithdrawUri(talerWithdrawUri);
|
|
|
|
if (!uriResult) {
|
2019-12-20 11:35:51 +01:00
|
|
|
throw Error(`can't parse URL ${talerWithdrawUri}`);
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
2020-07-27 13:39:52 +02:00
|
|
|
const reqUrl = new URL(
|
2020-07-27 20:21:41 +02:00
|
|
|
`api/withdraw-operation/${uriResult.withdrawalOperationId}`,
|
2020-07-27 13:39:52 +02:00
|
|
|
uriResult.bankIntegrationApiBaseUrl,
|
|
|
|
);
|
|
|
|
const resp = await ws.http.get(reqUrl.href);
|
2020-07-22 10:52:03 +02:00
|
|
|
const status = await readSuccessResponseJsonOrThrow(
|
|
|
|
resp,
|
|
|
|
codecForWithdrawOperationStatusResponse(),
|
|
|
|
);
|
2019-12-19 20:42:49 +01:00
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
return {
|
|
|
|
amount: Amounts.parseOrThrow(status.amount),
|
|
|
|
confirmTransferUrl: status.confirm_transfer_url,
|
2020-07-27 13:39:52 +02:00
|
|
|
extractedStatusUrl: uriResult.bankIntegrationApiBaseUrl,
|
2019-12-02 00:42:40 +01:00
|
|
|
selectionDone: status.selection_done,
|
|
|
|
senderWire: status.sender_wire,
|
|
|
|
suggestedExchange: status.suggested_exchange,
|
|
|
|
transferDone: status.transfer_done,
|
|
|
|
wireTypes: status.wire_types,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-07-16 11:43:52 +02:00
|
|
|
/**
|
|
|
|
* Return denominations that can potentially used for a withdrawal.
|
|
|
|
*/
|
2019-12-02 00:42:40 +01:00
|
|
|
async function getPossibleDenoms(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
exchangeBaseUrl: string,
|
|
|
|
): Promise<DenominationRecord[]> {
|
2019-12-19 20:42:49 +01:00
|
|
|
return await ws.db
|
|
|
|
.iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeBaseUrl)
|
2020-03-30 12:39:32 +02:00
|
|
|
.filter((d) => {
|
2019-12-19 20:42:49 +01:00
|
|
|
return (
|
2020-03-12 14:55:38 +01:00
|
|
|
(d.status === DenominationStatus.Unverified ||
|
|
|
|
d.status === DenominationStatus.VerifiedGood) &&
|
|
|
|
!d.isRevoked
|
2019-12-19 20:42:49 +01:00
|
|
|
);
|
|
|
|
});
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given a planchet, withdraw a coin from the exchange.
|
|
|
|
*/
|
|
|
|
async function processPlanchet(
|
|
|
|
ws: InternalWalletState,
|
2020-04-02 17:03:01 +02:00
|
|
|
withdrawalGroupId: string,
|
2019-12-02 00:42:40 +01:00
|
|
|
coinIdx: number,
|
|
|
|
): Promise<void> {
|
2020-04-02 17:03:01 +02:00
|
|
|
const withdrawalGroup = await ws.db.get(
|
|
|
|
Stores.withdrawalGroups,
|
|
|
|
withdrawalGroupId,
|
2019-12-02 00:42:40 +01:00
|
|
|
);
|
2020-04-02 17:03:01 +02:00
|
|
|
if (!withdrawalGroup) {
|
2019-12-02 00:42:40 +01:00
|
|
|
return;
|
|
|
|
}
|
2020-05-11 17:35:00 +02:00
|
|
|
let planchet = await ws.db.getIndexed(Stores.planchets.byGroupAndIndex, [
|
2020-05-11 14:33:25 +02:00
|
|
|
withdrawalGroupId,
|
|
|
|
coinIdx,
|
|
|
|
]);
|
2019-12-02 00:42:40 +01:00
|
|
|
if (!planchet) {
|
2020-05-11 17:13:19 +02:00
|
|
|
let ci = 0;
|
|
|
|
let denomPubHash: string | undefined;
|
2020-05-12 21:50:40 +02:00
|
|
|
for (
|
|
|
|
let di = 0;
|
|
|
|
di < withdrawalGroup.denomsSel.selectedDenoms.length;
|
|
|
|
di++
|
|
|
|
) {
|
2020-05-11 17:13:19 +02:00
|
|
|
const d = withdrawalGroup.denomsSel.selectedDenoms[di];
|
|
|
|
if (coinIdx >= ci && coinIdx < ci + d.count) {
|
|
|
|
denomPubHash = d.denomPubHash;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
ci += d.count;
|
|
|
|
}
|
|
|
|
if (!denomPubHash) {
|
|
|
|
throw Error("invariant violated");
|
|
|
|
}
|
2020-05-12 21:50:40 +02:00
|
|
|
const denom = await ws.db.getIndexed(
|
|
|
|
Stores.denominations.denomPubHashIndex,
|
|
|
|
denomPubHash,
|
|
|
|
);
|
2020-05-11 17:13:19 +02:00
|
|
|
if (!denom) {
|
|
|
|
throw Error("invariant violated");
|
|
|
|
}
|
|
|
|
if (withdrawalGroup.source.type != WithdrawalSourceType.Reserve) {
|
|
|
|
throw Error("invariant violated");
|
|
|
|
}
|
2020-05-12 21:50:40 +02:00
|
|
|
const reserve = await ws.db.get(
|
|
|
|
Stores.reserves,
|
|
|
|
withdrawalGroup.source.reservePub,
|
|
|
|
);
|
2020-05-11 17:13:19 +02:00
|
|
|
if (!reserve) {
|
|
|
|
throw Error("invariant violated");
|
|
|
|
}
|
|
|
|
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,
|
|
|
|
coinEvHash: r.coinEvHash,
|
|
|
|
coinIdx,
|
|
|
|
coinPriv: r.coinPriv,
|
|
|
|
coinPub: r.coinPub,
|
|
|
|
coinValue: r.coinValue,
|
|
|
|
denomPub: r.denomPub,
|
|
|
|
denomPubHash: r.denomPubHash,
|
|
|
|
isFromTip: false,
|
|
|
|
reservePub: r.reservePub,
|
|
|
|
withdrawalDone: false,
|
|
|
|
withdrawSig: r.withdrawSig,
|
|
|
|
withdrawalGroupId: withdrawalGroupId,
|
|
|
|
};
|
|
|
|
await ws.db.runWithWriteTransaction([Stores.planchets], async (tx) => {
|
|
|
|
const p = await tx.getIndexed(Stores.planchets.byGroupAndIndex, [
|
|
|
|
withdrawalGroupId,
|
|
|
|
coinIdx,
|
|
|
|
]);
|
|
|
|
if (p) {
|
2020-05-11 17:35:00 +02:00
|
|
|
planchet = p;
|
2020-05-11 17:13:19 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
await tx.put(Stores.planchets, newPlanchet);
|
2020-05-11 17:35:00 +02:00
|
|
|
planchet = newPlanchet;
|
2020-05-11 17:13:19 +02:00
|
|
|
});
|
2020-05-11 17:35:00 +02:00
|
|
|
}
|
|
|
|
if (!planchet) {
|
|
|
|
throw Error("invariant violated");
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
2020-05-11 14:33:25 +02:00
|
|
|
if (planchet.withdrawalDone) {
|
2020-07-20 09:16:32 +02:00
|
|
|
logger.warn("processPlanchet: planchet already withdrawn");
|
2020-05-11 14:33:25 +02:00
|
|
|
return;
|
|
|
|
}
|
2019-12-12 22:39:45 +01:00
|
|
|
const exchange = await ws.db.get(
|
2019-12-02 00:42:40 +01:00
|
|
|
Stores.exchanges,
|
2020-04-02 17:03:01 +02:00
|
|
|
withdrawalGroup.exchangeBaseUrl,
|
2019-12-02 00:42:40 +01:00
|
|
|
);
|
|
|
|
if (!exchange) {
|
2020-07-20 09:16:32 +02:00
|
|
|
logger.error("db inconsistent: exchange for planchet not found");
|
2019-12-02 00:42:40 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-12-12 22:39:45 +01:00
|
|
|
const denom = await ws.db.get(Stores.denominations, [
|
2020-04-02 17:03:01 +02:00
|
|
|
withdrawalGroup.exchangeBaseUrl,
|
2019-12-02 00:42:40 +01:00
|
|
|
planchet.denomPub,
|
|
|
|
]);
|
|
|
|
|
|
|
|
if (!denom) {
|
|
|
|
console.error("db inconsistent: denom for planchet not found");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-05-12 21:50:40 +02:00
|
|
|
logger.trace(
|
|
|
|
`processing planchet #${coinIdx} in withdrawal ${withdrawalGroupId}`,
|
|
|
|
);
|
2020-05-11 17:13:19 +02:00
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
const wd: any = {};
|
|
|
|
wd.denom_pub_hash = planchet.denomPubHash;
|
|
|
|
wd.reserve_pub = planchet.reservePub;
|
|
|
|
wd.reserve_sig = planchet.withdrawSig;
|
|
|
|
wd.coin_ev = planchet.coinEv;
|
2020-03-12 14:55:38 +01:00
|
|
|
const reqUrl = new URL(
|
|
|
|
`reserves/${planchet.reservePub}/withdraw`,
|
|
|
|
exchange.baseUrl,
|
|
|
|
).href;
|
2020-07-20 14:16:49 +02:00
|
|
|
|
2020-07-22 10:52:03 +02:00
|
|
|
const resp = await ws.http.postJson(reqUrl, wd);
|
|
|
|
const r = await readSuccessResponseJsonOrThrow(
|
|
|
|
resp,
|
|
|
|
codecForWithdrawResponse(),
|
|
|
|
);
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2020-05-11 17:13:19 +02:00
|
|
|
logger.trace(`got response for /withdraw`);
|
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
const denomSig = await ws.cryptoApi.rsaUnblind(
|
|
|
|
r.ev_sig,
|
|
|
|
planchet.blindingKey,
|
|
|
|
planchet.denomPub,
|
|
|
|
);
|
|
|
|
|
2019-12-19 20:42:49 +01:00
|
|
|
const isValid = await ws.cryptoApi.rsaVerify(
|
|
|
|
planchet.coinPub,
|
|
|
|
denomSig,
|
|
|
|
planchet.denomPub,
|
|
|
|
);
|
2020-05-11 17:13:19 +02:00
|
|
|
|
2019-12-05 19:38:19 +01:00
|
|
|
if (!isValid) {
|
|
|
|
throw Error("invalid RSA signature by the exchange");
|
|
|
|
}
|
|
|
|
|
2020-05-11 17:13:19 +02:00
|
|
|
logger.trace(`unblinded and verified`);
|
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
const coin: CoinRecord = {
|
|
|
|
blindingKey: planchet.blindingKey,
|
|
|
|
coinPriv: planchet.coinPriv,
|
|
|
|
coinPub: planchet.coinPub,
|
|
|
|
currentAmount: planchet.coinValue,
|
|
|
|
denomPub: planchet.denomPub,
|
|
|
|
denomPubHash: planchet.denomPubHash,
|
|
|
|
denomSig,
|
2020-04-02 17:03:01 +02:00
|
|
|
exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl,
|
2019-12-02 00:42:40 +01:00
|
|
|
status: CoinStatus.Fresh,
|
2020-03-11 20:14:28 +01:00
|
|
|
coinSource: {
|
|
|
|
type: CoinSourceType.Withdraw,
|
|
|
|
coinIndex: coinIdx,
|
|
|
|
reservePub: planchet.reservePub,
|
2020-04-02 17:03:01 +02:00
|
|
|
withdrawalGroupId: withdrawalGroupId,
|
2020-03-12 14:55:38 +01:00
|
|
|
},
|
2020-03-24 10:55:04 +01:00
|
|
|
suspended: false,
|
2019-12-02 00:42:40 +01:00
|
|
|
};
|
|
|
|
|
2020-04-02 17:03:01 +02:00
|
|
|
let withdrawalGroupFinished = false;
|
2019-12-05 19:38:19 +01:00
|
|
|
|
2020-05-11 17:35:00 +02:00
|
|
|
const planchetCoinPub = planchet.coinPub;
|
|
|
|
|
2019-12-12 22:39:45 +01:00
|
|
|
const success = await ws.db.runWithWriteTransaction(
|
2020-05-11 14:33:25 +02:00
|
|
|
[Stores.coins, Stores.withdrawalGroups, Stores.reserves, Stores.planchets],
|
2020-03-30 12:39:32 +02:00
|
|
|
async (tx) => {
|
2020-04-02 17:03:01 +02:00
|
|
|
const ws = await tx.get(Stores.withdrawalGroups, withdrawalGroupId);
|
2019-12-02 00:42:40 +01:00
|
|
|
if (!ws) {
|
2019-12-06 02:52:16 +01:00
|
|
|
return false;
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
2020-05-11 17:35:00 +02:00
|
|
|
const p = await tx.get(Stores.planchets, planchetCoinPub);
|
2020-05-11 14:33:25 +02:00
|
|
|
if (!p) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (p.withdrawalDone) {
|
2019-12-02 00:42:40 +01:00
|
|
|
// Already withdrawn
|
2019-12-06 02:52:16 +01:00
|
|
|
return false;
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
2020-05-11 14:33:25 +02:00
|
|
|
p.withdrawalDone = true;
|
|
|
|
await tx.put(Stores.planchets, p);
|
|
|
|
|
2020-05-11 17:13:19 +02:00
|
|
|
let numTotal = 0;
|
|
|
|
|
|
|
|
for (const ds of ws.denomsSel.selectedDenoms) {
|
|
|
|
numTotal += ds.count;
|
|
|
|
}
|
|
|
|
|
|
|
|
let numDone = 0;
|
2020-05-11 14:33:25 +02:00
|
|
|
|
2020-05-12 21:50:40 +02:00
|
|
|
await tx
|
|
|
|
.iterIndexed(Stores.planchets.byGroup, withdrawalGroupId)
|
|
|
|
.forEach((x) => {
|
|
|
|
if (x.withdrawalDone) {
|
|
|
|
numDone++;
|
|
|
|
}
|
|
|
|
});
|
2020-05-11 14:33:25 +02:00
|
|
|
|
2020-05-11 17:13:19 +02:00
|
|
|
if (numDone > numTotal) {
|
2020-05-12 21:50:40 +02:00
|
|
|
throw Error(
|
|
|
|
"invariant violated (created more planchets than expected)",
|
|
|
|
);
|
2020-05-11 17:13:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (numDone == numTotal) {
|
2019-12-16 16:20:45 +01:00
|
|
|
ws.timestampFinish = getTimestampNow();
|
2019-12-05 22:17:01 +01:00
|
|
|
ws.lastError = undefined;
|
2019-12-05 19:38:19 +01:00
|
|
|
ws.retryInfo = initRetryInfo(false);
|
2020-04-02 17:03:01 +02:00
|
|
|
withdrawalGroupFinished = true;
|
2019-12-05 19:38:19 +01:00
|
|
|
}
|
2020-04-02 17:03:01 +02:00
|
|
|
await tx.put(Stores.withdrawalGroups, ws);
|
2019-12-02 00:42:40 +01:00
|
|
|
await tx.add(Stores.coins, coin);
|
2019-12-06 02:52:16 +01:00
|
|
|
return true;
|
2019-12-02 00:42:40 +01:00
|
|
|
},
|
|
|
|
);
|
2019-12-05 19:38:19 +01:00
|
|
|
|
2020-05-11 17:13:19 +02:00
|
|
|
logger.trace(`withdrawal result stored in DB`);
|
|
|
|
|
2019-12-06 02:52:16 +01:00
|
|
|
if (success) {
|
2019-12-19 20:42:49 +01:00
|
|
|
ws.notify({
|
2019-12-06 02:52:16 +01:00
|
|
|
type: NotificationType.CoinWithdrawn,
|
2019-12-19 20:42:49 +01:00
|
|
|
});
|
2019-12-06 02:52:16 +01:00
|
|
|
}
|
|
|
|
|
2020-04-02 17:03:01 +02:00
|
|
|
if (withdrawalGroupFinished) {
|
2019-12-05 19:38:19 +01:00
|
|
|
ws.notify({
|
2020-04-02 17:03:01 +02:00
|
|
|
type: NotificationType.WithdrawGroupFinished,
|
|
|
|
withdrawalSource: withdrawalGroup.source,
|
2019-12-05 19:38:19 +01:00
|
|
|
});
|
|
|
|
}
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
2020-06-03 13:16:25 +02:00
|
|
|
export function denomSelectionInfoToState(
|
|
|
|
dsi: DenominationSelectionInfo,
|
|
|
|
): DenomSelectionState {
|
2020-05-15 12:33:52 +02:00
|
|
|
return {
|
|
|
|
selectedDenoms: dsi.selectedDenoms.map((x) => {
|
|
|
|
return {
|
|
|
|
count: x.count,
|
2020-06-03 13:16:25 +02:00
|
|
|
denomPubHash: x.denom.denomPubHash,
|
2020-05-15 12:33:52 +02:00
|
|
|
};
|
|
|
|
}),
|
|
|
|
totalCoinValue: dsi.totalCoinValue,
|
|
|
|
totalWithdrawCost: dsi.totalWithdrawCost,
|
2020-06-03 13:16:25 +02:00
|
|
|
};
|
2020-05-15 12:33:52 +02:00
|
|
|
}
|
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2020-05-15 12:33:52 +02:00
|
|
|
export async function selectWithdrawalDenoms(
|
2019-12-02 00:42:40 +01:00
|
|
|
ws: InternalWalletState,
|
|
|
|
exchangeBaseUrl: string,
|
|
|
|
amount: AmountJson,
|
2020-05-11 14:33:25 +02:00
|
|
|
): Promise<DenominationSelectionInfo> {
|
2019-12-12 22:39:45 +01:00
|
|
|
const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
|
2019-12-02 00:42:40 +01:00
|
|
|
if (!exchange) {
|
2020-07-20 09:16:32 +02:00
|
|
|
logger.error("exchange not found");
|
2019-12-02 00:42:40 +01:00
|
|
|
throw Error(`exchange ${exchangeBaseUrl} not found`);
|
|
|
|
}
|
|
|
|
const exchangeDetails = exchange.details;
|
|
|
|
if (!exchangeDetails) {
|
2020-07-20 09:16:32 +02:00
|
|
|
logger.error("exchange details not available");
|
2019-12-02 00:42:40 +01:00
|
|
|
throw Error(`exchange ${exchangeBaseUrl} details not available`);
|
|
|
|
}
|
|
|
|
|
|
|
|
let allValid = false;
|
2020-05-11 14:33:25 +02:00
|
|
|
let selectedDenoms: DenominationSelectionInfo;
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2020-07-16 11:43:52 +02:00
|
|
|
// Find a denomination selection for the requested amount.
|
|
|
|
// If a selected denomination has not been validated yet
|
|
|
|
// and turns our to be invalid, we try again with the
|
|
|
|
// reduced set of denominations.
|
2019-12-02 00:42:40 +01:00
|
|
|
do {
|
|
|
|
allValid = true;
|
2020-07-16 11:43:52 +02:00
|
|
|
const nextPossibleDenoms = await getPossibleDenoms(ws, exchange.baseUrl);
|
|
|
|
selectedDenoms = getWithdrawDenomList(amount, nextPossibleDenoms);
|
2020-05-11 14:33:25 +02:00
|
|
|
for (const denomSel of selectedDenoms.selectedDenoms) {
|
|
|
|
const denom = denomSel.denom;
|
2019-12-02 00:42:40 +01:00
|
|
|
if (denom.status === DenominationStatus.Unverified) {
|
|
|
|
const valid = await ws.cryptoApi.isValidDenom(
|
|
|
|
denom,
|
|
|
|
exchangeDetails.masterPublicKey,
|
|
|
|
);
|
|
|
|
if (!valid) {
|
|
|
|
denom.status = DenominationStatus.VerifiedBad;
|
|
|
|
allValid = false;
|
|
|
|
} else {
|
|
|
|
denom.status = DenominationStatus.VerifiedGood;
|
|
|
|
}
|
2019-12-12 22:39:45 +01:00
|
|
|
await ws.db.put(Stores.denominations, denom);
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
}
|
2020-05-11 14:33:25 +02:00
|
|
|
} while (selectedDenoms.selectedDenoms.length > 0 && !allValid);
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2020-07-16 13:52:03 +02:00
|
|
|
if (Amounts.cmp(selectedDenoms.totalWithdrawCost, amount) > 0) {
|
|
|
|
throw Error("Bug: withdrawal coin selection is wrong");
|
|
|
|
}
|
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
return selectedDenoms;
|
|
|
|
}
|
|
|
|
|
2019-12-05 19:38:19 +01:00
|
|
|
async function incrementWithdrawalRetry(
|
|
|
|
ws: InternalWalletState,
|
2020-04-02 17:03:01 +02:00
|
|
|
withdrawalGroupId: string,
|
2020-07-22 10:52:03 +02:00
|
|
|
err: OperationErrorDetails | undefined,
|
2019-12-05 19:38:19 +01:00
|
|
|
): Promise<void> {
|
2020-04-02 17:14:12 +02:00
|
|
|
await ws.db.runWithWriteTransaction([Stores.withdrawalGroups], async (tx) => {
|
|
|
|
const wsr = await tx.get(Stores.withdrawalGroups, withdrawalGroupId);
|
|
|
|
if (!wsr) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!wsr.retryInfo) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
wsr.retryInfo.retryCounter++;
|
|
|
|
updateRetryInfoTimeout(wsr.retryInfo);
|
|
|
|
wsr.lastError = err;
|
|
|
|
await tx.put(Stores.withdrawalGroups, wsr);
|
|
|
|
});
|
2020-07-20 12:50:32 +02:00
|
|
|
if (err) {
|
|
|
|
ws.notify({ type: NotificationType.WithdrawOperationError, error: err });
|
|
|
|
}
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
2020-04-02 17:03:01 +02:00
|
|
|
export async function processWithdrawGroup(
|
2019-12-02 00:42:40 +01:00
|
|
|
ws: InternalWalletState,
|
2020-04-02 17:03:01 +02:00
|
|
|
withdrawalGroupId: string,
|
2020-04-06 17:45:41 +02:00
|
|
|
forceNow = false,
|
2019-12-05 19:38:19 +01:00
|
|
|
): Promise<void> {
|
2020-07-22 10:52:03 +02:00
|
|
|
const onOpErr = (e: OperationErrorDetails): Promise<void> =>
|
2020-04-02 17:03:01 +02:00
|
|
|
incrementWithdrawalRetry(ws, withdrawalGroupId, e);
|
2019-12-05 19:38:19 +01:00
|
|
|
await guardOperationException(
|
2020-04-02 17:03:01 +02:00
|
|
|
() => processWithdrawGroupImpl(ws, withdrawalGroupId, forceNow),
|
2019-12-05 19:38:19 +01:00
|
|
|
onOpErr,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-04-02 17:03:01 +02:00
|
|
|
async function resetWithdrawalGroupRetry(
|
2019-12-05 19:38:19 +01:00
|
|
|
ws: InternalWalletState,
|
2020-04-02 17:03:01 +02:00
|
|
|
withdrawalGroupId: string,
|
2020-04-07 10:07:32 +02:00
|
|
|
): Promise<void> {
|
2020-04-02 17:03:01 +02:00
|
|
|
await ws.db.mutate(Stores.withdrawalGroups, withdrawalGroupId, (x) => {
|
2019-12-07 22:02:11 +01:00
|
|
|
if (x.retryInfo.active) {
|
|
|
|
x.retryInfo = initRetryInfo();
|
|
|
|
}
|
|
|
|
return x;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-05-12 21:50:40 +02:00
|
|
|
async function processInBatches(
|
|
|
|
workGen: Iterator<Promise<void>>,
|
|
|
|
batchSize: number,
|
|
|
|
): Promise<void> {
|
2020-05-11 14:33:25 +02:00
|
|
|
for (;;) {
|
|
|
|
const batch: Promise<void>[] = [];
|
|
|
|
for (let i = 0; i < batchSize; i++) {
|
|
|
|
const wn = workGen.next();
|
|
|
|
if (wn.done) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
batch.push(wn.value);
|
|
|
|
}
|
|
|
|
if (batch.length == 0) {
|
|
|
|
break;
|
|
|
|
}
|
2020-05-11 17:21:45 +02:00
|
|
|
logger.trace(`processing withdrawal batch of ${batch.length} elements`);
|
2020-05-11 14:33:25 +02:00
|
|
|
await Promise.all(batch);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-02 17:03:01 +02:00
|
|
|
async function processWithdrawGroupImpl(
|
2019-12-07 22:02:11 +01:00
|
|
|
ws: InternalWalletState,
|
2020-04-02 17:03:01 +02:00
|
|
|
withdrawalGroupId: string,
|
2019-12-07 22:02:11 +01:00
|
|
|
forceNow: boolean,
|
2019-12-02 00:42:40 +01:00
|
|
|
): Promise<void> {
|
2020-04-02 17:03:01 +02:00
|
|
|
logger.trace("processing withdraw group", withdrawalGroupId);
|
2019-12-07 22:02:11 +01:00
|
|
|
if (forceNow) {
|
2020-04-02 17:03:01 +02:00
|
|
|
await resetWithdrawalGroupRetry(ws, withdrawalGroupId);
|
2019-12-07 22:02:11 +01:00
|
|
|
}
|
2020-04-02 17:03:01 +02:00
|
|
|
const withdrawalGroup = await ws.db.get(
|
|
|
|
Stores.withdrawalGroups,
|
|
|
|
withdrawalGroupId,
|
2019-12-02 00:42:40 +01:00
|
|
|
);
|
2020-04-02 17:03:01 +02:00
|
|
|
if (!withdrawalGroup) {
|
2019-12-02 00:42:40 +01:00
|
|
|
logger.trace("withdraw session doesn't exist");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-05-11 14:33:25 +02:00
|
|
|
const numDenoms = withdrawalGroup.denomsSel.selectedDenoms.length;
|
2020-05-12 21:50:40 +02:00
|
|
|
const genWork = function* (): Iterator<Promise<void>> {
|
2020-05-11 14:33:25 +02:00
|
|
|
let coinIdx = 0;
|
|
|
|
for (let i = 0; i < numDenoms; i++) {
|
2020-05-11 17:13:19 +02:00
|
|
|
const count = withdrawalGroup.denomsSel.selectedDenoms[i].count;
|
2020-05-11 14:33:25 +02:00
|
|
|
for (let j = 0; j < count; j++) {
|
|
|
|
yield processPlanchet(ws, withdrawalGroupId, coinIdx);
|
|
|
|
coinIdx++;
|
|
|
|
}
|
|
|
|
}
|
2020-05-12 21:50:40 +02:00
|
|
|
};
|
2020-05-11 14:33:25 +02:00
|
|
|
|
|
|
|
// Withdraw coins in batches.
|
|
|
|
// The batch size is relatively large
|
2020-05-11 18:17:35 +02:00
|
|
|
await processInBatches(genWork(), 10);
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
2019-12-09 19:59:08 +01:00
|
|
|
export async function getExchangeWithdrawalInfo(
|
2019-12-02 00:42:40 +01:00
|
|
|
ws: InternalWalletState,
|
|
|
|
baseUrl: string,
|
|
|
|
amount: AmountJson,
|
2019-12-09 19:59:08 +01:00
|
|
|
): Promise<ExchangeWithdrawDetails> {
|
2019-12-02 00:42:40 +01:00
|
|
|
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`);
|
|
|
|
}
|
|
|
|
|
2020-06-03 13:16:25 +02:00
|
|
|
const selectedDenoms = await selectWithdrawalDenoms(ws, baseUrl, amount);
|
2019-12-02 00:42:40 +01:00
|
|
|
const exchangeWireAccounts: string[] = [];
|
2020-04-06 17:45:41 +02:00
|
|
|
for (const account of exchangeWireInfo.accounts) {
|
2020-01-19 19:02:47 +01:00
|
|
|
exchangeWireAccounts.push(account.payto_uri);
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const { isTrusted, isAudited } = await getExchangeTrust(ws, exchangeInfo);
|
|
|
|
|
2020-05-11 14:33:25 +02:00
|
|
|
let earliestDepositExpiration =
|
|
|
|
selectedDenoms.selectedDenoms[0].denom.stampExpireDeposit;
|
|
|
|
for (let i = 1; i < selectedDenoms.selectedDenoms.length; i++) {
|
|
|
|
const expireDeposit =
|
|
|
|
selectedDenoms.selectedDenoms[i].denom.stampExpireDeposit;
|
2019-12-02 00:42:40 +01:00
|
|
|
if (expireDeposit.t_ms < earliestDepositExpiration.t_ms) {
|
|
|
|
earliestDepositExpiration = expireDeposit;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-19 20:42:49 +01:00
|
|
|
const possibleDenoms = await ws.db
|
|
|
|
.iterIndex(Stores.denominations.exchangeBaseUrlIndex, baseUrl)
|
2020-03-30 12:39:32 +02:00
|
|
|
.filter((d) => d.isOffered);
|
2019-12-02 00:42:40 +01:00
|
|
|
|
|
|
|
const trustedAuditorPubs = [];
|
2019-12-19 20:42:49 +01:00
|
|
|
const currencyRecord = await ws.db.get(Stores.currencies, amount.currency);
|
2019-12-02 00:42:40 +01:00
|
|
|
if (currencyRecord) {
|
2020-03-30 12:39:32 +02:00
|
|
|
trustedAuditorPubs.push(
|
|
|
|
...currencyRecord.auditors.map((a) => a.auditorPub),
|
|
|
|
);
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
let versionMatch;
|
|
|
|
if (exchangeDetails.protocolVersion) {
|
|
|
|
versionMatch = LibtoolVersion.compare(
|
2019-12-16 16:59:09 +01:00
|
|
|
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
2019-12-02 00:42:40 +01:00
|
|
|
exchangeDetails.protocolVersion,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (
|
|
|
|
versionMatch &&
|
|
|
|
!versionMatch.compatible &&
|
|
|
|
versionMatch.currentCmp === -1
|
|
|
|
) {
|
|
|
|
console.warn(
|
2019-12-16 16:59:09 +01:00
|
|
|
`wallet's support for exchange protocol version ${WALLET_EXCHANGE_PROTOCOL_VERSION} might be outdated ` +
|
2019-12-02 00:42:40 +01:00
|
|
|
`(exchange has ${exchangeDetails.protocolVersion}), checking for updates`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-09 19:59:08 +01:00
|
|
|
let tosAccepted = false;
|
|
|
|
|
|
|
|
if (exchangeInfo.termsOfServiceAcceptedTimestamp) {
|
2019-12-19 20:42:49 +01:00
|
|
|
if (
|
|
|
|
exchangeInfo.termsOfServiceAcceptedEtag ==
|
|
|
|
exchangeInfo.termsOfServiceLastEtag
|
|
|
|
) {
|
2019-12-09 19:59:08 +01:00
|
|
|
tosAccepted = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-11 14:33:25 +02:00
|
|
|
const withdrawFee = Amounts.sub(
|
|
|
|
selectedDenoms.totalWithdrawCost,
|
|
|
|
selectedDenoms.totalCoinValue,
|
|
|
|
).amount;
|
|
|
|
|
2019-12-09 19:59:08 +01:00
|
|
|
const ret: ExchangeWithdrawDetails = {
|
2019-12-02 00:42:40 +01:00
|
|
|
earliestDepositExpiration,
|
|
|
|
exchangeInfo,
|
|
|
|
exchangeWireAccounts,
|
|
|
|
exchangeVersion: exchangeDetails.protocolVersion || "unknown",
|
|
|
|
isAudited,
|
|
|
|
isTrusted,
|
|
|
|
numOfferedDenoms: possibleDenoms.length,
|
2020-05-11 14:33:25 +02:00
|
|
|
overhead: Amounts.sub(amount, selectedDenoms.totalWithdrawCost).amount,
|
2019-12-02 00:42:40 +01:00
|
|
|
selectedDenoms,
|
|
|
|
trustedAuditorPubs,
|
|
|
|
versionMatch,
|
2019-12-16 16:59:09 +01:00
|
|
|
walletVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
|
2019-12-02 00:42:40 +01:00
|
|
|
wireFees: exchangeWireInfo,
|
2020-05-11 14:33:25 +02:00
|
|
|
withdrawFee,
|
2019-12-09 19:59:08 +01:00
|
|
|
termsOfServiceAccepted: tosAccepted,
|
2019-12-02 00:42:40 +01:00
|
|
|
};
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function getWithdrawDetailsForUri(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
talerWithdrawUri: string,
|
|
|
|
maybeSelectedExchange?: string,
|
2020-07-16 11:14:59 +02:00
|
|
|
): Promise<WithdrawalDetailsResponse> {
|
2019-12-09 19:59:08 +01:00
|
|
|
const info = await getBankWithdrawalInfo(ws, talerWithdrawUri);
|
|
|
|
let rci: ExchangeWithdrawDetails | undefined = undefined;
|
2019-12-02 00:42:40 +01:00
|
|
|
if (maybeSelectedExchange) {
|
2019-12-09 19:59:08 +01:00
|
|
|
rci = await getExchangeWithdrawalInfo(
|
2019-12-02 00:42:40 +01:00
|
|
|
ws,
|
|
|
|
maybeSelectedExchange,
|
|
|
|
info.amount,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return {
|
2019-12-09 19:59:08 +01:00
|
|
|
bankWithdrawDetails: info,
|
|
|
|
exchangeWithdrawDetails: rci,
|
2019-12-02 00:42:40 +01:00
|
|
|
};
|
|
|
|
}
|