diff --git a/cli/smartcontract/generate_test.go b/cli/smartcontract/generate_test.go index be3cd313e..d5674e5b0 100644 --- a/cli/smartcontract/generate_test.go +++ b/cli/smartcontract/generate_test.go @@ -437,12 +437,12 @@ func TestAssistedRPCBindings(t *testing.T) { } for _, hasDefinedHash := range []bool{true, false} { - checkBinding(filepath.Join("testdata", "types"), hasDefinedHash, false) - checkBinding(filepath.Join("testdata", "structs"), hasDefinedHash, false) + checkBinding(filepath.Join("testdata", "rpcbindings", "types"), hasDefinedHash, false) + checkBinding(filepath.Join("testdata", "rpcbindings", "structs"), hasDefinedHash, false) } - checkBinding(filepath.Join("testdata", "notifications"), true, false) - checkBinding(filepath.Join("testdata", "notifications"), true, false, "_extended") - checkBinding(filepath.Join("testdata", "notifications"), true, true, "_guessed") + checkBinding(filepath.Join("testdata", "rpcbindings", "notifications"), true, false) + checkBinding(filepath.Join("testdata", "rpcbindings", "notifications"), true, false, "_extended") + checkBinding(filepath.Join("testdata", "rpcbindings", "notifications"), true, true, "_guessed") require.False(t, rewriteExpectedOutputs) } @@ -540,10 +540,10 @@ func TestCompile_GuessEventTypes(t *testing.T) { } t.Run("not declared in manifest", func(t *testing.T) { - check(t, filepath.Join("testdata", "invalid5"), "inconsistent usages of event `Non declared event`: not declared in the contract config") + check(t, filepath.Join("testdata", "rpcbindings", "invalid1"), "inconsistent usages of event `Non declared event`: not declared in the contract config") }) t.Run("invalid number of params", func(t *testing.T) { - check(t, filepath.Join("testdata", "invalid6"), "inconsistent usages of event `SomeEvent` against config: number of params mismatch: 2 vs 1") + check(t, filepath.Join("testdata", "rpcbindings", "invalid2"), "inconsistent usages of event `SomeEvent` against config: number of params mismatch: 2 vs 1") }) /* // TODO: this on is a controversial one. If event information is provided in the config file, then conversion code @@ -552,13 +552,61 @@ func TestCompile_GuessEventTypes(t *testing.T) { // Thus, this testcase is always failing (no compilation error occures). // Question: do we want to compare `RealType` of the emitted parameter with the one expected in the manifest? t.Run("SC parameter type mismatch", func(t *testing.T) { - check(t, filepath.Join("testdata", "invalid7"), "inconsistent usages of event `SomeEvent` against config: number of params mismatch: 2 vs 1") + check(t, filepath.Join("testdata", "rpcbindings", "invalid3"), "inconsistent usages of event `SomeEvent` against config: number of params mismatch: 2 vs 1") }) */ t.Run("extended types mismatch", func(t *testing.T) { - check(t, filepath.Join("testdata", "invalid8"), "inconsistent usages of event `SomeEvent`: extended type of param #0 mismatch") + check(t, filepath.Join("testdata", "rpcbindings", "invalid4"), "inconsistent usages of event `SomeEvent`: extended type of param #0 mismatch") }) t.Run("named types redeclare", func(t *testing.T) { - check(t, filepath.Join("testdata", "invalid9"), "configured declared named type intersects with the contract's one: `invalid9.NamedStruct`") + check(t, filepath.Join("testdata", "rpcbindings", "invalid5"), "configured declared named type intersects with the contract's one: `invalid5.NamedStruct`") + }) +} + +func TestGenerateRPCBindings_Errors(t *testing.T) { + app := cli.NewApp() + app.Commands = NewCommands() + app.ExitErrHandler = func(*cli.Context, error) {} + + t.Run("duplicating resulting fields", func(t *testing.T) { + check := func(t *testing.T, packageName string, autogen bool, expectedError string) { + tmpDir := t.TempDir() + source := filepath.Join("testdata", "rpcbindings", packageName) + configFile := filepath.Join(source, "invalid.yml") + out := filepath.Join(tmpDir, "rpcbindings.out") + manifestF := filepath.Join(tmpDir, "manifest.json") + bindingF := filepath.Join(tmpDir, "binding.yml") + nefF := filepath.Join(tmpDir, "out.nef") + cmd := []string{"", "contract", "compile", + "--in", source, + "--config", configFile, + "--manifest", manifestF, + "--bindings", bindingF, + "--out", nefF, + } + if autogen { + cmd = append(cmd, "--guess-eventtypes") + } + require.NoError(t, app.Run(cmd)) + + cmds := []string{"", "contract", "generate-rpcwrapper", + "--config", bindingF, + "--manifest", manifestF, + "--out", out, + } + err := app.Run(cmds) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), expectedError), err.Error()) + } + + t.Run("event", func(t *testing.T) { + check(t, "invalid6", false, "error during generation: named type `SomeStruct` has two fields with identical resulting binding name `Field`") + }) + t.Run("autogen event", func(t *testing.T) { + check(t, "invalid7", true, "error during generation: named type `invalid7.SomeStruct` has two fields with identical resulting binding name `Field`") + }) + t.Run("struct", func(t *testing.T) { + check(t, "invalid8", false, "error during generation: named type `invalid8.SomeStruct` has two fields with identical resulting binding name `Field`") + }) }) } diff --git a/cli/smartcontract/testdata/invalid5/invalid.go b/cli/smartcontract/testdata/rpcbindings/invalid1/invalid.go similarity index 86% rename from cli/smartcontract/testdata/invalid5/invalid.go rename to cli/smartcontract/testdata/rpcbindings/invalid1/invalid.go index 0cae2ed71..821a7d8db 100644 --- a/cli/smartcontract/testdata/invalid5/invalid.go +++ b/cli/smartcontract/testdata/rpcbindings/invalid1/invalid.go @@ -1,4 +1,4 @@ -package invalid5 +package invalid1 import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" diff --git a/cli/smartcontract/testdata/invalid5/invalid.yml b/cli/smartcontract/testdata/rpcbindings/invalid1/invalid.yml similarity index 100% rename from cli/smartcontract/testdata/invalid5/invalid.yml rename to cli/smartcontract/testdata/rpcbindings/invalid1/invalid.yml diff --git a/cli/smartcontract/testdata/invalid6/invalid.go b/cli/smartcontract/testdata/rpcbindings/invalid2/invalid.go similarity index 87% rename from cli/smartcontract/testdata/invalid6/invalid.go rename to cli/smartcontract/testdata/rpcbindings/invalid2/invalid.go index dd3a3ecdd..6aa771c26 100644 --- a/cli/smartcontract/testdata/invalid6/invalid.go +++ b/cli/smartcontract/testdata/rpcbindings/invalid2/invalid.go @@ -1,4 +1,4 @@ -package invalid6 +package invalid2 import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" diff --git a/cli/smartcontract/testdata/invalid6/invalid.yml b/cli/smartcontract/testdata/rpcbindings/invalid2/invalid.yml similarity index 100% rename from cli/smartcontract/testdata/invalid6/invalid.yml rename to cli/smartcontract/testdata/rpcbindings/invalid2/invalid.yml diff --git a/cli/smartcontract/testdata/invalid7/invalid.go b/cli/smartcontract/testdata/rpcbindings/invalid3/invalid.go similarity index 86% rename from cli/smartcontract/testdata/invalid7/invalid.go rename to cli/smartcontract/testdata/rpcbindings/invalid3/invalid.go index 41bc20c47..7f9298f6f 100644 --- a/cli/smartcontract/testdata/invalid7/invalid.go +++ b/cli/smartcontract/testdata/rpcbindings/invalid3/invalid.go @@ -1,4 +1,4 @@ -package invalid7 +package invalid3 import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" diff --git a/cli/smartcontract/testdata/invalid7/invalid.yml b/cli/smartcontract/testdata/rpcbindings/invalid3/invalid.yml similarity index 100% rename from cli/smartcontract/testdata/invalid7/invalid.yml rename to cli/smartcontract/testdata/rpcbindings/invalid3/invalid.yml diff --git a/cli/smartcontract/testdata/invalid8/invalid.go b/cli/smartcontract/testdata/rpcbindings/invalid4/invalid.go similarity index 95% rename from cli/smartcontract/testdata/invalid8/invalid.go rename to cli/smartcontract/testdata/rpcbindings/invalid4/invalid.go index dba9173ca..70f887be1 100644 --- a/cli/smartcontract/testdata/invalid8/invalid.go +++ b/cli/smartcontract/testdata/rpcbindings/invalid4/invalid.go @@ -1,4 +1,4 @@ -package invalid8 +package invalid4 import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" diff --git a/cli/smartcontract/testdata/invalid8/invalid.yml b/cli/smartcontract/testdata/rpcbindings/invalid4/invalid.yml similarity index 100% rename from cli/smartcontract/testdata/invalid8/invalid.yml rename to cli/smartcontract/testdata/rpcbindings/invalid4/invalid.yml diff --git a/cli/smartcontract/testdata/invalid9/invalid.go b/cli/smartcontract/testdata/rpcbindings/invalid5/invalid.go similarity index 92% rename from cli/smartcontract/testdata/invalid9/invalid.go rename to cli/smartcontract/testdata/rpcbindings/invalid5/invalid.go index 5036bca9f..3d31565ca 100644 --- a/cli/smartcontract/testdata/invalid9/invalid.go +++ b/cli/smartcontract/testdata/rpcbindings/invalid5/invalid.go @@ -1,4 +1,4 @@ -package invalid9 +package invalid5 import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" diff --git a/cli/smartcontract/testdata/invalid9/invalid.yml b/cli/smartcontract/testdata/rpcbindings/invalid5/invalid.yml similarity index 72% rename from cli/smartcontract/testdata/invalid9/invalid.yml rename to cli/smartcontract/testdata/rpcbindings/invalid5/invalid.yml index 40bbf66db..ccd05f4c2 100644 --- a/cli/smartcontract/testdata/invalid9/invalid.yml +++ b/cli/smartcontract/testdata/rpcbindings/invalid5/invalid.yml @@ -6,11 +6,11 @@ events: type: Array extendedtype: base: Array - name: invalid9.NamedStruct + name: invalid5.NamedStruct namedtypes: - invalid9.NamedStruct: + invalid5.NamedStruct: base: Array - name: invalid9.NamedStruct + name: invalid5.NamedStruct fields: - field: SomeInt base: Integer diff --git a/cli/smartcontract/testdata/rpcbindings/invalid6/invalid.go b/cli/smartcontract/testdata/rpcbindings/invalid6/invalid.go new file mode 100644 index 000000000..2b210e733 --- /dev/null +++ b/cli/smartcontract/testdata/rpcbindings/invalid6/invalid.go @@ -0,0 +1,14 @@ +package invalid6 + +import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + +type SomeStruct struct { + Field int + // RPC binding generator will convert this field into exported, which matches + // exactly the existing Field. + field int +} + +func Main() { + runtime.Notify("SomeEvent", SomeStruct{Field: 123, field: 123}) +} diff --git a/cli/smartcontract/testdata/rpcbindings/invalid6/invalid.yml b/cli/smartcontract/testdata/rpcbindings/invalid6/invalid.yml new file mode 100644 index 000000000..c9fe64b23 --- /dev/null +++ b/cli/smartcontract/testdata/rpcbindings/invalid6/invalid.yml @@ -0,0 +1,18 @@ +name: Test duplicating event fields +events: + - name: SomeEvent + parameters: + - name: p1 + type: Struct + extendedtype: + base: Struct + name: SomeStruct +namedtypes: + SomeStruct: + base: Struct + name: SomeStruct + fields: + - field: Field + base: Integer + - field: field + base: Integer diff --git a/cli/smartcontract/testdata/rpcbindings/invalid7/invalid.go b/cli/smartcontract/testdata/rpcbindings/invalid7/invalid.go new file mode 100644 index 000000000..6cb9e6a68 --- /dev/null +++ b/cli/smartcontract/testdata/rpcbindings/invalid7/invalid.go @@ -0,0 +1,14 @@ +package invalid7 + +import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + +type SomeStruct struct { + Field int + // RPC binding generator will convert this field into exported, which matches + // exactly the existing Field. + field int +} + +func Main() { + runtime.Notify("SomeEvent", SomeStruct{Field: 123, field: 123}) +} diff --git a/cli/smartcontract/testdata/rpcbindings/invalid7/invalid.yml b/cli/smartcontract/testdata/rpcbindings/invalid7/invalid.yml new file mode 100644 index 000000000..cbc8c5674 --- /dev/null +++ b/cli/smartcontract/testdata/rpcbindings/invalid7/invalid.yml @@ -0,0 +1,6 @@ +name: Test duplicating autogenerated event fields +events: + - name: SomeEvent + parameters: + - name: p1 + type: Struct diff --git a/cli/smartcontract/testdata/rpcbindings/invalid8/invalid.go b/cli/smartcontract/testdata/rpcbindings/invalid8/invalid.go new file mode 100644 index 000000000..db9652324 --- /dev/null +++ b/cli/smartcontract/testdata/rpcbindings/invalid8/invalid.go @@ -0,0 +1,16 @@ +package invalid8 + +type SomeStruct struct { + Field int + // RPC binding generator will convert this field into exported, which matches + // exactly the existing Field. + field int +} + +func Main() SomeStruct { + s := SomeStruct{ + Field: 1, + field: 2, + } + return s +} diff --git a/cli/smartcontract/testdata/rpcbindings/invalid8/invalid.yml b/cli/smartcontract/testdata/rpcbindings/invalid8/invalid.yml new file mode 100644 index 000000000..2b308a573 --- /dev/null +++ b/cli/smartcontract/testdata/rpcbindings/invalid8/invalid.yml @@ -0,0 +1 @@ +name: Test duplicating struct fields diff --git a/cli/smartcontract/testdata/notifications/config.yml b/cli/smartcontract/testdata/rpcbindings/notifications/config.yml similarity index 78% rename from cli/smartcontract/testdata/notifications/config.yml rename to cli/smartcontract/testdata/rpcbindings/notifications/config.yml index ef2866318..40b13bf70 100644 --- a/cli/smartcontract/testdata/notifications/config.yml +++ b/cli/smartcontract/testdata/rpcbindings/notifications/config.yml @@ -16,4 +16,8 @@ events: - name: "SomeArray" parameters: - name: a - type: Array \ No newline at end of file + type: Array + - name: "SomeUnexportedField" + parameters: + - name: s + type: Struct \ No newline at end of file diff --git a/cli/smartcontract/testdata/notifications/config_extended.yml b/cli/smartcontract/testdata/rpcbindings/notifications/config_extended.yml similarity index 77% rename from cli/smartcontract/testdata/notifications/config_extended.yml rename to cli/smartcontract/testdata/rpcbindings/notifications/config_extended.yml index ac8f06d6d..6c6e418c9 100644 --- a/cli/smartcontract/testdata/notifications/config_extended.yml +++ b/cli/smartcontract/testdata/rpcbindings/notifications/config_extended.yml @@ -36,6 +36,13 @@ events: base: Array value: base: Integer + - name: "SomeUnexportedField" + parameters: + - name: s + type: Struct + extendedtype: + base: Struct + name: simpleStruct namedtypes: crazyStruct: base: Struct @@ -44,4 +51,10 @@ namedtypes: - field: I base: Integer - field: B - base: Boolean \ No newline at end of file + base: Boolean + simpleStruct: + base: Struct + name: simpleStruct + fields: + - field: i + base: Integer \ No newline at end of file diff --git a/cli/smartcontract/testdata/notifications/config_guessed.yml b/cli/smartcontract/testdata/rpcbindings/notifications/config_guessed.yml similarity index 78% rename from cli/smartcontract/testdata/notifications/config_guessed.yml rename to cli/smartcontract/testdata/rpcbindings/notifications/config_guessed.yml index ef2866318..40b13bf70 100644 --- a/cli/smartcontract/testdata/notifications/config_guessed.yml +++ b/cli/smartcontract/testdata/rpcbindings/notifications/config_guessed.yml @@ -16,4 +16,8 @@ events: - name: "SomeArray" parameters: - name: a - type: Array \ No newline at end of file + type: Array + - name: "SomeUnexportedField" + parameters: + - name: s + type: Struct \ No newline at end of file diff --git a/cli/smartcontract/testdata/notifications/notifications.go b/cli/smartcontract/testdata/rpcbindings/notifications/notifications.go similarity index 65% rename from cli/smartcontract/testdata/notifications/notifications.go rename to cli/smartcontract/testdata/rpcbindings/notifications/notifications.go index 80564a4e4..e88a41388 100644 --- a/cli/smartcontract/testdata/notifications/notifications.go +++ b/cli/smartcontract/testdata/rpcbindings/notifications/notifications.go @@ -23,3 +23,11 @@ func Struct() { func Array() { runtime.Notify("SomeArray", [][]int{}) } + +// UnexportedField emits notification with unexported field that must be converted +// to exported in the resulting RPC binding. +func UnexportedField() { + runtime.Notify("SomeUnexportedField", struct { + i int + }{i: 123}) +} diff --git a/cli/smartcontract/testdata/notifications/rpcbindings.out b/cli/smartcontract/testdata/rpcbindings/notifications/rpcbindings.out similarity index 90% rename from cli/smartcontract/testdata/notifications/rpcbindings.out rename to cli/smartcontract/testdata/rpcbindings/notifications/rpcbindings.out index 74c872833..be2bb47be 100644 --- a/cli/smartcontract/testdata/notifications/rpcbindings.out +++ b/cli/smartcontract/testdata/rpcbindings/notifications/rpcbindings.out @@ -97,6 +97,11 @@ type SomeArrayEvent struct { A []any } +// SomeUnexportedFieldEvent represents "SomeUnexportedField" event emitted by the contract. +type SomeUnexportedFieldEvent struct { + S []any +} + // Actor is used by Contract to call state-changing methods. type Actor interface { MakeCall(contract util.Uint160, method string, params ...any) (*transaction.Transaction, error) @@ -207,6 +212,28 @@ func (c *Contract) StructUnsigned() (*transaction.Transaction, error) { return c.actor.MakeUnsignedCall(c.hash, "struct", nil) } +// UnexportedField creates a transaction invoking `unexportedField` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) UnexportedField() (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "unexportedField") +} + +// UnexportedFieldTransaction creates a transaction invoking `unexportedField` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) UnexportedFieldTransaction() (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "unexportedField") +} + +// UnexportedFieldUnsigned creates a transaction invoking `unexportedField` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) UnexportedFieldUnsigned() (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "unexportedField", nil) +} + // itemToLedgerBlock converts stack item into *LedgerBlock. func itemToLedgerBlock(item stackitem.Item, err error) (*LedgerBlock, error) { if err != nil { @@ -1030,3 +1057,68 @@ func (e *SomeArrayEvent) FromStackItem(item *stackitem.Array) error { return nil } + +// SomeUnexportedFieldEventsFromApplicationLog retrieves a set of all emitted events +// with "SomeUnexportedField" name from the provided [result.ApplicationLog]. +func SomeUnexportedFieldEventsFromApplicationLog(log *result.ApplicationLog) ([]*SomeUnexportedFieldEvent, error) { + if log == nil { + return nil, errors.New("nil application log") + } + + var res []*SomeUnexportedFieldEvent + for i, ex := range log.Executions { + for j, e := range ex.Events { + if e.Name != "SomeUnexportedField" { + continue + } + event := new(SomeUnexportedFieldEvent) + err := event.FromStackItem(e.Item) + if err != nil { + return nil, fmt.Errorf("failed to deserialize SomeUnexportedFieldEvent from stackitem (execution #%d, event #%d): %w", i, j, err) + } + res = append(res, event) + } + } + + return res, nil +} + +// FromStackItem converts provided [stackitem.Array] to SomeUnexportedFieldEvent or +// returns an error if it's not possible to do to so. +func (e *SomeUnexportedFieldEvent) FromStackItem(item *stackitem.Array) error { + if item == nil { + return errors.New("nil item") + } + arr, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not an array") + } + if len(arr) != 1 { + return errors.New("wrong number of structure elements") + } + + var ( + index = -1 + err error + ) + index++ + e.S, err = func (item stackitem.Item) ([]any, error) { + arr, ok := item.Value().([]stackitem.Item) + if !ok { + return nil, errors.New("not an array") + } + res := make([]any, len(arr)) + for i := range res { + res[i], err = arr[i].Value(), error(nil) + if err != nil { + return nil, fmt.Errorf("item %d: %w", i, err) + } + } + return res, nil + } (arr[index]) + if err != nil { + return fmt.Errorf("field S: %w", err) + } + + return nil +} diff --git a/cli/smartcontract/testdata/notifications/rpcbindings_extended.out b/cli/smartcontract/testdata/rpcbindings/notifications/rpcbindings_extended.out similarity index 89% rename from cli/smartcontract/testdata/notifications/rpcbindings_extended.out rename to cli/smartcontract/testdata/rpcbindings/notifications/rpcbindings_extended.out index 8a71c6b32..915814a00 100755 --- a/cli/smartcontract/testdata/notifications/rpcbindings_extended.out +++ b/cli/smartcontract/testdata/rpcbindings/notifications/rpcbindings_extended.out @@ -83,6 +83,11 @@ type LedgerWitnessRule struct { Condition *LedgerWitnessCondition } +// SimpleStruct is a contract-specific simpleStruct type used by its methods. +type SimpleStruct struct { + I *big.Int +} + // ComplicatedNameEvent represents "! complicated name %$#" event emitted by the contract. type ComplicatedNameEvent struct { ComplicatedParam string @@ -103,6 +108,11 @@ type SomeArrayEvent struct { A [][]*big.Int } +// SomeUnexportedFieldEvent represents "SomeUnexportedField" event emitted by the contract. +type SomeUnexportedFieldEvent struct { + S *SimpleStruct +} + // Actor is used by Contract to call state-changing methods. type Actor interface { MakeCall(contract util.Uint160, method string, params ...any) (*transaction.Transaction, error) @@ -213,6 +223,28 @@ func (c *Contract) StructUnsigned() (*transaction.Transaction, error) { return c.actor.MakeUnsignedCall(c.hash, "struct", nil) } +// UnexportedField creates a transaction invoking `unexportedField` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) UnexportedField() (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "unexportedField") +} + +// UnexportedFieldTransaction creates a transaction invoking `unexportedField` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) UnexportedFieldTransaction() (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "unexportedField") +} + +// UnexportedFieldUnsigned creates a transaction invoking `unexportedField` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) UnexportedFieldUnsigned() (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "unexportedField", nil) +} + // itemToCrazyStruct converts stack item into *CrazyStruct. func itemToCrazyStruct(item stackitem.Item, err error) (*CrazyStruct, error) { if err != nil { @@ -816,6 +848,40 @@ func (res *LedgerWitnessRule) FromStackItem(item stackitem.Item) error { return nil } +// itemToSimpleStruct converts stack item into *SimpleStruct. +func itemToSimpleStruct(item stackitem.Item, err error) (*SimpleStruct, error) { + if err != nil { + return nil, err + } + var res = new(SimpleStruct) + err = res.FromStackItem(item) + return res, err +} + +// FromStackItem retrieves fields of SimpleStruct from the given +// [stackitem.Item] or returns an error if it's not possible to do to so. +func (res *SimpleStruct) FromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not an array") + } + if len(arr) != 1 { + return errors.New("wrong number of structure elements") + } + + var ( + index = -1 + err error + ) + index++ + res.I, err = arr[index].TryInteger() + if err != nil { + return fmt.Errorf("field I: %w", err) + } + + return nil +} + // ComplicatedNameEventsFromApplicationLog retrieves a set of all emitted events // with "! complicated name %$#" name from the provided [result.ApplicationLog]. func ComplicatedNameEventsFromApplicationLog(log *result.ApplicationLog) ([]*ComplicatedNameEvent, error) { @@ -1126,3 +1192,55 @@ func (e *SomeArrayEvent) FromStackItem(item *stackitem.Array) error { return nil } + +// SomeUnexportedFieldEventsFromApplicationLog retrieves a set of all emitted events +// with "SomeUnexportedField" name from the provided [result.ApplicationLog]. +func SomeUnexportedFieldEventsFromApplicationLog(log *result.ApplicationLog) ([]*SomeUnexportedFieldEvent, error) { + if log == nil { + return nil, errors.New("nil application log") + } + + var res []*SomeUnexportedFieldEvent + for i, ex := range log.Executions { + for j, e := range ex.Events { + if e.Name != "SomeUnexportedField" { + continue + } + event := new(SomeUnexportedFieldEvent) + err := event.FromStackItem(e.Item) + if err != nil { + return nil, fmt.Errorf("failed to deserialize SomeUnexportedFieldEvent from stackitem (execution #%d, event #%d): %w", i, j, err) + } + res = append(res, event) + } + } + + return res, nil +} + +// FromStackItem converts provided [stackitem.Array] to SomeUnexportedFieldEvent or +// returns an error if it's not possible to do to so. +func (e *SomeUnexportedFieldEvent) FromStackItem(item *stackitem.Array) error { + if item == nil { + return errors.New("nil item") + } + arr, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not an array") + } + if len(arr) != 1 { + return errors.New("wrong number of structure elements") + } + + var ( + index = -1 + err error + ) + index++ + e.S, err = itemToSimpleStruct(arr[index], nil) + if err != nil { + return fmt.Errorf("field S: %w", err) + } + + return nil +} diff --git a/cli/smartcontract/testdata/notifications/rpcbindings_guessed.out b/cli/smartcontract/testdata/rpcbindings/notifications/rpcbindings_guessed.out similarity index 91% rename from cli/smartcontract/testdata/notifications/rpcbindings_guessed.out rename to cli/smartcontract/testdata/rpcbindings/notifications/rpcbindings_guessed.out index eb5c650b1..bf7032c70 100755 --- a/cli/smartcontract/testdata/notifications/rpcbindings_guessed.out +++ b/cli/smartcontract/testdata/rpcbindings/notifications/rpcbindings_guessed.out @@ -103,6 +103,11 @@ type SomeArrayEvent struct { A [][]*big.Int } +// SomeUnexportedFieldEvent represents "SomeUnexportedField" event emitted by the contract. +type SomeUnexportedFieldEvent struct { + S *Unnamed +} + // Actor is used by Contract to call state-changing methods. type Actor interface { MakeCall(contract util.Uint160, method string, params ...any) (*transaction.Transaction, error) @@ -213,6 +218,28 @@ func (c *Contract) StructUnsigned() (*transaction.Transaction, error) { return c.actor.MakeUnsignedCall(c.hash, "struct", nil) } +// UnexportedField creates a transaction invoking `unexportedField` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) UnexportedField() (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "unexportedField") +} + +// UnexportedFieldTransaction creates a transaction invoking `unexportedField` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) UnexportedFieldTransaction() (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "unexportedField") +} + +// UnexportedFieldUnsigned creates a transaction invoking `unexportedField` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) UnexportedFieldUnsigned() (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "unexportedField", nil) +} + // itemToLedgerBlock converts stack item into *LedgerBlock. func itemToLedgerBlock(item stackitem.Item, err error) (*LedgerBlock, error) { if err != nil { @@ -1139,3 +1166,55 @@ func (e *SomeArrayEvent) FromStackItem(item *stackitem.Array) error { return nil } + +// SomeUnexportedFieldEventsFromApplicationLog retrieves a set of all emitted events +// with "SomeUnexportedField" name from the provided [result.ApplicationLog]. +func SomeUnexportedFieldEventsFromApplicationLog(log *result.ApplicationLog) ([]*SomeUnexportedFieldEvent, error) { + if log == nil { + return nil, errors.New("nil application log") + } + + var res []*SomeUnexportedFieldEvent + for i, ex := range log.Executions { + for j, e := range ex.Events { + if e.Name != "SomeUnexportedField" { + continue + } + event := new(SomeUnexportedFieldEvent) + err := event.FromStackItem(e.Item) + if err != nil { + return nil, fmt.Errorf("failed to deserialize SomeUnexportedFieldEvent from stackitem (execution #%d, event #%d): %w", i, j, err) + } + res = append(res, event) + } + } + + return res, nil +} + +// FromStackItem converts provided [stackitem.Array] to SomeUnexportedFieldEvent or +// returns an error if it's not possible to do to so. +func (e *SomeUnexportedFieldEvent) FromStackItem(item *stackitem.Array) error { + if item == nil { + return errors.New("nil item") + } + arr, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not an array") + } + if len(arr) != 1 { + return errors.New("wrong number of structure elements") + } + + var ( + index = -1 + err error + ) + index++ + e.S, err = itemToUnnamed(arr[index], nil) + if err != nil { + return fmt.Errorf("field S: %w", err) + } + + return nil +} diff --git a/cli/smartcontract/testdata/structs/config.yml b/cli/smartcontract/testdata/rpcbindings/structs/config.yml similarity index 100% rename from cli/smartcontract/testdata/structs/config.yml rename to cli/smartcontract/testdata/rpcbindings/structs/config.yml diff --git a/cli/smartcontract/testdata/structs/rpcbindings.out b/cli/smartcontract/testdata/rpcbindings/structs/rpcbindings.out similarity index 99% rename from cli/smartcontract/testdata/structs/rpcbindings.out rename to cli/smartcontract/testdata/rpcbindings/structs/rpcbindings.out index 32acf3089..d9c7ebdcc 100644 --- a/cli/smartcontract/testdata/structs/rpcbindings.out +++ b/cli/smartcontract/testdata/rpcbindings/structs/rpcbindings.out @@ -152,6 +152,7 @@ type StructsInternal struct { ArrOfH160 []util.Uint160 Map map[*big.Int]keys.PublicKeys Struct *StructsInternal + UnexportedField *big.Int } // Invoker is used by ContractReader to call various safe methods. @@ -1410,7 +1411,7 @@ func (res *StructsInternal) FromStackItem(item stackitem.Item) error { if !ok { return errors.New("not an array") } - if len(arr) != 13 { + if len(arr) != 14 { return errors.New("wrong number of structure elements") } @@ -1622,5 +1623,11 @@ func (res *StructsInternal) FromStackItem(item stackitem.Item) error { return fmt.Errorf("field Struct: %w", err) } + index++ + res.UnexportedField, err = arr[index].TryInteger() + if err != nil { + return fmt.Errorf("field UnexportedField: %w", err) + } + return nil } diff --git a/cli/smartcontract/testdata/structs/rpcbindings_dynamic_hash.out b/cli/smartcontract/testdata/rpcbindings/structs/rpcbindings_dynamic_hash.out similarity index 99% rename from cli/smartcontract/testdata/structs/rpcbindings_dynamic_hash.out rename to cli/smartcontract/testdata/rpcbindings/structs/rpcbindings_dynamic_hash.out index 69558da30..98280d010 100755 --- a/cli/smartcontract/testdata/structs/rpcbindings_dynamic_hash.out +++ b/cli/smartcontract/testdata/rpcbindings/structs/rpcbindings_dynamic_hash.out @@ -149,6 +149,7 @@ type StructsInternal struct { ArrOfH160 []util.Uint160 Map map[*big.Int]keys.PublicKeys Struct *StructsInternal + UnexportedField *big.Int } // Invoker is used by ContractReader to call various safe methods. @@ -1406,7 +1407,7 @@ func (res *StructsInternal) FromStackItem(item stackitem.Item) error { if !ok { return errors.New("not an array") } - if len(arr) != 13 { + if len(arr) != 14 { return errors.New("wrong number of structure elements") } @@ -1618,5 +1619,11 @@ func (res *StructsInternal) FromStackItem(item stackitem.Item) error { return fmt.Errorf("field Struct: %w", err) } + index++ + res.UnexportedField, err = arr[index].TryInteger() + if err != nil { + return fmt.Errorf("field UnexportedField: %w", err) + } + return nil } diff --git a/cli/smartcontract/testdata/rpcbindings/structs/structs.go b/cli/smartcontract/testdata/rpcbindings/structs/structs.go new file mode 100644 index 000000000..cf3fa2867 --- /dev/null +++ b/cli/smartcontract/testdata/rpcbindings/structs/structs.go @@ -0,0 +1,40 @@ +package structs + +import ( + "github.com/nspcc-dev/neo-go/pkg/interop" + "github.com/nspcc-dev/neo-go/pkg/interop/native/ledger" + "github.com/nspcc-dev/neo-go/pkg/interop/native/management" +) + +type Internal struct { + Bool bool + Int int + Bytes []byte + String string + H160 interop.Hash160 + H256 interop.Hash256 + PK interop.PublicKey + PubKey interop.PublicKey + Sign interop.Signature + ArrOfBytes [][]byte + ArrOfH160 []interop.Hash160 + Map map[int][]interop.PublicKey + Struct *Internal + unexportedField int // this one should be exported in the resulting RPC binding. +} + +func Contract(mc management.Contract) management.Contract { + return mc +} + +func Block(b *ledger.Block) *ledger.Block { + return b +} + +func Transaction(t *ledger.Transaction) *ledger.Transaction { + return t +} + +func Struct(s *Internal) *Internal { + return s +} diff --git a/cli/smartcontract/testdata/types/config.yml b/cli/smartcontract/testdata/rpcbindings/types/config.yml similarity index 100% rename from cli/smartcontract/testdata/types/config.yml rename to cli/smartcontract/testdata/rpcbindings/types/config.yml diff --git a/cli/smartcontract/testdata/types/rpcbindings.out b/cli/smartcontract/testdata/rpcbindings/types/rpcbindings.out similarity index 100% rename from cli/smartcontract/testdata/types/rpcbindings.out rename to cli/smartcontract/testdata/rpcbindings/types/rpcbindings.out diff --git a/cli/smartcontract/testdata/types/rpcbindings_dynamic_hash.out b/cli/smartcontract/testdata/rpcbindings/types/rpcbindings_dynamic_hash.out similarity index 100% rename from cli/smartcontract/testdata/types/rpcbindings_dynamic_hash.out rename to cli/smartcontract/testdata/rpcbindings/types/rpcbindings_dynamic_hash.out diff --git a/cli/smartcontract/testdata/types/types.go b/cli/smartcontract/testdata/rpcbindings/types/types.go similarity index 100% rename from cli/smartcontract/testdata/types/types.go rename to cli/smartcontract/testdata/rpcbindings/types/types.go diff --git a/cli/smartcontract/testdata/structs/structs.go b/cli/smartcontract/testdata/structs/structs.go deleted file mode 100644 index 5a54fb224..000000000 --- a/cli/smartcontract/testdata/structs/structs.go +++ /dev/null @@ -1,39 +0,0 @@ -package structs - -import ( - "github.com/nspcc-dev/neo-go/pkg/interop" - "github.com/nspcc-dev/neo-go/pkg/interop/native/ledger" - "github.com/nspcc-dev/neo-go/pkg/interop/native/management" -) - -type Internal struct { - Bool bool - Int int - Bytes []byte - String string - H160 interop.Hash160 - H256 interop.Hash256 - PK interop.PublicKey - PubKey interop.PublicKey - Sign interop.Signature - ArrOfBytes [][]byte - ArrOfH160 []interop.Hash160 - Map map[int][]interop.PublicKey - Struct *Internal -} - -func Contract(mc management.Contract) management.Contract { - return mc -} - -func Block(b *ledger.Block) *ledger.Block { - return b -} - -func Transaction(t *ledger.Transaction) *ledger.Transaction { - return t -} - -func Struct(s *Internal) *Internal { - return s -} diff --git a/pkg/smartcontract/rpcbinding/binding.go b/pkg/smartcontract/rpcbinding/binding.go index 697682b63..942791d9a 100644 --- a/pkg/smartcontract/rpcbinding/binding.go +++ b/pkg/smartcontract/rpcbinding/binding.go @@ -23,7 +23,7 @@ const ( // {{.Name}} represents "{{.ManifestName}}" event emitted by the contract. type {{.Name}} struct { {{- range $index, $arg := .Parameters}} - {{.Name}} {{.Type}} + {{ upperFirst .Name}} {{.Type}} {{- end}} } {{ end }}` @@ -126,7 +126,7 @@ var Hash = {{ .Hash }} // {{toTypeName $name}} is a contract-specific {{$name}} type used by its methods. type {{toTypeName $name}} struct { {{- range $m := $typ.Fields}} - {{.Field}} {{etTypeToStr .ExtendedType}} + {{ upperFirst .Field}} {{etTypeToStr .ExtendedType}} {{- end}} } {{end}} @@ -262,9 +262,9 @@ func (res *{{toTypeName $name}}) FromStackItem(item stackitem.Item) error { ) {{- range $m := $typ.Fields}} index++ - res.{{.Field}}, err = {{etTypeConverter .ExtendedType "arr[index]"}} + res.{{ upperFirst .Field}}, err = {{etTypeConverter .ExtendedType "arr[index]"}} if err != nil { - return fmt.Errorf("field {{.Field}}: %w", err) + return fmt.Errorf("field {{ upperFirst .Field}}: %w", err) } {{end}} {{- end}} @@ -317,9 +317,9 @@ func (e *{{$e.Name}}) FromStackItem(item *stackitem.Array) error { ) {{- range $p := $e.Parameters}} index++ - e.{{.Name}}, err = {{etTypeConverter .ExtType "arr[index]"}} + e.{{ upperFirst .Name}}, err = {{etTypeConverter .ExtType "arr[index]"}} if err != nil { - return fmt.Errorf("field {{.Name}}: %w", err) + return fmt.Errorf("field {{ upperFirst .Name}}: %w", err) } {{end}} {{- end}} @@ -430,6 +430,28 @@ func Generate(cfg binding.Config) error { ctr = scTemplateToRPC(cfg, ctr, imports, scTypeToGo) ctr.NamedTypes = cfg.NamedTypes + // Check resulting named types and events don't have duplicating field names. + for _, t := range ctr.NamedTypes { + fDict := make(map[string]struct{}) + for _, n := range t.Fields { + name := upperFirst(n.Field) + if _, ok := fDict[name]; ok { + return fmt.Errorf("named type `%s` has two fields with identical resulting binding name `%s`", t.Name, name) + } + fDict[name] = struct{}{} + } + } + for _, e := range ctr.CustomEvents { + fDict := make(map[string]struct{}) + for _, n := range e.Parameters { + name := upperFirst(n.Name) + if _, ok := fDict[name]; ok { + return fmt.Errorf("event `%s` has two fields with identical resulting binding name `%s`", e.Name, name) + } + fDict[name] = struct{}{} + } + } + var srcTemplate = template.Must(template.New("generate").Funcs(template.FuncMap{ "addIndent": addIndent, "etTypeConverter": etTypeConverter, @@ -439,6 +461,7 @@ func Generate(cfg binding.Config) error { }, "toTypeName": toTypeName, "cutPointer": cutPointer, + "upperFirst": upperFirst, }).Parse(srcTmpl)) return srcTemplate.Execute(cfg.Output, ctr) @@ -863,7 +886,7 @@ func toTypeName(s string) string { return -1 } return c - }, strings.ToUpper(s[0:1])+s[1:]) + }, upperFirst(s)) } func addIndent(str string, ind string) string {