wallet-core: pull out ToS into separate object store

This commit is contained in:
Florian Dold 2022-10-14 22:10:03 +02:00
parent f1cba79c65
commit a57fcb144d
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
8 changed files with 171 additions and 94 deletions

View File

@ -1165,8 +1165,6 @@ export interface BackupExchange {
currency: string;
protocol_version_range: string;
/**
* Time when the pointer to the exchange details
* was last updated.

View File

@ -620,7 +620,7 @@ export interface KnownBankAccounts {
accounts: KnownBankAccountsInfo[];
}
export interface ExchangeTos {
export interface ExchangeTosStatusDetails {
acceptedVersion?: string;
currentVersion?: string;
contentType?: string;
@ -805,7 +805,7 @@ export interface ExchangeFullDetails {
exchangeBaseUrl: string;
currency: string;
paytoUris: string[];
tos: ExchangeTos;
tos: ExchangeTosStatusDetails;
auditors: ExchangeAuditor[];
wireInfo: WireInfo;
denomFees: DenomOperationMap<FeeDescription[]>;
@ -817,7 +817,7 @@ export interface ExchangeListItem {
exchangeBaseUrl: string;
currency: string;
paytoUris: string[];
tos: ExchangeTos;
tos: ExchangeTosStatusDetails;
}
const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> =>
@ -833,8 +833,8 @@ const codecForExchangeAuditor = (): Codec<ExchangeAuditor> =>
.property("denomination_keys", codecForList(codecForAuditorDenomSig()))
.build("codecForExchangeAuditor");
const codecForExchangeTos = (): Codec<ExchangeTos> =>
buildCodecForObject<ExchangeTos>()
const codecForExchangeTos = (): Codec<ExchangeTosStatusDetails> =>
buildCodecForObject<ExchangeTosStatusDetails>()
.property("acceptedVersion", codecOptional(codecForString()))
.property("currentVersion", codecOptional(codecForString()))
.property("contentType", codecOptional(codecForString()))

View File

@ -80,7 +80,7 @@ import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
* for all previous versions must be written, which should be
* avoided.
*/
export const TALER_DB_NAME = "taler-wallet-main-v6";
export const TALER_DB_NAME = "taler-wallet-main-v7";
/**
* Name of the metadata database. This database is used
@ -99,7 +99,7 @@ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName";
* backwards-compatible way or object stores and indices
* are added.
*/
export const WALLET_DB_MINOR_VERSION = 2;
export const WALLET_DB_MINOR_VERSION = 1;
/**
* Ranges for operation status fields.
@ -450,40 +450,41 @@ export interface ExchangeDetailsRecord {
*/
signingKeys: ExchangeSignKeyJson[];
/**
* Etag of the current ToS of the exchange.
*/
tosCurrentEtag: string;
/**
* Information about ToS acceptance from the user.
*/
tosAccepted:
| {
etag: string;
timestamp: TalerProtocolTimestamp;
}
| undefined;
wireInfo: WireInfo;
}
export interface ExchangeTosRecord {
exchangeBaseUrl: string;
etag: string;
/**
* Terms of service text or undefined if not downloaded yet.
*
* This is just used as a cache of the last downloaded ToS.
*
* FIXME: Put in separate object store!
*/
termsOfServiceText: string | undefined;
/**
* content-type of the last downloaded termsOfServiceText.
*
* * FIXME: Put in separate object store!
* Content-type of the last downloaded termsOfServiceText.
*/
termsOfServiceContentType: string | undefined;
/**
* ETag for last terms of service download.
*/
termsOfServiceLastEtag: string | undefined;
/**
* ETag for last terms of service accepted.
*/
termsOfServiceAcceptedEtag: string | undefined;
/**
* Timestamp when the ToS was accepted.
*
* Used during backup merging.
*/
termsOfServiceAcceptedTimestamp: TalerProtocolTimestamp | undefined;
wireInfo: WireInfo;
}
export interface ExchangeDetailsPointer {
@ -491,11 +492,6 @@ export interface ExchangeDetailsPointer {
currency: string;
/**
* Last observed protocol version range offered by the exchange.
*/
protocolVersionRange: string;
/**
* Timestamp when the (masterPublicKey, currency) pointer
* has been updated.
@ -1899,6 +1895,14 @@ export const WalletStoresV1 = {
byReservePub: describeIndex("byReservePub", "reservePub", {}),
},
),
exchangeTos: describeStore(
"exchangeTos",
describeContents<ExchangeTosRecord>({
keyPath: ["exchangeBaseUrl", "etag"],
autoIncrement: true,
}),
{},
),
config: describeStore(
"config",
describeContents<ConfigRecord>({ keyPath: "key" }),
@ -2116,7 +2120,6 @@ export const WalletStoresV1 = {
"bankAccounts",
describeContents<BankAccountsRecord>({
keyPath: "uri",
versionAdded: 2,
}),
{},
),

View File

@ -298,7 +298,6 @@ export async function exportBackup(
currency: dp.currency,
master_public_key: dp.masterPublicKey,
update_clock: dp.updateClock,
protocol_version_range: dp.protocolVersionRange,
});
});
@ -358,8 +357,8 @@ export async function exportBackup(
purseTimeout: x.purseTimeout,
startDate: x.startDate,
})),
tos_accepted_etag: ex.termsOfServiceAcceptedEtag,
tos_accepted_timestamp: ex.termsOfServiceAcceptedTimestamp,
tos_accepted_etag: ex.tosAccepted?.etag,
tos_accepted_timestamp: ex.tosAccepted?.timestamp,
denominations:
backupDenominationsByExchange[ex.exchangeBaseUrl] ?? [],
});

View File

@ -351,7 +351,6 @@ export async function importBackup(
currency: backupExchange.currency,
masterPublicKey: backupExchange.master_public_key,
updateClock: backupExchange.update_clock,
protocolVersionRange: backupExchange.protocol_version_range,
},
permanent: true,
lastUpdate: undefined,
@ -388,14 +387,18 @@ export async function importBackup(
wadFee: Amounts.parseOrThrow(fee.wad_fee),
});
}
let tosAccepted = undefined;
if (
backupExchangeDetails.tos_accepted_etag &&
backupExchangeDetails.tos_accepted_timestamp
) {
tosAccepted = {
etag: backupExchangeDetails.tos_accepted_etag,
timestamp: backupExchangeDetails.tos_accepted_timestamp,
};
}
await tx.exchangeDetails.put({
exchangeBaseUrl: backupExchangeDetails.base_url,
termsOfServiceAcceptedEtag: backupExchangeDetails.tos_accepted_etag,
termsOfServiceText: undefined,
termsOfServiceLastEtag: undefined,
termsOfServiceContentType: undefined,
termsOfServiceAcceptedTimestamp:
backupExchangeDetails.tos_accepted_timestamp,
wireInfo,
currency: backupExchangeDetails.currency,
auditors: backupExchangeDetails.auditors.map((x) => ({
@ -406,6 +409,8 @@ export async function importBackup(
masterPublicKey: backupExchangeDetails.master_public_key,
protocolVersionRange: backupExchangeDetails.protocol_version,
reserveClosingDelay: backupExchangeDetails.reserve_closing_delay,
tosCurrentEtag: backupExchangeDetails.tos_accepted_etag || "",
tosAccepted,
globalFees: backupExchangeDetails.global_fees.map((x) => ({
accountFee: Amounts.parseOrThrow(x.accountFee),
historyFee: Amounts.parseOrThrow(x.historyFee),
@ -419,7 +424,6 @@ export async function importBackup(
purseTimeout: x.purseTimeout,
startDate: x.startDate,
})),
signingKeys: backupExchangeDetails.signing_keys.map((x) => ({
key: x.key,
master_sig: x.master_sig,

View File

@ -174,24 +174,40 @@ export async function getExchangeDetails(
getExchangeDetails.makeContext = (db: DbAccess<typeof WalletStoresV1>) =>
db.mktx((x) => [x.exchanges, x.exchangeDetails]);
/**
* 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.exchangeDetails])
.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.termsOfServiceText = tos.tosText;
d.termsOfServiceContentType = tos.tosContentType;
d.termsOfServiceLastEtag = tos.tosEtag;
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,
@ -202,7 +218,10 @@ export async function acceptExchangeTermsOfService(
.runReadWrite(async (tx) => {
const d = await getExchangeDetails(tx, exchangeBaseUrl);
if (d) {
d.termsOfServiceAcceptedEtag = etag;
d.tosAccepted = {
etag: etag || d.tosCurrentEtag,
timestamp: TalerProtocolTimestamp.now(),
};
await tx.exchangeDetails.put(d);
}
});
@ -611,7 +630,8 @@ export async function updateExchangeFromUrlHandler(
["text/plain"],
);
const tosHasBeenAccepted =
exchangeDetails?.termsOfServiceAcceptedEtag === tosDownload.tosEtag;
exchangeDetails?.tosAccepted &&
exchangeDetails.tosAccepted.etag === tosDownload.tosEtag;
let recoupGroupId: string | undefined;
@ -647,13 +667,13 @@ export async function updateExchangeFromUrlHandler(
globalFees,
exchangeBaseUrl: r.baseUrl,
wireInfo,
termsOfServiceText: tosDownload.tosText,
termsOfServiceAcceptedEtag: tosHasBeenAccepted
? tosDownload.tosEtag
tosCurrentEtag: tosDownload.tosContentType,
tosAccepted: tosHasBeenAccepted
? {
etag: tosDownload.tosEtag,
timestamp: TalerProtocolTimestamp.now(),
}
: undefined,
termsOfServiceContentType: tosDownload.tosContentType,
termsOfServiceLastEtag: tosDownload.tosEtag,
termsOfServiceAcceptedTimestamp: TalerProtocolTimestamp.now(),
};
// FIXME: only update if pointer got updated
r.lastUpdate = TalerProtocolTimestamp.now();
@ -665,7 +685,6 @@ export async function updateExchangeFromUrlHandler(
masterPublicKey: details.masterPublicKey,
// FIXME: only change if pointer really changed
updateClock: TalerProtocolTimestamp.now(),
protocolVersionRange: keysInfo.protocolVersion,
};
await tx.exchanges.put(r);
await tx.exchangeDetails.put(details);

View File

@ -69,6 +69,7 @@ import {
CoinStatus,
DenominationRecord,
DenominationVerificationStatus,
ExchangeTosRecord,
PlanchetRecord,
PlanchetStatus,
WalletStoresV1,
@ -1278,12 +1279,8 @@ export async function getExchangeWithdrawalInfo(
}
let tosAccepted = false;
if (exchangeDetails.termsOfServiceLastEtag) {
if (
exchangeDetails.termsOfServiceAcceptedEtag ===
exchangeDetails.termsOfServiceLastEtag
) {
if (exchangeDetails.tosAccepted?.timestamp) {
if (exchangeDetails.tosAccepted.etag === exchangeDetails.tosCurrentEtag) {
tosAccepted = true;
}
}
@ -1357,7 +1354,12 @@ export async function getWithdrawalDetailsForUri(
const exchanges: ExchangeListItem[] = [];
await ws.db
.mktx((x) => [x.exchanges, x.exchangeDetails, x.denominations])
.mktx((x) => [
x.exchanges,
x.exchangeDetails,
x.exchangeTos,
x.denominations,
])
.runReadOnly(async (tx) => {
const exchangeRecords = await tx.exchanges.iter().toArray();
for (const r of exchangeRecords) {
@ -1366,14 +1368,19 @@ export async function getWithdrawalDetailsForUri(
.iter(r.baseUrl)
.toArray();
if (details && denominations) {
const tosRecord = await tx.exchangeTos.get([
details.exchangeBaseUrl,
details.tosCurrentEtag,
]);
exchanges.push({
exchangeBaseUrl: details.exchangeBaseUrl,
currency: details.currency,
// FIXME: We probably don't want to include the full ToS here!
tos: {
acceptedVersion: details.termsOfServiceAcceptedEtag,
currentVersion: details.termsOfServiceLastEtag,
contentType: details.termsOfServiceContentType,
content: details.termsOfServiceText,
acceptedVersion: details.tosAccepted?.etag,
currentVersion: details.tosCurrentEtag,
contentType: tosRecord?.termsOfServiceContentType ?? "",
content: tosRecord?.termsOfServiceText ?? "",
},
paytoUris: details.wireInfo.accounts.map((x) => x.payto_uri),
});

View File

@ -94,6 +94,7 @@ import {
WalletCoreVersion,
WalletNotification,
codecForSetDevModeRequest,
ExchangeTosStatusDetails,
} from "@gnu-taler/taler-util";
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
import {
@ -107,6 +108,8 @@ import {
CoinStatus,
ConfigRecordKey,
DenominationRecord,
ExchangeDetailsRecord,
ExchangeTosRecord,
exportDb,
importDb,
WalletStoresV1,
@ -228,7 +231,11 @@ import {
OpenedPromise,
openPromise,
} from "./util/promiseUtils.js";
import { DbAccess, GetReadWriteAccess } from "./util/query.js";
import {
DbAccess,
GetReadOnlyAccess,
GetReadWriteAccess,
} from "./util/query.js";
import { OperationAttemptResult } from "./util/retries.js";
import { TimerAPI, TimerGroup } from "./util/timer.js";
import {
@ -461,6 +468,10 @@ async function fillDefaults(ws: InternalWalletState): Promise<void> {
});
}
/**
* Get the exchange ToS in the requested format.
* Try to download in the accepted format not cached.
*/
async function getExchangeTos(
ws: InternalWalletState,
exchangeBaseUrl: string,
@ -468,9 +479,14 @@ async function getExchangeTos(
): Promise<GetExchangeTosResult> {
// FIXME: download ToS in acceptable format if passed!
const { exchangeDetails } = await updateExchangeFromUrl(ws, exchangeBaseUrl);
const content = exchangeDetails.termsOfServiceText;
const currentEtag = exchangeDetails.termsOfServiceLastEtag;
const contentType = exchangeDetails.termsOfServiceContentType;
const tosDetails = await ws.db
.mktx((x) => [x.exchangeTos])
.runReadOnly(async (tx) => {
return await getExchangeTosStatusDetails(tx, exchangeDetails);
});
const content = tosDetails.content;
const currentEtag = tosDetails.currentVersion;
const contentType = tosDetails.contentType;
if (
content === undefined ||
currentEtag === undefined ||
@ -483,7 +499,7 @@ async function getExchangeTos(
acceptedFormat.findIndex((f) => f === contentType) !== -1
) {
return {
acceptedEtag: exchangeDetails.termsOfServiceAcceptedEtag,
acceptedEtag: exchangeDetails.tosAccepted?.etag,
currentEtag,
content,
contentType,
@ -499,16 +515,17 @@ async function getExchangeTos(
if (tosDownload.tosContentType === contentType) {
return {
acceptedEtag: exchangeDetails.termsOfServiceAcceptedEtag,
acceptedEtag: exchangeDetails.tosAccepted?.etag,
currentEtag,
content,
contentType,
};
}
await updateExchangeTermsOfService(ws, exchangeBaseUrl, tosDownload);
return {
acceptedEtag: exchangeDetails.termsOfServiceAcceptedEtag,
acceptedEtag: exchangeDetails.tosAccepted?.etag,
currentEtag: tosDownload.tosEtag,
content: tosDownload.tosText,
contentType: tosDownload.tosContentType,
@ -585,12 +602,43 @@ async function forgetKnownBankAccounts(
return;
}
async function getExchangeTosStatusDetails(
tx: GetReadOnlyAccess<{ exchangeTos: typeof WalletStoresV1.exchangeTos }>,
exchangeDetails: ExchangeDetailsRecord,
): Promise<ExchangeTosStatusDetails> {
let exchangeTos = await tx.exchangeTos.get([
exchangeDetails.exchangeBaseUrl,
exchangeDetails.tosCurrentEtag,
]);
if (!exchangeTos) {
exchangeTos = {
etag: "not-available",
termsOfServiceContentType: "text/plain",
termsOfServiceText: "terms of service unavailable",
exchangeBaseUrl: exchangeDetails.exchangeBaseUrl,
};
}
return {
acceptedVersion: exchangeDetails.tosAccepted?.etag,
content: exchangeTos.termsOfServiceContentType,
contentType: exchangeTos.termsOfServiceContentType,
currentVersion: exchangeTos.etag,
};
}
async function getExchanges(
ws: InternalWalletState,
): Promise<ExchangesListResponse> {
const exchanges: ExchangeListItem[] = [];
await ws.db
.mktx((x) => [x.exchanges, x.exchangeDetails, x.denominations])
.mktx((x) => [
x.exchanges,
x.exchangeDetails,
x.exchangeTos,
x.denominations,
])
.runReadOnly(async (tx) => {
const exchangeRecords = await tx.exchanges.iter().toArray();
for (const r of exchangeRecords) {
@ -612,15 +660,12 @@ async function getExchanges(
continue;
}
const tos = await getExchangeTosStatusDetails(tx, exchangeDetails);
exchanges.push({
exchangeBaseUrl: r.baseUrl,
currency,
tos: {
acceptedVersion: exchangeDetails.termsOfServiceAcceptedEtag,
currentVersion: exchangeDetails.termsOfServiceLastEtag,
contentType: exchangeDetails.termsOfServiceContentType,
content: exchangeDetails.termsOfServiceText,
},
tos,
paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
});
}
@ -634,7 +679,12 @@ async function getExchangeDetailedInfo(
): Promise<ExchangeFullDetails> {
//TODO: should we use the forceUpdate parameter?
const exchange = await ws.db
.mktx((x) => [x.exchanges, x.exchangeDetails, x.denominations])
.mktx((x) => [
x.exchanges,
x.exchangeTos,
x.exchangeDetails,
x.denominations,
])
.runReadOnly(async (tx) => {
const ex = await tx.exchanges.get(exchangeBaseurl);
const dp = ex?.detailsPointer;
@ -656,6 +706,8 @@ async function getExchangeDetailedInfo(
return;
}
const tos = await getExchangeTosStatusDetails(tx, exchangeDetails);
const denominations: DenominationInfo[] = denominationRecords.map((x) =>
DenominationRecord.toDenomInfo(x),
);
@ -664,12 +716,7 @@ async function getExchangeDetailedInfo(
info: {
exchangeBaseUrl: ex.baseUrl,
currency,
tos: {
acceptedVersion: exchangeDetails.termsOfServiceAcceptedEtag,
currentVersion: exchangeDetails.termsOfServiceLastEtag,
contentType: exchangeDetails.termsOfServiceContentType,
content: exchangeDetails.termsOfServiceText,
},
tos,
paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
auditors: exchangeDetails.auditors,
wireInfo: exchangeDetails.wireInfo,