Merge pull request #1965 from nspcc-dev/native/remove_nns

native: remove NNS contract
This commit is contained in:
Roman Khimov 2021-05-17 22:51:58 +03:00 committed by GitHub
commit e0779f2d6e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1318 additions and 2214 deletions

View file

@ -34,8 +34,8 @@ func TestNEP11Import(t *testing.T) {
walletPath := path.Join(tmpDir, "walletForImport.json")
defer os.Remove(walletPath)
nnsContractHash, err := e.Chain.GetNativeContractScriptHash(nativenames.NameService)
require.NoError(t, err)
// deploy NFT NeoNameService contract
nnsContractHash := deployNNSContract(t, e)
neoContractHash, err := e.Chain.GetNativeContractScriptHash(nativenames.Neo)
require.NoError(t, err)
e.Run(t, "neo-go", "wallet", "init", "--wallet", walletPath)
@ -325,3 +325,7 @@ func TestNEP11_OwnerOf_BalanceOf_Transfer(t *testing.T) {
func deployNFTContract(t *testing.T, e *executor) util.Uint160 {
return deployContract(t, e, "../examples/nft-nd/nft.go", "../examples/nft-nd/nft.yml", nftOwnerWallet, nftOwnerAddr, nftOwnerPass)
}
func deployNNSContract(t *testing.T, e *executor) util.Uint160 {
return deployContract(t, e, "../examples/nft-nd-nns/", "../examples/nft-nd-nns/nns.yml", validatorWallet, validatorAddr, "one")
}

View file

@ -268,8 +268,7 @@ func TestNEP17ImportToken(t *testing.T) {
require.NoError(t, err)
gasContractHash, err := e.Chain.GetNativeContractScriptHash(nativenames.Gas)
require.NoError(t, err)
nnsContractHash, err := e.Chain.GetNativeContractScriptHash(nativenames.NameService)
require.NoError(t, err)
nnsContractHash := deployNNSContract(t, e)
e.Run(t, "neo-go", "wallet", "init", "--wallet", walletPath)
// missing token hash

View file

@ -31,7 +31,6 @@ ProtocolConfiguration:
PolicyContract: [0]
RoleManagement: [0]
OracleContract: [0]
NameService: [0]
ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -27,7 +27,6 @@ ProtocolConfiguration:
PolicyContract: [0]
RoleManagement: [0]
OracleContract: [0]
NameService: [0]
ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -27,7 +27,6 @@ ProtocolConfiguration:
PolicyContract: [0]
RoleManagement: [0]
OracleContract: [0]
NameService: [0]
ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -21,7 +21,6 @@ ProtocolConfiguration:
PolicyContract: [0]
RoleManagement: [0]
OracleContract: [0]
NameService: [0]
ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -27,7 +27,6 @@ ProtocolConfiguration:
PolicyContract: [0]
RoleManagement: [0]
OracleContract: [0]
NameService: [0]
ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -27,7 +27,6 @@ ProtocolConfiguration:
PolicyContract: [0]
RoleManagement: [0]
OracleContract: [0]
NameService: [0]
ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -27,7 +27,6 @@ ProtocolConfiguration:
PolicyContract: [0]
RoleManagement: [0]
OracleContract: [0]
NameService: [0]
ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -31,7 +31,6 @@ ProtocolConfiguration:
PolicyContract: [0]
RoleManagement: [0]
OracleContract: [0]
NameService: [0]
ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file.

View file

@ -19,7 +19,6 @@ ProtocolConfiguration:
PolicyContract: [0]
RoleManagement: [0]
OracleContract: [0]
NameService: [0]
Notary: [0]
ApplicationConfiguration:

View file

@ -28,7 +28,6 @@ ProtocolConfiguration:
PolicyContract: [0]
RoleManagement: [0]
OracleContract: [0]
NameService: [0]
Notary: [0]
ApplicationConfiguration:

View file

@ -26,6 +26,7 @@ See the table below for the detailed examples description.
| [events](events) | The contract shows how execution notifications with the different arguments types can be sent with the help of `runtime.Notify` function of the `runtime` interop package. Please, refer to the `runtime.Notify` [function documentation](../pkg/interop/runtime/runtime.go) for details. |
| [iterator](iterator) | This example describes a way to work with NEO iterators. Please, refer to the `iterator` [package documentation](../pkg/interop/iterator/iterator.go) for details. |
| [nft-nd](nft-nd) | NEP-11 non-divisible NFT. See NEP-11 token standard [specification](https://github.com/neo-project/proposals/pull/130) for details. |
| [nft-nd-nns](nft-nd-nns) | Neo Name Service contract which is NEP-11 non-divisible NFT. The contract implements methods for Neo domain name system managing such as domains registration/transferring, records addition and names resolving. |
| [oracle](oracle) | Oracle demo contract exposing two methods that you can use to process URLs. It uses oracle native contract, see [interop package documentation](../pkg/interop/native/oracle/oracle.go) also. |
| [runtime](runtime) | This contract demonstrates how to use special `_initialize` and `_deploy` methods. See the [compiler documentation](../docs/compiler.md#vm-api-interop-layer ) for methods details. It also shows the pattern for checking owner witness inside the contract with the help of `runtime.CheckWitness` interop [function](../pkg/interop/runtime/runtime.go). |
| [storage](storage) | The contract implements API for basic operations with a contract storage. It shows hos to use `storage` interop package. See the `storage` [package documentation](../pkg/interop/storage/storage.go). |

View file

@ -0,0 +1,31 @@
package nns
import (
"github.com/nspcc-dev/neo-go/pkg/interop"
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
)
// NameState represents domain name state.
type NameState struct {
Owner interop.Hash160
Name string
Expiration int
Admin interop.Hash160
}
// ensureNotExpired panics if domain name is expired.
func (n NameState) ensureNotExpired() {
if runtime.GetTime() >= n.Expiration {
panic("name has expired")
}
}
// checkAdmin panics if script container is not signed by the domain name admin.
func (n NameState) checkAdmin() {
if runtime.CheckWitness(n.Owner) {
return
}
if n.Admin == nil || !runtime.CheckWitness(n.Admin) {
panic("not witnessed by admin")
}
}

650
examples/nft-nd-nns/nns.go Normal file
View file

@ -0,0 +1,650 @@
/*
Package nns contains non-divisible non-fungible NEP11-compatible token
implementation. This token is a compatible analogue of C# Neo Name Service
token and is aimed to serve as a domain name service for Neo smart-contracts,
thus it's NeoNameService. This token can be minted with new domain name
registration, the domain name itself is your NFT. Corresponding domain root
must be added by the committee before new domain name can be registered.
*/
package nns
import (
"github.com/nspcc-dev/neo-go/pkg/interop"
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
"github.com/nspcc-dev/neo-go/pkg/interop/iterator"
"github.com/nspcc-dev/neo-go/pkg/interop/native/crypto"
"github.com/nspcc-dev/neo-go/pkg/interop/native/management"
"github.com/nspcc-dev/neo-go/pkg/interop/native/neo"
"github.com/nspcc-dev/neo-go/pkg/interop/native/std"
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
"github.com/nspcc-dev/neo-go/pkg/interop/util"
)
// Prefixes used for contract data storage.
const (
// prefixTotalSupply contains total supply of minted domains.
prefixTotalSupply byte = 0x00
// prefixBalance contains map from owner to his balance.
prefixBalance byte = 0x01
// prefixAccountToken contains map from (owner + token key) to token ID,
// where token key = hash160(token ID) and token ID = domain name.
prefixAccountToken byte = 0x02
// prefixRegisterPrice contains price for new domain name registration.
prefixRegisterPrice byte = 0x10
// prefixRoot contains set of roots (map from root to 0).
prefixRoot byte = 0x20
// prefixName contains map from token key to token where token is domain
// NameState structure.
prefixName byte = 0x21
// prefixRecord contains map from (token key + hash160(token name) + record type)
// to record.
prefixRecord byte = 0x22
)
// Values constraints.
const (
// maxRegisterPrice is the maximum price of register method.
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
// 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
)
// Other constants.
const (
// defaultRegisterPrice is the default price for new domain registration.
defaultRegisterPrice = 10_0000_0000
// millisecondsInYear is amount of milliseconds per year.
millisecondsInYear = 365 * 24 * 3600 * 1000
)
// Update updates NameService contract.
func Update(nef []byte, manifest string) {
checkCommittee()
management.Update(nef, []byte(manifest))
}
// _deploy initializes defaults (total supply and registration price) on contract deploy.
func _deploy(data interface{}, isUpdate bool) {
if isUpdate {
return
}
ctx := storage.GetContext()
storage.Put(ctx, []byte{prefixTotalSupply}, 0)
storage.Put(ctx, []byte{prefixRegisterPrice}, defaultRegisterPrice)
}
// Symbol returns NeoNameService symbol.
func Symbol() string {
return "NNS"
}
// Decimals returns NeoNameService decimals.
func Decimals() int {
return 0
}
// TotalSupply returns overall number of domains minted by the NeoNameService contract.
func TotalSupply() int {
ctx := storage.GetReadOnlyContext()
return getTotalSupply(ctx)
}
// OwnerOf returns owner of the specified domain.
func OwnerOf(tokenID []byte) interop.Hash160 {
ctx := storage.GetReadOnlyContext()
ns := getNameState(ctx, tokenID)
return ns.Owner
}
// Properties returns domain name and expiration date of the specified domain.
func Properties(tokenID []byte) map[string]interface{} {
ctx := storage.GetReadOnlyContext()
ns := getNameState(ctx, tokenID)
return map[string]interface{}{
"name": ns.Name,
"expiration": ns.Expiration,
}
}
// BalanceOf returns overall number of domains owned by the specified owner.
func BalanceOf(owner interop.Hash160) int {
if !isValid(owner) {
panic(`invalid owner`)
}
ctx := storage.GetReadOnlyContext()
balance := storage.Get(ctx, append([]byte{prefixBalance}, owner...))
if balance == nil {
return 0
}
return balance.(int)
}
// Tokens returns iterator over a set of all registered domain names.
func Tokens() iterator.Iterator {
ctx := storage.GetReadOnlyContext()
return storage.Find(ctx, []byte{prefixName}, storage.ValuesOnly|storage.DeserializeValues|storage.PickField1)
}
// TokensOf returns iterator over minted domains owned by the specified owner.
func TokensOf(owner interop.Hash160) iterator.Iterator {
if !isValid(owner) {
panic(`invalid owner`)
}
ctx := storage.GetReadOnlyContext()
return storage.Find(ctx, append([]byte{prefixAccountToken}, owner...), storage.ValuesOnly)
}
// Transfer transfers domain with the specified name to new owner.
func Transfer(to interop.Hash160, tokenID []byte, data interface{}) bool {
if !isValid(to) {
panic(`invalid receiver`)
}
var (
tokenKey = getTokenKey(tokenID)
ctx = storage.GetContext()
)
ns := getNameStateWithKey(ctx, tokenKey)
from := ns.Owner
if !runtime.CheckWitness(from) {
return false
}
if !util.Equals(from, to) {
// update token info
ns.Owner = to
ns.Admin = nil
putNameStateWithKey(ctx, tokenKey, ns)
// update `from` balance
updateBalance(ctx, tokenID, from, -1)
// update `to` balance
updateBalance(ctx, tokenID, to, +1)
}
postTransfer(from, to, tokenID, data)
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()
return storage.Find(ctx, []byte{prefixRoot}, storage.KeysOnly|storage.RemovePrefix)
}
// SetPrice sets the domain registration price.
func SetPrice(price int) {
checkCommittee()
if price < 0 || price > maxRegisterPrice {
panic("The price is out of range.")
}
ctx := storage.GetContext()
storage.Put(ctx, []byte{prefixRegisterPrice}, price)
}
// GetPrice returns the domain registration price.
func GetPrice() int {
ctx := storage.GetReadOnlyContext()
return storage.Get(ctx, []byte{prefixRegisterPrice}).(int)
}
// IsAvailable checks whether provided domain name is available.
func IsAvailable(name string) bool {
fragments := splitAndCheck(name, false)
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")
}
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
}
// 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)
if fragments == nil {
panic("invalid domain name format")
}
ctx := storage.GetContext()
if storage.Get(ctx, append([]byte{prefixRoot}, []byte(fragments[1])...)) == nil {
panic("root not found")
}
if !isValid(owner) {
panic("invalid owner")
}
if !runtime.CheckWitness(owner) {
panic("not witnessed by owner")
}
runtime.BurnGas(GetPrice())
var (
tokenKey = getTokenKey([]byte(name))
oldOwner interop.Hash160
)
nsBytes := storage.Get(ctx, append([]byte{prefixName}, tokenKey...))
if nsBytes != nil {
ns := std.Deserialize(nsBytes.([]byte)).(NameState)
if runtime.GetTime() < ns.Expiration {
return false
}
oldOwner = ns.Owner
updateBalance(ctx, []byte(name), oldOwner, -1)
} else {
updateTotalSupply(ctx, +1)
}
ns := NameState{
Owner: owner,
Name: name,
Expiration: runtime.GetTime() + millisecondsInYear,
}
putNameStateWithKey(ctx, tokenKey, ns)
updateBalance(ctx, []byte(name), owner, +1)
postTransfer(oldOwner, owner, []byte(name), nil)
return true
}
// Renew increases domain expiration date.
func Renew(name string) int {
if len(name) > maxDomainNameLength {
panic("invalid domain name format")
}
runtime.BurnGas(GetPrice())
ctx := storage.GetContext()
ns := getNameState(ctx, []byte(name))
ns.Expiration += millisecondsInYear
putNameState(ctx, ns)
return ns.Expiration
}
// SetAdmin updates domain admin.
func SetAdmin(name string, admin interop.Hash160) {
if len(name) > maxDomainNameLength {
panic("invalid domain name format")
}
if admin != nil && !runtime.CheckWitness(admin) {
panic("not witnessed by admin")
}
ctx := storage.GetContext()
ns := getNameState(ctx, []byte(name))
if !runtime.CheckWitness(ns.Owner) {
panic("not witnessed by owner")
}
ns.Admin = admin
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))
var ok bool
switch typ {
case A:
ok = checkIPv4(data)
case CNAME:
ok = splitAndCheck(data, true) != nil
case TXT:
ok = len(data) <= maxTXTRecordLength
case AAAA:
ok = checkIPv6(data)
default:
panic("unsupported record type")
}
if !ok {
panic("invalid record data")
}
ctx := storage.GetContext()
ns := getNameState(ctx, tokenID)
ns.checkAdmin()
putRecord(ctx, tokenID, name, typ, data)
}
// 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))
ctx := storage.GetReadOnlyContext()
_ = getNameState(ctx, tokenID) // ensure not expired
return getRecord(ctx, tokenID, name, typ)
}
// DeleteRecord removes domain record with the specified type.
func DeleteRecord(name string, typ RecordType) {
tokenID := []byte(tokenIDFromName(name))
ctx := storage.GetContext()
ns := getNameState(ctx, tokenID)
ns.checkAdmin()
recordKey := getRecordKey(tokenID, name, typ)
storage.Delete(ctx, recordKey)
}
// Resolve resolves given name (not more then three redirects are allowed).
func Resolve(name string, typ RecordType) string {
ctx := storage.GetReadOnlyContext()
return resolve(ctx, name, typ, 2)
}
// updateBalance updates account's balance and account's tokens.
func updateBalance(ctx storage.Context, tokenId []byte, acc interop.Hash160, diff int) {
balanceKey := append([]byte{prefixBalance}, acc...)
var balance int
if b := storage.Get(ctx, balanceKey); b != nil {
balance = b.(int)
}
balance += diff
if balance == 0 {
storage.Delete(ctx, balanceKey)
} else {
storage.Put(ctx, balanceKey, balance)
}
tokenKey := getTokenKey(tokenId)
accountTokenKey := append(append([]byte{prefixAccountToken}, acc...), tokenKey...)
if diff < 0 {
storage.Delete(ctx, accountTokenKey)
} else {
storage.Put(ctx, accountTokenKey, tokenId)
}
}
// postTransfer sends Transfer notification to the network and calls onNEP11Payment
// method.
func postTransfer(from, to interop.Hash160, tokenID []byte, data interface{}) {
runtime.Notify("Transfer", from, to, 1, tokenID)
if management.GetContract(to) != nil {
contract.Call(to, "onNEP11Payment", contract.All, from, 1, tokenID, data)
}
}
// getTotalSupply returns total supply from storage.
func getTotalSupply(ctx storage.Context) int {
val := storage.Get(ctx, []byte{prefixTotalSupply})
return val.(int)
}
// updateTotalSupply adds specified diff to the total supply.
func updateTotalSupply(ctx storage.Context, diff int) {
tsKey := []byte{prefixTotalSupply}
ts := getTotalSupply(ctx)
storage.Put(ctx, tsKey, ts+diff)
}
// getTokenKey computes hash160 from the given tokenID.
func getTokenKey(tokenID []byte) []byte {
return crypto.Ripemd160(tokenID)
}
// getNameState returns domain name state by the specified tokenID.
func getNameState(ctx storage.Context, tokenID []byte) NameState {
tokenKey := getTokenKey(tokenID)
return getNameStateWithKey(ctx, tokenKey)
}
// getNameStateWithKey returns domain name state by the specified token key.
func getNameStateWithKey(ctx storage.Context, tokenKey []byte) NameState {
nameKey := append([]byte{prefixName}, tokenKey...)
nsBytes := storage.Get(ctx, nameKey)
if nsBytes == nil {
panic("token not found")
}
ns := std.Deserialize(nsBytes.([]byte)).(NameState)
ns.ensureNotExpired()
return ns
}
// putNameState stores domain name state.
func putNameState(ctx storage.Context, ns NameState) {
tokenKey := getTokenKey([]byte(ns.Name))
putNameStateWithKey(ctx, tokenKey, ns)
}
// putNameStateWithKey stores domain name state with the specified token key.
func putNameStateWithKey(ctx storage.Context, tokenKey []byte, ns NameState) {
nameKey := append([]byte{prefixName}, tokenKey...)
nsBytes := std.Serialize(ns)
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)
record := storage.Get(ctx, recordKey)
return record.(string)
}
// putRecord stores domain record.
func putRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType, record string) {
recordKey := getRecordKey(tokenId, name, typ)
storage.Put(ctx, recordKey, record)
}
// getRecordKey returns key used to store domain records.
func getRecordKey(tokenId []byte, name string, typ RecordType) []byte {
recordKey := append([]byte{prefixRecord}, getTokenKey(tokenId)...)
recordKey = append(recordKey, getTokenKey([]byte(name))...)
return append(recordKey, []byte{byte(typ)}...)
}
// isValid returns true if the provided address is a valid Uint160.
func isValid(address interop.Hash160) bool {
return address != nil && len(address) == 20
}
// checkCommittee panics if the script container is not signed by the committee.
func checkCommittee() {
committee := neo.GetCommittee()
if committee == nil {
panic("failed to get committee")
}
l := len(committee)
committeeMultisig := contract.CreateMultisigAccount(l-(l-1)/2, committee)
if committeeMultisig == nil || !runtime.CheckWitness(committeeMultisig) {
panic("not witnessed by committee")
}
}
// checkFragment validates root or a part of domain name.
func checkFragment(v string, isRoot bool) bool {
maxLength := maxDomainNameFragmentLength
if isRoot {
maxLength = maxRootLength
}
if len(v) == 0 || len(v) > maxLength {
return false
}
c := v[0]
if isRoot {
if !(c >= 'a' && c <= 'z') {
return false
}
} else {
if !isAlNum(c) {
return false
}
}
for i := 1; i < len(v); i++ {
if !isAlNum(v[i]) {
return false
}
}
return true
}
// isAlNum checks whether provided char is a lowercase letter or a number.
func isAlNum(c uint8) bool {
return c >= 'a' && c <= 'z' || c >= '0' && c <= '9'
}
// splitAndCheck splits domain name into parts and validates it.
func splitAndCheck(name string, allowMultipleFragments bool) []string {
l := len(name)
if l < minDomainNameLength || maxDomainNameLength < l {
return nil
}
fragments := std.StringSplit(name, ".")
l = len(fragments)
if l < 2 {
return nil
}
if l > 2 && !allowMultipleFragments {
return nil
}
for i := 0; i < l; i++ {
if !checkFragment(fragments[i], i == l-1) {
return nil
}
}
return fragments
}
// checkIPv4 checks record on IPv4 compliance.
func checkIPv4(data string) bool {
l := len(data)
if l < 7 || 15 < l {
return false
}
fragments := std.StringSplit(data, ".")
if len(fragments) != 4 {
return false
}
numbers := make([]int, 4)
for i, f := range fragments {
if len(f) == 0 {
return false
}
number := std.Atoi10(f)
if number < 0 || 255 < number {
panic("not a byte")
}
if number > 0 && f[0] == '0' {
return false
}
if number == 0 && len(f) > 1 {
return false
}
numbers[i] = number
}
n0 := numbers[0]
n1 := numbers[1]
n3 := numbers[3]
if n0 == 0 ||
n0 == 10 ||
n0 == 127 ||
n0 >= 224 ||
(n0 == 169 && n1 == 254) ||
(n0 == 172 && 16 <= n1 && n1 <= 31) ||
(n0 == 192 && n1 == 168) ||
n3 == 0 ||
n3 == 255 {
return false
}
return true
}
// checkIPv6 checks record on IPv6 compliance.
func checkIPv6(data string) bool {
l := len(data)
if l < 2 || 39 < l {
return false
}
fragments := std.StringSplit(data, ":")
l = len(fragments)
if l < 3 || 8 < l {
return false
}
if fragments[0] == "2001" { // example addresses prefix
return false
}
var hasEmpty bool
for i := 1; i < l; i++ {
f := fragments[i]
fLen := len(f)
if fLen == 0 {
if i < l-1 && hasEmpty {
return false
}
hasEmpty = true
} else {
if fLen > 4 {
return false
}
_ = std.Atoi(f, 16) // check it won't panic
}
}
return true
}
// tokenIDFromName returns token ID (domain.root) from provided name.
func tokenIDFromName(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):]
}
// 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 {
if redirect < 0 {
panic("invalid redirect")
}
records := getRecords(ctx, name)
cname := ""
for iterator.Next(records) {
r := iterator.Value(records).([]string)
key := []byte(r[0])
value := r[1]
rTyp := key[len(key)-1]
if rTyp == byte(typ) {
return value
}
if rTyp == byte(CNAME) {
cname = value
}
}
if cname == "" {
return string([]byte(nil))
}
return resolve(ctx, cname, typ, redirect-1)
}
// 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 := append([]byte{prefixRecord}, getTokenKey(tokenID)...)
recordsKey = append(recordsKey, getTokenKey([]byte(name))...)
return storage.Find(ctx, recordsKey, storage.None)
}

