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, opKnownHttpFailure, 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, codecForExchangeKeys, } from "./types.js"; import { addPaginationParams } from "./utils.js"; export type TalerExchangeResultByMethod< prop extends keyof TalerExchangeHttpClient, > = ResultByMethod; export type TalerExchangeErrorsByMethod< prop extends keyof TalerExchangeHttpClient, > = FailCasesByMethod; /** */ export class TalerExchangeHttpClient { httpLib: HttpRequestLibrary; public readonly PROTOCOL_VERSION = "18:0:1"; 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-exchange.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()); } } /** * https://docs.taler.net/core/api-merchant.html#get--config * * PARTIALLY IMPLEMENTED!! */ async getKeys() { const url = new URL(`keys`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "GET", }); switch (resp.status) { case HttpStatusCode.Ok: return opSuccess(resp, codecForExchangeKeys()); default: return opUnknownFailure(resp, await resp.text()); } } // TERMS // // 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(resp, { 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 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(resp, { 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 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(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 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, }; }