diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9adb7df..a544bf0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,6 +37,39 @@ npm run build `npm run e2e:testnet` is an optional, read-only check against a running backend and live testnet; see the header of `e2e/testnet.e2e.test.ts`. +## Measuring realtime feeds + +Use separate terminals: + +```bash +node scripts/realtime-burst.mjs --manual --port 4310 +NEXT_PUBLIC_API_URL=http://localhost:4310/api/v1 npm run dev +``` + +Open `/markets?profile=1` or `/?profile=1` and keep that tab visible. +Development-only React Profiler logs `[realtime-profile]`, the component +name, commit count and total `actualDuration` every second. Let the page +settle, then trigger each sample from another terminal: + +```bash +curl 'http://localhost:4310/burst?count=500&rate=500' +# Wait for completion before starting the next sample. +curl 'http://localhost:4310/burst?count=500&rate=50' +``` + +Sum logged intervals covering each burst and its final settling interval. +Use the same browser, viewport and animation preference for both revisions. +For a baseline, instrument the original feed with the same profiler wrapper; +the original feeds need no functional changes. Profiler time measures React +render work, not socket validation, paint, or end-to-end latency. Slow +baselines may coalesce events under load, so fewer commits alone do not +prove better responsiveness. Production ignores `?profile=1`. + +The simulator serves an empty market API and emits synthetic public trades; +it never signs or submits transactions. `--count`, `--rate` and `--port` also +configure its automatic burst (omit `--manual` to emit after the first +connection). See [docs/STATUS.md](docs/STATUS.md) for measured results. + ## When the contracts change The contracts live in diff --git a/app/copy-trade/page.tsx b/app/copy-trade/page.tsx index 8b58598..ade7e66 100644 --- a/app/copy-trade/page.tsx +++ b/app/copy-trade/page.tsx @@ -1,5 +1,10 @@ "use client"; +import { TxReview, TxReviewPreferenceControl, shouldSkipTxReview } from "@/components/tx-review"; +import type { ReviewModel } from "@/lib/stellar/tx-decode"; +import { useSubmitQueueState } from "@/lib/stellar/submit-queue"; + + import React, { useState, useRef, useEffect, useCallback, Suspense } from "react"; import { useSearchParams, useRouter } from "next/navigation"; import { useWallet } from "@/lib/contexts/WalletContext"; @@ -7,12 +12,15 @@ 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, useAccountReserve, usePrepareBuy } from "@/lib/hooks/useStellar"; +import { useBuy, useQuoteBuy, useUsdcBalance, useAccountReserve, usePrepareBuy, useOnChainMarket, fetchOnChainMarket, useUsdcTrustline, useEnableUsdc, useUsdcBalanceState, type PreparedWriteReview } from "@/lib/hooks/useStellar"; import { explorerTx } from "@/lib/stellar/config"; import { formatUsdc, formatXlm, minOut, shareOf } from "@/lib/stellar/units"; import { TxPhase } from "@/lib/stellar/tx-lifecycle"; /** Wait this long after the slider stops before asking for a new quote. */ +import { getLedgerNow, useLedgerNow } from "@/lib/stellar/ledger-clock"; +import { MIN_TX_TIMEOUT_S, SAFETY_MARGIN_S } from "@/lib/stellar/deadline"; +import { PendingValue } from "@/components/pending-value"; const QUOTE_DEBOUNCE_MS = 400; function renderTxPhase(phase: TxPhase): string | null { @@ -60,6 +68,13 @@ function CopyTradeContent() { const onChainMarket = useOnChainMarket(onChainMarketId); const balance = useUsdcBalance(address); + const balanceState = useUsdcBalanceState(address); + const ledger = useLedgerNow(); + const trustline = useUsdcTrustline(address); + const enableUsdc = useEnableUsdc(); + const trustlineMissing = trustline.data?.kind === "missing"; + const trustlineReady = trustline.data?.kind === "native" || (trustline.data?.kind === "present" && trustline.data.authorized); + const marketCanTrade = onChainMarket.data?.market.status.tag === "Open" && ledger.now !== null && Number(onChainMarket.data.market.close_time) - ledger.now > SAFETY_MARGIN_S + MIN_TX_TIMEOUT_S; const reserve = useAccountReserve(address); const buy = useBuy(); const submitQueue = useSubmitQueueState(address); @@ -241,6 +256,7 @@ function CopyTradeContent() { collateralIn: amountRaw, slippageBps, prepared: prepareBuy.data, + review: reviewPreparedBuy, }); if (result.txHash) await confirmCopyTrade(copyTradeId, result.txHash).catch(() => undefined); @@ -362,7 +378,7 @@ function CopyTradeContent() {