From 82641e2e99eed16be4b4e0532ee4b507ccac45c3 Mon Sep 17 00:00:00 2001 From: Alexander Chuprov Date: Fri, 6 Sep 2024 16:37:35 +0300 Subject: [PATCH] [#109] nns: Add notification sending Signed-off-by: Alexander Chuprov --- nns/config.yml | 20 +++ nns/nns_contract.go | 6 + rpcclient/nns/client.go | 354 ++++++++++++++++++++++++++++++++++++++++ tests/nns_test.go | 38 ++++- 4 files changed, 414 insertions(+), 4 deletions(-) diff --git a/nns/config.yml b/nns/config.yml index 75a111d..9ae83cd 100644 --- a/nns/config.yml +++ b/nns/config.yml @@ -4,6 +4,26 @@ 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: 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..56dfb2c 100644 --- a/nns/nns_contract.go +++ b/nns/nns_contract.go @@ -407,6 +407,7 @@ func register(ctx storage.Context, name string, owner interop.Hash160, email str putSoaRecord(ctx, name, email, refresh, retry, expire, ttl) updateBalance(ctx, []byte(name), owner, +1) postTransfer(oldOwner, owner, []byte(name), nil) + runtime.Notify("RegisterDomain", name) return true } @@ -523,6 +524,7 @@ func deleteRecords(ctx storage.Context, name string, typ RecordType) { storage.Delete(ctx, r) } updateSoaSerial(ctx, tokenID) + runtime.Notify("DeleteRecords", name, typ) } // DeleteDomain deletes the domain with the given name. @@ -549,6 +551,7 @@ func deleteDomain(ctx storage.Context, name string) { deleteRecords(ctx, name, A) deleteRecords(ctx, name, AAAA) storage.Delete(ctx, nameKey) + runtime.Notify("DeleteDomain", name) } // Resolve resolves given name (not more then three redirects are allowed). @@ -736,6 +739,7 @@ func storeRecord(ctx storage.Context, recordKey []byte, name string, typ RecordT } recBytes := std.Serialize(rs) storage.Put(ctx, recordKey, recBytes) + runtime.Notify("AddRecord", name, typ) } // putSoaRecord stores soa domain record. @@ -756,6 +760,7 @@ func putSoaRecord(ctx storage.Context, name, email string, refresh, retry, expir } recBytes := std.Serialize(rs) storage.Put(ctx, recordKey, recBytes) + runtime.Notify("AddRecord", name, SOA) } // putCnameRecord stores CNAME domain record. @@ -772,6 +777,7 @@ func putCnameRecord(ctx storage.Context, name, data string) { } recBytes := std.Serialize(rs) storage.Put(ctx, recordKey, recBytes) + runtime.Notify("AddRecord", name, CNAME) } // updateSoaSerial stores soa domain record. 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..6c111b0 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,34 @@ 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: "AddRecord", 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")), + stackitem.NewBigInteger(big.NewInt(int64(nns.CNAME)))}) + c.CheckTxNotificationEvent(t, tx, 0, state.NotificationEvent{ScriptHash: c.Hash, Name: "DeleteRecords", Item: expected}) + + expected = stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte("testdomain.com"))}) + c.CheckTxNotificationEvent(t, tx, 4, 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) {