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/src/http-client/exchange.ts | |
parent | d5f3a51c5684d6d29a3b9c1b956b2cfc68434f23 (diff) | |
download | wallet-core-aa4fc564777aab82e16dd4c02012682ff67a3e8a.tar.xz |
sync aml/kyc api, wip
Diffstat (limited to 'packages/taler-util/src/http-client/exchange.ts')
-rw-r--r-- | packages/taler-util/src/http-client/exchange.ts | 447 |
1 files changed, 399 insertions, 48 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)); |