Skip to content

Commit

Permalink
feat: SSR rendering for arena data
Browse files Browse the repository at this point in the history
  • Loading branch information
k0beLeenders committed Dec 11, 2024
1 parent f623732 commit 901f1ba
Show file tree
Hide file tree
Showing 11 changed files with 496 additions and 146 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,9 @@ const MobileNavbar = () => {
const { isIOS, isPWA } = useOs();

const activeLink = React.useMemo(() => {
const fullLink = mobileLinks.findIndex((link) => link.href === router.pathname);
if (fullLink > -1) return `link${fullLink}`;

const firstSegment = router.pathname.split("/")[1];
const firstSegmentLink = mobileLinks.findIndex((link) => link.href.includes(firstSegment));
if (firstSegmentLink) return `link${firstSegmentLink}`;

return "linknone";
const pathname = router.pathname;
const index = mobileLinks.findIndex((link) => link.href === pathname);
return index >= 0 ? `link${index}` : "linknone";
}, [router.pathname]);

if (isActionBoxInputFocussed) return null;
Expand Down
33 changes: 25 additions & 8 deletions apps/marginfi-v2-trading/src/context/TradeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,38 @@ export const TradePovider: React.FC<{
}> = ({ children }) => {
const router = useRouter();
const debounceId = React.useRef<NodeJS.Timeout | null>(null);
const { wallet, isOverride, connected } = useWallet();
const { wallet, connected } = useWallet();
const { connection } = useConnection();

const [fetchExtendedArenaGroups, fetchArenaGroups, initialized] = useTradeStoreV2((state) => [
state.fetchExtendedArenaGroups,
state.fetchArenaGroups,
state.initialized,
]);
const [fetchExtendedArenaGroups, fetchArenaGroups, setHydrationComplete, initialized, hydrationComplete] =
useTradeStoreV2((state) => [
state.fetchExtendedArenaGroups,
state.fetchArenaGroups,
state.setHydrationComplete,
state.initialized,
state.hydrationComplete,
]);

const [fetchPriorityFee] = useUiStore((state) => [state.fetchPriorityFee]);
const [isLoggedIn, setIsLoggedIn] = React.useState(false);

React.useEffect(() => {
fetchArenaGroups();
}, [fetchArenaGroups]);
const hydrate = async () => {
if (!hydrationComplete) {
await fetchArenaGroups();
setHydrationComplete();
}
};

hydrate();
}, [fetchArenaGroups, hydrationComplete, setHydrationComplete]);

React.useEffect(() => {
if (!initialized) {
console.log("fetching arena groups");
fetchArenaGroups();
}
}, [fetchArenaGroups, initialized]);

