From fef4f6d155509fe76d203a8fee7b8e75cc197c1b Mon Sep 17 00:00:00 2001
From: Evgenii Stratonikov <evgeniy@morphbits.ru>
Date: Thu, 1 Sep 2022 14:07:34 +0300
Subject: [PATCH] [#1748] neofs-adm: Allow to dump hashes from a custom zone

Signed-off-by: Evgenii Stratonikov <evgeniy@morphbits.ru>
---
 .../internal/modules/morph/dump_hashes.go     | 144 +++++++++++++++---
 cmd/neofs-adm/internal/modules/morph/root.go  |   1 +
 2 files changed, 121 insertions(+), 24 deletions(-)

diff --git a/cmd/neofs-adm/internal/modules/morph/dump_hashes.go b/cmd/neofs-adm/internal/modules/morph/dump_hashes.go
index 9fa92a598..349dd1bbb 100644
--- a/cmd/neofs-adm/internal/modules/morph/dump_hashes.go
+++ b/cmd/neofs-adm/internal/modules/morph/dump_hashes.go
@@ -3,14 +3,21 @@ package morph
 import (
 	"bytes"
 	"fmt"
+	"strings"
 	"text/tabwriter"
 
+	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
 	"github.com/nspcc-dev/neo-go/pkg/io"
+	"github.com/nspcc-dev/neo-go/pkg/rpcclient"
+	"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
+	"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
 	"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/vm/stackitem"
 	"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
+	"github.com/nspcc-dev/neo-go/pkg/wallet"
 	"github.com/nspcc-dev/neofs-contract/nns"
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
@@ -35,6 +42,11 @@ func dumpContractHashes(cmd *cobra.Command, _ []string) error {
 		return err
 	}
 
+	zone, _ := cmd.Flags().GetString(customZoneFlag)
+	if zone != "" {
+		return dumpCustomZoneHashes(cmd, cs.Hash, zone, c)
+	}
+
 	infos := []contractDumpInfo{{name: nnsContract, hash: cs.Hash}}
 
 	irSize := 0
@@ -47,7 +59,6 @@ func dumpContractHashes(cmd *cobra.Command, _ []string) error {
 		}
 	}
 
-	buf := bytes.NewBuffer(nil)
 	bw := io.NewBufBinWriter()
 
 	if irSize != 0 {
@@ -91,37 +102,65 @@ func dumpContractHashes(cmd *cobra.Command, _ []string) error {
 		infos = append(infos, info)
 	}
 
-	bw.Reset()
-	for i := range infos {
-		if infos[i].hash.Equals(util.Uint160{}) {
-			emit.Int(bw.BinWriter, 0)
-		} else {
-			emit.AppCall(bw.BinWriter, infos[i].hash, "version", callflag.NoneFlag)
-		}
-	}
+	fillContractVersion(cmd, c, infos)
+	printContractInfo(cmd, infos)
 
-	res, err := c.InvokeScript(bw.Bytes(), nil)
+	return nil
+}
+
+func dumpCustomZoneHashes(cmd *cobra.Command, nnsHash util.Uint160, zone string, c Client) error {
+	const nnsMaxTokens = 100
+
+	// The actual signer is not important here.
+	account, err := wallet.NewAccount()
 	if err != nil {
-		return fmt.Errorf("can't fetch info from NNS: %w", err)
+		return fmt.Errorf("can't create a temporary account: %w", err)
 	}
 
-	if res.State == vmstate.Halt.String() {
-		for i := range res.Stack {
-			infos[i].version = parseContractVersion(res.Stack[i])
+	signers := []actor.SignerAccount{{
+		Signer: transaction.Signer{
+			Account: account.PrivateKey().GetScriptHash(),
+		},
+		Account: account,
+	}}
+	a, err := actor.New(c.(*rpcclient.Client), signers)
+	if err != nil {
+		return fmt.Errorf("can't get a list of NNS domains: %w", err)
+	}
+
+	arr, err := unwrap.Array(a.CallAndExpandIterator(nnsHash, "tokens", nnsMaxTokens))
+	if err != nil {
+		return fmt.Errorf("can't get a list of NNS domains: %w", err)
+	}
+
+	if !strings.HasPrefix(zone, ".") {
+		zone = "." + zone
+	}
+
+	var infos []contractDumpInfo
+	for i := range arr {
+		bs, err := arr[i].TryBytes()
+		if err != nil {
+			continue
 		}
-	}
 
-	tw := tabwriter.NewWriter(buf, 0, 2, 2, ' ', 0)
-	for _, info := range infos {
-		if info.version == "" {
-			info.version = "unknown"
+		if !bytes.HasSuffix(bs, []byte(zone)) {
+			continue
 		}
-		_, _ = tw.Write([]byte(fmt.Sprintf("%s\t(%s):\t%s\n",
-			info.name, info.version, info.hash.StringLE())))
-	}
-	_ = tw.Flush()
 
-	cmd.Print(buf.String())
+		h, err := nnsResolveHash(c, nnsHash, string(bs))
+		if err != nil {
+			continue
+		}
+
+		infos = append(infos, contractDumpInfo{
+			hash: h,
+			name: strings.TrimSuffix(string(bs), zone),
+		})
+	}
+
+	fillContractVersion(cmd, c, infos)
+	printContractInfo(cmd, infos)
 
 	return nil
 }
@@ -138,3 +177,60 @@ func parseContractVersion(item stackitem.Item) string {
 	patch := v % 1_000
 	return fmt.Sprintf("v%d.%d.%d", major, minor, patch)
 }
+
+func printContractInfo(cmd *cobra.Command, infos []contractDumpInfo) {
+	if len(infos) == 0 {
+		return
+	}
+
+	buf := bytes.NewBuffer(nil)
+	tw := tabwriter.NewWriter(buf, 0, 2, 2, ' ', 0)
+	for _, info := range infos {
+		if info.version == "" {
+			info.version = "unknown"
+		}
+		_, _ = tw.Write([]byte(fmt.Sprintf("%s\t(%s):\t%s\n",
+			info.name, info.version, info.hash.StringLE())))
+	}
+	_ = tw.Flush()
+
+	cmd.Print(buf.String())
+}
+
+func fillContractVersion(cmd *cobra.Command, c Client, infos []contractDumpInfo) {
+	bw := io.NewBufBinWriter()
+	sub := io.NewBufBinWriter()
+	for i := range infos {
+		if infos[i].hash.Equals(util.Uint160{}) {
+			emit.Int(bw.BinWriter, 0)
+		} else {
+			sub.Reset()
+			emit.AppCall(sub.BinWriter, infos[i].hash, "version", callflag.NoneFlag)
+			if sub.Err != nil {
+				panic(fmt.Errorf("BUG: can't create version script: %w", bw.Err))
+			}
+
+			script := sub.Bytes()
+			emit.Instruction(bw.BinWriter, opcode.TRY, []byte{byte(3 + len(script) + 2), 0})
+			bw.BinWriter.WriteBytes(script)
+			emit.Instruction(bw.BinWriter, opcode.ENDTRY, []byte{2 + 1})
+			emit.Opcodes(bw.BinWriter, opcode.PUSH0)
+		}
+	}
+	emit.Opcodes(bw.BinWriter, opcode.NOP) // for the last ENDTRY target
+	if bw.Err != nil {
+		panic(fmt.Errorf("BUG: can't create version script: %w", bw.Err))
+	}
+
+	res, err := c.InvokeScript(bw.Bytes(), nil)
+	if err != nil {
+		cmd.Printf("Can't fetch version from NNS: %v\n", err)
+		return
+	}
+
+	if res.State == vmstate.Halt.String() {
+		for i := range res.Stack {
+			infos[i].version = parseContractVersion(res.Stack[i])
+		}
+	}
+}
diff --git a/cmd/neofs-adm/internal/modules/morph/root.go b/cmd/neofs-adm/internal/modules/morph/root.go
index dd1dec03f..3a354123d 100644
--- a/cmd/neofs-adm/internal/modules/morph/root.go
+++ b/cmd/neofs-adm/internal/modules/morph/root.go
@@ -258,6 +258,7 @@ func init() {
 
 	RootCmd.AddCommand(dumpContractHashesCmd)
 	dumpContractHashesCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
+	dumpContractHashesCmd.Flags().String(customZoneFlag, "", "Custom zone to search.")
 
 	RootCmd.AddCommand(dumpNetworkConfigCmd)
 	dumpNetworkConfigCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")