core: remove native nonfungible token

This commit is contained in:
Anna Shaleva 2021-05-17 20:30:43 +03:00
parent 99b37efc31
commit c9099fa555
4 changed files with 0 additions and 723 deletions

View file

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

View file

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

View file

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

View file

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