React.useEffect(() => {
if (initialized) {
Expand Down
17 changes: 11 additions & 6 deletions apps/marginfi-v2-trading/src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,19 @@ export default function MrgnApp({ Component, pageProps, path, bank }: AppProps &
const [broadcastType, priorityFees] = useUiStore((state) => [state.broadcastType, state.priorityFees]);

React.useEffect(() => {
const init = async () => {
const rpcEndpoint = await generateEndpoint(config.rpcEndpoint, process.env.NEXT_PUBLIC_RPC_PROXY_KEY ?? "");
setRpcEndpoint(rpcEndpoint);
setReady(true);
initAnalytics();
const initializeApp = () => {
try {
const endpoint = generateEndpoint(config.rpcEndpoint, process.env.NEXT_PUBLIC_RPC_PROXY_KEY ?? "");

setRpcEndpoint(endpoint);
setReady(true);
initAnalytics();
} catch (error) {
console.error("Failed to initialize:", error);
}
};

init();
initializeApp();
}, []);

return (
Expand Down
171 changes: 171 additions & 0 deletions apps/marginfi-v2-trading/src/pages/api/birdeye/arenaTokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { BankMetadata, loadBankMetadatas } from "@mrgnlabs/mrgn-common";
import { NextApiRequest, NextApiResponse } from "next";
import { BANK_METADATA_MAP } from "~/config/trade";

import type { TokenData } from "~/types";

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
let bankMetadataCache: {
[address: string]: BankMetadata;
} = {};

try {
bankMetadataCache = await loadBankMetadatas(BANK_METADATA_MAP);
} catch (error) {
console.error("Error:", error);
res.status(500).json({ error: "Error fetching bank metadata" });
return;
}

const allTokens = [...new Set(Object.values(bankMetadataCache).map((bank) => bank.tokenAddress))];

const batchSize = 50;
const tokens = Array.from({ length: Math.ceil(allTokens.length / batchSize) }, (_, i) =>
allTokens.slice(i * batchSize, (i + 1) * batchSize)
);

// Fetch token volume
let tokenVolumePromisses: Promise<Response>[] = [];
let tokenMetadataPromisses: Promise<Response>[] = [];

tokens.forEach((tokenBatch) => {
const addresses = tokenBatch.join(",");

const bodyData = {
list_address: addresses,
// type: "24h",
};

tokenVolumePromisses.push(
fetch(`https://public-api.birdeye.so/defi/price_volume/multi`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"x-chain": "solana",
"X-Api-Key": process.env.BIRDEYE_API_KEY || "",
},
body: JSON.stringify(bodyData),
})
);

//A list of addresses in string separated by commas (,)

tokenMetadataPromisses.push(
fetch(`https://public-api.birdeye.so/defi/v3/token/meta-data/multiple?list_address=${addresses}`, {
method: "GET",
headers: {
Accept: "application/json",
"x-chain": "solana",
"X-Api-Key": process.env.BIRDEYE_API_KEY || "",
},
})
);
});

// Fetch from API and update cache
try {
const volumeResponses = await Promise.all(tokenVolumePromisses);
const metadataResponses = await Promise.all(tokenMetadataPromisses);

if (!volumeResponses.every((response) => response.ok) || !metadataResponses.every((response) => response.ok)) {
throw new Error("Network response was not ok");
}

const volumeDataRaw = (await Promise.all(
volumeResponses.map((response) => response.json())
)) as TokenVolumeDataRaw[];
const metadataDataRaw = (await Promise.all(
metadataResponses.map((response) => response.json())
)) as TokenMetaDataRaw[];

if (!volumeDataRaw || !metadataDataRaw) {
res.status(404).json({ error: "Token data not found" });
return;
}

const volumeData = {
data: volumeDataRaw.reduce(
(acc, curr) => ({
...acc,
...curr.data,
}),
{}
),
}.data as TokenVolumeData;

const metadataData = {
data: metadataDataRaw.reduce(
(acc, curr) => ({
...acc,
...curr.data,
}),
{}
),
}.data as TokenMetaData;

const metadataDataList = Object.values(metadataData);

const tokenDatas: TokenData[] = metadataDataList.map((data) => {
const volume = volumeData[data.address];

return {
address: data.address,
name: data.name,
symbol: data.symbol,
imageUrl: data.logo_uri,
decimals: data.decimals,
price: volume.price,
priceChange24h: volume.priceChangePercent,
volume24h: volume.volumeUSD,
volumeChange24h: volume.volumeChangePercent,
volume4h: 0,
volumeChange4h: 0,
marketcap: 0,
};
});

// 5 min cache
res.setHeader("Cache-Control", "s-maxage=300, stale-while-revalidate=59");
res.status(200).json(tokenDatas);
} catch (error) {
console.error("Error:", error);
res.status(500).json({ error: "Error fetching data" });
}
}

type TokenVolumeData = {
[address: string]: {
price: number;
updateUnixTime: number;
updateHumanTime: string;
volumeUSD: number;
volumeChangePercent: number;
priceChangePercent: number;
};
};

type TokenVolumeDataRaw = {
data: TokenVolumeData;
};

type TokenMetaData = {
[address: string]: {
address: string;
symbol: string;
name: string;
decimals: number;
extensions: {
coingecko_id: string;
website: string;
twitter: string;
discord: string;
medium: string;
};
logo_uri: string;
};
};

type TokenMetaDataRaw = {
data: TokenMetaData;
};
37 changes: 30 additions & 7 deletions apps/marginfi-v2-trading/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";

import { useRouter } from "next/router";
import { GetStaticProps } from "next";

import { IconSortAscending, IconSortDescending, IconSparkles, IconGridDots, IconList } from "@tabler/icons-react";
import { motion } from "framer-motion";
Expand All @@ -19,6 +20,12 @@ import { Button } from "~/components/ui/button";
import { Loader } from "~/components/common/Loader";
import { ToggleGroup, ToggleGroupItem } from "~/components/ui/toggle-group";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select";
import {
fetchInitialArenaState,
getArenaStaticProps,
InitialArenaState,
StaticArenaProps,
} from "~/utils/trade-store.utils";

const sortOptions: {
value: TradePoolFilterStates;
Expand All @@ -37,22 +44,38 @@ enum View {
LIST = "list",
}

export default function HomePage() {
export const getStaticProps: GetStaticProps<StaticArenaProps> = async (context) => {
return getArenaStaticProps(context);
};

export default function HomePage({ initialData }: StaticArenaProps) {
const router = useRouter();
const isMobile = useIsMobile();

const [initialized, arenaPoolsSummary, sortBy, setSortBy] = useTradeStoreV2((state) => [
state.initialized,
state.arenaPoolsSummary,
state.sortBy,
state.setSortBy,
]);
const [initialized, arenaPoolsSummary, sortBy, setSortBy, fetchArenaGroups, setHydrationComplete] = useTradeStoreV2(
(state) => [
state.initialized,
state.arenaPoolsSummary,
state.sortBy,
state.setSortBy,
state.fetchArenaGroups,
state.setHydrationComplete,
]
);

const [isActionComplete, previousTxn, setIsActionComplete] = useActionBoxStore((state) => [
state.isActionComplete,
state.previousTxn,
state.setIsActionComplete,
]);

React.useEffect(() => {
if (initialData) {
fetchArenaGroups(initialData);
setHydrationComplete();
}
}, [initialData, fetchArenaGroups, setHydrationComplete]);

const [view, setView] = React.useState<View>(View.GRID);

const dir = React.useMemo(() => {
Expand Down
21 changes: 19 additions & 2 deletions apps/marginfi-v2-trading/src/pages/portfolio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,33 @@ import { Loader } from "~/components/common/Loader";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { useExtendedPools } from "~/hooks/useExtendedPools";
import { GroupStatus } from "~/store/tradeStoreV2";
import { GetStaticProps } from "next";
import { StaticArenaProps, getArenaStaticProps } from "~/utils";

export default function PortfolioPage() {
const [initialized] = useTradeStoreV2((state) => [state.initialized]);
export const getStaticProps: GetStaticProps<StaticArenaProps> = async (context) => {
return getArenaStaticProps(context);
};

export default function PortfolioPage({ initialData }: StaticArenaProps) {
const [initialized, fetchArenaGroups, setHydrationComplete] = useTradeStoreV2((state) => [
state.initialized,
state.fetchArenaGroups,
state.setHydrationComplete,
]);
const extendedPools = useExtendedPools();
const [isActionComplete, previousTxn, setIsActionComplete] = useActionBoxStore((state) => [
state.isActionComplete,
state.previousTxn,
state.setIsActionComplete,
]);

React.useEffect(() => {
if (initialData) {
fetchArenaGroups(initialData);
setHydrationComplete();
}
}, [initialData, fetchArenaGroups, setHydrationComplete]);

const shortPositions = React.useMemo(
() => extendedPools.filter((pool) => pool.status === GroupStatus.SHORT),
[extendedPools]
Expand Down
Loading

0 comments on commit 901f1ba

Please sign in to comment.