Compare commits

...

16 commits

Author SHA1 Message Date
Anna Shaleva
14c9858df2 nns: use MillisecondsInSecond constant where appropriate 2022-09-16 11:21:03 +03:00
Anna Shaleva
d8e0a02a86 nns: keep isAvailable in sync with register
If conflicting records '*.domain' are present on new domain
registration, then `isAvailable` should return false for this
domain. Ref.
f25296b17a.
2022-09-16 11:20:59 +03:00
Anna Shaleva
d5b1c0e429 nns: check for conflicting records on domain registration
Port f25296b17a.
2022-09-16 11:04:58 +03:00
Anna Shaleva
db9cea5ecc nns: allow arbitrary level domains
Port
d10da892d9.
2022-09-16 11:04:55 +03:00
Anna Shaleva
ea934b8e30 nns: support SOA records
Port 7f1ec13ae5
and f4762c1b56.
2022-09-16 10:59:23 +03:00
Anna Shaleva
8790602f69 nns: ensure records with the same type are not repeated
Port https://github.com/nspcc-dev/neofs-contract/pull/170.
2022-09-09 19:36:16 +03:00
Anna Shaleva
c9050cef4b nns: allow multiple records of the same type
Except for the CNAME records. Port
6ea4573ef8
and
f4762c1b56.
2022-09-09 19:36:13 +03:00
Anna Shaleva
c296f8804c nns: add test for getAllRecords 2022-09-09 19:35:54 +03:00
Anna Shaleva
4543de0923 *: update basic test chain
Apply new NNS rules.
2022-09-08 14:19:39 +03:00
Anna Shaleva
d77b35c385 nns: add admin to properties
See 14f43ba8cf/src/NameService/NameService.cs (L69).
2022-09-08 14:19:39 +03:00
Anna Shaleva
225152f2d7 nns: allow to resolve FQDN
Port 4041924a75.
2022-09-08 14:19:39 +03:00
Anna Shaleva
baf24d1c66 nns: check domain expiration for read functions
Port 432c02a369.
2022-09-08 14:19:39 +03:00
Anna Shaleva
017a6b9bc1 nns: require admin signature for subdomain registration
Port
14fc086291.
2022-09-08 14:19:39 +03:00
Anna Shaleva
5cb2a1219c nns: replace root with TLD
Port
4b86891d57.
2022-09-08 14:19:39 +03:00
Anna Shaleva
c11481b119 nns: allow hyphen in domain names
Port https://github.com/nspcc-dev/neofs-contract/pull/183.
2022-09-08 14:19:39 +03:00
Anna Shaleva
bd3722041a nns: adjust maxDomainNameFragmentLength
Port https://github.com/nspcc-dev/neofs-contract/pull/238.
2022-09-08 14:19:39 +03:00
8 changed files with 665 additions and 243 deletions

View file

