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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ jobs:
needs: [lint-and-typecheck]
env:
NEXT_PUBLIC_ALCHEMY_API_KEY: ${{ secrets.NEXT_PUBLIC_ALCHEMY_API_KEY }}
NEXT_PUBLIC_PRIVY_APP_ID: ${{ secrets.NEXT_PUBLIC_PRIVY_APP_ID }}
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down
4 changes: 3 additions & 1 deletion packages/erc7984example/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@
# You'll need to prefix the variables names with NEXT_PUBLIC_ if you want to access them on the client side.
# More info: https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables
NEXT_PUBLIC_ALCHEMY_API_KEY=""
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=""

# Privy App ID - Get yours at https://dashboard.privy.io
NEXT_PUBLIC_PRIVY_APP_ID=""
46 changes: 35 additions & 11 deletions packages/erc7984example/app/_components/ERC7984Demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { FHEBenchmark } from "./FHEBenchmark";
import { ethers } from "ethers";
import { useFhevm } from "fhevm-sdk";
import { useAccount } from "wagmi";
import { RainbowKitCustomConnectButton } from "~~/components/helper/RainbowKitCustomConnectButton";
import { PrivyConnectButton } from "~~/components/helper/PrivyConnectButton";
import { useERC7984Wagmi } from "~~/hooks/erc7984/useERC7984Wagmi";
import { useDeployedContractInfo } from "~~/hooks/helper";
import { useDeployedContractInfo, useIsSmartWallet } from "~~/hooks/helper";
import type { AllowedChainIds } from "~~/utils/helper/networks";
import { notification } from "~~/utils/helper/notification";

