diff --git a/src/ProtectedLayout.tsx b/src/ProtectedLayout.tsx index 630ae118b..ff8170241 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() { } /> + + + + } + /> + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/BeanstalkStatField.tsx b/src/components/BeanstalkStatField.tsx new file mode 100644 index 000000000..e92574b15 --- /dev/null +++ b/src/components/BeanstalkStatField.tsx @@ -0,0 +1,67 @@ +import TextSkeleton from "@/components/TextSkeleton"; +import { Button } from "@/components/ui/Button"; +import { ReactNode } from "react"; + +interface BeanstalkStatFieldAction { + label: string; + onClick?: () => 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/ComboInputField.tsx b/src/components/ComboInputField.tsx index dfdc9f271..75041aba1 100644 --- a/src/components/ComboInputField.tsx +++ b/src/components/ComboInputField.tsx @@ -105,6 +105,7 @@ export interface ComboInputProps extends InputHTMLAttributes { // Additional info display showAdditionalInfo?: boolean; + hideUsdValue?: boolean; } function ComboInputField({ @@ -146,6 +147,7 @@ function ComboInputField({ enableSlider, sliderMarkers, showAdditionalInfo = true, + hideUsdValue = false, }: ComboInputProps) { const tokenData = useTokenData(); const { balances } = useFarmerBalances(); @@ -230,8 +232,9 @@ function ComboInputField({ } // If customMaxAmount is provided and greater than 0, use the minimum of base balance and customMaxAmount + // If base balance is 0 (no token selected or no farmer balance), use customMaxAmount directly if (customMaxAmount?.gt(0)) { - return TokenValue.min(baseBalance, customMaxAmount); + return baseBalance.gt(0) ? TokenValue.min(baseBalance, customMaxAmount) : customMaxAmount; } // Otherwise use base balance @@ -256,7 +259,12 @@ function ComboInputField({ return tokenAndBalanceMap.get(selectedToken) ?? TokenValue.ZERO; } // Always use farmerTokenBalance for display, not maxAmount (which may be limited by customMaxAmount) - return getFarmerBalanceByMode(farmerTokenBalance, balanceFrom); + const farmerBalance = getFarmerBalanceByMode(farmerTokenBalance, balanceFrom); + // If farmer balance is 0 and customMaxAmount is provided, show customMaxAmount as the balance + if (farmerBalance.eq(0) && customMaxAmount?.gt(0)) { + return customMaxAmount; + } + return farmerBalance; }, [mode, selectedPlots, tokenAndBalanceMap, selectedToken, farmerTokenBalance, balanceFrom, getFarmerBalanceByMode]); /** @@ -630,7 +638,7 @@ function ComboInputField({ {!disableInlineBalance && (
- {shouldShowAdditionalInfo() && mode !== "plots" ? ( + {shouldShowAdditionalInfo() && mode !== "plots" && !hideUsdValue ? ( {formatter.usd(inputValue)} diff --git a/src/components/FertilizerCard.tsx b/src/components/FertilizerCard.tsx new file mode 100644 index 000000000..4d0d7a688 --- /dev/null +++ b/src/components/FertilizerCard.tsx @@ -0,0 +1,67 @@ +import fertilizerIcon from "@/assets/protocol/Fertilizer.svg"; +import CheckmarkCircle from "@/components/CheckmarkCircle"; +import IconImage from "@/components/ui/IconImage"; +import { Input } from "@/components/ui/Input"; +import { formatter } from "@/utils/format"; + +interface FertilizerCardProps { + fertId: bigint; + amount: string; + isSelected: boolean; + maxBalance: bigint; + sprouts: string; + humidity: string; + onToggleSelection: (fertId: bigint) => void; + onAmountChange: (fertId: bigint, value: string, maxBalance: bigint) => void; +} + +export default function FertilizerCard({ + fertId, + amount, + isSelected, + maxBalance, + sprouts, + humidity, + onToggleSelection, + onAmountChange, +}: FertilizerCardProps) { + return ( +
+ {/* Checkbox */} +
onToggleSelection(fertId)}> + +
+ + {/* Fertilizer icon */} +
+ +
+ + {/* Fertilizer info */} +
+
+ {sprouts} Sprouts + Humidity: {humidity} +
+
+ {formatter.number(Number(maxBalance))} bsFERT - ID {formatter.number(Number(fertId))} +
+
+ + {/* Amount input */} +
+ onAmountChange(fertId, e.target.value, maxBalance)} + outlined={true} + containerClassName="border border-pinto-green-4 focus-within:border-pinto-green-4" + min="0" + max={maxBalance.toString()} + step="1" + /> +
+
+ ); +} diff --git a/src/components/PintoAssetTransferNotice.tsx b/src/components/PintoAssetTransferNotice.tsx index 9ddd76671..1e5528e42 100644 --- a/src/components/PintoAssetTransferNotice.tsx +++ b/src/components/PintoAssetTransferNotice.tsx @@ -76,9 +76,26 @@ export default function PintoAssetTransferNotice({ animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -20 }} transition={{ duration: 0.3, ease: "easeInOut" }} - className="text-2xl" + className="flex flex-col gap-8" > - Note: be sure recipient address is intended. +

Note: be sure recipient address is intended.

+
+ { + if (checked !== "indeterminate") { + setTransferNotice(checked); + } else { + setTransferNotice(false); + } + }} + /> + +
)} diff --git a/src/components/PodLineGraph.tsx b/src/components/PodLineGraph.tsx index 39d64a50b..ccf5093a9 100644 --- a/src/components/PodLineGraph.tsx +++ b/src/components/PodLineGraph.tsx @@ -53,14 +53,32 @@ interface PodLineGraphProps { label?: string; /** Optional: specify label type */ labelType?: "title" | "label"; + /** Optional: override harvestable index (for non-default fields like repayment field) */ + customHarvestableIndex?: TokenValue; + /** Optional: override pod index (for non-default fields like repayment field) */ + customPodIndex?: TokenValue; } /** * Groups nearby plots for visual display while keeping each plot individually interactive */ -function combinePlots(plots: Plot[], harvestableIndex: TokenValue, selectedIndices: Set): CombinedPlot[] { +function combinePlots( + plots: Plot[], + harvestableIndex: TokenValue, + selectedIndices: Set, + podLine: TokenValue, +): CombinedPlot[] { if (plots.length === 0) return []; + // Dynamically scale the grouping gap based on pod line size + const podLineNum = podLine.toNumber(); + let maxGap = MAX_GAP_TO_COMBINE; + if (podLineNum > 200_000_000) { + maxGap = TokenValue.fromHuman("50000000", PODS.decimals); // 50M gap for large pod lines + } else if (podLineNum > 50_000_000) { + maxGap = TokenValue.fromHuman("10000000", PODS.decimals); // 10M gap for medium pod lines + } + // Sort plots by index const sortedPlots = [...plots].sort((a, b) => a.index.sub(b.index).toNumber()); @@ -78,7 +96,7 @@ function combinePlots(plots: Plot[], harvestableIndex: TokenValue, selectedIndic const gap = nextPlot.index.sub(plot.index.add(plot.pods)); // If gap is small enough, continue grouping - if (gap.lt(MAX_GAP_TO_COMBINE)) { + if (gap.lt(maxGap)) { continue; } } @@ -116,9 +134,19 @@ function combinePlots(plots: Plot[], harvestableIndex: TokenValue, selectedIndic */ function generateAxisLabels(min: number, max: number): number[] { const labels: number[] = []; - const start = Math.floor(min / AXIS_INTERVAL) * AXIS_INTERVAL; + const range = max - min; + + // Dynamically choose interval based on range size + let interval = AXIS_INTERVAL; // default 10M + if (range > 200_000_000) { + interval = 100_000_000; // 100M intervals for large ranges + } else if (range > 50_000_000) { + interval = 50_000_000; // 50M intervals for medium ranges + } - for (let value = start; value <= max; value += AXIS_INTERVAL) { + const start = Math.floor(min / interval) * interval; + + for (let value = start; value <= max; value += interval) { if (value >= min) { labels.push(value); } @@ -191,10 +219,15 @@ export default function PodLineGraph({ className, label = "My Pods In Line", labelType = "label", + customHarvestableIndex, + customPodIndex, }: PodLineGraphProps) { const farmerField = useFarmerField(); - const harvestableIndex = useHarvestableIndex(); - const podIndex = usePodIndex(); + const defaultHarvestableIndex = useHarvestableIndex(); + const defaultPodIndex = usePodIndex(); + + const harvestableIndex = customHarvestableIndex ?? defaultHarvestableIndex; + const podIndex = customPodIndex ?? defaultPodIndex; const [hoveredPlotIndex, setHoveredPlotIndex] = useState(null); const [tooltipData, setTooltipData] = useState<{ @@ -253,8 +286,8 @@ export default function PodLineGraph({ // Combine plots for visualization const combinedPlots = useMemo( - () => combinePlots(plots, harvestableIndex, selectedSet), - [plots, harvestableIndex, selectedSet], + () => combinePlots(plots, harvestableIndex, selectedSet, podLine), + [plots, harvestableIndex, selectedSet, podLine], ); // Separate harvested and unharvested plots diff --git a/src/components/ReadMoreAccordion.tsx b/src/components/ReadMoreAccordion.tsx index 4a8046057..b2b046cdd 100644 --- a/src/components/ReadMoreAccordion.tsx +++ b/src/components/ReadMoreAccordion.tsx @@ -1,5 +1,5 @@ import { cn } from "@/utils/utils"; -import { motion } from "framer-motion"; +import { AnimatePresence, motion } from "framer-motion"; import { useState } from "react"; import { Col } from "./Container"; @@ -26,20 +26,21 @@ export default function ReadMoreAccordion({ if (inline) { return ( - + {open && ( + + {children} + )} - > - {open && {children}} - + {open ? " Read less" : " Read more"} diff --git a/src/components/ReviewTractorOrderDialog.tsx b/src/components/ReviewTractorOrderDialog.tsx index 5918443e3..437ec20e5 100644 --- a/src/components/ReviewTractorOrderDialog.tsx +++ b/src/components/ReviewTractorOrderDialog.tsx @@ -4,13 +4,15 @@ import { useProtocolAddress } from "@/hooks/pinto/useProtocolAddress"; import useSignTractorBlueprint from "@/hooks/tractor/useSignTractorBlueprint"; import useTransaction from "@/hooks/useTransaction"; import { Blueprint, PublisherTractorExecution, Requisition, useGetBlueprintHash } from "@/lib/Tractor"; +import { queryKeys } from "@/state/queryKeys"; import { cn } from "@/utils/utils"; import { CheckIcon } from "@radix-ui/react-icons"; +import { useQueryClient } from "@tanstack/react-query"; import { useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { toast } from "sonner"; import { encodeFunctionData } from "viem"; -import { useAccount } from "wagmi"; +import { useAccount, usePublicClient } from "wagmi"; import { Col, Row } from "./Container"; import LoadingSpinner from "./LoadingSpinner"; import { HighlightedCallData } from "./Tractor/HighlightedCallData"; @@ -64,6 +66,8 @@ export default function ReviewTractorOrderDialog({ const [decodeAbi, setDecodeAbi] = useState(false); const protocolAddress = useProtocolAddress(); const navigate = useNavigate(); + const queryClient = useQueryClient(); + const publicClient = usePublicClient(); // Get order type configuration from registry // Memoize the order config to avoid re-rendering the component on every render @@ -114,6 +118,8 @@ export default function ReviewTractorOrderDialog({ try { setSubmitting(true); + let txHash: `0x${string}` | undefined; + // Check if we need to include deposit optimization calls if (depositOptimizationCalls && depositOptimizationCalls.length > 0) { console.debug(`Publishing requisition with ${depositOptimizationCalls.length} deposit optimization calls`); @@ -130,7 +136,7 @@ export default function ReviewTractorOrderDialog({ const farmCalls = [...depositOptimizationCalls, publishRequisitionCall]; // Execute as farm call - await writeWithEstimateGas({ + txHash = await writeWithEstimateGas({ address: protocolAddress, abi: diamondABI, functionName: "farm", @@ -140,7 +146,7 @@ export default function ReviewTractorOrderDialog({ console.debug("Publishing requisition without deposit optimization"); // Call publish requisition directly (like before) - await writeWithEstimateGas({ + txHash = await writeWithEstimateGas({ address: protocolAddress, abi: diamondABI, functionName: "publishRequisition", @@ -148,11 +154,13 @@ export default function ReviewTractorOrderDialog({ }); } - // Success handling - toast.success("Order published successfully"); + // Wait for tx confirmation before refreshing data + if (txHash && publicClient) { + await publicClient.waitForTransactionReceipt({ hash: txHash, confirmations: 1 }); + } - // Close the dialog - onOpenChange(false); + // Invalidate all tractor queries to refresh order lists + queryClient.invalidateQueries({ queryKey: [queryKeys.base.tractor] }); // Navigate to the Field page with tractor tab active if (orderData.type === "sow") { @@ -160,14 +168,10 @@ export default function ReviewTractorOrderDialog({ } // Call the parent success callback to refresh data - if (onSuccess) { - onSuccess(); - } + onSuccess?.(); // Call the onOrderPublished callback if provided - if (onOrderPublished) { - onOrderPublished(); - } + onOrderPublished?.(); } catch (error) { console.error("Error publishing requisition:", error); } finally { diff --git a/src/components/Tractor/AutomateClaim/AutomateClaimContext.tsx b/src/components/Tractor/AutomateClaim/AutomateClaimContext.tsx new file mode 100644 index 000000000..3af09c2be --- /dev/null +++ b/src/components/Tractor/AutomateClaim/AutomateClaimContext.tsx @@ -0,0 +1,42 @@ +import { createContext, useCallback, useContext, useMemo, useState } from "react"; + +// ──────────────────────────────────────────────────────────────────────────────── +// Context shape +// ──────────────────────────────────────────────────────────────────────────────── + +interface AutomateClaimContextValue { + /** Whether the SpecifyConditionsDialog is open. */ + isOpen: boolean; + /** Toggle or set the dialog open state. */ + setIsOpen: (open: boolean) => void; +} + +const AutomateClaimContext = createContext(null); + +// ──────────────────────────────────────────────────────────────────────────────── +// Provider +// ──────────────────────────────────────────────────────────────────────────────── + +export function AutomateClaimProvider({ children }: { children: React.ReactNode }) { + const [isOpen, setIsOpenRaw] = useState(false); + + const setIsOpen = useCallback((open: boolean) => { + setIsOpenRaw(open); + }, []); + + const value = useMemo(() => ({ isOpen, setIsOpen }), [isOpen, setIsOpen]); + + return {children}; +} + +// ──────────────────────────────────────────────────────────────────────────────── +// Hook +// ──────────────────────────────────────────────────────────────────────────────── + +export function useAutomateClaimContext(): AutomateClaimContextValue { + const ctx = useContext(AutomateClaimContext); + if (!ctx) { + throw new Error("useAutomateClaimContext must be used within an AutomateClaimProvider"); + } + return ctx; +} diff --git a/src/components/Tractor/AutomateClaim/AutomateClaimExecutionHistory.tsx b/src/components/Tractor/AutomateClaim/AutomateClaimExecutionHistory.tsx new file mode 100644 index 000000000..bd2bc31d7 --- /dev/null +++ b/src/components/Tractor/AutomateClaim/AutomateClaimExecutionHistory.tsx @@ -0,0 +1,107 @@ +import { TV } from "@/classes/TokenValue"; +import { formatter } from "@/utils/format"; +import { format } from "date-fns"; +import { useMemo } from "react"; +import { AutomateClaimOrderData, ExecutionHistoryProps } from "../types"; + +// Helper function to shorten addresses +function shortenAddress(address: string): string { + return `${address.substring(0, 6)}...${address.substring(address.length - 4)}`; +} + +// Helper function to format dates with time +const formatDate = (timestamp?: number) => { + if (!timestamp) return "Unknown"; + return format(new Date(timestamp), "MM/dd/yyyy h:mm a"); +}; + +export function AutomateClaimExecutionHistory({ executionHistory, orderData }: ExecutionHistoryProps) { + if (orderData.type !== "automateClaim") { + throw new Error("AutomateClaimExecutionHistory requires automateClaim order data"); + } + + const claimData = orderData as AutomateClaimOrderData; + + const enabledOps = useMemo(() => { + const ops: string[] = []; + if (claimData.mowEnabled) ops.push("Mow"); + if (claimData.plantEnabled) ops.push("Plant"); + if (claimData.harvestEnabled) ops.push("Harvest"); + return ops; + }, [claimData.mowEnabled, claimData.plantEnabled, claimData.harvestEnabled]); + + const totalTipsPaid = useMemo(() => { + const tipAmount = claimData.operatorTip ? TV.fromHuman(claimData.operatorTip, 6) : TV.ZERO; + return tipAmount.mul(executionHistory.length); + }, [claimData.operatorTip, executionHistory.length]); + + const sortedExecutions = useMemo( + () => + [...executionHistory].sort((a, b) => { + if (a.timestamp && b.timestamp) { + return b.timestamp - a.timestamp; + } + return b.blockNumber - a.blockNumber; + }), + [executionHistory], + ); + + if (executionHistory.length === 0) { + return
No executions yet
; + } + + return ( +
+ {/* Summary Section */} +
+
+ Total Executions + {executionHistory.length} +
+
+ Enabled Operations + {enabledOps.join(", ") || "None"} +
+
+ Total Tips Paid + {formatter.number(totalTipsPaid)} PINTO +
+
+ + {/* Execution Table */} +
+ + + + + + + + + + + {sortedExecutions.map((execution, index) => ( + + + + + + + ))} + +
ExecutionOperatorDate & TimeActions
#{executionHistory.length - index}{shortenAddress(execution.operator)} + {execution.timestamp ? formatDate(execution.timestamp) : `Block ${execution.blockNumber}`} + + + View Transaction + +
+
+
+ ); +} diff --git a/src/components/Tractor/AutomateClaim/AutomateClaimVisualization.tsx b/src/components/Tractor/AutomateClaim/AutomateClaimVisualization.tsx new file mode 100644 index 000000000..edd23fea4 --- /dev/null +++ b/src/components/Tractor/AutomateClaim/AutomateClaimVisualization.tsx @@ -0,0 +1,93 @@ +import { OrderVisualization } from "@/components/OrderVisualization"; +import { useMainToken } from "@/state/useTokenData"; +import { CheckCircledIcon, CrossCircledIcon } from "@radix-ui/react-icons"; +import { AutomateClaimOrderData, TractorOrderVisualizationProps } from "../types"; + +/** + * Visualizes decoded AutomateClaim order data in the Tractor orders panel. + * Displays enabled operations (Mow, Plant, Harvest) and operator tip configuration. + */ +export function AutomateClaimVisualization({ orderData, className }: TractorOrderVisualizationProps) { + if (orderData.type !== "automateClaim") { + throw new Error("AutomateClaimVisualization requires automateClaim order data"); + } + + const claimData = orderData as AutomateClaimOrderData; + const mainToken = useMainToken(); + + const operations = [ + { label: "Mow", enabled: claimData.mowEnabled }, + { label: "Plant", enabled: claimData.plantEnabled }, + { label: "Harvest", enabled: claimData.harvestEnabled }, + ]; + + const enabledOps = operations.filter((op) => op.enabled); + + return ( +
+
+
+
+ +
+ {/* Automate Claim Section */} +
+ + + {enabledOps.length} operation{enabledOps.length !== 1 ? "s" : ""} enabled + + ), + }, + ]} + size="sm" + /> + ({ + text: ( + + {op.enabled ? ( + + ) : ( + + )} + {op.label} + {op.enabled ? "enabled" : "disabled"} + + ), + }))} + size="sm" + /> + +
+ + {/* Tip Section */} +
+ + + {claimData.operatorTip} PINTO + PINTO + + ), + }, + { type: "context", content: "per execution to Operator" }, + ]} + size="sm" + /> + +
+
+
+ ); +} diff --git a/src/components/Tractor/AutomateClaim/ConditionSection.tsx b/src/components/Tractor/AutomateClaim/ConditionSection.tsx new file mode 100644 index 000000000..50e51142a --- /dev/null +++ b/src/components/Tractor/AutomateClaim/ConditionSection.tsx @@ -0,0 +1,148 @@ +import { Col, Row } from "@/components/Container"; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/Accordion"; +import { isValidCustomValue } from "@/lib/Tractor/claimOrder/automate-claim-helpers"; +import type { ClaimFrequencyPreset } from "@/lib/Tractor/claimOrder/tractor-claim-types"; +import { cn } from "@/utils/utils"; +import { CheckIcon } from "@radix-ui/react-icons"; + +// ──────────────────────────────────────────────────────────────────────────────── +// Types +// ──────────────────────────────────────────────────────────────────────────────── + +export interface ConditionSectionProps { + /** Section title shown in the accordion header (e.g. "Mow my Silo"). */ + title: string; + /** Whether a preset has been selected (shows green checkmark when true). */ + enabled: boolean; + /** Currently selected preset, or null if none selected. */ + preset: ClaimFrequencyPreset | null; + /** Value for the custom numeric input field. */ + customValue: string; + /** Callback when a preset button is clicked. */ + onPresetChange: (preset: ClaimFrequencyPreset | null) => void; + /** Callback when the custom input value changes. */ + onCustomValueChange: (value: string) => void; + /** Unit label shown inside the custom input (e.g. "Stalk" or "PINTO"). */ + unit?: string; +} + +// ──────────────────────────────────────────────────────────────────────────────── +// Preset button metadata +// ──────────────────────────────────────────────────────────────────────────────── + +interface PresetMeta { + key: ClaimFrequencyPreset; + label: string; +} + +const PRESET_BUTTONS: PresetMeta[] = [ + { key: "high", label: "Aggressively" }, + { key: "medium", label: "Medium-Aggressively" }, + { key: "low", label: "Not-Aggressively" }, + { key: "custom", label: "Custom" }, +]; + +// ──────────────────────────────────────────────────────────────────────────────── +// Component +// ──────────────────────────────────────────────────────────────────────────────── + +export const ConditionSection = ({ + title, + enabled, + preset, + customValue, + onPresetChange, + onCustomValueChange, + unit, +}: ConditionSectionProps) => { + return ( + + + {/* Header: title + green checkmark + chevron */} + + + {title} + {enabled && } + + + + {/* Expanded content: preset buttons + optional custom input */} + + + {/* Preset buttons grid */} +
+ {PRESET_BUTTONS.map((btn) => { + const isSelected = preset === btn.key; + + // When "custom" is selected, show inline input instead of button + if (btn.key === "custom" && isSelected) { + const isValid = isValidCustomValue(customValue); + + return ( +
+ onCustomValueChange(e.target.value)} + className={cn( + "w-full text-center pinto-xs bg-transparent outline-none", + isValid + ? "text-pinto-green-4 placeholder:text-pinto-green-4/50" + : "text-pinto-gray-4 placeholder:text-pinto-gray-3", + )} + /> +
+ ); + } + + return ( + onPresetChange(isSelected ? null : btn.key)} + /> + ); + })} +
+ +
+
+
+ ); +}; + +// ──────────────────────────────────────────────────────────────────────────────── +// Internal: Preset Button +// ──────────────────────────────────────────────────────────────────────────────── + +const PresetButton = ({ + label, + selected, + onClick, +}: { + label: string; + selected: boolean; + onClick: () => void; +}) => ( + +); diff --git a/src/components/Tractor/AutomateClaim/SpecifyConditionsDialog.tsx b/src/components/Tractor/AutomateClaim/SpecifyConditionsDialog.tsx new file mode 100644 index 000000000..6f32d89cc --- /dev/null +++ b/src/components/Tractor/AutomateClaim/SpecifyConditionsDialog.tsx @@ -0,0 +1,331 @@ +import { Col, Row } from "@/components/Container"; +import { Form } from "@/components/Form"; +import ReviewTractorOrderDialog from "@/components/ReviewTractorOrderDialog"; +import TooltipSimple from "@/components/TooltipSimple"; +import { Button } from "@/components/ui/Button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, +} from "@/components/ui/Dialog"; +import IconImage from "@/components/ui/IconImage"; +import { Separator } from "@/components/ui/Separator"; +import { useAutomateClaimOrder } from "@/hooks/tractor/useAutomateClaimOrder"; +import { estimatedTotalTipRange } from "@/lib/Tractor/claimOrder"; +import { isValidCustomValue } from "@/lib/Tractor/claimOrder/automate-claim-helpers"; +import type { ClaimFrequencyPreset } from "@/lib/Tractor/claimOrder/tractor-claim-types"; +import { queryKeys } from "@/state/queryKeys"; +import useTractorOperatorAverageTipPaid from "@/state/tractor/useTractorOperatorAverageTipPaid"; +import { useFarmerSilo } from "@/state/useFarmerSilo"; +import { useMainToken } from "@/state/useTokenData"; +import { formatter } from "@/utils/format"; +import { useQueryClient } from "@tanstack/react-query"; +import { useCallback, useMemo, useState } from "react"; +import { useWatch } from "react-hook-form"; +import { toast } from "sonner"; +import { + OperatorTipFormField, + type TractorOperatorTipStrategy, + getTractorOperatorTipAmountFromPreset, +} from "../form/fields/sharedFields"; +import { defaultAutomateClaimValues, useAutomateClaimForm } from "../form/schema/automateClaim.schema"; +import { ConditionSection } from "./ConditionSection"; + +// ──────────────────────────────────────────────────────────────────────────────── +// Preset display configs +// ──────────────────────────────────────────────────────────────────────────────── + +// ──────────────────────────────────────────────────────────────────────────────── +// Props +// ──────────────────────────────────────────────────────────────────────────────── + +export interface SpecifyConditionsDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +// ──────────────────────────────────────────────────────────────────────────────── +// Component +// ──────────────────────────────────────────────────────────────────────────────── + +export const SpecifyConditionsDialog = ({ open, onOpenChange }: SpecifyConditionsDialogProps) => { + const mainToken = useMainToken(); + const farmerSilo = useFarmerSilo(); + const { data: averageTipPaid = 0.15 } = useTractorOperatorAverageTipPaid(); + const queryClient = useQueryClient(); + + // Form state + const { form } = useAutomateClaimForm(); + + // Operator tip preset state + const [operatorTipPreset, setOperatorTipPreset] = useState("Normal"); + + // Blueprint creation state + const { state, orderData, isLoading, handleCreateBlueprint } = useAutomateClaimOrder(); + const [showReviewDialog, setShowReviewDialog] = useState(false); + + // Watch form values for condition sections + const [ + mowPreset, + plantPreset, + harvestPreset, + mowCustomValue, + plantCustomValue, + harvestCustomValue, + customOperatorTip, + ] = useWatch({ + control: form.control, + name: [ + "mowPreset", + "plantPreset", + "harvestPreset", + "mowCustomValue", + "plantCustomValue", + "harvestCustomValue", + "customOperatorTip", + ], + }); + + // Derived: which operations are enabled (a preset has been selected) + const mowEnabled = mowPreset !== null; + const plantEnabled = plantPreset !== null; + const harvestEnabled = harvestPreset !== null; + + // ── Preset change handlers ────────────────────────────────────────────────── + + const handleMowPresetChange = useCallback( + (preset: ClaimFrequencyPreset | null) => { + form.setValue("mowPreset", preset, { shouldValidate: true }); + form.setValue("mowEnabled", preset !== null, { shouldValidate: true }); + }, + [form], + ); + + const handlePlantPresetChange = useCallback( + (preset: ClaimFrequencyPreset | null) => { + form.setValue("plantPreset", preset, { shouldValidate: true }); + form.setValue("plantEnabled", preset !== null, { shouldValidate: true }); + }, + [form], + ); + + const handleHarvestPresetChange = useCallback( + (preset: ClaimFrequencyPreset | null) => { + form.setValue("harvestPreset", preset, { shouldValidate: true }); + form.setValue("harvestEnabled", preset !== null, { shouldValidate: true }); + }, + [form], + ); + + // ── Submit handler ─────────────────────────────────────────────────────────── + + const handleSubmit = useCallback( + async (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + // Validate the form first + const isValid = await form.trigger(); + if (!isValid) return; + + try { + await handleCreateBlueprint(form, averageTipPaid, operatorTipPreset, farmerSilo.deposits, { + onSuccess: () => { + setShowReviewDialog(true); + }, + onFailure: () => { + toast.error("Failed to create Automate Claim blueprint"); + }, + }); + } catch (error) { + console.error("[SpecifyConditionsDialog] Error creating blueprint:", error); + toast.error("Failed to create Automate Claim blueprint"); + } + }, + [form, handleCreateBlueprint, averageTipPaid, operatorTipPreset, farmerSilo.deposits], + ); + + // ── Estimated tip range ───────────────────────────────────────────────────── + + const tipRange = useMemo(() => { + const enabledCount = (mowEnabled ? 1 : 0) + (plantEnabled ? 1 : 0) + (harvestEnabled ? 1 : 0); + + if (enabledCount === 0) { + return { min: 0n, max: 0n }; + } + + const tipTV = getTractorOperatorTipAmountFromPreset( + operatorTipPreset, + averageTipPaid, + customOperatorTip, + mainToken.decimals, + ); + + if (!tipTV) { + return { min: 0n, max: 0n }; + } + + return estimatedTotalTipRange(tipTV.toBigInt(), enabledCount); + }, [ + mowEnabled, + plantEnabled, + harvestEnabled, + operatorTipPreset, + averageTipPaid, + customOperatorTip, + mainToken.decimals, + ]); + + // Check if submit should be disabled + const hasAnyEnabled = mowEnabled || plantEnabled || harvestEnabled; + + const hasInvalidCustom = + (mowPreset === "custom" && !isValidCustomValue(mowCustomValue)) || + (plantPreset === "custom" && !isValidCustomValue(plantCustomValue)) || + (harvestPreset === "custom" && !isValidCustomValue(harvestCustomValue)); + + if (!open) return null; + + return ( + <> + {/* modal={false} to prevent operator tip popover from blocking input events */} + + + + + + Specify conditions + + Configure which Silo claim operations to automate and their execution thresholds. + + + +
+ + {/* Conditions area */} + + + + {/* Condition Sections */} + + form.setValue("mowCustomValue", v)} + unit="Stalk" + /> + form.setValue("plantCustomValue", v)} + unit="PINTO" + /> + form.setValue("harvestCustomValue", v)} + unit="PINTO" + /> + + + + {/* Fixed footer: always pinned to bottom */} + + + + + + + + + + + + +
+
+
+ + {/* Review → Sign → Publish dialog */} + {showReviewDialog && state && orderData && ( + { + queryClient.invalidateQueries({ queryKey: [queryKeys.base.tractor] }); + form.reset({ ...defaultAutomateClaimValues }); + setShowReviewDialog(false); + onOpenChange(false); + }} + orderData={{ + type: "automateClaim" as const, + ...orderData, + }} + encodedData={state.encodedData} + operatorPasteInstrs={state.operatorPasteInstructions} + blueprint={state.blueprint} + depositOptimizationCalls={state.depositOptimizationCalls} + /> + )} + + ); +}; + +// ──────────────────────────────────────────────────────────────────────────────── +// Internal: Estimated Total Tip Range display +// ──────────────────────────────────────────────────────────────────────────────── + +const EstimatedTotalTipRange = ({ + tipRange, + mainToken, +}: { + tipRange: { min: bigint; max: bigint }; + mainToken: ReturnType; +}) => { + const formatTip = (value: bigint) => { + const human = Number(value) / 10 ** mainToken.decimals; + return formatter.number(human.toString(), { minDecimals: 2, maxDecimals: 3 }); + }; + + return ( + + +
Estimated Total Tip
+ +
+ + + {formatTip(tipRange.min)} - {formatTip(tipRange.max)} + +
+ ); +}; diff --git a/src/components/Tractor/AutomateClaim/index.ts b/src/components/Tractor/AutomateClaim/index.ts new file mode 100644 index 000000000..04eabd7a4 --- /dev/null +++ b/src/components/Tractor/AutomateClaim/index.ts @@ -0,0 +1,4 @@ +export { SpecifyConditionsDialog } from "./SpecifyConditionsDialog"; +export { ConditionSection } from "./ConditionSection"; +export { AutomateClaimVisualization } from "./AutomateClaimVisualization"; +export { AutomateClaimExecutionHistory } from "./AutomateClaimExecutionHistory"; diff --git a/src/components/Tractor/HighlightedCallData.tsx b/src/components/Tractor/HighlightedCallData.tsx index 7052342b9..ba9bc6652 100644 --- a/src/components/Tractor/HighlightedCallData.tsx +++ b/src/components/Tractor/HighlightedCallData.tsx @@ -74,6 +74,52 @@ function ConvertUpBlueprintDisplay({ params }: { params: any }) { ); } +function AutomateClaimBlueprintDisplay({ params }: { params: any }) { + return ( +
+
Function: automateClaimBlueprint
+
+
minMowAmount: {params.claimParams.minMowAmount.toString()}
+
minPlantAmount: {params.claimParams.minPlantAmount.toString()}
+
+ fieldHarvestConfigs: + {params.claimParams.fieldHarvestConfigs.length > 0 ? ( +
+ {params.claimParams.fieldHarvestConfigs.map((config: any, index: number) => ( +
+ [{index}] fieldId: {config.fieldId.toString()}, minHarvestAmount: {config.minHarvestAmount.toString()} +
+ ))} +
+ ) : ( + [] + )} +
+
minRinseAmount: {params.claimParams.minRinseAmount.toString()}
+
minUnripeClaimAmount: {params.claimParams.minUnripeClaimAmount.toString()}
+
+ operatorTips: +
+
mowTipAmount: {params.opParams.mowTipAmount.toString()}
+
plantTipAmount: {params.opParams.plantTipAmount.toString()}
+
harvestTipAmount: {params.opParams.harvestTipAmount.toString()}
+
rinseTipAmount: {params.opParams.rinseTipAmount.toString()}
+
unripeClaimTipAmount: {params.opParams.unripeClaimTipAmount.toString()}
+
+
+
+ operatorParams: +
+
operatorTipAmount: {params.opParams.baseOpParams.operatorTipAmount.toString()}
+
tipAddress: {params.opParams.baseOpParams.tipAddress}
+
whitelistedOperators: [{params.opParams.baseOpParams.whitelistedOperators.join(", ")}]
+
+
+
+
+ ); +} + function GenericParameterDisplay({ selector, data, @@ -147,7 +193,7 @@ function RequisitionDataDisplay({ } } -const knownBlueprintTypes = new Set(["sow", "convertUp"]); +const knownBlueprintTypes = new Set(["sow", "convertUp", "automateClaim"]); export function HighlightedCallData({ blueprintData, @@ -176,6 +222,8 @@ export function HighlightedCallData({ return ; case "convertUp": return ; + case "automateClaim": + return ; case "generic": return ( ; + executions?: PublisherTractorExecution[]; + onOrderClick: (req: RequisitionEvent) => void; + onCancelClick: (req: RequisitionEvent, e: React.MouseEvent) => void; + isSubmitting?: boolean; + isConfirming?: boolean; +} + +const FarmerTractorAutomateClaimOrderCard = ({ + req, + executions, + onOrderClick, + onCancelClick, + isSubmitting = false, + isConfirming = false, +}: FarmerTractorAutomateClaimOrderCardProps) => { + if (req.requisitionType !== "automateClaimBlueprint" || !req.decodedData) return null; + + const transformed = transformAutomateClaimRequisitionEvent(req.decodedData); + if (!transformed) return null; + + const { mowEnabled, plantEnabled, harvestEnabled } = getEnabledClaimOps(transformed); + const enabledOps = getEnabledClaimOpLabels({ mowEnabled, plantEnabled, harvestEnabled }); + + const operatorTip = TokenValue.fromBlockchain(transformed.operatorParams.operatorTipAmount, 6); + + // Build condition labels with threshold details + const conditions: { text: string }[] = []; + if (mowEnabled) { + conditions.push({ text: getClaimOpConditionLabel("mow", true, transformed.claimParams.minMowAmount) }); + } + if (plantEnabled) { + conditions.push({ text: getClaimOpConditionLabel("plant", true, transformed.claimParams.minPlantAmount) }); + } + if (harvestEnabled) { + const harvestThreshold = transformed.claimParams.fieldHarvestConfigs[0]?.minHarvestAmount ?? 0n; + conditions.push({ text: getClaimOpConditionLabel("harvest", true, harvestThreshold) }); + } + + const blueprintExecutions = executions || []; + const executionCount = blueprintExecutions.length; + const publishDate = req.timestamp ? format(new Date(req.timestamp), "dd MMM yyyy") : "Unknown"; + + return ( + + onOrderClick(req)} + > +
+
+ + {enabledOps.join(", ")} + + ), + }, + ]} + /> + +
+ + +
+
+ + +
+ + Published {publishDate} +
+ + + + Executed {executionCount} time{executionCount !== 1 ? "s" : ""} + + + + + +
+ + ); +}; + +export default FarmerTractorAutomateClaimOrderCard; diff --git a/src/components/Tractor/farmer-orders/TractorFarmerMixedOrders.types.ts b/src/components/Tractor/farmer-orders/TractorFarmerMixedOrders.types.ts index 34730c8e6..86f17f033 100644 --- a/src/components/Tractor/farmer-orders/TractorFarmerMixedOrders.types.ts +++ b/src/components/Tractor/farmer-orders/TractorFarmerMixedOrders.types.ts @@ -1,4 +1,6 @@ +import { AutomateClaimOrderData as BaseAutomateClaimOrderData } from "@/components/Tractor/types"; import { PublisherTractorExecution, SowBlueprintData, TractorRequisitionEvent } from "@/lib/Tractor"; +import { AutomateClaimBlueprintStruct } from "@/lib/Tractor/claimOrder/tractor-claim-types"; import { ConvertUpOrderbookEntry } from "@/lib/Tractor/convertUp/tractor-convert-up-types"; import { OrderType } from "./TractorFarmerOrderTypeRegistry"; @@ -14,13 +16,16 @@ export interface UnifiedTractorOrder { isComplete?: boolean; // Order-specific data (discriminated union) - orderData: SowOrderData | ConvertUpOrderData; + orderData: SowOrderData | ConvertUpOrderData | AutomateClaimOrderData; // Executions executions?: PublisherTractorExecution[]; // Raw data for dialogs - requisition: ConvertUpOrderbookEntry | TractorRequisitionEvent; + requisition: + | ConvertUpOrderbookEntry + | TractorRequisitionEvent + | TractorRequisitionEvent; } // Sow-specific order data @@ -50,6 +55,11 @@ export interface ConvertUpOrderData { strategy: string; } +// AutomateClaim-specific order data (extends base with percentComplete for unified order tracking) +export interface AutomateClaimOrderData extends BaseAutomateClaimOrderData { + percentComplete: number; +} + // Type guards export function isSowOrder(order: UnifiedTractorOrder): order is UnifiedTractorOrder & { orderData: SowOrderData; @@ -64,6 +74,13 @@ export function isConvertUpOrder( return order.orderData.type === "convertUp" && order.requisition.requisitionType === "convertUpBlueprint"; } +export function isAutomateClaimOrder(order: UnifiedTractorOrder): order is UnifiedTractorOrder & { + orderData: AutomateClaimOrderData; + requisition: TractorRequisitionEvent; +} { + return order.orderData.type === "automateClaim" && order.requisition.requisitionType === "automateClaimBlueprint"; +} + // Sorting options for mixed orders export type MixedOrderSortBy = "newest" | "oldest" | "type" | "operatorTip" | "percentComplete" | "publisher"; diff --git a/src/components/Tractor/farmer-orders/TractorFarmerMixedOrders.utils.ts b/src/components/Tractor/farmer-orders/TractorFarmerMixedOrders.utils.ts index 571a00297..5b5ca30ff 100644 --- a/src/components/Tractor/farmer-orders/TractorFarmerMixedOrders.utils.ts +++ b/src/components/Tractor/farmer-orders/TractorFarmerMixedOrders.utils.ts @@ -1,8 +1,15 @@ import { TokenValue } from "@/classes/TokenValue"; import { PublisherTractorExecution, SowBlueprintData, TractorRequisitionEvent } from "@/lib/Tractor"; +import { + getEnabledClaimOpLabels, + getEnabledClaimOps, + transformAutomateClaimRequisitionEvent, +} from "@/lib/Tractor/claimOrder"; +import { AutomateClaimBlueprintStruct } from "@/lib/Tractor/claimOrder/tractor-claim-types"; import { ConvertUpOrderbookEntry } from "@/lib/Tractor/convertUp/tractor-convert-up-types"; import { getTokenNameByIndex } from "@/utils/token"; import { + AutomateClaimOrderData, ConvertUpOrderData, MixedOrderFilters, MixedOrderSortBy, @@ -144,6 +151,46 @@ export function transformConvertUpOrderToUnified( }; } +// Transform AutomateClaim order to unified format +export function transformAutomateClaimOrderToUnified( + req: TractorRequisitionEvent, + executions: PublisherTractorExecution[] = [], +): UnifiedTractorOrder { + if (!req.decodedData) { + throw new Error("Missing decoded data for AutomateClaim order"); + } + + const transformed = transformAutomateClaimRequisitionEvent(req.decodedData); + if (!transformed) { + throw new Error("Failed to transform AutomateClaim data"); + } + + const { mowEnabled, plantEnabled, harvestEnabled } = getEnabledClaimOps(transformed); + const operatorTip = TokenValue.fromBlockchain(transformed.operatorParams.operatorTipAmount, 6).toHuman(); + + const automateClaimOrderData: AutomateClaimOrderData = { + type: "automateClaim", + mowEnabled, + plantEnabled, + harvestEnabled, + operatorTip, + percentComplete: 0, + }; + + return { + id: req.requisition.blueprintHash, + type: "automateClaim", + timestamp: req.timestamp, + blockNumber: req.blockNumber, + publisher: req.requisition.blueprint.publisher, + isCancelled: req.isCancelled, + isComplete: false, + orderData: automateClaimOrderData, + executions, + requisition: req, + }; +} + // Sort unified orders export function sortUnifiedOrders(orders: UnifiedTractorOrder[], sortBy: MixedOrderSortBy): UnifiedTractorOrder[] { const sortedOrders = [...orders]; @@ -213,6 +260,11 @@ export function filterUnifiedOrders(orders: UnifiedTractorOrder[], filters: Mixe // Format order summary for display export function getOrderSummary(order: UnifiedTractorOrder): string { + if (order.type === "automateClaim") { + const data = order.orderData as AutomateClaimOrderData; + const ops = getEnabledClaimOpLabels(data); + return `Automate Claim • ${ops.join(", ")} • Active`; + } const typeLabel = order.type === "sow" ? "Sow" : "Convert Up"; const amount = order.type === "sow" @@ -223,7 +275,7 @@ export function getOrderSummary(order: UnifiedTractorOrder): string { } // Get order type badge info -export function getOrderTypeBadge(orderType: "sow" | "convertUp") { +export function getOrderTypeBadge(orderType: "sow" | "convertUp" | "automateClaim") { switch (orderType) { case "sow": return { @@ -237,5 +289,11 @@ export function getOrderTypeBadge(orderType: "sow" | "convertUp") { className: "bg-green-100 text-green-800 border-green-200", icon: "⬆️", }; + case "automateClaim": + return { + label: "Automate Claim", + className: "bg-green-100 text-green-800 border-green-200", + icon: "🔄", + }; } } diff --git a/src/components/Tractor/farmer-orders/TractorFarmerOrderTypeRegistry.tsx b/src/components/Tractor/farmer-orders/TractorFarmerOrderTypeRegistry.tsx index c9aa62eda..c9f7d750d 100644 --- a/src/components/Tractor/farmer-orders/TractorFarmerOrderTypeRegistry.tsx +++ b/src/components/Tractor/farmer-orders/TractorFarmerOrderTypeRegistry.tsx @@ -7,12 +7,17 @@ import { SowBlueprintData, decodeSowTractorData, } from "@/lib/Tractor"; +import { + decodeAutomateClaimBlueprint, + getEnabledClaimOps, + transformAutomateClaimRequisitionEvent, +} from "@/lib/Tractor/claimOrder"; +import { AutomateClaimBlueprintStruct } from "@/lib/Tractor/claimOrder/tractor-claim-types"; import { decodeConvertUpTractorOrder } from "@/lib/Tractor/convertUp/tractor-convert-up"; import { ConvertUpOrderbookEntry } from "@/lib/Tractor/convertUp/tractor-convert-up-types"; import { prepareRequisitionEventForTxn } from "@/lib/Tractor/utils"; -import React from "react"; import { base } from "viem/chains"; -import { Col } from "../../Container"; +import { AutomateClaimExecutionHistory, AutomateClaimVisualization } from "../AutomateClaim"; import ConvertUpExecutionHistory from "../executions/ConvertUpExecutionHistory"; import SowExecutionHistory from "../executions/SowExecutionHistory"; import { OrderTypeConfig, TractorOrderData } from "../types"; @@ -77,6 +82,30 @@ const ConvertUpOrderDescription = ({ isViewOnly }: { isViewOnly: boolean }) => { ); }; +// Automate Claim Order Description +const AutomateClaimDescription = ({ isViewOnly }: { isViewOnly: boolean }) => { + if (isViewOnly) { + return ( + + This is your active Automate Claim Order. It allows an Operator to execute Silo claim operations (Mow, Plant, + Harvest) for you when the conditions are met. + + ); + } + + return ( + + + An Automate Claim Order allows you to pay an Operator to execute Silo claim operations (Mow, Plant, Harvest) for + you automatically. + + + This allows you to interact with the Pinto protocol autonomously when the conditions of your Order are met. + + + ); +}; + // Transform functions for order data const transformSowOrderData = ( req: RequisitionEvent, @@ -125,6 +154,19 @@ const transformConvertUpOrderData = ( }; }; +const transformAutomateClaimOrderData = (req: RequisitionEvent): TractorOrderData => { + if (!req.decodedData) throw new Error("Missing decoded data for AutomateClaim order"); + + const transformed = transformAutomateClaimRequisitionEvent(req.decodedData); + if (!transformed) throw new Error("Failed to transform AutomateClaim data"); + + return { + type: "automateClaim", + ...getEnabledClaimOps(transformed), + operatorTip: transformed.operatorParams.operatorTipAmountAsString, + }; +}; + // Unified transform function with overloads for type safety function transformOrderData( req: RequisitionEvent, @@ -132,7 +174,11 @@ function transformOrderData( ): TractorOrderData; function transformOrderData(req: ConvertUpOrderbookEntry, getStrategyProps: GetStrategyProps): TractorOrderData; function transformOrderData( - req: RequisitionEvent | ConvertUpOrderbookEntry, + req: RequisitionEvent, + getStrategyProps: GetStrategyProps, +): TractorOrderData; +function transformOrderData( + req: RequisitionEvent | ConvertUpOrderbookEntry | RequisitionEvent, getStrategyProps: GetStrategyProps, ): TractorOrderData { if (isSowRequest(req)) { @@ -141,6 +187,9 @@ function transformOrderData( if (isConvertUpRequest(req)) { return transformConvertUpOrderData(req, getStrategyProps); } + if (isAutomateClaimRequest(req)) { + return transformAutomateClaimOrderData(req); + } throw new Error("Unknown request type for order transformation"); } @@ -166,16 +215,21 @@ function isConvertUpRequest(req: any): req is ConvertUpOrderbookEntry { return req && "orderInfo" in req && "totalAvailableBdv" in req; } +function isAutomateClaimRequest(req: any): req is RequisitionEvent { + return req && "requisitionType" in req && req.requisitionType === "automateClaimBlueprint"; +} + // Type for the strategy props hook type GetStrategyProps = ReturnType; // Type for decoded data type DecodedSowData = ReturnType; type DecodedConvertUpData = ReturnType; +type DecodedAutomateClaimData = ReturnType; // Extended OrderTypeConfig interface with only the used fields export interface ExtendedOrderTypeConfig extends OrderTypeConfig { - decodeData: (blueprintData: `0x${string}`) => DecodedSowData | DecodedConvertUpData | null; + decodeData: (blueprintData: `0x${string}`) => DecodedSowData | DecodedConvertUpData | DecodedAutomateClaimData | null; prepareForCancellation?: typeof prepareForCancellation; transformOrderData: typeof transformOrderData; } @@ -211,6 +265,21 @@ export const ORDER_TYPE_REGISTRY = { prepareForCancellation: prepareForCancellation, transformOrderData: transformOrderData, }, + + automateClaim: { + // UI Components + visualization: AutomateClaimVisualization, + executionHistory: AutomateClaimExecutionHistory, + + // Metadata + title: "Review Automate Claim Order", + description: (isViewOnly: boolean) => , + + // Data Handling + decodeData: decodeAutomateClaimBlueprint, + prepareForCancellation: prepareForCancellation, + transformOrderData: transformOrderData, + }, } as const satisfies Record; // Helper function to get order configuration by type diff --git a/src/components/Tractor/farmer-orders/TractorOrdersPanel.tsx b/src/components/Tractor/farmer-orders/TractorOrdersPanel.tsx index 4ee224833..5d66cc7a4 100644 --- a/src/components/Tractor/farmer-orders/TractorOrdersPanel.tsx +++ b/src/components/Tractor/farmer-orders/TractorOrdersPanel.tsx @@ -10,6 +10,7 @@ import useTransaction from "@/hooks/useTransaction"; import { PublisherTractorExecution } from "@/lib/Tractor"; import type { Blueprint } from "@/lib/Tractor"; import { queryKeys } from "@/state/queryKeys"; +import { useTractorAutomateClaimOrderbook } from "@/state/tractor/useTractorAutomateClaimOrders"; import { useTractorConvertUpOrderbook } from "@/state/tractor/useTractorConvertUpOrders"; import usePublisherTractorExecutions from "@/state/tractor/useTractorExecutions"; import useTractorOperatorAverageTipPaid from "@/state/tractor/useTractorOperatorAverageTipPaid"; @@ -24,12 +25,14 @@ import { toast } from "sonner"; import { useAccount } from "wagmi"; import ModifyConvertUpOrderDialog from "../ModifyConvertUpOrderDialog"; import ModifyTractorOrderDialog from "../ModifySowOrderDialog"; +import FarmerTractorAutomateClaimOrderCard from "./FarmerTractorAutomateClaimOrderCard"; import FarmerTractorConvertUpOrderCard from "./FarmerTractorConvertUpOrderCard"; import FarmerTractorSowOrderCard from "./FarmerTractorSowOrderCard"; import { MixedOrderFilters, MixedOrderSortBy, UnifiedTractorOrder, + isAutomateClaimOrder, isConvertUpOrder, isSowOrder, } from "./TractorFarmerMixedOrders.types"; @@ -37,6 +40,7 @@ import { filterUnifiedOrders, getOrderTypeBadge, sortUnifiedOrders, + transformAutomateClaimOrderToUnified, transformConvertUpOrderToUnified, transformSowOrderToUnified, } from "./TractorFarmerMixedOrders.utils"; @@ -50,7 +54,7 @@ interface TractorOrdersPanelProps { initialFilters?: Partial; } -const ORDER_TYPES: OrderType[] = ["sow", "convertUp"]; +const ORDER_TYPES: OrderType[] = ["sow", "convertUp", "automateClaim"]; function TractorOrdersPanelGeneric({ orderTypes: _orderTypes = ORDER_TYPES, @@ -96,6 +100,11 @@ function TractorOrdersPanelGeneric({ enabled: !!address && filters.orderTypes.includes("convertUp"), }); + const { data: automateClaimOrders, ...automateClaimOrdersQuery } = useTractorAutomateClaimOrderbook({ + address, + enabled: !!address && filters.orderTypes.includes("automateClaim"), + }); + const executionsByHash = useMemo(() => { const byHash = executions?.reduce>((acc, curr) => { if (curr.blueprintHash in acc) acc[curr.blueprintHash].push(curr); @@ -152,10 +161,27 @@ function TractorOrdersPanelGeneric({ }); } + // Process AutomateClaim orders + if (filters.orderTypes.includes("automateClaim") && automateClaimOrders) { + automateClaimOrders + .filter((req) => stringEq(req.requisition.blueprint.publisher, address)) + .forEach((req) => { + const decodedData = + req.decodedData ?? ORDER_TYPE_REGISTRY.automateClaim.decodeData(req.requisition.blueprint.data); + if (!decodedData) { + return; + } + const reqWithDecodedData = { ...req, decodedData }; + const orderExecutions = executionsByHash?.[req.requisition.blueprintHash] || []; + + unified.push(transformAutomateClaimOrderToUnified(reqWithDecodedData, orderExecutions)); + }); + } + // Apply filters and sorting const filtered = filterUnifiedOrders(unified, filters); return sortUnifiedOrders(filtered, currentSortBy); - }, [sowOrders, convertUpOrders, executions, address, filters, currentSortBy, executionsByHash]); + }, [sowOrders, convertUpOrders, automateClaimOrders, executions, address, filters, currentSortBy, executionsByHash]); // Loading and error states const dataHasLoaded = address ? Boolean(executions !== undefined) : true; @@ -163,9 +189,11 @@ function TractorOrdersPanelGeneric({ executionsQuery.isLoading || (filters.orderTypes.includes("sow") && sowOrdersQuery.isLoading) || (filters.orderTypes.includes("convertUp") && convertUpOrdersQuery.isLoading) || + (filters.orderTypes.includes("automateClaim") && automateClaimOrdersQuery.isLoading) || !dataHasLoaded; - const error = executionsQuery.error || sowOrdersQuery.error || convertUpOrdersQuery.error; + const error = + executionsQuery.error || sowOrdersQuery.error || convertUpOrdersQuery.error || automateClaimOrdersQuery.error; const [lastRefetchedCounter, setLastRefetchedCounter] = useState(0); @@ -180,6 +208,9 @@ function TractorOrdersPanelGeneric({ if (filters.orderTypes.includes("convertUp")) { convertUpOrdersQuery.refetch(); } + if (filters.orderTypes.includes("automateClaim")) { + automateClaimOrdersQuery.refetch(); + } } }, [refreshData, dataHasLoaded, filters.orderTypes]); @@ -189,7 +220,7 @@ function TractorOrdersPanelGeneric({ errorMessage: "Failed to cancel order", successCallback: useCallback(() => { // Invalidate tractor-related queries to refresh order data - queryClient.invalidateQueries({ queryKey: queryKeys.base.tractor }); + queryClient.invalidateQueries({ queryKey: [queryKeys.base.tractor] }); }, [queryClient]), }); @@ -316,6 +347,17 @@ function TractorOrdersPanelGeneric({ isConfirming={isConfirming} /> )} + + {isAutomateClaimOrder(order) && ( + handleOrderClick(order)} + onCancelClick={(_, e) => handleCancelOrder(order, e)} + isSubmitting={submitting} + isConfirming={isConfirming} + /> + )}
); diff --git a/src/components/Tractor/form/SowOrderV0Fields.tsx b/src/components/Tractor/form/SowOrderV0Fields.tsx index 0d6e7d64e..b210b0fb0 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/Tractor/form/schema/automateClaim.schema.ts b/src/components/Tractor/form/schema/automateClaim.schema.ts new file mode 100644 index 000000000..b36eed425 --- /dev/null +++ b/src/components/Tractor/form/schema/automateClaim.schema.ts @@ -0,0 +1,127 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useCallback } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +// ──────────────────────────────────────────────────────────────────────────────── +// SCHEMA +// ──────────────────────────────────────────────────────────────────────────────── + +const claimFrequencyPreset = z.enum(["high", "medium", "low", "custom"]).nullable().default(null); + +export const automateClaimSchemaErrors = { + noOperationEnabled: "At least one operation (Mow, Plant, or Harvest) must be enabled", + customValueRequired: "A positive numeric value is required when Custom is selected", +} as const; + +export const automateClaimSchema = z + .object({ + mowEnabled: z.boolean().default(false), + mowPreset: claimFrequencyPreset, + mowCustomValue: z.string().default(""), + + plantEnabled: z.boolean().default(false), + plantPreset: claimFrequencyPreset, + plantCustomValue: z.string().default(""), + + harvestEnabled: z.boolean().default(false), + harvestPreset: claimFrequencyPreset, + harvestCustomValue: z.string().default(""), + + operatorTipPreset: z.enum(["Custom", "Low", "Normal", "High"] as const).default("Normal"), + customOperatorTip: z.string().default(""), + }) + .superRefine((data, ctx) => { + // At least one operation must be enabled (Req 4.1) + if (!data.mowEnabled && !data.plantEnabled && !data.harvestEnabled) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: automateClaimSchemaErrors.noOperationEnabled, + path: ["mowEnabled"], + }); + } + + // Custom preset validation — positive numeric value required (Req 4.2, 4.3) + const operations = [ + { enabled: data.mowEnabled, preset: data.mowPreset, customValue: data.mowCustomValue, field: "mowCustomValue" }, + { + enabled: data.plantEnabled, + preset: data.plantPreset, + customValue: data.plantCustomValue, + field: "plantCustomValue", + }, + { + enabled: data.harvestEnabled, + preset: data.harvestPreset, + customValue: data.harvestCustomValue, + field: "harvestCustomValue", + }, + ] as const; + + for (const op of operations) { + if (op.enabled && op.preset === "custom") { + const trimmed = op.customValue.trim(); + const num = Number(trimmed); + if (!trimmed || Number.isNaN(num) || num <= 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: automateClaimSchemaErrors.customValueRequired, + path: [op.field], + }); + } + } + } + }); + +// ──────────────────────────────────────────────────────────────────────────────── +// TYPES +// ──────────────────────────────────────────────────────────────────────────────── + +export type AutomateClaimFormValues = z.infer; + +// ──────────────────────────────────────────────────────────────────────────────── +// DEFAULT VALUES +// ──────────────────────────────────────────────────────────────────────────────── + +export const defaultAutomateClaimValues: AutomateClaimFormValues = { + mowEnabled: false, + mowPreset: null, + mowCustomValue: "", + + plantEnabled: false, + plantPreset: null, + plantCustomValue: "", + + harvestEnabled: false, + harvestPreset: null, + harvestCustomValue: "", + + operatorTipPreset: "Normal", + customOperatorTip: "", +}; + +// ──────────────────────────────────────────────────────────────────────────────── +// FORM HOOK +// ──────────────────────────────────────────────────────────────────────────────── + +export type IAutomateClaimForm = { + form: ReturnType>; + prefillValues: (values: Partial) => void; +}; + +export const useAutomateClaimForm = (): IAutomateClaimForm => { + const form = useForm({ + resolver: zodResolver(automateClaimSchema), + defaultValues: { ...defaultAutomateClaimValues }, + mode: "onChange", + }); + + const prefillValues = useCallback( + (values: Partial) => { + form.reset(values, { keepDirty: true }); + }, + [form.reset], + ); + + return { form, prefillValues } as const; +}; diff --git a/src/components/Tractor/types.ts b/src/components/Tractor/types.ts index 9eea42fb9..9c187ed45 100644 --- a/src/components/Tractor/types.ts +++ b/src/components/Tractor/types.ts @@ -6,7 +6,7 @@ import { TimeScaleSelect } from "./form/fields/sharedFields"; // Base interfaces for all tractor orders export interface BaseTractorOrderData { operatorTip: string; - type: "sow" | "convertUp"; + type: "sow" | "convertUp" | "automateClaim"; } // Sow-specific order data @@ -42,8 +42,16 @@ export interface ConvertUpOrderData extends BaseTractorOrderData { tokenStrategy: TractorTokenStrategyUnion; } +// AutomateClaim-specific order data +export interface AutomateClaimOrderData extends BaseTractorOrderData { + type: "automateClaim"; + mowEnabled: boolean; + plantEnabled: boolean; + harvestEnabled: boolean; +} + // Union type for all order data -export type TractorOrderData = SowOrderData | ConvertUpOrderData; +export type TractorOrderData = SowOrderData | ConvertUpOrderData | AutomateClaimOrderData; // Generic execution event interface export interface BaseExecutionEvent { diff --git a/src/components/nav/nav/Navbar.tsx b/src/components/nav/nav/Navbar.tsx index 39d9c9a8d..aa4729fbe 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 da6f6eec1..4a6072f81 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 7581cce54..c57cad248 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/context/BeanstalkMarketContext.tsx b/src/context/BeanstalkMarketContext.tsx new file mode 100644 index 000000000..c4fb08a51 --- /dev/null +++ b/src/context/BeanstalkMarketContext.tsx @@ -0,0 +1,17 @@ +import { createContext, useContext } from "react"; + +export interface BeanstalkMarketContextValue { + isBeanstalkMarketplace: boolean; + fieldId: bigint; + podMarketplaceId: string | undefined; +} + +export const BeanstalkMarketContext = createContext({ + isBeanstalkMarketplace: false, + fieldId: 0n, + podMarketplaceId: undefined, +}); + +export function useBeanstalkMarket() { + return useContext(BeanstalkMarketContext); +} diff --git a/src/encoders/createPodOrder.ts b/src/encoders/createPodOrder.ts index 15b52a97a..ac1c4528d 100644 --- a/src/encoders/createPodOrder.ts +++ b/src/encoders/createPodOrder.ts @@ -12,6 +12,7 @@ export default function createPodOrder( minFill?: TokenValue, balanceFrom?: FarmFromMode, clipboard?: `0x${string}`, + fieldId: bigint = 0n, ) { if (!amount || !pricePerPod || !maxPlaceInLine || !minFill || !balanceFrom) { return { @@ -26,7 +27,7 @@ export default function createPodOrder( args: [ { orderer: account, - fieldId: 0n, + fieldId: fieldId, pricePerPod, maxPlaceInLine: maxPlaceInLine.toBigInt(), minFillAmount: minFill.toBigInt(), diff --git a/src/encoders/fillPodListing.ts b/src/encoders/fillPodListing.ts index c5cb777ab..59c075b42 100644 --- a/src/encoders/fillPodListing.ts +++ b/src/encoders/fillPodListing.ts @@ -16,6 +16,7 @@ export default function fillPodListing( amountIn?: TokenValue, // amountIn balanceFrom?: FarmFromMode, // fromMode clipboard?: `0x${string}`, + fieldId: bigint = 0n, // fieldId ) { if ( account === undefined || @@ -41,7 +42,7 @@ export default function fillPodListing( args: [ { lister: account, // account - fieldId: 0n, + fieldId: fieldId, index: index.toBigInt(), // index start: start.toBigInt(), // start podAmount: amount.toBigInt(), // amount diff --git a/src/generated/gql/exchange/graphql.ts b/src/generated/gql/exchange/graphql.ts index 964297858..bbcf894a0 100644 --- a/src/generated/gql/exchange/graphql.ts +++ b/src/generated/gql/exchange/graphql.ts @@ -29,44 +29,6 @@ export type Scalars = { Timestamp: { input: any; output: any; } }; -export type Account = { - __typename?: 'Account'; - id: Scalars['Bytes']['output']; - trades: Array; -}; - - -export type AccountTradesArgs = { - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - where?: InputMaybe; -}; - -export type AccountFilter = { - /** Filter for the block changed event. */ - _change_block?: InputMaybe; - and?: InputMaybe>>; - id?: InputMaybe; - id_contains?: InputMaybe; - id_gt?: InputMaybe; - id_gte?: InputMaybe; - id_in?: InputMaybe>; - id_lt?: InputMaybe; - id_lte?: InputMaybe; - id_not?: InputMaybe; - id_not_contains?: InputMaybe; - id_not_in?: InputMaybe>; - or?: InputMaybe>>; - trades_?: InputMaybe; -}; - -export enum AccountOrderBy { - id = 'id', - trades = 'trades' -} - export enum AggregationInterval { day = 'day', hour = 'hour' @@ -1284,6 +1246,7 @@ export type ConvertCandidateFilter = { export enum ConvertCandidateOrderBy { addLiquidityTrade = 'addLiquidityTrade', + addLiquidityTrade__account = 'addLiquidityTrade__account', addLiquidityTrade__blockNumber = 'addLiquidityTrade__blockNumber', addLiquidityTrade__hash = 'addLiquidityTrade__hash', addLiquidityTrade__id = 'addLiquidityTrade__id', @@ -1298,6 +1261,7 @@ export enum ConvertCandidateOrderBy { addLiquidityTrade__transferVolumeUSD = 'addLiquidityTrade__transferVolumeUSD', id = 'id', removeLiquidityTrade = 'removeLiquidityTrade', + removeLiquidityTrade__account = 'removeLiquidityTrade__account', removeLiquidityTrade__blockNumber = 'removeLiquidityTrade__blockNumber', removeLiquidityTrade__hash = 'removeLiquidityTrade__hash', removeLiquidityTrade__id = 'removeLiquidityTrade__id', @@ -1402,8 +1366,6 @@ export type Query = { __typename?: 'Query'; /** Access to subgraph metadata */ _meta?: Maybe; - account?: Maybe; - accounts: Array; aquifer?: Maybe; aquifers: Array; beanstalk?: Maybe; @@ -1444,24 +1406,6 @@ export type QueryMetaArgs = { }; -export type QueryAccountArgs = { - block?: InputMaybe; - id: Scalars['ID']['input']; - subgraphError?: SubgraphErrorPolicy; -}; - - -export type QueryAccountsArgs = { - block?: InputMaybe; - first?: InputMaybe; - orderBy?: InputMaybe; - orderDirection?: InputMaybe; - skip?: InputMaybe; - subgraphError?: SubgraphErrorPolicy; - where?: InputMaybe; -}; - - export type QueryAquiferArgs = { block?: InputMaybe; id: Scalars['ID']['input']; @@ -1953,7 +1897,7 @@ export enum TokenOrderBy { export type Trade = { __typename?: 'Trade'; /** Account that sent this transaction */ - account: Account; + account: Scalars['Bytes']['output']; /** Well.reserves after this event */ afterReserves: Array; /** Well.tokenRates before this event */ @@ -2015,27 +1959,16 @@ export enum TradeType { export type TradeFilter = { /** Filter for the block changed event. */ _change_block?: InputMaybe; - account?: InputMaybe; - account_?: InputMaybe; - account_contains?: InputMaybe; - account_contains_nocase?: InputMaybe; - account_ends_with?: InputMaybe; - account_ends_with_nocase?: InputMaybe; - account_gt?: InputMaybe; - account_gte?: InputMaybe; - account_in?: InputMaybe>; - account_lt?: InputMaybe; - account_lte?: InputMaybe; - account_not?: InputMaybe; - account_not_contains?: InputMaybe; - account_not_contains_nocase?: InputMaybe; - account_not_ends_with?: InputMaybe; - account_not_ends_with_nocase?: InputMaybe; - account_not_in?: InputMaybe>; - account_not_starts_with?: InputMaybe; - account_not_starts_with_nocase?: InputMaybe; - account_starts_with?: InputMaybe; - account_starts_with_nocase?: InputMaybe; + account?: InputMaybe; + account_contains?: InputMaybe; + account_gt?: InputMaybe; + account_gte?: InputMaybe; + account_in?: InputMaybe>; + account_lt?: InputMaybe; + account_lte?: InputMaybe; + account_not?: InputMaybe; + account_not_contains?: InputMaybe; + account_not_in?: InputMaybe>; afterReserves?: InputMaybe>; afterReserves_contains?: InputMaybe>; afterReserves_contains_nocase?: InputMaybe>; @@ -2255,7 +2188,6 @@ export type TradeFilter = { export enum TradeOrderBy { account = 'account', - account__id = 'account__id', afterReserves = 'afterReserves', afterTokenRates = 'afterTokenRates', beforeReserves = 'beforeReserves', diff --git a/src/generated/gql/pintostalk/gql.ts b/src/generated/gql/pintostalk/gql.ts index 508c651f8..f64db3c39 100644 --- a/src/generated/gql/pintostalk/gql.ts +++ b/src/generated/gql/pintostalk/gql.ts @@ -22,9 +22,9 @@ type Documents = { "query BeanstalkSeasonsTable($from: Int, $to: Int) {\n seasons(\n first: 1000\n orderBy: season\n orderDirection: desc\n where: {season_gte: $from, season_lte: $to}\n ) {\n id\n sunriseBlock\n rewardBeans\n price\n deltaBeans\n raining\n season\n }\n fieldHourlySnapshots(\n first: 1000\n orderBy: season\n orderDirection: desc\n where: {field: \"0xd1a0d188e861ed9d15773a2f3574a2e94134ba8f\", season_gte: $from, season_lte: $to}\n ) {\n id\n caseId\n issuedSoil\n deltaSownBeans\n sownBeans\n deltaPodDemand\n blocksToSoldOutSoil\n podRate\n temperature\n deltaTemperature\n season\n }\n siloHourlySnapshots(\n first: 1000\n orderBy: season\n orderDirection: desc\n where: {silo: \"0xd1a0d188e861ed9d15773a2f3574a2e94134ba8f\", season_gte: $from, season_lte: $to}\n ) {\n id\n beanToMaxLpGpPerBdvRatio\n deltaBeanToMaxLpGpPerBdvRatio\n season\n }\n}": typeof types.BeanstalkSeasonsTableDocument, "query SiloSnapshots($first: Int!, $id: Bytes!) {\n siloHourlySnapshots(\n first: $first\n orderBy: season\n orderDirection: desc\n where: {silo_: {id: $id}}\n ) {\n beanToMaxLpGpPerBdvRatio\n deltaBeanMints\n season\n }\n}": typeof types.SiloSnapshotsDocument, "query SiloYields {\n siloYields(\n orderBy: season\n orderDirection: desc\n where: {emaWindow: ROLLING_30_DAY}\n first: 1\n ) {\n beansPerSeasonEMA\n beta\n createdAt\n season\n id\n u\n whitelistedTokens\n emaWindow\n tokenAPYS {\n beanAPY\n stalkAPY\n season\n createdAt\n token\n }\n }\n}": typeof types.SiloYieldsDocument, - "query AllMarketActivity($first: Int = 1000, $listings_createdAt_gt: BigInt, $orders_createdAt_gt: BigInt, $fill_createdAt_gt: BigInt) {\n podListings(\n first: $first\n where: {createdAt_gt: $listings_createdAt_gt, status_not: FILLED_PARTIAL}\n ) {\n ...PodListing\n }\n podOrders(\n first: $first\n orderBy: createdAt\n orderDirection: desc\n where: {createdAt_gt: $orders_createdAt_gt}\n ) {\n ...PodOrder\n }\n podFills(first: $first, where: {createdAt_gt: $fill_createdAt_gt}) {\n ...PodFill\n }\n}": typeof types.AllMarketActivityDocument, - "query AllPodListings($first: Int = 1000, $status: MarketStatus = ACTIVE, $maxHarvestableIndex: BigInt!, $skip: Int = 0) {\n podListings(\n first: $first\n skip: $skip\n where: {status: $status, maxHarvestableIndex_gt: $maxHarvestableIndex, remainingAmount_gt: \"100000\"}\n orderBy: index\n orderDirection: asc\n ) {\n ...PodListing\n }\n}": typeof types.AllPodListingsDocument, - "query AllPodOrders($first: Int = 1000, $status: MarketStatus = ACTIVE, $skip: Int = 0) {\n podOrders(\n first: $first\n skip: $skip\n orderBy: createdAt\n orderDirection: desc\n where: {status: $status}\n ) {\n ...PodOrder\n }\n}": typeof types.AllPodOrdersDocument, + "query AllMarketActivity($first: Int = 1000, $listings_createdAt_gt: BigInt, $orders_createdAt_gt: BigInt, $fill_createdAt_gt: BigInt, $listings_podMarketplace: String, $orders_podMarketplace: String) {\n podListings(\n first: $first\n where: {createdAt_gt: $listings_createdAt_gt, status_not: FILLED_PARTIAL, podMarketplace: $listings_podMarketplace}\n ) {\n ...PodListing\n }\n podOrders(\n first: $first\n orderBy: createdAt\n orderDirection: desc\n where: {createdAt_gt: $orders_createdAt_gt, podMarketplace: $orders_podMarketplace}\n ) {\n ...PodOrder\n }\n podFills(first: $first, where: {createdAt_gt: $fill_createdAt_gt}) {\n ...PodFill\n }\n}": typeof types.AllMarketActivityDocument, + "query AllPodListings($first: Int = 1000, $status: MarketStatus = ACTIVE, $maxHarvestableIndex: BigInt!, $skip: Int = 0, $podMarketplace: String) {\n podListings(\n first: $first\n skip: $skip\n where: {status: $status, maxHarvestableIndex_gt: $maxHarvestableIndex, remainingAmount_gt: \"100000\", podMarketplace: $podMarketplace}\n orderBy: index\n orderDirection: asc\n ) {\n ...PodListing\n }\n}": typeof types.AllPodListingsDocument, + "query AllPodOrders($first: Int = 1000, $status: MarketStatus = ACTIVE, $skip: Int = 0, $podMarketplace: String) {\n podOrders(\n first: $first\n skip: $skip\n orderBy: createdAt\n orderDirection: desc\n where: {status: $status, podMarketplace: $podMarketplace}\n ) {\n ...PodOrder\n }\n}": typeof types.AllPodOrdersDocument, "query FarmerMarketActivity($first: Int = 1000, $account: String!, $listings_createdAt_gt: BigInt, $orders_createdAt_gt: BigInt, $fill_createdAt_gt: BigInt) {\n podListings(\n first: $first\n where: {farmer: $account, createdAt_gt: $listings_createdAt_gt, status_not: FILLED_PARTIAL}\n ) {\n ...PodListing\n }\n podOrders(\n first: $first\n orderBy: createdAt\n orderDirection: desc\n where: {farmer: $account, createdAt_gt: $orders_createdAt_gt}\n ) {\n ...PodOrder\n }\n podFills(\n first: $first\n where: {and: [{createdAt_gt: $fill_createdAt_gt}, {or: [{fromFarmer: $account}, {toFarmer: $account}]}]}\n ) {\n ...PodFill\n }\n}": typeof types.FarmerMarketActivityDocument, "fragment PodFill on PodFill {\n id\n placeInLine\n amount\n index\n start\n costInBeans\n fromFarmer {\n id\n }\n toFarmer {\n id\n }\n listing {\n id\n originalAmount\n }\n order {\n id\n beanAmount\n }\n createdAt\n}": typeof types.PodFillFragmentDoc, "fragment PodListing on PodListing {\n id\n farmer {\n id\n }\n historyID\n index\n start\n mode\n pricingType\n pricePerPod\n pricingFunction\n maxHarvestableIndex\n minFillAmount\n originalIndex\n originalPlaceInLine\n originalAmount\n filled\n amount\n remainingAmount\n filledAmount\n fill {\n placeInLine\n }\n status\n createdAt\n updatedAt\n creationHash\n}": typeof types.PodListingFragmentDoc, @@ -50,9 +50,9 @@ const documents: Documents = { "query BeanstalkSeasonsTable($from: Int, $to: Int) {\n seasons(\n first: 1000\n orderBy: season\n orderDirection: desc\n where: {season_gte: $from, season_lte: $to}\n ) {\n id\n sunriseBlock\n rewardBeans\n price\n deltaBeans\n raining\n season\n }\n fieldHourlySnapshots(\n first: 1000\n orderBy: season\n orderDirection: desc\n where: {field: \"0xd1a0d188e861ed9d15773a2f3574a2e94134ba8f\", season_gte: $from, season_lte: $to}\n ) {\n id\n caseId\n issuedSoil\n deltaSownBeans\n sownBeans\n deltaPodDemand\n blocksToSoldOutSoil\n podRate\n temperature\n deltaTemperature\n season\n }\n siloHourlySnapshots(\n first: 1000\n orderBy: season\n orderDirection: desc\n where: {silo: \"0xd1a0d188e861ed9d15773a2f3574a2e94134ba8f\", season_gte: $from, season_lte: $to}\n ) {\n id\n beanToMaxLpGpPerBdvRatio\n deltaBeanToMaxLpGpPerBdvRatio\n season\n }\n}": types.BeanstalkSeasonsTableDocument, "query SiloSnapshots($first: Int!, $id: Bytes!) {\n siloHourlySnapshots(\n first: $first\n orderBy: season\n orderDirection: desc\n where: {silo_: {id: $id}}\n ) {\n beanToMaxLpGpPerBdvRatio\n deltaBeanMints\n season\n }\n}": types.SiloSnapshotsDocument, "query SiloYields {\n siloYields(\n orderBy: season\n orderDirection: desc\n where: {emaWindow: ROLLING_30_DAY}\n first: 1\n ) {\n beansPerSeasonEMA\n beta\n createdAt\n season\n id\n u\n whitelistedTokens\n emaWindow\n tokenAPYS {\n beanAPY\n stalkAPY\n season\n createdAt\n token\n }\n }\n}": types.SiloYieldsDocument, - "query AllMarketActivity($first: Int = 1000, $listings_createdAt_gt: BigInt, $orders_createdAt_gt: BigInt, $fill_createdAt_gt: BigInt) {\n podListings(\n first: $first\n where: {createdAt_gt: $listings_createdAt_gt, status_not: FILLED_PARTIAL}\n ) {\n ...PodListing\n }\n podOrders(\n first: $first\n orderBy: createdAt\n orderDirection: desc\n where: {createdAt_gt: $orders_createdAt_gt}\n ) {\n ...PodOrder\n }\n podFills(first: $first, where: {createdAt_gt: $fill_createdAt_gt}) {\n ...PodFill\n }\n}": types.AllMarketActivityDocument, - "query AllPodListings($first: Int = 1000, $status: MarketStatus = ACTIVE, $maxHarvestableIndex: BigInt!, $skip: Int = 0) {\n podListings(\n first: $first\n skip: $skip\n where: {status: $status, maxHarvestableIndex_gt: $maxHarvestableIndex, remainingAmount_gt: \"100000\"}\n orderBy: index\n orderDirection: asc\n ) {\n ...PodListing\n }\n}": types.AllPodListingsDocument, - "query AllPodOrders($first: Int = 1000, $status: MarketStatus = ACTIVE, $skip: Int = 0) {\n podOrders(\n first: $first\n skip: $skip\n orderBy: createdAt\n orderDirection: desc\n where: {status: $status}\n ) {\n ...PodOrder\n }\n}": types.AllPodOrdersDocument, + "query AllMarketActivity($first: Int = 1000, $listings_createdAt_gt: BigInt, $orders_createdAt_gt: BigInt, $fill_createdAt_gt: BigInt, $listings_podMarketplace: String, $orders_podMarketplace: String) {\n podListings(\n first: $first\n where: {createdAt_gt: $listings_createdAt_gt, status_not: FILLED_PARTIAL, podMarketplace: $listings_podMarketplace}\n ) {\n ...PodListing\n }\n podOrders(\n first: $first\n orderBy: createdAt\n orderDirection: desc\n where: {createdAt_gt: $orders_createdAt_gt, podMarketplace: $orders_podMarketplace}\n ) {\n ...PodOrder\n }\n podFills(first: $first, where: {createdAt_gt: $fill_createdAt_gt}) {\n ...PodFill\n }\n}": types.AllMarketActivityDocument, + "query AllPodListings($first: Int = 1000, $status: MarketStatus = ACTIVE, $maxHarvestableIndex: BigInt!, $skip: Int = 0, $podMarketplace: String) {\n podListings(\n first: $first\n skip: $skip\n where: {status: $status, maxHarvestableIndex_gt: $maxHarvestableIndex, remainingAmount_gt: \"100000\", podMarketplace: $podMarketplace}\n orderBy: index\n orderDirection: asc\n ) {\n ...PodListing\n }\n}": types.AllPodListingsDocument, + "query AllPodOrders($first: Int = 1000, $status: MarketStatus = ACTIVE, $skip: Int = 0, $podMarketplace: String) {\n podOrders(\n first: $first\n skip: $skip\n orderBy: createdAt\n orderDirection: desc\n where: {status: $status, podMarketplace: $podMarketplace}\n ) {\n ...PodOrder\n }\n}": types.AllPodOrdersDocument, "query FarmerMarketActivity($first: Int = 1000, $account: String!, $listings_createdAt_gt: BigInt, $orders_createdAt_gt: BigInt, $fill_createdAt_gt: BigInt) {\n podListings(\n first: $first\n where: {farmer: $account, createdAt_gt: $listings_createdAt_gt, status_not: FILLED_PARTIAL}\n ) {\n ...PodListing\n }\n podOrders(\n first: $first\n orderBy: createdAt\n orderDirection: desc\n where: {farmer: $account, createdAt_gt: $orders_createdAt_gt}\n ) {\n ...PodOrder\n }\n podFills(\n first: $first\n where: {and: [{createdAt_gt: $fill_createdAt_gt}, {or: [{fromFarmer: $account}, {toFarmer: $account}]}]}\n ) {\n ...PodFill\n }\n}": types.FarmerMarketActivityDocument, "fragment PodFill on PodFill {\n id\n placeInLine\n amount\n index\n start\n costInBeans\n fromFarmer {\n id\n }\n toFarmer {\n id\n }\n listing {\n id\n originalAmount\n }\n order {\n id\n beanAmount\n }\n createdAt\n}": types.PodFillFragmentDoc, "fragment PodListing on PodListing {\n id\n farmer {\n id\n }\n historyID\n index\n start\n mode\n pricingType\n pricePerPod\n pricingFunction\n maxHarvestableIndex\n minFillAmount\n originalIndex\n originalPlaceInLine\n originalAmount\n filled\n amount\n remainingAmount\n filledAmount\n fill {\n placeInLine\n }\n status\n createdAt\n updatedAt\n creationHash\n}": types.PodListingFragmentDoc, @@ -119,15 +119,15 @@ export function graphql(source: "query SiloYields {\n siloYields(\n orderBy: /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query AllMarketActivity($first: Int = 1000, $listings_createdAt_gt: BigInt, $orders_createdAt_gt: BigInt, $fill_createdAt_gt: BigInt) {\n podListings(\n first: $first\n where: {createdAt_gt: $listings_createdAt_gt, status_not: FILLED_PARTIAL}\n ) {\n ...PodListing\n }\n podOrders(\n first: $first\n orderBy: createdAt\n orderDirection: desc\n where: {createdAt_gt: $orders_createdAt_gt}\n ) {\n ...PodOrder\n }\n podFills(first: $first, where: {createdAt_gt: $fill_createdAt_gt}) {\n ...PodFill\n }\n}"): (typeof documents)["query AllMarketActivity($first: Int = 1000, $listings_createdAt_gt: BigInt, $orders_createdAt_gt: BigInt, $fill_createdAt_gt: BigInt) {\n podListings(\n first: $first\n where: {createdAt_gt: $listings_createdAt_gt, status_not: FILLED_PARTIAL}\n ) {\n ...PodListing\n }\n podOrders(\n first: $first\n orderBy: createdAt\n orderDirection: desc\n where: {createdAt_gt: $orders_createdAt_gt}\n ) {\n ...PodOrder\n }\n podFills(first: $first, where: {createdAt_gt: $fill_createdAt_gt}) {\n ...PodFill\n }\n}"]; +export function graphql(source: "query AllMarketActivity($first: Int = 1000, $listings_createdAt_gt: BigInt, $orders_createdAt_gt: BigInt, $fill_createdAt_gt: BigInt, $listings_podMarketplace: String, $orders_podMarketplace: String) {\n podListings(\n first: $first\n where: {createdAt_gt: $listings_createdAt_gt, status_not: FILLED_PARTIAL, podMarketplace: $listings_podMarketplace}\n ) {\n ...PodListing\n }\n podOrders(\n first: $first\n orderBy: createdAt\n orderDirection: desc\n where: {createdAt_gt: $orders_createdAt_gt, podMarketplace: $orders_podMarketplace}\n ) {\n ...PodOrder\n }\n podFills(first: $first, where: {createdAt_gt: $fill_createdAt_gt}) {\n ...PodFill\n }\n}"): (typeof documents)["query AllMarketActivity($first: Int = 1000, $listings_createdAt_gt: BigInt, $orders_createdAt_gt: BigInt, $fill_createdAt_gt: BigInt, $listings_podMarketplace: String, $orders_podMarketplace: String) {\n podListings(\n first: $first\n where: {createdAt_gt: $listings_createdAt_gt, status_not: FILLED_PARTIAL, podMarketplace: $listings_podMarketplace}\n ) {\n ...PodListing\n }\n podOrders(\n first: $first\n orderBy: createdAt\n orderDirection: desc\n where: {createdAt_gt: $orders_createdAt_gt, podMarketplace: $orders_podMarketplace}\n ) {\n ...PodOrder\n }\n podFills(first: $first, where: {createdAt_gt: $fill_createdAt_gt}) {\n ...PodFill\n }\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query AllPodListings($first: Int = 1000, $status: MarketStatus = ACTIVE, $maxHarvestableIndex: BigInt!, $skip: Int = 0) {\n podListings(\n first: $first\n skip: $skip\n where: {status: $status, maxHarvestableIndex_gt: $maxHarvestableIndex, remainingAmount_gt: \"100000\"}\n orderBy: index\n orderDirection: asc\n ) {\n ...PodListing\n }\n}"): (typeof documents)["query AllPodListings($first: Int = 1000, $status: MarketStatus = ACTIVE, $maxHarvestableIndex: BigInt!, $skip: Int = 0) {\n podListings(\n first: $first\n skip: $skip\n where: {status: $status, maxHarvestableIndex_gt: $maxHarvestableIndex, remainingAmount_gt: \"100000\"}\n orderBy: index\n orderDirection: asc\n ) {\n ...PodListing\n }\n}"]; +export function graphql(source: "query AllPodListings($first: Int = 1000, $status: MarketStatus = ACTIVE, $maxHarvestableIndex: BigInt!, $skip: Int = 0, $podMarketplace: String) {\n podListings(\n first: $first\n skip: $skip\n where: {status: $status, maxHarvestableIndex_gt: $maxHarvestableIndex, remainingAmount_gt: \"100000\", podMarketplace: $podMarketplace}\n orderBy: index\n orderDirection: asc\n ) {\n ...PodListing\n }\n}"): (typeof documents)["query AllPodListings($first: Int = 1000, $status: MarketStatus = ACTIVE, $maxHarvestableIndex: BigInt!, $skip: Int = 0, $podMarketplace: String) {\n podListings(\n first: $first\n skip: $skip\n where: {status: $status, maxHarvestableIndex_gt: $maxHarvestableIndex, remainingAmount_gt: \"100000\", podMarketplace: $podMarketplace}\n orderBy: index\n orderDirection: asc\n ) {\n ...PodListing\n }\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query AllPodOrders($first: Int = 1000, $status: MarketStatus = ACTIVE, $skip: Int = 0) {\n podOrders(\n first: $first\n skip: $skip\n orderBy: createdAt\n orderDirection: desc\n where: {status: $status}\n ) {\n ...PodOrder\n }\n}"): (typeof documents)["query AllPodOrders($first: Int = 1000, $status: MarketStatus = ACTIVE, $skip: Int = 0) {\n podOrders(\n first: $first\n skip: $skip\n orderBy: createdAt\n orderDirection: desc\n where: {status: $status}\n ) {\n ...PodOrder\n }\n}"]; +export function graphql(source: "query AllPodOrders($first: Int = 1000, $status: MarketStatus = ACTIVE, $skip: Int = 0, $podMarketplace: String) {\n podOrders(\n first: $first\n skip: $skip\n orderBy: createdAt\n orderDirection: desc\n where: {status: $status, podMarketplace: $podMarketplace}\n ) {\n ...PodOrder\n }\n}"): (typeof documents)["query AllPodOrders($first: Int = 1000, $status: MarketStatus = ACTIVE, $skip: Int = 0, $podMarketplace: String) {\n podOrders(\n first: $first\n skip: $skip\n orderBy: createdAt\n orderDirection: desc\n where: {status: $status, podMarketplace: $podMarketplace}\n ) {\n ...PodOrder\n }\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/src/generated/gql/pintostalk/graphql.ts b/src/generated/gql/pintostalk/graphql.ts index 78aa7d75a..1ff4fd753 100644 --- a/src/generated/gql/pintostalk/graphql.ts +++ b/src/generated/gql/pintostalk/graphql.ts @@ -3413,6 +3413,8 @@ export type Plot = { __typename?: 'Plot'; /** Number of beans spent for each pod, whether through sowing or on the marketplace */ beansPerPod: Scalars['BigInt']['output']; + /** Block number of the most recent plot combination, null if never combined */ + combinedAtBlock?: Maybe; /** Timestamp of entity creation (not sown) */ createdAt: Scalars['BigInt']['output']; /** Transaction hash of when this plot entity was created (not sown) */ @@ -3496,6 +3498,14 @@ export type PlotFilter = { beansPerPod_lte?: InputMaybe; beansPerPod_not?: InputMaybe; beansPerPod_not_in?: InputMaybe>; + combinedAtBlock?: InputMaybe; + combinedAtBlock_gt?: InputMaybe; + combinedAtBlock_gte?: InputMaybe; + combinedAtBlock_in?: InputMaybe>; + combinedAtBlock_lt?: InputMaybe; + combinedAtBlock_lte?: InputMaybe; + combinedAtBlock_not?: InputMaybe; + combinedAtBlock_not_in?: InputMaybe>; createdAt?: InputMaybe; createdAt_gt?: InputMaybe; createdAt_gte?: InputMaybe; @@ -3811,6 +3821,7 @@ export type PlotFilter = { export enum PlotOrderBy { beansPerPod = 'beansPerPod', + combinedAtBlock = 'combinedAtBlock', createdAt = 'createdAt', creationHash = 'creationHash', farmer = 'farmer', @@ -5140,6 +5151,7 @@ export enum PodListingOrderBy { originalPlaceInLine = 'originalPlaceInLine', plot = 'plot', plot__beansPerPod = 'plot__beansPerPod', + plot__combinedAtBlock = 'plot__combinedAtBlock', plot__createdAt = 'plot__createdAt', plot__creationHash = 'plot__creationHash', plot__fieldId = 'plot__fieldId', @@ -14095,6 +14107,8 @@ export type AllMarketActivityQueryVariables = Exact<{ listings_createdAt_gt?: InputMaybe; orders_createdAt_gt?: InputMaybe; fill_createdAt_gt?: InputMaybe; + listings_podMarketplace?: InputMaybe; + orders_podMarketplace?: InputMaybe; }>; @@ -14105,6 +14119,7 @@ export type AllPodListingsQueryVariables = Exact<{ status?: InputMaybe; maxHarvestableIndex: Scalars['BigInt']['input']; skip?: InputMaybe; + podMarketplace?: InputMaybe; }>; @@ -14114,6 +14129,7 @@ export type AllPodOrdersQueryVariables = Exact<{ first?: InputMaybe; status?: InputMaybe; skip?: InputMaybe; + podMarketplace?: InputMaybe; }>; @@ -14239,9 +14255,9 @@ export const FieldSnapshotsDocument = {"kind":"Document","definitions":[{"kind": export const BeanstalkSeasonsTableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BeanstalkSeasonsTable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"from"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"to"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"seasons"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1000"}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"EnumValue","value":"season"}},{"kind":"Argument","name":{"kind":"Name","value":"orderDirection"},"value":{"kind":"EnumValue","value":"desc"}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"season_gte"},"value":{"kind":"Variable","name":{"kind":"Name","value":"from"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"season_lte"},"value":{"kind":"Variable","name":{"kind":"Name","value":"to"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"sunriseBlock"}},{"kind":"Field","name":{"kind":"Name","value":"rewardBeans"}},{"kind":"Field","name":{"kind":"Name","value":"price"}},{"kind":"Field","name":{"kind":"Name","value":"deltaBeans"}},{"kind":"Field","name":{"kind":"Name","value":"raining"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}},{"kind":"Field","name":{"kind":"Name","value":"fieldHourlySnapshots"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1000"}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"EnumValue","value":"season"}},{"kind":"Argument","name":{"kind":"Name","value":"orderDirection"},"value":{"kind":"EnumValue","value":"desc"}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"field"},"value":{"kind":"StringValue","value":"0xd1a0d188e861ed9d15773a2f3574a2e94134ba8f","block":false}},{"kind":"ObjectField","name":{"kind":"Name","value":"season_gte"},"value":{"kind":"Variable","name":{"kind":"Name","value":"from"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"season_lte"},"value":{"kind":"Variable","name":{"kind":"Name","value":"to"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"caseId"}},{"kind":"Field","name":{"kind":"Name","value":"issuedSoil"}},{"kind":"Field","name":{"kind":"Name","value":"deltaSownBeans"}},{"kind":"Field","name":{"kind":"Name","value":"sownBeans"}},{"kind":"Field","name":{"kind":"Name","value":"deltaPodDemand"}},{"kind":"Field","name":{"kind":"Name","value":"blocksToSoldOutSoil"}},{"kind":"Field","name":{"kind":"Name","value":"podRate"}},{"kind":"Field","name":{"kind":"Name","value":"temperature"}},{"kind":"Field","name":{"kind":"Name","value":"deltaTemperature"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}},{"kind":"Field","name":{"kind":"Name","value":"siloHourlySnapshots"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1000"}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"EnumValue","value":"season"}},{"kind":"Argument","name":{"kind":"Name","value":"orderDirection"},"value":{"kind":"EnumValue","value":"desc"}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"silo"},"value":{"kind":"StringValue","value":"0xd1a0d188e861ed9d15773a2f3574a2e94134ba8f","block":false}},{"kind":"ObjectField","name":{"kind":"Name","value":"season_gte"},"value":{"kind":"Variable","name":{"kind":"Name","value":"from"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"season_lte"},"value":{"kind":"Variable","name":{"kind":"Name","value":"to"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"beanToMaxLpGpPerBdvRatio"}},{"kind":"Field","name":{"kind":"Name","value":"deltaBeanToMaxLpGpPerBdvRatio"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}}]}}]} as unknown as DocumentNode; export const SiloSnapshotsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SiloSnapshots"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Bytes"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"siloHourlySnapshots"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"EnumValue","value":"season"}},{"kind":"Argument","name":{"kind":"Name","value":"orderDirection"},"value":{"kind":"EnumValue","value":"desc"}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"silo_"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"beanToMaxLpGpPerBdvRatio"}},{"kind":"Field","name":{"kind":"Name","value":"deltaBeanMints"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}}]}}]} as unknown as DocumentNode; export const SiloYieldsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SiloYields"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"siloYields"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"EnumValue","value":"season"}},{"kind":"Argument","name":{"kind":"Name","value":"orderDirection"},"value":{"kind":"EnumValue","value":"desc"}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"emaWindow"},"value":{"kind":"EnumValue","value":"ROLLING_30_DAY"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"beansPerSeasonEMA"}},{"kind":"Field","name":{"kind":"Name","value":"beta"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"season"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"u"}},{"kind":"Field","name":{"kind":"Name","value":"whitelistedTokens"}},{"kind":"Field","name":{"kind":"Name","value":"emaWindow"}},{"kind":"Field","name":{"kind":"Name","value":"tokenAPYS"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"beanAPY"}},{"kind":"Field","name":{"kind":"Name","value":"stalkAPY"}},{"kind":"Field","name":{"kind":"Name","value":"season"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"token"}}]}}]}}]}}]} as unknown as DocumentNode; -export const AllMarketActivityDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AllMarketActivity"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"1000"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"listings_createdAt_gt"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"BigInt"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orders_createdAt_gt"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"BigInt"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"fill_createdAt_gt"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"BigInt"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"podListings"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"createdAt_gt"},"value":{"kind":"Variable","name":{"kind":"Name","value":"listings_createdAt_gt"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"status_not"},"value":{"kind":"EnumValue","value":"FILLED_PARTIAL"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PodListing"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podOrders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"EnumValue","value":"createdAt"}},{"kind":"Argument","name":{"kind":"Name","value":"orderDirection"},"value":{"kind":"EnumValue","value":"desc"}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"createdAt_gt"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orders_createdAt_gt"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PodOrder"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podFills"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"createdAt_gt"},"value":{"kind":"Variable","name":{"kind":"Name","value":"fill_createdAt_gt"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PodFill"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PodListing"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PodListing"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"farmer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"historyID"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"start"}},{"kind":"Field","name":{"kind":"Name","value":"mode"}},{"kind":"Field","name":{"kind":"Name","value":"pricingType"}},{"kind":"Field","name":{"kind":"Name","value":"pricePerPod"}},{"kind":"Field","name":{"kind":"Name","value":"pricingFunction"}},{"kind":"Field","name":{"kind":"Name","value":"maxHarvestableIndex"}},{"kind":"Field","name":{"kind":"Name","value":"minFillAmount"}},{"kind":"Field","name":{"kind":"Name","value":"originalIndex"}},{"kind":"Field","name":{"kind":"Name","value":"originalPlaceInLine"}},{"kind":"Field","name":{"kind":"Name","value":"originalAmount"}},{"kind":"Field","name":{"kind":"Name","value":"filled"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"remainingAmount"}},{"kind":"Field","name":{"kind":"Name","value":"filledAmount"}},{"kind":"Field","name":{"kind":"Name","value":"fill"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"placeInLine"}}]}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"creationHash"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PodOrder"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PodOrder"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"farmer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"historyID"}},{"kind":"Field","name":{"kind":"Name","value":"pricingType"}},{"kind":"Field","name":{"kind":"Name","value":"pricePerPod"}},{"kind":"Field","name":{"kind":"Name","value":"pricingFunction"}},{"kind":"Field","name":{"kind":"Name","value":"maxPlaceInLine"}},{"kind":"Field","name":{"kind":"Name","value":"minFillAmount"}},{"kind":"Field","name":{"kind":"Name","value":"beanAmount"}},{"kind":"Field","name":{"kind":"Name","value":"podAmountFilled"}},{"kind":"Field","name":{"kind":"Name","value":"beanAmountFilled"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"creationHash"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PodFill"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PodFill"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"placeInLine"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"start"}},{"kind":"Field","name":{"kind":"Name","value":"costInBeans"}},{"kind":"Field","name":{"kind":"Name","value":"fromFarmer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"toFarmer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"listing"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"originalAmount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"beanAmount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode; -export const AllPodListingsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AllPodListings"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"1000"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"status"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"MarketStatus"}},"defaultValue":{"kind":"EnumValue","value":"ACTIVE"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"maxHarvestableIndex"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"BigInt"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"skip"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"0"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"podListings"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"skip"},"value":{"kind":"Variable","name":{"kind":"Name","value":"skip"}}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"status"},"value":{"kind":"Variable","name":{"kind":"Name","value":"status"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"maxHarvestableIndex_gt"},"value":{"kind":"Variable","name":{"kind":"Name","value":"maxHarvestableIndex"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"remainingAmount_gt"},"value":{"kind":"StringValue","value":"100000","block":false}}]}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"EnumValue","value":"index"}},{"kind":"Argument","name":{"kind":"Name","value":"orderDirection"},"value":{"kind":"EnumValue","value":"asc"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PodListing"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PodListing"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PodListing"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"farmer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"historyID"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"start"}},{"kind":"Field","name":{"kind":"Name","value":"mode"}},{"kind":"Field","name":{"kind":"Name","value":"pricingType"}},{"kind":"Field","name":{"kind":"Name","value":"pricePerPod"}},{"kind":"Field","name":{"kind":"Name","value":"pricingFunction"}},{"kind":"Field","name":{"kind":"Name","value":"maxHarvestableIndex"}},{"kind":"Field","name":{"kind":"Name","value":"minFillAmount"}},{"kind":"Field","name":{"kind":"Name","value":"originalIndex"}},{"kind":"Field","name":{"kind":"Name","value":"originalPlaceInLine"}},{"kind":"Field","name":{"kind":"Name","value":"originalAmount"}},{"kind":"Field","name":{"kind":"Name","value":"filled"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"remainingAmount"}},{"kind":"Field","name":{"kind":"Name","value":"filledAmount"}},{"kind":"Field","name":{"kind":"Name","value":"fill"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"placeInLine"}}]}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"creationHash"}}]}}]} as unknown as DocumentNode; -export const AllPodOrdersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AllPodOrders"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"1000"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"status"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"MarketStatus"}},"defaultValue":{"kind":"EnumValue","value":"ACTIVE"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"skip"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"0"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"podOrders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"skip"},"value":{"kind":"Variable","name":{"kind":"Name","value":"skip"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"EnumValue","value":"createdAt"}},{"kind":"Argument","name":{"kind":"Name","value":"orderDirection"},"value":{"kind":"EnumValue","value":"desc"}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"status"},"value":{"kind":"Variable","name":{"kind":"Name","value":"status"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PodOrder"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PodOrder"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PodOrder"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"farmer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"historyID"}},{"kind":"Field","name":{"kind":"Name","value":"pricingType"}},{"kind":"Field","name":{"kind":"Name","value":"pricePerPod"}},{"kind":"Field","name":{"kind":"Name","value":"pricingFunction"}},{"kind":"Field","name":{"kind":"Name","value":"maxPlaceInLine"}},{"kind":"Field","name":{"kind":"Name","value":"minFillAmount"}},{"kind":"Field","name":{"kind":"Name","value":"beanAmount"}},{"kind":"Field","name":{"kind":"Name","value":"podAmountFilled"}},{"kind":"Field","name":{"kind":"Name","value":"beanAmountFilled"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"creationHash"}}]}}]} as unknown as DocumentNode; +export const AllMarketActivityDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AllMarketActivity"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"1000"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"listings_createdAt_gt"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"BigInt"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orders_createdAt_gt"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"BigInt"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"fill_createdAt_gt"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"BigInt"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"listings_podMarketplace"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orders_podMarketplace"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"podListings"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"createdAt_gt"},"value":{"kind":"Variable","name":{"kind":"Name","value":"listings_createdAt_gt"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"status_not"},"value":{"kind":"EnumValue","value":"FILLED_PARTIAL"}},{"kind":"ObjectField","name":{"kind":"Name","value":"podMarketplace"},"value":{"kind":"Variable","name":{"kind":"Name","value":"listings_podMarketplace"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PodListing"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podOrders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"EnumValue","value":"createdAt"}},{"kind":"Argument","name":{"kind":"Name","value":"orderDirection"},"value":{"kind":"EnumValue","value":"desc"}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"createdAt_gt"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orders_createdAt_gt"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"podMarketplace"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orders_podMarketplace"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PodOrder"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podFills"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"createdAt_gt"},"value":{"kind":"Variable","name":{"kind":"Name","value":"fill_createdAt_gt"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PodFill"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PodListing"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PodListing"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"farmer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"historyID"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"start"}},{"kind":"Field","name":{"kind":"Name","value":"mode"}},{"kind":"Field","name":{"kind":"Name","value":"pricingType"}},{"kind":"Field","name":{"kind":"Name","value":"pricePerPod"}},{"kind":"Field","name":{"kind":"Name","value":"pricingFunction"}},{"kind":"Field","name":{"kind":"Name","value":"maxHarvestableIndex"}},{"kind":"Field","name":{"kind":"Name","value":"minFillAmount"}},{"kind":"Field","name":{"kind":"Name","value":"originalIndex"}},{"kind":"Field","name":{"kind":"Name","value":"originalPlaceInLine"}},{"kind":"Field","name":{"kind":"Name","value":"originalAmount"}},{"kind":"Field","name":{"kind":"Name","value":"filled"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"remainingAmount"}},{"kind":"Field","name":{"kind":"Name","value":"filledAmount"}},{"kind":"Field","name":{"kind":"Name","value":"fill"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"placeInLine"}}]}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"creationHash"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PodOrder"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PodOrder"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"farmer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"historyID"}},{"kind":"Field","name":{"kind":"Name","value":"pricingType"}},{"kind":"Field","name":{"kind":"Name","value":"pricePerPod"}},{"kind":"Field","name":{"kind":"Name","value":"pricingFunction"}},{"kind":"Field","name":{"kind":"Name","value":"maxPlaceInLine"}},{"kind":"Field","name":{"kind":"Name","value":"minFillAmount"}},{"kind":"Field","name":{"kind":"Name","value":"beanAmount"}},{"kind":"Field","name":{"kind":"Name","value":"podAmountFilled"}},{"kind":"Field","name":{"kind":"Name","value":"beanAmountFilled"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"creationHash"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PodFill"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PodFill"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"placeInLine"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"start"}},{"kind":"Field","name":{"kind":"Name","value":"costInBeans"}},{"kind":"Field","name":{"kind":"Name","value":"fromFarmer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"toFarmer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"listing"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"originalAmount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"beanAmount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode; +export const AllPodListingsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AllPodListings"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"1000"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"status"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"MarketStatus"}},"defaultValue":{"kind":"EnumValue","value":"ACTIVE"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"maxHarvestableIndex"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"BigInt"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"skip"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"0"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"podMarketplace"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"podListings"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"skip"},"value":{"kind":"Variable","name":{"kind":"Name","value":"skip"}}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"status"},"value":{"kind":"Variable","name":{"kind":"Name","value":"status"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"maxHarvestableIndex_gt"},"value":{"kind":"Variable","name":{"kind":"Name","value":"maxHarvestableIndex"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"remainingAmount_gt"},"value":{"kind":"StringValue","value":"100000","block":false}},{"kind":"ObjectField","name":{"kind":"Name","value":"podMarketplace"},"value":{"kind":"Variable","name":{"kind":"Name","value":"podMarketplace"}}}]}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"EnumValue","value":"index"}},{"kind":"Argument","name":{"kind":"Name","value":"orderDirection"},"value":{"kind":"EnumValue","value":"asc"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PodListing"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PodListing"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PodListing"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"farmer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"historyID"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"start"}},{"kind":"Field","name":{"kind":"Name","value":"mode"}},{"kind":"Field","name":{"kind":"Name","value":"pricingType"}},{"kind":"Field","name":{"kind":"Name","value":"pricePerPod"}},{"kind":"Field","name":{"kind":"Name","value":"pricingFunction"}},{"kind":"Field","name":{"kind":"Name","value":"maxHarvestableIndex"}},{"kind":"Field","name":{"kind":"Name","value":"minFillAmount"}},{"kind":"Field","name":{"kind":"Name","value":"originalIndex"}},{"kind":"Field","name":{"kind":"Name","value":"originalPlaceInLine"}},{"kind":"Field","name":{"kind":"Name","value":"originalAmount"}},{"kind":"Field","name":{"kind":"Name","value":"filled"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"remainingAmount"}},{"kind":"Field","name":{"kind":"Name","value":"filledAmount"}},{"kind":"Field","name":{"kind":"Name","value":"fill"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"placeInLine"}}]}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"creationHash"}}]}}]} as unknown as DocumentNode; +export const AllPodOrdersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AllPodOrders"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"1000"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"status"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"MarketStatus"}},"defaultValue":{"kind":"EnumValue","value":"ACTIVE"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"skip"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"0"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"podMarketplace"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"podOrders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"skip"},"value":{"kind":"Variable","name":{"kind":"Name","value":"skip"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"EnumValue","value":"createdAt"}},{"kind":"Argument","name":{"kind":"Name","value":"orderDirection"},"value":{"kind":"EnumValue","value":"desc"}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"status"},"value":{"kind":"Variable","name":{"kind":"Name","value":"status"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"podMarketplace"},"value":{"kind":"Variable","name":{"kind":"Name","value":"podMarketplace"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PodOrder"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PodOrder"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PodOrder"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"farmer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"historyID"}},{"kind":"Field","name":{"kind":"Name","value":"pricingType"}},{"kind":"Field","name":{"kind":"Name","value":"pricePerPod"}},{"kind":"Field","name":{"kind":"Name","value":"pricingFunction"}},{"kind":"Field","name":{"kind":"Name","value":"maxPlaceInLine"}},{"kind":"Field","name":{"kind":"Name","value":"minFillAmount"}},{"kind":"Field","name":{"kind":"Name","value":"beanAmount"}},{"kind":"Field","name":{"kind":"Name","value":"podAmountFilled"}},{"kind":"Field","name":{"kind":"Name","value":"beanAmountFilled"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"creationHash"}}]}}]} as unknown as DocumentNode; export const FarmerMarketActivityDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FarmerMarketActivity"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"1000"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"account"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"listings_createdAt_gt"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"BigInt"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orders_createdAt_gt"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"BigInt"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"fill_createdAt_gt"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"BigInt"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"podListings"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"farmer"},"value":{"kind":"Variable","name":{"kind":"Name","value":"account"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"createdAt_gt"},"value":{"kind":"Variable","name":{"kind":"Name","value":"listings_createdAt_gt"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"status_not"},"value":{"kind":"EnumValue","value":"FILLED_PARTIAL"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PodListing"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podOrders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"EnumValue","value":"createdAt"}},{"kind":"Argument","name":{"kind":"Name","value":"orderDirection"},"value":{"kind":"EnumValue","value":"desc"}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"farmer"},"value":{"kind":"Variable","name":{"kind":"Name","value":"account"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"createdAt_gt"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orders_createdAt_gt"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PodOrder"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podFills"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"and"},"value":{"kind":"ListValue","values":[{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"createdAt_gt"},"value":{"kind":"Variable","name":{"kind":"Name","value":"fill_createdAt_gt"}}}]},{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"or"},"value":{"kind":"ListValue","values":[{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"fromFarmer"},"value":{"kind":"Variable","name":{"kind":"Name","value":"account"}}}]},{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"toFarmer"},"value":{"kind":"Variable","name":{"kind":"Name","value":"account"}}}]}]}}]}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PodFill"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PodListing"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PodListing"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"farmer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"historyID"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"start"}},{"kind":"Field","name":{"kind":"Name","value":"mode"}},{"kind":"Field","name":{"kind":"Name","value":"pricingType"}},{"kind":"Field","name":{"kind":"Name","value":"pricePerPod"}},{"kind":"Field","name":{"kind":"Name","value":"pricingFunction"}},{"kind":"Field","name":{"kind":"Name","value":"maxHarvestableIndex"}},{"kind":"Field","name":{"kind":"Name","value":"minFillAmount"}},{"kind":"Field","name":{"kind":"Name","value":"originalIndex"}},{"kind":"Field","name":{"kind":"Name","value":"originalPlaceInLine"}},{"kind":"Field","name":{"kind":"Name","value":"originalAmount"}},{"kind":"Field","name":{"kind":"Name","value":"filled"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"remainingAmount"}},{"kind":"Field","name":{"kind":"Name","value":"filledAmount"}},{"kind":"Field","name":{"kind":"Name","value":"fill"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"placeInLine"}}]}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"creationHash"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PodOrder"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PodOrder"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"farmer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"historyID"}},{"kind":"Field","name":{"kind":"Name","value":"pricingType"}},{"kind":"Field","name":{"kind":"Name","value":"pricePerPod"}},{"kind":"Field","name":{"kind":"Name","value":"pricingFunction"}},{"kind":"Field","name":{"kind":"Name","value":"maxPlaceInLine"}},{"kind":"Field","name":{"kind":"Name","value":"minFillAmount"}},{"kind":"Field","name":{"kind":"Name","value":"beanAmount"}},{"kind":"Field","name":{"kind":"Name","value":"podAmountFilled"}},{"kind":"Field","name":{"kind":"Name","value":"beanAmountFilled"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"creationHash"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PodFill"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PodFill"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"placeInLine"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"start"}},{"kind":"Field","name":{"kind":"Name","value":"costInBeans"}},{"kind":"Field","name":{"kind":"Name","value":"fromFarmer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"toFarmer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"listing"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"originalAmount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"beanAmount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode; export const FarmerReferralDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FarmerReferral"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"farmer"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"totalReferralRewardPodsReceived"}},{"kind":"Field","name":{"kind":"Name","value":"refereeCount"}}]}}]}}]} as unknown as DocumentNode; export const ReferralLeaderboardDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ReferralLeaderboard"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"skip"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"block"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Block_height"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"farmers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"skip"},"value":{"kind":"Variable","name":{"kind":"Name","value":"skip"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"EnumValue","value":"totalReferralRewardPodsReceived"}},{"kind":"Argument","name":{"kind":"Name","value":"orderDirection"},"value":{"kind":"EnumValue","value":"desc"}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"totalReferralRewardPodsReceived_gt"},"value":{"kind":"StringValue","value":"0","block":false}}]}},{"kind":"Argument","name":{"kind":"Name","value":"block"},"value":{"kind":"Variable","name":{"kind":"Name","value":"block"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"refereeCount"}},{"kind":"Field","name":{"kind":"Name","value":"totalReferralRewardPodsReceived"}}]}}]}}]} as unknown as DocumentNode; diff --git a/src/hooks/tractor/useAutomateClaimOrder.ts b/src/hooks/tractor/useAutomateClaimOrder.ts new file mode 100644 index 000000000..0c59b47ba --- /dev/null +++ b/src/hooks/tractor/useAutomateClaimOrder.ts @@ -0,0 +1,141 @@ +import { TV } from "@/classes/TokenValue"; +import { + TractorOperatorTipStrategy, + getTractorOperatorTipAmountFromPreset, +} from "@/components/Tractor/form/fields/sharedFields"; +import { type AutomateClaimFormValues } from "@/components/Tractor/form/schema/automateClaim.schema"; +import { useProtocolAddress } from "@/hooks/pinto/useProtocolAddress"; +import { Blueprint, createBlueprintFromBlock } from "@/lib/Tractor"; +import { createAutomateClaimTractorData } from "@/lib/Tractor/claimOrder"; +import { useFarmerSilo } from "@/state/useFarmerSilo"; +import { useCallback, useState } from "react"; +import { useForm } from "react-hook-form"; +import { useAccount, usePublicClient } from "wagmi"; + +// ──────────────────────────────────────────────────────────────────────────────── +// TYPES +// ──────────────────────────────────────────────────────────────────────────────── + +export type AutomateClaimOrderState = { + blueprint: Blueprint; + encodedData: `0x${string}`; + operatorPasteInstructions: `0x${string}`[]; + depositOptimizationCalls: `0x${string}`[]; +}; + +export type AutomateClaimOrderData = { + mowEnabled: boolean; + plantEnabled: boolean; + harvestEnabled: boolean; + operatorTip: string; +}; + +// ──────────────────────────────────────────────────────────────────────────────── +// HOOK +// ──────────────────────────────────────────────────────────────────────────────── + +export const useAutomateClaimOrder = () => { + const client = usePublicClient(); + const { address } = useAccount(); + const protocolAddress = useProtocolAddress(); + + const [state, setState] = useState(undefined); + const [orderData, setOrderData] = useState(undefined); + const [isLoading, setIsLoading] = useState(false); + + const handleCreateBlueprint = useCallback( + async ( + form: ReturnType>, + averageTipPaid: number, + operatorTipPreset: TractorOperatorTipStrategy, + deposits?: ReturnType["deposits"], + options?: { + onFailure?: () => void; + onSuccess?: () => void; + }, + ) => { + if (!client) { + throw new Error("No public client available."); + } + if (!address) { + throw new Error("Signer not found."); + } + + setIsLoading(true); + + try { + const formData = form.getValues(); + + // Resolve operator tip amount from preset + const tipTV = getTractorOperatorTipAmountFromPreset( + operatorTipPreset, + averageTipPaid, + formData.customOperatorTip, + 6, // PINTO decimals + ); + + if (!tipTV) { + throw new Error("Unable to resolve operator tip amount."); + } + + const tipPerExecution = tipTV.toBigInt(); + + // Create tractor data and fetch latest block in parallel + const [tractorData, block] = await Promise.all([ + createAutomateClaimTractorData({ + formValues: formData, + tipPerExecution, + whitelistedOperators: [], + publicClient: client, + farmerDeposits: deposits, + userAddress: address, + protocolAddress, + }).catch((e) => { + console.error("[useAutomateClaimOrder] Error creating tractor data:", e); + throw e; + }), + client.getBlock({ blockTag: "latest" }), + ]); + + // Create the blueprint + const blueprint = createBlueprintFromBlock({ + block, + publisher: address, + data: tractorData.data, + operatorPasteInstrs: tractorData.operatorPasteInstrs, + maxNonce: TV.MAX_UINT256.toBigInt(), + }); + + // Set order data for display in ReviewTractorOrderDialog + setOrderData({ + mowEnabled: formData.mowEnabled, + plantEnabled: formData.plantEnabled, + harvestEnabled: formData.harvestEnabled, + operatorTip: tipTV.toHuman(), + }); + + // Set state for the blueprint and encoded data + setState({ + blueprint, + encodedData: tractorData.data, + operatorPasteInstructions: tractorData.operatorPasteInstrs, + depositOptimizationCalls: tractorData.depositOptimizationCalls || [], + }); + + options?.onSuccess?.(); + } catch (_) { + options?.onFailure?.(); + } finally { + setIsLoading(false); + } + }, + [client, address, protocolAddress], + ); + + return { + state, + orderData, + isLoading, + handleCreateBlueprint, + } as const; +}; diff --git a/src/hooks/useAllFertilizerIds.ts b/src/hooks/useAllFertilizerIds.ts new file mode 100644 index 000000000..5be3443ed --- /dev/null +++ b/src/hooks/useAllFertilizerIds.ts @@ -0,0 +1,133 @@ +import { abiSnippets } from "@/constants/abiSnippets"; +import { BARN_PAYBACK_ADDRESS } from "@/constants/address"; +import { useQuery } from "@tanstack/react-query"; +import { useCallback, useMemo } from "react"; +import { PublicClient, parseAbiItem } from "viem"; +import { usePublicClient, useReadContract } from "wagmi"; + +/** + * Hook that fetches all active fertilizer IDs from the BarnPayback contract. + * + * Strategy: + * 1. Call fert() → get fertFirst, fertLast, activeFertilizer + * 2. If activeFertilizer == 0 && fertFirst == 0 → no fertilizers exist, bail out + * 3. Collect unique IDs from TransferSingle/TransferBatch events on the BarnPayback contract + */ + +const DEPLOYMENT_BLOCK = 42040733n; +const BARN_PAYBACK = BARN_PAYBACK_ADDRESS as `0x${string}`; + +const transferSingleEvent = parseAbiItem( + "event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value)", +); + +const transferBatchEvent = parseAbiItem( + "event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values)", +); + +export interface FertData { + fertilizedIndex: bigint; + unfertilizedIndex: bigint; + fertilizedPaidIndex: bigint; + leftoverBeans: bigint; + activeFertilizer: bigint; + fertFirst: bigint; + fertLast: bigint; + bpf: bigint; +} + +/** Fetch unique fertilizer IDs from event logs */ +async function fetchFertilizerIds(publicClient: PublicClient): Promise { + const uniqueIds = new Set(); + + // Scan TransferSingle events + const singleLogs = await publicClient.getLogs({ + address: BARN_PAYBACK, + event: transferSingleEvent, + fromBlock: DEPLOYMENT_BLOCK, + toBlock: "latest", + }); + + for (const log of singleLogs) { + if (log.args.id !== undefined) { + uniqueIds.add(log.args.id); + } + } + + // Scan TransferBatch events + const batchLogs = await publicClient.getLogs({ + address: BARN_PAYBACK, + event: transferBatchEvent, + fromBlock: DEPLOYMENT_BLOCK, + toBlock: "latest", + }); + + for (const log of batchLogs) { + if (log.args.ids) { + for (const id of log.args.ids) { + uniqueIds.add(id); + } + } + } + + return Array.from(uniqueIds).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); +} + +export function useAllFertilizerIds() { + const publicClient = usePublicClient(); + + // Step 1: fert() call — get contract state + const fertQuery = useReadContract({ + address: BARN_PAYBACK, + abi: abiSnippets.barnPayback, + functionName: "fert", + query: { + staleTime: 5 * 60 * 1000, + }, + }); + + // Parse fert() results + const fertData = useMemo((): FertData | null => { + if (!fertQuery.data) return null; + const result = fertQuery.data as readonly [bigint, bigint, bigint, bigint, bigint, bigint, bigint, bigint]; + return { + fertilizedIndex: result[0], + unfertilizedIndex: result[1], + fertilizedPaidIndex: result[2], + leftoverBeans: result[3], + activeFertilizer: result[4], + fertFirst: result[5], + fertLast: result[6], + bpf: result[7], + }; + }, [fertQuery.data]); + + const hasActiveFert = fertData ? !(fertData.activeFertilizer === 0n && fertData.fertFirst === 0n) : false; + + // Step 2: Scan event logs — cached via React Query (staleTime: 1 hour) + const idsQuery = useQuery({ + queryKey: ["beanstalk", "fertilizerIds", "eventScan"], + queryFn: () => fetchFertilizerIds(publicClient as PublicClient), + enabled: !!publicClient && !!fertData && hasActiveFert, + staleTime: 60 * 60 * 1000, // 1 hour — these IDs rarely change + gcTime: 60 * 60 * 1000, // keep in cache 1 hour after last use + refetchOnMount: false, + refetchOnWindowFocus: false, + }); + + const refetch = useCallback(async () => { + await fertQuery.refetch(); + await idsQuery.refetch(); + }, [fertQuery, idsQuery]); + + // If no active fertilizer, return empty without error + const fertIds = !hasActiveFert ? [] : idsQuery.data ?? []; + + return { + fertilizerIds: fertIds, + fertData, + isLoading: fertQuery.isLoading || (hasActiveFert && idsQuery.isLoading), + isError: fertQuery.isError || (hasActiveFert && idsQuery.isError), + refetch, + }; +} diff --git a/src/lib/Tractor/blueprint-decoders/automate-claim-decoder.ts b/src/lib/Tractor/blueprint-decoders/automate-claim-decoder.ts new file mode 100644 index 000000000..31f2a0707 --- /dev/null +++ b/src/lib/Tractor/blueprint-decoders/automate-claim-decoder.ts @@ -0,0 +1,30 @@ +import { automateClaimBlueprintABI } from "@/constants/abi/AutomateClaimBlueprintABI"; +import { AUTOMATE_CLAIM_BLUEPRINT_SELECTOR } from "@/constants/address"; +import { decodeFunctionData } from "viem"; +import type { BlueprintDecoder, DecodedBlueprintResult } from "./index"; + +export const automateClaimBlueprintDecoder: BlueprintDecoder = { + selector: AUTOMATE_CLAIM_BLUEPRINT_SELECTOR, + abi: automateClaimBlueprintABI, + functionName: "automateClaimBlueprint", + decode: (callData: string): DecodedBlueprintResult | null => { + try { + const decoded = decodeFunctionData({ + abi: automateClaimBlueprintABI, + data: callData as `0x${string}`, + }); + + if (decoded.functionName === "automateClaimBlueprint" && decoded.args[0]) { + return { + type: "automateClaim", + functionName: "automateClaimBlueprint", + params: decoded.args[0], + }; + } + } catch (error) { + console.error("Error decoding automateClaimBlueprint:", error); + } + + return null; + }, +}; diff --git a/src/lib/Tractor/blueprint-decoders/index.ts b/src/lib/Tractor/blueprint-decoders/index.ts index 01faf3198..f230a02f1 100644 --- a/src/lib/Tractor/blueprint-decoders/index.ts +++ b/src/lib/Tractor/blueprint-decoders/index.ts @@ -1,9 +1,11 @@ import { + AUTOMATE_CLAIM_BLUEPRINT_SELECTOR, CONVERT_UP_BLUEPRINT_V0_SELECTOR, SOW_BLUEPRINT_REFERRAL_V0_SELECTOR, SOW_BLUEPRINT_V0_SELECTOR, } from "@/constants/address"; import { extractTractorBlueprintCall } from "../requisitions/tractor-requisition"; +import { automateClaimBlueprintDecoder } from "./automate-claim-decoder"; import { convertUpBlueprintDecoder } from "./convert-up-decoder"; import { genericBlueprintDecoder } from "./generic-decoder"; import { sowBlueprintDecoder } from "./sow-decoder"; @@ -17,7 +19,7 @@ export interface BlueprintDecoder { } export interface DecodedBlueprintResult { - type: "sow" | "convertUp" | "generic"; + type: "sow" | "convertUp" | "automateClaim" | "generic"; functionName: string; params: any; } @@ -26,6 +28,7 @@ export const BLUEPRINT_REGISTRY: Record = { [SOW_BLUEPRINT_V0_SELECTOR]: sowBlueprintDecoder, [SOW_BLUEPRINT_REFERRAL_V0_SELECTOR]: sowBlueprintReferralDecoder, [CONVERT_UP_BLUEPRINT_V0_SELECTOR]: convertUpBlueprintDecoder, + [AUTOMATE_CLAIM_BLUEPRINT_SELECTOR]: automateClaimBlueprintDecoder, } as const; export function getBlueprintDecoder(selector: string): BlueprintDecoder | null { @@ -71,4 +74,4 @@ export function decodeBlueprintCallData(callData: string): DecodedBlueprintResul return genericResult; } -export type BlueprintType = "sow" | "convertUp" | "auto"; +export type BlueprintType = "sow" | "convertUp" | "automateClaim" | "auto"; diff --git a/src/lib/Tractor/claimOrder/automate-claim-helpers.ts b/src/lib/Tractor/claimOrder/automate-claim-helpers.ts new file mode 100644 index 000000000..ba8239597 --- /dev/null +++ b/src/lib/Tractor/claimOrder/automate-claim-helpers.ts @@ -0,0 +1,70 @@ +import { maxUint256 } from "viem"; +import { CLAIM_PRESETS } from "./tractor-claim"; +import type { AutomateClaimBlueprintData } from "./tractor-claim-types"; + +/** + * Determine which claim operations are enabled from decoded blueprint data. + * Centralizes the MAX_UINT256 comparison logic used across multiple components. + */ +export function getEnabledClaimOps(transformed: AutomateClaimBlueprintData) { + const mowEnabled = transformed.claimParams.minMowAmount !== maxUint256; + const plantEnabled = transformed.claimParams.minPlantAmount !== maxUint256; + const harvestEnabled = transformed.claimParams.fieldHarvestConfigs.length > 0; + + return { mowEnabled, plantEnabled, harvestEnabled }; +} + +/** + * Get a list of enabled operation labels (e.g. ["Mow", "Plant"]). + */ +export function getEnabledClaimOpLabels(ops: { + mowEnabled: boolean; + plantEnabled: boolean; + harvestEnabled: boolean; +}): string[] { + return [ops.mowEnabled && "Mow", ops.plantEnabled && "Plant", ops.harvestEnabled && "Harvest"].filter( + Boolean, + ) as string[]; +} + +/** + * Get a descriptive condition label for a claim operation based on its threshold. + * Matches the threshold against known presets, or shows the custom value. + */ +export function getClaimOpConditionLabel( + op: "mow" | "plant" | "harvest", + enabled: boolean, + thresholdBigInt: bigint, +): string { + if (!enabled) return ""; + + const presets = CLAIM_PRESETS[op]; + const unit = op === "mow" ? "Stalk" : "PINTO"; + const opLabel = op === "mow" ? "Mow" : op === "plant" ? "Plant" : "Harvest"; + + if (thresholdBigInt === presets.high.scaled) { + return `${opLabel}: Aggressive (≥ ${presets.high.value} ${unit})`; + } + if (thresholdBigInt === presets.medium.scaled) { + return `${opLabel}: Moderate (≥ ${presets.medium.value} ${unit})`; + } + if (thresholdBigInt === presets.low.scaled) { + return `${opLabel}: Conservative (≥ ${presets.low.value} ${unit})`; + } + + // Custom value + const decimals = op === "mow" ? 10 : 6; + const humanValue = Number(thresholdBigInt) / 10 ** decimals; + return `${opLabel}: Custom (≥ ${humanValue} ${unit})`; +} + +/** + * Validate a custom numeric input value (used in ConditionSection and SpecifyConditionsDialog). + * Returns true if the value is a positive number. + */ +export function isValidCustomValue(v: string): boolean { + const normalized = v.trim().replace(",", "."); + if (!normalized) return false; + const num = Number(normalized); + return !Number.isNaN(num) && num > 0; +} diff --git a/src/lib/Tractor/claimOrder/index.ts b/src/lib/Tractor/claimOrder/index.ts new file mode 100644 index 000000000..d4a8b8f99 --- /dev/null +++ b/src/lib/Tractor/claimOrder/index.ts @@ -0,0 +1,3 @@ +export * from "./tractor-claim"; +export * from "./tractor-claim-types"; +export * from "./automate-claim-helpers"; diff --git a/src/lib/Tractor/claimOrder/tractor-claim-types.ts b/src/lib/Tractor/claimOrder/tractor-claim-types.ts new file mode 100644 index 000000000..01f0c0e00 --- /dev/null +++ b/src/lib/Tractor/claimOrder/tractor-claim-types.ts @@ -0,0 +1,119 @@ +// ──────────────────────────────────────────────────────────────────────────────── +// AutomateClaim Blueprint — TypeScript Type Definitions +// ──────────────────────────────────────────────────────────────────────────────── + +/** + * Frequency preset for claim operation thresholds. + * - "high": frequent execution (low thresholds) + * - "medium": moderate execution frequency + * - "low": infrequent execution (high thresholds) + * - "custom": user-defined threshold value + */ +export type ClaimFrequencyPreset = "high" | "medium" | "low" | "custom"; + +// ──────────────────────────────────────────────────────────────────────────────── +// Contract Struct Types (match ABI exactly) +// ──────────────────────────────────────────────────────────────────────────────── + +/** + * Per-field harvest configuration. + * Maps to `AutomateClaimBlueprint.FieldHarvestConfig` in the contract. + */ +export interface FieldHarvestConfig { + fieldId: bigint; + minHarvestAmount: bigint; +} + +/** + * Claim parameters for the AutomateClaimBlueprint contract. + * Maps to `AutomateClaimBlueprint.AutomateClaimParams` in the contract. + * + * Disabled operations use MAX_UINT256 for their min amounts. + * Harvest is disabled by passing an empty `fieldHarvestConfigs` array. + */ +export interface AutomateClaimParams { + minMowAmount: bigint; + minTwaDeltaB: bigint; + minPlantAmount: bigint; + fieldHarvestConfigs: FieldHarvestConfig[]; + minRinseAmount: bigint; + minUnripeClaimAmount: bigint; + sourceTokenIndices: readonly number[]; + maxGrownStalkPerBdv: bigint; + slippageRatio: bigint; +} + +/** + * Extended operator parameters with per-operation tip amounts. + * Maps to `AutomateClaimBlueprint.OperatorParamsExtended` in the contract. + */ +export interface OperatorParamsExtended { + baseOpParams: { + whitelistedOperators: readonly `0x${string}`[]; + tipAddress: `0x${string}`; + operatorTipAmount: bigint; + }; + mowTipAmount: bigint; + plantTipAmount: bigint; + harvestTipAmount: bigint; + rinseTipAmount: bigint; + unripeClaimTipAmount: bigint; +} + +/** + * Top-level struct passed to the `automateClaimBlueprint` contract function. + * Maps to `AutomateClaimBlueprint.AutomateClaimBlueprintStruct` in the contract. + */ +export interface AutomateClaimBlueprintStruct { + claimParams: AutomateClaimParams; + opParams: OperatorParamsExtended; +} + +// ──────────────────────────────────────────────────────────────────────────────── +// Decoded Blueprint Data (for display / visualization) +// ──────────────────────────────────────────────────────────────────────────────── + +/** + * Decoded and display-ready representation of an AutomateClaim blueprint. + * Includes both raw bigint values and human-readable string representations. + */ +export interface AutomateClaimBlueprintData { + claimParams: { + minMowAmount: bigint; + minMowAmountAsString: string; + minTwaDeltaB: bigint; + minTwaDeltaBAsString: string; + minPlantAmount: bigint; + minPlantAmountAsString: string; + fieldHarvestConfigs: Array<{ + fieldId: bigint; + minHarvestAmount: bigint; + minHarvestAmountAsString: string; + }>; + minRinseAmount: bigint; + minRinseAmountAsString: string; + minUnripeClaimAmount: bigint; + minUnripeClaimAmountAsString: string; + sourceTokenIndices: readonly number[]; + maxGrownStalkPerBdv: bigint; + maxGrownStalkPerBdvAsString: string; + slippageRatio: bigint; + slippageRatioAsString: string; + }; + operatorParams: { + whitelistedOperators: readonly `0x${string}`[]; + tipAddress: `0x${string}`; + operatorTipAmount: bigint; + operatorTipAmountAsString: string; + mowTipAmount: bigint; + mowTipAmountAsString: string; + plantTipAmount: bigint; + plantTipAmountAsString: string; + harvestTipAmount: bigint; + harvestTipAmountAsString: string; + rinseTipAmount: bigint; + rinseTipAmountAsString: string; + unripeClaimTipAmount: bigint; + unripeClaimTipAmountAsString: string; + }; +} diff --git a/src/lib/Tractor/claimOrder/tractor-claim.ts b/src/lib/Tractor/claimOrder/tractor-claim.ts new file mode 100644 index 000000000..7a4d41b1e --- /dev/null +++ b/src/lib/Tractor/claimOrder/tractor-claim.ts @@ -0,0 +1,377 @@ +import { TokenValue } from "@/classes/TokenValue"; +import { automateClaimBlueprintABI } from "@/constants/abi/AutomateClaimBlueprintABI"; +import { AUTOMATE_CLAIM_BLUEPRINT_ADDRESS } from "@/constants/address"; +import { useFarmerSilo } from "@/state/useFarmerSilo"; +import { AdvancedPipeCall } from "@/utils/types"; +import { PublicClient, decodeFunctionData, encodeFunctionData, maxUint256 } from "viem"; +import { base } from "viem/chains"; +import { + CreateTractorDataReturnType, + decodeEncodedTractorDataToAdvancedPipeCalls, + encodeTractorAndOptimizeDeposits, +} from "../core"; +import { + AutomateClaimBlueprintData, + AutomateClaimBlueprintStruct, + ClaimFrequencyPreset, + FieldHarvestConfig, +} from "./tractor-claim-types"; + +// ──────────────────────────────────────────────────────────────────────────────── +// PRESET VALUES +// ──────────────────────────────────────────────────────────────────────────────── + +export const CLAIM_PRESETS = { + mow: { + high: { value: 50, scaled: 50n * 10_000_000_000n }, + medium: { value: 100, scaled: 100n * 10_000_000_000n }, + low: { value: 1000, scaled: 1000n * 10_000_000_000n }, + }, + plant: { + high: { value: 10, scaled: 10_000_000n }, + medium: { value: 50, scaled: 50_000_000n }, + low: { value: 500, scaled: 500_000_000n }, + }, + harvest: { + high: { value: 10, scaled: 10_000_000n }, + medium: { value: 50, scaled: 50_000_000n }, + low: { value: 500, scaled: 500_000_000n }, + }, +} as const; + +// Default minTwaDeltaB: 10 PINTO × 1e6 +const DEFAULT_MIN_TWA_DELTA_B = 10_000_000n; + +// ──────────────────────────────────────────────────────────────────────────────── +// HELPERS +// ──────────────────────────────────────────────────────────────────────────────── + +/** + * Resolve a preset or custom value to a scaled bigint for a given operation. + * - Mow: value × 1e10 (stalk decimals) + * - Plant/Harvest: value × 1e6 (PINTO decimals) + */ +export function resolvePresetValue( + operation: "mow" | "plant" | "harvest", + preset: ClaimFrequencyPreset | null, + customValue: string, +): bigint { + if (preset && preset !== "custom") { + return CLAIM_PRESETS[operation][preset].scaled; + } + + // Custom value — scale based on operation type + const numericValue = parseFloat(customValue); + if (Number.isNaN(numericValue) || numericValue <= 0) { + throw new Error(`Invalid custom value for ${operation}: ${customValue}`); + } + + if (operation === "mow") { + // Stalk: value × 1e10 + return BigInt(Math.round(numericValue * 1e10)); + } + // PINTO: value × 1e6 + return BigInt(Math.round(numericValue * 1e6)); +} + +/** + * Calculate per-operation tip amounts based on which operations are enabled. + * Disabled operations get 0 tip. Rinse and UnripeClaim are always 0. + */ +export function calculateTips( + formValues: { mowEnabled: boolean; plantEnabled: boolean; harvestEnabled: boolean }, + tipPerExecution: bigint, +) { + return { + mowTipAmount: formValues.mowEnabled ? tipPerExecution : 0n, + plantTipAmount: formValues.plantEnabled ? tipPerExecution : 0n, + harvestTipAmount: formValues.harvestEnabled ? tipPerExecution : 0n, + rinseTipAmount: 0n, + unripeClaimTipAmount: 0n, + }; +} + +/** + * Estimate the total tip range for a given tip amount and number of enabled operations. + */ +export function estimatedTotalTipRange(tipPerExecution: bigint, enabledOpsCount: number) { + return { + min: tipPerExecution * 1n, + max: tipPerExecution * BigInt(enabledOpsCount), + }; +} + +// ──────────────────────────────────────────────────────────────────────────────── +// STRUCT BUILDER +// ──────────────────────────────────────────────────────────────────────────────── + +export interface AutomateClaimFormValues { + mowEnabled: boolean; + mowPreset: ClaimFrequencyPreset | null; + mowCustomValue: string; + + plantEnabled: boolean; + plantPreset: ClaimFrequencyPreset | null; + plantCustomValue: string; + + harvestEnabled: boolean; + harvestPreset: ClaimFrequencyPreset | null; + harvestCustomValue: string; + + // Advanced defaults + sourceTokenIndices?: number[]; + maxGrownStalkPerBdv?: string; + slippageRatio?: string; +} + +/** + * Build the AutomateClaimBlueprintStruct from form values. + */ +export function buildAutomateClaimStruct( + formValues: AutomateClaimFormValues, + tipPerExecution: bigint, + whitelistedOperators: readonly `0x${string}`[], +): AutomateClaimBlueprintStruct { + // Mow + const minMowAmount = formValues.mowEnabled + ? resolvePresetValue("mow", formValues.mowPreset, formValues.mowCustomValue) + : maxUint256; + + // Plant + const minPlantAmount = formValues.plantEnabled + ? resolvePresetValue("plant", formValues.plantPreset, formValues.plantCustomValue) + : maxUint256; + + // Harvest + const fieldHarvestConfigs: FieldHarvestConfig[] = formValues.harvestEnabled + ? [ + { + fieldId: 0n, + minHarvestAmount: resolvePresetValue("harvest", formValues.harvestPreset, formValues.harvestCustomValue), + }, + { + fieldId: 1n, + minHarvestAmount: resolvePresetValue("harvest", formValues.harvestPreset, formValues.harvestCustomValue), + }, + ] + : []; + + // Tips + const tips = calculateTips(formValues, tipPerExecution); + + const sourceTokenIndices = formValues.sourceTokenIndices ?? [255]; + const maxGrownStalkPerBdv = formValues.maxGrownStalkPerBdv + ? BigInt(Math.round(parseFloat(formValues.maxGrownStalkPerBdv) * 1e6)) + : BigInt(1e18); + const slippageRatio = formValues.slippageRatio + ? BigInt(Math.round(parseFloat(formValues.slippageRatio) * 1e18)) + : BigInt(1e18); + + return { + claimParams: { + minMowAmount, + minTwaDeltaB: DEFAULT_MIN_TWA_DELTA_B, + minPlantAmount, + fieldHarvestConfigs, + minRinseAmount: maxUint256, + minUnripeClaimAmount: maxUint256, + sourceTokenIndices, + maxGrownStalkPerBdv, + slippageRatio, + }, + opParams: { + baseOpParams: { + whitelistedOperators, + tipAddress: "0x0000000000000000000000000000000000000000" as `0x${string}`, + operatorTipAmount: tipPerExecution, + }, + ...tips, + }, + }; +} + +// ──────────────────────────────────────────────────────────────────────────────── +// CREATE ORDER (ENCODING) +// ──────────────────────────────────────────────────────────────────────────────── + +/** + * Creates encoded tractor data from form values. + * Follows the advancedFarm(advancedPipe(automateClaimBlueprint(...))) wrapping pattern. + */ +export async function createAutomateClaimTractorData({ + formValues, + tipPerExecution, + whitelistedOperators, + publicClient, + farmerDeposits, + userAddress, + protocolAddress, +}: { + formValues: AutomateClaimFormValues; + tipPerExecution: bigint; + whitelistedOperators: readonly `0x${string}`[]; + publicClient: PublicClient; + farmerDeposits?: ReturnType["deposits"]; + userAddress: `0x${string}`; + protocolAddress: `0x${string}`; +}): Promise { + // 1. Build struct from form values + const struct = buildAutomateClaimStruct(formValues, tipPerExecution, whitelistedOperators); + + // 2. Encode the automateClaimBlueprint function call + // Convert sourceTokenIndices from number[] to bigint[] for ABI encoding (uint256[]) + const abiStruct = { + ...struct, + claimParams: { + ...struct.claimParams, + sourceTokenIndices: struct.claimParams.sourceTokenIndices.map((i) => BigInt(i)), + }, + }; + + const blueprintCall = encodeFunctionData({ + abi: automateClaimBlueprintABI, + functionName: "automateClaimBlueprint", + args: [abiStruct], + }); + + // 3. Create advancedPipe struct targeting the blueprint contract + const advPipeStruct: AdvancedPipeCall = { + target: AUTOMATE_CLAIM_BLUEPRINT_ADDRESS, + callData: blueprintCall, + clipboard: "0x0000" as `0x${string}`, + }; + + // 4. Wrap with encodeTractorAndOptimizeDeposits + const { data, depositOptimizationCalls } = await encodeTractorAndOptimizeDeposits( + { client: publicClient, protocolAddress, farmerAddress: userAddress }, + advPipeStruct, + farmerDeposits, + ); + + return { + data, + operatorPasteInstrs: [], + rawCall: blueprintCall, + depositOptimizationCalls, + }; +} + +// ──────────────────────────────────────────────────────────────────────────────── +// DECODE BLUEPRINT DATA +// ──────────────────────────────────────────────────────────────────────────────── + +/** + * Decode an automateClaimBlueprint call from advancedPipe calls. + * Extracts the inner function call and decodes using the ABI. + */ +export function decodeAutomateClaimBlueprintFromAdvancedPipe( + calls: readonly AdvancedPipeCall[] | undefined, +): AutomateClaimBlueprintStruct | null { + if (!calls?.length) return null; + + try { + const decoded = decodeFunctionData({ + abi: automateClaimBlueprintABI, + data: calls[0].callData, + }); + + const params = decoded.args?.[0]; + if (!params) return null; + + return params as unknown as AutomateClaimBlueprintStruct; + } catch (error) { + console.error("[Tractor/decodeAutomateClaimBlueprintFromAdvancedPipe] Failed to decode:", error); + return null; + } +} + +/** + * Decode encoded tractor data (advancedFarm → advancedPipe → automateClaimBlueprint). + * Entry point for decoding a full encoded blueprint. + */ +export function decodeAutomateClaimBlueprint(encodedData: `0x${string}`): AutomateClaimBlueprintStruct | null { + try { + const pipeCalls = decodeEncodedTractorDataToAdvancedPipeCalls(encodedData, "automateClaim"); + + if (pipeCalls?.length) { + return decodeAutomateClaimBlueprintFromAdvancedPipe(pipeCalls); + } + } catch (e) { + console.error("[Tractor/decodeAutomateClaimBlueprint] Failed to decode:", e); + } + + return null; +} + +// ──────────────────────────────────────────────────────────────────────────────── +// TRANSFORM (raw decoded → display-ready data) +// ──────────────────────────────────────────────────────────────────────────────── + +/** + * Transform raw decoded AutomateClaimBlueprintStruct into display-ready + * AutomateClaimBlueprintData with string representations. + */ +export function transformAutomateClaimRequisitionEvent( + params: unknown | null, + _chainId: number = base.id, +): AutomateClaimBlueprintData | null { + try { + if (!params || typeof params !== "object") { + return null; + } + + const struct = params as AutomateClaimBlueprintStruct; + + if (!struct.claimParams || !struct.opParams) { + return null; + } + + const cp = struct.claimParams; + const op = struct.opParams; + + return { + claimParams: { + minMowAmount: cp.minMowAmount, + minMowAmountAsString: TokenValue.fromBlockchain(cp.minMowAmount, 10).toHuman(), + minTwaDeltaB: cp.minTwaDeltaB, + minTwaDeltaBAsString: TokenValue.fromBlockchain(cp.minTwaDeltaB, 6).toHuman(), + minPlantAmount: cp.minPlantAmount, + minPlantAmountAsString: TokenValue.fromBlockchain(cp.minPlantAmount, 6).toHuman(), + fieldHarvestConfigs: cp.fieldHarvestConfigs.map((config) => ({ + fieldId: config.fieldId, + minHarvestAmount: config.minHarvestAmount, + minHarvestAmountAsString: TokenValue.fromBlockchain(config.minHarvestAmount, 6).toHuman(), + })), + minRinseAmount: cp.minRinseAmount, + minRinseAmountAsString: TokenValue.fromBlockchain(cp.minRinseAmount, 6).toHuman(), + minUnripeClaimAmount: cp.minUnripeClaimAmount, + minUnripeClaimAmountAsString: TokenValue.fromBlockchain(cp.minUnripeClaimAmount, 6).toHuman(), + sourceTokenIndices: cp.sourceTokenIndices, + maxGrownStalkPerBdv: cp.maxGrownStalkPerBdv, + maxGrownStalkPerBdvAsString: TokenValue.fromBlockchain(cp.maxGrownStalkPerBdv, 6).toHuman(), + slippageRatio: cp.slippageRatio, + slippageRatioAsString: TokenValue.fromBlockchain(cp.slippageRatio, 18).toHuman(), + }, + operatorParams: { + whitelistedOperators: op.baseOpParams.whitelistedOperators, + tipAddress: op.baseOpParams.tipAddress, + operatorTipAmount: op.baseOpParams.operatorTipAmount, + operatorTipAmountAsString: TokenValue.fromBlockchain(op.baseOpParams.operatorTipAmount, 6).toHuman(), + mowTipAmount: op.mowTipAmount, + mowTipAmountAsString: TokenValue.fromBlockchain(op.mowTipAmount, 6).toHuman(), + plantTipAmount: op.plantTipAmount, + plantTipAmountAsString: TokenValue.fromBlockchain(op.plantTipAmount, 6).toHuman(), + harvestTipAmount: op.harvestTipAmount, + harvestTipAmountAsString: TokenValue.fromBlockchain(op.harvestTipAmount, 6).toHuman(), + rinseTipAmount: op.rinseTipAmount, + rinseTipAmountAsString: TokenValue.fromBlockchain(op.rinseTipAmount, 6).toHuman(), + unripeClaimTipAmount: op.unripeClaimTipAmount, + unripeClaimTipAmountAsString: TokenValue.fromBlockchain(op.unripeClaimTipAmount, 6).toHuman(), + }, + }; + } catch (e) { + console.debug("[Tractor/transformAutomateClaimRequisitionEvent] Failed to transform:", e); + } + + return null; +} diff --git a/src/lib/Tractor/core/constants.ts b/src/lib/Tractor/core/constants.ts index f74cae17e..dd8b5a5af 100644 --- a/src/lib/Tractor/core/constants.ts +++ b/src/lib/Tractor/core/constants.ts @@ -7,6 +7,7 @@ import { LowStalkDepositsMode } from "./shared-tractor-types"; // Block number at which Tractor was deployed - use this as starting point for event queries export const TRACTOR_DEPLOYMENT_BLOCK = 28930876n; export const TRACTOR_DEPLOYMENT_BLOCK_CONVERT_UP = 37073327n; +export const TRACTOR_DEPLOYMENT_BLOCK_AUTOMATE_CLAIM = 41417660n; export const TRACTOR_TOKEN_STRATEGY_INDICIES = { LOWEST_PRICE: 254, @@ -16,6 +17,7 @@ export const TRACTOR_TOKEN_STRATEGY_INDICIES = { export const TRACTOR_DEPLOYMENT_BLOCKS_BY_TYPE = { sowBlueprintv0: TRACTOR_DEPLOYMENT_BLOCK, convertUpBlueprint: TRACTOR_DEPLOYMENT_BLOCK_CONVERT_UP, + automateClaimBlueprint: TRACTOR_DEPLOYMENT_BLOCK_AUTOMATE_CLAIM, } as const; export const LOW_STALK_DEPOSIT_MODES_TO_LABELS = { diff --git a/src/lib/Tractor/core/shared-tractor-types.ts b/src/lib/Tractor/core/shared-tractor-types.ts index 5e2c155ba..5362efc44 100644 --- a/src/lib/Tractor/core/shared-tractor-types.ts +++ b/src/lib/Tractor/core/shared-tractor-types.ts @@ -1,9 +1,9 @@ import { TV } from "@/classes/TokenValue"; import { Address } from "viem"; -export type RequisitionType = "sowBlueprintv0" | "convertUpBlueprint" | "unknown"; +export type RequisitionType = "sowBlueprintv0" | "convertUpBlueprint" | "automateClaimBlueprint" | "unknown"; -export type TractorBlueprintType = "sow" | "convertUp"; +export type TractorBlueprintType = "sow" | "convertUp" | "automateClaim"; export interface CreateTractorDataReturnType { data: `0x${string}`; diff --git a/src/lib/Tractor/index.ts b/src/lib/Tractor/index.ts index 5eb37cceb..0396ce32a 100644 --- a/src/lib/Tractor/index.ts +++ b/src/lib/Tractor/index.ts @@ -2,6 +2,7 @@ import TractorAPI from "./api"; export * from "./types"; export * from "./convertUp"; +export * from "./claimOrder"; export * from "./blueprint"; export * from "./sowOrder"; export * from "./utils"; diff --git a/src/lib/Tractor/requisitions/tractor-requisition.ts b/src/lib/Tractor/requisitions/tractor-requisition.ts index 283347b83..35f4f0adf 100644 --- a/src/lib/Tractor/requisitions/tractor-requisition.ts +++ b/src/lib/Tractor/requisitions/tractor-requisition.ts @@ -1,4 +1,5 @@ import { TV, TokenValue } from "@/classes/TokenValue"; +import { automateClaimBlueprintABI } from "@/constants/abi/AutomateClaimBlueprintABI"; import { sowBlueprintReferralV0ABI } from "@/constants/abi/SowBlueprintReferralV0ABI"; import { sowBlueprintv0ABI } from "@/constants/abi/SowBlueprintv0ABI"; import { convertUpBlueprintV0ABI } from "@/constants/abi/convertUpBlueprintV0ABI"; @@ -14,6 +15,8 @@ import { SignableMessage, decodeFunctionData } from "viem"; import { PublicClient } from "viem"; import { base } from "viem/chains"; import { decodeBlueprintCallData } from "../blueprint-decoders"; +import { transformAutomateClaimRequisitionEvent } from "../claimOrder/tractor-claim"; +import { AutomateClaimBlueprintData, AutomateClaimBlueprintStruct } from "../claimOrder/tractor-claim-types"; import { ConvertUpBlueprintStruct } from "../convertUp"; import { Requisition, @@ -153,7 +156,12 @@ type SelectRequisitionTypeArgs = { data: Awaited>; }; -const combinedABI = [...sowBlueprintv0ABI, ...sowBlueprintReferralV0ABI, ...convertUpBlueprintV0ABI] as const; +const combinedABI = [ + ...sowBlueprintv0ABI, + ...sowBlueprintReferralV0ABI, + ...convertUpBlueprintV0ABI, + ...automateClaimBlueprintABI, +] as const; type BaseDecodedTractorRequisition = { type: RequisitionType; @@ -169,7 +177,12 @@ type DecodedConvertUpRequisition = BaseDecodedTractorRequisition & { data: NonNullable>; }; -type DecodedTractorRequisition = DecodedSowRequisition | DecodedConvertUpRequisition; +type DecodedAutomateClaimRequisition = BaseDecodedTractorRequisition & { + type: "automateClaimBlueprint"; + data: NonNullable>; +}; + +type DecodedTractorRequisition = DecodedSowRequisition | DecodedConvertUpRequisition | DecodedAutomateClaimRequisition; const blueprintTransformerLookup = { sowBlueprintv0: { @@ -191,6 +204,10 @@ const blueprintTransformerLookup = { transformer: transformConvertUpRequisitionEvent, type: "convertUpBlueprint", }, + automateClaimBlueprint: { + transformer: transformAutomateClaimRequisitionEvent, + type: "automateClaimBlueprint", + }, } as const; export const decodeTractorBlueprint = ( @@ -279,9 +296,11 @@ export const getSelectRequisitionType = (requisitionsType: MayArray[]; convertUpBlueprint: TractorRequisitionEvent>[]; + automateClaimBlueprint: TractorRequisitionEvent[]; } = { sowBlueprintv0: [], convertUpBlueprint: [], + automateClaimBlueprint: [], }; for (const event of publishEvents) { @@ -330,6 +349,15 @@ export const getSelectRequisitionType = (requisitionsType: MayArray { + 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. A portion + of new Pinto mints go towards repaying these obligations across 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/Market.tsx b/src/pages/Market.tsx index 229afc2c6..ec2a9c295 100644 --- a/src/pages/Market.tsx +++ b/src/pages/Market.tsx @@ -10,9 +10,12 @@ import ReadMoreAccordion from "@/components/ReadMoreAccordion"; import ScatterChart, { PointClickPayload, PointHoverPayload, ScatterChartRef } from "@/components/charts/ScatterChart"; import { Card } from "@/components/ui/Card"; import { Separator } from "@/components/ui/Separator"; +import { Switch } from "@/components/ui/Switch"; import { ANALYTICS_EVENTS } from "@/constants/analytics-events"; +import { BeanstalkMarketContext } from "@/context/BeanstalkMarketContext"; import useNavHeight from "@/hooks/display/useNavHeight"; import { useAllMarket } from "@/state/market/useAllMarket"; +import { useFarmerBeanstalkRepayment } from "@/state/useFarmerBeanstalkRepayment"; import { useHarvestableIndex, usePodLine } from "@/state/useFieldData"; import { trackSimpleEvent } from "@/utils/analytics"; import { calculatePodScore } from "@/utils/podScore"; @@ -21,7 +24,7 @@ import { exists } from "@/utils/utils"; import { ActiveElement, ChartEvent, PointStyle, TooltipOptions } from "chart.js"; import { Chart } from "chart.js"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { useNavigate, useParams } from "react-router-dom"; +import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import { AllActivityTable } from "./market/AllActivityTable"; import { FarmerActivityTable } from "./market/FarmerActivityTable"; import MarketModeSelect from "./market/MarketModeSelect"; @@ -213,12 +216,50 @@ export function Market() { const hoverInfoRef = useRef(null); const lastPositionSideRef = useRef<{ isRight: boolean; isAbove: boolean } | null>(null); const navigate = useNavigate(); - const { data, isLoaded } = useAllMarket(); + const [searchParams, setSearchParams] = useSearchParams(); + const isBeanstalkMarketplace = searchParams.get("beanstalk") === "true"; + const fieldId = isBeanstalkMarketplace ? 1n : 0n; + const podMarketplaceId = isBeanstalkMarketplace ? "1" : undefined; + + const handleToggleBeanstalk = useCallback( + (checked: boolean) => { + setSearchParams( + (prev) => { + const next = new URLSearchParams(prev); + if (checked) { + next.set("beanstalk", "true"); + } else { + next.delete("beanstalk"); + } + return next; + }, + { replace: true }, + ); + }, + [setSearchParams], + ); + const buildMarketPath = useCallback( + (path: string) => { + if (isBeanstalkMarketplace) { + return `${path}?beanstalk=true`; + } + return path; + }, + [isBeanstalkMarketplace], + ); + + const { data, isLoaded } = useAllMarket(podMarketplaceId); const podLine = usePodLine(); - const podLineAsNumber = podLine.toNumber() / MILLION; + const harvestableIndex = useHarvestableIndex(); + const repayment = useFarmerBeanstalkRepayment(); + + // Toggle durumuna göre pod line ve harvestable index seçimi + const activePodLine = isBeanstalkMarketplace ? repayment.pods.podIndex.sub(repayment.pods.harvestableIndex) : podLine; + const activeHarvestableIndex = isBeanstalkMarketplace ? repayment.pods.harvestableIndex : harvestableIndex; + + const podLineAsNumber = activePodLine.toNumber() / MILLION; // Chart rounds X max to nearest 10 (not ceil), so we need to match that for validation const chartXMax = Math.round((podLineAsNumber / 10) * 10); - const harvestableIndex = useHarvestableIndex(); const navHeight = useNavHeight(); const [mounted, setMounted] = useState(false); @@ -257,7 +298,7 @@ export function Market() { if (!selectedPlotData) return []; return selectedPlotData.listingData.map((data) => { - const placeInLine = data.index.sub(harvestableIndex).toNumber(); + const placeInLine = data.index.sub(activeHarvestableIndex).toNumber(); const placeInLineMillions = placeInLine / MILLION; const podScore = calculatePodScore(selectedPlotData.pricePerPod, placeInLineMillions); @@ -273,11 +314,11 @@ export function Market() { }; }); }, - [harvestableIndex], + [activeHarvestableIndex], ); const scatterChartData: MarketScatterChartData[] = useMemo(() => { - const baseData = shapeScatterChartData(data || [], harvestableIndex); + const baseData = shapeScatterChartData(data || [], activeHarvestableIndex); // Add selected plots dataset if available if (!selectedPlotData || selectedPlotData.listingData.length === 0 || selectedPlotData.pricePerPod <= 0) { @@ -317,7 +358,7 @@ export function Market() { }; return [...baseData, selectedPlotsDataset]; - }, [data, harvestableIndex, selectedPlotData, transformSelectedPlotsToChartPoints]); + }, [data, activeHarvestableIndex, selectedPlotData, transformSelectedPlotsToChartPoints]); // Calculate highlighted event IDs (filtered by FillListing parameters) const highlightedEventIds = useMemo(() => { @@ -519,13 +560,13 @@ export function Market() { useEffect(() => { if (!mode) { // No mode specified (e.g. /market/pods), redirect to buy/fill - navigate("/market/pods/buy/fill", { replace: true }); + navigate(buildMarketPath("/market/pods/buy/fill"), { replace: true }); } else if (mode === "buy" && !id) { - navigate("/market/pods/buy/fill", { replace: true }); + navigate(buildMarketPath("/market/pods/buy/fill"), { replace: true }); } else if (mode === "sell" && !id) { - navigate("/market/pods/sell/create", { replace: true }); + navigate(buildMarketPath("/market/pods/sell/create"), { replace: true }); } - }, [id, mode, navigate]); + }, [id, mode, navigate, buildMarketPath]); // Clear preview plots and unfreeze chart when route changes away from create listing // Also unfreeze when switching between listing/selling pages @@ -599,14 +640,14 @@ export function Market() { if (selection === TABLE_SLUGS[1]) { setIsNavigating(true); - navigate(`/market/pods/buy/fill`); + navigate(buildMarketPath(`/market/pods/buy/fill`)); } else if (selection === TABLE_SLUGS[2]) { setIsNavigating(true); - navigate(`/market/pods/sell/fill`); + navigate(buildMarketPath(`/market/pods/sell/fill`)); } handleChangeTab(selection); }, - [navigate, tab], + [navigate, tab, buildMarketPath], ); const handleSecondaryTabClick = useCallback( @@ -779,7 +820,7 @@ export function Market() { setIsCrosshairFrozen(false); setContextMenu(null); setIsContextMenuClosing(false); - navigate(path, { state }); + navigate(buildMarketPath(path), { state }); // Reset flag after navigation setTimeout(() => { @@ -787,7 +828,7 @@ export function Market() { }, 100); }, 200); // Match fade-out animation duration }, - [navigate], + [navigate, buildMarketPath], ); const contextMenuOptions = useMemo(() => { @@ -860,7 +901,7 @@ export function Market() { // Switch to buy mode and fill action if (mode !== "buy" || id !== "fill") { setIsNavigating(true); - navigate("/market/pods/buy/fill"); + navigate(buildMarketPath("/market/pods/buy/fill")); } handleChangeTab(TABLE_SLUGS[1]); // Switch to listings tab } else { @@ -871,7 +912,7 @@ export function Market() { // Switch to sell mode and fill action if (mode !== "sell" || id !== "fill") { setIsNavigating(true); - navigate("/market/pods/sell/fill"); + navigate(buildMarketPath("/market/pods/sell/fill")); } handleChangeTab(TABLE_SLUGS[2]); // Switch to orders tab } @@ -905,7 +946,7 @@ export function Market() { // Switch to buy mode and fill action if (mode !== "buy" || id !== "fill") { setIsNavigating(true); - navigate("/market/pods/buy/fill"); + navigate(buildMarketPath("/market/pods/buy/fill")); } handleChangeTab(TABLE_SLUGS[1]); // Switch to listings tab } else { @@ -916,7 +957,7 @@ export function Market() { // Switch to sell mode and fill action if (mode !== "sell" || id !== "fill") { setIsNavigating(true); - navigate("/market/pods/sell/fill"); + navigate(buildMarketPath("/market/pods/sell/fill")); } handleChangeTab(TABLE_SLUGS[2]); // Switch to orders tab } @@ -967,166 +1008,200 @@ export function Market() { }); // Navigate to CreateListing with plot indices (not full Plot objects to avoid serialization issues) - navigate("/market/pods/sell/create", { + navigate(buildMarketPath("/market/pods/sell/create"), { state: { selectedPlotIndices: plotIndices }, }); }, - [navigate], + [navigate, buildMarketPath], ); // Default to buy/fill when no mode is selected const viewMode = mode || "buy"; const viewAction = id || (viewMode === "buy" ? "fill" : "create"); + const contextValue = useMemo( + () => ({ + isBeanstalkMarketplace, + fieldId, + podMarketplaceId, + }), + [isBeanstalkMarketplace, fieldId, podMarketplaceId], + ); + return ( - <> -
-

