# 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