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) + }) +}