Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 41 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
153 changes: 152 additions & 1 deletion docs/node/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment suggests that using a specific block number reduces memory usage, but this isn't necessarily accurate. Specific block numbers provide reproducibility, while using 'latest' might actually use more memory due to ongoing state changes. The comment should clarify this distinction.

Suggested change
forkBlockNumber: 18500000, // Specific block reduces memory
forkBlockNumber: 18500000, // Specific block ensures reproducibility and may use less memory than 'latest'

Copilot uses AI. Check for mistakes.
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
Expand Down Expand Up @@ -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
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
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The placeholder address 0x... in the code example should be replaced with a complete valid Ethereum address format or a more descriptive placeholder like 0x1234...5678 to show the expected format.

Suggested change
const balance = await node.getBalance('0x...'); // Your test account address
const balance = await node.getBalance('0x1234...5678'); // Your test account address

Copilot uses AI. Check for mistakes.
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
});
```
86 changes: 74 additions & 12 deletions docs/node/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The comment mentions 'stable state' but doesn't explain what makes this specific block number stable or how developers should choose their own block number. Consider adding guidance on selecting appropriate fork blocks.

Suggested change
forkBlockNumber: 18500000, // Use a known good block with stable state
forkBlockNumber: 18500000, // Use a recent, finalized block where the protocol state is stable.
// Choose a block that is not during a major upgrade or reorg, and ideally after all relevant transactions have settled.
// You can find suitable block numbers using a block explorer (e.g., Etherscan) by looking for blocks just before or after important events.

Copilot uses AI. Check for mistakes.
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({
Expand All @@ -88,42 +91,90 @@ 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()
);

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');
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Using a specific NFT collection (BAYC) in the example may become outdated or unavailable. Consider using a more generic example or explaining how developers should substitute their own collection URLs.

Suggested change
await page.goto('https://opensea.io/collection/bored-ape-yacht-club');
// Replace <your-nft-collection> with your desired NFT collection slug
await page.goto('https://opensea.io/collection/<your-nft-collection>');

Copilot uses AI. Check for mistakes.

// 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
Expand All @@ -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.
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.