docs(README.sonrctl.md): Add sonrctl README documentation

This commit is contained in:
Prad Nukala
2025-10-24 10:03:27 -04:00
parent 6dd00e8cf6
commit 3c7c4bb253
18 changed files with 2082 additions and 0 deletions
+396
View File
@@ -0,0 +1,396 @@
# sonrctl 🚀
**Blazingly fast CLI for managing Sonr blockchain nodes, validators, and networks**
Built with [Bun](https://bun.sh) - the all-in-one JavaScript runtime.
## Features
-**Blazingly Fast** - Built on Bun for instant startup and execution
- 🎯 **Simple API** - Clean, intuitive commands for node management
- 🐳 **Docker Integration** - Seamless container orchestration for testnets
- 💾 **SQLite Config** - Lightweight, fast configuration management
- 🎨 **Beautiful Output** - Colored, formatted CLI output
- 🔧 **Node Management** - Initialize, start, stop, and monitor nodes
- 🌐 **Network Operations** - Full testnet deployment and management
## Installation
### Install Bun (if not already installed)
```bash
curl -fsSL https://bun.sh/install | bash
```
### Install sonrctl
```bash
# Clone the repository
git clone https://github.com/sonr-io/sonr.git
cd sonr
# Install dependencies
bun install
# Link the CLI globally
bun link
```
### Install snrd binary
```bash
sonrctl install latest
```
## Quick Start
```bash
# Show help
sonrctl help
# Initialize a validator node
sonrctl init validator val-naruto --chain-id sonrtest_1-1
# Start the entire testnet network
sonrctl start network --detach
# Check network status
sonrctl status network
# Stop the network
sonrctl stop network
```
## Commands
### `sonrctl install [version]`
Install or update the snrd binary.
```bash
# Install latest version
sonrctl install latest
# Install specific version (coming soon)
sonrctl install v1.0.0
```
### `sonrctl init <type> <name> [options]`
Initialize a new node configuration.
**Types:** `validator`, `sentry`, `full`
**Options:**
- `--chain-id <id>` - Chain ID (default: sonrtest_1-1)
- `--home <path>` - Home directory (default: ~/.sonr/<name>)
- `--rpc-port <port>` - RPC port (default: 26657)
- `--rest-port <port>` - REST API port (default: 1317)
- `--grpc-port <port>` - gRPC port (default: 9090)
- `--grpc-web-port <port>` - gRPC-Web port (default: 9091)
- `--json-rpc-port <port>` - JSON-RPC port (default: 8545)
- `--json-rpc-ws-port <port>` - JSON-RPC WebSocket port (default: 8546)
**Examples:**
```bash
# Initialize a validator
sonrctl init validator val-naruto
# Initialize a sentry with custom home
sonrctl init sentry sentry-naruto --home ~/.sonr/custom-sentry
# Initialize with custom ports
sonrctl init validator my-val --rpc-port 26658 --rest-port 1318
```
### `sonrctl start <target> [options]`
Start a node or the entire network.
**Targets:**
- `<node-name>` - Start a specific registered node
- `network` - Start the entire testnet using docker-compose
**Options:**
- `--detach`, `-d` - Run in background (network only)
**Examples:**
```bash
# Start a specific node
sonrctl start val-naruto
# Start network in foreground
sonrctl start network
# Start network in background
sonrctl start network --detach
```
### `sonrctl stop <target>`
Stop a node, network, or all containers.
**Targets:**
- `<node-name>` - Stop a specific container
- `network` - Stop the entire testnet
- `all` - Stop all running containers
**Examples:**
```bash
# Stop a specific node
sonrctl stop val-naruto
# Stop the network
sonrctl stop network
# Stop all containers
sonrctl stop all
```
### `sonrctl status [target]`
Check status of nodes and network.
**Targets:**
- (none) - Show overall status
- `<node-name>` - Show specific node status
- `network` - Show network status
**Examples:**
```bash
# Show overall status
sonrctl status
# Show specific node status
sonrctl status val-naruto
# Show network status
sonrctl status network
```
### `sonrctl config <command> [args]`
Manage sonrctl configuration.
**Commands:**
- `list` - List all configuration
- `get <key>` - Get a specific value
- `set <key> <value>` - Set a configuration value
**Examples:**
```bash
# List all configuration
sonrctl config list
# Get chain ID
sonrctl config get chain_id
# Set home directory
sonrctl config set home ~/.sonr-custom
```
## Configuration
sonrctl stores its configuration in:
- **Config Database:** `~/.config/sonr/config.db`
- **Node Home:** `~/.sonr/` (default)
The SQLite database stores:
- Global configuration (chain ID, binary path, etc.)
- Registered nodes
- Node metadata
## Project Structure
```
src/
├── index.ts # Main CLI entry point
├── types.ts # TypeScript type definitions
├── commands/ # Command implementations
│ ├── install.ts # Install snrd binary
│ ├── init.ts # Initialize nodes
│ ├── start.ts # Start nodes/network
│ ├── stop.ts # Stop nodes/network
│ ├── status.ts # Check status
│ └── config.ts # Configuration management
└── lib/ # Utility libraries
├── constants.ts # Constants and defaults
├── logger.ts # Beautiful logging
├── config.ts # SQLite configuration manager
├── toml.ts # TOML file utilities
├── docker.ts # Docker operations
└── node.ts # Node operations
```
## Architecture
sonrctl is built with a clean, modular architecture:
1. **Command Layer** - Handles user input and command routing
2. **Library Layer** - Provides utilities for common operations
3. **Storage Layer** - SQLite database for configuration persistence
### Key Design Decisions
- **Bun Runtime** - For blazingly fast execution and built-in tools
- **SQLite Storage** - Lightweight, fast, and portable
- **No External Dependencies** - Minimal deps, maximum performance
- **Clean Separation** - Commands, libraries, and utilities are separate
- **Type Safety** - Full TypeScript support
## Docker Integration
sonrctl integrates seamlessly with Docker for testnet deployment:
- Start/stop docker-compose networks
- Manage individual containers
- Monitor container status
- Execute commands in containers
- View container logs
The CLI can work as a drop-in replacement for manual docker-compose operations.
## Development
### Prerequisites
- [Bun](https://bun.sh) v1.0+
- Docker (for network operations)
- Git
### Running Locally
```bash
# Run directly with Bun
bun run src/index.ts help
# Run a specific command
bun run src/index.ts status
```
### Adding New Commands
1. Create a new file in `src/commands/`
2. Export an async function that takes `args: string[]`
3. Import and add to the switch statement in `src/index.ts`
4. Update the help text
Example:
```typescript
// src/commands/mycommand.ts
export async function mycommand(args: string[]) {
Logger.header('My Command');
// Implementation
}
// src/index.ts
import { mycommand } from './commands/mycommand';
// Add to switch statement
case 'mycommand':
await mycommand(commandArgs);
break;
```
## Examples
### Setting Up a Testnet
```bash
# 1. Install the binary
sonrctl install
# 2. Start the testnet network
sonrctl start network --detach
# 3. Check status
sonrctl status network
# 4. View logs
docker logs -f val-naruto
# 5. Stop when done
sonrctl stop network
```
### Creating a Custom Node
```bash
# 1. Initialize a custom validator
sonrctl init validator my-validator \
--chain-id mychain-1 \
--home ~/.sonr/my-validator \
--rpc-port 26667
# 2. Configure genesis (manual step)
# Edit ~/.sonr/my-validator/config/genesis.json
# 3. Start the node
sonrctl start my-validator
# 4. Check status
sonrctl status my-validator
```
## Troubleshooting
### Command not found
```bash
# Make sure Bun's bin directory is in your PATH
export PATH="$HOME/.bun/bin:$PATH"
# Re-link the CLI
bun link
```
### Docker permission denied
```bash
# Add your user to the docker group
sudo usermod -aG docker $USER
# Log out and back in, or run:
newgrp docker
```
### Binary not found after install
```bash
# Check if ~/.local/bin is in your PATH
export PATH="$HOME/.local/bin:$PATH"
# Add to your shell profile
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
```
## Contributing
Contributions are welcome! Please:
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests if applicable
5. Submit a pull request
## License
Apache 2.0 - See LICENSE file for details
## Links
- [Sonr Network](https://sonr.io)
- [Documentation](https://docs.sonr.io)
- [GitHub](https://github.com/sonr-io/sonr)
- [Bun](https://bun.sh)
---
**Built with ❤️ using Bun**
+34
View File
@@ -0,0 +1,34 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "sonr",
"dependencies": {
"@iarna/toml": "^2.2.5",
},
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@iarna/toml": ["@iarna/toml@2.2.5", "", {}, "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="],
"@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
"@types/node": ["@types/node@24.9.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="],
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
}
}
+23
View File
@@ -0,0 +1,23 @@
{
"name": "sonrctl",
"version": "0.1.0",
"module": "src/index.ts",
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
},
"bin": {
"sonrctl": "./src/index.ts"
},
"description": "Sonr Network Control CLI - Manage Sonr blockchain nodes, validators, and networks",
"private": false,
"publishConfig": {
"access": "public"
},
"type": "module",
"dependencies": {
"@iarna/toml": "^2.2.5"
}
}
+75
View File
@@ -0,0 +1,75 @@
// Config command - Manage sonrctl configuration
import { Logger } from '../lib/logger';
import { ConfigManager } from '../lib/config';
export async function config(args: string[]) {
const subcommand = args[0];
if (!subcommand || !['get', 'set', 'list'].includes(subcommand)) {
Logger.error('Usage: sonrctl config <get|set|list> [key] [value]');
Logger.info('');
Logger.info('Examples:');
Logger.info(' sonrctl config list # List all configuration');
Logger.info(' sonrctl config get chain_id # Get a specific value');
Logger.info(' sonrctl config set home ~/.sonr # Set a configuration value');
process.exit(1);
}
const manager = new ConfigManager();
try {
if (subcommand === 'list') {
await listConfig(manager);
} else if (subcommand === 'get') {
await getConfig(manager, args[1]);
} else if (subcommand === 'set') {
await setConfig(manager, args[1], args[2]);
}
} catch (error) {
Logger.error(`Config operation failed: ${error}`);
process.exit(1);
} finally {
manager.close();
}
}
async function listConfig(manager: ConfigManager) {
Logger.header('Sonrctl Configuration');
const allConfig = manager.getAll();
if (Object.keys(allConfig).length === 0) {
Logger.info('No configuration set');
return;
}
Logger.table(allConfig);
}
async function getConfig(manager: ConfigManager, key?: string) {
if (!key) {
Logger.error('Key is required');
Logger.info('Usage: sonrctl config get <key>');
process.exit(1);
}
const value = manager.get(key);
if (value === null) {
Logger.warn(`Configuration key '${key}' not found`);
process.exit(1);
}
console.log(value);
}
async function setConfig(manager: ConfigManager, key?: string, value?: string) {
if (!key || !value) {
Logger.error('Key and value are required');
Logger.info('Usage: sonrctl config set <key> <value>');
process.exit(1);
}
manager.set(key, value);
Logger.success(`Set ${key} = ${value}`);
}
+120
View File
@@ -0,0 +1,120 @@
// Init command - Initialize node configurations
import { Logger } from '../lib/logger';
import { ConfigManager } from '../lib/config';
import { NodeManager } from '../lib/node';
import { DEFAULT_CHAIN_ID, DEFAULT_HOME_DIR, DEFAULT_PORTS } from '../lib/constants';
import type { NodeConfig } from '../types';
export async function init(args: string[]) {
const subcommand = args[0];
if (!subcommand || !['validator', 'sentry', 'full'].includes(subcommand)) {
Logger.error('Usage: sonrctl init <validator|sentry|full> <name> [options]');
Logger.info('');
Logger.info('Examples:');
Logger.info(' sonrctl init validator val-naruto');
Logger.info(' sonrctl init sentry sentry-naruto --home ~/.sonr/sentry');
Logger.info(' sonrctl init full my-node --chain-id sonrtest_1-1');
process.exit(1);
}
const name = args[1];
if (!name) {
Logger.error('Node name is required');
Logger.info('Usage: sonrctl init <validator|sentry|full> <name>');
process.exit(1);
}
// Parse options
const options: Record<string, any> = {};
for (let i = 2; i < args.length; i += 2) {
const key = args[i].replace(/^--/, '');
const value = args[i + 1];
options[key] = value;
}
const config = new ConfigManager();
// Build node configuration
const nodeConfig: NodeConfig = {
moniker: name,
chainId: options['chain-id'] || config.get('chain_id') || DEFAULT_CHAIN_ID,
home: options.home || `${DEFAULT_HOME_DIR}/${name}`,
nodeType: subcommand as 'validator' | 'sentry' | 'full',
ports: {
rpc: parseInt(options['rpc-port']) || DEFAULT_PORTS.rpc,
rest: parseInt(options['rest-port']) || DEFAULT_PORTS.rest,
grpc: parseInt(options['grpc-port']) || DEFAULT_PORTS.grpc,
grpcWeb: parseInt(options['grpc-web-port']) || DEFAULT_PORTS.grpcWeb,
jsonRpc: parseInt(options['json-rpc-port']) || DEFAULT_PORTS.jsonRpc,
jsonRpcWs: parseInt(options['json-rpc-ws-port']) || DEFAULT_PORTS.jsonRpcWs,
},
};
Logger.header(`Initialize ${nodeConfig.nodeType.toUpperCase()} Node`);
try {
// Check if binary is available
const binary = config.get('binary') || 'snrd';
const available = await NodeManager.isBinaryAvailable(binary);
if (!available) {
Logger.error(`Binary '${binary}' not found`);
Logger.info('Install it using: sonrctl install');
process.exit(1);
}
// Check if home directory already exists
const homeExists = await Bun.file(nodeConfig.home).exists();
if (homeExists) {
Logger.warn(`Home directory already exists: ${nodeConfig.home}`);
const response = await confirmPrompt('Overwrite existing configuration?');
if (!response) {
Logger.info('Initialization cancelled');
process.exit(0);
}
// Remove existing directory
await Bun.$`rm -rf ${nodeConfig.home}`;
}
// Initialize node
await NodeManager.init(nodeConfig, binary);
// Register node in database
config.addNode(name, nodeConfig.nodeType, nodeConfig.chainId, nodeConfig.home, nodeConfig.moniker);
Logger.success('Node initialized successfully!');
Logger.table({
Name: name,
Type: nodeConfig.nodeType,
'Chain ID': nodeConfig.chainId,
Home: nodeConfig.home,
Moniker: nodeConfig.moniker,
});
Logger.info('');
Logger.info('Next steps:');
Logger.info(` 1. Configure genesis: ${nodeConfig.home}/config/genesis.json`);
Logger.info(` 2. Start the node: sonrctl start ${name}`);
Logger.info(` 3. Check status: sonrctl status ${name}`);
} catch (error) {
Logger.error(`Initialization failed: ${error}`);
process.exit(1);
} finally {
config.close();
}
}
// Simple confirmation prompt
async function confirmPrompt(message: string): Promise<boolean> {
process.stdout.write(`${message} (y/N): `);
for await (const line of console) {
const answer = line.trim().toLowerCase();
return answer === 'y' || answer === 'yes';
}
return false;
}
+92
View File
@@ -0,0 +1,92 @@
// Install command - Install or update snrd binary
import { Logger } from '../lib/logger';
import { ConfigManager } from '../lib/config';
const GITHUB_REPO = 'sonr-io/sonr';
const BINARY_NAME = 'snrd';
export async function install(args: string[]) {
Logger.header('Install Sonr Node Binary');
const version = args[0] || 'latest';
Logger.info(`Installing ${BINARY_NAME} version: ${version}`);
try {
// Check if running on supported platform
const platform = process.platform;
const arch = process.arch;
Logger.info(`Detected platform: ${platform}/${arch}`);
if (version === 'latest') {
// Get latest release from GitHub
Logger.step(1, 3, 'Fetching latest release information');
const response = await fetch(`https://api.github.com/repos/${GITHUB_REPO}/releases/latest`);
if (!response.ok) {
throw new Error('Failed to fetch release information');
}
const release = await response.json();
const latestVersion = release.tag_name;
Logger.info(`Latest version: ${latestVersion}`);
// Download binary
Logger.step(2, 3, `Downloading ${BINARY_NAME}`);
const assetName = `${BINARY_NAME}-${platform}-${arch}`;
const asset = release.assets.find((a: any) => a.name.includes(assetName));
if (!asset) {
throw new Error(`No binary found for ${platform}/${arch}`);
}
const stop = Logger.loading('Downloading binary...');
const binaryResponse = await fetch(asset.browser_download_url);
if (!binaryResponse.ok) {
stop();
throw new Error('Failed to download binary');
}
stop();
// Save binary to /usr/local/bin or ~/.local/bin
Logger.step(3, 3, 'Installing binary');
const binaryData = await binaryResponse.arrayBuffer();
try {
// Try to install to /usr/local/bin first (system-wide)
await Bun.write(`/usr/local/bin/${BINARY_NAME}`, binaryData);
await Bun.$`chmod +x /usr/local/bin/${BINARY_NAME}`;
Logger.success(`Installed ${BINARY_NAME} to /usr/local/bin`);
} catch {
// Fall back to ~/.local/bin (user-level)
const localBinDir = `${process.env.HOME}/.local/bin`;
await Bun.$`mkdir -p ${localBinDir}`;
await Bun.write(`${localBinDir}/${BINARY_NAME}`, binaryData);
await Bun.$`chmod +x ${localBinDir}/${BINARY_NAME}`;
Logger.success(`Installed ${BINARY_NAME} to ${localBinDir}`);
Logger.warn(`Make sure ${localBinDir} is in your PATH`);
}
// Verify installation
const versionCheck = await Bun.$`${BINARY_NAME} version`.quiet();
const installedVersion = (await versionCheck.text()).trim();
Logger.success(`Successfully installed ${BINARY_NAME} ${installedVersion}`);
// Update config
const config = new ConfigManager();
config.set('binary', BINARY_NAME);
config.set('version', installedVersion);
config.close();
} else {
throw new Error('Specific version installation not yet supported. Use "latest" for now.');
}
} catch (error) {
Logger.error(`Installation failed: ${error}`);
process.exit(1);
}
}
+148
View File
@@ -0,0 +1,148 @@
// Start command - Start nodes or network
import { Logger } from '../lib/logger';
import { ConfigManager } from '../lib/config';
import { DockerManager } from '../lib/docker';
import { NodeManager } from '../lib/node';
import { join } from 'path';
export async function start(args: string[]) {
const subcommand = args[0];
if (!subcommand) {
Logger.error('Usage: sonrctl start <node-name|network>');
Logger.info('');
Logger.info('Examples:');
Logger.info(' sonrctl start val-naruto # Start a specific node');
Logger.info(' sonrctl start network # Start entire testnet with docker-compose');
Logger.info(' sonrctl start network --detach # Start network in background');
process.exit(1);
}
const config = new ConfigManager();
try {
if (subcommand === 'network') {
await startNetwork(args.slice(1));
} else {
await startNode(subcommand, config);
}
} catch (error) {
Logger.error(`Failed to start: ${error}`);
process.exit(1);
} finally {
config.close();
}
}
async function startNode(name: string, config: ConfigManager) {
Logger.header(`Start Node: ${name}`);
// Get node from registry
const node = config.getNode(name);
if (!node) {
Logger.error(`Node '${name}' not found`);
Logger.info('Initialize it first using: sonrctl init <type> <name>');
Logger.info('');
Logger.info('Registered nodes:');
const nodes = config.listNodes();
if (nodes.length === 0) {
Logger.info(' (none)');
} else {
nodes.forEach((n: any) => Logger.info(` - ${n.name} (${n.type})`));
}
process.exit(1);
}
const binary = config.get('binary') || 'snrd';
Logger.info(`Starting ${node.type} node: ${node.moniker}`);
Logger.table({
Name: node.name,
Type: node.type,
'Chain ID': node.chain_id,
Home: node.home,
});
// Check if binary is available
const available = await NodeManager.isBinaryAvailable(binary);
if (!available) {
Logger.error(`Binary '${binary}' not found`);
Logger.info('Install it using: sonrctl install');
process.exit(1);
}
Logger.info('');
Logger.info('Starting node...');
Logger.info('Press Ctrl+C to stop');
Logger.info('');
// Start node (this will block)
await NodeManager.start(node.home, node.chain_id, binary);
}
async function startNetwork(args: string[]) {
Logger.header('Start Sonr Testnet Network');
// Check if docker is available
const dockerAvailable = await DockerManager.isAvailable();
if (!dockerAvailable) {
Logger.error('Docker is not available or not running');
Logger.info('Install Docker: https://docs.docker.com/get-docker/');
process.exit(1);
}
const composeAvailable = await DockerManager.isComposeAvailable();
if (!composeAvailable) {
Logger.error('docker-compose is not available');
Logger.info('Install docker-compose: https://docs.docker.com/compose/install/');
process.exit(1);
}
// Parse options
const detached = args.includes('--detach') || args.includes('-d');
// Find docker-compose file
const cwd = process.cwd();
const composePath = join(cwd, 'networks', 'testnet', 'docker-compose.yml');
const composeExists = await Bun.file(composePath).exists();
if (!composeExists) {
Logger.error(`docker-compose.yml not found at: ${composePath}`);
Logger.info('Make sure you are in the sonr repository root directory');
process.exit(1);
}
Logger.info('Starting network with docker-compose...');
Logger.info(`Compose file: ${composePath}`);
Logger.info('');
try {
await DockerManager.composeUp(composePath, detached);
if (detached) {
Logger.success('Network started in background');
Logger.info('');
Logger.info('Check status with: docker ps');
Logger.info('View logs with: docker logs -f <container-name>');
Logger.info('Stop network with: sonrctl stop network');
} else {
Logger.success('Network started');
Logger.info('Press Ctrl+C to stop');
}
// Show running containers
Logger.info('');
Logger.section('Running Containers');
const containers = await DockerManager.listContainers('sonr-testnet');
if (containers.length > 0) {
for (const container of containers) {
Logger.info(` ${container.name}: ${container.status}`);
}
}
} catch (error) {
Logger.error(`Failed to start network: ${error}`);
throw error;
}
}
+172
View File
@@ -0,0 +1,172 @@
// Status command - Check node and network status
import { Logger } from '../lib/logger';
import { ConfigManager } from '../lib/config';
import { DockerManager } from '../lib/docker';
import { NodeManager } from '../lib/node';
export async function status(args: string[]) {
const target = args[0];
if (!target) {
// Show overall status
await showOverallStatus();
} else if (target === 'network') {
await showNetworkStatus();
} else {
await showNodeStatus(target);
}
}
async function showOverallStatus() {
Logger.header('Sonr Network Status');
const config = new ConfigManager();
// Show registered nodes
Logger.section('Registered Nodes');
const nodes = config.listNodes();
if (nodes.length === 0) {
Logger.info('No nodes registered');
Logger.info('Initialize a node using: sonrctl init <type> <name>');
} else {
console.log();
console.log(' Name Type Chain ID Home');
console.log(' ───────────────── ────────── ──────────────── ─────────────────────');
for (const node of nodes) {
const name = node.name.padEnd(17);
const type = node.type.padEnd(10);
const chainId = node.chain_id.padEnd(16);
console.log(` ${name} ${type} ${chainId} ${node.home}`);
}
console.log();
}
// Show docker containers
const dockerAvailable = await DockerManager.isAvailable();
if (dockerAvailable) {
Logger.section('Docker Containers');
const containers = await DockerManager.listContainers();
if (containers.length === 0) {
Logger.info('No containers running');
} else {
console.log();
console.log(' Name Status Image');
console.log(' ────────────────────── ──────────────────────── ────────────────────');
for (const container of containers) {
const name = container.name.padEnd(22);
const status = container.status.substring(0, 24).padEnd(24);
console.log(` ${name} ${status} ${container.image}`);
}
console.log();
}
}
config.close();
}
async function showNetworkStatus() {
Logger.header('Testnet Network Status');
const dockerAvailable = await DockerManager.isAvailable();
if (!dockerAvailable) {
Logger.error('Docker is not available');
process.exit(1);
}
// Get all testnet containers
const containers = await DockerManager.listContainers('sonr-testnet');
if (containers.length === 0) {
Logger.info('No network containers found');
Logger.info('Start the network using: sonrctl start network');
return;
}
Logger.section('Network Containers');
console.log();
for (const container of containers) {
const { running, status } = await DockerManager.getContainerStatus(container.name);
const statusIcon = running ? '✓' : '✗';
const statusColor = running ? '\x1b[32m' : '\x1b[31m';
console.log(` ${statusColor}${statusIcon}\x1b[0m ${container.name}`);
console.log(` Status: ${status}`);
console.log(` Image: ${container.image}`);
// Try to get node status if running
if (running) {
try {
const rpcPort = container.name.includes('sentry') ? '26657' : null;
if (rpcPort) {
const nodeStatus = await NodeManager.getStatus(`http://localhost:${rpcPort}`);
const syncInfo = nodeStatus.result.sync_info;
const catchingUp = syncInfo.catching_up ? 'Yes' : 'No';
console.log(` Height: ${syncInfo.latest_block_height}`);
console.log(` Syncing: ${catchingUp}`);
}
} catch {
// Ignore errors getting node status
}
}
console.log();
}
}
async function showNodeStatus(name: string) {
Logger.header(`Node Status: ${name}`);
const config = new ConfigManager();
const node = config.getNode(name);
if (!node) {
Logger.error(`Node '${name}' not found`);
Logger.info('List registered nodes using: sonrctl status');
config.close();
process.exit(1);
}
// Show node info
Logger.section('Node Information');
Logger.table({
Name: node.name,
Type: node.type,
'Chain ID': node.chain_id,
Home: node.home,
Moniker: node.moniker,
});
// Check docker container status
const dockerAvailable = await DockerManager.isAvailable();
if (dockerAvailable) {
const { running, status } = await DockerManager.getContainerStatus(name);
Logger.section('Container Status');
Logger.info(`Running: ${running ? 'Yes' : 'No'}`);
Logger.info(`Status: ${status}`);
if (running) {
// Try to get node status
try {
const nodeStatus = await NodeManager.getStatus('http://localhost:26657');
const syncInfo = nodeStatus.result.sync_info;
Logger.section('Blockchain Status');
Logger.table({
'Latest Height': syncInfo.latest_block_height,
'Latest Block Time': new Date(syncInfo.latest_block_time).toLocaleString(),
'Catching Up': syncInfo.catching_up ? 'Yes' : 'No',
});
} catch (error) {
Logger.warn('Unable to fetch node status');
}
}
}
config.close();
}
+103
View File
@@ -0,0 +1,103 @@
// Stop command - Stop nodes or network
import { Logger } from '../lib/logger';
import { ConfigManager } from '../lib/config';
import { DockerManager } from '../lib/docker';
import { join } from 'path';
export async function stop(args: string[]) {
const target = args[0];
if (!target) {
Logger.error('Usage: sonrctl stop <node-name|network|all>');
Logger.info('');
Logger.info('Examples:');
Logger.info(' sonrctl stop val-naruto # Stop a specific node container');
Logger.info(' sonrctl stop network # Stop entire testnet network');
Logger.info(' sonrctl stop all # Stop all running containers');
process.exit(1);
}
try {
if (target === 'network') {
await stopNetwork();
} else if (target === 'all') {
await stopAll();
} else {
await stopNode(target);
}
} catch (error) {
Logger.error(`Failed to stop: ${error}`);
process.exit(1);
}
}
async function stopNode(name: string) {
Logger.header(`Stop Node: ${name}`);
const dockerAvailable = await DockerManager.isAvailable();
if (!dockerAvailable) {
Logger.error('Docker is not available');
process.exit(1);
}
// Check if container is running
const running = await DockerManager.isContainerRunning(name);
if (!running) {
Logger.warn(`Container '${name}' is not running`);
return;
}
await DockerManager.stopContainer(name);
}
async function stopNetwork() {
Logger.header('Stop Sonr Testnet Network');
const dockerAvailable = await DockerManager.isAvailable();
if (!dockerAvailable) {
Logger.error('Docker is not available');
process.exit(1);
}
// Find docker-compose file
const cwd = process.cwd();
const composePath = join(cwd, 'networks', 'testnet', 'docker-compose.yml');
const composeExists = await Bun.file(composePath).exists();
if (!composeExists) {
Logger.error(`docker-compose.yml not found at: ${composePath}`);
Logger.info('Make sure you are in the sonr repository root directory');
process.exit(1);
}
await DockerManager.composeDown(composePath);
}
async function stopAll() {
Logger.header('Stop All Containers');
const dockerAvailable = await DockerManager.isAvailable();
if (!dockerAvailable) {
Logger.error('Docker is not available');
process.exit(1);
}
// Get all containers
const containers = await DockerManager.listContainers();
if (containers.length === 0) {
Logger.info('No running containers found');
return;
}
Logger.info(`Found ${containers.length} containers`);
for (const container of containers) {
if (container.status.startsWith('Up')) {
Logger.info(`Stopping ${container.name}...`);
await DockerManager.stopContainer(container.name);
}
}
Logger.success('All containers stopped');
}
Executable
+161
View File
@@ -0,0 +1,161 @@
#!/usr/bin/env bun
// Sonrctl - Sonr Network Control CLI
import { Logger } from './lib/logger';
import { install } from './commands/install';
import { init } from './commands/init';
import { start } from './commands/start';
import { stop } from './commands/stop';
import { status } from './commands/status';
import { config } from './commands/config';
const VERSION = '0.1.0';
// Main CLI router
async function main() {
const args = process.argv.slice(2);
if (args.length === 0 || args[0] === 'help' || args[0] === '--help' || args[0] === '-h') {
showHelp();
process.exit(0);
}
if (args[0] === 'version' || args[0] === '--version' || args[0] === '-v') {
console.log(VERSION);
process.exit(0);
}
const command = args[0];
const commandArgs = args.slice(1);
try {
switch (command) {
case 'install':
await install(commandArgs);
break;
case 'init':
await init(commandArgs);
break;
case 'start':
await start(commandArgs);
break;
case 'stop':
await stop(commandArgs);
break;
case 'status':
await status(commandArgs);
break;
case 'config':
await config(commandArgs);
break;
default:
Logger.error(`Unknown command: ${command}`);
Logger.info('Run "sonrctl help" for usage information');
process.exit(1);
}
} catch (error) {
Logger.error(`Command failed: ${error}`);
process.exit(1);
}
}
function showHelp() {
const COLORS = {
reset: '\x1b[0m',
bright: '\x1b[1m',
cyan: '\x1b[36m',
blue: '\x1b[34m',
gray: '\x1b[90m',
};
console.log(`
${COLORS.bright}${COLORS.cyan}sonrctl${COLORS.reset} - Sonr Network Control CLI
${COLORS.bright}USAGE${COLORS.reset}
${COLORS.cyan}sonrctl${COLORS.reset} <command> [options]
${COLORS.bright}COMMANDS${COLORS.reset}
${COLORS.cyan}install${COLORS.reset} [version]
Install or update the snrd binary
${COLORS.gray}Example: sonrctl install latest${COLORS.reset}
${COLORS.cyan}init${COLORS.reset} <validator|sentry|full> <name> [options]
Initialize a new node configuration
${COLORS.gray}Example: sonrctl init validator val-naruto${COLORS.reset}
${COLORS.gray}Options:${COLORS.reset}
${COLORS.gray} --chain-id <id> Chain ID (default: sonrtest_1-1)${COLORS.reset}
${COLORS.gray} --home <path> Home directory${COLORS.reset}
${COLORS.gray} --rpc-port <port> RPC port (default: 26657)${COLORS.reset}
${COLORS.gray} --rest-port <port> REST API port (default: 1317)${COLORS.reset}
${COLORS.gray} --grpc-port <port> gRPC port (default: 9090)${COLORS.reset}
${COLORS.cyan}start${COLORS.reset} <node-name|network> [options]
Start a node or the entire network
${COLORS.gray}Example: sonrctl start val-naruto${COLORS.reset}
${COLORS.gray}Example: sonrctl start network${COLORS.reset}
${COLORS.gray}Options:${COLORS.reset}
${COLORS.gray} --detach, -d Run in background${COLORS.reset}
${COLORS.cyan}stop${COLORS.reset} <node-name|network|all>
Stop a node, network, or all containers
${COLORS.gray}Example: sonrctl stop val-naruto${COLORS.reset}
${COLORS.gray}Example: sonrctl stop network${COLORS.reset}
${COLORS.cyan}status${COLORS.reset} [node-name|network]
Check status of nodes and network
${COLORS.gray}Example: sonrctl status${COLORS.reset}
${COLORS.gray}Example: sonrctl status val-naruto${COLORS.reset}
${COLORS.cyan}config${COLORS.reset} <get|set|list> [key] [value]
Manage sonrctl configuration
${COLORS.gray}Example: sonrctl config list${COLORS.reset}
${COLORS.gray}Example: sonrctl config get chain_id${COLORS.reset}
${COLORS.gray}Example: sonrctl config set home ~/.sonr${COLORS.reset}
${COLORS.cyan}help${COLORS.reset}
Show this help message
${COLORS.cyan}version${COLORS.reset}
Show version information
${COLORS.bright}GLOBAL OPTIONS${COLORS.reset}
${COLORS.gray}-h, --help Show help${COLORS.reset}
${COLORS.gray}-v, --version Show version${COLORS.reset}
${COLORS.bright}EXAMPLES${COLORS.reset}
${COLORS.gray}# Install the snrd binary${COLORS.reset}
${COLORS.blue}sonrctl install${COLORS.reset}
${COLORS.gray}# Initialize a validator node${COLORS.reset}
${COLORS.blue}sonrctl init validator val-naruto --chain-id sonrtest_1-1${COLORS.reset}
${COLORS.gray}# Start the entire testnet network${COLORS.reset}
${COLORS.blue}sonrctl start network --detach${COLORS.reset}
${COLORS.gray}# Check network status${COLORS.reset}
${COLORS.blue}sonrctl status network${COLORS.reset}
${COLORS.gray}# Stop the network${COLORS.reset}
${COLORS.blue}sonrctl stop network${COLORS.reset}
${COLORS.bright}CONFIGURATION${COLORS.reset}
Config directory: ${COLORS.cyan}~/.config/sonr${COLORS.reset}
Node home: ${COLORS.cyan}~/.sonr${COLORS.reset}
${COLORS.bright}MORE INFO${COLORS.reset}
Docs: ${COLORS.blue}https://docs.sonr.io${COLORS.reset}
Repo: ${COLORS.blue}https://github.com/sonr-io/sonr${COLORS.reset}
`);
}
// Run the CLI
main().catch((error) => {
Logger.error(`Fatal error: ${error}`);
process.exit(1);
});
+120
View File
@@ -0,0 +1,120 @@
// Configuration management using bun:sqlite
import { Database } from 'bun:sqlite';
import { homedir } from 'os';
import { join } from 'path';
import { DEFAULT_CHAIN_ID, DEFAULT_HOME_DIR, DEFAULT_BINARY } from './constants';
const CONFIG_DIR = join(homedir(), '.config', 'sonr');
const CONFIG_DB = join(CONFIG_DIR, 'config.db');
export class ConfigManager {
private db: Database;
constructor() {
// Ensure config directory exists
if (!Bun.file(CONFIG_DIR).size) {
Bun.spawnSync(['mkdir', '-p', CONFIG_DIR]);
}
this.db = new Database(CONFIG_DB, { create: true });
this.init();
}
/**
* Initialize database tables
*/
private init() {
this.db.run(`
CREATE TABLE IF NOT EXISTS config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)
`);
this.db.run(`
CREATE TABLE IF NOT EXISTS nodes (
name TEXT PRIMARY KEY,
type TEXT NOT NULL,
chain_id TEXT NOT NULL,
home TEXT NOT NULL,
moniker TEXT NOT NULL,
created_at INTEGER NOT NULL
)
`);
// Set defaults if not exists
const defaults = {
chain_id: DEFAULT_CHAIN_ID,
home: DEFAULT_HOME_DIR,
binary: DEFAULT_BINARY,
};
for (const [key, value] of Object.entries(defaults)) {
const existing = this.get(key);
if (!existing) {
this.set(key, value);
}
}
}
/**
* Get a configuration value
*/
get(key: string): string | null {
const result = this.db.query('SELECT value FROM config WHERE key = ?').get(key) as { value: string } | null;
return result?.value || null;
}
/**
* Set a configuration value
*/
set(key: string, value: string) {
this.db.run('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)', [key, value]);
}
/**
* Get all configuration
*/
getAll(): Record<string, string> {
const rows = this.db.query('SELECT key, value FROM config').all() as Array<{ key: string; value: string }>;
return Object.fromEntries(rows.map(row => [row.key, row.value]));
}
/**
* Add a node to the registry
*/
addNode(name: string, type: string, chainId: string, home: string, moniker: string) {
this.db.run(
'INSERT OR REPLACE INTO nodes (name, type, chain_id, home, moniker, created_at) VALUES (?, ?, ?, ?, ?, ?)',
[name, type, chainId, home, moniker, Date.now()]
);
}
/**
* Get a node from the registry
*/
getNode(name: string): any {
return this.db.query('SELECT * FROM nodes WHERE name = ?').get(name);
}
/**
* List all nodes
*/
listNodes(): any[] {
return this.db.query('SELECT * FROM nodes ORDER BY created_at DESC').all();
}
/**
* Remove a node
*/
removeNode(name: string) {
this.db.run('DELETE FROM nodes WHERE name = ?', [name]);
}
/**
* Close database connection
*/
close() {
this.db.close();
}
}
+39
View File
@@ -0,0 +1,39 @@
// Constants for sonrctl
import { homedir } from 'os';
import { join } from 'path';
export const DEFAULT_CHAIN_ID = 'sonrtest_1-1';
export const DEFAULT_DENOM = 'usnr';
export const DEFAULT_HOME_DIR = join(homedir(), '.sonr');
export const DEFAULT_KEYRING_BACKEND = 'test';
export const DEFAULT_BINARY = 'snrd';
export const DEFAULT_DOCKER_IMAGE = 'onsonr/snrd:latest';
export const DEFAULT_PORTS = {
rpc: 26657,
rest: 1317,
grpc: 9090,
grpcWeb: 9091,
jsonRpc: 8545,
jsonRpcWs: 8546,
p2p: 26656,
};
export const CONSENSUS_TIMEOUTS = {
propose: '5s',
prevote: '1s',
precommit: '1s',
commit: '1s',
};
export const TESTNET_VALIDATORS = [
{ name: 'val-naruto', network: 'net-naruto' },
{ name: 'val-senku', network: 'net-senku' },
{ name: 'val-yaeger', network: 'net-yaeger' },
];
export const TESTNET_SENTRIES = [
{ name: 'sentry-naruto', network: 'net-naruto', validator: 'val-naruto' },
{ name: 'sentry-senku', network: 'net-senku', validator: 'val-senku' },
{ name: 'sentry-yaeger', network: 'net-yaeger', validator: 'val-yaeger' },
];
+165
View File
@@ -0,0 +1,165 @@
// Docker utilities using Bun.$
import type { DockerNode } from '../types';
import { Logger } from './logger';
export class DockerManager {
/**
* Check if Docker is installed and running
*/
static async isAvailable(): Promise<boolean> {
try {
const result = await Bun.$`docker info`.quiet();
return result.exitCode === 0;
} catch {
return false;
}
}
/**
* Check if docker-compose is available
*/
static async isComposeAvailable(): Promise<boolean> {
try {
const result = await Bun.$`docker compose version`.quiet();
return result.exitCode === 0;
} catch {
return false;
}
}
/**
* Pull a Docker image
*/
static async pullImage(image: string): Promise<void> {
Logger.info(`Pulling Docker image: ${image}`);
const stop = Logger.loading('Pulling image...');
try {
await Bun.$`docker pull ${image}`.quiet();
stop();
Logger.success(`Image pulled: ${image}`);
} catch (error) {
stop();
throw new Error(`Failed to pull image: ${error}`);
}
}
/**
* Check if a container is running
*/
static async isContainerRunning(name: string): Promise<boolean> {
try {
const result = await Bun.$`docker ps --filter name=${name} --format {{.Names}}`.quiet();
const output = await result.text();
return output.trim() === name;
} catch {
return false;
}
}
/**
* Start a container
*/
static async startContainer(name: string): Promise<void> {
Logger.info(`Starting container: ${name}`);
await Bun.$`docker start ${name}`;
Logger.success(`Container started: ${name}`);
}
/**
* Stop a container
*/
static async stopContainer(name: string): Promise<void> {
Logger.info(`Stopping container: ${name}`);
await Bun.$`docker stop ${name}`;
Logger.success(`Container stopped: ${name}`);
}
/**
* Remove a container
*/
static async removeContainer(name: string, force: boolean = false): Promise<void> {
Logger.info(`Removing container: ${name}`);
const forceFlag = force ? '-f' : '';
await Bun.$`docker rm ${forceFlag} ${name}`;
Logger.success(`Container removed: ${name}`);
}
/**
* Run a command in a container
*/
static async exec(container: string, command: string): Promise<string> {
const result = await Bun.$`docker exec ${container} sh -c ${command}`.quiet();
return await result.text();
}
/**
* Get container logs
*/
static async logs(container: string, tail: number = 50): Promise<string> {
const result = await Bun.$`docker logs ${container} --tail ${tail}`.quiet();
return await result.text();
}
/**
* Run docker-compose command
*/
static async composeUp(composePath: string, detached: boolean = true): Promise<void> {
const cwd = Bun.file(composePath).name ? composePath.split('/').slice(0, -1).join('/') : composePath;
const flags = detached ? '-d' : '';
Logger.info('Starting network with docker-compose...');
await Bun.$`cd ${cwd} && docker compose up ${flags}`;
Logger.success('Network started successfully');
}
/**
* Stop docker-compose services
*/
static async composeDown(composePath: string): Promise<void> {
const cwd = Bun.file(composePath).name ? composePath.split('/').slice(0, -1).join('/') : composePath;
Logger.info('Stopping network...');
await Bun.$`cd ${cwd} && docker compose down`;
Logger.success('Network stopped successfully');
}
/**
* Get container status
*/
static async getContainerStatus(name: string): Promise<{ running: boolean; status: string }> {
try {
const result = await Bun.$`docker ps -a --filter name=${name} --format {{.Status}}`.quiet();
const status = (await result.text()).trim();
const running = status.startsWith('Up');
return { running, status };
} catch {
return { running: false, status: 'Not found' };
}
}
/**
* List all containers with a prefix
*/
static async listContainers(prefix?: string): Promise<Array<{ name: string; status: string; image: string }>> {
try {
const filter = prefix ? `--filter name=${prefix}` : '';
const result = await Bun.$`docker ps -a ${filter} --format {{.Names}}|||{{.Status}}|||{{.Image}}`.quiet();
const output = await result.text();
if (!output.trim()) {
return [];
}
return output
.trim()
.split('\n')
.map(line => {
const [name, status, image] = line.split('|||');
return { name, status, image };
});
} catch {
return [];
}
}
}
+102
View File
@@ -0,0 +1,102 @@
// Beautiful logging utilities for sonrctl
const COLORS = {
reset: '\x1b[0m',
bright: '\x1b[1m',
dim: '\x1b[2m',
// Foreground colors
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m',
gray: '\x1b[90m',
} as const;
const ICONS = {
info: '',
success: '✓',
warning: '⚠',
error: '✗',
rocket: '🚀',
gear: '⚙',
chain: '⛓',
node: '🔗',
docker: '🐳',
};
export class Logger {
static info(message: string, ...args: any[]) {
console.log(`${COLORS.blue}${ICONS.info}${COLORS.reset} ${message}`, ...args);
}
static success(message: string, ...args: any[]) {
console.log(`${COLORS.green}${ICONS.success}${COLORS.reset} ${message}`, ...args);
}
static warn(message: string, ...args: any[]) {
console.log(`${COLORS.yellow}${ICONS.warning}${COLORS.reset} ${message}`, ...args);
}
static error(message: string, ...args: any[]) {
console.error(`${COLORS.red}${ICONS.error}${COLORS.reset} ${message}`, ...args);
}
static debug(message: string, ...args: any[]) {
if (process.env.DEBUG) {
console.log(`${COLORS.gray}[DEBUG]${COLORS.reset} ${message}`, ...args);
}
}
static header(message: string) {
const line = '═'.repeat(message.length + 4);
console.log(`\n${COLORS.cyan}${line}${COLORS.reset}`);
console.log(`${COLORS.cyan} ${message}${COLORS.reset}`);
console.log(`${COLORS.cyan}${line}${COLORS.reset}\n`);
}
static section(message: string) {
console.log(`\n${COLORS.bright}${COLORS.cyan}${message}${COLORS.reset}`);
}
static step(step: number, total: number, message: string) {
console.log(`${COLORS.dim}[${step}/${total}]${COLORS.reset} ${message}`);
}
static command(cmd: string) {
console.log(`${COLORS.dim}$ ${cmd}${COLORS.reset}`);
}
static json(obj: any) {
console.log(JSON.stringify(obj, null, 2));
}
static table(data: Record<string, any>) {
const maxKeyLength = Math.max(...Object.keys(data).map(k => k.length));
console.log();
for (const [key, value] of Object.entries(data)) {
const paddedKey = key.padEnd(maxKeyLength);
console.log(` ${COLORS.cyan}${paddedKey}${COLORS.reset} : ${value}`);
}
console.log();
}
static loading(message: string): () => void {
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
let i = 0;
const timer = setInterval(() => {
process.stdout.write(`\r${COLORS.blue}${frames[i]}${COLORS.reset} ${message}`);
i = (i + 1) % frames.length;
}, 80);
return () => {
clearInterval(timer);
process.stdout.write('\r\x1b[K'); // Clear line
};
}
}
+173
View File
@@ -0,0 +1,173 @@
// Node operations utilities
import { join } from 'path';
import { Logger } from './logger';
import { TomlManager } from './toml';
import { DEFAULT_PORTS, CONSENSUS_TIMEOUTS } from './constants';
import type { NodeConfig } from '../types';
export class NodeManager {
/**
* Check if snrd binary is available
*/
static async isBinaryAvailable(binary: string = 'snrd'): Promise<boolean> {
try {
const result = await Bun.$`which ${binary}`.quiet();
return result.exitCode === 0;
} catch {
return false;
}
}
/**
* Get snrd version
*/
static async getVersion(binary: string = 'snrd'): Promise<string> {
try {
const result = await Bun.$`${binary} version`.quiet();
return (await result.text()).trim();
} catch (error) {
throw new Error(`Failed to get version: ${error}`);
}
}
/**
* Initialize a node
*/
static async init(config: NodeConfig, binary: string = 'snrd'): Promise<void> {
Logger.section(`Initializing ${config.nodeType} node: ${config.moniker}`);
// Initialize chain
Logger.step(1, 3, 'Initializing chain directory');
await Bun.$`${binary} init ${config.moniker} --chain-id ${config.chainId} --home ${config.home}`;
// Configure node
Logger.step(2, 3, 'Configuring node settings');
await this.configureNode(config);
// Set VRF keys if needed
Logger.step(3, 3, 'Generating VRF keypair');
try {
await this.generateVRFKey(config.home, binary);
} catch (error) {
Logger.warn('VRF key generation failed, but continuing...');
}
Logger.success(`Node initialized: ${config.moniker}`);
}
/**
* Configure node settings
*/
static async configureNode(config: NodeConfig): Promise<void> {
const configToml = join(config.home, 'config', 'config.toml');
const appToml = join(config.home, 'config', 'app.toml');
// Configure RPC
if (config.ports?.rpc) {
await TomlManager.setValue(configToml, 'rpc.laddr', `tcp://0.0.0.0:${config.ports.rpc}`);
await TomlManager.setValue(configToml, 'rpc.cors_allowed_origins', ['*']);
}
// Configure P2P
await TomlManager.setValue(configToml, 'p2p.laddr', `tcp://0.0.0.0:${DEFAULT_PORTS.p2p}`);
// Set consensus timeouts
await TomlManager.setValue(configToml, 'consensus.timeout_propose', CONSENSUS_TIMEOUTS.propose);
await TomlManager.setValue(configToml, 'consensus.timeout_prevote', CONSENSUS_TIMEOUTS.prevote);
await TomlManager.setValue(configToml, 'consensus.timeout_precommit', CONSENSUS_TIMEOUTS.precommit);
await TomlManager.setValue(configToml, 'consensus.timeout_commit', CONSENSUS_TIMEOUTS.commit);
// Configure API
if (config.ports?.rest) {
await TomlManager.setValue(appToml, 'api.enable', true);
await TomlManager.setValue(appToml, 'api.address', `tcp://0.0.0.0:${config.ports.rest}`);
await TomlManager.setValue(appToml, 'api.enabled-unsafe-cors', true);
}
// Configure gRPC
if (config.ports?.grpc) {
await TomlManager.setValue(appToml, 'grpc.address', `0.0.0.0:${config.ports.grpc}`);
}
// Configure gRPC-Web
if (config.ports?.grpcWeb) {
await TomlManager.setValue(appToml, 'grpc-web.address', `0.0.0.0:${config.ports.grpcWeb}`);
}
// Configure JSON-RPC
if (config.ports?.jsonRpc && config.ports?.jsonRpcWs) {
await TomlManager.setValue(appToml, 'json-rpc.enable', true);
await TomlManager.setValue(appToml, 'json-rpc.address', `0.0.0.0:${config.ports.jsonRpc}`);
await TomlManager.setValue(appToml, 'json-rpc.ws-address', `0.0.0.0:${config.ports.jsonRpcWs}`);
await TomlManager.setValue(appToml, 'json-rpc.api', 'eth,txpool,personal,net,debug,web3');
}
// Set pruning
await TomlManager.setValue(appToml, 'pruning', 'nothing');
// Set minimum gas prices
await TomlManager.setValue(appToml, 'minimum-gas-prices', '0usnr');
}
/**
* Generate VRF keypair
*/
static async generateVRFKey(home: string, binary: string = 'snrd'): Promise<void> {
try {
await Bun.$`${binary} keys vrf generate --home ${home}`.quiet();
} catch (error) {
throw new Error(`Failed to generate VRF key: ${error}`);
}
}
/**
* Start a node
*/
static async start(home: string, chainId: string, binary: string = 'snrd'): Promise<void> {
Logger.info(`Starting node from: ${home}`);
await Bun.$`${binary} start --home ${home} --chain-id ${chainId} --pruning nothing --minimum-gas-prices 0usnr`;
}
/**
* Get node status
*/
static async getStatus(rpcUrl: string = 'http://localhost:26657'): Promise<any> {
try {
const response = await fetch(`${rpcUrl}/status`);
return await response.json();
} catch (error) {
throw new Error(`Failed to get node status: ${error}`);
}
}
/**
* Wait for node to be ready
*/
static async waitForReady(rpcUrl: string = 'http://localhost:26657', maxAttempts: number = 30): Promise<void> {
Logger.info('Waiting for node to be ready...');
for (let i = 0; i < maxAttempts; i++) {
try {
const response = await fetch(`${rpcUrl}/health`);
if (response.ok) {
Logger.success('Node is ready!');
return;
}
} catch {
// Ignore errors and retry
}
await Bun.sleep(1000);
}
throw new Error('Node failed to become ready');
}
/**
* Check if node is syncing
*/
static async isSyncing(rpcUrl: string = 'http://localhost:26657'): Promise<boolean> {
const status = await this.getStatus(rpcUrl);
return status.result.sync_info.catching_up;
}
}
+77
View File
@@ -0,0 +1,77 @@
// TOML utilities for configuration management
import TOML from '@iarna/toml';
export class TomlManager {
/**
* Read and parse a TOML file
*/
static async read(filePath: string): Promise<any> {
const file = Bun.file(filePath);
const content = await file.text();
return TOML.parse(content);
}
/**
* Write an object to a TOML file
*/
static async write(filePath: string, data: any): Promise<void> {
const content = TOML.stringify(data);
await Bun.write(filePath, content);
}
/**
* Set a value in a TOML file (supports nested paths with dot notation)
*/
static async setValue(filePath: string, path: string, value: any): Promise<void> {
const data = await this.read(filePath);
const keys = path.split('.');
let current = data;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!current[key]) {
current[key] = {};
}
current = current[key];
}
current[keys[keys.length - 1]] = value;
await this.write(filePath, data);
}
/**
* Get a value from a TOML file
*/
static async getValue(filePath: string, path: string): Promise<any> {
const data = await this.read(filePath);
const keys = path.split('.');
let current = data;
for (const key of keys) {
if (current[key] === undefined) {
return undefined;
}
current = current[key];
}
return current;
}
/**
* Merge configurations
*/
static merge(base: any, override: any): any {
const result = { ...base };
for (const [key, value] of Object.entries(override)) {
if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
result[key] = this.merge(result[key] || {}, value);
} else {
result[key] = value;
}
}
return result;
}
}
+53
View File
@@ -0,0 +1,53 @@
// Type definitions for sonrctl
export interface NodeConfig {
moniker: string;
chainId: string;
home: string;
nodeType: 'validator' | 'sentry' | 'full';
ports?: {
rpc?: number;
rest?: number;
grpc?: number;
grpcWeb?: number;
jsonRpc?: number;
jsonRpcWs?: number;
};
}
export interface NetworkConfig {
name: string;
chainId: string;
validators: ValidatorConfig[];
sentries: SentryConfig[];
}
export interface ValidatorConfig {
name: string;
moniker: string;
network: string;
home: string;
}
export interface SentryConfig {
name: string;
moniker: string;
network: string;
home: string;
exposedPorts: boolean;
}
export interface DockerNode {
name: string;
image: string;
command: string;
environment: Record<string, string>;
volumes: string[];
networks: string[];
ports?: string[];
}
export interface CommandContext {
args: string[];
options: Record<string, any>;
}
+29
View File
@@ -0,0 +1,29 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}