This document covers the architecture, key interfaces, and development guidelines for the Arbitrum Chain Playbook.
- Architecture Overview
- Project Structure
- Key Interfaces
- Playbook Interface
- Adding a New Playbook
- Development Commands
- Testing
The application follows a layered architecture with clear separation of concerns:
┌─────────────────────────────────────────────────────────────┐
│ Menu Layer (UI) │
│ mainMenu.ts, nodeController.ts, playbooks │
├─────────────────────────────────────────────────────────────┤
│ Business Logic Layer │
│ NodeManager, deployChain │
├─────────────────────────────────────────────────────────────┤
│ State Layer │
│ ChainEnv (singleton), SendersEnv (singleton) │
├─────────────────────────────────────────────────────────────┤
│ Infrastructure Layer │
│ Docker, ConfigService, Guards, Logger │
└─────────────────────────────────────────────────────────────┘
Design Principles:
- UI/Business Separation: UI logic (inquirer prompts) is in Controllers, business logic is in Managers
- Centralized State:
ChainEnvandSendersEnvsingletons manage all application state - Guard Pattern: Common validation checks are centralized in
OperationGuard - Configuration Service: Environment variables and constants are accessed via
ConfigService
src/
├── index.ts # Application entry point
├── init.ts # App initialization logic
├── config/ # Configuration management
│ └── index.ts # ConfigService singleton
├── devnode/ # Devnode mode
│ ├── devnodeConfig.ts # Devnode configuration
│ ├── devnodeManager.ts # Devnode lifecycle management
│ ├── devnodeMode.ts # Mode setup
│ └── devnodeNodeManager.ts # NodeManager wrapper for devnode
├── remoteRpc/ # Remote RPC mode
│ ├── remoteRpcMode.ts # Mode setup and validation
│ └── remoteRpcConfig.ts # Remote RPC configuration
├── menu/ # Main menu system
│ └── mainMenu.ts # Interactive CLI menu
├── core/ # Core functionality
│ ├── deployChain/ # Chain deployment
│ │ ├── deployChain.ts # Deployment implementation
│ │ └── prepareNodeConfig.ts # Node config preparation
│ ├── docker/ # Docker/Node management
│ │ ├── nodeManager.ts # NodeManager (business logic)
│ │ ├── nodeController.ts # NodeController (UI logic)
│ │ └── nodeConfigExtractors.ts # Config extractors
│ ├── interactChain/ # Chain interaction
│ │ └── interactChainOperations.ts
│ ├── nodeConfig/ # Node config operations
│ │ └── nodeConfigOperations.ts
│ └── monitoring/ # Process monitoring
│ └── processMonitor.ts
├── state/ # State management
│ ├── chainEnv/ # ChainEnv singleton
│ │ ├── index.ts # ChainEnv class
│ │ ├── types.ts # Related types
│ │ ├── persistence.ts # Load/save state
│ │ ├── fromTxHash.ts # State from tx hash
│ │ └── accessors/ # Accessor classes
│ │ ├── statusAccessor.ts
│ │ ├── nodeConfigAccessor.ts
│ │ └── chainConfigAccessor.ts
│ └── sendersEnv/ # SendersEnv singleton
│ ├── index.ts
│ └── types.ts
├── playbooks/ # Playbook modules
│ ├── types.ts # Playbook interface
│ ├── index.ts # Playbook registry
│ └── malicious-validator/ # Example playbook
├── types/ # Shared types
│ ├── index.ts # Enums, interfaces
│ └── constants.ts # Application constants
└── utils/ # Utilities
├── logger.ts # Logging utilities
├── guards.ts # OperationGuard class
├── nodeConfigUtils.ts # Node config helpers
└── inquirerUtils.ts # Inquirer helpers
tests/ # Test suites
└── core/docker/
└── nodeManager.spec.ts
The central singleton for managing chain lifecycle and state.
import { ChainEnv } from './state/chainEnv';
const chainEnv = ChainEnv.getInstance();Accessors:
| Accessor | Methods | Description |
|---|---|---|
chainEnv.status |
isInitiated(), get(), set(status) |
Chain status management |
chainEnv.nodeConfig |
get(), getPath(type), setPath(type, path) |
Node configuration |
chainEnv.chainConfig |
get(), getChainId(), getCoreContracts() |
Chain configuration |
chainEnv.nodeManager |
NodeManager instance | Docker node control |
chainEnv.parentChainClient |
PublicClient | Parent chain RPC client |
Lifecycle Methods:
// Load/save chain configuration
chainEnv.load(): boolean
chainEnv.save(): void
chainEnv.reset(): void
// Set deployment result
chainEnv.setDeploymentResult(chainConfig, nodeConfig, coreContracts, nodeConfigPaths): void
// Set parent chain client
chainEnv.setParentChainClient(client: PublicClient): void
// Mode availability
chainEnv.isChainModeAvailable(): boolean
chainEnv.isRemoteRpcModeAvailable(): booleanManages sender accounts for transactions.
import { SendersEnv, SenderRole } from './state/sendersEnv';
const sendersEnv = SendersEnv.getInstance();SenderRole Enum:
enum SenderRole {
Validator = 'validator',
BatchPoster = 'batchPoster',
RegularSender = 'regularSender',
}Methods:
// Get accounts
sendersEnv.getAll(): SenderAccount[]
sendersEnv.getAllByRole(role: SenderRole): SenderAccount[]
// Add accounts
sendersEnv.add(account: SenderAccount): void
sendersEnv.addByPrivateKey(privateKey: string, role: SenderRole): SenderAccount
// Remove accounts
sendersEnv.removeByAddress(address: string): boolean
sendersEnv.clear(): voidManages Docker containers for Nitro nodes. Business logic only - UI is handled by NodeController.
const nodeManager = chainEnv.nodeManager;Node Lifecycle:
// Start a node
await nodeManager.startNode(NodeType.MAIN): Promise<NodeInstance | null>
await nodeManager.startNode(NodeType.HONEST): Promise<NodeInstance | null>
await nodeManager.startNode(NodeType.MALICIOUS): Promise<NodeInstance | null>
// Stop nodes
await nodeManager.stopNode(nodeId): Promise<boolean>
await nodeManager.stopAllNodes(): Promise<void>
// Discover existing containers
await nodeManager.discoverExistingContainers(): Promise<void>Node Queries:
nodeManager.getNodes(): Map<string, NodeInstance>
nodeManager.getNode(nodeId): NodeInstance | undefined
nodeManager.getRunningNodes(): NodeInstance[]
nodeManager.displayStatus(): voidHealth Monitoring:
await nodeManager.checkNodeHealth(nodeId): Promise<boolean>
await nodeManager.getNodeUptime(nodeId): Promise<string>
nodeManager.isMonitoringActive(): boolean
await nodeManager.startHealthMonitoring(): Promise<void>
nodeManager.stopHealthMonitoring(): voidNodeInstance Interface:
interface NodeInstance {
config: SingleNodeConfig;
status: NodeStatus;
containerId?: string;
containerName?: string;
startedAt?: Date;
publicClient?: PublicClient;
}Handles UI interactions for node management. Separates UI logic from business logic.
import { nodeController } from './core/docker/nodeController';
// Show interactive management menu
await nodeController.showManagementMenu(): Promise<void>
// Select and stop a node (with user prompt)
await nodeController.selectAndStopNode(): Promise<void>
// Show node details (with user prompt)
await nodeController.showNodeDetails(): Promise<void>Centralized validation checks to reduce code duplication.
import { guard } from './utils/guards';
// Check if chain is initialized (logs error if not)
if (!guard.requireChainInitiated()) return;
// Check chain mode specifically
if (!guard.requireChainModeInitiated()) return;
// Get NodeManager (returns null and logs error if not available)
const nodeManager = guard.requireNodeManager();
if (!nodeManager) return;
// Get main node (returns null if not found)
const mainNode = guard.requireMainNode();
// Get main node with PublicClient (returns null if client not available)
const mainNodeWithClient = guard.requireMainNodeWithClient();
// Combined check: chain initialized + NodeManager available
const nodeManager = guard.requireChainAndNodeManager();Centralized configuration management.
import { config } from './config';
// Application config (from environment variables)
config.app.parentChainRpc // PARENT_CHAIN_RPC
config.app.chainRpc // CHAIN_RPC
config.app.deploymentTxHash // CHAIN_DEPLOYMENT_TRANSACTION_HASH
config.app.deployerPrivateKey // MAIN_PRIVATE_KEY
// Docker config (constants)
config.docker.image // Docker image name
config.docker.containerPrefix // Container name prefix
config.docker.dataDir // Container data directory
config.docker.user // Docker user
// Network config
config.network.defaultHttpPort
config.network.defaultWsPort
// Helper methods
config.isChainModeAvailable(): boolean
config.isRemoteRpcModeAvailable(): boolean
config.hasDeployerKey(): boolean
config.getDeploymentTxHash(): `0x${string}` | undefinedEach playbook must implement the Playbook interface:
export interface Playbook {
/** Unique identifier */
id: string;
/** Display name */
name: string;
/** Brief description */
description: string;
/** Operation modes in which this playbook is runnable */
supportedModes: OperationMode[];
/** Show interactive menu */
showMenu(): Promise<void>;
/** Optional: drive the playbook non-interactively from `yarn run:script ...` */
runHeadless?(command: string, params: unknown, ctx?: OperationContext): Promise<PlaybookActionResult>;
listHeadlessCommands?(): HeadlessCommandSpec[];
}Headless contract: when implementing runHeadless, factor your demo into a private executeXxx(config, ctx) that both the menu handler and the headless dispatch call. This keeps the two entry points in lockstep — anything you fix or add in interactive mode automatically applies to scripted runs. See MaliciousValidatorPlaybook for the canonical pattern.
mkdir -p src/playbooks/your-playbookCreate src/playbooks/your-playbook/index.ts:
import inquirer from 'inquirer';
import { Playbook } from '../types';
import logger from '../../utils/logger';
import { guard } from '../../utils/guards';
enum YourPlaybookAction {
ACTION_ONE = 'action_one',
ACTION_TWO = 'action_two',
BACK = 'back',
}
class YourPlaybook implements Playbook {
id = 'your-playbook';
name = 'Your Playbook Name';
description = 'Description of what your playbook does';
async showMenu(): Promise<void> {
logger.section('Your Playbook');
while (true) {
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'Select an action:',
choices: [
{ name: 'Action One', value: YourPlaybookAction.ACTION_ONE },
{ name: 'Action Two', value: YourPlaybookAction.ACTION_TWO },
new inquirer.Separator(),
{ name: '← Back to Playbook List', value: YourPlaybookAction.BACK },
],
},
]);
switch (action) {
case YourPlaybookAction.ACTION_ONE:
await this.handleActionOne();
break;
case YourPlaybookAction.ACTION_TWO:
await this.handleActionTwo();
break;
case YourPlaybookAction.BACK:
return;
}
logger.newline();
}
}
private async handleActionOne(): Promise<void> {
// Use guard for validation
if (!guard.requireChainInitiated()) return;
const nodeManager = guard.requireNodeManager();
if (!nodeManager) return;
// Your implementation here
logger.info('Action one executed');
}
private async handleActionTwo(): Promise<void> {
// Your implementation here
logger.info('Action two executed');
}
}
export const yourPlaybook = new YourPlaybook();
export default yourPlaybook;Update src/playbooks/index.ts:
import { yourPlaybook } from './your-playbook';
class PlaybookRegistry {
constructor() {
this.register(maliciousValidatorPlaybook);
this.register(yourPlaybook); // Add your playbook here
}
// ...
}# Run in development mode (auto-reload)
yarn dev
# Type check
yarn build
# or
npx tsc --noEmit
# Format code
yarn format
# Check formatting
yarn format:check
# Lint
yarn lint# Run all tests
yarn test
# Run specific test file
yarn test tests/core/docker/nodeManager.spec.ts
# Run with coverage
yarn test --coverageNote: Some tests (e.g., nodeManager.spec.ts) require Docker to be running.