14 KiB
14 KiB
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
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
mkdir -p services/wallet-core/src
cd services/wallet-core
Create package.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
:
{
"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
:
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
:
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
:
export * from './accounts';
export * from './networks';
export * from './storage';
3. Implement storage adapters
Create src/storage/keystore.ts
:
// 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
:
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
:
import { NetworksDataState, NetworksFormData } from '../types';
import { getInternetCredentials, setInternetCredentials } from '../storage/keystore';
import { DEFAULT_NETWORKS } from './constants';
export async function retrieveNetworksData(): Promise<NetworksDataState[]> {
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<NetworksDataState[]> {
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
:
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<string | undefined> {
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<string | undefined> {
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<string | undefined> {
// 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<string> {
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
:
import { useCallback, useState } from 'react';
import { createSiweMessage, signMessage } from '../crypto/signing';
import { Account } from '../types';
export function useWallet() {
const [accounts, setAccounts] = useState<Account[]>([]);
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
:
'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
:
// 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
- Build the package:
cd services/wallet-core
pnpm build
- Test in the frontend app by adding the dependency:
"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