gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: support for x-taler-bank and


From: gnunet
Subject: [taler-wallet-core] branch master updated: support for x-taler-bank and fix cache invalidation when new account is created
Date: Tue, 06 Feb 2024 20:52:21 +0100

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new 4eda6ac07 support for x-taler-bank and fix cache invalidation when new 
account is created
4eda6ac07 is described below

commit 4eda6ac07c78bcb3c2daa7846b4cd36048f9c7dd
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Tue Feb 6 16:51:15 2024 -0300

    support for x-taler-bank and fix cache invalidation when new account is 
created
---
 packages/demobank-ui/src/Routing.tsx               |  11 +-
 .../demobank-ui/src/components/Cashouts/views.tsx  |  20 +-
 packages/demobank-ui/src/context/config.ts         |  25 +-
 packages/demobank-ui/src/hooks/access.ts           |  16 +-
 packages/demobank-ui/src/hooks/circuit.ts          |  24 +-
 packages/demobank-ui/src/pages/PaymentOptions.tsx  |  15 +-
 .../src/pages/PaytoWireTransferForm.tsx            | 357 ++++++++++----
 .../demobank-ui/src/pages/SolveChallengePage.tsx   |   6 +-
 .../demobank-ui/src/pages/admin/AccountForm.tsx    | 536 +++++++++++----------
 .../demobank-ui/src/pages/admin/AccountList.tsx    |  24 +-
 packages/demobank-ui/src/pages/admin/AdminHome.tsx |   6 +-
 .../src/pages/admin/CreateNewAccount.tsx           |   2 -
 .../src/pages/business/CreateCashout.tsx           |  23 +-
 .../src/pages/business/ShowCashoutDetails.tsx      |  12 +-
 packages/demobank-ui/src/utils.ts                  |  99 ++--
 packages/taler-util/src/http-client/types.ts       |   6 +
 16 files changed, 714 insertions(+), 468 deletions(-)

