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:
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}`;
+ }
}
}