diff --git a/README.md b/README.md index 0f2d27013..ebc81ea00 100644 --- a/README.md +++ b/README.md @@ -24,15 +24,18 @@ From a high level, here is what happens to register with collateral on Vanta Net ### From Source ```bash -git clone +git clone git@github.com:taoshidev/vanta-cli.git cd vanta-cli pip install . ``` + ### Homebrew (macOS/Linux) Coming soon ### Pip -Coming soon +```bash +pip install --upgrade git+https://github.com/taoshidev/vanta-cli.git +``` ## Commands diff --git a/pyproject.toml b/pyproject.toml index f213d2bc9..6c2686e8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi" [project] name = "vanta-cli" -version = "2.0.3" +version = "2.1.0" description = "Vanta Network CLI" readme = "README.md" authors = [ diff --git a/vanta_cli/src/commands/entity/__init__.py b/vanta_cli/src/commands/entity/__init__.py new file mode 100644 index 000000000..ad96a3e46 --- /dev/null +++ b/vanta_cli/src/commands/entity/__init__.py @@ -0,0 +1,4 @@ +"""Entity management commands.""" +from vanta_cli.src.commands.entity import register, create_subaccount + +__all__ = ['register', 'create_subaccount'] diff --git a/vanta_cli/src/commands/entity/create_subaccount.py b/vanta_cli/src/commands/entity/create_subaccount.py new file mode 100644 index 000000000..2720d6930 --- /dev/null +++ b/vanta_cli/src/commands/entity/create_subaccount.py @@ -0,0 +1,232 @@ +"""Subaccount creation command.""" +import getpass +import json +import typer +from typing import Optional +from rich.panel import Panel +from rich.text import Text +from rich.table import Table + +from bittensor_wallet import Wallet +from bittensor_cli.src.bittensor.utils import console + +from vanta_cli.src.config import VANTA_API_BASE_URL_MAINNET, VANTA_API_BASE_URL_TESTNET +from vanta_cli.src.utils.api import make_api_request + +async def create_subaccount( + wallet: Wallet, + network: str, + account_size: float, + asset_class: str, + prompt: bool, + quiet: bool = False, + verbose: bool = False, + json_output: bool = False, + password: Optional[str] = None +): + """ + Create a new subaccount for an entity on the Vanta Network. + + This command: + 1. Fetches entity configuration (cost per theta, max account size) + 2. Validates account size and asset class + 3. Calculates required collateral + 4. Checks miner's collateral balance + 5. Prompts for deposit if insufficient collateral + 6. Creates subaccount with signature-based authentication + + Args: + wallet: Bittensor wallet instance + network: Network to use ('test' or 'finney') + account_size: Account size in USD + asset_class: Asset class selection ('crypto' or 'forex') + prompt: Whether to prompt for confirmation + quiet: If True, suppresses all console output (for programmatic calls) + verbose: Enable verbose logging + json_output: Output results as JSON + password: Optional wallet password. If provided, skips interactive prompt. + Used for programmatic calls from miner server. + + Returns: + dict: Response with status, message, and subaccount details (if successful) + Success: {"status": "success", "message": str, "subaccount": dict, "collateral_charged": float} + Error: {"status": "error", "message": str} + """ + # Display header + if not json_output and not quiet: + title = Text("πŸ”— VANTA NETWORK πŸ”—", style="bold blue") + subtitle = Text("Subaccount Creation", style="italic cyan") + panel = Panel.fit( + f"{title}\n{subtitle}", + style="bold blue", + border_style="bright_blue" + ) + console.print(panel) + console.print("[blue]Creating subaccount on Vanta Network[/blue]") + + # Determine base URL + base_url = VANTA_API_BASE_URL_TESTNET if network == "test" else VANTA_API_BASE_URL_MAINNET + + max_account_size = 100_000 + cost_per_theta = 5000 + + # Step 2: Validate account size + if account_size > max_account_size: + error_msg = f"Account size ${account_size:,.0f} exceeds maximum ${max_account_size:,.0f}" + if not quiet: + console.print(f"[red]{error_msg}[/red]") + return {"status": "error", "message": error_msg} + + if account_size <= 0: + error_msg = "Account size must be positive" + if not quiet: + console.print(f"[red]{error_msg}[/red]") + return {"status": "error", "message": error_msg} + + # Step 3: Calculate required collateral + required_theta = account_size / cost_per_theta + + # Display configuration + if not json_output and not quiet: + config_table = Table(title="Subaccount Creation Configuration", show_header=True, header_style="bold cyan") + config_table.add_column("Parameter", style="cyan") + config_table.add_column("Value", style="green") + + config_table.add_row("Network", "Testnet" if network == "test" else "Mainnet") + config_table.add_row("Account Size", f"${account_size:,.2f}") + config_table.add_row("Asset Class", asset_class) + config_table.add_row("Cost per Theta", f"${cost_per_theta:,.0f}") + config_table.add_row("Required Collateral", f"{required_theta:.4f} Theta") + + console.print(config_table) + + # Step 4: Get password (use provided password or prompt interactively) + if password is None: + password = getpass.getpass(prompt='Enter your wallet password: ') + + try: + coldkey = wallet.get_coldkey(password=password) + hotkey = wallet.hotkey + except Exception as e: + error_msg = f"Failed to unlock wallet: {e}" + if not quiet: + console.print(f"[red]{error_msg}[/red]") + return {"status": "error", "message": error_msg} + + # Step 5: Confirm creation + if prompt: + confirm = typer.confirm( + f"Create subaccount for entity {hotkey.ss58_address} with " + f"${account_size:,.2f} account size and {asset_class} asset class " + f"(costs {required_theta:.4f} Theta)?" + ) + if not confirm: + cancel_msg = "Subaccount creation cancelled" + if not quiet: + console.print(f"[yellow]{cancel_msg}[/yellow]") + return {"status": "error", "message": cancel_msg} + + # Step 6: Ensure sufficient collateral + response = make_api_request(f"/collateral/balance/{hotkey.ss58_address}", method="GET", base_url=base_url, dev_mode=verbose) + if not response or response.get("balance_theta", 0) < required_theta: + balance = response.get('balance_theta', 0) if response else 0 + error_msg = f"Insufficient collateral for subaccount creation: {balance:.4f} Theta available, {required_theta:.4f} Theta required" + if not quiet: + console.print(f"[red]{error_msg}[/red]") + return {"status": "error", "message": error_msg} + + # Step 7: Prepare and sign subaccount creation request + if not quiet: + console.print("\n[cyan]Signing subaccount creation request...[/cyan]") + + subaccount_data = { + "entity_coldkey": coldkey.ss58_address, + "entity_hotkey": hotkey.ss58_address, + "account_size": account_size, + "asset_class": asset_class + } + + # Create message to sign (sorted JSON) + message = json.dumps(subaccount_data, sort_keys=True) + + # Sign the message with coldkey + signature = coldkey.sign(message.encode('utf-8')).hex() + + # Prepare payload + payload = { + "entity_coldkey": coldkey.ss58_address, + "entity_hotkey": hotkey.ss58_address, + "account_size": account_size, + "asset_class": asset_class, + "signature": signature + } + + # Step 8: Send subaccount creation request + if not quiet: + console.print("\n[cyan]Sending subaccount creation request...[/cyan]") + + try: + response = make_api_request("/entity/create-subaccount", payload, base_url=base_url, dev_mode=verbose) + + if response is None: + error_msg = "Subaccount creation failed - no response from API" + if not quiet: + console.print(f"[red]{error_msg}[/red]") + return {"status": "error", "message": error_msg} + + # Check success + if response.get("status") == "success": + success_msg = response.get('message', 'Subaccount created successfully') + subaccount = response.get('subaccount', {}) + + if not quiet: + console.print(f"[green]{success_msg}[/green]") + + # Display success info + success_table = Table(title="Subaccount Created Successfully", show_header=True, header_style="bold green") + success_table.add_column("Field", style="cyan") + success_table.add_column("Value", style="green") + + success_table.add_row("Synthetic Hotkey", subaccount.get('synthetic_hotkey')) + success_table.add_row("Subaccount ID", str(subaccount.get('subaccount_id'))) + success_table.add_row("Subaccount UUID", subaccount.get('subaccount_uuid')) + success_table.add_row("Account Size", f"${subaccount.get('account_size'):,.2f}") + success_table.add_row("Asset Class", subaccount.get('asset_class')) + success_table.add_row("Status", subaccount.get('status')) + success_table.add_row("Collateral Charged", f"{required_theta:.4f} Theta") + + console.print(success_table) + + success_panel = Panel.fit( + f"πŸŽ‰ Subaccount created successfully!\n" + f"Synthetic Hotkey: {subaccount.get('synthetic_hotkey')}\n" + f"Use this hotkey to place orders and track performance.", + style="bold green", + border_style="green" + ) + console.print(success_panel) + + return { + "status": "success", + "message": success_msg, + "subaccount": { + "synthetic_hotkey": subaccount.get('synthetic_hotkey'), + "subaccount_id": subaccount.get('subaccount_id'), + "subaccount_uuid": subaccount.get('subaccount_uuid'), + "account_size": subaccount.get('account_size'), + "asset_class": subaccount.get('asset_class'), + "status": subaccount.get('status') + }, + "collateral_charged": required_theta + } + else: + error_message = response.get("error") or "Unknown error occurred" + if not quiet: + console.print(f"[red]Subaccount creation failed: {error_message}[/red]") + return {"status": "error", "message": f"Subaccount creation failed: {error_message}"} + + except Exception as e: + error_msg = f"Error during subaccount creation: {e}" + if not quiet: + console.print(f"[red]{error_msg}[/red]") + return {"status": "error", "message": error_msg} diff --git a/vanta_cli/src/commands/entity/register.py b/vanta_cli/src/commands/entity/register.py new file mode 100644 index 000000000..af4a24711 --- /dev/null +++ b/vanta_cli/src/commands/entity/register.py @@ -0,0 +1,166 @@ +"""Entity registration command.""" +import getpass +import json +import typer +from rich.panel import Panel +from rich.text import Text +from rich.table import Table + +from bittensor_wallet import Wallet +from bittensor_cli.src.bittensor.utils import console + +from vanta_cli.src.config import VANTA_API_BASE_URL_MAINNET, VANTA_API_BASE_URL_TESTNET +from vanta_cli.src.utils.api import make_api_request + + +async def register( + wallet: Wallet, + network: str, + max_subaccounts: int, + prompt: bool, + quiet: bool = False, + verbose: bool = False, + json_output: bool = False +): + """ + Register a new entity on the Vanta Network. + + This command: + 1. Fetches entity registration fee from validator + 2. Checks miner's collateral balance + 3. Prompts for deposit if insufficient collateral + 4. Registers entity with signature-based authentication + """ + # Display header + if not json_output: + title = Text("πŸ”— VANTA NETWORK πŸ”—", style="bold blue") + subtitle = Text("Entity Registration", style="italic cyan") + panel = Panel.fit( + f"{title}\n{subtitle}", + style="bold blue", + border_style="bright_blue" + ) + console.print(panel) + console.print("[blue]Registering entity on Vanta Network[/blue]") + + # Determine base URL + base_url = VANTA_API_BASE_URL_TESTNET if network == "test" else VANTA_API_BASE_URL_MAINNET + + registration_fee = 5000 # Theta + + # Display configuration + if not json_output: + config_table = Table(title="Entity Registration Configuration", show_header=True, header_style="bold cyan") + config_table.add_column("Parameter", style="cyan") + config_table.add_column("Value", style="green") + + config_table.add_row("Network", "Testnet" if network == "test" else "Mainnet") + config_table.add_row("Registration Fee", f"{registration_fee} Theta") + config_table.add_row("Max Subaccounts", str(max_subaccounts)) + + console.print(config_table) + + # Step 2: Get password + password = getpass.getpass(prompt='Enter your wallet password: ') + + try: + coldkey = wallet.get_coldkey(password=password) + hotkey = wallet.hotkey + except Exception as e: + console.print(f"[red]Failed to unlock wallet: {e}[/red]") + return False + + # Step 3: Confirm registration + if prompt: + confirm = typer.confirm( + f"Register entity {hotkey.ss58_address} with {max_subaccounts} max subaccounts " + f"(costs {registration_fee} Theta)?" + ) + if not confirm: + console.print("[yellow]Entity registration cancelled[/yellow]") + return False + + # Step 4: Ensure sufficient collateral + response = make_api_request(f"/collateral/balance/{hotkey.ss58_address}", method="GET", base_url=base_url, dev_mode=verbose) + if not response or response.get("balance_theta") < registration_fee: + console.print(f"[red]Insufficient collateral for entity registration: {response.get('balance_theta')}[/red]") + return False + + + # collateral_success, collateral_msg = ensure_sufficient_collateral( + # wallet=wallet, + # network=network, + # required_theta=registration_fee, + # purpose="entity registration", + # password=password, + # base_url=base_url, + # verbose=verbose + # ) + # + # if not collateral_success: + # console.print(f"[red]Collateral check failed: {collateral_msg}[/red]") + # return False + + # Step 5: Prepare and sign registration request + console.print("\n[cyan]Signing entity registration request...[/cyan]") + + registration_data = { + "entity_coldkey": coldkey.ss58_address, + "entity_hotkey": hotkey.ss58_address, + # "max_subaccounts": max_subaccounts + } + + # Create message to sign (sorted JSON) + message = json.dumps(registration_data, sort_keys=True) + + # Sign the message with coldkey + signature = coldkey.sign(message.encode('utf-8')).hex() + + # Prepare payload + payload = { + "entity_coldkey": coldkey.ss58_address, + "entity_hotkey": hotkey.ss58_address, + # "max_subaccounts": max_subaccounts, + "signature": signature + } + + # Step 6: Send registration request + console.print("\n[cyan]Sending entity registration request...[/cyan]") + + try: + response = make_api_request("/entity/register", payload, base_url=base_url, dev_mode=verbose) + + if response is None: + console.print("[red]Entity registration failed - no response[/red]") + return False + + # Check success + if response.get("status") == "success": + console.print(f"[green]{response.get('message')}[/green]") + + # Display success info + success_table = Table(title="Entity Registered Successfully", show_header=True, header_style="bold green") + success_table.add_column("Field", style="cyan") + success_table.add_column("Value", style="green") + + success_table.add_row("Entity Hotkey", response.get('entity_hotkey')) + success_table.add_row("Max Subaccounts", str(response.get('max_subaccounts'))) + success_table.add_row("Fee Charged", f"{registration_fee} Theta") + + console.print(success_table) + + success_panel = Panel.fit( + f"πŸŽ‰ Entity registration completed!\nYou can now create subaccounts using 'vanta entity create-subaccount'", + style="bold green", + border_style="green" + ) + console.print(success_panel) + return True + else: + error_message = response.get("error") or "Unknown error occurred" + console.print(f"[red]Entity registration failed: {error_message}[/red]") + return False + + except Exception as e: + console.print(f"[red]Error during entity registration: {e}[/red]") + return False diff --git a/vanta_cli/src/config.py b/vanta_cli/src/config.py index 6f0efcfd8..29fce922c 100644 --- a/vanta_cli/src/config.py +++ b/vanta_cli/src/config.py @@ -1,8 +1,8 @@ """Configuration module with project constants.""" # Vanta API configuration -VANTA_API_BASE_URL_TESTNET = "http://34.187.154.219:48888" -VANTA_API_BASE_URL_MAINNET = "http://34.65.245.134:48888" +VANTA_API_BASE_URL_TESTNET = "https://validator.testnet.vantatrading.io" +VANTA_API_BASE_URL_MAINNET = "https://validator.mainnet.vantatrading.io" # Collateral configuration COLLATERAL_DEST_ADDRESS_TESTNET = '5F4xUo5pBJmzzFjqxmXNXL1NKPF3ugcAfYUvAc2LGERgrxNJ' diff --git a/vanta_cli/vanta.py b/vanta_cli/vanta.py index a6755107f..cb35cffe6 100644 --- a/vanta_cli/vanta.py +++ b/vanta_cli/vanta.py @@ -18,6 +18,10 @@ from vanta_cli.src.commands.asset import ( select as select_asset ) +from vanta_cli.src.commands.entity import ( + register as register_entity, + create_subaccount as create_subaccount_entity +) _epilog = "Made with [bold red]:heart:[/bold red] by The VanΟ„a NeΟ„work" @@ -45,12 +49,14 @@ class VantaCLIManager(CLIManager): collateral_app: typer.Typer asset_app: typer.Typer + entity_app: typer.Typer def __init__(self): super().__init__() self.collateral_app = typer.Typer(epilog=_epilog) self.asset_app = typer.Typer(epilog=_epilog) + self.entity_app = typer.Typer(epilog=_epilog) self.app.add_typer( self.collateral_app, @@ -64,6 +70,12 @@ def __init__(self): short_help="Asset command for choosing asset", no_args_is_help=True ) + self.app.add_typer( + self.entity_app, + name="entity", + short_help="Entity management commands", + no_args_is_help=True + ) self.collateral_app.command( "list", rich_help_panel="Collateral Management" @@ -79,6 +91,13 @@ def __init__(self): "select", rich_help_panel="Asset class selection" )(self.asset_select) + self.entity_app.command( + "register", rich_help_panel="Entity Management" + )(self.entity_register) + self.entity_app.command( + "create-subaccount", rich_help_panel="Entity Management" + )(self.entity_create_subaccount) + def collateral_list( self, wallet_name: Optional[str] = Options.wallet_name, @@ -240,6 +259,117 @@ def asset_select( ) ) + def entity_register( + self, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey_ss58, + network: str = VantaOptions.vanta_network, + max_subaccounts: Optional[int] = typer.Option( + None, + "--max-subaccounts", + help="Maximum number of subaccounts (default 500)" + ), + prompt: bool = VantaOptions.prompt, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """ + Register a new entity on the Vanta Network + """ + self.verbosity_handler(quiet, verbose, json_output) + + ask_for = [WO.NAME, WO.HOTKEY] + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=ask_for, + validate=WV.WALLET_AND_HOTKEY, + ) + + if max_subaccounts is None: + max_subaccounts = int(FloatPrompt.ask("Enter desired max number of subaccounts [500]", default=500)) + + return self._run_command( + register_entity.register( + wallet, + network, + max_subaccounts, + prompt, + quiet, + verbose, + json_output + ) + ) + + def entity_create_subaccount( + self, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey_ss58, + network: str = VantaOptions.vanta_network, + account_size: Optional[float] = typer.Option( + None, + "--account-size", + help="Account size in USD" + ), + asset_class: Optional[str] = typer.Option( + None, + "--asset-class", + help="Asset class selection (crypto, forex, etc.)" + ), + prompt: bool = VantaOptions.prompt, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """ + Create a new subaccount for an entity + """ + self.verbosity_handler(quiet, verbose, json_output) + + ask_for = [WO.NAME, WO.HOTKEY] + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=ask_for, + validate=WV.WALLET_AND_HOTKEY, + ) + + # Prompt for account_size if not provided + if account_size is None: + account_size = FloatPrompt.ask("Enter subaccount size in USD") + + # Prompt for asset_class if not provided + if asset_class is None: + assets = ["crypto", "forex"] + console.print("\nAvailable asset classes:") + for idx, asset in enumerate(assets, start=1): + console.print(f"{idx}. {asset}") + + choice = IntPrompt.ask( + "\nEnter the number of the asset class", + choices=[str(i) for i in range(1, len(assets) + 1)], + show_choices=False, + ) + asset_class = assets[choice - 1] + + return self._run_command( + create_subaccount_entity.create_subaccount( + wallet, + network, + account_size, + asset_class, + prompt, + quiet, + verbose, + json_output + ) + ) + def main(): manager = VantaCLIManager()