Merge pull request #1891 from nspcc-dev/nep11-example
examples: add NFT example, fix #1845
This commit is contained in:
commit
fdb4836f17
3 changed files with 265 additions and 0 deletions
|
@ -25,6 +25,7 @@ See the table below for the detailed examples description.
|
|||
| [engine](engine) | This contract demonstrates how to use `runtime` interop package which implements an API for `System.Runtime.*` NEO system calls. Please, refer to the `runtime` [package documentation](../pkg/interop/doc.go) for details. |
|
||||
| [events](events) | The contract shows how execution notifications with the different arguments types can be sent with the help of `runtime.Notify` function of the `runtime` interop package. Please, refer to the `runtime.Notify` [function documentation](../pkg/interop/runtime/runtime.go) for details. |
|
||||
| [iterator](iterator) | This example describes a way to work with NEO iterators. Please, refer to the `iterator` [package documentation](../pkg/interop/iterator/iterator.go) for details. |
|
||||
| [nft-nd](nft-nd) | NEP-11 non-divisible NFT. See NEP-11 token standard [specification](https://github.com/neo-project/proposals/pull/130) for details. |
|
||||
| [oracle](oracle) | Oracle demo contract exposing two methods that you can use to process URLs. It uses oracle native contract, see [interop package documentation](../pkg/interop/native/oracle/oracle.go) also. |
|
||||
| [runtime](runtime) | This contract demonstrates how to use special `_initialize` and `_deploy` methods. See the [compiler documentation](../docs/compiler.md#vm-api-interop-layer ) for methods details. It also shows the pattern for checking owner witness inside the contract with the help of `runtime.CheckWitness` interop [function](../pkg/interop/runtime/runtime.go). |
|
||||
| [storage](storage) | The contract implements API for basic operations with a contract storage. It shows hos to use `storage` interop package. See the `storage` [package documentation](../pkg/interop/storage/storage.go). |
|
||||
|
|
250
examples/nft-nd/nft.go
Normal file
250
examples/nft-nd/nft.go
Normal file
|
@ -0,0 +1,250 @@
|
|||
/*
|
||||
Package nft contains non-divisible non-fungible NEP11-compatible token
|
||||
implementation. This token can be minted with GAS transfer to contract address,
|
||||
it will hash some data (including data provided in transfer) and produce
|
||||
base58-encoded string that is your NFT. Since it's based on hashing and basically
|
||||
you own a hash it's HASHY.
|
||||
*/
|
||||
package nft
|
||||
|
||||
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/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 (
|
||||
totalSupplyPrefix = "s"
|
||||
accountPrefix = "a"
|
||||
tokenPrefix = "t"
|
||||
)
|
||||
|
||||
var (
|
||||
// contractOwner is a special address that can perform some management
|
||||
// functions on this contract like updating/destroying it and can also
|
||||
// be used for contract address verification.
|
||||
contractOwner = util.FromAddress("NX1yL5wDx3inK2qUVLRVaqCLUxYnAbv85S")
|
||||
)
|
||||
|
||||
// Symbol returns token symbol, it's HASHY.
|
||||
func Symbol() string {
|
||||
return "HASHY"
|
||||
}
|
||||
|
||||
// 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 totalSupply(storage.GetReadOnlyContext())
|
||||
}
|
||||
|
||||
// totalSupply is an internal implementation of TotalSupply operating with
|
||||
// given context. The number itself is stored raw in the DB with totalSupplyPrefix
|
||||
// key.
|
||||
func totalSupply(ctx storage.Context) int {
|
||||
var res int
|
||||
|
||||
val := storage.Get(ctx, []byte(totalSupplyPrefix))
|
||||
if val != nil {
|
||||
res = val.(int)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// mkAccountKey creates DB key for account specified by concatenating accountPrefix
|
||||
// and account address.
|
||||
func mkAccountKey(holder interop.Hash160) []byte {
|
||||
res := []byte(accountPrefix)
|
||||
return append(res, holder...)
|
||||
}
|
||||
|
||||
// mkStringKey creates DB key for token specified by concatenating tokenPrefix
|
||||
// and token ID.
|
||||
func mkTokenKey(token []byte) []byte {
|
||||
res := []byte(tokenPrefix)
|
||||
return append(res, token...)
|
||||
}
|
||||
|
||||
// BalanceOf returns the number of tokens owned by specified address.
|
||||
func BalanceOf(holder interop.Hash160) int {
|
||||
if len(holder) != 20 {
|
||||
panic("bad owner address")
|
||||
}
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
tokens := getTokensOf(ctx, holder)
|
||||
return len(tokens)
|
||||
}
|
||||
|
||||
// getTokensOf is an internal implementation of TokensOf, tokens are stored
|
||||
// as a serialized slice of strings in the DB, so it gets and unwraps them
|
||||
// (or returns an empty slice).
|
||||
func getTokensOf(ctx storage.Context, holder interop.Hash160) []string {
|
||||
var res = []string{}
|
||||
|
||||
key := mkAccountKey(holder)
|
||||
val := storage.Get(ctx, key)
|
||||
if val != nil {
|
||||
res = std.Deserialize(val.([]byte)).([]string)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// setTokensOf saves current tokens owned by account if there are any,
|
||||
// otherwise it just drops the appropriate key from the DB.
|
||||
func setTokensOf(ctx storage.Context, holder interop.Hash160, tokens []string) {
|
||||
key := mkAccountKey(holder)
|
||||
if len(tokens) != 0 {
|
||||
val := std.Serialize(tokens)
|
||||
storage.Put(ctx, key, val)
|
||||
} else {
|
||||
storage.Delete(ctx, key)
|
||||
}
|
||||
}
|
||||
|
||||
// TokensOf returns an iterator with all tokens held by specified address.
|
||||
func TokensOf(holder interop.Hash160) iterator.Iterator {
|
||||
if len(holder) != 20 {
|
||||
panic("bad owner address")
|
||||
}
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
tokens := getTokensOf(ctx, holder)
|
||||
|
||||
return iterator.Create(tokens)
|
||||
}
|
||||
|
||||
// getOwnerOf returns current owner of the specified token or panics if token
|
||||
// ID is invalid. Owner is stored as value of the token key (prefix + token ID).
|
||||
func getOwnerOf(ctx storage.Context, token []byte) interop.Hash160 {
|
||||
key := mkTokenKey(token)
|
||||
val := storage.Get(ctx, key)
|
||||
if val == nil {
|
||||
panic("no token found")
|
||||
}
|
||||
return val.(interop.Hash160)
|
||||
}
|
||||
|
||||
// setOwnerOf writes current owner of the specified token into the DB.
|
||||
func setOwnerOf(ctx storage.Context, token []byte, holder interop.Hash160) {
|
||||
key := mkTokenKey(token)
|
||||
storage.Put(ctx, key, holder)
|
||||
}
|
||||
|
||||
// OwnerOf returns owner of specified token.
|
||||
func OwnerOf(token []byte) interop.Hash160 {
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
return getOwnerOf(ctx, token)
|
||||
}
|
||||
|
||||
// Transfer token from its owner to another user, notice that it only has two
|
||||
// parameters because token owner can be deduced from token ID itself and RC1
|
||||
// implementation doesn't yet have 'data' parameter as in NEP-17 Transfer.
|
||||
func Transfer(to interop.Hash160, token []byte) bool {
|
||||
if len(to) != 20 {
|
||||
panic("invalid 'to' address")
|
||||
}
|
||||
ctx := storage.GetContext()
|
||||
owner := getOwnerOf(ctx, token)
|
||||
|
||||
// Note that although calling script hash is not checked explicitly in
|
||||
// this contract it is in fact checked for in `CheckWitness` itself.
|
||||
if !runtime.CheckWitness(owner) {
|
||||
return false
|
||||
}
|
||||
|
||||
if string(owner) != string(to) {
|
||||
toksOwner := getTokensOf(ctx, owner)
|
||||
toksTo := getTokensOf(ctx, to)
|
||||
|
||||
var newToksOwner = []string{}
|
||||
for _, tok := range toksOwner {
|
||||
if tok != string(token) {
|
||||
newToksOwner = append(newToksOwner, tok)
|
||||
}
|
||||
}
|
||||
toksTo = append(toksTo, string(token))
|
||||
setTokensOf(ctx, owner, newToksOwner)
|
||||
setTokensOf(ctx, to, toksTo)
|
||||
setOwnerOf(ctx, token, to)
|
||||
}
|
||||
postTransfer(owner, to, token)
|
||||
return true
|
||||
}
|
||||
|
||||
// postTransfer emits Transfer event and calls onNEP11Payment if needed.
|
||||
func postTransfer(from interop.Hash160, to interop.Hash160, token []byte) {
|
||||
runtime.Notify("Transfer", from, to, 1, token)
|
||||
if management.GetContract(to) != nil {
|
||||
contract.Call(to, "onNEP11Payment", contract.All, from, 1, token)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 interface{}) {
|
||||
if string(runtime.GetCallingScriptHash()) != gas.Hash {
|
||||
panic("only GAS is accepted")
|
||||
}
|
||||
if amount < 10_00000000 {
|
||||
panic("minting HASHY costs at least 10 GAS")
|
||||
}
|
||||
var tokIn = []byte{}
|
||||
var ctx = storage.GetContext()
|
||||
|
||||
total := totalSupply(ctx)
|
||||
tokIn = append(tokIn, []byte(std.Itoa(total, 10))...)
|
||||
tokIn = append(tokIn, []byte(std.Itoa(amount, 10))...)
|
||||
tokIn = append(tokIn, from...)
|
||||
tx := runtime.GetScriptContainer()
|
||||
tokIn = append(tokIn, tx.Hash...)
|
||||
if data != nil {
|
||||
tokIn = append(tokIn, std.Serialize(data)...)
|
||||
}
|
||||
|
||||
tokenHash := crypto.Sha256(tokIn)
|
||||
token := std.Base58Encode(tokenHash)
|
||||
|
||||
toksOf := getTokensOf(ctx, from)
|
||||
toksOf = append(toksOf, token)
|
||||
setTokensOf(ctx, from, toksOf)
|
||||
setOwnerOf(ctx, []byte(token), from)
|
||||
|
||||
total++
|
||||
storage.Put(ctx, []byte(totalSupplyPrefix), total)
|
||||
|
||||
postTransfer(nil, from, []byte(token))
|
||||
}
|
||||
|
||||
// Verify allows owner to manage contract's address, including earned GAS
|
||||
// transfer from contract's address to somewhere else. It just checks for transaction
|
||||
// to also be signed by contract owner, so contract's witness should be empty.
|
||||
func Verify() bool {
|
||||
return runtime.CheckWitness(contractOwner)
|
||||
}
|
||||
|
||||
// Destroy destroys the contract, only owner can do that.
|
||||
func Destroy() {
|
||||
if !Verify() {
|
||||
panic("only owner can destroy")
|
||||
}
|
||||
management.Destroy()
|
||||
}
|
||||
|
||||
// Update updates the contract, only owner can do that.
|
||||
func Update(nef, manifest []byte) {
|
||||
if !Verify() {
|
||||
panic("only owner can update")
|
||||
}
|
||||
management.Update(nef, manifest)
|
||||
}
|
14
examples/nft-nd/nft.yml
Normal file
14
examples/nft-nd/nft.yml
Normal file
|
@ -0,0 +1,14 @@
|
|||
name: "HASHY NFT"
|
||||
supportedstandards: ["NEP-11"]
|
||||
safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf"]
|
||||
events:
|
||||
- name: Transfer
|
||||
parameters:
|
||||
- name: from
|
||||
type: Hash160
|
||||
- name: to
|
||||
type: Hash160
|
||||
- name: amount
|
||||
type: Integer
|
||||
- name: tokenId
|
||||
type: ByteArray
|
Loading…
Reference in a new issue