Your screen size is too small to access the Pod Market.

-

- If you're on Desktop, zoom out on your browser to access the Pod Market. -

-
-
-
- -
-
Market
-
- Buy and sell Pods on the open market. + + <> +
+

Your screen size is too small to access the Pod Market.

+

+ If you're on Desktop, zoom out on your browser to access the Pod Market. +

+
+
+
+ +
+
Market
+
+ Buy and sell Pods on the open market. +
-
- - The Pod Market is a decentralized marketplace where users can trade Pods, which are protocol-native debt - instruments that represent future Pinto tokens. When you buy Pods, you're essentially purchasing the right - to redeem them for Pinto tokens at a fixed rate when they become harvestable. The market operates on a - first-in-first-out (FIFO) basis, meaning the oldest Pods become harvestable first. You can place buy - orders to acquire Pods at a specific price, or create listings to sell your existing Pods to other users. - The scatter chart above visualizes all active orders and listings, showing their place in line and price - per Pod. This allows you to see market depth and make informed trading decisions based on current market - conditions and your investment strategy. - - - -
-
-
- {!isLoaded && ( -
- + + + The Pod Market is a decentralized marketplace where users can trade Pods, which are protocol-native + debt instruments that represent future Pinto tokens. When you buy Pods, you're essentially purchasing + the right to redeem them for Pinto tokens at a fixed rate when they become harvestable. The market + operates on a first-in-first-out (FIFO) basis, meaning the oldest Pods become harvestable first. You + can place buy orders to acquire Pods at a specific price, or create listings to sell your existing + Pods to other users. The scatter chart above visualizes all active orders and listings, showing their + place in line and price per Pod. This allows you to see market depth and make informed trading + decisions based on current market conditions and your investment strategy. + + + + +
+
+
+ Toggle Beanstalk Marketplace + +
+
+ {!isLoaded && ( +
+ +
+ )} + + + {/* Gradient Legend - positioned in top-right corner */} +
+
- )} - - - {/* Gradient Legend - positioned in top-right corner */} -
- +
+
+ +
+
+ {TABLE_SLUGS.map((s, idx) => ( +

+ {TABLE_LABELS[idx]} +

+ ))} +
+ +
+ {tab === TABLE_SLUGS[0] && } + {tab === TABLE_SLUGS[1] && } + {tab === TABLE_SLUGS[2] && } + {tab === TABLE_SLUGS[3] && }
-
- -
-
- {TABLE_SLUGS.map((s, idx) => ( -

- {TABLE_LABELS[idx]} -

- ))} -
- -
- {tab === TABLE_SLUGS[0] && } - {tab === TABLE_SLUGS[1] && } - {tab === TABLE_SLUGS[2] && } - {tab === TABLE_SLUGS[3] && } -
-
-
- -
- -
- {viewMode === "buy" && viewAction === "create" && } - {viewMode === "buy" && viewAction === "fill" && ( - - )} - {viewMode === "sell" && viewAction === "create" && ( - - )} - {viewMode === "sell" && viewAction === "fill" && ( - - )} +
+ +
+ +
+ {viewMode === "buy" && viewAction === "create" && ( + + )} + {viewMode === "buy" && viewAction === "fill" && ( + + )} + {viewMode === "sell" && viewAction === "create" && ( + + )} + {viewMode === "sell" && viewAction === "fill" && ( + + )} +
-
- + +
-
- - {/* Hover info - rendered once, updated via direct DOM manipulation for performance */} -
-
- Price per Pod: 0.000 -
-
- Place in line: 0.0M + + {/* Hover info - rendered once, updated via direct DOM manipulation for performance */} +
+
+ Price per Pod: 0.000 +
+
+ Place in line: 0.0M +
-
- - {contextMenu && ( - { - // Only unfreeze if NOT navigating (closed via Escape/click outside/scroll) - // If navigating, handleUnfreezeAndNavigate already handles unfreeze - if (!isNavigatingRef.current) { - // Trigger closing animation - setIsContextMenuClosing(true); - - // Wait for animation to complete before unfreezing - setTimeout(() => { - if (isCrosshairFrozen) { - chartRef.current?.unfreeze(); - setIsCrosshairFrozen(false); - } - setContextMenu(null); - setIsContextMenuClosing(false); - }, 200); // Match fade-out animation duration - } - }} - /> - )} - + + {contextMenu && ( + { + // Only unfreeze if NOT navigating (closed via Escape/click outside/scroll) + // If navigating, handleUnfreezeAndNavigate already handles unfreeze + if (!isNavigatingRef.current) { + // Trigger closing animation + setIsContextMenuClosing(true); + + // Wait for animation to complete before unfreezing + setTimeout(() => { + if (isCrosshairFrozen) { + chartRef.current?.unfreeze(); + setIsCrosshairFrozen(false); + } + setContextMenu(null); + setIsContextMenuClosing(false); + }, 200); // Match fade-out animation duration + } + }} + /> + )} + + ); } diff --git a/src/pages/Silo.tsx b/src/pages/Silo.tsx index dd649c3c7..0b4539b4a 100644 --- a/src/pages/Silo.tsx +++ b/src/pages/Silo.tsx @@ -3,6 +3,7 @@ import GerminationNotice from "@/components/GerminationNotice"; import HelperLink from "@/components/HelperLink"; import ReadMoreAccordion from "@/components/ReadMoreAccordion"; import StatPanel from "@/components/StatPanel"; +import { SpecifyConditionsDialog } from "@/components/Tractor/AutomateClaim"; import PageContainer from "@/components/ui/PageContainer"; import { Separator } from "@/components/ui/Separator"; import { PINTO_WETH_TOKEN, PINTO_WSOL_TOKEN } from "@/constants/tokens"; @@ -40,6 +41,7 @@ function Silo() { const pintoWSOLLP = useChainConstant(PINTO_WSOL_TOKEN); const [showConvertUpOrderDialog, setShowConvertUpOrderDialog] = useState(false); + const [showAutomateClaimDialog, setShowAutomateClaimDialog] = useState(false); const [hoveredButton, setHoveredButton] = useState("claim"); const enableStatPanels = farmerSilo.depositsUSD.gt(0) || farmerSilo.activeStalkBalance.gt(0) || farmerSilo.activeSeedsBalance.gt(0); @@ -152,8 +154,16 @@ function Silo() {
Deposit Whitelist
-
- These are Deposits which are currently incentivized by Pinto. +
+
+ These are Deposits which are currently incentivized by Pinto. +
+ setShowAutomateClaimDialog(true)} + > + Automate your Silo with Tractor 🚜 +
@@ -180,6 +190,8 @@ function Silo() {
+ + ); } diff --git a/src/pages/Transfer.tsx b/src/pages/Transfer.tsx index 57117cf28..4206fd51b 100644 --- a/src/pages/Transfer.tsx +++ b/src/pages/Transfer.tsx @@ -4,6 +4,9 @@ import { useParams } from "react-router-dom"; import { useChainId, useConfig } from "wagmi"; import TransferActions from "./transfer/TransferActions"; import TransferAll from "./transfer/actions/TransferAll"; +import TransferBeanstalkFertilizer from "./transfer/actions/TransferBeanstalkFertilizer"; +import TransferBeanstalkPods from "./transfer/actions/TransferBeanstalkPods"; +import TransferBeanstalkSilo from "./transfer/actions/TransferBeanstalkSilo"; import TransferDeposits from "./transfer/actions/TransferDeposits"; import TransferFarmBalance from "./transfer/actions/TransferFarmBalance"; import TransferPods from "./transfer/actions/TransferPods"; @@ -23,7 +26,13 @@ function Transfer() {
{`Move tokens to a different address${currentChain ? ` on ${currentChain.name}.` : "."}`}
- {mode === "all" ? ( + {mode === "beanstalk-silo" ? ( + + ) : mode === "beanstalk-pods" ? ( + + ) : mode === "beanstalk-fertilizer" ? ( + + ) : mode === "all" ? ( ) : mode === "farmbalance" ? ( diff --git a/src/pages/beanstalk/components/BeanstalkFertilizerSection.tsx b/src/pages/beanstalk/components/BeanstalkFertilizerSection.tsx new file mode 100644 index 000000000..74ccbd886 --- /dev/null +++ b/src/pages/beanstalk/components/BeanstalkFertilizerSection.tsx @@ -0,0 +1,68 @@ +import { TokenValue } from "@/classes/TokenValue"; +import BeanstalkStatField from "@/components/BeanstalkStatField"; +import TextSkeleton from "@/components/TextSkeleton"; +import { formatter } from "@/utils/format"; + +interface BeanstalkFertilizerSectionProps { + tokenCount: bigint; + fertilized: TokenValue; + unfertilized: TokenValue; + totalUnfertilizedSprouts: TokenValue; + isLoading: boolean; + disabled?: boolean; + onRinse?: () => void; + onSend?: () => void; +} + +/** + * Section component displaying fertilizer token count (bsFERT ERC1155 balance) + * Shows unfertilized sprouts as primary value, fertilized (rinsable) as secondary + */ +const BeanstalkFertilizerSection: React.FC = ({ + tokenCount, + fertilized, + unfertilized, + totalUnfertilizedSprouts, + isLoading, + disabled = false, + onRinse, + onSend, +}) => { + const hasBalance = tokenCount > 0n; + const hasRinsable = !fertilized.isZero; + + const sharePercent = totalUnfertilizedSprouts.gt(0) + ? ((unfertilized.toNumber() / totalUnfertilizedSprouts.toNumber()) * 100).toFixed(2) + : "0.00"; + + return ( + + + {disabled ? ( + N/A + ) : ( +
+
+ {formatter.number(unfertilized, { minDecimals: 2, maxDecimals: 2 })} + Sprouts ({sharePercent}%) +
+
+ {formatter.number(fertilized, { minDecimals: 2, maxDecimals: 2 })} Fertilized +
+
+ )} +
+
+ ); +}; + +export default BeanstalkFertilizerSection; diff --git a/src/pages/beanstalk/components/BeanstalkGlobalCard.tsx b/src/pages/beanstalk/components/BeanstalkGlobalCard.tsx new file mode 100644 index 000000000..30e791a8c --- /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 000000000..112b8e823 --- /dev/null +++ b/src/pages/beanstalk/components/BeanstalkObligationsCard.tsx @@ -0,0 +1,177 @@ +import { Button } from "@/components/ui/Button"; +import { abiSnippets } from "@/constants/abiSnippets"; +import { BARN_PAYBACK_ADDRESS, SILO_PAYBACK_ADDRESS } from "@/constants/address"; +import { beanstalkAbi, beanstalkAddress } from "@/generated/contractHooks"; +import useTransaction from "@/hooks/useTransaction"; +import { useBeanstalkGlobalStats } from "@/state/useBeanstalkGlobalStats"; +import { useFarmerBeanstalkRepayment } from "@/state/useFarmerBeanstalkRepayment"; +import { useCallback, useMemo } from "react"; +import { useNavigate } from "react-router-dom"; +import { useAccount, useChainId } 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 chainId = useChainId(); + const navigate = useNavigate(); + const { silo, pods, fertilizer, isLoading, isError, refetch } = useFarmerBeanstalkRepayment(); + const globalStats = useBeanstalkGlobalStats(); + + const isConnected = !!account.address; + const showDisabled = !isConnected || isError; + + // Total bsFERT token count from per-ID ERC1155 balances + const totalBsFert = useMemo(() => { + let total = 0n; + for (const detail of fertilizer.perIdData.values()) { + total += detail.balance; + } + return total; + }, [fertilizer.perIdData]); + + // Transaction hook for claim operations + const { writeWithEstimateGas, setSubmitting } = useTransaction({ + successCallback: () => { + refetch(); + }, + successMessage: "Claim successful", + errorMessage: "Claim failed", + }); + + // Silo Claim — Claim earned urBDV from SiloPayback contract directly + const handleClaimSilo = useCallback(async () => { + if (!account.address || silo.earned.isZero) return; + + try { + setSubmitting(true); + + // Call claim(address recipient, enum LibTransfer.To toMode) directly on SiloPayback contract + // toMode: 0 = INTERNAL, 1 = EXTERNAL (to wallet), 2 = INTERNAL_TOLERANT + await writeWithEstimateGas({ + address: SILO_PAYBACK_ADDRESS, + abi: abiSnippets.siloPayback, + functionName: "claim", + args: [account.address, 1], // Claim to wallet (EXTERNAL) + }); + } catch (error) { + console.error("Silo claim error:", error); + setSubmitting(false); + } + }, [account.address, silo.earned, writeWithEstimateGas, setSubmitting]); + + // Pods Harvest — Harvest harvestable pods from repayment field (fieldId=1) + const handleHarvestPods = useCallback(async () => { + if (!account.address) return; + + const harvestablePlotIndices = pods.plots.filter((p) => p.harvestablePods?.gt(0)).map((p) => p.index.toBigInt()); + + if (harvestablePlotIndices.length === 0) return; + + try { + setSubmitting(true); + + await writeWithEstimateGas({ + address: beanstalkAddress[chainId as keyof typeof beanstalkAddress], + abi: beanstalkAbi, + functionName: "harvest", + args: [1n, harvestablePlotIndices, 1], // fieldId=1, plot indices, EXTERNAL + }); + } catch (error) { + console.error("Pods harvest error:", error); + setSubmitting(false); + } + }, [account.address, pods.plots, writeWithEstimateGas, setSubmitting, chainId]); + + // Fertilizer Rinse — Rinse fertilized sprouts from BarnPayback contract directly + const handleRinseFert = useCallback(async () => { + if (!account.address) return; + + if (!fertilizer.fertilizerIds || fertilizer.fertilizerIds.length === 0) { + console.warn("Cannot rinse fertilizer: No fertilizer IDs available."); + return; + } + + try { + setSubmitting(true); + + await writeWithEstimateGas({ + address: BARN_PAYBACK_ADDRESS, + abi: abiSnippets.barnPayback, + functionName: "claimFertilized", + args: [fertilizer.fertilizerIds, 1], + }); + } catch (error) { + console.error("Fertilizer rinse error:", error); + setSubmitting(false); + } + }, [account.address, fertilizer.fertilizerIds, writeWithEstimateGas, setSubmitting]); + + const handleSendSilo = () => { + navigate("/transfer/beanstalk-silo"); + }; + + const handleSendPods = () => { + navigate("/transfer/beanstalk-pods"); + }; + + const handleMarketPods = () => { + navigate("/market/pods/buy/fill?beanstalk=true"); + }; + + const handleSendFertilizer = () => { + navigate("/transfer/beanstalk-fertilizer"); + }; + + 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 000000000..6e207290a --- /dev/null +++ b/src/pages/beanstalk/components/BeanstalkPodsSection.tsx @@ -0,0 +1,67 @@ +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; + harvestableIndex: TokenValue; + podIndex: TokenValue; + isLoading: boolean; + disabled?: boolean; + onHarvest?: () => void; + onSend?: () => void; + onMarket?: () => void; +} + +/** + * Section component displaying pods from the repayment field (fieldId=1) + * Shows PodLineGraph visualization + */ +const BeanstalkPodsSection: React.FC = ({ + plots, + totalPods, + harvestableIndex, + podIndex, + isLoading, + disabled = false, + onHarvest, + onSend, + onMarket, +}) => { + const hasPlots = plots.length > 0; + const hasPods = !totalPods.isZero; + const hasHarvestablePods = plots.some((p) => p.harvestablePods?.gt(0)); + 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 000000000..1464b156e --- /dev/null +++ b/src/pages/beanstalk/components/BeanstalkSiloSection.tsx @@ -0,0 +1,68 @@ +import { TokenValue } from "@/classes/TokenValue"; +import BeanstalkStatField from "@/components/BeanstalkStatField"; +import TextSkeleton from "@/components/TextSkeleton"; +import { formatter } from "@/utils/format"; + +interface BeanstalkSiloSectionProps { + balance: TokenValue; + earned: TokenValue; + totalDistributed: TokenValue; + isLoading: boolean; + disabled?: boolean; + onClaim?: () => void; + onSend?: () => void; +} + +/** + * Section component displaying urBDV token balance for Silo Payback + * Shows total balance as primary value, earned (claimable) as secondary + */ +const BeanstalkSiloSection: React.FC = ({ + balance, + earned, + totalDistributed, + isLoading, + disabled = false, + onClaim, + onSend, +}) => { + const hasBalance = !balance.isZero; + const hasEarned = !earned.isZero; + + const sharePercent = totalDistributed.gt(0) + ? ((balance.toNumber() / totalDistributed.toNumber()) * 100).toFixed(2) + : "0.00"; + + return ( + + + {disabled ? ( + N/A + ) : ( +
+
+ {formatter.number(balance, { minDecimals: 2, maxDecimals: 2 })} + + Beanstalk Silo Tokens ({sharePercent}%) + +
+
+ {formatter.number(earned, { minDecimals: 2, maxDecimals: 2 })} Earned +
+
+ )} +
+
+ ); +}; + +export default BeanstalkSiloSection; diff --git a/src/pages/market/MarketModeSelect.tsx b/src/pages/market/MarketModeSelect.tsx index fd03cd506..3e2556300 100644 --- a/src/pages/market/MarketModeSelect.tsx +++ b/src/pages/market/MarketModeSelect.tsx @@ -3,7 +3,7 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/Tabs"; import { ANALYTICS_EVENTS } from "@/constants/analytics-events"; import { trackSimpleEvent } from "@/utils/analytics"; import { useCallback, useMemo } from "react"; -import { useNavigate, useParams } from "react-router-dom"; +import { useNavigate, useParams, useSearchParams } from "react-router-dom"; type MarketMode = "buy" | "sell"; type MarketAction = "create" | "fill"; @@ -34,6 +34,16 @@ const ACTION_LABELS: Record> = { export default function MarketModeSelect({ onMainSelectionChange, onSecondarySelectionChange }: MarketModeSelectProps) { const { mode, id } = useParams(); const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + + // Helper to preserve current search params (e.g. ?beanstalk=true) during navigation + const buildPath = useCallback( + (path: string) => { + const params = searchParams.toString(); + return params ? `${path}?${params}` : path; + }, + [searchParams], + ); // Derive current state from URL params const { mainTab, secondaryTab, secondaryTabValue } = useMemo(() => { @@ -62,10 +72,10 @@ export default function MarketModeSelect({ onMainSelectionChange, onSecondarySel secondary_tab: secondaryTab, }); - navigate(`/market/pods/${newMode}/${defaultAction}`); + navigate(buildPath(`/market/pods/${newMode}/${defaultAction}`)); onMainSelectionChange?.(v); }, - [navigate, onMainSelectionChange, mainTab, secondaryTab], + [navigate, onMainSelectionChange, mainTab, secondaryTab, buildPath], ); const handleSecondaryChange = useCallback( @@ -79,13 +89,13 @@ export default function MarketModeSelect({ onMainSelectionChange, onSecondarySel }); if (v === "create") { - navigate(`/market/pods/${currentMode}/create`); + navigate(buildPath(`/market/pods/${currentMode}/create`)); } else if (v === "fill") { - navigate(`/market/pods/${currentMode}/fill`); + navigate(buildPath(`/market/pods/${currentMode}/fill`)); } onSecondarySelectionChange?.(v); }, - [mainTab, navigate, onSecondarySelectionChange, secondaryTab], + [mainTab, navigate, onSecondarySelectionChange, secondaryTab, buildPath], ); return ( diff --git a/src/pages/market/PodListingsTable.tsx b/src/pages/market/PodListingsTable.tsx index 35d750e2f..7e68f948d 100644 --- a/src/pages/market/PodListingsTable.tsx +++ b/src/pages/market/PodListingsTable.tsx @@ -7,6 +7,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import IconImage from "@/components/ui/IconImage"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/Table"; import { PODS } from "@/constants/internalTokens"; +import { useBeanstalkMarket } from "@/context/BeanstalkMarketContext"; import usePodListings from "@/state/market/usePodListings"; import { useHarvestableIndex } from "@/state/useFieldData"; import useTokenData from "@/state/useTokenData"; @@ -17,8 +18,9 @@ import { useNavigate, useParams } from "react-router-dom"; export function PodListingsTable() { const { id: selectedListing } = useParams(); const BEAN = useTokenData().mainToken; + const { podMarketplaceId } = useBeanstalkMarket(); - const podListingsQuery = usePodListings(); + const podListingsQuery = usePodListings(podMarketplaceId); const podListings = podListingsQuery.data?.podListings; const harvestableIndex = useHarvestableIndex(); diff --git a/src/pages/market/PodOrdersTable.tsx b/src/pages/market/PodOrdersTable.tsx index 8022c7370..c0a95398b 100644 --- a/src/pages/market/PodOrdersTable.tsx +++ b/src/pages/market/PodOrdersTable.tsx @@ -7,6 +7,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import IconImage from "@/components/ui/IconImage"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/Table"; import { PODS } from "@/constants/internalTokens"; +import { useBeanstalkMarket } from "@/context/BeanstalkMarketContext"; import usePodOrders from "@/state/market/usePodOrders"; import useTokenData from "@/state/useTokenData"; import { formatter } from "@/utils/format"; @@ -16,7 +17,8 @@ import { useNavigate, useParams } from "react-router-dom"; export function PodOrdersTable() { const { id: selectedOrder } = useParams(); const BEAN = useTokenData().mainToken; - const podOrdersQuery = usePodOrders(); + const { podMarketplaceId } = useBeanstalkMarket(); + const podOrdersQuery = usePodOrders(podMarketplaceId); const podOrders = podOrdersQuery.data?.podOrders; const filteredOrders: NonNullable["data"]>["podOrders"] = []; diff --git a/src/pages/market/actions/CancelListing.tsx b/src/pages/market/actions/CancelListing.tsx index 032308f97..4af2a7535 100644 --- a/src/pages/market/actions/CancelListing.tsx +++ b/src/pages/market/actions/CancelListing.tsx @@ -1,6 +1,7 @@ import { TokenValue } from "@/classes/TokenValue"; import SmartSubmitButton from "@/components/SmartSubmitButton"; import { PODS } from "@/constants/internalTokens"; +import { useBeanstalkMarket } from "@/context/BeanstalkMarketContext"; import { beanstalkAbi } from "@/generated/contractHooks"; import { AllPodListingsQuery } from "@/generated/gql/pintostalk/graphql"; import { useProtocolAddress } from "@/hooks/pinto/useProtocolAddress"; @@ -21,6 +22,7 @@ export default function CancelListing({ listing }: CancelListingProps) { const diamondAddress = useProtocolAddress(); const account = useAccount(); const harvestableIndex = useHarvestableIndex(); + const { fieldId } = useBeanstalkMarket(); const navigate = useNavigate(); const queryClient = useQueryClient(); @@ -50,7 +52,7 @@ export default function CancelListing({ listing }: CancelListingProps) { abi: beanstalkAbi, functionName: "cancelPodListing", args: [ - 0n, // fieldId + fieldId, // fieldId TokenValue.fromBlockchain(listing.index, PODS.decimals).toBigInt(), // index ], }); @@ -62,7 +64,7 @@ export default function CancelListing({ listing }: CancelListingProps) { } finally { setSubmitting(false); } - }, [listing, diamondAddress, setSubmitting, writeWithEstimateGas]); + }, [listing, diamondAddress, fieldId, setSubmitting, writeWithEstimateGas]); return ( <> diff --git a/src/pages/market/actions/CancelOrder.tsx b/src/pages/market/actions/CancelOrder.tsx index 65f80fd86..98aa5b560 100644 --- a/src/pages/market/actions/CancelOrder.tsx +++ b/src/pages/market/actions/CancelOrder.tsx @@ -4,6 +4,7 @@ import FarmBalanceToggle from "@/components/FarmBalanceToggle"; import SmartSubmitButton from "@/components/SmartSubmitButton"; import { Separator } from "@/components/ui/Separator"; import { PODS } from "@/constants/internalTokens"; +import { useBeanstalkMarket } from "@/context/BeanstalkMarketContext"; import { beanstalkAbi } from "@/generated/contractHooks"; import { AllPodOrdersQuery } from "@/generated/gql/pintostalk/graphql"; import { useProtocolAddress } from "@/hooks/pinto/useProtocolAddress"; @@ -29,6 +30,7 @@ export default function CancelOrder({ order }: CancelOrderProps) { const diamondAddress = useProtocolAddress(); const { queryKeys: balanceQKs } = useFarmerBalances(); const account = useAccount(); + const { fieldId } = useBeanstalkMarket(); const navigate = useNavigate(); const [mode, toFarm, setMode] = useFarmTogglePreference(); @@ -71,7 +73,7 @@ export default function CancelOrder({ order }: CancelOrderProps) { args: [ { orderer: account.address, // account - fieldId: 0n, // fieldId + fieldId: fieldId, // fieldId pricePerPod, // pricePerPod maxPlaceInLine, // maxPlaceInLine minFillAmount, // minFillAmount @@ -87,7 +89,7 @@ export default function CancelOrder({ order }: CancelOrderProps) { } finally { setSubmitting(false); } - }, [order, diamondAddress, account, toFarm, mainToken, setSubmitting, writeWithEstimateGas]); + }, [order, diamondAddress, account, fieldId, toFarm, mainToken, setSubmitting, writeWithEstimateGas]); return ( <> diff --git a/src/pages/market/actions/CreateListing.tsx b/src/pages/market/actions/CreateListing.tsx index 5cb237f8f..dbd1fbdf6 100644 --- a/src/pages/market/actions/CreateListing.tsx +++ b/src/pages/market/actions/CreateListing.tsx @@ -11,9 +11,11 @@ import { MultiSlider, Slider } from "@/components/ui/Slider"; import { Switch } from "@/components/ui/Switch"; import { ANALYTICS_EVENTS } from "@/constants/analytics-events"; import { PODS } from "@/constants/internalTokens"; +import { useBeanstalkMarket } from "@/context/BeanstalkMarketContext"; import { beanstalkAbi } from "@/generated/contractHooks"; import { useProtocolAddress } from "@/hooks/pinto/useProtocolAddress"; import useTransaction from "@/hooks/useTransaction"; +import { useFarmerBeanstalkRepayment } from "@/state/useFarmerBeanstalkRepayment"; import { useFarmerField } from "@/state/useFarmerField"; import { useHarvestableIndex, usePodIndex } from "@/state/useFieldData"; import { useQueryKeys } from "@/state/useQueryKeys"; @@ -89,12 +91,24 @@ export default function CreateListing({ onSelectionChange }: CreateListingProps const navigate = useNavigate(); const location = useLocation(); const farmerField = useFarmerField(); + const { isBeanstalkMarketplace, fieldId } = useBeanstalkMarket(); + const repayment = useFarmerBeanstalkRepayment(); const queryClient = useQueryClient(); const { allPodListings, allMarket, farmerMarket } = useQueryKeys({ account, harvestableIndex }); const allQK = useMemo(() => [allPodListings, allMarket, farmerMarket], [allPodListings, allMarket, farmerMarket]); - const userPlots = useMemo(() => farmerField?.plots || [], [farmerField?.plots]); + const userPlots = useMemo(() => { + if (isBeanstalkMarketplace) { + return repayment.pods.plots; + } + return farmerField?.plots || []; + }, [isBeanstalkMarketplace, farmerField?.plots, repayment.pods.plots]); + + const activeHarvestableIndex = useMemo( + () => (isBeanstalkMarketplace ? repayment.pods.harvestableIndex : harvestableIndex), + [isBeanstalkMarketplace, repayment.pods.harvestableIndex, harvestableIndex], + ); const [plot, setPlot] = useState([]); const [amount, setAmount] = useState(0); @@ -107,13 +121,18 @@ export default function CreateListing({ onSelectionChange }: CreateListingProps const [successAmount, setSuccessAmount] = useState(null); const [successPrice, setSuccessPrice] = useState(null); const podIndex = usePodIndex(); - const maxExpiration = Number.parseInt(podIndex.toHuman()) - Number.parseInt(harvestableIndex.toHuman()) || 0; + const activePodIndex = useMemo( + () => (isBeanstalkMarketplace ? repayment.pods.podIndex : podIndex), + [isBeanstalkMarketplace, repayment.pods.podIndex, podIndex], + ); + const maxExpiration = + Number.parseInt(activePodIndex.toHuman()) - Number.parseInt(activeHarvestableIndex.toHuman()) || 0; const [expiresIn, setExpiresIn] = useState(null); const selectedExpiresIn = expiresIn ?? maxExpiration; const minFill = TokenValue.fromHuman(0.1, PODS.decimals); const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); - const plotPosition = plot.length > 0 ? plot[0].index.sub(harvestableIndex) : TV.ZERO; + const plotPosition = plot.length > 0 ? plot[0].index.sub(activeHarvestableIndex) : TV.ZERO; // Helper: Find nearest plot within 10% tolerance (for context menu prefill) const findNearestPlot = useCallback( @@ -125,7 +144,7 @@ export default function CreateListing({ onSelectionChange }: CreateListingProps let minDistance = Infinity; for (const p of userPlots) { - const plotPos = Number(p.index.sub(harvestableIndex).toBigInt()); + const plotPos = Number(p.index.sub(activeHarvestableIndex).toBigInt()); const distance = Math.abs(plotPos - targetPosition); const tolerance = targetPosition * 0.1; // 10% tolerance @@ -137,19 +156,19 @@ export default function CreateListing({ onSelectionChange }: CreateListingProps return nearestPlot; }, - [userPlots, harvestableIndex], + [userPlots, activeHarvestableIndex], ); // Calculate max pods based on selected plots OR all farmer plots const maxPodAmount = useMemo(() => { - const plotsToUse = plot.length > 0 ? plot : farmerField.plots; + const plotsToUse = plot.length > 0 ? plot : userPlots; if (plotsToUse.length === 0) return 0; return plotsToUse.reduce((sum, p) => sum + p.pods.toNumber(), 0); - }, [plot, farmerField.plots]); + }, [plot, userPlots]); // Calculate position range in line const positionInfo = useMemo(() => { - const plotsToUse = plot.length > 0 ? plot : farmerField.plots; + const plotsToUse = plot.length > 0 ? plot : userPlots; if (plotsToUse.length === 0) return null; const minIndex = plotsToUse.reduce((min, p) => (p.index.lt(min) ? p.index : min), plotsToUse[0].index); @@ -159,10 +178,10 @@ export default function CreateListing({ onSelectionChange }: CreateListingProps }, plotsToUse[0].index); return { - start: minIndex.sub(harvestableIndex), - end: maxIndex.sub(harvestableIndex), + start: minIndex.sub(activeHarvestableIndex), + end: maxIndex.sub(activeHarvestableIndex), }; - }, [plot, farmerField.plots, harvestableIndex]); + }, [plot, userPlots, activeHarvestableIndex]); // Calculate selected pod range for PodLineGraph partial selection const selectedPodRange = useMemo(() => { @@ -243,7 +262,7 @@ export default function CreateListing({ onSelectionChange }: CreateListingProps const scores = listingData .map((data) => { - const placeInLine = data.index.sub(harvestableIndex).toNumber(); + const placeInLine = data.index.sub(activeHarvestableIndex).toNumber(); // Use placeInLine in millions for consistent scaling return calculatePodScore(pricePerPod, placeInLine / MILLION); }) @@ -255,7 +274,7 @@ export default function CreateListing({ onSelectionChange }: CreateListingProps const max = Math.max(...scores); return { min, max, isSingle: scores.length === 1 || min === max }; - }, [listingData, pricePerPod, harvestableIndex]); + }, [listingData, pricePerPod, activeHarvestableIndex]); // Notify parent component of selection changes useEffect(() => { @@ -324,7 +343,7 @@ export default function CreateListing({ onSelectionChange }: CreateListingProps } // Find matching plots from farmer's field using string comparison - const validPlots = farmerField.plots.filter((p) => selectedPlotIndices.includes(p.index.toHuman())); + const validPlots = userPlots.filter((p) => selectedPlotIndices.includes(p.index.toHuman())); if (validPlots.length > 0) { // Mark this selection as processed @@ -348,7 +367,7 @@ export default function CreateListing({ onSelectionChange }: CreateListingProps // Clean up location state to prevent re-selection on re-mount window.history.replaceState({}, document.title); } - }, [location.state, farmerField.plots, sortPlotsByIndex]); + }, [location.state, userPlots, sortPlotsByIndex]); // Prefill from context menu click - always update when new values arrive useEffect(() => { @@ -402,7 +421,7 @@ export default function CreateListing({ onSelectionChange }: CreateListingProps prefillExpiresIn, maxExpiration, findNearestPlot, - harvestableIndex, + activeHarvestableIndex, navigate, location.pathname, sortPlotsByIndex, @@ -468,7 +487,7 @@ export default function CreateListing({ onSelectionChange }: CreateListingProps const handlePlotGroupSelect = useCallback( (plotIndices: string[]) => { - const plotsInGroup = farmerField.plots.filter((p) => plotIndices.includes(p.index.toHuman())); + const plotsInGroup = userPlots.filter((p) => plotIndices.includes(p.index.toHuman())); if (plotsInGroup.length === 0) return; const allSelected = plotIndices.every((index) => plot.some((p) => p.index.toHuman() === index)); @@ -491,7 +510,7 @@ export default function CreateListing({ onSelectionChange }: CreateListingProps handlePlotSelection(newPlots); } }, - [farmerField.plots, plot, handlePlotSelection], + [userPlots, plot, handlePlotSelection], ); // reset form and invalidate pod listing query @@ -538,7 +557,7 @@ export default function CreateListing({ onSelectionChange }: CreateListingProps // pricePerPod should be encoded as uint24 with 6 decimals (0.5 * 1_000_000 = 500000) const encodedPricePerPod = pricePerPod ? Math.floor(pricePerPod * PRICE_PER_POD_CONFIG.DECIMAL_MULTIPLIER) : 0; const _expiresIn = TokenValue.fromHuman(selectedExpiresIn, PODS.decimals); - const maxHarvestableIndex = _expiresIn.add(harvestableIndex); + const maxHarvestableIndex = _expiresIn.add(activeHarvestableIndex); try { setSubmitting(true); toast.loading(`Creating ${listingData.length} Listing${listingData.length > 1 ? "s" : ""}...`); @@ -549,7 +568,7 @@ export default function CreateListing({ onSelectionChange }: CreateListingProps for (const data of listingData) { const listingArgs = { lister: account, - fieldId: 0n, + fieldId: fieldId, index: data.index.toBigInt(), start: data.start.toBigInt(), podAmount: data.amount.toBigInt(), @@ -588,7 +607,8 @@ export default function CreateListing({ onSelectionChange }: CreateListingProps pricePerPod, selectedExpiresIn, balanceTo, - harvestableIndex, + activeHarvestableIndex, + fieldId, minFill, plot, listingData, @@ -615,6 +635,9 @@ export default function CreateListing({ onSelectionChange }: CreateListingProps selectedPodRange={selectedPodRange} label="My Pods In Line" onPlotGroupSelect={handlePlotGroupSelect} + plots={userPlots} + customHarvestableIndex={activeHarvestableIndex} + customPodIndex={activePodIndex} /> {/* Position in Line Display (below graph) */} @@ -757,7 +780,7 @@ export default function CreateListing({ onSelectionChange }: CreateListingProps podAmount={amount} listingData={listingData} pricePerPod={pricePerPod} - harvestableIndex={harvestableIndex} + harvestableIndex={activeHarvestableIndex} /> )} (undefined); const [pricePerPod, setPricePerPod] = useState(PRICE_PER_POD_CONFIG.MIN); @@ -388,6 +394,7 @@ export default function CreateOrder() { minFill, fromMode, orderClipboard?.clipboard, + fieldId, ); advFarm.push(orderCallStruct); @@ -427,6 +434,7 @@ export default function CreateOrder() { tokenIn.symbol, podsOut, amountIn, + fieldId, ]); const swapDataNotReady = (shouldSwap && (!swapData || !swapBuild)) || !!swapQuery.error; @@ -438,8 +446,8 @@ export default function CreateOrder() { // Calculate orderRangeEnd for PodLineGraph overlay const orderRangeEnd = useMemo(() => { if (!maxPlaceInLine) return undefined; - return harvestableIndex.add(TokenValue.fromHuman(maxPlaceInLine.toString(), PODS.decimals)); - }, [maxPlaceInLine, harvestableIndex]); + return activeHarvestableIndex.add(TokenValue.fromHuman(maxPlaceInLine.toString(), PODS.decimals)); + }, [maxPlaceInLine, activeHarvestableIndex]); return (
diff --git a/src/pages/market/actions/FillListing.tsx b/src/pages/market/actions/FillListing.tsx index b00a9239f..fbcde708d 100644 --- a/src/pages/market/actions/FillListing.tsx +++ b/src/pages/market/actions/FillListing.tsx @@ -16,6 +16,7 @@ import { Separator } from "@/components/ui/Separator"; import { Slider } from "@/components/ui/Slider"; import { ANALYTICS_EVENTS } from "@/constants/analytics-events"; import { PODS } from "@/constants/internalTokens"; +import { useBeanstalkMarket } from "@/context/BeanstalkMarketContext"; import fillPodListing from "@/encoders/fillPodListing"; import { beanstalkAbi } from "@/generated/contractHooks"; import { useProtocolAddress } from "@/hooks/pinto/useProtocolAddress"; @@ -30,6 +31,7 @@ import useTransaction from "@/hooks/useTransaction"; import usePriceImpactSummary from "@/hooks/wells/usePriceImpactSummary"; import usePodListings from "@/state/market/usePodListings"; import { useFarmerBalances } from "@/state/useFarmerBalances"; +import { useFarmerBeanstalkRepayment } from "@/state/useFarmerBeanstalkRepayment"; import { useFarmerPlotsQuery } from "@/state/useFarmerField"; import { useHarvestableIndex, usePodIndex } from "@/state/useFieldData"; import { useQueryKeys } from "@/state/useQueryKeys"; @@ -96,6 +98,8 @@ export default function FillListing({ selectedListingId, selectedPlaceInLine, on const account = useAccount(); const farmerBalances = useFarmerBalances(); const harvestableIndex = useHarvestableIndex(); + const { podMarketplaceId, fieldId, isBeanstalkMarketplace } = useBeanstalkMarket(); + const repayment = useFarmerBeanstalkRepayment(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); // Use prop if provided, otherwise fall back to URL param @@ -119,7 +123,7 @@ export default function FillListing({ selectedListingId, selectedPlaceInLine, on filterLP: true, }); - const podListings = usePodListings(); + const podListings = usePodListings(podMarketplaceId); const allListings = podListings.data; const [didSetPreferred, setDidSetPreferred] = useState(false); @@ -139,7 +143,9 @@ export default function FillListing({ selectedListingId, selectedPlaceInLine, on // Place in line state const podIndex = usePodIndex(); - const maxPlace = Number.parseInt(podIndex.toHuman()) - Number.parseInt(harvestableIndex.toHuman()) || 0; + const activeHarvestableIndex = isBeanstalkMarketplace ? repayment.pods.harvestableIndex : harvestableIndex; + const activePodIndex = isBeanstalkMarketplace ? repayment.pods.podIndex : podIndex; + const maxPlace = Number.parseInt(activePodIndex.toHuman()) - Number.parseInt(activeHarvestableIndex.toHuman()) || 0; const [maxPlaceInLine, setMaxPlaceInLine] = useState(undefined); const [hasInitializedPlace, setHasInitializedPlace] = useState(false); @@ -213,12 +219,12 @@ export default function FillListing({ selectedListingId, selectedPlaceInLine, on if (Number.isNaN(placeInLine) || placeInLine <= 0) { // Fallback to calculating from listing index if URL value is invalid const listingIndex = TokenValue.fromBlockchain(listing.index, PODS.decimals); - placeInLine = listingIndex.sub(harvestableIndex).toNumber(); + placeInLine = listingIndex.sub(activeHarvestableIndex).toNumber(); } } else { // Calculate listing's place in line from index (fallback for direct URL access) const listingIndex = TokenValue.fromBlockchain(listing.index, PODS.decimals); - placeInLine = listingIndex.sub(harvestableIndex).toNumber(); + placeInLine = listingIndex.sub(activeHarvestableIndex).toNumber(); } // Set max place in line to the place in line plus one (to include the current plot) @@ -226,7 +232,7 @@ export default function FillListing({ selectedListingId, selectedPlaceInLine, on const maxPlaceValue = Math.min(maxPlace, Math.max(0, placeInLine + 1)); setMaxPlaceInLine(maxPlaceValue); setHasInitializedPlace(true); // Mark as initialized to prevent default value override - }, [listingId, allListings, maxPlace, mainToken.decimals, harvestableIndex, placeInLineFromUrl]); + }, [listingId, allListings, maxPlace, mainToken.decimals, activeHarvestableIndex, placeInLineFromUrl]); // Token selection handler with tracking const handleTokenSelection = useCallback( @@ -325,7 +331,7 @@ export default function FillListing({ selectedListingId, selectedPlaceInLine, on // Calculate place in line boundary for filtering const maxPlaceIndex = maxPlaceInLine - ? harvestableIndex.add(TokenValue.fromHuman(maxPlaceInLine.toString(), PODS.decimals)) + ? activeHarvestableIndex.add(TokenValue.fromHuman(maxPlaceInLine.toString(), PODS.decimals)) : undefined; // Determine eligible listings (shown as green on graph) @@ -349,13 +355,13 @@ export default function FillListing({ selectedListingId, selectedPlaceInLine, on // Calculate range overlay for visual feedback on graph const overlay = maxPlaceInLine ? { - start: harvestableIndex, - end: harvestableIndex.add(TokenValue.fromHuman(maxPlaceInLine.toString(), PODS.decimals)), + start: activeHarvestableIndex, + end: activeHarvestableIndex.add(TokenValue.fromHuman(maxPlaceInLine.toString(), PODS.decimals)), } : undefined; return { listingPlots: plots, eligibleListingIds: eligible, rangeOverlay: overlay }; - }, [allListings, maxPricePerPod, maxPlaceInLine, mainToken.decimals, harvestableIndex]); + }, [allListings, maxPricePerPod, maxPlaceInLine, mainToken.decimals, activeHarvestableIndex]); // Notify parent component when filter values change for chart highlighting useEffect(() => { @@ -468,7 +474,7 @@ export default function FillListing({ selectedListingId, selectedPlaceInLine, on const { listing, beanAmount } = listingsToFill[0]; const listingPrice = TokenValue.fromBlockchain(listing.pricePerPod, mainToken.decimals); const podsFromListing = beanAmount.div(listingPrice); - const listingPlace = TokenValue.fromBlockchain(listing.index, PODS.decimals).sub(harvestableIndex); + const listingPlace = TokenValue.fromBlockchain(listing.index, PODS.decimals).sub(activeHarvestableIndex); return { avgPricePerPod: listingPrice, @@ -485,7 +491,7 @@ export default function FillListing({ selectedListingId, selectedPlaceInLine, on for (const { listing, beanAmount } of listingsToFill) { const listingPrice = TokenValue.fromBlockchain(listing.pricePerPod, mainToken.decimals); const podsFromListing = beanAmount.div(listingPrice); - const listingPlace = TokenValue.fromBlockchain(listing.index, PODS.decimals).sub(harvestableIndex); + const listingPlace = TokenValue.fromBlockchain(listing.index, PODS.decimals).sub(activeHarvestableIndex); const pods = podsFromListing.toNumber(); totalValue += listingPrice.toNumber() * pods; @@ -501,7 +507,7 @@ export default function FillListing({ selectedListingId, selectedPlaceInLine, on avgPlaceInLine: TokenValue.fromHuman(avgPlaceInLine, PODS.decimals), totalPods, }; - }, [listingsToFill, mainToken.decimals, harvestableIndex]); + }, [listingsToFill, mainToken.decimals, activeHarvestableIndex]); // Calculate total tokens needed to fill eligible listings const totalMainTokensToFill = useMemo(() => { @@ -574,7 +580,7 @@ export default function FillListing({ selectedListingId, selectedPlaceInLine, on args: [ { lister: listing.farmer.id as Address, - fieldId: 0n, + fieldId: fieldId, index: TokenValue.fromBlockchain(listing.index, PODS.decimals).toBigInt(), start: TokenValue.fromBlockchain(listing.start, PODS.decimals).toBigInt(), podAmount: TokenValue.fromBlockchain(listing.amount, PODS.decimals).toBigInt(), @@ -628,6 +634,7 @@ export default function FillListing({ selectedListingId, selectedPlaceInLine, on beanAmount, FarmFromMode.INTERNAL, clipboard, + fieldId, ); advFarm.push(fillCall); diff --git a/src/pages/market/actions/FillOrder.tsx b/src/pages/market/actions/FillOrder.tsx index a75aa2af9..b21b86956 100644 --- a/src/pages/market/actions/FillOrder.tsx +++ b/src/pages/market/actions/FillOrder.tsx @@ -10,12 +10,14 @@ import { Separator } from "@/components/ui/Separator"; import { MultiSlider } from "@/components/ui/Slider"; import { ANALYTICS_EVENTS } from "@/constants/analytics-events"; import { PODS } from "@/constants/internalTokens"; +import { useBeanstalkMarket } from "@/context/BeanstalkMarketContext"; import { beanstalkAbi } from "@/generated/contractHooks"; import { useProtocolAddress } from "@/hooks/pinto/useProtocolAddress"; import { useFarmTogglePreference } from "@/hooks/useFarmTogglePreference"; import useTransaction from "@/hooks/useTransaction"; import usePodOrders from "@/state/market/usePodOrders"; import { useFarmerBalances } from "@/state/useFarmerBalances"; +import { useFarmerBeanstalkRepayment } from "@/state/useFarmerBeanstalkRepayment"; import { useFarmerField, useFarmerPlotsQuery } from "@/state/useFarmerField"; import { useHarvestableIndex, usePodIndex } from "@/state/useFieldData"; import { useQueryKeys } from "@/state/useQueryKeys"; @@ -32,7 +34,6 @@ import { useAccount } from "wagmi"; import CancelOrder from "./CancelOrder"; // Constants -const FIELD_ID = 0n; const MIN_PODS_THRESHOLD = 1; // Minimum pods required for order eligibility // Helper Functions @@ -84,6 +85,7 @@ export default function FillOrder({ selectedOrderId }: FillOrderProps) { const podLine = podIndex.sub(harvestableIndex); const navigate = useNavigate(); const [searchParams] = useSearchParams(); + const { isBeanstalkMarketplace, fieldId, podMarketplaceId } = useBeanstalkMarket(); // Use prop if provided, otherwise fall back to URL param const orderId = selectedOrderId || searchParams.get("orderId"); @@ -118,9 +120,22 @@ export default function FillOrder({ selectedOrderId }: FillOrderProps) { prevTotalCapacityRef.current = -1; // Reset to allow re-triggering range update }, [selectedOrderIds]); - const podOrders = usePodOrders(); + const podOrders = usePodOrders(podMarketplaceId); const allOrders = podOrders.data; const farmerField = useFarmerField(); + const repayment = useFarmerBeanstalkRepayment(); + + const userPlots = useMemo(() => { + if (isBeanstalkMarketplace) { + return repayment.pods.plots; + } + return farmerField?.plots || []; + }, [isBeanstalkMarketplace, farmerField?.plots, repayment.pods.plots]); + + const activeHarvestableIndex = useMemo( + () => (isBeanstalkMarketplace ? repayment.pods.harvestableIndex : harvestableIndex), + [isBeanstalkMarketplace, repayment.pods.harvestableIndex, harvestableIndex], + ); const { selectedOrders, orderPositions, totalCapacity } = useMemo(() => { if (!allOrders?.podOrders) return { selectedOrders: [], orderPositions: [], totalCapacity: 0 }; @@ -195,7 +210,7 @@ export default function FillOrder({ selectedOrderId }: FillOrderProps) { if (!allOrders?.podOrders) return []; // Get farmer's frontmost pod position (lowest index) - const farmerPlots = farmerField.plots; + const farmerPlots = userPlots; const farmerFrontmostPodIndex = farmerPlots.length > 0 ? farmerPlots.reduce((min, plot) => (plot.index.lt(min) ? plot.index : min), farmerPlots[0].index) @@ -208,17 +223,19 @@ export default function FillOrder({ selectedOrderId }: FillOrderProps) { } // Check if farmer has pods that can fill this order - // Order's maxPlaceInLine + harvestableIndex must be >= farmer's frontmost pod index + // Order's maxPlaceInLine + activeHarvestableIndex must be >= farmer's frontmost pod index if (!farmerFrontmostPodIndex) { return false; // No pods available } - const orderMaxPlaceIndex = harvestableIndex.add(TokenValue.fromBlockchain(order.maxPlaceInLine, PODS.decimals)); + const orderMaxPlaceIndex = activeHarvestableIndex.add( + TokenValue.fromBlockchain(order.maxPlaceInLine, PODS.decimals), + ); // Farmer's pod must be at or before the order's maxPlaceInLine position return farmerFrontmostPodIndex.lte(orderMaxPlaceIndex); }); - }, [allOrders?.podOrders, mainToken.decimals, podLine, farmerField.plots, harvestableIndex]); + }, [allOrders?.podOrders, mainToken.decimals, podLine, userPlots, activeHarvestableIndex]); useEffect(() => { if (totalCapacity !== prevTotalCapacityRef.current) { @@ -246,7 +263,7 @@ export default function FillOrder({ selectedOrderId }: FillOrderProps) { return eligibleOrders.map((order) => { const orderMaxPlace = TokenValue.fromBlockchain(order.maxPlaceInLine, PODS.decimals); - const markerIndex = harvestableIndex.add(orderMaxPlace); + const markerIndex = activeHarvestableIndex.add(orderMaxPlace); return { index: markerIndex, @@ -255,7 +272,7 @@ export default function FillOrder({ selectedOrderId }: FillOrderProps) { id: order.id, } as Plot; }); - }, [eligibleOrders, harvestableIndex]); + }, [eligibleOrders, activeHarvestableIndex]); const plotsForGraph = useMemo(() => { return orderMarkers; @@ -294,7 +311,7 @@ export default function FillOrder({ selectedOrderId }: FillOrderProps) { }); const onSubmit = useCallback(async () => { - if (ordersToFill.length === 0 || !account || farmerField.plots.length === 0) { + if (ordersToFill.length === 0 || !account || userPlots.length === 0) { return; } @@ -324,7 +341,7 @@ export default function FillOrder({ selectedOrderId }: FillOrderProps) { toast.loading(`Filling ${ordersToFill.length} Order${ordersToFill.length !== 1 ? "s" : ""}...`); // Sort farmer plots by index to use them in order (only sort once) - const sortedPlots = [...farmerField.plots].sort((a, b) => a.index.sub(b.index).toNumber()); + const sortedPlots = [...userPlots].sort((a, b) => a.index.sub(b.index).toNumber()); if (sortedPlots.length === 0) { throw new Error("No pods available to fill orders"); @@ -340,7 +357,7 @@ export default function FillOrder({ selectedOrderId }: FillOrderProps) { for (const { order: orderToFill, amount: fillAmount } of ordersToFill) { let remainingAmount = fillAmount; - const orderMaxPlaceIndex = harvestableIndex.add( + const orderMaxPlaceIndex = activeHarvestableIndex.add( TokenValue.fromBlockchain(orderToFill.maxPlaceInLine, PODS.decimals), ); @@ -373,7 +390,7 @@ export default function FillOrder({ selectedOrderId }: FillOrderProps) { // Create fillPodOrder call for this order with pod allocation from current plot const fillOrderArgs = { orderer: orderToFill.farmer.id as Address, - fieldId: FIELD_ID, + fieldId: fieldId, maxPlaceInLine: BigInt(orderToFill.maxPlaceInLine), pricePerPod: Number(orderToFill.pricePerPod), minFillAmount: BigInt(orderToFill.minFillAmount), @@ -430,13 +447,14 @@ export default function FillOrder({ selectedOrderId }: FillOrderProps) { }, [ ordersToFill, account, - farmerField.plots, + userPlots, writeWithEstimateGas, setSubmitting, diamondAddress, amount, weightedAvgPricePerPod, - harvestableIndex, + activeHarvestableIndex, + fieldId, ]); const isOwnOrder = useMemo(() => { diff --git a/src/pages/overview/FarmerOverview.tsx b/src/pages/overview/FarmerOverview.tsx index 2a8b3cba7..9a4ae628f 100644 --- a/src/pages/overview/FarmerOverview.tsx +++ b/src/pages/overview/FarmerOverview.tsx @@ -569,4 +569,4 @@ const Overview = () => { export default Overview; -const ORDER_TYPES: OrderType[] = ["sow", "convertUp"] as const; +const ORDER_TYPES: OrderType[] = ["sow", "convertUp", "automateClaim"] as const; diff --git a/src/pages/silo/RewardsClaim.tsx b/src/pages/silo/RewardsClaim.tsx index 4cb0dd0d1..26a25462d 100644 --- a/src/pages/silo/RewardsClaim.tsx +++ b/src/pages/silo/RewardsClaim.tsx @@ -1,4 +1,5 @@ import { TokenValue } from "@/classes/TokenValue"; +import { SpecifyConditionsDialog } from "@/components/Tractor/AutomateClaim"; import { Button } from "@/components/ui/Button"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/Card"; import { Label } from "@/components/ui/Label"; @@ -8,6 +9,7 @@ import { useFarmerSilo } from "@/state/useFarmerSilo"; import { useSiloData } from "@/state/useSiloData"; import useTokenData from "@/state/useTokenData"; import { useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; import { toast } from "sonner"; import { encodeFunctionData } from "viem"; import { useAccount } from "wagmi"; @@ -17,6 +19,7 @@ function RewardsClaim() { const chainId = useChainId(); const account = useAccount(); const data = useFarmerSilo(); + const [showAutomateClaimDialog, setShowAutomateClaimDialog] = useState(false); const siloData = useSiloData(); const { mainToken: BEAN, whitelistedTokens: SILO_WHITELIST } = useTokenData(); const stalkRewards = siloData.tokenData.get(BEAN)?.rewards.stalk; @@ -68,35 +71,45 @@ function RewardsClaim() { } return ( - - - Rewards - Claim your Silo Rewards - - -
- -
{data.earnedBeansBalance.toHuman("short")}
-
-
- -
{data.earnedBeansBalance.mul(stalkRewards ?? TokenValue.ZERO).toHuman("short")}
-
-
- -
{data.earnedBeansBalance.mul(seedsRewards ?? TokenValue.ZERO).toHuman("short")}
-
-
- -
{grownStalk.toHuman("short")}
-
-
- - - -
+ <> + + + Rewards + Claim your Silo Rewards + + +
+ +
{data.earnedBeansBalance.toHuman("short")}
+
+
+ +
{data.earnedBeansBalance.mul(stalkRewards ?? TokenValue.ZERO).toHuman("short")}
+
+
+ +
{data.earnedBeansBalance.mul(seedsRewards ?? TokenValue.ZERO).toHuman("short")}
+
+
+ +
{grownStalk.toHuman("short")}
+
+
+ + + setShowAutomateClaimDialog(true)} + > + Automate with Tractor + + +
+ + + ); } diff --git a/src/pages/silo/SiloConvertUpContent.tsx b/src/pages/silo/SiloConvertUpContent.tsx index f398b776f..c8db5efce 100644 --- a/src/pages/silo/SiloConvertUpContent.tsx +++ b/src/pages/silo/SiloConvertUpContent.tsx @@ -5,7 +5,7 @@ import ConvertUpOrderForm from "@/components/Tractor/ConvertUpOrderForm"; import ConvertUpOrderbookDialog from "@/components/Tractor/ConvertUpOrderbook"; import ConvertUpTractorOrderBookChart from "@/components/Tractor/ConvertUpTractorOrderBookChart"; import ConvertUpTractorOrders from "@/components/Tractor/ConvertUpTractorOrders"; -import { TractorConvertUpOrdersPanel } from "@/components/Tractor/farmer-orders/TractorOrdersPanel"; +import TractorOrdersPanelGeneric from "@/components/Tractor/farmer-orders/TractorOrdersPanel"; import CompactSeasonalLineChart from "@/components/charts/CompactSeasonalLineChart"; import { tabToSeasonalLookback } from "@/components/charts/SeasonalChart"; import TimeTabsSelector, { TimeTab } from "@/components/charts/TimeTabs"; @@ -31,6 +31,8 @@ import SiloConvertUpStats from "./SiloConvertUpStats"; const mobileOnlyActions = ["orders"] as const; const actions = [...mobileOnlyActions, "tractor"] as const; +const SILO_ORDER_TYPES = ["convertUp", "automateClaim"] as const; + // Pull this out to minimize re-renders const useConvertUpTractorActiveTab = () => { const isMobile = useIsMobile(); @@ -116,7 +118,7 @@ export const SiloConvertUpContent = () => { setOpen(true)} /> - +
diff --git a/src/pages/transfer/TransferActions.tsx b/src/pages/transfer/TransferActions.tsx index c4ad9c69a..fea40e4d6 100644 --- a/src/pages/transfer/TransferActions.tsx +++ b/src/pages/transfer/TransferActions.tsx @@ -3,10 +3,12 @@ import { TokenValue } from "@/classes/TokenValue"; import { Button } from "@/components/ui/Button"; import IconImage from "@/components/ui/IconImage"; import { useFarmerBalances } from "@/state/useFarmerBalances"; +import { useFarmerBeanstalkRepayment } from "@/state/useFarmerBeanstalkRepayment"; import { useFarmerField } from "@/state/useFarmerField"; import { useFarmerSilo } from "@/state/useFarmerSilo"; import { usePriceData } from "@/state/usePriceData"; import { formatter } from "@/utils/format"; +import { useMemo } from "react"; import { Link } from "react-router-dom"; export default function TransferActions() { @@ -15,6 +17,16 @@ export default function TransferActions() { const farmerBalance = useFarmerBalances(); const farmerSilo = useFarmerSilo(); const farmerField = useFarmerField(); + const repayment = useFarmerBeanstalkRepayment(); + + // Compute total bsFERT token count from per-ID balances + const totalBsFert = useMemo(() => { + let total = 0n; + for (const detail of repayment.fertilizer.perIdData.values()) { + total += detail.balance; + } + return total; + }, [repayment.fertilizer.perIdData]); const totalInternalBalance = Array.from(farmerBalance.balances).reduce( (total: TokenValue, tokenBalance) => @@ -25,7 +37,13 @@ export default function TransferActions() { TokenValue.ZERO, ); - const disableSendAll = totalInternalBalance.eq(0) && farmerSilo.depositsUSD.eq(0) && farmerField.totalPods.eq(0); + const disableSendAll = + totalInternalBalance.eq(0) && + farmerSilo.depositsUSD.eq(0) && + farmerField.totalPods.eq(0) && + repayment.silo.balance.eq(0) && + repayment.pods.totalPods.eq(0) && + totalBsFert === 0n; return (
@@ -75,6 +93,39 @@ export default function TransferActions() {
+ + +
); } diff --git a/src/pages/transfer/actions/TransferAll.tsx b/src/pages/transfer/actions/TransferAll.tsx index 6f166f11b..6878f1433 100644 --- a/src/pages/transfer/actions/TransferAll.tsx +++ b/src/pages/transfer/actions/TransferAll.tsx @@ -1,12 +1,15 @@ import FlowForm from "@/components/FormFlow"; +import { abiSnippets } from "@/constants/abiSnippets"; +import { BARN_PAYBACK_ADDRESS, SILO_PAYBACK_ADDRESS } from "@/constants/address"; import { beanstalkAbi, beanstalkAddress } from "@/generated/contractHooks"; import useTransaction from "@/hooks/useTransaction"; import { useFarmerBalances } from "@/state/useFarmerBalances"; +import { useFarmerBeanstalkRepayment } from "@/state/useFarmerBeanstalkRepayment"; import { useFarmerField } from "@/state/useFarmerField"; import { useFarmerSilo } from "@/state/useFarmerSilo"; import { FarmFromMode, FarmToMode } from "@/utils/types"; import { useQueryClient } from "@tanstack/react-query"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { toast } from "sonner"; import { type Address, encodeFunctionData } from "viem"; @@ -35,6 +38,16 @@ export default function TransferAll() { const hasPlots = farmerField.plots.length > 0; + const repayment = useFarmerBeanstalkRepayment(); + const hasBeanstalkSilo = repayment.silo.balance.gt(0); + const hasBeanstalkPods = repayment.pods.plots.length > 0; + const hasBeanstalkFert = useMemo(() => { + for (const detail of repayment.fertilizer.perIdData.values()) { + if (detail.balance > 0n) return true; + } + return false; + }, [repayment.fertilizer.perIdData]); + const queryClient = useQueryClient(); const navigate = useNavigate(); @@ -43,19 +56,11 @@ export default function TransferAll() { }, [destination]); const { writeWithEstimateGas, setSubmitting } = useTransaction({ - successCallback: () => { - for (const queryKey of farmerSilo.queryKeys) { - queryClient.invalidateQueries({ queryKey }); - } - farmerBalances.refetch(); - farmerField.refetch(); - navigate("/transfer"); - }, successMessage: "Transfer success", errorMessage: "Transfer failed", }); - function onSubmit() { + async function onSubmit() { try { setSubmitting(true); toast.loading("Transferring..."); @@ -93,9 +98,8 @@ export default function TransferAll() { farmData.push(depositTransferCall); } - // Plot Transfers + // Plot Transfers (fieldId=0) if (hasPlots) { - // todo: add support for more than one plot line const fieldId = BigInt(0); const ids: bigint[] = []; const starts: bigint[] = []; @@ -108,24 +112,82 @@ export default function TransferAll() { const plotTransferCall = encodeFunctionData({ abi: beanstalkAbi, functionName: "transferPlots", - args: [ - account.address, - destination as Address, - fieldId, - ids, //plot ids - starts, // starts - ends, // ends - ], + args: [account.address, destination as Address, fieldId, ids, starts, ends], + }); + farmData.push(plotTransferCall); + } + + // Beanstalk Repayment Pods (fieldId=1) + if (hasBeanstalkPods) { + const fieldId = BigInt(1); + const ids: bigint[] = []; + const starts: bigint[] = []; + const ends: bigint[] = []; + for (const plotData of repayment.pods.plots) { + ids.push(plotData.index.toBigInt()); + starts.push(BigInt(0)); + ends.push(plotData.pods.toBigInt()); + } + const plotTransferCall = encodeFunctionData({ + abi: beanstalkAbi, + functionName: "transferPlots", + args: [account.address, destination as Address, fieldId, ids, starts, ends], }); farmData.push(plotTransferCall); } - return writeWithEstimateGas({ - address: beanstalkAddress[chainId as keyof typeof beanstalkAddress], - abi: beanstalkAbi, - functionName: "farm", - args: [farmData], - }); + // Execute farm() with all batched calls (skip if nothing to batch) + if (farmData.length > 0) { + await writeWithEstimateGas({ + address: beanstalkAddress[chainId as keyof typeof beanstalkAddress], + abi: beanstalkAbi, + functionName: "farm", + args: [farmData], + }); + } + + // Beanstalk Repayment Fertilizer — direct ERC1155 transfer (not via farm) + // BarnPayback is a separate contract; calling via farm() makes diamond the msg.sender + // which requires the user to have approved diamond. Direct call avoids this. + if (hasBeanstalkFert) { + const fertIds: bigint[] = []; + const fertValues: bigint[] = []; + for (const [idStr, detail] of repayment.fertilizer.perIdData) { + if (detail.balance > 0n) { + fertIds.push(BigInt(idStr)); + fertValues.push(detail.balance); + } + } + if (fertIds.length > 0) { + toast.loading("Transferring bsFERT..."); + await writeWithEstimateGas({ + address: BARN_PAYBACK_ADDRESS as Address, + abi: abiSnippets.barnPayback, + functionName: "safeBatchTransferFrom", + args: [account.address, destination as Address, fertIds, fertValues, "0x"], + }); + } + } + + // urBDV transfer is a separate contract call (not via farm()) + if (hasBeanstalkSilo) { + toast.loading("Transferring urBDV..."); + await writeWithEstimateGas({ + address: SILO_PAYBACK_ADDRESS as Address, + abi: abiSnippets.siloPayback, + functionName: "transfer", + args: [destination as Address, repayment.silo.balance.toBigInt()], + }); + } + + // All transfers complete — invalidate caches and navigate + for (const queryKey of farmerSilo.queryKeys) { + queryClient.invalidateQueries({ queryKey }); + } + farmerBalances.refetch(); + farmerField.refetch(); + repayment.refetch(); + navigate("/transfer"); } catch (e) { console.error(e); toast.dismiss(); diff --git a/src/pages/transfer/actions/TransferBeanstalkFertilizer.tsx b/src/pages/transfer/actions/TransferBeanstalkFertilizer.tsx new file mode 100644 index 000000000..4a78b9ca5 --- /dev/null +++ b/src/pages/transfer/actions/TransferBeanstalkFertilizer.tsx @@ -0,0 +1,115 @@ +import FlowForm from "@/components/FormFlow"; +import { BARN_PAYBACK_ADDRESS } from "@/constants/address"; +import { beanstalkAbi, beanstalkAddress } from "@/generated/contractHooks"; +import useTransaction from "@/hooks/useTransaction"; +import { useFarmerBeanstalkRepayment } from "@/state/useFarmerBeanstalkRepayment"; +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { toast } from "sonner"; +import { type Address, encodeFunctionData } from "viem"; +import { useAccount, useChainId } from "wagmi"; +import FinalStep from "./beanstalk-fertilizer/FinalStep"; +import StepOne from "./beanstalk-fertilizer/StepOne"; + +export interface FertilizerTransferItem { + id: bigint; + value: bigint; +} + +export default function TransferBeanstalkFertilizer() { + const account = useAccount(); + const chainId = useChainId(); + const navigate = useNavigate(); + + const [step, setStep] = useState(1); + const [destination, setDestination] = useState(); + const [selectedIds, setSelectedIds] = useState([]); + const [transferNotice, setTransferNotice] = useState(false); + + const repayment = useFarmerBeanstalkRepayment(); + + useEffect(() => { + setTransferNotice(false); + }, [destination]); + + const stepDescription = step === 1 ? "Select Fertilizer IDs and recipient" : "Confirm send"; + + const enableNextStep = + step === 1 + ? selectedIds.length > 0 && selectedIds.every((item) => item.value > 0n) && !!destination && transferNotice + : true; + + const { writeWithEstimateGas, setSubmitting } = useTransaction({ + successCallback: () => { + repayment.refetch(); + navigate("/transfer"); + }, + successMessage: "Transfer success", + errorMessage: "Transfer failed", + }); + + function onSubmit() { + setSubmitting(true); + toast.loading("Transferring..."); + try { + if (!account.address || !destination) return; + + const farmData: `0x${string}`[] = []; + + if (selectedIds.length === 1) { + // Single ID: use transferERC1155 + const transferCall = encodeFunctionData({ + abi: beanstalkAbi, + functionName: "transferERC1155", + args: [BARN_PAYBACK_ADDRESS as Address, destination as Address, selectedIds[0].id, selectedIds[0].value], + }); + farmData.push(transferCall); + } else { + // Multiple IDs: use batchTransferERC1155 + const ids = selectedIds.map((item) => item.id); + const values = selectedIds.map((item) => item.value); + const batchTransferCall = encodeFunctionData({ + abi: beanstalkAbi, + functionName: "batchTransferERC1155", + args: [BARN_PAYBACK_ADDRESS as Address, destination as Address, ids, values], + }); + farmData.push(batchTransferCall); + } + + return writeWithEstimateGas({ + address: beanstalkAddress[chainId as keyof typeof beanstalkAddress], + abi: beanstalkAbi, + functionName: "farm", + args: [farmData], + }); + } catch (e) { + console.error("Transfer beanstalk fertilizer failed", e); + toast.dismiss(); + toast.error("Transfer failed"); + } + } + + return ( + + {step === 1 ? ( + + ) : ( + + )} + + ); +} diff --git a/src/pages/transfer/actions/TransferBeanstalkPods.tsx b/src/pages/transfer/actions/TransferBeanstalkPods.tsx new file mode 100644 index 000000000..cbd934c5e --- /dev/null +++ b/src/pages/transfer/actions/TransferBeanstalkPods.tsx @@ -0,0 +1,125 @@ +import { TokenValue } from "@/classes/TokenValue"; +import FlowForm from "@/components/FormFlow"; +import { beanstalkAbi, beanstalkAddress } from "@/generated/contractHooks"; +import useTransaction from "@/hooks/useTransaction"; +import { useFarmerBeanstalkRepayment } from "@/state/useFarmerBeanstalkRepayment"; +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { toast } from "sonner"; +import { type Address, encodeFunctionData } from "viem"; +import { useAccount, useChainId } from "wagmi"; +import FinalStep from "./beanstalk-pods/FinalStep"; +import StepOne from "./beanstalk-pods/StepOne"; + +export interface PodTransferData { + id: TokenValue; + start: TokenValue; + end: TokenValue; +} + +export default function TransferBeanstalkPods() { + const account = useAccount(); + const chainId = useChainId(); + const navigate = useNavigate(); + + const [step, setStep] = useState(1); + const [destination, setDestination] = useState(); + const [transferData, setTransferData] = useState([]); + const [transferNotice, setTransferNotice] = useState(false); + + const repayment = useFarmerBeanstalkRepayment(); + + useEffect(() => { + setTransferNotice(false); + }, [destination]); + + const stepDescription = () => { + switch (step) { + case 1: + return "Select Beanstalk Plots"; + default: + return "Confirm send"; + } + }; + + const enableNextStep = () => { + switch (step) { + case 1: + return transferData.length > 0 && !!destination && transferNotice; + default: + return true; + } + }; + + const { writeWithEstimateGas, setSubmitting } = useTransaction({ + successCallback: () => { + repayment.refetch(); + navigate("/transfer"); + }, + successMessage: "Transfer success", + errorMessage: "Transfer failed", + }); + + function onSubmit() { + setSubmitting(true); + toast.loading("Transferring..."); + try { + if (!account.address || !destination) return; + + const farmData: `0x${string}`[] = []; + + // Plot Transfers — fieldId=1 for Beanstalk Repayment Field + const fieldId = BigInt(1); + const ids: bigint[] = []; + const starts: bigint[] = []; + const ends: bigint[] = []; + for (const plotData of transferData) { + ids.push(plotData.id.toBigInt()); + starts.push(plotData.start.toBigInt()); + ends.push(plotData.end.toBigInt()); + } + const plotTransferCall = encodeFunctionData({ + abi: beanstalkAbi, + functionName: "transferPlots", + args: [account.address, destination as Address, fieldId, ids, starts, ends], + }); + farmData.push(plotTransferCall); + + return writeWithEstimateGas({ + address: beanstalkAddress[chainId as keyof typeof beanstalkAddress], + abi: beanstalkAbi, + functionName: "farm", + args: [farmData], + }); + } catch (e) { + console.error("Transfer beanstalk pods failed", e); + toast.dismiss(); + toast.error("Transfer failed"); + } + } + + return ( + + {step === 1 ? ( + + ) : ( + + )} + + ); +} diff --git a/src/pages/transfer/actions/TransferBeanstalkSilo.tsx b/src/pages/transfer/actions/TransferBeanstalkSilo.tsx new file mode 100644 index 000000000..3ae20257c --- /dev/null +++ b/src/pages/transfer/actions/TransferBeanstalkSilo.tsx @@ -0,0 +1,92 @@ +import { TokenValue } from "@/classes/TokenValue"; +import FlowForm from "@/components/FormFlow"; +import { abiSnippets } from "@/constants/abiSnippets"; +import { SILO_PAYBACK_ADDRESS } from "@/constants/address"; +import useTransaction from "@/hooks/useTransaction"; +import { useFarmerBeanstalkRepayment } from "@/state/useFarmerBeanstalkRepayment"; +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { toast } from "sonner"; +import { type Address } from "viem"; +import { useAccount } from "wagmi"; +import FinalStep from "./beanstalk-silo/FinalStep"; +import StepOne from "./beanstalk-silo/StepOne"; + +const URBDV_DECIMALS = 6; + +export default function TransferBeanstalkSilo() { + const account = useAccount(); + const navigate = useNavigate(); + + const [step, setStep] = useState(1); + const [destination, setDestination] = useState(); + const [amount, setAmount] = useState(""); + const [transferNotice, setTransferNotice] = useState(false); + + const repayment = useFarmerBeanstalkRepayment(); + + useEffect(() => { + setTransferNotice(false); + }, [destination]); + + const stepDescription = step === 1 ? "Specify amount and recipient address" : "Confirm send"; + + const { writeWithEstimateGas, setSubmitting } = useTransaction({ + successCallback: () => { + repayment.refetch(); + navigate("/transfer"); + }, + successMessage: "Transfer success", + errorMessage: "Transfer failed", + }); + + function onSubmit() { + try { + setSubmitting(true); + toast.loading("Transferring..."); + + if (!account.address || !destination || !amount) return; + + const parsedAmount = TokenValue.fromHuman(amount, URBDV_DECIMALS); + if (parsedAmount.eq(0)) return; + + // urBDV is an ERC20 token on SiloPayback contract — direct transfer to recipient wallet + return writeWithEstimateGas({ + address: SILO_PAYBACK_ADDRESS as Address, + abi: abiSnippets.siloPayback, + functionName: "transfer", + args: [destination as Address, parsedAmount.toBigInt()], + }); + } catch (e) { + console.error("Transfer Beanstalk Silo failed", e); + toast.dismiss(); + toast.error("Transfer failed"); + } + } + + const numericAmount = Number(amount) || 0; + + return ( + 0} + onSubmit={onSubmit} + stepDescription={stepDescription} + > + {step === 1 ? ( + + ) : ( + + )} + + ); +} diff --git a/src/pages/transfer/actions/TransferPods.tsx b/src/pages/transfer/actions/TransferPods.tsx index 541985559..ae8cfa530 100644 --- a/src/pages/transfer/actions/TransferPods.tsx +++ b/src/pages/transfer/actions/TransferPods.tsx @@ -10,7 +10,6 @@ import { type Address, encodeFunctionData } from "viem"; import { useAccount, useChainId } from "wagmi"; import FinalStep from "./pods/FinalStep"; import StepOne from "./pods/StepOne"; -import StepTwo from "./pods/StepTwo"; export interface PodTransferData { id: TokenValue; @@ -37,9 +36,7 @@ export default function TransferPods() { const stepDescription = () => { switch (step) { case 1: - return "Select Plots"; - case 2: - return "Specify amount and address"; + return "Select Pinto Plots"; default: return "Confirm send"; } @@ -48,15 +45,7 @@ export default function TransferPods() { const enableNextStep = () => { switch (step) { case 1: - return transferData.length > 0; - case 2: - if (!!destination && transferNotice) { - if (transferData.length === 1) { - return transferData[0].end.gt(transferData[0].start); - } - return true; - } - return false; + return transferData.length > 0 && !!destination && transferNotice; default: return true; } @@ -121,16 +110,14 @@ export default function TransferPods() { {step === 1 ? ( - - ) : step === 2 ? ( - ({ token, deposit })); const harvestableIndex = useHarvestableIndex(); + const repayment = useFarmerBeanstalkRepayment(); const hasBalance = balancesToSend .reduce((total, balanceToSend) => total.add(balanceToSend.balance.internal), TokenValue.ZERO) @@ -34,6 +35,17 @@ export default function FinalStep({ destination }: StepTwoProps) { const hasDeposits = depositsToSend.reduce((total, depositToSend) => total + depositToSend.deposit.deposits.length, 0) > 0; + const hasBeanstalkSilo = repayment.silo.balance.gt(0); + const hasBeanstalkPods = repayment.pods.plots.length > 0; + const totalBsFert = useMemo(() => { + let total = 0n; + for (const detail of repayment.fertilizer.perIdData.values()) { + total += detail.balance; + } + return total; + }, [repayment.fertilizer.perIdData]); + const hasBeanstalkFert = totalBsFert > 0n; + return (
@@ -71,6 +83,32 @@ export default function FinalStep({ destination }: StepTwoProps) {
)} + {hasBeanstalkSilo && ( +
+ +
+ {formatter.twoDec(repayment.silo.balance)} urBDV +
+
+ )} + {hasBeanstalkPods && ( +
+ +
+ {formatter.twoDec(repayment.pods.totalPods)} + Plot + Pods +
+
+ )} + {hasBeanstalkFert && ( +
+ +
+ {formatter.number(Number(totalBsFert))} bsFERT +
+
+ )}
diff --git a/src/pages/transfer/actions/beanstalk-fertilizer/FinalStep.tsx b/src/pages/transfer/actions/beanstalk-fertilizer/FinalStep.tsx new file mode 100644 index 000000000..2a3e41099 --- /dev/null +++ b/src/pages/transfer/actions/beanstalk-fertilizer/FinalStep.tsx @@ -0,0 +1,39 @@ +import AddressLink from "@/components/AddressLink"; +import { Label } from "@/components/ui/Label"; +import { formatter } from "@/utils/format"; +import { type FertilizerTransferItem } from "../TransferBeanstalkFertilizer"; + +interface FinalStepProps { + destination: string | undefined; + selectedIds: FertilizerTransferItem[]; +} + +export default function FinalStep({ destination, selectedIds }: FinalStepProps) { + if (!destination || selectedIds.length === 0) { + return null; + } + + return ( +
+
+ +
+ {selectedIds.map((item) => ( +
+ {formatter.number(Number(item.value))} + bsFERT + ID {formatter.number(Number(item.id))} +
+ ))} +
+
+
+ + +
+
+ ); +} diff --git a/src/pages/transfer/actions/beanstalk-fertilizer/StepOne.tsx b/src/pages/transfer/actions/beanstalk-fertilizer/StepOne.tsx new file mode 100644 index 000000000..4b607825e --- /dev/null +++ b/src/pages/transfer/actions/beanstalk-fertilizer/StepOne.tsx @@ -0,0 +1,229 @@ +import AddressInputField from "@/components/AddressInputField"; +import FertilizerCard from "@/components/FertilizerCard"; +import PintoAssetTransferNotice from "@/components/PintoAssetTransferNotice"; +import { Button } from "@/components/ui/Button"; +import { Label } from "@/components/ui/Label"; +import { useFarmerBeanstalkRepayment } from "@/state/useFarmerBeanstalkRepayment"; +import { formatter } from "@/utils/format"; +import { AnimatePresence, motion } from "framer-motion"; +import { Dispatch, SetStateAction, useCallback, useState } from "react"; +import { type FertilizerTransferItem } from "../TransferBeanstalkFertilizer"; + +interface StepOneProps { + selectedIds: FertilizerTransferItem[]; + setSelectedIds: Dispatch>; + destination: string | undefined; + setDestination: Dispatch>; + transferNotice: boolean; + setTransferNotice: Dispatch>; +} + +const variants = { + hidden: { + opacity: 0, + transition: { opacity: { duration: 0.2 } }, + }, + visible: { + opacity: 1, + transition: { opacity: { duration: 0.2 } }, + }, + exit: { + opacity: 0, + transition: { opacity: { duration: 0.2 } }, + }, +}; + +export default function StepOne({ + selectedIds, + setSelectedIds, + destination, + setDestination, + transferNotice, + setTransferNotice, +}: StepOneProps) { + const repayment = useFarmerBeanstalkRepayment(); + const fertilizerIds = repayment.fertilizer.fertilizerIds; + const perIdData = repayment.fertilizer.perIdData; + + // Get actual balance per fertilizer ID from on-chain data + const getFertilizerBalance = useCallback( + (fertId: bigint): bigint => { + return perIdData.get(fertId.toString())?.balance ?? 0n; + }, + [perIdData], + ); + + // Local state for amount inputs per fertilizer ID + const [amounts, setAmounts] = useState>(() => { + const initial: Record = {}; + for (const item of selectedIds) { + initial[item.id.toString()] = item.value.toString(); + } + return initial; + }); + + // Track which fertilizers are selected (have amount > 0) + const [selected, setSelected] = useState>(() => { + const initial = new Set(); + for (const item of selectedIds) { + if (item.value > 0n) { + initial.add(item.id.toString()); + } + } + return initial; + }); + + const toggleSelection = useCallback( + (fertId: bigint) => { + const idStr = fertId.toString(); + const balance = getFertilizerBalance(fertId); + setSelected((prev) => { + const newSet = new Set(prev); + if (newSet.has(idStr)) { + newSet.delete(idStr); + // Clear amount when deselecting + setAmounts((prevAmounts) => ({ ...prevAmounts, [idStr]: "" })); + setSelectedIds((prev) => prev.filter((item) => item.id !== fertId)); + } else { + newSet.add(idStr); + // Set default amount to full balance when selecting + const defaultAmount = balance > 0n ? balance : 1n; + setAmounts((prevAmounts) => ({ ...prevAmounts, [idStr]: defaultAmount.toString() })); + setSelectedIds((prev) => [...prev, { id: fertId, value: defaultAmount }]); + } + return newSet; + }); + }, + [setSelectedIds, getFertilizerBalance], + ); + + const handleAmountChange = useCallback( + (fertId: bigint, value: string, maxBalance: bigint) => { + const idStr = fertId.toString(); + + // Validate against max balance + let numValue = value === "" ? 0n : BigInt(Math.max(0, Math.floor(Number(value)))); + if (numValue > maxBalance) { + numValue = maxBalance; + } + + const displayValue = numValue === 0n ? "" : numValue.toString(); + setAmounts((prev) => ({ ...prev, [idStr]: displayValue })); + + // Update selectedIds with the new amount + setSelectedIds((prev) => { + const existing = prev.find((item) => item.id === fertId); + if (numValue === 0n) { + // Remove if amount is 0 + setSelected((prevSelected) => { + const newSet = new Set(prevSelected); + newSet.delete(idStr); + return newSet; + }); + return prev.filter((item) => item.id !== fertId); + } + // Auto-select if amount is entered + setSelected((prevSelected) => new Set(prevSelected).add(idStr)); + if (existing) { + // Update existing + return prev.map((item) => (item.id === fertId ? { ...item, value: numValue } : item)); + } + // Add new + return [...prev, { id: fertId, value: numValue }]; + }); + }, + [setSelectedIds], + ); + + const selectAll = useCallback(() => { + const newAmounts: Record = {}; + const newSelectedIds: FertilizerTransferItem[] = []; + const newSelected = new Set(); + + for (const fertId of fertilizerIds) { + const idStr = fertId.toString(); + const balance = getFertilizerBalance(fertId); + const amount = balance > 0n ? balance : 1n; + newAmounts[idStr] = amount.toString(); + newSelectedIds.push({ id: fertId, value: amount }); + newSelected.add(idStr); + } + + setAmounts(newAmounts); + setSelectedIds(newSelectedIds); + setSelected(newSelected); + }, [fertilizerIds, setSelectedIds, getFertilizerBalance]); + + if (fertilizerIds.length === 0) { + return ( +
+
No Beanstalk Repayment Fertilizer found.
+
+ ); + } + + return ( + +
+ +
+ + + +
+ {fertilizerIds.map((fertId) => { + const idStr = fertId.toString(); + const amount = amounts[idStr] || ""; + const isSelected = selected.has(idStr); + const detail = perIdData.get(idStr); + const maxBalance = detail?.balance ?? 0n; + // Format sprouts: raw value is balance * remainingBpf (no decimals on balance, 6 decimals on bpf) + const sproutsRaw = detail?.sprouts ?? 0n; + const sprouts = formatter.number(Number(sproutsRaw) / 1e6); + const humidity = detail?.humidity !== undefined ? `${formatter.number(detail.humidity)}%` : "—"; + + return ( + + ); + })} +
+
+ + + + + + {destination && ( + + + + )} + + +
+ ); +} diff --git a/src/pages/transfer/actions/beanstalk-pods/FinalStep.tsx b/src/pages/transfer/actions/beanstalk-pods/FinalStep.tsx new file mode 100644 index 000000000..4136bdd72 --- /dev/null +++ b/src/pages/transfer/actions/beanstalk-pods/FinalStep.tsx @@ -0,0 +1,62 @@ +import podIcon from "@/assets/protocol/Pod.png"; +import AddressLink from "@/components/AddressLink"; +import { Label } from "@/components/ui/Label"; +import { useFarmerBeanstalkRepayment } from "@/state/useFarmerBeanstalkRepayment"; +import { formatter } from "@/utils/format"; +import { computeSummaryRange } from "@/utils/podTransferUtils"; +import { useMemo } from "react"; +import { PodTransferData } from "../TransferBeanstalkPods"; + +interface FinalStepProps { + destination: string | undefined; + transferData: PodTransferData[]; +} + +export default function FinalStep({ destination, transferData }: FinalStepProps) { + const harvestableIndex = useFarmerBeanstalkRepayment().pods.harvestableIndex; + + const summary = useMemo(() => { + if (transferData.length === 0) return null; + return computeSummaryRange(transferData, harvestableIndex); + }, [transferData, harvestableIndex]); + + if (!destination || !summary) { + return null; + } + + const { totalPods, placeInLineStart, placeInLineEnd } = summary; + const isSinglePlot = transferData.length === 1; + + return ( +
+
+ +
+
+
+ {formatter.number(totalPods)} + Plot + Pods +
+
+ {isSinglePlot ? ( + <> + @ + {formatter.number(placeInLineStart)} in Line + + ) : ( + + between {formatter.number(placeInLineStart)} - {formatter.number(placeInLineEnd)} in Line + + )} +
+
+
+
+
+ + +
+
+ ); +} diff --git a/src/pages/transfer/actions/beanstalk-pods/StepOne.tsx b/src/pages/transfer/actions/beanstalk-pods/StepOne.tsx new file mode 100644 index 000000000..bbc11af11 --- /dev/null +++ b/src/pages/transfer/actions/beanstalk-pods/StepOne.tsx @@ -0,0 +1,220 @@ +import AddressInputField from "@/components/AddressInputField"; +import PintoAssetTransferNotice from "@/components/PintoAssetTransferNotice"; +import PodLineGraph from "@/components/PodLineGraph"; +import { Label } from "@/components/ui/Label"; +import { MultiSlider } from "@/components/ui/Slider"; +import { useFarmerBeanstalkRepayment } from "@/state/useFarmerBeanstalkRepayment"; +import { formatter } from "@/utils/format"; +import { computeTransferData, offsetToAbsoluteIndex } from "@/utils/podTransferUtils"; +import { Plot } from "@/utils/types"; +import { AnimatePresence, motion } from "framer-motion"; +import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { PodTransferData } from "../TransferBeanstalkPods"; + +interface StepOneProps { + transferData: PodTransferData[]; + setTransferData: Dispatch>; + destination: string | undefined; + setDestination: Dispatch>; + transferNotice: boolean; + setTransferNotice: Dispatch>; +} + +function sortPlotsByIndex(plots: Plot[]): Plot[] { + return [...plots].sort((a, b) => a.index.sub(b.index).toNumber()); +} + +export default function StepOne({ + transferData, + setTransferData, + destination, + setDestination, + transferNotice, + setTransferNotice, +}: StepOneProps) { + const repaymentPods = useFarmerBeanstalkRepayment().pods; + const { plots, harvestableIndex, podIndex } = repaymentPods; + + const [selectedPlots, setSelectedPlots] = useState([]); + const [podRange, setPodRange] = useState<[number, number]>([0, 0]); + + const mountedRef = useRef(false); + + // Restore selection from existing transferData on mount + useEffect(() => { + if (mountedRef.current) return; + mountedRef.current = true; + if (transferData.length === 0) return; + const restoredPlots = transferData + .map((data) => plots.find((p) => p.index.eq(data.id))) + .filter((p): p is Plot => p !== undefined); + if (restoredPlots.length > 0) { + const sorted = sortPlotsByIndex(restoredPlots); + setSelectedPlots(sorted); + const total = sorted.reduce((sum, p) => sum + p.pods.toNumber(), 0); + setPodRange([0, total]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Total pods across selected plots + const totalPods = useMemo(() => { + return selectedPlots.reduce((sum, p) => sum + p.pods.toNumber(), 0); + }, [selectedPlots]); + + const amount = podRange[1] - podRange[0]; + + const selectedPlotIndices = useMemo(() => selectedPlots.map((p) => p.index.toHuman()), [selectedPlots]); + + const positionInfo = useMemo(() => { + if (selectedPlots.length === 0) return null; + const first = selectedPlots[0]; + const last = selectedPlots[selectedPlots.length - 1]; + return { + start: first.index.sub(harvestableIndex), + end: last.index.add(last.pods).sub(harvestableIndex), + }; + }, [selectedPlots, harvestableIndex]); + + const selectedPodRange = useMemo(() => { + if (selectedPlots.length === 0) return undefined; + return { + start: offsetToAbsoluteIndex(podRange[0], selectedPlots), + end: offsetToAbsoluteIndex(podRange[1], selectedPlots), + }; + }, [selectedPlots, podRange]); + + const handlePlotSelection = useCallback( + (newPlots: Plot[]) => { + const sorted = sortPlotsByIndex(newPlots); + setSelectedPlots(sorted); + + if (sorted.length > 0) { + const newTotal = sorted.reduce((sum, p) => sum + p.pods.toNumber(), 0); + setPodRange([0, newTotal]); + setTransferData(computeTransferData(sorted, [0, newTotal])); + } else { + setPodRange([0, 0]); + setTransferData([]); + } + }, + [setTransferData], + ); + + const handlePlotGroupSelect = useCallback( + (plotIndices: string[]) => { + const groupSet = new Set(plotIndices); + const plotsInGroup = plots.filter((p) => groupSet.has(p.index.toHuman())); + if (plotsInGroup.length === 0) return; + + const selectedSet = new Set(selectedPlots.map((p) => p.index.toHuman())); + const allSelected = plotIndices.every((idx) => selectedSet.has(idx)); + + if (allSelected) { + handlePlotSelection(selectedPlots.filter((p) => !groupSet.has(p.index.toHuman()))); + return; + } + + const newPlots = [...selectedPlots]; + for (const plotToAdd of plotsInGroup) { + if (!selectedSet.has(plotToAdd.index.toHuman())) { + newPlots.push(plotToAdd); + } + } + handlePlotSelection(newPlots); + }, + [plots, selectedPlots, handlePlotSelection], + ); + + const handlePodRangeChange = useCallback( + (value: number[]) => { + const newRange: [number, number] = [value[0], value[1]]; + setPodRange(newRange); + setTransferData(computeTransferData(selectedPlots, newRange)); + }, + [selectedPlots, setTransferData], + ); + + return ( +
+
+ + + + {positionInfo && ( +
+

+ {positionInfo.start.toHuman("short")} - {positionInfo.end.toHuman("short")} +

+
+ )} +
+ + {totalPods > 0 && ( +
+

Total Pods to send:

+

{formatter.noDec(amount)} Pods

+
+ )} + + {selectedPlots.length > 0 && ( +
+
+

Select Pods

+
+

{formatter.noDec(podRange[0])}

+
+ {totalPods > 0 && ( + + )} +
+

{formatter.noDec(podRange[1])}

+
+
+
+ )} + + + + + + {destination && ( + + + + )} + + +
+ ); +} diff --git a/src/pages/transfer/actions/beanstalk-pods/StepTwo.tsx b/src/pages/transfer/actions/beanstalk-pods/StepTwo.tsx new file mode 100644 index 000000000..86237f983 --- /dev/null +++ b/src/pages/transfer/actions/beanstalk-pods/StepTwo.tsx @@ -0,0 +1,39 @@ +import AddressInputField from "@/components/AddressInputField"; +import PintoAssetTransferNotice from "@/components/PintoAssetTransferNotice"; +import { Label } from "@/components/ui/Label"; +import { AnimatePresence, motion } from "framer-motion"; +import { Dispatch, SetStateAction } from "react"; + +interface StepTwoProps { + destination: string | undefined; + setDestination: Dispatch>; + transferNotice: boolean; + setTransferNotice: Dispatch>; +} + +export default function StepTwo({ destination, setDestination, transferNotice, setTransferNotice }: StepTwoProps) { + return ( +
+
+ + + + {destination && ( + + + + )} + +
+
+ ); +} diff --git a/src/pages/transfer/actions/beanstalk-silo/FinalStep.tsx b/src/pages/transfer/actions/beanstalk-silo/FinalStep.tsx new file mode 100644 index 000000000..83d2a8703 --- /dev/null +++ b/src/pages/transfer/actions/beanstalk-silo/FinalStep.tsx @@ -0,0 +1,25 @@ +import AddressLink from "@/components/AddressLink"; +import { Label } from "@/components/ui/Label"; + +interface FinalStepProps { + amount: string; + destination: string | undefined; +} + +export default function FinalStep({ amount, destination }: FinalStepProps) { + return ( +
+
+ +
+ {amount} + urBDV +
+
+
+ + +
+
+ ); +} diff --git a/src/pages/transfer/actions/beanstalk-silo/StepOne.tsx b/src/pages/transfer/actions/beanstalk-silo/StepOne.tsx new file mode 100644 index 000000000..64219a347 --- /dev/null +++ b/src/pages/transfer/actions/beanstalk-silo/StepOne.tsx @@ -0,0 +1,81 @@ +import AddressInputField from "@/components/AddressInputField"; +import { ComboInputField } from "@/components/ComboInputField"; +import PintoAssetTransferNotice from "@/components/PintoAssetTransferNotice"; +import { Label } from "@/components/ui/Label"; +import { useFarmerBeanstalkRepayment } from "@/state/useFarmerBeanstalkRepayment"; +import { AnimatePresence, motion } from "framer-motion"; +import { Dispatch, SetStateAction } from "react"; + +interface StepOneProps { + amount: string; + setAmount: Dispatch>; + destination: string | undefined; + setDestination: Dispatch>; + transferNotice: boolean; + setTransferNotice: Dispatch>; +} + +const variants = { + hidden: { + opacity: 0, + transition: { opacity: { duration: 0.2 } }, + }, + visible: { + opacity: 1, + transition: { opacity: { duration: 0.2 } }, + }, + exit: { + opacity: 0, + transition: { opacity: { duration: 0.2 } }, + }, +}; + +export default function StepOne({ + amount, + setAmount, + destination, + setDestination, + transferNotice, + setTransferNotice, +}: StepOneProps) { + const repayment = useFarmerBeanstalkRepayment(); + const maxAmount = repayment.silo.balance; + + return ( + + + + + + + + + + + {destination && ( + + + + )} + + + + ); +} diff --git a/src/pages/transfer/actions/pods/FinalStep.tsx b/src/pages/transfer/actions/pods/FinalStep.tsx index e6a18bb51..6a25d4149 100644 --- a/src/pages/transfer/actions/pods/FinalStep.tsx +++ b/src/pages/transfer/actions/pods/FinalStep.tsx @@ -3,6 +3,8 @@ import AddressLink from "@/components/AddressLink"; import { Label } from "@/components/ui/Label"; import { useHarvestableIndex } from "@/state/useFieldData"; import { formatter } from "@/utils/format"; +import { computeSummaryRange } from "@/utils/podTransferUtils"; +import { useMemo } from "react"; import { PodTransferData } from "../TransferPods"; interface FinalStepProps { @@ -13,36 +15,42 @@ interface FinalStepProps { export default function FinalStep({ destination, transferData }: FinalStepProps) { const harvestableIndex = useHarvestableIndex(); - if (!destination || transferData.length === 0) { + const summary = useMemo(() => { + if (transferData.length === 0) return null; + return computeSummaryRange(transferData, harvestableIndex); + }, [transferData, harvestableIndex]); + + if (!destination || !summary) { return null; } + const { totalPods, placeInLineStart, placeInLineEnd } = summary; + const isSinglePlot = transferData.length === 1; + return (
- {transferData.map((transfer) => { - const placeInLine = transfer.id.sub(harvestableIndex); - const podAmount = transfer.end.sub(transfer.start); - - return ( -
-
- {formatter.number(podAmount)} - Plot - Pods -
-
- @ - {formatter.number(placeInLine.add(transfer.start))} in Line -
-
- ); - })} +
+
+ {formatter.number(totalPods)} + Plot + Pods +
+
+ {isSinglePlot ? ( + <> + @ + {formatter.number(placeInLineStart)} in Line + + ) : ( + + between {formatter.number(placeInLineStart)} - {formatter.number(placeInLineEnd)} in Line + + )} +
+
diff --git a/src/pages/transfer/actions/pods/StepOne.tsx b/src/pages/transfer/actions/pods/StepOne.tsx index 5e08ea3c0..170413832 100644 --- a/src/pages/transfer/actions/pods/StepOne.tsx +++ b/src/pages/transfer/actions/pods/StepOne.tsx @@ -1,93 +1,230 @@ -import { TokenValue } from "@/classes/TokenValue"; -import PlotsTable from "@/components/PlotsTable"; -import { Button } from "@/components/ui/Button"; +import AddressInputField from "@/components/AddressInputField"; +import PintoAssetTransferNotice from "@/components/PintoAssetTransferNotice"; +import PodLineGraph from "@/components/PodLineGraph"; import { Label } from "@/components/ui/Label"; -import { ToggleGroup } from "@/components/ui/ToggleGroup"; +import { MultiSlider } from "@/components/ui/Slider"; import { useFarmerField } from "@/state/useFarmerField"; +import { useHarvestableIndex } from "@/state/useFieldData"; +import { formatter } from "@/utils/format"; +import { computeTransferData, offsetToAbsoluteIndex } from "@/utils/podTransferUtils"; import { Plot } from "@/utils/types"; -import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { PodTransferData } from "../TransferPods"; interface StepOneProps { transferData: PodTransferData[]; setTransferData: Dispatch>; + destination: string | undefined; + setDestination: Dispatch>; + transferNotice: boolean; + setTransferNotice: Dispatch>; } -export default function StepOne({ transferData, setTransferData }: StepOneProps) { - const [selected, setSelected] = useState(); +function sortPlotsByIndex(plots: Plot[]): Plot[] { + return [...plots].sort((a, b) => a.index.sub(b.index).toNumber()); +} + +export default function StepOne({ + transferData, + setTransferData, + destination, + setDestination, + transferNotice, + setTransferNotice, +}: StepOneProps) { const { plots } = useFarmerField(); + const harvestableIndex = useHarvestableIndex(); + + const [selectedPlots, setSelectedPlots] = useState([]); + const [podRange, setPodRange] = useState<[number, number]>([0, 0]); + + const mountedRef = useRef(false); + // Restore selection from existing transferData on mount useEffect(() => { - const _newPlots: string[] = []; - for (const data of transferData) { - const _plot = plots.find((plot) => plot.index.eq(data.id)); - if (_plot) { - _newPlots.push(_plot.index.toHuman()); - } + if (mountedRef.current) return; + mountedRef.current = true; + if (transferData.length === 0) return; + const restoredPlots = transferData + .map((data) => plots.find((p) => p.index.eq(data.id))) + .filter((p): p is Plot => p !== undefined); + if (restoredPlots.length > 0) { + const sorted = sortPlotsByIndex(restoredPlots); + setSelectedPlots(sorted); + const total = sorted.reduce((sum, p) => sum + p.pods.toNumber(), 0); + setPodRange([0, total]); } - setSelected(_newPlots); + // Only run on mount + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // Total pods across selected plots + const totalPods = useMemo(() => { + return selectedPlots.reduce((sum, p) => sum + p.pods.toNumber(), 0); + }, [selectedPlots]); + + // Derived amount from slider range — no separate state needed + const amount = podRange[1] - podRange[0]; + + // Memoize selectedPlotIndices to avoid new array ref each render + const selectedPlotIndices = useMemo(() => selectedPlots.map((p) => p.index.toHuman()), [selectedPlots]); + + // Position info — plots are already sorted, use first/last directly + const positionInfo = useMemo(() => { + if (selectedPlots.length === 0) return null; + const first = selectedPlots[0]; + const last = selectedPlots[selectedPlots.length - 1]; + return { + start: first.index.sub(harvestableIndex), + end: last.index.add(last.pods).sub(harvestableIndex), + }; + }, [selectedPlots, harvestableIndex]); + + // Compute selectedPodRange for PodLineGraph (absolute indices) + const selectedPodRange = useMemo(() => { + if (selectedPlots.length === 0) return undefined; + return { + start: offsetToAbsoluteIndex(podRange[0], selectedPlots), + end: offsetToAbsoluteIndex(podRange[1], selectedPlots), + }; + }, [selectedPlots, podRange]); + + // Handle plot selection changes: sort, reset slider, update transferData const handlePlotSelection = useCallback( - (value: string[]) => { - // Update selected plots - setSelected(value); - - // Get selected plots data - const selectedPlots = value - .map((plotIndex) => { - const plot = plots.find((p) => p.index.toHuman() === plotIndex); - return plot; - }) - .filter((plot): plot is Plot => plot !== undefined && !plot.fullyHarvested); - - // If no valid plots selected, clear transfer data - if (selectedPlots.length === 0) { + (newPlots: Plot[]) => { + const sorted = sortPlotsByIndex(newPlots); + setSelectedPlots(sorted); + + if (sorted.length > 0) { + const newTotal = sorted.reduce((sum, p) => sum + p.pods.toNumber(), 0); + setPodRange([0, newTotal]); + setTransferData(computeTransferData(sorted, [0, newTotal])); + } else { + setPodRange([0, 0]); setTransferData([]); + } + }, + [setTransferData], + ); + + // Toggle logic: if all in group selected → deselect, else add + const handlePlotGroupSelect = useCallback( + (plotIndices: string[]) => { + const groupSet = new Set(plotIndices); + const plotsInGroup = plots.filter((p) => groupSet.has(p.index.toHuman())); + if (plotsInGroup.length === 0) return; + + const selectedSet = new Set(selectedPlots.map((p) => p.index.toHuman())); + const allSelected = plotIndices.every((idx) => selectedSet.has(idx)); + + if (allSelected) { + handlePlotSelection(selectedPlots.filter((p) => !groupSet.has(p.index.toHuman()))); return; } - // Create plot transfer data - const transferData = selectedPlots.map((plot) => { - return { - id: plot.index, - start: TokenValue.ZERO, - end: plot.pods, - }; - }); - - // Update transfer data - setTransferData(transferData); + const newPlots = [...selectedPlots]; + for (const plotToAdd of plotsInGroup) { + if (!selectedSet.has(plotToAdd.index.toHuman())) { + newPlots.push(plotToAdd); + } + } + handlePlotSelection(newPlots); }, - [plots, setTransferData], + [plots, selectedPlots, handlePlotSelection], ); - const selectAllPlots = useCallback(() => { - const plotIndexes = plots.map((plot) => plot.index.toHuman()); - handlePlotSelection(plotIndexes); - }, [plots, handlePlotSelection]); + // Slider change handler + const handlePodRangeChange = useCallback( + (value: number[]) => { + const newRange: [number, number] = [value[0], value[1]]; + setPodRange(newRange); + setTransferData(computeTransferData(selectedPlots, newRange)); + }, + [selectedPlots, setTransferData], + ); return ( - <> -
- -
-
- - - - +
+ {/* Pod Line Graph Visualization */} +
+ + + + {/* Position in Line Display */} + {positionInfo && ( +
+

+ {positionInfo.start.toHuman("short")} - {positionInfo.end.toHuman("short")} +

+
+ )}
- + + {/* Total Pods Summary */} + {totalPods > 0 && ( +
+

Total Pods to send:

+

{formatter.noDec(amount)} Pods

+
+ )} + + {/* MultiSlider for pod range selection */} + {selectedPlots.length > 0 && ( +
+
+

Select Pods

+
+

{formatter.noDec(podRange[0])}

+
+ {totalPods > 0 && ( + + )} +
+

{formatter.noDec(podRange[1])}

+
+
+
+ )} + + + + + + {destination && ( + + + + )} + + +
); } diff --git a/src/pages/transfer/actions/pods/StepTwo.tsx b/src/pages/transfer/actions/pods/StepTwo.tsx index 1f8c07d78..86237f983 100644 --- a/src/pages/transfer/actions/pods/StepTwo.tsx +++ b/src/pages/transfer/actions/pods/StepTwo.tsx @@ -1,135 +1,19 @@ -import { TokenValue } from "@/classes/TokenValue"; import AddressInputField from "@/components/AddressInputField"; -import { ComboInputField } from "@/components/ComboInputField"; import PintoAssetTransferNotice from "@/components/PintoAssetTransferNotice"; -import PodRangeSelector from "@/components/PodRangeSelector"; import { Label } from "@/components/ui/Label"; -import { PODS } from "@/constants/internalTokens"; -import { useFarmerField } from "@/state/useFarmerField"; -import { Plot } from "@/utils/types"; import { AnimatePresence, motion } from "framer-motion"; -import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; -import { PodTransferData } from "../TransferPods"; +import { Dispatch, SetStateAction } from "react"; interface StepTwoProps { - transferData: PodTransferData[]; - setTransferData: Dispatch>; destination: string | undefined; setDestination: Dispatch>; transferNotice: boolean; setTransferNotice: Dispatch>; } -export default function StepTwo({ - transferData, - setTransferData, - destination, - setDestination, - transferNotice, - setTransferNotice, -}: StepTwoProps) { - const { plots } = useFarmerField(); - const [selectedPlots, setSelectedPlots] = useState([]); - const [amount, setAmount] = useState("0"); - const [range, setRange] = useState<[TokenValue, TokenValue]>([TokenValue.ZERO, TokenValue.ZERO]); - - useEffect(() => { - const _newPlots: Plot[] = []; - for (const data of transferData) { - const _plot = plots.find((plot) => plot.index.eq(data.id)); - if (_plot) { - _newPlots.push(_plot); - } - } - setSelectedPlots(_newPlots); - }, []); - - useEffect(() => { - if (selectedPlots.length === 1) { - const plot = selectedPlots[0]; - setRange([plot.index, plot.index.add(plot.pods)]); - } - }, [selectedPlots]); - - const handleRangeChange = useCallback( - (newRange: TokenValue[]) => { - if (selectedPlots.length === 0 || selectedPlots.length > 1) return; - const plot = selectedPlots[0]; - const newStart = newRange[0]; - const newEnd = newRange[1]; - - const relativeStart = newStart.sub(plot.index); - const relativeEnd = newEnd.sub(plot.index); - - const newData = [ - { - id: plot.index, - start: relativeStart, - end: relativeEnd, - }, - ]; - - const newAmount = newEnd.sub(newStart).toHuman(); - - const batchUpdate = () => { - setRange([newStart, newEnd]); - setTransferData(newData); - setAmount(newAmount); - }; - batchUpdate(); - }, - [selectedPlots, setTransferData], - ); - - const handleAmountChange = useCallback( - (value: string) => { - const newAmount = value; - - if (selectedPlots.length === 0) return; - - const plot = selectedPlots[0]; - const amountValue = TokenValue.fromHuman(newAmount || "0", PODS.decimals); - const newEnd = plot.index.add(amountValue); - - const newStart = plot.index; - const relativeStart = newStart.sub(plot.index); - const relativeEnd = newEnd.sub(plot.index); - - const newData = [ - { - id: plot.index, - start: relativeStart, - end: relativeEnd, - }, - ]; - - const batchUpdate = () => { - setAmount(newAmount); - if (selectedPlots.length === 1) { - setRange([newStart, newEnd]); - setTransferData(newData); - } - }; - batchUpdate(); - }, - [selectedPlots, setTransferData], - ); - +export default function StepTwo({ destination, setDestination, transferNotice, setTransferNotice }: StepTwoProps) { return (
-
- - 1} - altText={selectedPlots.length > 1 ? "Balance:" : "Plot Balance:"} - /> -
@@ -148,11 +32,8 @@ export default function StepTwo({ /> )} - {" "} +
- {selectedPlots.length === 1 && ( - - )}
); } diff --git a/src/queries/beanstalk/podmarket/AllMarketActivity.graphql b/src/queries/beanstalk/podmarket/AllMarketActivity.graphql index cd5672688..0c14f2a16 100644 --- a/src/queries/beanstalk/podmarket/AllMarketActivity.graphql +++ b/src/queries/beanstalk/podmarket/AllMarketActivity.graphql @@ -7,10 +7,16 @@ query AllMarketActivity( $listings_createdAt_gt: BigInt $orders_createdAt_gt: BigInt $fill_createdAt_gt: BigInt + $listings_podMarketplace: String + $orders_podMarketplace: String ) { podListings( first: $first - where: { createdAt_gt: $listings_createdAt_gt, status_not: FILLED_PARTIAL } + where: { + createdAt_gt: $listings_createdAt_gt + status_not: FILLED_PARTIAL + podMarketplace: $listings_podMarketplace + } ) { ...PodListing } @@ -19,7 +25,10 @@ query AllMarketActivity( first: $first orderBy: createdAt orderDirection: desc - where: { createdAt_gt: $orders_createdAt_gt } + where: { + createdAt_gt: $orders_createdAt_gt + podMarketplace: $orders_podMarketplace + } ) { ...PodOrder } diff --git a/src/queries/beanstalk/podmarket/AllPodListings.graphql b/src/queries/beanstalk/podmarket/AllPodListings.graphql index bb2884cb3..32898b9c4 100644 --- a/src/queries/beanstalk/podmarket/AllPodListings.graphql +++ b/src/queries/beanstalk/podmarket/AllPodListings.graphql @@ -5,6 +5,7 @@ query AllPodListings( $status: MarketStatus = ACTIVE $maxHarvestableIndex: BigInt! $skip: Int = 0 + $podMarketplace: String ) { podListings( first: $first @@ -13,6 +14,7 @@ query AllPodListings( status: $status maxHarvestableIndex_gt: $maxHarvestableIndex remainingAmount_gt: "100000" # = 0.10 Pods. hides dust pods. + podMarketplace: $podMarketplace } orderBy: index # index of the listed plot orderDirection: asc # start from earliest listings diff --git a/src/queries/beanstalk/podmarket/AllPodOrders.graphql b/src/queries/beanstalk/podmarket/AllPodOrders.graphql index 24e785528..6eb7f22ef 100644 --- a/src/queries/beanstalk/podmarket/AllPodOrders.graphql +++ b/src/queries/beanstalk/podmarket/AllPodOrders.graphql @@ -4,13 +4,14 @@ query AllPodOrders( $first: Int = 1000 $status: MarketStatus = ACTIVE $skip: Int = 0 + $podMarketplace: String ) { podOrders( first: $first skip: $skip orderBy: createdAt orderDirection: desc - where: { status: $status } + where: { status: $status, podMarketplace: $podMarketplace } ) { ...PodOrder } diff --git a/src/state/market/useAllMarket.ts b/src/state/market/useAllMarket.ts index f09bab3f0..8499c9c96 100644 --- a/src/state/market/useAllMarket.ts +++ b/src/state/market/useAllMarket.ts @@ -2,25 +2,30 @@ import { subgraphs } from "@/constants/subgraph"; import { AllMarketActivityDocument } from "@/generated/gql/pintostalk/graphql"; import { useQuery } from "@tanstack/react-query"; import request from "graphql-request"; +import { useMemo } from "react"; import { useChainId } from "wagmi"; import { useQueryKeys } from "../useQueryKeys"; import { useMarketEntities } from "./useMarketEntities"; -export function useAllMarket() { +export function useAllMarket(podMarketplaceId?: string) { const chainId = useChainId(); const { allMarket: queryKey } = useQueryKeys({ chainId }); + const marketQueryKey = useMemo(() => [...queryKey, { podMarketplaceId }], [queryKey, podMarketplaceId]); + const { data, isFetching } = useQuery({ - queryKey, + queryKey: marketQueryKey, queryFn: async () => request(subgraphs[chainId].beanstalk, AllMarketActivityDocument, { listings_createdAt_gt: 0, orders_createdAt_gt: 0, fill_createdAt_gt: 0, first: 1000, + listings_podMarketplace: podMarketplaceId ?? "0", + orders_podMarketplace: podMarketplaceId ?? "0", }), }); - return useMarketEntities(data, isFetching, queryKey); + return useMarketEntities(data, isFetching, marketQueryKey); } diff --git a/src/state/market/usePodListings.ts b/src/state/market/usePodListings.ts index 8c0c7d5a0..97b52a865 100644 --- a/src/state/market/usePodListings.ts +++ b/src/state/market/usePodListings.ts @@ -10,19 +10,22 @@ import { useChainId } from "wagmi"; import { useHarvestableIndex } from "../useFieldData"; import { useQueryKeys } from "../useQueryKeys"; -export default function usePodListings() { +export default function usePodListings(podMarketplaceId?: string) { const chainId = useChainId(); const harvestableIndex = useHarvestableIndex(); const { allPodListings: queryKey } = useQueryKeys({ chainId, harvestableIndex }); const podListings = useQuery({ - queryKey: queryKey, - queryFn: async () => - request(subgraphs[chainId].beanstalk, AllPodListingsDocument, { + queryKey: [...queryKey, { podMarketplaceId }], + queryFn: async () => { + const variables: { maxHarvestableIndex: string; skip: number; podMarketplace?: string } = { maxHarvestableIndex: harvestableIndex.toBigInt().toString(), skip: 0, - }), + }; + if (podMarketplaceId) variables.podMarketplace = podMarketplaceId; + return request(subgraphs[chainId].beanstalk, AllPodListingsDocument, variables); + }, enabled: harvestableIndex.gt(0), }); diff --git a/src/state/market/usePodOrders.ts b/src/state/market/usePodOrders.ts index c874da161..1db4ec904 100644 --- a/src/state/market/usePodOrders.ts +++ b/src/state/market/usePodOrders.ts @@ -5,17 +5,18 @@ import request from "graphql-request"; import { useChainId } from "wagmi"; import { useQueryKeys } from "../useQueryKeys"; -export default function usePodOrders() { +export default function usePodOrders(podMarketplaceId?: string) { const chainId = useChainId(); const { allPodOrders: queryKey } = useQueryKeys({ chainId }); const podOrders = useQuery({ - queryKey: queryKey, - queryFn: async () => - request(subgraphs[chainId].beanstalk, AllPodOrdersDocument, { - skip: 0, - }), + queryKey: [...queryKey, { podMarketplaceId }], + queryFn: async () => { + const variables: { skip: number; podMarketplace?: string } = { skip: 0 }; + if (podMarketplaceId) variables.podMarketplace = podMarketplaceId; + return request(subgraphs[chainId].beanstalk, AllPodOrdersDocument, variables); + }, }); return { diff --git a/src/state/queryKeys.ts b/src/state/queryKeys.ts index 85846e6b2..72eaa78e9 100644 --- a/src/state/queryKeys.ts +++ b/src/state/queryKeys.ts @@ -87,6 +87,12 @@ const tractorQueryKeys = { publisher ?? "no-publisher", lastUpdatedBlock?.toString() ?? "0", ], + automateClaimOrders: (address?: string, chainId?: number) => [ + BASE_QKS.tractor, + "automateClaimOrders", + address ?? "no-address", + chainId?.toString() ?? "0", + ], ...convertUpQK, } as const; diff --git a/src/state/tractor/useTractorAutomateClaimOrders.ts b/src/state/tractor/useTractorAutomateClaimOrders.ts new file mode 100644 index 000000000..fe87f6c75 --- /dev/null +++ b/src/state/tractor/useTractorAutomateClaimOrders.ts @@ -0,0 +1,93 @@ +import { defaultQuerySettingsMedium } from "@/constants/query"; +import { useProtocolAddress } from "@/hooks/pinto/useProtocolAddress"; +import { TractorRequisitionData, TractorRequisitionEvent } from "@/lib/Tractor"; +import { decodeBlueprintCallData } from "@/lib/Tractor/blueprint-decoders"; +import { decodeAutomateClaimBlueprint } from "@/lib/Tractor/claimOrder"; +import { AutomateClaimBlueprintStruct } from "@/lib/Tractor/claimOrder/tractor-claim-types"; +import { TRACTOR_DEPLOYMENT_BLOCK } from "@/lib/Tractor/core"; +import { fetchTractorEvents } from "@/lib/Tractor/events/tractor-events"; +import { queryKeys } from "@/state/queryKeys"; +import { stringEq } from "@/utils/string"; +import { useQuery } from "@tanstack/react-query"; +import { useCallback } from "react"; +import { useChainId, usePublicClient } from "wagmi"; + +interface UseTractorAutomateClaimOrderbookOptions { + address?: `0x${string}`; + enabled?: boolean; +} + +export function useTractorAutomateClaimOrderbook({ + address, + enabled = true, +}: UseTractorAutomateClaimOrderbookOptions = {}) { + const chainId = useChainId(); + const client = usePublicClient({ chainId }); + const diamond = useProtocolAddress(); + + const query = useQuery({ + queryKey: queryKeys.tractor.automateClaimOrders(address, chainId), + queryFn: async (): Promise[]> => { + if (!client || !diamond) return []; + + const { publishEvents, cancelledHashes } = await fetchTractorEvents(client, diamond, TRACTOR_DEPLOYMENT_BLOCK); + + // Get latest block for timestamp approximation + const latestBlock = await client.getBlock({ blockTag: "latest" }); + const latestTimestamp = Number(latestBlock.timestamp); + const latestBlockNumber = Number(latestBlock.number); + + const automateClaimOrders: TractorRequisitionEvent[] = []; + + for (const event of publishEvents) { + const requisition = event.args?.requisition as TractorRequisitionData; + if (!requisition?.blueprint || !requisition?.blueprintHash || !requisition?.signature) { + continue; + } + + // Filter by publisher if address is provided + if (address && !stringEq(requisition.blueprint.publisher, address)) { + continue; + } + + const blueprintData = requisition.blueprint.data; + if (!blueprintData) continue; + + // Try direct decode first, fall back to blueprint-decoders system + let decodedData = decodeAutomateClaimBlueprint(blueprintData); + + if (!decodedData) { + const blueprintDecoded = decodeBlueprintCallData(blueprintData); + if (blueprintDecoded?.type === "automateClaim" && blueprintDecoded.params) { + decodedData = blueprintDecoded.params as AutomateClaimBlueprintStruct; + } + } + + if (!decodedData) continue; + + // Approximate timestamp from block number difference (~2s per block on Base) + const eventBlockNumber = Number(event.blockNumber ?? 0); + const timestamp = latestTimestamp * 1000 - (latestBlockNumber - eventBlockNumber) * 2000; + + automateClaimOrders.push({ + requisition, + blockNumber: eventBlockNumber, + timestamp, + isCancelled: cancelledHashes.has(requisition.blueprintHash), + requisitionType: "automateClaimBlueprint", + decodedData, + }); + } + + return automateClaimOrders; + }, + enabled: !!client && !!diamond && enabled, + ...defaultQuerySettingsMedium, + }); + + const refetch = useCallback(async () => { + return query.refetch(); + }, [query]); + + return { ...query, refetch } as const; +} diff --git a/src/state/useBeanstalkGlobalStats.ts b/src/state/useBeanstalkGlobalStats.ts new file mode 100644 index 000000000..922ea475a --- /dev/null +++ b/src/state/useBeanstalkGlobalStats.ts @@ -0,0 +1,141 @@ +import { TokenValue } from "@/classes/TokenValue"; +import { abiSnippets } from "@/constants/abiSnippets"; +import { BARN_PAYBACK_ADDRESS, SILO_PAYBACK_ADDRESS } from "@/constants/address"; +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 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; + +/** + * Interface for the global Beanstalk statistics data + */ +export interface BeanstalkGlobalStatsData { + totalUrBdvDistributed: TokenValue; + totalPodsInRepaymentField: TokenValue; + totalUnfertilizedSprouts: TokenValue; + totalPintoPaidOut: TokenValue; + siloRemaining: TokenValue; + barnRemaining: 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: + * - Silo Payback: siloRemaining() and totalDistributed() from Silo_Payback contract + * - Total pods in the repayment field (fieldId=1) + * - Barn Payback: barnRemaining() from Barn_Payback contract + * - Total Pinto paid out to holders (TODO: calculated total) + * + * 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], + }, + // Silo Payback global stats + { + address: SILO_PAYBACK_ADDRESS, + abi: abiSnippets.siloPayback, + functionName: "siloRemaining", + }, + { + address: SILO_PAYBACK_ADDRESS, + abi: abiSnippets.siloPayback, + functionName: "totalDistributed", + }, + // Barn Payback global stats + { + address: BARN_PAYBACK_ADDRESS, + abi: abiSnippets.barnPayback, + functionName: "barnRemaining", + }, + // Total Pinto received by Silo Payback contract + { + address: SILO_PAYBACK_ADDRESS, + abi: abiSnippets.siloPayback, + functionName: "totalReceived", + }, + ], + allowFailure: true, + query: { + ...defaultQuerySettingsMedium, // 5 minutes staleTime for global stats + }, + }); + + // Process global data — defaults to ZERO on error + const globalData = useMemo(() => { + if (globalQuery.isError) { + return { + totalUrBdvDistributed: TokenValue.ZERO, + totalPodsInRepaymentField: TokenValue.ZERO, + totalUnfertilizedSprouts: TokenValue.ZERO, + totalPintoPaidOut: TokenValue.ZERO, + siloRemaining: TokenValue.ZERO, + barnRemaining: TokenValue.ZERO, + }; + } + + const totalPodsInRepaymentField = globalQuery.data?.[0]?.result; + const siloRemainingResult = globalQuery.data?.[1]?.result; + const totalDistributedResult = globalQuery.data?.[2]?.result; + const barnRemainingResult = globalQuery.data?.[3]?.result; + const totalReceivedResult = globalQuery.data?.[4]?.result; + + return { + totalUrBdvDistributed: TokenValue.fromBlockchain(totalDistributedResult ?? 0n, URBDV.decimals), + totalPodsInRepaymentField: TokenValue.fromBlockchain(totalPodsInRepaymentField ?? 0n, PODS.decimals), + totalUnfertilizedSprouts: TokenValue.fromBlockchain(barnRemainingResult ?? 0n, SPROUTS.decimals), + // totalPintoPaidOut: Use totalReceived for now (will be > 0 once shipments start) + // Alternative: Could sum totalDistributed as a proxy for "issued" amount + totalPintoPaidOut: TokenValue.fromBlockchain(totalReceivedResult ?? 0n, URBDV.decimals), + siloRemaining: TokenValue.fromBlockchain(siloRemainingResult ?? 0n, URBDV.decimals), + barnRemaining: TokenValue.fromBlockchain(barnRemainingResult ?? 0n, SPROUTS.decimals), + }; + }, [globalQuery.data, globalQuery.isError]); + + // 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 000000000..9ef3ec359 --- /dev/null +++ b/src/state/useFarmerBeanstalkRepayment.ts @@ -0,0 +1,407 @@ +import { TokenValue } from "@/classes/TokenValue"; +import { abiSnippets } from "@/constants/abiSnippets"; +import { BARN_PAYBACK_ADDRESS, SILO_PAYBACK_ADDRESS, ZERO_ADDRESS } from "@/constants/address"; +import { BSFERT, PODS } from "@/constants/internalTokens"; +import { defaultQuerySettings } from "@/constants/query"; +import { PINTO } from "@/constants/tokens"; +import { beanstalkAbi } from "@/generated/contractHooks"; +import { useProtocolAddress } from "@/hooks/pinto/useProtocolAddress"; +import { useAllFertilizerIds } from "@/hooks/useAllFertilizerIds"; +import { Plot } from "@/utils/types"; +import { useCallback, useMemo } from "react"; +import { toHex } from "viem"; +import { useAccount, useReadContracts } from "wagmi"; + +/** + * Interface for silo payback data + */ +interface SiloPaybackData { + balance: TokenValue; // getBalanceCombined(account) - total urBDV balance + earned: TokenValue; // earned(account) - earned but unclaimed + totalDistributed: TokenValue; // totalDistributed() - total distributed + totalReceived: TokenValue; // totalReceived() - total received +} + +/** + * Interface for pods data from repayment field (fieldId=1) + */ +interface PodsData { + plots: Plot[]; + totalPods: TokenValue; + harvestableIndex: TokenValue; + podIndex: TokenValue; +} + +/** + * Per-ID fertilizer detail + */ +export interface FertilizerIdDetail { + balance: bigint; // bsFERT balance for this ID + sprouts: bigint; // unfertilized beans remaining (amount * max(0, id - currentBpf)) + humidity: number; // humidity percentage, e.g. 500 means 500% +} + +/** + * Interface for fertilizer data + */ +interface FertilizerData { + balance: TokenValue; // Total bsFERT balance + fertilized: TokenValue; // balanceOfFertilized(account, ids) + unfertilized: TokenValue; // balanceOfUnfertilized(account, ids) + fertilizerIds: bigint[]; // Fertilizer IDs owned by the user + perIdData: Map; // Per-ID balance, sprouts, humidity +} + +/** + * 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; + +/** + * Hook for fetching farmer-specific Beanstalk repayment data + * + * This hook fetches: + * - Silo payback data from Silo_Payback contract (earned, balance, totalDistributed, totalReceived) + * - Pods data from repayment field (fieldId=1) - from on-chain + * - Fertilizer data from Barn_Payback contract (fertilized, unfertilized balances) + * + * Fertilizer flow (4 phases): + * 1. useAllFertilizerIds() — get all global fertilizer IDs from linked list + * 2. multicall balanceOf(user, id) for each global ID + * 3. filter IDs where balance > 0 (userOwnedIds) + * 4. balanceOfFertilized + balanceOfUnfertilized for userOwnedIds + * + * @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 + }, + { + address: protocolAddress, + abi: [ + { + inputs: [{ name: "fieldId", type: "uint256" }], + name: "getHarvestableIndex", + outputs: [{ name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + ] as const, + functionName: "getHarvestableIndex", + args: [1n], // fieldId=1 for repayment field + }, + { + address: protocolAddress, + abi: beanstalkAbi, + functionName: "podIndex", + args: [1n], // fieldId=1 for repayment field + }, + ], + allowFailure: true, + query: { + // Always fetch - will use ZERO_ADDRESS if wallet not connected + ...defaultQuerySettings, // 20 minutes staleTime + }, + }); + + // --- Phase 1: Get all global fertilizer IDs from linked list --- + const { + fertilizerIds: allFertilizerIds, + fertData, + isLoading: fertIdsLoading, + isError: fertIdsError, + refetch: refetchFertIds, + } = useAllFertilizerIds(); + + // --- Phase 2: Multicall balanceOf(user, id) for each global ID --- + const balanceChecksEnabled = allFertilizerIds.length > 0 && farmerAddress !== ZERO_ADDRESS; + const balanceCheckContracts = useMemo( + () => + allFertilizerIds.map((id) => ({ + address: BARN_PAYBACK_ADDRESS, + abi: abiSnippets.barnPayback, + functionName: "balanceOf" as const, + args: [farmerAddress, id] as const, + })), + [allFertilizerIds, farmerAddress], + ); + const balanceChecks = useReadContracts({ + contracts: balanceChecksEnabled ? balanceCheckContracts : [], + allowFailure: true, + query: { + ...defaultQuerySettings, + enabled: balanceChecksEnabled, + }, + }); + + // --- Phase 3: Filter IDs where balance > 0 and build per-ID balance map --- + const { userOwnedIds, perIdBalances } = useMemo(() => { + if (!balanceChecks.data) return { userOwnedIds: [] as bigint[], perIdBalances: new Map() }; + const owned: bigint[] = []; + const balances = new Map(); + for (let i = 0; i < allFertilizerIds.length; i++) { + const result = balanceChecks.data?.[i]; + if (result?.status === "success" && (result.result as bigint) > 0n) { + const id = allFertilizerIds[i]; + owned.push(id); + balances.set(id.toString(), result.result as bigint); + } + } + return { userOwnedIds: owned, perIdBalances: balances }; + }, [balanceChecks.data, allFertilizerIds]); + + // --- Phase 4: balanceOfFertilized + balanceOfUnfertilized for userOwnedIds --- + const fertQueryEnabled = userOwnedIds.length > 0; + const fertilizerQuery = useReadContracts({ + contracts: [ + { + address: BARN_PAYBACK_ADDRESS, + abi: abiSnippets.barnPayback, + functionName: "balanceOfFertilized", + args: [farmerAddress, userOwnedIds], + }, + { + address: BARN_PAYBACK_ADDRESS, + abi: abiSnippets.barnPayback, + functionName: "balanceOfUnfertilized", + args: [farmerAddress, userOwnedIds], + }, + ], + allowFailure: true, + query: { + ...defaultQuerySettings, + enabled: fertQueryEnabled, + }, + }); + + // Query for Silo Payback data from Silo_Payback contract + const siloQuery = useReadContracts({ + contracts: [ + { + address: SILO_PAYBACK_ADDRESS, + abi: abiSnippets.siloPayback, + functionName: "earned", + args: [farmerAddress], + }, + { + address: SILO_PAYBACK_ADDRESS, + abi: abiSnippets.siloPayback, + functionName: "getBalanceCombined", + args: [farmerAddress], + }, + { + address: SILO_PAYBACK_ADDRESS, + abi: abiSnippets.siloPayback, + functionName: "totalDistributed", + }, + { + address: SILO_PAYBACK_ADDRESS, + abi: abiSnippets.siloPayback, + functionName: "totalReceived", + }, + ], + allowFailure: true, + query: { + ...defaultQuerySettings, + }, + }); + + // Process Silo Payback data — defaults to ZERO on error + const siloData = useMemo((): SiloPaybackData => { + if (siloQuery.isError) { + return { + balance: TokenValue.ZERO, + earned: TokenValue.ZERO, + totalDistributed: TokenValue.ZERO, + totalReceived: TokenValue.ZERO, + }; + } + + const earnedResult = siloQuery.data?.[0]?.result; + const balanceResult = siloQuery.data?.[1]?.result; + const totalDistributedResult = siloQuery.data?.[2]?.result; + const totalReceivedResult = siloQuery.data?.[3]?.result; + + return { + balance: TokenValue.fromBlockchain(balanceResult ?? 0n, URBDV_DECIMALS), + earned: TokenValue.fromBlockchain(earnedResult ?? 0n, URBDV_DECIMALS), + totalDistributed: TokenValue.fromBlockchain(totalDistributedResult ?? 0n, URBDV_DECIMALS), + totalReceived: TokenValue.fromBlockchain(totalReceivedResult ?? 0n, URBDV_DECIMALS), + }; + }, [siloQuery.data, siloQuery.isError]); + + // Process pods data — defaults to ZERO on error + const podsData = useMemo((): PodsData => { + if (podsQuery.isError) { + return { + plots: [], + totalPods: TokenValue.ZERO, + harvestableIndex: TokenValue.ZERO, + podIndex: TokenValue.ZERO, + }; + } + + const plotsResult = podsQuery.data?.[0]?.result as readonly { index: bigint; pods: bigint }[] | undefined; + const totalPodsResult = podsQuery.data?.[1]?.result; + const harvestableIndexResult = podsQuery.data?.[2]?.result as bigint | undefined; + const podIndexResult = podsQuery.data?.[3]?.result as bigint | undefined; + + const harvestableIndex = TokenValue.fromBigInt(harvestableIndexResult ?? 0n, PODS.decimals); + let podIndex = TokenValue.fromBigInt(podIndexResult ?? 0n, PODS.decimals); + + const plots: Plot[] = (plotsResult ?? []).map((plotData) => { + const index = TokenValue.fromBigInt(plotData.index, PODS.decimals); + const pods = TokenValue.fromBigInt(plotData.pods, PODS.decimals); + const endIndex = index.add(pods); + + let harvestablePods = TokenValue.ZERO; + let unharvestablePods = pods; + + if (harvestableIndex.gt(index)) { + if (harvestableIndex.gte(endIndex)) { + harvestablePods = pods; + unharvestablePods = TokenValue.ZERO; + } else { + harvestablePods = harvestableIndex.sub(index); + unharvestablePods = pods.sub(harvestablePods); + } + } + + return { + id: index.toHuman(), + idHex: toHex(`${plotData.index}${plotData.pods}`), + index, + pods, + harvestedPods: TokenValue.ZERO, + harvestablePods, + unharvestablePods, + }; + }); + + // If podIndex is 0 (e.g. beanstalk repayment field has no native pod index), + // derive it from the last plot's end position + if (podIndex.isZero && plots.length > 0) { + const lastPlot = plots[plots.length - 1]; + podIndex = lastPlot.index.add(lastPlot.pods); + } + + return { + plots, + totalPods: TokenValue.fromBlockchain(totalPodsResult ?? 0n, PODS.decimals), + harvestableIndex, + podIndex, + }; + }, [podsQuery.data, podsQuery.isError]); + + // Process Barn Payback (bsFERT) data — defaults to ZERO on error + const fertilizerData = useMemo((): FertilizerData => { + // Build per-ID detail map with balance, sprouts, humidity + const perIdData = new Map(); + const currentBpf = fertData?.bpf ?? 0n; + + for (const id of userOwnedIds) { + const idStr = id.toString(); + const balance = perIdBalances.get(idStr) ?? 0n; + // Sprouts (unfertilized beans) = balance * max(0, id - currentBpf) + const remainingBpf = id > currentBpf ? id - currentBpf : 0n; + const sprouts = balance * remainingBpf; + // Humidity: fertId = endBpf = (1 + humidity/100) * 1e6 + // humidity% = (id / 1e6 - 1) * 100 + const humidity = (Number(id) / 1e6 - 1) * 100; + perIdData.set(idStr, { balance, sprouts, humidity }); + } + + if (fertilizerQuery.isError) { + return { + balance: TokenValue.ZERO, + fertilized: TokenValue.ZERO, + unfertilized: TokenValue.ZERO, + fertilizerIds: userOwnedIds, + perIdData, + }; + } + + const fertilizedResult = fertilizerQuery.data?.[0]?.result; + const unfertilizedResult = fertilizerQuery.data?.[1]?.result; + + const fertilized = TokenValue.fromBlockchain(fertilizedResult ?? 0n, PINTO.decimals); + const unfertilized = TokenValue.fromBlockchain(unfertilizedResult ?? 0n, PINTO.decimals); + // Total balance is the sum of fertilized + unfertilized + const balance = fertilized.add(unfertilized); + + return { + balance, + fertilized, + unfertilized, + fertilizerIds: userOwnedIds, + perIdData, + }; + }, [fertilizerQuery.data, fertilizerQuery.isError, userOwnedIds, perIdBalances, fertData]); + + // Refetch all queries + const refetch = useCallback(async () => { + await Promise.all([ + siloQuery.refetch(), + podsQuery.refetch(), + refetchFertIds(), + balanceChecks.refetch(), + fertilizerQuery.refetch(), + ]); + }, [siloQuery.refetch, podsQuery.refetch, refetchFertIds, balanceChecks.refetch, fertilizerQuery.refetch]); + + // Loading and error states from all queries + // Only count isError from queries that are actually enabled, + // disabled queries can report isError spuriously + const isLoading = + siloQuery.isLoading || + podsQuery.isLoading || + fertIdsLoading || + (balanceChecksEnabled && balanceChecks.isLoading) || + (fertQueryEnabled && fertilizerQuery.isLoading); + const isError = + siloQuery.isError || + podsQuery.isError || + fertIdsError || + (balanceChecksEnabled && balanceChecks.isError) || + (fertQueryEnabled && fertilizerQuery.isError); + + return useMemo( + () => ({ + silo: siloData, + pods: podsData, + fertilizer: fertilizerData, + isLoading, + isError, + refetch, + }), + [siloData, podsData, fertilizerData, isLoading, isError, refetch], + ); +} diff --git a/src/utils/podTransferUtils.ts b/src/utils/podTransferUtils.ts new file mode 100644 index 000000000..ebbff1758 --- /dev/null +++ b/src/utils/podTransferUtils.ts @@ -0,0 +1,98 @@ +import { TokenValue } from "@/classes/TokenValue"; +import { PODS } from "@/constants/internalTokens"; +import { PodTransferData } from "@/pages/transfer/actions/TransferPods"; +import { Plot } from "@/utils/types"; + +/** + * Converts a cumulative offset range [rangeStart, rangeEnd] across sorted plots + * into per-plot PodTransferData records with relative start/end values. + * + * Adapted from CreateListing's `listingData` useMemo logic. + * + * @param selectedPlots - Plots sorted by index + * @param podRange - [rangeStart, rangeEnd] cumulative offset (0 to totalPods) + * @returns PodTransferData[] with one entry per intersecting plot + */ +export function computeTransferData(selectedPlots: Plot[], podRange: [number, number]): PodTransferData[] { + const result: PodTransferData[] = []; + let cumulativeStart = 0; + + for (const plot of selectedPlots) { + const plotPods = plot.pods.toNumber(); + const cumulativeEnd = cumulativeStart + plotPods; + + // Check if this plot intersects with the selected range + if (podRange[1] > cumulativeStart && podRange[0] < cumulativeEnd) { + const startInPlot = Math.max(0, podRange[0] - cumulativeStart); + const endInPlot = Math.min(plotPods, podRange[1] - cumulativeStart); + + if (endInPlot > startInPlot) { + result.push({ + id: plot.index, + start: TokenValue.fromHuman(startInPlot, PODS.decimals), + end: TokenValue.fromHuman(endInPlot, PODS.decimals), + }); + } + } + + cumulativeStart = cumulativeEnd; + } + + return result; +} + +/** + * Converts a cumulative offset (relative to sorted plots) into an absolute + * TokenValue index on the pod line. + * + * Adapted from CreateListing's `selectedPodRange` useMemo logic. + * + * @param offset - Cumulative offset (0 to totalPods) + * @param sortedPlots - Plots sorted by index + * @returns Absolute TokenValue index on the pod line + */ +export function offsetToAbsoluteIndex(offset: number, sortedPlots: Plot[]): TokenValue { + let remainingOffset = offset; + + for (const plot of sortedPlots) { + const plotPods = plot.pods.toNumber(); + if (remainingOffset <= plotPods) { + return plot.index.add(TokenValue.fromHuman(remainingOffset, PODS.decimals)); + } + remainingOffset -= plotPods; + } + + // Fallback: offset exceeds total pods, clamp to end of last plot + const lastPlot = sortedPlots[sortedPlots.length - 1]; + return lastPlot.index.add(lastPlot.pods); +} + +/** + * Computes a consolidated summary range from transfer data records. + * + * - totalPods: sum of (end - start) across all records + * - placeInLineStart: first record's (id + start) - harvestableIndex + * - placeInLineEnd: last record's (id + end) - harvestableIndex + * + * @param transferData - Array of PodTransferData (must have at least one entry) + * @param harvestableIndex - Current harvestable index on the pod line + * @returns { totalPods, placeInLineStart, placeInLineEnd } + */ +export function computeSummaryRange( + transferData: PodTransferData[], + harvestableIndex: TokenValue, +): { totalPods: TokenValue; placeInLineStart: TokenValue; placeInLineEnd: TokenValue } { + const totalPods = transferData.reduce((sum, record) => sum.add(record.end.sub(record.start)), TokenValue.ZERO); + + const first = transferData[0]; + const last = transferData[transferData.length - 1]; + + const rangeStart = first.id.add(first.start); + const rangeEnd = last.id.add(last.end); + + return { + totalPods, + placeInLineStart: rangeStart.sub(harvestableIndex), + placeInLineEnd: rangeEnd.sub(harvestableIndex), + }; +}