Merge pull request #2666 from nspcc-dev/nns-partial-wrapper

rpcclient: add enough of NNS into nns to deprecate NNS methods
This commit is contained in:
Roman Khimov 2022-08-29 13:05:56 +03:00 committed by GitHub
commit 840d755baa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 428 additions and 27 deletions

View file

@ -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 {

View file

@ -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
}

View file

@ -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])
}

View file

@ -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
}

View file

@ -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"),
})))
}

View file

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