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:
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> {