Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f4ae165
Implement new tractor order form
feyyazcigim Dec 7, 2025
6dfb3f1
Update ModifySowOrderDialog based on new implementation
feyyazcigim Dec 7, 2025
b6863aa
Fix slider bugs, change validation
feyyazcigim Dec 8, 2025
869dc67
Update tractor order form tooltips and labels
claude[bot] Dec 8, 2025
52a5818
refactor: Extract shared Tractor order form logic into reusable hooks
fr1jo Dec 8, 2025
1dc2b63
refactor: Consolidate shared Tractor order form logic and improve cod…
fr1jo Dec 8, 2025
a6c470a
refactor: Consolidate Sow order estimated tip paid calculation logic
fr1jo Dec 8, 2025
ec1261a
refactor: Move cultivation factor logic and math utilities to shared …
fr1jo Dec 8, 2025
a6fd379
refactor: Consolidate Sow order form logic with shared hooks and impr…
fr1jo Dec 8, 2025
e8b7656
refactor: Update SowOrderV0Fields to use shared estimation and calcul…
fr1jo Dec 8, 2025
b39023e
fix: extract select function to stable reference in useCultivationFactor
github-actions[bot] Dec 8, 2025
5a0ae3c
Merge pull request #339 from pinto-org/frijo/tractor-order-refactor
fr1jo Dec 8, 2025
44dfc8d
Revert error handling for advanced form
feyyazcigim Dec 8, 2025
5d99600
Merge branch 'tractor-order-refactor' of https://github.com/pinto-org…
feyyazcigim Dec 8, 2025
317f5cb
Add morning auction row to summary
feyyazcigim Dec 8, 2025
87dbbc2
Fix auto fill on temperature input
feyyazcigim Dec 10, 2025
5db2afe
Fix type and change tooltips
feyyazcigim Dec 10, 2025
28ed77d
Update Morning tooltip text with detailed explanation
github-actions[bot] Dec 11, 2025
421072b
chore: auto-format and lint code
github-actions[bot] Dec 11, 2025
7b00dfc
Update tractor order form tooltips
github-actions[bot] Dec 11, 2025
d32601e
chore: auto-format and lint code
github-actions[bot] Dec 11, 2025
15eab67
Improve error handling
feyyazcigim Dec 11, 2025
c9e4274
Fix visual bug on total amount slider
feyyazcigim Dec 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
421 changes: 333 additions & 88 deletions src/components/SowOrderDialog.tsx

Large diffs are not rendered by default.

361 changes: 277 additions & 84 deletions src/components/Tractor/ModifySowOrderDialog.tsx

Large diffs are not rendered by default.

129 changes: 129 additions & 0 deletions src/components/Tractor/Sow/SowOrderEstimatedTipPaid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { TV } from "@/classes/TokenValue";
import { Row } from "@/components/Container";
import TooltipSimple from "@/components/TooltipSimple";
import IconImage from "@/components/ui/IconImage";
import { DEFAULT_DELTA, INITIAL_CULTIVATION_FACTOR } from "@/constants/calculations";
import { useCultivationFactor } from "@/hooks/pinto/useCultivationFactor";
import { useInitialSoil } from "@/state/useFieldData";
import { usePriceData } from "@/state/usePriceData";
import { useMainToken } from "@/state/useTokenData";
import { formatter } from "@/utils/format";
import { solveArithmeticSeriesForN } from "@/utils/math";
import { postSanitizedSanitizedValue } from "@/utils/string";
import { useMemo } from "react";
import { useFormContext, useWatch } from "react-hook-form";
import { SowOrderV0FormSchema } from "../form/SowOrderV0Schema";
import { TractorOperatorTipStrategy, getTractorOperatorTipAmountFromPreset } from "../form/fields/sharedFields";

interface SowOrderEstimatedTipPaidProps {
averageTipPaid: number;
operatorTipPreset: TractorOperatorTipStrategy;
}

