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 { InternalWalletState } from "./state";
|
2020-03-09 09:49:22 +01:00
|
|
|
import {
|
|
|
|
ExchangeKeysJson,
|
|
|
|
Denomination,
|
|
|
|
codecForExchangeKeysJson,
|
|
|
|
codecForExchangeWireJson,
|
|
|
|
} from "../types/talerTypes";
|
2019-12-19 20:42:49 +01:00
|
|
|
import { OperationError } from "../types/walletTypes";
|
2019-12-02 00:42:40 +01:00
|
|
|
import {
|
|
|
|
ExchangeRecord,
|
|
|
|
ExchangeUpdateStatus,
|
|
|
|
Stores,
|
|
|
|
DenominationRecord,
|
|
|
|
DenominationStatus,
|
|
|
|
WireFee,
|
2019-12-16 12:53:22 +01:00
|
|
|
ExchangeUpdateReason,
|
|
|
|
ExchangeUpdatedEventRecord,
|
2020-03-11 20:14:28 +01:00
|
|
|
CoinStatus,
|
2019-12-12 20:53:15 +01:00
|
|
|
} from "../types/dbTypes";
|
2020-03-09 09:49:22 +01:00
|
|
|
import { canonicalizeBaseUrl } from "../util/helpers";
|
2019-12-02 00:42:40 +01:00
|
|
|
import * as Amounts from "../util/amounts";
|
|
|
|
import { parsePaytoUri } from "../util/payto";
|
2019-12-06 00:24:34 +01:00
|
|
|
import {
|
|
|
|
OperationFailedAndReportedError,
|
|
|
|
guardOperationException,
|
|
|
|
} from "./errors";
|
2020-03-09 09:59:13 +01:00
|
|
|
import {
|
|
|
|
WALLET_CACHE_BREAKER_CLIENT_VERSION,
|
|
|
|
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
|
|
|
} from "./versions";
|
2019-12-19 20:42:49 +01:00
|
|
|
import { getTimestampNow } from "../util/time";
|
2020-03-09 09:59:13 +01:00
|
|
|
import { compare } from "../util/libtoolVersion";
|
2020-03-11 20:14:28 +01:00
|
|
|
import { createRecoupGroup, processRecoupGroup } from "./recoup";
|
2019-12-02 00:42:40 +01:00
|
|
|
|
|
|
|
async function denominationRecordFromKeys(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
exchangeBaseUrl: string,
|
|
|
|
denomIn: Denomination,
|
|
|
|
): Promise<DenominationRecord> {
|
|
|
|
const denomPubHash = await ws.cryptoApi.hashDenomPub(denomIn.denom_pub);
|
|
|
|
const d: DenominationRecord = {
|
|
|
|
denomPub: denomIn.denom_pub,
|
|
|
|
denomPubHash,
|
|
|
|
exchangeBaseUrl,
|
|
|
|
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,
|
2019-12-02 00:42:40 +01:00
|
|
|
status: DenominationStatus.Unverified,
|
|
|
|
value: Amounts.parseOrThrow(denomIn.value),
|
|
|
|
};
|
|
|
|
return d;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function setExchangeError(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
baseUrl: string,
|
|
|
|
err: OperationError,
|
|
|
|
): Promise<void> {
|
2020-03-09 12:07:46 +01:00
|
|
|
console.log(`last error for exchange ${baseUrl}:`, err);
|
2019-12-02 00:42:40 +01:00
|
|
|
const mut = (exchange: ExchangeRecord) => {
|
|
|
|
exchange.lastError = err;
|
|
|
|
return exchange;
|
|
|
|
};
|
2019-12-16 12:53:22 +01:00
|
|
|
await ws.db.mutate(Stores.exchanges, baseUrl, mut);
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fetch the exchange's /keys and update our database accordingly.
|
|
|
|
*
|
|
|
|
* Exceptions thrown in this method must be caught and reported
|
|
|
|
* in the pending operations.
|
|
|
|
*/
|
|
|
|
async function updateExchangeWithKeys(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
baseUrl: string,
|
|
|
|
): Promise<void> {
|
2019-12-16 12:53:22 +01:00
|
|
|
const existingExchangeRecord = await ws.db.get(Stores.exchanges, baseUrl);
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2019-12-16 12:53:22 +01:00
|
|
|
if (existingExchangeRecord?.updateStatus != ExchangeUpdateStatus.FetchKeys) {
|
2019-12-02 00:42:40 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
const keysUrl = new URL("keys", baseUrl);
|
|
|
|
keysUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
|
|
|
|
|
|
|
|
let keysResp;
|
|
|
|
try {
|
2019-12-09 13:29:11 +01:00
|
|
|
const r = await ws.http.get(keysUrl.href);
|
|
|
|
if (r.status !== 200) {
|
|
|
|
throw Error(`unexpected status for keys: ${r.status}`);
|
|
|
|
}
|
|
|
|
keysResp = await r.json();
|
2019-12-02 00:42:40 +01:00
|
|
|
} catch (e) {
|
|
|
|
const m = `Fetching keys failed: ${e.message}`;
|
2020-03-12 14:55:38 +01:00
|
|
|
const opErr = {
|
2019-12-02 00:42:40 +01:00
|
|
|
type: "network",
|
|
|
|
details: {
|
|
|
|
requestUrl: e.config?.url,
|
|
|
|
},
|
|
|
|
message: m,
|
2020-03-12 14:55:38 +01:00
|
|
|
};
|
|
|
|
await setExchangeError(ws, baseUrl, opErr);
|
|
|
|
throw new OperationFailedAndReportedError(opErr);
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
2019-12-19 20:42:49 +01:00
|
|
|
let exchangeKeysJson: ExchangeKeysJson;
|
2019-12-02 00:42:40 +01:00
|
|
|
try {
|
2019-12-19 20:42:49 +01:00
|
|
|
exchangeKeysJson = codecForExchangeKeysJson().decode(keysResp);
|
2019-12-02 00:42:40 +01:00
|
|
|
} catch (e) {
|
|
|
|
const m = `Parsing /keys response failed: ${e.message}`;
|
2020-03-12 14:55:38 +01:00
|
|
|
const opErr = {
|
2019-12-02 00:42:40 +01:00
|
|
|
type: "protocol-violation",
|
|
|
|
details: {},
|
|
|
|
message: m,
|
2020-03-12 14:55:38 +01:00
|
|
|
};
|
|
|
|
await setExchangeError(ws, baseUrl, opErr);
|
|
|
|
throw new OperationFailedAndReportedError(opErr);
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
2020-03-09 09:49:22 +01:00
|
|
|
const lastUpdateTimestamp = exchangeKeysJson.list_issue_date;
|
2019-12-02 00:42:40 +01:00
|
|
|
if (!lastUpdateTimestamp) {
|
|
|
|
const m = `Parsing /keys response failed: invalid list_issue_date.`;
|
2020-03-12 14:55:38 +01:00
|
|
|
const opErr = {
|
2019-12-02 00:42:40 +01:00
|
|
|
type: "protocol-violation",
|
|
|
|
details: {},
|
|
|
|
message: m,
|
2020-03-12 14:55:38 +01:00
|
|
|
};
|
|
|
|
await setExchangeError(ws, baseUrl, opErr);
|
|
|
|
throw new OperationFailedAndReportedError(opErr);
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (exchangeKeysJson.denoms.length === 0) {
|
|
|
|
const m = "exchange doesn't offer any denominations";
|
2020-03-12 14:55:38 +01:00
|
|
|
const opErr = {
|
2019-12-02 00:42:40 +01:00
|
|
|
type: "protocol-violation",
|
|
|
|
details: {},
|
|
|
|
message: m,
|
2020-03-12 14:55:38 +01:00
|
|
|
};
|
|
|
|
await setExchangeError(ws, baseUrl, opErr);
|
|
|
|
throw new OperationFailedAndReportedError(opErr);
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const protocolVersion = exchangeKeysJson.version;
|
|
|
|
if (!protocolVersion) {
|
|
|
|
const m = "outdate exchange, no version in /keys response";
|
2020-03-12 14:55:38 +01:00
|
|
|
const opErr = {
|
2019-12-02 00:42:40 +01:00
|
|
|
type: "protocol-violation",
|
|
|
|
details: {},
|
|
|
|
message: m,
|
2020-03-12 14:55:38 +01:00
|
|
|
};
|
|
|
|
await setExchangeError(ws, baseUrl, opErr);
|
|
|
|
throw new OperationFailedAndReportedError(opErr);
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
2020-03-09 09:59:13 +01:00
|
|
|
const versionRes = compare(WALLET_EXCHANGE_PROTOCOL_VERSION, protocolVersion);
|
|
|
|
if (versionRes?.compatible != true) {
|
|
|
|
const m = "exchange protocol version not compatible with wallet";
|
2020-03-12 14:55:38 +01:00
|
|
|
const opErr = {
|
2020-03-09 09:59:13 +01:00
|
|
|
type: "protocol-incompatible",
|
|
|
|
details: {
|
|
|
|
exchangeProtocolVersion: protocolVersion,
|
|
|
|
walletProtocolVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
|
|
|
|
},
|
|
|
|
message: m,
|
2020-03-12 14:55:38 +01:00
|
|
|
};
|
|
|
|
await setExchangeError(ws, baseUrl, opErr);
|
|
|
|
throw new OperationFailedAndReportedError(opErr);
|
2020-03-09 09:59:13 +01:00
|
|
|
}
|
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
const currency = Amounts.parseOrThrow(exchangeKeysJson.denoms[0].value)
|
|
|
|
.currency;
|
|
|
|
|
|
|
|
const newDenominations = await Promise.all(
|
|
|
|
exchangeKeysJson.denoms.map(d =>
|
|
|
|
denominationRecordFromKeys(ws, baseUrl, d),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
2020-03-11 20:14:28 +01:00
|
|
|
let recoupGroupId: string | undefined = undefined;
|
|
|
|
|
2019-12-12 22:39:45 +01:00
|
|
|
await ws.db.runWithWriteTransaction(
|
2020-03-12 14:55:38 +01:00
|
|
|
[Stores.exchanges, Stores.denominations, Stores.recoupGroups, Stores.coins],
|
2019-12-02 00:42:40 +01:00
|
|
|
async tx => {
|
|
|
|
const r = await tx.get(Stores.exchanges, baseUrl);
|
|
|
|
if (!r) {
|
|
|
|
console.warn(`exchange ${baseUrl} no longer present`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (r.details) {
|
|
|
|
// 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
|
2019-12-02 00:42:40 +01:00
|
|
|
r.details = {
|
|
|
|
auditors: exchangeKeysJson.auditors,
|
|
|
|
currency: currency,
|
|
|
|
lastUpdateTime: lastUpdateTimestamp,
|
|
|
|
masterPublicKey: exchangeKeysJson.master_public_key,
|
|
|
|
protocolVersion: protocolVersion,
|
2020-03-13 14:34:16 +01:00
|
|
|
signingKeys: exchangeKeysJson.signkeys,
|
2019-12-02 00:42:40 +01:00
|
|
|
};
|
2019-12-16 12:53:22 +01:00
|
|
|
r.updateStatus = ExchangeUpdateStatus.FetchWire;
|
2019-12-02 00:42:40 +01:00
|
|
|
r.lastError = undefined;
|
|
|
|
await tx.put(Stores.exchanges, r);
|
|
|
|
|
|
|
|
for (const newDenom of newDenominations) {
|
|
|
|
const oldDenom = await tx.get(Stores.denominations, [
|
|
|
|
baseUrl,
|
|
|
|
newDenom.denomPub,
|
|
|
|
]);
|
|
|
|
if (oldDenom) {
|
|
|
|
// FIXME: Do consistency check
|
|
|
|
} else {
|
|
|
|
await tx.put(Stores.denominations, newDenom);
|
|
|
|
}
|
|
|
|
}
|
2020-03-11 20:14:28 +01:00
|
|
|
|
|
|
|
// Handle recoup
|
|
|
|
const recoupDenomList = exchangeKeysJson.recoup ?? [];
|
|
|
|
const newlyRevokedCoinPubs: string[] = [];
|
2020-03-12 14:55:38 +01:00
|
|
|
console.log("recoup list from exchange", recoupDenomList);
|
|
|
|
for (const recoupInfo of recoupDenomList) {
|
2020-03-11 20:14:28 +01:00
|
|
|
const oldDenom = await tx.getIndexed(
|
|
|
|
Stores.denominations.denomPubHashIndex,
|
2020-03-12 14:55:38 +01:00
|
|
|
recoupInfo.h_denom_pub,
|
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-03-12 14:55:38 +01:00
|
|
|
console.log("denom already revoked");
|
2020-03-11 20:14:28 +01:00
|
|
|
continue;
|
|
|
|
}
|
2020-03-12 14:55:38 +01:00
|
|
|
console.log("revoking denom", recoupInfo.h_denom_pub);
|
2020-03-11 20:14:28 +01:00
|
|
|
oldDenom.isRevoked = true;
|
|
|
|
await tx.put(Stores.denominations, oldDenom);
|
|
|
|
const affectedCoins = await tx
|
2020-03-12 14:55:38 +01:00
|
|
|
.iterIndexed(Stores.coins.denomPubHashIndex, 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) {
|
2020-03-12 14:55:38 +01:00
|
|
|
console.log("recouping coins", newlyRevokedCoinPubs);
|
2020-03-11 20:14:28 +01:00
|
|
|
await createRecoupGroup(ws, tx, newlyRevokedCoinPubs);
|
|
|
|
}
|
2019-12-02 00:42:40 +01: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.
|
2020-03-12 14:55:38 +01:00
|
|
|
processRecoupGroup(ws, recoupGroupId).catch(e => {
|
2020-03-11 20:14:28 +01:00
|
|
|
console.log("error while recouping coins:", e);
|
|
|
|
});
|
|
|
|
}
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
2019-12-16 12:53:22 +01:00
|
|
|
async function updateExchangeFinalize(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
exchangeBaseUrl: string,
|
|
|
|
) {
|
|
|
|
const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
|
|
|
|
if (!exchange) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (exchange.updateStatus != ExchangeUpdateStatus.FinalizeUpdate) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
await ws.db.runWithWriteTransaction(
|
|
|
|
[Stores.exchanges, Stores.exchangeUpdatedEvents],
|
|
|
|
async tx => {
|
|
|
|
const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
|
|
|
|
if (!r) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (r.updateStatus != ExchangeUpdateStatus.FinalizeUpdate) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
r.updateStatus = ExchangeUpdateStatus.Finished;
|
|
|
|
await tx.put(Stores.exchanges, r);
|
|
|
|
const updateEvent: ExchangeUpdatedEventRecord = {
|
|
|
|
exchangeBaseUrl: exchange.baseUrl,
|
|
|
|
timestamp: getTimestampNow(),
|
|
|
|
};
|
|
|
|
await tx.put(Stores.exchangeUpdatedEvents, updateEvent);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-12-09 19:59:08 +01:00
|
|
|
async function updateExchangeWithTermsOfService(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
exchangeBaseUrl: string,
|
|
|
|
) {
|
2019-12-12 22:39:45 +01:00
|
|
|
const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
|
2019-12-09 19:59:08 +01:00
|
|
|
if (!exchange) {
|
|
|
|
return;
|
|
|
|
}
|
2019-12-16 12:53:22 +01:00
|
|
|
if (exchange.updateStatus != ExchangeUpdateStatus.FetchTerms) {
|
2019-12-09 19:59:08 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
const reqUrl = new URL("terms", exchangeBaseUrl);
|
|
|
|
reqUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
|
|
|
|
const headers = {
|
|
|
|
Accept: "text/plain",
|
|
|
|
};
|
|
|
|
|
|
|
|
const resp = await ws.http.get(reqUrl.href, { headers });
|
|
|
|
if (resp.status !== 200) {
|
|
|
|
throw Error(`/terms response has unexpected status code (${resp.status})`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const tosText = await resp.text();
|
|
|
|
const tosEtag = resp.headers.get("etag") || undefined;
|
|
|
|
|
2019-12-12 22:39:45 +01:00
|
|
|
await ws.db.runWithWriteTransaction([Stores.exchanges], async tx => {
|
2019-12-09 19:59:08 +01:00
|
|
|
const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
|
|
|
|
if (!r) {
|
|
|
|
return;
|
|
|
|
}
|
2019-12-16 12:53:22 +01:00
|
|
|
if (r.updateStatus != ExchangeUpdateStatus.FetchTerms) {
|
2019-12-09 19:59:08 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
r.termsOfServiceText = tosText;
|
|
|
|
r.termsOfServiceLastEtag = tosEtag;
|
2019-12-16 12:53:22 +01:00
|
|
|
r.updateStatus = ExchangeUpdateStatus.FinalizeUpdate;
|
2019-12-09 19:59:08 +01:00
|
|
|
await tx.put(Stores.exchanges, r);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function acceptExchangeTermsOfService(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
exchangeBaseUrl: string,
|
|
|
|
etag: string | undefined,
|
|
|
|
) {
|
2019-12-12 22:39:45 +01:00
|
|
|
await ws.db.runWithWriteTransaction([Stores.exchanges], async tx => {
|
2019-12-09 19:59:08 +01:00
|
|
|
const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
|
|
|
|
if (!r) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
r.termsOfServiceAcceptedEtag = etag;
|
|
|
|
r.termsOfServiceAcceptedTimestamp = getTimestampNow();
|
|
|
|
await tx.put(Stores.exchanges, r);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
/**
|
|
|
|
* Fetch wire information for an exchange and store it in the database.
|
|
|
|
*
|
|
|
|
* @param exchangeBaseUrl Exchange base URL, assumed to be already normalized.
|
|
|
|
*/
|
|
|
|
async function updateExchangeWithWireInfo(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
exchangeBaseUrl: string,
|
|
|
|
) {
|
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) {
|
|
|
|
return;
|
|
|
|
}
|
2019-12-16 12:53:22 +01:00
|
|
|
if (exchange.updateStatus != ExchangeUpdateStatus.FetchWire) {
|
2019-12-02 00:42:40 +01:00
|
|
|
return;
|
|
|
|
}
|
2019-12-05 23:07:46 +01:00
|
|
|
const details = exchange.details;
|
|
|
|
if (!details) {
|
|
|
|
throw Error("invalid exchange state");
|
|
|
|
}
|
2019-12-02 00:42:40 +01:00
|
|
|
const reqUrl = new URL("wire", exchangeBaseUrl);
|
2019-12-05 23:07:46 +01:00
|
|
|
reqUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
|
2019-12-02 00:42:40 +01:00
|
|
|
|
|
|
|
const resp = await ws.http.get(reqUrl.href);
|
2019-12-09 13:29:11 +01:00
|
|
|
if (resp.status !== 200) {
|
|
|
|
throw Error(`/wire response has unexpected status code (${resp.status})`);
|
|
|
|
}
|
|
|
|
const wiJson = await resp.json();
|
2019-12-02 00:42:40 +01:00
|
|
|
if (!wiJson) {
|
|
|
|
throw Error("/wire response malformed");
|
|
|
|
}
|
2019-12-19 20:42:49 +01:00
|
|
|
const wireInfo = codecForExchangeWireJson().decode(wiJson);
|
2019-12-05 23:07:46 +01:00
|
|
|
for (const a of wireInfo.accounts) {
|
|
|
|
console.log("validating exchange acct");
|
|
|
|
const isValid = await ws.cryptoApi.isValidWireAccount(
|
2020-01-19 17:06:47 +01:00
|
|
|
a.payto_uri,
|
2019-12-05 23:07:46 +01:00
|
|
|
a.master_sig,
|
|
|
|
details.masterPublicKey,
|
|
|
|
);
|
|
|
|
if (!isValid) {
|
|
|
|
throw Error("exchange acct signature invalid");
|
|
|
|
}
|
|
|
|
}
|
2019-12-02 00:42:40 +01:00
|
|
|
const feesForType: { [wireMethod: string]: WireFee[] } = {};
|
|
|
|
for (const wireMethod of Object.keys(wireInfo.fees)) {
|
|
|
|
const feeList: WireFee[] = [];
|
|
|
|
for (const x of wireInfo.fees[wireMethod]) {
|
2019-12-19 20:42:49 +01:00
|
|
|
const startStamp = x.start_date;
|
|
|
|
const endStamp = x.end_date;
|
2019-12-05 23:07:46 +01:00
|
|
|
const fee: WireFee = {
|
2019-12-02 00:42:40 +01:00
|
|
|
closingFee: Amounts.parseOrThrow(x.closing_fee),
|
|
|
|
endStamp,
|
|
|
|
sig: x.sig,
|
|
|
|
startStamp,
|
|
|
|
wireFee: Amounts.parseOrThrow(x.wire_fee),
|
2019-12-05 23:07:46 +01:00
|
|
|
};
|
|
|
|
const isValid = await ws.cryptoApi.isValidWireFee(
|
|
|
|
wireMethod,
|
|
|
|
fee,
|
|
|
|
details.masterPublicKey,
|
|
|
|
);
|
|
|
|
if (!isValid) {
|
|
|
|
throw Error("exchange wire fee signature invalid");
|
|
|
|
}
|
|
|
|
feeList.push(fee);
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
feesForType[wireMethod] = feeList;
|
|
|
|
}
|
|
|
|
|
2019-12-12 22:39:45 +01:00
|
|
|
await ws.db.runWithWriteTransaction([Stores.exchanges], async tx => {
|
2019-12-02 00:42:40 +01:00
|
|
|
const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
|
|
|
|
if (!r) {
|
|
|
|
return;
|
|
|
|
}
|
2019-12-16 12:53:22 +01:00
|
|
|
if (r.updateStatus != ExchangeUpdateStatus.FetchWire) {
|
2019-12-02 00:42:40 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
r.wireInfo = {
|
|
|
|
accounts: wireInfo.accounts,
|
|
|
|
feesForType: feesForType,
|
|
|
|
};
|
2019-12-16 12:53:22 +01:00
|
|
|
r.updateStatus = ExchangeUpdateStatus.FetchTerms;
|
2019-12-02 00:42:40 +01:00
|
|
|
r.lastError = undefined;
|
|
|
|
await tx.put(Stores.exchanges, r);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-12-06 00:24:34 +01:00
|
|
|
export async function updateExchangeFromUrl(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
baseUrl: string,
|
2019-12-07 22:02:11 +01:00
|
|
|
forceNow: boolean = false,
|
2019-12-06 00:24:34 +01:00
|
|
|
): Promise<ExchangeRecord> {
|
|
|
|
const onOpErr = (e: OperationError) => setExchangeError(ws, baseUrl, e);
|
|
|
|
return await guardOperationException(
|
2019-12-07 22:02:11 +01:00
|
|
|
() => updateExchangeFromUrlImpl(ws, baseUrl, forceNow),
|
2019-12-06 00:24:34 +01:00
|
|
|
onOpErr,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-12-02 00:42:40 +01: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.
|
|
|
|
*/
|
2019-12-06 00:24:34 +01:00
|
|
|
async function updateExchangeFromUrlImpl(
|
2019-12-02 00:42:40 +01:00
|
|
|
ws: InternalWalletState,
|
|
|
|
baseUrl: string,
|
2019-12-07 22:02:11 +01:00
|
|
|
forceNow: boolean = false,
|
2019-12-02 00:42:40 +01:00
|
|
|
): Promise<ExchangeRecord> {
|
|
|
|
const now = getTimestampNow();
|
|
|
|
baseUrl = canonicalizeBaseUrl(baseUrl);
|
|
|
|
|
2019-12-12 22:39:45 +01:00
|
|
|
const r = await ws.db.get(Stores.exchanges, baseUrl);
|
2019-12-02 00:42:40 +01:00
|
|
|
if (!r) {
|
|
|
|
const newExchangeRecord: ExchangeRecord = {
|
2019-12-16 12:53:22 +01:00
|
|
|
builtIn: false,
|
2019-12-02 00:42:40 +01:00
|
|
|
baseUrl: baseUrl,
|
|
|
|
details: undefined,
|
|
|
|
wireInfo: undefined,
|
2019-12-16 12:53:22 +01:00
|
|
|
updateStatus: ExchangeUpdateStatus.FetchKeys,
|
2019-12-02 00:42:40 +01:00
|
|
|
updateStarted: now,
|
2019-12-16 12:53:22 +01:00
|
|
|
updateReason: ExchangeUpdateReason.Initial,
|
2019-12-02 00:42:40 +01:00
|
|
|
timestampAdded: getTimestampNow(),
|
2019-12-09 19:59:08 +01:00
|
|
|
termsOfServiceAcceptedEtag: undefined,
|
|
|
|
termsOfServiceAcceptedTimestamp: undefined,
|
|
|
|
termsOfServiceLastEtag: undefined,
|
|
|
|
termsOfServiceText: undefined,
|
2019-12-16 16:20:45 +01:00
|
|
|
updateDiff: undefined,
|
2019-12-02 00:42:40 +01:00
|
|
|
};
|
2019-12-12 22:39:45 +01:00
|
|
|
await ws.db.put(Stores.exchanges, newExchangeRecord);
|
2019-12-02 00:42:40 +01:00
|
|
|
} else {
|
2019-12-12 22:39:45 +01:00
|
|
|
await ws.db.runWithWriteTransaction([Stores.exchanges], async t => {
|
2019-12-02 00:42:40 +01:00
|
|
|
const rec = await t.get(Stores.exchanges, baseUrl);
|
|
|
|
if (!rec) {
|
|
|
|
return;
|
|
|
|
}
|
2019-12-16 12:53:22 +01:00
|
|
|
if (rec.updateStatus != ExchangeUpdateStatus.FetchKeys && !forceNow) {
|
2019-12-02 00:42:40 +01:00
|
|
|
return;
|
|
|
|
}
|
2019-12-16 12:53:22 +01:00
|
|
|
if (rec.updateStatus != ExchangeUpdateStatus.FetchKeys && forceNow) {
|
|
|
|
rec.updateReason = ExchangeUpdateReason.Forced;
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
rec.updateStarted = now;
|
2019-12-16 12:53:22 +01:00
|
|
|
rec.updateStatus = ExchangeUpdateStatus.FetchKeys;
|
2019-12-02 00:42:40 +01:00
|
|
|
rec.lastError = undefined;
|
|
|
|
t.put(Stores.exchanges, rec);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
await updateExchangeWithKeys(ws, baseUrl);
|
|
|
|
await updateExchangeWithWireInfo(ws, baseUrl);
|
2019-12-09 19:59:08 +01:00
|
|
|
await updateExchangeWithTermsOfService(ws, baseUrl);
|
2019-12-16 12:53:22 +01:00
|
|
|
await updateExchangeFinalize(ws, baseUrl);
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2019-12-12 22:39:45 +01:00
|
|
|
const updatedExchange = await ws.db.get(Stores.exchanges, baseUrl);
|
2019-12-02 00:42:40 +01:00
|
|
|
|
|
|
|
if (!updatedExchange) {
|
|
|
|
// This should practically never happen
|
|
|
|
throw Error("exchange not found");
|
|
|
|
}
|
|
|
|
return updatedExchange;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if and how an exchange is trusted and/or audited.
|
|
|
|
*/
|
|
|
|
export async function getExchangeTrust(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
exchangeInfo: ExchangeRecord,
|
|
|
|
): Promise<{ isTrusted: boolean; isAudited: boolean }> {
|
|
|
|
let isTrusted = false;
|
|
|
|
let isAudited = false;
|
|
|
|
const exchangeDetails = exchangeInfo.details;
|
|
|
|
if (!exchangeDetails) {
|
|
|
|
throw Error(`exchange ${exchangeInfo.baseUrl} details not available`);
|
|
|
|
}
|
2019-12-12 22:39:45 +01:00
|
|
|
const currencyRecord = await ws.db.get(
|
2019-12-02 00:42:40 +01:00
|
|
|
Stores.currencies,
|
|
|
|
exchangeDetails.currency,
|
|
|
|
);
|
|
|
|
if (currencyRecord) {
|
|
|
|
for (const trustedExchange of currencyRecord.exchanges) {
|
|
|
|
if (trustedExchange.exchangePub === exchangeDetails.masterPublicKey) {
|
|
|
|
isTrusted = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (const trustedAuditor of currencyRecord.auditors) {
|
|
|
|
for (const exchangeAuditor of exchangeDetails.auditors) {
|
|
|
|
if (trustedAuditor.auditorPub === exchangeAuditor.auditor_pub) {
|
|
|
|
isAudited = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return { isTrusted, isAudited };
|
|
|
|
}
|
|
|
|
|
|
|
|
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.
|
|
|
|
const exchangeRecord = await updateExchangeFromUrl(ws, exchangeBaseUrl);
|
|
|
|
if (!exchangeRecord) {
|
|
|
|
throw Error(`Exchange '${exchangeBaseUrl}' not found.`);
|
|
|
|
}
|
|
|
|
const exchangeWireInfo = exchangeRecord.wireInfo;
|
|
|
|
if (!exchangeWireInfo) {
|
|
|
|
throw Error(`Exchange wire info for '${exchangeBaseUrl}' not found.`);
|
|
|
|
}
|
|
|
|
for (let account of exchangeWireInfo.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");
|
|
|
|
}
|