import { HttpRequestLibrary } from "../http-common.js"; import { HttpStatusCode } from "../http-status-codes.js"; import { createPlatformHttpLib } from "../http.js"; import { LibtoolVersion } from "../libtool-version.js"; import { hash } from "../nacl-fast.js"; import { FailCasesByMethod, ResultByMethod, opEmptySuccess, opFixedSuccess, opKnownFailure, opSuccess, opUnknownFailure } from "../operation.js"; import { TalerSignaturePurpose, amountToBuffer, bufferForUint32, buildSigPS, decodeCrock, eddsaSign, encodeCrock, stringToBytes, timestampRoundedToBuffer } from "../taler-crypto.js"; import { OfficerAccount, PaginationParams, SigningKey, TalerExchangeApi, codecForAmlDecisionDetails, codecForAmlRecords, codecForExchangeConfig } from "./types.js"; import { addPaginationParams } from "./utils.js"; export type TalerExchangeResultByMethod = ResultByMethod export type TalerExchangeErrorsByMethod = FailCasesByMethod /** */ export class TalerExchangeHttpClient { httpLib: HttpRequestLibrary; public readonly PROTOCOL_VERSION = "17:0:0"; constructor( readonly baseUrl: string, httpClient?: HttpRequestLibrary, ) { this.httpLib = httpClient ?? createPlatformHttpLib(); } isCompatible(version: string): boolean { const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version) return compare?.compatible ?? false } /** * https://docs.taler.net/core/api-merchant.html#get--config * */ async getConfig() { const url = new URL(`config`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "GET" }); switch (resp.status) { case HttpStatusCode.Ok: return opSuccess(resp, codecForExchangeConfig()) default: return opUnknownFailure(resp, await resp.text()) } } // // AML operations // /** * 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) 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 opSuccess(resp, codecForAmlRecords()) case HttpStatusCode.NoContent: return opFixedSuccess({ records: [] }) //this should be unauthorized case HttpStatusCode.Forbidden: return opKnownFailure("unauthorized", resp); case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); case HttpStatusCode.NotFound: return opKnownFailure("officer-not-found", resp); case HttpStatusCode.Conflict: return opKnownFailure("officer-disabled", resp); default: return opUnknownFailure(resp, await resp.text()) } } /** * 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 opSuccess(resp, codecForAmlDecisionDetails()) case HttpStatusCode.NoContent: return opFixedSuccess({ aml_history: [], kyc_attributes: [] }) //this should be unauthorized case HttpStatusCode.Forbidden: return opKnownFailure("unauthorized", resp); case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); case HttpStatusCode.NotFound: return opKnownFailure("officer-not-found", resp); case HttpStatusCode.Conflict: return opKnownFailure("officer-disabled", resp); default: return opUnknownFailure(resp, await resp.text()) } } /** * https://docs.taler.net/core/api-exchange.html#post--aml-$OFFICER_PUB-decision * */ async addDecisionDetails(auth: OfficerAccount, decision: Omit) { 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() //FIXME: this should be unauthorized case HttpStatusCode.Forbidden: return opKnownFailure("unauthorized", resp); case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); //FIXME: this two need to be splitted by error code case HttpStatusCode.NotFound: return opKnownFailure("officer-or-account-not-found", resp); case HttpStatusCode.Conflict: return opKnownFailure("officer-disabled-or-recent-decision", resp); default: return opUnknownFailure(resp, await resp.text()) } } } function buildQuerySignature(key: SigningKey): string { const sigBlob = buildSigPS( TalerSignaturePurpose.TALER_SIGNATURE_AML_QUERY, ).build(); return encodeCrock(eddsaSign(sigBlob, key)); } function buildDecisionSignature( key: SigningKey, decision: Omit, ): TalerExchangeApi.AmlDecision { 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(decodeCrock(decision.h_payto)) .put(zero) //kyc_requirement .put(bufferForUint32(decision.new_state)) .build(); const officer_sig = encodeCrock(eddsaSign(sigBlob, key)); return { ...decision, officer_sig } }