@ -47,22 +47,27 @@ const (
maxRegisterPrice = 1_0000_0000_0000
// maxRootLength is the maximum domain root length.
maxRootLength = 16
// maxDomainNameFragmentLength is the maximum length of the domain name fragment.
maxDomainNameFragmentLength = 62
// maxDomainNameFragmentLength is the maximum length of the domain name fragment
maxDomainNameFragmentLength = 63
// minDomainNameLength is minimum domain length.
minDomainNameLength = 3
// maxDomainNameLength is maximum domain length.
maxDomainNameLength = 255
// maxTXTRecordLength is the maximum length of the TXT domain record.
maxTXTRecordLength = 255
// maxRecordID is the maximum value of record ID (the upper bound for the number
// of records with the same type).
maxRecordID = 255
)
// Other constants.
const (
// defaultRegisterPrice is the default price for new domain registration.
defaultRegisterPrice = 10_0000_0000
// millisecondsInSecond is the amount of milliseconds per second.
millisecondsInSecond = 1000
// millisecondsInYear is amount of milliseconds per year.
millisecondsInYear = 365 * 24 * 3600 * 1000
millisecondsInYear = 365 * 24 * 3600 * millisecondsInSecond
)
// RecordState is a type that registered entities are saved to.
@ -70,6 +75,7 @@ type RecordState struct {
Name string
Type RecordType
Data string
ID byte
}
// Update updates NameService contract.
@ -118,6 +124,7 @@ func Properties(tokenID []byte) map[string]interface{} {
return map[string]interface{}{
"name": ns.Name,
"expiration": ns.Expiration,
"admin": ns.Admin,
}
}
@ -179,22 +186,6 @@ func Transfer(to interop.Hash160, tokenID []byte, data interface{}) bool {
return true
}
// AddRoot registers new root.
func AddRoot(root string) {
checkCommittee()
if !checkFragment(root, true) {
panic("invalid root format")
}
var (
ctx = storage.GetContext()
rootKey = append([]byte{prefixRoot}, []byte(root)...)
)
if storage.Get(ctx, rootKey) != nil {
panic("root already exists")
}
storage.Put(ctx, rootKey, 0)
}
// Roots returns iterator over a set of NameService roots.
func Roots() iterator.Iterator {
ctx := storage.GetReadOnlyContext()
@ -219,31 +210,94 @@ func GetPrice() int {
// IsAvailable checks whether provided domain name is available.
func IsAvailable(name string) bool {
fragments := splitAndCheck(name, false)
fragments := splitAndCheck(name, true)
if fragments == nil {
panic("invalid domain name format")
}
ctx := storage.GetReadOnlyContext()
if storage.Get(ctx, append([]byte{prefixRoot}, []byte(fragments[1])...)) == nil {
panic("root not found")
l := len(fragments)
if storage.Get(ctx, append([]byte{prefixRoot}, []byte(fragments[l-1])...)) == nil {
if l != 1 {
panic("TLD not found")
}
return true
}
if !parentExpired(ctx, 0, fragments) {
return false
}
return len(getParentConflictingRecord(ctx, name, fragments)) == 0
}
// getPrentConflictingRecord returns record of '*.name' format if they are presented.
// These records conflict with domain name to be registered.
func getParentConflictingRecord(ctx storage.Context, name string, fragments []string) string {
parentKey := getTokenKey([]byte(name[len(fragments[0])+1:]))
parentRecKey := append([]byte{prefixRecord}, parentKey...)
it := storage.Find(ctx, parentRecKey, storage.ValuesOnly|storage.DeserializeValues)
suffix := []byte(name)
for iterator.Next(it) {
r := iterator.Value(it).(RecordState)
ind := std.MemorySearchLastIndex([]byte(r.Name), suffix, len(r.Name))
if ind > 0 && ind+len(suffix) == len(r.Name) {
return r.Name
}
}
return ""
}
// parentExpired returns true if any domain from fragments doesn't exist or expired.
// first denotes the deepest subdomain to check.
func parentExpired(ctx storage.Context, first int, fragments []string) bool {
now := runtime.GetTime()
last := len(fragments) - 1
name := fragments[last]
for i := last; i >= first; i-- {
if i != last {
name = fragments[i] + "." + name
}
nsBytes := storage.Get(ctx, append([]byte{prefixName}, getTokenKey([]byte(name))...))
if nsBytes == nil {
return true
}
ns := std.Deserialize(nsBytes.([]byte)).(NameState)
return runtime.GetTime() >= ns.Expiration
if now >= ns.Expiration {
return true
}
}
return false
}
// Register registers new domain with the specified owner and name if it's available.
func Register(name string, owner interop.Hash160) bool {
fragments := splitAndCheck(name, false)
func Register(name string, owner interop.Hash160, email string, refresh, retry, expire, ttl int) bool {
fragments := splitAndCheck(name, true)
if fragments == nil {
panic("invalid domain name format")
}
l := len(fragments)
tldKey := append([]byte{prefixRoot}, []byte(fragments[l-1])...)
ctx := storage.GetContext()
if storage.Get(ctx, append([]byte{prefixRoot}, []byte(fragments[1])...)) == nil {
panic("root not found")
tldBytes := storage.Get(ctx, tldKey)
if l == 1 {
checkCommittee()
if tldBytes != nil {
panic("TLD already exists")
}
storage.Put(ctx, tldKey, 0)
} else {
if tldBytes == nil {
panic("TLD not found")
}
if parentExpired(ctx, 1, fragments) {
panic("one of the parent domains is not registered")
}
parentKey := getTokenKey([]byte(name[len(fragments[0])+1:]))
nsBytes := storage.Get(ctx, append([]byte{prefixName}, parentKey...))
ns := std.Deserialize(nsBytes.([]byte)).(NameState)
ns.checkAdmin()
if conflict := getParentConflictingRecord(ctx, name, fragments); len(conflict) != 0 {
panic("parent domain has conflicting records: " + conflict)
}
}
if !isValid(owner) {
@ -275,14 +329,62 @@ func Register(name string, owner interop.Hash160) bool {
ns := NameState{
Owner: owner,
Name: name,
Expiration: runtime.GetTime() + millisecondsInYear,
Expiration: runtime.GetTime() + expire*millisecondsInSecond,
}
putNameStateWithKey(ctx, tokenKey, ns)
putSoaRecord(ctx, name, email, refresh, retry, expire, ttl)
updateBalance(ctx, []byte(name), owner, +1)
postTransfer(oldOwner, owner, []byte(name), nil)
return true
}
// UpdateSOA updates soa record.
func UpdateSOA(name, email string, refresh, retry, expire, ttl int) {
if len(name) > maxDomainNameLength {
panic("invalid domain name format")
}
ctx := storage.GetContext()
ns := getNameState(ctx, []byte(name))
ns.checkAdmin()
putSoaRecord(ctx, name, email, refresh, retry, expire, ttl)
}
// putSoaRecord stores SOA domain record.
func putSoaRecord(ctx storage.Context, name, email string, refresh, retry, expire, ttl int) {
data := name + " " + email + " " +
std.Itoa(runtime.GetTime(), 10) + " " +
std.Itoa(refresh, 10) + " " +
std.Itoa(retry, 10) + " " +
std.Itoa(expire, 10) + " " +
std.Itoa(ttl, 10)
tokenId := []byte(tokenIDFromName(ctx, name))
putRecord(ctx, tokenId, name, SOA, 0, data)
}
// updateSoaSerial updates serial of the corresponding SOA domain record.
func updateSoaSerial(ctx storage.Context, tokenId []byte) {
recordKey := getRecordKey(tokenId, string(tokenId), SOA, 0)
recBytes := storage.Get(ctx, recordKey)
if recBytes == nil {
panic("SOA record not found")
}
rec := std.Deserialize(recBytes.([]byte)).(RecordState)
split := std.StringSplitNonEmpty(rec.Data, " ")
if len(split) != 7 {
panic("corrupted SOA record format")
}
split[2] = std.Itoa(runtime.GetTime(), 10) // update serial
rec.Data = split[0] + " " + split[1] + " " +
split[2] + " " + split[3] + " " +
split[4] + " " + split[5] + " " +
split[6]
recBytes = std.Serialize(rec)
storage.Put(ctx, recordKey, recBytes)
}
// Renew increases domain expiration date.
func Renew(name string) int {
if len(name) > maxDomainNameLength {
@ -313,9 +415,46 @@ func SetAdmin(name string, admin interop.Hash160) {
putNameState(ctx, ns)
}
// SetRecord adds new record of the specified type to the provided domain.
func SetRecord(name string, typ RecordType, data string) {
tokenID := []byte(tokenIDFromName(name))
// SetRecord updates record of the specified type and ID.
func SetRecord(name string, typ RecordType, id byte, data string) {
ctx := storage.GetContext()
tokenID := checkRecord(ctx, name, typ, data)
recordKey := getRecordKey(tokenID, name, typ, id)
recBytes := storage.Get(ctx, recordKey)
if recBytes == nil {
panic("unknown record")
}
putRecord(ctx, tokenID, name, typ, id, data)
updateSoaSerial(ctx, tokenID)
}
// AddRecord adds new record of the specified type to the provided domain.
func AddRecord(name string, typ RecordType, data string) {
ctx := storage.GetContext()
tokenID := checkRecord(ctx, name, typ, data)
recordsPrefix := getRecordsByTypePrefix(tokenID, name, typ)
var id byte
records := storage.Find(ctx, recordsPrefix, storage.ValuesOnly|storage.DeserializeValues)
for iterator.Next(records) {
r := iterator.Value(records).(RecordState)
if r.Name == name && r.Type == typ && r.Data == data {
panic("record already exists")
}
id++
}
if id > maxRecordID {
panic("maximum number of records reached")
}
if typ == CNAME && id != 0 {
panic("multiple CNAME records")
}
putRecord(ctx, tokenID, name, typ, id, data)
updateSoaSerial(ctx, tokenID)
}
// checkRecord performs record validness check and returns token ID.
func checkRecord(ctx storage.Context, name string, typ RecordType, data string) []byte {
tokenID := []byte(tokenIDFromName(ctx, name))
var ok bool
switch typ {
case A:
@ -332,44 +471,50 @@ func SetRecord(name string, typ RecordType, data string) {
if !ok {
panic("invalid record data")
}
ctx := storage.GetContext()
ns := getNameState(ctx, tokenID)
ns.checkAdmin()
putRecord(ctx, tokenID, name, typ, data)
return tokenID
}
// GetRecord returns domain record of the specified type if it exists or an empty
// string if not.
func GetRecord(name string, typ RecordType) string {
tokenID := []byte(tokenIDFromName(name))
// GetRecords returns domain records of the specified type if they exist or an empty
// array if not.
func GetRecords(name string, typ RecordType) []string {
ctx := storage.GetReadOnlyContext()
tokenID := []byte(tokenIDFromName(ctx, name))
_ = getNameState(ctx, tokenID) // ensure not expired
return getRecord(ctx, tokenID, name, typ)
return getRecordsByType(ctx, tokenID, name, typ)
}
// DeleteRecord removes domain record with the specified type.
func DeleteRecord(name string, typ RecordType) {
tokenID := []byte(tokenIDFromName(name))
// DeleteRecords removes all domain records with the specified type.
func DeleteRecords(name string, typ RecordType) {
if typ == SOA {
panic("forbidden to delete SOA record")
}
ctx := storage.GetContext()
tokenID := []byte(tokenIDFromName(ctx, name))
ns := getNameState(ctx, tokenID)
ns.checkAdmin()
recordKey := getRecordKey(tokenID, name, typ)
storage.Delete(ctx, recordKey)
recordsPrefix := getRecordsByTypePrefix(tokenID, name, typ)
records := storage.Find(ctx, recordsPrefix, storage.KeysOnly)
for iterator.Next(records) {
key := iterator.Value(records).(string)
storage.Delete(ctx, key)
}
updateSoaSerial(ctx, tokenID)
}
// Resolve resolves given name (not more then three redirects are allowed).
func Resolve(name string, typ RecordType) string {
// Resolve resolves given name (not more than three redirects are allowed) to a set
// of domain records.
func Resolve(name string, typ RecordType) []string {
ctx := storage.GetReadOnlyContext()
return resolve(ctx, name, typ, 2)
res := []string{}
return resolve(ctx, res, name, typ, 2)
}
// GetAllRecords returns an Iterator with RecordState items for given name.
func GetAllRecords(name string) iterator.Iterator {
tokenID := []byte(tokenIDFromName(name))
ctx := storage.GetReadOnlyContext()
_ = getNameState(ctx, tokenID) // ensure not expired
recordsKey := getRecordsKey(tokenID, name)
return storage.Find(ctx, recordsKey, storage.ValuesOnly|storage.DeserializeValues)
return getAllRecords(ctx, name)
}
// updateBalance updates account's balance and account's tokens.
@ -425,12 +570,17 @@ func getTokenKey(tokenID []byte) []byte {
// getNameState returns domain name state by the specified tokenID.
func getNameState(ctx storage.Context, tokenID []byte) NameState {
tokenKey := getTokenKey(tokenID)
return getNameStateWithKey(ctx, tokenKey)
ns := getNameStateWithKey(ctx, tokenKey)
fragments := std.StringSplit(string(tokenID), ".")
if parentExpired(ctx, 1, fragments) {
panic("parent domain has expired")
}
return ns
}
// getNameStateWithKey returns domain name state by the specified token key.
func getNameStateWithKey(ctx storage.Context, tokenKey []byte) NameState {
nameKey := append([]byte{prefixName}, tokenKey...)
nameKey := getNameStateKey(tokenKey)
nsBytes := storage.Get(ctx, nameKey)
if nsBytes == nil {
panic("token not found")
@ -440,6 +590,11 @@ func getNameStateWithKey(ctx storage.Context, tokenKey []byte) NameState {
return ns
}
// getNameStateKey returns NameState key for the provided token key.
func getNameStateKey(tokenKey []byte) []byte {
return append([]byte{prefixName}, tokenKey...)
}
// putNameState stores domain name state.
func putNameState(ctx storage.Context, ns NameState) {
tokenKey := getTokenKey([]byte(ns.Name))
@ -453,41 +608,53 @@ func putNameStateWithKey(ctx storage.Context, tokenKey []byte, ns NameState) {
storage.Put(ctx, nameKey, nsBytes)
}
// getRecord returns domain record.
func getRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType) string {
recordKey := getRecordKey(tokenId, name, typ)
recBytes := storage.Get(ctx, recordKey)
if recBytes == nil {
return recBytes.(string) // A hack to actually return NULL.
// getRecordsByType returns domain records of the specified type or an empty array if no records found.
func getRecordsByType(ctx storage.Context, tokenId []byte, name string, typ RecordType) []string {
recordsPrefix := getRecordsByTypePrefix(tokenId, name, typ)
records := storage.Find(ctx, recordsPrefix, storage.ValuesOnly|storage.DeserializeValues)
res := []string{} // return empty slice if no records was found.
for iterator.Next(records) {
r := iterator.Value(records).(RecordState)
if r.Type == typ {
res = append(res, r.Data)
}
record := std.Deserialize(recBytes.([]byte)).(RecordState)
return record.Data
}
return res
}
// putRecord stores domain record.
func putRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType, record string) {
recordKey := getRecordKey(tokenId, name, typ)
// putRecord puts the specified record to the contract storage without any additional checks.
func putRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType, id byte, data string) {
recordKey := getRecordKey(tokenId, name, typ, id)
rs := RecordState{
Name: name,
Type: typ,
Data: record,
Data: data,
ID: id,
}
recBytes := std.Serialize(rs)
storage.Put(ctx, recordKey, recBytes)
}
// getRecordsKey returns prefix used to store domain records of different types.
func getRecordsKey(tokenId []byte, name string) []byte {
recordKey := append([]byte{prefixRecord}, getTokenKey(tokenId)...)
return append(recordKey, getTokenKey([]byte(name))...)
// getRecordKey returns key used to store domain record with the specified type and ID.
// This key always have a single corresponding value.
func getRecordKey(tokenId []byte, name string, typ RecordType, id byte) []byte {
prefix := getRecordsByTypePrefix(tokenId, name, typ)
return append(prefix, id)
}
// getRecordKey returns key used to store domain records.
func getRecordKey(tokenId []byte, name string, typ RecordType) []byte {
recordKey := getRecordsKey(tokenId, name)
// getRecordsByTypePrefix returns prefix used to store domain records with the
// specified type of different IDs.
func getRecordsByTypePrefix(tokenId []byte, name string, typ RecordType) []byte {
recordKey := getRecordsPrefix(tokenId, name)
return append(recordKey, []byte{byte(typ)}...)
}
// getRecordsPrefix returns prefix used to store domain records of different types.
func getRecordsPrefix(tokenId []byte, name string) []byte {
recordKey := append([]byte{prefixRecord}, getTokenKey(tokenId)...)
return append(recordKey, getTokenKey([]byte(name))...)
}
// isValid returns true if the provided address is a valid Uint160.
func isValid(address interop.Hash160) bool {
return address != nil && len(address) == 20
@ -507,6 +674,8 @@ func checkCommittee() {
}
// checkFragment validates root or a part of domain name.
// 1. Root domain must start with a letter.
// 2. All other fragments must start and end in a letter or a digit.
func checkFragment(v string, isRoot bool) bool {
maxLength := maxDomainNameFragmentLength
if isRoot {
@ -525,12 +694,12 @@ func checkFragment(v string, isRoot bool) bool {
return false
}
}
for i := 1; i < len(v); i++ {
if !isAlNum(v[i]) {
for i := 1; i < len(v)-1; i++ {
if v[i] != '-' && !isAlNum(v[i]) {
return false
}
}
return true
return isAlNum(v[len(v)-1])
}
// isAlNum checks whether provided char is a lowercase letter or a number.
@ -546,9 +715,6 @@ func splitAndCheck(name string, allowMultipleFragments bool) []string {
}
fragments := std.StringSplit(name, ".")
l = len(fragments)
if l < 2 {
return nil
}
if l > 2 && !allowMultipleFragments {
return nil
}
@ -671,48 +837,66 @@ func checkIPv6(data string) bool {
}
// tokenIDFromName returns token ID (domain.root) from provided name.
func tokenIDFromName(name string) string {
func tokenIDFromName(ctx storage.Context, name string) string {
fragments := splitAndCheck(name, true)
if fragments == nil {
panic("invalid domain name format")
}
l := len(fragments)
return name[len(name)-(len(fragments[l-1])+len(fragments[l-2])+1):]
sum := 0
for i := 0; i < len(fragments)-1; i++ {
tokenKey := getTokenKey([]byte(name[sum:]))
nameKey := getNameStateKey(tokenKey)
nsBytes := storage.Get(ctx, nameKey)
if nsBytes != nil {
ns := std.Deserialize(nsBytes.([]byte)).(NameState)
if runtime.GetTime() < ns.Expiration {
return name[sum:]
}
}
sum += len(fragments[i]) + 1
}
return name
}
// resolve resolves provided name using record with the specified type and given
// maximum redirections constraint.
func resolve(ctx storage.Context, name string, typ RecordType, redirect int) string {
func resolve(ctx storage.Context, res []string, name string, typ RecordType, redirect int) []string {
if redirect < 0 {
panic("invalid redirect")
}
records := getRecords(ctx, name)
if len(name) == 0 {
panic("invalid name")
}
if name[len(name)-1] == '.' {
name = name[:len(name)-1]
}
records := getAllRecords(ctx, name)
cname := ""
for iterator.Next(records) {
r := iterator.Value(records).(struct {
key string
rs RecordState
})
value := r.rs.Data
rTyp := r.key[len(r.key)-1]
if rTyp == byte(typ) {
return value
r := iterator.Value(records).(RecordState)
if r.Type == typ {
res = append(res, r.Data)
}
if rTyp == byte(CNAME) {
cname = value
if r.Type == CNAME {
cname = r.Data
}
}
if cname == "" {
return string([]byte(nil))
}
return resolve(ctx, cname, typ, redirect-1)
if cname == "" || typ == CNAME {
return res
}
// getRecords returns iterator over the set of records corresponded with the
// specified name.
func getRecords(ctx storage.Context, name string) iterator.Iterator {
tokenID := []byte(tokenIDFromName(name))
_ = getNameState(ctx, tokenID)
recordsKey := getRecordsKey(tokenID, name)
return storage.Find(ctx, recordsKey, storage.DeserializeValues)
// TODO: the line below must be removed from the neofs nns:
// res = append(res, cname)
// @roman-khimov, it is done in a separate commit in neofs-contracts repo, is it OK?
return resolve(ctx, res, cname, typ, redirect-1)
}
// getAllRecords returns iterator over the set of records corresponded with the
// specified name. Records returned are of different types and/or different IDs.
// No keys are returned.
func getAllRecords(ctx storage.Context, name string) iterator.Iterator {
tokenID := []byte(tokenIDFromName(ctx, name))
_ = getNameState(ctx, tokenID) // ensure not expired.
recordsPrefix := getRecordsPrefix(tokenID, name)
return storage.Find(ctx, recordsPrefix, storage.ValuesOnly|storage.DeserializeValues)
}

View file

@ -2,7 +2,7 @@ name: "NameService"
sourceurl: https://github.com/nspcc-dev/neo-go/
supportedstandards: ["NEP-11"]
safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf",
"tokens", "properties", "roots", "getPrice", "isAvailable", "getRecord",
"tokens", "properties", "roots", "getPrice", "isAvailable", "getRecords",
"resolve", "getAllRecords"]
events:
- name: Transfer

View file

@ -1,6 +1,8 @@
package nns_test
import (
"math/big"
"strconv"
"strings"
"testing"
@ -9,18 +11,30 @@ import (
"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/smartcontract"
"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 {
const (
millisecondsInYear = 365 * 24 * 3600 * 1000
maxDomainNameFragmentLength = 63
)
func newNSClient(t *testing.T, registerComTLD bool) *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)
ctr := neotest.CompileFile(t, e.CommitteeHash, ".", "nns.yml")
e.DeployContract(t, ctr, nil)
return e.CommitteeInvoker(c.Hash)
c := e.CommitteeInvoker(ctr.Hash)
if registerComTLD {
// Set expiration big enough to pass all tests.
mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
c.Invoke(t, true, "register", "com", c.CommitteeHash, mail, refresh, retry, expire, ttl)
}
return c
}
func TestNameService_Price(t *testing.T) {
@ -29,7 +43,7 @@ func TestNameService_Price(t *testing.T) {
maxPrice = int64(10000_00000000)
)
c := newNSClient(t)
c := newNSClient(t, false)
t.Run("set, not signed by committee", func(t *testing.T) {
acc := c.NewAccount(t)
@ -62,7 +76,7 @@ func TestNameService_Price(t *testing.T) {
}
func TestNonfungible(t *testing.T) {
c := newNSClient(t)
c := newNSClient(t, false)
c.Signers = []neotest.Signer{c.NewAccount(t)}
c.Invoke(t, "NNS", "symbol")
@ -70,104 +84,109 @@ func TestNonfungible(t *testing.T) {
c.Invoke(t, 0, "totalSupply")
}
func TestAddRoot(t *testing.T) {
c := newNSClient(t)
func TestRegisterTLD(t *testing.T) {
c := newNSClient(t, false)
mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
t.Run("invalid format", func(t *testing.T) {
c.InvokeFail(t, "invalid root format", "addRoot", "")
c.InvokeFail(t, "invalid domain name format", "register", "", c.CommitteeHash, mail, refresh, retry, expire, ttl)
})
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.InvokeFail(t, "not witnessed by committee", "register", "some", c.CommitteeHash, mail, refresh, retry, expire, ttl)
})
c.Invoke(t, stackitem.Null{}, "addRoot", "some")
c.Invoke(t, true, "register", "some", c.CommitteeHash, mail, refresh, retry, expire, ttl)
t.Run("already exists", func(t *testing.T) {
c.InvokeFail(t, "already exists", "addRoot", "some")
c.InvokeFail(t, "TLD already exists", "register", "some", c.CommitteeHash, mail, refresh, retry, expire, ttl)
})
}
func TestExpiration(t *testing.T) {
c := newNSClient(t)
c := newNSClient(t, true)
e := c.Executor
bc := e.Chain
mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
acc := e.NewAccount(t)
cAcc := c.WithSigners(acc)
cAccCommittee := c.WithSigners(acc, c.Committee) // acc + committee signers for ".com"'s subdomains registration
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")
cAccCommittee.Invoke(t, true, "register", "first.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl)
cAcc.Invoke(t, stackitem.Null{}, "addRecord", "first.com", int64(nns.TXT), "sometext")
b1 := e.TopBlock(t)
tx := cAcc.PrepareInvoke(t, "register", "second.com", acc.ScriptHash())
tx := cAccCommittee.PrepareInvoke(t, "register", "second.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl)
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))
b3 := e.NewUnsignedBlock(t)
b3.Index = b2.Index + 1
b3.PrevHash = b2.Hash()
b3.Timestamp = b1.Timestamp + (uint64(expire) * 1000)
require.NoError(t, bc.AddBlock(e.SignBlock(b3)))
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, "isAvailable", "first.com") // "first.com" has been expired
cAcc.Invoke(t, true, "isAvailable", "second.com") // TLD "com" has been expired
cAcc.InvokeFail(t, "name has expired", "getRecords", "first.com", int64(nns.TXT))
cAcc.Invoke(t, true, "register", "first.com", acc.ScriptHash()) // Re-register.
cAcc.Invoke(t, stackitem.Null{}, "resolve", "first.com", int64(nns.TXT))
// TODO: According to the new code, we can't re-register expired "com" TLD, because it's already registered; at the
// same time we can't renew it because it's already expired. We likely need to change this logic in the contract and
// after that uncomment the lines below.
// c.Invoke(t, true, "renew", "com")
// 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
func TestRegisterAndRenew(t *testing.T) {
c := newNSClient(t)
c := newNSClient(t, false)
e := c.Executor
mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
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.InvokeFail(t, "TLD not found", "isAvailable", "neo.com")
c.Invoke(t, true, "register", "org", c.CommitteeHash, mail, refresh, retry, expire, ttl)
c.InvokeFail(t, "TLD not found", "isAvailable", "neo.com")
c.Invoke(t, true, "register", "com", c.CommitteeHash, mail, refresh, retry, expire, ttl)
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)
c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo.org", e.CommitteeHash, mail, refresh, retry, expire, ttl)
c.InvokeFail(t, "one of the parent domains is not registered", "register", "docs.neo.org", e.CommitteeHash, mail, refresh, retry, expire, ttl)
c.InvokeFail(t, "invalid domain name format", "register", "\nneo.com'", e.CommitteeHash, mail, refresh, retry, expire, ttl)
c.InvokeFail(t, "invalid domain name format", "register", "neo.com\n", e.CommitteeHash, mail, refresh, retry, expire, ttl)
c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo.org", e.CommitteeHash, mail, refresh, retry, expire, ttl)
c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceDomainPrice, "register", "neo.com", e.CommitteeHash, mail, refresh, retry, expire, ttl)
var maxLenFragment string
for i := 0; i < maxDomainNameFragmentLength; i++ {
maxLenFragment += "q"
}
c.Invoke(t, true, "isAvailable", maxLenFragment+".com")
c.Invoke(t, true, "register", maxLenFragment+".com", e.CommitteeHash, mail, refresh, retry, expire, ttl)
c.InvokeFail(t, "invalid domain name format", "register", maxLenFragment+"q.com", e.CommitteeHash, mail, refresh, retry, expire, ttl)
c.Invoke(t, true, "isAvailable", "neo.com")
c.Invoke(t, 0, "balanceOf", e.CommitteeHash)
c.Invoke(t, true, "register", "neo.com", e.CommitteeHash)
c.Invoke(t, 3, "balanceOf", e.CommitteeHash) // org, com, qqq...qqq.com
c.Invoke(t, true, "register", "neo.com", e.CommitteeHash, mail, refresh, retry, expire, ttl)
topBlock := e.TopBlock(t)
expectedExpiration := topBlock.Timestamp + millisecondsInYear
c.Invoke(t, false, "register", "neo.com", e.CommitteeHash)
expectedExpiration := topBlock.Timestamp + uint64(expire*1000)
c.Invoke(t, false, "register", "neo.com", e.CommitteeHash, mail, refresh, retry, expire, ttl)
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, mail, refresh, retry, expire, ttl)
c.InvokeFail(t, "invalid domain name format", "register", "testdomain-.com", e.CommitteeHash, mail, refresh, retry, expire, ttl)
c.Invoke(t, true, "register", "test-domain.com", e.CommitteeHash, mail, refresh, retry, expire, ttl)
})
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, 1, "balanceOf", e.CommitteeHash)
c.Invoke(t, 5, "balanceOf", e.CommitteeHash) // org, com, qqq...qqq.com, neo.com, test-domain.com
c.Invoke(t, e.CommitteeHash.BytesBE(), "ownerOf", []byte("neo.com"))
t.Run("invalid token ID", func(t *testing.T) {
@ -185,42 +204,59 @@ func TestRegisterAndRenew(t *testing.T) {
c.Invoke(t, props, "properties", "neo.com")
}
func TestSetGetRecord(t *testing.T) {
c := newNSClient(t)
func TestSetAddGetRecord(t *testing.T) {
c := newNSClient(t, true)
e := c.Executor
mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
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.InvokeFail(t, "token not found", "addRecord", "neo.com", int64(nns.TXT), "sometext")
})
c.Invoke(t, true, "register", "neo.com", e.CommitteeHash)
c.Invoke(t, true, "register", "neo.com", e.CommitteeHash, mail, refresh, retry, expire, ttl)
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")
c.InvokeFail(t, "unsupported record type", "addRecord", "neo.com", int64(0xFF), "1.2.3.4")
c.InvokeFail(t, "invalid record", "addRecord", "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")
cAcc.InvokeFail(t, "not witnessed by admin", "addRecord", "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")
c.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "getRecords", "neo.com", int64(nns.A))
c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4")
c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("1.2.3.4")}), "getRecords", "neo.com", int64(nns.A))
c.InvokeFail(t, "record already exists", "addRecord", "neo.com", int64(nns.A), "1.2.3.4") // Duplicating record.
c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("1.2.3.4")}), "getRecords", "neo.com", int64(nns.A))
c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.AAAA), "2001:0201:1f1f:0000:0000:0100:11a0:11df")
c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.CNAME), "nspcc.ru")
c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext")
// Add multiple records and update some of them.
c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext1")
c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext2")
c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext3")
c.Invoke(t, stackitem.NewArray([]stackitem.Item{
stackitem.Make("sometext"),
stackitem.Make("sometext1"),
stackitem.Make("sometext2"),
stackitem.Make("sometext3"),
}), "getRecords", "neo.com", int64(nns.TXT))
c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), 2, "sometext22")
c.Invoke(t, stackitem.NewArray([]stackitem.Item{
stackitem.Make("sometext"),
stackitem.Make("sometext1"),
stackitem.Make("sometext22"),
stackitem.Make("sometext3"),
}), "getRecords", "neo.com", int64(nns.TXT))
// Delete record.
t.Run("invalid witness", func(t *testing.T) {
cAcc.InvokeFail(t, "not witnessed by admin", "deleteRecord", "neo.com", int64(nns.CNAME))
cAcc.InvokeFail(t, "not witnessed by admin", "deleteRecords", "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))
c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("nspcc.ru")}), "getRecords", "neo.com", int64(nns.CNAME))
c.Invoke(t, stackitem.Null{}, "deleteRecords", "neo.com", int64(nns.CNAME))
c.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "getRecords", "neo.com", int64(nns.CNAME))
c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("1.2.3.4")}), "getRecords", "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
@ -280,9 +316,10 @@ func TestSetGetRecord(t *testing.T) {
args := []interface{}{"neo.com", int64(testCase.Type), testCase.Name}
t.Run(testCase.Name, func(t *testing.T) {
if testCase.ShouldFail {
c.InvokeFail(t, "", "setRecord", args...)
c.InvokeFail(t, "", "addRecord", args...)
} else {
c.Invoke(t, stackitem.Null{}, "setRecord", args...)
c.Invoke(t, stackitem.Null{}, "addRecord", args...)
c.Invoke(t, stackitem.Null{}, "deleteRecords", "neo.com", int64(testCase.Type)) // clear records after test to avoid duplicating records.
}
})
}
@ -290,19 +327,21 @@ func TestSetGetRecord(t *testing.T) {
}
func TestSetAdmin(t *testing.T) {
c := newNSClient(t)
c := newNSClient(t, true)
e := c.Executor
mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
owner := e.NewAccount(t)
cOwner := c.WithSigners(owner)
cOwnerCommittee := c.WithSigners(owner, c.Committee)
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())
cOwner.InvokeFail(t, "not witnessed by admin", "register", "neo.com", owner.ScriptHash(), mail, refresh, retry, expire, ttl) // admin is committee
cOwnerCommittee.Invoke(t, true, "register", "neo.com", owner.ScriptHash(), mail, refresh, retry, expire, ttl)
expectedExpiration := e.TopBlock(t).Timestamp + uint64(expire)*1000
cGuest.InvokeFail(t, "not witnessed", "setAdmin", "neo.com", admin.ScriptHash())
// Must be witnessed by both owner and admin.
@ -310,36 +349,42 @@ func TestSetAdmin(t *testing.T) {
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))
cAdmin.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "sometext")
cGuest.InvokeFail(t, "not witnessed by admin", "deleteRecords", "neo.com", int64(nns.TXT))
cAdmin.Invoke(t, stackitem.Null{}, "deleteRecords", "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")
cAdmin.Invoke(t, stackitem.Null{}, "addRecord", "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))
cAdmin.InvokeFail(t, "not witnessed by admin", "deleteRecords", "neo.com", int64(nns.TXT))
})
}
func TestTransfer(t *testing.T) {
c := newNSClient(t)
c := newNSClient(t, true)
e := c.Executor
mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
from := e.NewAccount(t)
cFrom := c.WithSigners(from)
cFromCommittee := c.WithSigners(from, c.Committee)
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")
cFromCommittee.Invoke(t, true, "register", "neo.com", from.ScriptHash(), mail, refresh, retry, expire, ttl)
cFrom.Invoke(t, stackitem.Null{}, "addRecord", "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, 2, "totalSupply") // com, neo.com
cFrom.Invoke(t, to.ScriptHash().BytesBE(), "ownerOf", "neo.com")
// without onNEP11Transfer
@ -358,30 +403,32 @@ func TestTransfer(t *testing.T) {
&compiler.Options{Name: "foo"})
e.DeployContract(t, ctr, nil)
cTo.Invoke(t, true, "transfer", ctr.Hash, []byte("neo.com"), nil)
cFrom.Invoke(t, 1, "totalSupply")
cFrom.Invoke(t, 2, "totalSupply") // com, neo.com
cFrom.Invoke(t, ctr.Hash.BytesBE(), "ownerOf", []byte("neo.com"))
}
func TestTokensOf(t *testing.T) {
c := newNSClient(t)
c := newNSClient(t, false)
e := c.Executor
mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
acc1 := e.NewAccount(t)
cAcc1 := c.WithSigners(acc1)
cAcc1Committee := c.WithSigners(acc1, c.Committee)
acc2 := e.NewAccount(t)
cAcc2 := c.WithSigners(acc2)
cAcc2Committee := c.WithSigners(acc2, c.Committee)
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())
tld := []byte("com")
c.Invoke(t, true, "register", tld, c.CommitteeHash, mail, refresh, retry, expire, ttl)
cAcc1Committee.Invoke(t, true, "register", "neo.com", acc1.ScriptHash(), mail, refresh, retry, expire, ttl)
cAcc2Committee.Invoke(t, true, "register", "nspcc.com", acc2.ScriptHash(), mail, refresh, retry, expire, ttl)
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
testTokensOf(t, c, tld, [][]byte{[]byte("neo.com")}, acc1.ScriptHash().BytesBE())
testTokensOf(t, c, tld, [][]byte{[]byte("nspcc.com")}, acc2.ScriptHash().BytesBE())
testTokensOf(t, c, tld, [][]byte{[]byte("neo.com"), []byte("nspcc.com")})
testTokensOf(t, c, tld, [][]byte{}, util.Uint160{}.BytesBE()) // empty hash is a valid hash still
}
func testTokensOf(t *testing.T, c *neotest.ContractInvoker, result [][]byte, args ...interface{}) {
func testTokensOf(t *testing.T, c *neotest.ContractInvoker, tld []byte, result [][]byte, args ...interface{}) {
method := "tokensOf"
if len(args) == 0 {
method = "tokens"
@ -399,31 +446,195 @@ func testTokensOf(t *testing.T, c *neotest.ContractInvoker, result [][]byte, arg
require.Equal(t, result[i], iter.Value().Value())
arr = append(arr, stackitem.Make(result[i]))
}
if method == "tokens" {
require.True(t, iter.Next())
require.Equal(t, tld, iter.Value().Value())
} else {
require.False(t, iter.Next())
}
}
func TestResolve(t *testing.T) {
c := newNSClient(t)
c := newNSClient(t, true)
e := c.Executor
mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
acc := e.NewAccount(t)
cAcc := c.WithSigners(acc)
cAccCommittee := c.WithSigners(acc, c.Committee)
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")
cAccCommittee.Invoke(t, true, "register", "neo.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl)
cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4")
cAcc.Invoke(t, stackitem.Null{}, "addRecord", "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")
cAccCommittee.Invoke(t, true, "register", "alias.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl)
cAcc.Invoke(t, stackitem.Null{}, "addRecord", "alias.com", int64(nns.TXT), "sometxt from alias1")
cAcc.Invoke(t, stackitem.Null{}, "addRecord", "alias.com", int64(nns.CNAME), "alias2.com")
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, stackitem.Null{}, "resolve", "neo.com", int64(nns.AAAA))
cAccCommittee.Invoke(t, true, "register", "alias2.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl)
cAcc.Invoke(t, stackitem.Null{}, "addRecord", "alias2.com", int64(nns.TXT), "sometxt from alias2")
c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("1.2.3.4")}), "resolve", "neo.com", int64(nns.A))
c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("1.2.3.4")}), "resolve", "neo.com.", int64(nns.A))
c.InvokeFail(t, "invalid domain name format", "resolve", "neo.com..", int64(nns.A))
// Check CNAME is properly resolved and is not included into the result.
c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("sometxt from alias1"), stackitem.Make("sometxt from alias2")}), "resolve", "neo.com", int64(nns.TXT))
// Check CNAME is included into the result and is not resolved.
c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("alias.com")}), "resolve", "neo.com", int64(nns.CNAME))
// Empty result.
c.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "resolve", "neo.com", int64(nns.AAAA))
}
func TestGetAllRecords(t *testing.T) {
c := newNSClient(t, true)
e := c.Executor
mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
acc := e.NewAccount(t)
cAcc := c.WithSigners(acc)
cAccCommittee := c.WithSigners(acc, c.Committee)
cAccCommittee.Invoke(t, true, "register", "neo.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl)
cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4")
cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.CNAME), "alias.com")
cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), "bla0")
cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), 0, "bla1") // overwrite
time := e.TopBlock(t).Timestamp
// Add some arbitrary data.
cAccCommittee.Invoke(t, true, "register", "alias.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl)
cAcc.Invoke(t, stackitem.Null{}, "addRecord", "alias.com", int64(nns.TXT), "sometxt")
script, err := smartcontract.CreateCallAndUnwrapIteratorScript(c.Hash, "getAllRecords", 10, "neo.com")
require.NoError(t, err)
h := e.InvokeScript(t, script, []neotest.Signer{acc})
e.CheckHalt(t, h, stackitem.NewArray([]stackitem.Item{
stackitem.NewStruct([]stackitem.Item{
stackitem.NewByteArray([]byte("neo.com")),
stackitem.Make(nns.A),
stackitem.NewByteArray([]byte("1.2.3.4")),
stackitem.NewBigInteger(big.NewInt(0)),
}),
stackitem.NewStruct([]stackitem.Item{
stackitem.NewByteArray([]byte("neo.com")),
stackitem.Make(nns.CNAME),
stackitem.NewByteArray([]byte("alias.com")),
stackitem.NewBigInteger(big.NewInt(0)),
}),
stackitem.NewStruct([]stackitem.Item{
stackitem.NewByteArray([]byte("neo.com")),
stackitem.Make(nns.SOA),
stackitem.NewBuffer([]byte("neo.com" + " " + mail + " " +
strconv.Itoa(int(time)) + " " + strconv.Itoa(int(refresh)) + " " +
strconv.Itoa(int(retry)) + " " + strconv.Itoa(int(expire)) + " " +
strconv.Itoa(int(ttl)))),
stackitem.NewBigInteger(big.NewInt(0)),
}),
stackitem.NewStruct([]stackitem.Item{
stackitem.NewByteArray([]byte("neo.com")),
stackitem.Make(nns.TXT),
stackitem.NewByteArray([]byte("bla1")),
stackitem.NewBigInteger(big.NewInt(0)),
}),
}))
}
func TestGetRecords(t *testing.T) {
c := newNSClient(t, true)
e := c.Executor
mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
acc := e.NewAccount(t)
cAcc := c.WithSigners(acc)
cAccCommittee := c.WithSigners(acc, c.Committee)
cAccCommittee.Invoke(t, true, "register", "neo.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl)
cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4")
cAcc.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.CNAME), "alias.com")
// Add some arbitrary data.
cAccCommittee.Invoke(t, true, "register", "alias.com", acc.ScriptHash(), mail, refresh, retry, expire, ttl)
cAcc.Invoke(t, stackitem.Null{}, "addRecord", "alias.com", int64(nns.TXT), "sometxt")
c.Invoke(t, stackitem.NewArray([]stackitem.Item{stackitem.Make("1.2.3.4")}), "getRecords", "neo.com", int64(nns.A))
// Check empty result of `getRecords`.
c.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "getRecords", "neo.com", int64(nns.AAAA))
}
func TestNNSAddRecord(t *testing.T) {
c := newNSClient(t, true)
cAccCommittee := c.WithSigners(c.Committee)
mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
cAccCommittee.Invoke(t, true, "register", "neo.com", c.CommitteeHash, mail, refresh, retry, expire, ttl)
for i := 0; i <= maxRecordID+1; i++ {
if i == maxRecordID+1 {
c.InvokeFail(t, "maximum number of records reached", "addRecord", "neo.com", int64(nns.TXT), strconv.Itoa(i))
} else {
c.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.TXT), strconv.Itoa(i))
}
}
}
func TestNNSRegisterArbitraryLevelDomain(t *testing.T) {
c := newNSClient(t, true)
newArgs := func(domain string, account neotest.Signer) []interface{} {
return []interface{}{
domain, account.ScriptHash(), "doesnt@matter.com",
int64(101), int64(102), int64(103), int64(104),
}
}
acc := c.NewAccount(t)
cBoth := c.WithSigners(c.Committee, acc)
args := newArgs("neo.com", acc)
cBoth.Invoke(t, true, "register", args...)
c1 := c.WithSigners(acc)
// parent domain is missing
args[0] = "testnet.fs.neo.com"
c1.InvokeFail(t, "one of the parent domains is not registered", "register", args...)
args[0] = "fs.neo.com"
c1.Invoke(t, true, "register", args...)
args[0] = "testnet.fs.neo.com"
c1.Invoke(t, true, "register", args...)
acc2 := c.NewAccount(t)
c2 := c.WithSigners(c.Committee, acc2)
args = newArgs("mainnet.fs.neo.com", acc2)
c2.InvokeFail(t, "not witnessed by admin", "register", args...)
c1.Invoke(t, stackitem.Null{}, "addRecord",
"something.mainnet.fs.neo.com", int64(nns.A), "1.2.3.4")
c1.Invoke(t, stackitem.Null{}, "addRecord",
"another.fs.neo.com", int64(nns.A), "4.3.2.1")
c2 = c.WithSigners(acc, acc2)
c2.Invoke(t, stackitem.NewBool(false), "isAvailable", "mainnet.fs.neo.com")
c2.InvokeFail(t, "parent domain has conflicting records: something.mainnet.fs.neo.com",
"register", args...)
c1.Invoke(t, stackitem.Null{}, "deleteRecords",
"something.mainnet.fs.neo.com", int64(nns.A))
c2.Invoke(t, stackitem.NewBool(true), "isAvailable", "mainnet.fs.neo.com")
c2.Invoke(t, true, "register", args...)
c2 = c.WithSigners(acc2)
c2.Invoke(t, stackitem.Null{}, "addRecord",
"cdn.mainnet.fs.neo.com", int64(nns.A), "166.15.14.13")
result := stackitem.NewArray([]stackitem.Item{
stackitem.NewByteArray([]byte("166.15.14.13")),
})
c2.Invoke(t, result, "resolve", "cdn.mainnet.fs.neo.com", int64(nns.A))
}
const (
defaultNameServiceDomainPrice = 10_0000_0000
defaultNameServiceSysfee = 6000_0000
maxRecordID = 255
)

