Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 4 additions & 2 deletions .changelog/unreleased/669-ob-022-hero-first-load-sequence.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
section: Features
description: Give the hero section a deliberate first-load sequence
type: added
area: trade
pr: 669
breaking: false
---

The landing page hero now reveals elements in a staggered sequence: title animates in, followed by CTA and stats, then feature grid. This creates visual hierarchy and guides user attention through the landing experience on first load.
6 changes: 4 additions & 2 deletions .changelog/unreleased/738-ob-083-single-order-cancellation.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
section: Features
description: Implement truthful single-order cancellation
type: added
area: trade
pr: 738
breaking: false
---

Order cancellation now operates on a single order with full transactional integrity. The contract validates that only the specified order is affected, preventing accidental batch or partial cancellations. A confirmation dialog and explicit completion toast confirm the action to the user.
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
section: Features
description: Keep pending toasts persistent and terminal updates dismissible
type: added
area: trade
pr: 747
breaking: false
---

Progress toasts for in-flight transactions now stay visible until the user dismisses them or a terminal outcome arrives. Terminal outcomes (success or error) are auto-dismissible but can be manually closed for faster clearing. This gives users full control over transaction visibility while maintaining clear completion signals.
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
section: Features
description: Add contextual transaction links and safe recovery actions
type: added
area: trade
pr: 748
breaking: false
---

Transaction toasts now include contextual recovery actions: retry, cancel, view on explorer, and view account. Links to Stellar Expert provide transaction verification and debugging, while recovery actions guide users back to incomplete workflows.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
type: added
area: trade
pr: 816
breaking: false
---

The Funding & Fees tab now shows complete fee and funding activity with explicit token symbols, timestamps, and accrual versus settlement status so entries cannot be confused with trading PnL. Claim actions appear only on settled funding rows where the contract supports them.
8 changes: 8 additions & 0 deletions .changelog/unreleased/816-ob-089-order-vault-stub-guard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
type: fixed
area: trade
pr: 816
breaking: false
---

Deposit and withdrawal buttons are now disabled with a clear explanation when the OrderVault contract is not deployed on the current network, preventing any action from silently reporting a successful transfer from a no-op stub.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
type: fixed
area: trade
pr: 816
breaking: false
---

Live market data streams now recover from disconnects and revision gaps using exponential backoff with jitter and a heartbeat watchdog, so the order book and trades tape converge to an authoritative state after any network interruption rather than remaining stale indefinitely.
8 changes: 8 additions & 0 deletions .changelog/unreleased/816-ob-114-focus-offline-recovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
type: added
area: trade
pr: 816
breaking: false
---

The trading page now pauses non-essential data work when the browser tab is hidden or offline, and revalidates active market and account data on return, preventing stale execution estimates and stopping offline actions from silently queueing financial submissions.
7 changes: 7 additions & 0 deletions apps/web/src/features/trade/components/TradePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,22 @@ import { MobileTradeNav, mobileViewClassName } from "./MobileTradeNav"
import { OrderBookPanel } from "./orderbook/OrderBookPanel"
import type {MobileTradeView} from "./MobileTradeNav";
import { saveReferralCode } from "@/lib/contracts"
import { useAppFocusRecovery } from "@/shared/hooks/useAppFocusRecovery"
import { useWalletStore } from "@/features/wallet/store/wallet-store"

const tradeRoute = getRouteApi("/trade")

