diff --git a/docs/_docs.json b/docs/_docs.json new file mode 100644 index 0000000..f725069 --- /dev/null +++ b/docs/_docs.json @@ -0,0 +1,578 @@ +{ + "$schema": "https://mintlify.com/docs.json", + "theme": "mint", + "name": "Base Documentation", + "colors": { + "primary": "#578BFA", + "light": "#578BFA", + "dark": "#578BFA" + }, + "favicon": "/favicon.svg", + "contextual": { + "options": ["copy", "chatgpt", "claude"] + }, + "navigation": { + "tabs": [ + { + "tab": "Get Started", + "groups": [ + { + "group": "Introduction", + "pages": ["get-started/base"] + }, + { + "group": "Browse by", + "pages": ["get-started/products", "get-started/use-cases"] + }, + { + "group": "Quickstart", + "pages": [ + "get-started/build-app", + "get-started/launch-token", + "get-started/deploy-chain", + "get-started/deploy-smart-contracts", + "get-started/get-funded" + ] + }, + { + "group": "Build with AI", + "pages": ["get-started/ai-prompting", "get-started/prompt-library"] + } + ], + "global": { + "anchors": [ + { + "anchor": "Status", + "href": "https://status.base.org/", + "icon": "signal-bars" + }, + { + "anchor": "Faucet", + "href": "/base-chain/tools/network-faucets", + "icon": "gas-pump" + }, + { + "anchor": "Bridge", + "href": "/base-chain/network-information/bridges-mainnet", + "icon": "coin" + } + ] + } + }, + { + "tab": "Base Chain", + "groups": [ + { + "group": "General", + "pages": [ + "base-chain/general/network-fees", + "base-chain/general/differences-ethereum-base", + "base-chain/general/base-contracts" + ] + }, + { + "group": "Quickstart", + "pages": [ + "base-chain/quickstart/use-base", + "base-chain/quickstart/deploy-on-base", + "base-chain/quickstart/run-base-node", + "base-chain/quickstart/bridge-token" + ] + }, + { + "group": "Network Information", + "pages": [ + "base-chain/network-information/diffs-ethereum-base", + "base-chain/network-information/base-contracts" + ] + }, + { + "group": "Flashblocks", + "pages": [ + "base-chain/flashblocks/apps", + "base-chain/flashblocks/node-providers" + ] + }, + { + "group": "Node Operators", + "pages": [ + "base-chain/node-operators/performance-tuning", + "base-chain/node-operators/snapshots", + "base-chain/node-operators/troubleshooting" + ] + }, + { + "group": "Tools", + "pages": [ + "base-chain/tools/base-products", + "base-chain/tools/onchain-registry-api", + "base-chain/tools/node-providers", + "base-chain/tools/block-explorers", + "base-chain/tools/network-faucets", + "base-chain/tools/oracles", + "base-chain/tools/data-indexers", + "base-chain/tools/cross-chain", + "base-chain/tools/account-abstraction", + "base-chain/tools/onramps" + ] + }, + { + "group": "Security", + "pages": [ + "base-chain/security/security-council", + "base-chain/security/avoid-malicious-flags", + "base-chain/security/report-vulnerability" + ] + } + ], + "global": { + "anchors": [ + { + "anchor": "GitHub", + "href": "https://github.com/base", + "icon": "github" + }, + { + "anchor": "Status", + "href": "https://status.base.org/", + "icon": "signal-bars" + }, + { + "anchor": "Chain Stats", + "href": "https://www.base.org/stats", + "icon": "chart-line" + }, + { + "anchor": "Explorer", + "href": "https://basescan.com/", + "icon": "magnifying-glass" + }, + { + "anchor": "Support", + "href": "https://discord.com/invite/base", + "icon": "discord" + } + ] + } + }, + { + "tab": "Smart Wallet", + "groups": [ + { + "group": "Get started", + "pages": [ + "smart-wallet/overview", + "smart-wallet/quickstart", + "smart-wallet/recommend-libraries", + "smart-wallet/base-gasless-campaign" + ] + }, + { + "group": "Features", + "pages": [ + "smart-wallet/features/single-sign-on", + "smart-wallet/features/networks", + "smart-wallet/features/passkeys", + "smart-wallet/features/recovery", + "smart-wallet/features/magicspend", + { + "group": "Optional Features", + "pages": [ + "smart-wallet/features/gas-free-transactions", + "smart-wallet/features/spend-permissions", + "smart-wallet/features/batch-transactions", + "smart-wallet/features/custom-gas-tokens", + "smart-wallet/features/sub-accounts" + ] + } + ] + }, + { + "group": "Usage", + "pages": [ + "smart-wallet/usage/signature-verification", + "smart-wallet/usage/popups", + "smart-wallet/usage/simulations", + "smart-wallet/usage/gas-usage", + "smart-wallet/usage/self-calls" + ] + }, + { + "group": "SDK", + "pages": [ + "smart-wallet/sdk/install", + "smart-wallet/sdk/setup", + "smart-wallet/sdk/make-web3-provider", + "smart-wallet/sdk/upgrading-from-3x", + "smart-wallet/sdk/coinbase-wallet-provider" + ] + }, + { + "group": "Guides", + "pages": [ + "smart-wallet/guides/update-existing-app", + "smart-wallet/guides/signing-and-verifying", + "smart-wallet/guides/sign-in-with-ethereum", + "smart-wallet/guides/magicspend", + "smart-wallet/guides/batch-transactions", + "smart-wallet/guides/paymasters", + "smart-wallet/guides/erc20-paymasters", + { + "group": "Sub Accounts", + "pages": [ + "smart-wallet/guides/sub-accounts/overview", + "smart-wallet/guides/sub-accounts/setup", + "smart-wallet/guides/sub-accounts/creating", + "smart-wallet/guides/sub-accounts/using" + ] + }, + { + "group": "Spend Permissions", + "pages": [ + "smart-wallet/guides/spend-permissions/overview", + "smart-wallet/guides/spend-permissions/quickstart", + "smart-wallet/guides/spend-permissions/api-reference" + ] + } + ] + } + ], + "global": { + "anchors": [ + { + "anchor": "GitHub", + "href": "https://github.com/coinbase/onchainkit", + "icon": "github" + }, + { + "anchor": "Support", + "href": "https://discord.com/invite/cdp", + "icon": "discord" + } + ] + } + }, + { + "tab": "OnchainKit", + "groups": [ + { + "group": "Get Started", + "pages": [ + "onchainkit/quickstart", + "onchainkit/templates", + { + "group": "Installation", + "pages": ["onchainkit/nextjs", "onchainkit/vite"] + }, + "onchainkit/troubleshoot", + "onchainkit/telemetry" + ] + }, + { + "group": "Guides", + "pages": [ + "onchainkit/guides/build-with-ai", + "onchainkit/guides/lifecycle-status", + "onchainkit/guides/reach-more-users-with-minikit", + "onchainkit/guides/tailwind-css-integration", + "onchainkit/guides/theme-customization", + "onchainkit/guides/use-basenames" + ] + }, + { + "group": "Components", + "pages": [ + "onchainkit/components/bridge", + "onchainkit/components/buy", + "onchainkit/components/checkout", + "onchainkit/components/earn", + "onchainkit/components/fund", + "onchainkit/components/identity", + "onchainkit/components/mint", + "onchainkit/components/swap", + "onchainkit/components/token", + "onchainkit/components/transaction", + "onchainkit/components/wallet" + ] + }, + { + "group": "API", + "openapi": { + "source": "docs/openapi/onchainkit.yaml", + "directory": "api-reference" + } + }, + { + "group": "Utilities", + "pages": ["onchainkit/utilities/is-base"] + }, + { + "group": "Contribute", + "pages": ["onchainkit/contribute/report-a-bug"] + } + ], + "global": { + "anchors": [ + { + "anchor": "GitHub", + "href": "https://github.com/coinbase/onchainkit", + "icon": "github" + }, + { + "anchor": "Playground", + "href": "https://onchainkit.xyz/playground", + "icon": "gamepad" + }, + { + "anchor": "Support", + "href": "https://discord.com/invite/cdp", + "icon": "discord" + } + ] + } + }, + { + "tab": "OnchainTestKit", + "groups": [ + { + "group": "Overview", + "pages": [ + "onchaintestkit/root/root" + ] + }, + { + "group": "Modules", + "pages": [ + "onchaintestkit/contracts/contracts", + "onchaintestkit/node/node", + "onchaintestkit/wallets/wallets" + ] + } + ], + "global": { + "anchors": [ + { + "anchor": "GitHub", + "href": "https://github.com/coinbase/onchaintestkit", + "icon": "github" + } + ] + } + }, + { + "tab": "Cookbook", + "groups": [ + { + "group": "Use Cases", + "pages": [ + "cookbook/onboard-any-user", + "cookbook/accept-crypto-payments", + "cookbook/launch-ai-agents", + "cookbook/launch-tokens", + "cookbook/deploy-a-chain", + "cookbook/decentralize-your-social-app", + "cookbook/defi-your-app", + "cookbook/go-gasless" + ] + }, + { + "group": "Build with AI", + "pages": ["cookbook/ai-prompting", "cookbook/base-builder-mcp"] + } + ] + }, + { + "tab": "Showcase", + "pages": ["showcase"] + }, + { + "tab": "Learn", + "groups": [ + { + "group": "Building Onchain", + "pages": [ + "learn/welcome", + { + "group": "Onchain Concepts", + "pages": [ + "learn/onchain-concepts/the-onchain-tech-stack", + "learn/onchain-concepts/unique-aspects-of-building-onchain", + "learn/onchain-concepts/onchain-development-flow", + "learn/onchain-concepts/offchain-to-onchain" + ] + } + ] + }, + { + "group": "Ethereum 101", + "pages": [ + "learn/introduction-to-ethereum", + "learn/ethereum-dev-overview", + "learn/ethereum-applications", + "learn/gas-use-in-eth-transactions", + "learn/evm-diagram", + "learn/guide-to-base" + ] + }, + { + "group": "Onchain App Development", + "pages": [ + "learn/deploy-with-fleek", + "learn/account-abstraction", + "learn/cross-chain-development", + "learn/client-side-development" + ] + }, + { + "group": "Smart Contract Development", + "pages": [ + "learn/solidity/introduction", + "learn/solidity/anatomy", + { + "group": "Introduction to Solidity", + "pages": [ + "learn/solidity/video-tutorial", + "learn/solidity/overview", + "learn/solidity/introduction-to-remix", + "learn/solidity/remix-guide", + "learn/solidity/deployment-in-remix", + "learn/solidity/step-by-step" + ] + }, + { + "group": "Contracts and Basic Functions", + "pages": [ + "learn/solidity/introduction-to-contracts", + "learn/solidity/hello-world", + "learn/solidity/basic-types", + "learn/solidity/exercise-basics" + ] + }, + { + "group": "Deploying to a Testnet", + "pages": [ + "learn/solidity/test-networks-overview", + "learn/solidity/test-networks", + "learn/solidity/deploy-to-sepolia", + "learn/solidity/contract-verification", + "learn/solidity/exercise-deployment" + ] + }, + { + "group": "Control Structures", + "pages": [ + "learn/solidity/standard-control-structures", + "learn/solidity/loops", + "learn/solidity/require-revert-error", + "learn/solidity/control-overview", + "learn/solidity/exercise-control" + ] + } + ] + }, + { + "group": "Development with Foundry", + "pages": [ + "learn/foundry/introduction-to-foundry", + "learn/foundry/testing" + ] + }, + { + "group": "Development with Hardhat", + "pages": [ + { + "group": "Hardhat Setup and Overview", + "pages": [ + "learn/hardhat/overview", + "learn/hardhat/creating-project", + "learn/hardhat/setup" + ] + }, + { + "group": "Testing with Typescript", + "pages": [ + "learn/hardhat/testing", + "learn/hardhat/writing-tests", + "learn/hardhat/contract-abi-testing", + "learn/hardhat/testing-guide" + ] + }, + { + "group": "Etherscan", + "pages": [ + "learn/hardhat/etherscan-guide", + "learn/hardhat/etherscan-video" + ] + }, + { + "group": "Deploying Smart Contracts", + "pages": [ + "learn/hardhat/installing-deploy", + "learn/hardhat/setup-deploy-script", + "learn/hardhat/testing-deployment", + "learn/hardhat/network-configuration", + "learn/hardhat/deployment", + "learn/hardhat/deployment-guide" + ] + }, + { + "group": "Verifying Smart Contracts", + "pages": [ + "learn/hardhat/verify-video", + "learn/hardhat/verify-guide" + ] + }, + { + "group": "Mainnet Forking", + "pages": [ + "learn/hardhat/fork-video", + "learn/hardhat/fork-guide" + ] + } + ] + }, + { + "group": "Token Development", + "pages": [] + }, + { + "group": "Exercise Contracts", + "pages": ["learn/exercise-contracts"] + } + ] + } + ] + }, + "logo": { + "light": "/logo/light.svg", + "dark": "/logo/dark.svg" + }, + "navbar": { + "links": [ + { + "label": "Blog", + "href": "https://blog.base.dev/" + }, + { + "label": "GitHub", + "href": "https://github.com/base" + }, + { + "label": "Support", + "href": "https://discord.com/invite/base" + } + ], + "primary": { + "type": "button", + "label": "Base.org", + "href": "https://base.org" + } + }, + "footer": { + "socials": { + "x": "https://x.com/base", + "github": "https://github.com/base", + "linkedin": "https://linkedin.com/company/mintlify" + } + } +} diff --git a/docs/docs.json b/docs/docs.json index ba79e7d..db2cce4 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -632,6 +632,84 @@ ] } }, + { + "tab": "OnchainTestKit", + "groups": [ + { + "group": "Introduction", + "pages": [ + "onchaintestkit/overview" + ] + }, + { + "group": "Getting Started", + "pages": [ + "onchaintestkit/quickstart", + "onchaintestkit/installation", + "onchaintestkit/configuration" + ] + }, + { + "group": "Guides", + "pages": [ + "onchaintestkit/writing-tests", + "onchaintestkit/smart-contracts" + ] + }, + { + "group": "Advanced", + "pages": [ + "onchaintestkit/ci-cd" + ] + }, + { + "group": "Components", + "pages": [ + { + "group": "Contracts", + "pages": [ + "onchaintestkit/contracts/overview", + "onchaintestkit/contracts/proxy-deployer", + "onchaintestkit/contracts/smart-contract-manager" + ] + }, + { + "group": "Node", + "pages": [ + "onchaintestkit/node/overview", + "onchaintestkit/node/configuration", + "onchaintestkit/node/api-reference" + ] + }, + { + "group": "Wallets", + "pages": [ + "onchaintestkit/wallets/overview", + "onchaintestkit/wallets/common-actions", + "onchaintestkit/wallets/metamask", + "onchaintestkit/wallets/coinbase", + "onchaintestkit/wallets/advanced-features", + "onchaintestkit/wallets/api-reference" + ] + } + ] + } + ], + "global": { + "anchors": [ + { + "anchor": "GitHub", + "href": "https://github.com/coinbase/onchaintestkit", + "icon": "github" + }, + { + "anchor": "Support", + "href": "https://discord.com/invite/cdp", + "icon": "discord" + } + ] + } + }, { "tab": "Wallet App", "groups": [ diff --git a/docs/onchaintestkit/ci-cd.mdx b/docs/onchaintestkit/ci-cd.mdx new file mode 100644 index 0000000..d7bb8c5 --- /dev/null +++ b/docs/onchaintestkit/ci-cd.mdx @@ -0,0 +1,148 @@ +--- +title: "CI/CD Integration" +description: "Set up OnchainTestKit tests in your continuous integration pipeline" +--- + +Learn how to integrate OnchainTestKit into your CI/CD pipeline for automated blockchain testing on every commit. The example will likely will be slightly different depending on your project structure. + +## GitHub Actions + +### Basic Example Setup + +Create `.github/workflows/e2e-tests.yml`: + +```yaml +name: E2E Tests + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + +jobs: + e2e: + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + + - name: Set up Corepack + yarn + run: | + npm install -g corepack + yarn set version 4.9.2 + + - name: Install root dependencies + run: yarn + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Build contracts + run: | + cd smart-contracts + forge install foundry-rs/forge-std + forge install OpenZeppelin/openzeppelin-contracts + forge build + + - name: Install Playwright browsers + run: yarn playwright install --with-deps + + - name: Prepare wallet extensions + run: | + yarn prepare-metamask + yarn prepare-coinbase + + - name: Build application + run: | + echo "E2E_TEST_SEED_PHRASE=${{ secrets.E2E_TEST_SEED_PHRASE }}" > .env + echo "E2E_CONTRACT_PROJECT_ROOT=../smart-contracts" >> .env + yarn build + + - name: Install xvfb + run: sudo apt-get update && sudo apt-get install -y xvfb + + - name: Run E2E tests + env: + NODE_OPTIONS: '--dns-result-order=ipv4first' + run: xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" yarn test:e2e +``` + +## Optimization Tips + + + + +Always run headless in CI for better performance: + +```typescript +use: { + headless: !!process.env.CI, + video: process.env.CI ? 'retain-on-failure' : 'off', +} +``` + + + +Add smart retry logic for flaky tests: + +```typescript +retries: process.env.CI ? 2 : 0, +``` + + + +## Troubleshooting + + + + +This error occurs when Node.js resolves "localhost" to ::1 (IPv6) but Anvil only listens on IPv4 addresses. + +**Solution:** Force Node.js to prefer IPv4 addresses by setting the `NODE_OPTIONS` environment variable: + +```yaml +- name: Run E2E tests + env: + NODE_OPTIONS: '--dns-result-order=ipv4first' + run: xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" yarn test:e2e +``` + +This ensures: +- `localhost` resolves to `127.0.0.1` (IPv4) instead of `::1` (IPv6) +- Anvil can properly listen on the resolved address +- Tests can successfully connect to the local blockchain + + + +E2E tests may become less reliable with increased parallelism, especially when multiple wallet interactions occur simultaneously. This is +not an issue locally though. + +**Solutions:** +- Reduce test parallelism in CI environments: + ```typescript + workers: process.env.CI ? 1 : 4, + ``` +- Add longer timeouts for wallet operations: + ```typescript + timeout: 60_000, // 60 seconds for wallet-heavy tests + ``` +- Use test isolation to prevent state conflicts: + ```typescript + testInfo.annotations.push({ type: 'serial' }); + ``` + + + + +## Next Steps + +- [Browse examples](/onchaintestkit/examples) \ No newline at end of file diff --git a/docs/onchaintestkit/configuration.mdx b/docs/onchaintestkit/configuration.mdx new file mode 100644 index 0000000..9f91f6c --- /dev/null +++ b/docs/onchaintestkit/configuration.mdx @@ -0,0 +1,286 @@ +--- +title: "Configuration" +description: "Learn how to configure wallets, networks, and test environments" +--- + +OnchainTestKit uses a fluent builder pattern for configuration, making it easy to set up different wallet and network combinations for your tests. + +## Configuration Builder + +The `configure()` function provides a chainable API for building test configurations: + +```typescript +import { configure } from '@coinbase/onchaintestkit'; + +const config = configure() + .withLocalNode({ chainId: 1337 }) + .withMetaMask() + .withNetwork({ + name: 'Base Sepolia', + rpcUrl: 'http://localhost:8545', + chainId: 84532, + symbol: 'ETH', + }) + .withSeedPhrase({ + seedPhrase: process.env.E2E_TEST_SEED_PHRASE!, + password: 'PASSWORD', + }) + .build(); +``` + +## Wallet Configuration + +### MetaMask Configuration + + +```typescript Basic Setup +import { configure } from '@coinbase/onchaintestkit'; + +const metamaskConfig = configure() + .withMetaMask() + .withSeedPhrase({ + seedPhrase: process.env.E2E_TEST_SEED_PHRASE!, + password: 'PASSWORD', + }) + .build(); +``` + +```typescript With Local Node +import { baseSepolia } from 'viem/chains'; + +const metamaskConfig = configure() + .withLocalNode({ + chainId: baseSepolia.id, + forkUrl: process.env.E2E_TEST_FORK_URL, + forkBlockNumber: BigInt(process.env.E2E_TEST_FORK_BLOCK_NUMBER ?? "0"), + hardfork: 'cancun', + }) + .withMetaMask() + .withSeedPhrase({ + seedPhrase: process.env.E2E_TEST_SEED_PHRASE!, + password: 'PASSWORD', + }) + .withNetwork({ + name: 'Base Sepolia', + chainId: baseSepolia.id, + symbol: 'ETH', + rpcUrl: 'http://localhost:8545', + }) + .build(); +``` + +```typescript Complete Example +// e2e/config/metamask.config.ts +import { baseSepolia } from "viem/chains" +import { configure } from "@coinbase/onchaintestkit" + +export const DEFAULT_PASSWORD = "PASSWORD" +export const DEFAULT_SEED_PHRASE = process.env.E2E_TEST_SEED_PHRASE + +const config = configure() + .withLocalNode({ + chainId: baseSepolia.id, + forkUrl: process.env.E2E_TEST_FORK_URL, + forkBlockNumber: BigInt(process.env.E2E_TEST_FORK_BLOCK_NUMBER ?? "0"), + hardfork: "cancun", + }) + .withMetaMask() + .withSeedPhrase({ + seedPhrase: DEFAULT_SEED_PHRASE ?? "", + password: DEFAULT_PASSWORD, + }) + .withNetwork({ + name: "Base Sepolia", + chainId: baseSepolia.id, + symbol: "ETH", + rpcUrl: "http://localhost:8545", + }) + .build() + +export const metamaskWalletConfig = config +``` + + +### Coinbase Wallet Configuration + + +```typescript Basic Setup +import { configure } from '@coinbase/onchaintestkit'; + +const coinbaseConfig = configure() + .withCoinbase() + .withSeedPhrase({ + seedPhrase: process.env.E2E_TEST_SEED_PHRASE!, + password: 'COMPLEXPASSWORD1', + }) + .build(); +``` + +```typescript Complete Example +// e2e/config/coinbase.config.ts +import { baseSepolia } from "viem/chains" +import { configure } from "@coinbase/onchaintestkit" + +export const DEFAULT_PASSWORD = "COMPLEXPASSWORD1" +export const DEFAULT_SEED_PHRASE = process.env.E2E_TEST_SEED_PHRASE + +const config = configure() + .withLocalNode({ + chainId: baseSepolia.id, + forkUrl: process.env.E2E_TEST_FORK_URL, + forkBlockNumber: BigInt(process.env.E2E_TEST_FORK_BLOCK_NUMBER ?? "0"), + hardfork: "cancun", + }) + .withCoinbase() + .withSeedPhrase({ + seedPhrase: DEFAULT_SEED_PHRASE ?? "", + password: DEFAULT_PASSWORD, + }) + .withNetwork({ + name: "Base Sepolia", + chainId: baseSepolia.id, + symbol: "ETH", + rpcUrl: "http://localhost:8545", + }) + .build() + +export const coinbaseWalletConfig = config +``` + + +## Network Configuration + +### Local Node Options + +Configure a local Anvil node with specific parameters: + +```typescript +.withLocalNode({ + // Required: Chain ID for the local network + chainId: 1337, + + // Optional: Fork from an existing network + forkUrl: "https://mainnet.base.org", + + // Optional: Fork from a specific block + forkBlockNumber: BigInt("12345678"), + + // Optional: EVM hardfork to use + hardfork: "cancun", + + // Optional: Port range for parallel testing + minPort: 9545, + maxPort: 9645, +}) +``` + +### Network Details + +Add custom networks to your wallet: + +```typescript +.withNetwork({ + name: "My Custom Network", + chainId: 12345, + symbol: "ETH", + rpcUrl: "https://my-rpc-endpoint.com", // This is ususally localhost:8545 since the onchaintestkit network interceptor listens to this port and forwards RPC requests sent to this port to the anvil node + isTestnet: true, // Optional: mark as testnet +}) +``` + +## Environment Variables + + +Use environment variables to keep sensitive data out of your code and enable different configurations for different environments. + + +### Required Variables + +```bash .env +# Test wallet seed phrase +E2E_TEST_SEED_PHRASE="test test test test test test test test test test test junk" +``` + +### Other Env Variables + +```bash .env +# Fork configuration +E2E_TEST_FORK_URL="https://mainnet.base.org" +E2E_TEST_FORK_BLOCK_NUMBER="12345678" + +# Smart contract project +E2E_CONTRACT_PROJECT_ROOT="../smart-contracts" + +# Custom RPC endpoints +E2E_BASE_MAINNET_RPC="https://my-custom-rpc.com" +E2E_BASE_SEPOLIA_RPC="https://my-testnet-rpc.com" + +# Test timeouts +E2E_TEST_TIMEOUT="60000" +``` + +## Using Configurations in Tests + +```typescript +import { createOnchainTest } from '@coinbase/onchaintestkit'; +import { metamaskWalletConfig } from './config/metamask.config'; + +const test = createOnchainTest(metamaskWalletConfig); + +test('my test', async ({ page, metamask, node }) => { + // Your test code here + console.log(`Local node running on port: ${node?.port}`); +}); +``` + +## Configuration Best Practices + + + +Create separate configuration files for each wallet type to keep your code organized: + +``` +e2e/config/ +├── metamask.config.ts +├── coinbase.config.ts +└── shared.config.ts +``` + + + +Never hardcode sensitive data. Always use environment variables for: +- Seed phrases +- API keys +- RPC endpoints +- Private keys + + + +Export typed configurations for better IDE support: + +```typescript +import type { OnchainTestConfig } from '@coinbase/onchaintestkit'; + +export const metamaskConfig: OnchainTestConfig = configure() + .withMetaMask() + // ... rest of config + .build(); +``` + + + +Add validation for required environment variables: + +```typescript +if (!process.env.E2E_TEST_SEED_PHRASE) { + throw new Error('E2E_TEST_SEED_PHRASE is required'); +} +``` + + + +## Next Steps + +- [Write your first test](/onchaintestkit/writing-tests) +- [Test smart contracts](/onchaintestkit/smart-contracts) +- [See complete examples](/onchaintestkit/examples) \ No newline at end of file diff --git a/docs/onchaintestkit/contracts/overview.mdx b/docs/onchaintestkit/contracts/overview.mdx new file mode 100644 index 0000000..e3171d8 --- /dev/null +++ b/docs/onchaintestkit/contracts/overview.mdx @@ -0,0 +1,166 @@ +--- +title: "Smart Contract Testing Overview" +description: "Deterministic contract deployment and testing with OnchainTestKit" +--- + +## Overview + +The smart contract manager in `@coinbase/onchaintestkit` provides a robust, deterministic, and high-performance framework for deploying and interacting with Ethereum smart contracts in end-to-end (E2E) testing environments. These utilities are designed for use with local Ethereum nodes (e.g., Anvil), and leverage the [viem](https://viem.sh/) library for fast, type-safe blockchain interactions. + +### Why Is This Important? + +- **Deterministic Testing:** Ensures contracts are always deployed at the same address for a given salt and bytecode, making tests reproducible and reliable. +- **Performance:** Uses viem for fast, lightweight blockchain interactions, reducing test flakiness and runtime. +- **Parallelization:** Designed for parallel test execution, avoiding port and state conflicts. +- **Automation:** Automates complex setup steps, allowing focus on test logic rather than blockchain plumbing. +- **Integration:** Seamlessly integrates with Playwright and the broader `@coinbase/onchaintestkit` E2E testing ecosystem. + +## Key Features + + + + Deploy contracts at predictable addresses using the CREATE2 opcode and a deterministic deployment proxy. + + + + Ensure the deterministic deployment proxy is present and deployed as needed. + + + + Deploy contracts and execute function calls as part of test setup, supporting complex test scenarios. + + + + Designed to work with the `LocalNodeManager` for parallel test execution, ensuring isolated and reproducible blockchain state. + + + + Loads contract ABIs and bytecode directly from Foundry build artifacts. + + + +## Architecture + + +```mermaid +flowchart TD + subgraph Test Environment + A[Playwright Test] + B[LocalNodeManager] + C[SmartContractManager] + D[ProxyDeployer] + E[Anvil Node] + end + A -->|uses| B + A -->|uses| C + C -->|uses| D + B -->|manages| E + C -->|deploys contracts| E + D -->|deploys proxy| E +``` + + +## Components + +### ProxyDeployer + +A utility class to manage the deterministic deployment proxy contract, which enables CREATE2-based deployments at predictable addresses. The proxy ensures that contracts can be deployed to the same address across different test runs. + +### SmartContractManager + +A high-level manager for deploying contracts (using CREATE2), executing contract calls, and orchestrating contract state for tests. It handles the complexity of contract deployment and interaction, allowing you to focus on test logic. + +## How It Works + + +```mermaid +sequenceDiagram + participant Test as Playwright Test + participant SCM as SmartContractManager + participant PD as ProxyDeployer + participant Node as LocalNodeManager + participant Anvil as Anvil Node + + Test->>Node: start() + Test->>SCM: initialize(node) + SCM->>PD: ensureProxyDeployed() + PD->>Anvil: Deploy proxy if needed + SCM->>Anvil: Deploy contract via proxy (CREATE2) + SCM->>Anvil: Execute contract calls + Test->>Node: stop() +``` + + +## Use Cases + +### Testing DeFi Protocols + +Deploy and test complex DeFi protocols with deterministic addresses: + +```typescript +await scm.setContractState({ + deployments: [ + { + name: 'Token', + args: ['USDC', 'USDC', 6], + salt: generateSalt('usdc'), + deployer: accounts[0], + }, + { + name: 'LiquidityPool', + args: [tokenAddress], + salt: generateSalt('pool'), + deployer: accounts[0], + }, + ], + calls: [ + { + target: tokenAddress, + functionName: 'mint', + args: [poolAddress, parseUnits('1000000', 6)], + account: accounts[0], + }, + ], +}, node); +``` + +### Testing NFT Marketplaces + +Set up NFT contracts and marketplace listings: + +```typescript +const nftAddress = await scm.deployContract({ + name: 'NFTCollection', + args: ['My NFTs', 'NFT'], + salt: generateSalt('nft'), + deployer: artist, +}); + +const marketAddress = await scm.deployContract({ + name: 'Marketplace', + args: [feeRecipient, 250], // 2.5% fee + salt: generateSalt('market'), + deployer: deployer, +}); +``` + +## Best Practices + + +Always use deterministic salts for reproducible tests. Consider using a salt generation function that includes test names or IDs. + + + +Remember that CREATE2 addresses depend on the bytecode. Contract upgrades or compiler changes will result in different addresses. + + +## Next Steps + + + + Learn about the deterministic deployment proxy + + + Explore the contract management API + + \ No newline at end of file diff --git a/docs/onchaintestkit/contracts/proxy-deployer.mdx b/docs/onchaintestkit/contracts/proxy-deployer.mdx new file mode 100644 index 0000000..10bb1a8 --- /dev/null +++ b/docs/onchaintestkit/contracts/proxy-deployer.mdx @@ -0,0 +1,150 @@ +--- +title: "ProxyDeployer" +description: "Deterministic deployment proxy management for CREATE2 deployments" +--- + +## Overview + +The `ProxyDeployer` handles deployment and verification of the deterministic deployment proxy contract. This proxy is required for CREATE2-based deterministic contract deployments, ensuring that contracts can be deployed at predictable addresses across different test runs. + +## Why Use ProxyDeployer? + + +CREATE2 allows contracts to be deployed at deterministic addresses, but it requires a factory contract. The ProxyDeployer manages this factory (proxy) contract, which is deployed at a fixed address across all networks. + + +## Constructor + +```typescript +new ProxyDeployer(node: LocalNodeManager) +``` + +Creates a new ProxyDeployer instance. + + + The local Ethereum node manager instance + + +## Properties + + + The fixed address at which the proxy is deployed: `0x4e59b44847b379578588920cA78FbF26c0B4956C` + + + + Raw transaction data for deploying the proxy + + + + viem public client for interacting with the node + + + + RPC URL of the local node + + +## Methods + +### isProxyDeployed() + +```typescript +async isProxyDeployed(): Promise +``` + +Checks if the deterministic deployment proxy is already deployed. + +**Returns:** `true` if the proxy is deployed, `false` otherwise + +**Example:** +```typescript +const isDeployed = await proxyDeployer.isProxyDeployed(); +if (!isDeployed) { + console.log('Proxy needs to be deployed'); +} +``` + +### ensureProxyDeployed() + +```typescript +async ensureProxyDeployed(): Promise +``` + +Deploys the proxy if not already present. This method is idempotent - it's safe to call multiple times. + +**Example:** +```typescript +await proxyDeployer.ensureProxyDeployed(); +// Proxy is now guaranteed to be deployed +``` + +### getProxyAddress() + +```typescript +getProxyAddress(): Address +``` + +Returns the address of the deterministic deployment proxy. + +**Returns:** The proxy address (`0x4e59b44847b379578588920cA78FbF26c0B4956C`) + +**Example:** +```typescript +const proxyAddress = proxyDeployer.getProxyAddress(); +console.log(`Proxy deployed at: ${proxyAddress}`); +``` + +## How It Works + +The ProxyDeployer uses a pre-signed transaction to deploy the deterministic deployment proxy. This ensures that: + +1. The proxy is always deployed at the same address +2. No private keys are needed for deployment +3. The deployment is deterministic across all networks + +### Deployment Process + + +```mermaid +sequenceDiagram + participant Test + participant PD as ProxyDeployer + participant Node as Anvil Node + + Test->>PD: ensureProxyDeployed() + PD->>Node: Check if proxy exists + alt Proxy not deployed + PD->>Node: Send raw deployment TX + Node-->>PD: Proxy deployed + end + PD-->>Test: Ready for CREATE2 deployments +``` + + +## Technical Details + +### Proxy Contract + +The deterministic deployment proxy is a minimal contract that enables CREATE2 deployments. It's based on [this method](https://github.com/Arachnid/deterministic-deployment-proxy) for deterministic contract deployment. + +### Fixed Address + +The proxy is always deployed at `0x4e59b44847b379578588920cA78FbF26c0B4956C` across all EVM-compatible chains. This address is derived from: +- A specific deployer address +- A zero nonce +- The proxy's bytecode + +## Best Practices + + +Always ensure the proxy is deployed before attempting CREATE2 deployments. The `SmartContractManager` handles this automatically. + + + +Don't attempt to deploy contracts via CREATE2 without the proxy. It will fail with an error. + + +## See Also + +- [SmartContractManager](/onchaintestkit/contracts/smart-contract-manager) - High-level contract management +- [LocalNodeManager](/onchaintestkit/node/overview) - Node management for testing +- [CREATE2 Opcode](https://eips.ethereum.org/EIPS/eip-1014) - EIP-1014 specification \ No newline at end of file diff --git a/docs/onchaintestkit/contracts/smart-contract-manager.mdx b/docs/onchaintestkit/contracts/smart-contract-manager.mdx new file mode 100644 index 0000000..7a269c8 --- /dev/null +++ b/docs/onchaintestkit/contracts/smart-contract-manager.mdx @@ -0,0 +1,224 @@ +--- +title: "SmartContractManager" +description: "High-level API for deterministic contract deployment and testing" +--- + +## Overview + +The `SmartContractManager` is a high-level manager for deploying contracts using CREATE2, executing contract calls, and orchestrating contract state for tests. It handles the complexity of contract deployment and interaction, allowing you to focus on test logic rather than blockchain plumbing. + +## Constructor + +```typescript +new SmartContractManager(projectRoot: string) +``` + +Creates a new SmartContractManager instance. + + + Path to the root of the project (used to locate contract artifacts) + + +## Properties + + + Root directory for artifact lookup + + + + viem public client (initialized in `initialize`) + + + + viem wallet client (initialized in `initialize`) + + + + Map of deployed contract addresses to their ABIs + + + + Instance of ProxyDeployer for managing the deployment proxy + + +## Methods + +### initialize() + +```typescript +async initialize(node: LocalNodeManager): Promise +``` + +Initializes viem clients and ensures the proxy is deployed. Must be called before using other methods. + + + The local node manager instance + + +### deployContract() + +```typescript +async deployContract(deployment: ContractDeployment): Promise
+``` + +Deploys a contract using CREATE2 and stores its ABI. + + + Contract deployment configuration + + +**Returns:** The deployed contract address + +### executeCall() + +```typescript +async executeCall(call: ContractCall): Promise +``` + +Executes a contract function call as a transaction. + + + Contract call configuration + + +**Returns:** The transaction hash + +### setContractState() + +```typescript +async setContractState( + config: SetupConfig, + node: LocalNodeManager +): Promise +``` + +Deploys contracts and executes calls as specified in a setup config. This is useful for setting up complex test scenarios. + + + Setup configuration with deployments and calls + + + + The local node manager instance + + +### predictContractAddress() + +```typescript +predictContractAddress( + salt: Hex, + bytecode: Hex, + args: readonly unknown[] +): Address +``` + +Predicts the address for a CREATE2 deployment without actually deploying. + + + 32-byte salt for CREATE2 + + + + Contract bytecode + + + + Constructor arguments + + +**Returns:** The predicted contract address + +## Contract Artifacts + +The SmartContractManager loads contract artifacts from your project's build directory. It supports: + +- **Foundry**: Looks for artifacts in `out/` directory +- **Hardhat**: Looks for artifacts in `artifacts/` directory + +### Artifact Structure + +Artifacts should contain: +```typescript +{ + abi: [...], // Contract ABI + bytecode: "0x..." // Contract bytecode +} +``` + +## Complete Examples + +### Basic Token Deployment + +```typescript +import { SmartContractManager } from '@coinbase/onchaintestkit/contracts/SmartContractManager'; +import { LocalNodeManager } from '@coinbase/onchaintestkit/node/LocalNodeManager'; + +async function deployToken() { + const node = new LocalNodeManager({ chainId: 31337 }); + await node.start(); + + try { + const scm = new SmartContractManager(process.cwd()); + await scm.initialize(node); + + // Deploy ERC20 token + const tokenAddress = await scm.deployContract({ + name: 'ERC20Token', + args: ['Test Token', 'TEST', 1000000n * 10n ** 18n], + salt: '0x' + '01'.repeat(32), + deployer: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + }); + + console.log(`Token deployed at: ${tokenAddress}`); + + // Mint tokens + await scm.executeCall({ + target: tokenAddress, + functionName: 'mint', + args: ['0x70997970C51812dc3A010C7d01b50e0d17dc79C8', 1000n * 10n ** 18n], + account: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + }); + + } finally { + await node.stop(); + } +} +``` + +## Best Practices + + + + Always call `initialize()` before using other methods + + + + Generate salts based on test names or contract purposes for reproducibility + + + + Wrap deployments in try-catch blocks to handle failures gracefully + + + + Always stop the node after tests complete + + + +## Tips and Tricks + + +Use the `predictContractAddress` method to get addresses before deployment. This is useful for setting up circular dependencies. + + + +The SmartContractManager keeps track of deployed contracts' ABIs, making it easier to interact with them later. + + + +Contract addresses depend on bytecode. Changing compiler settings or contract code will result in different addresses. + + +## See Also + +- [ProxyDeployer](/onchaintestkit/contracts/proxy-deployer) - Lower-level proxy management \ No newline at end of file diff --git a/docs/onchaintestkit/installation.mdx b/docs/onchaintestkit/installation.mdx new file mode 100644 index 0000000..c848db7 --- /dev/null +++ b/docs/onchaintestkit/installation.mdx @@ -0,0 +1,245 @@ +--- +title: "Installation & Setup" +description: "Complete installation guide for OnchainTestKit with troubleshooting" +--- + +This guide provides detailed instructions for installing and setting up OnchainTestKit in your project. + +## System Requirements + + +OnchainTestKit requires the following software to be installed on your system: + + +- **Node.js**: Version 14 or higher +- **Package Manager**: npm, yarn, bun, or pnpm +- **Foundry**: For running local Anvil nodes +- **Operating System**: macOS, Linux, or Windows (with WSL2) + +## Installation Steps + + + +```bash +# Install OnchainTestKit and Playwright +yarn add -D @coinbase/onchaintestkit @playwright/test + +# Install Playwright browsers with system dependencies +yarn playwright install --with-deps + +# Download wallet extensions +yarn prepare-metamask +yarn prepare-coinbase +``` + + + +```bash +# Install OnchainTestKit and Playwright +npm install -D @coinbase/onchaintestkit @playwright/test + +# Install Playwright browsers with system dependencies +npx playwright install --with-deps + +# Download wallet extensions +npm run prepare-metamask +npm run prepare-coinbase +``` + + + +```bash +# Install OnchainTestKit and Playwright +bun add -D @coinbase/onchaintestkit @playwright/test + +# Install Playwright browsers with system dependencies +bunx playwright install --with-deps + +# Download wallet extensions +bun prepare-metamask +bun prepare-coinbase +``` + + + +## Installing Foundry + +OnchainTestKit uses Anvil (part of Foundry) for local blockchain nodes. Install Foundry using the official installer: + +```bash +# Install Foundry +curl -L https://foundry.paradigm.xyz | bash + +# Update PATH (may need to restart terminal) +source ~/.bashrc + +# Verify installation +anvil --version +``` + + +For Windows users, we recommend using WSL2 for the best experience. Foundry works natively on macOS and Linux. + + +## Environment Configuration + +Create a `.env` file in your project root with the following variables: + +```bash .env +# Test wallet seed phrase (NEVER use production wallets!) +E2E_TEST_SEED_PHRASE="test test test test test test test test test test test junk" + +# Optional: Fork configuration for testing against mainnet state +E2E_TEST_FORK_URL="https://mainnet.base.org" +E2E_TEST_FORK_BLOCK_NUMBER="12345678" + +# Optional: Smart contract project root +E2E_CONTRACT_PROJECT_ROOT="../smart-contracts" +``` + + +Never commit `.env` files to version control. Add `.env` to your `.gitignore` file. + + +## Project Structure + +We recommend organizing your tests in the following structure: + +``` +your-project/ +├── e2e/ +│ ├── tests/ +│ │ ├── connect.spec.ts +│ │ ├── transactions.spec.ts +│ │ └── contracts.spec.ts +│ ├── config/ +│ │ ├── metamask.config.ts +│ │ └── coinbase.config.ts +│ └── helpers/ +│ └── wallet.helpers.ts +├── src/ # Your application code +├── .env # Environment variables +└── playwright.config.ts # Playwright configuration +``` + +## Playwright Configuration + +Create a `playwright.config.ts` file: + +```typescript playwright.config.ts +import { defineConfig, devices } from "@playwright/test" +require("dotenv").config({ path: "./.env" }) + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ + + +// Use process.env.PORT by default and fallback to port 4000 +const PORT = process.env.PORT || 4000 + +// Set webServer.url and use.baseURL with the location of the WebServer respecting the correct set port +const baseURL = `http://localhost:${PORT}` + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + timeout: 120000, + testDir: "./e2e", + /* Run tests in files in parallel */ + fullyParallel: false, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 3 : 0, + /* Opt into parallel tests on CI. */ + workers: 10, + maxFailures: 3, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "line", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + // Run your local dev server before starting the tests: + // https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests + webServer: { + command: "yarn start -p 4000", + url: baseURL, + timeout: 30 * 1000, + reuseExistingServer: !process.env.CI, + }, +}) + +``` + +## Verifying Installation + +Run this command to verify everything is installed correctly: + +```bash +# Check Node.js +node --version + +# Check Playwright +npx playwright --version + +# Check Anvil +anvil --version + +# List installed browsers +npx playwright show-browsers +``` + +## Next Steps + +Now that you have OnchainTestKit installed, you're ready to: + +- [Configure your wallets](/onchaintestkit/configuration) +- [Write your first test](/onchaintestkit/writing-tests) +- [Set up smart contract testing](/onchaintestkit/smart-contracts) \ No newline at end of file diff --git a/docs/onchaintestkit/node/api-reference.mdx b/docs/onchaintestkit/node/api-reference.mdx new file mode 100644 index 0000000..3de3ae6 --- /dev/null +++ b/docs/onchaintestkit/node/api-reference.mdx @@ -0,0 +1,359 @@ +--- +title: "Node API Reference" +description: "Complete API reference for LocalNodeManager" +--- + +## LocalNodeManager Class + +Manages the lifecycle and state of a local Anvil Ethereum node for testing. Handles dynamic port allocation, process management, and exposes a rich set of methods for manipulating the blockchain state. + +### Constructor + +```typescript +new LocalNodeManager(config?: NodeConfig) +``` + +Creates a new instance with the specified configuration. + + + Optional node configuration options. See [Configuration](/onchaintestkit/node/configuration) for details. + + +### Properties + + + The allocated port number, or -1 if not started. + + + + The RPC URL for the running node (e.g., `http://localhost:12345`). + + +## Lifecycle Methods + +### start() + +```typescript +async start(): Promise +``` + +Starts the Anvil node with the configured options. Allocates a port, spawns the process, and waits for readiness. + +```typescript +const node = new LocalNodeManager() +await node.start() +console.log(`Node running at ${node.rpcUrl}`) +``` + +### stop() + +```typescript +async stop(): Promise +``` + +Stops the running Anvil node and cleans up resources. + +```typescript +await node.stop() +``` + +### getPort() + +```typescript +getPort(): number | null +``` + +Returns the allocated port number for this node instance, or `null` if not started. + +```typescript +const port = node.getPort() +// Use port for configuration +``` + +## State Management Methods + +### snapshot() + +```typescript +async snapshot(): Promise +``` + +Takes a snapshot of the current chain state. Returns a snapshot ID for later use with `revert()`. + +```typescript +const snapshotId = await node.snapshot() +// Make changes... +await node.revert(snapshotId) +``` + +### revert() + +```typescript +async revert(snapshotId: string): Promise +``` + +Reverts the chain state to a previous snapshot. + + + The ID returned from `snapshot()` + + +### reset() + +```typescript +async reset(forkBlock?: bigint): Promise +``` + +Resets the chain state to the initial state or a specified fork block. + + + Optional block number to reset to (when in fork mode) + + +## Mining Methods + +### mine() + +```typescript +async mine(blocks = 1): Promise +``` + +Mines a specified number of blocks. + + + Number of blocks to mine + + +```typescript +// Mine 10 blocks +await node.mine(10) +``` + +### setAutomine() + +```typescript +async setAutomine(enabled: boolean): Promise +``` + +Enables or disables automatic block mining. + + + Whether to enable automatic mining + + +```typescript +// Disable automine for manual control +await node.setAutomine(false) +``` + +## Time Manipulation Methods + +### setNextBlockTimestamp() + +```typescript +async setNextBlockTimestamp(timestamp: number): Promise +``` + +Sets the timestamp for the next block. + + + Unix timestamp in seconds + + +### increaseTime() + +```typescript +async increaseTime(seconds: number): Promise +``` + +Increases chain time by the specified number of seconds. + + + Number of seconds to advance + + +```typescript +// Advance time by 1 day +await node.increaseTime(86400) +``` + +### setTime() + +```typescript +async setTime(timestamp: number): Promise +``` + +Sets absolute chain time. + + + Unix timestamp in seconds + + +## Account Management Methods + +### getAccounts() + +```typescript +async getAccounts(): Promise +``` + +Returns an array of available account addresses. + +```typescript +const accounts = await node.getAccounts() +console.log(`Available accounts: ${accounts.length}`) +``` + +### setBalance() + +```typescript +async setBalance(address: string, balance: bigint): Promise +``` + +Sets the balance for the specified address. + + + The address to modify + + + + The new balance in wei + + +```typescript +await node.setBalance( + "0x742d35Cc6634C0532925a3b844Bc9e7595f8fA65", + parseEther("100") +) +``` + +### setNonce() + +```typescript +async setNonce(address: string, nonce: number): Promise +``` + +Sets the nonce for the specified address. + + + The address to modify + + + + The new nonce value + + +### impersonateAccount() + +```typescript +async impersonateAccount(address: string): Promise +``` + +Enables impersonation of the specified account. + + + The address to impersonate + + +```typescript +// Impersonate a whale address +await node.impersonateAccount("0xWhaleAddress") +``` + +### stopImpersonatingAccount() + +```typescript +async stopImpersonatingAccount(address: string): Promise +``` + +Disables impersonation of the specified account. + + + The address to stop impersonating + + +## Contract Methods + +### setCode() + +```typescript +async setCode(address: string, code: string): Promise +``` + +Sets the contract code at the specified address. + + + The contract address + + + + The contract bytecode (hex string) + + +### setStorageAt() + +```typescript +async setStorageAt( + address: string, + slot: string, + value: string +): Promise +``` + +Sets the storage value at the specified slot for a contract. + + + The contract address + + + + The storage slot (hex string) + + + + The value to store (hex string) + + +## Gas Configuration Methods + +### setNextBlockBaseFeePerGas() + +```typescript +async setNextBlockBaseFeePerGas(fee: bigint): Promise +``` + +Sets the base fee for the next block (EIP-1559). + + + The base fee in wei + + +### setMinGasPrice() + +```typescript +async setMinGasPrice(price: bigint): Promise +``` + +Sets the minimum gas price. + + + The minimum gas price in wei + + +## Network Methods + +### setChainId() + +```typescript +async setChainId(chainId: number): Promise +``` + +Sets the chain ID. + + + The new chain ID + + +## Next Steps + +- Learn about [configuration options](/onchaintestkit/node/configuration) \ No newline at end of file diff --git a/docs/onchaintestkit/node/configuration.mdx b/docs/onchaintestkit/node/configuration.mdx new file mode 100644 index 0000000..7225b94 --- /dev/null +++ b/docs/onchaintestkit/node/configuration.mdx @@ -0,0 +1,246 @@ +--- +title: "Node Configuration" +description: "Configuration options for LocalNodeManager" +--- + +## Configuration Overview + +The `LocalNodeManager` constructor accepts a `NodeConfig` object that allows you to customize the behavior of your local Anvil node. All configuration options are optional, with sensible defaults provided. + +## Basic Configuration + +```typescript +import { LocalNodeManager } from '@coinbase/onchaintestkit' + +const node = new LocalNodeManager({ + chainId: 84532, + port: 8545, + blockTime: 1, +}) +``` + +## NodeConfig Type + +```typescript +interface NodeConfig { + port?: number; + portRange?: [number, number]; + chainId?: number; + mnemonic?: string; + forkUrl?: string; + forkBlockNumber?: bigint; + forkRetryInterval?: number; + defaultBalance?: bigint; + totalAccounts?: number; + blockTime?: number; + blockGasLimit?: bigint; + noMining?: boolean; + hardfork?: "london" | "berlin" | "cancun"; +} +``` + +## Configuration Options + +### Network Settings + + + The chain ID for the network. Common values: + - `1` - Ethereum Mainnet + - `8453` - Base Mainnet + - `84532` - Base Sepolia + - `31337` - Default Hardhat/Anvil + + + + Specific hardfork to use. Options: `"london"`, `"berlin"`, `"cancun"` + + +### Port Configuration + + + Fixed port number for the RPC server. If not specified, a port will be dynamically allocated. + + + + Port range for automatic port selection when `port` is not specified. + + +### Fork Mode + + + URL to fork from (e.g., mainnet or testnet RPC endpoint). Enables fork mode. + + ```typescript + forkUrl: "https://mainnet.base.org" + ``` + + + + Specific block number to fork from. Must be used with `forkUrl`. + + ```typescript + forkBlockNumber: 18000000n + ``` + + + + Retry interval for fork requests in milliseconds. + + +### Account Configuration + + + Mnemonic phrase for generating test accounts. + + ```typescript + mnemonic: "test test test test test test test test test test test junk" + ``` + + + + Default balance for test accounts in wei. + + ```typescript + defaultBalance: parseEther("100") + ``` + + + + Number of test accounts to generate. + + +### Mining Configuration + + + Time between blocks in seconds. Set to `0` for instant mining (default). + + + + Disable automatic mining. Blocks must be manually mined with `node.mine()`. + + + + Gas limit per block. + + +## Configuration Examples + +### Basic Local Testing + +```typescript +const node = new LocalNodeManager({ + chainId: 31337, + defaultBalance: parseEther("1000"), + totalAccounts: 5, +}) +``` + +### Fork Mainnet + +```typescript +const node = new LocalNodeManager({ + chainId: 1, + forkUrl: process.env.ETH_MAINNET_RPC, + forkBlockNumber: 18500000n, + hardfork: "cancun", +}) +``` + +### Base Sepolia Fork + +```typescript +const node = new LocalNodeManager({ + chainId: baseSepolia.id, + forkUrl: process.env.BASE_SEPOLIA_RPC, + forkBlockNumber: BigInt(process.env.FORK_BLOCK || "0"), + mnemonic: process.env.TEST_MNEMONIC, +}) +``` + +### Manual Mining Mode + +```typescript +const node = new LocalNodeManager({ + noMining: true, + blockGasLimit: 30_000_000n, +}) + +await node.start() + +// Manually mine blocks when needed +await node.mine(1) +``` + +### Fixed Port Configuration + +```typescript +const node = new LocalNodeManager({ + port: 8545, // Always use port 8545 + chainId: 31337, +}) +``` + +### Custom Port Range + +```typescript +const node = new LocalNodeManager({ + portRange: [20000, 30000], // Use ports 20000-30000 + chainId: 31337, +}) +``` + +## Environment Variables + +It's recommended to use environment variables for sensitive or environment-specific configuration: + +```bash +# .env.test +E2E_TEST_FORK_URL=https://mainnet.base.org +E2E_TEST_FORK_BLOCK_NUMBER=18500000 +E2E_TEST_SEED_PHRASE="test test test test test test test test test test test junk" +``` + +```typescript +const node = new LocalNodeManager({ + chainId: baseSepolia.id, + forkUrl: process.env.E2E_TEST_FORK_URL, + forkBlockNumber: BigInt(process.env.E2E_TEST_FORK_BLOCK_NUMBER ?? "0"), + mnemonic: process.env.E2E_TEST_SEED_PHRASE, +}) +``` + +## Default Values + +When no configuration is provided, these defaults are used: + +- **chainId**: `84532` (Base Sepolia) +- **portRange**: `[10000, 20000]` +- **defaultBalance**: `10000 ETH` per account +- **totalAccounts**: `10` +- **blockTime**: `0` (instant mining) +- **noMining**: `false` + +## Best Practices + + + + Store RPC URLs, mnemonics, and API keys in environment variables + + + + Use fork mode to test against real mainnet/testnet state + + + + Let the system allocate ports automatically for parallel test execution + + + + Share configuration between tests using a common config file + + + +## Next Steps + +- Learn about [LocalNodeManager API](/onchaintestkit/node/api-reference) +- See [complete examples](https://github.com/coinbase/onchaintestkit/tree/master/example/frontend/e2e) \ No newline at end of file diff --git a/docs/onchaintestkit/node/overview.mdx b/docs/onchaintestkit/node/overview.mdx new file mode 100644 index 0000000..2b59a39 --- /dev/null +++ b/docs/onchaintestkit/node/overview.mdx @@ -0,0 +1,154 @@ +--- +title: "Node Testing Overview" +description: "Local blockchain node management for testing with OnchainTestKit" +--- + +## Overview + +The `LocalNodeManager` is a core utility of the [`@coinbase/onchaintestkit`](https://www.npmjs.com/package/@coinbase/onchaintestkit) package, providing robust, programmatic control over local Ethereum (Anvil) nodes for end-to-end blockchain application testing. It is designed for seamless integration with Playwright and supports advanced scenarios such as parallel test execution, dynamic port allocation, chain state manipulation, and account impersonation. + +### Why is this important? + +Testing blockchain applications often requires fine-grained control over the blockchain state, fast resets, and the ability to run multiple isolated nodes in parallel. `LocalNodeManager` abstracts away the complexity of managing Anvil nodes, enabling reliable, reproducible, and scalable test environments for dApps, smart contracts, and wallet integrations. + +## Architecture + + +```mermaid +flowchart TD + subgraph "Test Runner (Playwright)" + A[Test File 1] + B[Test File 2] + C[Test File N] + end + subgraph LocalNodeManager + D[Port Allocator] + E[Anvil Process Manager] + F[Chain State Controller] + G[RPC Provider] + end + subgraph System + H[Anvil Node 1] + I[Anvil Node 2] + J[Anvil Node N] + end + + A --> D + B --> D + C --> D + D --> E + E --> H + E --> I + E --> J + F --> G + G --> H + G --> I + G --> J +``` + + +## Quick Start + +```typescript +import { LocalNodeManager } from '@coinbase/onchaintestkit' +import { baseSepolia } from 'viem/chains' + +// Create a node manager with automatic port allocation +const node = new LocalNodeManager({ + chainId: baseSepolia.id, + forkUrl: process.env.E2E_TEST_FORK_URL, + forkBlockNumber: BigInt(process.env.E2E_TEST_FORK_BLOCK_NUMBER ?? "0"), + hardfork: "cancun", +}) + +// Start the node +await node.start() +console.log(`Node running on port ${node.getPort()}`) + +// Manipulate chain state +const snapshotId = await node.snapshot() +// ...run tests... +await node.revert(snapshotId) + +// Clean up +await node.stop() +``` + +## Key Features + + + + Automatically finds available ports for each node instance, enabling parallel test execution without conflicts. + + + + Take snapshots, revert to previous states, and manipulate blockchain time for comprehensive testing scenarios. + + + + Set balances, impersonate accounts, and manage nonces for precise test conditions. + + + + Fork from mainnet or testnet at specific blocks to test against real-world state. + + + + Built-in fixtures and utilities for seamless integration with Playwright tests. + + + +## Common Use Cases + +### Testing Smart Contracts + +```typescript +const node = new LocalNodeManager({ + chainId: 31337, + defaultBalance: parseEther("100"), +}) + +await node.start() + +// Deploy and test contracts +const provider = new ethers.JsonRpcProvider(node.rpcUrl) +// ... deploy contracts, run tests +``` + +### Testing onchain Interactions + +```typescript +// Fork mainnet for realistic testing +const node = new LocalNodeManager({ + chainId: 1, + forkUrl: "https://eth-mainnet.g.alchemy.com/v2/YOUR-API-KEY", + forkBlockNumber: 18000000n, +}) + +await node.start() + +// Test against real mainnet state +``` + +### Time-Dependent Testing + +```typescript +await node.start() + +// Test staking rewards after 30 days +await node.increaseTime(30 * 24 * 60 * 60) + +// Mine a block to apply the time change +await node.mine() +``` + +## Next Steps + + + + Learn about all configuration options for LocalNodeManager + + + Explore the complete API with all methods and properties + + \ No newline at end of file diff --git a/docs/onchaintestkit/overview.mdx b/docs/onchaintestkit/overview.mdx new file mode 100644 index 0000000..b5b1f27 --- /dev/null +++ b/docs/onchaintestkit/overview.mdx @@ -0,0 +1,107 @@ +--- +title: "OnchainTestKit Overview" +description: "Type-safe framework for end-to-end testing of blockchain applications" +--- + +## What is OnchainTestKit? + +`@coinbase/onchaintestkit` is a comprehensive, type-safe framework for end-to-end testing of blockchain applications. Built to complement OnchainKit, it provides seamless integration with Playwright for robust automation of browser-based wallet interactions, local blockchain node management, and common blockchain testing scenarios. + + + + Get up and running with OnchainTestKit in minutes + + + Learn how to configure wallets and networks + + + Write your first blockchain tests + + + Integrate these in CI/CD + + + + +**Important**: To effectively use OnchainTestKit, we strongly recommend studying: + +1. **[The main repository](https://github.com/coinbase/onchaintestkit)** - Explore the source code, understand the architecture, and see how components work together +2. **[The example tests](https://github.com/coinbase/onchaintestkit/tree/master/example/frontend/e2e)** - Real-world examples showing wallet connections, transactions, token swaps, and more + +These resources contain practical patterns and best practices that will accelerate your testing implementation. The examples demonstrate common scenarios you'll encounter when testing blockchain applications. + + +## Why OnchainTestKit? + +Modern blockchain applications require rigorous testing of wallet interactions, transaction flows, and network behavior. Manual testing is error-prone and slow, especially when dealing with complex wallet UIs and multiple networks. OnchainTestKit automates these processes, ensuring: + +- **Reliability**: Consistent test execution across environments +- **Reproducibility**: Deterministic test results +- **Scalability**: Parallel test execution support +- **Type Safety**: Full TypeScript support + +## Architecture + + +```mermaid +flowchart TD + subgraph "Test Runner" + A[Playwright Test] + B[Onchain Test Kit] + end + subgraph Blockchain + C["LocalNodeManager
(Anvil Node)"] + D["Wallet Extension
(MetaMask/Coinbase/Etc...)"] + end + A --> B + B -- manages --> D + B -- manages --> C + D -- interacts --> C + B -- configures --> C + B -- automates --> D +``` + + +## Key Features + + + + Automate browser-based wallet and dApp interactions with the power of Playwright's testing framework. + + + + Built-in support for MetaMask and Coinbase Wallet, with an extensible architecture for adding more wallets. + + + + Automate connect, transaction, signature, approval, and network switching flows with simple APIs. + + + + Use local Anvil nodes or remote RPC endpoints, with dynamic port allocation for parallel test execution. + + + + Full TypeScript support for all configuration and test APIs, catching errors at compile time. + + + + Builder pattern for intuitive wallet and node setup, making configuration readable and maintainable. + + + +## Next Steps + + + + Follow our [quickstart guide](/onchaintestkit/quickstart) to set up your testing environment + + + + Learn how to [configure wallets](/onchaintestkit/configuration) for your test scenarios + + + + Start [writing tests](/onchaintestkit/writing-tests) with our comprehensive guide + + \ No newline at end of file diff --git a/docs/onchaintestkit/quickstart.mdx b/docs/onchaintestkit/quickstart.mdx new file mode 100644 index 0000000..5b0528c --- /dev/null +++ b/docs/onchaintestkit/quickstart.mdx @@ -0,0 +1,180 @@ +--- +title: "Quickstart" +description: "Get up and running with OnchainTestKit in under 10 minutes" +--- + +This quickstart guide will help you set up OnchainTestKit and write your first blockchain test in minutes. We'll create a simple test that connects a wallet and performs a transaction. + +## Prerequisites + +Before you begin, ensure you have: + +- Node.js ≥ 14 +- npm, yarn, or bun (this guide uses yarn) +- [Foundry](https://book.getfoundry.sh/) installed for Anvil local node + + +Need to install Foundry? Run: +```bash +curl -L https://foundry.paradigm.xyz | bash +foundryup +``` + + +## Quick Setup + + + +Install OnchainTestKit and Playwright: + +```bash +yarn add -D @coinbase/onchaintestkit @playwright/test +yarn playwright install --with-deps +``` + + + +Prepare the wallet extensions for testing: + +```bash +# Download MetaMask +yarn prepare-metamask + +# Download Coinbase Wallet +yarn prepare-coinbase +``` + + + +Create a `.env` file in your project root: + +```bash .env +# Test wallet seed phrase (NEVER use production wallets!) +E2E_TEST_SEED_PHRASE="test test test test test test test test test test test junk" +``` + + +This is a test seed phrase. Never use your real wallet seed phrase in tests! + + + + +Create `e2e/connectWallet.spec.ts`: + +```typescript e2e/connectWallet.spec.ts +import { createOnchainTest } from "@coinbase/onchaintestkit" +import { configure } from "@coinbase/onchaintestkit" +import { baseSepolia } from "viem/chains" + +// Configure MetaMask +const config = configure() + .withLocalNode({ + chainId: baseSepolia.id, + forkUrl: process.env.E2E_TEST_FORK_URL, // This can be mainnet or testnet sepolia url + forkBlockNumber: BigInt(process.env.E2E_TEST_FORK_BLOCK_NUMBER ?? "0"), + hardfork: "cancun", + }) + .withMetaMask() + .withSeedPhrase({ + seedPhrase: DEFAULT_SEED_PHRASE ?? "", + password: DEFAULT_PASSWORD, + }) + // Add the network with the actual port in a custom setup + .withNetwork({ + name: "Base Sepolia", + chainId: baseSepolia.id, + symbol: "ETH", + // placeholder for the actual rpcUrl, which is auto injected by the node fixture + rpcUrl: "http://localhost:8545", + }) + .build() + +// Create test with configuration +const test = createOnchainTest(config) + +test.describe("Wallet Connection", () => { + test("should connect to MetaMask", async ({ page, metamask }) => { + if (!metamask) throw new Error("MetaMask not initialized") + + await page.getByTestId('ockConnectButton').first().click(); + console.log('[connectWallet] Wallet connect modal opened'); + + // Select MetaMask from wallet options + await page + .getByTestId('ockModalOverlay') + .first() + .getByRole('button', { name: 'MetaMask' }) + .click(); + console.log('[connectWallet] MetaMask option clicked'); + + // Handle MetaMask connection request + await metamask.handleAction(BaseActionType.CONNECT_TO_DAPP); + console.log('[connectWallet] MetaMask handleAction finished, URL after connect:', page.url()); + }) +}) +``` + + + +Execute your test: + +```bash +yarn playwright test e2e/connectWallet.spec.ts +``` + +To see the browser in action: + +```bash +yarn playwright test e2e/connectWallet.spec.ts --headed +``` + + + + +Want to see more real-world examples? Check out the **[comprehensive test examples](https://github.com/coinbase/onchaintestkit/tree/master/example/frontend/e2e)** in the OnchainTestKit repository. These examples cover: +- Token swaps +- NFT minting +- Multiple wallet interactions +- Complex transaction flows +- And much more! + + +## What's Next? + + + + Detailed installation instructions and troubleshooting + + + Learn about advanced configuration options + + + Deep dive into writing comprehensive tests + + + Test smart contract interactions + + + +## Common Issues + + + +Make sure you've run the prepare commands: +```bash +yarn prepare-metamask +yarn prepare-coinbase +``` + + + +Increase the test timeout in your config: +```typescript +test.setTimeout(60000) // 60 seconds +``` + + + +The local node might be using a port that's already taken. OnchainTestKit automatically handles port allocation for parallel tests. + + \ No newline at end of file diff --git a/docs/onchaintestkit/smart-contracts.mdx b/docs/onchaintestkit/smart-contracts.mdx new file mode 100644 index 0000000..318ee89 --- /dev/null +++ b/docs/onchaintestkit/smart-contracts.mdx @@ -0,0 +1,600 @@ +--- +title: "Smart Contract Testing" +description: "Test smart contract deployments and interactions with OnchainTestKit" +--- + +OnchainTestKit provides powerful tools for testing smart contract interactions alongside your dApp UI. This guide covers setting up contract testing, deploying contracts deterministically, and testing complex contract scenarios. + +## Smart Contract Setup + +### Prerequisites + + + +Foundry is required for compiling contracts: + +```bash +curl -L https://foundry.paradigm.xyz | bash +foundryup +``` + + + +Set up your smart contracts project: + +```bash +mkdir smart-contracts && cd smart-contracts +forge init +``` + + + +Add to your `.env` file: + +```bash +# Path to your smart contracts +E2E_CONTRACT_PROJECT_ROOT=../smart-contracts +``` + + + +### Project Structure + +``` +your-project/ +├── frontend/ # Your dApp +│ └── e2e/ # E2E tests +└── smart-contracts/ # Foundry project + ├── foundry.toml + ├── src/ # Contract source files + ├── test/ # Contract unit tests + └── script/ # Deployment scripts +``` + +### Foundry Configuration + +Create `smart-contracts/foundry.toml`: + +```toml +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +solc = "0.8.20" +optimizer = true +optimizer_runs = 200 + +[rpc_endpoints] +sepolia = "${SEPOLIA_RPC_URL}" +mainnet = "${MAINNET_RPC_URL}" +``` + +## Writing Smart Contracts + +### Example Token Contract + +```solidity +// smart-contracts/src/SimpleToken.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract SimpleToken is ERC20, Ownable { + uint256 public constant MAX_SUPPLY = 1000000 * 10**18; + + constructor() ERC20("Simple Token", "STK") Ownable(msg.sender) { + _mint(msg.sender, MAX_SUPPLY / 10); + } + + function mint(address to, uint256 amount) public onlyOwner { + require(totalSupply() + amount <= MAX_SUPPLY, "Exceeds max supply"); + _mint(to, amount); + } +} +``` + +### Building Contracts + +```bash +cd smart-contracts + +# Install dependencies +forge install foundry-rs/forge-std OpenZeppelin/openzeppelin-contracts + +# Build contracts +forge build +``` + +## Using SmartContractManager + +The `smartContractManager` fixture is automatically available when `E2E_CONTRACT_PROJECT_ROOT` is set: + +```typescript +test("deploy and test contract", async ({ + page, + metamask, + smartContractManager, + node +}) => { + // Contract deployment and testing +}) +``` + +## Contract Deployment + +### Deterministic Deployment with CREATE2 + +OnchainTestKit uses CREATE2 for deterministic contract addresses: + +```typescript +test("should deploy SimpleToken contract using CREATE2", async ({ + page, + smartContractManager, + node, +}) => { + if (!smartContractManager || !node) { + throw new Error("SmartContractManager or node not initialized") + } + + // Deploy the SimpleToken contract + const salt = + "0x0000000000000000000000000000000000000000000000000000000000000001" as Hex + const deployer = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" as Address // Anvil's first account + + const tokenAddress = await smartContractManager.deployContract({ + name: "SimpleToken", + args: [], + salt, + deployer, + }) + + // Verify the contract was deployed + expect(tokenAddress).toBeDefined() + expect(tokenAddress).toMatch(/^0x[a-fA-F0-9]{40}$/) + + // Create a public client to verify the deployment + const publicClient = createPublicClient({ + chain: localhost, + transport: http(`http://localhost:${node.port}`), + }) + + // Check the contract code exists + const code = await publicClient.getBytecode({ address: tokenAddress }) + expect(code).toBeDefined() + expect(code).not.toBe("0x") + + console.log(`SimpleToken deployed at: ${tokenAddress}`) +}) +``` + + +CREATE2 ensures the same contract address across test runs when using the same salt and deployer. + + +### Verifying Deterministic Addresses + +```typescript +test("should deploy at deterministic address with same salt", async ({ + page, + smartContractManager, + node, +}) => { + if (!smartContractManager || !node) { + throw new Error("SmartContractManager or node not initialized") + } + + const salt = + "0x0000000000000000000000000000000000000000000000000000000000000002" as Hex + const deployer = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" as Address + + // Take a snapshot before deployment + const snapshotId = await node.snapshot() + + // Deploy first time + const firstAddress = await smartContractManager.deployContract({ + name: "SimpleToken", + args: [], + salt, + deployer, + }) + + // Revert to snapshot to simulate a fresh chain + await node.revert(snapshotId) + + // Deploy again with same salt + const secondAddress = await smartContractManager.deployContract({ + name: "SimpleToken", + args: [], + salt, + deployer, + }) + + // Addresses should be the same due to CREATE2 + expect(firstAddress).toBe(secondAddress) +}) +``` + +## Testing Contract Interactions + +### Reading Contract State + +```typescript +test("should interact with deployed contract", async ({ + page, + smartContractManager, + node, +}) => { + if (!smartContractManager || !node) { + throw new Error("SmartContractManager or node not initialized") + } + + // Deploy the contract + const salt = + "0x0000000000000000000000000000000000000000000000000000000000000003" as Hex + const deployer = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" as Address + + const tokenAddress = await smartContractManager.deployContract({ + name: "SimpleToken", + args: [], + salt, + deployer, + }) + + // Create a public client to interact with the contract + const publicClient = createPublicClient({ + chain: localhost, + transport: http(`http://localhost:${node.port}`), + }) + + // Check the owner of the contract + const owner = await publicClient.readContract({ + address: tokenAddress, + abi: [ + { + name: "owner", + type: "function", + stateMutability: "view", + inputs: [], + outputs: [{ name: "", type: "address" }], + }, + ], + functionName: "owner", + }) as Address + + console.log(`Contract owner: ${owner}`) + + // Check the owner's balance (should have 10% of max supply from constructor) + const ownerBalance = await publicClient.readContract({ + address: tokenAddress, + abi: [ + { + name: "balanceOf", + type: "function", + stateMutability: "view", + inputs: [{ name: "account", type: "address" }], + outputs: [{ name: "", type: "uint256" }], + }, + ], + functionName: "balanceOf", + args: [owner], + }) + + expect(ownerBalance).toBe(BigInt("100000000000000000000000")) // 100k tokens (10% of 1M) + + // Verify contract metadata + const [name, symbol, totalSupply] = await Promise.all([ + publicClient.readContract({ + address: tokenAddress, + abi: [ + { + name: "name", + type: "function", + stateMutability: "view", + inputs: [], + outputs: [{ name: "", type: "string" }], + }, + ], + functionName: "name", + }), + publicClient.readContract({ + address: tokenAddress, + abi: [ + { + name: "symbol", + type: "function", + stateMutability: "view", + inputs: [], + outputs: [{ name: "", type: "string" }], + }, + ], + functionName: "symbol", + }), + publicClient.readContract({ + address: tokenAddress, + abi: [ + { + name: "totalSupply", + type: "function", + stateMutability: "view", + inputs: [], + outputs: [{ name: "", type: "uint256" }], + }, + ], + functionName: "totalSupply", + }), + ]) + + expect(name).toBe("Simple Token") + expect(symbol).toBe("STK") + expect(totalSupply).toBe(BigInt("100000000000000000000000")) // 100k tokens initially minted +}) +``` + +### UI Contract Interactions + +```typescript +test("should connect wallet and interact with deployed contract", async ({ + page, + metamask, + smartContractManager, + node, +}) => { + if (!metamask) { + throw new Error("MetaMask is not defined") + } + + // Deploy contract first + const salt = + "0x0000000000000000000000000000000000000000000000000000000000000006" as Hex + const deployer = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" as Address + + const tokenAddress = await smartContractManager.deployContract({ + name: "SimpleToken", + args: [], + salt, + deployer, + }) + + // Connect wallet to the app + await page.getByTestId("ockConnectButton").first().click() + await page + .getByTestId("ockModalOverlay") + .first() + .getByRole("button", { name: "MetaMask" }) + .click() + + // Handle MetaMask connection + await metamask.handleAction(BaseActionType.CONNECT_TO_DAPP) + + // Verify wallet is connected + await page.waitForSelector("text=/0x[a-fA-F0-9]{4}.*[a-fA-F0-9]{4}/", { + timeout: 10000, + }) + + // Now the user could interact with the deployed contract through the UI + console.log(`User can now interact with token at: ${tokenAddress}`) +}) +``` + +## Batch Operations + +Deploy multiple contracts and set up complex state: + +```typescript +test("should perform batch operations", async ({ + page, + smartContractManager, + node, +}) => { + if (!smartContractManager || !node) { + throw new Error("SmartContractManager or node not initialized") + } + + const deployer = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" as Address + + // Deploy multiple contracts and execute multiple calls + await smartContractManager.setContractState( + { + deployments: [ + { + name: "SimpleToken", + args: [], + salt: "0x0000000000000000000000000000000000000000000000000000000000000004" as Hex, + deployer, + }, + ], + calls: [], + }, + node, + ) + + // Get the deployed contract address + const publicClient = createPublicClient({ + chain: localhost, + transport: http(`http://localhost:${node.port}`), + }) + + // Verify deployment by checking for events or code + const logs = await publicClient.getLogs({ + fromBlock: "latest", + toBlock: "latest", + }) + + expect(logs.length).toBeGreaterThan(0) +}) +``` + +## Testing State Persistence + +Test contract state persistence across snapshots: + +```typescript +test("should test contract state persistence across snapshots", async ({ + page, + smartContractManager, + node, +}) => { + if (!smartContractManager || !node) { + throw new Error("SmartContractManager or node not initialized") + } + + // Deploy contract + const salt = + "0x0000000000000000000000000000000000000000000000000000000000000007" as Hex + const deployer = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" as Address + + const tokenAddress = await smartContractManager.deployContract({ + name: "SimpleToken", + args: [], + salt, + deployer, + }) + + // Create public client + const publicClient = createPublicClient({ + chain: localhost, + transport: http(`http://localhost:${node.port}`), + }) + + const owner = await publicClient.readContract({ + address: tokenAddress, + abi: [ + { + name: "owner", + type: "function", + stateMutability: "view", + inputs: [], + outputs: [{ name: "", type: "address" }], + }, + ], + functionName: "owner", + }) as Address + + // Take a snapshot + const snapshotId = await node.snapshot() + + // Check initial state + const initialOwnerBalance = await publicClient.readContract({ + address: tokenAddress, + abi: [ + { + name: "balanceOf", + type: "function", + stateMutability: "view", + inputs: [{ name: "account", type: "address" }], + outputs: [{ name: "", type: "uint256" }], + }, + ], + functionName: "balanceOf", + args: [owner], + }) + + expect(initialOwnerBalance).toBe(BigInt("100000000000000000000000")) // 100k tokens + + // Deploy another contract to change state + const salt2 = + "0x0000000000000000000000000000000000000000000000000000000000000008" as Hex + const secondTokenAddress = await smartContractManager.deployContract({ + name: "SimpleToken", + args: [], + salt: salt2, + deployer, + }) + + // Verify the second contract was deployed + const secondContractCode = await publicClient.getBytecode({ + address: secondTokenAddress, + }) + expect(secondContractCode).toBeDefined() + expect(secondContractCode).not.toBe("0x") + + // Revert to snapshot + await node.revert(snapshotId) + + // Check that the second contract no longer exists + const codeAfterRevert = await publicClient.getBytecode({ + address: secondTokenAddress, + }) + expect(codeAfterRevert).toBeUndefined() + + // Verify the first contract still exists and has the same state + const firstContractCodeAfterRevert = await publicClient.getBytecode({ + address: tokenAddress, + }) + expect(firstContractCodeAfterRevert).toBeDefined() + expect(firstContractCodeAfterRevert).not.toBe("0x") + + const ownerBalanceAfterRevert = await publicClient.readContract({ + address: tokenAddress, + abi: [ + { + name: "balanceOf", + type: "function", + stateMutability: "view", + inputs: [{ name: "account", type: "address" }], + outputs: [{ name: "", type: "uint256" }], + }, + ], + functionName: "balanceOf", + args: [owner], + }) + + expect(ownerBalanceAfterRevert).toBe(initialOwnerBalance) +}) +``` + +## Best Practices + + + +Always use CREATE2 with consistent salts for predictable contract addresses: + +```typescript +const SALTS = { + TOKEN: "0x01" as Hex, + STAKING: "0x02" as Hex, + GOVERNANCE: "0x03" as Hex, +} +``` + + + +Deploy fresh contracts for each test to ensure isolation: + +```typescript +test.beforeEach(async ({ smartContractManager, node }) => { + // Fresh deployment for each test + await smartContractManager.deployContract({ + name: "SimpleToken", + args: [], + salt: `0x${Date.now().toString(16)}` as Hex, + deployer: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" as Address, + }) +}) +``` + + + +Always test both successful and failing contract interactions: + +```typescript +test.describe("Token minting", () => { + test("owner can mint", async ({ /* ... */ }) => { + // Test successful mint + }) + + test("non-owner cannot mint", async ({ /* ... */ }) => { + // Test revert + }) +}) +``` + + + +## Next Steps + +- [Browse complete examples](https://github.com/coinbase/onchaintestkit/tree/master/example/frontend/e2e) +- [Set up CI/CD](/onchaintestkit/ci-cd) +- [Learn best practices](/onchaintestkit/best-practices) \ No newline at end of file diff --git a/docs/onchaintestkit/wallets/api-reference.mdx b/docs/onchaintestkit/wallets/api-reference.mdx new file mode 100644 index 0000000..b9a7dab --- /dev/null +++ b/docs/onchaintestkit/wallets/api-reference.mdx @@ -0,0 +1,394 @@ +--- +title: "Wallet API Reference" +description: "Complete API reference for wallet testing with OnchainTestKit" +--- + +## Classes + +### BaseWallet + +Abstract base class for wallet implementations. + +```typescript +abstract class BaseWallet { + constructor(page: Page, context: BrowserContext); + + abstract handleAction(action: string, options?: ActionOptions): Promise; + abstract identifyNotificationType(): Promise; +} +``` + +### MetaMask + +MetaMask wallet automation class. + +```typescript +class MetaMask extends BaseWallet { + static async initialize(): Promise<{ page: Page; context: BrowserContext }>; + static async createContext(): Promise; + + async handleAction( + action: BaseActionType | MetaMaskSpecificActionType, + options?: ActionOptions + ): Promise; + + async identifyNotificationType(): Promise; +} +``` + +### CoinbaseWallet + +Coinbase Wallet automation class. + +```typescript +class CoinbaseWallet extends BaseWallet { + static async initialize(): Promise<{ page: Page; context: BrowserContext }>; + static async createContext(): Promise; + + async handleAction( + action: BaseActionType | CoinbaseSpecificActionType, + options?: ActionOptions + ): Promise; + + async identifyNotificationType(): Promise; + + async handlePasskeyPopup(options: PasskeyOptions): Promise; +} +``` + +### PasskeyAuthenticator + +WebAuthn virtual authenticator for Coinbase Wallet. + +```typescript +class PasskeyAuthenticator { + constructor(page: Page); + + async setup(options: PasskeySetupOptions): Promise; + async createCredential(options: CredentialCreationOptions): Promise; + async getCredential(options: CredentialRequestOptions): Promise; + async cleanup(): Promise; +} +``` + +## Enums + +### BaseActionType + +Common actions supported by all wallets. + +```typescript +enum BaseActionType { + IMPORT_WALLET_FROM_SEED = "importWalletFromSeed", + IMPORT_WALLET_FROM_PRIVATE_KEY = "importWalletFromPrivateKey", + SWITCH_NETWORK = "switchNetwork", + CONNECT_TO_DAPP = "connectToDapp", + HANDLE_TRANSACTION = "handleTransaction", + HANDLE_SIGNATURE = "handleSignature", + CHANGE_SPENDING_CAP = "changeSpendingCap", + REMOVE_SPENDING_CAP = "removeSpendingCap", +} +``` + +### MetaMaskSpecificActionType + +MetaMask-specific actions. + +```typescript +enum MetaMaskSpecificActionType { + LOCK = "lock", // Not yet implemented + UNLOCK = "unlock", // Not yet implemented + ADD_TOKEN = "addToken", + ADD_ACCOUNT = "addAccount", + RENAME_ACCOUNT = "renameAccount", // Not yet implemented + REMOVE_ACCOUNT = "removeAccount", + SWITCH_ACCOUNT = "switchAccount", + ADD_NETWORK = "addNetwork", + APPROVE_ADD_NETWORK = "approveAddNetwork", +} +``` + +### CoinbaseSpecificActionType + +Coinbase Wallet-specific actions. + +```typescript +enum CoinbaseSpecificActionType { + LOCK = "lock", + UNLOCK = "unlock", + ADD_TOKEN = "addToken", + ADD_ACCOUNT = "addAccount", + SWITCH_ACCOUNT = "switchAccount", + ADD_NETWORK = "addNetwork", + SEND_TOKENS = "sendTokens", // Not yet implemented + HANDLE_PASSKEY_POPUP = "handlePasskeyPopup", +} +``` + +### NotificationPageType + +Types of wallet notifications. + +```typescript +enum NotificationPageType { + SpendingCap = "spending-cap", + Signature = "signature", + Transaction = "transaction", + RemoveSpendCap = "remove-spend-cap", +} +``` + +### ActionApprovalType + +Approval types for wallet actions. + +```typescript +enum ActionApprovalType { + APPROVE = "approve", + REJECT = "reject", +} +``` + +## Types + +### ActionOptions + +Options for wallet actions. + +```typescript +interface ActionOptions { + // Common options + approvalType?: ActionApprovalType; + + // Network switching + networkName?: string; + isTestnet?: boolean; + + // Wallet import + seedPhrase?: string; + password?: string; + privateKey?: string; + + // Account management + accountName?: string; + + // Token management + tokenAddress?: string; + tokenSymbol?: string; + tokenDecimals?: number; + + // Network management + network?: NetworkConfig; + + // Passkey options (Coinbase only) + mainPage?: Page; + popup?: Page; + passkeyAction?: 'register' | 'approve'; + passkeyConfig?: PasskeyConfig; +} +``` + +### NetworkConfig + +Network configuration object. + +```typescript +interface NetworkConfig { + name: string; + rpcUrl: string; + chainId: number; + symbol: string; + isTestnet?: boolean; + blockExplorerUrl?: string; +} +``` + +### WalletSetupContext + +Context provided during wallet setup. + +```typescript +interface WalletSetupContext { + localNodePort?: number; +} +``` + +### PasskeyConfig + +Configuration for passkey authentication (Coinbase only). + +```typescript +interface PasskeyConfig { + name: string; + rpId: string; + rpName: string; + userId: string; + isUserVerified?: boolean; +} +``` + +### MetaMaskConfig + +MetaMask wallet configuration. + +```typescript +interface MetaMaskConfig { + type: 'metamask'; + seedPhrase?: { + phrase: string; + password: string; + }; + network?: NetworkConfig; + customSetup?: (wallet: MetaMask, context: WalletSetupContext) => Promise; +} +``` + +### CoinbaseConfig + +Coinbase Wallet configuration. + +```typescript +interface CoinbaseConfig { + type: 'coinbase'; + seedPhrase?: { + phrase: string; + password: string; + }; + network?: NetworkConfig; + customSetup?: (wallet: CoinbaseWallet, context: WalletSetupContext) => Promise; +} +``` + +## Configuration Builder + +### configure() + +Creates a configuration builder. + +```typescript +function configure(): ConfigBuilder; +``` + +### ConfigBuilder Methods + +```typescript +interface ConfigBuilder { + withMetaMask(): ConfigBuilder; + withCoinbase(): ConfigBuilder; + withSeedPhrase(config: SeedPhraseConfig): ConfigBuilder; + withNetwork(network: NetworkConfig): ConfigBuilder; + withLocalNode(config: LocalNodeConfig): ConfigBuilder; + withCustomSetup(setup: CustomSetupFunction): ConfigBuilder; + build(): OnchainTestConfig; +} +``` + +## Test Creation + +### createOnchainTest() + +Creates a test function with wallet fixtures. + +```typescript +function createOnchainTest(config: OnchainTestConfig): TestFunction; +``` + +### Test Fixtures + +```typescript +interface TestFixtures { + page: Page; // Playwright page + metamask?: MetaMask; // MetaMask instance (if configured) + coinbase?: CoinbaseWallet; // Coinbase instance (if configured) + node?: LocalNodeManager; // Local node manager (if configured) + smartContractManager?: SmartContractManager; // Smart contract manager +} +``` + +## Usage Examples + +### Basic Wallet Action + +```typescript +await wallet.handleAction(BaseActionType.CONNECT_TO_DAPP); +``` + +### Action with Options + +```typescript +await wallet.handleAction(BaseActionType.HANDLE_TRANSACTION, { + approvalType: ActionApprovalType.APPROVE +}); +``` + +### Network Switching + +```typescript +await wallet.handleAction(BaseActionType.SWITCH_NETWORK, { + networkName: 'Base Sepolia', + isTestnet: true +}); +``` + +### Token Addition (MetaMask) + +```typescript +await metamask.handleAction(MetaMaskSpecificActionType.ADD_TOKEN, { + tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + tokenSymbol: 'USDC', + tokenDecimals: 6 +}); +``` + +### Passkey Handling (Coinbase) + +```typescript +await coinbase.handleAction(CoinbaseSpecificActionType.HANDLE_PASSKEY_POPUP, { + mainPage: page, + popup: popup, + passkeyAction: 'register', + passkeyConfig: { + name: 'Test Passkey', + rpId: 'localhost', + rpName: 'My dApp', + userId: 'user123', + isUserVerified: true + } +}); +``` + +## Error Handling + +All wallet methods may throw errors. Common error scenarios: + +- Wallet extension not found +- Network not available +- Account not found +- Transaction rejected +- Timeout exceeded + +```typescript +try { + await wallet.handleAction(BaseActionType.CONNECT_TO_DAPP); +} catch (error) { + if (error.message.includes('timeout')) { + // Handle timeout + } else if (error.message.includes('rejected')) { + // Handle rejection + } +} +``` + +## Best Practices + +1. **Always check fixture existence**: Verify wallet fixtures are available before use +2. **Use appropriate action types**: Use base actions for common operations, specific actions for wallet-unique features +3. **Handle errors gracefully**: Wrap wallet actions in try-catch blocks +4. **Wait for UI updates**: Allow time for UI to update after wallet actions +5. **Use notification detection**: Use `identifyNotificationType()` for dynamic flows + +## See Also + +- [Common Wallet Actions](/onchaintestkit/wallets/common-actions) +- [MetaMask Features](/onchaintestkit/wallets/metamask) +- [Coinbase Wallet Features](/onchaintestkit/wallets/coinbase) \ No newline at end of file diff --git a/docs/onchaintestkit/wallets/coinbase.mdx b/docs/onchaintestkit/wallets/coinbase.mdx new file mode 100644 index 0000000..2d1d0c1 --- /dev/null +++ b/docs/onchaintestkit/wallets/coinbase.mdx @@ -0,0 +1,373 @@ +--- +title: "Coinbase Wallet Testing" +description: "Coinbase Wallet-specific features including passkey authentication support" +--- + +Coinbase Wallet provides unique features like passkey/WebAuthn support alongside standard wallet functionality. OnchainTestKit offers comprehensive automation for all Coinbase Wallet features. + +## Coinbase Wallet-Specific Actions + +In addition to the [common wallet actions](/onchaintestkit/wallets/common-actions), Coinbase Wallet supports these specific actions: + +```typescript +enum CoinbaseSpecificActionType { + LOCK = "lock", // Lock the wallet + UNLOCK = "unlock", // Unlock the wallet + ADD_TOKEN = "addToken", // Add a custom token + ADD_ACCOUNT = "addAccount", // Create a new account + SWITCH_ACCOUNT = "switchAccount", // Switch between accounts + ADD_NETWORK = "addNetwork", // Add a custom network + SEND_TOKENS = "sendTokens", // Send tokens (not yet implemented) + HANDLE_PASSKEY_POPUP = "handlePasskeyPopup", // Handle WebAuthn/Passkey authentication +} +``` + +## Passkey/WebAuthn Support + +One of Coinbase Wallet's unique features is support for passkey authentication, providing enhanced security through WebAuthn. + +### Passkey Configuration + +```typescript +interface PasskeyConfig { + name: string; // Display name for the passkey + rpId: string; // Relying Party ID (domain) + rpName: string; // Relying Party Name + userId: string; // User identifier + isUserVerified?: boolean; // Whether user verification is required +} +``` + +### Passkey Registration + +Register a new passkey for wallet authentication: + +```typescript +test('register passkey', async ({ page, coinbase }) => { + // Trigger passkey registration + const [popup] = await Promise.all([ + page.context().waitForEvent('page'), + page.getByTestId('register-passkey').click(), + ]); + + // Handle passkey registration + await coinbase.handleAction(CoinbaseSpecificActionType.HANDLE_PASSKEY_POPUP, { + mainPage: page, + popup: popup, + passkeyAction: 'register', + passkeyConfig: { + name: 'My Test Passkey', + rpId: 'localhost', + rpName: 'My dApp', + userId: 'test-user-123', + isUserVerified: true, + }, + }); +}); +``` + +### Passkey Authentication + +Use passkey for transaction approval: + +```typescript +test('approve transaction with passkey', async ({ page, coinbase }) => { + // Trigger transaction + const [txPopup] = await Promise.all([ + page.context().waitForEvent('page'), + page.getByRole('button', { name: 'Send Transaction' }).click(), + ]); + + // Approve with passkey + await coinbase.handleAction(CoinbaseSpecificActionType.HANDLE_PASSKEY_POPUP, { + mainPage: page, + popup: txPopup, + passkeyAction: 'approve', + }); +}); +``` + +### Complete Passkey Flow Example + +```typescript +test('complete passkey flow', async ({ page, coinbase }) => { + if (!coinbase) throw new Error('Coinbase fixture is required'); + + // Connect wallet + await page.getByTestId('connect-wallet').click(); + await page.getByRole('button', { name: 'Coinbase' }).click(); + await coinbase.handleAction(BaseActionType.CONNECT_TO_DAPP); + + // Register passkey + const [registerPopup] = await Promise.all([ + page.context().waitForEvent('page'), + page.getByTestId('setup-passkey').click(), + ]); + + await coinbase.handleAction(CoinbaseSpecificActionType.HANDLE_PASSKEY_POPUP, { + mainPage: page, + popup: registerPopup, + passkeyAction: 'register', + passkeyConfig: { + name: 'Test Passkey', + rpId: 'localhost', + rpName: 'Test dApp', + userId: 'user-' + Date.now(), + isUserVerified: true, + }, + }); + + // Use passkey for transaction + await page.getByRole('button', { name: 'Send ETH' }).click(); + + const [txPopup] = await Promise.all([ + page.context().waitForEvent('page'), + page.getByRole('button', { name: 'Confirm' }).click(), + ]); + + await coinbase.handleAction(CoinbaseSpecificActionType.HANDLE_PASSKEY_POPUP, { + mainPage: page, + popup: txPopup, + passkeyAction: 'approve', + }); + + // Verify transaction success + await expect(page.getByText('Transaction successful')).toBeVisible(); +}); +``` + +## Account Management + +### Add New Account + +```typescript +await coinbase.handleAction(CoinbaseSpecificActionType.ADD_ACCOUNT, { + accountName: 'DeFi Account', +}); +``` + +### Switch Between Accounts + +```typescript +await coinbase.handleAction(CoinbaseSpecificActionType.SWITCH_ACCOUNT, { + accountName: 'DeFi Account', +}); +``` + +### Account Management Example + +```typescript +test('manage Coinbase accounts', async ({ page, coinbase }) => { + // Add multiple accounts + await coinbase.handleAction(CoinbaseSpecificActionType.ADD_ACCOUNT, { + accountName: 'Trading Account', + }); + + await coinbase.handleAction(CoinbaseSpecificActionType.ADD_ACCOUNT, { + accountName: 'Savings Account', + }); + + // Switch to specific account + await coinbase.handleAction(CoinbaseSpecificActionType.SWITCH_ACCOUNT, { + accountName: 'Trading Account', + }); +}); +``` + +## Network Management + +### Add Custom Network + +```typescript +await coinbase.handleAction(CoinbaseSpecificActionType.ADD_NETWORK, { + network: { + name: 'Base Mainnet', + rpcUrl: 'https://mainnet.base.org', + chainId: 8453, + symbol: 'ETH', + }, +}); +``` + +## Wallet Lock/Unlock + +### Lock Wallet + +```typescript +await coinbase.handleAction(CoinbaseSpecificActionType.LOCK); +``` + +### Unlock Wallet + +```typescript +await coinbase.handleAction(CoinbaseSpecificActionType.UNLOCK, { + password: 'YourWalletPassword', +}); +``` + +## Coinbase Wallet Configuration + +### Basic Configuration + +```typescript +import { configure } from '@coinbase/onchaintestkit'; + +const coinbaseConfig = configure() + .withCoinbase() + .withSeedPhrase({ + seedPhrase: process.env.E2E_TEST_SEED_PHRASE!, + password: 'COMPLEXPASSWORD1', + }) + .build(); +``` + +### Advanced Configuration + +```typescript +const coinbaseConfig = configure() + .withCoinbase() + .withSeedPhrase({ + seedPhrase: process.env.E2E_TEST_SEED_PHRASE!, + password: 'COMPLEXPASSWORD1', + }) + .withNetwork({ + name: 'Base Sepolia', + chainId: 84532, + symbol: 'ETH', + rpcUrl: 'https://sepolia.base.org', + }) + .withCustomSetup(async (wallet, context) => { + // Add custom tokens + await wallet.handleAction(CoinbaseSpecificActionType.ADD_TOKEN, { + tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + tokenSymbol: 'USDC', + tokenDecimals: 6, + }); + + // Add additional accounts + await wallet.handleAction(CoinbaseSpecificActionType.ADD_ACCOUNT, { + accountName: 'DeFi Operations', + }); + }) + .build(); +``` + +## Complete Coinbase Wallet Test Example + +Here's a comprehensive example demonstrating various Coinbase Wallet features: + +```typescript +import { createOnchainTest, CoinbaseSpecificActionType, BaseActionType } from '@coinbase/onchaintestkit'; +import { configure } from '@coinbase/onchaintestkit'; + +const config = configure() + .withLocalNode({ + chainId: 84532, + forkUrl: process.env.E2E_TEST_FORK_URL, + }) + .withCoinbase() + .withSeedPhrase({ + seedPhrase: process.env.E2E_TEST_SEED_PHRASE!, + password: 'COMPLEXPASSWORD1', + }) + .build(); + +const test = createOnchainTest(config); + +test('comprehensive Coinbase test', async ({ page, coinbase, node }) => { + if (!coinbase) throw new Error('Coinbase fixture is required'); + + // Navigate to dApp + await page.goto('https://app.example.com'); + + // Connect wallet + await page.getByTestId('connect-wallet').click(); + await page.getByRole('button', { name: 'Coinbase' }).click(); + await coinbase.handleAction(BaseActionType.CONNECT_TO_DAPP); + + // Setup passkey + const [passkeyPopup] = await Promise.all([ + page.context().waitForEvent('page'), + page.getByTestId('enable-passkey').click(), + ]); + + await coinbase.handleAction(CoinbaseSpecificActionType.HANDLE_PASSKEY_POPUP, { + mainPage: page, + popup: passkeyPopup, + passkeyAction: 'register', + passkeyConfig: { + name: 'Test Passkey', + rpId: 'localhost', + rpName: 'Example dApp', + userId: 'test-user', + isUserVerified: true, + }, + }); + + // Add custom network + await coinbase.handleAction(CoinbaseSpecificActionType.ADD_NETWORK, { + network: { + name: 'Local Test Network', + rpcUrl: `http://localhost:${node?.port}`, + chainId: 84532, + symbol: 'ETH', + }, + }); + + // Switch to the network + await coinbase.handleAction(BaseActionType.SWITCH_NETWORK, { + networkName: 'Local Test Network', + }); + + // Perform transaction with passkey approval + await page.getByRole('button', { name: 'Send Transaction' }).click(); + + const [txPopup] = await Promise.all([ + page.context().waitForEvent('page'), + page.getByRole('button', { name: 'Confirm' }).click(), + ]); + + await coinbase.handleAction(CoinbaseSpecificActionType.HANDLE_PASSKEY_POPUP, { + mainPage: page, + popup: txPopup, + passkeyAction: 'approve', + }); + + // Verify success + await expect(page.getByText('Transaction complete')).toBeVisible(); +}); +``` + +## PasskeyAuthenticator Class + +Coinbase Wallet uses the `PasskeyAuthenticator` class internally to manage WebAuthn credentials: + +```typescript +class PasskeyAuthenticator { + constructor(page: Page); + + async setup(options: PasskeySetupOptions): Promise; + async createCredential(options: CredentialCreationOptions): Promise; + async getCredential(options: CredentialRequestOptions): Promise; + async cleanup(): Promise; +} +``` + +## Best Practices + + +Always handle popups when working with passkeys - they open in separate browser windows + + + +Passkey credentials are tied to specific domains (rpId). Use consistent values across tests + + + +Coinbase Wallet requires stronger passwords than some other wallets. Use complex passwords with numbers and special characters + + +## Next Steps + +- Explore the [API reference](/onchaintestkit/wallets/api-reference) +- See [complete test examples](/onchaintestkit/examples) \ No newline at end of file diff --git a/docs/onchaintestkit/wallets/common-actions.mdx b/docs/onchaintestkit/wallets/common-actions.mdx new file mode 100644 index 0000000..578c360 --- /dev/null +++ b/docs/onchaintestkit/wallets/common-actions.mdx @@ -0,0 +1,289 @@ +--- +title: "Common Wallet Actions" +description: "Actions and operations supported by all wallet implementations" +--- + +This guide covers the common actions and operations that are supported by both MetaMask and Coinbase Wallet. These actions form the foundation of wallet testing with OnchainTestKit. + +## Base Actions + +All wallets support these fundamental actions through the `BaseActionType` enum: + +```typescript +enum BaseActionType { + IMPORT_WALLET_FROM_SEED = "importWalletFromSeed", + IMPORT_WALLET_FROM_PRIVATE_KEY = "importWalletFromPrivateKey", + SWITCH_NETWORK = "switchNetwork", + CONNECT_TO_DAPP = "connectToDapp", + HANDLE_TRANSACTION = "handleTransaction", + HANDLE_SIGNATURE = "handleSignature", + CHANGE_SPENDING_CAP = "changeSpendingCap", + REMOVE_SPENDING_CAP = "removeSpendingCap", +} +``` + +## Wallet Import and Setup + +### Import from Seed Phrase + + +```typescript MetaMask +await metamask.handleAction(BaseActionType.IMPORT_WALLET_FROM_SEED, { + seedPhrase: "test test test test test test test test test test test junk", + password: "MyPassword123!" +}); +``` + +```typescript Coinbase Wallet +await coinbase.handleAction(BaseActionType.IMPORT_WALLET_FROM_SEED, { + seedPhrase: "test test test test test test test test test test test junk", + password: "MyPassword123!" +}); +``` + + +### Import from Private Key + + +```typescript MetaMask +await metamask.handleAction(BaseActionType.IMPORT_WALLET_FROM_PRIVATE_KEY, { + privateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +}); +``` + +```typescript Coinbase Wallet +await coinbase.handleAction(BaseActionType.IMPORT_WALLET_FROM_PRIVATE_KEY, { + privateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +}); +``` + + +## Network Operations + +### Switch Network + + +```typescript Basic Network Switch +await wallet.handleAction(BaseActionType.SWITCH_NETWORK, { + networkName: "Base Sepolia" +}); +``` + +```typescript Testnet Switch +await wallet.handleAction(BaseActionType.SWITCH_NETWORK, { + networkName: "Base Sepolia", + isTestnet: true +}); +``` + + +## dApp Connection + +### Connect to dApp + +```typescript +test('connect wallet to dApp', async ({ page, metamask }) => { + // Click connect button on dApp + await page.getByTestId('ockConnectButton').click(); + + // Select wallet from modal + await page.getByRole('button', { name: 'MetaMask' }).click(); + + // Handle connection in wallet + await metamask.handleAction(BaseActionType.CONNECT_TO_DAPP); + + // Verify connection + await expect(page.getByTestId('wallet-connected')).toBeVisible(); +}); +``` + +## Transaction Handling + +### Approve Transaction + +```typescript +await wallet.handleAction(BaseActionType.HANDLE_TRANSACTION, { + approvalType: ActionApprovalType.APPROVE +}); +``` + +### Reject Transaction + +```typescript +await wallet.handleAction(BaseActionType.HANDLE_TRANSACTION, { + approvalType: ActionApprovalType.REJECT +}); +``` + +### Complete Transaction Flow + +```typescript +test('complete transaction', async ({ page, metamask }) => { + // Trigger transaction on dApp + await page.getByRole('button', { name: 'Send Transaction' }).click(); + + // Wait for wallet notification + await page.waitForTimeout(1000); + + // Approve transaction + await metamask.handleAction(BaseActionType.HANDLE_TRANSACTION, { + approvalType: ActionApprovalType.APPROVE + }); + + // Wait for confirmation + await expect(page.getByText('Transaction confirmed')).toBeVisible(); +}); +``` + +## Signature Handling + +### Sign Message + +```typescript +await wallet.handleAction(BaseActionType.HANDLE_SIGNATURE, { + approvalType: ActionApprovalType.APPROVE +}); +``` + +### Sign Typed Data + +```typescript +test('sign typed data', async ({ page, metamask }) => { + // Trigger signature request + await page.getByRole('button', { name: 'Sign Message' }).click(); + + // Approve signature + await metamask.handleAction(BaseActionType.HANDLE_SIGNATURE, { + approvalType: ActionApprovalType.APPROVE + }); + + // Verify signature + await expect(page.getByTestId('signature-result')).toBeVisible(); +}); +``` + +## Token Approvals + +### Change Spending Cap + +```typescript +await wallet.handleAction(BaseActionType.CHANGE_SPENDING_CAP, { + approvalType: ActionApprovalType.APPROVE +}); +``` + +### Remove Spending Cap + +```typescript +await wallet.handleAction(BaseActionType.REMOVE_SPENDING_CAP, { + approvalType: ActionApprovalType.APPROVE +}); +``` + +## Notification Detection + +Both wallets support detecting the type of notification currently displayed: + +```typescript +const notificationType = await wallet.identifyNotificationType(); +// Returns: 'Transaction' | 'SpendingCap' | 'Signature' | 'RemoveSpendCap' + +switch (notificationType) { + case 'Transaction': + await wallet.handleAction(BaseActionType.HANDLE_TRANSACTION, { + approvalType: ActionApprovalType.APPROVE + }); + break; + case 'SpendingCap': + await wallet.handleAction(BaseActionType.CHANGE_SPENDING_CAP, { + approvalType: ActionApprovalType.APPROVE + }); + break; + case 'Signature': + await wallet.handleAction(BaseActionType.HANDLE_SIGNATURE, { + approvalType: ActionApprovalType.APPROVE + }); + break; +} +``` + +## Approval Types + +All approval actions support these types: + +```typescript +enum ActionApprovalType { + APPROVE = "approve", + REJECT = "reject", +} +``` + +## Complete Example + +Here's a complete example that demonstrates multiple common actions: + +```typescript +import { createOnchainTest, BaseActionType, ActionApprovalType } from '@coinbase/onchaintestkit'; +import { metamaskWalletConfig } from './config/metamaskWalletConfig'; + +const test = createOnchainTest(metamaskWalletConfig); + +test('complete wallet flow', async ({ page, metamask }) => { + if (!metamask) throw new Error('MetaMask fixture is required'); + + // Connect to dApp + await page.getByTestId('ockConnectButton').click(); + await page.getByRole('button', { name: 'MetaMask' }).click(); + await metamask.handleAction(BaseActionType.CONNECT_TO_DAPP); + + // Switch network + await page.getByTestId('switch-network').click(); + await metamask.handleAction(BaseActionType.SWITCH_NETWORK, { + networkName: 'Base Sepolia', + isTestnet: true + }); + + // Perform swap transaction + await page.locator('input[placeholder="0.0"]').fill('0.1'); + await page.getByRole('button', { name: 'Swap' }).click(); + + // Handle spending cap if needed + let notificationType = await metamask.identifyNotificationType(); + if (notificationType === 'SpendingCap') { + await metamask.handleAction(BaseActionType.CHANGE_SPENDING_CAP, { + approvalType: ActionApprovalType.APPROVE + }); + } + + // Handle transaction + notificationType = await metamask.identifyNotificationType(); + if (notificationType === 'Transaction') { + await metamask.handleAction(BaseActionType.HANDLE_TRANSACTION, { + approvalType: ActionApprovalType.APPROVE + }); + } + + // Verify success + await expect(page.getByRole('link', { name: 'View on Explorer' })).toBeVisible(); +}); +``` + +## Best Practices + + +Always check for wallet fixture existence before using it in tests + + + +Never use production wallets or seed phrases in tests + + + +Use `identifyNotificationType()` when handling complex flows with multiple approval steps + + +## Next Steps + +- Learn about [MetaMask-specific features](/onchaintestkit/wallets/metamask) +- Explore [Coinbase Wallet-specific features](/onchaintestkit/wallets/coinbase) +- Discover [advanced testing features](/onchaintestkit/wallets/advanced-features) \ No newline at end of file diff --git a/docs/onchaintestkit/wallets/metamask.mdx b/docs/onchaintestkit/wallets/metamask.mdx new file mode 100644 index 0000000..b0a874d --- /dev/null +++ b/docs/onchaintestkit/wallets/metamask.mdx @@ -0,0 +1,303 @@ +--- +title: "MetaMask Wallet Testing" +description: "MetaMask-specific features and actions for automated testing" +--- + +MetaMask is one of the most popular Ethereum wallets, and OnchainTestKit provides comprehensive support for automating MetaMask interactions in your tests. + +## MetaMask-Specific Actions + +In addition to the [common wallet actions](/onchaintestkit/wallets/common-actions), MetaMask supports these specific actions: + +```typescript +enum MetaMaskSpecificActionType { + LOCK = "lock", // Lock the wallet (not yet implemented) + UNLOCK = "unlock", // Unlock the wallet (not yet implemented) + ADD_TOKEN = "addToken", // Add a custom token + ADD_ACCOUNT = "addAccount", // Create a new account + RENAME_ACCOUNT = "renameAccount", // Rename an account (not yet implemented) + REMOVE_ACCOUNT = "removeAccount", // Remove an account + SWITCH_ACCOUNT = "switchAccount", // Switch between accounts + ADD_NETWORK = "addNetwork", // Add a custom network + APPROVE_ADD_NETWORK = "approveAddNetwork", // Approve network addition request +} +``` + +## Account Management + +### Add New Account + +Create additional accounts in MetaMask: + +```typescript +await metamask.handleAction(MetaMaskSpecificActionType.ADD_ACCOUNT, { + accountName: 'Trading Account', +}); +``` + +### Switch Between Accounts + +```typescript +await metamask.handleAction(MetaMaskSpecificActionType.SWITCH_ACCOUNT, { + accountName: 'Trading Account', +}); +``` + +### Remove Account + +```typescript +await metamask.handleAction(MetaMaskSpecificActionType.REMOVE_ACCOUNT, { + accountName: 'Old Account', +}); +``` + +### Complete Account Management Example + +```typescript +test('MetaMask account management', async ({ page, metamask }) => { + if (!metamask) throw new Error('MetaMask fixture is required'); + + // Add a new account + await metamask.handleAction(MetaMaskSpecificActionType.ADD_ACCOUNT, { + accountName: 'DeFi Account', + }); + + // Add another account + await metamask.handleAction(MetaMaskSpecificActionType.ADD_ACCOUNT, { + accountName: 'Trading Account', + }); + + // Switch between accounts + await metamask.handleAction(MetaMaskSpecificActionType.SWITCH_ACCOUNT, { + accountName: 'DeFi Account', + }); + + // Remove an account + await metamask.handleAction(MetaMaskSpecificActionType.REMOVE_ACCOUNT, { + accountName: 'Trading Account', + }); +}); +``` + +## Network Management + +### Add Custom Network + +Add a custom network to MetaMask: + +```typescript +await metamask.handleAction(MetaMaskSpecificActionType.ADD_NETWORK, { + network: { + name: 'Custom L2', + rpcUrl: 'https://rpc.custom-l2.network', + chainId: 12345, + symbol: 'ETH', + blockExplorerUrl: 'https://explorer.custom-l2.network', // Optional + }, +}); +``` + +### Approve Network Addition + +When a dApp requests to add a network: + +```typescript +test('approve network addition from dApp', async ({ page, metamask }) => { + // dApp triggers network addition + await page.getByRole('button', { name: 'Add Custom Network' }).click(); + + // Approve in MetaMask + await metamask.handleAction(MetaMaskSpecificActionType.APPROVE_ADD_NETWORK, { + approvalType: ActionApprovalType.APPROVE, + }); +}); +``` + +### Network Switching + +Switch to a specific network with testnet detection: + +```typescript +await metamask.handleAction(BaseActionType.SWITCH_NETWORK, { + networkName: 'Base Sepolia', + isTestnet: true, // Helps locate testnet networks +}); +``` + +## Token Management + +### Add Custom Token + +Add ERC-20 tokens to MetaMask: + +```typescript +await metamask.handleAction(MetaMaskSpecificActionType.ADD_TOKEN, { + tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC + tokenSymbol: 'USDC', + tokenDecimals: 6, +}); +``` + +### Token Management Example + +```typescript +test('add multiple tokens', async ({ page, metamask }) => { + // Add USDC + await metamask.handleAction(MetaMaskSpecificActionType.ADD_TOKEN, { + tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + tokenSymbol: 'USDC', + tokenDecimals: 6, + }); + + // Add DAI + await metamask.handleAction(MetaMaskSpecificActionType.ADD_TOKEN, { + tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + tokenSymbol: 'DAI', + tokenDecimals: 18, + }); +}); +``` + +## MetaMask Configuration + +### Basic Configuration + +```typescript +import { configure } from '@coinbase/onchaintestkit'; + +const metamaskConfig = configure() + .withMetaMask() + .withSeedPhrase({ + seedPhrase: process.env.E2E_TEST_SEED_PHRASE!, + password: 'PASSWORD', + }) + .build(); +``` + +### Advanced Configuration with Custom Setup + +```typescript +const metamaskConfig = configure() + .withMetaMask() + .withSeedPhrase({ + seedPhrase: process.env.E2E_TEST_SEED_PHRASE!, + password: 'PASSWORD', + }) + .withCustomSetup(async (wallet, context) => { + // Import additional accounts + await wallet.handleAction(BaseActionType.IMPORT_WALLET_FROM_PRIVATE_KEY, { + privateKey: process.env.SECONDARY_PRIVATE_KEY!, + }); + + // Add custom tokens + await wallet.handleAction(MetaMaskSpecificActionType.ADD_TOKEN, { + tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + tokenSymbol: 'USDC', + tokenDecimals: 6, + }); + + // Add custom networks + await wallet.handleAction(MetaMaskSpecificActionType.ADD_NETWORK, { + network: { + name: 'Base Mainnet', + rpcUrl: 'https://mainnet.base.org', + chainId: 8453, + symbol: 'ETH', + }, + }); + }) + .build(); +``` + +## Complete MetaMask Test Example + +Here's a comprehensive example demonstrating various MetaMask features: + +```typescript +import { createOnchainTest, MetaMaskSpecificActionType, BaseActionType } from '@coinbase/onchaintestkit'; +import { configure } from '@coinbase/onchaintestkit'; +import { baseSepolia } from "viem/chains" + +const config = configure() + .withLocalNode({ + chainId: baseSepolia.id, + forkUrl: process.env.E2E_TEST_FORK_URL, + }) + .withMetaMask() + .withSeedPhrase({ + seedPhrase: process.env.E2E_TEST_SEED_PHRASE!, + password: 'PASSWORD', + }) + .withNetwork({ + name: "Base Sepolia", + chainId: baseSepolia.id, + symbol: "ETH", + // placeholder for the actual rpcUrl, which is auto injected by the node fixture + rpcUrl: "http://localhost:8545", + }) + .build(); + +const test = createOnchainTest(config); + +test('comprehensive MetaMask test', async ({ page, metamask }) => { + if (!metamask) throw new Error('MetaMask fixture is required'); + + // Add accounts + await metamask.handleAction(MetaMaskSpecificActionType.ADD_ACCOUNT, { + accountName: 'DeFi Account', + }); + + // Add custom network + await metamask.handleAction(MetaMaskSpecificActionType.ADD_NETWORK, { + network: { + name: 'Base Mainnet', + rpcUrl: 'https://mainnet.base.org', + chainId: 8453, + symbol: 'ETH', + }, + }); + + // Switch network + await metamask.handleAction(BaseActionType.SWITCH_NETWORK, { + networkName: 'Base Mainnet', + }); + + // Add tokens + await metamask.handleAction(MetaMaskSpecificActionType.ADD_TOKEN, { + tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + tokenSymbol: 'USDC', + tokenDecimals: 6, + }); + + // Connect to dApp + await page.goto('https://app.example.com'); + await page.getByTestId('connect-wallet').click(); + await page.getByRole('button', { name: 'MetaMask' }).click(); + await metamask.handleAction(BaseActionType.CONNECT_TO_DAPP); + + // Perform transaction + await page.getByRole('button', { name: 'Send Transaction' }).click(); + await metamask.handleAction(BaseActionType.HANDLE_TRANSACTION, { + approvalType: ActionApprovalType.APPROVE, + }); +}); +``` + +## Best Practices + + +When adding networks, always include the chainId to ensure proper network identification + + + +Account removal is permanent within the test context. Plan your test flow accordingly + + + +MetaMask automatically saves state between actions, so network and token additions persist throughout the test + + +## Next Steps + +- Explore [Coinbase Wallet features](/onchaintestkit/wallets/coinbase) +- See the [API reference](/onchaintestkit/wallets/api-reference) \ No newline at end of file diff --git a/docs/onchaintestkit/wallets/overview.mdx b/docs/onchaintestkit/wallets/overview.mdx new file mode 100644 index 0000000..8111a70 --- /dev/null +++ b/docs/onchaintestkit/wallets/overview.mdx @@ -0,0 +1,108 @@ +--- +title: "Wallet Testing Overview" +description: "Comprehensive overview of wallet testing with OnchainTestKit" +--- + +## Architecture + + +```mermaid +flowchart TD + subgraph "Test Runner (Playwright)" + T1[Test File 1] + T2[Test File 2] + T3[Test File N] + end + + subgraph LocalNodeManager + LN1[Anvil Node 1] + LN2[Anvil Node 2] + LN3[Anvil Node N] + end + + subgraph Wallets + MM[MetaMask] + CBW[Coinbase Wallet] + end + + T1 -- uses --> MM + T2 -- uses --> CBW + T3 -- uses --> MM + T1 -- connects --> LN1 + T2 -- connects --> LN2 + T3 -- connects --> LN3 + MM -- interacts --> LN1 + CBW -- interacts --> LN2 +``` + + +## Core Concepts + +### 1. Wallet Abstraction + +- **MetaMask** and **Coinbase Wallet** are modeled as programmable objects. +- Actions like importing wallets, switching networks, and handling notifications are exposed as high-level methods. + +### 2. Local Node Management + +- Each test can spin up its own local Ethereum node (Anvil) with automatic port allocation. +- Supports chain state manipulation (snapshots, reverts), time travel, and account impersonation. + +### 3. Parallelization + +- Designed for parallel Playwright test execution. +- Each worker gets a unique node and wallet context, avoiding cross-test interference. + + +```mermaid +sequenceDiagram + participant Worker1 as Playwright Worker 1 + participant Node1 as Anvil Node 1 + participant Worker2 as Playwright Worker 2 + participant Node2 as Anvil Node 2 + + Worker1->>Node1: Start node on port 10001 + Worker2->>Node2: Start node on port 10002 + Worker1->>Node1: Run tests + Worker2->>Node2: Run tests + Worker1->>Node1: Stop node + Worker2->>Node2: Stop node +``` + + +## Supported Wallets + + + + Full support for MetaMask automation including account management, network switching, and transaction handling + + + Complete Coinbase Wallet support with passkey authentication and WebAuthn integration + + + +## Best Practices + +- Always check for wallet fixture existence before running actions +- Use environment variables for sensitive data (seed phrases, passwords) +- Use Playwright's parallelization features for fast test execution +- Clean up local nodes after tests to free resources +- Use snapshots for efficient state management between test steps +- For Coinbase Wallet passkey tests, ensure proper credential management between registration and approval steps +- When testing network switching, ensure the network is already added or use the ADD_NETWORK action first + +## Next Steps + + + + Follow our [Quick Start guide](/onchaintestkit/quickstart) to get up and running quickly + + + + Explore [common wallet actions](/onchaintestkit/wallets/common-actions) available across all wallets + + + + Dive into [MetaMask](/onchaintestkit/wallets/metamask) or [Coinbase Wallet](/onchaintestkit/wallets/coinbase) specific features + + \ No newline at end of file diff --git a/docs/onchaintestkit/writing-tests.mdx b/docs/onchaintestkit/writing-tests.mdx new file mode 100644 index 0000000..a46ebef --- /dev/null +++ b/docs/onchaintestkit/writing-tests.mdx @@ -0,0 +1,221 @@ +--- +title: "Writing Tests" +description: "Learn how to write comprehensive blockchain tests with OnchainTestKit" +--- + +This guide covers everything you need to know about writing tests with OnchainTestKit, from basic wallet connections to complex transaction scenarios. NOTE that some of these examples may be different from what you implement depending on your frontend code. + +## Available Fixtures + +OnchainTestKit provides several fixtures for your tests: + + +Fixtures are automatically injected into your test functions and handle setup/teardown. + + +| Fixture | Type | Description | +|---------|------|-------------| +| `page` | `Page` | Playwright page object for browser automation | +| `metamask` | `MetaMask` | MetaMask wallet automation interface | +| `coinbase` | `CoinbaseWallet` | Coinbase wallet automation interface | +| `node` | `LocalNodeManager` | Local blockchain node manager | +| `smartContractManager` | `SmartContractManager` | Smart contract deployment and interaction | + +## Basic Wallet Operations + +### Connecting a Wallet + + + +```typescript +test("connect MetaMask", async ({ page, metamask }) => { + if (!metamask) throw new Error("MetaMask not initialized") + + // Open wallet connect modal + await page.getByTestId("ockConnectButton").first().click() + + // Select MetaMask from wallet options + await page + .getByTestId("ockModalOverlay") + .first() + .getByRole("button", { name: "MetaMask" }) + .click() + + // Handle MetaMask connection request + await metamask.handleAction(BaseActionType.CONNECT_TO_DAPP) +}) +``` + + + +```typescript +test("connect Coinbase Wallet", async ({ page, coinbase }) => { + if (!coinbase) throw new Error("Coinbase not initialized") + + // Open wallet connect modal + await page.getByTestId("ockConnectButton").first().click() + + // Select Coinbase from wallet options + await page + .getByTestId("ockModalOverlay") + .first() + .getByRole("button", { name: "Coinbase" }) + .click() + + // Handle Coinbase connection request + await coinbase.handleAction(BaseActionType.CONNECT_TO_DAPP) +}) +``` + + + +### Network Switching + +```typescript +test("switch networks", async ({ page, metamask }) => { + // Connect wallet first + await connectWallet(page, metamask) + + // Switch to Base Sepolia + await page.getByTestId("switch-to-base-sepolia").click() + + // Handle network switch in wallet + await metamask.handleAction(BaseActionType.SWITCH_NETWORK) +}) +``` + +## Transaction Testing + +### Basic Transaction + +```typescript +test("send transaction", async ({ page, metamask }) => { + // Connect wallet + await connectWallet(page, metamask) + + // Ideally, you have some purchase button + + // Submit transaction + await page.getByTestId("purchase-button").click() + + // Approve transaction in wallet + await metamask.handleAction(BaseActionType.HANDLE_TRANSACTION, { + approvalType: ActionApprovalType.APPROVE, + }) + + // Wait for confirmation + await expect(page.getByText("Transaction confirmed!")).toBeVisible() +}) +``` + +### Rejecting Transactions + +```typescript +test("reject transaction", async ({ page, metamask }) => { + await connectWallet(page, metamask) + + // Trigger transaction + await page.getByTestId("purchase-button").click() + + // Reject in wallet + await metamask.handleAction(BaseActionType.HANDLE_TRANSACTION, { + approvalType: ActionApprovalType.REJECT, + }) + + // Verify rejection handled + await expect(page.getByText("Transaction rejected")).toBeVisible() +}) +``` + +## Advanced Testing Patterns + +### Parallel Test Execution + +```typescript +test.describe.parallel("Parallel tests", () => { + test("test 1", async ({ page, metamask, node }) => { + console.log(`Test 1 using port: ${node?.port}`) + // Each test gets its own isolated node + }) + + test("test 2", async ({ page, metamask, node }) => { + console.log(`Test 2 using port: ${node?.port}`) + // Different port, isolated environment + }) +}) +``` + +## Best Practices + + + + +Always wait for UI updates after wallet actions: + +```typescript +// Good +await metamask.handleAction(BaseActionType.CONNECT_TO_DAPP) +await page.waitForSelector('[data-testid="wallet-connected"]') + +// Bad - might be flaky +await metamask.handleAction(BaseActionType.CONNECT_TO_DAPP) +expect(page.getByText("Connected")).toBeVisible() // Might fail +``` + + + +Always include error scenarios in your tests: + +```typescript +test("handle wallet rejection", async ({ page, metamask }) => { + try { + await metamask.handleAction(BaseActionType.CONNECT_TO_DAPP, { + approvalType: ActionApprovalType.REJECT, + }) + } catch (error) { + // Verify error is handled in UI + await expect(page.getByText("Connection rejected")).toBeVisible() + } +}) +``` + + + +## Debugging Tests + +### Visual Debugging + +```bash +# Run tests in headed mode +yarn playwright test --headed + +# Use Playwright Inspector +yarn playwright test --debug + +# Slow down execution +yarn playwright test --slow-mo=1000 +``` + +### Console Logs + +```typescript +test("debug test", async ({ page, metamask }) => { + // Log page errors + page.on('pageerror', error => { + console.error('Page error:', error) + }) + + // Log console messages + page.on('console', msg => { + console.log('Console:', msg.text()) + }) + + // Your test code +}) +``` + +## Next Steps + +- [Test smart contracts](/onchaintestkit/smart-contracts) +- [See complete examples](/onchaintestkit/examples) +- [Set up CI/CD](/onchaintestkit/ci-cd) \ No newline at end of file