package nns_test import ( "strings" "testing" nns "github.com/nspcc-dev/neo-go/examples/nft-nd-nns" "github.com/nspcc-dev/neo-go/pkg/compiler" "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" "github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/neotest/chain" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" ) func newNSClient(t *testing.T) *neotest.ContractInvoker { bc, acc := chain.NewSingle(t) e := neotest.NewExecutor(t, bc, acc, acc) c := neotest.CompileFile(t, e.CommitteeHash, ".", "nns.yml") e.DeployContract(t, c, nil) return e.CommitteeInvoker(c.Hash) } func TestNameService_Price(t *testing.T) { const ( minPrice = int64(0) maxPrice = int64(10000_00000000) ) c := newNSClient(t) t.Run("set, not signed by committee", func(t *testing.T) { acc := c.NewAccount(t) cAcc := c.WithSigners(acc) cAcc.InvokeFail(t, "not witnessed by committee", "setPrice", minPrice+1) }) t.Run("get, default value", func(t *testing.T) { c.Invoke(t, defaultNameServiceDomainPrice, "getPrice") }) t.Run("set, too small value", func(t *testing.T) { c.InvokeFail(t, "The price is out of range.", "setPrice", minPrice-1) }) t.Run("set, too large value", func(t *testing.T) { c.InvokeFail(t, "The price is out of range.", "setPrice", maxPrice+1) }) t.Run("set, success", func(t *testing.T) { txSet := c.PrepareInvoke(t, "setPrice", int64(defaultNameServiceDomainPrice+1)) txGet := c.PrepareInvoke(t, "getPrice") c.AddBlockCheckHalt(t, txSet, txGet) c.CheckHalt(t, txSet.Hash(), stackitem.Null{}) c.CheckHalt(t, txGet.Hash(), stackitem.Make(defaultNameServiceDomainPrice+1)) // Get in the next block. c.Invoke(t, stackitem.Make(defaultNameServiceDomainPrice+1), "getPrice") }) } func TestNonfungible(t *testing.T) { c := newNSClient(t) c.Signers = []neotest.Signer{c.NewAccount(t)} c.Invoke(t, "NNS", "symbol") c.Invoke(t, 0, "decimals") c.Invoke(t, 0, "totalSupply") } func TestAddRoot(t *testing.T) { c := newNSClient(t) t.Run("invalid format", func(t *testing.T) { c.InvokeFail(t, "invalid root format", "addRoot", "") }) t.Run("not signed by committee", func(t *testing.T) { acc := c.NewAccount(t) c := c.WithSigners(acc) c.InvokeFail(t, "not witnessed by committee", "addRoot", "some") }) c.Invoke(t, stackitem.Null{}, "addRoot", "some") t.Run("already exists", func(t *testing.T) { c.InvokeFail(t, "already exists", "addRoot", "some") }) } func TestExpiration(t *testing.T) { c := newNSClient(t) e := c.Executor bc := e.Chain acc := e.NewAccount(t) cAcc := c.WithSigners(acc) c.Invoke(t, stackitem.Null{}, "addRoot", "com") cAcc.Invoke(t, true, "register", "first.com", acc.ScriptHash()) cAcc.Invoke(t, stackitem.Null{}, "setRecord", "first.com", int64(nns.TXT), "sometext") b1 := e.TopBlock(t) tx := cAcc.PrepareInvoke(t, "register", "second.com", acc.ScriptHash()) b2 := e.NewUnsignedBlock(t, tx) b2.Index = b1.Index + 1 b2.PrevHash = b1.Hash() b2.Timestamp = b1.Timestamp + 10000 require.NoError(t, bc.AddBlock(e.SignBlock(b2))) e.CheckHalt(t, tx.Hash()) tx = cAcc.PrepareInvoke(t, "isAvailable", "first.com") b3 := e.NewUnsignedBlock(t, tx) b3.Index = b2.Index + 1 b3.PrevHash = b2.Hash() b3.Timestamp = b1.Timestamp + (millisecondsInYear + 1) require.NoError(t, bc.AddBlock(e.SignBlock(b3))) e.CheckHalt(t, tx.Hash(), stackitem.NewBool(true)) tx = cAcc.PrepareInvoke(t, "isAvailable", "second.com") b4 := e.NewUnsignedBlock(t, tx) b4.Index = b3.Index + 1 b4.PrevHash = b3.Hash() b4.Timestamp = b3.Timestamp + 1000 require.NoError(t, bc.AddBlock(e.SignBlock(b4))) e.CheckHalt(t, tx.Hash(), stackitem.NewBool(false)) tx = cAcc.PrepareInvoke(t, "getRecord", "first.com", int64(nns.TXT)) b5 := e.NewUnsignedBlock(t, tx) b5.Index = b4.Index + 1 b5.PrevHash = b4.Hash() b5.Timestamp = b4.Timestamp + 1000 require.NoError(t, bc.AddBlock(e.SignBlock(b5))) e.CheckFault(t, tx.Hash(), "name has expired") cAcc.Invoke(t, true, "register", "first.com", acc.ScriptHash()) // Re-register. cAcc.Invoke(t, stackitem.Null{}, "resolve", "first.com", int64(nns.TXT)) } const ( millisecondsInYear = 365 * 24 * 3600 * 1000 maxDomainNameFragmentLength = 63 ) func TestRegisterAndRenew(t *testing.T) { c := newNSClient(t) e := c.Executor c.InvokeFail(t, "root not found", "isAvailable", "neo.com") c.Invoke(t, stackitem.Null{}, "addRoot", "org") c.InvokeFail(t, "root not found", "isAvailable", "neo.com") c.Invoke(t, stackitem.Null{}, "addRoot", "com") c.Invoke(t, true, "isAvailable", "neo.com") c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo.org", e.CommitteeHash) c.InvokeFail(t, "invalid domain name format", "register", "docs.neo.org", e.CommitteeHash) c.InvokeFail(t, "invalid domain name format", "register", "\nneo.com'", e.CommitteeHash) c.InvokeFail(t, "invalid domain name format", "register", "neo.com\n", e.CommitteeHash) c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo.org", e.CommitteeHash) c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceDomainPrice, "register", "neo.com", e.CommitteeHash) var maxLenFragment string for range maxDomainNameFragmentLength { maxLenFragment += "q" } c.Invoke(t, true, "isAvailable", maxLenFragment+".com") c.Invoke(t, true, "register", maxLenFragment+".com", e.CommitteeHash) c.InvokeFail(t, "invalid domain name format", "register", maxLenFragment+"q.com", e.CommitteeHash) c.Invoke(t, true, "isAvailable", "neo.com") c.Invoke(t, 1, "balanceOf", e.CommitteeHash) c.Invoke(t, true, "register", "neo.com", e.CommitteeHash) topBlock := e.TopBlock(t) expectedExpiration := topBlock.Timestamp + millisecondsInYear c.Invoke(t, false, "register", "neo.com", e.CommitteeHash) c.Invoke(t, false, "isAvailable", "neo.com") t.Run("domain names with hyphen", func(t *testing.T) { c.InvokeFail(t, "invalid domain name format", "register", "-testdomain.com", e.CommitteeHash) c.InvokeFail(t, "invalid domain name format", "register", "testdomain-.com", e.CommitteeHash) c.Invoke(t, true, "register", "test-domain.com", e.CommitteeHash) }) props := stackitem.NewMap() props.Add(stackitem.Make("name"), stackitem.Make("neo.com")) props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration)) props.Add(stackitem.Make("admin"), stackitem.Null{}) // no admin was set c.Invoke(t, props, "properties", "neo.com") c.Invoke(t, 3, "balanceOf", e.CommitteeHash) c.Invoke(t, e.CommitteeHash.BytesBE(), "ownerOf", []byte("neo.com")) t.Run("invalid token ID", func(t *testing.T) { c.InvokeFail(t, "token not found", "properties", "not.exists") c.InvokeFail(t, "token not found", "ownerOf", "not.exists") c.InvokeFail(t, "invalid conversion", "properties", []any{}) c.InvokeFail(t, "invalid conversion", "ownerOf", []any{}) }) // Renew expectedExpiration += millisecondsInYear c.Invoke(t, expectedExpiration, "renew", "neo.com") props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration)) c.Invoke(t, props, "properties", "neo.com") } func TestSetGetRecord(t *testing.T) { c := newNSClient(t) e := c.Executor acc := e.NewAccount(t) cAcc := c.WithSigners(acc) c.Invoke(t, stackitem.Null{}, "addRoot", "com") t.Run("set before register", func(t *testing.T) { c.InvokeFail(t, "token not found", "setRecord", "neo.com", int64(nns.TXT), "sometext") }) c.Invoke(t, true, "register", "neo.com", e.CommitteeHash) t.Run("invalid parameters", func(t *testing.T) { c.InvokeFail(t, "unsupported record type", "setRecord", "neo.com", int64(0xFF), "1.2.3.4") c.InvokeFail(t, "invalid record", "setRecord", "neo.com", int64(nns.A), "not.an.ip.address") }) t.Run("invalid witness", func(t *testing.T) { cAcc.InvokeFail(t, "not witnessed by admin", "setRecord", "neo.com", int64(nns.A), "1.2.3.4") }) c.Invoke(t, stackitem.Null{}, "getRecord", "neo.com", int64(nns.A)) c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") c.Invoke(t, "1.2.3.4", "getRecord", "neo.com", int64(nns.A)) c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") c.Invoke(t, "1.2.3.4", "getRecord", "neo.com", int64(nns.A)) c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.AAAA), "2001:0201:1f1f:0000:0000:0100:11a0:11df") c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.CNAME), "nspcc.ru") c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "sometext") // Delete record. t.Run("invalid witness", func(t *testing.T) { cAcc.InvokeFail(t, "not witnessed by admin", "deleteRecord", "neo.com", int64(nns.CNAME)) }) c.Invoke(t, "nspcc.ru", "getRecord", "neo.com", int64(nns.CNAME)) c.Invoke(t, stackitem.Null{}, "deleteRecord", "neo.com", int64(nns.CNAME)) c.Invoke(t, stackitem.Null{}, "getRecord", "neo.com", int64(nns.CNAME)) c.Invoke(t, "1.2.3.4", "getRecord", "neo.com", int64(nns.A)) t.Run("SetRecord_compatibility", func(t *testing.T) { // tests are got from the NNS C# implementation and changed accordingly to non-native implementation behavior testCases := []struct { Type nns.RecordType Name string ShouldFail bool }{ {Type: nns.A, Name: "0.0.0.0", ShouldFail: true}, {Type: nns.A, Name: "1.1.0.1"}, {Type: nns.A, Name: "10.10.10.10", ShouldFail: true}, {Type: nns.A, Name: "255.255.255.255", ShouldFail: true}, {Type: nns.A, Name: "192.168.1.1", ShouldFail: true}, {Type: nns.A, Name: "1a", ShouldFail: true}, {Type: nns.A, Name: "256.0.0.0", ShouldFail: true}, {Type: nns.A, Name: "01.01.01.01", ShouldFail: true}, {Type: nns.A, Name: "00.0.0.0", ShouldFail: true}, {Type: nns.A, Name: "0.0.0.-1", ShouldFail: true}, {Type: nns.A, Name: "0.0.0.0.1", ShouldFail: true}, {Type: nns.A, Name: "11111111.11111111.11111111.11111111", ShouldFail: true}, {Type: nns.A, Name: "11111111.11111111.11111111.11111111", ShouldFail: true}, {Type: nns.A, Name: "ff.ff.ff.ff", ShouldFail: true}, {Type: nns.A, Name: "0.0.256", ShouldFail: true}, {Type: nns.A, Name: "0.0.0", ShouldFail: true}, {Type: nns.A, Name: "0.257", ShouldFail: true}, {Type: nns.A, Name: "1.1", ShouldFail: true}, {Type: nns.A, Name: "257", ShouldFail: true}, {Type: nns.A, Name: "1", ShouldFail: true}, // {2000} & {2001} & ]2002, 3ffe[ & {3fff} are valid values for IPv6 fragment0 {Type: nns.AAAA, Name: "2002:db8::8:800:200c:417a", ShouldFail: true}, {Type: nns.AAAA, Name: "3ffd:1b8::8:800:200c:417a"}, {Type: nns.AAAA, Name: "3ffd::101"}, {Type: nns.AAAA, Name: "2003::1"}, {Type: nns.AAAA, Name: "2003::"}, {Type: nns.AAAA, Name: "2002:db8:0:0:8:800:200c:417a", ShouldFail: true}, {Type: nns.AAAA, Name: "3ffd:db8:0:0:8:800:200c:417a"}, {Type: nns.AAAA, Name: "3ffd:0:0:0:0:0:0:101"}, {Type: nns.AAAA, Name: "2002:0:0:0:0:0:0:101", ShouldFail: true}, {Type: nns.AAAA, Name: "3ffd:0:0:0:0:0:0:101"}, {Type: nns.AAAA, Name: "2001:200:0:0:0:0:0:1"}, {Type: nns.AAAA, Name: "0:0:0:0:0:0:0:1", ShouldFail: true}, {Type: nns.AAAA, Name: "2002:0:0:0:0:0:0:1", ShouldFail: true}, {Type: nns.AAAA, Name: "2001:200:0:0:0:0:0:0"}, {Type: nns.AAAA, Name: "2002:0:0:0:0:0:0:0", ShouldFail: true}, {Type: nns.AAAA, Name: "2002:DB8::8:800:200C:417A", ShouldFail: true}, {Type: nns.AAAA, Name: "3FFD:1B8::8:800:200C:417A"}, {Type: nns.AAAA, Name: "3FFD::101"}, {Type: nns.AAAA, Name: "3fFD::101"}, {Type: nns.AAAA, Name: "2002:DB8:0:0:8:800:200C:417A", ShouldFail: true}, {Type: nns.AAAA, Name: "3FFD:DB8:0:0:8:800:200C:417A"}, {Type: nns.AAAA, Name: "3FFD:0:0:0:0:0:0:101"}, {Type: nns.AAAA, Name: "3FFD::ffff:1.01.1.01", ShouldFail: true}, {Type: nns.AAAA, Name: "2001:DB8:0:0:8:800:200C:4Z", ShouldFail: true}, {Type: nns.AAAA, Name: "2001::13.1.68.3", ShouldFail: true}, } for _, testCase := range testCases { args := []any{"neo.com", int64(testCase.Type), testCase.Name} t.Run(testCase.Name, func(t *testing.T) { if testCase.ShouldFail { c.InvokeFail(t, "", "setRecord", args...) } else { c.Invoke(t, stackitem.Null{}, "setRecord", args...) } }) } }) } func TestSetAdmin(t *testing.T) { c := newNSClient(t) e := c.Executor owner := e.NewAccount(t) cOwner := c.WithSigners(owner) admin := e.NewAccount(t) cAdmin := c.WithSigners(admin) guest := e.NewAccount(t) cGuest := c.WithSigners(guest) c.Invoke(t, stackitem.Null{}, "addRoot", "com") cOwner.Invoke(t, true, "register", "neo.com", owner.ScriptHash()) expectedExpiration := e.TopBlock(t).Timestamp + millisecondsInYear cGuest.InvokeFail(t, "not witnessed", "setAdmin", "neo.com", admin.ScriptHash()) // Must be witnessed by both owner and admin. cOwner.InvokeFail(t, "not witnessed by admin", "setAdmin", "neo.com", admin.ScriptHash()) cAdmin.InvokeFail(t, "not witnessed by owner", "setAdmin", "neo.com", admin.ScriptHash()) cc := c.WithSigners(owner, admin) cc.Invoke(t, stackitem.Null{}, "setAdmin", "neo.com", admin.ScriptHash()) props := stackitem.NewMap() props.Add(stackitem.Make("name"), stackitem.Make("neo.com")) props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration)) props.Add(stackitem.Make("admin"), stackitem.Make(admin.ScriptHash().BytesBE())) c.Invoke(t, props, "properties", "neo.com") t.Run("set and delete by admin", func(t *testing.T) { cAdmin.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "sometext") cGuest.InvokeFail(t, "not witnessed by admin", "deleteRecord", "neo.com", int64(nns.TXT)) cAdmin.Invoke(t, stackitem.Null{}, "deleteRecord", "neo.com", int64(nns.TXT)) }) t.Run("set admin to null", func(t *testing.T) { cAdmin.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "sometext") cOwner.Invoke(t, stackitem.Null{}, "setAdmin", "neo.com", nil) cAdmin.InvokeFail(t, "not witnessed by admin", "deleteRecord", "neo.com", int64(nns.TXT)) }) } func TestTransfer(t *testing.T) { c := newNSClient(t) e := c.Executor from := e.NewAccount(t) cFrom := c.WithSigners(from) to := e.NewAccount(t) cTo := c.WithSigners(to) c.Invoke(t, stackitem.Null{}, "addRoot", "com") cFrom.Invoke(t, true, "register", "neo.com", from.ScriptHash()) cFrom.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") cFrom.InvokeFail(t, "token not found", "transfer", to.ScriptHash(), "not.exists", nil) c.Invoke(t, false, "transfer", to.ScriptHash(), "neo.com", nil) cFrom.Invoke(t, true, "transfer", to.ScriptHash(), "neo.com", nil) cFrom.Invoke(t, 1, "totalSupply") cFrom.Invoke(t, to.ScriptHash().BytesBE(), "ownerOf", "neo.com") e.DisableCoverage() // contracts below have no source files which leads to unprocessable coverage data // without onNEP11Transfer ctr := neotest.CompileSource(t, e.CommitteeHash, strings.NewReader(`package foo func Main() int { return 0 }`), &compiler.Options{Name: "foo"}) e.DeployContract(t, ctr, nil) cTo.InvokeFail(t, "method not found", "transfer", ctr.Hash, []byte("neo.com"), nil) // with onNEP11Transfer ctr = neotest.CompileSource(t, e.CommitteeHash, strings.NewReader(`package foo import "github.com/nspcc-dev/neo-go/pkg/interop" func OnNEP11Payment(from interop.Hash160, amount int, token []byte, data any) {}`), &compiler.Options{Name: "foo"}) e.DeployContract(t, ctr, nil) e.EnableCoverage() // contracts above have no source files which leads to unprocessable coverage data cTo.Invoke(t, true, "transfer", ctr.Hash, []byte("neo.com"), nil) cFrom.Invoke(t, 1, "totalSupply") cFrom.Invoke(t, ctr.Hash.BytesBE(), "ownerOf", []byte("neo.com")) } func TestTokensOf(t *testing.T) { c := newNSClient(t) e := c.Executor acc1 := e.NewAccount(t) cAcc1 := c.WithSigners(acc1) acc2 := e.NewAccount(t) cAcc2 := c.WithSigners(acc2) c.Invoke(t, stackitem.Null{}, "addRoot", "com") cAcc1.Invoke(t, true, "register", "neo.com", acc1.ScriptHash()) cAcc2.Invoke(t, true, "register", "nspcc.com", acc2.ScriptHash()) testTokensOf(t, c, [][]byte{[]byte("neo.com")}, acc1.ScriptHash().BytesBE()) testTokensOf(t, c, [][]byte{[]byte("nspcc.com")}, acc2.ScriptHash().BytesBE()) testTokensOf(t, c, [][]byte{[]byte("neo.com"), []byte("nspcc.com")}) testTokensOf(t, c, [][]byte{}, util.Uint160{}.BytesBE()) // empty hash is a valid hash still } func testTokensOf(t *testing.T, c *neotest.ContractInvoker, result [][]byte, args ...any) { method := "tokensOf" if len(args) == 0 { method = "tokens" } s, err := c.TestInvoke(t, method, args...) if result == nil { require.Error(t, err) return } require.NoError(t, err) iter := s.Pop().Interop().Value().(*storage.Iterator) for i := range result { require.True(t, iter.Next()) require.Equal(t, result[i], iter.Value().Value()) } require.False(t, iter.Next()) } func TestResolve(t *testing.T) { c := newNSClient(t) e := c.Executor acc := e.NewAccount(t) cAcc := c.WithSigners(acc) c.Invoke(t, stackitem.Null{}, "addRoot", "com") cAcc.Invoke(t, true, "register", "neo.com", acc.ScriptHash()) cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.CNAME), "alias.com") cAcc.Invoke(t, true, "register", "alias.com", acc.ScriptHash()) cAcc.Invoke(t, stackitem.Null{}, "setRecord", "alias.com", int64(nns.TXT), "sometxt") c.Invoke(t, "1.2.3.4", "resolve", "neo.com", int64(nns.A)) c.Invoke(t, "alias.com", "resolve", "neo.com", int64(nns.CNAME)) c.Invoke(t, "sometxt", "resolve", "neo.com", int64(nns.TXT)) c.Invoke(t, "sometxt", "resolve", "neo.com.", int64(nns.TXT)) c.InvokeFail(t, "invalid domain name format", "resolve", "neo.com..", int64(nns.TXT)) c.Invoke(t, stackitem.Null{}, "resolve", "neo.com", int64(nns.AAAA)) } const ( defaultNameServiceDomainPrice = 10_0000_0000 defaultNameServiceSysfee = 6000_0000 )