diff --git a/packages/demobank-ui/src/Routing.tsx 
b/packages/demobank-ui/src/Routing.tsx
index 9f9475210..00811f2a7 100644
--- a/packages/demobank-ui/src/Routing.tsx
+++ b/packages/demobank-ui/src/Routing.tsx
@@ -57,7 +57,7 @@ export function Routing(): VNode {
   if (backend.state.status === "loggedIn") {
     const { isUserAdministrator, username } = backend.state;
     return (
-      <BankFrame account={username}>
+      <BankFrame account={username} 
routeAccountDetails={privatePages.myAccountDetails}>
         <PrivateRouting username={username} isAdmin={isUserAdministrator} />
       </BankFrame>
     );
@@ -147,7 +147,6 @@ function PublicRounting({
           <div class="sm:mx-auto sm:w-full sm:max-w-sm">
             <h2 class="text-center text-2xl font-bold leading-9 tracking-tight 
text-gray-900">{i18n.str`Welcome to ${settings.bankName}!`}</h2>
           </div>
-
           <LoginForm routeRegister={publicPages.register} />
         </Fragment>
       );
@@ -228,19 +227,19 @@ export const privatePages = {
   myAccountPassword: urlPattern(/\/my-password/, () => "#/my-password"),
   myAccountCashouts: urlPattern(/\/my-cashouts/, () => "#/my-cashouts"),
   accountDetails: urlPattern<{ account: string }>(
-    /\/profile\/(?<account>[a-zA-Z0-9]+)\/details/,
+    /\/profile\/(?<account>[a-zA-Z0-9_-]+)\/details/,
     ({ account }) => `#/profile/${account}/details`,
   ),
   accountChangePassword: urlPattern<{ account: string }>(
-    /\/profile\/(?<account>[a-zA-Z0-9]+)\/change-password/,
+    /\/profile\/(?<account>[a-zA-Z0-9_-]+)\/change-password/,
     ({ account }) => `#/profile/${account}/change-password`,
   ),
   accountDelete: urlPattern<{ account: string }>(
-    /\/profile\/(?<account>[a-zA-Z0-9]+)\/delete/,
+    /\/profile\/(?<account>[a-zA-Z0-9_-]+)\/delete/,
     ({ account }) => `#/profile/${account}/delete`,
   ),
   accountCashouts: urlPattern<{ account: string }>(
-    /\/profile\/(?<account>[a-zA-Z0-9]+)\/cashouts/,
+    /\/profile\/(?<account>[a-zA-Z0-9_-]+)\/cashouts/,
     ({ account }) => `#/profile/${account}/cashouts`,
   ),
   startOperation: urlPattern<{ wopid: string }>(
diff --git a/packages/demobank-ui/src/components/Cashouts/views.tsx 
b/packages/demobank-ui/src/components/Cashouts/views.tsx
index d036ec7d2..80eea6379 100644
--- a/packages/demobank-ui/src/components/Cashouts/views.tsx
+++ b/packages/demobank-ui/src/components/Cashouts/views.tsx
@@ -39,8 +39,10 @@ export function FailedView({ error }: State.Failed) {
       return (
         <Attention
           type="danger"
-          title={i18n.str`Cashout not implemented`}
-        ></Attention>
+          title={i18n.str`Cashout are disabled`}
+        >
+          <i18n.Translate>Cashout should be enable by configuration and the 
conversion rate should be initialized with fee, ratio and rounding 
mode.</i18n.Translate>
+        </Attention>
       );
     }
     default:
@@ -66,8 +68,10 @@ export function ReadyView({
         return (
           <Attention
             type="danger"
-            title={i18n.str`Cashout not implemented`}
-          ></Attention>
+            title={i18n.str`Cashout are disabled`}
+          >
+            <i18n.Translate>Cashout should be enable by configuration and the 
conversion rate should be initialized with fee, ratio and rounding 
mode.</i18n.Translate>
+          </Attention>
         );
       }
       default:
@@ -82,8 +86,8 @@ export function ReadyView({
         cur.creation_time.t_s === "never"
           ? ""
           : format(cur.creation_time.t_s * 1000, "dd/MM/yyyy", {
-              locale: dateLocale,
-            });
+            locale: dateLocale,
+          });
       if (!prev[d]) {
         prev[d] = [];
       }
@@ -141,8 +145,8 @@ export function ReadyView({
                       item.creation_time.t_s === "never"
                         ? ""
                         : format(item.creation_time.t_s * 1000, "HH:mm:ss", {
-                            locale: dateLocale,
-                          });
+                          locale: dateLocale,
+                        });
                     return (
                       <tr
                         key={idx}
diff --git a/packages/demobank-ui/src/context/config.ts 
b/packages/demobank-ui/src/context/config.ts
index 5d8a5c73f..1cabab51c 100644
--- a/packages/demobank-ui/src/context/config.ts
+++ b/packages/demobank-ui/src/context/config.ts
@@ -157,8 +157,8 @@ export class CacheAwareApi extends TalerCoreBankHttpClient {
   async deleteAccount(auth: UserAndToken, cid?: string | undefined) {
     const resp = await super.deleteAccount(auth, cid);
     if (resp.type === "ok") {
-      revalidatePublicAccounts();
-      revalidateBusinessAccounts();
+      await revalidatePublicAccounts();
+      await revalidateBusinessAccounts();
     }
     return resp;
   }
@@ -168,8 +168,11 @@ export class CacheAwareApi extends TalerCoreBankHttpClient 
{
   ) {
     const resp = await super.createAccount(auth, body);
     if (resp.type === "ok") {
-      revalidatePublicAccounts();
-      revalidateBusinessAccounts();
+      // admin balance change on new account
+      await revalidateAccountDetails();
+      await revalidateTransactions();
+      await revalidatePublicAccounts();
+      await revalidateBusinessAccounts();
     }
     return resp;
   }
@@ -180,7 +183,7 @@ export class CacheAwareApi extends TalerCoreBankHttpClient {
   ) {
     const resp = await super.updateAccount(auth, body, cid);
     if (resp.type === "ok") {
-      revalidateAccountDetails();
+      await revalidateAccountDetails();
     }
     return resp;
   }
@@ -191,8 +194,8 @@ export class CacheAwareApi extends TalerCoreBankHttpClient {
   ) {
     const resp = await super.createTransaction(auth, body, cid);
     if (resp.type === "ok") {
-      revalidateAccountDetails();
-      revalidateTransactions();
+      await revalidateAccountDetails();
+      await revalidateTransactions();
     }
     return resp;
   }
@@ -203,8 +206,8 @@ export class CacheAwareApi extends TalerCoreBankHttpClient {
   ) {
     const resp = await super.confirmWithdrawalById(auth, wid, cid);
     if (resp.type === "ok") {
-      revalidateAccountDetails();
-      revalidateTransactions();
+      await revalidateAccountDetails();
+      await revalidateTransactions();
     }
     return resp;
   }
@@ -215,8 +218,8 @@ export class CacheAwareApi extends TalerCoreBankHttpClient {
   ) {
     const resp = await super.createCashout(auth, body, cid);
     if (resp.type === "ok") {
-      revalidateAccountDetails();
-      revalidateCashouts();
+      await revalidateAccountDetails();
+      await revalidateCashouts();
     }
     return resp;
   }
diff --git a/packages/demobank-ui/src/hooks/access.ts 
b/packages/demobank-ui/src/hooks/access.ts
index 85d030245..e07a3d1b1 100644
--- a/packages/demobank-ui/src/hooks/access.ts
+++ b/packages/demobank-ui/src/hooks/access.ts
@@ -35,7 +35,7 @@ export interface InstanceTemplateFilter {
 }
 
 export function revalidateAccountDetails() {
-  mutate(
+  return mutate(
     (key) => Array.isArray(key) && key[key.length - 1] === "getAccount",
     undefined,
     { revalidate: true },
@@ -62,9 +62,7 @@ export function useAccountDetails(account: string) {
 }
 
 export function revalidateWithdrawalDetails() {
-  mutate(
-    (key) => Array.isArray(key) && key[key.length - 1] === "getWithdrawalById",
-  );
+  return mutate((key) => Array.isArray(key) && key[key.length - 1] === 
"getWithdrawalById", undefined, { revalidate: true });
 }
 
 export function useWithdrawalDetails(wid: string) {
@@ -111,8 +109,8 @@ export function useWithdrawalDetails(wid: string) {
 }
 
 export function revalidateTransactionDetails() {
-  mutate(
-    (key) => Array.isArray(key) && key[key.length - 1] === 
"getTransactionById",
+  return mutate(
+    (key) => Array.isArray(key) && key[key.length - 1] === 
"getTransactionById", undefined, { revalidate: true }
   );
 }
 export function useTransactionDetails(account: string, tid: number) {
@@ -150,8 +148,8 @@ export function useTransactionDetails(account: string, tid: 
number) {
 }
 
 export function revalidatePublicAccounts() {
-  mutate(
-    (key) => Array.isArray(key) && key[key.length - 1] === "getPublicAccounts",
+  return mutate(
+    (key) => Array.isArray(key) && key[key.length - 1] === 
"getPublicAccounts", undefined, { revalidate: true }
   );
 }
 export function usePublicAccounts(
@@ -221,7 +219,7 @@ export function usePublicAccounts(
 }
 
 export function revalidateTransactions() {
-  mutate(
+  return mutate(
     (key) => Array.isArray(key) && key[key.length - 1] === "getTransactions",
     undefined,
     { revalidate: true },
diff --git a/packages/demobank-ui/src/hooks/circuit.ts 
b/packages/demobank-ui/src/hooks/circuit.ts
index 2b0781465..88ca7b947 100644
--- a/packages/demobank-ui/src/hooks/circuit.ts
+++ b/packages/demobank-ui/src/hooks/circuit.ts
@@ -52,7 +52,7 @@ type CashoutEstimators = {
 };
 
 export function revalidateConversionInfo() {
-  mutate(
+  return mutate(
     (key) =>
       Array.isArray(key) && key[key.length - 1] === "getConversionInfoAPI",
   );
@@ -130,7 +130,7 @@ export function useEstimator(): CashoutEstimators {
 }
 
 export function revalidateBusinessAccounts() {
-  mutate((key) => Array.isArray(key) && key[key.length - 1] === "getAccounts");
+  return mutate((key) => Array.isArray(key) && key[key.length - 1] === 
"getAccounts", undefined, { revalidate: true });
 }
 export function useBusinessAccounts() {
   const { state: credentials } = useBackendState();
@@ -199,9 +199,9 @@ function notUndefined(c: CashoutWithId | undefined): c is 
CashoutWithId {
   return c !== undefined;
 }
 export function revalidateOnePendingCashouts() {
-  mutate(
+  return mutate(
     (key) =>
-      Array.isArray(key) && key[key.length - 1] === "useOnePendingCashouts",
+      Array.isArray(key) && key[key.length - 1] === "useOnePendingCashouts", 
undefined, { revalidate: true }
   );
 }
 export function useOnePendingCashouts(account: string) {
@@ -215,13 +215,11 @@ export function useOnePendingCashouts(account: string) {
     if (list.type !== "ok") {
       return list;
     }
-    const pendingCashout = list.body.cashouts.find(
-      (c) => c.status === "pending",
-    );
+    const pendingCashout = list.body.cashouts.length > 0 ? 
list.body.cashouts[0] : undefined;
     if (!pendingCashout) return opFixedSuccess(list.httpResp, undefined);
     const cashoutInfo = await api.getCashoutById(
       { username, token },
-      pendingCashout?.cashout_id,
+      pendingCashout.cashout_id,
     );
     if (cashoutInfo.type !== "ok") {
       return cashoutInfo;
@@ -261,7 +259,7 @@ export function useOnePendingCashouts(account: string) {
 }
 
 export function revalidateCashouts() {
-  mutate((key) => Array.isArray(key) && key[key.length - 1] === "useCashouts");
+  return mutate((key) => Array.isArray(key) && key[key.length - 1] === 
"useCashouts");
 }
 export function useCashouts(account: string) {
   const { state: credentials } = useBackendState();
@@ -312,8 +310,8 @@ export function useCashouts(account: string) {
 }
 
 export function revalidateCashoutDetails() {
-  mutate(
-    (key) => Array.isArray(key) && key[key.length - 1] === "getCashoutById",
+  return mutate(
+    (key) => Array.isArray(key) && key[key.length - 1] === "getCashoutById", 
undefined, { revalidate: true }
   );
 }
 export function useCashoutDetails(cashoutId: number | undefined) {
@@ -361,8 +359,8 @@ export type LastMonitor = {
   previous: TalerCoreBankResultByMethod<"getMonitor">;
 };
 export function revalidateLastMonitorInfo() {
-  mutate(
-    (key) => Array.isArray(key) && key[key.length - 1] === 
"useLastMonitorInfo",
+  return mutate(
+    (key) => Array.isArray(key) && key[key.length - 1] === 
"useLastMonitorInfo", undefined, { revalidate: true }
   );
 }
 export function useLastMonitorInfo(
diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx 
b/packages/demobank-ui/src/pages/PaymentOptions.tsx
index 39b31a094..a508845e1 100644
--- a/packages/demobank-ui/src/pages/PaymentOptions.tsx
+++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx
@@ -33,18 +33,19 @@ function ShowOperationPendingTag({
 }): VNode {
   const { i18n } = useTranslationContext();
   const result = useWithdrawalDetails(woid);
+  const loading = !result
   const error =
-    !result || result instanceof TalerError || result.type === "fail";
-  const completed =
-    !error &&
-    (result.body.status === "aborted" || result.body.status === "confirmed");
+    !loading && (result instanceof TalerError || result.type === "fail");
+  const pending =
+    !loading && !error &&
+    (result.body.status === "pending" || result.body.status === "selected");
   useEffect(() => {
-    if (completed && onOperationAlreadyCompleted) {
+    if (!loading && !pending && onOperationAlreadyCompleted) {
       onOperationAlreadyCompleted();
     }
-  }, [completed]);
+  }, [pending]);
 
-  if (error || completed) {
+  if (error || !pending) {
     return <Fragment />;
   }
 
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx 
b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
index 3643e1f6b..54ceb81a9 100644
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -23,28 +23,30 @@ import {
   FRAC_SEPARATOR,
   HttpStatusCode,
   PaytoString,
+  PaytoUri,
   TalerErrorCode,
   TranslatedString,
   assertUnreachable,
   buildPayto,
   parsePaytoUri,
-  stringifyPaytoUri,
+  stringifyPaytoUri
 } from "@gnu-taler/taler-util";
 import {
+  InternationalizationAPI,
   LocalNotificationBanner,
   ShowInputErrorLabel,
   notifyInfo,
   useLocalNotification,
   useTranslationContext,
 } from "@gnu-taler/web-util/browser";
-import { Ref, VNode, h } from "preact";
+import { ComponentChildren, Fragment, Ref, VNode, h } from "preact";
 import { useState } from "preact/hooks";
 import { mutate } from "swr";
 import { useBankCoreApiContext } from "../context/config.js";
 import { useBackendState } from "../hooks/backend.js";
 import { useBankState } from "../hooks/bank-state.js";
 import { RouteDefinition } from "../route.js";
-import { undefinedIfEmpty, validateIBAN } from "../utils.js";
+import { undefinedIfEmpty, validateIBAN, validateTalerBank } from 
"../utils.js";
 
 export function PaytoWireTransferForm({
   focus,
@@ -65,11 +67,11 @@ export function PaytoWireTransferForm({
 }): VNode {
   const [isRawPayto, setIsRawPayto] = useState(false);
   const { state: credentials } = useBackendState();
-  const { api } = useBankCoreApiContext();
+  const { api, config, url } = useBankCoreApiContext();
 
   const sendingToFixedAccount = toAccount !== undefined;
-  // FIXME: support other destination that just IBAN
-  const [iban, setIban] = useState<string | undefined>(toAccount);
+
+  const [account, setAccount] = useState<string | undefined>(toAccount);
   const [subject, setSubject] = useState<string | undefined>();
   const [amount, setAmount] = useState<string | undefined>();
   const [, updateBankState] = useBankState();
@@ -78,49 +80,35 @@ export function PaytoWireTransferForm({
     undefined,
   );
   const { i18n } = useTranslationContext();
-  const ibanRegex = "^[A-Z][A-Z][0-9]+$";
 
   const trimmedAmountStr = amount?.trim();
   const parsedAmount = Amounts.parse(`${limit.currency}:${trimmedAmountStr}`);
-  const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
   const [notification, notify, handleError] = useLocalNotification();
 
+  const paytoType = config.wire_type === "X_TALER_BANK" ? "x-taler-bank" as 
const : "iban" as const;
+
   const errorsWire = undefinedIfEmpty({
-    iban: !iban
+    account: !account
       ? i18n.str`Required`
-      : !IBAN_REGEX.test(iban)
-        ? i18n.str`IBAN should have just uppercased letters and numbers`
-        : validateIBAN(iban, i18n),
-    subject: !subject ? i18n.str`Required` : undefined,
+      : paytoType === "iban" ? validateIBAN(account, i18n) :
+        paytoType === "x-taler-bank" ? validateTalerBank(account, i18n) :
+          undefined,
+    subject: !subject ? i18n.str`Required` : validateSubject(subject, i18n),
     amount: !trimmedAmountStr
       ? i18n.str`Required`
       : !parsedAmount
         ? i18n.str`Not valid`
-        : Amounts.isZero(parsedAmount)
-          ? i18n.str`Should be greater than 0`
-          : Amounts.cmp(limit, parsedAmount) === -1
-            ? i18n.str`Balance is not enough`
-            : undefined,
+        : validateAmount(parsedAmount, limit, i18n),
   });
 
   const parsed = !rawPaytoInput ? undefined : parsePaytoUri(rawPaytoInput);
 
+
   const errorsPayto = undefinedIfEmpty({
     rawPaytoInput: !rawPaytoInput
       ? i18n.str`Required`
-      : !parsed
-        ? i18n.str`Does not follow the pattern`
-        : !parsed.isKnown || parsed.targetType !== "iban"
-          ? i18n.str`Only "IBAN" target are supported`
-          : !parsed.params.amount
-            ? i18n.str`Use the "amount" parameter to specify the amount to be 
transferred`
-            : Amounts.parse(parsed.params.amount) === undefined
-              ? i18n.str`The amount is not valid`
-              : !parsed.params.message
-                ? i18n.str`Use the "message" parameter to specify a reference 
text for the transfer`
-                : !IBAN_REGEX.test(parsed.iban)
-                  ? i18n.str`IBAN should have just uppercased letters and 
numbers`
-                  : validateIBAN(parsed.iban, i18n),
+      : !parsed ? i18n.str`Does not follow the pattern`
+        : validateRawPayto(parsed, limit, url.host, i18n, paytoType),
   });
 
   async function doSend() {
@@ -128,18 +116,30 @@ export function PaytoWireTransferForm({
     let sendingAmount: AmountString | undefined;
 
     if (credentials.status !== "loggedIn") return;
-    if (rawPaytoInput) {
-      const p = parsePaytoUri(rawPaytoInput);
+    if (isRawPayto) {
+      const p = parsePaytoUri(rawPaytoInput!);
       if (!p) return;
       sendingAmount = p.params.amount as AmountString;
       delete p.params.amount;
       // if this payto is valid then it already have message
       payto_uri = stringifyPaytoUri(p);
     } else {
-      if (!iban || !subject) return;
-      const ibanPayto = buildPayto("iban", iban, undefined);
-      ibanPayto.params.message = encodeURIComponent(subject);
-      payto_uri = stringifyPaytoUri(ibanPayto);
+      if (!account || !subject) return;
+      let payto;
+      switch (paytoType) {
+        case "x-taler-bank": {
+          payto = buildPayto("x-taler-bank", url.host, account);
+          break;
+        }
+        case "iban": {
+          payto = buildPayto("iban", account, undefined);
+          break;
+        }
+        default: assertUnreachable(paytoType)
+      }
+
+      payto.params.message = encodeURIComponent(subject);
+      payto_uri = stringifyPaytoUri(payto);
       sendingAmount = `${limit.currency}:${trimmedAmountStr}` as AmountString;
     }
     const puri = payto_uri;
@@ -212,7 +212,7 @@ export function PaytoWireTransferForm({
       notifyInfo(i18n.str`Wire transfer created!`);
       onSuccess();
       setAmount(undefined);
-      setIban(undefined);
+      setAccount(undefined);
       setSubject(undefined);
       rawPaytoInputSetter(undefined);
     });
@@ -243,13 +243,24 @@ export function PaytoWireTransferForm({
                 aria-labelledby="project-type-0-label"
                 aria-describedby="project-type-0-description-0 
project-type-0-description-1"
                 onChange={() => {
-                  if (
-                    parsed &&
-                    parsed.isKnown &&
-                    parsed.targetType === "iban"
-                  ) {
-                    setIban(parsed.iban);
-                    const amountStr = parsed.params["amount"];
+                  if (parsed && parsed.isKnown) {
+                    switch (parsed.targetType) {
+                      case "iban": {
+                        setAccount(parsed.iban);
+                        break;
+                      }
+                      case "x-taler-bank": {
+                        setAccount(parsed.account);
+                        break;
+                      }
+                      case "bitcoin": {
+                        break;
+                      }
+                      default: {
+                        assertUnreachable(parsed)
+                      }
+                    }
+                    const amountStr = parsed.params["amount"] ?? 
`${config.currency}:0`;
                     if (amountStr) {
                       const amount = Amounts.parse(parsed.params["amount"]);
                       if (amount) {
@@ -290,14 +301,32 @@ export function PaytoWireTransferForm({
                   aria-labelledby="project-type-1-label"
                   aria-describedby="project-type-1-description-0 
project-type-1-description-1"
                   onChange={() => {
-                    if (iban) {
-                      const payto = buildPayto("iban", iban, undefined);
-                      if (parsedAmount) {
-                        payto.params["amount"] =
-                          Amounts.stringify(parsedAmount);
-                      }
-                      if (subject) {
-                        payto.params["message"] = subject;
+                    if (account) {
+                      let payto;
+                      switch (paytoType) {
+                        case "x-taler-bank": {
+                          payto = buildPayto("x-taler-bank", url.host, 
account);
+                          if (parsedAmount) {
+                            payto.params["amount"] =
+                              Amounts.stringify(parsedAmount);
+                          }
+                          if (subject) {
+                            payto.params["message"] = subject;
+                          }
+                          break;
+                        }
+                        case "iban": {
+                          payto = buildPayto("iban", account, undefined);
+                          if (parsedAmount) {
+                            payto.params["amount"] =
+                              Amounts.stringify(parsedAmount);
+                          }
+                          if (subject) {
+                            payto.params["message"] = subject;
+                          }
+                          break;
+                        }
+                        default: assertUnreachable(paytoType)
                       }
                       rawPaytoInputSetter(stringifyPaytoUri(payto));
                     }
@@ -328,39 +357,37 @@ export function PaytoWireTransferForm({
         <div class="p-4 sm:p-8">
           {!isRawPayto ? (
             <div class="grid max-w-xs grid-cols-1 gap-x-6 gap-y-8 
sm:grid-cols-6">
-              <div class="sm:col-span-5">
-                <label
-                  for="iban"
-                  class="block text-sm font-medium leading-6 text-gray-900"
-                >{i18n.str`Recipient`}</label>
-                <div class="mt-2">
-                  <input
-                    ref={focus ? doAutoFocus : undefined}
-                    type="text"
-                    class="block w-full disabled:bg-gray-200 rounded-md 
border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 
placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 
sm:text-sm sm:leading-6"
-                    name="iban"
-                    id="iban"
-                    disabled={sendingToFixedAccount}
-                    value={iban ?? ""}
-                    placeholder="CC0123456789"
-                    autocomplete="off"
-                    required
-                    pattern={ibanRegex}
-                    onInput={(e): void => {
-                      setIban(e.currentTarget.value.toUpperCase());
-                    }}
-                  />
-                  <ShowInputErrorLabel
-                    message={errorsWire?.iban}
-                    isDirty={iban !== undefined}
-                  />
-                </div>
-                <p class="mt-2 text-sm text-gray-500">
-                  <i18n.Translate>
-                    IBAN of the recipient's account
-                  </i18n.Translate>
-                </p>
-              </div>
+              {(() => {
+                switch (paytoType) {
+                  case "x-taler-bank": {
+                    return <TextField
+                      id="x-taler-bank"
+                      label={i18n.str`Recipient`}
+                      help={i18n.str`Id of the recipient's account`}
+                      error={errorsWire?.account}
+                      onChange={setAccount}
+                      value={account}
+                      placeholder={i18n.str`username`}
+                      focus={focus}
+                      disabled={sendingToFixedAccount}
+                    />
+                  }
+                  case "iban": {
+                    return <TextField
+                      id="iban"
+                      label={i18n.str`Recipient`}
+                      help={i18n.str`IBAN of the recipient's account`}
+                      placeholder={"CC0123456789" as TranslatedString}
+                      error={errorsWire?.account}
+                      onChange={(v) => setAccount(v.toUpperCase())}
+                      value={account}
+                      focus={focus}
+                      disabled={sendingToFixedAccount}
+                    />
+                  }
+                  default: assertUnreachable(paytoType)
+                }
+              })()}
 
               <div class="sm:col-span-5">
                 <label
@@ -434,7 +461,13 @@ export function PaytoWireTransferForm({
                     value={rawPaytoInput ?? ""}
                     required
                     title={i18n.str`Uniform resource identifier of the target 
account`}
-                    
placeholder={i18n.str`payto://iban/[receiver-iban]?message=[subject]&amount=[${limit.currency}:X.Y]`}
+
+                    placeholder={((): TranslatedString => {
+                      switch (paytoType) {
+                        case "x-taler-bank": return 
i18n.str`payto://x-taler-bank/[bank-host]/[receiver-account]?message=[subject]&amount=[${limit.currency}:X.Y]`
+                        case "iban": return 
i18n.str`payto://iban/[receiver-iban]?message=[subject]&amount=[${limit.currency}:X.Y]`
+                      }
+                    })()}
                     onInput={(e): void => {
                       rawPaytoInputSetter(e.currentTarget.value);
                     }}
@@ -538,13 +571,13 @@ export function InputAmount(
             if (
               sep_pos !== -1 &&
               l - sep_pos - 1 >
-                config.currency_specification.num_fractional_input_digits
+              config.currency_specification.num_fractional_input_digits
             ) {
               e.currentTarget.value = e.currentTarget.value.substring(
                 0,
                 sep_pos +
-                  config.currency_specification.num_fractional_input_digits +
-                  1,
+                config.currency_specification.num_fractional_input_digits +
+                1,
               );
             }
             onChange(e.currentTarget.value);
@@ -587,3 +620,147 @@ export function RenderAmount({
     </span>
   );
 }
+
+
+function validateRawPayto(parsed: PaytoUri, limit: AmountJson, host: string, 
i18n: InternationalizationAPI, type: "iban" | "x-taler-bank"): TranslatedString 
| undefined {
+  if (!parsed.isKnown) {
+    return i18n.str`The target type is unknown, use "${type}"`
+  }
+  let result: TranslatedString | undefined;
+  switch (type) {
+    case "x-taler-bank": {
+      if (parsed.targetType !== "x-taler-bank") {
+        return i18n.str`Only "x-taler-bank" target are supported`
+      }
+
+      if (parsed.host !== host) {
+        return i18n.str`Only this host is allowed. Use "${host}"`
+      }
+
+      if (!parsed.account) {
+        return i18n.str`Missing account name`
+      }
+      const result = validateTalerBank(parsed.account, i18n)
+      if (result) return result
+      break;
+    }
+    case "iban": {
+      if (parsed.targetType !== "iban") {
+        return i18n.str`Only "IBAN" target are supported`
+      }
+      const result = validateIBAN(parsed.iban, i18n)
+      if (result) return result
+      break;
+    }
+    default: assertUnreachable(type)
+  }
+  if (!parsed.params.amount) {
+    return i18n.str`Missing "amount" parameter to specify the amount to be 
transferred`
+  }
+  const amount = Amounts.parse(parsed.params.amount)
+  if (!amount) {
+    return i18n.str`The "amount" parameter is not valid`
+  }
+  result = validateAmount(amount, limit, i18n)
+  if (result) return result;
+
+  if (!parsed.params.message) {
+    return i18n.str`Missing the "message" parameter to specify a reference 
text for the transfer`
+  }
+  const subject = parsed.params.message
+  result = validateSubject(subject, i18n)
+  if (result) return result;
+
+  return undefined
+}
+
+function validateAmount(amount: AmountJson, limit: AmountJson, i18n: 
InternationalizationAPI): TranslatedString | undefined {
+  if (amount.currency !== limit.currency) {
+    return i18n.str`The only currecy allowed is "${limit.currency}"`
+  }
+  if (Amounts.isZero(amount)) {
+    return i18n.str`Can't transfer zero amount`
+  }
+  if (Amounts.cmp(limit, amount) === -1) {
+    return i18n.str`Balance is not enough`
+  }
+  return undefined
+}
+
+function validateSubject(text: string, i18n: InternationalizationAPI): 
TranslatedString | undefined {
+  if (text.length < 2) {
+    return i18n.str`Use a longer subject`
+  }
+  return undefined
+}
+
+interface PaytoFieldProps {
+  id: string,
+  label: TranslatedString;
+  help?: TranslatedString;
+  placeholder?: TranslatedString;
+  error: string | undefined;
+  value: string | undefined;
+  rightIcons?: VNode;
+  onChange: (p: string) => void;
+  focus?: boolean;
+  disabled?: boolean;
+}
+
+function Wrapper({ withIcon, children }: { withIcon: boolean, children: 
ComponentChildren }): VNode {
+  if (withIcon) {
+    return <div class="flex justify-between">
+      {children}
+    </div>
+  }
+  return <Fragment>{children}</Fragment>
+}
+
+export function TextField({
+  id,
+  label,
+  help,
+  focus,
+  disabled,
+  onChange,
+  placeholder,
+  rightIcons,
+  value,
+  error,
+}: PaytoFieldProps): VNode {
+  return <div class="sm:col-span-5">
+    <label
+      for={id}
+      class="block text-sm font-medium leading-6 text-gray-900"
+    >{label}</label>
+    <div class="mt-2">
+      <Wrapper withIcon={rightIcons !== undefined}>
+        <input
+          ref={focus ? doAutoFocus : undefined}
+          type="text"
+          class="block w-full disabled:bg-gray-200 rounded-md border-0 py-1.5 
text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 
placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 
sm:text-sm sm:leading-6"
+          name={id}
+          id={id}
+          disabled={disabled}
+          value={value ?? ""}
+          placeholder={placeholder}
+          autocomplete="off"
+          required
+          onInput={(e): void => {
+            onChange(e.currentTarget.value);
+          }}
+        />
+        {rightIcons}
+      </Wrapper>
+      <ShowInputErrorLabel
+        message={error}
+        isDirty={value !== undefined}
+      />
+    </div>
+    {help &&
+      <p class="mt-2 text-sm text-gray-500">
+        {help}
+      </p>
+    }
+  </div>
+}
diff --git a/packages/demobank-ui/src/pages/SolveChallengePage.tsx 
b/packages/demobank-ui/src/pages/SolveChallengePage.tsx
index de0ba483f..61decf586 100644
--- a/packages/demobank-ui/src/pages/SolveChallengePage.tsx
+++ b/packages/demobank-ui/src/pages/SolveChallengePage.tsx
@@ -666,8 +666,10 @@ function ShowCashoutDetails({
         return (
           <Attention
             type="danger"
-            title={i18n.str`Cashout not implemented`}
-          ></Attention>
+            title={i18n.str`Cashout are disabled`}
+          >
+            <i18n.Translate>Cashout should be enable by configuration and the 
conversion rate should be initialized with fee, ratio and rounding 
mode.</i18n.Translate>
+          </Attention>
         );
       }
       default:
diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx 
b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
index 5d4a5c5db..3aba99cea 100644
--- a/packages/demobank-ui/src/pages/admin/AccountForm.tsx
+++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
@@ -39,11 +39,11 @@ import {
   TanChannel,
   undefinedIfEmpty,
   validateIBAN,
+  validateTalerBank,
 } from "../../utils.js";
-import { InputAmount, doAutoFocus } from "../PaytoWireTransferForm.js";
+import { InputAmount, TextField, doAutoFocus } from 
"../PaytoWireTransferForm.js";
 import { getRandomPassword } from "../rnd.js";
 
-const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
 const EMAIL_REGEX =
   
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
 const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/;
@@ -90,7 +90,7 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
   onChange: ChangeByPurposeType[PurposeType];
   purpose: PurposeType;
 }): VNode {
-  const { config, hints } = useBankCoreApiContext();
+  const { config, hints, url } = useBankCoreApiContext();
   const { i18n } = useTranslationContext();
   const { state: credentials } = useBackendState();
   const [form, setForm] = useState<AccountFormData>({});
@@ -99,6 +99,9 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
     ErrorMessageMappingFor<typeof defaultValue> | undefined
   >(undefined);
 
+  const paytoType = config.wire_type === "X_TALER_BANK" ? "x-taler-bank" as 
const : "iban" as const;
+  const cashoutPaytoType: typeof paytoType = "iban" as const;
+
   const defaultValue: AccountFormData = {
     debit_threshold: Amounts.stringifyValue(
       template?.debit_threshold ?? config.default_debit_threshold,
@@ -107,8 +110,8 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
     isPublic: template?.is_public,
     name: template?.name ?? "",
     cashout_payto_uri:
-      stringifyIbanPayto(template?.cashout_payto_uri) ?? ("" as PaytoString),
-    payto_uri: stringifyIbanPayto(template?.payto_uri) ?? ("" as PaytoString),
+      getAccountId(cashoutPaytoType, template?.cashout_payto_uri) ?? ("" as 
PaytoString),
+    payto_uri: getAccountId(paytoType, template?.payto_uri) ?? ("" as 
PaytoString),
     email: template?.contact_data?.email ?? "",
     phone: template?.contact_data?.phone ?? "",
     username: username ?? "",
@@ -117,10 +120,6 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
 
   const OLD_CASHOUT_API = hints.indexOf(VersionHint.CASHOUT_BEFORE_2FA) !== -1;
 
-  const showingCurrentUserInfo =
-    credentials.status !== "loggedIn"
-      ? false
-      : username === credentials.username;
   const userIsAdmin =
     credentials.status !== "loggedIn" ? false : 
credentials.isUserAdministrator;
 
@@ -131,7 +130,6 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
 
   const isCashoutEnabled = config.allow_conversion;
   const editableCashout =
-    showingCurrentUserInfo &&
     (purpose === "create" ||
       (purpose === "update" &&
         (config.allow_edit_cashout_payto_uri || userIsAdmin)));
@@ -143,13 +141,6 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
   const hasEmail = !!defaultValue.email || !!form.email;
 
   function updateForm(newForm: typeof defaultValue): void {
-    const cashoutParsed = !newForm.cashout_payto_uri
-      ? undefined
-      : buildPayto("iban", newForm.cashout_payto_uri, undefined);
-
-    const internalParsed = !newForm.payto_uri
-      ? undefined
-      : buildPayto("iban", newForm.payto_uri, undefined);
 
     const trimmedAmountStr = newForm.debit_threshold?.trim();
     const parsedAmount = Amounts.parse(
@@ -163,24 +154,20 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
         ? undefined
         : !editableCashout
           ? undefined
-          : !cashoutParsed
-            ? i18n.str`Doesn't have the pattern of an IBAN number`
-            : !cashoutParsed.isKnown || cashoutParsed.targetType !== "iban"
-              ? i18n.str`Only "IBAN" target are supported`
-              : !IBAN_REGEX.test(cashoutParsed.iban)
-                ? i18n.str`IBAN should have just uppercased letters and 
numbers`
-                : validateIBAN(cashoutParsed.iban, i18n),
+          : !newForm.cashout_payto_uri ? undefined
+            : cashoutPaytoType === "iban" ? 
validateIBAN(newForm.cashout_payto_uri, i18n) :
+              cashoutPaytoType === "x-taler-bank" ? 
validateTalerBank(newForm.cashout_payto_uri, i18n) :
+                undefined,
+
       payto_uri: !newForm.payto_uri
         ? undefined
         : !editableAccount
           ? undefined
-          : !internalParsed
-            ? i18n.str`Doesn't have the pattern of an IBAN number`
-            : !internalParsed.isKnown || internalParsed.targetType !== "iban"
-              ? i18n.str`Only "IBAN" target are supported`
-              : !IBAN_REGEX.test(internalParsed.iban)
-                ? i18n.str`IBAN should have just uppercased letters and 
numbers`
-                : validateIBAN(internalParsed.iban, i18n),
+          : !newForm.payto_uri ? undefined
+            : paytoType === "iban" ? validateIBAN(newForm.payto_uri, i18n) :
+              paytoType === "x-taler-bank" ? 
validateTalerBank(newForm.payto_uri, i18n) :
+                undefined,
+
       email: !newForm.email
         ? undefined
         : !EMAIL_REGEX.test(newForm.email)
@@ -219,14 +206,31 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
     if (errors) {
       onChange(undefined);
     } else {
-      const cashout = !newForm.cashout_payto_uri
-        ? undefined
-        : buildPayto("iban", newForm.cashout_payto_uri, undefined);
+      let cashout;
+      if (newForm.cashout_payto_uri) switch (cashoutPaytoType) {
+        case "x-taler-bank": {
+          cashout = buildPayto("x-taler-bank", url.host, 
newForm.cashout_payto_uri);
+          break;
+        }
+        case "iban": {
+          cashout = buildPayto("iban", newForm.cashout_payto_uri, undefined);
+          break;
+        }
+        default: assertUnreachable(cashoutPaytoType)
+      }
       const cashoutURI = !cashout ? undefined : stringifyPaytoUri(cashout);
-
-      const internal = !newForm.payto_uri
-        ? undefined
-        : buildPayto("iban", newForm.payto_uri, undefined);
+      let internal;
+      if (newForm.payto_uri) switch (paytoType) {
+        case "x-taler-bank": {
+          internal = buildPayto("x-taler-bank", url.host, newForm.payto_uri);
+          break;
+        }
+        case "iban": {
+          internal = buildPayto("iban", newForm.payto_uri, undefined);
+          break;
+        }
+        default: assertUnreachable(paytoType)
+      }
       const internalURI = !internal ? undefined : stringifyPaytoUri(internal);
 
       const threshold = !parsedAmount
@@ -328,7 +332,7 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
               />
             </div>
             <p class="mt-2 text-sm text-gray-500">
-              <i18n.Translate>Account identification</i18n.Translate>
+              <i18n.Translate>Account id for authentication</i18n.Translate>
             </p>
           </div>
 
@@ -366,22 +370,26 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
             </p>
           </div>
 
-          <PaytoField
-            type="iban"
-            name="internal-account"
-            label={i18n.str`Internal IBAN`}
+          <TextField
+            id="internal-account"
+            label={i18n.str`Internal account`}
             help={
               purpose === "create"
-                ? i18n.str`If empty a random account number will be assigned`
-                : i18n.str`Account number for bank transfers`
+                ? i18n.str`If empty a random account id will be assigned`
+                : i18n.str`Share this id to receive bank transfers`
             }
-            value={(form.payto_uri ?? defaultValue.payto_uri) as PaytoString}
-            disabled={!editableAccount}
+
             error={errors?.payto_uri}
             onChange={(e) => {
               form.payto_uri = e as PaytoString;
               updateForm(structuredClone(form));
             }}
+            rightIcons={<CopyButton
+              class="p-2 rounded-full  text-black shadow-sm  
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+              getContent={() => form.payto_uri ?? defaultValue.payto_uri ?? ""}
+            />}
+            value={(form.payto_uri ?? defaultValue.payto_uri) as PaytoString}
+            disabled={!editableAccount}
           />
 
           <div class="sm:col-span-5">
@@ -411,6 +419,9 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
                 isDirty={form.email !== undefined}
               />
             </div>
+            <p class="mt-2 text-sm text-gray-500">
+              <i18n.Translate>To be used when second factor authentication is 
enabled</i18n.Translate>
+            </p>
           </div>
 
           <div class="sm:col-span-5">
@@ -440,102 +451,26 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
                 isDirty={form.phone !== undefined}
               />
             </div>
+            <p class="mt-2 text-sm text-gray-500">
+              <i18n.Translate>To be used when second factor authentication is 
enabled</i18n.Translate>
+            </p>
           </div>
 
-          {showingCurrentUserInfo && isCashoutEnabled && (
-            <PaytoField
-              type="iban"
-              name="cashout-account"
-              label={i18n.str`Cashout IBAN`}
+          {isCashoutEnabled && (
+            <TextField
+              id="cashout-account"
+              label={i18n.str`Cashout account`}
               help={i18n.str`External account number where the money is going 
to be sent when doing cashouts`}
-              value={
-                (form.cashout_payto_uri ??
-                  defaultValue.cashout_payto_uri) as PaytoString
-              }
-              disabled={!editableCashout}
               error={errors?.cashout_payto_uri}
               onChange={(e) => {
                 form.cashout_payto_uri = e as PaytoString;
                 updateForm(structuredClone(form));
               }}
+              value={(form.cashout_payto_uri ?? 
defaultValue.cashout_payto_uri) as PaytoString}
+              disabled={!editableCashout}
             />
           )}
 
-          <div class="sm:col-span-5">
-            <label
-              for="debit"
-              class="block text-sm font-medium leading-6 text-gray-900"
-            >{i18n.str`Max debt`}</label>
-            <InputAmount
-              name="debit"
-              left
-              currency={config.currency}
-              value={form.debit_threshold ?? defaultValue.debit_threshold}
-              onChange={
-                !editableThreshold
-                  ? undefined
-                  : (e) => {
-                    form.debit_threshold = e as AmountString;
-                    updateForm(structuredClone(form));
-                  }
-              }
-            />
-            <ShowInputErrorLabel
-              message={
-                errors?.debit_threshold
-                  ? String(errors?.debit_threshold)
-                  : undefined
-              }
-              isDirty={form.debit_threshold !== undefined}
-            />
-            <p class="mt-2 text-sm text-gray-500">
-              <i18n.Translate>
-                How much is user able to transfer after zero balance
-              </i18n.Translate>
-            </p>
-          </div>
-
-          {purpose !== "create" || !userIsAdmin ? undefined : (
-            <div class="sm:col-span-5">
-              <div class="flex items-center justify-between">
-                <span class="flex flex-grow flex-col">
-                  <span
-                    class="text-sm text-black font-medium leading-6 "
-                    id="availability-label"
-                  >
-                    <i18n.Translate>Is this a payment 
provider?</i18n.Translate>
-                  </span>
-                </span>
-                <button
-                  type="button"
-                  data-enabled={
-                    form.isExchange ?? defaultValue.isExchange
-                      ? "true"
-                      : "false"
-                  }
-                  class="bg-indigo-600 data-[enabled=false]:bg-gray-200 
relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full 
border-2 border-transparent transition-colors duration-200 ease-in-out 
focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
-                  role="switch"
-                  aria-checked="false"
-                  aria-labelledby="availability-label"
-                  aria-describedby="availability-description"
-                  onClick={() => {
-                    form.isExchange = !form.isExchange;
-                    updateForm(structuredClone(form));
-                  }}
-                >
-                  <span
-                    aria-hidden="true"
-                    data-enabled={
-                      form.isExchange ?? defaultValue.isExchange
-                        ? "true"
-                        : "false"
-                    }
-                    class="translate-x-5 data-[enabled=false]:translate-x-0 
pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow 
ring-0 transition duration-200 ease-in-out"
-                  ></span>
-                </button>
-              </div>
-            </div>
-          )}
           {/* channel, not shown if old cashout api */}
           {OLD_CASHOUT_API ||
             config.supported_tan_channels.length === 0 ? undefined : (
@@ -584,7 +519,7 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
                           </span>
                           {purpose !== "show" &&
                             !hasEmail &&
-                            i18n.str`Add a email in your profile to enable 
this option`}
+                            i18n.str`Add an email in your profile to enable 
this option`}
                         </span>
                       </span>
                       <svg
@@ -668,6 +603,38 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
             </div>
           )}
 
+          <div class="sm:col-span-5">
+            <label
+              for="debit"
+              class="block text-sm font-medium leading-6 text-gray-900"
+            >{i18n.str`Max debt`}</label>
+            <InputAmount
+              name="debit"
+              left
+              currency={config.currency}
+              value={form.debit_threshold ?? defaultValue.debit_threshold}
+              onChange={
+                !editableThreshold
+                  ? undefined
+                  : (e) => {
+                    form.debit_threshold = e as AmountString;
+                    updateForm(structuredClone(form));
+                  }
+              }
+            />
+            <ShowInputErrorLabel
+              message={
+                errors?.debit_threshold
+                  ? String(errors?.debit_threshold)
+                  : undefined
+              }
+              isDirty={form.debit_threshold !== undefined}
+            />
+            <p class="mt-2 text-sm text-gray-500">
+              <i18n.Translate>How much the balance can go below 
zero.</i18n.Translate>
+            </p>
+          </div>
+
           <div class="sm:col-span-5">
             <div class="flex items-center justify-between">
               <span class="flex flex-grow flex-col">
@@ -703,11 +670,51 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
               </button>
             </div>
             <p class="mt-2 text-sm text-gray-500">
-              <i18n.Translate>
-                Public accounts have their balance publicly accessible
-              </i18n.Translate>
+              <i18n.Translate>Public accounts have their balance publicly 
accessible</i18n.Translate>
             </p>
           </div>
+
+          {purpose !== "create" || !userIsAdmin ? undefined : (
+            <div class="sm:col-span-5">
+              <div class="flex items-center justify-between">
+                <span class="flex flex-grow flex-col">
+                  <span
+                    class="text-sm text-black font-medium leading-6 "
+                    id="availability-label"
+                  >
+                    <i18n.Translate>Is this account a payment 
provider?</i18n.Translate>
+                  </span>
+                </span>
+                <button
+                  type="button"
+                  data-enabled={
+                    form.isExchange ?? defaultValue.isExchange
+                      ? "true"
+                      : "false"
+                  }
+                  class="bg-indigo-600 data-[enabled=false]:bg-gray-200 
relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full 
border-2 border-transparent transition-colors duration-200 ease-in-out 
focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+                  role="switch"
+                  aria-checked="false"
+                  aria-labelledby="availability-label"
+                  aria-describedby="availability-description"
+                  onClick={() => {
+                    form.isExchange = !form.isExchange;
+                    updateForm(structuredClone(form));
+                  }}
+                >
+                  <span
+                    aria-hidden="true"
+                    data-enabled={
+                      form.isExchange ?? defaultValue.isExchange
+                        ? "true"
+                        : "false"
+                    }
+                    class="translate-x-5 data-[enabled=false]:translate-x-0 
pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow 
ring-0 transition duration-200 ease-in-out"
+                  ></span>
+                </button>
+              </div>
+            </div>
+          )}
         </div>
       </div>
       {children}
@@ -715,13 +722,14 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
   );
 }
 
-function stringifyIbanPayto(s: PaytoString | undefined): string | undefined {
+function getAccountId(type: "iban" | "x-taler-bank", s: PaytoString | 
undefined): string | undefined {
   if (s === undefined) return undefined;
   const p = parsePaytoUri(s);
   if (p === undefined) return undefined;
-  if (!p.isKnown) return undefined;
-  if (p.targetType !== "iban") return undefined;
-  return p.iban;
+  if (!p.isKnown) return "<unkown>";
+  if (type === "iban" && p.targetType === "iban") return p.iban;
+  if (type === "x-taler-bank" && p.targetType === "x-taler-bank") return 
p.account;
+  return "<unsupported>";
 }
 
 {
@@ -762,126 +770,128 @@ function stringifyIbanPayto(s: PaytoString | 
undefined): string | undefined {
           </div> */
 }
 
-function PaytoField({
-  name,
-  label,
-  help,
-  type,
-  value,
-  disabled,
-  onChange,
-  error,
-}: {
-  error: TranslatedString | undefined;
-  name: string;
-  label: TranslatedString;
-  help: TranslatedString;
-  onChange: (s: string) => void;
-  type: "iban" | "x-taler-bank" | "bitcoin";
-  disabled?: boolean;
-  value: string | undefined;
-}): VNode {
-  if (type === "iban") {
-    return (
-      <div class="sm:col-span-5">
-        <label
-          class="block text-sm font-medium leading-6 text-gray-900"
-          for={name}
-        >
-          {label}
-        </label>
-        <div class="mt-2">
-          <div class="flex justify-between">
-            <input
-              type="text"
-              class="mr-4 w-full block-inline  disabled:bg-gray-100 rounded-md 
border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 
data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 
focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
-              name={name}
-              id={name}
-              disabled={disabled}
-              value={value ?? ""}
-              onChange={(e) => {
-                onChange(e.currentTarget.value);
-              }}
-            />
-            <CopyButton
-              class="p-2 rounded-full  text-black shadow-sm  
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
-              getContent={() => value ?? ""}
-            />
-          </div>
-          <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
-        </div>
-        <p class="mt-2 text-sm text-gray-500">{help}</p>
-      </div>
-    );
-  }
-  if (type === "x-taler-bank") {
-    return (
-      <div class="sm:col-span-5">
-        <label
-          class="block text-sm font-medium leading-6 text-gray-900"
-          for={name}
-        >
-          {label}
-        </label>
-        <div class="mt-2">
-          <div class="flex justify-between">
-            <input
-              type="text"
-              class="mr-4 w-full block-inline  disabled:bg-gray-100 rounded-md 
border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 
data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 
focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
-              name={name}
-              id={name}
-              disabled={disabled}
-              value={value ?? ""}
-            />
-            <CopyButton
-              class="p-2 rounded-full  text-black shadow-sm  
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
-              getContent={() => value ?? ""}
-            />
-          </div>
-          <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
-        </div>
-        <p class="mt-2 text-sm text-gray-500">
-          {/* <i18n.Translate>internal account id</i18n.Translate> */}
-          {help}
-        </p>
-      </div>
-    );
-  }
-  if (type === "bitcoin") {
-    return (
-      <div class="sm:col-span-5">
-        <label
-          class="block text-sm font-medium leading-6 text-gray-900"
-          for={name}
-        >
-          {label}
-        </label>
-        <div class="mt-2">
-          <div class="flex justify-between">
-            <input
-              type="text"
-              class="mr-4 w-full block-inline  disabled:bg-gray-100 rounded-md 
border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 
data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 
focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
-              name={name}
-              id={name}
-              disabled={disabled}
-              value={value ?? ""}
-            />
-            <CopyButton
-              class="p-2 rounded-full  text-black shadow-sm  
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
-              getContent={() => value ?? ""}
-            />
-            <ShowInputErrorLabel
-              message={error}
-              isDirty={value !== undefined}
-            />
-          </div>
-        </div>
-        <p class="mt-2 text-sm text-gray-500">
-          {/* <i18n.Translate>bitcoin address</i18n.Translate> */}
-          {help}
-        </p>
-      </div>
-    );
-  }
-  assertUnreachable(type);
-}
+// function PaytoField({
+//   name,
+//   label,
+//   help,
+//   type,
+//   value,
+//   disabled,
+//   onChange,
+//   error,
+// }: {
+//   error: TranslatedString | undefined;
+//   name: string;
+//   label: TranslatedString;
+//   help: TranslatedString;
+//   onChange: (s: string) => void;
+//   type: "iban" | "x-taler-bank" | "bitcoin";
+//   disabled?: boolean;
+//   value: string | undefined;
+// }): VNode {
+//   if (type === "iban") {
+//     return (
+//       <div class="sm:col-span-5">
+//         <label
+//           class="block text-sm font-medium leading-6 text-gray-900"
+//           for={name}
+//         >
+//           {label}
+//         </label>
+//         <div class="mt-2">
+//           <div class="flex justify-between">
+//             <input
+//               type="text"
+//               class="mr-4 w-full block-inline  disabled:bg-gray-100 
rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset 
ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 
focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+//               name={name}
+//               id={name}
+//               disabled={disabled}
+//               value={value ?? ""}
+//               onChange={(e) => {
+//                 onChange(e.currentTarget.value);
+//               }}
+//             />
+//             <CopyButton
+//               class="p-2 rounded-full  text-black shadow-sm  
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+//               getContent={() => value ?? ""}
+//             />
+//           </div>
+//           <ShowInputErrorLabel message={error} isDirty={value !== 
undefined} />
+//         </div>
+//         <p class="mt-2 text-sm text-gray-500">{help}</p>
+//       </div>
+//     );
+//   }
+//   if (type === "x-taler-bank") {
+//     return (
+//       <div class="sm:col-span-5">
+//         <label
+//           class="block text-sm font-medium leading-6 text-gray-900"
+//           for={name}
+//         >
+//           {label}
+//         </label>
+//         <div class="mt-2">
+//           <div class="flex justify-between">
+//             <input
+//               type="text"
+//               class="mr-4 w-full block-inline  disabled:bg-gray-100 
rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset 
ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 
focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+//               name={name}
+//               id={name}
+//               disabled={disabled}
+//               value={value ?? ""}
+//               onChange={(e) => {
+//                 onChange(e.currentTarget.value);
+//               }}
+//             />
+//             <CopyButton
+//               class="p-2 rounded-full  text-black shadow-sm  
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+//               getContent={() => value ?? ""}
+//             />
+//           </div>
+//           <ShowInputErrorLabel message={error} isDirty={value !== 
undefined} />
+//         </div>
+//         <p class="mt-2 text-sm text-gray-500">
+//           {help}
+//         </p>
+//       </div>
+//     );
+//   }
+//   if (type === "bitcoin") {
+//     return (
+//       <div class="sm:col-span-5">
+//         <label
+//           class="block text-sm font-medium leading-6 text-gray-900"
+//           for={name}
+//         >
+//           {label}
+//         </label>
+//         <div class="mt-2">
+//           <div class="flex justify-between">
+//             <input
+//               type="text"
+//               class="mr-4 w-full block-inline  disabled:bg-gray-100 
rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset 
ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 
focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+//               name={name}
+//               id={name}
+//               disabled={disabled}
+//               value={value ?? ""}
+//             />
+//             <CopyButton
+//               class="p-2 rounded-full  text-black shadow-sm  
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+//               getContent={() => value ?? ""}
+//             />
+//             <ShowInputErrorLabel
+//               message={error}
+//               isDirty={value !== undefined}
+//             />
+//           </div>
+//         </div>
+//         <p class="mt-2 text-sm text-gray-500">
+//           {/* <i18n.Translate>bitcoin address</i18n.Translate> */}
+//           {help}
+//         </p>
+//       </div>
+//     );
+//   }
+//   assertUnreachable(type);
+// }
diff --git a/packages/demobank-ui/src/pages/admin/AccountList.tsx 
b/packages/demobank-ui/src/pages/admin/AccountList.tsx
index 41d54c43d..5528b5226 100644
--- a/packages/demobank-ui/src/pages/admin/AccountList.tsx
+++ b/packages/demobank-ui/src/pages/admin/AccountList.tsx
@@ -62,6 +62,7 @@ export function AccountList({
     }
   }
 
+
   const { accounts } = result.data.body;
   return (
     <Fragment>
@@ -170,15 +171,20 @@ export function AccountList({
                               <i18n.Translate>Change password</i18n.Translate>
                             </a>
                             <br />
-                            <a
-                              href={routeShowCashoutsAccount.url({
-                                account: item.username,
-                              })}
-                              class="text-indigo-600 hover:text-indigo-900"
-                            >
-                              <i18n.Translate>Cashouts</i18n.Translate>
-                            </a>
-                            <br />
+                            {config.allow_conversion ?
+                              <Fragment>
+
+                                <a
+                                  href={routeShowCashoutsAccount.url({
+                                    account: item.username,
+                                  })}
+                                  class="text-indigo-600 hover:text-indigo-900"
+                                >
+                                  <i18n.Translate>Cashouts</i18n.Translate>
+                                </a>
+                                <br />
+                              </Fragment>
+                              : undefined}
                             {noBalance ? (
                               <a
                                 href={routeRemoveAccount.url({
diff --git a/packages/demobank-ui/src/pages/admin/AdminHome.tsx 
b/packages/demobank-ui/src/pages/admin/AdminHome.tsx
index 1a7edd6b9..35106edeb 100644
--- a/packages/demobank-ui/src/pages/admin/AdminHome.tsx
+++ b/packages/demobank-ui/src/pages/admin/AdminHome.tsx
@@ -170,8 +170,10 @@ function Metrics({ routeDownloadStats }: {
         return (
           <Attention
             type="danger"
-            title={i18n.str`Cashout not implemented`}
-          ></Attention>
+            title={i18n.str`Cashout are disabled`}
+          >
+            <i18n.Translate>Cashout should be enable by configuration and the 
conversion rate should be initialized with fee, ratio and rounding 
mode.</i18n.Translate>
+          </Attention>
         );
       }
       default:
diff --git a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx 
b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
index c4e4266f9..23d5a1e90 100644
--- a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
+++ b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
@@ -29,7 +29,6 @@ import {
 } from "@gnu-taler/web-util/browser";
 import { Fragment, VNode, h } from "preact";
 import { useState } from "preact/hooks";
-import { mutate } from "swr";
 import { useBankCoreApiContext } from "../../context/config.js";
 import { useBackendState } from "../../hooks/backend.js";
 import { RouteDefinition } from "../../route.js";
@@ -70,7 +69,6 @@ export function CreateNewAccount({
 
       const resp = await api.createAccount(token, submitAccount);
       if (resp.type === "ok") {
-        mutate(() => true); // clean account list
         notifyInfo(
           i18n.str`Account created with password "${submitAccount.password}". 
The user must change the password on the next login.`,
         );
diff --git a/packages/demobank-ui/src/pages/business/CreateCashout.tsx 
b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
index 8ec34276f..6d538575b 100644
--- a/packages/demobank-ui/src/pages/business/CreateCashout.tsx
+++ b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
@@ -140,8 +140,10 @@ export function CreateCashout({
         return (
           <Attention
             type="danger"
-            title={i18n.str`Cashout not implemented`}
-          ></Attention>
+            title={i18n.str`Cashout are disabled`}
+          >
+            <i18n.Translate>Cashout should be enable by configuration and the 
conversion rate should be initialized with fee, ratio and rounding 
mode.</i18n.Translate>
+          </Attention>
         );
       }
       default:
@@ -188,8 +190,7 @@ export function CreateCashout({
    * depending on the isDebit flag
    */
   const inputAmount = Amounts.parseOrThrow(
-    `${form.isDebit ? regional_currency : fiat_currency}:${
-      !form.amount ? "0" : form.amount
+    `${form.isDebit ? regional_currency : fiat_currency}:${!form.amount ? "0" 
: form.amount
     }`,
   );
 
@@ -291,7 +292,7 @@ export function CreateCashout({
           case HttpStatusCode.NotImplemented:
             return notify({
               type: "error",
-              title: i18n.str`Cashouts are not supported`,
+              title: i18n.str`Cashout are disabled`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
             });
@@ -471,9 +472,9 @@ export function CreateCashout({
                       cashoutDisabled
                         ? undefined
                         : (value) => {
-                            form.amount = value;
-                            updateForm(structuredClone(form));
-                          }
+                          form.amount = value;
+                          updateForm(structuredClone(form));
+                        }
                     }
                   />
                   <ShowInputErrorLabel
@@ -514,7 +515,7 @@ export function CreateCashout({
                       </dd>
                     </div>
                     {Amounts.isZero(sellFee) ||
-                    Amounts.isZero(calc.beforeFee) ? undefined : (
+                      Amounts.isZero(calc.beforeFee) ? undefined : (
                       <div class="flex items-center justify-between border-t-2 
afu pt-4">
                         <dt class="flex items-center text-sm text-gray-600">
                           <span>
@@ -547,7 +548,7 @@ export function CreateCashout({
 
               {/* channel, not shown if new cashout api */}
               {!OLD_CASHOUT_API ? undefined : config.supported_tan_channels
-                  .length === 0 ? (
+                .length === 0 ? (
                 <div class="sm:col-span-5">
                   <Attention
                     type="warning"
@@ -619,7 +620,7 @@ export function CreateCashout({
                       )}
 
                       {config.supported_tan_channels.indexOf(TanChannel.SMS) 
===
-                      -1 ? undefined : (
+                        -1 ? undefined : (
                         <label
                           onClick={() => {
                             if (!resultAccount.body.contact_data?.phone) 
return;
diff --git a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx 
b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
index 7b251d3ca..1e70886ad 100644
--- a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
+++ b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
@@ -69,8 +69,10 @@ export function ShowCashoutDetails({ id, routeClose }: 
Props): VNode {
         return (
           <Attention
             type="warning"
-            title={i18n.str`Cashouts are not supported`}
-          ></Attention>
+            title={i18n.str`Cashout are disabled`}
+          >
+            <i18n.Translate>Cashout should be enable by configuration and the 
conversion rate should be initialized with fee, ratio and rounding 
mode.</i18n.Translate>
+          </Attention>
         );
       default:
         assertUnreachable(result);
@@ -87,7 +89,11 @@ export function ShowCashoutDetails({ id, routeClose }: 
Props): VNode {
     switch (info.case) {
       case HttpStatusCode.NotImplemented: {
         return (
-          <Attention type="danger" title={i18n.str`Cashout not implemented`} />
+          <Attention type="danger"
+            title={i18n.str`Cashout are disabled`}
+          >
+            <i18n.Translate>Cashout should be enable by configuration and the 
conversion rate should be initialized with fee, ratio and rounding 
mode.</i18n.Translate>
+          </Attention>
         );
       }
       default:
diff --git a/packages/demobank-ui/src/utils.ts 
b/packages/demobank-ui/src/utils.ts
index 4413ce814..ab0b60d72 100644
--- a/packages/demobank-ui/src/utils.ts
+++ b/packages/demobank-ui/src/utils.ts
@@ -23,6 +23,7 @@ import {
 } from "@gnu-taler/taler-util";
 import {
   ErrorNotification,
+  InternationalizationAPI,
   notify,
   notifyError,
   useTranslationContext,
@@ -72,36 +73,36 @@ export type PartialButDefined<T> = {
  */
 export type WithIntermediate<Type> = {
   [prop in keyof Type]: Type[prop] extends PaytoString
-    ? Type[prop] | undefined
-    : Type[prop] extends AmountString
-      ? Type[prop] | undefined
-      : Type[prop] extends TranslatedString
-        ? Type[prop] | undefined
-        : Type[prop] extends object
-          ? WithIntermediate<Type[prop]>
-          : Type[prop] | undefined;
+  ? Type[prop] | undefined
+  : Type[prop] extends AmountString
+  ? Type[prop] | undefined
+  : Type[prop] extends TranslatedString
+  ? Type[prop] | undefined
+  : Type[prop] extends object
+  ? WithIntermediate<Type[prop]>
+  : Type[prop] | undefined;
 };
 export type RecursivePartial<Type> = {
   [P in keyof Type]?: Type[P] extends (infer U)[]
-    ? RecursivePartial<U>[]
-    : Type[P] extends object
-      ? RecursivePartial<Type[P]>
-      : Type[P];
+  ? RecursivePartial<U>[]
+  : Type[P] extends object
+  ? RecursivePartial<Type[P]>
+  : Type[P];
 };
 export type ErrorMessageMappingFor<Type> = {
   [prop in keyof Type]+?: Exclude<Type[prop], undefined> extends PaytoString 
// enumerate known object
-    ? TranslatedString
-    : Exclude<Type[prop], undefined> extends AmountString
-      ? TranslatedString
-      : Exclude<Type[prop], undefined> extends TranslatedString
-        ? TranslatedString
-        : // arrays: every element
-          Exclude<Type[prop], undefined> extends (infer U)[]
-          ? ErrorMessageMappingFor<U>[]
-          : // map: every field
-            Exclude<Type[prop], undefined> extends object
-            ? ErrorMessageMappingFor<Type[prop]>
-            : TranslatedString;
+  ? TranslatedString
+  : Exclude<Type[prop], undefined> extends AmountString
+  ? TranslatedString
+  : Exclude<Type[prop], undefined> extends TranslatedString
+  ? TranslatedString
+  : // arrays: every element
+  Exclude<Type[prop], undefined> extends (infer U)[]
+  ? ErrorMessageMappingFor<U>[]
+  : // map: every field
+  Exclude<Type[prop], undefined> extends object
+  ? ErrorMessageMappingFor<Type[prop]>
+  : TranslatedString;
 };
 
 export enum TanChannel {
@@ -367,26 +368,30 @@ export const COUNTRY_TABLE = {
  * If the remainder is 1, the check digit test is passed and the IBAN might be 
valid.
  *
  */
+const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
 export function validateIBAN(
-  iban: string,
-  i18n: ReturnType<typeof useTranslationContext>["i18n"],
+  account: string,
+  i18n: InternationalizationAPI,
 ): TranslatedString | undefined {
+  if (!IBAN_REGEX.test(account)) {
+    return i18n.str`IBAN only have uppercased letters and numbers`
+  }
   // Check total length
-  if (iban.length < 4)
-    return i18n.str`IBAN numbers usually have more that 4 digits`;
-  if (iban.length > 34)
-    return i18n.str`IBAN numbers usually have less that 34 digits`;
+  if (account.length < 4)
+    return i18n.str`IBAN numbers have more that 4 digits`;
+  if (account.length > 34)
+    return i18n.str`IBAN numbers have less that 34 digits`;
 
   const A_code = "A".charCodeAt(0);
   const Z_code = "Z".charCodeAt(0);
-  const IBAN = iban.toUpperCase();
+  const IBAN = account.toUpperCase();
   // check supported country
   const code = IBAN.substring(0, 2);
   const found = code in COUNTRY_TABLE;
   if (!found) return i18n.str`IBAN country code not found`;
 
   // 2.- Move the four initial characters to the end of the string
-  const step2 = IBAN.substring(4) + iban.substring(0, 4);
+  const step2 = IBAN.substring(4) + account.substring(0, 4);
   const step3 = Array.from(step2)
     .map((letter) => {
       const code = letter.charCodeAt(0);
@@ -411,3 +416,33 @@ function calculate_iban_checksum(str: string): number {
   }
   return result;
 }
+
+const USERNAME_REGEX = /^[A-Za-z][A-Za-z0-9]*$/;
+
+export function validateTalerBank(
+  account: string,
+  i18n: InternationalizationAPI,
+): TranslatedString | undefined {
+  if (!USERNAME_REGEX.test(account)) {
+    return i18n.str`Account only have letters and numbers`
+  }
+  return undefined
+}
+
+export function validateRawIBAN(
+  payto: string,
+  i18n: InternationalizationAPI,
+): TranslatedString | undefined {
+  return undefined
+}
+
+
+
+export function validateRawTalerBank(
+  payto: string,
+  currentHost: string,
+  i18n: InternationalizationAPI,
+): TranslatedString | undefined {
+  return undefined
+}
+
diff --git a/packages/taler-util/src/http-client/types.ts 
b/packages/taler-util/src/http-client/types.ts
index a04d629d3..6c8bf4efd 100644
--- a/packages/taler-util/src/http-client/types.ts
+++ b/packages/taler-util/src/http-client/types.ts
@@ -291,6 +291,7 @@ export const codecForCoreBankConfig = (): 
Codec<TalerCorebankApi.Config> =>
         ),
       ),
     )
+    .property("wire_type", codecForString())
     .build("TalerCorebankApi.Config");
 
 export const codecForMerchantConfig =
@@ -1339,6 +1340,11 @@ export namespace TalerCorebankApi {
 
     // TAN channels supported by the server
     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;
   }
 
   export interface BankAccountCreateWithdrawalRequest {

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]