360 lines
10 KiB
Go
360 lines
10 KiB
Go
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}
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
// Initialize implements interop.Contract interface.
|
|
func (n nonfungible) Initialize(ic *interop.Context) error {
|
|
return setIntWithKey(n.ContractID, 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.ContractID, nftTotalSupplyKey)
|
|
if si == nil {
|
|
panic(errors.New("total supply is not initialized"))
|
|
}
|
|
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()...)
|
|
}
|