Skip to content

Add privy#2

Closed
luu-alex wants to merge 10 commits intomainfrom
add-privy
Closed

Add privy#2
luu-alex wants to merge 10 commits intomainfrom
add-privy

Conversation

@luu-alex
Copy link

No description provided.

priom and others added 3 commits March 16, 2026 15:16
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly upgrades the application's wallet management by integrating Privy for a more streamlined and robust authentication experience. It also enhances Hyperliquid trading capabilities by implementing a 'Unified Account' system, allowing for more flexible collateral usage. Concurrently, the project undergoes a rebranding to 'HyperOdd', reflecting a fresh identity across the platform.

Highlights

  • Privy Integration: Integrated Privy for wallet authentication, replacing the previous custom wallet connection UI and logic. This includes adding Privy dependencies, configuring Privy, and refactoring wallet-related components to use Privy's hooks.
  • Unified Account Support: Introduced a 'Unified Account' concept for Hyperliquid, allowing spot stablecoin balances (USDC/USDH) to be used as collateral for perpetuals. This involves changes to balance derivation, order sizing, and agent registration logic.
  • Application Rebranding: The application has been rebranded from 'HypeTerminal' to 'HyperOdd', with corresponding updates to app names, URLs, and project configurations across various files.
  • Wagmi Configuration Update: Refactored the Wagmi configuration to integrate seamlessly with @privy-io/wagmi, simplifying the wallet setup and removing custom mock wallet implementations.
  • Build Process Enhancements: Added vite-plugin-node-polyfills and a custom SSR stub plugin to vite.config.ts to improve build compatibility and performance, especially for server-side rendering.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request successfully integrates Privy for authentication, replacing the custom wallet connection logic. This is a significant refactoring that simplifies wallet management and introduces new login methods like email and social logins. The changes are consistent across the application, and the introduction of a usePrivyWagmiSync hook to handle state synchronization between Privy and wagmi is a great addition.

I've identified a few areas for improvement:

  • A hardcoded chain ID in the Vite configuration for SSR could pose future maintenance challenges.
  • A component seems to have become dead code after the refactoring.
  • The new unified account logic for available trading balance appears to be missing a case for builderPerp markets.

Overall, this is a solid implementation of a major new feature. Addressing these points will enhance the robustness and maintainability of the new authentication system.

