diff options
author | Sebastian <sebasjm@gmail.com> | 2024-08-13 15:27:41 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2024-08-13 15:27:59 -0300 |
commit | aa4fc564777aab82e16dd4c02012682ff67a3e8a (patch) | |
tree | 79470e282d3b953982be30db4b2ae3d5a056d4c1 /packages/taler-util | |
parent | d5f3a51c5684d6d29a3b9c1b956b2cfc68434f23 (diff) | |
download | wallet-core-aa4fc564777aab82e16dd4c02012682ff67a3e8a.tar.xz |
sync aml/kyc api, wip
Diffstat (limited to 'packages/taler-util')
-rw-r--r-- | packages/taler-util/src/http-client/exchange.ts | 447 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/officer-account.ts | 68 | ||||
-rw-r--r-- | packages/taler-util/src/types-taler-common.ts | 28 | ||||
-rw-r--r-- | packages/taler-util/src/types-taler-exchange.ts | 743 |
4 files changed, 1160 insertions, 126 deletions
diff --git a/packages/taler-util/src/http-client/exchange.ts b/packages/taler-util/src/http-client/exchange.ts index 2b81855d6..4a27c824f 100644 --- a/packages/taler-util/src/http-client/exchange.ts +++ b/packages/taler-util/src/http-client/exchange.ts @@ -14,9 +14,10 @@ import { ResultByMethod, opEmptySuccess, opFixedSuccess, + opKnownAlternativeFailure, opKnownHttpFailure, opSuccessFromHttp, - opUnknownFailure, + opUnknownFailure } from "../operation.js"; import { TalerSignaturePurpose, @@ -30,22 +31,35 @@ import { timestampRoundedToBuffer, } from "../taler-crypto.js"; import { + AccessToken, + AmountString, OfficerAccount, PaginationParams, + ReserveAccount, SigningKey, - codecForTalerCommonConfigResponse, + codecForTalerCommonConfigResponse } from "../types-taler-common.js"; import { - codecForAmlDecisionDetails, - codecForAmlRecords, + AmlDecisionRequest, + ExchangeVersionResponse, + KycRequirementInformationId, + WalletKycRequest, + codecForAccountKycStatus, + codecForAmlDecisionsResponse, + codecForAmlKycAttributes, + codecForAmlWalletKycCheckResponse, + codecForAvailableMeasureSummary, + codecForEventCounter, codecForExchangeConfig, codecForExchangeKeys, + codecForKycProcessClientInformation, + codecForLegitimizationNeededResponse } from "../types-taler-exchange.js"; -import { CacheEvictor, addPaginationParams, nullEvictor } from "./utils.js"; +import { CacheEvictor, addMerchantPaginationParams, nullEvictor } from "./utils.js"; import { TalerError } from "../errors.js"; import { TalerErrorCode } from "../taler-error-codes.js"; -import * as TalerExchangeApi from "../types-taler-exchange.js"; +import { AmountJson, Duration } from "../index.node.js"; export type TalerExchangeResultByMethod< prop extends keyof TalerExchangeHttpClient, @@ -62,7 +76,7 @@ export enum TalerExchangeCacheEviction { */ export class TalerExchangeHttpClient { httpLib: HttpRequestLibrary; - public readonly PROTOCOL_VERSION = "18:0:1"; + public readonly PROTOCOL_VERSION = "20:0:0"; cacheEvictor: CacheEvictor<TalerExchangeCacheEviction>; constructor( @@ -105,7 +119,7 @@ export class TalerExchangeHttpClient { */ async getConfig(): Promise< | OperationFail<HttpStatusCode.NotFound> - | OperationOk<TalerExchangeApi.ExchangeVersionResponse> + | OperationOk<ExchangeVersionResponse> > { const url = new URL(`config`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { @@ -171,6 +185,166 @@ export class TalerExchangeHttpClient { // TERMS // + // KYC operations + // + + /** + * https://docs.taler.net/core/api-exchange.html#post--kyc-wallet + * + */ + async notifyKycBalanceLimit(account: ReserveAccount, balance: AmountString) { + const url = new URL(`kyc-wallet`, this.baseUrl); + + const body: WalletKycRequest = { + balance, + reserve_pub: account.id, + reserve_sig: encodeCrock(account.signingKey), + } + + 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#post--kyc-wallet + * + */ + async checkKycStatus(account: ReserveAccount, requirementId: number, params: { + timeout?: number, + } = {}) { + const url = new URL(`kyc-check/${String(requirementId)}`, this.baseUrl); + + if (params.timeout !== undefined) { + url.searchParams.set("timeout_ms", String(params.timeout)); + } + + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers: { + "Account-Owner-Signature": buildKYCQuerySignature(account.signingKey), + }, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForAccountKycStatus()); + case HttpStatusCode.Accepted: + return opKnownAlternativeFailure(resp, resp.status, codecForAccountKycStatus()); + case HttpStatusCode.NoContent: + return opEmptySuccess(resp); + case HttpStatusCode.Forbidden: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-exchange.html#get--kyc-info-$ACCESS_TOKEN + * + */ + async checkKycInfo(token: AccessToken, known: KycRequirementInformationId[], params: { + timeout?: number, + } = {}) { + const url = new URL(`kyc-info/${token}`, this.baseUrl); + + if (params.timeout !== undefined) { + url.searchParams.set("timeout_ms", String(params.timeout)); + } + + 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.NoContent: + return opEmptySuccess(resp); + 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(requirement: KycRequirementInformationId, body: object) { + const url = new URL(`kyc-upload/${requirement}`, this.baseUrl); + + + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + }); + + 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.PayloadTooLarge: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-exchange.html#post--kyc-start-$ID + * + */ + async startKycProcess(requirement: KycRequirementInformationId) { + const url = new URL(`kyc-start/${requirement}`, this.baseUrl); + + + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + }); + + 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.PayloadTooLarge: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + // // AML operations // @@ -178,34 +352,206 @@ export class TalerExchangeHttpClient { * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-decisions-$STATE * */ - async getDecisionsByState( - auth: OfficerAccount, - state: TalerExchangeApi.AmlState, - pagination?: PaginationParams, - ) { - const url = new URL( - `aml/${auth.id}/decisions/${TalerExchangeApi.AmlState[state]}`, - this.baseUrl, - ); - addPaginationParams(url, pagination); + // async getDecisionsByState( + // auth: OfficerAccount, + // state: TalerExchangeApi.AmlState, + // pagination?: PaginationParams, + // ) { + // const url = new URL( + // `aml/${auth.id}/decisions/${TalerExchangeApi.AmlState[state]}`, + // this.baseUrl, + // ); + // addPaginationParams(url, pagination); + + // const resp = await this.httpLib.fetch(url.href, { + // method: "GET", + // headers: { + // "Taler-AML-Officer-Signature": buildQuerySignature(auth.signingKey), + // }, + // }); + + // switch (resp.status) { + // case HttpStatusCode.Ok: + // return opSuccessFromHttp(resp, codecForAmlRecords()); + // case HttpStatusCode.NoContent: + // return opFixedSuccess({ records: [] }); + // //this should be unauthorized + // case HttpStatusCode.Forbidden: + // return opKnownHttpFailure(resp.status, resp); + // case HttpStatusCode.Unauthorized: + // return opKnownHttpFailure(resp.status, resp); + // case HttpStatusCode.NotFound: + // return opKnownHttpFailure(resp.status, resp); + // case HttpStatusCode.Conflict: + // return opKnownHttpFailure(resp.status, resp); + // default: + // return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + // } + // } + + // /** + // * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-decision-$H_PAYTO + // * + // */ + // async getDecisionDetails(auth: OfficerAccount, account: string) { + // const url = new URL(`aml/${auth.id}/decision/${account}`, this.baseUrl); + + // const resp = await this.httpLib.fetch(url.href, { + // method: "GET", + // headers: { + // "Taler-AML-Officer-Signature": buildQuerySignature(auth.signingKey), + // }, + // }); + + // switch (resp.status) { + // case HttpStatusCode.Ok: + // return opSuccessFromHttp(resp, codecForAmlDecisionDetails()); + // case HttpStatusCode.NoContent: + // return opFixedSuccess({ aml_history: [], kyc_attributes: [] }); + // //this should be unauthorized + // case HttpStatusCode.Forbidden: + // return opKnownHttpFailure(resp.status, resp); + // case HttpStatusCode.Unauthorized: + // return opKnownHttpFailure(resp.status, resp); + // case HttpStatusCode.NotFound: + // return opKnownHttpFailure(resp.status, resp); + // case HttpStatusCode.Conflict: + // return opKnownHttpFailure(resp.status, resp); + // default: + // return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + // } + // } + + // /** + // * https://docs.taler.net/core/api-exchange.html#post--aml-$OFFICER_PUB-decision + // * + // */ + // async addDecisionDetails( + // auth: OfficerAccount, + // decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig">, + // ) { + // const url = new URL(`aml/${auth.id}/decision`, this.baseUrl); + + // const body = buildDecisionSignature(auth.signingKey, decision); + // const resp = await this.httpLib.fetch(url.href, { + // method: "POST", + // body, + // }); + + // switch (resp.status) { + // case HttpStatusCode.NoContent: + // return opEmptySuccess(resp); + // //FIXME: this should be unauthorized + // case HttpStatusCode.Forbidden: + // return opKnownHttpFailure(resp.status, resp); + // case HttpStatusCode.Unauthorized: + // return opKnownHttpFailure(resp.status, resp); + // //FIXME: this two need to be split by error code + // case HttpStatusCode.NotFound: + // return opKnownHttpFailure(resp.status, resp); + // case HttpStatusCode.Conflict: + // return opKnownHttpFailure(resp.status, resp); + // default: + // return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + // } + // } + + /** + * 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": buildQuerySignature(auth.signingKey), + "Taler-AML-Officer-Signature": buildAMLQuerySignature(auth.signingKey), }, }); switch (resp.status) { case HttpStatusCode.Ok: - return opSuccessFromHttp(resp, codecForAmlRecords()); + return opSuccessFromHttp(resp, codecForAvailableMeasureSummary()); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-measures + * + */ + async getAmlKycStatistics(auth: OfficerAccount, name: string, filter: { + since?: Date + until?: Date + } = {}) { + const url = new URL(`aml/${auth.id}/kyc-statistics/${name}`, this.baseUrl); + + if (filter.since !== undefined) { + url.searchParams.set( + "start_date", + String(filter.since.getTime()) + ); + } + if (filter.until !== undefined) { + url.searchParams.set( + "end_date", + String(filter.until.getTime()) + ); + } + + + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers: { + "Taler-AML-Officer-Signature": buildAMLQuerySignature(auth.signingKey), + }, + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForEventCounter()); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(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); + + addMerchantPaginationParams(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, { + method: "GET", + headers: { + "Taler-AML-Officer-Signature": buildAMLQuerySignature(auth.signingKey), + }, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForAmlDecisionsResponse()); case HttpStatusCode.NoContent: return opFixedSuccess({ records: [] }); - //this should be unauthorized case HttpStatusCode.Forbidden: return opKnownHttpFailure(resp.status, resp); - case HttpStatusCode.Unauthorized: - return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.Conflict: @@ -216,29 +562,27 @@ export class TalerExchangeHttpClient { } /** - * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-decision-$H_PAYTO + * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-attributes-$H_PAYTO * */ - async getDecisionDetails(auth: OfficerAccount, account: string) { - const url = new URL(`aml/${auth.id}/decision/${account}`, this.baseUrl); + async getAmlAttributesForAccount(auth: OfficerAccount, account: string, params: PaginationParams = {}) { + const url = new URL(`aml/${auth.id}/attributes/${account}`, this.baseUrl); + addMerchantPaginationParams(url, params); const resp = await this.httpLib.fetch(url.href, { method: "GET", headers: { - "Taler-AML-Officer-Signature": buildQuerySignature(auth.signingKey), + "Taler-AML-Officer-Signature": buildAMLQuerySignature(auth.signingKey), }, }); switch (resp.status) { case HttpStatusCode.Ok: - return opSuccessFromHttp(resp, codecForAmlDecisionDetails()); + return opSuccessFromHttp(resp, codecForAmlKycAttributes()); case HttpStatusCode.NoContent: - return opFixedSuccess({ aml_history: [], kyc_attributes: [] }); - //this should be unauthorized + return opFixedSuccess({ details: [] }); case HttpStatusCode.Forbidden: return opKnownHttpFailure(resp.status, resp); - case HttpStatusCode.Unauthorized: - return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.Conflict: @@ -248,31 +592,28 @@ export class TalerExchangeHttpClient { } } + /** - * https://docs.taler.net/core/api-exchange.html#post--aml-$OFFICER_PUB-decision + * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-attributes-$H_PAYTO * */ - async addDecisionDetails( - auth: OfficerAccount, - decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig">, - ) { + async makeAmlDesicion(auth: OfficerAccount, decision: Omit<AmlDecisionRequest, "officer_sig">) { const url = new URL(`aml/${auth.id}/decision`, this.baseUrl); - const body = buildDecisionSignature(auth.signingKey, decision); + const body = buildAMLDecisionSignature(auth.signingKey, decision); const resp = await this.httpLib.fetch(url.href, { method: "POST", + headers: { + "Taler-AML-Officer-Signature": buildAMLQuerySignature(auth.signingKey), + }, body, }); switch (resp.status) { case HttpStatusCode.NoContent: return opEmptySuccess(resp); - //FIXME: this should be unauthorized case HttpStatusCode.Forbidden: return opKnownHttpFailure(resp.status, resp); - case HttpStatusCode.Unauthorized: - return opKnownHttpFailure(resp.status, resp); - //FIXME: this two need to be split by error code case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.Conflict: @@ -281,9 +622,19 @@ export class TalerExchangeHttpClient { return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } + +} + +function buildKYCQuerySignature(key: SigningKey): string { + const sigBlob = buildSigPS( + TalerSignaturePurpose.TALER_SIGNATURE_AML_QUERY, + // TalerSignaturePurpose.TALER_SIGNATURE_WALLET_ACCOUNT_SETUP, + ).build(); + + return encodeCrock(eddsaSign(sigBlob, key)); } -function buildQuerySignature(key: SigningKey): string { +function buildAMLQuerySignature(key: SigningKey): string { const sigBlob = buildSigPS( TalerSignaturePurpose.TALER_SIGNATURE_AML_QUERY, ).build(); @@ -291,20 +642,20 @@ function buildQuerySignature(key: SigningKey): string { return encodeCrock(eddsaSign(sigBlob, key)); } -function buildDecisionSignature( +function buildAMLDecisionSignature( key: SigningKey, - decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig">, -): TalerExchangeApi.AmlDecision { + decision: Omit<AmlDecisionRequest, "officer_sig">, +): AmlDecisionRequest { const zero = new Uint8Array(new ArrayBuffer(64)); const sigBlob = buildSigPS(TalerSignaturePurpose.TALER_SIGNATURE_AML_DECISION) //TODO: new need the null terminator, also in the exchange .put(hash(stringToBytes(decision.justification))) //check null .put(timestampRoundedToBuffer(decision.decision_time)) - .put(amountToBuffer(decision.new_threshold)) + // .put(amountToBuffer(decision.new_threshold)) .put(decodeCrock(decision.h_payto)) .put(zero) //kyc_requirement - .put(bufferForUint32(decision.new_state)) + // .put(bufferForUint32(decision.new_state)) .build(); const officer_sig = encodeCrock(eddsaSign(sigBlob, key)); diff --git a/packages/taler-util/src/http-client/officer-account.ts b/packages/taler-util/src/http-client/officer-account.ts index 01b3681c0..612fd815e 100644 --- a/packages/taler-util/src/http-client/officer-account.ts +++ b/packages/taler-util/src/http-client/officer-account.ts @@ -17,8 +17,11 @@ import { EncryptionNonce, LockedAccount, + LockedReserve, OfficerAccount, OfficerId, + ReserveAccount, + ReserveId, SigningKey, createEddsaKeyPair, decodeCrock, @@ -96,6 +99,71 @@ export async function createNewOfficerAccount( return { id: accountId, signingKey, safe }; } +/** + * Restore previous session and unlock account with password + * + * @param salt string from which crypto params will be derived + * @param key secured private key + * @param password password for the private key + * @returns + */ +export async function unlockWalletKycAccount( + account: LockedReserve, + password: string, +): Promise<ReserveAccount> { + const rawKey = decodeCrock(account); + const rawPassword = stringToBytes(password); + + const signingKey = (await decryptWithDerivedKey( + rawKey, + rawPassword, + password, + ).catch((e) => { + throw new UnwrapKeyError(e instanceof Error ? e.message : String(e)); + })) as SigningKey; + + const publicKey = eddsaGetPublic(signingKey); + + const accountId = encodeCrock(publicKey) as ReserveId; + + return { id: accountId, signingKey }; +} + +/** + * Create new account (secured private key) + * secured with the given password + * + * @param extraNonce + * @param password + * @returns + */ +export async function createNewWalletKycAccount( + extraNonce: EncryptionNonce, + password: string, +): Promise<OfficerAccount & { safe: LockedAccount }> { + const { eddsaPriv, eddsaPub } = createEddsaKeyPair(); + + const key = stringToBytes(password); + + const localRnd = getRandomBytesF(24); + const mergedRnd: EncryptionNonce = extraNonce + ? kdf(24, stringToBytes("aml-officer"), extraNonce, localRnd) + : localRnd; + + const protectedPrivKey = await encryptWithDerivedKey( + mergedRnd, + key, + eddsaPriv, + password, + ); + + const signingKey = eddsaPriv as SigningKey; + const accountId = encodeCrock(eddsaPub) as OfficerId; + const safe = encodeCrock(protectedPrivKey) as LockedAccount; + + return { id: accountId, signingKey, safe }; +} + export class UnwrapKeyError extends Error { public cause: string; constructor(cause: string) { diff --git a/packages/taler-util/src/types-taler-common.ts b/packages/taler-util/src/types-taler-common.ts index 2a5d017a7..6fc314f25 100644 --- a/packages/taler-util/src/types-taler-common.ts +++ b/packages/taler-util/src/types-taler-common.ts @@ -518,12 +518,21 @@ export type UserAndToken = { }; declare const opaque_OfficerAccount: unique symbol; +/** + * Sealed private key for AML officer + */ export type LockedAccount = string & { [opaque_OfficerAccount]: true }; declare const opaque_OfficerId: unique symbol; +/** + * Public key for AML officer + */ export type OfficerId = string & { [opaque_OfficerId]: true }; declare const opaque_OfficerSigningKey: unique symbol; +/** + * Private key for AML officer + */ export type SigningKey = Uint8Array & { [opaque_OfficerSigningKey]: true }; export interface OfficerAccount { @@ -531,6 +540,25 @@ export interface OfficerAccount { signingKey: SigningKey; } + +declare const opaque_ReserveAccount: unique symbol; +/** + * Sealed private key for AML officer + */ +export type LockedReserve = string & { [opaque_ReserveAccount]: true }; + +declare const opaque_ReserveId: unique symbol; +/** + * Public key for AML officer + */ +export type ReserveId = string & { [opaque_ReserveId]: true }; + +export interface ReserveAccount { + id: ReserveId; + signingKey: SigningKey; +} + + export type PaginationParams = { /** * row identifier as the starting point of the query diff --git a/packages/taler-util/src/types-taler-exchange.ts b/packages/taler-util/src/types-taler-exchange.ts index 421b62058..b71f302f5 100644 --- a/packages/taler-util/src/types-taler-exchange.ts +++ b/packages/taler-util/src/types-taler-exchange.ts @@ -33,6 +33,7 @@ import { codecForBoolean, codecForConstString, codecForCurrencySpecificiation, + codecForEither, codecForMap, codecForURN, strcmp, @@ -41,13 +42,18 @@ import { Edx25519PublicKeyEnc } from "./taler-crypto.js"; import { TalerProtocolDuration, TalerProtocolTimestamp, + codecForAbsoluteTime, codecForDuration, codecForTimestamp, } from "./time.js"; import { + AccessToken, AmlOfficerPublicKeyP, AmountString, Base32String, + codecForAccessToken, + codecForInternationalizedString, + codecForURLString, CoinPublicKeyString, Cs25519Point, CurrencySpecification, @@ -1380,31 +1386,80 @@ export interface BatchDepositRequestCoin { h_age_commitment?: string; } -export enum AmlState { - normal = 0, - pending = 1, - frozen = 2, +export interface AvailableMeasureSummary { + + // Available original measures that can be + // triggered directly by default rules. + roots: { [measure_name: string]: MeasureInformation; }; + + // Available AML programs. + programs: { [prog_name: string]: AmlProgramRequirement; }; + + // Available KYC checks. + checks: { [check_name: string]: KycCheckInformation; }; + } -export interface AmlRecords { - // Array of AML records matching the query. - records: AmlRecord[]; +export interface MeasureInformation { + + // Name of a KYC check. + check_name: string; + + // Name of an AML program. + prog_name: string; + + // Context for the check. Optional. + context?: Object; + } -export interface AmlRecord { - // Which payto-address is this record about. - // Identifies a GNU Taler wallet or an affected bank account. - h_payto: PaytoHash; - // What is the current AML state. - current_state: AmlState; +export interface AmlProgramRequirement { - // Monthly transaction threshold before a review will be triggered - threshold: AmountString; + // Description of what the AML program does. + description: string; - // RowID of the record. - rowid: Integer; + // List of required field names in the context to run this + // AML program. SPA must check that the AML staff is providing + // adequate CONTEXT when defining a measure using this program. + context: string[]; + + // List of required attribute names in the + // input of this AML program. These attributes + // are the minimum that the check must produce + // (it may produce more). + inputs: string[]; + +} + +export interface KycCheckInformation { + + // Description of the KYC check. Should be shown + // to the AML staff but will also be shown to the + // client when they initiate the check in the KYC SPA. + description: string; + + // Map from IETF BCP 47 language tags to localized + // description texts. + description_i18n?: { [lang_tag: string]: string }; + + // Names of the fields that the CONTEXT must provide + // as inputs to this check. + // SPA must check that the AML staff is providing + // adequate CONTEXT when defining a measure using + // this check. + requires: string[]; + + // Names of the attributes the check will output. + // SPA must check that the outputs match the + // required inputs when combining a KYC check + // with an AML program into a measure. + outputs: string[]; + + // Name of a root measure taken when this check fails. + fallback: string; } + export interface AmlDecisionDetails { // Array of AML decisions made for this account. Possibly // contains only the most recent decision if "history" was @@ -1447,34 +1502,6 @@ export interface KycDetail { expiration_time: Timestamp; } -export interface AmlDecision { - // Human-readable justification for the decision. - justification: string; - - // At what monthly transaction volume should the - // decision be automatically reviewed? - new_threshold: AmountString; - - // Which payto-address is the decision about? - // Identifies a GNU Taler wallet or an affected bank account. - h_payto: PaytoHash; - - // What is the new AML state (e.g. frozen, unfrozen, etc.) - // Numerical values are defined in AmlDecisionState. - new_state: Integer; - - // Signature by the AML officer over a - // TALER_MasterAmlOfficerStatusPS. - // Must have purpose TALER_SIGNATURE_MASTER_AML_KEY. - officer_sig: EddsaSignatureString; - - // When was the decision made? - decision_time: Timestamp; - - // Optional argument to impose new KYC requirements - // that the customer has to satisfy to unblock transactions. - kyc_requirements?: string[]; -} export interface ExchangeVersionResponse { // libtool-style representation of the Exchange protocol version, see @@ -1525,6 +1552,421 @@ export interface WireAccount { master_sig: EddsaSignatureString; } +export interface WalletKycRequest { + + // Balance threshold (not necessarily exact balance) + // to be crossed by the wallet that (may) trigger + // additional KYC requirements. + balance: AmountString; + + // EdDSA signature of the wallet affirming the + // request, must be of purpose + // TALER_SIGNATURE_WALLET_ACCOUNT_SETUP + reserve_sig: EddsaSignatureString; + + // long-term wallet reserve-account + // public key used to create the signature. + reserve_pub: EddsaPublicKeyString; +} + +export interface WalletKycCheckResponse { + + // Next balance limit above which a KYC check + // may be required. Optional, not given if no + // threshold exists (assume infinity). + next_threshold?: AmountString; + + // When does the current set of AML/KYC rules + // expire and the wallet needs to check again + // for updated thresholds. + expiration_time: Timestamp; + +} + + +// Implemented in this style since exchange +// protocol **v20**. +export interface LegitimizationNeededResponse { + + // Numeric error code unique to the condition. + // Should always be TALER_EC_EXCHANGE_GENERIC_KYC_REQUIRED. + code: number; + + // Human-readable description of the error, i.e. "missing parameter", + // "commitment violation", ... Should give a human-readable hint + // about the error's nature. Optional, may change without notice! + hint?: string; + + // Hash of the payto:// account URI for which KYC + // is required. + h_payto: PaytoHash; + + // Public key associated with the account. The client must sign + // the initial request for the KYC status using the corresponding + // private key. Will be either a reserve public key or a merchant + // (instance) public key. + // + // Absent if no public key is currently associated + // with the account and the client MUST thus first + // credit the exchange via an inbound wire transfer + // to associate a public key with the debited account. + account_pub?: EddsaPublicKeyString; + + // Identifies a set of measures that were triggered and that are + // now preventing this operation from proceeding. Gives the + // account holder a starting point for understanding why the + // transaction was blocked and how to lift it. The account holder + // should use the number to check for the account's AML/KYC status + // using the /kyc-check/$REQUIREMENT_ROW endpoint. + requirement_row: Integer; + +} + +export interface AccountKycStatus { + + // Current AML state for the target account. True if + // operations are not happening due to staff processing + // paperwork *or* due to legal requirements (so the + // client cannot do anything but wait). + // + // Note that not every AML staff action may be legally + // exposed to the client, so this is merely a hint that + // a client should be told that AML staff is currently + // reviewing the account. AML staff *may* review + // accounts without this flag being set! + aml_review: boolean; + + // Access token needed to construct the /kyc-spa/ + // URL that the user should open in a browser to + // proceed with the KYC process (optional if the status + // type is 200 Ok, mandatory if the HTTP status + // is 202 Accepted). + access_token: AccessToken; + + // Array with limitations that currently apply to this + // account and that may be increased or lifted if the + // KYC check is passed. + // Note that additional limits *may* exist and not be + // communicated to the client. If such limits are + // reached, this *may* be indicated by the account + // going into aml_review state. However, it is + // also possible that the exchange may legally have + // to deny operations without being allowed to provide + // any justification. + // The limits should be used by the client to + // possibly structure their operations (e.g. withdraw + // what is possible below the limit, ask the user to + // pass KYC checks or withdraw the rest after the time + // limit is passed, warn the user to not withdraw too + // much or even prevent the user from generating a + // request that would cause it to exceed hard limits). + limits?: AccountLimit[]; + +} +export interface AccountLimit { + + // Operation that is limited. + // Must be one of "WITHDRAW", "DEPOSIT", "P2P-RECEIVE" + // or "WALLET-BALANCE". + operation_type: "WITHDRAW" | "DEPOSIT" | "P2P-RECEIVE" | "WALLET-BALANCE"; + + // Timeframe during which the limit applies. + timeframe: RelativeTime; + + // Maximum amount allowed during the given timeframe. + // Zero if the operation is simply forbidden. + threshold: AmountString; + + // True if this is a soft limit that could be raised + // by passing KYC checks. Clients *may* deliberately + // try to cross limits and trigger measures resulting + // in 451 responses to begin KYC processes. + // Clients that are aware of hard limits *should* + // inform users about the hard limit and prevent flows + // in the UI that would cause violations of hard limits. + soft_limit: boolean; +} + +export interface KycProcessClientInformation { + + // Array of requirements. + requirements: KycRequirementInformation[]; + + // True if the client is expected to eventually satisfy all requirements. + // Default (if missing) is false. + is_and_combinator?: boolean + + // List of available voluntary checks the client could pay for. + // Since **vATTEST**. + voluntary_checks?: { [name: string]: KycCheckPublicInformation }; +} + +declare const opaque_kycReq: unique symbol; +export type KycRequirementInformationId = string & { [opaque_kycReq]: true } + +export interface KycRequirementInformation { + + // Which form should be used? Common values include "INFO" + // (to just show the descriptions but allow no action), + // "LINK" (to enable the user to obtain a link via + // /kyc-start/) or any build-in form name supported + // by the SPA. + form: string; + + // English description of the requirement. + description: string; + + // Map from IETF BCP 47 language tags to localized + // description texts. + description_i18n?: { [lang_tag: string]: string }; + + // ID of the requirement, useful to construct the + // /kyc-upload/$ID or /kyc-start/$ID endpoint URLs. + // Present if and only if "form" is not "INFO". The + // $ID value may itself contain / or ? and + // basically encode any URL path (and optional arguments). + id?: KycRequirementInformationId; +} + +// Since **vATTEST**. +export interface KycCheckPublicInformation { + + // English description of the check. + description: string; + + // Map from IETF BCP 47 language tags to localized + // description texts. + description_i18n?: { [lang_tag: string]: string }; + + // FIXME: is the above in any way sufficient + // to begin the check? Do we not need at least + // something more??!? +} + + +export interface EventCounter { + // Number of events of the specified type in + // the given range. + counter: Integer; +} + +export interface AmlDecisionsResponse { + + // Array of AML decisions matching the query. + records: AmlDecision[]; +} + +export interface AmlDecision { + + // Which payto-address is this record about. + // Identifies a GNU Taler wallet or an affected bank account. + h_payto: PaytoHash; + + // Row ID of the record. Used to filter by offset. + rowid: Integer; + + // Justification for the decision. NULL if none + // is available. + justification?: string; + + // When was the decision made? + decision_time: Timestamp; + + // Free-form properties about the account. + // Can be used to store properties such as PEP, + // risk category, type of business, hits on + // sanctions lists, etc. + properties?: AccountProperties; + + // What are the new rules? + limits: LegitimizationRuleSet; + + // True if the account is under investigation by AML staff + // after this decision. + to_investigate: boolean; + + // True if this is the active decision for the + // account. + is_active: boolean; + +} + +// All fields in this object are optional. The actual +// properties collected depend fully on the discretion +// of the exchange operator; +// however, some common fields are standardized +// and thus described here. +export interface AccountProperties { + + // True if this is a politically exposed account. + // Rules for classifying accounts as politically + // exposed are country-dependent. + pep?: boolean; + + // True if this is a sanctioned account. + // Rules for classifying accounts as sanctioned + // are country-dependent. + sanctioned?: boolean; + + // True if this is a high-risk account. + // Rules for classifying accounts as at-risk + // are exchange operator-dependent. + high_risk?: boolean; + + // Business domain of the account owner. + // The list of possible business domains is + // operator- or country-dependent. + business_domain?: string; + + // Is the client's account currently frozen? + is_frozen?: boolean; + + // Was the client's account reported to the authorities? + was_reported?: boolean; + +} + +export interface LegitimizationRuleSet { + + // When does this set of rules expire and + // we automatically transition to the successor + // measure? + expiration_time: Timestamp; + + // Name of the measure to apply when the expiration time is + // reached. If not set, we refer to the default + // set of rules (and the default account state). + successor_measure?: string; + + // Legitimization rules that are to be applied + // to this account. + rules: KycRule[]; + + // Custom measures that KYC rules and the + // successor_measure may refer to. + custom_measures: { [measure_name: string]: MeasureInformation; }; + +} + +export interface AmlDecisionRequest { + + // Human-readable justification for the decision. + justification: string; + + // Which payto-address is the decision about? + // Identifies a GNU Taler wallet or an affected bank account. + h_payto: PaytoHash; + + // What are the new rules? + // New since protocol **v20**. + new_rules: LegitimizationRuleSet; + + // What are the new account properties? + // New since protocol **v20**. + properties: AccountProperties; + + // New measure to apply immediately to the account. + // Should typically be used to give the user some + // information or request additional information. + // Use "verboten" to communicate to the customer + // that there is no KYC check that could be passed + // to modify the new_rules. + // New since protocol **v20**. + new_measure?: string; + + // True if the account should remain under investigation by AML staff. + // New since protocol **v20**. + keep_investigating: boolean; + + // Signature by the AML officer over a TALER_AmlDecisionPS. + // Must have purpose TALER_SIGNATURE_MASTER_AML_KEY. + officer_sig: EddsaSignatureString; + + // When was the decision made? + decision_time: Timestamp; + +} + + +export interface KycRule { + + // Type of operation to which the rule applies. + operation_type: string; + + // The measures will be taken if the given + // threshold is crossed over the given timeframe. + threshold: AmountString; + + // Over which duration should the threshold be + // computed. All amounts of the respective + // operation_type will be added up for this + // duration and the sum compared to the threshold. + timeframe: RelativeTime; + + // Array of names of measures to apply. + // Names listed can be original measures or + // custom measures from the AmlOutcome. + // A special measure "verboten" is used if the + // threshold may never be crossed. + measures: string[]; + + // If multiple rules apply to the same account + // at the same time, the number with the highest + // rule determines which set of measures will + // be activated and thus become visible for the + // user. + display_priority: Integer; + + // True if the rule (specifically, operation_type, + // threshold, timeframe) and the general nature of + // the measures (verboten or approval required) + // should be exposed to the client. + // Defaults to "false" if not set. + exposed?: boolean; + + // True if all the measures will eventually need to + // be satisfied, false if any of the measures should + // do. Primarily used by the SPA to indicate how + // the measures apply when showing them to the user; + // in the end, AML programs will decide after each + // measure what to do next. + // Default (if missing) is false. + is_and_combinator?: boolean; + +} + + +export interface KycAttributes { + + // Matching KYC attribute history of the account. + details: KycAttributeCollectionEvent[]; + +} +export interface KycAttributeCollectionEvent { + + // Row ID of the record. Used to filter by offset. + rowid: Integer; + + // Name of the provider + // which was used to collect the attributes. NULL if they were + // just uploaded via a form by the account owner. + provider_name?: string; + + // The collected KYC data. NULL if the attribute data could not + // be decrypted (internal error of the exchange, likely the + // attribute key was changed). + attributes?: Object; + + // Time when the KYC data was collected + collection_time: Timestamp; + +} + +export enum AmlState { + normal = 0, + pending = 1, + frozen = 2, +} export interface ExchangeKeysResponse { // libtool-style representation of the Exchange protocol version, see // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning @@ -1841,40 +2283,68 @@ export const codecForExchangeConfig = (): Codec<ExchangeVersionResponse> => .property("currency_specification", codecForCurrencySpecificiation()) .property("supported_kyc_requirements", codecForList(codecForString())) .build("TalerExchangeApi.ExchangeVersionResponse"); - export const codecForExchangeKeys = (): Codec<ExchangeKeysResponse> => buildCodecForObject<ExchangeKeysResponse>() .property("version", codecForString()) .property("base_url", codecForString()) .property("currency", codecForString()) .build("TalerExchangeApi.ExchangeKeysResponse"); -export const codecForAmlRecords = (): Codec<AmlRecords> => - buildCodecForObject<AmlRecords>() - .property("records", codecForList(codecForAmlRecord())) - .build("TalerExchangeApi.AmlRecords"); - -export const codecForAmlRecord = (): Codec<AmlRecord> => - buildCodecForObject<AmlRecord>() - .property("current_state", codecForNumber()) - .property("h_payto", codecForString()) - .property("rowid", codecForNumber()) - .property("threshold", codecForAmountString()) - .build("TalerExchangeApi.AmlRecord"); - -export const codecForAmlDecisionDetails = (): Codec<AmlDecisionDetails> => - buildCodecForObject<AmlDecisionDetails>() - .property("aml_history", codecForList(codecForAmlDecisionDetail())) - .property("kyc_attributes", codecForList(codecForKycDetail())) - .build("TalerExchangeApi.AmlDecisionDetails"); - -export const codecForAmlDecisionDetail = (): Codec<AmlDecisionDetail> => - buildCodecForObject<AmlDecisionDetail>() - .property("justification", codecForString()) - .property("new_state", codecForNumber()) - .property("decision_time", codecForTimestamp) - .property("new_threshold", codecForAmountString()) - .property("decider_pub", codecForString()) - .build("TalerExchangeApi.AmlDecisionDetail"); + +export const codecForEventCounter = (): Codec<EventCounter> => + buildCodecForObject<EventCounter>() + .property("counter", codecForNumber()) + .build("TalerExchangeApi.EventCounter"); + + +export const codecForAmlDecisionsResponse = (): Codec<AmlDecisionsResponse> => + buildCodecForObject<AmlDecisionsResponse>() + .property("records", codecForList(codecForAmlDecision())) + .build("TalerExchangeApi.AmlDecisionsResponse"); + +export const codecForAvailableMeasureSummary = (): Codec<AvailableMeasureSummary> => + buildCodecForObject<AvailableMeasureSummary>() + .property("checks", codecForMap(codecForKycCheckInformation())) + .property("programs", codecForMap(codecForAmlProgramRequirement())) + .property("roots", codecForMap(codecForMeasureInformation())) + .build("TalerExchangeApi.AvailableMeasureSummary"); + +export const codecForAmlProgramRequirement = (): Codec<AmlProgramRequirement> => + buildCodecForObject<AmlProgramRequirement>() + .property("description", codecForString()) + .property("context", codecForList(codecForString())) + .property("inputs", codecForList(codecForString())) + .build("TalerExchangeApi.AmlProgramRequirement"); + +export const codecForKycCheckInformation = (): Codec<KycCheckInformation> => + buildCodecForObject<KycCheckInformation>() + .property("description", codecForString()) + .property("description_i18n", codecForInternationalizedString()) + .property("fallback", codecForString()) + .property("outputs", codecForList(codecForString())) + .property("requires", codecForList(codecForString())) + .build("TalerExchangeApi.KycCheckInformation"); + +export const codecForMeasureInformation = (): Codec<MeasureInformation> => + buildCodecForObject<MeasureInformation>() + .property("prog_name", codecForString()) + .property("check_name", codecForString()) + .property("context", codecForAny()) + .build("TalerExchangeApi.MeasureInformation"); + +// export const codecForAmlDecisionDetails = (): Codec<AmlDecisionDetails> => +// buildCodecForObject<AmlDecisionDetails>() +// .property("aml_history", codecForList(codecForAmlDecisionDetail())) +// .property("kyc_attributes", codecForList(codecForKycDetail())) +// .build("TalerExchangeApi.AmlDecisionDetails"); + +// export const codecForAmlDecisionDetail = (): Codec<AmlDecisionDetail> => +// buildCodecForObject<AmlDecisionDetail>() +// .property("justification", codecForString()) +// .property("new_state", codecForNumber()) +// .property("decision_time", codecForTimestamp) +// .property("new_threshold", codecForAmountString()) +// .property("decider_pub", codecForString()) +// .build("TalerExchangeApi.AmlDecisionDetail"); export const codecForKycDetail = (): Codec<KycDetail> => buildCodecForObject<KycDetail>() @@ -1886,11 +2356,128 @@ export const codecForKycDetail = (): Codec<KycDetail> => export const codecForAmlDecision = (): Codec<AmlDecision> => buildCodecForObject<AmlDecision>() - .property("justification", codecForString()) - .property("new_threshold", codecForAmountString()) .property("h_payto", codecForString()) - .property("new_state", codecForNumber()) - .property("officer_sig", codecForString()) + .property("rowid", codecForNumber()) + .property("justification", codecOptional(codecForString())) .property("decision_time", codecForTimestamp) - .property("kyc_requirements", codecOptional(codecForList(codecForString()))) + .property("properties", codecForAccountProperties()) + .property("limits", codecForLegitimizationRuleSet()) + .property("to_investigate", codecForBoolean()) + .property("is_active", codecForBoolean()) .build("TalerExchangeApi.AmlDecision"); + +export const codecForAccountProperties = (): Codec<AccountProperties> => + buildCodecForObject<AccountProperties>() + .property("pep", codecOptional(codecForBoolean())) + .property("sanctioned", codecOptional(codecForBoolean())) + .property("high_risk", codecOptional(codecForBoolean())) + .property("business_domain", codecOptional(codecForString())) + .property("is_frozen", codecOptional(codecForBoolean())) + .property("was_reported", codecOptional(codecForBoolean())) + .build("TalerExchangeApi.AccountProperties"); + + +export const codecForLegitimizationRuleSet = (): Codec<LegitimizationRuleSet> => + buildCodecForObject<LegitimizationRuleSet>() + .property("expiration_time", (codecForTimestamp)) + .property("successor_measure", codecOptional(codecForString())) + .property("rules", codecForList(codecForKycRules())) + .property("custom_measures", codecForMap(codecForMeasureInformation())) + .build("TalerExchangeApi.LegitimizationRuleSet"); + +export const codecForKycRules = (): Codec<KycRule> => + buildCodecForObject<KycRule>() + .property("operation_type", codecForString()) + .property("threshold", codecForAmountString()) + .property("timeframe", codecForDuration) + .property("measures", codecForList(codecForString())) + .property("display_priority", codecForNumber()) + .property("exposed", codecOptional(codecForBoolean())) + .property("is_and_combinator", codecOptional(codecForBoolean())) + .build("TalerExchangeApi.KycRule"); + + + +export const codecForAmlKycAttributes = (): Codec<KycAttributes> => + buildCodecForObject<KycAttributes>() + .property("details", codecForList(codecForKycAttributeCollectionEvent())) + .build("TalerExchangeApi.KycAttributes"); + +export const codecForKycAttributeCollectionEvent = (): Codec<KycAttributeCollectionEvent> => + buildCodecForObject<KycAttributeCollectionEvent>() + .property("rowid", codecForNumber()) + .property("provider_name", codecOptional(codecForString())) + .property("collection_time", codecForTimestamp) + .property("attributes", codecOptional(codecForAny())) + .build("TalerExchangeApi.KycAttributeCollectionEvent"); + +export const codecForAmlWalletKycCheckResponse = (): Codec<WalletKycCheckResponse> => + buildCodecForObject<WalletKycCheckResponse>() + .property("next_threshold", codecOptional(codecForAmountString())) + .property("expiration_time", codecForTimestamp) + .build("TalerExchangeApi.WalletKycCheckResponse"); + +export const codecForLegitimizationNeededResponse = (): Codec<LegitimizationNeededResponse> => + buildCodecForObject<LegitimizationNeededResponse>() + .property("code", (codecForNumber())) + .property("hint", codecOptional(codecForString())) + .property("h_payto", (codecForString())) + .property("account_pub", codecOptional(codecForString())) + .property("requirement_row", (codecForNumber())) + .build("TalerExchangeApi.LegitimizationNeededResponse"); + +export const codecForAccountKycStatus = (): Codec<AccountKycStatus> => + buildCodecForObject<AccountKycStatus>() + .property("aml_review", codecForBoolean()) + .property("access_token", codecForAccessToken()) + .property("limits", codecOptional(codecForList(codecForAccountLimit()))) + .build("TalerExchangeApi.AccountKycStatus"); + +export const codecForAccountLimit = (): Codec<AccountLimit> => + buildCodecForObject<AccountLimit>() + .property("operation_type", codecForEither( + codecForConstString("WITHDRAW"), + codecForConstString("DEPOSIT"), + codecForConstString("P2P-RECEIVE"), + codecForConstString("WALLET-BALANCE")) + ) + .property("timeframe", codecForDuration) + .property("threshold", codecForAmountString()) + .property("soft_limit", codecForBoolean()) + .build("TalerExchangeApi.AccountLimit"); + + +export const codecForKycCheckPublicInformation = (): Codec<KycCheckPublicInformation> => + buildCodecForObject<KycCheckPublicInformation>() + .property("description", codecForString()) + .property("description_i18n", codecForInternationalizedString()) + .build("TalerExchangeApi.KycCheckPublicInformation"); + +export const codecForKycRequirementInformationId = + (): Codec<KycRequirementInformationId> => codecForString() as Codec<KycRequirementInformationId>; + +export const codecForKycRequirementInformation = (): Codec<KycRequirementInformation> => + buildCodecForObject<KycRequirementInformation>() + .property("form", codecForString()) + .property("description", codecForString()) + .property("description_i18n", codecForInternationalizedString()) + .property("id", codecOptional(codecForKycRequirementInformationId())) + .build("TalerExchangeApi.KycRequirementInformation"); + +export const codecForKycProcessClientInformation = (): Codec<KycProcessClientInformation> => + buildCodecForObject<KycProcessClientInformation>() + .property("requirements", codecForList(codecForKycRequirementInformation())) + .property("is_and_combinator", codecOptional(codecForBoolean())) + .property("voluntary_checks", codecForMap(codecForKycCheckPublicInformation())) + .build("TalerExchangeApi.KycProcessClientInformation"); + +interface KycProcessStartInformation { + + // URL to open. + redirect_url: string; +} + +export const codecForKycProcessStartInformation = (): Codec<KycProcessStartInformation> => + buildCodecForObject<KycProcessStartInformation>() + .property("redirect_url", codecForURLString()) + .build("TalerExchangeApi.KycProcessStartInformation"); |