From 9485e4bd10916a3141c53ba688a38d5f7d42b858 Mon Sep 17 00:00:00 2001 From: Basil K Y Date: Fri, 23 Sep 2022 22:01:28 +0530 Subject: [PATCH] REST api added for asset transfer in Golang Signed-off-by: Basil K Y --- asset-transfer-basic/rest-api-go/.gitignore | 3 + asset-transfer-basic/rest-api-go/README.md | 39 +++++++ asset-transfer-basic/rest-api-go/go.mod | 19 ++++ asset-transfer-basic/rest-api-go/main.go | 26 +++++ asset-transfer-basic/rest-api-go/web/app.go | 31 +++++ .../rest-api-go/web/initialize.go | 106 ++++++++++++++++++ .../rest-api-go/web/invoke.go | 40 +++++++ asset-transfer-basic/rest-api-go/web/query.go | 25 +++++ 8 files changed, 289 insertions(+) create mode 100644 asset-transfer-basic/rest-api-go/.gitignore create mode 100644 asset-transfer-basic/rest-api-go/README.md create mode 100644 asset-transfer-basic/rest-api-go/go.mod create mode 100644 asset-transfer-basic/rest-api-go/main.go create mode 100644 asset-transfer-basic/rest-api-go/web/app.go create mode 100644 asset-transfer-basic/rest-api-go/web/initialize.go create mode 100644 asset-transfer-basic/rest-api-go/web/invoke.go create mode 100644 asset-transfer-basic/rest-api-go/web/query.go diff --git a/asset-transfer-basic/rest-api-go/.gitignore b/asset-transfer-basic/rest-api-go/.gitignore new file mode 100644 index 00000000..69ceaeb9 --- /dev/null +++ b/asset-transfer-basic/rest-api-go/.gitignore @@ -0,0 +1,3 @@ +go.sum +Requests.http +rest-api-go \ No newline at end of file diff --git a/asset-transfer-basic/rest-api-go/README.md b/asset-transfer-basic/rest-api-go/README.md new file mode 100644 index 00000000..331411b7 --- /dev/null +++ b/asset-transfer-basic/rest-api-go/README.md @@ -0,0 +1,39 @@ +# Asset Transfer REST API Sample + +This is a simple REST server written in golang with endpoints for chaincode invoke and query. + + +## Usage + +- Setup fabric test network and deploy the asset transfer chaincode by [following this instructions](https://hyperledger-fabric.readthedocs.io/en/release-2.4/test_network.html). + +- cd into rest-api-go directory +- Download required dependencies using `go mod download` +- Run `go run main.go` to run the REST server + +## Sending Requests + +Invoke endpoint accepts POST requests with chaincode function and arguments. Query endpoint accepts get requests with chaincode function and arguments. + +Sample chaincode invoke for the "createAsset" function. Response will contain transaction ID for a successful invoke. + +``` sh +curl --request POST \ + --url http://localhost:3000/invoke \ + --header 'content-type: application/x-www-form-urlencoded' \ + --data = \ + --data channelid=mychannel \ + --data chaincodeid=basic \ + --data function=createAsset \ + --data args=Asset123 \ + --data args=yellow \ + --data args=54 \ + --data args=Tom \ + --data args=13005 +``` +Sample chaincode query for getting asset details. + +``` sh +curl --request GET \ + --url 'http://localhost:3000/query?channelid=mychannel&chaincodeid=basic&function=ReadAsset&args=Asset123' + ``` diff --git a/asset-transfer-basic/rest-api-go/go.mod b/asset-transfer-basic/rest-api-go/go.mod new file mode 100644 index 00000000..cbfee953 --- /dev/null +++ b/asset-transfer-basic/rest-api-go/go.mod @@ -0,0 +1,19 @@ +module rest-api-go + +go 1.19 + +require ( + github.com/hyperledger/fabric-gateway v1.1.0 + google.golang.org/grpc v1.49.0 +) + +require ( + github.com/golang/protobuf v1.5.2 // indirect + github.com/hyperledger/fabric-protos-go-apiv2 v0.0.0-20220615102044-467be1c7b2e7 // indirect + github.com/miekg/pkcs11 v1.1.1 // indirect + golang.org/x/net v0.0.0-20220526153639-5463443f8c37 // indirect + golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect + golang.org/x/text v0.3.7 // indirect + google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 // indirect + google.golang.org/protobuf v1.28.0 // indirect +) diff --git a/asset-transfer-basic/rest-api-go/main.go b/asset-transfer-basic/rest-api-go/main.go new file mode 100644 index 00000000..4e60fa76 --- /dev/null +++ b/asset-transfer-basic/rest-api-go/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" + "rest-api-go/web" +) + +func main() { + //Initialize setup for Org1 + cryptoPath := "../../test-network/organizations/peerOrganizations/org1.example.com" + orgConfig := web.OrgSetup{ + OrgName: "Org1", + MSPID: "Org1MSP", + CertPath: cryptoPath + "/users/User1@org1.example.com/msp/signcerts/cert.pem", + KeyPath: cryptoPath + "/users/User1@org1.example.com/msp/keystore/", + TLSCertPath: cryptoPath + "/peers/peer0.org1.example.com/tls/ca.crt", + PeerEndpoint: "localhost:7051", + GatewayPeer: "peer0.org1.example.com", + } + + orgSetup, err := web.Initialize(orgConfig) + if err != nil { + fmt.Println("Error initializing setup for Org1: ", err) + } + web.Serve(web.OrgSetup(*orgSetup)) +} diff --git a/asset-transfer-basic/rest-api-go/web/app.go b/asset-transfer-basic/rest-api-go/web/app.go new file mode 100644 index 00000000..b8d8c1b3 --- /dev/null +++ b/asset-transfer-basic/rest-api-go/web/app.go @@ -0,0 +1,31 @@ +package web + +import ( + "fmt" + "net/http" + + "github.com/hyperledger/fabric-gateway/pkg/client" +) + +// OrgSetup contains organization's config to interact with the network. +type OrgSetup struct { + OrgName string + MSPID string + CryptoPath string + CertPath string + KeyPath string + TLSCertPath string + PeerEndpoint string + GatewayPeer string + Gateway client.Gateway +} + +// Serve starts http web server. +func Serve(setups OrgSetup) { + http.HandleFunc("/query", setups.Query) + http.HandleFunc("/invoke", setups.Invoke) + fmt.Println("Listening (http://localhost:3000/)...") + if err := http.ListenAndServe(":3000", nil); err != nil { + fmt.Println(err) + } +} diff --git a/asset-transfer-basic/rest-api-go/web/initialize.go b/asset-transfer-basic/rest-api-go/web/initialize.go new file mode 100644 index 00000000..87dd5f4c --- /dev/null +++ b/asset-transfer-basic/rest-api-go/web/initialize.go @@ -0,0 +1,106 @@ +package web + +import ( + "crypto/x509" + "fmt" + "io/ioutil" + "log" + "path" + "time" + + "github.com/hyperledger/fabric-gateway/pkg/client" + "github.com/hyperledger/fabric-gateway/pkg/identity" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +// Initialize the setup for the organization. +func Initialize(setup OrgSetup) (*OrgSetup, error) { + log.Printf("Initializing connection for %s...\n", setup.OrgName) + clientConnection := setup.newGrpcConnection() + id := setup.newIdentity() + sign := setup.newSign() + + gateway, err := client.Connect( + id, + client.WithSign(sign), + client.WithClientConnection(clientConnection), + client.WithEvaluateTimeout(5*time.Second), + client.WithEndorseTimeout(15*time.Second), + client.WithSubmitTimeout(5*time.Second), + client.WithCommitStatusTimeout(1*time.Minute), + ) + if err != nil { + panic(err) + } + setup.Gateway = *gateway + log.Println("Initialization complete") + return &setup, nil +} + +// newGrpcConnection creates a gRPC connection to the Gateway server. +func (setup OrgSetup) newGrpcConnection() *grpc.ClientConn { + certificate, err := loadCertificate(setup.TLSCertPath) + if err != nil { + panic(err) + } + + certPool := x509.NewCertPool() + certPool.AddCert(certificate) + transportCredentials := credentials.NewClientTLSFromCert(certPool, setup.GatewayPeer) + + connection, err := grpc.Dial(setup.PeerEndpoint, grpc.WithTransportCredentials(transportCredentials)) + if err != nil { + panic(fmt.Errorf("failed to create gRPC connection: %w", err)) + } + + return connection +} + +// newIdentity creates a client identity for this Gateway connection using an X.509 certificate. +func (setup OrgSetup) newIdentity() *identity.X509Identity { + certificate, err := loadCertificate(setup.CertPath) + if err != nil { + panic(err) + } + + id, err := identity.NewX509Identity(setup.MSPID, certificate) + if err != nil { + panic(err) + } + + return id +} + +// newSign creates a function that generates a digital signature from a message digest using a private key. +func (setup OrgSetup) newSign() identity.Sign { + files, err := ioutil.ReadDir(setup.KeyPath) + if err != nil { + panic(fmt.Errorf("failed to read private key directory: %w", err)) + } + privateKeyPEM, err := ioutil.ReadFile(path.Join(setup.KeyPath, files[0].Name())) + + if err != nil { + panic(fmt.Errorf("failed to read private key file: %w", err)) + } + + privateKey, err := identity.PrivateKeyFromPEM(privateKeyPEM) + if err != nil { + panic(err) + } + + sign, err := identity.NewPrivateKeySign(privateKey) + if err != nil { + panic(err) + } + + return sign +} + +func loadCertificate(filename string) (*x509.Certificate, error) { + certificatePEM, err := ioutil.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("failed to read certificate file: %w", err) + } + return identity.CertificateFromPEM(certificatePEM) +} diff --git a/asset-transfer-basic/rest-api-go/web/invoke.go b/asset-transfer-basic/rest-api-go/web/invoke.go new file mode 100644 index 00000000..f2907d58 --- /dev/null +++ b/asset-transfer-basic/rest-api-go/web/invoke.go @@ -0,0 +1,40 @@ +package web + +import ( + "fmt" + "net/http" + + "github.com/hyperledger/fabric-gateway/pkg/client" +) + +// Invoke handles chaincode invoke requests. +func (setup *OrgSetup) Invoke(w http.ResponseWriter, r *http.Request) { + fmt.Println("Received Invoke request") + if err := r.ParseForm(); err != nil { + fmt.Fprintf(w, "ParseForm() err: %s", err) + return + } + chainCodeName := r.FormValue("chaincodeid") + channelID := r.FormValue("channelid") + function := r.FormValue("function") + args := r.Form["args"] + fmt.Printf("channel: %s, chaincode: %s, function: %s, args: %s\n", channelID, chainCodeName, function, args) + network := setup.Gateway.GetNetwork(channelID) + contract := network.GetContract(chainCodeName) + txn_proposal, err := contract.NewProposal(function, client.WithArguments(args...)) + if err != nil { + fmt.Fprintf(w, "Error creating txn proposal: %s", err) + return + } + txn_endorsed, err := txn_proposal.Endorse() + if err != nil { + fmt.Fprintf(w, "Error endorsing txn: %s", err) + return + } + txn_committed, err := txn_endorsed.Submit() + if err != nil { + fmt.Fprintf(w, "Error submitting transaction: %s", err) + return + } + fmt.Fprintf(w, "Transaction ID : %s Response: %s", txn_committed.TransactionID(), txn_endorsed.Result()) +} diff --git a/asset-transfer-basic/rest-api-go/web/query.go b/asset-transfer-basic/rest-api-go/web/query.go new file mode 100644 index 00000000..eff1a70a --- /dev/null +++ b/asset-transfer-basic/rest-api-go/web/query.go @@ -0,0 +1,25 @@ +package web + +import ( + "fmt" + "net/http" +) + +// Query handles chaincode query requests. +func (setup OrgSetup) Query(w http.ResponseWriter, r *http.Request) { + fmt.Println("Received Query request") + queryParams := r.URL.Query() + chainCodeName := queryParams.Get("chaincodeid") + channelID := queryParams.Get("channelid") + function := queryParams.Get("function") + args := r.URL.Query()["args"] + fmt.Printf("channel: %s, chaincode: %s, function: %s, args: %s\n", channelID, chainCodeName, function, args) + network := setup.Gateway.GetNetwork(channelID) + contract := network.GetContract(chainCodeName) + evaluateResponse, err := contract.EvaluateTransaction(function, args...) + if err != nil { + fmt.Fprintf(w, "Error: %s", err) + return + } + fmt.Fprintf(w, "Response: %s", evaluateResponse) +}