Comment on lines +203 to +209
if (market?.kind === "perp") {
const subscriptionHasBalance = fromSubscription && (fromSubscription[0] > 0 || fromSubscription[1] > 0);
if (!subscriptionHasBalance) {
const unified = getUnifiedStablecoinAvailable(spotBalances);
if (unified > 0) return [unified, unified];
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The logic to fall back to the unified stablecoin balance for availableToTrade only considers market?.kind === "perp". However, builderPerp markets also use spot collateral for unified accounts. This logic should probably be extended to include builderPerp markets to ensure consistent behavior for all perpetual-style markets under the unified account model.

Suggested change
if (market?.kind === "perp") {
const subscriptionHasBalance = fromSubscription && (fromSubscription[0] > 0 || fromSubscription[1] > 0);
if (!subscriptionHasBalance) {
const unified = getUnifiedStablecoinAvailable(spotBalances);
if (unified > 0) return [unified, unified];
}
}
if (market?.kind === "perp" || market?.kind === "builderPerp") {
const subscriptionHasBalance = fromSubscription && (fromSubscription[0] > 0 || fromSubscription[1] > 0);
if (!subscriptionHasBalance) {
const unified = getUnifiedStablecoinAvailable(spotBalances);
if (unified > 0) return [unified, unified];
}
}

Comment on lines 1 to 22
import { useLogin } from "@privy-io/react-auth";
import { useEffect } from "react";

interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
}

export function WalletDialog({ open, onOpenChange }: Props) {
const connectors = useConnectors();
const { mutateAsync: connectAsync, isPending, error } = useConnect();
const [connectingId, setConnectingId] = useState<string | null>(null);
const [showHelp, setShowHelp] = useState(false);
const [lastUsedWallet] = useState(() => getLastUsedWallet());
const [customAddress, setCustomAddress] = useState("");
const [customAddressError, setCustomAddressError] = useState<string | null>(null);

const { mockConnectors, regularConnectors } = useMemo(() => {
const mocks: Connector[] = [];
const regular: Connector[] = [];

for (const connector of connectors) {
if (isMockConnector(connector)) {
mocks.push(connector);
} else {
regular.push(connector);
}
}

return { mockConnectors: mocks, regularConnectors: regular };
}, [connectors]);

const availableConnectors = useMemo(() => {
const sortByPriority = (a: Connector, b: Connector) => {
if (lastUsedWallet) {
if (a.id === lastUsedWallet) return -1;
if (b.id === lastUsedWallet) return 1;
}
const priorityA = getWalletInfo(a).priority ?? 50;
const priorityB = getWalletInfo(b).priority ?? 50;
return priorityA - priorityB;
};

const popular = regularConnectors.filter((c) => getWalletInfo(c).popular).sort(sortByPriority);
const other = regularConnectors.filter((c) => !getWalletInfo(c).popular).sort(sortByPriority);
return { popular, other, all: regularConnectors };
}, [regularConnectors, lastUsedWallet]);

const handleConnect = async (connector: Connector) => {
setConnectingId(connector.uid);
setLastUsedWallet(connector.id);
try {
await connectAsync({ connector });
onOpenChange(false);
} finally {
setConnectingId(null);
}
};

const handleCustomAddressConnect = async () => {
const trimmed = customAddress.trim();
if (!trimmed) {
setCustomAddressError("Please enter an address");
return;
}
if (!isAddress(trimmed)) {
setCustomAddressError("Invalid Ethereum address");
return;
}
setCustomAddressError(null);

const mockWalletIndex = MOCK_WALLETS.findIndex((w) => w.address.toLowerCase() === trimmed.toLowerCase());

if (mockWalletIndex !== -1 && mockConnectors[mockWalletIndex]) {
handleConnect(mockConnectors[mockWalletIndex]);
} else {
const customMockConnector = mock({
accounts: [trimmed as Address],
features: { reconnect: true },
});
setConnectingId("custom-mock");
try {
await connectAsync({ connector: customMockConnector });
onOpenChange(false);
} catch {
setCustomAddressError("Failed to connect with custom address");
} finally {
setConnectingId(null);
}
const { login } = useLogin({
onComplete: () => onOpenChange(false),
onError: () => onOpenChange(false),
});

useEffect(() => {
if (open) {
login();
}
};

const hasConnectors = availableConnectors.all.length > 0 || mockConnectors.length > 0;

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md gap-0 p-0 overflow-hidden">
<div className="p-6 pb-4 border-b border-border-200/50">
<DialogHeader className="space-y-2">
<DialogTitle className="flex items-center gap-2 text-lg">
<WalletIcon className="size-5 text-primary-default" />
<Trans>Connect Wallet</Trans>
</DialogTitle>
<DialogDescription>
<Trans>Connect your wallet to start trading on Hyperliquid</Trans>
</DialogDescription>
</DialogHeader>
</div>

<div className="p-4 space-y-4 max-h-[60vh] overflow-y-auto">
{availableConnectors.popular.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium text-text-600 uppercase tracking-wider px-1">
<Trans>Popular</Trans>
</p>
<div className="space-y-2">
{availableConnectors.popular.map((connector) => {
const walletInfo = getWalletInfo(connector);
const Icon = walletInfo.icon;
const isConnecting = connectingId === connector.uid;

return (
<Button
key={connector.uid}
variant="text"
size="none"
onClick={() => handleConnect(connector)}
disabled={isPending}
className={cn(
"w-full gap-3 p-3 rounded-lg border",
"bg-surface-base hover:bg-surface-analysis/50 hover:border-primary-default/30",
"group focus:ring-2 focus:ring-primary-default/50",
)}
>
<div className="size-10 rounded-lg overflow-hidden flex-shrink-0 shadow-sm">
<Icon className="size-full" />
</div>
<div className="flex-1 text-left min-w-0">
<p className="font-medium text-sm group-hover:text-primary-default transition-colors">
{connector.name}
</p>
<p className="text-xs text-text-600 truncate">{walletInfo.description}</p>
</div>
{isConnecting ? (
<SpinnerGapIcon className="size-4 animate-spin text-primary-default flex-shrink-0" />
) : (
<div className="size-4 rounded-full border border-border-200 group-hover:border-primary-default/50 flex-shrink-0 transition-colors" />
)}
</Button>
);
})}
</div>
</div>
)}

{availableConnectors.other.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium text-text-600 uppercase tracking-wider px-1">
<Trans>Other Options</Trans>
</p>
<div className="space-y-2">
{availableConnectors.other.map((connector) => {
const walletInfo = getWalletInfo(connector);
const Icon = walletInfo.icon;
const isConnecting = connectingId === connector.uid;

return (
<Button
key={connector.uid}
variant="text"
size="none"
onClick={() => handleConnect(connector)}
disabled={isPending}
className={cn(
"w-full gap-3 p-3 rounded-lg border",
"bg-surface-base hover:bg-surface-analysis/50 hover:border-primary-default/30",
"group focus:ring-2 focus:ring-primary-default/50",
)}
>
<div className="size-10 rounded-lg overflow-hidden flex-shrink-0 shadow-sm">
<Icon className="size-full" />
</div>
<div className="flex-1 text-left min-w-0">
<p className="font-medium text-sm group-hover:text-primary-default transition-colors">
{connector.name}
</p>
<p className="text-xs text-text-600 truncate">{walletInfo.description}</p>
</div>
{isConnecting ? (
<SpinnerGapIcon className="size-4 animate-spin text-primary-default flex-shrink-0" />
) : (
<div className="size-4 rounded-full border border-border-200 group-hover:border-primary-default/50 flex-shrink-0 transition-colors" />
)}
</Button>
);
})}
</div>
</div>
)}

{mockConnectors.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium text-warning-700 uppercase tracking-wider px-1">
<Trans>Mock Wallet (Testing)</Trans>
</p>
<div className="space-y-2">
{mockConnectors.map((connector, index) => {
const config = MOCK_WALLETS[index];
const isConnecting = connectingId === connector.uid;

return (
<Button
key={connector.uid}
variant="text"
size="none"
onClick={() => handleConnect(connector)}
disabled={isPending}
className={cn(
"w-full gap-3 p-3 rounded-lg border border-warning-700/30",
"bg-warning-700/5 hover:bg-warning-700/10 hover:border-warning-700/50",
"group focus:ring-2 focus:ring-warning-700/50",
)}
>
<div className="size-10 rounded-lg overflow-hidden flex-shrink-0 shadow-sm bg-warning-700/20 flex items-center justify-center">
<FlaskIcon className="size-5 text-warning-700" />
</div>
<div className="flex-1 text-left min-w-0">
<p className="font-medium text-sm group-hover:text-warning-700 transition-colors">
{config?.name ?? connector.name}
</p>
<p className="text-xs text-text-600 truncate font-mono">{config?.address ?? "Mock wallet"}</p>
</div>
{isConnecting ? (
<SpinnerGapIcon className="size-4 animate-spin text-warning-700 flex-shrink-0" />
) : (
<div className="size-4 rounded-full border border-warning-700/50 flex-shrink-0 transition-colors" />
)}
</Button>
);
})}
</div>
<div className="pt-2 space-y-2">
<div className="flex gap-2">
<Input
placeholder="0x..."
value={customAddress}
onChange={(e) => {
setCustomAddress(e.target.value);
setCustomAddressError(null);
}}
className="font-mono text-xs"
/>
<Button
variant="outlined"
size="sm"
onClick={handleCustomAddressConnect}
disabled={isPending}
className="shrink-0"
>
<Trans>Connect</Trans>
</Button>
</div>
{customAddressError && <p className="text-xs text-error-700 px-1">{customAddressError}</p>}
</div>
</div>
)}

{!hasConnectors && (
<div className="py-8 text-center space-y-3">
<div className="size-12 rounded-full bg-surface-analysis flex items-center justify-center mx-auto">
<WarningCircleIcon className="size-6 text-text-600" />
</div>
<div>
<p className="text-sm font-medium">
<Trans>No wallets found</Trans>
</p>
<p className="text-xs text-text-600 mt-1">
<Trans>Install a wallet extension to continue</Trans>
</p>
</div>
</div>
)}

{error && (
<div className="flex items-start gap-2 p-3 rounded-xs bg-error-700/10 border border-error-700/20">
<WarningCircleIcon className="size-4 text-error-700 shrink-0 mt-0.5" />
<p className="text-xs text-error-700">{error.message}</p>
</div>
)}
</div>

<div className="border-t border-border-200/50 bg-surface-analysis">
<Button
variant="text"
size="none"
onClick={() => setShowHelp(!showHelp)}
className="w-full justify-between p-4 text-sm text-text-600 hover:text-text-950 hover:bg-transparent"
>
<span className="flex items-center gap-2">
<QuestionIcon className="size-4" />
<Trans>New to wallets?</Trans>
</span>
<span className={cn("transition-transform", showHelp && "rotate-180")}>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<path
d="M2.5 4.5L6 8L9.5 4.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</Button>
}, [open, login]);

