From 36c851f9d9a5fb02098ff1de9c79708f0f0a435d Mon Sep 17 00:00:00 2001 From: burr Date: Wed, 12 Nov 2025 14:45:42 +0900 Subject: [PATCH 01/24] feat: add wsteth + pintowsteth --- src/constants/tokens.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/constants/tokens.ts b/src/constants/tokens.ts index 1c62fe63d..dc955c84c 100644 --- a/src/constants/tokens.ts +++ b/src/constants/tokens.ts @@ -174,8 +174,39 @@ export const WSOL_TOKEN: ChainLookup = { }, } as const; +export const WSTETH_TOKEN: ChainLookup = { + [base.id]: { + chainId: base.id, + name: "Wrapped stETH", + symbol: "WSTETH", + address: "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452", + decimals: 18, + displayDecimals: 2, + isLPUnderlying: true, + isCompositeLPWhitelisted: false, + logoURI: "https://assets.coingecko.com/coins/images/18834/thumb/wstETH.png?1696518295", + color: "#00A3FF", + }, +}; + // -------------------- LP TOKENS -------------------- +export const PINTO_WSTETH_TOKEN: ChainLookup = { + [base.id]: { + chainId: base.id, + name: "PINTOWSTETH LP", + symbol: "PINTOWSTETH", + address: "0xbd2d86B89353e0d441A3CC3d939A48f17CCDDff5", + decimals: 18, + displayDecimals: 6, // show 6 decimal places for this token + isLP: true, + isWhitelisted: false, + logoURI: pintoWsolIcon, + color: "#00A3FF", + tokens: [MAIN_TOKEN[base.id].address, WSTETH_TOKEN[base.id].address], + }, +} as const; + export const PINTO_WSOL_TOKEN: ChainLookup = { [base.id]: { chainId: base.id, From 47fc67a538d19343568742f0ae09857c90d0a4b2 Mon Sep 17 00:00:00 2001 From: burr Date: Wed, 12 Nov 2025 14:52:50 +0900 Subject: [PATCH 02/24] feat: add wsteth to silo page --- src/constants/tokens.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/constants/tokens.ts b/src/constants/tokens.ts index dc955c84c..b2a2088eb 100644 --- a/src/constants/tokens.ts +++ b/src/constants/tokens.ts @@ -183,7 +183,7 @@ export const WSTETH_TOKEN: ChainLookup = { decimals: 18, displayDecimals: 2, isLPUnderlying: true, - isCompositeLPWhitelisted: false, + isCompositeLPWhitelisted: true, logoURI: "https://assets.coingecko.com/coins/images/18834/thumb/wstETH.png?1696518295", color: "#00A3FF", }, @@ -200,7 +200,7 @@ export const PINTO_WSTETH_TOKEN: ChainLookup = { decimals: 18, displayDecimals: 6, // show 6 decimal places for this token isLP: true, - isWhitelisted: false, + isWhitelisted: true, logoURI: pintoWsolIcon, color: "#00A3FF", tokens: [MAIN_TOKEN[base.id].address, WSTETH_TOKEN[base.id].address], @@ -398,6 +398,7 @@ export const LP_TOKENS: ChainLookup = { PINTO_CBETH_TOKEN[base.id], PINTO_CBBTC_TOKEN[base.id], PINTO_WSOL_TOKEN[base.id], + PINTO_WSTETH_TOKEN[base.id], ], } as const; @@ -430,6 +431,7 @@ export const UNDERLYING_TOKENS: ChainLookup = { CBETH_TOKEN[base.id], CBBTC_TOKEN[base.id], WSOL_TOKEN[base.id], + WSTETH_TOKEN[base.id], ], } as const; From e41517674acaea7f4e7be78c97d4c46a336cd26a Mon Sep 17 00:00:00 2001 From: burr Date: Thu, 13 Nov 2025 13:01:33 +0800 Subject: [PATCH 03/24] feat: add wsteth --- src/assets/tokens/wstETH.svg | 11 +++++++++++ src/components/nav/PriceButton.tsx | 23 ++++++++++++++++++----- src/constants/tokens.ts | 28 ++++++++++++++++++++++------ src/pages/Swap.tsx | 1 - src/state/useTokenData.ts | 9 +++++---- src/utils/token.ts | 6 +++++- src/utils/types.ts | 20 ++++++++++++++++++++ 7 files changed, 81 insertions(+), 17 deletions(-) create mode 100644 src/assets/tokens/wstETH.svg diff --git a/src/assets/tokens/wstETH.svg b/src/assets/tokens/wstETH.svg new file mode 100644 index 000000000..552ceaa09 --- /dev/null +++ b/src/assets/tokens/wstETH.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/components/nav/PriceButton.tsx b/src/components/nav/PriceButton.tsx index be58800ba..6b2204bd0 100644 --- a/src/components/nav/PriceButton.tsx +++ b/src/components/nav/PriceButton.tsx @@ -12,6 +12,7 @@ import { PoolData, usePriceData, useTwaDeltaBLPQuery, useTwaDeltaBQuery } from " import useTokenData from "@/state/useTokenData"; import { withTracking } from "@/utils/analytics"; import { formatter } from "@/utils/format"; +import { stringEq } from "@/utils/string"; import { getTokenIndex } from "@/utils/token"; import { Token } from "@/utils/types"; import { cn } from "@/utils/utils"; @@ -19,7 +20,7 @@ import { HTMLAttributes, memo, useMemo, useState } from "react"; import { Link } from "react-router-dom"; import { useChainId } from "wagmi"; import { renderAnnouncement } from "../AnnouncementBanner"; -import { InlineCenterSpan, Row } from "../Container"; +import { InlineCenterSpan } from "../Container"; import { ExternalLinkIcon, ForwardArrowIcon } from "../Icons"; import TooltipSimple from "../TooltipSimple"; import { Card, CardContent, CardFooter, CardHeader } from "../ui/Card"; @@ -301,14 +302,18 @@ const PoolGroup = ({ if (props.showPrices) { return ( <> - + ); } return ( { const backingTokenIndex = pool.tokens.findIndex((token: Token) => !token.isMain); + + if (backingTokenIndex === -1) { + return null; + } + const tokenToShow = priceData.tokenPrices.get(pool.tokens[backingTokenIndex]); - if (!tokenToShow) return null; + + if (!tokenToShow) { + return null; + } return (
@@ -375,7 +388,7 @@ const PoolCard = ({ pool, chainId, priceData, expandAll, useTwa, twaDeltaBMap }: const token0BalanceUsd = pool.balances[0].mul(token0Price ?? TokenValue.ZERO) || 0n; const token1BalanceUsd = pool.balances[1].mul(token1Price ?? TokenValue.ZERO) || 0n; const mainTokenIndex = pool.tokens.findIndex((token: Token) => token.isMain); - const deltaBar = pool.balances[mainTokenIndex].gt(0) + const deltaBar = pool.balances[mainTokenIndex]?.gt(0) ? Number(pool.deltaB.abs().div(pool.balances[mainTokenIndex]).mul(100).toHuman()).toFixed(2) : "0"; diff --git a/src/constants/tokens.ts b/src/constants/tokens.ts index b2a2088eb..6620950c4 100644 --- a/src/constants/tokens.ts +++ b/src/constants/tokens.ts @@ -11,6 +11,7 @@ import spectrasPintoYTIcon from "@/assets/tokens/SPECTRA-sPINTO-YT.png"; import wsolIcon from "@/assets/tokens/WSOL.png"; import crsPintoIcon from "@/assets/tokens/crsPINTO.png"; import sPintoIcon from "@/assets/tokens/sPINTO.png"; +import wstETHIcon from "@/assets/tokens/wstETH.svg"; import { Token, Token3PIntegration } from "@/utils/types"; import { ChainLookup } from "@/utils/types.generic"; import { arbitrum, base } from "viem/chains"; @@ -177,18 +178,25 @@ export const WSOL_TOKEN: ChainLookup = { export const WSTETH_TOKEN: ChainLookup = { [base.id]: { chainId: base.id, - name: "Wrapped stETH", - symbol: "WSTETH", + name: "Wrapped liquid staked Ether 2.0", + symbol: "wstETH", address: "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452", decimals: 18, displayDecimals: 2, isLPUnderlying: true, isCompositeLPWhitelisted: true, - logoURI: "https://assets.coingecko.com/coins/images/18834/thumb/wstETH.png?1696518295", + logoURI: wstETHIcon, color: "#00A3FF", }, }; +const defaultLPTokenIndicies = { + tokenIndicies: { + main: 0, + pair: 1, + }, +} as const; + // -------------------- LP TOKENS -------------------- export const PINTO_WSTETH_TOKEN: ChainLookup = { @@ -196,14 +204,15 @@ export const PINTO_WSTETH_TOKEN: ChainLookup = { chainId: base.id, name: "PINTOWSTETH LP", symbol: "PINTOWSTETH", - address: "0xbd2d86B89353e0d441A3CC3d939A48f17CCDDff5", + address: "0x1957F43834d4e538cE99378FB26059579cf9bEe1", // temp address decimals: 18, - displayDecimals: 6, // show 6 decimal places for this token + displayDecimals: 2, isLP: true, isWhitelisted: true, logoURI: pintoWsolIcon, color: "#00A3FF", tokens: [MAIN_TOKEN[base.id].address, WSTETH_TOKEN[base.id].address], + ...defaultLPTokenIndicies, }, } as const; @@ -220,6 +229,7 @@ export const PINTO_WSOL_TOKEN: ChainLookup = { logoURI: pintoWsolIcon, color: "#9945FF", tokens: [MAIN_TOKEN[base.id].address, WSOL_TOKEN[base.id].address], + ...defaultLPTokenIndicies, }, } as const; @@ -236,6 +246,7 @@ export const PINTO_WETH_TOKEN: ChainLookup = { logoURI: pintoWethIcon, color: "#8C8C8C", tokens: [MAIN_TOKEN[base.id].address, WETH_TOKEN[base.id].address], + ...defaultLPTokenIndicies, }, } as const; @@ -252,6 +263,7 @@ export const PINTO_CBETH_TOKEN: ChainLookup = { logoURI: pintoCbethIcon, color: "#0052FF", tokens: [MAIN_TOKEN[base.id].address, CBETH_TOKEN[base.id].address], + ...defaultLPTokenIndicies, }, } as const; @@ -268,6 +280,7 @@ export const PINTO_USDC_TOKEN: ChainLookup = { logoURI: pintoUsdcIcon, color: "#2775CA", tokens: [MAIN_TOKEN[base.id].address, USDC_TOKEN[base.id].address], + ...defaultLPTokenIndicies, }, } as const; @@ -284,6 +297,7 @@ export const PINTO_CBBTC_TOKEN: ChainLookup = { logoURI: pintoCbbtcIcon, color: "#F7931A", tokens: [MAIN_TOKEN[base.id].address, CBBTC_TOKEN[base.id].address], + ...defaultLPTokenIndicies, }, } as const; @@ -479,7 +493,7 @@ export const tokens: { [chainId: number]: Token[] } = { export const PINTO = MAIN_TOKEN[defaultChain]; // Native Chain Token -export const ETH = NATIVE_TOKEN[defaultChain][1]; +export const ETH = NATIVE_TOKEN[defaultChain]; // Well LP Tokens export const PINTOWETH = LP_TOKENS[defaultChain][0]; @@ -487,6 +501,7 @@ export const PINTOCBETH = LP_TOKENS[defaultChain][1]; export const PINTOUSDC = LP_TOKENS[defaultChain][2]; export const PINTOCBBTC = LP_TOKENS[defaultChain][3]; export const PINTOWSOL = LP_TOKENS[defaultChain][4]; +export const PINTOWSTETH = LP_TOKENS[defaultChain][5]; // Underlying Tokens export const WETH = UNDERLYING_TOKENS[defaultChain][0]; @@ -494,3 +509,4 @@ export const USDC = UNDERLYING_TOKENS[defaultChain][1]; export const CBETH = UNDERLYING_TOKENS[defaultChain][2]; export const CBBTC = UNDERLYING_TOKENS[defaultChain][3]; export const WSOL = UNDERLYING_TOKENS[defaultChain][4]; +export const WSTETH = UNDERLYING_TOKENS[defaultChain][5]; diff --git a/src/pages/Swap.tsx b/src/pages/Swap.tsx index 7b7fb4028..f3f0b90c3 100644 --- a/src/pages/Swap.tsx +++ b/src/pages/Swap.tsx @@ -1,4 +1,3 @@ -import { TokenValue } from "@/classes/TokenValue"; import { ComboInputField } from "@/components/ComboInputField"; import DestinationBalanceSelect from "@/components/DestinationBalanceSelect"; import { UpDownArrowsIcon } from "@/components/Icons"; diff --git a/src/state/useTokenData.ts b/src/state/useTokenData.ts index cbf94bd6f..b2b24fc97 100644 --- a/src/state/useTokenData.ts +++ b/src/state/useTokenData.ts @@ -1,6 +1,7 @@ import { LP_TOKENS, MAIN_TOKEN, NATIVE_TOKEN, S_MAIN_TOKEN, WETH_TOKEN, tokens } from "@/constants/tokens"; import { useChainConstant, useResolvedChainId } from "@/utils/chain"; -import { Token } from "@/utils/types"; +import { isLPToken } from "@/utils/token"; +import { LPToken, Token } from "@/utils/types"; import { useMemo } from "react"; export function useWhitelistedTokens() { @@ -35,19 +36,19 @@ export default function useTokenData() { const wrappedNativeToken = useChainConstant(WETH_TOKEN); return useMemo(() => { - const lpTokens: Token[] = []; + const lpTokens: LPToken[] = []; const preferredTokens: Token[] = []; const whitelistedTokens: Token[] = []; const deWhitelistedTokens: Token[] = []; - const siloWrappedToken3p = tokens[chainId].find((token) => token.is3PSiloWrapped) as Token; + const siloWrappedToken3p = tokens[chainId].find((token) => token.is3PSiloWrapped); if (!siloWrappedToken3p) { throw new Error("3p wrapped native token not found"); } for (const token of tokens[chainId]) { - if (token.isLP) { + if (isLPToken(token)) { lpTokens.push(token); } if (!token.isNative && !token.isLP) { diff --git a/src/utils/token.ts b/src/utils/token.ts index 5f563b3e9..afb0d9ee0 100644 --- a/src/utils/token.ts +++ b/src/utils/token.ts @@ -3,7 +3,7 @@ import { PINTO, tokens } from "@/constants/tokens"; import { Address } from "viem"; import { resolveChainId } from "./chain"; import { stringEq } from "./string"; -import { AddressMap, InternalToken, SiloTokenData, Token, TokenDepositData } from "./types"; +import { AddressMap, InternalToken, LPToken, SiloTokenData, Token, TokenDepositData } from "./types"; import { ChainLookup } from "./types.generic"; import { exists } from "./utils"; @@ -47,6 +47,10 @@ export const tokensEqual = (a: TokenIsh | undefined | null, b: TokenIsh | undefi return stringEq(a.address, b.address) && stringEq(a.symbol, b.symbol); }; +export const isLPToken = (token: Token): token is LPToken => { + return !!token.tokens && !!token.tokenIndicies && !!token.isLP; +}; + /** * Sort tokens for tables, with configurable sorting modes: * diff --git a/src/utils/types.ts b/src/utils/types.ts index 8e65bcec4..5b9836481 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -3,6 +3,7 @@ import { SeasonalChartData } from "@/components/charts/SeasonalChart"; import { PlotSource } from "@/generated/gql/pintostalk/graphql"; import { ProtocolIntegration } from "@/state/integrations/types"; import { APYWindow } from "@/state/seasonal/queries/useSeasonalAPY"; +import { Prettify } from "@/utils/types.generic"; import { QueryKey, UseQueryOptions } from "@tanstack/react-query"; import { DateTime } from "luxon"; import { ReactNode } from "react"; @@ -20,6 +21,16 @@ export enum FarmToMode { INTERNAL = "1", } +interface LPIndicies { + main: number; + pair: number; +} + +interface LPTokenIsh { + tokens: Address[]; + tokenIndicies: LPIndicies; +} + export interface Token { name: string; symbol: string; @@ -77,8 +88,17 @@ export interface Token { * Description of the token (If Applicable). */ description?: string; + /** + * The Token indexes of the main & pair token for LPs + */ + tokenIndicies?: { + main: number; + pair: number; + }; } +export type LPToken = Prettify & LPTokenIsh & { isLP: true }>; + export interface Token3PIntegration extends Token { integration: ProtocolIntegration; } From 2c0267630d5f9009b76f7551efbddee0892f9539 Mon Sep 17 00:00:00 2001 From: burr Date: Tue, 18 Nov 2025 16:47:23 +0900 Subject: [PATCH 04/24] feat: fix dev page keys + update pintowsteth address --- src/components/DevPage.tsx | 9 ++++++--- src/components/nav/PriceButton.tsx | 2 +- src/constants/tokens.ts | 2 +- src/state/usePriceData.ts | 4 ++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/components/DevPage.tsx b/src/components/DevPage.tsx index 0e5b637e9..a722981e0 100644 --- a/src/components/DevPage.tsx +++ b/src/components/DevPage.tsx @@ -1180,8 +1180,8 @@ const ViewFunctionCaller = () => { className="h-10 px-3 py-2 text-sm border rounded-md bg-white" > - {viewFunctions.map((fn) => ( - @@ -1204,7 +1204,10 @@ const ViewFunctionCaller = () => {
Function Parameters:
{selectedFunctionObj.inputs.map((input, idx) => ( -
+
diff --git a/src/components/nav/PriceButton.tsx b/src/components/nav/PriceButton.tsx index 6b2204bd0..c9d8678e5 100644 --- a/src/components/nav/PriceButton.tsx +++ b/src/components/nav/PriceButton.tsx @@ -39,7 +39,7 @@ function PriceButtonPanel() { const { data: twaDeltaBMap } = useTwaDeltaBLPQuery(); const whitelistedPools = useMemo(() => { - return priceData.pools.filter((pool) => pool.pool.isWhitelisted); + return priceData.pools.filter((pool) => pool.pool && pool.pool.isWhitelisted); }, [priceData.pools]); const mainTokenBalances = useMemo(() => { diff --git a/src/constants/tokens.ts b/src/constants/tokens.ts index 6620950c4..7723801d4 100644 --- a/src/constants/tokens.ts +++ b/src/constants/tokens.ts @@ -204,7 +204,7 @@ export const PINTO_WSTETH_TOKEN: ChainLookup = { chainId: base.id, name: "PINTOWSTETH LP", symbol: "PINTOWSTETH", - address: "0x1957F43834d4e538cE99378FB26059579cf9bEe1", // temp address + address: "0x3e1155480Fce43686793dd18E43104A01abCBC92", // temp address decimals: 18, displayDecimals: 2, isLP: true, diff --git a/src/state/usePriceData.ts b/src/state/usePriceData.ts index c0b1267e1..12a6e5bb5 100644 --- a/src/state/usePriceData.ts +++ b/src/state/usePriceData.ts @@ -70,7 +70,7 @@ const useSelectWellPriceData = () => { const pool = tokenMap[getTokenIndex(data.pool)]; const tokens = data.tokens.map((token) => tokenMap[getTokenIndex(token)]); - const balances = tokens.map((token, index) => TokenValue.fromBigInt(data.balances[index], token.decimals)); + const balances = tokens.map((token, index) => TokenValue.fromBigInt(data.balances[index], token?.decimals ?? 0)); return { pool, @@ -247,7 +247,7 @@ export function usePriceData() { const combined = [...price.ps, ...deWhitelistedWellsQuery.data]; for (const wellPriceData of combined) { const poolData = selectWellPriceData(wellPriceData); - if (poolData) { + if (poolData && poolData.pool) { pools.push(poolData); } } From 880ec075aa5bfe5f8faa179bfa4890023c39be78 Mon Sep 17 00:00:00 2001 From: burr Date: Tue, 18 Nov 2025 22:57:23 +0900 Subject: [PATCH 05/24] feat: update to use multicall instead of advpipe on silo convert cache --- src/lib/siloConvert/SiloConvert.cache.ts | 190 ++++++++++++++++++++++- 1 file changed, 188 insertions(+), 2 deletions(-) diff --git a/src/lib/siloConvert/SiloConvert.cache.ts b/src/lib/siloConvert/SiloConvert.cache.ts index e7614ff31..cb6f76750 100644 --- a/src/lib/siloConvert/SiloConvert.cache.ts +++ b/src/lib/siloConvert/SiloConvert.cache.ts @@ -1,5 +1,6 @@ import { Clipboard } from "@/classes/Clipboard"; import { TV } from "@/classes/TokenValue"; +import { diamondABI } from "@/constants/abi/diamondABI"; import { diamondPriceABI } from "@/constants/abi/diamondPriceABI"; import { abiSnippets } from "@/constants/abiSnippets"; import { MAIN_TOKEN } from "@/constants/tokens"; @@ -13,13 +14,22 @@ import { beanstalkPriceAddress } from "@/generated/contractHooks"; import { AdvancedPipeWorkflow } from "@/lib/farm/workflow"; import { SiloConvertContext } from "@/lib/siloConvert/types"; import { ExchangeWell } from "@/lib/well/ExchangeWell"; -import { PoolData } from "@/state/usePriceData"; +import { BasePoolData, PoolData } from "@/state/usePriceData"; import { resolveChainId } from "@/utils/chain"; +import { stringEq } from "@/utils/string"; import { getChainToken, getChainTokenMap, getTokenIndex } from "@/utils/token"; import { tokensEqual } from "@/utils/token"; import { AdvancedPipeCall, Token } from "@/utils/types"; import { AddressLookup, HashString } from "@/utils/types.generic"; -import { Address, decodeFunctionResult, encodeFunctionData } from "viem"; +import { + Address, + ContractFunctionParameters, + MulticallResponse, + MulticallReturnType, + decodeFunctionResult, + encodeFunctionData, +} from "viem"; +import { multicall } from "viem/actions"; /** * Extended pool data for the SiloConvertCache. @@ -92,6 +102,11 @@ export class SiloConvertPriceCache { /** The last time the cache was updated. */ lastUpdateTimestamp: number = 0; + /** + * A set of well addresses that failed to return any form of price data from the price / diamond contract + */ + private invalidWells: Set = new Set(); + constructor(context: SiloConvertContext) { this.context = context; this.lp2Pair = getLpTokensToPairs(context.chainId); @@ -292,6 +307,134 @@ export class SiloConvertPriceCache { return advPipe; } + async fetchMulticall() { + const client = this.context.wagmiConfig.getClient({ chainId: this.context.chainId }); + const tokenMap = getChainTokenMap(this.context.chainId); + + const erroredWells: Set = new Set(); + + const calls = this.getMultiCallPriceContracts(); + + const { priceCalls, dewhitelistedWellLPCalls, lpPairTokenPriceCalls } = calls; + + const datas = await multicall(client, { + contracts: [...priceCalls, ...dewhitelistedWellLPCalls, ...lpPairTokenPriceCalls], + allowFailure: true, + }); + + const [priceData, ...remaining] = datas; + const dewhitelistedLPData = remaining.slice(0, dewhitelistedWellLPCalls.length) as MulticallReturnType< + typeof dewhitelistedWellLPCalls, + true + >; + const lpPairTokenPriceData = remaining.slice(dewhitelistedWellLPCalls.length, remaining.length); + + const priceResult = priceData.result; + + if (!priceResult || typeof priceResult !== "object" || !("ps" in priceResult)) { + throw new Error("[SiloConvertCache/fetchMulticall] Price data error"); + } + + const map: AddressLookup = {}; + + const dwLPs = Object.values(this.dewhitelistedLP); + + const dwLPData = dewhitelistedLPData + .map((d, i) => { + if (d.result && typeof d.result === "object") { + return d.result; + } + + console.debug( + `[SiloConvertCache/fetchMulticall] Error decoding dewhitelisted LP data for ${dwLPs[i]?.address}. Adding to erroredWells set.`, + ); + const erroredWell = Object.values(this.dewhitelistedLP)[i]?.address; + erroredWells.add(Object.values(this.dewhitelistedLP)[i]?.address); + return undefined; + }) + .filter((d) => d !== undefined); + + const reducedPs = priceResult.ps.reduce>>((prev, curr) => { + prev[getTokenIndex(curr.pool)] = { + ...curr, + tokens: [curr.tokens[0], curr.tokens[1]] satisfies Address[], + balances: [curr.balances[0], curr.balances[1]] satisfies bigint[], + }; + return prev; + }, {}); + + for (const [index, [lpTokenIndex, pairToken]] of Object.entries(this.lp2Pair).entries()) { + const data = lpPairTokenPriceData[index]; + const result = data.result; + + if (!result || typeof result !== "bigint") { + continue; + } + + let poolResult = reducedPs[getTokenIndex(lpTokenIndex)]; + + if (!poolResult) { + const mayPoolResult = dwLPData.find((d) => stringEq(d.pool, lpTokenIndex)); + if (mayPoolResult) { + poolResult = { + ...mayPoolResult, + tokens: [mayPoolResult.tokens[0], mayPoolResult.tokens[1]] satisfies Address[], + balances: [mayPoolResult.balances[0], mayPoolResult.balances[1]] satisfies bigint[], + }; + } else { + erroredWells.add(lpTokenIndex); + console.debug( + `[SiloConvertCache/fetchMulticall] No pool result found for ${lpTokenIndex}. Adding to erroredWells set.`, + ); + continue; + } + } + + const wellTokens = poolResult.tokens.map((t) => getChainToken(this.context.chainId, t)); + if (!wellTokens.length) { + erroredWells.add(lpTokenIndex); + console.debug( + `[SiloConvertCache/fetchMulticall] No well tokens found with address: ${lpTokenIndex}. Adding to erroredWells set.`, + ); + continue; + } + + const pairPrice = TV.fromBigInt(result, pairToken.decimals); + const poolPrice = TV.fromBigInt(poolResult.price, 6); + + const pairData = { + token: pairToken, + index: wellTokens[0].isMain ? 1 : 0, + price: pairPrice, + }; + + map[lpTokenIndex] = { + pool: tokenMap[lpTokenIndex], + price: TV.fromBigInt(poolResult.price, 6), + pair: pairData, + tokens: wellTokens, + liquidity: TV.fromBigInt(poolResult.liquidity, 6), + lpUsd: TV.fromBigInt(poolResult.lpUsd, 6), + lpBdv: TV.fromBigInt(poolResult.lpBdv, 6), + deltaB: TV.fromBigInt(poolResult.deltaB, 6), + balances: wellTokens.map((t, i) => TV.fromBigInt(poolResult.balances[i], t.decimals)), + prices: wellTokens.map((t) => (t.isMain ? poolPrice : pairData.price)), + }; + } + + if (!dewhitelistedLPData.length || !lpPairTokenPriceData.length) { + throw new Error("No dewhitelistedLPData or lpPairTokenPriceData found"); + } + + return { + deltaB: TV.fromBigInt(priceResult.deltaB, 6), + price: TV.fromBigInt(priceResult.price, 6), + liquidity: TV.fromBigInt(priceResult.liquidity, 6), + pools: map, + erroredWells, + }; + } + /** * Fetches the relevant pool data from on chain */ @@ -302,6 +445,8 @@ export class SiloConvertPriceCache { const advPipe = this.constructPriceAdvPipe(); + const others = await this.fetchMulticall(); + // Fetch price contract data & price oracle data const advPipeResult = await advPipe.readStatic(); @@ -359,6 +504,47 @@ export class SiloConvertPriceCache { pools: map, }; } + + /** + * + * constructs the contracts for the price multicall + */ + private getMultiCallPriceContracts() { + const priceAddress = beanstalkPriceAddress[resolveChainId(this.context.chainId)]; + + // Put this in an array so TS doesn't complain about the type mismatch when not in an array + const priceCalls: ContractFunctionParameters[] = [ + { + address: priceAddress, + abi: diamondPriceABI, + functionName: "price", + args: [], + }, + ]; + const dewhitelistedWellLPCalls: ContractFunctionParameters[] = + Object.values(this.dewhitelistedLP).map((dwlp) => { + return { + address: priceAddress, + abi: diamondPriceABI, + functionName: "getWell", + args: [dwlp.address], + }; + }); + const lpPairTokenPriceCalls = Object.values(this.lp2Pair).map((data) => { + return { + address: this.context.diamond, + abi: diamondABI, + functionName: "getTokenUsdPrice", + args: [data.address], + } as const; + }); + + return { + priceCalls, + dewhitelistedWellLPCalls, + lpPairTokenPriceCalls, + } as const; + } } /** From 9f01c005166215c00daaa51bee2196fe0c99543e Mon Sep 17 00:00:00 2001 From: burr Date: Tue, 18 Nov 2025 23:05:34 +0900 Subject: [PATCH 06/24] feat: add invalid well swap strategizer --- src/lib/siloConvert/SiloConvert.cache.ts | 11 ++++++- .../siloConvert/siloConvert.strategizer.ts | 30 +++++++++++-------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/lib/siloConvert/SiloConvert.cache.ts b/src/lib/siloConvert/SiloConvert.cache.ts index cb6f76750..7544f3a31 100644 --- a/src/lib/siloConvert/SiloConvert.cache.ts +++ b/src/lib/siloConvert/SiloConvert.cache.ts @@ -186,6 +186,14 @@ export class SiloConvertPriceCache { return well.pair.price; } + getIsWellAvailable(well: Address) { + if (this.invalidWells.has(getTokenIndex(well))) { + return false; + } + + return true; + } + /** * Returns the Extended Well data for a given Well address. */ @@ -211,7 +219,8 @@ export class SiloConvertPriceCache { console.debug("[SiloConvertCache/update] updating cache..."); const diff = Date.now() - this.lastUpdateTimestamp; if (force || this.lastUpdateTimestamp === 0 || diff > REFETCH_INTERVAL) { - const priceResult = await this.fetch(); + const priceResult = await this.fetchMulticall(); + this.invalidWells = priceResult.erroredWells; this.priceStruct = priceResult; this.lastUpdateTimestamp = Date.now(); console.debug("[PipelineConvert/Cache/update]: ", this); diff --git a/src/lib/siloConvert/siloConvert.strategizer.ts b/src/lib/siloConvert/siloConvert.strategizer.ts index 8b1e98791..78efdd538 100644 --- a/src/lib/siloConvert/siloConvert.strategizer.ts +++ b/src/lib/siloConvert/siloConvert.strategizer.ts @@ -137,19 +137,23 @@ export class Strategizer { const sourceWell = source.isLP ? this.cache.getWell(source.address) : undefined; // if SourceWell exists, target must be main due to validation above. - if (sourceWell && !this.maxConvertQuoter.isAggDisabledToken(source)) { - routes.push({ - source, - target, - strategies: [ - { - strategy: new LP2MainPipeline(sourceWell, target, this.context), - amount: amountIn, - }, - ], - convertType: "LP2MainPipeline", - }); - } + // if ( + // sourceWell && + // !this.maxConvertQuoter.isAggDisabledToken(source) && + // this.cache.getIsWellAvailable(source.address) + // ) { + // routes.push({ + // source, + // target, + // strategies: [ + // { + // strategy: new LP2MainPipeline(sourceWell, target, this.context), + // amount: amountIn, + // }, + // ], + // convertType: "LP2MainPipeline", + // }); + // } return routes; }, "strategizeLPAndMain"); From aeabe091af5d93dd457c234b3e1135b910e57823 Mon Sep 17 00:00:00 2001 From: burr Date: Tue, 18 Nov 2025 23:32:22 +0900 Subject: [PATCH 07/24] feat: update silo convert --- src/lib/siloConvert/SiloConvert.cache.ts | 81 ++----------------- src/lib/siloConvert/SiloConvert.ts | 4 + .../siloConvert/siloConvert.strategizer.ts | 34 ++++---- 3 files changed, 28 insertions(+), 91 deletions(-) diff --git a/src/lib/siloConvert/SiloConvert.cache.ts b/src/lib/siloConvert/SiloConvert.cache.ts index 7544f3a31..27fc91184 100644 --- a/src/lib/siloConvert/SiloConvert.cache.ts +++ b/src/lib/siloConvert/SiloConvert.cache.ts @@ -24,7 +24,6 @@ import { AddressLookup, HashString } from "@/utils/types.generic"; import { Address, ContractFunctionParameters, - MulticallResponse, MulticallReturnType, decodeFunctionResult, encodeFunctionData, @@ -260,15 +259,19 @@ export class SiloConvertPriceCache { } getPriceCallStructs(): AdvancedPipeCall[] { + const availWells = Object.values(this.dewhitelistedLP).filter((tk) => this.getIsWellAvailable(tk.address)); + return [ encodePrice(this.context.chainId), - ...Object.values(this.dewhitelistedLP).map((tk) => encodeGetWell(this.context.chainId, tk.address)), + ...availWells.map((tk) => encodeGetWell(this.context.chainId, tk.address)), ]; } decodePriceCallResults(results: HashString[]) { + const availWells = Object.values(this.dewhitelistedLP).filter((tk) => this.getIsWellAvailable(tk.address)); + // +1 for the price call - const expectedLength = Object.keys(this.dewhitelistedLP).length + 1; + const expectedLength = availWells.length + 1; if (results.length < expectedLength) { throw new Error(`Cannot decode price call results. Expected ${expectedLength} results but got ${results.length}`); @@ -279,7 +282,7 @@ export class SiloConvertPriceCache { priceResult.pools = { ...priceResult.pools }; - Object.values(this.dewhitelistedLP).forEach((tk, idx) => { + availWells.forEach((tk, idx) => { const res = decodeGetWell(dewhitelistedLPResults[idx]); priceResult.pools[getTokenIndex(tk.address)] = res; }); @@ -444,76 +447,6 @@ export class SiloConvertPriceCache { }; } - /** - * Fetches the relevant pool data from on chain - */ - async fetch(): Promise { - console.debug("[SiloConvertCache/fetch] fetching price data..."); - const tokenMap = getChainTokenMap(this.context.chainId); - const mainToken = MAIN_TOKEN[resolveChainId(this.context.chainId)]; - - const advPipe = this.constructPriceAdvPipe(); - - const others = await this.fetchMulticall(); - - // Fetch price contract data & price oracle data - - const advPipeResult = await advPipe.readStatic(); - const sliceIdx = Object.keys(this.dewhitelistedLP).length + 1; - const priceFragments = advPipeResult.slice(0, sliceIdx); - const tokenUsdData = advPipeResult.slice(sliceIdx, advPipeResult.length); - - const priceResult = this.decodePriceCallResults(priceFragments); - - const map: AddressLookup = {}; - - for (const [index, [lpTokenIndex, pairToken]] of Object.entries(this.lp2Pair).entries()) { - const pairPriceBigInt = decodeFunctionResult({ - abi: abiSnippets.price.getTokenUsdPrice, - functionName: "getTokenUsdPrice", - data: tokenUsdData[index], - }); - - const poolResult = priceResult.pools[lpTokenIndex]; - const wellTokens = poolResult?.tokens.map((t) => getChainToken(this.context.chainId, t)); - - if (!poolResult) { - throw new Error(`Pool result not found for ${lpTokenIndex}`); - } - if (!wellTokens.length) { - throw new Error(`No well tokens found with address: ${lpTokenIndex}`); - } - - const poolPrice = TV.fromBigInt(poolResult.price, mainToken.decimals); - - const pairData = { - token: pairToken, - index: wellTokens[0].isMain ? 1 : 0, - price: TV.fromBigInt(pairPriceBigInt, mainToken.decimals), - }; - - map[lpTokenIndex] = { - pool: tokenMap[lpTokenIndex], - price: poolPrice, - pair: pairData, - tokens: wellTokens, - liquidity: TV.fromBigInt(poolResult.liquidity, 6), - lpUsd: TV.fromBigInt(poolResult.lpUsd, mainToken.decimals), - lpBdv: TV.fromBigInt(poolResult.lpBdv, mainToken.decimals), - deltaB: TV.fromBigInt(poolResult.deltaB, mainToken.decimals), - balances: wellTokens.map((t, i) => TV.fromBigInt(poolResult.balances[i], t.decimals)), - prices: wellTokens.map((t) => (t.isMain ? poolPrice : pairData.price)), - }; - } - - return { - deltaB: TV.fromBigInt(priceResult.deltaB, 6), - price: TV.fromBigInt(priceResult.price, 6), - liquidity: TV.fromBigInt(priceResult.liquidity, 6), - pools: map, - }; - } - /** * * constructs the contracts for the price multicall diff --git a/src/lib/siloConvert/SiloConvert.ts b/src/lib/siloConvert/SiloConvert.ts index 9173560c8..a5968fddc 100644 --- a/src/lib/siloConvert/SiloConvert.ts +++ b/src/lib/siloConvert/SiloConvert.ts @@ -397,6 +397,10 @@ export class SiloConvert { // price result is the last element in the static call result const priceResult = staticCallResult.pop(); + console.log({ + priceResult, + }); + const decodedConvertResults = decodeConvertResults(staticCallResult, route.convertType); const decodedPriceCalls = priceResult ? AdvancedPipeWorkflow.decodeResult(priceResult) : undefined; diff --git a/src/lib/siloConvert/siloConvert.strategizer.ts b/src/lib/siloConvert/siloConvert.strategizer.ts index 78efdd538..b14b59237 100644 --- a/src/lib/siloConvert/siloConvert.strategizer.ts +++ b/src/lib/siloConvert/siloConvert.strategizer.ts @@ -137,23 +137,23 @@ export class Strategizer { const sourceWell = source.isLP ? this.cache.getWell(source.address) : undefined; // if SourceWell exists, target must be main due to validation above. - // if ( - // sourceWell && - // !this.maxConvertQuoter.isAggDisabledToken(source) && - // this.cache.getIsWellAvailable(source.address) - // ) { - // routes.push({ - // source, - // target, - // strategies: [ - // { - // strategy: new LP2MainPipeline(sourceWell, target, this.context), - // amount: amountIn, - // }, - // ], - // convertType: "LP2MainPipeline", - // }); - // } + if ( + sourceWell && + !this.maxConvertQuoter.isAggDisabledToken(source) && + this.cache.getIsWellAvailable(source.address) + ) { + routes.push({ + source, + target, + strategies: [ + { + strategy: new LP2MainPipeline(sourceWell, target, this.context), + amount: amountIn, + }, + ], + convertType: "LP2MainPipeline", + }); + } return routes; }, "strategizeLPAndMain"); From b9d3d0ab21cc303b77a31f806a1c9417f01e3fc7 Mon Sep 17 00:00:00 2001 From: burr Date: Wed, 19 Nov 2025 22:35:13 +0900 Subject: [PATCH 08/24] bug: fix swaps div by zero bug --- src/constants/slots.ts | 2 + src/hooks/swap/useSwapSummary.ts | 10 ++++- src/lib/Swap/price-cache.ts | 70 +++++++++++++++++--------------- 3 files changed, 47 insertions(+), 35 deletions(-) diff --git a/src/constants/slots.ts b/src/constants/slots.ts index 246c4d612..5453ebcb2 100644 --- a/src/constants/slots.ts +++ b/src/constants/slots.ts @@ -6,6 +6,7 @@ import { USDC_TOKEN, WETH_TOKEN, WSOL_TOKEN, + WSTETH_TOKEN, } from "@/constants/tokens"; import { AddressMap } from "@/utils/types"; import { ChainLookup } from "@/utils/types.generic"; @@ -15,6 +16,7 @@ export const addressAllowanceSlotMap: ChainLookup> = { [base.id]: { [MAIN_TOKEN[base.id].address.toLowerCase()]: 1, [WETH_TOKEN[base.id].address.toLowerCase()]: 4, + [WSTETH_TOKEN[base.id].address.toLowerCase()]: 2, [CBETH_TOKEN[base.id].address.toLowerCase()]: 52, [USDC_TOKEN[base.id].address.toLowerCase()]: 10, [CBBTC_TOKEN[base.id].address.toLowerCase()]: 10, diff --git a/src/hooks/swap/useSwapSummary.ts b/src/hooks/swap/useSwapSummary.ts index b77ace324..7bb10dc12 100644 --- a/src/hooks/swap/useSwapSummary.ts +++ b/src/hooks/swap/useSwapSummary.ts @@ -60,7 +60,13 @@ export default function useSwapSummary(quote: BeanSwapNodeQuote | undefined): Sw if (node instanceof WellSyncSwapNode) { addLiquidityRoute = route; - addLiquiditySlippage = quote.usdIn.sub(quote.usdOut).div(quote.usdIn).mul(100).toNumber(); + const usdOut = quote.usdOut.eq(0) ? TV.ONE : quote.usdOut; + const usdIn = quote.usdIn.eq(0) ? TV.ONE : quote.usdIn; + if (usdIn.isZero || usdOut.isZero) { + addLiquiditySlippage = undefined; + } else { + addLiquiditySlippage = usdIn.sub(usdOut).div(usdIn).mul(100).toNumber(); + } routes.push(route); } else { if (node instanceof ZeroXSwapNode) { @@ -69,7 +75,7 @@ export default function useSwapSummary(quote: BeanSwapNodeQuote | undefined): Sw if (fee?.feeToken) { const feeTokenUSD = tokenPrices.get(fee.feeToken); - if (feeTokenUSD) { + if (feeTokenUSD && !node.usdIn.isZero && !node.usdIn.isZero) { const feeUSD = fee.fee.mul(feeTokenUSD.instant); const feePct = feeUSD.div(node.usdIn).mul(100).toNumber(); route.exchangeFee = feeUSD.toNumber(); diff --git a/src/lib/Swap/price-cache.ts b/src/lib/Swap/price-cache.ts index 9c98ec773..48bf8f194 100644 --- a/src/lib/Swap/price-cache.ts +++ b/src/lib/Swap/price-cache.ts @@ -8,7 +8,7 @@ import { resolveChainId } from "@/utils/chain"; import { getTokenIndex, tokensEqual } from "@/utils/token"; import { Token } from "@/utils/types"; import { multicall } from "@wagmi/core"; -import { ContractFunctionParameters } from "viem"; +import { ContractFunctionParameters, MulticallReturnType } from "viem"; import { readContract } from "viem/actions"; const TWO_MINS = 1000 * 60 * 2; @@ -77,21 +77,21 @@ export class SwapPriceCache { const diff = now - this.#lastFetchedTimestamp; if (force || diff > TWO_MINS) { - const { priceMap, wellPriceMap } = await this.#fetchPrices(); + const { priceMap, wellPriceMap } = await this.fetchPrices(); this.#priceMap = priceMap; this.#wellPriceMap = wellPriceMap; this.#lastFetchedTimestamp = now; } } - async #fetchPrices() { - const { tokens, contracts, price: priceContract } = this.#buildMulticall(); + private async fetchPrices() { + const { tokens, contracts, price: priceContract } = this.buildMulticall(); const [prices, redemptionForOneMainToken] = await Promise.all([ // all token prices multicall(this.#context.config, { contracts: [...contracts, priceContract], - allowFailure: false, + allowFailure: true, }), // silo wrapped token redemption for 1 main token readContract(this.#context.config.getClient({ chainId: this.#context.chainId }), { @@ -100,35 +100,47 @@ export class SwapPriceCache { functionName: "previewRedeem", args: [BigInt(10 ** this.#context.siloWrappedToken.decimals)], }), + // multicall(this.#context.config, { + // contracts: this.tokens + // }) ]); const priceMap = new Map(); const wellPriceMap = new Map(); - for (const [index, price] of prices.entries()) { - const token = tokens[index]; + // price result is the last element in the prices array + const priceResult = prices[prices.length - 1]; + + const tokenPrices = [...prices].slice(0, contracts.length); + + // set the token prices into the map + for (const [index, token] of tokens.entries()) { + const price = tokenPrices[index]; + const result = price.result; + + if (result && typeof result === "bigint") { + priceMap.set(token, TV.fromBigInt(result, 6)); - if (typeof price === "bigint") { if (token.isWrappedNative) { - priceMap.set(this.#context.native, TV.fromBlockchain(price, 6)); + priceMap.set(this.#context.native, TV.fromBlockchain(result, 6)); } - const mainTokenPrice = TV.fromBlockchain(price, 6); - priceMap.set(token, mainTokenPrice); - priceMap.set(this.#context.siloWrappedToken, mainTokenPrice); - } else if (price.price) { - priceMap.set(this.#context.mainToken, TV.fromBlockchain(price.price, 6)); - - price.ps.forEach((pool) => { - const well = this.#context.tokenMap[getTokenIndex(pool.pool)]; - if (well) { - wellPriceMap.set(well, TV.fromBlockchain(pool.price, 6)); - priceMap.set(well, TV.fromBlockchain(pool.lpUsd, 6)); - } - }); } } - this.#updatePriceMapWithSiloWrappedToken(redemptionForOneMainToken, priceMap); + if (priceResult.result && typeof priceResult.result === "object") { + const result = priceResult.result; + priceMap.set(this.#context.mainToken, TV.fromBlockchain(result.price, 6)); + + result.ps.forEach((pool) => { + const well = this.#context.tokenMap[getTokenIndex(pool.pool)]; + if (well) { + wellPriceMap.set(well, TV.fromBlockchain(pool.price, 6)); + priceMap.set(well, TV.fromBlockchain(pool.lpUsd, 6)); + } + }); + } + + this.updatePriceMapWithSiloWrappedToken(redemptionForOneMainToken, priceMap); return { priceMap, @@ -136,7 +148,7 @@ export class SwapPriceCache { }; } - #buildMulticall() { + private buildMulticall() { const tokens = Object.keys(this.#context.underlying2LP).map((token) => { return this.#context.tokenMap[getTokenIndex(token)]; }); @@ -156,22 +168,14 @@ export class SwapPriceCache { args: [], }; - const siloWrappedExchangeRate: ContractFunctionParameters = { - address: this.#context.siloWrappedToken.address, - abi: siloedPintoABI, - functionName: "previewRedeem", - args: [BigInt(10 ** this.#context.siloWrappedToken.decimals)], - }; - return { tokens, contracts: contracts, - siloWrappedExchangeRate, price, }; } - #updatePriceMapWithSiloWrappedToken(amount: bigint, priceMap: Map): void { + private updatePriceMapWithSiloWrappedToken(amount: bigint, priceMap: Map): void { const baseAmount = TV.fromHuman(1, this.#context.mainToken.decimals); const redemptionAmount = TV.fromBigInt(amount, this.#context.mainToken.decimals); const mainTokenUSD = priceMap.get(this.#context.mainToken); From d3d4606c9052c87fbb72cc23a1072b84e11e005b Mon Sep 17 00:00:00 2001 From: burr Date: Wed, 19 Nov 2025 22:48:56 +0900 Subject: [PATCH 09/24] feat: remove unnecessary comment --- src/lib/Swap/price-cache.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/lib/Swap/price-cache.ts b/src/lib/Swap/price-cache.ts index 48bf8f194..ef8f7a649 100644 --- a/src/lib/Swap/price-cache.ts +++ b/src/lib/Swap/price-cache.ts @@ -100,9 +100,6 @@ export class SwapPriceCache { functionName: "previewRedeem", args: [BigInt(10 ** this.#context.siloWrappedToken.decimals)], }), - // multicall(this.#context.config, { - // contracts: this.tokens - // }) ]); const priceMap = new Map(); From 05577185503c4b3457d8bf33651d88b15e29585d Mon Sep 17 00:00:00 2001 From: burr Date: Mon, 24 Nov 2025 23:02:33 +0900 Subject: [PATCH 10/24] feat: add new token icons --- src/assets/tokens/PINTO_wstETH.png | Bin 0 -> 21117 bytes src/assets/tokens/wstETH.png | Bin 0 -> 17422 bytes src/assets/tokens/wstETH.svg | 11 ----------- 3 files changed, 11 deletions(-) create mode 100644 src/assets/tokens/PINTO_wstETH.png create mode 100644 src/assets/tokens/wstETH.png delete mode 100644 src/assets/tokens/wstETH.svg diff --git a/src/assets/tokens/PINTO_wstETH.png b/src/assets/tokens/PINTO_wstETH.png new file mode 100644 index 0000000000000000000000000000000000000000..2e4402765b424df44becb9a6c956048ed2ea8435 GIT binary patch literal 21117 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
=z3(*dw;1m$|EkMA-#5??si_2vM<*bfj-lIb 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 UuZ)MHp8x;=07*qoM6N<$f`}))oB#j- literal 0 HcmV?d00001 diff --git a/src/assets/tokens/wstETH.png b/src/assets/tokens/wstETH.png new file mode 100644 index 0000000000000000000000000000000000000000..f7772f6dc32e0f1b7b48d3c93f8cf0d4e97cb0aa GIT binary patch literal 17422 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 literal 0 HcmV?d00001 diff --git a/src/assets/tokens/wstETH.svg b/src/assets/tokens/wstETH.svg deleted file mode 100644 index 552ceaa09..000000000 --- a/src/assets/tokens/wstETH.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - From df925cdb64ac1dd9ea0c052b5452743279cf63b5 Mon Sep 17 00:00:00 2001 From: burr Date: Mon, 24 Nov 2025 23:59:38 +0900 Subject: [PATCH 11/24] feat: update silo convert quoteing --- src/constants/tokens.ts | 7 +- src/lib/siloConvert/SiloConvert.ts | 81 +++++++++++-------- src/lib/siloConvert/siloConvert.swapQuoter.ts | 2 +- 3 files changed, 52 insertions(+), 38 deletions(-) diff --git a/src/constants/tokens.ts b/src/constants/tokens.ts index 7723801d4..82686f977 100644 --- a/src/constants/tokens.ts +++ b/src/constants/tokens.ts @@ -5,13 +5,14 @@ import pintoWethIcon from "@/assets/tokens/PINTO_WETH.png"; import pintoWsolIcon from "@/assets/tokens/PINTO_WSOL.png"; import pintoCbbtcIcon from "@/assets/tokens/PINTO_cbBTC.png"; import pintoCbethIcon from "@/assets/tokens/PINTO_cbETH.png"; +import pintoWstethIcon from "@/assets/tokens/PINTO_wstETH.png"; import spectrasPintoLPIcon from "@/assets/tokens/SPECTRA-sPINTO-LP.png"; import spectrasPintoPTIcon from "@/assets/tokens/SPECTRA-sPINTO-PT.png"; import spectrasPintoYTIcon from "@/assets/tokens/SPECTRA-sPINTO-YT.png"; import wsolIcon from "@/assets/tokens/WSOL.png"; import crsPintoIcon from "@/assets/tokens/crsPINTO.png"; import sPintoIcon from "@/assets/tokens/sPINTO.png"; -import wstETHIcon from "@/assets/tokens/wstETH.svg"; +import wstETHIcon from "@/assets/tokens/wstETH.png"; import { Token, Token3PIntegration } from "@/utils/types"; import { ChainLookup } from "@/utils/types.generic"; import { arbitrum, base } from "viem/chains"; @@ -204,12 +205,12 @@ export const PINTO_WSTETH_TOKEN: ChainLookup = { chainId: base.id, name: "PINTOWSTETH LP", symbol: "PINTOWSTETH", - address: "0x3e1155480Fce43686793dd18E43104A01abCBC92", // temp address + address: "0x3e1155245FF9a6a019Bc35827e801c6ED2CE91b9", // temp address decimals: 18, displayDecimals: 2, isLP: true, isWhitelisted: true, - logoURI: pintoWsolIcon, + logoURI: pintoWstethIcon, color: "#00A3FF", tokens: [MAIN_TOKEN[base.id].address, WSTETH_TOKEN[base.id].address], ...defaultLPTokenIndicies, diff --git a/src/lib/siloConvert/SiloConvert.ts b/src/lib/siloConvert/SiloConvert.ts index a5968fddc..48f8594a5 100644 --- a/src/lib/siloConvert/SiloConvert.ts +++ b/src/lib/siloConvert/SiloConvert.ts @@ -295,28 +295,43 @@ export class SiloConvert { throw e; }); - const simulationsRawResults = await Promise.all( - quotedRoutes.map((route) => - route.workflow - .simulate({ - account: this.context.account, - after: this.priceCache.constructPriceAdvPipe({ noTokenPrices: true }), - }) - .catch((e) => { - logError("[SiloConvert/quote] FAILED to simulate routes : ", e); - throw new SimulationError("quote", e instanceof Error ? e.message : "Unknown error", { - routes, - quotedRoutes, - }); - }) - .then((r) => { - console.debug("[SiloConvert/quote] simulated route!: ", route, r); - return r; - }), - ), - ); + const runSimulate = (getPrices: boolean, warn?: boolean) => { + return Promise.all( + quotedRoutes.map((route) => + route.workflow + .simulate({ + account: this.context.account, + after: getPrices ? this.priceCache.constructPriceAdvPipe({ noTokenPrices: true }) : undefined, + }) + .catch((e) => { + logError("[SiloConvert/quote] FAILED to simulate routes : ", e, warn); + throw new SimulationError("quote", e instanceof Error ? e.message : "Unknown error", { + routes, + quotedRoutes, + }); + }) + .then((r) => { + console.debug("[SiloConvert/quote] simulated route!: ", route, r); + return r; + }), + ), + ); + }; + + let didSucceedWithPrices = true; + + const simulationsRawResults = await runSimulate(true, true).catch((e) => { + didSucceedWithPrices = false; + logError("[SiloConvert/quote] RETRYING to simulate routes w/o prices: ", e, true); + return runSimulate(false, false).catch((e) => { + throw new SimulationError("quote", e instanceof Error ? e.message : "Unknown error", { + routes, + quotedRoutes, + }); + }); + }); - console.debug("[SiloConvert/quote] post simulation results: ", { + console.log("[SiloConvert/quote] post simulation results: ", { quotedRoutes, simulationsRawResults, }); @@ -333,7 +348,7 @@ export class SiloConvert { let decoded: ReturnType; try { - decoded = this.decodeRouteAndPriceResults(staticCallResult, route.route); + decoded = this.decodeRouteAndPriceResults(staticCallResult, route.route, didSucceedWithPrices); } catch (e) { logError("[SiloConvert/quote] FAILED to decode route and price results: ", e); throw new ConversionQuotationError("Failed to decode route and price results", { @@ -389,24 +404,22 @@ export class SiloConvert { private decodeRouteAndPriceResults( rawResponse: HashString[], route: SiloConvertRoute, + priceCallSuccess: boolean, ): Pick, "results" | "reducedResults" | "postPriceData"> { const mainToken = getChainConstant(this.context.chainId, MAIN_TOKEN); try { const staticCallResult = [...rawResponse]; // price result is the last element in the static call result - const priceResult = staticCallResult.pop(); - - console.log({ - priceResult, - }); + const priceResult = !priceCallSuccess ? undefined : staticCallResult.pop(); const decodedConvertResults = decodeConvertResults(staticCallResult, route.convertType); const decodedPriceCalls = priceResult ? AdvancedPipeWorkflow.decodeResult(priceResult) : undefined; - const postPriceData = decodedPriceCalls?.length - ? this.priceCache.decodePriceCallResults([...decodedPriceCalls]) - : undefined; + const postPriceData = + decodedPriceCalls?.length && priceCallSuccess + ? this.priceCache.decodePriceCallResults([...decodedPriceCalls]) + : undefined; return { postPriceData, @@ -462,12 +475,12 @@ export class SiloConvert { } } -function logError(prefix: string, e: unknown) { +function logError(prefix: string, e: unknown, warn?: boolean) { if (e instanceof SiloConvertError) { - console.error(prefix, e.toLogObject()); + console[warn ? "warn" : "error"](prefix, e.toLogObject()); } else if (e instanceof Error) { - console.error(prefix, e.message); + console[warn ? "warn" : "error"](prefix, e.message); } else { - console.error("[SiloConvert] Unknown error: ", e); + console[warn ? "warn" : "error"]("[SiloConvert] Unknown error: ", e); } } diff --git a/src/lib/siloConvert/siloConvert.swapQuoter.ts b/src/lib/siloConvert/siloConvert.swapQuoter.ts index 3e722c120..86f047851 100644 --- a/src/lib/siloConvert/siloConvert.swapQuoter.ts +++ b/src/lib/siloConvert/siloConvert.swapQuoter.ts @@ -81,7 +81,7 @@ export class SiloConvertSwapQuoter { const fee = quote.fees?.zeroExFee; - if (fee) { + if (fee && !usdIn.isZero && !usdOut.isZero) { const feeToken = stringEq(fee.token, sellToken.address) ? sellToken : buyToken; const feeAmount = TV.fromBlockchain(fee.amount, feeToken.decimals); const feeTokenUSD = tokensEqual(feeToken, sellToken) ? sellTokenUSD : buyTokenUSD; From 649332e305c017434f708cdb44253e8d5ded8ad9 Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Wed, 26 Nov 2025 00:44:51 -0500 Subject: [PATCH 12/24] Wsteth market performance wip --- .../charts/MarketPerformanceChart.tsx | 2 +- .../queries/useSeasonalMarketPerformance.ts | 75 ++++++++++++------- 2 files changed, 47 insertions(+), 30 deletions(-) diff --git a/src/components/charts/MarketPerformanceChart.tsx b/src/components/charts/MarketPerformanceChart.tsx index 75c2c8abe..9fd25e7d6 100644 --- a/src/components/charts/MarketPerformanceChart.tsx +++ b/src/components/charts/MarketPerformanceChart.tsx @@ -165,7 +165,7 @@ const MarketPerformanceChart = ({ season, size, className }: MarketPerformanceCh const chartData: LineChartData[] = []; const tokens: (Token | undefined)[] = []; const chartStrokeGradients: StrokeGradientFunction[] = []; - for (const token of ["NET", "WETH", "cbETH", "cbBTC", "WSOL"]) { + for (const token of ["NET", "WETH", "cbETH", "wstETH", "cbBTC", "WSOL"]) { for (let i = 0; i < allData[token].length; i++) { chartData[i] ??= { timestamp: allData[token][i].timestamp, diff --git a/src/state/seasonal/queries/useSeasonalMarketPerformance.ts b/src/state/seasonal/queries/useSeasonalMarketPerformance.ts index 75ba5d270..f5b408c5b 100644 --- a/src/state/seasonal/queries/useSeasonalMarketPerformance.ts +++ b/src/state/seasonal/queries/useSeasonalMarketPerformance.ts @@ -120,8 +120,18 @@ export function useMarketPerformanceCalc( const responseData = useMemo(() => { const result: SeasonalMarketPerformanceChartData = {}; if (seasonalData) { - for (let i = 0; i < seasonalData.length; ++i) { - const season = seasonalData[i]; + const test = [ + ...seasonalData, + { + ...seasonalData[0], + percentChange: [...(seasonalData[0].percentChange as string[]), "0.01"], + totalPercentChange: seasonalData[0].totalPercentChange, + usdChange: [...(seasonalData[0].usdChange as string[]), "12345"], + totalUsdChange: seasonalData[0].totalUsdChange, + }, + ]; + for (let i = 0; i < test.length; ++i) { + const season = test[i]; if (chartType !== SMPChartType.TOKEN_PRICES) { if (season.season <= (startSeasons.NET ?? 0)) { continue; @@ -149,6 +159,10 @@ export function useMarketPerformanceCalc( }); } + if (i === test.length - 1) { + console.log("abc", season, test[i - 1]); + } + let tokenIdx = 0; for (const token of season.silo.allWhitelistedTokens) { // Skip Pinto token @@ -167,35 +181,38 @@ export function useMarketPerformanceCalc( } if (chartType !== SMPChartType.TOKEN_PRICES) { - result[symbol] ??= [ - { - season: season.season - 1, - value: 0, - timestamp: new Date((Number(season.timestamp) - 60 * 60) * 1000), - }, - ]; - const arr = result[symbol]; + const seasonValue = season[CHART_FIELDS[chartType % 2][0]][tokenIdx] ?? null; + if (!!seasonValue) { + result[symbol] ??= [ + { + season: season.season - 1, + value: 0, + timestamp: new Date((Number(season.timestamp) - 60 * 60) * 1000), + }, + ]; + const arr = result[symbol]; - const value = - chartType < SMPChartType.USD_CUMULATIVE - ? Number(season[CHART_FIELDS[chartType % 2][0]][tokenIdx]) - : accumulator(chartType)( - arr[arr.length - 1].value, - Number(season[CHART_FIELDS[chartType % 2][0]][tokenIdx]), - ); - arr.push({ - season: season.season, - value, - timestamp: new Date(Number(season.timestamp) * 1000), - }); + const value = + chartType < SMPChartType.USD_CUMULATIVE + ? Number(seasonValue) + : accumulator(chartType)(arr[arr.length - 1].value, Number(seasonValue)); + arr.push({ + season: season.season, + value, + timestamp: new Date(Number(season.timestamp) * 1000), + }); + } } else { - result[symbol] ??= []; - result[symbol].push({ - season: season.season, - // biome-ignore lint/style/noNonNullAssertion: can't be null given only valid=true is retrieved from sg. - value: Number(season.thisSeasonTokenUsdPrices![tokenIdx]), - timestamp: new Date(Number(season.timestamp) * 1000), - }); + // biome-ignore lint/style/noNonNullAssertion: can't be null given only valid=true is retrieved from sg. + const seasonValue = season.thisSeasonTokenUsdPrices![tokenIdx] ?? null; + if (!!seasonValue) { + result[symbol] ??= []; + result[symbol].push({ + season: season.season, + value: Number(seasonValue), + timestamp: new Date(Number(season.timestamp) * 1000), + }); + } } ++tokenIdx; } From b79231d17de586d1b2385d5522fee99f0582292f Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Thu, 27 Nov 2025 01:14:36 -0500 Subject: [PATCH 13/24] Arranges newly appearing datapoints --- src/components/charts/LineChart.tsx | 12 +++++-- .../charts/MarketPerformanceChart.tsx | 34 ++++++++++++++---- .../queries/useSeasonalMarketPerformance.ts | 35 ++++++++++++------- 3 files changed, 59 insertions(+), 22 deletions(-) diff --git a/src/components/charts/LineChart.tsx b/src/components/charts/LineChart.tsx index 75430e6a8..3dc32f676 100644 --- a/src/components/charts/LineChart.tsx +++ b/src/components/charts/LineChart.tsx @@ -18,7 +18,7 @@ import { LineChartHorizontalReferenceLine, plugins } from "./chartHelpers"; Chart.register(LineController, LineElement, LinearScale, LogarithmicScale, CategoryScale, PointElement, Filler); export type LineChartData = { - values: number[]; + values: Array; } & Record; export type MakeGradientFunction = ( @@ -96,8 +96,14 @@ const LineChart = React.memo( const [yTickMin, yTickMax] = useMemo(() => { // Otherwise calculate based on data - const maxData = data.reduce((acc, next) => Math.max(acc, ...next.values), Number.MIN_SAFE_INTEGER); - const minData = data.reduce((acc, next) => Math.min(acc, ...next.values), Number.MAX_SAFE_INTEGER); + const maxData = data.reduce( + (acc, next) => Math.max(acc, ...next.values.filter((v): v is number => v !== null)), + Number.MIN_SAFE_INTEGER, + ); + const minData = data.reduce( + (acc, next) => Math.min(acc, ...next.values.filter((v): v is number => v !== null)), + Number.MAX_SAFE_INTEGER, + ); const maxTick = maxData === minData && maxData === 0 ? 1 : maxData; let minTick = minData - (maxData - minData) * 0.1; diff --git a/src/components/charts/MarketPerformanceChart.tsx b/src/components/charts/MarketPerformanceChart.tsx index 9fd25e7d6..8fffdcbf6 100644 --- a/src/components/charts/MarketPerformanceChart.tsx +++ b/src/components/charts/MarketPerformanceChart.tsx @@ -165,24 +165,41 @@ const MarketPerformanceChart = ({ season, size, className }: MarketPerformanceCh const chartData: LineChartData[] = []; const tokens: (Token | undefined)[] = []; const chartStrokeGradients: StrokeGradientFunction[] = []; - for (const token of ["NET", "WETH", "cbETH", "wstETH", "cbBTC", "WSOL"]) { + const allTokens = ["NET", "WETH", "cbETH", "wstETH", "cbBTC", "WSOL"]; + const tokensPresent: string[] = []; + for (const token of allTokens) { + if (allData[token].length > 0) { + tokensPresent.push(token); + } + } + for (const token of tokensPresent) { + const missingDatapoints = allData.NET.length - allData[token].length; for (let i = 0; i < allData[token].length; i++) { - chartData[i] ??= { + chartData[i + missingDatapoints] ??= { timestamp: allData[token][i].timestamp, values: [], }; if (dataType !== DataType.PRICE) { - chartData[i].values.push(allData[token][i].value); + chartData[i + missingDatapoints].values.push(allData[token][i].value); } else { - chartData[i].values.push( + chartData[i + missingDatapoints].values.push( transformValue(allData[token][i].value, minValues[token], maxValues[token], priceTransformRanges[token]), ); } } + for (let i = 0; i < missingDatapoints; i++) { + // Datapoint was missing for this token but present for other tokens + chartData[i].values.push(null); + } const tokenObj = tokenConfig.find((t) => t.symbol === token); tokens.push(tokenObj); chartStrokeGradients.push(gradientFunctions.solid(tokenObj?.color ?? "green")); } + console.log("abc cd", { + chartData, + tokens, + chartStrokeGradients, + }); return { chartData, tokens, @@ -287,10 +304,12 @@ const MarketPerformanceChart = ({ season, size, className }: MarketPerformanceCh
- Season {allData.NET[displayIndex].season} + {/* TODO(pp): revisit this ?. it only crashes on hovering at the end */} + Season {allData.NET[displayIndex]?.season ?? 12345643}
- {formatDate(allData.NET[displayIndex].timestamp)} + {/* TODO(pp): revisit this &&, ?. it only crashes on hovering at the end */} + {allData.NET[displayIndex]?.timestamp && formatDate(allData.NET[displayIndex]?.timestamp)}
@@ -324,7 +343,8 @@ const MarketPerformanceChart = ({ season, size, className }: MarketPerformanceCh
{tokenSymbol === "NET" && "Total: "}

- {displayValueFormatter(allData[tokenSymbol][displayIndex].value)} + {/* TODO(pp): revisit this ?. */} + {displayValueFormatter(allData[tokenSymbol][displayIndex]?.value)}

{idx < Object.keys(allData).length - 1 && ( diff --git a/src/state/seasonal/queries/useSeasonalMarketPerformance.ts b/src/state/seasonal/queries/useSeasonalMarketPerformance.ts index f5b408c5b..5f33b65cc 100644 --- a/src/state/seasonal/queries/useSeasonalMarketPerformance.ts +++ b/src/state/seasonal/queries/useSeasonalMarketPerformance.ts @@ -120,16 +120,30 @@ export function useMarketPerformanceCalc( const responseData = useMemo(() => { const result: SeasonalMarketPerformanceChartData = {}; if (seasonalData) { + /// TODO(pp): replace test with seasonalData when done testing this mock data const test = [ - ...seasonalData, + ...seasonalData.slice(0, -1), { - ...seasonalData[0], - percentChange: [...(seasonalData[0].percentChange as string[]), "0.01"], - totalPercentChange: seasonalData[0].totalPercentChange, - usdChange: [...(seasonalData[0].usdChange as string[]), "12345"], - totalUsdChange: seasonalData[0].totalUsdChange, + ...seasonalData[seasonalData.length - 1], + season: seasonalData[seasonalData.length - 1].season, + silo: { + ...seasonalData[seasonalData.length - 1].silo, + allWhitelistedTokens: [ + ...seasonalData[seasonalData.length - 1].silo.allWhitelistedTokens, + "0x3e1155245ff9a6a019bc35827e801c6ed2ce91b9", + ], + }, + percentChange: [...(seasonalData[seasonalData.length - 1].percentChange as string[]), "0.01"], + totalPercentChange: seasonalData[seasonalData.length - 1].totalPercentChange, + usdChange: [...(seasonalData[seasonalData.length - 1].usdChange as string[]), "12345"], + totalUsdChange: seasonalData[seasonalData.length - 1].totalUsdChange, + thisSeasonTokenUsdPrices: [ + ...(seasonalData[seasonalData.length - 1].thisSeasonTokenUsdPrices as string[]), + "3000", + ], }, ]; + /// for (let i = 0; i < test.length; ++i) { const season = test[i]; if (chartType !== SMPChartType.TOKEN_PRICES) { @@ -159,10 +173,6 @@ export function useMarketPerformanceCalc( }); } - if (i === test.length - 1) { - console.log("abc", season, test[i - 1]); - } - let tokenIdx = 0; for (const token of season.silo.allWhitelistedTokens) { // Skip Pinto token @@ -181,7 +191,7 @@ export function useMarketPerformanceCalc( } if (chartType !== SMPChartType.TOKEN_PRICES) { - const seasonValue = season[CHART_FIELDS[chartType % 2][0]][tokenIdx] ?? null; + const seasonValue = season[CHART_FIELDS[chartType % 2][0]][tokenIdx]; if (!!seasonValue) { result[symbol] ??= [ { @@ -204,7 +214,7 @@ export function useMarketPerformanceCalc( } } else { // biome-ignore lint/style/noNonNullAssertion: can't be null given only valid=true is retrieved from sg. - const seasonValue = season.thisSeasonTokenUsdPrices![tokenIdx] ?? null; + const seasonValue = season.thisSeasonTokenUsdPrices![tokenIdx]; if (!!seasonValue) { result[symbol] ??= []; result[symbol].push({ @@ -220,6 +230,7 @@ export function useMarketPerformanceCalc( } return result; }, [seasonalData, chartType, startSeasons, mainToken.address, lpToUnderlyingMap]); + return responseData; } From a03c2219119c815fd9a6e61405a2f34b21ce0386 Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Fri, 28 Nov 2025 13:38:53 -0500 Subject: [PATCH 14/24] Fix incorrect selected value display --- .../charts/MarketPerformanceChart.tsx | 11 +++++----- .../queries/useSeasonalMarketPerformance.ts | 21 ++++++++++++++++++- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/components/charts/MarketPerformanceChart.tsx b/src/components/charts/MarketPerformanceChart.tsx index 8fffdcbf6..b04d99b91 100644 --- a/src/components/charts/MarketPerformanceChart.tsx +++ b/src/components/charts/MarketPerformanceChart.tsx @@ -304,12 +304,10 @@ const MarketPerformanceChart = ({ season, size, className }: MarketPerformanceCh
- {/* TODO(pp): revisit this ?. it only crashes on hovering at the end */} - Season {allData.NET[displayIndex]?.season ?? 12345643} + Season {allData.NET[displayIndex].season}
- {/* TODO(pp): revisit this &&, ?. it only crashes on hovering at the end */} - {allData.NET[displayIndex]?.timestamp && formatDate(allData.NET[displayIndex]?.timestamp)} + {formatDate(allData.NET[displayIndex].timestamp)}
@@ -317,6 +315,8 @@ const MarketPerformanceChart = ({ season, size, className }: MarketPerformanceCh {chartDataset.tokens.map((token, idx) => { const tokenSymbol = token?.symbol ?? "NET"; const isNetToken = tokenSymbol === "NET"; + const missingDatapoints = allData.NET.length - allData[tokenSymbol].length; + const value = allData[tokenSymbol][displayIndex - missingDatapoints]?.value; return (
{tokenSymbol === "NET" && "Total: "}

- {/* TODO(pp): revisit this ?. */} - {displayValueFormatter(allData[tokenSymbol][displayIndex]?.value)} + {!!value ? displayValueFormatter(value) : "-"}

{idx < Object.keys(allData).length - 1 && ( diff --git a/src/state/seasonal/queries/useSeasonalMarketPerformance.ts b/src/state/seasonal/queries/useSeasonalMarketPerformance.ts index 5f33b65cc..e6350d86c 100644 --- a/src/state/seasonal/queries/useSeasonalMarketPerformance.ts +++ b/src/state/seasonal/queries/useSeasonalMarketPerformance.ts @@ -122,7 +122,26 @@ export function useMarketPerformanceCalc( if (seasonalData) { /// TODO(pp): replace test with seasonalData when done testing this mock data const test = [ - ...seasonalData.slice(0, -1), + ...seasonalData.slice(0, -2), + { + ...seasonalData[seasonalData.length - 2], + season: seasonalData[seasonalData.length - 2].season, + silo: { + ...seasonalData[seasonalData.length - 2].silo, + allWhitelistedTokens: [ + ...seasonalData[seasonalData.length - 2].silo.allWhitelistedTokens, + "0x3e1155245ff9a6a019bc35827e801c6ed2ce91b9", + ], + }, + percentChange: [...(seasonalData[seasonalData.length - 2].percentChange as string[]), "0.09"], + totalPercentChange: seasonalData[seasonalData.length - 2].totalPercentChange, + usdChange: [...(seasonalData[seasonalData.length - 2].usdChange as string[]), "10000"], + totalUsdChange: seasonalData[seasonalData.length - 2].totalUsdChange, + thisSeasonTokenUsdPrices: [ + ...(seasonalData[seasonalData.length - 2].thisSeasonTokenUsdPrices as string[]), + "2900", + ], + }, { ...seasonalData[seasonalData.length - 1], season: seasonalData[seasonalData.length - 1].season, From 97423bb03eef31026ba90ea4b6e46efcb9ff3edb Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Fri, 28 Nov 2025 13:50:57 -0500 Subject: [PATCH 15/24] Proper undefined check --- .../charts/MarketPerformanceChart.tsx | 2 +- .../queries/useSeasonalMarketPerformance.ts | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/components/charts/MarketPerformanceChart.tsx b/src/components/charts/MarketPerformanceChart.tsx index b04d99b91..c77d4e628 100644 --- a/src/components/charts/MarketPerformanceChart.tsx +++ b/src/components/charts/MarketPerformanceChart.tsx @@ -343,7 +343,7 @@ const MarketPerformanceChart = ({ season, size, className }: MarketPerformanceCh
{tokenSymbol === "NET" && "Total: "}

- {!!value ? displayValueFormatter(value) : "-"} + {typeof value === "number" ? displayValueFormatter(value) : "-"}

{idx < Object.keys(allData).length - 1 && ( diff --git a/src/state/seasonal/queries/useSeasonalMarketPerformance.ts b/src/state/seasonal/queries/useSeasonalMarketPerformance.ts index e6350d86c..45a02f8d3 100644 --- a/src/state/seasonal/queries/useSeasonalMarketPerformance.ts +++ b/src/state/seasonal/queries/useSeasonalMarketPerformance.ts @@ -123,6 +123,25 @@ export function useMarketPerformanceCalc( /// TODO(pp): replace test with seasonalData when done testing this mock data const test = [ ...seasonalData.slice(0, -2), + { + ...seasonalData[seasonalData.length - 3], + season: seasonalData[seasonalData.length - 3].season, + silo: { + ...seasonalData[seasonalData.length - 3].silo, + allWhitelistedTokens: [ + ...seasonalData[seasonalData.length - 3].silo.allWhitelistedTokens, + "0x3e1155245ff9a6a019bc35827e801c6ed2ce91b9", + ], + }, + percentChange: [...(seasonalData[seasonalData.length - 3].percentChange as string[]), "-0.02"], + totalPercentChange: seasonalData[seasonalData.length - 3].totalPercentChange, + usdChange: [...(seasonalData[seasonalData.length - 3].usdChange as string[]), "-500"], + totalUsdChange: seasonalData[seasonalData.length - 3].totalUsdChange, + thisSeasonTokenUsdPrices: [ + ...(seasonalData[seasonalData.length - 3].thisSeasonTokenUsdPrices as string[]), + "2700", + ], + }, { ...seasonalData[seasonalData.length - 2], season: seasonalData[seasonalData.length - 2].season, From 9156f56ff3296695855b3b4774ce53294d61c3dc Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Fri, 28 Nov 2025 13:59:50 -0500 Subject: [PATCH 16/24] Wsteth added to big chart --- src/state/useChartSetupData.ts | 45 +++++++++++++++++++++++++++++++++- src/state/useSeasonsData.ts | 14 +++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/state/useChartSetupData.ts b/src/state/useChartSetupData.ts index 6f5412a4c..5ff6bab94 100644 --- a/src/state/useChartSetupData.ts +++ b/src/state/useChartSetupData.ts @@ -2,7 +2,7 @@ import pintoExchangeLogo from "@/assets/misc/pinto-exchange-logo.svg"; import podIcon from "@/assets/protocol/Pod.png"; import stalkIcon from "@/assets/protocol/Stalk.png"; import { TokenValue } from "@/classes/TokenValue"; -import { CBBTC_TOKEN, CBETH_TOKEN, WETH_TOKEN, WSOL_TOKEN } from "@/constants/tokens"; +import { CBBTC_TOKEN, CBETH_TOKEN, WETH_TOKEN, WSOL_TOKEN, WSTETH_TOKEN } from "@/constants/tokens"; import { chartFormatters, formatNum, formatPct, formatUSD } from "@/utils/format"; import { Token } from "@/utils/types"; import { useMemo } from "react"; @@ -1137,6 +1137,15 @@ const createMarketCharts = (mainToken: Token): ChartSetupBase[] => { valueAxis: "ethPrice", formatter: usdFormatter, }), + marketEntry({ + id: "marketPriceWsteth", + name: "wstETH Price", + icon: WSTETH_TOKEN[mainToken.chainId].logoURI, + tooltipTitle: "wstEth Price", + description: "wstEth Price", + valueAxis: "ethPrice", + formatter: usdFormatter, + }), marketEntry({ id: "marketPriceCbbtc", name: "cbBTC Price", @@ -1180,6 +1189,15 @@ const createMarketCharts = (mainToken: Token): ChartSetupBase[] => { formatter: usdFormatter, inputOptions: "SEASON", }), + marketEntry({ + id: "marketCumulativeWstethUsd", + name: "Protocol Cumulative cbETH Value Change (USD)", + icon: WSTETH_TOKEN[mainToken.chainId].logoURI, + tooltipTitle: "Market: Cumulative wstETH Value Change", + description: "Change of wstETH liquidity USD value since a selectable starting season.", + formatter: usdFormatter, + inputOptions: "SEASON", + }), marketEntry({ id: "marketCumulativeCbbtcUsd", name: "Protocol Cumulative cbBTC Value Change (USD)", @@ -1222,6 +1240,14 @@ const createMarketCharts = (mainToken: Token): ChartSetupBase[] => { description: "Change of cbETH liquidity USD value by season.", formatter: usdFormatter, }), + marketEntry({ + id: "marketSeasonalWstethUsd", + name: "Protocol Seasonal wstETH Value Change (USD)", + icon: WSTETH_TOKEN[mainToken.chainId].logoURI, + tooltipTitle: "Market: Seasonal wstETH Value Change", + description: "Change of wstETH liquidity USD value by season.", + formatter: usdFormatter, + }), marketEntry({ id: "marketSeasonalCbbtcUsd", name: "Protocol Seasonal cbBTC Value Change (USD)", @@ -1265,6 +1291,15 @@ const createMarketCharts = (mainToken: Token): ChartSetupBase[] => { formatter: chartFormatters.percentFormatter(4), inputOptions: "SEASON", }), + marketEntry({ + id: "marketCumulativeWstethPercent", + name: "Protocol Cumulative wstETH Value Change (%)", + icon: WSTETH_TOKEN[mainToken.chainId].logoURI, + tooltipTitle: "Market: Cumulative wstETH Value Change", + description: "Percentage change of wstETH liquidity value since a selectable starting season.", + formatter: chartFormatters.percentFormatter(4), + inputOptions: "SEASON", + }), marketEntry({ id: "marketCumulativeCbbtcPercent", name: "Protocol Cumulative cbBTC Value Change (%)", @@ -1307,6 +1342,14 @@ const createMarketCharts = (mainToken: Token): ChartSetupBase[] => { description: "Percentage change of cbETH liquidity value by season.", formatter: chartFormatters.percentFormatter(4), }), + marketEntry({ + id: "marketSeasonalWstethPercent", + name: "Protocol Seasonal wstETH Value Change (%)", + icon: WSTETH_TOKEN[mainToken.chainId].logoURI, + tooltipTitle: "Market: Seasonal wstETH Value Change", + description: "Percentage change of wstETH liquidity value by season.", + formatter: chartFormatters.percentFormatter(4), + }), marketEntry({ id: "marketSeasonalCbbtcPercent", name: "Protocol Seasonal cbBTC Value Change (%)", diff --git a/src/state/useSeasonsData.ts b/src/state/useSeasonsData.ts index 7841ed50a..f7de85b34 100644 --- a/src/state/useSeasonsData.ts +++ b/src/state/useSeasonsData.ts @@ -130,26 +130,31 @@ export interface SeasonsTableData { inflowFieldDeltaVolume: number; marketPriceWeth: number; marketPriceCbeth: number; + marketPriceWsteth: number; marketPriceCbbtc: number; marketPriceWsol: number; marketCumulativeNonPintoUsd: number; marketCumulativeWethUsd: number; marketCumulativeCbethUsd: number; + marketCumulativeWstethUsd: number; marketCumulativeCbbtcUsd: number; marketCumulativeWsolUsd: number; marketSeasonalNonPintoUsd: number; marketSeasonalWethUsd: number; marketSeasonalCbethUsd: number; + marketSeasonalWstethUsd: number; marketSeasonalCbbtcUsd: number; marketSeasonalWsolUsd: number; marketCumulativeNonPintoPercent: number; marketCumulativeWethPercent: number; marketCumulativeCbethPercent: number; + marketCumulativeWstethPercent: number; marketCumulativeCbbtcPercent: number; marketCumulativeWsolPercent: number; marketSeasonalNonPintoPercent: number; marketSeasonalWethPercent: number; marketSeasonalCbethPercent: number; + marketSeasonalWstethPercent: number; marketSeasonalCbbtcPercent: number; marketSeasonalWsolPercent: number; } @@ -641,6 +646,7 @@ export default function useSeasonsData( if (marketPerformanceData && idx + syncOffset < countSubgraphSeasons) { allData.marketPriceWeth = marketPrices.WETH[marketPrices.WETH.length - 1 - idx - syncOffset].value; allData.marketPriceCbeth = marketPrices.cbETH[marketPrices.cbETH.length - 1 - idx - syncOffset].value; + allData.marketPriceWsteth = marketPrices.wstETH[marketPrices.wstETH.length - 1 - idx - syncOffset].value; allData.marketPriceCbbtc = marketPrices.cbBTC[marketPrices.cbBTC.length - 1 - idx - syncOffset].value; allData.marketPriceWsol = marketPrices.WSOL[marketPrices.WSOL.length - 1 - idx - syncOffset].value; allData.marketCumulativeNonPintoUsd = @@ -649,6 +655,8 @@ export default function useSeasonsData( marketUsdCumulative.WETH?.[marketUsdCumulative.WETH.length - 1 - idx - syncOffset]?.value; allData.marketCumulativeCbethUsd = marketUsdCumulative.cbETH?.[marketUsdCumulative.cbETH.length - 1 - idx - syncOffset]?.value; + allData.marketCumulativeWstethUsd = + marketUsdCumulative.wstETH?.[marketUsdCumulative.wstETH.length - 1 - idx - syncOffset]?.value; allData.marketCumulativeCbbtcUsd = marketUsdCumulative.cbBTC?.[marketUsdCumulative.cbBTC.length - 1 - idx - syncOffset]?.value; allData.marketCumulativeWsolUsd = @@ -659,6 +667,8 @@ export default function useSeasonsData( marketUsdSeasonal.WETH[marketUsdSeasonal.WETH.length - 1 - idx - syncOffset]?.value; allData.marketSeasonalCbethUsd = marketUsdSeasonal.cbETH[marketUsdSeasonal.cbETH.length - 1 - idx - syncOffset]?.value; + allData.marketSeasonalWstethUsd = + marketUsdSeasonal.wstETH[marketUsdSeasonal.wstETH.length - 1 - idx - syncOffset]?.value; allData.marketSeasonalCbbtcUsd = marketUsdSeasonal.cbBTC[marketUsdSeasonal.cbBTC.length - 1 - idx - syncOffset]?.value; allData.marketSeasonalWsolUsd = @@ -669,6 +679,8 @@ export default function useSeasonsData( marketPercentCumulative.WETH?.[marketPercentCumulative.WETH.length - 1 - idx - syncOffset]?.value; allData.marketCumulativeCbethPercent = marketPercentCumulative.cbETH?.[marketPercentCumulative.cbETH.length - 1 - idx - syncOffset]?.value; + allData.marketCumulativeWstethPercent = + marketPercentCumulative.wstETH?.[marketPercentCumulative.wstETH.length - 1 - idx - syncOffset]?.value; allData.marketCumulativeCbbtcPercent = marketPercentCumulative.cbBTC?.[marketPercentCumulative.cbBTC.length - 1 - idx - syncOffset]?.value; allData.marketCumulativeWsolPercent = @@ -679,6 +691,8 @@ export default function useSeasonsData( marketPercentSeasonal.WETH[marketPercentSeasonal.WETH.length - 1 - idx - syncOffset]?.value; allData.marketSeasonalCbethPercent = marketPercentSeasonal.cbETH[marketPercentSeasonal.cbETH.length - 1 - idx - syncOffset]?.value; + allData.marketSeasonalWstethPercent = + marketPercentSeasonal.wstETH[marketPercentSeasonal.wstETH.length - 1 - idx - syncOffset]?.value; allData.marketSeasonalCbbtcPercent = marketPercentSeasonal.cbBTC[marketPercentSeasonal.cbBTC.length - 1 - idx - syncOffset]?.value; allData.marketSeasonalWsolPercent = From 75e61aec22722b9c35b849121a2c5a197d4760a0 Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Fri, 28 Nov 2025 14:41:34 -0500 Subject: [PATCH 17/24] Correct sync offset --- .../queries/useSeasonalMarketPerformance.ts | 7 -- src/state/useSeasonsData.ts | 84 +++++++++---------- 2 files changed, 40 insertions(+), 51 deletions(-) diff --git a/src/state/seasonal/queries/useSeasonalMarketPerformance.ts b/src/state/seasonal/queries/useSeasonalMarketPerformance.ts index 45a02f8d3..9a09367dd 100644 --- a/src/state/seasonal/queries/useSeasonalMarketPerformance.ts +++ b/src/state/seasonal/queries/useSeasonalMarketPerformance.ts @@ -1,11 +1,4 @@ import { subgraphs } from "@/constants/subgraph"; -import { - PINTO_CBBTC_TOKEN, - PINTO_CBETH_TOKEN, - PINTO_USDC_TOKEN, - PINTO_WETH_TOKEN, - PINTO_WSOL_TOKEN, -} from "@/constants/tokens"; import { BeanstalkSeasonalMarketPerformanceDocument, BeanstalkSeasonalMarketPerformanceQuery, diff --git a/src/state/useSeasonsData.ts b/src/state/useSeasonsData.ts index f7de85b34..561d1f738 100644 --- a/src/state/useSeasonsData.ts +++ b/src/state/useSeasonsData.ts @@ -445,7 +445,8 @@ export default function useSeasonsData( 18, ).toNumber(); allData.deltaBeans = TokenValue.fromBlockchain(currStalkSeasons.deltaBeans, tokenData.mainToken.decimals); - allData.price = TokenValue.fromHuman(currStalkSeasons.price, 4); + // No sync offset needed for price + allData.price = TokenValue.fromHuman(stalkResults.seasons[idx].price, 4); allData.raining = currStalkSeasons.raining; allData.rewardBeans = TokenValue.fromHuman(currStalkSeasons.rewardBeans, 2); allData.deltaPodDemand = TokenValue.fromBlockchain(currFieldHourlySnapshots.deltaPodDemand, 18); @@ -508,14 +509,17 @@ export default function useSeasonsData( if (beanData && idx + syncOffset < countSubgraphSeasons) { const beanHourly = beanResults[idx + syncOffset].beanHourlySnapshot; allData.crosses = beanHourly.crosses; - allData.marketCap = Number(beanHourly.marketCap); allData.supply = TokenValue.fromBlockchain(beanHourly.supply, tokenData.mainToken.decimals); allData.supplyInPegLP = TokenValue.fromBlockchain(beanHourly.supply, tokenData.mainToken.decimals); - allData.instDeltaB = TokenValue.fromHuman(beanHourly.instDeltaB, tokenData.mainToken.decimals); - allData.instPrice = TokenValue.fromHuman(beanHourly.instPrice, tokenData.mainToken.decimals); - allData.l2sr = TokenValue.fromHuman(beanHourly.l2sr * 100, 2); - allData.twaDeltaB = TokenValue.fromHuman(beanHourly.twaDeltaB, 2); - allData.twaPrice = TokenValue.fromHuman(beanHourly.twaPrice, 4); + + // No sync offset needed for some of these + const beanHourlyNoSync = beanResults[idx].beanHourlySnapshot; + allData.marketCap = Number(beanHourlyNoSync.marketCap); + allData.l2sr = TokenValue.fromHuman(beanHourlyNoSync.l2sr * 100, 2); + allData.instPrice = TokenValue.fromHuman(beanHourlyNoSync.instPrice, tokenData.mainToken.decimals); + allData.instDeltaB = TokenValue.fromHuman(beanHourlyNoSync.instDeltaB, tokenData.mainToken.decimals); + allData.twaDeltaB = TokenValue.fromHuman(beanHourlyNoSync.twaDeltaB, 2); + allData.twaPrice = TokenValue.fromHuman(beanHourlyNoSync.twaPrice, 4); if (!allData.season) { const season = beanResults[idx].season; @@ -643,60 +647,52 @@ export default function useSeasonsData( } } - if (marketPerformanceData && idx + syncOffset < countSubgraphSeasons) { + if (marketPerformanceData && idx < countSubgraphSeasons) { allData.marketPriceWeth = marketPrices.WETH[marketPrices.WETH.length - 1 - idx - syncOffset].value; - allData.marketPriceCbeth = marketPrices.cbETH[marketPrices.cbETH.length - 1 - idx - syncOffset].value; - allData.marketPriceWsteth = marketPrices.wstETH[marketPrices.wstETH.length - 1 - idx - syncOffset].value; - allData.marketPriceCbbtc = marketPrices.cbBTC[marketPrices.cbBTC.length - 1 - idx - syncOffset].value; - allData.marketPriceWsol = marketPrices.WSOL[marketPrices.WSOL.length - 1 - idx - syncOffset].value; + allData.marketPriceCbeth = marketPrices.cbETH[marketPrices.cbETH.length - 1 - idx].value; + allData.marketPriceWsteth = marketPrices.wstETH[marketPrices.wstETH.length - 1 - idx]?.value; + allData.marketPriceCbbtc = marketPrices.cbBTC[marketPrices.cbBTC.length - 1 - idx].value; + allData.marketPriceWsol = marketPrices.WSOL[marketPrices.WSOL.length - 1 - idx].value; allData.marketCumulativeNonPintoUsd = - marketUsdCumulative.NET?.[marketUsdCumulative.NET.length - 1 - idx - syncOffset]?.value; - allData.marketCumulativeWethUsd = - marketUsdCumulative.WETH?.[marketUsdCumulative.WETH.length - 1 - idx - syncOffset]?.value; + marketUsdCumulative.NET?.[marketUsdCumulative.NET.length - 1 - idx]?.value; + allData.marketCumulativeWethUsd = marketUsdCumulative.WETH?.[marketUsdCumulative.WETH.length - 1 - idx]?.value; allData.marketCumulativeCbethUsd = - marketUsdCumulative.cbETH?.[marketUsdCumulative.cbETH.length - 1 - idx - syncOffset]?.value; + marketUsdCumulative.cbETH?.[marketUsdCumulative.cbETH.length - 1 - idx]?.value; allData.marketCumulativeWstethUsd = - marketUsdCumulative.wstETH?.[marketUsdCumulative.wstETH.length - 1 - idx - syncOffset]?.value; + marketUsdCumulative.wstETH?.[marketUsdCumulative.wstETH.length - 1 - idx]?.value; allData.marketCumulativeCbbtcUsd = - marketUsdCumulative.cbBTC?.[marketUsdCumulative.cbBTC.length - 1 - idx - syncOffset]?.value; - allData.marketCumulativeWsolUsd = - marketUsdCumulative.WSOL?.[marketUsdCumulative.WSOL.length - 1 - idx - syncOffset]?.value; - allData.marketSeasonalNonPintoUsd = - marketUsdSeasonal.NET[marketUsdSeasonal.NET.length - 1 - idx - syncOffset]?.value; - allData.marketSeasonalWethUsd = - marketUsdSeasonal.WETH[marketUsdSeasonal.WETH.length - 1 - idx - syncOffset]?.value; - allData.marketSeasonalCbethUsd = - marketUsdSeasonal.cbETH[marketUsdSeasonal.cbETH.length - 1 - idx - syncOffset]?.value; - allData.marketSeasonalWstethUsd = - marketUsdSeasonal.wstETH[marketUsdSeasonal.wstETH.length - 1 - idx - syncOffset]?.value; - allData.marketSeasonalCbbtcUsd = - marketUsdSeasonal.cbBTC[marketUsdSeasonal.cbBTC.length - 1 - idx - syncOffset]?.value; - allData.marketSeasonalWsolUsd = - marketUsdSeasonal.WSOL[marketUsdSeasonal.WSOL.length - 1 - idx - syncOffset]?.value; + marketUsdCumulative.cbBTC?.[marketUsdCumulative.cbBTC.length - 1 - idx]?.value; + allData.marketCumulativeWsolUsd = marketUsdCumulative.WSOL?.[marketUsdCumulative.WSOL.length - 1 - idx]?.value; + allData.marketSeasonalNonPintoUsd = marketUsdSeasonal.NET[marketUsdSeasonal.NET.length - 1 - idx]?.value; + allData.marketSeasonalWethUsd = marketUsdSeasonal.WETH[marketUsdSeasonal.WETH.length - 1 - idx]?.value; + allData.marketSeasonalCbethUsd = marketUsdSeasonal.cbETH[marketUsdSeasonal.cbETH.length - 1 - idx]?.value; + allData.marketSeasonalWstethUsd = marketUsdSeasonal.wstETH[marketUsdSeasonal.wstETH.length - 1 - idx]?.value; + allData.marketSeasonalCbbtcUsd = marketUsdSeasonal.cbBTC[marketUsdSeasonal.cbBTC.length - 1 - idx]?.value; + allData.marketSeasonalWsolUsd = marketUsdSeasonal.WSOL[marketUsdSeasonal.WSOL.length - 1 - idx]?.value; allData.marketCumulativeNonPintoPercent = - marketPercentCumulative.NET?.[marketPercentCumulative.NET.length - 1 - idx - syncOffset]?.value; + marketPercentCumulative.NET?.[marketPercentCumulative.NET.length - 1 - idx]?.value; allData.marketCumulativeWethPercent = - marketPercentCumulative.WETH?.[marketPercentCumulative.WETH.length - 1 - idx - syncOffset]?.value; + marketPercentCumulative.WETH?.[marketPercentCumulative.WETH.length - 1 - idx]?.value; allData.marketCumulativeCbethPercent = - marketPercentCumulative.cbETH?.[marketPercentCumulative.cbETH.length - 1 - idx - syncOffset]?.value; + marketPercentCumulative.cbETH?.[marketPercentCumulative.cbETH.length - 1 - idx]?.value; allData.marketCumulativeWstethPercent = - marketPercentCumulative.wstETH?.[marketPercentCumulative.wstETH.length - 1 - idx - syncOffset]?.value; + marketPercentCumulative.wstETH?.[marketPercentCumulative.wstETH.length - 1 - idx]?.value; allData.marketCumulativeCbbtcPercent = - marketPercentCumulative.cbBTC?.[marketPercentCumulative.cbBTC.length - 1 - idx - syncOffset]?.value; + marketPercentCumulative.cbBTC?.[marketPercentCumulative.cbBTC.length - 1 - idx]?.value; allData.marketCumulativeWsolPercent = - marketPercentCumulative.WSOL?.[marketPercentCumulative.WSOL.length - 1 - idx - syncOffset]?.value; + marketPercentCumulative.WSOL?.[marketPercentCumulative.WSOL.length - 1 - idx]?.value; allData.marketSeasonalNonPintoPercent = - marketPercentSeasonal.NET[marketPercentSeasonal.NET.length - 1 - idx - syncOffset]?.value; + marketPercentSeasonal.NET[marketPercentSeasonal.NET.length - 1 - idx]?.value; allData.marketSeasonalWethPercent = - marketPercentSeasonal.WETH[marketPercentSeasonal.WETH.length - 1 - idx - syncOffset]?.value; + marketPercentSeasonal.WETH[marketPercentSeasonal.WETH.length - 1 - idx]?.value; allData.marketSeasonalCbethPercent = - marketPercentSeasonal.cbETH[marketPercentSeasonal.cbETH.length - 1 - idx - syncOffset]?.value; + marketPercentSeasonal.cbETH[marketPercentSeasonal.cbETH.length - 1 - idx]?.value; allData.marketSeasonalWstethPercent = - marketPercentSeasonal.wstETH[marketPercentSeasonal.wstETH.length - 1 - idx - syncOffset]?.value; + marketPercentSeasonal.wstETH[marketPercentSeasonal.wstETH.length - 1 - idx]?.value; allData.marketSeasonalCbbtcPercent = - marketPercentSeasonal.cbBTC[marketPercentSeasonal.cbBTC.length - 1 - idx - syncOffset]?.value; + marketPercentSeasonal.cbBTC[marketPercentSeasonal.cbBTC.length - 1 - idx]?.value; allData.marketSeasonalWsolPercent = - marketPercentSeasonal.WSOL[marketPercentSeasonal.WSOL.length - 1 - idx - syncOffset]?.value; + marketPercentSeasonal.WSOL[marketPercentSeasonal.WSOL.length - 1 - idx]?.value; if (!allData.season) { const season = marketPerformanceResults[marketPerformanceResults.length - 1 - idx].season; From 91c107d7c13dea5d31778792b7914275c5c3de86 Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Sat, 29 Nov 2025 01:05:52 -0500 Subject: [PATCH 18/24] minimal allowed starting cumulative season to wsteth deployment --- src/components/nav/ChartSelectPanel.tsx | 20 +++++++++------ src/state/useChartSetupData.ts | 33 +++++++++++++------------ 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/components/nav/ChartSelectPanel.tsx b/src/components/nav/ChartSelectPanel.tsx index c9dc7660a..bf33e2142 100644 --- a/src/components/nav/ChartSelectPanel.tsx +++ b/src/components/nav/ChartSelectPanel.tsx @@ -6,10 +6,10 @@ import { useDebouncedEffect } from "@/utils/useDebounce"; import { cn } from "@/utils/utils"; import { useAtom } from "jotai"; import { isEqual } from "lodash"; -import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { memo, useCallback, useEffect, useMemo, useState } from "react"; import { renderAnnouncement } from "../AnnouncementBanner"; import { ChevronDownIcon, SearchIcon } from "../Icons"; -import { MIN_ADV_SEASON, chartSeasonInputsAtom, selectedChartsAtom } from "../charts/AdvancedChart"; +import { chartSeasonInputsAtom, selectedChartsAtom } from "../charts/AdvancedChart"; import { Input } from "../ui/Input"; import { ScrollArea } from "../ui/ScrollArea"; import { Separator } from "../ui/Separator"; @@ -105,10 +105,11 @@ const ChartSelectPanel = memo(() => { } else { // When selecting, initialize season input to min value const chartData = chartSetupData.find((chart) => chart.index === selection); - if (chartData && chartData.inputOptions === "SEASON") { + const opts = chartData?.inputOptions; + if (opts && opts.type === "SEASON") { setInternalSeasonInputs((prev) => ({ ...prev, - [chartData.id]: MIN_ADV_SEASON, + [chartData.id]: opts.minSeason, })); } selectedItems.push(selection); @@ -159,7 +160,12 @@ const ChartSelectPanel = memo(() => { () => { const clampedInputs = {}; for (const chartId in rawSeasonInputs) { - clampedInputs[chartId] = Math.max(MIN_ADV_SEASON, Math.min(currentSeason, rawSeasonInputs[chartId])); + const chartData = chartSetupData.find((chart) => chart.id === chartId); + clampedInputs[chartId] = Math.max( + // biome-ignore lint/style/noNonNullAssertion: Season input cannot change for chart that didn't configure seasons. + chartData!.inputOptions!.minSeason, + Math.min(currentSeason, rawSeasonInputs[chartId]), + ); } setInternalSeasonInputs(clampedInputs); }, @@ -230,7 +236,7 @@ const ChartSelectPanel = memo(() => {
{data.shortDescription}
- {isSelected && data.inputOptions === "SEASON" && ( + {isSelected && data.inputOptions?.type === "SEASON" && (
e.stopPropagation()}>