View file

@ -0,0 +1,15 @@
name: "NameService"
supportedstandards: ["NEP-11"]
safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf",
"tokens", "properties", "roots", "getPrice", "isAvailable", "getRecord", "resolve"]
events:
- name: Transfer
parameters:
- name: from
type: Hash160
- name: to
type: Hash160
- name: amount
type: Integer
- name: tokenId
type: ByteArray

View file

@ -0,0 +1,20 @@
package nns
// RecordType is domain name service record types.
type RecordType byte
// Record types defined in [RFC 1035](https://tools.ietf.org/html/rfc1035)
const (
// A represents address record type.
A RecordType = 1
// CNAME represents canonical name record type.
CNAME RecordType = 5
// TXT represents text record type.
TXT RecordType = 16
)
// Record types defined in [RFC 3596](https://tools.ietf.org/html/rfc3596)
const (
// AAAA represents IPv6 address record type.
AAAA RecordType = 28
)

View file

@ -9,14 +9,12 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
"github.com/nspcc-dev/neo-go/pkg/core/native"
"github.com/nspcc-dev/neo-go/pkg/core/native/nnsrecords"
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/interop/native/crypto"
"github.com/nspcc-dev/neo-go/pkg/interop/native/gas"
"github.com/nspcc-dev/neo-go/pkg/interop/native/ledger"
"github.com/nspcc-dev/neo-go/pkg/interop/native/management"
"github.com/nspcc-dev/neo-go/pkg/interop/native/nameservice"
"github.com/nspcc-dev/neo-go/pkg/interop/native/neo"
"github.com/nspcc-dev/neo-go/pkg/interop/native/notary"
"github.com/nspcc-dev/neo-go/pkg/interop/native/oracle"
@ -38,7 +36,6 @@ func TestContractHashes(t *testing.T) {
require.Equalf(t, []byte(oracle.Hash), cs.Oracle.Hash.BytesBE(), "%q", string(cs.Oracle.Hash.BytesBE()))
require.Equalf(t, []byte(roles.Hash), cs.Designate.Hash.BytesBE(), "%q", string(cs.Designate.Hash.BytesBE()))
require.Equalf(t, []byte(policy.Hash), cs.Policy.Hash.BytesBE(), "%q", string(cs.Policy.Hash.BytesBE()))
require.Equalf(t, []byte(nameservice.Hash), cs.NameService.Hash.BytesBE(), "%q", string(cs.NameService.Hash.BytesBE()))
require.Equalf(t, []byte(ledger.Hash), cs.Ledger.Hash.BytesBE(), "%q", string(cs.Ledger.Hash.BytesBE()))
require.Equalf(t, []byte(management.Hash), cs.Management.Hash.BytesBE(), "%q", string(cs.Management.Hash.BytesBE()))
require.Equalf(t, []byte(notary.Hash), cs.Notary.Hash.BytesBE(), "%q", string(cs.Notary.Hash.BytesBE()))
@ -69,13 +66,6 @@ func TestRoleManagementRole(t *testing.T) {
require.EqualValues(t, noderoles.P2PNotary, roles.P2PNotary)
}
func TestNameServiceRecordType(t *testing.T) {
require.EqualValues(t, nnsrecords.A, nameservice.TypeA)
require.EqualValues(t, nnsrecords.CNAME, nameservice.TypeCNAME)
require.EqualValues(t, nnsrecords.TXT, nameservice.TypeTXT)
require.EqualValues(t, nnsrecords.AAAA, nameservice.TypeAAAA)
}
func TestCryptoLibNamedCurve(t *testing.T) {
require.EqualValues(t, native.Secp256k1, crypto.Secp256k1)
require.EqualValues(t, native.Secp256r1, crypto.Secp256r1)
@ -150,31 +140,6 @@ func TestNativeHelpersCompile(t *testing.T) {
{"setStoragePrice", []string{"42"}},
{"unblockAccount", []string{u160}},
})
runNativeTestCases(t, cs.NameService.ContractMD, "nameservice", []nativeTestCase{
// nonfungible
{"symbol", nil},
{"decimals", nil},
{"totalSupply", nil},
{"ownerOf", []string{`"neo.com"`}},
{"balanceOf", []string{u160}},
{"properties", []string{`"neo.com"`}},
{"tokens", nil},
{"tokensOf", []string{u160}},
{"transfer", []string{u160, `"neo.com"`, "nil"}},
// name service
{"addRoot", []string{`"com"`}},
{"deleteRecord", []string{`"neo.com"`, "nameservice.TypeA"}},
{"isAvailable", []string{`"neo.com"`}},
{"getPrice", nil},
{"getRecord", []string{`"neo.com"`, "nameservice.TypeA"}},
{"register", []string{`"neo.com"`, u160}},
{"renew", []string{`"neo.com"`}},
{"resolve", []string{`"neo.com"`, "nameservice.TypeA"}},
{"setPrice", []string{"42"}},
{"setAdmin", []string{`"neo.com"`, u160}},
{"setRecord", []string{`"neo.com"`, "nameservice.TypeA", `"1.1.1.1"`}},
})
runNativeTestCases(t, cs.Ledger.ContractMD, "ledger", []nativeTestCase{
{"currentHash", nil},
{"currentIndex", nil},

View file

@ -14,6 +14,7 @@ import (
"testing"
"time"
nns "github.com/nspcc-dev/neo-go/examples/nft-nd-nns"
"github.com/nspcc-dev/neo-go/internal/testchain"
"github.com/nspcc-dev/neo-go/pkg/compiler"
"github.com/nspcc-dev/neo-go/pkg/config"
@ -22,7 +23,6 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/chaindump"
"github.com/nspcc-dev/neo-go/pkg/core/fee"
"github.com/nspcc-dev/neo-go/pkg/core/native"
"github.com/nspcc-dev/neo-go/pkg/core/native/nnsrecords"
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/storage"
@ -47,6 +47,31 @@ import (
// multisig address which possess all NEO.
var neoOwner = testchain.MultisigScriptHash()
// examplesPrefix is a prefix of the example smart-contracts.
const examplesPrefix = "../../examples/"
// newTestChainWithNS should be called before newBlock invocation to properly setup
// global state.
func newTestChainWithNS(t *testing.T) (*Blockchain, util.Uint160) {
bc := newTestChainWithCustomCfg(t, nil)
acc := newAccountWithGAS(t, bc)
// Push NameService contract into the chain.
nsPath := examplesPrefix + "nft-nd-nns/"
nsConfigPath := nsPath + "nns.yml"
txDeploy4, _ := newDeployTx(t, bc, acc.PrivateKey().GetScriptHash(), nsPath, nsPath, &nsConfigPath)
txDeploy4.Nonce = 123
txDeploy4.ValidUntilBlock = bc.BlockHeight() + 1
require.NoError(t, addNetworkFee(bc, txDeploy4, acc))
require.NoError(t, acc.SignTx(testchain.Network(), txDeploy4))
b := bc.newBlock(txDeploy4)
require.NoError(t, bc.AddBlock(b))
checkTxHalt(t, bc, txDeploy4.Hash())
h, err := bc.GetContractScriptHash(1)
require.NoError(t, err)
return bc, h
}
// newTestChain should be called before newBlock invocation to properly setup
// global state.
func newTestChain(t *testing.T) *Blockchain {
@ -435,21 +460,36 @@ func initBasicChain(t *testing.T, bc *Blockchain) {
require.NoError(t, addNetworkFee(bc, txDeploy3, acc0))
require.NoError(t, acc0.SignTx(testchain.Network(), txDeploy3))
b = bc.newBlock(txDeploy3)
require.NoError(t, bc.AddBlock(b))
require.NoError(t, bc.AddBlock(b)) // block #10
checkTxHalt(t, bc, txDeploy3.Hash())
// register `neo.com` with A record type and priv0 owner via NNS
transferFundsToCommittee(t, bc) // block #11
// Push NameService contract into the chain.
nsPath := examplesPrefix + "nft-nd-nns/"
nsConfigPath := nsPath + "nns.yml"
txDeploy4, _ := newDeployTx(t, bc, priv0ScriptHash, nsPath, nsPath, &nsConfigPath)
txDeploy4.Nonce = getNextNonce()
txDeploy4.ValidUntilBlock = validUntilBlock
require.NoError(t, addNetworkFee(bc, txDeploy4, acc0))
require.NoError(t, acc0.SignTx(testchain.Network(), txDeploy4))
b = bc.newBlock(txDeploy4)
require.NoError(t, bc.AddBlock(b)) // block #11
checkTxHalt(t, bc, txDeploy4.Hash())
nsHash, err := bc.GetContractScriptHash(4)
require.NoError(t, err)
t.Logf("contract (%s): \n\tHash: %s\n", nsPath, nsHash.StringLE())
// register `neo.com` with A record type and priv0 owner via NS
transferFundsToCommittee(t, bc) // block #12
res, err := invokeContractMethodGeneric(bc, defaultNameServiceSysfee,
bc.contracts.NameService.Hash, "addRoot", true, "com") // block #12
nsHash, "addRoot", true, "com") // block #13
require.NoError(t, err)
checkResult(t, res, stackitem.Null{})
res, err = invokeContractMethodGeneric(bc, native.DefaultDomainPrice+defaultNameServiceSysfee,
bc.contracts.NameService.Hash, "register", acc0, "neo.com", priv0ScriptHash) // block #13
res, err = invokeContractMethodGeneric(bc, defaultNameServiceDomainPrice+defaultNameServiceSysfee+1_0000_000,
nsHash, "register", acc0, "neo.com", priv0ScriptHash) // block #14
require.NoError(t, err)
checkResult(t, res, stackitem.NewBool(true))
res, err = invokeContractMethodGeneric(bc, defaultNameServiceSysfee, bc.contracts.NameService.Hash,
"setRecord", acc0, "neo.com", int64(nnsrecords.A), "1.2.3.4") // block #14
res, err = invokeContractMethodGeneric(bc, defaultNameServiceSysfee, nsHash,
"setRecord", acc0, "neo.com", int64(nns.A), "1.2.3.4") // block #15
require.NoError(t, err)
checkResult(t, res, stackitem.Null{})
@ -471,8 +511,16 @@ func newNEP17Transfer(sc, from, to util.Uint160, amount int64, additionalArgs ..
func newDeployTx(t *testing.T, bc *Blockchain, sender util.Uint160, name, ctrName string, cfgName *string) (*transaction.Transaction, util.Uint160) {
c, err := ioutil.ReadFile(name)
require.NoError(t, err)
tx, h, avm, err := testchain.NewDeployTx(bc, ctrName, sender, bytes.NewReader(c), cfgName)
var (
tx *transaction.Transaction
h util.Uint160
avm []byte
)
if err == nil {
tx, h, avm, err = testchain.NewDeployTx(bc, ctrName, sender, bytes.NewReader(c), cfgName)
} else {
tx, h, avm, err = testchain.NewDeployTx(bc, ctrName, sender, nil, cfgName)
}
require.NoError(t, err)
t.Logf("contract (%s): \n\tHash: %s\n\tAVM: %s", name, h.StringLE(), base64.StdEncoding.EncodeToString(avm))
return tx, h

View file

@ -32,7 +32,7 @@ func LoadToken(ic *interop.Context) func(id int32) error {
}
cs, err := ic.GetContract(tok.Hash)
if err != nil {
return fmt.Errorf("contract not found: %w", err)
return fmt.Errorf("token contract %s not found: %w", tok.Hash.StringLE(), err)
}
return callInternal(ic, cs, tok.Method, tok.CallFlag, tok.HasReturn, args)
}
@ -53,7 +53,7 @@ func Call(ic *interop.Context) error {
}
cs, err := ic.GetContract(u)
if err != nil {
return fmt.Errorf("contract not found: %w", err)
return fmt.Errorf("called contract %s not found: %w", u.StringLE(), err)
}
if strings.HasPrefix(method, "_") {
return errors.New("invalid method name (starts with '_')")

View file

@ -15,18 +15,17 @@ const reservedContractID = -100
// Contracts is a set of registered native contracts.
type Contracts struct {
Management *Management
Ledger *Ledger
NEO *NEO
GAS *GAS
Policy *Policy
Oracle *Oracle
Designate *Designate
NameService *NameService
Notary *Notary
Crypto *Crypto
Std *Std
Contracts []interop.Contract
Management *Management
Ledger *Ledger
NEO *NEO
GAS *GAS
Policy *Policy
Oracle *Oracle
Designate *Designate
Notary *Notary
Crypto *Crypto
Std *Std
Contracts []interop.Contract
// persistScript is vm script which executes "onPersist" method of every native contract.
persistScript []byte
// postPersistScript is vm script which executes "postPersist" method of every native contract.
@ -103,11 +102,6 @@ func NewContracts(p2pSigExtensionsEnabled bool, nativeUpdateHistories map[string
cs.Oracle = oracle
cs.Contracts = append(cs.Contracts, oracle)
ns := newNameService()
ns.NEO = neo
cs.NameService = ns
cs.Contracts = append(cs.Contracts, ns)
if p2pSigExtensionsEnabled {
notary := newNotary()
notary.GAS = gas

View file

@ -1,723 +0,0 @@
package native
import (
"encoding/binary"
"errors"
"math"
"math/big"
"net"
"regexp"
"sort"
"strings"
"unicode/utf8"
"github.com/nspcc-dev/neo-go/pkg/core/dao"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/interop/runtime"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/core/native/nnsrecords"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/encoding/bigint"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
// NameService represents native NameService contract.
type NameService struct {
nonfungible
NEO *NEO
}
type nameState struct {
state.NFTTokenState
// Expiration is token expiration height.
Expiration uint32
// HasAdmin is true if token has admin.
HasAdmin bool
// Admin is token admin.
Admin util.Uint160
}
const (
nameServiceID = -10
prefixRoots = 10
prefixDomainPrice = 22
prefixExpiration = 20
prefixRecord = 12
secondsInYear = 365 * 24 * 3600
// DefaultDomainPrice is the default price of register method.
DefaultDomainPrice = 10_00000000
// MinDomainNameLength is minimum domain length.
MinDomainNameLength = 3
// MaxDomainNameLength is maximum domain length.
MaxDomainNameLength = 255
)
var (
// Lookahead is not supported by Go, but it is simple `(?=.{3,255}$)`,
// so we check name length explicitly.
nameRegex = regexp.MustCompile(`^([a-z0-9]{1,62}\.)+[a-z][a-z0-9]{0,15}$`)
ipv4Regex = regexp.MustCompile(`^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])$`)
ipv6Regex = regexp.MustCompile("(?:^)(([0-9a-f]{1,4}:){7,7}[0-9a-f]{1,4}|([0-9a-f]{1,4}:){1,7}:|([0-9a-f]{1,4}:){1,6}:[0-9a-f]{1,4}|([0-9a-f]{1,4}:){1,5}(:[0-9a-f]{1,4}){1,2}|([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,3}|([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,4}|([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,5}|[0-9a-f]{1,4}:((:[0-9a-f]{1,4}){1,6})|:((:[0-9a-f]{1,4}){1,7}|:))$")
rootRegex = regexp.MustCompile("^[a-z][a-z0-9]{0,15}$")
)
// matchName checks if provided name is valid.
func matchName(name string) bool {
ln := len(name)
return MinDomainNameLength <= ln && ln <= MaxDomainNameLength &&
nameRegex.Match([]byte(name))
}
func newNameService() *NameService {
nf := newNonFungible(nativenames.NameService, nameServiceID, "NNS", 0)
nf.getTokenKey = func(tokenID []byte) []byte {
return append([]byte{prefixNFTToken}, hash.Hash160(tokenID).BytesBE()...)
}
nf.newTokenState = func() nftTokenState {
return new(nameState)
}
nf.onTransferred = func(tok nftTokenState) {
tok.(*nameState).HasAdmin = false
}
n := &NameService{nonfungible: *nf}
defer n.UpdateHash()
desc := newDescriptor("addRoot", smartcontract.VoidType,
manifest.NewParameter("root", smartcontract.StringType))
md := newMethodAndPrice(n.addRoot, 1<<15, callflag.States)
n.AddMethod(md, desc)
desc = newDescriptor("setPrice", smartcontract.VoidType,
manifest.NewParameter("price", smartcontract.IntegerType))
md = newMethodAndPrice(n.setPrice, 1<<15, callflag.States)
n.AddMethod(md, desc)
desc = newDescriptor("getPrice", smartcontract.IntegerType)
md = newMethodAndPrice(n.getPrice, 1<<15, callflag.ReadStates)
n.AddMethod(md, desc)
desc = newDescriptor("isAvailable", smartcontract.BoolType,
manifest.NewParameter("name", smartcontract.StringType))
md = newMethodAndPrice(n.isAvailable, 1<<15, callflag.ReadStates)
n.AddMethod(md, desc)
desc = newDescriptor("register", smartcontract.BoolType,
manifest.NewParameter("name", smartcontract.StringType),
manifest.NewParameter("owner", smartcontract.Hash160Type))
md = newMethodAndPrice(n.register, 1<<15, callflag.States)
n.AddMethod(md, desc)
desc = newDescriptor("renew", smartcontract.IntegerType,
manifest.NewParameter("name", smartcontract.StringType))
md = newMethodAndPrice(n.renew, 0, callflag.States)
n.AddMethod(md, desc)
desc = newDescriptor("setAdmin", smartcontract.VoidType,
manifest.NewParameter("name", smartcontract.StringType),
manifest.NewParameter("admin", smartcontract.Hash160Type))
md = newMethodAndPrice(n.setAdmin, 1<<15, callflag.States)
md.StorageFee = 20
n.AddMethod(md, desc)
desc = newDescriptor("setRecord", smartcontract.VoidType,
manifest.NewParameter("name", smartcontract.StringType),
manifest.NewParameter("type", smartcontract.IntegerType),
manifest.NewParameter("data", smartcontract.StringType))
md = newMethodAndPrice(n.setRecord, 1<<15, callflag.States)
md.StorageFee = 200
n.AddMethod(md, desc)
desc = newDescriptor("getRecord", smartcontract.StringType,
manifest.NewParameter("name", smartcontract.StringType),
manifest.NewParameter("type", smartcontract.IntegerType))
md = newMethodAndPrice(n.getRecord, 1<<15, callflag.ReadStates)
n.AddMethod(md, desc)
desc = newDescriptor("deleteRecord", smartcontract.VoidType,
manifest.NewParameter("name", smartcontract.StringType),
manifest.NewParameter("type", smartcontract.IntegerType))
md = newMethodAndPrice(n.deleteRecord, 1<<15, callflag.States)
n.AddMethod(md, desc)
desc = newDescriptor("resolve", smartcontract.StringType,
manifest.NewParameter("name", smartcontract.StringType),
manifest.NewParameter("type", smartcontract.IntegerType))
md = newMethodAndPrice(n.resolve, 1<<17, callflag.ReadStates)
n.AddMethod(md, desc)
return n
}
// Metadata implements interop.Contract interface.
func (n *NameService) Metadata() *interop.ContractMD {
return &n.ContractMD
}
// Initialize implements interop.Contract interface.
func (n *NameService) Initialize(ic *interop.Context) error {
if err := n.nonfungible.Initialize(ic); err != nil {
return err
}
if err := setIntWithKey(n.ID, ic.DAO, []byte{prefixDomainPrice}, DefaultDomainPrice); err != nil {
return err
}
roots := stringList{}
return putSerializableToDAO(n.ID, ic.DAO, []byte{prefixRoots}, &roots)
}
// OnPersist implements interop.Contract interface.
func (n *NameService) OnPersist(ic *interop.Context) error {
now := uint32(ic.Block.Timestamp/1000 + 1)
keys := []string{}
ic.DAO.Seek(n.ID, []byte{prefixExpiration}, func(k, v []byte) {
if binary.BigEndian.Uint32(k) >= now {
return
}
// Removal is done separately because of `Seek` takes storage mutex.
keys = append(keys, string(k))
})
var keysToRemove [][]byte
key := []byte{prefixExpiration}
keyRecord := []byte{prefixRecord}
for i := range keys {
key[0] = prefixExpiration
key = append(key[:1], []byte(keys[i])...)
if err := ic.DAO.DeleteStorageItem(n.ID, key); err != nil {
return err
}
keysToRemove = keysToRemove[:0]
key[0] = prefixRecord
key = append(key[:1], keys[i][4:]...)
ic.DAO.Seek(n.ID, key, func(k, v []byte) {
keysToRemove = append(keysToRemove, k)
})
for i := range keysToRemove {
keyRecord = append(keyRecord[:0], key...)
keyRecord = append(keyRecord, keysToRemove[i]...)
err := ic.DAO.DeleteStorageItem(n.ID, keyRecord)
if err != nil {
return err
}
}
key[0] = prefixNFTToken
n.burnByKey(ic, key)
}
return nil
}
// PostPersist implements interop.Contract interface.
func (n *NameService) PostPersist(ic *interop.Context) error {
return nil
}
func (n *NameService) addRoot(ic *interop.Context, args []stackitem.Item) stackitem.Item {
root := toString(args[0])
if !rootRegex.Match([]byte(root)) {
panic("invalid root")
}
n.checkCommittee(ic)
roots, _ := n.getRootsInternal(ic.DAO)
if !roots.add(root) {
panic("name already exists")
}
err := putSerializableToDAO(n.ID, ic.DAO, []byte{prefixRoots}, &roots)
if err != nil {
panic(err)
}
return stackitem.Null{}
}
var maxPrice = big.NewInt(10000_00000000)
func (n *NameService) setPrice(ic *interop.Context, args []stackitem.Item) stackitem.Item {
price := toBigInt(args[0])
if price.Sign() <= 0 || price.Cmp(maxPrice) >= 0 {
panic("invalid price")
}
n.checkCommittee(ic)
err := ic.DAO.PutStorageItem(n.ID, []byte{prefixDomainPrice}, bigint.ToBytes(price))
if err != nil {
panic(err)
}
return stackitem.Null{}
}
func (n *NameService) getPrice(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
return stackitem.NewBigInteger(n.getPriceInternal(ic.DAO))
}
func (n *NameService) getPriceInternal(d dao.DAO) *big.Int {
si := d.GetStorageItem(n.ID, []byte{prefixDomainPrice})
return bigint.FromBytes(si)
}
func (n *NameService) parseName(item stackitem.Item) (string, []string, []byte) {
name := toName(item)
names := strings.Split(name, ".")
if len(names) != 2 {
panic("invalid name")
}
return name, names, n.getTokenKey([]byte(name))
}
func (n *NameService) isAvailable(ic *interop.Context, args []stackitem.Item) stackitem.Item {
_, names, key := n.parseName(args[0])
if ic.DAO.GetStorageItem(n.ID, key) != nil {
return stackitem.NewBool(false)
}
roots, _ := n.getRootsInternal(ic.DAO)
_, ok := roots.index(names[1])
if !ok {
panic("domain is not registered")
}
return stackitem.NewBool(true)
}
func (n *NameService) getRootsInternal(d dao.DAO) (stringList, bool) {
var sl stringList
err := getSerializableFromDAO(n.ID, d, []byte{prefixRoots}, &sl)
if err != nil {
// Roots are being stored in `Initialize()` and thus must always be present.
panic(err)
}
return sl, true
}
func (n *NameService) register(ic *interop.Context, args []stackitem.Item) stackitem.Item {
name, names, key := n.parseName(args[0])
owner := toUint160(args[1])
if !n.checkWitness(ic, owner) {
panic("owner is not witnessed")
}
if ic.DAO.GetStorageItem(n.ID, key) != nil {
return stackitem.NewBool(false)
}
roots, _ := n.getRootsInternal(ic.DAO)
if _, ok := roots.index(names[1]); !ok {
panic("missing root")
}
if !ic.VM.AddGas(n.getPriceInternal(ic.DAO).Int64()) {
panic("insufficient gas")
}
token := &nameState{
NFTTokenState: state.NFTTokenState{
Owner: owner,
Name: name,
},
Expiration: uint32(ic.Block.Timestamp/1000 + secondsInYear),
}
n.mint(ic, token)
err := ic.DAO.PutStorageItem(n.ID,
makeExpirationKey(token.Expiration, token.ID()),
state.StorageItem{0})
if err != nil {
panic(err)
}
return stackitem.NewBool(true)
}
func (n *NameService) renew(ic *interop.Context, args []stackitem.Item) stackitem.Item {
_, _, key := n.parseName(args[0])
if !ic.VM.AddGas(n.getPriceInternal(ic.DAO).Int64()) {
panic("insufficient gas")
}
token := new(nameState)
err := getSerializableFromDAO(n.ID, ic.DAO, key, token)
if err != nil {
panic(err)
}
keyExpiration := makeExpirationKey(token.Expiration, token.ID())
if err := ic.DAO.DeleteStorageItem(n.ID, keyExpiration); err != nil {
panic(err)
}
token.Expiration += secondsInYear
err = putSerializableToDAO(n.ID, ic.DAO, key, token)
if err != nil {
panic(err)
}
binary.BigEndian.PutUint32(key[1:], token.Expiration)
err = ic.DAO.PutStorageItem(n.ID, key, state.StorageItem{0})
if err != nil {
panic(err)
}
bi := new(big.Int).SetUint64(uint64(token.Expiration))
return stackitem.NewBigInteger(bi)
}
func (n *NameService) setAdmin(ic *interop.Context, args []stackitem.Item) stackitem.Item {
_, _, key := n.parseName(args[0])
var admin util.Uint160
_, isNull := args[1].(stackitem.Null)
if !isNull {
admin = toUint160(args[1])
if !n.checkWitness(ic, admin) {
panic("not witnessed by admin")
}
}
token := new(nameState)
err := getSerializableFromDAO(n.ID, ic.DAO, key, token)
if err != nil {
panic(err)
}
if !n.checkWitness(ic, token.Owner) {
panic("only owner can set admin")
}
token.HasAdmin = !isNull
token.Admin = admin
err = putSerializableToDAO(n.ID, ic.DAO, key, token)
if err != nil {
panic(err)
}
return stackitem.Null{}
}
func (n *NameService) checkWitness(ic *interop.Context, owner util.Uint160) bool {
ok, err := runtime.CheckHashedWitness(ic, owner)
if err != nil {
panic(err)
}
return ok
}
func (n *NameService) checkCommittee(ic *interop.Context) {
if !n.NEO.checkCommittee(ic) {
panic("not witnessed by committee")
}
}
func (n *NameService) checkAdmin(ic *interop.Context, token *nameState) bool {
if n.checkWitness(ic, token.Owner) {
return true
}
return token.HasAdmin && n.checkWitness(ic, token.Admin)
}
func (n *NameService) setRecord(ic *interop.Context, args []stackitem.Item) stackitem.Item {
name := toName(args[0])
rt := toRecordType(args[1])
data := toString(args[2])
checkName(rt, data)
domain := toDomain(name)
token, _, err := n.tokenState(ic.DAO, []byte(domain))
if err != nil {
panic(err)
}
if !n.checkAdmin(ic, token.(*nameState)) {
panic("not witnessed by admin")
}
key := makeRecordKey(domain, name, rt)
si := state.StorageItem(data)
if err := ic.DAO.PutStorageItem(n.ID, key, si); err != nil {
panic(err)
}
return stackitem.Null{}
}
func checkName(rt nnsrecords.Type, name string) {
var valid bool
switch rt {
case nnsrecords.A:
// We can't rely on `len(ip) == net.IPv4len` because
// IPv4 can be parsed to mapped representation.
valid = ipv4Regex.MatchString(name) &&
net.ParseIP(name) != nil
case nnsrecords.CNAME:
valid = matchName(name)
case nnsrecords.TXT:
valid = utf8.RuneCountInString(name) <= 255
case nnsrecords.AAAA:
valid = ipv6Regex.MatchString(name) &&
net.ParseIP(name) != nil
}
if !valid {
panic("invalid name")
}
}
func (n *NameService) getRecord(ic *interop.Context, args []stackitem.Item) stackitem.Item {
name := toName(args[0])
domain := toDomain(name)
rt := toRecordType(args[1])
key := makeRecordKey(domain, name, rt)
si := ic.DAO.GetStorageItem(n.ID, key)
if si == nil {
return stackitem.Null{}
}
return stackitem.NewByteArray(si)
}
func (n *NameService) deleteRecord(ic *interop.Context, args []stackitem.Item) stackitem.Item {
name := toName(args[0])
rt := toRecordType(args[1])
domain := toDomain(name)
key := n.getTokenKey([]byte(domain))
token := new(nameState)
err := getSerializableFromDAO(n.ID, ic.DAO, key, token)
if err != nil {
panic(err)
}
if !n.checkAdmin(ic, token) {
panic("not witnessed by admin")
}
key = makeRecordKey(domain, name, rt)
if err := ic.DAO.DeleteStorageItem(n.ID, key); err != nil {
panic(err)
}
return stackitem.Null{}
}
func (n *NameService) resolve(ic *interop.Context, args []stackitem.Item) stackitem.Item {
name := toString(args[0])
rt := toRecordType(args[1])
result, ok := n.resolveInternal(ic, name, rt, 2)
if !ok {
return stackitem.Null{}
}
return stackitem.NewByteArray([]byte(result))
}
func (n *NameService) resolveInternal(ic *interop.Context, name string, t nnsrecords.Type, redirect int) (string, bool) {
if redirect < 0 {
panic("invalid redirect")
}
records := n.getRecordsInternal(ic.DAO, name)
if data, ok := records[t]; ok {
return data, true
}
data, ok := records[nnsrecords.CNAME]
if !ok {
return "", false
}
return n.resolveInternal(ic, data, t, redirect-1)
}
func (n *NameService) getRecordsInternal(d dao.DAO, name string) map[nnsrecords.Type]string {
domain := toDomain(name)
key := makeRecordKey(domain, name, 0)
key = key[:len(key)-1]
res := make(map[nnsrecords.Type]string)
d.Seek(n.ID, key, func(k, v []byte) {
rt := nnsrecords.Type(k[len(k)-1])
res[rt] = string(v)
})
return res
}
func makeRecordKey(domain, name string, rt nnsrecords.Type) []byte {
key := make([]byte, 1+util.Uint160Size+util.Uint160Size+1)
key[0] = prefixRecord
i := 1
i += copy(key[i:], hash.Hash160([]byte(domain)).BytesBE())
i += copy(key[i:], hash.Hash160([]byte(name)).BytesBE())
key[i] = byte(rt)
return key
}
func makeExpirationKey(expiration uint32, tokenID []byte) []byte {
key := make([]byte, 1+4+util.Uint160Size)
key[0] = prefixExpiration
binary.BigEndian.PutUint32(key[1:], expiration)
copy(key[5:], hash.Hash160(tokenID).BytesBE())
return key
}
// ToMap implements nftTokenState interface.
func (s *nameState) ToMap() *stackitem.Map {
m := s.NFTTokenState.ToMap()
m.Add(stackitem.NewByteArray([]byte("expiration")),
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(s.Expiration))))
return m
}
// EncodeBinary implements io.Serializable.
func (s *nameState) EncodeBinary(w *io.BinWriter) {
stackitem.EncodeBinaryStackItem(s.ToStackItem(), w)
}
// DecodeBinary implements io.Serializable.
func (s *nameState) DecodeBinary(r *io.BinReader) {
item := stackitem.DecodeBinaryStackItem(r)
if r.Err == nil {
r.Err = s.FromStackItem(item)
}
}
// ToStackItem implements nftTokenState interface.
func (s *nameState) ToStackItem() stackitem.Item {
item := s.NFTTokenState.ToStackItem().(*stackitem.Struct)
exp := new(big.Int).SetUint64(uint64(s.Expiration))
item.Append(stackitem.NewBigInteger(exp))
if s.HasAdmin {
item.Append(stackitem.NewByteArray(s.Admin.BytesBE()))
} else {
item.Append(stackitem.Null{})
}
return item
}
// FromStackItem implements nftTokenState interface.
func (s *nameState) FromStackItem(item stackitem.Item) error {
err := s.NFTTokenState.FromStackItem(item)
if err != nil {
return err
}
elems := item.Value().([]stackitem.Item)
if len(elems) < 4 {
return errors.New("invalid stack item")
}
bi, err := elems[2].TryInteger()
if err != nil || !bi.IsUint64() {
return errors.New("invalid stack item")
}
_, isNull := elems[3].(stackitem.Null)
if !isNull {
bs, err := elems[3].TryBytes()
if err != nil {
return err
}
u, err := util.Uint160DecodeBytesBE(bs)
if err != nil {
return err
}
s.Admin = u
}
s.Expiration = uint32(bi.Uint64())
s.HasAdmin = !isNull
return nil
}
// Helpers
func domainFromString(name string) (string, bool) {
i := strings.LastIndexAny(name, ".")
if i < 0 {
return "", false
}
i = strings.LastIndexAny(name[:i], ".")
if i < 0 {
return name, true
}
return name[i+1:], true
}
func toDomain(name string) string {
domain, ok := domainFromString(name)
if !ok {
panic("invalid record")
}
return domain
}
func toRecordType(item stackitem.Item) nnsrecords.Type {
bi, err := item.TryInteger()
if err != nil || !bi.IsInt64() {
panic("invalid record type")
}
val := bi.Uint64()
if val > math.MaxUint8 {
panic("invalid record type")
}
switch rt := nnsrecords.Type(val); rt {
case nnsrecords.A, nnsrecords.CNAME, nnsrecords.TXT, nnsrecords.AAAA:
return rt
default:
panic("invalid record type")
}
}
func toName(item stackitem.Item) string {
name := toString(item)
if !matchName(name) {
panic("invalid name")
}
return name
}
type stringList []string
// ToStackItem converts sl to stack item.
func (sl stringList) ToStackItem() stackitem.Item {
arr := make([]stackitem.Item, len(sl))
for i := range sl {
arr[i] = stackitem.NewByteArray([]byte(sl[i]))
}
return stackitem.NewArray(arr)
}
// FromStackItem converts stack item to string list.
func (sl *stringList) FromStackItem(item stackitem.Item) error {
arr, ok := item.Value().([]stackitem.Item)
if !ok {
return errors.New("invalid stack item")
}
res := make([]string, len(arr))
for i := range res {
s, err := stackitem.ToString(arr[i])
if err != nil {
return err
}
res[i] = s
}
*sl = res
return nil
}
// EncodeBinary implements io.Serializable.
func (sl stringList) EncodeBinary(w *io.BinWriter) {
stackitem.EncodeBinaryStackItem(sl.ToStackItem(), w)
}
// DecodeBinary implements io.Serializable.
func (sl *stringList) DecodeBinary(r *io.BinReader) {
item := stackitem.DecodeBinaryStackItem(r)
if r.Err == nil {
r.Err = sl.FromStackItem(item)
}
}
func (sl stringList) index(s string) (int, bool) {
index := sort.Search(len(sl), func(i int) bool {
return sl[i] >= s
})
return index, index < len(sl) && sl[index] == s
}
func (sl *stringList) add(s string) bool {
index, has := sl.index(s)
if has {
return false
}
*sl = append(*sl, "")
copy((*sl)[index+1:], (*sl)[index:])
(*sl)[index] = s
return true
}

View file

@ -1,96 +0,0 @@
package native
import (
"testing"
"github.com/nspcc-dev/neo-go/pkg/core/native/nnsrecords"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest/standard"
"github.com/stretchr/testify/require"
)
// The specification is following C# code:
// string domain = string.Join('.', name.Split('.')[^2..]);
func TestParseDomain(t *testing.T) {
testCases := []struct {
name string
domain string
}{
{"sim.pl.e", "pl.e"},
{"some.long.d.o.m.a.i.n", "i.n"},
{"t.wo", "t.wo"},
{".dot", ".dot"},
{".d.ot", "d.ot"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
dom, ok := domainFromString(tc.name)
require.True(t, ok)
require.Equal(t, tc.domain, dom)
})
}
_, ok := domainFromString("nodots")
require.False(t, ok)
}
func TestNameService_CheckName(t *testing.T) {
// tests are got from the C# implementation
testCases := []struct {
Type nnsrecords.Type
Name string
ShouldFail bool
}{
{Type: nnsrecords.A, Name: "0.0.0.0"},
{Type: nnsrecords.A, Name: "10.10.10.10"},
{Type: nnsrecords.A, Name: "255.255.255.255"},
{Type: nnsrecords.A, Name: "192.168.1.1"},
{Type: nnsrecords.A, Name: "1a", ShouldFail: true},
{Type: nnsrecords.A, Name: "256.0.0.0", ShouldFail: true},
{Type: nnsrecords.A, Name: "01.01.01.01", ShouldFail: true},
{Type: nnsrecords.A, Name: "00.0.0.0", ShouldFail: true},
{Type: nnsrecords.A, Name: "0.0.0.-1", ShouldFail: true},
{Type: nnsrecords.A, Name: "0.0.0.0.1", ShouldFail: true},
{Type: nnsrecords.A, Name: "11111111.11111111.11111111.11111111", ShouldFail: true},
{Type: nnsrecords.A, Name: "11111111.11111111.11111111.11111111", ShouldFail: true},
{Type: nnsrecords.A, Name: "ff.ff.ff.ff", ShouldFail: true},
{Type: nnsrecords.A, Name: "0.0.256", ShouldFail: true},
{Type: nnsrecords.A, Name: "0.0.0", ShouldFail: true},
{Type: nnsrecords.A, Name: "0.257", ShouldFail: true},
{Type: nnsrecords.A, Name: "1.1", ShouldFail: true},
{Type: nnsrecords.A, Name: "257", ShouldFail: true},
{Type: nnsrecords.A, Name: "1", ShouldFail: true},
{Type: nnsrecords.AAAA, Name: "2001:db8::8:800:200c:417a"},
{Type: nnsrecords.AAAA, Name: "ff01::101"},
{Type: nnsrecords.AAAA, Name: "::1"},
{Type: nnsrecords.AAAA, Name: "::"},
{Type: nnsrecords.AAAA, Name: "2001:db8:0:0:8:800:200c:417a"},
{Type: nnsrecords.AAAA, Name: "ff01:0:0:0:0:0:0:101"},
{Type: nnsrecords.AAAA, Name: "0:0:0:0:0:0:0:1"},
{Type: nnsrecords.AAAA, Name: "0:0:0:0:0:0:0:0"},
{Type: nnsrecords.AAAA, Name: "2001:DB8::8:800:200C:417A", ShouldFail: true},
{Type: nnsrecords.AAAA, Name: "FF01::101", ShouldFail: true},
{Type: nnsrecords.AAAA, Name: "fF01::101", ShouldFail: true},
{Type: nnsrecords.AAAA, Name: "2001:DB8:0:0:8:800:200C:417A", ShouldFail: true},
{Type: nnsrecords.AAAA, Name: "FF01:0:0:0:0:0:0:101", ShouldFail: true},
{Type: nnsrecords.AAAA, Name: "::ffff:1.01.1.01", ShouldFail: true},
{Type: nnsrecords.AAAA, Name: "2001:DB8:0:0:8:800:200C:4Z", ShouldFail: true},
{Type: nnsrecords.AAAA, Name: "::13.1.68.3", ShouldFail: true},
}
for _, testCase := range testCases {
if testCase.ShouldFail {
require.Panics(t, func() {
checkName(testCase.Type, testCase.Name)
})
} else {
require.NotPanics(t, func() {
checkName(testCase.Type, testCase.Name)
})
}
}
}
func TestNameService_NEP11(t *testing.T) {
ns := newNameService()
require.NoError(t, standard.Check(&ns.Manifest, manifest.NEP11StandardName))
}

View file

@ -10,7 +10,6 @@ const (
Oracle = "OracleContract"
Designation = "RoleManagement"
Notary = "Notary"
NameService = "NameService"
CryptoLib = "CryptoLib"
StdLib = "StdLib"
)
@ -25,7 +24,6 @@ func IsValid(name string) bool {
name == Oracle ||
name == Designation ||
name == Notary ||
name == NameService ||
name == CryptoLib ||
name == StdLib
}

View file

@ -1,12 +0,0 @@
package nnsrecords
// Type represents name record type.
type Type byte
// Pre-defined record types.
const (
A Type = 1
CNAME Type = 5
TXT Type = 16
AAAA Type = 28
)

View file

@ -1,410 +0,0 @@
package native
import (
"bytes"
"errors"
"math/big"
"sort"
"github.com/nspcc-dev/neo-go/pkg/core/dao"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/interop/contract"
"github.com/nspcc-dev/neo-go/pkg/core/interop/runtime"
istorage "github.com/nspcc-dev/neo-go/pkg/core/interop/storage"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/storage"
"github.com/nspcc-dev/neo-go/pkg/encoding/bigint"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
type nonfungible struct {
interop.ContractMD
tokenSymbol string
tokenDecimals byte
onTransferred func(nftTokenState)
getTokenKey func([]byte) []byte
newTokenState func() nftTokenState
}
type nftTokenState interface {
io.Serializable
ToStackItem() stackitem.Item
FromStackItem(stackitem.Item) error
ToMap() *stackitem.Map
ID() []byte
Base() *state.NFTTokenState
}
const (
prefixNFTTotalSupply = 11
prefixNFTAccount = 7
prefixNFTToken = 5
)
var (
nftTotalSupplyKey = []byte{prefixNFTTotalSupply}
)
func newNonFungible(name string, id int32, symbol string, decimals byte) *nonfungible {
n := &nonfungible{
ContractMD: *interop.NewContractMD(name, id),
tokenSymbol: symbol,
tokenDecimals: decimals,
getTokenKey: func(tokenID []byte) []byte {
return append([]byte{prefixNFTToken}, tokenID...)
},
newTokenState: func() nftTokenState {
return new(state.NFTTokenState)
},
}
n.Manifest.SupportedStandards = []string{manifest.NEP11StandardName}
desc := newDescriptor("symbol", smartcontract.StringType)
md := newMethodAndPrice(n.symbol, 0, callflag.NoneFlag)
n.AddMethod(md, desc)
desc = newDescriptor("decimals", smartcontract.IntegerType)
md = newMethodAndPrice(n.decimals, 0, callflag.NoneFlag)
n.AddMethod(md, desc)
desc = newDescriptor("totalSupply", smartcontract.IntegerType)
md = newMethodAndPrice(n.totalSupply, 1<<15, callflag.ReadStates)
n.AddMethod(md, desc)
desc = newDescriptor("ownerOf", smartcontract.Hash160Type,
manifest.NewParameter("tokenId", smartcontract.ByteArrayType))
md = newMethodAndPrice(n.OwnerOf, 1<<15, callflag.ReadStates)
n.AddMethod(md, desc)
desc = newDescriptor("balanceOf", smartcontract.IntegerType,
manifest.NewParameter("owner", smartcontract.Hash160Type))
md = newMethodAndPrice(n.BalanceOf, 1<<15, callflag.ReadStates)
n.AddMethod(md, desc)
desc = newDescriptor("properties", smartcontract.MapType,
manifest.NewParameter("tokenId", smartcontract.ByteArrayType))
md = newMethodAndPrice(n.Properties, 1<<15, callflag.ReadStates)
n.AddMethod(md, desc)
desc = newDescriptor("tokens", smartcontract.InteropInterfaceType)
md = newMethodAndPrice(n.tokens, 1<<15, callflag.ReadStates)
n.AddMethod(md, desc)
desc = newDescriptor("tokensOf", smartcontract.InteropInterfaceType,
manifest.NewParameter("owner", smartcontract.Hash160Type))
md = newMethodAndPrice(n.tokensOf, 1<<15, callflag.ReadStates)
n.AddMethod(md, desc)
desc = newDescriptor("transfer", smartcontract.BoolType,
manifest.NewParameter("to", smartcontract.Hash160Type),
manifest.NewParameter("tokenId", smartcontract.ByteArrayType),
manifest.NewParameter("data", smartcontract.AnyType))
md = newMethodAndPrice(n.transfer, 1<<17, callflag.States|callflag.AllowNotify)
md.StorageFee = 50
n.AddMethod(md, desc)
n.AddEvent("Transfer",
manifest.NewParameter("from", smartcontract.Hash160Type),
manifest.NewParameter("to", smartcontract.Hash160Type),
manifest.NewParameter("amount", smartcontract.IntegerType),
manifest.NewParameter("tokenId", smartcontract.ByteArrayType))
return n
}
// Initialize implements interop.Contract interface.
func (n nonfungible) Initialize(ic *interop.Context) error {
return setIntWithKey(n.ID, ic.DAO, nftTotalSupplyKey, 0)
}
func (n *nonfungible) symbol(_ *interop.Context, _ []stackitem.Item) stackitem.Item {
return stackitem.NewByteArray([]byte(n.tokenSymbol))
}
func (n *nonfungible) decimals(_ *interop.Context, _ []stackitem.Item) stackitem.Item {
return stackitem.NewBigInteger(big.NewInt(int64(n.tokenDecimals)))
}
func (n *nonfungible) totalSupply(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
return stackitem.NewBigInteger(n.TotalSupply(ic.DAO))
}
func (n *nonfungible) TotalSupply(d dao.DAO) *big.Int {
si := d.GetStorageItem(n.ID, nftTotalSupplyKey)
if si == nil {
panic(errors.New("total supply is not initialized"))
}
return bigint.FromBytes(si)
}
func (n *nonfungible) setTotalSupply(d dao.DAO, ts *big.Int) {
err := d.PutStorageItem(n.ID, nftTotalSupplyKey, bigint.ToBytes(ts))
if err != nil {
panic(err)
}
}
func (n *nonfungible) tokenState(d dao.DAO, tokenID []byte) (nftTokenState, []byte, error) {
key := n.getTokenKey(tokenID)
s := n.newTokenState()
err := getSerializableFromDAO(n.ID, d, key, s)
return s, key, err
}
func (n *nonfungible) accountState(d dao.DAO, owner util.Uint160) (*state.NFTAccountState, []byte, error) {
acc := new(state.NFTAccountState)
keyAcc := makeNFTAccountKey(owner)
err := getSerializableFromDAO(n.ID, d, keyAcc, acc)
return acc, keyAcc, err
}
func (n *nonfungible) putAccountState(d dao.DAO, key []byte, acc *state.NFTAccountState) {
var err error
if acc.Balance.Sign() == 0 {
err = d.DeleteStorageItem(n.ID, key)
} else {
err = putSerializableToDAO(n.ID, d, key, acc)
}
if err != nil {
panic(err)
}
}
func (n *nonfungible) OwnerOf(ic *interop.Context, args []stackitem.Item) stackitem.Item {
tokenID, err := args[0].TryBytes()
if err != nil {
panic(err)
}
s, _, err := n.tokenState(ic.DAO, tokenID)
if err != nil {
panic(err)
}
return stackitem.NewByteArray(s.Base().Owner.BytesBE())
}
func (n *nonfungible) Properties(ic *interop.Context, args []stackitem.Item) stackitem.Item {
tokenID, err := args[0].TryBytes()
if err != nil {
panic(err)
}
s, _, err := n.tokenState(ic.DAO, tokenID)
if err != nil {
panic(err)
}
return s.ToMap()
}
func (n *nonfungible) BalanceOf(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
s, _, err := n.accountState(ic.DAO, owner)
if err != nil {
if errors.Is(err, storage.ErrKeyNotFound) {
return stackitem.NewBigInteger(big.NewInt(0))
}
panic(err)
}
return stackitem.NewBigInteger(&s.Balance)
}
func (n *nonfungible) tokens(ic *interop.Context, args []stackitem.Item) stackitem.Item {
prefix := []byte{prefixNFTToken}
siMap, err := ic.DAO.GetStorageItemsWithPrefix(n.ID, prefix)
if err != nil {
panic(err)
}
filteredMap := stackitem.NewMap()
for k, v := range siMap {
filteredMap.Add(stackitem.NewByteArray(append(prefix, []byte(k)...)), stackitem.NewByteArray(v))
}
sort.Slice(filteredMap.Value().([]stackitem.MapElement), func(i, j int) bool {
return bytes.Compare(filteredMap.Value().([]stackitem.MapElement)[i].Key.Value().([]byte),
filteredMap.Value().([]stackitem.MapElement)[j].Key.Value().([]byte)) == -1
})
iter := istorage.NewIterator(filteredMap, 1, istorage.FindValuesOnly|istorage.FindDeserialize|istorage.FindPick1)
return stackitem.NewInterop(iter)
}
func (n *nonfungible) tokensOf(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
s, _, err := n.accountState(ic.DAO, owner)
if err != nil {
panic(err)
}
arr := make([]stackitem.Item, len(s.Tokens))
for i := range arr {
arr[i] = stackitem.NewByteArray(s.Tokens[i])
}
iter := newArrayIterator(arr)
return stackitem.NewInterop(iter)
}
func (n *nonfungible) mint(ic *interop.Context, s nftTokenState) {
key := n.getTokenKey(s.ID())
if ic.DAO.GetStorageItem(n.ID, key) != nil {
panic("token is already minted")
}
if err := putSerializableToDAO(n.ID, ic.DAO, key, s); err != nil {
panic(err)
}
owner := s.Base().Owner
acc, keyAcc, err := n.accountState(ic.DAO, owner)
if err != nil && !errors.Is(err, storage.ErrKeyNotFound) {
panic(err)
}
acc.Add(s.ID())
n.putAccountState(ic.DAO, keyAcc, acc)
ts := n.TotalSupply(ic.DAO)
ts.Add(ts, intOne)
n.setTotalSupply(ic.DAO, ts)
n.postTransfer(ic, nil, &owner, s.ID(), stackitem.Null{})
}
func (n *nonfungible) postTransfer(ic *interop.Context, from, to *util.Uint160, tokenID []byte, data stackitem.Item) {
ne := state.NotificationEvent{
ScriptHash: n.Hash,
Name: "Transfer",
Item: stackitem.NewArray([]stackitem.Item{
addrToStackItem(from),
addrToStackItem(to),
stackitem.NewBigInteger(intOne),
stackitem.NewByteArray(tokenID),
}),
}
ic.Notifications = append(ic.Notifications, ne)
if to == nil {
return
}
cs, err := ic.GetContract(*to)
if err != nil {
return
}
fromArg := stackitem.Item(stackitem.Null{})
if from != nil {
fromArg = stackitem.NewByteArray((*from).BytesBE())
}
args := []stackitem.Item{
fromArg,
stackitem.NewBigInteger(intOne),
stackitem.NewByteArray(tokenID),
data,
}
if err := contract.CallFromNative(ic, n.Hash, cs, manifest.MethodOnNEP11Payment, args, false); err != nil {
panic(err)
}
}
var _ = (*nonfungible).burn // fix unused warning
func (n *nonfungible) burn(ic *interop.Context, tokenID []byte) {
key := n.getTokenKey(tokenID)
n.burnByKey(ic, key)
}
func (n *nonfungible) burnByKey(ic *interop.Context, key []byte) {
token := n.newTokenState()
err := getSerializableFromDAO(n.ID, ic.DAO, key, token)
if err != nil {
panic(err)
}
if err := ic.DAO.DeleteStorageItem(n.ID, key); err != nil {
panic(err)
}
owner := token.Base().Owner
acc, keyAcc, err := n.accountState(ic.DAO, owner)
if err != nil {
panic(err)
}
id := token.ID()
acc.Remove(id)
n.putAccountState(ic.DAO, keyAcc, acc)
ts := n.TotalSupply(ic.DAO)
ts.Sub(ts, intOne)
n.setTotalSupply(ic.DAO, ts)
n.postTransfer(ic, &owner, nil, id, stackitem.Null{})
}
func (n *nonfungible) transfer(ic *interop.Context, args []stackitem.Item) stackitem.Item {
to := toUint160(args[0])
tokenID, err := args[1].TryBytes()
if err != nil {
panic(err)
}
token, tokenKey, err := n.tokenState(ic.DAO, tokenID)
if err != nil {
panic(err)
}
from := token.Base().Owner
ok, err := runtime.CheckHashedWitness(ic, from)
if err != nil || !ok {
return stackitem.NewBool(false)
}
if from != to {
acc, key, err := n.accountState(ic.DAO, from)
if err != nil {
panic(err)
}
acc.Remove(tokenID)
n.putAccountState(ic.DAO, key, acc)
token.Base().Owner = to
n.onTransferred(token)
err = putSerializableToDAO(n.ID, ic.DAO, tokenKey, token)
if err != nil {
panic(err)
}
acc, key, err = n.accountState(ic.DAO, to)
if err != nil && !errors.Is(err, storage.ErrKeyNotFound) {
panic(err)
}
acc.Add(tokenID)
n.putAccountState(ic.DAO, key, acc)
}
n.postTransfer(ic, &from, &to, tokenID, args[2])
return stackitem.NewBool(true)
}
func makeNFTAccountKey(owner util.Uint160) []byte {
return append([]byte{prefixNFTAccount}, owner.BytesBE()...)
}
type arrayWrapper struct {
index int
value []stackitem.Item
}
func newArrayIterator(arr []stackitem.Item) *arrayWrapper {
return &arrayWrapper{
index: -1,
value: arr,
}
}
func (a *arrayWrapper) Next() bool {
if next := a.index + 1; next < len(a.value) {
a.index = next
return true
}
return false
}
func (a *arrayWrapper) Value() stackitem.Item {
return a.value[a.index]
}

View file

@ -1,14 +0,0 @@
package native
import (
"testing"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest/standard"
"github.com/stretchr/testify/require"
)
func TestNonfungibleNEP11(t *testing.T) {
n := newNonFungible("NFToken", -100, "SYM", 1)
require.NoError(t, standard.Check(&n.ContractMD.Manifest, manifest.NEP11StandardName))
}

View file

@ -1,404 +0,0 @@
package core
import (
"testing"
"github.com/nspcc-dev/neo-go/internal/testchain"
"github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
"github.com/nspcc-dev/neo-go/pkg/core/native"
"github.com/nspcc-dev/neo-go/pkg/core/native/nnsrecords"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/stretchr/testify/require"
)
func TestNameService_Price(t *testing.T) {
bc := newTestChain(t)
testGetSet(t, bc, bc.contracts.NameService.Hash, "Price",
native.DefaultDomainPrice, 1, 10000_00000000)
}
func TestNonfungible(t *testing.T) {
bc := newTestChain(t)
acc := newAccountWithGAS(t, bc)
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "symbol", "NNS")
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "decimals", 0)
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "totalSupply", 0)
}
func TestAddRoot(t *testing.T) {
bc := newTestChain(t)
transferFundsToCommittee(t, bc)
nsHash := bc.contracts.NameService.Hash
t.Run("invalid format", func(t *testing.T) {
testNameServiceInvoke(t, bc, "addRoot", nil, "")
})
t.Run("not signed by committee", func(t *testing.T) {
aer, err := invokeContractMethod(bc, 1000_0000, nsHash, "addRoot", "some")
require.NoError(t, err)
checkFAULTState(t, aer)
})
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "some")
t.Run("already exists", func(t *testing.T) {
testNameServiceInvoke(t, bc, "addRoot", nil, "some")
})
}
func TestExpiration(t *testing.T) {
bc := newTestChain(t)
transferFundsToCommittee(t, bc)
acc := newAccountWithGAS(t, bc)
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "com")
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, acc, "register",
true, "first.com", acc.Contract.ScriptHash())
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc,
"setRecord", stackitem.Null{}, "first.com", int64(nnsrecords.TXT), "sometext")
b1 := bc.topBlock.Load().(*block.Block)
tx, err := prepareContractMethodInvokeGeneric(bc, defaultRegisterSysfee, bc.contracts.NameService.Hash,
"register", acc, "second.com", acc.Contract.ScriptHash())
require.NoError(t, err)
b2 := newBlockCustom(bc.GetConfig(), func(b *block.Block) {
b.Index = b1.Index + 1
b.PrevHash = b1.Hash()
b.Timestamp = b1.Timestamp + 10000
}, tx)
require.NoError(t, bc.AddBlock(b2))
checkTxHalt(t, bc, tx.Hash())
tx, err = prepareContractMethodInvokeGeneric(bc, defaultNameServiceSysfee, bc.contracts.NameService.Hash,
"isAvailable", acc, "first.com")
require.NoError(t, err)
b3 := newBlockCustom(bc.GetConfig(), func(b *block.Block) {
b.Index = b2.Index + 1
b.PrevHash = b2.Hash()
b.Timestamp = b1.Timestamp + (secondsInYear+1)*1000
}, tx)
require.NoError(t, bc.AddBlock(b3))
aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application)
require.NoError(t, err)
checkResult(t, &aer[0], stackitem.NewBool(true))
tx, err = prepareContractMethodInvokeGeneric(bc, defaultNameServiceSysfee, bc.contracts.NameService.Hash,
"isAvailable", acc, "second.com")
require.NoError(t, err)
b4 := newBlockCustom(bc.GetConfig(), func(b *block.Block) {
b.Index = b3.Index + 1
b.PrevHash = b3.Hash()
b.Timestamp = b3.Timestamp + 1000
}, tx)
require.NoError(t, bc.AddBlock(b4))
aer, err = bc.GetAppExecResults(tx.Hash(), trigger.Application)
require.NoError(t, err)
checkResult(t, &aer[0], stackitem.NewBool(false))
tx, err = prepareContractMethodInvokeGeneric(bc, defaultNameServiceSysfee, bc.contracts.NameService.Hash,
"getRecord", acc, "first.com", int64(nnsrecords.TXT))
require.NoError(t, err)
b5 := newBlockCustom(bc.GetConfig(), func(b *block.Block) {
b.Index = b4.Index + 1
b.PrevHash = b4.Hash()
b.Timestamp = b4.Timestamp + 1000
}, tx)
require.NoError(t, bc.AddBlock(b5))
aer, err = bc.GetAppExecResults(tx.Hash(), trigger.Application)
require.NoError(t, err)
checkResult(t, &aer[0], stackitem.Null{})
}
const secondsInYear = 365 * 24 * 3600
func TestRegisterAndRenew(t *testing.T) {
bc := newTestChain(t)
transferFundsToCommittee(t, bc)
testNameServiceInvoke(t, bc, "isAvailable", nil, "neo.com")
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "org")
testNameServiceInvoke(t, bc, "isAvailable", nil, "neo.com")
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "com")
testNameServiceInvoke(t, bc, "isAvailable", true, "neo.com")
testNameServiceInvoke(t, bc, "register", nil, "neo.org", testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, "register", nil, "docs.neo.org", testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, "register", nil, "\nneo.com'", testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, "register", nil, "neo.com\n", testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, "register", nil, "neo.com", testchain.CommitteeScriptHash())
testNameServiceInvokeAux(t, bc, native.DefaultDomainPrice, true, "register",
nil, "neo.com", testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, "isAvailable", true, "neo.com")
testNameServiceInvoke(t, bc, "balanceOf", 0, testchain.CommitteeScriptHash())
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, true, "register",
true, "neo.com", testchain.CommitteeScriptHash())
topBlock := bc.topBlock.Load().(*block.Block)
expectedExpiration := topBlock.Timestamp/1000 + secondsInYear
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, true, "register",
false, "neo.com", testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, "isAvailable", false, "neo.com")
props := stackitem.NewMap()
props.Add(stackitem.Make("name"), stackitem.Make("neo.com"))
props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration))
testNameServiceInvoke(t, bc, "properties", props, "neo.com")
testNameServiceInvoke(t, bc, "balanceOf", 1, testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, "ownerOf", testchain.CommitteeScriptHash().BytesBE(), []byte("neo.com"))
t.Run("invalid token ID", func(t *testing.T) {
testNameServiceInvoke(t, bc, "properties", nil, "not.exists")
testNameServiceInvoke(t, bc, "ownerOf", nil, "not.exists")
testNameServiceInvoke(t, bc, "properties", nil, []interface{}{})
testNameServiceInvoke(t, bc, "ownerOf", nil, []interface{}{})
})
// Renew
expectedExpiration += secondsInYear
testNameServiceInvokeAux(t, bc, 100_0000_0000, true, "renew", expectedExpiration, "neo.com")
props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration))
testNameServiceInvoke(t, bc, "properties", props, "neo.com")
}
func TestSetGetRecord(t *testing.T) {
bc := newTestChain(t)
transferFundsToCommittee(t, bc)
acc := newAccountWithGAS(t, bc)
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "com")
t.Run("set before register", func(t *testing.T) {
testNameServiceInvoke(t, bc, "setRecord", nil, "neo.com", int64(nnsrecords.TXT), "sometext")
})
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, true, "register",
true, "neo.com", testchain.CommitteeScriptHash())
t.Run("invalid parameters", func(t *testing.T) {
testNameServiceInvoke(t, bc, "setRecord", nil, "neo.com", int64(0xFF), "1.2.3.4")
testNameServiceInvoke(t, bc, "setRecord", nil, "neo.com", int64(nnsrecords.A), "not.an.ip.address")
})
t.Run("invalid witness", func(t *testing.T) {
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "setRecord", nil,
"neo.com", int64(nnsrecords.A), "1.2.3.4")
})
testNameServiceInvoke(t, bc, "getRecord", stackitem.Null{}, "neo.com", int64(nnsrecords.A))
testNameServiceInvoke(t, bc, "setRecord", stackitem.Null{}, "neo.com", int64(nnsrecords.A), "1.2.3.4")
testNameServiceInvoke(t, bc, "getRecord", "1.2.3.4", "neo.com", int64(nnsrecords.A))
testNameServiceInvoke(t, bc, "setRecord", stackitem.Null{}, "neo.com", int64(nnsrecords.A), "1.2.3.4")
testNameServiceInvoke(t, bc, "getRecord", "1.2.3.4", "neo.com", int64(nnsrecords.A))
testNameServiceInvoke(t, bc, "setRecord", stackitem.Null{}, "neo.com", int64(nnsrecords.AAAA), "2001:0000:1f1f:0000:0000:0100:11a0:addf")
testNameServiceInvoke(t, bc, "setRecord", stackitem.Null{}, "neo.com", int64(nnsrecords.CNAME), "nspcc.ru")
testNameServiceInvoke(t, bc, "setRecord", stackitem.Null{}, "neo.com", int64(nnsrecords.TXT), "sometext")
// Delete record.
t.Run("invalid witness", func(t *testing.T) {
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "setRecord", nil,
"neo.com", int64(nnsrecords.CNAME))
})
testNameServiceInvoke(t, bc, "getRecord", "nspcc.ru", "neo.com", int64(nnsrecords.CNAME))
testNameServiceInvoke(t, bc, "deleteRecord", stackitem.Null{}, "neo.com", int64(nnsrecords.CNAME))
testNameServiceInvoke(t, bc, "getRecord", stackitem.Null{}, "neo.com", int64(nnsrecords.CNAME))
testNameServiceInvoke(t, bc, "getRecord", "1.2.3.4", "neo.com", int64(nnsrecords.A))
}
func TestSetAdmin(t *testing.T) {
bc := newTestChain(t)
transferFundsToCommittee(t, bc)
owner := newAccountWithGAS(t, bc)
admin := newAccountWithGAS(t, bc)
guest := newAccountWithGAS(t, bc)
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "com")
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, owner, "register", true,
"neo.com", owner.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, guest, "setAdmin", nil,
"neo.com", admin.PrivateKey().GetScriptHash())
// Must be witnessed by both owner and admin.
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, owner, "setAdmin", nil,
"neo.com", admin.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, admin, "setAdmin", nil,
"neo.com", admin.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, []*wallet.Account{owner, admin},
"setAdmin", stackitem.Null{},
"neo.com", admin.PrivateKey().GetScriptHash())
t.Run("set and delete by admin", func(t *testing.T) {
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, admin, "setRecord", stackitem.Null{},
"neo.com", int64(nnsrecords.TXT), "sometext")
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, guest, "deleteRecord", nil,
"neo.com", int64(nnsrecords.TXT))
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, admin, "deleteRecord", stackitem.Null{},
"neo.com", int64(nnsrecords.TXT))
})
t.Run("set admin to null", func(t *testing.T) {
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, admin, "setRecord", stackitem.Null{},
"neo.com", int64(nnsrecords.TXT), "sometext")
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, owner, "setAdmin", stackitem.Null{},
"neo.com", nil)
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, admin, "deleteRecord", nil,
"neo.com", int64(nnsrecords.TXT))
})
}
func TestTransfer(t *testing.T) {
bc := newTestChain(t)
transferFundsToCommittee(t, bc)
from := newAccountWithGAS(t, bc)
to := newAccountWithGAS(t, bc)
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "com")
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, from, "register",
true, "neo.com", from.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, from, "setRecord", stackitem.Null{},
"neo.com", int64(nnsrecords.A), "1.2.3.4")
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, from, "transfer",
nil, to.Contract.ScriptHash().BytesBE(), []byte("not.exists"), nil)
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, true, "transfer",
false, to.Contract.ScriptHash().BytesBE(), []byte("neo.com"), nil)
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, from, "transfer",
true, to.Contract.ScriptHash().BytesBE(), []byte("neo.com"), nil)
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, from, "totalSupply", 1)
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, from, "ownerOf",
to.Contract.ScriptHash().BytesBE(), []byte("neo.com"))
cs, cs2 := getTestContractState(bc) // cs2 doesn't have OnNEP11Transfer
require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs))
require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs2))
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, to, "transfer",
nil, cs2.Hash.BytesBE(), []byte("neo.com"), nil)
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, to, "transfer",
true, cs.Hash.BytesBE(), []byte("neo.com"), nil)
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, from, "totalSupply", 1)
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, from, "ownerOf",
cs.Hash.BytesBE(), []byte("neo.com"))
}
func TestTokensOf(t *testing.T) {
bc := newTestChain(t)
transferFundsToCommittee(t, bc)
acc1 := newAccountWithGAS(t, bc)
acc2 := newAccountWithGAS(t, bc)
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "com")
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, acc1, "register",
true, "neo.com", acc1.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, acc2, "register",
true, "nspcc.com", acc2.PrivateKey().GetScriptHash())
testTokensOf(t, bc, acc1, [][]byte{[]byte("neo.com")}, acc1.Contract.ScriptHash().BytesBE())
testTokensOf(t, bc, acc1, [][]byte{[]byte("nspcc.com")}, acc2.Contract.ScriptHash().BytesBE())
testTokensOf(t, bc, acc1, [][]byte{[]byte("neo.com"), []byte("nspcc.com")})
testTokensOf(t, bc, acc1, nil, util.Uint160{}.BytesBE())
}
func testTokensOf(t *testing.T, bc *Blockchain, signer *wallet.Account, result [][]byte, args ...interface{}) {
method := "tokensOf"
if len(args) == 0 {
method = "tokens"
}
w := io.NewBufBinWriter()
emit.AppCall(w.BinWriter, bc.contracts.NameService.Hash, method, callflag.All, args...)
for range result {
emit.Opcodes(w.BinWriter, opcode.DUP)
emit.Syscall(w.BinWriter, interopnames.SystemIteratorNext)
emit.Opcodes(w.BinWriter, opcode.ASSERT)
emit.Opcodes(w.BinWriter, opcode.DUP)
emit.Syscall(w.BinWriter, interopnames.SystemIteratorValue)
emit.Opcodes(w.BinWriter, opcode.SWAP)
}
emit.Opcodes(w.BinWriter, opcode.DROP)
emit.Int(w.BinWriter, int64(len(result)))
emit.Opcodes(w.BinWriter, opcode.PACK)
require.NoError(t, w.Err)
script := w.Bytes()
tx := transaction.New(script, defaultNameServiceSysfee)
tx.ValidUntilBlock = bc.BlockHeight() + 1
signTxWithAccounts(bc, tx, signer)
aers, err := persistBlock(bc, tx)
require.NoError(t, err)
if result == nil {
checkFAULTState(t, aers[0])
return
}
arr := make([]stackitem.Item, 0, len(result))
for i := len(result) - 1; i >= 0; i-- {
arr = append(arr, stackitem.Make(result[i]))
}
checkResult(t, aers[0], stackitem.NewArray(arr))
}
func TestResolve(t *testing.T) {
bc := newTestChain(t)
transferFundsToCommittee(t, bc)
acc := newAccountWithGAS(t, bc)
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "com")
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, acc, "register",
true, "neo.com", acc.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{},
"neo.com", int64(nnsrecords.A), "1.2.3.4")
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{},
"neo.com", int64(nnsrecords.CNAME), "alias.com")
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, acc, "register",
true, "alias.com", acc.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{},
"alias.com", int64(nnsrecords.TXT), "sometxt")
testNameServiceInvoke(t, bc, "resolve", "1.2.3.4",
"neo.com", int64(nnsrecords.A))
testNameServiceInvoke(t, bc, "resolve", "alias.com",
"neo.com", int64(nnsrecords.CNAME))
testNameServiceInvoke(t, bc, "resolve", "sometxt",
"neo.com", int64(nnsrecords.TXT))
testNameServiceInvoke(t, bc, "resolve", stackitem.Null{},
"neo.com", int64(nnsrecords.AAAA))
}
const (
defaultNameServiceSysfee = 4000_0000
defaultRegisterSysfee = 10_0000_0000 + native.DefaultDomainPrice
)
func testNameServiceInvoke(t *testing.T, bc *Blockchain, method string, result interface{}, args ...interface{}) {
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, true, method, result, args...)
}
func testNameServiceInvokeAux(t *testing.T, bc *Blockchain, sysfee int64, signer interface{}, method string, result interface{}, args ...interface{}) {
if sysfee < 0 {
sysfee = defaultNameServiceSysfee
}
aer, err := invokeContractMethodGeneric(bc, sysfee, bc.contracts.NameService.Hash, method, signer, args...)
require.NoError(t, err)
if result == nil {
checkFAULTState(t, aer)
return
}
checkResult(t, aer, stackitem.Make(result))
}
func newAccountWithGAS(t *testing.T, bc *Blockchain) *wallet.Account {
acc, err := wallet.NewAccount()
require.NoError(t, err)
transferTokenFromMultisigAccount(t, bc, acc.PrivateKey().GetScriptHash(), bc.contracts.GAS.Hash, 1000_00000000)
return acc
}