{showHelp && (
<div className="px-4 pb-4 space-y-3 animate-in slide-in-from-top-2 duration-200">
<div className="flex items-start gap-3 text-xs">
<ShieldIcon className="size-4 text-market-up-600 flex-shrink-0 mt-0.5" />
<div>
<p className="font-medium text-text-950">
<Trans>Secure & Private</Trans>
</p>
<p className="text-text-600 mt-0.5">
<Trans>Only you control your funds. No email or password required.</Trans>
</p>
</div>
</div>
<div className="flex items-start gap-3 text-xs">
<WalletIcon className="size-4 text-primary-default flex-shrink-0 mt-0.5" />
<div>
<p className="font-medium text-text-950">
<Trans>What is a wallet?</Trans>
</p>
<p className="text-text-600 mt-0.5">
<Trans>A crypto wallet lets you store and manage your digital assets securely.</Trans>
</p>
</div>
</div>
<Button variant="outlined" size="sm" className="w-full mt-2" asChild>
<a href="https://ethereum.org/en/wallets/" target="_blank" rel="noopener noreferrer">
<Trans>Learn more</Trans>
<ArrowSquareOutIcon className="size-3 ml-1.5" />
</a>
</Button>
</div>
)}
</div>
</DialogContent>
</Dialog>
);
return null;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This component has been refactored to be a headless component that triggers the Privy login modal. However, all usages of this WalletDialog component seem to have been removed from the codebase in this pull request and replaced with direct calls to the login function from useLogin. If this component is no longer used anywhere, it should be removed to avoid dead code. If it is still used, its name is now misleading, as it no longer renders a dialog. Consider renaming it to something like PrivyLoginTrigger to better reflect its new purpose.

export const useSwitchChain = () => ({ switchChain: () => {}, chains: [], status: 'idle', error: null });
export const useWalletClient = () => ({ data: null, isLoading: false, error: null });
export const usePublicClient = () => undefined;
export const useChainId = () => 42161;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The chain ID 42161 is hardcoded in the SSR stub for wagmi's useChainId hook. While this is correct for Arbitrum, which is the only supported chain currently, this could become a maintenance issue if the application adds support for other chains in the future. The server-side rendering will always assume Arbitrum, which might lead to incorrect behavior (e.g., for generating block explorer links) if chain-dependent logic is ever rendered on the server. Consider defining this as a constant at the top of the file to make it more explicit and easier to update when new chains are added.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Mar 18, 2026

Deploying hyperterminal with  Cloudflare Pages  Cloudflare Pages

Latest commit: 95e2b95
Status:⚡️  Build in progress...

View logs

@socket-security
Copy link

socket-security bot commented Mar 18, 2026

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

View full report

@luu-alex luu-alex closed this Mar 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants