wallet-core: simplify exchanges list response

This commit is contained in:
Florian Dold 2022-10-15 12:59:26 +02:00
parent e075134ffc
commit bd88dcebbc
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
11 changed files with 78 additions and 75 deletions

View File

@ -900,11 +900,18 @@ export interface ExchangeFullDetails {
globalFees: FeeDescription[]; globalFees: FeeDescription[];
} }
export enum ExchangeTosStatus {
New = "new",
Accepted = "accepted",
Changed = "changed",
NotFound = "not-found",
}
export interface ExchangeListItem { export interface ExchangeListItem {
exchangeBaseUrl: string; exchangeBaseUrl: string;
currency: string; currency: string;
paytoUris: string[]; paytoUris: string[];
tos: ExchangeTosStatusDetails; tosStatus: ExchangeTosStatus;
} }
const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> => const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> =>
@ -976,7 +983,7 @@ export const codecForExchangeListItem = (): Codec<ExchangeListItem> =>
.property("currency", codecForString()) .property("currency", codecForString())
.property("exchangeBaseUrl", codecForString()) .property("exchangeBaseUrl", codecForString())
.property("paytoUris", codecForList(codecForString())) .property("paytoUris", codecForList(codecForString()))
.property("tos", codecForExchangeTos()) .property("tosStatus", codecForAny())
.build("ExchangeListItem"); .build("ExchangeListItem");
export const codecForExchangesListResponse = (): Codec<ExchangesListResponse> => export const codecForExchangesListResponse = (): Codec<ExchangesListResponse> =>
@ -1146,6 +1153,8 @@ export interface GetExchangeTosResult {
* Accepted content type * Accepted content type
*/ */
contentType: string; contentType: string;
tosStatus: ExchangeTosStatus;
} }
export interface TestPayArgs { export interface TestPayArgs {

View File

@ -22,6 +22,7 @@ import {
Amounts, Amounts,
CoinRefreshRequest, CoinRefreshRequest,
CoinStatus, CoinStatus,
ExchangeTosStatus,
j2s, j2s,
Logger, Logger,
RefreshReason, RefreshReason,
@ -31,7 +32,7 @@ import {
TransactionIdStr, TransactionIdStr,
TransactionType, TransactionType,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { WalletStoresV1, CoinRecord } from "../db.js"; import { WalletStoresV1, CoinRecord, ExchangeDetailsRecord } from "../db.js";
import { makeErrorDetail, TalerError } from "../errors.js"; import { makeErrorDetail, TalerError } from "../errors.js";
import { InternalWalletState } from "../internal-wallet-state.js"; import { InternalWalletState } from "../internal-wallet-state.js";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
@ -307,3 +308,15 @@ export function makeTombstoneId(
): TombstoneIdStr { ): TombstoneIdStr {
return `tmb:${type}:${args.map((x) => encodeURIComponent(x)).join(":")}`; return `tmb:${type}:${args.map((x) => encodeURIComponent(x)).join(":")}`;
} }
export function getExchangeTosStatus(
exchangeDetails: ExchangeDetailsRecord,
): ExchangeTosStatus {
if (!exchangeDetails.tosAccepted) {
return ExchangeTosStatus.New;
}
if (exchangeDetails.tosAccepted?.etag == exchangeDetails.tosCurrentEtag) {
return ExchangeTosStatus.Accepted;
}
return ExchangeTosStatus.Changed;
}

View File

@ -83,6 +83,7 @@ import {
} from "../errors.js"; } from "../errors.js";
import { InternalWalletState } from "../internal-wallet-state.js"; import { InternalWalletState } from "../internal-wallet-state.js";
import { import {
getExchangeTosStatus,
makeCoinAvailable, makeCoinAvailable,
runOperationWithErrorReporting, runOperationWithErrorReporting,
} from "../operations/common.js"; } from "../operations/common.js";
@ -1359,26 +1360,20 @@ export async function getWithdrawalDetailsForUri(
.runReadOnly(async (tx) => { .runReadOnly(async (tx) => {
const exchangeRecords = await tx.exchanges.iter().toArray(); const exchangeRecords = await tx.exchanges.iter().toArray();
for (const r of exchangeRecords) { for (const r of exchangeRecords) {
const details = await ws.exchangeOps.getExchangeDetails(tx, r.baseUrl); const exchangeDetails = await ws.exchangeOps.getExchangeDetails(tx, r.baseUrl);
const denominations = await tx.denominations.indexes.byExchangeBaseUrl const denominations = await tx.denominations.indexes.byExchangeBaseUrl
.iter(r.baseUrl) .iter(r.baseUrl)
.toArray(); .toArray();
if (details && denominations) { if (exchangeDetails && denominations) {
const tosRecord = await tx.exchangeTos.get([ const tosRecord = await tx.exchangeTos.get([
details.exchangeBaseUrl, exchangeDetails.exchangeBaseUrl,
details.tosCurrentEtag, exchangeDetails.tosCurrentEtag,
]); ]);
exchanges.push({ exchanges.push({
exchangeBaseUrl: details.exchangeBaseUrl, exchangeBaseUrl: exchangeDetails.exchangeBaseUrl,
currency: details.currency, currency: exchangeDetails.currency,
// FIXME: We probably don't want to include the full ToS here! paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
tos: { tosStatus: getExchangeTosStatus(exchangeDetails),
acceptedVersion: details.tosAccepted?.etag,
currentVersion: details.tosCurrentEtag,
contentType: tosRecord?.termsOfServiceContentType ?? "",
content: tosRecord?.termsOfServiceText ?? "",
},
paytoUris: details.wireInfo.accounts.map((x) => x.payto_uri),
}); });
} }
} }

View File

@ -146,7 +146,7 @@ import {
} from "./operations/backup/index.js"; } from "./operations/backup/index.js";
import { setWalletDeviceId } from "./operations/backup/state.js"; import { setWalletDeviceId } from "./operations/backup/state.js";
import { getBalances } from "./operations/balance.js"; import { getBalances } from "./operations/balance.js";
import { runOperationWithErrorReporting } from "./operations/common.js"; import { getExchangeTosStatus, runOperationWithErrorReporting } from "./operations/common.js";
import { import {
createDepositGroup, createDepositGroup,
getFeeForDeposit, getFeeForDeposit,
@ -503,6 +503,7 @@ async function getExchangeTos(
currentEtag, currentEtag,
content, content,
contentType, contentType,
tosStatus: getExchangeTosStatus(exchangeDetails),
}; };
} }
@ -519,6 +520,7 @@ async function getExchangeTos(
currentEtag, currentEtag,
content, content,
contentType, contentType,
tosStatus: getExchangeTosStatus(exchangeDetails),
}; };
} }
@ -529,6 +531,7 @@ async function getExchangeTos(
currentEtag: tosDownload.tosEtag, currentEtag: tosDownload.tosEtag,
content: tosDownload.tosText, content: tosDownload.tosText,
contentType: tosDownload.tosContentType, contentType: tosDownload.tosContentType,
tosStatus: getExchangeTosStatus(exchangeDetails),
}; };
} }
@ -665,7 +668,7 @@ async function getExchanges(
exchanges.push({ exchanges.push({
exchangeBaseUrl: r.baseUrl, exchangeBaseUrl: r.baseUrl,
currency, currency,
tos, tosStatus: getExchangeTosStatus(exchangeDetails),
paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri), paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
}); });
} }

