From 5f3f51f8178a8c8e8396112bc305b108eb7f9346 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 16 Jun 2026 16:10:14 -0400 Subject: [PATCH 1/4] feat: add `subnet add-validator` command (AddSubnetValidatorTx) Adds a validator to a permissioned subnet via AddSubnetValidatorTx, the classic path for managing validators on a permissioned subnet (distinct from `l1 register-validator`, which targets converted L1s). - pkg/pchain: AddSubnetValidator + issuer seam/config, mirroring the existing interface-based issue*Tx helpers - cmd/subnet: `subnet add-validator` with --subnet-id/--node-id/--weight /--start/--duration; subnet owner authorizes via subnet auth, so the wallet is loaded tracking the subnet - tests: pchain unit test + e2e help/missing-args coverage - docs: usage.md and pchain-operations.md --- cmd/subnet.go | 86 +++++++++++++++++++++++++++++++++++++++ docs/pchain-operations.md | 1 + docs/usage.md | 8 ++++ e2e/cli_test.go | 13 +++++- pkg/pchain/pchain.go | 46 +++++++++++++++++++++ pkg/pchain/pchain_test.go | 57 ++++++++++++++++++++++++++ 6 files changed, 210 insertions(+), 1 deletion(-) diff --git a/cmd/subnet.go b/cmd/subnet.go index eebe3ff..1c473b2 100644 --- a/cmd/subnet.go +++ b/cmd/subnet.go @@ -23,6 +23,11 @@ var ( subnetValBalance float64 subnetMockVal bool subnetValidatorWeights string + + subnetValNodeID string + subnetValWeight uint64 + subnetValStartTime string + subnetValDuration string ) var subnetCmd = &cobra.Command{ @@ -237,12 +242,86 @@ var subnetConvertL1Cmd = &cobra.Command{ }, } +var subnetAddValidatorCmd = &cobra.Command{ + Use: "add-validator", + Short: "Add a validator to a permissioned subnet", + 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 + } + if !end.After(start) { + return fmt.Errorf("end time must be after start time") + } + + netConfig, err := getNetworkConfig(ctx) + if err != nil { + return fmt.Errorf("failed to get network config: %w", err) + } + + 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 +339,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')") + subnetAddValidatorCmd.Flags().StringVar(&subnetValDuration, "duration", "336h", "Validation duration (must fall within the node's primary network validation period)") } diff --git a/docs/pchain-operations.md b/docs/pchain-operations.md index 121b612..268a0af 100644 --- a/docs/pchain-operations.md +++ b/docs/pchain-operations.md @@ -10,6 +10,7 @@ | Create Subnet | `subnet create` | `IssueCreateSubnetTx` | | Transfer Subnet Ownership | `subnet transfer-ownership` | `IssueTransferSubnetOwnershipTx` | | Convert to L1 | `subnet convert-l1` | `IssueConvertSubnetToL1Tx` | +| Add Subnet Validator | `subnet add-validator` | `IssueAddSubnetValidatorTx` | | Register L1 Validator | `l1 register-validator` | `IssueRegisterL1ValidatorTx` | | Set L1 Validator Weight | `l1 set-weight` | `IssueSetL1ValidatorWeightTx` | | Increase L1 Balance | `l1 add-balance` | `IssueIncreaseL1ValidatorBalanceTx` | diff --git a/docs/usage.md b/docs/usage.md index 4f94e82..b552993 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -100,8 +100,16 @@ platform subnet convert-l1 --subnet-id --chain-id \ --validator-bls-pops , \ [--manager ] platform subnet convert-l1 --subnet-id --chain-id --mock-validator +platform subnet add-validator --subnet-id --node-id NodeID-... --weight [--start ] [--duration ] ``` +`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-l1` notes: - `--manager` / `--contract-address` is the validator manager contract address (hex). - `--chain-id` is the chain where the validator manager contract is deployed. diff --git a/e2e/cli_test.go b/e2e/cli_test.go index ecdc65f..b4c42c2 100644 --- a/e2e/cli_test.go +++ b/e2e/cli_test.go @@ -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-l1", "add-validator"} for _, cmd := range expected { if !strings.Contains(stdout, cmd) { t.Errorf("subnet help missing subcommand: %s", cmd) @@ -374,6 +374,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.Logf("stderr: %s", stderr) + } +} + // ============================================================================= // CLI Full L1 Lifecycle Test // ============================================================================= 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(), From 24044122c38a559adde9868284da82f8726adcad Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 16 Jun 2026 16:30:13 -0400 Subject: [PATCH 2/4] refactor: align P-Chain command names with avalanchego tx types Rename commands so each maps 1:1 to the avalanchego transaction it issues, keeping the previous names as deprecated aliases (a warning is printed when an alias is used). pchain function names already matched the tx types. Renames (old name kept as alias): - 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 Also annotates each command's Short help with its tx type, adds a shared warnIfDeprecatedAlias helper, an e2e alias-deprecation test, and updates docs (usage.md, pchain-operations.md) and CLAUDE.md. --- CLAUDE.md | 6 +++--- cmd/l1.go | 20 ++++++++++++-------- cmd/root.go | 12 +++++++++++- cmd/subnet.go | 14 ++++++++------ cmd/validator.go | 16 ++++++++++------ docs/pchain-operations.md | 36 ++++++++++++++++++++---------------- docs/usage.md | 26 +++++++++++++++++--------- e2e/cli_test.go | 33 ++++++++++++++++++++++++--------- 8 files changed, 105 insertions(+), 58 deletions(-) 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/l1.go b/cmd/l1.go index 2805f2a..23155d1 100644 --- a/cmd/l1.go +++ b/cmd/l1.go @@ -24,7 +24,7 @@ var l1Cmd = &cobra.Command{ 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,10 +80,12 @@ var l1RegisterValidatorCmd = &cobra.Command{ } var l1SetWeightCmd = &cobra.Command{ - Use: "set-weight", - Short: "Set L1 validator weight", - Long: `Set the weight of a validator on an L1 blockchain.`, + Use: "set-validator-weight", + Aliases: []string{"set-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 { + warnIfDeprecatedAlias(cmd) ctx, cancel := getOperationContext() defer cancel() @@ -118,10 +120,12 @@ var l1SetWeightCmd = &cobra.Command{ } var l1AddBalanceCmd = &cobra.Command{ - Use: "add-balance", - Short: "Increase L1 validator balance", - Long: `Increase the balance of a validator on an L1 blockchain for continuous fees.`, + Use: "increase-validator-balance", + Aliases: []string{"add-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 { + warnIfDeprecatedAlias(cmd) ctx, cancel := getOperationContext() defer cancel() @@ -165,7 +169,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/root.go b/cmd/root.go index 5e903d4..27bd8c5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -43,7 +43,7 @@ var rootCmd = &cobra.Command{ 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 +81,16 @@ func init() { }) } +// warnIfDeprecatedAlias prints a deprecation notice to stderr when a command is +// invoked through a deprecated alias rather than its canonical name. Commands are +// named to mirror the avalanchego transaction type they issue; the previous names +// are retained as aliases for backward compatibility. +func warnIfDeprecatedAlias(cmd *cobra.Command) { + if called := cmd.CalledAs(); called != "" && called != cmd.Name() { + fmt.Fprintf(os.Stderr, "Warning: %q is deprecated; use %q instead.\n", called, cmd.CommandPath()) + } +} + // 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 1c473b2..be1401f 100644 --- a/cmd/subnet.go +++ b/cmd/subnet.go @@ -38,7 +38,7 @@ var subnetCmd = &cobra.Command{ 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() @@ -72,7 +72,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() @@ -117,10 +117,12 @@ var subnetTransferOwnershipCmd = &cobra.Command{ } var subnetConvertL1Cmd = &cobra.Command{ - Use: "convert-l1", - Short: "Convert subnet to L1", - Long: `Convert a permissioned subnet to an L1 blockchain.`, + Use: "convert-to-l1", + Aliases: []string{"convert-l1"}, + Short: "Convert subnet to L1 (ConvertSubnetToL1Tx)", + Long: `Convert a permissioned subnet to an L1 blockchain.`, RunE: func(cmd *cobra.Command, args []string) error { + warnIfDeprecatedAlias(cmd) ctx, cancel := getOperationContext() defer cancel() @@ -244,7 +246,7 @@ var subnetConvertL1Cmd = &cobra.Command{ var subnetAddValidatorCmd = &cobra.Command{ Use: "add-validator", - Short: "Add a validator to a permissioned subnet", + 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 diff --git a/cmd/validator.go b/cmd/validator.go index 128de19..7ff9689 100644 --- a/cmd/validator.go +++ b/cmd/validator.go @@ -33,10 +33,12 @@ var validatorCmd = &cobra.Command{ } var validatorAddCmd = &cobra.Command{ - Use: "add", - Short: "Add a primary network validator", - Long: `Add a validator to the Avalanche primary network.`, + Use: "add-permissionless", + Aliases: []string{"add"}, + 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 { + warnIfDeprecatedAlias(cmd) ctx, cancel := getOperationContext() defer cancel() @@ -126,10 +128,12 @@ var validatorAddCmd = &cobra.Command{ } var validatorDelegateCmd = &cobra.Command{ - Use: "delegate", - Short: "Delegate to a primary network validator", - Long: `Delegate stake to an existing primary network validator.`, + Use: "add-permissionless-delegator", + Aliases: []string{"delegate"}, + 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 { + warnIfDeprecatedAlias(cmd) ctx, cancel := getOperationContext() defer cancel() diff --git a/docs/pchain-operations.md b/docs/pchain-operations.md index 268a0af..e9004ee 100644 --- a/docs/pchain-operations.md +++ b/docs/pchain-operations.md @@ -1,18 +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` | -| Add Subnet Validator | `subnet add-validator` | `IssueAddSubnetValidatorTx` | -| 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. Previous +names are retained as deprecated aliases (see the last column) and print a +warning when used. + +| Tx Type | Command | SDK Method | Deprecated alias | +|---------|---------|------------|------------------| +| `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 b552993..c51d2b9 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,33 @@ 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 ``` +> Command names mirror the avalanchego transaction they issue. The previous +> names (`add`, `delegate`) still work as deprecated aliases. + ### 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-to-l1` was previously `convert-l1`, which still works as a deprecated alias. + `add-validator` notes: - Adds a validator to a **permissioned** subnet (`AddSubnetValidatorTx`). - The node must already validate the primary network, and the validation period @@ -110,7 +115,7 @@ platform subnet add-validator --subnet-id --node-id NodeID-... --weight --node-id NodeID-... --weight --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 ``` +> Deprecated aliases still work: `set-validator-weight` ← `set-weight`, +> `increase-validator-balance` ← `add-balance`. + ### Chains ```bash diff --git a/e2e/cli_test.go b/e2e/cli_test.go index b4c42c2..76fd4d0 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", "add-validator"} + 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,24 @@ func TestCLISubnetConvertL1MissingArgs(t *testing.T) { } } +// TestCLIDeprecatedAliasWarns verifies that an old command name still works via +// its alias and emits a deprecation warning pointing at the canonical name. +func TestCLIDeprecatedAliasWarns(t *testing.T) { + // "convert-l1" is the deprecated alias for "convert-to-l1"; invoking it with + // no args reaches RunE (which prints the warning) before the missing-arg error. + _, stderr, err := runCLI(t, "subnet", "convert-l1") + if err == nil { + t.Error("expected error when missing required args") + } + + if !strings.Contains(stderr, "deprecated") || !strings.Contains(stderr, "convert-to-l1") { + t.Errorf("expected deprecation warning pointing at canonical name, got stderr: %s", stderr) + } +} + func TestCLISubnetConvertL1EmptyValidators(t *testing.T) { _, stderr, err := runCLI(t, - "subnet", "convert-l1", + "subnet", "convert-to-l1", "--subnet-id", "2ebCneQ9z9v56N6sryhU6P8L3s1f6BDoed6ox2q6iM8Qv7w6s", "--chain-id", "2ebCneQ9z9v56N6sryhU6P8L3s1f6BDoed6ox2q6iM8Qv7w6s", "--validators", ", , ,", @@ -452,7 +467,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") From 3e9ae74aca9ec1b78586ab27af14588a9e7d65b0 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 16 Jun 2026 17:01:22 -0400 Subject: [PATCH 3/4] refactor!: remove deprecated command aliases (hard cutover) BREAKING CHANGE: the previous command names are removed entirely (no aliases). Old names now error with "unknown command". Migrate: - 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 - drop Aliases + warnIfDeprecatedAlias helper - command groups (validator/subnet/l1) now reject unknown subcommands via requireSubcommand so removed names fail loudly (exit 1) - e2e: assert old names are rejected; docs reframed as v2.0.0 migration --- cmd/l1.go | 17 +++++++---------- cmd/root.go | 14 +++++++------- cmd/subnet.go | 9 ++++----- cmd/validator.go | 17 +++++++---------- docs/pchain-operations.md | 10 +++++----- docs/usage.md | 14 +++++++------- e2e/cli_test.go | 30 ++++++++++++++++++------------ 7 files changed, 55 insertions(+), 56 deletions(-) diff --git a/cmd/l1.go b/cmd/l1.go index 23155d1..5e4c079 100644 --- a/cmd/l1.go +++ b/cmd/l1.go @@ -20,6 +20,7 @@ var l1Cmd = &cobra.Command{ Use: "l1", Short: "L1 validator operations", Long: `Manage validators on Avalanche L1 blockchains.`, + RunE: requireSubcommand, } var l1RegisterValidatorCmd = &cobra.Command{ @@ -80,12 +81,10 @@ var l1RegisterValidatorCmd = &cobra.Command{ } var l1SetWeightCmd = &cobra.Command{ - Use: "set-validator-weight", - Aliases: []string{"set-weight"}, - Short: "Set L1 validator weight (SetL1ValidatorWeightTx)", - Long: `Set the weight of a validator on an L1 blockchain.`, + 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 { - warnIfDeprecatedAlias(cmd) ctx, cancel := getOperationContext() defer cancel() @@ -120,12 +119,10 @@ var l1SetWeightCmd = &cobra.Command{ } var l1AddBalanceCmd = &cobra.Command{ - Use: "increase-validator-balance", - Aliases: []string{"add-balance"}, - Short: "Increase L1 validator balance (IncreaseL1ValidatorBalanceTx)", - Long: `Increase the balance of a validator on an L1 blockchain for continuous fees.`, + 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 { - warnIfDeprecatedAlias(cmd) ctx, cancel := getOperationContext() defer cancel() diff --git a/cmd/root.go b/cmd/root.go index 27bd8c5..a6cf6dd 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -81,14 +81,14 @@ func init() { }) } -// warnIfDeprecatedAlias prints a deprecation notice to stderr when a command is -// invoked through a deprecated alias rather than its canonical name. Commands are -// named to mirror the avalanchego transaction type they issue; the previous names -// are retained as aliases for backward compatibility. -func warnIfDeprecatedAlias(cmd *cobra.Command) { - if called := cmd.CalledAs(); called != "" && called != cmd.Name() { - fmt.Fprintf(os.Stderr, "Warning: %q is deprecated; use %q instead.\n", called, cmd.CommandPath()) +// 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. diff --git a/cmd/subnet.go b/cmd/subnet.go index be1401f..86872d7 100644 --- a/cmd/subnet.go +++ b/cmd/subnet.go @@ -34,6 +34,7 @@ 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{ @@ -117,12 +118,10 @@ var subnetTransferOwnershipCmd = &cobra.Command{ } var subnetConvertL1Cmd = &cobra.Command{ - Use: "convert-to-l1", - Aliases: []string{"convert-l1"}, - Short: "Convert subnet to L1 (ConvertSubnetToL1Tx)", - Long: `Convert a permissioned subnet to an L1 blockchain.`, + 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 { - warnIfDeprecatedAlias(cmd) ctx, cancel := getOperationContext() defer cancel() diff --git a/cmd/validator.go b/cmd/validator.go index 7ff9689..05a92cc 100644 --- a/cmd/validator.go +++ b/cmd/validator.go @@ -30,15 +30,14 @@ 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-permissionless", - Aliases: []string{"add"}, - Short: "Add a primary network validator (AddPermissionlessValidatorTx)", - Long: `Add a permissionless 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 { - warnIfDeprecatedAlias(cmd) ctx, cancel := getOperationContext() defer cancel() @@ -128,12 +127,10 @@ var validatorAddCmd = &cobra.Command{ } var validatorDelegateCmd = &cobra.Command{ - Use: "add-permissionless-delegator", - Aliases: []string{"delegate"}, - Short: "Delegate to a primary network validator (AddPermissionlessDelegatorTx)", - Long: `Delegate stake to an existing 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 { - warnIfDeprecatedAlias(cmd) ctx, cancel := getOperationContext() defer cancel() diff --git a/docs/pchain-operations.md b/docs/pchain-operations.md index e9004ee..b015c6d 100644 --- a/docs/pchain-operations.md +++ b/docs/pchain-operations.md @@ -1,11 +1,11 @@ # P-Chain Operations Reference -Command names mirror the avalanchego transaction type each one issues. Previous -names are retained as deprecated aliases (see the last column) and print a -warning when used. +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 | Deprecated alias | -|---------|---------|------------|------------------| +| Tx Type | Command | SDK Method | Previous name (removed in v2.0.0) | +|---------|---------|------------|-----------------------------------| | `BaseTx` | `transfer send` | `IssueBaseTx` | — | | `ExportTx` | `transfer export` | `IssueExportTx` | — | | `ImportTx` | `transfer import` | `IssueImportTx` | — | diff --git a/docs/usage.md b/docs/usage.md index c51d2b9..cfd1bc0 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -87,8 +87,13 @@ platform validator add-permissionless-delegator \ --duration 336h ``` -> Command names mirror the avalanchego transaction they issue. The previous -> names (`add`, `delegate`) still work as deprecated aliases. +> **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 @@ -106,8 +111,6 @@ platform subnet convert-to-l1 --subnet-id --chain-id --m platform subnet add-validator --subnet-id --node-id NodeID-... --weight [--start ] [--duration ] ``` -> `convert-to-l1` was previously `convert-l1`, which still works as a deprecated alias. - `add-validator` notes: - Adds a validator to a **permissioned** subnet (`AddSubnetValidatorTx`). - The node must already validate the primary network, and the validation period @@ -140,9 +143,6 @@ platform l1 increase-validator-balance --validation-id --balance # platform l1 disable-validator --validation-id ``` -> Deprecated aliases still work: `set-validator-weight` ← `set-weight`, -> `increase-validator-balance` ← `add-balance`. - ### Chains ```bash diff --git a/e2e/cli_test.go b/e2e/cli_test.go index 76fd4d0..7bfa7ed 100644 --- a/e2e/cli_test.go +++ b/e2e/cli_test.go @@ -358,18 +358,24 @@ func TestCLISubnetConvertL1MissingArgs(t *testing.T) { } } -// TestCLIDeprecatedAliasWarns verifies that an old command name still works via -// its alias and emits a deprecation warning pointing at the canonical name. -func TestCLIDeprecatedAliasWarns(t *testing.T) { - // "convert-l1" is the deprecated alias for "convert-to-l1"; invoking it with - // no args reaches RunE (which prints the warning) before the missing-arg error. - _, stderr, err := runCLI(t, "subnet", "convert-l1") - if err == nil { - t.Error("expected error when missing required args") - } - - if !strings.Contains(stderr, "deprecated") || !strings.Contains(stderr, "convert-to-l1") { - t.Errorf("expected deprecation warning pointing at canonical name, got stderr: %s", stderr) +// 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) + } } } From 1342d9344ef038eb7f38bc13c0ccc61bafa1520b Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 18 Jun 2026 12:30:54 -0400 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20address=20PR=20review=20=E2=80=94=20?= =?UTF-8?q?CI=20guard,=20group=20footgun,=20subnet=20add-validator=20polis?= =?UTF-8?q?h?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses @federiconardelli7's re-review of #33: - ci.yml: add RemovedOldNames + UnknownSubcommand to the clie2e -run filter so the cutover guards actually execute in CI (were compiled-only before) - apply requireSubcommand to root + chain/transfer/keys/wallet/node so an unknown subcommand errors (exit 1) instead of silently printing help; annotate `chain create` Short with (CreateChainTx) - subnet add-validator: add local min-duration check mirroring add-permissionless (friendly error instead of network-only failure) - note on --start that post-Durango networks ignore it (validator starts at acceptance) on subnet add-validator + the two validator commands; verified against avalanchego v1.14.1 staker_tx_verification.go - e2e: real t.Errorf assertion in TestCLISubnetAddValidatorMissingArgs; new TestCLIUnknownSubcommandRejected covering the newly-guarded groups --- .github/workflows/ci.yml | 2 +- cmd/chain.go | 3 ++- cmd/keys.go | 1 + cmd/node.go | 1 + cmd/root.go | 1 + cmd/subnet.go | 8 ++++---- cmd/transfer.go | 1 + cmd/validator.go | 4 ++-- cmd/wallet.go | 1 + e2e/cli_test.go | 25 ++++++++++++++++++++++++- 10 files changed, 38 insertions(+), 9 deletions(-) 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/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/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 a6cf6dd..b3ee764 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -39,6 +39,7 @@ 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: diff --git a/cmd/subnet.go b/cmd/subnet.go index 86872d7..a63dc05 100644 --- a/cmd/subnet.go +++ b/cmd/subnet.go @@ -279,14 +279,14 @@ authorizes the transaction, so load the owner key via --key-name or --ledger.`, if err != nil { return err } - if !end.After(start) { - return fmt.Errorf("end time must be after start time") - } 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 { @@ -345,6 +345,6 @@ func init() { 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')") + 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 05a92cc..b9832f2 100644 --- a/cmd/validator.go +++ b/cmd/validator.go @@ -281,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)") @@ -289,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/e2e/cli_test.go b/e2e/cli_test.go index 7bfa7ed..9daa610 100644 --- a/e2e/cli_test.go +++ b/e2e/cli_test.go @@ -379,6 +379,29 @@ func TestCLIRemovedOldNamesRejected(t *testing.T) { } } +// 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-to-l1", @@ -402,7 +425,7 @@ func TestCLISubnetAddValidatorMissingArgs(t *testing.T) { } if !strings.Contains(stderr, "subnet-id") { - t.Logf("stderr: %s", stderr) + t.Errorf("expected error to mention subnet-id, got stderr: %s", stderr) } }