taler-typescript-core

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

commit 3aecdac2716d89d284f0ddf1c4181da9b6e853b4
parent 6dc112f9bd000e4fb5cbfaa5d7bb95f0a33e5205
Author: Sebastian <sebasjm@gmail.com>
Date:   Fri, 11 Apr 2025 15:28:11 -0300

fix issue in bank account reported by vlada: payto was not parsed correctly added support to eth

Diffstat:
Mpackages/bank-ui/src/pages/OperationState/views.tsx | 29+++++++++++++++++++++++++++++
Mpackages/bank-ui/src/pages/PaytoWireTransferForm.tsx | 21++++++++++++++-------
Mpackages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx | 48+++++++++++++++++++++++++++++++++++++++++++++---
Mpackages/merchant-backoffice-ui/src/components/modal/index.tsx | 4+++-
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx | 3+++
Mpackages/taler-util/src/errors.ts | 6++++++
Mpackages/taler-util/src/payto.ts | 183++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mpackages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx | 39+++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx | 3+++
11 files changed, 256 insertions(+), 84 deletions(-)

diff --git a/packages/bank-ui/src/pages/OperationState/views.tsx b/packages/bank-ui/src/pages/OperationState/views.tsx @@ -342,6 +342,35 @@ export function NeedConfirmationView({ </Fragment> ); } + case "ethereum": { + const name = details.account.params["receiver-name"]; + return ( + <Fragment> + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900"> + <i18n.Translate> + Payment Service Provider's account address + </i18n.Translate> + </dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> + {details.account.address} + </dd> + </div> + {name && ( + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900"> + <i18n.Translate> + Payment Service Provider's name + </i18n.Translate> + </dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> + {name} + </dd> + </div> + )} + </Fragment> + ); + } default: { assertUnreachable(details.account); } diff --git a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx @@ -171,9 +171,11 @@ export function PaytoWireTransferForm({ ? undefined // FIXME: unsupported payto:// : p.targetType === "bitcoin" ? p.address - : p.targetType === "x-taler-bank" - ? p.account - : assertUnreachable(p); + : p.targetType === "ethereum" + ? p.address + : p.targetType === "x-taler-bank" + ? p.account + : assertUnreachable(p); } else { if (!account || !subject) return; let payto; @@ -333,6 +335,11 @@ export function PaytoWireTransferForm({ break; } case "bitcoin": { + // FIXME: unsupported payto + break; + } + case "ethereum": { + // FIXME: unsupported payto break; } default: { @@ -983,13 +990,13 @@ export function TextField({ ); } -const PAYTO_START_REGEX = /(?:payto:\/\/[^\s]*)/ +const PAYTO_START_REGEX = /(?:payto:\/\/[^\s]*)/; function searchPaytoInHumanReadableText( text: string | undefined, ): string | undefined { if (!text) return undefined; - const result = PAYTO_START_REGEX.exec(text) - if (!result) return undefined - return result[0] + const result = PAYTO_START_REGEX.exec(text); + if (!result) return undefined; + return result[0]; } diff --git a/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx @@ -237,7 +237,10 @@ export function WithdrawalConfirmationQuestion({ <i18n.Translate>Confirm the withdrawal operation</i18n.Translate> </h3> <div class="mt-3 text-sm leading-6"> - <ShouldBeSameUser username={details.username} onAuthorizationRequired={onAuthorizationRequired}> + <ShouldBeSameUser + username={details.username} + onAuthorizationRequired={onAuthorizationRequired} + > <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-2 bg-gray-100 my-4 px-4 pb-4 rounded-lg"> <form class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2" @@ -379,6 +382,37 @@ export function WithdrawalConfirmationQuestion({ </Fragment> ); } + case "ethereum": { + const name = + details.account.params["receiver-name"]; + return ( + <Fragment> + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900"> + <i18n.Translate> + Payment Service Provider's account + address + </i18n.Translate> + </dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> + {details.account.address} + </dd> + </div> + {name && ( + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900"> + <i18n.Translate> + Payment Service Provider's name + </i18n.Translate> + </dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> + {name} + </dd> + </div> + )} + </Fragment> + ); + } default: { assertUnreachable(details.account); } @@ -469,7 +503,11 @@ export function ShouldBeSameUser({ return ( <Fragment> <Attention type="info" title={i18n.str`Authentication required`} /> - <LoginForm currentUser={username} fixedUser onAuthorizationRequired={onAuthorizationRequired}/> + <LoginForm + currentUser={username} + fixedUser + onAuthorizationRequired={onAuthorizationRequired} + /> </Fragment> ); } @@ -480,7 +518,11 @@ export function ShouldBeSameUser({ type="warning" title={i18n.str`This operation was created with another username`} /> - <LoginForm currentUser={username} fixedUser onAuthorizationRequired={onAuthorizationRequired}/> + <LoginForm + currentUser={username} + fixedUser + onAuthorizationRequired={onAuthorizationRequired} + /> </Fragment> ); } diff --git a/packages/merchant-backoffice-ui/src/components/modal/index.tsx b/packages/merchant-backoffice-ui/src/components/modal/index.tsx @@ -469,7 +469,9 @@ export function ValidBankAccount({ ? origin.reservePub : origin.targetType === "bitcoin" ? `${origin.address.substring(0, 8)}...` - : origin.account; + : origin.targetType === "ethereum" + ? `${origin.address.substring(0, 8)}...` + : origin.account; return ( <ConfirmModal diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx @@ -85,7 +85,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { payto_uri: !state.payto_uri ? i18n.str`Required` : undefined, credit_facade_credentials: undefinedIfEmpty( - !state.credit_facade_credentials + !state.credit_facade_credentials || !state.credit_facade_url ? undefined : { username: diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx @@ -105,7 +105,7 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { const emptyList: Record< PaytoType | "unknown", { parsed: PaytoUri; acc: Entity }[] - > = { bitcoin: [], "x-taler-bank": [], iban: [], taler: [], unknown: [] }; + > = { bitcoin: [], "x-taler-bank": [], iban: [], taler: [], unknown: [], ethereum: [] }; const accountsByType = accounts.reduce((prev, acc) => { const parsed = parsePaytoUri(acc.payto_uri); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx @@ -473,6 +473,9 @@ export function paytoDisplayAccountName(account: PaytoString): string { case "bitcoin": { return p.address; } + case "ethereum": { + return p.address; + } default: { assertUnreachable(p); } diff --git a/packages/taler-util/src/errors.ts b/packages/taler-util/src/errors.ts @@ -363,6 +363,12 @@ export function getErrorDetailFromException(e: any): TalerErrorDetail { return err; } +/** + * This function should not be called at runtime. + * Useful on switch/case to detect that all path has been contemplated. + * + * @param x + */ export function assertUnreachable(x: never): never { throw new Error("Didn't expect to get here"); } diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts @@ -25,6 +25,7 @@ import { } from "./codec.js"; import { AccessToken, + assertUnreachable, codecForAccessToken, codecOptional, hashTruncate32, @@ -37,6 +38,7 @@ export type PaytoUri = | PaytoUriIBAN | PaytoUriTaler | PaytoUriTalerBank + | PaytoUriEthereum | PaytoUriBitcoin; // declare const __payto_str: unique symbol; @@ -99,9 +101,20 @@ export interface PaytoUriBitcoin extends PaytoUriGeneric { segwitAddrs: Array<string>; } +export interface PaytoUriEthereum extends PaytoUriGeneric { + isKnown: true; + targetType: "ethereum"; + address: string; +} + const paytoPfx = "payto://"; -export type PaytoType = "iban" | "bitcoin" | "x-taler-bank" | "taler"; +export type PaytoType = + | "iban" + | "bitcoin" + | "x-taler-bank" + | "taler" + | "ethereum"; export function buildPayto( type: "iban", @@ -146,6 +159,17 @@ export function buildPayto( }; return result; } + case "ethereum": { + const uppercased = first.toUpperCase(); + const result: PaytoUriEthereum = { + isKnown: true, + targetType: "ethereum", + targetPath: first, + address: uppercased, + params, + }; + return result; + } case "iban": { const uppercased = first.toUpperCase(); const result: PaytoUriIBAN = { @@ -281,6 +305,9 @@ export function hashNormalizedPaytoUri(p: PaytoUri | string): Uint8Array { case "bitcoin": paytoStr = `payto://bitcoin/${p.address}`; break; + case "ethereum": + paytoStr = `payto://ethereum/${p.address}`; + break; case "taler": paytoStr = `payto://taler/${p.exchange}/${p.reservePub}`; break; @@ -338,7 +365,7 @@ export function parsePaytoUri(s: string): PaytoUri | undefined { return undefined; } - const targetType = acct.slice(0, firstSlashPos); + const targetType = acct.slice(0, firstSlashPos) as PaytoType; const targetPath = acct.slice(firstSlashPos + 1); const params: { [k: string]: string } = {}; @@ -350,79 +377,93 @@ export function parsePaytoUri(s: string): PaytoUri | undefined { params[k] = v; //decodeURIComponent(v); }); - if (targetType === "taler") { - const parts = targetPath.split("/"); - const exchange = parts[0]; - const reservePub = parts[1]; - return { - targetPath, - targetType, - params, - isKnown: true, - exchange, - reservePub, - }; - } - if (targetType === "x-taler-bank") { - const parts = targetPath.split("/"); - const host = parts[0]; - const account = parts[1]; - return { - targetPath, - targetType, - params, - isKnown: true, - host, - account, - }; - } - if (targetType === "iban") { - const parts = targetPath.split("/"); - let iban: string | undefined = undefined; - let bic: string | undefined = undefined; - if (parts.length === 1) { - iban = parts[0].toUpperCase(); + switch (targetType) { + case "iban": { + const parts = targetPath.split("/"); + let iban: string | undefined = undefined; + let bic: string | undefined = undefined; + if (parts.length === 1) { + iban = parts[0].toUpperCase(); + } + if (parts.length === 2) { + bic = parts[0]; + iban = parts[1].toUpperCase(); + } else { + iban = targetPath.toUpperCase(); + } + return { + isKnown: true, + targetPath, + targetType, + params, + iban, + bic, + }; } - if (parts.length === 2) { - bic = parts[0]; - iban = parts[1].toUpperCase(); - } else { - iban = targetPath.toUpperCase(); + case "bitcoin": { + const msg = /\b([A-Z0-9]{52})\b/.exec(params["message"]); + const reserve = !msg ? params["subject"] : msg[0]; + const segwitAddrs = !reserve + ? [] + : generateFakeSegwitAddress(reserve, targetPath); + + const result: PaytoUriBitcoin = { + isKnown: true, + targetPath, + targetType, + address: targetPath, + params, + segwitAddrs, + }; + + return result; } - return { - isKnown: true, - targetPath, - targetType, - params, - iban, - bic, - }; - } - if (targetType === "bitcoin") { - const msg = /\b([A-Z0-9]{52})\b/.exec(params["message"]); - const reserve = !msg ? params["subject"] : msg[0]; - const segwitAddrs = !reserve - ? [] - : generateFakeSegwitAddress(reserve, targetPath); - - const uppercased = targetType.toUpperCase(); - const result: PaytoUriBitcoin = { - isKnown: true, - targetPath, - targetType, - address: uppercased, - params, - segwitAddrs, - }; + case "x-taler-bank": { + const parts = targetPath.split("/"); + const host = parts[0]; + const account = parts[1]; + return { + targetPath, + targetType, + params, + isKnown: true, + host, + account, + }; + } + case "taler": { + const parts = targetPath.split("/"); + const exchange = parts[0]; + const reservePub = parts[1]; + return { + targetPath, + targetType, + params, + isKnown: true, + exchange, + reservePub, + }; + } + case "ethereum": { + const result: PaytoUriEthereum = { + isKnown: true, + targetPath, + targetType, + address: targetPath, + params, + }; - return result; + return result; + } + default: { + return { + targetPath, + targetType, + params, + isKnown: false, + }; + } } - return { - targetPath, - targetType, - params, - isKnown: false, - }; } export function talerPaytoFromExchangeReserve( diff --git a/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx b/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx @@ -134,6 +134,45 @@ export function BankDetailsByPaytoType({ ); } + if (payto.isKnown && payto.targetType === "ethereum") { + const min = segwitMinAmount(amount.currency); + const copyContent = `${payto.targetPath} ${Amounts.stringifyValue(amount)}`; + return ( + <Frame + title={i18n.str`Ethereum transfer details`} + accounts={accounts} + updateIndex={setIndex} + currentIndex={index} + defaultCurrency={amount.currency} + > + <p> + <i18n.Translate> + You need to wire to the service provider account. + </i18n.Translate> + </p> + + <table> + <tr> + <td> + <div> + {payto.targetPath} <Amount value={amount} hideCurrency /> ETH + </div> + </td> + <td></td> + <td> + <CopyButton getContent={() => copyContent} /> + </td> + </tr> + </table> + <p> + <i18n.Translate> + Make sure the amount show {Amounts.stringifyValue(amount)} ETH + </i18n.Translate> + </p> + </Frame> + ); + } + return ( <Frame title={i18n.str`Bank transfer details`} diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx @@ -409,5 +409,8 @@ function describeAccount(paytoUri: string): string { case "bitcoin": { return `${p.address}`; } + case "ethereum": { + return `${p.address}`; + } } }