Merge pull request #2333 from nspcc-dev/examples/nep11-d

examples: add NEP11 Divisible token example
This commit is contained in:
Roman Khimov 2022-01-24 13:17:54 +03:00 committed by GitHub
commit f9e8dcbed8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 466 additions and 13 deletions

View file

@ -20,18 +20,19 @@ You can use `my_wallet.json` to deploy example contracts.
See the table below for the detailed examples description. See the table below for the detailed examples description.
| Example | Description | | Example | 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. | | [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. | | [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. | | [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. | | [nft-d](nft-d) | NEP-11 divisible NFT. See NEP-11 token standard [specification](https://github.com/neo-project/proposals/blob/master/nep-11.mediawiki) for details. |
| [nft-nd](nft-nd) | NEP-11 non-divisible NFT. See NEP-11 token standard [specification](https://github.com/neo-project/proposals/blob/master/nep-11.mediawiki) for details. |
| [nft-nd-nns](nft-nd-nns) | Neo Name Service contract which is NEP-11 non-divisible NFT. The contract implements methods for Neo domain name system managing such as domains registration/transferring, records addition and names resolving. | | [nft-nd-nns](nft-nd-nns) | Neo Name Service contract which is NEP-11 non-divisible NFT. The contract implements methods for Neo domain name system managing such as domains registration/transferring, records addition and names resolving. |
| [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. | | [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). | | [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). | | [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). |
| [timer](timer) | The idea of the contract is to count `tick` method invocations and destroy itself after the third invocation. It shows how to use `contract.Call` interop function to call, update (migrate) and destroy the contract. Please, refer to the `contract.Call` [function documentation](../pkg/interop/contract/contract.go) | | [timer](timer) | The idea of the contract is to count `tick` method invocations and destroy itself after the third invocation. It shows how to use `contract.Call` interop function to call, update (migrate) and destroy the contract. Please, refer to the `contract.Call` [function documentation](../pkg/interop/contract/contract.go) |
| [token](token) | This contract implements NEP-17 token standard (like NEO and GAS tokens) with all required methods and operations. See the NEP-17 token standard [specification](https://github.com/neo-project/proposals/pull/126) for details. | | [token](token) | This contract implements NEP-17 token standard (like NEO and GAS tokens) with all required methods and operations. See the NEP-17 token standard [specification](https://github.com/neo-project/proposals/pull/126) for details. |
| [token-sale](token-sale) | The contract represents a token with `allowance`. It means that the token owner should approve token withdrawing before the transfer. The contract demonstrates how interop packages can be combined to work together. | | [token-sale](token-sale) | The contract represents a token with `allowance`. It means that the token owner should approve token withdrawing before the transfer. The contract demonstrates how interop packages can be combined to work together. |
## Compile ## Compile

5
examples/nft-d/go.mod Normal file
View file

@ -0,0 +1,5 @@
module github.com/nspcc-dev/neo-go/examples/nft
go 1.15
require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20220118080652-4eddfdbbc652

2
examples/nft-d/go.sum Normal file
View file

@ -0,0 +1,2 @@
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20220118080652-4eddfdbbc652 h1:Paq5oU7mlXjzFcVDD97RA4sxFljAmFrnLrcsObBGIGY=
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20220118080652-4eddfdbbc652/go.mod h1:/zA6GVDzpSkwq8/HQJxPWDcvfn2BbZnahUO9A1wAevM=

416
examples/nft-d/nft.go Normal file
View file

@ -0,0 +1,416 @@
/*
Package nft contains divisible non-fungible NEP-11-compatible token
implementation. This token can be minted with GAS transfer to contract address,
it will retrieve NeoFS container ID and object ID from the transfer data and
produce NFT which represents NeoFS object.
*/
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/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"
)
const (
decimals = 2
multiplier = 100
)
// Prefixes used for contract data storage.
const (
totalSupplyPrefix = "s"
// balancePrefix contains map from [address + token id] to address's balance of the specified token.
balancePrefix = "b"
// tokenOwnerPrefix contains map from [token id + owner] to token's owner.
tokenOwnerPrefix = "t"
// tokenPrefix contains map from token id to empty array.
tokenPrefix = "i"
)
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("NbrUYaZgyhSkNoRo9ugRyEMdUZxrhkNaWB")
)
// ObjectIdentifier represents NFT structure and contains the container ID and
// object ID of the NeoFS object.
type ObjectIdentifier struct {
ContainerID []byte
ObjectID []byte
}
// Common methods
// Symbol returns token symbol, it's NFSO.
func Symbol() string {
return "NFSO"
}
// Decimals returns token decimals, this NFT is divisible.
func Decimals() int {
return decimals
}
// 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
}
// mkBalancePrefix creates DB key-prefix for account balances specified
// by concatenating balancePrefix and account address.
func mkBalancePrefix(holder interop.Hash160) []byte {
res := []byte(balancePrefix)
return append(res, holder...)
}
// mkBalanceKey creates DB key for account specified by concatenating balancePrefix,
// account address and token ID.
func mkBalanceKey(holder interop.Hash160, tokenID []byte) []byte {
res := mkBalancePrefix(holder)
return append(res, tokenID...)
}
// mkTokenOwnerPrefix creates DB key prefix for token specified by its ID.
func mkTokenOwnerPrefix(tokenID []byte) []byte {
res := []byte(tokenOwnerPrefix)
return append(res, tokenID...)
}
// mkTokenOwnerKey creates DB key for token specified by concatenating tokenOwnerPrefix,
// token ID and holder.
func mkTokenOwnerKey(tokenID []byte, holder interop.Hash160) []byte {
res := mkTokenOwnerPrefix(tokenID)
return append(res, holder...)
}
// mkTokenKey creates DB key for token specified by its ID.
func mkTokenKey(tokenID []byte) []byte {
res := []byte(tokenPrefix)
return append(res, tokenID...)
}
// BalanceOf returns the overall number of tokens owned by specified address.
func BalanceOf(holder interop.Hash160) int {
if len(holder) != interop.Hash160Len {
panic("bad owner address")
}
ctx := storage.GetReadOnlyContext()
balance := 0
iter := tokensOf(ctx, holder)
for iterator.Next(iter) {
tokenID := iterator.Value(iter).([]byte)
key := mkBalanceKey(holder, tokenID)
balance += getBalanceOf(ctx, key)
}
return balance
}
// getBalanceOf returns balance of the account of the specified tokenID 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 amount to the account balance. Amount can be negative. It returns
// updated balance value.
func addToBalance(ctx storage.Context, holder interop.Hash160, tokenID []byte, amount int) int {
key := mkBalanceKey(holder, tokenID)
old := getBalanceOf(ctx, key)
old += amount
if old > 0 {
storage.Put(ctx, key, old)
} else {
storage.Delete(ctx, key)
}
return old
}
// TokensOf returns an iterator with all tokens held by specified address.
func TokensOf(holder interop.Hash160) iterator.Iterator {
if len(holder) != interop.Hash160Len {
panic("bad owner address")
}
ctx := storage.GetReadOnlyContext()
return tokensOf(ctx, holder)
}
func tokensOf(ctx storage.Context, holder interop.Hash160) iterator.Iterator {
key := mkBalancePrefix(holder)
// We don't store zero balances, thus only relevant token IDs of the holder will
// be returned.
iter := storage.Find(ctx, key, storage.KeysOnly|storage.RemovePrefix)
return iter
}
// Transfer token from its owner to another user, if there's one owner of the token.
// It will return false if token is shared between multiple owners.
func Transfer(to interop.Hash160, token []byte, data interface{}) bool {
if len(to) != interop.Hash160Len {
panic("invalid 'to' address")
}
ctx := storage.GetContext()
var (
owner interop.Hash160
ok bool
)
iter := ownersOf(ctx, token)
for iterator.Next(iter) {
if ok {
// Token is shared between multiple owners.
return false
}
owner = iterator.Value(iter).(interop.Hash160)
ok = true
}
if !ok {
panic("unknown 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
}
key := mkBalanceKey(owner, token)
amount := getBalanceOf(ctx, key)
if string(owner) != string(to) {
addToBalance(ctx, owner, token, -amount)
removeOwner(ctx, token, owner)
addToBalance(ctx, to, token, amount)
addOwner(ctx, token, to)
}
postTransfer(owner, to, token, amount, data)
return true
}
// postTransfer emits Transfer event and calls onNEP11Payment if needed.
func postTransfer(from interop.Hash160, to interop.Hash160, token []byte, amount int, data interface{}) {
runtime.Notify("Transfer", from, to, amount, token)
if management.GetContract(to) != nil {
contract.Call(to, "onNEP11Payment", contract.All, from, amount, token, data)
}
}
// end of common methods.
// Optional methods.
// Properties returns properties of the given NFT.
func Properties(id []byte) map[string]string {
ctx := storage.GetReadOnlyContext()
if !isTokenValid(ctx, id) {
panic("unknown token")
}
t := std.Deserialize(id).(ObjectIdentifier)
result := map[string]string{
"name": "NFSO " + string(id),
"fullName": "NeoFS Object",
"containerID": string(t.ContainerID),
"objectID": string(t.ObjectID),
}
return result
}
// Tokens returns all token IDs minted by the contract.
func Tokens() iterator.Iterator {
ctx := storage.GetReadOnlyContext()
prefix := []byte(tokenPrefix)
iter := storage.Find(ctx, prefix, storage.KeysOnly|storage.RemovePrefix)
return iter
}
func isTokenValid(ctx storage.Context, tokenID []byte) bool {
key := mkTokenKey(tokenID)
result := storage.Get(ctx, key)
return result != nil
}
// End of optional methods.
// Divisible methods.
// TransferDivisible 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 TransferDivisible(from, to interop.Hash160, amount int, token []byte, data interface{}) bool {
if len(from) != interop.Hash160Len {
panic("invalid 'from' address")
}
if len(to) != interop.Hash160Len {
panic("invalid 'to' address")
}
if amount < 0 {
panic("negative 'amount'")
}
if amount > multiplier {
panic("invalid 'amount'")
}
ctx := storage.GetContext()
if !isTokenValid(ctx, token) {
panic("unknown 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(from) {
return false
}
key := mkBalanceKey(from, token)
balance := getBalanceOf(ctx, key)
if amount > balance {
return false
}
if string(from) != string(to) {
updBalance := addToBalance(ctx, from, token, -amount)
if updBalance == 0 {
removeOwner(ctx, token, from)
}
updBalance = addToBalance(ctx, to, token, amount)
if updBalance != 0 {
addOwner(ctx, token, to)
}
}
postTransfer(from, to, token, amount, data)
return true
}
// OwnerOf returns owner of specified token.
func OwnerOf(token []byte) iterator.Iterator {
ctx := storage.GetReadOnlyContext()
if !isTokenValid(ctx, token) {
panic("unknown token")
}
return ownersOf(ctx, token)
}
// BalanceOfDivisible returns the number of token with the specified tokenID owned by specified address.
func BalanceOfDivisible(holder interop.Hash160, token []byte) int {
if len(holder) != interop.Hash160Len {
panic("bad holder address")
}
ctx := storage.GetReadOnlyContext()
key := mkBalanceKey(holder, token)
return getBalanceOf(ctx, key)
}
// end of divisible methods.
// ownersOf returns iterator over owners of the specified token. Owner is
// stored as value of the token key (prefix + token ID + owner).
func ownersOf(ctx storage.Context, token []byte) iterator.Iterator {
key := mkTokenOwnerPrefix(token)
iter := storage.Find(ctx, key, storage.ValuesOnly)
return iter
}
func addOwner(ctx storage.Context, token []byte, holder interop.Hash160) {
key := mkTokenOwnerKey(token, holder)
storage.Put(ctx, key, holder)
}
func removeOwner(ctx storage.Context, token []byte, holder interop.Hash160) {
key := mkTokenOwnerKey(token, holder)
storage.Delete(ctx, key)
}
// 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 NFSO costs at least 10 GAS")
}
tokenInfo := data.([]interface{})
if len(tokenInfo) != 2 {
panic("invalid 'data'")
}
containerID := tokenInfo[0].([]byte)
if len(containerID) != 32 {
panic("invalid container ID")
}
objectID := tokenInfo[1].([]byte)
if len(objectID) != 32 {
panic("invalid object ID")
}
t := ObjectIdentifier{
ContainerID: containerID,
ObjectID: objectID,
}
id := std.Serialize(t)
var ctx = storage.GetContext()
if isTokenValid(ctx, id) {
panic("NFSO for the specified address is already minted")
}
key := mkTokenKey(id)
storage.Put(ctx, key, []byte{})
total := totalSupply(ctx)
addOwner(ctx, from, id)
addToBalance(ctx, from, id, multiplier)
total++
storage.Put(ctx, []byte(totalSupplyPrefix), total)
postTransfer(nil, from, id, multiplier, nil) // no `data` during minting
}
// 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)
}

22
examples/nft-d/nft.yml Normal file
View file

@ -0,0 +1,22 @@
name: "NFSO NFT"
sourceurl: https://github.com/nspcc-dev/neo-go/
supportedstandards: ["NEP-11"]
safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf", "properties", "tokens"]
events:
- name: Transfer
parameters:
- name: from
type: Hash160
- name: to
type: Hash160
- name: amount
type: Integer
- name: tokenId
type: ByteArray
permissions:
- hash: fffdc93764dbaddd97c48f252a53ea4643faa3fd
methods: ["update", "destroy"]
- methods: ["onNEP11Payment"]
overloads:
balanceOfDivisible: balanceOf
transferDivisible: transfer

View file

@ -461,6 +461,13 @@ func (di *DebugInfo) ConvertToManifest(o *Options) (*manifest.Manifest, error) {
"multiple methods with the same number of parameters", name) "multiple methods with the same number of parameters", name)
} }
m.Name = emitName m.Name = emitName
// Check the resulting name against set of safe methods.
for i := range o.SafeMethods {
if m.Name == o.SafeMethods[i] {
m.Safe = true
break
}
}
} }
return result, nil return result, nil
} }

View file

@ -129,7 +129,7 @@ func checkMethod(m *manifest.Manifest, expected *manifest.Method,
} }
} }
if expected.Safe != actual.Safe { if expected.Safe != actual.Safe {
return fmt.Errorf("%w: expected %t", ErrSafeMethodMismatch, expected.Safe) return fmt.Errorf("'%s' %w: expected %t", expected.Name, ErrSafeMethodMismatch, expected.Safe)
} }
return nil return nil
} }

View file

@ -103,7 +103,7 @@ var nep11Divisible = &Standard{
Parameters: []manifest.Parameter{ Parameters: []manifest.Parameter{
{Name: "tokenId", Type: smartcontract.ByteArrayType}, {Name: "tokenId", Type: smartcontract.ByteArrayType},
}, },
ReturnType: smartcontract.AnyType, ReturnType: smartcontract.InteropInterfaceType, // iterator
Safe: true, Safe: true,
}, },
{ {