diff --git a/packages/taler-util/src/backupTypes.ts b/packages/taler-util/src/backupTypes.ts index a1506e90f..0270f2586 100644 --- a/packages/taler-util/src/backupTypes.ts +++ b/packages/taler-util/src/backupTypes.ts @@ -1165,8 +1165,6 @@ export interface BackupExchange { currency: string; - protocol_version_range: string; - /** * Time when the pointer to the exchange details * was last updated. diff --git a/packages/taler-util/src/walletTypes.ts b/packages/taler-util/src/walletTypes.ts index c6063597d..0b2ef1d5f 100644 --- a/packages/taler-util/src/walletTypes.ts +++ b/packages/taler-util/src/walletTypes.ts @@ -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; @@ -817,7 +817,7 @@ export interface ExchangeListItem { exchangeBaseUrl: string; currency: string; paytoUris: string[]; - tos: ExchangeTos; + tos: ExchangeTosStatusDetails; } const codecForAuditorDenomSig = (): Codec => @@ -833,8 +833,8 @@ const codecForExchangeAuditor = (): Codec => .property("denomination_keys", codecForList(codecForAuditorDenomSig())) .build("codecForExchangeAuditor"); -const codecForExchangeTos = (): Codec => - buildCodecForObject() +const codecForExchangeTos = (): Codec => + buildCodecForObject() .property("acceptedVersion", codecOptional(codecForString())) .property("currentVersion", codecOptional(codecForString())) .property("contentType", codecOptional(codecForString())) diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 6dfb06c15..304efd852 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -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({ + keyPath: ["exchangeBaseUrl", "etag"], + autoIncrement: true, + }), + {}, + ), config: describeStore( "config", describeContents({ keyPath: "key" }), @@ -2116,7 +2120,6 @@ export const WalletStoresV1 = { "bankAccounts", describeContents({ keyPath: "uri", - versionAdded: 2, }), {}, ), diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts index 3ba0d85e6..30e61e382 100644 --- a/packages/taler-wallet-core/src/operations/backup/export.ts +++ b/packages/taler-wallet-core/src/operations/backup/export.ts @@ -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] ?? [], }); diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts index 59fd3c398..599b02dea 100644 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ b/packages/taler-wallet-core/src/operations/backup/import.ts @@ -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, diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index d3905b74b..6569cb394 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -174,24 +174,40 @@ export async function getExchangeDetails( getExchangeDetails.makeContext = (db: DbAccess) => 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 { 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); diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 145a2d9c7..700c4620c 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -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), }); diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 22cbeb4b8..ef7a745ab 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -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 { }); } +/** + * 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 { // 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 { + 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 { 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 { //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,