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...)
|
return newBlockWithState(cfg, index, prev, nil, txs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newBlockWithState(cfg config.ProtocolConfiguration, index uint32, prev util.Uint256,
|
func newBlockCustom(cfg config.ProtocolConfiguration, f func(b *block.Block),
|
||||||
prevState *util.Uint256, txs ...*transaction.Transaction) *block.Block {
|
txs ...*transaction.Transaction) *block.Block {
|
||||||
validators, _ := validatorsFromConfig(cfg)
|
validators, _ := validatorsFromConfig(cfg)
|
||||||
valScript, _ := smartcontract.CreateDefaultMultiSigRedeemScript(validators)
|
valScript, _ := smartcontract.CreateDefaultMultiSigRedeemScript(validators)
|
||||||
witness := transaction.Witness{
|
witness := transaction.Witness{
|
||||||
|
@ -103,10 +103,6 @@ func newBlockWithState(cfg config.ProtocolConfiguration, index uint32, prev util
|
||||||
b := &block.Block{
|
b := &block.Block{
|
||||||
Base: block.Base{
|
Base: block.Base{
|
||||||
Network: testchain.Network(),
|
Network: testchain.Network(),
|
||||||
Version: 0,
|
|
||||||
PrevHash: prev,
|
|
||||||
Timestamp: uint64(time.Now().UTC().Unix())*1000 + uint64(index),
|
|
||||||
Index: index,
|
|
||||||
NextConsensus: witness.ScriptHash(),
|
NextConsensus: witness.ScriptHash(),
|
||||||
Script: witness,
|
Script: witness,
|
||||||
},
|
},
|
||||||
|
@ -116,15 +112,27 @@ func newBlockWithState(cfg config.ProtocolConfiguration, index uint32, prev util
|
||||||
},
|
},
|
||||||
Transactions: txs,
|
Transactions: txs,
|
||||||
}
|
}
|
||||||
if prevState != nil {
|
f(b)
|
||||||
b.StateRootEnabled = true
|
|
||||||
b.PrevStateRoot = *prevState
|
|
||||||
}
|
|
||||||
b.RebuildMerkleRoot()
|
b.RebuildMerkleRoot()
|
||||||
b.Script.InvocationScript = testchain.Sign(b.GetSignedPart())
|
b.Script.InvocationScript = testchain.Sign(b.GetSignedPart())
|
||||||
return b
|
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) {
|
func (bc *Blockchain) genBlocks(n int) ([]*block.Block, error) {
|
||||||
blocks := make([]*block.Block, n)
|
blocks := make([]*block.Block, n)
|
||||||
lastHash := bc.topBlock.Load().(*block.Block).Hash()
|
lastHash := bc.topBlock.Load().(*block.Block).Hash()
|
||||||
|
|
|
@ -15,14 +15,15 @@ const reservedContractID = -100
|
||||||
|
|
||||||
// Contracts is a set of registered native contracts.
|
// Contracts is a set of registered native contracts.
|
||||||
type Contracts struct {
|
type Contracts struct {
|
||||||
Management *Management
|
Management *Management
|
||||||
NEO *NEO
|
NEO *NEO
|
||||||
GAS *GAS
|
GAS *GAS
|
||||||
Policy *Policy
|
Policy *Policy
|
||||||
Oracle *Oracle
|
Oracle *Oracle
|
||||||
Designate *Designate
|
Designate *Designate
|
||||||
Notary *Notary
|
NameService *NameService
|
||||||
Contracts []interop.Contract
|
Notary *Notary
|
||||||
|
Contracts []interop.Contract
|
||||||
// persistScript is vm script which executes "onPersist" method of every native contract.
|
// persistScript is vm script which executes "onPersist" method of every native contract.
|
||||||
persistScript []byte
|
persistScript []byte
|
||||||
// postPersistScript is vm script which executes "postPersist" method of every native contract.
|
// 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.Oracle = oracle
|
||||||
cs.Contracts = append(cs.Contracts, oracle)
|
cs.Contracts = append(cs.Contracts, oracle)
|
||||||
|
|
||||||
|
ns := newNameService()
|
||||||
|
ns.NEO = neo
|
||||||
|
cs.NameService = ns
|
||||||
|
cs.Contracts = append(cs.Contracts, ns)
|
||||||
|
|
||||||
if p2pSigExtensionsEnabled {
|
if p2pSigExtensionsEnabled {
|
||||||
notary := newNotary()
|
notary := newNotary()
|
||||||
notary.GAS = gas
|
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"
|
Oracle = "OracleContract"
|
||||||
Designation = "RoleManagement"
|
Designation = "RoleManagement"
|
||||||
Notary = "Notary"
|
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/core/storage"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
"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 {
|
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())
|
copy(k[1:], h.BytesBE())
|
||||||
return k
|
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) {
|
func transferFundsToCommittee(t *testing.T, chain *Blockchain) {
|
||||||
transferTokenFromMultisigAccount(t, chain, testchain.CommitteeScriptHash(),
|
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) {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IteratorCreate handles syscall System.Iterator.Create.
|
// NewIterator creates new iterator from the provided stack item.
|
||||||
func IteratorCreate(v *VM) error {
|
func NewIterator(item stackitem.Item) (stackitem.Item, error) {
|
||||||
data := v.Estack().Pop()
|
switch t := item.(type) {
|
||||||
var item stackitem.Item
|
|
||||||
switch t := data.value.(type) {
|
|
||||||
case *stackitem.Array, *stackitem.Struct:
|
case *stackitem.Array, *stackitem.Struct:
|
||||||
item = stackitem.NewInterop(&arrayWrapper{
|
return stackitem.NewInterop(&arrayWrapper{
|
||||||
index: -1,
|
index: -1,
|
||||||
value: t.Value().([]stackitem.Item),
|
value: t.Value().([]stackitem.Item),
|
||||||
})
|
}), nil
|
||||||
case *stackitem.Map:
|
case *stackitem.Map:
|
||||||
item = NewMapIterator(t)
|
return NewMapIterator(t), nil
|
||||||
default:
|
default:
|
||||||
data, err := t.TryBytes()
|
data, err := t.TryBytes()
|
||||||
if err != nil {
|
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,
|
index: -1,
|
||||||
value: data,
|
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})
|
v.Estack().Push(&Element{value: item})
|
||||||
|
|
Loading…
Reference in a new issue