diff --git a/src/ProtectedLayout.tsx b/src/ProtectedLayout.tsx index 630ae118..ff817024 100644 --- a/src/ProtectedLayout.tsx +++ b/src/ProtectedLayout.tsx @@ -2,6 +2,7 @@ import { isDev } from "@/utils/utils"; import { Navigate, Route, Routes } from "react-router-dom"; import DevPage from "./components/DevPage"; import PageMetaWrapper from "./components/PageMetaWrapper"; +import Beanstalk from "./pages/Beanstalk"; import Collection from "./pages/Collection"; import Error404 from "./pages/Error404"; import Explorer from "./pages/Explorer"; @@ -69,6 +70,14 @@ export default function ProtectedLayout() { } /> + + + + } + /> void; + disabled?: boolean; +} + +interface BeanstalkStatFieldProps { + title: string; + value: ReactNode; + isLoading?: boolean; + disabled?: boolean; + actions?: BeanstalkStatFieldAction[]; + children?: ReactNode; +} + +/** + * Reusable stat field component with title, value, and optional action buttons + * Used in Beanstalk obligations and global stats cards + */ +const BeanstalkStatField: React.FC = ({ + title, + value, + isLoading = false, + disabled = false, + actions, + children, +}) => { + return ( +
+
+
{title}
+ {actions && actions.length > 0 && ( +
+ {actions.map((action) => ( + + ))} +
+ )} +
+ {children ? ( + children + ) : ( + +
{disabled ? N/A : value}
+
+ )} +
+ ); +}; + +export default BeanstalkStatField; diff --git a/src/components/Tractor/form/SowOrderV0Fields.tsx b/src/components/Tractor/form/SowOrderV0Fields.tsx index 0d6e7d64..b210b0fb 100644 --- a/src/components/Tractor/form/SowOrderV0Fields.tsx +++ b/src/components/Tractor/form/SowOrderV0Fields.tsx @@ -496,7 +496,7 @@ SowOrderV0Fields.MorningAuction = function MorningAuction() { // TODO: ADD REFERRAL CODE VALIDATOR! -const BONUS_MULTIPLIER = 0.1; +const BONUS_MULTIPLIER = 0.05; SowOrderV0Fields.PodDisplay = function PodDisplay({ onOpenReferralPopover, diff --git a/src/components/nav/nav/Navbar.tsx b/src/components/nav/nav/Navbar.tsx index 39d9c9a8..aa4729fb 100644 --- a/src/components/nav/nav/Navbar.tsx +++ b/src/components/nav/nav/Navbar.tsx @@ -332,6 +332,7 @@ export const navLinks = { field: "/field", swap: "/swap", referral: "/referral", + beanstalk: "/beanstalk", sPinto: "/sPinto", collection: "/collection", podmarket: "/market/pods", diff --git a/src/components/nav/nav/Navi.desktop.tsx b/src/components/nav/nav/Navi.desktop.tsx index da6f6eec..4a6072f8 100644 --- a/src/components/nav/nav/Navi.desktop.tsx +++ b/src/components/nav/nav/Navi.desktop.tsx @@ -110,6 +110,9 @@ const AppNavi = () => { Referral + + Beanstalk + sPinto diff --git a/src/components/nav/nav/Navi.mobile.tsx b/src/components/nav/nav/Navi.mobile.tsx index 7581cce5..c57cad24 100644 --- a/src/components/nav/nav/Navi.mobile.tsx +++ b/src/components/nav/nav/Navi.mobile.tsx @@ -175,6 +175,9 @@ function MobileNavContent({ learnOpen, setLearnOpen, unmount, close }: IMobileNa > Referral + + Beanstalk + = { description: "Share Pinto and earn rewards through referrals.", url: "https://pinto.money/referral", }, + beanstalk: { + title: "Beanstalk Obligations | Pinto", + description: "View your legacy Beanstalk holder obligations including Silo Payback, Pods, and Fertilizer.", + url: "https://pinto.money/beanstalk", + }, }; export default PINTO_META; diff --git a/src/pages/Beanstalk.tsx b/src/pages/Beanstalk.tsx new file mode 100644 index 00000000..7fe23357 --- /dev/null +++ b/src/pages/Beanstalk.tsx @@ -0,0 +1,54 @@ +import ReadMoreAccordion from "@/components/ReadMoreAccordion"; +import { Card } from "@/components/ui/Card"; +import PageContainer from "@/components/ui/PageContainer"; +import { Separator } from "@/components/ui/Separator"; +import { Link } from "react-router-dom"; +import BeanstalkGlobalCard from "./beanstalk/components/BeanstalkGlobalCard"; +import BeanstalkObligationsCard from "./beanstalk/components/BeanstalkObligationsCard"; + +const Beanstalk = () => { + return ( + +
+
+ {/* Hero Section */} +
+
Beanstalk Obligations
+
+ Beanstalk Debt issued by Pinto. + + Beanstalk participants at the time of Pinto launch were issued assets based on their holdings. When + Pinto exceeds 1 Billion in supply, 3% of mints go towards these between Beanstalk Silo Tokens, Pods, and + Fertilizer.{" "} + + Learn more + + +
+
+ + + {/* Main Cards - Two Column Layout */} +
+ {/* Left Panel - Obligations Card */} + + + + + {/* Right Panel - Global Stats Card */} + + + +
+
+
+
+ ); +}; + +export default Beanstalk; diff --git a/src/pages/beanstalk/components/BeanstalkFertilizerSection.tsx b/src/pages/beanstalk/components/BeanstalkFertilizerSection.tsx new file mode 100644 index 00000000..6ab7e299 --- /dev/null +++ b/src/pages/beanstalk/components/BeanstalkFertilizerSection.tsx @@ -0,0 +1,39 @@ +import { TokenValue } from "@/classes/TokenValue"; +import BeanstalkStatField from "@/components/BeanstalkStatField"; +import { formatter } from "@/utils/format"; + +interface BeanstalkFertilizerSectionProps { + balance: TokenValue; + isLoading: boolean; + disabled?: boolean; + onRinse?: () => void; + onSend?: () => void; +} + +/** + * Section component displaying fertilizer balance + */ +const BeanstalkFertilizerSection: React.FC = ({ + balance, + isLoading, + disabled = false, + onRinse, + onSend, +}) => { + const hasBalance = !balance.isZero; + + return ( + + ); +}; + +export default BeanstalkFertilizerSection; diff --git a/src/pages/beanstalk/components/BeanstalkGlobalCard.tsx b/src/pages/beanstalk/components/BeanstalkGlobalCard.tsx new file mode 100644 index 00000000..30e791a8 --- /dev/null +++ b/src/pages/beanstalk/components/BeanstalkGlobalCard.tsx @@ -0,0 +1,66 @@ +import BeanstalkStatField from "@/components/BeanstalkStatField"; +import { Button } from "@/components/ui/Button"; +import { useBeanstalkGlobalStats } from "@/state/useBeanstalkGlobalStats"; +import { formatter } from "@/utils/format"; + +/** + * Component displaying global Beanstalk repayment statistics + * Shows total urBDV distributed, total pods in repayment field, + * total unfertilized sprouts, and total Pinto paid out + * Shows N/A values when data cannot be loaded + */ +const BeanstalkGlobalCard: React.FC = () => { + const { + totalUrBdvDistributed, + totalPodsInRepaymentField, + totalUnfertilizedSprouts, + totalPintoPaidOut, + isLoading, + isError, + refetch, + } = useBeanstalkGlobalStats(); + + const formatValue = (value: typeof totalUrBdvDistributed) => { + return formatter.number(value, { minDecimals: 2, maxDecimals: 2 }); + }; + + return ( +
+ {isError && ( +
+ +
+ )} +
+ + + + +
+
+ ); +}; + +export default BeanstalkGlobalCard; diff --git a/src/pages/beanstalk/components/BeanstalkObligationsCard.tsx b/src/pages/beanstalk/components/BeanstalkObligationsCard.tsx new file mode 100644 index 00000000..2bc0ad65 --- /dev/null +++ b/src/pages/beanstalk/components/BeanstalkObligationsCard.tsx @@ -0,0 +1,47 @@ +import { Button } from "@/components/ui/Button"; +import { useFarmerBeanstalkRepayment } from "@/state/useFarmerBeanstalkRepayment"; +import { useAccount } from "wagmi"; +import BeanstalkFertilizerSection from "./BeanstalkFertilizerSection"; +import BeanstalkPodsSection from "./BeanstalkPodsSection"; +import BeanstalkSiloSection from "./BeanstalkSiloSection"; + +/** + * Container component for displaying user's Beanstalk obligations + * Shows Silo Payback (urBDV), Pods from repayment field, and Fertilizer data + * Shows N/A values when wallet is not connected or data cannot be loaded + */ +const BeanstalkObligationsCard: React.FC = () => { + const account = useAccount(); + const { silo, pods, fertilizer, isLoading, isError, refetch } = useFarmerBeanstalkRepayment(); + + const isConnected = !!account.address; + const showDisabled = !isConnected || isError; + + return ( +
+ {isConnected && isError && ( +
+ +
+ )} +
+ + + +
+
+ ); +}; + +export default BeanstalkObligationsCard; diff --git a/src/pages/beanstalk/components/BeanstalkPodsSection.tsx b/src/pages/beanstalk/components/BeanstalkPodsSection.tsx new file mode 100644 index 00000000..e26a2f5d --- /dev/null +++ b/src/pages/beanstalk/components/BeanstalkPodsSection.tsx @@ -0,0 +1,53 @@ +import { TokenValue } from "@/classes/TokenValue"; +import BeanstalkStatField from "@/components/BeanstalkStatField"; +import PodLineGraph from "@/components/PodLineGraph"; +import TextSkeleton from "@/components/TextSkeleton"; +import { Plot } from "@/utils/types"; + +interface BeanstalkPodsSectionProps { + plots: Plot[]; + totalPods: TokenValue; + isLoading: boolean; + disabled?: boolean; + onHarvest?: () => void; + onSend?: () => void; +} + +/** + * Section component displaying pods from the repayment field (fieldId=1) + * Shows PodLineGraph visualization + */ +const BeanstalkPodsSection: React.FC = ({ + plots, + totalPods, + isLoading, + disabled = false, + onHarvest, + onSend, +}) => { + const hasPlots = plots.length > 0; + const hasPods = !totalPods.isZero; + const showDisabledGraph = disabled || !hasPlots; + + return ( + + {isLoading ? ( + + ) : ( +
+ +
+ )} +
+ ); +}; + +export default BeanstalkPodsSection; diff --git a/src/pages/beanstalk/components/BeanstalkSiloSection.tsx b/src/pages/beanstalk/components/BeanstalkSiloSection.tsx new file mode 100644 index 00000000..c24ca4dd --- /dev/null +++ b/src/pages/beanstalk/components/BeanstalkSiloSection.tsx @@ -0,0 +1,39 @@ +import { TokenValue } from "@/classes/TokenValue"; +import BeanstalkStatField from "@/components/BeanstalkStatField"; +import { formatter } from "@/utils/format"; + +interface BeanstalkSiloSectionProps { + balance: TokenValue; + isLoading: boolean; + disabled?: boolean; + onClaim?: () => void; + onSend?: () => void; +} + +/** + * Section component displaying urBDV token balance for Silo Payback + */ +const BeanstalkSiloSection: React.FC = ({ + balance, + isLoading, + disabled = false, + onClaim, + onSend, +}) => { + const hasBalance = !balance.isZero; + + return ( + + ); +}; + +export default BeanstalkSiloSection; diff --git a/src/state/useBeanstalkGlobalStats.ts b/src/state/useBeanstalkGlobalStats.ts new file mode 100644 index 00000000..4a3518fb --- /dev/null +++ b/src/state/useBeanstalkGlobalStats.ts @@ -0,0 +1,136 @@ +import { TokenValue } from "@/classes/TokenValue"; +import { PODS, SPROUTS, URBDV } from "@/constants/internalTokens"; +import { defaultQuerySettingsMedium } from "@/constants/query"; +import { useProtocolAddress } from "@/hooks/pinto/useProtocolAddress"; +import { useCallback, useMemo } from "react"; +import { useReadContracts } from "wagmi"; + +/** + * ABI snippets for Silo Payback contract global functions + * NOTE: These functions don't exist in the protocol yet - will be indexed from subgraph later + */ +// const siloPaybackGlobalAbi = [ +// { +// inputs: [], +// name: "totalUrBdvDistributed", +// outputs: [{ internalType: "uint256", name: "", type: "uint256" }], +// stateMutability: "view", +// type: "function", +// }, +// { +// inputs: [], +// name: "totalPintoPaidOut", +// outputs: [{ internalType: "uint256", name: "", type: "uint256" }], +// stateMutability: "view", +// type: "function", +// }, +// ] as const; + +/** + * ABI snippets for Field contract global functions + */ +const fieldGlobalAbi = [ + { + inputs: [{ internalType: "uint256", name: "fieldId", type: "uint256" }], + name: "totalPods", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, +] as const; + +/** + * ABI snippets for Barn Payback contract global functions + * NOTE: This function doesn't exist in the protocol yet - will be indexed from subgraph later + */ +// const barnPaybackGlobalAbi = [ +// { +// inputs: [], +// name: "totalUnfertilizedSprouts", +// outputs: [{ internalType: "uint256", name: "", type: "uint256" }], +// stateMutability: "view", +// type: "function", +// }, +// ] as const; + +/** + * Interface for the global Beanstalk statistics data + */ +export interface BeanstalkGlobalStatsData { + totalUrBdvDistributed: TokenValue; + totalPodsInRepaymentField: TokenValue; + totalUnfertilizedSprouts: TokenValue; + totalPintoPaidOut: TokenValue; + isLoading: boolean; + isError: boolean; + refetch: () => Promise; +} + +// Field ID for the Beanstalk repayment field +const BEANSTALK_REPAYMENT_FIELD_ID = 1n; + +/** + * Hook for fetching global Beanstalk repayment statistics + * + * This hook fetches protocol-wide statistics: + * - Total urBDV distributed across all holders (TODO: from subgraph) + * - Total pods in the repayment field (fieldId=1) + * - Total unfertilized sprouts (TODO: from subgraph) + * - Total Pinto paid out to holders (TODO: from subgraph) + * + * Uses a 5-minute stale time for more frequent updates of global stats + * + * @returns BeanstalkGlobalStatsData with all global statistics + */ +export function useBeanstalkGlobalStats(): BeanstalkGlobalStatsData { + const protocolAddress = useProtocolAddress(); + + // Query for available global statistics (only totalPods exists in protocol) + const globalQuery = useReadContracts({ + contracts: [ + { + address: protocolAddress, + abi: fieldGlobalAbi, + functionName: "totalPods", + args: [BEANSTALK_REPAYMENT_FIELD_ID], + }, + // TODO: These functions don't exist in the protocol yet + // Will be indexed from subgraph later: + // - totalUrBdvDistributed + // - totalUnfertilizedSprouts + // - totalPintoPaidOut + ], + allowFailure: true, + query: { + ...defaultQuerySettingsMedium, // 5 minutes staleTime for global stats + }, + }); + + // Process global data + const globalData = useMemo(() => { + const totalPodsInRepaymentField = globalQuery.data?.[0]?.result; + + return { + // TODO: These will come from subgraph later + totalUrBdvDistributed: TokenValue.fromBlockchain(0n, URBDV.decimals), + totalPodsInRepaymentField: TokenValue.fromBlockchain(totalPodsInRepaymentField ?? 0n, PODS.decimals), + totalUnfertilizedSprouts: TokenValue.fromBlockchain(0n, SPROUTS.decimals), + totalPintoPaidOut: TokenValue.fromBlockchain(0n, URBDV.decimals), + }; + }, [globalQuery.data]); + + // Refetch function + const refetch = useCallback(async () => { + await globalQuery.refetch(); + }, [globalQuery.refetch]); + + return useMemo( + () => ({ + ...globalData, + isLoading: globalQuery.isLoading, + isError: globalQuery.isError, + refetch, + }), + [globalData, globalQuery.isLoading, globalQuery.isError, refetch], + ); +} diff --git a/src/state/useFarmerBeanstalkRepayment.ts b/src/state/useFarmerBeanstalkRepayment.ts new file mode 100644 index 00000000..28bc670f --- /dev/null +++ b/src/state/useFarmerBeanstalkRepayment.ts @@ -0,0 +1,223 @@ +import { TokenValue } from "@/classes/TokenValue"; +import { ZERO_ADDRESS } from "@/constants/address"; +import { PODS } from "@/constants/internalTokens"; +import { defaultQuerySettings } from "@/constants/query"; +import { beanstalkAbi } from "@/generated/contractHooks"; +import { useProtocolAddress } from "@/hooks/pinto/useProtocolAddress"; +import { Plot } from "@/utils/types"; +import { useCallback, useMemo } from "react"; +import { toHex } from "viem"; +import { useAccount, useReadContracts } from "wagmi"; + +/** + * ABI snippets for Silo Payback contract functions + * NOTE: These functions don't exist in the protocol yet - will be indexed from subgraph later + */ +// const siloPaybackAbi = [ +// { +// inputs: [{ internalType: "address", name: "account", type: "address" }], +// name: "balanceOfUrBdv", +// outputs: [{ internalType: "uint256", name: "", type: "uint256" }], +// stateMutability: "view", +// type: "function", +// }, +// { +// inputs: [{ internalType: "address", name: "account", type: "address" }], +// name: "earnedUrBdv", +// outputs: [{ internalType: "uint256", name: "", type: "uint256" }], +// stateMutability: "view", +// type: "function", +// }, +// { +// inputs: [{ internalType: "address", name: "account", type: "address" }], +// name: "totalDistributedToAccount", +// outputs: [{ internalType: "uint256", name: "", type: "uint256" }], +// stateMutability: "view", +// type: "function", +// }, +// { +// inputs: [{ internalType: "address", name: "account", type: "address" }], +// name: "totalReceivedByAccount", +// outputs: [{ internalType: "uint256", name: "", type: "uint256" }], +// stateMutability: "view", +// type: "function", +// }, +// ] as const; + +/** + * ABI snippets for Barn Payback contract functions + * NOTE: These functions don't exist in the protocol yet - will be indexed from subgraph later + */ +// const barnPaybackAbi = [ +// { +// inputs: [{ internalType: "address", name: "account", type: "address" }], +// name: "balanceOfFertilizer", +// outputs: [{ internalType: "uint256", name: "", type: "uint256" }], +// stateMutability: "view", +// type: "function", +// }, +// { +// inputs: [{ internalType: "address", name: "account", type: "address" }], +// name: "balanceOfSprouts", +// outputs: [{ internalType: "uint256", name: "", type: "uint256" }], +// stateMutability: "view", +// type: "function", +// }, +// { +// inputs: [{ internalType: "address", name: "account", type: "address" }], +// name: "balanceOfFertilized", +// outputs: [{ internalType: "uint256", name: "", type: "uint256" }], +// stateMutability: "view", +// type: "function", +// }, +// { +// inputs: [], +// name: "humidity", +// outputs: [{ internalType: "uint256", name: "", type: "uint256" }], +// stateMutability: "view", +// type: "function", +// }, +// ] as const; + +/** + * Interface for silo payback data + */ +interface SiloPaybackData { + balance: TokenValue; +} + +/** + * Interface for pods data from repayment field (fieldId=1) + */ +interface PodsData { + plots: Plot[]; + totalPods: TokenValue; +} + +/** + * Interface for fertilizer data + */ +interface FertilizerData { + balance: TokenValue; +} + +/** + * Interface for the complete farmer Beanstalk repayment data + */ +export interface FarmerBeanstalkRepaymentData { + silo: SiloPaybackData; + pods: PodsData; + fertilizer: FertilizerData; + isLoading: boolean; + isError: boolean; + refetch: () => Promise; +} + +// Token decimals for urBDV (same as BEAN - 6 decimals) +const URBDV_DECIMALS = 6; +// Token decimals for fertilizer amounts +const FERTILIZER_DECIMALS = 6; + +/** + * Hook for fetching farmer-specific Beanstalk repayment data + * + * This hook fetches: + * - Silo payback data (TODO: from subgraph - urBDV balance) + * - Pods data from repayment field (fieldId=1) - from on-chain + * - Fertilizer data (TODO: from subgraph) + * + * @returns FarmerBeanstalkRepaymentData with all farmer obligations data + */ +export function useFarmerBeanstalkRepayment(): FarmerBeanstalkRepaymentData { + const account = useAccount(); + const protocolAddress = useProtocolAddress(); + const farmerAddress = account.address ?? ZERO_ADDRESS; + + // Query for pods data from repayment field (fieldId=1) + // These are the only functions that exist in the protocol + const podsQuery = useReadContracts({ + contracts: [ + { + address: protocolAddress, + abi: beanstalkAbi, + functionName: "getPlotsFromAccount", + args: [farmerAddress, 1n], // fieldId=1 for repayment field + }, + { + address: protocolAddress, + abi: beanstalkAbi, + functionName: "balanceOfPods", + args: [farmerAddress, 1n], // fieldId=1 for repayment field + }, + ], + allowFailure: true, + query: { + // Always fetch - will use ZERO_ADDRESS if wallet not connected + ...defaultQuerySettings, // 20 minutes staleTime + }, + }); + + // TODO: Silo payback data will come from subgraph + // Functions don't exist in protocol: balanceOfUrBdv, earnedUrBdv, totalDistributedToAccount, totalReceivedByAccount + const siloData = useMemo((): SiloPaybackData => { + return { + balance: TokenValue.fromBlockchain(0n, URBDV_DECIMALS), + }; + }, []); + + // Process pods data - these functions exist in protocol + const podsData = useMemo((): PodsData => { + const plotsResult = podsQuery.data?.[0]?.result as readonly { index: bigint; pods: bigint }[] | undefined; + const totalPodsResult = podsQuery.data?.[1]?.result; + + const plots: Plot[] = (plotsResult ?? []).map((plotData) => { + const index = TokenValue.fromBigInt(plotData.index, PODS.decimals); + const pods = TokenValue.fromBigInt(plotData.pods, PODS.decimals); + + return { + id: index.toHuman(), + idHex: toHex(`${plotData.index}${plotData.pods}`), + index, + pods, + harvestedPods: TokenValue.ZERO, + harvestablePods: TokenValue.ZERO, + unharvestablePods: pods, + }; + }); + + return { + plots, + totalPods: TokenValue.fromBlockchain(totalPodsResult ?? 0n, PODS.decimals), + }; + }, [podsQuery.data]); + + // TODO: Fertilizer data will come from subgraph + // Functions don't exist in protocol: balanceOfFertilizer, balanceOfSprouts, balanceOfFertilized + // humidity() exists as getCurrentHumidity() but not needed for now + const fertilizerData = useMemo((): FertilizerData => { + return { + balance: TokenValue.fromBlockchain(0n, FERTILIZER_DECIMALS), + }; + }, []); + + // Refetch pods query (only one that works) + const refetch = useCallback(async () => { + await podsQuery.refetch(); + }, [podsQuery.refetch]); + + // Loading and error states only from pods query + const isLoading = podsQuery.isLoading; + const isError = podsQuery.isError; + + return useMemo( + () => ({ + silo: siloData, + pods: podsData, + fertilizer: fertilizerData, + isLoading, + isError, + refetch, + }), + [siloData, podsData, fertilizerData, isLoading, isError, refetch], + ); +}