View File

@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { GetExchangeTosResult } from "@gnu-taler/taler-util"; import { ExchangeTosStatus, GetExchangeTosResult } from "@gnu-taler/taler-util";
export function buildTermsOfServiceState( export function buildTermsOfServiceState(
tos: GetExchangeTosResult, tos: GetExchangeTosResult,
@ -24,26 +24,7 @@ export function buildTermsOfServiceState(
tos.content, tos.content,
); );
const status: TermsStatus = buildTermsOfServiceStatus( return { content, status: tos.tosStatus, version: tos.currentEtag };
tos.content,
tos.acceptedEtag,
tos.currentEtag,
);
return { content, status, version: tos.currentEtag };
}
export function buildTermsOfServiceStatus(
content: string | undefined,
acceptedVersion: string | undefined,
currentVersion: string | undefined,
): TermsStatus {
return !content
? "notfound"
: !acceptedVersion
? "new"
: acceptedVersion !== currentVersion
? "changed"
: "accepted";
} }
function parseTermsOfServiceContent( function parseTermsOfServiceContent(
@ -91,12 +72,10 @@ function parseTermsOfServiceContent(
export type TermsState = { export type TermsState = {
content: TermsDocument | undefined; content: TermsDocument | undefined;
status: TermsStatus; status: ExchangeTosStatus;
version: string; version: string;
}; };
type TermsStatus = "new" | "accepted" | "changed" | "notfound";
export type TermsDocument = export type TermsDocument =
| TermsDocumentXml | TermsDocumentXml
| TermsDocumentHtml | TermsDocumentHtml

View File

@ -29,6 +29,7 @@ import {
import { ExchangeXmlTos } from "../../components/ExchangeToS.js"; import { ExchangeXmlTos } from "../../components/ExchangeToS.js";
import { ToggleHandler } from "../../mui/handlers.js"; import { ToggleHandler } from "../../mui/handlers.js";
import { Button } from "../../mui/Button.js"; import { Button } from "../../mui/Button.js";
import { ExchangeTosStatus } from "@gnu-taler/taler-util";
export function LoadingUriView({ error }: State.LoadingUriError): VNode { export function LoadingUriView({ error }: State.LoadingUriError): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
@ -100,7 +101,7 @@ export function ShowButtonsNonAcceptedTosView({
if (!ableToReviewTermsOfService) { if (!ableToReviewTermsOfService) {
return ( return (
<Fragment> <Fragment>
{terms.status === "notfound" && ( {terms.status === ExchangeTosStatus.NotFound && (
<section style={{ justifyContent: "space-around", display: "flex" }}> <section style={{ justifyContent: "space-around", display: "flex" }}>
<WarningText> <WarningText>
<i18n.Translate> <i18n.Translate>
@ -115,7 +116,7 @@ export function ShowButtonsNonAcceptedTosView({
return ( return (
<Fragment> <Fragment>
{terms.status === "notfound" && ( {terms.status === ExchangeTosStatus.NotFound && (
<section style={{ justifyContent: "space-around", display: "flex" }}> <section style={{ justifyContent: "space-around", display: "flex" }}>
<WarningText> <WarningText>
<i18n.Translate> <i18n.Translate>
@ -163,7 +164,7 @@ export function ShowTosContentView({
return ( return (
<Fragment> <Fragment>
{terms.status !== "notfound" && !terms.content && ( {terms.status !== ExchangeTosStatus.NotFound && !terms.content && (
<section style={{ justifyContent: "space-around", display: "flex" }}> <section style={{ justifyContent: "space-around", display: "flex" }}>
<WarningBox> <WarningBox>
<i18n.Translate> <i18n.Translate>
@ -204,7 +205,7 @@ export function ShowTosContentView({
</LinkSuccess> </LinkSuccess>
</section> </section>
)} )}
{termsAccepted && terms.status !== "notfound" && ( {termsAccepted && terms.status !== ExchangeTosStatus.NotFound && (
<section style={{ justifyContent: "space-around", display: "flex" }}> <section style={{ justifyContent: "space-around", display: "flex" }}>
<CheckboxOutlined <CheckboxOutlined
name="terms" name="terms"

View File

@ -15,7 +15,12 @@
*/ */
/* eslint-disable react-hooks/rules-of-hooks */ /* eslint-disable react-hooks/rules-of-hooks */
import { AmountJson, Amounts, ExchangeListItem } from "@gnu-taler/taler-util"; import {
AmountJson,
Amounts,
ExchangeListItem,
ExchangeTosStatus,
} from "@gnu-taler/taler-util";
import { TalerError } from "@gnu-taler/taler-wallet-core"; import { TalerError } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
@ -173,10 +178,8 @@ function exchangeSelectionState(
const [ageRestricted, setAgeRestricted] = useState(0); const [ageRestricted, setAgeRestricted] = useState(0);
const currentExchange = selectedExchange.selected; const currentExchange = selectedExchange.selected;
const tosNeedToBeAccepted = const tosNeedToBeAccepted =
!currentExchange.tos.acceptedVersion || currentExchange.tosStatus == ExchangeTosStatus.New ||
currentExchange.tos.currentVersion !== currentExchange.tosStatus == ExchangeTosStatus.Changed;
currentExchange.tos.acceptedVersion;
/** /**
* With the exchange and amount, ask the wallet the information * With the exchange and amount, ask the wallet the information
* about the withdrawal * about the withdrawal

View File

@ -22,6 +22,7 @@
import { import {
Amounts, Amounts,
ExchangeFullDetails, ExchangeFullDetails,
ExchangeTosStatus,
GetExchangeTosResult, GetExchangeTosResult,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { expect } from "chai"; import { expect } from "chai";
@ -169,6 +170,7 @@ describe("Withdraw CTA states", () => {
content: "just accept", content: "just accept",
acceptedEtag: "v1", acceptedEtag: "v1",
currentEtag: "v1", currentEtag: "v1",
tosStatus: ExchangeTosStatus.Accepted,
}), }),
} as any, } as any,
), ),
@ -254,6 +256,7 @@ describe("Withdraw CTA states", () => {
content: "just accept", content: "just accept",
acceptedEtag: "v1", acceptedEtag: "v1",
currentEtag: "v2", currentEtag: "v2",
tosStatus: ExchangeTosStatus.Changed,
}), }),
setExchangeTosAccepted: async () => ({}), setExchangeTosAccepted: async () => ({}),
} as any, } as any,

View File

@ -37,6 +37,7 @@ import editIcon from "../../svg/edit_24px.svg";
import { ExchangeDetails, WithdrawDetails } from "../../wallet/Transaction.js"; import { ExchangeDetails, WithdrawDetails } from "../../wallet/Transaction.js";
import { TermsOfService } from "../../components/TermsOfService/index.js"; import { TermsOfService } from "../../components/TermsOfService/index.js";
import { State } from "./index.js"; import { State } from "./index.js";
import { ExchangeTosStatus } from "@gnu-taler/taler-util";
export function LoadingUriView({ error }: State.LoadingUriError): VNode { export function LoadingUriView({ error }: State.LoadingUriError): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
@ -65,8 +66,7 @@ export function LoadingInfoView({ error }: State.LoadingInfoError): VNode {
export function SuccessView(state: State.Success): VNode { export function SuccessView(state: State.Success): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const currentTosVersionIsAccepted = const currentTosVersionIsAccepted =
state.currentExchange.tos.acceptedVersion === state.currentExchange.tosStatus === ExchangeTosStatus.Accepted;
state.currentExchange.tos.currentVersion;
return ( return (
<WalletAction> <WalletAction>
<LogoHeader /> <LogoHeader />

View File

@ -17,6 +17,7 @@
import { import {
Amounts, Amounts,
CoinDumpJson, CoinDumpJson,
CoinStatus,
ExchangeListItem, ExchangeListItem,
NotificationType, NotificationType,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
@ -86,7 +87,7 @@ type CoinsInfo = CoinDumpJson["coins"];
type CalculatedCoinfInfo = { type CalculatedCoinfInfo = {
ageKeysCount: number | undefined; ageKeysCount: number | undefined;
denom_value: number; denom_value: number;
remain_value: number; //remain_value: number;
status: string; status: string;
from_refresh: boolean; from_refresh: boolean;
id: string; id: string;
@ -143,10 +144,10 @@ export function View({
prev[cur.exchange_base_url].push({ prev[cur.exchange_base_url].push({
ageKeysCount: cur.ageCommitmentProof?.proof.privateKeys.length, ageKeysCount: cur.ageCommitmentProof?.proof.privateKeys.length,
denom_value: parseFloat(Amounts.stringifyValue(denom)), denom_value: parseFloat(Amounts.stringifyValue(denom)),
remain_value: parseFloat( // remain_value: parseFloat(
Amounts.stringifyValue(Amounts.parseOrThrow(cur.remaining_value)), // Amounts.stringifyValue(Amounts.parseOrThrow(cur.remaining_value)),
), // ),
status: cur.coin_suspended ? "suspended" : "ok", status: cur.coin_status,
from_refresh: cur.refresh_parent_coin_pub !== undefined, from_refresh: cur.refresh_parent_coin_pub !== undefined,
id: cur.coin_pub, id: cur.coin_pub,
}); });
@ -254,8 +255,8 @@ export function View({
const coins = allcoins.reduce( const coins = allcoins.reduce(
(prev, cur) => { (prev, cur) => {
if (cur.remain_value > 0) prev.usable.push(cur); if (cur.status === CoinStatus.Fresh) prev.usable.push(cur);
if (cur.remain_value === 0) prev.spent.push(cur); if (cur.status === CoinStatus.Dormant) prev.spent.push(cur);
return prev; return prev;
}, },
{ {
@ -356,7 +357,6 @@ function ShowAllCoins({
<tr key={idx}> <tr key={idx}>
<td>{c.id.substring(0, 5)}</td> <td>{c.id.substring(0, 5)}</td>
<td>{c.denom_value}</td> <td>{c.denom_value}</td>
<td>{c.remain_value}</td>
<td>{c.status}</td> <td>{c.status}</td>
<td>{c.from_refresh ? "true" : "false"}</td> <td>{c.from_refresh ? "true" : "false"}</td>
<td>{String(c.ageKeysCount)}</td> <td>{String(c.ageKeysCount)}</td>
@ -396,7 +396,6 @@ function ShowAllCoins({
<tr key={idx}> <tr key={idx}>
<td>{c.id.substring(0, 5)}</td> <td>{c.id.substring(0, 5)}</td>
<td>{c.denom_value}</td> <td>{c.denom_value}</td>
<td>{c.remain_value}</td>
<td>{c.status}</td> <td>{c.status}</td>
<td>{c.from_refresh ? "true" : "false"}</td> <td>{c.from_refresh ? "true" : "false"}</td>
</tr> </tr>

View File

@ -14,7 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { ExchangeListItem, WalletCoreVersion } from "@gnu-taler/taler-util"; import {
ExchangeListItem,
ExchangeTosStatus,
WalletCoreVersion,
} from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact"; import { Fragment, h, VNode } from "preact";
import { Checkbox } from "../components/Checkbox.js"; import { Checkbox } from "../components/Checkbox.js";
import { ErrorTalerOperation } from "../components/ErrorTalerOperation.js"; import { ErrorTalerOperation } from "../components/ErrorTalerOperation.js";
@ -36,7 +40,6 @@ import { useBackupDeviceName } from "../hooks/useBackupDeviceName.js";
import { useAutoOpenPermissions } from "../hooks/useAutoOpenPermissions.js"; import { useAutoOpenPermissions } from "../hooks/useAutoOpenPermissions.js";
import { ToggleHandler } from "../mui/handlers.js"; import { ToggleHandler } from "../mui/handlers.js";
import { Pages } from "../NavigationBar.js"; import { Pages } from "../NavigationBar.js";
import { buildTermsOfServiceStatus } from "../components/TermsOfService/utils.js";
import * as wxApi from "../wxApi.js"; import * as wxApi from "../wxApi.js";
import { platform } from "../platform/api.js"; import { platform } from "../platform/api.js";
import { useClipboardPermissions } from "../hooks/useClipboardPermissions.js"; import { useClipboardPermissions } from "../hooks/useClipboardPermissions.js";
@ -181,26 +184,21 @@ export function SettingsView({
<tbody> <tbody>
{knownExchanges.map((e, idx) => { {knownExchanges.map((e, idx) => {
function Status(): VNode { function Status(): VNode {
const status = buildTermsOfServiceStatus( switch (e.tosStatus) {
e.tos.content, case ExchangeTosStatus.Accepted:
e.tos.acceptedVersion,
e.tos.currentVersion,
);
switch (status) {
case "accepted":
return ( return (
<SuccessText> <SuccessText>
<i18n.Translate>ok</i18n.Translate> <i18n.Translate>ok</i18n.Translate>
</SuccessText> </SuccessText>
); );
case "changed": case ExchangeTosStatus.Changed:
return ( return (
<WarningText> <WarningText>
<i18n.Translate>changed</i18n.Translate> <i18n.Translate>changed</i18n.Translate>
</WarningText> </WarningText>
); );
case "new": case ExchangeTosStatus.New:
case "notfound": case ExchangeTosStatus.NotFound:
return ( return (
<DestructiveText> <DestructiveText>
<i18n.Translate>not accepted</i18n.Translate> <i18n.Translate>not accepted</i18n.Translate>