From 3f1e66d5190c4fcbd049e0f59ee7119d06c37d52 Mon Sep 17 00:00:00 2001 From: Enrico Rubboli Date: Mon, 12 Oct 2026 15:53:08 +0200 Subject: [PATCH] feat: order book rows pre-fill the order form --- app/src/components/OrderBook.tsx | 133 +++++++++++-------------------- 1 file changed, 47 insertions(+), 86 deletions(-) diff --git a/app/src/components/OrderBook.tsx b/app/src/components/OrderBook.tsx index c77e795..54bd551 100644 --- a/app/src/components/OrderBook.tsx +++ b/app/src/components/OrderBook.tsx @@ -143,6 +143,7 @@ function BuySellPanel({ tokenBalance, onCreated, onPairRefresh, + prefill, }: { selectedTokenId: string | null; ticker: string; @@ -152,6 +153,8 @@ function BuySellPanel({ tokenBalance: string | null; onCreated: () => void; onPairRefresh: () => void; + /** Order-book rows never execute here — a click prefills this form. */ + prefill: { side: "buy" | "sell"; price: string; amount: string } | null; }) { const [side, setSide] = useState<"buy" | "sell">("buy"); const [type, setType] = useState<"limit" | "market">("limit"); @@ -164,6 +167,17 @@ function BuySellPanel({ const [totp, setTotp] = useState(""); const totpRef = useRef(null); + // A click on an order-book row drops its price/size into this form as a + // limit order. The object identity changes per click, so repeated + // clicks on the SAME row re-apply (and overwrite any edits). + useEffect(() => { + if (!prefill) return; + setSide(prefill.side); + setType("limit"); + setPrice(prefill.price); + setAmount(prefill.amount); + }, [prefill]); + const effectiveTokenId = selectedTokenId ?? tokenId; const effectiveTicker = selectedTokenId ? ticker : (tokenId ? tokenId.slice(0, 8) + "…" : "TOKEN"); @@ -575,22 +589,14 @@ function PairBookRow({ order, side, ticker, - onFilled, - totp, - onTotpConsumed, + onTake, }: { order: OrderInfo; side: "ask" | "bid"; ticker: string; - onFilled: () => void; - totp: string; // shared section-level 2FA code - onTotpConsumed: () => void; // clear + refocus after burn + /** Never executes: hands the price/size to the place-order form. */ + onTake: (price: string, amount: string, takeSide: "buy" | "sell") => void; }) { - const [fillAmount, setFillAmount] = useState(""); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const needsTotp = totp.length !== 6; - const data = order.existing_order_data; if (!data || data.is_frozen || order.is_marked_as_concluded_in_wallet) return null; @@ -599,66 +605,23 @@ function PairBookRow({ const tokenQty = side === "ask" ? data.give_balance.decimal : data.ask_balance.decimal; const mlQty = side === "ask" ? data.ask_balance.decimal : data.give_balance.decimal; const price = calcPrice(mlQty, tokenQty); - const fillLabel = side === "ask" ? "ML" : ticker; - - const handleFill = async () => { - setLoading(true); - setError(null); - try { - await submitWithToast( - async () => { - const r = await stakeTrade({ - action: "order_fill", - params: { - account: 0, - order_id: order.order_id, - fill_amount_in_ask_currency: { decimal: fillAmount }, - output_address: null, - options: {}, - }, - }, totp); - if (!r.ok) { - if (r.code_consumed) onTotpConsumed(); - throw new Error(r.error); - } - onTotpConsumed(); // burned - clear the field - return r.results[0]?.tx_id as string ?? "submitted"; - }, - watchTx, - ); - setFillAmount(""); - onFilled(); - } catch (err) { - setError(friendlyError(err)); - } finally { - setLoading(false); - } - }; + // Taking an ask = you buy the token; taking a bid = you sell it. + const takeSide = side === "ask" ? "buy" : "sell"; return ( - {price} + {price} {tokenQty} {mlQty} -
- setFillAmount(e.target.value)} - disabled={loading} - className="w-24 rounded-lg bg-gray-800 border border-gray-700 text-gray-100 placeholder:text-gray-600 - px-2 py-1 text-xs font-mono focus:outline-none focus:ring-2 focus:ring-mint-600 disabled:opacity-50" - /> - -
- {error &&

{error}

} + ); @@ -746,18 +709,14 @@ function PairBookPanel({ side, ticker, colorClass, - onFilled, - totp, - onTotpConsumed, + onTake, }: { title: string; orders: OrderInfo[]; side: "ask" | "bid"; ticker: string; colorClass: string; - onFilled: () => void; - totp: string; - onTotpConsumed: () => void; + onTake: (price: string, amount: string, takeSide: "buy" | "sell") => void; }) { return (
@@ -773,13 +732,12 @@ function PairBookPanel({ Price (ML/{ticker}) {ticker} amount ML total - Fill + Take {orders.map(o => ( - + ))} @@ -824,11 +782,17 @@ export default function OrderBook({ initialOwnOrders, balanceTokens = [], initia // Shared 2FA codes: filling order-book rows and concluding own orders both // move funds and burn a code server-side. One field per section keeps the // rows uncluttered. - const [fillTotp, setFillTotp] = useState(""); - const fillTotpRef = useRef(null); + // Order-book rows never execute — a click on a row hands its price/size + // to the place-order form below. + const [bookPrefill, setBookPrefill] = useState<{ + side: "buy" | "sell"; price: string; amount: string; + } | null>(null); + const takeFromBook = (price: string, amount: string, side: "buy" | "sell") => { + setBookPrefill({ side, price, amount }); + document.getElementById("place-order")?.scrollIntoView({ behavior: "smooth", block: "start" }); + }; const [concludeTotp, setConcludeTotp] = useState(""); const concludeTotpRef = useRef(null); - const consumeFillTotp = () => { setFillTotp(""); setTimeout(() => fillTotpRef.current?.focus(), 0); }; const consumeConcludeTotp = () => { setConcludeTotp(""); setTimeout(() => concludeTotpRef.current?.focus(), 0); }; useEffect(() => { @@ -1109,9 +1073,9 @@ export default function OrderBook({ initialOwnOrders, balanceTokens = [], initia {!pairLoading && (
-

- Filling an order moves funds and needs your 2FA code. + "Fill form" pre-fills the order form below with that price and size — nothing is + submitted from the book itself.

loadPairOrders(selectedTokenId)} - totp={fillTotp} - onTotpConsumed={consumeFillTotp} + onTake={takeFromBook} /> loadPairOrders(selectedTokenId)} - totp={fillTotp} - onTotpConsumed={consumeFillTotp} + onTake={takeFromBook} />
@@ -1143,7 +1103,7 @@ export default function OrderBook({ initialOwnOrders, balanceTokens = [], initia {/* ── Buy / Sell ── */}
-

Place Order

+

Place Order

selectedTokenId && loadPairOrders(selectedTokenId)} + prefill={bookPrefill} />