commit 65324a5c030e997115e2b240dcdc5560052c1117
parent 3e6435b613986621b7b2bf20f448b87d152705d2
Author: Florian Dold <florian@dold.me>
Date: Wed, 30 Apr 2025 22:25:58 +0200
update supported exchange version, have version only in once place
Diffstat:
5 files changed, 1411 insertions(+), 1398 deletions(-)
diff --git a/packages/taler-util/src/http-client/exchange-client.ts b/packages/taler-util/src/http-client/exchange-client.ts
@@ -0,0 +1,1390 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2025 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 { Codec, codecForAny } from "../codec.js";
+import {
+ HttpRequestLibrary,
+ HttpRequestOptions,
+ HttpResponse,
+ readSuccessResponseJsonOrThrow,
+ readTalerErrorResponse,
+} from "../http-common.js";
+import { HttpStatusCode } from "../http-status-codes.js";
+import { createPlatformHttpLib } from "../http.js";
+import { LibtoolVersion } from "../libtool-version.js";
+import {
+ FailCasesByMethod,
+ OperationAlternative,
+ OperationFail,
+ OperationOk,
+ ResultByMethod,
+ opEmptySuccess,
+ opFixedSuccess,
+ opKnownAlternativeFailure,
+ opKnownHttpFailure,
+ opSuccessFromHttp,
+ opUnknownFailure,
+ opUnknownHttpFailure,
+} from "../operation.js";
+import { EddsaPrivP, encodeCrock } from "../taler-crypto.js";
+import {
+ AccessToken,
+ AmountString,
+ OfficerAccount,
+ PaginationParams,
+ ReserveAccount,
+ codecForTalerCommonConfigResponse,
+} from "../types-taler-common.js";
+import {
+ AccountKycStatus,
+ AmlDecisionRequest,
+ BatchWithdrawResponse,
+ ExchangeGetContractResponse,
+ ExchangeKycUploadFormRequest,
+ ExchangeLegacyBatchWithdrawRequest,
+ ExchangeMergeConflictResponse,
+ ExchangeMergeSuccessResponse,
+ ExchangePurseDeposits,
+ ExchangePurseMergeRequest,
+ ExchangePurseStatus,
+ ExchangeReservePurseRequest,
+ ExchangeVersionResponse,
+ KycRequirementInformationId,
+ LegitimizationNeededResponse,
+ PurseConflict,
+ PurseConflictPartial,
+ WalletKycRequest,
+ codecForAccountKycStatus,
+ codecForAmlDecisionsResponse,
+ codecForAmlKycAttributes,
+ codecForAmlWalletKycCheckResponse,
+ codecForAvailableMeasureSummary,
+ codecForEventCounter,
+ codecForExchangeConfig,
+ codecForExchangeGetContractResponse,
+ codecForExchangeKeysResponse,
+ codecForExchangeMergeConflictResponse,
+ codecForExchangeMergeSuccessResponse,
+ codecForExchangePurseStatus,
+ codecForExchangeTransferList,
+ codecForKycProcessClientInformation,
+ codecForKycProcessStartInformation,
+ codecForLegitimizationNeededResponse,
+ codecForPurseConflict,
+ codecForPurseConflictPartial,
+} from "../types-taler-exchange.js";
+import { CacheEvictor, addPaginationParams, nullEvictor } from "./utils.js";
+
+import { TalerError } from "../errors.js";
+import {
+ AmountJson,
+ Amounts,
+ CancellationToken,
+ LongpollQueue,
+ signAmlDecision,
+ signAmlQuery,
+ signKycAuth,
+ signWalletAccountSetup,
+} from "../index.js";
+import { TalerErrorCode } from "../taler-error-codes.js";
+import { AbsoluteTime } from "../time.js";
+import { codecForEmptyObject } from "../types-taler-wallet.js";
+
+export type TalerExchangeResultByMethod<
+ prop extends keyof TalerExchangeHttpClient,
+> = ResultByMethod<TalerExchangeHttpClient, prop>;
+export type TalerExchangeErrorsByMethod<
+ prop extends keyof TalerExchangeHttpClient,
+> = FailCasesByMethod<TalerExchangeHttpClient, prop>;
+
+export enum TalerExchangeCacheEviction {
+ UPLOAD_KYC_FORM,
+ MAKE_AML_DECISION,
+}
+
+declare const __pubId: unique symbol;
+export type ReservePub = string & { [__pubId]: true };
+
+/**
+ * Client library for the GNU Taler exchange service.
+ */
+export class TalerExchangeHttpClient {
+ public static readonly SUPPORTED_EXCHANGE_PROTOCOL_VERSION = "27:0:2";
+ private httpLib: HttpRequestLibrary;
+ private cacheEvictor: CacheEvictor<TalerExchangeCacheEviction>;
+ private preventCompression: boolean;
+ private cancelationToken: CancellationToken;
+ private longPollQueue: LongpollQueue;
+
+ constructor(
+ readonly baseUrl: string,
+ params: {
+ httpClient?: HttpRequestLibrary;
+ cacheEvictor?: CacheEvictor<TalerExchangeCacheEviction>;
+ preventCompression?: boolean;
+ cancelationToken?: CancellationToken;
+ longPollQueue?: LongpollQueue;
+ },
+ ) {
+ this.httpLib = params.httpClient ?? createPlatformHttpLib();
+ this.cacheEvictor = params.cacheEvictor ?? nullEvictor;
+ this.preventCompression = !!params.preventCompression;
+ this.cancelationToken =
+ params.cancelationToken ?? CancellationToken.CONTINUE;
+ this.longPollQueue = params.longPollQueue ?? new LongpollQueue();
+ }
+
+ isCompatible(version: string): boolean {
+ const compare = LibtoolVersion.compare(
+ TalerExchangeHttpClient.SUPPORTED_EXCHANGE_PROTOCOL_VERSION,
+ version,
+ );
+ 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,
+ });
+ }
+ }
+
+ // TERMS
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--seed
+ *
+ */
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--seed
+ *
+ */
+
+ // EXCHANGE INFORMATION
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--seed
+ *
+ */
+ async getSeed(): Promise<
+ | OperationOk<Uint8Array<ArrayBuffer>>
+ | OperationFail<HttpStatusCode.NotFound>
+ > {
+ const resp = await this.fetch("seed");
+ switch (resp.status) {
+ 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-exchange.html#get--config
+ *
+ */
+ async getConfig(): Promise<
+ | OperationFail<HttpStatusCode.NotFound>
+ | OperationOk<ExchangeVersionResponse>
+ > {
+ 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(
+ resp,
+ codecForExchangeConfig(),
+ );
+ return opFixedSuccess(body);
+ }
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get--config
+ *
+ * PARTIALLY IMPLEMENTED!!
+ */
+ async getKeys() {
+ const resp = await this.fetch("keys");
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForExchangeKeysResponse());
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ //
+ // MANAGEMENT
+ //
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--management-keys
+ *
+ */
+ async getFutureKeys(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--management-keys
+ *
+ */
+ async signFutureKeys(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--management-denominations-$H_DENOM_PUB-revoke
+ *
+ */
+ async revokeFutureDenominationKeys(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--management-signkeys-$EXCHANGE_PUB-revoke
+ *
+ */
+ async revokeFutureSigningKeys(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--management-auditors
+ *
+ */
+ async enableAuditor(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--management-auditors-$AUDITOR_PUB-disable
+ *
+ */
+ async disableAuditor(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--management-wire-fee
+ *
+ */
+ async configWireFee(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--management-global-fees
+ *
+ */
+ async configGlobalFees(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--management-wire
+ *
+ */
+ async enableWireMethod(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--management-wire-disable
+ *
+ */
+ async disableWireMethod(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--management-drain
+ *
+ */
+ async drainProfits(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--management-aml-officers
+ *
+ */
+ async updateOfficer(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--management-partners
+ *
+ */
+ async enablePartner(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ //
+ // AUDITOR
+ //
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--auditors-$AUDITOR_PUB-$H_DENOM_PUB
+ *
+ */
+ async addAuditor(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ //
+ // WITHDRAWAL
+ //
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--reserves-$RESERVE_PUB
+ *
+ */
+ async getReserveInfo(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--csr-withdraw
+ *
+ */
+ async prepareCsrWithdawal(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--reserves-$RESERVE_PUB-batch-withdraw
+ *
+ */
+ async withdraw(rid: ReservePub, body: ExchangeLegacyBatchWithdrawRequest) {
+ const resp = await this.fetch(`reserves/${rid}/batch-withdraw`, {
+ method: "POST",
+ body,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(
+ resp,
+ codecForAny() as Codec<BatchWithdrawResponse>,
+ );
+ case HttpStatusCode.Forbidden:
+ case HttpStatusCode.BadRequest:
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.Conflict:
+ case HttpStatusCode.Gone:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.UnavailableForLegalReasons:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForLegitimizationNeededResponse(),
+ );
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#withdraw-with-age-restriction
+ *
+ */
+ async withdrawWithAge(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--age-withdraw-$ACH-reveal
+ *
+ */
+ async revealCoinsForAge(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ //
+ // RESERVE HISTORY
+ //
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--reserves-$RESERVE_PUB-history
+ *
+ */
+ async getResverveHistory(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ //
+ // COIN HISTORY
+ //
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--coins-$COIN_PUB-history
+ *
+ */
+ async getCoinHistory(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ //
+ // DEPOSIT
+ //
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--batch-deposit
+ *
+ */
+ async deposit(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ //
+ // REFRESH
+ //
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--csr-melt
+ *
+ */
+ async prepareCsrMelt(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--coins-$COIN_PUB-melt
+ *
+ */
+ async meltCoin(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--coins-$COIN_PUB-link
+ *
+ */
+ async linkCoin(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ //
+ // RECOUP
+ //
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--coins-$COIN_PUB-recoup
+ *
+ */
+ async recoupReserveCoin(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--coins-$COIN_PUB-recoup-refresh
+ *
+ */
+ async recoupRefreshCoin(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ // WIRE TRANSFER
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--transfers-$WTID
+ *
+ */
+ async getWireTransferInfo(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--deposits-$H_WIRE-$MERCHANT_PUB-$H_CONTRACT_TERMS-$COIN_PUB
+ *
+ */
+ async getWireTransferIdForDeposit(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ // REFUND
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--coins-$COIN_PUB-refund
+ *
+ */
+ async refund(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ // WALLET TO WALLET
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--purses-$PURSE_PUB-merge
+ *
+ */
+ async getPurseStatusAtMerge(
+ pursePub: string,
+ longpoll: boolean = false,
+ ): Promise<
+ | OperationOk<ExchangePurseStatus>
+ | OperationFail<HttpStatusCode.NotFound>
+ | OperationFail<HttpStatusCode.Gone>
+ > {
+ const resp = await this.fetch(`purses/${pursePub}/merge`, {}, longpoll);
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForExchangePurseStatus());
+ case HttpStatusCode.Gone:
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--purses-$PURSE_PUB-deposit
+ *
+ */
+ async getPurseStatusAtDeposit(
+ pursePub: string,
+ longpoll: boolean = false,
+ ): Promise<
+ | OperationOk<ExchangePurseStatus>
+ | OperationFail<HttpStatusCode.NotFound>
+ | OperationFail<HttpStatusCode.Gone>
+ > {
+ const resp = await this.fetch(`purses/${pursePub}/deposit`, {}, longpoll);
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForExchangePurseStatus());
+ case HttpStatusCode.Gone:
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-create
+ *
+ */
+ async createPurseFromDeposit(
+ pursePub: string,
+ body: any, // FIXME
+ ): Promise<
+ | OperationOk<void>
+ | OperationFail<HttpStatusCode.Forbidden>
+ | OperationFail<HttpStatusCode.NotFound>
+ | OperationAlternative<HttpStatusCode.Conflict, PurseConflict>
+ | OperationFail<HttpStatusCode.TooEarly>
+ > {
+ const resp = await this.fetch(`purses/${pursePub}/create`, {
+ method: "POST",
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ // FIXME: parse PurseCreateSuccessResponse
+ return opSuccessFromHttp(resp, codecForAny());
+ case HttpStatusCode.Conflict:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForPurseConflict(),
+ );
+ case HttpStatusCode.Forbidden:
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.TooEarly:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#delete--purses-$PURSE_PUB
+ *
+ */
+ async deletePurse(
+ pursePub: string,
+ purseSig: string,
+ ): Promise<
+ | OperationOk<void>
+ | OperationFail<HttpStatusCode.NotFound>
+ | OperationFail<HttpStatusCode.Conflict>
+ | OperationFail<HttpStatusCode.Forbidden>
+ > {
+ const resp = await this.fetch(`purses/${pursePub}`, {
+ method: "DELETE",
+ headers: {
+ "taler-purse-signature": purseSig,
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ /**
+ * POST /purses/$PURSE_PUB/merge
+ *
+ * https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
+ */
+ async postPurseMerge(
+ pursePub: string,
+ body: ExchangePurseMergeRequest,
+ ): Promise<
+ | OperationOk<ExchangeMergeSuccessResponse>
+ | OperationAlternative<
+ HttpStatusCode.UnavailableForLegalReasons,
+ LegitimizationNeededResponse
+ >
+ | OperationAlternative<
+ HttpStatusCode.Conflict,
+ ExchangeMergeConflictResponse
+ >
+ | OperationFail<HttpStatusCode.Forbidden>
+ | OperationFail<HttpStatusCode.NotFound>
+ | OperationFail<HttpStatusCode.Gone>
+ > {
+ const resp = await this.fetch(`purses/${pursePub}/merge`, {
+ method: "POST",
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForExchangeMergeSuccessResponse());
+ case HttpStatusCode.UnavailableForLegalReasons:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForLegitimizationNeededResponse(),
+ );
+ case HttpStatusCode.Conflict:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForExchangeMergeConflictResponse(),
+ );
+ case HttpStatusCode.Gone:
+ case HttpStatusCode.Forbidden:
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--reserves-$RESERVE_PUB-purse
+ *
+ */
+ async createPurseFromReserve(
+ pursePub: string,
+ body: ExchangeReservePurseRequest,
+ ): Promise<
+ | OperationOk<void>
+ | OperationFail<HttpStatusCode.PaymentRequired>
+ | OperationFail<HttpStatusCode.Forbidden>
+ | OperationFail<HttpStatusCode.NotFound>
+ | OperationAlternative<HttpStatusCode.Conflict, PurseConflictPartial>
+ | OperationAlternative<
+ HttpStatusCode.UnavailableForLegalReasons,
+ LegitimizationNeededResponse
+ >
+ > {
+ const resp = await this.fetch(`reserves/${pursePub}/purse`, {
+ method: "POST",
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ // FIXME: parse PurseCreateSuccessResponse
+ return opSuccessFromHttp(resp, codecForAny());
+ case HttpStatusCode.PaymentRequired:
+ case HttpStatusCode.Forbidden:
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForPurseConflictPartial(),
+ );
+ case HttpStatusCode.UnavailableForLegalReasons:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForLegitimizationNeededResponse(),
+ );
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--contracts-$CONTRACT_PUB
+ *
+ */
+ async getContract(
+ pursePub: string,
+ ): Promise<
+ | OperationOk<ExchangeGetContractResponse>
+ | OperationFail<HttpStatusCode.NotFound>
+ > {
+ const resp = await this.fetch(`contracts/${pursePub}`);
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForExchangeGetContractResponse());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-deposit
+ *
+ */
+ async depositIntoPurse(
+ pursePub: string,
+ body: ExchangePurseDeposits,
+ ): Promise<
+ | OperationOk<void>
+ | OperationAlternative<HttpStatusCode.Conflict, PurseConflict>
+ | OperationFail<HttpStatusCode.Forbidden>
+ | OperationFail<HttpStatusCode.NotFound>
+ | OperationFail<HttpStatusCode.Gone>
+ > {
+ const resp = await this.fetch(`purses/${pursePub}/deposit`, {
+ method: "POST",
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ // FIXME: parse PurseDepositSuccessResponse
+ return opSuccessFromHttp(resp, codecForAny());
+ case HttpStatusCode.Conflict:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForPurseConflict(),
+ );
+ case HttpStatusCode.Forbidden:
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.Gone:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ // WADS
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--wads-$WAD_ID
+ *
+ */
+ async getWadInfo(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ //
+ // KYC
+ //
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--kyc-wallet
+ *
+ */
+ async notifyKycBalanceLimit(account: ReserveAccount, balance: AmountString) {
+ const body: WalletKycRequest = {
+ balance,
+ reserve_pub: account.id,
+ reserve_sig: encodeCrock(
+ signWalletAccountSetup(account.signingKey, balance),
+ ),
+ };
+ const resp = await this.fetch(`kyc-wallet`, {
+ method: "POST",
+ body,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForAmlWalletKycCheckResponse());
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.UnavailableForLegalReasons:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForLegitimizationNeededResponse(),
+ );
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--kyc-check-$H_NORMALIZED_PAYTO
+ *
+ */
+ async checkKycStatus(
+ signingKey: EddsaPrivP | string,
+ paytoHash: string,
+ longpoll: boolean = false,
+ params: {
+ awaitAuth?: boolean;
+ } = {},
+ ): Promise<
+ | OperationOk<void>
+ | OperationAlternative<HttpStatusCode.Ok, AccountKycStatus>
+ | OperationAlternative<HttpStatusCode.Accepted, AccountKycStatus>
+ | OperationFail<HttpStatusCode.Forbidden>
+ | OperationFail<HttpStatusCode.NotFound>
+ | OperationFail<HttpStatusCode.Conflict>
+ > {
+ const url = new URL(`kyc-check/${paytoHash}`, this.baseUrl);
+ if (params.awaitAuth !== undefined) {
+ url.searchParams.set("await_auth", params.awaitAuth ? "YES" : "NO");
+ }
+
+ const signature =
+ typeof signingKey === "string"
+ ? signingKey
+ : encodeCrock(signKycAuth(signingKey));
+
+ const resp = await this.fetch(
+ url,
+ {
+ headers: {
+ "Account-Owner-Signature": signature,
+ },
+ },
+ longpoll,
+ );
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ case HttpStatusCode.Accepted:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForAccountKycStatus(),
+ );
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.Forbidden:
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--kyc-info-$ACCESS_TOKEN
+ *
+ */
+ async checkKycInfo(
+ token: AccessToken,
+ known: KycRequirementInformationId[],
+ longpoll: boolean = false,
+ ) {
+ const resp = await this.fetch(
+ `kyc-info/${token}`,
+ {
+ method: "GET",
+ headers: {
+ "If-None-Match": known.length ? known.join(",") : undefined,
+ },
+ },
+ longpoll,
+ );
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForKycProcessClientInformation());
+ case HttpStatusCode.Accepted:
+ case HttpStatusCode.NoContent:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForEmptyObject(),
+ );
+ case HttpStatusCode.NotModified:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--kyc-upload-$ID
+ *
+ */
+ async uploadKycForm<T extends ExchangeKycUploadFormRequest>(
+ requirement: KycRequirementInformationId,
+ body: T,
+ ) {
+ const resp = await this.fetch(`kyc-upload/${requirement}`, {
+ method: "POST",
+ body,
+ compress: this.preventCompression ? undefined : "deflate",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerExchangeCacheEviction.UPLOAD_KYC_FORM,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.Conflict:
+ case HttpStatusCode.PayloadTooLarge:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--kyc-start-$ID
+ *
+ */
+ async startExternalKycProcess(
+ requirement: KycRequirementInformationId,
+ body: object = {},
+ ) {
+ const resp = await this.fetch(`kyc-start/${requirement}`, {
+ method: "POST",
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForKycProcessStartInformation());
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.Conflict:
+ case HttpStatusCode.PayloadTooLarge:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--kyc-proof-$PROVIDER_NAME?state=$H_PAYTO
+ *
+ */
+ async completeExternalKycProcess(
+ provider: string,
+ state: string,
+ code: string,
+ ) {
+ const resp = await this.fetch(
+ `kyc-proof/${provider}?state=${state}&code=${code}`,
+ {
+ method: "GET",
+ redirect: "manual",
+ },
+ );
+
+ switch (resp.status) {
+ case HttpStatusCode.SeeOther:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ //
+ // AML operations
+ //
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-measures
+ *
+ */
+ async getAmlMesasures(auth: OfficerAccount) {
+ const resp = await this.fetch(`aml/${auth.id}/measures`, {
+ method: "GET",
+ headers: {
+ "Taler-AML-Officer-Signature": encodeCrock(
+ signAmlQuery(auth.signingKey),
+ ),
+ },
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForAvailableMeasureSummary());
+ case HttpStatusCode.Conflict:
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-measures
+ *
+ */
+ async getAmlKycStatistics(
+ auth: OfficerAccount,
+ name: string,
+ filter: {
+ since?: AbsoluteTime;
+ until?: AbsoluteTime;
+ } = {},
+ ) {
+ const url = new URL(`aml/${auth.id}/kyc-statistics/${name}`, this.baseUrl);
+
+ if (filter.since !== undefined && filter.since.t_ms !== "never") {
+ url.searchParams.set("start_date", String(filter.since.t_ms));
+ }
+ if (filter.until !== undefined && filter.until.t_ms !== "never") {
+ url.searchParams.set("end_date", String(filter.until.t_ms));
+ }
+
+ const resp = await this.fetch(url, {
+ method: "GET",
+ headers: {
+ "Taler-AML-Officer-Signature": encodeCrock(
+ signAmlQuery(auth.signingKey),
+ ),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForEventCounter());
+ case HttpStatusCode.Conflict:
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-decisions
+ *
+ */
+ async getAmlDecisions(
+ auth: OfficerAccount,
+ params: PaginationParams & {
+ account?: string;
+ active?: boolean;
+ investigation?: boolean;
+ } = {},
+ ) {
+ const url = new URL(`aml/${auth.id}/decisions`, this.baseUrl);
+
+ addPaginationParams(url, params);
+ if (params.account !== undefined) {
+ url.searchParams.set("h_payto", params.account);
+ }
+ if (params.active !== undefined) {
+ url.searchParams.set("active", params.active ? "YES" : "NO");
+ }
+ if (params.investigation !== undefined) {
+ url.searchParams.set(
+ "investigation",
+ params.investigation ? "YES" : "NO",
+ );
+ }
+
+ const resp = await this.fetch(url, {
+ headers: {
+ "Taler-AML-Officer-Signature": encodeCrock(
+ signAmlQuery(auth.signingKey),
+ ),
+ },
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForAmlDecisionsResponse());
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess({ records: [] });
+ case HttpStatusCode.Forbidden:
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-attributes-$H_PAYTO
+ *
+ */
+ async getAmlAttributesForAccount(
+ auth: OfficerAccount,
+ account: string,
+ params: PaginationParams = {},
+ ) {
+ const url = new URL(`aml/${auth.id}/attributes/${account}`, this.baseUrl);
+
+ addPaginationParams(url, params);
+ const resp = await this.fetch(url, {
+ headers: {
+ "Taler-AML-Officer-Signature": encodeCrock(
+ signAmlQuery(auth.signingKey),
+ ),
+ },
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForAmlKycAttributes());
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess({ details: [] });
+ case HttpStatusCode.Forbidden:
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--aml-$OFFICER_PUB-decision
+ *
+ */
+ async makeAmlDesicion(
+ auth: OfficerAccount,
+ decision: Omit<AmlDecisionRequest, "officer_sig">,
+ ) {
+ const body: AmlDecisionRequest = {
+ officer_sig: encodeCrock(signAmlDecision(auth.signingKey, decision)),
+ ...decision,
+ };
+ const resp = await this.fetch(`aml/${auth.id}/decision`, {
+ method: "POST",
+ headers: {
+ "Taler-AML-Officer-Signature": encodeCrock(
+ signAmlQuery(auth.signingKey),
+ ),
+ },
+ body,
+ compress: this.preventCompression ? undefined : "deflate",
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerExchangeCacheEviction.MAKE_AML_DECISION,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Forbidden:
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-transfers-credit
+ *
+ */
+ async getTransfersCredit(
+ auth: OfficerAccount,
+ params: PaginationParams & { threshold?: AmountJson } = {},
+ ) {
+ const url = new URL(`aml/${auth.id}/transfers-credit`, this.baseUrl);
+
+ addPaginationParams(url, params);
+
+ if (params.threshold) {
+ url.searchParams.set("threshold", Amounts.stringify(params.threshold));
+ }
+
+ const resp = await this.fetch(url, {
+ headers: {
+ "Taler-AML-Officer-Signature": encodeCrock(
+ signAmlQuery(auth.signingKey),
+ ),
+ },
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForExchangeTransferList());
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess({ transfers: [] });
+ case HttpStatusCode.Forbidden:
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-transfers-debit
+ *
+ */
+ async getTransfersDebit(
+ auth: OfficerAccount,
+ params: PaginationParams & { threshold?: AmountJson } = {},
+ ) {
+ const url = new URL(`aml/${auth.id}/transfers-debit`, this.baseUrl);
+
+ addPaginationParams(url, params);
+
+ if (params.threshold) {
+ url.searchParams.set("threshold", Amounts.stringify(params.threshold));
+ }
+
+ const resp = await this.fetch(url, {
+ headers: {
+ "Taler-AML-Officer-Signature": encodeCrock(
+ signAmlQuery(auth.signingKey),
+ ),
+ },
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForExchangeTransferList());
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess({ transfers: [] });
+ case HttpStatusCode.Forbidden:
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ // RESERVE control
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--reserves-$RESERVE_PUB-open
+ *
+ */
+ async reserveOpen(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--reserves-attest-$RESERVE_PUB
+ *
+ */
+ async getReserveAttributes(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--reserves-attest-$RESERVE_PUB
+ *
+ */
+ async signReserveAttributes(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--reserves-$RESERVE_PUB-close
+ *
+ */
+ async closeReserve(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#delete--reserves-$RESERVE_PUB
+ *
+ */
+ async deleteReserve(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+}
diff --git a/packages/taler-util/src/http-client/exchange.ts b/packages/taler-util/src/http-client/exchange.ts
@@ -1,1389 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022-2025 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 { Codec, codecForAny } from "../codec.js";
-import {
- HttpRequestLibrary,
- HttpRequestOptions,
- HttpResponse,
- readSuccessResponseJsonOrThrow,
- readTalerErrorResponse,
-} from "../http-common.js";
-import { HttpStatusCode } from "../http-status-codes.js";
-import { createPlatformHttpLib } from "../http.js";
-import { LibtoolVersion } from "../libtool-version.js";
-import {
- FailCasesByMethod,
- OperationAlternative,
- OperationFail,
- OperationOk,
- ResultByMethod,
- opEmptySuccess,
- opFixedSuccess,
- opKnownAlternativeFailure,
- opKnownHttpFailure,
- opSuccessFromHttp,
- opUnknownFailure,
- opUnknownHttpFailure,
-} from "../operation.js";
-import { EddsaPrivP, encodeCrock } from "../taler-crypto.js";
-import {
- AccessToken,
- AmountString,
- OfficerAccount,
- PaginationParams,
- ReserveAccount,
- codecForTalerCommonConfigResponse,
-} from "../types-taler-common.js";
-import {
- AccountKycStatus,
- AmlDecisionRequest,
- BatchWithdrawResponse,
- ExchangeGetContractResponse,
- ExchangeKycUploadFormRequest,
- ExchangeLegacyBatchWithdrawRequest,
- ExchangeMergeConflictResponse,
- ExchangeMergeSuccessResponse,
- ExchangePurseDeposits,
- ExchangePurseMergeRequest,
- ExchangePurseStatus,
- ExchangeReservePurseRequest,
- ExchangeVersionResponse,
- KycRequirementInformationId,
- LegitimizationNeededResponse,
- PurseConflict,
- PurseConflictPartial,
- WalletKycRequest,
- codecForAccountKycStatus,
- codecForAmlDecisionsResponse,
- codecForAmlKycAttributes,
- codecForAmlWalletKycCheckResponse,
- codecForAvailableMeasureSummary,
- codecForEventCounter,
- codecForExchangeConfig,
- codecForExchangeGetContractResponse,
- codecForExchangeKeysResponse,
- codecForExchangeMergeConflictResponse,
- codecForExchangeMergeSuccessResponse,
- codecForExchangePurseStatus,
- codecForExchangeTransferList,
- codecForKycProcessClientInformation,
- codecForKycProcessStartInformation,
- codecForLegitimizationNeededResponse,
- codecForPurseConflict,
- codecForPurseConflictPartial,
-} from "../types-taler-exchange.js";
-import { CacheEvictor, addPaginationParams, nullEvictor } from "./utils.js";
-
-import { TalerError } from "../errors.js";
-import {
- AmountJson,
- Amounts,
- CancellationToken,
- LongpollQueue,
- signAmlDecision,
- signAmlQuery,
- signKycAuth,
- signWalletAccountSetup,
-} from "../index.js";
-import { TalerErrorCode } from "../taler-error-codes.js";
-import { AbsoluteTime } from "../time.js";
-import { codecForEmptyObject } from "../types-taler-wallet.js";
-
-export type TalerExchangeResultByMethod<
- prop extends keyof TalerExchangeHttpClient,
-> = ResultByMethod<TalerExchangeHttpClient, prop>;
-export type TalerExchangeErrorsByMethod<
- prop extends keyof TalerExchangeHttpClient,
-> = FailCasesByMethod<TalerExchangeHttpClient, prop>;
-
-export enum TalerExchangeCacheEviction {
- UPLOAD_KYC_FORM,
- MAKE_AML_DECISION,
-}
-
-declare const __pubId: unique symbol;
-export type ReservePub = string & { [__pubId]: true };
-/**
- */
-export class TalerExchangeHttpClient {
- public readonly PROTOCOL_VERSION = "21:0:0";
- private httpLib: HttpRequestLibrary;
- private cacheEvictor: CacheEvictor<TalerExchangeCacheEviction>;
- private preventCompression: boolean;
- private cancelationToken: CancellationToken;
- private longPollQueue: LongpollQueue;
-
- constructor(
- readonly baseUrl: string,
- params: {
- httpClient?: HttpRequestLibrary;
- cacheEvictor?: CacheEvictor<TalerExchangeCacheEviction>;
- preventCompression?: boolean;
- cancelationToken?: CancellationToken;
- longPollQueue?: LongpollQueue;
- },
- ) {
- this.httpLib = params.httpClient ?? createPlatformHttpLib();
- this.cacheEvictor = params.cacheEvictor ?? nullEvictor;
- this.preventCompression = !!params.preventCompression;
- this.cancelationToken =
- params.cancelationToken ?? CancellationToken.CONTINUE;
- this.longPollQueue = params.longPollQueue ?? new LongpollQueue();
- }
-
- isCompatible(version: string): boolean {
- const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version);
- 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,
- });
- }
- }
-
- // TERMS
-
- /**
- * https://docs.taler.net/core/api-exchange.html#get--seed
- *
- */
- /**
- * https://docs.taler.net/core/api-exchange.html#get--seed
- *
- */
-
- // EXCHANGE INFORMATION
-
- /**
- * https://docs.taler.net/core/api-exchange.html#get--seed
- *
- */
- async getSeed(): Promise<
- | OperationOk<Uint8Array<ArrayBuffer>>
- | OperationFail<HttpStatusCode.NotFound>
- > {
- const resp = await this.fetch("seed");
- switch (resp.status) {
- 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-exchange.html#get--config
- *
- */
- async getConfig(): Promise<
- | OperationFail<HttpStatusCode.NotFound>
- | OperationOk<ExchangeVersionResponse>
- > {
- 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 ${this.PROTOCOL_VERSION}, server supports ${minBody.version}`,
- });
- }
- // Now that we've checked the basic body, re-parse the full response.
- const body = await readSuccessResponseJsonOrThrow(
- resp,
- codecForExchangeConfig(),
- );
- return {
- type: "ok",
- case: "ok",
- body,
- };
- }
- case HttpStatusCode.NotFound:
- return opKnownHttpFailure(resp.status, resp);
- default:
- return opUnknownHttpFailure(resp);
- }
- }
-
- /**
- * https://docs.taler.net/core/api-merchant.html#get--config
- *
- * PARTIALLY IMPLEMENTED!!
- */
- async getKeys() {
- const resp = await this.fetch("keys");
- switch (resp.status) {
- case HttpStatusCode.Ok:
- return opSuccessFromHttp(resp, codecForExchangeKeysResponse());
- default:
- return opUnknownHttpFailure(resp);
- }
- }
-
- //
- // MANAGEMENT
- //
-
- /**
- * https://docs.taler.net/core/api-exchange.html#get--management-keys
- *
- */
- async getFutureKeys(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--management-keys
- *
- */
- async signFutureKeys(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--management-denominations-$H_DENOM_PUB-revoke
- *
- */
- async revokeFutureDenominationKeys(): Promise<never> {
- throw Error("not yet implemented");
- }
- /**
- * https://docs.taler.net/core/api-exchange.html#post--management-signkeys-$EXCHANGE_PUB-revoke
- *
- */
- async revokeFutureSigningKeys(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--management-auditors
- *
- */
- async enableAuditor(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--management-auditors-$AUDITOR_PUB-disable
- *
- */
- async disableAuditor(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--management-wire-fee
- *
- */
- async configWireFee(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--management-global-fees
- *
- */
- async configGlobalFees(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--management-wire
- *
- */
- async enableWireMethod(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--management-wire-disable
- *
- */
- async disableWireMethod(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--management-drain
- *
- */
- async drainProfits(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--management-aml-officers
- *
- */
- async updateOfficer(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--management-partners
- *
- */
- async enablePartner(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- //
- // AUDITOR
- //
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--auditors-$AUDITOR_PUB-$H_DENOM_PUB
- *
- */
- async addAuditor(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- //
- // WITHDRAWAL
- //
-
- /**
- * https://docs.taler.net/core/api-exchange.html#get--reserves-$RESERVE_PUB
- *
- */
- async getReserveInfo(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--csr-withdraw
- *
- */
- async prepareCsrWithdawal(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--reserves-$RESERVE_PUB-batch-withdraw
- *
- */
- async withdraw(rid: ReservePub, body: ExchangeLegacyBatchWithdrawRequest) {
- const resp = await this.fetch(`reserves/${rid}/batch-withdraw`, {
- method: "POST",
- body,
- });
-
- switch (resp.status) {
- case HttpStatusCode.Ok:
- return opSuccessFromHttp(
- resp,
- codecForAny() as Codec<BatchWithdrawResponse>,
- );
- case HttpStatusCode.Forbidden:
- case HttpStatusCode.BadRequest:
- case HttpStatusCode.NotFound:
- case HttpStatusCode.Conflict:
- case HttpStatusCode.Gone:
- return opKnownHttpFailure(resp.status, resp);
- case HttpStatusCode.UnavailableForLegalReasons:
- return opKnownAlternativeFailure(
- resp,
- resp.status,
- codecForLegitimizationNeededResponse(),
- );
- default:
- return opUnknownHttpFailure(resp);
- }
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#withdraw-with-age-restriction
- *
- */
- async withdrawWithAge(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--age-withdraw-$ACH-reveal
- *
- */
- async revealCoinsForAge(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- //
- // RESERVE HISTORY
- //
-
- /**
- * https://docs.taler.net/core/api-exchange.html#get--reserves-$RESERVE_PUB-history
- *
- */
- async getResverveHistory(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- //
- // COIN HISTORY
- //
-
- /**
- * https://docs.taler.net/core/api-exchange.html#get--coins-$COIN_PUB-history
- *
- */
- async getCoinHistory(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- //
- // DEPOSIT
- //
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--batch-deposit
- *
- */
- async deposit(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- //
- // REFRESH
- //
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--csr-melt
- *
- */
- async prepareCsrMelt(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--coins-$COIN_PUB-melt
- *
- */
- async meltCoin(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#get--coins-$COIN_PUB-link
- *
- */
- async linkCoin(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- //
- // RECOUP
- //
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--coins-$COIN_PUB-recoup
- *
- */
- async recoupReserveCoin(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--coins-$COIN_PUB-recoup-refresh
- *
- */
- async recoupRefreshCoin(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- // WIRE TRANSFER
-
- /**
- * https://docs.taler.net/core/api-exchange.html#get--transfers-$WTID
- *
- */
- async getWireTransferInfo(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#get--deposits-$H_WIRE-$MERCHANT_PUB-$H_CONTRACT_TERMS-$COIN_PUB
- *
- */
- async getWireTransferIdForDeposit(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- // REFUND
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--coins-$COIN_PUB-refund
- *
- */
- async refund(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- // WALLET TO WALLET
-
- /**
- * https://docs.taler.net/core/api-exchange.html#get--purses-$PURSE_PUB-merge
- *
- */
- async getPurseStatusAtMerge(
- pursePub: string,
- longpoll: boolean = false,
- ): Promise<
- | OperationOk<ExchangePurseStatus>
- | OperationFail<HttpStatusCode.NotFound>
- | OperationFail<HttpStatusCode.Gone>
- > {
- const resp = await this.fetch(`purses/${pursePub}/merge`, {}, longpoll);
- switch (resp.status) {
- case HttpStatusCode.Ok:
- return opSuccessFromHttp(resp, codecForExchangePurseStatus());
- case HttpStatusCode.Gone:
- case HttpStatusCode.NotFound:
- return opKnownHttpFailure(resp.status, resp);
- default:
- return opUnknownHttpFailure(resp);
- }
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#get--purses-$PURSE_PUB-deposit
- *
- */
- async getPurseStatusAtDeposit(
- pursePub: string,
- longpoll: boolean = false,
- ): Promise<
- | OperationOk<ExchangePurseStatus>
- | OperationFail<HttpStatusCode.NotFound>
- | OperationFail<HttpStatusCode.Gone>
- > {
- const resp = await this.fetch(`purses/${pursePub}/deposit`, {}, longpoll);
- switch (resp.status) {
- case HttpStatusCode.Ok:
- return opSuccessFromHttp(resp, codecForExchangePurseStatus());
- case HttpStatusCode.Gone:
- case HttpStatusCode.NotFound:
- return opKnownHttpFailure(resp.status, resp);
- default:
- return opUnknownHttpFailure(resp);
- }
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-create
- *
- */
- async createPurseFromDeposit(
- pursePub: string,
- body: any, // FIXME
- ): Promise<
- | OperationOk<void>
- | OperationFail<HttpStatusCode.Forbidden>
- | OperationFail<HttpStatusCode.NotFound>
- | OperationAlternative<HttpStatusCode.Conflict, PurseConflict>
- | OperationFail<HttpStatusCode.TooEarly>
- > {
- const resp = await this.fetch(`purses/${pursePub}/create`, {
- method: "POST",
- body,
- });
- switch (resp.status) {
- case HttpStatusCode.Ok:
- // FIXME: parse PurseCreateSuccessResponse
- return opSuccessFromHttp(resp, codecForAny());
- case HttpStatusCode.Conflict:
- return opKnownAlternativeFailure(
- resp,
- resp.status,
- codecForPurseConflict(),
- );
- case HttpStatusCode.Forbidden:
- case HttpStatusCode.NotFound:
- case HttpStatusCode.TooEarly:
- return opKnownHttpFailure(resp.status, resp);
- default:
- return opUnknownHttpFailure(resp);
- }
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#delete--purses-$PURSE_PUB
- *
- */
- async deletePurse(
- pursePub: string,
- purseSig: string,
- ): Promise<
- | OperationOk<void>
- | OperationFail<HttpStatusCode.NotFound>
- | OperationFail<HttpStatusCode.Conflict>
- | OperationFail<HttpStatusCode.Forbidden>
- > {
- const resp = await this.fetch(`purses/${pursePub}`, {
- method: "DELETE",
- headers: {
- "taler-purse-signature": purseSig,
- },
- });
- switch (resp.status) {
- case HttpStatusCode.NoContent:
- return opEmptySuccess(resp);
- case HttpStatusCode.NotFound:
- return opKnownHttpFailure(resp.status, resp);
- case HttpStatusCode.Conflict:
- return opKnownHttpFailure(resp.status, resp);
- case HttpStatusCode.Forbidden:
- return opKnownHttpFailure(resp.status, resp);
- default:
- return opUnknownHttpFailure(resp);
- }
- }
-
- /**
- * POST /purses/$PURSE_PUB/merge
- *
- * https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
- */
- async postPurseMerge(
- pursePub: string,
- body: ExchangePurseMergeRequest,
- ): Promise<
- | OperationOk<ExchangeMergeSuccessResponse>
- | OperationAlternative<
- HttpStatusCode.UnavailableForLegalReasons,
- LegitimizationNeededResponse
- >
- | OperationAlternative<
- HttpStatusCode.Conflict,
- ExchangeMergeConflictResponse
- >
- | OperationFail<HttpStatusCode.Forbidden>
- | OperationFail<HttpStatusCode.NotFound>
- | OperationFail<HttpStatusCode.Gone>
- > {
- const resp = await this.fetch(`purses/${pursePub}/merge`, {
- method: "POST",
- body,
- });
- switch (resp.status) {
- case HttpStatusCode.Ok:
- return opSuccessFromHttp(resp, codecForExchangeMergeSuccessResponse());
- case HttpStatusCode.UnavailableForLegalReasons:
- return opKnownAlternativeFailure(
- resp,
- resp.status,
- codecForLegitimizationNeededResponse(),
- );
- case HttpStatusCode.Conflict:
- return opKnownAlternativeFailure(
- resp,
- resp.status,
- codecForExchangeMergeConflictResponse(),
- );
- case HttpStatusCode.Gone:
- case HttpStatusCode.Forbidden:
- case HttpStatusCode.NotFound:
- return opKnownHttpFailure(resp.status, resp);
- default:
- return opUnknownHttpFailure(resp);
- }
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--reserves-$RESERVE_PUB-purse
- *
- */
- async createPurseFromReserve(
- pursePub: string,
- body: ExchangeReservePurseRequest,
- ): Promise<
- | OperationOk<void>
- | OperationFail<HttpStatusCode.PaymentRequired>
- | OperationFail<HttpStatusCode.Forbidden>
- | OperationFail<HttpStatusCode.NotFound>
- | OperationAlternative<HttpStatusCode.Conflict, PurseConflictPartial>
- | OperationAlternative<
- HttpStatusCode.UnavailableForLegalReasons,
- LegitimizationNeededResponse
- >
- > {
- const resp = await this.fetch(`reserves/${pursePub}/purse`, {
- method: "POST",
- body,
- });
- switch (resp.status) {
- case HttpStatusCode.Ok:
- // FIXME: parse PurseCreateSuccessResponse
- return opSuccessFromHttp(resp, codecForAny());
- case HttpStatusCode.PaymentRequired:
- case HttpStatusCode.Forbidden:
- case HttpStatusCode.NotFound:
- return opKnownHttpFailure(resp.status, resp);
- case HttpStatusCode.Conflict:
- return opKnownAlternativeFailure(
- resp,
- resp.status,
- codecForPurseConflictPartial(),
- );
- case HttpStatusCode.UnavailableForLegalReasons:
- return opKnownAlternativeFailure(
- resp,
- resp.status,
- codecForLegitimizationNeededResponse(),
- );
- default:
- return opUnknownHttpFailure(resp);
- }
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#get--contracts-$CONTRACT_PUB
- *
- */
- async getContract(
- pursePub: string,
- ): Promise<
- | OperationOk<ExchangeGetContractResponse>
- | OperationFail<HttpStatusCode.NotFound>
- > {
- const resp = await this.fetch(`contracts/${pursePub}`);
- switch (resp.status) {
- case HttpStatusCode.Ok:
- return opSuccessFromHttp(resp, codecForExchangeGetContractResponse());
- case HttpStatusCode.NotFound:
- return opKnownHttpFailure(resp.status, resp);
- default:
- return opUnknownHttpFailure(resp);
- }
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-deposit
- *
- */
- async depositIntoPurse(
- pursePub: string,
- body: ExchangePurseDeposits,
- ): Promise<
- | OperationOk<void>
- | OperationAlternative<HttpStatusCode.Conflict, PurseConflict>
- | OperationFail<HttpStatusCode.Forbidden>
- | OperationFail<HttpStatusCode.NotFound>
- | OperationFail<HttpStatusCode.Gone>
- > {
- const resp = await this.fetch(`purses/${pursePub}/deposit`, {
- method: "POST",
- body,
- });
- switch (resp.status) {
- case HttpStatusCode.Ok:
- // FIXME: parse PurseDepositSuccessResponse
- return opSuccessFromHttp(resp, codecForAny());
- case HttpStatusCode.Conflict:
- return opKnownAlternativeFailure(
- resp,
- resp.status,
- codecForPurseConflict(),
- );
- case HttpStatusCode.Forbidden:
- case HttpStatusCode.NotFound:
- case HttpStatusCode.Gone:
- return opKnownHttpFailure(resp.status, resp);
- default:
- return opUnknownHttpFailure(resp);
- }
- }
-
- // WADS
-
- /**
- * https://docs.taler.net/core/api-exchange.html#get--wads-$WAD_ID
- *
- */
- async getWadInfo(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- //
- // KYC
- //
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--kyc-wallet
- *
- */
- async notifyKycBalanceLimit(account: ReserveAccount, balance: AmountString) {
- const body: WalletKycRequest = {
- balance,
- reserve_pub: account.id,
- reserve_sig: encodeCrock(
- signWalletAccountSetup(account.signingKey, balance),
- ),
- };
- const resp = await this.fetch(`kyc-wallet`, {
- method: "POST",
- body,
- });
-
- switch (resp.status) {
- case HttpStatusCode.Ok:
- return opSuccessFromHttp(resp, codecForAmlWalletKycCheckResponse());
- case HttpStatusCode.NoContent:
- return opEmptySuccess(resp);
- case HttpStatusCode.Forbidden:
- return opKnownHttpFailure(resp.status, resp);
- case HttpStatusCode.UnavailableForLegalReasons:
- return opKnownAlternativeFailure(
- resp,
- resp.status,
- codecForLegitimizationNeededResponse(),
- );
- default:
- return opUnknownFailure(resp, await readTalerErrorResponse(resp));
- }
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#get--kyc-check-$H_NORMALIZED_PAYTO
- *
- */
- async checkKycStatus(
- signingKey: EddsaPrivP | string,
- paytoHash: string,
- longpoll: boolean = false,
- params: {
- awaitAuth?: boolean;
- } = {},
- ): Promise<
- | OperationOk<void>
- | OperationAlternative<HttpStatusCode.Ok, AccountKycStatus>
- | OperationAlternative<HttpStatusCode.Accepted, AccountKycStatus>
- | OperationFail<HttpStatusCode.Forbidden>
- | OperationFail<HttpStatusCode.NotFound>
- | OperationFail<HttpStatusCode.Conflict>
- > {
- const url = new URL(`kyc-check/${paytoHash}`, this.baseUrl);
- if (params.awaitAuth !== undefined) {
- url.searchParams.set("await_auth", params.awaitAuth ? "YES" : "NO");
- }
-
- const signature =
- typeof signingKey === "string"
- ? signingKey
- : encodeCrock(signKycAuth(signingKey));
-
- const resp = await this.fetch(
- url,
- {
- headers: {
- "Account-Owner-Signature": signature,
- },
- },
- longpoll,
- );
-
- switch (resp.status) {
- case HttpStatusCode.Ok:
- case HttpStatusCode.Accepted:
- return opKnownAlternativeFailure(
- resp,
- resp.status,
- codecForAccountKycStatus(),
- );
- case HttpStatusCode.NoContent:
- return opEmptySuccess(resp);
- case HttpStatusCode.Forbidden:
- case HttpStatusCode.NotFound:
- case HttpStatusCode.Conflict:
- return opKnownHttpFailure(resp.status, resp);
- default:
- return opUnknownHttpFailure(resp);
- }
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#get--kyc-info-$ACCESS_TOKEN
- *
- */
- async checkKycInfo(
- token: AccessToken,
- known: KycRequirementInformationId[],
- longpoll: boolean = false,
- ) {
- const resp = await this.fetch(
- `kyc-info/${token}`,
- {
- method: "GET",
- headers: {
- "If-None-Match": known.length ? known.join(",") : undefined,
- },
- },
- longpoll,
- );
- switch (resp.status) {
- case HttpStatusCode.Ok:
- return opSuccessFromHttp(resp, codecForKycProcessClientInformation());
- case HttpStatusCode.Accepted:
- case HttpStatusCode.NoContent:
- return opKnownAlternativeFailure(
- resp,
- resp.status,
- codecForEmptyObject(),
- );
- case HttpStatusCode.NotModified:
- return opKnownHttpFailure(resp.status, resp);
- default:
- return opUnknownFailure(resp, await readTalerErrorResponse(resp));
- }
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--kyc-upload-$ID
- *
- */
- async uploadKycForm<T extends ExchangeKycUploadFormRequest>(
- requirement: KycRequirementInformationId,
- body: T,
- ) {
- const resp = await this.fetch(`kyc-upload/${requirement}`, {
- method: "POST",
- body,
- compress: this.preventCompression ? undefined : "deflate",
- });
- switch (resp.status) {
- case HttpStatusCode.NoContent: {
- this.cacheEvictor.notifySuccess(
- TalerExchangeCacheEviction.UPLOAD_KYC_FORM,
- );
- return opEmptySuccess(resp);
- }
- case HttpStatusCode.NotFound:
- case HttpStatusCode.Conflict:
- case HttpStatusCode.PayloadTooLarge:
- return opKnownHttpFailure(resp.status, resp);
- default:
- return opUnknownHttpFailure(resp);
- }
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--kyc-start-$ID
- *
- */
- async startExternalKycProcess(
- requirement: KycRequirementInformationId,
- body: object = {},
- ) {
- const resp = await this.fetch(`kyc-start/${requirement}`, {
- method: "POST",
- body,
- });
- switch (resp.status) {
- case HttpStatusCode.Ok:
- return opSuccessFromHttp(resp, codecForKycProcessStartInformation());
- case HttpStatusCode.NotFound:
- case HttpStatusCode.Conflict:
- case HttpStatusCode.PayloadTooLarge:
- return opKnownHttpFailure(resp.status, resp);
- default:
- return opUnknownHttpFailure(resp);
- }
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#get--kyc-proof-$PROVIDER_NAME?state=$H_PAYTO
- *
- */
- async completeExternalKycProcess(
- provider: string,
- state: string,
- code: string,
- ) {
- const resp = await this.fetch(
- `kyc-proof/${provider}?state=${state}&code=${code}`,
- {
- method: "GET",
- redirect: "manual",
- },
- );
-
- switch (resp.status) {
- case HttpStatusCode.SeeOther:
- return opEmptySuccess(resp);
- case HttpStatusCode.NotFound:
- return opKnownHttpFailure(resp.status, resp);
- default:
- return opUnknownHttpFailure(resp);
- }
- }
-
- //
- // AML operations
- //
-
- /**
- * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-measures
- *
- */
- async getAmlMesasures(auth: OfficerAccount) {
- const resp = await this.fetch(`aml/${auth.id}/measures`, {
- method: "GET",
- headers: {
- "Taler-AML-Officer-Signature": encodeCrock(
- signAmlQuery(auth.signingKey),
- ),
- },
- });
-
- switch (resp.status) {
- case HttpStatusCode.Ok:
- return opSuccessFromHttp(resp, codecForAvailableMeasureSummary());
- case HttpStatusCode.Conflict:
- case HttpStatusCode.NotFound:
- case HttpStatusCode.Forbidden:
- return opKnownHttpFailure(resp.status, resp);
- default:
- return opUnknownHttpFailure(resp);
- }
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-measures
- *
- */
- async getAmlKycStatistics(
- auth: OfficerAccount,
- name: string,
- filter: {
- since?: AbsoluteTime;
- until?: AbsoluteTime;
- } = {},
- ) {
- const url = new URL(`aml/${auth.id}/kyc-statistics/${name}`, this.baseUrl);
-
- if (filter.since !== undefined && filter.since.t_ms !== "never") {
- url.searchParams.set("start_date", String(filter.since.t_ms));
- }
- if (filter.until !== undefined && filter.until.t_ms !== "never") {
- url.searchParams.set("end_date", String(filter.until.t_ms));
- }
-
- const resp = await this.fetch(url, {
- method: "GET",
- headers: {
- "Taler-AML-Officer-Signature": encodeCrock(
- signAmlQuery(auth.signingKey),
- ),
- },
- });
- switch (resp.status) {
- case HttpStatusCode.Ok:
- return opSuccessFromHttp(resp, codecForEventCounter());
- case HttpStatusCode.Conflict:
- case HttpStatusCode.NotFound:
- case HttpStatusCode.Forbidden:
- return opKnownHttpFailure(resp.status, resp);
- default:
- return opUnknownHttpFailure(resp);
- }
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-decisions
- *
- */
- async getAmlDecisions(
- auth: OfficerAccount,
- params: PaginationParams & {
- account?: string;
- active?: boolean;
- investigation?: boolean;
- } = {},
- ) {
- const url = new URL(`aml/${auth.id}/decisions`, this.baseUrl);
-
- addPaginationParams(url, params);
- if (params.account !== undefined) {
- url.searchParams.set("h_payto", params.account);
- }
- if (params.active !== undefined) {
- url.searchParams.set("active", params.active ? "YES" : "NO");
- }
- if (params.investigation !== undefined) {
- url.searchParams.set(
- "investigation",
- params.investigation ? "YES" : "NO",
- );
- }
-
- const resp = await this.fetch(url, {
- headers: {
- "Taler-AML-Officer-Signature": encodeCrock(
- signAmlQuery(auth.signingKey),
- ),
- },
- });
-
- switch (resp.status) {
- case HttpStatusCode.Ok:
- return opSuccessFromHttp(resp, codecForAmlDecisionsResponse());
- case HttpStatusCode.NoContent:
- return opFixedSuccess({ records: [] });
- case HttpStatusCode.Forbidden:
- case HttpStatusCode.NotFound:
- case HttpStatusCode.Conflict:
- return opKnownHttpFailure(resp.status, resp);
- default:
- return opUnknownHttpFailure(resp);
- }
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-attributes-$H_PAYTO
- *
- */
- async getAmlAttributesForAccount(
- auth: OfficerAccount,
- account: string,
- params: PaginationParams = {},
- ) {
- const url = new URL(`aml/${auth.id}/attributes/${account}`, this.baseUrl);
-
- addPaginationParams(url, params);
- const resp = await this.fetch(url, {
- headers: {
- "Taler-AML-Officer-Signature": encodeCrock(
- signAmlQuery(auth.signingKey),
- ),
- },
- });
-
- switch (resp.status) {
- case HttpStatusCode.Ok:
- return opSuccessFromHttp(resp, codecForAmlKycAttributes());
- case HttpStatusCode.NoContent:
- return opFixedSuccess({ details: [] });
- case HttpStatusCode.Forbidden:
- case HttpStatusCode.NotFound:
- case HttpStatusCode.Conflict:
- return opKnownHttpFailure(resp.status, resp);
- default:
- return opUnknownHttpFailure(resp);
- }
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--aml-$OFFICER_PUB-decision
- *
- */
- async makeAmlDesicion(
- auth: OfficerAccount,
- decision: Omit<AmlDecisionRequest, "officer_sig">,
- ) {
- const body: AmlDecisionRequest = {
- officer_sig: encodeCrock(signAmlDecision(auth.signingKey, decision)),
- ...decision,
- };
- const resp = await this.fetch(`aml/${auth.id}/decision`, {
- method: "POST",
- headers: {
- "Taler-AML-Officer-Signature": encodeCrock(
- signAmlQuery(auth.signingKey),
- ),
- },
- body,
- compress: this.preventCompression ? undefined : "deflate",
- });
-
- switch (resp.status) {
- case HttpStatusCode.NoContent: {
- this.cacheEvictor.notifySuccess(
- TalerExchangeCacheEviction.MAKE_AML_DECISION,
- );
- return opEmptySuccess(resp);
- }
- case HttpStatusCode.Forbidden:
- case HttpStatusCode.NotFound:
- case HttpStatusCode.Conflict:
- return opKnownHttpFailure(resp.status, resp);
- default:
- return opUnknownHttpFailure(resp);
- }
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-transfers-credit
- *
- */
- async getTransfersCredit(
- auth: OfficerAccount,
- params: PaginationParams & { threshold?: AmountJson } = {},
- ) {
- const url = new URL(`aml/${auth.id}/transfers-credit`, this.baseUrl);
-
- addPaginationParams(url, params);
-
- if (params.threshold) {
- url.searchParams.set("threshold", Amounts.stringify(params.threshold));
- }
-
- const resp = await this.fetch(url, {
- headers: {
- "Taler-AML-Officer-Signature": encodeCrock(
- signAmlQuery(auth.signingKey),
- ),
- },
- });
-
- switch (resp.status) {
- case HttpStatusCode.Ok:
- return opSuccessFromHttp(resp, codecForExchangeTransferList());
- case HttpStatusCode.NoContent:
- return opFixedSuccess({ transfers: [] });
- case HttpStatusCode.Forbidden:
- case HttpStatusCode.NotFound:
- case HttpStatusCode.Conflict:
- return opKnownHttpFailure(resp.status, resp);
- default:
- return opUnknownHttpFailure(resp);
- }
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-transfers-debit
- *
- */
- async getTransfersDebit(
- auth: OfficerAccount,
- params: PaginationParams & { threshold?: AmountJson } = {},
- ) {
- const url = new URL(`aml/${auth.id}/transfers-debit`, this.baseUrl);
-
- addPaginationParams(url, params);
-
- if (params.threshold) {
- url.searchParams.set("threshold", Amounts.stringify(params.threshold));
- }
-
- const resp = await this.fetch(url, {
- headers: {
- "Taler-AML-Officer-Signature": encodeCrock(
- signAmlQuery(auth.signingKey),
- ),
- },
- });
-
- switch (resp.status) {
- case HttpStatusCode.Ok:
- return opSuccessFromHttp(resp, codecForExchangeTransferList());
- case HttpStatusCode.NoContent:
- return opFixedSuccess({ transfers: [] });
- case HttpStatusCode.Forbidden:
- case HttpStatusCode.NotFound:
- case HttpStatusCode.Conflict:
- return opKnownHttpFailure(resp.status, resp);
- default:
- return opUnknownHttpFailure(resp);
- }
- }
-
- // RESERVE control
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--reserves-$RESERVE_PUB-open
- *
- */
- async reserveOpen(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#get--reserves-attest-$RESERVE_PUB
- *
- */
- async getReserveAttributes(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--reserves-attest-$RESERVE_PUB
- *
- */
- async signReserveAttributes(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#post--reserves-$RESERVE_PUB-close
- *
- */
- async closeReserve(): Promise<never> {
- throw Error("not yet implemented");
- }
-
- /**
- * https://docs.taler.net/core/api-exchange.html#delete--reserves-$RESERVE_PUB
- *
- */
- async deleteReserve(): Promise<never> {
- throw Error("not yet implemented");
- }
-}
diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts
@@ -16,7 +16,7 @@ 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/challenger.js";
-export * from "./http-client/exchange.js";
+export * from "./http-client/exchange-client.js";
export * from "./http-client/merchant.js";
export * from "./http-client/officer-account.js";
@@ -33,6 +33,7 @@ export * from "./invariants.js";
export * from "./kdf.js";
export * from "./libtool-version.js";
export * from "./logging.js";
+export * from "./longpool-queue.js";
export {
crypto_sign_keyPair_fromSeed,
randomBytes,
@@ -57,7 +58,6 @@ export * from "./time.js";
export * from "./timer.js";
export * from "./transaction-test-data.js";
export * from "./url.js";
-export * from "./longpool-queue.js"
// FIXME: remove all this, needs refactor
export * from "./types-taler-bank-conversion.js";
@@ -87,8 +87,8 @@ export * from "./account-restrictions.js";
export * from "./aml/aml-events.js";
export * from "./aml/aml-properties.js";
-export * from "./taler-form-attributes.js";
export * from "./taler-account-properties.js";
+export * from "./taler-form-attributes.js";
export * from "./iso-3166.js";
export * from "./iso-4217.js";
diff --git a/packages/taler-wallet-core/src/versions.ts b/packages/taler-wallet-core/src/versions.ts
@@ -14,12 +14,15 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { TalerExchangeHttpClient } from "@gnu-taler/taler-util";
+
/**
* Protocol version spoken with the exchange.
*
* Uses libtool's current:revision:age versioning.
*/
-export const WALLET_EXCHANGE_PROTOCOL_VERSION = "17:0:0";
+export const WALLET_EXCHANGE_PROTOCOL_VERSION =
+ TalerExchangeHttpClient.SUPPORTED_EXCHANGE_PROTOCOL_VERSION;
/**
* Protocol version spoken with the merchant.
diff --git a/packages/web-util/src/context/exchange-api.ts b/packages/web-util/src/context/exchange-api.ts
@@ -56,8 +56,8 @@ export type ExchangeContextType = {
/**
* Do not use. This is here because the AML dashboard does too many
* request to the event API
- *
- * FIXME: The server should expose a better API
+ *
+ * FIXME: The server should expose a better API
* https://bugs.gnunet.org/view.php?id=9776
* @deprecated
*/
@@ -171,7 +171,12 @@ export const ExchangeApiProvider = ({
});
}
- const { lib: unthrottledApi } = buildExchangeApiClient(baseUrl, evictors, !!preventCompression, true)
+ const { lib: unthrottledApi } = buildExchangeApiClient(
+ baseUrl,
+ evictors,
+ !!preventCompression,
+ true,
+ );
const value: ExchangeContextType = {
url: baseUrl,
@@ -206,7 +211,11 @@ function buildExchangeApiClient(
},
});
- const ex = new TalerExchangeHttpClient(url.href, { httpClient: httpLib, cacheEvictor: evictors.exchange, preventCompression });
+ const ex = new TalerExchangeHttpClient(url.href, {
+ httpClient: httpLib,
+ cacheEvictor: evictors.exchange,
+ preventCompression,
+ });
async function getRemoteConfig(): Promise<KeysAndConfigType> {
const configResp = await ex.getConfig();
@@ -237,7 +246,7 @@ function buildExchangeApiClient(
return {
getRemoteConfig,
- VERSION: ex.PROTOCOL_VERSION,
+ VERSION: TalerExchangeHttpClient.SUPPORTED_EXCHANGE_PROTOCOL_VERSION,
lib: {
exchange: ex,
},