diff options
author | Sebastian <sebasjm@gmail.com> | 2024-08-13 15:27:41 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2024-08-13 15:27:59 -0300 |
commit | aa4fc564777aab82e16dd4c02012682ff67a3e8a (patch) | |
tree | 79470e282d3b953982be30db4b2ae3d5a056d4c1 | |
parent | d5f3a51c5684d6d29a3b9c1b956b2cfc68434f23 (diff) | |
download | wallet-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.tsx | 95 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx | 24 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/Cases.stories.tsx | 22 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/Cases.tsx | 153 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx | 68 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx | 2 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/exchange.ts | 447 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/officer-account.ts | 68 | ||||
-rw-r--r-- | packages/taler-util/src/types-taler-common.ts | 28 | ||||
-rw-r--r-- | packages/taler-util/src/types-taler-exchange.ts | 743 |
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"); |