aboutsummaryrefslogtreecommitdiff
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
parentd5f3a51c5684d6d29a3b9c1b956b2cfc68434f23 (diff)
downloadwallet-core-aa4fc564777aab82e16dd4c02012682ff67a3e8a.tar.xz
sync aml/kyc api, wip
-rw-r--r--packages/aml-backoffice-ui/src/hooks/account.ts (renamed from packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts)6
-rw-r--r--packages/aml-backoffice-ui/src/hooks/decisions.ts (renamed from packages/aml-backoffice-ui/src/hooks/useCases.ts)62
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseDetails.tsx95
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx24
-rw-r--r--packages/aml-backoffice-ui/src/pages/Cases.stories.tsx22
-rw-r--r--packages/aml-backoffice-ui/src/pages/Cases.tsx153
-rw-r--r--packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx68
-rw-r--r--packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx2
-rw-r--r--packages/taler-util/src/http-client/exchange.ts447
-rw-r--r--packages/taler-util/src/http-client/officer-account.ts68
-rw-r--r--packages/taler-util/src/types-taler-common.ts28
-rw-r--r--packages/taler-util/src/types-taler-exchange.ts743
12 files changed, 1371 insertions, 347 deletions
diff --git a/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts b/packages/aml-backoffice-ui/src/hooks/account.ts
index 78574ada4..1c5f013a7 100644
--- a/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts
+++ b/packages/aml-backoffice-ui/src/hooks/account.ts
@@ -20,17 +20,17 @@ import { useOfficer } from "./officer.js";
import { useExchangeApiContext } from "@gnu-taler/web-util/browser";
const useSWR = _useSWR as unknown as SWRHook;
-export function useCaseDetails(paytoHash: string) {
+export function useAccountInformation(paytoHash: string) {
const officer = useOfficer();
const session = officer.state === "ready" ? officer.account : undefined;
const { lib: {exchange: api} } = useExchangeApiContext();
async function fetcher([officer, account]: [OfficerAccount, PaytoString]) {
- return await api.getDecisionDetails(officer, account)
+ return await api.getAmlAttributesForAccount(officer, account)
}
- const { data, error } = useSWR<TalerExchangeResultByMethod<"getDecisionDetails">, TalerHttpError>(
+ const { data, error } = useSWR<TalerExchangeResultByMethod<"getAmlAttributesForAccount">, TalerHttpError>(
!session ? undefined : [session, paytoHash], fetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
diff --git a/packages/aml-backoffice-ui/src/hooks/useCases.ts b/packages/aml-backoffice-ui/src/hooks/decisions.ts
index d3a1c1018..e652f233e 100644
--- a/packages/aml-backoffice-ui/src/hooks/useCases.ts
+++ b/packages/aml-backoffice-ui/src/hooks/decisions.ts
@@ -19,13 +19,12 @@ import { useState } from "preact/hooks";
import {
OfficerAccount,
OperationOk,
- TalerExchangeApi,
TalerExchangeResultByMethod,
- TalerHttpError,
+ TalerHttpError
} from "@gnu-taler/taler-util";
+import { useExchangeApiContext } from "@gnu-taler/web-util/browser";
import _useSWR, { SWRHook } from "swr";
import { useOfficer } from "./officer.js";
-import { useExchangeApiContext } from "@gnu-taler/web-util/browser";
const useSWR = _useSWR as unknown as SWRHook;
export const PAGINATED_LIST_SIZE = 10;
@@ -34,12 +33,54 @@ export const PAGINATED_LIST_SIZE = 10;
export const PAGINATED_LIST_REQUEST = PAGINATED_LIST_SIZE + 1;
/**
- * FIXME: mutate result when balance change (transaction )
* @param account
* @param args
* @returns
*/
-export function useCases(state: TalerExchangeApi.AmlState) {
+export function useCurrentDecisions() {
+ const officer = useOfficer();
+ const session = officer.state === "ready" ? officer.account : undefined;
+ const {
+ lib: { exchange: api },
+ } = useExchangeApiContext();
+
+ const [offset, setOffset] = useState<string>();
+
+ async function fetcher([officer, offset]: [
+ OfficerAccount,
+ string | undefined,
+ ]) {
+ return await api.getAmlDecisions(officer, {
+ order: "asc",
+ offset,
+ active: true,
+ limit: PAGINATED_LIST_REQUEST,
+ });
+ }
+
+ const { data, error } = useSWR<
+ TalerExchangeResultByMethod<"getAmlDecisions">,
+ TalerHttpError
+ >(
+ !session ? undefined : [session, offset, "getAmlDecisions"],
+ fetcher,
+ );
+
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
+
+ return buildPaginatedResult(data.body.records, offset, setOffset, (d) =>
+ String(d.rowid),
+ );
+}
+
+/**
+ * @param account
+ * @param args
+ * @returns
+ */
+export function useAccountDecisions(accountStr: string) {
const officer = useOfficer();
const session = officer.state === "ready" ? officer.account : undefined;
const {
@@ -48,23 +89,24 @@ export function useCases(state: TalerExchangeApi.AmlState) {
const [offset, setOffset] = useState<string>();
- async function fetcher([officer, state, offset]: [
+ async function fetcher([officer, account, offset]: [
OfficerAccount,
- TalerExchangeApi.AmlState,
+ string,
string | undefined,
]) {
- return await api.getDecisionsByState(officer, state, {
+ return await api.getAmlDecisions(officer, {
order: "asc",
offset,
+ account,
limit: PAGINATED_LIST_REQUEST,
});
}
const { data, error } = useSWR<
- TalerExchangeResultByMethod<"getDecisionsByState">,
+ TalerExchangeResultByMethod<"getAmlDecisions">,
TalerHttpError
>(
- !session ? undefined : [session, state, offset, "getDecisionsByState"],
+ !session ? undefined : [session, accountStr, offset, "getAmlDecisions"],
fetcher,
);
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
index bb936cebf..2fd95d2c6 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
@@ -45,7 +45,7 @@ import { useState } from "preact/hooks";
import { privatePages } from "../Routing.js";
import { useUiFormsContext } from "../context/ui-forms.js";
import { preloadedForms } from "../forms/index.js";
-import { useCaseDetails } from "../hooks/useCaseDetails.js";
+import { useAccountInformation } from "../hooks/account.js";
import { ShowConsolidated } from "./ShowConsolidated.js";
export type AmlEvent =
@@ -77,7 +77,7 @@ type KycCollectionEvent = {
when: AbsoluteTime;
title: TranslatedString;
values: object;
- provider: string;
+ provider?: string;
};
type KycExpirationEvent = {
type: "kyc-expiration";
@@ -115,45 +115,54 @@ function titleForJustification(
}
export function getEventsFromAmlHistory(
- aml: TalerExchangeApi.AmlDecisionDetail[],
- kyc: TalerExchangeApi.KycDetail[],
+ events: TalerExchangeApi.KycAttributeCollectionEvent[],
i18n: InternationalizationAPI,
forms: FormMetadata[],
): AmlEvent[] {
- const ae: AmlEvent[] = aml.map((a) => {
- const just = parseJustification(a.justification, forms);
+ // const ae: AmlEvent[] = aml.map((a) => {
+ // const just = parseJustification(a.justification, forms);
+ // return {
+ // type: just.type === "ok" ? "aml-form" : "aml-form-error",
+ // state: a.new_state,
+ // threshold: Amounts.parseOrThrow(a.new_threshold),
+ // title: titleForJustification(just, i18n),
+ // metadata: just.type === "ok" ? just.body.metadata : undefined,
+ // justification: just.type === "ok" ? just.body.justification : undefined,
+ // when: {
+ // t_ms:
+ // a.decision_time.t_s === "never"
+ // ? "never"
+ // : a.decision_time.t_s * 1000,
+ // },
+ // } as AmlEvent;
+ // });
+ // const ke = kyc.reduce((prev, k) => {
+ // prev.push({
+ // type: "kyc-collection",
+ // title: i18n.str`collection`,
+ // when: AbsoluteTime.fromProtocolTimestamp(k.collection_time),
+ // values: !k.attributes ? {} : k.attributes,
+ // provider: k.provider_section,
+ // });
+ // prev.push({
+ // type: "kyc-expiration",
+ // title: i18n.str`expiration`,
+ // when: AbsoluteTime.fromProtocolTimestamp(k.expiration_time),
+ // fields: !k.attributes ? [] : Object.keys(k.attributes),
+ // });
+ // return prev;
+ // }, [] as AmlEvent[]);
+
+ const ke = events.map((event) => {
return {
- type: just.type === "ok" ? "aml-form" : "aml-form-error",
- state: a.new_state,
- threshold: Amounts.parseOrThrow(a.new_threshold),
- title: titleForJustification(just, i18n),
- metadata: just.type === "ok" ? just.body.metadata : undefined,
- justification: just.type === "ok" ? just.body.justification : undefined,
- when: {
- t_ms:
- a.decision_time.t_s === "never"
- ? "never"
- : a.decision_time.t_s * 1000,
- },
- } as AmlEvent;
- });
- const ke = kyc.reduce((prev, k) => {
- prev.push({
type: "kyc-collection",
title: i18n.str`collection`,
- when: AbsoluteTime.fromProtocolTimestamp(k.collection_time),
- values: !k.attributes ? {} : k.attributes,
- provider: k.provider_section,
- });
- prev.push({
- type: "kyc-expiration",
- title: i18n.str`expiration`,
- when: AbsoluteTime.fromProtocolTimestamp(k.expiration_time),
- fields: !k.attributes ? [] : Object.keys(k.attributes),
- });
- return prev;
- }, [] as AmlEvent[]);
- return ae.concat(ke).sort(selectSooner);
+ when: AbsoluteTime.fromProtocolTimestamp(event.collection_time),
+ values: !event.attributes ? {} : event.attributes,
+ provider: event.provider_name,
+ } as AmlEvent
+ });
+ return ke.sort(selectSooner);
}
export function CaseDetails({ account }: { account: string }) {
@@ -164,7 +173,7 @@ export function CaseDetails({ account }: { account: string }) {
}>();
const { i18n } = useTranslationContext();
- const details = useCaseDetails(account);
+ const details = useAccountInformation(account);
const { forms } = useUiFormsContext();
const allForms = [...forms, ...preloadedForms(i18n)];
@@ -176,7 +185,7 @@ export function CaseDetails({ account }: { account: string }) {
}
if (details.type === "fail") {
switch (details.case) {
- case HttpStatusCode.Unauthorized:
+ // case HttpStatusCode.Unauthorized:
case HttpStatusCode.Forbidden:
case HttpStatusCode.NotFound:
case HttpStatusCode.Conflict:
@@ -185,11 +194,11 @@ export function CaseDetails({ account }: { account: string }) {
assertUnreachable(details);
}
}
- const { aml_history, kyc_attributes } = details.body;
+ const { details: accountDetails } = details.body;
+
const events = getEventsFromAmlHistory(
- aml_history,
- kyc_attributes,
+ accountDetails,
i18n,
allForms,
);
@@ -424,9 +433,9 @@ function parseJustification(
listOfAllKnownForms: FormMetadata[],
):
| OperationOk<{
- justification: Justification;
- metadata: FormMetadata;
- }>
+ justification: Justification;
+ metadata: FormMetadata;
+ }>
| OperationFail<ParseJustificationFail> {
try {
const justification = JSON.parse(s);
diff --git a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
index 7801625d0..d1257c8fa 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
@@ -157,20 +157,25 @@ export function CaseUpdate({
value: validatedForm,
};
- const decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig"> =
+ const decision: Omit<TalerExchangeApi.AmlDecisionRequest, "officer_sig"> =
{
justification: JSON.stringify(justification),
decision_time: TalerProtocolTimestamp.now(),
h_payto: account,
- new_state: justification.value
- .state as TalerExchangeApi.AmlState,
- new_threshold: Amounts.stringify(
- justification.value.threshold as AmountJson,
- ),
- kyc_requirements: undefined,
+ keep_investigating: false,
+ new_rules: {
+ custom_measures: {},
+ expiration_time: {
+ t_s: "never"
+ },
+ rules: [],
+ successor_measure: undefined
+ },
+ properties: {},
+ new_measure: undefined,
};
- return api.addDecisionDetails(officer.account, decision);
+ return api.makeAmlDesicion(officer.account, decision);
},
() => {
window.location.href = privatePages.cases.url({});
@@ -178,10 +183,9 @@ export function CaseUpdate({
(fail) => {
switch (fail.case) {
case HttpStatusCode.Forbidden:
- case HttpStatusCode.Unauthorized:
return i18n.str`Wrong credentials for "${officer.account}"`;
case HttpStatusCode.NotFound:
- return i18n.str`Officer or account not found`;
+ return i18n.str`The account was not found`;
case HttpStatusCode.Conflict:
return i18n.str`Officer disabled or more recent decision was already submitted.`;
default:
diff --git a/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx b/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx
index 22a6d1867..372fb912f 100644
--- a/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx
@@ -21,21 +21,33 @@
import * as tests from "@gnu-taler/web-util/testing";
import { CasesUI as TestedComponent } from "./Cases.js";
-import { AmountString, TalerExchangeApi } from "@gnu-taler/taler-util";
export default {
title: "cases",
};
export const OneRow = tests.createExample(TestedComponent, {
- filter: TalerExchangeApi.AmlState.normal,
- onChangeFilter: () => null,
records: [
{
- current_state: TalerExchangeApi.AmlState.normal,
+ // current_state: TalerExchangeApi.AmlState.normal,
h_payto: "QWEQWEQWEQWE",
rowid: 1,
- threshold: "USD:1" as AmountString,
+ decision_time: {
+ t_s: "never"
+ },
+ is_active: false,
+ limits: {
+ custom_measures: {},
+ expiration_time: {
+ t_s: "never"
+ },
+ rules: [],
+ successor_measure: undefined,
+ },
+ to_investigate: false,
+ justification: undefined,
+ properties: undefined,
+ // threshold: "USD:1" as AmountString,
},
],
});
diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx
index f66eca33f..613e57493 100644
--- a/packages/aml-backoffice-ui/src/pages/Cases.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx
@@ -29,8 +29,7 @@ import {
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
-import { useEffect, useState } from "preact/hooks";
-import { useCases } from "../hooks/useCases.js";
+import { useCurrentDecisions } from "../hooks/decisions.js";
import { privatePages } from "../Routing.js";
import { FormErrors, RecursivePartial, useFormState } from "../hooks/form.js";
@@ -38,58 +37,58 @@ import { undefinedIfEmpty } from "./CreateAccount.js";
import { Officer } from "./Officer.js";
type FormType = {
- state: TalerExchangeApi.AmlState;
+ // state: TalerExchangeApi.AmlState;
};
export function CasesUI({
records,
- filter,
- onChangeFilter,
+ // filter,
+ // onChangeFilter,
onFirstPage,
onNext,
}: {
onFirstPage?: () => void;
onNext?: () => void;
- filter: TalerExchangeApi.AmlState;
- onChangeFilter: (f: TalerExchangeApi.AmlState) => void;
- records: TalerExchangeApi.AmlRecord[];
+ // filter: TalerExchangeApi.AmlState;
+ // onChangeFilter: (f: TalerExchangeApi.AmlState) => void;
+ records: TalerExchangeApi.AmlDecision[];
}): VNode {
const { i18n } = useTranslationContext();
- const [form, status] = useFormState<FormType>(
- [".state"] as Array<UIHandlerId>,
- {
- state: filter,
- },
- (state) => {
- const errors = undefinedIfEmpty<FormErrors<FormType>>({
- state: state.state === undefined ? i18n.str`required` : undefined,
- });
- if (errors === undefined) {
- const result: FormType = {
- state: state.state!,
- };
- return {
- status: "ok",
- result,
- errors,
- };
- }
- const result: RecursivePartial<FormType> = {
- state: state.state,
- };
- return {
- status: "fail",
- result,
- errors,
- };
- },
- );
- useEffect(() => {
- if (status.status === "ok" && filter !== status.result.state) {
- onChangeFilter(status.result.state);
- }
- }, [form?.state?.value]);
+ // const [form, status] = useFormState<FormType>(
+ // [".state"] as Array<UIHandlerId>,
+ // {
+ // // state: filter,
+ // },
+ // (state) => {
+ // const errors = undefinedIfEmpty<FormErrors<FormType>>({
+ // state: state.state === undefined ? i18n.str`required` : undefined,
+ // });
+ // if (errors === undefined) {
+ // const result: FormType = {
+ // state: state.state!,
+ // };
+ // return {
+ // status: "ok",
+ // result,
+ // errors,
+ // };
+ // }
+ // const result: RecursivePartial<FormType> = {
+ // state: state.state,
+ // };
+ // return {
+ // status: "fail",
+ // result,
+ // errors,
+ // };
+ // },
+ // );
+ // useEffect(() => {
+ // if (status.status === "ok" && filter !== status.result.state) {
+ // onChangeFilter(status.result.state);
+ // }
+ // }, [form?.state?.value]);
return (
<div>
@@ -105,7 +104,7 @@ export function CasesUI({
</p>
</div>
<div class="px-2">
- <InputChoiceHorizontal<FormType, "state">
+ {/* <InputChoiceHorizontal<FormType, "state">
name="state"
label={i18n.str`Filter`}
handler={form.state}
@@ -124,7 +123,7 @@ export function CasesUI({
value: "normal",
},
]}
- />
+ /> */}
</div>
</div>
<div class="mt-8 flow-root">
@@ -173,34 +172,10 @@ export function CasesUI({
</div>
</td>
<td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500">
- {((state: TalerExchangeApi.AmlState): VNode => {
- switch (state) {
- case TalerExchangeApi.AmlState.normal: {
- return (
- <span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
- Normal
- </span>
- );
- }
- case TalerExchangeApi.AmlState.pending: {
- return (
- <span class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-green-600/20">
- Pending
- </span>
- );
- }
- case TalerExchangeApi.AmlState.frozen: {
- return (
- <span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-green-600/20">
- Frozen
- </span>
- );
- }
- }
- })(r.current_state)}
+ {r.rowid}
</td>
<td class="whitespace-nowrap px-3 py-5 text-sm text-gray-900">
- {r.threshold}
+ ???
</td>
</tr>
);
@@ -217,11 +192,11 @@ export function CasesUI({
}
export function Cases() {
- const [stateFilter, setStateFilter] = useState(
- TalerExchangeApi.AmlState.pending,
- );
+ // const [stateFilter, setStateFilter] = useState(
+ // TalerExchangeApi.AmlState.pending,
+ // );
- const list = useCases(stateFilter);
+ const list = useCurrentDecisions();
const { i18n } = useTranslationContext();
if (!list) {
@@ -238,28 +213,38 @@ export function Cases() {
<Fragment>
<Attention type="danger" title={i18n.str`Operation denied`}>
<i18n.Translate>
- This account doesn't have access. Request account activation
- sending your public key.
+ This account signature is wrong, contact administrator or create a new one.
+ </i18n.Translate>
+ </Attention>
+ <Officer />
+ </Fragment>
+ );
+ }
+ case HttpStatusCode.NotFound: {
+ return (
+ <Fragment>
+ <Attention type="danger" title={i18n.str`Operation denied`}>
+ <i18n.Translate>
+ This account is not known.
</i18n.Translate>
</Attention>
<Officer />
</Fragment>
);
}
- case HttpStatusCode.Unauthorized: {
+ case HttpStatusCode.Conflict: {
return (
<Fragment>
<Attention type="danger" title={i18n.str`Operation denied`}>
<i18n.Translate>
- This account is not allowed to perform list the cases.
+ This account doesn't have access. Request account activation
+ sending your public key.
</i18n.Translate>
</Attention>
<Officer />
</Fragment>
);
}
- case HttpStatusCode.NotFound:
- case HttpStatusCode.Conflict:
return <Officer />;
default:
assertUnreachable(list);
@@ -271,10 +256,10 @@ export function Cases() {
records={list.body}
onFirstPage={list.isFirstPage ? undefined : list.loadFirst}
onNext={list.isLastPage ? undefined : list.loadNext}
- filter={stateFilter}
- onChangeFilter={(d) => {
- setStateFilter(d);
- }}
+ // filter={stateFilter}
+ // onChangeFilter={(d) => {
+ // setStateFilter(d);
+ // }}
/>
);
}
diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx
index 714bf6580..2fc661bd4 100644
--- a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx
+++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx
@@ -42,7 +42,7 @@ const nullTranslator: InternationalizationAPI = {
};
export const WithEmptyHistory = tests.createExample(TestedComponent, {
- history: getEventsFromAmlHistory([], [], nullTranslator, []),
+ history: getEventsFromAmlHistory([], nullTranslator, []),
until: AbsoluteTime.now(),
});
@@ -50,79 +50,17 @@ export const WithSomeEvents = tests.createExample(TestedComponent, {
history: getEventsFromAmlHistory(
[
{
- decider_pub: "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
- justification:
- '{"index":0,"name":"Simple comment","value":{"fullName":"loggedIn_user_fullname","when":{"t_ms":1700207199558},"state":1,"threshold":{"currency":"STATER","fraction":0,"value":0},"comment":"test"}}',
- new_threshold: "STATER:0" as AmountString,
- new_state: 1,
- decision_time: {
- t_s: 1700208199,
- },
- },
- {
- decider_pub: "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
- justification:
- '{"index":0,"name":"Simple comment","value":{"fullName":"loggedIn_user_fullname","when":{"t_ms":1700207199558},"state":1,"threshold":{"currency":"STATER","fraction":0,"value":0},"comment":"test"}}',
- new_threshold: "STATER:0" as AmountString,
- new_state: 1,
- decision_time: {
- t_s: 1700208211,
- },
- },
- {
- decider_pub: "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
- justification:
- '{"index":0,"name":"Simple comment","value":{"fullName":"loggedIn_user_fullname","when":{"t_ms":1700207199558},"state":1,"threshold":{"currency":"STATER","fraction":0,"value":0},"comment":"test"}}',
- new_threshold: "STATER:0" as AmountString,
- new_state: 1,
- decision_time: {
- t_s: 1700208220,
- },
- },
- {
- decider_pub: "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
- justification:
- '{"index":4,"name":"Declaration for trusts (902.13e)","value":{"fullName":"loggedIn_user_fullname","when":{"t_ms":1700208362854},"state":1,"threshold":{"currency":"STATER","fraction":0,"value":0},"contractingPartner":"f","knownAs":"a","trust":{"name":"b","type":"discretionary","revocability":"irrevocable"}}}',
- new_threshold: "STATER:0" as AmountString,
- new_state: 1,
- decision_time: {
- t_s: 1700208385,
- },
- },
- {
- decider_pub: "6CD3J8XSKWQPFFDJY4SP4RK2D7T7WW7JRJDTXHNZY7YKGXDCE2QG",
- justification:
- '{"id":"simple_comment","label":"Simple comment","version":1,"value":{"when":{"t_ms":1700488420810},"state":1,"threshold":{"currency":"STATER","fraction":0,"value":0},"comment":"qwe"}}',
- new_threshold: "STATER:0" as AmountString,
- new_state: 1,
- decision_time: {
- t_s: 1700488423,
- },
- },
- {
- decider_pub: "6CD3J8XSKWQPFFDJY4SP4RK2D7T7WW7JRJDTXHNZY7YKGXDCE2QG",
- justification:
- '{"id":"simple_comment","label":"Simple comment","version":1,"value":{"when":{"t_ms":1700488671251},"state":1,"threshold":{"currency":"STATER","fraction":0,"value":0},"comment":"asd asd asd "}}',
- new_threshold: "STATER:0" as AmountString,
- new_state: 1,
- decision_time: {
- t_s: 1700488677,
- },
- },
- ],
- [
- {
collection_time: AbsoluteTime.toProtocolTimestamp(
AbsoluteTime.subtractDuraction(
AbsoluteTime.now(),
Duration.fromPrettyString("1d"),
),
),
- expiration_time: { t_s: "never" },
- provider_section: "asd",
+ provider_name: "asd",
attributes: {
email: "sebasjm@qwdde.com",
},
+ rowid: 1,
},
],
nullTranslator,
diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
index cdc5d0bc1..2fbbefe0c 100644
--- a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
+++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
@@ -125,7 +125,7 @@ interface Consolidated {
kyc: {
[field: string]: {
value: unknown;
- provider: string;
+ provider?: string;
since: AbsoluteTime;
};
};
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");