From 09402bc07640de71d2435d07cb0453cac0547966 Mon Sep 17 00:00:00 2001 From: Stanislav Jakuschevskij Date: Thu, 7 Nov 2024 20:16:26 +0100 Subject: [PATCH] Add off-chain-data go client application Created project structure, fixed typos. Implemented connect.go and getAllAssets.go. The latter uses an assetTransferBasic struct which provides a simple API for basic asset operations like create, transfer, etc. Added transact.go with some util functions. Using google uuid package to generate random UUIDs for the transactions. Implemented pretty printing of JSON results. Signed-off-by: Stanislav Jakuschevskij --- .../application-gateway-go/connect.go | 6 +- off_chain_data/README.md | 4 +- off_chain_data/application-go/app.go | 14 ++ off_chain_data/application-go/connect.go | 142 ++++++++++++++++++ off_chain_data/application-go/contract.go | 77 ++++++++++ off_chain_data/application-go/getAllAssets.go | 39 +++++ off_chain_data/application-go/go.mod | 20 +++ off_chain_data/application-go/go.sum | 34 +++++ off_chain_data/application-go/transact.go | 55 +++++++ off_chain_data/application-go/utils.go | 20 +++ 10 files changed, 406 insertions(+), 5 deletions(-) create mode 100644 off_chain_data/application-go/app.go create mode 100644 off_chain_data/application-go/connect.go create mode 100644 off_chain_data/application-go/contract.go create mode 100644 off_chain_data/application-go/getAllAssets.go create mode 100644 off_chain_data/application-go/go.mod create mode 100644 off_chain_data/application-go/go.sum create mode 100644 off_chain_data/application-go/transact.go create mode 100644 off_chain_data/application-go/utils.go diff --git a/asset-transfer-private-data/application-gateway-go/connect.go b/asset-transfer-private-data/application-gateway-go/connect.go index 3bd768634c..334ffd9fb4 100644 --- a/asset-transfer-private-data/application-gateway-go/connect.go +++ b/asset-transfer-private-data/application-gateway-go/connect.go @@ -1,5 +1,5 @@ /* -Copyright 2022 IBM All Rights Reserved. +Copyright 2024 IBM All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ @@ -77,8 +77,8 @@ func newIdentity(certDirectoryPath, mspId string) *identity.X509Identity { } // newSign creates a function that generates a digital signature from a message digest using a private key. -func newSign(keyDirectoryPash string) identity.Sign { - privateKeyPEM, err := readFirstFile(keyDirectoryPash) +func newSign(keyDirectoryPath string) identity.Sign { + privateKeyPEM, err := readFirstFile(keyDirectoryPath) if err != nil { panic(fmt.Errorf("failed to read private key file: %w", err)) } diff --git a/off_chain_data/README.md b/off_chain_data/README.md index 3b931492ac..a78b84b0e9 100644 --- a/off_chain_data/README.md +++ b/off_chain_data/README.md @@ -28,7 +28,7 @@ The client application provides several "commands" that can be invoked using the To keep the sample code concise, the **listen** command writes ledger updates to an output file named `store.log` in the current working directory (which for the Java sample is the `application-java/app` directory). A real implementation could write ledger updates directly to an off-chain data store of choice. You can inspect the information captured in this file as you run the sample. -Note that the **listen** command is is restartable and will resume event listening after the last successfully processed block / transaction. This is achieved using a checkpointer to persist the current listening position. Checkpoint state is persisted to a file named `checkpoint.json` in the current working directory. If no checkpoint state is present, event listening begins from the start of the ledger (block number zero). +Note that the **listen** command is restartable and will resume event listening after the last successfully processed block / transaction. This is achieved using a checkpointer to persist the current listening position. Checkpoint state is persisted to a file named `checkpoint.json` in the current working directory. If no checkpoint state is present, event listening begins from the start of the ledger (block number zero). ### Smart Contract @@ -112,4 +112,4 @@ When you are finished, you can bring down the test network (from the `test-netwo ``` ./network.sh down -``` \ No newline at end of file +``` diff --git a/off_chain_data/application-go/app.go b/off_chain_data/application-go/app.go new file mode 100644 index 0000000000..eb2e5c01f7 --- /dev/null +++ b/off_chain_data/application-go/app.go @@ -0,0 +1,14 @@ +/* + * Copyright 2024 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package main + +func main() { + client := newGrpcConnection() + defer client.Close() + + getAllAssets(client) +} diff --git a/off_chain_data/application-go/connect.go b/off_chain_data/application-go/connect.go new file mode 100644 index 0000000000..91a69676b9 --- /dev/null +++ b/off_chain_data/application-go/connect.go @@ -0,0 +1,142 @@ +/* + * Copyright 2024 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package main + +import ( + "crypto/x509" + "fmt" + "os" + "path" + "time" + + "github.com/hyperledger/fabric-gateway/pkg/client" + "github.com/hyperledger/fabric-gateway/pkg/hash" + "github.com/hyperledger/fabric-gateway/pkg/identity" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +const peerName = "peer0.org1.example.com" + +var ( + channelName = envOrDefault("CHANNEL_NAME", "mychannel") + chaincodeName = envOrDefault("CHAINCODE_NAME", "basic") + mspID = envOrDefault("MSP_ID", "Org1MSP") + + // Path to crypto materials. + cryptoPath = envOrDefault("CRYPTO_PATH", "../../test-network/organizations/peerOrganizations/org1.example.com") + + // Path to user private key directory. + keyDirectoryPath = envOrDefault("KEY_DIRECTORY_PATH", cryptoPath+"/users/User1@org1.example.com/msp/keystore") + + // Path to user certificate. + certPath = envOrDefault("CERT_PATH", cryptoPath+"/users/User1@org1.example.com/msp/signcerts/cert.pem") + + // Path to peer tls certificate. + tlsCertPath = envOrDefault("TLS_CERT_PATH", cryptoPath+"/peers/peer0.org1.example.com/tls/ca.crt") + + // Gateway peer endpoint. + peerEndpoint = envOrDefault("PEER_ENDPOINT", "dns:///localhost:7051") + + // Gateway peer SSL host name override. + peerHostAlias = envOrDefault("PEER_HOST_ALIAS", peerName) +) + +func envOrDefault(key, defaultValue string) string { + result := os.Getenv(key) + if result == "" { + return defaultValue + } + return result +} + +func newGrpcConnection() *grpc.ClientConn { + certificatePEM, err := os.ReadFile(tlsCertPath) + if err != nil { + panic(fmt.Errorf("failed to read TLS certificate file: %w", err)) + } + + certificate, err := identity.CertificateFromPEM(certificatePEM) + if err != nil { + panic(err) + } + + certPool := x509.NewCertPool() + certPool.AddCert(certificate) + transportCredentials := credentials.NewClientTLSFromCert(certPool, peerHostAlias) + + connection, err := grpc.NewClient(peerEndpoint, grpc.WithTransportCredentials(transportCredentials)) + if err != nil { + panic(fmt.Errorf("failed to create gRPC connection: %w", err)) + } + + return connection +} + +func newConnectOptions(clientConnection *grpc.ClientConn) (identity.Identity, []client.ConnectOption) { + return newIdentity(), []client.ConnectOption{ + client.WithSign(newSign()), + client.WithHash(hash.SHA256), + client.WithClientConnection(clientConnection), + client.WithEvaluateTimeout(5 * time.Second), + client.WithEndorseTimeout(15 * time.Second), + client.WithSubmitTimeout(5 * time.Second), + client.WithCommitStatusTimeout(1 * time.Minute), + } +} + +func newIdentity() *identity.X509Identity { + certificatePEM, err := os.ReadFile(certPath) + if err != nil { + panic(fmt.Errorf("failed to read certificate file: %w", err)) + } + + certificate, err := identity.CertificateFromPEM(certificatePEM) + if err != nil { + panic(err) + } + + id, err := identity.NewX509Identity(mspID, certificate) + if err != nil { + panic(err) + } + + return id +} + +func newSign() identity.Sign { + privateKeyPEM, err := readFirstFile(keyDirectoryPath) + 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 readFirstFile(dirPath string) ([]byte, error) { + dir, err := os.Open(dirPath) + if err != nil { + return nil, err + } + + fileNames, err := dir.Readdirnames(1) + if err != nil { + return nil, err + } + + return os.ReadFile(path.Join(dirPath, fileNames[0])) +} diff --git a/off_chain_data/application-go/contract.go b/off_chain_data/application-go/contract.go new file mode 100644 index 0000000000..bc1f8d03fa --- /dev/null +++ b/off_chain_data/application-go/contract.go @@ -0,0 +1,77 @@ +/* + * Copyright 2024 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package main + +import ( + "strconv" + + "github.com/hyperledger/fabric-gateway/pkg/client" +) + +type asset struct { + ID string + Color string + Size uint64 + Owner string + AppraisedValue uint64 +} + +type assetTransferBasic struct { + contract *client.Contract +} + +func newAssetTransferBasic(contract *client.Contract) *assetTransferBasic { + return &assetTransferBasic{contract} +} + +func (atb *assetTransferBasic) createAsset(anAsset asset) { + if _, err := atb.contract.Submit( + "CreateAsset", + client.WithArguments( + anAsset.ID, + anAsset.Color, + strconv.FormatUint(anAsset.Size, 10), + anAsset.Owner, + strconv.FormatUint(anAsset.AppraisedValue, 10), + )); err != nil { + panic(err) + } +} + +func (atb *assetTransferBasic) transferAsset(id, newOwner string) string { + result, err := atb.contract.Submit( + "TransferAsset", + client.WithArguments( + id, + newOwner, + ), + ) + if err != nil { + panic(err) + } + + return string(result) +} + +func (atb *assetTransferBasic) deleteAsset(id string) { + if _, err := atb.contract.Submit( + "DeleteAsset", + client.WithArguments( + id, + ), + ); err != nil { + panic(err) + } +} + +func (atb *assetTransferBasic) getAllAssets() []byte { + result, err := atb.contract.Evaluate("GetAllAssets") + if err != nil { + panic(err) + } + return result +} diff --git a/off_chain_data/application-go/getAllAssets.go b/off_chain_data/application-go/getAllAssets.go new file mode 100644 index 0000000000..d59471809d --- /dev/null +++ b/off_chain_data/application-go/getAllAssets.go @@ -0,0 +1,39 @@ +/* + * Copyright 2024 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package main + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/hyperledger/fabric-gateway/pkg/client" + "google.golang.org/grpc" +) + +func getAllAssets(clientConnection *grpc.ClientConn) { + id, options := newConnectOptions(clientConnection) + gateway, err := client.Connect(id, options...) + if err != nil { + panic((err)) + } + defer gateway.Close() + + contract := gateway.GetNetwork(channelName).GetContract(chaincodeName) + smartContract := newAssetTransferBasic(contract) + assets := smartContract.getAllAssets() + + fmt.Printf("%s\n", formatJSON(assets)) +} + +func formatJSON(data []byte) string { + var prettyJSON bytes.Buffer + if err := json.Indent(&prettyJSON, data, "", " "); err != nil { + panic(fmt.Errorf("failed to parse JSON: %w", err)) + } + return prettyJSON.String() +} diff --git a/off_chain_data/application-go/go.mod b/off_chain_data/application-go/go.mod new file mode 100644 index 0000000000..36ac5fff48 --- /dev/null +++ b/off_chain_data/application-go/go.mod @@ -0,0 +1,20 @@ +module offChainData + +go 1.22.0 + +require ( + github.com/hyperledger/fabric-gateway v1.7.0 + google.golang.org/grpc v1.67.1 +) + +require ( + github.com/google/uuid v1.6.0 // indirect + github.com/hyperledger/fabric-protos-go-apiv2 v0.3.4 // indirect + github.com/miekg/pkcs11 v1.1.1 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect + google.golang.org/protobuf v1.35.1 // indirect +) diff --git a/off_chain_data/application-go/go.sum b/off_chain_data/application-go/go.sum new file mode 100644 index 0000000000..c876dc53cf --- /dev/null +++ b/off_chain_data/application-go/go.sum @@ -0,0 +1,34 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hyperledger/fabric-gateway v1.7.0 h1:bd1quU8qYPYqYO69m1tPIDSjB+D+u/rBJfE1eWFcpjY= +github.com/hyperledger/fabric-gateway v1.7.0/go.mod h1:TItDGnq71eJcgz5TW+m5Sq3kWGp0AEI1HPCNxj0Eu7k= +github.com/hyperledger/fabric-protos-go-apiv2 v0.3.4 h1:YJrd+gMaeY0/vsN0aS0QkEKTivGoUnSRIXxGJ7KI+Pc= +github.com/hyperledger/fabric-protos-go-apiv2 v0.3.4/go.mod h1:bau/6AJhvEcu9GKKYHlDXAxXKzYNfhP6xu2GXuxEcFk= +github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= +github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/off_chain_data/application-go/transact.go b/off_chain_data/application-go/transact.go new file mode 100644 index 0000000000..22fb9bd847 --- /dev/null +++ b/off_chain_data/application-go/transact.go @@ -0,0 +1,55 @@ +package main + +import ( + "fmt" + + "github.com/google/uuid" +) + +type transactApp struct { + smartContract assetTransferBasic + batchSize uint +} + +func newTransactApp(smartContract assetTransferBasic) *transactApp { + return &transactApp{smartContract, 10} +} + +var ( + colors = []string{"red", "green", "blue"} + owners = []string{"alice", "bob", "charlie"} +) + +const ( + maxInitialValue = 1000 + maxInitialSize = 10 +) + +// use go routine +func (ta *transactApp) run() { + for i := 0; i < int(ta.batchSize); i++ { + go ta.transact() + } +} + +func (ta *transactApp) transact() { + anAsset := ta.newAsset() + ta.smartContract.createAsset(anAsset) + fmt.Printf("Created asset %s\n", anAsset.ID) + // CONTINUE HERE +} + +func (ta *transactApp) newAsset() asset { + id, err := uuid.NewRandom() + if err != nil { + panic(err) + } + + return asset{ + ID: id.String(), + Color: randomElement(colors), + Size: uint64(randomInt(maxInitialSize) + 1), + Owner: randomElement(owners), + AppraisedValue: uint64(randomInt(maxInitialValue) + 1), + } +} diff --git a/off_chain_data/application-go/utils.go b/off_chain_data/application-go/utils.go new file mode 100644 index 0000000000..b734e01f20 --- /dev/null +++ b/off_chain_data/application-go/utils.go @@ -0,0 +1,20 @@ +package main + +import ( + "crypto/rand" + "math/big" +) + +func randomElement(values []string) string { + result := values[randomInt(len(values))] + return result +} + +func randomInt(max int) int { + result, err := rand.Int(rand.Reader, big.NewInt(int64(max))) + if err != nil { + panic(err) + } + + return int(result.Int64()) +}