mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84536d3477 | ||
|
|
2a3ea4b8b4 | ||
|
|
4448a10430 | ||
|
|
bddf742cd9 | ||
|
|
d8356c03e2 | ||
|
|
3c7c4bb253 | ||
|
|
6dd00e8cf6 |
@@ -0,0 +1,19 @@
|
|||||||
|
[scopes]
|
||||||
|
deps = ["go.sum", "go.mod", "package.json", "bun.lock", "pnpm-lock.yaml"]
|
||||||
|
techdocs = ["docs"]
|
||||||
|
actions = [".github/workflows"]
|
||||||
|
config = [".github", "networks/localnet", "networks/testnet", "networks", "devbox.json", "devbox.lock", "docker-compose.yml", "wrangler.toml"]
|
||||||
|
cli = ["src", "app/commands", "cmd/sonrctl"]
|
||||||
|
cmd = ["cmd/snrd", "cmd"]
|
||||||
|
app = ["app/context", "app/decorators", "app/params", "app/upgrades", "app"]
|
||||||
|
ante = ["app/ante"]
|
||||||
|
api = ["api/dex", "api/did", "api/dwn", "api/svc", "api"]
|
||||||
|
proto = ["proto/dex", "proto/did", "proto/dwn", "proto/svc", "proto"]
|
||||||
|
dex = ["x/dex"]
|
||||||
|
did = ["x/did"]
|
||||||
|
dwn = ["x/dwn"]
|
||||||
|
svc = ["x/svc"]
|
||||||
|
chains = ["chains"]
|
||||||
|
scripts = ["scripts"]
|
||||||
|
test = ["test/e2e", "test/integration", "test/oauth", "test"]
|
||||||
|
build = ["Makefile"]
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"scope": "deps",
|
|
||||||
"path": "go.sum"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "deps",
|
|
||||||
"path": "go.mod"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "api",
|
|
||||||
"path": "api/dex"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "api",
|
|
||||||
"path": "api/did"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "api",
|
|
||||||
"path": "api/dwn"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "api",
|
|
||||||
"path": "api/svc"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "api",
|
|
||||||
"path": "api"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "ante",
|
|
||||||
"path": "app/ante"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "cli",
|
|
||||||
"path": "app/commands"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "app",
|
|
||||||
"path": "app/context"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "app",
|
|
||||||
"path": "app/decorators"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "app",
|
|
||||||
"path": "app/params"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "app",
|
|
||||||
"path": "app/upgrades"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "app",
|
|
||||||
"path": "app"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "chains",
|
|
||||||
"path": "chains"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "cmd",
|
|
||||||
"path": "cmd/snrd"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "cmd",
|
|
||||||
"path": "cmd"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "docs",
|
|
||||||
"path": "docs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "config",
|
|
||||||
"path": "networks/localnet"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "config",
|
|
||||||
"path": "networks/testnet"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "config",
|
|
||||||
"path": "networks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "dex",
|
|
||||||
"path": "proto/dex"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "did",
|
|
||||||
"path": "proto/did"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "dwn",
|
|
||||||
"path": "proto/dwn"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "svc",
|
|
||||||
"path": "proto/svc"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "proto",
|
|
||||||
"path": "proto"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "scripts",
|
|
||||||
"path": "scripts"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "test",
|
|
||||||
"path": "test/e2e"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "test",
|
|
||||||
"path": "test/integration"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "test",
|
|
||||||
"path": "test/oauth"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "test",
|
|
||||||
"path": "test"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "dex",
|
|
||||||
"path": "x/dex"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "did",
|
|
||||||
"path": "x/did"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "dwn",
|
|
||||||
"path": "x/dwn"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "svc",
|
|
||||||
"path": "x/svc"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "ci",
|
|
||||||
"path": "Makefile"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "ci",
|
|
||||||
"path": ".github"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"scope": "deps",
|
|
||||||
"path": "go.sum"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "deps",
|
|
||||||
"path": "go.mod"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "mdx",
|
|
||||||
"path": "docs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "ci",
|
|
||||||
"path": "Makefile"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "ci",
|
|
||||||
"path": ".github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "dex",
|
|
||||||
"path": "x/dex"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "dex",
|
|
||||||
"path": "proto/dex"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "did",
|
|
||||||
"path": "x/did"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "did",
|
|
||||||
"path": "proto/did"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "dwn",
|
|
||||||
"path": "x/dwn"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "dwn",
|
|
||||||
"path": "proto/dwn"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "svc",
|
|
||||||
"path": "x/svc"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "svc",
|
|
||||||
"path": "proto/svc"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "config",
|
|
||||||
"path": "networks"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -121,3 +121,4 @@ tmp-openapi-gen/**/*.json
|
|||||||
# Allow the final combined openapi.yml
|
# Allow the final combined openapi.yml
|
||||||
!docs/static/openapi.yml
|
!docs/static/openapi.yml
|
||||||
.mcp.json
|
.mcp.json
|
||||||
|
node_modules
|
||||||
|
|||||||
@@ -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**
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[scopes]
|
||||||
|
deps = ["go.sum", "go.mod"]
|
||||||
|
cli = ["src", "package.json", "app/commands"]
|
||||||
|
api = ["api/dex", "api/did", "api/dwn", "api/svc", "api"]
|
||||||
|
ante = ["app/ante"]
|
||||||
|
app = ["app/context", "app/decorators", "app/params", "app/upgrades", "app"]
|
||||||
|
chains = ["chains"]
|
||||||
|
cmd = ["cmd/snrd", "cmd"]
|
||||||
|
docs = ["docs"]
|
||||||
|
config = ["networks/localnet", "networks/testnet", "networks"]
|
||||||
|
dex = ["proto/dex", "x/dex"]
|
||||||
|
did = ["proto/did", "x/did"]
|
||||||
|
dwn = ["proto/dwn", "x/dwn"]
|
||||||
|
svc = ["proto/svc", "x/svc"]
|
||||||
|
proto = ["proto"]
|
||||||
|
scripts = ["scripts"]
|
||||||
|
test = ["test/e2e", "test/integration", "test/oauth", "test"]
|
||||||
|
ci = ["Makefile", ".github"]
|
||||||
@@ -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=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}`);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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
@@ -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);
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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' },
|
||||||
|
];
|
||||||
@@ -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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user