wallet-core/packages/taler-wallet-core/src/operations/exchanges.ts

965 lines
26 KiB
TypeScript
Raw Normal View History

/*
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 {
AbsoluteTime,
2021-03-17 17:56:37 +01:00
Amounts,
CancellationToken,
canonicalizeBaseUrl,
2020-03-09 09:49:22 +01:00
codecForExchangeKeysJson,
codecForExchangeWireJson,
DenominationPubKey,
2021-03-17 17:56:37 +01:00
Duration,
durationFromSpec,
encodeCrock,
ExchangeAuditor,
ExchangeDenomination,
ExchangeGlobalFees,
ExchangeSignKeyJson,
ExchangeWireJson,
2022-10-12 19:36:30 +02:00
GlobalFees,
hashDenomPub,
j2s,
LibtoolVersion,
2021-06-08 20:58:13 +02:00
Logger,
2021-03-17 17:56:37 +01:00
NotificationType,
parsePaytoUri,
Recoup,
2021-03-17 17:56:37 +01:00
TalerErrorCode,
2022-03-18 15:32:41 +01:00
TalerProtocolDuration,
TalerProtocolTimestamp,
URL,
WireFee,
WireFeeMap,
WireInfo,
2021-03-17 17:56:37 +01:00
} from "@gnu-taler/taler-util";
import {
DenominationRecord,
2021-08-24 15:43:06 +02:00
DenominationVerificationStatus,
ExchangeDetailsRecord,
2021-03-17 17:56:37 +01:00
ExchangeRecord,
WalletStoresV1,
2021-03-17 17:56:37 +01:00
} from "../db.js";
import { TalerError } from "../errors.js";
import { InternalWalletState, TrustInfo } from "../internal-wallet-state.js";
import {
2022-03-18 15:32:41 +01:00
getExpiry,
HttpRequestLibrary,
readSuccessResponseJsonOrThrow,
readSuccessResponseTextOrThrow,
} from "../util/http.js";
import { checkDbInvariant } from "../util/invariants.js";
import {
DbAccess,
GetReadOnlyAccess,
GetReadWriteAccess,
} from "../util/query.js";
2022-09-16 19:27:24 +02:00
import {
OperationAttemptResult,
OperationAttemptResultType,
RetryTags,
unwrapOperationHandlerResultOrThrow,
2022-09-16 19:27:24 +02:00
} from "../util/retries.js";
import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "../versions.js";
import { runOperationWithErrorReporting } from "./common.js";
2022-11-01 15:31:48 +01:00
import { isWithdrawableDenom } from "./withdraw.js";
const logger = new Logger("exchanges.ts");
function denominationRecordFromKeys(
exchangeBaseUrl: string,
exchangeMasterPub: string,
2022-03-18 15:32:41 +01:00
listIssueDate: TalerProtocolTimestamp,
2021-11-27 20:56:58 +01:00
denomIn: ExchangeDenomination,
): DenominationRecord {
2021-11-27 20:56:58 +01:00
let denomPub: DenominationPubKey;
denomPub = denomIn.denom_pub;
2021-11-27 20:56:58 +01:00
const denomPubHash = encodeCrock(hashDenomPub(denomPub));
const value = Amounts.parseOrThrow(denomIn.value);
const d: DenominationRecord = {
2021-11-27 20:56:58 +01:00
denomPub,
denomPubHash,
exchangeBaseUrl,
exchangeMasterPub,
fees: {
2022-11-02 17:42:14 +01:00
feeDeposit: Amounts.stringify(denomIn.fee_deposit),
feeRefresh: Amounts.stringify(denomIn.fee_refresh),
feeRefund: Amounts.stringify(denomIn.fee_refund),
feeWithdraw: Amounts.stringify(denomIn.fee_withdraw),
},
isOffered: true,
isRevoked: false,
masterSig: denomIn.master_sig,
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,
amountFrac: value.fraction,
amountVal: value.value,
currency: value.currency,
listIssueDate,
};
return d;
}
export function getExchangeRequestTimeout(): Duration {
2022-03-18 15:32:41 +01:00
return Duration.fromSpec({
seconds: 5,
});
}
export interface ExchangeTosDownloadResult {
tosText: string;
tosEtag: string;
tosContentType: string;
}
export async function downloadExchangeWithTermsOfService(
exchangeBaseUrl: string,
http: HttpRequestLibrary,
timeout: Duration,
contentType: string,
): Promise<ExchangeTosDownloadResult> {
logger.info(`downloading exchange tos (type ${contentType})`);
const reqUrl = new URL("terms", exchangeBaseUrl);
const headers = {
Accept: contentType,
};
const resp = await http.get(reqUrl.href, {
headers,
timeout,
});
const tosText = await readSuccessResponseTextOrThrow(resp);
const tosEtag = resp.headers.get("etag") || "unknown";
const tosContentType = resp.headers.get("content-type") || "text/plain";
return { tosText, tosEtag, tosContentType };
}
/**
* Get exchange details from the database.
*/
export async function getExchangeDetails(
2021-06-09 15:14:17 +02:00
tx: GetReadOnlyAccess<{
exchanges: typeof WalletStoresV1.exchanges;
exchangeDetails: typeof WalletStoresV1.exchangeDetails;
}>,
exchangeBaseUrl: string,
): Promise<ExchangeDetailsRecord | undefined> {
2021-06-09 15:14:17 +02:00
const r = await tx.exchanges.get(exchangeBaseUrl);
if (!r) {
return;
}
const dp = r.detailsPointer;
if (!dp) {
return;
}
const { currency, masterPublicKey } = dp;
return await tx.exchangeDetails.indexes.byPointer.get([
r.baseUrl,
currency,
masterPublicKey,
]);
}
2021-06-09 15:14:17 +02:00
getExchangeDetails.makeContext = (db: DbAccess<typeof WalletStoresV1>) =>
db.mktx((x) => [x.exchanges, x.exchangeDetails]);
2021-06-09 15:14:17 +02:00
/**
* Update the database based on the download of the terms of service.
*/
export async function updateExchangeTermsOfService(
ws: InternalWalletState,
exchangeBaseUrl: string,
tos: ExchangeTosDownloadResult,
): Promise<void> {
await ws.db
.mktx((x) => [x.exchanges, x.exchangeTos, x.exchangeDetails])
.runReadWrite(async (tx) => {
const d = await getExchangeDetails(tx, exchangeBaseUrl);
let tosRecord = await tx.exchangeTos.get([exchangeBaseUrl, tos.tosEtag]);
if (!tosRecord) {
tosRecord = {
etag: tos.tosEtag,
exchangeBaseUrl,
termsOfServiceContentType: tos.tosContentType,
termsOfServiceText: tos.tosText,
};
await tx.exchangeTos.put(tosRecord);
}
if (d) {
d.tosCurrentEtag = tos.tosEtag;
await tx.exchangeDetails.put(d);
}
});
}
/**
* Mark a ToS version as accepted by the user.
*
* @param etag version of the ToS to accept, or current ToS version of not given
*/
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) => [x.exchanges, x.exchangeDetails])
2021-06-09 15:14:17 +02:00
.runReadWrite(async (tx) => {
const d = await getExchangeDetails(tx, exchangeBaseUrl);
if (d) {
d.tosAccepted = {
etag: etag || d.tosCurrentEtag,
timestamp: TalerProtocolTimestamp.now(),
};
2021-06-09 15:14:17 +02:00
await tx.exchangeDetails.put(d);
}
2021-06-09 15:14:17 +02:00
});
}
async function validateWireInfo(
ws: InternalWalletState,
2021-11-27 20:56:58 +01:00
versionCurrent: number,
wireInfo: ExchangeWireJson,
masterPublicKey: string,
): Promise<WireInfo> {
for (const a of wireInfo.accounts) {
logger.trace("validating exchange acct");
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,
versionCurrent,
2022-03-23 21:24:23 +01:00
});
isValid = v;
}
if (!isValid) {
throw Error("exchange acct signature invalid");
}
}
const feesForType: WireFeeMap = {};
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 = {
2022-11-02 17:42:14 +01:00
closingFee: Amounts.stringify(x.closing_fee),
endStamp,
sig: x.sig,
startStamp,
2022-11-02 17:42:14 +01:00
wireFee: Amounts.stringify(x.wire_fee),
};
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;
}
if (!isValid) {
throw Error("exchange wire fee signature invalid");
}
feeList.push(fee);
}
feesForType[wireMethod] = feeList;
}
return {
accounts: wireInfo.accounts,
feesForType,
};
}
2022-10-12 19:36:30 +02:00
async function validateGlobalFees(
ws: InternalWalletState,
fees: GlobalFees[],
masterPub: string,
): Promise<ExchangeGlobalFees[]> {
const egf: ExchangeGlobalFees[] = [];
2022-10-12 19:36:30 +02:00
for (const gf of fees) {
logger.trace("validating exchange global fees");
let isValid = false;
if (ws.insecureTrustExchange) {
isValid = true;
} else {
const { valid: v } = await ws.cryptoApi.isValidGlobalFees({
masterPub,
gf,
});
isValid = v;
}
if (!isValid) {
throw Error("exchange global fees signature invalid: " + gf.master_sig);
}
egf.push({
2022-11-02 17:42:14 +01:00
accountFee: Amounts.stringify(gf.account_fee),
historyFee: Amounts.stringify(gf.history_fee),
purseFee: Amounts.stringify(gf.purse_fee),
startDate: gf.start_date,
endDate: gf.end_date,
signature: gf.master_sig,
historyTimeout: gf.history_expiration,
purseLimit: gf.purse_account_limit,
purseTimeout: gf.purse_timeout,
});
2022-10-12 19:36:30 +02:00
}
return egf;
2022-10-12 19:36:30 +02: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,
};
}
/**
* Fetch wire information for an exchange.
*
* @param exchangeBaseUrl Exchange base URL, assumed to be already normalized.
*/
async function downloadExchangeWireInfo(
exchangeBaseUrl: string,
http: HttpRequestLibrary,
timeout: Duration,
): Promise<ExchangeWireJson> {
const reqUrl = new URL("wire", exchangeBaseUrl);
const resp = await http.get(reqUrl.href, {
timeout,
});
const wireInfo = await readSuccessResponseJsonOrThrow(
resp,
codecForExchangeWireJson(),
);
return wireInfo;
}
export async function provideExchangeRecordInTx(
ws: InternalWalletState,
tx: GetReadWriteAccess<{
exchanges: typeof WalletStoresV1.exchanges;
exchangeDetails: typeof WalletStoresV1.exchangeDetails;
}>,
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;
}> {
let exchange = await tx.exchanges.get(baseUrl);
if (!exchange) {
const r: ExchangeRecord = {
permanent: true,
baseUrl: baseUrl,
detailsPointer: undefined,
lastUpdate: undefined,
nextUpdate: AbsoluteTime.toTimestamp(now),
nextRefreshCheck: AbsoluteTime.toTimestamp(now),
lastKeysEtag: undefined,
lastWireEtag: undefined,
};
await tx.exchanges.put(r);
exchange = r;
}
const exchangeDetails = await getExchangeDetails(tx, baseUrl);
return { exchange, exchangeDetails };
}
interface ExchangeKeysDownloadResult {
masterPublicKey: string;
currency: string;
2021-11-27 20:56:58 +01:00
auditors: ExchangeAuditor[];
currentDenominations: DenominationRecord[];
protocolVersion: string;
signingKeys: ExchangeSignKeyJson[];
2022-03-18 15:32:41 +01:00
reserveClosingDelay: TalerProtocolDuration;
expiry: TalerProtocolTimestamp;
recoup: Recoup[];
2022-03-18 15:32:41 +01:00
listIssueDate: TalerProtocolTimestamp;
2022-10-12 19:36:30 +02:00
globalFees: GlobalFees[];
}
/**
* Download and validate an exchange's /keys data.
*/
async function downloadExchangeKeysInfo(
baseUrl: string,
http: HttpRequestLibrary,
timeout: Duration,
): Promise<ExchangeKeysDownloadResult> {
const keysUrl = new URL("keys", baseUrl);
const resp = await http.get(keysUrl.href, {
timeout,
});
2021-11-27 20:56:58 +01:00
const exchangeKeysJsonUnchecked = await readSuccessResponseJsonOrThrow(
resp,
codecForExchangeKeysJson(),
);
2021-11-27 20:56:58 +01:00
if (exchangeKeysJsonUnchecked.denoms.length === 0) {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
{
exchangeBaseUrl: baseUrl,
},
"exchange doesn't offer any denominations",
);
}
2021-11-27 20:56:58 +01:00
const protocolVersion = exchangeKeysJsonUnchecked.version;
const versionRes = LibtoolVersion.compare(
WALLET_EXCHANGE_PROTOCOL_VERSION,
protocolVersion,
);
if (versionRes?.compatible != true) {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE,
{
exchangeProtocolVersion: protocolVersion,
walletProtocolVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
},
"exchange protocol version not compatible with wallet",
);
}
const currency = Amounts.parseOrThrow(
2021-11-27 20:56:58 +01:00
exchangeKeysJsonUnchecked.denoms[0].value,
).currency.toUpperCase();
return {
2021-11-27 20:56:58 +01:00
masterPublicKey: exchangeKeysJsonUnchecked.master_public_key,
currency,
2021-11-27 20:56:58 +01:00
auditors: exchangeKeysJsonUnchecked.auditors,
currentDenominations: exchangeKeysJsonUnchecked.denoms.map((d) =>
denominationRecordFromKeys(
baseUrl,
2021-11-27 20:56:58 +01:00
exchangeKeysJsonUnchecked.master_public_key,
exchangeKeysJsonUnchecked.list_issue_date,
d,
),
),
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,
2022-10-12 19:36:30 +02:00
globalFees: exchangeKeysJsonUnchecked.global_fees,
};
}
export async function downloadTosFromAcceptedFormat(
ws: InternalWalletState,
baseUrl: string,
timeout: Duration,
acceptedFormat?: string[],
): Promise<ExchangeTosDownloadResult> {
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;
}
}
if (tosFound !== undefined) {
return tosFound;
}
// If none of the specified format was found try text/plain
return await downloadExchangeWithTermsOfService(
baseUrl,
ws.http,
timeout,
"text/plain",
);
}
2022-09-05 18:12:30 +02:00
export async function updateExchangeFromUrl(
ws: InternalWalletState,
baseUrl: string,
options: {
forceNow?: boolean;
cancellationToken?: CancellationToken;
} = {},
): Promise<{
exchange: ExchangeRecord;
exchangeDetails: ExchangeDetailsRecord;
}> {
const canonUrl = canonicalizeBaseUrl(baseUrl);
return unwrapOperationHandlerResultOrThrow(
await runOperationWithErrorReporting(
ws,
RetryTags.forExchangeUpdateFromUrl(canonUrl),
() => updateExchangeFromUrlHandler(ws, canonUrl, options),
),
2022-09-05 18:12:30 +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.
*/
2022-09-05 18:12:30 +02:00
export async function updateExchangeFromUrlHandler(
ws: InternalWalletState,
exchangeBaseUrl: string,
options: {
forceNow?: boolean;
cancellationToken?: CancellationToken;
} = {},
2022-09-05 18:12:30 +02:00
): Promise<
OperationAttemptResult<{
exchange: ExchangeRecord;
exchangeDetails: ExchangeDetailsRecord;
}>
> {
const forceNow = options.forceNow ?? false;
logger.info(
`updating exchange info for ${exchangeBaseUrl}, forced: ${forceNow}`,
);
2022-05-18 20:57:10 +02:00
2022-03-18 15:32:41 +01:00
const now = AbsoluteTime.now();
exchangeBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl);
let isNewExchange = true;
const { exchange, exchangeDetails } = await ws.db
.mktx((x) => [x.exchanges, x.exchangeDetails])
.runReadWrite(async (tx) => {
let oldExch = await tx.exchanges.get(exchangeBaseUrl);
if (oldExch) {
isNewExchange = false;
}
return provideExchangeRecordInTx(ws, tx, exchangeBaseUrl, 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");
2022-09-05 18:12:30 +02:00
return {
type: OperationAttemptResultType.Finished,
result: { exchange, exchangeDetails },
};
}
logger.info("updating exchange /keys info");
const timeout = getExchangeRequestTimeout();
const keysInfo = await downloadExchangeKeysInfo(
exchangeBaseUrl,
ws.http,
timeout,
);
2021-08-06 16:27:18 +02:00
logger.info("updating exchange /wire info");
const wireInfoDownload = await downloadExchangeWireInfo(
exchangeBaseUrl,
ws.http,
timeout,
);
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");
}
const wireInfo = await validateWireInfo(
ws,
2021-11-27 20:56:58 +01:00
version.current,
wireInfoDownload,
keysInfo.masterPublicKey,
);
2022-10-12 19:36:30 +02:00
const globalFees = await validateGlobalFees(
ws,
keysInfo.globalFees,
keysInfo.masterPublicKey,
);
2021-08-06 16:27:18 +02:00
logger.info("finished validating exchange /wire info");
// We download the text/plain version here,
// because that one needs to exist, and we
// will get the current etag from the response.
const tosDownload = await downloadTosFromAcceptedFormat(
ws,
exchangeBaseUrl,
timeout,
["text/plain"],
);
let recoupGroupId: string | undefined;
2021-08-06 16:27:18 +02:00
logger.trace("updating exchange info in database");
let detailsPointerChanged = false;
2022-11-01 15:31:48 +01:00
let ageMask = 0;
for (const x of keysInfo.currentDenominations) {
if (isWithdrawableDenom(x) && x.denomPub.age_mask != 0) {
ageMask = x.denomPub.age_mask;
break;
}
}
2021-06-09 15:14:17 +02:00
const updated = await ws.db
.mktx((x) => [
x.exchanges,
x.exchangeTos,
x.exchangeDetails,
x.exchangeSignkeys,
x.denominations,
x.coins,
x.refreshGroups,
x.recoupGroups,
])
2021-06-09 15:14:17 +02:00
.runReadWrite(async (tx) => {
const r = await tx.exchanges.get(exchangeBaseUrl);
if (!r) {
logger.warn(`exchange ${exchangeBaseUrl} no longer present`);
return;
}
const existingDetails = await getExchangeDetails(tx, r.baseUrl);
if (!existingDetails) {
detailsPointerChanged = true;
}
if (existingDetails) {
if (existingDetails.masterPublicKey !== keysInfo.masterPublicKey) {
detailsPointerChanged = true;
}
if (existingDetails.currency !== keysInfo.currency) {
detailsPointerChanged = true;
}
// FIXME: We need to do some consistency checks!
}
const existingTosAccepted = existingDetails?.tosAccepted;
const newDetails: ExchangeDetailsRecord = {
auditors: keysInfo.auditors,
currency: keysInfo.currency,
masterPublicKey: keysInfo.masterPublicKey,
2022-10-14 21:00:13 +02:00
protocolVersionRange: keysInfo.protocolVersion,
reserveClosingDelay: keysInfo.reserveClosingDelay,
2022-10-12 19:36:30 +02:00
globalFees,
exchangeBaseUrl: r.baseUrl,
wireInfo,
tosCurrentEtag: tosDownload.tosEtag,
tosAccepted: existingTosAccepted,
2022-11-01 15:31:48 +01:00
ageMask,
};
if (existingDetails?.rowId) {
newDetails.rowId = existingDetails.rowId;
}
2022-03-18 15:32:41 +01:00
r.lastUpdate = TalerProtocolTimestamp.now();
r.nextUpdate = keysInfo.expiry;
// New denominations might be available.
2022-03-18 15:32:41 +01:00
r.nextRefreshCheck = TalerProtocolTimestamp.now();
if (detailsPointerChanged) {
r.detailsPointer = {
currency: newDetails.currency,
masterPublicKey: newDetails.masterPublicKey,
updateClock: TalerProtocolTimestamp.now(),
};
}
2021-06-09 15:14:17 +02:00
await tx.exchanges.put(r);
const drRowId = await tx.exchangeDetails.put(newDetails);
checkDbInvariant(typeof drRowId.key === "number");
let tosRecord = await tx.exchangeTos.get([
exchangeBaseUrl,
tosDownload.tosEtag,
]);
if (!tosRecord || tosRecord.etag !== existingTosAccepted?.etag) {
tosRecord = {
etag: tosDownload.tosEtag,
exchangeBaseUrl,
termsOfServiceContentType: tosDownload.tosContentType,
termsOfServiceText: tosDownload.tosText,
};
await tx.exchangeTos.put(tosRecord);
}
for (const sk of keysInfo.signingKeys) {
// FIXME: validate signing keys before inserting them
await tx.exchangeSignKeys.put({
exchangeDetailsRowId: drRowId.key,
masterSig: sk.master_sig,
signkeyPub: sk.key,
stampEnd: sk.stamp_end,
stampExpire: sk.stamp_expire,
stampStart: sk.stamp_start,
});
}
2022-01-13 22:01:14 +01:00
logger.info("updating denominations in database");
const currentDenomSet = new Set<string>(
keysInfo.currentDenominations.map((x) => x.denomPubHash),
);
for (const currentDenom of keysInfo.currentDenominations) {
2021-06-09 15:14:17 +02:00
const oldDenom = await tx.denominations.get([
exchangeBaseUrl,
currentDenom.denomPubHash,
]);
if (oldDenom) {
// FIXME: Do consistency check, report to auditor if necessary.
} else {
2021-06-09 15:14:17 +02:00
await tx.denominations.put(currentDenom);
}
}
// 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");
// Handle recoup
const recoupDenomList = keysInfo.recoup;
const newlyRevokedCoinPubs: string[] = [];
logger.trace("recoup list from exchange", recoupDenomList);
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,
recoupInfo.h_denom_pub,
2020-09-08 17:33:10 +02: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
logger.trace("denom already revoked");
continue;
}
2022-01-11 12:48:32 +01:00
logger.info("revoking denom", recoupInfo.h_denom_pub);
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)
.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,
exchange.baseUrl,
2021-06-22 12:18:12 +02:00
newlyRevokedCoinPubs,
);
}
return {
exchange: r,
exchangeDetails: newDetails,
};
2021-06-09 15:14:17 +02: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) => {
logger.error("error while recouping coins:", e);
});
}
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");
if (isNewExchange) {
ws.notify({
type: NotificationType.ExchangeAdded,
});
}
return {
2022-09-05 18:12:30 +02:00
type: OperationAttemptResultType.Finished,
result: {
exchange: updated.exchange,
exchangeDetails: updated.exchangeDetails,
},
2019-12-09 19:59:08 +01:00
};
}
/**
* Find a payto:// URI of the exchange that is of one
* of the given target types.
*
* Throws if no matching account was found.
*/
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) => {
return getExchangeDetails(tx, exchangeBaseUrl);
2021-06-09 15:14:17 +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);
if (!res) {
continue;
}
if (supportedTargetTypes.includes(res.targetType)) {
2020-01-19 19:02:47 +01:00
return account.payto_uri;
}
}
throw Error(
`no matching account found at exchange ${exchangeBaseUrl} for wire types ${j2s(
supportedTargetTypes,
)}`,
);
}
/**
* 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) => [
x.exchanges,
x.exchangeDetails,
x.exchangeTrust,
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.exchangeTrust.indexes.byExchangeMasterPub.get(
2022-01-13 22:01:14 +01:00
exchangeDetails.masterPublicKey,
);
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);
if (auditorTrustRecord && auditorTrustRecord.uids.length > 0) {
isAudited = true;
break;
}
}
return { isTrusted, isAudited };
});
}