755 lines
22 KiB
Markdown
755 lines
22 KiB
Markdown
# Phase 3: Clerk Authentication Integration
|
|
|
|
## Overview
|
|
|
|
Phase 3 completes the wallet integration by connecting it to Clerk authentication, implementing middleware protection, and creating a unified authentication flow. This phase eliminates the need for separate auth systems and provides a seamless experience.
|
|
|
|
## Timeline
|
|
|
|
**Duration**: 2 weeks
|
|
**Dependencies**: Phase 1 & 2 completion
|
|
**Team**: Auth & Backend team
|
|
|
|
## Architecture
|
|
|
|
```mermaid
|
|
graph TD
|
|
A[Browser] --> B[Next.js Middleware]
|
|
B --> C{Auth Check}
|
|
|
|
C -->|No Auth| D[Clerk Sign-in]
|
|
C -->|Has Auth| E[Protected Routes]
|
|
|
|
D --> F[GitHub OAuth]
|
|
D --> G[Wallet Auth]
|
|
|
|
F --> H[Clerk Session]
|
|
G --> H
|
|
|
|
H --> I[User Metadata]
|
|
I --> J[Wallet Address]
|
|
|
|
E --> K[Wallet Operations]
|
|
K --> L[API Routes]
|
|
L --> M[Server Actions]
|
|
```
|
|
|
|
## Migration Reference
|
|
|
|
These implementations adapt functionality from:
|
|
|
|
- Wallet auth flow → from `/repos/laconic-wallet-web/src/screens/AutoSignIn.tsx`
|
|
- Session management → from `/repos/laconic-wallet-web/src/App.tsx` session handling
|
|
- Clerk integration → extends existing `/apps/deploy-fe/src/middleware.ts`
|
|
|
|
## Security Considerations
|
|
|
|
This phase deals with authentication and sensitive wallet operations. Consider these critical security aspects:
|
|
|
|
1. **Session Management**: Implement proper session handling using Clerk's secure mechanisms
|
|
2. **Wallet-Clerk Linking**: Verify wallet ownership through cryptographic signatures before linking to a user account
|
|
3. **Authorization**: Use proper authorization checks for all wallet operations
|
|
4. **Error States**: Gracefully handle network failures and blockchain errors
|
|
5. **Rate Limiting**: Implement rate limiting for sensitive operations like wallet linking
|
|
|
|
## Step-by-Step Implementation
|
|
|
|
### 1. Extend Clerk User Metadata
|
|
|
|
Create a TypeScript interface for the extended user metadata in `apps/deploy-fe/src/types/clerk.d.ts`:
|
|
|
|
```typescript
|
|
import { User } from '@clerk/nextjs/server'
|
|
|
|
declare module '@clerk/nextjs/server' {
|
|
interface User {
|
|
publicMetadata: {
|
|
walletAddress?: string
|
|
walletChainId?: string
|
|
walletConnected?: boolean
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. Implement Clerk API Routes for Wallet Auth
|
|
|
|
Create `apps/deploy-fe/src/app/api/clerk/wallet/link/route.ts`:
|
|
|
|
```typescript
|
|
import { auth, clerkClient } from '@clerk/nextjs/server'
|
|
import { validateSignature } from '@workspace/wallet-core'
|
|
import { NextResponse } from 'next/server'
|
|
import { z } from 'zod'
|
|
|
|
// Define request validation schema with Zod
|
|
const linkWalletSchema = z.object({
|
|
address: z.string()
|
|
.refine(addr => /^(0x[a-fA-F0-9]{40}|laconic[a-zA-Z0-9]{39,59})$/.test(addr), {
|
|
message: "Invalid wallet address format"
|
|
}),
|
|
message: z.string().min(10),
|
|
signature: z.string().min(1),
|
|
chainId: z.string().min(1)
|
|
})
|
|
|
|
export async function POST(request: Request) {
|
|
// Rate limiting - in production implement proper rate limiting middleware
|
|
// This is a placeholder to show where it should be implemented
|
|
|
|
const { userId } = await auth()
|
|
|
|
if (!userId) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
}
|
|
|
|
try {
|
|
const body = await request.json()
|
|
|
|
// Validate request data
|
|
const validationResult = linkWalletSchema.safeParse(body)
|
|
if (!validationResult.success) {
|
|
return NextResponse.json(
|
|
{ error: 'Invalid request data', details: validationResult.error.format() },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
const { address, message, signature, chainId } = validationResult.data
|
|
|
|
// Ensure the signed message actually contains the correct address
|
|
// This prevents replay attacks where a valid signature for one address is used for another
|
|
if (!message.includes(address)) {
|
|
return NextResponse.json(
|
|
{ error: 'Address in message does not match provided address' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
// Validate signature using wallet-core
|
|
const isValid = await validateSignature(message, signature)
|
|
|
|
if (!isValid.success) {
|
|
return NextResponse.json(
|
|
{ error: 'Invalid signature' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
// Update Clerk user metadata with wallet info
|
|
await clerkClient.users.updateUser(userId, {
|
|
publicMetadata: {
|
|
walletAddress: address,
|
|
walletChainId: chainId,
|
|
walletConnected: true
|
|
}
|
|
})
|
|
|
|
return NextResponse.json({ success: true })
|
|
} catch (error) {
|
|
console.error('Error linking wallet:', error)
|
|
return NextResponse.json(
|
|
{ error: error instanceof Error ? error.message : 'Unknown error' },
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
}
|
|
```
|
|
|
|
Create `apps/deploy-fe/src/app/api/clerk/wallet/unlink/route.ts`:
|
|
|
|
```typescript
|
|
import { auth, clerkClient } from '@clerk/nextjs/server'
|
|
import { NextResponse } from 'next/server'
|
|
|
|
export async function POST(request: Request) {
|
|
const { userId } = await auth()
|
|
|
|
if (!userId) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
}
|
|
|
|
try {
|
|
const user = await clerkClient.users.getUser(userId)
|
|
|
|
// Remove wallet information from metadata
|
|
await clerkClient.users.updateUser(userId, {
|
|
publicMetadata: {
|
|
...user.publicMetadata,
|
|
walletAddress: null,
|
|
walletChainId: null,
|
|
walletConnected: false
|
|
}
|
|
})
|
|
|
|
return NextResponse.json({ success: true })
|
|
} catch (error) {
|
|
console.error('Error unlinking wallet:', error)
|
|
return NextResponse.json(
|
|
{ error: error instanceof Error ? error.message : 'Unknown error' },
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Enhance Middleware with Wallet Verification
|
|
|
|
Update `apps/deploy-fe/src/middleware.ts`:
|
|
|
|
```typescript
|
|
import { clerkMiddleware, createRouteMatcher, getAuth } from '@clerk/nextjs/server'
|
|
import { NextResponse } from 'next/server'
|
|
|
|
// Define routes that require wallet connection
|
|
const requiresWalletAuth = createRouteMatcher([
|
|
'/wallet(.*)',
|
|
'/projects(.*)',
|
|
'/buy-prepaid-service(.*)'
|
|
])
|
|
|
|
// Define public routes that don't require any auth
|
|
const isPublicRoute = createRouteMatcher([
|
|
'/sign-in(.*)',
|
|
'/sign-up(.*)',
|
|
'/api/github/webhook'
|
|
])
|
|
|
|
export default clerkMiddleware(async (auth, req) => {
|
|
const { userId } = auth
|
|
|
|
// Skip auth check for webhook endpoint
|
|
if (req.nextUrl.pathname === '/api/github/webhook') {
|
|
return NextResponse.next()
|
|
}
|
|
|
|
// For public routes, allow access
|
|
if (isPublicRoute(req)) {
|
|
return NextResponse.next()
|
|
}
|
|
|
|
// For all other routes, require authentication
|
|
if (!userId) {
|
|
return NextResponse.redirect(new URL('/sign-in', req.url))
|
|
}
|
|
|
|
// For wallet-required routes, check wallet connection
|
|
if (requiresWalletAuth(req)) {
|
|
const user = auth.user
|
|
|
|
// If wallet not connected, redirect to wallet connection page
|
|
if (!user?.publicMetadata?.walletConnected) {
|
|
return NextResponse.redirect(new URL('/wallet/connect', req.url))
|
|
}
|
|
}
|
|
|
|
return NextResponse.next()
|
|
})
|
|
|
|
export const config = {
|
|
matcher: [
|
|
// Skip Next.js internals and all static files
|
|
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
|
|
// Always run for API routes
|
|
'/(api|trpc)(.*)'
|
|
]
|
|
}
|
|
```
|
|
|
|
### 4. Create Wallet Connection Page
|
|
|
|
Create `apps/deploy-fe/src/app/(web3-authenticated)/wallet/connect/page.tsx`:
|
|
|
|
```tsx
|
|
'use client'
|
|
|
|
import React, { useEffect, useState } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
import { useUser } from '@clerk/nextjs'
|
|
import { WalletConnectButton, useWalletUI } from '@workspace/ui/wallet'
|
|
import { Button } from '@workspace/ui/components/button'
|
|
import { PageWrapper } from '@/components/foundation'
|
|
import { createSiweMessage, signMessage } from '@workspace/wallet-core'
|
|
import { toast } from 'sonner'
|
|
import { ArrowLeft } from 'lucide-react'
|
|
|
|
export default function ConnectWalletPage() {
|
|
const router = useRouter()
|
|
const { isConnected, wallet } = useWalletUI()
|
|
const { user, isLoaded } = useUser()
|
|
const [isLinking, setIsLinking] = useState(false)
|
|
|
|
// Check if user already has a linked wallet
|
|
useEffect(() => {
|
|
if (isLoaded && user?.publicMetadata?.walletConnected) {
|
|
router.push('/')
|
|
}
|
|
}, [isLoaded, user, router])
|
|
|
|
// Function to link wallet to Clerk user
|
|
const linkWallet = async () => {
|
|
if (!wallet?.address) return
|
|
|
|
setIsLinking(true)
|
|
try {
|
|
// Create SIWE message
|
|
const message = await createSiweMessage(wallet.address)
|
|
|
|
// Sign message with wallet
|
|
const signature = await signMessage({
|
|
message,
|
|
namespace: 'eip155',
|
|
chainId: '1',
|
|
accountId: 0
|
|
})
|
|
|
|
// Link wallet to Clerk user
|
|
const response = await fetch('/api/clerk/wallet/link', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
address: wallet.address,
|
|
message,
|
|
signature,
|
|
chainId: '1'
|
|
})
|
|
})
|
|
|
|
if (response.ok) {
|
|
toast.success('Wallet connected successfully')
|
|
router.push('/')
|
|
} else {
|
|
const data = await response.json()
|
|
throw new Error(data.error || 'Failed to link wallet')
|
|
}
|
|
} catch (error) {
|
|
console.error('Error linking wallet:', error)
|
|
toast.error(error instanceof Error ? error.message : 'Failed to link wallet')
|
|
} finally {
|
|
setIsLinking(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<PageWrapper header={{ title: 'Connect Wallet' }}>
|
|
<div className="max-w-md mx-auto text-center py-12">
|
|
<h1 className="text-2xl font-bold mb-6">Connect Your Wallet</h1>
|
|
<p className="mb-8 text-muted-foreground">
|
|
Connect your wallet to access additional features like deployments
|
|
and blockchain-related functionality.
|
|
</p>
|
|
|
|
<div className="space-y-6">
|
|
{!isConnected ? (
|
|
<WalletConnectButton size="lg" />
|
|
) : (
|
|
<div className="space-y-4">
|
|
<p className="font-medium">
|
|
Wallet connected: {wallet?.address?.slice(0, 6)}...{wallet?.address?.slice(-4)}
|
|
</p>
|
|
<Button
|
|
onClick={linkWallet}
|
|
disabled={isLinking}
|
|
size="lg"
|
|
>
|
|
{isLinking ? 'Linking...' : 'Link Wallet with Account'}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
<Button variant="ghost" onClick={() => router.back()}>
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
Go Back
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</PageWrapper>
|
|
)
|
|
}
|
|
```
|
|
|
|
### 5. Update User Profile to Show Wallet Information
|
|
|
|
Create `apps/deploy-fe/src/components/user-profile/WalletInfo.tsx`:
|
|
|
|
```tsx
|
|
'use client'
|
|
|
|
import React from 'react'
|
|
import { useUser } from '@clerk/nextjs'
|
|
import { Button } from '@workspace/ui/components/button'
|
|
import { WalletConnectButton } from '@workspace/ui/wallet'
|
|
import { toast } from 'sonner'
|
|
|
|
export function WalletInfo() {
|
|
const { user, isLoaded } = useUser()
|
|
const walletAddress = user?.publicMetadata?.walletAddress as string | undefined
|
|
const walletConnected = user?.publicMetadata?.walletConnected as boolean
|
|
|
|
const disconnectWallet = async () => {
|
|
try {
|
|
const response = await fetch('/api/clerk/wallet/unlink', {
|
|
method: 'POST'
|
|
})
|
|
|
|
if (response.ok) {
|
|
toast.success('Wallet disconnected')
|
|
// Reload user to update metadata
|
|
user?.reload()
|
|
} else {
|
|
const data = await response.json()
|
|
throw new Error(data.error || 'Failed to disconnect wallet')
|
|
}
|
|
} catch (error) {
|
|
console.error('Error disconnecting wallet:', error)
|
|
toast.error(error instanceof Error ? error.message : 'Failed to disconnect wallet')
|
|
}
|
|
}
|
|
|
|
if (!isLoaded) {
|
|
return <div className="animate-pulse h-12 bg-gray-200 rounded" />
|
|
}
|
|
|
|
return (
|
|
<div className="border rounded-lg p-4">
|
|
<h3 className="text-lg font-medium mb-2">Wallet</h3>
|
|
|
|
{walletConnected && walletAddress ? (
|
|
<div className="space-y-4">
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Connected Address:</p>
|
|
<p className="font-mono text-sm">
|
|
{walletAddress.slice(0, 8)}...{walletAddress.slice(-6)}
|
|
</p>
|
|
</div>
|
|
<Button variant="destructive" size="sm" onClick={disconnectWallet}>
|
|
Disconnect
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-4">
|
|
<p className="text-sm text-muted-foreground mb-4">
|
|
No wallet connected to your account
|
|
</p>
|
|
<WalletConnectButton />
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
### 6. Create Server Action to Check Wallet Auth
|
|
|
|
Create `apps/deploy-fe/src/app/actions/wallet.ts`:
|
|
|
|
```typescript
|
|
'use server'
|
|
|
|
import { auth, clerkClient } from '@clerk/nextjs/server'
|
|
import { checkBalance } from '@workspace/wallet-core'
|
|
|
|
export async function checkWalletBalance(chainId: string, amount: string) {
|
|
const { userId } = await auth()
|
|
|
|
if (!userId) {
|
|
throw new Error('Unauthorized')
|
|
}
|
|
|
|
const user = await clerkClient.users.getUser(userId)
|
|
const walletAddress = user.publicMetadata.walletAddress as string | undefined
|
|
|
|
if (!walletAddress) {
|
|
throw new Error('No wallet connected')
|
|
}
|
|
|
|
// Use the wallet-core to check balance
|
|
return checkBalance(chainId, walletAddress, amount)
|
|
}
|
|
|
|
export async function getWalletStatus() {
|
|
const { userId } = await auth()
|
|
|
|
if (!userId) {
|
|
return { isConnected: false }
|
|
}
|
|
|
|
const user = await clerkClient.users.getUser(userId)
|
|
const walletConnected = user.publicMetadata.walletConnected as boolean
|
|
const walletAddress = user.publicMetadata.walletAddress as string | undefined
|
|
|
|
return {
|
|
isConnected: !!walletConnected,
|
|
address: walletAddress
|
|
}
|
|
}
|
|
```
|
|
|
|
### 7. Update Balance Checking Components
|
|
|
|
Replace `apps/deploy-fe/src/components/projects/project/deployments/CheckBalanceWrapper.tsx`:
|
|
|
|
```tsx
|
|
'use client'
|
|
|
|
import React, { useEffect, useState } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
import { checkWalletBalance } from '@/app/actions/wallet'
|
|
import { toast } from 'sonner'
|
|
|
|
interface CheckBalanceWrapperProps {
|
|
children: React.ReactNode
|
|
requiredAmount: string
|
|
redirectTo?: string
|
|
}
|
|
|
|
export function CheckBalanceWrapper({
|
|
children,
|
|
requiredAmount,
|
|
redirectTo = '/buy-prepaid-service'
|
|
}: CheckBalanceWrapperProps) {
|
|
const router = useRouter()
|
|
const [isChecking, setIsChecking] = useState(true)
|
|
const [hasSufficientBalance, setHasSufficientBalance] = useState<boolean>()
|
|
|
|
useEffect(() => {
|
|
const checkBalance = async () => {
|
|
try {
|
|
setIsChecking(true)
|
|
const chainId = process.env.NEXT_PUBLIC_LACONICD_CHAIN_ID || ''
|
|
const result = await checkWalletBalance(chainId, requiredAmount)
|
|
|
|
setHasSufficientBalance(result.hasEnoughBalance)
|
|
|
|
if (!result.hasEnoughBalance) {
|
|
toast.error('Insufficient balance for this operation')
|
|
router.push(redirectTo)
|
|
}
|
|
} catch (error) {
|
|
console.error('Error checking balance:', error)
|
|
toast.error('Failed to check wallet balance')
|
|
} finally {
|
|
setIsChecking(false)
|
|
}
|
|
}
|
|
|
|
checkBalance()
|
|
}, [requiredAmount, redirectTo, router])
|
|
|
|
if (isChecking) {
|
|
return <div className="animate-pulse h-full w-full bg-gray-100 rounded opacity-50" />
|
|
}
|
|
|
|
if (hasSufficientBalance === false) {
|
|
return null
|
|
}
|
|
|
|
return <>{children}</>
|
|
}
|
|
```
|
|
|
|
### 8. Create Wallet Status React Context
|
|
|
|
Create `apps/deploy-fe/src/context/WalletStatusContext.tsx`:
|
|
|
|
```tsx
|
|
'use client'
|
|
|
|
import React, { createContext, useContext, useEffect, useState } from 'react'
|
|
import { useUser } from '@clerk/nextjs'
|
|
|
|
interface WalletStatus {
|
|
isConnected: boolean
|
|
address?: string
|
|
}
|
|
|
|
interface WalletStatusContextType {
|
|
walletStatus: WalletStatus
|
|
refreshWalletStatus: () => void
|
|
isLoading: boolean
|
|
}
|
|
|
|
const WalletStatusContext = createContext<WalletStatusContextType | undefined>(undefined)
|
|
|
|
export function WalletStatusProvider({ children }: { children: React.ReactNode }) {
|
|
const { user, isLoaded } = useUser()
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [walletStatus, setWalletStatus] = useState<WalletStatus>({
|
|
isConnected: false
|
|
})
|
|
|
|
const refreshWalletStatus = () => {
|
|
if (isLoaded && user) {
|
|
const walletConnected = user.publicMetadata.walletConnected as boolean
|
|
const walletAddress = user.publicMetadata.walletAddress as string | undefined
|
|
|
|
setWalletStatus({
|
|
isConnected: !!walletConnected,
|
|
address: walletAddress
|
|
})
|
|
setIsLoading(false)
|
|
} else {
|
|
setWalletStatus({ isConnected: false })
|
|
setIsLoading(!isLoaded)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
refreshWalletStatus()
|
|
}, [isLoaded, user])
|
|
|
|
return (
|
|
<WalletStatusContext.Provider value={{ walletStatus, refreshWalletStatus, isLoading }}>
|
|
{children}
|
|
</WalletStatusContext.Provider>
|
|
)
|
|
}
|
|
|
|
export function useWalletStatus() {
|
|
const context = useContext(WalletStatusContext)
|
|
|
|
if (context === undefined) {
|
|
throw new Error('useWalletStatus must be used within a WalletStatusProvider')
|
|
}
|
|
|
|
return context
|
|
}
|
|
```
|
|
|
|
### 9. Update Providers
|
|
|
|
Update `apps/deploy-fe/src/components/providers/index.tsx`:
|
|
|
|
```tsx
|
|
'use client'
|
|
|
|
import React, { ReactNode } from 'react'
|
|
import { ThemeProvider } from 'next-themes'
|
|
import { WalletProvider } from '../wallet/WalletProvider'
|
|
import { WalletStatusProvider } from '@/context/WalletStatusContext'
|
|
|
|
export function Providers({ children }: { children: ReactNode }) {
|
|
return (
|
|
<ThemeProvider attribute="class" defaultTheme="light" enableSystem>
|
|
<WalletStatusProvider>
|
|
<WalletProvider>{children}</WalletProvider>
|
|
</WalletStatusProvider>
|
|
</ThemeProvider>
|
|
)
|
|
}
|
|
```
|
|
|
|
### 10. Add Wallet Status to Application UI
|
|
|
|
Update `apps/deploy-fe/src/components/foundation/top-navigation/TopNavigation.tsx`:
|
|
|
|
```tsx
|
|
'use client'
|
|
|
|
import React from 'react'
|
|
import { WalletConnectButton } from '@workspace/ui/wallet'
|
|
import { useWalletStatus } from '@/context/WalletStatusContext'
|
|
import { Button } from '@workspace/ui/components/button'
|
|
import Link from 'next/link'
|
|
import { Wallet } from 'lucide-react'
|
|
|
|
export function TopNavigation() {
|
|
const { walletStatus, isLoading } = useWalletStatus()
|
|
|
|
return (
|
|
<header className="border-b sticky top-0 z-40 bg-background">
|
|
<div className="container flex h-16 items-center px-4 sm:px-6">
|
|
<nav className="flex-1 flex items-center justify-between">
|
|
{/* Logo and other navigation items */}
|
|
|
|
{/* Right side items */}
|
|
<div className="flex items-center space-x-4">
|
|
{isLoading ? (
|
|
<div className="h-9 w-32 bg-gray-200 rounded animate-pulse" />
|
|
) : walletStatus.isConnected ? (
|
|
<Button variant="outline" size="sm" asChild>
|
|
<Link href="/wallet">
|
|
<Wallet className="mr-2 h-4 w-4" />
|
|
{walletStatus.address?.slice(0, 6)}...{walletStatus.address?.slice(-4)}
|
|
</Link>
|
|
</Button>
|
|
) : (
|
|
<Button asChild>
|
|
<Link href="/wallet/connect">
|
|
<Wallet className="mr-2 h-4 w-4" />
|
|
Connect Wallet
|
|
</Link>
|
|
</Button>
|
|
)}
|
|
{/* Other right-side items */}
|
|
</div>
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
)
|
|
}
|
|
```
|
|
|
|
## Testing
|
|
|
|
1. Run the frontend app:
|
|
```bash
|
|
cd apps/deploy-fe
|
|
pnpm dev
|
|
```
|
|
|
|
2. Test the integration:
|
|
- Sign in with Clerk (GitHub OAuth)
|
|
- Connect and link wallet
|
|
- Check wallet information in user profile
|
|
- Test protected routes requiring wallet connection
|
|
- Test balance checking functionality
|
|
- Test wallet disconnection
|
|
|
|
## Fallback Handling
|
|
|
|
To ensure robustness, implement these fallback mechanisms:
|
|
|
|
```typescript
|
|
// Example fallback for chain disconnections
|
|
async function getBalanceWithFallback(address: string, chainId: string) {
|
|
try {
|
|
// Try primary RPC endpoint
|
|
return await checkBalance(chainId, address, "1")
|
|
} catch (error) {
|
|
console.error("Primary RPC failed:", error)
|
|
|
|
// Try fallback RPC
|
|
try {
|
|
const fallbackRpc = getFallbackRpcForChain(chainId)
|
|
return await checkBalanceWithCustomRpc(fallbackRpc, address, "1")
|
|
} catch (fallbackError) {
|
|
console.error("Fallback RPC failed:", fallbackError)
|
|
throw new Error("Unable to connect to blockchain nodes")
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Integration Complete
|
|
|
|
At this point, the integration of the Laconic wallet within the Next.js application is complete. The system provides:
|
|
|
|
1. **Unified Authentication**: GitHub OAuth through Clerk combined with wallet authentication
|
|
2. **Secure Middleware**: Route protection based on auth status and wallet connection
|
|
3. **Clean Architecture**: No iframe dependencies, using server actions and API routes
|
|
4. **Improved UX**: Seamless integration with the existing UI components
|
|
5. **Robust Validation**: Type-safe validation with Zod throughout the application
|
|
6. **Proper Error Handling**: Comprehensive error states with fallback mechanisms
|
|
|
|
This completes all three phases of migrating the wallet from an iframe-based implementation to a native Next.js integration with Clerk auth.
|
|
|
|
## Additional Resources
|
|
|
|
- [Clerk Webhook Documentation](https://clerk.com/docs/users/sync-data-to-your-backend) - For keeping external systems in sync
|
|
- [Next.js Middleware Documentation](https://nextjs.org/docs/app/building-your-application/routing/middleware) - For advanced route protection
|
|
- [Ethers.js Documentation](https://docs.ethers.org/v6/) - For Ethereum wallet functionality
|
|
- [CosmJS Documentation](https://cosmos.github.io/cosmjs/) - For Cosmos wallet functionality
|