taler-typescript-core

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

commit 26e4780afc1442f7cc568541ddbbe682620af67c
parent f8c228464bfcd74e493aa5284d569fcf949a0b58
Author: Sebastian <sebasjm@gmail.com>
Date:   Mon, 10 Nov 2025 08:43:21 -0300

add support for exchange v31

Diffstat:
Mpackages/taler-util/src/http-client/exchange-client.ts | 65++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mpackages/taler-util/src/payto.ts | 94++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mpackages/taler-util/src/types-taler-common.ts | 2--
Mpackages/taler-util/src/types-taler-exchange.ts | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
4 files changed, 189 insertions(+), 48 deletions(-)

diff --git a/packages/taler-util/src/http-client/exchange-client.ts b/packages/taler-util/src/http-client/exchange-client.ts @@ -47,7 +47,6 @@ import { LongPollParams, OfficerAccount, PaginationParams, - PaytoHash } from "../types-taler-common.js"; import { AccountKycStatus, @@ -82,6 +81,7 @@ import { PurseConflictPartial, WalletKycRequest, codecForAccountKycStatus, + codecForAmlDecisionsAccounts, codecForAmlDecisionsResponse, codecForAmlKycAttributes, codecForAmlStatisticsResponse, @@ -101,7 +101,7 @@ import { codecForLegitimizationMeasuresList, codecForLegitimizationNeededResponse, codecForPurseConflict, - codecForPurseConflictPartial + codecForPurseConflictPartial, } from "../types-taler-exchange.js"; import { CacheEvictor, @@ -115,6 +115,7 @@ import { Amounts, CancellationToken, LongpollQueue, + PaytoHash, signAmlDecision, signAmlQuery, } from "../index.js"; @@ -137,7 +138,7 @@ export enum TalerExchangeCacheEviction { * Client library for the GNU Taler exchange service. */ export class TalerExchangeHttpClient { - public static readonly SUPPORTED_EXCHANGE_PROTOCOL_VERSION = "27:0:2"; + public static readonly SUPPORTED_EXCHANGE_PROTOCOL_VERSION = "31:0:6"; private httpLib: HttpRequestLibrary; private cacheEvictor: CacheEvictor<TalerExchangeCacheEviction>; private preventCompression: boolean; @@ -927,8 +928,12 @@ export class TalerExchangeHttpClient { switch (resp.status) { case HttpStatusCode.Ok: return opSuccessFromHttp(resp, codecForAmlStatisticsResponse()); - case HttpStatusCode.NoContent:{ - return opFixedSuccess({ statistics: names.map(name => ({counter: 0, name} as EventCounter)) }); + case HttpStatusCode.NoContent: { + return opFixedSuccess({ + statistics: names.map( + (name) => ({ counter: 0, name }) as EventCounter, + ), + }); } case HttpStatusCode.Conflict: case HttpStatusCode.NotFound: @@ -943,6 +948,56 @@ export class TalerExchangeHttpClient { * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-decisions * */ + async getAmlAccounts( + auth: OfficerAccount, + params: PaginationParams & { + highRisk?: boolean; + open?: boolean; + investigation?: boolean; + } = {}, + ) { + const url = new URL(`aml/${auth.id}/accounts`, this.baseUrl); + + addPaginationParams(url, params); + if (params.investigation !== undefined) { + url.searchParams.set( + "investigation", + params.investigation ? "YES" : "NO", + ); + } + if (params.open !== undefined && params.open) { + url.searchParams.set("open", "YES"); + } + if (params.highRisk !== undefined && params.highRisk) { + url.searchParams.set("high_risk", "YES"); + } + + const resp = await this.fetch(url, { + headers: { + "Taler-AML-Officer-Signature": encodeCrock( + signAmlQuery(auth.signingKey), + ), + }, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForAmlDecisionsAccounts()); + case HttpStatusCode.NoContent: + return opFixedSuccess({ accounts: [] }); + 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-decisions + * + */ async getAmlDecisions( auth: OfficerAccount, params: PaginationParams & { diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts @@ -17,13 +17,13 @@ import { BitcoinBech32 } from "./bech32.js"; import { generateFakeSegwitAddress } from "./bitcoin.js"; import { Codec, Context, DecodingError, renderContext } from "./codec.js"; +import { assertUnreachable } from "./errors.js"; +import { IbanString, parseIban } from "./iban.js"; import { opFixedSuccess, opKnownFailure, opKnownFailureWithBody, } from "./operation.js"; -import { IbanString, parseIban } from "./iban.js"; -import { assertUnreachable } from "./errors.js"; import { decodeCrock, encodeCrock, @@ -86,6 +86,10 @@ export enum PaytoParseError { INVALID_TARGET_PATH, } +// TODO: use unique symbol to remove compat with string +// TODO: verify in account aml types if we can merge it since this is also an aml account +export type PaytoHash = string; + export namespace Paytos { export type URI = | PaytoUnsupported @@ -339,7 +343,7 @@ export namespace Paytos { bic: string | undefined, params: Record<string, string> = {}, ): PaytoIBAN { - iban = iban.toUpperCase() as IbanString + iban = iban.toUpperCase() as IbanString; return { targetType: PaytoType.IBAN, iban, @@ -675,42 +679,58 @@ export namespace Paytos { } } } +} - export function codecFullForPaytoString(): Codec<FullPaytoString> { - return { - decode(x: any, c?: Context): FullPaytoString { - if (typeof x !== "string") { - throw new DecodingError( - `expected string at ${renderContext(c)} but got ${typeof x}`, - ); - } - if (!x.startsWith(PAYTO_PREFIX)) { - throw new DecodingError( - `expected start with payto at ${renderContext(c)} but got "${x}"`, - ); - } - return x as FullPaytoString; - }, - }; - } +export function codecForPaytoHash(): Codec<PaytoHash> { + return { + decode(x: any, c?: Context): PaytoHash { + // TODO: implement a stronger validation and reject invalid hash + // maybe check charset and also length + if (typeof x !== "string") { + throw new DecodingError( + `expected string at ${renderContext(c)} but got ${typeof x}`, + ); + } + return x as PaytoHash; + }, + }; +} +export function codecFullForPaytoString(): Codec<Paytos.FullPaytoString> { + return { + decode(x: any, c?: Context): Paytos.FullPaytoString { + // TODO: implement a stronger validation and reject invalid paytos + if (typeof x !== "string") { + throw new DecodingError( + `expected string at ${renderContext(c)} but got ${typeof x}`, + ); + } + if (!x.startsWith(PAYTO_PREFIX)) { + throw new DecodingError( + `expected start with payto at ${renderContext(c)} but got "${x}"`, + ); + } + return x as Paytos.FullPaytoString; + }, + }; +} - export function codecNormalizedForPaytoString(): Codec<NormalizedPaytoString> { - return { - decode(x: any, c?: Context): NormalizedPaytoString { - if (typeof x !== "string") { - throw new DecodingError( - `expected string at ${renderContext(c)} but got ${typeof x}`, - ); - } - if (!x.startsWith(PAYTO_PREFIX)) { - throw new DecodingError( - `expected start with payto at ${renderContext(c)} but got "${x}"`, - ); - } - return x as NormalizedPaytoString; - }, - }; - } +export function codecNormalizedForPaytoString(): Codec<Paytos.NormalizedPaytoString> { + return { + decode(x: any, c?: Context): Paytos.NormalizedPaytoString { + // TODO: implement a stronger validation and reject invalid paytos + if (typeof x !== "string") { + throw new DecodingError( + `expected string at ${renderContext(c)} but got ${typeof x}`, + ); + } + if (!x.startsWith(PAYTO_PREFIX)) { + throw new DecodingError( + `expected start with payto at ${renderContext(c)} but got "${x}"`, + ); + } + return x as Paytos.NormalizedPaytoString; + }, + }; } /** diff --git a/packages/taler-util/src/types-taler-common.ts b/packages/taler-util/src/types-taler-common.ts @@ -50,8 +50,6 @@ import { // 64-byte hash code. export type HashCode = string; -export type PaytoHash = string; - export type AmlOfficerPublicKeyP = string; // 32-byte hash code. diff --git a/packages/taler-util/src/types-taler-exchange.ts b/packages/taler-util/src/types-taler-exchange.ts @@ -34,7 +34,13 @@ import { codecOptionalDefault, } from "./codec.js"; import { strcmp } from "./helpers.js"; -import { codecForPaytoString } from "./payto.js"; +import { + PaytoHash, + Paytos, + codecForPaytoHash, + codecForPaytoString, + codecFullForPaytoString, +} from "./payto.js"; import { Edx25519PublicKeyEnc } from "./taler-crypto.js"; import { TalerErrorCode } from "./taler-error-codes.js"; @@ -59,7 +65,6 @@ import { Integer, InternationalizedString, LibtoolVersionString, - PaytoHash, RelativeTime, RsaPublicKey, RsaPublicKeyString, @@ -412,6 +417,12 @@ export interface ExchangeKeysResponse { */ currency: string; + // Open banking gateway base URL where wallets can + // initiate wire transfers to withdraw + // digital cash from this exchange. + // @since protocol **v30**. + open_banking_gateway?: string; + // Instructs wallets to use certain bank-specific // language (for buttons) and/or other UI/UX customization // for compliance with the rules of that bank. @@ -1850,6 +1861,12 @@ export interface ExchangeVersionResponse { // AML decisions. // @since protocol **v24**. aml_spa_dialect?: AmlSpaDialect; + + // Open banking gateway base URL where wallets can + // initiate wire transfers to withdraw + // digital cash from this exchange. + // @since protocol **v30**. + open_banking_gateway?: string; } export interface WalletKycRequest { @@ -2409,7 +2426,6 @@ interface ExtensionManifest { config?: object; } - export interface GlobalFees { // What date (inclusive) does these fees go into effect? start_date: Timestamp; @@ -2534,7 +2550,7 @@ export const codecForExchangeConfig = (): Codec<ExchangeVersionResponse> => export const codecForExchangeKeysResponse = (): Codec<ExchangeKeysResponse> => buildCodecForObject<ExchangeKeysResponse>() .property("version", codecForString()) - .property("base_url", codecForString()) + .property("base_url", codecForURLString()) .property("currency", codecForString()) .property("accounts", codecForAny()) .property("asset_type", codecForAny()) @@ -2563,6 +2579,7 @@ export const codecForExchangeKeysResponse = (): Codec<ExchangeKeysResponse> => .property("shopping_url", codecOptional(codecForString())) .property("tiny_amount", codecOptional(codecForAmountString())) .property("bank_compliance_language", codecOptional(codecForString())) + .property("open_banking_gateway", codecOptional(codecForURLString())) .deprecatedProperty("rewards_allowed") .build("TalerExchangeApi.ExchangeKeysResponse"); @@ -2646,6 +2663,57 @@ export const codecForAmlDecisionsResponse = (): Codec<AmlDecisionsResponse> => .property("records", codecForList(codecForAmlDecision())) .build("TalerExchangeApi.AmlDecisionsResponse"); +export interface CustomerAccountSummary { + // Which payto-address is this record about. + // Identifies a GNU Taler wallet or an affected bank account. + h_payto: PaytoHash; + + // Full payto URL of the account that the decision is + // about. + full_payto: Paytos.FullPaytoString; + + // True if the account was assessed as being high risk. + high_risk: boolean; + + // Latest comments about the account (if any). + comments?: string; + + // Row of the account in the exchange tables. Useful to filter + // by offset. + rowid: Integer; + + // When was the account opened? "never" if it was never opened. + open_time: Timestamp; + + // When was the account opened? "never" if it was never closed. + close_time: Timestamp; + + // True if the account is under investigation by AML staff + // after this decision. + to_investigate: boolean; +} +export interface AmlAccountsResponse { + // Array of customer accounts matching the query. + accounts: CustomerAccountSummary[]; +} +export const codecForAmlCustomerAccountSummary = + (): Codec<CustomerAccountSummary> => + buildCodecForObject<CustomerAccountSummary>() + .property("h_payto", codecForPaytoHash()) + .property("close_time", codecForTimestamp) + .property("open_time", codecForTimestamp) + .property("comments", codecOptional(codecForString())) + .property("full_payto", codecFullForPaytoString()) + .property("high_risk", codecForBoolean()) + .property("rowid", codecForNumber()) + .property("to_investigate", codecForBoolean()) + .build("TalerExchangeApi.CustomerAccountSummary"); + +export const codecForAmlDecisionsAccounts = (): Codec<AmlAccountsResponse> => + buildCodecForObject<AmlAccountsResponse>() + .property("accounts", codecForList(codecForAmlCustomerAccountSummary())) + .build("TalerExchangeApi.AmlAccountsResponse"); + // export const codecForAmlDecisionDetails = (): Codec<AmlDecisionDetails> => // buildCodecForObject<AmlDecisionDetails>() // .property("aml_history", codecForList(codecForAmlDecisionDetail()))