Skip to content

Commit

Permalink
Merge pull request #950 from mrgnlabs/fix/bigNumber-nan-values
Browse files Browse the repository at this point in the history
fix(mrgn-ui): add fallbacks to failing oracle prices from crossbar
  • Loading branch information
k0beLeenders authored Nov 12, 2024
2 parents fd05059 + 5f71140 commit 2206dd0
Show file tree
Hide file tree
Showing 12 changed files with 709 additions and 778 deletions.
225 changes: 210 additions & 15 deletions apps/marginfi-v2-trading/src/pages/api/oracle/price.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
Object.entries(feedIdMapRaw).map(([key, value]) => [key, new PublicKey(value)])
);

const requestedOraclesData = banksMap.map((b) => ({
oracleKey: findOracleKey(BankConfig.fromAccountParsed(b.data.config), feedIdMap).toBase58(),
oracleSetup: parseOracleSetup(b.data.config.oracleSetup),
maxAge: b.data.config.oracleMaxAge,
}));

const oracleMintMap = new Map<string, PublicKey>();
const feedHashMintMap = new Map<string, PublicKey>();

const requestedOraclesData = banksMap.map((b) => {
const oracleKey = findOracleKey(BankConfig.fromAccountParsed(b.data.config), feedIdMap).toBase58();
oracleMintMap.set(oracleKey, b.data.mint);

return {
oracleKey,
oracleSetup: parseOracleSetup(b.data.config.oracleSetup),
maxAge: b.data.config.oracleMaxAge,
};
});
// Fetch on-chain data for all oracles
const oracleAis = await chunkedGetRawMultipleAccountInfoOrdered(connection, [
...requestedOraclesData.map((oracleData) => oracleData.oracleKey),
Expand All @@ -107,17 +114,38 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
for (const index in requestedOraclesData) {
const oracleData = requestedOraclesData[index];
const priceDataRaw = oracleAis[index];
const oraclePrice = parsePriceInfo(oracleData.oracleSetup, priceDataRaw.data);
const mintData = oracleMintMap.get(oracleData.oracleKey)!;
let oraclePrice = parsePriceInfo(oracleData.oracleSetup, priceDataRaw.data);

if (oraclePrice.priceRealtime.price.isNaN()) {
oraclePrice = {
...oraclePrice,
priceRealtime: {
price: new BigNumber(0),
confidence: new BigNumber(0),
lowestPrice: new BigNumber(0),
highestPrice: new BigNumber(0),
},
priceWeighted: {
price: new BigNumber(0),
confidence: new BigNumber(0),
lowestPrice: new BigNumber(0),
highestPrice: new BigNumber(0),
},
};
}

const currentTime = Math.round(Date.now() / 1000);
const oracleTime = oraclePrice.timestamp.toNumber();
const isStale = currentTime - oracleTime > oracleData.maxAge;

// If on-chain data is recent enough, use it even for SwitchboardPull oracles
if (oracleData.oracleSetup === OracleSetup.SwitchboardPull && isStale) {
const feedHash = Buffer.from(decodeSwitchboardPullFeedData(priceDataRaw.data).feed_hash).toString("hex");
feedHashMintMap.set(feedHash, mintData);
swbPullOraclesStale.push({
data: { ...oracleData, timestamp: oraclePrice.timestamp },
feedHash: Buffer.from(decodeSwitchboardPullFeedData(priceDataRaw.data).feed_hash).toString("hex"),
feedHash: feedHash,
});
continue;
}
Expand All @@ -128,16 +156,34 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (swbPullOraclesStale.length > 0) {
// Batch-fetch and cache price data from Crossbar for stale SwitchboardPull oracles
const feedHashes = swbPullOraclesStale.map((value) => value.feedHash);
const crossbarPrices = await fetchCrossbarPrices(feedHashes);
const crossbarPrices = await handleFetchCrossbarPrices(feedHashes, feedHashMintMap);

for (const {
data: { oracleKey, timestamp },
feedHash,
} of swbPullOraclesStale) {
const crossbarPrice = crossbarPrices.get(feedHash);
let crossbarPrice = crossbarPrices.get(feedHash);
if (!crossbarPrice) {
throw new Error(`Crossbar didn't return data for ${feedHash}`);
}
if (crossbarPrice.priceRealtime.price.isNaN()) {
crossbarPrice = {
...crossbarPrice,
priceRealtime: {
price: new BigNumber(0),
confidence: new BigNumber(0),
lowestPrice: new BigNumber(0),
highestPrice: new BigNumber(0),
},
priceWeighted: {
price: new BigNumber(0),
confidence: new BigNumber(0),
lowestPrice: new BigNumber(0),
highestPrice: new BigNumber(0),
},
};
}

const updatedOraclePrice = { ...crossbarPrice, timestamp } as OraclePrice;

updatedOraclePrices.set(oracleKey, updatedOraclePrice);
Expand All @@ -154,16 +200,115 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
}

async function fetchCrossbarPrices(feedHashes: string[]): Promise<Map<string, OraclePrice>> {
async function handleFetchCrossbarPrices(
feedHashes: string[],
mintMap: Map<string, PublicKey>
): Promise<Map<string, OraclePrice>> {
try {
// main crossbar
const payload: CrossbarSimulatePayload = [];

const { payload: mainPayload, brokenFeeds: mainBrokenFeeds } = await fetchCrossbarPrices(
feedHashes,
SWITCHBOARD_CROSSSBAR_API
);

payload.push(...mainPayload);

if (!mainBrokenFeeds.length) {
return crossbarPayloadToOraclePricePerFeedHash(payload);
}

if (process.env.SWITCHBOARD_CROSSSBAR_API_FALLBACK) {
// fallback crossbar
const { payload: fallbackPayload, brokenFeeds: fallbackBrokenFeeds } = await fetchCrossbarPrices(
mainBrokenFeeds,
process.env.SWITCHBOARD_CROSSSBAR_API_FALLBACK
);
payload.push(...fallbackPayload);

if (!fallbackBrokenFeeds.length) {
return crossbarPayloadToOraclePricePerFeedHash(payload);
}
}

// birdeye as last resort
const { payload: birdeyePayload, brokenFeeds: birdeyeBrokenFeeds } = await fetchBirdeyePrices(feedHashes, mintMap);

payload.push(...birdeyePayload);

birdeyeBrokenFeeds.forEach((feed) => {
payload.push({
feedHash: feed,
results: [0],
});
});

return crossbarPayloadToOraclePricePerFeedHash(payload);
} catch (error) {
console.error("Error:", error);
throw new Error("Couldn't fetch from crossbar");
}
}

async function fetchBirdeyePrices(
feedHashes: string[],
mintMap: Map<string, PublicKey>
): Promise<{ payload: CrossbarSimulatePayload; brokenFeeds: string[] }> {
try {
const brokenFeeds: string[] = [];

const tokens = feedHashes
.map((feedHash) => {
const mint = mintMap.get(feedHash)?.toBase58();
if (!mint) {
console.error("Error:", `Mint not found for feedHash ${feedHash}`);
brokenFeeds.push(feedHash);
}
return mint;
})
.filter((mint): mint is string => mint !== undefined);

const response = await fetchMultiPrice(tokens);

const priceData = response.data;

const finalPayload: CrossbarSimulatePayload = feedHashes.map((feedHash) => {
const tokenAddress = mintMap.get(feedHash)!.toBase58();
const price = priceData[tokenAddress];
return {
feedHash,
results: [price.value],
};
});

return { payload: finalPayload, brokenFeeds };
} catch (error) {
console.error("Error:", error);
return { payload: [], brokenFeeds: feedHashes };
}
}

async function fetchCrossbarPrices(
feedHashes: string[],
endpoint: string,
username?: string,
bearer?: string
): Promise<{ payload: CrossbarSimulatePayload; brokenFeeds: string[] }> {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, 5000);
}, 8000);

const isAuth = username && bearer;

const basicAuth = isAuth ? Buffer.from(`${username}:${bearer}`).toString("base64") : undefined;

try {
const feedHashesString = feedHashes.join(",");
const response = await fetch(`${SWITCHBOARD_CROSSSBAR_API}/simulate/${feedHashesString}`, {
const response = await fetch(`${endpoint}/simulate/${feedHashesString}`, {
headers: {
Authorization: basicAuth ? `Basic ${basicAuth}` : "",
Accept: "application/json",
},
signal: controller.signal,
Expand All @@ -174,12 +319,15 @@ async function fetchCrossbarPrices(feedHashes: string[]): Promise<Map<string, Or
if (!response.ok) {
throw new Error("Network response was not ok");
}

const payload = (await response.json()) as CrossbarSimulatePayload;

return crossbarPayloadToOraclePricePerFeedHash(payload);
const brokenFeeds = payload.filter((feed) => feed.results[0] === null).map((feed) => feed.feedHash);

return { payload: payload, brokenFeeds: brokenFeeds };
} catch (error) {
console.error("Error:", error);
throw new Error("Couldn't fetch from crossbar");
return { payload: [], brokenFeeds: feedHashes };
}
}

Expand Down Expand Up @@ -241,3 +389,50 @@ function extractHost(referer: string | undefined): string | undefined {
const url = new URL(referer);
return url.origin;
}

const BIRDEYE_API = "https://public-api.birdeye.so";

interface BirdeyeTokenPrice {
value: number;
updateUnixTime: number;
updateHumanTime: string;
priceChange24h: number;
}

interface BirdeyePriceResponse {
success: boolean;
data: {
[tokenAddress: string]: BirdeyeTokenPrice;
};
}

async function fetchMultiPrice(tokens: string[]): Promise<BirdeyePriceResponse> {
if (!tokens) {
throw new Error("No tokens provided");
}

// 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=${tokens.join("%2C")}`, {
headers: {
Accept: "application/json",
"x-chain": "solana",
"X-Api-Key": process.env.BIRDEYE_API_KEY || "",
},
signal: controller.signal,
});
clearTimeout(timeoutId);

const data = (await response.json()) as BirdeyePriceResponse;
return data;
} catch (error) {
console.error("Error:", error);
throw new Error("Error fetching birdey prices");
}
}
5 changes: 4 additions & 1 deletion apps/marginfi-v2-ui/src/components/common/Stake/stake.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { IconCheck } from "@tabler/icons-react";

import { useMrgnlendStore } from "~/store";
import { useWallet } from "~/components/wallet-v2";
import { LST_MINT, SOL_MINT } from "~/store/lstStore";
import { IntegrationsData, LSTOverview, fetchLSTOverview } from "~/components/common/Stake/utils/stake-utils";

import { Button } from "~/components/ui/button";
Expand All @@ -20,6 +19,10 @@ import {
MfiIntegrationCard,
ArenaIntegrationCard,
} from "~/components/common/Stake";
import { PublicKey } from "@solana/web3.js";

const SOL_MINT = new PublicKey("So11111111111111111111111111111111111111112");
const LST_MINT = new PublicKey("LSTxxxnJzKDFSLr4dUkPcmCf5VyryEqzPLz5j4bpxFp");

const Stake = () => {
const { connected } = useWallet();
Expand Down
4 changes: 4 additions & 0 deletions apps/marginfi-v2-ui/src/pages/api/lst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
apy: 0,
};

if (!process.env.VALIDATOR_API_URL) {
return res.status(500).json({ error: "No validator API URL provided" });
}

const validatorResponse = await fetch(process.env.VALIDATOR_API_URL!);

if (validatorResponse.ok) {
Expand Down
Loading

0 comments on commit 2206dd0

Please sign in to comment.