feat: add nft contract

This commit is contained in:
Mihail Pestrikov 2025-01-20 16:35:42 +00:00
parent 433ece3487
commit 6f8dc65c04

319
contracts/nft.go Normal file
View 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)
}