taler-typescript-core

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

commit dfb2e8735a84f00ca5d6b3b79833a78d0293606e
parent e4fe7896d82540b70a33fffd8c0066a378bebd14
Author: Sebastian <sebasjm@gmail.com>
Date:   Wed, 24 Sep 2025 09:55:49 -0300

fixing getConfig of all http api

Diffstat:
Mpackages/merchant-backoffice-ui/src/paths/login/index.tsx | 3+++
Mpackages/taler-harness/src/index.ts | 28++++------------------------
Mpackages/taler-harness/src/integrationtests/test-donau-minus-t.ts | 17++++++++---------
Mpackages/taler-harness/src/integrationtests/test-donau.ts | 17++++++++---------
Mpackages/taler-util/src/http-client/README.md | 22+++++++++++++++++++++-
Mpackages/taler-util/src/http-client/bank-conversion.ts | 17+++++++++++------
Mpackages/taler-util/src/http-client/bank-core.ts | 45++++++++-------------------------------------
Mpackages/taler-util/src/http-client/bank-integration.ts | 30++++++++++++++----------------
Mpackages/taler-util/src/http-client/bank-revenue.ts | 28++++++++++++----------------
Mpackages/taler-util/src/http-client/bank-wire.ts | 38++++++++++++++++++++++++++++++++------
Mpackages/taler-util/src/http-client/challenger.ts | 9++++++---
Mpackages/taler-util/src/http-client/donau-client.ts | 363++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mpackages/taler-util/src/http-client/exchange-client.ts | 45++++++++++++---------------------------------
Mpackages/taler-util/src/http-client/merchant.ts | 50+++++++++++---------------------------------------
Mpackages/taler-util/src/http-common.ts | 1+
Mpackages/taler-util/src/operation.ts | 44++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-util/src/types-donau.ts | 116+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mpackages/taler-util/src/types-taler-bank-integration.ts | 10+++++-----
Mpackages/taler-util/src/types-taler-common.ts | 1-
Mpackages/taler-util/src/types-taler-wire-gateway.ts | 32+++++++++++++++++++++++++++++++-
Mpackages/taler-wallet-core/src/donau.ts | 26++++++++++++--------------
Mpackages/taler-wallet-core/src/withdraw.ts | 27+++------------------------
Mpackages/web-util/src/context/bank-api.ts | 2+-
Mpackages/web-util/src/context/challenger-api.ts | 2+-
Mpackages/web-util/src/context/merchant-api.ts | 2+-
25 files changed, 623 insertions(+), 352 deletions(-)

