From 3f91df4fce35d146e04664a180b8e3c646388446 Mon Sep 17 00:00:00 2001 From: Aushveen Vimalathas Date: Tue, 24 Jun 2025 21:58:10 -0500 Subject: [PATCH 1/2] testing adding docs --- docs/_docs.json | 578 ++++++++++++++++++++ docs/onchaintestkit/contracts/contracts.mdx | 309 +++++++++++ docs/onchaintestkit/node/node.mdx | 475 ++++++++++++++++ docs/onchaintestkit/root/root.mdx | 395 +++++++++++++ docs/onchaintestkit/utils/utils.mdx | 323 +++++++++++ docs/onchaintestkit/wallets/wallets.mdx | 561 +++++++++++++++++++ 6 files changed, 2641 insertions(+) create mode 100644 docs/_docs.json create mode 100644 docs/onchaintestkit/contracts/contracts.mdx create mode 100644 docs/onchaintestkit/node/node.mdx create mode 100644 docs/onchaintestkit/root/root.mdx create mode 100644 docs/onchaintestkit/utils/utils.mdx create mode 100644 docs/onchaintestkit/wallets/wallets.mdx 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/onchaintestkit/contracts/contracts.mdx b/docs/onchaintestkit/contracts/contracts.mdx new file mode 100644 index 0000000..1df699f --- /dev/null +++ b/docs/onchaintestkit/contracts/contracts.mdx @@ -0,0 +1,309 @@ +# Smart Contract Manager + +> **Package:** [`@coinbase/onchaintestkit`](https://www.npmjs.com/package/@coinbase/onchaintestkit) + +--- + +## Overview + +The smart contract manager in `@coinbase/onchaintestkit` provide 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. + +Key features include: + +- **Deterministic Contract Deployment:** Deploy contracts at predictable addresses using the CREATE2 opcode and a deterministic deployment proxy. +- **Automated Proxy Management:** Ensure the deterministic deployment proxy is present and deployed as needed. +- **Contract State Management:** Deploy contracts and execute function calls as part of test setup, supporting complex test scenarios. +- **Parallel Test Safety:** Designed to work with the `LocalNodeManager` for parallel test execution, ensuring isolated and reproducible blockchain state. +- **Artifact Integration:** Loads contract ABIs and bytecode directly from Foundry build artifacts. + +This toolkit is essential for blockchain application developers and QA engineers who require reliable, reproducible, and scalable E2E testing of smart contract workflows. + +--- + +## 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 + +### 1. ProxyDeployer + +A utility class to manage the deterministic deployment proxy contract, which enables CREATE2-based deployments at predictable addresses. + +### 2. SmartContractManager + +A high-level manager for deploying contracts (using CREATE2), executing contract calls, and orchestrating contract state for tests. + +--- + +## API Reference + +### ProxyDeployer + +#### Description + +Handles deployment and verification of the deterministic deployment proxy contract. This proxy is required for CREATE2-based deterministic contract deployments. + +#### Constructor + +```typescript +new ProxyDeployer(node: LocalNodeManager) +``` + +- **node**: `LocalNodeManager` — The local Ethereum node manager instance. + +#### Methods + +| Method | Description | Returns | +|---------------------------|----------------------------------------------------------------------------------------------|------------------------| +| `isProxyDeployed()` | Checks if the deterministic deployment proxy is already deployed. | `Promise` | +| `ensureProxyDeployed()` | Deploys the proxy if not already present. | `Promise` | +| `getProxyAddress()` | Returns the address of the deterministic deployment proxy. | `Address` | + +#### Variables + +| Variable | Description | Type | +|---------------------|--------------------------------------------------------------------------------------------------|--------------| +| `PROXY_DEPLOYMENT_TX` | Raw transaction data for deploying the proxy. | `string` | +| `PROXY_ADDRESS` | The fixed address at which the proxy is deployed. | `Address` | +| `publicClient` | viem public client for interacting with the node. | `PublicClient`| +| `rpcUrl` | RPC URL of the local node. | `string` | + +#### Example + +```typescript +import { LocalNodeManager } from '@coinbase/onchaintestkit/node/LocalNodeManager'; +import { ProxyDeployer } from '@coinbase/onchaintestkit/contracts/ProxyDeployer'; + +const node = new LocalNodeManager({ chainId: 31337, mnemonic: '...' }); +await node.start(); + +const proxyDeployer = new ProxyDeployer(node); +await proxyDeployer.ensureProxyDeployed(); + +console.log('Proxy deployed at:', proxyDeployer.getProxyAddress()); +``` + +--- + +### SmartContractManager + +#### Description + +Manages smart contract deployments and interactions for E2E tests, supporting deterministic deployments via CREATE2 and orchestrating contract state setup. + +#### Constructor + +```typescript +new SmartContractManager(projectRoot: string) +``` + +- **projectRoot**: `string` — Path to the root of the project (used to locate contract artifacts). + +#### Methods + +| Method | Description | Returns | +|------------------------------|------------------------------------------------------------------------------------------------------|------------------------| +| `initialize(node)` | Initializes viem clients and ensures the proxy is deployed. | `Promise` | +| `deployContract(deployment)` | Deploys a contract using CREATE2 and stores its ABI. | `Promise
` | +| `executeCall(call)` | Executes a contract function call as a transaction. | `Promise` | +| `setContractState(config, node)` | Deploys contracts and executes calls as specified in a setup config. | `Promise` | +| `predictContractAddress(salt, bytecode, args)` | Predicts the address for a CREATE2 deployment. | `Address` | + +#### Variables + +| Variable | Description | Type | +|-----------------------|-----------------------------------------------------------------------------|----------------------------| +| `projectRoot` | Root directory for artifact lookup. | `string` | +| `publicClient` | viem public client (initialized in `initialize`). | `PublicClient` | +| `walletClient` | viem wallet client (initialized in `initialize`). | `WalletClient` | +| `deployedContracts` | Map of deployed contract addresses to their ABIs. | `Map` | +| `proxyDeployer` | Instance of `ProxyDeployer`. | `ProxyDeployer` | + +#### Example + +```typescript +import { LocalNodeManager } from '@coinbase/onchaintestkit/node/LocalNodeManager'; +import { SmartContractManager } from '@coinbase/onchaintestkit/contracts/SmartContractManager'; + +const node = new LocalNodeManager({ chainId: 31337, mnemonic: '...' }); +await node.start(); + +const scm = new SmartContractManager(process.cwd()); +await scm.initialize(node); + +const contractAddress = await scm.deployContract({ + name: 'MyContract', + args: [42, 'hello'], + salt: '0x1234...abcd', + deployer: '0xabc...def', +}); + +const txHash = await scm.executeCall({ + target: contractAddress, + functionName: 'setValue', + args: [100], + account: '0xabc...def', +}); +``` + +--- + +### Types + +#### `ContractDeployment` + +| Field | Type | Description | +|-----------|-------------------|------------------------------------------------| +| `name` | `string` | Contract name (matches artifact name). | +| `args` | `readonly unknown[]` | Constructor arguments. | +| `salt` | `0x${string}` | Salt for CREATE2 deployment (32 bytes). | +| `deployer`| `0x${string}` | Account deploying the contract. | + +#### `ContractCall` + +| Field | Type | Description | +|----------------|----------------------|---------------------------------------------| +| `target` | `0x${string}` | Contract address. | +| `functionName` | `string` | Function to call. | +| `args` | `readonly unknown[]` | Arguments for the function. | +| `account` | `0x${string}` | Account making the call. | +| `value` | `bigint` (optional) | ETH value to send. | + +#### `SetupConfig` + +| Field | Type | Description | +|---------------|--------------------------|---------------------------------------------| +| `deployments` | `ContractDeployment[]` | Contracts to deploy. | +| `calls` | `ContractCall[]` | Calls to execute after deployment. | + +#### `ContractArtifact` + +| Field | Type | Description | +|-----------|-------------------|------------------------------------------------| +| `abi` | `readonly unknown[]` | Contract ABI. | +| `bytecode`| `0x${string}` | Contract bytecode. | + +--- + +## Example: Full Contract State Setup + +```typescript +import { LocalNodeManager } from '@coinbase/onchaintestkit/node/LocalNodeManager'; +import { SmartContractManager } from '@coinbase/onchaintestkit/contracts/SmartContractManager'; + +const node = new LocalNodeManager({ chainId: 31337, mnemonic: '...' }); +await node.start(); + +const scm = new SmartContractManager(process.cwd()); +await scm.initialize(node); + +await scm.setContractState({ + deployments: [ + { + name: 'Token', + args: ['Test Token', 'TTK', 18], + salt: '0x0000000000000000000000000000000000000000000000000000000000000001', + deployer: '0xabc...def', + }, + ], + calls: [ + { + target: '0x1234...abcd', + functionName: 'mint', + args: ['0xabc...def', 1000n], + account: '0xabc...def', + }, + ], +}, node); +``` + +--- + +## Sequence Diagram: Deterministic Deployment + +```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() +``` + +--- + +## Events + +These classes do not emit Node.js events, but the following actions are observable in the test process: + +| Event/Action | Description | +|--------------------------------|----------------------------------------------------------------| +| Proxy deployed | Proxy contract is deployed to the node. | +| Contract deployed | Contract is deployed at deterministic address. | +| Contract call executed | Function call is executed as a transaction. | +| Contract state setup complete | All deployments and calls in `setContractState` are complete. | + +--- + +## 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. + +--- + +## See Also + +- [`LocalNodeManager`](../node/node.mdx): For managing local Ethereum nodes and state. +- [`@coinbase/onchaintestkit`](https://www.npmjs.com/package/@coinbase/onchaintestkit): Main package documentation. + +--- + +## Summary Table + +| Component | Purpose | Key Methods | +|-----------------------|-----------------------------------------------------|----------------------------------| +| ProxyDeployer | Deploys/ensures deterministic deployment proxy | isProxyDeployed, ensureProxyDeployed, getProxyAddress | +| SmartContractManager | Manages contract deployment and calls for tests | initialize, deployContract, executeCall, setContractState | + +--- + +## Further Example: Predicting Contract Address + +```typescript +const predicted = scm.predictContractAddress( + '0x0000...0001', + '0x600060...', // bytecode + [42, 'hello'] +); +console.log('Predicted address:', predicted); +``` \ No newline at end of file diff --git a/docs/onchaintestkit/node/node.mdx b/docs/onchaintestkit/node/node.mdx new file mode 100644 index 0000000..a817316 --- /dev/null +++ b/docs/onchaintestkit/node/node.mdx @@ -0,0 +1,475 @@ +# LocalNodeManager API Documentation + +> **Package:** `@coinbase/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 +``` + +--- + +## Usage Example + +```typescript +import { LocalNodeManager } from '@coinbase/onchaintestkit' + +// 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", +}) + +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) + +await node.stop() +``` + +--- + +## API Reference + +### Class: `LocalNodeManager` + +#### Description + +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 + +| Signature | Description | +|-----------|-------------| +| `new LocalNodeManager(config?: NodeConfig)` | Creates a new instance with the specified configuration. | + +#### Parameters + +| Name | Type | Description | +|--------|-------------|-------------| +| config | `NodeConfig` | Optional. Node configuration options (see [NodeConfig](#type-nodeconfig)). | + +--- + +### Methods + +| Method | Description | +|--------|-------------| +| [`start()`](#start) | Starts the Anvil node with the configured options. | +| [`stop()`](#stop) | Stops the running Anvil node and cleans up resources. | +| [`getPort()`](#getport) | Returns the allocated port number for this node instance. | +| [`snapshot()`](#snapshot) | Takes a snapshot of the current chain state. | +| [`revert(snapshotId)`](#revert) | Reverts the chain state to a previous snapshot. | +| [`reset(forkBlock?)`](#reset) | Resets the chain state to initial state or specified fork block. | +| [`mine(blocks?)`](#mine) | Mines a specified number of blocks. | +| [`setAutomine(enabled)`](#setautomine) | Enables or disables automatic block mining. | +| [`setNextBlockTimestamp(timestamp)`](#setnextblocktimestamp) | Sets the timestamp for the next block. | +| [`increaseTime(seconds)`](#increasetime) | Increases chain time by specified seconds. | +| [`setTime(timestamp)`](#settime) | Sets absolute chain time. | +| [`getAccounts()`](#getaccounts) | Gets list of available accounts. | +| [`setBalance(address, balance)`](#setbalance) | Sets balance for specified address. | +| [`setNonce(address, nonce)`](#setnonce) | Sets nonce for specified address. | +| [`setCode(address, code)`](#setcode) | Sets contract code at specified address. | +| [`setStorageAt(address, slot, value)`](#setstorageat) | Sets storage value at specified slot. | +| [`setNextBlockBaseFeePerGas(fee)`](#setnextblockbasefeepergas) | Sets base fee for next block (EIP-1559). | +| [`setMinGasPrice(price)`](#setmingasprice) | Sets minimum gas price. | +| [`setChainId(chainId)`](#setchainid) | Sets chain ID. | +| [`impersonateAccount(address)`](#impersonateaccount) | Enables impersonation of specified account. | +| [`stopImpersonatingAccount(address)`](#stopimpersonatingaccount) | Disables impersonation of specified account. | + +--- + +#### Method Details + +##### start + +```typescript +async start(): Promise +``` +Starts the Anvil node with the configured options. Allocates a port, spawns the process, and waits for readiness. + +--- + +##### stop + +```typescript +async stop(): Promise +``` +Stops the running Anvil node and cleans up resources. + +--- + +##### getPort + +```typescript +getPort(): number | null +``` +Returns the allocated port number for this node instance, or `null` if not started. + +--- + +##### snapshot + +```typescript +async snapshot(): Promise +``` +Takes a snapshot of the current chain state. Returns a snapshot ID for later use with `revert()`. + +--- + +##### revert + +```typescript +async revert(snapshotId: string): Promise +``` +Reverts the chain state to a previous snapshot. + +- `snapshotId`: 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. + +- `forkBlock`: Optional block number to reset to (when in fork mode). + +--- + +##### mine + +```typescript +async mine(blocks = 1): Promise +``` +Mines a specified number of blocks (default: 1). + +--- + +##### setAutomine + +```typescript +async setAutomine(enabled: boolean): Promise +``` +Enables or disables automatic block mining. + +--- + +##### setNextBlockTimestamp + +```typescript +async setNextBlockTimestamp(timestamp: number): Promise +``` +Sets the timestamp for the next block (Unix seconds). + +--- + +##### increaseTime + +```typescript +async increaseTime(seconds: number): Promise +``` +Increases chain time by the specified number of seconds. + +--- + +##### setTime + +```typescript +async setTime(timestamp: number): Promise +``` +Sets absolute chain time (Unix seconds). + +--- + +##### getAccounts + +```typescript +async getAccounts(): Promise +``` +Returns an array of available account addresses. + +--- + +##### setBalance + +```typescript +async setBalance(address: string, balance: bigint): Promise +``` +Sets the balance for the specified address (in wei). + +--- + +##### setNonce + +```typescript +async setNonce(address: string, nonce: number): Promise +``` +Sets the nonce for the specified address. + +--- + +##### setCode + +```typescript +async setCode(address: string, code: string): Promise +``` +Sets the contract code at the specified address. + +--- + +##### setStorageAt + +```typescript +async setStorageAt(address: string, slot: string, value: string): Promise +``` +Sets the storage value at the specified slot for a contract. + +--- + +##### setNextBlockBaseFeePerGas + +```typescript +async setNextBlockBaseFeePerGas(fee: bigint): Promise +``` +Sets the base fee for the next block (EIP-1559). + +--- + +##### setMinGasPrice + +```typescript +async setMinGasPrice(price: bigint): Promise +``` +Sets the minimum gas price. + +--- + +##### setChainId + +```typescript +async setChainId(chainId: number): Promise +``` +Sets the chain ID. + +--- + +##### impersonateAccount + +```typescript +async impersonateAccount(address: string): Promise +``` +Enables impersonation of the specified account. + +--- + +##### stopImpersonatingAccount + +```typescript +async stopImpersonatingAccount(address: string): Promise +``` +Disables impersonation of the specified account. + +--- + +### Properties + +| Name | Type | Description | +|------|------|-------------| +| `port` | `number` | The allocated port number, or -1 if not started. | +| `rpcUrl` | `string` | The RPC URL for the running node (e.g., `http://localhost:12345`). | + +--- + +### Type: `NodeConfig` + +Configuration options for `LocalNodeManager`. + +| Property | Type | Description | +|----------|------|-------------| +| `port` | `number` | Port number for RPC server (optional). | +| `portRange` | `[number, number]` | Port range for automatic port selection (optional). | +| `chainId` | `number` | Chain ID for the network (default: 84532). | +| `mnemonic` | `string` | Mnemonic for the network (optional). | +| `forkUrl` | `string` | URL to fork from (e.g., mainnet) (optional). | +| `forkBlockNumber` | `bigint` | Block number to fork from (optional). | +| `forkRetryInterval` | `number` | Retry interval for fork requests (optional). | +| `defaultBalance` | `bigint` | Default balance for test accounts (optional). | +| `totalAccounts` | `number` | Number of test accounts to generate (optional). | +| `blockTime` | `number` | Time between blocks (0 for instant, default: 0). | +| `blockGasLimit` | `bigint` | Gas limit per block (optional). | +| `noMining` | `boolean` | Disable automatic mining (optional). | +| `hardfork` | `"london" \| "berlin" \| "cancun"` | Specific hardfork to use (optional). | + +--- + +## Parallel Test Execution + +`LocalNodeManager` is designed for parallel test execution. It dynamically allocates ports for each node instance, ensuring no conflicts even across multiple processes. + +```mermaid +sequenceDiagram + participant TestWorker1 + participant TestWorker2 + participant LocalNodeManager1 + participant LocalNodeManager2 + participant System + + TestWorker1->>LocalNodeManager1: new LocalNodeManager() + LocalNodeManager1->>System: Find available port (e.g., 12001) + LocalNodeManager1->>System: Start Anvil on 12001 + + TestWorker2->>LocalNodeManager2: new LocalNodeManager() + LocalNodeManager2->>System: Find available port (e.g., 12002) + LocalNodeManager2->>System: Start Anvil on 12002 +``` + +--- + +## Advanced: RPC Port Interception + +When your dApp or test expects the RPC endpoint at `http://localhost:8545`, but your node is running on a dynamic port, use the provided utility to intercept and rewrite requests: + +```typescript +import { setupRpcPortInterceptor } from '@coinbase/onchaintestkit' +import { Page } from '@playwright/test' + +await setupRpcPortInterceptor(page, node.getPort()) +``` + +This ensures all requests to `http://localhost:8545` are transparently redirected to the correct port. + +--- + +## NodeFixturesBuilder + +For Playwright integration, use `NodeFixturesBuilder` to automatically manage node lifecycle in your test fixtures: + +```typescript +import { NodeFixturesBuilder } from '@coinbase/onchaintestkit' + +const nodeFixtures = new NodeFixturesBuilder({ + chainId: 84532, + mnemonic: process.env.E2E_TEST_SEED_PHRASE, +}).build() + +export const test = nodeFixtures +``` + +--- + +## Variables + +| Variable | Type | Description | +|----------|------|-------------| +| `DEFAULT_PORT_RANGE` | `[number, number]` | Default port range for dynamic allocation (`[10000, 20000]`). | +| `MAX_PORT_ALLOCATION_RETRIES` | `number` | Maximum retries for port allocation (`5`). | +| `process` | `ChildProcess \| null` | Reference to the running Anvil process. | +| `provider` | `ethers.providers.JsonRpcProvider \| null` | JSON-RPC provider for interacting with the node. | +| `config` | `NodeConfig` | Configuration for the node. | +| `allocatedPort` | `number \| null` | The port allocated for this node instance. | + +--- + +## Events + +| Event | Description | +|-------|-------------| +| `process.on("error", handler)` | Emitted if the Anvil process encounters an error. | +| `process.on("exit", handler)` | Emitted when the Anvil process exits. | +| `process.stdout.on("data", handler)` | Emitted when Anvil outputs to stdout (used for readiness detection). | +| `process.stderr.on("data", handler)` | Emitted when Anvil outputs to stderr (for debugging). | + +--- + +## Example: Parallel Playwright Test + +```typescript +import { test as base } from '@playwright/test' +import { NodeFixturesBuilder } from '@coinbase/onchaintestkit' + +const nodeFixtures = new NodeFixturesBuilder({ + chainId: 84532, + mnemonic: process.env.E2E_TEST_SEED_PHRASE, +}).build() + +const test = nodeFixtures + +test('runs with isolated node', async ({ node }) => { + await node.mine(5) + const accounts = await node.getAccounts() + // ...test logic... +}) +``` + +--- + +## Best Practices + +- Always call `await node.stop()` after your test to free resources. +- Use `test.afterEach()` or fixture scopes to ensure cleanup. +- Use snapshots and reverts for fast, deterministic test state. +- Use a wide port range for parallel test reliability. +- Use `setupRpcPortInterceptor` for seamless dApp RPC integration. + +--- + +## See Also + +- [@coinbase/onchaintestkit on npm](https://www.npmjs.com/package/@coinbase/onchaintestkit) +- [Playwright documentation](https://playwright.dev/) +- [Anvil documentation](https://book.getfoundry.sh/anvil/) + +--- + +**© Coinbase, Inc.** \ No newline at end of file diff --git a/docs/onchaintestkit/root/root.mdx b/docs/onchaintestkit/root/root.mdx new file mode 100644 index 0000000..043fa6b --- /dev/null +++ b/docs/onchaintestkit/root/root.mdx @@ -0,0 +1,395 @@ +# @coinbase/onchaintestkit + +End-to-end testing toolkit for blockchain applications, powered by Playwright. + +--- + +## Overview + +`@coinbase/onchaintestkit` is a comprehensive, type-safe framework for end-to-end testing of blockchain applications. It provides seamless integration with Playwright, enabling robust automation of browser-based wallet interactions, local blockchain node management, and common blockchain testing scenarios. The toolkit supports multiple wallets (MetaMask, Coinbase Wallet), dynamic network configuration, and parallel test execution with isolated local nodes. + +### Why is it Important? + +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. `@coinbase/onchaintestkit` automates these processes, ensuring reliability, reproducibility, and scalability for your dApp's test suite. + +--- + +## 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)"] + end + A --> B + B -- manages --> D + B -- manages --> C + D -- interacts --> C + B -- configures --> C + B -- automates --> D +``` + +--- + +## Key Features + +- **Playwright Integration**: Automate browser-based wallet and dApp interactions. +- **Wallet Support**: MetaMask and Coinbase Wallet, with extensible architecture. +- **Action Handling**: Automate connect, transaction, signature, approval, and network switching flows. +- **Network Management**: Use local Anvil nodes or remote RPC endpoints, with dynamic port allocation for parallelism. +- **Type Safety**: Full TypeScript support for all configuration and test APIs. +- **Fluent Configuration**: Builder pattern for wallet and node setup. +- **Parallel Testing**: Reliable cross-process port allocation for running multiple nodes in parallel. + +--- + +## Getting Started + +### Example: Basic Wallet Test + +```typescript +import { configure, createOnchainTest, BaseActionType } from '@coinbase/onchaintestkit'; + +const test = createOnchainTest( + 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() +); + +test('connect wallet and swap', async ({ page, metamask }) => { + await page.getByTestId('ockConnectButton').click(); + await metamask.handleAction(BaseActionType.CONNECT_TO_DAPP); + // ... further dApp and wallet interactions +}); +``` + +--- + +## Configuration Builder + +The toolkit uses a fluent builder pattern for configuration, supporting both MetaMask and Coinbase Wallet. + +### Example: Full Configuration + +```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: 'your seed phrase', + password: 'your password', + }) + .withCustomSetup(async (wallet) => { + await wallet.importToken('0x...'); + }) + .build(); +``` + +### Configuration Builder API + +| Method | Description | +|-----------------------|----------------------------------------------------------------------------------------------| +| `withMetaMask()` | Initialize MetaMask wallet configuration. | +| `withCoinbase()` | Initialize Coinbase Wallet configuration. | +| `withSeedPhrase()` | Set wallet seed phrase and optional password. | +| `withNetwork()` | Configure network (name, rpcUrl, chainId, symbol). | +| `withLocalNode()` | Configure local Anvil node (chainId, mnemonic, port range, etc.). | +| `withCustomSetup()` | Add custom async setup steps for the wallet (e.g., import tokens, set up contracts, etc.). | +| `build()` | Finalize and return the configuration object. | + +--- + +## Wallet Actions + +### Base Actions + +```typescript +enum BaseActionType { + CONNECT_TO_DAPP = 'CONNECT_TO_DAPP', + HANDLE_TRANSACTION = 'HANDLE_TRANSACTION', + HANDLE_SIGNATURE = 'HANDLE_SIGNATURE', + CHANGE_SPENDING_CAP = 'CHANGE_SPENDING_CAP', + SWITCH_NETWORK = 'SWITCH_NETWORK', + IMPORT_WALLET_FROM_SEED = 'IMPORT_WALLET_FROM_SEED', +} +``` + +### Notification Types + +```typescript +enum NotificationPageType { + Transaction = 'Transaction', + SpendingCap = 'SpendingCap', + Signature = 'Signature', +} +``` + +### Approval Types + +```typescript +enum ActionApprovalType { + APPROVE = 'APPROVE', + REJECT = 'REJECT', +} +``` + +--- + +## LocalNodeManager + +The `LocalNodeManager` class manages local Anvil Ethereum nodes for isolated, reproducible blockchain state during tests. + +### Features + +- **Lifecycle**: Start/stop nodes, get allocated port. +- **State**: Create/revert snapshots, reset chain. +- **Time**: Time travel, mine blocks. +- **Accounts**: Set balances, impersonate accounts. +- **Network**: Set gas price, chain ID. +- **Parallelism**: Dynamic port allocation for parallel test workers. + +### Example: Parallel Node Management + +```typescript +import { LocalNodeManager } from '@coinbase/onchaintestkit'; + +const nodeManager = new LocalNodeManager({ + chainId: 84532, + mnemonic: process.env.E2E_TEST_SEED_PHRASE, + // Optional: portRange: [10000, 20000] +}); + +await nodeManager.start(); +const port = nodeManager.getPort(); +console.log(`Node running on port ${port}`); +// ... run tests ... +await nodeManager.stop(); +``` + +#### Parallel Test Execution + +```mermaid +sequenceDiagram + participant Worker1 as Playwright Worker 1 + participant Worker2 as Playwright Worker 2 + participant Node1 as Anvil Node 1 (port 10001) + participant Node2 as Anvil Node 2 (port 10002) + Worker1->>Node1: Start node (allocates port 10001) + Worker2->>Node2: Start node (allocates port 10002) + Worker1->>Node1: Run tests + Worker2->>Node2: Run tests + Worker1->>Node1: Stop node + Worker2->>Node2: Stop node +``` + +--- + +## API Reference + +### Functions + +| Function | Description | +|--------------------------------|-------------------------------------------------------------------------------------------------------| +| `configure()` | Creates a new configuration builder instance. | +| `createOnchainTest(options)` | Creates a Playwright test instance with wallet and node fixtures based on the provided configuration. | +| `setupMetaMask()` | Utility to prepare MetaMask extension for testing. | +| `setupRpcPortInterceptor()` | Intercepts RPC requests to redirect to the correct local node port. | + +### Classes + +#### ConfigBuilder + +| Method | Description | +|------------------------|----------------------------------------------------------------------------------------------| +| `withMetaMask()` | Initialize MetaMask wallet configuration. | +| `withCoinbase()` | Initialize Coinbase Wallet configuration. | +| `withSeedPhrase()` | Set wallet seed phrase and optional password. | +| `withNetwork()` | Configure network (name, rpcUrl, chainId, symbol). | +| `withLocalNode()` | Configure local Anvil node (chainId, mnemonic, port range, etc.). | +| `withCustomSetup()` | Add custom async setup steps for the wallet. | +| `build()` | Finalize and return the configuration object. | + +#### LocalNodeManager + +| Method | Description | +|------------------|--------------------------------------------------| +| `start()` | Starts the local Anvil node. | +| `stop()` | Stops the node and releases resources. | +| `getPort()` | Returns the allocated port for the node. | +| `snapshot()` | Creates a blockchain state snapshot. | +| `revert()` | Reverts to a previously created snapshot. | +| `setBalance()` | Sets the balance of an account. | +| `impersonate()` | Impersonates an account for testing. | + +--- + +### Variables + +| Variable | Description | +|----------------------|---------------------------------------------------------------------| +| `CACHE_DIR_NAME` | Directory name for caching test artifacts. | +| `fixtureBuilderMap` | Map of wallet names to their fixture builder functions. | + +--- + +### Events + +- **Wallet Action Events**: Triggered when wallet actions are performed (e.g., connect, sign, approve). +- **Node Lifecycle Events**: Start/stop events for local nodes. +- **Network Interception Events**: When RPC requests are redirected to the correct node port. + +--- + +## Example: MetaMask Wallet Test + +```typescript +import { configure, createOnchainTest, BaseActionType } from '@coinbase/onchaintestkit'; + +const test = createOnchainTest( + 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() +); + +test('connect wallet and swap', async ({ page, metamask }) => { + await page.getByTestId('ockConnectButton').click(); + await metamask.handleAction(BaseActionType.CONNECT_TO_DAPP); + // ... further dApp and wallet interactions +}); +``` + +--- + +## Example: Coinbase Wallet Test + +```typescript +import { configure, createOnchainTest, BaseActionType } from '@coinbase/onchaintestkit'; + +const test = createOnchainTest( + configure() + .withLocalNode({ chainId: 1337 }) + .withCoinbase() + .withNetwork({ + name: 'Base Sepolia', + rpcUrl: 'http://localhost:8545', + chainId: 84532, + symbol: 'ETH', + }) + .withSeedPhrase({ + seedPhrase: process.env.E2E_TEST_SEED_PHRASE!, + password: 'PASSWORD', + }) + .build() +); + +test('connect coinbase wallet', async ({ page, coinbase }) => { + await page.getByTestId('ockConnectButton').click(); + await coinbase.handleAction(BaseActionType.CONNECT_TO_DAPP); + // ... further dApp and wallet interactions +}); +``` + +--- + +## Example: Custom Wallet Setup + +```typescript +import { configure, createOnchainTest } from '@coinbase/onchaintestkit'; + +const test = createOnchainTest( + configure() + .withMetaMask() + .withSeedPhrase({ + seedPhrase: process.env.E2E_TEST_SEED_PHRASE!, + password: 'PASSWORD', + }) + .withCustomSetup(async (wallet) => { + await wallet.importToken('0x...'); + }) + .build() +); + +test('custom setup', async ({ metamask }) => { + // Custom setup steps have already run +}); +``` + +--- + +## Types + +### WalletFixtureOptions + +```typescript +type WalletFixtureOptions = { + wallets: { + metamask?: MetaMaskConfig; + coinbase?: CoinbaseConfig; + }; + nodeConfig?: NodeConfig; +}; +``` + +### OnchainFixtures + +```typescript +type OnchainFixtures = { + metamask?: MetaMask; + coinbase?: CoinbaseWallet; + node?: LocalNodeManager; + smartContractManager?: SmartContractManager; +}; +``` + +--- + +## Best Practices + +- Always call `nodeManager.stop()` after tests to release resources. +- Use `test.afterEach()` to ensure cleanup even on failure. +- Use environment variables for sensitive data (e.g., seed phrases). +- Use snapshots for efficient state resets between test steps. +- Keep port ranges large enough for your parallel worker count. + +--- + +## Summary + +`@coinbase/onchaintestkit` provides a robust, extensible, and type-safe foundation for end-to-end blockchain application testing. By automating wallet and node management, it enables reliable, scalable, and maintainable test suites for modern dApps. + +--- \ No newline at end of file diff --git a/docs/onchaintestkit/utils/utils.mdx b/docs/onchaintestkit/utils/utils.mdx new file mode 100644 index 0000000..f307187 --- /dev/null +++ b/docs/onchaintestkit/utils/utils.mdx @@ -0,0 +1,323 @@ +# Onchain Test Kit + +**@coinbase/onchaintestkit** is an end-to-end testing toolkit for blockchain applications, built on top of Playwright. It provides a robust, type-safe, and extensible framework for automating wallet interactions, managing local blockchain nodes, and orchestrating complex blockchain testing scenarios. + +--- + +## Overview + +**Onchain Test Kit** is designed to streamline the process of writing, running, and maintaining reliable E2E tests for decentralized applications (dApps). It abstracts away the complexity of browser automation, wallet management, and local node orchestration, enabling developers to focus on business logic and user flows. + +**Key Capabilities:** + +- **Wallet Automation:** Seamlessly interact with MetaMask and Coinbase Wallet in browser-based tests. +- **Network Management:** Easily configure and switch between test networks using [viem](https://viem.sh/) chains. +- **Local Node Orchestration:** Spin up, snapshot, and manage local Ethereum nodes (Anvil) for deterministic testing. +- **Parallelization:** Supports parallel test execution with automatic port allocation for multiple local nodes. +- **Type Safety:** Built with TypeScript for robust, maintainable test code. + +--- + +## Why Onchain Test Kit? + +Testing blockchain applications is uniquely challenging due to: + +- The need for deterministic blockchain state. +- Complex wallet interactions (signatures, approvals, network switching). +- Parallel test execution without port or state conflicts. +- Automation of browser extensions (wallets) in headless environments. + +**@coinbase/onchaintestkit** solves these challenges by providing a unified, extensible toolkit that integrates tightly with Playwright and modern blockchain tooling. + +--- + +## Utility Functions + +This section documents the utility functions provided by the toolkit, which are essential for managing temporary directories, browser extensions, Playwright fixtures, and filesystem cleanup. + +--- + +### API Reference + +| Function | Description | +|-----------------------|---------------------------------------------------------------------------------------------| +| `createTempDir` | Creates a unique temporary directory with a given prefix. | +| `getExtensionId` | Discovers and retrieves the unique identifier for a browser extension in Playwright context.| +| `mergeFixtures` | Merges custom Playwright fixtures with the base test type. | +| `removeTempDir` | Removes a temporary directory, logging a warning if cleanup fails. | + +--- + +### Function Details + +#### `createTempDir(prefix: string): Promise` + +Creates a unique temporary directory in the system's temp folder. + +**Parameters:** + +| Name | Type | Description | +|--------|--------|----------------------------| +| prefix | string | Prefix for the temp folder. | + +**Returns:** +`Promise` – The path to the created temporary directory. + +**Example:** + +```typescript +import { createTempDir } from '@coinbase/onchaintestkit'; + +const tempDir = await createTempDir('mytest-'); +console.log(tempDir); // e.g., /tmp/mytest-abc123 +``` + +--- + +#### `getExtensionId(context: BrowserContext, extensionName: string): Promise` + +Discovers and retrieves the unique identifier for a browser extension in a Playwright browser context. + +**Parameters:** + +| Name | Type | Description | +|---------------|-----------------|----------------------------------------------| +| context | BrowserContext | Playwright browser context. | +| extensionName | string | Name of the extension to search for. | + +**Returns:** +`Promise` – The extension's unique identifier. + +**Example:** + +```typescript +import { getExtensionId } from '@coinbase/onchaintestkit'; +import { chromium } from '@playwright/test'; + +const context = await chromium.launchPersistentContext('', { headless: false }); +const metamaskId = await getExtensionId(context, 'MetaMask'); +console.log(metamaskId); // e.g., 'nkbihfbeogaeaoehlefnkodbefgpgknn' +``` + +--- + +#### `mergeFixtures(customFixtures: TestType): TestType` + +Merges custom Playwright fixtures with the base test type, allowing you to extend the test environment with additional fixtures. + +**Parameters:** + +| Name | Type | Description | +|----------------|--------------------------------------|-----------------------------| +| customFixtures | TestType | Custom fixtures to merge. | + +**Returns:** +`TestType` – The merged test type. + +**Example:** + +```typescript +import { mergeFixtures } from '@coinbase/onchaintestkit'; +import { test as base } from '@playwright/test'; + +const test = mergeFixtures(base.extend({ + myFixture: async ({}, use) => { + await use('value'); + }, +})); + +test('uses custom fixture', async ({ myFixture }) => { + expect(myFixture).toBe('value'); +}); +``` + +--- + +#### `removeTempDir(dirPath: string): Promise` + +Removes a temporary directory and logs a warning if cleanup fails. + +**Parameters:** + +| Name | Type | Description | +|---------|--------|--------------------------------| +| dirPath | string | Path to the directory to remove.| + +**Returns:** +`Promise` – Returns `null` if successful, or the error if cleanup failed. + +**Example:** + +```typescript +import { removeTempDir } from '@coinbase/onchaintestkit'; + +const error = await removeTempDir('/tmp/mytest-abc123'); +if (error) { + console.warn('Cleanup failed:', error); +} +``` + +--- + +## LocalNodeManager + +The `LocalNodeManager` class provides a comprehensive interface for managing local Anvil Ethereum nodes during testing. It is essential for deterministic, parallelizable blockchain testing. + +### Features + +- **Node Lifecycle:** Start and stop Anvil nodes programmatically. +- **State Management:** Create and revert snapshots, reset chain state. +- **Time Control:** Advance time, mine blocks. +- **Account Management:** Set balances, impersonate accounts. +- **Network Configuration:** Set gas price, chain ID. +- **Parallelization:** Automatic, cross-process port allocation for parallel test runs. + +--- + +### Mermaid Diagram: Node Lifecycle + +```mermaid +flowchart TD + StartNode([Start Node]) + AllocatePort([Allocate Port]) + LaunchAnvil([Launch Anvil Process]) + Ready([Node Ready]) + StopNode([Stop Node]) + Cleanup([Cleanup Resources]) + + StartNode --> AllocatePort + AllocatePort --> LaunchAnvil + LaunchAnvil --> Ready + StopNode --> Cleanup +``` + +--- + +### Example Usage + +```typescript +import { LocalNodeManager } from '@coinbase/onchaintestkit'; + +const nodeManager = new LocalNodeManager({ + chainId: 84532, + mnemonic: process.env.E2E_TEST_SEED_PHRASE, + // Optional: portRange: [10000, 20000] +}); + +await nodeManager.start(); +const port = nodeManager.getPort(); +console.log(`Node running on port ${port}`); + +// ...run tests... + +await nodeManager.stop(); +``` + +--- + +### API Table + +| Method | Description | +|----------------|---------------------------------------------------------------------------------------------| +| `start()` | Starts the Anvil node, automatically allocating an available port. | +| `stop()` | Stops the Anvil node and cleans up resources. | +| `getPort()` | Returns the port the node is running on. | +| `snapshot()` | Creates a blockchain state snapshot. | +| `revert()` | Reverts to the last snapshot. | +| `setBalance()` | Sets the balance of an account. | +| `impersonateAccount()` | Starts impersonating an account. | +| `setGasPrice()`| Sets the gas price for the node. | +| ... | ...and more (see full class documentation). | + +--- + +### Parallel Test Execution + +**@coinbase/onchaintestkit** enables parallel Playwright test execution by ensuring each test process gets a unique Anvil node and port. + +#### Mermaid Diagram: Parallel Node Allocation + +```mermaid +flowchart LR + subgraph TestProcesses + A1[Test File 1] --> N1[Node 1] + A2[Test File 2] --> N2[Node 2] + A3[Test File 3] --> N3[Node 3] + end + N1 -.->|Port 10001| Anvil + N2 -.->|Port 10002| Anvil + N3 -.->|Port 10003| Anvil +``` + +--- + +### Best Practices + +- Always call `stop()` on your `LocalNodeManager` to free resources. +- Use `test.afterEach()` to ensure cleanup even on test failure. +- Use snapshots for efficient state resets between test steps. +- Configure a sufficiently large port range for your expected parallelism. + +--- + +## Example: Full E2E Test with Wallet and Node + +```typescript +import { createOnchainTest, LocalNodeManager } from '@coinbase/onchaintestkit'; +import { configure } from '@coinbase/onchaintestkit'; +import { baseSepolia } from 'viem/chains'; + +const nodeManager = new LocalNodeManager({ + chainId: baseSepolia.id, + mnemonic: process.env.E2E_TEST_SEED_PHRASE, +}); +await nodeManager.start(); + +const walletConfig = configure() + .withMetaMask() + .withSeedPhrase({ + seedPhrase: process.env.E2E_TEST_SEED_PHRASE ?? '', + password: 'PASSWORD', + }) + .withNetwork({ + name: baseSepolia.name, + rpcUrl: `http://localhost:${nodeManager.getPort()}`, + chainId: baseSepolia.id, + symbol: baseSepolia.nativeCurrency.symbol, + }) + .build(); + +const test = createOnchainTest(walletConfig); + +test('connect wallet and swap', async ({ page, metamask }) => { + // ...test logic as shown in the overview... +}); + +await nodeManager.stop(); +``` + +--- + +## Variables + +| Variable | Description | +|-----------------------|-----------------------------------------------------------| +| `DEFAULT_PASSWORD` | Default password for wallet seed phrase import. | +| `DEFAULT_SEED_PHRASE` | Seed phrase for test wallet, typically from env variable. | +| `metamaskWalletConfig`| Example wallet configuration using MetaMask. | + +--- + +## Events + +This toolkit is primarily function- and class-based; it does not expose custom event emitters. All asynchronous actions (such as node start/stop, wallet actions) return Promises and should be awaited. + +--- + +## Summary + +**@coinbase/onchaintestkit** is an essential toolkit for robust, parallelizable, and deterministic E2E testing of blockchain applications. By abstracting away the complexity of wallet automation, local node management, and Playwright integration, it empowers teams to deliver high-quality dApps with confidence. + +For more advanced usage, see the full API documentation and explore the example test suites included in the repository. + +--- \ No newline at end of file diff --git a/docs/onchaintestkit/wallets/wallets.mdx b/docs/onchaintestkit/wallets/wallets.mdx new file mode 100644 index 0000000..10acb56 --- /dev/null +++ b/docs/onchaintestkit/wallets/wallets.mdx @@ -0,0 +1,561 @@ +## Overview + +**@coinbase/onchaintestkit** is a comprehensive end-to-end testing framework designed for blockchain applications. It provides robust, type-safe abstractions for automating wallet interactions, network management, and common blockchain testing scenarios. Built on top of [Playwright](https://playwright.dev/), it enables developers to write reliable, maintainable, and parallelizable tests for dApps that interact with wallets like MetaMask and Coinbase Wallet. + +### Why is it important? + +- **Automates Real User Flows:** Simulates real-world wallet interactions, including onboarding, network switching, transaction approvals, and more. +- **Parallel Test Execution:** Supports parallel test runs with isolated local blockchain nodes, ensuring fast and reliable CI/CD pipelines. +- **Type Safety and Extensibility:** Written in TypeScript with extensible APIs for custom wallet actions and network configurations. +- **Production-Grade Reliability:** Handles edge cases like notification popups, passkey authentication, and extension state management. + +--- + +## 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. + +--- + +## Quick Start + +### 1. Install + +```bash +npm install --save-dev @playwright/test @coinbase/onchaintestkit +``` + +### 2. Configure Environment + +```env +E2E_TEST_SEED_PHRASE="your test wallet seed phrase" +``` + +### 3. Create Wallet Configuration + +```typescript +import { configure } from '@coinbase/onchaintestkit'; +import { baseSepolia } from 'viem/chains'; + +export const metamaskWalletConfig = configure() + .withMetaMask() + .withSeedPhrase({ + seedPhrase: process.env.E2E_TEST_SEED_PHRASE ?? '', + password: 'PASSWORD', + }) + .withNetwork({ + name: baseSepolia.name, + rpcUrl: baseSepolia.rpcUrls.default.http[0], + chainId: baseSepolia.id, + symbol: baseSepolia.nativeCurrency.symbol, + }) + .build(); +``` + +### 4. Write a Test + +```typescript +import { metamaskWalletConfig } from './walletConfig/metamaskWalletConfig'; +import { createOnchainTest } from '@coinbase/onchaintestkit'; + +const test = createOnchainTest(metamaskWalletConfig); +const { expect } = test; + +test('connect wallet and swap', async ({ page, metamask }) => { + if (!metamask) throw new Error('MetaMask fixture is required'); + + await page.getByTestId('ockConnectButton').click(); + await page.getByTestId('ockModalOverlay').first().getByRole('button', { name: 'MetaMask' }).click(); + await metamask.handleAction('CONNECT_TO_DAPP'); + await page.getByTestId('tos-accept-button').click(); + + await page.locator('input[placeholder="0.0"]').first().fill('0.0001'); + await page.getByRole('button', { name: 'Swap' }).click(); + await page.getByRole('button', { name: 'Confirm' }).click(); + + let notificationType = await metamask.identifyNotificationType(); + + if (notificationType === 'SpendingCap') { + await metamask.handleAction('CHANGE_SPENDING_CAP', { approvalType: 'APPROVE' }); + } + + notificationType = await metamask.identifyNotificationType(); + + if (notificationType === 'SpendingCap') { + await metamask.handleAction('HANDLE_SIGNATURE', { approvalType: 'APPROVE' }); + } + + notificationType = await metamask.identifyNotificationType(); + + if (notificationType === 'Transaction') { + await metamask.handleAction('HANDLE_TRANSACTION', { approvalType: 'APPROVE' }); + } + + await expect(page.getByRole('link', { name: 'View on Explorer' })).toBeVisible(); +}); +``` + +--- + +## Configuration Builder + +The toolkit uses a fluent builder pattern to configure wallets and networks: + +```typescript +const config = configure() + .withMetaMask() + .withSeedPhrase({ + seedPhrase: 'your seed phrase', + password: 'your password', + }) + .withNetwork({ + name: 'Network Name', + rpcUrl: 'RPC URL', + chainId: 1, + symbol: 'ETH', + }) + .build(); +``` + +#### Builder Methods + +| Method | Description | +|-----------------------|---------------------------------------------| +| `withMetaMask()` | Use MetaMask wallet | +| `withCoinbase()` | Use Coinbase Wallet | +| `withSeedPhrase()` | Set wallet seed phrase and password | +| `withNetwork()` | Set network parameters | +| `withCustomSetup()` | Add custom wallet setup logic | + +--- + +## Common Wallet Actions + +### Base Actions (Supported by Both 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", +} +``` + +### Notification Types + +```typescript +enum NotificationPageType { + SpendingCap = "spending-cap", + Signature = "signature", + Transaction = "transaction", + RemoveSpendCap = "remove-spend-cap", +} +``` + +### Approval Types + +```typescript +enum ActionApprovalType { + APPROVE = "approve", + REJECT = "reject", +} +``` + +--- + +## MetaMask-Specific Features + +### MetaMask-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 +} +``` + +### MetaMask Example: Advanced Account Management + +```typescript +import { configure, createOnchainTest, MetaMaskSpecificActionType } from '@coinbase/onchaintestkit'; + +const config = configure() + .withMetaMask() + .withSeedPhrase({ seedPhrase: 'test test ...', password: 'testpass' }) + .build(); + +const test = createOnchainTest(config); + +test('MetaMask account management', async ({ page, metamask }) => { + // Add a new account + await metamask.handleAction(MetaMaskSpecificActionType.ADD_ACCOUNT, { + accountName: 'Trading Account', + }); + + // Switch between accounts + await metamask.handleAction(MetaMaskSpecificActionType.SWITCH_ACCOUNT, { + accountName: 'Trading Account', + }); + + // Add a custom network + await metamask.handleAction(MetaMaskSpecificActionType.ADD_NETWORK, { + network: { + name: 'Custom Network', + rpcUrl: 'https://rpc.custom.network', + chainId: 12345, + symbol: 'CUSTOM', + }, + }); + + // Handle network addition approval popup + await metamask.handleAction(MetaMaskSpecificActionType.APPROVE_ADD_NETWORK, { + approvalType: 'APPROVE', + }); +}); +``` + +### MetaMask Network Switching + +```typescript +test('MetaMask network switching', async ({ page, metamask }) => { + // Switch to a specific network + await metamask.handleAction('SWITCH_NETWORK', { + networkName: 'Base Sepolia', + isTestnet: true, + }); +}); +``` + +--- + +## Coinbase Wallet-Specific Features + +### Coinbase Wallet-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 + +Coinbase Wallet supports passkey authentication for enhanced security. The toolkit provides automatic handling of WebAuthn prompts: + +```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 +} +``` + +### Coinbase Wallet Example: Passkey Authentication + +```typescript +import { configure, createOnchainTest, CoinbaseSpecificActionType } from '@coinbase/onchaintestkit'; + +const config = configure() + .withCoinbase() + .withSeedPhrase({ seedPhrase: 'test test ...', password: 'testpass' }) + .build(); + +const test = createOnchainTest(config); + +test('Coinbase Wallet with passkey', async ({ page, coinbase }) => { + // Handle passkey registration + const [popup] = await Promise.all([ + page.context().waitForEvent('page'), + page.getByTestId('register-passkey').click(), + ]); + + 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, + }, + }); + + // Later, handle passkey approval for transactions + const [txPopup] = await Promise.all([ + page.context().waitForEvent('page'), + page.getByRole('button', { name: 'Send Transaction' }).click(), + ]); + + await coinbase.handleAction(CoinbaseSpecificActionType.HANDLE_PASSKEY_POPUP, { + mainPage: page, + popup: txPopup, + passkeyAction: 'approve', + }); +}); +``` + +### Coinbase Wallet Account Management + +```typescript +test('Coinbase Wallet accounts', async ({ page, coinbase }) => { + // Add a new account + await coinbase.handleAction(CoinbaseSpecificActionType.ADD_ACCOUNT, { + accountName: 'DeFi Account', + }); + + // Switch between accounts + await coinbase.handleAction(CoinbaseSpecificActionType.SWITCH_ACCOUNT, { + accountName: 'DeFi Account', + }); + + // Add a custom network + await coinbase.handleAction(CoinbaseSpecificActionType.ADD_NETWORK, { + network: { + name: 'Base Mainnet', + rpcUrl: 'https://mainnet.base.org', + chainId: 8453, + symbol: 'ETH', + }, + }); +}); +``` + +--- + +## Advanced Features + +### Detecting Notification Types + +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('HANDLE_TRANSACTION', { approvalType: 'APPROVE' }); + break; + case 'SpendingCap': + await wallet.handleAction('CHANGE_SPENDING_CAP', { approvalType: 'APPROVE' }); + break; + case 'Signature': + await wallet.handleAction('HANDLE_SIGNATURE', { approvalType: 'APPROVE' }); + break; +} +``` + +### Custom Wallet Setup + +Both wallets support custom setup logic via the configuration builder: + +```typescript +const config = configure() + .withMetaMask() + .withSeedPhrase({ seedPhrase: '...', password: '...' }) + .withCustomSetup(async (wallet, context) => { + // Import additional accounts + await wallet.handleAction('IMPORT_WALLET_FROM_PRIVATE_KEY', { + privateKey: '0xabc...', + }); + + // Add custom tokens + await wallet.handleAction(MetaMaskSpecificActionType.ADD_TOKEN, { + tokenAddress: '0x...', + tokenSymbol: 'USDC', + tokenDecimals: 6, + }); + }) + .build(); +``` + +--- + +## API Reference + +### Classes + +| Class | Description | +|-----------------------|------------------------------------------------------------------| +| `MetaMask` | MetaMask wallet automation class | +| `CoinbaseWallet` | Coinbase Wallet automation class | +| `BaseWallet` | Abstract base class for wallet implementations | +| `PasskeyAuthenticator`| WebAuthn virtual authenticator for Coinbase Wallet | + +### Methods + +#### Common Wallet Methods + +| Method | Description | +|-------------------------------------|------------------------------------------------------------------| +| `handleAction(action, options)` | Handles a wallet action (connect, switch network, etc.) | +| `identifyNotificationType()` | Detects the current notification type in the wallet UI | + +#### MetaMask-Specific Methods + +| Method | Description | +|-------------------------------------|------------------------------------------------------------------| +| `static initialize()` | Creates MetaMask context and returns page/context | +| `static createContext()` | Creates browser context with MetaMask extension | + +#### Coinbase Wallet-Specific Methods + +| Method | Description | +|-------------------------------------|------------------------------------------------------------------| +| `static initialize()` | Creates Coinbase Wallet context and returns page/context | +| `static createContext()` | Creates browser context with Coinbase extension | +| `handlePasskeyPopup()` | Handles WebAuthn/Passkey authentication flows | + +### Types + +| Type | Description | +|-------------------------|------------------------------------------------------------------| +| `NetworkConfig` | Network configuration object | +| `ActionOptions` | Options for wallet actions (e.g., approvalType) | +| `WalletSetupContext` | Context for wallet setup (e.g., localNodePort) | +| `MetaMaskConfig` | MetaMask wallet configuration | +| `CoinbaseConfig` | Coinbase Wallet configuration | +| `PasskeyConfig` | Configuration for passkey authentication | + +--- + +## LocalNodeManager + +The `LocalNodeManager` provides an interface for managing local Ethereum nodes for testing. + +### Features + +- **Automatic Port Allocation:** Ensures each test gets a unique port for its node. +- **Chain State Manipulation:** Snapshots, reverts, resets, time travel, and block mining. +- **Account Management:** Set balances, impersonate accounts. +- **Parallelization:** Designed for multi-worker Playwright test runs. + +### Usage + +```typescript +import { LocalNodeManager } from '@coinbase/onchaintestkit'; + +const nodeManager = new LocalNodeManager({ + chainId: 84532, + mnemonic: process.env.E2E_TEST_SEED_PHRASE, +}); + +await nodeManager.start(); +const port = nodeManager.getPort(); +console.log(`Node running on port ${port}`); +// ... run tests +await nodeManager.stop(); +``` + +### Parallel Test Execution + +```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 +``` + +--- + +## 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. + +--- + +## Summary + +**@coinbase/onchaintestkit** provides comprehensive wallet automation for both MetaMask and Coinbase Wallet, each with their unique features: + +- **MetaMask**: Rich account management, network customization, and transaction handling +- **Coinbase Wallet**: Passkey/WebAuthn support, similar core features to MetaMask + +The toolkit abstracts away the complexity of wallet automation, local node management, and parallel test execution, enabling you to focus on building robust, production-ready dApps. + +For more examples and advanced usage, see the [project repository](https://github.com/coinbase/onchaintestkit) and the `e2e/` directory. + +--- \ No newline at end of file From 2a7badc9160c98fc2be9bfb6b795eb83274e84d9 Mon Sep 17 00:00:00 2001 From: Aushveen Vimalathas Date: Mon, 30 Jun 2025 23:34:48 -0700 Subject: [PATCH 2/2] onchaintestkit docs --- docs/docs.json | 78 +++ docs/onchaintestkit/ci-cd.mdx | 148 +++++ docs/onchaintestkit/configuration.mdx | 286 +++++++++ docs/onchaintestkit/contracts/contracts.mdx | 309 --------- docs/onchaintestkit/contracts/overview.mdx | 166 +++++ .../contracts/proxy-deployer.mdx | 150 +++++ .../contracts/smart-contract-manager.mdx | 224 +++++++ docs/onchaintestkit/installation.mdx | 245 +++++++ docs/onchaintestkit/node/api-reference.mdx | 359 +++++++++++ docs/onchaintestkit/node/configuration.mdx | 246 +++++++ docs/onchaintestkit/node/node.mdx | 475 -------------- docs/onchaintestkit/node/overview.mdx | 154 +++++ docs/onchaintestkit/overview.mdx | 107 ++++ docs/onchaintestkit/quickstart.mdx | 180 ++++++ docs/onchaintestkit/root/root.mdx | 395 ------------ docs/onchaintestkit/smart-contracts.mdx | 600 ++++++++++++++++++ docs/onchaintestkit/utils/utils.mdx | 323 ---------- docs/onchaintestkit/wallets/api-reference.mdx | 394 ++++++++++++ docs/onchaintestkit/wallets/coinbase.mdx | 373 +++++++++++ .../onchaintestkit/wallets/common-actions.mdx | 289 +++++++++ docs/onchaintestkit/wallets/metamask.mdx | 303 +++++++++ docs/onchaintestkit/wallets/overview.mdx | 108 ++++ docs/onchaintestkit/wallets/wallets.mdx | 561 ---------------- docs/onchaintestkit/writing-tests.mdx | 221 +++++++ 24 files changed, 4631 insertions(+), 2063 deletions(-) create mode 100644 docs/onchaintestkit/ci-cd.mdx create mode 100644 docs/onchaintestkit/configuration.mdx delete mode 100644 docs/onchaintestkit/contracts/contracts.mdx create mode 100644 docs/onchaintestkit/contracts/overview.mdx create mode 100644 docs/onchaintestkit/contracts/proxy-deployer.mdx create mode 100644 docs/onchaintestkit/contracts/smart-contract-manager.mdx create mode 100644 docs/onchaintestkit/installation.mdx create mode 100644 docs/onchaintestkit/node/api-reference.mdx create mode 100644 docs/onchaintestkit/node/configuration.mdx delete mode 100644 docs/onchaintestkit/node/node.mdx create mode 100644 docs/onchaintestkit/node/overview.mdx create mode 100644 docs/onchaintestkit/overview.mdx create mode 100644 docs/onchaintestkit/quickstart.mdx delete mode 100644 docs/onchaintestkit/root/root.mdx create mode 100644 docs/onchaintestkit/smart-contracts.mdx delete mode 100644 docs/onchaintestkit/utils/utils.mdx create mode 100644 docs/onchaintestkit/wallets/api-reference.mdx create mode 100644 docs/onchaintestkit/wallets/coinbase.mdx create mode 100644 docs/onchaintestkit/wallets/common-actions.mdx create mode 100644 docs/onchaintestkit/wallets/metamask.mdx create mode 100644 docs/onchaintestkit/wallets/overview.mdx delete mode 100644 docs/onchaintestkit/wallets/wallets.mdx create mode 100644 docs/onchaintestkit/writing-tests.mdx 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/contracts.mdx b/docs/onchaintestkit/contracts/contracts.mdx deleted file mode 100644 index 1df699f..0000000 --- a/docs/onchaintestkit/contracts/contracts.mdx +++ /dev/null @@ -1,309 +0,0 @@ -# Smart Contract Manager - -> **Package:** [`@coinbase/onchaintestkit`](https://www.npmjs.com/package/@coinbase/onchaintestkit) - ---- - -## Overview - -The smart contract manager in `@coinbase/onchaintestkit` provide 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. - -Key features include: - -- **Deterministic Contract Deployment:** Deploy contracts at predictable addresses using the CREATE2 opcode and a deterministic deployment proxy. -- **Automated Proxy Management:** Ensure the deterministic deployment proxy is present and deployed as needed. -- **Contract State Management:** Deploy contracts and execute function calls as part of test setup, supporting complex test scenarios. -- **Parallel Test Safety:** Designed to work with the `LocalNodeManager` for parallel test execution, ensuring isolated and reproducible blockchain state. -- **Artifact Integration:** Loads contract ABIs and bytecode directly from Foundry build artifacts. - -This toolkit is essential for blockchain application developers and QA engineers who require reliable, reproducible, and scalable E2E testing of smart contract workflows. - ---- - -## 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 - -### 1. ProxyDeployer - -A utility class to manage the deterministic deployment proxy contract, which enables CREATE2-based deployments at predictable addresses. - -### 2. SmartContractManager - -A high-level manager for deploying contracts (using CREATE2), executing contract calls, and orchestrating contract state for tests. - ---- - -## API Reference - -### ProxyDeployer - -#### Description - -Handles deployment and verification of the deterministic deployment proxy contract. This proxy is required for CREATE2-based deterministic contract deployments. - -#### Constructor - -```typescript -new ProxyDeployer(node: LocalNodeManager) -``` - -- **node**: `LocalNodeManager` — The local Ethereum node manager instance. - -#### Methods - -| Method | Description | Returns | -|---------------------------|----------------------------------------------------------------------------------------------|------------------------| -| `isProxyDeployed()` | Checks if the deterministic deployment proxy is already deployed. | `Promise` | -| `ensureProxyDeployed()` | Deploys the proxy if not already present. | `Promise` | -| `getProxyAddress()` | Returns the address of the deterministic deployment proxy. | `Address` | - -#### Variables - -| Variable | Description | Type | -|---------------------|--------------------------------------------------------------------------------------------------|--------------| -| `PROXY_DEPLOYMENT_TX` | Raw transaction data for deploying the proxy. | `string` | -| `PROXY_ADDRESS` | The fixed address at which the proxy is deployed. | `Address` | -| `publicClient` | viem public client for interacting with the node. | `PublicClient`| -| `rpcUrl` | RPC URL of the local node. | `string` | - -#### Example - -```typescript -import { LocalNodeManager } from '@coinbase/onchaintestkit/node/LocalNodeManager'; -import { ProxyDeployer } from '@coinbase/onchaintestkit/contracts/ProxyDeployer'; - -const node = new LocalNodeManager({ chainId: 31337, mnemonic: '...' }); -await node.start(); - -const proxyDeployer = new ProxyDeployer(node); -await proxyDeployer.ensureProxyDeployed(); - -console.log('Proxy deployed at:', proxyDeployer.getProxyAddress()); -``` - ---- - -### SmartContractManager - -#### Description - -Manages smart contract deployments and interactions for E2E tests, supporting deterministic deployments via CREATE2 and orchestrating contract state setup. - -#### Constructor - -```typescript -new SmartContractManager(projectRoot: string) -``` - -- **projectRoot**: `string` — Path to the root of the project (used to locate contract artifacts). - -#### Methods - -| Method | Description | Returns | -|------------------------------|------------------------------------------------------------------------------------------------------|------------------------| -| `initialize(node)` | Initializes viem clients and ensures the proxy is deployed. | `Promise` | -| `deployContract(deployment)` | Deploys a contract using CREATE2 and stores its ABI. | `Promise
` | -| `executeCall(call)` | Executes a contract function call as a transaction. | `Promise` | -| `setContractState(config, node)` | Deploys contracts and executes calls as specified in a setup config. | `Promise` | -| `predictContractAddress(salt, bytecode, args)` | Predicts the address for a CREATE2 deployment. | `Address` | - -#### Variables - -| Variable | Description | Type | -|-----------------------|-----------------------------------------------------------------------------|----------------------------| -| `projectRoot` | Root directory for artifact lookup. | `string` | -| `publicClient` | viem public client (initialized in `initialize`). | `PublicClient` | -| `walletClient` | viem wallet client (initialized in `initialize`). | `WalletClient` | -| `deployedContracts` | Map of deployed contract addresses to their ABIs. | `Map` | -| `proxyDeployer` | Instance of `ProxyDeployer`. | `ProxyDeployer` | - -#### Example - -```typescript -import { LocalNodeManager } from '@coinbase/onchaintestkit/node/LocalNodeManager'; -import { SmartContractManager } from '@coinbase/onchaintestkit/contracts/SmartContractManager'; - -const node = new LocalNodeManager({ chainId: 31337, mnemonic: '...' }); -await node.start(); - -const scm = new SmartContractManager(process.cwd()); -await scm.initialize(node); - -const contractAddress = await scm.deployContract({ - name: 'MyContract', - args: [42, 'hello'], - salt: '0x1234...abcd', - deployer: '0xabc...def', -}); - -const txHash = await scm.executeCall({ - target: contractAddress, - functionName: 'setValue', - args: [100], - account: '0xabc...def', -}); -``` - ---- - -### Types - -#### `ContractDeployment` - -| Field | Type | Description | -|-----------|-------------------|------------------------------------------------| -| `name` | `string` | Contract name (matches artifact name). | -| `args` | `readonly unknown[]` | Constructor arguments. | -| `salt` | `0x${string}` | Salt for CREATE2 deployment (32 bytes). | -| `deployer`| `0x${string}` | Account deploying the contract. | - -#### `ContractCall` - -| Field | Type | Description | -|----------------|----------------------|---------------------------------------------| -| `target` | `0x${string}` | Contract address. | -| `functionName` | `string` | Function to call. | -| `args` | `readonly unknown[]` | Arguments for the function. | -| `account` | `0x${string}` | Account making the call. | -| `value` | `bigint` (optional) | ETH value to send. | - -#### `SetupConfig` - -| Field | Type | Description | -|---------------|--------------------------|---------------------------------------------| -| `deployments` | `ContractDeployment[]` | Contracts to deploy. | -| `calls` | `ContractCall[]` | Calls to execute after deployment. | - -#### `ContractArtifact` - -| Field | Type | Description | -|-----------|-------------------|------------------------------------------------| -| `abi` | `readonly unknown[]` | Contract ABI. | -| `bytecode`| `0x${string}` | Contract bytecode. | - ---- - -## Example: Full Contract State Setup - -```typescript -import { LocalNodeManager } from '@coinbase/onchaintestkit/node/LocalNodeManager'; -import { SmartContractManager } from '@coinbase/onchaintestkit/contracts/SmartContractManager'; - -const node = new LocalNodeManager({ chainId: 31337, mnemonic: '...' }); -await node.start(); - -const scm = new SmartContractManager(process.cwd()); -await scm.initialize(node); - -await scm.setContractState({ - deployments: [ - { - name: 'Token', - args: ['Test Token', 'TTK', 18], - salt: '0x0000000000000000000000000000000000000000000000000000000000000001', - deployer: '0xabc...def', - }, - ], - calls: [ - { - target: '0x1234...abcd', - functionName: 'mint', - args: ['0xabc...def', 1000n], - account: '0xabc...def', - }, - ], -}, node); -``` - ---- - -## Sequence Diagram: Deterministic Deployment - -```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() -``` - ---- - -## Events - -These classes do not emit Node.js events, but the following actions are observable in the test process: - -| Event/Action | Description | -|--------------------------------|----------------------------------------------------------------| -| Proxy deployed | Proxy contract is deployed to the node. | -| Contract deployed | Contract is deployed at deterministic address. | -| Contract call executed | Function call is executed as a transaction. | -| Contract state setup complete | All deployments and calls in `setContractState` are complete. | - ---- - -## 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. - ---- - -## See Also - -- [`LocalNodeManager`](../node/node.mdx): For managing local Ethereum nodes and state. -- [`@coinbase/onchaintestkit`](https://www.npmjs.com/package/@coinbase/onchaintestkit): Main package documentation. - ---- - -## Summary Table - -| Component | Purpose | Key Methods | -|-----------------------|-----------------------------------------------------|----------------------------------| -| ProxyDeployer | Deploys/ensures deterministic deployment proxy | isProxyDeployed, ensureProxyDeployed, getProxyAddress | -| SmartContractManager | Manages contract deployment and calls for tests | initialize, deployContract, executeCall, setContractState | - ---- - -## Further Example: Predicting Contract Address - -```typescript -const predicted = scm.predictContractAddress( - '0x0000...0001', - '0x600060...', // bytecode - [42, 'hello'] -); -console.log('Predicted address:', predicted); -``` \ 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/node.mdx b/docs/onchaintestkit/node/node.mdx deleted file mode 100644 index a817316..0000000 --- a/docs/onchaintestkit/node/node.mdx +++ /dev/null @@ -1,475 +0,0 @@ -# LocalNodeManager API Documentation - -> **Package:** `@coinbase/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 -``` - ---- - -## Usage Example - -```typescript -import { LocalNodeManager } from '@coinbase/onchaintestkit' - -// 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", -}) - -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) - -await node.stop() -``` - ---- - -## API Reference - -### Class: `LocalNodeManager` - -#### Description - -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 - -| Signature | Description | -|-----------|-------------| -| `new LocalNodeManager(config?: NodeConfig)` | Creates a new instance with the specified configuration. | - -#### Parameters - -| Name | Type | Description | -|--------|-------------|-------------| -| config | `NodeConfig` | Optional. Node configuration options (see [NodeConfig](#type-nodeconfig)). | - ---- - -### Methods - -| Method | Description | -|--------|-------------| -| [`start()`](#start) | Starts the Anvil node with the configured options. | -| [`stop()`](#stop) | Stops the running Anvil node and cleans up resources. | -| [`getPort()`](#getport) | Returns the allocated port number for this node instance. | -| [`snapshot()`](#snapshot) | Takes a snapshot of the current chain state. | -| [`revert(snapshotId)`](#revert) | Reverts the chain state to a previous snapshot. | -| [`reset(forkBlock?)`](#reset) | Resets the chain state to initial state or specified fork block. | -| [`mine(blocks?)`](#mine) | Mines a specified number of blocks. | -| [`setAutomine(enabled)`](#setautomine) | Enables or disables automatic block mining. | -| [`setNextBlockTimestamp(timestamp)`](#setnextblocktimestamp) | Sets the timestamp for the next block. | -| [`increaseTime(seconds)`](#increasetime) | Increases chain time by specified seconds. | -| [`setTime(timestamp)`](#settime) | Sets absolute chain time. | -| [`getAccounts()`](#getaccounts) | Gets list of available accounts. | -| [`setBalance(address, balance)`](#setbalance) | Sets balance for specified address. | -| [`setNonce(address, nonce)`](#setnonce) | Sets nonce for specified address. | -| [`setCode(address, code)`](#setcode) | Sets contract code at specified address. | -| [`setStorageAt(address, slot, value)`](#setstorageat) | Sets storage value at specified slot. | -| [`setNextBlockBaseFeePerGas(fee)`](#setnextblockbasefeepergas) | Sets base fee for next block (EIP-1559). | -| [`setMinGasPrice(price)`](#setmingasprice) | Sets minimum gas price. | -| [`setChainId(chainId)`](#setchainid) | Sets chain ID. | -| [`impersonateAccount(address)`](#impersonateaccount) | Enables impersonation of specified account. | -| [`stopImpersonatingAccount(address)`](#stopimpersonatingaccount) | Disables impersonation of specified account. | - ---- - -#### Method Details - -##### start - -```typescript -async start(): Promise -``` -Starts the Anvil node with the configured options. Allocates a port, spawns the process, and waits for readiness. - ---- - -##### stop - -```typescript -async stop(): Promise -``` -Stops the running Anvil node and cleans up resources. - ---- - -##### getPort - -```typescript -getPort(): number | null -``` -Returns the allocated port number for this node instance, or `null` if not started. - ---- - -##### snapshot - -```typescript -async snapshot(): Promise -``` -Takes a snapshot of the current chain state. Returns a snapshot ID for later use with `revert()`. - ---- - -##### revert - -```typescript -async revert(snapshotId: string): Promise -``` -Reverts the chain state to a previous snapshot. - -- `snapshotId`: 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. - -- `forkBlock`: Optional block number to reset to (when in fork mode). - ---- - -##### mine - -```typescript -async mine(blocks = 1): Promise -``` -Mines a specified number of blocks (default: 1). - ---- - -##### setAutomine - -```typescript -async setAutomine(enabled: boolean): Promise -``` -Enables or disables automatic block mining. - ---- - -##### setNextBlockTimestamp - -```typescript -async setNextBlockTimestamp(timestamp: number): Promise -``` -Sets the timestamp for the next block (Unix seconds). - ---- - -##### increaseTime - -```typescript -async increaseTime(seconds: number): Promise -``` -Increases chain time by the specified number of seconds. - ---- - -##### setTime - -```typescript -async setTime(timestamp: number): Promise -``` -Sets absolute chain time (Unix seconds). - ---- - -##### getAccounts - -```typescript -async getAccounts(): Promise -``` -Returns an array of available account addresses. - ---- - -##### setBalance - -```typescript -async setBalance(address: string, balance: bigint): Promise -``` -Sets the balance for the specified address (in wei). - ---- - -##### setNonce - -```typescript -async setNonce(address: string, nonce: number): Promise -``` -Sets the nonce for the specified address. - ---- - -##### setCode - -```typescript -async setCode(address: string, code: string): Promise -``` -Sets the contract code at the specified address. - ---- - -##### setStorageAt - -```typescript -async setStorageAt(address: string, slot: string, value: string): Promise -``` -Sets the storage value at the specified slot for a contract. - ---- - -##### setNextBlockBaseFeePerGas - -```typescript -async setNextBlockBaseFeePerGas(fee: bigint): Promise -``` -Sets the base fee for the next block (EIP-1559). - ---- - -##### setMinGasPrice - -```typescript -async setMinGasPrice(price: bigint): Promise -``` -Sets the minimum gas price. - ---- - -##### setChainId - -```typescript -async setChainId(chainId: number): Promise -``` -Sets the chain ID. - ---- - -##### impersonateAccount - -```typescript -async impersonateAccount(address: string): Promise -``` -Enables impersonation of the specified account. - ---- - -##### stopImpersonatingAccount - -```typescript -async stopImpersonatingAccount(address: string): Promise -``` -Disables impersonation of the specified account. - ---- - -### Properties - -| Name | Type | Description | -|------|------|-------------| -| `port` | `number` | The allocated port number, or -1 if not started. | -| `rpcUrl` | `string` | The RPC URL for the running node (e.g., `http://localhost:12345`). | - ---- - -### Type: `NodeConfig` - -Configuration options for `LocalNodeManager`. - -| Property | Type | Description | -|----------|------|-------------| -| `port` | `number` | Port number for RPC server (optional). | -| `portRange` | `[number, number]` | Port range for automatic port selection (optional). | -| `chainId` | `number` | Chain ID for the network (default: 84532). | -| `mnemonic` | `string` | Mnemonic for the network (optional). | -| `forkUrl` | `string` | URL to fork from (e.g., mainnet) (optional). | -| `forkBlockNumber` | `bigint` | Block number to fork from (optional). | -| `forkRetryInterval` | `number` | Retry interval for fork requests (optional). | -| `defaultBalance` | `bigint` | Default balance for test accounts (optional). | -| `totalAccounts` | `number` | Number of test accounts to generate (optional). | -| `blockTime` | `number` | Time between blocks (0 for instant, default: 0). | -| `blockGasLimit` | `bigint` | Gas limit per block (optional). | -| `noMining` | `boolean` | Disable automatic mining (optional). | -| `hardfork` | `"london" \| "berlin" \| "cancun"` | Specific hardfork to use (optional). | - ---- - -## Parallel Test Execution - -`LocalNodeManager` is designed for parallel test execution. It dynamically allocates ports for each node instance, ensuring no conflicts even across multiple processes. - -```mermaid -sequenceDiagram - participant TestWorker1 - participant TestWorker2 - participant LocalNodeManager1 - participant LocalNodeManager2 - participant System - - TestWorker1->>LocalNodeManager1: new LocalNodeManager() - LocalNodeManager1->>System: Find available port (e.g., 12001) - LocalNodeManager1->>System: Start Anvil on 12001 - - TestWorker2->>LocalNodeManager2: new LocalNodeManager() - LocalNodeManager2->>System: Find available port (e.g., 12002) - LocalNodeManager2->>System: Start Anvil on 12002 -``` - ---- - -## Advanced: RPC Port Interception - -When your dApp or test expects the RPC endpoint at `http://localhost:8545`, but your node is running on a dynamic port, use the provided utility to intercept and rewrite requests: - -```typescript -import { setupRpcPortInterceptor } from '@coinbase/onchaintestkit' -import { Page } from '@playwright/test' - -await setupRpcPortInterceptor(page, node.getPort()) -``` - -This ensures all requests to `http://localhost:8545` are transparently redirected to the correct port. - ---- - -## NodeFixturesBuilder - -For Playwright integration, use `NodeFixturesBuilder` to automatically manage node lifecycle in your test fixtures: - -```typescript -import { NodeFixturesBuilder } from '@coinbase/onchaintestkit' - -const nodeFixtures = new NodeFixturesBuilder({ - chainId: 84532, - mnemonic: process.env.E2E_TEST_SEED_PHRASE, -}).build() - -export const test = nodeFixtures -``` - ---- - -## Variables - -| Variable | Type | Description | -|----------|------|-------------| -| `DEFAULT_PORT_RANGE` | `[number, number]` | Default port range for dynamic allocation (`[10000, 20000]`). | -| `MAX_PORT_ALLOCATION_RETRIES` | `number` | Maximum retries for port allocation (`5`). | -| `process` | `ChildProcess \| null` | Reference to the running Anvil process. | -| `provider` | `ethers.providers.JsonRpcProvider \| null` | JSON-RPC provider for interacting with the node. | -| `config` | `NodeConfig` | Configuration for the node. | -| `allocatedPort` | `number \| null` | The port allocated for this node instance. | - ---- - -## Events - -| Event | Description | -|-------|-------------| -| `process.on("error", handler)` | Emitted if the Anvil process encounters an error. | -| `process.on("exit", handler)` | Emitted when the Anvil process exits. | -| `process.stdout.on("data", handler)` | Emitted when Anvil outputs to stdout (used for readiness detection). | -| `process.stderr.on("data", handler)` | Emitted when Anvil outputs to stderr (for debugging). | - ---- - -## Example: Parallel Playwright Test - -```typescript -import { test as base } from '@playwright/test' -import { NodeFixturesBuilder } from '@coinbase/onchaintestkit' - -const nodeFixtures = new NodeFixturesBuilder({ - chainId: 84532, - mnemonic: process.env.E2E_TEST_SEED_PHRASE, -}).build() - -const test = nodeFixtures - -test('runs with isolated node', async ({ node }) => { - await node.mine(5) - const accounts = await node.getAccounts() - // ...test logic... -}) -``` - ---- - -## Best Practices - -- Always call `await node.stop()` after your test to free resources. -- Use `test.afterEach()` or fixture scopes to ensure cleanup. -- Use snapshots and reverts for fast, deterministic test state. -- Use a wide port range for parallel test reliability. -- Use `setupRpcPortInterceptor` for seamless dApp RPC integration. - ---- - -## See Also - -- [@coinbase/onchaintestkit on npm](https://www.npmjs.com/package/@coinbase/onchaintestkit) -- [Playwright documentation](https://playwright.dev/) -- [Anvil documentation](https://book.getfoundry.sh/anvil/) - ---- - -**© Coinbase, Inc.** \ 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/root/root.mdx b/docs/onchaintestkit/root/root.mdx deleted file mode 100644 index 043fa6b..0000000 --- a/docs/onchaintestkit/root/root.mdx +++ /dev/null @@ -1,395 +0,0 @@ -# @coinbase/onchaintestkit - -End-to-end testing toolkit for blockchain applications, powered by Playwright. - ---- - -## Overview - -`@coinbase/onchaintestkit` is a comprehensive, type-safe framework for end-to-end testing of blockchain applications. It provides seamless integration with Playwright, enabling robust automation of browser-based wallet interactions, local blockchain node management, and common blockchain testing scenarios. The toolkit supports multiple wallets (MetaMask, Coinbase Wallet), dynamic network configuration, and parallel test execution with isolated local nodes. - -### Why is it Important? - -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. `@coinbase/onchaintestkit` automates these processes, ensuring reliability, reproducibility, and scalability for your dApp's test suite. - ---- - -## 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)"] - end - A --> B - B -- manages --> D - B -- manages --> C - D -- interacts --> C - B -- configures --> C - B -- automates --> D -``` - ---- - -## Key Features - -- **Playwright Integration**: Automate browser-based wallet and dApp interactions. -- **Wallet Support**: MetaMask and Coinbase Wallet, with extensible architecture. -- **Action Handling**: Automate connect, transaction, signature, approval, and network switching flows. -- **Network Management**: Use local Anvil nodes or remote RPC endpoints, with dynamic port allocation for parallelism. -- **Type Safety**: Full TypeScript support for all configuration and test APIs. -- **Fluent Configuration**: Builder pattern for wallet and node setup. -- **Parallel Testing**: Reliable cross-process port allocation for running multiple nodes in parallel. - ---- - -## Getting Started - -### Example: Basic Wallet Test - -```typescript -import { configure, createOnchainTest, BaseActionType } from '@coinbase/onchaintestkit'; - -const test = createOnchainTest( - 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() -); - -test('connect wallet and swap', async ({ page, metamask }) => { - await page.getByTestId('ockConnectButton').click(); - await metamask.handleAction(BaseActionType.CONNECT_TO_DAPP); - // ... further dApp and wallet interactions -}); -``` - ---- - -## Configuration Builder - -The toolkit uses a fluent builder pattern for configuration, supporting both MetaMask and Coinbase Wallet. - -### Example: Full Configuration - -```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: 'your seed phrase', - password: 'your password', - }) - .withCustomSetup(async (wallet) => { - await wallet.importToken('0x...'); - }) - .build(); -``` - -### Configuration Builder API - -| Method | Description | -|-----------------------|----------------------------------------------------------------------------------------------| -| `withMetaMask()` | Initialize MetaMask wallet configuration. | -| `withCoinbase()` | Initialize Coinbase Wallet configuration. | -| `withSeedPhrase()` | Set wallet seed phrase and optional password. | -| `withNetwork()` | Configure network (name, rpcUrl, chainId, symbol). | -| `withLocalNode()` | Configure local Anvil node (chainId, mnemonic, port range, etc.). | -| `withCustomSetup()` | Add custom async setup steps for the wallet (e.g., import tokens, set up contracts, etc.). | -| `build()` | Finalize and return the configuration object. | - ---- - -## Wallet Actions - -### Base Actions - -```typescript -enum BaseActionType { - CONNECT_TO_DAPP = 'CONNECT_TO_DAPP', - HANDLE_TRANSACTION = 'HANDLE_TRANSACTION', - HANDLE_SIGNATURE = 'HANDLE_SIGNATURE', - CHANGE_SPENDING_CAP = 'CHANGE_SPENDING_CAP', - SWITCH_NETWORK = 'SWITCH_NETWORK', - IMPORT_WALLET_FROM_SEED = 'IMPORT_WALLET_FROM_SEED', -} -``` - -### Notification Types - -```typescript -enum NotificationPageType { - Transaction = 'Transaction', - SpendingCap = 'SpendingCap', - Signature = 'Signature', -} -``` - -### Approval Types - -```typescript -enum ActionApprovalType { - APPROVE = 'APPROVE', - REJECT = 'REJECT', -} -``` - ---- - -## LocalNodeManager - -The `LocalNodeManager` class manages local Anvil Ethereum nodes for isolated, reproducible blockchain state during tests. - -### Features - -- **Lifecycle**: Start/stop nodes, get allocated port. -- **State**: Create/revert snapshots, reset chain. -- **Time**: Time travel, mine blocks. -- **Accounts**: Set balances, impersonate accounts. -- **Network**: Set gas price, chain ID. -- **Parallelism**: Dynamic port allocation for parallel test workers. - -### Example: Parallel Node Management - -```typescript -import { LocalNodeManager } from '@coinbase/onchaintestkit'; - -const nodeManager = new LocalNodeManager({ - chainId: 84532, - mnemonic: process.env.E2E_TEST_SEED_PHRASE, - // Optional: portRange: [10000, 20000] -}); - -await nodeManager.start(); -const port = nodeManager.getPort(); -console.log(`Node running on port ${port}`); -// ... run tests ... -await nodeManager.stop(); -``` - -#### Parallel Test Execution - -```mermaid -sequenceDiagram - participant Worker1 as Playwright Worker 1 - participant Worker2 as Playwright Worker 2 - participant Node1 as Anvil Node 1 (port 10001) - participant Node2 as Anvil Node 2 (port 10002) - Worker1->>Node1: Start node (allocates port 10001) - Worker2->>Node2: Start node (allocates port 10002) - Worker1->>Node1: Run tests - Worker2->>Node2: Run tests - Worker1->>Node1: Stop node - Worker2->>Node2: Stop node -``` - ---- - -## API Reference - -### Functions - -| Function | Description | -|--------------------------------|-------------------------------------------------------------------------------------------------------| -| `configure()` | Creates a new configuration builder instance. | -| `createOnchainTest(options)` | Creates a Playwright test instance with wallet and node fixtures based on the provided configuration. | -| `setupMetaMask()` | Utility to prepare MetaMask extension for testing. | -| `setupRpcPortInterceptor()` | Intercepts RPC requests to redirect to the correct local node port. | - -### Classes - -#### ConfigBuilder - -| Method | Description | -|------------------------|----------------------------------------------------------------------------------------------| -| `withMetaMask()` | Initialize MetaMask wallet configuration. | -| `withCoinbase()` | Initialize Coinbase Wallet configuration. | -| `withSeedPhrase()` | Set wallet seed phrase and optional password. | -| `withNetwork()` | Configure network (name, rpcUrl, chainId, symbol). | -| `withLocalNode()` | Configure local Anvil node (chainId, mnemonic, port range, etc.). | -| `withCustomSetup()` | Add custom async setup steps for the wallet. | -| `build()` | Finalize and return the configuration object. | - -#### LocalNodeManager - -| Method | Description | -|------------------|--------------------------------------------------| -| `start()` | Starts the local Anvil node. | -| `stop()` | Stops the node and releases resources. | -| `getPort()` | Returns the allocated port for the node. | -| `snapshot()` | Creates a blockchain state snapshot. | -| `revert()` | Reverts to a previously created snapshot. | -| `setBalance()` | Sets the balance of an account. | -| `impersonate()` | Impersonates an account for testing. | - ---- - -### Variables - -| Variable | Description | -|----------------------|---------------------------------------------------------------------| -| `CACHE_DIR_NAME` | Directory name for caching test artifacts. | -| `fixtureBuilderMap` | Map of wallet names to their fixture builder functions. | - ---- - -### Events - -- **Wallet Action Events**: Triggered when wallet actions are performed (e.g., connect, sign, approve). -- **Node Lifecycle Events**: Start/stop events for local nodes. -- **Network Interception Events**: When RPC requests are redirected to the correct node port. - ---- - -## Example: MetaMask Wallet Test - -```typescript -import { configure, createOnchainTest, BaseActionType } from '@coinbase/onchaintestkit'; - -const test = createOnchainTest( - 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() -); - -test('connect wallet and swap', async ({ page, metamask }) => { - await page.getByTestId('ockConnectButton').click(); - await metamask.handleAction(BaseActionType.CONNECT_TO_DAPP); - // ... further dApp and wallet interactions -}); -``` - ---- - -## Example: Coinbase Wallet Test - -```typescript -import { configure, createOnchainTest, BaseActionType } from '@coinbase/onchaintestkit'; - -const test = createOnchainTest( - configure() - .withLocalNode({ chainId: 1337 }) - .withCoinbase() - .withNetwork({ - name: 'Base Sepolia', - rpcUrl: 'http://localhost:8545', - chainId: 84532, - symbol: 'ETH', - }) - .withSeedPhrase({ - seedPhrase: process.env.E2E_TEST_SEED_PHRASE!, - password: 'PASSWORD', - }) - .build() -); - -test('connect coinbase wallet', async ({ page, coinbase }) => { - await page.getByTestId('ockConnectButton').click(); - await coinbase.handleAction(BaseActionType.CONNECT_TO_DAPP); - // ... further dApp and wallet interactions -}); -``` - ---- - -## Example: Custom Wallet Setup - -```typescript -import { configure, createOnchainTest } from '@coinbase/onchaintestkit'; - -const test = createOnchainTest( - configure() - .withMetaMask() - .withSeedPhrase({ - seedPhrase: process.env.E2E_TEST_SEED_PHRASE!, - password: 'PASSWORD', - }) - .withCustomSetup(async (wallet) => { - await wallet.importToken('0x...'); - }) - .build() -); - -test('custom setup', async ({ metamask }) => { - // Custom setup steps have already run -}); -``` - ---- - -## Types - -### WalletFixtureOptions - -```typescript -type WalletFixtureOptions = { - wallets: { - metamask?: MetaMaskConfig; - coinbase?: CoinbaseConfig; - }; - nodeConfig?: NodeConfig; -}; -``` - -### OnchainFixtures - -```typescript -type OnchainFixtures = { - metamask?: MetaMask; - coinbase?: CoinbaseWallet; - node?: LocalNodeManager; - smartContractManager?: SmartContractManager; -}; -``` - ---- - -## Best Practices - -- Always call `nodeManager.stop()` after tests to release resources. -- Use `test.afterEach()` to ensure cleanup even on failure. -- Use environment variables for sensitive data (e.g., seed phrases). -- Use snapshots for efficient state resets between test steps. -- Keep port ranges large enough for your parallel worker count. - ---- - -## Summary - -`@coinbase/onchaintestkit` provides a robust, extensible, and type-safe foundation for end-to-end blockchain application testing. By automating wallet and node management, it enables reliable, scalable, and maintainable test suites for modern dApps. - ---- \ 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/utils/utils.mdx b/docs/onchaintestkit/utils/utils.mdx deleted file mode 100644 index f307187..0000000 --- a/docs/onchaintestkit/utils/utils.mdx +++ /dev/null @@ -1,323 +0,0 @@ -# Onchain Test Kit - -**@coinbase/onchaintestkit** is an end-to-end testing toolkit for blockchain applications, built on top of Playwright. It provides a robust, type-safe, and extensible framework for automating wallet interactions, managing local blockchain nodes, and orchestrating complex blockchain testing scenarios. - ---- - -## Overview - -**Onchain Test Kit** is designed to streamline the process of writing, running, and maintaining reliable E2E tests for decentralized applications (dApps). It abstracts away the complexity of browser automation, wallet management, and local node orchestration, enabling developers to focus on business logic and user flows. - -**Key Capabilities:** - -- **Wallet Automation:** Seamlessly interact with MetaMask and Coinbase Wallet in browser-based tests. -- **Network Management:** Easily configure and switch between test networks using [viem](https://viem.sh/) chains. -- **Local Node Orchestration:** Spin up, snapshot, and manage local Ethereum nodes (Anvil) for deterministic testing. -- **Parallelization:** Supports parallel test execution with automatic port allocation for multiple local nodes. -- **Type Safety:** Built with TypeScript for robust, maintainable test code. - ---- - -## Why Onchain Test Kit? - -Testing blockchain applications is uniquely challenging due to: - -- The need for deterministic blockchain state. -- Complex wallet interactions (signatures, approvals, network switching). -- Parallel test execution without port or state conflicts. -- Automation of browser extensions (wallets) in headless environments. - -**@coinbase/onchaintestkit** solves these challenges by providing a unified, extensible toolkit that integrates tightly with Playwright and modern blockchain tooling. - ---- - -## Utility Functions - -This section documents the utility functions provided by the toolkit, which are essential for managing temporary directories, browser extensions, Playwright fixtures, and filesystem cleanup. - ---- - -### API Reference - -| Function | Description | -|-----------------------|---------------------------------------------------------------------------------------------| -| `createTempDir` | Creates a unique temporary directory with a given prefix. | -| `getExtensionId` | Discovers and retrieves the unique identifier for a browser extension in Playwright context.| -| `mergeFixtures` | Merges custom Playwright fixtures with the base test type. | -| `removeTempDir` | Removes a temporary directory, logging a warning if cleanup fails. | - ---- - -### Function Details - -#### `createTempDir(prefix: string): Promise` - -Creates a unique temporary directory in the system's temp folder. - -**Parameters:** - -| Name | Type | Description | -|--------|--------|----------------------------| -| prefix | string | Prefix for the temp folder. | - -**Returns:** -`Promise` – The path to the created temporary directory. - -**Example:** - -```typescript -import { createTempDir } from '@coinbase/onchaintestkit'; - -const tempDir = await createTempDir('mytest-'); -console.log(tempDir); // e.g., /tmp/mytest-abc123 -``` - ---- - -#### `getExtensionId(context: BrowserContext, extensionName: string): Promise` - -Discovers and retrieves the unique identifier for a browser extension in a Playwright browser context. - -**Parameters:** - -| Name | Type | Description | -|---------------|-----------------|----------------------------------------------| -| context | BrowserContext | Playwright browser context. | -| extensionName | string | Name of the extension to search for. | - -**Returns:** -`Promise` – The extension's unique identifier. - -**Example:** - -```typescript -import { getExtensionId } from '@coinbase/onchaintestkit'; -import { chromium } from '@playwright/test'; - -const context = await chromium.launchPersistentContext('', { headless: false }); -const metamaskId = await getExtensionId(context, 'MetaMask'); -console.log(metamaskId); // e.g., 'nkbihfbeogaeaoehlefnkodbefgpgknn' -``` - ---- - -#### `mergeFixtures(customFixtures: TestType): TestType` - -Merges custom Playwright fixtures with the base test type, allowing you to extend the test environment with additional fixtures. - -**Parameters:** - -| Name | Type | Description | -|----------------|--------------------------------------|-----------------------------| -| customFixtures | TestType | Custom fixtures to merge. | - -**Returns:** -`TestType` – The merged test type. - -**Example:** - -```typescript -import { mergeFixtures } from '@coinbase/onchaintestkit'; -import { test as base } from '@playwright/test'; - -const test = mergeFixtures(base.extend({ - myFixture: async ({}, use) => { - await use('value'); - }, -})); - -test('uses custom fixture', async ({ myFixture }) => { - expect(myFixture).toBe('value'); -}); -``` - ---- - -#### `removeTempDir(dirPath: string): Promise` - -Removes a temporary directory and logs a warning if cleanup fails. - -**Parameters:** - -| Name | Type | Description | -|---------|--------|--------------------------------| -| dirPath | string | Path to the directory to remove.| - -**Returns:** -`Promise` – Returns `null` if successful, or the error if cleanup failed. - -**Example:** - -```typescript -import { removeTempDir } from '@coinbase/onchaintestkit'; - -const error = await removeTempDir('/tmp/mytest-abc123'); -if (error) { - console.warn('Cleanup failed:', error); -} -``` - ---- - -## LocalNodeManager - -The `LocalNodeManager` class provides a comprehensive interface for managing local Anvil Ethereum nodes during testing. It is essential for deterministic, parallelizable blockchain testing. - -### Features - -- **Node Lifecycle:** Start and stop Anvil nodes programmatically. -- **State Management:** Create and revert snapshots, reset chain state. -- **Time Control:** Advance time, mine blocks. -- **Account Management:** Set balances, impersonate accounts. -- **Network Configuration:** Set gas price, chain ID. -- **Parallelization:** Automatic, cross-process port allocation for parallel test runs. - ---- - -### Mermaid Diagram: Node Lifecycle - -```mermaid -flowchart TD - StartNode([Start Node]) - AllocatePort([Allocate Port]) - LaunchAnvil([Launch Anvil Process]) - Ready([Node Ready]) - StopNode([Stop Node]) - Cleanup([Cleanup Resources]) - - StartNode --> AllocatePort - AllocatePort --> LaunchAnvil - LaunchAnvil --> Ready - StopNode --> Cleanup -``` - ---- - -### Example Usage - -```typescript -import { LocalNodeManager } from '@coinbase/onchaintestkit'; - -const nodeManager = new LocalNodeManager({ - chainId: 84532, - mnemonic: process.env.E2E_TEST_SEED_PHRASE, - // Optional: portRange: [10000, 20000] -}); - -await nodeManager.start(); -const port = nodeManager.getPort(); -console.log(`Node running on port ${port}`); - -// ...run tests... - -await nodeManager.stop(); -``` - ---- - -### API Table - -| Method | Description | -|----------------|---------------------------------------------------------------------------------------------| -| `start()` | Starts the Anvil node, automatically allocating an available port. | -| `stop()` | Stops the Anvil node and cleans up resources. | -| `getPort()` | Returns the port the node is running on. | -| `snapshot()` | Creates a blockchain state snapshot. | -| `revert()` | Reverts to the last snapshot. | -| `setBalance()` | Sets the balance of an account. | -| `impersonateAccount()` | Starts impersonating an account. | -| `setGasPrice()`| Sets the gas price for the node. | -| ... | ...and more (see full class documentation). | - ---- - -### Parallel Test Execution - -**@coinbase/onchaintestkit** enables parallel Playwright test execution by ensuring each test process gets a unique Anvil node and port. - -#### Mermaid Diagram: Parallel Node Allocation - -```mermaid -flowchart LR - subgraph TestProcesses - A1[Test File 1] --> N1[Node 1] - A2[Test File 2] --> N2[Node 2] - A3[Test File 3] --> N3[Node 3] - end - N1 -.->|Port 10001| Anvil - N2 -.->|Port 10002| Anvil - N3 -.->|Port 10003| Anvil -``` - ---- - -### Best Practices - -- Always call `stop()` on your `LocalNodeManager` to free resources. -- Use `test.afterEach()` to ensure cleanup even on test failure. -- Use snapshots for efficient state resets between test steps. -- Configure a sufficiently large port range for your expected parallelism. - ---- - -## Example: Full E2E Test with Wallet and Node - -```typescript -import { createOnchainTest, LocalNodeManager } from '@coinbase/onchaintestkit'; -import { configure } from '@coinbase/onchaintestkit'; -import { baseSepolia } from 'viem/chains'; - -const nodeManager = new LocalNodeManager({ - chainId: baseSepolia.id, - mnemonic: process.env.E2E_TEST_SEED_PHRASE, -}); -await nodeManager.start(); - -const walletConfig = configure() - .withMetaMask() - .withSeedPhrase({ - seedPhrase: process.env.E2E_TEST_SEED_PHRASE ?? '', - password: 'PASSWORD', - }) - .withNetwork({ - name: baseSepolia.name, - rpcUrl: `http://localhost:${nodeManager.getPort()}`, - chainId: baseSepolia.id, - symbol: baseSepolia.nativeCurrency.symbol, - }) - .build(); - -const test = createOnchainTest(walletConfig); - -test('connect wallet and swap', async ({ page, metamask }) => { - // ...test logic as shown in the overview... -}); - -await nodeManager.stop(); -``` - ---- - -## Variables - -| Variable | Description | -|-----------------------|-----------------------------------------------------------| -| `DEFAULT_PASSWORD` | Default password for wallet seed phrase import. | -| `DEFAULT_SEED_PHRASE` | Seed phrase for test wallet, typically from env variable. | -| `metamaskWalletConfig`| Example wallet configuration using MetaMask. | - ---- - -## Events - -This toolkit is primarily function- and class-based; it does not expose custom event emitters. All asynchronous actions (such as node start/stop, wallet actions) return Promises and should be awaited. - ---- - -## Summary - -**@coinbase/onchaintestkit** is an essential toolkit for robust, parallelizable, and deterministic E2E testing of blockchain applications. By abstracting away the complexity of wallet automation, local node management, and Playwright integration, it empowers teams to deliver high-quality dApps with confidence. - -For more advanced usage, see the full API documentation and explore the example test suites included in the repository. - ---- \ 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/wallets/wallets.mdx b/docs/onchaintestkit/wallets/wallets.mdx deleted file mode 100644 index 10acb56..0000000 --- a/docs/onchaintestkit/wallets/wallets.mdx +++ /dev/null @@ -1,561 +0,0 @@ -## Overview - -**@coinbase/onchaintestkit** is a comprehensive end-to-end testing framework designed for blockchain applications. It provides robust, type-safe abstractions for automating wallet interactions, network management, and common blockchain testing scenarios. Built on top of [Playwright](https://playwright.dev/), it enables developers to write reliable, maintainable, and parallelizable tests for dApps that interact with wallets like MetaMask and Coinbase Wallet. - -### Why is it important? - -- **Automates Real User Flows:** Simulates real-world wallet interactions, including onboarding, network switching, transaction approvals, and more. -- **Parallel Test Execution:** Supports parallel test runs with isolated local blockchain nodes, ensuring fast and reliable CI/CD pipelines. -- **Type Safety and Extensibility:** Written in TypeScript with extensible APIs for custom wallet actions and network configurations. -- **Production-Grade Reliability:** Handles edge cases like notification popups, passkey authentication, and extension state management. - ---- - -## 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. - ---- - -## Quick Start - -### 1. Install - -```bash -npm install --save-dev @playwright/test @coinbase/onchaintestkit -``` - -### 2. Configure Environment - -```env -E2E_TEST_SEED_PHRASE="your test wallet seed phrase" -``` - -### 3. Create Wallet Configuration - -```typescript -import { configure } from '@coinbase/onchaintestkit'; -import { baseSepolia } from 'viem/chains'; - -export const metamaskWalletConfig = configure() - .withMetaMask() - .withSeedPhrase({ - seedPhrase: process.env.E2E_TEST_SEED_PHRASE ?? '', - password: 'PASSWORD', - }) - .withNetwork({ - name: baseSepolia.name, - rpcUrl: baseSepolia.rpcUrls.default.http[0], - chainId: baseSepolia.id, - symbol: baseSepolia.nativeCurrency.symbol, - }) - .build(); -``` - -### 4. Write a Test - -```typescript -import { metamaskWalletConfig } from './walletConfig/metamaskWalletConfig'; -import { createOnchainTest } from '@coinbase/onchaintestkit'; - -const test = createOnchainTest(metamaskWalletConfig); -const { expect } = test; - -test('connect wallet and swap', async ({ page, metamask }) => { - if (!metamask) throw new Error('MetaMask fixture is required'); - - await page.getByTestId('ockConnectButton').click(); - await page.getByTestId('ockModalOverlay').first().getByRole('button', { name: 'MetaMask' }).click(); - await metamask.handleAction('CONNECT_TO_DAPP'); - await page.getByTestId('tos-accept-button').click(); - - await page.locator('input[placeholder="0.0"]').first().fill('0.0001'); - await page.getByRole('button', { name: 'Swap' }).click(); - await page.getByRole('button', { name: 'Confirm' }).click(); - - let notificationType = await metamask.identifyNotificationType(); - - if (notificationType === 'SpendingCap') { - await metamask.handleAction('CHANGE_SPENDING_CAP', { approvalType: 'APPROVE' }); - } - - notificationType = await metamask.identifyNotificationType(); - - if (notificationType === 'SpendingCap') { - await metamask.handleAction('HANDLE_SIGNATURE', { approvalType: 'APPROVE' }); - } - - notificationType = await metamask.identifyNotificationType(); - - if (notificationType === 'Transaction') { - await metamask.handleAction('HANDLE_TRANSACTION', { approvalType: 'APPROVE' }); - } - - await expect(page.getByRole('link', { name: 'View on Explorer' })).toBeVisible(); -}); -``` - ---- - -## Configuration Builder - -The toolkit uses a fluent builder pattern to configure wallets and networks: - -```typescript -const config = configure() - .withMetaMask() - .withSeedPhrase({ - seedPhrase: 'your seed phrase', - password: 'your password', - }) - .withNetwork({ - name: 'Network Name', - rpcUrl: 'RPC URL', - chainId: 1, - symbol: 'ETH', - }) - .build(); -``` - -#### Builder Methods - -| Method | Description | -|-----------------------|---------------------------------------------| -| `withMetaMask()` | Use MetaMask wallet | -| `withCoinbase()` | Use Coinbase Wallet | -| `withSeedPhrase()` | Set wallet seed phrase and password | -| `withNetwork()` | Set network parameters | -| `withCustomSetup()` | Add custom wallet setup logic | - ---- - -## Common Wallet Actions - -### Base Actions (Supported by Both 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", -} -``` - -### Notification Types - -```typescript -enum NotificationPageType { - SpendingCap = "spending-cap", - Signature = "signature", - Transaction = "transaction", - RemoveSpendCap = "remove-spend-cap", -} -``` - -### Approval Types - -```typescript -enum ActionApprovalType { - APPROVE = "approve", - REJECT = "reject", -} -``` - ---- - -## MetaMask-Specific Features - -### MetaMask-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 -} -``` - -### MetaMask Example: Advanced Account Management - -```typescript -import { configure, createOnchainTest, MetaMaskSpecificActionType } from '@coinbase/onchaintestkit'; - -const config = configure() - .withMetaMask() - .withSeedPhrase({ seedPhrase: 'test test ...', password: 'testpass' }) - .build(); - -const test = createOnchainTest(config); - -test('MetaMask account management', async ({ page, metamask }) => { - // Add a new account - await metamask.handleAction(MetaMaskSpecificActionType.ADD_ACCOUNT, { - accountName: 'Trading Account', - }); - - // Switch between accounts - await metamask.handleAction(MetaMaskSpecificActionType.SWITCH_ACCOUNT, { - accountName: 'Trading Account', - }); - - // Add a custom network - await metamask.handleAction(MetaMaskSpecificActionType.ADD_NETWORK, { - network: { - name: 'Custom Network', - rpcUrl: 'https://rpc.custom.network', - chainId: 12345, - symbol: 'CUSTOM', - }, - }); - - // Handle network addition approval popup - await metamask.handleAction(MetaMaskSpecificActionType.APPROVE_ADD_NETWORK, { - approvalType: 'APPROVE', - }); -}); -``` - -### MetaMask Network Switching - -```typescript -test('MetaMask network switching', async ({ page, metamask }) => { - // Switch to a specific network - await metamask.handleAction('SWITCH_NETWORK', { - networkName: 'Base Sepolia', - isTestnet: true, - }); -}); -``` - ---- - -## Coinbase Wallet-Specific Features - -### Coinbase Wallet-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 - -Coinbase Wallet supports passkey authentication for enhanced security. The toolkit provides automatic handling of WebAuthn prompts: - -```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 -} -``` - -### Coinbase Wallet Example: Passkey Authentication - -```typescript -import { configure, createOnchainTest, CoinbaseSpecificActionType } from '@coinbase/onchaintestkit'; - -const config = configure() - .withCoinbase() - .withSeedPhrase({ seedPhrase: 'test test ...', password: 'testpass' }) - .build(); - -const test = createOnchainTest(config); - -test('Coinbase Wallet with passkey', async ({ page, coinbase }) => { - // Handle passkey registration - const [popup] = await Promise.all([ - page.context().waitForEvent('page'), - page.getByTestId('register-passkey').click(), - ]); - - 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, - }, - }); - - // Later, handle passkey approval for transactions - const [txPopup] = await Promise.all([ - page.context().waitForEvent('page'), - page.getByRole('button', { name: 'Send Transaction' }).click(), - ]); - - await coinbase.handleAction(CoinbaseSpecificActionType.HANDLE_PASSKEY_POPUP, { - mainPage: page, - popup: txPopup, - passkeyAction: 'approve', - }); -}); -``` - -### Coinbase Wallet Account Management - -```typescript -test('Coinbase Wallet accounts', async ({ page, coinbase }) => { - // Add a new account - await coinbase.handleAction(CoinbaseSpecificActionType.ADD_ACCOUNT, { - accountName: 'DeFi Account', - }); - - // Switch between accounts - await coinbase.handleAction(CoinbaseSpecificActionType.SWITCH_ACCOUNT, { - accountName: 'DeFi Account', - }); - - // Add a custom network - await coinbase.handleAction(CoinbaseSpecificActionType.ADD_NETWORK, { - network: { - name: 'Base Mainnet', - rpcUrl: 'https://mainnet.base.org', - chainId: 8453, - symbol: 'ETH', - }, - }); -}); -``` - ---- - -## Advanced Features - -### Detecting Notification Types - -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('HANDLE_TRANSACTION', { approvalType: 'APPROVE' }); - break; - case 'SpendingCap': - await wallet.handleAction('CHANGE_SPENDING_CAP', { approvalType: 'APPROVE' }); - break; - case 'Signature': - await wallet.handleAction('HANDLE_SIGNATURE', { approvalType: 'APPROVE' }); - break; -} -``` - -### Custom Wallet Setup - -Both wallets support custom setup logic via the configuration builder: - -```typescript -const config = configure() - .withMetaMask() - .withSeedPhrase({ seedPhrase: '...', password: '...' }) - .withCustomSetup(async (wallet, context) => { - // Import additional accounts - await wallet.handleAction('IMPORT_WALLET_FROM_PRIVATE_KEY', { - privateKey: '0xabc...', - }); - - // Add custom tokens - await wallet.handleAction(MetaMaskSpecificActionType.ADD_TOKEN, { - tokenAddress: '0x...', - tokenSymbol: 'USDC', - tokenDecimals: 6, - }); - }) - .build(); -``` - ---- - -## API Reference - -### Classes - -| Class | Description | -|-----------------------|------------------------------------------------------------------| -| `MetaMask` | MetaMask wallet automation class | -| `CoinbaseWallet` | Coinbase Wallet automation class | -| `BaseWallet` | Abstract base class for wallet implementations | -| `PasskeyAuthenticator`| WebAuthn virtual authenticator for Coinbase Wallet | - -### Methods - -#### Common Wallet Methods - -| Method | Description | -|-------------------------------------|------------------------------------------------------------------| -| `handleAction(action, options)` | Handles a wallet action (connect, switch network, etc.) | -| `identifyNotificationType()` | Detects the current notification type in the wallet UI | - -#### MetaMask-Specific Methods - -| Method | Description | -|-------------------------------------|------------------------------------------------------------------| -| `static initialize()` | Creates MetaMask context and returns page/context | -| `static createContext()` | Creates browser context with MetaMask extension | - -#### Coinbase Wallet-Specific Methods - -| Method | Description | -|-------------------------------------|------------------------------------------------------------------| -| `static initialize()` | Creates Coinbase Wallet context and returns page/context | -| `static createContext()` | Creates browser context with Coinbase extension | -| `handlePasskeyPopup()` | Handles WebAuthn/Passkey authentication flows | - -### Types - -| Type | Description | -|-------------------------|------------------------------------------------------------------| -| `NetworkConfig` | Network configuration object | -| `ActionOptions` | Options for wallet actions (e.g., approvalType) | -| `WalletSetupContext` | Context for wallet setup (e.g., localNodePort) | -| `MetaMaskConfig` | MetaMask wallet configuration | -| `CoinbaseConfig` | Coinbase Wallet configuration | -| `PasskeyConfig` | Configuration for passkey authentication | - ---- - -## LocalNodeManager - -The `LocalNodeManager` provides an interface for managing local Ethereum nodes for testing. - -### Features - -- **Automatic Port Allocation:** Ensures each test gets a unique port for its node. -- **Chain State Manipulation:** Snapshots, reverts, resets, time travel, and block mining. -- **Account Management:** Set balances, impersonate accounts. -- **Parallelization:** Designed for multi-worker Playwright test runs. - -### Usage - -```typescript -import { LocalNodeManager } from '@coinbase/onchaintestkit'; - -const nodeManager = new LocalNodeManager({ - chainId: 84532, - mnemonic: process.env.E2E_TEST_SEED_PHRASE, -}); - -await nodeManager.start(); -const port = nodeManager.getPort(); -console.log(`Node running on port ${port}`); -// ... run tests -await nodeManager.stop(); -``` - -### Parallel Test Execution - -```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 -``` - ---- - -## 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. - ---- - -## Summary - -**@coinbase/onchaintestkit** provides comprehensive wallet automation for both MetaMask and Coinbase Wallet, each with their unique features: - -- **MetaMask**: Rich account management, network customization, and transaction handling -- **Coinbase Wallet**: Passkey/WebAuthn support, similar core features to MetaMask - -The toolkit abstracts away the complexity of wallet automation, local node management, and parallel test execution, enabling you to focus on building robust, production-ready dApps. - -For more examples and advanced usage, see the [project repository](https://github.com/coinbase/onchaintestkit) and the `e2e/` directory. - ---- \ 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