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
26 changes: 0 additions & 26 deletions src/components/trade/header/top-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,6 @@ import { UserMenu } from "./user-menu";
const SCOPE_NAV_ITEMS = [
{ scope: "all" as const, label: <Trans>All</Trans>, to: "/", activeClass: "text-text-950 font-medium" },
{ scope: "perp" as const, label: <Trans>Perp</Trans>, to: "/perp", activeClass: "text-scope-perp font-medium" },
{ scope: "spot" as const, label: <Trans>Spot</Trans>, to: "/spot", activeClass: "text-scope-spot font-medium" },
{
scope: "builders-perp" as const,
label: <Trans>Builders</Trans>,
to: "/builders-perp",
activeClass: "text-scope-builders font-medium",
},
] as const;

const STATIC_NAV_ITEMS = [
{ key: "vaults", label: <Trans>Vaults</Trans> },
{ key: "portfolio", label: <Trans>Portfolio</Trans> },
{ key: "staking", label: <Trans>Staking</Trans> },
{ key: "leaderboard", label: <Trans>Leaderboard</Trans> },
] as const;

function getScopeAccentClass(scope: string): string {
Expand Down Expand Up @@ -81,18 +67,6 @@ export function TopNav() {
{item.label}
</Link>
))}
<div className="h-4 w-px bg-border-200 mx-1" />
{STATIC_NAV_ITEMS.map((item) => (
<button
key={item.key}
type="button"
disabled
className="px-2.5 py-1.5 text-text-950/40 cursor-not-allowed"
tabIndex={-1}
>
{item.label}
</button>
))}
</nav>
</div>

