diff --git a/pkg/rpcclient/nns/contract.go b/pkg/rpcclient/nns/contract.go index f51294be7..f73d5b0d3 100644 --- a/pkg/rpcclient/nns/contract.go +++ b/pkg/rpcclient/nns/contract.go @@ -1,29 +1,59 @@ -/* -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 provide RPC wrappers for the non-native NNS contract. +// This is Neo N3 NNS contract wrapper, the source code of the contract can be found here: +// https://github.com/neo-project/non-native-contracts/blob/8d72b92e5e5705d763232bcc24784ced0fb8fc87/src/NameService/NameService.cs package nns import ( + "errors" "fmt" + "math/big" + "unicode/utf8" - "github.com/google/uuid" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11" "github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" "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. +// MaxNameLength is the max length of domain name. +const MaxNameLength = 255 + +// SetAdminEvent represents "SetAdmin" event emitted by the contract. +type SetAdminEvent struct { + Name string + OldAdmin util.Uint160 + NewAdmin util.Uint160 +} + +// RenewEvent represents "Renew" event emitted by the contract. +type RenewEvent struct { + Name string + OldExpiration *big.Int + NewExpiration *big.Int +} + +// Invoker is used by ContractReader to call various safe methods. type Invoker interface { nep11.Invoker } -// ContractReader provides an interface to call read-only NNS contract methods. +// Actor is used by Contract to call state-changing methods. +type Actor interface { + Invoker + nep11.Actor + + MakeCall(contract util.Uint160, method string, params ...any) (*transaction.Transaction, error) + MakeRun(script []byte) (*transaction.Transaction, error) + MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...any) (*transaction.Transaction, error) + MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) + SendCall(contract util.Uint160, method string, params ...any) (util.Uint256, uint32, error) + SendRun(script []byte) (util.Uint256, uint32, error) +} + +// ContractReader implements safe contract methods. type ContractReader struct { nep11.NonDivisibleReader @@ -31,38 +61,70 @@ type ContractReader struct { hash util.Uint160 } -// RecordIterator is used for iterating over GetAllRecords results. -type RecordIterator struct { - client Invoker - session uuid.UUID - iterator result.Iterator +// Contract provides full NeoNameService interface, both safe and state-changing methods. +type Contract struct { + ContractReader + nep11.BaseWriter + + actor Actor + hash util.Uint160 } -// NewReader creates an instance of ContractReader that can be used to read -// data from the contract. +// NewReader creates an instance of ContractReader using provided contract hash and the given Invoker. func NewReader(invoker Invoker, hash util.Uint160) *ContractReader { return &ContractReader{*nep11.NewNonDivisibleReader(invoker, hash), 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")) +// New creates an instance of Contract using provided contract hash and the given Actor. +func New(actor Actor, hash util.Uint160) *Contract { + var nep11ndt = nep11.NewNonDivisible(actor, hash) + return &Contract{ContractReader{nep11ndt.NonDivisibleReader, actor, hash}, nep11ndt.BaseWriter, actor, hash} } -// IsAvailable checks whether the domain given is available for registration. +// Roots invokes `roots` method of contract. +func (c *ContractReader) Roots() (*RootIterator, error) { + sess, iter, err := unwrap.SessionIterator(c.invoker.Call(c.hash, "roots")) + if err != nil { + return nil, err + } + + return &RootIterator{ + client: c.invoker, + iterator: iter, + session: sess, + }, nil +} + +// RootsExpanded is similar to Roots (uses the same contract +// 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 the specified +// number 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) RootsExpanded(_numOfIteratorItems int) ([]string, error) { + arr, err := unwrap.Array(c.invoker.CallAndExpandIterator(c.hash, "roots", _numOfIteratorItems)) + if err != nil { + return nil, err + } + + return itemsToRoots(arr) +} + +// GetPrice invokes `getPrice` method of contract. +func (c *ContractReader) GetPrice(length uint8) (*big.Int, error) { + return unwrap.BigInt(c.invoker.Call(c.hash, "getPrice", length)) +} + +// IsAvailable invokes `isAvailable` method of contract. 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))) +// GetRecord invokes `getRecord` method of contract. +func (c *ContractReader) GetRecord(name string, typev RecordType) (string, error) { + return unwrap.UTF8String(c.invoker.Call(c.hash, "getRecord", name, typev)) } -// 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. +// GetAllRecords invokes `getAllRecords` method of contract. func (c *ContractReader) GetAllRecords(name string) (*RecordIterator, error) { sess, iter, err := unwrap.SessionIterator(c.invoker.Call(c.hash, "getAllRecords", name)) if err != nil { @@ -76,47 +138,412 @@ func (c *ContractReader) GetAllRecords(name string) (*RecordIterator, error) { }, 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 +// GetAllRecordsExpanded is similar to GetAllRecords (uses the same contract // 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)) +// doesn't expand iterators. It creates a script that will get the specified +// number 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, _numOfIteratorItems int) ([]RecordState, error) { + arr, err := unwrap.Array(c.invoker.CallAndExpandIterator(c.hash, "getAllRecords", _numOfIteratorItems, 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) +// Resolve invokes `resolve` method of contract. +func (c *ContractReader) Resolve(name string, typev RecordType) (string, error) { + return unwrap.UTF8String(c.invoker.Call(c.hash, "resolve", name, int64(typev))) +} + +// Update creates a transaction invoking `update` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) Update(nef []byte, manifest string) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "update", nef, manifest) +} + +// UpdateTransaction creates a transaction invoking `update` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) UpdateTransaction(nef []byte, manifest string) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "update", nef, manifest) +} + +// UpdateUnsigned creates a transaction invoking `update` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) UpdateUnsigned(nef []byte, manifest string) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "update", nil, nef, manifest) +} + +// AddRoot creates a transaction invoking `addRoot` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) AddRoot(root string) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "addRoot", root) +} + +// AddRootTransaction creates a transaction invoking `addRoot` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) AddRootTransaction(root string) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "addRoot", root) +} + +// AddRootUnsigned creates a transaction invoking `addRoot` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) AddRootUnsigned(root string) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "addRoot", nil, root) +} + +// SetPrice creates a transaction invoking `setPrice` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) SetPrice(priceList []int64) (util.Uint256, uint32, error) { + anyPriceList := make([]any, len(priceList)) + for i, price := range priceList { + anyPriceList[i] = price + } + return c.actor.SendCall(c.hash, "setPrice", anyPriceList) +} + +// SetPriceTransaction creates a transaction invoking `setPrice` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) SetPriceTransaction(priceList []int64) (*transaction.Transaction, error) { + anyPriceList := make([]any, len(priceList)) + for i, price := range priceList { + anyPriceList[i] = price + } + return c.actor.MakeCall(c.hash, "setPrice", anyPriceList) +} + +// SetPriceUnsigned creates a transaction invoking `setPrice` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) SetPriceUnsigned(priceList []int64) (*transaction.Transaction, error) { + anyPriceList := make([]any, len(priceList)) + for i, price := range priceList { + anyPriceList[i] = price + } + return c.actor.MakeUnsignedCall(c.hash, "setPrice", nil, anyPriceList) +} + +func (c *Contract) scriptForRegister(name string, owner util.Uint160) ([]byte, error) { + return smartcontract.CreateCallWithAssertScript(c.hash, "register", name, owner) +} + +// Register creates a transaction invoking `register` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) Register(name string, owner util.Uint160) (util.Uint256, uint32, error) { + script, err := c.scriptForRegister(name, owner) + if err != nil { + return util.Uint256{}, 0, err + } + return c.actor.SendRun(script) +} + +// RegisterTransaction creates a transaction invoking `register` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) RegisterTransaction(name string, owner util.Uint160) (*transaction.Transaction, error) { + script, err := c.scriptForRegister(name, owner) + if err != nil { + return nil, err + } + return c.actor.MakeRun(script) +} + +// RegisterUnsigned creates a transaction invoking `register` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) RegisterUnsigned(name string, owner util.Uint160) (*transaction.Transaction, error) { + script, err := c.scriptForRegister(name, owner) + if err != nil { + return nil, err + } + return c.actor.MakeUnsignedRun(script, nil) +} + +// Renew creates a transaction invoking `renew` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) Renew(name string) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "renew", name) +} + +// RenewTransaction creates a transaction invoking `renew` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) RenewTransaction(name string) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "renew", name) +} + +// RenewUnsigned creates a transaction invoking `renew` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) RenewUnsigned(name string) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "renew", nil, name) +} + +// Renew2 creates a transaction invoking `renew` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) Renew2(name string, years int64) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "renew", name, years) +} + +// Renew2Transaction creates a transaction invoking `renew` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) Renew2Transaction(name string, years int64) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "renew", name, years) +} + +// Renew2Unsigned creates a transaction invoking `renew` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) Renew2Unsigned(name string, years int64) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "renew", nil, name, years) +} + +// SetAdmin creates a transaction invoking `setAdmin` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) SetAdmin(name string, admin util.Uint160) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "setAdmin", name, admin) +} + +// SetAdminTransaction creates a transaction invoking `setAdmin` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) SetAdminTransaction(name string, admin util.Uint160) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "setAdmin", name, admin) +} + +// SetAdminUnsigned creates a transaction invoking `setAdmin` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) SetAdminUnsigned(name string, admin util.Uint160) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "setAdmin", nil, name, admin) +} + +// SetRecord creates a transaction invoking `setRecord` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) SetRecord(name string, typev RecordType, data string) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "setRecord", name, typev, data) +} + +// SetRecordTransaction creates a transaction invoking `setRecord` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) SetRecordTransaction(name string, typev RecordType, data string) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "setRecord", name, typev, data) +} + +// SetRecordUnsigned creates a transaction invoking `setRecord` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) SetRecordUnsigned(name string, typev RecordType, data string) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "setRecord", nil, name, typev, data) +} + +// DeleteRecord creates a transaction invoking `deleteRecord` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) DeleteRecord(name string, typev RecordType) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "deleteRecord", name, typev) +} + +// DeleteRecordTransaction creates a transaction invoking `deleteRecord` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) DeleteRecordTransaction(name string, typev RecordType) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "deleteRecord", name, typev) +} + +// DeleteRecordUnsigned creates a transaction invoking `deleteRecord` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) DeleteRecordUnsigned(name string, typev RecordType) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "deleteRecord", nil, name, typev) +} + +// SetAdminEventsFromApplicationLog retrieves a set of all emitted events +// with "SetAdmin" name from the provided [result.ApplicationLog]. +func SetAdminEventsFromApplicationLog(log *result.ApplicationLog) ([]*SetAdminEvent, error) { + if log == nil { + return nil, errors.New("nil application log") + } + + var res []*SetAdminEvent + for i, ex := range log.Executions { + for j, e := range ex.Events { + if e.Name != "SetAdmin" { + continue + } + event := new(SetAdminEvent) + err := event.FromStackItem(e.Item) + if err != nil { + return nil, fmt.Errorf("failed to deserialize SetAdminEvent from stackitem (execution #%d, event #%d): %w", i, j, err) + } + res = append(res, event) } } + return res, nil } + +// FromStackItem converts provided [stackitem.Array] to SetAdminEvent or +// returns an error if it's not possible to do to so. +func (e *SetAdminEvent) FromStackItem(item *stackitem.Array) error { + if item == nil { + return errors.New("nil item") + } + arr, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not an array") + } + if len(arr) != 3 { + return errors.New("wrong number of structure elements") + } + + var ( + index = -1 + err error + ) + index++ + e.Name, err = func(item stackitem.Item) (string, error) { + b, err := item.TryBytes() + if err != nil { + return "", err + } + if !utf8.Valid(b) { + return "", errors.New("not a UTF-8 string") + } + return string(b), nil + }(arr[index]) + if err != nil { + return fmt.Errorf("field Name: %w", err) + } + + index++ + e.OldAdmin, err = func(item stackitem.Item) (util.Uint160, error) { + b, err := item.TryBytes() + if err != nil { + return util.Uint160{}, err + } + u, err := util.Uint160DecodeBytesBE(b) + if err != nil { + return util.Uint160{}, err + } + return u, nil + }(arr[index]) + if err != nil { + return fmt.Errorf("field OldAdmin: %w", err) + } + + index++ + e.NewAdmin, err = func(item stackitem.Item) (util.Uint160, error) { + b, err := item.TryBytes() + if err != nil { + return util.Uint160{}, err + } + u, err := util.Uint160DecodeBytesBE(b) + if err != nil { + return util.Uint160{}, err + } + return u, nil + }(arr[index]) + if err != nil { + return fmt.Errorf("field NewAdmin: %w", err) + } + + return nil +} + +// RenewEventsFromApplicationLog retrieves a set of all emitted events +// with "Renew" name from the provided [result.ApplicationLog]. +func RenewEventsFromApplicationLog(log *result.ApplicationLog) ([]*RenewEvent, error) { + if log == nil { + return nil, errors.New("nil application log") + } + + var res []*RenewEvent + for i, ex := range log.Executions { + for j, e := range ex.Events { + if e.Name != "Renew" { + continue + } + event := new(RenewEvent) + err := event.FromStackItem(e.Item) + if err != nil { + return nil, fmt.Errorf("failed to deserialize RenewEvent from stackitem (execution #%d, event #%d): %w", i, j, err) + } + res = append(res, event) + } + } + + return res, nil +} + +// FromStackItem converts provided [stackitem.Array] to RenewEvent or +// returns an error if it's not possible to do to so. +func (e *RenewEvent) FromStackItem(item *stackitem.Array) error { + if item == nil { + return errors.New("nil item") + } + arr, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not an array") + } + if len(arr) != 3 { + return errors.New("wrong number of structure elements") + } + + var ( + index = -1 + err error + ) + index++ + e.Name, err = func(item stackitem.Item) (string, error) { + b, err := item.TryBytes() + if err != nil { + return "", err + } + if !utf8.Valid(b) { + return "", errors.New("not a UTF-8 string") + } + return string(b), nil + }(arr[index]) + if err != nil { + return fmt.Errorf("field Name: %w", err) + } + + index++ + e.OldExpiration, err = arr[index].TryInteger() + if err != nil { + return fmt.Errorf("field OldExpiration: %w", err) + } + + index++ + e.NewExpiration, err = arr[index].TryInteger() + if err != nil { + return fmt.Errorf("field NewExpiration: %w", err) + } + + return nil +} diff --git a/pkg/rpcclient/nns/contract_test.go b/pkg/rpcclient/nns/contract_test.go index 98a4ef20d..1e3e96232 100644 --- a/pkg/rpcclient/nns/contract_test.go +++ b/pkg/rpcclient/nns/contract_test.go @@ -2,10 +2,13 @@ package nns import ( "errors" + "math/big" "testing" "github.com/google/uuid" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" @@ -14,6 +17,9 @@ import ( type testAct struct { err error res *result.Invoke + tx *transaction.Transaction + txh util.Uint256 + vub uint32 } func (t *testAct) Call(contract util.Uint160, operation string, params ...any) (*result.Invoke, error) { @@ -29,17 +35,42 @@ func (t *testAct) TraverseIterator(sessionID uuid.UUID, iterator *result.Iterato return t.res.Stack, t.err } +func (t *testAct) MakeRun(script []byte) (*transaction.Transaction, error) { + return t.tx, t.err +} +func (t *testAct) MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) { + return t.tx, t.err +} +func (t *testAct) SendRun(script []byte) (util.Uint256, uint32, error) { + return t.txh, t.vub, t.err +} +func (t *testAct) MakeCall(contract util.Uint160, method string, params ...any) (*transaction.Transaction, error) { + return t.tx, t.err +} +func (t *testAct) MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...any) (*transaction.Transaction, error) { + return t.tx, t.err +} +func (t *testAct) SendCall(contract util.Uint160, method string, params ...any) (util.Uint256, uint32, error) { + return t.txh, t.vub, t.err +} + +func (t *testAct) SignAndSend(tx *transaction.Transaction) (util.Uint256, uint32, error) { + return t.txh, t.vub, t.err +} + func TestSimpleGetters(t *testing.T) { ta := &testAct{} nns := NewReader(ta, util.Uint160{1, 2, 3}) ta.err = errors.New("") - _, err := nns.GetPrice() + _, err := nns.GetPrice(uint8(A)) require.Error(t, err) _, err = nns.IsAvailable("nspcc.neo") require.Error(t, err) _, err = nns.Resolve("nspcc.neo", A) require.Error(t, err) + _, err = nns.GetRecord("nspcc.neo", A) + require.Error(t, err) ta.err = nil ta.res = &result.Invoke{ @@ -48,9 +79,9 @@ func TestSimpleGetters(t *testing.T) { stackitem.Make(100500), }, } - price, err := nns.GetPrice() + price, err := nns.GetPrice(uint8(A)) require.NoError(t, err) - require.Equal(t, int64(100500), price) + require.Equal(t, new(big.Int).SetInt64(100500), price) ta.res = &result.Invoke{ State: "HALT", @@ -71,6 +102,10 @@ func TestSimpleGetters(t *testing.T) { txt, err := nns.Resolve("nspcc.neo", TXT) require.NoError(t, err) require.Equal(t, "some text", txt) + + rec, err := nns.GetRecord("nspcc.neo", TXT) + require.NoError(t, err) + require.Equal(t, "some text", rec) } func TestGetAllRecords(t *testing.T) { @@ -108,7 +143,6 @@ func TestGetAllRecords(t *testing.T) { 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{ @@ -154,6 +188,26 @@ func TestGetAllRecords(t *testing.T) { ta.err = errors.New("") err = iter.Terminate() require.NoError(t, err) + + ta.err = nil + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.NewInterop(result.Iterator{ + Values: []stackitem.Item{ + stackitem.Make("valid data"), + stackitem.Make(-1), + }, + }), + }, + } + iter, err = nns.GetAllRecords("nspcc.neo") + require.NoError(t, err) + + _, err = iter.Next(10) + + require.Error(t, err) + require.Contains(t, err.Error(), "item #0: ") } func TestGetAllRecordsExpanded(t *testing.T) { @@ -195,3 +249,520 @@ func TestGetAllRecordsExpanded(t *testing.T) { Data: "cool", }, vals[0]) } + +func TestRoots(t *testing.T) { + ta := &testAct{} + nns := NewReader(ta, util.Uint160{1, 2, 3}) + ta.err = errors.New("") + _, err := nns.Roots() + require.Error(t, err) + iid := uuid.New() + + // Session-based iterator. + sid := uuid.New() + ta.res = &result.Invoke{ + Session: sid, + State: "HALT", + Stack: []stackitem.Item{ + stackitem.NewInterop(result.Iterator{ + ID: &iid, + }), + }, + } + ta.err = nil + iter, err := nns.Roots() + require.NoError(t, err) + + ta.res = &result.Invoke{ + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{ + stackitem.Make("n3"), + stackitem.Make("aaaaaa"), + stackitem.Make("cool"), + }), + }, + } + vals, err := iter.Next(10) + require.NoError(t, err) + require.Equal(t, 1, len(vals)) + require.Equal(t, "n3", 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("aaaaaa"), + stackitem.Make("cool"), + }, + }), + }, + } + iter, err = nns.Roots() + require.NoError(t, err) + + ta.err = errors.New("") + err = iter.Terminate() + require.NoError(t, err) + + sid = uuid.New() + iid = uuid.New() + ta.res = &result.Invoke{ + Session: sid, + State: "HALT", + Stack: []stackitem.Item{ + stackitem.NewInterop(result.Iterator{ + ID: &iid, + Values: []stackitem.Item{ + stackitem.Make("incorrect format"), + }, + }), + }, + } + ta.err = nil + iter, err = nns.Roots() + require.NoError(t, err) + + _, err = iter.Next(10) + require.Error(t, err) + require.Equal(t, "wrong number of elements", err.Error()) + + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{ + stackitem.Make([]stackitem.Item{ + stackitem.Make("root1"), + }), + stackitem.Make([]stackitem.Item{ + stackitem.Make("root2"), + }), + }), + }, + } + + roots, err := nns.RootsExpanded(10) + require.NoError(t, err) + require.Equal(t, []string{"root1", "root2"}, roots) + + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make("incorrect format"), // Not a slice of stackitem.Item + }, + } + + _, err = nns.RootsExpanded(10) + require.Error(t, err) + require.Equal(t, "not an array", err.Error()) + + ta.err = errors.New("call and expand iterator error") + _, err = nns.RootsExpanded(10) + require.Error(t, err) + require.Equal(t, "call and expand iterator error", err.Error()) +} + +func TestUpdate(t *testing.T) { + ta := &testAct{} + nns := New(ta, util.Uint160{1, 2, 3}) + + nef := []byte{0x01, 0x02, 0x03} + manifest := "manifest data" + + ta.err = errors.New("test error") + _, _, err := nns.Update(nef, manifest) + require.Error(t, err) + + // Test successful update + ta.err = nil + ta.txh = util.Uint256{0x04, 0x05, 0x06} + txh, vub, err := nns.Update(nef, manifest) + require.NoError(t, err) + require.Equal(t, ta.txh, txh) + require.Equal(t, ta.vub, vub) + + for _, fun := range []func(nef []byte, manifest string) (*transaction.Transaction, error){ + nns.UpdateTransaction, + nns.UpdateUnsigned, + } { + ta.err = errors.New("") + _, err := fun(nil, "") + require.Error(t, err) + + ta.err = nil + ta.tx = &transaction.Transaction{Nonce: 100500, ValidUntilBlock: 42} + tx, err := fun(nil, "") + require.NoError(t, err) + require.Equal(t, ta.tx, tx) + } +} + +func TestAddRoot(t *testing.T) { + ta := &testAct{} + nns := New(ta, util.Uint160{1, 2, 3}) + + root := "example.root" + params, err := smartcontract.NewParameterFromValue(root) + require.NoError(t, err) + ta.err = errors.New("test error") + _, _, err = nns.AddRoot(params.Value.(string)) + require.Error(t, err) + + // Test success case + ta.err = nil + ta.txh = util.Uint256{0x07, 0x08, 0x09} + txh, vub, err := nns.AddRoot(root) + require.NoError(t, err) + require.Equal(t, ta.txh, txh) + require.Equal(t, ta.vub, vub) + + ta.tx = &transaction.Transaction{Nonce: 100500, ValidUntilBlock: 42} + tx, err := nns.AddRootTransaction(root) + require.NoError(t, err) + require.Equal(t, ta.tx, tx) + tx, err = nns.AddRootUnsigned(root) + require.NoError(t, err) + require.Equal(t, ta.tx, tx) + + ta.err = errors.New("") + _, err = nns.AddRootTransaction(root) + require.Error(t, err) + + ta.err = errors.New("") + _, err = nns.AddRootUnsigned(root) + require.Error(t, err) +} + +func TestSetPrice(t *testing.T) { + ta := &testAct{} + nns := New(ta, util.Uint160{1, 2, 3}) + + priceList := []int64{100, 200} + ta.err = errors.New("test error") + _, _, err := nns.SetPrice(priceList) + require.Error(t, err) + _, err = nns.SetPriceTransaction(priceList) + require.Error(t, err) + _, err = nns.SetPriceUnsigned(priceList) + require.Error(t, err) + + // Test success case + ta.err = nil + ta.txh = util.Uint256{0x0A, 0x0B, 0x0C} + ta.vub = 42 + + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make(42), + }, + } + + txh, vub, err := nns.SetPrice(priceList) + require.NoError(t, err) + require.Equal(t, ta.txh, txh) + require.Equal(t, ta.vub, vub) + + ta.tx = &transaction.Transaction{Nonce: 100500, ValidUntilBlock: 42} + tx, err := nns.SetPriceTransaction(priceList) + require.NoError(t, err) + require.Equal(t, ta.tx, tx) + tx, err = nns.SetPriceUnsigned(priceList) + require.NoError(t, err) + require.Equal(t, ta.tx, tx) + + ta.err = errors.New("") + _, err = nns.SetPriceTransaction(priceList) + require.Error(t, err) + + ta.err = errors.New("") + _, err = nns.SetPriceUnsigned(priceList) + require.Error(t, err) +} + +func TestRegister(t *testing.T) { + ta := &testAct{} + nns := New(ta, util.Uint160{1, 2, 3}) + + name := "example.neo" + owner := util.Uint160{0x0D, 0x0E, 0x0F} + + ta.err = errors.New("test error") + txh, vub, err := nns.Register(name, owner) + require.Error(t, err) + require.Equal(t, util.Uint256{}, txh) // Check if returned Uint256 is zero-initialized + require.Equal(t, uint32(0), vub) + + // Test success case + ta.err = nil + ta.txh = util.Uint256{0x10, 0x11, 0x12} + txh, vub, err = nns.Register(name, owner) + require.NoError(t, err) + require.Equal(t, ta.txh, txh) + require.Equal(t, ta.vub, vub) + + ta.tx = &transaction.Transaction{Nonce: 100500, ValidUntilBlock: 42} + tx, err := nns.RegisterTransaction(name, owner) + require.NoError(t, err) + require.Equal(t, ta.tx, tx) + + tx, err = nns.RegisterUnsigned(name, owner) + require.NoError(t, err) + require.Equal(t, ta.tx, tx) + + ta.err = errors.New("") + _, err = nns.RegisterTransaction(name, owner) + require.Error(t, err) + + ta.err = errors.New("") + _, err = nns.RegisterUnsigned(name, owner) + require.Error(t, err) +} + +func TestRenew(t *testing.T) { + ta := &testAct{} + nns := New(ta, util.Uint160{1, 2, 3}) + + name := "example.neo" + + ta.err = errors.New("test error") + _, _, err := nns.Renew(name) + require.Error(t, err) + + // Test success case + ta.err = nil + ta.txh = util.Uint256{0x13, 0x14, 0x15} + txh, vub, err := nns.Renew(name) + require.NoError(t, err) + require.Equal(t, ta.txh, txh) + require.Equal(t, ta.vub, vub) + + txh, vub, err = nns.Renew2(name, 1) + require.NoError(t, err) + require.Equal(t, ta.txh, txh) + require.Equal(t, ta.vub, vub) + + ta.tx = &transaction.Transaction{Nonce: 100500, ValidUntilBlock: 42} + tx, err := nns.RenewTransaction(name) + require.NoError(t, err) + require.Equal(t, ta.tx, tx) + + tx, err = nns.RenewUnsigned(name) + require.NoError(t, err) + require.Equal(t, ta.tx, tx) + + ta.err = errors.New("") + _, err = nns.RenewTransaction(name) + require.Error(t, err) + + ta.err = errors.New("") + _, err = nns.RenewUnsigned(name) + require.Error(t, err) +} + +func TestSetAdmin(t *testing.T) { + ta := &testAct{} + c := New(ta, util.Uint160{1, 2, 3}) + + name := "example.neo" + admin := util.Uint160{4, 5, 6} + txMock := &transaction.Transaction{Nonce: 100500, ValidUntilBlock: 42} + txhMock := util.Uint256{0x13, 0x14, 0x15} + + testCases := []struct { + name string + setup func() + testFunc func() (interface{}, error) + want interface{} + wantErr bool + }{ + { + name: "SetAdmin - Error", + setup: func() { + ta.err = errors.New("test error") + }, + testFunc: func() (interface{}, error) { + txh, vub, err := c.SetAdmin(name, admin) + return []interface{}{txh, vub}, err + }, + wantErr: true, + }, + { + name: "SetAdmin - Success", + setup: func() { + ta.err = nil + ta.txh = txhMock + ta.vub = 42 + }, + testFunc: func() (interface{}, error) { + txh, vub, err := c.SetAdmin(name, admin) + return []interface{}{txh, vub}, err + }, + want: []interface{}{txhMock, uint32(42)}, + }, + { + name: "SetAdminTransaction - Success", + setup: func() { + ta.err = nil + ta.tx = txMock + }, + testFunc: func() (interface{}, error) { + return c.SetAdminTransaction(name, admin) + }, + want: txMock, + }, + { + name: "SetAdminTransaction - Error", + setup: func() { + ta.err = errors.New("test error") + }, + testFunc: func() (interface{}, error) { + return c.SetAdminTransaction(name, admin) + }, + wantErr: true, + }, + { + name: "SetAdminUnsigned - Success", + setup: func() { + ta.err = nil + ta.tx = txMock + }, + testFunc: func() (interface{}, error) { + return c.SetAdminUnsigned(name, admin) + }, + want: txMock, + }, + { + name: "SetAdminUnsigned - Error", + setup: func() { + ta.err = errors.New("test error") + }, + testFunc: func() (interface{}, error) { + return c.SetAdminUnsigned(name, admin) + }, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.setup() + got, err := tc.testFunc() + if tc.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tc.want, got) + } + }) + } +} + +func TestSetRecord(t *testing.T) { + ta := &testAct{} + c := New(ta, util.Uint160{1, 2, 3}) + + name := "example.neo" + typev := A + data := "record data" + txMock := &transaction.Transaction{Nonce: 100500, ValidUntilBlock: 42} + txhMock := util.Uint256{0x13, 0x14, 0x15} + + testCases := []struct { + name string + setup func() + testFunc func() (interface{}, error) + want interface{} + wantErr bool + }{ + { + name: "SetRecord - Error", + setup: func() { + ta.err = errors.New("test error") + }, + testFunc: func() (interface{}, error) { + txh, vub, err := c.SetRecord(name, typev, data) + return []interface{}{txh, vub}, err + }, + wantErr: true, + }, + { + name: "SetRecord - Success", + setup: func() { + ta.err = nil + ta.txh = txhMock + ta.vub = 42 + }, + testFunc: func() (interface{}, error) { + txh, vub, err := c.SetRecord(name, typev, data) + return []interface{}{txh, vub}, err + }, + want: []interface{}{txhMock, uint32(42)}, + }, + { + name: "SetRecordTransaction - Success", + setup: func() { + ta.err = nil + ta.tx = txMock + }, + testFunc: func() (interface{}, error) { + return c.SetRecordTransaction(name, typev, data) + }, + want: txMock, + }, + { + name: "SetRecordTransaction - Error", + setup: func() { + ta.err = errors.New("test error") + }, + testFunc: func() (interface{}, error) { + return c.SetRecordTransaction(name, typev, data) + }, + wantErr: true, + }, + { + name: "SetRecordUnsigned - Success", + setup: func() { + ta.err = nil + ta.tx = txMock + }, + testFunc: func() (interface{}, error) { + return c.SetRecordUnsigned(name, typev, data) + }, + want: txMock, + }, + { + name: "SetRecordUnsigned - Error", + setup: func() { + ta.err = errors.New("test error") + }, + testFunc: func() (interface{}, error) { + return c.SetRecordUnsigned(name, typev, data) + }, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.setup() + got, err := tc.testFunc() + if tc.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tc.want, got) + } + }) + } +} diff --git a/pkg/rpcclient/nns/iterators.go b/pkg/rpcclient/nns/iterators.go new file mode 100644 index 000000000..95b1abce1 --- /dev/null +++ b/pkg/rpcclient/nns/iterators.go @@ -0,0 +1,90 @@ +package nns + +import ( + "errors" + "fmt" + + "github.com/google/uuid" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" +) + +// RecordIterator is used for iterating over GetAllRecords results. +type RecordIterator struct { + client Invoker + session uuid.UUID + iterator result.Iterator +} + +// RootIterator is used for iterating over Roots results. +type RootIterator struct { + client Invoker + session uuid.UUID + iterator result.Iterator +} + +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 +} + +func itemsToRoots(arr []stackitem.Item) ([]string, error) { + res := make([]string, len(arr)) + for i := range arr { + rs, ok := arr[i].Value().([]stackitem.Item) + if !ok { + return nil, errors.New("wrong number of elements") + } + myval, _ := rs[0].TryBytes() + res[i] = string(myval) + } + return res, 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) +} + +// 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 *RootIterator) Next(num int) ([]string, error) { + items, err := r.client.TraverseIterator(r.session, &r.iterator, num) + if err != nil { + return nil, err + } + return itemsToRoots(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) +} + +// Terminate closes the iterator session used by RootIterator (if it's +// session-based). +func (r *RootIterator) Terminate() error { + if r.iterator.ID == nil { + return nil + } + return r.client.TerminateSession(r.session) +}