2019-12-02 00:42:40 +01:00
|
|
|
/*
|
|
|
|
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,
|
|
|
|
ConfirmReserveRequest,
|
|
|
|
OperationError,
|
2019-12-16 16:59:09 +01:00
|
|
|
AcceptWithdrawalResponse,
|
2019-12-12 20:53:15 +01:00
|
|
|
} from "../types/walletTypes";
|
2019-12-02 00:42:40 +01:00
|
|
|
import { canonicalizeBaseUrl } from "../util/helpers";
|
|
|
|
import { InternalWalletState } from "./state";
|
|
|
|
import {
|
|
|
|
ReserveRecordStatus,
|
|
|
|
ReserveRecord,
|
|
|
|
CurrencyRecord,
|
|
|
|
Stores,
|
2020-04-02 17:03:01 +02:00
|
|
|
WithdrawalGroupRecord,
|
2019-12-05 19:38:19 +01:00
|
|
|
initRetryInfo,
|
|
|
|
updateRetryInfoTimeout,
|
2019-12-16 12:53:22 +01:00
|
|
|
ReserveUpdatedEventRecord,
|
2020-04-02 17:03:01 +02:00
|
|
|
WalletReserveHistoryItemType,
|
|
|
|
DenominationRecord,
|
|
|
|
PlanchetRecord,
|
|
|
|
WithdrawalSourceType,
|
2019-12-12 20:53:15 +01:00
|
|
|
} from "../types/dbTypes";
|
2019-12-02 00:42:40 +01:00
|
|
|
import { Logger } from "../util/logging";
|
2020-04-02 17:03:01 +02:00
|
|
|
import { Amounts } from "../util/amounts";
|
2019-12-16 21:10:57 +01:00
|
|
|
import {
|
|
|
|
updateExchangeFromUrl,
|
|
|
|
getExchangeTrust,
|
|
|
|
getExchangePaytoUri,
|
|
|
|
} from "./exchanges";
|
2020-03-16 13:16:57 +01:00
|
|
|
import {
|
|
|
|
WithdrawOperationStatusResponse,
|
|
|
|
codecForWithdrawOperationStatusResponse,
|
|
|
|
} from "../types/talerTypes";
|
2019-12-02 00:42:40 +01:00
|
|
|
import { assertUnreachable } from "../util/assertUnreachable";
|
2019-12-16 12:53:22 +01:00
|
|
|
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
|
2019-12-02 00:42:40 +01:00
|
|
|
import { randomBytes } from "../crypto/primitives/nacl-fast";
|
|
|
|
import {
|
|
|
|
getVerifiedWithdrawDenomList,
|
2020-04-02 17:03:01 +02:00
|
|
|
processWithdrawGroup,
|
2019-12-16 16:59:09 +01:00
|
|
|
getBankWithdrawalInfo,
|
2019-12-02 00:42:40 +01:00
|
|
|
} from "./withdraw";
|
2019-12-16 21:10:57 +01:00
|
|
|
import {
|
|
|
|
guardOperationException,
|
|
|
|
OperationFailedAndReportedError,
|
2019-12-19 21:22:29 +01:00
|
|
|
OperationFailedError,
|
2019-12-16 21:10:57 +01:00
|
|
|
} from "./errors";
|
2019-12-12 20:53:15 +01:00
|
|
|
import { NotificationType } from "../types/notifications";
|
2019-12-16 12:53:22 +01:00
|
|
|
import { codecForReserveStatus } from "../types/ReserveStatus";
|
2019-12-19 20:42:49 +01:00
|
|
|
import { getTimestampNow } from "../util/time";
|
2020-04-02 17:03:01 +02:00
|
|
|
import {
|
|
|
|
reconcileReserveHistory,
|
|
|
|
summarizeReserveHistory,
|
|
|
|
} from "../util/reserveHistoryUtil";
|
2019-12-02 00:42:40 +01:00
|
|
|
|
|
|
|
const logger = new Logger("reserves.ts");
|
|
|
|
|
2020-03-16 13:16:57 +01:00
|
|
|
async function resetReserveRetry(ws: InternalWalletState, reservePub: string) {
|
2020-03-30 12:39:32 +02:00
|
|
|
await ws.db.mutate(Stores.reserves, reservePub, (x) => {
|
2020-01-22 16:00:49 +01:00
|
|
|
if (x.retryInfo.active) {
|
|
|
|
x.retryInfo = initRetryInfo();
|
|
|
|
}
|
|
|
|
return x;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
/**
|
|
|
|
* 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 = {
|
2019-12-16 16:20:45 +01:00
|
|
|
timestampCreated: now,
|
2019-12-02 00:42:40 +01:00
|
|
|
exchangeBaseUrl: canonExchange,
|
|
|
|
reservePriv: keypair.priv,
|
|
|
|
reservePub: keypair.pub,
|
|
|
|
senderWire: req.senderWire,
|
|
|
|
timestampConfirmed: undefined,
|
|
|
|
timestampReserveInfoPosted: undefined,
|
|
|
|
bankWithdrawStatusUrl: req.bankWithdrawStatusUrl,
|
|
|
|
exchangeWire: req.exchangeWire,
|
|
|
|
reserveStatus,
|
2019-12-05 19:38:19 +01:00
|
|
|
lastSuccessfulStatusQuery: undefined,
|
|
|
|
retryInfo: initRetryInfo(),
|
|
|
|
lastError: undefined,
|
2019-12-16 12:53:22 +01:00
|
|
|
reserveTransactions: [],
|
2020-04-02 17:03:01 +02:00
|
|
|
currency: req.amount.currency,
|
2019-12-02 00:42:40 +01:00
|
|
|
};
|
|
|
|
|
2020-04-02 17:03:01 +02:00
|
|
|
reserveRecord.reserveTransactions.push({
|
|
|
|
type: WalletReserveHistoryItemType.Credit,
|
|
|
|
expectedAmount: req.amount,
|
|
|
|
});
|
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
const senderWire = req.senderWire;
|
|
|
|
if (senderWire) {
|
|
|
|
const rec = {
|
|
|
|
paytoUri: senderWire,
|
|
|
|
};
|
2019-12-12 22:39:45 +01:00
|
|
|
await ws.db.put(Stores.senderWires, rec);
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange);
|
|
|
|
const exchangeDetails = exchangeInfo.details;
|
|
|
|
if (!exchangeDetails) {
|
2019-12-02 17:35:47 +01:00
|
|
|
console.log(exchangeDetails);
|
2019-12-02 00:42:40 +01:00
|
|
|
throw Error("exchange not updated");
|
|
|
|
}
|
|
|
|
const { isAudited, isTrusted } = await getExchangeTrust(ws, exchangeInfo);
|
2019-12-12 22:39:45 +01:00
|
|
|
let currencyRecord = await ws.db.get(
|
2019-12-02 00:42:40 +01:00
|
|
|
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;
|
|
|
|
|
2019-12-12 22:39:45 +01:00
|
|
|
const resp = await ws.db.runWithWriteTransaction(
|
2019-12-02 00:42:40 +01:00
|
|
|
[Stores.currencies, Stores.reserves, Stores.bankWithdrawUris],
|
2020-03-30 12:39:32 +02:00
|
|
|
async (tx) => {
|
2019-12-02 00:42:40 +01:00
|
|
|
// 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;
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
2019-12-06 03:23:35 +01:00
|
|
|
ws.notify({ type: NotificationType.ReserveCreated });
|
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
// Asynchronously process the reserve, but return
|
|
|
|
// to the caller already.
|
2020-03-30 12:39:32 +02:00
|
|
|
processReserve(ws, resp.reservePub, true).catch((e) => {
|
2020-03-23 13:03:38 +01:00
|
|
|
console.error("Processing reserve (after createReserve) failed:", e);
|
2019-12-02 00:42:40 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
return resp;
|
|
|
|
}
|
|
|
|
|
2020-03-12 14:55:38 +01:00
|
|
|
/**
|
|
|
|
* Re-query the status of a reserve.
|
|
|
|
*/
|
|
|
|
export async function forceQueryReserve(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
reservePub: string,
|
|
|
|
): Promise<void> {
|
2020-03-30 12:39:32 +02:00
|
|
|
await ws.db.runWithWriteTransaction([Stores.reserves], async (tx) => {
|
2020-03-12 14:55:38 +01:00
|
|
|
const reserve = await tx.get(Stores.reserves, reservePub);
|
|
|
|
if (!reserve) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Only force status query where it makes sense
|
|
|
|
switch (reserve.reserveStatus) {
|
|
|
|
case ReserveRecordStatus.DORMANT:
|
|
|
|
case ReserveRecordStatus.WITHDRAWING:
|
|
|
|
case ReserveRecordStatus.QUERYING_STATUS:
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
reserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
|
|
|
|
reserve.retryInfo = initRetryInfo();
|
|
|
|
await tx.put(Stores.reserves, reserve);
|
|
|
|
});
|
2020-03-16 12:48:46 +01:00
|
|
|
await processReserve(ws, reservePub, true);
|
2020-03-12 14:55:38 +01:00
|
|
|
}
|
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
/**
|
|
|
|
* 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,
|
2019-12-05 19:38:19 +01:00
|
|
|
forceNow: boolean = false,
|
2019-12-02 00:42:40 +01:00
|
|
|
): Promise<void> {
|
2019-12-05 19:38:19 +01:00
|
|
|
return ws.memoProcessReserve.memo(reservePub, async () => {
|
|
|
|
const onOpError = (err: OperationError) =>
|
|
|
|
incrementReserveRetry(ws, reservePub, err);
|
|
|
|
await guardOperationException(
|
|
|
|
() => processReserveImpl(ws, reservePub, forceNow),
|
|
|
|
onOpError,
|
2019-12-02 00:42:40 +01:00
|
|
|
);
|
2019-12-05 19:38:19 +01:00
|
|
|
});
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async function registerReserveWithBank(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
reservePub: string,
|
|
|
|
): Promise<void> {
|
2019-12-12 22:39:45 +01:00
|
|
|
let reserve = await ws.db.get(Stores.reserves, reservePub);
|
2019-12-02 00:42:40 +01:00
|
|
|
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,
|
|
|
|
});
|
2020-03-30 12:39:32 +02:00
|
|
|
await ws.db.mutate(Stores.reserves, reservePub, (r) => {
|
2019-12-02 00:42:40 +01:00
|
|
|
switch (r.reserveStatus) {
|
|
|
|
case ReserveRecordStatus.REGISTERING_BANK:
|
|
|
|
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
r.timestampReserveInfoPosted = getTimestampNow();
|
|
|
|
r.reserveStatus = ReserveRecordStatus.WAIT_CONFIRM_BANK;
|
2019-12-05 19:38:19 +01:00
|
|
|
r.retryInfo = initRetryInfo();
|
2019-12-02 00:42:40 +01:00
|
|
|
return r;
|
|
|
|
});
|
2019-12-16 21:10:57 +01:00
|
|
|
ws.notify({ type: NotificationType.Wildcard });
|
2019-12-02 00:42:40 +01:00
|
|
|
return processReserveBankStatus(ws, reservePub);
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function processReserveBankStatus(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
reservePub: string,
|
2019-12-05 19:38:19 +01:00
|
|
|
): Promise<void> {
|
|
|
|
const onOpError = (err: OperationError) =>
|
|
|
|
incrementReserveRetry(ws, reservePub, err);
|
|
|
|
await guardOperationException(
|
|
|
|
() => processReserveBankStatusImpl(ws, reservePub),
|
|
|
|
onOpError,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function processReserveBankStatusImpl(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
reservePub: string,
|
2019-12-02 00:42:40 +01:00
|
|
|
): Promise<void> {
|
2019-12-12 22:39:45 +01:00
|
|
|
let reserve = await ws.db.get(Stores.reserves, reservePub);
|
2019-12-02 00:42:40 +01:00
|
|
|
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);
|
2019-12-09 13:29:11 +01:00
|
|
|
if (statusResp.status !== 200) {
|
2019-12-16 21:10:57 +01:00
|
|
|
throw Error(
|
|
|
|
`unexpected status ${statusResp.status} for bank status query`,
|
|
|
|
);
|
2019-12-09 13:29:11 +01:00
|
|
|
}
|
2020-03-16 13:16:57 +01:00
|
|
|
status = codecForWithdrawOperationStatusResponse().decode(
|
|
|
|
await statusResp.json(),
|
|
|
|
);
|
2019-12-02 00:42:40 +01:00
|
|
|
} catch (e) {
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
|
2019-12-16 21:10:57 +01:00
|
|
|
ws.notify({ type: NotificationType.Wildcard });
|
2019-12-06 03:23:35 +01:00
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
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) {
|
2020-03-30 12:39:32 +02:00
|
|
|
await ws.db.mutate(Stores.reserves, reservePub, (r) => {
|
2019-12-02 00:42:40 +01:00
|
|
|
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;
|
2019-12-05 19:38:19 +01:00
|
|
|
r.retryInfo = initRetryInfo();
|
2019-12-02 00:42:40 +01:00
|
|
|
return r;
|
|
|
|
});
|
2019-12-05 19:38:19 +01:00
|
|
|
await processReserveImpl(ws, reservePub, true);
|
2019-12-02 00:42:40 +01:00
|
|
|
} else {
|
2020-03-30 12:39:32 +02:00
|
|
|
await ws.db.mutate(Stores.reserves, reservePub, (r) => {
|
2019-12-02 00:42:40 +01:00
|
|
|
switch (r.reserveStatus) {
|
|
|
|
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
r.bankWithdrawConfirmUrl = status.confirm_transfer_url;
|
|
|
|
return r;
|
|
|
|
});
|
2019-12-06 11:01:39 +01:00
|
|
|
await incrementReserveRetry(ws, reservePub, undefined);
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
2019-12-16 21:10:57 +01:00
|
|
|
ws.notify({ type: NotificationType.Wildcard });
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
2019-12-05 19:38:19 +01:00
|
|
|
async function incrementReserveRetry(
|
2019-12-02 00:42:40 +01:00
|
|
|
ws: InternalWalletState,
|
|
|
|
reservePub: string,
|
2019-12-05 19:38:19 +01:00
|
|
|
err: OperationError | undefined,
|
2019-12-02 00:42:40 +01:00
|
|
|
): Promise<void> {
|
2020-03-30 12:39:32 +02:00
|
|
|
await ws.db.runWithWriteTransaction([Stores.reserves], async (tx) => {
|
2019-12-05 19:38:19 +01:00
|
|
|
const r = await tx.get(Stores.reserves, reservePub);
|
|
|
|
if (!r) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!r.retryInfo) {
|
|
|
|
return;
|
|
|
|
}
|
2019-12-19 21:22:29 +01:00
|
|
|
console.log("updating retry info");
|
|
|
|
console.log("before", r.retryInfo);
|
2019-12-05 19:38:19 +01:00
|
|
|
r.retryInfo.retryCounter++;
|
|
|
|
updateRetryInfoTimeout(r.retryInfo);
|
2019-12-19 21:22:29 +01:00
|
|
|
console.log("after", r.retryInfo);
|
2019-12-05 19:38:19 +01:00
|
|
|
r.lastError = err;
|
|
|
|
await tx.put(Stores.reserves, r);
|
|
|
|
});
|
2019-12-16 21:10:57 +01:00
|
|
|
if (err) {
|
|
|
|
ws.notify({
|
|
|
|
type: NotificationType.ReserveOperationError,
|
|
|
|
operationError: err,
|
|
|
|
});
|
|
|
|
}
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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> {
|
2019-12-12 22:39:45 +01:00
|
|
|
const reserve = await ws.db.get(Stores.reserves, reservePub);
|
2019-12-02 00:42:40 +01:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-03-09 12:07:46 +01:00
|
|
|
const reqUrl = new URL(`reserves/${reservePub}`, reserve.exchangeBaseUrl);
|
2019-12-02 00:42:40 +01:00
|
|
|
let resp;
|
|
|
|
try {
|
|
|
|
resp = await ws.http.get(reqUrl.href);
|
2020-03-09 12:07:46 +01:00
|
|
|
console.log("got reserves/${RESERVE_PUB} response", await resp.json());
|
2019-12-09 13:29:11 +01:00
|
|
|
if (resp.status === 404) {
|
2020-03-16 13:16:57 +01:00
|
|
|
const m = "reserve not known to the exchange yet";
|
2020-03-12 14:55:38 +01:00
|
|
|
throw new OperationFailedError({
|
2019-12-19 21:22:29 +01:00
|
|
|
type: "waiting",
|
|
|
|
message: m,
|
|
|
|
details: {},
|
|
|
|
});
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
2019-12-09 13:29:11 +01:00
|
|
|
if (resp.status !== 200) {
|
2019-12-16 21:10:57 +01:00
|
|
|
throw Error(`unexpected status code ${resp.status} for reserve/status`);
|
2019-12-09 13:29:11 +01:00
|
|
|
}
|
|
|
|
} catch (e) {
|
2019-12-19 21:22:29 +01:00
|
|
|
logger.trace("caught exception for reserve/status");
|
2019-12-09 13:29:11 +01:00
|
|
|
const m = e.message;
|
2020-03-12 14:55:38 +01:00
|
|
|
const opErr = {
|
2019-12-09 13:29:11 +01:00
|
|
|
type: "network",
|
|
|
|
details: {},
|
|
|
|
message: m,
|
2020-03-12 14:55:38 +01:00
|
|
|
};
|
|
|
|
await incrementReserveRetry(ws, reservePub, opErr);
|
|
|
|
throw new OperationFailedAndReportedError(opErr);
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
2019-12-16 12:53:22 +01:00
|
|
|
const respJson = await resp.json();
|
2019-12-19 20:42:49 +01:00
|
|
|
const reserveInfo = codecForReserveStatus().decode(respJson);
|
2019-12-02 00:42:40 +01:00
|
|
|
const balance = Amounts.parseOrThrow(reserveInfo.balance);
|
2020-04-02 17:03:01 +02:00
|
|
|
const currency = balance.currency;
|
2019-12-16 21:10:57 +01:00
|
|
|
await ws.db.runWithWriteTransaction(
|
|
|
|
[Stores.reserves, Stores.reserveUpdatedEvents],
|
2020-03-30 12:39:32 +02:00
|
|
|
async (tx) => {
|
2019-12-16 21:10:57 +01:00
|
|
|
const r = await tx.get(Stores.reserves, reservePub);
|
|
|
|
if (!r) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (r.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) {
|
2019-12-02 00:42:40 +01:00
|
|
|
return;
|
|
|
|
}
|
2019-12-16 21:10:57 +01:00
|
|
|
|
|
|
|
const newHistoryTransactions = reserveInfo.history.slice(
|
|
|
|
r.reserveTransactions.length,
|
|
|
|
);
|
|
|
|
|
|
|
|
const reserveUpdateId = encodeCrock(getRandomBytes(32));
|
|
|
|
|
2020-04-02 17:03:01 +02:00
|
|
|
const reconciled = reconcileReserveHistory(
|
|
|
|
r.reserveTransactions,
|
|
|
|
reserveInfo.history,
|
|
|
|
);
|
|
|
|
|
2020-04-02 17:14:12 +02:00
|
|
|
console.log(
|
|
|
|
"reconciled history:",
|
|
|
|
JSON.stringify(reconciled, undefined, 2),
|
|
|
|
);
|
2020-04-02 17:03:01 +02:00
|
|
|
|
|
|
|
const summary = summarizeReserveHistory(
|
|
|
|
reconciled.updatedLocalHistory,
|
|
|
|
currency,
|
|
|
|
);
|
|
|
|
console.log("summary", summary);
|
|
|
|
|
|
|
|
if (
|
|
|
|
reconciled.newAddedItems.length + reconciled.newMatchedItems.length !=
|
|
|
|
0
|
|
|
|
) {
|
2019-12-16 21:10:57 +01:00
|
|
|
const reserveUpdate: ReserveUpdatedEventRecord = {
|
|
|
|
reservePub: r.reservePub,
|
|
|
|
timestamp: getTimestampNow(),
|
2020-04-02 17:03:01 +02:00
|
|
|
amountReserveBalance: Amounts.stringify(balance),
|
|
|
|
amountExpected: Amounts.stringify(summary.awaitedReserveAmount),
|
2019-12-16 21:10:57 +01:00
|
|
|
newHistoryTransactions,
|
|
|
|
reserveUpdateId,
|
|
|
|
};
|
|
|
|
await tx.put(Stores.reserveUpdatedEvents, reserveUpdate);
|
2020-03-16 12:48:46 +01:00
|
|
|
r.reserveStatus = ReserveRecordStatus.WITHDRAWING;
|
2020-04-02 17:03:01 +02:00
|
|
|
r.retryInfo = initRetryInfo();
|
2019-12-02 00:42:40 +01:00
|
|
|
} else {
|
2020-04-02 17:03:01 +02:00
|
|
|
r.reserveStatus = ReserveRecordStatus.DORMANT;
|
2020-03-16 13:16:57 +01:00
|
|
|
r.retryInfo = initRetryInfo(false);
|
|
|
|
}
|
2020-04-02 17:03:01 +02:00
|
|
|
r.lastSuccessfulStatusQuery = getTimestampNow();
|
|
|
|
r.reserveTransactions = reconciled.updatedLocalHistory;
|
|
|
|
r.lastError = undefined;
|
2019-12-16 21:10:57 +01:00
|
|
|
await tx.put(Stores.reserves, r);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
ws.notify({ type: NotificationType.ReserveUpdated });
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async function processReserveImpl(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
reservePub: string,
|
2019-12-05 19:38:19 +01:00
|
|
|
forceNow: boolean = false,
|
2019-12-02 00:42:40 +01:00
|
|
|
): Promise<void> {
|
2019-12-12 22:39:45 +01:00
|
|
|
const reserve = await ws.db.get(Stores.reserves, reservePub);
|
2019-12-02 00:42:40 +01:00
|
|
|
if (!reserve) {
|
|
|
|
console.log("not processing reserve: reserve does not exist");
|
|
|
|
return;
|
|
|
|
}
|
2019-12-05 19:38:19 +01:00
|
|
|
if (!forceNow) {
|
|
|
|
const now = getTimestampNow();
|
|
|
|
if (reserve.retryInfo.nextRetry.t_ms > now.t_ms) {
|
|
|
|
logger.trace("processReserve retry not due yet");
|
|
|
|
return;
|
|
|
|
}
|
2020-01-22 16:00:49 +01:00
|
|
|
} else {
|
|
|
|
await resetReserveRetry(ws, reservePub);
|
2019-12-05 19:38:19 +01:00
|
|
|
}
|
2019-12-02 00:42:40 +01:00
|
|
|
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);
|
2019-12-15 16:59:00 +01:00
|
|
|
return await processReserveImpl(ws, reservePub, true);
|
2019-12-02 00:42:40 +01:00
|
|
|
case ReserveRecordStatus.QUERYING_STATUS:
|
|
|
|
await updateReserve(ws, reservePub);
|
2019-12-15 16:59:00 +01:00
|
|
|
return await processReserveImpl(ws, reservePub, true);
|
2019-12-02 00:42:40 +01:00
|
|
|
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();
|
2020-03-30 12:39:32 +02:00
|
|
|
await ws.db.mutate(Stores.reserves, req.reservePub, (reserve) => {
|
2019-12-02 00:42:40 +01:00
|
|
|
if (reserve.reserveStatus !== ReserveRecordStatus.UNCONFIRMED) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
reserve.timestampConfirmed = now;
|
|
|
|
reserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
|
2019-12-05 19:38:19 +01:00
|
|
|
reserve.retryInfo = initRetryInfo();
|
2019-12-02 00:42:40 +01:00
|
|
|
return reserve;
|
|
|
|
});
|
|
|
|
|
2019-12-05 19:38:19 +01:00
|
|
|
ws.notify({ type: NotificationType.ReserveUpdated });
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2020-03-30 12:39:32 +02:00
|
|
|
processReserve(ws, req.reservePub, true).catch((e) => {
|
2020-03-23 13:03:38 +01:00
|
|
|
console.log("processing reserve (after confirmReserve) failed:", e);
|
2019-12-02 00:42:40 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-04-02 17:03:01 +02:00
|
|
|
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,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
/**
|
|
|
|
* 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> {
|
2019-12-12 22:39:45 +01:00
|
|
|
const reserve = await ws.db.get(Stores.reserves, reservePub);
|
2019-12-02 00:42:40 +01:00
|
|
|
if (!reserve) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (reserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
logger.trace(`depleting reserve ${reservePub}`);
|
|
|
|
|
2020-04-02 17:03:01 +02:00
|
|
|
const summary = summarizeReserveHistory(
|
|
|
|
reserve.reserveTransactions,
|
|
|
|
reserve.currency,
|
|
|
|
);
|
|
|
|
|
|
|
|
const withdrawAmount = summary.unclaimedReserveAmount;
|
2019-12-02 00:42:40 +01:00
|
|
|
|
|
|
|
logger.trace(`getting denom list`);
|
|
|
|
|
|
|
|
const denomsForWithdraw = await getVerifiedWithdrawDenomList(
|
|
|
|
ws,
|
|
|
|
reserve.exchangeBaseUrl,
|
|
|
|
withdrawAmount,
|
|
|
|
);
|
|
|
|
logger.trace(`got denom list`);
|
|
|
|
if (denomsForWithdraw.length === 0) {
|
2020-04-02 17:03:01 +02:00
|
|
|
// 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;
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
logger.trace("selected denominations");
|
|
|
|
|
2020-04-02 17:03:01 +02:00
|
|
|
const withdrawalGroupId = encodeCrock(randomBytes(32));
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2020-03-30 12:39:32 +02:00
|
|
|
const totalCoinValue = Amounts.sum(denomsForWithdraw.map((x) => x.value))
|
2019-12-05 19:38:19 +01:00
|
|
|
.amount;
|
2019-12-03 01:33:25 +01:00
|
|
|
|
2020-04-02 17:03:01 +02:00
|
|
|
const planchets: PlanchetRecord[] = [];
|
|
|
|
for (const d of denomsForWithdraw) {
|
|
|
|
const p = await makePlanchet(ws, reserve, d);
|
|
|
|
planchets.push(p);
|
|
|
|
}
|
|
|
|
|
|
|
|
const withdrawalRecord: WithdrawalGroupRecord = {
|
|
|
|
withdrawalGroupId: withdrawalGroupId,
|
2019-12-02 00:42:40 +01:00
|
|
|
exchangeBaseUrl: reserve.exchangeBaseUrl,
|
|
|
|
source: {
|
2020-04-02 17:03:01 +02:00
|
|
|
type: WithdrawalSourceType.Reserve,
|
2019-12-02 00:42:40 +01:00
|
|
|
reservePub: reserve.reservePub,
|
|
|
|
},
|
2019-12-03 01:33:25 +01:00
|
|
|
rawWithdrawalAmount: withdrawAmount,
|
2019-12-16 16:20:45 +01:00
|
|
|
timestampStart: getTimestampNow(),
|
2020-03-30 12:39:32 +02:00
|
|
|
denoms: denomsForWithdraw.map((x) => x.denomPub),
|
|
|
|
withdrawn: denomsForWithdraw.map((x) => false),
|
2020-04-02 17:03:01 +02:00
|
|
|
planchets,
|
2019-12-03 01:33:25 +01:00
|
|
|
totalCoinValue,
|
2019-12-05 19:38:19 +01:00
|
|
|
retryInfo: initRetryInfo(),
|
2019-12-16 12:53:22 +01:00
|
|
|
lastErrorPerCoin: {},
|
2019-12-05 19:38:19 +01:00
|
|
|
lastError: undefined,
|
2019-12-02 00:42:40 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
const totalCoinWithdrawFee = Amounts.sum(
|
2020-03-30 12:39:32 +02:00
|
|
|
denomsForWithdraw.map((x) => x.feeWithdraw),
|
2019-12-02 00:42:40 +01:00
|
|
|
).amount;
|
|
|
|
const totalWithdrawAmount = Amounts.add(totalCoinValue, totalCoinWithdrawFee)
|
|
|
|
.amount;
|
|
|
|
|
2019-12-12 22:39:45 +01:00
|
|
|
const success = await ws.db.runWithWriteTransaction(
|
2020-04-02 17:03:01 +02:00
|
|
|
[Stores.withdrawalGroups, Stores.reserves],
|
2020-03-30 12:39:32 +02:00
|
|
|
async (tx) => {
|
2020-04-02 17:03:01 +02:00
|
|
|
const newReserve = await tx.get(Stores.reserves, reservePub);
|
|
|
|
if (!newReserve) {
|
2019-12-02 00:42:40 +01:00
|
|
|
return false;
|
|
|
|
}
|
2020-04-02 17:03:01 +02:00
|
|
|
if (newReserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
|
2019-12-02 00:42:40 +01:00
|
|
|
return false;
|
|
|
|
}
|
2020-04-02 17:03:01 +02:00
|
|
|
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);
|
2019-12-02 00:42:40 +01:00
|
|
|
return true;
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
if (success) {
|
2020-04-02 17:03:01 +02:00
|
|
|
console.log("processing new withdraw group");
|
2019-12-06 02:52:16 +01:00
|
|
|
ws.notify({
|
2020-04-02 17:03:01 +02:00
|
|
|
type: NotificationType.WithdrawGroupCreated,
|
|
|
|
withdrawalGroupId: withdrawalGroupId,
|
2019-12-06 02:52:16 +01:00
|
|
|
});
|
2020-04-02 17:03:01 +02:00
|
|
|
await processWithdrawGroup(ws, withdrawalGroupId);
|
2019-12-02 00:42:40 +01:00
|
|
|
} else {
|
|
|
|
console.trace("withdraw session already existed");
|
|
|
|
}
|
|
|
|
}
|
2019-12-16 16:59:09 +01:00
|
|
|
|
|
|
|
export async function createTalerWithdrawReserve(
|
|
|
|
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,
|
|
|
|
};
|
2019-12-16 21:10:57 +01:00
|
|
|
}
|