diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index aba1b1185..7e538b2d9 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -905,13 +905,27 @@ export enum ExchangeTosStatus { Accepted = "accepted", Changed = "changed", NotFound = "not-found", + Unknown = "unknown", } +export enum ExchangeEntryStatus { + Unknown = "unknown", + Outdated = "outdated", + Ok = "ok", +} + +// FIXME: This should probably include some error status. export interface ExchangeListItem { exchangeBaseUrl: string; - currency: string; + currency: string | undefined; paytoUris: string[]; tosStatus: ExchangeTosStatus; + exchangeStatus: ExchangeEntryStatus; + /** + * Permanently added to the wallet, as opposed to just + * temporarily queried. + */ + permanent: boolean; } const codecForAuditorDenomSig = (): Codec => @@ -984,6 +998,8 @@ export const codecForExchangeListItem = (): Codec => .property("exchangeBaseUrl", codecForString()) .property("paytoUris", codecForList(codecForString())) .property("tosStatus", codecForAny()) + .property("exchangeStatus", codecForAny()) + .property("permanent", codecForBoolean()) .build("ExchangeListItem"); export const codecForExchangesListResponse = (): Codec => diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts index b3abbac6f..249198e3c 100644 --- a/packages/taler-wallet-cli/src/index.ts +++ b/packages/taler-wallet-cli/src/index.ts @@ -549,6 +549,25 @@ exchangesCli }); }); +exchangesCli + .subcommand("exchangesShowCmd", "show", { + help: "Show exchange details", + }) + .requiredArgument("url", clk.STRING, { + help: "Base URL of the exchange.", + }) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.GetExchangeDetailedInfo, + { + exchangeBaseUrl: args.exchangesShowCmd.url, + }, + ); + console.log(JSON.stringify(resp, undefined, 2)); + }); + }); + exchangesCli .subcommand("exchangesAddCmd", "add", { help: "Add an exchange by base URL.", diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 4b031ace3..59980621f 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -17,45 +17,42 @@ /** * Imports. */ +import { Event, IDBDatabase } from "@gnu-taler/idb-bridge"; import { - describeStore, - describeContents, - describeIndex, -} from "./util/query.js"; -import { + AgeCommitmentProof, AmountJson, AmountString, - ExchangeAuditor, - CoinDepositPermission, + CoinEnvelope, + CoinRefreshRequest, + CoinStatus, ContractTerms, + DenominationInfo, DenominationPubKey, - ExchangeSignKeyJson, + DenomSelectionState, + EddsaPublicKeyString, + EddsaSignatureString, + ExchangeAuditor, + ExchangeGlobalFees, InternationalizedString, + Location, MerchantInfo, + PayCoinSelection, + PeerContractTerms, Product, RefreshReason, TalerErrorDetail, - UnblindedSignature, - CoinEnvelope, - TalerProtocolTimestamp, TalerProtocolDuration, - AgeCommitmentProof, - PayCoinSelection, - PeerContractTerms, - Location, - WireInfo, - DenominationInfo, - GlobalFees, - ExchangeGlobalFees, - DenomSelectionState, + TalerProtocolTimestamp, TransactionIdStr, - CoinRefreshRequest, - CoinStatus, - EddsaPublicKeyString, - EddsaSignatureString, + UnblindedSignature, + WireInfo, } from "@gnu-taler/taler-util"; +import { + describeContents, + describeIndex, + describeStore, +} from "./util/query.js"; import { RetryInfo, RetryTags } from "./util/retries.js"; -import { Event, IDBDatabase } from "@gnu-taler/idb-bridge"; /** * This file contains the database schema of the Taler wallet together @@ -354,8 +351,6 @@ export interface DenominationRecord { * Was this denomination still offered by the exchange the last time * we checked? * Only false when the exchange redacts a previously published denomination. - * - * FIXME: Consider rolling this and isRevoked into some bitfield? */ isOffered: boolean; @@ -526,6 +521,8 @@ export interface ExchangeRecord { * Should usually not change. Only changes when the * exchange advertises a different master public key and/or * currency. + * + * FIXME: Use a rowId here? */ detailsPointer: ExchangeDetailsPointer | undefined; @@ -1168,8 +1165,6 @@ export interface PurchaseRecord { /** * Timestamp of the first time that sending a payment to the merchant * for this purchase was successful. - * - * FIXME: Does this need to be a timestamp, doesn't boolean suffice? */ timestampFirstSuccessfulPay: TalerProtocolTimestamp | undefined; @@ -1369,6 +1364,8 @@ export interface WithdrawalGroupRecord { /** * Wire information (as payto URI) for the bank account that * transferred funds for this reserve. + * + * FIXME: Doesn't this belong to the bankAccounts object store? */ senderWire?: string; @@ -1604,7 +1601,7 @@ export interface GhostDepositGroupRecord { export interface TombstoneRecord { /** - * Tombstone ID, with the syntax ":". + * Tombstone ID, with the syntax "tmb::". */ id: string; } diff --git a/packages/taler-wallet-core/src/operations/common.ts b/packages/taler-wallet-core/src/operations/common.ts index ee7a1b46e..95441be2b 100644 --- a/packages/taler-wallet-core/src/operations/common.ts +++ b/packages/taler-wallet-core/src/operations/common.ts @@ -22,6 +22,8 @@ import { Amounts, CoinRefreshRequest, CoinStatus, + ExchangeEntryStatus, + ExchangeListItem, ExchangeTosStatus, j2s, Logger, @@ -32,7 +34,12 @@ import { TransactionIdStr, TransactionType, } from "@gnu-taler/taler-util"; -import { WalletStoresV1, CoinRecord, ExchangeDetailsRecord } from "../db.js"; +import { + WalletStoresV1, + CoinRecord, + ExchangeDetailsRecord, + ExchangeRecord, +} from "../db.js"; import { makeErrorDetail, TalerError } from "../errors.js"; import { InternalWalletState } from "../internal-wallet-state.js"; import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; @@ -320,3 +327,29 @@ export function getExchangeTosStatus( } return ExchangeTosStatus.Changed; } + +export function makeExchangeListItem( + r: ExchangeRecord, + exchangeDetails: ExchangeDetailsRecord | undefined, +): ExchangeListItem { + if (!exchangeDetails) { + return { + exchangeBaseUrl: r.baseUrl, + currency: undefined, + tosStatus: ExchangeTosStatus.Unknown, + paytoUris: [], + exchangeStatus: ExchangeEntryStatus.Unknown, + permanent: r.permanent, + }; + } + let exchangeStatus; + exchangeStatus = ExchangeEntryStatus.Ok; + return { + exchangeBaseUrl: r.baseUrl, + currency: exchangeDetails.currency, + tosStatus: getExchangeTosStatus(exchangeDetails), + paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri), + exchangeStatus, + permanent: r.permanent, + }; +} diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index e89364ad1..142bedd83 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -705,8 +705,6 @@ export async function updateExchangeFromUrlHandler( }; } await tx.exchanges.put(r); - logger.info(`existing details ${j2s(existingDetails)}`); - logger.info(`inserting new details ${j2s(newDetails)}`); const drRowId = await tx.exchangeDetails.put(newDetails); checkDbInvariant(typeof drRowId.key === "number"); diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 1113fb87a..59863101a 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -85,6 +85,7 @@ import { InternalWalletState } from "../internal-wallet-state.js"; import { getExchangeTosStatus, makeCoinAvailable, + makeExchangeListItem, runOperationWithErrorReporting, } from "../operations/common.js"; import { walletCoreDebugFlags } from "../util/debugFlags.js"; @@ -1367,18 +1368,7 @@ export async function getWithdrawalDetailsForUri( .iter(r.baseUrl) .toArray(); if (exchangeDetails && denominations) { - const tosRecord = await tx.exchangeTos.get([ - exchangeDetails.exchangeBaseUrl, - exchangeDetails.tosCurrentEtag, - ]); - exchanges.push({ - exchangeBaseUrl: exchangeDetails.exchangeBaseUrl, - currency: exchangeDetails.currency, - paytoUris: exchangeDetails.wireInfo.accounts.map( - (x) => x.payto_uri, - ), - tosStatus: getExchangeTosStatus(exchangeDetails), - }); + exchanges.push(makeExchangeListItem(r, exchangeDetails)); } } }); diff --git a/packages/taler-wallet-core/src/util/retries.ts b/packages/taler-wallet-core/src/util/retries.ts index 697d6531e..3597a4311 100644 --- a/packages/taler-wallet-core/src/util/retries.ts +++ b/packages/taler-wallet-core/src/util/retries.ts @@ -49,7 +49,6 @@ export enum OperationAttemptResultType { Longpoll = "longpoll", } -// FIXME: not part of DB! export type OperationAttemptResult = | OperationAttemptFinishedResult | OperationAttemptErrorResult diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index 63d960f81..3841bd8d3 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -48,6 +48,7 @@ import { CreateDepositGroupRequest, CreateDepositGroupResponse, DeleteTransactionRequest, + ExchangeDetailedResponse, ExchangesListResponse, ForceRefreshRequest, GetExchangeTosRequest, @@ -109,6 +110,7 @@ export enum WalletApiOperation { ApplyRefund = "applyRefund", AcceptBankIntegratedWithdrawal = "acceptBankIntegratedWithdrawal", GetExchangeTos = "getExchangeTos", + GetExchangeDetailedInfo = "getExchangeDetailedInfo", RetryPendingNow = "retryPendingNow", AbortFailedPayWithRefund = "abortFailedPayWithRefund", ConfirmPay = "confirmPay", @@ -333,6 +335,15 @@ export type GetExchangeTosOp = { response: GetExchangeTosResult; }; +/** + * Get the current terms of a service of an exchange. + */ +export type GetExchangeDetailedInfoOp = { + op: WalletApiOperation.GetExchangeDetailedInfo; + request: AddExchangeRequest; + response: ExchangeDetailedResponse; +}; + /** * List currencies known to the wallet. */ @@ -661,6 +672,7 @@ export type WalletOperations = { [WalletApiOperation.AddExchange]: AddExchangeOp; [WalletApiOperation.SetExchangeTosAccepted]: SetExchangeTosAcceptedOp; [WalletApiOperation.GetExchangeTos]: GetExchangeTosOp; + [WalletApiOperation.GetExchangeDetailedInfo]: GetExchangeDetailedInfoOp; [WalletApiOperation.TrackDepositGroup]: TrackDepositGroupOp; [WalletApiOperation.CreateDepositGroup]: CreateDepositGroupOp; [WalletApiOperation.SetWalletDeviceId]: SetWalletDeviceIdOp; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 3c7194059..7839f3dab 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -97,6 +97,8 @@ import { ExchangeTosStatusDetails, CoinRefreshRequest, CoinStatus, + ExchangeEntryStatus, + ExchangeTosStatus, } from "@gnu-taler/taler-util"; import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js"; import { @@ -146,7 +148,11 @@ import { } from "./operations/backup/index.js"; import { setWalletDeviceId } from "./operations/backup/state.js"; import { getBalances } from "./operations/balance.js"; -import { getExchangeTosStatus, runOperationWithErrorReporting } from "./operations/common.js"; +import { + getExchangeTosStatus, + makeExchangeListItem, + runOperationWithErrorReporting, +} from "./operations/common.js"; import { createDepositGroup, getFeeForDeposit, @@ -645,32 +651,8 @@ async function getExchanges( .runReadOnly(async (tx) => { const exchangeRecords = await tx.exchanges.iter().toArray(); for (const r of exchangeRecords) { - const dp = r.detailsPointer; - if (!dp) { - continue; - } - const { currency } = dp; const exchangeDetails = await getExchangeDetails(tx, r.baseUrl); - if (!exchangeDetails) { - continue; - } - - const denominations = await tx.denominations.indexes.byExchangeBaseUrl - .iter(r.baseUrl) - .toArray(); - - if (!denominations) { - continue; - } - - const tos = await getExchangeTosStatusDetails(tx, exchangeDetails); - - exchanges.push({ - exchangeBaseUrl: r.baseUrl, - currency, - tosStatus: getExchangeTosStatus(exchangeDetails), - paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri), - }); + exchanges.push(makeExchangeListItem(r, exchangeDetails)); } }); return { exchanges }; diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection.tsx b/packages/taler-wallet-webextension/src/wallet/DestinationSelection.tsx index 3143aafa1..94e6ab442 100644 --- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection.tsx @@ -185,7 +185,11 @@ export function SelectCurrency({ ); } const list: Record = {}; - hook.response.exchanges.forEach((e) => (list[e.currency] = e.currency)); + hook.response.exchanges.forEach((e) => { + if (e.currency) { + list[e.currency] = e.currency; + } + }); list[""] = "Select a currency"; return ; } diff --git a/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx b/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx index a292914fb..3714ae538 100644 --- a/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx @@ -112,7 +112,7 @@ export function ManualWithdrawPage({ amount, onCancel }: Props): VNode { const exchangeList = state.response.exchanges.reduce( (p, c) => ({ ...p, - [c.exchangeBaseUrl]: c.currency, + [c.exchangeBaseUrl]: c.currency || "??", }), {} as Record, ); diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.tsx index 8412c4a12..2ff9f15f5 100644 --- a/packages/taler-wallet-webextension/src/wallet/Settings.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Settings.tsx @@ -204,6 +204,14 @@ export function SettingsView({ not accepted ); + case ExchangeTosStatus.Unknown: + return ( + + + unknown (exchange status should be updated) + + + ); } } return (