Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions packages/frontend/src/api/services/exploits/exploitsEndpoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import queryString from "query-string";
import { Logger } from "src/api/utils/logging";
import CONFIG from "../../../config/config";
import { alphadayApi } from "../alphadayApi";
import {
TGetExploitsRequest,
TGetExploitsRawResponse,
TGetExploitsResponse,
} from "./types";

const { EXPLOITS } = CONFIG.API.DEFAULT.ROUTES;

const parseAmount = (amount: string | null): number | null => {
if (amount === null) return null;
const parsed = parseFloat(amount);
return Number.isNaN(parsed) ? null : parsed;
};

const exploitsApi = alphadayApi.injectEndpoints({
endpoints: (builder) => ({
getExploits: builder.query<TGetExploitsResponse, TGetExploitsRequest>({
query: (req) => {
const params: string = queryString.stringify({
...req,
limit: req.limit ?? 30,
});
const path = `${String(EXPLOITS.BASE)}${String(
EXPLOITS.DEFAULT
)}?${params}`;
Logger.debug("getExploits: querying", path);
return path;
},
transformResponse: (
r: TGetExploitsRawResponse,
_meta,
_arg
): TGetExploitsResponse => {
Logger.debug("[EXPLOITS] transformResponse: raw response =", r);
Logger.debug(
"[EXPLOITS] transformResponse: typeof r =",
typeof r,
"| keys =",
r && typeof r === "object" ? Object.keys(r) : "n/a",
"| results isArray =",
Array.isArray(r?.results),
"| results length =",
r?.results?.length
);
try {
const facadeData = r.results
.filter(
(item, index, self) =>
self.findIndex(
(innerItem) => innerItem.id === item.id
) === index
)
.map((data) => ({
id: data.id,
hash: data.hash,
title: data.title,
url: data.url,
image: data.image,
shortDescription: data.short_description,
description: data.description,
protocol: data.protocol,
amountUsd: parseAmount(data.amount_usd),
chain: data.chain,
attackType: data.attack_type?.name ?? null,
date: data.date,
source: data.source,
tags: data.tags ?? [],
}));
Logger.debug(
"[EXPLOITS] transformResponse: mapped facade length =",
facadeData.length,
"| first item =",
facadeData[0]
);
return {
...r,
results: facadeData,
};
} catch (error) {
Logger.error(
"[EXPLOITS] transformResponse: FAILED to parse response — returning empty list. error =",
error,
"| raw response was =",
r
);
return {
...r,
results: [],
};
}
},
}),
}),
overrideExisting: false,
});

export const { useGetExploitsQuery } = exploitsApi;
69 changes: 69 additions & 0 deletions packages/frontend/src/api/services/exploits/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { TExploit } from "src/api/types";
import { TPagination } from "../baseTypes";

/**
* Primitive types
*/

export type TRemoteExploitSource = {
name: string;
slug: string;
icon: string | null;
};

export type TRemoteExploitTag = {
id: number;
name: string;
slug: string;
};

export type TRemoteExploitAttackType = {
id: number;
name: string;
slug: string;
description: string | null;
};

export type TRemoteExploit = {
id: number;
hash: string;
title: string;
url: string;
image: string | null;
short_description: string | null;
description: string | null;
protocol: string | null;
// amount lost, expressed in millions of USD
amount_usd: string | null;
chain: string | null;
attack_type: TRemoteExploitAttackType | null;
date: string;
source: TRemoteExploitSource | null;
tags: TRemoteExploitTag[];
};

/**
* Queries
*/

export type TGetExploitsRequest = {
page?: number | undefined;
limit?: number | undefined;
attack_type?: string | undefined;
chain?: string | undefined;
date?: string | undefined;
min_amount?: number | undefined;
period?: string | undefined;
protocol?: string | undefined;
sort_by?: string | undefined;
sources?: string | undefined;
tags?: string | undefined;
};

export type TGetExploitsRawResponse = TPagination & {
results: TRemoteExploit[];
};

export type TGetExploitsResponse = TPagination & {
results: TExploit[];
};
3 changes: 3 additions & 0 deletions packages/frontend/src/api/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export * from "./yields/types";
export * from "./fees/feesEndpoints";
export * from "./fees/types";

export * from "./exploits/exploitsEndpoints";
export * from "./exploits/types";

export * from "./developerActivity/developerActivityEndpoints";
export * from "./developerActivity/types";