Expand All @@ -18,6 +18,7 @@ import { notification } from "~~/utils/helper/notification";
*/
export const ERC7984Demo = () => {
const { isConnected, chain, address } = useAccount();
const { isSmartWallet } = useIsSmartWallet();

const chainId = chain?.id;

Expand Down Expand Up @@ -214,7 +215,7 @@ export const ERC7984Demo = () => {
Connect your wallet to use the ERC7984 confidential token demo.
</p>
<div className="flex items-center justify-center">
<RainbowKitCustomConnectButton />
<PrivyConnectButton />
</div>
</div>
</div>
Expand All @@ -231,6 +232,27 @@ export const ERC7984Demo = () => {
</p>
</div>

{/* Smart Wallet Warning */}
{isSmartWallet && (
<div className="glass-card-strong p-6 mb-6 border-2 border-[#FFD208] bg-[#FFD208]/10">
<div className="flex items-start gap-4">
<span className="text-3xl">⚠️</span>
<div>
<h3 className="font-bold text-[#2D2D2D] text-lg mb-2">Smart Wallet Detected</h3>
<p className="text-[#2D2D2D]/80 mb-3">
You&apos;re using a smart contract wallet (like Coinbase Smart Wallet or Safe). FHE balance decryption
is <strong>not currently supported</strong> with smart wallets due to signature format
incompatibilities.
</p>
<p className="text-[#2D2D2D]/70 text-sm">
<strong>Workaround:</strong> Connect with a regular wallet (EOA) like MetaMask. Transfers will still
work normally.
</p>
</div>
</div>
</div>
)}

{/* Balance Handle Display */}
<div className={sectionClass}>
<h3 className={titleClass}>💰 Confidential Balance</h3>
Expand Down Expand Up @@ -279,16 +301,18 @@ export const ERC7984Demo = () => {
<div className="grid grid-cols-1 gap-4 text-black">
<button
className={erc7984.isDecrypted ? successButtonClass : primaryButtonClass}
disabled={!erc7984.canDecrypt}
disabled={!erc7984.canDecrypt || isSmartWallet}
onClick={erc7984.decryptBalanceHandle}
>
{erc7984.canDecrypt
? "🔓 Decrypt Balance"
: erc7984.isDecrypted
? `✅ Decrypted: ${erc7984.clear}`
: erc7984.isDecrypting
? "⏳ Decrypting..."
: "❌ Nothing to decrypt"}
{isSmartWallet
? "🚫 Decrypt unavailable (Smart Wallet)"
: erc7984.canDecrypt
? "🔓 Decrypt Balance"
: erc7984.isDecrypted
? `✅ Decrypted: ${erc7984.clear}`
: erc7984.isDecrypting
? "⏳ Decrypting..."
: "❌ Nothing to decrypt"}
</button>
</div>

Expand Down
7 changes: 2 additions & 5 deletions packages/erc7984example/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Script from "next/script";
import "@rainbow-me/rainbowkit/styles.css";
import { DappWrapperWithProviders } from "~~/components/DappWrapperWithProviders";
import { ThemeProvider } from "~~/components/ThemeProvider";
import "~~/styles/globals.css";
Expand All @@ -13,11 +12,9 @@ export const metadata = getMetadata({
const DappWrapper = ({ children }: { children: React.ReactNode }) => {
return (
<html suppressHydrationWarning className={``}>
<head>
<link href="https://api.fontshare.com/v2/css?f[]=telegraf@400,500,700&display=swap" rel="stylesheet" />
</head>
<head></head>
<body suppressHydrationWarning>
<Script src="https://cdn.zama.org/relayer-sdk-js/0.4.0-2/relayer-sdk-js.umd.cjs" strategy="beforeInteractive" />
<Script src="https://cdn.zama.org/relayer-sdk-js/0.4.0-4/relayer-sdk-js.umd.cjs" strategy="beforeInteractive" />
<ThemeProvider enableSystem>
<DappWrapperWithProviders>{children}</DappWrapperWithProviders>
</ThemeProvider>
Expand Down
90 changes: 90 additions & 0 deletions packages/erc7984example/app/providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"use client";

import { type ReactNode } from "react";
import { PrivyProvider } from "@privy-io/react-auth";
import { WagmiProvider as PrivyWagmiProvider, createConfig } from "@privy-io/wagmi";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { sepolia } from "viem/chains";
import { createConfig as createWagmiConfig, http } from "wagmi";
import { WagmiProvider as StandardWagmiProvider } from "wagmi";
import scaffoldConfig from "~~/scaffold.config";

type Props = {
children: ReactNode;
};

const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
});

// Get Privy App ID from environment
const PRIVY_APP_ID = process.env.NEXT_PUBLIC_PRIVY_APP_ID ?? "";

if (!PRIVY_APP_ID && typeof window !== "undefined") {
console.warn("NEXT_PUBLIC_PRIVY_APP_ID is not set. Wallet connection will not work.");
}

const { alchemyApiKey } = scaffoldConfig;

// Use Sepolia as the primary chain (Privy doesn't work well with local hardhat)
const activeChain = sepolia;

// Build RPC URL
const rpcUrl = alchemyApiKey
? `https://eth-sepolia.g.alchemy.com/v2/${alchemyApiKey}`
: "https://ethereum-sepolia-rpc.publicnode.com";

// Create Wagmi config using Privy's createConfig (for when Privy is configured)
export const wagmiConfig = createConfig({
chains: [activeChain] as const,
transports: {
[activeChain.id]: http(rpcUrl),
} as Record<typeof activeChain.id, ReturnType<typeof http>>,
});

// Create standard Wagmi config (for fallback when Privy is not configured)
const standardWagmiConfig = createWagmiConfig({
chains: [activeChain],
transports: {
[activeChain.id]: http(rpcUrl),
},
});

export function Providers({ children }: Props) {
if (!PRIVY_APP_ID) {
// Fallback for when Privy is not configured - use standard wagmi provider
return (
<StandardWagmiProvider config={standardWagmiConfig}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</StandardWagmiProvider>
);
}

return (
<PrivyProvider
appId={PRIVY_APP_ID}
config={{
embeddedWallets: {
ethereum: {
createOnLogin: "users-without-wallets",
},
},
loginMethods: ["wallet", "email"],
supportedChains: [activeChain],
defaultChain: activeChain,
appearance: {
showWalletLoginFirst: true,
walletChainType: "ethereum-only",
},
}}
>
<QueryClientProvider client={queryClient}>
<PrivyWagmiProvider config={wagmiConfig}>{children}</PrivyWagmiProvider>
</QueryClientProvider>
</PrivyProvider>
);
}
51 changes: 11 additions & 40 deletions packages/erc7984example/components/DappWrapperWithProviders.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,22 @@
"use client";

import { useEffect, useState } from "react";
import { RainbowKitProvider, darkTheme, lightTheme } from "@rainbow-me/rainbowkit";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { InMemoryStorageProvider } from "fhevm-sdk";
import { AppProgressBar as ProgressBar } from "next-nprogress-bar";
import { useTheme } from "next-themes";
import { Toaster } from "react-hot-toast";
import { WagmiProvider } from "wagmi";
import { Providers } from "~~/app/providers";
import { Header } from "~~/components/Header";
import { BlockieAvatar } from "~~/components/helper";
import { wagmiConfig } from "~~/services/web3/wagmiConfig";

export const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
});

export const DappWrapperWithProviders = ({ children }: { children: React.ReactNode }) => {
const { resolvedTheme } = useTheme();
const isDarkMode = resolvedTheme === "dark";
const [mounted, setMounted] = useState(false);

useEffect(() => {
setMounted(true);
}, []);

return (
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider
avatar={BlockieAvatar}
theme={mounted ? (isDarkMode ? darkTheme() : lightTheme()) : lightTheme()}
>
<ProgressBar height="3px" color="#FFD208" />
<div className={`flex flex-col min-h-screen`}>
<Header />
<main className="relative flex flex-col flex-1 z-10">
<InMemoryStorageProvider>{children}</InMemoryStorageProvider>
</main>
</div>
<Toaster />
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
<Providers>
<ProgressBar height="3px" color="#FFD208" />
<div className={`flex flex-col min-h-screen`}>
<Header />
<main className="relative flex flex-col flex-1 z-10">
<InMemoryStorageProvider>{children}</InMemoryStorageProvider>
</main>
</div>
<Toaster />
</Providers>
);
};
4 changes: 2 additions & 2 deletions packages/erc7984example/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import React, { useRef } from "react";
import Image from "next/image";
import Link from "next/link";
import { RainbowKitCustomConnectButton } from "~~/components/helper";
import { PrivyConnectButton } from "~~/components/helper/PrivyConnectButton";
import { useOutsideClick } from "~~/hooks/helper";

/**
Expand Down Expand Up @@ -33,7 +33,7 @@ export const Header = () => {

{/* Connect Button */}
<div className="navbar-end">
<RainbowKitCustomConnectButton />
<PrivyConnectButton />
</div>
</div>
);
Expand Down
11 changes: 8 additions & 3 deletions packages/erc7984example/components/helper/BlockieAvatar.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
"use client";

import { AvatarComponent } from "@rainbow-me/rainbowkit";
import { blo } from "blo";

// Custom Avatar for RainbowKit
export const BlockieAvatar: AvatarComponent = ({ address, ensImage, size }) => (
type BlockieAvatarProps = {
address: string;
ensImage?: string | null;
size: number;
};

// Custom Avatar component
export const BlockieAvatar = ({ address, ensImage, size }: BlockieAvatarProps) => (
// Don't want to use nextJS Image here (and adding remote patterns for the URL)
// eslint-disable-next-line @next/next/no-img-element
<img
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"use client";

import { Balance } from "../Balance";
import { AddressInfoDropdown } from "./AddressInfoDropdown";
import { WrongNetworkDropdown } from "./WrongNetworkDropdown";
import { usePrivy } from "@privy-io/react-auth";
import { Address } from "viem";
import { useAccount } from "wagmi";
import { useTargetNetwork } from "~~/hooks/helper/useTargetNetwork";
import { getBlockExplorerAddressLink } from "~~/utils/helper";

// Check if Privy is configured
const PRIVY_APP_ID = process.env.NEXT_PUBLIC_PRIVY_APP_ID ?? "";

/**
* Privy Connect Button - Client-side only component
*/
const PrivyConnectButtonClient = () => {
const { address, isConnected, chain } = useAccount();
const { targetNetwork } = useTargetNetwork();

const blockExplorerAddressLink = address ? getBlockExplorerAddressLink(targetNetwork, address) : undefined;

// If Privy is not configured, show disabled button
if (!PRIVY_APP_ID) {
return (
<button className="glass-button px-6 py-3 text-[#2D2D2D] font-semibold opacity-50" disabled type="button">
Privy Not Configured
</button>
);
}

// Connected - show balance and address dropdown
if (isConnected && address) {
// Wrong network
if (chain && chain.id !== targetNetwork.id) {
return <WrongNetworkDropdown />;
}

return (
<>
<div className="flex flex-col items-center mr-1 text-[#2D2D2D]">
<Balance address={address as Address} className="min-h-0 h-auto" />
<span className="text-xs text-[#2D2D2D] font-medium">{chain?.name || targetNetwork.name}</span>
</div>
<AddressInfoDropdown
address={address as Address}
displayName={address.slice(0, 6) + "..." + address.slice(-4)}
ensAvatar={undefined}
blockExplorerAddressLink={blockExplorerAddressLink}
/>
</>
);
}

// Not connected - show connect button using Privy
return <PrivyConnectButtonInner />;
};

/**
* Inner component that uses usePrivy hook
*/
const PrivyConnectButtonInner = () => {
const { connectWallet, ready } = usePrivy();

if (!ready) {
return (
<button className="glass-button px-6 py-3 text-[#2D2D2D] font-semibold opacity-50" disabled type="button">
Loading...
</button>
);
}

return (
<button
className="glass-button px-6 py-3 text-[#2D2D2D] font-semibold cursor-pointer"
onClick={() => connectWallet()}
type="button"
>
Connect Wallet
</button>
);
};

export default PrivyConnectButtonClient;
Loading
Loading