forked from TrueCloudLab/neoneo-go
Merge pull request #1373 from nspcc-dev/feature/standard
manifest: support interface checking
This commit is contained in:
commit
badb1d6d49
24 changed files with 523 additions and 21 deletions
|
@ -104,3 +104,72 @@ func TestComlileAndInvokeFunction(t *testing.T) {
|
||||||
require.Equal(t, []byte("on update|sub update"), res.Stack[0].Value())
|
require.Equal(t, []byte("on update|sub update"), res.Stack[0].Value())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCompileExamples(t *testing.T) {
|
||||||
|
const examplePath = "../examples"
|
||||||
|
infos, err := ioutil.ReadDir(examplePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// For proper nef generation.
|
||||||
|
config.Version = "0.90.0-test"
|
||||||
|
|
||||||
|
tmpDir := os.TempDir()
|
||||||
|
|
||||||
|
e := newExecutor(t, false)
|
||||||
|
defer e.Close(t)
|
||||||
|
|
||||||
|
for _, info := range infos {
|
||||||
|
t.Run(info.Name(), func(t *testing.T) {
|
||||||
|
infos, err := ioutil.ReadDir(path.Join(examplePath, info.Name()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, len(infos) == 0, "detected smart contract folder with no contract in it")
|
||||||
|
|
||||||
|
outPath := path.Join(tmpDir, info.Name()+".nef")
|
||||||
|
manifestPath := path.Join(tmpDir, info.Name()+".manifest.json")
|
||||||
|
defer func() {
|
||||||
|
os.Remove(outPath)
|
||||||
|
os.Remove(manifestPath)
|
||||||
|
}()
|
||||||
|
|
||||||
|
cfgName := filterFilename(infos, ".yml")
|
||||||
|
opts := []string{
|
||||||
|
"neo-go", "contract", "compile",
|
||||||
|
"--in", path.Join(examplePath, info.Name()),
|
||||||
|
"--out", outPath,
|
||||||
|
"--manifest", manifestPath,
|
||||||
|
"--config", path.Join(examplePath, info.Name(), cfgName),
|
||||||
|
}
|
||||||
|
e.Run(t, opts...)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("invalid events in manifest", func(t *testing.T) {
|
||||||
|
const dir = "./testdata/"
|
||||||
|
for _, name := range []string{"invalid1", "invalid2", "invalid3"} {
|
||||||
|
outPath := path.Join(tmpDir, name+".nef")
|
||||||
|
manifestPath := path.Join(tmpDir, name+".manifest.json")
|
||||||
|
defer func() {
|
||||||
|
os.Remove(outPath)
|
||||||
|
os.Remove(manifestPath)
|
||||||
|
}()
|
||||||
|
e.RunWithError(t, "neo-go", "contract", "compile",
|
||||||
|
"--in", path.Join(dir, name),
|
||||||
|
"--out", outPath,
|
||||||
|
"--manifest", manifestPath,
|
||||||
|
"--config", path.Join(dir, name, "invalid.yml"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterFilename(infos []os.FileInfo, ext string) string {
|
||||||
|
for _, info := range infos {
|
||||||
|
if !info.IsDir() {
|
||||||
|
name := info.Name()
|
||||||
|
if strings.HasSuffix(name, ext) {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
|
@ -150,6 +150,14 @@ func NewCommands() []cli.Command {
|
||||||
Name: "config, c",
|
Name: "config, c",
|
||||||
Usage: "Configuration input file (*.yml)",
|
Usage: "Configuration input file (*.yml)",
|
||||||
},
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "no-standards",
|
||||||
|
Usage: "do not check compliance with supported standards",
|
||||||
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "no-events",
|
||||||
|
Usage: "do not check emitted events with the manifest",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -398,6 +406,9 @@ func contractCompile(ctx *cli.Context) error {
|
||||||
|
|
||||||
DebugInfo: debugFile,
|
DebugInfo: debugFile,
|
||||||
ManifestFile: manifestFile,
|
ManifestFile: manifestFile,
|
||||||
|
|
||||||
|
NoStandardCheck: ctx.Bool("no-standards"),
|
||||||
|
NoEventsCheck: ctx.Bool("no-events"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(confFile) != 0 {
|
if len(confFile) != 0 {
|
||||||
|
|
21
cli/testdata/invalid1/invalid.go
vendored
Normal file
21
cli/testdata/invalid1/invalid.go
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
// invalid is an example of contract which doesn't pass event check.
|
||||||
|
package invalid1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/interop"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Notify1 emits correctly typed event.
|
||||||
|
func Notify1() bool {
|
||||||
|
runtime.Notify("Event", interop.Hash160{1, 2, 3})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify2 emits invalid event (ByteString instead of Hash160).
|
||||||
|
func Notify2() bool {
|
||||||
|
runtime.Notify("Event", []byte{1, 2, 3})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
7
cli/testdata/invalid1/invalid.yml
vendored
Normal file
7
cli/testdata/invalid1/invalid.yml
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
name: "Invalid example"
|
||||||
|
supportedstandards: []
|
||||||
|
events:
|
||||||
|
- name: Event
|
||||||
|
parameters:
|
||||||
|
- name: address
|
||||||
|
type: Hash160
|
21
cli/testdata/invalid2/invalid.go
vendored
Normal file
21
cli/testdata/invalid2/invalid.go
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
// invalid is an example of contract which doesn't pass event check.
|
||||||
|
package invalid2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/interop"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Notify1 emits correctly typed event.
|
||||||
|
func Notify1() bool {
|
||||||
|
runtime.Notify("Event", interop.Hash160{1, 2, 3})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify2 emits invalid event (extra parameter).
|
||||||
|
func Notify2() bool {
|
||||||
|
runtime.Notify("Event", interop.Hash160{1, 2, 3}, "extra parameter")
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
7
cli/testdata/invalid2/invalid.yml
vendored
Normal file
7
cli/testdata/invalid2/invalid.yml
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
name: "Invalid example"
|
||||||
|
supportedstandards: []
|
||||||
|
events:
|
||||||
|
- name: Event
|
||||||
|
parameters:
|
||||||
|
- name: address
|
||||||
|
type: Hash160
|
21
cli/testdata/invalid3/invalid.go
vendored
Normal file
21
cli/testdata/invalid3/invalid.go
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
// invalid is an example of contract which doesn't pass event check.
|
||||||
|
package invalid3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/interop"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Notify1 emits correctly typed event.
|
||||||
|
func Notify1() bool {
|
||||||
|
runtime.Notify("Event", interop.Hash160{1, 2, 3})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify2 emits invalid event (missing from manifest).
|
||||||
|
func Notify2() bool {
|
||||||
|
runtime.Notify("AnotherEvent", interop.Hash160{1, 2, 3})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
7
cli/testdata/invalid3/invalid.yml
vendored
Normal file
7
cli/testdata/invalid3/invalid.yml
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
name: "Invalid example"
|
||||||
|
supportedstandards: []
|
||||||
|
events:
|
||||||
|
- name: Event
|
||||||
|
parameters:
|
||||||
|
- name: address
|
||||||
|
type: Hash160
|
|
@ -4,4 +4,16 @@ events:
|
||||||
- name: Tx
|
- name: Tx
|
||||||
parameters:
|
parameters:
|
||||||
- name: txHash
|
- name: txHash
|
||||||
type: ByteString
|
type: Hash256
|
||||||
|
- name: Calling
|
||||||
|
parameters:
|
||||||
|
- name: hash
|
||||||
|
type: Hash160
|
||||||
|
- name: Executing
|
||||||
|
parameters:
|
||||||
|
- name: hash
|
||||||
|
type: Hash160
|
||||||
|
- name: Entry
|
||||||
|
parameters:
|
||||||
|
- name: hash
|
||||||
|
type: Hash160
|
|
@ -13,7 +13,9 @@ func NotifyKeysAndValues() bool {
|
||||||
keys := iterator.Keys(iter)
|
keys := iterator.Keys(iter)
|
||||||
|
|
||||||
runtime.Notify("found storage values", values)
|
runtime.Notify("found storage values", values)
|
||||||
runtime.Notify("found storage keys", keys)
|
// For illustration purposes event is emitted with 'Any' type.
|
||||||
|
var typedKeys interface{} = keys
|
||||||
|
runtime.Notify("found storage keys", typedKeys)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ events:
|
||||||
- name: found storage values
|
- name: found storage values
|
||||||
parameters:
|
parameters:
|
||||||
- name: values
|
- name: values
|
||||||
type: Any
|
type: InteropInterface
|
||||||
- name: found storage keys
|
- name: found storage keys
|
||||||
parameters:
|
parameters:
|
||||||
- name: keys
|
- name: keys
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package tokensale
|
package tokensale
|
||||||
|
|
||||||
import (
|
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/runtime"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
|
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/interop/util"
|
"github.com/nspcc-dev/neo-go/pkg/interop/util"
|
||||||
|
@ -134,39 +135,39 @@ func checkOwnerWitness() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decimals returns the token decimals
|
// Decimals returns the token decimals
|
||||||
func Decimals() interface{} {
|
func Decimals() int {
|
||||||
if trigger != runtime.Application {
|
if trigger != runtime.Application {
|
||||||
return false
|
panic("invalid trigger")
|
||||||
}
|
}
|
||||||
return token.Decimals
|
return token.Decimals
|
||||||
}
|
}
|
||||||
|
|
||||||
// Symbol returns the token symbol
|
// Symbol returns the token symbol
|
||||||
func Symbol() interface{} {
|
func Symbol() string {
|
||||||
if trigger != runtime.Application {
|
if trigger != runtime.Application {
|
||||||
return false
|
panic("invalid trigger")
|
||||||
}
|
}
|
||||||
return token.Symbol
|
return token.Symbol
|
||||||
}
|
}
|
||||||
|
|
||||||
// TotalSupply returns the token total supply value
|
// TotalSupply returns the token total supply value
|
||||||
func TotalSupply() interface{} {
|
func TotalSupply() int {
|
||||||
if trigger != runtime.Application {
|
if trigger != runtime.Application {
|
||||||
return false
|
panic("invalid trigger")
|
||||||
}
|
}
|
||||||
return getIntFromDB(ctx, token.CirculationKey)
|
return getIntFromDB(ctx, token.CirculationKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BalanceOf returns the amount of token on the specified address
|
// BalanceOf returns the amount of token on the specified address
|
||||||
func BalanceOf(holder []byte) interface{} {
|
func BalanceOf(holder interop.Hash160) int {
|
||||||
if trigger != runtime.Application {
|
if trigger != runtime.Application {
|
||||||
return false
|
panic("invalid trigger")
|
||||||
}
|
}
|
||||||
return getIntFromDB(ctx, holder)
|
return getIntFromDB(ctx, holder)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transfer transfers specified amount of token from one user to another
|
// Transfer transfers specified amount of token from one user to another
|
||||||
func Transfer(from, to []byte, amount int) bool {
|
func Transfer(from, to interop.Hash160, amount int, _ interface{}) bool {
|
||||||
if trigger != runtime.Application {
|
if trigger != runtime.Application {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,11 @@
|
||||||
name: "My awesome token"
|
name: "My awesome token"
|
||||||
supportedstandards: ["NEP-17"]
|
supportedstandards: ["NEP-17"]
|
||||||
events: []
|
events:
|
||||||
|
- name: Transfer
|
||||||
|
parameters:
|
||||||
|
- name: from
|
||||||
|
type: Hash160
|
||||||
|
- name: to
|
||||||
|
type: Hash160
|
||||||
|
- name: amount
|
||||||
|
type: Integer
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package nep17
|
package nep17
|
||||||
|
|
||||||
import (
|
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/runtime"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
|
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/interop/util"
|
"github.com/nspcc-dev/neo-go/pkg/interop/util"
|
||||||
|
@ -44,7 +45,7 @@ func (t Token) BalanceOf(ctx storage.Context, holder []byte) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transfer token from one user to another
|
// Transfer token from one user to another
|
||||||
func (t Token) Transfer(ctx storage.Context, from []byte, to []byte, amount int, data interface{}) bool {
|
func (t Token) Transfer(ctx storage.Context, from, to interop.Hash160, amount int, data interface{}) bool {
|
||||||
amountFrom := t.CanTransfer(ctx, from, to, amount)
|
amountFrom := t.CanTransfer(ctx, from, to, amount)
|
||||||
if amountFrom == -1 {
|
if amountFrom == -1 {
|
||||||
return false
|
return false
|
||||||
|
@ -62,7 +63,7 @@ func (t Token) Transfer(ctx storage.Context, from []byte, to []byte, amount int,
|
||||||
amountTo := getIntFromDB(ctx, to)
|
amountTo := getIntFromDB(ctx, to)
|
||||||
totalAmountTo := amountTo + amount
|
totalAmountTo := amountTo + amount
|
||||||
storage.Put(ctx, to, totalAmountTo)
|
storage.Put(ctx, to, totalAmountTo)
|
||||||
runtime.Notify("transfer", from, to, amount)
|
runtime.Notify("Transfer", from, to, amount)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,7 +106,7 @@ func IsUsableAddress(addr []byte) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mint initial supply of tokens
|
// Mint initial supply of tokens
|
||||||
func (t Token) Mint(ctx storage.Context, to []byte) bool {
|
func (t Token) Mint(ctx storage.Context, to interop.Hash160) bool {
|
||||||
if !IsUsableAddress(t.Owner) {
|
if !IsUsableAddress(t.Owner) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -117,6 +118,7 @@ func (t Token) Mint(ctx storage.Context, to []byte) bool {
|
||||||
storage.Put(ctx, to, t.TotalSupply)
|
storage.Put(ctx, to, t.TotalSupply)
|
||||||
storage.Put(ctx, []byte("minted"), true)
|
storage.Put(ctx, []byte("minted"), true)
|
||||||
storage.Put(ctx, []byte(t.CirculationKey), t.TotalSupply)
|
storage.Put(ctx, []byte(t.CirculationKey), t.TotalSupply)
|
||||||
runtime.Notify("transfer", "", to, t.TotalSupply)
|
var from interop.Hash160
|
||||||
|
runtime.Notify("Transfer", from, to, t.TotalSupply)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@ func TotalSupply() int {
|
||||||
}
|
}
|
||||||
|
|
||||||
// BalanceOf returns the amount of token on the specified address
|
// BalanceOf returns the amount of token on the specified address
|
||||||
func BalanceOf(holder interop.Hash160) interface{} {
|
func BalanceOf(holder interop.Hash160) int {
|
||||||
return token.BalanceOf(ctx, holder)
|
return token.BalanceOf(ctx, holder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,6 +58,6 @@ func Transfer(from interop.Hash160, to interop.Hash160, amount int, data interfa
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mint initial supply of tokens
|
// Mint initial supply of tokens
|
||||||
func Mint(to []byte) bool {
|
func Mint(to interop.Hash160) bool {
|
||||||
return token.Mint(ctx, to)
|
return token.Mint(ctx, to)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,8 @@ events:
|
||||||
- name: Transfer
|
- name: Transfer
|
||||||
parameters:
|
parameters:
|
||||||
- name: from
|
- name: from
|
||||||
type: ByteString
|
type: Hash160
|
||||||
- name: to
|
- name: to
|
||||||
type: ByteString
|
type: Hash160
|
||||||
- name: amount
|
- name: amount
|
||||||
type: Integer
|
type: Integer
|
||||||
|
|
|
@ -87,6 +87,9 @@ type codegen struct {
|
||||||
// docIndex maps file path to an index in documents array.
|
// docIndex maps file path to an index in documents array.
|
||||||
docIndex map[string]int
|
docIndex map[string]int
|
||||||
|
|
||||||
|
// emittedEvents contains all events emitted by contract.
|
||||||
|
emittedEvents map[string][][]string
|
||||||
|
|
||||||
// Label table for recording jump destinations.
|
// Label table for recording jump destinations.
|
||||||
l []int
|
l []int
|
||||||
}
|
}
|
||||||
|
@ -870,6 +873,15 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
|
||||||
ast.Walk(c, n.Fun)
|
ast.Walk(c, n.Fun)
|
||||||
emit.Opcodes(c.prog.BinWriter, opcode.CALLA)
|
emit.Opcodes(c.prog.BinWriter, opcode.CALLA)
|
||||||
case isSyscall(f):
|
case isSyscall(f):
|
||||||
|
if f.pkg.Name() == "runtime" && f.name == "Notify" {
|
||||||
|
tv := c.typeAndValueOf(n.Args[0])
|
||||||
|
params := make([]string, 0, len(n.Args[1:]))
|
||||||
|
for _, p := range n.Args[1:] {
|
||||||
|
params = append(params, c.scTypeFromExpr(p))
|
||||||
|
}
|
||||||
|
name := constant.StringVal(tv.Value)
|
||||||
|
c.emittedEvents[name] = append(c.emittedEvents[name], params)
|
||||||
|
}
|
||||||
c.convertSyscall(n, f.pkg.Name(), f.name)
|
c.convertSyscall(n, f.pkg.Name(), f.name)
|
||||||
default:
|
default:
|
||||||
emit.Call(c.prog.BinWriter, opcode.CALLL, f.label)
|
emit.Call(c.prog.BinWriter, opcode.CALLL, f.label)
|
||||||
|
@ -1883,6 +1895,7 @@ func newCodegen(info *buildInfo, pkg *loader.PackageInfo) *codegen {
|
||||||
initEndOffset: -1,
|
initEndOffset: -1,
|
||||||
deployEndOffset: -1,
|
deployEndOffset: -1,
|
||||||
|
|
||||||
|
emittedEvents: make(map[string][][]string),
|
||||||
sequencePoints: make(map[string][]DebugSeqPoint),
|
sequencePoints: make(map[string][]DebugSeqPoint),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest/standard"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/nef"
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/nef"
|
||||||
"golang.org/x/tools/go/loader"
|
"golang.org/x/tools/go/loader"
|
||||||
)
|
)
|
||||||
|
@ -34,6 +35,14 @@ type Options struct {
|
||||||
// The name of the output for contract manifest file.
|
// The name of the output for contract manifest file.
|
||||||
ManifestFile string
|
ManifestFile string
|
||||||
|
|
||||||
|
// NoEventsCheck specifies if events emitted by contract needs to be present in manifest.
|
||||||
|
// This setting has effect only if manifest is emitted.
|
||||||
|
NoEventsCheck bool
|
||||||
|
|
||||||
|
// NoStandardCheck specifies if supported standards compliance needs to be checked.
|
||||||
|
// This setting has effect only if manifest is emitted.
|
||||||
|
NoStandardCheck bool
|
||||||
|
|
||||||
// Name is contract's name to be written to manifest.
|
// Name is contract's name to be written to manifest.
|
||||||
Name string
|
Name string
|
||||||
|
|
||||||
|
@ -214,6 +223,33 @@ func CompileAndSave(src string, o *Options) ([]byte, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return b, fmt.Errorf("failed to convert debug info to manifest: %w", err)
|
return b, fmt.Errorf("failed to convert debug info to manifest: %w", err)
|
||||||
}
|
}
|
||||||
|
if !o.NoStandardCheck {
|
||||||
|
if err := standard.Check(m, o.ContractSupportedStandards...); err != nil {
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !o.NoEventsCheck {
|
||||||
|
for name := range di.EmittedEvents {
|
||||||
|
ev := m.ABI.GetEvent(name)
|
||||||
|
if ev == nil {
|
||||||
|
return nil, fmt.Errorf("event '%s' is emitted but not specified in manifest", name)
|
||||||
|
}
|
||||||
|
argsList := di.EmittedEvents[name]
|
||||||
|
for i := range argsList {
|
||||||
|
if len(argsList[i]) != len(ev.Parameters) {
|
||||||
|
return nil, fmt.Errorf("event '%s' should have %d parameters but has %d",
|
||||||
|
name, len(ev.Parameters), len(argsList[i]))
|
||||||
|
}
|
||||||
|
for j := range ev.Parameters {
|
||||||
|
expected := ev.Parameters[j].Type.String()
|
||||||
|
if argsList[i][j] != expected {
|
||||||
|
return nil, fmt.Errorf("event '%s' should have '%s' as type of %d parameter, "+
|
||||||
|
"got: %s", name, expected, j+1, argsList[i][j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
mData, err := json.Marshal(m)
|
mData, err := json.Marshal(m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return b, fmt.Errorf("failed to marshal manifest to JSON: %w", err)
|
return b, fmt.Errorf("failed to marshal manifest to JSON: %w", err)
|
||||||
|
|
|
@ -24,6 +24,8 @@ type DebugInfo struct {
|
||||||
Documents []string `json:"documents"`
|
Documents []string `json:"documents"`
|
||||||
Methods []MethodDebugInfo `json:"methods"`
|
Methods []MethodDebugInfo `json:"methods"`
|
||||||
Events []EventDebugInfo `json:"events"`
|
Events []EventDebugInfo `json:"events"`
|
||||||
|
// EmittedEvents contains events occuring in code.
|
||||||
|
EmittedEvents map[string][][]string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MethodDebugInfo represents smart-contract's method debug information.
|
// MethodDebugInfo represents smart-contract's method debug information.
|
||||||
|
@ -162,6 +164,7 @@ func (c *codegen) emitDebugInfo(contract []byte) *DebugInfo {
|
||||||
}
|
}
|
||||||
d.Methods = append(d.Methods, *m)
|
d.Methods = append(d.Methods, *m)
|
||||||
}
|
}
|
||||||
|
d.EmittedEvents = c.emittedEvents
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -89,6 +89,16 @@ func (a *ABI) GetMethod(name string) *Method {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetEvent returns event with the specified name.
|
||||||
|
func (a *ABI) GetEvent(name string) *Event {
|
||||||
|
for i := range a.Events {
|
||||||
|
if a.Events[i].Name == name {
|
||||||
|
return &a.Events[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// CanCall returns true is current contract is allowed to call
|
// CanCall returns true is current contract is allowed to call
|
||||||
// method of another contract.
|
// method of another contract.
|
||||||
func (m *Manifest) CanCall(toCall *Manifest, method string) bool {
|
func (m *Manifest) CanCall(toCall *Manifest, method string) bool {
|
||||||
|
|
76
pkg/smartcontract/manifest/standard/comply.go
Normal file
76
pkg/smartcontract/manifest/standard/comply.go
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package standard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Various validation errors.
|
||||||
|
var (
|
||||||
|
ErrMethodMissing = errors.New("method missing")
|
||||||
|
ErrEventMissing = errors.New("event missing")
|
||||||
|
ErrInvalidReturnType = errors.New("invalid return type")
|
||||||
|
ErrInvalidParameterCount = errors.New("invalid parameter count")
|
||||||
|
ErrInvalidParameterType = errors.New("invalid parameter type")
|
||||||
|
)
|
||||||
|
|
||||||
|
var checks = map[string]*manifest.Manifest{
|
||||||
|
manifest.NEP17StandardName: nep17,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check checks if manifest complies with all provided standards.
|
||||||
|
// Currently only NEP-17 is supported.
|
||||||
|
func Check(m *manifest.Manifest, standards ...string) error {
|
||||||
|
for i := range standards {
|
||||||
|
s, ok := checks[standards[i]]
|
||||||
|
if ok {
|
||||||
|
if err := Comply(m, s); err != nil {
|
||||||
|
return fmt.Errorf("manifest is not compliant with '%s': %w", standards[i], err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comply if m has all methods and event from st manifest and they have the same signature.
|
||||||
|
// Parameter names are ignored.
|
||||||
|
func Comply(m, st *manifest.Manifest) error {
|
||||||
|
for _, stm := range st.ABI.Methods {
|
||||||
|
name := stm.Name
|
||||||
|
md := m.ABI.GetMethod(name)
|
||||||
|
if md == nil {
|
||||||
|
return fmt.Errorf("%w: '%s'", ErrMethodMissing, name)
|
||||||
|
} else if stm.ReturnType != md.ReturnType {
|
||||||
|
return fmt.Errorf("%w: '%s' (expected %s, got %s)", ErrInvalidReturnType,
|
||||||
|
name, stm.ReturnType, md.ReturnType)
|
||||||
|
} else if len(stm.Parameters) != len(md.Parameters) {
|
||||||
|
return fmt.Errorf("%w: '%s' (expected %d, got %d)", ErrInvalidParameterCount,
|
||||||
|
name, len(stm.Parameters), len(md.Parameters))
|
||||||
|
}
|
||||||
|
for i := range stm.Parameters {
|
||||||
|
if stm.Parameters[i].Type != md.Parameters[i].Type {
|
||||||
|
return fmt.Errorf("%w: '%s'[%d] (expected %s, got %s)", ErrInvalidParameterType,
|
||||||
|
name, i, stm.Parameters[i].Type, md.Parameters[i].Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ste := range st.ABI.Events {
|
||||||
|
name := ste.Name
|
||||||
|
ed := m.ABI.GetEvent(name)
|
||||||
|
if ed == nil {
|
||||||
|
return fmt.Errorf("%w: event '%s'", ErrEventMissing, name)
|
||||||
|
} else if len(ste.Parameters) != len(ed.Parameters) {
|
||||||
|
return fmt.Errorf("%w: event '%s' (expected %d, got %d)", ErrInvalidParameterCount,
|
||||||
|
name, len(ste.Parameters), len(ed.Parameters))
|
||||||
|
}
|
||||||
|
for i := range ste.Parameters {
|
||||||
|
if ste.Parameters[i].Type != ed.Parameters[i].Type {
|
||||||
|
return fmt.Errorf("%w: event '%s' (expected %s, got %s)", ErrInvalidParameterType,
|
||||||
|
name, ste.Parameters[i].Type, ed.Parameters[i].Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
113
pkg/smartcontract/manifest/standard/comply_test.go
Normal file
113
pkg/smartcontract/manifest/standard/comply_test.go
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
package standard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fooMethodBarEvent() *manifest.Manifest {
|
||||||
|
return &manifest.Manifest{
|
||||||
|
ABI: manifest.ABI{
|
||||||
|
Methods: []manifest.Method{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Parameters: []manifest.Parameter{
|
||||||
|
{Type: smartcontract.ByteArrayType},
|
||||||
|
{Type: smartcontract.PublicKeyType},
|
||||||
|
},
|
||||||
|
ReturnType: smartcontract.IntegerType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Events: []manifest.Event{
|
||||||
|
{
|
||||||
|
Name: "bar",
|
||||||
|
Parameters: []manifest.Parameter{
|
||||||
|
{Type: smartcontract.StringType},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComplyMissingMethod(t *testing.T) {
|
||||||
|
m := fooMethodBarEvent()
|
||||||
|
m.ABI.GetMethod("foo").Name = "notafoo"
|
||||||
|
err := Comply(m, fooMethodBarEvent())
|
||||||
|
require.True(t, errors.Is(err, ErrMethodMissing))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComplyInvalidReturnType(t *testing.T) {
|
||||||
|
m := fooMethodBarEvent()
|
||||||
|
m.ABI.GetMethod("foo").ReturnType = smartcontract.VoidType
|
||||||
|
err := Comply(m, fooMethodBarEvent())
|
||||||
|
require.True(t, errors.Is(err, ErrInvalidReturnType))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComplyMethodParameterCount(t *testing.T) {
|
||||||
|
t.Run("Method", func(t *testing.T) {
|
||||||
|
m := fooMethodBarEvent()
|
||||||
|
f := m.ABI.GetMethod("foo")
|
||||||
|
f.Parameters = append(f.Parameters, manifest.Parameter{Type: smartcontract.BoolType})
|
||||||
|
err := Comply(m, fooMethodBarEvent())
|
||||||
|
require.True(t, errors.Is(err, ErrInvalidParameterCount))
|
||||||
|
})
|
||||||
|
t.Run("Event", func(t *testing.T) {
|
||||||
|
m := fooMethodBarEvent()
|
||||||
|
ev := m.ABI.GetEvent("bar")
|
||||||
|
ev.Parameters = append(ev.Parameters[:0])
|
||||||
|
err := Comply(m, fooMethodBarEvent())
|
||||||
|
require.True(t, errors.Is(err, ErrInvalidParameterCount))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComplyParameterType(t *testing.T) {
|
||||||
|
t.Run("Method", func(t *testing.T) {
|
||||||
|
m := fooMethodBarEvent()
|
||||||
|
m.ABI.GetMethod("foo").Parameters[0].Type = smartcontract.InteropInterfaceType
|
||||||
|
err := Comply(m, fooMethodBarEvent())
|
||||||
|
require.True(t, errors.Is(err, ErrInvalidParameterType))
|
||||||
|
})
|
||||||
|
t.Run("Event", func(t *testing.T) {
|
||||||
|
m := fooMethodBarEvent()
|
||||||
|
m.ABI.GetEvent("bar").Parameters[0].Type = smartcontract.InteropInterfaceType
|
||||||
|
err := Comply(m, fooMethodBarEvent())
|
||||||
|
require.True(t, errors.Is(err, ErrInvalidParameterType))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMissingEvent(t *testing.T) {
|
||||||
|
m := fooMethodBarEvent()
|
||||||
|
m.ABI.GetEvent("bar").Name = "notabar"
|
||||||
|
err := Comply(m, fooMethodBarEvent())
|
||||||
|
require.True(t, errors.Is(err, ErrEventMissing))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComplyValid(t *testing.T) {
|
||||||
|
m := fooMethodBarEvent()
|
||||||
|
m.ABI.Methods = append(m.ABI.Methods, manifest.Method{
|
||||||
|
Name: "newmethod",
|
||||||
|
Offset: 123,
|
||||||
|
ReturnType: smartcontract.ByteArrayType,
|
||||||
|
})
|
||||||
|
m.ABI.Events = append(m.ABI.Events, manifest.Event{
|
||||||
|
Name: "otherevent",
|
||||||
|
Parameters: []manifest.Parameter{{
|
||||||
|
Name: "names do not matter",
|
||||||
|
Type: smartcontract.IntegerType,
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
require.NoError(t, Comply(m, fooMethodBarEvent()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheck(t *testing.T) {
|
||||||
|
m := manifest.NewManifest(util.Uint160{}, "Test")
|
||||||
|
require.Error(t, Check(m, manifest.NEP17StandardName))
|
||||||
|
|
||||||
|
require.NoError(t, Check(nep17, manifest.NEP17StandardName))
|
||||||
|
}
|
5
pkg/smartcontract/manifest/standard/doc.go
Normal file
5
pkg/smartcontract/manifest/standard/doc.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
/*
|
||||||
|
Package standard contains interfaces for well-defined standards
|
||||||
|
and function for checking if arbitrary manifest complies with them.
|
||||||
|
*/
|
||||||
|
package standard
|
57
pkg/smartcontract/manifest/standard/nep17.go
Normal file
57
pkg/smartcontract/manifest/standard/nep17.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package standard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
||||||
|
)
|
||||||
|
|
||||||
|
var nep17 = &manifest.Manifest{
|
||||||
|
ABI: manifest.ABI{
|
||||||
|
Methods: []manifest.Method{
|
||||||
|
{
|
||||||
|
Name: "balanceOf",
|
||||||
|
Parameters: []manifest.Parameter{
|
||||||
|
{Type: smartcontract.Hash160Type},
|
||||||
|
},
|
||||||
|
ReturnType: smartcontract.IntegerType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "decimals",
|
||||||
|
ReturnType: smartcontract.IntegerType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "symbol",
|
||||||
|
ReturnType: smartcontract.StringType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "totalSupply",
|
||||||
|
ReturnType: smartcontract.IntegerType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "transfer",
|
||||||
|
Parameters: []manifest.Parameter{
|
||||||
|
{Type: smartcontract.Hash160Type},
|
||||||
|
{Type: smartcontract.Hash160Type},
|
||||||
|
{Type: smartcontract.IntegerType},
|
||||||
|
{Type: smartcontract.AnyType},
|
||||||
|
},
|
||||||
|
ReturnType: smartcontract.BoolType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Events: []manifest.Event{
|
||||||
|
{
|
||||||
|
Name: "Transfer",
|
||||||
|
Parameters: []manifest.Parameter{
|
||||||
|
{Type: smartcontract.Hash160Type},
|
||||||
|
{Type: smartcontract.Hash160Type},
|
||||||
|
{Type: smartcontract.IntegerType},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNEP17 checks if m is NEP-17 compliant.
|
||||||
|
func IsNEP17(m *manifest.Manifest) error {
|
||||||
|
return Comply(m, nep17)
|
||||||
|
}
|
Loading…
Reference in a new issue