diff --git a/asset-transfer-private-data/application-gateway-go/connect.go b/asset-transfer-private-data/application-gateway-go/connect.go index 3bd768634..334ffd9fb 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 3b931492a..a78b84b0e 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 000000000..b3f2e2177 --- /dev/null +++ b/off_chain_data/application-go/app.go @@ -0,0 +1,61 @@ +/* + * Copyright 2024 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package main + +import ( + "errors" + "fmt" + "os" + "strings" + + "google.golang.org/grpc" +) + +var allCommands = map[string]func(clientConnection *grpc.ClientConn){ + "getAllAssets": getAllAssets, + "transact": transact, +} + +func main() { + commands := os.Args[1:] + if len(commands) == 0 { + printUsage() + panic(errors.New("missing command")) + } + + for _, name := range commands { + if _, exists := allCommands[name]; !exists { + printUsage() + panic(fmt.Errorf("unknown command: %s", name)) + } + fmt.Printf("command: %s\n", name) + } + + client := newGrpcConnection() + defer client.Close() + + for _, name := range commands { + command := allCommands[name] + command(client) + } +} + +func printUsage() { + fmt.Println("Arguments: [ ...]") + fmt.Printf("Available commands: %v\n", availableCommands()) +} + +func availableCommands() string { + result := make([]string, len(allCommands)) + i := 0 + for command := range allCommands { + result[i] = command + i++ + } + + return strings.Join(result, ", ") +} diff --git a/off_chain_data/application-go/connect.go b/off_chain_data/application-go/connect.go new file mode 100644 index 000000000..91a69676b --- /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 000000000..bc1f8d03f --- /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 000000000..9c6643dae --- /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 result bytes.Buffer + if err := json.Indent(&result, data, "", " "); err != nil { + panic(fmt.Errorf("failed to parse JSON: %w", err)) + } + return result.String() +} diff --git a/off_chain_data/application-go/go.mod b/off_chain_data/application-go/go.mod new file mode 100644 index 000000000..36ac5fff4 --- /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 000000000..c876dc53c --- /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 000000000..66b6e2fca --- /dev/null +++ b/off_chain_data/application-go/transact.go @@ -0,0 +1,84 @@ +package main + +import ( + "fmt" + + "github.com/google/uuid" + "github.com/hyperledger/fabric-gateway/pkg/client" + "google.golang.org/grpc" +) + +func transact(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) + app := newTransactApp(smartContract) + app.run() +} + +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 +) + +func (t *transactApp) run() { + for i := 0; i < int(t.batchSize); i++ { + go t.transact() + } +} + +func (t *transactApp) transact() { + anAsset := t.newAsset() + + t.smartContract.createAsset(anAsset) + fmt.Printf("\nCreated asset %s\n", anAsset.ID) + + // Transfer randomly 1 in 2 assets to a new owner. + if randomInt(2) == 0 { + newOwner := differentElement(owners, anAsset.Owner) + oldOwner := t.smartContract.transferAsset(anAsset.ID, newOwner) + fmt.Printf("Transferred asset %s from %s to %s\n", anAsset.ID, oldOwner, newOwner) + } + + // Delete randomly 1 in 4 created assets. + if randomInt(4) == 0 { + t.smartContract.deleteAsset(anAsset.ID) + fmt.Printf("Deleted asset %s\n", anAsset.ID) + } +} + +func (t *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 000000000..5f4992af5 --- /dev/null +++ b/off_chain_data/application-go/utils.go @@ -0,0 +1,30 @@ +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()) +} + +func differentElement(values []string, currentValue string) string { + candidateValues := []string{} + for _, v := range values { + if v != currentValue { + candidateValues = append(candidateValues, v) + } + } + return randomElement(candidateValues) +}