View file

@ -9,6 +9,8 @@ const (
A RecordType = 1
// CNAME represents canonical name record type.
CNAME RecordType = 5
// SOA represents start of authority record type.
SOA RecordType = 6
// TXT represents text record type.
TXT RecordType = 16
)

View file

@ -21,7 +21,10 @@ import (
"github.com/stretchr/testify/require"
)
const neoAmount = 99999000
const (
neoAmount = 99999000
millisecondsInYear = 365 * 24 * 3600 * 1000
)
// Init pushes some predefined set of transactions into the given chain, it needs a path to
// the root project directory.
@ -158,17 +161,19 @@ func Init(t *testing.T, rootpath string, e *neotest.Executor) {
_, _, nsHash := deployContractFromPriv0(t, nsPath, nsPath, nsConfigPath, 4) // block #11
nsCommitteeInvoker := e.CommitteeInvoker(nsHash)
nsPriv0Invoker := e.NewInvoker(nsHash, acc0)
nsPriv0CommitteeInvoker := e.NewInvoker(nsHash, acc0, e.Committee)
// Block #12: transfer funds to committee for further NS record registration.
gasValidatorInvoker.Invoke(t, true, "transfer",
e.Validator.ScriptHash(), e.Committee.ScriptHash(), 1000_00000000, nil) // block #12
// Block #13: add `.com` root to NNS.
nsCommitteeInvoker.Invoke(t, stackitem.Null{}, "addRoot", "com") // block #13
mail, refresh, retry, expire, ttl := "sami@nspcc.ru", int64(101), int64(102), int64(millisecondsInYear/1000*100), int64(104)
nsCommitteeInvoker.Invoke(t, true, "register", "com", nsCommitteeInvoker.CommitteeHash, mail, refresh, retry, expire, ttl) // block #13
// Block #14: register `neo.com` via NNS.
registerTxH := nsPriv0Invoker.Invoke(t, true, "register",
"neo.com", priv0ScriptHash) // block #14
registerTxH := nsPriv0CommitteeInvoker.Invoke(t, true, "register",
"neo.com", priv0ScriptHash, mail, refresh, retry, expire, ttl) // block #14
res := e.GetTxExecResult(t, registerTxH)
require.Equal(t, 1, len(res.Events)) // transfer
tokenID, err := res.Events[0].Item.Value().([]stackitem.Item)[3].TryBytes()
@ -176,7 +181,7 @@ func Init(t *testing.T, rootpath string, e *neotest.Executor) {
t.Logf("NNS token #1 ID (hex): %s", hex.EncodeToString(tokenID))
// Block #15: set A record type with priv0 owner via NNS.
nsPriv0Invoker.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4") // block #15
nsPriv0Invoker.Invoke(t, stackitem.Null{}, "addRecord", "neo.com", int64(nns.A), "1.2.3.4") // block #15
// Block #16: invoke `test_contract.go`: put new value with the same key to check `getstate` RPC call
txPutNewValue := rublPriv0Invoker.PrepareInvoke(t, "putValue", "testkey", "newtestvalue") // tx1

View file

@ -1370,7 +1370,7 @@ func TestClient_NEP11_ND(t *testing.T) {
t.Run("TotalSupply", func(t *testing.T) {
s, err := n11.TotalSupply()
require.NoError(t, err)
require.EqualValues(t, big.NewInt(1), s) // the only `neo.com` of acc0
require.EqualValues(t, big.NewInt(2), s) // `neo.com` of acc0 and TLD `com` of committee
})
t.Run("Symbol", func(t *testing.T) {
sym, err := n11.Symbol()
@ -1403,14 +1403,14 @@ func TestClient_NEP11_ND(t *testing.T) {
require.NoError(t, err)
items, err := iter.Next(config.DefaultMaxIteratorResultItems)
require.NoError(t, err)
require.Equal(t, 1, len(items))
require.Equal(t, [][]byte{[]byte("neo.com")}, items)
require.Equal(t, 2, len(items))
require.Equal(t, [][]byte{[]byte("neo.com"), []byte("com")}, items)
require.NoError(t, iter.Terminate())
})
t.Run("TokensExpanded", func(t *testing.T) {
items, err := n11.TokensExpanded(config.DefaultMaxIteratorResultItems)
require.NoError(t, err)
require.Equal(t, [][]byte{[]byte("neo.com")}, items)
require.Equal(t, [][]byte{[]byte("neo.com"), []byte("com")}, items)
})
t.Run("Properties", func(t *testing.T) {
p, err := n11.Properties([]byte("neo.com"))
@ -1421,6 +1421,7 @@ func TestClient_NEP11_ND(t *testing.T) {
expected := stackitem.NewMap()
expected.Add(stackitem.Make([]byte("name")), stackitem.Make([]byte("neo.com")))
expected.Add(stackitem.Make([]byte("expiration")), stackitem.Make(blockRegisterDomain.Timestamp+365*24*3600*1000)) // expiration formula
expected.Add(stackitem.Make([]byte("admin")), stackitem.Null{})
require.EqualValues(t, expected, p)
})
t.Run("Transfer", func(t *testing.T) {

View file

@ -74,12 +74,12 @@ const (
verifyContractHash = "06ed5314c2e4cb103029a60b86d46afa2fb8f67c"
verifyContractAVM = "VwIAQS1RCDBwDBTunqIsJ+NL0BSPxBCOCPdOj1BIskrZMCQE2zBxaBPOStkoJATbKGlK2SgkBNsol0A="
verifyWithArgsContractHash = "0dce75f52adb1a4c5c6eaa6a34eb26db2e5b3781"
nnsContractHash = "bdbfe1a280a0e23ca5b569c8f5845169bd93cb06"
nnsContractHash = "cb93bcab0d6d435b61fa96a3bbce3b6f043968b5"
nnsToken1ID = "6e656f2e636f6d"
nfsoContractHash = "0e15ca0df00669a2cd5dcb03bfd3e2b3849c2969"
nfsoToken1ID = "7e244ffd6aa85fb1579d2ed22e9b761ab62e3486"
invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA"
block20StateRootLE = "f1380226a217b5e35ea968d42c50e20b9af7ab83b91416c8fb85536c61004332"
block20StateRootLE = "7f80c7e265a44faa7374953d4d5059d21b34e65e06a7695d57ca8c59cc9a36fa"
storageContractHash = "ebc0c16a76c808cd4dde6bcc063f09e45e331ec7"
)
@ -287,6 +287,7 @@ var rpcTestCases = map[string][]rpcTestCase{
return &map[string]interface{}{
"name": "neo.com",
"expiration": "lhbLRl0B",
"admin": nil, // no admin was set
}
},
},
@ -935,7 +936,7 @@ var rpcTestCases = map[string][]rpcTestCase{
chg := []dboper.Operation{{
State: "Changed",
Key: []byte{0xfa, 0xff, 0xff, 0xff, 0xb},
Value: []byte{0xf6, 0x8b, 0x4e, 0x9d, 0x51, 0x79, 0x12},
Value: []byte{0x6e, 0xaf, 0xba, 0x5e, 0x51, 0x79, 0x12},
}, {
State: "Added",
Key: []byte{0xfb, 0xff, 0xff, 0xff, 0x14, 0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0xb},
@ -947,7 +948,7 @@ var rpcTestCases = map[string][]rpcTestCase{
}, {
State: "Changed",
Key: []byte{0xfa, 0xff, 0xff, 0xff, 0x14, 0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x8, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2},
Value: []byte{0x41, 0x01, 0x21, 0x05, 0xe4, 0x74, 0xef, 0xdb, 0x08},
Value: []byte{0x41, 0x01, 0x21, 0x05, 0xda, 0xb5, 0x8c, 0xda, 0x08},
}}
// Can be returned in any order.
assert.ElementsMatch(t, chg, res.Diagnostics.Changes)
@ -963,7 +964,7 @@ var rpcTestCases = map[string][]rpcTestCase{
cryptoHash, _ := e.chain.GetNativeContractScriptHash(nativenames.CryptoLib)
return &result.Invoke{
State: "HALT",
GasConsumed: 15928320,
GasConsumed: 22192980,
Script: script,
Stack: []stackitem.Item{stackitem.Make("1.2.3.4")},
Notifications: []state.NotificationEvent{},
@ -975,6 +976,15 @@ var rpcTestCases = map[string][]rpcTestCase{
{
Current: nnsHash,
Calls: []*invocations.Tree{
{
Current: stdHash,
},
{
Current: cryptoHash,
},
{
Current: stdHash,
},
{
Current: stdHash,
},
@ -1078,7 +1088,7 @@ var rpcTestCases = map[string][]rpcTestCase{
cryptoHash, _ := e.chain.GetNativeContractScriptHash(nativenames.CryptoLib)
return &result.Invoke{
State: "HALT",
GasConsumed: 15928320,
GasConsumed: 22192980,
Script: script,
Stack: []stackitem.Item{stackitem.Make("1.2.3.4")},
Notifications: []state.NotificationEvent{},
@ -1090,6 +1100,15 @@ var rpcTestCases = map[string][]rpcTestCase{
{
Current: nnsHash,
Calls: []*invocations.Tree{
{
Current: stdHash,
},
{
Current: cryptoHash,
},
{
Current: stdHash,
},
{
Current: stdHash,
},
@ -2717,7 +2736,7 @@ func checkNep17Balances(t *testing.T, e *executor, acc interface{}) {
},
{
Asset: e.chain.UtilityTokenHash(),
Amount: "37099660700",
Amount: "37076412050",
LastUpdated: 22,
Decimals: 8,
Name: "GasToken",

Binary file not shown.