Expand Down
41 changes: 41 additions & 0 deletions packages/frontend/src/api/types/exploits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export type TExploitSource = {
name: string;
slug: string;
icon: string | null;
};

export type TExploitTag = {
id: number;
name: string;
slug: string;
};

export type TExploit = {
id: number;
hash: string;
title: string;
url: string;
image: string | null;
shortDescription: string | null;
description: string | null;
protocol: string | null;
// amount lost, expressed in millions of USD (null when unknown)
amountUsd: number | null;
chain: string | null;
attackType: string | null;
date: string;
source: TExploitSource | null;
tags: TExploitTag[];
};

export enum EExploitPeriod {
Last24 = "LAST_24",
LastWeek = "LAST_WEEK",
LastMonth = "LAST_MONTH",
LastQuarter = "LAST_QUARTER",
}

export enum EExploitSortBy {
Date = "-date",
Amount = "-amount_usd",
}
1 change: 1 addition & 0 deletions packages/frontend/src/api/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from "./dao";
export * from "./developerActivity";
export * from "./discord";
export * from "./event";
export * from "./exploits";
export * from "./features";
export * from "./fees";
export * from "./forum";
Expand Down
107 changes: 107 additions & 0 deletions packages/frontend/src/components/exploits/ExploitItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { FC, useState, KeyboardEvent } from "react";
import moment from "moment-with-locales-es6";
import { TExploit } from "src/api/types";
import { formatNumber, ENumberStyle } from "src/api/utils/format";
import ChevronSVG from "src/assets/icons/chevron-down2.svg?react";

interface IExploitItemProps {
exploit: TExploit;
}

const formatAmount = (amountUsd: number | null): string => {
if (amountUsd === null) return "—";
// amount_usd is expressed in millions of USD
return formatNumber({
value: amountUsd * 1_000_000,
style: ENumberStyle.Currency,
currency: "USD",
}).value;
};

export const ExploitItem: FC<IExploitItemProps> = ({ exploit }) => {
const [isExpanded, setIsExpanded] = useState(false);
const description = exploit.description ?? exploit.shortDescription;

const toggle = () => setIsExpanded((prev) => !prev);

const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
toggle();
}
};

return (
<div
role="button"
tabIndex={0}
title="Open/close details"
aria-expanded={isExpanded}
onClick={toggle}
onKeyDown={handleKeyDown}
className="flex flex-col w-full py-3 px-2 cursor-pointer hover:bg-backgroundVariant200 transition-colors"
>
<div className="flex flex-row w-full">
<div className="flex flex-col min-w-0 flex-1 pr-2">
<span className="text-primary fontGroup-highlightSemi truncate capitalize">
{exploit.protocol ?? exploit.title}
</span>
<span
className={`fontGroup-normal text-primaryVariant100 mt-0.5 ${
isExpanded ? "" : "truncate"
}`}
>
{exploit.title}
</span>
<div className="flex flex-row items-center gap-1.5 mt-1.5 flex-wrap">
{exploit.chain && (
<span className="fontGroup-supportBold tracking-wide text-primaryVariant100 px-1.5 py-0.5 rounded bg-backgroundVariant200">
{exploit.chain}
</span>
)}
{exploit.attackType && (
<span className="fontGroup-supportBold tracking-wide text-primaryVariant100 px-1.5 py-0.5 rounded bg-backgroundVariant200">
{exploit.attackType}
</span>
)}
<span className="fontGroup-support tabular-nums text-primaryVariant100">
{moment(exploit.date).format("MMM D, YYYY")}
</span>
</div>
</div>
<div className="flex flex-row items-center shrink-0 gap-2">
<div className="flex flex-col items-end justify-center">
<span className="text-secondaryOrange fontGroup-highlightSemi tabular-nums whitespace-nowrap">
{formatAmount(exploit.amountUsd)}
</span>
<span className="fontGroup-supportBold tracking-wide text-primaryVariant100">
lost
</span>
</div>
<ChevronSVG
className={`w-3.5 h-3.5 text-primaryVariant100 transition-transform duration-200 ${
isExpanded ? "rotate-90" : ""
}`}
/>
</div>
</div>

{isExpanded && description && (
<div className="mt-2.5 pt-2.5 border-t border-borderLine">
<p className="fontGroup-normal mb-0 whitespace-pre-line max-w-[68ch]">
{description}
</p>
<a
href={exploit.url}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="inline-block mt-2 fontGroup-highlightSemi text-secondaryOrange hover:underline"
>
Read more →
</a>
</div>
)}
</div>
);
};
Loading
Loading