aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-util/src/http-client/exchange.ts
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2024-08-13 15:27:41 -0300
committerSebastian <sebasjm@gmail.com>2024-08-13 15:27:59 -0300
commitaa4fc564777aab82e16dd4c02012682ff67a3e8a (patch)
tree79470e282d3b953982be30db4b2ae3d5a056d4c1 /packages/taler-util/src/http-client/exchange.ts
parentd5f3a51c5684d6d29a3b9c1b956b2cfc68434f23 (diff)
downloadwallet-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.ts447
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));