diff --git a/packages/merchant-backoffice-ui/src/paths/login/index.tsx b/packages/merchant-backoffice-ui/src/paths/login/index.tsx @@ -25,6 +25,7 @@ import { HttpStatusCode, LoginTokenRequest, LoginTokenScope, + TalerError, TranslatedString, } from "@gnu-taler/taler-util"; import { @@ -109,10 +110,12 @@ export function LoginPage(_p: Props): VNode { } } } catch (error) { + const details = error instanceof TalerError ? JSON.stringify(error.errorDetail) : undefined setNotif({ message: i18n.str`Failed to login.`, type: "ERROR", description: error instanceof Error ? error.message : undefined, + details }); } }, diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts @@ -879,28 +879,8 @@ deploymentCli httpLib, ); - const bc = await bank.getConfig(); - if (bc.type === "fail") { - logger.error(`couldn't get bank config. ${bc.detail?.hint}`); - return; - } - if (!bank.isCompatible(bc.body.version)) { - logger.error( - `bank server version is not compatible: ${bc.body.version}, client version: ${bank.PROTOCOL_VERSION}`, - ); - return; - } - const mc = await merchantManager.getConfig(); - if (mc.type === "fail") { - logger.error(`couldn't get merchant config. ${mc.detail?.hint}`); - return; - } - if (!merchantManager.isCompatible(mc.body.version)) { - logger.error( - `merchant server version is not compatible: ${mc.body.version}, client version: ${merchantManager.PROTOCOL_VERSION}`, - ); - return; - } + const bc = succeedOrThrow(await bank.getConfig()); + const mc = succeedOrThrow(await merchantManager.getConfig()); let bankAdminToken: AccessToken | undefined; if (bankAdminPassword) { @@ -1049,8 +1029,8 @@ deploymentCli * create template */ if (args.provisionBankMerchant.template) { - let currency = bc.body.currency; - if (bc.body.allow_conversion) { + let currency = bc.currency; + if (bc.allow_conversion) { const cc = await conv.getConfig(); if (cc.type === "ok") { currency = cc.body.fiat_currency; diff --git a/packages/taler-harness/src/integrationtests/test-donau-minus-t.ts b/packages/taler-harness/src/integrationtests/test-donau-minus-t.ts @@ -18,6 +18,7 @@ * Imports. */ import { + AccessToken, ConfirmPayResultType, DonauHttpClient, j2s, @@ -90,15 +91,13 @@ export async function runDonauMinusTTest(t: GlobalTestState) { const currentYear = new Date().getFullYear(); const charityResp = succeedOrThrow( - await donauClient.postCharity({ - body: { - charity_pub: merchantPub, - current_year: currentYear, - max_per_year: "TESTKUDOS:1000", - charity_name: "42", - receipts_to_date: "TESTKUDOS:0", - charity_url: merchant.makeInstanceBaseUrl(), - }, + await donauClient.createCharity("" as AccessToken, { + charity_pub: merchantPub, + current_year: currentYear, + max_per_year: "TESTKUDOS:1000", + charity_name: "42", + receipts_to_date: "TESTKUDOS:0", + charity_url: merchant.makeInstanceBaseUrl(), }), ); diff --git a/packages/taler-harness/src/integrationtests/test-donau.ts b/packages/taler-harness/src/integrationtests/test-donau.ts @@ -18,6 +18,7 @@ * Imports. */ import { + AccessToken, ConfirmPayResultType, DonauHttpClient, j2s, @@ -91,15 +92,13 @@ export async function runDonauTest(t: GlobalTestState) { const currentYear = new Date().getFullYear(); const charityResp = succeedOrThrow( - await donauClient.postCharity({ - body: { - charity_pub: merchantPub, - current_year: currentYear, - max_per_year: "TESTKUDOS:1000", - charity_name: "42", - receipts_to_date: "TESTKUDOS:0", - charity_url: merchant.makeInstanceBaseUrl(), - }, + await donauClient.createCharity("" as AccessToken, { + charity_pub: merchantPub, + current_year: currentYear, + max_per_year: "TESTKUDOS:1000", + charity_name: "42", + receipts_to_date: "TESTKUDOS:0", + charity_url: merchant.makeInstanceBaseUrl(), }), ); diff --git a/packages/taler-util/src/http-client/README.md b/packages/taler-util/src/http-client/README.md @@ -1,4 +1,4 @@ -## HTTP Cclients +## HTTP Clients This folder contain class or function specifically designed to facilitate HTTP client interactions with a the core systems. @@ -17,3 +17,23 @@ These API defines: 4. **Error Handling**: Providing robust error handling and retry mechanisms for failed HTTP requests, including logging and potentially user notifications for critical failures. 5. **Data Validation**: Before sending requests, it could validate the data to ensure it meets the API's expected format, types, and value ranges, reducing the likelihood of errors and improving system reliability. + +## Design desicions + +The https clients follows this decisions for consistency. + +To make it easier to keep track with the spec: + * One API component per file. + * All functions should have a comment that points to the respective Spec + * All functions should be in order as described in the spec + +To make it easier to spot incomatible usage use strong typing: + * If the request ended succesfully (the operation is commited or the data return is the expected) then return OperationOk + * If the operation fail with an error code documented in the spec then return OperationFail (or OperationAlternative if the failure contains a body) + * If the response contains any other error or the response is unreadable then throw an TalerError with all the information + * Function arguments should use types from the protocol when possible + +To make it easier to reuse + * Class instance should be thread safe and methods should be reentrant + * Only use Taler HttpRequestLibrary interface to make a request + * Do not expose http: URL parameters and Headers can be string arguments diff --git a/packages/taler-util/src/http-client/bank-conversion.ts b/packages/taler-util/src/http-client/bank-conversion.ts @@ -18,17 +18,17 @@ * Imports. */ import { AmountJson, Amounts } from "../amounts.js"; -import { HttpRequestLibrary, readTalerErrorResponse } from "../http-common.js"; +import { HttpRequestLibrary } from "../http-common.js"; import { HttpStatusCode } from "../http-status-codes.js"; import { createPlatformHttpLib } from "../http.js"; import { LibtoolVersion } from "../libtool-version.js"; import { FailCasesByMethod, ResultByMethod, + carefullyParseConfig, opEmptySuccess, opKnownHttpFailure, opSuccessFromHttp, - opUnknownFailure, opUnknownHttpFailure, } from "../operation.js"; import { TalerErrorCode } from "../taler-error-codes.js"; @@ -62,7 +62,7 @@ export enum TalerBankConversionCacheEviction { * The API is used by the wallets. */ export class TalerBankConversionHttpClient { - public readonly PROTOCOL_VERSION = "0:0:0"; + public static readonly PROTOCOL_VERSION = "0:0:0"; httpLib: HttpRequestLibrary; cacheEvictor: CacheEvictor<TalerBankConversionCacheEviction>; @@ -76,7 +76,7 @@ export class TalerBankConversionHttpClient { this.cacheEvictor = cacheEvictor ?? nullEvictor; } - isCompatible(version: string): boolean { + static isCompatible(version: string): boolean { const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version); return compare?.compatible ?? false; } @@ -92,7 +92,12 @@ export class TalerBankConversionHttpClient { }); switch (resp.status) { case HttpStatusCode.Ok: - return opSuccessFromHttp(resp, codecForConversionBankConfig()); + return carefullyParseConfig( + "taler-conversion-info", + TalerBankConversionHttpClient.PROTOCOL_VERSION, + resp, + codecForConversionBankConfig(), + ); case HttpStatusCode.NotImplemented: return opKnownHttpFailure(resp.status, resp); default: @@ -112,7 +117,7 @@ export class TalerBankConversionHttpClient { } const resp = await this.httpLib.fetch(url.href, { method: "GET", - headers + headers, }); switch (resp.status) { case HttpStatusCode.Ok: diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts @@ -20,16 +20,15 @@ import { HttpStatusCode, LibtoolVersion, LongPollParams, - OperationAlternative, OperationFail, OperationOk, PaginationParams, TalerError, TalerErrorCode, TokenRequest, - UserAndPassword, UserAndToken, assertUnreachable, + carefullyParseConfig, codecForTalerCommonConfigResponse, codecForTokenInfoList, codecForTokenSuccessResponse, @@ -60,7 +59,6 @@ import { CashoutRequest, ConversionRateClassInput, CreateTransactionRequest, - CreateTransactionResponse, MonitorTimeframeParam, RegisterAccountRequest, TalerCorebankConfigResponse, @@ -84,7 +82,6 @@ import { codecForWithdrawalPublicInfo, } from "../types-taler-corebank.js"; import { - ChallengeResponse, ChallengeSolveRequest, codecForChallengeResponse, } from "../types-taler-merchant.js"; @@ -138,7 +135,7 @@ export type BearerCredentials = { * Uses libtool's current:revision:age versioning. */ export class TalerCoreBankHttpClient { - public readonly PROTOCOL_VERSION = "10:0:2"; + public static readonly PROTOCOL_VERSION = "10:0:2"; httpLib: HttpRequestLibrary; cacheEvictor: CacheEvictor<TalerCoreBankCacheEviction>; @@ -151,7 +148,7 @@ export class TalerCoreBankHttpClient { this.cacheEvictor = cacheEvictor ?? nullEvictor; } - isCompatible(version: string): boolean { + static isCompatible(version: string): boolean { const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version); return compare?.compatible ?? false; } @@ -282,45 +279,19 @@ export class TalerCoreBankHttpClient { * https://docs.taler.net/core/api-corebank.html#config * */ - async getConfig(): Promise< - | OperationFail<HttpStatusCode.NotFound> - | OperationOk<TalerCorebankConfigResponse> - > { + 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: { - const minBody = await readSuccessResponseJsonOrThrow( - resp, - codecForTalerCommonConfigResponse(), - ); - // FIXME: Re-enable the check once fakebank and libeufin-bank return the name. - // const expectedName = "taler-corebank"; - // if (minBody.name !== expectedName) { - // throw TalerError.fromUncheckedDetail({ - // code: TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR, - // requestUrl: resp.requestUrl, - // httpStatusCode: resp.status, - // detail: `Unexpected server component name (got ${minBody.name}, expected ${expectedName}})`, - // }); - // } - if (!this.isCompatible(minBody.version)) { - throw TalerError.fromUncheckedDetail({ - code: TalerErrorCode.GENERIC_CLIENT_UNSUPPORTED_PROTOCOL_VERSION, - requestUrl: resp.requestUrl, - httpStatusCode: resp.status, - detail: `Unsupported protocol version, client supports ${this.PROTOCOL_VERSION}, server supports ${minBody.version}`, - }); - } - // Now that we've checked the basic body, re-parse the full response. - const body = await readSuccessResponseJsonOrThrow( + case HttpStatusCode.Ok: + return carefullyParseConfig( + "taler-corebank", + TalerCoreBankHttpClient.PROTOCOL_VERSION, resp, codecForCoreBankConfig(), ); - return opFixedSuccess(body); - } case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); default: diff --git a/packages/taler-util/src/http-client/bank-integration.ts b/packages/taler-util/src/http-client/bank-integration.ts @@ -21,23 +21,20 @@ import { LibtoolVersion } from "../libtool-version.js"; import { Logger } from "../logging.js"; import { FailCasesByMethod, - OperationFail, - OperationOk, ResultByMethod, + carefullyParseConfig, opEmptySuccess, opKnownHttpFailure, opKnownTalerFailure, opSuccessFromHttp, - opUnknownFailure, - opUnknownHttpFailure, + opUnknownHttpFailure } from "../operation.js"; import { TalerErrorCode } from "../taler-error-codes.js"; import { BankWithdrawalOperationPostRequest, - BankWithdrawalOperationStatus, WithdrawalOperationStatusFlag, codecForBankWithdrawalOperationPostResponse, - codecForBankWithdrawalOperationStatus, + codecForBankWithdrawalOperationStatus } from "../types-taler-bank-integration.js"; import { LongPollParams } from "../types-taler-common.js"; import { codecForIntegrationBankConfig } from "../types-taler-corebank.js"; @@ -57,9 +54,7 @@ const logger = new Logger("bank-integration.ts"); * The API is used by the wallets. */ export class TalerBankIntegrationHttpClient { - public static readonly PROTOCOL_VERSION = "2:0:1"; - public readonly PROTOCOL_VERSION = - TalerBankIntegrationHttpClient.PROTOCOL_VERSION; + public static readonly PROTOCOL_VERSION = "5:0:0"; httpLib: HttpRequestLibrary; @@ -70,7 +65,7 @@ export class TalerBankIntegrationHttpClient { this.httpLib = httpClient ?? createPlatformHttpLib(); } - isCompatible(version: string): boolean { + static isCompatible(version: string): boolean { const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version); return compare?.compatible ?? false; } @@ -86,9 +81,13 @@ export class TalerBankIntegrationHttpClient { }); switch (resp.status) { case HttpStatusCode.Ok: - return opSuccessFromHttp(resp, codecForIntegrationBankConfig()); + return carefullyParseConfig( + "taler-bank-integration", + TalerBankIntegrationHttpClient.PROTOCOL_VERSION, + resp, + codecForIntegrationBankConfig(), + ); default: - logger.warn(`config request failed, status ${resp.status}`); return opUnknownHttpFailure(resp); } } @@ -102,10 +101,7 @@ export class TalerBankIntegrationHttpClient { params?: { old_state?: WithdrawalOperationStatusFlag; } & LongPollParams, - ): Promise< - | OperationOk<BankWithdrawalOperationStatus> - | OperationFail<HttpStatusCode.NotFound> - > { + ) { const url = new URL(`withdrawal-operation/${woid}`, this.baseUrl); addLongPollingParam(url, params); if (params) { @@ -153,6 +149,8 @@ export class TalerBankIntegrationHttpClient { const body = await readTalerErrorResponse(resp); const details = codecForTalerErrorDetail().decode(body); switch (details.code) { + case TalerErrorCode.BANK_UPDATE_ABORT_CONFLICT: + return opKnownTalerFailure(details.code, details); case TalerErrorCode.BANK_WITHDRAWAL_OPERATION_RESERVE_SELECTION_CONFLICT: return opKnownTalerFailure(details.code, details); case TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT: diff --git a/packages/taler-util/src/http-client/bank-revenue.ts b/packages/taler-util/src/http-client/bank-revenue.ts @@ -14,28 +14,20 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { - HttpRequestLibrary, - makeBasicAuthHeader, - readTalerErrorResponse, -} from "../http-common.js"; +import { HttpRequestLibrary } from "../http-common.js"; import { HttpStatusCode } from "../http-status-codes.js"; import { createPlatformHttpLib } from "../http.js"; import { LibtoolVersion } from "../libtool-version.js"; import { + carefullyParseConfig, FailCasesByMethod, - ResultByMethod, opFixedSuccess, opKnownHttpFailure, opSuccessFromHttp, - opUnknownFailure, opUnknownHttpFailure, + ResultByMethod, } from "../operation.js"; -import { - AccessToken, - LongPollParams, - PaginationParams, -} from "../types-taler-common.js"; +import { LongPollParams, PaginationParams } from "../types-taler-common.js"; import { codecForRevenueConfig, codecForRevenueIncomingHistory, @@ -54,7 +46,6 @@ export type TalerBankRevenueErrorsByMethod< prop extends keyof TalerRevenueHttpClient, > = FailCasesByMethod<TalerRevenueHttpClient, prop>; - /** * The API is used by the merchant (or other parties) to query * for incoming transactions to their account. @@ -69,9 +60,9 @@ export class TalerRevenueHttpClient { this.httpLib = httpClient ?? createPlatformHttpLib(); } - public readonly PROTOCOL_VERSION = "1:0:0"; + public static readonly PROTOCOL_VERSION = "1:0:0"; - isCompatible(version: string): boolean { + static isCompatible(version: string): boolean { const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version); return compare?.compatible ?? false; } @@ -90,7 +81,12 @@ export class TalerRevenueHttpClient { }); switch (resp.status) { case HttpStatusCode.Ok: - return opSuccessFromHttp(resp, codecForRevenueConfig()); + return carefullyParseConfig( + "taler-revenue", + TalerRevenueHttpClient.PROTOCOL_VERSION, + resp, + codecForRevenueConfig(), + ); case HttpStatusCode.Unauthorized: return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotFound: diff --git a/packages/taler-util/src/http-client/bank-wire.ts b/packages/taler-util/src/http-client/bank-wire.ts @@ -14,11 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { - HttpRequestLibrary, - makeBasicAuthHeader, - readTalerErrorResponse, -} from "../http-common.js"; +import { HttpRequestLibrary, makeBasicAuthHeader } from "../http-common.js"; import { HttpStatusCode } from "../http-status-codes.js"; import { createPlatformHttpLib } from "../http.js"; import { @@ -27,7 +23,6 @@ import { opFixedSuccess, opKnownHttpFailure, opSuccessFromHttp, - opUnknownFailure, opUnknownHttpFailure, } from "../operation.js"; import { @@ -41,6 +36,7 @@ import { addLongPollingParam, addPaginationParams } from "./utils.js"; import { LongPollParams, PaginationParams } from "../types-taler-common.js"; import * as TalerWireGatewayApi from "../types-taler-wire-gateway.js"; +import { carefullyParseConfig, LibtoolVersion } from "../index.js"; export type TalerWireGatewayResultByMethod< prop extends keyof TalerWireGatewayHttpClient, @@ -63,6 +59,7 @@ export interface TalerWireGatewayAuth { */ export class TalerWireGatewayHttpClient { httpLib: HttpRequestLibrary; + public static readonly PROTOCOL_VERSION = "4:0:0"; constructor( readonly baseUrl: string, @@ -73,6 +70,35 @@ export class TalerWireGatewayHttpClient { 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-wire.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-wire-gateway", + TalerWireGatewayHttpClient.PROTOCOL_VERSION, + resp, + TalerWireGatewayApi.codecForWireConfigResponse(), + ); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + /** * https://docs.taler.net/core/api-bank-wire.html#post--transfer * diff --git a/packages/taler-util/src/http-client/challenger.ts b/packages/taler-util/src/http-client/challenger.ts @@ -26,6 +26,7 @@ import { LibtoolVersion } from "../libtool-version.js"; import { FailCasesByMethod, ResultByMethod, + carefullyParseConfig, opKnownAlternativeHttpFailure, opKnownHttpFailure, opSuccessFromHttp, @@ -64,7 +65,7 @@ export enum ChallengerCacheEviction { export class ChallengerHttpClient { httpLib: HttpRequestLibrary; cacheEvictor: CacheEvictor<ChallengerCacheEviction>; - public readonly PROTOCOL_VERSION = "2:0:0"; + public static readonly PROTOCOL_VERSION = "2:0:0"; constructor( readonly baseUrl: string, @@ -75,7 +76,7 @@ export class ChallengerHttpClient { this.cacheEvictor = cacheEvictor ?? nullEvictor; } - isCompatible(version: string): boolean { + static isCompatible(version: string): boolean { const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version); return compare?.compatible ?? false; } @@ -90,7 +91,9 @@ export class ChallengerHttpClient { }); switch (resp.status) { case HttpStatusCode.Ok: - return opSuccessFromHttp( + return carefullyParseConfig( + "challenger", + ChallengerHttpClient.PROTOCOL_VERSION, resp, codecForChallengerTermsOfServiceResponse(), ); diff --git a/packages/taler-util/src/http-client/donau-client.ts b/packages/taler-util/src/http-client/donau-client.ts @@ -14,42 +14,36 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { - HttpRequestLibrary, - HttpRequestOptions, - HttpResponse, - readSuccessResponseJsonOrThrow, -} from "../http-common.js"; +import { HttpRequestLibrary } from "../http-common.js"; import { HttpStatusCode } from "../http-status-codes.js"; import { createPlatformHttpLib } from "../http.js"; import { LibtoolVersion } from "../libtool-version.js"; import { - OperationFail, - OperationOk, + carefullyParseConfig, + opEmptySuccess, opFixedSuccess, + opKnownFailure, opKnownHttpFailure, opSuccessFromHttp, opUnknownHttpFailure, } from "../operation.js"; -import { TalerError } from "../errors.js"; -import { - CancellationToken, - codecForTalerCommonConfigResponse, - LongpollQueue, -} from "../index.js"; -import { TalerErrorCode } from "../taler-error-codes.js"; +import { AccessToken, Codec, codecForAny } from "../index.js"; import { + BlindedDonationReceiptSignatures, + Charities, + Charity, CharityRequest, codecForDonauCharityResponse, codecForDonauDonationStatementResponse, codecForDonauKeysResponse, codecForDonauVersionResponse, - DonauCharityResponse, - DonauKeysResponse, - DonauVersionResponse, + codecForIssuePrepareResponse, + IssuePrepareRequest, + IssueReceiptsRequest, SubmitDonationReceiptsRequest, } from "../types-donau.js"; +import { makeBearerTokenAuthHeader } from "./utils.js"; /** * Client library for the GNU Taler donau service. @@ -57,25 +51,18 @@ import { export class DonauHttpClient { public static readonly SUPPORTED_DONAU_PROTOCOL_VERSION = "0:0:0"; private httpLib: HttpRequestLibrary; - private cancelationToken: CancellationToken; - private longPollQueue: LongpollQueue; constructor( readonly baseUrl: string, params: { httpClient?: HttpRequestLibrary; preventCompression?: boolean; - cancelationToken?: CancellationToken; - longPollQueue?: LongpollQueue; } = {}, ) { this.httpLib = params.httpClient ?? createPlatformHttpLib(); - this.cancelationToken = - params.cancelationToken ?? CancellationToken.CONTINUE; - this.longPollQueue = params.longPollQueue ?? new LongpollQueue(); } - isCompatible(version: string): boolean { + static isCompatible(version: string): boolean { const compare = LibtoolVersion.compare( DonauHttpClient.SUPPORTED_DONAU_PROTOCOL_VERSION, version, @@ -83,69 +70,63 @@ export class DonauHttpClient { return compare?.compatible ?? false; } - private async fetch( - url_or_path: URL | string, - opts: HttpRequestOptions = {}, - longpoll: boolean = false, - ): Promise<HttpResponse> { - const url = - typeof url_or_path == "string" - ? new URL(url_or_path, this.baseUrl) - : url_or_path; - if (longpoll || url.searchParams.has("timeout_ms")) { - return this.longPollQueue.run( - url, - this.cancelationToken, - async (timeoutMs) => { - url.searchParams.set("timeout_ms", String(timeoutMs)); - return this.httpLib.fetch(url.href, { - cancellationToken: this.cancelationToken, - ...opts, - }); - }, - ); - } else { - return this.httpLib.fetch(url.href, { - cancellationToken: this.cancelationToken, - ...opts, - }); + /** + * https://docs.taler.net/core/api-donau.html#get--keys + * + * @returns + */ + async getKeys() { + const url = new URL(`keys`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForDonauKeysResponse()); + default: + return opUnknownHttpFailure(resp); } } - async getConfig(): Promise< - OperationFail<HttpStatusCode.NotFound> | OperationOk<DonauVersionResponse> - > { - const resp = await this.fetch("config"); + /** + * https://docs.taler.net/core/api-donau.html#get--seed + * + */ + async getSeed() { + const url = new URL(`keys`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + }); switch (resp.status) { - case HttpStatusCode.Ok: { - const minBody = await readSuccessResponseJsonOrThrow( - resp, - codecForTalerCommonConfigResponse(), - ); - const expectedName = "donau"; - if (minBody.name !== expectedName) { - throw TalerError.fromUncheckedDetail({ - code: TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR, - requestUrl: resp.requestUrl, - httpStatusCode: resp.status, - detail: `Unexpected server component name (got ${minBody.name}, expected ${expectedName}})`, - }); - } - if (!this.isCompatible(minBody.version)) { - throw TalerError.fromUncheckedDetail({ - code: TalerErrorCode.GENERIC_CLIENT_UNSUPPORTED_PROTOCOL_VERSION, - requestUrl: resp.requestUrl, - httpStatusCode: resp.status, - detail: `Unsupported protocol version, client supports ${DonauHttpClient.SUPPORTED_DONAU_PROTOCOL_VERSION}, server supports ${minBody.version}`, - }); - } - // Now that we've checked the basic body, re-parse the full response. - const body = await readSuccessResponseJsonOrThrow( + case HttpStatusCode.Ok: + const buffer = await resp.bytes(); + const uintar = new Uint8Array(buffer); + return opFixedSuccess(uintar); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-donau.html#get--config + * + * @returns + */ + 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( + "donau", + DonauHttpClient.SUPPORTED_DONAU_PROTOCOL_VERSION, resp, codecForDonauVersionResponse(), ); - return opFixedSuccess(body); - } case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); default: @@ -153,57 +134,233 @@ export class DonauHttpClient { } } - async postCharity(args: { - body: CharityRequest; - }): Promise<OperationOk<DonauCharityResponse>> { - const resp = await this.fetch("charities", { + /** + * https://docs.taler.net/core/api-donau.html#csr-issue + * + * @param args + * @returns + */ + async prepareIssueReceipt(body: IssuePrepareRequest) { + const url = new URL(`csr-issue`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { method: "POST", - body: args.body, + body, }); switch (resp.status) { - case HttpStatusCode.Created: - return opSuccessFromHttp(resp, codecForDonauCharityResponse()); + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForIssuePrepareResponse()); + case HttpStatusCode.NotFound: + return opKnownFailure(resp.status); + case HttpStatusCode.Gone: + return opKnownFailure(resp.status); default: return opUnknownHttpFailure(resp); } } - async postBatchSubmit(args: { body: SubmitDonationReceiptsRequest }) { - const resp = await this.fetch("batch-submit", { + /** + * https://docs.taler.net/core/api-donau.html#post--batch-issue-$CHARITY_ID + * + * @param args + * @returns + */ + async issueReceipts(charityId: number, body: IssueReceiptsRequest) { + const url = new URL(`batch-issue/${charityId}`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { method: "POST", - body: args.body, + body, + }); + //FIXME: incomplete + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp( + resp, + codecForAny() as Codec<BlindedDonationReceiptSignatures>, + ); + case HttpStatusCode.Forbidden: + return opKnownFailure(resp.status); + case HttpStatusCode.NotFound: + return opKnownFailure(resp.status); + case HttpStatusCode.Conflict: + return opKnownFailure(resp.status); + case HttpStatusCode.Gone: + return opKnownFailure(resp.status); + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-donau.html#post--batch-submit + * + * @param args + * @returns + */ + async submitDonationReceipts(body: SubmitDonationReceiptsRequest) { + const url = new URL(`batch-submit`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, }); switch (resp.status) { case HttpStatusCode.Created: - return opFixedSuccess({}); + return opEmptySuccess(); + case HttpStatusCode.Forbidden: + return opKnownFailure(resp.status); + case HttpStatusCode.NotFound: + return opKnownFailure(resp.status); default: return opUnknownHttpFailure(resp); } } - async getDonationStatement(args: { year: number; taxIdHash: string }) { - const resp = await this.fetch( - `donation-statement/${args.year}/${args.taxIdHash}`, - { - method: "GET", - }, - ); + /** + * https://docs.taler.net/core/api-donau.html#get--donation-statement-$YEAR-$HASH_DONOR_ID + * + * @param args + * @returns + */ + async getDonationStatement(year: number, hash: string) { + const url = new URL(`donation-statement/${year}/${hash}`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + }); switch (resp.status) { case HttpStatusCode.Ok: return opSuccessFromHttp( resp, codecForDonauDonationStatementResponse(), ); + case HttpStatusCode.Forbidden: + return opKnownFailure(resp.status); + case HttpStatusCode.NotFound: + return opKnownFailure(resp.status); default: return opUnknownHttpFailure(resp); } } - async getKeys(): Promise<OperationOk<DonauKeysResponse>> { - const resp = await this.fetch("keys"); + /** + * https://docs.taler.net/core/api-donau.html#get--charities + * + * @param args + * @returns + */ + async getCharities(token: AccessToken) { + const url = new URL(`charities`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers: { + Authorization: makeBearerTokenAuthHeader(token), + }, + }); switch (resp.status) { case HttpStatusCode.Ok: - return opSuccessFromHttp(resp, codecForDonauKeysResponse()); + return opSuccessFromHttp(resp, codecForAny() as Codec<Charities>); // FIXME: complete codec + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-donau.html#get--charities-$CHARITY_ID + * + * @param args + * @returns + */ + async getCharitiesById(token: AccessToken, id: string) { + const url = new URL(`charities/${id}`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers: { + Authorization: makeBearerTokenAuthHeader(token), + }, + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForAny() as Codec<Charity>); // FIXME: complete codec + case HttpStatusCode.NotFound: + return opKnownFailure(resp.status); + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-donau.html#post--charities + * + * @param args + * @returns + */ + async createCharity(token: AccessToken, body: CharityRequest) { + const url = new URL(`charities`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + headers: { + Authorization: makeBearerTokenAuthHeader(token), + }, + }); + switch (resp.status) { + case HttpStatusCode.Created: + return opSuccessFromHttp(resp, codecForDonauCharityResponse()); + case HttpStatusCode.NoContent: + return opKnownFailure(resp.status); + case HttpStatusCode.Forbidden: + return opKnownFailure(resp.status); + case HttpStatusCode.NotFound: + return opKnownFailure(resp.status); + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-donau.html#patch--charities-id + * + * @returns + */ + async updateCharity(token: AccessToken, id: number, body: CharityRequest) { + const url = new URL(`charities/${id}`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "PATCH", + body, + headers: { + Authorization: makeBearerTokenAuthHeader(token), + }, + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opEmptySuccess(); + case HttpStatusCode.Forbidden: + return opKnownFailure(resp.status); + case HttpStatusCode.NotFound: // FIXME: missing in the spec + return opKnownFailure(resp.status); + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-donau.html#patch--charities-id + * + * @returns + */ + async deleteCharity(token: AccessToken, id: number) { + const url = new URL(`charities/${id}`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "DELETE", + headers: { + Authorization: makeBearerTokenAuthHeader(token), + }, + }); + switch (resp.status) { + case HttpStatusCode.NoContent: + return opEmptySuccess(); + case HttpStatusCode.Forbidden: + return opKnownFailure(resp.status); + case HttpStatusCode.NotFound: // FIXME: missing in the spec + return opKnownFailure(resp.status); default: return opUnknownHttpFailure(resp); } diff --git a/packages/taler-util/src/http-client/exchange-client.ts b/packages/taler-util/src/http-client/exchange-client.ts @@ -30,6 +30,7 @@ import { OperationFail, OperationOk, ResultByMethod, + carefullyParseConfig, opEmptySuccess, opFixedSuccess, opKnownAlternativeHttpFailure, @@ -165,7 +166,7 @@ export class TalerExchangeHttpClient { this.longPollQueue = params.longPollQueue ?? new LongpollQueue(); } - isCompatible(version: string): boolean { + static isCompatible(version: string): boolean { const compare = LibtoolVersion.compare( TalerExchangeHttpClient.SUPPORTED_EXCHANGE_PROTOCOL_VERSION, version, @@ -206,10 +207,7 @@ export class TalerExchangeHttpClient { * https://docs.taler.net/core/api-exchange.html#get--seed * */ - async getSeed(): Promise< - | OperationOk<Uint8Array<ArrayBuffer>> - | OperationFail<HttpStatusCode.NotFound> - > { + async getSeed() { const resp = await this.fetch("seed"); switch (resp.status) { case HttpStatusCode.Ok: @@ -232,35 +230,13 @@ export class TalerExchangeHttpClient { > { const resp = await this.fetch("config"); switch (resp.status) { - case HttpStatusCode.Ok: { - const minBody = await readSuccessResponseJsonOrThrow( - resp, - codecForTalerCommonConfigResponse(), - ); - const expectedName = "taler-exchange"; - if (minBody.name !== expectedName) { - throw TalerError.fromUncheckedDetail({ - code: TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR, - requestUrl: resp.requestUrl, - httpStatusCode: resp.status, - detail: `Unexpected server component name (got ${minBody.name}, expected ${expectedName}})`, - }); - } - if (!this.isCompatible(minBody.version)) { - throw TalerError.fromUncheckedDetail({ - code: TalerErrorCode.GENERIC_CLIENT_UNSUPPORTED_PROTOCOL_VERSION, - requestUrl: resp.requestUrl, - httpStatusCode: resp.status, - detail: `Unsupported protocol version, client supports ${TalerExchangeHttpClient.SUPPORTED_EXCHANGE_PROTOCOL_VERSION}, server supports ${minBody.version}`, - }); - } - // Now that we've checked the basic body, re-parse the full response. - const body = await readSuccessResponseJsonOrThrow( + case HttpStatusCode.Ok: + return carefullyParseConfig( + "taler-exchange", + TalerExchangeHttpClient.SUPPORTED_EXCHANGE_PROTOCOL_VERSION, resp, codecForExchangeConfig(), ); - return opFixedSuccess(body); - } case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); default: @@ -939,7 +915,10 @@ export class TalerExchangeHttpClient { until?: AbsoluteTime; } = {}, ) { - const url = new URL(`aml/${auth.id}/kyc-statistics/${names.join(" ")}`, this.baseUrl); + const url = new URL( + `aml/${auth.id}/kyc-statistics/${names.join(" ")}`, + this.baseUrl, + ); if (filter.since !== undefined && filter.since.t_ms !== "never") { url.searchParams.set("start_date", String(filter.since.t_ms)); @@ -1038,7 +1017,7 @@ export class TalerExchangeHttpClient { } = {}, ): Promise<OperationOk<LegitimizationMeasuresList>> { const url = new URL(`aml/${officer.id}/legitimizations`, this.baseUrl); - + addPaginationParams(url, params); if (params.account !== undefined) { url.searchParams.set("h_payto", params.account); diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts @@ -28,10 +28,9 @@ import { OperationOk, PaginationParams, ResultByMethod, - TalerError, TalerErrorCode, TalerMerchantApi, - TalerMerchantConfigResponse, + carefullyParseConfig, codecForAbortResponse, codecForAccountAddResponse, codecForAccountKycRedirects, @@ -60,7 +59,6 @@ import { codecForStatusGoto, codecForStatusPaid, codecForStatusStatusUnpaid, - codecForTalerCommonConfigResponse, codecForTalerMerchantConfigResponse, codecForTansferList, codecForTemplateDetails, @@ -77,14 +75,13 @@ import { opKnownAlternativeHttpFailure, opKnownHttpFailure, opKnownTalerFailure, - opUnknownHttpFailure, + opUnknownHttpFailure } from "@gnu-taler/taler-util"; import { HttpRequestLibrary, HttpResponse, createPlatformHttpLib, - readSuccessResponseJsonOrThrow, - readTalerErrorResponse, + readTalerErrorResponse } from "@gnu-taler/taler-util/http"; import { opSuccessFromHttp } from "../operation.js"; import { @@ -155,7 +152,7 @@ export enum TalerMerchantManagementCacheEviction { * Uses libtool's current:revision:age versioning. */ export class TalerMerchantInstanceHttpClient { - public readonly PROTOCOL_VERSION = "21:0:3"; + public static readonly PROTOCOL_VERSION = "21:0:3"; readonly httpLib: HttpRequestLibrary; readonly cacheEvictor: CacheEvictor<TalerMerchantInstanceCacheEviction>; @@ -172,52 +169,27 @@ export class TalerMerchantInstanceHttpClient { this.cancellationToken = cancellationToken; } - isCompatible(version: string): boolean { - const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version); + static isCompatible(version: string): boolean { + const compare = LibtoolVersion.compare(TalerMerchantInstanceHttpClient.PROTOCOL_VERSION, version); return compare?.compatible ?? false; } /** * https://docs.taler.net/core/api-merchant.html#get--config */ - async getConfig(): Promise< - | OperationFail<HttpStatusCode.NotFound> - | OperationOk<TalerMerchantConfigResponse> - > { + 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: { - const minBody = await readSuccessResponseJsonOrThrow( - resp, - codecForTalerCommonConfigResponse(), - ); - const expectedName = "taler-merchant"; - if (minBody.name !== expectedName) { - throw TalerError.fromUncheckedDetail({ - code: TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR, - requestUrl: resp.requestUrl, - httpStatusCode: resp.status, - detail: `Unexpected server component name (got ${minBody.name}, expected ${expectedName}})`, - }); - } - if (!this.isCompatible(minBody.version)) { - throw TalerError.fromUncheckedDetail({ - code: TalerErrorCode.GENERIC_CLIENT_UNSUPPORTED_PROTOCOL_VERSION, - requestUrl: resp.requestUrl, - httpStatusCode: resp.status, - detail: `Unsupported protocol version, client supports ${this.PROTOCOL_VERSION}, server supports ${minBody.version}`, - }); - } - // Now that we've checked the basic body, re-parse the full response. - const body = await readSuccessResponseJsonOrThrow( + case HttpStatusCode.Ok: + return carefullyParseConfig( + "taler-merchant", + TalerMerchantInstanceHttpClient.PROTOCOL_VERSION, resp, codecForTalerMerchantConfigResponse(), ); - return opFixedSuccess(body); - } case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); default: diff --git a/packages/taler-util/src/http-common.ts b/packages/taler-util/src/http-common.ts @@ -150,6 +150,7 @@ export async function readTalerErrorResponse( requestUrl: httpResponse.requestUrl, requestMethod: httpResponse.requestMethod, httpStatusCode: httpResponse.status, + response: await httpResponse.text(), contentType: contentType || "<null>", }, "Error response did not even contain JSON. The request URL might be wrong or the service might be unavailable.", diff --git a/packages/taler-util/src/operation.ts b/packages/taler-util/src/operation.ts @@ -25,7 +25,9 @@ import { } from "./http-common.js"; import { Codec, + codecForTalerCommonConfigResponse, HttpStatusCode, + LibtoolVersion, TalerError, TalerErrorCode, TalerErrorDetail, @@ -123,6 +125,48 @@ export function opKnownFailureWithBody<T, B>( } /** + * 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 + */ +export async function carefullyParseConfig<T>( + expectedName: string, + clientVersion: string, + httpResponse: HttpResponse, + codec: Codec<T>, +) { + const minBody = await readSuccessResponseJsonOrThrow( + httpResponse, + codecForTalerCommonConfigResponse(), + ); + if (minBody.name !== expectedName) { + throw TalerError.fromUncheckedDetail({ + code: TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR, + requestUrl: httpResponse.requestUrl, + httpStatusCode: httpResponse.status, + detail: `Unexpected server component name (got ${minBody.name}, expected ${expectedName}})`, + }); + } + + if (!LibtoolVersion.compare(clientVersion, minBody.version)) { + throw TalerError.fromUncheckedDetail({ + code: TalerErrorCode.GENERIC_CLIENT_UNSUPPORTED_PROTOCOL_VERSION, + requestUrl: httpResponse.requestUrl, + httpStatusCode: httpResponse.status, + detail: `Unsupported protocol version, client supports ${clientVersion}, server supports ${minBody.version}`, + }); + } + // Now that we've checked the basic body, re-parse the full response. + const body = await readSuccessResponseJsonOrThrow(httpResponse, codec); + return opFixedSuccess(body); +} + +/** * * @param resp * @param s diff --git a/packages/taler-util/src/types-donau.ts b/packages/taler-util/src/types-donau.ts @@ -24,6 +24,8 @@ import { } from "./codec.js"; import { AmountString, + BlindedRsaSignature, + buildCodecForUnion, codecForAmountString, codecForAny, codecForEddsaPublicKey, @@ -33,7 +35,10 @@ import { codecForTimestamp, Cs25519Point, Cs25519Scalar, + CSNonce, + CsRPublic, DenomKeyType, + EddsaPublicKey, EddsaPublicKeyString, EddsaSignatureString, HashCodeString, @@ -227,18 +232,106 @@ export interface CsDonauUnitPubKey { } export interface CharityRequest { - charity_pub: EddsaPublicKeyString; - charity_name: string; + // Long-term EdDSA public key that identifies the charity. + charity_pub: EddsaPublicKey; + // Canonical URL that should be presented to donors. charity_url: string; + // Human-readable display name of the charity. + charity_name: string; + // Allowed donation volume for the charity per calendar year. max_per_year: AmountString; + // Donation volume that has already been received for current_year. receipts_to_date: AmountString; + // Calendar year the accounting information refers to. current_year: Integer; } +export interface IssuePrepareRequest { + // Nonce to be used by the donau to derive + // its private inputs from. Must not have ever + // been used before. + nonce: CSNonce; + + // Hash of the public key of the donation unit + // the request relates to. + du_pub_hash: HashCodeString; +} export interface DonauCharityResponse { charity_id: Integer; } +export interface IssueReceiptsRequest { + // Signature by the charity approving that the + // Donau should sign the donation receipts below. + charity_sig: EddsaSignatureString; + + // Year for which the donation receipts are expected. + // Also determines which keys are used to sign the + // blinded donation receipts. + year: Integer; + + // Array of blinded donation receipts to sign. + // Must NOT be empty (if no donation receipts + // are desired, just leave the entire donau + // argument blank). + budikeypairs: BlindedDonationReceiptKeyPair[]; +} + +export type IssuePrepareResponse = DonauIssueValue; + +export type DonauIssueValue = DonauRsaIssueValue | DonauCsIssueValue; + +export interface DonauRsaIssueValue { + cipher: "RSA"; +} + +export interface DonauCsIssueValue { + cipher: "CS"; + + // CSR R0 value + r_pub_0: CsRPublic; + + // CSR R1 value + r_pub_1: CsRPublic; +} + +export interface BlindedDonationReceiptSignatures { + blind_signed_receipt_signatures: BlindedDonationReceiptSignature[]; +} +export type BlindedDonationReceiptSignature = + | RSABlindedDonationReceiptSignature + | CSBlindedDonationReceiptSignature; +export interface RSABlindedDonationReceiptSignature { + cipher: "RSA"; + + // (blinded) RSA signature + blinded_rsa_signature: BlindedRsaSignature; +} +export interface CSBlindedDonationReceiptSignature { + cipher: "CS"; + + // Signer chosen bit value, 0 or 1, used + // in Clause Blind Schnorr to make the + // ROS problem harder. + b: Integer; + + // Blinded scalar calculated from c_b. + s: Cs25519Scalar; +} + +export const codecForIssuePrepareResponse = (): Codec<IssuePrepareResponse> => + buildCodecForUnion<IssuePrepareResponse>() + .discriminateOn("cipher") + .alternative( + "CS", + codecForAny(), //FIXME: complete + ) + .alternative( + "RSA", + codecForAny(), //FIXME: complete + ) + .build<IssuePrepareResponse>("DonauApi.IssuePrepareResponse"); + export const codecForDonauCharityResponse = (): Codec<DonauCharityResponse> => buildCodecForObject<DonauCharityResponse>() .property("charity_id", codecForNumber()) @@ -274,3 +367,22 @@ export const codecForDonauDonationStatementResponse = .property("donau_pub", codecForEddsaPublicKey()) .property("donation_statement_sig", codecForEddsaSignature()) .build("DonauApi.DonationStatementResponse"); + +export interface Charities { + charities: CharitySummary[]; +} + +export interface CharitySummary { + charity_id: Integer; + name: string; + max_per_year: AmountString; + receipts_to_date: AmountString; +} +export interface Charity { + charity_pub: EddsaPublicKey; + name: string; + url: string; + max_per_year: AmountString; + receipts_to_date: AmountString; + current_year: Integer; +} diff --git a/packages/taler-util/src/types-taler-bank-integration.ts b/packages/taler-util/src/types-taler-bank-integration.ts @@ -78,13 +78,13 @@ export interface BankWithdrawalOperationStatus { // (raw amount without fee considerations). Only // given once the amount is fixed and cannot be changed. // Optional since **vC2EC**. - amount?: AmountString | undefined; + amount?: AmountString; // Suggestion for the amount to be withdrawn with this // operation. Given if a suggestion was made but the // user may still change the amount. // Optional since **vC2EC**. - suggested_amount?: AmountString | undefined; + suggested_amount?: AmountString; // Minimum amount that the wallet can choose to withdraw. // Only applicable when the amount is not fixed. @@ -100,7 +100,7 @@ export interface BankWithdrawalOperationStatus { // to pay to the bank / payment service provider // they are using to make the withdrawal. // @since **vC2EC** - card_fees?: AmountString | undefined; + card_fees?: AmountString; // Bank account of the customer that is debiting, as an // RFC 8905 payto URI. @@ -154,8 +154,8 @@ export interface BankWithdrawalOperationPostRequest { // Selected amount to be transferred. Optional if the // backend already knows the amount. - // @since **vC2EC** - amount?: AmountString | undefined; + // @since **v4** + amount?: AmountString; } export interface BankWithdrawalOperationPostResponse { diff --git a/packages/taler-util/src/types-taler-common.ts b/packages/taler-util/src/types-taler-common.ts @@ -244,7 +244,6 @@ export const codecForTalerCommonConfigResponse = .allowExtra() .property("name", codecForString()) .property("version", codecForString()) - .allowExtra() .build("TalerCommonConfigResponse"); export enum ExchangeProtocolVersion { diff --git a/packages/taler-util/src/types-taler-wire-gateway.ts b/packages/taler-util/src/types-taler-wire-gateway.ts @@ -22,6 +22,7 @@ import { buildCodecForObject, buildCodecForUnion, codecForAmountString, + codecForBoolean, codecForConstString, codecForEither, codecForList, @@ -57,6 +58,26 @@ export interface TransferResponse { row_id: SafeUint64; } +export interface WireConfig { + // Name of the API. + name: "taler-wire-gateway"; + + // libtool-style representation of the Bank 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 gateway. + currency: string; + + // URN of the implementation (needed to interpret 'revision' in version). + // @since v0, may become mandatory in the future. + implementation?: string; + + // Wether implementation support account existence check + support_account_check: boolean; +} + export interface TransferRequest { // Nonce to make the request idempotent. Requests with the same // request_uid that differ in any of the other fields @@ -221,7 +242,6 @@ export interface AddKycauthRequest { debit_account: string; } - export interface AddIncomingResponse { // Timestamp that indicates when the wire transfer will be executed. // In cases where the wire transfer gateway is unable to know when @@ -317,6 +337,16 @@ export interface BankWireTransferStatus { timestamp: Timestamp; } +export const codecForWireConfigResponse = + (): Codec<TalerWireGatewayApi.WireConfig> => + buildCodecForObject<TalerWireGatewayApi.WireConfig>() + .property("currency", codecForString()) + .property("implementation", codecForString()) + .property("name", codecForConstString("taler-wire-gateway")) + .property("support_account_check", codecForBoolean()) + .property("version", codecForString()) + .build("TalerWireGatewayApi.WireConfig"); + export const codecForTransferResponse = (): Codec<TalerWireGatewayApi.TransferResponse> => buildCodecForObject<TalerWireGatewayApi.TransferResponse>() diff --git a/packages/taler-wallet-core/src/donau.ts b/packages/taler-wallet-core/src/donau.ts @@ -113,24 +113,22 @@ export async function handleGetDonauStatements( continue; } succeedOrThrow( - await donauClient.postBatchSubmit({ - body: { - h_donor_tax_id: r0.donorTaxIdHash, - donation_receipts: batch.map((x) => ({ - donation_unit_sig: x.donationUnitSig, - h_donation_unit_pub: x.donationUnitPubHash, - nonce: x.udiNonce, - })), - donation_year: r0.donationYear, - }, + await donauClient.submitDonationReceipts({ + h_donor_tax_id: r0.donorTaxIdHash, + donation_receipts: batch.map((x) => ({ + donation_unit_sig: x.donationUnitSig, + h_donation_unit_pub: x.donationUnitPubHash, + nonce: x.udiNonce, + })), + donation_year: r0.donationYear, }), ); const stmt = succeedOrThrow( - await donauClient.getDonationStatement({ - taxIdHash: r0.donorTaxIdHash, - year: r0.donationYear, - }), + await donauClient.getDonationStatement( + r0.donationYear, + r0.donorTaxIdHash, + ), ); const parsedDonauUrl = new URL(r0.donauBaseUrl); const proto = parsedDonauUrl.protocol == "http:" ? "donau+http" : "donau"; diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts @@ -102,6 +102,7 @@ import { parsePaytoUri, parseTalerUri, parseWithdrawUri, + succeedOrThrow, } from "@gnu-taler/taler-util"; import { HttpRequestLibrary, @@ -1313,32 +1314,10 @@ export async function getBankWithdrawalInfo( const { body: config } = await bankApi.getConfig(); - if (!bankApi.isCompatible(config.version)) { - throw TalerError.fromDetail( - TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE, - { - bankProtocolVersion: config.version, - walletProtocolVersion: bankApi.PROTOCOL_VERSION, - }, - "bank integration protocol version not compatible with wallet", - ); - } - - const resp = await bankApi.getWithdrawalOperationById( - uriResult.withdrawalOperationId, + const status = succeedOrThrow( + await bankApi.getWithdrawalOperationById(uriResult.withdrawalOperationId), ); - if (resp.type === "fail") { - if (resp.detail) { - throw TalerError.fromUncheckedDetail(resp.detail); - } else { - throw TalerError.fromException( - new Error("failed to get bank remote config"), - ); - } - } - const { body: status } = resp; - const maxAmount = status.max_amount === undefined ? undefined diff --git a/packages/web-util/src/context/bank-api.ts b/packages/web-util/src/context/bank-api.ts @@ -199,7 +199,7 @@ function buildBankApiClient( return { getRemoteConfig, - VERSION: bank.PROTOCOL_VERSION, + VERSION: TalerCoreBankHttpClient.PROTOCOL_VERSION, lib: { bank, conversion, diff --git a/packages/web-util/src/context/challenger-api.ts b/packages/web-util/src/context/challenger-api.ts @@ -194,7 +194,7 @@ function buildChallengerApiClient( return { getRemoteConfig, - VERSION: challenger.PROTOCOL_VERSION, + VERSION: ChallengerHttpClient.PROTOCOL_VERSION, lib: { challenger, }, diff --git a/packages/web-util/src/context/merchant-api.ts b/packages/web-util/src/context/merchant-api.ts @@ -204,7 +204,7 @@ function buildMerchantApiClient( return { getRemoteConfig, - VERSION: instance.PROTOCOL_VERSION, + VERSION: TalerMerchantManagementHttpClient.PROTOCOL_VERSION, lib: { instance, subInstanceApi: getSubInstanceAPI,