diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60af97d..fe4860a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,7 +54,7 @@ jobs: run: go test -race -coverprofile=coverage.out ./... - name: Run CLI e2e smoke tests - run: go test -tags=clie2e -v ./e2e/... -run "Help|Params|MissingArgs" + run: go test -tags=clie2e -v ./e2e/... -run "Help|Params|MissingArgs|RemovedOldNames|UnknownSubcommand" - name: Compile integration tests run: go test -tags=integration -run '^$' ./pkg/pchain/... diff --git a/CLAUDE.md b/CLAUDE.md index 7064be4..9ff6293 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,9 +20,9 @@ cmd/ - Cobra CLI commands (user-facing interface) ├── keys.go - Key management: generate, import, export, delete, default ├── wallet.go - Wallet info: address, balance ├── transfer.go - Transfers: send, p-to-c, c-to-p, export, import -├── validator.go - Staking: add validator, delegate -├── subnet.go - Subnets: create, transfer-ownership, convert-l1 -├── l1.go - L1 validators: register, set-weight, add-balance, disable +├── validator.go - Staking: add-permissionless, add-permissionless-delegator +├── subnet.go - Subnets: create, transfer-ownership, convert-to-l1, add-validator +├── l1.go - L1 validators: register-validator, set-validator-weight, increase-validator-balance, disable-validator ├── chain.go - Chains: create chain on subnet └── node.go - Node utilities: info diff --git a/cmd/chain.go b/cmd/chain.go index 0c150d6..e8fd133 100644 --- a/cmd/chain.go +++ b/cmd/chain.go @@ -29,11 +29,12 @@ var chainCmd = &cobra.Command{ Use: "chain", Short: "Chain management", Long: `Create and manage chains on subnets.`, + RunE: requireSubcommand, } var chainCreateCmd = &cobra.Command{ Use: "create", - Short: "Create a new chain", + Short: "Create a new chain (CreateChainTx)", Long: `Create a new blockchain on a subnet.`, RunE: func(cmd *cobra.Command, args []string) error { ctx, cancel := getOperationContext() diff --git a/cmd/keys.go b/cmd/keys.go index 7da80e2..566b76c 100644 --- a/cmd/keys.go +++ b/cmd/keys.go @@ -47,6 +47,7 @@ Subcommands: list List all stored keys export Export a key (show private key) delete Remove a stored key`, + RunE: requireSubcommand, } var keysImportCmd = &cobra.Command{ diff --git a/cmd/l1.go b/cmd/l1.go index 2805f2a..5e4c079 100644 --- a/cmd/l1.go +++ b/cmd/l1.go @@ -20,11 +20,12 @@ var l1Cmd = &cobra.Command{ Use: "l1", Short: "L1 validator operations", Long: `Manage validators on Avalanche L1 blockchains.`, + RunE: requireSubcommand, } var l1RegisterValidatorCmd = &cobra.Command{ Use: "register-validator", - Short: "Register a new L1 validator", + Short: "Register a new L1 validator (RegisterL1ValidatorTx)", Long: `Register a new validator on an L1 blockchain.`, RunE: func(cmd *cobra.Command, args []string) error { ctx, cancel := getOperationContext() @@ -80,8 +81,8 @@ var l1RegisterValidatorCmd = &cobra.Command{ } var l1SetWeightCmd = &cobra.Command{ - Use: "set-weight", - Short: "Set L1 validator weight", + Use: "set-validator-weight", + Short: "Set L1 validator weight (SetL1ValidatorWeightTx)", Long: `Set the weight of a validator on an L1 blockchain.`, RunE: func(cmd *cobra.Command, args []string) error { ctx, cancel := getOperationContext() @@ -118,8 +119,8 @@ var l1SetWeightCmd = &cobra.Command{ } var l1AddBalanceCmd = &cobra.Command{ - Use: "add-balance", - Short: "Increase L1 validator balance", + Use: "increase-validator-balance", + Short: "Increase L1 validator balance (IncreaseL1ValidatorBalanceTx)", Long: `Increase the balance of a validator on an L1 blockchain for continuous fees.`, RunE: func(cmd *cobra.Command, args []string) error { ctx, cancel := getOperationContext() @@ -165,7 +166,7 @@ var l1AddBalanceCmd = &cobra.Command{ var l1DisableValidatorCmd = &cobra.Command{ Use: "disable-validator", - Short: "Disable an L1 validator", + Short: "Disable an L1 validator (DisableL1ValidatorTx)", Long: `Disable a validator on an L1 blockchain and return remaining funds.`, RunE: func(cmd *cobra.Command, args []string) error { ctx, cancel := getOperationContext() diff --git a/cmd/node.go b/cmd/node.go index 14d9c40..f381aa9 100644 --- a/cmd/node.go +++ b/cmd/node.go @@ -11,6 +11,7 @@ var nodeCmd = &cobra.Command{ Use: "node", Short: "Node information", Long: `Node information operations including getting node ID and BLS key.`, + RunE: requireSubcommand, } var nodeIP string diff --git a/cmd/root.go b/cmd/root.go index 5e903d4..b3ee764 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -39,11 +39,12 @@ var rootCmd = &cobra.Command{ Short: "Avalanche P-Chain CLI", SilenceErrors: true, SilenceUsage: true, + RunE: requireSubcommand, Long: `Avalanche P-Chain operations: staking, subnets, transfers, and L1 validators. Example usage: platform wallet balance --key-name mykey - platform validator add --node-id NodeID-... --stake 2000 + platform validator add-permissionless --node-id NodeID-... --stake 2000 platform transfer p-to-c --amount 10 --key-name mykey platform subnet create --network fuji --key-name mykey @@ -81,6 +82,16 @@ func init() { }) } +// requireSubcommand is the RunE for command groups: it prints help when the +// group is invoked bare, and rejects any unknown subcommand with an error so +// removed command names fail loudly instead of silently printing help. +func requireSubcommand(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + return fmt.Errorf("unknown command %q for %q", args[0], cmd.CommandPath()) + } + return cmd.Help() +} + // avaxToNAVAX converts AVAX amount to nAVAX with validation. // Returns error if amount is negative or would overflow. func avaxToNAVAX(avax float64) (uint64, error) { diff --git a/cmd/subnet.go b/cmd/subnet.go index eebe3ff..a63dc05 100644 --- a/cmd/subnet.go +++ b/cmd/subnet.go @@ -23,17 +23,23 @@ var ( subnetValBalance float64 subnetMockVal bool subnetValidatorWeights string + + subnetValNodeID string + subnetValWeight uint64 + subnetValStartTime string + subnetValDuration string ) var subnetCmd = &cobra.Command{ Use: "subnet", Short: "Subnet management", Long: `Create and manage subnets on the Avalanche P-Chain.`, + RunE: requireSubcommand, } var subnetCreateCmd = &cobra.Command{ Use: "create", - Short: "Create a new subnet", + Short: "Create a new subnet (CreateSubnetTx)", Long: `Create a new subnet on the P-Chain.`, RunE: func(cmd *cobra.Command, args []string) error { ctx, cancel := getOperationContext() @@ -67,7 +73,7 @@ var subnetCreateCmd = &cobra.Command{ var subnetTransferOwnershipCmd = &cobra.Command{ Use: "transfer-ownership", - Short: "Transfer subnet ownership", + Short: "Transfer subnet ownership (TransferSubnetOwnershipTx)", Long: `Transfer ownership of a subnet to a new address.`, RunE: func(cmd *cobra.Command, args []string) error { ctx, cancel := getOperationContext() @@ -112,8 +118,8 @@ var subnetTransferOwnershipCmd = &cobra.Command{ } var subnetConvertL1Cmd = &cobra.Command{ - Use: "convert-l1", - Short: "Convert subnet to L1", + Use: "convert-to-l1", + Short: "Convert subnet to L1 (ConvertSubnetToL1Tx)", Long: `Convert a permissioned subnet to an L1 blockchain.`, RunE: func(cmd *cobra.Command, args []string) error { ctx, cancel := getOperationContext() @@ -237,12 +243,86 @@ var subnetConvertL1Cmd = &cobra.Command{ }, } +var subnetAddValidatorCmd = &cobra.Command{ + Use: "add-validator", + Short: "Add a validator to a permissioned subnet (AddSubnetValidatorTx)", + Long: `Add a validator to a permissioned subnet (AddSubnetValidatorTx). + +The node must already be a primary network validator, and the validation period +must fall within its primary network validation window. The subnet owner key +authorizes the transaction, so load the owner key via --key-name or --ledger.`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx, cancel := getOperationContext() + defer cancel() + + if subnetID == "" { + return fmt.Errorf("--subnet-id is required") + } + if subnetValNodeID == "" { + return fmt.Errorf("--node-id is required") + } + if subnetValWeight == 0 { + return fmt.Errorf("--weight is required and must be positive") + } + + sid, err := ids.FromString(subnetID) + if err != nil { + return fmt.Errorf("invalid subnet ID: %w", err) + } + + nodeID, err := ids.NodeIDFromString(subnetValNodeID) + if err != nil { + return fmt.Errorf("invalid node ID: %w", err) + } + + start, end, err := parseTimeRange(subnetValStartTime, subnetValDuration) + if err != nil { + return err + } + + netConfig, err := getNetworkConfig(ctx) + if err != nil { + return fmt.Errorf("failed to get network config: %w", err) + } + if end.Sub(start) < netConfig.MinStakeDuration { + return fmt.Errorf("duration too short for %s: minimum is %s", netConfig.Name, netConfig.MinStakeDuration) + } + + w, cleanup, err := loadPChainWalletWithSubnet(ctx, netConfig, sid) + if err != nil { + return fmt.Errorf("failed to create wallet: %w", err) + } + defer cleanup() + + fmt.Printf("Adding validator %s to subnet %s...\n", nodeID, sid) + fmt.Printf(" Weight: %d\n", subnetValWeight) + fmt.Printf(" Start: %s\n", start.UTC().Format("2006-01-02 15:04:05 MST")) + fmt.Printf(" End: %s\n", end.UTC().Format("2006-01-02 15:04:05 MST")) + fmt.Println("Submitting transaction...") + + txID, err := pchain.AddSubnetValidator(ctx, w, pchain.AddSubnetValidatorConfig{ + SubnetID: sid, + NodeID: nodeID, + Start: start, + End: end, + Weight: subnetValWeight, + }) + if err != nil { + return err + } + + fmt.Printf("TX ID: %s\n", txID) + return nil + }, +} + func init() { rootCmd.AddCommand(subnetCmd) subnetCmd.AddCommand(subnetCreateCmd) subnetCmd.AddCommand(subnetTransferOwnershipCmd) subnetCmd.AddCommand(subnetConvertL1Cmd) + subnetCmd.AddCommand(subnetAddValidatorCmd) // Transfer ownership flags subnetTransferOwnershipCmd.Flags().StringVar(&subnetID, "subnet-id", "", "Subnet ID") @@ -260,4 +340,11 @@ func init() { subnetConvertL1Cmd.Flags().Float64Var(&subnetValBalance, "validator-balance", 1.0, "Balance per validator in AVAX") subnetConvertL1Cmd.Flags().StringVar(&subnetValidatorWeights, "validator-weights", "", "Comma-separated validator weights (uint64). Must match validator count. Defaults to 100 per validator if omitted.") subnetConvertL1Cmd.Flags().BoolVar(&subnetMockVal, "mock-validator", false, "Use a mock validator (for testing)") + + // Add validator flags + subnetAddValidatorCmd.Flags().StringVar(&subnetID, "subnet-id", "", "Subnet ID") + subnetAddValidatorCmd.Flags().StringVar(&subnetValNodeID, "node-id", "", "Validator node ID (must already validate the primary network)") + subnetAddValidatorCmd.Flags().Uint64Var(&subnetValWeight, "weight", 0, "Validator sampling weight on the subnet") + subnetAddValidatorCmd.Flags().StringVar(&subnetValStartTime, "start", "now", "Start time (RFC3339 or 'now'). Post-Durango networks ignore this; validation begins at tx acceptance") + subnetAddValidatorCmd.Flags().StringVar(&subnetValDuration, "duration", "336h", "Validation duration (must fall within the node's primary network validation period)") } diff --git a/cmd/transfer.go b/cmd/transfer.go index d33338f..965a5d2 100644 --- a/cmd/transfer.go +++ b/cmd/transfer.go @@ -29,6 +29,7 @@ Amount Precision: Warning: Float amounts may lose precision for values > 9007199254740992 nAVAX (~9M AVAX). For large transfers, use --amount-navax for guaranteed precision.`, + RunE: requireSubcommand, } // getTransferAmountNAVAX returns the transfer amount in nAVAX. diff --git a/cmd/validator.go b/cmd/validator.go index 128de19..b9832f2 100644 --- a/cmd/validator.go +++ b/cmd/validator.go @@ -30,12 +30,13 @@ var validatorCmd = &cobra.Command{ Use: "validator", Short: "Primary network staking", Long: `Add validators and delegators to the Avalanche primary network.`, + RunE: requireSubcommand, } var validatorAddCmd = &cobra.Command{ - Use: "add", - Short: "Add a primary network validator", - Long: `Add a validator to the Avalanche primary network.`, + Use: "add-permissionless", + Short: "Add a primary network validator (AddPermissionlessValidatorTx)", + Long: `Add a permissionless validator to the Avalanche primary network.`, RunE: func(cmd *cobra.Command, args []string) error { ctx, cancel := getOperationContext() defer cancel() @@ -126,8 +127,8 @@ var validatorAddCmd = &cobra.Command{ } var validatorDelegateCmd = &cobra.Command{ - Use: "delegate", - Short: "Delegate to a primary network validator", + Use: "add-permissionless-delegator", + Short: "Delegate to a primary network validator (AddPermissionlessDelegatorTx)", Long: `Delegate stake to an existing primary network validator.`, RunE: func(cmd *cobra.Command, args []string) error { ctx, cancel := getOperationContext() @@ -280,7 +281,7 @@ func init() { validatorAddCmd.Flags().StringVar(&valBLSPublicKey, "bls-public-key", "", "Validator BLS public key (hex, recommended/manual mode)") validatorAddCmd.Flags().StringVar(&valBLSPoP, "bls-pop", "", "Validator BLS proof of possession signature (hex, recommended/manual mode)") validatorAddCmd.Flags().Float64Var(&valStakeAmount, "stake", 0, "Stake amount in AVAX (min 2000)") - validatorAddCmd.Flags().StringVar(&valStartTime, "start", "now", "Start time (RFC3339 or 'now')") + validatorAddCmd.Flags().StringVar(&valStartTime, "start", "now", "Start time (RFC3339 or 'now'). Post-Durango networks ignore this; validation begins at tx acceptance") validatorAddCmd.Flags().StringVar(&valDuration, "duration", "336h", "Validation duration (min 14 days)") validatorAddCmd.Flags().Float64Var(&valDelegationFee, "delegation-fee", 0.02, "Delegation fee (0.02 = 2%)") validatorAddCmd.Flags().StringVar(&valRewardAddr, "reward-address", "", "Reward address (default: own address)") @@ -288,7 +289,7 @@ func init() { // Delegate flags validatorDelegateCmd.Flags().StringVar(&valNodeID, "node-id", "", "Node ID to delegate to") validatorDelegateCmd.Flags().Float64Var(&valStakeAmount, "stake", 0, "Stake amount in AVAX (min 25)") - validatorDelegateCmd.Flags().StringVar(&valStartTime, "start", "now", "Start time (RFC3339 or 'now')") + validatorDelegateCmd.Flags().StringVar(&valStartTime, "start", "now", "Start time (RFC3339 or 'now'). Post-Durango networks ignore this; validation begins at tx acceptance") validatorDelegateCmd.Flags().StringVar(&valDuration, "duration", "336h", "Delegation duration (min 14 days)") validatorDelegateCmd.Flags().StringVar(&valRewardAddr, "reward-address", "", "Reward address (default: own address)") } diff --git a/cmd/wallet.go b/cmd/wallet.go index 203f4a8..313483f 100644 --- a/cmd/wallet.go +++ b/cmd/wallet.go @@ -26,6 +26,7 @@ var walletCmd = &cobra.Command{ Use: "wallet", Short: "Wallet operations", Long: `Wallet operations including balance check and address display.`, + RunE: requireSubcommand, } var balanceCmd = &cobra.Command{ diff --git a/docs/pchain-operations.md b/docs/pchain-operations.md index 121b612..b015c6d 100644 --- a/docs/pchain-operations.md +++ b/docs/pchain-operations.md @@ -1,17 +1,22 @@ # P-Chain Operations Reference -| Operation | Command | SDK Method | -|-----------|---------|------------| -| Send AVAX | `transfer send` | `IssueBaseTx` | -| Export | `transfer export` | `IssueExportTx` | -| Import | `transfer import` | `IssueImportTx` | -| Add Validator | `validator add` | `IssueAddPermissionlessValidatorTx` | -| Add Delegator | `validator delegate` | `IssueAddPermissionlessDelegatorTx` | -| Create Subnet | `subnet create` | `IssueCreateSubnetTx` | -| Transfer Subnet Ownership | `subnet transfer-ownership` | `IssueTransferSubnetOwnershipTx` | -| Convert to L1 | `subnet convert-l1` | `IssueConvertSubnetToL1Tx` | -| Register L1 Validator | `l1 register-validator` | `IssueRegisterL1ValidatorTx` | -| Set L1 Validator Weight | `l1 set-weight` | `IssueSetL1ValidatorWeightTx` | -| Increase L1 Balance | `l1 add-balance` | `IssueIncreaseL1ValidatorBalanceTx` | -| Disable L1 Validator | `l1 disable-validator` | `IssueDisableL1ValidatorTx` | -| Create Chain | `chain create` | `IssueCreateChainTx` | +Command names mirror the avalanchego transaction type each one issues. The +"Previous name" column lists names that were **removed in v2.0.0** (no aliases) — +use it to migrate existing scripts. + +| Tx Type | Command | SDK Method | Previous name (removed in v2.0.0) | +|---------|---------|------------|-----------------------------------| +| `BaseTx` | `transfer send` | `IssueBaseTx` | — | +| `ExportTx` | `transfer export` | `IssueExportTx` | — | +| `ImportTx` | `transfer import` | `IssueImportTx` | — | +| `AddPermissionlessValidatorTx` | `validator add-permissionless` | `IssueAddPermissionlessValidatorTx` | `validator add` | +| `AddPermissionlessDelegatorTx` | `validator add-permissionless-delegator` | `IssueAddPermissionlessDelegatorTx` | `validator delegate` | +| `CreateSubnetTx` | `subnet create` | `IssueCreateSubnetTx` | — | +| `TransferSubnetOwnershipTx` | `subnet transfer-ownership` | `IssueTransferSubnetOwnershipTx` | — | +| `ConvertSubnetToL1Tx` | `subnet convert-to-l1` | `IssueConvertSubnetToL1Tx` | `subnet convert-l1` | +| `AddSubnetValidatorTx` | `subnet add-validator` | `IssueAddSubnetValidatorTx` | — | +| `RegisterL1ValidatorTx` | `l1 register-validator` | `IssueRegisterL1ValidatorTx` | — | +| `SetL1ValidatorWeightTx` | `l1 set-validator-weight` | `IssueSetL1ValidatorWeightTx` | `l1 set-weight` | +| `IncreaseL1ValidatorBalanceTx` | `l1 increase-validator-balance` | `IssueIncreaseL1ValidatorBalanceTx` | `l1 add-balance` | +| `DisableL1ValidatorTx` | `l1 disable-validator` | `IssueDisableL1ValidatorTx` | — | +| `CreateChainTx` | `chain create` | `IssueCreateChainTx` | — | diff --git a/docs/usage.md b/docs/usage.md index 4f94e82..cfd1bc0 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -72,7 +72,7 @@ platform transfer import --from p --to c ```bash # Add validator (mainnet minimum: 2000 AVAX, 14 days) -platform validator add \ +platform validator add-permissionless \ --node-id NodeID-... \ --bls-public-key \ --bls-pop \ @@ -81,28 +81,44 @@ platform validator add \ --delegation-fee 0.02 # Delegate to validator (mainnet minimum: 25 AVAX) -platform validator delegate \ +platform validator add-permissionless-delegator \ --node-id NodeID-... \ --stake 100 \ --duration 336h ``` +> **Breaking (v2.0.0):** command names now mirror the avalanchego transaction +> they issue, and the old names were removed (no aliases): +> `validator add` → `validator add-permissionless`, +> `validator delegate` → `validator add-permissionless-delegator`, +> `subnet convert-l1` → `subnet convert-to-l1`, +> `l1 set-weight` → `l1 set-validator-weight`, +> `l1 add-balance` → `l1 increase-validator-balance`. + ### Subnets ```bash platform subnet create platform subnet transfer-ownership --subnet-id --new-owner
-platform subnet convert-l1 --subnet-id --chain-id --validators [--manager ] -platform subnet convert-l1 --subnet-id --chain-id --validators [--contract-address ] -platform subnet convert-l1 --subnet-id --chain-id \ +platform subnet convert-to-l1 --subnet-id --chain-id --validators [--manager ] +platform subnet convert-to-l1 --subnet-id --chain-id --validators [--contract-address ] +platform subnet convert-to-l1 --subnet-id --chain-id \ --validator-node-ids NodeID-...,NodeID-... \ --validator-bls-public-keys , \ --validator-bls-pops , \ [--manager ] -platform subnet convert-l1 --subnet-id --chain-id --mock-validator +platform subnet convert-to-l1 --subnet-id --chain-id --mock-validator +platform subnet add-validator --subnet-id --node-id NodeID-... --weight [--start ] [--duration ] ``` -`convert-l1` notes: +`add-validator` notes: +- Adds a validator to a **permissioned** subnet (`AddSubnetValidatorTx`). +- The node must already validate the primary network, and the validation period + must fall within its primary network validation window. +- The subnet owner key authorizes the tx (subnet auth), so load the owner key via + `--key-name` or `--ledger`. + +`convert-to-l1` notes: - `--manager` / `--contract-address` is the validator manager contract address (hex). - `--chain-id` is the chain where the validator manager contract is deployed. In many setups, this is the same as the new L1 chain ID. @@ -122,8 +138,8 @@ platform subnet convert-l1 --subnet-id --chain-id --mock ```bash platform l1 register-validator --balance --pop --message # balance > 0 -platform l1 set-weight --message -platform l1 add-balance --validation-id --balance # balance > 0 +platform l1 set-validator-weight --message +platform l1 increase-validator-balance --validation-id --balance # balance > 0 platform l1 disable-validator --validation-id ``` diff --git a/e2e/cli_test.go b/e2e/cli_test.go index ecdc65f..9daa610 100644 --- a/e2e/cli_test.go +++ b/e2e/cli_test.go @@ -125,7 +125,7 @@ func TestCLIValidatorHelp(t *testing.T) { t.Fatalf("validator help failed: %v", err) } - expected := []string{"add", "delegate"} + expected := []string{"add-permissionless", "add-permissionless-delegator"} for _, cmd := range expected { if !strings.Contains(stdout, cmd) { t.Errorf("validator help missing subcommand: %s", cmd) @@ -141,7 +141,7 @@ func TestCLISubnetHelp(t *testing.T) { t.Fatalf("subnet help failed: %v", err) } - expected := []string{"create", "transfer-ownership", "convert-l1"} + expected := []string{"create", "transfer-ownership", "convert-to-l1", "add-validator"} for _, cmd := range expected { if !strings.Contains(stdout, cmd) { t.Errorf("subnet help missing subcommand: %s", cmd) @@ -157,7 +157,7 @@ func TestCLIL1Help(t *testing.T) { t.Fatalf("l1 help failed: %v", err) } - expected := []string{"register-validator", "set-weight", "add-balance", "disable-validator"} + expected := []string{"register-validator", "set-validator-weight", "increase-validator-balance", "disable-validator"} for _, cmd := range expected { if !strings.Contains(stdout, cmd) { t.Errorf("l1 help missing subcommand: %s", cmd) @@ -285,7 +285,7 @@ func TestCLISubnetCreate(t *testing.T) { // ============================================================================= func TestCLIValidatorAddMissingArgs(t *testing.T) { - _, stderr, err := runCLI(t, "validator", "add") + _, stderr, err := runCLI(t, "validator", "add-permissionless") if err == nil { t.Error("expected error when missing required args") } @@ -296,7 +296,7 @@ func TestCLIValidatorAddMissingArgs(t *testing.T) { } func TestCLIValidatorDelegateMissingArgs(t *testing.T) { - _, stderr, err := runCLI(t, "validator", "delegate") + _, stderr, err := runCLI(t, "validator", "add-permissionless-delegator") if err == nil { t.Error("expected error when missing required args") } @@ -311,7 +311,7 @@ func TestCLIValidatorDelegateMissingArgs(t *testing.T) { // ============================================================================= func TestCLIL1AddBalanceMissingArgs(t *testing.T) { - _, stderr, err := runCLI(t, "l1", "add-balance", "--balance", "1") + _, stderr, err := runCLI(t, "l1", "increase-validator-balance", "--balance", "1") if err == nil { t.Error("expected error when missing required args") } @@ -348,7 +348,7 @@ func TestCLIChainCreateMissingArgs(t *testing.T) { } func TestCLISubnetConvertL1MissingArgs(t *testing.T) { - _, stderr, err := runCLI(t, "subnet", "convert-l1") + _, stderr, err := runCLI(t, "subnet", "convert-to-l1") if err == nil { t.Error("expected error when missing required args") } @@ -358,9 +358,53 @@ func TestCLISubnetConvertL1MissingArgs(t *testing.T) { } } +// TestCLIRemovedOldNamesRejected verifies the v2.0.0 hard cutover: the old +// command names were removed (no aliases) and are now rejected as unknown. +func TestCLIRemovedOldNamesRejected(t *testing.T) { + cases := [][]string{ + {"validator", "add"}, + {"validator", "delegate"}, + {"subnet", "convert-l1"}, + {"l1", "set-weight"}, + {"l1", "add-balance"}, + } + for _, args := range cases { + _, stderr, err := runCLI(t, args...) + if err == nil { + t.Errorf("%v: expected error for removed command name", args) + } + if !strings.Contains(stderr, "unknown command") { + t.Errorf("%v: expected \"unknown command\", got stderr: %s", args, stderr) + } + } +} + +// TestCLIUnknownSubcommandRejected verifies the root command and every command +// group reject an unknown subcommand with an error instead of silently printing +// help and exiting 0 (the footgun requireSubcommand closes). +func TestCLIUnknownSubcommandRejected(t *testing.T) { + cases := [][]string{ + {"definitely-not-a-command"}, // root + {"chain", "bogus"}, + {"transfer", "bogus"}, + {"keys", "bogus"}, + {"wallet", "bogus"}, + {"node", "bogus"}, + } + for _, args := range cases { + _, stderr, err := runCLI(t, args...) + if err == nil { + t.Errorf("%v: expected error for unknown subcommand", args) + } + if !strings.Contains(stderr, "unknown command") { + t.Errorf("%v: expected \"unknown command\", got stderr: %s", args, stderr) + } + } +} + func TestCLISubnetConvertL1EmptyValidators(t *testing.T) { _, stderr, err := runCLI(t, - "subnet", "convert-l1", + "subnet", "convert-to-l1", "--subnet-id", "2ebCneQ9z9v56N6sryhU6P8L3s1f6BDoed6ox2q6iM8Qv7w6s", "--chain-id", "2ebCneQ9z9v56N6sryhU6P8L3s1f6BDoed6ox2q6iM8Qv7w6s", "--validators", ", , ,", @@ -374,6 +418,17 @@ func TestCLISubnetConvertL1EmptyValidators(t *testing.T) { } } +func TestCLISubnetAddValidatorMissingArgs(t *testing.T) { + _, stderr, err := runCLI(t, "subnet", "add-validator") + if err == nil { + t.Error("expected error when missing required args") + } + + if !strings.Contains(stderr, "subnet-id") { + t.Errorf("expected error to mention subnet-id, got stderr: %s", stderr) + } +} + // ============================================================================= // CLI Full L1 Lifecycle Test // ============================================================================= @@ -441,7 +496,7 @@ func TestCLIL1Lifecycle(t *testing.T) { // Step 4: Convert subnet to L1 using mock validator t.Log("Step 3: Converting subnet to L1...") - convertOut, stderr, err := runCLI(t, "subnet", "convert-l1", + convertOut, stderr, err := runCLI(t, "subnet", "convert-to-l1", "--subnet-id", subnetID, "--chain-id", chainID, "--mock-validator") diff --git a/pkg/pchain/pchain.go b/pkg/pchain/pchain.go index 3f40f21..0fb24f5 100644 --- a/pkg/pchain/pchain.go +++ b/pkg/pchain/pchain.go @@ -65,6 +65,11 @@ type convertSubnetToL1TxIssuer interface { IssueConvertSubnetToL1Tx(subnetID ids.ID, chainID ids.ID, address []byte, validators []*txs.ConvertSubnetToL1Validator, options ...common.Option) (*txs.Tx, error) } +// addSubnetValidatorTxIssuer issues an AddSubnetValidatorTx. +type addSubnetValidatorTxIssuer interface { + IssueAddSubnetValidatorTx(vdr *txs.SubnetValidator, options ...common.Option) (*txs.Tx, error) +} + // createChainTxIssuer issues a CreateChainTx. type createChainTxIssuer interface { IssueCreateChainTx(subnetID ids.ID, genesis []byte, vmID ids.ID, fxIDs []ids.ID, chainName string, options ...common.Option) (*txs.Tx, error) @@ -410,6 +415,47 @@ func issueConvertSubnetToL1Tx( return tx.ID(), nil } +// AddSubnetValidatorConfig holds configuration for adding a validator to a +// permissioned subnet. +type AddSubnetValidatorConfig struct { + SubnetID ids.ID + NodeID ids.NodeID + Start time.Time + End time.Time + Weight uint64 // sampling weight on the subnet (not a stake amount) +} + +// AddSubnetValidator adds a validator to a permissioned subnet +// (IssueAddSubnetValidatorTx). The node must already validate the primary +// network, and the subnet owner authorizes the tx via subnet auth (resolved by +// the wallet backend, so the wallet must track the subnet). +func AddSubnetValidator(ctx context.Context, w *wallet.Wallet, cfg AddSubnetValidatorConfig) (ids.ID, error) { + return issueAddSubnetValidatorTx(w.PWallet(), cfg, common.WithContext(ctx)) +} + +func issueAddSubnetValidatorTx( + issuer addSubnetValidatorTxIssuer, + cfg AddSubnetValidatorConfig, + options ...common.Option, +) (ids.ID, error) { + tx, err := issuer.IssueAddSubnetValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: cfg.NodeID, + Start: uint64(cfg.Start.Unix()), + End: uint64(cfg.End.Unix()), + Wght: cfg.Weight, + }, + Subnet: cfg.SubnetID, + }, + options..., + ) + if err != nil { + return ids.Empty, fmt.Errorf("failed to issue AddSubnetValidatorTx: %w", err) + } + return tx.ID(), nil +} + // ============================================================================= // L1 Validator Operations // ============================================================================= diff --git a/pkg/pchain/pchain_test.go b/pkg/pchain/pchain_test.go index ed42b72..8f315e4 100644 --- a/pkg/pchain/pchain_test.go +++ b/pkg/pchain/pchain_test.go @@ -154,6 +154,19 @@ func (s *stubConvertSubnetToL1TxIssuer) IssueConvertSubnetToL1Tx(subnetID ids.ID return s.tx, s.err } +// stubAddSubnetValidatorTxIssuer implements addSubnetValidatorTxIssuer. +type stubAddSubnetValidatorTxIssuer struct { + tx *txs.Tx + err error + + gotVdr *txs.SubnetValidator +} + +func (s *stubAddSubnetValidatorTxIssuer) IssueAddSubnetValidatorTx(vdr *txs.SubnetValidator, _ ...common.Option) (*txs.Tx, error) { + s.gotVdr = vdr + return s.tx, s.err +} + // stubCreateChainTxIssuer implements createChainTxIssuer. type stubCreateChainTxIssuer struct { tx *txs.Tx @@ -512,6 +525,50 @@ func TestIssueConvertSubnetToL1Tx(t *testing.T) { } } +func TestIssueAddSubnetValidatorTx(t *testing.T) { + subnetID := ids.GenerateTestID() + nodeID := ids.GenerateTestNodeID() + start := time.Unix(1_700_000_200, 0).UTC() + end := start.Add(48 * time.Hour) + cfg := AddSubnetValidatorConfig{ + SubnetID: subnetID, + NodeID: nodeID, + Start: start, + End: end, + Weight: 100, + } + txID := ids.GenerateTestID() + + issuer := &stubAddSubnetValidatorTxIssuer{tx: &txs.Tx{TxID: txID}} + gotTxID, err := issueAddSubnetValidatorTx( + issuer, + cfg, + ) + if err != nil { + t.Fatalf("issueAddSubnetValidatorTx() returned error: %v", err) + } + if gotTxID != txID { + t.Fatalf("issueAddSubnetValidatorTx() txID = %s, want %s", gotTxID, txID) + } + gotVdr := issuer.gotVdr + if gotVdr == nil { + t.Fatal("issueAddSubnetValidatorTx() validator is nil") + } + if gotVdr.Subnet != subnetID { + t.Fatalf("issueAddSubnetValidatorTx() subnet = %s, want %s", gotVdr.Subnet, subnetID) + } + if gotVdr.Validator.NodeID != cfg.NodeID { + t.Fatalf("issueAddSubnetValidatorTx() nodeID = %s, want %s", gotVdr.Validator.NodeID, cfg.NodeID) + } + if gotVdr.Validator.Start != uint64(cfg.Start.Unix()) || gotVdr.Validator.End != uint64(cfg.End.Unix()) { + t.Fatalf("issueAddSubnetValidatorTx() time range = [%d,%d], want [%d,%d]", + gotVdr.Validator.Start, gotVdr.Validator.End, uint64(cfg.Start.Unix()), uint64(cfg.End.Unix())) + } + if gotVdr.Validator.Wght != cfg.Weight { + t.Fatalf("issueAddSubnetValidatorTx() weight = %d, want %d", gotVdr.Validator.Wght, cfg.Weight) + } +} + func TestIssueCreateChainTx(t *testing.T) { cfg := CreateChainConfig{ SubnetID: ids.GenerateTestID(),