taler-typescript-core

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

commit 3583ad5a7fd3b29fe22710106455eb6fbdc99fd1
parent 4f19fd6fcc2387165636cf1fd0ba3d9ae09b577c
Author: Florian Dold <florian@dold.me>
Date:   Thu,  4 Dec 2025 19:12:07 +0100

wallet-core: implement bban / DD75 conversions

Diffstat:
Apackages/taler-harness/src/integrationtests/test-wallet-bban.ts | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 2++
Mpackages/taler-util/src/iban.ts | 26++++++++++++++++++++------
Mpackages/taler-util/src/operation.ts | 76++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mpackages/taler-wallet-core/src/wallet.ts | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
5 files changed, 245 insertions(+), 42 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-wallet-bban.ts b/packages/taler-harness/src/integrationtests/test-wallet-bban.ts @@ -0,0 +1,124 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Imports. + */ +import { Logger } from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { createWalletDaemonWithClient } from "../harness/environments.js"; +import { GlobalTestState } from "../harness/harness.js"; + +const logger = new Logger("test-wallet-bban.ts"); + +/** + * Test how the wallet handles an expired denomination. + */ +export async function runWalletBbanTest(t: GlobalTestState) { + const { walletClient } = await createWalletDaemonWithClient(t, { + name: "wallet", + }); + + { + const res = await walletClient.call( + WalletApiOperation.ConvertIbanPaytoToAccountField, + { + paytoUri: "payto://iban/HU30162000031000163100000000", + }, + ); + console.log(res); + t.assertDeepEqual(res.type, "bban"); + t.assertDeepEqual(res.value, "1620000310001631"); + } + + { + const res = await walletClient.call( + WalletApiOperation.ConvertIbanPaytoToAccountField, + { + paytoUri: "payto://iban/HU30162000031000163100000000", + }, + ); + console.log(res); + t.assertDeepEqual(res.type, "bban"); + t.assertDeepEqual(res.value, "1620000310001631"); + } + { + const res = await walletClient.call( + WalletApiOperation.ConvertIbanPaytoToAccountField, + { + paytoUri: "payto://iban/DE74100500009492290003", + }, + ); + console.log(res); + t.assertDeepEqual(res.type, "iban"); + t.assertDeepEqual(res.value, "DE74100500009492290003"); + } + + { + const res = await walletClient.call( + WalletApiOperation.ConvertIbanAccountFieldToPayto, + { + value: "DE741005000", + currency: "EUR", + }, + ); + t.assertDeepEqual(res.ok, false); + } + + { + const res = await walletClient.call( + WalletApiOperation.ConvertIbanAccountFieldToPayto, + { + value: "DE74100500009492290003", + currency: "EUR", + }, + ); + t.assertDeepEqual(res.ok, true); + t.assertDeepEqual(res.paytoUri, "payto://iban/DE74100500009492290003"); + } + + { + const res = await walletClient.call( + WalletApiOperation.ConvertIbanAccountFieldToPayto, + { + value: "1620000310001631", + currency: "HUF", + }, + ); + t.assertDeepEqual(res.ok, true); + t.assertDeepEqual( + res.paytoUri, + "payto://iban/HU30162000031000163100000000", + ); + } + + { + const res = await walletClient.call( + WalletApiOperation.ConvertIbanAccountFieldToPayto, + { + value: "16200003-10001631", + currency: "HUF", + }, + ); + t.assertDeepEqual(res.ok, true); + t.assertDeepEqual( + res.paytoUri, + "payto://iban/HU30162000031000163100000000", + ); + } +} + +runWalletBbanTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -166,6 +166,7 @@ import { runWalletBackupDoublespendTest } from "./test-wallet-backup-doublespend import { runWalletBalanceNotificationsTest } from "./test-wallet-balance-notifications.js"; import { runWalletBalanceZeroTest } from "./test-wallet-balance-zero.js"; import { runWalletBalanceTest } from "./test-wallet-balance.js"; +import { runWalletBbanTest } from "./test-wallet-bban.js"; import { runWalletBlockedDepositTest } from "./test-wallet-blocked-deposit.js"; import { runWalletBlockedPayMerchantTest } from "./test-wallet-blocked-pay-merchant.js"; import { runWalletBlockedPayPeerPullTest } from "./test-wallet-blocked-pay-peer-pull.js"; @@ -407,6 +408,7 @@ const allTests: TestMainFunction[] = [ runMerchantWireTest, runWalletExchangeFeaturesTest, runRepurchaseV1Test, + runWalletBbanTest, ]; export interface TestRunSpec { diff --git a/packages/taler-util/src/iban.ts b/packages/taler-util/src/iban.ts @@ -14,8 +14,11 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { OperationResult, opFixedSuccess, opKnownFailure } from "./operation.js"; - +import { + OperationResult, + opFixedSuccess, + opKnownFailure, +} from "./operation.js"; /** * IBAN validation. @@ -115,8 +118,8 @@ function mod97(digits: number[]): number { /** * Check the IBAN is correct and return canonical form - * @param ibanString - * @returns + * @param ibanString + * @returns */ export function parseIban( ibanString: string, @@ -227,11 +230,22 @@ export function validateIban(ibanString: string): IbanValidationResult { } export function generateIban(countryCode: string, length: number): IbanString { - let ibanSuffix = ""; - let digits: number[] = []; + let bban = ""; for (let i = 0; i < length; i++) { const cc = ccZero + (Math.floor(Math.random() * 100) % 10); + bban += String.fromCharCode(cc); + } + + return constructIban(countryCode, bban); +} + +export function constructIban(countryCode: string, bban: string): IbanString { + let ibanSuffix = ""; + let digits: number[] = []; + + for (let i = 0; i < bban.length; i++) { + const cc = bban.charCodeAt(i); appendDigit(digits, cc); ibanSuffix += String.fromCharCode(cc); } diff --git a/packages/taler-util/src/operation.ts b/packages/taler-util/src/operation.ts @@ -123,14 +123,14 @@ export function opKnownFailureWithBody<const T, const B>( } /** - * Before using the codec, try read minimum json body and + * Before using the codec, try read minimum json body and * verify that component and version matches. - * - * @param expectedName - * @param clientVersion - * @param httpResponse - * @param codec - * @returns + * + * @param expectedName + * @param clientVersion + * @param httpResponse + * @param codec + * @returns */ export async function carefullyParseConfig<T>( expectedName: string, @@ -165,13 +165,16 @@ export async function carefullyParseConfig<T>( } /** - * - * @param resp - * @param s - * @param codec - * @returns + * + * @param resp + * @param s + * @param codec + * @returns */ -export async function opKnownAlternativeHttpFailure<T extends HttpStatusCode, B>( +export async function opKnownAlternativeHttpFailure< + T extends HttpStatusCode, + B, +>( resp: HttpResponse, s: T, codec: Codec<B>, @@ -183,10 +186,10 @@ export async function opKnownAlternativeHttpFailure<T extends HttpStatusCode, B> /** * Constructor of a failure response of the API that is already documented in the spec. * The `case` parameter is a reason of the error. - * - * @param case - * @param resp - * @returns + * + * @param case + * @param resp + * @returns */ export async function opKnownHttpFailure<T extends HttpStatusCode>( _case: T, @@ -200,7 +203,7 @@ export async function opKnownHttpFailure<T extends HttpStatusCode>( } /** - * Constructor of an unexpected error, usually when the response of the API + * Constructor of an unexpected error, usually when the response of the API * is not in the spec. * * If the response hasn't already been read, this function will add the information @@ -231,10 +234,10 @@ export async function opUnknownHttpFailure( /** * Constructor of a failure response of the API that is already documented in the spec. * The `case` parameter is a reason of the error. - * - * @param case - * @param resp - * @returns + * + * @param case + * @param resp + * @returns */ export function opKnownTalerFailure<T extends TalerErrorCode>( _case: T, @@ -246,12 +249,13 @@ export function opKnownTalerFailure<T extends TalerErrorCode>( export function opUnknownFailure(error: unknown): never { throw TalerError.fromException(error); } + /** * The operation result should be ok * Return the body of the result - * - * @param resp - * @returns + * + * @param resp + * @returns */ export function succeedOrThrow<R>(resp: OperationResult<R, unknown>): R { if (isOperationOk(resp)) { @@ -263,7 +267,11 @@ export function succeedOrThrow<R>(resp: OperationResult<R, unknown>): R { } throw TalerError.fromException(resp); } -export function succeedOrValue<R,V>(resp: OperationResult<R, unknown>, v:V): R | V { + +export function succeedOrValue<R, V>( + resp: OperationResult<R, unknown>, + v: V, +): R | V { if (isOperationOk(resp)) { return resp.body; } @@ -275,10 +283,10 @@ export function succeedOrValue<R,V>(resp: OperationResult<R, unknown>, v:V): R | * The operation is expected to fail with a body. * Return the body of the result. * Throw if the operation didn't fail with expected code. - * - * @param resp - * @param s - * @returns + * + * @param resp + * @param s + * @returns */ export function alternativeOrThrow<Error, Body, Alt>( resp: @@ -308,10 +316,10 @@ export function alternativeOrThrow<Error, Body, Alt>( * The operation is expected to fail. * Return the error details. * Throw if the operation didn't fail with expected code. - * - * @param resp - * @param s - * @returns + * + * @param resp + * @param s + * @returns */ export function failOrThrow<E>( resp: OperationResult<unknown, E>, diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -113,6 +113,7 @@ import { ObservableHttpClientLibrary, OpenedPromise, PartialWalletRunConfig, + Paytos, PrepareWithdrawExchangeRequest, PrepareWithdrawExchangeResponse, RecoverStoredBackupRequest, @@ -255,12 +256,14 @@ import { codecForUserAttentionsRequest, codecForValidateIbanRequest, codecForWithdrawTestBalance, + constructIban, encodeCrock, getErrorDetailFromException, getQrCodesForPayto, getRandomBytes, j2s, openPromise, + parseIban, parsePaytoUri, parseTalerUri, performanceNow, @@ -268,6 +271,7 @@ import { setDangerousTimetravel, setGlobalLogLevelFromString, stringifyScopeInfo, + succeedOrThrow, validateIban, } from "@gnu-taler/taler-util"; import { @@ -1940,14 +1944,65 @@ export async function handleConvertIbanAccountFieldToPayto( wex: WalletExecutionContext, req: ConvertIbanAccountFieldToPaytoRequest, ): Promise<ConvertIbanAccountFieldToPaytoResponse> { - throw Error("not implemented"); + const strippedInput = req.value.replace(/[- ]/g, ""); + if (req.currency === "HUF") { + if ( + strippedInput.length <= 24 && + strippedInput.length >= 16 && + /[0-9+]/.test(strippedInput) + ) { + let bban: string; + if (strippedInput.length === 16) { + bban = `${strippedInput}00000000`; + } else { + bban = strippedInput; + } + return { + ok: true, + paytoUri: `payto://iban/${constructIban("HU", bban)}`, + type: "iban", + }; + } else { + return { + ok: false, + }; + } + } + const parsedIban = parseIban(strippedInput); + switch (parsedIban.case) { + case "ok": + return { + ok: true, + paytoUri: `payto://iban/${strippedInput}`, + type: "iban", + }; + default: + return { + ok: false, + }; + } } export async function handleConvertIbanPaytoToAccountField( wex: WalletExecutionContext, req: ConvertIbanPaytoToAccountFieldRequest, ): Promise<ConvertIbanPaytoToAccountFieldResponse> { - throw Error("not implemented"); + const payto = succeedOrThrow(Paytos.fromString(req.paytoUri)); + const iban = payto.normalizedPath; + if (iban.startsWith("HU")) { + let bban = iban.slice(4); + if (bban.endsWith("00000000")) { + bban = bban.slice(0, bban.length - 8); + } + return { + type: "bban", + value: bban, + }; + } + return { + type: "iban", + value: iban, + }; } interface HandlerWithValidator<Tag extends WalletApiOperation> {