commit 26e4780afc1442f7cc568541ddbbe682620af67c
parent f8c228464bfcd74e493aa5284d569fcf949a0b58
Author: Sebastian <sebasjm@gmail.com>
Date: Mon, 10 Nov 2025 08:43:21 -0300
add support for exchange v31
Diffstat:
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()))