Compare commits
10 Commits
aef994de3a
...
0b60602833
Author | SHA1 | Date | |
---|---|---|---|
0b60602833 | |||
|
9c17b7cd92 | ||
|
7e37b34744 | ||
|
e90f1b4206 | ||
|
43f5572779 | ||
|
c60f6c039a | ||
|
8288413786 | ||
|
535c04be5c | ||
|
2335c3418c | ||
|
24d05d87ec |
@ -3,4 +3,4 @@ set -exuo pipefail
|
|||||||
|
|
||||||
job_dir=$(dirname "${BASH_SOURCE[0]}")
|
job_dir=$(dirname "${BASH_SOURCE[0]}")
|
||||||
|
|
||||||
codespell -I "${job_dir}"/dictionary.txt
|
codespell -I "${job_dir}"/dictionary.txt -S "*.bib,*.bst,*.cls,*.json,*.png,*.svg,*.wav,*.gz,*/templating/test?/**,**/auditor/*.sql,**/templating/mustach**,*.fees,*key,*.tag,*.info,*.latexmkrc,*.ecc,*.jpg,*.zkey,*.sqlite,*/contrib/hellos/**,*/vpn/tests/**,*.priv,*.file,*.tgz,*.woff,*.gif,*.odt,*.fee,*.deflate,*.dat,*.jpeg,*.eps,*.odg,*/m4/ax_lib_postgresql.m4,*/m4/libgcrypt.m4,*.rpath,config.status,ABOUT-NLS,*/doc/texinfo.tex,*.PNG,*.??.json,*.docx,*.ods,*.doc,*.docx,*.xcf,*.xlsx,*.ecc,*.ttf,*.woff2,*.eot,*.ttf,*.eot,*.mp4,*.pptx,*.epgz,*.min.js,**/*.map,**/fonts/**,*.pack.js,*.po,*.bbl,*/afl-tests/*,*/.git/**,*.pdf,*.epub,**/signing-key.asc,**/pnpm-lock.yaml,**/*.svg,**/*.cls,**/rfc.bib,**/*.bst,*/cbdc-es.tex,*/cbdc-it.tex,**/ExchangeSelection/example.ts,*/testcurl/test_tricky.c,*/i18n/strings.ts,*/src/anastasis-data.ts,**/doc/flows/main.de.tex"
|
||||||
|
@ -1,14 +1,23 @@
|
|||||||
import {
|
import {
|
||||||
bytesToString,
|
Amounts,
|
||||||
|
TalerSignaturePurpose,
|
||||||
|
amountToBuffer,
|
||||||
|
bufferForUint32,
|
||||||
|
buildSigPS,
|
||||||
createEddsaKeyPair,
|
createEddsaKeyPair,
|
||||||
decodeCrock,
|
decodeCrock,
|
||||||
decryptWithDerivedKey,
|
decryptWithDerivedKey,
|
||||||
eddsaGetPublic,
|
eddsaGetPublic,
|
||||||
|
eddsaSign,
|
||||||
encodeCrock,
|
encodeCrock,
|
||||||
encryptWithDerivedKey,
|
encryptWithDerivedKey,
|
||||||
getRandomBytesF,
|
getRandomBytesF,
|
||||||
|
hash,
|
||||||
|
hashTruncate32,
|
||||||
stringToBytes,
|
stringToBytes,
|
||||||
|
timestampRoundedToBuffer
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
|
import { AmlExchangeBackend } from "./types.js";
|
||||||
|
|
||||||
export interface Account {
|
export interface Account {
|
||||||
accountId: AccountId;
|
accountId: AccountId;
|
||||||
@ -45,6 +54,30 @@ export async function unlockAccount(
|
|||||||
return { accountId, signingKey };
|
return { accountId, signingKey };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildQuerySignature(key: SigningKey): string {
|
||||||
|
const sigBlob = buildSigPS(
|
||||||
|
TalerSignaturePurpose.TALER_SIGNATURE_AML_QUERY,
|
||||||
|
).build();
|
||||||
|
|
||||||
|
return encodeCrock(eddsaSign(sigBlob, key));
|
||||||
|
}
|
||||||
|
export function buildDecisionSignature(
|
||||||
|
key: SigningKey,
|
||||||
|
decision: AmlExchangeBackend.AmlDecision,
|
||||||
|
): string {
|
||||||
|
|
||||||
|
const sigBlob = buildSigPS(TalerSignaturePurpose.TALER_SIGNATURE_AML_DECISION)
|
||||||
|
.put(hash(stringToBytes(decision.justification)))
|
||||||
|
// .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();
|
||||||
|
|
||||||
|
return encodeCrock(eddsaSign(sigBlob, key));
|
||||||
|
}
|
||||||
|
|
||||||
declare const opaque_Account: unique symbol;
|
declare const opaque_Account: unique symbol;
|
||||||
export type LockedAccount = string & { [opaque_Account]: true };
|
export type LockedAccount = string & { [opaque_Account]: true };
|
||||||
|
|
||||||
|
@ -1,12 +1,7 @@
|
|||||||
import {
|
import { TranslatedString } from "@gnu-taler/taler-util";
|
||||||
AbsoluteTime,
|
|
||||||
AmountJson,
|
|
||||||
TranslatedString,
|
|
||||||
} from "@gnu-taler/taler-util";
|
|
||||||
import { FormState } from "../handlers/FormProvider.js";
|
import { FormState } from "../handlers/FormProvider.js";
|
||||||
import { FlexibleForm } from "./index.js";
|
|
||||||
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
||||||
import { AmlState } from "../types.js";
|
import { FlexibleForm } from "./index.js";
|
||||||
import { Simplest, resolutionSection } from "./simplest.js";
|
import { Simplest, resolutionSection } from "./simplest.js";
|
||||||
|
|
||||||
export const v1 = (current: State): FlexibleForm<Form902_11.Form> => ({
|
export const v1 = (current: State): FlexibleForm<Form902_11.Form> => ({
|
||||||
|
@ -1,12 +1,7 @@
|
|||||||
import {
|
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
|
||||||
AbsoluteTime,
|
|
||||||
AmountJson,
|
|
||||||
TranslatedString,
|
|
||||||
} from "@gnu-taler/taler-util";
|
|
||||||
import { FormState } from "../handlers/FormProvider.js";
|
import { FormState } from "../handlers/FormProvider.js";
|
||||||
import { FlexibleForm } from "./index.js";
|
|
||||||
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
||||||
import { AmlState } from "../types.js";
|
import { FlexibleForm } from "./index.js";
|
||||||
import { Simplest, resolutionSection } from "./simplest.js";
|
import { Simplest, resolutionSection } from "./simplest.js";
|
||||||
|
|
||||||
export const v1 = (current: State): FlexibleForm<Form902_12.Form> => ({
|
export const v1 = (current: State): FlexibleForm<Form902_12.Form> => ({
|
||||||
|
@ -1,12 +1,7 @@
|
|||||||
import {
|
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
|
||||||
AbsoluteTime,
|
|
||||||
AmountJson,
|
|
||||||
TranslatedString,
|
|
||||||
} from "@gnu-taler/taler-util";
|
|
||||||
import { FormState } from "../handlers/FormProvider.js";
|
import { FormState } from "../handlers/FormProvider.js";
|
||||||
import { FlexibleForm } from "./index.js";
|
|
||||||
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
||||||
import { AmlState } from "../types.js";
|
import { FlexibleForm } from "./index.js";
|
||||||
import { Simplest, resolutionSection } from "./simplest.js";
|
import { Simplest, resolutionSection } from "./simplest.js";
|
||||||
|
|
||||||
export const v1 = (current: State): FlexibleForm<Form902_13.Form> => ({
|
export const v1 = (current: State): FlexibleForm<Form902_13.Form> => ({
|
||||||
|
@ -1,12 +1,7 @@
|
|||||||
import {
|
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
|
||||||
AbsoluteTime,
|
|
||||||
AmountJson,
|
|
||||||
TranslatedString,
|
|
||||||
} from "@gnu-taler/taler-util";
|
|
||||||
import { FormState } from "../handlers/FormProvider.js";
|
import { FormState } from "../handlers/FormProvider.js";
|
||||||
import { FlexibleForm } from "./index.js";
|
|
||||||
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
||||||
import { AmlState } from "../types.js";
|
import { FlexibleForm } from "./index.js";
|
||||||
import { Simplest, resolutionSection } from "./simplest.js";
|
import { Simplest, resolutionSection } from "./simplest.js";
|
||||||
|
|
||||||
export const v1 = (current: State): FlexibleForm<Form902_15.Form> => ({
|
export const v1 = (current: State): FlexibleForm<Form902_15.Form> => ({
|
||||||
|
@ -1,14 +1,7 @@
|
|||||||
import {
|
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
|
||||||
AbsoluteTime,
|
|
||||||
AmountJson,
|
|
||||||
Amounts,
|
|
||||||
TranslatedString,
|
|
||||||
} from "@gnu-taler/taler-util";
|
|
||||||
import { FlexibleForm, languageList } from "./index.js";
|
|
||||||
import { FormState } from "../handlers/FormProvider.js";
|
import { FormState } from "../handlers/FormProvider.js";
|
||||||
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
||||||
import { AmlState } from "../types.js";
|
import { FlexibleForm, languageList } from "./index.js";
|
||||||
import { amlStateConverter } from "../pages/CaseDetails.js";
|
|
||||||
import { Simplest, resolutionSection } from "./simplest.js";
|
import { Simplest, resolutionSection } from "./simplest.js";
|
||||||
|
|
||||||
export const v1 = (current: State): FlexibleForm<Form902_1.Form> => ({
|
export const v1 = (current: State): FlexibleForm<Form902_1.Form> => ({
|
||||||
|
@ -1,17 +1,10 @@
|
|||||||
import {
|
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
|
||||||
AbsoluteTime,
|
|
||||||
AmountJson,
|
|
||||||
Amounts,
|
|
||||||
TranslatedString,
|
|
||||||
} from "@gnu-taler/taler-util";
|
|
||||||
import { FormState } from "../handlers/FormProvider.js";
|
|
||||||
import { FlexibleForm } from "./index.js";
|
|
||||||
import { ArrowRightIcon } from "@heroicons/react/24/outline";
|
import { ArrowRightIcon } from "@heroicons/react/24/outline";
|
||||||
import { h as create } from "preact";
|
|
||||||
import { ChevronRightIcon } from "@heroicons/react/24/solid";
|
import { ChevronRightIcon } from "@heroicons/react/24/solid";
|
||||||
|
import { h as create } from "preact";
|
||||||
|
import { FormState } from "../handlers/FormProvider.js";
|
||||||
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
||||||
import { AmlState } from "../types.js";
|
import { FlexibleForm } from "./index.js";
|
||||||
import { amlStateConverter } from "../pages/CaseDetails.js";
|
|
||||||
import { Simplest, resolutionSection } from "./simplest.js";
|
import { Simplest, resolutionSection } from "./simplest.js";
|
||||||
|
|
||||||
export const v1 = (current: State): FlexibleForm<Form902_4.Form> => ({
|
export const v1 = (current: State): FlexibleForm<Form902_4.Form> => ({
|
||||||
|
@ -1,12 +1,7 @@
|
|||||||
import {
|
import { TranslatedString } from "@gnu-taler/taler-util";
|
||||||
AbsoluteTime,
|
|
||||||
AmountJson,
|
|
||||||
TranslatedString,
|
|
||||||
} from "@gnu-taler/taler-util";
|
|
||||||
import { FormState } from "../handlers/FormProvider.js";
|
import { FormState } from "../handlers/FormProvider.js";
|
||||||
import { FlexibleForm, currencyList } from "./index.js";
|
|
||||||
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
||||||
import { AmlState } from "../types.js";
|
import { FlexibleForm, currencyList } from "./index.js";
|
||||||
import { Simplest, resolutionSection } from "./simplest.js";
|
import { Simplest, resolutionSection } from "./simplest.js";
|
||||||
|
|
||||||
export const v1 = (current: State): FlexibleForm<Form902_5.Form> => ({
|
export const v1 = (current: State): FlexibleForm<Form902_5.Form> => ({
|
||||||
|
@ -1,12 +1,7 @@
|
|||||||
import {
|
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
|
||||||
AbsoluteTime,
|
|
||||||
AmountJson,
|
|
||||||
TranslatedString,
|
|
||||||
} from "@gnu-taler/taler-util";
|
|
||||||
import { FormState } from "../handlers/FormProvider.js";
|
import { FormState } from "../handlers/FormProvider.js";
|
||||||
import { FlexibleForm } from "./index.js";
|
|
||||||
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
||||||
import { AmlState } from "../types.js";
|
import { FlexibleForm } from "./index.js";
|
||||||
import { Simplest, resolutionSection } from "./simplest.js";
|
import { Simplest, resolutionSection } from "./simplest.js";
|
||||||
|
|
||||||
export const v1 = (current: State): FlexibleForm<Form902_9.Form> => ({
|
export const v1 = (current: State): FlexibleForm<Form902_9.Form> => ({
|
||||||
|
@ -5,11 +5,11 @@ import {
|
|||||||
TranslatedString,
|
TranslatedString,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { FormState } from "../handlers/FormProvider.js";
|
import { FormState } from "../handlers/FormProvider.js";
|
||||||
import { FlexibleForm } from "./index.js";
|
import { DoubleColumnFormSection } from "../handlers/forms.js";
|
||||||
import { AmlState } from "../types.js";
|
|
||||||
import { amlStateConverter } from "../pages/CaseDetails.js";
|
|
||||||
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
||||||
import { DoubleColumnFormSection, UIFormField } from "../handlers/forms.js";
|
import { amlStateConverter } from "../pages/CaseDetails.js";
|
||||||
|
import { AmlExchangeBackend } from "../types.js";
|
||||||
|
import { FlexibleForm } from "./index.js";
|
||||||
|
|
||||||
export const v1 = (current: State): FlexibleForm<Simplest.Form> => ({
|
export const v1 = (current: State): FlexibleForm<Simplest.Form> => ({
|
||||||
versionId: "2023-05-25",
|
versionId: "2023-05-25",
|
||||||
@ -36,7 +36,7 @@ export const v1 = (current: State): FlexibleForm<Simplest.Form> => ({
|
|||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
threshold: {
|
threshold: {
|
||||||
disabled: v.state === AmlState.frozen,
|
disabled: v.state === AmlExchangeBackend.AmlState.frozen,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -46,7 +46,7 @@ export namespace Simplest {
|
|||||||
export interface WithResolution {
|
export interface WithResolution {
|
||||||
when: AbsoluteTime;
|
when: AbsoluteTime;
|
||||||
threshold: AmountJson;
|
threshold: AmountJson;
|
||||||
state: AmlState;
|
state: AmlExchangeBackend.AmlState;
|
||||||
}
|
}
|
||||||
export interface Form extends WithResolution {
|
export interface Form extends WithResolution {
|
||||||
comment: string;
|
comment: string;
|
||||||
@ -77,15 +77,15 @@ export function resolutionSection(current: State): DoubleColumnFormSection {
|
|||||||
converter: amlStateConverter,
|
converter: amlStateConverter,
|
||||||
choices: [
|
choices: [
|
||||||
{
|
{
|
||||||
value: AmlState.frozen,
|
value: AmlExchangeBackend.AmlState.frozen,
|
||||||
label: "Frozen" as TranslatedString,
|
label: "Frozen" as TranslatedString,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: AmlState.pending,
|
value: AmlExchangeBackend.AmlState.pending,
|
||||||
label: "Pending" as TranslatedString,
|
label: "Pending" as TranslatedString,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: AmlState.normal,
|
value: AmlExchangeBackend.AmlState.normal,
|
||||||
label: "Normal" as TranslatedString,
|
label: "Normal" as TranslatedString,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -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>;
|
||||||
};
|
};
|
||||||
|
83
packages/aml-backoffice-ui/src/hooks/useBackend.ts
Normal file
83
packages/aml-backoffice-ui/src/hooks/useBackend.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import {
|
||||||
|
HttpResponseOk,
|
||||||
|
RequestOptions,
|
||||||
|
useApiContext,
|
||||||
|
} from "@gnu-taler/web-util/browser";
|
||||||
|
import { useCallback } from "preact/hooks";
|
||||||
|
import { uiSettings } from "../settings.js";
|
||||||
|
import { canonicalizeBaseUrl } from "@gnu-taler/taler-util";
|
||||||
|
import { useOfficer } from "./useOfficer.js";
|
||||||
|
import { buildQuerySignature } from "../account.js";
|
||||||
|
|
||||||
|
interface useBackendType {
|
||||||
|
request: <T>(
|
||||||
|
path: string,
|
||||||
|
options?: RequestOptions,
|
||||||
|
) => Promise<HttpResponseOk<T>>;
|
||||||
|
fetcher: <T>(args: [string, string]) => Promise<HttpResponseOk<T>>;
|
||||||
|
paginatedFetcher: <T>(
|
||||||
|
args: [string, number, number, string],
|
||||||
|
) => Promise<HttpResponseOk<T>>;
|
||||||
|
}
|
||||||
|
export function usePublicBackend(): useBackendType {
|
||||||
|
const { request: requestHandler } = useApiContext();
|
||||||
|
|
||||||
|
const baseUrl = getInitialBackendBaseURL();
|
||||||
|
|
||||||
|
const request = useCallback(
|
||||||
|
function requestImpl<T>(
|
||||||
|
path: string,
|
||||||
|
options: RequestOptions = {},
|
||||||
|
): Promise<HttpResponseOk<T>> {
|
||||||
|
return requestHandler<T>(baseUrl, path, options);
|
||||||
|
},
|
||||||
|
[baseUrl],
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetcher = useCallback(
|
||||||
|
function fetcherImpl<T>([endpoint, talerAmlOfficerSignature]: [string,string]): Promise<HttpResponseOk<T>> {
|
||||||
|
return requestHandler<T>(baseUrl, endpoint, {
|
||||||
|
talerAmlOfficerSignature
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[baseUrl],
|
||||||
|
);
|
||||||
|
const paginatedFetcher = useCallback(
|
||||||
|
function fetcherImpl<T>([endpoint, page, size, talerAmlOfficerSignature]: [
|
||||||
|
string,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
]): Promise<HttpResponseOk<T>> {
|
||||||
|
return requestHandler<T>(baseUrl, endpoint, {
|
||||||
|
params: { page: page || 1, size },
|
||||||
|
talerAmlOfficerSignature,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[baseUrl],
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
request,
|
||||||
|
fetcher,
|
||||||
|
paginatedFetcher,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInitialBackendBaseURL(): string {
|
||||||
|
const overrideUrl =
|
||||||
|
typeof localStorage !== "undefined"
|
||||||
|
? localStorage.getItem("exchange-aml-base-url")
|
||||||
|
: undefined;
|
||||||
|
if (!overrideUrl) {
|
||||||
|
//normal path
|
||||||
|
if (!uiSettings.backendBaseURL) {
|
||||||
|
console.error(
|
||||||
|
"ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'",
|
||||||
|
);
|
||||||
|
return canonicalizeBaseUrl(window.origin);
|
||||||
|
}
|
||||||
|
return canonicalizeBaseUrl(uiSettings.backendBaseURL);
|
||||||
|
}
|
||||||
|
// testing/development path
|
||||||
|
return canonicalizeBaseUrl(overrideUrl);
|
||||||
|
}
|
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);
|
||||||
|
};
|
||||||
|
}
|
147
packages/aml-backoffice-ui/src/hooks/useCases.ts
Normal file
147
packages/aml-backoffice-ui/src/hooks/useCases.ts
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
import { AmlExchangeBackend } from "../types.js";
|
||||||
|
import {
|
||||||
|
HttpResponse,
|
||||||
|
HttpResponseOk,
|
||||||
|
HttpResponsePaginated,
|
||||||
|
RequestError,
|
||||||
|
} from "@gnu-taler/web-util/browser";
|
||||||
|
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
|
||||||
|
import _useSWR, { SWRHook } from "swr";
|
||||||
|
import { usePublicBackend } from "./useBackend.js";
|
||||||
|
import { AccountId, buildQuerySignature } from "../account.js";
|
||||||
|
import { useOfficer } from "./useOfficer.js";
|
||||||
|
const useSWR = _useSWR as unknown as SWRHook;
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10;
|
||||||
|
const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1;
|
||||||
|
/**
|
||||||
|
* FIXME: mutate result when balance change (transaction )
|
||||||
|
* @param account
|
||||||
|
* @param args
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function useCases(
|
||||||
|
account: AccountId,
|
||||||
|
state: AmlExchangeBackend.AmlState,
|
||||||
|
signature: string | undefined,
|
||||||
|
): HttpResponsePaginated<
|
||||||
|
AmlExchangeBackend.AmlRecords,
|
||||||
|
AmlExchangeBackend.AmlError
|
||||||
|
> {
|
||||||
|
const { paginatedFetcher } = usePublicBackend();
|
||||||
|
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: afterData,
|
||||||
|
error: afterError,
|
||||||
|
isValidating: loadingAfter,
|
||||||
|
} = useSWR<
|
||||||
|
HttpResponseOk<AmlExchangeBackend.AmlRecords>,
|
||||||
|
RequestError<AmlExchangeBackend.AmlError>
|
||||||
|
>(
|
||||||
|
[
|
||||||
|
`aml/${account}/decisions/${AmlExchangeBackend.AmlState[state]}`,
|
||||||
|
page,
|
||||||
|
PAGE_SIZE,
|
||||||
|
signature,
|
||||||
|
],
|
||||||
|
paginatedFetcher,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [lastAfter, setLastAfter] = useState<
|
||||||
|
HttpResponse<AmlExchangeBackend.AmlRecords, AmlExchangeBackend.AmlError>
|
||||||
|
>({ loading: true });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (afterData) setLastAfter(afterData);
|
||||||
|
}, [afterData]);
|
||||||
|
|
||||||
|
if (afterError) {
|
||||||
|
return afterError.cause;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the query returns less that we ask, then we have reach the end or beginning
|
||||||
|
const isReachingEnd =
|
||||||
|
afterData && afterData.data && afterData.data.records.length < PAGE_SIZE;
|
||||||
|
const isReachingStart = false;
|
||||||
|
|
||||||
|
const pagination = {
|
||||||
|
isReachingEnd,
|
||||||
|
isReachingStart,
|
||||||
|
loadMore: () => {
|
||||||
|
if (!afterData || isReachingEnd) return;
|
||||||
|
if (afterData.data && afterData.data.records.length < MAX_RESULT_SIZE) {
|
||||||
|
setPage(page + 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loadMorePrev: () => {
|
||||||
|
null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const records = !afterData
|
||||||
|
? []
|
||||||
|
: ((afterData ?? lastAfter).data ?? { records: [] }).records;
|
||||||
|
console.log("afterdata", afterData, lastAfter, records)
|
||||||
|
if (loadingAfter) return { loading: true, data: { records } };
|
||||||
|
if (afterData) {
|
||||||
|
return { ok: true, data: { records }, ...pagination };
|
||||||
|
}
|
||||||
|
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,
|
||||||
};
|
};
|
||||||
|
@ -11,8 +11,8 @@ import { v1 as form_902_9e_v1 } from "../forms/902_9e.js";
|
|||||||
import { v1 as simplest } from "../forms/simplest.js";
|
import { v1 as simplest } from "../forms/simplest.js";
|
||||||
import { DocumentDuplicateIcon } from "@heroicons/react/24/solid";
|
import { DocumentDuplicateIcon } from "@heroicons/react/24/solid";
|
||||||
import { AbsoluteTime } from "@gnu-taler/taler-util";
|
import { AbsoluteTime } from "@gnu-taler/taler-util";
|
||||||
import { AmlState } from "../types.js";
|
|
||||||
import { AmountJson, Amounts } from "@gnu-taler/taler-util";
|
import { AmountJson, Amounts } from "@gnu-taler/taler-util";
|
||||||
|
import { AmlExchangeBackend } from "../types.js";
|
||||||
|
|
||||||
export function AntiMoneyLaunderingForm({ number }: { number?: string }) {
|
export function AntiMoneyLaunderingForm({ number }: { number?: string }) {
|
||||||
const selectedForm = Number.parseInt(number ?? "0", 10);
|
const selectedForm = Number.parseInt(number ?? "0", 10);
|
||||||
@ -28,7 +28,7 @@ export function AntiMoneyLaunderingForm({ number }: { number?: string }) {
|
|||||||
<NiceForm
|
<NiceForm
|
||||||
initial={storedValue}
|
initial={storedValue}
|
||||||
form={showingFrom({
|
form={showingFrom({
|
||||||
state: AmlState.pending,
|
state: AmlExchangeBackend.AmlState.pending,
|
||||||
threshold: Amounts.parseOrThrow("USD:10"),
|
threshold: Amounts.parseOrThrow("USD:10"),
|
||||||
})}
|
})}
|
||||||
onUpdate={() => {}}
|
onUpdate={() => {}}
|
||||||
@ -37,7 +37,7 @@ export function AntiMoneyLaunderingForm({ number }: { number?: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
state: AmlState;
|
state: AmlExchangeBackend.AmlState;
|
||||||
threshold: AmountJson;
|
threshold: AmountJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,4 @@
|
|||||||
import { Fragment, VNode, h } from "preact";
|
import { Fragment, VNode, h } from "preact";
|
||||||
import {
|
|
||||||
AmlDecisionDetail,
|
|
||||||
AmlDecisionDetails,
|
|
||||||
AmlState,
|
|
||||||
KycDetail,
|
|
||||||
} from "../types.js";
|
|
||||||
import {
|
import {
|
||||||
AbsoluteTime,
|
AbsoluteTime,
|
||||||
AmountJson,
|
AmountJson,
|
||||||
@ -18,70 +12,20 @@ import { NiceForm } from "../NiceForm.js";
|
|||||||
import { FlexibleForm } from "../forms/index.js";
|
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 { 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: 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";
|
||||||
when: AbsoluteTime;
|
when: AbsoluteTime;
|
||||||
title: TranslatedString;
|
title: TranslatedString;
|
||||||
state: AmlState;
|
state: AmlExchangeBackend.AmlState;
|
||||||
threshold: AmountJson;
|
threshold: AmountJson;
|
||||||
};
|
};
|
||||||
type KycCollectionEvent = {
|
type KycCollectionEvent = {
|
||||||
@ -105,8 +49,8 @@ function selectSooner(a: WithTime, b: WithTime) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getEventsFromAmlHistory(
|
function getEventsFromAmlHistory(
|
||||||
aml: AmlDecisionDetail[],
|
aml: AmlExchangeBackend.AmlDecisionDetail[],
|
||||||
kyc: KycDetail[],
|
kyc: AmlExchangeBackend.KycDetail[],
|
||||||
): AmlEvent[] {
|
): AmlEvent[] {
|
||||||
const ae: AmlEvent[] = aml.map((a) => {
|
const ae: AmlEvent[] = aml.map((a) => {
|
||||||
return {
|
return {
|
||||||
@ -132,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),
|
||||||
});
|
});
|
||||||
@ -141,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
|
||||||
@ -187,7 +142,7 @@ export function CaseDetails({ account }: { account?: string }) {
|
|||||||
switch (e.type) {
|
switch (e.type) {
|
||||||
case "aml-form": {
|
case "aml-form": {
|
||||||
switch (e.state) {
|
switch (e.state) {
|
||||||
case AmlState.normal: {
|
case AmlExchangeBackend.AmlState.normal: {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<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">
|
<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">
|
||||||
@ -200,7 +155,7 @@ export function CaseDetails({ account }: { account?: string }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case AmlState.pending: {
|
case AmlExchangeBackend.AmlState.pending: {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<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">
|
<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">
|
||||||
@ -213,7 +168,7 @@ export function CaseDetails({ account }: { account?: string }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case AmlState.frozen: {
|
case AmlExchangeBackend.AmlState.frozen: {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<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">
|
<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">
|
||||||
@ -276,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: [
|
||||||
{
|
{
|
||||||
@ -304,15 +270,15 @@ function ShowConsolidated({
|
|||||||
choices: [
|
choices: [
|
||||||
{
|
{
|
||||||
label: "Frozen" as TranslatedString,
|
label: "Frozen" as TranslatedString,
|
||||||
value: AmlState.frozen,
|
value: AmlExchangeBackend.AmlState.frozen,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Pending" as TranslatedString,
|
label: "Pending" as TranslatedString,
|
||||||
value: AmlState.pending,
|
value: AmlExchangeBackend.AmlState.pending,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Normal" as TranslatedString,
|
label: "Normal" as TranslatedString,
|
||||||
value: AmlState.normal,
|
value: AmlExchangeBackend.AmlState.normal,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -361,8 +327,8 @@ function ShowConsolidated({
|
|||||||
|
|
||||||
interface Consolidated {
|
interface Consolidated {
|
||||||
aml: {
|
aml: {
|
||||||
state?: AmlState;
|
state: AmlExchangeBackend.AmlState;
|
||||||
threshold?: AmountJson;
|
threshold: AmountJson;
|
||||||
since: AbsoluteTime;
|
since: AbsoluteTime;
|
||||||
};
|
};
|
||||||
kyc: {
|
kyc: {
|
||||||
@ -380,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: {},
|
||||||
};
|
};
|
||||||
@ -396,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": {
|
||||||
@ -421,26 +395,26 @@ export const amlStateConverter = {
|
|||||||
fromStringUI: parseAmlState,
|
fromStringUI: parseAmlState,
|
||||||
};
|
};
|
||||||
|
|
||||||
function stringifyAmlState(s: AmlState | undefined): string {
|
function stringifyAmlState(s: AmlExchangeBackend.AmlState | undefined): string {
|
||||||
if (s === undefined) return "";
|
if (s === undefined) return "";
|
||||||
switch (s) {
|
switch (s) {
|
||||||
case AmlState.normal:
|
case AmlExchangeBackend.AmlState.normal:
|
||||||
return "normal";
|
return "normal";
|
||||||
case AmlState.pending:
|
case AmlExchangeBackend.AmlState.pending:
|
||||||
return "pending";
|
return "pending";
|
||||||
case AmlState.frozen:
|
case AmlExchangeBackend.AmlState.frozen:
|
||||||
return "frozen";
|
return "frozen";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseAmlState(s: string | undefined): AmlState {
|
function parseAmlState(s: string | undefined): AmlExchangeBackend.AmlState {
|
||||||
switch (s) {
|
switch (s) {
|
||||||
case "normal":
|
case "normal":
|
||||||
return AmlState.normal;
|
return AmlExchangeBackend.AmlState.normal;
|
||||||
case "pending":
|
case "pending":
|
||||||
return AmlState.pending;
|
return AmlExchangeBackend.AmlState.pending;
|
||||||
case "frozen":
|
case "frozen":
|
||||||
return AmlState.frozen;
|
return AmlExchangeBackend.AmlState.frozen;
|
||||||
default:
|
default:
|
||||||
throw Error(`unknown AML state: ${s}`);
|
throw Error(`unknown AML state: ${s}`);
|
||||||
}
|
}
|
||||||
|
@ -1,77 +1,40 @@
|
|||||||
import { VNode, h } from "preact";
|
|
||||||
import { Pages } from "../pages.js";
|
|
||||||
import { AmlRecords, AmlState } from "../types.js";
|
|
||||||
import { InputChoiceHorizontal } from "../handlers/InputChoiceHorizontal.js";
|
|
||||||
import { createNewForm } from "../handlers/forms.js";
|
|
||||||
import { TranslatedString } from "@gnu-taler/taler-util";
|
import { TranslatedString } from "@gnu-taler/taler-util";
|
||||||
import { amlStateConverter as amlStateConverter } from "./CaseDetails.js";
|
import { VNode, h } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
|
import { createNewForm } from "../handlers/forms.js";
|
||||||
|
import { useCases } from "../hooks/useCases.js";
|
||||||
import { useOfficer } from "../hooks/useOfficer.js";
|
import { useOfficer } from "../hooks/useOfficer.js";
|
||||||
|
import { Pages } from "../pages.js";
|
||||||
const response: AmlRecords = {
|
import { AmlExchangeBackend } from "../types.js";
|
||||||
records: [
|
import { amlStateConverter } from "./CaseDetails.js";
|
||||||
{
|
import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
|
||||||
current_state: 0,
|
import { buildQuerySignature } from "../account.js";
|
||||||
h_payto: "QWEQWEQWEQWEWQE",
|
import { handleNotOkResult } from "../utils/errors.js";
|
||||||
rowid: 1,
|
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
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: 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();
|
||||||
if (officer.state !== "ready") {
|
if (officer.state !== "ready") {
|
||||||
return <HandleAccountNotReady officer={officer} />;
|
return <HandleAccountNotReady officer={officer} />;
|
||||||
}
|
}
|
||||||
const form = createNewForm<{
|
const form = createNewForm<{
|
||||||
state: AmlState;
|
state: AmlExchangeBackend.AmlState;
|
||||||
}>();
|
}>();
|
||||||
const initial = { state: AmlState.pending };
|
|
||||||
const [list, setList] = useState(doFilter(response.records, initial.state));
|
const signature =
|
||||||
|
officer.state === "ready"
|
||||||
|
? buildQuerySignature(officer.account.signingKey)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const initial = AmlExchangeBackend.AmlState.pending;
|
||||||
|
const [stateFilter, setStateFilter] = useState(initial);
|
||||||
|
const list = useCases(officer.account.accountId, stateFilter, signature);
|
||||||
|
|
||||||
|
if (!list.ok && !list.loading) {
|
||||||
|
return handleNotOkResult(i18n)(list);
|
||||||
|
}
|
||||||
|
const records = list.loading ? [] : list.data.records
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div class="px-4 sm:px-6 lg:px-8">
|
<div class="px-4 sm:px-6 lg:px-8">
|
||||||
@ -85,9 +48,9 @@ export function Cases() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<form.Provider
|
<form.Provider
|
||||||
initialValue={initial}
|
initialValue={{ state: stateFilter }}
|
||||||
onUpdate={(v) => {
|
onUpdate={(v) => {
|
||||||
setList(doFilter(response.records, v.state));
|
setStateFilter(v.state ?? initial);
|
||||||
}}
|
}}
|
||||||
onSubmit={(v) => {}}
|
onSubmit={(v) => {}}
|
||||||
>
|
>
|
||||||
@ -98,15 +61,15 @@ export function Cases() {
|
|||||||
choices={[
|
choices={[
|
||||||
{
|
{
|
||||||
label: "Pending" as TranslatedString,
|
label: "Pending" as TranslatedString,
|
||||||
value: AmlState.pending,
|
value: AmlExchangeBackend.AmlState.pending,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Frozen" as TranslatedString,
|
label: "Frozen" as TranslatedString,
|
||||||
value: AmlState.frozen,
|
value: AmlExchangeBackend.AmlState.frozen,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Normal" as TranslatedString,
|
label: "Normal" as TranslatedString,
|
||||||
value: AmlState.normal,
|
value: AmlExchangeBackend.AmlState.normal,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@ -114,6 +77,9 @@ export function Cases() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-8 flow-root">
|
<div class="mt-8 flow-root">
|
||||||
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||||
|
{!records.length ? (
|
||||||
|
<div>empty result </div>
|
||||||
|
) : (
|
||||||
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||||
<Pagination />
|
<Pagination />
|
||||||
<table class="min-w-full divide-y divide-gray-300">
|
<table class="min-w-full divide-y divide-gray-300">
|
||||||
@ -140,7 +106,7 @@ export function Cases() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200 bg-white">
|
<tbody class="divide-y divide-gray-200 bg-white">
|
||||||
{list.map((r) => {
|
{records.map((r) => {
|
||||||
return (
|
return (
|
||||||
<tr class="hover:bg-gray-100 ">
|
<tr class="hover:bg-gray-100 ">
|
||||||
<td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 ">
|
<td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 ">
|
||||||
@ -154,23 +120,23 @@ export function Cases() {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500">
|
<td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500">
|
||||||
{((state: AmlState): VNode => {
|
{((state: AmlExchangeBackend.AmlState): VNode => {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case AmlState.normal: {
|
case AmlExchangeBackend.AmlState.normal: {
|
||||||
return (
|
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">
|
<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
|
Normal
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case AmlState.pending: {
|
case AmlExchangeBackend.AmlState.pending: {
|
||||||
return (
|
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">
|
<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
|
Pending
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case AmlState.frozen: {
|
case AmlExchangeBackend.AmlState.frozen: {
|
||||||
return (
|
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">
|
<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
|
Frozen
|
||||||
@ -190,6 +156,7 @@ export function Cases() {
|
|||||||
</table>
|
</table>
|
||||||
<Pagination />
|
<Pagination />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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 { AmlState } from "../types.js";
|
import { AbsoluteTime, Amounts, TalerProtocolTimestamp } from "@gnu-taler/taler-util";
|
||||||
import { AbsoluteTime, Amounts } from "@gnu-taler/taler-util";
|
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: 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">
|
||||||
|
@ -30,6 +30,9 @@ export function UnlockAccount({
|
|||||||
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
|
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
|
||||||
<div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
|
<div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
|
||||||
<Form.Provider
|
<Form.Provider
|
||||||
|
initialValue={{
|
||||||
|
password: "welcometo.5146",
|
||||||
|
}}
|
||||||
onSubmit={async (v) => {
|
onSubmit={async (v) => {
|
||||||
try {
|
try {
|
||||||
await onAccountUnlocked(v.password!);
|
await onAccountUnlocked(v.password!);
|
||||||
|
35
packages/aml-backoffice-ui/src/settings.ts
Normal file
35
packages/aml-backoffice-ui/src/settings.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2022 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface UiSettings {
|
||||||
|
backendBaseURL: string;
|
||||||
|
allowRegistrations: boolean;
|
||||||
|
uiName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global settings for the UI.
|
||||||
|
*/
|
||||||
|
const defaultSettings: UiSettings = {
|
||||||
|
backendBaseURL: "https://exchange.demo.taler.net/",
|
||||||
|
allowRegistrations: true,
|
||||||
|
uiName: "Taler Bank",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uiSettings: UiSettings =
|
||||||
|
"talerExchangeAmlSettings" in globalThis
|
||||||
|
? (globalThis as any).talerExchangeAmlSettings
|
||||||
|
: defaultSettings;
|
@ -1,4 +1,10 @@
|
|||||||
export interface AmlDecisionDetails {
|
export namespace AmlExchangeBackend {
|
||||||
|
// FIXME: placeholder
|
||||||
|
export interface AmlError {
|
||||||
|
code: number;
|
||||||
|
hint: string;
|
||||||
|
}
|
||||||
|
export interface AmlDecisionDetails {
|
||||||
// Array of AML decisions made for this account. Possibly
|
// Array of AML decisions made for this account. Possibly
|
||||||
// contains only the most recent decision if "history" was
|
// contains only the most recent decision if "history" was
|
||||||
// not set to 'true'.
|
// not set to 'true'.
|
||||||
@ -6,11 +12,11 @@ export interface AmlDecisionDetails {
|
|||||||
|
|
||||||
// Array of KYC attributes obtained for this account.
|
// Array of KYC attributes obtained for this account.
|
||||||
kyc_attributes: KycDetail[];
|
kyc_attributes: KycDetail[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type AmlOfficerPublicKeyP = string;
|
type AmlOfficerPublicKeyP = string;
|
||||||
|
|
||||||
export interface AmlDecisionDetail {
|
export interface AmlDecisionDetail {
|
||||||
// What was the justification given?
|
// What was the justification given?
|
||||||
justification: string;
|
justification: string;
|
||||||
|
|
||||||
@ -25,8 +31,8 @@ export interface AmlDecisionDetail {
|
|||||||
|
|
||||||
// Who made the decision?
|
// Who made the decision?
|
||||||
decider_pub: AmlOfficerPublicKeyP;
|
decider_pub: AmlOfficerPublicKeyP;
|
||||||
}
|
}
|
||||||
export interface KycDetail {
|
export interface KycDetail {
|
||||||
// Name of the configuration section that specifies the provider
|
// Name of the configuration section that specifies the provider
|
||||||
// which was used to collect the KYC details
|
// which was used to collect the KYC details
|
||||||
provider_section: string;
|
provider_section: string;
|
||||||
@ -41,25 +47,28 @@ export interface KycDetail {
|
|||||||
|
|
||||||
// Time when the validity of the KYC data will expire
|
// Time when the validity of the KYC data will expire
|
||||||
expiration_time: Timestamp;
|
expiration_time: Timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Timestamp {
|
interface Timestamp {
|
||||||
// Seconds since epoch, or the special
|
// Seconds since epoch, or the special
|
||||||
// value "never" to represent an event that will
|
// value "never" to represent an event that will
|
||||||
// never happen.
|
// never happen.
|
||||||
t_s: number | "never";
|
t_s: number | "never";
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
||||||
records: AmlRecord[];
|
records: AmlRecord[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AmlRecord {
|
interface AmlRecord {
|
||||||
// Which payto-address is this record about.
|
// Which payto-address is this record about.
|
||||||
// Identifies a GNU Taler wallet or an affected bank account.
|
// Identifies a GNU Taler wallet or an affected bank account.
|
||||||
h_payto: PaytoHash;
|
h_payto: PaytoHash;
|
||||||
@ -72,10 +81,44 @@ interface AmlRecord {
|
|||||||
|
|
||||||
// RowID of the record.
|
// RowID of the record.
|
||||||
rowid: Integer;
|
rowid: Integer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AmlState {
|
export enum AmlState {
|
||||||
normal = 0,
|
normal = 0,
|
||||||
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[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
43
packages/aml-backoffice-ui/src/utils/Loading.tsx
Normal file
43
packages/aml-backoffice-ui/src/utils/Loading.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2022 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { h, VNode } from "preact";
|
||||||
|
|
||||||
|
export function Loading(): VNode {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class="columns is-centered is-vcentered"
|
||||||
|
style={{
|
||||||
|
height: "calc(100% - 3rem)",
|
||||||
|
position: "absolute",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Spinner(): VNode {
|
||||||
|
return (
|
||||||
|
<div class="lds-ring">
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
54
packages/aml-backoffice-ui/src/utils/QR.tsx
Normal file
54
packages/aml-backoffice-ui/src/utils/QR.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2022 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { h, VNode } from "preact";
|
||||||
|
import { useEffect, useRef } from "preact/hooks";
|
||||||
|
// import qrcode from "qrcode-generator";
|
||||||
|
|
||||||
|
export function QR({ text }: { text: string }): VNode {
|
||||||
|
const divRef = useRef<HTMLDivElement>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
// const qr = qrcode(0, "L");
|
||||||
|
// qr.addData(text);
|
||||||
|
// qr.make();
|
||||||
|
// if (divRef.current)
|
||||||
|
// divRef.current.innerHTML = qr.createSvgTag({
|
||||||
|
// scalable: true,
|
||||||
|
// });
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "left",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "50%",
|
||||||
|
minWidth: 200,
|
||||||
|
maxWidth: 300,
|
||||||
|
marginRight: "auto",
|
||||||
|
marginLeft: "auto",
|
||||||
|
}}
|
||||||
|
ref={divRef}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
77
packages/aml-backoffice-ui/src/utils/errors.tsx
Normal file
77
packages/aml-backoffice-ui/src/utils/errors.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import {
|
||||||
|
ErrorType,
|
||||||
|
HttpResponse,
|
||||||
|
HttpResponsePaginated,
|
||||||
|
notifyError,
|
||||||
|
useTranslationContext,
|
||||||
|
} from "@gnu-taler/web-util/browser";
|
||||||
|
import { VNode, h } from "preact";
|
||||||
|
import { Loading } from "./Loading.js";
|
||||||
|
import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
|
||||||
|
import { AmlExchangeBackend } from "../types.js";
|
||||||
|
|
||||||
|
export function handleNotOkResult<Error extends AmlExchangeBackend.AmlError>(
|
||||||
|
i18n: ReturnType<typeof useTranslationContext>["i18n"],
|
||||||
|
): <T>(
|
||||||
|
result: HttpResponsePaginated<T, Error> | HttpResponse<T, Error>,
|
||||||
|
) => VNode {
|
||||||
|
return function handleNotOkResult2<T>(
|
||||||
|
result: HttpResponsePaginated<T, Error> | HttpResponse<T, Error>,
|
||||||
|
): VNode {
|
||||||
|
if (result.loading) return <Loading />;
|
||||||
|
if (!result.ok) {
|
||||||
|
switch (result.type) {
|
||||||
|
case ErrorType.TIMEOUT: {
|
||||||
|
notifyError(i18n.str`Request timeout, try again later.`, undefined);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ErrorType.CLIENT: {
|
||||||
|
if (result.status === HttpStatusCode.Unauthorized) {
|
||||||
|
notifyError(i18n.str`Wrong credentials`, undefined);
|
||||||
|
return <div> not authorized</div>;
|
||||||
|
}
|
||||||
|
const errorData = result.payload;
|
||||||
|
notifyError(
|
||||||
|
i18n.str`Could not load due to a client error`,
|
||||||
|
errorData.hint as TranslatedString,
|
||||||
|
JSON.stringify(result),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ErrorType.SERVER: {
|
||||||
|
notifyError(
|
||||||
|
i18n.str`Server returned with error`,
|
||||||
|
result.payload.hint as TranslatedString,
|
||||||
|
JSON.stringify(result.payload),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ErrorType.UNREADABLE: {
|
||||||
|
notifyError(
|
||||||
|
i18n.str`Unexpected error.`,
|
||||||
|
`Response from ${result.info?.url} is unreadable, http status: ${result.status}` as TranslatedString,
|
||||||
|
JSON.stringify(result),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ErrorType.UNEXPECTED: {
|
||||||
|
notifyError(
|
||||||
|
i18n.str`Unexpected error.`,
|
||||||
|
`Diagnostic from ${result.info?.url} is "${result.message}"` as TranslatedString,
|
||||||
|
JSON.stringify(result),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
assertUnreachable(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>error</div>;
|
||||||
|
}
|
||||||
|
return <div />;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export function assertUnreachable(x: never): never {
|
||||||
|
throw new Error("Didn't expect to get here");
|
||||||
|
}
|
@ -158,7 +158,6 @@ interface useBackendType {
|
|||||||
sandboxCashoutFetcher: <T>(endpoint: string[]) => Promise<HttpResponseOk<T>>;
|
sandboxCashoutFetcher: <T>(endpoint: string[]) => Promise<HttpResponseOk<T>>;
|
||||||
}
|
}
|
||||||
export function usePublicBackend(): useBackendType {
|
export function usePublicBackend(): useBackendType {
|
||||||
const { state } = useBackendContext();
|
|
||||||
const { request: requestHandler } = useApiContext();
|
const { request: requestHandler } = useApiContext();
|
||||||
|
|
||||||
const baseUrl = getInitialBackendBaseURL();
|
const baseUrl = getInitialBackendBaseURL();
|
||||||
|
@ -35,6 +35,8 @@ import { Logger } from "./logging.js";
|
|||||||
import { secretbox } from "./nacl-fast.js";
|
import { secretbox } from "./nacl-fast.js";
|
||||||
import * as fflate from "fflate";
|
import * as fflate from "fflate";
|
||||||
import { canonicalJson } from "./helpers.js";
|
import { canonicalJson } from "./helpers.js";
|
||||||
|
import { TalerProtocolDuration, TalerProtocolTimestamp } from "./time.js";
|
||||||
|
import { AmountLike, Amounts } from "./amounts.js";
|
||||||
|
|
||||||
export type Flavor<T, FlavorT extends string> = T & {
|
export type Flavor<T, FlavorT extends string> = T & {
|
||||||
_flavor?: `taler.${FlavorT}`;
|
_flavor?: `taler.${FlavorT}`;
|
||||||
@ -961,6 +963,9 @@ export enum TalerSignaturePurpose {
|
|||||||
WALLET_PURSE_DELETE = 1220,
|
WALLET_PURSE_DELETE = 1220,
|
||||||
EXCHANGE_CONFIRM_RECOUP = 1039,
|
EXCHANGE_CONFIRM_RECOUP = 1039,
|
||||||
EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
|
EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
|
||||||
|
TALER_SIGNATURE_AML_DECISION = 1350,
|
||||||
|
TALER_SIGNATURE_AML_QUERY = 1351,
|
||||||
|
TALER_SIGNATURE_MASTER_AML_KEY = 1017,
|
||||||
ANASTASIS_POLICY_UPLOAD = 1400,
|
ANASTASIS_POLICY_UPLOAD = 1400,
|
||||||
ANASTASIS_POLICY_DOWNLOAD = 1401,
|
ANASTASIS_POLICY_DOWNLOAD = 1401,
|
||||||
SYNC_BACKUP_UPLOAD = 1450,
|
SYNC_BACKUP_UPLOAD = 1450,
|
||||||
@ -1544,3 +1549,61 @@ export async function decryptContractForDeposit(
|
|||||||
contractTerms: JSON.parse(contractTermsString),
|
contractTerms: JSON.parse(contractTermsString),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function amountToBuffer(amount: AmountLike): Uint8Array {
|
||||||
|
const amountJ = Amounts.jsonifyAmount(amount);
|
||||||
|
const buffer = new ArrayBuffer(8 + 4 + 12);
|
||||||
|
const dvbuf = new DataView(buffer);
|
||||||
|
const u8buf = new Uint8Array(buffer);
|
||||||
|
const curr = stringToBytes(amountJ.currency);
|
||||||
|
if (typeof dvbuf.setBigUint64 !== "undefined") {
|
||||||
|
dvbuf.setBigUint64(0, BigInt(amountJ.value));
|
||||||
|
} else {
|
||||||
|
const arr = bigint(amountJ.value).toArray(2 ** 8).value;
|
||||||
|
let offset = 8 - arr.length;
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
dvbuf.setUint8(offset++, arr[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dvbuf.setUint32(8, amountJ.fraction);
|
||||||
|
u8buf.set(curr, 8 + 4);
|
||||||
|
|
||||||
|
return u8buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function timestampRoundedToBuffer(ts: TalerProtocolTimestamp): Uint8Array {
|
||||||
|
const b = new ArrayBuffer(8);
|
||||||
|
const v = new DataView(b);
|
||||||
|
// The buffer we sign over represents the timestamp in microseconds.
|
||||||
|
if (typeof v.setBigUint64 !== "undefined") {
|
||||||
|
const s = BigInt(ts.t_s) * BigInt(1000 * 1000);
|
||||||
|
v.setBigUint64(0, s);
|
||||||
|
} else {
|
||||||
|
const s =
|
||||||
|
ts.t_s === "never" ? bigint.zero : bigint(ts.t_s).multiply(1000 * 1000);
|
||||||
|
const arr = s.toArray(2 ** 8).value;
|
||||||
|
let offset = 8 - arr.length;
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
v.setUint8(offset++, arr[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Uint8Array(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function durationRoundedToBuffer(ts: TalerProtocolDuration): Uint8Array {
|
||||||
|
const b = new ArrayBuffer(8);
|
||||||
|
const v = new DataView(b);
|
||||||
|
// The buffer we sign over represents the timestamp in microseconds.
|
||||||
|
if (typeof v.setBigUint64 !== "undefined") {
|
||||||
|
const s = BigInt(ts.d_us);
|
||||||
|
v.setBigUint64(0, s);
|
||||||
|
} else {
|
||||||
|
const s = ts.d_us === "forever" ? bigint.zero : bigint(ts.d_us);
|
||||||
|
const arr = s.toArray(2 ** 8).value;
|
||||||
|
let offset = 8 - arr.length;
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
v.setUint8(offset++, arr[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Uint8Array(b);
|
||||||
|
}
|
||||||
|
@ -31,6 +31,7 @@ import {
|
|||||||
AmountLike,
|
AmountLike,
|
||||||
Amounts,
|
Amounts,
|
||||||
AmountString,
|
AmountString,
|
||||||
|
amountToBuffer,
|
||||||
BlindedDenominationSignature,
|
BlindedDenominationSignature,
|
||||||
bufferForUint32,
|
bufferForUint32,
|
||||||
bufferForUint64,
|
bufferForUint64,
|
||||||
@ -44,6 +45,7 @@ import {
|
|||||||
decryptContractForMerge,
|
decryptContractForMerge,
|
||||||
DenomKeyType,
|
DenomKeyType,
|
||||||
DepositInfo,
|
DepositInfo,
|
||||||
|
durationRoundedToBuffer,
|
||||||
ecdhGetPublic,
|
ecdhGetPublic,
|
||||||
eddsaGetPublic,
|
eddsaGetPublic,
|
||||||
EddsaPublicKeyString,
|
EddsaPublicKeyString,
|
||||||
@ -82,6 +84,7 @@ import {
|
|||||||
TalerProtocolDuration,
|
TalerProtocolDuration,
|
||||||
TalerProtocolTimestamp,
|
TalerProtocolTimestamp,
|
||||||
TalerSignaturePurpose,
|
TalerSignaturePurpose,
|
||||||
|
timestampRoundedToBuffer,
|
||||||
UnblindedSignature,
|
UnblindedSignature,
|
||||||
validateIban,
|
validateIban,
|
||||||
WireFee,
|
WireFee,
|
||||||
@ -1698,64 +1701,6 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function amountToBuffer(amount: AmountLike): Uint8Array {
|
|
||||||
const amountJ = Amounts.jsonifyAmount(amount);
|
|
||||||
const buffer = new ArrayBuffer(8 + 4 + 12);
|
|
||||||
const dvbuf = new DataView(buffer);
|
|
||||||
const u8buf = new Uint8Array(buffer);
|
|
||||||
const curr = stringToBytes(amountJ.currency);
|
|
||||||
if (typeof dvbuf.setBigUint64 !== "undefined") {
|
|
||||||
dvbuf.setBigUint64(0, BigInt(amountJ.value));
|
|
||||||
} else {
|
|
||||||
const arr = bigint(amountJ.value).toArray(2 ** 8).value;
|
|
||||||
let offset = 8 - arr.length;
|
|
||||||
for (let i = 0; i < arr.length; i++) {
|
|
||||||
dvbuf.setUint8(offset++, arr[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dvbuf.setUint32(8, amountJ.fraction);
|
|
||||||
u8buf.set(curr, 8 + 4);
|
|
||||||
|
|
||||||
return u8buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
function timestampRoundedToBuffer(ts: TalerProtocolTimestamp): Uint8Array {
|
|
||||||
const b = new ArrayBuffer(8);
|
|
||||||
const v = new DataView(b);
|
|
||||||
// The buffer we sign over represents the timestamp in microseconds.
|
|
||||||
if (typeof v.setBigUint64 !== "undefined") {
|
|
||||||
const s = BigInt(ts.t_s) * BigInt(1000 * 1000);
|
|
||||||
v.setBigUint64(0, s);
|
|
||||||
} else {
|
|
||||||
const s =
|
|
||||||
ts.t_s === "never" ? bigint.zero : bigint(ts.t_s).multiply(1000 * 1000);
|
|
||||||
const arr = s.toArray(2 ** 8).value;
|
|
||||||
let offset = 8 - arr.length;
|
|
||||||
for (let i = 0; i < arr.length; i++) {
|
|
||||||
v.setUint8(offset++, arr[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new Uint8Array(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
function durationRoundedToBuffer(ts: TalerProtocolDuration): Uint8Array {
|
|
||||||
const b = new ArrayBuffer(8);
|
|
||||||
const v = new DataView(b);
|
|
||||||
// The buffer we sign over represents the timestamp in microseconds.
|
|
||||||
if (typeof v.setBigUint64 !== "undefined") {
|
|
||||||
const s = BigInt(ts.d_us);
|
|
||||||
v.setBigUint64(0, s);
|
|
||||||
} else {
|
|
||||||
const s = ts.d_us === "forever" ? bigint.zero : bigint(ts.d_us);
|
|
||||||
const arr = s.toArray(2 ** 8).value;
|
|
||||||
let offset = 8 - arr.length;
|
|
||||||
for (let i = 0; i < arr.length; i++) {
|
|
||||||
v.setUint8(offset++, arr[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new Uint8Array(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EddsaSignRequest {
|
export interface EddsaSignRequest {
|
||||||
msg: string;
|
msg: string;
|
||||||
priv: string;
|
priv: string;
|
||||||
|
@ -699,6 +699,7 @@ function buildTransactionForManualWithdraw(
|
|||||||
withdrawalGroup.status === WithdrawalGroupStatus.Finished ||
|
withdrawalGroup.status === WithdrawalGroupStatus.Finished ||
|
||||||
withdrawalGroup.status === WithdrawalGroupStatus.PendingReady,
|
withdrawalGroup.status === WithdrawalGroupStatus.PendingReady,
|
||||||
},
|
},
|
||||||
|
kycUrl: withdrawalGroup.kycUrl,
|
||||||
exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl,
|
exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl,
|
||||||
timestamp: withdrawalGroup.timestampStart,
|
timestamp: withdrawalGroup.timestampStart,
|
||||||
transactionId: constructTransactionIdentifier({
|
transactionId: constructTransactionIdentifier({
|
||||||
|
@ -725,6 +725,9 @@ interface WithdrawalBatchResult {
|
|||||||
coinIdxs: number[];
|
coinIdxs: number[];
|
||||||
batchResp: ExchangeWithdrawBatchResponse;
|
batchResp: ExchangeWithdrawBatchResponse;
|
||||||
}
|
}
|
||||||
|
enum AmlStatus {
|
||||||
|
normal= 0, pending = 1, fronzen = 2,
|
||||||
|
}
|
||||||
|
|
||||||
async function handleKycRequired(
|
async function handleKycRequired(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
@ -757,6 +760,7 @@ async function handleKycRequired(
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
});
|
});
|
||||||
let kycUrl: string;
|
let kycUrl: string;
|
||||||
|
let amlStatus: AmlStatus | undefined;
|
||||||
if (
|
if (
|
||||||
kycStatusRes.status === HttpStatusCode.Ok ||
|
kycStatusRes.status === HttpStatusCode.Ok ||
|
||||||
//FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
|
//FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
|
||||||
@ -769,6 +773,10 @@ async function handleKycRequired(
|
|||||||
const kycStatus = await kycStatusRes.json();
|
const kycStatus = await kycStatusRes.json();
|
||||||
logger.info(`kyc status: ${j2s(kycStatus)}`);
|
logger.info(`kyc status: ${j2s(kycStatus)}`);
|
||||||
kycUrl = kycStatus.kyc_url;
|
kycUrl = kycStatus.kyc_url;
|
||||||
|
} else if (kycStatusRes.status === HttpStatusCode.UnavailableForLegalReasons) {
|
||||||
|
const kycStatus = await kycStatusRes.json();
|
||||||
|
logger.info(`aml status: ${j2s(kycStatus)}`);
|
||||||
|
amlStatus = kycStatus.aml_status;
|
||||||
} else {
|
} else {
|
||||||
throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
|
throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
|
||||||
}
|
}
|
||||||
@ -801,7 +809,11 @@ async function handleKycRequired(
|
|||||||
requirementRow: uuidResp.requirement_row,
|
requirementRow: uuidResp.requirement_row,
|
||||||
};
|
};
|
||||||
wg2.kycUrl = kycUrl;
|
wg2.kycUrl = kycUrl;
|
||||||
wg2.status = WithdrawalGroupStatus.PendingKyc;
|
wg2.status = amlStatus === AmlStatus.normal || amlStatus === undefined ? WithdrawalGroupStatus.PendingKyc :
|
||||||
|
amlStatus === AmlStatus.pending ? WithdrawalGroupStatus.PendingAml :
|
||||||
|
amlStatus === AmlStatus.fronzen ? WithdrawalGroupStatus.SuspendedAml :
|
||||||
|
assertUnreachable(amlStatus);
|
||||||
|
|
||||||
await tx.withdrawalGroups.put(wg2);
|
await tx.withdrawalGroups.put(wg2);
|
||||||
const newTxState = computeWithdrawalTransactionStatus(wg2);
|
const newTxState = computeWithdrawalTransactionStatus(wg2);
|
||||||
return {
|
return {
|
||||||
@ -1428,7 +1440,6 @@ async function processWithdrawalGroupPendingKyc(
|
|||||||
url.searchParams.set("timeout_ms", "30000");
|
url.searchParams.set("timeout_ms", "30000");
|
||||||
|
|
||||||
const retryTag = TaskIdentifiers.forWithdrawal(withdrawalGroup);
|
const retryTag = TaskIdentifiers.forWithdrawal(withdrawalGroup);
|
||||||
|
|
||||||
runLongpollAsync(ws, retryTag, async (cancellationToken) => {
|
runLongpollAsync(ws, retryTag, async (cancellationToken) => {
|
||||||
logger.info(`long-polling for withdrawal KYC status via ${url.href}`);
|
logger.info(`long-polling for withdrawal KYC status via ${url.href}`);
|
||||||
const kycStatusRes = await ws.http.fetch(url.href, {
|
const kycStatusRes = await ws.http.fetch(url.href, {
|
||||||
@ -1451,6 +1462,10 @@ async function processWithdrawalGroupPendingKyc(
|
|||||||
logger.info(`kyc status: ${j2s(kycStatus)}`);
|
logger.info(`kyc status: ${j2s(kycStatus)}`);
|
||||||
// FIXME: do we need to update the KYC url, or does it always stay constant?
|
// FIXME: do we need to update the KYC url, or does it always stay constant?
|
||||||
return { ready: false };
|
return { ready: false };
|
||||||
|
} else if (kycStatusRes.status === HttpStatusCode.UnavailableForLegalReasons) {
|
||||||
|
const kycStatus = await kycStatusRes.json();
|
||||||
|
logger.info(`aml status: ${j2s(kycStatus)}`);
|
||||||
|
return {ready : false}
|
||||||
} else {
|
} else {
|
||||||
throw Error(
|
throw Error(
|
||||||
`unexpected response from kyc-check (${kycStatusRes.status})`,
|
`unexpected response from kyc-check (${kycStatusRes.status})`,
|
||||||
|
@ -438,6 +438,10 @@ async function runTaskLoop(
|
|||||||
numDue++;
|
numDue++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`running task loop, iter=${iteration}, #tasks=${pending.pendingOperations.length} #lifeness=${numGivingLiveness}, #due=${numDue}`,
|
||||||
|
);
|
||||||
|
|
||||||
if (opts.stopWhenDone && numGivingLiveness === 0 && iteration !== 0) {
|
if (opts.stopWhenDone && numGivingLiveness === 0 && iteration !== 0) {
|
||||||
logger.warn(`stopping, as no pending operations have lifeness`);
|
logger.warn(`stopping, as no pending operations have lifeness`);
|
||||||
ws.isTaskLoopRunning = false;
|
ws.isTaskLoopRunning = false;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { TranslatedString } from "@gnu-taler/taler-util";
|
import { TranslatedString } from "@gnu-taler/taler-util";
|
||||||
import { StateUpdater, useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import { memoryMap } from "../index.browser.js";
|
import { memoryMap } from "../index.browser.js";
|
||||||
|
|
||||||
export type NotificationMessage = ErrorNotification | InfoNotification;
|
export type NotificationMessage = ErrorNotification | InfoNotification;
|
||||||
@ -15,7 +15,7 @@ interface InfoNotification {
|
|||||||
title: TranslatedString;
|
title: TranslatedString;
|
||||||
}
|
}
|
||||||
|
|
||||||
const storage = memoryMap<NotificationMessage[]>();
|
const storage = memoryMap<Map<string, NotificationMessage>>();
|
||||||
const NOTIFICATION_KEY = "notification";
|
const NOTIFICATION_KEY = "notification";
|
||||||
|
|
||||||
export function notifyError(
|
export function notifyError(
|
||||||
@ -23,20 +23,24 @@ export function notifyError(
|
|||||||
description: TranslatedString | undefined,
|
description: TranslatedString | undefined,
|
||||||
debug?: any,
|
debug?: any,
|
||||||
) {
|
) {
|
||||||
const currentState: NotificationMessage[] =
|
const currentState: Map<string, NotificationMessage> =
|
||||||
storage.get(NOTIFICATION_KEY) ?? [];
|
storage.get(NOTIFICATION_KEY) ?? new Map();
|
||||||
const newState = currentState.concat({
|
|
||||||
type: "error",
|
const notif = {
|
||||||
|
type: "error" as const,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
debug,
|
debug,
|
||||||
});
|
};
|
||||||
|
const newState = currentState.set(hash(notif), notif);
|
||||||
storage.set(NOTIFICATION_KEY, newState);
|
storage.set(NOTIFICATION_KEY, newState);
|
||||||
}
|
}
|
||||||
export function notifyInfo(title: TranslatedString) {
|
export function notifyInfo(title: TranslatedString) {
|
||||||
const currentState: NotificationMessage[] =
|
const currentState: Map<string, NotificationMessage> =
|
||||||
storage.get(NOTIFICATION_KEY) ?? [];
|
storage.get(NOTIFICATION_KEY) ?? new Map();
|
||||||
const newState = currentState.concat({ type: "info", title });
|
|
||||||
|
const notif = { type: "info" as const, title };
|
||||||
|
const newState = currentState.set(hash(notif), notif);
|
||||||
storage.set(NOTIFICATION_KEY, newState);
|
storage.set(NOTIFICATION_KEY, newState);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,22 +50,48 @@ type Notification = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function useNotifications(): Notification[] {
|
export function useNotifications(): Notification[] {
|
||||||
const [value, setter] = useState<NotificationMessage[]>([]);
|
const [value, setter] = useState<Map<string, NotificationMessage>>(new Map());
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return storage.onUpdate(NOTIFICATION_KEY, () => {
|
return storage.onUpdate(NOTIFICATION_KEY, () => {
|
||||||
const mem = storage.get(NOTIFICATION_KEY) ?? [];
|
const mem = storage.get(NOTIFICATION_KEY) ?? new Map();
|
||||||
setter(mem);
|
setter(mem);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return value.map((message, idx) => {
|
|
||||||
|
return Array.from(value.values()).map((message, idx) => {
|
||||||
return {
|
return {
|
||||||
message,
|
message,
|
||||||
remove: () => {
|
remove: () => {
|
||||||
const mem = storage.get(NOTIFICATION_KEY) ?? [];
|
const mem = storage.get(NOTIFICATION_KEY) ?? new Map();
|
||||||
const newState = Array.from(mem);
|
const newState = new Map(mem);
|
||||||
newState.splice(idx, 1);
|
newState.delete(hash(message));
|
||||||
storage.set(NOTIFICATION_KEY, newState);
|
storage.set(NOTIFICATION_KEY, newState);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hashCode(str: string): string {
|
||||||
|
if (str.length === 0) return "0";
|
||||||
|
let hash = 0;
|
||||||
|
let chr;
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
chr = str.charCodeAt(i);
|
||||||
|
hash = (hash << 5) - hash + chr;
|
||||||
|
hash |= 0; // Convert to 32bit integer
|
||||||
|
}
|
||||||
|
return hash.toString(16);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hash(msg: NotificationMessage): string {
|
||||||
|
let str = (msg.type + ":" + msg.title) as string;
|
||||||
|
if (msg.type === "error") {
|
||||||
|
if (msg.description) {
|
||||||
|
str += ":" + msg.description;
|
||||||
|
}
|
||||||
|
if (msg.debug) {
|
||||||
|
str += ":" + msg.debug;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hashCode(str);
|
||||||
|
}
|
||||||
|
@ -48,6 +48,11 @@ export async function defaultRequestHandler<T>(
|
|||||||
requestHeaders["Content-Type"] =
|
requestHeaders["Content-Type"] =
|
||||||
options.contentType === "json" ? "application/json" : "text/plain";
|
options.contentType === "json" ? "application/json" : "text/plain";
|
||||||
|
|
||||||
|
if (options.talerAmlOfficerSignature) {
|
||||||
|
requestHeaders["Taler-AML-Officer-Signature"] =
|
||||||
|
options.talerAmlOfficerSignature;
|
||||||
|
}
|
||||||
|
|
||||||
const requestMethod = options?.method ?? "GET";
|
const requestMethod = options?.method ?? "GET";
|
||||||
const requestBody = options?.data;
|
const requestBody = options?.data;
|
||||||
const requestTimeout = options?.timeout ?? 5 * 1000;
|
const requestTimeout = options?.timeout ?? 5 * 1000;
|
||||||
@ -269,6 +274,7 @@ export interface RequestOptions {
|
|||||||
params?: unknown;
|
params?: unknown;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
contentType?: "text" | "json";
|
contentType?: "text" | "json";
|
||||||
|
talerAmlOfficerSignature?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildRequestOk<T>(
|
async function buildRequestOk<T>(
|
||||||
|
Loading…
Reference in New Issue
Block a user