taler-typescript-core

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

commit 600526ec63e680019f948bc45d19d276525b4f6c
parent f9d323d391ebe883f7cd9c07d49e4f47bb2fa849
Author: Antoine A <>
Date:   Wed,  8 Apr 2026 18:01:00 +0200

common: add prepared transfer API client

Diffstat:
Apackages/taler-harness/src/integrationtests/test-prepared-transfer.ts | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 2++
Apackages/taler-util/src/http-client/bank-prepared.ts | 141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-util/src/index.ts | 4+++-
Mpackages/taler-util/src/taler-error-codes.ts | 206++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Apackages/taler-util/src/types-taler-prepared-transfer.ts | 234+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 680 insertions(+), 8 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-prepared-transfer.ts b/packages/taler-harness/src/integrationtests/test-prepared-transfer.ts @@ -0,0 +1,101 @@ +/* + This file is part of GNU Taler + (C) 2026 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 { + alternativeOrThrow, + encodeCrock, + HttpStatusCode, + succeedOrThrow, + TalerCoreBankHttpClient, + TalerPreparedTransferHttpClient, +} from "@gnu-taler/taler-util"; +import { + BankService, + getTestHarnessPaytoForLabel, + GlobalTestState, + LibeufinBankService, + setupDb, +} from "../harness/harness.js"; +import { createSyncCryptoApi } from "@gnu-taler/taler-wallet-core"; + +export async function runPreparedTransferTest(t: GlobalTestState) { + // Set up test environment + + const cryptoApi = createSyncCryptoApi(); + + const db = await setupDb(t); + + const bank: BankService = await LibeufinBankService.create(t, { + allowRegistrations: true, + currency: "TESTKUDOS", + database: db.connStr, + httpPort: 8082, + }); + + const receiverName = "Exchange"; + const exchangeBankUsername = "exchange"; + const exchangeBankPassword = "mypw-password"; + const exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); + await bank.start(); + + const bankClient = new TalerCoreBankHttpClient(bank.corebankApiBaseUrl); + const preparedClient = new TalerPreparedTransferHttpClient(`${bank.corebankApiBaseUrl}accounts/${exchangeBankUsername}/taler-prepared-transfer/`,) + const bankAdminTok = await bank.getAdminTok(); + + succeedOrThrow(await bankClient.createAccount(bankAdminTok, { + name: receiverName, + password: exchangeBankPassword, + username: exchangeBankUsername, + is_taler_exchange: true, + payto_uri: exchangePaytoUri, + })); + + // Check config + succeedOrThrow(await preparedClient.getConfig()) + + // Check registration + const keypair = await cryptoApi.createEddsaKeypair({}); + succeedOrThrow(await preparedClient.register({ + credit_amount: "TESTKUDOS:10", + type: "reserve", + alg: "EdDSA", + account_pub: keypair.pub, + authorization_pub: keypair.pub, + authorization_sig: (await cryptoApi.eddsaSign({ + msg: keypair.pub, + priv: keypair.priv + })).sig, + recurrent: false + })) + + // Check unregistration + const timestamp = new Date().toISOString() + const uint8 = new TextEncoder().encode(timestamp); + const res = await preparedClient.unregister({ + timestamp: timestamp, + authorization_pub: keypair.pub, + authorization_sig: (await cryptoApi.eddsaSign({ + msg: encodeCrock(uint8.buffer), + priv: keypair.priv + })).sig + }) + console.log(res) +} + +runPreparedTransferTest.suites = ["fakebank"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -224,6 +224,7 @@ import { runWithdrawalHugeTest } from "./test-withdrawal-huge.js"; import { runWithdrawalIdempotentTest } from "./test-withdrawal-idempotent.js"; import { runWithdrawalManualTest } from "./test-withdrawal-manual.js"; import { runWithdrawalPrepareTest } from "./test-withdrawal-prepare.js"; +import { runPreparedTransferTest } from "./test-prepared-transfer.js" /** * Test runner. @@ -432,6 +433,7 @@ const allTests: TestMainFunction[] = [ runMerchantDepositLargeTest, runWireMetadataTest, runMerchantPaytoReuseTest, + runPreparedTransferTest ]; export interface TestRunSpec { diff --git a/packages/taler-util/src/http-client/bank-prepared.ts b/packages/taler-util/src/http-client/bank-prepared.ts @@ -0,0 +1,141 @@ +/* + This file is part of GNU Taler + (C) 2026 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/> + */ + +import { HttpRequestLibrary, readTalerErrorResponse } from "../http-common.js"; +import { HttpStatusCode } from "../http-status-codes.js"; +import { createPlatformHttpLib } from "../http.js"; +import { + FailCasesByMethod, + ResultByMethod, + opKnownHttpFailure, + opSuccessFromHttp, + opUnknownHttpFailure, +} from "../operation.js"; +import { carefullyParseConfig, codecForTalerErrorDetail, LibtoolVersion, opEmptySuccess, opKnownTalerFailure, TalerErrorCode, TalerPreparedTransferApi } from "../index.js"; +import { codecForPreparedTransferConfig, codecForRegistrationResponse } from "../types-taler-prepared-transfer.js"; + +export type TalerPreparedTransferResultByMethod< + prop extends keyof TalerPreparedTransferHttpClient, +> = ResultByMethod<TalerPreparedTransferHttpClient, prop>; +export type TalerPreparedTransferErrorsByMethod< + prop extends keyof TalerPreparedTransferHttpClient, +> = FailCasesByMethod<TalerPreparedTransferHttpClient, prop>; + +/** + * Allows Taler clients to prepared wire transfers, enabling recurring + * wire transfers and optimized transfer flow. + * + * https://docs.taler.net/core/api-bank-transfer.html + */ +export class TalerPreparedTransferHttpClient { + httpLib: HttpRequestLibrary; + public static readonly PROTOCOL_VERSION = "0:0:0"; + + constructor( + readonly baseUrl: string, + options: { + httpClient?: HttpRequestLibrary; + } = {}, + ) { + this.httpLib = options.httpClient ?? createPlatformHttpLib(); + } + + static isCompatible(version: string): boolean { + const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version); + return compare?.compatible ?? false; + } + + /** + * https://docs.taler.net/core/api-bank-transfer.html#get--config + * + */ + async getConfig() { + const url = new URL(`config`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return carefullyParseConfig( + "taler-prepared-transfer", + TalerPreparedTransferHttpClient.PROTOCOL_VERSION, + resp, + codecForPreparedTransferConfig(), + ); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-bank-transfer.html#post--registration + * + */ + async register(body: TalerPreparedTransferApi.RegistrationRequest) { + const url = new URL(`registration`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + }); + console.log(body); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForRegistrationResponse()); + case HttpStatusCode.BadRequest: + case HttpStatusCode.Unauthorized: + case HttpStatusCode.NotFound: + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-bank-transfer.html#delete--registration + * + */ + async unregister(body: TalerPreparedTransferApi.Unregistration) { + const url = new URL(`unregistration`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + }); + switch (resp.status) { + case HttpStatusCode.NoContent: + return opEmptySuccess(); + case HttpStatusCode.BadRequest: + case HttpStatusCode.Unauthorized: + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: { + const body = await readTalerErrorResponse(resp); + const details = codecForTalerErrorDetail().decode(body); + switch (details.code) { + case TalerErrorCode.BANK_OLD_TIMESTAMP: + case TalerErrorCode.BANK_BAD_SIGNATURE: + return opKnownTalerFailure(details.code, details); + default: + return opUnknownHttpFailure(resp, details); + } + } + default: + return opUnknownHttpFailure(resp); + } + } +} diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts @@ -1,5 +1,5 @@ export * from "./amounts.js"; -export * from "./bank-api-client.js"; +export * from "./bank-api-client.js" export * from "./base64.js"; export * from "./bech32.js"; export * from "./bitcoin.js"; @@ -16,6 +16,7 @@ export * from "./http-client/bank-core.js"; export * from "./http-client/bank-integration.js"; export * from "./http-client/bank-revenue.js"; export * from "./http-client/bank-wire.js"; +export * from "./http-client/bank-prepared.js"; export * from "./http-client/challenger.js"; export * from "./http-client/donau-client.js"; export * from "./http-client/exchange-client.js"; @@ -88,6 +89,7 @@ export * as TalerMailboxApi from "./types-taler-mailbox.js"; export * as TalerMerchantApi from "./types-taler-merchant.js"; export * as TalerRevenueApi from "./types-taler-revenue.js"; export * as TalerWireGatewayApi from "./types-taler-wire-gateway.js"; +export * as TalerPreparedTransferApi from "./types-taler-prepared-transfer.js"; export * from "./taler-account-properties.js"; export * from "./taler-form-attributes.js"; diff --git a/packages/taler-util/src/taler-error-codes.ts b/packages/taler-util/src/taler-error-codes.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - Copyright (C) 2012-2020 Taler Systems SA + Copyright (C) 2012-2026 Taler Systems SA GNU Taler is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published @@ -217,6 +217,14 @@ export enum TalerErrorCode { /** + * A parameter in the request was given that must not be present. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_PARAMETER_EXTRA = 33, + + + /** * The service refused the request due to lack of proper authorization. Accessing this endpoint requires an access token from the account owner. * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401). * (A value of 0 indicates that the error is generated client-side). @@ -401,6 +409,14 @@ export enum TalerErrorCode { /** + * The requested content type is not supported by the server. The client should try requesting a different format. + * Returned with an HTTP status code of #MHD_HTTP_NOT_ACCEPTABLE (406). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_REQUESTED_FORMAT_UNSUPPORTED = 78, + + + /** * Exchange is badly configured and thus cannot operate. * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). * (A value of 0 indicates that the error is generated client-side). @@ -801,6 +817,14 @@ export enum TalerErrorCode { /** + * The specified AML officer does not have write access at this time. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_AML_OFFICER_READ_ONLY = 1050, + + + /** * The exchange did not find information about the specified transaction in the database. * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). * (A value of 0 indicates that the error is generated client-side). @@ -954,7 +978,7 @@ export enum TalerErrorCode { /** * The total sum of amounts from the denominations did overflow. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). * (A value of 0 indicates that the error is generated client-side). */ EXCHANGE_WITHDRAW_AMOUNT_OVERFLOW = 1162, @@ -2401,11 +2425,11 @@ export enum TalerErrorCode { /** - * The merchant was unable to obtain a valid answer to /wire from the exchange. + * The master key of the exchange does not match the one configured for this merchant. As a result, we refuse to do business with this exchange. The administrator should check if they configured the exchange correctly in the merchant backend. * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). * (A value of 0 indicates that the error is generated client-side). */ - MERCHANT_GENERIC_EXCHANGE_WIRE_REQUEST_FAILED = 2002, + MERCHANT_GENERIC_EXCHANGE_MASTER_KEY_MISMATCH = 2002, /** @@ -2697,6 +2721,46 @@ export enum TalerErrorCode { /** + * The merchant does not have a charity associated with the selected Donau. As a result, it cannot generate the requested donation receipt. This could happen if the charity was removed from the backend between order creation and payment. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_DONAU_CHARITY_UNKNOWN = 2039, + + + /** + * The merchant does not expect any transfer with the given ID and can thus not return any details about it. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_EXPECTED_TRANSFER_UNKNOWN = 2040, + + + /** + * The Donau is not known to the backend. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_DONAU_UNKNOWN = 2041, + + + /** + * The access token is not known to the backend. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_ACCESS_TOKEN_UNKNOWN = 2042, + + + /** + * One of the binaries needed to generate the PDF is not installed. If this feature is required, the system administrator should make sure Typst and pdftk are both installed. + * Returned with an HTTP status code of #MHD_HTTP_NOT_IMPLEMENTED (501). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_NO_TYPST_OR_PDFTK = 2048, + + + /** * The exchange failed to provide a valid answer to the tracking request, thus those details are not in the response. * Returned with an HTTP status code of #MHD_HTTP_OK (200). * (A value of 0 indicates that the error is generated client-side). @@ -2737,8 +2801,8 @@ export enum TalerErrorCode { /** - * The contract terms version is not invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * The contract terms version is not understood by the merchant backend. Most likely the merchant backend was downgraded to a version incompatible with the content of the database. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). * (A value of 0 indicates that the error is generated client-side). */ MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_VERSION = 2107, @@ -3225,6 +3289,30 @@ export enum TalerErrorCode { /** + * The merchant backend failed to reach the banking gateway to shorten the wire transfer subject. This probably means that the banking gateway of the exchange is currently down. Contact the exchange operator or simply retry again later. + * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ACCOUNTS_KYCAUTH_BANK_GATEWAY_UNREACHABLE = 2275, + + + /** + * The merchant backend failed to reach the banking gateway to shorten the wire transfer subject. This probably means that the banking gateway of the exchange is currently down. Contact the exchange operator or simply retry again later. + * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ACCOUNTS_EXCHANGE_TOO_OLD = 2276, + + + /** + * The merchant backend failed to reach the specified exchange. This probably means that the exchange is currently down. Contact the exchange operator or simply retry again later. + * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ACCOUNTS_KYCAUTH_EXCHANGE_UNREACHABLE = 2277, + + + /** * We could not claim the order because the backend is unaware of it. * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). * (A value of 0 indicates that the error is generated client-side). @@ -3249,6 +3337,14 @@ export enum TalerErrorCode { /** + * The unclaim signature of the wallet is not valid for the given contract hash. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_UNCLAIM_SIGNATURE_INVALID = 2303, + + + /** * The backend failed to sign the refund request. * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). * (A value of 0 indicates that the error is generated client-side). @@ -3441,6 +3537,14 @@ export enum TalerErrorCode { /** + * The client requested a report granularity that is not available at the backend. Possible solutions include extending the backend code and/or the database statistic triggers to support the desired data granularity. Alternatively, the client could request a different granularity. + * Returned with an HTTP status code of #MHD_HTTP_GONE (410). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_GET_STATISTICS_REPORT_GRANULARITY_UNAVAILABLE = 2525, + + + /** * The amount to be refunded is inconsistent: either is lower than the previous amount being awarded, or it exceeds the original price paid by the customer. * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). * (A value of 0 indicates that the error is generated client-side). @@ -3537,7 +3641,7 @@ export enum TalerErrorCode { /** - * The backend could not persist the wire transfer due to the state of the backend. This usually means that the bank account specified is not known to the backend for this instance. + * The backend could not persist the wire transfer due to the state of the backend. This usually means that a wire transfer with the same wire transfer subject but a different amount was previously submitted to the backend. * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). * (A value of 0 indicates that the error is generated client-side). */ @@ -3633,6 +3737,14 @@ export enum TalerErrorCode { /** + * The bank account specified is not acceptable for this exchange. The exchange either does not support the wire method or something else about the specific account. Consult the exchange account constraints and specify a different bank account if you want to use this exchange. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_ACCOUNT_NOT_ELIGIBLE_FOR_EXCHANGE = 2628, + + + /** * The product ID exists. * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). * (A value of 0 indicates that the error is generated client-side). @@ -3833,6 +3945,30 @@ export enum TalerErrorCode { /** + * The selected template has a different type than the one specified in the request of the client. This may happen if the template was updated since the last time the client fetched it. The client should re-fetch the current template and send a request of the correct type. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_USING_TEMPLATES_WRONG_TYPE = 2864, + + + /** + * The selected template does not allow one of the specified products to be included in the order. This may happen if the template was updated since the last time the client fetched it. The client should re-fetch the current template and send a request of the correct type. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_USING_TEMPLATES_WRONG_PRODUCT = 2865, + + + /** + * The selected combination of products does not allow the backend to compute a price for the order in any of the supported currencies. This may happen if the template was updated since the last time the client fetched it or if the wallet assembled an unsupported combination of products. The site administrator might want to specify additional prices for products, while the client should re-fetch the current template and send a request with a combination of products for which prices exist in the same currency. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_USING_TEMPLATES_NO_CURRENCY = 2866, + + + /** * The webhook ID elready exists. * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). * (A value of 0 indicates that the error is generated client-side). @@ -4345,6 +4481,54 @@ export enum TalerErrorCode { /** + * This subject format is not supported. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_UNSUPPORTED_SUBJECT_FORMAT = 5158, + + + /** + * The derived subject is already used. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_DERIVATION_REUSE = 5159, + + + /** + * The provided signature is invalid. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_BAD_SIGNATURE = 5160, + + + /** + * The provided timestamp is too old. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_OLD_TIMESTAMP = 5161, + + + /** + * The authorization_pub for a request to transfer funds has already been used for another non recurrent transfer. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_TRANSFER_MAPPING_REUSED = 5162, + + + /** + * The authorization_pub for a request to transfer funds is not currently registered. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_TRANSFER_MAPPING_UNKNOWN = 5163, + + + /** * The sync service failed find the account in its database. * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). * (A value of 0 indicates that the error is generated client-side). @@ -5385,6 +5569,14 @@ export enum TalerErrorCode { /** + * The requested operation is not valid for the cipher used by the selected denomination. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + DONAU_GENERIC_INVALID_DENOMINATION_CIPHER_FOR_OPERATION = 8606, + + + /** * The Donau failed to perform the operation as it could not find the private keys. This is a problem with the Donau setup, not with the client's request. * Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503). * (A value of 0 indicates that the error is generated client-side). diff --git a/packages/taler-util/src/types-taler-prepared-transfer.ts b/packages/taler-util/src/types-taler-prepared-transfer.ts @@ -0,0 +1,233 @@ +/* + This file is part of GNU Taler + (C) 2026 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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 Affero General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + + SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { + Codec, + buildCodecForObject, + buildCodecForUnion, + codecForBoolean, + codecForConstString, + codecForEither, + codecForList, + codecForString, + codecForStringURL, + codecOptional, +} from "./codec.js"; +import { TalerPreparedTransferApi } from "./index.js"; +import { codecForAmountString } from "./amounts.js"; +import { codecForTimestamp } from "./time.js"; +import { + AmountString, + EddsaPublicKey, + EddsaSignatureString as EddsaSignature, + Timestamp, + codecForEddsaPublicKey, + codecForEddsaSignature +} from "./types-taler-common.js"; + +export type SubjectFormat = + | "SIMPLE" + | "URI" + | "CH_QR_BILL"; + +export interface PreparedTransferConfig { + // Name of the API. + name: "taler-prepared-transfer"; + + // libtool-style representation of the protocol version, see + // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning + // The format is "current:revision:age". + version: string; + + // Currency used by this API. + currency: string; + + // URN of the implementation (needed to interpret 'revision' in version). + // @since v0, may become mandatory in the future. + implementation?: string; + + // Supported formats for registration, there must at least one. + supported_formats: SubjectFormat[]; +} + +export interface RegistrationRequest { + // Amount to transfer + credit_amount: AmountString; + + // Transfer types + type: "reserve" | "kyc"; + + // Public key algorithm + alg: "EdDSA"; + + // Account public key for the exchange + account_pub: EddsaPublicKey; + + // Public key encoded inside the subject + authorization_pub: EddsaPublicKey; + + // Signature of the account_pub key using the authorization_pub private key + authorization_sig: EddsaSignature; + + // Whether the authorization_pub will be reused for recurrent transfers + // Disable bounces in case of authorization_pub reuse + recurrent: boolean; +} + +// Union discriminated by the "type" field. +export type TransferSubject = + | SimpleSubject + | UriSubject + | SwissQrBillSubject; + +export interface SimpleSubject { + // Subject for system accepting large subjects + type: "SIMPLE"; + + // Amount to transfer + credit_amount: AmountString; + + // Encoded string containing either the full key and transfer type or a + // derived short subject + subject: string; +} + +export interface UriSubject { + // Subject for system accepting prepared payments + type: "URI"; + + // Amount to transfer + credit_amount: AmountString; + + // Prepared payments confirmation URI + uri: string; +} + +export interface SwissQrBillSubject { + // Subject for Swiss QR Bill + type: "CH_QR_BILL"; + + // Amount to transfer + credit_amount: AmountString; + + // 27-digit QR Reference number + qr_reference_number: string; +} + +export interface RegistrationResponse { + // The transfer subject encoded in all supported formats + subjects: TransferSubject[]; + + // Expiration date after which this subject is expected to be reused + expiration: Timestamp; +} + +export interface Unregistration { + // Current timestamp in the ISO 8601 + timestamp: string; + + // Public key used for registration + authorization_pub: EddsaPublicKey; + + // Signature of the timestamp using the authorization_pub private key + // Prevent replay attack + authorization_sig: EddsaSignature; +} + +export const codeForSubjectFormat = + (): Codec<TalerPreparedTransferApi.SubjectFormat> => + codecForEither( + codecForConstString("SIMPLE"), + codecForConstString("URI"), + codecForConstString("CH_QR_BILL"), + ); + +export const codecForPreparedTransferConfig = + (): Codec<TalerPreparedTransferApi.PreparedTransferConfig> => + buildCodecForObject<TalerPreparedTransferApi.PreparedTransferConfig>() + .property("currency", codecForString()) + .property("implementation", codecOptional(codecForString())) + .property("name", codecForConstString("taler-prepared-transfer")) + .property("supported_formats", codecForList(codeForSubjectFormat())) + .property("version", codecForString()) + .build("TalerPreparedTransferApi.PreparedTransferConfig"); + +export const codecForRegistrationRequest = + (): Codec<TalerPreparedTransferApi.RegistrationRequest> => + buildCodecForObject<TalerPreparedTransferApi.RegistrationRequest>() + .property("credit_amount", codecForAmountString()) + .property("type", codecForEither( + codecForConstString("reserve"), + codecForConstString("kyc"), + )) + .property("alg", codecForEither( + codecForConstString("EdDSA") + )) + .property("account_pub", codecForEddsaPublicKey()) + .property("authorization_pub", codecForEddsaPublicKey()) + .property("authorization_sig", codecForEddsaSignature()) + .property("recurrent", codecForBoolean()) + .build("TalerWireGatewayApi.RegistrationRequest"); + +export const codecForTransferSubject = + (): Codec<TalerPreparedTransferApi.TransferSubject> => + buildCodecForUnion<TalerPreparedTransferApi.TransferSubject>() + .discriminateOn("type") + .alternative("SIMPLE", codecForSimpleSubject()) + .alternative("URI", codecForUriSubject()) + .alternative("CH_QR_BILL", codecForSwissQrBillSubject()) + .build("TalerPreparedTransferApi.TransferSubject"); + +export const codecForSimpleSubject = + (): Codec<TalerPreparedTransferApi.SimpleSubject> => + buildCodecForObject<TalerPreparedTransferApi.SimpleSubject>() + .property("type", codecForConstString("SIMPLE")) + .property("credit_amount", codecForAmountString()) + .property("subject", codecForString()) + .build("TalerPreparedTransferApi.SimpleSubject"); + +export const codecForUriSubject = + (): Codec<TalerPreparedTransferApi.UriSubject> => + buildCodecForObject<TalerPreparedTransferApi.UriSubject>() + .property("type", codecForConstString("URI")) + .property("credit_amount", codecForAmountString()) + .property("uri", codecForStringURL()) + .build("TalerPreparedTransferApi.UriSubject"); + +export const codecForSwissQrBillSubject = + (): Codec<TalerPreparedTransferApi.SwissQrBillSubject> => + buildCodecForObject<TalerPreparedTransferApi.SwissQrBillSubject>() + .property("type", codecForConstString("CH_QR_BILL")) + .property("credit_amount", codecForAmountString()) + .property("qr_reference_number", codecForString()) + .build("TalerPreparedTransferApi.SwissQrBillSubject"); + +export const codecForRegistrationResponse = + (): Codec<TalerPreparedTransferApi.RegistrationResponse> => + buildCodecForObject<TalerPreparedTransferApi.RegistrationResponse>() + .property("subjects", codecForList(codecForTransferSubject())) + .property("expiration", codecForTimestamp) + .build("TalerWireGatewayApi.RegistrationResponse"); + +export const codecForUnregistration = + (): Codec<TalerPreparedTransferApi.Unregistration> => + buildCodecForObject<TalerPreparedTransferApi.Unregistration>() + .property("timestamp", codecForString()) + .property("authorization_pub", codecForEddsaPublicKey()) + .property("authorization_sig", codecForEddsaSignature()) + .build("TalerPreparedTransferApi.Unregistration"); +\ No newline at end of file