View file

@ -0,0 +1,468 @@
package core
import (
"testing"
nns "github.com/nspcc-dev/neo-go/examples/nft-nd-nns"
"github.com/nspcc-dev/neo-go/internal/testchain"
"github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/stretchr/testify/require"
)
func TestNameService_Price(t *testing.T) {
bc, nsHash := newTestChainWithNS(t)
testGetSet(t, bc, nsHash, "Price",
defaultNameServiceDomainPrice, 0, 10000_00000000)
}
func TestNonfungible(t *testing.T) {
bc, nsHash := newTestChainWithNS(t)
acc := newAccountWithGAS(t, bc)
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "symbol", "NNS")
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "decimals", 0)
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "totalSupply", 0)
}
func TestAddRoot(t *testing.T) {
bc, nsHash := newTestChainWithNS(t)
transferFundsToCommittee(t, bc)
t.Run("invalid format", func(t *testing.T) {
testNameServiceInvoke(t, bc, nsHash, "addRoot", nil, "")
})
t.Run("not signed by committee", func(t *testing.T) {
aer, err := invokeContractMethod(bc, 1000_0000, nsHash, "addRoot", "some")
require.NoError(t, err)
checkFAULTState(t, aer)
})
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "some")
t.Run("already exists", func(t *testing.T) {
testNameServiceInvoke(t, bc, nsHash, "addRoot", nil, "some")
})
}
func TestExpiration(t *testing.T) {
bc, nsHash := newTestChainWithNS(t)
transferFundsToCommittee(t, bc)
acc := newAccountWithGAS(t, bc)
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com")
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, acc, "register",
true, "first.com", acc.Contract.ScriptHash())
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc,
"setRecord", stackitem.Null{}, "first.com", int64(nns.TXT), "sometext")
b1 := bc.topBlock.Load().(*block.Block)
tx, err := prepareContractMethodInvokeGeneric(bc, defaultRegisterSysfee, nsHash,
"register", acc, "second.com", acc.Contract.ScriptHash())
require.NoError(t, err)
b2 := newBlockCustom(bc.GetConfig(), func(b *block.Block) {
b.Index = b1.Index + 1
b.PrevHash = b1.Hash()
b.Timestamp = b1.Timestamp + 10000
}, tx)
require.NoError(t, bc.AddBlock(b2))
checkTxHalt(t, bc, tx.Hash())
tx, err = prepareContractMethodInvokeGeneric(bc, defaultNameServiceSysfee, nsHash,
"isAvailable", acc, "first.com")
require.NoError(t, err)
b3 := newBlockCustom(bc.GetConfig(), func(b *block.Block) {
b.Index = b2.Index + 1
b.PrevHash = b2.Hash()
b.Timestamp = b1.Timestamp + (millisecondsInYear + 1)
}, tx)
require.NoError(t, bc.AddBlock(b3))
aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application)
require.NoError(t, err)
checkResult(t, &aer[0], stackitem.NewBool(true))
tx, err = prepareContractMethodInvokeGeneric(bc, defaultNameServiceSysfee, nsHash,
"isAvailable", acc, "second.com")
require.NoError(t, err)
b4 := newBlockCustom(bc.GetConfig(), func(b *block.Block) {
b.Index = b3.Index + 1
b.PrevHash = b3.Hash()
b.Timestamp = b3.Timestamp + 1000
}, tx)
require.NoError(t, bc.AddBlock(b4))
aer, err = bc.GetAppExecResults(tx.Hash(), trigger.Application)
require.NoError(t, err)
checkResult(t, &aer[0], stackitem.NewBool(false))
tx, err = prepareContractMethodInvokeGeneric(bc, defaultNameServiceSysfee, nsHash,
"getRecord", acc, "first.com", int64(nns.TXT))
require.NoError(t, err)
b5 := newBlockCustom(bc.GetConfig(), func(b *block.Block) {
b.Index = b4.Index + 1
b.PrevHash = b4.Hash()
b.Timestamp = b4.Timestamp + 1000
}, tx)
require.NoError(t, bc.AddBlock(b5))
aer, err = bc.GetAppExecResults(tx.Hash(), trigger.Application)
require.NoError(t, err)
checkFAULTState(t, &aer[0]) // name has expired (panic)
}
const millisecondsInYear = 365 * 24 * 3600 * 1000
func TestRegisterAndRenew(t *testing.T) {
bc, nsHash := newTestChainWithNS(t)
transferFundsToCommittee(t, bc)
testNameServiceInvoke(t, bc, nsHash, "isAvailable", nil, "neo.com")
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "org")
testNameServiceInvoke(t, bc, nsHash, "isAvailable", nil, "neo.com")
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com")
testNameServiceInvoke(t, bc, nsHash, "isAvailable", true, "neo.com")
testNameServiceInvoke(t, bc, nsHash, "register", nil, "neo.org", testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, nsHash, "register", nil, "docs.neo.org", testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, nsHash, "register", nil, "\nneo.com'", testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, nsHash, "register", nil, "neo.com\n", testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, nsHash, "register", nil, "neo.com", testchain.CommitteeScriptHash())
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceDomainPrice, true, "register",
nil, "neo.com", testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, nsHash, "isAvailable", true, "neo.com")
testNameServiceInvoke(t, bc, nsHash, "balanceOf", 0, testchain.CommitteeScriptHash())
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, true, "register",
true, "neo.com", testchain.CommitteeScriptHash())
topBlock := bc.topBlock.Load().(*block.Block)
expectedExpiration := topBlock.Timestamp + millisecondsInYear
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, true, "register",
false, "neo.com", testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, nsHash, "isAvailable", false, "neo.com")
props := stackitem.NewMap()
props.Add(stackitem.Make("name"), stackitem.Make("neo.com"))
props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration))
testNameServiceInvoke(t, bc, nsHash, "properties", props, "neo.com")
testNameServiceInvoke(t, bc, nsHash, "balanceOf", 1, testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, nsHash, "ownerOf", testchain.CommitteeScriptHash().BytesBE(), []byte("neo.com"))
t.Run("invalid token ID", func(t *testing.T) {
testNameServiceInvoke(t, bc, nsHash, "properties", nil, "not.exists")
testNameServiceInvoke(t, bc, nsHash, "ownerOf", nil, "not.exists")
testNameServiceInvoke(t, bc, nsHash, "properties", nil, []interface{}{})
testNameServiceInvoke(t, bc, nsHash, "ownerOf", nil, []interface{}{})
})
// Renew
expectedExpiration += millisecondsInYear
testNameServiceInvokeAux(t, bc, nsHash, 100_0000_0000, true, "renew", expectedExpiration, "neo.com")
props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration))
testNameServiceInvoke(t, bc, nsHash, "properties", props, "neo.com")
}
func TestSetGetRecord(t *testing.T) {
bc, nsHash := newTestChainWithNS(t)
transferFundsToCommittee(t, bc)
acc := newAccountWithGAS(t, bc)
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com")
t.Run("set before register", func(t *testing.T) {
testNameServiceInvoke(t, bc, nsHash, "setRecord", nil, "neo.com", int64(nns.TXT), "sometext")
})
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, true, "register",
true, "neo.com", testchain.CommitteeScriptHash())
t.Run("invalid parameters", func(t *testing.T) {
testNameServiceInvoke(t, bc, nsHash, "setRecord", nil, "neo.com", int64(0xFF), "1.2.3.4")
testNameServiceInvoke(t, bc, nsHash, "setRecord", nil, "neo.com", int64(nns.A), "not.an.ip.address")
})
t.Run("invalid witness", func(t *testing.T) {
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "setRecord", nil,
"neo.com", int64(nns.A), "1.2.3.4")
})
testNameServiceInvoke(t, bc, nsHash, "getRecord", stackitem.Null{}, "neo.com", int64(nns.A))
testNameServiceInvoke(t, bc, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.A), "1.2.3.4")
testNameServiceInvoke(t, bc, nsHash, "getRecord", "1.2.3.4", "neo.com", int64(nns.A))
testNameServiceInvoke(t, bc, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.A), "1.2.3.4")
testNameServiceInvoke(t, bc, nsHash, "getRecord", "1.2.3.4", "neo.com", int64(nns.A))
testNameServiceInvoke(t, bc, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.AAAA), "2002:0000:1f1f:0000:0000:0100:11a0:addf")
testNameServiceInvoke(t, bc, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.CNAME), "nspcc.ru")
testNameServiceInvoke(t, bc, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.TXT), "sometext")
// Delete record.
t.Run("invalid witness", func(t *testing.T) {
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "setRecord", nil,
"neo.com", int64(nns.CNAME))
})
testNameServiceInvoke(t, bc, nsHash, "getRecord", "nspcc.ru", "neo.com", int64(nns.CNAME))
testNameServiceInvoke(t, bc, nsHash, "deleteRecord", stackitem.Null{}, "neo.com", int64(nns.CNAME))
testNameServiceInvoke(t, bc, nsHash, "getRecord", stackitem.Null{}, "neo.com", int64(nns.CNAME))
testNameServiceInvoke(t, bc, nsHash, "getRecord", "1.2.3.4", "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 behaviour
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},
{Type: nns.AAAA, Name: "2001:db8::8:800:200c:417a", ShouldFail: true},
{Type: nns.AAAA, Name: "ff01:db8::8:800:200c:417a"},
{Type: nns.AAAA, Name: "ff01::101"},
{Type: nns.AAAA, Name: "::1"},
{Type: nns.AAAA, Name: "::"},
{Type: nns.AAAA, Name: "2001:db8:0:0:8:800:200c:417a", ShouldFail: true},
{Type: nns.AAAA, Name: "ff01:db8:0:0:8:800:200c:417a"},
{Type: nns.AAAA, Name: "ff01:0:0:0:0:0:0:101"},
{Type: nns.AAAA, Name: "2001:0:0:0:0:0:0:101", ShouldFail: true},
{Type: nns.AAAA, Name: "ff01:0:0:0:0:0:0:101"},
{Type: nns.AAAA, Name: "0:0:0:0:0:0:0:1"},
{Type: nns.AAAA, Name: "2001:0:0:0:0:0:0:1", ShouldFail: true},
{Type: nns.AAAA, Name: "0:0:0:0:0:0:0:0"},
{Type: nns.AAAA, Name: "2001:0:0:0:0:0:0:0", ShouldFail: true},
{Type: nns.AAAA, Name: "2001:DB8::8:800:200C:417A", ShouldFail: true},
{Type: nns.AAAA, Name: "FF01:DB8::8:800:200C:417A"},
{Type: nns.AAAA, Name: "FF01::101"},
{Type: nns.AAAA, Name: "fF01::101"},
{Type: nns.AAAA, Name: "2001:DB8:0:0:8:800:200C:417A", ShouldFail: true},
{Type: nns.AAAA, Name: "FF01:DB8:0:0:8:800:200C:417A"},
{Type: nns.AAAA, Name: "FF01:0:0:0:0:0:0:101"},
{Type: nns.AAAA, Name: "::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: "::13.1.68.3", ShouldFail: true},
}
for _, testCase := range testCases {
var expected interface{}
if testCase.ShouldFail {
expected = nil
} else {
expected = stackitem.Null{}
}
t.Run(testCase.Name, func(t *testing.T) {
testNameServiceInvoke(t, bc, nsHash, "setRecord", expected, "neo.com", int64(testCase.Type), testCase.Name)
})
}
})
}
func TestSetAdmin(t *testing.T) {
bc, nsHash := newTestChainWithNS(t)
transferFundsToCommittee(t, bc)
owner := newAccountWithGAS(t, bc)
admin := newAccountWithGAS(t, bc)
guest := newAccountWithGAS(t, bc)
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com")
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, owner, "register", true,
"neo.com", owner.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, guest, "setAdmin", nil,
"neo.com", admin.PrivateKey().GetScriptHash())
// Must be witnessed by both owner and admin.
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, owner, "setAdmin", nil,
"neo.com", admin.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, admin, "setAdmin", nil,
"neo.com", admin.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, []*wallet.Account{owner, admin},
"setAdmin", stackitem.Null{},
"neo.com", admin.PrivateKey().GetScriptHash())
t.Run("set and delete by admin", func(t *testing.T) {
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, admin, "setRecord", stackitem.Null{},
"neo.com", int64(nns.TXT), "sometext")
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, guest, "deleteRecord", nil,
"neo.com", int64(nns.TXT))
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, admin, "deleteRecord", stackitem.Null{},
"neo.com", int64(nns.TXT))
})
t.Run("set admin to null", func(t *testing.T) {
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, admin, "setRecord", stackitem.Null{},
"neo.com", int64(nns.TXT), "sometext")
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, owner, "setAdmin", stackitem.Null{},
"neo.com", nil)
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, admin, "deleteRecord", nil,
"neo.com", int64(nns.TXT))
})
}
func TestTransfer(t *testing.T) {
bc, nsHash := newTestChainWithNS(t)
transferFundsToCommittee(t, bc)
from := newAccountWithGAS(t, bc)
to := newAccountWithGAS(t, bc)
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com")
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, from, "register",
true, "neo.com", from.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, from, "setRecord", stackitem.Null{},
"neo.com", int64(nns.A), "1.2.3.4")
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, from, "transfer",
nil, to.Contract.ScriptHash().BytesBE(), []byte("not.exists"), nil)
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, true, "transfer",
false, to.Contract.ScriptHash().BytesBE(), []byte("neo.com"), nil)
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, from, "transfer",
true, to.Contract.ScriptHash().BytesBE(), []byte("neo.com"), nil)
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, from, "totalSupply", 1)
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, from, "ownerOf",
to.Contract.ScriptHash().BytesBE(), []byte("neo.com"))
cs, cs2 := getTestContractState(bc) // cs2 doesn't have OnNEP11Transfer
require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs))
require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs2))
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, to, "transfer",
nil, cs2.Hash.BytesBE(), []byte("neo.com"), nil)
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, to, "transfer",
true, cs.Hash.BytesBE(), []byte("neo.com"), nil)
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, from, "totalSupply", 1)
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, from, "ownerOf",
cs.Hash.BytesBE(), []byte("neo.com"))
}
func TestTokensOf(t *testing.T) {
bc, nsHash := newTestChainWithNS(t)
transferFundsToCommittee(t, bc)
acc1 := newAccountWithGAS(t, bc)
acc2 := newAccountWithGAS(t, bc)
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com")
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, acc1, "register",
true, "neo.com", acc1.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, acc2, "register",
true, "nspcc.com", acc2.PrivateKey().GetScriptHash())
testTokensOf(t, bc, nsHash, acc1, [][]byte{[]byte("neo.com")}, acc1.Contract.ScriptHash().BytesBE())
testTokensOf(t, bc, nsHash, acc1, [][]byte{[]byte("nspcc.com")}, acc2.Contract.ScriptHash().BytesBE())
testTokensOf(t, bc, nsHash, acc1, [][]byte{[]byte("neo.com"), []byte("nspcc.com")})
testTokensOf(t, bc, nsHash, acc1, [][]byte{}, util.Uint160{}.BytesBE()) // empty hash is a valid hash still
}
func testTokensOf(t *testing.T, bc *Blockchain, nsHash util.Uint160, signer *wallet.Account, result [][]byte, args ...interface{}) {
method := "tokensOf"
if len(args) == 0 {
method = "tokens"
}
w := io.NewBufBinWriter()
emit.AppCall(w.BinWriter, nsHash, method, callflag.All, args...)
for range result {
emit.Opcodes(w.BinWriter, opcode.DUP)
emit.Syscall(w.BinWriter, interopnames.SystemIteratorNext)
emit.Opcodes(w.BinWriter, opcode.ASSERT)
emit.Opcodes(w.BinWriter, opcode.DUP)
emit.Syscall(w.BinWriter, interopnames.SystemIteratorValue)
emit.Opcodes(w.BinWriter, opcode.SWAP)
}
emit.Opcodes(w.BinWriter, opcode.DROP)
emit.Int(w.BinWriter, int64(len(result)))
emit.Opcodes(w.BinWriter, opcode.PACK)
require.NoError(t, w.Err)
script := w.Bytes()
tx := transaction.New(script, defaultNameServiceSysfee)
tx.ValidUntilBlock = bc.BlockHeight() + 1
signTxWithAccounts(bc, tx, signer)
aers, err := persistBlock(bc, tx)
require.NoError(t, err)
if result == nil {
checkFAULTState(t, aers[0])
return
}
arr := make([]stackitem.Item, 0, len(result))
for i := len(result) - 1; i >= 0; i-- {
arr = append(arr, stackitem.Make(result[i]))
}
checkResult(t, aers[0], stackitem.NewArray(arr))
}
func TestResolve(t *testing.T) {
bc, nsHash := newTestChainWithNS(t)
transferFundsToCommittee(t, bc)
acc := newAccountWithGAS(t, bc)
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com")
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, acc, "register",
true, "neo.com", acc.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{},
"neo.com", int64(nns.A), "1.2.3.4")
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{},
"neo.com", int64(nns.CNAME), "alias.com")
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, acc, "register",
true, "alias.com", acc.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{},
"alias.com", int64(nns.TXT), "sometxt")
testNameServiceInvoke(t, bc, nsHash, "resolve", "1.2.3.4",
"neo.com", int64(nns.A))
testNameServiceInvoke(t, bc, nsHash, "resolve", "alias.com",
"neo.com", int64(nns.CNAME))
testNameServiceInvoke(t, bc, nsHash, "resolve", "sometxt",
"neo.com", int64(nns.TXT))
testNameServiceInvoke(t, bc, nsHash, "resolve", stackitem.Null{},
"neo.com", int64(nns.AAAA))
}
const (
defaultNameServiceDomainPrice = 10_0000_0000
defaultNameServiceSysfee = 4000_0000
defaultRegisterSysfee = 10_0000_0000 + defaultNameServiceDomainPrice
)
func testNameServiceInvoke(t *testing.T, bc *Blockchain, nsHash util.Uint160, method string, result interface{}, args ...interface{}) {
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, true, method, result, args...)
}
func testNameServiceInvokeAux(t *testing.T, bc *Blockchain, nsHash util.Uint160, sysfee int64, signer interface{}, method string, result interface{}, args ...interface{}) {
if sysfee < 0 {
sysfee = defaultNameServiceSysfee
}
aer, err := invokeContractMethodGeneric(bc, sysfee, nsHash, method, signer, args...)
require.NoError(t, err)
if result == nil {
checkFAULTState(t, aer)
return
}
checkResult(t, aer, stackitem.Make(result))
}
func newAccountWithGAS(t *testing.T, bc *Blockchain) *wallet.Account {
acc, err := wallet.NewAccount()
require.NoError(t, err)
transferTokenFromMultisigAccount(t, bc, acc.PrivateKey().GetScriptHash(), bc.contracts.GAS.Hash, 1000_00000000)
return acc
}

