taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 87066659bf4c62a06bfcca2fbd6c3a81ed4fef40
parent be412f419efd37dd558753a5f79a687b4672d90f
Author: Florian Dold <florian@dold.me>
Date:   Mon, 15 Jul 2024 17:32:26 +0200

harness,util,others: check version in /config responses

Diffstat:
Mpackages/bank-ui/src/pages/admin/AccountForm.tsx | 15+++++++++------
Mpackages/bank-ui/src/pages/regional/CreateCashout.tsx | 40+++++++++++++++++++++-------------------
Mpackages/bank-ui/src/stories.test.ts | 4++--
Mpackages/merchant-backoffice-ui/src/Application.tsx | 22++++++++++++----------
Mpackages/merchant-backoffice-ui/src/context/session.ts | 13+++++++------
Mpackages/taler-harness/src/harness/harness.ts | 63++++++++++++++++++++++++++++++++++++++++++++++++---------------
Mpackages/taler-util/src/MerchantApiClient.ts | 6+++---
Mpackages/taler-util/src/errors.ts | 8++++----
Mpackages/taler-util/src/http-client/bank-core.ts | 42+++++++++++++++++++++++++++++++++++++++---
Mpackages/taler-util/src/http-client/exchange.ts | 50++++++++++++++++++++++++++++++++++++++++++++++----
Mpackages/taler-util/src/http-client/merchant.ts | 50++++++++++++++++++++++++++++++++++++++++++++------
Mpackages/taler-util/src/taler-error-codes.ts | 48++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-util/src/types-taler-common.ts | 12+++++-------
Mpackages/taler-util/src/types-taler-corebank.ts | 202++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mpackages/taler-util/src/types-taler-merchant.ts | 6+++---
Mpackages/web-util/src/context/bank-api.ts | 12++++++------
Mpackages/web-util/src/context/merchant-api.ts | 12+++++++-----
17 files changed, 406 insertions(+), 199 deletions(-)

