Merge pull request #2408 from nspcc-dev/improve-examples

Improve examples&documentation
This commit is contained in:
Roman Khimov 2022-03-25 18:16:50 +03:00 committed by GitHub
commit c039133acf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 151 additions and 344 deletions

View file

@ -22,8 +22,7 @@ a dialect of Go rather than a complete port of the language:
* `defer` and `recover` are supported except for cases where panic occurs in
`return` statement, because this complicates implementation and imposes runtime
overhead for all contracts. This can easily be mitigated by first storing values
in variables and returning the result. `defer` can't be used in
conditional code (#2293).
in variables and returning the result.
* lambdas are supported, but closures are not.
* maps are supported, but valid map keys are booleans, integers and strings with length <= 64

View file

@ -27,13 +27,12 @@ See the table below for the detailed examples description.
| [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. |
| [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. The package also contains tests implemented with [neotest](https://pkg.go.dev/github.com/nspcc-dev/neo-go/pkg/neotest). |
| [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

View file

@ -6,11 +6,28 @@ import (
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
)
// NotifyKeysAndValues sends notification with `foo` storage keys and values
// _deploy primes contract's storage with some data to be used later.
func _deploy(_ interface{}, _ bool) {
ctx := storage.GetContext() // RW context.
storage.Put(ctx, "foo1", "1")
storage.Put(ctx, "foo2", "2")
storage.Put(ctx, "foo3", "3")
}
// NotifyKeysAndValues sends notification with `foo` storage keys and values.
func NotifyKeysAndValues() bool {
iter := storage.Find(storage.GetContext(), []byte("foo"), storage.None)
iter := storage.Find(storage.GetReadOnlyContext(), []byte("foo"), storage.None)
for iterator.Next(iter) {
runtime.Notify("found storage key-value pair", iterator.Value(iter))
runtime.Notify("Key-Value", iterator.Value(iter))
}
return true
}
// NotifyValues sends notification with `foo` storage values.
func NotifyValues() bool {
iter := storage.Find(storage.GetReadOnlyContext(), []byte("foo"), storage.ValuesOnly)
for iterator.Next(iter) {
runtime.Notify("Value", iterator.Value(iter))
}
return true
}

View file

@ -2,7 +2,11 @@ name: "Iterator example"
sourceurl: https://github.com/nspcc-dev/neo-go/
supportedstandards: []
events:
- name: found storage key-value pair
- name: Key-Value
parameters:
- name: value
type: Any
- name: Value
parameters:
- name: value
type: Any

View file

@ -351,6 +351,12 @@ func removeOwner(ctx storage.Context, token []byte, holder interop.Hash160) {
// 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{}) {
defer func() {
if r := recover(); r != nil {
runtime.Log(r.(string))
util.Abort()
}
}()
if string(runtime.GetCallingScriptHash()) != gas.Hash {
panic("only GAS is accepted")
}

View file

@ -1,4 +1,4 @@
package tests
package nns_test
import (
"strings"
@ -17,7 +17,7 @@ import (
func newNSClient(t *testing.T) *neotest.ContractInvoker {
bc, acc := chain.NewSingle(t)
e := neotest.NewExecutor(t, bc, acc, acc)
c := neotest.CompileFile(t, e.CommitteeHash, "..", "../nns.yml")
c := neotest.CompileFile(t, e.CommitteeHash, ".", "nns.yml")
e.DeployContract(t, c, nil)
return e.CommitteeInvoker(c.Hash)

View file

@ -210,6 +210,12 @@ func postTransfer(from interop.Hash160, to interop.Hash160, token []byte, data i
// 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{}) {
defer func() {
if r := recover(); r != nil {
runtime.Log(r.(string))
util.Abort()
}
}()
if string(runtime.GetCallingScriptHash()) != gas.Hash {
panic("only GAS is accepted")
}

View file

@ -1,6 +1,7 @@
package runtimecontract
import (
"github.com/nspcc-dev/neo-go/pkg/interop/native/management"
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
"github.com/nspcc-dev/neo-go/pkg/interop/util"
)
@ -8,48 +9,73 @@ import (
var (
// Check if the invoker of the contract is the specified owner
owner = util.FromAddress("NbrUYaZgyhSkNoRo9ugRyEMdUZxrhkNaWB")
trigger byte
)
// init initializes trigger before any other contract method is called
// init is transformed into _initialize method that is called whenever contract
// is being loaded (so you'll see this log entry with every invocation).
func init() {
trigger = runtime.GetTrigger()
// No events and logging allowed in verification context.
if runtime.GetTrigger() != runtime.Verification {
runtime.Log("init called")
}
}
// _deploy is called after contract deployment or update, it'll be called
// in deployment transaction and if call update method of this contract.
func _deploy(_ interface{}, isUpdate bool) {
if isUpdate {
Log("_deploy method called before contract update")
Log("_deploy method called after contract update")
return
}
Log("_deploy method called before contract creation")
Log("_deploy method called after contract creation")
}
// CheckWitness checks owner's witness
// CheckWitness checks owner's witness. It returns true if invoked by the owner
// and false otherwise.
func CheckWitness() bool {
// Log owner upon Verification trigger
if trigger != runtime.Verification {
return false
}
if runtime.CheckWitness(owner) {
runtime.Log("Verified Owner")
}
return true
}
return false
}
// Log logs given message
func Log(message string) bool {
if trigger != runtime.Application {
return false
}
// Log logs given message.
func Log(message string) {
runtime.Log(message)
return true
}
// Notify notifies about given message
func Notify(event interface{}) bool {
if trigger != runtime.Application {
// Notify emits an event with the specified data.
func Notify(event interface{}) {
runtime.Notify("Event", event)
}
// Verify method is used when contract is being used as a signer of transaction,
// it can have parameters (that then need to be present in invocation script)
// and it returns simple pass/fail result. This implementation just checks for
// owner's signature presence.
func Verify() bool {
// Technically this restriction is not needed, but you can see the difference
// between invokefunction and invokecontractverify RPC methods with it.
if runtime.GetTrigger() != runtime.Verification {
return false
}
runtime.Notify("Event", event)
return true
return CheckWitness()
}
// 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. _deploy will be called
// after update.
func Update(nef, manifest []byte) {
if !Verify() {
panic("only owner can update")
}
management.Update(nef, manifest)
}

View file

@ -6,3 +6,6 @@ events:
parameters:
- name: event
type: Any
permissions:
- hash: fffdc93764dbaddd97c48f252a53ea4643faa3fd
methods: ["update", "destroy"]

View file

@ -1,5 +0,0 @@
module github.com/nspcc-dev/neo-go/examples/token-sale
go 1.16
require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20220321144137-d5a9af5860af

View file

@ -1,2 +0,0 @@
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20220321144137-d5a9af5860af h1:QO3pU/jSYyX3EHBX8BPO01oRkVhGBXPrQaQEhn+4fv8=
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20220321144137-d5a9af5860af/go.mod h1:QBE0I30F2kOAISNpT5oks82yF4wkkUq3SCfI3Hqgx/Y=

View file

@ -1,279 +0,0 @@
package tokensale
import (
"github.com/nspcc-dev/neo-go/pkg/interop"
"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 = 8
multiplier = decimals * 10
)
var (
owner = util.FromAddress("NbrUYaZgyhSkNoRo9ugRyEMdUZxrhkNaWB")
trigger byte
token TokenConfig
ctx storage.Context
)
// TokenConfig holds information about the token we want to use for the sale.
type TokenConfig struct {
// Name of the token.
Name string
// 3 letter abbreviation of the token.
Symbol string
// How decimals this token will have.
Decimals int
// Address of the token owner. This is the Uint160 hash.
Owner []byte
// The total amount of tokens created. Notice that we need to multiply the
// amount by 100000000. (10^8)
TotalSupply int
// Initial amount is number of tokens that are available for the token sale.
InitialAmount int
// How many NEO will be worth 1 token. For example:
// Lets say 1 euro per token, where 1 NEO is 60 euro. This means buyers
// will get (60 * 10^8) tokens for 1 NEO.
AmountPerNEO int
// How many Gas will be worth 1 token. This is the same calculation as
// for the AmountPerNEO, except Gas price will have a different value.
AmountPerGas int
// The maximum amount you can mint in the limited round. For example:
// 500 NEO/buyer * 60 tokens/NEO * 10^8
MaxExchangeLimitRound int
// When to start the token sale.
SaleStart int
// When to end the initial limited round if there is one. For example:
// SaleStart + 10000
LimitRoundEnd int
// The prefix used to store how many tokens there are in circulation.
CirculationKey []byte
// The prefix used to store how many tokens there are in the limited round.
LimitRoundKey []byte
// The prefix used to store the addresses that are registered with KYC.
KYCKey []byte
}
// newTokenConfig returns the initialized TokenConfig.
func newTokenConfig() TokenConfig {
return TokenConfig{
Name: "My awesome token",
Symbol: "MAT",
Decimals: decimals,
Owner: owner,
TotalSupply: 10000000 * multiplier,
InitialAmount: 5000000 * multiplier,
AmountPerNEO: 60 * multiplier,
AmountPerGas: 40 * multiplier,
MaxExchangeLimitRound: 500 * 60 * multiplier,
SaleStart: 75500,
LimitRoundEnd: 75500 + 10000,
CirculationKey: []byte("in_circulation"),
LimitRoundKey: []byte("r1"),
KYCKey: []byte("kyc_ok"),
}
}
// getIntFromDB is a helper that checks for nil result of storage.Get and returns
// zero as the default value.
func getIntFromDB(ctx storage.Context, key []byte) int {
var res int
val := storage.Get(ctx, key)
if val != nil {
res = val.(int)
}
return res
}
// InCirculation returns the amount of total tokens that are in circulation.
func InCirculation() int {
return getIntFromDB(ctx, token.CirculationKey)
}
// addToCirculation sets the given amount as "in circulation" in the storage.
func addToCirculation(amount int) bool {
if amount < 0 {
return false
}
supply := getIntFromDB(ctx, token.CirculationKey)
supply += amount
if supply > token.TotalSupply {
return false
}
storage.Put(ctx, token.CirculationKey, supply)
return true
}
// AvailableAmount returns the total amount of available tokens left
// to be distributed.
func AvailableAmount() int {
inCirc := getIntFromDB(ctx, token.CirculationKey)
return token.TotalSupply - inCirc
}
// init initializes runtime trigger, TokenConfig and storage context before any
// other contract method is called
func init() {
trigger = runtime.GetTrigger()
token = newTokenConfig()
ctx = storage.GetContext()
}
// checkOwnerWitness is a helper function which checks whether the invoker is the
// owner of the contract.
func checkOwnerWitness() bool {
// This is used to verify if a transfer of system assets (NEO and Gas)
// involving this contract's address can proceed.
if trigger == runtime.Application {
// Check if the invoker is the owner of the contract.
return runtime.CheckWitness(token.Owner)
}
return false
}
// Decimals returns the token decimals
func Decimals() int {
if trigger != runtime.Application {
panic("invalid trigger")
}
return token.Decimals
}
// Symbol returns the token symbol
func Symbol() string {
if trigger != runtime.Application {
panic("invalid trigger")
}
return token.Symbol
}
// TotalSupply returns the token total supply value
func TotalSupply() int {
if trigger != runtime.Application {
panic("invalid trigger")
}
return getIntFromDB(ctx, token.CirculationKey)
}
// BalanceOf returns the amount of token on the specified address
func BalanceOf(holder interop.Hash160) int {
if trigger != runtime.Application {
panic("invalid trigger")
}
return getIntFromDB(ctx, holder)
}
// Transfer transfers specified amount of token from one user to another
func Transfer(from, to interop.Hash160, amount int, _ interface{}) bool {
if trigger != runtime.Application {
return false
}
if amount <= 0 || len(to) != 20 || !runtime.CheckWitness(from) {
return false
}
amountFrom := getIntFromDB(ctx, from)
if amountFrom < amount {
return false
}
if amountFrom == amount {
storage.Delete(ctx, from)
} else {
diff := amountFrom - amount
storage.Put(ctx, from, diff)
}
amountTo := getIntFromDB(ctx, to)
totalAmountTo := amountTo + amount
if totalAmountTo != 0 {
storage.Put(ctx, to, totalAmountTo)
}
return true
}
// TransferFrom transfers specified amount of token from one user to another.
// It differs from Transfer in that it use allowance value to store the amount
// of token available to transfer.
func TransferFrom(from, to []byte, amount int) bool {
if trigger != runtime.Application {
return false
}
if amount <= 0 {
return false
}
availableKey := append(from, to...)
if len(availableKey) != 40 {
return false
}
availableTo := getIntFromDB(ctx, availableKey)
if availableTo < amount {
return false
}
fromBalance := getIntFromDB(ctx, from)
if fromBalance < amount {
return false
}
toBalance := getIntFromDB(ctx, to)
newFromBalance := fromBalance - amount
newToBalance := toBalance + amount
storage.Put(ctx, to, newToBalance)
storage.Put(ctx, from, newFromBalance)
newAllowance := availableTo - amount
if newAllowance == 0 {
storage.Delete(ctx, availableKey)
} else {
storage.Put(ctx, availableKey, newAllowance)
}
return true
}
// Approve stores token transfer data if the owner has enough token to send.
func Approve(owner, spender []byte, amount int) bool {
if !checkOwnerWitness() || amount < 0 {
return false
}
if len(spender) != 20 {
return false
}
toSpend := getIntFromDB(ctx, owner)
if toSpend < amount {
return false
}
approvalKey := append(owner, spender...)
if amount == 0 {
storage.Delete(ctx, approvalKey)
} else {
storage.Put(ctx, approvalKey, amount)
}
return true
}
// Allowance returns allowance value for specified sender and receiver.
func Allowance(from, to []byte) interface{} {
if trigger != runtime.Application {
return false
}
key := append(from, to...)
return getIntFromDB(ctx, key)
}
// Mint initial supply of tokens
func Mint(to []byte) bool {
if trigger != runtime.Application {
return false
}
if !checkOwnerWitness() {
return false
}
minted := storage.Get(ctx, []byte("minted"))
if minted != nil && minted.(bool) == true {
return false
}
storage.Put(ctx, to, token.TotalSupply)
storage.Put(ctx, []byte("minted"), true)
addToCirculation(token.TotalSupply)
return true
}

View file

@ -1,13 +0,0 @@
name: "My awesome token"
sourceurl: https://github.com/nspcc-dev/neo-go/
supportedstandards: ["NEP-17"]
safemethods: ["balanceOf", "decimals", "symbol", "totalSupply"]
events:
- name: Transfer
parameters:
- name: from
type: Hash160
- name: to
type: Hash160
- name: amount
type: Integer

View file

@ -131,7 +131,7 @@ func (c *codegen) processStdlibCall(f *funcScope, args []ast.Expr) {
func (c *codegen) processNotify(f *funcScope, args []ast.Expr) {
if c.scope != nil && c.isVerifyFunc(c.scope.decl) &&
c.scope.pkg == c.mainPkg.Types && !c.buildInfo.options.NoEventsCheck {
c.scope.pkg == c.mainPkg.Types && (c.buildInfo.options == nil || !c.buildInfo.options.NoEventsCheck) {
c.prog.Err = fmt.Errorf("runtime.%s is not allowed in `Verify`", f.name)
return
}

1
pkg/interop/LICENSE.md Symbolic link
View file

@ -0,0 +1 @@
../../LICENSE.md

View file

@ -17,6 +17,16 @@ import (
"go.uber.org/zap/zaptest"
)
const (
// MaxTraceableBlocks is the default MaxTraceableBlocks setting used for test chains.
// We don't need a lot of traceable blocks for tests.
MaxTraceableBlocks = 1000
// SecondsPerBlock is the default SecondsPerBlock setting used for test chains.
// Usually blocks are created by tests bypassing this setting.
SecondsPerBlock = 1
)
const singleValidatorWIF = "KxyjQ8eUa4FHt3Gvioyt1Wz29cTUrE4eTqX3yFSk1YFCsPL8uNsY"
// committeeWIFs is a list of unencrypted WIFs sorted by public key.
@ -106,23 +116,32 @@ func init() {
}
// NewSingle creates new blockchain instance with a single validator and
// setups cleanup functions.
// setups cleanup functions. The configuration used is with netmode.UnitTestNet
// magic, and SecondsPerBlock/MaxTraceableBlocks options defined by constants in
// this package. MemoryStore is used as the backend storage, so all of the chain
// contents is always in RAM. The Signer returned is validator (and committee at
// the same time).
func NewSingle(t *testing.T) (*core.Blockchain, neotest.Signer) {
return NewSingleWithCustomConfig(t, nil)
}
// NewSingleWithCustomConfig creates new blockchain instance with custom protocol
// configuration and a single validator. It also setups cleanup functions.
// NewSingleWithCustomConfig is similar to NewSingle, but allows to override the
// default configuration.
func NewSingleWithCustomConfig(t *testing.T, f func(*config.ProtocolConfiguration)) (*core.Blockchain, neotest.Signer) {
st := storage.NewMemoryStore()
return NewSingleWithCustomConfigAndStore(t, f, st, true)
}
// NewSingleWithCustomConfigAndStore is similar to NewSingleWithCustomConfig, but
// also allows to override backend Store being used. The last parameter controls if
// Run method is called on the Blockchain instance, if not then it's caller's
// responsibility to do that before using the chain and its caller's responsibility
// also to properly Close the chain when done.
func NewSingleWithCustomConfigAndStore(t *testing.T, f func(cfg *config.ProtocolConfiguration), st storage.Store, run bool) (*core.Blockchain, neotest.Signer) {
protoCfg := config.ProtocolConfiguration{
Magic: netmode.UnitTestNet,
MaxTraceableBlocks: 1000, // We don't need a lot of traceable blocks for tests.
SecondsPerBlock: 1,
MaxTraceableBlocks: MaxTraceableBlocks,
SecondsPerBlock: SecondsPerBlock,
StandbyCommittee: []string{hex.EncodeToString(committeeAcc.PrivateKey().PublicKey().Bytes())},
ValidatorsCount: 1,
VerifyBlocks: true,
@ -141,19 +160,20 @@ func NewSingleWithCustomConfigAndStore(t *testing.T, f func(cfg *config.Protocol
return bc, neotest.NewMultiSigner(committeeAcc)
}
// NewMulti creates new blockchain instance with 4 validators and 6 committee members.
// Second return value is for validator signer, third -- for committee.
// NewMulti creates new blockchain instance with four validators and six
// committee members, otherwise not differring much from NewSingle. The
// second value returned contains validators Signer, the third -- committee one.
func NewMulti(t *testing.T) (*core.Blockchain, neotest.Signer, neotest.Signer) {
return NewMultiWithCustomConfig(t, nil)
}
// NewMultiWithCustomConfig creates new blockchain instance with custom protocol
// configuration, 4 validators and 6 committee members. Second return value is
// for validator signer, third -- for committee.
// NewMultiWithCustomConfig is similar to NewMulti except it allows to override the
// default configuration.
func NewMultiWithCustomConfig(t *testing.T, f func(*config.ProtocolConfiguration)) (*core.Blockchain, neotest.Signer, neotest.Signer) {
protoCfg := config.ProtocolConfiguration{
Magic: netmode.UnitTestNet,
SecondsPerBlock: 1,
MaxTraceableBlocks: MaxTraceableBlocks,
SecondsPerBlock: SecondsPerBlock,
StandbyCommittee: standByCommittee,
ValidatorsCount: 4,
VerifyBlocks: true,

7
pkg/neotest/chain/doc.go Normal file
View file

@ -0,0 +1,7 @@
/*
Package chain contains functions creating new test blockchain instances.
Different configurations can be used, but all chains created here use
well-known keys. Most of the time single-node chain is the best choice to use
unless you specifically need multiple validators and large committee.
*/
package chain

18
pkg/neotest/doc.go Normal file
View file

@ -0,0 +1,18 @@
/*
Package neotest contains framework for automated contract testing.
It can be used to implement unit-tests for contracts in Go using regular Go
conventions.
Usually it's used like this:
* an instance of blockchain is created using chain subpackage
* target contract is compiled using one of Compile* functions
* and Executor is created for blockchain
* it's used to deploy contract with DeployContract
* CommitteeInvoker and/or ValidatorInvoker are then created to perform test invocations
* if needed NewAccount is used to create appropriate number of accounts for the test
Higher-order methods provided in Executor and ContractInvoker hide the details
of transaction creation for the most part, but there are lower-level methods as
well that can be used for specific tasks.
*/
package neotest