View file

@ -1,174 +0,0 @@
package state
import (
"bytes"
"errors"
"math/big"
"sort"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
// NFTTokenState represents state of nonfungible token.
type NFTTokenState struct {
Owner util.Uint160
Name string
}
// NFTAccountState represents state of nonfunglible account.
type NFTAccountState struct {
NEP17BalanceState
Tokens [][]byte
}
// Base returns base class.
func (s *NFTTokenState) Base() *NFTTokenState {
return s
}
// ToStackItem converts NFTTokenState to stackitem.
func (s *NFTTokenState) ToStackItem() stackitem.Item {
owner := s.Owner
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewByteArray(owner.BytesBE()),
stackitem.NewByteArray([]byte(s.Name)),
})
}
// EncodeBinary implements io.Serializable.
func (s *NFTTokenState) EncodeBinary(w *io.BinWriter) {
stackitem.EncodeBinaryStackItem(s.ToStackItem(), w)
}
// FromStackItem converts stackitem to NFTTokenState.
func (s *NFTTokenState) FromStackItem(item stackitem.Item) error {
arr, ok := item.Value().([]stackitem.Item)
if !ok || len(arr) < 2 {
return errors.New("invalid stack item")
}
bs, err := arr[0].TryBytes()
if err != nil {
return err
}
owner, err := util.Uint160DecodeBytesBE(bs)
if err != nil {
return err
}
name, err := stackitem.ToString(arr[1])
if err != nil {
return err
}
s.Owner = owner
s.Name = name
return nil
}
// DecodeBinary implements io.Serializable.
func (s *NFTTokenState) DecodeBinary(r *io.BinReader) {
item := stackitem.DecodeBinaryStackItem(r)
if r.Err == nil {
r.Err = s.FromStackItem(item)
}
}
// ToMap converts NFTTokenState to Map stackitem.
func (s *NFTTokenState) ToMap() *stackitem.Map {
return stackitem.NewMapWithValue([]stackitem.MapElement{
{
Key: stackitem.NewByteArray([]byte("name")),
Value: stackitem.NewByteArray([]byte(s.Name)),
},
})
}
// ID returns token id.
func (s *NFTTokenState) ID() []byte {
return []byte(s.Name)
}
// ToStackItem converts NFTAccountState to stackitem.
func (s *NFTAccountState) ToStackItem() stackitem.Item {
st := s.NEP17BalanceState.toStackItem().(*stackitem.Struct)
arr := make([]stackitem.Item, len(s.Tokens))
for i := range arr {
arr[i] = stackitem.NewByteArray(s.Tokens[i])
}
st.Append(stackitem.NewArray(arr))
return st
}
// FromStackItem converts stackitem to NFTAccountState.
func (s *NFTAccountState) FromStackItem(item stackitem.Item) error {
s.NEP17BalanceState.fromStackItem(item)
arr := item.Value().([]stackitem.Item)
if len(arr) < 2 {
return errors.New("invalid stack item")
}
arr, ok := arr[1].Value().([]stackitem.Item)
if !ok {
return errors.New("invalid stack item")
}
s.Tokens = make([][]byte, len(arr))
for i := range s.Tokens {
bs, err := arr[i].TryBytes()
if err != nil {
return err
}
s.Tokens[i] = bs
}
return nil
}
// EncodeBinary implements io.Serializable.
func (s *NFTAccountState) EncodeBinary(w *io.BinWriter) {
stackitem.EncodeBinaryStackItem(s.ToStackItem(), w)
}
// DecodeBinary implements io.Serializable.
func (s *NFTAccountState) DecodeBinary(r *io.BinReader) {
item := stackitem.DecodeBinaryStackItem(r)
if r.Err == nil {
r.Err = s.FromStackItem(item)
}
}
func (s *NFTAccountState) index(tokenID []byte) (int, bool) {
lt := len(s.Tokens)
index := sort.Search(lt, func(i int) bool {
return bytes.Compare(s.Tokens[i], tokenID) >= 0
})
return index, index < lt && bytes.Equal(s.Tokens[index], tokenID)
}
// Add adds token id to the set of account tokens
// and returns true on success.
func (s *NFTAccountState) Add(tokenID []byte) bool {
index, isPresent := s.index(tokenID)
if isPresent {
return false
}
s.Balance.Add(&s.Balance, big.NewInt(1))
s.Tokens = append(s.Tokens, []byte{})
copy(s.Tokens[index+1:], s.Tokens[index:])
s.Tokens[index] = tokenID
return true
}
// Remove removes token id to the set of account tokens
// and returns true on success.
func (s *NFTAccountState) Remove(tokenID []byte) bool {
index, isPresent := s.index(tokenID)
if !isPresent {
return false
}
s.Balance.Sub(&s.Balance, big.NewInt(1))
copy(s.Tokens[index:], s.Tokens[index+1:])
s.Tokens = s.Tokens[:len(s.Tokens)-1]
return true
}

