package morph import ( "encoding/json" "fmt" "os" "strings" "git.frostfs.info/TrueCloudLab/frostfs-contract/nns" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/util" "github.com/nspcc-dev/neo-go/cli/cmdargs" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/rpcclient/management" "github.com/nspcc-dev/neo-go/pkg/services/rpcsrv/params" "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/vm/opcode" "github.com/spf13/cobra" "github.com/spf13/viper" ) const ( contractPathFlag = "contract" updateFlag = "update" customZoneFlag = "domain" ) var deployCmd = &cobra.Command{ Use: "deploy", Short: "Deploy additional smart-contracts", Long: `Deploy additional smart-contract which are not related to core. All contracts are deployed by the committee, so access to the alphabet wallets is required. Optionally, arguments can be provided to be passed to a contract's _deploy function. The syntax is the same as for 'neo-go contract testinvokefunction' command. Compiled contract file name must contain '_contract.nef' suffix. Contract's manifest file name must be 'config.json'. NNS name is taken by stripping '_contract.nef' from the NEF file (similar to frostfs contracts).`, PreRun: func(cmd *cobra.Command, _ []string) { _ = viper.BindPFlag(util.AlphabetWalletsFlag, cmd.Flags().Lookup(util.AlphabetWalletsFlag)) _ = viper.BindPFlag(util.EndpointFlag, cmd.Flags().Lookup(util.EndpointFlag)) }, RunE: deployContractCmd, } func init() { ff := deployCmd.Flags() ff.String(util.AlphabetWalletsFlag, "", util.AlphabetWalletsFlagDesc) _ = deployCmd.MarkFlagFilename(util.AlphabetWalletsFlag) ff.StringP(util.EndpointFlag, "r", "", util.EndpointFlagDesc) ff.String(contractPathFlag, "", "Path to the contract directory") _ = deployCmd.MarkFlagFilename(contractPathFlag) ff.Bool(updateFlag, false, "Update an existing contract") ff.String(customZoneFlag, "frostfs", "Custom zone for NNS") } func deployContractCmd(cmd *cobra.Command, args []string) error { v := viper.GetViper() c, err := util.NewInitializeContext(cmd, v) if err != nil { return fmt.Errorf("initialization error: %w", err) } defer c.Close() ctrPath, _ := cmd.Flags().GetString(contractPathFlag) ctrName, err := probeContractName(ctrPath) if err != nil { return err } cs, err := util.ReadContract(ctrPath, ctrName) if err != nil { return err } r := management.NewReader(c.ReadOnlyInvoker) nnsCs, err := r.GetContractByID(1) if err != nil { return fmt.Errorf("can't fetch NNS contract state: %w", err) } callHash := management.Hash method := deployMethodName zone, _ := cmd.Flags().GetString(customZoneFlag) domain := ctrName + "." + zone isUpdate, _ := cmd.Flags().GetBool(updateFlag) if isUpdate { cs.Hash, err = util.NNSResolveHash(c.ReadOnlyInvoker, nnsCs.Hash, domain) if err != nil { return fmt.Errorf("can't fetch contract hash from NNS: %w", err) } callHash = cs.Hash method = updateMethodName } else { cs.Hash = state.CreateContractHash( c.CommitteeAcc.Contract.ScriptHash(), cs.NEF.Checksum, cs.Manifest.Name) } writer := io.NewBufBinWriter() if err := emitDeploymentArguments(writer.BinWriter, args); err != nil { return err } emit.Bytes(writer.BinWriter, cs.RawManifest) emit.Bytes(writer.BinWriter, cs.RawNEF) emit.Int(writer.BinWriter, 3) emit.Opcodes(writer.BinWriter, opcode.PACK) emit.AppCallNoArgs(writer.BinWriter, callHash, method, callflag.All) emit.Opcodes(writer.BinWriter, opcode.DROP) // contract state on stack if !isUpdate { err := registerNNS(nnsCs, c, zone, domain, cs, writer) if err != nil { return err } } if writer.Err != nil { panic(fmt.Errorf("BUG: can't create deployment script: %w", writer.Err)) } if err := c.SendCommitteeTx(writer.Bytes(), false); err != nil { return err } return c.AwaitTx() } func registerNNS(nnsCs *state.Contract, c *util.InitializeContext, zone string, domain string, cs *util.ContractState, writer *io.BufBinWriter) error { bw := io.NewBufBinWriter() emit.Instruction(bw.BinWriter, opcode.INITSSLOT, []byte{1}) emit.AppCall(bw.BinWriter, nnsCs.Hash, "getPrice", callflag.All) emit.Opcodes(bw.BinWriter, opcode.STSFLD0) emit.AppCall(bw.BinWriter, nnsCs.Hash, "setPrice", callflag.All, 1) start := bw.Len() needRecord := false ok, err := c.NNSRootRegistered(nnsCs.Hash, zone) if err != nil { return err } else if !ok { needRecord = true emit.AppCall(bw.BinWriter, nnsCs.Hash, "register", callflag.All, zone, c.CommitteeAcc.Contract.ScriptHash(), util.FrostfsOpsEmail, int64(3600), int64(600), int64(util.DefaultExpirationTime), int64(3600)) emit.Opcodes(bw.BinWriter, opcode.ASSERT) emit.AppCall(bw.BinWriter, nnsCs.Hash, "register", callflag.All, domain, c.CommitteeAcc.Contract.ScriptHash(), util.FrostfsOpsEmail, int64(3600), int64(600), int64(util.DefaultExpirationTime), int64(3600)) emit.Opcodes(bw.BinWriter, opcode.ASSERT) } else { s, ok, err := c.NNSRegisterDomainScript(nnsCs.Hash, cs.Hash, domain) if err != nil { return err } needRecord = !ok if len(s) != 0 { bw.WriteBytes(s) } } if needRecord { emit.AppCall(bw.BinWriter, nnsCs.Hash, "deleteRecords", callflag.All, domain, int64(nns.TXT)) emit.AppCall(bw.BinWriter, nnsCs.Hash, "addRecord", callflag.All, domain, int64(nns.TXT), address.Uint160ToString(cs.Hash)) } if bw.Err != nil { panic(fmt.Errorf("BUG: can't create deployment script: %w", writer.Err)) } else if bw.Len() != start { writer.WriteBytes(bw.Bytes()) emit.Opcodes(writer.BinWriter, opcode.LDSFLD0, opcode.PUSH1, opcode.PACK) emit.AppCallNoArgs(writer.BinWriter, nnsCs.Hash, "setPrice", callflag.All) if needRecord { c.Command.Printf("NNS: Set %s -> %s\n", domain, cs.Hash.StringLE()) } } return nil } func emitDeploymentArguments(w *io.BinWriter, args []string) error { _, ps, err := cmdargs.ParseParams(args, true) if err != nil { return err } if len(ps) == 0 { emit.Opcodes(w, opcode.NEWARRAY0) return nil } if len(ps) != 1 { return fmt.Errorf("at most one argument is expected for deploy, got %d", len(ps)) } // We could emit this directly, but round-trip through JSON is more robust. // This a CLI, so optimizing the conversion is not worth the effort. data, err := json.Marshal(ps) if err != nil { return err } var pp params.Params if err := json.Unmarshal(data, &pp); err != nil { return err } return params.ExpandArrayIntoScript(w, pp) } func probeContractName(ctrPath string) (string, error) { ds, err := os.ReadDir(ctrPath) if err != nil { return "", fmt.Errorf("can't read directory: %w", err) } var ctrName string for i := range ds { if strings.HasSuffix(ds[i].Name(), "_contract.nef") { ctrName = strings.TrimSuffix(ds[i].Name(), "_contract.nef") break } } if ctrName == "" { return "", fmt.Errorf("can't find any NEF files in %s", ctrPath) } return ctrName, nil }