From 394ebb3b00f45a6664696bca12c76917ab499a05 Mon Sep 17 00:00:00 2001 From: adewunmiodunlamipriscilla-glitch Date: Sat, 17 Oct 2026 16:40:01 +0100 Subject: [PATCH] USDC trust line --- app/copy-trade/page.tsx | 94 ++++- app/markets/quick-view/page.tsx | 136 ++++++- components/layout/WalletDropdown.tsx | 34 +- docs/architecture.md | 5 +- docs/contract-requests.md | 8 + docs/manual-test.md | 8 +- lib/hooks/useStellar.ts | 402 +++++++++++++++++++-- lib/stellar/__tests__/deadline.test.ts | 26 ++ lib/stellar/__tests__/ledger-clock.test.ts | 33 ++ lib/stellar/__tests__/quote.test.ts | 59 +++ lib/stellar/__tests__/sell-solver.test.ts | 77 ++++ lib/stellar/clients.ts | 14 +- lib/stellar/deadline.ts | 40 ++ lib/stellar/errors.ts | 7 +- lib/stellar/ledger-clock.ts | 127 +++++++ lib/stellar/quote.ts | 68 ++++ lib/stellar/sell-solver.ts | 68 ++++ lib/stellar/trustline.ts | 165 +++++++++ 18 files changed, 1311 insertions(+), 60 deletions(-) create mode 100644 lib/stellar/__tests__/deadline.test.ts create mode 100644 lib/stellar/__tests__/ledger-clock.test.ts create mode 100644 lib/stellar/__tests__/quote.test.ts create mode 100644 lib/stellar/__tests__/sell-solver.test.ts create mode 100644 lib/stellar/deadline.ts create mode 100644 lib/stellar/ledger-clock.ts create mode 100644 lib/stellar/quote.ts create mode 100644 lib/stellar/sell-solver.ts create mode 100644 lib/stellar/trustline.ts diff --git a/app/copy-trade/page.tsx b/app/copy-trade/page.tsx index 3bd60a0..91745cb 100644 --- a/app/copy-trade/page.tsx +++ b/app/copy-trade/page.tsx @@ -7,9 +7,20 @@ import { getTrace, ReasoningTrace } from "@/lib/api/traces"; import { confirmCopyTrade, initiateCopyTrade } from "@/lib/api/trade"; import { ApiError } from "@/lib/api/client"; import { useMarket } from "@/lib/hooks/useMarkets"; -import { useBuy, useQuoteBuy, useUsdcBalance } from "@/lib/hooks/useStellar"; +import { + useBuy, + useEnableUsdc, + fetchOnChainMarket, + useLedgerNow, + useOnChainMarket, + useQuoteBuy, + useUsdcBalance, + useUsdcTrustline, +} from "@/lib/hooks/useStellar"; import { explorerTx } from "@/lib/stellar/config"; import { formatUsdc, minOut, shareOf } from "@/lib/stellar/units"; +import { MIN_TX_TIMEOUT_S, SAFETY_MARGIN_S } from "@/lib/stellar/deadline"; +import { getLedgerNow } from "@/lib/stellar/ledger-clock"; /** Wait this long after the slider stops before asking for a new quote. */ const QUOTE_DEBOUNCE_MS = 400; @@ -35,8 +46,12 @@ function CopyTradeContent() { const [traceError, setTraceError] = useState(null); const { data: market } = useMarket(marketIdParam ?? trace?.marketId ?? undefined); const onChainMarketId = market?.onChainMarketId ?? null; + const onChainMarket = useOnChainMarket(onChainMarketId); const balance = useUsdcBalance(address); + const trustline = useUsdcTrustline(address); + const enableUsdc = useEnableUsdc(); + const ledger = useLedgerNow(); const buy = useBuy(); // Allocation as a share of the wallet's USDC balance, in basis points. @@ -62,6 +77,19 @@ function CopyTradeContent() { const debouncedAmount = useDebounced(amountRaw, QUOTE_DEBOUNCE_MS); const quote = useQuoteBuy(onChainMarketId, side ?? "Yes", side ? debouncedAmount : null); const minShares = quote.data !== undefined ? minOut(quote.data, slippageBps) : null; + const closeTime = onChainMarket.data?.market.close_time; + const secondsToClose = + closeTime !== undefined && ledger.now !== null + ? Number(closeTime - BigInt(Math.floor(ledger.now))) + : null; + const marketCanTrade = + onChainMarket.data?.market.status.tag === "Open" && + secondsToClose !== null && + secondsToClose > SAFETY_MARGIN_S + MIN_TX_TIMEOUT_S; + const trustlineMissing = trustline.data?.kind === "missing"; + const trustlineReady = + trustline.data?.kind === "native" || + (trustline.data?.kind === "present" && trustline.data.authorized); const handleSliderInteraction = (clientX: number) => { if (!sliderRef.current) return; @@ -114,6 +142,24 @@ function CopyTradeContent() { setStatusMessage("This market isn't on-chain yet, so it can't be traded."); return; } + if (!onChainMarket.data) { + setStatusMessage("Checking the on-chain market and ledger time…"); + return; + } + if (!marketCanTrade) { + setStatusMessage("This market is closed or too close to its deadline to safely place a trade."); + return; + } + if (!trustlineReady) { + setStatusMessage( + trustlineMissing + ? "Enable your USDC trustline before copying this trade." + : trustline.data?.kind === "present" + ? "The issuer hasn't authorized your USDC trustline." + : "Checking your USDC trustline…", + ); + return; + } if (!amountRaw || amountRaw <= 0n) { setStatusMessage("Choose an allocation greater than zero. You may need USDC in your wallet first."); return; @@ -127,6 +173,17 @@ function CopyTradeContent() { return; } + const freshMarket = await fetchOnChainMarket(onChainMarketId); + const freshLedgerNow = getLedgerNow(); + if ( + freshLedgerNow === null || + freshMarket.market.status.tag !== "Open" || + BigInt(Math.floor(freshLedgerNow)) + BigInt(SAFETY_MARGIN_S + MIN_TX_TIMEOUT_S) >= freshMarket.market.close_time + ) { + setStatusMessage("This market is closed or too close to its deadline to safely place a trade."); + return; + } + setStatusMessage("Recording your copy trade…"); const { copyTradeId } = await initiateCopyTrade({ traceId: trace.id, @@ -159,6 +216,16 @@ function CopyTradeContent() { } }; + const handleEnableUsdc = async () => { + setStatusMessage("Checking XLM reserve, then opening your wallet…"); + try { + await enableUsdc.mutateAsync(); + setStatusMessage("USDC is enabled. Click Confirm Copy Trade when you're ready."); + } catch (error) { + setStatusMessage(error instanceof Error ? error.message : "Couldn't enable USDC."); + } + }; + return (