diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c7600ee38..5a62c1daf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Changelog for NeoFS Node ## [Unreleased] ### Added +- `neofs-adm mainchain update-contract` command (#3799) ### Fixed diff --git a/cmd/neofs-adm/internal/modules/fschain/balance.go b/cmd/neofs-adm/internal/modules/fschain/balance.go index 11f8848673..5216b08b2b 100644 --- a/cmd/neofs-adm/internal/modules/fschain/balance.go +++ b/cmd/neofs-adm/internal/modules/fschain/balance.go @@ -59,7 +59,7 @@ func balanceContainerPayment(cmd *cobra.Command, args []string) error { } } - c, err := getN3Client(viper.GetViper()) + c, err := GetN3Client(viper.GetViper()) if err != nil { return fmt.Errorf("can't create N3 client: %w", err) } @@ -155,7 +155,7 @@ func dumpBalances(cmd *cobra.Command, _ []string) error { nmHash util.Uint160 ) - c, err := getN3Client(viper.GetViper()) + c, err := GetN3Client(viper.GetViper()) if err != nil { return err } diff --git a/cmd/neofs-adm/internal/modules/fschain/config.go b/cmd/neofs-adm/internal/modules/fschain/config.go index a8871e411f..e44d731432 100644 --- a/cmd/neofs-adm/internal/modules/fschain/config.go +++ b/cmd/neofs-adm/internal/modules/fschain/config.go @@ -22,7 +22,7 @@ import ( const forceConfigSet = "force" func dumpNetworkConfig(cmd *cobra.Command, _ []string) error { - c, err := getN3Client(viper.GetViper()) + c, err := GetN3Client(viper.GetViper()) if err != nil { return fmt.Errorf("can't create N3 client: %w", err) } diff --git a/cmd/neofs-adm/internal/modules/fschain/container.go b/cmd/neofs-adm/internal/modules/fschain/container.go index 1f9885fe12..6b60ac42f4 100644 --- a/cmd/neofs-adm/internal/modules/fschain/container.go +++ b/cmd/neofs-adm/internal/modules/fschain/container.go @@ -58,7 +58,7 @@ func dumpContainers(cmd *cobra.Command, _ []string) error { return err } - c, err := getN3Client(viper.GetViper()) + c, err := GetN3Client(viper.GetViper()) if err != nil { return fmt.Errorf("can't create N3 client: %w", err) } @@ -118,7 +118,7 @@ func dumpContainers(cmd *cobra.Command, _ []string) error { } func listContainers(cmd *cobra.Command, _ []string) error { - c, err := getN3Client(viper.GetViper()) + c, err := GetN3Client(viper.GetViper()) if err != nil { return fmt.Errorf("can't create N3 client: %w", err) } diff --git a/cmd/neofs-adm/internal/modules/fschain/dump_hashes.go b/cmd/neofs-adm/internal/modules/fschain/dump_hashes.go index 599275b26b..84d046799e 100644 --- a/cmd/neofs-adm/internal/modules/fschain/dump_hashes.go +++ b/cmd/neofs-adm/internal/modules/fschain/dump_hashes.go @@ -35,7 +35,7 @@ type contractDumpInfo struct { } func dumpContractHashes(cmd *cobra.Command, _ []string) error { - c, err := getN3Client(viper.GetViper()) + c, err := GetN3Client(viper.GetViper()) if err != nil { return fmt.Errorf("can't create N3 client: %w", err) } diff --git a/cmd/neofs-adm/internal/modules/fschain/dump_names.go b/cmd/neofs-adm/internal/modules/fschain/dump_names.go index 82846735a7..8b3692212e 100644 --- a/cmd/neofs-adm/internal/modules/fschain/dump_names.go +++ b/cmd/neofs-adm/internal/modules/fschain/dump_names.go @@ -24,7 +24,7 @@ type nameExp struct { } func dumpNames(cmd *cobra.Command, _ []string) error { - c, err := getN3Client(viper.GetViper()) + c, err := GetN3Client(viper.GetViper()) if err != nil { return fmt.Errorf("can't create N3 client: %w", err) } diff --git a/cmd/neofs-adm/internal/modules/fschain/initialize.go b/cmd/neofs-adm/internal/modules/fschain/initialize.go index 248d34c253..f17f3e2dd5 100644 --- a/cmd/neofs-adm/internal/modules/fschain/initialize.go +++ b/cmd/neofs-adm/internal/modules/fschain/initialize.go @@ -49,17 +49,17 @@ func newInitializeContext(cmd *cobra.Command, v *viper.Viper) (*initializeContex return nil, err } - c, err := getN3Client(v) + c, err := GetN3Client(v) if err != nil { return nil, fmt.Errorf("can't create N3 client: %w", err) } - committeeAcc, err := getWalletAccount(wallets[0], committeeAccountName) + committeeAcc, err := GetWalletAccount(wallets[0], committeeAccountName) if err != nil { return nil, fmt.Errorf("can't find committee account: %w", err) } - consensusAcc, err := getWalletAccount(wallets[0], consensusAccountName) + consensusAcc, err := GetWalletAccount(wallets[0], consensusAccountName) if err != nil { return nil, fmt.Errorf("can't find consensus account: %w", err) } @@ -85,7 +85,7 @@ func newInitializeContext(cmd *cobra.Command, v *viper.Viper) (*initializeContex accounts := make([]*wallet.Account, len(wallets)) for i, w := range wallets { - acc, err := getWalletAccount(w, singleAccountName) + acc, err := GetWalletAccount(w, singleAccountName) if err != nil { return nil, fmt.Errorf("wallet %s is invalid (no single account): %w", w.Path(), err) } @@ -346,7 +346,8 @@ func (c *initializeContext) sendMultiTx(script []byte, fancyScope bool, withCons return c.sendTx(tx, c.Command, false) } -func getWalletAccount(w *wallet.Wallet, typ string) (*wallet.Account, error) { +// GetWalletAccount returns account with the given label from the wallet. +func GetWalletAccount(w *wallet.Wallet, typ string) (*wallet.Account, error) { for i := range w.Accounts { if w.Accounts[i].Label == typ { return w.Accounts[i], nil diff --git a/cmd/neofs-adm/internal/modules/fschain/initialize_transfer.go b/cmd/neofs-adm/internal/modules/fschain/initialize_transfer.go index 2d9bf742b6..7521a0a37b 100644 --- a/cmd/neofs-adm/internal/modules/fschain/initialize_transfer.go +++ b/cmd/neofs-adm/internal/modules/fschain/initialize_transfer.go @@ -25,7 +25,7 @@ func (c *initializeContext) multiSign(tx *transaction.Transaction, accType strin h = c.ConsensusAcc.Contract.ScriptHash() } for _, w := range c.Wallets { - acc, err := getWalletAccount(w, accType) + acc, err := GetWalletAccount(w, accType) if err != nil { return fmt.Errorf("can't find %s wallet account: %w", accType, err) } diff --git a/cmd/neofs-adm/internal/modules/fschain/load.go b/cmd/neofs-adm/internal/modules/fschain/load.go index ebbd232992..6784ab603b 100644 --- a/cmd/neofs-adm/internal/modules/fschain/load.go +++ b/cmd/neofs-adm/internal/modules/fschain/load.go @@ -38,7 +38,7 @@ func reportsFunc(cmd *cobra.Command, _ []string) error { return fmt.Errorf("invalid container ID: %w", err) } - c, err := getN3Client(viper.GetViper()) + c, err := GetN3Client(viper.GetViper()) if err != nil { return fmt.Errorf("can't create N3 client: %w", err) } @@ -122,7 +122,7 @@ func loadSummaryFunc(cmd *cobra.Command, args []string) error { } } - c, err := getN3Client(viper.GetViper()) + c, err := GetN3Client(viper.GetViper()) if err != nil { return fmt.Errorf("can't create N3 client: %w", err) } diff --git a/cmd/neofs-adm/internal/modules/fschain/n3client.go b/cmd/neofs-adm/internal/modules/fschain/n3client.go index 8827d425e8..64cc3e4ad3 100644 --- a/cmd/neofs-adm/internal/modules/fschain/n3client.go +++ b/cmd/neofs-adm/internal/modules/fschain/n3client.go @@ -48,7 +48,8 @@ type clientContext struct { SentTxs []hashVUBPair } -func getN3Client(v *viper.Viper) (*rpcclient.Client, error) { +// GetN3Client creates and initializes neo-go RPC client using configuration from viper. +func GetN3Client(v *viper.Viper) (*rpcclient.Client, error) { // number of opened connections // by neo-go client per one host const ( diff --git a/cmd/neofs-adm/internal/modules/fschain/netmap_candidates.go b/cmd/neofs-adm/internal/modules/fschain/netmap_candidates.go index d9b9d4801a..202f59df0b 100644 --- a/cmd/neofs-adm/internal/modules/fschain/netmap_candidates.go +++ b/cmd/neofs-adm/internal/modules/fschain/netmap_candidates.go @@ -12,7 +12,7 @@ import ( ) func listNetmapCandidatesNodes(cmd *cobra.Command, _ []string) error { - c, err := getN3Client(viper.GetViper()) + c, err := GetN3Client(viper.GetViper()) if err != nil { return err } diff --git a/cmd/neofs-adm/internal/modules/fschain/nodes.go b/cmd/neofs-adm/internal/modules/fschain/nodes.go index a171a9a039..eee37e78f3 100644 --- a/cmd/neofs-adm/internal/modules/fschain/nodes.go +++ b/cmd/neofs-adm/internal/modules/fschain/nodes.go @@ -40,7 +40,7 @@ func nodesFunc(cmd *cobra.Command, _ []string) error { return fmt.Errorf("invalid container ID: %w", err) } - c, err := getN3Client(viper.GetViper()) + c, err := GetN3Client(viper.GetViper()) if err != nil { return fmt.Errorf("can't create N3 client: %w", err) } diff --git a/cmd/neofs-adm/internal/modules/fschain/notary.go b/cmd/neofs-adm/internal/modules/fschain/notary.go index 7fc9ce4823..b64405a874 100644 --- a/cmd/neofs-adm/internal/modules/fschain/notary.go +++ b/cmd/neofs-adm/internal/modules/fschain/notary.go @@ -79,7 +79,7 @@ func depositNotary(cmd *cobra.Command, _ []string) error { } } - c, err := getN3Client(viper.GetViper()) + c, err := GetN3Client(viper.GetViper()) if err != nil { return err } diff --git a/cmd/neofs-adm/internal/modules/fschain/quota.go b/cmd/neofs-adm/internal/modules/fschain/quota.go index 080eb68351..c0643a8822 100644 --- a/cmd/neofs-adm/internal/modules/fschain/quota.go +++ b/cmd/neofs-adm/internal/modules/fschain/quota.go @@ -74,7 +74,7 @@ func quotaContainerFunc(cmd *cobra.Command, args []string) error { return fmt.Errorf("invalid container ID: %w", err) } - c, err := getN3Client(viper.GetViper()) + c, err := GetN3Client(viper.GetViper()) if err != nil { return fmt.Errorf("can't create N3 client: %w", err) } @@ -171,7 +171,7 @@ func quotaUserFunc(cmd *cobra.Command, args []string) error { return fmt.Errorf("invalid user account: %w", err) } - c, err := getN3Client(viper.GetViper()) + c, err := GetN3Client(viper.GetViper()) if err != nil { return fmt.Errorf("can't create N3 client: %w", err) } diff --git a/cmd/neofs-adm/internal/modules/fschain/verified_domains.go b/cmd/neofs-adm/internal/modules/fschain/verified_domains.go index 841eb6819b..a3aec08077 100644 --- a/cmd/neofs-adm/internal/modules/fschain/verified_domains.go +++ b/cmd/neofs-adm/internal/modules/fschain/verified_domains.go @@ -30,7 +30,7 @@ const tokenNotFound = "token not found" func verifiedNodesDomainAccessList(cmd *cobra.Command, _ []string) error { vpr := viper.GetViper() - n3Client, err := getN3Client(vpr) + n3Client, err := GetN3Client(vpr) if err != nil { return fmt.Errorf("open connection: %w", err) } @@ -150,7 +150,7 @@ func verifiedNodesDomainSetAccessList(cmd *cobra.Command, _ []string) error { return fmt.Errorf("failed to unlock the account with password: %w", err) } - n3Client, err := getN3Client(vpr) + n3Client, err := GetN3Client(vpr) if err != nil { return fmt.Errorf("open connection: %w", err) } diff --git a/cmd/neofs-adm/internal/modules/mainchain/root.go b/cmd/neofs-adm/internal/modules/mainchain/root.go new file mode 100644 index 0000000000..50bc27ae0a --- /dev/null +++ b/cmd/neofs-adm/internal/modules/mainchain/root.go @@ -0,0 +1,17 @@ +package mainchain + +import ( + "github.com/spf13/cobra" +) + +// RootCmd is the root command for mainchain operations. +var RootCmd = &cobra.Command{ + Use: "mainchain", + Short: "Main chain network operations", +} + +func init() { + RootCmd.AddCommand(updateContractCommand) + + initUpdateContractCmd() +} diff --git a/cmd/neofs-adm/internal/modules/mainchain/update.go b/cmd/neofs-adm/internal/modules/mainchain/update.go new file mode 100644 index 0000000000..d194d7a54c --- /dev/null +++ b/cmd/neofs-adm/internal/modules/mainchain/update.go @@ -0,0 +1,351 @@ +package mainchain + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/encoding/address" + io2 "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" + scContext "github.com/nspcc-dev/neo-go/pkg/smartcontract/context" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/nspcc-dev/neo-go/pkg/vm/vmstate" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "github.com/nspcc-dev/neofs-node/cmd/neofs-adm/internal/modules/config" + "github.com/nspcc-dev/neofs-node/cmd/neofs-adm/internal/modules/fschain" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +const ( + alphabetWalletsFlag = "alphabet-wallets" + endpointFlag = "rpc-endpoint" + contractHashFlag = "contract-hash" + nefFlag = "nef" + manifestFlag = "manifest" + dataFlag = "data" + awaitFlag = "await" + gasWalletFlag = "gas-wallet" + + committeeAccountName = "committee" + + maxAttemptsTxWait = 20 +) + +var updateContractCommand = &cobra.Command{ + Use: "update-contract", + Short: "Update contract in main chain Neo network", + Long: `Update contract in main chain Neo network using alphabet wallets for multisig. +This command creates a transaction calling the 'update' method of the specified contract, +signs it with alphabet wallets, and sends to the Neo RPC node. + +The transaction requires two signers: +1. Gas wallet (pays transaction fees) - must have sufficient GAS balance +2. Committee multisig (authorizes the contract update) - alphabet wallets + +Example: + neofs-adm mainchain update-contract \ + --config ./wallet-config.yml \ + --alphabet-wallets ./alphabet-wallets \ + --gas-wallet ./gas-wallet.json \ + --rpc-endpoint http://main-chain.neofs.devenv:30333 \ + --contract-hash Nd7UQEh78WaVdfVaGKu6WaL8Hj9TGQ7Z3J \ + --nef neofs/contract.nef \ + --manifest neofs/manifest.json \ + --data nil \ + --await`, + PreRun: func(cmd *cobra.Command, _ []string) { + _ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag)) + _ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag)) + _ = viper.BindPFlag(gasWalletFlag, cmd.Flags().Lookup(gasWalletFlag)) + }, + RunE: updateContractCmd, +} + +func updateContractCmd(cmd *cobra.Command, _ []string) error { + contractHashStr, _ := cmd.Flags().GetString(contractHashFlag) + if contractHashStr == "" { + return errors.New("contract hash is required") + } + + contractHash, err := util.Uint160DecodeStringLE(strings.TrimPrefix(contractHashStr, "0x")) + if err != nil { + contractHash, err = address.StringToUint160(contractHashStr) + if err != nil { + return fmt.Errorf("invalid contract hash: %w", err) + } + } + + nefFile, _ := cmd.Flags().GetString(nefFlag) + if nefFile == "" { + return errors.New("NEF file is required") + } + + manifestFile, _ := cmd.Flags().GetString(manifestFlag) + if manifestFile == "" { + return errors.New("manifest file is required") + } + + dataStr, _ := cmd.Flags().GetString(dataFlag) + var data any + if dataStr != "" && dataStr != "nil" { + data = dataStr + } + + nefBytes, err := os.ReadFile(nefFile) + if err != nil { + return fmt.Errorf("can't read NEF file: %w", err) + } + + manifestBytes, err := os.ReadFile(manifestFile) + if err != nil { + return fmt.Errorf("can't read manifest file: %w", err) + } + + v := viper.GetViper() + walletDir := config.ResolveHomePath(viper.GetString(alphabetWalletsFlag)) + wallets, err := openAlphabetWallets(v, walletDir) + if err != nil { + return fmt.Errorf("can't open alphabet wallets: %w", err) + } + + gasWalletPath, _ := cmd.Flags().GetString(gasWalletFlag) + if gasWalletPath == "" { + return errors.New("gas wallet is required (use --gas-wallet flag)") + } + gasWallet, err := openWallet(gasWalletPath, v) + if err != nil { + return fmt.Errorf("can't open gas wallet: %w", err) + } + gasAcc, err := fschain.GetWalletAccount(gasWallet, "single") + if err != nil { + return fmt.Errorf("can't find gas wallet account: %w", err) + } + + c, err := fschain.GetN3Client(v) + if err != nil { + return fmt.Errorf("can't create N3 client: %w", err) + } + + committeeAcc, err := fschain.GetWalletAccount(wallets[0], committeeAccountName) + if err != nil { + return fmt.Errorf("can't find committee account: %w", err) + } + + // Create two signers: + // 1. Gas wallet - pays fees (FeePayer with CalledByEntry) + // 2. Committee multisig - authorizes contract call (CalledByEntry) + signers := []actor.SignerAccount{ + { + Signer: transaction.Signer{ + Account: gasAcc.Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: gasAcc, + }, + { + Signer: transaction.Signer{ + Account: committeeAcc.Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: committeeAcc, + }, + } + + act, err := actor.New(c, signers) + if err != nil { + return fmt.Errorf("can't create actor: %w", err) + } + + w := io2.NewBufBinWriter() + emit.AppCall(w.BinWriter, contractHash, "update", callflag.All, nefBytes, manifestBytes, data) + script := w.Bytes() + + tx, err := act.MakeUnsignedRun(script, nil) + if err != nil { + return fmt.Errorf("can't create unsigned transaction: %w", err) + } + + networkMagic := act.GetNetwork() + + gasSignature := gasAcc.PrivateKey().SignHashable(uint32(networkMagic), tx) + gasWitness := transaction.Witness{ + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, gasSignature...), + VerificationScript: gasAcc.Contract.Script, + } + gasScriptHash := gasAcc.Contract.ScriptHash() + for i := range tx.Signers { + if tx.Signers[i].Account == gasScriptHash { + tx.Scripts[i] = gasWitness + break + } + } + + if err := multiSignTransaction(tx, wallets, committeeAccountName, networkMagic); err != nil { + return fmt.Errorf("can't sign transaction: %w", err) + } + + txHash, err := c.SendRawTransaction(tx) + if err != nil { + return fmt.Errorf("can't send transaction: %w", err) + } + + cmd.Printf("Transaction sent successfully!\n") + cmd.Printf("Transaction hash: %s\n", txHash.StringLE()) + + await, _ := cmd.Flags().GetBool(awaitFlag) + if await { + cmd.Println("Waiting for transaction to be included in block...") + if err := waitForTxMainChain(c, txHash, tx.ValidUntilBlock); err != nil { + return fmt.Errorf("transaction failed: %w", err) + } + cmd.Println("Transaction confirmed") + } + + return nil +} + +func multiSignTransaction(tx *transaction.Transaction, wallets []*wallet.Wallet, accType string, network netmode.Magic) error { + firstAcc, err := fschain.GetWalletAccount(wallets[0], accType) + if err != nil { + return fmt.Errorf("can't get first wallet account: %w", err) + } + + scriptHash := firstAcc.Contract.ScriptHash() + + pc := scContext.NewParameterContext("", network, tx) + + for _, w := range wallets { + acc, err := fschain.GetWalletAccount(w, accType) + if err != nil { + continue // Skip wallets that don't have this account + } + + priv := acc.PrivateKey() + sign := priv.SignHashable(uint32(network), tx) + + if err := pc.AddSignature(scriptHash, acc.Contract, priv.PublicKey(), sign); err != nil { + return fmt.Errorf("can't add signature: %w", err) + } + + if len(pc.Items[scriptHash].Signatures) == len(acc.Contract.Parameters) { + break + } + } + + witness, err := pc.GetWitness(scriptHash) + if err != nil { + return fmt.Errorf("incomplete signature: %w", err) + } + + for i := range tx.Signers { + if tx.Signers[i].Account == scriptHash { + if i < len(tx.Scripts) { + tx.Scripts[i] = *witness + } else if i == len(tx.Scripts) { + tx.Scripts = append(tx.Scripts, *witness) + } else { + panic("BUG: invalid signing order") + } + return nil + } + } + + return fmt.Errorf("%s account was not found among transaction signers", accType) +} + +func waitForTxMainChain(c fschain.Client, hash util.Uint256, vub uint32) error { + for range maxAttemptsTxWait { + time.Sleep(time.Second) + + height, err := c.GetBlockCount() + if err == nil && height > vub { + return fmt.Errorf("transaction expired: current height %d > vub %d", height, vub) + } + + appLog, err := c.GetApplicationLog(hash, nil) + if err == nil && len(appLog.Executions) > 0 { + if appLog.Executions[0].VMState.HasFlag(vmstate.Halt) { + return nil + } + return fmt.Errorf("transaction failed with state: %s, exception: %s", + appLog.Executions[0].VMState, appLog.Executions[0].FaultException) + } + } + + return errors.New("timeout waiting for transaction") +} + +func openAlphabetWallets(v *viper.Viper, walletDir string) ([]*wallet.Wallet, error) { + walletFiles, err := os.ReadDir(walletDir) + if err != nil { + return nil, fmt.Errorf("can't read alphabet wallets dir: %w", err) + } + + var wallets []*wallet.Wallet + + for _, walletFile := range walletFiles { + isJson := strings.HasSuffix(walletFile.Name(), ".json") + if walletFile.IsDir() || !isJson { + continue // Ignore garbage. + } + + w, err := openWallet(filepath.Join(walletDir, walletFile.Name()), v) + if err != nil { + return nil, fmt.Errorf("can't open wallet %s: %w", walletFile.Name(), err) + } + + wallets = append(wallets, w) + } + if len(wallets) == 0 { + return nil, errors.New("alphabet wallets dir is empty (run `generate-alphabet` command first)") + } + + return wallets, nil +} + +func openWallet(path string, v *viper.Viper) (*wallet.Wallet, error) { + path = config.ResolveHomePath(path) + w, err := wallet.NewWalletFromFile(path) + if err != nil { + return nil, fmt.Errorf("can't open gas wallet: %w", err) + } + walletName := filepath.Base(path) + walletName = strings.TrimSuffix(walletName, ".json") + password, err := config.GetPassword(v, walletName) + if err != nil { + return nil, fmt.Errorf("can't fetch password: %w", err) + } + for i := range w.Accounts { + if err := w.Accounts[i].Decrypt(password, w.Scrypt); err != nil { + return nil, fmt.Errorf("can't unlock wallet: %w", err) + } + } + return w, nil +} + +func initUpdateContractCmd() { + flags := updateContractCommand.Flags() + flags.String(alphabetWalletsFlag, "", "Path to alphabet wallets dir") + flags.StringP(endpointFlag, "r", "", "N3 RPC node endpoint") + flags.String(gasWalletFlag, "", "Path to wallet that will pay transaction fees (must have GAS)") + flags.String(contractHashFlag, "", "Contract hash (address or hex)") + flags.String(nefFlag, "", "Path to NEF file") + flags.String(manifestFlag, "", "Path to manifest file") + flags.String(dataFlag, "", "Optional data parameter for update method (default: nil)") + flags.Bool(awaitFlag, false, "Wait for the transaction to be included in a block") + + _ = updateContractCommand.MarkFlagRequired(gasWalletFlag) + _ = updateContractCommand.MarkFlagRequired(contractHashFlag) + _ = updateContractCommand.MarkFlagRequired(nefFlag) + _ = updateContractCommand.MarkFlagRequired(manifestFlag) +} diff --git a/cmd/neofs-adm/internal/modules/root.go b/cmd/neofs-adm/internal/modules/root.go index cec1f5894d..cc9b01087b 100644 --- a/cmd/neofs-adm/internal/modules/root.go +++ b/cmd/neofs-adm/internal/modules/root.go @@ -5,6 +5,7 @@ import ( "github.com/nspcc-dev/neofs-node/cmd/neofs-adm/internal/modules/config" "github.com/nspcc-dev/neofs-node/cmd/neofs-adm/internal/modules/fschain" + "github.com/nspcc-dev/neofs-node/cmd/neofs-adm/internal/modules/mainchain" "github.com/nspcc-dev/neofs-node/cmd/neofs-adm/internal/modules/storagecfg" "github.com/nspcc-dev/neofs-node/misc" "github.com/nspcc-dev/neofs-node/pkg/util/autocomplete" @@ -40,6 +41,7 @@ func init() { rootCmd.AddCommand(config.RootCmd) rootCmd.AddCommand(fschain.RootCmd) + rootCmd.AddCommand(mainchain.RootCmd) rootCmd.AddCommand(storagecfg.RootCmd) rootCmd.AddCommand(autocomplete.Command("neofs-adm"))