diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index e2163e8628..f91315d72b 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -2333,6 +2333,118 @@ paths: description: Success default: description: "" + /namespaces/{ns}/contracts/interfaces/generate: + post: + description: 'TODO: Description' + operationId: postGenerateContractInterface + parameters: + - description: 'TODO: Description' + in: path + name: ns + required: true + schema: + example: default + type: string + - description: Server-side request timeout (millseconds, or set a custom suffix + like 10s) + in: header + name: Request-Timeout + schema: + default: 120s + type: string + requestBody: + content: + application/json: + schema: + properties: + description: + type: string + input: + type: string + name: + type: string + namespace: + type: string + version: + type: string + type: object + responses: + "200": + content: + application/json: + schema: + properties: + description: + type: string + events: + items: + properties: + contract: {} + description: + type: string + id: {} + name: + type: string + namespace: + type: string + params: + items: + properties: + name: + type: string + schema: + type: string + type: object + type: array + pathname: + type: string + type: object + type: array + id: {} + message: {} + methods: + items: + properties: + contract: {} + description: + type: string + id: {} + name: + type: string + namespace: + type: string + params: + items: + properties: + name: + type: string + schema: + type: string + type: object + type: array + pathname: + type: string + returns: + items: + properties: + name: + type: string + schema: + type: string + type: object + type: array + type: object + type: array + name: + type: string + namespace: + type: string + version: + type: string + type: object + description: Success + default: + description: "" /namespaces/{ns}/contracts/invoke: post: description: 'TODO: Description' diff --git a/internal/apiserver/route_post_contract_interface_generate.go b/internal/apiserver/route_post_contract_interface_generate.go new file mode 100644 index 0000000000..e095e5b45e --- /dev/null +++ b/internal/apiserver/route_post_contract_interface_generate.go @@ -0,0 +1,46 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apiserver + +import ( + "net/http" + + "github.com/hyperledger/firefly/internal/config" + "github.com/hyperledger/firefly/internal/i18n" + "github.com/hyperledger/firefly/internal/oapispec" + "github.com/hyperledger/firefly/pkg/fftypes" +) + +var postContractInterfaceGenerate = &oapispec.Route{ + Name: "postGenerateContractInterface", + Path: "namespaces/{ns}/contracts/interfaces/generate", + Method: http.MethodPost, + PathParams: []*oapispec.PathParam{ + {Name: "ns", ExampleFromConf: config.NamespacesDefault, Description: i18n.MsgTBD}, + }, + QueryParams: []*oapispec.QueryParam{}, + FilterFactory: nil, + Description: i18n.MsgTBD, + JSONInputValue: func() interface{} { return &fftypes.FFIGenerationRequest{} }, + JSONInputMask: nil, + JSONOutputValue: func() interface{} { return &fftypes.FFI{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *oapispec.APIRequest) (output interface{}, err error) { + generationRequest := r.Input.(*fftypes.FFIGenerationRequest) + return getOr(r.Ctx).Contracts().GenerateFFI(r.Ctx, r.PP["ns"], generationRequest) + }, +} diff --git a/internal/apiserver/route_post_contract_interface_generate_test.go b/internal/apiserver/route_post_contract_interface_generate_test.go new file mode 100644 index 0000000000..774d2450aa --- /dev/null +++ b/internal/apiserver/route_post_contract_interface_generate_test.go @@ -0,0 +1,47 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apiserver + +import ( + "bytes" + "encoding/json" + "net/http/httptest" + "testing" + + "github.com/hyperledger/firefly/mocks/contractmocks" + "github.com/hyperledger/firefly/pkg/fftypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPostContractInterfaceGenerate(t *testing.T) { + o, r := newTestAPIServer() + mcm := &contractmocks.Manager{} + o.On("Contracts").Return(mcm) + input := fftypes.Datatype{} + var buf bytes.Buffer + json.NewEncoder(&buf).Encode(&input) + req := httptest.NewRequest("POST", "/api/v1/namespaces/ns1/contracts/interfaces/generate", &buf) + req.Header.Set("Content-Type", "application/json; charset=utf-8") + res := httptest.NewRecorder() + + mcm.On("GenerateFFI", mock.Anything, "ns1", mock.Anything). + Return(&fftypes.FFI{}, nil) + r.ServeHTTP(res, req) + + assert.Equal(t, 200, res.Result().StatusCode) +} diff --git a/internal/apiserver/routes.go b/internal/apiserver/routes.go index 97ac7d6698..8bfc5e117f 100644 --- a/internal/apiserver/routes.go +++ b/internal/apiserver/routes.go @@ -98,6 +98,7 @@ var routes = []*oapispec.Route{ postContractInterfaceInvoke, postContractInterfaceQuery, postContractInterfaceSubscribe, + postContractInterfaceGenerate, postNewContractAPI, getContractAPIByName, diff --git a/internal/blockchain/ethereum/ethereum.go b/internal/blockchain/ethereum/ethereum.go index 41314f1936..3f37bfd564 100644 --- a/internal/blockchain/ethereum/ethereum.go +++ b/internal/blockchain/ethereum/ethereum.go @@ -38,6 +38,11 @@ import ( const ( broadcastBatchEventSignature = "BatchPin(address,uint256,string,bytes32,bytes32,string,bytes32[])" + booleanType = "boolean" + integerType = "integer" + stringType = "string" + arrayType = "array" + objectType = "object" ) type Ethereum struct { @@ -68,7 +73,7 @@ type asyncTXSubmission struct { } type queryOutput struct { - Output string `json:"output"` + Output interface{} `json:"output"` } type ethWSCommandPayload struct { @@ -81,10 +86,22 @@ type Location struct { } type paramDetails struct { - Type string - InternalType string - Indexed bool - Index int + Type string `json:"type"` + InternalType string `json:"internalType,omitempty"` + Indexed bool `json:"indexed,omitempty"` + Index *int `json:"index,omitempty"` +} + +type Schema struct { + Type string `json:"type"` + Details *paramDetails `json:"details,omitempty"` + Properties map[string]*Schema `json:"properties,omitempty"` + Items *Schema `json:"items,omitempty"` +} + +func (s *Schema) ToJSON() string { + b, _ := json.Marshal(s) + return string(b) } // ABIArgumentMarshaling is abi.ArgumentMarshaling @@ -671,11 +688,12 @@ func (e *Ethereum) addParamsToList(ctx context.Context, abiParamList []ABIArgume func processField(name string, schema *jsonschema.Schema) ABIArgumentMarshaling { details := getParamDetails(schema) arg := ABIArgumentMarshaling{ - Name: name, - Type: details.Type, - Indexed: details.Indexed, + Name: name, + Type: details.Type, + InternalType: details.InternalType, + Indexed: details.Indexed, } - if schema.Types[0] == "object" { + if schema.Types[0] == objectType { arg.Components = buildABIArgumentArray(schema.Properties) } return arg @@ -686,7 +704,7 @@ func buildABIArgumentArray(properties map[string]*jsonschema.Schema) []ABIArgume for propertyName, propertySchema := range properties { details := getParamDetails(propertySchema) arg := processField(propertyName, propertySchema) - args[details.Index] = arg + args[*details.Index] = arg } return args } @@ -700,11 +718,15 @@ func getParamDetails(schema *jsonschema.Schema) *paramDetails { } if i, ok := details["index"]; ok { index, _ := i.(json.Number).Int64() - paramDetails.Index = int(index) + paramDetails.Index = new(int) + *paramDetails.Index = int(index) } if i, ok := details["indexed"]; ok { paramDetails.Indexed = i.(bool) } + if i, ok := details["internalType"]; ok { + paramDetails.InternalType = i.(string) + } return paramDetails } @@ -733,3 +755,126 @@ func (e *Ethereum) getContractAddress(ctx context.Context, instancePath string) } return output["address"], nil } + +func (e *Ethereum) GenerateFFI(ctx context.Context, generationRequest *fftypes.FFIGenerationRequest) (*fftypes.FFI, error) { + var abi []ABIElementMarshaling + err := json.Unmarshal(generationRequest.Input.Bytes(), &abi) + if err != nil { + return nil, i18n.NewError(ctx, i18n.MsgFFIGenerationFailed, "unable to deserialize JSON as ABI") + } + ffi := e.convertABIToFFI(generationRequest.Namespace, generationRequest.Name, generationRequest.Version, generationRequest.Description, abi) + return ffi, nil +} + +func (e *Ethereum) convertABIToFFI(ns, name, version, description string, abi []ABIElementMarshaling) *fftypes.FFI { + ffi := &fftypes.FFI{ + Namespace: ns, + Name: name, + Version: version, + Description: description, + Methods: []*fftypes.FFIMethod{}, + Events: []*fftypes.FFIEvent{}, + } + + for _, element := range abi { + switch element.Type { + case "event": + event := &fftypes.FFIEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: element.Name, + Params: e.convertABIArgumentsToFFI(element.Inputs), + }, + } + ffi.Events = append(ffi.Events, event) + case "function": + method := &fftypes.FFIMethod{ + Name: element.Name, + Params: e.convertABIArgumentsToFFI(element.Inputs), + Returns: e.convertABIArgumentsToFFI(element.Outputs), + } + ffi.Methods = append(ffi.Methods, method) + } + } + return ffi +} + +func (e *Ethereum) convertABIArgumentsToFFI(args []ABIArgumentMarshaling) fftypes.FFIParams { + ffiParams := fftypes.FFIParams{} + for _, arg := range args { + param := &fftypes.FFIParam{ + Name: arg.Name, + } + s := e.getSchema(arg) + param.Schema = fftypes.JSONAnyPtr(s.ToJSON()) + ffiParams = append(ffiParams, param) + } + return ffiParams +} + +func (e *Ethereum) getSchema(arg ABIArgumentMarshaling) *Schema { + s := &Schema{ + Type: e.getFFIType(arg.Type), + Details: ¶mDetails{ + Type: arg.Type, + InternalType: arg.InternalType, + Indexed: arg.Indexed, + }, + } + var properties map[string]*Schema + if len(arg.Components) > 0 { + properties = e.getSchemaForObjectComponents(arg) + } + if s.Type == arrayType { + levels := strings.Count(arg.Type, "[]") + innerType := e.getFFIType(strings.ReplaceAll(arg.Type, "[]", "")) + innerSchema := &Schema{ + Type: innerType, + } + if len(arg.Components) > 0 { + innerSchema.Properties = e.getSchemaForObjectComponents(arg) + } + for i := 1; i < levels; i++ { + innerSchema = &Schema{ + Type: arrayType, + Items: innerSchema, + } + } + s.Items = innerSchema + } else { + s.Properties = properties + } + return s +} + +func (e *Ethereum) getSchemaForObjectComponents(arg ABIArgumentMarshaling) map[string]*Schema { + m := make(map[string]*Schema, len(arg.Components)) + for i, component := range arg.Components { + componentSchema := e.getSchema(component) + componentSchema.Details.Index = new(int) + *componentSchema.Details.Index = i + m[component.Name] = componentSchema + } + return m +} + +func (e *Ethereum) getFFIType(solitidyType string) string { + + switch solitidyType { + case stringType, "address": + return stringType + case "bool": + return booleanType + case "tuple": + return objectType + default: + switch { + case strings.HasSuffix(solitidyType, "[]"): + return arrayType + case strings.Contains(solitidyType, "byte"): + return stringType + case strings.Contains(solitidyType, "int"): + return integerType + } + } + return "" +} diff --git a/internal/blockchain/ethereum/ethereum_test.go b/internal/blockchain/ethereum/ethereum_test.go index 16bb866462..751a7e4762 100644 --- a/internal/blockchain/ethereum/ethereum_test.go +++ b/internal/blockchain/ethereum/ethereum_test.go @@ -1888,6 +1888,51 @@ func TestFFIMethodToABIObject(t *testing.T) { assert.Equal(t, expectedABIElement, abi) } +func TestFFIMethodToABINestedArray(t *testing.T) { + e, _ := newTestEthereum() + + method := &fftypes.FFIMethod{ + Name: "set", + Params: []*fftypes.FFIParam{ + { + Name: "widget", + Schema: fftypes.JSONAnyPtr(`{ + "type": "array", + "details": { + "type": "string[][]", + "internalType": "string[][]" + }, + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }`), + }, + }, + Returns: []*fftypes.FFIParam{}, + } + + expectedABIElement := ABIElementMarshaling{ + Name: "set", + Type: "function", + Inputs: []ABIArgumentMarshaling{ + { + Name: "widget", + Type: "string[][]", + InternalType: "string[][]", + Indexed: false, + }, + }, + Outputs: []ABIArgumentMarshaling{}, + } + + abi, err := e.FFIMethodToABI(context.Background(), method) + assert.NoError(t, err) + assert.Equal(t, expectedABIElement, abi) +} + func TestFFIMethodToABIInvalidJSON(t *testing.T) { e, _ := newTestEthereum() @@ -1951,3 +1996,346 @@ func TestFFIMethodToABIBadReturn(t *testing.T) { _, err := e.FFIMethodToABI(context.Background(), method) assert.Regexp(t, "compilation failed", err) } + +func TestConvertABIToFFI(t *testing.T) { + e, _ := newTestEthereum() + + abi := []ABIElementMarshaling{ + { + Name: "set", + Type: "function", + Inputs: []ABIArgumentMarshaling{ + { + Name: "newValue", + Type: "uint256", + InternalType: "uint256", + }, + }, + Outputs: []ABIArgumentMarshaling{}, + }, + { + Name: "get", + Type: "function", + Inputs: []ABIArgumentMarshaling{}, + Outputs: []ABIArgumentMarshaling{ + { + Name: "value", + Type: "uint256", + InternalType: "uint256", + }, + }, + }, + { + Name: "Updated", + Type: "event", + Inputs: []ABIArgumentMarshaling{{ + Name: "value", + Type: "uint256", + InternalType: "uint256", + }}, + Outputs: []ABIArgumentMarshaling{}, + }, + } + + schema := fftypes.JSONAnyPtr(`{"type":"integer","details":{"type":"uint256","internalType":"uint256"}}`) + + expectedFFI := &fftypes.FFI{ + Name: "SimpleStorage", + Version: "v0.0.1", + Namespace: "default", + Description: "desc", + Methods: []*fftypes.FFIMethod{ + { + Name: "set", + Params: fftypes.FFIParams{ + { + Name: "newValue", + Schema: schema, + }, + }, + Returns: fftypes.FFIParams{}, + }, + { + Name: "get", + Params: fftypes.FFIParams{}, + Returns: fftypes.FFIParams{ + { + Name: "value", + Schema: schema, + }, + }, + }, + }, + Events: []*fftypes.FFIEvent{ + { + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "Updated", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: schema, + }, + }, + }, + }, + }, + } + + actualFFI := e.convertABIToFFI("default", "SimpleStorage", "v0.0.1", "desc", abi) + assert.NotNil(t, actualFFI) + assert.Equal(t, expectedFFI, actualFFI) +} + +func TestConvertABIToFFIWithObject(t *testing.T) { + e, _ := newTestEthereum() + + abi := []ABIElementMarshaling{ + { + Name: "set", + Type: "function", + Inputs: []ABIArgumentMarshaling{ + { + Name: "newValue", + Type: "tuple", + InternalType: "struct WidgetFactory.Widget", + Components: []ABIArgumentMarshaling{ + { + Name: "size", + Type: "uint256", + InternalType: "uint256", + }, + { + Name: "description", + Type: "string", + InternalType: "string", + }, + }, + }, + }, + Outputs: []ABIArgumentMarshaling{}, + }, + } + + schema := fftypes.JSONAnyPtr(`{"type":"object","details":{"type":"tuple","internalType":"struct WidgetFactory.Widget"},"properties":{"description":{"type":"string","details":{"type":"string","internalType":"string","index":1}},"size":{"type":"integer","details":{"type":"uint256","internalType":"uint256","index":0}}}}`) + + expectedFFI := &fftypes.FFI{ + Name: "WidgetTest", + Version: "v0.0.1", + Namespace: "default", + Description: "desc", + Methods: []*fftypes.FFIMethod{ + { + Name: "set", + Params: fftypes.FFIParams{ + { + Name: "newValue", + Schema: schema, + }, + }, + Returns: fftypes.FFIParams{}, + }, + }, + Events: []*fftypes.FFIEvent{}, + } + + actualFFI := e.convertABIToFFI("default", "WidgetTest", "v0.0.1", "desc", abi) + assert.NotNil(t, actualFFI) + assert.Equal(t, expectedFFI, actualFFI) +} + +func TestConvertABIToFFIWithArray(t *testing.T) { + e, _ := newTestEthereum() + + abi := []ABIElementMarshaling{ + { + Name: "set", + Type: "function", + Inputs: []ABIArgumentMarshaling{ + { + Name: "newValue", + Type: "string[]", + InternalType: "string[]", + }, + }, + Outputs: []ABIArgumentMarshaling{}, + }, + } + + schema := fftypes.JSONAnyPtr(`{"type":"array","details":{"type":"string[]","internalType":"string[]"},"items":{"type":"string"}}`) + + expectedFFI := &fftypes.FFI{ + Name: "WidgetTest", + Version: "v0.0.1", + Namespace: "default", + Description: "desc", + Methods: []*fftypes.FFIMethod{ + { + Name: "set", + Params: fftypes.FFIParams{ + { + Name: "newValue", + Schema: schema, + }, + }, + Returns: fftypes.FFIParams{}, + }, + }, + Events: []*fftypes.FFIEvent{}, + } + + actualFFI := e.convertABIToFFI("default", "WidgetTest", "v0.0.1", "desc", abi) + assert.NotNil(t, actualFFI) + assert.Equal(t, expectedFFI, actualFFI) +} + +func TestConvertABIToFFIWithNestedArray(t *testing.T) { + e, _ := newTestEthereum() + + abi := []ABIElementMarshaling{ + { + Name: "set", + Type: "function", + Inputs: []ABIArgumentMarshaling{ + { + Name: "newValue", + Type: "string[][]", + InternalType: "string[][]", + }, + }, + Outputs: []ABIArgumentMarshaling{}, + }, + } + + schema := fftypes.JSONAnyPtr(`{"type":"array","details":{"type":"string[][]","internalType":"string[][]"},"items":{"type":"array","items":{"type":"string"}}}`) + expectedFFI := &fftypes.FFI{ + Name: "WidgetTest", + Version: "v0.0.1", + Namespace: "default", + Description: "desc", + Methods: []*fftypes.FFIMethod{ + { + Name: "set", + Params: fftypes.FFIParams{ + { + Name: "newValue", + Schema: schema, + }, + }, + Returns: fftypes.FFIParams{}, + }, + }, + Events: []*fftypes.FFIEvent{}, + } + + actualFFI := e.convertABIToFFI("default", "WidgetTest", "v0.0.1", "desc", abi) + assert.NotNil(t, actualFFI) + assert.Equal(t, expectedFFI, actualFFI) +} + +func TestConvertABIToFFIWithNestedArrayOfObjects(t *testing.T) { + e, _ := newTestEthereum() + + abi := []ABIElementMarshaling{ + { + Name: "set", + Type: "function", + Inputs: []ABIArgumentMarshaling{ + { + InternalType: "struct WidgetFactory.Widget[][]", + Name: "gears", + Type: "tuple[][]", + Components: []ABIArgumentMarshaling{ + { + InternalType: "string", + Name: "description", + Type: "string", + }, + { + InternalType: "uint256", + Name: "size", + Type: "uint256", + }, + { + InternalType: "bool", + Name: "inUse", + Type: "bool", + }, + }, + }, + }, + Outputs: []ABIArgumentMarshaling{}, + }, + } + + schema := fftypes.JSONAnyPtr(`{"type":"array","details":{"type":"tuple[][]","internalType":"struct WidgetFactory.Widget[][]"},"items":{"type":"array","items":{"type":"object","properties":{"description":{"type":"string","details":{"type":"string","internalType":"string","index":0}},"inUse":{"type":"boolean","details":{"type":"bool","internalType":"bool","index":2}},"size":{"type":"integer","details":{"type":"uint256","internalType":"uint256","index":1}}}}}}`) + expectedFFI := &fftypes.FFI{ + Name: "WidgetTest", + Version: "v0.0.1", + Namespace: "default", + Description: "desc", + Methods: []*fftypes.FFIMethod{ + { + Name: "set", + Params: fftypes.FFIParams{ + { + Name: "gears", + Schema: schema, + }, + }, + Returns: fftypes.FFIParams{}, + }, + }, + Events: []*fftypes.FFIEvent{}, + } + + actualFFI := e.convertABIToFFI("default", "WidgetTest", "v0.0.1", "desc", abi) + assert.NotNil(t, actualFFI) + assert.Equal(t, expectedFFI, actualFFI) +} + +func TestGenerateFFI(t *testing.T) { + e, _ := newTestEthereum() + _, err := e.GenerateFFI(context.Background(), &fftypes.FFIGenerationRequest{ + Name: "Simple", + Version: "v0.0.1", + Description: "desc", + Input: fftypes.JSONAnyPtr(`[]`), + }) + assert.NoError(t, err) +} + +func TestGenerateFFIInlineNamespace(t *testing.T) { + e, _ := newTestEthereum() + ffi, err := e.GenerateFFI(context.Background(), &fftypes.FFIGenerationRequest{ + Name: "Simple", + Version: "v0.0.1", + Description: "desc", + Namespace: "ns1", + Input: fftypes.JSONAnyPtr(`[]`), + }) + assert.NoError(t, err) + assert.Equal(t, ffi.Namespace, "ns1") +} + +func TestGenerateFFIFail(t *testing.T) { + e, _ := newTestEthereum() + _, err := e.GenerateFFI(context.Background(), &fftypes.FFIGenerationRequest{ + Name: "Simple", + Version: "v0.0.1", + Description: "desc", + Input: fftypes.JSONAnyPtr(`{"type": "not an ABI"}`), + }) + assert.Regexp(t, "FF10346", err) +} + +func TestGetFFIType(t *testing.T) { + e, _ := newTestEthereum() + assert.Equal(t, e.getFFIType("string"), "string") + assert.Equal(t, e.getFFIType("address"), "string") + assert.Equal(t, e.getFFIType("byte"), "string") + assert.Equal(t, e.getFFIType("bool"), "boolean") + assert.Equal(t, e.getFFIType("uint256"), "integer") + assert.Equal(t, e.getFFIType("string[]"), "array") + assert.Equal(t, e.getFFIType("tuple"), "object") + assert.Equal(t, e.getFFIType("foobar"), "") +} diff --git a/internal/blockchain/fabric/fabric.go b/internal/blockchain/fabric/fabric.go index 59a27f571c..989c84f135 100644 --- a/internal/blockchain/fabric/fabric.go +++ b/internal/blockchain/fabric/fabric.go @@ -649,3 +649,7 @@ func (f *Fabric) GetFFIParamValidator(ctx context.Context) (fftypes.FFIParamVali // Fabconnect does not require any additional validation beyond "JSON Schema correctness" at this time return nil, nil } + +func (f *Fabric) GenerateFFI(ctx context.Context, generationRequest *fftypes.FFIGenerationRequest) (*fftypes.FFI, error) { + return nil, i18n.NewError(ctx, i18n.MsgFFIGenerationUnsupported) +} diff --git a/internal/blockchain/fabric/fabric_test.go b/internal/blockchain/fabric/fabric_test.go index 7717ab2bff..34b171bd32 100644 --- a/internal/blockchain/fabric/fabric_test.go +++ b/internal/blockchain/fabric/fabric_test.go @@ -1501,3 +1501,14 @@ func TestGetFFIParamValidator(t *testing.T) { _, err := e.GetFFIParamValidator(context.Background()) assert.NoError(t, err) } + +func TestGenerateFFI(t *testing.T) { + e, _ := newTestFabric() + _, err := e.GenerateFFI(context.Background(), &fftypes.FFIGenerationRequest{ + Name: "Simple", + Version: "v0.0.1", + Description: "desc", + Input: fftypes.JSONAnyPtr(`[]`), + }) + assert.Regexp(t, "FF10347", err) +} diff --git a/internal/contracts/manager.go b/internal/contracts/manager.go index 2ac6358587..683435f2a5 100644 --- a/internal/contracts/manager.go +++ b/internal/contracts/manager.go @@ -53,6 +53,7 @@ type Manager interface { GetContractSubscriptionByNameOrID(ctx context.Context, ns, nameOrID string) (*fftypes.ContractSubscription, error) GetContractSubscriptions(ctx context.Context, ns string, filter database.AndFilter) ([]*fftypes.ContractSubscription, *database.FilterResult, error) DeleteContractSubscriptionByNameOrID(ctx context.Context, ns, nameOrID string) error + GenerateFFI(ctx context.Context, ns string, generationRequest *fftypes.FFIGenerationRequest) (*fftypes.FFI, error) } type contractManager struct { @@ -592,3 +593,8 @@ func (cm *contractManager) checkParamSchema(ctx context.Context, input interface } return nil } + +func (cm *contractManager) GenerateFFI(ctx context.Context, ns string, generationRequest *fftypes.FFIGenerationRequest) (*fftypes.FFI, error) { + generationRequest.Namespace = ns + return cm.blockchain.GenerateFFI(ctx, generationRequest) +} diff --git a/internal/contracts/manager_test.go b/internal/contracts/manager_test.go index 477cf498fc..de23c58d26 100644 --- a/internal/contracts/manager_test.go +++ b/internal/contracts/manager_test.go @@ -1898,6 +1898,18 @@ func TestAddJSONSchemaExtension(t *testing.T) { assert.NotNil(t, c) } +func TestGenerateFFI(t *testing.T) { + cm := newTestContractManager() + mbi := cm.blockchain.(*blockchainmocks.Plugin) + mbi.On("GenerateFFI", mock.Anything, mock.Anything).Return(&fftypes.FFI{ + Name: "generated", + }, nil) + ffi, err := cm.GenerateFFI(context.Background(), "default", &fftypes.FFIGenerationRequest{}) + assert.NoError(t, err) + assert.NotNil(t, ffi) + assert.Equal(t, "generated", ffi.Name) +} + type MockFFIParamValidator struct{} func (v MockFFIParamValidator) Compile(ctx jsonschema.CompilerContext, m map[string]interface{}) (jsonschema.ExtSchema, error) { diff --git a/internal/i18n/en_translations.go b/internal/i18n/en_translations.go index da753d466e..a983660f5a 100644 --- a/internal/i18n/en_translations.go +++ b/internal/i18n/en_translations.go @@ -262,4 +262,6 @@ var ( MsgInvalidTXTypeForMessage = ffm("FF10343", "Invalid transaction type for sending a message: %s", 400) MsgGroupRequired = ffm("FF10344", "Group must be set", 400) MsgDBLockFailed = ffm("FF10345", "Database lock failed") + MsgFFIGenerationFailed = ffm("FF10346", "Error generating smart contract interface: %s", 400) + MsgFFIGenerationUnsupported = ffm("FF10347", "Smart contract interface generation is not supported by this blockchain plugin", 400) ) diff --git a/mocks/blockchainmocks/plugin.go b/mocks/blockchainmocks/plugin.go index 17c1a0ec67..6f3d31e4be 100644 --- a/mocks/blockchainmocks/plugin.go +++ b/mocks/blockchainmocks/plugin.go @@ -62,6 +62,29 @@ func (_m *Plugin) DeleteSubscription(ctx context.Context, subscription *fftypes. return r0 } +// GenerateFFI provides a mock function with given fields: ctx, generationRequest +func (_m *Plugin) GenerateFFI(ctx context.Context, generationRequest *fftypes.FFIGenerationRequest) (*fftypes.FFI, error) { + ret := _m.Called(ctx, generationRequest) + + var r0 *fftypes.FFI + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.FFIGenerationRequest) *fftypes.FFI); ok { + r0 = rf(ctx, generationRequest) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*fftypes.FFI) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.FFIGenerationRequest) error); ok { + r1 = rf(ctx, generationRequest) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetFFIParamValidator provides a mock function with given fields: ctx func (_m *Plugin) GetFFIParamValidator(ctx context.Context) (fftypes.FFIParamValidator, error) { ret := _m.Called(ctx) diff --git a/mocks/contractmocks/manager.go b/mocks/contractmocks/manager.go index af8fca637c..546d058fcf 100644 --- a/mocks/contractmocks/manager.go +++ b/mocks/contractmocks/manager.go @@ -100,6 +100,29 @@ func (_m *Manager) DeleteContractSubscriptionByNameOrID(ctx context.Context, ns return r0 } +// GenerateFFI provides a mock function with given fields: ctx, ns, generationRequest +func (_m *Manager) GenerateFFI(ctx context.Context, ns string, generationRequest *fftypes.FFIGenerationRequest) (*fftypes.FFI, error) { + ret := _m.Called(ctx, ns, generationRequest) + + var r0 *fftypes.FFI + if rf, ok := ret.Get(0).(func(context.Context, string, *fftypes.FFIGenerationRequest) *fftypes.FFI); ok { + r0 = rf(ctx, ns, generationRequest) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*fftypes.FFI) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, *fftypes.FFIGenerationRequest) error); ok { + r1 = rf(ctx, ns, generationRequest) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetContractAPI provides a mock function with given fields: ctx, httpServerURL, ns, apiName func (_m *Manager) GetContractAPI(ctx context.Context, httpServerURL string, ns string, apiName string) (*fftypes.ContractAPI, error) { ret := _m.Called(ctx, httpServerURL, ns, apiName) diff --git a/pkg/blockchain/plugin.go b/pkg/blockchain/plugin.go index eba9bed8aa..477b3a22c7 100644 --- a/pkg/blockchain/plugin.go +++ b/pkg/blockchain/plugin.go @@ -61,6 +61,9 @@ type Plugin interface { // GetFFIParamValidator returns a blockchain-plugin-specific validator for FFIParams and their JSON Schema GetFFIParamValidator(ctx context.Context) (fftypes.FFIParamValidator, error) + + // GenerateFFI returns an FFI from a blockchain specific interface format e.g. an Ethereum ABI + GenerateFFI(ctx context.Context, generationRequest *fftypes.FFIGenerationRequest) (*fftypes.FFI, error) } // Callbacks is the interface provided to the blockchain plugin, to allow it to pass events back to firefly. diff --git a/pkg/fftypes/ffi.go b/pkg/fftypes/ffi.go index f72ff1afd0..81357d2a50 100644 --- a/pkg/fftypes/ffi.go +++ b/pkg/fftypes/ffi.go @@ -41,9 +41,9 @@ type FFI struct { ID *UUID `json:"id,omitempty"` Message *UUID `json:"message,omitempty"` Namespace string `json:"namespace,omitempty"` - Name string `json:"name,omitempty"` + Name string `json:"name"` Description string `json:"description"` - Version string `json:"version,omitempty"` + Version string `json:"version"` Methods []*FFIMethod `json:"methods,omitempty"` Events []*FFIEvent `json:"events,omitempty"` } @@ -80,6 +80,14 @@ type FFIParam struct { type FFIParams []*FFIParam +type FFIGenerationRequest struct { + Namespace string `json:"namespace,omitempty"` + Name string `json:"name"` + Description string `json:"description"` + Version string `json:"version"` + Input *JSONAny `json:"input"` +} + func (f *FFI) Validate(ctx context.Context, existing bool) (err error) { if err = ValidateFFNameField(ctx, f.Namespace, "namespace"); err != nil { return err