From 135672186237914d094cd28d7e416b3cf888ce1b Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 10 Aug 2023 20:23:54 +0300 Subject: [PATCH] rpcbinding: export any unexported fields of structures/events Perform private -> public transformation at the last step of RPC binding generation so that it works not only with NeoGo contracts, but with any other contracts. Close #3083. Signed-off-by: Anna Shaleva --- .../testdata/notifications/config.yml | 6 +- .../notifications/config_extended.yml | 15 ++- .../testdata/notifications/config_guessed.yml | 6 +- .../testdata/notifications/notifications.go | 8 ++ .../testdata/notifications/rpcbindings.out | 92 ++++++++++++++ .../notifications/rpcbindings_extended.out | 118 ++++++++++++++++++ .../notifications/rpcbindings_guessed.out | 79 ++++++++++++ .../testdata/structs/rpcbindings.out | 9 +- .../structs/rpcbindings_dynamic_hash.out | 9 +- cli/smartcontract/testdata/structs/structs.go | 27 ++-- pkg/smartcontract/rpcbinding/binding.go | 13 +- 11 files changed, 358 insertions(+), 24 deletions(-) diff --git a/cli/smartcontract/testdata/notifications/config.yml b/cli/smartcontract/testdata/notifications/config.yml index ef2866318..40b13bf70 100644 --- a/cli/smartcontract/testdata/notifications/config.yml +++ b/cli/smartcontract/testdata/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/notifications/config_extended.yml index ac8f06d6d..6c6e418c9 100644 --- a/cli/smartcontract/testdata/notifications/config_extended.yml +++ b/cli/smartcontract/testdata/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/notifications/config_guessed.yml index ef2866318..40b13bf70 100644 --- a/cli/smartcontract/testdata/notifications/config_guessed.yml +++ b/cli/smartcontract/testdata/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/notifications/notifications.go index 80564a4e4..e88a41388 100644 --- a/cli/smartcontract/testdata/notifications/notifications.go +++ b/cli/smartcontract/testdata/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/notifications/rpcbindings.out index 74c872833..be2bb47be 100644 --- a/cli/smartcontract/testdata/notifications/rpcbindings.out +++ b/cli/smartcontract/testdata/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/notifications/rpcbindings_extended.out index 8a71c6b32..915814a00 100755 --- a/cli/smartcontract/testdata/notifications/rpcbindings_extended.out +++ b/cli/smartcontract/testdata/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/notifications/rpcbindings_guessed.out index eb5c650b1..bf7032c70 100755 --- a/cli/smartcontract/testdata/notifications/rpcbindings_guessed.out +++ b/cli/smartcontract/testdata/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/rpcbindings.out b/cli/smartcontract/testdata/structs/rpcbindings.out index 32acf3089..d9c7ebdcc 100644 --- a/cli/smartcontract/testdata/structs/rpcbindings.out +++ b/cli/smartcontract/testdata/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/structs/rpcbindings_dynamic_hash.out index 69558da30..98280d010 100755 --- a/cli/smartcontract/testdata/structs/rpcbindings_dynamic_hash.out +++ b/cli/smartcontract/testdata/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/structs/structs.go b/cli/smartcontract/testdata/structs/structs.go index 5a54fb224..cf3fa2867 100644 --- a/cli/smartcontract/testdata/structs/structs.go +++ b/cli/smartcontract/testdata/structs/structs.go @@ -7,19 +7,20 @@ import ( ) 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 + 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 { diff --git a/pkg/smartcontract/rpcbinding/binding.go b/pkg/smartcontract/rpcbinding/binding.go index 697682b63..f1ee03345 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}} @@ -439,6 +439,7 @@ func Generate(cfg binding.Config) error { }, "toTypeName": toTypeName, "cutPointer": cutPointer, + "upperFirst": upperFirst, }).Parse(srcTmpl)) return srcTemplate.Execute(cfg.Output, ctr)