Expand Down
23 changes: 3 additions & 20 deletions src/components/trade/mobile/mobile-trade-view.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { CaretDownIcon, SpinnerGapIcon, TrendDownIcon, TrendUpIcon } from "@phosphor-icons/react";
import { useLogin } from "@privy-io/react-auth";
import { useEffect, useMemo, useState } from "react";
import { useConnection, useSwitchChain, useWalletClient } from "wagmi";
import { useConnection } from "wagmi";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Slider, type SliderMark } from "@/components/ui/slider";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { FALLBACK_VALUE_PLACEHOLDER, ORDER_MIN_NOTIONAL_USD, UI_TEXT } from "@/config/constants";
import { ARBITRUM_CHAIN_ID } from "@/config/contracts";
import { getBaseQuoteFromPairName } from "@/domain/market";
import { formatPriceForOrder, formatSizeForOrder, throwIfResponseError } from "@/domain/trade/orders";
import { useAccountBalances } from "@/hooks/trade/use-account-balances";
Expand Down Expand Up @@ -46,12 +45,8 @@ interface MobileTradeViewProps {

export function MobileTradeView({ className }: MobileTradeViewProps) {
const { address, isConnected } = useConnection();
const { data: walletClient, isLoading: isWalletLoading, error: walletClientError } = useWalletClient();
const { switchChain, isPending: isSwitchingChain } = useSwitchChain();
const { login } = useLogin();

const needsChainSwitch = !!walletClientError && walletClientError.message.includes("does not match");

const { data: market } = useSelectedMarketInfo();
const { scope } = useExchangeScope();
const { setSelectedMarket } = useMarketActions();
Expand All @@ -68,7 +63,7 @@ export function MobileTradeView({ className }: MobileTradeViewProps) {
const { isReady: isAgentApproved } = useAgentStatus();
const { register: registerAgent, status: registerStatus } = useAgentRegistration();

const canApprove = !!walletClient && !!address;
const canApprove = !!address;
const isRegistering =
registerStatus === "switching_account_mode" ||
registerStatus === "approving_fee" ||
Expand Down Expand Up @@ -149,14 +144,12 @@ export function MobileTradeView({ className }: MobileTradeViewProps) {
return side === "buy" ? price - buffer : price + buffer;
})();

const canSign = isAgentApproved || !!walletClient;
const canSign = isAgentApproved || !!address;

const validation = useMemo(() => {
const errors: string[] = [];
if (!isConnected)
return { valid: false, errors: [ORDER_TEXT.ERROR_NOT_CONNECTED], canSubmit: false, needsApproval: false };
if (isWalletLoading)
return { valid: false, errors: [ORDER_TEXT.ERROR_LOADING_WALLET], canSubmit: false, needsApproval: false };
if (availableBalance <= 0)
return { valid: false, errors: [ORDER_TEXT.ERROR_NO_BALANCE], canSubmit: false, needsApproval: false };
if (!market) return { valid: false, errors: [ORDER_TEXT.ERROR_NO_MARKET], canSubmit: false, needsApproval: false };
Expand All @@ -174,7 +167,6 @@ export function MobileTradeView({ className }: MobileTradeViewProps) {
return { valid: errors.length === 0, errors, canSubmit: errors.length === 0, needsApproval: false };
}, [
isConnected,
isWalletLoading,
availableBalance,
market,
isMarketExecution,
Expand Down Expand Up @@ -218,8 +210,6 @@ export function MobileTradeView({ className }: MobileTradeViewProps) {
if (markPx > 0) setLimitPriceInput(markPx.toFixed(szDecimalsToPriceDecimals(market?.szDecimals ?? 4)));
};

const handleSwitchChain = () => switchChain({ chainId: ARBITRUM_CHAIN_ID });

const handleApprove = async () => {
if (isRegistering) return;
setApprovalError(null);
Expand Down Expand Up @@ -290,13 +280,6 @@ export function MobileTradeView({ className }: MobileTradeViewProps) {
disabled: false,
variant: "cyan" as const,
};
if (needsChainSwitch)
return {
text: isSwitchingChain ? ORDER_TEXT.BUTTON_SWITCHING : ORDER_TEXT.BUTTON_SWITCH_CHAIN,
action: handleSwitchChain,
disabled: isSwitchingChain,
variant: "cyan" as const,
};
if (availableBalance <= 0)
return {
text: ORDER_TEXT.BUTTON_DEPOSIT,
Expand Down
14 changes: 3 additions & 11 deletions src/components/trade/tradebox/trade-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { t } from "@lingui/core/macro";
import { SpinnerGapIcon } from "@phosphor-icons/react";
import { useLogin } from "@privy-io/react-auth";
import { useCallback, useEffect, useId, useMemo, useState } from "react";
import { useConnection, useSwitchChain, useWalletClient } from "wagmi";
import { useConnection } from "wagmi";
import { Button } from "@/components/ui/button";
import { DEFAULT_QUOTE_TOKEN, isUsdStablecoin, TWAP_MINUTES_MAX, TWAP_MINUTES_MIN } from "@/config/constants";
import { APPROVAL_ERROR_DISMISS_MS } from "@/config/time";
Expand All @@ -13,7 +13,6 @@ import { buildOrderPlan } from "@/domain/trade/order-intent";
import { formatPriceForOrder, formatSizeForOrder, throwIfResponseError } from "@/domain/trade/orders";
import { useFeeRates } from "@/hooks/trade/use-fee-rates";
import { useOrderEntryData } from "@/hooks/trade/use-order-entry-data";
import { cn } from "@/lib/cn";
import { useAgentRegistration, useAgentStatus, useSelectedMarketInfo, useUserPositions } from "@/lib/hyperliquid";
import { useExchangeOrder } from "@/lib/hyperliquid/hooks/exchange/useExchangeOrder";
import { useExchangeTwapOrder } from "@/lib/hyperliquid/hooks/exchange/useExchangeTwapOrder";
Expand Down Expand Up @@ -67,10 +66,7 @@ export function TradePanel() {
const tpSlId = useId();

const { address, isConnected } = useConnection();
const { data: walletClient, isLoading: isWalletLoading, error: walletClientError } = useWalletClient();
const switchChain = useSwitchChain();
const { login } = useLogin();
const needsChainSwitch = !!walletClientError && walletClientError.message.includes("does not match");

const { data: market } = useSelectedMarketInfo();

Expand Down Expand Up @@ -201,11 +197,10 @@ export function TradePanel() {

const needsAgentApproval = !isAgentReady;
const isReadyToTrade = isAgentReady;
const canApprove = !!walletClient && !!address;
const canApprove = !!address;

const baseInput = {
isConnected,
isWalletLoading,
availableBalance,
hasMarket: !!market,
hasAssetIndex: typeof market?.assetId === "number",
Expand Down Expand Up @@ -421,9 +416,6 @@ export function TradePanel() {

const buttonContent = useButtonContent({
isConnected,
needsChainSwitch,
isSwitchingChain: switchChain.isPending,
switchChain: (chainId) => switchChain.mutate({ chainId }),
availableBalance,
validation,
isAgentLoading,
Expand Down Expand Up @@ -501,7 +493,7 @@ export function TradePanel() {
size="lg"
onClick={buttonContent.action}
disabled={buttonContent.disabled}
className={cn("w-full", !isConnected && "text-primary-text")}
className="w-full"
aria-label={buttonContent.text}
>
{(isSubmitting || isRegistering) && <SpinnerGapIcon className="size-3 animate-spin" />}
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const buttonVariants = cva(
{
variant: "contained",
tone: "accent",
class: "bg-primary-default text-white hover:bg-primary-hover active:bg-primary-active",
class: "bg-primary-default text-primary-text hover:bg-primary-hover active:bg-primary-active",
},
{
variant: "outlined",
Expand Down
4 changes: 2 additions & 2 deletions src/config/privy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { PrivyClientConfig } from "@privy-io/react-auth";
import { arbitrum } from "wagmi/chains";
import { arbitrum, arbitrumSepolia } from "wagmi/chains";

export const privyConfig: PrivyClientConfig = {
loginMethods: ["email", "google", "wallet"],
Expand All @@ -11,5 +11,5 @@ export const privyConfig: PrivyClientConfig = {
appearance: {
showWalletLoginFirst: false,
},
supportedChains: [arbitrum],
supportedChains: [arbitrum, arbitrumSepolia],
};
5 changes: 3 additions & 2 deletions src/config/wagmi.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { createConfig } from "@privy-io/wagmi";
import { http } from "wagmi";
import { arbitrum } from "wagmi/chains";
import { arbitrum, arbitrumSepolia } from "wagmi/chains";

export const config = createConfig({
chains: [arbitrum],
chains: [arbitrum, arbitrumSepolia],
transports: {
[arbitrum.id]: http(),
[arbitrumSepolia.id]: http(),
},
});
17 changes: 13 additions & 4 deletions src/hooks/use-privy-wagmi-sync.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { usePrivy } from "@privy-io/react-auth";
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import { useConnection, useDisconnect } from "wagmi";

/**
Expand All @@ -11,16 +11,25 @@ import { useConnection, useDisconnect } from "wagmi";
*
* This hook detects that mismatch once Privy is ready and force-disconnects
* wagmi so both layers agree on the unauthenticated state.
*
* The check runs only when Privy becomes ready (once on init), not reactively
* on auth/connection changes — otherwise it would disconnect wagmi mid-login
* before Privy finishes the wallet authentication handshake.
*/
export function usePrivyWagmiSync() {
const { ready, authenticated } = usePrivy();
const { isConnected } = useConnection();
const { disconnect } = useDisconnect();
const { mutate: disconnect } = useDisconnect();

const authenticatedRef = useRef(authenticated);
const isConnectedRef = useRef(isConnected);
authenticatedRef.current = authenticated;
isConnectedRef.current = isConnected;

useEffect(() => {
if (!ready) return;
if (!authenticated && isConnected) {
if (!authenticatedRef.current && isConnectedRef.current) {
disconnect();
}
}, [ready, authenticated, isConnected, disconnect]);
}, [ready, disconnect]);
}
8 changes: 1 addition & 7 deletions src/lib/errors/stacks/perp-order.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { noBalanceValidator } from "../definitions/balance";
import {
signerNotReadyValidator,
walletLoadingValidator,
walletNotConnectedValidator,
} from "../definitions/connection";
import { signerNotReadyValidator, walletNotConnectedValidator } from "../definitions/connection";
import { marketNotReadyValidator, noMarketValidator, noMarkPriceValidator } from "../definitions/market";
import {
enterLimitPriceValidator,
Expand Down Expand Up @@ -33,7 +29,6 @@ import { runValidators, type ValidationError, type Validator } from "../types";

export interface PerpOrderContext extends OrderInputContext, TpSlContext, TriggerContext, ScaleContext, TwapContext {
isConnected: boolean;
isWalletLoading: boolean;
isReadyToTrade: boolean;
needsAgentApproval: boolean;
availableBalance: number;
Expand All @@ -51,7 +46,6 @@ export interface PerpOrderValidationResult {

const perpOrderValidators: Validator<PerpOrderContext>[] = [
walletNotConnectedValidator,
walletLoadingValidator,
noBalanceValidator,
noMarketValidator,
marketNotReadyValidator,
Expand Down
8 changes: 1 addition & 7 deletions src/lib/errors/stacks/spot-order.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { noBalanceValidator } from "../definitions/balance";
import {
signerNotReadyValidator,
walletLoadingValidator,
walletNotConnectedValidator,
} from "../definitions/connection";
import { signerNotReadyValidator, walletNotConnectedValidator } from "../definitions/connection";
import { marketNotReadyValidator, noMarketValidator } from "../definitions/market";
import {
insufficientBaseBalanceValidator,
Expand All @@ -20,7 +16,6 @@ import { runValidators, type ValidationError, type Validator } from "../types";

export interface SpotOrderContext extends SpotInputContext, SpotBalanceContext {
isConnected: boolean;
isWalletLoading: boolean;
isReadyToTrade: boolean;
needsAgentApproval: boolean;
availableBalance: number;
Expand All @@ -37,7 +32,6 @@ export interface SpotOrderValidationResult {

const spotOrderValidators: Validator<SpotOrderContext>[] = [
walletNotConnectedValidator,
walletLoadingValidator,
noBalanceValidator,
noMarketValidator,
marketNotReadyValidator,
Expand Down
40 changes: 31 additions & 9 deletions src/lib/hyperliquid/hooks/useClients.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { ExchangeClient, InfoClient, SubscriptionClient } from "@nktkas/hyperliquid";
import { useWallets } from "@privy-io/react-auth";
import { useMemo } from "react";
import { useConnection, useWalletClient } from "wagmi";
import { useConnection } from "wagmi";
import { arbitrum, arbitrumSepolia } from "wagmi/chains";
import { createExchangeClient } from "@/lib/hyperliquid/clients";
import { useHyperliquid } from "@/lib/hyperliquid/provider";
import { useAgentWallet } from "@/lib/hyperliquid/signing/use-agent-wallet";
import { toHyperliquidWallet } from "@/lib/hyperliquid/wallet";

export interface HyperliquidClients {
info: InfoClient;
Expand All @@ -16,22 +17,43 @@ export interface HyperliquidClients {
}

export function useHyperliquidClients(): HyperliquidClients {
const { info, subscription } = useHyperliquid();
const { info, subscription, env } = useHyperliquid();
const { signer, isReady: agentReady } = useAgentWallet();
const { address } = useConnection();
const { data: walletClient } = useWalletClient();
const { wallets } = useWallets();

const trading = useMemo(() => {
if (!signer || !agentReady) return null;
return createExchangeClient(signer);
}, [signer, agentReady]);

const user = useMemo(() => {
if (!walletClient || !address) return null;
const wallet = toHyperliquidWallet(walletClient, address);
if (!wallet) return null;
return createExchangeClient(wallet);
}, [walletClient, address]);
if (!address) return null;
const privyWallet = wallets.find((w) => w.address.toLowerCase() === address.toLowerCase());
if (!privyWallet) return null;
const chain = env === "Testnet" ? arbitrumSepolia : arbitrum;
return createExchangeClient({
address,
// Required for SDK to detect this as AbstractViemJsonRpcAccount and call getChainId()
// for signatureChainId. Without these, SDK falls back to chainId 1 (mainnet), which
// causes switchChain(1) and Hyperliquid signature validation to fail.
getAddresses: async () => [address],
getChainId: async () => chain.id,
signTypedData: async (params) => {
const provider = await privyWallet.getEthereumProvider();
// Switch chain only if needed — avoids redundant popups on subsequent signing steps
// MetaMask v11+ rejects eth_signTypedData_v4 if domain chainId ≠ active chain
const currentChainId = parseInt((await provider.request({ method: "eth_chainId" })) as string, 16);
if (currentChainId !== chain.id) {
await privyWallet.switchChain(chain.id);
}
return provider.request({
method: "eth_signTypedData_v4",
params: [address, JSON.stringify(params)],
}) as Promise<`0x${string}`>;
},
});
}, [address, wallets, env]);

return {
info,
Expand Down
Loading
Loading