From 4359dee016ff1f0b5ba80e1cdec36e45e6b43032 Mon Sep 17 00:00:00 2001 From: Pedro Ribeiro <47680931+tubarao312@users.noreply.github.com> Date: Tue, 12 Mar 2024 03:52:06 +0000 Subject: [PATCH 1/3] Add categories to nodes --- src/api/labels/labels.ts | 55 ++++++++++++++++ src/api/model/getCategory200.ts | 11 ++++ src/api/model/getCategoryBody.ts | 11 ++++ src/api/model/getCategoryParams.ts | 14 +++++ src/api/model/index.ts | 3 + src/api/ward-analytics-api.yml | 32 ++++++++++ src/components/common/RiskIndicator.tsx | 10 +-- .../graph/analysis_window/header/Header.tsx | 25 +++++++- .../AddressNode/AddressNode/AddressNode.tsx | 39 ++++++++++-- src/utils/categories/classes.tsx | 63 +++++++++++++++++++ 10 files changed, 250 insertions(+), 13 deletions(-) create mode 100644 src/api/model/getCategory200.ts create mode 100644 src/api/model/getCategoryBody.ts create mode 100644 src/api/model/getCategoryParams.ts diff --git a/src/api/labels/labels.ts b/src/api/labels/labels.ts index 21d5b74c..779ad819 100644 --- a/src/api/labels/labels.ts +++ b/src/api/labels/labels.ts @@ -12,6 +12,12 @@ import type { MutationFunction, UseMutationOptions } from 'react-query' +import type { + GetCategory200 +} from '../model/getCategory200' +import type { + GetCategoryBody +} from '../model/getCategoryBody' import type { SearchLabels200 } from '../model/searchLabels200' @@ -71,4 +77,53 @@ export const getSearchLabelsMutationOptions = { + + + return instance( + {url: `/labels/get-category`, method: 'POST', + headers: {'Content-Type': 'application/json', }, + data: getCategoryBody + }, + ); + } + + + +export const getGetCategoryMutationOptions = (options?: { mutation?:UseMutationOptions>, TError,{data: GetCategoryBody}, TContext>, } +): UseMutationOptions>, TError,{data: GetCategoryBody}, TContext> => { + const {mutation: mutationOptions} = options ?? {}; + + + + + const mutationFn: MutationFunction>, {data: GetCategoryBody}> = (props) => { + const {data} = props ?? {}; + + return getCategory(data,) + } + + + + + return { mutationFn, ...mutationOptions }} + + export type GetCategoryMutationResult = NonNullable>> + export type GetCategoryMutationBody = GetCategoryBody + export type GetCategoryMutationError = unknown + + export const useGetCategory = (options?: { mutation?:UseMutationOptions>, TError,{data: GetCategoryBody}, TContext>, } +) => { + + const mutationOptions = getGetCategoryMutationOptions(options); + + return useMutation(mutationOptions); + } \ No newline at end of file diff --git a/src/api/model/getCategory200.ts b/src/api/model/getCategory200.ts new file mode 100644 index 00000000..42e7f426 --- /dev/null +++ b/src/api/model/getCategory200.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v6.23.0 🍺 + * Do not edit manually. + * compliance-queries-api + * The ward's compliance queires endpoints + * OpenAPI spec version: 1.0 + */ + +export type GetCategory200 = { + categories?: string[]; +}; diff --git a/src/api/model/getCategoryBody.ts b/src/api/model/getCategoryBody.ts new file mode 100644 index 00000000..1b382af1 --- /dev/null +++ b/src/api/model/getCategoryBody.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v6.23.0 🍺 + * Do not edit manually. + * compliance-queries-api + * The ward's compliance queires endpoints + * OpenAPI spec version: 1.0 + */ + +export type GetCategoryBody = { + labels?: string[]; +}; diff --git a/src/api/model/getCategoryParams.ts b/src/api/model/getCategoryParams.ts new file mode 100644 index 00000000..ce373034 --- /dev/null +++ b/src/api/model/getCategoryParams.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v6.23.0 🍺 + * Do not edit manually. + * compliance-queries-api + * The ward's compliance queires endpoints + * OpenAPI spec version: 1.0 + */ + +export type GetCategoryParams = { +/** + * The labels to get categories for + */ +labels: string[]; +}; diff --git a/src/api/model/index.ts b/src/api/model/index.ts index dff7fc6d..67b6bc93 100644 --- a/src/api/model/index.ts +++ b/src/api/model/index.ts @@ -15,6 +15,9 @@ export * from './category'; export * from './entity'; export * from './errorResponse'; export * from './exposure'; +export * from './getCategory200'; +export * from './getCategoryBody'; +export * from './getCategoryParams'; export * from './getCombinedTransactions200'; export * from './getCombinedTransactionsParams'; export * from './getCombinedTransactionsTransactionType'; diff --git a/src/api/ward-analytics-api.yml b/src/api/ward-analytics-api.yml index f65cc735..5410be49 100644 --- a/src/api/ward-analytics-api.yml +++ b/src/api/ward-analytics-api.yml @@ -160,6 +160,38 @@ paths: required: - labels + # Accepts a list of labels and returns a list of categories in {"categories": []} format + /labels/get-category: + post: + operationId: get-category + description: Get category for a list of labels + tags: + - labels + requestBody: + content: + application/json: + schema: + type: object + properties: + labels: + type: array + items: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + categories: + type: array + items: + type: string + required: + - categories + components: schemas: # Address Analysis diff --git a/src/components/common/RiskIndicator.tsx b/src/components/common/RiskIndicator.tsx index 49ea7f09..f15ff3b3 100644 --- a/src/components/common/RiskIndicator.tsx +++ b/src/components/common/RiskIndicator.tsx @@ -30,7 +30,7 @@ const RiskIndicator: FC = ({ return ( = ({
  • - Direct Exposure, both incoming and - outgoing + Direct Exposure, both incoming + and outgoing
  • - Indirect Exposure, both incoming and - outgoing + Indirect Exposure, both + incoming and outgoing
  • diff --git a/src/components/graph/analysis_window/header/Header.tsx b/src/components/graph/analysis_window/header/Header.tsx index 9f1321c9..00f6cc66 100644 --- a/src/components/graph/analysis_window/header/Header.tsx +++ b/src/components/graph/analysis_window/header/Header.tsx @@ -5,7 +5,7 @@ import { TrashIcon } from "@heroicons/react/24/outline"; import { XMarkIcon as XMarkSmallIcon } from "@heroicons/react/16/solid"; import clsx from "clsx"; -import { FC, useCallback, useContext } from "react"; +import { FC, useCallback, useContext, useEffect, useState } from "react"; import { addCustomAddressTag, @@ -33,6 +33,8 @@ import { import WithAuth, { WithAuthProps } from "../../../auth/WithAuth"; import useAuthState from "../../../../hooks/useAuthState"; import TagInput from "./TagInput"; +import { getCategory } from "../../../../api/labels/labels"; +import { CategoryClasses } from "../../../../utils/categories"; interface ModeButtonProps { isActive: boolean; @@ -112,6 +114,13 @@ const LabelsAndTags: FC = ({ // Labels get displayed first in the flex-wrap const labels = analysisData!.labels; + const [categories, setCategories] = useState([]); + + useEffect(() => { + getCategory({ labels }).then((res) => { + setCategories(res.categories!); + }); + }, [labels]); // Get user and address already existing tags from Firestore const { tags: userCustomTags } = useCustomUserTags(user ? user.uid : ""); @@ -141,13 +150,21 @@ const LabelsAndTags: FC = ({ // Display everything and user input at the end in a flex-wrap return ( + {categories.map((category) => ( + + ))} {labels.map((label) => ( - + ))} {addressCustomTags.map((tag) => ( { @@ -213,7 +230,9 @@ const Header: FC = ({ {/* Address Hash - Sliced when in non-expanded mode*/}

    + {analysisData?.labels.length && analysisData.labels[0] + " ("} {displayedAddress} + {analysisData?.labels.length && ")"} = ({ labels, tags }) => { + // Get all categories for this address + const [categories, setCategories] = useState([]); + + useEffect(() => { + getCategory({ labels }).then((res) => { + setCategories(res.categories!); + }); + }, [labels]); + // Display everything and user input at the end in a flex-wrap return ( + {categories.map((category) => ( + + ))} {labels.map((label) => ( - + ))} {tags.map((tag) => ( - + ))} ); @@ -262,15 +282,24 @@ const AddressNode: FC = ({

    - {`${address.slice(0, 5)}...${address.slice(-5)}`} + {analysisData?.labels && ( + <> + {analysisData.labels[0]} + + {analysisData.labels.length > 0 && "("} + {`${address.slice(0, 5)}...${address.slice(-5)}`} + {analysisData.labels.length > 0 && ")"} + + + )} {analysisData && analysisData.labels.length > 0 && ( )}

    diff --git a/src/utils/categories/classes.tsx b/src/utils/categories/classes.tsx index 2ff62fac..77e278de 100644 --- a/src/utils/categories/classes.tsx +++ b/src/utils/categories/classes.tsx @@ -21,157 +21,220 @@ import { BanknotesIcon, UserIcon, } from "@heroicons/react/24/solid"; + +// Also import all icons as small icons +import { + NoSymbolIcon as NoSymbolIconSmall, + CreditCardIcon as CreditCardIconSmall, + CpuChipIcon as CpuChipIconSmall, + LockClosedIcon as LockClosedIconSmall, + CurrencyDollarIcon as CurrencyDollarIconSmall, + EnvelopeIcon as EnvelopeIconSmall, + ExclamationCircleIcon as ExclamationCircleIconSmall, + ArrowPathRoundedSquareIcon as ArrowPathRoundedSquareIconSmall, + CubeIcon as CubeIconSmall, + ArrowsPointingInIcon as ArrowsPointingInIconSmall, + ArrowTopRightOnSquareIcon as ArrowTopRightOnSquareIconSmall, + BoltIcon as BoltIconSmall, + WalletIcon as WalletIconSmall, + BuildingLibraryIcon as BuildingLibraryIconSmall, + ShieldCheckIcon as ShieldCheckIconSmall, + PhotoIcon as PhotoIconSmall, + ShoppingCartIcon as ShoppingCartIconSmall, + CloudIcon as CloudIconSmall, + EyeDropperIcon as EyeDropperIconSmall, + BanknotesIcon as BanknotesIconSmall, + UserIcon as UserIconSmall, +} from "@heroicons/react/16/solid"; + import Category from "./enum"; type CategoryClass = { risk: number; icon: React.FC; + iconSmall: React.FC; }; const CategoryClasses: Record = { [Category.GOVERNMENT_CRIMINAL_BLACKLIST]: { risk: 10, icon: NoSymbolIcon, + iconSmall: NoSymbolIconSmall, }, [Category.DARKNET_MARKET]: { risk: 10, icon: NoSymbolIcon, + iconSmall: NoSymbolIconSmall, }, [Category.DARK_MARKET]: { risk: 10, icon: NoSymbolIcon, + iconSmall: NoSymbolIconSmall, }, [Category.DARKNET]: { risk: 10, icon: NoSymbolIcon, + iconSmall: NoSymbolIconSmall, }, [Category.PHISHING_HACKING]: { risk: 7.5, icon: CpuChipIcon, + iconSmall: CpuChipIconSmall, }, [Category.RANSOMWARE]: { risk: 7.5, icon: LockClosedIcon, + iconSmall: LockClosedIconSmall, }, [Category.MONEY_LAUNDERING]: { risk: 7.5, icon: CurrencyDollarIcon, + iconSmall: CurrencyDollarIconSmall, }, [Category.BLACKMAIL]: { risk: 7.5, icon: EnvelopeIcon, + iconSmall: EnvelopeIconSmall, }, [Category.SCAM]: { risk: 7.5, icon: ExclamationCircleIcon, + iconSmall: ExclamationCircleIconSmall, }, [Category.TUMBLER]: { risk: 7.5, icon: ArrowPathRoundedSquareIcon, + iconSmall: ArrowPathRoundedSquareIconSmall, }, [Category.MIXER]: { risk: 7.5, icon: ArrowPathRoundedSquareIcon, + iconSmall: ArrowPathRoundedSquareIconSmall, }, [Category.PARITY_BUG]: { risk: 7.5, icon: ExclamationCircleIcon, + iconSmall: ExclamationCircleIconSmall, }, [Category.PONZI_SCHEME]: { risk: 7.5, icon: CurrencyDollarIcon, + iconSmall: CurrencyDollarIconSmall, }, [Category.BLOCKED]: { risk: 5, icon: NoSymbolIcon, + iconSmall: NoSymbolIconSmall, }, [Category.GAMBLING]: { risk: 5, icon: CubeIcon, + iconSmall: CubeIconSmall, }, [Category.DEFI]: { risk: 5, icon: ArrowsPointingInIcon, + iconSmall: ArrowsPointingInIconSmall, }, [Category.DEX]: { risk: 5, icon: ArrowsPointingInIcon, + iconSmall: ArrowsPointingInIconSmall, }, [Category.BRIDGE]: { risk: 5, icon: ArrowTopRightOnSquareIcon, + iconSmall: ArrowTopRightOnSquareIconSmall, }, [Category.MEV_BOT]: { risk: 5, icon: BoltIcon, + iconSmall: BoltIconSmall, }, [Category.P2P_FINANCIAL_INFRASTRUCTURE_SERVICE]: { risk: 2.5, icon: ArrowsPointingInIcon, + iconSmall: ArrowsPointingInIconSmall, }, [Category.P2P_FINANCIAL_SERVICE]: { risk: 2.5, icon: ArrowsPointingInIcon, + iconSmall: ArrowsPointingInIconSmall, }, [Category.WALLET]: { risk: 2.5, icon: WalletIcon, + iconSmall: WalletIconSmall, }, [Category.CUSTODIAL_WALLET]: { risk: 1, icon: WalletIcon, + iconSmall: WalletIconSmall, }, [Category.CENTRALIZED_EXCHANGE]: { risk: 1, icon: BuildingLibraryIcon, + iconSmall: BuildingLibraryIconSmall, }, [Category.CYBER_SECURITY_SERVICE]: { risk: 1, icon: ShieldCheckIcon, + iconSmall: ShieldCheckIconSmall, }, [Category.EXCHANGE]: { risk: 1, icon: BuildingLibraryIcon, + iconSmall: BuildingLibraryIconSmall, }, [Category.NFT_MARKETPLACE]: { risk: 1, icon: PhotoIcon, + iconSmall: PhotoIconSmall, }, [Category.MARKETPLACE]: { risk: 1, icon: ShoppingCartIcon, + iconSmall: ShoppingCartIconSmall, }, [Category.CLOUD_MINING]: { risk: 1, icon: CloudIcon, + iconSmall: CloudIconSmall, }, [Category.FAUCET]: { risk: 1, icon: EyeDropperIcon, + iconSmall: EyeDropperIconSmall, }, [Category.PAYMENT_PROCESSOR]: { risk: 1, icon: CreditCardIcon, + iconSmall: CreditCardIconSmall, }, [Category.MINING_POOL]: { risk: 1, icon: CubeIcon, + iconSmall: CubeIconSmall, }, [Category.E_COMMERCE]: { risk: 1, icon: ShoppingCartIcon, + iconSmall: ShoppingCartIconSmall, }, [Category.FUND]: { risk: 1, icon: BanknotesIcon, + iconSmall: BanknotesIconSmall, }, [Category.FIAT_GATEWAY]: { risk: 1, icon: CurrencyDollarIcon, + iconSmall: CurrencyDollarIconSmall, }, [Category.OTHER]: { risk: 0, icon: UserIcon, + iconSmall: UserIconSmall, }, }; From 3c0e345348098ba598f06ba5f42dee4cabe28502 Mon Sep 17 00:00:00 2001 From: Pedro Ribeiro <47680931+tubarao312@users.noreply.github.com> Date: Tue, 12 Mar 2024 03:57:17 +0000 Subject: [PATCH 2/3] Update Header.tsx --- .../graph/analysis_window/header/Header.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/components/graph/analysis_window/header/Header.tsx b/src/components/graph/analysis_window/header/Header.tsx index 00f6cc66..86f3283b 100644 --- a/src/components/graph/analysis_window/header/Header.tsx +++ b/src/components/graph/analysis_window/header/Header.tsx @@ -216,7 +216,10 @@ const Header: FC = ({ const { analysisData, address } = useContext(AnalysisContext); // When minimized, the address hash should be sliced off - const displayedAddress = address.slice(0, 8) + "..." + address.slice(-6); + const displayedAddress = + address.slice(0, analysisData?.labels.length ? 4 : 8) + + ".." + + address.slice(-2); const risk = analysisData!.risk; return ( @@ -229,13 +232,17 @@ const Header: FC = ({
    {/* Address Hash - Sliced when in non-expanded mode*/} -

    - {analysisData?.labels.length && analysisData.labels[0] + " ("} - {displayedAddress} - {analysisData?.labels.length && ")"} +

    + {analysisData?.labels.length && analysisData.labels[0]} + + {analysisData?.labels.length && "("} + {displayedAddress} + {analysisData?.labels.length && ")"} + +

    From fd5b8df02c33601815fe2eb8eccc557cda11b159 Mon Sep 17 00:00:00 2001 From: Pedro Ribeiro <47680931+tubarao312@users.noreply.github.com> Date: Tue, 12 Mar 2024 04:10:30 +0000 Subject: [PATCH 3/3] Add labels to txn listing --- src/api/labels/labels.ts | 70 ++++++++++++++++++- src/api/model/getLabels200.ts | 12 ++++ src/api/model/getLabels200Data.ts | 12 ++++ src/api/model/index.ts | 2 + src/api/ward-analytics-api.yml | 28 ++++++++ .../content/transactions/Transactions.tsx | 24 +++++++ 6 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 src/api/model/getLabels200.ts create mode 100644 src/api/model/getLabels200Data.ts diff --git a/src/api/labels/labels.ts b/src/api/labels/labels.ts index 779ad819..a39aa2ed 100644 --- a/src/api/labels/labels.ts +++ b/src/api/labels/labels.ts @@ -6,11 +6,16 @@ * OpenAPI spec version: 1.0 */ import { - useMutation + useMutation, + useQuery } from 'react-query' import type { MutationFunction, - UseMutationOptions + QueryFunction, + QueryKey, + UseMutationOptions, + UseQueryOptions, + UseQueryResult } from 'react-query' import type { GetCategory200 @@ -18,6 +23,9 @@ import type { import type { GetCategoryBody } from '../model/getCategoryBody' +import type { + GetLabels200 +} from '../model/getLabels200' import type { SearchLabels200 } from '../model/searchLabels200' @@ -28,6 +36,64 @@ import { instance } from '.././instance'; +/** + * Get labels for an address + */ +export const getLabels = ( + address: string, + signal?: AbortSignal +) => { + + + return instance( + {url: `/addresses/${address}/labels`, method: 'GET', signal + }, + ); + } + + +export const getGetLabelsQueryKey = (address: string,) => { + return [`/addresses/${address}/labels`] as const; + } + + +export const getGetLabelsQueryOptions = >, TError = unknown>(address: string, options?: { query?:UseQueryOptions>, TError, TData>, } +) => { + +const {query: queryOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetLabelsQueryKey(address); + + + + const queryFn: QueryFunction>> = ({ signal }) => getLabels(address, signal); + + + + + + return { queryKey, queryFn, enabled: !!(address), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: QueryKey } +} + +export type GetLabelsQueryResult = NonNullable>> +export type GetLabelsQueryError = unknown + +export const useGetLabels = >, TError = unknown>( + address: string, options?: { query?:UseQueryOptions>, TError, TData>, } + + ): UseQueryResult & { queryKey: QueryKey } => { + + const queryOptions = getGetLabelsQueryOptions(address,options) + + const query = useQuery(queryOptions) as UseQueryResult & { queryKey: QueryKey }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + /** * Search labels and returns the labels that match the query limited by the set limit */ diff --git a/src/api/model/getLabels200.ts b/src/api/model/getLabels200.ts new file mode 100644 index 00000000..10cb6d0f --- /dev/null +++ b/src/api/model/getLabels200.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v6.23.0 🍺 + * Do not edit manually. + * compliance-queries-api + * The ward's compliance queires endpoints + * OpenAPI spec version: 1.0 + */ +import type { GetLabels200Data } from './getLabels200Data'; + +export type GetLabels200 = { + data?: GetLabels200Data; +}; diff --git a/src/api/model/getLabels200Data.ts b/src/api/model/getLabels200Data.ts new file mode 100644 index 00000000..ea87db84 --- /dev/null +++ b/src/api/model/getLabels200Data.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v6.23.0 🍺 + * Do not edit manually. + * compliance-queries-api + * The ward's compliance queires endpoints + * OpenAPI spec version: 1.0 + */ +import type { Label } from './label'; + +export type GetLabels200Data = { + labels?: Label[]; +}; diff --git a/src/api/model/index.ts b/src/api/model/index.ts index 67b6bc93..edf6248e 100644 --- a/src/api/model/index.ts +++ b/src/api/model/index.ts @@ -21,6 +21,8 @@ export * from './getCategoryParams'; export * from './getCombinedTransactions200'; export * from './getCombinedTransactionsParams'; export * from './getCombinedTransactionsTransactionType'; +export * from './getLabels200'; +export * from './getLabels200Data'; export * from './getTokenMetadata200'; export * from './getTokenMetadataBody'; export * from './getTransactions200'; diff --git a/src/api/ward-analytics-api.yml b/src/api/ward-analytics-api.yml index 5410be49..d6a7d274 100644 --- a/src/api/ward-analytics-api.yml +++ b/src/api/ward-analytics-api.yml @@ -129,6 +129,34 @@ paths: type: integer page_size: type: integer + /addresses/{address}/labels: + get: + operationId: get-labels + description: Get labels for an address + tags: + - labels + parameters: + - name: address + in: path + description: The address to get labels for + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + data: + type: object + properties: + labels: + type: array + items: + $ref: "#/components/schemas/Label" /labels/search-labels: post: operationId: search-labels diff --git a/src/components/graph/analysis_window/content/transactions/Transactions.tsx b/src/components/graph/analysis_window/content/transactions/Transactions.tsx index 1d9f51be..220a7ea5 100644 --- a/src/components/graph/analysis_window/content/transactions/Transactions.tsx +++ b/src/components/graph/analysis_window/content/transactions/Transactions.tsx @@ -5,6 +5,7 @@ import { BarsArrowDownIcon, CheckIcon, PlusIcon, + TagIcon, } from "@heroicons/react/16/solid"; import { FC, useContext, useEffect, useState } from "react"; import { Output, Transaction } from "../../../../../api/model"; @@ -21,6 +22,7 @@ import { import { GraphContext } from "../../../Graph"; import TokenLogo from "../../../../common/TokenLogo"; +import { useGetLabels } from "../../../../../api/labels/labels"; interface TransactionRowProps { usdValue: number; @@ -50,6 +52,27 @@ const TransactionRow: FC = ({ day: "numeric", }); + const [label, setLabel] = useState(); + const { refetch } = useGetLabels(addresses[0], { + query: { + enabled: false, + refetchOnWindowFocus: false, + retry: true, + cacheTime: 1000, // 1 second + staleTime: 1000, // 1 second + onSuccess: (data) => { + const l = data.data!.labels!; + if (l.length > 0) { + setLabel(l[0].label); + } + }, + }, + }); + + useEffect(() => { + refetch(); + }, [addresses]); + const addressText: string = addresses.length === 1 ? addresses[0].slice(0, 8) + "..." + addresses[0].slice(-8) @@ -76,6 +99,7 @@ const TransactionRow: FC = ({ Icon={incoming ? ArrowDownLeftIcon : ArrowUpRightIcon} text={incoming ? "IN" : "OUT"} /> + {label && }