From 6f8dc65c04a4e82ae4d20b4afb3ef05c42941c75 Mon Sep 17 00:00:00 2001 From: MihailPestrikov Date: Mon, 20 Jan 2025 16:35:42 +0000 Subject: [PATCH] feat: add nft contract --- contracts/nft.go | 319 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 contracts/nft.go diff --git a/contracts/nft.go b/contracts/nft.go new file mode 100644 index 0000000..dfa7e08 --- /dev/null +++ b/contracts/nft.go @@ -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;, while is optional + // To set 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) +}