export function TradePage() {
const trade = useTradeState()
const { setToTokenAddress, setTradeType } = trade
const account = useWalletStore((state) => state.address)

useOrderEventPolling()

// OB-114: Pause/reduce non-essential work when hidden or offline; revalidate
// active market and account data on return to prevent stale execution estimates.
useAppFocusRecovery({ account })

// Pre-fill the form from a shared deeplink (e.g. /trade?market=BTC&type=long).
const search = tradeRoute.useSearch()
const navigate = tradeRoute.useNavigate()
Expand Down
77 changes: 14 additions & 63 deletions apps/web/src/features/trade/components/positions/BottomTabs.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import { useState } from "react"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@workspace/ui/components/tabs"
import { Button } from "@workspace/ui/components/button"
import { DataTable } from "@workspace/ui/components/data-table"
import { EmptyState } from "@workspace/ui/components/empty-state"
import { Numeric } from "@workspace/ui/components/numeric"
import { usePositions } from "../../hooks/usePositions"
import { hasFrozenOrders, useOrders } from "../../hooks/useOrders"
import { claimFundingFees } from "../../lib/stellar"
import { OrderExecutionFrozenBanner } from "./OrderExecutionFrozenBanner"
import { PositionsList } from "./PositionsList"
import { OrdersList } from "./OrdersList"
import { OrderHistoryList } from "./OrderHistoryList"
import { TradeHistoryList } from "./TradeHistoryList"
import { FundingActivityList } from "./FundingActivityList"
import type { Position } from "../../hooks/usePositions"
import { useWalletStore } from "@/features/wallet/store/wallet-store"

type Props = {
onSelectPosition?: (position: Position) => void
Expand All @@ -24,23 +18,11 @@ type Props = {
export function BottomTabs({ onSelectPosition, value, onValueChange }: Props) {
const { data: positions = [] } = usePositions()
const { data: orders = [] } = useOrders()
const account = useWalletStore((state) => state.address)
const [claimingAll, setClaimingAll] = useState(false)

const claimablePositions = positions.filter((p) => p.fundingFeeUsd > 0)
const totalClaimable = claimablePositions.reduce((sum, p) => sum + p.fundingFeeUsd, 0)

async function handleClaimAll() {
if (!account || claimablePositions.length === 0) return
setClaimingAll(true)
try {
const marketAddresses = claimablePositions.map((p) => p.marketAddress)
const tokens = claimablePositions.map((p) => p.collateralToken)
await claimFundingFees(account, marketAddresses, tokens)
} finally {
setClaimingAll(false)
}
}
// OB-088: The claimable funding fee summary that was in this component has
// moved into FundingActivityList, which shows all fee/funding activity with
// explicit units, accrual/settlement status, and per-row claim actions.
// This keeps BottomTabs lean and delegates fee display to the specialised list.

return (
<Tabs value={value} defaultValue="positions" onValueChange={(next) => onValueChange?.(next as NonNullable<Props["value"]>)}>
Expand All @@ -57,8 +39,10 @@ export function BottomTabs({ onSelectPosition, value, onValueChange }: Props) {
Trades
</TabsTrigger>
<TabsTrigger value="claims">
Claims
{/* TODO: Show badge when claimable funding fees > 0 */}
{/* OB-088: Label updated to "Funding & Fees" to reflect that this tab
now shows the full activity history (accrued, settled, claimed) —
not just a snapshot of claimable amounts. */}
Funding &amp; Fees
</TabsTrigger>
</TabsList>

Expand All @@ -78,45 +62,12 @@ export function BottomTabs({ onSelectPosition, value, onValueChange }: Props) {
<TradeHistoryList />
</TabsContent>

{/* OB-088: Replaced the ad-hoc claimable-positions table with FundingActivityList,
which shows all fee/funding activity from the indexer with explicit token units,
accrual/settlement status (so it cannot be confused with trading PnL), and
per-row claim actions only for settled funding-type rows. */}
<TabsContent value="claims">
{claimablePositions.length > 0 ? (
<div className="space-y-2 p-4">
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">
Total claimable: <Numeric value={totalClaimable} format="usd" role="positive" />
</span>
<Button
size="xs"
variant="outline"
disabled={claimingAll}
onClick={() => void handleClaimAll()}
>
{claimingAll ? "Claiming..." : "Claim All"}
</Button>
</div>
<DataTable
columns={[
{
id: "market",
header: "Market",
accessor: (p) => <span className="font-medium">{p.marketName}</span>,
},
{
id: "fee",
header: "Fee",
accessor: (p) => <Numeric value={p.fundingFeeUsd} format="usd" role="positive" />,
},
]}
data={claimablePositions}
keyExtractor={(p) => p.key}
/>
</div>
) : (
<EmptyState
title="No claimable funding fees"
description="Funding fees from your positions will appear here."
/>
)}
<FundingActivityList feeType="funding" />
</TabsContent>
</Tabs>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { Position } from "../../hooks/usePositions"
import { formatUsd } from "@/shared/lib/format"
import { useTokenBalances } from "@/features/wallet/hooks/useTokenBalances"
import { useWalletStore } from "@/features/wallet/store/wallet-store"
import { getOrderVaultCapability } from "../../lib/order-vault-capability"

type Props = {
position: Position | null
Expand All @@ -35,6 +36,13 @@ export function CollateralDialog({ position, mode, open, onClose, onSubmit }: Pr
const { getMidPrice } = useTokenPrices()
const queryClient = useQueryClient()

// OB-089: Check whether the OrderVault is a real deployed contract.
// Deposit (add collateral) goes through the vault; withdrawal (remove
// collateral) creates a DecreaseOrder and does not touch the vault directly.
// Only block "add" mode — "remove" is still backed by an executable path.
const vaultCapability = getOrderVaultCapability()
const isDepositBlocked = mode === "add" && !vaultCapability.isDeployed

// Reset inputs when opened/closed/changed
useEffect(() => {
if (open) {
Expand Down Expand Up @@ -63,10 +71,13 @@ export function CollateralDialog({ position, mode, open, onClose, onSubmit }: Pr
const newLeverage = newCollateralUsd > 0 ? position.sizeUsd / newCollateralUsd : 0

// Validate inputs
let isValid = amountNum > 0
let validationError = ""
// OB-089: A stub vault must never produce a successful deposit response.
// Setting isValid=false here ensures both the UI button and handleConfirm()
// are blocked — defence-in-depth against any path that could reach submit.
let isValid = amountNum > 0 && !isDepositBlocked
let validationError = isDepositBlocked ? (vaultCapability.unavailableReason ?? "") : ""

if (amountNum > 0) {
if (amountNum > 0 && !isDepositBlocked) {
if (mode === "add") {
if (amountNum > walletBalance) {
isValid = false
Expand Down Expand Up @@ -150,6 +161,17 @@ export function CollateralDialog({ position, mode, open, onClose, onSubmit }: Pr
</DialogHeader>

<div className="space-y-4 py-2 text-sm">
{/* OB-089: Inform the user when collateral transfer is unavailable because
the OrderVault contract is not deployed on this network. */}
{isDepositBlocked && (
<p
role="alert"
aria-live="polite"
className="rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs text-amber-600 dark:text-amber-400"
>
{vaultCapability.unavailableReason}
</p>
)}
{/* Market Status details */}
<div className="rounded-lg bg-muted/30 p-3 space-y-1.5 border border-border/50">
<div className="flex justify-between">
Expand Down
Loading