diff --git a/packages/bank-ui/src/pages/admin/AccountForm.tsx b/packages/bank-ui/src/pages/admin/AccountForm.tsx @@ -26,11 +26,11 @@ import { import { CopyButton, ShowInputErrorLabel, + useBankCoreApiContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { ComponentChildren, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { useBankCoreApiContext } from "@gnu-taler/web-util/browser"; import { useSessionState } from "../../hooks/session.js"; import { ErrorMessageMappingFor, @@ -110,7 +110,9 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ const defaultValue: AccountFormData = { debit_threshold: Amounts.stringifyValue( - template?.debit_threshold ?? config.default_debit_threshold, + template?.debit_threshold ?? + config.default_debit_threshold ?? + `${config.currency}:0`, ), min_cashout: Amounts.stringifyValue( template?.min_cashout ?? `${config.currency}:0`, @@ -213,10 +215,11 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ : undefined, name: !editableName ? undefined // disabled - : purpose === "update" && newForm.name === undefined ? undefined // the field hasn't been changed - : !newForm.name - ? i18n.str`Required` - : undefined, + : purpose === "update" && newForm.name === undefined + ? undefined // the field hasn't been changed + : !newForm.name + ? i18n.str`Required` + : undefined, username: !editableUsername ? undefined : !newForm.username diff --git a/packages/bank-ui/src/pages/regional/CreateCashout.tsx b/packages/bank-ui/src/pages/regional/CreateCashout.tsx @@ -29,15 +29,16 @@ import { Attention, Loading, LocalNotificationBanner, + RouteDefinition, ShowInputErrorLabel, notifyInfo, + useBankCoreApiContext, useLocalNotification, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js"; -import { useBankCoreApiContext } from "@gnu-taler/web-util/browser"; import { useAccountDetails } from "../../hooks/account.js"; import { useBankState } from "../../hooks/bank-state.js"; import { @@ -46,7 +47,6 @@ import { useConversionInfo, } from "../../hooks/regional.js"; import { useSessionState } from "../../hooks/session.js"; -import { RouteDefinition } from "@gnu-taler/web-util/browser"; import { TanChannel, undefinedIfEmpty } from "../../utils.js"; import { LoginForm } from "../LoginForm.js"; import { @@ -182,7 +182,10 @@ export function CreateCashout({ balanceIsDebit: resultAccount.body.balance.credit_debit_indicator == "debit", debitThreshold: Amounts.parseOrThrow(resultAccount.body.debit_threshold), - minCashout: resultAccount.body.min_cashout === undefined ? regionalZero : Amounts.parseOrThrow(resultAccount.body.min_cashout) + minCashout: + resultAccount.body.min_cashout === undefined + ? regionalZero + : Amounts.parseOrThrow(resultAccount.body.min_cashout), }; const limit = account.balanceIsDebit @@ -245,25 +248,24 @@ export function CreateCashout({ : Amounts.cmp(limit, calc.debit) === -1 ? i18n.str`Balance is not enough` : calculationResult === "amount-is-too-small" - ? i18n.str`Amount needs to be higher` - : Amounts.cmp(calc.debit, conversionInfo.cashout_min_amount) < 0 - ? i18n.str`No account can't cashout less than ${ - Amounts.stringifyValueWithSpec( - Amounts.parseOrThrow(conversionInfo.cashout_min_amount), - regional_currency_specification, - ).normal - }` - : Amounts.cmp(calc.debit, account.minCashout) < 0 - ? i18n.str`Your account can't cashout less than ${ + ? i18n.str`Amount needs to be higher` + : Amounts.cmp(calc.debit, conversionInfo.cashout_min_amount) < 0 + ? i18n.str`No account can't cashout less than ${ Amounts.stringifyValueWithSpec( - Amounts.parseOrThrow(account.minCashout), + Amounts.parseOrThrow(conversionInfo.cashout_min_amount), regional_currency_specification, ).normal }` - - : Amounts.isZero(calc.credit) - ? i18n.str`The total transfer at destination will be zero` - : undefined, + : Amounts.cmp(calc.debit, account.minCashout) < 0 + ? i18n.str`Your account can't cashout less than ${ + Amounts.stringifyValueWithSpec( + Amounts.parseOrThrow(account.minCashout), + regional_currency_specification, + ).normal + }` + : Amounts.isZero(calc.credit) + ? i18n.str`The total transfer at destination will be zero` + : undefined, }); const trimmedAmountStr = form.amount?.trim(); @@ -364,7 +366,7 @@ export function CreateCashout({ }); } const cashoutDisabled = - config.supported_tan_channels.length < 1 || + (config.supported_tan_channels ?? []).length < 1 || !resultAccount.body.cashout_payto_uri; const cashoutAccount = !resultAccount.body.cashout_payto_uri diff --git a/packages/bank-ui/src/stories.test.ts b/packages/bank-ui/src/stories.test.ts @@ -20,7 +20,7 @@ */ import { AmountString, - TalerCorebankApi, + TalerCorebankConfigResponse, setupI18n, } from "@gnu-taler/taler-util"; import { @@ -54,7 +54,7 @@ describe("All the examples:", () => { }); function DefaultTestingContext(_props: { children: ComponentChildren }): VNode { - const cfg: TalerCorebankApi.Config = { + const cfg: TalerCorebankConfigResponse = { name: "libeufin-bank", allow_deletions: true, bank_name: "taler bank", diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx @@ -41,6 +41,7 @@ import { SWRConfig } from "swr"; import { Routing } from "./Routing.js"; import { Loading } from "./components/exception/loading.js"; import { NotificationCard } from "./components/menu/index.js"; +import { SessionContextProvider } from "./context/session.js"; import { SettingsProvider } from "./context/settings.js"; import { revalidateBankAccountDetails, @@ -52,6 +53,10 @@ import { revalidateManagedInstanceDetails, } from "./hooks/instance.js"; import { + revalidateInstanceOrders, + revalidateOrderDetails, +} from "./hooks/order.js"; +import { revalidateInstanceOtpDevices, revalidateOtpDeviceDetails, } from "./hooks/otp.js"; @@ -63,6 +68,10 @@ import { revalidateInstanceTemplates, revalidateTemplateDetails, } from "./hooks/templates.js"; +import { + revalidateTokenFamilies, + revalidateTokenFamilyDetails, +} from "./hooks/tokenfamily.js"; import { revalidateInstanceTransfers } from "./hooks/transfer.js"; import { revalidateInstanceWebhooks, @@ -74,15 +83,6 @@ import { buildDefaultBackendBaseURL, fetchSettings, } from "./settings.js"; -import { - revalidateInstanceOrders, - revalidateOrderDetails, -} from "./hooks/order.js"; -import { SessionContextProvider } from "./context/session.js"; -import { - revalidateTokenFamilies, - revalidateTokenFamilyDetails, -} from "./hooks/tokenfamily.js"; const WITH_LOCAL_STORAGE_CACHE = false; export function Application(): VNode { @@ -197,7 +197,9 @@ function localStorageProvider(): Map<unknown, unknown> { function OnConfigError({ state, }: { - state: ConfigResultFail<TalerMerchantApi.VersionResponse> | undefined; + state: + | ConfigResultFail<TalerMerchantApi.TalerMerchantConfigResponse> + | undefined; }): VNode { const { i18n } = useTranslationContext(); if (!state) { diff --git a/packages/merchant-backoffice-ui/src/context/session.ts b/packages/merchant-backoffice-ui/src/context/session.ts @@ -18,6 +18,7 @@ import { AccessToken, Codec, TalerMerchantApi, + TalerMerchantConfigResponse, buildCodecForObject, codecForString, codecForURL, @@ -99,7 +100,7 @@ export const defaultState = (url: URL): SavedSession => { export interface SessionStateHandler { lib: MerchantLib; - config: TalerMerchantApi.VersionResponse; + config: TalerMerchantConfigResponse; state: SessionState; /** @@ -144,9 +145,9 @@ export const useSessionContext = (): SessionStateHandler => useContext(Context); * Infer the instance name based on the URL. * Create the instance of the merchant api http rest. * Returns API that handle impersonation. - * - * @param param0 - * @returns + * + * @param param0 + * @returns */ export const SessionContextProvider = ({ children, @@ -162,7 +163,7 @@ export const SessionContextProvider = ({ } = useMerchantApiContext(); const [status, setStatus] = useState<"loggedIn" | "loggedOut">("loggedIn"); const [currentConfig, setCurrentConfig] = - useState<TalerMerchantApi.VersionResponse>(); + useState<TalerMerchantConfigResponse>(); const { value: state, update } = useLocalStorage( SESSION_STATE_KEY, defaultState(merchantUrl), @@ -171,7 +172,7 @@ export const SessionContextProvider = ({ const currentInstance = inferInstanceName(state.backendUrl); let lib: MerchantLib; - let config: TalerMerchantApi.VersionResponse; + let config: TalerMerchantConfigResponse; const doingImpersonation = state.backendUrl.href !== merchantUrl.href; if (doingImpersonation) { /** diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts @@ -36,9 +36,12 @@ import { MerchantInstanceConfig, PartialMerchantInstanceConfig, PaytoString, + TalerCoreBankHttpClient, TalerCorebankApiClient, TalerError, + TalerExchangeHttpClient, TalerMerchantApi, + TalerMerchantManagementHttpClient, WalletNotification, createEddsaKeyPair, eddsaGetPublic, @@ -736,9 +739,20 @@ export class FakebankService "bank", ); await this.pingUntilAvailable(); - const bankClient = new TalerCorebankApiClient(this.corebankApiBaseUrl); - for (const acc of this.accounts) { - await bankClient.registerAccount(acc.accountName, acc.accountPassword); + // Check version + { + const bankClient = new TalerCoreBankHttpClient(this.corebankApiBaseUrl); + // This would fail/throw if the version doesn't match. + const resp = await bankClient.getConfig(); + this.globalTestState.assertTrue(resp.type === "ok"); + } + // Register bank accounts + { + // FIXME: This is using the old bank client! + const bankClient = new TalerCorebankApiClient(this.corebankApiBaseUrl); + for (const acc of this.accounts) { + await bankClient.registerAccount(acc.accountName, acc.accountPassword); + } } } @@ -805,11 +819,7 @@ export class LibeufinBankService "registration_bonus", `${bc.currency}:100`, ); - config.setString( - "libeufin-bank", - "ALLOW_REGISTRATION", - "yes", - ); + config.setString("libeufin-bank", "ALLOW_REGISTRATION", "yes"); const cfgFilename = testDir + "/bank.conf"; config.writeTo(cfgFilename, { excludeDefaults: true }); @@ -895,9 +905,20 @@ export class LibeufinBankService "libeufin-bank-httpd", ); await this.pingUntilAvailable(); - const bankClient = new TalerCorebankApiClient(this.corebankApiBaseUrl); - for (const acc of this.accounts) { - await bankClient.registerAccount(acc.accountName, acc.accountPassword); + // Check version + { + const bankClient = new TalerCoreBankHttpClient(this.corebankApiBaseUrl); + // This would fail/throw if the version doesn't match. + const resp = await bankClient.getConfig(); + this.globalTestState.assertTrue(resp.type === "ok"); + } + // Register accounts + { + // FIXME: This still uses the old-style client. + const bankClient = new TalerCorebankApiClient(this.corebankApiBaseUrl); + for (const acc of this.accounts) { + await bankClient.registerAccount(acc.accountName, acc.accountPassword); + } } } @@ -1581,6 +1602,13 @@ export class ExchangeService implements ExchangeServiceInterface { await this.pingUntilAvailable(); + { + const exchangeClient = new TalerExchangeHttpClient(this.baseUrl); + // Would throw on incompatible version. + const configResp = await exchangeClient.getConfig(); + this.globalState.assertTrue(configResp.type === "ok"); + } + const skipKeyup = opts.skipKeyup ?? false; if (!skipKeyup) { @@ -1726,6 +1754,15 @@ export class MerchantService implements MerchantServiceInterface { ], `merchant-${this.merchantConfig.name}`, ); + + await this.pingUntilAvailable(); + { + const merchantClient = new TalerMerchantManagementHttpClient( + this.makeInstanceBaseUrl(), + ); + const configResp = await merchantClient.getConfig(); + this.globalState.assertTrue(configResp.type === "ok"); + } } static async create( @@ -2280,10 +2317,6 @@ export function generateRandomTestIban(salt: string | null = null): string { return `DE${check_digits}${bban}`; } -export function getWireMethodForTest(): string { - return "x-taler-bank"; -} - /** * Generate a payto address, whose authority depends * on whether the banking is served by euFin or Pybank. diff --git a/packages/taler-util/src/MerchantApiClient.ts b/packages/taler-util/src/MerchantApiClient.ts @@ -38,7 +38,7 @@ import { TalerProtocolDuration } from "./time.js"; import { AmountString } from "./types-taler-common.js"; import { OtpDeviceAddDetails, - codecForMerchantConfig, + codecForTalerMerchantConfigResponse, codecForMerchantOrderPrivateStatusResponse, codecForPostOrderResponse, } from "./types-taler-merchant.js"; @@ -294,14 +294,14 @@ export class MerchantApiClient { * https://docs.taler.net/core/api-merchant.html#get--config * */ - async getConfig(): Promise<OperationOk<TalerMerchantApi.VersionResponse>> { + async getConfig(): Promise<OperationOk<TalerMerchantApi.TalerMerchantConfigResponse>> { const url = new URL(`config`, this.baseUrl); const resp = await this.httpClient.fetch(url.href, { method: "GET", }); switch (resp.status) { case HttpStatusCode.Ok: - return opSuccessFromHttp(resp, codecForMerchantConfig()); + return opSuccessFromHttp(resp, codecForTalerMerchantConfigResponse()); default: return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } diff --git a/packages/taler-util/src/errors.ts b/packages/taler-util/src/errors.ts @@ -188,7 +188,7 @@ export function makeErrorDetail<C extends TalerErrorCode>( hint?: string, ): TalerErrorDetail { if (!hint && !(detail as any).hint) { - hint = getDefaultHint(code); + hint = getDefaultTalerErrorHint(code); } const when = AbsoluteTime.now(); return { code, when, hint, ...detail }; @@ -210,7 +210,7 @@ export function summarizeTalerErrorDetail(ed: TalerErrorDetail): string { return `Error (${ed.code}/${errName})`; } -function getDefaultHint(code: number): string { +export function getDefaultTalerErrorHint(code: number): string { const errName = TalerErrorCode[code]; if (errName) { return `Error (${errName})`; @@ -257,7 +257,7 @@ export function makeTalerErrorDetail<C extends TalerErrorCode>( hint?: string, ): TalerErrorDetail { if (!hint) { - hint = getDefaultHint(code); + hint = getDefaultTalerErrorHint(code); } return { code, hint, ...errBody }; } @@ -279,7 +279,7 @@ export class TalerError<T = any> extends Error { cause?: Error, ): TalerError { if (!hint) { - hint = getDefaultHint(code); + hint = getDefaultTalerErrorHint(code); } const when = AbsoluteTime.now(); return new TalerError<unknown>({ code, when, hint, ...detail }, cause); diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts @@ -24,8 +24,10 @@ import { OperationFail, OperationOk, PaginationParams, + TalerError, TalerErrorCode, UserAndToken, + codecForTalerCommonConfigResponse, opKnownAlternativeFailure, opKnownHttpFailure, opKnownTalerFailure, @@ -33,6 +35,7 @@ import { import { HttpRequestLibrary, createPlatformHttpLib, + readSuccessResponseJsonOrThrow, readTalerErrorResponse, } from "@gnu-taler/taler-util/http"; import { @@ -125,14 +128,47 @@ export class TalerCoreBankHttpClient { * https://docs.taler.net/core/api-corebank.html#config * */ - async getConfig() { + async getConfig(): Promise< + OperationFail<HttpStatusCode.NotFound> | OperationOk<TalerCorebankApi.TalerCorebankConfigResponse> + > { const url = new URL(`config`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "GET", }); switch (resp.status) { - case HttpStatusCode.Ok: - return opSuccessFromHttp(resp, codecForCoreBankConfig()); + case HttpStatusCode.Ok: { + const minBody = await readSuccessResponseJsonOrThrow( + resp, + codecForTalerCommonConfigResponse(), + ); + // FIXME: Re-enable the check once fakebank and libeufin-bank return the name. + // const expectedName = "taler-corebank"; + // if (minBody.name !== expectedName) { + // throw TalerError.fromUncheckedDetail({ + // code: TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR, + // requestUrl: resp.requestUrl, + // httpStatusCode: resp.status, + // detail: `Unexpected server component name (got ${minBody.name}, expected ${expectedName}})`, + // }); + // } + if (!this.isCompatible(minBody.version)) { + throw TalerError.fromUncheckedDetail({ + code: TalerErrorCode.GENERIC_CLIENT_UNSUPPORTED_PROTOCOL_VERSION, + requestUrl: resp.requestUrl, + httpStatusCode: resp.status, + detail: `Unsupported protocol version, client supports ${this.PROTOCOL_VERSION}, server supports ${minBody.version}`, + }); + } + // Now that we've checked the basic body, re-parse the full response. + const body = await readSuccessResponseJsonOrThrow( + resp, + codecForCoreBankConfig(), + ); + return { + type: "ok", + body, + }; + } case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); default: diff --git a/packages/taler-util/src/http-client/exchange.ts b/packages/taler-util/src/http-client/exchange.ts @@ -1,10 +1,16 @@ -import { HttpRequestLibrary, readTalerErrorResponse } from "../http-common.js"; +import { + HttpRequestLibrary, + readSuccessResponseJsonOrThrow, + readTalerErrorResponse, +} from "../http-common.js"; import { HttpStatusCode } from "../http-status-codes.js"; import { createPlatformHttpLib } from "../http.js"; import { LibtoolVersion } from "../libtool-version.js"; import { hash } from "../nacl-fast.js"; import { FailCasesByMethod, + OperationFail, + OperationOk, ResultByMethod, opEmptySuccess, opFixedSuccess, @@ -27,6 +33,7 @@ import { OfficerAccount, PaginationParams, SigningKey, + codecForTalerCommonConfigResponse, } from "../types-taler-common.js"; import { codecForAmlDecisionDetails, @@ -36,6 +43,8 @@ import { } from "../types-taler-exchange.js"; import { CacheEvictor, addPaginationParams, nullEvictor } from "./utils.js"; +import { TalerError } from "../errors.js"; +import { TalerErrorCode } from "../taler-error-codes.js"; import * as TalerExchangeApi from "../types-taler-exchange.js"; export type TalerExchangeResultByMethod< @@ -94,14 +103,47 @@ export class TalerExchangeHttpClient { * https://docs.taler.net/core/api-exchange.html#get--config * */ - async getConfig() { + async getConfig(): Promise< + | OperationFail<HttpStatusCode.NotFound> + | OperationOk<TalerExchangeApi.ExchangeVersionResponse> + > { const url = new URL(`config`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "GET", }); switch (resp.status) { - case HttpStatusCode.Ok: - return opSuccessFromHttp(resp, codecForExchangeConfig()); + case HttpStatusCode.Ok: { + const minBody = await readSuccessResponseJsonOrThrow( + resp, + codecForTalerCommonConfigResponse(), + ); + const expectedName = "taler-exchange"; + if (minBody.name !== expectedName) { + throw TalerError.fromUncheckedDetail({ + code: TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR, + requestUrl: resp.requestUrl, + httpStatusCode: resp.status, + detail: `Unexpected server component name (got ${minBody.name}, expected ${expectedName}})`, + }); + } + if (!this.isCompatible(minBody.version)) { + throw TalerError.fromUncheckedDetail({ + code: TalerErrorCode.GENERIC_CLIENT_UNSUPPORTED_PROTOCOL_VERSION, + requestUrl: resp.requestUrl, + httpStatusCode: resp.status, + detail: `Unsupported protocol version, client supports ${this.PROTOCOL_VERSION}, server supports ${minBody.version}`, + }); + } + // Now that we've checked the basic body, re-parse the full response. + const body = await readSuccessResponseJsonOrThrow( + resp, + codecForExchangeConfig(), + ); + return { + type: "ok", + body, + }; + } case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); default: diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts @@ -19,9 +19,14 @@ import { FailCasesByMethod, HttpStatusCode, LibtoolVersion, + OperationFail, + OperationOk, PaginationParams, ResultByMethod, + TalerError, + TalerErrorCode, TalerMerchantApi, + TalerMerchantConfigResponse, codecForAbortResponse, codecForAccountAddResponse, codecForAccountKycRedirects, @@ -30,7 +35,6 @@ import { codecForClaimResponse, codecForInstancesResponse, codecForInventorySummaryResponse, - codecForMerchantConfig, codecForMerchantOrderPrivateStatusResponse, codecForMerchantPosProductDetail, codecForMerchantRefundResponse, @@ -46,6 +50,8 @@ import { codecForStatusGoto, codecForStatusPaid, codecForStatusStatusUnpaid, + codecForTalerCommonConfigResponse, + codecForTalerMerchantConfigResponse, codecForTansferList, codecForTemplateDetails, codecForTemplateSummaryResponse, @@ -63,6 +69,7 @@ import { HttpRequestLibrary, HttpResponse, createPlatformHttpLib, + readSuccessResponseJsonOrThrow, readTalerErrorResponse, } from "@gnu-taler/taler-util/http"; import { opSuccessFromHttp, opUnknownFailure } from "../operation.js"; @@ -146,17 +153,48 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get--config - * */ - async getConfig() { + async getConfig(): Promise< + | OperationFail<HttpStatusCode.NotFound> + | OperationOk<TalerMerchantConfigResponse> + > { const url = new URL(`config`, this.baseUrl); - const resp = await this.httpLib.fetch(url.href, { method: "GET", }); switch (resp.status) { - case HttpStatusCode.Ok: - return opSuccessFromHttp(resp, codecForMerchantConfig()); + case HttpStatusCode.Ok: { + const minBody = await readSuccessResponseJsonOrThrow( + resp, + codecForTalerCommonConfigResponse(), + ); + const expectedName = "taler-merchant"; + if (minBody.name !== expectedName) { + throw TalerError.fromUncheckedDetail({ + code: TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR, + requestUrl: resp.requestUrl, + httpStatusCode: resp.status, + detail: `Unexpected server component name (got ${minBody.name}, expected ${expectedName}})`, + }); + } + if (!this.isCompatible(minBody.version)) { + throw TalerError.fromUncheckedDetail({ + code: TalerErrorCode.GENERIC_CLIENT_UNSUPPORTED_PROTOCOL_VERSION, + requestUrl: resp.requestUrl, + httpStatusCode: resp.status, + detail: `Unsupported protocol version, client supports ${this.PROTOCOL_VERSION}, server supports ${minBody.version}`, + }); + } + // Now that we've checked the basic body, re-parse the full response. + const body = await readSuccessResponseJsonOrThrow( + resp, + codecForTalerMerchantConfigResponse(), + ); + return { + type: "ok", + body, + }; + } case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); default: diff --git a/packages/taler-util/src/taler-error-codes.ts b/packages/taler-util/src/taler-error-codes.ts @@ -49,6 +49,14 @@ export enum TalerErrorCode { /** + * The client does not support the protocol version advertised by the server. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_CLIENT_UNSUPPORTED_PROTOCOL_VERSION = 3, + + + /** * The response we got from the server was not in the expected format. Most likely, the server does not speak the GNU Taler protocol. Check the URL and/or the network connection to the server. * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). * (A value of 0 indicates that the error is generated client-side). @@ -177,6 +185,14 @@ export enum TalerErrorCode { /** + * A segment in the path of the URL provided by the client is malformed. Check that you are using the correct encoding for the URL. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_PATH_SEGMENT_MALFORMED = 29, + + + /** * The currency involved in the operation is not acceptable for this server. Check your configuration and make sure the currency specified for a given service provider is one of the currencies supported by that provider. * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). * (A value of 0 indicates that the error is generated client-side). @@ -2033,6 +2049,38 @@ export enum TalerErrorCode { /** + * The form has been previously uploaded, and may only be filed once. The user should be redirected to their main KYC page and see if any other steps need to be taken. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_KYC_FORM_ALREADY_UPLOADED = 1941, + + + /** + * The internal state of the exchange specifying KYC measures is malformed. Please contact technical support. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_KYC_MEASURES_MALFORMED = 1942, + + + /** + * The specified index does not refer to a valid KYC measure. Please check the URL. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_KYC_MEASURE_INDEX_INVALID = 1943, + + + /** + * The operation is not supported by the selected KYC logic. This is either caused by a configuration change or some invalid use of the API. Please contact technical support. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_KYC_INVALID_LOGIC_TO_CHECK = 1944, + + + /** * The exchange does not know a contract under the given contract public key. * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). * (A value of 0 indicates that the error is generated client-side). diff --git a/packages/taler-util/src/types-taler-common.ts b/packages/taler-util/src/types-taler-common.ts @@ -261,19 +261,17 @@ export const codecForCurrencySpecificiation = .property("alt_unit_names", codecForMap(codecForString())) .build("CurrencySpecification"); -export interface MerchantConfigResponse { - currency: string; +export interface TalerCommonConfigResponse { name: string; version: string; } -export const codecForMerchantConfigResponse = - (): Codec<MerchantConfigResponse> => - buildCodecForObject<MerchantConfigResponse>() - .property("currency", codecForString()) +export const codecForTalerCommonConfigResponse = + (): Codec<TalerCommonConfigResponse> => + buildCodecForObject<TalerCommonConfigResponse>() .property("name", codecForString()) .property("version", codecForString()) - .build("MerchantConfigResponse"); + .build("TalerCommonConfigResponse"); export enum ExchangeProtocolVersion { /** diff --git a/packages/taler-util/src/types-taler-corebank.ts b/packages/taler-util/src/types-taler-corebank.ts @@ -62,10 +62,14 @@ export interface IntegrationConfig { // Name of the API. name: "taler-bank-integration"; } -export interface Config { - // Name of this API, always "taler-corebank". - name: "libeufin-bank"; - // name: "taler-corebank"; + +export interface TalerCorebankConfigResponse { + /** + * Name of this API, always "taler-corebank". + * + * For legacy reasons, libeufin-bank will also be accepted for some time. + */ + name: "libeufin-bank" | "taler-corebank"; // API version in the form $n:$n:$n version: string; @@ -73,7 +77,7 @@ export interface Config { // Bank display name to be used in user interfaces. // For consistency use "Taler Bank" if missing. // @since v4, will become mandatory in the next version. - bank_name: string; + bank_name?: string; // Advertised base URL to use when you sharing an URL with another // program. @@ -82,26 +86,26 @@ export interface Config { // If 'true' the server provides local currency conversion support // If 'false' some parts of the API are not supported and return 501 - allow_conversion: boolean; + allow_conversion?: boolean; // If 'true' anyone can register // If 'false' only the admin can - allow_registrations: boolean; + allow_registrations?: boolean; // If 'true' account can delete themselves // If 'false' only the admin can delete accounts - allow_deletions: boolean; + allow_deletions?: boolean; // If 'true' anyone can edit their name // If 'false' only admin can - allow_edit_name: boolean; + allow_edit_name?: boolean; // If 'true' anyone can edit their cashout account // If 'false' only the admin - allow_edit_cashout_payto_uri: boolean; + allow_edit_cashout_payto_uri?: boolean; // Default debt limit for newly created accounts - default_debit_threshold: AmountString; + default_debit_threshold?: AmountString; // Currency used by this bank. currency: string; @@ -110,12 +114,12 @@ export interface Config { currency_specification: CurrencySpecification; // TAN channels supported by the server - supported_tan_channels: TanChannel[]; + supported_tan_channels?: TanChannel[]; // Wire transfer type supported by the bank. // Default to 'iban' is missing // @since v4, may become mandatory in the future. - wire_type: string; + wire_type?: string; // Wire transfer execution fees. // @since v4, will become mandatory in the next version. @@ -630,26 +634,34 @@ export const codecForIntegrationBankConfig = (): Codec<IntegrationConfig> => .property("currency_specification", codecForCurrencySpecificiation()) .build("TalerCorebankApi.IntegrationConfig"); -export const codecForCoreBankConfig = (): Codec<Config> => - buildCodecForObject<Config>() - .property("name", codecForConstString("libeufin-bank")) +export const codecForCoreBankConfig = (): Codec<TalerCorebankConfigResponse> => + buildCodecForObject<TalerCorebankConfigResponse>() + .property( + "name", + codecForEither( + codecForConstString("taler-corebank"), + codecForConstString("libeufin-bank"), + ), + ) .property("version", codecForString()) - .property("bank_name", codecForString()) + .property("bank_name", codecOptional(codecForString())) .property("base_url", codecOptional(codecForString())) - .property("allow_conversion", codecForBoolean()) - .property("allow_registrations", codecForBoolean()) - .property("allow_deletions", codecForBoolean()) - .property("allow_edit_name", codecForBoolean()) - .property("allow_edit_cashout_payto_uri", codecForBoolean()) - .property("default_debit_threshold", codecForAmountString()) + .property("allow_conversion", codecOptional(codecForBoolean())) + .property("allow_registrations", codecOptional(codecForBoolean())) + .property("allow_deletions", codecOptional(codecForBoolean())) + .property("allow_edit_name", codecOptional(codecForBoolean())) + .property("allow_edit_cashout_payto_uri", codecOptional(codecForBoolean())) + .property("default_debit_threshold", codecOptional(codecForAmountString())) .property("currency", codecForString()) .property("currency_specification", codecForCurrencySpecificiation()) .property( "supported_tan_channels", - codecForList( - codecForEither( - codecForConstString(TanChannel.SMS), - codecForConstString(TanChannel.EMAIL), + codecOptional( + codecForList( + codecForEither( + codecForConstString(TanChannel.SMS), + codecForConstString(TanChannel.EMAIL), + ), ), ), ) @@ -684,28 +696,27 @@ export const codecForPublicAccountsResponse = .property("public_accounts", codecForList(codecForPublicAccount())) .build("TalerCorebankApi.PublicAccountsResponse"); -export const codecForAccountMinimalData = - (): Codec<AccountMinimalData> => - buildCodecForObject<AccountMinimalData>() - .property("username", codecForString()) - .property("name", codecForString()) - .property("payto_uri", codecForPaytoString()) - .property("balance", codecForBalance()) - .property("row_id", codecForNumber()) - .property("debit_threshold", codecForAmountString()) - .property("min_cashout", codecOptional(codecForAmountString())) - .property("is_public", codecForBoolean()) - .property("is_taler_exchange", codecForBoolean()) - .property( - "status", - codecOptional( - codecForEither( - codecForConstString("active"), - codecForConstString("deleted"), - ), +export const codecForAccountMinimalData = (): Codec<AccountMinimalData> => + buildCodecForObject<AccountMinimalData>() + .property("username", codecForString()) + .property("name", codecForString()) + .property("payto_uri", codecForPaytoString()) + .property("balance", codecForBalance()) + .property("row_id", codecForNumber()) + .property("debit_threshold", codecForAmountString()) + .property("min_cashout", codecOptional(codecForAmountString())) + .property("is_public", codecForBoolean()) + .property("is_taler_exchange", codecForBoolean()) + .property( + "status", + codecOptional( + codecForEither( + codecForConstString("active"), + codecForConstString("deleted"), ), - ) - .build("TalerCorebankApi.AccountMinimalData"); + ), + ) + .build("TalerCorebankApi.AccountMinimalData"); export const codecForListBankAccountsResponse = (): Codec<ListBankAccountsResponse> => @@ -744,33 +755,28 @@ export const codecForAccountData = (): Codec<AccountData> => ) .build("TalerCorebankApi.AccountData"); -export const codecForChallengeContactData = - (): Codec<ChallengeContactData> => - buildCodecForObject<ChallengeContactData>() - .property("email", codecOptional(codecForString())) - .property("phone", codecOptional(codecForString())) - .build("TalerCorebankApi.ChallengeContactData"); +export const codecForChallengeContactData = (): Codec<ChallengeContactData> => + buildCodecForObject<ChallengeContactData>() + .property("email", codecOptional(codecForString())) + .property("phone", codecOptional(codecForString())) + .build("TalerCorebankApi.ChallengeContactData"); -export const codecForWithdrawalPublicInfo = - (): Codec<WithdrawalPublicInfo> => - buildCodecForObject<WithdrawalPublicInfo>() - .property( - "status", - codecForEither( - codecForConstString("pending"), - codecForConstString("selected"), - codecForConstString("aborted"), - codecForConstString("confirmed"), - ), - ) - .property("amount", codecOptional(codecForAmountString())) - .property("username", codecForString()) - .property("selected_reserve_pub", codecOptional(codecForString())) - .property( - "selected_exchange_account", - codecOptional(codecForPaytoString()), - ) - .build("TalerCorebankApi.WithdrawalPublicInfo"); +export const codecForWithdrawalPublicInfo = (): Codec<WithdrawalPublicInfo> => + buildCodecForObject<WithdrawalPublicInfo>() + .property( + "status", + codecForEither( + codecForConstString("pending"), + codecForConstString("selected"), + codecForConstString("aborted"), + codecForConstString("confirmed"), + ), + ) + .property("amount", codecOptional(codecForAmountString())) + .property("username", codecForString()) + .property("selected_reserve_pub", codecOptional(codecForString())) + .property("selected_exchange_account", codecOptional(codecForPaytoString())) + .build("TalerCorebankApi.WithdrawalPublicInfo"); export const codecForBankAccountTransactionsResponse = (): Codec<BankAccountTransactionsResponse> => @@ -818,11 +824,10 @@ export const codecForBankAccountCreateWithdrawalResponse = .property("withdrawal_id", codecForString()) .build("TalerCorebankApi.BankAccountCreateWithdrawalResponse"); -export const codecForCashoutPending = - (): Codec<CashoutResponse> => - buildCodecForObject<CashoutResponse>() - .property("cashout_id", codecForNumber()) - .build("TalerCorebankApi.CashoutPending"); +export const codecForCashoutPending = (): Codec<CashoutResponse> => + buildCodecForObject<CashoutResponse>() + .property("cashout_id", codecForNumber()) + .build("TalerCorebankApi.CashoutPending"); export const codecForCashouts = (): Codec<Cashouts> => buildCodecForObject<Cashouts>() @@ -834,27 +839,24 @@ export const codecForCashoutInfo = (): Codec<CashoutInfo> => .property("cashout_id", codecForNumber()) .build("TalerCorebankApi.CashoutInfo"); -export const codecForGlobalCashouts = - (): Codec<GlobalCashouts> => - buildCodecForObject<GlobalCashouts>() - .property("cashouts", codecForList(codecForGlobalCashoutInfo())) - .build("TalerCorebankApi.GlobalCashouts"); - -export const codecForGlobalCashoutInfo = - (): Codec<GlobalCashoutInfo> => - buildCodecForObject<GlobalCashoutInfo>() - .property("cashout_id", codecForNumber()) - .property("username", codecForString()) - .build("TalerCorebankApi.GlobalCashoutInfo"); - -export const codecForCashoutStatusResponse = - (): Codec<CashoutStatusResponse> => - buildCodecForObject<CashoutStatusResponse>() - .property("amount_debit", codecForAmountString()) - .property("amount_credit", codecForAmountString()) - .property("subject", codecForString()) - .property("creation_time", codecForTimestamp) - .build("TalerCorebankApi.CashoutStatusResponse"); +export const codecForGlobalCashouts = (): Codec<GlobalCashouts> => + buildCodecForObject<GlobalCashouts>() + .property("cashouts", codecForList(codecForGlobalCashoutInfo())) + .build("TalerCorebankApi.GlobalCashouts"); + +export const codecForGlobalCashoutInfo = (): Codec<GlobalCashoutInfo> => + buildCodecForObject<GlobalCashoutInfo>() + .property("cashout_id", codecForNumber()) + .property("username", codecForString()) + .build("TalerCorebankApi.GlobalCashoutInfo"); + +export const codecForCashoutStatusResponse = (): Codec<CashoutStatusResponse> => + buildCodecForObject<CashoutStatusResponse>() + .property("amount_debit", codecForAmountString()) + .property("amount_credit", codecForAmountString()) + .property("subject", codecForString()) + .property("creation_time", codecForTimestamp) + .build("TalerCorebankApi.CashoutStatusResponse"); export const codecForConversionRatesResponse = (): Codec<ConversionRatesResponse> => diff --git a/packages/taler-util/src/types-taler-merchant.ts b/packages/taler-util/src/types-taler-merchant.ts @@ -731,7 +731,7 @@ export const codecForMerchantContractTerms = (): Codec<MerchantContractTerms> => .property("minimum_age", codecOptional(codecForNumber())) .build("MerchantContractTerms"); -export interface VersionResponse { +export interface TalerMerchantConfigResponse { // libtool-style representation of the Merchant protocol version, see // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning // The format is "current:revision:age". @@ -2734,8 +2734,8 @@ const codecForExchangeConfigInfo = (): Codec<ExchangeConfigInfo> => .property("master_pub", codecForString()) .build("TalerMerchantApi.ExchangeConfigInfo"); -export const codecForMerchantConfig = (): Codec<VersionResponse> => - buildCodecForObject<VersionResponse>() +export const codecForTalerMerchantConfigResponse = (): Codec<TalerMerchantConfigResponse> => + buildCodecForObject<TalerMerchantConfigResponse>() .property("name", codecForConstString("taler-merchant")) .property("currency", codecForString()) .property("version", codecForString()) diff --git a/packages/web-util/src/context/bank-api.ts b/packages/web-util/src/context/bank-api.ts @@ -16,6 +16,7 @@ import { CacheEvictor, + TalerCorebankConfigResponse, LibtoolVersion, ObservabilityEvent, ObservableHttpClientLibrary, @@ -24,7 +25,6 @@ import { TalerBankConversionHttpClient, TalerCoreBankCacheEviction, TalerCoreBankHttpClient, - TalerCorebankApi, TalerError, } from "@gnu-taler/taler-util"; import { @@ -35,9 +35,9 @@ import { h, } from "preact"; import { useContext, useEffect, useState } from "preact/hooks"; +import { BrowserFetchHttpLib, ErrorLoading } from "../index.browser.js"; import { APIClient, ActiviyTracker, BankLib, Subscriber } from "./activity.js"; import { useTranslationContext } from "./translation.js"; -import { BrowserFetchHttpLib, ErrorLoading } from "../index.browser.js"; /** * @@ -46,7 +46,7 @@ import { BrowserFetchHttpLib, ErrorLoading } from "../index.browser.js"; export type BankContextType = { url: URL; - config: TalerCorebankApi.Config; + config: TalerCorebankConfigResponse; lib: BankLib; hints: VersionHint[]; onActivity: Subscriber<ObservabilityEvent>; @@ -88,7 +88,7 @@ export const BankApiProvider = ({ frameOnError: FunctionComponent<{ children: ComponentChildren }>; }): VNode => { const [checked, setChecked] = - useState<ConfigResult<TalerCorebankApi.Config>>(); + useState<ConfigResult<TalerCorebankConfigResponse>>(); const { i18n } = useTranslationContext(); const { getRemoteConfig, VERSION, lib, cancelRequest, onActivity } = @@ -165,7 +165,7 @@ export const BankApiProvider = ({ function buildBankApiClient( url: URL, evictors: Evictors, -): APIClient<BankLib, TalerCorebankApi.Config> { +): APIClient<BankLib, TalerCorebankConfigResponse> { const httpFetch = new BrowserFetchHttpLib({ enableThrottling: true, requireTls: false, @@ -189,7 +189,7 @@ function buildBankApiClient( httpLib, ); - async function getRemoteConfig(): Promise<TalerCorebankApi.Config> { + async function getRemoteConfig(): Promise<TalerCorebankConfigResponse> { const resp = await bank.getConfig(); if (resp.type === "fail") { throw TalerError.fromUncheckedDetail(resp.detail); diff --git a/packages/web-util/src/context/merchant-api.ts b/packages/web-util/src/context/merchant-api.ts @@ -49,7 +49,7 @@ import { export type MerchantContextType = { url: URL; - config: TalerMerchantApi.VersionResponse; + config: TalerMerchantApi.TalerMerchantConfigResponse; lib: MerchantLib; hints: VersionHint[]; onActivity: Subscriber<ObservabilityEvent>; @@ -95,11 +95,13 @@ export const MerchantApiProvider = ({ evictors?: Evictors; children: ComponentChildren; frameOnError: FunctionComponent<{ - state: ConfigResultFail<TalerMerchantApi.VersionResponse> | undefined; + state: + | ConfigResultFail<TalerMerchantApi.TalerMerchantConfigResponse> + | undefined; }>; }): VNode => { const [checked, setChecked] = - useState<ConfigResult<TalerMerchantApi.VersionResponse>>(); + useState<ConfigResult<TalerMerchantApi.TalerMerchantConfigResponse>>(); const [merchantEndpoint, changeMerchantEndpoint] = useState(baseUrl); @@ -162,7 +164,7 @@ export const MerchantApiProvider = ({ function buildMerchantApiClient( url: URL, evictors: Evictors, -): APIClient<MerchantLib, TalerMerchantApi.VersionResponse> { +): APIClient<MerchantLib, TalerMerchantApi.TalerMerchantConfigResponse> { const httpFetch = new BrowserFetchHttpLib({ enableThrottling: true, requireTls: false, @@ -193,7 +195,7 @@ function buildMerchantApiClient( return api.lib; } - async function getRemoteConfig(): Promise<TalerMerchantApi.VersionResponse> { + async function getRemoteConfig(): Promise<TalerMerchantApi.TalerMerchantConfigResponse> { const resp = await instance.getConfig(); if (resp.type === "fail") { throw TalerError.fromUncheckedDetail(resp.detail);