diff --git a/samples/application/ccapi/CCAPIFlow.png b/samples/application/ccapi/CCAPIFlow.png new file mode 100644 index 000000000..17e8dfcf7 Binary files /dev/null and b/samples/application/ccapi/CCAPIFlow.png differ diff --git a/samples/application/ccapi/Dockerfile b/samples/application/ccapi/Dockerfile new file mode 100644 index 000000000..725748381 --- /dev/null +++ b/samples/application/ccapi/Dockerfile @@ -0,0 +1,61 @@ +# Use an official Golang runtime as a parent image +FROM golang:1.19-alpine AS build + +ENV PATH="${PATH}:/usr/bin/" + +RUN apk update + +RUN apk add \ + docker \ + openrc \ + git \ + gcc \ + gcompat \ + libc-dev \ + libc6-compat \ + libstdc++ && \ + ln -s /lib/libc.so.6 /usr/lib/libresolv.so.2 + +# Set the working directory to /rest-server +WORKDIR /rest-server + +# Copy the go.mod and go.sum files for dependency management +COPY go.mod go.sum ./ + +# Install go dependencies +RUN go mod download + +RUN go mod vendor + +# Copy the current directory contents into the container at /rest-server +COPY . . + +# Build the Go ccapi +RUN go build -o ccapi + +# Use an official Alpine runtime as a parent image +FROM alpine:latest + +ENV PATH="${PATH}:/usr/bin/" + +RUN apk update + +RUN apk add \ + docker \ + openrc \ + git \ + gcc \ + gcompat \ + libc-dev \ + libc6-compat \ + libstdc++ && \ + ln -s /lib/libc.so.6 /usr/lib/libresolv.so.2 + +# Set the working directory to /rest-server +WORKDIR /rest-server + +# Copy the ccapi binary from the build container to the current directory in the Alpine container +COPY --from=build /rest-server/ccapi /usr/bin/ccapi + +# Run the ccapi binary +CMD ["ccapi"] \ No newline at end of file diff --git a/samples/application/ccapi/README.md b/samples/application/ccapi/README.md new file mode 100644 index 000000000..8f1fb7f86 --- /dev/null +++ b/samples/application/ccapi/README.md @@ -0,0 +1,30 @@ +# CCAPI - A web server developed to interface with CC-tools chaincode + +## Motivation + +As continuation to the [cc-tools-demo]() tutorial on how to integrate the cc-tools project with FPC chaincodes, we start by utilizing another powerful solution offered by cc-tools is the CCAPI. It's a complete web server that simplifies the communication with the peers and Fabric components to replace the need to deal with CLI applications. + +## Architecture + +The following diagram explains the process where we modified the API server developed for a demo on cc-tools ([CCAPI](https://github.com/hyperledger-labs/cc-tools-demo/tree/main/ccapi)) and modified it to communicate with FPC code. + +The transaction client invocation process, as illustrated in the diagram, consists of several key steps that require careful integration between FPC and cc-tools. + +1. Step 1-2: The API server is listening for requests on a specified port over an HTTP channel and sends it to the handler. +2. Step 3: The handler starts by determining the appropriate transaction invocation based on the requested endpoint and calling the corresponding chaincode API. +3. Step 4: The chaincode API is responsible for parsing and ensuring the payload is correctly parsed into a format that is FPC-friendly. This parsing step is crucial, as it prepares the data to meet FPC’s privacy and security requirements before it reaches the peer. +4. Step 5: FPCUtils is the step where the actual transaction invocation happens and it follows the steps explained in the previous diagram as it builds on top of the FPC Client SDK. + +![CCAPIFlow](./CCAPIFlow.png) + +## User Experience + +CCAPI is using docker and docker-compose for spinning up all the required components needed to work. + +Have a look at the [fpc-docker-compose.yaml](./fpc-docker-compose.yaml) to see how we use different env vars. Most of these environment variables are required by any client application to work and communicate with FPC. If you followed the [cc-tools-demo](../../chaincode/cc-tools-demo/README.md) tutorial, the values should be the same. + +Start by running `docker-compose -f fpc-docker-compose.yaml up` then go to the browser and type `localhost:80` to open the swagger api and start executing functions. + +## Future work + +CCAPI have another component for the dashboard frontend application but it's not yet utilized with diff --git a/samples/application/ccapi/chaincode/event.go b/samples/application/ccapi/chaincode/event.go new file mode 100644 index 000000000..0db0f9090 --- /dev/null +++ b/samples/application/ccapi/chaincode/event.go @@ -0,0 +1,145 @@ +package chaincode + +import ( + "encoding/json" + "fmt" + "log" + "os" + "regexp" + + "github.com/hyperledger-labs/ccapi/common" + ev "github.com/hyperledger/fabric-sdk-go/pkg/client/event" + "github.com/hyperledger/fabric-sdk-go/pkg/common/providers/fab" +) + +func getEventClient(channelName string) (*ev.Client, error) { + // create channel manager + fabMngr, err := common.NewFabricChClient(channelName, os.Getenv("USER"), os.Getenv("ORG")) + if err != nil { + return nil, err + } + + // Create event client + ec, err := ev.New(fabMngr.Provider, ev.WithBlockEvents()) + if err != nil { + return nil, err + } + + return ec, nil +} + +func WaitForEvent(channelName, ccName, eventName string, fn func(*fab.CCEvent)) { + ec, err := getEventClient(channelName) + if err != nil { + log.Println("error getting event client: ", err) + return + } + + for { + // Register chaincode event + registration, notifier, err := ec.RegisterChaincodeEvent(ccName, eventName) + if err != nil { + log.Println("error registering chaincode event: ", err) + return + } + + // Execute handler function on event notification + ccEvent := <-notifier + fmt.Printf("Received CC event: %v\n", ccEvent) + fn(ccEvent) + + ec.Unregister(registration) + } +} + +func HandleEvent(channelName, ccName string, event EventHandler) { + ec, err := getEventClient(channelName) + if err != nil { + log.Println("error getting event client: ", err) + return + } + + for { + // Register chaincode event + registration, notifier, err := ec.RegisterChaincodeEvent(ccName, event.Tag) + if err != nil { + log.Println("error registering chaincode event: ", err) + return + } + + // Execute handler function on event notification + ccEvent := <-notifier + fmt.Printf("Received CC event: %v\n", ccEvent) + event.Execute(ccEvent) + + ec.Unregister(registration) + } +} + +func RegisterForEvents() { + // Get registered events on the chaincode + res, _, err := Invoke(os.Getenv("CHANNEL"), os.Getenv("CCNAME"), "getEvents", os.Getenv("USER"), nil, nil) + if err != nil { + fmt.Println("error registering for events: ", err) + return + } + + var events []interface{} + nerr := json.Unmarshal(res.Payload, &events) + if nerr != nil { + fmt.Println("error unmarshalling events: ", nerr) + return + } + + msp := common.GetClientOrg() + "MSP" + + for _, event := range events { + eventMap := event.(map[string]interface{}) + receiverArr, ok := eventMap["receivers"] + + isReceiver := true + // Verify if the MSP is a receiver for the event + if ok { + isReceiver = false + receivers := receiverArr.([]interface{}) + for _, r := range receivers { + receiver := r.(string) + + if len(receiver) <= 1 { + continue + } + if receiver[0] == '$' { + match, err := regexp.MatchString(receiver[1:], msp) + if err != nil { + fmt.Println("error matching regexp: ", err) + return + } + if match { + isReceiver = true + break + } + } else { + if receiver == msp { + isReceiver = true + break + } + } + } + } + + if isReceiver { + eventHandler := EventHandler{ + Tag: eventMap["tag"].(string), + Type: EventType(eventMap["type"].(float64)), + Transaction: eventMap["transaction"].(string), + Channel: eventMap["channel"].(string), + Chaincode: eventMap["chaincode"].(string), + Label: eventMap["label"].(string), + BaseLog: eventMap["baseLog"].(string), + ReadOnly: eventMap["readOnly"].(bool), + } + + go HandleEvent(os.Getenv("CHANNEL"), os.Getenv("CCNAME"), eventHandler) + } + } +} diff --git a/samples/application/ccapi/chaincode/eventHandler.go b/samples/application/ccapi/chaincode/eventHandler.go new file mode 100644 index 000000000..07b72f988 --- /dev/null +++ b/samples/application/ccapi/chaincode/eventHandler.go @@ -0,0 +1,97 @@ +package chaincode + +import ( + b64 "encoding/base64" + "encoding/json" + "fmt" + "os" + + "github.com/hyperledger/fabric-sdk-go/pkg/common/providers/fab" +) + +type EventType float64 + +const ( + EventLog EventType = iota + EventTransaction + EventCustom +) + +type EventHandler struct { + Tag string + Label string + Type EventType + Transaction string + Channel string + Chaincode string + BaseLog string + ReadOnly bool +} + +func (event EventHandler) Execute(ccEvent *fab.CCEvent) { + if len(event.BaseLog) > 0 { + fmt.Println(event.BaseLog) + } + + if event.Type == EventLog { + var logStr string + nerr := json.Unmarshal(ccEvent.Payload, &logStr) + if nerr != nil { + fmt.Println("error unmarshalling log: ", nerr) + return + } + + if len(logStr) > 0 { + fmt.Println("Event '", event.Label, "' log: ", logStr) + } + } else if event.Type == EventTransaction { + ch := os.Getenv("CHANNEL") + if event.Channel != "" { + ch = event.Channel + } + cc := os.Getenv("CCNAME") + if event.Chaincode != "" { + cc = event.Chaincode + } + + res, _, err := Invoke(ch, cc, event.Transaction, os.Getenv("USER"), [][]byte{ccEvent.Payload}, nil) + if err != nil { + fmt.Println("error invoking transaction: ", err) + return + } + + var response map[string]interface{} + nerr := json.Unmarshal(res.Payload, &response) + if nerr != nil { + fmt.Println("error unmarshalling response: ", nerr) + return + } + fmt.Println("Response: ", response) + } else if event.Type == EventCustom { + // Encode payload to base64 + b64Encode := b64.StdEncoding.EncodeToString([]byte(ccEvent.Payload)) + + args, ok := json.Marshal(map[string]interface{}{ + "eventTag": event.Tag, + "payload": b64Encode, + }) + if ok != nil { + fmt.Println("failed to encode args to JSON format") + return + } + + // Invoke tx + txName := "executeEvent" + if event.ReadOnly { + txName = "runEvent" + } + + _, _, err := Invoke(os.Getenv("CHANNEL"), os.Getenv("CCNAME"), txName, os.Getenv("USER"), [][]byte{args}, nil) + if err != nil { + fmt.Println("error invoking transaction: ", err) + return + } + } else { + fmt.Println("Event type not supported") + } +} diff --git a/samples/application/ccapi/chaincode/invoke.go b/samples/application/ccapi/chaincode/invoke.go new file mode 100644 index 000000000..b9030597a --- /dev/null +++ b/samples/application/ccapi/chaincode/invoke.go @@ -0,0 +1,37 @@ +package chaincode + +import ( + "net/http" + "os" + + "github.com/hyperledger-labs/ccapi/common" + "github.com/hyperledger/fabric-sdk-go/pkg/client/channel" + "github.com/hyperledger/fabric-sdk-go/pkg/common/errors/retry" +) + +func Invoke(channelName, ccName, txName, user string, txArgs [][]byte, transientRequest []byte) (*channel.Response, int, error) { + // create channel manager + fabMngr, err := common.NewFabricChClient(channelName, user, os.Getenv("ORG")) + if err != nil { + return nil, http.StatusInternalServerError, err + } + + // Execute chaincode with channel's client + rq := channel.Request{ChaincodeID: ccName, Fcn: txName} + if len(txArgs) > 0 { + rq.Args = txArgs + } + + if len(transientRequest) != 0 { + transientMap := make(map[string][]byte) + transientMap["@request"] = transientRequest + rq.TransientMap = transientMap + } + + res, err := fabMngr.Client.Execute(rq, channel.WithRetry(retry.DefaultChannelOpts)) + if err != nil { + return nil, extractStatusCode(err.Error()), err + } + + return &res, http.StatusOK, nil +} diff --git a/samples/application/ccapi/chaincode/invokeFPC.go b/samples/application/ccapi/chaincode/invokeFPC.go new file mode 100644 index 000000000..d74b29b91 --- /dev/null +++ b/samples/application/ccapi/chaincode/invokeFPC.go @@ -0,0 +1,24 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package chaincode + +import ( + "net/http" + + "github.com/hyperledger-labs/ccapi/common" +) + +func InvokeFpc(channelName string, chaincodeName string, txname string, args [][]byte) ([]byte, int, error) { + stringArgs := make([]string, len(args)) + for i, b := range args { + stringArgs[i] = string(b) + } + + client := common.NewFpcClient(channelName, chaincodeName) + res := client.Invoke(txname, stringArgs[0:]...) + return []byte(res), http.StatusOK, nil +} diff --git a/samples/application/ccapi/chaincode/invokeFPCDefault.go b/samples/application/ccapi/chaincode/invokeFPCDefault.go new file mode 100644 index 000000000..11b2a023c --- /dev/null +++ b/samples/application/ccapi/chaincode/invokeFPCDefault.go @@ -0,0 +1,24 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package chaincode + +import ( + "net/http" + + "github.com/hyperledger-labs/ccapi/common" +) + +func InvokeFpcDefault(txname string, args [][]byte) ([]byte, int, error) { + stringArgs := make([]string, len(args)) + for i, b := range args { + stringArgs[i] = string(b) + } + + client := common.NewDefaultFpcClient() + res := client.Invoke(txname, stringArgs[0:]...) + return []byte(res), http.StatusOK, nil +} diff --git a/samples/application/ccapi/chaincode/invokeGateway.go b/samples/application/ccapi/chaincode/invokeGateway.go new file mode 100644 index 000000000..2d2edb976 --- /dev/null +++ b/samples/application/ccapi/chaincode/invokeGateway.go @@ -0,0 +1,61 @@ +package chaincode + +import ( + "os" + + "github.com/hyperledger-labs/ccapi/common" + "github.com/hyperledger/fabric-gateway/pkg/client" + "github.com/pkg/errors" +) + +func InvokeGateway(channelName, chaincodeName, txName, user string, args []string, transientArgs []byte, endorsingOrgs []string) ([]byte, error) { + // Gateway endpoint + endpoint := os.Getenv("FABRIC_GATEWAY_ENDPOINT") + + // Create client grpc connection + grpcConn, err := common.CreateGrpcConnection(endpoint) + if err != nil { + return nil, errors.Wrap(err, "failed to create grpc connection") + } + defer grpcConn.Close() + + // Create gateway connection + gw, err := common.CreateGatewayConnection(grpcConn, user) + if err != nil { + return nil, errors.Wrap(err, "failed to create gateway connection") + } + defer gw.Close() + + // Obtain smart contract deployed on the network. + network := gw.GetNetwork(channelName) + contract := network.GetContract(chaincodeName) + + // Make transient request + transientMap := make(map[string][]byte) + transientMap["@request"] = transientArgs + + // Invoke transaction + if transientArgs != nil && len(endorsingOrgs) > 0 { + return contract.Submit(txName, + client.WithArguments(args...), + client.WithTransient(transientMap), + client.WithEndorsingOrganizations(endorsingOrgs...), + ) + } + + if transientArgs != nil { + return contract.Submit(txName, + client.WithArguments(args...), + client.WithTransient(transientMap), + ) + } + + if len(endorsingOrgs) > 0 { + return contract.Submit(txName, + client.WithArguments(args...), + client.WithEndorsingOrganizations(endorsingOrgs...), + ) + } + + return contract.SubmitTransaction(txName, args...) +} diff --git a/samples/application/ccapi/chaincode/query.go b/samples/application/ccapi/chaincode/query.go new file mode 100644 index 000000000..a069c7be6 --- /dev/null +++ b/samples/application/ccapi/chaincode/query.go @@ -0,0 +1,32 @@ +package chaincode + +import ( + "net/http" + "os" + + "github.com/hyperledger-labs/ccapi/common" + "github.com/hyperledger/fabric-sdk-go/pkg/client/channel" + "github.com/hyperledger/fabric-sdk-go/pkg/common/errors/retry" +) + +func Query(channelName, ccName, txName, user string, txArgs [][]byte) (*channel.Response, int, error) { + // create channel manager + fabMngr, err := common.NewFabricChClient(channelName, user, os.Getenv("ORG")) + if err != nil { + return nil, http.StatusInternalServerError, err + } + + // Execute chaincode with channel's client + rq := channel.Request{ChaincodeID: ccName, Fcn: txName} + if len(txArgs) > 0 { + rq.Args = txArgs + } + + res, err := fabMngr.Client.Query(rq, channel.WithRetry(retry.DefaultChannelOpts)) + if err != nil { + status := extractStatusCode(err.Error()) + return nil, status, err + } + + return &res, http.StatusOK, nil +} diff --git a/samples/application/ccapi/chaincode/queryFPC.go b/samples/application/ccapi/chaincode/queryFPC.go new file mode 100644 index 000000000..7f8c8e804 --- /dev/null +++ b/samples/application/ccapi/chaincode/queryFPC.go @@ -0,0 +1,24 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package chaincode + +import ( + "net/http" + + "github.com/hyperledger-labs/ccapi/common" +) + +func QueryFpc(chaincodeName string, channelName string, txName string, args [][]byte) ([]byte, int, error) { + stringArgs := make([]string, len(args)) + for i, b := range args { + stringArgs[i] = string(b) + } + + client := common.NewFpcClient(chaincodeName, channelName) + res := client.Query(txName, stringArgs[0:]...) + return []byte(res), http.StatusOK, nil +} diff --git a/samples/application/ccapi/chaincode/queryFPCDefault.go b/samples/application/ccapi/chaincode/queryFPCDefault.go new file mode 100644 index 000000000..8cc7d41d9 --- /dev/null +++ b/samples/application/ccapi/chaincode/queryFPCDefault.go @@ -0,0 +1,24 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package chaincode + +import ( + "net/http" + + "github.com/hyperledger-labs/ccapi/common" +) + +func QueryFpcDefault(txName string, args [][]byte) ([]byte, int, error) { + stringArgs := make([]string, len(args)) + for i, b := range args { + stringArgs[i] = string(b) + } + + client := common.NewDefaultFpcClient() + res := client.Query(txName, stringArgs[0:]...) + return []byte(res), http.StatusOK, nil +} diff --git a/samples/application/ccapi/chaincode/queryGateway.go b/samples/application/ccapi/chaincode/queryGateway.go new file mode 100644 index 000000000..96e9914f2 --- /dev/null +++ b/samples/application/ccapi/chaincode/queryGateway.go @@ -0,0 +1,38 @@ +package chaincode + +import ( + "os" + + "github.com/hyperledger-labs/ccapi/common" + "github.com/pkg/errors" +) + +func QueryGateway(channelName, chaincodeName, txName, user string, args []string) ([]byte, error) { + // Gateway endpoint + endpoint := os.Getenv("FABRIC_GATEWAY_ENDPOINT") + + // Create client grpc connection + grpcConn, err := common.CreateGrpcConnection(endpoint) + if err != nil { + return nil, errors.Wrap(err, "failed to create grpc connection") + } + defer grpcConn.Close() + + // Create gateway connection + gw, err := common.CreateGatewayConnection(grpcConn, user) + if err != nil { + return nil, errors.Wrap(err, "failed to create gateway connection") + } + defer gw.Close() + + // Obtain smart contract deployed on the network. + network := gw.GetNetwork(channelName) + contract := network.GetContract(chaincodeName) + + // Query transaction + if len(args) == 0 { + return contract.EvaluateTransaction(txName) + } + + return contract.EvaluateTransaction(txName, args...) +} diff --git a/samples/application/ccapi/chaincode/utils.go b/samples/application/ccapi/chaincode/utils.go new file mode 100644 index 000000000..6132c9cdd --- /dev/null +++ b/samples/application/ccapi/chaincode/utils.go @@ -0,0 +1,26 @@ +package chaincode + +import ( + "fmt" + "net/http" + "regexp" + "strconv" +) + +func extractStatusCode(msg string) int { + re := regexp.MustCompile(`Code:\s*\((\d+)\)`) + + matches := re.FindStringSubmatch(msg) + if len(matches) == 0 { + fmt.Println("No status code found in message") + return http.StatusInternalServerError + } + + statusCode, err := strconv.Atoi(matches[1]) + if err != nil { + fmt.Println("Failed to parse string to int when extracting status code") + return http.StatusInternalServerError + } + + return statusCode +} diff --git a/samples/application/ccapi/common/abort.go b/samples/application/ccapi/common/abort.go new file mode 100644 index 000000000..669702b84 --- /dev/null +++ b/samples/application/ccapi/common/abort.go @@ -0,0 +1,29 @@ +package common + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +func Abort(c *gin.Context, status int, err error) { + c.JSON(status, gin.H{ + "status": status, + "error": err.Error(), + }) + c.Error(err) +} + +func Respond(c *gin.Context, res interface{}, status int, err error) { + if err != nil { + c.JSON(status, gin.H{ + "response": res, + "status": status, + "error": err.Error(), + }) + c.Error(err) + return + } + + c.JSON(http.StatusOK, res) +} diff --git a/samples/application/ccapi/common/fabsdk.go b/samples/application/ccapi/common/fabsdk.go new file mode 100644 index 000000000..9d1f1fde5 --- /dev/null +++ b/samples/application/ccapi/common/fabsdk.go @@ -0,0 +1,193 @@ +package common + +import ( + "fmt" + "log" + "os" + + "github.com/hyperledger/fabric-sdk-go/pkg/common/providers/context" + "github.com/hyperledger/fabric-sdk-go/pkg/core/config" + "github.com/hyperledger/fabric-sdk-go/pkg/fabsdk" +) + +type sdk struct { + // sdk belongs to org defined in the configsdk.yaml file + Sdk *fabsdk.FabricSDK + Path string +} + +// CreateContext allows creation of transactions using the supplied identity as the credential. +func (s *sdk) CreateClientContext(options ...fabsdk.ContextOption) context.ClientProvider { + return s.Sdk.Context(options...) +} + +func (s *sdk) CreateChannelContext(channelName string, options ...fabsdk.ContextOption) context.ChannelProvider { + return s.Sdk.ChannelContext(channelName, options...) +} + +// Log config path which sdk was created +func (s *sdk) LogPath() { + log.Printf("sdk created from '%s'", s.Path) +} + +// Singleton sdk instance +var instance *sdk + +// GetSDK returns a fabric sdk instance. +// +// A new sdk is created if: +// - it is the first time it is beeing used, or +// - new sdk options are given +// +// Otherwise, it returns the one previoulsy created. +// If options are given, the new sdk is not a singleton, and must +// be closed by whoever invoked it. +// +// The configsdk file can be set via environment variable and defaults +// to './config/configsdk.yaml' +func GetSDK(sdkOpts ...fabsdk.Option) (*sdk, error) { + + // return new sdk instance if sdkOpts are given. + // user must close sdk + if len(sdkOpts) != 0 { + cfgPath := getCfgPath() + configOpt := config.FromFile(cfgPath) + s, err := fabsdk.New(configOpt, sdkOpts...) + + return &sdk{ + Sdk: s, + Path: cfgPath, + }, err + } + + if instance == nil { + cfgPath := getCfgPath() + configOpt := config.FromFile(cfgPath) + s, err := fabsdk.New(configOpt) + if err != nil { + return nil, err + } + + instance = &sdk{ + Sdk: s, + Path: cfgPath, + } + instance.LogPath() + } + + return instance, nil +} + +// getCfgPath parses path for the configsdk +// from environmet, and defaults to './config/configsdk.yaml' +func getCfgPath() (cfgPath string) { + cfgPath = os.Getenv("SDK_PATH") + if cfgPath == "" { + cfgPath = "./config/configsdk.yaml" + } + return +} + +// GetClientOrg returns the name of the client organization +func GetClientOrg() string { + sdk, err := GetSDK() + if err != nil { + return "" + } + + cfg, err := sdk.Sdk.Config() + if err != nil { + return "" + } + + i, ok := cfg.Lookup("client") + if !ok { + return "" + } + m, ok := i.(map[string]interface{}) + if !ok { + return "" + } + + org := m["organization"] + orgName, ok := org.(string) + if !ok { + return "" + } + + return orgName +} + +func GetCryptoPath() string { + sdk, err := GetSDK() + if err != nil { + return "" + } + + cfg, err := sdk.Sdk.Config() + if err != nil { + return "" + } + + i, ok := cfg.Lookup("client.cryptoconfig.path") + if !ok { + return "" + } + basePath, _ := i.(string) + + i, ok = cfg.Lookup(fmt.Sprintf("organizations.%s.cryptoPath", os.Getenv("ORG"))) + if !ok { + return "" + } + + certPath, _ := i.(string) + return basePath + "/" + certPath +} + +func GetTLSCACert() string { + sdk, err := GetSDK() + if err != nil { + return "" + } + + cfg, err := sdk.Sdk.Config() + if err != nil { + return "" + } + + i, ok := cfg.Lookup("client.tlsCerts.client.cacertfile") + if !ok { + return "" + } + + certPath, _ := i.(string) + return certPath +} + +func GetMSPID() string { + sdk, err := GetSDK() + if err != nil { + return "" + } + + cfg, err := sdk.Sdk.Config() + if err != nil { + return "" + } + + i, ok := cfg.Lookup(fmt.Sprintf("organizations.%s.mspid", os.Getenv("ORG"))) + if !ok { + return "" + } + + mspid, _ := i.(string) + return mspid +} + +// Closes sdk instance if it was created +func CloseSDK() { + if instance != nil { + instance.Sdk.Close() + instance = nil + } +} diff --git a/samples/application/ccapi/common/fpc.go b/samples/application/ccapi/common/fpc.go new file mode 100644 index 000000000..d752c8b90 --- /dev/null +++ b/samples/application/ccapi/common/fpc.go @@ -0,0 +1,68 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package common + +import ( + "fmt" + "os" + "strconv" + + pkgFpc "github.com/hyperledger-labs/ccapi/fpcUtils" +) + +var ( + defaultFpcConfig *pkgFpc.Config +) + +func InitFpcConfig() { + + getStrEnv := func(key string) string { + val := os.Getenv(key) + if val == "" { + panic(fmt.Sprintf("%s not set", key)) + } + return val + } + + getBoolEnv := func(key string) bool { + val := getStrEnv(key) + ret, err := strconv.ParseBool(val) + if err != nil { + if val == "" { + panic(fmt.Sprintf("invalid bool value for %s", key)) + } + } + return ret + } + + defaultFpcConfig = &pkgFpc.Config{ + CorePeerAddress: getStrEnv("CORE_PEER_ADDRESS"), + CorePeerId: getStrEnv("CORE_PEER_ID"), + CorePeerLocalMSPID: getStrEnv("CORE_PEER_LOCALMSPID"), + CorePeerMSPConfigPath: getStrEnv("CORE_PEER_MSPCONFIGPATH"), + CorePeerTLSCertFile: getStrEnv("CORE_PEER_TLS_CERT_FILE"), + CorePeerTLSEnabled: getBoolEnv("CORE_PEER_TLS_ENABLED"), + CorePeerTLSKeyFile: getStrEnv("CORE_PEER_TLS_KEY_FILE"), + CorePeerTLSRootCertFile: getStrEnv("CORE_PEER_TLS_ROOTCERT_FILE"), + OrdererCA: getStrEnv("ORDERER_CA"), + ChaincodeId: getStrEnv("CCNAME"), + ChannelId: getStrEnv("CHANNEL"), + GatewayConfigPath: getStrEnv("GATEWAY_CONFIG"), + } + +} + +func NewDefaultFpcClient() *pkgFpc.Client { + return pkgFpc.NewClient(defaultFpcConfig) +} + +func NewFpcClient(channelName string, chaincodeName string) *pkgFpc.Client { + fpcConfig := defaultFpcConfig + fpcConfig.ChannelId = channelName + fpcConfig.ChaincodeId = chaincodeName + return pkgFpc.NewClient(fpcConfig) +} diff --git a/samples/application/ccapi/common/gateway.go b/samples/application/ccapi/common/gateway.go new file mode 100644 index 000000000..16c6decbe --- /dev/null +++ b/samples/application/ccapi/common/gateway.go @@ -0,0 +1,198 @@ +package common + +import ( + "context" + "crypto/x509" + "fmt" + "net/http" + "os" + "regexp" + "strconv" + "strings" + "time" + + "github.com/hyperledger/fabric-gateway/pkg/client" + "github.com/hyperledger/fabric-gateway/pkg/identity" + "github.com/hyperledger/fabric-protos-go-apiv2/gateway" + "github.com/pkg/errors" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/status" +) + +var ( + gatewayTLSCredentials *credentials.TransportCredentials +) + +func CreateGrpcConnection(endpoint string) (*grpc.ClientConn, error) { + // Check TLS credential was created + if gatewayTLSCredentials == nil { + gatewayServerName := os.Getenv("FABRIC_GATEWAY_NAME") + + cred, err := createTransportCredential(GetTLSCACert(), gatewayServerName) + if err != nil { + return nil, errors.Wrap(err, "failed to create tls credentials") + } + + gatewayTLSCredentials = &cred + } + + // Create client grpc connection + return grpc.Dial(endpoint, grpc.WithTransportCredentials(*gatewayTLSCredentials)) +} + +func CreateGatewayConnection(grpcConn *grpc.ClientConn, user string) (*client.Gateway, error) { + // Create identity + id, err := newIdentity(getSignCert(user), GetMSPID()) + if err != nil { + return nil, errors.Wrap(err, "failed to create new identity") + } + gatewayId := id + + // Create sign function + sign, err := newSign(getSignKey(user)) + if err != nil { + return nil, errors.Wrap(err, "failed to create new sign function") + } + + gatewaySign := sign + + // Create a Gateway connection for a specific client identity. + return client.Connect( + gatewayId, + client.WithSign(gatewaySign), + client.WithClientConnection(grpcConn), + + // Default timeouts for different gRPC calls + client.WithEvaluateTimeout(5*time.Second), + client.WithEndorseTimeout(15*time.Second), + client.WithSubmitTimeout(5*time.Second), + client.WithCommitStatusTimeout(1*time.Minute), + ) +} + +// Create transport credential +func createTransportCredential(tlsCertPath, serverName string) (credentials.TransportCredentials, error) { + certificate, err := loadCertificate(tlsCertPath) + if err != nil { + return nil, err + } + + certPool := x509.NewCertPool() + certPool.AddCert(certificate) + return credentials.NewClientTLSFromCert(certPool, serverName), nil +} + +// Creates a client identity for a gateway connection using an X.509 certificate. +func newIdentity(certPath, mspID string) (*identity.X509Identity, error) { + certificate, err := loadCertificate(certPath) + if err != nil { + return nil, err + } + + id, err := identity.NewX509Identity(mspID, certificate) + if err != nil { + return nil, err + } + + return id, nil +} + +// Creates a function that generates a digital signature from a message digest using a private key. +func newSign(keyPath string) (identity.Sign, error) { + privateKeyPEM, err := os.ReadFile(keyPath) + if err != nil { + return nil, errors.Wrap(err, "failed to read private key file") + } + + privateKey, err := identity.PrivateKeyFromPEM(privateKeyPEM) + if err != nil { + return nil, err + } + + sign, err := identity.NewPrivateKeySign(privateKey) + if err != nil { + return nil, errors.Wrap(err, "failed to create signer function") + } + + return sign, nil +} + +// Returns error and status code +func ParseError(err error) (error, int) { + var errMsg string + + switch err := err.(type) { + case *client.EndorseError: + errMsg = "endorse error for transaction" + case *client.SubmitError: + errMsg = "submit error for transaction" + case *client.CommitStatusError: + if errors.Is(err, context.DeadlineExceeded) { + errMsg = "timeout waiting for transaction commit status" + } else { + errMsg = "error obtaining commit status for transaction" + } + case *client.CommitError: + errMsg = "transaction failed to commit" + default: + errMsg = "unexpected error type:" + err.Error() + } + + statusErr := status.Convert(err) + + details := statusErr.Details() + if len(details) == 0 { + return errors.New(errMsg), http.StatusInternalServerError + } + + for _, detail := range details { + switch detail := detail.(type) { + case *gateway.ErrorDetail: + status, msg := extractStatusAndMessage(detail.Message) + return errors.New(msg), status + } + } + + return errors.New(errMsg), http.StatusInternalServerError +} + +func extractStatusAndMessage(msg string) (int, string) { + pattern := `chaincode response (\b(\d{3})\b), ` + reg := regexp.MustCompile(pattern) + matches := reg.FindStringSubmatch(msg) + + if len(matches) == 0 { + return http.StatusInternalServerError, msg + } + + errMsg := strings.Replace(msg, matches[0], "", 1) + status, err := strconv.Atoi(matches[1]) + if err != nil { + status = http.StatusInternalServerError + } + + return status, errMsg +} + +func loadCertificate(filename string) (*x509.Certificate, error) { + certificatePEM, err := os.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("failed to read certificate file: %w", err) + } + return identity.CertificateFromPEM(certificatePEM) +} + +func getSignCert(user string) string { + cryptoPath := GetCryptoPath() + filename := user + "@" + os.Getenv("ORG") + "." + os.Getenv("DOMAIN") + "-cert.pem" + + return strings.Replace(cryptoPath, "{username}", user, 1) + "/signcerts/" + filename +} + +func getSignKey(user string) string { + cryptoPath := GetCryptoPath() + filename := "priv_sk" + + return strings.Replace(cryptoPath, "{username}", user, 1) + "/keystore/" + filename +} diff --git a/samples/application/ccapi/common/resmgnt.go b/samples/application/ccapi/common/resmgnt.go new file mode 100644 index 000000000..301b8761e --- /dev/null +++ b/samples/application/ccapi/common/resmgnt.go @@ -0,0 +1,110 @@ +package common + +import ( + "github.com/hyperledger/fabric-sdk-go/pkg/client/channel" + "github.com/hyperledger/fabric-sdk-go/pkg/client/ledger" + "github.com/hyperledger/fabric-sdk-go/pkg/client/resmgmt" + "github.com/hyperledger/fabric-sdk-go/pkg/common/providers/context" + "github.com/hyperledger/fabric-sdk-go/pkg/fabsdk" +) + +type fabricResmgtmClient struct { + Provider context.ClientProvider + Client *resmgmt.Client +} +type fabricChannelClient struct { + Provider context.ChannelProvider + Client *channel.Client +} +type fabricLedgerClient struct { + Provider context.ChannelProvider + Client *ledger.Client +} + +// Returns a client which has access resource management capabilities +// These are, but not limited to: create channel, query cfg, cc lifecycle... +// +// Function works like this: +// 1. Get sdk +// 2. Use sdk to create a ClientProvider () +// 3. From client provider create resmgmt Client +// You can then use this .Client to call for specific functionalities +func NewFabricResmgmtClient(orgName, userName string, opts ...resmgmt.ClientOption) (*fabricResmgtmClient, error) { + sdk, err := GetSDK() + if err != nil { + return nil, err + } + + // Create ClientProvider + clientProvider := sdk.CreateClientContext(fabsdk.WithOrg(orgName), fabsdk.WithUser(userName)) + + // Resource management client is responsible for managing channels (create/update channel) + // Supply user that has privileges to create channel + resMgmtClient, err := resmgmt.New(clientProvider, opts...) + if err != nil { + return nil, err + } + + return &fabricResmgtmClient{ + Provider: clientProvider, + Client: resMgmtClient, + }, nil +} + +// Returns a client which has channel transaction capabilities +// These are, but not limited to: Execute, Query, Invoke cc... +// +// Function works like this: +// 1. Get sdk +// 2. Use sdk to create a ChannelProvider () +// 3. From channel provider create channel Client +// You can then use this .Client to call for specific functionalities +func NewFabricChClient(channelName, userName, orgName string) (*fabricChannelClient, error) { + sdk, err := GetSDK() + if err != nil { + return nil, err + } + + // Create Channel Provider + chProvider := sdk.CreateChannelContext(channelName, fabsdk.WithUser(userName), fabsdk.WithOrg(orgName)) + + // Create Channel's chClient + chClient, err := channel.New(chProvider) + if err != nil { + return nil, err + } + + return &fabricChannelClient{ + Provider: chProvider, + Client: chClient, + }, nil +} + +// Returns a client which can query a channel's underlying ledger, +// such as QueryBlock and QueryConfig +// +// Function works like this: +// 1. Get sdk +// 2. Use sdk to create a ChannelProvider () +// 3. From channel provider create ledger Client +// You can then use this .Client to call for specific functionalities +func NewFabricLedgerClient(channelName, user, orgName string) (*fabricLedgerClient, error) { + sdk, err := GetSDK() + if err != nil { + return nil, err + } + + // Create Channel Provider + chProvider := sdk.CreateChannelContext(channelName, fabsdk.WithUser(user), fabsdk.WithOrg(orgName)) + // chProvider := sdk.CreateChannelContext(channelName, ) + // Create Channel's chClient + ledgerClient, err := ledger.New(chProvider) + if err != nil { + return nil, err + } + + return &fabricLedgerClient{ + Provider: chProvider, + Client: ledgerClient, + }, nil +} diff --git a/samples/application/ccapi/config/configsdk-org.yaml b/samples/application/ccapi/config/configsdk-org.yaml new file mode 100644 index 000000000..359c5d99a --- /dev/null +++ b/samples/application/ccapi/config/configsdk-org.yaml @@ -0,0 +1,220 @@ +version: 1.0.0 + +# +# The client section used by GO SDK. +# +client: + # Which organization does this application instance belong to? The value must be the name of an org + # defined under "organizations" + organization: org + logging: + # Develope can using debug to get more information + # level: debug + level: info + cryptoconfig: + path: "/fabric/organizations" + # Some SDKs support pluggable KV stores, the properties under "credentialStore" + # are implementation specific + credentialStore: + # [Optional]. Used by user store. Not needed if all credentials are embedded in configuration + # and enrollments are performed elswhere. + path: "/tmp/examplestore" + + # [Optional] BCCSP config for the client. Used by GO SDK. + BCCSP: + security: + enabled: true + default: + provider: "SW" + hashAlgorithm: "SHA2" + softVerify: true + level: 256 + + tlsCerts: + # [Optional]. Use system certificate pool when connecting to peers, orderers (for negotiating TLS) Default: false + systemCertPool: true + # [Optional]. Client key and cert for TLS handshake with peers and orderers + client: + # 使用byfn中Admin@org的证书 + keyfile: /fabric/organizations/peerOrganizations/org.example.com/users/Admin@org.example.com/tls/client.key + certfile: /fabric/organizations/peerOrganizations/org.example.com/users/Admin@org.example.com/tls/client.crt + cacertfile: /fabric/organizations/peerOrganizations/org.example.com/users/Admin@org.example.com/tls/ca.crt + +################################## General part ################################## + +# +# [Optional]. But most apps would have this section so that channel objects can be constructed +# based on the content below. If an app is creating channels, then it likely will not need this +# section. +# +channels: + # name of the channel + mainchannel: + # Required. list of orderers designated by the application to use for transactions on this + # channel. This list can be a result of access control ("org" can only access "ordererA"), or + # operational decisions to share loads from applications among the orderers. The values must + # be "names" of orgs defined under "organizations/peers" + # deprecated: not recommended, to override any orderer configuration items, entity matchers should be used. + # orderers: + # - orderer.example.com + + # 不要缺少当前channel的orderer节点 + # orderers: + # - orderer.example.com + + # Required. list of peers from participating orgs + peers: + peer0.org.example.com: + # [Optional]. will this peer be sent transaction proposals for endorsement? The peer must + # have the chaincode installed. The app can also use this property to decide which peers + # to send the chaincode install request. Default: true + endorsingPeer: true + + # [Optional]. will this peer be sent query proposals? The peer must have the chaincode + # installed. The app can also use this property to decide which peers to send the + # chaincode install request. Default: true + chaincodeQuery: true + + # [Optional]. will this peer be sent query proposals that do not require chaincodes, like + # queryBlock(), queryTransaction(), etc. Default: true + ledgerQuery: true + + # [Optional]. will this peer be the target of the SDK's listener registration? All peers can + # produce events but the app typically only needs to connect to one to listen to events. + # Default: true + eventSource: true + + # [Optional]. The application can use these options to perform channel operations like retrieving channel + # config etc. + policies: + #[Optional] options for retrieving channel configuration blocks + queryChannelConfig: + #[Optional] min number of success responses (from targets/peers) + minResponses: 1 + #[Optional] channel config will be retrieved for these number of random targets + maxTargets: 1 + #[Optional] retry options for query config block + retryOpts: + #[Optional] number of retry attempts + attempts: 5 + #[Optional] the back off interval for the first retry attempt + initialBackoff: 500ms + #[Optional] the maximum back off interval for any retry attempt + maxBackoff: 5s + #[Optional] he factor by which the initial back off period is exponentially incremented + backoffFactor: 2.0 + +# +# list of participating organizations in this network +# +organizations: + org: + mspid: orgMSP + # set msp files path (this path is relative to client.cryptoConfig defined above) + cryptoPath: peerOrganizations/org.example.com/users/{username}@org.example.com/msp + + # Add peers for org + peers: + - peer0.org.example.com + + + # the profile will contain public information about organizations other than the one it belongs to. + # These are necessary information to make transaction lifecycles work, including MSP IDs and + # peers with a public URL to send transaction proposals. The file will not contain private + # information reserved for members of the organization, such as admin key and certificate, + # fabric-ca registrar enroll ID and secret, etc. + + # Orderer Org name + OrdererOrg: + # Membership Service Provider ID for this organization + mspID: OrdererMSP + cryptoPath: ordererOrganizations/example.com/users/Admin@example.com/msp + peers: + - orderer.example.com + +# +# List of orderers to send transaction and channel create/update requests to. For the time +# being only one orderer is needed. If more than one is defined, which one get used by the +# SDK is implementation specific. Consult each SDK's documentation for its handling of orderers. +# +orderers: + orderer.example.com: + # [Optional] Default: Infer from hostname + url: grpcs://orderer.example.com:7050 + + # these are standard properties defined by the gRPC library + # they will be passed in as-is to gRPC client constructor + grpcOptions: + ssl-target-name-override: orderer.example.com + keep-alive-time: 0s + keep-alive-timeout: 20s + keep-alive-permit: false + fail-fast: false + + #will be taken into consideration if address has no protocol defined, if true then grpc or else grpcs + allow-insecure: false + + tlsCACerts: + # Certificate location absolute path + # Replace to orderer cert path + path: /fabric/organizations/ordererOrganizations/example.com/tlsca/tlsca.example.com-cert.pem + +# +# List of peers to send various requests to, including endorsement, query +# and event listener registration. +# +peers: + peer0.org.example.com: + # this URL is used to send endorsement and query requests + # [Optional] Default: Infer from hostname + # url: grpcs://peer0.org.example.com:7051 + url: grpcs://peer0.org.example.com:7051 + + grpcOptions: + ssl-target-name-override: peer0.org.example.com + keep-alive-time: 0s + keep-alive-timeout: 20s + keep-alive-permit: false + fail-fast: false + + #will be taken into consideration if address has no protocol defined, if true then grpc or else grpcs + allow-insecure: false + + tlsCACerts: + # Certificate location absolute path + path: /fabric/organizations/peerOrganizations/org.example.com/tlsca/tlsca.org.example.com-cert.pem + + + orderer.example.com: + # [Optional] Default: Infer from hostname + url: grpcs://orderer.example.com:7050 + + # these are standard properties defined by the gRPC library + # they will be passed in as-is to gRPC client constructor + grpcOptions: + ssl-target-name-override: orderer.example.com + keep-alive-time: 0s + keep-alive-timeout: 20s + keep-alive-permit: false + fail-fast: false + + #will be taken into consideration if address has no protocol defined, if true then grpc or else grpcs + allow-insecure: false + + tlsCACerts: + # Certificate location absolute path + # Replace to orderer cert path + path: /fabric/organizations/ordererOrganizations/example.com/tlsca/tlsca.example.com-cert.pem + +entitymatchers: + peer: + - pattern: (\w*)peer0.org.example.com(\w*) + urlsubstitutionexp: grpcs://peer0.org.example.com:7051 + ssltargetoverrideurlsubstitutionexp: peer0.org.example.com + mappedhost: peer0.org.example.com + + orderer: + - pattern: (\w*)orderer.example.com(\w*) + urlsubstitutionexp: orderer.example.com:7050 + ssltargetoverrideurlsubstitutionexp: orderer.example.com + mappedhost: orderer.example.com diff --git a/samples/application/ccapi/config/configsdk-org1.yaml b/samples/application/ccapi/config/configsdk-org1.yaml new file mode 100644 index 000000000..93080f20d --- /dev/null +++ b/samples/application/ccapi/config/configsdk-org1.yaml @@ -0,0 +1,220 @@ +version: 1.0.0 + +# +# The client section used by GO SDK. +# +client: + # Which organization does this application instance belong to? The value must be the name of an org + # defined under "organizations" + organization: org1 + logging: + # Develope can using debug to get more information + # level: debug + level: info + cryptoconfig: + path: "/fabric/organizations" + # Some SDKs support pluggable KV stores, the properties under "credentialStore" + # are implementation specific + credentialStore: + # [Optional]. Used by user store. Not needed if all credentials are embedded in configuration + # and enrollments are performed elswhere. + path: "/tmp/examplestore" + + # [Optional] BCCSP config for the client. Used by GO SDK. + BCCSP: + security: + enabled: true + default: + provider: "SW" + hashAlgorithm: "SHA2" + softVerify: true + level: 256 + + tlsCerts: + # [Optional]. Use system certificate pool when connecting to peers, orderers (for negotiating TLS) Default: false + systemCertPool: false + # [Optional]. Client key and cert for TLS handshake with peers and orderers + client: + # 使用byfn中Admin@org1的证书 + keyfile: /fabric/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/tls/client.key + certfile: /fabric/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/tls/client.cert + cacertfile: /fabric/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/tls/ca.crt + +################################## General part ################################## + +# +# [Optional]. But most apps would have this section so that channel objects can be constructed +# based on the content below. If an app is creating channels, then it likely will not need this +# section. +# +channels: + # name of the channel + mychannel: + # Required. list of orderers designated by the application to use for transactions on this + # channel. This list can be a result of access control ("org1" can only access "ordererA"), or + # operational decisions to share loads from applications among the orderers. The values must + # be "names" of orgs defined under "organizations/peers" + # deprecated: not recommended, to override any orderer configuration items, entity matchers should be used. + # orderers: + # - orderer.example.com + + # 不要缺少当前channel的orderer节点 + # orderers: + # - orderer.example.com + + # Required. list of peers from participating orgs + peers: + peer0.org1.example.com: + # [Optional]. will this peer be sent transaction proposals for endorsement? The peer must + # have the chaincode installed. The app can also use this property to decide which peers + # to send the chaincode install request. Default: true + endorsingPeer: true + + # [Optional]. will this peer be sent query proposals? The peer must have the chaincode + # installed. The app can also use this property to decide which peers to send the + # chaincode install request. Default: true + chaincodeQuery: true + + # [Optional]. will this peer be sent query proposals that do not require chaincodes, like + # queryBlock(), queryTransaction(), etc. Default: true + ledgerQuery: true + + # [Optional]. will this peer be the target of the SDK's listener registration? All peers can + # produce events but the app typically only needs to connect to one to listen to events. + # Default: true + eventSource: true + + # [Optional]. The application can use these options to perform channel operations like retrieving channel + # config etc. + policies: + #[Optional] options for retrieving channel configuration blocks + queryChannelConfig: + #[Optional] min number of success responses (from targets/peers) + minResponses: 1 + #[Optional] channel config will be retrieved for these number of random targets + maxTargets: 1 + #[Optional] retry options for query config block + retryOpts: + #[Optional] number of retry attempts + attempts: 5 + #[Optional] the back off interval for the first retry attempt + initialBackoff: 500ms + #[Optional] the maximum back off interval for any retry attempt + maxBackoff: 5s + #[Optional] he factor by which the initial back off period is exponentially incremented + backoffFactor: 2.0 + +# +# list of participating organizations in this network +# +organizations: + org1: + mspid: org1MSP + # set msp files path (this path is relative to client.cryptoConfig defined above) + cryptoPath: peerOrganizations/org1.example.com/users/{username}@org1.example.com/msp + + # Add peers for org1 + peers: + - peer0.org1.example.com + + + # the profile will contain public information about organizations other than the one it belongs to. + # These are necessary information to make transaction lifecycles work, including MSP IDs and + # peers with a public URL to send transaction proposals. The file will not contain private + # information reserved for members of the organization, such as admin key and certificate, + # fabric-ca registrar enroll ID and secret, etc. + + # Orderer Org name + OrdererOrg: + # Membership Service Provider ID for this organization + mspID: OrdererMSP + cryptoPath: ordererOrganizations/example.com/users/Admin@example.com/msp + peers: + - orderer.example.com + +# +# List of orderers to send transaction and channel create/update requests to. For the time +# being only one orderer is needed. If more than one is defined, which one get used by the +# SDK is implementation specific. Consult each SDK's documentation for its handling of orderers. +# +orderers: + orderer.example.com: + # [Optional] Default: Infer from hostname + url: grpcs://orderer.example.com:7050 + + # these are standard properties defined by the gRPC library + # they will be passed in as-is to gRPC client constructor + grpcOptions: + ssl-target-name-override: orderer.example.com + keep-alive-time: 0s + keep-alive-timeout: 20s + keep-alive-permit: false + fail-fast: false + + #will be taken into consideration if address has no protocol defined, if true then grpc or else grpcs + allow-insecure: false + + tlsCACerts: + # Certificate location absolute path + # Replace to orderer cert path + path: /fabric/organizations/ordererOrganizations/example.com/tlsca/tlsca.example.com-cert.pem + +# +# List of peers to send various requests to, including endorsement, query +# and event listener registration. +# +peers: + peer0.org1.example.com: + # this URL is used to send endorsement and query requests + # [Optional] Default: Infer from hostname + # url: grpcs://peer0.org1.example.com:7051 + url: grpcs://peer0.org1.example.com:7051 + + grpcOptions: + ssl-target-name-override: peer0.org1.example.com + keep-alive-time: 0s + keep-alive-timeout: 20s + keep-alive-permit: false + fail-fast: false + + #will be taken into consideration if address has no protocol defined, if true then grpc or else grpcs + allow-insecure: false + + tlsCACerts: + # Certificate location absolute path + path: /fabric/organizations/peerOrganizations/org1.example.com/tlsca/tlsca.org1.example.com-cert.pem + + + orderer.example.com: + # [Optional] Default: Infer from hostname + url: grpcs://orderer.example.com:7050 + + # these are standard properties defined by the gRPC library + # they will be passed in as-is to gRPC client constructor + grpcOptions: + ssl-target-name-override: orderer.example.com + keep-alive-time: 0s + keep-alive-timeout: 20s + keep-alive-permit: false + fail-fast: false + + #will be taken into consideration if address has no protocol defined, if true then grpc or else grpcs + allow-insecure: false + + tlsCACerts: + # Certificate location absolute path + # Replace to orderer cert path + path: /fabric/organizations/ordererOrganizations/example.com/tlsca/tlsca.example.com-cert.pem + +entitymatchers: + peer: + - pattern: (\w*)peer0.org1.example.com(\w*) + urlsubstitutionexp: grpcs://peer0.org1.example.com:7051 + ssltargetoverrideurlsubstitutionexp: peer0.org1.example.com + mappedhost: peer0.org1.example.com + + orderer: + - pattern: (\w*)orderer.example.com(\w*) + urlsubstitutionexp: orderer.example.com:7050 + ssltargetoverrideurlsubstitutionexp: orderer.example.com + mappedhost: orderer.example.com diff --git a/samples/application/ccapi/config/configsdk-org2.yaml b/samples/application/ccapi/config/configsdk-org2.yaml new file mode 100644 index 000000000..6521bf539 --- /dev/null +++ b/samples/application/ccapi/config/configsdk-org2.yaml @@ -0,0 +1,220 @@ +version: 1.0.0 + +# +# The client section used by GO SDK. +# +client: + # Which organization does this application instance belong to? The value must be the name of an org + # defined under "organizations" + organization: org2 + logging: + # Develope can using debug to get more information + # level: debug + level: info + cryptoconfig: + path: "/fabric/organizations" + # Some SDKs support pluggable KV stores, the properties under "credentialStore" + # are implementation specific + credentialStore: + # [Optional]. Used by user store. Not needed if all credentials are embedded in configuration + # and enrollments are performed elswhere. + path: "/tmp/examplestore" + + # [Optional] BCCSP config for the client. Used by GO SDK. + BCCSP: + security: + enabled: true + default: + provider: "SW" + hashAlgorithm: "SHA2" + softVerify: true + level: 256 + + tlsCerts: + # [Optional]. Use system certificate pool when connecting to peers, orderers (for negotiating TLS) Default: false + systemCertPool: false + # [Optional]. Client key and cert for TLS handshake with peers and orderers + client: + # 使用byfn中Admin@org2的证书 + keyfile: /fabric/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/tls/client.key + certfile: /fabric/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/tls/client.cert + cacertfile: /fabric/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/tls/ca.crt + +################################## General part ################################## + +# +# [Optional]. But most apps would have this section so that channel objects can be constructed +# based on the content below. If an app is creating channels, then it likely will not need this +# section. +# +channels: + # name of the channel + mychannel: + # Required. list of orderers designated by the application to use for transactions on this + # channel. This list can be a result of access control ("org2" can only access "ordererA"), or + # operational decisions to share loads from applications among the orderers. The values must + # be "names" of orgs defined under "organizations/peers" + # deprecated: not recommended, to override any orderer configuration items, entity matchers should be used. + # orderers: + # - orderer.example.com + + # 不要缺少当前channel的orderer节点 + # orderers: + # - orderer.example.com + + # Required. list of peers from participating orgs + peers: + peer0.org2.example.com: + # [Optional]. will this peer be sent transaction proposals for endorsement? The peer must + # have the chaincode installed. The app can also use this property to decide which peers + # to send the chaincode install request. Default: true + endorsingPeer: true + + # [Optional]. will this peer be sent query proposals? The peer must have the chaincode + # installed. The app can also use this property to decide which peers to send the + # chaincode install request. Default: true + chaincodeQuery: true + + # [Optional]. will this peer be sent query proposals that do not require chaincodes, like + # queryBlock(), queryTransaction(), etc. Default: true + ledgerQuery: true + + # [Optional]. will this peer be the target of the SDK's listener registration? All peers can + # produce events but the app typically only needs to connect to one to listen to events. + # Default: true + eventSource: true + + # [Optional]. The application can use these options to perform channel operations like retrieving channel + # config etc. + policies: + #[Optional] options for retrieving channel configuration blocks + queryChannelConfig: + #[Optional] min number of success responses (from targets/peers) + minResponses: 1 + #[Optional] channel config will be retrieved for these number of random targets + maxTargets: 1 + #[Optional] retry options for query config block + retryOpts: + #[Optional] number of retry attempts + attempts: 5 + #[Optional] the back off interval for the first retry attempt + initialBackoff: 500ms + #[Optional] the maximum back off interval for any retry attempt + maxBackoff: 5s + #[Optional] he factor by which the initial back off period is exponentially incremented + backoffFactor: 2.0 + +# +# list of participating organizations in this network +# +organizations: + org2: + mspid: org2MSP + # set msp files path (this path is relative to client.cryptoConfig defined above) + cryptoPath: peerOrganizations/org2.example.com/users/{username}@org2.example.com/msp + + # Add peers for org2 + peers: + - peer0.org2.example.com + + + # the profile will contain public information about organizations other than the one it belongs to. + # These are necessary information to make transaction lifecycles work, including MSP IDs and + # peers with a public URL to send transaction proposals. The file will not contain private + # information reserved for members of the organization, such as admin key and certificate, + # fabric-ca registrar enroll ID and secret, etc. + + # Orderer Org name + OrdererOrg: + # Membership Service Provider ID for this organization + mspID: OrdererMSP + cryptoPath: ordererOrganizations/example.com/users/Admin@example.com/msp + peers: + - orderer.example.com + +# +# List of orderers to send transaction and channel create/update requests to. For the time +# being only one orderer is needed. If more than one is defined, which one get used by the +# SDK is implementation specific. Consult each SDK's documentation for its handling of orderers. +# +orderers: + orderer.example.com: + # [Optional] Default: Infer from hostname + url: grpcs://orderer.example.com:7050 + + # these are standard properties defined by the gRPC library + # they will be passed in as-is to gRPC client constructor + grpcOptions: + ssl-target-name-override: orderer.example.com + keep-alive-time: 0s + keep-alive-timeout: 20s + keep-alive-permit: false + fail-fast: false + + #will be taken into consideration if address has no protocol defined, if true then grpc or else grpcs + allow-insecure: false + + tlsCACerts: + # Certificate location absolute path + # Replace to orderer cert path + path: /fabric/organizations/ordererOrganizations/example.com/tlsca/tlsca.example.com-cert.pem + +# +# List of peers to send various requests to, including endorsement, query +# and event listener registration. +# +peers: + peer0.org2.example.com: + # this URL is used to send endorsement and query requests + # [Optional] Default: Infer from hostname + # url: grpcs://peer0.org2.example.com:7051 + url: grpcs://peer0.org2.example.com:7051 + + grpcOptions: + ssl-target-name-override: peer0.org2.example.com + keep-alive-time: 0s + keep-alive-timeout: 20s + keep-alive-permit: false + fail-fast: false + + #will be taken into consideration if address has no protocol defined, if true then grpc or else grpcs + allow-insecure: false + + tlsCACerts: + # Certificate location absolute path + path: /fabric/organizations/peerOrganizations/org2.example.com/tlsca/tlsca.org2.example.com-cert.pem + + + orderer.example.com: + # [Optional] Default: Infer from hostname + url: grpcs://orderer.example.com:7050 + + # these are standard properties defined by the gRPC library + # they will be passed in as-is to gRPC client constructor + grpcOptions: + ssl-target-name-override: orderer.example.com + keep-alive-time: 0s + keep-alive-timeout: 20s + keep-alive-permit: false + fail-fast: false + + #will be taken into consideration if address has no protocol defined, if true then grpc or else grpcs + allow-insecure: false + + tlsCACerts: + # Certificate location absolute path + # Replace to orderer cert path + path: /fabric/organizations/ordererOrganizations/example.com/tlsca/tlsca.example.com-cert.pem + +entitymatchers: + peer: + - pattern: (\w*)peer0.org2.example.com(\w*) + urlsubstitutionexp: grpcs://peer0.org2.example.com:7051 + ssltargetoverrideurlsubstitutionexp: peer0.org2.example.com + mappedhost: peer0.org2.example.com + + orderer: + - pattern: (\w*)orderer.example.com(\w*) + urlsubstitutionexp: orderer.example.com:7050 + ssltargetoverrideurlsubstitutionexp: orderer.example.com + mappedhost: orderer.example.com diff --git a/samples/application/ccapi/config/configsdk-org3.yaml b/samples/application/ccapi/config/configsdk-org3.yaml new file mode 100644 index 000000000..41dc0e1fa --- /dev/null +++ b/samples/application/ccapi/config/configsdk-org3.yaml @@ -0,0 +1,220 @@ +version: 1.0.0 + +# +# The client section used by GO SDK. +# +client: + # Which organization does this application instance belong to? The value must be the name of an org + # defined under "organizations" + organization: org3 + logging: + # Develope can using debug to get more information + # level: debug + level: info + cryptoconfig: + path: "/fabric/organizations" + # Some SDKs support pluggable KV stores, the properties under "credentialStore" + # are implementation specific + credentialStore: + # [Optional]. Used by user store. Not needed if all credentials are embedded in configuration + # and enrollments are performed elswhere. + path: "/tmp/examplestore" + + # [Optional] BCCSP config for the client. Used by GO SDK. + BCCSP: + security: + enabled: true + default: + provider: "SW" + hashAlgorithm: "SHA2" + softVerify: true + level: 256 + + tlsCerts: + # [Optional]. Use system certificate pool when connecting to peers, orderers (for negotiating TLS) Default: false + systemCertPool: true + # [Optional]. Client key and cert for TLS handshake with peers and orderers + client: + # 使用byfn中Admin@org3的证书 + keyfile: /fabric/organizations/peerOrganizations/org3.example.com/users/Admin@org3.example.com/tls/client.key + certfile: /fabric/organizations/peerOrganizations/org3.example.com/users/Admin@org3.example.com/tls/client.cert + cacertfile: /fabric/organizations/peerOrganizations/org3.example.com/users/Admin@org3.example.com/tls/ca.crt + +################################## General part ################################## + +# +# [Optional]. But most apps would have this section so that channel objects can be constructed +# based on the content below. If an app is creating channels, then it likely will not need this +# section. +# +channels: + # name of the channel + mainchannel: + # Required. list of orderers designated by the application to use for transactions on this + # channel. This list can be a result of access control ("org3" can only access "ordererA"), or + # operational decisions to share loads from applications among the orderers. The values must + # be "names" of orgs defined under "organizations/peers" + # deprecated: not recommended, to override any orderer configuration items, entity matchers should be used. + # orderers: + # - orderer.example.com + + # 不要缺少当前channel的orderer节点 + # orderers: + # - orderer.example.com + + # Required. list of peers from participating orgs + peers: + peer0.org3.example.com: + # [Optional]. will this peer be sent transaction proposals for endorsement? The peer must + # have the chaincode installed. The app can also use this property to decide which peers + # to send the chaincode install request. Default: true + endorsingPeer: true + + # [Optional]. will this peer be sent query proposals? The peer must have the chaincode + # installed. The app can also use this property to decide which peers to send the + # chaincode install request. Default: true + chaincodeQuery: true + + # [Optional]. will this peer be sent query proposals that do not require chaincodes, like + # queryBlock(), queryTransaction(), etc. Default: true + ledgerQuery: true + + # [Optional]. will this peer be the target of the SDK's listener registration? All peers can + # produce events but the app typically only needs to connect to one to listen to events. + # Default: true + eventSource: true + + # [Optional]. The application can use these options to perform channel operations like retrieving channel + # config etc. + policies: + #[Optional] options for retrieving channel configuration blocks + queryChannelConfig: + #[Optional] min number of success responses (from targets/peers) + minResponses: 1 + #[Optional] channel config will be retrieved for these number of random targets + maxTargets: 1 + #[Optional] retry options for query config block + retryOpts: + #[Optional] number of retry attempts + attempts: 5 + #[Optional] the back off interval for the first retry attempt + initialBackoff: 500ms + #[Optional] the maximum back off interval for any retry attempt + maxBackoff: 5s + #[Optional] he factor by which the initial back off period is exponentially incremented + backoffFactor: 2.0 + +# +# list of participating organizations in this network +# +organizations: + org3: + mspid: org3MSP + # set msp files path (this path is relative to client.cryptoConfig defined above) + cryptoPath: peerOrganizations/org3.example.com/users/{username}@org3.example.com/msp + + # Add peers for org3 + peers: + - peer0.org3.example.com + + + # the profile will contain public information about organizations other than the one it belongs to. + # These are necessary information to make transaction lifecycles work, including MSP IDs and + # peers with a public URL to send transaction proposals. The file will not contain private + # information reserved for members of the organization, such as admin key and certificate, + # fabric-ca registrar enroll ID and secret, etc. + + # Orderer Org name + OrdererOrg: + # Membership Service Provider ID for this organization + mspID: OrdererMSP + cryptoPath: ordererOrganizations/example.com/users/Admin@example.com/msp + peers: + - orderer.example.com + +# +# List of orderers to send transaction and channel create/update requests to. For the time +# being only one orderer is needed. If more than one is defined, which one get used by the +# SDK is implementation specific. Consult each SDK's documentation for its handling of orderers. +# +orderers: + orderer.example.com: + # [Optional] Default: Infer from hostname + url: grpcs://orderer.example.com:7050 + + # these are standard properties defined by the gRPC library + # they will be passed in as-is to gRPC client constructor + grpcOptions: + ssl-target-name-override: orderer.example.com + keep-alive-time: 0s + keep-alive-timeout: 20s + keep-alive-permit: false + fail-fast: false + + #will be taken into consideration if address has no protocol defined, if true then grpc or else grpcs + allow-insecure: false + + tlsCACerts: + # Certificate location absolute path + # Replace to orderer cert path + path: /fabric/organizations/ordererOrganizations/example.com/tlsca/tlsca.example.com-cert.pem + +# +# List of peers to send various requests to, including endorsement, query +# and event listener registration. +# +peers: + peer0.org3.example.com: + # this URL is used to send endorsement and query requests + # [Optional] Default: Infer from hostname + # url: grpcs://peer0.org3.example.com:7051 + url: grpcs://peer0.org3.example.com:7051 + + grpcOptions: + ssl-target-name-override: peer0.org3.example.com + keep-alive-time: 0s + keep-alive-timeout: 20s + keep-alive-permit: false + fail-fast: false + + #will be taken into consideration if address has no protocol defined, if true then grpc or else grpcs + allow-insecure: false + + tlsCACerts: + # Certificate location absolute path + path: /fabric/organizations/peerOrganizations/org3.example.com/tlsca/tlsca.org3.example.com-cert.pem + + + orderer.example.com: + # [Optional] Default: Infer from hostname + url: grpcs://orderer.example.com:7050 + + # these are standard properties defined by the gRPC library + # they will be passed in as-is to gRPC client constructor + grpcOptions: + ssl-target-name-override: orderer.example.com + keep-alive-time: 0s + keep-alive-timeout: 20s + keep-alive-permit: false + fail-fast: false + + #will be taken into consideration if address has no protocol defined, if true then grpc or else grpcs + allow-insecure: false + + tlsCACerts: + # Certificate location absolute path + # Replace to orderer cert path + path: /fabric/organizations/ordererOrganizations/example.com/tlsca/tlsca.example.com-cert.pem + +entitymatchers: + peer: + - pattern: (\w*)peer0.org3.example.com(\w*) + urlsubstitutionexp: grpcs://peer0.org3.example.com:7051 + ssltargetoverrideurlsubstitutionexp: peer0.org3.example.com + mappedhost: peer0.org3.example.com + + orderer: + - pattern: (\w*)orderer.example.com(\w*) + urlsubstitutionexp: orderer.example.com:7050 + ssltargetoverrideurlsubstitutionexp: orderer.example.com + mappedhost: orderer.example.com diff --git a/samples/application/ccapi/docs/docs.go b/samples/application/ccapi/docs/docs.go new file mode 100644 index 000000000..94bb251e0 --- /dev/null +++ b/samples/application/ccapi/docs/docs.go @@ -0,0 +1,35 @@ +// Code generated by swaggo/swag. DO NOT EDIT. + +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": {} +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "", + Description: "", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/samples/application/ccapi/docs/swagger.yaml b/samples/application/ccapi/docs/swagger.yaml new file mode 100644 index 000000000..ca2113ba9 --- /dev/null +++ b/samples/application/ccapi/docs/swagger.yaml @@ -0,0 +1,1087 @@ +openapi: 3.0.0 +info: + description: Documentation of the Chaincode API. This API is used to interact with the chaincode through the Gateway service. + version: "1.0" + title: CC Tools Demo +servers: + - url: /api +tags: + - name: Basic Operations + - name: Select Channel and Chaincode + - name: Blockchain +components: + securitySchemes: + basicAuth: + type: "http" + scheme: "basic" +paths: + /invoke/{txName}: + post: + tags: + - Basic Operations + security: + - basicAuth: [] + summary: Executes transaction txName and writes the result to the blockchain. + parameters: + - in: path + name: txName + schema: + type: string + required: true + description: Name of the transaction to be executed. + requestBody: + description: The request body must match the definition of the transaction arguments. + content: + application/json: + schema: + type: object + responses: + "200": + description: OK + "4XX": + description: Bad Request + 5XX: + description: Internal error + /query/{txName}: + post: + tags: + - Basic Operations + security: + - basicAuth: [] + summary: Executes transaction txName and returns only the result, without writing it to the blockchain. + parameters: + - in: path + name: txName + schema: + type: string + required: true + description: Name of the transaction to be executed. + requestBody: + description: The request body must match the definition of the transaction arguments. + content: + application/json: + schema: + type: object + responses: + "200": + description: OK + "4XX": + description: Bad Request + 5XX: + description: Internal error + + /query/getHeader: + get: + tags: + - Basic Operations + security: + - basicAuth: [] + summary: Retrieves information about the chaincode. + responses: + "200": + description: OK + 5XX: + description: Internal error. + + /query/getTx: + get: + tags: + - Basic Operations + security: + - basicAuth: [] + summary: Requests the list of defined transactions. + responses: + "200": + description: OK + 5XX: + description: Internal error + post: + tags: + - Basic Operations + security: + - basicAuth: [] + summary: Gets the description of a specific transaction. + requestBody: + description: The txName field must contain the name of a transaction defined by the chaincode. + content: + application/json: + schema: + txName: + type: string + examples: + getTx: + value: + txName: getTx + responses: + "200": + description: OK + "400": + description: Bad Request + "404": + description: Transaction not found + 5XX: + description: Internal error + + /query/getSchema: + get: + tags: + - Basic Operations + security: + - basicAuth: [] + summary: Searches the list of existing assets. + responses: + "200": + description: OK + 5XX: + description: Internal error + post: + tags: + - Basic Operations + security: + - basicAuth: [] + summary: Gets the description of a specific asset type. + requestBody: + description: The assetType must contain an asset type defined by the chaincode. + content: + application/json: + schema: + assetType: + type: string + examples: + person: + value: + assetType: person + responses: + "200": + description: OK + "400": + description: Bad Request + "404": + description: Asset type not found + 5XX: + description: Internal error + + /invoke/createAsset: + post: + tags: + - Basic Operations + security: + - basicAuth: [] + summary: Create asset on the blockchain + requestBody: + description: The asset must be an array of objects. Each object must contain the asset type in the @assetType field and the asset data in the other fields. + content: + application/json: + schema: + type: object + properties: + asset: + type: array + items: + description: Any asset type defined by the chaincode. Check via getSchema. + type: object + examples: + person: + summary: "Create person" + value: + asset: + - "@assetType": person + name: "Maria" + id: "318.207.920-48" + responses: + "200": + description: OK + "400": + description: Bad format + "409": + description: Asset already exists + 5XX: + description: Internal error + + /query/readAsset: + post: + tags: + - Basic Operations + security: + - basicAuth: [] + summary: "Reads an asset from the blockchain using its primary key." + requestBody: + content: + application/json: + schema: + type: object + properties: + key: + type: object + examples: + person: + summary: person + value: + key: + "@assetType": person + id: "318.207.920-48" + responses: + "200": + description: OK + "404": + description: Asset not found + 5XX: + description: Internal error + + /query/readAssetHistory: + post: + tags: + - Basic Operations + security: + - basicAuth: [] + summary: "Reads the history of an asset from the blockchain using its primary key." + requestBody: + content: + application/json: + schema: + type: object + properties: + key: + type: object + examples: + person: + summary: person + value: + key: + "@assetType": person + id: "318.207.920-48" + responses: + "200": + description: OK + "404": + description: Asset not found + 5XX: + description: Internal error + + /query/search: + post: + tags: + - Basic Operations + security: + - basicAuth: [] + summary: Searches the blockchain world state using CouchDB rich queries + description: "Query JSON as defined by CouchDB docs: https://docs.couchdb.org/en/stable/api/database/find.html" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + query: + selector: + type: object + limit: + type: integer + bookmark: + type: string + examples: + personAll: + summary: Get all assets of type person + value: + query: + selector: + "@assetType": person + personFirst10: + summary: Get first 10 assets of type person + value: + query: + selector: + "@assetType": person + limit: 10 + bookmark: "" + person10to20: + summary: Get assets 10-20 of type person + value: + query: + selector: + "@assetType": person + limit: 10 + bookmark: "g1AAAACGeJzLYWBgYMpgSmHgKy5JLCrJTq2MT8lPzkzJBYprF6QWFefnWaVaGBmbWCan6BqZJ6fpmqalWOgmGSWZ65qbWFommpkZWCYlW4KM4IAZQarmLAD0pSXP" + responses: + "200": + description: OK + "400": + description: Bad format + 5XX: + description: Internal error + + /invoke/updateAsset: + put: + tags: + - Basic Operations + security: + - basicAuth: [] + summary: Updates an existing asset + requestBody: + content: + application/json: + schema: + type: object + properties: + "@assetType": + type: string + examples: + updateHeight: + summary: "Change person height" + value: + update: + "@assetType": "person" + name: "Maria" + height: 1.66 + description: The asset must contain the primary key of the asset and the fields to be updated. + required: true + responses: + "200": + description: OK + "400": + description: Bad format + "404": + description: Asset not found + 5XX: + description: Internal error + + /invoke/deleteAsset: + delete: + tags: + - Basic Operations + security: + - basicAuth: [] + summary: Deletes an existing asset + requestBody: + content: + application/json: + schema: + type: object + properties: + "@assetType": + type: string + examples: + deletePerson: + summary: 'Delete person with name "Maria"' + value: + key: + "@assetType": person + id: "318.207.920-48" + description: The asset must contain the primary key of the asset. + required: true + responses: + "200": + description: OK + "400": + description: Bad format + "404": + description: Asset not found + 5XX: + description: Internal error + /{channelName}/{chaincodeName}/invoke/{txName}: + post: + tags: + - Select Channel and Chaincode + security: + - basicAuth: [] + summary: Executes transaction txName and writes the result to the blockchain. + parameters: + - in: path + name: channelName + schema: + type: string + required: true + description: Name of the channel. + - in: path + name: chaincodeName + schema: + type: string + required: true + description: Name of the chaincode in channel. + - in: path + name: txName + schema: + type: string + required: true + description: Name of the transaction to be executed. + requestBody: + description: The request body must match the definition of the transaction arguments. + content: + application/json: + schema: + type: object + responses: + "200": + description: OK + "4XX": + description: Bad Request + 5XX: + description: Internal error + + /{channelName}/{chaincodeName}/query/{txName}: + post: + tags: + - Select Channel and Chaincode + security: + - basicAuth: [] + summary: Executes transaction txName and returns only the result, without writing it to the blockchain. + parameters: + - in: path + name: channelName + schema: + type: string + required: true + description: Name of the channel. + - in: path + name: chaincodeName + schema: + type: string + required: true + description: Name of the chaincode in channel. + - in: path + name: txName + schema: + type: string + required: true + description: Name of the transaction to be executed. + requestBody: + description: The request body must match the definition of the transaction arguments. + content: + application/json: + schema: + type: object + responses: + "200": + description: OK + "4XX": + description: Bad Request + 5XX: + description: Internal error + + /{channelName}/{chaincodeName}/query/getHeader: + get: + tags: + - Select Channel and Chaincode + security: + - basicAuth: [] + summary: Retrieves information about the chaincode. + parameters: + - in: path + name: channelName + schema: + type: string + required: true + description: Name of the channel. + - in: path + name: chaincodeName + schema: + type: string + required: true + description: Name of the chaincode in channel. + responses: + "200": + description: OK + 5XX: + description: Internal error. + + /{channelName}/{chaincodeName}/query/getTx: + get: + tags: + - Select Channel and Chaincode + security: + - basicAuth: [] + summary: Requests the list of defined transactions. + parameters: + - in: path + name: channelName + schema: + type: string + required: true + description: Name of the channel. + - in: path + name: chaincodeName + schema: + type: string + required: true + description: Name of the chaincode in channel. + responses: + "200": + description: OK + 5XX: + description: Internal error + post: + tags: + - Select Channel and Chaincode + security: + - basicAuth: [] + summary: Gets the description of a specific transaction. + parameters: + - in: path + name: channelName + schema: + type: string + required: true + description: Name of the channel. + - in: path + name: chaincodeName + schema: + type: string + required: true + description: Name of the chaincode in channel. + requestBody: + description: The txName field must contain the name of a transaction defined by the chaincode. + content: + application/json: + schema: + txName: + type: string + examples: + getTx: + value: + txName: getTx + responses: + "200": + description: OK + "400": + description: Bad Request + "404": + description: Transaction not found + 5XX: + description: Internal error + + /{channelName}/{chaincodeName}/query/getSchema: + get: + tags: + - Select Channel and Chaincode + security: + - basicAuth: [] + summary: Searches the list of existing assets. + parameters: + - in: path + name: channelName + schema: + type: string + required: true + description: Name of the channel. + - in: path + name: chaincodeName + schema: + type: string + required: true + description: Name of the chaincode in channel. + responses: + "200": + description: OK + 5XX: + description: Internal error + post: + tags: + - Select Channel and Chaincode + security: + - basicAuth: [] + summary: Gets the description of a specific asset type. + parameters: + - in: path + name: channelName + schema: + type: string + required: true + description: Name of the channel. + - in: path + name: chaincodeName + schema: + type: string + required: true + description: Name of the chaincode in channel. + requestBody: + description: The assetType must contain an asset type defined by the chaincode. + content: + application/json: + schema: + assetType: + type: string + examples: + person: + value: + assetType: person + responses: + "200": + description: OK + "400": + description: Bad Request + "404": + description: Asset type not found + 5XX: + description: Internal error + + /{channelName}/{chaincodeName}/invoke/createAsset: + post: + tags: + - Select Channel and Chaincode + security: + - basicAuth: [] + summary: Create asset on the blockchain + parameters: + - in: path + name: channelName + schema: + type: string + required: true + description: Name of the channel. + - in: path + name: chaincodeName + schema: + type: string + required: true + description: Name of the chaincode in channel. + requestBody: + description: The asset must be an array of objects. Each object must contain the asset type in the @assetType field and the asset data in the other fields. + content: + application/json: + schema: + type: object + properties: + asset: + type: array + items: + description: Any asset type defined by the chaincode. Check via getSchema. + type: object + examples: + person: + summary: "Create person" + value: + asset: + - "@assetType": person + name: "Maria" + id: "318.207.920-48" + responses: + "200": + description: OK + "400": + description: Bad format + "409": + description: Asset already exists + 5XX: + description: Internal error + + /{channelName}/{chaincodeName}/query/readAsset: + post: + tags: + - Select Channel and Chaincode + security: + - basicAuth: [] + summary: "Reads an asset from the blockchain using its primary key." + parameters: + - in: path + name: channelName + schema: + type: string + required: true + description: Name of the channel. + - in: path + name: chaincodeName + schema: + type: string + required: true + description: Name of the chaincode in channel. + requestBody: + content: + application/json: + schema: + type: object + properties: + key: + type: object + examples: + person: + summary: person + value: + key: + "@assetType": person + id: "318.207.920-48" + responses: + "200": + description: OK + "404": + description: Asset not found + 5XX: + description: Internal error + + /{channelName}/{chaincodeName}/query/readAssetHistory: + post: + tags: + - Select Channel and Chaincode + security: + - basicAuth: [] + summary: "Reads the history of an asset from the blockchain using its primary key." + parameters: + - in: path + name: channelName + schema: + type: string + required: true + description: Name of the channel. + - in: path + name: chaincodeName + schema: + type: string + required: true + description: Name of the chaincode in channel. + requestBody: + content: + application/json: + schema: + type: object + properties: + key: + type: object + examples: + person: + summary: person + value: + key: + "@assetType": person + id: "318.207.920-48" + responses: + "200": + description: OK + "404": + description: Asset not found + 5XX: + description: Internal error + + /{channelName}/{chaincodeName}/query/search: + post: + tags: + - Select Channel and Chaincode + security: + - basicAuth: [] + summary: Searches the blockchain world state using CouchDB rich queries + parameters: + - in: path + name: channelName + schema: + type: string + required: true + description: Name of the channel. + - in: path + name: chaincodeName + schema: + type: string + required: true + description: Name of the chaincode in channel. + description: "Query JSON as defined by CouchDB docs: https://docs.couchdb.org/en/stable/api/database/find.html" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + query: + selector: + type: object + limit: + type: integer + bookmark: + type: string + examples: + personAll: + summary: Get all assets of type person + value: + query: + selector: + "@assetType": person + personFirst10: + summary: Get first 10 assets of type person + value: + query: + selector: + "@assetType": person + limit: 10 + bookmark: "" + person10to20: + summary: Get assets 10-20 of type person + value: + query: + selector: + "@assetType": person + limit: 10 + bookmark: "g1AAAACGeJzLYWBgYMpgSmHgKy5JLCrJTq2MT8lPzkzJBYprF6QWFefnWaVaGBmbWCan6BqZJ6fpmqalWOgmGSWZ65qbWFommpkZWCYlW4KM4IAZQarmLAD0pSXP" + responses: + "200": + description: OK + "400": + description: Bad format + 5XX: + description: Internal error + + /{channelName}/{chaincodeName}/invoke/updateAsset: + put: + tags: + - Select Channel and Chaincode + security: + - basicAuth: [] + summary: Updates an existing asset + parameters: + - in: path + name: channelName + schema: + type: string + required: true + description: Name of the channel. + - in: path + name: chaincodeName + schema: + type: string + required: true + description: Name of the chaincode in channel. + requestBody: + content: + application/json: + schema: + type: object + properties: + "@assetType": + type: string + examples: + updateHeight: + summary: "Change person height" + value: + update: + "@assetType": "person" + name: "Maria" + height: 1.66 + description: The asset must contain the primary key of the asset and the fields to be updated. + required: true + responses: + "200": + description: OK + "400": + description: Bad format + "404": + description: Asset not found + 5XX: + description: Internal error + + /{channelName}/{chaincodeName}/invoke/deleteAsset: + delete: + tags: + - Select Channel and Chaincode + security: + - basicAuth: [] + summary: Deletes an existing asset + parameters: + - in: path + name: channelName + schema: + type: string + required: true + description: Name of the channel. + - in: path + name: chaincodeName + schema: + type: string + required: true + description: Name of the chaincode in channel. + requestBody: + content: + application/json: + schema: + type: object + properties: + "@assetType": + type: string + examples: + deletePerson: + summary: 'Delete person with name "Maria"' + value: + key: + "@assetType": person + id: "318.207.920-48" + description: The asset must contain the primary key of the asset. + required: true + responses: + "200": + description: OK + "400": + description: Bad format + "404": + description: Asset not found + 5XX: + description: Internal error + /{channelName}/qscc/getBlockByNumber: + get: + summary: Get block by number + description: Retrieves a block by its number from the specified channel. + parameters: + - name: channelName + in: path + required: true + schema: + type: string + example: mainchannel + - name: number + in: query + required: true + schema: + type: integer + example: 10 + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + '400': + description: Bad request + '404': + description: Block not found + tags: + - Blockchain + security: + - basicAuth: [] + consumes: + - application/json + produces: + - application/json + /{channelName}/qscc/getBlockByHash: + get: + summary: Get block by hash + description: Retrieves a block by its hash from the specified channel. + tags: + - Blockchain + security: + - basicAuth: [] + parameters: + - name: channelName + in: path + required: true + schema: + type: string + example: mainchannel + description: Name of the channel. + - name: hash + in: query + required: true + schema: + type: string + example: dbd2b14fb3d61b7aeac3add76f99cd9b47850c7c95ca5e489696a6b543fc6b2d + description: The hash of the block to be retrieved. + responses: + "200": + description: Successful response + content: + application/json: + schema: + type: object + "400": + description: Bad request + "404": + description: Block not found + "500": + description: Internal server error + /{channelName}/qscc/getChainInfo: + get: + summary: Get chain info + description: Retrieves chain information from the specified channel. + parameters: + - name: channelName + in: path + required: true + schema: + type: string + example: mainchannel + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + '400': + description: Bad request + '404': + description: Chain info not found + tags: + - Blockchain + security: + - basicAuth: [] + consumes: + - application/json + produces: + - application/json + /{channelName}/qscc/getTransactionByID: + get: + summary: Get transaction by ID + description: Retrieves a transaction by its ID from the specified channel. + parameters: + - name: channelName + in: path + required: true + schema: + type: string + example: mainchannel + - name: txid + in: query + required: true + schema: + type: string + example: 41675014bf3205b68e2620a802247f77adc730c77426885b15eebc28add6a414 + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + '400': + description: Bad request + '404': + description: Transaction not found + tags: + - Blockchain + security: + - basicAuth: [] + consumes: + - application/json + produces: + - application/json + /{channelName}/qscc/getBlockByTxID: + get: + summary: Get block by transaction ID + description: Retrieves a block by its transaction ID from the specified channel. + parameters: + - name: channelName + in: path + required: true + schema: + type: string + example: mainchannel + - name: txid + in: query + required: true + schema: + type: string + example: 41675014bf3205b68e2620a802247f77adc730c77426885b15eebc28add6a414 + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + '400': + description: Bad request + '404': + description: Block not found + tags: + - Blockchain + security: + - basicAuth: [] + consumes: + - application/json + produces: + - application/json \ No newline at end of file diff --git a/samples/application/ccapi/fpc-docker-compose.yaml b/samples/application/ccapi/fpc-docker-compose.yaml new file mode 100644 index 000000000..4aea2bbcd --- /dev/null +++ b/samples/application/ccapi/fpc-docker-compose.yaml @@ -0,0 +1,83 @@ +version: "2" +services: + ccapi.org1.example.com: + build: + dockerfile: Dockerfile + context: . + ports: + - 80:80 + volumes: + - ./:/rest-server + - /src/github.com/hyperledger/fabric-private-chaincode/samples/deployment/test-network/fabric-samples/test-network/organizations:/fabric/organizations + - /src/github.com/hyperledger/fabric-private-chaincode/samples/deployment/test-network/fabric-samples/test-network/organizations/:/project/src/github.com/hyperledger/fabric-private-chaincode/samples/deployment/test-network/fabric-samples/test-network/organizations/ + logging: + options: + max-size: 50m + environment: + - SDK_PATH=./config/configsdk-org1.yaml + - USER=Admin + - ORG=org1 + - DOMAIN=example.com + - CHANNEL=mychannel + - CCNAME=cc-tools-demo + - FABRIC_GATEWAY_ENDPOINT=peer0.org1.example.com:7051 + - FABRIC_GATEWAY_NAME=peer0.org1.example.com + - GOLANG_PROTOBUF_REGISTRATION_CONFLICT=warn + - FPC_ENABLED=true + - SGX_MODE=SIM + - CORE_PEER_ADDRESS=peer0.org1.example.com:7051 + - CORE_PEER_ID=peer0.org1.example.com + - CORE_PEER_LOCALMSPID=Org1MSP + - CORE_PEER_MSPCONFIGPATH=/fabric/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp + - CORE_PEER_TLS_CERT_FILE=/fabric/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.crt + - CORE_PEER_TLS_ENABLED="true" + - CORE_PEER_TLS_KEY_FILE=/fabric/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.key + - CORE_PEER_TLS_ROOTCERT_FILE=/fabric/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt + - ORDERER_CA=/fabric/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem + - GATEWAY_CONFIG=/fabric/organizations/peerOrganizations/org1.example.com/external-connection-org1.yaml + working_dir: /rest-server + container_name: ccapi.org1.example.com + networks: + - fabric_test + ccapi.org2.example.com: + build: + dockerfile: Dockerfile + context: . + ports: + - 980:80 + volumes: + - ./:/rest-server + - /src/github.com/hyperledger/fabric-private-chaincode/samples/deployment/test-network/fabric-samples/test-network/organizations:/fabric/organizations + - /src/github.com/hyperledger/fabric-private-chaincode/samples/deployment/test-network/fabric-samples/test-network/organizations/:/project/src/github.com/hyperledger/fabric-private-chaincode/samples/deployment/test-network/fabric-samples/test-network/organizations/ + logging: + options: + max-size: 50m + environment: + - SDK_PATH=./config/configsdk-org2.yaml + - USER=Admin + - ORG=org2 + - DOMAIN=example.com + - CHANNEL=mychannel + - CCNAME=cc-tools-demo + - FABRIC_GATEWAY_ENDPOINT=peer0.org2.example.com:7051 + - FABRIC_GATEWAY_NAME=peer0.org2.example.com + - GOLANG_PROTOBUF_REGISTRATION_CONFLICT=warn + - FPC_MODE=true + - SGX_MODE=SIM + - CORE_PEER_ADDRESS=peer0.org2.example.com:7051 + - CORE_PEER_ID=peer0.org2.example.com + - CORE_PEER_LOCALMSPID=Org2MSP + - CORE_PEER_MSPCONFIGPATH=/fabric/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp + - CORE_PEER_TLS_CERT_FILE=/fabric/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/server.crt + - CORE_PEER_TLS_ENABLED="true" + - CORE_PEER_TLS_KEY_FILE=/fabric/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/server.key + - CORE_PEER_TLS_ROOTCERT_FILE=/fabric/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt + - ORDERER_CA=/fabric/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem + - GATEWAY_CONFIG=/fabric/organizations/peerOrganizations/org2.example.com/external-connection-org2.yaml + working_dir: /rest-server + container_name: ccapi.org2.example.com + networks: + - fabric_test +networks: + fabric_test: + external: true diff --git a/samples/application/ccapi/fpcUtils/config.go b/samples/application/ccapi/fpcUtils/config.go new file mode 100644 index 000000000..397a04855 --- /dev/null +++ b/samples/application/ccapi/fpcUtils/config.go @@ -0,0 +1,23 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package fpcUtils + +type Config struct { + CorePeerAddress string + CorePeerId string + CorePeerLocalMSPID string + CorePeerMSPConfigPath string + CorePeerTLSCertFile string + CorePeerTLSEnabled bool + CorePeerTLSKeyFile string + CorePeerTLSRootCertFile string + OrdererCA string + FpcPath string + ChaincodeId string + ChannelId string + GatewayConfigPath string +} diff --git a/samples/application/ccapi/fpcUtils/connections.go b/samples/application/ccapi/fpcUtils/connections.go new file mode 100644 index 000000000..7bc89b7d8 --- /dev/null +++ b/samples/application/ccapi/fpcUtils/connections.go @@ -0,0 +1,41 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package fpcUtils + +import ( + "os" + + "gopkg.in/yaml.v2" +) + +type Connections struct { + Peers map[string]struct { + Url string + } + + Orderers map[string]struct { + Url string + } +} + +func NewConnections(path string) (*Connections, error) { + connections := &Connections{} + + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + d := yaml.NewDecoder(file) + + if err := d.Decode(&connections); err != nil { + return nil, err + } + + return connections, nil +} diff --git a/samples/application/ccapi/fpcUtils/fpcadmin.go b/samples/application/ccapi/fpcUtils/fpcadmin.go new file mode 100644 index 000000000..484cd51dc --- /dev/null +++ b/samples/application/ccapi/fpcUtils/fpcadmin.go @@ -0,0 +1,89 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package fpcUtils + +import ( + "fmt" + "path/filepath" + + fpcmgmt "github.com/hyperledger/fabric-private-chaincode/client_sdk/go/pkg/client/resmgmt" + "github.com/hyperledger/fabric-private-chaincode/client_sdk/go/pkg/sgx" + "github.com/hyperledger/fabric-sdk-go/pkg/client/resmgmt" + "github.com/hyperledger/fabric-sdk-go/pkg/common/errors/retry" + cfg "github.com/hyperledger/fabric-sdk-go/pkg/core/config" + "github.com/hyperledger/fabric-sdk-go/pkg/fabsdk" +) + +type Admin struct { + sdk *fabsdk.FabricSDK + client *fpcmgmt.Client + config *Config + connections *Connections +} + +func (a *Admin) Close() { + a.sdk.Close() +} + +func NewAdmin(config *Config) *Admin { + connections, err := NewConnections(filepath.Clean(config.GatewayConfigPath)) + if err != nil { + logger.Fatalf("failed to parse connections: %v", err) + } + + sdk, err := fabsdk.New(cfg.FromFile(filepath.Clean(config.GatewayConfigPath))) + if err != nil { + logger.Fatalf("failed to create sdk: %v", err) + } + //defer sdk.Close() + + orgAdmin := "Admin" + orgName := "org1" + adminContext := sdk.Context(fabsdk.WithUser(orgAdmin), fabsdk.WithOrg(orgName)) + + client, err := fpcmgmt.New(adminContext) + if err != nil { + logger.Fatalf("failed to create context: %v", err) + } + logger.Infof("I AM HERE 1") + return &Admin{sdk: sdk, client: client, config: config, connections: connections} +} + +func (a *Admin) InitEnclave(targetPeer string) error { + + logger.Infof("--> Collection attestation params ") + attestationParams, err := sgx.CreateAttestationParamsFromEnvironment() + if err != nil { + logger.Errorf("failed to load attestation params from environment: %v", err) + return fmt.Errorf("failed to load attestation params from environment: %v", err) + } + logger.Infof("I AM HERE 2") + + initReq := fpcmgmt.LifecycleInitEnclaveRequest{ + ChaincodeID: a.config.ChaincodeId, + EnclavePeerEndpoint: targetPeer, // define the peer where we wanna init our enclave + AttestationParams: attestationParams, + } + logger.Infof("I AM HERE 3") + + peers := []string{"peer0-org1", "peer0-org2", "peer0-org3"} + orderer := "orderer0" + + logger.Infof("--> LifecycleInitEnclave ") + _, err = a.client.LifecycleInitEnclave(a.config.ChannelId, initReq, + // Note that these options are currently ignored by our implementation + resmgmt.WithRetry(retry.DefaultResMgmtOpts), + resmgmt.WithTargetEndpoints(peers...), // peers that are responsible for enclave registration + resmgmt.WithOrdererEndpoint(orderer), + ) + + if err != nil { + return err + } + + return nil +} diff --git a/samples/application/ccapi/fpcUtils/fpcclient.go b/samples/application/ccapi/fpcUtils/fpcclient.go new file mode 100644 index 000000000..934ca3e79 --- /dev/null +++ b/samples/application/ccapi/fpcUtils/fpcclient.go @@ -0,0 +1,124 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package fpcUtils + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + fpc "github.com/hyperledger/fabric-private-chaincode/client_sdk/go/pkg/gateway" + cfg "github.com/hyperledger/fabric-sdk-go/pkg/core/config" + "github.com/hyperledger/fabric-sdk-go/pkg/gateway" + "github.com/pkg/errors" +) + +type Client struct { + contract fpc.Contract +} + +func NewClient(config *Config) *Client { + return &Client{contract: newContract(config)} +} + +func findSigningCert(mspConfigPath string) (string, error) { + p := filepath.Join(mspConfigPath, "signcerts") + files, err := os.ReadDir(p) + if err != nil { + return "", errors.Wrapf(err, "error while searching pem in %s", mspConfigPath) + } + + // return first pem we find + for _, f := range files { + if !f.IsDir() && strings.HasSuffix(f.Name(), ".pem") { + return filepath.Join(p, f.Name()), nil + } + } + + return "", errors.Errorf("cannot find pem in %s", mspConfigPath) +} + +func populateWallet(wallet *gateway.Wallet, config *Config) error { + logger.Debugf("============ Populating wallet ============") + certPath, err := findSigningCert(config.CorePeerMSPConfigPath) + if err != nil { + return err + } + + // read the certificate pem + cert, err := os.ReadFile(filepath.Clean(certPath)) + if err != nil { + return err + } + + keyDir := filepath.Join(config.CorePeerMSPConfigPath, "keystore") + // there's a single file in this dir containing the private key + files, err := os.ReadDir(keyDir) + if err != nil { + return err + } + if len(files) != 1 { + return fmt.Errorf("keystore folder should have contain one file") + } + keyPath := filepath.Join(keyDir, files[0].Name()) + key, err := os.ReadFile(filepath.Clean(keyPath)) + if err != nil { + return err + } + + identity := gateway.NewX509Identity(config.CorePeerLocalMSPID, string(cert), string(key)) + + return wallet.Put("appUser", identity) +} + +func newContract(config *Config) fpc.Contract { + + wallet := gateway.NewInMemoryWallet() + err := populateWallet(wallet, config) + if err != nil { + logger.Fatalf("Failed to populate wallet contents: %v", err) + } + + gw, err := gateway.Connect( + gateway.WithConfig(cfg.FromFile(filepath.Clean(config.GatewayConfigPath))), + gateway.WithIdentity(wallet, "appUser"), + ) + if err != nil { + logger.Fatalf("Failed to connect to gateway: %v", err) + } + defer gw.Close() + + network, err := gw.GetNetwork(config.ChannelId) + if err != nil { + logger.Fatalf("Failed to get network: %v", err) + } + + // Get FPC Contract + contract := fpc.GetContract(network, config.ChaincodeId) + return contract +} + +func (c *Client) Invoke(function string, args ...string) string { + logger.Infof("--> Invoke FPC chaincode with %s %s", function, args) + result, err := c.contract.SubmitTransaction(function, args...) + if err != nil { + logger.Infof("Failed to Submit transaction: %v", err) + } + logger.Debugf("--> Result: %s", string(result)) + return string(result) +} + +func (c *Client) Query(function string, args ...string) string { + logger.Debugf("--> Query FPC chaincode with %s %s", function, args) + result, err := c.contract.EvaluateTransaction(function, args...) + if err != nil { + logger.Fatalf("Failed to evaluate transaction: %v", err) + } + logger.Debugf("--> Result: %s", string(result)) + return string(result) +} diff --git a/samples/application/ccapi/fpcUtils/logging.go b/samples/application/ccapi/fpcUtils/logging.go new file mode 100644 index 000000000..f5c5d2d34 --- /dev/null +++ b/samples/application/ccapi/fpcUtils/logging.go @@ -0,0 +1,72 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package fpcUtils + +import ( + "fmt" + + "github.com/hyperledger/fabric-sdk-go/pkg/common/logging" + "github.com/hyperledger/fabric-sdk-go/pkg/core/logging/api" + "github.com/hyperledger/fabric/common/flogging" +) + +var logger = flogging.MustGetLogger("fpc.cli") + +func init() { + logging.Initialize(&provider{}) +} + +type provider struct { +} + +func (p *provider) GetLogger(module string) api.Logger { + name := "client.sdk-go" + e := &extendedFlogger{flogging.MustGetLogger(name)} + + return e +} + +type extendedFlogger struct { + *flogging.FabricLogger +} + +func (e *extendedFlogger) Fatalln(v ...interface{}) { + e.Fatal(v...) +} + +func (e *extendedFlogger) Panicln(v ...interface{}) { + e.Panic(v...) +} + +func (e *extendedFlogger) Print(v ...interface{}) { + fmt.Print(v...) +} + +func (e *extendedFlogger) Printf(format string, v ...interface{}) { + fmt.Printf(format, v...) +} + +func (e *extendedFlogger) Println(v ...interface{}) { + fmt.Println(v...) + +} + +func (e *extendedFlogger) Debugln(args ...interface{}) { + e.Debug(args...) +} + +func (e *extendedFlogger) Infoln(args ...interface{}) { + e.Info(args...) +} + +func (e *extendedFlogger) Warnln(args ...interface{}) { + e.Warn(args...) +} + +func (e *extendedFlogger) Errorln(args ...interface{}) { + e.Error(args...) +} diff --git a/samples/application/ccapi/handlers/invoke.go b/samples/application/ccapi/handlers/invoke.go new file mode 100644 index 000000000..0a2e3dbb1 --- /dev/null +++ b/samples/application/ccapi/handlers/invoke.go @@ -0,0 +1,100 @@ +package handlers + +import ( + "encoding/base64" + "encoding/json" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/hyperledger-labs/ccapi/chaincode" + "github.com/hyperledger-labs/ccapi/common" +) + +func Invoke(c *gin.Context) { + // Get channel information from request + req := make(map[string]interface{}) + err := c.BindJSON(&req) + if err != nil { + common.Abort(c, http.StatusBadRequest, err) + return + } + channelName := c.Param("channelName") + chaincodeName := c.Param("chaincodeName") + txName := c.Param("txname") + + var collections []string + collectionsQuery := c.Query("@collections") + if collectionsQuery != "" { + collectionsByte, err := base64.StdEncoding.DecodeString(collectionsQuery) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "the @collections query parameter must be a base64-encoded JSON array of strings", + }) + return + } + + err = json.Unmarshal(collectionsByte, &collections) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "the @collections query parameter must be a base64-encoded JSON array of strings", + }) + return + } + } else { + collectionsQuery := c.QueryArray("collections") + if len(collectionsQuery) > 0 { + collections = collectionsQuery + } else { + collections = []string{c.Query("collections")} + } + } + + transientMap := make(map[string]interface{}) + for key, value := range req { + if key[0] == '~' { + keyTrimmed := strings.TrimPrefix(key, "~") + transientMap[keyTrimmed] = value + delete(req, key) + } + } + + args, err := json.Marshal(req) + if err != nil { + common.Abort(c, http.StatusInternalServerError, err) + return + } + + transientMapByte, err := json.Marshal(transientMap) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + }) + return + } + + argList := [][]byte{} + if args != nil { + argList = append(argList, args) + } + + user := c.GetHeader("User") + if user == "" { + user = "Admin" + } + + res, status, err := chaincode.Invoke(channelName, chaincodeName, txName, user, argList, transientMapByte) + if err != nil { + common.Abort(c, status, err) + return + } + + var payload interface{} + err = json.Unmarshal(res.Payload, &payload) + if err != nil { + common.Abort(c, http.StatusInternalServerError, err) + return + } + + common.Respond(c, payload, status, err) +} diff --git a/samples/application/ccapi/handlers/invokeFpc.go b/samples/application/ccapi/handlers/invokeFpc.go new file mode 100644 index 000000000..d63a11850 --- /dev/null +++ b/samples/application/ccapi/handlers/invokeFpc.go @@ -0,0 +1,93 @@ +package handlers + +import ( + "encoding/base64" + "encoding/json" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/hyperledger-labs/ccapi/chaincode" + "github.com/hyperledger-labs/ccapi/common" +) + +func InvokeFpc(c *gin.Context) { + // Get channel information from request + req := make(map[string]interface{}) + err := c.BindJSON(&req) + if err != nil { + common.Abort(c, http.StatusBadRequest, err) + return + } + channelName := c.Param("channelName") + chaincodeName := c.Param("chaincodeName") + txName := c.Param("txname") + + var collections []string + collectionsQuery := c.Query("@collections") + if collectionsQuery != "" { + collectionsByte, err := base64.StdEncoding.DecodeString(collectionsQuery) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "the @collections query parameter must be a base64-encoded JSON array of strings", + }) + return + } + + err = json.Unmarshal(collectionsByte, &collections) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "the @collections query parameter must be a base64-encoded JSON array of strings", + }) + return + } + } else { + collectionsQuery := c.QueryArray("collections") + if len(collectionsQuery) > 0 { + collections = collectionsQuery + } else { + collections = []string{c.Query("collections")} + } + } + + transientMap := make(map[string]interface{}) + for key, value := range req { + if key[0] == '~' { + keyTrimmed := strings.TrimPrefix(key, "~") + transientMap[keyTrimmed] = value + delete(req, key) + } + } + + args, err := json.Marshal(req) + if err != nil { + common.Abort(c, http.StatusInternalServerError, err) + return + } + + argList := [][]byte{} + if args != nil { + argList = append(argList, args) + } + + user := c.GetHeader("User") + if user == "" { + user = "Admin" + } + + res, status, err := chaincode.InvokeFpc(channelName, chaincodeName, txName, argList) + + if err != nil { + common.Abort(c, status, err) + return + } + + var payload interface{} + err = json.Unmarshal(res, &payload) + if err != nil { + common.Abort(c, http.StatusInternalServerError, err) + return + } + + common.Respond(c, payload, status, err) +} diff --git a/samples/application/ccapi/handlers/invokeFpcDefault.go b/samples/application/ccapi/handlers/invokeFpcDefault.go new file mode 100644 index 000000000..d0dd3b050 --- /dev/null +++ b/samples/application/ccapi/handlers/invokeFpcDefault.go @@ -0,0 +1,91 @@ +package handlers + +import ( + "encoding/base64" + "encoding/json" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/hyperledger-labs/ccapi/chaincode" + "github.com/hyperledger-labs/ccapi/common" +) + +func InvokeFpcDefault(c *gin.Context) { + // Get transaction information from request + req := make(map[string]interface{}) + err := c.BindJSON(&req) + if err != nil { + common.Abort(c, http.StatusBadRequest, err) + return + } + txName := c.Param("txname") + + var collections []string + collectionsQuery := c.Query("@collections") + if collectionsQuery != "" { + collectionsByte, err := base64.StdEncoding.DecodeString(collectionsQuery) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "the @collections query parameter must be a base64-encoded JSON array of strings", + }) + return + } + + err = json.Unmarshal(collectionsByte, &collections) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "the @collections query parameter must be a base64-encoded JSON array of strings", + }) + return + } + } else { + collectionsQuery := c.QueryArray("collections") + if len(collectionsQuery) > 0 { + collections = collectionsQuery + } else { + collections = []string{c.Query("collections")} + } + } + + transientMap := make(map[string]interface{}) + for key, value := range req { + if key[0] == '~' { + keyTrimmed := strings.TrimPrefix(key, "~") + transientMap[keyTrimmed] = value + delete(req, key) + } + } + + args, err := json.Marshal(req) + if err != nil { + common.Abort(c, http.StatusInternalServerError, err) + return + } + + argList := [][]byte{} + if args != nil { + argList = append(argList, args) + } + + user := c.GetHeader("User") + if user == "" { + user = "Admin" + } + + res, status, err := chaincode.InvokeFpcDefault(txName, argList) + + if err != nil { + common.Abort(c, status, err) + return + } + + var payload interface{} + err = json.Unmarshal(res, &payload) + if err != nil { + common.Abort(c, http.StatusInternalServerError, err) + return + } + + common.Respond(c, payload, status, err) +} diff --git a/samples/application/ccapi/handlers/invokeGateway.go b/samples/application/ccapi/handlers/invokeGateway.go new file mode 100644 index 000000000..123ad4db8 --- /dev/null +++ b/samples/application/ccapi/handlers/invokeGateway.go @@ -0,0 +1,106 @@ +package handlers + +import ( + "encoding/base64" + "encoding/json" + "net/http" + "os" + "strings" + + "github.com/gin-gonic/gin" + "github.com/hyperledger-labs/ccapi/chaincode" + "github.com/hyperledger-labs/ccapi/common" + "github.com/pkg/errors" +) + +func InvokeGatewayDefault(c *gin.Context) { + channelName := os.Getenv("CHANNEL") + chaincodeName := os.Getenv("CCNAME") + + invokeGateway(c, channelName, chaincodeName) +} + +func InvokeGatewayCustom(c *gin.Context) { + channelName := c.Param("channelName") + chaincodeName := c.Param("chaincodeName") + + invokeGateway(c, channelName, chaincodeName) +} + +func invokeGateway(c *gin.Context, channelName, chaincodeName string) { + // Get request body + req := make(map[string]interface{}) + err := c.BindJSON(&req) + if err != nil { + common.Abort(c, http.StatusBadRequest, err) + return + } + + txName := c.Param("txname") + + // Get endorsers names + var endorsers []string + endorsersQuery := c.Query("@endorsers") + if endorsersQuery != "" { + endorsersByte, err := base64.StdEncoding.DecodeString(endorsersQuery) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "the @endorsers query parameter must be a base64-encoded JSON array of strings", + }) + return + } + + err = json.Unmarshal(endorsersByte, &endorsers) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "the @endorsers query parameter must be a base64-encoded JSON array of strings", + }) + return + } + } + + // Make transient request + transientMap := make(map[string]interface{}) + for key, value := range req { + if key[0] == '~' { + keyTrimmed := strings.TrimPrefix(key, "~") + transientMap[keyTrimmed] = value + delete(req, key) + } + } + + transientBytes, _ := json.Marshal(transientMap) + if len(transientMap) == 0 { + transientMap = nil + } + + // Make args + reqBytes, err := json.Marshal(req) + if err != nil { + common.Abort(c, http.StatusInternalServerError, errors.Wrap(err, "failed to marshal req body")) + return + } + + // Invoke + user := c.GetHeader("User") + if user == "" { + user = "Admin" + } + + result, err := chaincode.InvokeGateway(channelName, chaincodeName, txName, user, []string{string(reqBytes)}, transientBytes, endorsers) + if err != nil { + err, status := common.ParseError(err) + common.Abort(c, status, err) + return + } + + // Parse response + var payload interface{} + err = json.Unmarshal(result, &payload) + if err != nil { + common.Abort(c, http.StatusInternalServerError, err) + return + } + + common.Respond(c, payload, http.StatusOK, nil) +} diff --git a/samples/application/ccapi/handlers/invokeV1.go b/samples/application/ccapi/handlers/invokeV1.go new file mode 100644 index 000000000..f6eca2985 --- /dev/null +++ b/samples/application/ccapi/handlers/invokeV1.go @@ -0,0 +1,102 @@ +package handlers + +import ( + "encoding/base64" + "encoding/json" + "net/http" + "os" + "strings" + + "github.com/gin-gonic/gin" + "github.com/hyperledger-labs/ccapi/chaincode" + "github.com/hyperledger-labs/ccapi/common" +) + +func InvokeV1(c *gin.Context) { + // Get channel information from request + req := make(map[string]interface{}) + err := c.BindJSON(&req) + if err != nil { + common.Abort(c, http.StatusBadRequest, err) + return + } + + channelName := os.Getenv("CHANNEL") + chaincodeName := os.Getenv("CCNAME") + txName := c.Param("txname") + + var collections []string + collectionsQuery := c.Query("@collections") + if collectionsQuery != "" { + collectionsByte, err := base64.StdEncoding.DecodeString(collectionsQuery) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "the @collections query parameter must be a base64-encoded JSON array of strings", + }) + return + } + + err = json.Unmarshal(collectionsByte, &collections) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "the @collections query parameter must be a base64-encoded JSON array of strings", + }) + return + } + } else { + collectionsQuery := c.QueryArray("collections") + if len(collectionsQuery) > 0 { + collections = collectionsQuery + } else { + collections = []string{c.Query("collections")} + } + } + + transientMap := make(map[string]interface{}) + for key, value := range req { + if key[0] == '~' { + keyTrimmed := strings.TrimPrefix(key, "~") + transientMap[keyTrimmed] = value + delete(req, key) + } + } + + args, err := json.Marshal(req) + if err != nil { + common.Abort(c, http.StatusInternalServerError, err) + return + } + + transientMapByte, err := json.Marshal(transientMap) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + }) + return + } + + argList := [][]byte{} + if args != nil { + argList = append(argList, args) + } + + user := c.GetHeader("User") + if user == "" { + user = "Admin" + } + + res, status, err := chaincode.Invoke(channelName, chaincodeName, txName, user, argList, transientMapByte) + if err != nil { + common.Abort(c, status, err) + return + } + + var payload interface{} + err = json.Unmarshal(res.Payload, &payload) + if err != nil { + common.Abort(c, http.StatusInternalServerError, err) + return + } + + common.Respond(c, payload, status, err) +} diff --git a/samples/application/ccapi/handlers/qscc.go b/samples/application/ccapi/handlers/qscc.go new file mode 100644 index 000000000..d999d3934 --- /dev/null +++ b/samples/application/ccapi/handlers/qscc.go @@ -0,0 +1,451 @@ +package handlers + +import ( + "encoding/hex" + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/hyperledger-labs/ccapi/chaincode" + "github.com/hyperledger-labs/ccapi/common" + protos "github.com/hyperledger/fabric-protos-go-apiv2/common" + queryresultprotos "github.com/hyperledger/fabric-protos-go-apiv2/ledger/queryresult" + rwsetprotos "github.com/hyperledger/fabric-protos-go-apiv2/ledger/rwset" + mspprotos "github.com/hyperledger/fabric-protos-go-apiv2/msp" + peerprotos "github.com/hyperledger/fabric-protos-go-apiv2/peer" + "github.com/pkg/errors" + "google.golang.org/protobuf/proto" +) + +func QueryQSCC(c *gin.Context) { + channelName := c.Param("channelName") + txname := c.Param("txname") + + switch txname { + case "getBlockByNumber": + getBlockByNumber(c, channelName) + case "getBlockByHash": + getBlockByHash(c, channelName) + case "getTransactionByID": + getTransactionByID(c, channelName) + case "getChainInfo": + getChainInfo(c, channelName) + case "getBlockByTxID": + getBlockByTxID(c, channelName) + default: + common.Abort(c, http.StatusNotFound, fmt.Errorf("unknown endpoint call")) + } +} + +func getChainInfo(c *gin.Context, channelName string) { + // Query + user := c.GetHeader("User") + if user == "" { + user = "Admin" + } + + result, err := chaincode.QueryGateway(channelName, "qscc", "GetChainInfo", user, []string{channelName}) + if err != nil { + err, status := common.ParseError(err) + common.Abort(c, status, err) + return + } + + var chainInfo protos.BlockchainInfo + + err = proto.Unmarshal(result, &chainInfo) + if err != nil { + common.Abort(c, http.StatusInternalServerError, err) + return + } + + chainInfoRes := map[string]interface{}{ + "height": chainInfo.Height, + "current_block_hash": hex.EncodeToString(chainInfo.CurrentBlockHash), + "previous_block_hash": hex.EncodeToString(chainInfo.PreviousBlockHash), + } + + common.Respond(c, chainInfoRes, http.StatusOK, nil) +} + +func getBlockByNumber(c *gin.Context, channelName string) { + // Query + user := c.GetHeader("User") + if user == "" { + user = "Admin" + } + + number, ok := c.GetQuery("number") + if !ok { + common.Abort(c, http.StatusBadRequest, fmt.Errorf("missing number")) + return + } + + result, err := chaincode.QueryGateway(channelName, "qscc", "GetBlockByNumber", user, []string{channelName, number}) + if err != nil { + err, status := common.ParseError(err) + common.Abort(c, status, err) + return + } + + blockMap, err := decodeBlock(result) + if err != nil { + common.Abort(c, http.StatusInternalServerError, err) + return + } + + common.Respond(c, blockMap, http.StatusOK, nil) +} + +func getBlockByTxID(c *gin.Context, channelName string) { + // Query + user := c.GetHeader("User") + if user == "" { + user = "Admin" + } + + txid, ok := c.GetQuery("txid") + if !ok { + common.Abort(c, http.StatusBadRequest, fmt.Errorf("missing number")) + return + } + + result, err := chaincode.QueryGateway(channelName, "qscc", "GetBlockByTxID", user, []string{channelName, txid}) + if err != nil { + err, status := common.ParseError(err) + common.Abort(c, status, err) + return + } + + blockMap, err := decodeBlock(result) + if err != nil { + common.Abort(c, http.StatusInternalServerError, err) + return + } + + common.Respond(c, blockMap, http.StatusOK, nil) +} + +func getBlockByHash(c *gin.Context, channelName string) { + // Query + user := c.GetHeader("User") + if user == "" { + user = "Admin" + } + + hash, ok := c.GetQuery("hash") + if !ok { + common.Abort(c, http.StatusBadRequest, fmt.Errorf("missing hash")) + return + } + + hashBytes, err := hex.DecodeString(hash) + + if err != nil { + common.Abort(c, http.StatusBadRequest, fmt.Errorf("invalid hash format: %s", hash)) + return + } + + result, err := chaincode.QueryGateway(channelName, "qscc", "GetBlockByHash", user, []string{channelName, string(hashBytes)}) + + if err != nil { + err, status := common.ParseError(err) + common.Abort(c, status, err) + return + } + + blockMap, err := decodeBlock(result) + if err != nil { + common.Abort(c, http.StatusInternalServerError, err) + return + } + + common.Respond(c, blockMap, http.StatusOK, nil) +} + +func getTransactionByID(c *gin.Context, channelName string) { + // Query + user := c.GetHeader("User") + if user == "" { + user = "Admin" + } + + fmt.Println("getting txid") + txid, ok := c.GetQuery("txid") + if !ok { + common.Abort(c, http.StatusBadRequest, fmt.Errorf("missing txid")) + return + } + + fmt.Println("calling GetTransactionByID") + result, err := chaincode.QueryGateway(channelName, "qscc", "GetTransactionByID", user, []string{channelName, txid}) + if err != nil { + fmt.Println("error calling GetTransactionByID: ", err) + err, status := common.ParseError(err) + common.Abort(c, status, err) + return + } + + fmt.Println("decoding transaction") + m, err := decodeProcessedTransaction(result) + if err != nil { + fmt.Println("error decoding transaction: ", err) + common.Abort(c, http.StatusInternalServerError, err) + return + } + + fmt.Println("responding") + common.Respond(c, m, http.StatusOK, nil) +} + +func decodeBlock(b []byte) (map[string]interface{}, error) { + var block protos.Block + + err := proto.Unmarshal(b, &block) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal block") + } + + blockDataProto := block.GetData() + dataProto := blockDataProto.GetData() + + dataList := make([]interface{}, 0) + + for _, dataP := range dataProto { + var envelope protos.Envelope + + err := proto.Unmarshal(dataP, &envelope) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal envelope") + } + + var payload protos.Payload + + err = proto.Unmarshal(envelope.Payload, &payload) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal payload") + } + + var channelHeader protos.ChannelHeader + + err = proto.Unmarshal(payload.Header.ChannelHeader, &channelHeader) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal channel header") + } + + var tx interface{} + if channelHeader.Type == int32(protos.HeaderType_ENDORSER_TRANSACTION) { + tx, err = decodeTransaction(payload.Data) + if err != nil { + return nil, errors.Wrap(err, "failed to decode transaction") + } + } else { + tx = payload.Data + } + + dataList = append(dataList, map[string]interface{}{ + "payload": map[string]interface{}{ + "header": map[string]interface{}{ + "channel_header": &channelHeader, + "signature_header": payload.Header.SignatureHeader, + }, + "data": tx, + }, + "signature": envelope.Signature, + }) + + } + + blockMap := map[string]interface{}{ + "header": map[string]interface{}{ + "number": block.Header.Number, + "previous_hash": hex.EncodeToString(block.Header.PreviousHash), + "data_hash": hex.EncodeToString(block.Header.DataHash), + }, + "metadata": block.Metadata, + "data": dataList, + } + return blockMap, nil +} + +func decodeTransaction(b []byte) (map[string]interface{}, error) { + var transaction peerprotos.Transaction + + err := proto.Unmarshal(b, &transaction) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal transaction") + } + + actions := transaction.GetActions() + + actionList := make([]interface{}, 0) + for _, action := range actions { + headerB := action.GetHeader() + payloadB := action.GetPayload() + + var sigHeader protos.SignatureHeader + err = proto.Unmarshal(headerB, &sigHeader) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal signature header") + } + + var creator mspprotos.SerializedIdentity + err = proto.Unmarshal(sigHeader.Creator, &creator) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("failed to unmarshal creator, %s", string(sigHeader.Creator))) + } + + var ccActionPayload peerprotos.ChaincodeActionPayload + err = proto.Unmarshal(payloadB, &ccActionPayload) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal chaincode action payload") + } + + var ccProposalPayload peerprotos.ChaincodeProposalPayload + err = proto.Unmarshal(ccActionPayload.ChaincodeProposalPayload, &ccProposalPayload) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal chaincode proposal payload") + } + + var input peerprotos.ChaincodeInvocationSpec + err = proto.Unmarshal(ccProposalPayload.Input, &input) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal chaincode invocation spec") + } + + args := input.ChaincodeSpec.Input.Args + + inputList := make([]string, 0) + for _, arg := range args { + inputList = append(inputList, string(arg)) + } + + ccEndorsedAction := ccActionPayload.Action + + var proposalResponsePayload peerprotos.ProposalResponsePayload + err = proto.Unmarshal(ccEndorsedAction.ProposalResponsePayload, &proposalResponsePayload) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal proposal response payload") + } + + extension := proposalResponsePayload.Extension + proposalHash := proposalResponsePayload.ProposalHash + + var chaincodeAction peerprotos.ChaincodeAction + err = proto.Unmarshal(extension, &chaincodeAction) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal chaincode action") + } + + var txRWSet rwsetprotos.TxReadWriteSet + err = proto.Unmarshal(chaincodeAction.Results, &txRWSet) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal tx read write set") + } + + nsRWList := make([]interface{}, 0) + for _, nsRWSet := range txRWSet.NsRwset { + var kvSet queryresultprotos.KV + err = proto.Unmarshal(nsRWSet.Rwset, &kvSet) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal kv read write set") + } + + nsRWList = append(nsRWList, map[string]interface{}{ + "namespace": nsRWSet.Namespace, + "rwset": map[string]interface{}{ + "key": kvSet.Key, + "value": string(kvSet.Value), + "namespace": kvSet.Namespace, + }, + "collections": nsRWSet.CollectionHashedRwset, + }) + } + + endorsements := ccEndorsedAction.Endorsements + + endorsementList := make([]interface{}, 0) + for _, endorsement := range endorsements { + var endorser mspprotos.SerializedIdentity + err = proto.Unmarshal(endorsement.Endorser, &endorser) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal endorser") + } + + endorsementList = append(endorsementList, map[string]interface{}{ + "endorser": &endorser, + "signature": endorsement.Signature, + }) + } + + actionList = append(actionList, map[string]interface{}{ + "header": map[string]interface{}{ + "creator": &creator, + "nonce": sigHeader.Nonce, + }, + "payload": map[string]interface{}{ + "chaincode_proposal_payload": map[string]interface{}{ + "chaincode_id": input.ChaincodeSpec.ChaincodeId, + // "type": input.ChaincodeSpec.Type, + // "timeout": input.ChaincodeSpec.Timeout, + "input": inputList, + }, + "action": map[string]interface{}{ + "proposal_response_payload": map[string]interface{}{ + "proposal_hash": proposalHash, + "extension": map[string]interface{}{ + "results": map[string]interface{}{ + "ns_rwset": nsRWList, + "data_model": txRWSet.DataModel, + }, + "response": map[string]interface{}{ + "status": chaincodeAction.Response.Status, + "message": chaincodeAction.Response.Message, + "payload": string(chaincodeAction.Response.Payload), + }, + "chaincode_id": chaincodeAction.ChaincodeId, + "events": chaincodeAction.Events, + }, + }, + "endorsements": endorsementList, + }, + }, + }) + } + + transactionMap := map[string]interface{}{ + "actions": actionList, + } + + return transactionMap, nil +} + +func decodeProcessedTransaction(t []byte) (map[string]interface{}, error) { + var processedTransaction peerprotos.ProcessedTransaction + err := proto.Unmarshal(t, &processedTransaction) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal transaction") + } + + transactionEnv := processedTransaction.TransactionEnvelope + transactionPayload := transactionEnv.GetPayload() + + var payload protos.Payload + + err = proto.Unmarshal(transactionPayload, &payload) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal payload") + } + + transaction, err := decodeTransaction(payload.Data) + if err != nil { + return nil, errors.Wrap(err, "failed to decode transaction") + } + + processedTransactionMap := map[string]interface{}{ + "payload": transaction, + "signature": transactionEnv.Signature, + } + + return processedTransactionMap, nil +} \ No newline at end of file diff --git a/samples/application/ccapi/handlers/query.go b/samples/application/ccapi/handlers/query.go new file mode 100644 index 000000000..0e013d4e5 --- /dev/null +++ b/samples/application/ccapi/handlers/query.go @@ -0,0 +1,60 @@ +package handlers + +import ( + "encoding/base64" + "encoding/json" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/hyperledger-labs/ccapi/chaincode" + "github.com/hyperledger-labs/ccapi/common" +) + +func Query(c *gin.Context) { + var args []byte + var err error + + if c.Request.Method == "GET" { + request := c.Query("@request") + if request != "" { + args, _ = base64.StdEncoding.DecodeString(request) + } + } else if c.Request.Method == "POST" { + req := make(map[string]interface{}) + c.ShouldBind(&req) + args, err = json.Marshal(req) + if err != nil { + common.Abort(c, http.StatusInternalServerError, err) + return + } + } + + channelName := c.Param("channelName") + chaincodeName := c.Param("chaincodeName") + txName := c.Param("txname") + + argList := [][]byte{} + if args != nil { + argList = append(argList, args) + } + + user := c.GetHeader("User") + if user == "" { + user = "Admin" + } + + res, status, err := chaincode.Query(channelName, chaincodeName, txName, user, argList) + if err != nil { + common.Abort(c, status, err) + return + } + + var payload interface{} + err = json.Unmarshal(res.Payload, &payload) + if err != nil { + common.Abort(c, http.StatusInternalServerError, err) + return + } + + common.Respond(c, payload, status, err) +} diff --git a/samples/application/ccapi/handlers/queryFpc.go b/samples/application/ccapi/handlers/queryFpc.go new file mode 100644 index 000000000..8a059618f --- /dev/null +++ b/samples/application/ccapi/handlers/queryFpc.go @@ -0,0 +1,60 @@ +package handlers + +import ( + "encoding/base64" + "encoding/json" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/hyperledger-labs/ccapi/chaincode" + "github.com/hyperledger-labs/ccapi/common" +) + +func QueryFpc(c *gin.Context) { + var args []byte + var err error + + if c.Request.Method == "GET" { + request := c.Query("@request") + if request != "" { + args, _ = base64.StdEncoding.DecodeString(request) + } + } else if c.Request.Method == "POST" { + req := make(map[string]interface{}) + c.ShouldBind(&req) + args, err = json.Marshal(req) + if err != nil { + common.Abort(c, http.StatusInternalServerError, err) + return + } + } + + channelName := c.Param("channelName") + chaincodeName := c.Param("chaincodeName") + txName := c.Param("txname") + + argList := [][]byte{} + if args != nil { + argList = append(argList, args) + } + + user := c.GetHeader("User") + if user == "" { + user = "Admin" + } + + res, status, err := chaincode.QueryFpc(chaincodeName, channelName, txName, argList) + if err != nil { + common.Abort(c, status, err) + return + } + + var payload interface{} + err = json.Unmarshal(res, &payload) + if err != nil { + common.Abort(c, http.StatusInternalServerError, err) + return + } + + common.Respond(c, payload, status, err) +} diff --git a/samples/application/ccapi/handlers/queryFpcDefault.go b/samples/application/ccapi/handlers/queryFpcDefault.go new file mode 100644 index 000000000..38a740389 --- /dev/null +++ b/samples/application/ccapi/handlers/queryFpcDefault.go @@ -0,0 +1,58 @@ +package handlers + +import ( + "encoding/base64" + "encoding/json" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/hyperledger-labs/ccapi/chaincode" + "github.com/hyperledger-labs/ccapi/common" +) + +func QueryFpcDefault(c *gin.Context) { + var args []byte + var err error + + if c.Request.Method == "GET" { + request := c.Query("@request") + if request != "" { + args, _ = base64.StdEncoding.DecodeString(request) + } + } else if c.Request.Method == "POST" { + req := make(map[string]interface{}) + c.ShouldBind(&req) + args, err = json.Marshal(req) + if err != nil { + common.Abort(c, http.StatusInternalServerError, err) + return + } + } + + txName := c.Param("txname") + + argList := [][]byte{} + if args != nil { + argList = append(argList, args) + } + + user := c.GetHeader("User") + if user == "" { + user = "Admin" + } + + res, status, err := chaincode.QueryFpcDefault(txName, argList) + if err != nil { + common.Abort(c, status, err) + return + } + + var payload interface{} + err = json.Unmarshal(res, &payload) + if err != nil { + common.Abort(c, http.StatusInternalServerError, err) + return + } + + common.Respond(c, payload, status, err) +} diff --git a/samples/application/ccapi/handlers/queryGateway.go b/samples/application/ccapi/handlers/queryGateway.go new file mode 100644 index 000000000..2cee7c3f9 --- /dev/null +++ b/samples/application/ccapi/handlers/queryGateway.go @@ -0,0 +1,72 @@ +package handlers + +import ( + "encoding/base64" + "encoding/json" + "net/http" + "os" + + "github.com/gin-gonic/gin" + "github.com/hyperledger-labs/ccapi/chaincode" + "github.com/hyperledger-labs/ccapi/common" +) + +func QueryGatewayDefault(c *gin.Context) { + channelName := os.Getenv("CHANNEL") + chaincodeName := os.Getenv("CCNAME") + + queryGateway(c, channelName, chaincodeName) +} + +func QueryGatewayCustom(c *gin.Context) { + channelName := c.Param("channelName") + chaincodeName := c.Param("chaincodeName") + + queryGateway(c, channelName, chaincodeName) +} + +func queryGateway(c *gin.Context, channelName, chaincodeName string) { + var args []byte + var err error + + // Get request data + if c.Request.Method == "GET" { + request := c.Query("@request") + if request != "" { + args, _ = base64.StdEncoding.DecodeString(request) + } + } else if c.Request.Method == "POST" { + req := make(map[string]interface{}) + c.ShouldBind(&req) + args, err = json.Marshal(req) + if err != nil { + common.Abort(c, http.StatusInternalServerError, err) + return + } + } + + txName := c.Param("txname") + + // Query + user := c.GetHeader("User") + if user == "" { + user = "Admin" + } + + result, err := chaincode.QueryGateway(channelName, chaincodeName, txName, user, []string{string(args)}) + if err != nil { + err, status := common.ParseError(err) + common.Abort(c, status, err) + return + } + + // Parse response + var payload interface{} + err = json.Unmarshal(result, &payload) + if err != nil { + common.Abort(c, http.StatusInternalServerError, err) + return + } + + common.Respond(c, payload, http.StatusOK, nil) +} diff --git a/samples/application/ccapi/handlers/queryV1.go b/samples/application/ccapi/handlers/queryV1.go new file mode 100644 index 000000000..a68d30e82 --- /dev/null +++ b/samples/application/ccapi/handlers/queryV1.go @@ -0,0 +1,61 @@ +package handlers + +import ( + "encoding/base64" + "encoding/json" + "net/http" + "os" + + "github.com/gin-gonic/gin" + "github.com/hyperledger-labs/ccapi/chaincode" + "github.com/hyperledger-labs/ccapi/common" +) + +func QueryV1(c *gin.Context) { + var args []byte + var err error + + if c.Request.Method == "GET" { + request := c.Query("@request") + if request != "" { + args, _ = base64.StdEncoding.DecodeString(request) + } + } else if c.Request.Method == "POST" { + req := make(map[string]interface{}) + c.ShouldBind(&req) + args, err = json.Marshal(req) + if err != nil { + common.Abort(c, http.StatusInternalServerError, err) + return + } + } + + channelName := os.Getenv("CHANNEL") + chaincodeName := os.Getenv("CCNAME") + txName := c.Param("txname") + + argList := [][]byte{} + if args != nil { + argList = append(argList, args) + } + + user := c.GetHeader("User") + if user == "" { + user = "Admin" + } + + res, status, err := chaincode.Query(channelName, chaincodeName, txName, user, argList) + if err != nil { + common.Abort(c, status, err) + return + } + + var payload interface{} + err = json.Unmarshal(res.Payload, &payload) + if err != nil { + common.Abort(c, http.StatusInternalServerError, err) + return + } + + common.Respond(c, payload, status, err) +} diff --git a/samples/application/ccapi/main.go b/samples/application/ccapi/main.go new file mode 100644 index 000000000..ac39d69ad --- /dev/null +++ b/samples/application/ccapi/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "context" + "log" + "os" + "os/signal" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" + "github.com/hyperledger-labs/ccapi/chaincode" + "github.com/hyperledger-labs/ccapi/server" + "github.com/hyperledger/fabric-sdk-go/pkg/common/providers/fab" +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + + // Create gin handler and start server + r := gin.Default() + r.Use(cors.New(cors.Config{ + AllowOrigins: []string{ + "http://localhost:8080", // Test addresses + "*", + }, + AllowMethods: []string{"GET", "POST", "PUT", "DELETE"}, + AllowHeaders: []string{"Authorization", "Origin", "Content-Type"}, + AllowCredentials: true, + })) + go server.Serve(r, ctx) + // Events are not integrated with FPC + if os.Getenv("FPC_ENABLED") != "true" { + + // Register to chaincode events + go chaincode.WaitForEvent(os.Getenv("CHANNEL"), os.Getenv("CCNAME"), "eventName", func(ccEvent *fab.CCEvent) { + log.Println("Received CC event: ", ccEvent) + }) + + chaincode.RegisterForEvents() + } + + quit := make(chan os.Signal, 1) + signal.Notify(quit, os.Interrupt) + + <-quit + cancel() +} diff --git a/samples/application/ccapi/routes/chaincode.go b/samples/application/ccapi/routes/chaincode.go new file mode 100644 index 000000000..543e9c99d --- /dev/null +++ b/samples/application/ccapi/routes/chaincode.go @@ -0,0 +1,68 @@ +package routes + +import ( + "os" + + "github.com/hyperledger-labs/ccapi/handlers" + + "github.com/gin-gonic/gin" +) + +func addCCRoutes(rg *gin.RouterGroup) { + if os.Getenv("FPC_ENABLED") == "true" { + //Use FPC Handlers + rg.POST("/:channelName/:chaincodeName/invoke/:txname", handlers.InvokeFpc) + rg.PUT("/:channelName/:chaincodeName/invoke/:txname", handlers.InvokeFpc) + rg.DELETE("/:channelName/:chaincodeName/invoke/:txname", handlers.InvokeFpc) + rg.POST("/:channelName/:chaincodeName/query/:txname", handlers.QueryFpc) + rg.GET("/:channelName/:chaincodeName/query/:txname", handlers.QueryFpc) + + rg.POST("/invoke/:txname/", handlers.InvokeFpcDefault) + rg.POST("/invoke/:txname", handlers.InvokeFpcDefault) + rg.PUT("/invoke/:txname/", handlers.InvokeFpcDefault) + rg.PUT("/invoke/:txname", handlers.InvokeFpcDefault) + rg.DELETE("/invoke/:txname/", handlers.InvokeFpcDefault) + rg.DELETE("/invoke/:txname", handlers.InvokeFpcDefault) + rg.POST("/query/:txname/", handlers.QueryFpcDefault) + rg.POST("/query/:txname", handlers.QueryFpcDefault) + rg.GET("/query/:txname/", handlers.QueryFpcDefault) + rg.GET("/query/:txname", handlers.QueryFpcDefault) + + rg.GET("/:channelName/qscc/:txname", handlers.QueryQSCC) + + } else { + //Use Fabric Handlers + // Gateway routes + rg.POST("/gateway/:channelName/:chaincodeName/invoke/:txname", handlers.InvokeGatewayCustom) + rg.PUT("/gateway/:channelName/:chaincodeName/invoke/:txname", handlers.InvokeGatewayCustom) + rg.DELETE("/gateway/:channelName/:chaincodeName/invoke/:txname", handlers.InvokeGatewayCustom) + rg.POST("/gateway/:channelName/:chaincodeName/query/:txname", handlers.QueryGatewayCustom) + rg.GET("/gateway/:channelName/:chaincodeName/query/:txname", handlers.QueryGatewayCustom) + + rg.POST("/gateway/invoke/:txname", handlers.InvokeGatewayDefault) + rg.PUT("/gateway/invoke/:txname", handlers.InvokeGatewayDefault) + rg.DELETE("/gateway/invoke/:txname", handlers.InvokeGatewayDefault) + rg.POST("/gateway/query/:txname", handlers.QueryGatewayDefault) + rg.GET("/gateway/query/:txname", handlers.QueryGatewayDefault) + + // Other + rg.POST("/:channelName/:chaincodeName/invoke/:txname", handlers.Invoke) + rg.PUT("/:channelName/:chaincodeName/invoke/:txname", handlers.Invoke) + rg.DELETE("/:channelName/:chaincodeName/invoke/:txname", handlers.Invoke) + rg.POST("/:channelName/:chaincodeName/query/:txname", handlers.Query) + rg.GET("/:channelName/:chaincodeName/query/:txname", handlers.Query) + + rg.POST("/invoke/:txname/", handlers.InvokeV1) + rg.POST("/invoke/:txname", handlers.InvokeV1) + rg.PUT("/invoke/:txname/", handlers.InvokeV1) + rg.PUT("/invoke/:txname", handlers.InvokeV1) + rg.DELETE("/invoke/:txname/", handlers.InvokeV1) + rg.DELETE("/invoke/:txname", handlers.InvokeV1) + rg.POST("/query/:txname/", handlers.QueryV1) + rg.POST("/query/:txname", handlers.QueryV1) + rg.GET("/query/:txname/", handlers.QueryV1) + rg.GET("/query/:txname", handlers.QueryV1) + + rg.GET("/:channelName/qscc/:txname", handlers.QueryQSCC) + } +} diff --git a/samples/application/ccapi/routes/routes.go b/samples/application/ccapi/routes/routes.go new file mode 100644 index 000000000..6a1712332 --- /dev/null +++ b/samples/application/ccapi/routes/routes.go @@ -0,0 +1,36 @@ +package routes + +import ( + "github.com/gin-gonic/gin" + "github.com/hyperledger-labs/ccapi/docs" + swaggerfiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" +) + +// Register routes and handlers used by engine +func AddRoutesToEngine(r *gin.Engine) { + r.GET("/", func(c *gin.Context) { + c.Redirect(301, "/api-docs/index.html") + }) + + r.GET("/ping", func(c *gin.Context) { + c.JSON(200, gin.H{ + "status": "ok", + }) + }) + + // serve swagger files + docs.SwaggerInfo.BasePath = "/api" + r.StaticFile("/swagger.yaml", "./docs/swagger.yaml") + + url := ginSwagger.URL("/swagger.yaml") + r.GET("/api-docs/*any", ginSwagger.WrapHandler(swaggerfiles.Handler, url)) + + // CHANNEL routes + chaincodeRG := r.Group("/api") + addCCRoutes(chaincodeRG) + + // Update SDK route + sdkRG := r.Group("/sdk") + addSDKRoutes(sdkRG) +} diff --git a/samples/application/ccapi/routes/sdk.go b/samples/application/ccapi/routes/sdk.go new file mode 100644 index 000000000..417f6b06b --- /dev/null +++ b/samples/application/ccapi/routes/sdk.go @@ -0,0 +1,10 @@ +package routes + +import ( + "github.com/gin-gonic/gin" +) + +func addSDKRoutes(rg *gin.RouterGroup) { + // Update SDK route + // rg.POST("/update", handlers.UpdateSDK) +} diff --git a/samples/application/ccapi/server/server.go b/samples/application/ccapi/server/server.go new file mode 100644 index 000000000..4dbc5c06e --- /dev/null +++ b/samples/application/ccapi/server/server.go @@ -0,0 +1,92 @@ +package server + +import ( + "context" + "log" + "net/http" + "os" + "sync" + "time" + + "github.com/gin-gonic/gin" + "github.com/hyperledger-labs/ccapi/common" + "github.com/hyperledger-labs/ccapi/routes" +) + +func defaultServer(r *gin.Engine) *http.Server { + return &http.Server{ + Addr: ":80", + Handler: r, + } +} + +// Serve starts the server with gin's default engine. +// Server gracefully shut's down +func Serve(r *gin.Engine, ctx context.Context) { + // Defer close sdk to clear cache and free memory + defer common.CloseSDK() + + if os.Getenv("FPC_ENABLED") == "true" { + common.InitFpcConfig() + } + + // Register routes and handlers + routes.AddRoutesToEngine(r) + + // Returns a http.Server from provided handler + srv := defaultServer(r) + + // listen and serve on 0.0.0.0:80 (for windows "localhost:80") + go func(server *http.Server) { + log.Println("Listening on port 80") + err := srv.ListenAndServe() + if err != http.ErrServerClosed { + log.Panic(err) + } + }(srv) + + // Graceful shutdown + <-ctx.Done() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := srv.Shutdown(ctx); err != nil { + log.Panic(err) + } + log.Println("Shutting down") +} + +// Serve sync starts the server with a given wait group. +// When server starts, the wait group counter is increased and processes +// that depend on server can be ran synchronously with it +func ServeSync(ctx context.Context, wg *sync.WaitGroup) { + gin.SetMode(gin.TestMode) + r := gin.New() + + routes.AddRoutesToEngine(r) + + srv := defaultServer(r) + + go func(server *http.Server) { + log.Println("Listening on port 80") + err := srv.ListenAndServe() + if err != http.ErrServerClosed { + log.Panic(err) + } + // finish wait group + time.Sleep(1 * time.Second) + wg.Done() + }(srv) + + wg.Add(1) + <-ctx.Done() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := srv.Shutdown(ctx); err != nil { + log.Panic(err) + } + log.Println("Shutting down") +} diff --git a/samples/application/ccapi/web-client/docker-compose-goinitus.yaml b/samples/application/ccapi/web-client/docker-compose-goinitus.yaml new file mode 100644 index 000000000..2cb1acb3c --- /dev/null +++ b/samples/application/ccapi/web-client/docker-compose-goinitus.yaml @@ -0,0 +1,14 @@ +version: '3' + +networks: + fabric_test: + external: true + +services: + goinitus: + image: goledger/cc-webclient:latest + container_name: goinitus + ports: + - "8080:80" + networks: + - fabric_test