taler-typescript-core

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

commit 7d801a7bb4f35ccd75dea2794b90dc0420469144
parent 73c38b829b377797ed1cfd2d5398dce2e24e657c
Author: Florian Dold <florian@dold.me>
Date:   Thu,  7 Nov 2024 16:56:45 +0100

harness: use normalized payto hashing

Diffstat:
Mpackages/aml-backoffice-ui/src/hooks/account.ts | 8++------
Mpackages/aml-backoffice-ui/src/pages/Search.tsx | 293+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx | 8+-------
Mpackages/taler-harness/src/integrationtests/test-kyc-balance-withdrawal.ts | 4++--
Mpackages/taler-harness/src/integrationtests/test-kyc-exchange-wallet.ts | 8+++-----
Mpackages/taler-harness/src/integrationtests/test-kyc-initial-decision.ts | 4++--
Mpackages/taler-harness/src/integrationtests/test-kyc-merchant-deposit.ts | 12+++++++++---
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 2+-
Mpackages/taler-util/src/payto.ts | 39++++++++++++++++++++++++++++++++++++++-
Mpackages/taler-wallet-core/src/exchanges.ts | 4++--
10 files changed, 211 insertions(+), 171 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/hooks/account.ts b/packages/aml-backoffice-ui/src/hooks/account.ts @@ -14,19 +14,15 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { - encodeCrock, - hashPaytoUri, OfficerAccount, PaytoString, - PaytoUri, - stringifyPaytoUri, TalerExchangeResultByMethod, TalerHttpError, } from "@gnu-taler/taler-util"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook, mutate } from "swr"; -import { useOfficer } from "./officer.js"; import { useExchangeApiContext } from "@gnu-taler/web-util/browser"; +import _useSWR, { mutate, SWRHook } from "swr"; +import { useOfficer } from "./officer.js"; const useSWR = _useSWR as unknown as SWRHook; export function revalidateAccountInformation() { diff --git a/packages/aml-backoffice-ui/src/pages/Search.tsx b/packages/aml-backoffice-ui/src/pages/Search.tsx @@ -18,7 +18,7 @@ import { assertUnreachable, buildPayto, encodeCrock, - hashPaytoUri, + hashNormalizedPaytoUri, HttpStatusCode, parsePaytoUri, PaytoUri, @@ -41,6 +41,8 @@ import { } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; +import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js"; +import { useAccountDecisions } from "../hooks/decisions.js"; import { FormErrors, FormStatus, @@ -50,13 +52,10 @@ import { useFormState, } from "../hooks/form.js"; import { useOfficer } from "../hooks/officer.js"; -import { undefinedIfEmpty } from "./CreateAccount.js"; -import { HandleAccountNotReady } from "./HandleAccountNotReady.js"; -import { useAccountInformation } from "../hooks/account.js"; -import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js"; -import { useAccountDecisions } from "../hooks/decisions.js"; import { privatePages } from "../Routing.js"; import { Pagination, ToInvestigateIcon } from "./Cases.js"; +import { undefinedIfEmpty } from "./CreateAccount.js"; +import { HandleAccountNotReady } from "./HandleAccountNotReady.js"; export function Search() { const officer = useOfficer(); @@ -101,7 +100,7 @@ export function Search() { </form> {paytoForm.status.status !== "ok" ? undefined : paytoForm.status.result - .paytoType === "x-taler-bank" ? ( + .paytoType === "x-taler-bank" ? ( <XTalerBankForm onSearch={setPayto} /> ) : paytoForm.status.result.paytoType === "iban" ? ( <IbanForm onSearch={setPayto} /> @@ -114,13 +113,13 @@ export function Search() { } function ShowResult({ payto }: { payto: PaytoUri }): VNode { - const paytoStr = stringifyPaytoUri(payto) - const account = encodeCrock(hashPaytoUri(paytoStr)); + const paytoStr = stringifyPaytoUri(payto); + const account = encodeCrock(hashNormalizedPaytoUri(paytoStr)); const { i18n } = useTranslationContext(); const history = useAccountDecisions(account); if (!history) { - return <Loading /> + return <Loading />; } if (history instanceof TalerError) { return <ErrorLoadingWithDebug error={history} />; @@ -150,7 +149,6 @@ function ShowResult({ payto }: { payto: PaytoUri }): VNode { </Attention> </Fragment> ); - } case HttpStatusCode.NotFound: { return ( @@ -162,125 +160,136 @@ function ShowResult({ payto }: { payto: PaytoUri }): VNode { ); } default: { - assertUnreachable(history) + assertUnreachable(history); } } } if (history.body.length) { - return <div class="mt-8"> - <div class="mb-2"> + return ( + <div class="mt-8"> + <div class="mb-2"> + <a + href={privatePages.caseDetails.url({ + cid: account, + })} + class="text-indigo-600 hover:text-indigo-900" + > + <i18n.Translate>Check account details</i18n.Translate> + </a> + </div> + <div class="sm:flex sm:items-center"> + <div class="sm:flex-auto"> + <div> + <h1 class="text-base font-semibold leading-6 text-gray-900"> + <i18n.Translate>Account most recent decisions</i18n.Translate> + </h1> + </div> + </div> + </div> + + <div class="flow-root"> + <div class="overflow-x-auto"> + {!history.body.length ? ( + <div>empty result </div> + ) : ( + <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"> + <table class="min-w-full divide-y divide-gray-300"> + <thead> + <tr> + <th + scope="col" + class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-80" + > + <i18n.Translate>When</i18n.Translate> + </th> + <th + scope="col" + class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-80" + > + <i18n.Translate>Justification</i18n.Translate> + </th> + <th + scope="col" + class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-40" + > + <i18n.Translate>Status</i18n.Translate> + </th> + </tr> + </thead> + <tbody class="divide-y divide-gray-200 bg-white"> + {history.body.map((r, idx) => { + return ( + <tr key={r.h_payto} class="hover:bg-gray-100 "> + <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 "> + <Time + format="dd/MM/yyyy HH:mm" + timestamp={AbsoluteTime.fromProtocolTimestamp( + r.decision_time, + )} + /> + </td> + <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 "> + {r.justification} + </td> + <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-900"> + {idx === 0 ? ( + <span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10"> + <i18n.Translate>LATEST</i18n.Translate> + </span> + ) : undefined} + {r.is_active ? ( + <span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10"> + <i18n.Translate>ACTIVE</i18n.Translate> + </span> + ) : undefined} + {r.decision_time ? ( + <span title="require investigation"> + <ToInvestigateIcon /> + </span> + ) : undefined} + </td> + </tr> + ); + })} + </tbody> + </table> + <Pagination + onFirstPage={ + history.isFirstPage ? undefined : history.loadFirst + } + onNext={history.isLastPage ? undefined : history.loadNext} + /> + </div> + )} + </div> + </div> + </div> + ); + } + return ( + <div class="mt-4"> + <Attention title={i18n.str`Account not found`} type="warning"> + <i18n.Translate> + There is no history known for this account yet. + </i18n.Translate> + &nbsp; <a - href={privatePages.caseDetails.url({ + href={privatePages.caseDetailsNewAccount.url({ cid: account, + payto: encodeCrockForURI(paytoStr), })} class="text-indigo-600 hover:text-indigo-900" > <i18n.Translate> - Check account details + You can make a decision for this account anyway. </i18n.Translate> </a> - </div> - <div class="sm:flex sm:items-center"> - <div class="sm:flex-auto"> - <div> - <h1 class="text-base font-semibold leading-6 text-gray-900"> - <i18n.Translate>Account most recent decisions</i18n.Translate> - </h1> - </div> - </div> - </div> - - <div class="flow-root"> - <div class="overflow-x-auto"> - {!history.body.length ? ( - <div>empty result </div> - ) : ( - <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"> - <table class="min-w-full divide-y divide-gray-300"> - <thead> - <tr> - <th - scope="col" - class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-80" - > - <i18n.Translate>When</i18n.Translate> - </th> - <th - scope="col" - class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-80" - > - <i18n.Translate>Justification</i18n.Translate> - </th> - <th - scope="col" - class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-40" - > - <i18n.Translate>Status</i18n.Translate> - </th> - </tr> - </thead> - <tbody class="divide-y divide-gray-200 bg-white"> - {history.body.map((r, idx) => { - return ( - <tr key={r.h_payto} class="hover:bg-gray-100 "> - <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 "> - <Time format="dd/MM/yyyy HH:mm" timestamp={AbsoluteTime.fromProtocolTimestamp(r.decision_time)} /> - </td> - <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 "> - {r.justification} - </td> - <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-900"> - - - {idx === 0 ? <span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10"> - <i18n.Translate>LATEST</i18n.Translate> - </span> : undefined} - {r.is_active ? <span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10"> - <i18n.Translate>ACTIVE</i18n.Translate> - </span> : undefined} - {r.decision_time ? ( - <span title="require investigation"> - <ToInvestigateIcon /> - </span> - ) : undefined} - </td> - </tr> - ); - })} - </tbody> - </table> - <Pagination - onFirstPage={history.isFirstPage ? undefined : history.loadFirst} - onNext={history.isLastPage ? undefined : history.loadNext} /> - </div> - )} - </div> - </div> + </Attention> </div> - } - return <div class="mt-4"> - <Attention title={i18n.str`Account not found`} type="warning"> - <i18n.Translate> - There is no history known for this account yet. - </i18n.Translate> - &nbsp; - <a - href={privatePages.caseDetailsNewAccount.url({ - cid: account, - payto: encodeCrockForURI(paytoStr), - })} - class="text-indigo-600 hover:text-indigo-900" - > - <i18n.Translate> - You can make a decision for this account anyway. - </i18n.Translate> - </a> - </Attention> - </div> + ); } - function XTalerBankForm({ onSearch, }: { @@ -297,13 +306,13 @@ function XTalerBankForm({ form.status.status === "fail" ? undefined : buildPayto( - "x-taler-bank", - form.status.result.hostname, - form.status.result.account, - { - "receiver-name": (form.status.result.name), - }, - ); + "x-taler-bank", + form.status.result.hostname, + form.status.result.account, + { + "receiver-name": form.status.result.name, + }, + ); return ( <form @@ -346,8 +355,8 @@ function IbanForm({ form.status.status === "fail" ? undefined : buildPayto("iban", form.status.result.account, form.status.result.bic, { - "receiver-name": (form.status.result.name), - }); + "receiver-name": form.status.result.name, + }); return ( <form @@ -616,25 +625,25 @@ const genericFields: ( const ibanFields: (i18n: InternationalizationAPI) => UIFormElementConfig[] = ( i18n, ) => [ - { - id: "account" as UIHandlerId, - type: "text", - required: true, - label: i18n.str`Account`, - help: i18n.str`International Bank Account Number`, - placeholder: i18n.str`DE1231231231`, - // validator: (value) => validateIBAN(value, i18n), - }, - receiverName(i18n), - { - id: "bic" as UIHandlerId, - type: "text", - label: i18n.str`Bank`, - help: i18n.str`Business Identifier Code`, - placeholder: i18n.str`GENODEM1GLS`, - // validator: (value) => validateIBAN(value, i18n), - }, - ]; + { + id: "account" as UIHandlerId, + type: "text", + required: true, + label: i18n.str`Account`, + help: i18n.str`International Bank Account Number`, + placeholder: i18n.str`DE1231231231`, + // validator: (value) => validateIBAN(value, i18n), + }, + receiverName(i18n), + { + id: "bic" as UIHandlerId, + type: "text", + label: i18n.str`Bank`, + help: i18n.str`Business Identifier Code`, + placeholder: i18n.str`GENODEM1GLS`, + // validator: (value) => validateIBAN(value, i18n), + }, +]; const talerBankFields: ( i18n: InternationalizationAPI, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx @@ -19,13 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { - AccessToken, - encodeCrock, - hashPaytoUri, - hashWire, - TalerMerchantApi, -} from "@gnu-taler/taler-util"; +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; diff --git a/packages/taler-harness/src/integrationtests/test-kyc-balance-withdrawal.ts b/packages/taler-harness/src/integrationtests/test-kyc-balance-withdrawal.ts @@ -20,7 +20,7 @@ import { encodeCrock, ExchangeWalletKycStatus, - hashPaytoUri, + hashFullPaytoUri, j2s, TalerCorebankApiClient, TransactionIdStr, @@ -247,7 +247,7 @@ export async function runKycBalanceWithdrawalTest(t: GlobalTestState) { t.assertTrue(!!kycReservePub); // FIXME: Create/user helper function for this! - const hPayto = hashPaytoUri( + const hPayto = hashFullPaytoUri( `payto://taler-reserve-http/localhost:${exchange.port}/${kycReservePub}`, ); diff --git a/packages/taler-harness/src/integrationtests/test-kyc-exchange-wallet.ts b/packages/taler-harness/src/integrationtests/test-kyc-exchange-wallet.ts @@ -20,7 +20,7 @@ import { encodeCrock, ExchangeWalletKycStatus, - hashPaytoUri, + hashFullPaytoUri, j2s, TalerCorebankApiClient, } from "@gnu-taler/taler-util"; @@ -30,6 +30,7 @@ import { WalletApiOperation, } from "@gnu-taler/taler-wallet-core"; import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; +import { EnvOptions, postAmlDecisionNoRules } from "../harness/environments.js"; import { BankService, DbInfo, @@ -41,7 +42,6 @@ import { WalletClient, WalletService, } from "../harness/harness.js"; -import { EnvOptions, postAmlDecisionNoRules } from "../harness/environments.js"; interface KycTestEnv { commonDb: DbInfo; @@ -225,12 +225,10 @@ export async function runKycExchangeWalletTest(t: GlobalTestState) { t.assertTrue(!!kycReservePub); // FIXME: Create/user helper function for this! - const hPayto = hashPaytoUri( + const hPayto = hashFullPaytoUri( `payto://taler-reserve-http/localhost:${exchange.port}/${kycReservePub}`, ); - console.log(`hPayto: ${hPayto}`); - await postAmlDecisionNoRules(t, { amlPriv: amlKeypair.priv, amlPub: amlKeypair.pub, diff --git a/packages/taler-harness/src/integrationtests/test-kyc-initial-decision.ts b/packages/taler-harness/src/integrationtests/test-kyc-initial-decision.ts @@ -21,7 +21,7 @@ import { Configuration, Duration, encodeCrock, - hashPaytoUri, + hashFullPaytoUri, TalerProtocolTimestamp, } from "@gnu-taler/taler-util"; import { @@ -56,7 +56,7 @@ export async function runKycInitialDecisionTest(t: GlobalTestState) { const merchantPayto = getTestHarnessPaytoForLabel("merchant-default"); - const kycPaytoHash = encodeCrock(hashPaytoUri(merchantPayto)); + const kycPaytoHash = encodeCrock(hashFullPaytoUri(merchantPayto)); // Make a decision where the exchange doesn't know the account yet. await postAmlDecision(t, { diff --git a/packages/taler-harness/src/integrationtests/test-kyc-merchant-deposit.ts b/packages/taler-harness/src/integrationtests/test-kyc-merchant-deposit.ts @@ -22,7 +22,7 @@ import { codecForKycProcessClientInformation, codecForQueryInstancesResponse, encodeCrock, - hashPaytoUri, + hashNormalizedPaytoUri, j2s, Logger, MerchantAccountKycRedirectsResponse, @@ -38,7 +38,11 @@ import { postAmlDecisionNoRules, withdrawViaBankV3, } from "../harness/environments.js"; -import { delayMs, GlobalTestState, harnessHttpLib } from "../harness/harness.js"; +import { + delayMs, + GlobalTestState, + harnessHttpLib, +} from "../harness/harness.js"; const logger = new Logger(`test-kyc-merchant-deposit.ts`); @@ -203,7 +207,9 @@ export async function runKycMerchantDepositTest(t: GlobalTestState) { amlPriv: amlKeypair.priv, amlPub: amlKeypair.pub, exchangeBaseUrl: exchange.baseUrl, - paytoHash: encodeCrock(hashPaytoUri(kycRespTwo.kyc_data[0].payto_uri)), + paytoHash: encodeCrock( + hashNormalizedPaytoUri(kycRespTwo.kyc_data[0].payto_uri), + ), }); while (true) { diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -312,7 +312,7 @@ export interface TestInfo { function updateCurrentSymlink(testDir: string): void { const currLink = path.join( os.tmpdir(), - `taler-integrationtests-${os.userInfo().username}-current`, + `taler-integrationtests-latest-${os.userInfo().username}`, ); try { fs.unlinkSync(currLink); diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts @@ -217,11 +217,48 @@ export function stringifyPaytoUri(p: PaytoUri): PaytoString { return url.href as PaytoString; } -export function hashPaytoUri(p: PaytoUri | string): Uint8Array { +export function hashFullPaytoUri(p: PaytoUri | string): Uint8Array { const paytoUri = typeof p === "string" ? p : stringifyPaytoUri(p); return hashTruncate32(stringToBytes(paytoUri + "\0")); } +/** + * Normalize and then hash a payto URI. + */ +export function hashNormalizedPaytoUri(p: PaytoUri | string): Uint8Array { + const paytoUri = typeof p === "string" ? p : stringifyPaytoUri(p); + if (typeof p === "string") { + const parseRes = parsePaytoUri(p); + if (!parseRes) { + throw Error("invalid payto URI"); + } + p = parseRes; + } + let paytoStr: string; + if (!p.isKnown) { + const normalizedPayto: PaytoUri = { + targetType: p.targetType, + targetPath: p.targetPath, + isKnown: false, + params: {}, + }; + paytoStr = stringifyPaytoUri(normalizedPayto); + } else { + switch (p.targetType) { + case "iban": + // FIXME: Strip BIC? + paytoStr = `payto://iban/${p.targetPath}`; + break; + case "x-taler-bank": + paytoStr = `payto://x-taler-bank/${p.host}/${p.account}`; + break; + case "bitcoin": + paytoStr = `payto://bitcoin/${p.address}`; + } + } + return hashTruncate32(stringToBytes(paytoStr + "\0")); +} + export function stringifyReservePaytoUri( exchangeBaseUrl: string, reservePub: string, diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts @@ -104,7 +104,7 @@ import { encodeCrock, getRandomBytes, hashDenomPub, - hashPaytoUri, + hashFullPaytoUri, j2s, makeErrorDetail, makeTalerErrorDetail, @@ -3518,7 +3518,7 @@ async function handleExchangeKycPendingLegitimization( reserve.reservePub, ); - const paytoHash = encodeCrock(hashPaytoUri(reservePayto)); + const paytoHash = encodeCrock(hashFullPaytoUri(reservePayto)); const resp = await wex.ws.runLongpollQueueing( wex,