View file

@ -1,123 +0,0 @@
package state
import (
"math/big"
"testing"
"github.com/nspcc-dev/neo-go/internal/random"
"github.com/nspcc-dev/neo-go/internal/testserdes"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require"
)
func newStruct(args ...interface{}) *stackitem.Struct {
arr := make([]stackitem.Item, len(args))
for i := range args {
arr[i] = stackitem.Make(args[i])
}
return stackitem.NewStruct(arr)
}
func TestNFTTokenState_Serializable(t *testing.T) {
t.Run("valid", func(t *testing.T) {
s := &NFTTokenState{
Owner: random.Uint160(),
Name: "random name",
}
id := s.ID()
actual := new(NFTTokenState)
testserdes.EncodeDecodeBinary(t, s, actual)
require.Equal(t, id, actual.ID())
})
t.Run("invalid", func(t *testing.T) {
errCases := []struct {
name string
item stackitem.Item
}{
{"invalid type", stackitem.NewByteArray([]byte{1, 2, 3})},
{"invalid owner type",
newStruct(stackitem.NewArray(nil), "name", "desc")},
{"invalid owner uint160", newStruct("123", "name", "desc")},
{"invalid name",
newStruct(random.Uint160().BytesBE(), []byte{0x80}, "desc")},
}
for _, tc := range errCases {
t.Run(tc.name, func(t *testing.T) {
w := io.NewBufBinWriter()
stackitem.EncodeBinaryStackItem(tc.item, w.BinWriter)
require.NoError(t, w.Err)
require.Error(t, testserdes.DecodeBinary(w.Bytes(), new(NFTTokenState)))
})
}
})
}
func TestNFTTokenState_ToMap(t *testing.T) {
s := &NFTTokenState{
Owner: random.Uint160(),
Name: "random name",
}
m := s.ToMap()
elems := m.Value().([]stackitem.MapElement)
i := m.Index(stackitem.Make("name"))
require.True(t, i < len(elems))
require.Equal(t, []byte("random name"), elems[i].Value.Value())
}
func TestNFTAccountState_Serializable(t *testing.T) {
t.Run("good", func(t *testing.T) {
s := &NFTAccountState{
NEP17BalanceState: NEP17BalanceState{
Balance: *big.NewInt(10),
},
Tokens: [][]byte{
{1, 2, 3},
{3},
},
}
testserdes.EncodeDecodeBinary(t, s, new(NFTAccountState))
})
t.Run("invalid", func(t *testing.T) {
errCases := []struct {
name string
item stackitem.Item
}{
{"small size", newStruct(42)},
{"not an array", newStruct(11, stackitem.NewByteArray([]byte{}))},
{"not an array",
newStruct(11, stackitem.NewArray([]stackitem.Item{
stackitem.NewArray(nil),
}))},
}
for _, tc := range errCases {
t.Run(tc.name, func(t *testing.T) {
w := io.NewBufBinWriter()
stackitem.EncodeBinaryStackItem(tc.item, w.BinWriter)
require.NoError(t, w.Err)
require.Error(t, testserdes.DecodeBinary(w.Bytes(), new(NFTAccountState)))
})
}
})
}
func TestNFTAccountState_AddRemove(t *testing.T) {
var s NFTAccountState
require.True(t, s.Add([]byte{1, 2, 3}))
require.EqualValues(t, 1, s.Balance.Int64())
require.True(t, s.Add([]byte{1}))
require.EqualValues(t, 2, s.Balance.Int64())
require.False(t, s.Add([]byte{1, 2, 3}))
require.EqualValues(t, 2, s.Balance.Int64())
require.True(t, s.Remove([]byte{1}))
require.EqualValues(t, 1, s.Balance.Int64())
require.False(t, s.Remove([]byte{1}))
require.EqualValues(t, 1, s.Balance.Int64())
require.True(t, s.Remove([]byte{1, 2, 3}))
require.EqualValues(t, 0, s.Balance.Int64())
}

