diff --git a/apps/marginfi-v2-ui/src/components/common/Stake/stake.tsx b/apps/marginfi-v2-ui/src/components/common/Stake/stake.tsx index 8c9233601f..db88ef7322 100644 --- a/apps/marginfi-v2-ui/src/components/common/Stake/stake.tsx +++ b/apps/marginfi-v2-ui/src/components/common/Stake/stake.tsx @@ -46,7 +46,7 @@ const Stake = () => { React.useEffect(() => { const fetchIntegrations = async () => { try { - const res = await fetch(`/api/birdeye/markets?token=` + LST_MINT); + const res = await fetch(`/api/tokens/markets?token=` + LST_MINT); if (!res.ok) { return; } diff --git a/apps/marginfi-v2-ui/src/pages/api/birdeye/index.ts b/apps/marginfi-v2-ui/src/pages/api/birdeye/index.ts deleted file mode 100644 index a10dd0b427..0000000000 --- a/apps/marginfi-v2-ui/src/pages/api/birdeye/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { NextApiRequest, NextApiResponse } from "next"; - -const BIRDEYE_API = "https://public-api.birdeye.so"; - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const { mintList } = req.query; - if (!mintList) { - res.status(400).json({ error: "No mintList provided" }); - return; - } - - // use abort controller to restrict fetch to 10 seconds - const controller = new AbortController(); - const timeoutId = setTimeout(() => { - controller.abort(); - }, 5000); - - // Fetch from API and update cache - try { - const response = await fetch(`${BIRDEYE_API}/defi/multi_price?list_address=${mintList}`, { - headers: { - Accept: "application/json", - "X-Api-Key": process.env.BIRDEYE_API_KEY || "", - }, - signal: controller.signal, - }); - - clearTimeout(timeoutId); - - if (!response.ok) { - throw new Error("Network response was not ok"); - } - const data = await response.json(); - - // cache for 20 minutes - res.setHeader("Cache-Control", "s-maxage=1200, stale-while-revalidate=300"); - res.status(200).json(data); - } catch (error) { - console.error("Error:", error); - res.status(500).json({ error: "Error fetching data" }); - } -} diff --git a/apps/marginfi-v2-ui/src/pages/api/birdeye/markets.ts b/apps/marginfi-v2-ui/src/pages/api/tokens/markets.ts similarity index 100% rename from apps/marginfi-v2-ui/src/pages/api/birdeye/markets.ts rename to apps/marginfi-v2-ui/src/pages/api/tokens/markets.ts diff --git a/apps/marginfi-v2-ui/src/pages/api/tokens/multi.ts b/apps/marginfi-v2-ui/src/pages/api/tokens/multi.ts new file mode 100644 index 0000000000..0efb73f327 --- /dev/null +++ b/apps/marginfi-v2-ui/src/pages/api/tokens/multi.ts @@ -0,0 +1,108 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { + BankMetadata, + loadBankMetadatas, + chunkedGetRawMultipleAccountInfoOrdered, + Wallet, +} from "@mrgnlabs/mrgn-common"; +import { Connection, PublicKey } from "@solana/web3.js"; +import { Program, AnchorProvider } from "@coral-xyz/anchor"; +import { Bank, BankRaw, MARGINFI_IDL, MarginfiIdlType, MarginfiProgram } from "@mrgnlabs/marginfi-client-v2"; +import config from "~/config/marginfi"; + +const BIRDEYE_API = "https://public-api.birdeye.so"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { mintList } = req.query; + if (!mintList) { + res.status(400).json({ error: "No mintList provided" }); + return; + } + + // use abort controller to restrict fetch to 10 seconds + const controller = new AbortController(); + const timeoutId = setTimeout(() => { + controller.abort(); + }, 5000); + + let bankMetadataCache: { + [address: string]: BankMetadata; + } = {}; + + try { + // load bank metadata + bankMetadataCache = await loadBankMetadatas(); + + // fetch mfi banks to get emissions mints + const connection = new Connection(process.env.PRIVATE_RPC_ENDPOINT_OVERRIDE || ""); + const idl = { ...MARGINFI_IDL, address: config.mfiConfig.programId.toBase58() } as unknown as MarginfiIdlType; + + const provider = new AnchorProvider(connection, {} as Wallet, { + ...AnchorProvider.defaultOptions(), + commitment: connection.commitment ?? AnchorProvider.defaultOptions().commitment, + }); + const program = new Program(idl, provider) as any as MarginfiProgram; + + const bankAddresses = Object.keys(bankMetadataCache); + + const banksAis = await chunkedGetRawMultipleAccountInfoOrdered(connection, bankAddresses); + let banksMap: { address: PublicKey; data: BankRaw }[] = banksAis.map((account, index) => ({ + address: new PublicKey(bankAddresses[index]), + data: Bank.decodeBankRaw(account.data, program.idl), + })); + + // all supported tokens, banks / emissions mints + const allTokens = [ + ...new Set([ + ...Object.values(bankMetadataCache).map((bank) => bank.tokenAddress), + ...banksMap + .map((bank) => bank.data.emissionsMint.toBase58()) + .filter((mint) => mint !== PublicKey.default.toBase58()), + ]), + ]; + + // filter out restricted tokens + const requestedMints = (mintList as string).split(","); + const supportedMints = requestedMints.filter((mint) => allTokens.includes(mint)); + const restrictedMints = requestedMints.filter((mint) => !allTokens.includes(mint)); + + if (restrictedMints.length > 0) { + console.log("Filtered out restricted tokens:", restrictedMints); + } + + // if no supported tokens, return error + if (supportedMints.length === 0) { + res.status(400).json({ + error: "No supported tokens in request", + }); + return; + } + + // continue with birdeye API call only for supported tokens + const response = await fetch(`${BIRDEYE_API}/defi/multi_price?list_address=${supportedMints.join(",")}`, { + headers: { + Accept: "application/json", + "X-Api-Key": process.env.BIRDEYE_API_KEY || "", + }, + signal: controller.signal, + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + return res.status(response.status).json({ + error: `Birdeye API error: ${response.status} ${response.statusText}`, + }); + } + const data = await response.json(); + + // cache for 20 minutes + res.setHeader("Cache-Control", "s-maxage=1200, stale-while-revalidate=300"); + res.status(200).json(data); + } catch (error) { + console.error("Error:", error); + res.status(500).json({ + error: error instanceof Error ? error.message : "Error fetching data", + }); + } +} diff --git a/apps/marginfi-v2-ui/src/pages/api/birdeye/overview.ts b/apps/marginfi-v2-ui/src/pages/api/tokens/overview.ts similarity index 100% rename from apps/marginfi-v2-ui/src/pages/api/birdeye/overview.ts rename to apps/marginfi-v2-ui/src/pages/api/tokens/overview.ts diff --git a/packages/marginfi-v2-ui-state/src/lib/mrgnlend.ts b/packages/marginfi-v2-ui-state/src/lib/mrgnlend.ts index d7275d77f3..4a0e9b801c 100644 --- a/packages/marginfi-v2-ui-state/src/lib/mrgnlend.ts +++ b/packages/marginfi-v2-ui-state/src/lib/mrgnlend.ts @@ -142,7 +142,7 @@ function makeBankInfo(bank: Bank, oraclePrice: OraclePrice, emissionTokenData?: export async function fetchBirdeyePrices(mints: PublicKey[], apiKey?: string): Promise { const mintList = mints.map((mint) => mint.toBase58()).join(","); - const response = await fetch(`/api/birdeye?mintList=${mintList}`, { + const response = await fetch(`/api/tokens/multi?mintList=${mintList}`, { method: "GET", headers: { "Content-Type": "application/json",