diff --git a/common/version.go b/common/version.go index 420f98c7..632cc521 100644 --- a/common/version.go +++ b/common/version.go @@ -9,7 +9,7 @@ const ( // Versions from which an update should be performed. // These should be used in a group (so prevMinor can be equal to minor if there are - // any migration routines. + // any migration routines). prevMajor = 0 prevMinor = 15 prevPatch = 4 diff --git a/contracts/nns/config.yml b/contracts/nns/config.yml index d336597d..8341e943 100644 --- a/contracts/nns/config.yml +++ b/contracts/nns/config.yml @@ -14,7 +14,25 @@ events: type: Integer - name: tokenId type: ByteArray + - name: SetAdmin + parameters: + - name: name + type: String + - name: oldAdmin + type: Hash160 + - name: newAdmin + type: Hash160 + - name: Renew + parameters: + - name: name + type: String + - name: oldExpiration + type: Integer + - name: newExpiration + type: Integer permissions: - hash: fffdc93764dbaddd97c48f252a53ea4643faa3fd methods: ["update"] - methods: ["onNEP11Payment"] +overloads: + renewDefault: renew diff --git a/contracts/nns/contract.go b/contracts/nns/contract.go index 7bd61311..d2a8ef79 100644 --- a/contracts/nns/contract.go +++ b/contracts/nns/contract.go @@ -72,6 +72,8 @@ const ( millisecondsInSecond = 1000 // millisecondsInYear is amount of milliseconds per year. millisecondsInYear = int64(365 * 24 * 3600 * millisecondsInSecond) + // millisecondsInTenYears is the amount of milliseconds per ten years. + millisecondsInTenYears = 10 * millisecondsInYear ) // RecordState is a type that registered entities are saved to. @@ -461,17 +463,36 @@ func saveDomain(ctx storage.Context, name, email string, refresh, retry, expire, putSoaRecord(ctx, name, email, refresh, retry, expire, ttl) } -// Renew increases domain expiration date. -func Renew(name string) int64 { +// RenewDefault increases domain expiration date for 1 year and returns +// the new expriration timestamp. +func RenewDefault(name string) int64 { + return Renew(name, 1) +} + +// Renew increases domain expiration date up to the specified amount of years +// (from 1 to 10, can't renew for more than 10 years). Returns new expiration +// timestamp. +func Renew(name string, years int) int64 { + if years < 1 || years > 10 { + panic("invalid renewal period value") + } if len(name) > maxDomainNameLength { - panic("too long name") + panic("invalid domain name format") } - runtime.BurnGas(GetPrice()) + runtime.BurnGas(int(GetPrice()) * years) ctx := storage.GetContext() ns := getNameState(ctx, []byte(name)) ns.checkAdmin() - ns.Expiration += millisecondsInYear + oldExpiration := ns.Expiration + ns.Expiration += millisecondsInYear * int64(years) + + fragments := splitAndCheck(name) + // TLDs are not subject to this check. + if len(fragments) > 1 && ns.Expiration > int64(runtime.GetTime())+millisecondsInTenYears { + panic("10 years of expiration period at max is allowed") + } putNameState(ctx, ns) + runtime.Notify("Renew", name, oldExpiration, ns.Expiration) return ns.Expiration } @@ -503,8 +524,10 @@ func SetAdmin(name string, admin interop.Hash160) { ctx := storage.GetContext() ns := getFragmentedNameState(ctx, []byte(name), fragments) common.CheckOwnerWitness(ns.Owner) + oldAdm := ns.Admin ns.Admin = admin putNameState(ctx, ns) + runtime.Notify("SetAdmin", name, oldAdm, admin) } // SetRecord updates existing domain record with the specified type and ID. diff --git a/contracts/nns/contract.nef b/contracts/nns/contract.nef index f0593ef5..b82818c5 100755 Binary files a/contracts/nns/contract.nef and b/contracts/nns/contract.nef differ diff --git a/contracts/nns/manifest.json b/contracts/nns/manifest.json index 57d5be38..3861659e 100755 --- a/contracts/nns/manifest.json +++ b/contracts/nns/manifest.json @@ -1 +1 @@ -{"name":"NameService","abi":{"methods":[{"name":"_initialize","offset":0,"parameters":[],"returntype":"Void","safe":false},{"name":"_deploy","offset":32,"parameters":[{"name":"data","type":"Any"},{"name":"isUpdate","type":"Boolean"}],"returntype":"Void","safe":false},{"name":"addRecord","offset":3135,"parameters":[{"name":"name","type":"String"},{"name":"typ","type":"Integer"},{"name":"data","type":"String"}],"returntype":"Void","safe":false},{"name":"balanceOf","offset":872,"parameters":[{"name":"owner","type":"Hash160"}],"returntype":"Integer","safe":true},{"name":"decimals","offset":677,"parameters":[],"returntype":"Integer","safe":true},{"name":"deleteRecords","offset":3452,"parameters":[{"name":"name","type":"String"},{"name":"typ","type":"Integer"}],"returntype":"Void","safe":false},{"name":"getAllRecords","offset":3699,"parameters":[{"name":"name","type":"String"}],"returntype":"InteropInterface","safe":true},{"name":"getPrice","offset":1323,"parameters":[],"returntype":"Integer","safe":true},{"name":"getRecords","offset":3366,"parameters":[{"name":"name","type":"String"},{"name":"typ","type":"Integer"}],"returntype":"Array","safe":true},{"name":"isAvailable","offset":1357,"parameters":[{"name":"name","type":"String"}],"returntype":"Boolean","safe":true},{"name":"ownerOf","offset":699,"parameters":[{"name":"tokenID","type":"ByteArray"}],"returntype":"Hash160","safe":true},{"name":"properties","offset":769,"parameters":[{"name":"tokenID","type":"ByteArray"}],"returntype":"Map","safe":true},{"name":"register","offset":1775,"parameters":[{"name":"name","type":"String"},{"name":"owner","type":"Hash160"},{"name":"email","type":"String"},{"name":"refresh","type":"Integer"},{"name":"retry","type":"Integer"},{"name":"expire","type":"Integer"},{"name":"ttl","type":"Integer"}],"returntype":"Boolean","safe":false},{"name":"registerTLD","offset":2291,"parameters":[{"name":"name","type":"String"},{"name":"email","type":"String"},{"name":"refresh","type":"Integer"},{"name":"retry","type":"Integer"},{"name":"expire","type":"Integer"},{"name":"ttl","type":"Integer"}],"returntype":"Void","safe":false},{"name":"renew","offset":2485,"parameters":[{"name":"name","type":"String"}],"returntype":"Integer","safe":false},{"name":"resolve","offset":3636,"parameters":[{"name":"name","type":"String"},{"name":"typ","type":"Integer"}],"returntype":"Array","safe":true},{"name":"roots","offset":1217,"parameters":[],"returntype":"InteropInterface","safe":true},{"name":"setAdmin","offset":2670,"parameters":[{"name":"name","type":"String"},{"name":"admin","type":"Hash160"}],"returntype":"Void","safe":false},{"name":"setPrice","offset":1245,"parameters":[{"name":"price","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setRecord","offset":2832,"parameters":[{"name":"name","type":"String"},{"name":"typ","type":"Integer"},{"name":"id","type":"Integer"},{"name":"data","type":"String"}],"returntype":"Void","safe":false},{"name":"symbol","offset":671,"parameters":[],"returntype":"String","safe":true},{"name":"tokens","offset":948,"parameters":[],"returntype":"InteropInterface","safe":true},{"name":"tokensOf","offset":977,"parameters":[{"name":"owner","type":"Hash160"}],"returntype":"InteropInterface","safe":true},{"name":"totalSupply","offset":683,"parameters":[],"returntype":"Integer","safe":true},{"name":"transfer","offset":1039,"parameters":[{"name":"to","type":"Hash160"},{"name":"tokenID","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Boolean","safe":false},{"name":"update","offset":587,"parameters":[{"name":"nef","type":"ByteArray"},{"name":"manifest","type":"String"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"updateSOA","offset":2593,"parameters":[{"name":"name","type":"String"},{"name":"email","type":"String"},{"name":"refresh","type":"Integer"},{"name":"retry","type":"Integer"},{"name":"expire","type":"Integer"},{"name":"ttl","type":"Integer"}],"returntype":"Void","safe":false},{"name":"version","offset":679,"parameters":[],"returntype":"Integer","safe":true}],"events":[{"name":"Transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"tokenId","type":"ByteArray"}]}]},"features":{},"groups":[],"permissions":[{"contract":"0xfffdc93764dbaddd97c48f252a53ea4643faa3fd","methods":["update"]},{"contract":"*","methods":["onNEP11Payment"]}],"supportedstandards":["NEP-11"],"trusts":[],"extra":null} \ No newline at end of file +{"name":"NameService","abi":{"methods":[{"name":"_initialize","offset":0,"parameters":[],"returntype":"Void","safe":false},{"name":"_deploy","offset":32,"parameters":[{"name":"data","type":"Any"},{"name":"isUpdate","type":"Boolean"}],"returntype":"Void","safe":false},{"name":"addRecord","offset":3345,"parameters":[{"name":"name","type":"String"},{"name":"typ","type":"Integer"},{"name":"data","type":"String"}],"returntype":"Void","safe":false},{"name":"balanceOf","offset":872,"parameters":[{"name":"owner","type":"Hash160"}],"returntype":"Integer","safe":true},{"name":"decimals","offset":677,"parameters":[],"returntype":"Integer","safe":true},{"name":"deleteRecords","offset":3662,"parameters":[{"name":"name","type":"String"},{"name":"typ","type":"Integer"}],"returntype":"Void","safe":false},{"name":"getAllRecords","offset":3909,"parameters":[{"name":"name","type":"String"}],"returntype":"InteropInterface","safe":true},{"name":"getPrice","offset":1323,"parameters":[],"returntype":"Integer","safe":true},{"name":"getRecords","offset":3576,"parameters":[{"name":"name","type":"String"},{"name":"typ","type":"Integer"}],"returntype":"Array","safe":true},{"name":"isAvailable","offset":1357,"parameters":[{"name":"name","type":"String"}],"returntype":"Boolean","safe":true},{"name":"ownerOf","offset":699,"parameters":[{"name":"tokenID","type":"ByteArray"}],"returntype":"Hash160","safe":true},{"name":"properties","offset":769,"parameters":[{"name":"tokenID","type":"ByteArray"}],"returntype":"Map","safe":true},{"name":"register","offset":1775,"parameters":[{"name":"name","type":"String"},{"name":"owner","type":"Hash160"},{"name":"email","type":"String"},{"name":"refresh","type":"Integer"},{"name":"retry","type":"Integer"},{"name":"expire","type":"Integer"},{"name":"ttl","type":"Integer"}],"returntype":"Boolean","safe":false},{"name":"registerTLD","offset":2291,"parameters":[{"name":"name","type":"String"},{"name":"email","type":"String"},{"name":"refresh","type":"Integer"},{"name":"retry","type":"Integer"},{"name":"expire","type":"Integer"},{"name":"ttl","type":"Integer"}],"returntype":"Void","safe":false},{"name":"renew","offset":2494,"parameters":[{"name":"name","type":"String"},{"name":"years","type":"Integer"}],"returntype":"Integer","safe":false},{"name":"renew","offset":2485,"parameters":[{"name":"name","type":"String"}],"returntype":"Integer","safe":false},{"name":"resolve","offset":3846,"parameters":[{"name":"name","type":"String"},{"name":"typ","type":"Integer"}],"returntype":"Array","safe":true},{"name":"roots","offset":1217,"parameters":[],"returntype":"InteropInterface","safe":true},{"name":"setAdmin","offset":2852,"parameters":[{"name":"name","type":"String"},{"name":"admin","type":"Hash160"}],"returntype":"Void","safe":false},{"name":"setPrice","offset":1245,"parameters":[{"name":"price","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setRecord","offset":3042,"parameters":[{"name":"name","type":"String"},{"name":"typ","type":"Integer"},{"name":"id","type":"Integer"},{"name":"data","type":"String"}],"returntype":"Void","safe":false},{"name":"symbol","offset":671,"parameters":[],"returntype":"String","safe":true},{"name":"tokens","offset":948,"parameters":[],"returntype":"InteropInterface","safe":true},{"name":"tokensOf","offset":977,"parameters":[{"name":"owner","type":"Hash160"}],"returntype":"InteropInterface","safe":true},{"name":"totalSupply","offset":683,"parameters":[],"returntype":"Integer","safe":true},{"name":"transfer","offset":1039,"parameters":[{"name":"to","type":"Hash160"},{"name":"tokenID","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Boolean","safe":false},{"name":"update","offset":587,"parameters":[{"name":"nef","type":"ByteArray"},{"name":"manifest","type":"String"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"updateSOA","offset":2775,"parameters":[{"name":"name","type":"String"},{"name":"email","type":"String"},{"name":"refresh","type":"Integer"},{"name":"retry","type":"Integer"},{"name":"expire","type":"Integer"},{"name":"ttl","type":"Integer"}],"returntype":"Void","safe":false},{"name":"version","offset":679,"parameters":[],"returntype":"Integer","safe":true}],"events":[{"name":"Transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"tokenId","type":"ByteArray"}]},{"name":"SetAdmin","parameters":[{"name":"name","type":"String"},{"name":"oldAdmin","type":"Hash160"},{"name":"newAdmin","type":"Hash160"}]},{"name":"Renew","parameters":[{"name":"name","type":"String"},{"name":"oldExpiration","type":"Integer"},{"name":"newExpiration","type":"Integer"}]}]},"features":{},"groups":[],"permissions":[{"contract":"0xfffdc93764dbaddd97c48f252a53ea4643faa3fd","methods":["update"]},{"contract":"*","methods":["onNEP11Payment"]}],"supportedstandards":["NEP-11"],"trusts":[],"extra":null} \ No newline at end of file diff --git a/rpc/nns/rpcbinding.go b/rpc/nns/rpcbinding.go index a5c5d26b..c50e19e4 100644 --- a/rpc/nns/rpcbinding.go +++ b/rpc/nns/rpcbinding.go @@ -26,6 +26,20 @@ type NnsNameState struct { Admin util.Uint160 } +// 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 @@ -231,22 +245,44 @@ func (c *Contract) RegisterTLDUnsigned(name string, email string, refresh *big.I // 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) +func (c *Contract) Renew(name string, years *big.Int) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "renew", name, years) } // 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) +func (c *Contract) RenewTransaction(name string, years *big.Int) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "renew", name, years) } // 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) { +func (c *Contract) RenewUnsigned(name string, years *big.Int) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "renew", nil, name, years) +} + +// 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) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "renew", name) +} + +// 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) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "renew", name) +} + +// 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) (*transaction.Transaction, error) { return c.actor.MakeUnsignedCall(c.hash, "renew", nil, name) } @@ -440,3 +476,169 @@ func (res *NnsNameState) FromStackItem(item stackitem.Item) error { return nil } + +// 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/tests/nns_test.go b/tests/nns_test.go index 2d2837b1..444707df 100644 --- a/tests/nns_test.go +++ b/tests/nns_test.go @@ -10,6 +10,7 @@ import ( "time" "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/encoding/address" "github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/util" @@ -354,7 +355,16 @@ func TestNNSSetAdmin(t *testing.T) { "testdomain.com", int64(recordtype.TXT), "won't be added") c1 := c.WithSigners(c.Committee, acc) - c1.Invoke(t, stackitem.Null{}, "setAdmin", "testdomain.com", acc.ScriptHash()) + h := c1.Invoke(t, stackitem.Null{}, "setAdmin", "testdomain.com", acc.ScriptHash()) + c1.CheckTxNotificationEvent(t, h, 0, state.NotificationEvent{ + ScriptHash: c1.Hash, + Name: "SetAdmin", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray([]byte("testdomain.com")), + stackitem.Null{}, + stackitem.NewByteArray(acc.ScriptHash().BytesBE()), + }), + }) expiration := top.Timestamp + uint64(expire*1000) expectedProps := stackitem.NewMapWithValue([]stackitem.MapElement{ @@ -401,16 +411,44 @@ func TestNNSRenew(t *testing.T) { const msPerYear = 365 * 24 * time.Hour / time.Millisecond b := c.TopBlock(t) - ts := b.Timestamp + uint64(expire*1000) + uint64(msPerYear) + renewalPeriod := int64(2) + oldExpiration := b.Timestamp + uint64(expire*1000) + ts := oldExpiration + uint64(msPerYear)*uint64(renewalPeriod) cAcc := c.WithSigners(acc) - cAcc.InvokeFail(t, "not witnessed by admin", "renew", "testdomain.com") - c1.Invoke(t, ts, "renew", "testdomain.com") + cAcc.InvokeFail(t, "not witnessed by admin", "renew", "testdomain.com", renewalPeriod) + h := c1.Invoke(t, ts, "renew", "testdomain.com", renewalPeriod) + cAcc.CheckTxNotificationEvent(t, h, 0, state.NotificationEvent{ + ScriptHash: cAcc.Hash, + Name: "Renew", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray([]byte("testdomain.com")), + stackitem.Make(oldExpiration), + stackitem.Make(ts), + }), + }) expected := stackitem.NewMapWithValue([]stackitem.MapElement{ {Key: stackitem.Make("name"), Value: stackitem.Make("testdomain.com")}, {Key: stackitem.Make("expiration"), Value: stackitem.Make(ts)}, {Key: stackitem.Make("admin"), Value: stackitem.Null{}}}) cAcc.Invoke(t, expected, "properties", "testdomain.com") + + // Invalid renewal period. + c1.InvokeFail(t, "invalid renewal period value", "renew", "testdomain.com", 11) + // Too large expiration period. + c1.InvokeFail(t, "10 years of expiration period at max is allowed", "renew", "testdomain.com", 10) + + // Default renewal period. + h = c1.Invoke(t, ts+uint64(msPerYear), "renew", "testdomain.com") + c1.CheckTxNotificationEvent(t, h, 0, state.NotificationEvent{ + ScriptHash: cAcc.Hash, + Name: "Renew", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray([]byte("testdomain.com")), + stackitem.Make(ts), + stackitem.Make(ts + uint64(msPerYear)), + }), + }) } func TestNNSResolve(t *testing.T) {