diff --git a/cli/contract_test.go b/cli/contract_test.go index 002274a9f..7781f603a 100644 --- a/cli/contract_test.go +++ b/cli/contract_test.go @@ -104,3 +104,72 @@ func TestComlileAndInvokeFunction(t *testing.T) { 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 "" +} diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 4b92ca465..0e35439cb 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -150,6 +150,14 @@ func NewCommands() []cli.Command { Name: "config, c", 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, ManifestFile: manifestFile, + + NoStandardCheck: ctx.Bool("no-standards"), + NoEventsCheck: ctx.Bool("no-events"), } if len(confFile) != 0 { diff --git a/cli/testdata/invalid1/invalid.go b/cli/testdata/invalid1/invalid.go new file mode 100644 index 000000000..634971c15 --- /dev/null +++ b/cli/testdata/invalid1/invalid.go @@ -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 +} diff --git a/cli/testdata/invalid1/invalid.yml b/cli/testdata/invalid1/invalid.yml new file mode 100644 index 000000000..73839caba --- /dev/null +++ b/cli/testdata/invalid1/invalid.yml @@ -0,0 +1,7 @@ +name: "Invalid example" +supportedstandards: [] +events: + - name: Event + parameters: + - name: address + type: Hash160 diff --git a/cli/testdata/invalid2/invalid.go b/cli/testdata/invalid2/invalid.go new file mode 100644 index 000000000..3af8a53ce --- /dev/null +++ b/cli/testdata/invalid2/invalid.go @@ -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 +} diff --git a/cli/testdata/invalid2/invalid.yml b/cli/testdata/invalid2/invalid.yml new file mode 100644 index 000000000..73839caba --- /dev/null +++ b/cli/testdata/invalid2/invalid.yml @@ -0,0 +1,7 @@ +name: "Invalid example" +supportedstandards: [] +events: + - name: Event + parameters: + - name: address + type: Hash160 diff --git a/cli/testdata/invalid3/invalid.go b/cli/testdata/invalid3/invalid.go new file mode 100644 index 000000000..ea8cd7618 --- /dev/null +++ b/cli/testdata/invalid3/invalid.go @@ -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 +} diff --git a/cli/testdata/invalid3/invalid.yml b/cli/testdata/invalid3/invalid.yml new file mode 100644 index 000000000..73839caba --- /dev/null +++ b/cli/testdata/invalid3/invalid.yml @@ -0,0 +1,7 @@ +name: "Invalid example" +supportedstandards: [] +events: + - name: Event + parameters: + - name: address + type: Hash160 diff --git a/examples/engine/engine.yml b/examples/engine/engine.yml index 4b2d24672..df94d5a01 100644 --- a/examples/engine/engine.yml +++ b/examples/engine/engine.yml @@ -4,4 +4,16 @@ events: - name: Tx parameters: - 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 \ No newline at end of file diff --git a/examples/iterator/iterator.go b/examples/iterator/iterator.go index fd5a0ad16..534bd0885 100644 --- a/examples/iterator/iterator.go +++ b/examples/iterator/iterator.go @@ -13,7 +13,9 @@ func NotifyKeysAndValues() bool { keys := iterator.Keys(iter) 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 } diff --git a/examples/iterator/iterator.yml b/examples/iterator/iterator.yml index ab4788d0a..e714e9d8a 100644 --- a/examples/iterator/iterator.yml +++ b/examples/iterator/iterator.yml @@ -4,7 +4,7 @@ events: - name: found storage values parameters: - name: values - type: Any + type: InteropInterface - name: found storage keys parameters: - name: keys diff --git a/examples/token-sale/token_sale.go b/examples/token-sale/token_sale.go index e48510516..d75cf32ad 100644 --- a/examples/token-sale/token_sale.go +++ b/examples/token-sale/token_sale.go @@ -1,6 +1,7 @@ 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" @@ -134,39 +135,39 @@ func checkOwnerWitness() bool { } // Decimals returns the token decimals -func Decimals() interface{} { +func Decimals() int { if trigger != runtime.Application { - return false + panic("invalid trigger") } return token.Decimals } // Symbol returns the token symbol -func Symbol() interface{} { +func Symbol() string { if trigger != runtime.Application { - return false + panic("invalid trigger") } return token.Symbol } // TotalSupply returns the token total supply value -func TotalSupply() interface{} { +func TotalSupply() int { if trigger != runtime.Application { - return false + panic("invalid trigger") } return getIntFromDB(ctx, token.CirculationKey) } // 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 { - return false + panic("invalid trigger") } return getIntFromDB(ctx, holder) } // 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 { return false } diff --git a/examples/token-sale/token_sale.yml b/examples/token-sale/token_sale.yml index 2d6181dca..bda6ba182 100644 --- a/examples/token-sale/token_sale.yml +++ b/examples/token-sale/token_sale.yml @@ -1,3 +1,11 @@ name: "My awesome token" supportedstandards: ["NEP-17"] -events: [] +events: +- name: Transfer + parameters: + - name: from + type: Hash160 + - name: to + type: Hash160 + - name: amount + type: Integer diff --git a/examples/token/nep17/nep17.go b/examples/token/nep17/nep17.go index fd07567f5..a962d19d3 100644 --- a/examples/token/nep17/nep17.go +++ b/examples/token/nep17/nep17.go @@ -1,6 +1,7 @@ package nep17 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" @@ -44,7 +45,7 @@ func (t Token) BalanceOf(ctx storage.Context, holder []byte) int { } // 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) if amountFrom == -1 { return false @@ -62,7 +63,7 @@ func (t Token) Transfer(ctx storage.Context, from []byte, to []byte, amount int, amountTo := getIntFromDB(ctx, to) totalAmountTo := amountTo + amount storage.Put(ctx, to, totalAmountTo) - runtime.Notify("transfer", from, to, amount) + runtime.Notify("Transfer", from, to, amount) return true } @@ -105,7 +106,7 @@ func IsUsableAddress(addr []byte) bool { } // 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) { 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, []byte("minted"), true) 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 } diff --git a/examples/token/token.go b/examples/token/token.go index 3252964ad..f0a7eb09c 100644 --- a/examples/token/token.go +++ b/examples/token/token.go @@ -48,7 +48,7 @@ func TotalSupply() int { } // 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) } @@ -58,6 +58,6 @@ func Transfer(from interop.Hash160, to interop.Hash160, amount int, data interfa } // Mint initial supply of tokens -func Mint(to []byte) bool { +func Mint(to interop.Hash160) bool { return token.Mint(ctx, to) } diff --git a/examples/token/token.yml b/examples/token/token.yml index cea35644b..32fd428b5 100644 --- a/examples/token/token.yml +++ b/examples/token/token.yml @@ -4,8 +4,8 @@ events: - name: Transfer parameters: - name: from - type: ByteString + type: Hash160 - name: to - type: ByteString + type: Hash160 - name: amount type: Integer diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index 764830d7e..daf288664 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -87,6 +87,9 @@ type codegen struct { // docIndex maps file path to an index in documents array. docIndex map[string]int + // emittedEvents contains all events emitted by contract. + emittedEvents map[string][][]string + // Label table for recording jump destinations. l []int } @@ -870,6 +873,15 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { ast.Walk(c, n.Fun) emit.Opcodes(c.prog.BinWriter, opcode.CALLA) 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) default: emit.Call(c.prog.BinWriter, opcode.CALLL, f.label) @@ -1883,6 +1895,7 @@ func newCodegen(info *buildInfo, pkg *loader.PackageInfo) *codegen { initEndOffset: -1, deployEndOffset: -1, + emittedEvents: make(map[string][][]string), sequencePoints: make(map[string][]DebugSeqPoint), } } diff --git a/pkg/compiler/compiler.go b/pkg/compiler/compiler.go index cc7ce5f7f..41fe8d0c8 100644 --- a/pkg/compiler/compiler.go +++ b/pkg/compiler/compiler.go @@ -14,6 +14,7 @@ import ( "strings" "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" "golang.org/x/tools/go/loader" ) @@ -34,6 +35,14 @@ type Options struct { // The name of the output for contract manifest file. 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 string @@ -214,6 +223,33 @@ func CompileAndSave(src string, o *Options) ([]byte, error) { if err != nil { 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) if err != nil { return b, fmt.Errorf("failed to marshal manifest to JSON: %w", err) diff --git a/pkg/compiler/debug.go b/pkg/compiler/debug.go index 02181a665..88e6bcc9a 100644 --- a/pkg/compiler/debug.go +++ b/pkg/compiler/debug.go @@ -24,6 +24,8 @@ type DebugInfo struct { Documents []string `json:"documents"` Methods []MethodDebugInfo `json:"methods"` Events []EventDebugInfo `json:"events"` + // EmittedEvents contains events occuring in code. + EmittedEvents map[string][][]string `json:"-"` } // 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.EmittedEvents = c.emittedEvents return d } diff --git a/pkg/smartcontract/manifest/manifest.go b/pkg/smartcontract/manifest/manifest.go index 89de8dbd4..f4f8ad636 100644 --- a/pkg/smartcontract/manifest/manifest.go +++ b/pkg/smartcontract/manifest/manifest.go @@ -89,6 +89,16 @@ func (a *ABI) GetMethod(name string) *Method { 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 // method of another contract. func (m *Manifest) CanCall(toCall *Manifest, method string) bool { diff --git a/pkg/smartcontract/manifest/standard/comply.go b/pkg/smartcontract/manifest/standard/comply.go new file mode 100644 index 000000000..afd6e4000 --- /dev/null +++ b/pkg/smartcontract/manifest/standard/comply.go @@ -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 +} diff --git a/pkg/smartcontract/manifest/standard/comply_test.go b/pkg/smartcontract/manifest/standard/comply_test.go new file mode 100644 index 000000000..1fd5b8caf --- /dev/null +++ b/pkg/smartcontract/manifest/standard/comply_test.go @@ -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)) +} diff --git a/pkg/smartcontract/manifest/standard/doc.go b/pkg/smartcontract/manifest/standard/doc.go new file mode 100644 index 000000000..9b22b93ac --- /dev/null +++ b/pkg/smartcontract/manifest/standard/doc.go @@ -0,0 +1,5 @@ +/* +Package standard contains interfaces for well-defined standards +and function for checking if arbitrary manifest complies with them. +*/ +package standard diff --git a/pkg/smartcontract/manifest/standard/nep17.go b/pkg/smartcontract/manifest/standard/nep17.go new file mode 100644 index 000000000..ff76c4199 --- /dev/null +++ b/pkg/smartcontract/manifest/standard/nep17.go @@ -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) +}