From fbb5b41f26a9be6cfb6707a8551054c9df393db6 Mon Sep 17 00:00:00 2001 From: Alexander Chuprov Date: Thu, 5 Sep 2024 18:17:04 +0300 Subject: [PATCH] [#109] nns: Add notification sending Signed-off-by: Alexander Chuprov --- nns/config.yml | 26 +++ nns/nns_contract.go | 10 +- rpcclient/nns/client.go | 354 ++++++++++++++++++++++++++++++++++++++++ tests/nns_test.go | 34 +++- 4 files changed, 419 insertions(+), 5 deletions(-) diff --git a/nns/config.yml b/nns/config.yml index 75a111d..7a5f774 100644 --- a/nns/config.yml +++ b/nns/config.yml @@ -4,6 +4,32 @@ safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "own "tokens", "properties", "roots", "getPrice", "isAvailable", "getRecords", "resolve", "version"] events: + - name: RegisterDomain + parameters: + - name: name + type: String + - name: AddRecord + parameters: + - name: name + type: String + - name: type + type: Integer + - name: SetRecord + parameters: + - name: name + type: String + - name: type + type: Integer + - name: DeleteRecords + parameters: + - name: name + type: String + - name: type + type: Integer + - name: DeleteDomain + parameters: + - name: name + type: String - name: Transfer parameters: - name: from diff --git a/nns/nns_contract.go b/nns/nns_contract.go index bef2586..971b2a8 100644 --- a/nns/nns_contract.go +++ b/nns/nns_contract.go @@ -336,7 +336,11 @@ func parentExpired(ctx storage.Context, fragments []string) string { // Register registers a new domain with the specified owner and name if it's available. func Register(name string, owner interop.Hash160, email string, refresh, retry, expire, ttl int) bool { ctx := storage.GetContext() - return register(ctx, name, owner, email, refresh, retry, expire, ttl) + st := register(ctx, name, owner, email, refresh, retry, expire, ttl) + if st == true { + runtime.Notify("RegisterDomain", name) + } + return st } // Register registers a new domain with the specified owner and name if it's available. @@ -455,6 +459,7 @@ func SetRecord(name string, typ RecordType, id byte, data string) { ns.checkAdmin() putRecord(ctx, tokenID, name, typ, id, data) updateSoaSerial(ctx, tokenID) + runtime.Notify("SetRecord", name, typ) } func checkBaseRecords(typ RecordType, data string) bool { @@ -483,6 +488,7 @@ func AddRecord(name string, typ RecordType, data string) { ns.checkAdmin() addRecord(ctx, tokenID, name, typ, data) updateSoaSerial(ctx, tokenID) + runtime.Notify("AddRecord", name, typ) } // GetRecords returns domain record of the specified type if it exists or an empty @@ -498,6 +504,7 @@ func GetRecords(name string, typ RecordType) []string { func DeleteRecords(name string, typ RecordType) { ctx := storage.GetContext() deleteRecords(ctx, name, typ) + runtime.Notify("DeleteRecords", name, typ) } // DeleteRecords removes domain records with the specified type. @@ -529,6 +536,7 @@ func deleteRecords(ctx storage.Context, name string, typ RecordType) { func DeleteDomain(name string) { ctx := storage.GetContext() deleteDomain(ctx, name) + runtime.Notify("DeleteDomain", name) } func deleteDomain(ctx storage.Context, name string) { diff --git a/rpcclient/nns/client.go b/rpcclient/nns/client.go index 858819d..014cbda 100644 --- a/rpcclient/nns/client.go +++ b/rpcclient/nns/client.go @@ -4,6 +4,8 @@ package nameservice import ( + "errors" + "fmt" "github.com/google/uuid" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" @@ -13,8 +15,37 @@ import ( "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "math/big" + "unicode/utf8" ) +// RegisterDomainEvent represents "RegisterDomain" event emitted by the contract. +type RegisterDomainEvent struct { + Name string +} + +// AddRecordEvent represents "AddRecord" event emitted by the contract. +type AddRecordEvent struct { + Name string + Type *big.Int +} + +// SetRecordEvent represents "SetRecord" event emitted by the contract. +type SetRecordEvent struct { + Name string + Type *big.Int +} + +// DeleteRecordsEvent represents "DeleteRecords" event emitted by the contract. +type DeleteRecordsEvent struct { + Name string + Type *big.Int +} + +// DeleteDomainEvent represents "DeleteDomain" event emitted by the contract. +type DeleteDomainEvent struct { + Name string +} + // Invoker is used by ContractReader to call various safe methods. type Invoker interface { nep11.Invoker @@ -356,3 +387,326 @@ func (c *Contract) UpdateSOATransaction(name string, email string, refresh *big. func (c *Contract) UpdateSOAUnsigned(name string, email string, refresh *big.Int, retry *big.Int, expire *big.Int, ttl *big.Int) (*transaction.Transaction, error) { return c.actor.MakeUnsignedCall(c.hash, "updateSOA", nil, name, email, refresh, retry, expire, ttl) } + +// RegisterDomainEventsFromApplicationLog retrieves a set of all emitted events +// with "RegisterDomain" name from the provided [result.ApplicationLog]. +func RegisterDomainEventsFromApplicationLog(log *result.ApplicationLog) ([]*RegisterDomainEvent, error) { + if log == nil { + return nil, errors.New("nil application log") + } + + var res []*RegisterDomainEvent + for i, ex := range log.Executions { + for j, e := range ex.Events { + if e.Name != "RegisterDomain" { + continue + } + event := new(RegisterDomainEvent) + err := event.FromStackItem(e.Item) + if err != nil { + return nil, fmt.Errorf("failed to deserialize RegisterDomainEvent from stackitem (execution #%d, event #%d): %w", i, j, err) + } + res = append(res, event) + } + } + + return res, nil +} + +// FromStackItem converts provided [stackitem.Array] to RegisterDomainEvent or +// returns an error if it's not possible to do to so. +func (e *RegisterDomainEvent) 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) != 1 { + 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) + } + + return nil +} + +// AddRecordEventsFromApplicationLog retrieves a set of all emitted events +// with "AddRecord" name from the provided [result.ApplicationLog]. +func AddRecordEventsFromApplicationLog(log *result.ApplicationLog) ([]*AddRecordEvent, error) { + if log == nil { + return nil, errors.New("nil application log") + } + + var res []*AddRecordEvent + for i, ex := range log.Executions { + for j, e := range ex.Events { + if e.Name != "AddRecord" { + continue + } + event := new(AddRecordEvent) + err := event.FromStackItem(e.Item) + if err != nil { + return nil, fmt.Errorf("failed to deserialize AddRecordEvent from stackitem (execution #%d, event #%d): %w", i, j, err) + } + res = append(res, event) + } + } + + return res, nil +} + +// FromStackItem converts provided [stackitem.Array] to AddRecordEvent or +// returns an error if it's not possible to do to so. +func (e *AddRecordEvent) 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) != 2 { + 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.Type, err = arr[index].TryInteger() + if err != nil { + return fmt.Errorf("field Type: %w", err) + } + + return nil +} + +// SetRecordEventsFromApplicationLog retrieves a set of all emitted events +// with "SetRecord" name from the provided [result.ApplicationLog]. +func SetRecordEventsFromApplicationLog(log *result.ApplicationLog) ([]*SetRecordEvent, error) { + if log == nil { + return nil, errors.New("nil application log") + } + + var res []*SetRecordEvent + for i, ex := range log.Executions { + for j, e := range ex.Events { + if e.Name != "SetRecord" { + continue + } + event := new(SetRecordEvent) + err := event.FromStackItem(e.Item) + if err != nil { + return nil, fmt.Errorf("failed to deserialize SetRecordEvent from stackitem (execution #%d, event #%d): %w", i, j, err) + } + res = append(res, event) + } + } + + return res, nil +} + +// FromStackItem converts provided [stackitem.Array] to SetRecordEvent or +// returns an error if it's not possible to do to so. +func (e *SetRecordEvent) 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) != 2 { + 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.Type, err = arr[index].TryInteger() + if err != nil { + return fmt.Errorf("field Type: %w", err) + } + + return nil +} + +// DeleteRecordsEventsFromApplicationLog retrieves a set of all emitted events +// with "DeleteRecords" name from the provided [result.ApplicationLog]. +func DeleteRecordsEventsFromApplicationLog(log *result.ApplicationLog) ([]*DeleteRecordsEvent, error) { + if log == nil { + return nil, errors.New("nil application log") + } + + var res []*DeleteRecordsEvent + for i, ex := range log.Executions { + for j, e := range ex.Events { + if e.Name != "DeleteRecords" { + continue + } + event := new(DeleteRecordsEvent) + err := event.FromStackItem(e.Item) + if err != nil { + return nil, fmt.Errorf("failed to deserialize DeleteRecordsEvent from stackitem (execution #%d, event #%d): %w", i, j, err) + } + res = append(res, event) + } + } + + return res, nil +} + +// FromStackItem converts provided [stackitem.Array] to DeleteRecordsEvent or +// returns an error if it's not possible to do to so. +func (e *DeleteRecordsEvent) 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) != 2 { + 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.Type, err = arr[index].TryInteger() + if err != nil { + return fmt.Errorf("field Type: %w", err) + } + + return nil +} + +// DeleteDomainEventsFromApplicationLog retrieves a set of all emitted events +// with "DeleteDomain" name from the provided [result.ApplicationLog]. +func DeleteDomainEventsFromApplicationLog(log *result.ApplicationLog) ([]*DeleteDomainEvent, error) { + if log == nil { + return nil, errors.New("nil application log") + } + + var res []*DeleteDomainEvent + for i, ex := range log.Executions { + for j, e := range ex.Events { + if e.Name != "DeleteDomain" { + continue + } + event := new(DeleteDomainEvent) + err := event.FromStackItem(e.Item) + if err != nil { + return nil, fmt.Errorf("failed to deserialize DeleteDomainEvent from stackitem (execution #%d, event #%d): %w", i, j, err) + } + res = append(res, event) + } + } + + return res, nil +} + +// FromStackItem converts provided [stackitem.Array] to DeleteDomainEvent or +// returns an error if it's not possible to do to so. +func (e *DeleteDomainEvent) 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) != 1 { + 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) + } + + return nil +} diff --git a/tests/nns_test.go b/tests/nns_test.go index e824303..05126c6 100644 --- a/tests/nns_test.go +++ b/tests/nns_test.go @@ -10,6 +10,7 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-contract/nns" "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" + "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/rpcclient/gas" @@ -125,19 +126,28 @@ func TestNNSRegister(t *testing.T) { "test-domain.com", acc.ScriptHash(), "myemail@frostfs.info", refresh, retry, expire, ttl) }) - c3.Invoke(t, true, "register", + expected := stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray([]byte("testdomain.com")), + }) + tx := c3.Invoke(t, true, "register", "testdomain.com", acc.ScriptHash(), "myemail@frostfs.info", refresh, retry, expire, ttl) + c.CheckTxNotificationEvent(t, tx, -1, state.NotificationEvent{ScriptHash: c.Hash, Name: "RegisterDomain", Item: expected}) b := c.TopBlock(t) - expected := stackitem.NewArray([]stackitem.Item{stackitem.NewBuffer( + expected = stackitem.NewArray([]stackitem.Item{stackitem.NewBuffer( []byte(fmt.Sprintf("testdomain.com myemail@frostfs.info %d %d %d %d %d", b.Timestamp, refresh, retry, expire, ttl)))}) c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.SOA)) cAcc := c.WithSigners(acc) - cAcc.Invoke(t, stackitem.Null{}, "addRecord", + + expected = stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte("testdomain.com")), + stackitem.NewBigInteger(big.NewInt(int64(nns.TXT)))}) + tx = cAcc.Invoke(t, stackitem.Null{}, "addRecord", "testdomain.com", int64(nns.TXT), "first TXT record") + c.CheckTxNotificationEvent(t, tx, 0, state.NotificationEvent{ScriptHash: c.Hash, Name: "AddRecord", Item: expected}) + cAcc.InvokeFail(t, "record already exists", "addRecord", "testdomain.com", int64(nns.TXT), "first TXT record") cAcc.Invoke(t, stackitem.Null{}, "addRecord", @@ -149,14 +159,30 @@ func TestNNSRegister(t *testing.T) { }) c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.TXT)) - cAcc.Invoke(t, stackitem.Null{}, "setRecord", + expected = stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte("testdomain.com")), + stackitem.NewBigInteger(big.NewInt(int64(nns.TXT)))}) + tx = cAcc.Invoke(t, stackitem.Null{}, "setRecord", "testdomain.com", int64(nns.TXT), int64(0), "replaced first") + c.CheckTxNotificationEvent(t, tx, 0, state.NotificationEvent{ScriptHash: c.Hash, Name: "SetRecord", Item: expected}) expected = stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray([]byte("replaced first")), stackitem.NewByteArray([]byte("second TXT record")), }) c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.TXT)) + + tx = cAcc.Invoke(t, stackitem.Null{}, "deleteRecords", "testdomain.com", int64(nns.TXT)) + expected = stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte("testdomain.com")), + stackitem.NewBigInteger(big.NewInt(int64(nns.TXT)))}) + c.CheckTxNotificationEvent(t, tx, 0, state.NotificationEvent{ScriptHash: c.Hash, Name: "DeleteRecords", Item: expected}) + + c.Invoke(t, stackitem.Null{}, "getRecords", "testdomain.com", int64(nns.TXT)) + + tx = cAcc.Invoke(t, stackitem.Null{}, "deleteDomain", "testdomain.com") + expected = stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte("testdomain.com"))}) + c.CheckTxNotificationEvent(t, tx, 0, state.NotificationEvent{ScriptHash: c.Hash, Name: "DeleteDomain", Item: expected}) + + c.InvokeFail(t, "token not found", "getRecords", "testdomain.com", int64(nns.SOA)) } func TestGlobalDomain(t *testing.T) {