diff --git a/README.md b/README.md index 007a0f9..264fc4f 100644 --- a/README.md +++ b/README.md @@ -104,44 +104,73 @@ Fork mode allows you to create a local blockchain that mirrors the exact state o ### What is Fork Mode? -Fork mode creates a local copy of a blockchain network at a specific point in time. This means you can: -- Test with real DeFi protocols (Uniswap, Aave, Compound, etc.) -- Access actual token balances and contract states -- Reproduce production bugs in a controlled environment -- Test complex scenarios without deployment costs +Fork mode creates a local copy of a blockchain network at a specific point in time. Think of it as taking a "snapshot" of mainnet (or any network) and running it locally for testing purposes. + +**Why is this powerful?** Instead of deploying your own test contracts, you can test directly against the real contracts that are already deployed on mainnet. This means you can: + +- **Test with real DeFi protocols** (Uniswap, Aave, Compound, etc.) - no need to deploy or mock these complex systems +- **Access actual token balances and contract states** - test with the exact same data your users will interact with +- **Reproduce production bugs** in a controlled environment where you can debug safely +- **Test complex scenarios without deployment costs** - no gas fees, no waiting for transactions +- **Validate integrations** with existing protocols before going live + +**Perfect for:** Integration testing, debugging production issues, testing with real market conditions, and validating complex multi-protocol interactions. ### Quick Fork Mode Example ```typescript import { configure, createOnchainTest } from '@coinbase/onchaintestkit'; -// Fork Ethereum mainnet for testing +// Fork Ethereum mainnet for testing with real contracts and liquidity const test = createOnchainTest( configure() .withLocalNode({ - fork: 'https://eth-mainnet.g.alchemy.com/v2/your-api-key', - forkBlockNumber: 18500000, // Optional: fork from specific block + fork: 'https://eth-mainnet.g.alchemy.com/v2/your-api-key', // Your RPC endpoint + forkBlockNumber: 18500000, // Optional: fork from specific block for reproducible tests chainId: 1, + // Pre-fund test accounts with plenty of ETH for gas and testing + accounts: 10, + balance: '100000000000000000000', // 100 ETH per account }) .withMetaMask() .withNetwork({ name: 'Forked Ethereum', - rpcUrl: 'http://localhost:8545', + rpcUrl: 'http://localhost:8545', // Local node will run here chainId: 1, symbol: 'ETH', }) .build() ); -test('swap on forked Uniswap', async ({ page, metamask }) => { - // Test with real Uniswap contracts and liquidity +test('swap tokens on forked Uniswap', async ({ page, metamask }) => { + // Navigate to Uniswap - this will use the REAL Uniswap contracts! await page.goto('https://app.uniswap.org'); - // ... your test logic + + // Connect your test wallet + await page.getByRole('button', { name: 'Connect Wallet' }).click(); + await page.getByText('MetaMask').click(); + await metamask.handleAction(BaseActionType.CONNECT_TO_DAPP); + + // Now you can test swaps with real liquidity pools and contracts + // The local fork has all the same state as mainnet at block 18500000 + await page.getByRole('button', { name: 'Swap' }).click(); + // ... rest of your test logic }); ``` +**What's happening here?** +1. **Fork Creation**: We create a local blockchain that copies all data from Ethereum mainnet +2. **Real Contracts**: Your tests interact with the actual Uniswap contracts deployed on mainnet +3. **Test Accounts**: Pre-funded accounts let you test without worrying about gas or token balances +4. **Local Execution**: Everything runs locally, so it's fast and free + For detailed fork mode documentation, see [docs/node/overview.mdx](docs/node/overview.mdx) and [docs/node/configuration.mdx](docs/node/configuration.mdx). +**Learn more:** +- **[Fork Mode Overview](docs/node/overview.mdx)** - Concepts, benefits, and practical examples +- **[Node Configuration](docs/node/configuration.mdx)** - Complete setup guide and troubleshooting +- **[Fork Mode Example](example/fork-mode-example.js)** - Working code examples you can run + ## Configuration Builder The toolkit uses a fluent builder pattern for configuration: diff --git a/docs/node/configuration.mdx b/docs/node/configuration.mdx index 3edf190..c819ea2 100644 --- a/docs/node/configuration.mdx +++ b/docs/node/configuration.mdx @@ -367,6 +367,104 @@ const config = configure() .build(); ``` +#### 7. Memory and Performance Issues + +**Problem**: High memory usage or slow performance during testing + +**Solutions**: +- Limit the number of test accounts to reduce memory overhead +- Use specific block numbers instead of latest to reduce state size +- Close nodes properly after tests to free memory +- Consider using lighter RPC providers for non-critical tests + +```typescript +// Memory-optimized configuration +const config = configure() + .withLocalNode({ + fork: 'https://eth-mainnet.g.alchemy.com/v2/your-api-key', + forkBlockNumber: 18500000, // Specific block reduces memory + accounts: 3, // Fewer accounts = less memory + balance: '10000000000000000000', // 10 ETH is usually sufficient + gasPrice: '1000000000', // 1 gwei for faster execution + }) + .build(); +``` + +#### 8. Transaction Failures on Fork + +**Problem**: Transactions that work on mainnet fail on the fork + +**Solutions**: +- Check that the fork block has the necessary contract state +- Verify account balances are sufficient for the transaction +- Ensure gas limits are appropriate for the forked network +- Check that the contract exists at the expected address on the fork block + +```typescript +// Debug transaction failures +const config = configure() + .withLocalNode({ + fork: 'https://eth-mainnet.g.alchemy.com/v2/your-api-key', + forkBlockNumber: 18500000, + accounts: 10, + balance: '1000000000000000000000', // Large balance for debugging + gasPrice: '20000000000', // Higher gas price + gasLimit: '30000000', // Higher gas limit + }) + .build(); +``` + +#### 9. RPC Rate Limiting + +**Problem**: `Error: Too many requests` or connection timeouts + +**Solutions**: +- Upgrade to a paid RPC provider plan +- Use multiple RPC endpoints and rotate between them +- Add delays between requests during setup +- Cache fork state when possible + +```typescript +// Rate limit friendly configuration +const config = configure() + .withLocalNode({ + fork: 'https://eth-mainnet.g.alchemy.com/v2/your-api-key', + forkBlockNumber: 18500000, // Specific block reduces requests + accounts: 5, // Fewer accounts = fewer setup requests + }) + .build(); +``` + +#### 10. Network-Specific Issues + +**Problem**: Certain features don't work as expected on different networks + +**Solutions**: +- Verify the network supports the features you're testing +- Check block explorer for contract deployment blocks +- Use network-appropriate gas prices and limits +- Verify the RPC endpoint supports the specific network features + +```typescript +// Network-specific optimizations +const polygonConfig = configure() + .withLocalNode({ + fork: 'https://polygon-mainnet.g.alchemy.com/v2/your-api-key', + chainId: 137, + gasPrice: '30000000000', // Higher gas price for Polygon + forkBlockNumber: 50000000, // Recent Polygon block + }) + .build(); + +const arbitrumConfig = configure() + .withLocalNode({ + fork: 'https://arb-mainnet.g.alchemy.com/v2/your-api-key', + chainId: 42161, + gasPrice: '100000000', // Lower gas price for Arbitrum + }) + .build(); +``` + ### Performance Optimization #### 1. Choose the Right Block Number @@ -404,4 +502,57 @@ const config = configure() 4. **Cache fork state** when possible to speed up subsequent runs 5. **Monitor RPC usage** to avoid hitting rate limits 6. **Clean up nodes** after tests to free resources -7. **Use appropriate gas settings** for your test scenarios \ No newline at end of file +7. **Use appropriate gas settings** for your test scenarios + +### Debugging Tips + +When fork mode isn't working as expected, here are some debugging strategies: + +#### Enable Verbose Logging + +```typescript +// Add logging to understand what's happening +const config = configure() + .withLocalNode({ + fork: 'https://eth-mainnet.g.alchemy.com/v2/your-api-key', + forkBlockNumber: 18500000, + // Add these for debugging + verbose: true, // Enable detailed logging + debug: true, // Show debug information + }) + .build(); +``` + +#### Test Fork Connection First + +```typescript +// Simple test to verify fork is working +test('verify fork connection', async ({ node }) => { + // Check that we can query basic blockchain data + const blockNumber = await node.getBlockNumber(); + expect(blockNumber).toBeGreaterThan(18500000); + + // Check that we have test accounts with balance + const balance = await node.getBalance('0x...'); // Your test account address + expect(balance).toBeGreaterThan(0); + + // Verify we can interact with a known contract + const uniswapRouter = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; + const code = await node.getCode(uniswapRouter); + expect(code).not.toBe('0x'); // Contract should exist on fork +}); +``` + +#### Compare Fork vs Mainnet State + +```typescript +// Debug by comparing fork state to live mainnet +test('compare fork to mainnet', async ({ node }) => { + // Query the same data from both fork and mainnet + const forkBalance = await node.getBalance('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'); // Vitalik's address + console.log('Fork balance:', forkBalance); + + // This should match mainnet at the fork block number + // Use a mainnet RPC to verify the expected balance +}); +``` \ No newline at end of file diff --git a/docs/node/overview.mdx b/docs/node/overview.mdx index 0789d05..1dffc09 100644 --- a/docs/node/overview.mdx +++ b/docs/node/overview.mdx @@ -67,16 +67,19 @@ const test = createOnchainTest( ### Testing DeFi Interactions +This example shows how to test lending protocol interactions using real Aave contracts and liquidity: + ```typescript // Test lending protocol interactions with real liquidity const test = createOnchainTest( configure() .withLocalNode({ fork: 'https://eth-mainnet.g.alchemy.com/v2/your-api-key', - forkBlockNumber: 18500000, + forkBlockNumber: 18500000, // Use a known good block with stable state chainId: 1, - // Pre-fund test accounts with ETH - balance: '100000000000000000000', // 100 ETH + // Pre-fund test accounts with ETH for gas and testing + accounts: 5, + balance: '100000000000000000000', // 100 ETH per account }) .withMetaMask() .withNetwork({ @@ -88,30 +91,50 @@ const test = createOnchainTest( .build() ); -test('deposit ETH to Aave', async ({ page, metamask }) => { - // Navigate to Aave interface +test('deposit ETH to Aave lending pool', async ({ page, metamask }) => { + // Navigate to Aave interface - using real Aave protocol await page.goto('https://app.aave.com'); - // Connect wallet + // Connect wallet to the DApp await page.getByRole('button', { name: 'Connect Wallet' }).click(); await page.getByText('MetaMask').click(); await metamask.handleAction(BaseActionType.CONNECT_TO_DAPP); - // Perform deposit with real Aave contracts + // Perform deposit with real Aave contracts and current interest rates await page.getByRole('button', { name: 'Supply' }).click(); - // ... rest of test logic + await page.getByTestId('asset-ETH').click(); + + // Enter amount and confirm transaction + await page.getByPlaceholder('0.00').fill('1.0'); + await page.getByRole('button', { name: 'Continue' }).click(); + + // Handle the transaction approval in MetaMask + await metamask.handleAction(BaseActionType.APPROVE_TRANSACTION); + + // Verify the deposit was successful by checking aToken balance + await expect(page.getByTestId('supplied-balance')).toContainText('1.00 ETH'); }); ``` +**Why this works better than mocks:** +- **Real interest rates**: Test with actual APY calculations +- **Real contract interactions**: Catch integration issues that mocks might miss +- **Real token amounts**: Test with actual liquidity and slippage +- **Real gas costs**: Understand actual transaction costs (without paying them) + ### Testing NFT Marketplace +Here's how to test NFT marketplace interactions using real collections and marketplace contracts: + ```typescript -// Fork to test NFT interactions with real collections +// Fork to test NFT interactions with real collections and marketplace data const test = createOnchainTest( configure() .withLocalNode({ fork: 'https://eth-mainnet.g.alchemy.com/v2/your-api-key', chainId: 1, + // Ensure sufficient balance for NFT purchases + balance: '50000000000000000000', // 50 ETH per account }) .withMetaMask() .build() @@ -119,11 +142,39 @@ const test = createOnchainTest( test('bid on NFT auction', async ({ page, metamask }) => { // Test with real NFT collections and marketplace contracts - await page.goto('https://opensea.io/collection/your-collection'); - // ... test NFT bidding logic + await page.goto('https://opensea.io/collection/bored-ape-yacht-club'); + + // Connect wallet to OpenSea + await page.getByRole('button', { name: 'Connect wallet' }).click(); + await page.getByText('MetaMask').click(); + await metamask.handleAction(BaseActionType.CONNECT_TO_DAPP); + + // Select an NFT that's available for bidding + await page.getByTestId('asset-card').first().click(); + + // Make an offer using real marketplace contracts + await page.getByRole('button', { name: 'Make offer' }).click(); + await page.getByPlaceholder('Amount').fill('1.5'); + + // Approve WETH spending (real WETH contract interaction) + await page.getByRole('button', { name: 'Continue' }).click(); + await metamask.handleAction(BaseActionType.APPROVE_TOKEN); + + // Submit the bid transaction + await page.getByRole('button', { name: 'Make offer' }).click(); + await metamask.handleAction(BaseActionType.APPROVE_TRANSACTION); + + // Verify the bid was placed successfully + await expect(page.getByText('Offer submitted')).toBeVisible(); }); ``` +**Real marketplace benefits:** +- **Actual NFT metadata**: Test with real images, traits, and collection data +- **Real pricing**: Test with current floor prices and market dynamics +- **Real contract complexity**: Catch edge cases in marketplace logic +- **Real royalty handling**: Test creator royalties and marketplace fees + ## Key Benefits 1. **Realistic Testing Environment**: Your tests run against the exact same contracts and data as production @@ -141,4 +192,15 @@ Fork mode is ideal for: - Debugging production issues - End-to-end testing of complete user workflows -For simpler unit tests or when you need a clean state, consider using a fresh local node without forking. \ No newline at end of file +For simpler unit tests or when you need a clean state, consider using a fresh local node without forking. + +## Next Steps + +Ready to start using fork mode in your tests? Here's what to do next: + +1. **Setup**: Follow the [Node Configuration guide](./configuration.mdx) for detailed setup instructions +2. **Troubleshooting**: If you run into issues, check the comprehensive [troubleshooting section](./configuration.mdx#troubleshooting) +3. **Examples**: Try the [fork mode example](https://github.com/coinbase/onchaintestkit/blob/main/example/fork-mode-example.js) to see working code +4. **Advanced usage**: Learn about [multi-chain testing and time-sensitive scenarios](./configuration.mdx#advanced-fork-scenarios) + +**Pro tip**: Start with a simple example like forking mainnet and querying token balances before building complex test scenarios. \ No newline at end of file