feat: add nft contract
This commit is contained in:
parent
433ece3487
commit
6f8dc65c04
1 changed files with 319 additions and 0 deletions
319
contracts/nft.go
Normal file
319
contracts/nft.go
Normal file
|
@ -0,0 +1,319 @@
|
|||
package nft
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/iterator"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/native/crypto"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/native/gas"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/native/std"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/util"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
balancePrefix = "b"
|
||||
accountPrefix = "a"
|
||||
tokenPrefix = "t"
|
||||
totalSupplyKey = "s"
|
||||
)
|
||||
|
||||
const (
|
||||
QuestionTypeText = "text"
|
||||
QuestionTypeData = "data"
|
||||
)
|
||||
|
||||
type NFTItem struct {
|
||||
ID []byte
|
||||
Owner interop.Hash160
|
||||
Question string
|
||||
PrevOwners int
|
||||
QuestionType string
|
||||
Data string // todo: на подумать поменять название на что то другое
|
||||
}
|
||||
|
||||
func _deploy(data interface{}, isUpdate bool) {
|
||||
if isUpdate {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Symbol Returns the token symbol
|
||||
func Symbol() string {
|
||||
return "QUESTIONS"
|
||||
}
|
||||
|
||||
// Decimals Returns the integer 0 as token is non-divisible
|
||||
func Decimals() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// TotalSupply NFT total supply. Total supply= Amount of minted tokens - Amount of burned tokens
|
||||
func TotalSupply() int {
|
||||
return storage.Get(storage.GetReadOnlyContext(), totalSupplyKey).(int)
|
||||
}
|
||||
|
||||
// BalanceOf The total amount of NFT owned by the user
|
||||
func BalanceOf(holder interop.Hash160) int {
|
||||
if len(holder) != 20 {
|
||||
panic("bad owner address")
|
||||
}
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
|
||||
return getBalanceOf(ctx, makeBalanceKey(holder))
|
||||
}
|
||||
|
||||
// OwnerOf Returns the owner of the specified NFT.
|
||||
func OwnerOf(token []byte) interop.Hash160 {
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
return getNFT(ctx, token).Owner
|
||||
}
|
||||
|
||||
// Properties Returns the properties of the given NFT.
|
||||
func Properties(token []byte) map[string]string {
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
nft := getNFT(ctx, token)
|
||||
|
||||
result := map[string]string{
|
||||
"id": string(nft.ID),
|
||||
"owner": string(nft.Owner),
|
||||
"name": nft.Question,
|
||||
"prevOwners": std.Itoa10(nft.PrevOwners),
|
||||
"questionType": nft.QuestionType,
|
||||
"data": string(nft.Data),
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Tokens Returns all of the tokens minted by the contract.
|
||||
func Tokens() iterator.Iterator {
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
key := []byte(tokenPrefix)
|
||||
iter := storage.Find(ctx, key, storage.RemovePrefix|storage.KeysOnly)
|
||||
return iter
|
||||
}
|
||||
|
||||
func TokensList() []string {
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
key := []byte(tokenPrefix)
|
||||
iter := storage.Find(ctx, key, storage.RemovePrefix|storage.KeysOnly)
|
||||
var keys []string
|
||||
for iterator.Next(iter) {
|
||||
k := iterator.Value(iter)
|
||||
keys = append(keys, k.(string))
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// TokensOf Returns all of the token ids owned by the specified address
|
||||
func TokensOf(holder interop.Hash160) iterator.Iterator {
|
||||
if len(holder) != 20 {
|
||||
panic(fmt.Sprintf("owner adress=%s is not valid", holder))
|
||||
}
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
key := makeAccountKey(holder)
|
||||
iter := storage.Find(ctx, key, storage.ValuesOnly)
|
||||
return iter
|
||||
}
|
||||
|
||||
func TokensOfList(holder interop.Hash160) [][]byte {
|
||||
if len(holder) != 20 {
|
||||
panic(fmt.Sprintf("owner adress=%s is not valid", holder))
|
||||
}
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
key := makeAccountKey(holder)
|
||||
var res [][]byte
|
||||
iter := storage.Find(ctx, key, storage.ValuesOnly)
|
||||
for iterator.Next(iter) {
|
||||
res = append(res, iterator.Value(iter).([]byte))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Transfer token from its owner to another user
|
||||
func Transfer(to interop.Hash160, token []byte) bool {
|
||||
if len(to) != 20 {
|
||||
panic(fmt.Sprintf("to adress=%s is not valid", to))
|
||||
}
|
||||
ctx := storage.GetContext()
|
||||
nft := getNFT(ctx, token)
|
||||
from := nft.Owner
|
||||
|
||||
if !runtime.CheckWitness(from) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !from.Equals(to) {
|
||||
nft.Owner = to
|
||||
nft.PrevOwners += 1
|
||||
setNFT(ctx, token, nft)
|
||||
|
||||
addToBalance(ctx, from, -1)
|
||||
removeToken(ctx, from, token)
|
||||
addToBalance(ctx, to, 1)
|
||||
addToken(ctx, to, token)
|
||||
}
|
||||
|
||||
runtime.Notify("Transfer", from, to, 1, token)
|
||||
return true
|
||||
}
|
||||
|
||||
// Burn token
|
||||
func Burn(token []byte) bool {
|
||||
ctx := storage.GetContext()
|
||||
nft := getNFT(ctx, token)
|
||||
|
||||
storage.Delete(ctx, makeTokenKey(nft.ID))
|
||||
|
||||
total := storage.Get(ctx, totalSupplyKey).(int) - 1
|
||||
storage.Put(ctx, totalSupplyKey, total)
|
||||
|
||||
runtime.Notify("Burn", token)
|
||||
return true
|
||||
}
|
||||
|
||||
func getNFT(ctx storage.Context, token []byte) NFTItem {
|
||||
key := makeTokenKey(token)
|
||||
tokenData := storage.Get(ctx, key)
|
||||
if tokenData == nil {
|
||||
panic(fmt.Sprintf("token with key=%s not found", key))
|
||||
}
|
||||
|
||||
var deserializedNFT = std.Deserialize(tokenData.([]byte)).(NFTItem)
|
||||
return deserializedNFT
|
||||
}
|
||||
|
||||
func setNFT(ctx storage.Context, token []byte, item NFTItem) {
|
||||
key := makeTokenKey(token)
|
||||
val := std.Serialize(item)
|
||||
storage.Put(ctx, key, val)
|
||||
}
|
||||
|
||||
func nftExists(ctx storage.Context, token []byte) bool {
|
||||
key := makeTokenKey(token)
|
||||
return storage.Get(ctx, key) != nil
|
||||
}
|
||||
|
||||
func makeAccountKey(holder interop.Hash160) []byte {
|
||||
res := []byte(accountPrefix)
|
||||
return append(res, holder...)
|
||||
}
|
||||
|
||||
func makeBalanceKey(holder interop.Hash160) []byte {
|
||||
res := []byte(balancePrefix)
|
||||
return append(res, holder...)
|
||||
}
|
||||
|
||||
func makeTokenKey(tokenID []byte) []byte {
|
||||
res := []byte(tokenPrefix)
|
||||
return append(res, tokenID...)
|
||||
}
|
||||
|
||||
func getBalanceOf(ctx storage.Context, balanceKey []byte) int {
|
||||
balance := storage.Get(ctx, balanceKey)
|
||||
if balance != nil {
|
||||
return balance.(int)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// addToBalance adds an amount of tokens to the account balance.
|
||||
func addToBalance(ctx storage.Context, holder interop.Hash160, amount int) {
|
||||
key := makeBalanceKey(holder)
|
||||
prev := getBalanceOf(ctx, key)
|
||||
prev += amount
|
||||
if prev > 0 {
|
||||
storage.Put(ctx, key, prev)
|
||||
} else {
|
||||
storage.Delete(ctx, key)
|
||||
}
|
||||
}
|
||||
|
||||
// addToken adds token to the account.
|
||||
func addToken(ctx storage.Context, holder interop.Hash160, token []byte) {
|
||||
key := makeAccountKey(holder)
|
||||
storage.Put(ctx, append(key, token...), token)
|
||||
}
|
||||
|
||||
// removeToken removes token from the account.
|
||||
func removeToken(ctx storage.Context, holder interop.Hash160, token []byte) {
|
||||
key := makeAccountKey(holder)
|
||||
storage.Delete(ctx, append(key, token...))
|
||||
}
|
||||
|
||||
// OnNEP17Payment Creates new NFT token after payment on the contract wallet
|
||||
func OnNEP17Payment(from interop.Hash160, amount int, data any) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
runtime.Log(r.(string))
|
||||
util.Abort()
|
||||
}
|
||||
}()
|
||||
|
||||
callingHash := runtime.GetCallingScriptHash()
|
||||
if !callingHash.Equals(gas.Hash) {
|
||||
panic("only GAS is accepted")
|
||||
}
|
||||
|
||||
dataString, ok := data.(string)
|
||||
if !ok {
|
||||
panic("invalid data format")
|
||||
}
|
||||
|
||||
parts := strings.Split(dataString, ";")
|
||||
|
||||
// Data format is supposed to be question;type;<data>, while <data> is optional
|
||||
// To set <data> type should be data
|
||||
if len(parts) < 2 || len(parts) > 3 {
|
||||
panic("invalid data format: should have 2 or 3 parts")
|
||||
}
|
||||
|
||||
question := parts[0]
|
||||
|
||||
questionType := parts[1]
|
||||
|
||||
if questionType != QuestionTypeData && questionType != QuestionTypeText {
|
||||
panic("invalid question type")
|
||||
}
|
||||
|
||||
// 10 gas tokens in nano
|
||||
price := 10_0000_0000
|
||||
|
||||
var questionData string
|
||||
if questionType == QuestionTypeData {
|
||||
if len(parts) != 3 {
|
||||
panic("invalid data format for type 'data': expected 3 parts")
|
||||
}
|
||||
questionData = parts[2]
|
||||
price += 5_0000_0000
|
||||
}
|
||||
|
||||
if amount < price {
|
||||
panic("insufficient GAS for minting NFT")
|
||||
}
|
||||
|
||||
ctx := storage.GetContext()
|
||||
tokenID := crypto.Sha256([]byte(question))
|
||||
if nftExists(ctx, tokenID) {
|
||||
panic("token already exists")
|
||||
}
|
||||
|
||||
nft := NFTItem{
|
||||
ID: tokenID,
|
||||
Owner: from,
|
||||
Question: question,
|
||||
PrevOwners: 0,
|
||||
QuestionType: questionType,
|
||||
Data: questionData,
|
||||
}
|
||||
setNFT(ctx, tokenID, nft)
|
||||
addToBalance(ctx, from, 1)
|
||||
addToken(ctx, from, tokenID)
|
||||
|
||||
total := storage.Get(ctx, totalSupplyKey).(int) + 1
|
||||
storage.Put(ctx, totalSupplyKey, total)
|
||||
|
||||
runtime.Notify("Create", from, tokenID)
|
||||
}
|
Loading…
Add table
Reference in a new issue