export const SowOrderEstimatedTipPaid = ({ averageTipPaid, operatorTipPreset }: SowOrderEstimatedTipPaidProps) => {
const mainToken = useMainToken();
const form = useFormContext<SowOrderV0FormSchema>();

// Fetch data for accurate arithmetic series calculation
const { data: cultivationFactor, isLoading: isCultivationLoading } = useCultivationFactor();
const { initialSoil, isLoading: isInitialSoilLoading } = useInitialSoil();
const { price: pintoPrice } = usePriceData();

const [operatorTip, customOperatorTip, maxPerSeason, minSoil, totalAmount] = useWatch({
control: form.control,
name: ["operatorTip", "customOperatorTip", "maxPerSeason", "minSoil", "totalAmount"],
}) as [string | undefined, string | undefined, string, string, string];

const tipEstimations = useMemo(() => {
const total = postSanitizedSanitizedValue(totalAmount ?? "", mainToken.decimals).tv;
const max = postSanitizedSanitizedValue(maxPerSeason ?? "", mainToken.decimals).tv;
const min = postSanitizedSanitizedValue(minSoil ?? "", mainToken.decimals).tv;

// Calculate tip from preset (same as OperatorTipPresetDropdown does)
// Use customOperatorTip when preset is Custom, otherwise use operatorTip
const tipAmount = operatorTipPreset === "Custom" ? customOperatorTip : operatorTip;
const tip =
getTractorOperatorTipAmountFromPreset(operatorTipPreset, averageTipPaid, tipAmount, mainToken.decimals) ??
TV.ZERO;

if (total.eq(0) || tip.eq(0)) {
return {
min: TV.ZERO,
max: TV.ZERO,
};
}

// Min executions = total / maxPerSeason (fewer executions = lower tip)
const minTimes = max.gt(0) ? total.div(max) : TV.ZERO;

// Max executions using accurate arithmetic series calculation
let maxTimes: TV;

// Check if we have all required data for accurate calculation
if (!cultivationFactor || !initialSoil || isCultivationLoading || isInitialSoilLoading || !pintoPrice) {
// Fallback to simple division while loading or if data unavailable
maxTimes = min.gt(0) ? total.div(min) : TV.ZERO;
} else {
// Calculate initial value: initialSoil * INITIAL_CULTIVATION_FACTOR / cultivationFactor
const initialValue = initialSoil.mul(INITIAL_CULTIVATION_FACTOR).div(cultivationFactor);

// Calculate delta: (DEFAULT_DELTA * initialValue / 1e6) * pintoPrice / 1e6
// Note: pintoPrice is TokenValue with 6 decimals, so divide by 1e6 to normalize
const delta = initialValue.mul(DEFAULT_DELTA).div(1e6).mul(pintoPrice).div(1e6);

// Solve for number of executions using arithmetic series
const maxExecutions = solveArithmeticSeriesForN(total, initialValue, delta);

// Convert number to TokenValue
maxTimes = TV.fromHuman(maxExecutions, mainToken.decimals);
}

return {
min: minTimes.mul(tip),
max: maxTimes.mul(tip),
};
}, [
operatorTip,
customOperatorTip,
maxPerSeason,
minSoil,
totalAmount,
operatorTipPreset,
averageTipPaid,
mainToken.decimals,
cultivationFactor,
initialSoil,
pintoPrice,
isCultivationLoading,
isInitialSoilLoading,
]);

return (
<Row className="w-full justify-between">
<Row className="gap-1 items-center">
<div className="pinto-sm-light text-pinto-secondary">Estimated Total Tip</div>
<TooltipSimple
variant="outlined"
content={
<span>
The total tip paid depends on the number of executions needed to fill your order, based on the Soil supply
and Cultivation Factor.{" "}
<a
href="https://docs.pinto.money/pinto-mechanics/field-the-most-innovative-lending-facility-in-crypto/the-cultivation-system-optimal-soil-issuance"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-pinto-green-4"
>
Learn more
</a>
</span>
}
/>
</Row>
<Row className="gap-1 pinto-sm font-normal">
<IconImage src={mainToken.logoURI} alt="PINTO" size={4} className="rounded-full" />
{formatter.token(tipEstimations.min, mainToken)} - {formatter.token(tipEstimations.max, mainToken)}
</Row>
</Row>
);
};
163 changes: 163 additions & 0 deletions src/components/Tractor/Sow/SowOrderSharedComponents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { Col, Row } from "@/components/Container";
import TooltipSimple from "@/components/TooltipSimple";
import { Button } from "@/components/ui/Button";
import { Card } from "@/components/ui/Card";
import IconImage from "@/components/ui/IconImage";
import { Label } from "@/components/ui/Label";
import { Separator } from "@/components/ui/Separator";
import { useTokenMap } from "@/hooks/pinto/useTokenMap";
import { tractorTokenStrategyUtil as StrategyUtil } from "@/lib/Tractor";
import { TractorTokenStrategy } from "@/lib/Tractor/types";
import { formatter } from "@/utils/format";
import { getTokenIndex } from "@/utils/token";
import { MayPromise } from "@/utils/types.generic";
import React from "react";
import { useFormContext, useFormState, useWatch } from "react-hook-form";
import { TOOLTIP_COPY } from "../form/SowOrderV0Fields";
import { SowOrderV0FormSchema } from "../form/SowOrderV0Schema";
import { TractorFormButtonsRow } from "../form/fields/sharedFields";