View file

@ -1,129 +0,0 @@
/*
Package nameservice provides interface to NameService native contract.
It's a NEP-11 contract implementing a domain name service.
*/
package nameservice
import (
"github.com/nspcc-dev/neo-go/pkg/interop"
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
"github.com/nspcc-dev/neo-go/pkg/interop/iterator"
)
// RecordType represents NameService record type.
type RecordType byte
// Various record type.
const (
TypeA RecordType = 1
TypeCNAME RecordType = 5
TypeTXT RecordType = 16
TypeAAAA RecordType = 28
)
// Hash represents NameService contract hash.
const Hash = "\x6b\x59\x2b\x87\x66\xcc\x45\x8e\xfa\x7a\x90\x47\x56\x62\xcd\x92\x03\xcf\x8f\x7a"
// Symbol represents `symbol` method of NameService native contract.
func Symbol() string {
return contract.Call(interop.Hash160(Hash), "symbol", contract.NoneFlag).(string)
}
// Decimals represents `decimals` method of NameService native contract.
func Decimals() int {
return contract.Call(interop.Hash160(Hash), "decimals", contract.NoneFlag).(int)
}
// TotalSupply represents `totalSupply` method of NameService native contract.
func TotalSupply() int {
return contract.Call(interop.Hash160(Hash), "totalSupply", contract.ReadStates).(int)
}
// OwnerOf represents `ownerOf` method of NameService native contract.
func OwnerOf(tokenID string) interop.Hash160 {
return contract.Call(interop.Hash160(Hash), "ownerOf", contract.ReadStates, tokenID).(interop.Hash160)
}
// BalanceOf represents `balanceOf` method of NameService native contract.
func BalanceOf(owner interop.Hash160) int {
return contract.Call(interop.Hash160(Hash), "balanceOf", contract.ReadStates, owner).(int)
}
// Properties represents `properties` method of NameService native contract.
func Properties(tokenID string) map[string]interface{} {
return contract.Call(interop.Hash160(Hash), "properties", contract.ReadStates, tokenID).(map[string]interface{})
}
// Tokens represents `tokens` method of NameService native contract.
func Tokens() iterator.Iterator {
return contract.Call(interop.Hash160(Hash), "tokens",
contract.ReadStates).(iterator.Iterator)
}
// TokensOf represents `tokensOf` method of NameService native contract.
func TokensOf(addr interop.Hash160) iterator.Iterator {
return contract.Call(interop.Hash160(Hash), "tokensOf",
contract.ReadStates, addr).(iterator.Iterator)
}
// Transfer represents `transfer` method of NameService native contract.
func Transfer(to interop.Hash160, tokenID string, data interface{}) bool {
return contract.Call(interop.Hash160(Hash), "transfer",
contract.ReadStates|contract.States|contract.AllowNotify, to, tokenID, data).(bool)
}
// AddRoot represents `addRoot` method of NameService native contract.
func AddRoot(root string) {
contract.Call(interop.Hash160(Hash), "addRoot", contract.States, root)
}
// SetPrice represents `setPrice` method of NameService native contract.
func SetPrice(price int) {
contract.Call(interop.Hash160(Hash), "setPrice", contract.States, price)
}
// GetPrice represents `getPrice` method of NameService native contract.
func GetPrice() int {
return contract.Call(interop.Hash160(Hash), "getPrice", contract.ReadStates).(int)
}
// IsAvailable represents `isAvailable` method of NameService native contract.
func IsAvailable(name string) bool {
return contract.Call(interop.Hash160(Hash), "isAvailable", contract.ReadStates, name).(bool)
}
// Register represents `register` method of NameService native contract.
func Register(name string, owner interop.Hash160) bool {
return contract.Call(interop.Hash160(Hash), "register", contract.States, name, owner).(bool)
}
// Renew represents `renew` method of NameService native contract.
func Renew(name string) int {
return contract.Call(interop.Hash160(Hash), "renew", contract.States, name).(int)
}
// SetAdmin represents `setAdmin` method of NameService native contract.
func SetAdmin(name string, admin interop.Hash160) {
contract.Call(interop.Hash160(Hash), "setAdmin", contract.States, name, admin)
}
// SetRecord represents `setRecord` method of NameService native contract.
func SetRecord(name string, recType RecordType, data string) {
contract.Call(interop.Hash160(Hash), "setRecord", contract.States, name, recType, data)
}
// GetRecord represents `getRecord` method of NameService native contract.
// It returns `nil` if record is missing.
func GetRecord(name string, recType RecordType) []byte {
return contract.Call(interop.Hash160(Hash), "getRecord", contract.ReadStates, name, recType).([]byte)
}
// DeleteRecord represents `deleteRecord` method of NameService native contract.
func DeleteRecord(name string, recType RecordType) {
contract.Call(interop.Hash160(Hash), "deleteRecord", contract.States, name, recType)
}
// Resolve represents `resolve` method of NameService native contract.
func Resolve(name string, recType RecordType) []byte {
return contract.Call(interop.Hash160(Hash), "resolve", contract.ReadStates, name, recType).([]byte)
}

