native: add NameService
This commit is contained in:
parent
ec6317d643
commit
e4ff8326b5
12 changed files with 1921 additions and 30 deletions
|
@ -93,8 +93,8 @@ func newBlock(cfg config.ProtocolConfiguration, index uint32, prev util.Uint256,
|
|||
return newBlockWithState(cfg, index, prev, nil, txs...)
|
||||
}
|
||||
|
||||
func newBlockWithState(cfg config.ProtocolConfiguration, index uint32, prev util.Uint256,
|
||||
prevState *util.Uint256, txs ...*transaction.Transaction) *block.Block {
|
||||
func newBlockCustom(cfg config.ProtocolConfiguration, f func(b *block.Block),
|
||||
txs ...*transaction.Transaction) *block.Block {
|
||||
validators, _ := validatorsFromConfig(cfg)
|
||||
valScript, _ := smartcontract.CreateDefaultMultiSigRedeemScript(validators)
|
||||
witness := transaction.Witness{
|
||||
|
@ -103,10 +103,6 @@ func newBlockWithState(cfg config.ProtocolConfiguration, index uint32, prev util
|
|||
b := &block.Block{
|
||||
Base: block.Base{
|
||||
Network: testchain.Network(),
|
||||
Version: 0,
|
||||
PrevHash: prev,
|
||||
Timestamp: uint64(time.Now().UTC().Unix())*1000 + uint64(index),
|
||||
Index: index,
|
||||
NextConsensus: witness.ScriptHash(),
|
||||
Script: witness,
|
||||
},
|
||||
|
@ -116,15 +112,27 @@ func newBlockWithState(cfg config.ProtocolConfiguration, index uint32, prev util
|
|||
},
|
||||
Transactions: txs,
|
||||
}
|
||||
if prevState != nil {
|
||||
b.StateRootEnabled = true
|
||||
b.PrevStateRoot = *prevState
|
||||
}
|
||||
f(b)
|
||||
|
||||
b.RebuildMerkleRoot()
|
||||
b.Script.InvocationScript = testchain.Sign(b.GetSignedPart())
|
||||
return b
|
||||
}
|
||||
|
||||
func newBlockWithState(cfg config.ProtocolConfiguration, index uint32, prev util.Uint256,
|
||||
prevState *util.Uint256, txs ...*transaction.Transaction) *block.Block {
|
||||
return newBlockCustom(cfg, func(b *block.Block) {
|
||||
b.PrevHash = prev
|
||||
b.Timestamp = uint64(time.Now().UTC().Unix())*1000 + uint64(index)
|
||||
b.Index = index
|
||||
|
||||
if prevState != nil {
|
||||
b.StateRootEnabled = true
|
||||
b.PrevStateRoot = *prevState
|
||||
}
|
||||
}, txs...)
|
||||
}
|
||||
|
||||
func (bc *Blockchain) genBlocks(n int) ([]*block.Block, error) {
|
||||
blocks := make([]*block.Block, n)
|
||||
lastHash := bc.topBlock.Load().(*block.Block).Hash()
|
||||
|
|
|
@ -15,14 +15,15 @@ const reservedContractID = -100
|
|||
|
||||
// Contracts is a set of registered native contracts.
|
||||
type Contracts struct {
|
||||
Management *Management
|
||||
NEO *NEO
|
||||
GAS *GAS
|
||||
Policy *Policy
|
||||
Oracle *Oracle
|
||||
Designate *Designate
|
||||
Notary *Notary
|
||||
Contracts []interop.Contract
|
||||
Management *Management
|
||||
NEO *NEO
|
||||
GAS *GAS
|
||||
Policy *Policy
|
||||
Oracle *Oracle
|
||||
Designate *Designate
|
||||
NameService *NameService
|
||||
Notary *Notary
|
||||
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.
|
||||
|
@ -87,6 +88,11 @@ func NewContracts(p2pSigExtensionsEnabled bool) *Contracts {
|
|||
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
|
||||
|
|
749
pkg/core/native/name_service.go
Normal file
749
pkg/core/native/name_service.go
Normal file
|
@ -0,0 +1,749 @@
|
|||
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/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
|
||||
}
|
||||
|
||||
// RecordType represents name record type.
|
||||
type RecordType byte
|
||||
|
||||
// Pre-defined record types.
|
||||
const (
|
||||
RecordTypeA RecordType = 1
|
||||
RecordTypeCNAME RecordType = 5
|
||||
RecordTypeTXT RecordType = 16
|
||||
RecordTypeAAAA RecordType = 28
|
||||
)
|
||||
|
||||
const (
|
||||
nameServiceID = -7
|
||||
|
||||
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("^(2(5[0-5]|[0-4]\\d))|1?\\d{1,2}(\\.((2(5[0-5]|[0-4]\\d))|1?\\d{1,2})){3}$")
|
||||
ipv6Regex = regexp.MustCompile("^([a-f0-9A-F]{1,4}:){7}[a-f0-9A-F]{1,4}$")
|
||||
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}
|
||||
|
||||
desc := newDescriptor("addRoot", smartcontract.VoidType,
|
||||
manifest.NewParameter("root", smartcontract.StringType))
|
||||
md := newMethodAndPrice(n.addRoot, 3000000, callflag.WriteStates)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("setPrice", smartcontract.VoidType,
|
||||
manifest.NewParameter("price", smartcontract.IntegerType))
|
||||
md = newMethodAndPrice(n.setPrice, 3000000, callflag.WriteStates)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("getPrice", smartcontract.IntegerType)
|
||||
md = newMethodAndPrice(n.getPrice, 1000000, callflag.ReadStates)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("isAvailable", smartcontract.BoolType,
|
||||
manifest.NewParameter("name", smartcontract.StringType))
|
||||
md = newMethodAndPrice(n.isAvailable, 1000000, 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, 1000000, callflag.WriteStates)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("renew", smartcontract.IntegerType,
|
||||
manifest.NewParameter("name", smartcontract.StringType))
|
||||
md = newMethodAndPrice(n.renew, 0, callflag.WriteStates)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("setAdmin", smartcontract.VoidType,
|
||||
manifest.NewParameter("name", smartcontract.StringType),
|
||||
manifest.NewParameter("admin", smartcontract.Hash160Type))
|
||||
md = newMethodAndPrice(n.setAdmin, 3000000, callflag.WriteStates)
|
||||
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, 30000000, callflag.WriteStates)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("getRecord", smartcontract.StringType,
|
||||
manifest.NewParameter("name", smartcontract.StringType),
|
||||
manifest.NewParameter("type", smartcontract.IntegerType))
|
||||
md = newMethodAndPrice(n.getRecord, 1000000, 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, 1000000, callflag.WriteStates)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("resolve", smartcontract.StringType,
|
||||
manifest.NewParameter("name", smartcontract.StringType),
|
||||
manifest.NewParameter("type", smartcontract.IntegerType))
|
||||
md = newMethodAndPrice(n.resolve, 3000000, 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 {
|
||||
si := &state.StorageItem{Value: bigint.ToBytes(big.NewInt(DefaultDomainPrice))}
|
||||
if err := ic.DAO.PutStorageItem(n.ContractID, []byte{prefixDomainPrice}, si); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
roots := stringList{}
|
||||
return putSerializableToDAO(n.ContractID, 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.ContractID, []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.ContractID, key); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keysToRemove = keysToRemove[:0]
|
||||
key[0] = prefixRecord
|
||||
key = append(key[:1], keys[i][4:]...)
|
||||
ic.DAO.Seek(n.ContractID, 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.ContractID, 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.ContractID, 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)
|
||||
si := &state.StorageItem{Value: bigint.ToBytes(price)}
|
||||
err := ic.DAO.PutStorageItem(n.ContractID, []byte{prefixDomainPrice}, si)
|
||||
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.ContractID, []byte{prefixDomainPrice})
|
||||
return bigint.FromBytes(si.Value)
|
||||
}
|
||||
|
||||
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.ContractID, 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.ContractID, 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.ContractID, 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.ContractID,
|
||||
makeExpirationKey(token.Expiration, token.ID()),
|
||||
&state.StorageItem{Value: []byte{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.ContractID, ic.DAO, key, token)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
keyExpiration := makeExpirationKey(token.Expiration, token.ID())
|
||||
if err := ic.DAO.DeleteStorageItem(n.ContractID, keyExpiration); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
token.Expiration += secondsInYear
|
||||
err = putSerializableToDAO(n.ContractID, ic.DAO, key, token)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint32(key[1:], token.Expiration)
|
||||
si := &state.StorageItem{Value: []byte{0}}
|
||||
err = ic.DAO.PutStorageItem(n.ContractID, key, si)
|
||||
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.ContractID, 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.ContractID, 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])
|
||||
n.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{Value: []byte(data)}
|
||||
if err := ic.DAO.PutStorageItem(n.ContractID, key, si); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return stackitem.Null{}
|
||||
}
|
||||
|
||||
func (n *NameService) checkName(rt RecordType, name string) {
|
||||
var valid bool
|
||||
switch rt {
|
||||
case RecordTypeA:
|
||||
// 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 RecordTypeCNAME:
|
||||
valid = matchName(name)
|
||||
case RecordTypeTXT:
|
||||
valid = utf8.RuneCountInString(name) <= 255
|
||||
case RecordTypeAAAA:
|
||||
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.ContractID, key)
|
||||
if si == nil {
|
||||
return stackitem.Null{}
|
||||
}
|
||||
return stackitem.NewByteArray(si.Value)
|
||||
}
|
||||
|
||||
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.ContractID, 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.ContractID, 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 RecordType, 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[RecordTypeCNAME]
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
return n.resolveInternal(ic, data, t, redirect-1)
|
||||
}
|
||||
|
||||
func (n *NameService) getRecordsInternal(d dao.DAO, name string) map[RecordType]string {
|
||||
domain := toDomain(name)
|
||||
key := makeRecordKey(domain, name, 0)
|
||||
key = key[:len(key)-1]
|
||||
res := make(map[RecordType]string)
|
||||
d.Seek(n.ContractID, key, func(k, v []byte) {
|
||||
rt := RecordType(k[len(k)-1])
|
||||
var si state.StorageItem
|
||||
r := io.NewBinReaderFromBuf(v)
|
||||
si.DecodeBinary(r)
|
||||
if r.Err != nil {
|
||||
panic(r.Err)
|
||||
}
|
||||
res[rt] = string(si.Value)
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
func makeRecordKey(domain, name string, rt RecordType) []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 {
|
||||
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) < 5 {
|
||||
return errors.New("invalid stack item")
|
||||
}
|
||||
bi, err := elems[3].TryInteger()
|
||||
if err != nil || !bi.IsUint64() {
|
||||
return errors.New("invalid stack item")
|
||||
}
|
||||
|
||||
_, isNull := elems[4].(stackitem.Null)
|
||||
if !isNull {
|
||||
bs, err := elems[4].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) RecordType {
|
||||
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 := RecordType(val); rt {
|
||||
case RecordTypeA, RecordTypeCNAME, RecordTypeTXT, RecordTypeAAAA:
|
||||
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 {
|
||||
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) remove(s string) bool {
|
||||
index, has := sl.index(s)
|
||||
if !has {
|
||||
return false
|
||||
}
|
||||
|
||||
copy((*sl)[index:], (*sl)[index+1:])
|
||||
*sl = (*sl)[:len(*sl)-1]
|
||||
return true
|
||||
}
|
||||
|
||||
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
|
||||
}
|
32
pkg/core/native/name_service_test.go
Normal file
32
pkg/core/native/name_service_test.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package native
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"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)
|
||||
}
|
|
@ -9,4 +9,5 @@ const (
|
|||
Oracle = "OracleContract"
|
||||
Designation = "RoleManagement"
|
||||
Notary = "Notary"
|
||||
NameService = "NameService"
|
||||
)
|
||||
|
|
357
pkg/core/native/nonfungible.go
Normal file
357
pkg/core/native/nonfungible.go
Normal file
|
@ -0,0 +1,357 @@
|
|||
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/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"
|
||||
"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}
|
||||
|
||||
intOne = big.NewInt(1)
|
||||
)
|
||||
|
||||
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)
|
||||
},
|
||||
}
|
||||
|
||||
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, 1000000, callflag.ReadStates)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("ownerOf", smartcontract.Hash160Type,
|
||||
manifest.NewParameter("tokenId", smartcontract.ByteArrayType))
|
||||
md = newMethodAndPrice(n.OwnerOf, 1000000, callflag.ReadStates)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("balanceOf", smartcontract.IntegerType,
|
||||
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
||||
md = newMethodAndPrice(n.BalanceOf, 1000000, callflag.ReadStates)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("properties", smartcontract.MapType,
|
||||
manifest.NewParameter("tokenId", smartcontract.ByteArrayType))
|
||||
md = newMethodAndPrice(n.Properties, 1000000, callflag.ReadStates)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("tokens", smartcontract.InteropInterfaceType)
|
||||
md = newMethodAndPrice(n.tokens, 1000000, callflag.ReadStates)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("tokensOf", smartcontract.InteropInterfaceType,
|
||||
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
||||
md = newMethodAndPrice(n.tokensOf, 1000000, callflag.ReadStates)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("transfer", smartcontract.BoolType,
|
||||
manifest.NewParameter("to", smartcontract.Hash160Type),
|
||||
manifest.NewParameter("tokenId", smartcontract.ByteArrayType))
|
||||
md = newMethodAndPrice(n.transfer, 9000000, callflag.WriteStates|callflag.AllowNotify)
|
||||
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
|
||||
}
|
||||
|
||||
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.ContractID, nftTotalSupplyKey)
|
||||
if si == nil {
|
||||
return big.NewInt(0)
|
||||
}
|
||||
return bigint.FromBytes(si.Value)
|
||||
}
|
||||
|
||||
func (n *nonfungible) setTotalSupply(d dao.DAO, ts *big.Int) {
|
||||
si := &state.StorageItem{Value: bigint.ToBytes(ts)}
|
||||
err := d.PutStorageItem(n.ContractID, nftTotalSupplyKey, si)
|
||||
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.ContractID, 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.ContractID, 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.ContractID, key)
|
||||
} else {
|
||||
err = putSerializableToDAO(n.ContractID, 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.ContractID, 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.Value))
|
||||
}
|
||||
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, 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, _ := vm.NewIterator(stackitem.NewArray(arr))
|
||||
return iter
|
||||
}
|
||||
|
||||
func (n *nonfungible) mint(ic *interop.Context, s nftTokenState) {
|
||||
key := n.getTokenKey(s.ID())
|
||||
if ic.DAO.GetStorageItem(n.ContractID, key) != nil {
|
||||
panic("token is already minted")
|
||||
}
|
||||
if err := putSerializableToDAO(n.ContractID, 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())
|
||||
}
|
||||
|
||||
func (n *nonfungible) postTransfer(ic *interop.Context, from, to *util.Uint160, tokenID []byte) {
|
||||
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)
|
||||
}
|
||||
|
||||
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.ContractID, ic.DAO, key, token)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := ic.DAO.DeleteStorageItem(n.ContractID, 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)
|
||||
}
|
||||
|
||||
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.ContractID, 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)
|
||||
return stackitem.NewBool(true)
|
||||
}
|
||||
|
||||
func makeNFTAccountKey(owner util.Uint160) []byte {
|
||||
return append([]byte{prefixNFTAccount}, owner.BytesBE()...)
|
||||
}
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/nspcc-dev/neo-go/pkg/core/storage"
|
||||
"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"
|
||||
)
|
||||
|
||||
func getSerializableFromDAO(id int32, d dao.DAO, key []byte, item io.Serializable) error {
|
||||
|
@ -70,3 +71,11 @@ func makeUint160Key(prefix byte, h util.Uint160) []byte {
|
|||
copy(k[1:], h.BytesBE())
|
||||
return k
|
||||
}
|
||||
|
||||
func toString(item stackitem.Item) string {
|
||||
s, err := stackitem.ToString(item)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
|
406
pkg/core/native_name_service_test.go
Normal file
406
pkg/core/native_name_service_test.go
Normal file
|
@ -0,0 +1,406 @@
|
|||
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/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)
|
||||
defer bc.Close()
|
||||
|
||||
testGetSet(t, bc, bc.contracts.NameService.Hash, "Price",
|
||||
native.DefaultDomainPrice, 1, 10000_00000000)
|
||||
}
|
||||
|
||||
func TestNonfungible(t *testing.T) {
|
||||
bc := newTestChain(t)
|
||||
defer bc.Close()
|
||||
|
||||
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)
|
||||
defer bc.Close()
|
||||
|
||||
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)
|
||||
defer bc.Close()
|
||||
|
||||
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(native.RecordTypeTXT), "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(native.RecordTypeTXT))
|
||||
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)
|
||||
defer bc.Close()
|
||||
|
||||
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())
|
||||
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, true, "register",
|
||||
false, "neo.com", testchain.CommitteeScriptHash())
|
||||
testNameServiceInvoke(t, bc, "isAvailable", false, "neo.com")
|
||||
|
||||
topBlock := bc.topBlock.Load().(*block.Block)
|
||||
expectedExpiration := topBlock.Timestamp/1000 + secondsInYear
|
||||
|
||||
props := stackitem.NewMap()
|
||||
props.Add(stackitem.Make("name"), stackitem.Make("neo.com"))
|
||||
props.Add(stackitem.Make("description"), stackitem.Make(""))
|
||||
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)
|
||||
defer bc.Close()
|
||||
|
||||
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(native.RecordTypeTXT), "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(native.RecordTypeA), "not.an.ip.address")
|
||||
})
|
||||
t.Run("invalid witness", func(t *testing.T) {
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "setRecord", nil,
|
||||
"neo.com", int64(native.RecordTypeA), "1.2.3.4")
|
||||
})
|
||||
testNameServiceInvoke(t, bc, "getRecord", stackitem.Null{}, "neo.com", int64(native.RecordTypeA))
|
||||
testNameServiceInvoke(t, bc, "setRecord", stackitem.Null{}, "neo.com", int64(native.RecordTypeA), "1.2.3.4")
|
||||
testNameServiceInvoke(t, bc, "getRecord", "1.2.3.4", "neo.com", int64(native.RecordTypeA))
|
||||
testNameServiceInvoke(t, bc, "setRecord", stackitem.Null{}, "neo.com", int64(native.RecordTypeA), "1.2.3.4")
|
||||
testNameServiceInvoke(t, bc, "getRecord", "1.2.3.4", "neo.com", int64(native.RecordTypeA))
|
||||
testNameServiceInvoke(t, bc, "setRecord", stackitem.Null{}, "neo.com", int64(native.RecordTypeAAAA), "2001:0000:1F1F:0000:0000:0100:11A0:ADDF")
|
||||
testNameServiceInvoke(t, bc, "setRecord", stackitem.Null{}, "neo.com", int64(native.RecordTypeCNAME), "nspcc.ru")
|
||||
testNameServiceInvoke(t, bc, "setRecord", stackitem.Null{}, "neo.com", int64(native.RecordTypeTXT), "sometext")
|
||||
|
||||
// Delete record.
|
||||
t.Run("invalid witness", func(t *testing.T) {
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "setRecord", nil,
|
||||
"neo.com", int64(native.RecordTypeCNAME))
|
||||
})
|
||||
testNameServiceInvoke(t, bc, "getRecord", "nspcc.ru", "neo.com", int64(native.RecordTypeCNAME))
|
||||
testNameServiceInvoke(t, bc, "deleteRecord", stackitem.Null{}, "neo.com", int64(native.RecordTypeCNAME))
|
||||
testNameServiceInvoke(t, bc, "getRecord", stackitem.Null{}, "neo.com", int64(native.RecordTypeCNAME))
|
||||
testNameServiceInvoke(t, bc, "getRecord", "1.2.3.4", "neo.com", int64(native.RecordTypeA))
|
||||
}
|
||||
|
||||
func TestSetAdmin(t *testing.T) {
|
||||
bc := newTestChain(t)
|
||||
defer bc.Close()
|
||||
|
||||
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(native.RecordTypeTXT), "sometext")
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, guest, "deleteRecord", nil,
|
||||
"neo.com", int64(native.RecordTypeTXT))
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, admin, "deleteRecord", stackitem.Null{},
|
||||
"neo.com", int64(native.RecordTypeTXT))
|
||||
})
|
||||
|
||||
t.Run("set admin to null", func(t *testing.T) {
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, admin, "setRecord", stackitem.Null{},
|
||||
"neo.com", int64(native.RecordTypeTXT), "sometext")
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, owner, "setAdmin", stackitem.Null{},
|
||||
"neo.com", nil)
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, admin, "deleteRecord", nil,
|
||||
"neo.com", int64(native.RecordTypeTXT))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTransfer(t *testing.T) {
|
||||
bc := newTestChain(t)
|
||||
defer bc.Close()
|
||||
|
||||
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(native.RecordTypeA), "1.2.3.4")
|
||||
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, from, "transfer",
|
||||
nil, to.Contract.ScriptHash().BytesBE(), []byte("not.exists"))
|
||||
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, true, "transfer",
|
||||
false, to.Contract.ScriptHash().BytesBE(), []byte("neo.com"))
|
||||
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, from, "transfer",
|
||||
true, to.Contract.ScriptHash().BytesBE(), []byte("neo.com"))
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, from, "totalSupply", 1)
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, from, "ownerOf",
|
||||
to.Contract.ScriptHash().BytesBE(), []byte("neo.com"))
|
||||
}
|
||||
|
||||
func TestTokensOf(t *testing.T) {
|
||||
bc := newTestChain(t)
|
||||
defer bc.Close()
|
||||
|
||||
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(bc.GetConfig().Magic, 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)
|
||||
defer bc.Close()
|
||||
|
||||
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(native.RecordTypeA), "1.2.3.4")
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{},
|
||||
"neo.com", int64(native.RecordTypeCNAME), "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(native.RecordTypeTXT), "sometxt")
|
||||
|
||||
testNameServiceInvoke(t, bc, "resolve", "1.2.3.4",
|
||||
"neo.com", int64(native.RecordTypeA))
|
||||
testNameServiceInvoke(t, bc, "resolve", "alias.com",
|
||||
"neo.com", int64(native.RecordTypeCNAME))
|
||||
testNameServiceInvoke(t, bc, "resolve", "sometxt",
|
||||
"neo.com", int64(native.RecordTypeTXT))
|
||||
testNameServiceInvoke(t, bc, "resolve", stackitem.Null{},
|
||||
"neo.com", int64(native.RecordTypeAAAA))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
|
@ -17,7 +17,7 @@ import (
|
|||
|
||||
func transferFundsToCommittee(t *testing.T, chain *Blockchain) {
|
||||
transferTokenFromMultisigAccount(t, chain, testchain.CommitteeScriptHash(),
|
||||
chain.contracts.GAS.Hash, 100_00000000)
|
||||
chain.contracts.GAS.Hash, 1000_00000000)
|
||||
}
|
||||
|
||||
func testGetSet(t *testing.T, chain *Blockchain, hash util.Uint160, name string, defaultValue, minValue, maxValue int64) {
|
||||
|
|
185
pkg/core/state/nonfungible.go
Normal file
185
pkg/core/state/nonfungible.go
Normal file
|
@ -0,0 +1,185 @@
|
|||
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
|
||||
Description 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)),
|
||||
stackitem.NewByteArray([]byte(s.Description)),
|
||||
})
|
||||
}
|
||||
|
||||
// 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) < 3 {
|
||||
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
|
||||
}
|
||||
desc, err := stackitem.ToString(arr[2])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.Owner = owner
|
||||
s.Name = name
|
||||
s.Description = desc
|
||||
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)),
|
||||
},
|
||||
{
|
||||
Key: stackitem.NewByteArray([]byte("description")),
|
||||
Value: stackitem.NewByteArray([]byte(s.Description)),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
131
pkg/core/state/nonfungible_test.go
Normal file
131
pkg/core/state/nonfungible_test.go
Normal file
|
@ -0,0 +1,131 @@
|
|||
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",
|
||||
Description: "random description",
|
||||
}
|
||||
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")},
|
||||
{"invalid description",
|
||||
newStruct(random.Uint160().BytesBE(), "name", []byte{0x80})},
|
||||
}
|
||||
|
||||
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",
|
||||
Description: "random description",
|
||||
}
|
||||
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())
|
||||
|
||||
i = m.Index(stackitem.Make("description"))
|
||||
require.True(t, i < len(elems))
|
||||
require.Equal(t, []byte("random description"), 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())
|
||||
}
|
|
@ -122,27 +122,34 @@ func IteratorValue(v *VM) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// IteratorCreate handles syscall System.Iterator.Create.
|
||||
func IteratorCreate(v *VM) error {
|
||||
data := v.Estack().Pop()
|
||||
var item stackitem.Item
|
||||
switch t := data.value.(type) {
|
||||
// NewIterator creates new iterator from the provided stack item.
|
||||
func NewIterator(item stackitem.Item) (stackitem.Item, error) {
|
||||
switch t := item.(type) {
|
||||
case *stackitem.Array, *stackitem.Struct:
|
||||
item = stackitem.NewInterop(&arrayWrapper{
|
||||
return stackitem.NewInterop(&arrayWrapper{
|
||||
index: -1,
|
||||
value: t.Value().([]stackitem.Item),
|
||||
})
|
||||
}), nil
|
||||
case *stackitem.Map:
|
||||
item = NewMapIterator(t)
|
||||
return NewMapIterator(t), nil
|
||||
default:
|
||||
data, err := t.TryBytes()
|
||||
if err != nil {
|
||||
return fmt.Errorf("non-iterable type %s", t.Type())
|
||||
return nil, fmt.Errorf("non-iterable type %s", t.Type())
|
||||
}
|
||||
item = stackitem.NewInterop(&byteArrayWrapper{
|
||||
return stackitem.NewInterop(&byteArrayWrapper{
|
||||
index: -1,
|
||||
value: data,
|
||||
})
|
||||
}), nil
|
||||
}
|
||||
}
|
||||
|
||||
// IteratorCreate handles syscall System.Iterator.Create.
|
||||
func IteratorCreate(v *VM) error {
|
||||
data := v.Estack().Pop().Item()
|
||||
item, err := NewIterator(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v.Estack().Push(&Element{value: item})
|
||||
|
|
Loading…
Reference in a new issue