// ============================================================================
// Shared Components
// ============================================================================

export const SowOrderFormButtonRow = ({
handleBack,
handleNext,
isLoading,
}: {
handleBack: (e: React.MouseEvent<HTMLButtonElement>) => void;
handleNext: (e: React.MouseEvent<HTMLButtonElement>) => MayPromise<void>;
isLoading: boolean;
}) => {
const { errors } = useFormState<SowOrderV0FormSchema>();

const hasErrors = Boolean(Object.keys(errors).length);

return (
<TractorFormButtonsRow
handleLeft={handleBack}
handleRight={handleNext}
isLoading={isLoading}
right={{
content: "Submit",
disabled: Boolean(hasErrors),
}}
left={{
content: "← Back",
}}
/>
);
};

export const SowOrderEntryFormParametersSummary = () => {
const ctx = useFormContext<SowOrderV0FormSchema>();
const values = useWatch({ control: ctx.control });
const tokenMap = useTokenMap();

const totalPintosToSow = `${values.totalAmount} PINTO`;
const minimumTemperature = `${values.temperature}%`;

const summary = StrategyUtil.getSummary(
(values.selectedTokenStrategy ?? { type: "LOWEST_SEEDS" }) as TractorTokenStrategy,
);

const renderTokenStrategy = () => {
if (summary.isLowestPrice) return "Token with Best Price";
if (summary.isLowestSeeds) return "Token with Least Seeds";

const addresses = summary.addresses ?? [];

if ((summary.isMulti || summary.isSingle) && !!addresses.length) {
return (
<Col className="gap-1">
{addresses.map((adr) => {
const tk = tokenMap[getTokenIndex(adr)];
return (
<Row key={`${adr}-selected-token-strategy`} className="gap-1 items-center">
<IconImage src={tk.logoURI} size={4} alt={tk.symbol} />
<div className="pinto-sm font-normal">{tk.symbol}</div>
</Row>
);
})}
</Col>
);
}

return <></>;
};

const morningAuction = values.morningAuction ? "Yes" : "No";

return (
<>
<ReviewRow label="Total Pintos to Sow" tooltip={TOOLTIP_COPY.totalAmount} value={totalPintosToSow} />
<ReviewRow label="Token Sources" tooltip={TOOLTIP_COPY.tokenStrategy} value={renderTokenStrategy()} />
<ReviewRow label="Minimum Temperature" tooltip={TOOLTIP_COPY.temperature} value={minimumTemperature} />
<ReviewRow label="Execute During Morning" tooltip={TOOLTIP_COPY.morningAuction} value={morningAuction} />
</>
);
};

export const SowOrderFormAdvancedParametersSummary = ({
toggleEdit,
}: {
toggleEdit: (e: React.MouseEvent<HTMLButtonElement>) => void;
}) => {
const ctx = useFormContext<SowOrderV0FormSchema>();
const values = useWatch({ control: ctx.control });

const minSoil = values.minSoil;
const maxPerSeason = values.maxPerSeason;
const podLineLength = values.podLineLength;

return (
<Card className="flex flex-col gap-2 border-none">
<ReviewRow
label="Min per Season"
tooltip="The minimum amount of PINTO to Sow per season."
value={minSoil ? `${formatter.number(minSoil)} PINTO` : "--"}
/>
<ReviewRow
label="Max per Season"
tooltip="The maximum amount of PINTO to Sow per season."
value={maxPerSeason ? `${formatter.number(maxPerSeason)} PINTO` : "--"}
/>
<ReviewRow
label="Pod Line Length"
tooltip="The maximum pod line length at which this order can be executed."
value={podLineLength ? `${formatter.number(podLineLength)} PODS` : "--"}
/>
<Separator className="h-[0.5px] bg-pinto-gray-2 my-1" />
<Button variant="outline-primary-2" size="md" className="w-full rounded-sm" onClick={toggleEdit}>
<span>Edit Advanced Parameters</span>
</Button>
</Card>
);
};

const ReviewRow = ({
label,
tooltip,
value,
}: {
label: string;
tooltip?: string;
value: string | JSX.Element;
}) => {
return (
<Row className="w-full justify-between items-start">
<Row className="gap-1 items-center">
{tooltip ? (
<Row className="gap-1 items-center">
<div className="pinto-sm-light text-pinto-secondary">{label}</div>
<TooltipSimple content={tooltip} variant="outlined" triggerClassName="text-pinto-secondary" />
</Row>
) : (
<Label variant="form">{label}</Label>
)}
</Row>
{typeof value === "string" ? <div className="pinto-sm font-normal">{value}</div> : value}
</Row>
);
};
Loading