commit f77c4e71874a2f4493079f796976354759b3f95a parent 2ecc2e5a02ddb2df01aa64bc5dc0e76f4978b866 Author: Sebastian <sebasjm@gmail.com> Date: Sat, 3 May 2025 16:41:55 -0300 normal exchange httpclient without longpolling and renamed the wallet's exchange httpclient Diffstat:
17 files changed, 1511 insertions(+), 98 deletions(-)
diff --git a/packages/kyc-ui/src/hooks/kyc.ts b/packages/kyc-ui/src/hooks/kyc.ts @@ -36,7 +36,7 @@ export function useKycInfo(token: AccessToken) { } = useExchangeApiContext(); async function fetcher([ac]: [AccessToken]) { - return await api.checkKycInfo(ac, [], true); + return await api.checkKycInfo(ac, []); } const { data, error } = useSWR< TalerExchangeResultByMethod<"checkKycInfo">, diff --git a/packages/kyc-ui/src/pages/Start.tsx b/packages/kyc-ui/src/pages/Start.tsx @@ -77,6 +77,9 @@ export function ShowReqList({ case HttpStatusCode.Accepted: { return <div> accepted </div>; } + // case HttpStatusCode.Forbidden: { + // return <div> forbidden </div>; + // } default: { assertUnreachable(result); } diff --git a/packages/kyc-ui/src/pages/TriggerKyc.tsx b/packages/kyc-ui/src/pages/TriggerKyc.tsx @@ -13,6 +13,9 @@ 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 { encodeCrock } from "@gnu-taler/taler-util"; +import { WalletKycRequest } from "@gnu-taler/taler-util"; +import { signWalletAccountSetup } from "@gnu-taler/taler-util"; import { AbsoluteTime, AccessToken, @@ -98,7 +101,7 @@ export function TriggerKyc({ onKycStarted }: Props): VNode { case HttpStatusCode.Ok: case HttpStatusCode.Accepted: onKycStarted(result.body.access_token); - break + break; case HttpStatusCode.Forbidden: { notify({ type: "error", @@ -137,11 +140,15 @@ export function TriggerKyc({ onKycStarted }: Props): VNode { return withErrorHandler( async () => { const account = await accountPromise; - - return lib.exchange.notifyKycBalanceLimit( - account, - Amounts.stringify(amount), - ); + const balance = Amounts.stringify(amount); + const limit: WalletKycRequest = { + balance, + reserve_pub: account.id, + reserve_sig: encodeCrock( + signWalletAccountSetup(account.signingKey, balance), + ), + }; + return lib.exchange.notifyKycBalanceLimit(limit); }, (res) => { notify({ diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx @@ -21,16 +21,15 @@ import { getMerchantAccountKycStatusSimplified, - MerchantAccountKycStatus, MerchantAccountKycStatusSimplified, - TalerError, + TalerError } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; +import { Fragment, h, VNode } from "preact"; import { useSessionContext } from "../../context/session.js"; -import { useInstanceKYCDetails, useInstanceKYCDetailsLongPolling } from "../../hooks/instance.js"; -import { LangSelector } from "./LangSelector.js"; +import { useInstanceKYCDetailsLongPolling } from "../../hooks/instance.js"; import { usePreference } from "../../hooks/preference.js"; +import { LangSelector } from "./LangSelector.js"; // const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined; diff --git a/packages/taler-util/src/ReserveTransaction.ts b/packages/taler-util/src/ReserveTransaction.ts @@ -38,6 +38,8 @@ import { EddsaSignatureString, EddsaPublicKeyString, CoinPublicKeyString, + codecForEddsaSignature, + codecForEddsaPublicKey, } from "./types-taler-common.js"; import { AbsoluteTime, @@ -193,7 +195,7 @@ export const codecForReserveWithdrawTransaction = .property("amount", codecForAmountString()) .property("h_coin_envelope", codecForString()) .property("h_denom_pub", codecForString()) - .property("reserve_sig", codecForString()) + .property("reserve_sig", codecForEddsaSignature()) .property("type", codecForConstString(ReserveTransactionType.Withdraw)) .property("withdraw_fee", codecForAmountString()) .build("ReserveWithdrawTransaction"); @@ -213,8 +215,8 @@ export const codecForReserveClosingTransaction = buildCodecForObject<ReserveClosingTransaction>() .property("amount", codecForAmountString()) .property("closing_fee", codecForAmountString()) - .property("exchange_pub", codecForString()) - .property("exchange_sig", codecForString()) + .property("exchange_pub", codecForEddsaPublicKey()) + .property("exchange_sig", codecForEddsaSignature()) .property("h_wire", codecForString()) .property("timestamp", codecForTimestamp) .property("type", codecForConstString(ReserveTransactionType.Closing)) @@ -226,8 +228,8 @@ export const codecForReserveRecoupTransaction = buildCodecForObject<ReserveRecoupTransaction>() .property("amount", codecForAmountString()) .property("coin_pub", codecForString()) - .property("exchange_pub", codecForString()) - .property("exchange_sig", codecForString()) + .property("exchange_pub", codecForEddsaPublicKey()) + .property("exchange_sig", codecForEddsaSignature()) .property("timestamp", codecForTimestamp) .property("type", codecForConstString(ReserveTransactionType.Recoup)) .build("ReserveRecoupTransaction"); diff --git a/packages/taler-util/src/http-client/exchange-client.ts b/packages/taler-util/src/http-client/exchange-client.ts @@ -94,6 +94,7 @@ import { Amounts, CancellationToken, LongpollQueue, + ReservePub, signAmlDecision, signAmlQuery, signKycAuth, @@ -103,28 +104,25 @@ 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 type TalerExchangeResultByMethod2< + prop extends keyof TalerExchangeHttpClient2, +> = ResultByMethod<TalerExchangeHttpClient2, prop>; +export type TalerExchangeErrorsByMethod2< + prop extends keyof TalerExchangeHttpClient2, +> = FailCasesByMethod<TalerExchangeHttpClient2, prop>; -export enum TalerExchangeCacheEviction { +export enum TalerExchangeCacheEviction2 { 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 { +export class TalerExchangeHttpClient2 { public static readonly SUPPORTED_EXCHANGE_PROTOCOL_VERSION = "27:0:2"; private httpLib: HttpRequestLibrary; - private cacheEvictor: CacheEvictor<TalerExchangeCacheEviction>; + private cacheEvictor: CacheEvictor<TalerExchangeCacheEviction2>; private preventCompression: boolean; private cancelationToken: CancellationToken; private longPollQueue: LongpollQueue; @@ -133,7 +131,7 @@ export class TalerExchangeHttpClient { readonly baseUrl: string, params: { httpClient?: HttpRequestLibrary; - cacheEvictor?: CacheEvictor<TalerExchangeCacheEviction>; + cacheEvictor?: CacheEvictor<TalerExchangeCacheEviction2>; preventCompression?: boolean; cancelationToken?: CancellationToken; longPollQueue?: LongpollQueue; @@ -149,7 +147,7 @@ export class TalerExchangeHttpClient { isCompatible(version: string): boolean { const compare = LibtoolVersion.compare( - TalerExchangeHttpClient.SUPPORTED_EXCHANGE_PROTOCOL_VERSION, + TalerExchangeHttpClient2.SUPPORTED_EXCHANGE_PROTOCOL_VERSION, version, ); return compare?.compatible ?? false; @@ -246,7 +244,7 @@ export class TalerExchangeHttpClient { 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}`, + detail: `Unsupported protocol version, client supports ${TalerExchangeHttpClient2.SUPPORTED_EXCHANGE_PROTOCOL_VERSION}, server supports ${minBody.version}`, }); } // Now that we've checked the basic body, re-parse the full response. @@ -874,10 +872,10 @@ export class TalerExchangeHttpClient { async notifyKycBalanceLimit(account: ReserveAccount, balance: AmountString) { const body: WalletKycRequest = { balance, - reserve_pub: account.id, + reserve_pub: account.id as any, reserve_sig: encodeCrock( signWalletAccountSetup(account.signingKey, balance), - ), + ) as any, }; const resp = await this.fetch(`kyc-wallet`, { method: "POST", @@ -1012,7 +1010,7 @@ export class TalerExchangeHttpClient { switch (resp.status) { case HttpStatusCode.NoContent: { this.cacheEvictor.notifySuccess( - TalerExchangeCacheEviction.UPLOAD_KYC_FORM, + TalerExchangeCacheEviction2.UPLOAD_KYC_FORM, ); return opEmptySuccess(resp); } @@ -1240,7 +1238,7 @@ export class TalerExchangeHttpClient { decision: Omit<AmlDecisionRequest, "officer_sig">, ) { const body: AmlDecisionRequest = { - officer_sig: encodeCrock(signAmlDecision(auth.signingKey, decision)), + officer_sig: encodeCrock(signAmlDecision(auth.signingKey, decision)) as any, ...decision, }; const resp = await this.fetch(`aml/${auth.id}/decision`, { @@ -1257,7 +1255,7 @@ export class TalerExchangeHttpClient { switch (resp.status) { case HttpStatusCode.NoContent: { this.cacheEvictor.notifySuccess( - TalerExchangeCacheEviction.MAKE_AML_DECISION, + TalerExchangeCacheEviction2.MAKE_AML_DECISION, ); return opEmptySuccess(resp); } diff --git a/packages/taler-util/src/http-client/exchange.ts b/packages/taler-util/src/http-client/exchange.ts @@ -0,0 +1,1334 @@ +/* + 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, + 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, + OperationFail, + OperationOk, + ResultByMethod, + opEmptySuccess, + opFixedSuccess, + opKnownAlternativeFailure, + opKnownHttpFailure, + opSuccessFromHttp, + opUnknownFailure, + opUnknownHttpFailure +} from "../operation.js"; +import { EddsaPrivP, encodeCrock } from "../taler-crypto.js"; +import { + AccessToken, + EddsaPublicKeyString, + EddsaSignatureString, + OfficerAccount, + PaginationParams, + PaytoHash, + codecForTalerCommonConfigResponse +} from "../types-taler-common.js"; +import { + AmlDecisionRequest, + BatchWithdrawResponse, + ExchangeKycUploadFormRequest, + ExchangeLegacyBatchWithdrawRequest, + ExchangePurseDeposits, + ExchangePurseMergeRequest, + ExchangeReservePurseRequest, + ExchangeVersionResponse, + KycRequirementInformationId, + PurseCreate, + 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 +} 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 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.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() { + const url = new URL(`seed`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + }); + 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 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-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.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 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, 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 url = new URL(`reserves/${rid}/batch-withdraw`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp( + resp, + //FIXME: missing codec for BWR + 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: EddsaPublicKeyString) { + const url = new URL(`purses/${pursePub}/merge`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + }); + 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: EddsaPublicKeyString) { + const url = new URL(`purses/${pursePub}/deposit`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + }); + 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: EddsaPublicKeyString, + body: PurseCreate, + ) { + const url = new URL(`purses/${pursePub}/create`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + 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) { + const url = new URL(`purses/${pursePub}`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + 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) { + const url = new URL(`purses/${pursePub}/merge`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + 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: EddsaPublicKeyString, + body: ExchangeReservePurseRequest, + ) { + const url = new URL(`reserves/${pursePub}/purse`, this.baseUrl); + + const resp = await this.httpLib.fetch(url.href, { + 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: EddsaPublicKeyString) { + const url = new URL(`contracts/${pursePub}`, this.baseUrl); + + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + }); + 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: EddsaPublicKeyString, + body: ExchangePurseDeposits, + ) { + const url = new URL(`purses/${pursePub}/deposit`, this.baseUrl); + + const resp = await this.httpLib.fetch(url.href, { + 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(body: WalletKycRequest) { + // const body: WalletKycRequest = { + // balance, + // reserve_pub: account.id, + // reserve_sig: encodeCrock( + // signWalletAccountSetup(account.signingKey, balance), + // ), + // }; + const url = new URL(`kyc-wallet`, this.baseUrl); + + const resp = await this.httpLib.fetch(url.href, { + 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: PaytoHash, + params: { + awaitAuth?: boolean; + } = {}, + ) { + 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.httpLib.fetch(url.href, { + headers: { + "Account-Owner-Signature": signature, + }, + }); + + 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[]) { + const url = new URL(`kyc-info/${token}`, this.baseUrl); + + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers: { + "If-None-Match": known.length ? known.join(",") : undefined, + }, + }); + 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 url = new URL(`kyc-upload/${requirement}`, this.baseUrl); + + const resp = await this.httpLib.fetch(url.href, { + 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 url = new URL(`kyc-start/${requirement}`, this.baseUrl); + + const resp = await this.httpLib.fetch(url.href, { + 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 url = new URL(`kyc-proof/${provider}`, this.baseUrl); + url.searchParams.set("state", state); + url.searchParams.set("code", code); + const resp = await this.httpLib.fetch(url.href, { + 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 url = new URL(`aml/${auth.id}/measures`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + 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.httpLib.fetch(url.href, { + 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.httpLib.fetch(url.href, { + 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.httpLib.fetch(url.href, { + 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), + ) as EddsaSignatureString, + ...decision, + }; + const url = new URL(`aml/${auth.id}/decision`, this.baseUrl); + + const resp = await this.httpLib.fetch(url.href, { + 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.httpLib.fetch(url.href, { + 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.httpLib.fetch(url.href, { + 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,6 +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"; diff --git a/packages/taler-util/src/types-taler-common.ts b/packages/taler-util/src/types-taler-common.ts @@ -85,12 +85,12 @@ export type ClaimToken = string; // EdDSA and ECDHE public keys always point on Curve25519 // and represented using the standard 256 bits Ed25519 compact format, // converted to Crockford Base32. -export type EddsaPublicKey = string; +export type EddsaPublicKey = EddsaPublicKeyString; // EdDSA and ECDHE public keys always point on Curve25519 // and represented using the standard 256 bits Ed25519 compact format, // converted to Crockford Base32. -export type EddsaPrivateKey = string; +export type EddsaPrivateKey = EddsaPrivateKeyString; // Edx25519 public keys are points on Curve25519 and represented using the // standard 256 bits Ed25519 compact format converted to Crockford @@ -193,9 +193,12 @@ export type AmountString = | LitAmountString; // export type AmountString = string; export type Base32String = string; -export type EddsaSignatureString = string; -export type EddsaPublicKeyString = string; -export type EddsaPrivateKeyString = string; +declare const __eddsasign_str: unique symbol; +export type EddsaSignatureString = string;// & { [__eddsasign_str]: true }; +declare const __eddsapub_str: unique symbol; +export type EddsaPublicKeyString = string;// & { [__eddsapub_str]: true }; +declare const __eddsapriv_str: unique symbol; +export type EddsaPrivateKeyString = string;// & { [__eddsapriv_str]: true }; export type CoinPublicKeyString = string; // FIXME: implement this codec @@ -207,9 +210,11 @@ export const codecForCurrencyName = codecForString; // FIXME: implement this codec export const codecForDecimalNumber = codecForString; // FIXME: implement this codec -export const codecForEddsaPublicKey = codecForString; +export const codecForEddsaPublicKey = codecForString as () => Codec<EddsaPublicKeyString>; // FIXME: implement this codec -export const codecForEddsaSignature = codecForString; +export const codecForEddsaPrivateKey = codecForString as () => Codec<EddsaPrivateKeyString>; +// FIXME: implement this codec +export const codecForEddsaSignature = codecForString as () => Codec<EddsaSignatureString>; export const codecForInternationalizedString = (): Codec<InternationalizedString> => codecForMap(codecForString()); @@ -328,8 +333,8 @@ export interface DenominationExpiredMessage { export const codecForDenominationExpiredMessage = () => buildCodecForObject<DenominationExpiredMessage>() .property("code", codecForNumber()) - .property("exchange_sig", codecForString()) - .property("exchange_pub", codecForString()) + .property("exchange_sig", codecForEddsaSignature()) + .property("exchange_pub", codecForEddsaPublicKey()) .property("h_denom_pub", codecForString()) .property("timestamp", codecForTimestamp) .property("oper", codecForString()) diff --git a/packages/taler-util/src/types-taler-exchange.ts b/packages/taler-util/src/types-taler-exchange.ts @@ -71,6 +71,8 @@ import { Timestamp, WireSalt, codecForAccessToken, + codecForEddsaPublicKey, + codecForEddsaSignature, codecForInternationalizedString, codecForURLString, } from "./types-taler-common.js"; @@ -844,7 +846,7 @@ export const codecForExchangeWireAccount = (): Codec<ExchangeWireAccount> => .property("conversion_url", codecOptional(codecForStringURL())) .property("credit_restrictions", codecForList(codecForAny())) .property("debit_restrictions", codecForList(codecForAny())) - .property("master_sig", codecForString()) + .property("master_sig", codecForEddsaSignature()) .property("payto_uri", codecForString()) .property("bank_label", codecOptional(codecForString())) .property("priority", codecOptional(codecForNumber())) @@ -890,8 +892,8 @@ export interface ExchangeRefundSuccessResponse { export const codecForExchangeRefundSuccessResponse = (): Codec<ExchangeRefundSuccessResponse> => buildCodecForObject<ExchangeRefundSuccessResponse>() - .property("exchange_pub", codecForString()) - .property("exchange_sig", codecForString()) + .property("exchange_pub", codecForEddsaPublicKey()) + .property("exchange_sig", codecForEddsaSignature()) .build("ExchangeRefundSuccessResponse"); export type AccountRestriction = @@ -1000,8 +1002,8 @@ export const codecForRecoup = (): Codec<Recoup> => export const codecForExchangeSigningKey = (): Codec<ExchangeSignKeyJson> => buildCodecForObject<ExchangeSignKeyJson>() - .property("key", codecForString()) - .property("master_sig", codecForString()) + .property("key", codecForEddsaPublicKey()) + .property("master_sig", codecForEddsaSignature()) .property("stamp_end", codecForTimestamp) .property("stamp_start", codecForTimestamp) .property("stamp_expire", codecForTimestamp) @@ -1017,7 +1019,7 @@ export const codecForGlobalFees = (): Codec<GlobalFees> => .property("history_expiration", codecForDuration) .property("purse_account_limit", codecForNumber()) .property("purse_timeout", codecForDuration) - .property("master_sig", codecForString()) + .property("master_sig", codecForEddsaSignature()) .build("GlobalFees"); // FIXME: Validate properly! @@ -1126,8 +1128,8 @@ export const codecForExchangeRevealMeltResponseV2 = export const codecForExchangeMeltResponse = (): Codec<ExchangeMeltResponse> => buildCodecForObject<ExchangeMeltResponse>() - .property("exchange_pub", codecForString()) - .property("exchange_sig", codecForString()) + .property("exchange_pub", codecForEddsaPublicKey()) + .property("exchange_sig", codecForEddsaSignature()) .property("noreveal_index", codecForNumber()) .property("refresh_base_url", codecOptional(codecForString())) .build("ExchangeMeltResponse"); @@ -1266,8 +1268,8 @@ export const codecForExchangeMergeSuccessResponse = buildCodecForObject<ExchangeMergeSuccessResponse>() .property("merge_amount", codecForAmountString()) .property("exchange_timestamp", codecForTimestamp) - .property("exchange_sig", codecForString()) - .property("exchange_pub", codecForString()) + .property("exchange_sig", codecForEddsaSignature()) + .property("exchange_pub", codecForEddsaPublicKey()) .build("ExchangeMergeSuccessResponse"); /** @@ -1294,8 +1296,8 @@ export const codecForExchangeMergeConflictResponse = (): Codec<ExchangeMergeConflictResponse> => buildCodecForObject<ExchangeMergeConflictResponse>() .property("merge_timestamp", codecForTimestamp) - .property("merge_sig", codecForString()) - .property("reserve_pub", codecForString()) + .property("merge_sig", codecForEddsaSignature()) + .property("reserve_pub", codecForEddsaPublicKey()) .property("partner_url", codecOptional(codecForString())) .build("ExchangeMergeConflictResponse"); @@ -1484,8 +1486,8 @@ export interface BatchDepositSuccess { export const codecForBatchDepositSuccess = (): Codec<BatchDepositSuccess> => buildCodecForObject<BatchDepositSuccess>() - .property("exchange_pub", codecForString()) - .property("exchange_sig", codecForString()) + .property("exchange_pub", codecForEddsaPublicKey()) + .property("exchange_sig", codecForEddsaSignature()) .property("exchange_timestamp", codecForTimestamp) .property("transaction_base_url", codecOptional(codecForString())) .build("BatchDepositSuccess"); @@ -1544,8 +1546,8 @@ export const codecForTackTransactionWired = (): Codec<TrackTransactionWired> => .property("wtid", codecForString()) .property("execution_time", codecForTimestamp) .property("coin_contribution", codecForAmountString()) - .property("exchange_sig", codecForString()) - .property("exchange_pub", codecForString()) + .property("exchange_sig", codecForEddsaSignature()) + .property("exchange_pub", codecForEddsaPublicKey()) .build("TackTransactionWired"); export interface TrackTransactionAccepted { @@ -1585,7 +1587,7 @@ export const codecForTackTransactionAccepted = .property("requirement_row", codecOptional(codecForNumber())) .property("kyc_ok", codecForBoolean()) .property("execution_time", codecForTimestamp) - .property("account_pub", codecOptional(codecForString())) + .property("account_pub", codecOptional(codecForEddsaPublicKey())) .build("TackTransactionAccepted"); export const codecForPeerContractTerms = (): Codec<PeerContractTerms> => @@ -2689,7 +2691,7 @@ export const codecForLegitimizationNeededResponse = .property("code", codecForNumber()) .property("hint", codecOptional(codecForString())) .property("h_payto", codecForString()) - .property("account_pub", codecOptional(codecForString())) + .property("account_pub", codecOptional(codecForEddsaPublicKey())) .property("requirement_row", codecForNumber()) .property("bad_kyc_auth", codecOptional(codecForBoolean())) .build("TalerExchangeApi.LegitimizationNeededResponse"); @@ -3272,20 +3274,78 @@ interface PurseContractConflict { // Ephemeral public key for the DH operation to decrypt the contract. contract_pub: EddsaPublicKey; } + +export interface PurseCreate { + // Total value of the purse, excluding fees. + amount: Amount; + + // Minimum age required for all coins deposited into the purse. + min_age: Integer; + + // Optional encrypted contract, in case the buyer is + // proposing the contract and thus establishing the + // purse with the payment. + econtract?: EncryptedContract; + + // EdDSA public key used to approve merges of this purse. + merge_pub: EddsaPublicKey; + + // EdDSA signature of the purse over a + // TALER_PurseRequestSignaturePS + // of purpose TALER_SIGNATURE_WALLET_PURSE_CREATE + // confirming the key + // invariants associated with the purse. + // (amount, h_contract_terms, expiration). + purse_sig: EddsaSignature; + + // SHA-512 hash of the contact of the purse. + h_contract_terms: HashCode; + + // Array of coins being deposited into the purse. + // Maximum length is 128. + deposits: PurseDeposit[]; + + // Indicative time by which the purse should expire + // if it has not been merged into an account. At this + // point, all of the deposits made will be auto-refunded. + purse_expiration: Timestamp; +} + export const codecForPurseConflict = () => buildCodecForUnion<PurseConflict>() .discriminateOn("code") - .alternative(TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, codecForDepositDoubleSpendError()) - .alternative(TalerErrorCode.EXCHANGE_PURSE_CREATE_CONFLICTING_META_DATA, codecForPurseCreateConflict()) - .alternative(TalerErrorCode.EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA, codecForPurseDepositConflict()) - .alternative(TalerErrorCode.EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA, codecForPurseContractConflict()) + .alternative( + TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, + codecForDepositDoubleSpendError(), + ) + .alternative( + TalerErrorCode.EXCHANGE_PURSE_CREATE_CONFLICTING_META_DATA, + codecForPurseCreateConflict(), + ) + .alternative( + TalerErrorCode.EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA, + codecForPurseDepositConflict(), + ) + .alternative( + TalerErrorCode.EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA, + codecForPurseContractConflict(), + ) .build("PurseConflict"); export const codecForPurseConflictPartial = () => buildCodecForUnion<PurseConflictPartial>() .discriminateOn("code") - .alternative(TalerErrorCode.EXCHANGE_PURSE_CREATE_CONFLICTING_META_DATA, codecForPurseCreateConflict()) - .alternative(TalerErrorCode.EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA, codecForPurseDepositConflict()) - .alternative(TalerErrorCode.EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA, codecForPurseContractConflict()) + .alternative( + TalerErrorCode.EXCHANGE_PURSE_CREATE_CONFLICTING_META_DATA, + codecForPurseCreateConflict(), + ) + .alternative( + TalerErrorCode.EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA, + codecForPurseDepositConflict(), + ) + .alternative( + TalerErrorCode.EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA, + codecForPurseContractConflict(), + ) .build("PurseConflictPartial"); export const codecForDepositDoubleSpendError = () => buildCodecForObject<DepositDoubleSpendError>() @@ -3316,4 +3376,4 @@ export const codecForPurseContractConflict = () => .property("h_econtract", codecForString()) .property("econtract_sig", codecForString()) .property("contract_pub", codecForString()) - .build("PurseContractConflict"); -\ No newline at end of file + .build("PurseContractConflict"); diff --git a/packages/taler-util/src/types-taler-merchant.ts b/packages/taler-util/src/types-taler-merchant.ts @@ -78,6 +78,8 @@ import { WireTransferIdentifierRawP, codecForAccessToken, codecForCurrencySpecificiation, + codecForEddsaPublicKey, + codecForEddsaSignature, codecForInternationalizedString, codecForURLString, } from "./types-taler-common.js"; @@ -925,7 +927,7 @@ export interface MerchantBlindSigWrapperV1 { export const codecForAuditorHandle = (): Codec<AuditorHandle> => buildCodecForObject<AuditorHandle>() .property("name", codecForString()) - .property("auditor_pub", codecForString()) + .property("auditor_pub", codecForEddsaPublicKey()) .property("url", codecForString()) .build("AuditorHandle"); @@ -3422,7 +3424,7 @@ const codecForExchangeConfigInfo = (): Codec<ExchangeConfigInfo> => buildCodecForObject<ExchangeConfigInfo>() .property("base_url", codecForString()) .property("currency", codecForString()) - .property("master_pub", codecForString()) + .property("master_pub", codecForEddsaPublicKey()) .build("TalerMerchantApi.ExchangeConfigInfo"); export const codecForTalerMerchantConfigResponse = @@ -3439,13 +3441,13 @@ export const codecForTalerMerchantConfigResponse = export const codecForClaimResponse = (): Codec<ClaimResponse> => buildCodecForObject<ClaimResponse>() .property("contract_terms", codecForMerchantContractTerms()) - .property("sig", codecForString()) + .property("sig", codecForEddsaSignature()) .build("TalerMerchantApi.ClaimResponse"); export const codecForPaymentResponse = (): Codec<PaymentResponse> => buildCodecForObject<PaymentResponse>() .property("pos_confirmation", codecOptional(codecForString())) - .property("sig", codecForString()) + .property("sig", codecForEddsaSignature()) .build("TalerMerchantApi.PaymentResponse"); export const codecForPaymentDeniedLegallyResponse = @@ -3527,7 +3529,7 @@ export const codecForAbortResponse = (): Codec<AbortResponse> => export const codecForWalletRefundResponse = (): Codec<WalletRefundResponse> => buildCodecForObject<WalletRefundResponse>() - .property("merchant_pub", codecForString()) + .property("merchant_pub", codecForEddsaPublicKey()) .property("refund_amount", codecForAmountString()) .property("refunds", codecForList(codecForMerchantCoinRefundStatus())) .build("TalerMerchantApi.AbortResponse"); @@ -3536,12 +3538,12 @@ export const codecForMerchantCoinRefundSuccessStatus = (): Codec<MerchantCoinRefundSuccessStatus> => buildCodecForObject<MerchantCoinRefundSuccessStatus>() .property("type", codecForConstString("success")) - .property("coin_pub", codecForString()) + .property("coin_pub", codecForEddsaPublicKey()) .property("exchange_status", codecForConstNumber(200)) - .property("exchange_sig", codecForString()) + .property("exchange_sig", codecForEddsaSignature()) .property("rtransaction_id", codecForNumber()) .property("refund_amount", codecForAmountString()) - .property("exchange_pub", codecForString()) + .property("exchange_pub", codecForEddsaPublicKey()) .property("execution_time", codecForTimestamp) .build("TalerMerchantApi.MerchantCoinRefundSuccessStatus"); @@ -3549,7 +3551,7 @@ export const codecForMerchantCoinRefundFailureStatus = (): Codec<MerchantCoinRefundFailureStatus> => buildCodecForObject<MerchantCoinRefundFailureStatus>() .property("type", codecForConstString("failure")) - .property("coin_pub", codecForString()) + .property("coin_pub", codecForEddsaPublicKey()) .property("exchange_status", codecForNumber()) .property("rtransaction_id", codecForNumber()) .property("refund_amount", codecForAmountString()) @@ -3579,7 +3581,7 @@ export const codecForQueryInstancesResponse = .property("email", codecOptional(codecForString())) .property("website", codecOptional(codecForString())) .property("logo", codecOptional(codecForString())) - .property("merchant_pub", codecForString()) + .property("merchant_pub", codecForEddsaPublicKey()) .property("address", codecForLocation()) .property("jurisdiction", codecForLocation()) .property("use_stefan", codecForBoolean()) @@ -3801,7 +3803,7 @@ export const codecForMerchant = (): Codec<Merchant> => export const codecForExchange = (): Codec<Exchange> => buildCodecForObject<Exchange>() - .property("master_pub", codecForString()) + .property("master_pub", codecForEddsaPublicKey()) .property("priority", codecForNumber()) .property("url", codecForString()) .property("max_contribution", codecOptional(codecForAmountString())) @@ -3984,7 +3986,7 @@ export const codecForTransactionWireReport = (): Codec<TransactionWireReport> => .property("hint", codecForString()) .property("exchange_code", codecForNumber()) .property("exchange_http_status", codecForNumber()) - .property("coin_pub", codecForString()) + .property("coin_pub", codecForEddsaPublicKey()) .build("TalerMerchantApi.TransactionWireReport"); export const codecForMerchantRefundResponse = @@ -4151,7 +4153,7 @@ export const codecForInstance = (): Codec<Instance> => .property("website", codecOptional(codecForString())) .property("logo", codecOptional(codecForString())) .property("id", codecForString()) - .property("merchant_pub", codecForString()) + .property("merchant_pub", codecForEddsaPublicKey()) .property("payment_targets", codecForList(codecForString())) .property("deleted", codecForBoolean()) .build("TalerMerchantApi.Instance"); @@ -4168,5 +4170,5 @@ export const codecForMerchantReserveCreateConfirmation = (): Codec<MerchantReserveCreateConfirmation> => buildCodecForObject<MerchantReserveCreateConfirmation>() .property("accounts", codecForList(codecForExchangeWireAccount())) - .property("reserve_pub", codecForString()) + .property("reserve_pub", codecForEddsaPublicKey()) .build("MerchantReserveCreateConfirmation"); diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -56,6 +56,7 @@ import { TemplateParams, WithdrawalOperationStatusFlag, canonicalizeBaseUrl, + codecForEddsaPrivateKey } from "./index.js"; import { PaytoString, codecForPaytoString } from "./payto.js"; import { QrCodeSpec } from "./qr.js"; @@ -2099,7 +2100,7 @@ export const codecForAcceptManualWithdrawalRequest = .property("exchangeBaseUrl", codecForCanonBaseUrl()) .property("amount", codecForAmountString()) .property("restrictAge", codecOptional(codecForNumber())) - .property("forceReservePriv", codecOptional(codecForString())) + .property("forceReservePriv", codecOptional(codecForEddsaPrivateKey())) .build("AcceptManualWithdrawalRequest"); export interface GetWithdrawalDetailsForAmountRequest { diff --git a/packages/taler-util/src/types-taler-wire-gateway.ts b/packages/taler-util/src/types-taler-wire-gateway.ts @@ -34,6 +34,7 @@ import { PaytoString, codecForPaytoString } from "./payto.js"; import { codecForTimestamp } from "./time.js"; import { AmountString, + codecForEddsaPublicKey, EddsaPublicKey, HashCode, SafeUint64, @@ -348,7 +349,7 @@ export const codecForIncomingReserveTransaction = .property("amount", codecForAmountString()) .property("date", codecForTimestamp) .property("debit_account", codecForPaytoString()) - .property("reserve_pub", codecForString()) + .property("reserve_pub", codecForEddsaPublicKey()) .property("row_id", codecForNumber()) .property("type", codecForConstString("RESERVE")) .build("TalerWireGatewayApi.IncomingReserveTransaction"); @@ -359,7 +360,7 @@ export const codecForIncomingKycAuthTransaction = .property("amount", codecForAmountString()) .property("date", codecForTimestamp) .property("debit_account", codecForPaytoString()) - .property("account_pub", codecForString()) + .property("account_pub", codecForEddsaPublicKey()) .property("row_id", codecForNumber()) .property("type", codecForConstString("KYCAUTH")) .build("TalerWireGatewayApi.IncomingKycAuthTransaction"); diff --git a/packages/taler-util/src/types.test.ts b/packages/taler-util/src/types.test.ts @@ -15,14 +15,14 @@ */ import test from "ava"; -import { codecForMerchantContractTerms, MerchantContractTerms } from "./index.js"; +import { codecForMerchantContractTerms, EddsaPublicKeyString, MerchantContractTerms } from "./index.js"; test("contract terms validation", (t) => { const c = { nonce: "123123123", h_wire: "123", amount: "EUR:1.5", - exchanges: [{ master_pub: "foo", priority: 1, url: "foo" }], + exchanges: [{ master_pub: "foo" as EddsaPublicKeyString, priority: 1, url: "foo" }], fulfillment_url: "foo", max_fee: "EUR:1.5", merchant_pub: "12345", @@ -58,7 +58,7 @@ test("contract terms validation (locations)", (t) => { nonce: "123123123", h_wire: "123", amount: "EUR:1.5", - exchanges: [{ master_pub: "foo", priority: 1, url: "foo" }], + exchanges: [{ master_pub: "foo" as EddsaPublicKeyString, priority: 1, url: "foo" }], fulfillment_url: "foo", max_fee: "EUR:1.5", merchant_pub: "12345", diff --git a/packages/taler-wallet-core/src/versions.ts b/packages/taler-wallet-core/src/versions.ts @@ -14,7 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { TalerExchangeHttpClient } from "@gnu-taler/taler-util"; +import { TalerExchangeHttpClient2 } from "@gnu-taler/taler-util"; /** * Protocol version spoken with the exchange. @@ -22,7 +22,7 @@ import { TalerExchangeHttpClient } from "@gnu-taler/taler-util"; * Uses libtool's current:revision:age versioning. */ export const WALLET_EXCHANGE_PROTOCOL_VERSION = - TalerExchangeHttpClient.SUPPORTED_EXCHANGE_PROTOCOL_VERSION; + TalerExchangeHttpClient2.SUPPORTED_EXCHANGE_PROTOCOL_VERSION; /** * Protocol version spoken with the merchant. diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -114,6 +114,7 @@ import { TalerError, TalerErrorCode, TalerExchangeHttpClient, + TalerExchangeHttpClient2, TalerProtocolTimestamp, TalerUriAction, TestingGetDenomStatsRequest, @@ -424,8 +425,8 @@ export interface WalletExecutionContext { export function walletExchangeClient( baseUrl: string, wex: WalletExecutionContext, -): TalerExchangeHttpClient { - return new TalerExchangeHttpClient(baseUrl, { +): TalerExchangeHttpClient2 { + return new TalerExchangeHttpClient2(baseUrl, { httpClient: wex.http, cancelationToken: wex.cancellationToken, longPollQueue: wex.ws.longpollQueue, diff --git a/packages/web-util/src/context/exchange-api.ts b/packages/web-util/src/context/exchange-api.ts @@ -246,7 +246,7 @@ function buildExchangeApiClient( return { getRemoteConfig, - VERSION: TalerExchangeHttpClient.SUPPORTED_EXCHANGE_PROTOCOL_VERSION, + VERSION: TalerExchangeHttpClient.PROTOCOL_VERSION, lib: { exchange: ex, },