diff --git a/apps/backend/src/server.ts b/apps/backend/src/server.ts index 50a7c3d..af3ff87 100644 --- a/apps/backend/src/server.ts +++ b/apps/backend/src/server.ts @@ -1,4 +1,3 @@ -import { createServer } from 'node:http' import { ApolloServerPluginDrainHttpServer, ApolloServerPluginLandingPageLocalDefault, @@ -9,12 +8,12 @@ import cors from 'cors' import debug from 'debug' import express from 'express' import session from 'express-session' +import { createServer } from 'node:http' import { makeExecutableSchema } from '@graphql-tools/schema' import type { TypeSource } from '@graphql-tools/utils' import type { ServerConfig } from './config' -import { DEFAULT_GQL_PATH } from './constants' import authRouter from './routes/auth' import githubRouter from './routes/github' import stagingRouter from './routes/staging' @@ -38,7 +37,7 @@ export const createAndStartServer = async ( resolvers: any, service: Service ): Promise => { - const { host, port, gqlPath = DEFAULT_GQL_PATH } = serverConfig + const { host, port, gqlPath = '/graphql' } = serverConfig const { appOriginUrl, secret, domain, trustProxy } = serverConfig.session const app = express() diff --git a/apps/backend/src/utils.ts b/apps/backend/src/utils.ts index f55d79d..072ba44 100644 --- a/apps/backend/src/utils.ts +++ b/apps/backend/src/utils.ts @@ -1,7 +1,7 @@ -import assert from 'node:assert' -import path from 'node:path' import debug from 'debug' import fs from 'fs-extra' +import assert from 'node:assert' +import path from 'node:path' import type { Octokit } from 'octokit' import toml from 'toml' import type { @@ -12,26 +12,34 @@ import type { } from 'typeorm' import type { Config } from './config' -import { DEFAULT_CONFIG_FILE_PATH } from './constants' -import type { PackageJSON } from './types' + +interface PackageJSON { + name: string + description?: string + homepage?: string + license?: string + author?: string | { [key: string]: unknown } + version?: string + [key: string]: unknown +} const log = debug('snowball:utils') export async function getConfig() { - // TODO: get config path using cli - return await _getConfig(DEFAULT_CONFIG_FILE_PATH) + return await _getConfig( + path.join(__dirname, '../environments/local.toml') + ) } const _getConfig = async ( configFile: string ): Promise => { - const configFilePath = path.resolve(configFile) - const fileExists = await fs.pathExists(configFilePath) + const fileExists = await fs.pathExists(configFile) if (!fileExists) { - throw new Error(`Config file not found: ${configFilePath}`) + throw new Error(`Config file not found: ${configFile}`) } - const config = toml.parse(await fs.readFile(configFilePath, 'utf8')) + const config = toml.parse(await fs.readFile(configFile, 'utf8')) log('config', JSON.stringify(config, null, 2)) return config diff --git a/apps/deploy-fe/src/app/layout.tsx b/apps/deploy-fe/src/app/layout.tsx index ce8a3ed..2c6d621 100644 --- a/apps/deploy-fe/src/app/layout.tsx +++ b/apps/deploy-fe/src/app/layout.tsx @@ -1,6 +1,6 @@ import { Providers } from '@/components/providers' import { ClerkProvider } from '@clerk/nextjs' -// import '@workspace/ui/globals.css' +import '@workspace/ui/globals.css' import type { Metadata } from 'next' import { Inter } from 'next/font/google' diff --git a/apps/deploy-fe/src/components/projects/project/ProjectCard/ProjectCard.tsx b/apps/deploy-fe/src/components/projects/project/ProjectCard/ProjectCard.tsx index 5478d50..1e5ed01 100644 --- a/apps/deploy-fe/src/components/projects/project/ProjectCard/ProjectCard.tsx +++ b/apps/deploy-fe/src/components/projects/project/ProjectCard/ProjectCard.tsx @@ -1,6 +1,13 @@ import { getInitials } from '@/utils/getInitials' -import { Avatar, AvatarFallback, AvatarImage } from '@radix-ui/react-avatar' + +import { + Avatar, + AvatarFallback, + AvatarImage +} from '@workspace/ui/components/avatar' + import type { Project } from '@workspace/gql-client' + import { Card, CardContent, CardHeader } from '@workspace/ui/components/card' import { AlertTriangle } from 'lucide-react' import { useRouter } from 'next/router' diff --git a/docs/architecture/wallet_migration/0-wallet-integration-overview.md b/docs/architecture/wallet_migration/0-wallet-integration-overview.md new file mode 100644 index 0000000..7e38dab --- /dev/null +++ b/docs/architecture/wallet_migration/0-wallet-integration-overview.md @@ -0,0 +1,92 @@ +# Laconic Wallet Integration Project + +## Overview + +This project migrates the Laconic wallet from an iframe-based implementation to a native Next.js 15 integration within our Turborepo. The goal is to eliminate cross-origin communication issues, improve security, and leverage Clerk authentication. + +## Architecture + +```mermaid +graph TD + subgraph "Frontend Application" + A[apps/deploy-fe] --> B[Clerk Auth] + A --> C[Wallet Integration] + end + + subgraph "Monorepo Packages" + D[services/wallet-core] --> E[Crypto Operations] + D --> F[State Management] + D --> G[Server Actions] + + H[services/ui/wallet] --> I[UI Components] + H --> J[Connection Modals] + end + + B --- K[GitHub OAuth] + B --- L[Wallet Auth] + + C --> D + C --> H + + L --> D +``` + +## Implementation Phases + +### Phase 1: Core Wallet Package (Weeks 1-2) +- Create `services/wallet-core` package +- Implement core wallet functionality +- Develop storage adapters +- Setup TypeScript interfaces + +### Phase 2: UI Integration (Weeks 3-4) +- Create wallet UI components in `services/ui` +- Implement client hooks +- Connect wallet state management +- Create transaction signing UI + +### Phase 3: Auth Integration (Weeks 5-6) +- Integrate with Clerk authentication +- Implement middleware protection +- Create unified auth flow +- Test complete flow + +## Tech Stack + +- **Frontend**: Next.js 15, React 19 +- **Authentication**: Clerk with GitHub OAuth +- **State Management**: React Context + Server Components +- **Styling**: Tailwind CSS with shadcn/ui +- **Wallet**: Cosmos/Ethereum crypto +- **Validation**: Zod for type-safe schema validation +- **Security**: SIWE (Sign-In With Ethereum) standard + +## Documentation References + +- [Next.js 15 Server Components](https://nextjs.org/docs/app/building-your-application/rendering/server-components) +- [Clerk Authentication](https://clerk.com/docs/references/nextjs/overview) +- [SIWE Protocol](https://docs.login.xyz/) +- [Cosmos SDK](https://docs.cosmos.network/main/architecture/adr-057-app-wiring) +- [Zod Schema Validation](https://zod.dev/) + +## Migration Goals + +1. **Remove iframe dependency**: Eliminate cross-origin communication issues +2. **Improve security**: Direct integration with authenticated routes +3. **Better UX**: Seamless authentication flow with GitHub and wallet +4. **Maintainability**: Proper separation of concerns in monorepo structure + +## Deliverables + +- `services/wallet-core` package +- Wallet UI components in `services/ui` +- Server actions for wallet operations +- Clerk integration for wallet authentication +- Comprehensive documentation + +## Dependencies + +- Clerk authentication setup +- Cosmos JS SDK +- Ethereum libraries +- SIWE (Sign-In With Ethereum) diff --git a/docs/architecture/wallet_migration/1-phase-1-wallet-core.md b/docs/architecture/wallet_migration/1-phase-1-wallet-core.md new file mode 100644 index 0000000..126d2cf --- /dev/null +++ b/docs/architecture/wallet_migration/1-phase-1-wallet-core.md @@ -0,0 +1,583 @@ +# Phase 1: Wallet Core Package Implementation + +## Overview + +Phase 1 establishes the foundation by creating the `services/wallet-core` package that will replace the iframe-based wallet implementation. This package will contain all core wallet functionality without UI components. + +## Timeline + +**Duration**: 2 weeks +**Dependencies**: None +**Team**: Backend/Crypto team + +## Directory Structure + +```mermaid +graph TD + A[services/wallet-core] --> B[src/] + B --> C[accounts/] + B --> D[networks/] + B --> E[storage/] + B --> F[crypto/] + B --> G[actions/] + B --> H[hooks/] + B --> I[types/] + + C --> C1[accounts.ts] + C --> C2[accountsContext.ts] + + D --> D1[networks.ts] + D --> D2[networksContext.ts] + D --> D3[constants.ts] + + E --> E1[keystore.ts] + E --> E2[localStorage.ts] + E --> E3[sessionStorage.ts] + + F --> F1[eth.ts] + F --> F2[cosmos.ts] + F --> F3[signing.ts] + + G --> G1[walletActions.ts] + + H --> H1[useWallet.ts] + H --> H2[useNetwork.ts] + H --> H3[useAccounts.ts] + + I --> I1[accounts.d.ts] + I --> I2[networks.d.ts] + I --> I3[storage.d.ts] +``` + +## Step-by-Step Implementation + +### 1. Set up `services/wallet-core` package + +```bash +mkdir -p services/wallet-core/src +cd services/wallet-core +``` + +Create `package.json`: + +```json +{ + "name": "@workspace/wallet-core", + "version": "0.1.0", + "private": true, + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "sideEffects": false, + "license": "MIT", + "scripts": { + "build": "tsup src/index.ts --format esm,cjs --dts", + "dev": "tsup src/index.ts --format esm,cjs --watch --dts", + "lint": "eslint src/", + "clean": "rm -rf .turbo dist node_modules", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@cosmjs/proto-signing": "^0.31.1", + "@cosmjs/stargate": "^0.31.1", + "ethers": "^6.8.1", + "siwe": "^2.1.4", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^20.5.2", + "@types/react": "^18.2.0", + "@workspace/typescript-config": "*", + "eslint": "^8.46.0", + "typescript": "^5.1.6", + "tsup": "^7.2.0" + }, + "peerDependencies": { + "next": "^15.0.0", + "react": "^19.0.0" + } +} +``` + +Create `tsconfig.json`: + +```json +{ + "extends": "@workspace/typescript-config/react-library.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src", "../../services/ui/tailwind.config.ts"], + "exclude": ["node_modules", "dist"] +} +``` + +### 2. Implement core types and validation schemas + +Create `src/types/accounts.d.ts`: + +```typescript +export interface Account { + index: number; + address: string; + hdPath: string; + pubKey: string; +} + +export interface WalletState { + accounts: Account[]; + currentIndex: number; + isConnected: boolean; + isReady: boolean; +} + +// Validation schemas (using Zod) +import { z } from 'zod'; + +// Pattern for valid Ethereum address +const ethereumAddressPattern = /^0x[a-fA-F0-9]{40}$/; +// Pattern for valid Cosmos address (using laconic prefix) +const cosmosAddressPattern = /^laconic[a-zA-Z0-9]{39,59}$/; + +export const accountSchema = z.object({ + index: z.number().int().nonnegative(), + address: z.string().refine( + val => ethereumAddressPattern.test(val) || cosmosAddressPattern.test(val), + { message: "Invalid wallet address format" } + ), + hdPath: z.string(), + pubKey: z.string() +}); +``` + +Create `src/types/networks.d.ts`: + +```typescript +export interface NetworksFormData { + chainId: string; + networkName: string; + namespace: string; + rpcUrl: string; + blockExplorerUrl: string; + nativeDenom?: string; + addressPrefix?: string; + coinType: string; + gasPrice?: string; + isDefault?: boolean; + currencySymbol?: string; +} + +export interface NetworksDataState extends NetworksFormData { + networkId: string; +} + +export interface NetworkState { + networks: NetworksDataState[]; + selectedNetwork?: NetworksDataState; + networkType: string; +} +``` + +Create `src/types/index.ts`: + +```typescript +export * from './accounts'; +export * from './networks'; +export * from './storage'; +``` + +### 3. Implement storage adapters + +Create `src/storage/keystore.ts`: + +```typescript +// A next.js-friendly implementation of the key store +// ⚠️ SECURITY WARNING: This implementation is for demonstration purposes only +// Storing sensitive wallet data including private keys in browser storage can be a security risk +// For production, consider using a secure hardware wallet or a dedicated wallet provider + +// In place of localStorage in browser environments +export function setInternetCredentials(name: string, username: string, password: string): void { + if (typeof window !== 'undefined') { + sessionStorage.setItem(name, password); + } +} + +export function getInternetCredentials(name: string): string | null { + if (typeof window !== 'undefined') { + return sessionStorage.getItem(name); + } + return null; +} + +export function resetInternetCredentials(name: string): void { + if (typeof window !== 'undefined') { + sessionStorage.removeItem(name); + } +} + +// Helper to encrypt sensitive data +// In a production environment, consider using the Web Crypto API or a dedicated encryption library +export function encryptSensitiveData(data: string): string { + // Implement proper encryption in production + // This is a placeholder to emphasize sensitive data should be encrypted + return data; +} +``` + +### 4. Implement networks functionality + +Create `src/networks/constants.ts`: + +```typescript +import { NetworksFormData } from '../types'; + +export const EIP155 = 'eip155'; +export const COSMOS = 'cosmos'; + +export const DEFAULT_NETWORKS: NetworksFormData[] = [ + { + chainId: 'laconic-testnet-2', + networkName: 'laconicd testnet-2', + namespace: COSMOS, + rpcUrl: process.env.NEXT_PUBLIC_LACONICD_RPC_URL!, + blockExplorerUrl: '', + nativeDenom: 'alnt', + addressPrefix: 'laconic', + coinType: '118', + gasPrice: '0.001', + isDefault: true, + }, + { + chainId: '1', + networkName: 'Ethereum Mainnet', + namespace: EIP155, + rpcUrl: 'https://mainnet.infura.io/v3/your-key', + blockExplorerUrl: '', + currencySymbol: 'ETH', + coinType: '60', + isDefault: true, + }, +]; +``` + +Create `src/networks/networks.ts`: + +```typescript +import { NetworksDataState, NetworksFormData } from '../types'; +import { getInternetCredentials, setInternetCredentials } from '../storage/keystore'; +import { DEFAULT_NETWORKS } from './constants'; + +export async function retrieveNetworksData(): Promise { + console.log("Retrieving networks data"); + const networks = await getInternetCredentials('networks'); + + if(!networks){ + console.log("No networks found in credentials, using DEFAULT_NETWORKS"); + + // Convert NetworksFormData to NetworksDataState by adding networkId + const defaultNetworksWithId = DEFAULT_NETWORKS.map((network, index) => ({ + ...network, + networkId: String(index) + })); + + // Store default networks in credentials + await setInternetCredentials( + 'networks', + '_', + JSON.stringify(defaultNetworksWithId) + ); + + return defaultNetworksWithId; + } + + console.log("Networks found in credentials"); + const parsedNetworks: NetworksDataState[] = JSON.parse(networks); + + return parsedNetworks; +} + +export async function storeNetworkData( + networkData: NetworksFormData, +): Promise { + const networks = await getInternetCredentials('networks'); + let retrievedNetworks = []; + if (networks) { + retrievedNetworks = JSON.parse(networks!); + } + let networkId = 0; + if (retrievedNetworks.length > 0) { + networkId = retrievedNetworks[retrievedNetworks.length - 1].networkId + 1; + } + + const updatedNetworks: NetworksDataState[] = [ + ...retrievedNetworks, + { + ...networkData, + networkId: String(networkId), + }, + ]; + await setInternetCredentials( + 'networks', + '_', + JSON.stringify(updatedNetworks), + ); + return updatedNetworks; +} +``` + +### 5. Implement crypto functionality + +Create `src/crypto/signing.ts`: + +```typescript +import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing'; +import { ethers } from 'ethers'; +import { SiweMessage } from 'siwe'; + +import { getInternetCredentials } from '../storage/keystore'; +import { COSMOS, EIP155 } from '../networks/constants'; + +interface SignMessageParams { + message: string; + namespace: string; + chainId: string; + accountId: number; +} + +export async function signMessage({ + message, + namespace, + chainId, + accountId, +}: SignMessageParams): Promise { + const path = await getPathKey(`${namespace}:${chainId}`, accountId); + + switch (namespace) { + case EIP155: + return await signEthMessage(message, accountId, chainId); + case COSMOS: + return await signCosmosMessage(message, path.path, path.address); + default: + throw new Error('Invalid wallet type'); + } +} + +async function signEthMessage( + message: string, + accountId: number, + chainId: string, +): Promise { + try { + const privKey = (await getPathKey(`${EIP155}:${chainId}`, accountId)) + .privKey; + const wallet = new ethers.Wallet(privKey); + const signature = await wallet.signMessage(message); + + return signature; + } catch (error) { + console.error('Error signing Ethereum message:', error); + throw error; + } +} + +async function signCosmosMessage( + message: string, + path: string, + cosmosAddress: string, +): Promise { + // Implementation for Cosmos signing... + return "cosmos_signature_placeholder"; +} + +async function getPathKey( + namespaceChainId: string, + accountId: number, +): Promise<{ + path: string; + privKey: string; + pubKey: string; + address: string; +}> { + const pathKeyStore = await getInternetCredentials( + `accounts/${namespaceChainId}/${accountId}`, + ); + + if (!pathKeyStore) { + throw new Error('Error while fetching key data'); + } + + const pathKeyVal = pathKeyStore; + const pathkey = pathKeyVal.split(','); + const path = pathkey[0]; + const privKey = pathkey[1]; + const pubKey = pathkey[2]; + const address = pathkey[3]; + + return { path, privKey, pubKey, address }; +} + +export async function createSiweMessage(address: string, statement: string = 'Sign in With Ethereum.'): Promise { + const message = new SiweMessage({ + version: '1', + domain: typeof window !== 'undefined' ? window.location.host : '', + uri: typeof window !== 'undefined' ? window.location.origin : '', + chainId: 1, + address: address, + statement, + }).prepareMessage(); + + return message; +} +``` + +### 6. Create client hooks + +Create `src/hooks/useWallet.ts`: + +```typescript +import { useCallback, useState } from 'react'; +import { createSiweMessage, signMessage } from '../crypto/signing'; +import { Account } from '../types'; + +export function useWallet() { + const [accounts, setAccounts] = useState([]); + const [currentIndex, setCurrentIndex] = useState(0); + const [isConnected, setIsConnected] = useState(false); + const [isReady, setIsReady] = useState(false); + + const connect = useCallback(async () => { + // Implementation will come in Phase 2 + console.log('Connect wallet functionality'); + }, []); + + const disconnect = useCallback(() => { + setAccounts([]); + setIsConnected(false); + setIsReady(false); + }, []); + + const signIn = useCallback(async () => { + if (!accounts.length || currentIndex >= accounts.length) { + throw new Error('No account selected'); + } + + const account = accounts[currentIndex]; + const message = await createSiweMessage(account.address); + + try { + // Placeholder for the actual implementation + console.log('Sign in with wallet'); + return { success: true }; + } catch (error) { + console.error('Error signing in:', error); + return { success: false, error }; + } + }, [accounts, currentIndex]); + + return { + accounts, + setAccounts, + currentIndex, + setCurrentIndex, + isConnected, + setIsConnected, + isReady, + setIsReady, + connect, + disconnect, + signIn, + }; +} +``` + +### 7. Implement server actions + +Create `src/actions/walletActions.ts`: + +```typescript +'use server' + +import { cookies } from 'next/headers'; +import { createSiweMessage } from '../crypto/signing'; + +export async function validateSignature(message: string, signature: string) { + // This will be implemented in Phase 2 + // Verifies the signature on the server side + return { success: true }; +} + +export async function storeWalletSession(address: string) { + // Store wallet address in session cookie + cookies().set('wallet_address', address, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 60 * 60 * 24 * 7, // 1 week + path: '/', + }); + + return { success: true }; +} + +export async function getWalletSession() { + return cookies().get('wallet_address')?.value; +} + +export async function clearWalletSession() { + cookies().delete('wallet_address'); + return { success: true }; +} + +export async function checkBalance(chainId: string, address: string, amount: string) { + // This will connect to the blockchain RPC and check balance + // Implementation will come in Phase 2 + + // Mock implementation for now + return { + success: true, + hasEnoughBalance: true, + balance: "1000.00" + }; +} +``` + +### 8. Create package exports + +Create `src/index.ts`: + +```typescript +// Export core functionality +export * from './types'; +export * from './networks/constants'; +export * from './networks/networks'; +export * from './crypto/signing'; +export * from './hooks/useWallet'; +export * from './storage/keystore'; +export * from './actions/walletActions'; +``` + +## Testing + +1. Build the package: +```bash +cd services/wallet-core +pnpm build +``` + +2. Test in the frontend app by adding the dependency: +```json +"dependencies": { + "@workspace/wallet-core": "workspace:*", + // other dependencies +} +``` + +## Next Steps + +- Phase 2: Implement UI components in `services/ui/wallet` +- Phase 2: Integrate with the Next.js frontend app +- Phase 3: Complete Clerk auth integration diff --git a/docs/architecture/wallet_migration/2-phase-2-wallet-ui.md b/docs/architecture/wallet_migration/2-phase-2-wallet-ui.md new file mode 100644 index 0000000..fb35f44 --- /dev/null +++ b/docs/architecture/wallet_migration/2-phase-2-wallet-ui.md @@ -0,0 +1,592 @@ +# Phase 2: Wallet UI Components Implementation + +## Overview + +Phase 2 focuses on building UI components for the wallet and integrating them with the Next.js frontend. This phase transforms the core wallet functionality into usable UI components and connects them to the application. + +## Timeline + +**Duration**: 2 weeks +**Dependencies**: Phase 1 (wallet-core package) +**Team**: Frontend team + +## Directory Structure + +```mermaid +graph TD + A[services/ui/src/wallet] --> B[components/] + A --> C[hooks/] + A --> D[providers/] + + B --> B1[WalletConnectButton.tsx] + B --> B2[WalletModal.tsx] + B --> B3[TransactionApproval.tsx] + B --> B4[SignMessageModal.tsx] + B --> B5[AccountSelector.tsx] + B --> B6[NetworkSelector.tsx] + B --> B7[BalanceDisplay.tsx] + + C --> C1[useWalletUI.ts] + C --> C2[useTransaction.ts] + + D --> D1[WalletUIProvider.tsx] + + E[apps/deploy-fe/src] --> F[components/wallet/] + F --> F1[WalletProvider.tsx] + F --> F2[ConnectWallet.tsx] + + G[apps/deploy-fe/src/app/api] --> H[wallet/] + H --> H1[balance/route.ts] + H --> H2[sign/route.ts] + H --> H3[connect/route.ts] +``` + +## Migration Reference + +These implementations adapt functionality from: + +- `WalletModal.tsx` → from `/repos/laconic-wallet-web/src/components/wallet/AutoSignInIFrameModal.tsx` +- `TransactionApproval.tsx` → from `/repos/laconic-wallet-web/src/screens/ApproveTransaction.tsx` +- `WalletUIProvider.tsx` → from `/repos/laconic-wallet-web/src/context/WalletContextProvider.tsx` + +## Security Considerations + +Before implementation, please note these important security considerations: + +1. **CORS Configuration**: API routes must have proper CORS configuration to prevent unauthorized access +2. **Input Validation**: All user input and transaction data must be validated server-side using Zod or similar +3. **Error Handling**: Implement comprehensive error handling to prevent information leakage +4. **Key Management**: Never expose private keys in client-side code +5. **Signature Verification**: Always verify signatures on the server-side +6. **Responsive Design**: Wallet UI components should be mobile-responsive with proper accessibility attributes + +## Step-by-Step Implementation + +### 1. Create Wallet UI Components + +Create the directory structure: + +```bash +mkdir -p services/ui/src/wallet/components +mkdir -p services/ui/src/wallet/hooks +mkdir -p services/ui/src/wallet/providers +``` + +### 2. Implement Key Components + +Create `services/ui/src/wallet/components/WalletConnectButton.tsx`: + +```tsx +'use client' + +import { Button } from '@workspace/ui/components/button' +import React from 'react' +import { useWalletUI } from '../hooks/useWalletUI' + +interface WalletConnectButtonProps { + variant?: 'default' | 'outline' | 'ghost' + size?: 'default' | 'sm' | 'lg' +} + +export function WalletConnectButton({ + variant = 'default', + size = 'default' +}: WalletConnectButtonProps) { + const { isConnected, connect, disconnect, wallet } = useWalletUI() + + return ( + + ) +} +``` + +Create `services/ui/src/wallet/components/WalletModal.tsx`: + +```tsx +'use client' + +import React, { useEffect } from 'react' +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@workspace/ui/components/dialog' +import { Button } from '@workspace/ui/components/button' +import { useWalletUI } from '../hooks/useWalletUI' +import { signIn } from '@workspace/wallet-core' + +export function WalletModal() { + const { isOpen, closeModal, wallet, setWallet, connectWallet } = useWalletUI() + + useEffect(() => { + async function handleSignIn() { + if (wallet?.address) { + try { + // Replace the direct iframe messaging with server action + await signIn(wallet.address) + } catch (error) { + console.error('Error during sign-in:', error) + } + } + } + + if (wallet?.address) { + handleSignIn() + } + }, [wallet]) + + return ( + + + + Connect your wallet + +
+ +
+
+
+ ) +} +``` + +Create `services/ui/src/wallet/components/TransactionApproval.tsx`: + +```tsx +'use client' + +import React, { useState } from 'react' +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle +} from '@workspace/ui/components/card' +import { Button } from '@workspace/ui/components/button' +import { useWalletUI } from '../hooks/useWalletUI' + +interface TransactionApprovalProps { + amount: string + recipient: string + denom: string + onApprove: () => void + onReject: () => void +} + +export function TransactionApproval({ + amount, + recipient, + denom, + onApprove, + onReject +}: TransactionApprovalProps) { + const { wallet } = useWalletUI() + const [isProcessing, setIsProcessing] = useState(false) + + const handleApprove = async () => { + setIsProcessing(true) + try { + await onApprove() + } finally { + setIsProcessing(false) + } + } + + return ( + + + Approve Transaction + Review and approve this transaction + + +
+
+
From:
+
+ {wallet?.address || 'Not connected'} +
+
+
+
To:
+
{recipient}
+
+
+
Amount:
+
+ {amount} {denom} +
+
+
+
+ + + + +
+ ) +} +``` + +### 3. Implement UI Hooks + +Create `services/ui/src/wallet/hooks/useWalletUI.ts`: + +```tsx +'use client' + +import { useEffect, useState } from 'react' +import { useWallet } from '@workspace/wallet-core' +import { toast } from 'sonner' + +export function useWalletUI() { + const walletCore = useWallet() + const [isOpen, setIsOpen] = useState(false) + + const openModal = () => setIsOpen(true) + const closeModal = () => setIsOpen(false) + + const connectWallet = async () => { + try { + // This now uses server actions instead of iframe messaging + await walletCore.connect() + closeModal() + toast.success('Wallet connected') + } catch (error) { + toast.error('Failed to connect wallet') + console.error(error) + } + } + + const disconnect = () => { + walletCore.disconnect() + toast.info('Wallet disconnected') + } + + // Expose the core wallet methods and UI-specific methods + return { + ...walletCore, + isOpen, + openModal, + closeModal, + connectWallet + } +} +``` + +### 4. Implement Provider + +Create `services/ui/src/wallet/providers/WalletUIProvider.tsx`: + +```tsx +'use client' + +import React, { createContext, useContext, useEffect, useState } from 'react' +import { WalletProvider } from '@workspace/wallet-core' +import { WalletModal } from '../components/WalletModal' +import { toast } from 'sonner' + +interface WalletUIContextType { + openWalletModal: () => void + closeWalletModal: () => void + isModalOpen: boolean +} + +const WalletUIContext = createContext({ + openWalletModal: () => {}, + closeWalletModal: () => {}, + isModalOpen: false +}) + +export function useWalletUIContext() { + return useContext(WalletUIContext) +} + +export function WalletUIProvider({ children }: { children: React.ReactNode }) { + const [isModalOpen, setIsModalOpen] = useState(false) + + const openWalletModal = () => setIsModalOpen(true) + const closeWalletModal = () => setIsModalOpen(false) + + return ( + + + {children} + + + + ) +} +``` + +### 5. Create Index Exports + +Create `services/ui/src/wallet/index.ts`: + +```typescript +// Component exports +export { WalletConnectButton } from './components/WalletConnectButton' +export { WalletModal } from './components/WalletModal' +export { TransactionApproval } from './components/TransactionApproval' +export { AccountSelector } from './components/AccountSelector' +export { NetworkSelector } from './components/NetworkSelector' +export { BalanceDisplay } from './components/BalanceDisplay' + +// Hook exports +export { useWalletUI } from './hooks/useWalletUI' +export { useTransaction } from './hooks/useTransaction' + +// Provider exports +export { WalletUIProvider, useWalletUIContext } from './providers/WalletUIProvider' +``` + +### 6. Create API Routes for Wallet Communication + +Create `apps/deploy-fe/src/app/api/wallet/balance/route.ts`: + +```typescript +import { checkBalance } from '@workspace/wallet-core' +import { auth } 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 body = await request.json() + const { chainId, address, amount } = body + + const result = await checkBalance(chainId, address, amount) + + return NextResponse.json(result) + } catch (error) { + console.error('Balance check error:', error) + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ) + } +} +``` + +Create `apps/deploy-fe/src/app/api/wallet/sign/route.ts`: + +```typescript +import { validateSignature } from '@workspace/wallet-core' +import { auth } from '@clerk/nextjs/server' +import { NextResponse } from 'next/server' +import { z } from 'zod' + +// Define validation schema +const signRequestSchema = z.object({ + message: z.string().min(1, "Message is required"), + signature: z.string().min(1, "Signature is required") +}) + +export async function POST(request: Request) { + const { userId } = await auth() + + if (!userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + try { + const body = await request.json() + + // Validate input data + const result = signRequestSchema.safeParse(body) + if (!result.success) { + return NextResponse.json( + { error: 'Invalid request data', details: result.error.format() }, + { status: 400 } + ) + } + + const { message, signature } = result.data + + // Add additional validation for signature format if needed + // (e.g., check if it matches expected pattern) + + const validationResult = await validateSignature(message, signature) + + return NextResponse.json(validationResult) + } catch (error) { + console.error('Signature validation error:', error) + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ) + } +} +``` + +### 7. Integrate with Next.js App + +Create `apps/deploy-fe/src/components/wallet/WalletProvider.tsx`: + +```tsx +'use client' + +import { ReactNode } from 'react' +import { WalletUIProvider } from '@workspace/ui/wallet' + +export function WalletProvider({ children }: { children: ReactNode }) { + return {children} +} +``` + +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' + +export function Providers({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} +``` + +## Integration Example + +Create a wallet connection button in `apps/deploy-fe/src/components/foundation/top-navigation/TopNavigation.tsx`: + +```tsx +'use client' + +import React from 'react' +import { WalletConnectButton } from '@workspace/ui/wallet' + +export function TopNavigation() { + return ( +
+
+ +
+
+ ) +} +``` + +Replace `apps/deploy-fe/src/components/iframe/check-balance-iframe/CheckBalanceIframe.tsx` with server action: + +```tsx +'use client' + +import { useEffect } from 'react' +import { checkBalance } from '@workspace/wallet-core' + +interface CheckBalanceProps { + onBalanceChange: (value: boolean | undefined) => void + isPollingEnabled: boolean + amount: string +} + +export default function CheckBalance({ + onBalanceChange, + isPollingEnabled, + amount +}: CheckBalanceProps) { + useEffect(() => { + let interval: NodeJS.Timeout + + const fetchBalance = async () => { + try { + // Uses server action instead of iframe + const chainId = process.env.NEXT_PUBLIC_LACONICD_CHAIN_ID || '' + const result = await fetch('/api/wallet/balance', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ chainId, amount }) + }).then(res => res.json()) + + onBalanceChange(result.hasEnoughBalance) + } catch (error) { + console.error('Error checking balance:', error) + onBalanceChange(undefined) + } + } + + fetchBalance() + + if (isPollingEnabled) { + interval = setInterval(fetchBalance, 5000) + } + + return () => { + if (interval) clearInterval(interval) + } + }, [amount, isPollingEnabled, onBalanceChange]) + + // This component doesn't render anything visually + return null +} +``` + +## Testing + +1. Build the UI package: +```bash +cd services/ui +pnpm build +``` + +2. Run the frontend app: +```bash +cd apps/deploy-fe +pnpm dev +``` + +3. Test wallet functionality: + - Connection button in navigation + - Login flow with wallet + - Balance checks + - Transaction signing + +## Next Steps + +- Phase 3: Complete Clerk auth integration +- Phase 3: Implement comprehensive middleware +- Phase 3: Develop unified authentication flow diff --git a/docs/architecture/wallet_migration/3-phase-3-clerk-integration.md b/docs/architecture/wallet_migration/3-phase-3-clerk-integration.md new file mode 100644 index 0000000..c33cf1e --- /dev/null +++ b/docs/architecture/wallet_migration/3-phase-3-clerk-integration.md @@ -0,0 +1,754 @@ +# 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 ( + +
+

Connect Your Wallet

+

+ Connect your wallet to access additional features like deployments + and blockchain-related functionality. +

+ +
+ {!isConnected ? ( + + ) : ( +
+

+ Wallet connected: {wallet?.address?.slice(0, 6)}...{wallet?.address?.slice(-4)} +

+ +
+ )} + + +
+
+
+ ) +} +``` + +### 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
+ } + + return ( +
+

Wallet

+ + {walletConnected && walletAddress ? ( +
+
+
+

Connected Address:

+

+ {walletAddress.slice(0, 8)}...{walletAddress.slice(-6)} +

+
+ +
+
+ ) : ( +
+

+ No wallet connected to your account +

+ +
+ )} +
+ ) +} +``` + +### 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() + + 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
+ } + + 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(undefined) + +export function WalletStatusProvider({ children }: { children: React.ReactNode }) { + const { user, isLoaded } = useUser() + const [isLoading, setIsLoading] = useState(true) + const [walletStatus, setWalletStatus] = useState({ + 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 ( + + {children} + + ) +} + +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 ( + + + {children} + + + ) +} +``` + +### 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 ( +
+
+ +
+
+ ) +} +``` + +## 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 diff --git a/package.json b/package.json index ea3b9ee..68f0c59 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "build": "turbo build", "dev": "turbo dev", "lint": "turbo lint", + "start": "turbo start", "lint:fix": "turbo lint:fix", "format": "turbo format", "format:fix": "turbo format:fix", diff --git a/turbo.json b/turbo.json index f38d22c..34c4ce7 100644 --- a/turbo.json +++ b/turbo.json @@ -8,6 +8,11 @@ "outputs": [".next/**", "!.next/cache/**"], "env": ["NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY", "CLERK_SECRET_KEY"] }, + "start": { + "dependsOn": ["^build"], + "cache": false, + "persistent": true + }, "check-types": { "dependsOn": ["^build"], "outputs": []