web3-n3/nft/nep11/contract.go

343 lines
8.6 KiB
Go
Raw Normal View History

package contract
import (
"github.com/nspcc-dev/neo-go/pkg/interop"
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
"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/ledger"
"github.com/nspcc-dev/neo-go/pkg/interop/native/management"
"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"
)
// Prefixes used for contract data storage.
const (
balancePrefix = "b"
accountPrefix = "a"
tokenPrefix = "t"
ownerKey = 'o'
totalSupplyKey = 's'
)
const (
minNameLen = 3
)
type NFTItem struct {
ID []byte
Owner interop.Hash160
Name string
PrevOwners int
Created int
Bought int
}
func _deploy(data interface{}, isUpdate bool) {
if isUpdate {
return
}
args := data.(struct {
Admin interop.Hash160
})
if args.Admin == nil {
panic("invalid admin")
}
if len(args.Admin) != 20 {
panic("invalid admin hash length")
}
ctx := storage.GetContext()
storage.Put(ctx, ownerKey, args.Admin)
name := ownerAddress(args.Admin)
nft := NFTItem{
ID: crypto.Sha256([]byte(name)),
Owner: args.Admin,
Name: name,
PrevOwners: 0,
Created: ledger.CurrentIndex(),
Bought: ledger.CurrentIndex(),
}
setNFT(ctx, nft.ID, nft)
addToBalance(ctx, nft.Owner, 1)
addToken(ctx, nft.Owner, nft.ID)
storage.Put(ctx, totalSupplyKey, 1)
}
// Symbol returns token symbol, it's NICENAMES.
func Symbol() string {
return "NICENAMES"
}
// Decimals returns token decimals, this NFT is non-divisible, so it's 0.
func Decimals() int {
return 0
}
// TotalSupply is a contract method that returns the number of tokens minted.
func TotalSupply() int {
return storage.Get(storage.GetReadOnlyContext(), totalSupplyKey).(int)
}
// BalanceOf returns the number of tokens owned by the specified address.
func BalanceOf(holder interop.Hash160) int {
if len(holder) != 20 {
panic("bad owner address")
}
ctx := storage.GetReadOnlyContext()
return getBalanceOf(ctx, mkBalanceKey(holder))
}
// OwnerOf returns the owner of the specified token.
func OwnerOf(token []byte) interop.Hash160 {
ctx := storage.GetReadOnlyContext()
return getNFT(ctx, token).Owner
}
// Properties returns 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": ownerAddress(nft.Owner),
"name": nft.Name,
"prevOwners": std.Itoa10(nft.PrevOwners),
"created": std.Itoa10(nft.Created),
"bought": std.Itoa10(nft.Bought),
}
return result
}
// Tokens returns an iterator that contains all 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)
keys := []string{}
for iterator.Next(iter) {
k := iterator.Value(iter)
keys = append(keys, k.(string))
}
return keys
}
// TokensOf returns an iterator with all tokens held by the specified address.
func TokensOf(holder interop.Hash160) iterator.Iterator {
if len(holder) != 20 {
panic("bad owner address")
}
ctx := storage.GetReadOnlyContext()
key := mkAccountPrefix(holder)
iter := storage.Find(ctx, key, storage.ValuesOnly)
return iter
}
func TokensOfList(holder interop.Hash160) [][]byte {
if len(holder) != 20 {
panic("bad owner address")
}
ctx := storage.GetReadOnlyContext()
key := mkAccountPrefix(holder)
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, notice that it only has three
// parameters because token owner can be deduced from token ID itself.
func Transfer(to interop.Hash160, token []byte, data any) bool {
if len(to) != 20 {
panic("invalid 'to' address")
}
ctx := storage.GetContext()
nft := getNFT(ctx, token)
from := nft.Owner
if !runtime.CheckWitness(from) {
return false
}
if !from.Equals(to) {
nft.Owner = to
nft.Bought = ledger.CurrentIndex()
nft.PrevOwners += 1
setNFT(ctx, token, nft)
addToBalance(ctx, from, -1)
removeToken(ctx, from, token)
addToBalance(ctx, to, 1)
addToken(ctx, to, token)
}
postTransfer(from, to, token, data)
return true
}
func getNFT(ctx storage.Context, token []byte) NFTItem {
key := mkTokenKey(token)
val := storage.Get(ctx, key)
if val == nil {
panic("no token found")
}
serializedNFT := val.([]byte)
deserializedNFT := std.Deserialize(serializedNFT)
return deserializedNFT.(NFTItem)
}
func nftExists(ctx storage.Context, token []byte) bool {
key := mkTokenKey(token)
return storage.Get(ctx, key) != nil
}
func setNFT(ctx storage.Context, token []byte, item NFTItem) {
key := mkTokenKey(token)
val := std.Serialize(item)
storage.Put(ctx, key, val)
}
// postTransfer emits Transfer event and calls onNEP11Payment if needed.
func postTransfer(from interop.Hash160, to interop.Hash160, token []byte, data any) {
runtime.Notify("Transfer", from, to, 1, token)
if management.GetContract(to) != nil {
contract.Call(to, "onNEP11Payment", contract.All, from, 1, token, data)
}
}
// OnNEP17Payment mints tokens if at least 10 GAS is provided. You don't call
// this method directly, instead it's called by GAS contract when you transfer
// GAS from your address to the address of this NFT contract.
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")
}
name := data.(string)
if len(name) < 3 {
panic("name length at least 3 character")
}
price := 10_0000_0000
if len(name) < 10 {
price += 5_0000_0000
}
if len(name) < 6 {
price += 5_0000_0000
}
if amount < price {
panic("insufficient GAS for minting NFT")
}
ctx := storage.GetContext()
tokenID := crypto.Sha256([]byte(name))
if nftExists(ctx, tokenID) {
panic("token already exists")
}
nft := NFTItem{
ID: tokenID,
Owner: from,
Name: name,
PrevOwners: 0,
Created: ledger.CurrentIndex(),
Bought: ledger.CurrentIndex(),
}
setNFT(ctx, tokenID, nft)
addToBalance(ctx, from, 1)
addToken(ctx, from, tokenID)
total := storage.Get(ctx, totalSupplyKey).(int) + 1
storage.Put(ctx, totalSupplyKey, total)
postTransfer(nil, from, tokenID, nil)
}
// mkAccountPrefix creates DB key-prefix for the account tokens specified
// by concatenating accountPrefix and account address.
func mkAccountPrefix(holder interop.Hash160) []byte {
res := []byte(accountPrefix)
return append(res, holder...)
}
// mkBalanceKey creates DB key for the account specified by concatenating balancePrefix
// and account address.
func mkBalanceKey(holder interop.Hash160) []byte {
res := []byte(balancePrefix)
return append(res, holder...)
}
// mkTokenKey creates DB key for the token specified by concatenating tokenPrefix
// and token ID.
func mkTokenKey(tokenID []byte) []byte {
res := []byte(tokenPrefix)
return append(res, tokenID...)
}
// getBalanceOf returns the balance of an account using database key.
func getBalanceOf(ctx storage.Context, balanceKey []byte) int {
val := storage.Get(ctx, balanceKey)
if val != nil {
return val.(int)
}
return 0
}
// addToBalance adds an amount to the account balance. Amount can be negative.
func addToBalance(ctx storage.Context, holder interop.Hash160, amount int) {
key := mkBalanceKey(holder)
old := getBalanceOf(ctx, key)
old += amount
if old > 0 {
storage.Put(ctx, key, old)
} else {
storage.Delete(ctx, key)
}
}
// addToken adds a token to the account.
func addToken(ctx storage.Context, holder interop.Hash160, token []byte) {
key := mkAccountPrefix(holder)
storage.Put(ctx, append(key, token...), token)
}
// removeToken removes the token from the account.
func removeToken(ctx storage.Context, holder interop.Hash160, token []byte) {
key := mkAccountPrefix(holder)
storage.Delete(ctx, append(key, token...))
}
func ownerAddress(owner interop.Hash160) string {
b := append([]byte{0x35}, owner...)
return std.Base58CheckEncode(b)
}