diff --git a/cmd/neofs-adm/internal/modules/morph/generate.go b/cmd/neofs-adm/internal/modules/morph/generate.go index 88de2532..b40a2a44 100644 --- a/cmd/neofs-adm/internal/modules/morph/generate.go +++ b/cmd/neofs-adm/internal/modules/morph/generate.go @@ -156,12 +156,9 @@ func refillGas(cmd *cobra.Command, gasFlag string, createWallet bool) error { gasStr := viper.GetString(gasFlag) - gasAmount, err := fixedn.Fixed8FromString(gasStr) + gasAmount, err := parseGASAmount(gasStr) if err != nil { - return fmt.Errorf("invalid GAS amount %s: %w", gasStr, err) - } - if gasAmount <= 0 { - return fmt.Errorf("GAS amount must be positive (got %d)", gasAmount) + return err } wCtx, err := newInitializeContext(cmd, viper.GetViper()) @@ -184,3 +181,14 @@ func refillGas(cmd *cobra.Command, gasFlag string, createWallet bool) error { return wCtx.awaitTx() } + +func parseGASAmount(s string) (fixedn.Fixed8, error) { + gasAmount, err := fixedn.Fixed8FromString(s) + if err != nil { + return 0, fmt.Errorf("invalid GAS amount %s: %w", s, err) + } + if gasAmount <= 0 { + return 0, fmt.Errorf("GAS amount must be positive (got %d)", gasAmount) + } + return gasAmount, nil +} diff --git a/cmd/neofs-adm/internal/modules/morph/initialize.go b/cmd/neofs-adm/internal/modules/morph/initialize.go index ecd8b62e..5c67c829 100644 --- a/cmd/neofs-adm/internal/modules/morph/initialize.go +++ b/cmd/neofs-adm/internal/modules/morph/initialize.go @@ -128,21 +128,9 @@ func newInitializeContext(cmd *cobra.Command, v *viper.Viper) (*initializeContex } } - ns, err := c.GetNativeContracts() + nativeHashes, err := getNativeHashes(c) if err != nil { - return nil, fmt.Errorf("can't get native contract hashes: %w", err) - } - - notaryEnabled := false - nativeHashes := make(map[string]util.Uint160, len(ns)) - for i := range ns { - if ns[i].Manifest.Name == nativenames.Notary { - notaryEnabled = len(ns[i].UpdateHistory) > 0 - } - nativeHashes[ns[i].Manifest.Name] = ns[i].Hash - } - if !notaryEnabled { - return nil, errors.New("notary contract must be enabled") + return nil, err } accounts := make([]*wallet.Account, len(wallets)) @@ -155,19 +143,15 @@ func newInitializeContext(cmd *cobra.Command, v *viper.Viper) (*initializeContex } initCtx := &initializeContext{ - clientContext: clientContext{ - Client: c, - WaitDuration: time.Second * 30, - PollInterval: time.Second, - }, - ConsensusAcc: consensusAcc, - CommitteeAcc: committeeAcc, - Wallets: wallets, - Accounts: accounts, - Command: cmd, - Contracts: make(map[string]*contractState), - ContractPath: ctrPath, - Natives: nativeHashes, + clientContext: *defaultClientContext(c), + ConsensusAcc: consensusAcc, + CommitteeAcc: committeeAcc, + Wallets: wallets, + Accounts: accounts, + Command: cmd, + Contracts: make(map[string]*contractState), + ContractPath: ctrPath, + Natives: nativeHashes, } if needContracts { @@ -284,3 +268,23 @@ func getWalletAccount(w *wallet.Wallet, typ string) (*wallet.Account, error) { } return nil, fmt.Errorf("account for '%s' not found", typ) } + +func getNativeHashes(c *client.Client) (map[string]util.Uint160, error) { + ns, err := c.GetNativeContracts() + if err != nil { + return nil, fmt.Errorf("can't get native contract hashes: %w", err) + } + + notaryEnabled := false + nativeHashes := make(map[string]util.Uint160, len(ns)) + for i := range ns { + if ns[i].Manifest.Name == nativenames.Notary { + notaryEnabled = len(ns[i].UpdateHistory) > 0 + } + nativeHashes[ns[i].Manifest.Name] = ns[i].Hash + } + if !notaryEnabled { + return nil, errors.New("notary contract must be enabled") + } + return nativeHashes, nil +} diff --git a/cmd/neofs-adm/internal/modules/morph/n3client.go b/cmd/neofs-adm/internal/modules/morph/n3client.go index 4862816d..8db84d5a 100644 --- a/cmd/neofs-adm/internal/modules/morph/n3client.go +++ b/cmd/neofs-adm/internal/modules/morph/n3client.go @@ -35,6 +35,14 @@ func getN3Client(v *viper.Viper) (*client.Client, error) { return c, nil } +func defaultClientContext(c *client.Client) *clientContext { + return &clientContext{ + Client: c, + WaitDuration: time.Second * 30, + PollInterval: time.Second, + } +} + func (c *clientContext) sendTx(tx *transaction.Transaction, cmd *cobra.Command, await bool) error { h, err := c.Client.SendRawTransaction(tx) if err != nil { diff --git a/cmd/neofs-adm/internal/modules/morph/notary.go b/cmd/neofs-adm/internal/modules/morph/notary.go index e49eca08..b8f2bede 100644 --- a/cmd/neofs-adm/internal/modules/morph/notary.go +++ b/cmd/neofs-adm/internal/modules/morph/notary.go @@ -1 +1,132 @@ package morph + +import ( + "fmt" + "strconv" + + "github.com/nspcc-dev/neo-go/cli/input" + "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/encoding/address" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/rpc/client" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// defaultNotaryDepositLifetime is an amount of blocks notary deposit stays valid. +// https://github.com/nspcc-dev/neo-go/blob/master/pkg/core/native/notary.go#L48 +const defaultNotaryDepositLifetime = 5760 + +func depositNotary(cmd *cobra.Command, _ []string) error { + p, err := cmd.Flags().GetString(storageWalletFlag) + if err != nil { + return err + } else if p == "" { + return fmt.Errorf("missing wallet path (use '--%s ')", storageWalletFlag) + } + + w, err := wallet.NewWalletFromFile(p) + if err != nil { + return fmt.Errorf("can't open wallet: %v", err) + } + + accHash := w.GetChangeAddress() + if addr, err := cmd.Flags().GetString(walletAccountFlag); err == nil { + accHash, err = address.StringToUint160(addr) + if err != nil { + return fmt.Errorf("invalid address: %s", addr) + } + } + + acc := w.GetAccount(accHash) + if acc == nil { + return fmt.Errorf("can't find account for %s", accHash) + } + + prompt := fmt.Sprintf("Enter password for %s >", address.Uint160ToString(accHash)) + pass, err := input.ReadPassword(prompt) + if err != nil { + return fmt.Errorf("can't get password: %v", err) + } + + err = acc.Decrypt(pass, keys.NEP2ScryptParams()) + if err != nil { + return fmt.Errorf("can't unlock account: %v", err) + } + + gasStr, err := cmd.Flags().GetString(refillGasAmountFlag) + if err != nil { + return err + } + gasAmount, err := parseGASAmount(gasStr) + if err != nil { + return err + } + + till := int64(defaultNotaryDepositLifetime) + tillStr, err := cmd.Flags().GetString(notaryDepositTillFlag) + if err != nil { + return err + } + if tillStr != "" { + till, err = strconv.ParseInt(tillStr, 10, 64) + if err != nil || till <= 0 { + return fmt.Errorf("notary deposit lifetime must be a positive integer") + } + } + + c, err := getN3Client(viper.GetViper()) + if err != nil { + return err + } + + nhs, err := getNativeHashes(c) + if err != nil { + return fmt.Errorf("can't get native contract hashes: %w", err) + } + + gasHash, ok := nhs[nativenames.Gas] + if !ok { + return fmt.Errorf("can't retrieve %s contract hash", nativenames.Gas) + } + notaryHash, ok := nhs[nativenames.Notary] + if !ok { + return fmt.Errorf("can't retrieve %s contract hash", nativenames.Notary) + } + + height, err := c.GetBlockCount() + if err != nil { + return fmt.Errorf("can't get current height: %v", err) + } + + bw := io.NewBufBinWriter() + emit.AppCall(bw.BinWriter, gasHash, "transfer", callflag.All, + accHash, notaryHash.BytesBE(), int64(gasAmount), []interface{}{nil, int64(height) + till}) + if bw.Err != nil { + return fmt.Errorf("BUG: invalid transfer arguments: %w", bw.Err) + } + + tx, err := c.CreateTxFromScript(bw.Bytes(), acc, -1, 0, []client.SignerAccount{{ + Signer: transaction.Signer{ + Account: acc.Contract.ScriptHash(), + Scopes: transaction.Global, + }, + Account: acc, + }}) + if err != nil { + return fmt.Errorf("can't create tx: %w", err) + } + + err = acc.SignTx(c.GetNetwork(), tx) + if err != nil { + return fmt.Errorf("can't sign tx: %w", err) + } + + cc := defaultClientContext(c) + return cc.sendTx(tx, cmd, true) +} diff --git a/cmd/neofs-adm/internal/modules/morph/root.go b/cmd/neofs-adm/internal/modules/morph/root.go index 418c7f68..f90d7f9a 100644 --- a/cmd/neofs-adm/internal/modules/morph/root.go +++ b/cmd/neofs-adm/internal/modules/morph/root.go @@ -33,6 +33,8 @@ const ( containerContractFlag = "container-contract" containerIDsFlag = "cid" refillGasAmountFlag = "gas" + walletAccountFlag = "account" + notaryDepositTillFlag = "till" ) var ( @@ -150,6 +152,15 @@ var ( }, RunE: restoreContainers, } + + depositNotaryCmd = &cobra.Command{ + Use: "deposit-notary", + Short: "Deposit GAS for notary service.", + PreRun: func(cmd *cobra.Command, _ []string) { + _ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag)) + }, + RunE: depositNotary, + } ) func init() { @@ -207,4 +218,11 @@ func init() { refillGasCmd.Flags().String(refillGasAmountFlag, "", "additional amount of GAS to transfer") RootCmd.AddCommand(cmdSubnet) + + RootCmd.AddCommand(depositNotaryCmd) + depositNotaryCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint") + depositNotaryCmd.Flags().String(storageWalletFlag, "", "path to storage node wallet") + depositNotaryCmd.Flags().String(walletAccountFlag, "", "wallet account address") + depositNotaryCmd.Flags().String(refillGasAmountFlag, "", "amount of GAS to deposit") + depositNotaryCmd.Flags().String(notaryDepositTillFlag, "", "notary deposit duration in blocks") }