diff --git a/docs/api-reference/index.md b/docs/api-reference/index.md index 8378e90..2b535a7 100644 --- a/docs/api-reference/index.md +++ b/docs/api-reference/index.md @@ -134,7 +134,7 @@ interface ClearNodeConfig { } const config = { - endpoint: 'wss://clearnet.yellow.com/ws', + endpoint: 'wss://clearnet-sandbox.yellow.com/ws', // or wss://clearnet.yellow.com/ws for production timeout: 30000, retryAttempts: 3 }; @@ -224,8 +224,8 @@ try { ```typescript const CLEARNODE_ENDPOINTS = { - MAINNET: 'wss://clearnet.yellow.com/ws', - TESTNET: 'wss://testnet.clearnet.yellow.com/ws', + PRODUCTION: 'wss://clearnet.yellow.com/ws', + SANDBOX: 'wss://clearnet-sandbox.yellow.com/ws', LOCAL: 'ws://localhost:8080/ws' }; diff --git a/docs/build/quick-start/index.md b/docs/build/quick-start/index.md index d846148..3cab60d 100644 --- a/docs/build/quick-start/index.md +++ b/docs/build/quick-start/index.md @@ -68,13 +68,18 @@ pnpm add @erc7824/nitrolite ## Step 2: Connect to ClearNode -Create a file `app.js` and connect to the Yellow Network: +Create a file `app.js` and connect to the Yellow Network. + +:::tip Clearnode Endpoints +- **Production**: `wss://clearnet.yellow.com/ws` +- **Sandbox**: `wss://clearnet-sandbox.yellow.com/ws` (recommended for testing) +::: ```javascript title="app.js" showLineNumbers import { createAppSessionMessage, parseRPCResponse } from '@erc7824/nitrolite'; -// Connect to Yellow Network -const ws = new WebSocket('wss://clearnet.yellow.com/ws'); +// Connect to Yellow Network (using sandbox for testing) +const ws = new WebSocket('wss://clearnet-sandbox.yellow.com/ws'); ws.onopen = () => { console.log('✅ Connected to Yellow Network!'); @@ -245,8 +250,8 @@ class SimplePaymentApp { this.userAddress = userAddress; this.messageSigner = messageSigner; - // Step 2: Connect to ClearNode - this.ws = new WebSocket('wss://clearnet.yellow.com/ws'); + // Step 2: Connect to ClearNode (sandbox for testing) + this.ws = new WebSocket('wss://clearnet-sandbox.yellow.com/ws'); this.ws.onopen = () => { console.log('🟢 Connected to Yellow Network!'); diff --git a/docs/learn/advanced/architecture.md b/docs/learn/advanced/architecture.md deleted file mode 100644 index ebb729c..0000000 --- a/docs/learn/advanced/architecture.md +++ /dev/null @@ -1,293 +0,0 @@ ---- -sidebar_position: 2 -title: Architecture Deep Dive -description: Understanding state channel fundamentals and SDK architecture -keywords: [state channels, architecture, nitrolite, sdk, blockchain scaling] -displayed_sidebar: learnSidebar ---- - -# Architecture Deep Dive - -## State Channel Fundamentals - -State channels are the foundation of Yellow Apps. Understanding their mechanics is crucial for building robust applications: - -```mermaid -graph TD - A[Deposit Funds] --> B[Create Channel] - B --> C[Connect to ClearNode] - C --> D[Create App Session] - D --> E[Off-Chain Transactions] - E --> F[Close Session] - F --> G[Settle & Withdraw] -``` - -### How State Channels Work - -1. **Setup**: Lock funds in a smart contract between participants -2. **Operate**: Exchange signed state updates instantly off-chain -3. **Settle**: Submit final state to blockchain for fund distribution - -This enables applications to achieve: -- **Instant finality** - no waiting for block confirmations -- **Minimal gas costs** - pay only for setup and settlement -- **Unlimited throughput** - thousands of transactions per second - -### Core Data Structures - -```typescript title="types.ts" showLineNumbers -interface Channel { - participants: Address[]; // Channel participants - adjudicator: Address; // Contract that validates state transitions - challenge: bigint; // Dispute resolution period - nonce: bigint; // Unique channel identifier -} - -interface State { - intent: StateIntent; // Purpose of the state - version: bigint; // Incremental version number - data: Hex; // Application-specific data - allocations: Allocation[]; // Fund distribution - sigs: Hex[]; // Participant signatures -} - -enum StateIntent { - OPERATE = 0, // Normal operation - INITIALIZE = 1, // Channel funding - RESIZE = 2, // Allocation adjustment - FINALIZE = 3 // Channel closure -} -``` - -## SDK Architecture - -### NitroliteRPC - -The core communication protocol for real-time interaction with ClearNode infrastructure: - -```javascript title="nitro-client.js" showLineNumbers -import { createAppSessionMessage, parseRPCResponse } from '@erc7824/nitrolite'; - -// Connect to ClearNode -const ws = new WebSocket('wss://clearnet.yellow.com/ws'); - -// Create application session -const sessionMessage = await createAppSessionMessage(messageSigner, [{ - definition: appDefinition, - allocations: initialAllocations -}]); - -// Send to ClearNode -ws.send(sessionMessage); - -// Parse responses -ws.onmessage = (event) => { - const message = parseRPCResponse(event.data); - console.log('Received:', message); -}; -``` - -### Message Signing - -Secure message authentication using wallet signatures: - -```javascript title="message-signing.js" showLineNumbers -// Set up message signer with your wallet -const messageSigner = async (message) => { - return await window.ethereum.request({ - method: 'personal_sign', - params: [message, userAddress] - }); -}; - -// Use signer for session creation -const sessionMessage = await createAppSessionMessage(messageSigner, sessionData); -``` - -### ClearNode Infrastructure - -Network infrastructure providing: -- **Message routing**: Secure communication between participants -- **Session management**: Application session lifecycle -- **State coordination**: Off-chain state synchronization -- **Network resilience**: Redundancy and failover support - -## Application Session Management - -### Session Lifecycle - -```javascript title="SessionManager.js" showLineNumbers -class SessionManager { - constructor() { - this.activeSessions = new Map(); - this.ws = null; - this.messageSigner = null; - } - - async createSession(participants, protocol, allocations) { - const appDefinition = { - protocol, - participants, - weights: participants.map(() => 100 / participants.length), - quorum: 51, // Majority consensus - challenge: 0, - nonce: Date.now() - }; - - const sessionMessage = await createAppSessionMessage( - this.messageSigner, - [{ definition: appDefinition, allocations }] - ); - - // Send session creation request to ClearNode - this.ws.send(sessionMessage); - - return this.waitForSessionConfirmation(); - } - - async sendSessionMessage(sessionId, messageType, data) { - const message = { - sessionId, - type: messageType, - data, - timestamp: Date.now() - }; - - // Sign message - const signature = await this.messageSigner(JSON.stringify(message)); - - this.ws.send(JSON.stringify({ - ...message, - signature - })); - } - - handleSessionMessage(message) { - const parsedMessage = parseRPCResponse(message); - const { sessionId, type, data } = parsedMessage; - - switch (type) { - case 'state_update': - this.handleStateUpdate(sessionId, data); - break; - case 'participant_joined': - this.handleParticipantJoined(sessionId, data); - break; - case 'session_closed': - this.handleSessionClosed(sessionId, data); - break; - } - } -} -``` - -## State Management Patterns - -### Client-Side State Tracking - -```javascript title="StateTracker.js" showLineNumbers -class StateTracker { - constructor() { - this.channelStates = new Map(); - this.stateHistory = new Map(); - } - - updateChannelState(channelId, newState) { - // Validate state progression - const currentState = this.channelStates.get(channelId); - if (currentState && newState.version <= currentState.version) { - throw new Error('Invalid state version'); - } - - // Store state - this.channelStates.set(channelId, newState); - - // Maintain history for dispute resolution - if (!this.stateHistory.has(channelId)) { - this.stateHistory.set(channelId, []); - } - this.stateHistory.get(channelId).push(newState); - - // Emit event for UI updates - this.emit('stateUpdated', { channelId, state: newState }); - } - - getStateHistory(channelId) { - return this.stateHistory.get(channelId) || []; - } - - getLatestState(channelId) { - return this.channelStates.get(channelId); - } -} -``` - -### Reactive Updates - -```javascript title="ReactiveChannelManager.js" showLineNumbers -class ReactiveChannelManager { - constructor() { - this.stateSubjects = new Map(); - } - - getChannelObservable(channelId) { - if (!this.stateSubjects.has(channelId)) { - this.stateSubjects.set(channelId, new BehaviorSubject(null)); - } - return this.stateSubjects.get(channelId); - } - - updateChannelState(channelId, newState) { - const subject = this.getChannelObservable(channelId); - subject.next(newState); - } - - subscribeToChannel(channelId, callback) { - return this.getChannelObservable(channelId).subscribe(callback); - } -} -``` - -## Integration Patterns - -### Event-Driven Architecture - -```javascript title="EventDrivenApp.js" showLineNumbers -class EventDrivenApp extends EventEmitter { - constructor(config) { - super(); - this.client = new NitroliteClient(config); - this.setupEventHandlers(); - } - - setupEventHandlers() { - this.on('channel:created', this.onChannelCreated.bind(this)); - this.on('message:received', this.onMessageReceived.bind(this)); - this.on('session:closed', this.onSessionClosed.bind(this)); - } - - async onChannelCreated(channelId) { - // Automatically connect to ClearNode - await this.connectToClearNode(channelId); - - // Set up session - await this.createApplicationSession(channelId); - - this.emit('app:ready', channelId); - } - - onMessageReceived(channelId, message) { - // Route message based on type - switch (message.type) { - case 'payment': - this.handlePayment(channelId, message); - break; - case 'game_move': - this.handleGameMove(channelId, message); - break; - } - } -} -``` - -Understanding these SDK patterns is essential for building robust, scalable Yellow Apps focused on application development rather than protocol implementation. \ No newline at end of file diff --git a/docs/learn/advanced/deployment.md b/docs/learn/advanced/deployment.md deleted file mode 100644 index eb43ecc..0000000 --- a/docs/learn/advanced/deployment.md +++ /dev/null @@ -1,757 +0,0 @@ ---- -sidebar_position: 5 -title: Production Deployment -description: Deploy Yellow Apps to production with confidence - configuration, optimization, and best practices -keywords: [production, deployment, configuration, optimization, mainnet, scaling] -displayed_sidebar: learnSidebar ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -# Production Deployment - -Deploy your Yellow App to production with confidence using battle-tested configurations and optimization strategies. - -## Environment Configuration - - - - -```javascript title="production-config.js" showLineNumbers -// Production configuration for Polygon mainnet -const productionConfig = { - chainId: 137, - addresses: { - custody: '0x...', // Production custody contract - adjudicator: '0x...', // Your deployed adjudicator - tokenAddress: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174' // USDC on Polygon - }, - clearNodeUrl: 'wss://clearnet.yellow.com/ws', - challengeDuration: 7200n, // 2 hours for mainnet - reconnectConfig: { - maxAttempts: 10, - backoffMultiplier: 1.5, - initialDelay: 1000 - } -}; -``` - - - - -```javascript title="testnet-config.js" showLineNumbers -// Testnet configuration for development -const testnetConfig = { - chainId: 80001, // Mumbai testnet - addresses: { - custody: '0x...', // Testnet custody contract - adjudicator: '0x...', // Your test adjudicator - tokenAddress: '0x...' // Test USDC - }, - clearNodeUrl: 'wss://testnet.clearnet.yellow.com/ws', - challengeDuration: 100n, // Shorter for testing - reconnectConfig: { - maxAttempts: 3, - backoffMultiplier: 2, - initialDelay: 500 - } -}; -``` - - - - -## Error Handling & Recovery - -### Robust Initialization - -```javascript title="RobustYellowApp.js" showLineNumbers -class RobustYellowApp { - constructor(config) { - this.client = new NitroliteClient(config); - this.reconnectAttempts = 0; - this.maxReconnectAttempts = config.reconnectConfig.maxAttempts; - this.backoffMultiplier = config.reconnectConfig.backoffMultiplier; - } - - async initializeWithRetry() { - try { - await this.client.deposit(this.config.initialDeposit); - const channel = await this.client.createChannel(this.config.channelParams); - await this.connectToClearNode(); - return channel; - } catch (error) { - return this.handleInitializationError(error); - } - } - - async handleInitializationError(error) { - switch (error.code) { - case 'INSUFFICIENT_FUNDS': - throw new UserError('Please add more funds to your wallet'); - - case 'NETWORK_ERROR': - if (this.reconnectAttempts < this.maxReconnectAttempts) { - const delay = this.config.reconnectConfig.initialDelay * - Math.pow(this.backoffMultiplier, this.reconnectAttempts); - this.reconnectAttempts++; - await this.delay(delay); - return this.initializeWithRetry(); - } - throw new NetworkError('Unable to connect after maximum attempts'); - - case 'CONTRACT_ERROR': - throw new ContractError('Smart contract interaction failed: ' + error.message); - - default: - throw error; - } - } - - async connectToClearNode() { - return new Promise((resolve, reject) => { - const ws = new WebSocket(this.config.clearNodeUrl); - - const connectionTimeout = setTimeout(() => { - reject(new Error('ClearNode connection timeout')); - }, 10000); - - ws.onopen = () => { - clearTimeout(connectionTimeout); - this.setupHeartbeat(ws); - this.setupReconnectLogic(ws); - resolve(ws); - }; - - ws.onerror = (error) => { - clearTimeout(connectionTimeout); - reject(error); - }; - }); - } - - setupHeartbeat(ws) { - const heartbeatInterval = setInterval(() => { - if (ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() })); - } else { - clearInterval(heartbeatInterval); - } - }, 30000); - - // Clear interval when WebSocket closes - ws.addEventListener('close', () => { - clearInterval(heartbeatInterval); - }); - } - - setupReconnectLogic(ws) { - ws.addEventListener('close', async (event) => { - if (event.code !== 1000) { // Not a normal closure - console.log('Connection lost, attempting to reconnect...'); - await this.delay(5000); - await this.connectToClearNode(); - } - }); - } - - delay(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } -} -``` - -## Performance Optimization - -### Batch Operations - -```javascript title="OptimizedYellowApp.js" showLineNumbers -class OptimizedYellowApp { - async batchDepositsAndChannels(operations) { - // Prepare all transactions in parallel - const preparationPromises = operations.map(async (op) => ({ - deposit: await this.client.prepareDeposit(op.amount), - channel: await this.client.prepareCreateChannel(op.channelParams) - })); - - const prepared = await Promise.all(preparationPromises); - - // Execute in optimized batches - const batchSize = 5; // Adjust based on gas limits - const results = []; - - for (let i = 0; i < prepared.length; i += batchSize) { - const batch = prepared.slice(i, i + batchSize); - - const batchResults = await Promise.all( - batch.map(async ({ deposit, channel }) => { - const depositTx = await this.client.executeTransaction(deposit); - const channelTx = await this.client.executeTransaction(channel); - return { deposit: depositTx, channel: channelTx }; - }) - ); - - results.push(...batchResults); - } - - return results; - } - - async optimizeGasUsage() { - // Estimate gas for common operations - const gasEstimates = await Promise.all([ - this.client.estimateDeposit(1000000n), - this.client.estimateCreateChannel(this.defaultChannelParams), - this.client.estimateCloseChannel(this.defaultCloseParams) - ]); - - // Adjust gas limits based on network conditions - const gasMultiplier = await this.getNetworkCongestionMultiplier(); - - return { - deposit: gasEstimates[0] * gasMultiplier, - createChannel: gasEstimates[1] * gasMultiplier, - closeChannel: gasEstimates[2] * gasMultiplier - }; - } -} -``` - -### Memory-Efficient State Storage - -```javascript title="CompactStateStorage.js" showLineNumbers -class CompactStateStorage { - constructor() { - this.stateHashes = new Map(); // Store only hashes - this.criticalStates = new Map(); // Store full critical states - this.compressionLevel = 9; // High compression for storage - } - - storeState(channelId, state) { - const stateHash = keccak256(JSON.stringify(state)); - - // Always store hash for validation - this.stateHashes.set(`${channelId}-${state.version}`, stateHash); - - // Store full state only for checkpoints and final states - if (this.isCriticalState(state)) { - const compressed = this.compressState(state); - this.criticalStates.set(`${channelId}-${state.version}`, compressed); - } - } - - isCriticalState(state) { - return state.intent === StateIntent.INITIALIZE || - state.intent === StateIntent.FINALIZE || - state.version % 100 === 0; // Every 100th state - } - - compressState(state) { - // Implement compression logic for storage efficiency - return JSON.stringify(state); // Placeholder - } -} -``` - -## Infrastructure Setup - -### Load Balancing - -```javascript title="LoadBalancedConnection.js" showLineNumbers -class LoadBalancedConnection { - constructor(clearNodeUrls) { - this.clearNodeUrls = clearNodeUrls; - this.connectionPool = new Map(); - this.currentIndex = 0; - } - - async getConnection() { - const url = this.getNextUrl(); - - if (!this.connectionPool.has(url)) { - const ws = await this.createConnection(url); - this.connectionPool.set(url, ws); - } - - return this.connectionPool.get(url); - } - - getNextUrl() { - const url = this.clearNodeUrls[this.currentIndex]; - this.currentIndex = (this.currentIndex + 1) % this.clearNodeUrls.length; - return url; - } - - async healthCheck() { - const healthPromises = this.clearNodeUrls.map(async (url) => { - try { - const ws = await this.createConnection(url); - ws.close(); - return { url, healthy: true, latency: Date.now() }; - } catch (error) { - return { url, healthy: false, error: error.message }; - } - }); - - return Promise.all(healthPromises); - } -} -``` - -### Caching Strategy - -```javascript title="IntelligentCache.js" showLineNumbers -class IntelligentCache { - constructor() { - this.stateCache = new LRUCache(1000); // Most recent states - this.channelCache = new LRUCache(100); // Channel info - this.accountCache = new LRUCache(50); // Account data - } - - async getAccountInfo(address, maxAge = 30000) { - const cacheKey = `account:${address}`; - const cached = this.accountCache.get(cacheKey); - - if (cached && Date.now() - cached.timestamp < maxAge) { - return cached.data; - } - - const fresh = await this.client.getAccountInfo(); - this.accountCache.set(cacheKey, { - data: fresh, - timestamp: Date.now() - }); - - return fresh; - } - - invalidateChannel(channelId) { - // Remove all related cache entries - this.channelCache.delete(`channel:${channelId}`); - this.stateCache.delete(`latest:${channelId}`); - } -} -``` - -## Deployment Pipeline - -### Automated Deployment - -```yaml title="deploy.yml" showLineNumbers -# .github/workflows/deploy.yml -name: Deploy Yellow App - -on: - push: - branches: [main] - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: '18' - - - name: Install dependencies - run: npm ci - - - name: Run tests - run: npm test - - - name: Deploy contracts - run: | - npx hardhat deploy --network polygon - npx hardhat verify --network polygon - env: - PRIVATE_KEY: ${{ secrets.DEPLOYER_PRIVATE_KEY }} - POLYGONSCAN_API_KEY: ${{ secrets.POLYGONSCAN_API_KEY }} - - - name: Deploy frontend - run: | - npm run build - aws s3 sync ./build s3://${{ secrets.S3_BUCKET }} - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} -``` - -### Environment Management - -```javascript title="production.js" showLineNumbers -// config/production.js -export const productionConfig = { - contracts: { - custody: process.env.CUSTODY_CONTRACT_ADDRESS, - adjudicator: process.env.ADJUDICATOR_CONTRACT_ADDRESS, - tokenAddress: process.env.TOKEN_CONTRACT_ADDRESS - }, - network: { - chainId: parseInt(process.env.CHAIN_ID), - rpcUrl: process.env.RPC_URL, - clearNodeUrls: process.env.CLEARNODE_URLS.split(',') - }, - monitoring: { - sentryDsn: process.env.SENTRY_DSN, - logLevel: process.env.LOG_LEVEL || 'info' - } -}; -``` - -## Monitoring Setup - -### Health Checks - -```javascript title="ProductionHealthCheck.js" showLineNumbers -class ProductionHealthCheck { - constructor(client) { - this.client = client; - this.healthMetrics = { - lastSuccessfulDeposit: null, - lastSuccessfulChannel: null, - connectionUptime: 0, - errorRate: 0 - }; - } - - async runHealthCheck() { - const checks = [ - this.checkContractConnectivity(), - this.checkClearNodeConnectivity(), - this.checkAccountAccess(), - this.checkGasEstimation() - ]; - - const results = await Promise.allSettled(checks); - - return { - healthy: results.every(r => r.status === 'fulfilled'), - checks: results.map((r, i) => ({ - name: this.getCheckName(i), - status: r.status, - error: r.reason?.message - })), - timestamp: Date.now() - }; - } - - async checkContractConnectivity() { - const balance = await this.client.getTokenBalance(); - return balance !== null; - } - - async checkClearNodeConnectivity() { - return new Promise((resolve, reject) => { - const ws = new WebSocket(this.config.clearNodeUrl); - const timeout = setTimeout(() => reject(new Error('Timeout')), 5000); - - ws.onopen = () => { - clearTimeout(timeout); - ws.close(); - resolve(true); - }; - - ws.onerror = () => { - clearTimeout(timeout); - reject(new Error('Connection failed')); - }; - }); - } -} -``` - -### Logging and Alerting - -```javascript title="ProductionLogger.js" showLineNumbers -class ProductionLogger { - constructor(config) { - this.logger = winston.createLogger({ - level: config.logLevel, - format: winston.format.combine( - winston.format.timestamp(), - winston.format.errors({ stack: true }), - winston.format.json() - ), - transports: [ - new winston.transports.File({ filename: 'error.log', level: 'error' }), - new winston.transports.File({ filename: 'combined.log' }), - new winston.transports.Console({ - format: winston.format.simple() - }) - ] - }); - } - - logChannelEvent(event, channelId, data) { - this.logger.info('Channel event', { - event, - channelId, - data, - timestamp: Date.now() - }); - } - - logError(error, context) { - this.logger.error('Application error', { - error: error.message, - stack: error.stack, - context, - timestamp: Date.now() - }); - - // Send to alerting system - this.sendAlert('ERROR', error.message, context); - } - - async sendAlert(level, message, context) { - // Integration with alerting services (PagerDuty, Slack, etc.) - if (level === 'ERROR' || level === 'CRITICAL') { - await this.notifyOnCall(message, context); - } - } -} -``` - -## Scaling Strategies - -### Horizontal Scaling - -```javascript title="HorizontalScaler.js" showLineNumbers -class HorizontalScaler { - constructor() { - this.instances = new Map(); - this.loadBalancer = new LoadBalancer(); - } - - async scaleUp(requiredCapacity) { - const currentCapacity = this.getCurrentCapacity(); - - if (requiredCapacity > currentCapacity) { - const additionalInstances = Math.ceil( - (requiredCapacity - currentCapacity) / this.instanceCapacity - ); - - await this.deployInstances(additionalInstances); - } - } - - async deployInstances(count) { - const deploymentPromises = Array(count).fill().map(() => - this.deployInstance() - ); - - const newInstances = await Promise.all(deploymentPromises); - - newInstances.forEach(instance => { - this.instances.set(instance.id, instance); - this.loadBalancer.addInstance(instance); - }); - } - - async deployInstance() { - // Deploy new application instance - return { - id: generateInstanceId(), - client: new NitroliteClient(this.config), - capacity: this.instanceCapacity, - status: 'healthy' - }; - } -} -``` - -### Database Optimization - -```javascript title="OptimizedStateStorage.js" showLineNumbers -class OptimizedStateStorage { - constructor(dbConfig) { - this.db = new Database(dbConfig); - this.setupIndexes(); - } - - async setupIndexes() { - // Optimize queries for common patterns - await this.db.createIndex('states', ['channelId', 'version']); - await this.db.createIndex('states', ['channelId', 'intent']); - await this.db.createIndex('channels', ['participants']); - await this.db.createIndex('transactions', ['timestamp', 'status']); - } - - async storeStateOptimized(channelId, state) { - const compressed = await this.compressState(state); - - // Store with proper indexing - await this.db.transaction(async (tx) => { - await tx.states.insert({ - channelId, - version: state.version, - intent: state.intent, - data: compressed, - timestamp: Date.now() - }); - - // Update latest state cache - await tx.channels.update( - { id: channelId }, - { latestVersion: state.version, lastActivity: Date.now() } - ); - }); - } -} -``` - -## Security in Production - -### Key Management - -```javascript title="ProductionKeyManager.js" showLineNumbers -class ProductionKeyManager { - constructor() { - this.keyVault = new AzureKeyVault(); // or AWS KMS, HashiCorp Vault - this.localCache = new Map(); - } - - async getSigningKey(channelId) { - // Check local cache first - if (this.localCache.has(channelId)) { - return this.localCache.get(channelId); - } - - // Fetch from secure vault - const key = await this.keyVault.getSecret(`channel-${channelId}`); - - // Cache for limited time - this.localCache.set(channelId, key); - setTimeout(() => { - this.localCache.delete(channelId); - }, 300000); // 5 minutes - - return key; - } - - async rotateKeys(channelId) { - const newKey = await this.generateKey(); - await this.keyVault.setSecret(`channel-${channelId}`, newKey); - this.localCache.delete(channelId); // Clear cache - return newKey; - } -} -``` - -### Rate Limiting - -```javascript title="RateLimiter.js" showLineNumbers -class RateLimiter { - constructor() { - this.limits = new Map(); - this.windowSize = 60000; // 1 minute windows - } - - async checkLimit(identifier, limit) { - const now = Date.now(); - const windowStart = now - this.windowSize; - - if (!this.limits.has(identifier)) { - this.limits.set(identifier, []); - } - - const requests = this.limits.get(identifier); - - // Remove old requests - const recentRequests = requests.filter(time => time > windowStart); - this.limits.set(identifier, recentRequests); - - if (recentRequests.length >= limit) { - throw new Error('Rate limit exceeded'); - } - - // Add current request - recentRequests.push(now); - return true; - } -} -``` - -## Deployment Checklist - -### Pre-Production - -- [ ] **Contracts Audited**: Security audit completed by reputable firm -- [ ] **Gas Optimization**: All transactions optimized for cost -- [ ] **Error Handling**: Comprehensive error recovery implemented -- [ ] **State Validation**: All state transitions validated -- [ ] **Key Management**: Secure key storage and rotation -- [ ] **Monitoring**: Health checks and alerting configured -- [ ] **Testing**: Full integration test suite passing -- [ ] **Load Testing**: Performance validated under expected load -- [ ] **Documentation**: User guides and API docs complete - -### Production - -- [ ] **Mainnet Contracts**: Deployed and verified on block explorers -- [ ] **ClearNode Connection**: Production endpoints configured -- [ ] **Backup Systems**: Redundancy and failover implemented -- [ ] **User Support**: Documentation and support channels ready -- [ ] **Incident Response**: Monitoring and response procedures documented -- [ ] **Upgrade Path**: Contract upgrade strategy defined -- [ ] **Compliance**: Regulatory requirements addressed -- [ ] **Insurance**: Consider smart contract insurance -- [ ] **Legal Review**: Terms of service and liability reviewed - -### Post-Deployment - -- [ ] **Monitoring Active**: All systems being monitored 24/7 -- [ ] **Alerts Configured**: Team notified of critical issues -- [ ] **Backup Verified**: Disaster recovery tested -- [ ] **Performance Baseline**: Metrics baseline established -- [ ] **User Feedback**: Feedback collection system active -- [ ] **Update Process**: Smooth update and rollback procedures -- [ ] **Security Monitoring**: Anomaly detection active -- [ ] **Capacity Planning**: Growth projections and scaling plans - -## Disaster Recovery - -### Backup Strategies - -```javascript title="DisasterRecovery.js" showLineNumbers -class DisasterRecovery { - constructor() { - this.backupSchedule = new CronJob('0 */6 * * *', this.performBackup.bind(this)); - this.recoveryPlan = new RecoveryPlan(); - } - - async performBackup() { - // Backup critical state data - const criticalStates = await this.getCriticalStates(); - await this.uploadToSecureStorage(criticalStates); - - // Backup channel configurations - const channelConfigs = await this.getChannelConfigurations(); - await this.uploadToSecureStorage(channelConfigs); - - // Verify backup integrity - await this.verifyBackupIntegrity(); - } - - async emergencyRecovery(backupTimestamp) { - // 1. Stop all active operations - await this.gracefulShutdown(); - - // 2. Restore from backup - const backup = await this.downloadBackup(backupTimestamp); - await this.restoreState(backup); - - // 3. Validate restored state - await this.validateRestoredState(); - - // 4. Resume operations - await this.resumeOperations(); - } -} -``` - -Following these production deployment practices ensures your Yellow App can handle real-world usage with confidence, security, and reliability. \ No newline at end of file diff --git a/docs/learn/advanced/index.md b/docs/learn/advanced/index.md deleted file mode 100644 index f32bcaa..0000000 --- a/docs/learn/advanced/index.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -sidebar_position: 1 -title: Advanced Guide -description: Advanced topics for experienced developers -displayed_sidebar: learnSidebar ---- - -# Advanced Guide - -This section covers advanced topics for working with the Yellow SDK. These are intended for developers who need deeper control over the state channel operations or want to integrate with specialized systems. - -**Consider using the advanced features when:** - -- Building custom workflows that require more control than the high-level NitroliteClient API provides -- Integrating with smart contract wallets or other account abstraction systems -- Implementing specialized monitoring or management systems for state channels -- Developing cross-chain applications that require custom handling of state channel operations -- Optimizing gas usage through transaction batching and other techniques - -## Advanced Topics - -**[Architecture](./architecture)** - Deep dive into the Yellow Network architecture, understanding the core components, data flow, and system design principles that power decentralized clearing. - -**[Multi-Party State Channels](./multi-party)** - Learn about implementing complex multi-party state channel operations, handling multiple participants, and managing advanced channel lifecycle events. - -**[Deployment Strategies](./deployment)** - Master production deployment patterns, scaling considerations, monitoring setup, and operational best practices for Yellow Apps. - -**[Security Considerations](./security)** - Understand security best practices, threat models, secure key management, and how to build robust, attack-resistant applications. \ No newline at end of file diff --git a/docs/learn/advanced/managing-session-keys.mdx b/docs/learn/advanced/managing-session-keys.mdx new file mode 100644 index 0000000..ef0f2ad --- /dev/null +++ b/docs/learn/advanced/managing-session-keys.mdx @@ -0,0 +1,193 @@ +--- +sidebar_position: 1 +title: Managing Session Keys +description: Create, list, and revoke session keys with complete API examples +keywords: [session keys, authentication, API, create, revoke, manage] +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# Managing Session Keys + +This guide covers the operational details of creating, listing, and revoking session keys via the Clearnode API. + +:::info Prerequisites +Before diving into session key management, make sure you understand the core concepts: what session keys are, how applications and allowances work, and the expiration rules. See **[Session Keys](../core-concepts/session-keys.mdx)** for the conceptual foundation. +::: + +--- + +## How to Manage Session Keys + +### Clearnode + +#### Create and Configure + +To create a session key, use the `auth_request` method during authentication. This registers the session key with its configuration: + +**Request:** + +```json +{ + "req": [ + 1, + "auth_request", + { + "address": "0x1234567890abcdef...", + "session_key": "0x9876543210fedcba...", + "application": "Chess Game", + "allowances": [ + { + "asset": "usdc", + "amount": "100.0" + }, + { + "asset": "eth", + "amount": "0.5" + } + ], + "scope": "app.create", + "expires_at": 1762417328 + }, + 1619123456789 + ], + "sig": ["0x5432abcdef..."] +} +``` + +**Parameters:** + +- `address` (required): The wallet address that owns this session key +- `session_key` (required): The address of the session key to register +- `application` (optional): Name of the application using this session key (defaults to "clearnode" if not provided) +- `allowances` (optional): Array of asset allowances specifying spending limits +- `scope` (optional): Permission scope (e.g., "app.create", "ledger.readonly"). **Note:** This feature is not yet implemented +- `expires_at` (required): Unix timestamp (in seconds) when this session key expires + +:::note +When authenticating with an already registered session key, you must still fill in all fields in the request, at least with arbitrary values. This is required by the request itself, however, the values will be ignored as the system uses the session key configuration stored during initial registration. This behavior will be improved in future versions. +::: + +#### List Active Session Keys + +Use the `get_session_keys` method to retrieve all active (non-expired) session keys for the authenticated user: + +**Request:** + +```json +{ + "req": [1, "get_session_keys", {}, 1619123456789], + "sig": ["0x9876fedcba..."] +} +``` + +**Response:** + +```json +{ + "res": [ + 1, + "get_session_keys", + { + "session_keys": [ + { + "id": 1, + "session_key": "0xabcdef1234567890...", + "application": "Chess Game", + "allowances": [ + { + "asset": "usdc", + "allowance": "100.0", + "used": "45.0" + }, + { + "asset": "eth", + "allowance": "0.5", + "used": "0.0" + } + ], + "scope": "app.create", + "expires_at": "2024-12-31T23:59:59Z", + "created_at": "2024-01-01T00:00:00Z" + } + ] + }, + 1619123456789 + ], + "sig": ["0xabcd1234..."] +} +``` + +**Response Fields:** + +- `id`: Unique identifier for the session key record +- `session_key`: The address of the session key +- `application`: Application name this session key is authorized for +- `allowances`: Array of allowances with usage tracking: + - `asset`: Symbol of the asset (e.g., "usdc", "eth") + - `allowance`: Maximum amount the session key can spend + - `used`: Amount already spent by this session key +- `scope`: Permission scope (omitted if empty) +- `expires_at`: When this session key expires (ISO 8601 format) +- `created_at`: When the session key was created (ISO 8601 format) + +#### Revoke a Session Key + +To immediately invalidate a session key, use the `revoke_session_key` method: + +**Request:** + +```json +{ + "req": [ + 1, + "revoke_session_key", + { + "session_key": "0xabcdef1234567890..." + }, + 1619123456789 + ], + "sig": ["0x9876fedcba..."] +} +``` + +**Response:** + +```json +{ + "res": [ + 1, + "revoke_session_key", + { + "session_key": "0xabcdef1234567890..." + }, + 1619123456789 + ], + "sig": ["0xabcd1234..."] +} +``` + +**Permission Rules:** + +- A wallet can revoke any of its session keys +- A session key can revoke itself +- A session key with `application: "clearnode"` can revoke other session keys belonging to the same wallet +- A non-"clearnode" session key cannot revoke other session keys (only itself) + +**Important Notes:** + +- Revocation is **immediate and cannot be undone** +- After revocation, any operations attempted with the revoked session key will fail with a validation error +- The revoked session key will no longer appear in the `get_session_keys` response +- Revocation is useful for security purposes when a session key may have been compromised + +**Error Cases:** + +- Session key does not exist, belongs to another wallet, or is expired: `"operation denied: provided address is not an active session key of this user"` +- Non-"clearnode" session key attempting to revoke another session key: `"operation denied: insufficient permissions for the active session key"` + +### Nitrolite SDK + +The Nitrolite SDK provides a higher-level abstraction for managing session keys. For detailed information on using session keys with the Nitrolite SDK, please refer to the SDK documentation. + diff --git a/docs/learn/advanced/multi-party.md b/docs/learn/advanced/multi-party.md deleted file mode 100644 index b52f1bf..0000000 --- a/docs/learn/advanced/multi-party.md +++ /dev/null @@ -1,719 +0,0 @@ ---- -sidebar_position: 3 -title: Multi-Party Applications -description: Complex scenarios with multiple participants and application patterns -keywords: [multi-party, applications, consensus, state channels, patterns] -displayed_sidebar: learnSidebar ---- - -# Multi-Party Applications - -Build sophisticated applications with multiple participants using Yellow SDK's flexible channel architecture. - -## Three-Party Applications - -### Escrow Pattern - -Perfect for marketplace transactions with buyer, seller, and mediator: - -### Escrow Application Session - -```javascript title="createEscrowSession.js" showLineNumbers -import { createAppSessionMessage } from '@erc7824/nitrolite'; - -async function createEscrowSession(buyer, seller, mediator, amount) { - const appDefinition = { - protocol: 'escrow-v1', - participants: [buyer, seller, mediator], - weights: [33, 33, 34], // Equal voting with mediator tiebreaker - quorum: 67, // Requires 2 of 3 consensus - challenge: 0, - nonce: Date.now() - }; - - const allocations = [ - { participant: buyer, asset: 'usdc', amount: amount.toString() }, - { participant: seller, asset: 'usdc', amount: '0' }, - { participant: mediator, asset: 'usdc', amount: '0' } - ]; - - return createAppSessionMessage(messageSigner, [{ - definition: appDefinition, - allocations - }]); -} -``` - -## Tournament Structure - -Multi-player competitive applications with prize distribution: - -```javascript title="createTournament.js" showLineNumbers -async function createTournament(players, entryFee, messageSigner) { - // Create application session for tournament logic - const appDefinition = { - protocol: 'tournament-v1', - participants: [...players, houseAddress], - weights: [...players.map(() => 0), 100], // House controls tournament - quorum: 100, - challenge: 0, - nonce: Date.now() - }; - - const allocations = players.map(player => ({ - participant: player, - asset: 'usdc', - amount: entryFee.toString() - })).concat([{ - participant: houseAddress, - asset: 'usdc', - amount: '0' - }]); - - const tournamentMessage = await createAppSessionMessage(messageSigner, [{ - definition: appDefinition, - allocations - }]); - - // Send to ClearNode - ws.send(tournamentMessage); - console.log('🎮 Tournament session created!'); - - return appDefinition; -} -``` - -## Consensus Mechanisms - -### Weighted Voting - -Different participants can have different voting power: - -```javascript title="governanceSession.js" showLineNumbers -const governanceSession = { - protocol: 'governance-v1', - participants: [admin, moderator1, moderator2, user1, user2], - weights: [40, 20, 20, 10, 10], // Admin has 40% vote - quorum: 60, // Requires 60% consensus - challenge: 0, - nonce: Date.now() -}; -``` - -### Multi-Signature Requirements - -```javascript title="multiSigSession.js" showLineNumbers -const multiSigSession = { - protocol: 'multisig-v1', - participants: [signer1, signer2, signer3, beneficiary], - weights: [33, 33, 34, 0], // 3 signers, 1 beneficiary - quorum: 67, // Requires 2 of 3 signatures - challenge: 0, - nonce: Date.now() -}; -``` - -## Application Patterns - -### Payment Routing - -Route payments through multiple sessions for optimal liquidity: - -```javascript title="PaymentRouter.js" showLineNumbers -class PaymentRouter { - constructor() { - this.sessions = new Map(); // route -> sessionId - this.liquidity = new Map(); // sessionId -> available amounts - this.ws = null; - this.messageSigner = null; - } - - async routePayment(amount, recipient) { - // Find optimal path considering liquidity and fees - const path = await this.findOptimalPath(this.userAddress, recipient, amount); - - if (path.length === 1) { - // Direct session exists - return this.sendDirectPayment(path[0], amount, recipient); - } else { - // Multi-hop payment required - return this.executeMultiHopPayment(path, amount, recipient); - } - } - - async sendDirectPayment(sessionId, amount, recipient) { - const paymentMessage = { - sessionId, - type: 'payment', - amount: amount.toString(), - recipient, - timestamp: Date.now() - }; - - const signature = await this.messageSigner(JSON.stringify(paymentMessage)); - - this.ws.send(JSON.stringify({ - ...paymentMessage, - signature - })); - - return paymentMessage; - } -} -``` - -### State Synchronization - -Keep multiple channels synchronized for complex applications: - -```javascript title="ChannelSynchronizer.js" showLineNumbers -class ChannelSynchronizer { - constructor() { - this.channels = new Map(); - this.syncGroups = new Map(); - } - - addToSyncGroup(groupId, channelId) { - if (!this.syncGroups.has(groupId)) { - this.syncGroups.set(groupId, new Set()); - } - this.syncGroups.get(groupId).add(channelId); - } - - async syncStateUpdate(groupId, stateUpdate) { - const channels = this.syncGroups.get(groupId); - - // Apply update to all channels in sync group - const updatePromises = Array.from(channels).map(channelId => - this.applyStateUpdate(channelId, stateUpdate) - ); - - const results = await Promise.allSettled(updatePromises); - - // Handle any failures - const failures = results.filter(r => r.status === 'rejected'); - if (failures.length > 0) { - await this.handleSyncFailures(groupId, failures); - } - - return results; - } -} -``` - -## ClearNode Integration - -### Multi-Session Management - -```javascript title="MultiSessionManager.js" showLineNumbers -class MultiSessionManager { - constructor() { - this.connections = new Map(); // sessionId -> WebSocket - this.sessions = new Map(); // sessionId -> sessionData - this.messageSigner = null; - } - - async connectToSession(sessionConfig) { - const ws = new WebSocket('wss://clearnet.yellow.com/ws'); - - return new Promise((resolve, reject) => { - ws.onopen = async () => { - // Create application session - const sessionMessage = await createAppSessionMessage( - this.messageSigner, - [sessionConfig] - ); - - ws.send(sessionMessage); - - const sessionId = this.generateSessionId(); - this.connections.set(sessionId, ws); - this.sessions.set(sessionId, sessionConfig); - - resolve({ ws, sessionId }); - }; - - ws.onerror = reject; - - ws.onmessage = (event) => { - this.handleSessionMessage(sessionId, parseRPCResponse(event.data)); - }; - }); - } - - async broadcastToAllSessions(message) { - const broadcasts = Array.from(this.connections.entries()).map(([sessionId, ws]) => { - if (ws.readyState === WebSocket.OPEN) { - const signature = await this.messageSigner(JSON.stringify(message)); - return ws.send(JSON.stringify({ ...message, signature })); - } - }); - - return Promise.allSettled(broadcasts); - } - - handleSessionMessage(sessionId, message) { - // Route messages based on session and message type - switch (message.type) { - case 'session_created': - this.handleSessionCreated(sessionId, message.data); - break; - case 'participant_message': - this.handleParticipantMessage(sessionId, message.data); - break; - case 'error': - this.handleSessionError(sessionId, message.error); - break; - } - } -} -``` - -### Session Management - -```javascript title="SessionManager.js" showLineNumbers -class SessionManager { - constructor() { - this.activeSessions = new Map(); - this.sessionParticipants = new Map(); - } - - async createMultiPartySession(participants, sessionConfig) { - const appDefinition = { - protocol: sessionConfig.protocol, - participants, - weights: sessionConfig.weights || participants.map(() => 100 / participants.length), - quorum: sessionConfig.quorum || 51, - challenge: 0, - nonce: Date.now() - }; - - const allocations = participants.map((participant, index) => ({ - participant, - asset: sessionConfig.asset || 'usdc', - amount: sessionConfig.initialAmounts?.[index]?.toString() || '0' - })); - - const sessionMessage = await createAppSessionMessage( - this.messageSigner, - [{ definition: appDefinition, allocations }] - ); - - // Send to all participants' connections - await this.broadcastSessionCreation(participants, sessionMessage); - - return this.waitForSessionConfirmation(); - } - - async coordinateStateUpdate(sessionId, updateData) { - const session = this.activeSessions.get(sessionId); - const participants = this.sessionParticipants.get(sessionId); - - // Create coordinated update message - const updateMessage = { - type: 'coordinate_update', - sessionId, - data: updateData, - requiredSignatures: this.calculateRequiredSignatures(session), - timestamp: Date.now() - }; - - // Send to all participants - await this.broadcastToParticipants(participants, updateMessage); - - // Wait for consensus - return this.waitForConsensus(sessionId, updateMessage.timestamp); - } -} -``` - -## Advanced Patterns - -### Channel Hierarchies - -Create parent-child relationships between channels: - -```javascript title="HierarchicalChannels.js" showLineNumbers -class HierarchicalChannels { - constructor(client) { - this.client = client; - this.parentChannels = new Map(); - this.childChannels = new Map(); - } - - async createParentChannel(participants, totalCapacity) { - const parentChannel = await this.client.createChannel({ - participants: [...participants, this.coordinatorAddress], - initialAllocationAmounts: [...totalCapacity, 0n], - stateData: '0x' - }); - - this.parentChannels.set(parentChannel.channelId, { - participants, - totalCapacity, - childChannels: new Set() - }); - - return parentChannel; - } - - async createChildChannel(parentChannelId, subset, allocation) { - const parent = this.parentChannels.get(parentChannelId); - - // Validate subset is from parent participants - if (!subset.every(addr => parent.participants.includes(addr))) { - throw new Error('Child participants must be subset of parent'); - } - - const childChannel = await this.client.createChannel({ - participants: subset, - initialAllocationAmounts: allocation, - stateData: this.encodeParentReference(parentChannelId) - }); - - // Link to parent - parent.childChannels.add(childChannel.channelId); - this.childChannels.set(childChannel.channelId, parentChannelId); - - return childChannel; - } -} -``` - -### Cross-Channel Coordination - -Coordinate state updates across multiple related channels: - -```javascript title="CrossChannelCoordinator.js" showLineNumbers -class CrossChannelCoordinator { - constructor() { - this.channelGroups = new Map(); - this.pendingUpdates = new Map(); - } - - async coordinateAcrossChannels(groupId, operation) { - const channels = this.channelGroups.get(groupId); - - // Prepare updates for all channels - const updatePromises = channels.map(channelId => - this.prepareChannelUpdate(channelId, operation) - ); - - const preparedUpdates = await Promise.all(updatePromises); - - // Execute all updates atomically - try { - const results = await this.executeAtomicUpdates(preparedUpdates); - return results; - } catch (error) { - // Rollback all updates on failure - await this.rollbackUpdates(preparedUpdates); - throw error; - } - } - - async executeAtomicUpdates(updates) { - // Use two-phase commit protocol - - // Phase 1: Prepare all updates - const preparePromises = updates.map(update => - this.prepareUpdate(update) - ); - - const prepared = await Promise.all(preparePromises); - - // Phase 2: Commit all updates - const commitPromises = prepared.map(prep => - this.commitUpdate(prep) - ); - - return Promise.all(commitPromises); - } -} -``` - -## Real-World Applications - -### Gaming Lobby System - -```javascript title="GamingLobby.js" showLineNumbers -class GamingLobby { - constructor() { - this.gameRooms = new Map(); - this.playerQueues = new Map(); - this.ws = null; - this.messageSigner = null; - } - - async createGameRoom(gameType, maxPlayers, buyIn) { - const roomId = this.generateRoomId(); - - this.gameRooms.set(roomId, { - gameType, - maxPlayers, - buyIn, - players: [], - status: 'WAITING' - }); - - // Broadcast room creation - const roomMessage = { - type: 'room_created', - roomId, - gameType, - maxPlayers, - buyIn, - timestamp: Date.now() - }; - - const signature = await this.messageSigner(JSON.stringify(roomMessage)); - this.ws.send(JSON.stringify({ ...roomMessage, signature })); - - return roomId; - } - - async joinGameRoom(roomId, playerAddress) { - const room = this.gameRooms.get(roomId); - - if (room.players.length >= room.maxPlayers) { - throw new Error('Room is full'); - } - - room.players.push(playerAddress); - - // Start game when room is full - if (room.players.length === room.maxPlayers) { - await this.startGame(roomId); - } - - return room; - } - - async startGame(roomId) { - const room = this.gameRooms.get(roomId); - - // Create game application session - const appDefinition = { - protocol: `${room.gameType}-v1`, - participants: [...room.players, this.serverAddress], - weights: [...room.players.map(() => 0), 100], // Server controls game - quorum: 100, - challenge: 0, - nonce: Date.now() - }; - - const allocations = room.players.map(player => ({ - participant: player, - asset: 'usdc', - amount: room.buyIn.toString() - })).concat([{ - participant: this.serverAddress, - asset: 'usdc', - amount: '0' - }]); - - const gameSession = await createAppSessionMessage(this.messageSigner, [{ - definition: appDefinition, - allocations - }]); - - this.ws.send(gameSession); - - room.status = 'ACTIVE'; - room.sessionId = this.generateSessionId(); - - return room; - } -} -``` - -### Subscription Service - -```javascript title="SubscriptionService.js" showLineNumbers -class SubscriptionService { - constructor() { - this.subscriptions = new Map(); - this.ws = null; - this.messageSigner = null; - } - - async createSubscription(subscriber, provider, monthlyFee) { - const appDefinition = { - protocol: 'subscription-v1', - participants: [subscriber, provider, this.serviceAddress], - weights: [0, 100, 0], // Provider controls service delivery - quorum: 100, - challenge: 0, - nonce: Date.now() - }; - - const allocations = [ - { participant: subscriber, asset: 'usdc', amount: (monthlyFee * 12).toString() }, - { participant: provider, asset: 'usdc', amount: '0' }, - { participant: this.serviceAddress, asset: 'usdc', amount: '0' } - ]; - - const sessionMessage = await createAppSessionMessage(this.messageSigner, [{ - definition: appDefinition, - allocations - }]); - - // Send to ClearNode - this.ws.send(sessionMessage); - - const subscriptionId = this.generateSubscriptionId(); - this.subscriptions.set(subscriptionId, { - subscriber, - provider, - monthlyFee, - createdAt: Date.now() - }); - - return subscriptionId; - } - - async processMonthlyPayment(subscriptionId) { - const subscription = this.subscriptions.get(subscriptionId); - - // Create payment message for this month - const paymentMessage = { - type: 'monthly_payment', - subscriptionId, - amount: subscription.monthlyFee, - month: this.getCurrentMonth(), - timestamp: Date.now() - }; - - const signature = await this.messageSigner(JSON.stringify(paymentMessage)); - - this.ws.send(JSON.stringify({ - ...paymentMessage, - signature - })); - - return this.waitForPaymentConfirmation(subscriptionId); - } -} -``` - -## Best Practices - -### Participant Management - -```javascript title="ParticipantManager.js" showLineNumbers -class ParticipantManager { - constructor() { - this.participants = new Map(); - this.roles = new Map(); - } - - addParticipant(address, role, permissions) { - this.participants.set(address, { - role, - permissions: new Set(permissions), - joinedAt: Date.now(), - status: 'ACTIVE' - }); - } - - validateParticipantAction(address, action) { - const participant = this.participants.get(address); - if (!participant) { - throw new Error('Unknown participant'); - } - - if (!participant.permissions.has(action)) { - throw new Error(`Insufficient permissions for action: ${action}`); - } - - return true; - } - - async rotateParticipant(oldAddress, newAddress) { - const participant = this.participants.get(oldAddress); - - // Transfer permissions to new address - this.participants.set(newAddress, { - ...participant, - previousAddress: oldAddress, - rotatedAt: Date.now() - }); - - // Mark old address as rotated - participant.status = 'ROTATED'; - participant.rotatedTo = newAddress; - } -} -``` - -### Message Broadcasting - -```javascript title="MessageBroadcaster.js" showLineNumbers -class MessageBroadcaster { - constructor() { - this.connections = new Map(); - this.messageQueue = new Map(); - } - - async broadcastToParticipants(participants, message) { - const deliveryPromises = participants.map(async (participant) => { - const connection = this.connections.get(participant); - - if (!connection || connection.readyState !== WebSocket.OPEN) { - // Queue message for later delivery - this.queueMessage(participant, message); - return { participant, status: 'QUEUED' }; - } - - try { - connection.send(JSON.stringify(message)); - return { participant, status: 'DELIVERED' }; - } catch (error) { - this.queueMessage(participant, message); - return { participant, status: 'FAILED', error: error.message }; - } - }); - - return Promise.all(deliveryPromises); - } - - queueMessage(participant, message) { - if (!this.messageQueue.has(participant)) { - this.messageQueue.set(participant, []); - } - - this.messageQueue.get(participant).push({ - message, - timestamp: Date.now(), - retries: 0 - }); - } - - async deliverQueuedMessages(participant) { - const queue = this.messageQueue.get(participant); - if (!queue || queue.length === 0) return; - - const connection = this.connections.get(participant); - if (!connection || connection.readyState !== WebSocket.OPEN) return; - - // Deliver all queued messages - for (const queued of queue) { - try { - connection.send(JSON.stringify(queued.message)); - } catch (error) { - queued.retries++; - if (queued.retries < 3) { - continue; // Keep in queue for retry - } - } - } - - // Clear delivered messages - this.messageQueue.set(participant, - queue.filter(msg => msg.retries >= 3) - ); - } -} -``` - -Multi-party applications enable sophisticated logic while maintaining the performance benefits of state channels. Focus on application logic and participant coordination rather than low-level protocol details. \ No newline at end of file diff --git a/docs/learn/advanced/security.md b/docs/learn/advanced/security.md deleted file mode 100644 index 5752325..0000000 --- a/docs/learn/advanced/security.md +++ /dev/null @@ -1,408 +0,0 @@ ---- -sidebar_position: 6 -title: Security Best Practices -description: Secure your Yellow Apps with proper authentication, key management, and monitoring -keywords: [security, authentication, key management, best practices, yellow apps] ---- - -# Security Best Practices - -Secure your Yellow Apps with proper authentication, robust key management, and proactive monitoring. - -## Authentication & Authorization - -### Wallet Integration Security - -```javascript title="WalletSecurity.js" showLineNumbers -class WalletSecurity { - constructor() { - this.authorizedSessions = new Map(); - this.sessionTimeout = 30 * 60 * 1000; // 30 minutes - } - - async authenticateUser(walletAddress) { - // Verify wallet connection - if (!window.ethereum) { - throw new Error('No wallet provider found'); - } - - // Request account access - const accounts = await window.ethereum.request({ - method: 'eth_requestAccounts' - }); - - if (!accounts.includes(walletAddress)) { - throw new SecurityError('Wallet not authorized'); - } - - // Create session - const sessionId = this.generateSessionId(); - this.authorizedSessions.set(sessionId, { - address: walletAddress, - createdAt: Date.now(), - lastActivity: Date.now() - }); - - // Auto-expire session - setTimeout(() => { - this.authorizedSessions.delete(sessionId); - }, this.sessionTimeout); - - return sessionId; - } - - validateSession(sessionId) { - const session = this.authorizedSessions.get(sessionId); - if (!session) { - throw new SecurityError('Invalid or expired session'); - } - - // Update last activity - session.lastActivity = Date.now(); - return session; - } -} -``` - -### Environment Configuration - -```javascript title="SecureConfig.js" showLineNumbers -class SecureConfig { - constructor() { - this.requiredVars = [ - 'CLEARNODE_ENDPOINT', - 'CUSTODY_ADDRESS', - 'ADJUDICATOR_ADDRESS' - ]; - } - - validateEnvironment() { - // Check required environment variables - for (const varName of this.requiredVars) { - if (!process.env[varName]) { - throw new ConfigError(`Missing required environment variable: ${varName}`); - } - } - - // Validate addresses - if (!this.isValidAddress(process.env.CUSTODY_ADDRESS)) { - throw new ConfigError('Invalid custody contract address'); - } - - return { - clearNodeEndpoint: process.env.CLEARNODE_ENDPOINT, - custodyAddress: process.env.CUSTODY_ADDRESS, - adjudicatorAddress: process.env.ADJUDICATOR_ADDRESS, - environment: process.env.NODE_ENV || 'development' - }; - } - - isValidAddress(address) { - return /^0x[a-fA-F0-9]{40}$/.test(address); - } -} -``` - -## Input Validation & Sanitization - -### User Input Security - -```javascript title="InputValidator.js" showLineNumbers -class InputValidator { - validateChannelParams(params) { - return { - participants: this.validateAddresses(params.participants), - amount: this.validateAmount(params.amount), - sessionData: this.sanitizeSessionData(params.sessionData) - }; - } - - validateAddresses(addresses) { - if (!Array.isArray(addresses) || addresses.length < 2) { - throw new ValidationError('Invalid participants array'); - } - - return addresses.map(addr => { - if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) { - throw new ValidationError(`Invalid address: ${addr}`); - } - return addr.toLowerCase(); - }); - } - - validateAmount(amount) { - const bigIntValue = BigInt(amount); - - if (bigIntValue <= 0n) { - throw new ValidationError('Amount must be positive'); - } - - if (bigIntValue > BigInt('1000000000000000000000')) { // 1000 tokens max - throw new ValidationError('Amount exceeds maximum limit'); - } - - return bigIntValue; - } - - sanitizeSessionData(data) { - if (!data) return ''; - - // Remove potentially malicious content - const sanitized = data.replace(/)<[^<]*)*<\/script>/gi, ''); - - // Validate size limits - if (sanitized.length > 64 * 1024) { // 64KB limit - throw new ValidationError('Session data too large'); - } - - return sanitized; - } -} -``` - -## Error Handling - -### Secure Error Responses - -```javascript title="SecureErrorHandler.js" showLineNumbers -class SecureErrorHandler { - constructor() { - this.errorLogs = new Map(); - } - - handleError(error, context) { - // Log detailed error internally - this.logError(error, context); - - // Return sanitized error to user - return this.sanitizeError(error); - } - - sanitizeError(error) { - // Remove sensitive information from error messages - const safeErrors = { - 'ValidationError': 'Invalid input parameters', - 'SecurityError': 'Authentication failed', - 'NetworkError': 'Connection issue', - 'TimeoutError': 'Request timed out' - }; - - return { - message: safeErrors[error.constructor.name] || 'An error occurred', - code: this.getErrorCode(error), - timestamp: Date.now() - }; - } - - logError(error, context) { - const errorEntry = { - message: error.message, - stack: error.stack, - context, - timestamp: Date.now(), - severity: this.getErrorSeverity(error) - }; - - // Store securely (never expose to client) - this.errorLogs.set(this.generateErrorId(), errorEntry); - } -} -``` - -## Rate Limiting & Protection - -### DDoS Protection - -```javascript title="RateLimiter.js" showLineNumbers -class RateLimiter { - constructor() { - this.requestCounts = new Map(); - this.limits = { - perMinute: 100, - perHour: 1000, - perDay: 10000 - }; - } - - async checkRateLimit(userAddress, action) { - const key = `${userAddress}:${action}`; - const now = Date.now(); - - // Clean old entries - this.cleanupOldEntries(now); - - const userRequests = this.requestCounts.get(key) || []; - - // Check minute limit - const minuteCount = userRequests.filter(t => now - t < 60000).length; - if (minuteCount >= this.limits.perMinute) { - throw new SecurityError('Rate limit exceeded'); - } - - // Record request - userRequests.push(now); - this.requestCounts.set(key, userRequests); - - return true; - } - - cleanupOldEntries(now) { - const dayAgo = now - 24 * 60 * 60 * 1000; - - for (const [key, requests] of this.requestCounts) { - const filtered = requests.filter(t => t > dayAgo); - if (filtered.length === 0) { - this.requestCounts.delete(key); - } else { - this.requestCounts.set(key, filtered); - } - } - } -} -``` - -## Monitoring & Logging - -### Application Monitoring - -```javascript title="SecurityMonitor.js" showLineNumbers -class SecurityMonitor { - constructor() { - this.metrics = new Map(); - this.alerts = new Set(); - } - - trackUserActivity(userAddress, action, details) { - const key = `${userAddress}:${action}`; - const activity = { - timestamp: Date.now(), - action, - details, - userAgent: details.userAgent, - ipAddress: details.ipAddress - }; - - if (!this.metrics.has(key)) { - this.metrics.set(key, []); - } - - this.metrics.get(key).push(activity); - - // Check for suspicious patterns - this.detectSuspiciousActivity(userAddress, action); - } - - detectSuspiciousActivity(userAddress, action) { - const activities = this.metrics.get(`${userAddress}:${action}`) || []; - const recentActivities = activities.filter(a => - Date.now() - a.timestamp < 5 * 60 * 1000 // Last 5 minutes - ); - - // Flag rapid consecutive actions - if (recentActivities.length > 50) { - this.alerts.add({ - type: 'RAPID_ACTIVITY', - userAddress, - action, - count: recentActivities.length, - timestamp: Date.now() - }); - } - } - - getSecurityMetrics() { - return { - totalUsers: this.metrics.size, - activeAlerts: this.alerts.size, - recentActivity: this.getRecentActivityCount(), - timestamp: Date.now() - }; - } -} -``` - -## Security Testing - -### Application Security Tests - -```javascript title="security.test.js" showLineNumbers -describe('Yellow App Security', () => { - describe('Authentication', () => { - it('should reject unauthorized wallet connections', async () => { - const unauthorizedWallet = '0x1234567890123456789012345678901234567890'; - - await expect( - app.authenticateUser(unauthorizedWallet) - ).rejects.toThrow('Wallet not authorized'); - }); - - it('should expire sessions after timeout', async () => { - const session = await app.createSession(validWallet); - - // Fast-forward time - jest.advanceTimersByTime(31 * 60 * 1000); // 31 minutes - - expect(app.validateSession(session.id)).toThrow('Invalid or expired session'); - }); - }); - - describe('Input Validation', () => { - it('should reject invalid amounts', async () => { - await expect( - app.createPayment(-100n, recipient) - ).rejects.toThrow('Amount must be positive'); - }); - - it('should sanitize malicious session data', async () => { - const maliciousData = 'normal content'; - const sanitized = app.sanitizeSessionData(maliciousData); - - expect(sanitized).toBe('normal content'); - expect(sanitized).not.toContain('