Skip to content
Open
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

Large diffs are not rendered by default.

12 changes: 8 additions & 4 deletions apps/web/src/features/trade/hooks/useFundingRate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ function computeFundingRatePerHour(marketAddress: string): number {
const market = MARKETS.find((m) => m.address === marketAddress)
if (!market) return BASE_FUNDING_RATE_PER_HOUR

const variation = ((hashString(market.address) % 2001) / 1000 - 1) * FUNDING_VARIANCE_PER_MARKET
const variation =
((hashString(market.address) % 2001) / 1000 - 1) *
FUNDING_VARIANCE_PER_MARKET
return BASE_FUNDING_RATE_PER_HOUR + variation
}

Expand All @@ -35,7 +37,9 @@ function computeNextEpoch(): number {
return now - elapsed + FUNDING_INTERVAL_MS
}

async function fetchFundingRate(marketAddress: string): Promise<FundingRateInfo> {
async function fetchFundingRate(
marketAddress: string
): Promise<FundingRateInfo> {
// TODO: replace with on-chain DataStore read once contracts are deployed
return {
ratePerHour: computeFundingRatePerHour(marketAddress),
Expand All @@ -45,8 +49,8 @@ async function fetchFundingRate(marketAddress: string): Promise<FundingRateInfo>

export function useFundingRate(marketAddress: string = DEFAULT_MARKET_ADDRESS) {
return useQuery<FundingRateInfo>({
queryKey: queryKeys.trade.fundingRate("stellar-mainnet"),
queryFn: fetchFundingRate,
queryKey: queryKeys.trade.fundingRate(CHAIN_ID, marketAddress),
queryFn: () => fetchFundingRate(marketAddress),
staleTime: 60_000,
refetchInterval: 60_000,
})
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/features/trade/hooks/useOrderEventPolling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useQueryClient } from "@tanstack/react-query"
import { CONTRACTS } from "@/app/config/contracts"
import { sorobanRpc } from "@/lib/soroban/client"
import { useWalletStore } from "@/features/wallet/store/wallet-store"
import { queryKeys } from "./query-keys"
import { queryKeys } from "../lib/query-keys"

const CHAIN_ID = "stellar-mainnet"
const POLL_INTERVAL_MS = 5000
Expand Down
9 changes: 8 additions & 1 deletion apps/web/src/features/trade/lib/query-keys.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Centralized TanStack Query key factory — keeps cache invalidation consistent

export const queryKeys = {
// Centralized TanStack Query key factory — keeps cache invalidation consistent

const keys = {
// Token prices from oracle keeper (or Stellar oracle)
tokenPrices: (chainId: string) => ["tokenPrices", chainId] as const,

Expand Down Expand Up @@ -41,3 +43,8 @@ export const queryKeys = {
tokenBalances: (chainId: string, account: string) =>
["tokenBalances", chainId, account] as const,
}

export const queryKeys = {
...keys,
trade: keys,
}
24 changes: 13 additions & 11 deletions apps/web/src/features/trade/lib/stellar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,9 @@ export async function createIncreaseOrder(params: IncreaseOrderParams): Promise<
successDescription: (hash) => `Tx: ${hash.slice(0, 8)}...`,
onSuccess: (hash) => {
void invalidateTradeQueries(params.account)
window.open(explorerTxUrl(hash), "_blank", "noopener,noreferrer")
},
onError: parseSorobanError,
},
}
)
}

Expand All @@ -119,7 +118,7 @@ export async function createDecreaseOrder(params: DecreaseOrderParams): Promise<
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: queryKeys.trade.positions(CHAIN_ID, params.account) }),
onError: parseSorobanError,
},
}
)
}

Expand Down Expand Up @@ -152,7 +151,7 @@ export async function createSwapOrder(params: SwapOrderParams): Promise<string>
queryKey: queryKeys.trade.tokenBalances(CHAIN_ID, params.account),
}),
onError: parseSorobanError,
},
}
)
}

Expand All @@ -170,9 +169,12 @@ export async function cancelOrder(account: string, orderKey: OrderKey): Promise<
loadingMessage: "Cancelling order...",
successMessage: "Order cancelled",
successDescription: (hash) => `Tx: ${hash.slice(0, 8)}...`,
onSuccess: () => queryClient.invalidateQueries({ queryKey: queryKeys.trade.orders(CHAIN_ID, account) }),
onSuccess: () =>
queryClient.invalidateQueries({
queryKey: queryKeys.trade.orders(CHAIN_ID, account),
}),
onError: parseSorobanError,
},
}
)
}

Expand All @@ -192,10 +194,9 @@ export async function claimFundingFees(account: string, marketAddresses: Array<s
successDescription: (hash) => `${marketAddresses.length} market(s) | Tx: ${hash.slice(0, 8)}...`,
onSuccess: (hash) => {
void invalidateTradeQueries(account)
window.open(explorerTxUrl(hash), "_blank", "noopener,noreferrer")
},
onError: parseSorobanError,
},
}
)
}

Expand Down Expand Up @@ -254,10 +255,9 @@ export async function sendBatchOrderTxn(account: string, params: BatchOrderParam
successDescription: (hash) => `${opCount} operations | Tx: ${hash.slice(0, 8)}...`,
onSuccess: (hash) => {
void invalidateTradeQueries(account)
window.open(explorerTxUrl(hash), "_blank", "noopener,noreferrer")
},
onError: parseSorobanError,
},
}
)
}

Expand All @@ -272,7 +272,9 @@ export type SidecarOrderParams = {
indexToken: string
}

