diff --git a/examples/README.md b/examples/README.md index b6a787227..c24eff814 100644 --- a/examples/README.md +++ b/examples/README.md @@ -20,18 +20,19 @@ You can use `my_wallet.json` to deploy example contracts. See the table below for the detailed examples 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. | -| [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. | +| 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. | +| [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-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. | -| [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). | -| [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. | +| [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). | +| [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-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 diff --git a/examples/nft-d/go.mod b/examples/nft-d/go.mod new file mode 100644 index 000000000..47351af1f --- /dev/null +++ b/examples/nft-d/go.mod @@ -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 diff --git a/examples/nft-d/go.sum b/examples/nft-d/go.sum new file mode 100644 index 000000000..edda2d6e7 --- /dev/null +++ b/examples/nft-d/go.sum @@ -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= diff --git a/examples/nft-d/nft.go b/examples/nft-d/nft.go new file mode 100644 index 000000000..d7f18ec7a --- /dev/null +++ b/examples/nft-d/nft.go @@ -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) +} diff --git a/examples/nft-d/nft.yml b/examples/nft-d/nft.yml new file mode 100644 index 000000000..04df58681 --- /dev/null +++ b/examples/nft-d/nft.yml @@ -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