+
+ The Pod Market is a decentralized marketplace where users can trade Pods, which are protocol-native debt
+ instruments that represent future Pinto tokens. When you buy Pods, you're essentially purchasing the right
+ to redeem them for Pinto tokens at a fixed rate when they become harvestable. The market operates on a
+ first-in-first-out (FIFO) basis, meaning the oldest Pods become harvestable first. You can place buy
+ orders to acquire Pods at a specific price, or create listings to sell your existing Pods to other users.
+ The scatter chart above visualizes all active orders and listings, showing their place in line and price
+ per Pod. This allows you to see market depth and make informed trading decisions based on current market
+ conditions and your investment strategy.
+
+
+
+
+ You have successfully created a Pod Listing with {formatter.noDec(successAmount)} Pods at a price of{" "}
+ {formatter.number(successPrice, { minDecimals: 0, maxDecimals: 6 })} Pintos!
+
- in exchange for {formatter.noDec(podAmount)} Pods @ {plotPosition.toHuman("short")} in Line.
+ in exchange for {formatter.noDec(podAmount)} Pods {linePositions}.
diff --git a/src/pages/market/actions/CreateOrder.tsx b/src/pages/market/actions/CreateOrder.tsx
index 26386ed30..383d38819 100644
--- a/src/pages/market/actions/CreateOrder.tsx
+++ b/src/pages/market/actions/CreateOrder.tsx
@@ -2,11 +2,16 @@ import podIcon from "@/assets/protocol/Pod.png";
import { TV, TokenValue } from "@/classes/TokenValue";
import { ComboInputField } from "@/components/ComboInputField";
import FrameAnimator from "@/components/LoadingSpinner";
+import type { OverlayParams } from "@/components/MarketChartOverlay";
+import PodLineGraph from "@/components/PodLineGraph";
import RoutingAndSlippageInfo, { useRoutingAndSlippageWarning } from "@/components/RoutingAndSlippageInfo";
-import SimpleInputField from "@/components/SimpleInputField";
import SlippageButton from "@/components/SlippageButton";
+import SmartApprovalButton from "@/components/SmartApprovalButton";
import SmartSubmitButton from "@/components/SmartSubmitButton";
+import { Button } from "@/components/ui/Button";
+import { Input } from "@/components/ui/Input";
import { Separator } from "@/components/ui/Separator";
+import { Slider } from "@/components/ui/Slider";
import { ANALYTICS_EVENTS } from "@/constants/analytics-events";
import { PODS } from "@/constants/internalTokens";
import createPodOrder from "@/encoders/createPodOrder";
@@ -27,21 +32,42 @@ import { trackSimpleEvent } from "@/utils/analytics";
import { formatter } from "@/utils/format";
import { tokensEqual } from "@/utils/token";
import { FarmFromMode, FarmToMode, Token } from "@/utils/types";
+import { cn } from "@/utils/utils";
import { useQueryClient } from "@tanstack/react-query";
-import { useCallback, useEffect, useMemo, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
import { useAccount } from "wagmi";
-const pricePerPodValidation = {
- maxValue: 1,
- minValue: 0.000001,
- maxDecimals: 6,
+// Constants
+const PRICE_PER_POD_CONFIG = {
+ MAX: 1,
+ MIN: 0.001,
+ DECIMALS: 6,
+ DECIMAL_MULTIPLIER: 1_000_000, // 10^6 for 6 decimals
+} as const;
+
+const MILLION = 1_000_000;
+const MIN_FILL_AMOUNT = "1";
+
+const TextAdornment = ({ text, className }: { text: string; className?: string }) => {
+ return
+ for {formatter.number(beanAmount, { minDecimals: 0, maxDecimals: 2 })} Pinto at an average price of{" "}
+ {formatter.number(pricePerPod, { minDecimals: 2, maxDecimals: 6 })} per Pod
-
@ {plotPosition.toHuman("short")} in Line
);
diff --git a/src/pages/market/actions/FillOrder.tsx b/src/pages/market/actions/FillOrder.tsx
index 5bb0dd54a..a2f0b6a58 100644
--- a/src/pages/market/actions/FillOrder.tsx
+++ b/src/pages/market/actions/FillOrder.tsx
@@ -1,10 +1,10 @@
-import podIcon from "@/assets/protocol/Pod.png";
import pintoIcon from "@/assets/tokens/PINTO.png";
-import { TV, TokenValue } from "@/classes/TokenValue";
-import ComboPlotInputField from "@/components/ComboPlotInputField";
-import DestinationBalanceSelect from "@/components/DestinationBalanceSelect";
+import { TokenValue } from "@/classes/TokenValue";
+import PodLineGraph from "@/components/PodLineGraph";
import SmartSubmitButton from "@/components/SmartSubmitButton";
+import { Button } from "@/components/ui/Button";
import { Separator } from "@/components/ui/Separator";
+import { MultiSlider } from "@/components/ui/Slider";
import { ANALYTICS_EVENTS } from "@/constants/analytics-events";
import { PODS } from "@/constants/internalTokens";
import { beanstalkAbi } from "@/generated/contractHooks";
@@ -12,82 +12,270 @@ import { useProtocolAddress } from "@/hooks/pinto/useProtocolAddress";
import useTransaction from "@/hooks/useTransaction";
import usePodOrders from "@/state/market/usePodOrders";
import { useFarmerBalances } from "@/state/useFarmerBalances";
-import { useFarmerPlotsQuery } from "@/state/useFarmerField";
-import { useHarvestableIndex } from "@/state/useFieldData";
+import { useFarmerField, useFarmerPlotsQuery } from "@/state/useFarmerField";
+import { useHarvestableIndex, usePodIndex } from "@/state/useFieldData";
import { useQueryKeys } from "@/state/useQueryKeys";
import useTokenData from "@/state/useTokenData";
import { trackSimpleEvent } from "@/utils/analytics";
import { formatter } from "@/utils/format";
import { FarmToMode, Plot } from "@/utils/types";
import { useQueryClient } from "@tanstack/react-query";
-import { useCallback, useMemo, useState } from "react";
-import { useParams } from "react-router-dom";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { useNavigate, useSearchParams } from "react-router-dom";
import { toast } from "sonner";
-import { Address } from "viem";
+import { Address, encodeFunctionData } from "viem";
import { useAccount } from "wagmi";
import CancelOrder from "./CancelOrder";
+// Constants
+const FIELD_ID = 0n;
+const MIN_PODS_THRESHOLD = 1; // Minimum pods required for order eligibility
+
+// Helper Functions
+const calculateRemainingPods = (
+ order: {
+ beanAmount: string | bigint | number;
+ beanAmountFilled: string | bigint | number;
+ pricePerPod: string | bigint | number;
+ },
+ tokenDecimals: number,
+): number => {
+ const amount = TokenValue.fromBlockchain(order.beanAmount, tokenDecimals);
+ const amountFilled = TokenValue.fromBlockchain(order.beanAmountFilled, tokenDecimals);
+ const pricePerPod = TokenValue.fromBlockchain(order.pricePerPod, tokenDecimals);
+
+ return pricePerPod.gt(0) ? amount.sub(amountFilled).div(pricePerPod).toNumber() : 0;
+};
+
+const isOrderEligible = (
+ order: {
+ beanAmount: string | bigint | number;
+ beanAmountFilled: string | bigint | number;
+ pricePerPod: string | bigint | number;
+ maxPlaceInLine: string | bigint | number;
+ },
+ tokenDecimals: number,
+ podLine: TokenValue,
+): boolean => {
+ const amount = TokenValue.fromBlockchain(order.beanAmount, tokenDecimals);
+ const amountFilled = TokenValue.fromBlockchain(order.beanAmountFilled, tokenDecimals);
+ const pricePerPod = TokenValue.fromBlockchain(order.pricePerPod, tokenDecimals);
+ const remainingPods = pricePerPod.gt(0) ? amount.sub(amountFilled).div(pricePerPod) : TokenValue.ZERO;
+ const orderMaxPlace = TokenValue.fromBlockchain(order.maxPlaceInLine, PODS.decimals);
+
+ return remainingPods.gt(MIN_PODS_THRESHOLD) && orderMaxPlace.lte(podLine);
+};
+
export default function FillOrder() {
const mainToken = useTokenData().mainToken;
const diamondAddress = useProtocolAddress();
const { queryKeys: balanceQKs } = useFarmerBalances();
const account = useAccount();
+ const harvestableIndex = useHarvestableIndex();
+ const podIndex = usePodIndex();
+ const podLine = podIndex.sub(harvestableIndex);
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
+ const orderId = searchParams.get("orderId");
const queryClient = useQueryClient();
- const { allPodOrders, allMarket, farmerMarket, farmerField } = useQueryKeys({
+ const {
+ allPodOrders,
+ allMarket,
+ farmerMarket,
+ farmerField: farmerFieldQK,
+ } = useQueryKeys({
account: account.address,
});
const { queryKey: farmerPlotsQK } = useFarmerPlotsQuery();
const allQK = useMemo(
- () => [allPodOrders, allMarket, farmerMarket, farmerField, farmerPlotsQK, ...balanceQKs],
- [allPodOrders, allMarket, farmerMarket, farmerField, farmerPlotsQK, balanceQKs],
+ () => [allPodOrders, allMarket, farmerMarket, farmerFieldQK, farmerPlotsQK, ...balanceQKs],
+ [allPodOrders, allMarket, farmerMarket, farmerFieldQK, farmerPlotsQK, balanceQKs],
);
- const [plot, setPlot] = useState([]);
- // TODO: need to handle an edge case with amount where the first half of the plot is sellable, and the second half is not.
- // Currently this is handled my making such a plot not fillable via ComboPlotInputField.
- const [amount, setAmount] = useState(0);
- const [balanceTo, setBalanceTo] = useState(FarmToMode.INTERNAL);
+ const [podRange, setPodRange] = useState<[number, number]>([0, 0]);
+ const [selectedOrderIds, setSelectedOrderIds] = useState([]);
+ const [isSuccessful, setIsSuccessful] = useState(false);
+ const [successAmount, setSuccessAmount] = useState(null);
+ const [successAvgPrice, setSuccessAvgPrice] = useState(null);
+ const [successTotal, setSuccessTotal] = useState(null);
+
+ const prevTotalCapacityRef = useRef(0);
+ const selectedOrderIdsRef = useRef([]);
+ const successDataRef = useRef<{ amount: number; avgPrice: number; total: number } | null>(null);
+
+ // Keep ref in sync and reset capacity ref when selection changes
+ useEffect(() => {
+ selectedOrderIdsRef.current = selectedOrderIds;
+ prevTotalCapacityRef.current = -1; // Reset to allow re-triggering range update
+ }, [selectedOrderIds]);
- const { id } = useParams();
const podOrders = usePodOrders();
const allOrders = podOrders.data;
- const order = allOrders?.podOrders.find((order) => order.id === id);
-
- const amountOrder = TokenValue.fromBlockchain(order?.beanAmount || 0, mainToken.decimals);
- const amountFilled = TokenValue.fromBlockchain(order?.beanAmountFilled || 0, mainToken.decimals);
- const pricePerPod = TokenValue.fromBlockchain(order?.pricePerPod || 0, mainToken.decimals);
- const minFillAmount = TokenValue.fromBlockchain(order?.minFillAmount || 0, PODS.decimals);
- const remainingBeans = amountOrder.sub(amountFilled);
- // biome-ignore lint/correctness/useExhaustiveDependencies: All are derived from `order`
- const { remainingPods, maxPlaceInLine } = useMemo(() => {
+ const farmerField = useFarmerField();
+
+ const { selectedOrders, orderPositions, totalCapacity } = useMemo(() => {
+ if (!allOrders?.podOrders) return { selectedOrders: [], orderPositions: [], totalCapacity: 0 };
+
+ const orders = allOrders.podOrders.filter((order) => selectedOrderIds.includes(order.id));
+
+ let cumulative = 0;
+ const positions = orders.map((order) => {
+ const fillableAmount = calculateRemainingPods(order, mainToken.decimals);
+ const startPos = cumulative;
+ cumulative += fillableAmount;
+
+ return {
+ orderId: order.id,
+ startPos,
+ endPos: cumulative,
+ capacity: fillableAmount,
+ order,
+ };
+ });
+
return {
- remainingPods: pricePerPod.gt(0) ? remainingBeans.div(pricePerPod) : TokenValue.ZERO,
- maxPlaceInLine: TokenValue.fromBlockchain(order?.maxPlaceInLine || 0, PODS.decimals),
+ selectedOrders: orders,
+ orderPositions: positions,
+ totalCapacity: cumulative,
};
- }, [order]);
+ }, [allOrders, selectedOrderIds, mainToken.decimals]);
- const harvestableIndex = useHarvestableIndex();
- const amountToSell = TokenValue.fromHuman(amount || 0, PODS.decimals);
- const plotPosition = plot.length > 0 ? plot[0].index.sub(harvestableIndex) : TV.ZERO;
-
- // Plot selection handler with tracking
- const handlePlotSelection = useCallback(
- (plots: Plot[]) => {
- trackSimpleEvent(ANALYTICS_EVENTS.MARKET.LISTING_PLOT_SELECTED, {
- plot_count: plots.length,
- previous_count: plot.length,
- fill_action: "order",
- });
- setPlot(plots);
- },
- [plot.length],
- );
+ const amount = podRange[1] - podRange[0];
+
+ const ordersToFill = useMemo(() => {
+ const [rangeStart, rangeEnd] = podRange;
+
+ return orderPositions
+ .filter((pos) => pos.endPos > rangeStart && pos.startPos < rangeEnd)
+ .map((pos) => {
+ const overlapStart = Math.max(pos.startPos, rangeStart);
+ const overlapEnd = Math.min(pos.endPos, rangeEnd);
+ const fillAmount = overlapEnd - overlapStart;
+
+ return {
+ order: pos.order,
+ amount: fillAmount,
+ };
+ })
+ .filter((item) => item.amount > 0);
+ }, [orderPositions, podRange]);
+
+ // Calculate weighted average price per pod once for reuse
+ const weightedAvgPricePerPod = useMemo(() => {
+ if (ordersToFill.length === 0 || amount === 0) return 0;
+
+ // Single order - use its price directly
+ if (ordersToFill.length === 1) {
+ return TokenValue.fromBlockchain(ordersToFill[0].order.pricePerPod, mainToken.decimals).toNumber();
+ }
+
+ // Multiple orders - calculate weighted average
+ let totalValue = 0;
+ let totalPods = 0;
+
+ for (const { order, amount: fillAmount } of ordersToFill) {
+ const orderPricePerPod = TokenValue.fromBlockchain(order.pricePerPod, mainToken.decimals).toNumber();
+ totalValue += orderPricePerPod * fillAmount;
+ totalPods += fillAmount;
+ }
+
+ return totalPods > 0 ? totalValue / totalPods : 0;
+ }, [ordersToFill, amount, mainToken.decimals]);
+
+ const eligibleOrders = useMemo(() => {
+ if (!allOrders?.podOrders) return [];
+
+ // Get farmer's frontmost pod position (lowest index)
+ const farmerPlots = farmerField.plots;
+ const farmerFrontmostPodIndex =
+ farmerPlots.length > 0
+ ? farmerPlots.reduce((min, plot) => (plot.index.lt(min) ? plot.index : min), farmerPlots[0].index)
+ : null;
+
+ return allOrders.podOrders.filter((order) => {
+ // Check basic eligibility
+ if (!isOrderEligible(order, mainToken.decimals, podLine)) {
+ return false;
+ }
+
+ // Check if farmer has pods that can fill this order
+ // Order's maxPlaceInLine + harvestableIndex must be >= farmer's frontmost pod index
+ if (!farmerFrontmostPodIndex) {
+ return false; // No pods available
+ }
+
+ const orderMaxPlaceIndex = harvestableIndex.add(TokenValue.fromBlockchain(order.maxPlaceInLine, PODS.decimals));
+
+ // Farmer's pod must be at or before the order's maxPlaceInLine position
+ return farmerFrontmostPodIndex.lte(orderMaxPlaceIndex);
+ });
+ }, [allOrders?.podOrders, mainToken.decimals, podLine, farmerField.plots, harvestableIndex]);
+
+ useEffect(() => {
+ if (totalCapacity !== prevTotalCapacityRef.current) {
+ setPodRange([0, totalCapacity]);
+ prevTotalCapacityRef.current = totalCapacity;
+ }
+ }, [totalCapacity]);
+
+ // Pre-select order when orderId parameter is present (clicked from chart)
+ useEffect(() => {
+ if (!orderId || !allOrders?.podOrders) return;
+
+ // Find the order with matching ID
+ const order = allOrders.podOrders.find((o) => o.id === orderId);
+ if (!order) return;
+
+ // Add order to selection if not already selected
+ if (!selectedOrderIds.includes(orderId)) {
+ setSelectedOrderIds((prev) => [...prev, orderId]);
+ }
+ }, [orderId, allOrders, selectedOrderIds]);
+
+ const orderMarkers = useMemo(() => {
+ if (eligibleOrders.length === 0) return [];
+
+ return eligibleOrders.map((order) => {
+ const orderMaxPlace = TokenValue.fromBlockchain(order.maxPlaceInLine, PODS.decimals);
+ const markerIndex = harvestableIndex.add(orderMaxPlace);
+
+ return {
+ index: markerIndex,
+ pods: TokenValue.fromHuman(1, PODS.decimals),
+ harvestablePods: TokenValue.ZERO,
+ id: order.id,
+ } as Plot;
+ });
+ }, [eligibleOrders, harvestableIndex]);
+
+ const plotsForGraph = useMemo(() => {
+ return orderMarkers;
+ }, [orderMarkers]);
+
+ const handlePodRangeChange = useCallback((values: number[]) => {
+ const newRange = values as [number, number];
+ setPodRange(newRange);
+
+ const rangeAmount = newRange[1] - newRange[0];
+ if (rangeAmount === 0 && selectedOrderIdsRef.current.length > 0) {
+ setSelectedOrderIds([]);
+ }
+ }, []);
- // reset form and invalidate pod orders/farmer plot queries
const onSuccess = useCallback(() => {
- setPlot([]);
- setAmount(0);
+ // Set success state from ref (to avoid stale closure)
+ if (successDataRef.current) {
+ const { amount, avgPrice, total } = successDataRef.current;
+ setSuccessAmount(amount);
+ setSuccessAvgPrice(avgPrice);
+ setSuccessTotal(total);
+ setIsSuccessful(true);
+ successDataRef.current = null; // Clear ref after use
+ }
+
+ setPodRange([0, 0]);
+ setSelectedOrderIds([]);
allQK.forEach((key) => queryClient.invalidateQueries({ queryKey: key }));
}, [queryClient, allQK]);
@@ -97,149 +285,365 @@ export default function FillOrder() {
successCallback: onSuccess,
});
- const onSubmit = useCallback(() => {
- if (!order || !plot[0]) {
+ const onSubmit = useCallback(async () => {
+ if (ordersToFill.length === 0 || !account || farmerField.plots.length === 0) {
return;
}
- // Track pod order fill
- trackSimpleEvent(ANALYTICS_EVENTS.MARKET.POD_ORDER_FILL, {
- order_price_per_pod: Number(order.pricePerPod),
- order_max_place: Number(order.maxPlaceInLine),
- });
+ // Reset success state when starting new transaction
+ setIsSuccessful(false);
+ setSuccessAmount(null);
+ setSuccessAvgPrice(null);
+ setSuccessTotal(null);
+
+ // Save success data to ref (to avoid stale closure in onSuccess callback)
+ successDataRef.current = {
+ amount,
+ avgPrice: weightedAvgPricePerPod,
+ total: amount * weightedAvgPricePerPod,
+ };
+
+ // Track analytics for each order being filled
+ for (const { order: orderToFill } of ordersToFill) {
+ trackSimpleEvent(ANALYTICS_EVENTS.MARKET.POD_ORDER_FILL, {
+ order_price_per_pod: Number(orderToFill.pricePerPod),
+ order_max_place: Number(orderToFill.maxPlaceInLine),
+ });
+ }
try {
setSubmitting(true);
- toast.loading("Filling Order...");
+ toast.loading(`Filling ${ordersToFill.length} Order${ordersToFill.length !== 1 ? "s" : ""}...`);
+
+ // Sort farmer plots by index to use them in order (only sort once)
+ const sortedPlots = [...farmerField.plots].sort((a, b) => a.index.sub(b.index).toNumber());
+
+ if (sortedPlots.length === 0) {
+ throw new Error("No pods available to fill orders");
+ }
+
+ // Allocate pods from plots to orders
+ let plotIndex = 0;
+ let remainingPodsInCurrentPlot = sortedPlots[0].pods.toNumber();
+ let currentPlot = sortedPlots[0];
+ let currentPlotStartOffset = 0;
+
+ const farmData: `0x${string}`[] = [];
+
+ for (const { order: orderToFill, amount: fillAmount } of ordersToFill) {
+ let remainingAmount = fillAmount;
+ const orderMaxPlaceIndex = harvestableIndex.add(
+ TokenValue.fromBlockchain(orderToFill.maxPlaceInLine, PODS.decimals),
+ );
+
+ // Continue using plots until we have enough pods to fill this order
+ while (remainingAmount > 0 && plotIndex < sortedPlots.length) {
+ // Move to next plot if current one is exhausted
+ if (remainingPodsInCurrentPlot === 0) {
+ plotIndex++;
+ if (plotIndex >= sortedPlots.length) {
+ throw new Error(
+ `Insufficient pods in your plots to fill order. Need ${remainingAmount.toFixed(2)} more pods.`,
+ );
+ }
+ currentPlot = sortedPlots[plotIndex];
+ remainingPodsInCurrentPlot = currentPlot.pods.toNumber();
+ currentPlotStartOffset = 0;
+ }
+
+ // Validate that plot position is valid for this order
+ if (currentPlot.index.gt(orderMaxPlaceIndex)) {
+ throw new Error(
+ `Your pod at position ${currentPlot.index.toHuman()} is too far in line for order (max: ${orderMaxPlaceIndex.toHuman()})`,
+ );
+ }
+
+ const podsToUse = Math.min(remainingAmount, remainingPodsInCurrentPlot);
+ const podAmount = TokenValue.fromHuman(podsToUse, PODS.decimals);
+ const startOffset = TokenValue.fromHuman(currentPlotStartOffset, PODS.decimals);
+
+ // Create fillPodOrder call for this order with pod allocation from current plot
+ const fillOrderArgs = {
+ orderer: orderToFill.farmer.id as Address,
+ fieldId: FIELD_ID,
+ maxPlaceInLine: BigInt(orderToFill.maxPlaceInLine),
+ pricePerPod: Number(orderToFill.pricePerPod),
+ minFillAmount: BigInt(orderToFill.minFillAmount),
+ };
+
+ const fillCall = encodeFunctionData({
+ abi: beanstalkAbi,
+ functionName: "fillPodOrder",
+ args: [
+ fillOrderArgs,
+ currentPlot.index.toBigInt(),
+ startOffset.toBigInt(),
+ podAmount.toBigInt(),
+ Number(FarmToMode.INTERNAL),
+ ],
+ });
+
+ farmData.push(fillCall);
+
+ // Update tracking variables
+ remainingAmount -= podsToUse;
+ remainingPodsInCurrentPlot -= podsToUse;
+ currentPlotStartOffset += podsToUse;
+ }
+
+ // Validate all pods were allocated
+ if (remainingAmount > 0) {
+ throw new Error(
+ `Insufficient pods in your plots to fill order. Need ${remainingAmount.toFixed(2)} more pods.`,
+ );
+ }
+ }
+
+ if (farmData.length === 0) {
+ throw new Error("No valid fill operations to execute");
+ }
+
+ // Use farm to batch all order fills in one transaction
+ // Success state will be set in onSuccess callback via ref
writeWithEstimateGas({
address: diamondAddress,
abi: beanstalkAbi,
- functionName: "fillPodOrder",
- args: [
- {
- orderer: order.farmer.id as Address, // order - account
- fieldId: 0n, // plot - fieldId
- maxPlaceInLine: BigInt(order.maxPlaceInLine), // order - maxPlaceInLine
- pricePerPod: Number(order.pricePerPod), // order - pricePerPod
- minFillAmount: BigInt(order.minFillAmount), // order - minFillAmount
- },
- plot[0].index.toBigInt(), // index of plot to sell
- 0n, // start index within plot
- amountToSell.toBigInt(), // amount of pods to sell
- Number(balanceTo), //destination balance
- ],
+ functionName: "farm",
+ args: [farmData],
});
} catch (e) {
- console.error(e);
+ console.error("Fill order error:", e);
toast.dismiss();
- toast.error("Order Fill Failed");
- throw e;
- } finally {
+ const errorMessage =
+ e instanceof Error ? e.message : "Order Fill Failed. Please check your pod balance and try again.";
+ toast.error(errorMessage);
setSubmitting(false);
}
- }, [order, plot, amountToSell, balanceTo, writeWithEstimateGas, setSubmitting, diamondAddress]);
+ }, [
+ ordersToFill,
+ account,
+ farmerField.plots,
+ writeWithEstimateGas,
+ setSubmitting,
+ diamondAddress,
+ amount,
+ weightedAvgPricePerPod,
+ harvestableIndex,
+ ]);
+
+ const isOwnOrder = useMemo(() => {
+ return selectedOrders.some((order) => order.farmer.id === account.address?.toLowerCase());
+ }, [selectedOrders, account.address]);
- const isOwnOrder = order && order?.farmer.id === account.address?.toLowerCase();
- const disabled = !order || !plot[0] || !amount;
+ if (eligibleOrders.length === 0) {
+ return (
+
+
+
Select the order you want to fill (i):
+
+
+
+
There are no open orders that can be filled with your Pods.
+
+
+ );
+ }
return (
-
- {!order ? (
-
- Select an Order on the panel to the left
+
+ {/* Order Markers Visualization - Click to select orders (multi-select) */}
+
+
Select the orders you want to fill:
+
+ {/* Pod Line Graph - Shows order markers (orange thin lines at maxPlaceInLine) */}
+ {
+ // Multi-select toggle: add or remove clicked order group
+ if (plotIndices.length === 0) return;
+
+ // Check if all orders in the group are already selected
+ const allSelected = plotIndices.every((orderId) => selectedOrderIds.includes(orderId));
+
+ if (allSelected) {
+ // Deselect only this group - remove orders from this group
+ setSelectedOrderIds((prev) => prev.filter((id) => !plotIndices.includes(id)));
+ } else {
+ // Add this group to existing selection - merge with current selection (avoid duplicates)
+ setSelectedOrderIds((prev) => {
+ const newOrderIds = [...prev];
+ plotIndices.forEach((orderId) => {
+ if (!newOrderIds.includes(orderId)) {
+ newOrderIds.push(orderId);
+ }
+ });
+ return newOrderIds;
+ });
+ }
+ }}
+ />
+
+ {/* Total Pods Available - Simple text below graph */}
+
+
+ Total Pods that can be filled:{" "}
+ {formatter.noDec(
+ eligibleOrders.reduce((sum, order) => sum + calculateRemainingPods(order, mainToken.decimals), 0),
+ )}{" "}
+ Pods
+
- ) : (
-
-
-
-
Buyer
-
{order.farmer.id.substring(0, 6)}
-
-
-
Place in Line
-
0 - {maxPlaceInLine.toHuman("short")}
-
-
-
Pods Requested
-
-
-
{remainingPods.toHuman("short")}
-
-
-
-
Price per Pod
-
-
-
{pricePerPod.toHuman("short")}
-
-
-
-
Pinto Remaining
-
-
-
{remainingBeans.toHuman("short")}
-
-
-
+
+
+ {/* Show cancel option if user owns an order */}
+ {isOwnOrder && selectedOrders.length > 0 && (
+ <>
- {isOwnOrder ? (
-
- ) : (
+ {selectedOrders
+ .filter((order) => order.farmer.id === account.address?.toLowerCase())
+ .map((order) => (
+
+ ))}
+ >
+ )}
+
+ {/* Show form only if orders are selected and not own order (even if amount is 0) */}
+ {!isOwnOrder &&
+ selectedOrderIds.length > 0 &&
+ (() => {
+ const maxAmount = totalCapacity;
+
+ return (
<>
-
+ You have successfully filled {formatter.noDec(successAmount)} Pods at an average price of{" "}
+ {formatter.number(successAvgPrice, { minDecimals: 2, maxDecimals: 6 })} Pintos per Pod, for a total of{" "}
+ {formatter.number(successTotal, { minDecimals: 0, maxDecimals: 2 })} Pintos!
+