export async function createSidecarOrder(_params: SidecarOrderParams): Promise<string> {
export async function createSidecarOrder(
_params: SidecarOrderParams
): Promise<string> {
await fakeTxDelay(900)
return "DUMMY_TX_HASH"
}
Expand Down
25 changes: 19 additions & 6 deletions apps/web/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router"
import { Toaster } from "sonner"
import appCss from "@workspace/ui/globals.css?url"
import { AppProviders } from "../app/providers"

import { useTheme } from "../ui/theme-provider"

// Update this to your production domain before going live.
const SITE_URL = "https://so4.market"
Expand Down Expand Up @@ -87,7 +87,8 @@ export const Route = createRootRoute({
{ property: "og:image:type", content: "image/svg+xml" },
{
property: "og:image:alt",
content: "SO4 — On-chain perpetuals DEX · $8.42B 24h volume · 184 markets",
content:
"SO4 — On-chain perpetuals DEX · $8.42B 24h volume · 184 markets",
},

// ── Twitter / X Card ────────────────────────────────────────
Expand All @@ -99,7 +100,8 @@ export const Route = createRootRoute({
{ name: "twitter:image", content: OG_IMAGE },
{
name: "twitter:image:alt",
content: "SO4 — On-chain perpetuals DEX · $8.42B 24h volume · 184 markets",
content:
"SO4 — On-chain perpetuals DEX · $8.42B 24h volume · 184 markets",
},
],
links: [
Expand All @@ -114,7 +116,11 @@ export const Route = createRootRoute({

// ── Fonts ───────────────────────────────────────────────────
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
{ rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" },
{
rel: "preconnect",
href: "https://fonts.gstatic.com",
crossOrigin: "anonymous",
},
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Geist+Mono:wght@300;400;500;600&display=swap",
Expand All @@ -127,7 +133,9 @@ export const Route = createRootRoute({
notFoundComponent: () => (
<main className="mx-auto max-w-330 p-4 pt-16">
<h1 className="text-2xl font-medium text-foreground">404</h1>
<p className="mt-2 text-muted-foreground">The requested page could not be found.</p>
<p className="mt-2 text-muted-foreground">
The requested page could not be found.
</p>
</main>
),
shellComponent: RootDocument,
Expand All @@ -139,6 +147,11 @@ export const Route = createRootRoute({
const THEME_SCRIPT =
`(function(){try{var t=localStorage.getItem('so4-theme');var d=t==='dark'||((!t||t==='system')&&window.matchMedia('(prefers-color-scheme:dark)').matches);document.documentElement.classList.add(d?'dark':'light')}catch(e){}})()` as const

function ThemedToaster() {
const { theme } = useTheme()
return <Toaster richColors position="bottom-right" theme={theme} />
}

function RootDocument({ children }: { children: React.ReactNode }) {
return (
// suppressHydrationWarning: the blocking script adds a class before React
Expand All @@ -158,7 +171,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
<body>
<AppProviders>
{children}
<Toaster richColors position="bottom-right" />
<ThemedToaster />
</AppProviders>
<Scripts />
</body>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useCallback, useState } from "react"
import { toast } from "sonner"
import { sendAndPoll } from "@/lib/tx-builder"
import { explorerTxUrl } from "@/app/config/network"

export type SubmitTxOptions = {
loadingMessage: string
Expand Down Expand Up @@ -28,9 +29,23 @@ export async function submitTx(

await options.onSuccess?.(hash)

const description = options.successDescription?.(hash)

toast.success(options.successMessage, {
id: toastId,
description: options.successDescription?.(hash),
description: (
<div className="flex flex-col gap-1">
{description && <span>{description}</span>}
<a
href={explorerTxUrl(hash)}
target="_blank"
rel="noreferrer"
className="text-xs text-primary hover:underline"
>
View transaction →
</a>
</div>
),
})

return hash
Expand Down
50 changes: 37 additions & 13 deletions apps/web/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,44 @@
import { defineConfig } from "vite"
import { defineConfig, loadEnv } from "vite"
import { tanstackStart } from "@tanstack/react-start/plugin/vite"
import viteReact from "@vitejs/plugin-react"
import viteTsConfigPaths from "vite-tsconfig-paths"
import tailwindcss from "@tailwindcss/vite"
import { nitro } from "nitro/vite"
import path from "path"
import { fileURLToPath } from "url"

const config = defineConfig({
plugins: [
nitro(),
viteTsConfigPaths({
projects: ["./tsconfig.json"],
}),
tailwindcss(),
tanstackStart(),
viteReact(),
],
})
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

export default defineConfig(({ mode }) => {
// 1. Check root .env for VITE_NETWORK (defaults to testnet if not found)
const rootEnv = loadEnv(mode, path.resolve(__dirname, "../../"), "VITE_")
const network = rootEnv.VITE_NETWORK || "testnet"

export default config
// 2. Load the actual variables for that network from apps/web/.env.{network}
const networkEnv = loadEnv(network, __dirname, "VITE_")

return {
define: {
// Ensure the network variable itself is set
"import.meta.env.VITE_NETWORK": JSON.stringify(network),
// Spread in all variables from the network-specific env file
...Object.entries(networkEnv).reduce(
(acc, [key, val]) => {
acc[`import.meta.env.${key}`] = JSON.stringify(val)
return acc
},
{} as Record<string, string>
),
},
plugins: [
nitro(),
viteTsConfigPaths({
projects: ["./tsconfig.json"],
}),
tailwindcss(),
tanstackStart(),
viteReact(),
],
}
})