Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🚧 WIP: Add off-chain-data go client application 🚧 #1269

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2022 IBM All Rights Reserved.
Copyright 2024 IBM All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
Expand Down Expand Up @@ -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))
}
Expand Down
4 changes: 2 additions & 2 deletions off_chain_data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -112,4 +112,4 @@ When you are finished, you can bring down the test network (from the `test-netwo

```
./network.sh down
```
```
62 changes: 62 additions & 0 deletions off_chain_data/application-go/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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,
"listen": listen,
}

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.Println("command:", name)
}

client := newGrpcConnection()
defer client.Close()

for _, name := range commands {
command := allCommands[name]
command(client)
}
}

func printUsage() {
fmt.Println("Arguments: <command1> [<command2> ...]")
fmt.Println("Available commands:", availableCommands())
}

func availableCommands() string {
result := make([]string, len(allCommands))
i := 0
for command := range allCommands {
result[i] = command
i++
}

return strings.Join(result, ", ")
}
135 changes: 135 additions & 0 deletions off_chain_data/application-go/connect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright 2024 IBM All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/

package main

import (
"crypto/x509"
"fmt"
"offChainData/utils"
"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 = utils.EnvOrDefault("CHANNEL_NAME", "mychannel")
chaincodeName = utils.EnvOrDefault("CHAINCODE_NAME", "basic")
mspID = utils.EnvOrDefault("MSP_ID", "Org1MSP")

// Path to crypto materials.
cryptoPath = utils.EnvOrDefault("CRYPTO_PATH", "../../test-network/organizations/peerOrganizations/org1.example.com")

// Path to user private key directory.
keyDirectoryPath = utils.EnvOrDefault("KEY_DIRECTORY_PATH", cryptoPath+"/users/[email protected]/msp/keystore")

// Path to user certificate.
certPath = utils.EnvOrDefault("CERT_PATH", cryptoPath+"/users/[email protected]/msp/signcerts/cert.pem")

// Path to peer tls certificate.
tlsCertPath = utils.EnvOrDefault("TLS_CERT_PATH", cryptoPath+"/peers/peer0.org1.example.com/tls/ca.crt")

// Gateway peer endpoint.
peerEndpoint = utils.EnvOrDefault("PEER_ENDPOINT", "dns:///localhost:7051")

// Gateway peer SSL host name override.
peerHostAlias = utils.EnvOrDefault("PEER_HOST_ALIAS", peerName)
)

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]))
}
68 changes: 68 additions & 0 deletions off_chain_data/application-go/contract/contract.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright 2024 IBM All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/
package contract

import (
"strconv"

"github.com/hyperledger/fabric-gateway/pkg/client"
)

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
}
45 changes: 45 additions & 0 deletions off_chain_data/application-go/contract/model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2024 IBM All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/
package contract

import (
"offChainData/utils"

"github.com/google/uuid"
)

var (
colors = []string{"red", "green", "blue"}
Owners = []string{"alice", "bob", "charlie"}
)

const (
maxInitialValue = 1000
maxInitialSize = 10
)

type Asset struct {
ID string `json:"ID"`
Color string `json:"Color"`
Size uint64 `json:"Size"`
Owner string `json:"Owner"`
AppraisedValue uint64 `json:"AppraisedValue"`
}

func NewAsset() Asset {
id, err := uuid.NewRandom()
if err != nil {
panic(err)
}

return Asset{
ID: id.String(),
Color: utils.RandomElement(colors),
Size: uint64(utils.RandomInt(maxInitialSize) + 1),
Owner: utils.RandomElement(Owners),
AppraisedValue: uint64(utils.RandomInt(maxInitialValue) + 1),
}
}
40 changes: 40 additions & 0 deletions off_chain_data/application-go/getAllAssets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2024 IBM All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/

package main

import (
"bytes"
"encoding/json"
"fmt"
atb "offChainData/contract"

"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 := atb.NewAssetTransferBasic(contract)
assets := smartContract.GetAllAssets()

fmt.Println(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()
}
Loading
Loading