diff --git a/rpc/nns/hashes.go b/rpc/nns/hashes.go index affcff2d..41ac44c7 100644 --- a/rpc/nns/hashes.go +++ b/rpc/nns/hashes.go @@ -1,7 +1,10 @@ package nns import ( + "errors" + "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/util" ) @@ -10,6 +13,11 @@ import ( // an ID of 1. const ID = 1 +// ContractTLD is the default TLD for NeoFS contracts. It's a convention that +// is not likely to be used by any non-NeoFS networks, but for NeoFS ones it +// allows to find contract hashes more easily. +const ContractTLD = "neofs" + // ContractStateGetter is the interface required for contract state resolution // using a known contract ID. type ContractStateGetter interface { @@ -27,3 +35,28 @@ func InferHash(sg ContractStateGetter) (util.Uint160, error) { return c.Hash, nil } + +// ResolveFSContract is a convenience method that doesn't exist in the NNS +// contract itself (it doesn't care which data is stored there). It assumes +// that contracts follow the [ContractTLD] convention, gets simple contract +// names (like "container" or "netmap") and extracts the hash for the +// respective NNS record in any of the formats (of which historically there's +// been a few). +func (c *ContractReader) ResolveFSContract(name string) (util.Uint160, error) { + strs, err := c.Resolve(name+"."+ContractTLD, TXT) + if err != nil { + return util.Uint160{}, err + } + for i := range strs { + h, err := util.Uint160DecodeStringLE(strs[i]) + if err == nil { + return h, nil + } + + h, err = address.StringToUint160(strs[i]) + if err == nil { + return h, nil + } + } + return util.Uint160{}, errors.New("no valid hashes are found") +} diff --git a/rpc/nns/hashes_test.go b/rpc/nns/hashes_test.go index a7d87017..cac26970 100644 --- a/rpc/nns/hashes_test.go +++ b/rpc/nns/hashes_test.go @@ -4,8 +4,12 @@ import ( "errors" "testing" + "github.com/google/uuid" "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/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" ) @@ -35,3 +39,77 @@ func TestInferHash(t *testing.T) { require.NoError(t, err) require.Equal(t, util.Uint160{0x01, 0x02, 0x03}, h) } + +type testInv struct { + err error + res *result.Invoke +} + +func (t *testInv) Call(contract util.Uint160, operation string, params ...any) (*result.Invoke, error) { + return t.res, t.err +} + +func (t *testInv) CallAndExpandIterator(contract util.Uint160, operation string, i int, params ...any) (*result.Invoke, error) { + return t.res, t.err +} +func (t *testInv) TraverseIterator(uuid.UUID, *result.Iterator, int) ([]stackitem.Item, error) { + return nil, nil +} +func (t *testInv) TerminateSession(uuid.UUID) error { + return nil +} + +func TestBaseErrors(t *testing.T) { + ti := new(testInv) + r := NewReader(ti, util.Uint160{1, 2, 3}) + + ti.err = errors.New("bad") + _, err := r.ResolveFSContract("blah") + require.Error(t, err) + + ti.err = nil + ti.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{}), + }, + } + _, err = r.ResolveFSContract("blah") + require.Error(t, err) + + ti.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{ + stackitem.Make(100500), + }), + }, + } + _, err = r.ResolveFSContract("blah") + require.Error(t, err) + + h := util.Uint160{1, 2, 3, 4, 5} + ti.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{ + stackitem.Make(h.StringLE()), + }), + }, + } + res, err := r.ResolveFSContract("blah") + require.NoError(t, err) + require.Equal(t, h, res) + + ti.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{ + stackitem.Make(address.Uint160ToString(h)), + }), + }, + } + res, err = r.ResolveFSContract("blah") + require.NoError(t, err) + require.Equal(t, h, res) +}