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/>
|
|
|
|
*/
|
|
|
|
|
2021-03-17 17:56:37 +01:00
|
|
|
/**
|
|
|
|
* Imports.
|
|
|
|
*/
|
2020-03-09 09:49:22 +01:00
|
|
|
import {
|
2022-03-28 23:21:49 +02:00
|
|
|
AbsoluteTime,
|
2021-03-17 17:56:37 +01:00
|
|
|
Amounts,
|
2022-03-29 13:47:32 +02:00
|
|
|
CancellationToken,
|
2021-06-17 15:49:05 +02:00
|
|
|
canonicalizeBaseUrl,
|
2020-03-09 09:49:22 +01:00
|
|
|
codecForExchangeKeysJson,
|
|
|
|
codecForExchangeWireJson,
|
2022-03-28 23:21:49 +02:00
|
|
|
DenominationPubKey,
|
2021-03-17 17:56:37 +01:00
|
|
|
Duration,
|
|
|
|
durationFromSpec,
|
2022-03-28 23:21:49 +02:00
|
|
|
encodeCrock,
|
|
|
|
ExchangeAuditor,
|
|
|
|
ExchangeDenomination,
|
2021-06-02 13:23:51 +02:00
|
|
|
ExchangeSignKeyJson,
|
|
|
|
ExchangeWireJson,
|
2022-03-28 23:21:49 +02:00
|
|
|
hashDenomPub,
|
2022-04-19 17:12:43 +02:00
|
|
|
j2s,
|
2022-03-28 23:21:49 +02:00
|
|
|
LibtoolVersion,
|
2021-06-08 20:58:13 +02:00
|
|
|
Logger,
|
2021-03-17 17:56:37 +01:00
|
|
|
NotificationType,
|
|
|
|
parsePaytoUri,
|
2021-06-02 13:23:51 +02:00
|
|
|
Recoup,
|
2021-03-17 17:56:37 +01:00
|
|
|
TalerErrorCode,
|
2022-03-22 21:16:38 +01:00
|
|
|
TalerErrorDetail,
|
2022-03-18 15:32:41 +01:00
|
|
|
TalerProtocolDuration,
|
2022-03-28 23:21:49 +02:00
|
|
|
TalerProtocolTimestamp,
|
|
|
|
URL,
|
2021-03-17 17:56:37 +01:00
|
|
|
} from "@gnu-taler/taler-util";
|
2019-12-02 00:42:40 +01:00
|
|
|
import {
|
|
|
|
DenominationRecord,
|
2021-08-24 15:43:06 +02:00
|
|
|
DenominationVerificationStatus,
|
2021-06-17 15:49:05 +02:00
|
|
|
ExchangeDetailsRecord,
|
2021-03-17 17:56:37 +01:00
|
|
|
ExchangeRecord,
|
2021-06-17 15:49:05 +02:00
|
|
|
WalletStoresV1,
|
2019-12-02 00:42:40 +01:00
|
|
|
WireFee,
|
2021-06-02 13:23:51 +02:00
|
|
|
WireInfo,
|
2021-03-17 17:56:37 +01:00
|
|
|
} from "../db.js";
|
2022-03-28 23:21:49 +02:00
|
|
|
import { TalerError } from "../errors.js";
|
|
|
|
import { InternalWalletState, TrustInfo } from "../internal-wallet-state.js";
|
2021-06-15 18:52:43 +02:00
|
|
|
import {
|
2022-03-18 15:32:41 +01:00
|
|
|
getExpiry,
|
2021-06-15 18:52:43 +02:00
|
|
|
HttpRequestLibrary,
|
|
|
|
readSuccessResponseJsonOrThrow,
|
|
|
|
readSuccessResponseTextOrThrow,
|
|
|
|
} from "../util/http.js";
|
2021-06-09 15:14:17 +02:00
|
|
|
import { DbAccess, GetReadOnlyAccess } from "../util/query.js";
|
2022-03-29 13:47:32 +02:00
|
|
|
import { resetRetryInfo, RetryInfo } from "../util/retries.js";
|
2021-06-17 15:49:05 +02:00
|
|
|
import {
|
|
|
|
WALLET_CACHE_BREAKER_CLIENT_VERSION,
|
|
|
|
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
|
|
|
} from "../versions.js";
|
2022-03-23 13:11:36 +01:00
|
|
|
import { guardOperationException } from "./common.js";
|
2020-07-23 15:54:00 +02:00
|
|
|
|
|
|
|
const logger = new Logger("exchanges.ts");
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2021-06-02 13:23:51 +02:00
|
|
|
function denominationRecordFromKeys(
|
2019-12-02 00:42:40 +01:00
|
|
|
exchangeBaseUrl: string,
|
2021-06-11 13:26:18 +02:00
|
|
|
exchangeMasterPub: string,
|
2022-03-18 15:32:41 +01:00
|
|
|
listIssueDate: TalerProtocolTimestamp,
|
2021-11-27 20:56:58 +01:00
|
|
|
denomIn: ExchangeDenomination,
|
2021-06-02 13:23:51 +02:00
|
|
|
): DenominationRecord {
|
2021-11-27 20:56:58 +01:00
|
|
|
let denomPub: DenominationPubKey;
|
2022-02-21 12:40:51 +01:00
|
|
|
denomPub = denomIn.denom_pub;
|
2021-11-27 20:56:58 +01:00
|
|
|
const denomPubHash = encodeCrock(hashDenomPub(denomPub));
|
2019-12-02 00:42:40 +01:00
|
|
|
const d: DenominationRecord = {
|
2021-11-27 20:56:58 +01:00
|
|
|
denomPub,
|
2019-12-02 00:42:40 +01:00
|
|
|
denomPubHash,
|
|
|
|
exchangeBaseUrl,
|
2021-06-11 13:26:18 +02:00
|
|
|
exchangeMasterPub,
|
2019-12-02 00:42:40 +01:00
|
|
|
feeDeposit: Amounts.parseOrThrow(denomIn.fee_deposit),
|
|
|
|
feeRefresh: Amounts.parseOrThrow(denomIn.fee_refresh),
|
|
|
|
feeRefund: Amounts.parseOrThrow(denomIn.fee_refund),
|
|
|
|
feeWithdraw: Amounts.parseOrThrow(denomIn.fee_withdraw),
|
|
|
|
isOffered: true,
|
2020-03-11 20:14:28 +01:00
|
|
|
isRevoked: false,
|
2019-12-02 00:42:40 +01:00
|
|
|
masterSig: denomIn.master_sig,
|
2019-12-19 20:42:49 +01:00
|
|
|
stampExpireDeposit: denomIn.stamp_expire_deposit,
|
|
|
|
stampExpireLegal: denomIn.stamp_expire_legal,
|
|
|
|
stampExpireWithdraw: denomIn.stamp_expire_withdraw,
|
|
|
|
stampStart: denomIn.stamp_start,
|
2021-08-24 15:43:06 +02:00
|
|
|
verificationStatus: DenominationVerificationStatus.Unverified,
|
2019-12-02 00:42:40 +01:00
|
|
|
value: Amounts.parseOrThrow(denomIn.value),
|
2021-08-19 13:48:36 +02:00
|
|
|
listIssueDate,
|
2019-12-02 00:42:40 +01:00
|
|
|
};
|
|
|
|
return d;
|
|
|
|
}
|
|
|
|
|
2022-03-28 23:59:16 +02:00
|
|
|
async function reportExchangeUpdateError(
|
2019-12-02 00:42:40 +01:00
|
|
|
ws: InternalWalletState,
|
|
|
|
baseUrl: string,
|
2022-03-22 21:16:38 +01:00
|
|
|
err: TalerErrorDetail,
|
2019-12-02 00:42:40 +01:00
|
|
|
): Promise<void> {
|
2021-06-09 15:14:17 +02:00
|
|
|
await ws.db
|
|
|
|
.mktx((x) => ({ exchanges: x.exchanges }))
|
2022-01-13 05:31:53 +01:00
|
|
|
.runReadWrite(async (tx) => {
|
2021-06-09 15:14:17 +02:00
|
|
|
const exchange = await tx.exchanges.get(baseUrl);
|
|
|
|
if (!exchange) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
exchange.lastError = err;
|
2022-01-13 22:01:14 +01:00
|
|
|
await tx.exchanges.put(exchange);
|
2021-06-09 15:14:17 +02:00
|
|
|
});
|
2022-03-28 23:59:16 +02:00
|
|
|
ws.notify({ type: NotificationType.ExchangeOperationError, error: err });
|
|
|
|
}
|
|
|
|
|
2022-03-29 13:47:32 +02:00
|
|
|
async function setupExchangeUpdateRetry(
|
2022-03-28 23:59:16 +02:00
|
|
|
ws: InternalWalletState,
|
|
|
|
baseUrl: string,
|
2022-03-29 13:47:32 +02:00
|
|
|
options: {
|
|
|
|
reset: boolean;
|
|
|
|
},
|
2022-03-28 23:59:16 +02:00
|
|
|
): Promise<void> {
|
|
|
|
await ws.db
|
|
|
|
.mktx((x) => ({ exchanges: x.exchanges }))
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const exchange = await tx.exchanges.get(baseUrl);
|
|
|
|
if (!exchange) {
|
|
|
|
return;
|
|
|
|
}
|
2022-03-29 13:47:32 +02:00
|
|
|
if (options.reset) {
|
|
|
|
exchange.retryInfo = resetRetryInfo();
|
|
|
|
} else {
|
|
|
|
exchange.retryInfo = RetryInfo.increment(exchange.retryInfo);
|
2022-03-28 23:59:16 +02:00
|
|
|
}
|
|
|
|
delete exchange.lastError;
|
|
|
|
await tx.exchanges.put(exchange);
|
|
|
|
});
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
2022-01-24 18:12:12 +01:00
|
|
|
export function getExchangeRequestTimeout(): Duration {
|
2022-03-18 15:32:41 +01:00
|
|
|
return Duration.fromSpec({
|
|
|
|
seconds: 5,
|
|
|
|
});
|
2020-08-20 12:57:20 +02:00
|
|
|
}
|
|
|
|
|
2021-09-13 20:32:06 +02:00
|
|
|
export interface ExchangeTosDownloadResult {
|
2021-06-02 13:23:51 +02:00
|
|
|
tosText: string;
|
|
|
|
tosEtag: string;
|
2021-09-13 20:32:06 +02:00
|
|
|
tosContentType: string;
|
2021-06-02 13:23:51 +02:00
|
|
|
}
|
|
|
|
|
2021-09-13 20:32:06 +02:00
|
|
|
export async function downloadExchangeWithTermsOfService(
|
2021-06-02 13:23:51 +02:00
|
|
|
exchangeBaseUrl: string,
|
|
|
|
http: HttpRequestLibrary,
|
|
|
|
timeout: Duration,
|
2021-09-13 20:32:06 +02:00
|
|
|
contentType: string,
|
2021-06-02 13:23:51 +02:00
|
|
|
): Promise<ExchangeTosDownloadResult> {
|
|
|
|
const reqUrl = new URL("terms", exchangeBaseUrl);
|
|
|
|
reqUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
|
|
|
|
const headers = {
|
2021-09-13 20:32:06 +02:00
|
|
|
Accept: contentType,
|
2021-06-02 13:23:51 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
const resp = await http.get(reqUrl.href, {
|
|
|
|
headers,
|
|
|
|
timeout,
|
|
|
|
});
|
|
|
|
const tosText = await readSuccessResponseTextOrThrow(resp);
|
|
|
|
const tosEtag = resp.headers.get("etag") || "unknown";
|
2021-09-13 20:32:06 +02:00
|
|
|
const tosContentType = resp.headers.get("content-type") || "text/plain";
|
2021-06-02 13:23:51 +02:00
|
|
|
|
2021-09-13 20:32:06 +02:00
|
|
|
return { tosText, tosEtag, tosContentType };
|
2021-06-02 13:23:51 +02:00
|
|
|
}
|
|
|
|
|
2021-08-23 22:28:36 +02:00
|
|
|
/**
|
|
|
|
* Get exchange details from the database.
|
|
|
|
*/
|
2021-06-02 13:23:51 +02:00
|
|
|
export async function getExchangeDetails(
|
2021-06-09 15:14:17 +02:00
|
|
|
tx: GetReadOnlyAccess<{
|
|
|
|
exchanges: typeof WalletStoresV1.exchanges;
|
|
|
|
exchangeDetails: typeof WalletStoresV1.exchangeDetails;
|
|
|
|
}>,
|
2021-06-02 13:23:51 +02:00
|
|
|
exchangeBaseUrl: string,
|
|
|
|
): Promise<ExchangeDetailsRecord | undefined> {
|
2021-06-09 15:14:17 +02:00
|
|
|
const r = await tx.exchanges.get(exchangeBaseUrl);
|
2021-06-02 13:23:51 +02:00
|
|
|
if (!r) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const dp = r.detailsPointer;
|
|
|
|
if (!dp) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const { currency, masterPublicKey } = dp;
|
2021-06-09 15:14:17 +02:00
|
|
|
return await tx.exchangeDetails.get([r.baseUrl, currency, masterPublicKey]);
|
2021-06-02 13:23:51 +02:00
|
|
|
}
|
|
|
|
|
2021-06-09 15:14:17 +02:00
|
|
|
getExchangeDetails.makeContext = (db: DbAccess<typeof WalletStoresV1>) =>
|
|
|
|
db.mktx((x) => ({
|
|
|
|
exchanges: x.exchanges,
|
|
|
|
exchangeDetails: x.exchangeDetails,
|
|
|
|
}));
|
|
|
|
|
2022-01-24 18:12:12 +01:00
|
|
|
export async function updateExchangeTermsOfService(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
exchangeBaseUrl: string,
|
|
|
|
tos: ExchangeTosDownloadResult,
|
|
|
|
): Promise<void> {
|
|
|
|
await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
exchanges: x.exchanges,
|
|
|
|
exchangeDetails: x.exchangeDetails,
|
|
|
|
}))
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const d = await getExchangeDetails(tx, exchangeBaseUrl);
|
|
|
|
if (d) {
|
|
|
|
d.termsOfServiceText = tos.tosText;
|
|
|
|
d.termsOfServiceContentType = tos.tosContentType;
|
|
|
|
d.termsOfServiceLastEtag = tos.tosEtag;
|
|
|
|
await tx.exchangeDetails.put(d);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-06-02 13:23:51 +02:00
|
|
|
export async function acceptExchangeTermsOfService(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
exchangeBaseUrl: string,
|
|
|
|
etag: string | undefined,
|
|
|
|
): Promise<void> {
|
2021-06-09 15:14:17 +02:00
|
|
|
await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
exchanges: x.exchanges,
|
|
|
|
exchangeDetails: x.exchangeDetails,
|
|
|
|
}))
|
|
|
|
.runReadWrite(async (tx) => {
|
2021-06-02 13:23:51 +02:00
|
|
|
const d = await getExchangeDetails(tx, exchangeBaseUrl);
|
|
|
|
if (d) {
|
|
|
|
d.termsOfServiceAcceptedEtag = etag;
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.exchangeDetails.put(d);
|
2021-06-02 13:23:51 +02:00
|
|
|
}
|
2021-06-09 15:14:17 +02:00
|
|
|
});
|
2021-06-02 13:23:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async function validateWireInfo(
|
2021-12-08 16:23:00 +01:00
|
|
|
ws: InternalWalletState,
|
2021-11-27 20:56:58 +01:00
|
|
|
versionCurrent: number,
|
2021-06-02 13:23:51 +02:00
|
|
|
wireInfo: ExchangeWireJson,
|
|
|
|
masterPublicKey: string,
|
|
|
|
): Promise<WireInfo> {
|
|
|
|
for (const a of wireInfo.accounts) {
|
|
|
|
logger.trace("validating exchange acct");
|
2021-12-08 16:23:00 +01:00
|
|
|
let isValid = false;
|
|
|
|
if (ws.insecureTrustExchange) {
|
|
|
|
isValid = true;
|
|
|
|
} else {
|
2022-03-23 21:24:23 +01:00
|
|
|
const { valid: v } = await ws.cryptoApi.isValidWireAccount({
|
|
|
|
masterPub: masterPublicKey,
|
|
|
|
paytoUri: a.payto_uri,
|
|
|
|
sig: a.master_sig,
|
2021-12-08 16:23:00 +01:00
|
|
|
versionCurrent,
|
2022-03-23 21:24:23 +01:00
|
|
|
});
|
|
|
|
isValid = v;
|
2021-12-08 16:23:00 +01:00
|
|
|
}
|
2021-06-02 13:23:51 +02:00
|
|
|
if (!isValid) {
|
|
|
|
throw Error("exchange acct signature invalid");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const feesForType: { [wireMethod: string]: WireFee[] } = {};
|
|
|
|
for (const wireMethod of Object.keys(wireInfo.fees)) {
|
|
|
|
const feeList: WireFee[] = [];
|
|
|
|
for (const x of wireInfo.fees[wireMethod]) {
|
|
|
|
const startStamp = x.start_date;
|
|
|
|
const endStamp = x.end_date;
|
|
|
|
const fee: WireFee = {
|
|
|
|
closingFee: Amounts.parseOrThrow(x.closing_fee),
|
|
|
|
endStamp,
|
|
|
|
sig: x.sig,
|
|
|
|
startStamp,
|
|
|
|
wireFee: Amounts.parseOrThrow(x.wire_fee),
|
2022-03-07 12:09:38 +01:00
|
|
|
wadFee: Amounts.parseOrThrow(x.wad_fee),
|
2021-06-02 13:23:51 +02:00
|
|
|
};
|
2021-12-08 16:23:00 +01:00
|
|
|
let isValid = false;
|
|
|
|
if (ws.insecureTrustExchange) {
|
|
|
|
isValid = true;
|
|
|
|
} else {
|
2022-03-23 21:24:23 +01:00
|
|
|
const { valid: v } = await ws.cryptoApi.isValidWireFee({
|
|
|
|
masterPub: masterPublicKey,
|
|
|
|
type: wireMethod,
|
|
|
|
wf: fee,
|
|
|
|
});
|
|
|
|
isValid = v;
|
2021-12-08 16:23:00 +01:00
|
|
|
}
|
2021-06-02 13:23:51 +02:00
|
|
|
if (!isValid) {
|
|
|
|
throw Error("exchange wire fee signature invalid");
|
|
|
|
}
|
|
|
|
feeList.push(fee);
|
|
|
|
}
|
|
|
|
feesForType[wireMethod] = feeList;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
accounts: wireInfo.accounts,
|
|
|
|
feesForType,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-03-14 18:31:30 +01:00
|
|
|
export interface ExchangeInfo {
|
|
|
|
wire: ExchangeWireJson;
|
|
|
|
keys: ExchangeKeysDownloadResult;
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function downloadExchangeInfo(
|
|
|
|
exchangeBaseUrl: string,
|
|
|
|
http: HttpRequestLibrary,
|
|
|
|
): Promise<ExchangeInfo> {
|
|
|
|
const wireInfo = await downloadExchangeWireInfo(
|
|
|
|
exchangeBaseUrl,
|
|
|
|
http,
|
|
|
|
Duration.getForever(),
|
|
|
|
);
|
|
|
|
const keysInfo = await downloadExchangeKeysInfo(
|
|
|
|
exchangeBaseUrl,
|
|
|
|
http,
|
|
|
|
Duration.getForever(),
|
|
|
|
);
|
|
|
|
return {
|
|
|
|
keys: keysInfo,
|
|
|
|
wire: wireInfo,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
/**
|
2021-06-02 13:23:51 +02:00
|
|
|
* Fetch wire information for an exchange.
|
2019-12-02 00:42:40 +01:00
|
|
|
*
|
2021-06-02 13:23:51 +02:00
|
|
|
* @param exchangeBaseUrl Exchange base URL, assumed to be already normalized.
|
2019-12-02 00:42:40 +01:00
|
|
|
*/
|
2022-03-14 18:31:30 +01:00
|
|
|
async function downloadExchangeWireInfo(
|
2021-06-02 13:23:51 +02:00
|
|
|
exchangeBaseUrl: string,
|
|
|
|
http: HttpRequestLibrary,
|
|
|
|
timeout: Duration,
|
|
|
|
): Promise<ExchangeWireJson> {
|
|
|
|
const reqUrl = new URL("wire", exchangeBaseUrl);
|
|
|
|
reqUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
|
|
|
|
|
|
|
|
const resp = await http.get(reqUrl.href, {
|
|
|
|
timeout,
|
|
|
|
});
|
|
|
|
const wireInfo = await readSuccessResponseJsonOrThrow(
|
|
|
|
resp,
|
|
|
|
codecForExchangeWireJson(),
|
|
|
|
);
|
|
|
|
|
|
|
|
return wireInfo;
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function updateExchangeFromUrl(
|
2019-12-02 00:42:40 +01:00
|
|
|
ws: InternalWalletState,
|
|
|
|
baseUrl: string,
|
2022-03-29 13:47:32 +02:00
|
|
|
options: {
|
|
|
|
forceNow?: boolean;
|
|
|
|
cancellationToken?: CancellationToken;
|
|
|
|
} = {},
|
2021-06-02 13:23:51 +02:00
|
|
|
): Promise<{
|
|
|
|
exchange: ExchangeRecord;
|
|
|
|
exchangeDetails: ExchangeDetailsRecord;
|
|
|
|
}> {
|
2022-03-22 21:16:38 +01:00
|
|
|
const onOpErr = (e: TalerErrorDetail): Promise<void> =>
|
2022-03-28 23:59:16 +02:00
|
|
|
reportExchangeUpdateError(ws, baseUrl, e);
|
2021-06-02 13:23:51 +02:00
|
|
|
return await guardOperationException(
|
2022-03-29 13:47:32 +02:00
|
|
|
() => updateExchangeFromUrlImpl(ws, baseUrl, options),
|
2021-06-02 13:23:51 +02:00
|
|
|
onOpErr,
|
|
|
|
);
|
|
|
|
}
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2021-06-02 13:23:51 +02:00
|
|
|
async function provideExchangeRecord(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
baseUrl: string,
|
2022-03-18 15:32:41 +01:00
|
|
|
now: AbsoluteTime,
|
2022-03-15 04:24:39 +01:00
|
|
|
): Promise<{
|
|
|
|
exchange: ExchangeRecord;
|
|
|
|
exchangeDetails: ExchangeDetailsRecord | undefined;
|
|
|
|
}> {
|
2021-06-09 15:14:17 +02:00
|
|
|
return await ws.db
|
2022-03-15 04:24:39 +01:00
|
|
|
.mktx((x) => ({
|
|
|
|
exchanges: x.exchanges,
|
|
|
|
exchangeDetails: x.exchangeDetails,
|
|
|
|
}))
|
2021-06-09 15:14:17 +02:00
|
|
|
.runReadWrite(async (tx) => {
|
2022-03-15 04:24:39 +01:00
|
|
|
let exchange = await tx.exchanges.get(baseUrl);
|
|
|
|
if (!exchange) {
|
2022-03-18 15:32:41 +01:00
|
|
|
const r: ExchangeRecord = {
|
2021-06-09 15:14:17 +02:00
|
|
|
permanent: true,
|
|
|
|
baseUrl: baseUrl,
|
2022-03-29 13:47:32 +02:00
|
|
|
retryInfo: resetRetryInfo(),
|
2021-06-09 15:14:17 +02:00
|
|
|
detailsPointer: undefined,
|
2021-06-10 16:32:37 +02:00
|
|
|
lastUpdate: undefined,
|
2022-03-18 15:32:41 +01:00
|
|
|
nextUpdate: AbsoluteTime.toTimestamp(now),
|
|
|
|
nextRefreshCheck: AbsoluteTime.toTimestamp(now),
|
2021-06-09 15:14:17 +02:00
|
|
|
};
|
|
|
|
await tx.exchanges.put(r);
|
2022-03-15 04:24:39 +01:00
|
|
|
exchange = r;
|
2021-06-09 15:14:17 +02:00
|
|
|
}
|
2022-03-15 04:24:39 +01:00
|
|
|
const exchangeDetails = await getExchangeDetails(tx, baseUrl);
|
|
|
|
return { exchange, exchangeDetails };
|
2021-06-09 15:14:17 +02:00
|
|
|
});
|
2021-06-02 13:23:51 +02:00
|
|
|
}
|
2020-07-22 10:52:03 +02:00
|
|
|
|
2021-06-02 13:23:51 +02:00
|
|
|
interface ExchangeKeysDownloadResult {
|
|
|
|
masterPublicKey: string;
|
|
|
|
currency: string;
|
2021-11-27 20:56:58 +01:00
|
|
|
auditors: ExchangeAuditor[];
|
2021-06-02 13:23:51 +02:00
|
|
|
currentDenominations: DenominationRecord[];
|
|
|
|
protocolVersion: string;
|
|
|
|
signingKeys: ExchangeSignKeyJson[];
|
2022-03-18 15:32:41 +01:00
|
|
|
reserveClosingDelay: TalerProtocolDuration;
|
|
|
|
expiry: TalerProtocolTimestamp;
|
2021-06-02 13:23:51 +02:00
|
|
|
recoup: Recoup[];
|
2022-03-18 15:32:41 +01:00
|
|
|
listIssueDate: TalerProtocolTimestamp;
|
2021-06-02 13:23:51 +02:00
|
|
|
}
|
2020-08-05 21:00:36 +02:00
|
|
|
|
2021-06-02 13:23:51 +02:00
|
|
|
/**
|
|
|
|
* Download and validate an exchange's /keys data.
|
|
|
|
*/
|
2022-03-14 18:31:30 +01:00
|
|
|
async function downloadExchangeKeysInfo(
|
2021-06-02 13:23:51 +02:00
|
|
|
baseUrl: string,
|
|
|
|
http: HttpRequestLibrary,
|
|
|
|
timeout: Duration,
|
|
|
|
): Promise<ExchangeKeysDownloadResult> {
|
2019-12-02 00:42:40 +01:00
|
|
|
const keysUrl = new URL("keys", baseUrl);
|
|
|
|
keysUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
|
|
|
|
|
2021-06-02 13:23:51 +02:00
|
|
|
const resp = await http.get(keysUrl.href, {
|
|
|
|
timeout,
|
2020-08-20 12:57:20 +02:00
|
|
|
});
|
2021-11-27 20:56:58 +01:00
|
|
|
const exchangeKeysJsonUnchecked = await readSuccessResponseJsonOrThrow(
|
2020-07-22 10:52:03 +02:00
|
|
|
resp,
|
|
|
|
codecForExchangeKeysJson(),
|
|
|
|
);
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2022-05-06 22:29:42 +02:00
|
|
|
logger.trace("received /keys response");
|
|
|
|
logger.trace(`${j2s(exchangeKeysJsonUnchecked)}`);
|
2020-08-05 21:00:36 +02:00
|
|
|
|
2021-11-27 20:56:58 +01:00
|
|
|
if (exchangeKeysJsonUnchecked.denoms.length === 0) {
|
2022-03-22 21:16:38 +01:00
|
|
|
throw TalerError.fromDetail(
|
2020-07-22 10:52:03 +02:00
|
|
|
TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
|
|
|
|
{
|
|
|
|
exchangeBaseUrl: baseUrl,
|
|
|
|
},
|
2022-03-22 21:16:38 +01:00
|
|
|
"exchange doesn't offer any denominations",
|
2020-07-22 10:52:03 +02:00
|
|
|
);
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
2021-11-27 20:56:58 +01:00
|
|
|
const protocolVersion = exchangeKeysJsonUnchecked.version;
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2021-11-23 23:51:12 +01:00
|
|
|
const versionRes = LibtoolVersion.compare(
|
|
|
|
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
|
|
|
protocolVersion,
|
|
|
|
);
|
2020-03-09 09:59:13 +01:00
|
|
|
if (versionRes?.compatible != true) {
|
2022-03-22 21:16:38 +01:00
|
|
|
throw TalerError.fromDetail(
|
2020-07-22 10:52:03 +02:00
|
|
|
TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE,
|
|
|
|
{
|
2020-03-09 09:59:13 +01:00
|
|
|
exchangeProtocolVersion: protocolVersion,
|
|
|
|
walletProtocolVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
|
|
|
|
},
|
2022-03-22 21:16:38 +01:00
|
|
|
"exchange protocol version not compatible with wallet",
|
2020-07-22 10:52:03 +02:00
|
|
|
);
|
2020-03-09 09:59:13 +01:00
|
|
|
}
|
|
|
|
|
2021-06-02 13:23:51 +02:00
|
|
|
const currency = Amounts.parseOrThrow(
|
2021-11-27 20:56:58 +01:00
|
|
|
exchangeKeysJsonUnchecked.denoms[0].value,
|
2021-06-02 13:23:51 +02:00
|
|
|
).currency.toUpperCase();
|
2020-08-05 21:00:36 +02:00
|
|
|
|
2021-06-02 13:23:51 +02:00
|
|
|
return {
|
2021-11-27 20:56:58 +01:00
|
|
|
masterPublicKey: exchangeKeysJsonUnchecked.master_public_key,
|
2021-06-02 13:23:51 +02:00
|
|
|
currency,
|
2021-11-27 20:56:58 +01:00
|
|
|
auditors: exchangeKeysJsonUnchecked.auditors,
|
|
|
|
currentDenominations: exchangeKeysJsonUnchecked.denoms.map((d) =>
|
2021-06-11 13:26:18 +02:00
|
|
|
denominationRecordFromKeys(
|
|
|
|
baseUrl,
|
2021-11-27 20:56:58 +01:00
|
|
|
exchangeKeysJsonUnchecked.master_public_key,
|
|
|
|
exchangeKeysJsonUnchecked.list_issue_date,
|
2021-06-11 13:26:18 +02:00
|
|
|
d,
|
|
|
|
),
|
2019-12-02 00:42:40 +01:00
|
|
|
),
|
2021-11-27 20:56:58 +01:00
|
|
|
protocolVersion: exchangeKeysJsonUnchecked.version,
|
|
|
|
signingKeys: exchangeKeysJsonUnchecked.signkeys,
|
|
|
|
reserveClosingDelay: exchangeKeysJsonUnchecked.reserve_closing_delay,
|
2022-03-18 15:32:41 +01:00
|
|
|
expiry: AbsoluteTime.toTimestamp(
|
|
|
|
getExpiry(resp, {
|
|
|
|
minDuration: durationFromSpec({ hours: 1 }),
|
|
|
|
}),
|
|
|
|
),
|
2021-11-27 20:56:58 +01:00
|
|
|
recoup: exchangeKeysJsonUnchecked.recoup ?? [],
|
|
|
|
listIssueDate: exchangeKeysJsonUnchecked.list_issue_date,
|
2021-06-02 13:23:51 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-01-24 18:12:12 +01:00
|
|
|
export async function downloadTosFromAcceptedFormat(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
baseUrl: string,
|
|
|
|
timeout: Duration,
|
2022-03-07 12:09:38 +01:00
|
|
|
acceptedFormat?: string[],
|
|
|
|
): Promise<ExchangeTosDownloadResult> {
|
2022-01-24 18:12:12 +01:00
|
|
|
let tosFound: ExchangeTosDownloadResult | undefined;
|
|
|
|
//Remove this when exchange supports multiple content-type in accept header
|
|
|
|
if (acceptedFormat)
|
|
|
|
for (const format of acceptedFormat) {
|
|
|
|
const resp = await downloadExchangeWithTermsOfService(
|
|
|
|
baseUrl,
|
|
|
|
ws.http,
|
|
|
|
timeout,
|
|
|
|
format,
|
|
|
|
);
|
|
|
|
if (resp.tosContentType === format) {
|
|
|
|
tosFound = resp;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2022-03-07 12:09:38 +01:00
|
|
|
if (tosFound !== undefined) return tosFound;
|
2022-01-24 18:12:12 +01:00
|
|
|
// If none of the specified format was found try text/plain
|
|
|
|
return await downloadExchangeWithTermsOfService(
|
|
|
|
baseUrl,
|
|
|
|
ws.http,
|
|
|
|
timeout,
|
|
|
|
"text/plain",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-06-02 13:23:51 +02:00
|
|
|
/**
|
|
|
|
* Update or add exchange DB entry by fetching the /keys and /wire information.
|
|
|
|
* Optionally link the reserve entry to the new or existing
|
|
|
|
* exchange entry in then DB.
|
|
|
|
*/
|
|
|
|
async function updateExchangeFromUrlImpl(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
baseUrl: string,
|
2022-03-29 13:47:32 +02:00
|
|
|
options: {
|
|
|
|
forceNow?: boolean;
|
|
|
|
cancellationToken?: CancellationToken;
|
|
|
|
} = {},
|
2021-06-02 13:23:51 +02:00
|
|
|
): Promise<{
|
|
|
|
exchange: ExchangeRecord;
|
|
|
|
exchangeDetails: ExchangeDetailsRecord;
|
|
|
|
}> {
|
2022-03-29 13:47:32 +02:00
|
|
|
const forceNow = options.forceNow ?? false;
|
2022-03-15 04:24:39 +01:00
|
|
|
logger.info(`updating exchange info for ${baseUrl}, forced: ${forceNow}`);
|
2022-03-18 15:32:41 +01:00
|
|
|
const now = AbsoluteTime.now();
|
2021-06-02 13:23:51 +02:00
|
|
|
baseUrl = canonicalizeBaseUrl(baseUrl);
|
|
|
|
|
2022-03-18 15:32:41 +01:00
|
|
|
const { exchange, exchangeDetails } = await provideExchangeRecord(
|
|
|
|
ws,
|
|
|
|
baseUrl,
|
|
|
|
now,
|
|
|
|
);
|
2022-03-15 04:24:39 +01:00
|
|
|
|
2022-03-18 15:32:41 +01:00
|
|
|
if (
|
|
|
|
!forceNow &&
|
|
|
|
exchangeDetails !== undefined &&
|
|
|
|
!AbsoluteTime.isExpired(AbsoluteTime.fromTimestamp(exchange.nextUpdate))
|
|
|
|
) {
|
2022-03-15 04:24:39 +01:00
|
|
|
logger.info("using existing exchange info");
|
|
|
|
return { exchange, exchangeDetails };
|
2021-06-10 16:32:37 +02:00
|
|
|
}
|
|
|
|
|
2022-03-29 13:47:32 +02:00
|
|
|
await setupExchangeUpdateRetry(ws, baseUrl, { reset: forceNow });
|
2022-03-28 23:59:16 +02:00
|
|
|
|
2021-06-02 13:23:51 +02:00
|
|
|
logger.info("updating exchange /keys info");
|
|
|
|
|
2022-01-24 18:12:12 +01:00
|
|
|
const timeout = getExchangeRequestTimeout();
|
2021-06-02 13:23:51 +02:00
|
|
|
|
2022-03-14 18:31:30 +01:00
|
|
|
const keysInfo = await downloadExchangeKeysInfo(baseUrl, ws.http, timeout);
|
2021-06-02 13:23:51 +02:00
|
|
|
|
2021-08-06 16:27:18 +02:00
|
|
|
logger.info("updating exchange /wire info");
|
2022-03-14 18:31:30 +01:00
|
|
|
const wireInfoDownload = await downloadExchangeWireInfo(
|
2021-06-02 13:23:51 +02:00
|
|
|
baseUrl,
|
|
|
|
ws.http,
|
|
|
|
timeout,
|
2019-12-02 00:42:40 +01:00
|
|
|
);
|
|
|
|
|
2021-08-06 16:27:18 +02:00
|
|
|
logger.info("validating exchange /wire info");
|
|
|
|
|
2021-11-27 20:56:58 +01:00
|
|
|
const version = LibtoolVersion.parseVersion(keysInfo.protocolVersion);
|
|
|
|
if (!version) {
|
|
|
|
// Should have been validated earlier.
|
|
|
|
throw Error("unexpected invalid version");
|
|
|
|
}
|
|
|
|
|
2021-06-02 13:23:51 +02:00
|
|
|
const wireInfo = await validateWireInfo(
|
2021-12-08 16:23:00 +01:00
|
|
|
ws,
|
2021-11-27 20:56:58 +01:00
|
|
|
version.current,
|
2021-06-02 13:23:51 +02:00
|
|
|
wireInfoDownload,
|
|
|
|
keysInfo.masterPublicKey,
|
|
|
|
);
|
2020-08-05 21:00:36 +02:00
|
|
|
|
2021-08-06 16:27:18 +02:00
|
|
|
logger.info("finished validating exchange /wire info");
|
|
|
|
|
2022-03-07 12:09:38 +01:00
|
|
|
const tosDownload = await downloadTosFromAcceptedFormat(
|
|
|
|
ws,
|
|
|
|
baseUrl,
|
|
|
|
timeout,
|
2022-03-29 13:47:32 +02:00
|
|
|
["text/plain"],
|
2022-03-07 12:09:38 +01:00
|
|
|
);
|
2022-03-18 15:32:41 +01:00
|
|
|
const tosHasBeenAccepted =
|
|
|
|
exchangeDetails?.termsOfServiceAcceptedEtag === tosDownload.tosEtag;
|
2022-01-24 18:12:12 +01:00
|
|
|
|
|
|
|
let recoupGroupId: string | undefined;
|
2020-03-11 20:14:28 +01:00
|
|
|
|
2021-08-06 16:27:18 +02:00
|
|
|
logger.trace("updating exchange info in database");
|
|
|
|
|
2021-06-09 15:14:17 +02:00
|
|
|
const updated = await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
exchanges: x.exchanges,
|
|
|
|
exchangeDetails: x.exchangeDetails,
|
|
|
|
denominations: x.denominations,
|
|
|
|
coins: x.coins,
|
|
|
|
refreshGroups: x.refreshGroups,
|
|
|
|
recoupGroups: x.recoupGroups,
|
|
|
|
}))
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const r = await tx.exchanges.get(baseUrl);
|
2019-12-02 00:42:40 +01:00
|
|
|
if (!r) {
|
2020-08-14 12:23:50 +02:00
|
|
|
logger.warn(`exchange ${baseUrl} no longer present`);
|
2019-12-02 00:42:40 +01:00
|
|
|
return;
|
|
|
|
}
|
2021-06-02 13:23:51 +02:00
|
|
|
let details = await getExchangeDetails(tx, r.baseUrl);
|
|
|
|
if (details) {
|
2019-12-02 00:42:40 +01:00
|
|
|
// FIXME: We need to do some consistency checks!
|
|
|
|
}
|
2020-03-13 14:34:16 +01:00
|
|
|
// FIXME: validate signing keys and merge with old set
|
2021-06-02 13:23:51 +02:00
|
|
|
details = {
|
|
|
|
auditors: keysInfo.auditors,
|
|
|
|
currency: keysInfo.currency,
|
|
|
|
masterPublicKey: keysInfo.masterPublicKey,
|
|
|
|
protocolVersion: keysInfo.protocolVersion,
|
|
|
|
signingKeys: keysInfo.signingKeys,
|
|
|
|
reserveClosingDelay: keysInfo.reserveClosingDelay,
|
|
|
|
exchangeBaseUrl: r.baseUrl,
|
|
|
|
wireInfo,
|
|
|
|
termsOfServiceText: tosDownload.tosText,
|
2022-03-18 15:32:41 +01:00
|
|
|
termsOfServiceAcceptedEtag: tosHasBeenAccepted
|
|
|
|
? tosDownload.tosEtag
|
|
|
|
: undefined,
|
2021-09-13 20:32:06 +02:00
|
|
|
termsOfServiceContentType: tosDownload.tosContentType,
|
2021-06-02 13:23:51 +02:00
|
|
|
termsOfServiceLastEtag: tosDownload.tosEtag,
|
2022-03-18 15:32:41 +01:00
|
|
|
termsOfServiceAcceptedTimestamp: TalerProtocolTimestamp.now(),
|
2019-12-02 00:42:40 +01:00
|
|
|
};
|
2021-06-02 13:23:51 +02:00
|
|
|
// FIXME: only update if pointer got updated
|
2022-03-28 23:59:16 +02:00
|
|
|
delete r.lastError;
|
|
|
|
delete r.retryInfo;
|
2022-03-18 15:32:41 +01:00
|
|
|
r.lastUpdate = TalerProtocolTimestamp.now();
|
2021-08-23 22:28:36 +02:00
|
|
|
r.nextUpdate = keysInfo.expiry;
|
|
|
|
// New denominations might be available.
|
2022-03-18 15:32:41 +01:00
|
|
|
r.nextRefreshCheck = TalerProtocolTimestamp.now();
|
2021-06-02 13:23:51 +02:00
|
|
|
r.detailsPointer = {
|
|
|
|
currency: details.currency,
|
|
|
|
masterPublicKey: details.masterPublicKey,
|
|
|
|
// FIXME: only change if pointer really changed
|
2022-03-18 15:32:41 +01:00
|
|
|
updateClock: TalerProtocolTimestamp.now(),
|
2021-11-23 23:51:12 +01:00
|
|
|
protocolVersionRange: keysInfo.protocolVersion,
|
2021-06-02 13:23:51 +02:00
|
|
|
};
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.exchanges.put(r);
|
|
|
|
await tx.exchangeDetails.put(details);
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2022-01-13 22:01:14 +01:00
|
|
|
logger.info("updating denominations in database");
|
2021-08-23 22:28:36 +02:00
|
|
|
const currentDenomSet = new Set<string>(
|
|
|
|
keysInfo.currentDenominations.map((x) => x.denomPubHash),
|
|
|
|
);
|
2021-06-02 13:23:51 +02:00
|
|
|
for (const currentDenom of keysInfo.currentDenominations) {
|
2021-06-09 15:14:17 +02:00
|
|
|
const oldDenom = await tx.denominations.get([
|
2019-12-02 00:42:40 +01:00
|
|
|
baseUrl,
|
2021-06-02 13:23:51 +02:00
|
|
|
currentDenom.denomPubHash,
|
2019-12-02 00:42:40 +01:00
|
|
|
]);
|
|
|
|
if (oldDenom) {
|
2021-08-23 22:28:36 +02:00
|
|
|
// FIXME: Do consistency check, report to auditor if necessary.
|
2019-12-02 00:42:40 +01:00
|
|
|
} else {
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.denominations.put(currentDenom);
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
}
|
2021-08-23 22:28:36 +02:00
|
|
|
|
|
|
|
// Update list issue date for all denominations,
|
|
|
|
// and mark non-offered denominations as such.
|
|
|
|
await tx.denominations.indexes.byExchangeBaseUrl
|
|
|
|
.iter(r.baseUrl)
|
|
|
|
.forEachAsync(async (x) => {
|
|
|
|
if (!currentDenomSet.has(x.denomPubHash)) {
|
|
|
|
// FIXME: Here, an auditor report should be created, unless
|
|
|
|
// the denomination is really legally expired.
|
|
|
|
if (x.isOffered) {
|
|
|
|
x.isOffered = false;
|
|
|
|
logger.info(
|
|
|
|
`setting denomination ${x.denomPubHash} to offered=false`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
x.listIssueDate = keysInfo.listIssueDate;
|
|
|
|
if (!x.isOffered) {
|
|
|
|
x.isOffered = true;
|
|
|
|
logger.info(
|
|
|
|
`setting denomination ${x.denomPubHash} to offered=true`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
await tx.denominations.put(x);
|
|
|
|
});
|
|
|
|
|
2021-08-06 16:27:18 +02:00
|
|
|
logger.trace("done updating denominations in database");
|
2020-03-11 20:14:28 +01:00
|
|
|
|
|
|
|
// Handle recoup
|
2021-06-02 13:23:51 +02:00
|
|
|
const recoupDenomList = keysInfo.recoup;
|
2020-03-11 20:14:28 +01:00
|
|
|
const newlyRevokedCoinPubs: string[] = [];
|
2020-07-23 15:54:00 +02:00
|
|
|
logger.trace("recoup list from exchange", recoupDenomList);
|
2020-03-12 14:55:38 +01:00
|
|
|
for (const recoupInfo of recoupDenomList) {
|
2021-06-09 15:14:17 +02:00
|
|
|
const oldDenom = await tx.denominations.get([
|
2020-09-08 17:33:10 +02:00
|
|
|
r.baseUrl,
|
2020-03-12 14:55:38 +01:00
|
|
|
recoupInfo.h_denom_pub,
|
2020-09-08 17:33:10 +02:00
|
|
|
]);
|
2020-03-11 20:14:28 +01:00
|
|
|
if (!oldDenom) {
|
|
|
|
// We never even knew about the revoked denomination, all good.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (oldDenom.isRevoked) {
|
|
|
|
// We already marked the denomination as revoked,
|
|
|
|
// this implies we revoked all coins
|
2020-08-14 12:23:50 +02:00
|
|
|
logger.trace("denom already revoked");
|
2020-03-11 20:14:28 +01:00
|
|
|
continue;
|
|
|
|
}
|
2022-01-11 12:48:32 +01:00
|
|
|
logger.info("revoking denom", recoupInfo.h_denom_pub);
|
2020-03-11 20:14:28 +01:00
|
|
|
oldDenom.isRevoked = true;
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.denominations.put(oldDenom);
|
|
|
|
const affectedCoins = await tx.coins.indexes.byDenomPubHash
|
|
|
|
.iter(recoupInfo.h_denom_pub)
|
2020-03-11 20:14:28 +01:00
|
|
|
.toArray();
|
|
|
|
for (const ac of affectedCoins) {
|
|
|
|
newlyRevokedCoinPubs.push(ac.coinPub);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (newlyRevokedCoinPubs.length != 0) {
|
2022-01-11 12:48:32 +01:00
|
|
|
logger.info("recouping coins", newlyRevokedCoinPubs);
|
2021-06-22 12:18:12 +02:00
|
|
|
recoupGroupId = await ws.recoupOps.createRecoupGroup(
|
|
|
|
ws,
|
|
|
|
tx,
|
|
|
|
newlyRevokedCoinPubs,
|
|
|
|
);
|
2020-03-11 20:14:28 +01:00
|
|
|
}
|
2021-06-02 13:23:51 +02:00
|
|
|
return {
|
|
|
|
exchange: r,
|
|
|
|
exchangeDetails: details,
|
|
|
|
};
|
2021-06-09 15:14:17 +02:00
|
|
|
});
|
2020-03-11 20:14:28 +01:00
|
|
|
|
|
|
|
if (recoupGroupId) {
|
|
|
|
// Asynchronously start recoup. This doesn't need to finish
|
|
|
|
// for the exchange update to be considered finished.
|
2021-06-22 12:18:12 +02:00
|
|
|
ws.recoupOps.processRecoupGroup(ws, recoupGroupId).catch((e) => {
|
2020-08-14 12:23:50 +02:00
|
|
|
logger.error("error while recouping coins:", e);
|
2020-03-11 20:14:28 +01:00
|
|
|
});
|
|
|
|
}
|
2020-08-05 21:00:36 +02:00
|
|
|
|
2021-06-02 13:23:51 +02:00
|
|
|
if (!updated) {
|
|
|
|
throw Error("something went wrong with updating the exchange");
|
2019-12-16 12:53:22 +01:00
|
|
|
}
|
|
|
|
|
2021-08-06 16:27:18 +02:00
|
|
|
logger.trace("done updating exchange info in database");
|
|
|
|
|
2021-11-24 12:55:37 +01:00
|
|
|
ws.notify({
|
|
|
|
type: NotificationType.ExchangeAdded,
|
|
|
|
});
|
2021-06-02 13:23:51 +02:00
|
|
|
return {
|
|
|
|
exchange: updated.exchange,
|
|
|
|
exchangeDetails: updated.exchangeDetails,
|
2019-12-09 19:59:08 +01:00
|
|
|
};
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export async function getExchangePaytoUri(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
exchangeBaseUrl: string,
|
|
|
|
supportedTargetTypes: string[],
|
|
|
|
): Promise<string> {
|
|
|
|
// We do the update here, since the exchange might not even exist
|
|
|
|
// yet in our database.
|
2021-06-09 15:14:17 +02:00
|
|
|
const details = await getExchangeDetails
|
|
|
|
.makeContext(ws.db)
|
|
|
|
.runReadOnly(async (tx) => {
|
2021-06-02 13:23:51 +02:00
|
|
|
return getExchangeDetails(tx, exchangeBaseUrl);
|
2021-06-09 15:14:17 +02:00
|
|
|
});
|
2021-06-02 13:23:51 +02:00
|
|
|
const accounts = details?.wireInfo.accounts ?? [];
|
|
|
|
for (const account of accounts) {
|
2020-01-19 19:02:47 +01:00
|
|
|
const res = parsePaytoUri(account.payto_uri);
|
2019-12-02 00:42:40 +01:00
|
|
|
if (!res) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (supportedTargetTypes.includes(res.targetType)) {
|
2020-01-19 19:02:47 +01:00
|
|
|
return account.payto_uri;
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
throw Error("no matching exchange account found");
|
|
|
|
}
|
2021-06-17 15:49:05 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if and how an exchange is trusted and/or audited.
|
|
|
|
*/
|
|
|
|
export async function getExchangeTrust(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
exchangeInfo: ExchangeRecord,
|
|
|
|
): Promise<TrustInfo> {
|
|
|
|
let isTrusted = false;
|
|
|
|
let isAudited = false;
|
|
|
|
|
|
|
|
return await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
exchanges: x.exchanges,
|
|
|
|
exchangeDetails: x.exchangeDetails,
|
|
|
|
exchangesTrustStore: x.exchangeTrust,
|
|
|
|
auditorTrust: x.auditorTrust,
|
|
|
|
}))
|
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
const exchangeDetails = await getExchangeDetails(
|
|
|
|
tx,
|
|
|
|
exchangeInfo.baseUrl,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!exchangeDetails) {
|
|
|
|
throw Error(`exchange ${exchangeInfo.baseUrl} details not available`);
|
|
|
|
}
|
2022-01-13 22:01:14 +01:00
|
|
|
const exchangeTrustRecord =
|
|
|
|
await tx.exchangesTrustStore.indexes.byExchangeMasterPub.get(
|
|
|
|
exchangeDetails.masterPublicKey,
|
|
|
|
);
|
2021-06-17 15:49:05 +02:00
|
|
|
if (
|
|
|
|
exchangeTrustRecord &&
|
|
|
|
exchangeTrustRecord.uids.length > 0 &&
|
|
|
|
exchangeTrustRecord.currency === exchangeDetails.currency
|
|
|
|
) {
|
|
|
|
isTrusted = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const auditor of exchangeDetails.auditors) {
|
2022-01-13 22:01:14 +01:00
|
|
|
const auditorTrustRecord =
|
|
|
|
await tx.auditorTrust.indexes.byAuditorPub.get(auditor.auditor_pub);
|
2021-06-17 15:49:05 +02:00
|
|
|
if (auditorTrustRecord && auditorTrustRecord.uids.length > 0) {
|
|
|
|
isAudited = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return { isTrusted, isAudited };
|
|
|
|
});
|
|
|
|
}
|