From 62ef0b120fe6b60e30204da2df7221d9308b4dad Mon Sep 17 00:00:00 2001 From: feyyazcigim Date: Mon, 23 Feb 2026 06:23:37 +0300 Subject: [PATCH 1/7] feat(silo): introduce automated claim presets and order management --- src/__tests/conditionSection.test.ts | 36 ++ .../AutomateClaim/AutomateClaimContext.tsx | 42 ++ .../AutomateClaimExecutionHistory.tsx | 107 +++++ .../AutomateClaimVisualization.tsx | 93 +++++ .../AutomateClaim/ConditionSection.tsx | 149 +++++++ .../AutomateClaim/SpecifyConditionsDialog.tsx | 334 +++++++++++++++ src/components/Tractor/AutomateClaim/index.ts | 5 + .../Tractor/HighlightedCallData.tsx | 50 ++- .../FarmerTractorAutomateClaimOrderCard.tsx | 110 +++++ .../TractorFarmerMixedOrders.types.ts | 25 +- .../TractorFarmerMixedOrders.utils.ts | 62 ++- .../TractorFarmerOrderTypeRegistry.tsx | 77 +++- .../farmer-orders/TractorOrdersPanel.tsx | 49 ++- .../form/schema/automateClaim.schema.ts | 127 ++++++ src/components/Tractor/types.ts | 12 +- .../abi/AutomateClaimBlueprintABI.ts | 135 ++++++ src/constants/abiConfig.ts | 12 + src/constants/address.ts | 4 + src/hooks/tractor/useAutomateClaimOrder.ts | 147 +++++++ .../automate-claim-decoder.ts | 30 ++ src/lib/Tractor/blueprint-decoders/index.ts | 7 +- src/lib/Tractor/claimOrder/index.ts | 2 + .../Tractor/claimOrder/tractor-claim-types.ts | 119 ++++++ src/lib/Tractor/claimOrder/tractor-claim.ts | 388 ++++++++++++++++++ src/lib/Tractor/core/constants.ts | 2 + src/lib/Tractor/core/shared-tractor-types.ts | 4 +- src/lib/Tractor/index.ts | 1 + .../requisitions/tractor-requisition.ts | 32 +- src/pages/Silo.tsx | 16 +- src/pages/overview/FarmerOverview.tsx | 2 +- src/pages/silo/RewardsClaim.tsx | 71 ++-- src/pages/silo/SiloConvertUpContent.tsx | 6 +- .../tractor/useTractorAutomateClaimOrders.ts | 106 +++++ src/state/tractor/useTractorClaimOrders.ts | 8 + 34 files changed, 2317 insertions(+), 53 deletions(-) create mode 100644 src/__tests/conditionSection.test.ts create mode 100644 src/components/Tractor/AutomateClaim/AutomateClaimContext.tsx create mode 100644 src/components/Tractor/AutomateClaim/AutomateClaimExecutionHistory.tsx create mode 100644 src/components/Tractor/AutomateClaim/AutomateClaimVisualization.tsx create mode 100644 src/components/Tractor/AutomateClaim/ConditionSection.tsx create mode 100644 src/components/Tractor/AutomateClaim/SpecifyConditionsDialog.tsx create mode 100644 src/components/Tractor/AutomateClaim/index.ts create mode 100644 src/components/Tractor/farmer-orders/FarmerTractorAutomateClaimOrderCard.tsx create mode 100644 src/components/Tractor/form/schema/automateClaim.schema.ts create mode 100644 src/constants/abi/AutomateClaimBlueprintABI.ts create mode 100644 src/hooks/tractor/useAutomateClaimOrder.ts create mode 100644 src/lib/Tractor/blueprint-decoders/automate-claim-decoder.ts create mode 100644 src/lib/Tractor/claimOrder/index.ts create mode 100644 src/lib/Tractor/claimOrder/tractor-claim-types.ts create mode 100644 src/lib/Tractor/claimOrder/tractor-claim.ts create mode 100644 src/state/tractor/useTractorAutomateClaimOrders.ts create mode 100644 src/state/tractor/useTractorClaimOrders.ts diff --git a/src/__tests/conditionSection.test.ts b/src/__tests/conditionSection.test.ts new file mode 100644 index 000000000..fc57a292b --- /dev/null +++ b/src/__tests/conditionSection.test.ts @@ -0,0 +1,36 @@ +import { CLAIM_PRESETS } from "@/lib/Tractor/claimOrder/tractor-claim"; +import { describe, expect, it } from "vitest"; + +/** + * Unit tests for ConditionSection preset configuration and display logic. + * Validates Requirements 3.1-3.10, 6.2-6.5 + */ +describe("ConditionSection — Preset Configuration", () => { + it("should have correct preset values for Mow (Req 3.1-3.3)", () => { + expect(CLAIM_PRESETS.mow.high.value).toBe(50); + expect(CLAIM_PRESETS.mow.medium.value).toBe(100); + expect(CLAIM_PRESETS.mow.low.value).toBe(1000); + }); + + it("should have correct preset values for Plant (Req 3.4-3.6)", () => { + expect(CLAIM_PRESETS.plant.high.value).toBe(10); + expect(CLAIM_PRESETS.plant.medium.value).toBe(50); + expect(CLAIM_PRESETS.plant.low.value).toBe(500); + }); + + it("should have correct preset values for Harvest (Req 3.7-3.9)", () => { + expect(CLAIM_PRESETS.harvest.high.value).toBe(10); + expect(CLAIM_PRESETS.harvest.medium.value).toBe(50); + expect(CLAIM_PRESETS.harvest.low.value).toBe(500); + }); + + it("should have exactly 3 preset levels (high, medium, low) per operation", () => { + for (const op of ["mow", "plant", "harvest"] as const) { + const keys = Object.keys(CLAIM_PRESETS[op]); + expect(keys).toContain("high"); + expect(keys).toContain("medium"); + expect(keys).toContain("low"); + expect(keys).toHaveLength(3); + } + }); +}); 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..0f37f5456 --- /dev/null +++ b/src/components/Tractor/AutomateClaim/ConditionSection.tsx @@ -0,0 +1,149 @@ +import { Col, Row } from "@/components/Container"; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/Accordion"; +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 normalized = customValue.trim().replace(",", "."); + const num = Number(normalized); + const isValid = normalized !== "" && !Number.isNaN(num) && num > 0; + + 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..71543596c --- /dev/null +++ b/src/components/Tractor/AutomateClaim/SpecifyConditionsDialog.tsx @@ -0,0 +1,334 @@ +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 type { ClaimFrequencyPreset } from "@/lib/Tractor/claimOrder/tractor-claim-types"; +import useTractorOperatorAverageTipPaid from "@/state/tractor/useTractorOperatorAverageTipPaid"; +import { useFarmerSilo } from "@/state/useFarmerSilo"; +import { useMainToken } from "@/state/useTokenData"; +import { formatter } from "@/utils/format"; +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(); + + // 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; + + // Custom presets require a valid positive numeric value + const isValidCustomValue = (v: string) => { + const normalized = v.trim().replace(",", "."); + if (!normalized) return false; + const num = Number(normalized); + return !Number.isNaN(num) && num > 0; + }; + + 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 && ( + { + 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..ca3a9a8f9 --- /dev/null +++ b/src/components/Tractor/AutomateClaim/index.ts @@ -0,0 +1,5 @@ +export { AutomateClaimProvider, useAutomateClaimContext } from "./AutomateClaimContext"; +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 MAX_UINT256 = 2n ** 256n - 1n; + const mowEnabled = transformed.claimParams.minMowAmount !== MAX_UINT256; + const plantEnabled = transformed.claimParams.minPlantAmount !== MAX_UINT256; + const harvestEnabled = transformed.claimParams.fieldHarvestConfigs.length > 0; + + const enabledOps = [mowEnabled && "Mow", plantEnabled && "Plant", harvestEnabled && "Harvest"].filter(Boolean); + + const operatorTip = TokenValue.fromBlockchain(transformed.operatorParams.operatorTipAmount, 6); + + 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(", ")} + + ), + }, + ]} + /> + +
+ + ({ + text: `${op} enabled`, + }))} + /> +
+
+ + +
+ + 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..141998d5c 100644 --- a/src/components/Tractor/farmer-orders/TractorFarmerMixedOrders.types.ts +++ b/src/components/Tractor/farmer-orders/TractorFarmerMixedOrders.types.ts @@ -1,4 +1,5 @@ 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 +15,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 +54,16 @@ export interface ConvertUpOrderData { strategy: string; } +// AutomateClaim-specific order data +export interface AutomateClaimOrderData { + type: "automateClaim"; + mowEnabled: boolean; + plantEnabled: boolean; + harvestEnabled: boolean; + operatorTip: string; + percentComplete: number; +} + // Type guards export function isSowOrder(order: UnifiedTractorOrder): order is UnifiedTractorOrder & { orderData: SowOrderData; @@ -64,6 +78,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..869926d1e 100644 --- a/src/components/Tractor/farmer-orders/TractorFarmerMixedOrders.utils.ts +++ b/src/components/Tractor/farmer-orders/TractorFarmerMixedOrders.utils.ts @@ -1,8 +1,11 @@ import { TokenValue } from "@/classes/TokenValue"; import { PublisherTractorExecution, SowBlueprintData, TractorRequisitionEvent } from "@/lib/Tractor"; +import { 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 +147,50 @@ 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 MAX_UINT256 = 2n ** 256n - 1n; + const mowEnabled = transformed.claimParams.minMowAmount !== MAX_UINT256; + const plantEnabled = transformed.claimParams.minPlantAmount !== MAX_UINT256; + const harvestEnabled = transformed.claimParams.fieldHarvestConfigs.length > 0; + + 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,13 @@ 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 = [data.mowEnabled && "Mow", data.plantEnabled && "Plant", data.harvestEnabled && "Harvest"].filter( + Boolean, + ); + return `Automate Claim • ${ops.join(", ")} • Active`; + } const typeLabel = order.type === "sow" ? "Sow" : "Convert Up"; const amount = order.type === "sow" @@ -223,7 +277,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 +291,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-purple-100 text-purple-800 border-purple-200", + icon: "🔄", + }; } } diff --git a/src/components/Tractor/farmer-orders/TractorFarmerOrderTypeRegistry.tsx b/src/components/Tractor/farmer-orders/TractorFarmerOrderTypeRegistry.tsx index c9aa62eda..afbee0b71 100644 --- a/src/components/Tractor/farmer-orders/TractorFarmerOrderTypeRegistry.tsx +++ b/src/components/Tractor/farmer-orders/TractorFarmerOrderTypeRegistry.tsx @@ -7,12 +7,13 @@ import { SowBlueprintData, decodeSowTractorData, } from "@/lib/Tractor"; +import { decodeAutomateClaimBlueprint, 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 +78,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 +150,23 @@ 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"); + + const MAX_UINT256 = 2n ** 256n - 1n; + + return { + type: "automateClaim", + mowEnabled: transformed.claimParams.minMowAmount !== MAX_UINT256, + plantEnabled: transformed.claimParams.minPlantAmount !== MAX_UINT256, + harvestEnabled: transformed.claimParams.fieldHarvestConfigs.length > 0, + 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..11b55b9b3 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,12 @@ function TractorOrdersPanelGeneric({ enabled: !!address && filters.orderTypes.includes("convertUp"), }); + const { data: automateClaimOrders, ...automateClaimOrdersQuery } = useTractorAutomateClaimOrderbook({ + address, + filterOutCompleted: false, + 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 +162,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 +190,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 +209,9 @@ function TractorOrdersPanelGeneric({ if (filters.orderTypes.includes("convertUp")) { convertUpOrdersQuery.refetch(); } + if (filters.orderTypes.includes("automateClaim")) { + automateClaimOrdersQuery.refetch(); + } } }, [refreshData, dataHasLoaded, filters.orderTypes]); @@ -316,6 +348,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/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/constants/abi/AutomateClaimBlueprintABI.ts b/src/constants/abi/AutomateClaimBlueprintABI.ts new file mode 100644 index 000000000..d40ea3ec8 --- /dev/null +++ b/src/constants/abi/AutomateClaimBlueprintABI.ts @@ -0,0 +1,135 @@ +export const automateClaimBlueprintABI = [ + { + inputs: [ + { + components: [ + { + components: [ + { + internalType: "uint256", + name: "minMowAmount", + type: "uint256", + }, + { + internalType: "int256", + name: "minTwaDeltaB", + type: "int256", + }, + { + internalType: "uint256", + name: "minPlantAmount", + type: "uint256", + }, + { + components: [ + { + internalType: "uint256", + name: "fieldId", + type: "uint256", + }, + { + internalType: "uint256", + name: "minHarvestAmount", + type: "uint256", + }, + ], + internalType: "struct AutomateClaimBlueprint.FieldHarvestConfig[]", + name: "fieldHarvestConfigs", + type: "tuple[]", + }, + { + internalType: "uint256", + name: "minRinseAmount", + type: "uint256", + }, + { + internalType: "uint256", + name: "minUnripeClaimAmount", + type: "uint256", + }, + { + internalType: "uint256[]", + name: "sourceTokenIndices", + type: "uint256[]", + }, + { + internalType: "uint256", + name: "maxGrownStalkPerBdv", + type: "uint256", + }, + { + internalType: "uint256", + name: "slippageRatio", + type: "uint256", + }, + ], + internalType: "struct AutomateClaimBlueprint.AutomateClaimParams", + name: "claimParams", + type: "tuple", + }, + { + components: [ + { + components: [ + { + internalType: "address[]", + name: "whitelistedOperators", + type: "address[]", + }, + { + internalType: "address", + name: "tipAddress", + type: "address", + }, + { + internalType: "uint256", + name: "operatorTipAmount", + type: "uint256", + }, + ], + internalType: "struct AutomateClaimBlueprint.OperatorParams", + name: "baseOpParams", + type: "tuple", + }, + { + internalType: "uint256", + name: "mowTipAmount", + type: "uint256", + }, + { + internalType: "uint256", + name: "plantTipAmount", + type: "uint256", + }, + { + internalType: "uint256", + name: "harvestTipAmount", + type: "uint256", + }, + { + internalType: "uint256", + name: "rinseTipAmount", + type: "uint256", + }, + { + internalType: "uint256", + name: "unripeClaimTipAmount", + type: "uint256", + }, + ], + internalType: "struct AutomateClaimBlueprint.OperatorParamsExtended", + name: "opParams", + type: "tuple", + }, + ], + internalType: "struct AutomateClaimBlueprint.AutomateClaimBlueprintStruct", + name: "params", + type: "tuple", + }, + ], + name: "automateClaimBlueprint", + outputs: [], + stateMutability: "payable", + type: "function", + }, +] as const; diff --git a/src/constants/abiConfig.ts b/src/constants/abiConfig.ts index d41dea2be..2ae19efe3 100644 --- a/src/constants/abiConfig.ts +++ b/src/constants/abiConfig.ts @@ -1,5 +1,6 @@ import { Abi } from "viem"; import { arbitrum, base, foundry, localhost, mainnet } from "wagmi/chains"; +import { automateClaimBlueprintABI } from "./abi/AutomateClaimBlueprintABI"; import { pipelineABI } from "./abi/PipelineABI"; import { siloHelpersABI } from "./abi/SiloHelpersABI"; import { sowBlueprintv0ABI } from "./abi/SowBlueprintv0ABI"; @@ -9,6 +10,7 @@ import { depotABI } from "./abi/depotABI"; import { diamondABI } from "./abi/diamondABI"; import { diamondPriceABI } from "./abi/diamondPriceABI"; import { + AUTOMATE_CLAIM_BLUEPRINT_ADDRESS, CONVERT_UP_BLUEPRINT_V0_ADDRESS, DIAMOND_PRICE_ADDRESS, PIPELINE_ADDRESS, @@ -84,6 +86,12 @@ const ADDRESSES_LOOKUP = { [TESTNET_CHAIN_ID]: CONVERT_UP_BLUEPRINT_V0_ADDRESS, [foundry.id]: CONVERT_UP_BLUEPRINT_V0_ADDRESS, }, + automateClaimBlueprint: { + [base.id]: AUTOMATE_CLAIM_BLUEPRINT_ADDRESS, + [localhost.id]: AUTOMATE_CLAIM_BLUEPRINT_ADDRESS, + [TESTNET_CHAIN_ID]: AUTOMATE_CLAIM_BLUEPRINT_ADDRESS, + [foundry.id]: AUTOMATE_CLAIM_BLUEPRINT_ADDRESS, + }, } as const; const ABI_CONFIG = { @@ -119,6 +127,10 @@ const ABI_CONFIG = { abi: convertUpBlueprintV0ABI as Abi, address: ADDRESSES_LOOKUP.convertUpBlueprint, }, + automateClaimBlueprint: { + abi: automateClaimBlueprintABI as Abi, + address: ADDRESSES_LOOKUP.automateClaimBlueprint, + }, } as const; export default ABI_CONFIG; diff --git a/src/constants/address.ts b/src/constants/address.ts index eeb70471e..1e3674751 100644 --- a/src/constants/address.ts +++ b/src/constants/address.ts @@ -23,12 +23,16 @@ export const SOW_BLUEPRINT_REFERRAL_V0_ADDRESS: HashString = "0xD9DF9C4C01604017 export const CONVERT_UP_BLUEPRINT_V0_ADDRESS: HashString = "0x5167Ae1fF37bE08D9cc9188C7e64DB228B6F06ca"; +export const AUTOMATE_CLAIM_BLUEPRINT_ADDRESS: HashString = "0x0000000000000000000000000000000000000000"; // Placeholder - update after deployment + export const SOW_BLUEPRINT_V0_SELECTOR = "0x3ca8e1b2" as const; export const SOW_BLUEPRINT_REFERRAL_V0_SELECTOR = "0xd3936ead" as const; // Placeholder - update after deployment export const CONVERT_UP_BLUEPRINT_V0_SELECTOR = "0x0fd2f3ed" as const; +export const AUTOMATE_CLAIM_BLUEPRINT_SELECTOR = "0xf2a6fd1a" as const; + export const WELL_FUNCTION_ADDRESSES: ChainLookup<{ stable2: HashString; cp2: HashString; diff --git a/src/hooks/tractor/useAutomateClaimOrder.ts b/src/hooks/tractor/useAutomateClaimOrder.ts new file mode 100644 index 000000000..183bcd720 --- /dev/null +++ b/src/hooks/tractor/useAutomateClaimOrder.ts @@ -0,0 +1,147 @@ +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(); + console.debug("[useAutomateClaimOrder] Creating blueprint with form data:", formData); + + // 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(); + console.debug("[useAutomateClaimOrder] Tip per execution:", tipPerExecution.toString()); + + // 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" }), + ]); + + console.debug("[useAutomateClaimOrder] Tractor data created:", tractorData); + + // Create the blueprint (Req 5.2) + const blueprint = createBlueprintFromBlock({ + block, + publisher: address, + data: tractorData.data, + operatorPasteInstrs: tractorData.operatorPasteInstrs, + maxNonce: TV.MAX_UINT256.toBigInt(), + }); + + console.debug("[useAutomateClaimOrder] Blueprint created:", blueprint); + + // 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/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/index.ts b/src/lib/Tractor/claimOrder/index.ts new file mode 100644 index 000000000..d334add8c --- /dev/null +++ b/src/lib/Tractor/claimOrder/index.ts @@ -0,0 +1,2 @@ +export * from "./tractor-claim"; +export * from "./tractor-claim-types"; 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..ef2853a39 --- /dev/null +++ b/src/lib/Tractor/claimOrder/tractor-claim.ts @@ -0,0 +1,388 @@ +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) { + console.debug("[Tractor/decodeAutomateClaimBlueprintFromAdvancedPipe] No calls provided. Returning null."); + return null; + } + + const callData = calls[0].callData; + + try { + const decoded = decodeFunctionData({ + abi: automateClaimBlueprintABI, + data: 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 { + console.debug("[Tractor/decodeAutomateClaimBlueprint] Attempting to decode, data length:", encodedData.length); + const pipeCalls = decodeEncodedTractorDataToAdvancedPipeCalls(encodedData, "automateClaim"); + console.debug("[Tractor/decodeAutomateClaimBlueprint] Pipe calls result:", pipeCalls?.length ?? "undefined"); + + if (pipeCalls?.length) { + const result = decodeAutomateClaimBlueprintFromAdvancedPipe(pipeCalls); + console.debug("[Tractor/decodeAutomateClaimBlueprint] Decode result:", result ? "success" : "null"); + return result; + } + } 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") { + console.debug("[Tractor/transformAutomateClaimRequisitionEvent] Invalid params."); + return null; + } + + const struct = params as AutomateClaimBlueprintStruct; + + if (!struct.claimParams || !struct.opParams) { + console.debug("[Tractor/transformAutomateClaimRequisitionEvent] Missing claimParams or 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
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/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/state/tractor/useTractorAutomateClaimOrders.ts b/src/state/tractor/useTractorAutomateClaimOrders.ts new file mode 100644 index 000000000..404bdfbf0 --- /dev/null +++ b/src/state/tractor/useTractorAutomateClaimOrders.ts @@ -0,0 +1,106 @@ +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 { stringEq } from "@/utils/string"; +import { useQuery } from "@tanstack/react-query"; +import { useCallback } from "react"; +import { useChainId, usePublicClient } from "wagmi"; + +interface UseTractorAutomateClaimOrderbookOptions { + address?: `0x${string}`; + filterOutCompleted?: boolean; + enabled?: boolean; +} + +export function useTractorAutomateClaimOrderbook({ + address, + enabled = true, +}: UseTractorAutomateClaimOrderbookOptions = {}) { + const chainId = useChainId(); + const client = usePublicClient({ chainId }); + const diamond = useProtocolAddress(); + + const query = useQuery({ + queryKey: ["tractor", "automateClaimOrders", address, chainId], + queryFn: async (): Promise[]> => { + if (!client || !diamond) return []; + + // Use the general deployment block to catch all events + const { publishEvents, cancelledHashes } = await fetchTractorEvents(client, diamond, TRACTOR_DEPLOYMENT_BLOCK); + + console.debug("[useTractorAutomateClaimOrderbook] Total publish events:", publishEvents.length); + + 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; + + // First try the blueprint-decoders system to check if this is an automateClaim type + const blueprintDecoded = decodeBlueprintCallData(blueprintData); + + // Try to decode as automateClaimBlueprint using the direct decoder + const decodedData = decodeAutomateClaimBlueprint(blueprintData); + + // Accept if either decoder identifies this as automateClaim + if (!decodedData && blueprintDecoded?.type !== "automateClaim") { + continue; + } + + console.debug("[useTractorAutomateClaimOrderbook] Found automateClaim order:", { + hash: requisition.blueprintHash, + publisher: requisition.blueprint.publisher, + decodedData: !!decodedData, + blueprintDecodedType: blueprintDecoded?.type, + }); + + const isCancelled = cancelledHashes.has(requisition.blueprintHash); + + // If direct decode worked, use it. Otherwise try to extract from blueprint-decoders result + let finalDecodedData = decodedData; + if (!finalDecodedData && blueprintDecoded?.type === "automateClaim" && blueprintDecoded.params) { + // The blueprint-decoders system decoded the params directly from the inner call + finalDecodedData = blueprintDecoded.params as AutomateClaimBlueprintStruct; + } + + if (!finalDecodedData) continue; + + automateClaimOrders.push({ + requisition, + blockNumber: Number(event.blockNumber ?? 0), + timestamp: undefined, + isCancelled, + requisitionType: "automateClaimBlueprint", + decodedData: finalDecodedData, + }); + } + + console.debug("[useTractorAutomateClaimOrderbook] Found orders:", automateClaimOrders.length); + + 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/tractor/useTractorClaimOrders.ts b/src/state/tractor/useTractorClaimOrders.ts new file mode 100644 index 000000000..ae7b25d16 --- /dev/null +++ b/src/state/tractor/useTractorClaimOrders.ts @@ -0,0 +1,8 @@ +/** + * Re-export of automate claim orderbook hook. + * + * The design doc lists this module path (`useTractorClaimOrders`) as the + * canonical location for fetching & decoding automate-claim orders. + * The actual implementation lives in `useTractorAutomateClaimOrders.ts`. + */ +export { useTractorAutomateClaimOrderbook } from "./useTractorAutomateClaimOrders"; From 1f1421610b7f8ce4d8c164397a945e2bf9aee278 Mon Sep 17 00:00:00 2001 From: feyyazcigim Date: Wed, 25 Feb 2026 01:23:44 +0300 Subject: [PATCH 2/7] refactor: fix automated order indext and refactor implementation --- src/__tests/conditionSection.test.ts | 36 ---------- src/components/ReviewTractorOrderDialog.tsx | 30 ++++---- .../AutomateClaim/ConditionSection.tsx | 5 +- .../AutomateClaim/SpecifyConditionsDialog.tsx | 17 ++--- src/components/Tractor/AutomateClaim/index.ts | 1 - .../FarmerTractorAutomateClaimOrderCard.tsx | 34 +++++---- .../TractorFarmerMixedOrders.types.ts | 10 +-- .../TractorFarmerMixedOrders.utils.ts | 18 +++-- .../TractorFarmerOrderTypeRegistry.tsx | 12 ++-- .../farmer-orders/TractorOrdersPanel.tsx | 3 +- src/constants/address.ts | 2 +- src/hooks/tractor/useAutomateClaimOrder.ts | 8 +-- .../claimOrder/automate-claim-helpers.ts | 70 +++++++++++++++++++ src/lib/Tractor/claimOrder/index.ts | 1 + src/lib/Tractor/claimOrder/tractor-claim.ts | 17 +---- src/state/queryKeys.ts | 6 ++ .../tractor/useTractorAutomateClaimOrders.ts | 55 ++++++--------- src/state/tractor/useTractorClaimOrders.ts | 8 --- 18 files changed, 169 insertions(+), 164 deletions(-) delete mode 100644 src/__tests/conditionSection.test.ts create mode 100644 src/lib/Tractor/claimOrder/automate-claim-helpers.ts delete mode 100644 src/state/tractor/useTractorClaimOrders.ts diff --git a/src/__tests/conditionSection.test.ts b/src/__tests/conditionSection.test.ts deleted file mode 100644 index fc57a292b..000000000 --- a/src/__tests/conditionSection.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { CLAIM_PRESETS } from "@/lib/Tractor/claimOrder/tractor-claim"; -import { describe, expect, it } from "vitest"; - -/** - * Unit tests for ConditionSection preset configuration and display logic. - * Validates Requirements 3.1-3.10, 6.2-6.5 - */ -describe("ConditionSection — Preset Configuration", () => { - it("should have correct preset values for Mow (Req 3.1-3.3)", () => { - expect(CLAIM_PRESETS.mow.high.value).toBe(50); - expect(CLAIM_PRESETS.mow.medium.value).toBe(100); - expect(CLAIM_PRESETS.mow.low.value).toBe(1000); - }); - - it("should have correct preset values for Plant (Req 3.4-3.6)", () => { - expect(CLAIM_PRESETS.plant.high.value).toBe(10); - expect(CLAIM_PRESETS.plant.medium.value).toBe(50); - expect(CLAIM_PRESETS.plant.low.value).toBe(500); - }); - - it("should have correct preset values for Harvest (Req 3.7-3.9)", () => { - expect(CLAIM_PRESETS.harvest.high.value).toBe(10); - expect(CLAIM_PRESETS.harvest.medium.value).toBe(50); - expect(CLAIM_PRESETS.harvest.low.value).toBe(500); - }); - - it("should have exactly 3 preset levels (high, medium, low) per operation", () => { - for (const op of ["mow", "plant", "harvest"] as const) { - const keys = Object.keys(CLAIM_PRESETS[op]); - expect(keys).toContain("high"); - expect(keys).toContain("medium"); - expect(keys).toContain("low"); - expect(keys).toHaveLength(3); - } - }); -}); 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/ConditionSection.tsx b/src/components/Tractor/AutomateClaim/ConditionSection.tsx index 0f37f5456..50e51142a 100644 --- a/src/components/Tractor/AutomateClaim/ConditionSection.tsx +++ b/src/components/Tractor/AutomateClaim/ConditionSection.tsx @@ -1,5 +1,6 @@ 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"; @@ -75,9 +76,7 @@ export const ConditionSection = ({ // When "custom" is selected, show inline input instead of button if (btn.key === "custom" && isSelected) { - const normalized = customValue.trim().replace(",", "."); - const num = Number(normalized); - const isValid = normalized !== "" && !Number.isNaN(num) && num > 0; + const isValid = isValidCustomValue(customValue); return (
{ - const normalized = v.trim().replace(",", "."); - if (!normalized) return false; - const num = Number(normalized); - return !Number.isNaN(num) && num > 0; - }; - const hasInvalidCustom = (mowPreset === "custom" && !isValidCustomValue(mowCustomValue)) || (plantPreset === "custom" && !isValidCustomValue(plantCustomValue)) || @@ -200,7 +196,7 @@ export const SpecifyConditionsDialog = ({ open, onOpenChange }: SpecifyCondition - + Specify conditions @@ -215,7 +211,7 @@ export const SpecifyConditionsDialog = ({ open, onOpenChange }: SpecifyCondition {/* Condition Sections */} - + { + queryClient.invalidateQueries({ queryKey: [queryKeys.base.tractor] }); form.reset({ ...defaultAutomateClaimValues }); setShowReviewDialog(false); onOpenChange(false); diff --git a/src/components/Tractor/AutomateClaim/index.ts b/src/components/Tractor/AutomateClaim/index.ts index ca3a9a8f9..04eabd7a4 100644 --- a/src/components/Tractor/AutomateClaim/index.ts +++ b/src/components/Tractor/AutomateClaim/index.ts @@ -1,4 +1,3 @@ -export { AutomateClaimProvider, useAutomateClaimContext } from "./AutomateClaimContext"; export { SpecifyConditionsDialog } from "./SpecifyConditionsDialog"; export { ConditionSection } from "./ConditionSection"; export { AutomateClaimVisualization } from "./AutomateClaimVisualization"; diff --git a/src/components/Tractor/farmer-orders/FarmerTractorAutomateClaimOrderCard.tsx b/src/components/Tractor/farmer-orders/FarmerTractorAutomateClaimOrderCard.tsx index c03adaa8c..d09210e87 100644 --- a/src/components/Tractor/farmer-orders/FarmerTractorAutomateClaimOrderCard.tsx +++ b/src/components/Tractor/farmer-orders/FarmerTractorAutomateClaimOrderCard.tsx @@ -7,7 +7,12 @@ import { Card } from "@/components/ui/Card"; import IconImage from "@/components/ui/IconImage"; import { TractorRequisitionEvent as RequisitionEvent } from "@/lib/Tractor"; import { PublisherTractorExecution } from "@/lib/Tractor"; -import { transformAutomateClaimRequisitionEvent } from "@/lib/Tractor/claimOrder"; +import { + getClaimOpConditionLabel, + getEnabledClaimOpLabels, + getEnabledClaimOps, + transformAutomateClaimRequisitionEvent, +} from "@/lib/Tractor/claimOrder"; import { AutomateClaimBlueprintStruct } from "@/lib/Tractor/claimOrder/tractor-claim-types"; import { formatter } from "@/utils/format"; import { CalendarIcon, ClockIcon, Cross1Icon } from "@radix-ui/react-icons"; @@ -35,15 +40,24 @@ const FarmerTractorAutomateClaimOrderCard = ({ const transformed = transformAutomateClaimRequisitionEvent(req.decodedData); if (!transformed) return null; - const MAX_UINT256 = 2n ** 256n - 1n; - const mowEnabled = transformed.claimParams.minMowAmount !== MAX_UINT256; - const plantEnabled = transformed.claimParams.minPlantAmount !== MAX_UINT256; - const harvestEnabled = transformed.claimParams.fieldHarvestConfigs.length > 0; - - const enabledOps = [mowEnabled && "Mow", plantEnabled && "Plant", harvestEnabled && "Harvest"].filter(Boolean); + 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"; @@ -72,11 +86,7 @@ const FarmerTractorAutomateClaimOrderCard = ({
- ({ - text: `${op} enabled`, - }))} - /> +
diff --git a/src/components/Tractor/farmer-orders/TractorFarmerMixedOrders.types.ts b/src/components/Tractor/farmer-orders/TractorFarmerMixedOrders.types.ts index 141998d5c..86f17f033 100644 --- a/src/components/Tractor/farmer-orders/TractorFarmerMixedOrders.types.ts +++ b/src/components/Tractor/farmer-orders/TractorFarmerMixedOrders.types.ts @@ -1,3 +1,4 @@ +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"; @@ -54,13 +55,8 @@ export interface ConvertUpOrderData { strategy: string; } -// AutomateClaim-specific order data -export interface AutomateClaimOrderData { - type: "automateClaim"; - mowEnabled: boolean; - plantEnabled: boolean; - harvestEnabled: boolean; - operatorTip: string; +// AutomateClaim-specific order data (extends base with percentComplete for unified order tracking) +export interface AutomateClaimOrderData extends BaseAutomateClaimOrderData { percentComplete: number; } diff --git a/src/components/Tractor/farmer-orders/TractorFarmerMixedOrders.utils.ts b/src/components/Tractor/farmer-orders/TractorFarmerMixedOrders.utils.ts index 869926d1e..5b5ca30ff 100644 --- a/src/components/Tractor/farmer-orders/TractorFarmerMixedOrders.utils.ts +++ b/src/components/Tractor/farmer-orders/TractorFarmerMixedOrders.utils.ts @@ -1,6 +1,10 @@ import { TokenValue } from "@/classes/TokenValue"; import { PublisherTractorExecution, SowBlueprintData, TractorRequisitionEvent } from "@/lib/Tractor"; -import { transformAutomateClaimRequisitionEvent } from "@/lib/Tractor/claimOrder"; +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"; @@ -161,11 +165,7 @@ export function transformAutomateClaimOrderToUnified( throw new Error("Failed to transform AutomateClaim data"); } - const MAX_UINT256 = 2n ** 256n - 1n; - const mowEnabled = transformed.claimParams.minMowAmount !== MAX_UINT256; - const plantEnabled = transformed.claimParams.minPlantAmount !== MAX_UINT256; - const harvestEnabled = transformed.claimParams.fieldHarvestConfigs.length > 0; - + const { mowEnabled, plantEnabled, harvestEnabled } = getEnabledClaimOps(transformed); const operatorTip = TokenValue.fromBlockchain(transformed.operatorParams.operatorTipAmount, 6).toHuman(); const automateClaimOrderData: AutomateClaimOrderData = { @@ -262,9 +262,7 @@ export function filterUnifiedOrders(orders: UnifiedTractorOrder[], filters: Mixe export function getOrderSummary(order: UnifiedTractorOrder): string { if (order.type === "automateClaim") { const data = order.orderData as AutomateClaimOrderData; - const ops = [data.mowEnabled && "Mow", data.plantEnabled && "Plant", data.harvestEnabled && "Harvest"].filter( - Boolean, - ); + const ops = getEnabledClaimOpLabels(data); return `Automate Claim • ${ops.join(", ")} • Active`; } const typeLabel = order.type === "sow" ? "Sow" : "Convert Up"; @@ -294,7 +292,7 @@ export function getOrderTypeBadge(orderType: "sow" | "convertUp" | "automateClai case "automateClaim": return { label: "Automate Claim", - className: "bg-purple-100 text-purple-800 border-purple-200", + 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 afbee0b71..c9f7d750d 100644 --- a/src/components/Tractor/farmer-orders/TractorFarmerOrderTypeRegistry.tsx +++ b/src/components/Tractor/farmer-orders/TractorFarmerOrderTypeRegistry.tsx @@ -7,7 +7,11 @@ import { SowBlueprintData, decodeSowTractorData, } from "@/lib/Tractor"; -import { decodeAutomateClaimBlueprint, transformAutomateClaimRequisitionEvent } from "@/lib/Tractor/claimOrder"; +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"; @@ -156,13 +160,9 @@ const transformAutomateClaimOrderData = (req: RequisitionEvent 0, + ...getEnabledClaimOps(transformed), operatorTip: transformed.operatorParams.operatorTipAmountAsString, }; }; diff --git a/src/components/Tractor/farmer-orders/TractorOrdersPanel.tsx b/src/components/Tractor/farmer-orders/TractorOrdersPanel.tsx index 11b55b9b3..5d66cc7a4 100644 --- a/src/components/Tractor/farmer-orders/TractorOrdersPanel.tsx +++ b/src/components/Tractor/farmer-orders/TractorOrdersPanel.tsx @@ -102,7 +102,6 @@ function TractorOrdersPanelGeneric({ const { data: automateClaimOrders, ...automateClaimOrdersQuery } = useTractorAutomateClaimOrderbook({ address, - filterOutCompleted: false, enabled: !!address && filters.orderTypes.includes("automateClaim"), }); @@ -221,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]), }); diff --git a/src/constants/address.ts b/src/constants/address.ts index 1e3674751..1764f53c3 100644 --- a/src/constants/address.ts +++ b/src/constants/address.ts @@ -23,7 +23,7 @@ export const SOW_BLUEPRINT_REFERRAL_V0_ADDRESS: HashString = "0xD9DF9C4C01604017 export const CONVERT_UP_BLUEPRINT_V0_ADDRESS: HashString = "0x5167Ae1fF37bE08D9cc9188C7e64DB228B6F06ca"; -export const AUTOMATE_CLAIM_BLUEPRINT_ADDRESS: HashString = "0x0000000000000000000000000000000000000000"; // Placeholder - update after deployment +export const AUTOMATE_CLAIM_BLUEPRINT_ADDRESS: HashString = "0xe02919865F7ea6F342F09538BE801ee4a14884CD"; // Placeholder - update after deployment export const SOW_BLUEPRINT_V0_SELECTOR = "0x3ca8e1b2" as const; diff --git a/src/hooks/tractor/useAutomateClaimOrder.ts b/src/hooks/tractor/useAutomateClaimOrder.ts index 183bcd720..0c59b47ba 100644 --- a/src/hooks/tractor/useAutomateClaimOrder.ts +++ b/src/hooks/tractor/useAutomateClaimOrder.ts @@ -65,7 +65,6 @@ export const useAutomateClaimOrder = () => { try { const formData = form.getValues(); - console.debug("[useAutomateClaimOrder] Creating blueprint with form data:", formData); // Resolve operator tip amount from preset const tipTV = getTractorOperatorTipAmountFromPreset( @@ -80,7 +79,6 @@ export const useAutomateClaimOrder = () => { } const tipPerExecution = tipTV.toBigInt(); - console.debug("[useAutomateClaimOrder] Tip per execution:", tipPerExecution.toString()); // Create tractor data and fetch latest block in parallel const [tractorData, block] = await Promise.all([ @@ -99,9 +97,7 @@ export const useAutomateClaimOrder = () => { client.getBlock({ blockTag: "latest" }), ]); - console.debug("[useAutomateClaimOrder] Tractor data created:", tractorData); - - // Create the blueprint (Req 5.2) + // Create the blueprint const blueprint = createBlueprintFromBlock({ block, publisher: address, @@ -110,8 +106,6 @@ export const useAutomateClaimOrder = () => { maxNonce: TV.MAX_UINT256.toBigInt(), }); - console.debug("[useAutomateClaimOrder] Blueprint created:", blueprint); - // Set order data for display in ReviewTractorOrderDialog setOrderData({ mowEnabled: formData.mowEnabled, 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 index d334add8c..d4a8b8f99 100644 --- a/src/lib/Tractor/claimOrder/index.ts +++ b/src/lib/Tractor/claimOrder/index.ts @@ -1,2 +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.ts b/src/lib/Tractor/claimOrder/tractor-claim.ts index ef2853a39..7a4d41b1e 100644 --- a/src/lib/Tractor/claimOrder/tractor-claim.ts +++ b/src/lib/Tractor/claimOrder/tractor-claim.ts @@ -267,17 +267,12 @@ export async function createAutomateClaimTractorData({ export function decodeAutomateClaimBlueprintFromAdvancedPipe( calls: readonly AdvancedPipeCall[] | undefined, ): AutomateClaimBlueprintStruct | null { - if (!calls?.length) { - console.debug("[Tractor/decodeAutomateClaimBlueprintFromAdvancedPipe] No calls provided. Returning null."); - return null; - } - - const callData = calls[0].callData; + if (!calls?.length) return null; try { const decoded = decodeFunctionData({ abi: automateClaimBlueprintABI, - data: callData, + data: calls[0].callData, }); const params = decoded.args?.[0]; @@ -296,14 +291,10 @@ export function decodeAutomateClaimBlueprintFromAdvancedPipe( */ export function decodeAutomateClaimBlueprint(encodedData: `0x${string}`): AutomateClaimBlueprintStruct | null { try { - console.debug("[Tractor/decodeAutomateClaimBlueprint] Attempting to decode, data length:", encodedData.length); const pipeCalls = decodeEncodedTractorDataToAdvancedPipeCalls(encodedData, "automateClaim"); - console.debug("[Tractor/decodeAutomateClaimBlueprint] Pipe calls result:", pipeCalls?.length ?? "undefined"); if (pipeCalls?.length) { - const result = decodeAutomateClaimBlueprintFromAdvancedPipe(pipeCalls); - console.debug("[Tractor/decodeAutomateClaimBlueprint] Decode result:", result ? "success" : "null"); - return result; + return decodeAutomateClaimBlueprintFromAdvancedPipe(pipeCalls); } } catch (e) { console.error("[Tractor/decodeAutomateClaimBlueprint] Failed to decode:", e); @@ -326,14 +317,12 @@ export function transformAutomateClaimRequisitionEvent( ): AutomateClaimBlueprintData | null { try { if (!params || typeof params !== "object") { - console.debug("[Tractor/transformAutomateClaimRequisitionEvent] Invalid params."); return null; } const struct = params as AutomateClaimBlueprintStruct; if (!struct.claimParams || !struct.opParams) { - console.debug("[Tractor/transformAutomateClaimRequisitionEvent] Missing claimParams or opParams."); return null; } 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 index 404bdfbf0..fe87f6c75 100644 --- a/src/state/tractor/useTractorAutomateClaimOrders.ts +++ b/src/state/tractor/useTractorAutomateClaimOrders.ts @@ -6,6 +6,7 @@ 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"; @@ -13,7 +14,6 @@ import { useChainId, usePublicClient } from "wagmi"; interface UseTractorAutomateClaimOrderbookOptions { address?: `0x${string}`; - filterOutCompleted?: boolean; enabled?: boolean; } @@ -26,14 +26,16 @@ export function useTractorAutomateClaimOrderbook({ const diamond = useProtocolAddress(); const query = useQuery({ - queryKey: ["tractor", "automateClaimOrders", address, chainId], + queryKey: queryKeys.tractor.automateClaimOrders(address, chainId), queryFn: async (): Promise[]> => { if (!client || !diamond) return []; - // Use the general deployment block to catch all events const { publishEvents, cancelledHashes } = await fetchTractorEvents(client, diamond, TRACTOR_DEPLOYMENT_BLOCK); - console.debug("[useTractorAutomateClaimOrderbook] Total publish events:", publishEvents.length); + // 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[] = []; @@ -51,47 +53,32 @@ export function useTractorAutomateClaimOrderbook({ const blueprintData = requisition.blueprint.data; if (!blueprintData) continue; - // First try the blueprint-decoders system to check if this is an automateClaim type - const blueprintDecoded = decodeBlueprintCallData(blueprintData); + // Try direct decode first, fall back to blueprint-decoders system + let decodedData = decodeAutomateClaimBlueprint(blueprintData); - // Try to decode as automateClaimBlueprint using the direct decoder - const decodedData = decodeAutomateClaimBlueprint(blueprintData); - - // Accept if either decoder identifies this as automateClaim - if (!decodedData && blueprintDecoded?.type !== "automateClaim") { - continue; + if (!decodedData) { + const blueprintDecoded = decodeBlueprintCallData(blueprintData); + if (blueprintDecoded?.type === "automateClaim" && blueprintDecoded.params) { + decodedData = blueprintDecoded.params as AutomateClaimBlueprintStruct; + } } - console.debug("[useTractorAutomateClaimOrderbook] Found automateClaim order:", { - hash: requisition.blueprintHash, - publisher: requisition.blueprint.publisher, - decodedData: !!decodedData, - blueprintDecodedType: blueprintDecoded?.type, - }); + if (!decodedData) continue; - const isCancelled = cancelledHashes.has(requisition.blueprintHash); - - // If direct decode worked, use it. Otherwise try to extract from blueprint-decoders result - let finalDecodedData = decodedData; - if (!finalDecodedData && blueprintDecoded?.type === "automateClaim" && blueprintDecoded.params) { - // The blueprint-decoders system decoded the params directly from the inner call - finalDecodedData = blueprintDecoded.params as AutomateClaimBlueprintStruct; - } - - if (!finalDecodedData) 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: Number(event.blockNumber ?? 0), - timestamp: undefined, - isCancelled, + blockNumber: eventBlockNumber, + timestamp, + isCancelled: cancelledHashes.has(requisition.blueprintHash), requisitionType: "automateClaimBlueprint", - decodedData: finalDecodedData, + decodedData, }); } - console.debug("[useTractorAutomateClaimOrderbook] Found orders:", automateClaimOrders.length); - return automateClaimOrders; }, enabled: !!client && !!diamond && enabled, diff --git a/src/state/tractor/useTractorClaimOrders.ts b/src/state/tractor/useTractorClaimOrders.ts deleted file mode 100644 index ae7b25d16..000000000 --- a/src/state/tractor/useTractorClaimOrders.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Re-export of automate claim orderbook hook. - * - * The design doc lists this module path (`useTractorClaimOrders`) as the - * canonical location for fetching & decoding automate-claim orders. - * The actual implementation lives in `useTractorAutomateClaimOrders.ts`. - */ -export { useTractorAutomateClaimOrderbook } from "./useTractorAutomateClaimOrders"; From 71d87c8440702a3bcf55449956f513502e762da9 Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:11:15 -0500 Subject: [PATCH 3/7] Fix incorrect types in automate claim blueprint --- src/constants/abi/AutomateClaimBlueprintABI.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/constants/abi/AutomateClaimBlueprintABI.ts b/src/constants/abi/AutomateClaimBlueprintABI.ts index d40ea3ec8..af2f9f1ff 100644 --- a/src/constants/abi/AutomateClaimBlueprintABI.ts +++ b/src/constants/abi/AutomateClaimBlueprintABI.ts @@ -13,7 +13,7 @@ export const automateClaimBlueprintABI = [ { internalType: "int256", name: "minTwaDeltaB", - type: "int256", + type: "uint256", }, { internalType: "uint256", @@ -48,9 +48,9 @@ export const automateClaimBlueprintABI = [ type: "uint256", }, { - internalType: "uint256[]", + internalType: "uint8[]", name: "sourceTokenIndices", - type: "uint256[]", + type: "uint8[]", }, { internalType: "uint256", @@ -84,7 +84,7 @@ export const automateClaimBlueprintABI = [ { internalType: "uint256", name: "operatorTipAmount", - type: "uint256", + type: "int256", }, ], internalType: "struct AutomateClaimBlueprint.OperatorParams", @@ -94,27 +94,27 @@ export const automateClaimBlueprintABI = [ { internalType: "uint256", name: "mowTipAmount", - type: "uint256", + type: "int256", }, { internalType: "uint256", name: "plantTipAmount", - type: "uint256", + type: "int256", }, { internalType: "uint256", name: "harvestTipAmount", - type: "uint256", + type: "int256", }, { internalType: "uint256", name: "rinseTipAmount", - type: "uint256", + type: "int256", }, { internalType: "uint256", name: "unripeClaimTipAmount", - type: "uint256", + type: "int256", }, ], internalType: "struct AutomateClaimBlueprint.OperatorParamsExtended", From 5e6de79ffbf12f2a91c4c92d157dcdb0239910d0 Mon Sep 17 00:00:00 2001 From: feyyazcigim Date: Thu, 5 Mar 2026 19:54:26 +0300 Subject: [PATCH 4/7] refactor: add memoization and replace functions --- .../AutomateClaimExecutionHistory.tsx | 19 +++--------- .../AutomateClaim/ConditionSection.tsx | 6 +++- .../FarmerTractorAutomateClaimOrderCard.tsx | 31 ++++++++++++------- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/components/Tractor/AutomateClaim/AutomateClaimExecutionHistory.tsx b/src/components/Tractor/AutomateClaim/AutomateClaimExecutionHistory.tsx index bd2bc31d7..09a67f534 100644 --- a/src/components/Tractor/AutomateClaim/AutomateClaimExecutionHistory.tsx +++ b/src/components/Tractor/AutomateClaim/AutomateClaimExecutionHistory.tsx @@ -1,20 +1,9 @@ import { TV } from "@/classes/TokenValue"; import { formatter } from "@/utils/format"; -import { format } from "date-fns"; +import { truncateAddress } from "@/utils/string"; 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"); @@ -83,9 +72,11 @@ export function AutomateClaimExecutionHistory({ executionHistory, orderData }: E {sortedExecutions.map((execution, index) => ( #{executionHistory.length - index} - {shortenAddress(execution.operator)} - {execution.timestamp ? formatDate(execution.timestamp) : `Block ${execution.blockNumber}`} + {truncateAddress(execution.operator, { suffix: true })} + + + {execution.timestamp ? formatter.dateFromTS(execution.timestamp) : `Block ${execution.blockNumber}`} onCustomValueChange(e.target.value)} + onChange={(e) => { + const sanitized = sanitizeNumericInputValue(e.target.value, 18); + onCustomValueChange(sanitized.str); + }} className={cn( "w-full text-center pinto-xs bg-transparent outline-none", isValid diff --git a/src/components/Tractor/farmer-orders/FarmerTractorAutomateClaimOrderCard.tsx b/src/components/Tractor/farmer-orders/FarmerTractorAutomateClaimOrderCard.tsx index d09210e87..22146452a 100644 --- a/src/components/Tractor/farmer-orders/FarmerTractorAutomateClaimOrderCard.tsx +++ b/src/components/Tractor/farmer-orders/FarmerTractorAutomateClaimOrderCard.tsx @@ -18,6 +18,24 @@ import { formatter } from "@/utils/format"; import { CalendarIcon, ClockIcon, Cross1Icon } from "@radix-ui/react-icons"; import { format } from "date-fns"; +function buildConditions( + transformed: NonNullable>, + ops: { mowEnabled: boolean; plantEnabled: boolean; harvestEnabled: boolean }, +) { + const result: { text: string }[] = []; + if (ops.mowEnabled) { + result.push({ text: getClaimOpConditionLabel("mow", true, transformed.claimParams.minMowAmount) }); + } + if (ops.plantEnabled) { + result.push({ text: getClaimOpConditionLabel("plant", true, transformed.claimParams.minPlantAmount) }); + } + if (ops.harvestEnabled) { + const harvestThreshold = transformed.claimParams.fieldHarvestConfigs[0]?.minHarvestAmount ?? 0n; + result.push({ text: getClaimOpConditionLabel("harvest", true, harvestThreshold) }); + } + return result; +} + interface FarmerTractorAutomateClaimOrderCardProps { req: RequisitionEvent; executions?: PublisherTractorExecution[]; @@ -45,18 +63,7 @@ const FarmerTractorAutomateClaimOrderCard = ({ 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 conditions = buildConditions(transformed, { mowEnabled, plantEnabled, harvestEnabled }); const blueprintExecutions = executions || []; const executionCount = blueprintExecutions.length; From 3482a05ad4620e9094585f8a854aa5e12aec6c18 Mon Sep 17 00:00:00 2001 From: feyyazcigim Date: Fri, 6 Mar 2026 06:26:41 +0300 Subject: [PATCH 5/7] feat: add automate claim orderbook and operator execution interface --- .../AutomateClaim/AutomateClaimContext.tsx | 42 ---- .../AutomateClaim/AutomateClaimExecute.tsx | 193 +++++++++++++++ .../AutomateClaimExecutionHistory.tsx | 52 ++-- .../AutomateClaim/AutomateClaimOrderbook.tsx | 226 ++++++++++++++++++ .../AutomateClaimVisualization.tsx | 7 +- .../AutomateClaim/SpecifyConditionsDialog.tsx | 9 +- src/components/Tractor/AutomateClaim/index.ts | 2 + .../abi/AutomateClaimBlueprintABI.ts | 14 +- src/constants/address.ts | 9 +- src/hooks/tractor/useAutomateClaimOrder.ts | 4 +- .../claimOrder/automate-claim-helpers.ts | 3 +- src/lib/Tractor/claimOrder/tractor-claim.ts | 28 ++- src/pages/Silo.tsx | 40 +++- src/pages/silo/RewardsClaim.tsx | 8 + .../tractor/useTractorAutomateClaimOrders.ts | 10 +- 15 files changed, 537 insertions(+), 110 deletions(-) delete mode 100644 src/components/Tractor/AutomateClaim/AutomateClaimContext.tsx create mode 100644 src/components/Tractor/AutomateClaim/AutomateClaimExecute.tsx create mode 100644 src/components/Tractor/AutomateClaim/AutomateClaimOrderbook.tsx diff --git a/src/components/Tractor/AutomateClaim/AutomateClaimContext.tsx b/src/components/Tractor/AutomateClaim/AutomateClaimContext.tsx deleted file mode 100644 index 3af09c2be..000000000 --- a/src/components/Tractor/AutomateClaim/AutomateClaimContext.tsx +++ /dev/null @@ -1,42 +0,0 @@ -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/AutomateClaimExecute.tsx b/src/components/Tractor/AutomateClaim/AutomateClaimExecute.tsx new file mode 100644 index 000000000..7a4d5ab67 --- /dev/null +++ b/src/components/Tractor/AutomateClaim/AutomateClaimExecute.tsx @@ -0,0 +1,193 @@ +import { TokenValue } from "@/classes/TokenValue"; +import IconImage from "@/components/ui/IconImage"; +import { PINTO } from "@/constants/tokens"; +import { TractorRequisitionEvent } from "@/lib/Tractor"; +import { + getEnabledClaimOpLabels, + getEnabledClaimOps, + transformAutomateClaimRequisitionEvent, +} from "@/lib/Tractor/claimOrder"; +import { AutomateClaimBlueprintStruct } from "@/lib/Tractor/claimOrder/tractor-claim-types"; +import { useTractorAutomateClaimOrderbook } from "@/state/tractor/useTractorAutomateClaimOrders"; +import { usePriceData } from "@/state/usePriceData"; +import useTokenData from "@/state/useTokenData"; +import { formatter } from "@/utils/format"; +import { Token } from "@/utils/types"; +import { useCallback, useMemo } from "react"; +import { ColumnConfig, ExecuteOrdersTab } from "../ExecuteOrdersTab"; + +type AutomateClaimOrder = TractorRequisitionEvent; + +const BASESCAN_URL = "https://basescan.org/address/"; + +const formatDate = formatter.dateFromTS; + +const calculateUsdValue = ( + amount: bigint | TokenValue, + pintoToken: Token, + prices: Map, +): { tokenAmount: TokenValue; usdValue: TokenValue; usdValueNumber: number } | null => { + const tokenAmount = amount instanceof TokenValue ? amount : TokenValue.fromBlockchain(amount, 6); + const pintoPrice = prices.get(pintoToken)?.instant; + if (!pintoPrice) return null; + + const usdValue = tokenAmount.mul(pintoPrice).reDecimal(6); + return { + tokenAmount, + usdValue, + usdValueNumber: Number(usdValue.toHuman()), + }; +}; + +const formatOperatorTip = ( + amount: bigint | undefined, + pintoToken: Token, + tokenPrices: Map, +): string => { + if (amount === undefined) return "Failed to decode"; + + const usdData = calculateUsdValue(amount, pintoToken, tokenPrices); + if (!usdData) { + return `${formatter.number(TokenValue.fromBlockchain(amount, 6))} PINTO`; + } + + return `${formatter.number(usdData.tokenAmount)} PINTO (${formatter.usd(usdData.usdValue)})`; +}; + +const calculateProfit = ( + order: AutomateClaimOrder, + gasEstimate: bigint, + gasPrice: bigint | undefined, + mainToken: Token, + nativeToken: Token, + tokenPrices: Map, +): number => { + if (!order.decodedData) return -Infinity; + + const ethPrice = tokenPrices.get(nativeToken)?.instant; + if (!ethPrice) return -Infinity; + + const currentGasPrice = gasPrice || BigInt(1_000_000_000); + const gasCostInWei = gasEstimate * currentGasPrice; + const gasCostInEth = Number(gasCostInWei) / 1e18; + const ethPriceInUsd = Number(ethPrice.toNumber()) / 1e6; + const gasCostInUsd = gasCostInEth * ethPriceInUsd; + + // Use the base operator tip amount + const tipData = calculateUsdValue(order.decodedData.opParams.baseOpParams.operatorTipAmount, mainToken, tokenPrices); + if (!tipData) return -Infinity; + + return tipData.usdValueNumber - gasCostInUsd; +}; + +function getEnabledOpsLabel(order: AutomateClaimOrder): string { + if (!order.decodedData) return "Unknown"; + + const transformed = transformAutomateClaimRequisitionEvent(order.decodedData); + if (!transformed) return "Unknown"; + + const ops = getEnabledClaimOps(transformed); + const labels = getEnabledClaimOpLabels(ops); + return labels.length > 0 ? labels.join(", ") : "None"; +} + +export function AutomateClaimExecute() { + const { tokenPrices } = usePriceData(); + const { mainToken, nativeToken } = useTokenData(); + + const { data: orders = [], isLoading, refetch } = useTractorAutomateClaimOrderbook(); + + const filterOrders = useCallback((orders: AutomateClaimOrder[]): AutomateClaimOrder[] => { + return orders.filter((order) => { + if (order.isCancelled) return false; + if (!order.decodedData) return false; + + const tipAmount = order.decodedData.opParams.baseOpParams.operatorTipAmount; + return tipAmount > 0n; + }); + }, []); + + const calculateOrderProfit = useCallback( + ( + order: AutomateClaimOrder, + gasEstimate: bigint, + gasPrice: bigint | undefined, + mainToken: Token, + nativeToken: Token, + tokenPrices: Map, + ): number => { + return calculateProfit(order, gasEstimate, gasPrice, mainToken, nativeToken, tokenPrices); + }, + [], + ); + + const columns: ColumnConfig[] = useMemo( + () => [ + { + header: "Created At", + className: "px-0 w-44 max-w-44", + accessor: (order) => formatDate(order.timestamp), + }, + { + header: "Publisher", + className: "w-32 max-w-32", + accessor: (order) => ( + event.stopPropagation()} + > + {`${order.requisition.blueprint.publisher.slice(0, 4)}...${order.requisition.blueprint.publisher.slice(-4)}`} + + ), + }, + { + header: "Blueprint Hash", + className: "w-32 max-w-32", + accessor: (order) => ( + + {`${order.requisition.blueprintHash.slice(0, 4)}...${order.requisition.blueprintHash.slice(-3)}`} + + ), + }, + { + header: "Enabled Ops", + className: "w-36 max-w-36", + accessor: (order) => {getEnabledOpsLabel(order)}, + }, + { + header: "Operator Tip", + className: "text-right w-44 max-w-44", + accessor: (order) => ( +
+ + + {order.decodedData + ? formatOperatorTip(order.decodedData.opParams.baseOpParams.operatorTipAmount, mainToken, tokenPrices) + : "Failed to decode"} + +
+ ), + }, + ], + [mainToken, tokenPrices], + ); + + return ( + + ); +} diff --git a/src/components/Tractor/AutomateClaim/AutomateClaimExecutionHistory.tsx b/src/components/Tractor/AutomateClaim/AutomateClaimExecutionHistory.tsx index 09a67f534..ec46e7c81 100644 --- a/src/components/Tractor/AutomateClaim/AutomateClaimExecutionHistory.tsx +++ b/src/components/Tractor/AutomateClaim/AutomateClaimExecutionHistory.tsx @@ -1,28 +1,32 @@ -import { TV } from "@/classes/TokenValue"; +import { TokenValue } from "@/classes/TokenValue"; import { formatter } from "@/utils/format"; import { truncateAddress } from "@/utils/string"; import { useMemo } from "react"; +import { useChainId, useConfig } from "wagmi"; import { AutomateClaimOrderData, ExecutionHistoryProps } from "../types"; export function AutomateClaimExecutionHistory({ executionHistory, orderData }: ExecutionHistoryProps) { - if (orderData.type !== "automateClaim") { - throw new Error("AutomateClaimExecutionHistory requires automateClaim order data"); - } - - const claimData = orderData as AutomateClaimOrderData; + const config = useConfig(); + const chainId = useChainId(); + const blockExplorerUrl = + config.chains.find((chain) => chain.id === chainId)?.blockExplorers?.default.url ?? "https://basescan.org"; const enabledOps = useMemo(() => { + if (orderData.type !== "automateClaim") return []; + const claimData = orderData as AutomateClaimOrderData; 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]); + }, [orderData]); const totalTipsPaid = useMemo(() => { - const tipAmount = claimData.operatorTip ? TV.fromHuman(claimData.operatorTip, 6) : TV.ZERO; + if (orderData.type !== "automateClaim") return TokenValue.ZERO; + const claimData = orderData as AutomateClaimOrderData; + const tipAmount = claimData.operatorTip ? TokenValue.fromHuman(claimData.operatorTip, 6) : TokenValue.ZERO; return tipAmount.mul(executionHistory.length); - }, [claimData.operatorTip, executionHistory.length]); + }, [orderData, executionHistory.length]); const sortedExecutions = useMemo( () => @@ -35,8 +39,12 @@ export function AutomateClaimExecutionHistory({ executionHistory, orderData }: E [executionHistory], ); + if (orderData.type !== "automateClaim") { + return null; + } + if (executionHistory.length === 0) { - return
No executions yet
; + return
No executions yet
; } return ( @@ -44,15 +52,15 @@ export function AutomateClaimExecutionHistory({ executionHistory, orderData }: E {/* Summary Section */}
- Total Executions + Total Executions {executionHistory.length}
- Enabled Operations + Enabled Operations {enabledOps.join(", ") || "None"}
- Total Tips Paid + Total Tips Paid {formatter.number(totalTipsPaid)} PINTO
@@ -61,26 +69,26 @@ export function AutomateClaimExecutionHistory({ executionHistory, orderData }: E
- - - - - + + + + + {sortedExecutions.map((execution, index) => ( - + - - + + void}> + + + + + View Claim Orders + + + Execute Claim Orders + + + + + + + + + + + + + + + + + ); +} + +export default AutomateClaimOrderbookDialog; diff --git a/src/components/Tractor/AutomateClaim/AutomateClaimVisualization.tsx b/src/components/Tractor/AutomateClaim/AutomateClaimVisualization.tsx index edd23fea4..32ce8749d 100644 --- a/src/components/Tractor/AutomateClaim/AutomateClaimVisualization.tsx +++ b/src/components/Tractor/AutomateClaim/AutomateClaimVisualization.tsx @@ -8,12 +8,13 @@ import { AutomateClaimOrderData, TractorOrderVisualizationProps } from "../types * Displays enabled operations (Mow, Plant, Harvest) and operator tip configuration. */ export function AutomateClaimVisualization({ orderData, className }: TractorOrderVisualizationProps) { + const mainToken = useMainToken(); + if (orderData.type !== "automateClaim") { - throw new Error("AutomateClaimVisualization requires automateClaim order data"); + return null; } const claimData = orderData as AutomateClaimOrderData; - const mainToken = useMainToken(); const operations = [ { label: "Mow", enabled: claimData.mowEnabled }, @@ -24,7 +25,7 @@ export function AutomateClaimVisualization({ orderData, className }: TractorOrde const enabledOps = operations.filter((op) => op.enabled); return ( -
+
diff --git a/src/components/Tractor/AutomateClaim/SpecifyConditionsDialog.tsx b/src/components/Tractor/AutomateClaim/SpecifyConditionsDialog.tsx index 6f32d89cc..991d8a947 100644 --- a/src/components/Tractor/AutomateClaim/SpecifyConditionsDialog.tsx +++ b/src/components/Tractor/AutomateClaim/SpecifyConditionsDialog.tsx @@ -1,3 +1,4 @@ +import { TokenValue } from "@/classes/TokenValue"; import { Col, Row } from "@/components/Container"; import { Form } from "@/components/Form"; import ReviewTractorOrderDialog from "@/components/ReviewTractorOrderDialog"; @@ -18,12 +19,10 @@ 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"; @@ -56,7 +55,6 @@ export const SpecifyConditionsDialog = ({ open, onOpenChange }: SpecifyCondition const mainToken = useMainToken(); const farmerSilo = useFarmerSilo(); const { data: averageTipPaid = 0.15 } = useTractorOperatorAverageTipPaid(); - const queryClient = useQueryClient(); // Form state const { form } = useAutomateClaimForm(); @@ -278,7 +276,6 @@ export const SpecifyConditionsDialog = ({ open, onOpenChange }: SpecifyCondition open={showReviewDialog} onOpenChange={setShowReviewDialog} onSuccess={() => { - queryClient.invalidateQueries({ queryKey: [queryKeys.base.tractor] }); form.reset({ ...defaultAutomateClaimValues }); setShowReviewDialog(false); onOpenChange(false); @@ -309,8 +306,8 @@ const EstimatedTotalTipRange = ({ mainToken: ReturnType; }) => { const formatTip = (value: bigint) => { - const human = Number(value) / 10 ** mainToken.decimals; - return formatter.number(human.toString(), { minDecimals: 2, maxDecimals: 3 }); + const tv = TokenValue.fromBlockchain(value, mainToken.decimals); + return formatter.number(tv.toHuman(), { minDecimals: 2, maxDecimals: 3 }); }; return ( diff --git a/src/components/Tractor/AutomateClaim/index.ts b/src/components/Tractor/AutomateClaim/index.ts index 04eabd7a4..e53dfe17a 100644 --- a/src/components/Tractor/AutomateClaim/index.ts +++ b/src/components/Tractor/AutomateClaim/index.ts @@ -2,3 +2,5 @@ export { SpecifyConditionsDialog } from "./SpecifyConditionsDialog"; export { ConditionSection } from "./ConditionSection"; export { AutomateClaimVisualization } from "./AutomateClaimVisualization"; export { AutomateClaimExecutionHistory } from "./AutomateClaimExecutionHistory"; +export { AutomateClaimExecute } from "./AutomateClaimExecute"; +export { AutomateClaimOrderbookDialog } from "./AutomateClaimOrderbook"; diff --git a/src/constants/abi/AutomateClaimBlueprintABI.ts b/src/constants/abi/AutomateClaimBlueprintABI.ts index af2f9f1ff..192f76836 100644 --- a/src/constants/abi/AutomateClaimBlueprintABI.ts +++ b/src/constants/abi/AutomateClaimBlueprintABI.ts @@ -11,7 +11,7 @@ export const automateClaimBlueprintABI = [ type: "uint256", }, { - internalType: "int256", + internalType: "uint256", name: "minTwaDeltaB", type: "uint256", }, @@ -82,7 +82,7 @@ export const automateClaimBlueprintABI = [ type: "address", }, { - internalType: "uint256", + internalType: "int256", name: "operatorTipAmount", type: "int256", }, @@ -92,27 +92,27 @@ export const automateClaimBlueprintABI = [ type: "tuple", }, { - internalType: "uint256", + internalType: "int256", name: "mowTipAmount", type: "int256", }, { - internalType: "uint256", + internalType: "int256", name: "plantTipAmount", type: "int256", }, { - internalType: "uint256", + internalType: "int256", name: "harvestTipAmount", type: "int256", }, { - internalType: "uint256", + internalType: "int256", name: "rinseTipAmount", type: "int256", }, { - internalType: "uint256", + internalType: "int256", name: "unripeClaimTipAmount", type: "int256", }, diff --git a/src/constants/address.ts b/src/constants/address.ts index 1764f53c3..ed8b716ec 100644 --- a/src/constants/address.ts +++ b/src/constants/address.ts @@ -19,16 +19,13 @@ export const SILO_HELPERS_ADDRESS: HashString = "0xE145082A7C5EDd1767f8148A6c29a export const SOW_BLUEPRINT_V0_ADDRESS: HashString = "0xbb0a41927895F8ca2b4ECCc659ba158735fCF28B"; -export const SOW_BLUEPRINT_REFERRAL_V0_ADDRESS: HashString = "0xD9DF9C4C0160401702de1771AAAaD886e2375F65"; // Placeholder - update after deployment - +export const SOW_BLUEPRINT_REFERRAL_V0_ADDRESS: HashString = "0xD9DF9C4C0160401702de1771AAAaD886e2375F65"; export const CONVERT_UP_BLUEPRINT_V0_ADDRESS: HashString = "0x5167Ae1fF37bE08D9cc9188C7e64DB228B6F06ca"; -export const AUTOMATE_CLAIM_BLUEPRINT_ADDRESS: HashString = "0xe02919865F7ea6F342F09538BE801ee4a14884CD"; // Placeholder - update after deployment - +export const AUTOMATE_CLAIM_BLUEPRINT_ADDRESS: HashString = "0xe02919865F7ea6F342F09538BE801ee4a14884CD"; export const SOW_BLUEPRINT_V0_SELECTOR = "0x3ca8e1b2" as const; -export const SOW_BLUEPRINT_REFERRAL_V0_SELECTOR = "0xd3936ead" as const; // Placeholder - update after deployment - +export const SOW_BLUEPRINT_REFERRAL_V0_SELECTOR = "0xd3936ead" as const; export const CONVERT_UP_BLUEPRINT_V0_SELECTOR = "0x0fd2f3ed" as const; export const AUTOMATE_CLAIM_BLUEPRINT_SELECTOR = "0xf2a6fd1a" as const; diff --git a/src/hooks/tractor/useAutomateClaimOrder.ts b/src/hooks/tractor/useAutomateClaimOrder.ts index 0c59b47ba..3a37a815b 100644 --- a/src/hooks/tractor/useAutomateClaimOrder.ts +++ b/src/hooks/tractor/useAutomateClaimOrder.ts @@ -85,6 +85,7 @@ export const useAutomateClaimOrder = () => { createAutomateClaimTractorData({ formValues: formData, tipPerExecution, + // Empty array = all operators allowed (no whitelist restriction) whitelistedOperators: [], publicClient: client, farmerDeposits: deposits, @@ -123,7 +124,8 @@ export const useAutomateClaimOrder = () => { }); options?.onSuccess?.(); - } catch (_) { + } catch (error) { + console.error("[useAutomateClaimOrder] Error creating blueprint:", error); options?.onFailure?.(); } finally { setIsLoading(false); diff --git a/src/lib/Tractor/claimOrder/automate-claim-helpers.ts b/src/lib/Tractor/claimOrder/automate-claim-helpers.ts index ba8239597..8e4c54aec 100644 --- a/src/lib/Tractor/claimOrder/automate-claim-helpers.ts +++ b/src/lib/Tractor/claimOrder/automate-claim-helpers.ts @@ -1,3 +1,4 @@ +import { TokenValue } from "@/classes/TokenValue"; import { maxUint256 } from "viem"; import { CLAIM_PRESETS } from "./tractor-claim"; import type { AutomateClaimBlueprintData } from "./tractor-claim-types"; @@ -54,7 +55,7 @@ export function getClaimOpConditionLabel( // Custom value const decimals = op === "mow" ? 10 : 6; - const humanValue = Number(thresholdBigInt) / 10 ** decimals; + const humanValue = TokenValue.fromBlockchain(thresholdBigInt, decimals).toHuman(); return `${opLabel}: Custom (≥ ${humanValue} ${unit})`; } diff --git a/src/lib/Tractor/claimOrder/tractor-claim.ts b/src/lib/Tractor/claimOrder/tractor-claim.ts index 7a4d41b1e..30398e4e8 100644 --- a/src/lib/Tractor/claimOrder/tractor-claim.ts +++ b/src/lib/Tractor/claimOrder/tractor-claim.ts @@ -3,7 +3,7 @@ import { automateClaimBlueprintABI } from "@/constants/abi/AutomateClaimBlueprin 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 { type PublicClient, decodeFunctionData, encodeFunctionData, maxUint256 } from "viem"; import { base } from "viem/chains"; import { CreateTractorDataReturnType, @@ -68,10 +68,10 @@ export function resolvePresetValue( if (operation === "mow") { // Stalk: value × 1e10 - return BigInt(Math.round(numericValue * 1e10)); + return TokenValue.fromHuman(customValue, 10).toBigInt(); } // PINTO: value × 1e6 - return BigInt(Math.round(numericValue * 1e6)); + return TokenValue.fromHuman(customValue, 6).toBigInt(); } /** @@ -161,11 +161,11 @@ export function buildAutomateClaimStruct( const sourceTokenIndices = formValues.sourceTokenIndices ?? [255]; const maxGrownStalkPerBdv = formValues.maxGrownStalkPerBdv - ? BigInt(Math.round(parseFloat(formValues.maxGrownStalkPerBdv) * 1e6)) - : BigInt(1e18); + ? TokenValue.fromHuman(formValues.maxGrownStalkPerBdv, 6).toBigInt() + : TokenValue.fromHuman("1", 18).toBigInt(); const slippageRatio = formValues.slippageRatio - ? BigInt(Math.round(parseFloat(formValues.slippageRatio) * 1e18)) - : BigInt(1e18); + ? TokenValue.fromHuman(formValues.slippageRatio, 18).toBigInt() + : TokenValue.fromHuman("1", 18).toBigInt(); return { claimParams: { @@ -250,6 +250,7 @@ export async function createAutomateClaimTractorData({ return { data, + // AutomateClaim uses transient storage (getTractorData) for operator-provided data, not paste instructions operatorPasteInstrs: [], rawCall: blueprintCall, depositOptimizationCalls, @@ -329,23 +330,26 @@ export function transformAutomateClaimRequisitionEvent( const cp = struct.claimParams; const op = struct.opParams; + const humanOrDisabled = (value: bigint, decimals: number): string => + value === maxUint256 ? "Disabled" : TokenValue.fromBlockchain(value, decimals).toHuman(); + return { claimParams: { minMowAmount: cp.minMowAmount, - minMowAmountAsString: TokenValue.fromBlockchain(cp.minMowAmount, 10).toHuman(), + minMowAmountAsString: humanOrDisabled(cp.minMowAmount, 10), minTwaDeltaB: cp.minTwaDeltaB, minTwaDeltaBAsString: TokenValue.fromBlockchain(cp.minTwaDeltaB, 6).toHuman(), minPlantAmount: cp.minPlantAmount, - minPlantAmountAsString: TokenValue.fromBlockchain(cp.minPlantAmount, 6).toHuman(), + minPlantAmountAsString: humanOrDisabled(cp.minPlantAmount, 6), fieldHarvestConfigs: cp.fieldHarvestConfigs.map((config) => ({ fieldId: config.fieldId, minHarvestAmount: config.minHarvestAmount, - minHarvestAmountAsString: TokenValue.fromBlockchain(config.minHarvestAmount, 6).toHuman(), + minHarvestAmountAsString: humanOrDisabled(config.minHarvestAmount, 6), })), minRinseAmount: cp.minRinseAmount, - minRinseAmountAsString: TokenValue.fromBlockchain(cp.minRinseAmount, 6).toHuman(), + minRinseAmountAsString: humanOrDisabled(cp.minRinseAmount, 6), minUnripeClaimAmount: cp.minUnripeClaimAmount, - minUnripeClaimAmountAsString: TokenValue.fromBlockchain(cp.minUnripeClaimAmount, 6).toHuman(), + minUnripeClaimAmountAsString: humanOrDisabled(cp.minUnripeClaimAmount, 6), sourceTokenIndices: cp.sourceTokenIndices, maxGrownStalkPerBdv: cp.maxGrownStalkPerBdv, maxGrownStalkPerBdvAsString: TokenValue.fromBlockchain(cp.maxGrownStalkPerBdv, 6).toHuman(), diff --git a/src/pages/Silo.tsx b/src/pages/Silo.tsx index 0b4539b4a..474bbc5b0 100644 --- a/src/pages/Silo.tsx +++ b/src/pages/Silo.tsx @@ -3,7 +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 { AutomateClaimOrderbookDialog, 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"; @@ -42,6 +42,7 @@ function Silo() { const [showConvertUpOrderDialog, setShowConvertUpOrderDialog] = useState(false); const [showAutomateClaimDialog, setShowAutomateClaimDialog] = useState(false); + const [showAutomateClaimOrderbook, setShowAutomateClaimOrderbook] = useState(false); const [hoveredButton, setHoveredButton] = useState("claim"); const enableStatPanels = farmerSilo.depositsUSD.gt(0) || farmerSilo.activeStalkBalance.gt(0) || farmerSilo.activeSeedsBalance.gt(0); @@ -158,12 +159,36 @@ function Silo() {
These are Deposits which are currently incentivized by Pinto.
- setShowAutomateClaimDialog(true)} - > - Automate your Silo with Tractor 🚜 - +
+ setShowAutomateClaimOrderbook(true)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setShowAutomateClaimOrderbook(true); + } + }} + > + View Claim Orders + + setShowAutomateClaimDialog(true)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setShowAutomateClaimDialog(true); + } + }} + > + Automate your Silo with Tractor 🚜 + +
@@ -192,6 +217,7 @@ function Silo() { + ); } diff --git a/src/pages/silo/RewardsClaim.tsx b/src/pages/silo/RewardsClaim.tsx index 26a25462d..2a2fcac36 100644 --- a/src/pages/silo/RewardsClaim.tsx +++ b/src/pages/silo/RewardsClaim.tsx @@ -100,8 +100,16 @@ function RewardsClaim() { {"Claim Rewards"} setShowAutomateClaimDialog(true)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setShowAutomateClaimDialog(true); + } + }} > Automate with Tractor diff --git a/src/state/tractor/useTractorAutomateClaimOrders.ts b/src/state/tractor/useTractorAutomateClaimOrders.ts index fe87f6c75..781ca48ac 100644 --- a/src/state/tractor/useTractorAutomateClaimOrders.ts +++ b/src/state/tractor/useTractorAutomateClaimOrders.ts @@ -4,7 +4,7 @@ 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 { TRACTOR_DEPLOYMENT_BLOCK_AUTOMATE_CLAIM } from "@/lib/Tractor/core"; import { fetchTractorEvents } from "@/lib/Tractor/events/tractor-events"; import { queryKeys } from "@/state/queryKeys"; import { stringEq } from "@/utils/string"; @@ -30,7 +30,11 @@ export function useTractorAutomateClaimOrderbook({ queryFn: async (): Promise[]> => { if (!client || !diamond) return []; - const { publishEvents, cancelledHashes } = await fetchTractorEvents(client, diamond, TRACTOR_DEPLOYMENT_BLOCK); + const { publishEvents, cancelledHashes } = await fetchTractorEvents( + client, + diamond, + TRACTOR_DEPLOYMENT_BLOCK_AUTOMATE_CLAIM, + ); // Get latest block for timestamp approximation const latestBlock = await client.getBlock({ blockTag: "latest" }); @@ -87,7 +91,7 @@ export function useTractorAutomateClaimOrderbook({ const refetch = useCallback(async () => { return query.refetch(); - }, [query]); + }, [query.refetch]); return { ...query, refetch } as const; } From 1f378f37d07acd2e301a8e316713d595ea83ded8 Mon Sep 17 00:00:00 2001 From: feyyazcigim Date: Fri, 6 Mar 2026 20:43:24 +0300 Subject: [PATCH 6/7] fix(tractor): improve order flow, explorer links, and decimal handling --- src/components/ReviewTractorOrderDialog.tsx | 8 +++- .../AutomateClaim/AutomateClaimExecute.tsx | 12 +++-- .../AutomateClaim/AutomateClaimOrderbook.tsx | 10 ++-- .../AutomateClaim/ConditionSection.tsx | 1 + .../farmer-orders/TractorOrdersPanel.tsx | 6 ++- .../claimOrder/automate-claim-helpers.ts | 6 +-- src/lib/Tractor/claimOrder/tractor-claim.ts | 47 +++++++++++-------- 7 files changed, 59 insertions(+), 31 deletions(-) diff --git a/src/components/ReviewTractorOrderDialog.tsx b/src/components/ReviewTractorOrderDialog.tsx index 437ec20e5..cf2a96508 100644 --- a/src/components/ReviewTractorOrderDialog.tsx +++ b/src/components/ReviewTractorOrderDialog.tsx @@ -160,7 +160,13 @@ export default function ReviewTractorOrderDialog({ } // Invalidate all tractor queries to refresh order lists - queryClient.invalidateQueries({ queryKey: [queryKeys.base.tractor] }); + queryClient.invalidateQueries({ queryKey: queryKeys.base.tractor }); + + // Close the dialog + onOpenChange(false); + + // Show success toast + toast.success("Order published successfully"); // Navigate to the Field page with tractor tab active if (orderData.type === "sow") { diff --git a/src/components/Tractor/AutomateClaim/AutomateClaimExecute.tsx b/src/components/Tractor/AutomateClaim/AutomateClaimExecute.tsx index 7a4d5ab67..cdd7c1e86 100644 --- a/src/components/Tractor/AutomateClaim/AutomateClaimExecute.tsx +++ b/src/components/Tractor/AutomateClaim/AutomateClaimExecute.tsx @@ -14,12 +14,11 @@ import useTokenData from "@/state/useTokenData"; import { formatter } from "@/utils/format"; import { Token } from "@/utils/types"; import { useCallback, useMemo } from "react"; +import { useChainId, useConfig } from "wagmi"; import { ColumnConfig, ExecuteOrdersTab } from "../ExecuteOrdersTab"; type AutomateClaimOrder = TractorRequisitionEvent; -const BASESCAN_URL = "https://basescan.org/address/"; - const formatDate = formatter.dateFromTS; const calculateUsdValue = ( @@ -92,6 +91,11 @@ function getEnabledOpsLabel(order: AutomateClaimOrder): string { } export function AutomateClaimExecute() { + const config = useConfig(); + const chainId = useChainId(); + const blockExplorerUrl = + config.chains.find((chain) => chain.id === chainId)?.blockExplorers?.default.url ?? "https://basescan.org"; + const { tokenPrices } = usePriceData(); const { mainToken, nativeToken } = useTokenData(); @@ -133,7 +137,7 @@ export function AutomateClaimExecute() { className: "w-32 max-w-32", accessor: (order) => ( ; - -const BASESCAN_URL = "https://basescan.org/address/"; const formatDate = formatter.dateFromTS; function AutomateClaimOrderbookContent() { + const config = useConfig(); + const chainId = useChainId(); + const blockExplorerUrl = + config.chains.find((chain) => chain.id === chainId)?.blockExplorers?.default.url ?? "https://basescan.org"; + const [selectedOrder, setSelectedOrder] = useState(null); const [reviewDialogOpen, setReviewDialogOpen] = useState(false); @@ -101,7 +105,7 @@ function AutomateClaimOrderbookContent() { { + // 18 = max decimal precision to accommodate all token types (Stalk=10, PINTO=6) const sanitized = sanitizeNumericInputValue(e.target.value, 18); onCustomValueChange(sanitized.str); }} diff --git a/src/components/Tractor/farmer-orders/TractorOrdersPanel.tsx b/src/components/Tractor/farmer-orders/TractorOrdersPanel.tsx index 5d66cc7a4..b430002ba 100644 --- a/src/components/Tractor/farmer-orders/TractorOrdersPanel.tsx +++ b/src/components/Tractor/farmer-orders/TractorOrdersPanel.tsx @@ -174,7 +174,11 @@ function TractorOrdersPanelGeneric({ const reqWithDecodedData = { ...req, decodedData }; const orderExecutions = executionsByHash?.[req.requisition.blueprintHash] || []; - unified.push(transformAutomateClaimOrderToUnified(reqWithDecodedData, orderExecutions)); + try { + unified.push(transformAutomateClaimOrderToUnified(reqWithDecodedData, orderExecutions)); + } catch (error) { + console.error("Failed to transform AutomateClaim order:", error); + } }); } diff --git a/src/lib/Tractor/claimOrder/automate-claim-helpers.ts b/src/lib/Tractor/claimOrder/automate-claim-helpers.ts index 8e4c54aec..02b11b7bd 100644 --- a/src/lib/Tractor/claimOrder/automate-claim-helpers.ts +++ b/src/lib/Tractor/claimOrder/automate-claim-helpers.ts @@ -1,6 +1,7 @@ import { TokenValue } from "@/classes/TokenValue"; +import { PINTO } from "@/constants/tokens"; import { maxUint256 } from "viem"; -import { CLAIM_PRESETS } from "./tractor-claim"; +import { CLAIM_PRESETS, MOW_DECIMALS } from "./tractor-claim"; import type { AutomateClaimBlueprintData } from "./tractor-claim-types"; /** @@ -53,8 +54,7 @@ export function getClaimOpConditionLabel( return `${opLabel}: Conservative (≥ ${presets.low.value} ${unit})`; } - // Custom value - const decimals = op === "mow" ? 10 : 6; + const decimals = op === "mow" ? MOW_DECIMALS : PINTO.decimals; const humanValue = TokenValue.fromBlockchain(thresholdBigInt, decimals).toHuman(); return `${opLabel}: Custom (≥ ${humanValue} ${unit})`; } diff --git a/src/lib/Tractor/claimOrder/tractor-claim.ts b/src/lib/Tractor/claimOrder/tractor-claim.ts index 30398e4e8..4942b8dff 100644 --- a/src/lib/Tractor/claimOrder/tractor-claim.ts +++ b/src/lib/Tractor/claimOrder/tractor-claim.ts @@ -1,6 +1,7 @@ import { TokenValue } from "@/classes/TokenValue"; import { automateClaimBlueprintABI } from "@/constants/abi/AutomateClaimBlueprintABI"; import { AUTOMATE_CLAIM_BLUEPRINT_ADDRESS } from "@/constants/address"; +import { PINTO } from "@/constants/tokens"; import { useFarmerSilo } from "@/state/useFarmerSilo"; import { AdvancedPipeCall } from "@/utils/types"; import { type PublicClient, decodeFunctionData, encodeFunctionData, maxUint256 } from "viem"; @@ -17,6 +18,13 @@ import { FieldHarvestConfig, } from "./tractor-claim-types"; +// ──────────────────────────────────────────────────────────────────────────────── +// DECIMALS +// ──────────────────────────────────────────────────────────────────────────────── + +/** Mow thresholds are stored on-chain with 10 decimal places (Stalk precision) */ +export const MOW_DECIMALS = 10; + // ──────────────────────────────────────────────────────────────────────────────── // PRESET VALUES // ──────────────────────────────────────────────────────────────────────────────── @@ -48,8 +56,8 @@ const DEFAULT_MIN_TWA_DELTA_B = 10_000_000n; /** * 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) + * - Mow: scaled by MOW_DECIMALS (stalk precision) + * - Plant/Harvest: scaled by PINTO.decimals */ export function resolvePresetValue( operation: "mow" | "plant" | "harvest", @@ -67,11 +75,9 @@ export function resolvePresetValue( } if (operation === "mow") { - // Stalk: value × 1e10 - return TokenValue.fromHuman(customValue, 10).toBigInt(); + return TokenValue.fromHuman(customValue, MOW_DECIMALS).toBigInt(); } - // PINTO: value × 1e6 - return TokenValue.fromHuman(customValue, 6).toBigInt(); + return TokenValue.fromHuman(customValue, PINTO.decimals).toBigInt(); } /** @@ -336,23 +342,23 @@ export function transformAutomateClaimRequisitionEvent( return { claimParams: { minMowAmount: cp.minMowAmount, - minMowAmountAsString: humanOrDisabled(cp.minMowAmount, 10), + minMowAmountAsString: humanOrDisabled(cp.minMowAmount, MOW_DECIMALS), minTwaDeltaB: cp.minTwaDeltaB, - minTwaDeltaBAsString: TokenValue.fromBlockchain(cp.minTwaDeltaB, 6).toHuman(), + minTwaDeltaBAsString: TokenValue.fromBlockchain(cp.minTwaDeltaB, PINTO.decimals).toHuman(), minPlantAmount: cp.minPlantAmount, - minPlantAmountAsString: humanOrDisabled(cp.minPlantAmount, 6), + minPlantAmountAsString: humanOrDisabled(cp.minPlantAmount, PINTO.decimals), fieldHarvestConfigs: cp.fieldHarvestConfigs.map((config) => ({ fieldId: config.fieldId, minHarvestAmount: config.minHarvestAmount, - minHarvestAmountAsString: humanOrDisabled(config.minHarvestAmount, 6), + minHarvestAmountAsString: humanOrDisabled(config.minHarvestAmount, PINTO.decimals), })), minRinseAmount: cp.minRinseAmount, - minRinseAmountAsString: humanOrDisabled(cp.minRinseAmount, 6), + minRinseAmountAsString: humanOrDisabled(cp.minRinseAmount, PINTO.decimals), minUnripeClaimAmount: cp.minUnripeClaimAmount, - minUnripeClaimAmountAsString: humanOrDisabled(cp.minUnripeClaimAmount, 6), + minUnripeClaimAmountAsString: humanOrDisabled(cp.minUnripeClaimAmount, PINTO.decimals), sourceTokenIndices: cp.sourceTokenIndices, maxGrownStalkPerBdv: cp.maxGrownStalkPerBdv, - maxGrownStalkPerBdvAsString: TokenValue.fromBlockchain(cp.maxGrownStalkPerBdv, 6).toHuman(), + maxGrownStalkPerBdvAsString: TokenValue.fromBlockchain(cp.maxGrownStalkPerBdv, PINTO.decimals).toHuman(), slippageRatio: cp.slippageRatio, slippageRatioAsString: TokenValue.fromBlockchain(cp.slippageRatio, 18).toHuman(), }, @@ -360,17 +366,20 @@ export function transformAutomateClaimRequisitionEvent( whitelistedOperators: op.baseOpParams.whitelistedOperators, tipAddress: op.baseOpParams.tipAddress, operatorTipAmount: op.baseOpParams.operatorTipAmount, - operatorTipAmountAsString: TokenValue.fromBlockchain(op.baseOpParams.operatorTipAmount, 6).toHuman(), + operatorTipAmountAsString: TokenValue.fromBlockchain( + op.baseOpParams.operatorTipAmount, + PINTO.decimals, + ).toHuman(), mowTipAmount: op.mowTipAmount, - mowTipAmountAsString: TokenValue.fromBlockchain(op.mowTipAmount, 6).toHuman(), + mowTipAmountAsString: TokenValue.fromBlockchain(op.mowTipAmount, PINTO.decimals).toHuman(), plantTipAmount: op.plantTipAmount, - plantTipAmountAsString: TokenValue.fromBlockchain(op.plantTipAmount, 6).toHuman(), + plantTipAmountAsString: TokenValue.fromBlockchain(op.plantTipAmount, PINTO.decimals).toHuman(), harvestTipAmount: op.harvestTipAmount, - harvestTipAmountAsString: TokenValue.fromBlockchain(op.harvestTipAmount, 6).toHuman(), + harvestTipAmountAsString: TokenValue.fromBlockchain(op.harvestTipAmount, PINTO.decimals).toHuman(), rinseTipAmount: op.rinseTipAmount, - rinseTipAmountAsString: TokenValue.fromBlockchain(op.rinseTipAmount, 6).toHuman(), + rinseTipAmountAsString: TokenValue.fromBlockchain(op.rinseTipAmount, PINTO.decimals).toHuman(), unripeClaimTipAmount: op.unripeClaimTipAmount, - unripeClaimTipAmountAsString: TokenValue.fromBlockchain(op.unripeClaimTipAmount, 6).toHuman(), + unripeClaimTipAmountAsString: TokenValue.fromBlockchain(op.unripeClaimTipAmount, PINTO.decimals).toHuman(), }, }; } catch (e) { From 16f808edef4ff6720ab2f465c9892e74ca43964d Mon Sep 17 00:00:00 2001 From: feyyazcigim Date: Fri, 13 Mar 2026 20:00:19 +0300 Subject: [PATCH 7/7] refactor(tractor): simplify transaction flow in review dialog --- src/components/ReviewTractorOrderDialog.tsx | 50 ++++++++------------- 1 file changed, 18 insertions(+), 32 deletions(-) diff --git a/src/components/ReviewTractorOrderDialog.tsx b/src/components/ReviewTractorOrderDialog.tsx index cf2a96508..217a9401a 100644 --- a/src/components/ReviewTractorOrderDialog.tsx +++ b/src/components/ReviewTractorOrderDialog.tsx @@ -12,7 +12,7 @@ import { useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { toast } from "sonner"; import { encodeFunctionData } from "viem"; -import { useAccount, usePublicClient } from "wagmi"; +import { useAccount } from "wagmi"; import { Col, Row } from "./Container"; import LoadingSpinner from "./LoadingSpinner"; import { HighlightedCallData } from "./Tractor/HighlightedCallData"; @@ -67,7 +67,6 @@ export default function ReviewTractorOrderDialog({ 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 @@ -85,8 +84,22 @@ export default function ReviewTractorOrderDialog({ successMessage: "Order published successfully", errorMessage: "Failed to publish order", successCallback: () => { - // Close the dialog after successful submission + // Invalidate all tractor queries to refresh order lists + queryClient.invalidateQueries({ queryKey: queryKeys.base.tractor }); + + // Close the dialog onOpenChange(false); + + // Navigate to the Field page with tractor tab active + if (orderData.type === "sow") { + navigate("/field?tab=tractor"); + } + + // Call the parent success callback to refresh data + onSuccess?.(); + + // Call the onOrderPublished callback if provided + onOrderPublished?.(); }, }); @@ -118,7 +131,6 @@ 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) { @@ -136,7 +148,7 @@ export default function ReviewTractorOrderDialog({ const farmCalls = [...depositOptimizationCalls, publishRequisitionCall]; // Execute as farm call - txHash = await writeWithEstimateGas({ + await writeWithEstimateGas({ address: protocolAddress, abi: diamondABI, functionName: "farm", @@ -146,41 +158,15 @@ export default function ReviewTractorOrderDialog({ console.debug("Publishing requisition without deposit optimization"); // Call publish requisition directly (like before) - txHash = await writeWithEstimateGas({ + await writeWithEstimateGas({ address: protocolAddress, abi: diamondABI, functionName: "publishRequisition", args: [signedRequisition], }); } - - // Wait for tx confirmation before refreshing data - if (txHash && publicClient) { - await publicClient.waitForTransactionReceipt({ hash: txHash, confirmations: 1 }); - } - - // Invalidate all tractor queries to refresh order lists - queryClient.invalidateQueries({ queryKey: queryKeys.base.tractor }); - - // Close the dialog - onOpenChange(false); - - // Show success toast - toast.success("Order published successfully"); - - // Navigate to the Field page with tractor tab active - if (orderData.type === "sow") { - navigate("/field?tab=tractor"); - } - - // Call the parent success callback to refresh data - onSuccess?.(); - - // Call the onOrderPublished callback if provided - onOrderPublished?.(); } catch (error) { console.error("Error publishing requisition:", error); - } finally { setSubmitting(false); } };
ExecutionOperatorDate & TimeActions
ExecutionOperatorDate & TimeActions
#{executionHistory.length - index} + {truncateAddress(execution.operator, { suffix: true })} + {execution.timestamp ? formatter.dateFromTS(execution.timestamp) : `Block ${execution.blockNumber}`} ; + +const BASESCAN_URL = "https://basescan.org/address/"; +const formatDate = formatter.dateFromTS; + +function AutomateClaimOrderbookContent() { + const [selectedOrder, setSelectedOrder] = useState(null); + const [reviewDialogOpen, setReviewDialogOpen] = useState(false); + + const { data: orders = [], isLoading } = useTractorAutomateClaimOrderbook(); + + const handleRowClick = (order: AutomateClaimOrder) => { + if (!order.decodedData) return; + const transformed = transformAutomateClaimRequisitionEvent(order.decodedData); + if (!transformed) return; + + setSelectedOrder(order); + setReviewDialogOpen(true); + }; + + const getSortedOrders = useCallback(() => { + return [...orders] + .filter((order) => !order.isCancelled && order.decodedData) + .sort((a, b) => { + if (!a.decodedData || !b.decodedData) return 0; + const aTip = a.decodedData.opParams.baseOpParams.operatorTipAmount; + const bTip = b.decodedData.opParams.baseOpParams.operatorTipAmount; + if (bTip > aTip) return 1; + if (bTip < aTip) return -1; + return 0; + }); + }, [orders]); + + const sortedOrders = getSortedOrders(); + + const renderOrderRow = (order: AutomateClaimOrder) => { + if (!order.decodedData) return null; + + const transformed = transformAutomateClaimRequisitionEvent(order.decodedData); + if (!transformed) return null; + + const ops = getEnabledClaimOps(transformed); + const enabledLabels = getEnabledClaimOpLabels(ops); + const operatorTip = formatter.number(TokenValue.fromBlockchain(transformed.operatorParams.operatorTipAmount, 6), { + minDecimals: 2, + maxDecimals: 2, + }); + + const conditions: string[] = []; + if (ops.mowEnabled) { + conditions.push(getClaimOpConditionLabel("mow", true, transformed.claimParams.minMowAmount)); + } + if (ops.plantEnabled) { + conditions.push(getClaimOpConditionLabel("plant", true, transformed.claimParams.minPlantAmount)); + } + if (ops.harvestEnabled) { + const harvestThreshold = transformed.claimParams.fieldHarvestConfigs[0]?.minHarvestAmount ?? 0n; + conditions.push(getClaimOpConditionLabel("harvest", true, harvestThreshold)); + } + + return ( + handleRowClick(order)} + > + {enabledLabels.join(", ") || "None"} + + {conditions.join(" | ") || "-"} + + +
+ + {operatorTip} +
+
+ + {`0x${order.requisition.blueprintHash.slice(2, 7)}...${order.requisition.blueprintHash.slice(-4)}`} + + +
e.stopPropagation()} + > + {`0x${order.requisition.blueprint.publisher.slice(2, 6)}...${order.requisition.blueprint.publisher.slice(-3)}`} + + + {formatDate(order.timestamp)} + + ); + }; + + return ( +
+ + + + Enabled Ops + Conditions + Operator Tip + Blueprint Hash + Publisher + Created At + + + + {sortedOrders.map((order) => renderOrderRow(order))} + {sortedOrders.length === 0 && ( + + + {isLoading ? ( +
+ + Loading automate claim orders... +
+ ) : ( + "No active automate claim orders found" + )} +
+
+ )} +
+
+ + {selectedOrder?.decodedData && + (() => { + const transformed = transformAutomateClaimRequisitionEvent(selectedOrder.decodedData); + if (!transformed) return null; + const ops = getEnabledClaimOps(transformed); + return ( + + ); + })()} +
+ ); +} + +interface AutomateClaimOrderbookDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function AutomateClaimOrderbookDialog({ open, onOpenChange }: AutomateClaimOrderbookDialogProps) { + const [activeTab, setActiveTab] = useState<"view" | "execute">("view"); + + return ( + + + + + + Automate Claim Orders + +