commit 6a166fc986a02c2f44c61d39d51812dfaefd4193
parent d03eaf4f3da56b1f0131c848d458b0054620c268
Author: Sebastian <sebasjm@taler-systems.com>
Date: Mon, 27 Apr 2026 17:43:20 -0300
fix #11026
Diffstat:
8 files changed, 523 insertions(+), 27 deletions(-)
diff --git a/packages/merchant-backoffice-ui/src/Routing.tsx b/packages/merchant-backoffice-ui/src/Routing.tsx
@@ -98,6 +98,7 @@ import { NewAccount } from "./paths/newAccount/index.js";
import { ResetAccount } from "./paths/resetAccount/index.js";
import { Settings } from "./paths/settings/index.js";
import PosTokenCreatePage from "./paths/instance/accessTokens/create-pos/index.js";
+import ListExchanges from "./paths/instance/exchanges/list/index.js";
const TALER_SCREEN_ID = 3;
@@ -167,6 +168,8 @@ export enum InstancePaths {
reports_list = "/reports",
reports_update = "/reports/:cid/update",
reports_new = "/reports/new",
+
+ exchanges_list = "/exchanges"
}
export enum AdminPaths {
@@ -470,6 +473,13 @@ export function Routing(_p: Props): VNode {
* Scheduled report pages
*/}
<Route
+ path={InstancePaths.exchanges_list}
+ component={ListExchanges}
+ />
+ {/**
+ * Scheduled report pages
+ */}
+ <Route
path={InstancePaths.reports_list}
component={ListScheduledReport}
onCreate={() => {
diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
@@ -382,6 +382,18 @@ export function Sidebar({ mobile }: Props): VNode {
<span class="menu-item-label">{state.instance}</span>
</div>
</li>
+ <HtmlPersonaFlag
+ htmlElement="li"
+ point={UIElement.sidebar_exchanges}
+ >
+ <a href={"#/exchanges"} class="has-icon">
+ <i class="icon mdi mdi-key" />
+
+ <span class="menu-item-label">
+ <i18n.Translate>Exchanges</i18n.Translate>
+ </span>
+ </a>
+ </HtmlPersonaFlag>
{state.isAdmin && (
<Fragment>
<p class="menu-label">
@@ -467,8 +479,9 @@ export function getAvailableForPersona(p: MerchantPersona): ElementMap {
[UIElement.option_paymentTimeoutOnTemplate]: true,
[UIElement.action_useRevenueApi]: true,
[UIElement.option_inventoryTaxes]: true,
+ [UIElement.sidebar_exchanges]: true,
+
[UIElement.sidebar_statistics]: false,
-
[UIElement.sidebar_multicurrency]: false,
[UIElement.sidebar_discounts]: false,
[UIElement.sidebar_subscriptions]: false,
@@ -487,6 +500,7 @@ export function getAvailableForPersona(p: MerchantPersona): ElementMap {
[UIElement.sidebar_settings]: true,
[UIElement.sidebar_password]: true,
+ [UIElement.sidebar_exchanges]: false,
[UIElement.sidebar_multicurrency]: false,
[UIElement.sidebar_accessTokens]: false,
[UIElement.sidebar_categories]: false,
@@ -521,15 +535,16 @@ export function getAvailableForPersona(p: MerchantPersona): ElementMap {
[UIElement.sidebar_templates]: true,
[UIElement.sidebar_inventory]: true,
[UIElement.sidebar_categories]: true,
- [UIElement.sidebar_group]: false,
- [UIElement.sidebar_pots]: false,
- [UIElement.sidebar_reports]: false,
- [UIElement.sidebar_accessTokens]: false,
[UIElement.sidebar_kycStatus]: true,
[UIElement.sidebar_bankAccounts]: true,
[UIElement.sidebar_settings]: true,
[UIElement.sidebar_password]: true,
-
+
+ [UIElement.sidebar_group]: false,
+ [UIElement.sidebar_pots]: false,
+ [UIElement.sidebar_reports]: false,
+ [UIElement.sidebar_accessTokens]: false,
+ [UIElement.sidebar_exchanges]: false,
[UIElement.sidebar_multicurrency]: false,
[UIElement.sidebar_discounts]: false,
[UIElement.sidebar_subscriptions]: false,
@@ -554,13 +569,14 @@ export function getAvailableForPersona(p: MerchantPersona): ElementMap {
case "digital-publishing":
return {
[UIElement.sidebar_orders]: true,
- [UIElement.sidebar_accessTokens]: false,
[UIElement.sidebar_kycStatus]: true,
[UIElement.sidebar_bankAccounts]: true,
[UIElement.sidebar_settings]: true,
[UIElement.sidebar_password]: true,
+
+ [UIElement.sidebar_accessTokens]: false,
[UIElement.sidebar_statistics]: false,
-
+ [UIElement.sidebar_exchanges]: false,
[UIElement.sidebar_multicurrency]: false,
[UIElement.sidebar_templates]: false,
[UIElement.sidebar_categories]: false,
@@ -591,13 +607,14 @@ export function getAvailableForPersona(p: MerchantPersona): ElementMap {
return {
[UIElement.sidebar_orders]: true,
[UIElement.sidebar_webhooks]: true,
- [UIElement.sidebar_accessTokens]: false,
[UIElement.sidebar_kycStatus]: true,
[UIElement.sidebar_bankAccounts]: true,
[UIElement.sidebar_settings]: true,
[UIElement.sidebar_password]: true,
+
+ [UIElement.sidebar_accessTokens]: false,
[UIElement.sidebar_statistics]: false,
-
+ [UIElement.sidebar_exchanges]: false,
[UIElement.sidebar_multicurrency]: false,
[UIElement.sidebar_templates]: false,
[UIElement.sidebar_categories]: false,
diff --git a/packages/merchant-backoffice-ui/src/hooks/exchanges.ts b/packages/merchant-backoffice-ui/src/hooks/exchanges.ts
@@ -0,0 +1,65 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ 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/>
+ */
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import {
+ AccessToken,
+ TalerHttpError,
+ TalerMerchantApi,
+ TalerMerchantManagementResultByMethod,
+} from "@gnu-taler/taler-util";
+import _useSWR, { SWRHook, mutate } from "swr";
+import { useSessionContext } from "../context/session.js";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export function revalidateExchanges() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "listExchanges",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useBackendExchanges() {
+ const { lib } = useSessionContext();
+
+ async function fetcher([]: [AccessToken, string]) {
+ const list = await lib.instance.listExchanges();
+ if (list.type === "ok") {
+ list.body.exchanges.sort(importantFirst);
+ }
+ return list;
+ }
+
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"listExchanges">,
+ TalerHttpError
+ >(["listExchanges"], fetcher, {
+ refreshInterval: 5000,
+ });
+
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
+
+ return data;
+}
+
+function importantFirst(
+ a: TalerMerchantApi.ExchangeStatusDetail,
+ b: TalerMerchantApi.ExchangeStatusDetail,
+): 0 | 1 | -1 {
+ return 0;
+}
diff --git a/packages/merchant-backoffice-ui/src/hooks/preference.ts b/packages/merchant-backoffice-ui/src/hooks/preference.ts
@@ -30,6 +30,7 @@ import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
export enum UIElement {
sidebar_multicurrency,
+ sidebar_exchanges,
sidebar_orders,
sidebar_inventory,
sidebar_categories,
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/exchanges/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/exchanges/list/ListPage.tsx
@@ -0,0 +1,169 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ 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/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { AbsoluteTime, TalerMerchantApi, URL } from "@gnu-taler/taler-util";
+import { Time, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { Tooltip } from "../../../../components/Tooltip.js";
+
+const TALER_SCREEN_ID = 41;
+
+export interface Props {
+ exchanges: TalerMerchantApi.ExchangeStatusDetail[];
+ onSelect: (e: TalerMerchantApi.ExchangeStatusDetail) => void;
+}
+
+export function ListPage({ exchanges, onSelect }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <section class="section is-main-section">
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <i class="icon mdi mdi-clock" />
+ <i18n.Translate>Supported exchange status</i18n.Translate>
+ </p>
+
+ <div class="card-header-icon" aria-label="more options" />
+ </header>
+ <div class="card-content">
+ <div class=" has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {exchanges.length > 0 ? (
+ <PendingTable exchanges={exchanges} onSelect={onSelect} />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ );
+}
+interface PendingTableProps {
+ exchanges: TalerMerchantApi.ExchangeStatusDetail[];
+ onSelect: (e: TalerMerchantApi.ExchangeStatusDetail) => void;
+}
+
+function PendingTable({ exchanges, onSelect }: PendingTableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="">
+ <table class="table is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Base URL</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Status</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Next update</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {exchanges.map((e, i) => {
+ const isWorking =
+ e.keys_http_status >= 200 && e.keys_http_status < 300;
+ const legend = isWorking
+ ? i18n.str`This exchange is ready to be used.`
+ : i18n.str`This exchange can't be used due to an error. Contact the service provider.`;
+ const url = new URL(`/config`, e.exchange_url);
+ return (
+ <tr key={i}>
+ <td>
+ <Tooltip text={legend}>
+ <a
+ href={url.href}
+ target="_blank"
+ referrerpolicy="no-referrer"
+ >
+ {e.exchange_url}
+ </a>
+ </Tooltip>
+ </td>
+ <td>
+ <Tooltip text={legend}>
+ {isWorking ? (
+ <div style={{ color: "green" }}>
+ <i class="icon mdi mdi-check" />
+ {e.keys_http_status}
+ </div>
+ ) : (
+ <div style={{ color: "red" }}>
+ <i class="icon mdi mdi-close" />
+ {e.keys_http_status}
+ </div>
+ )}
+ </Tooltip>
+ </td>
+ <td>
+ <Tooltip text={legend}>
+ <Time
+ format="dd/MM/yyyy HH:mm:ss"
+ timestamp={AbsoluteTime.fromProtocolTimestamp(
+ e.next_download,
+ )}
+ />
+ </Tooltip>
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div key={i} class="buttons is-right">
+ <Tooltip text={i18n.str`Details of the status`}>
+ <button
+ class="button is-small is-info"
+ type="button"
+ onClick={() => onSelect(e)}
+ >
+ <i18n.Translate>Details</i18n.Translate>
+ </button>
+ </Tooltip>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="icon mdi mdi-emoticon-happy mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>No pending kyc verification!</i18n.Translate>
+ </p>
+ </div>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/exchanges/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/exchanges/list/index.tsx
@@ -0,0 +1,170 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2026 Taler Systems S.A.
+
+ 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/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ AbsoluteTime,
+ HttpStatusCode,
+ Paytos,
+ TalerError,
+ TalerMerchantApi,
+ TranslatedString,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ NotificationCardBulma,
+ Time,
+ useCommonPreferences,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
+import { Loading } from "../../../../components/exception/loading.js";
+import {
+ ConfirmModal,
+ ValidBankAccount,
+} from "../../../../components/modal/index.js";
+import { useBackendExchanges } from "../../../../hooks/exchanges.js";
+import { ListPage } from "./ListPage.js";
+import { useState } from "preact/hooks";
+
+const TALER_SCREEN_ID = 40;
+
+interface Props {
+ // onGetInfo: (id: string) => void;
+ // onShowInstructions: (id: string) => void;
+}
+
+export default function ListExchanges(_p: Props): VNode {
+ const result = useBackendExchanges();
+ const { i18n } = useTranslationContext();
+ const [showState, setShowState] = useState<
+ TalerMerchantApi.ExchangeStatusDetail | undefined
+ >(undefined);
+
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.NotFound:
+ return <div />;
+ case HttpStatusCode.InternalServerError:
+ return <div />;
+ default: {
+ assertUnreachable(result);
+ }
+ }
+ }
+ const { exchanges } = result.body;
+
+ if (!exchanges.length) {
+ return (
+ <NotificationCardBulma
+ notification={{
+ message: i18n.str`No exchanges supported`,
+ description: i18n.str`Currently the list of exchanges in the configuration is empty. Without this this server can process any payment.".`,
+ type: "WARN",
+ }}
+ />
+ );
+ }
+ return (
+ <Fragment>
+ {showState !== undefined ? (
+ <ShowExchangeStatus
+ e={showState}
+ onCancel={() => setShowState(undefined)}
+ />
+ ) : undefined}
+ <ListPage exchanges={exchanges} onSelect={(e) => setShowState(e)} />
+ </Fragment>
+ );
+}
+
+function ShowExchangeStatus({
+ e,
+ onCancel,
+}: {
+ e: TalerMerchantApi.ExchangeStatusDetail;
+ onCancel: () => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const isWorking = e.keys_http_status >= 200 && e.keys_http_status < 300;
+ if (isWorking) {
+ return (
+ <ConfirmModal
+ label={i18n.str`Ok`}
+ description={e.exchange_url as TranslatedString}
+ active
+ onCancel={onCancel}
+ >
+ <p style={{ paddingTop: 0 }}>
+ {e.keys_expiration ? (
+ <i18n.Translate>
+ Ready until{" "}
+ <Time
+ format="dd/MM/yyyy HH:mm:ss"
+ timestamp={AbsoluteTime.fromProtocolTimestamp(
+ e.keys_expiration,
+ )}
+ />
+ </i18n.Translate>
+ ) : undefined}
+ </p>
+ <p style={{ paddingTop: 0 }}>
+ <i18n.Translate>
+ Next update will be at{" "}
+ <Time
+ format="dd/MM/yyyy HH:mm:ss"
+ timestamp={AbsoluteTime.fromProtocolTimestamp(e.next_download)}
+ />
+ </i18n.Translate>
+ </p>
+ </ConfirmModal>
+ );
+ }
+ return (
+ <ConfirmModal
+ label={i18n.str`Error`}
+ description={e.exchange_url as TranslatedString}
+ active
+ onCancel={onCancel}
+ >
+ <p style={{ paddingTop: 0 }}>
+ <b><i18n.Translate>This exchange is down.</i18n.Translate></b>{" "}
+ <i18n.Translate>
+ Next update will be at{" "}
+ <Time
+ format="dd/MM/yyyy HH:mm:ss"
+ timestamp={AbsoluteTime.fromProtocolTimestamp(e.next_download)}
+ />
+ </i18n.Translate>
+ </p>
+ <p style={{ paddingTop: 0 }}>
+ <i18n.Translate>
+ Last HTTP status was {e.keys_http_status}: {e.keys_hint} (#{e.keys_ec}
+ )
+ </i18n.Translate>
+ </p>
+ </ConfirmModal>
+ );
+}
diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts
@@ -46,6 +46,7 @@ import {
codecForChallengeRequestResponse,
codecForChallengeResponse,
codecForClaimResponse,
+ codecForExchangeStatusResponse,
codecForExpectedTansferList,
codecForExpectedTransferDetails,
codecForFullInventoryDetailsResponse,
@@ -237,6 +238,26 @@ export class TalerMerchantInstanceHttpClient {
}
}
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get--exchanges
+ */
+ async listExchanges() {
+ const url = new URL(`exchanges`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForExchangeStatusResponse());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.InternalServerError:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
//
// Auth
//
@@ -256,17 +277,17 @@ export class TalerMerchantInstanceHttpClient {
| OperationFail<HttpStatusCode.NotFound>
| OperationOk<TalerMerchantApi.LoginTokenSuccessResponse>
| OperationAlternative<
- HttpStatusCode.Accepted,
- TalerMerchantApi.ChallengeResponse
- >
+ HttpStatusCode.Accepted,
+ TalerMerchantApi.ChallengeResponse
+ >
| OperationFail<HttpStatusCode.Unauthorized>
> {
const url = new URL(`private/token`, this.baseUrl);
const headers = authHeaders({
type: "basic",
username: instance,
- password
- })
+ password,
+ });
if (params.challengeIds && params.challengeIds.length > 0) {
headers["Taler-Challenge-Ids"] = params.challengeIds.join(", ");
}
@@ -931,9 +952,9 @@ export class TalerMerchantInstanceHttpClient {
| OperationOk<TalerMerchantApi.AccountAddResponse>
| OperationFail<HttpStatusCode.NotFound>
| OperationAlternative<
- HttpStatusCode.Accepted,
- TalerMerchantApi.ChallengeResponse
- >
+ HttpStatusCode.Accepted,
+ TalerMerchantApi.ChallengeResponse
+ >
| OperationFail<HttpStatusCode.Unauthorized>
| OperationFail<HttpStatusCode.Conflict>
> {
diff --git a/packages/taler-util/src/types-taler-merchant.ts b/packages/taler-util/src/types-taler-merchant.ts
@@ -87,7 +87,7 @@ import {
codecForInternationalizedString,
codecForURLString,
} from "./types-taler-common.js";
-import { PayWalletData } from "./types-taler-wallet.js";
+import { codecForCanonBaseUrl, PayWalletData } from "./types-taler-wallet.js";
/**
* Proposal returned from the contract URL.
@@ -3710,10 +3710,14 @@ export interface TemplateDetails {
required_currency?: string;
}
-export type UsingTemplateDetailsRequest = (UsingTemplateFixedOrderRequest | UsingTemplateInventoryCartRequest | UsingTemplatePaivanaRequest) & UsingTemplateCommonRequest;
+export type UsingTemplateDetailsRequest = (
+ | UsingTemplateFixedOrderRequest
+ | UsingTemplateInventoryCartRequest
+ | UsingTemplatePaivanaRequest
+) &
+ UsingTemplateCommonRequest;
export interface UsingTemplateCommonRequest {
-
// Type of the template being instantiated.
// Possible values include "fixed-order",
// "inventory-cart" and "paivana".
@@ -3733,12 +3737,10 @@ export interface UsingTemplateCommonRequest {
// fixed template currency.
// Since protocol **v25**.
tip?: AmountString;
-
}
export interface UsingTemplateFixedOrderRequest {
- template_type: TemplateType.FIXED_ORDER;
-
+ template_type: TemplateType.FIXED_ORDER;
}
export interface UsingTemplateInventoryCartRequest {
@@ -3768,10 +3770,8 @@ export interface UsingTemplatePaivanaRequest {
// This becomes the "session_id" for session-based
// access control.
paivana_id: string;
-
}
-
export interface WebhookAddDetails {
// Webhook ID to use.
webhook_id: string;
@@ -4473,6 +4473,49 @@ export interface MerchantTemplateAddDetails {
otp_id?: string;
}
+export interface ExchangeStatusResponse {
+ exchanges: ExchangeStatusDetail[];
+}
+
+export interface ExchangeStatusDetail {
+ // Base URL of the exchange this is about.
+ exchange_url: string;
+
+ // Time when the backend will download /keys next.
+ next_download: Timestamp;
+
+ // Time when the current /keys response is expected to
+ // expire. Missing if we do not have one.
+ keys_expiration?: Timestamp;
+
+ // HTTP status code returned by the exchange when we asked for
+ // /keys. 0 if we did not receive an HTTP status code.
+ // Usually 200 for success.
+ keys_http_status: Integer;
+
+ // Numeric error code indicating an
+ // error we had processing the /keys response.
+ keys_ec: Integer;
+
+ // Human-readable error description matching keys_ec.
+ keys_hint: string;
+}
+
+export const codecForExchangeStatusResponse = (): Codec<ExchangeStatusResponse> =>
+ buildCodecForObject<ExchangeStatusResponse>()
+ .property("exchanges", codecForList(codecForExchangeStatusDetail()))
+ .build("TalerMerchantApi.ExchangeStatusResponse");
+
+export const codecForExchangeStatusDetail = (): Codec<ExchangeStatusDetail> =>
+ buildCodecForObject<ExchangeStatusDetail>()
+ .property("exchange_url", codecForCanonBaseUrl())
+ .property("next_download", codecForTimestamp)
+ .property("keys_expiration", codecOptional(codecForTimestamp))
+ .property("keys_http_status", codecForNumber())
+ .property("keys_ec", codecForNumber())
+ .property("keys_hint", codecForString())
+ .build("TalerMerchantApi.ExchangeStatusDetail");
+
const codecForExchangeConfigInfo = (): Codec<ExchangeConfigInfo> =>
buildCodecForObject<ExchangeConfigInfo>()
.property("base_url", codecForString())
@@ -5315,7 +5358,7 @@ export const codecForTemplateContractInventoryCart =
"selected_products",
codecOptionalDefault(codecForList(codecForString()), []),
)
- .property("inventory_payload", codecOptional(codecForAny())) // FIXME: validate
+ .property("inventory_payload", codecOptional(codecForAny())) // FIXME: validate
.property("currency", codecOptional(codecForString()))
.property("max_pickup_duration", codecOptional(codecForDuration))