diff --git a/package.json b/package.json index d99fda8..2a6e4f3 100644 --- a/package.json +++ b/package.json @@ -13,12 +13,14 @@ "dependencies": { "@headlessui/react": "^1.7.18", "next": "14.1.0", + "numeral": "^2.0.6", "react": "^18", "react-dom": "^18", "tailwind-merge": "^2.2.1" }, "devDependencies": { "@types/node": "^20", + "@types/numeral": "^2.0.5", "@types/react": "^18", "@types/react-dom": "^18", "autoprefixer": "^10.0.1", diff --git a/src/app/(routes)/_components/hero-section.tsx b/src/app/(routes)/_components/hero-section.tsx index c263804..9efba42 100644 --- a/src/app/(routes)/_components/hero-section.tsx +++ b/src/app/(routes)/_components/hero-section.tsx @@ -9,7 +9,7 @@ import { INFORMATION_LINKS } from "../../_constants"; export function HeroSection() { return ( -
+
Across protocol diagram
diff --git a/src/app/(routes)/_components/stats-section.tsx b/src/app/(routes)/_components/stats-section.tsx new file mode 100644 index 0000000..96a23ae --- /dev/null +++ b/src/app/(routes)/_components/stats-section.tsx @@ -0,0 +1,73 @@ +import { Text } from "../../_components/text"; +import { StatBox } from "../../_components/stat-box"; + +import { getProtocolStats } from "../../_lib/scraper"; +import { humanReadableNumber } from "../../_lib/format"; + +async function getFormattedStatsData() { + const protocolStats = await getProtocolStats({ + revalidate: 24 * 60 * 60, // Update once a day + }); + + return { + totalVolumeUsd: `$${humanReadableNumber(protocolStats.totalVolumeUsd)}`, + totalDeposits: `${humanReadableNumber(protocolStats.totalDeposits)}`, + avgFillTimeInMinutes: `${protocolStats.avgFillTimeInMinutes < 1 ? "<" : ""} ${Math.max( + protocolStats.avgFillTimeInMinutes, + 1, + )}m`, + bridgeFee: "<$1", + }; +} + +export async function StatsSection() { + const formattedStatsData = await getFormattedStatsData(); + return ( +
+
+ + power in originality + + + Production ready
+ Empirically Proven +
+ + Across is the only cross-chain intents protocol in production today, enabling + the fastest and lowest-cost interoperability solution without security + tradeoffs. + +
+
+ + + + +
+
+ ); +} diff --git a/src/app/(routes)/_components/technology-section.tsx b/src/app/(routes)/_components/technology-section.tsx index fee3b49..78741e4 100644 --- a/src/app/(routes)/_components/technology-section.tsx +++ b/src/app/(routes)/_components/technology-section.tsx @@ -12,12 +12,21 @@ const sections = [ { Icon: FeatherIcon, title: "Elegant Abstraction", - body: "Intents replace explicit execution steps with implicit user outcomes, relying on a competitive network of market makers to fulfill outcomes. Cross-chain intents are a cross-chain limit order plus an action to execute.", + body: "Across connects users and applications via intents, not blockchains to other blockchains via complex or trusted message passing. Developers only need to attach a standard order to protocol actions to create seamless cross-chain experiences.", }, { Icon: BlocksDiagonalIcon, title: "Modular Interoperability", - body: "Intents replace explicit execution steps with implicit user outcomes, relying on a competitive network of market makers to fulfill outcomes. Cross-chain intents are a cross-chain limit order plus an action to execute.", + body: ( + <> + Cross-chain intents are powered by a modular system of 3 layers: +
    +
  1. Request for quote mechanism
  2. +
  3. Network of competitive market makers
  4. +
  5. Settlement layer to escrow user input funds, verify, and repay relayers
  6. +
+ + ), }, ]; diff --git a/src/app/(routes)/page.tsx b/src/app/(routes)/page.tsx index 13690f6..b86808c 100644 --- a/src/app/(routes)/page.tsx +++ b/src/app/(routes)/page.tsx @@ -2,6 +2,7 @@ import { Metadata } from "next"; import { HeroSection } from "./_components/hero-section"; import { TechnologySection } from "./_components/technology-section"; +import { StatsSection } from "./_components/stats-section"; export const metadata: Metadata = { title: "Home | Across Protocol", @@ -13,6 +14,7 @@ export default function Home() {
+
); } diff --git a/src/app/_components/stat-box.tsx b/src/app/_components/stat-box.tsx new file mode 100644 index 0000000..bf47500 --- /dev/null +++ b/src/app/_components/stat-box.tsx @@ -0,0 +1,45 @@ +import { ComponentProps } from "react"; +import { twMerge } from "tailwind-merge"; + +import { Text } from "./text"; + +type Props = ComponentProps<"div"> & { + title: string; + titleClassName: string; + value: string; + dividerClassName?: string; +}; + +export function StatBox({ + className, + title, + titleClassName, + value, + dividerClassName, + ...props +}: Props) { + return ( +
+
+
+ + {title} + + + {value} + +
+
+ ); +} diff --git a/src/app/_lib/format.ts b/src/app/_lib/format.ts new file mode 100644 index 0000000..cb56e6f --- /dev/null +++ b/src/app/_lib/format.ts @@ -0,0 +1,15 @@ +import numeral from "numeral"; + +/** + * Formats a number into a human readable format + * @param num The number to format + * @returns A human readable format. I.e. 1000 -> 1K, 1001 -> 1K+ + */ +export function humanReadableNumber(num: number, decimals = 0): string { + if (num <= 0) return "0"; + return ( + numeral(num) + .format(decimals <= 0 ? "0a" : `0.${"0".repeat(decimals)}a`) + .toUpperCase() + "+" + ); +} diff --git a/src/app/_lib/scraper.ts b/src/app/_lib/scraper.ts new file mode 100644 index 0000000..3e03930 --- /dev/null +++ b/src/app/_lib/scraper.ts @@ -0,0 +1,31 @@ +type ProtocolStatsResponse = { + totalDeposits: number; + avgFillTime: number; + totalVolumeUsd: number; +}; + +type ProtocolStatsFormatted = ProtocolStatsResponse & { + avgFillTimeInMinutes: number; +}; + +export async function getProtocolStats( + nextFetchRequestConfig?: NextFetchRequestConfig, +): Promise { + const response = await fetch(`https://public.api.across.to/deposits/stats`, { + next: nextFetchRequestConfig, + }); + if (!response.ok) { + throw new Error( + `Failed to fetch protocol stats: ${response.status} ${response.statusText}`, + ); + } + const data = await response.json(); + return formatResult(data); +} + +function formatResult(data: ProtocolStatsResponse) { + return { + ...data, + avgFillTimeInMinutes: Math.floor(data.avgFillTime / 60), + }; +} diff --git a/yarn.lock b/yarn.lock index 327a6c0..2a6bf97 100644 --- a/yarn.lock +++ b/yarn.lock @@ -241,6 +241,11 @@ dependencies: undici-types "~5.26.4" +"@types/numeral@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/numeral/-/numeral-2.0.5.tgz#388e5c4ff4b0e1787f130753cbbe83d3ba770858" + integrity sha512-kH8I7OSSwQu9DS9JYdFWbuvhVzvFRoCPCkGxNwoGgaPeDfEPJlcxNvEOypZhQ3XXHsGbfIuYcxcJxKUfJHnRfw== + "@types/prop-types@*": version "15.7.11" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563" @@ -1806,6 +1811,11 @@ normalize-range@^0.1.2: resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== +numeral@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/numeral/-/numeral-2.0.6.tgz#4ad080936d443c2561aed9f2197efffe25f4e506" + integrity sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA== + object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"