diff --git a/CHANGELOG.md b/CHANGELOG.md index c352caf2558..8eede54ddd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Changelog for NeoFS Node ## [Unreleased] ### Added +- `neofs-adm morph generate-storage-wallet` now supports more than one wallet generation per call (#2425) ### Fixed - Fund transfer deadlock in NeoFS chain auto-deploy/update procedure (#2681) diff --git a/cmd/neofs-adm/internal/modules/morph/generate.go b/cmd/neofs-adm/internal/modules/morph/generate.go index f7afea577c5..5c433e5995a 100644 --- a/cmd/neofs-adm/internal/modules/morph/generate.go +++ b/cmd/neofs-adm/internal/modules/morph/generate.go @@ -3,19 +3,18 @@ package morph import ( "errors" "fmt" + "math/big" "os" + "path" "path/filepath" + "strings" "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/encoding/fixedn" - "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/rpcclient/gas" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17" "github.com/nspcc-dev/neo-go/pkg/smartcontract" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "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/wallet" "github.com/nspcc-dev/neofs-node/cmd/neofs-adm/internal/modules/config" "github.com/nspcc-dev/neofs-node/pkg/util/glagolitsa" @@ -128,81 +127,67 @@ func addMultisigAccount(w *wallet.Wallet, m int, name, password string, pubs key } func generateStorageCreds(cmd *cobra.Command, _ []string) error { - return refillGas(cmd, storageGasConfigFlag, true) -} - -func refillGas(cmd *cobra.Command, gasFlag string, createWallet bool) (err error) { // storage wallet path is not part of the config storageWalletPath, _ := cmd.Flags().GetString(storageWalletFlag) - // wallet address is not part of the config - walletAddress, _ := cmd.Flags().GetString(walletAddressFlag) - - var gasReceiver util.Uint160 - - if len(walletAddress) != 0 { - gasReceiver, err = address.StringToUint160(walletAddress) - if err != nil { - return fmt.Errorf("invalid wallet address %s: %w", walletAddress, err) - } - } else { - if storageWalletPath == "" { - return fmt.Errorf("missing wallet path (use '--%s ')", storageWalletFlag) - } - - var w *wallet.Wallet - - if createWallet { - w, err = wallet.NewWallet(storageWalletPath) - } else { - w, err = wallet.NewWalletFromFile(storageWalletPath) - } - - if err != nil { - return fmt.Errorf("can't create wallet: %w", err) - } - - if createWallet { - var password string - - label, _ := cmd.Flags().GetString(storageWalletLabelFlag) - password, err := config.GetStoragePassword(viper.GetViper(), label) - if err != nil { - return fmt.Errorf("can't fetch password: %w", err) - } + if storageWalletPath == "" { + return fmt.Errorf("missing wallet path (use '--%s ')", storageWalletFlag) + } - if label == "" { - label = singleAccountName - } + walletsNumber, err := cmd.Flags().GetUint32(storageWalletsNumber) + if err != nil { + return err + } + if walletsNumber == 0 { + walletsNumber = 1 + } - if err := w.CreateAccount(label, password); err != nil { - return fmt.Errorf("can't create account: %w", err) - } - } + label, _ := cmd.Flags().GetString(storageWalletLabelFlag) + password, err := config.GetStoragePassword(viper.GetViper(), label) + if err != nil { + return fmt.Errorf("can't fetch password: %w", err) + } - gasReceiver = w.Accounts[0].Contract.ScriptHash() + if label == "" { + label = singleAccountName } - gasStr := viper.GetString(gasFlag) + hashes, err := createWallets(storageWalletPath, label, password, walletsNumber) + if err != nil { + return err + } - gasAmount, err := parseGASAmount(gasStr) + gasAmount, err := parseGASAmount(viper.GetString(refillGasAmountFlag)) if err != nil { return err } + return refillGas(cmd, int64(gasAmount), hashes) +} + +func refillGas(cmd *cobra.Command, gasAmount int64, receivers []util.Uint160) (err error) { wCtx, err := newInitializeContext(cmd, viper.GetViper()) if err != nil { return err } - bw := io.NewBufBinWriter() - emit.AppCall(bw.BinWriter, gas.Hash, "transfer", callflag.All, - wCtx.CommitteeAcc.Contract.ScriptHash(), gasReceiver, int64(gasAmount), nil) - emit.Opcodes(bw.BinWriter, opcode.ASSERT) - if bw.Err != nil { - return fmt.Errorf("BUG: invalid transfer arguments: %w", bw.Err) + committeeScriptHash := wCtx.CommitteeAcc.Contract.ScriptHash() + + var pp []nep17.TransferParameters + for _, receiver := range receivers { + pp = append(pp, nep17.TransferParameters{ + From: committeeScriptHash, + To: receiver, + Amount: big.NewInt(gasAmount), + }) } - if err := wCtx.sendCommitteeTx(bw.Bytes(), false); err != nil { + gToken := nep17.New(wCtx.CommitteeAct, gas.Hash) + tx, err := gToken.MultiTransferUnsigned(pp) + if err != nil { + return err + } + + if err := wCtx.multiSignAndSend(tx, committeeAccountName); err != nil { return err } @@ -219,3 +204,41 @@ func parseGASAmount(s string) (fixedn.Fixed8, error) { } return gasAmount, nil } + +func createWallets(fileNameTemplate, label, password string, number uint32) ([]util.Uint160, error) { + var res []util.Uint160 + ext := path.Ext(fileNameTemplate) + base := strings.TrimSuffix(fileNameTemplate, ext) + walletNumberFormat := fmt.Sprintf("%%0%dd", digitsNum(number)) + + for i := 0; i < int(number); i++ { + filename := fileNameTemplate + if number != 1 { + filename = base + "_" + fmt.Sprintf(walletNumberFormat, i) + ext + } + + w, err := wallet.NewWallet(filename) + if err != nil { + return nil, fmt.Errorf("wallet creation: %w", err) + } + + err = w.CreateAccount(label, password) + if err != nil { + return nil, fmt.Errorf("account creation: %w", err) + } + + res = append(res, w.Accounts[0].Contract.ScriptHash()) + } + + return res, nil +} + +func digitsNum(val uint32) int { + var res int + for val != 0 { + val /= 10 + res += 1 + } + + return res +} diff --git a/cmd/neofs-adm/internal/modules/morph/generate_test.go b/cmd/neofs-adm/internal/modules/morph/generate_test.go index f5a63d37d06..504b0ebdf34 100644 --- a/cmd/neofs-adm/internal/modules/morph/generate_test.go +++ b/cmd/neofs-adm/internal/modules/morph/generate_test.go @@ -2,9 +2,11 @@ package morph import ( "bytes" + "fmt" "io" "math/rand" "os" + "path" "path/filepath" "strconv" "testing" @@ -90,3 +92,34 @@ func setupTestTerminal(t *testing.T) *bytes.Buffer { return in } + +func TestCreateWallets(t *testing.T) { + dir := t.TempDir() + + const label = "label" + const password = "pass" + + hashes, err := createWallets(dir+"/test.json", label, password, 11) + require.NoError(t, err) + + entries, err := os.ReadDir(dir) + require.NoError(t, err) + + for i, entry := range entries { + if i < 10 { + require.Equal(t, fmt.Sprintf("test_0%d.json", i), entry.Name()) + } else { + require.Equal(t, fmt.Sprintf("test_%d.json", i), entry.Name()) + } + } + + for i, entry := range entries { + w, err := wallet.NewWalletFromFile(path.Join(dir, entry.Name())) + require.NoError(t, err) + + acc := w.GetAccount(hashes[i]) + require.NoError(t, acc.Decrypt(password, keys.NEP2ScryptParams())) + + require.Equal(t, label, acc.Label) + } +} diff --git a/cmd/neofs-adm/internal/modules/morph/root.go b/cmd/neofs-adm/internal/modules/morph/root.go index 1fcb5a6550a..2599715bfd4 100644 --- a/cmd/neofs-adm/internal/modules/morph/root.go +++ b/cmd/neofs-adm/internal/modules/morph/root.go @@ -1,6 +1,11 @@ package morph import ( + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/encoding/address" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -11,6 +16,7 @@ const ( endpointFlag = "rpc-endpoint" storageWalletFlag = "storage-wallet" storageWalletLabelFlag = "label" + storageWalletsNumber = "wallets-number" storageGasCLIFlag = "initial-gas" storageGasConfigFlag = "storage.initial_gas" contractsInitFlag = "contracts" @@ -106,7 +112,38 @@ var ( _ = viper.BindPFlag(refillGasAmountFlag, cmd.Flags().Lookup(refillGasAmountFlag)) }, RunE: func(cmd *cobra.Command, args []string) error { - return refillGas(cmd, refillGasAmountFlag, false) + var gasReceiver util.Uint160 + var err error + + // wallet address is not part of the config + walletAddress, _ := cmd.Flags().GetString(walletAddressFlag) + + if len(walletAddress) != 0 { + gasReceiver, err = address.StringToUint160(walletAddress) + if err != nil { + return fmt.Errorf("invalid wallet address %s: %w", walletAddress, err) + } + } else { + // storage wallet path is not part of the config + storageWalletPath, _ := cmd.Flags().GetString(storageWalletFlag) + if storageWalletPath == "" { + return fmt.Errorf("missing wallet path (use '--%s ')", storageWalletFlag) + } + + w, err := wallet.NewWallet(storageWalletPath) + if err != nil { + return fmt.Errorf("can't open wallet: %w", err) + } + + gasReceiver = w.Accounts[0].Contract.ScriptHash() + } + + gasAmount, err := parseGASAmount(viper.GetString(refillGasAmountFlag)) + if err != nil { + return err + } + + return refillGas(cmd, int64(gasAmount), []util.Uint160{gasReceiver}) }, } @@ -319,8 +356,9 @@ func init() { RootCmd.AddCommand(generateStorageCmd) generateStorageCmd.Flags().String(alphabetWalletsFlag, "", "Path to alphabet wallets dir") generateStorageCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint") - generateStorageCmd.Flags().String(storageWalletFlag, "", "Path to new storage node wallet") + generateStorageCmd.Flags().String(storageWalletFlag, "", "Path to new storage node wallet(s)") generateStorageCmd.Flags().String(storageGasCLIFlag, "", "Initial amount of GAS to transfer") + generateStorageCmd.Flags().Uint32(storageWalletsNumber, 1, "Number of wallets to generate, if more than 1, suffix-number will be added to the filename") generateStorageCmd.Flags().StringP(storageWalletLabelFlag, "l", "", "Wallet label") RootCmd.AddCommand(forceNewEpoch)