case details and missing decision encryption
This commit is contained in:
parent
e90f1b4206
commit
7e37b34744
@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
PaytoUri,
|
Amounts,
|
||||||
TalerSignaturePurpose,
|
TalerSignaturePurpose,
|
||||||
|
amountToBuffer,
|
||||||
bufferForUint32,
|
bufferForUint32,
|
||||||
buildSigPS,
|
buildSigPS,
|
||||||
createEddsaKeyPair,
|
createEddsaKeyPair,
|
||||||
@ -11,8 +12,10 @@ import {
|
|||||||
encodeCrock,
|
encodeCrock,
|
||||||
encryptWithDerivedKey,
|
encryptWithDerivedKey,
|
||||||
getRandomBytesF,
|
getRandomBytesF,
|
||||||
|
hash,
|
||||||
|
hashTruncate32,
|
||||||
stringToBytes,
|
stringToBytes,
|
||||||
stringifyPaytoUri,
|
timestampRoundedToBuffer
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { AmlExchangeBackend } from "./types.js";
|
import { AmlExchangeBackend } from "./types.js";
|
||||||
|
|
||||||
@ -60,12 +63,16 @@ export function buildQuerySignature(key: SigningKey): string {
|
|||||||
}
|
}
|
||||||
export function buildDecisionSignature(
|
export function buildDecisionSignature(
|
||||||
key: SigningKey,
|
key: SigningKey,
|
||||||
payto: PaytoUri,
|
decision: AmlExchangeBackend.AmlDecision,
|
||||||
state: AmlExchangeBackend.AmlState,
|
|
||||||
): string {
|
): string {
|
||||||
|
|
||||||
const sigBlob = buildSigPS(TalerSignaturePurpose.TALER_SIGNATURE_AML_DECISION)
|
const sigBlob = buildSigPS(TalerSignaturePurpose.TALER_SIGNATURE_AML_DECISION)
|
||||||
.put(decodeCrock(stringifyPaytoUri(payto)))
|
.put(hash(stringToBytes(decision.justification)))
|
||||||
.put(bufferForUint32(state))
|
// .put(timestampRoundedToBuffer(decision.decision_time))
|
||||||
|
.put(amountToBuffer(decision.new_threshold))
|
||||||
|
.put(decodeCrock(decision.h_payto))
|
||||||
|
// .put(hash(stringToBytes(decision.kyc_requirements)))
|
||||||
|
.put(bufferForUint32(decision.new_state))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
return encodeCrock(eddsaSign(sigBlob, key));
|
return encodeCrock(eddsaSign(sigBlob, key));
|
||||||
|
@ -29,7 +29,7 @@ export type FormState<T> = {
|
|||||||
? Partial<InputFieldState>
|
? Partial<InputFieldState>
|
||||||
: T[field] extends Array<infer P>
|
: T[field] extends Array<infer P>
|
||||||
? Partial<InputArrayFieldState<P>>
|
? Partial<InputArrayFieldState<P>>
|
||||||
: T[field] extends object
|
: T[field] extends (object | undefined)
|
||||||
? FormState<T[field]>
|
? FormState<T[field]>
|
||||||
: Partial<InputFieldState>;
|
: Partial<InputFieldState>;
|
||||||
};
|
};
|
||||||
|
@ -14,7 +14,7 @@ interface useBackendType {
|
|||||||
path: string,
|
path: string,
|
||||||
options?: RequestOptions,
|
options?: RequestOptions,
|
||||||
) => Promise<HttpResponseOk<T>>;
|
) => Promise<HttpResponseOk<T>>;
|
||||||
fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
|
fetcher: <T>(args: [string, string]) => Promise<HttpResponseOk<T>>;
|
||||||
paginatedFetcher: <T>(
|
paginatedFetcher: <T>(
|
||||||
args: [string, number, number, string],
|
args: [string, number, number, string],
|
||||||
) => Promise<HttpResponseOk<T>>;
|
) => Promise<HttpResponseOk<T>>;
|
||||||
@ -35,8 +35,10 @@ export function usePublicBackend(): useBackendType {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const fetcher = useCallback(
|
const fetcher = useCallback(
|
||||||
function fetcherImpl<T>(endpoint: string): Promise<HttpResponseOk<T>> {
|
function fetcherImpl<T>([endpoint, talerAmlOfficerSignature]: [string,string]): Promise<HttpResponseOk<T>> {
|
||||||
return requestHandler<T>(baseUrl, endpoint);
|
return requestHandler<T>(baseUrl, endpoint, {
|
||||||
|
talerAmlOfficerSignature
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[baseUrl],
|
[baseUrl],
|
||||||
);
|
);
|
||||||
|
162
packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts
Normal file
162
packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
HttpResponse,
|
||||||
|
HttpResponseOk,
|
||||||
|
RequestError
|
||||||
|
} from "@gnu-taler/web-util/browser";
|
||||||
|
import { AmlExchangeBackend } from "../types.js";
|
||||||
|
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
|
||||||
|
import _useSWR, { SWRHook, useSWRConfig } from "swr";
|
||||||
|
import { AccountId } from "../account.js";
|
||||||
|
import { usePublicBackend } from "./useBackend.js";
|
||||||
|
const useSWR = _useSWR as unknown as SWRHook;
|
||||||
|
|
||||||
|
export function useCaseDetails(
|
||||||
|
account: AccountId,
|
||||||
|
paytoHash: string,
|
||||||
|
signature: string | undefined,
|
||||||
|
): HttpResponse<
|
||||||
|
AmlExchangeBackend.AmlDecisionDetails,
|
||||||
|
AmlExchangeBackend.AmlError
|
||||||
|
> {
|
||||||
|
const { fetcher } = usePublicBackend();
|
||||||
|
|
||||||
|
const { data, error } = useSWR<
|
||||||
|
HttpResponseOk<AmlExchangeBackend.AmlDecisionDetails>,
|
||||||
|
RequestError<AmlExchangeBackend.AmlError>
|
||||||
|
>( [
|
||||||
|
`aml/${account}/decision/${(paytoHash)}`,
|
||||||
|
signature,
|
||||||
|
],
|
||||||
|
fetcher, {
|
||||||
|
refreshInterval: 0,
|
||||||
|
refreshWhenHidden: false,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
refreshWhenOffline: false,
|
||||||
|
errorRetryCount: 0,
|
||||||
|
errorRetryInterval: 1,
|
||||||
|
shouldRetryOnError: false,
|
||||||
|
keepPreviousData: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data) return data;
|
||||||
|
if (error) return error.cause;
|
||||||
|
return { loading: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const example1: AmlExchangeBackend.AmlDecisionDetails = {
|
||||||
|
aml_history: [
|
||||||
|
{
|
||||||
|
justification: "Lack of documentation",
|
||||||
|
decider_pub: "ASDASDASD",
|
||||||
|
decision_time: {
|
||||||
|
t_s: Date.now() / 1000,
|
||||||
|
},
|
||||||
|
new_state: 2,
|
||||||
|
new_threshold: "USD:0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
justification: "Doing a transfer of high amount",
|
||||||
|
decider_pub: "ASDASDASD",
|
||||||
|
decision_time: {
|
||||||
|
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 6,
|
||||||
|
},
|
||||||
|
new_state: 1,
|
||||||
|
new_threshold: "USD:2000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
justification: "Account is known to the system",
|
||||||
|
decider_pub: "ASDASDASD",
|
||||||
|
decision_time: {
|
||||||
|
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 9,
|
||||||
|
},
|
||||||
|
new_state: 0,
|
||||||
|
new_threshold: "USD:100",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
kyc_attributes: [
|
||||||
|
{
|
||||||
|
collection_time: {
|
||||||
|
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 8,
|
||||||
|
},
|
||||||
|
expiration_time: {
|
||||||
|
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 4,
|
||||||
|
},
|
||||||
|
provider_section: "asdasd",
|
||||||
|
attributes: {
|
||||||
|
name: "Sebastian",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
collection_time: {
|
||||||
|
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 5,
|
||||||
|
},
|
||||||
|
expiration_time: {
|
||||||
|
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 2,
|
||||||
|
},
|
||||||
|
provider_section: "asdasd",
|
||||||
|
attributes: {
|
||||||
|
creditCard: "12312312312",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const exampleResponse: HttpResponse<AmlExchangeBackend.AmlDecisionDetails,AmlExchangeBackend.AmlError> = {
|
||||||
|
ok: true,
|
||||||
|
data: example1,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function useAmlCasesAPI(): AmlCaseAPI {
|
||||||
|
const { request } = usePublicBackend();
|
||||||
|
const mutateAll = useMatchMutate();
|
||||||
|
|
||||||
|
const updateDecision = async (
|
||||||
|
officer: AccountId,
|
||||||
|
data: AmlExchangeBackend.AmlDecision,
|
||||||
|
): Promise<HttpResponseOk<void>> => {
|
||||||
|
const res = await request<void>(`aml/${officer}/decision`, {
|
||||||
|
method: "POST",
|
||||||
|
data,
|
||||||
|
contentType: "json",
|
||||||
|
});
|
||||||
|
await mutateAll(/.*aml.*/);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateDecision,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AmlCaseAPI {
|
||||||
|
updateDecision: (
|
||||||
|
officer: AccountId,
|
||||||
|
data: AmlExchangeBackend.AmlDecision,
|
||||||
|
) => Promise<HttpResponseOk<void>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function useMatchMutate(): (
|
||||||
|
re: RegExp,
|
||||||
|
value?: unknown,
|
||||||
|
) => Promise<any> {
|
||||||
|
const { cache, mutate } = useSWRConfig();
|
||||||
|
|
||||||
|
if (!(cache instanceof Map)) {
|
||||||
|
throw new Error(
|
||||||
|
"matchMutate requires the cache provider to be a Map instance",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return function matchRegexMutate(re: RegExp, value?: unknown) {
|
||||||
|
const allKeys = Array.from(cache.keys());
|
||||||
|
const keys = allKeys.filter((key) => re.test(key));
|
||||||
|
const mutations = keys.map((key) => {
|
||||||
|
return mutate(key, value, true);
|
||||||
|
});
|
||||||
|
return Promise.all(mutations);
|
||||||
|
};
|
||||||
|
}
|
@ -92,3 +92,56 @@ export function useCases(
|
|||||||
}
|
}
|
||||||
return { loading: true };
|
return { loading: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const example1: AmlExchangeBackend.AmlRecords = {
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
current_state: 0,
|
||||||
|
h_payto: "QWEQWEQWEQWEWQE",
|
||||||
|
rowid: 1,
|
||||||
|
threshold: "USD 100",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
current_state: 1,
|
||||||
|
h_payto: "ASDASDASD",
|
||||||
|
rowid: 1,
|
||||||
|
threshold: "USD 100",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
current_state: 2,
|
||||||
|
h_payto: "ZXCZXCZXCXZC",
|
||||||
|
rowid: 1,
|
||||||
|
threshold: "USD 1000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
current_state: 0,
|
||||||
|
h_payto: "QWEQWEQWEQWEWQE",
|
||||||
|
rowid: 1,
|
||||||
|
threshold: "USD 100",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
current_state: 1,
|
||||||
|
h_payto: "ASDASDASD",
|
||||||
|
rowid: 1,
|
||||||
|
threshold: "USD 100",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
current_state: 2,
|
||||||
|
h_payto: "ZXCZXCZXCXZC",
|
||||||
|
rowid: 1,
|
||||||
|
threshold: "USD 1000",
|
||||||
|
},
|
||||||
|
].map((e, idx) => {
|
||||||
|
e.rowid = idx;
|
||||||
|
e.threshold = `${e.threshold}${idx}`;
|
||||||
|
return e;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const exampleResponse: HttpResponsePaginated<AmlExchangeBackend.AmlRecords,AmlExchangeBackend.AmlError> = {
|
||||||
|
ok: true,
|
||||||
|
data: example1,
|
||||||
|
loadMore: () => {},
|
||||||
|
loadMorePrev: () => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ const cases: PageEntry = {
|
|||||||
url: "#/cases",
|
url: "#/cases",
|
||||||
view: Cases,
|
view: Cases,
|
||||||
};
|
};
|
||||||
const account: PageEntry<{ account?: string }> = {
|
const account: PageEntry<{ account: string }> = {
|
||||||
url: pageDefinition("#/account/:account"),
|
url: pageDefinition("#/account/:account"),
|
||||||
view: CaseDetails,
|
view: CaseDetails,
|
||||||
};
|
};
|
||||||
|
@ -13,64 +13,13 @@ import { FlexibleForm } from "../forms/index.js";
|
|||||||
import { UIFormField } from "../handlers/forms.js";
|
import { UIFormField } from "../handlers/forms.js";
|
||||||
import { Pages } from "../pages.js";
|
import { Pages } from "../pages.js";
|
||||||
import { AmlExchangeBackend } from "../types.js";
|
import { AmlExchangeBackend } from "../types.js";
|
||||||
|
import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
|
||||||
|
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
|
import { useOfficer } from "../hooks/useOfficer.js";
|
||||||
|
import { buildQuerySignature } from "../account.js";
|
||||||
|
import { useCaseDetails } from "../hooks/useCaseDetails.js";
|
||||||
|
import { handleNotOkResult } from "../utils/errors.js";
|
||||||
|
|
||||||
const response: AmlExchangeBackend.AmlDecisionDetails = {
|
|
||||||
aml_history: [
|
|
||||||
{
|
|
||||||
justification: "Lack of documentation",
|
|
||||||
decider_pub: "ASDASDASD",
|
|
||||||
decision_time: {
|
|
||||||
t_s: Date.now() / 1000,
|
|
||||||
},
|
|
||||||
new_state: 2,
|
|
||||||
new_threshold: "USD:0",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
justification: "Doing a transfer of high amount",
|
|
||||||
decider_pub: "ASDASDASD",
|
|
||||||
decision_time: {
|
|
||||||
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 6,
|
|
||||||
},
|
|
||||||
new_state: 1,
|
|
||||||
new_threshold: "USD:2000",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
justification: "Account is known to the system",
|
|
||||||
decider_pub: "ASDASDASD",
|
|
||||||
decision_time: {
|
|
||||||
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 9,
|
|
||||||
},
|
|
||||||
new_state: 0,
|
|
||||||
new_threshold: "USD:100",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
kyc_attributes: [
|
|
||||||
{
|
|
||||||
collection_time: {
|
|
||||||
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 8,
|
|
||||||
},
|
|
||||||
expiration_time: {
|
|
||||||
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 4,
|
|
||||||
},
|
|
||||||
provider_section: "asdasd",
|
|
||||||
attributes: {
|
|
||||||
name: "Sebastian",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
collection_time: {
|
|
||||||
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 5,
|
|
||||||
},
|
|
||||||
expiration_time: {
|
|
||||||
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 2,
|
|
||||||
},
|
|
||||||
provider_section: "asdasd",
|
|
||||||
attributes: {
|
|
||||||
creditCard: "12312312312",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
type AmlEvent = AmlFormEvent | KycCollectionEvent | KycExpirationEvent;
|
type AmlEvent = AmlFormEvent | KycCollectionEvent | KycExpirationEvent;
|
||||||
type AmlFormEvent = {
|
type AmlFormEvent = {
|
||||||
type: "aml-form";
|
type: "aml-form";
|
||||||
@ -127,7 +76,7 @@ function getEventsFromAmlHistory(
|
|||||||
});
|
});
|
||||||
prev.push({
|
prev.push({
|
||||||
type: "kyc-expiration",
|
type: "kyc-expiration",
|
||||||
title: "expired" as TranslatedString,
|
title: "expiration" as TranslatedString,
|
||||||
when: AbsoluteTime.fromProtocolTimestamp(k.expiration_time),
|
when: AbsoluteTime.fromProtocolTimestamp(k.expiration_time),
|
||||||
fields: !k.attributes ? [] : Object.keys(k.attributes),
|
fields: !k.attributes ? [] : Object.keys(k.attributes),
|
||||||
});
|
});
|
||||||
@ -136,19 +85,30 @@ function getEventsFromAmlHistory(
|
|||||||
return ae.concat(ke).sort(selectSooner);
|
return ae.concat(ke).sort(selectSooner);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CaseDetails({ account }: { account?: string }) {
|
export function CaseDetails({ account: paytoHash }: { account: string }) {
|
||||||
const events = getEventsFromAmlHistory(
|
const [selected, setSelected] = useState<AmlEvent | undefined>(undefined);
|
||||||
response.aml_history,
|
|
||||||
response.kyc_attributes,
|
const officer = useOfficer();
|
||||||
);
|
const { i18n } = useTranslationContext();
|
||||||
console.log("DETAILS", events, events[events.length - 1 - 2]);
|
if (officer.state !== "ready") {
|
||||||
const [selected, setSelected] = useState<AmlEvent>(
|
return <HandleAccountNotReady officer={officer} />;
|
||||||
events[events.length - 1 - 2],
|
}
|
||||||
);
|
const signature =
|
||||||
|
officer.state === "ready"
|
||||||
|
? buildQuerySignature(officer.account.signingKey)
|
||||||
|
: undefined;
|
||||||
|
const details = useCaseDetails(officer.account.accountId, paytoHash, signature)
|
||||||
|
if (!details.ok && !details.loading) {
|
||||||
|
return handleNotOkResult(i18n)(details);
|
||||||
|
}
|
||||||
|
const aml_history = details.loading ? [] : details.data.aml_history
|
||||||
|
const kyc_attributes = details.loading ? [] : details.data.kyc_attributes
|
||||||
|
const events = getEventsFromAmlHistory(aml_history,kyc_attributes);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<a
|
<a
|
||||||
href={Pages.newFormEntry.url({ account })}
|
href={Pages.newFormEntry.url({ account: paytoHash })}
|
||||||
class="m-4 block rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
|
class="m-4 block rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
|
||||||
>
|
>
|
||||||
New AML form
|
New AML form
|
||||||
@ -271,13 +231,24 @@ function ShowConsolidated({
|
|||||||
history: AmlEvent[];
|
history: AmlEvent[];
|
||||||
until: AmlEvent;
|
until: AmlEvent;
|
||||||
}): VNode {
|
}): VNode {
|
||||||
console.log("UNTIL", until);
|
|
||||||
const cons = getConsolidated(history, until.when);
|
const cons = getConsolidated(history, until.when);
|
||||||
|
|
||||||
const form: FlexibleForm<Consolidated> = {
|
const form: FlexibleForm<Consolidated> = {
|
||||||
versionId: "1",
|
versionId: "1",
|
||||||
behavior: (form) => {
|
behavior: (form) => {
|
||||||
return {};
|
return {
|
||||||
|
aml: {
|
||||||
|
threshold: {
|
||||||
|
hidden: !form.aml
|
||||||
|
},
|
||||||
|
since: {
|
||||||
|
hidden: !form.aml
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
hidden: !form.aml
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
},
|
},
|
||||||
design: [
|
design: [
|
||||||
{
|
{
|
||||||
@ -356,8 +327,8 @@ function ShowConsolidated({
|
|||||||
|
|
||||||
interface Consolidated {
|
interface Consolidated {
|
||||||
aml: {
|
aml: {
|
||||||
state?: AmlExchangeBackend.AmlState;
|
state: AmlExchangeBackend.AmlState;
|
||||||
threshold?: AmountJson;
|
threshold: AmountJson;
|
||||||
since: AbsoluteTime;
|
since: AbsoluteTime;
|
||||||
};
|
};
|
||||||
kyc: {
|
kyc: {
|
||||||
@ -375,7 +346,13 @@ function getConsolidated(
|
|||||||
): Consolidated {
|
): Consolidated {
|
||||||
const initial: Consolidated = {
|
const initial: Consolidated = {
|
||||||
aml: {
|
aml: {
|
||||||
since: AbsoluteTime.never(),
|
state: AmlExchangeBackend.AmlState.normal,
|
||||||
|
threshold: {
|
||||||
|
currency: "ARS",
|
||||||
|
value: 1000,
|
||||||
|
fraction: 0,
|
||||||
|
},
|
||||||
|
since: AbsoluteTime.never()
|
||||||
},
|
},
|
||||||
kyc: {},
|
kyc: {},
|
||||||
};
|
};
|
||||||
@ -391,9 +368,11 @@ function getConsolidated(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "aml-form": {
|
case "aml-form": {
|
||||||
prev.aml.threshold = cur.threshold;
|
prev.aml = {
|
||||||
prev.aml.state = cur.state;
|
since: cur.when,
|
||||||
prev.aml.since = cur.when;
|
state: cur.state,
|
||||||
|
threshold: cur.threshold
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "kyc-collection": {
|
case "kyc-collection": {
|
||||||
|
@ -12,59 +12,6 @@ import { buildQuerySignature } from "../account.js";
|
|||||||
import { handleNotOkResult } from "../utils/errors.js";
|
import { handleNotOkResult } from "../utils/errors.js";
|
||||||
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
|
|
||||||
const response: AmlExchangeBackend.AmlRecords = {
|
|
||||||
records: [
|
|
||||||
{
|
|
||||||
current_state: 0,
|
|
||||||
h_payto: "QWEQWEQWEQWEWQE",
|
|
||||||
rowid: 1,
|
|
||||||
threshold: "USD 100",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
current_state: 1,
|
|
||||||
h_payto: "ASDASDASD",
|
|
||||||
rowid: 1,
|
|
||||||
threshold: "USD 100",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
current_state: 2,
|
|
||||||
h_payto: "ZXCZXCZXCXZC",
|
|
||||||
rowid: 1,
|
|
||||||
threshold: "USD 1000",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
current_state: 0,
|
|
||||||
h_payto: "QWEQWEQWEQWEWQE",
|
|
||||||
rowid: 1,
|
|
||||||
threshold: "USD 100",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
current_state: 1,
|
|
||||||
h_payto: "ASDASDASD",
|
|
||||||
rowid: 1,
|
|
||||||
threshold: "USD 100",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
current_state: 2,
|
|
||||||
h_payto: "ZXCZXCZXCXZC",
|
|
||||||
rowid: 1,
|
|
||||||
threshold: "USD 1000",
|
|
||||||
},
|
|
||||||
].map((e, idx) => {
|
|
||||||
e.rowid = idx;
|
|
||||||
e.threshold = `${e.threshold}${idx}`;
|
|
||||||
return e;
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
function doFilter(
|
|
||||||
list: typeof response.records,
|
|
||||||
filter: AmlExchangeBackend.AmlState | undefined,
|
|
||||||
): typeof response.records {
|
|
||||||
if (filter === undefined) return list;
|
|
||||||
return list.filter((r) => r.current_state === filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Cases() {
|
export function Cases() {
|
||||||
const officer = useOfficer();
|
const officer = useOfficer();
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
@ -2,8 +2,12 @@ import { VNode, h } from "preact";
|
|||||||
import { allForms } from "./AntiMoneyLaunderingForm.js";
|
import { allForms } from "./AntiMoneyLaunderingForm.js";
|
||||||
import { Pages } from "../pages.js";
|
import { Pages } from "../pages.js";
|
||||||
import { NiceForm } from "../NiceForm.js";
|
import { NiceForm } from "../NiceForm.js";
|
||||||
import { AbsoluteTime, Amounts } from "@gnu-taler/taler-util";
|
import { AbsoluteTime, Amounts, TalerProtocolTimestamp } from "@gnu-taler/taler-util";
|
||||||
import { AmlExchangeBackend } from "../types.js";
|
import { AmlExchangeBackend } from "../types.js";
|
||||||
|
import { useAmlCasesAPI } from "../hooks/useCaseDetails.js";
|
||||||
|
import { useOfficer } from "../hooks/useOfficer.js";
|
||||||
|
import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
|
||||||
|
import { buildDecisionSignature, buildQuerySignature } from "../account.js";
|
||||||
|
|
||||||
export function NewFormEntry({
|
export function NewFormEntry({
|
||||||
account,
|
account,
|
||||||
@ -12,30 +16,58 @@ export function NewFormEntry({
|
|||||||
account?: string;
|
account?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
}): VNode {
|
}): VNode {
|
||||||
|
const officer = useOfficer();
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return <div>no account</div>;
|
return <div>no account</div>;
|
||||||
}
|
}
|
||||||
if (!type) {
|
if (!type) {
|
||||||
return <SelectForm account={account} />;
|
return <SelectForm account={account} />;
|
||||||
}
|
}
|
||||||
|
if (officer.state !== "ready") {
|
||||||
|
return <HandleAccountNotReady officer={officer} />;
|
||||||
|
}
|
||||||
|
|
||||||
const selectedForm = Number.parseInt(type ?? "0", 10);
|
const selectedForm = Number.parseInt(type ?? "0", 10);
|
||||||
if (Number.isNaN(selectedForm)) {
|
if (Number.isNaN(selectedForm)) {
|
||||||
return <div>WHAT! {type}</div>;
|
return <div>WHAT! {type}</div>;
|
||||||
}
|
}
|
||||||
const showingFrom = allForms[selectedForm].impl;
|
const showingFrom = allForms[selectedForm].impl;
|
||||||
|
const formName = allForms[selectedForm].name
|
||||||
const initial = {
|
const initial = {
|
||||||
fullName: "loggedIn_user_fullname",
|
fullName: "loggedIn_user_fullname",
|
||||||
when: AbsoluteTime.now(),
|
when: AbsoluteTime.now(),
|
||||||
state: AmlExchangeBackend.AmlState.pending,
|
state: AmlExchangeBackend.AmlState.pending,
|
||||||
threshold: Amounts.parseOrThrow("USD:10"),
|
threshold: Amounts.parseOrThrow("ARS:1000"),
|
||||||
};
|
};
|
||||||
|
const api = useAmlCasesAPI()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NiceForm
|
<NiceForm
|
||||||
initial={initial}
|
initial={initial}
|
||||||
form={showingFrom(initial)}
|
form={showingFrom(initial)}
|
||||||
onSubmit={(v) => {
|
onSubmit={(formValue) => {
|
||||||
alert(JSON.stringify(v));
|
if (formValue.state === undefined || formValue.threshold === undefined) return;
|
||||||
|
|
||||||
|
const justification = {
|
||||||
|
index: selectedForm,
|
||||||
|
name: formName,
|
||||||
|
value: formValue
|
||||||
|
}
|
||||||
|
const decision: AmlExchangeBackend.AmlDecision = {
|
||||||
|
justification: JSON.stringify(justification),
|
||||||
|
decision_time: TalerProtocolTimestamp.now(),
|
||||||
|
h_payto: account,
|
||||||
|
new_state: formValue.state,
|
||||||
|
new_threshold: Amounts.stringify(formValue.threshold),
|
||||||
|
officer_sig: "",
|
||||||
|
kyc_requirements: undefined
|
||||||
|
}
|
||||||
|
const signature = buildDecisionSignature(officer.account.signingKey, decision);
|
||||||
|
decision.officer_sig = signature
|
||||||
|
api.updateDecision(officer.account.accountId, decision);
|
||||||
|
|
||||||
|
// alert(JSON.stringify(formValue));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="mt-6 flex items-center justify-end gap-x-6">
|
<div class="mt-6 flex items-center justify-end gap-x-6">
|
||||||
|
@ -59,6 +59,9 @@ export namespace AmlExchangeBackend {
|
|||||||
type PaytoHash = string;
|
type PaytoHash = string;
|
||||||
type Integer = number;
|
type Integer = number;
|
||||||
type Amount = string;
|
type Amount = string;
|
||||||
|
// EdDSA signatures are transmitted as 64-bytes base32
|
||||||
|
// binary-encoded objects with just the R and S values (base32_ binary-only).
|
||||||
|
type EddsaSignature = string;
|
||||||
|
|
||||||
export interface AmlRecords {
|
export interface AmlRecords {
|
||||||
// Array of AML records matching the query.
|
// Array of AML records matching the query.
|
||||||
@ -85,4 +88,37 @@ export namespace AmlExchangeBackend {
|
|||||||
pending = 1,
|
pending = 1,
|
||||||
frozen = 2,
|
frozen = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface AmlDecision {
|
||||||
|
|
||||||
|
// Human-readable justification for the decision.
|
||||||
|
justification: string;
|
||||||
|
|
||||||
|
// At what monthly transaction volume should the
|
||||||
|
// decision be automatically reviewed?
|
||||||
|
new_threshold: Amount;
|
||||||
|
|
||||||
|
// 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: EddsaSignature;
|
||||||
|
|
||||||
|
// 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[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user