mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-05 10:51:41 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9db77a2bf4 |
@@ -1,19 +0,0 @@
|
|||||||
[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"]
|
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"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,4 +121,3 @@ 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
|
|
||||||
|
|||||||
@@ -1,396 +0,0 @@
|
|||||||
# 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**
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
[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"]
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
"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=="],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
// 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}`);
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
// 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();
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
// 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');
|
|
||||||
}
|
|
||||||
-161
@@ -1,161 +0,0 @@
|
|||||||
#!/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);
|
|
||||||
});
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
// 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
// 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' },
|
|
||||||
];
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
// 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 [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
// 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
@@ -1,173 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
// 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>;
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
# Noble Testnet Integration for Sonr DEX Module
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document describes the integration of the Noble testnet with Sonr's x/dex module. Noble is a Cosmos appchain purpose-built for native asset issuance, particularly USDC, in the IBC ecosystem.
|
||||||
|
|
||||||
|
## What is Noble?
|
||||||
|
|
||||||
|
Noble is an application-specific blockchain built on the Cosmos SDK that serves as the canonical issuance hub for USDC and other real-world assets (RWAs) in the Cosmos ecosystem. Key features:
|
||||||
|
|
||||||
|
- **Native USDC Issuance**: Circle's official USDC is natively issued on Noble
|
||||||
|
- **IBC Connectivity**: Seamlessly transfers assets across 50+ IBC-enabled chains
|
||||||
|
- **Packet Forwarding**: Enables "1-click" asset transfers between chains
|
||||||
|
- **CCTP Integration**: Circle's Cross-Chain Transfer Protocol for bridging from EVM chains
|
||||||
|
|
||||||
|
## Noble Testnet Configuration
|
||||||
|
|
||||||
|
### Chain Details
|
||||||
|
|
||||||
|
- **Chain ID**: `noble-grand-1`
|
||||||
|
- **RPC Endpoint**: `https://noble-testnet-rpc.polkachu.com:443`
|
||||||
|
- **gRPC Endpoint**: `noble-testnet-grpc.polkachu.com:21590`
|
||||||
|
- **USDC Denomination**: `uusdc` (micro-USDC, 6 decimals)
|
||||||
|
|
||||||
|
### Default Configuration
|
||||||
|
|
||||||
|
The Noble testnet is included in the default DEX module parameters:
|
||||||
|
|
||||||
|
```go
|
||||||
|
AllowedConnections: []string{
|
||||||
|
"noble-grand-1", // Noble testnet - USDC hub
|
||||||
|
"osmo-test-5", // Osmosis testnet
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration Features
|
||||||
|
|
||||||
|
### 1. USDC Helper Functions
|
||||||
|
|
||||||
|
The integration includes helper functions for working with USDC:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Convert base units to USDC
|
||||||
|
usdcAmount := ConvertToUSDC(sdk.NewInt(1500000)) // Returns 1.5 USDC
|
||||||
|
|
||||||
|
// Convert USDC to base units
|
||||||
|
baseUnits := ConvertFromUSDC(sdk.MustNewDecFromStr("1.5")) // Returns 1500000
|
||||||
|
|
||||||
|
// Format for display
|
||||||
|
formatted := FormatUSDCAmount(sdk.NewInt(1500000)) // Returns "1.500000 USDC"
|
||||||
|
|
||||||
|
// Parse from string
|
||||||
|
amount, err := ParseUSDCAmount("1.5") // Returns 1500000 base units
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Chain Configuration
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Get Noble testnet configuration
|
||||||
|
config := GetNobleTestnetConfig()
|
||||||
|
// Returns: NobleChainConfig{
|
||||||
|
// ChainID: "noble-grand-1",
|
||||||
|
// RPCEndpoint: "https://noble-testnet-rpc.polkachu.com:443",
|
||||||
|
// GRPCEndpoint: "noble-testnet-grpc.polkachu.com:21590",
|
||||||
|
// USDCDenom: "uusdc",
|
||||||
|
// Decimals: 6,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Check if a chain is Noble
|
||||||
|
isNoble := IsNobleChain("noble-grand-1") // Returns true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Trading Pairs
|
||||||
|
|
||||||
|
Common USDC trading pairs are predefined:
|
||||||
|
|
||||||
|
```go
|
||||||
|
pairs := GetNobleUSDCPairs()
|
||||||
|
// Returns pairs like:
|
||||||
|
// - ATOM/USDC
|
||||||
|
// - OSMO/USDC
|
||||||
|
// - AKT/USDC
|
||||||
|
// - JUNO/USDC
|
||||||
|
// - STARS/USDC
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Swap Parameters
|
||||||
|
|
||||||
|
Structured parameters for Noble swaps:
|
||||||
|
|
||||||
|
```go
|
||||||
|
swapParams := NobleSwapParams{
|
||||||
|
InputDenom: "uatom",
|
||||||
|
OutputDenom: "uusdc",
|
||||||
|
Amount: sdk.NewInt(1000000), // 1 ATOM
|
||||||
|
MinOutput: sdk.NewInt(5000000), // Min 5 USDC
|
||||||
|
Receiver: "sonr1...",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := swapParams.Validate(); err != nil {
|
||||||
|
// Handle validation error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Liquidity Parameters
|
||||||
|
|
||||||
|
Parameters for providing liquidity:
|
||||||
|
|
||||||
|
```go
|
||||||
|
liquidityParams := NobleLiquidityParams{
|
||||||
|
PoolID: "1",
|
||||||
|
Token0: "uusdc",
|
||||||
|
Token1: "uatom",
|
||||||
|
Amount0: sdk.NewInt(1000000), // 1 USDC
|
||||||
|
Amount1: sdk.NewInt(100000), // 0.1 ATOM
|
||||||
|
MinShares: sdk.NewInt(1),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := liquidityParams.Validate(); err != nil {
|
||||||
|
// Handle validation error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Registering a DEX Account for Noble
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Register an Interchain Account on Noble testnet
|
||||||
|
sonrd tx dex register-dex-account \
|
||||||
|
did:sonr:user:abc123 \
|
||||||
|
connection-0 \
|
||||||
|
noble-grand-1 \
|
||||||
|
--from mykey \
|
||||||
|
--chain-id sonr_1-1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Executing a Swap via Noble
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Swap ATOM for USDC on Noble
|
||||||
|
sonrd tx dex execute-swap \
|
||||||
|
did:sonr:user:abc123 \
|
||||||
|
connection-0 \
|
||||||
|
uatom \
|
||||||
|
uusdc \
|
||||||
|
1000000 \
|
||||||
|
5000000 \
|
||||||
|
--from mykey \
|
||||||
|
--chain-id sonr_1-1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Querying USDC Balance
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Query USDC balance on Noble for a DEX account
|
||||||
|
sonrd query dex balance \
|
||||||
|
did:sonr:user:abc123 \
|
||||||
|
connection-0 \
|
||||||
|
uusdc
|
||||||
|
```
|
||||||
|
|
||||||
|
## Module Parameters
|
||||||
|
|
||||||
|
The DEX module includes the following default parameters for Noble integration:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
params:
|
||||||
|
enabled: true
|
||||||
|
max_accounts_per_did: 5
|
||||||
|
default_timeout_seconds: 600
|
||||||
|
allowed_connections:
|
||||||
|
- noble-grand-1 # Noble testnet
|
||||||
|
- osmo-test-5 # Osmosis testnet
|
||||||
|
min_swap_amount: "1000"
|
||||||
|
max_daily_volume: "1000000000000"
|
||||||
|
rate_limits:
|
||||||
|
max_ops_per_block: 10
|
||||||
|
max_ops_per_did_per_day: 100
|
||||||
|
cooldown_blocks: 5
|
||||||
|
fees:
|
||||||
|
swap_fee_bps: 30 # 0.3%
|
||||||
|
liquidity_fee_bps: 20 # 0.2%
|
||||||
|
order_fee_bps: 10 # 0.1%
|
||||||
|
fee_collector: ""
|
||||||
|
```
|
||||||
|
|
||||||
|
## IBC Connection Setup
|
||||||
|
|
||||||
|
To establish an IBC connection with Noble testnet:
|
||||||
|
|
||||||
|
### 1. Create IBC Client
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create IBC client for Noble on Sonr chain
|
||||||
|
hermes create client \
|
||||||
|
--host-chain sonr_1-1 \
|
||||||
|
--reference-chain noble-grand-1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create IBC Connection
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create IBC connection
|
||||||
|
hermes create connection \
|
||||||
|
--a-chain sonr_1-1 \
|
||||||
|
--b-chain noble-grand-1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Create Transfer Channel
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create transfer channel
|
||||||
|
hermes create channel \
|
||||||
|
--a-chain sonr_1-1 \
|
||||||
|
--a-connection connection-0 \
|
||||||
|
--a-port transfer \
|
||||||
|
--b-port transfer
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Verify Connection
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Query IBC connections
|
||||||
|
sonrd query ibc connection connections
|
||||||
|
|
||||||
|
# Query IBC channels
|
||||||
|
sonrd query ibc channel channels
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### 1. Connection Whitelisting
|
||||||
|
|
||||||
|
Only connections listed in `allowed_connections` parameter can be used:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func ValidateNobleConnection(connectionID string, allowedConnections []string) error {
|
||||||
|
for _, allowed := range allowedConnections {
|
||||||
|
if connectionID == allowed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("connection %s not in allowed connections list", connectionID)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Rate Limiting
|
||||||
|
|
||||||
|
The module implements rate limiting to prevent abuse:
|
||||||
|
|
||||||
|
- **Per Block**: Maximum 10 operations per block
|
||||||
|
- **Per DID**: Maximum 100 operations per day
|
||||||
|
- **Cooldown**: 5 block cooldown between operations
|
||||||
|
|
||||||
|
### 3. Amount Validation
|
||||||
|
|
||||||
|
All amounts are validated before execution:
|
||||||
|
|
||||||
|
- Minimum swap amount: 1000 base units
|
||||||
|
- Maximum daily volume per DID: 1,000,000,000,000 base units
|
||||||
|
- Positive amount checks on all operations
|
||||||
|
|
||||||
|
### 4. UCAN Authorization
|
||||||
|
|
||||||
|
All DEX operations require valid UCAN tokens:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// UCAN capabilities for DEX operations
|
||||||
|
capabilities := []string{
|
||||||
|
"dex/swap",
|
||||||
|
"dex/liquidity/provide",
|
||||||
|
"dex/liquidity/remove",
|
||||||
|
"dex/order/create",
|
||||||
|
"dex/order/cancel",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration Testing
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run DEX module tests
|
||||||
|
cd x/dex
|
||||||
|
go test -v ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
### E2E Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run end-to-end tests
|
||||||
|
cd test/e2e
|
||||||
|
go test -v -run TestDEXModuleOperations
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Testing
|
||||||
|
|
||||||
|
1. **Start local testnet**:
|
||||||
|
```bash
|
||||||
|
cd networks/testnet
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Register account**:
|
||||||
|
```bash
|
||||||
|
sonrd tx dex register-dex-account did:sonr:test connection-0 noble-grand-1 --from test
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Execute swap**:
|
||||||
|
```bash
|
||||||
|
sonrd tx dex execute-swap did:sonr:test connection-0 uatom uusdc 1000000 500000 --from test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
### Phase 1: Testnet Integration ✅
|
||||||
|
- [x] Add Noble testnet to allowed connections
|
||||||
|
- [x] Create USDC helper functions
|
||||||
|
- [x] Implement chain configuration
|
||||||
|
- [x] Add documentation
|
||||||
|
|
||||||
|
### Phase 2: Enhanced Features 🚧
|
||||||
|
- [ ] Implement actual ICA swap execution
|
||||||
|
- [ ] Add multi-hop routing via USDC
|
||||||
|
- [ ] Integrate with Osmosis pools
|
||||||
|
- [ ] Add limit order support
|
||||||
|
|
||||||
|
### Phase 3: Mainnet Deployment 📋
|
||||||
|
- [ ] Security audit
|
||||||
|
- [ ] Add Noble mainnet configuration
|
||||||
|
- [ ] Implement advanced slippage protection
|
||||||
|
- [ ] Add monitoring and alerting
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
### Noble Documentation
|
||||||
|
- **Developer Hub**: https://www.noble.xyz/dev-hub
|
||||||
|
- **Docs**: https://docs.noble.xyz
|
||||||
|
- **GitHub**: https://github.com/noble-assets/noble
|
||||||
|
|
||||||
|
### Cosmos IBC
|
||||||
|
- **IBC Protocol**: https://ibc.cosmos.network
|
||||||
|
- **ICA Controller**: https://github.com/cosmos/ibc-go/tree/main/modules/apps/27-interchain-accounts
|
||||||
|
|
||||||
|
### Circle USDC
|
||||||
|
- **USDC on Noble**: https://www.circle.com/multi-chain-usdc/noble
|
||||||
|
- **CCTP**: https://www.circle.com/en/cross-chain-transfer-protocol
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For questions or issues with the Noble integration:
|
||||||
|
|
||||||
|
1. Check the [DEX Module README](README.md)
|
||||||
|
2. Review the [Noble documentation](https://docs.noble.xyz)
|
||||||
|
3. Open an issue on GitHub
|
||||||
|
4. Join the Sonr Discord community
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This integration is part of the Sonr blockchain and follows the same license terms.
|
||||||
@@ -5,6 +5,7 @@ import host "github.com/cosmos/ibc-go/v8/modules/core/24-host"
|
|||||||
// DefaultGenesisState returns the default module GenesisState.
|
// DefaultGenesisState returns the default module GenesisState.
|
||||||
func DefaultGenesisState() *GenesisState {
|
func DefaultGenesisState() *GenesisState {
|
||||||
return &GenesisState{
|
return &GenesisState{
|
||||||
|
Params: DefaultParams(),
|
||||||
PortId: PortID,
|
PortId: PortID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,6 +13,7 @@ func DefaultGenesisState() *GenesisState {
|
|||||||
// NewGenesisState initializes and returns a new GenesisState.
|
// NewGenesisState initializes and returns a new GenesisState.
|
||||||
func NewGenesisState() *GenesisState {
|
func NewGenesisState() *GenesisState {
|
||||||
return &GenesisState{
|
return &GenesisState{
|
||||||
|
Params: DefaultParams(),
|
||||||
PortId: PortID,
|
PortId: PortID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,5 +24,10 @@ func (gs *GenesisState) Validate() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate params
|
||||||
|
if err := gs.Params.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Noble testnet chain configuration
|
||||||
|
const (
|
||||||
|
// NobleTestnetChainID is the chain ID for Noble testnet (grand-1)
|
||||||
|
NobleTestnetChainID = "noble-grand-1"
|
||||||
|
|
||||||
|
// NobleMainnetChainID is the chain ID for Noble mainnet
|
||||||
|
NobleMainnetChainID = "noble-1"
|
||||||
|
|
||||||
|
// NobleUSDCDenom is the denomination for USDC on Noble
|
||||||
|
NobleUSDCDenom = "uusdc"
|
||||||
|
|
||||||
|
// NobleUSDCDecimals is the number of decimals for USDC (6 decimals like native USDC)
|
||||||
|
NobleUSDCDecimals = 6
|
||||||
|
)
|
||||||
|
|
||||||
|
// NobleChainConfig holds configuration for Noble blockchain integration
|
||||||
|
type NobleChainConfig struct {
|
||||||
|
ChainID string
|
||||||
|
RPCEndpoint string
|
||||||
|
GRPCEndpoint string
|
||||||
|
USDCDenom string
|
||||||
|
Decimals uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNobleTestnetConfig returns the configuration for Noble testnet
|
||||||
|
func GetNobleTestnetConfig() NobleChainConfig {
|
||||||
|
return NobleChainConfig{
|
||||||
|
ChainID: NobleTestnetChainID,
|
||||||
|
RPCEndpoint: "https://noble-testnet-rpc.polkachu.com:443",
|
||||||
|
GRPCEndpoint: "noble-testnet-grpc.polkachu.com:21590",
|
||||||
|
USDCDenom: NobleUSDCDenom,
|
||||||
|
Decimals: NobleUSDCDecimals,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNobleMainnetConfig returns the configuration for Noble mainnet
|
||||||
|
func GetNobleMainnetConfig() NobleChainConfig {
|
||||||
|
return NobleChainConfig{
|
||||||
|
ChainID: NobleMainnetChainID,
|
||||||
|
RPCEndpoint: "https://noble-rpc.polkachu.com:443",
|
||||||
|
GRPCEndpoint: "noble-grpc.polkachu.com:21590",
|
||||||
|
USDCDenom: NobleUSDCDenom,
|
||||||
|
Decimals: NobleUSDCDecimals,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNobleChain checks if a chain ID corresponds to Noble (testnet or mainnet)
|
||||||
|
func IsNobleChain(chainID string) bool {
|
||||||
|
return chainID == NobleTestnetChainID || chainID == NobleMainnetChainID
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateNobleConnection validates that a connection ID is properly configured for Noble
|
||||||
|
func ValidateNobleConnection(connectionID string, allowedConnections []string) error {
|
||||||
|
for _, allowed := range allowedConnections {
|
||||||
|
if connectionID == allowed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("connection %s not in allowed connections list", connectionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertToUSDC converts an amount from base units to USDC representation
|
||||||
|
// For example: 1000000 base units = 1.000000 USDC
|
||||||
|
func ConvertToUSDC(amount sdk.Int) sdk.Dec {
|
||||||
|
// Convert to decimal and divide by 10^6
|
||||||
|
return sdk.NewDecFromInt(amount).QuoInt64(1000000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertFromUSDC converts USDC amount to base units
|
||||||
|
// For example: 1.5 USDC = 1500000 base units
|
||||||
|
func ConvertFromUSDC(usdcAmount sdk.Dec) sdk.Int {
|
||||||
|
// Multiply by 10^6 and truncate to integer
|
||||||
|
return usdcAmount.MulInt64(1000000).TruncateInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatUSDCAmount formats a USDC amount for display
|
||||||
|
// For example: 1500000 -> "1.500000 USDC"
|
||||||
|
func FormatUSDCAmount(amount sdk.Int) string {
|
||||||
|
usdcDec := ConvertToUSDC(amount)
|
||||||
|
return fmt.Sprintf("%s USDC", usdcDec.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseUSDCAmount parses a string USDC amount to base units
|
||||||
|
// For example: "1.5" -> 1500000
|
||||||
|
func ParseUSDCAmount(amountStr string) (sdk.Int, error) {
|
||||||
|
usdcDec, err := sdk.NewDecFromStr(amountStr)
|
||||||
|
if err != nil {
|
||||||
|
return sdk.ZeroInt(), fmt.Errorf("invalid USDC amount: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if usdcDec.IsNegative() {
|
||||||
|
return sdk.ZeroInt(), fmt.Errorf("USDC amount cannot be negative")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ConvertFromUSDC(usdcDec), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NobleSwapParams defines parameters for a swap on Noble or via Noble USDC
|
||||||
|
type NobleSwapParams struct {
|
||||||
|
// InputDenom is the denomination being swapped from
|
||||||
|
InputDenom string
|
||||||
|
// OutputDenom is the denomination being swapped to
|
||||||
|
OutputDenom string
|
||||||
|
// Amount is the input amount in base units
|
||||||
|
Amount sdk.Int
|
||||||
|
// MinOutput is the minimum output amount (slippage protection)
|
||||||
|
MinOutput sdk.Int
|
||||||
|
// Receiver is the address to receive the output tokens
|
||||||
|
Receiver string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate performs basic validation on NobleSwapParams
|
||||||
|
func (p NobleSwapParams) Validate() error {
|
||||||
|
if p.InputDenom == "" {
|
||||||
|
return fmt.Errorf("input denom cannot be empty")
|
||||||
|
}
|
||||||
|
if p.OutputDenom == "" {
|
||||||
|
return fmt.Errorf("output denom cannot be empty")
|
||||||
|
}
|
||||||
|
if p.InputDenom == p.OutputDenom {
|
||||||
|
return fmt.Errorf("input and output denoms must be different")
|
||||||
|
}
|
||||||
|
if !p.Amount.IsPositive() {
|
||||||
|
return fmt.Errorf("amount must be positive")
|
||||||
|
}
|
||||||
|
if !p.MinOutput.IsPositive() {
|
||||||
|
return fmt.Errorf("min output must be positive")
|
||||||
|
}
|
||||||
|
if p.Receiver == "" {
|
||||||
|
return fmt.Errorf("receiver cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate receiver is a valid bech32 address
|
||||||
|
_, err := sdk.AccAddressFromBech32(p.Receiver)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid receiver address: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NobleLiquidityParams defines parameters for providing liquidity via Noble
|
||||||
|
type NobleLiquidityParams struct {
|
||||||
|
// PoolID is the target pool identifier
|
||||||
|
PoolID string
|
||||||
|
// Token0 is the first token denomination
|
||||||
|
Token0 string
|
||||||
|
// Token1 is the second token denomination
|
||||||
|
Token1 string
|
||||||
|
// Amount0 is the amount of first token
|
||||||
|
Amount0 sdk.Int
|
||||||
|
// Amount1 is the amount of second token
|
||||||
|
Amount1 sdk.Int
|
||||||
|
// MinShares is the minimum LP shares to receive
|
||||||
|
MinShares sdk.Int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate performs basic validation on NobleLiquidityParams
|
||||||
|
func (p NobleLiquidityParams) Validate() error {
|
||||||
|
if p.PoolID == "" {
|
||||||
|
return fmt.Errorf("pool ID cannot be empty")
|
||||||
|
}
|
||||||
|
if p.Token0 == "" {
|
||||||
|
return fmt.Errorf("token0 cannot be empty")
|
||||||
|
}
|
||||||
|
if p.Token1 == "" {
|
||||||
|
return fmt.Errorf("token1 cannot be empty")
|
||||||
|
}
|
||||||
|
if !p.Amount0.IsPositive() {
|
||||||
|
return fmt.Errorf("amount0 must be positive")
|
||||||
|
}
|
||||||
|
if !p.Amount1.IsPositive() {
|
||||||
|
return fmt.Errorf("amount1 must be positive")
|
||||||
|
}
|
||||||
|
if !p.MinShares.IsPositive() {
|
||||||
|
return fmt.Errorf("min shares must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNobleUSDCPairs returns common trading pairs that include Noble USDC
|
||||||
|
// This is useful for routing swaps through USDC as an intermediary
|
||||||
|
func GetNobleUSDCPairs() []TradingPair {
|
||||||
|
return []TradingPair{
|
||||||
|
{Base: "uatom", Quote: NobleUSDCDenom, Description: "ATOM/USDC"},
|
||||||
|
{Base: "uosmo", Quote: NobleUSDCDenom, Description: "OSMO/USDC"},
|
||||||
|
{Base: "uakt", Quote: NobleUSDCDenom, Description: "AKT/USDC"},
|
||||||
|
{Base: "ujuno", Quote: NobleUSDCDenom, Description: "JUNO/USDC"},
|
||||||
|
{Base: "ustars", Quote: NobleUSDCDenom, Description: "STARS/USDC"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TradingPair represents a trading pair on a DEX
|
||||||
|
type TradingPair struct {
|
||||||
|
Base string
|
||||||
|
Quote string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of the trading pair
|
||||||
|
func (tp TradingPair) String() string {
|
||||||
|
if tp.Description != "" {
|
||||||
|
return tp.Description
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s/%s", tp.Base, tp.Quote)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse returns the reverse trading pair (swaps base and quote)
|
||||||
|
func (tp TradingPair) Reverse() TradingPair {
|
||||||
|
return TradingPair{
|
||||||
|
Base: tp.Quote,
|
||||||
|
Quote: tp.Base,
|
||||||
|
Description: fmt.Sprintf("%s/%s", tp.Quote, tp.Base),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,533 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetNobleTestnetConfig(t *testing.T) {
|
||||||
|
config := GetNobleTestnetConfig()
|
||||||
|
|
||||||
|
require.Equal(t, NobleTestnetChainID, config.ChainID)
|
||||||
|
require.Equal(t, "https://noble-testnet-rpc.polkachu.com:443", config.RPCEndpoint)
|
||||||
|
require.Equal(t, "noble-testnet-grpc.polkachu.com:21590", config.GRPCEndpoint)
|
||||||
|
require.Equal(t, NobleUSDCDenom, config.USDCDenom)
|
||||||
|
require.Equal(t, uint8(6), config.Decimals)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNobleMainnetConfig(t *testing.T) {
|
||||||
|
config := GetNobleMainnetConfig()
|
||||||
|
|
||||||
|
require.Equal(t, NobleMainnetChainID, config.ChainID)
|
||||||
|
require.Equal(t, "https://noble-rpc.polkachu.com:443", config.RPCEndpoint)
|
||||||
|
require.Equal(t, "noble-grpc.polkachu.com:21590", config.GRPCEndpoint)
|
||||||
|
require.Equal(t, NobleUSDCDenom, config.USDCDenom)
|
||||||
|
require.Equal(t, uint8(6), config.Decimals)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsNobleChain(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
chainID string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "noble testnet",
|
||||||
|
chainID: NobleTestnetChainID,
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "noble mainnet",
|
||||||
|
chainID: NobleMainnetChainID,
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "osmosis",
|
||||||
|
chainID: "osmosis-1",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cosmos hub",
|
||||||
|
chainID: "cosmoshub-4",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := IsNobleChain(tc.chainID)
|
||||||
|
require.Equal(t, tc.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateNobleConnection(t *testing.T) {
|
||||||
|
allowedConnections := []string{"noble-grand-1", "osmo-test-5"}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
connectionID string
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid noble connection",
|
||||||
|
connectionID: "noble-grand-1",
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid osmosis connection",
|
||||||
|
connectionID: "osmo-test-5",
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid connection",
|
||||||
|
connectionID: "unknown-chain",
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := ValidateNobleConnection(tc.connectionID, allowedConnections)
|
||||||
|
if tc.expectErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertToUSDC(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
amount sdk.Int
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "1 USDC",
|
||||||
|
amount: sdk.NewInt(1000000),
|
||||||
|
expected: "1.000000000000000000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "1.5 USDC",
|
||||||
|
amount: sdk.NewInt(1500000),
|
||||||
|
expected: "1.500000000000000000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "0.1 USDC",
|
||||||
|
amount: sdk.NewInt(100000),
|
||||||
|
expected: "0.100000000000000000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "0 USDC",
|
||||||
|
amount: sdk.ZeroInt(),
|
||||||
|
expected: "0.000000000000000000",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := ConvertToUSDC(tc.amount)
|
||||||
|
require.Equal(t, tc.expected, result.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertFromUSDC(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
usdc sdk.Dec
|
||||||
|
expected int64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "1 USDC",
|
||||||
|
usdc: sdk.MustNewDecFromStr("1.0"),
|
||||||
|
expected: 1000000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "1.5 USDC",
|
||||||
|
usdc: sdk.MustNewDecFromStr("1.5"),
|
||||||
|
expected: 1500000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "0.1 USDC",
|
||||||
|
usdc: sdk.MustNewDecFromStr("0.1"),
|
||||||
|
expected: 100000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "0 USDC",
|
||||||
|
usdc: sdk.ZeroDec(),
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := ConvertFromUSDC(tc.usdc)
|
||||||
|
require.Equal(t, tc.expected, result.Int64())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatUSDCAmount(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
amount sdk.Int
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "1 USDC",
|
||||||
|
amount: sdk.NewInt(1000000),
|
||||||
|
expected: "1.000000000000000000 USDC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "1.5 USDC",
|
||||||
|
amount: sdk.NewInt(1500000),
|
||||||
|
expected: "1.500000000000000000 USDC",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := FormatUSDCAmount(tc.amount)
|
||||||
|
require.Equal(t, tc.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseUSDCAmount(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected int64
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "1 USDC",
|
||||||
|
input: "1.0",
|
||||||
|
expected: 1000000,
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "1.5 USDC",
|
||||||
|
input: "1.5",
|
||||||
|
expected: 1500000,
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "0 USDC",
|
||||||
|
input: "0",
|
||||||
|
expected: 0,
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid format",
|
||||||
|
input: "abc",
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative amount",
|
||||||
|
input: "-1.0",
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result, err := ParseUSDCAmount(tc.input)
|
||||||
|
if tc.expectErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.expected, result.Int64())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNobleSwapParams_Validate(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
params NobleSwapParams
|
||||||
|
expectErr bool
|
||||||
|
errMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid params",
|
||||||
|
params: NobleSwapParams{
|
||||||
|
InputDenom: "uatom",
|
||||||
|
OutputDenom: "uusdc",
|
||||||
|
Amount: sdk.NewInt(1000000),
|
||||||
|
MinOutput: sdk.NewInt(500000),
|
||||||
|
Receiver: "cosmos1abc123def456ghi789jkl012mno345pqr678st",
|
||||||
|
},
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty input denom",
|
||||||
|
params: NobleSwapParams{
|
||||||
|
OutputDenom: "uusdc",
|
||||||
|
Amount: sdk.NewInt(1000000),
|
||||||
|
MinOutput: sdk.NewInt(500000),
|
||||||
|
Receiver: "cosmos1abc123def456ghi789jkl012mno345pqr678st",
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "input denom cannot be empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty output denom",
|
||||||
|
params: NobleSwapParams{
|
||||||
|
InputDenom: "uatom",
|
||||||
|
Amount: sdk.NewInt(1000000),
|
||||||
|
MinOutput: sdk.NewInt(500000),
|
||||||
|
Receiver: "cosmos1abc123def456ghi789jkl012mno345pqr678st",
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "output denom cannot be empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same input and output denom",
|
||||||
|
params: NobleSwapParams{
|
||||||
|
InputDenom: "uusdc",
|
||||||
|
OutputDenom: "uusdc",
|
||||||
|
Amount: sdk.NewInt(1000000),
|
||||||
|
MinOutput: sdk.NewInt(500000),
|
||||||
|
Receiver: "cosmos1abc123def456ghi789jkl012mno345pqr678st",
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "input and output denoms must be different",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero amount",
|
||||||
|
params: NobleSwapParams{
|
||||||
|
InputDenom: "uatom",
|
||||||
|
OutputDenom: "uusdc",
|
||||||
|
Amount: sdk.ZeroInt(),
|
||||||
|
MinOutput: sdk.NewInt(500000),
|
||||||
|
Receiver: "cosmos1abc123def456ghi789jkl012mno345pqr678st",
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "amount must be positive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero min output",
|
||||||
|
params: NobleSwapParams{
|
||||||
|
InputDenom: "uatom",
|
||||||
|
OutputDenom: "uusdc",
|
||||||
|
Amount: sdk.NewInt(1000000),
|
||||||
|
MinOutput: sdk.ZeroInt(),
|
||||||
|
Receiver: "cosmos1abc123def456ghi789jkl012mno345pqr678st",
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "min output must be positive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty receiver",
|
||||||
|
params: NobleSwapParams{
|
||||||
|
InputDenom: "uatom",
|
||||||
|
OutputDenom: "uusdc",
|
||||||
|
Amount: sdk.NewInt(1000000),
|
||||||
|
MinOutput: sdk.NewInt(500000),
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "receiver cannot be empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid receiver address",
|
||||||
|
params: NobleSwapParams{
|
||||||
|
InputDenom: "uatom",
|
||||||
|
OutputDenom: "uusdc",
|
||||||
|
Amount: sdk.NewInt(1000000),
|
||||||
|
MinOutput: sdk.NewInt(500000),
|
||||||
|
Receiver: "invalid",
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "invalid receiver address",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := tc.params.Validate()
|
||||||
|
if tc.expectErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), tc.errMsg)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNobleLiquidityParams_Validate(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
params NobleLiquidityParams
|
||||||
|
expectErr bool
|
||||||
|
errMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid params",
|
||||||
|
params: NobleLiquidityParams{
|
||||||
|
PoolID: "1",
|
||||||
|
Token0: "uusdc",
|
||||||
|
Token1: "uatom",
|
||||||
|
Amount0: sdk.NewInt(1000000),
|
||||||
|
Amount1: sdk.NewInt(500000),
|
||||||
|
MinShares: sdk.NewInt(1),
|
||||||
|
},
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty pool ID",
|
||||||
|
params: NobleLiquidityParams{
|
||||||
|
Token0: "uusdc",
|
||||||
|
Token1: "uatom",
|
||||||
|
Amount0: sdk.NewInt(1000000),
|
||||||
|
Amount1: sdk.NewInt(500000),
|
||||||
|
MinShares: sdk.NewInt(1),
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "pool ID cannot be empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty token0",
|
||||||
|
params: NobleLiquidityParams{
|
||||||
|
PoolID: "1",
|
||||||
|
Token1: "uatom",
|
||||||
|
Amount0: sdk.NewInt(1000000),
|
||||||
|
Amount1: sdk.NewInt(500000),
|
||||||
|
MinShares: sdk.NewInt(1),
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "token0 cannot be empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty token1",
|
||||||
|
params: NobleLiquidityParams{
|
||||||
|
PoolID: "1",
|
||||||
|
Token0: "uusdc",
|
||||||
|
Amount0: sdk.NewInt(1000000),
|
||||||
|
Amount1: sdk.NewInt(500000),
|
||||||
|
MinShares: sdk.NewInt(1),
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "token1 cannot be empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero amount0",
|
||||||
|
params: NobleLiquidityParams{
|
||||||
|
PoolID: "1",
|
||||||
|
Token0: "uusdc",
|
||||||
|
Token1: "uatom",
|
||||||
|
Amount0: sdk.ZeroInt(),
|
||||||
|
Amount1: sdk.NewInt(500000),
|
||||||
|
MinShares: sdk.NewInt(1),
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "amount0 must be positive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero amount1",
|
||||||
|
params: NobleLiquidityParams{
|
||||||
|
PoolID: "1",
|
||||||
|
Token0: "uusdc",
|
||||||
|
Token1: "uatom",
|
||||||
|
Amount0: sdk.NewInt(1000000),
|
||||||
|
Amount1: sdk.ZeroInt(),
|
||||||
|
MinShares: sdk.NewInt(1),
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "amount1 must be positive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero min shares",
|
||||||
|
params: NobleLiquidityParams{
|
||||||
|
PoolID: "1",
|
||||||
|
Token0: "uusdc",
|
||||||
|
Token1: "uatom",
|
||||||
|
Amount0: sdk.NewInt(1000000),
|
||||||
|
Amount1: sdk.NewInt(500000),
|
||||||
|
MinShares: sdk.ZeroInt(),
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "min shares must be positive",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := tc.params.Validate()
|
||||||
|
if tc.expectErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), tc.errMsg)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNobleUSDCPairs(t *testing.T) {
|
||||||
|
pairs := GetNobleUSDCPairs()
|
||||||
|
|
||||||
|
require.NotEmpty(t, pairs)
|
||||||
|
require.GreaterOrEqual(t, len(pairs), 5)
|
||||||
|
|
||||||
|
// Check that all pairs have USDC as quote
|
||||||
|
for _, pair := range pairs {
|
||||||
|
require.Equal(t, NobleUSDCDenom, pair.Quote)
|
||||||
|
require.NotEmpty(t, pair.Base)
|
||||||
|
require.NotEmpty(t, pair.Description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTradingPair_String(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
pair TradingPair
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "with description",
|
||||||
|
pair: TradingPair{
|
||||||
|
Base: "uatom",
|
||||||
|
Quote: "uusdc",
|
||||||
|
Description: "ATOM/USDC",
|
||||||
|
},
|
||||||
|
expected: "ATOM/USDC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "without description",
|
||||||
|
pair: TradingPair{
|
||||||
|
Base: "uatom",
|
||||||
|
Quote: "uusdc",
|
||||||
|
},
|
||||||
|
expected: "uatom/uusdc",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := tc.pair.String()
|
||||||
|
require.Equal(t, tc.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTradingPair_Reverse(t *testing.T) {
|
||||||
|
pair := TradingPair{
|
||||||
|
Base: "uatom",
|
||||||
|
Quote: "uusdc",
|
||||||
|
Description: "ATOM/USDC",
|
||||||
|
}
|
||||||
|
|
||||||
|
reversed := pair.Reverse()
|
||||||
|
|
||||||
|
require.Equal(t, "uusdc", reversed.Base)
|
||||||
|
require.Equal(t, "uatom", reversed.Quote)
|
||||||
|
require.Equal(t, "uusdc/uatom", reversed.Description)
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"cosmossdk.io/math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultParams returns default module parameters.
|
||||||
|
func DefaultParams() Params {
|
||||||
|
return Params{
|
||||||
|
Enabled: true,
|
||||||
|
MaxAccountsPerDid: 5, // Maximum 5 ICA accounts per DID
|
||||||
|
DefaultTimeoutSeconds: 600, // 10 minutes timeout for ICA operations
|
||||||
|
AllowedConnections: []string{
|
||||||
|
// Noble testnet connection - USDC hub for Cosmos
|
||||||
|
"noble-grand-1",
|
||||||
|
// Osmosis testnet - Primary DEX
|
||||||
|
"osmo-test-5",
|
||||||
|
// Other testnets can be added here
|
||||||
|
},
|
||||||
|
MinSwapAmount: "1000", // Minimum 1000 base units
|
||||||
|
MaxDailyVolume: "1000000000000", // 1M units daily volume cap
|
||||||
|
RateLimits: RateLimitParams{
|
||||||
|
MaxOpsPerBlock: 10, // Maximum 10 operations per block
|
||||||
|
MaxOpsPerDidPerDay: 100, // Maximum 100 operations per DID per day
|
||||||
|
CooldownBlocks: 5, // 5 block cooldown between operations
|
||||||
|
},
|
||||||
|
Fees: FeeParams{
|
||||||
|
SwapFeeBps: 30, // 0.3% swap fee
|
||||||
|
LiquidityFeeBps: 20, // 0.2% liquidity fee
|
||||||
|
OrderFeeBps: 10, // 0.1% order fee
|
||||||
|
FeeCollector: "", // Empty means use module account
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate performs basic validation of module parameters.
|
||||||
|
func (p Params) Validate() error {
|
||||||
|
if p.MaxAccountsPerDid == 0 {
|
||||||
|
return fmt.Errorf("max_accounts_per_did must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.MaxAccountsPerDid > 100 {
|
||||||
|
return fmt.Errorf("max_accounts_per_did cannot exceed 100")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.DefaultTimeoutSeconds == 0 {
|
||||||
|
return fmt.Errorf("default_timeout_seconds must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.DefaultTimeoutSeconds > 3600 {
|
||||||
|
return fmt.Errorf("default_timeout_seconds cannot exceed 3600 (1 hour)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate swap amounts
|
||||||
|
if p.MinSwapAmount != "" {
|
||||||
|
minSwap, ok := math.NewIntFromString(p.MinSwapAmount)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid min_swap_amount: %s", p.MinSwapAmount)
|
||||||
|
}
|
||||||
|
if minSwap.IsNegative() {
|
||||||
|
return fmt.Errorf("min_swap_amount cannot be negative")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.MaxDailyVolume != "" {
|
||||||
|
maxVolume, ok := math.NewIntFromString(p.MaxDailyVolume)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid max_daily_volume: %s", p.MaxDailyVolume)
|
||||||
|
}
|
||||||
|
if maxVolume.IsNegative() {
|
||||||
|
return fmt.Errorf("max_daily_volume cannot be negative")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate rate limits
|
||||||
|
if err := p.RateLimits.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("invalid rate_limits: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate fees
|
||||||
|
if err := p.Fees.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("invalid fees: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate performs basic validation of rate limit parameters.
|
||||||
|
func (r RateLimitParams) Validate() error {
|
||||||
|
if r.MaxOpsPerBlock == 0 {
|
||||||
|
return fmt.Errorf("max_ops_per_block must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.MaxOpsPerBlock > 1000 {
|
||||||
|
return fmt.Errorf("max_ops_per_block cannot exceed 1000")
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.MaxOpsPerDidPerDay == 0 {
|
||||||
|
return fmt.Errorf("max_ops_per_did_per_day must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cooldown blocks can be 0 (no cooldown)
|
||||||
|
if r.CooldownBlocks > 1000 {
|
||||||
|
return fmt.Errorf("cooldown_blocks cannot exceed 1000")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate performs basic validation of fee parameters.
|
||||||
|
func (f FeeParams) Validate() error {
|
||||||
|
// Basis points validation (1 bps = 0.01%)
|
||||||
|
maxBps := uint32(10000) // 100%
|
||||||
|
|
||||||
|
if f.SwapFeeBps > maxBps {
|
||||||
|
return fmt.Errorf("swap_fee_bps cannot exceed %d (100%%)", maxBps)
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.LiquidityFeeBps > maxBps {
|
||||||
|
return fmt.Errorf("liquidity_fee_bps cannot exceed %d (100%%)", maxBps)
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.OrderFeeBps > maxBps {
|
||||||
|
return fmt.Errorf("order_fee_bps cannot exceed %d (100%%)", maxBps)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fee collector address validation is done by the SDK when set
|
||||||
|
// Empty string means use module account
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDefaultParams(t *testing.T) {
|
||||||
|
params := DefaultParams()
|
||||||
|
|
||||||
|
// Verify basic fields
|
||||||
|
require.True(t, params.Enabled)
|
||||||
|
require.Equal(t, uint32(5), params.MaxAccountsPerDid)
|
||||||
|
require.Equal(t, uint64(600), params.DefaultTimeoutSeconds)
|
||||||
|
|
||||||
|
// Verify Noble testnet is in allowed connections
|
||||||
|
require.Contains(t, params.AllowedConnections, "noble-grand-1")
|
||||||
|
|
||||||
|
// Verify amounts
|
||||||
|
require.Equal(t, "1000", params.MinSwapAmount)
|
||||||
|
require.Equal(t, "1000000000000", params.MaxDailyVolume)
|
||||||
|
|
||||||
|
// Verify rate limits
|
||||||
|
require.Equal(t, uint32(10), params.RateLimits.MaxOpsPerBlock)
|
||||||
|
require.Equal(t, uint32(100), params.RateLimits.MaxOpsPerDidPerDay)
|
||||||
|
require.Equal(t, uint32(5), params.RateLimits.CooldownBlocks)
|
||||||
|
|
||||||
|
// Verify fees
|
||||||
|
require.Equal(t, uint32(30), params.Fees.SwapFeeBps)
|
||||||
|
require.Equal(t, uint32(20), params.Fees.LiquidityFeeBps)
|
||||||
|
require.Equal(t, uint32(10), params.Fees.OrderFeeBps)
|
||||||
|
|
||||||
|
// Validate params
|
||||||
|
err := params.Validate()
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParams_Validate(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
params Params
|
||||||
|
expectErr bool
|
||||||
|
errMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid default params",
|
||||||
|
params: DefaultParams(),
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - zero max accounts",
|
||||||
|
params: Params{
|
||||||
|
MaxAccountsPerDid: 0,
|
||||||
|
DefaultTimeoutSeconds: 600,
|
||||||
|
MinSwapAmount: "1000",
|
||||||
|
MaxDailyVolume: "1000000",
|
||||||
|
RateLimits: DefaultParams().RateLimits,
|
||||||
|
Fees: DefaultParams().Fees,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "max_accounts_per_did must be positive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - max accounts too high",
|
||||||
|
params: Params{
|
||||||
|
MaxAccountsPerDid: 101,
|
||||||
|
DefaultTimeoutSeconds: 600,
|
||||||
|
MinSwapAmount: "1000",
|
||||||
|
MaxDailyVolume: "1000000",
|
||||||
|
RateLimits: DefaultParams().RateLimits,
|
||||||
|
Fees: DefaultParams().Fees,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "max_accounts_per_did cannot exceed 100",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - zero timeout",
|
||||||
|
params: Params{
|
||||||
|
MaxAccountsPerDid: 5,
|
||||||
|
DefaultTimeoutSeconds: 0,
|
||||||
|
MinSwapAmount: "1000",
|
||||||
|
MaxDailyVolume: "1000000",
|
||||||
|
RateLimits: DefaultParams().RateLimits,
|
||||||
|
Fees: DefaultParams().Fees,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "default_timeout_seconds must be positive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - timeout too high",
|
||||||
|
params: Params{
|
||||||
|
MaxAccountsPerDid: 5,
|
||||||
|
DefaultTimeoutSeconds: 3601,
|
||||||
|
MinSwapAmount: "1000",
|
||||||
|
MaxDailyVolume: "1000000",
|
||||||
|
RateLimits: DefaultParams().RateLimits,
|
||||||
|
Fees: DefaultParams().Fees,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "default_timeout_seconds cannot exceed 3600",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - negative min swap amount",
|
||||||
|
params: Params{
|
||||||
|
MaxAccountsPerDid: 5,
|
||||||
|
DefaultTimeoutSeconds: 600,
|
||||||
|
MinSwapAmount: "-1000",
|
||||||
|
MaxDailyVolume: "1000000",
|
||||||
|
RateLimits: DefaultParams().RateLimits,
|
||||||
|
Fees: DefaultParams().Fees,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "min_swap_amount cannot be negative",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - invalid min swap amount format",
|
||||||
|
params: Params{
|
||||||
|
MaxAccountsPerDid: 5,
|
||||||
|
DefaultTimeoutSeconds: 600,
|
||||||
|
MinSwapAmount: "invalid",
|
||||||
|
MaxDailyVolume: "1000000",
|
||||||
|
RateLimits: DefaultParams().RateLimits,
|
||||||
|
Fees: DefaultParams().Fees,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "invalid min_swap_amount",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - negative max daily volume",
|
||||||
|
params: Params{
|
||||||
|
MaxAccountsPerDid: 5,
|
||||||
|
DefaultTimeoutSeconds: 600,
|
||||||
|
MinSwapAmount: "1000",
|
||||||
|
MaxDailyVolume: "-1000000",
|
||||||
|
RateLimits: DefaultParams().RateLimits,
|
||||||
|
Fees: DefaultParams().Fees,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "max_daily_volume cannot be negative",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := tc.params.Validate()
|
||||||
|
if tc.expectErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), tc.errMsg)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRateLimitParams_Validate(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
params RateLimitParams
|
||||||
|
expectErr bool
|
||||||
|
errMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid default",
|
||||||
|
params: DefaultParams().RateLimits,
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - zero max ops per block",
|
||||||
|
params: RateLimitParams{
|
||||||
|
MaxOpsPerBlock: 0,
|
||||||
|
MaxOpsPerDidPerDay: 100,
|
||||||
|
CooldownBlocks: 5,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "max_ops_per_block must be positive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - max ops per block too high",
|
||||||
|
params: RateLimitParams{
|
||||||
|
MaxOpsPerBlock: 1001,
|
||||||
|
MaxOpsPerDidPerDay: 100,
|
||||||
|
CooldownBlocks: 5,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "max_ops_per_block cannot exceed 1000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - zero max ops per did per day",
|
||||||
|
params: RateLimitParams{
|
||||||
|
MaxOpsPerBlock: 10,
|
||||||
|
MaxOpsPerDidPerDay: 0,
|
||||||
|
CooldownBlocks: 5,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "max_ops_per_did_per_day must be positive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid - zero cooldown blocks",
|
||||||
|
params: RateLimitParams{
|
||||||
|
MaxOpsPerBlock: 10,
|
||||||
|
MaxOpsPerDidPerDay: 100,
|
||||||
|
CooldownBlocks: 0,
|
||||||
|
},
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - cooldown blocks too high",
|
||||||
|
params: RateLimitParams{
|
||||||
|
MaxOpsPerBlock: 10,
|
||||||
|
MaxOpsPerDidPerDay: 100,
|
||||||
|
CooldownBlocks: 1001,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "cooldown_blocks cannot exceed 1000",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := tc.params.Validate()
|
||||||
|
if tc.expectErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), tc.errMsg)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFeeParams_Validate(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
params FeeParams
|
||||||
|
expectErr bool
|
||||||
|
errMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid default",
|
||||||
|
params: DefaultParams().Fees,
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid - zero fees",
|
||||||
|
params: FeeParams{
|
||||||
|
SwapFeeBps: 0,
|
||||||
|
LiquidityFeeBps: 0,
|
||||||
|
OrderFeeBps: 0,
|
||||||
|
FeeCollector: "",
|
||||||
|
},
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid - max fees",
|
||||||
|
params: FeeParams{
|
||||||
|
SwapFeeBps: 10000,
|
||||||
|
LiquidityFeeBps: 10000,
|
||||||
|
OrderFeeBps: 10000,
|
||||||
|
FeeCollector: "",
|
||||||
|
},
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - swap fee too high",
|
||||||
|
params: FeeParams{
|
||||||
|
SwapFeeBps: 10001,
|
||||||
|
LiquidityFeeBps: 20,
|
||||||
|
OrderFeeBps: 10,
|
||||||
|
FeeCollector: "",
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "swap_fee_bps cannot exceed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - liquidity fee too high",
|
||||||
|
params: FeeParams{
|
||||||
|
SwapFeeBps: 30,
|
||||||
|
LiquidityFeeBps: 10001,
|
||||||
|
OrderFeeBps: 10,
|
||||||
|
FeeCollector: "",
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "liquidity_fee_bps cannot exceed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - order fee too high",
|
||||||
|
params: FeeParams{
|
||||||
|
SwapFeeBps: 30,
|
||||||
|
LiquidityFeeBps: 20,
|
||||||
|
OrderFeeBps: 10001,
|
||||||
|
FeeCollector: "",
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "order_fee_bps cannot exceed",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := tc.params.Validate()
|
||||||
|
if tc.expectErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), tc.errMsg)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user