diff --git a/pkg/rpcclient/native.go b/pkg/rpcclient/native.go index 0b6526c44..6fa335d3a 100644 --- a/pkg/rpcclient/native.go +++ b/pkg/rpcclient/native.go @@ -31,6 +31,8 @@ func (c *Client) GetOraclePrice() (int64, error) { } // GetNNSPrice invokes `getPrice` method on a NeoNameService contract with the specified hash. +// +// Deprecated: please use nns subpackage. This method will be removed in future versions. func (c *Client) GetNNSPrice(nnsHash util.Uint160) (int64, error) { return c.invokeNativeGetMethod(nnsHash, "getPrice") } @@ -84,6 +86,8 @@ func (c *Client) GetDesignatedByRole(role noderoles.Role, index uint32) (keys.Pu } // NNSResolve invokes `resolve` method on a NameService contract with the specified hash. +// +// Deprecated: please use nns subpackage. This method will be removed in future versions. func (c *Client) NNSResolve(nnsHash util.Uint160, name string, typ nns.RecordType) (string, error) { if typ == nns.CNAME { return "", errors.New("can't resolve CNAME record type") @@ -92,6 +96,8 @@ func (c *Client) NNSResolve(nnsHash util.Uint160, name string, typ nns.RecordTyp } // NNSIsAvailable invokes `isAvailable` method on a NeoNameService contract with the specified hash. +// +// Deprecated: please use nns subpackage. This method will be removed in future versions. func (c *Client) NNSIsAvailable(nnsHash util.Uint160, name string) (bool, error) { return unwrap.Bool(c.reader.Call(nnsHash, "isAvailable", name)) } @@ -101,6 +107,8 @@ func (c *Client) NNSIsAvailable(nnsHash util.Uint160, name string) (bool, error) // third one is an error. Use TraverseIterator method to traverse iterator values or // TerminateSession to terminate opened iterator session. See TraverseIterator and // TerminateSession documentation for more details. +// +// Deprecated: please use nns subpackage. This method will be removed in future versions. func (c *Client) NNSGetAllRecords(nnsHash util.Uint160, name string) (uuid.UUID, result.Iterator, error) { return unwrap.SessionIterator(c.reader.Call(nnsHash, "getAllRecords", name)) } @@ -109,6 +117,8 @@ func (c *Client) NNSGetAllRecords(nnsHash util.Uint160, name string) (uuid.UUID, // (config.DefaultMaxIteratorResultItems at max). It differs from NNSGetAllRecords in // that no iterator session is used to retrieve values from iterator. Instead, unpacking // VM script is created and invoked via `invokescript` JSON-RPC call. +// +// Deprecated: please use nns subpackage. This method will be removed in future versions. func (c *Client) NNSUnpackedGetAllRecords(nnsHash util.Uint160, name string) ([]nns.RecordState, error) { arr, err := unwrap.Array(c.reader.CallAndExpandIterator(nnsHash, "getAllRecords", config.DefaultMaxIteratorResultItems, name)) if err != nil { diff --git a/pkg/rpcclient/nns/contract.go b/pkg/rpcclient/nns/contract.go new file mode 100644 index 000000000..b358a818c --- /dev/null +++ b/pkg/rpcclient/nns/contract.go @@ -0,0 +1,122 @@ +/* +Package nns provide some RPC wrappers for the non-native NNS contract. + +It's not yet a complete interface because there are different NNS versions +available, yet it provides the most widely used ones that were available from +the old RPC client API. +*/ +package nns + +import ( + "fmt" + + "github.com/google/uuid" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" +) + +// Invoker is used by ContractReader to call various methods. +type Invoker interface { + Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error) + CallAndExpandIterator(contract util.Uint160, method string, maxItems int, params ...interface{}) (*result.Invoke, error) + TerminateSession(sessionID uuid.UUID) error + TraverseIterator(sessionID uuid.UUID, iterator *result.Iterator, num int) ([]stackitem.Item, error) +} + +// ContractReader provides an interface to call read-only NNS contract methods. +type ContractReader struct { + invoker Invoker + hash util.Uint160 +} + +// RecordIterator is used for iterating over GetAllRecords results. +type RecordIterator struct { + client Invoker + session uuid.UUID + iterator result.Iterator +} + +// NewReader creates an instance of ContractReader that can be used to read +// data from the contract. +func NewReader(invoker Invoker, hash util.Uint160) *ContractReader { + return &ContractReader{invoker, hash} +} + +// GetPrice returns current domain registration price in GAS. +func (c *ContractReader) GetPrice() (int64, error) { + return unwrap.Int64(c.invoker.Call(c.hash, "getPrice")) +} + +// IsAvailable checks whether the domain given is available for registration. +func (c *ContractReader) IsAvailable(name string) (bool, error) { + return unwrap.Bool(c.invoker.Call(c.hash, "isAvailable", name)) +} + +// Resolve resolves the given record type for the given domain (with no more +// than three redirects). +func (c *ContractReader) Resolve(name string, typ RecordType) (string, error) { + return unwrap.UTF8String(c.invoker.Call(c.hash, "resolve", name, int64(typ))) +} + +// GetAllRecords returns an iterator that allows to retrieve all RecordState +// items for the given domain name. It depends on the server to provide proper +// session-based iterator, but can also work with expanded one. +func (c *ContractReader) GetAllRecords(name string) (*RecordIterator, error) { + sess, iter, err := unwrap.SessionIterator(c.invoker.Call(c.hash, "getAllRecords", name)) + if err != nil { + return nil, err + } + + return &RecordIterator{ + client: c.invoker, + iterator: iter, + session: sess, + }, nil +} + +// Next returns the next set of elements from the iterator (up to num of them). +// It can return less than num elements in case iterator doesn't have that many +// or zero elements if the iterator has no more elements or the session is +// expired. +func (r *RecordIterator) Next(num int) ([]RecordState, error) { + items, err := r.client.TraverseIterator(r.session, &r.iterator, num) + if err != nil { + return nil, err + } + return itemsToRecords(items) +} + +// Terminate closes the iterator session used by RecordIterator (if it's +// session-based). +func (r *RecordIterator) Terminate() error { + if r.iterator.ID == nil { + return nil + } + return r.client.TerminateSession(r.session) +} + +// GetAllRecordsExpanded is similar to GetAllRecords (uses the same NNS +// method), but can be useful if the server used doesn't support sessions and +// doesn't expand iterators. It creates a script that will get num of result +// items from the iterator right in the VM and return them to you. It's only +// limited by VM stack and GAS available for RPC invocations. +func (c *ContractReader) GetAllRecordsExpanded(name string, num int) ([]RecordState, error) { + arr, err := unwrap.Array(c.invoker.CallAndExpandIterator(c.hash, "getAllRecords", num, name)) + if err != nil { + return nil, err + } + return itemsToRecords(arr) +} + +func itemsToRecords(arr []stackitem.Item) ([]RecordState, error) { + res := make([]RecordState, len(arr)) + for i := range arr { + err := res[i].FromStackItem(arr[i]) + if err != nil { + return nil, fmt.Errorf("item #%d: %w", i, err) + } + } + return res, nil +} diff --git a/pkg/rpcclient/nns/contract_test.go b/pkg/rpcclient/nns/contract_test.go new file mode 100644 index 000000000..f69079e68 --- /dev/null +++ b/pkg/rpcclient/nns/contract_test.go @@ -0,0 +1,197 @@ +package nns + +import ( + "errors" + "testing" + + "github.com/google/uuid" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/stretchr/testify/require" +) + +type testAct struct { + err error + res *result.Invoke +} + +func (t *testAct) Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error) { + return t.res, t.err +} +func (t *testAct) CallAndExpandIterator(contract util.Uint160, method string, maxItems int, params ...interface{}) (*result.Invoke, error) { + return t.res, t.err +} +func (t *testAct) TerminateSession(sessionID uuid.UUID) error { + return t.err +} +func (t *testAct) TraverseIterator(sessionID uuid.UUID, iterator *result.Iterator, num int) ([]stackitem.Item, error) { + return t.res.Stack, t.err +} + +func TestSimpleGetters(t *testing.T) { + ta := &testAct{} + nns := NewReader(ta, util.Uint160{1, 2, 3}) + + ta.err = errors.New("") + _, err := nns.GetPrice() + require.Error(t, err) + _, err = nns.IsAvailable("nspcc.neo") + require.Error(t, err) + _, err = nns.Resolve("nspcc.neo", A) + require.Error(t, err) + + ta.err = nil + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make(100500), + }, + } + price, err := nns.GetPrice() + require.NoError(t, err) + require.Equal(t, int64(100500), price) + + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make(true), + }, + } + ava, err := nns.IsAvailable("nspcc.neo") + require.NoError(t, err) + require.Equal(t, true, ava) + + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make("some text"), + }, + } + txt, err := nns.Resolve("nspcc.neo", TXT) + require.NoError(t, err) + require.Equal(t, "some text", txt) +} + +func TestGetAllRecords(t *testing.T) { + ta := &testAct{} + nns := NewReader(ta, util.Uint160{1, 2, 3}) + + ta.err = errors.New("") + _, err := nns.GetAllRecords("nspcc.neo") + require.Error(t, err) + + ta.err = nil + iid := uuid.New() + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.NewInterop(result.Iterator{ + ID: &iid, + }), + }, + } + _, err = nns.GetAllRecords("nspcc.neo") + require.Error(t, err) + + // Session-based iterator. + sid := uuid.New() + ta.res = &result.Invoke{ + Session: sid, + State: "HALT", + Stack: []stackitem.Item{ + stackitem.NewInterop(result.Iterator{ + ID: &iid, + }), + }, + } + iter, err := nns.GetAllRecords("nspcc.neo") + require.NoError(t, err) + + require.NoError(t, err) + ta.res = &result.Invoke{ + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{ + stackitem.Make("n3"), + stackitem.Make(16), + stackitem.Make("cool"), + }), + }, + } + vals, err := iter.Next(10) + require.NoError(t, err) + require.Equal(t, 1, len(vals)) + require.Equal(t, RecordState{ + Name: "n3", + Type: TXT, + Data: "cool", + }, vals[0]) + + ta.err = errors.New("") + _, err = iter.Next(1) + require.Error(t, err) + + err = iter.Terminate() + require.Error(t, err) + + // Value-based iterator. + ta.err = nil + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.NewInterop(result.Iterator{ + Values: []stackitem.Item{ + stackitem.Make("n3"), + stackitem.Make(16), + stackitem.Make("cool"), + }, + }), + }, + } + iter, err = nns.GetAllRecords("nspcc.neo") + require.NoError(t, err) + + ta.err = errors.New("") + err = iter.Terminate() + require.NoError(t, err) +} + +func TestGetAllRecordsExpanded(t *testing.T) { + ta := &testAct{} + nns := NewReader(ta, util.Uint160{1, 2, 3}) + + ta.err = errors.New("") + _, err := nns.GetAllRecordsExpanded("nspcc.neo", 8) + require.Error(t, err) + + ta.err = nil + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make(42), + }, + } + _, err = nns.GetAllRecordsExpanded("nspcc.neo", 8) + require.Error(t, err) + + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{ + stackitem.Make([]stackitem.Item{ + stackitem.Make("n3"), + stackitem.Make(16), + stackitem.Make("cool"), + }), + }), + }, + } + vals, err := nns.GetAllRecordsExpanded("nspcc.neo", 8) + require.NoError(t, err) + require.Equal(t, 1, len(vals)) + require.Equal(t, RecordState{ + Name: "n3", + Type: TXT, + Data: "cool", + }, vals[0]) +} diff --git a/pkg/rpcclient/nns/record.go b/pkg/rpcclient/nns/record.go index e276b4836..da017f9ab 100644 --- a/pkg/rpcclient/nns/record.go +++ b/pkg/rpcclient/nns/record.go @@ -1,5 +1,12 @@ package nns +import ( + "errors" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" +) + // RecordState is a type that registered entities are saved as. type RecordState struct { Name string @@ -25,3 +32,35 @@ const ( // AAAA represents IPv6 address record type. AAAA RecordType = 28 ) + +// FromStackItem fills RecordState with data from the given stack item if it can +// be correctly converted to RecordState. +func (r *RecordState) FromStackItem(itm stackitem.Item) error { + rs, ok := itm.Value().([]stackitem.Item) + if !ok { + return errors.New("not a struct") + } + if len(rs) != 3 { + return errors.New("wrong number of elements") + } + name, err := rs[0].TryBytes() + if err != nil { + return fmt.Errorf("bad name: %w", err) + } + typ, err := rs[1].TryInteger() + if err != nil { + return fmt.Errorf("bad type: %w", err) + } + data, err := rs[2].TryBytes() + if err != nil { + return fmt.Errorf("bad data: %w", err) + } + u64Typ := typ.Uint64() + if !typ.IsUint64() || u64Typ > 255 { + return errors.New("bad type") + } + r.Name = string(name) + r.Type = RecordType(u64Typ) + r.Data = string(data) + return nil +} diff --git a/pkg/rpcclient/nns/record_test.go b/pkg/rpcclient/nns/record_test.go new file mode 100644 index 000000000..865454ff5 --- /dev/null +++ b/pkg/rpcclient/nns/record_test.go @@ -0,0 +1,39 @@ +package nns + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/stretchr/testify/require" +) + +func TestRecordStateFromStackItem(t *testing.T) { + r := &RecordState{} + require.Error(t, r.FromStackItem(stackitem.Make(42))) + require.Error(t, r.FromStackItem(stackitem.Make([]stackitem.Item{}))) + require.Error(t, r.FromStackItem(stackitem.Make([]stackitem.Item{ + stackitem.Make([]stackitem.Item{}), + stackitem.Make(16), + stackitem.Make("cool"), + }))) + require.Error(t, r.FromStackItem(stackitem.Make([]stackitem.Item{ + stackitem.Make("n3"), + stackitem.Make([]stackitem.Item{}), + stackitem.Make("cool"), + }))) + require.Error(t, r.FromStackItem(stackitem.Make([]stackitem.Item{ + stackitem.Make("n3"), + stackitem.Make(16), + stackitem.Make([]stackitem.Item{}), + }))) + require.Error(t, r.FromStackItem(stackitem.Make([]stackitem.Item{ + stackitem.Make("n3"), + stackitem.Make(100500), + stackitem.Make("cool"), + }))) + require.NoError(t, r.FromStackItem(stackitem.Make([]stackitem.Item{ + stackitem.Make("n3"), + stackitem.Make(16), + stackitem.Make("cool"), + }))) +} diff --git a/pkg/services/rpcsrv/client_test.go b/pkg/services/rpcsrv/client_test.go index c977681e7..0ae239ac9 100644 --- a/pkg/services/rpcsrv/client_test.go +++ b/pkg/services/rpcsrv/client_test.go @@ -1402,51 +1402,45 @@ func TestClient_NNS(t *testing.T) { c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) require.NoError(t, c.Init()) + nnc := nns.NewReader(invoker.New(c, nil), nnsHash) - t.Run("NNSIsAvailable, false", func(t *testing.T) { - b, err := c.NNSIsAvailable(nnsHash, "neo.com") + t.Run("IsAvailable, false", func(t *testing.T) { + b, err := nnc.IsAvailable("neo.com") require.NoError(t, err) require.Equal(t, false, b) }) - t.Run("NNSIsAvailable, true", func(t *testing.T) { - b, err := c.NNSIsAvailable(nnsHash, "neogo.com") + t.Run("IsAvailable, true", func(t *testing.T) { + b, err := nnc.IsAvailable("neogo.com") require.NoError(t, err) require.Equal(t, true, b) }) - t.Run("NNSResolve, good", func(t *testing.T) { - b, err := c.NNSResolve(nnsHash, "neo.com", nns.A) + t.Run("Resolve, good", func(t *testing.T) { + b, err := nnc.Resolve("neo.com", nns.A) require.NoError(t, err) require.Equal(t, "1.2.3.4", b) }) - t.Run("NNSResolve, bad", func(t *testing.T) { - _, err := c.NNSResolve(nnsHash, "neogo.com", nns.A) + t.Run("Resolve, bad", func(t *testing.T) { + _, err := nnc.Resolve("neogo.com", nns.A) require.Error(t, err) }) - t.Run("NNSResolve, forbidden", func(t *testing.T) { - _, err := c.NNSResolve(nnsHash, "neogo.com", nns.CNAME) + t.Run("Resolve, CNAME", func(t *testing.T) { + _, err := nnc.Resolve("neogo.com", nns.CNAME) require.Error(t, err) }) - t.Run("NNSGetAllRecords, good", func(t *testing.T) { - sess, iter, err := c.NNSGetAllRecords(nnsHash, "neo.com") + t.Run("GetAllRecords, good", func(t *testing.T) { + iter, err := nnc.GetAllRecords("neo.com") require.NoError(t, err) - arr, err := c.TraverseIterator(sess, *iter.ID, config.DefaultMaxIteratorResultItems) + arr, err := iter.Next(config.DefaultMaxIteratorResultItems) require.NoError(t, err) require.Equal(t, 1, len(arr)) - rs := arr[0].Value().([]stackitem.Item) - require.Equal(t, 3, len(rs)) - actual := nns.RecordState{ - Name: string(rs[0].Value().([]byte)), - Type: nns.RecordType(rs[1].Value().(*big.Int).Int64()), - Data: string(rs[2].Value().([]byte)), - } require.Equal(t, nns.RecordState{ Name: "neo.com", Type: nns.A, Data: "1.2.3.4", - }, actual) + }, arr[0]) }) - t.Run("NNSUnpackedGetAllRecords, good", func(t *testing.T) { - rss, err := c.NNSUnpackedGetAllRecords(nnsHash, "neo.com") + t.Run("GetAllRecordsExpanded, good", func(t *testing.T) { + rss, err := nnc.GetAllRecordsExpanded("neo.com", 42) require.NoError(t, err) require.Equal(t, []nns.RecordState{ { @@ -1456,12 +1450,12 @@ func TestClient_NNS(t *testing.T) { }, }, rss) }) - t.Run("NNSGetAllRecords, bad", func(t *testing.T) { - _, _, err := c.NNSGetAllRecords(nnsHash, "neopython.com") + t.Run("GetAllRecords, bad", func(t *testing.T) { + _, err := nnc.GetAllRecords("neopython.com") require.Error(t, err) }) - t.Run("NNSUnpackedGetAllRecords, bad", func(t *testing.T) { - _, err := c.NNSUnpackedGetAllRecords(nnsHash, "neopython.com") + t.Run("GetAllRecordsExpanded, bad", func(t *testing.T) { + _, err := nnc.GetAllRecordsExpanded("neopython.com", 7) require.Error(t, err) }) }