View file

@ -6,11 +6,12 @@ import (
"errors"
"fmt"
nns "github.com/nspcc-dev/neo-go/examples/nft-nd-nns"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/core/native/nnsrecords"
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util"
)
// GetOraclePrice invokes `getPrice` method on a native Oracle contract.
@ -22,12 +23,8 @@ func (c *Client) GetOraclePrice() (int64, error) {
return c.invokeNativeGetMethod(oracleHash, "getPrice")
}
// GetNNSPrice invokes `getPrice` method on a native NameService contract.
func (c *Client) GetNNSPrice() (int64, error) {
nnsHash, err := c.GetNativeContractHash(nativenames.NameService)
if err != nil {
return 0, fmt.Errorf("failed to get native NameService hash: %w", err)
}
// GetNNSPrice invokes `getPrice` method on a NeoNameService contract with the specified hash.
func (c *Client) GetNNSPrice(nnsHash util.Uint160) (int64, error) {
return c.invokeNativeGetMethod(nnsHash, "getPrice")
}
@ -66,16 +63,12 @@ func (c *Client) GetDesignatedByRole(role noderoles.Role, index uint32) (keys.Pu
return topPublicKeysFromStack(result.Stack)
}
// NNSResolve invokes `resolve` method on a native NameService contract.
func (c *Client) NNSResolve(name string, typ nnsrecords.Type) (string, error) {
if typ == nnsrecords.CNAME {
// NNSResolve invokes `resolve` method on a NameService contract with the specified hash.
func (c *Client) NNSResolve(nnsHash util.Uint160, name string, typ nns.RecordType) (string, error) {
if typ == nns.CNAME {
return "", errors.New("can't resolve CNAME record type")
}
rmHash, err := c.GetNativeContractHash(nativenames.NameService)
if err != nil {
return "", fmt.Errorf("failed to get native NameService hash: %w", err)
}
result, err := c.InvokeFunction(rmHash, "resolve", []smartcontract.Parameter{
result, err := c.InvokeFunction(nnsHash, "resolve", []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: name,
@ -95,13 +88,9 @@ func (c *Client) NNSResolve(name string, typ nnsrecords.Type) (string, error) {
return topStringFromStack(result.Stack)
}
// NNSIsAvailable invokes `isAvailable` method on a native NameService contract.
func (c *Client) NNSIsAvailable(name string) (bool, error) {
rmHash, err := c.GetNativeContractHash(nativenames.NameService)
if err != nil {
return false, fmt.Errorf("failed to get native NameService hash: %w", err)
}
result, err := c.InvokeFunction(rmHash, "isAvailable", []smartcontract.Parameter{
// NNSIsAvailable invokes `isAvailable` method on a NeoNameService contract with the specified hash.
func (c *Client) NNSIsAvailable(nnsHash util.Uint160, name string) (bool, error) {
result, err := c.InvokeFunction(nnsHash, "isAvailable", []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: name,

View file

@ -458,7 +458,7 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{
{
name: "positive",
invoke: func(c *Client) (interface{}, error) {
return c.GetNNSPrice()
return c.GetNNSPrice(util.Uint160{1, 2, 3})
},
serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"state":"HALT","gasconsumed":"2007390","script":"EMAMDWdldEZlZVBlckJ5dGUMFJphpG7sl7iTBtfOgfFbRiCR0AkyQWJ9W1I=","stack":[{"type":"Integer","value":"1000000"}],"tx":null}}`,
result: func(c *Client) interface{} {

View file

@ -5,10 +5,10 @@ import (
"encoding/base64"
"testing"
nns "github.com/nspcc-dev/neo-go/examples/nft-nd-nns"
"github.com/nspcc-dev/neo-go/internal/testchain"
"github.com/nspcc-dev/neo-go/pkg/core/fee"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/core/native/nnsrecords"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
@ -784,7 +784,7 @@ func TestClient_NEP11(t *testing.T) {
require.NoError(t, err)
require.NoError(t, c.Init())
h, err := chain.GetNativeContractScriptHash(nativenames.NameService)
h, err := util.Uint160DecodeStringLE(nameServiceContractHash)
require.NoError(t, err)
acc := testchain.PrivateKeyByID(0).GetScriptHash()
@ -807,7 +807,7 @@ func TestClient_NEP11(t *testing.T) {
tok, err := c.NEP11TokenInfo(h)
require.NoError(t, err)
require.Equal(t, &wallet.Token{
Name: nativenames.NameService,
Name: "NameService",
Hash: h,
Decimals: 0,
Symbol: "NNS",
@ -827,12 +827,12 @@ func TestClient_NEP11(t *testing.T) {
t.Run("Properties", func(t *testing.T) {
p, err := c.NEP11Properties(h, "neo.com")
require.NoError(t, err)
blockRegisterDomain, err := chain.GetBlock(chain.GetHeaderHash(13)) // `neo.com` domain was registered in 13th block
blockRegisterDomain, err := chain.GetBlock(chain.GetHeaderHash(14)) // `neo.com` domain was registered in 14th block
require.NoError(t, err)
require.Equal(t, 1, len(blockRegisterDomain.Transactions))
expected := stackitem.NewMap()
expected.Add(stackitem.Make([]byte("name")), stackitem.Make([]byte("neo.com")))
expected.Add(stackitem.Make([]byte("expiration")), stackitem.Make(blockRegisterDomain.Timestamp/1000+365*24*3600)) // expiration formula
expected.Add(stackitem.Make([]byte("expiration")), stackitem.Make(blockRegisterDomain.Timestamp+365*24*3600*1000)) // expiration formula
require.EqualValues(t, expected, p)
})
t.Run("Transfer", func(t *testing.T) {
@ -850,27 +850,30 @@ func TestClient_NNS(t *testing.T) {
require.NoError(t, err)
require.NoError(t, c.Init())
nsHash, err := util.Uint160DecodeStringLE(nameServiceContractHash)
require.NoError(t, err)
t.Run("NNSIsAvailable, false", func(t *testing.T) {
b, err := c.NNSIsAvailable("neo.com")
b, err := c.NNSIsAvailable(nsHash, "neo.com")
require.NoError(t, err)
require.Equal(t, false, b)
})
t.Run("NNSIsAvailable, true", func(t *testing.T) {
b, err := c.NNSIsAvailable("neogo.com")
b, err := c.NNSIsAvailable(nsHash, "neogo.com")
require.NoError(t, err)
require.Equal(t, true, b)
})
t.Run("NNSResolve, good", func(t *testing.T) {
b, err := c.NNSResolve("neo.com", nnsrecords.A)
b, err := c.NNSResolve(nsHash, "neo.com", nns.A)
require.NoError(t, err)
require.Equal(t, "1.2.3.4", b)
})
t.Run("NNSResolve, bad", func(t *testing.T) {
_, err := c.NNSResolve("neogo.com", nnsrecords.A)
_, err := c.NNSResolve(nsHash, "neogo.com", nns.A)
require.Error(t, err)
})
t.Run("NNSResolve, forbidden", func(t *testing.T) {
_, err := c.NNSResolve("neogo.com", nnsrecords.CNAME)
_, err := c.NNSResolve(nsHash, "neogo.com", nns.CNAME)
require.Error(t, err)
})
}

View file

@ -55,13 +55,14 @@ type rpcTestCase struct {
}
const testContractHash = "63cc6571e990dd3f345f699fc9c2a6e49edb89af"
const deploymentTxHash = "4450d0047d4b6a20e85176c709df44fae4c63cfa9a698acb11871554b93016df"
const deploymentTxHash = "9b0c586eb07f8c9b6fc46b05c78d87651d50af8e1f44478827848d826f8cd174"
const genesisBlockHash = "73fe50b5564d57118296cbab0a78fe7cb11c97b7699d07a9a21fab60e79bb8fc"
const verifyContractHash = "c50082e0d8364d61ce6933bd24027a3363474dce"
const verifyContractAVM = "VwMAQS1RCDAhcAwU7p6iLCfjS9AUj8QQjgj3To9QSLLbMHFoE87bKGnbKJdA"
const verifyWithArgsContractHash = "8744ffdd07af8e9f18ab90685c8c2ebfd37c6415"
const invokescriptContractAVM = "VwcADBQBDAMOBQYMDQIODw0DDgcJAAAAANswcGhB+CfsjCGqJgQRQAwUDQ8DAgkAAgEDBwMEBQIBAA4GDAnbMHFpQfgn7IwhqiYEEkATQA=="
const nameServiceContractHash = "60d78a0fc048399438c3764f8a67d0fc86d6e0e6"
var rpcTestCases = map[string][]rpcTestCase{
"getapplicationlog": {
@ -648,7 +649,7 @@ var rpcTestCases = map[string][]rpcTestCase{
require.True(t, ok)
expected := result.UnclaimedGas{
Address: testchain.MultisigScriptHash(),
Unclaimed: *big.NewInt(7000),
Unclaimed: *big.NewInt(7500),
}
assert.Equal(t, expected, *actual)
},
@ -1407,7 +1408,7 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
require.NoErrorf(t, err, "could not parse response: %s", txOut)
assert.Equal(t, *block.Transactions[0], actual.Transaction)
assert.Equal(t, 15, actual.Confirmations)
assert.Equal(t, 16, actual.Confirmations)
assert.Equal(t, TXHash, actual.Transaction.Hash())
})
@ -1525,12 +1526,12 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
require.NoError(t, json.Unmarshal(res, actual))
checkNep17TransfersAux(t, e, actual, sent, rcvd)
}
t.Run("time frame only", func(t *testing.T) { testNEP17T(t, 4, 5, 0, 0, []int{8, 9, 10, 11}, []int{2, 3}) })
t.Run("time frame only", func(t *testing.T) { testNEP17T(t, 4, 5, 0, 0, []int{9, 10, 11, 12}, []int{2, 3}) })
t.Run("no res", func(t *testing.T) { testNEP17T(t, 100, 100, 0, 0, []int{}, []int{}) })
t.Run("limit", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 0, []int{5, 6}, []int{1}) })
t.Run("limit 2", func(t *testing.T) { testNEP17T(t, 4, 5, 2, 0, []int{8}, []int{2}) })
t.Run("limit with page", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 1, []int{7, 8}, []int{2}) })
t.Run("limit with page 2", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 2, []int{9, 10}, []int{3}) })
t.Run("limit", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 0, []int{6, 7}, []int{1}) })
t.Run("limit 2", func(t *testing.T) { testNEP17T(t, 4, 5, 2, 0, []int{9}, []int{2}) })
t.Run("limit with page", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 1, []int{8, 9}, []int{2}) })
t.Run("limit with page 2", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 2, []int{10, 11}, []int{3}) })
})
}
@ -1633,8 +1634,8 @@ func checkNep17Balances(t *testing.T, e *executor, acc interface{}) {
},
{
Asset: e.chain.UtilityTokenHash(),
Amount: "67960000780",
LastUpdated: 14,
Amount: "57941360260",
LastUpdated: 15,
}},
Address: testchain.PrivateKeyByID(0).GetScriptHash().StringLE(),
}
@ -1643,7 +1644,7 @@ func checkNep17Balances(t *testing.T, e *executor, acc interface{}) {
}
func checkNep17Transfers(t *testing.T, e *executor, acc interface{}) {
checkNep17TransfersAux(t, e, acc, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}, []int{0, 1, 2, 3, 4, 5, 6, 7})
checkNep17TransfersAux(t, e, acc, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}, []int{0, 1, 2, 3, 4, 5, 6, 7})
}
func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rcvd []int) {
@ -1652,12 +1653,12 @@ func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc
rublesHash, err := util.Uint160DecodeStringLE(testContractHash)
require.NoError(t, err)
blockSetRecord, err := e.chain.GetBlock(e.chain.GetHeaderHash(14)) // add type A record to `neo.com` domain via NNS
blockSetRecord, err := e.chain.GetBlock(e.chain.GetHeaderHash(15)) // add type A record to `neo.com` domain via NNS
require.NoError(t, err)
require.Equal(t, 1, len(blockSetRecord.Transactions))
txSetRecord := blockSetRecord.Transactions[0]
blockRegisterDomain, err := e.chain.GetBlock(e.chain.GetHeaderHash(13)) // register `neo.com` domain via NNS
blockRegisterDomain, err := e.chain.GetBlock(e.chain.GetHeaderHash(14)) // register `neo.com` domain via NNS
require.NoError(t, err)
require.Equal(t, 1, len(blockRegisterDomain.Transactions))
txRegisterDomain := blockRegisterDomain.Transactions[0]
@ -1665,6 +1666,11 @@ func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc
blockGASBounty2, err := e.chain.GetBlock(e.chain.GetHeaderHash(12)) // size of committee = 6
require.NoError(t, err)
blockDeploy4, err := e.chain.GetBlock(e.chain.GetHeaderHash(11)) // deploy ns.go (non-native neo name service contract)
require.NoError(t, err)
require.Equal(t, 1, len(blockDeploy4.Transactions))
txDeploy4 := blockDeploy4.Transactions[0]
blockDeploy3, err := e.chain.GetBlock(e.chain.GetHeaderHash(10)) // deploy verification_with_args_contract.go
require.NoError(t, err)
require.Equal(t, 1, len(blockDeploy3.Transactions))
@ -1728,7 +1734,7 @@ func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc
Asset: e.chain.UtilityTokenHash(),
Address: "", // burn
Amount: big.NewInt(txSetRecord.SystemFee + txSetRecord.NetworkFee).String(),
Index: 14,
Index: 15,
TxHash: blockSetRecord.Hash(),
},
{
@ -1736,9 +1742,17 @@ func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc
Asset: e.chain.UtilityTokenHash(),
Address: "", // burn
Amount: big.NewInt(txRegisterDomain.SystemFee + txRegisterDomain.NetworkFee).String(),
Index: 13,
Index: 14,
TxHash: blockRegisterDomain.Hash(),
},
{
Timestamp: blockDeploy4.Timestamp,
Asset: e.chain.UtilityTokenHash(),
Address: "", // burn
Amount: big.NewInt(txDeploy4.SystemFee + txDeploy4.NetworkFee).String(),
Index: 11,
TxHash: blockDeploy4.Hash(),
},
{
Timestamp: blockDeploy3.Timestamp,
Asset: e.chain.UtilityTokenHash(),

Binary file not shown.