From b9afe7a2f95245ca981ce6e63c0523aa9ddafb05 Mon Sep 17 00:00:00 2001
From: Pavel Pogodaev
Date: Mon, 10 Jul 2023 09:56:37 +0300
Subject: [PATCH] [#42] Add ResolveContractHash method
Signed-off-by: Pavel Pogodaev
---
ns/nns.go | 44 ++++++++++++++++++++++++++
ns/nns_test.go | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 127 insertions(+)
diff --git a/ns/nns.go b/ns/nns.go
index cb8dbd4..4af72f4 100644
--- a/ns/nns.go
+++ b/ns/nns.go
@@ -10,6 +10,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
"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/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
@@ -116,3 +117,46 @@ func (n *NNS) ResolveContainerDomain(domain container.Domain) (cid.ID, error) {
return cid.ID{}, errNotFound
}
+
+// ResolveContractHash looks up for NNS TXT records for the given container domain
+// by calling `resolve` method of NNS contract. Returns the first record which represents
+// valid contract hash 20 bytes long unsigned integer. Otherwise, returns an error.
+//
+// ResolveContractHash MUST NOT be called before successful Dial.
+//
+// See also https://docs.neo.org/docs/en-us/reference/nns.html.
+func (n *NNS) ResolveContractHash(domain container.Domain) (util.Uint160, error) {
+ item, err := unwrap.Item(n.invoker.Call(n.nnsContract, "resolve",
+ domain.Name()+"."+domain.Zone(), int64(nns.TXT),
+ ))
+ if err != nil {
+ return util.Uint160{}, fmt.Errorf("contract invocation: %w", err)
+ }
+
+ if _, ok := item.(stackitem.Null); !ok {
+ arr, ok := item.Value().([]stackitem.Item)
+ if !ok {
+ // unexpected for types from stackitem package
+ return util.Uint160{}, errors.New("invalid cast to stack item slice")
+ }
+
+ for i := range arr {
+ recordValue, err := arr[i].TryBytes()
+ if err != nil {
+ return util.Uint160{}, fmt.Errorf("convert array item to byte slice: %w", err)
+ }
+
+ strRecordValue := string(recordValue)
+ scriptHash, err := address.StringToUint160(strRecordValue)
+ if err == nil {
+ return scriptHash, nil
+ }
+ scriptHash, err = util.Uint160DecodeStringLE(strRecordValue)
+ if err == nil {
+ return scriptHash, nil
+ }
+ }
+ }
+
+ return util.Uint160{}, errNotFound
+}
diff --git a/ns/nns_test.go b/ns/nns_test.go
index 3180ac4..9970b88 100644
--- a/ns/nns_test.go
+++ b/ns/nns_test.go
@@ -154,3 +154,86 @@ func TestNNS_ResolveContainerDomain(t *testing.T) {
require.Equal(t, id, res)
})
}
+
+func TestNNS_ResolveContractHash(t *testing.T) {
+ var testContainerDomain container.Domain
+ testContainerDomain.SetName("some_container")
+
+ var nnsContract util.Uint160
+
+ rand.Read(nnsContract[:])
+
+ testC := &testNeoClient{
+ t: t,
+ expectedContract: nnsContract,
+ }
+
+ n := NNS{
+ nnsContract: nnsContract,
+ invoker: testC,
+ }
+
+ t.Run("invocation failure", func(t *testing.T) {
+ err1 := errors.New("invoke err")
+ testC.err = err1
+
+ _, err2 := n.ResolveContractHash(testContainerDomain)
+ require.ErrorIs(t, err2, err1)
+ })
+
+ testC.err = nil
+
+ t.Run("fault exception", func(t *testing.T) {
+ _, err := n.ResolveContractHash(testContainerDomain)
+ require.Error(t, err)
+ })
+
+ testC.res.State = vmstate.Halt.String()
+
+ t.Run("empty stack", func(t *testing.T) {
+ _, err := n.ResolveContractHash(testContainerDomain)
+ require.Error(t, err)
+ })
+
+ testC.res.Stack = make([]stackitem.Item, 1)
+
+ t.Run("non-array last stack item", func(t *testing.T) {
+ testC.res.Stack[0] = stackitem.NewBigInteger(big.NewInt(11))
+
+ _, err := n.ResolveContractHash(testContainerDomain)
+ require.Error(t, err)
+ })
+
+ t.Run("null array", func(t *testing.T) {
+ testC.res.Stack[0] = stackitem.Null{}
+
+ _, err := n.ResolveContractHash(testContainerDomain)
+ require.ErrorIs(t, err, errNotFound)
+ })
+
+ t.Run("array stack item with non-slice value", func(t *testing.T) {
+ testC.res.Stack[0] = brokenArrayStackItem{}
+
+ _, err := n.ResolveContractHash(testContainerDomain)
+ require.Error(t, err)
+ })
+
+ arr := make([]stackitem.Item, 2)
+ testC.res.Stack[0] = stackitem.NewArray(arr)
+
+ t.Run("non-bytes array element", func(t *testing.T) {
+ arr[0] = stackitem.NewArray(nil)
+
+ _, err := n.ResolveContractHash(testContainerDomain)
+ require.Error(t, err)
+ })
+
+ arr[0] = stackitem.NewByteArray([]byte("some byte array 1"))
+
+ t.Run("non-container array elements", func(t *testing.T) {
+ arr[1] = stackitem.NewByteArray([]byte("some byte array 2"))
+
+ _, err := n.ResolveContractHash(testContainerDomain)
+ require.Error(t, err)
+ })
+}