mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-17 15:35:09 +00:00
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. Implemented app.go entry point with error handling. The existing commands are getAllAssets, transact and listen. They can be called from the command line via: "go run . <command> <command> ...". They will be executed in order and if a command is not known an the application panics and aborts before executing any of the commands. Implementing listen.go. Added checkpointer, context setups, call to BlockEvents and all the interfaces needed for parsing. Started implementing the interfaces needed to represent a block bottom up in structs. Finished NamespaceReadWriteSet, ReadWriteSet and EndorserTransaction. Signed-off-by: Stanislav Jakuschevskij <stas@two-giants.com>
This commit is contained in:
parent
aa0c9d3004
commit
7244639e7c
12 changed files with 851 additions and 5 deletions
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
```
|
||||
|
|
|
|||
62
off_chain_data/application-go/app.go
Normal file
62
off_chain_data/application-go/app.go
Normal file
|
|
@ -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.Printf("command: %s\n", name)
|
||||
}
|
||||
|
||||
client := newGrpcConnection()
|
||||
defer client.Close()
|
||||
|
||||
for _, name := range commands {
|
||||
command := allCommands[name]
|
||||
command(client)
|
||||
}
|
||||
}
|
||||
|
||||
func printUsage() {
|
||||
fmt.Println("Arguments: <command1> [<command2> ...]")
|
||||
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, ", ")
|
||||
}
|
||||
277
off_chain_data/application-go/blockParser.go
Normal file
277
off_chain_data/application-go/blockParser.go
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/hyperledger/fabric-gateway/pkg/identity"
|
||||
"github.com/hyperledger/fabric-protos-go-apiv2/common"
|
||||
"github.com/hyperledger/fabric-protos-go-apiv2/ledger/rwset"
|
||||
"github.com/hyperledger/fabric-protos-go-apiv2/ledger/rwset/kvrwset"
|
||||
"github.com/hyperledger/fabric-protos-go-apiv2/peer"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type Block interface {
|
||||
GetNumber() uint64
|
||||
GetTransactions() []Transaction
|
||||
ToProto() *common.Block
|
||||
}
|
||||
|
||||
type Transaction interface {
|
||||
GetChannelHeader() *common.ChannelHeader
|
||||
GetCreator() identity.Identity
|
||||
GetValidationCode() uint64
|
||||
IsValid() bool
|
||||
GetNamespaceReadWriteSets() []NamespaceReadWriteSet
|
||||
ToProto() *common.Payload
|
||||
}
|
||||
|
||||
type ParsedBlock struct {
|
||||
block *common.Block
|
||||
validationCodes []byte
|
||||
transactions []Transaction
|
||||
}
|
||||
|
||||
func NewParsedBlock(block *common.Block) Block {
|
||||
validationCodes := getTransactionValidationCodes(block)
|
||||
|
||||
return &ParsedBlock{block, validationCodes, nil}
|
||||
}
|
||||
|
||||
func (pb *ParsedBlock) GetNumber() uint64 {
|
||||
header := assertDefined(pb.block.GetHeader(), "missing block header")
|
||||
return header.GetNumber()
|
||||
}
|
||||
|
||||
// TODO: needs cache, getPayloads, parsePayload
|
||||
func (pb *ParsedBlock) GetTransactions() []Transaction {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pb *ParsedBlock) ToProto() *common.Block {
|
||||
return nil
|
||||
}
|
||||
|
||||
type EndorserTransaction interface {
|
||||
GetReadWriteSets() []ReadWriteSet
|
||||
ToProto() *peer.Transaction
|
||||
}
|
||||
|
||||
type ParsedEndorserTransaction struct {
|
||||
transaction *peer.Transaction
|
||||
}
|
||||
|
||||
func NewParsedEndorserTransaction(transaction *peer.Transaction) EndorserTransaction {
|
||||
return &ParsedEndorserTransaction{transaction}
|
||||
}
|
||||
|
||||
// TODO add cache
|
||||
func (p *ParsedEndorserTransaction) GetReadWriteSets() []ReadWriteSet {
|
||||
chaincodeActionPayloads := p.getChaincodeActionPayloads()
|
||||
|
||||
chaincodeEndorsedActions := p.getChaincodeEndorsedActions(chaincodeActionPayloads)
|
||||
|
||||
proposalResponsePayloads := p.getProposalResponsePayloads(chaincodeEndorsedActions)
|
||||
|
||||
chaincodeActions := p.getChaincodeActions(proposalResponsePayloads)
|
||||
|
||||
txReadWriteSets := p.getTxReadWriteSets(chaincodeActions)
|
||||
|
||||
parsedReadWriteSets := p.parseReadWriteSets(txReadWriteSets)
|
||||
|
||||
return parsedReadWriteSets
|
||||
}
|
||||
|
||||
func (p *ParsedEndorserTransaction) getChaincodeActionPayloads() []*peer.ChaincodeActionPayload {
|
||||
result := []*peer.ChaincodeActionPayload{}
|
||||
for _, transactionAction := range p.transaction.GetActions() {
|
||||
chaincodeActionPayload := &peer.ChaincodeActionPayload{}
|
||||
if err := proto.Unmarshal(transactionAction.GetPayload(), chaincodeActionPayload); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
result = append(result, chaincodeActionPayload)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (*ParsedEndorserTransaction) getChaincodeEndorsedActions(chaincodeActionPayloads []*peer.ChaincodeActionPayload) []*peer.ChaincodeEndorsedAction {
|
||||
result := []*peer.ChaincodeEndorsedAction{}
|
||||
for _, payload := range chaincodeActionPayloads {
|
||||
result = append(
|
||||
result,
|
||||
assertDefined(
|
||||
payload.GetAction(),
|
||||
"missing chaincode endorsed action",
|
||||
),
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (*ParsedEndorserTransaction) getProposalResponsePayloads(chaincodeEndorsedActions []*peer.ChaincodeEndorsedAction) []*peer.ProposalResponsePayload {
|
||||
result := []*peer.ProposalResponsePayload{}
|
||||
for _, endorsedAction := range chaincodeEndorsedActions {
|
||||
proposalResponsePayload := &peer.ProposalResponsePayload{}
|
||||
if err := proto.Unmarshal(endorsedAction.GetProposalResponsePayload(), proposalResponsePayload); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
result = append(result, proposalResponsePayload)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (*ParsedEndorserTransaction) getChaincodeActions(proposalResponsePayloads []*peer.ProposalResponsePayload) []*peer.ChaincodeAction {
|
||||
result := []*peer.ChaincodeAction{}
|
||||
for _, proposalResponsePayload := range proposalResponsePayloads {
|
||||
chaincodeAction := &peer.ChaincodeAction{}
|
||||
if err := proto.Unmarshal(proposalResponsePayload.GetExtension(), chaincodeAction); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
result = append(result, chaincodeAction)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (*ParsedEndorserTransaction) getTxReadWriteSets(chaincodeActions []*peer.ChaincodeAction) []*rwset.TxReadWriteSet {
|
||||
result := []*rwset.TxReadWriteSet{}
|
||||
for _, chaincodeAction := range chaincodeActions {
|
||||
txReadWriteSet := &rwset.TxReadWriteSet{}
|
||||
if err := proto.Unmarshal(chaincodeAction.GetResults(), txReadWriteSet); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, txReadWriteSet)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (*ParsedEndorserTransaction) parseReadWriteSets(txReadWriteSets []*rwset.TxReadWriteSet) []ReadWriteSet {
|
||||
result := []ReadWriteSet{}
|
||||
for _, txReadWriteSet := range txReadWriteSets {
|
||||
parsedReadWriteSet := NewParsedReadWriteSet(txReadWriteSet)
|
||||
result = append(result, parsedReadWriteSet)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (p *ParsedEndorserTransaction) ToProto() *peer.Transaction {
|
||||
return p.transaction
|
||||
}
|
||||
|
||||
type ReadWriteSet interface {
|
||||
GetNamespaceReadWriteSets() []NamespaceReadWriteSet
|
||||
ToProto() *rwset.TxReadWriteSet
|
||||
}
|
||||
|
||||
type ParsedReadWriteSet struct {
|
||||
readWriteSet *rwset.TxReadWriteSet
|
||||
}
|
||||
|
||||
func NewParsedReadWriteSet(rwSet *rwset.TxReadWriteSet) ReadWriteSet {
|
||||
return &ParsedReadWriteSet{rwSet}
|
||||
}
|
||||
|
||||
func (p *ParsedReadWriteSet) GetNamespaceReadWriteSets() []NamespaceReadWriteSet {
|
||||
result := []NamespaceReadWriteSet{}
|
||||
for _, nsReadWriteSet := range p.readWriteSet.GetNsRwset() {
|
||||
parsedNamespaceReadWriteSet := NewParsedNamespaceReadWriteSet(nsReadWriteSet)
|
||||
result = append(result, parsedNamespaceReadWriteSet)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (p *ParsedReadWriteSet) ToProto() *rwset.TxReadWriteSet {
|
||||
return p.readWriteSet
|
||||
}
|
||||
|
||||
type NamespaceReadWriteSet interface {
|
||||
GetNamespace() string
|
||||
GetReadWriteSet() *kvrwset.KVRWSet
|
||||
ToProto() *rwset.NsReadWriteSet
|
||||
}
|
||||
|
||||
type ParsedNamespaceReadWriteSet struct {
|
||||
nsReadWriteSet *rwset.NsReadWriteSet
|
||||
}
|
||||
|
||||
func NewParsedNamespaceReadWriteSet(nsRwSet *rwset.NsReadWriteSet) NamespaceReadWriteSet {
|
||||
return &ParsedNamespaceReadWriteSet{nsRwSet}
|
||||
}
|
||||
|
||||
func (p *ParsedNamespaceReadWriteSet) GetNamespace() string {
|
||||
return p.nsReadWriteSet.GetNamespace()
|
||||
}
|
||||
|
||||
// TODO add cache
|
||||
func (p *ParsedNamespaceReadWriteSet) GetReadWriteSet() *kvrwset.KVRWSet {
|
||||
result := kvrwset.KVRWSet{}
|
||||
if err := proto.Unmarshal(p.nsReadWriteSet.GetRwset(), &result); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
func (p *ParsedNamespaceReadWriteSet) ToProto() *rwset.NsReadWriteSet {
|
||||
return p.nsReadWriteSet
|
||||
}
|
||||
|
||||
func (pb *ParsedBlock) payloads() []*common.Payload {
|
||||
var payloads []*common.Payload
|
||||
|
||||
for _, envelopeBytes := range pb.block.GetData().GetData() {
|
||||
envelope := &common.Envelope{}
|
||||
if err := proto.Unmarshal(envelopeBytes, envelope); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
payload := &common.Payload{}
|
||||
if err := proto.Unmarshal(envelope.Payload, payload); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
payloads = append(payloads, payload)
|
||||
}
|
||||
|
||||
return payloads
|
||||
}
|
||||
|
||||
// TODO not sure about this
|
||||
func (pb *ParsedBlock) statusCode(txIndex int) peer.TxValidationCode {
|
||||
blockMetadata := assertDefined(
|
||||
pb.block.GetMetadata(),
|
||||
"missing block metadata",
|
||||
)
|
||||
|
||||
metadata := blockMetadata.GetMetadata()
|
||||
if int(common.BlockMetadataIndex_TRANSACTIONS_FILTER) >= len(metadata) {
|
||||
return peer.TxValidationCode_INVALID_OTHER_REASON
|
||||
}
|
||||
|
||||
statusCodes := metadata[common.BlockMetadataIndex_TRANSACTIONS_FILTER]
|
||||
if txIndex >= len(statusCodes) {
|
||||
return peer.TxValidationCode_INVALID_OTHER_REASON
|
||||
}
|
||||
|
||||
return peer.TxValidationCode(statusCodes[txIndex])
|
||||
}
|
||||
|
||||
type Payload interface {
|
||||
GetChannelHeader() *common.ChannelHeader
|
||||
GetEndorserTransaction() EndorserTransaction
|
||||
GetSignatureHeader() *common.SignatureHeader
|
||||
GetTransactionValidationCode() uint64
|
||||
IsEndorserTransaction() bool
|
||||
IsValid() bool
|
||||
ToProto() *common.Payload
|
||||
}
|
||||
|
||||
func getTransactionValidationCodes(block *common.Block) []byte {
|
||||
metadata := assertDefined(
|
||||
block.GetMetadata(),
|
||||
"missing block metadata",
|
||||
)
|
||||
|
||||
return assertDefined(
|
||||
metadata.GetMetadata()[common.BlockMetadataIndex_TRANSACTIONS_FILTER],
|
||||
"missing transaction validation code",
|
||||
)
|
||||
}
|
||||
142
off_chain_data/application-go/connect.go
Normal file
142
off_chain_data/application-go/connect.go
Normal file
|
|
@ -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]))
|
||||
}
|
||||
93
off_chain_data/application-go/contract.go
Normal file
93
off_chain_data/application-go/contract.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright 2024 IBM All Rights Reserved.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hyperledger/fabric-gateway/pkg/client"
|
||||
)
|
||||
|
||||
type Asset struct {
|
||||
ID string
|
||||
Color string
|
||||
Size uint64
|
||||
Owner string
|
||||
AppraisedValue uint64
|
||||
}
|
||||
|
||||
func 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),
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
39
off_chain_data/application-go/getAllAssets.go
Normal file
39
off_chain_data/application-go/getAllAssets.go
Normal file
|
|
@ -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()
|
||||
}
|
||||
22
off_chain_data/application-go/go.mod
Normal file
22
off_chain_data/application-go/go.mod
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
module offChainData
|
||||
|
||||
go 1.22.7
|
||||
|
||||
toolchain go1.23.0
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hyperledger/fabric-gateway v1.7.0
|
||||
github.com/hyperledger/fabric-protos-go-apiv2 v0.3.4
|
||||
google.golang.org/grpc v1.68.0
|
||||
google.golang.org/protobuf v1.35.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
golang.org/x/crypto v0.29.0 // indirect
|
||||
golang.org/x/net v0.31.0 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/text v0.20.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 // indirect
|
||||
)
|
||||
36
off_chain_data/application-go/go.sum
Normal file
36
off_chain_data/application-go/go.sum
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
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/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
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.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
|
||||
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
|
||||
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
|
||||
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
|
||||
google.golang.org/protobuf v1.35.2/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=
|
||||
68
off_chain_data/application-go/listen.go
Normal file
68
off_chain_data/application-go/listen.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"github.com/hyperledger/fabric-gateway/pkg/client"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var checkpointFile = envOrDefault("CHECKPOINT_FILE", "checkpoint.json")
|
||||
var simulatedFailureCount = getSimulatedFailureCount()
|
||||
|
||||
func listen(clientConnection *grpc.ClientConn) {
|
||||
id, options := newConnectOptions(clientConnection)
|
||||
gateway, err := client.Connect(id, options...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer gateway.Close()
|
||||
|
||||
network := gateway.GetNetwork(channelName)
|
||||
|
||||
checkpointer, err := client.NewFileCheckpointer(checkpointFile)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer checkpointer.Close()
|
||||
|
||||
fmt.Printf("Start event listening from block %d\n", checkpointer.BlockNumber())
|
||||
fmt.Printf("Last processed transaction ID within block: %s\n", checkpointer.TransactionID())
|
||||
if simulatedFailureCount > 0 {
|
||||
fmt.Printf("Simulating a write failure every %d transactions", simulatedFailureCount)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
blocks, err := network.BlockEvents(
|
||||
ctx,
|
||||
client.WithStartBlock(0),
|
||||
client.WithCheckpoint(checkpointer),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for blockProto := range blocks {
|
||||
checkpointer.CheckpointBlock(blockProto.GetHeader().GetNumber())
|
||||
}
|
||||
}
|
||||
|
||||
func getSimulatedFailureCount() uint {
|
||||
valueAsString := envOrDefault("SIMULATED_FAILURE_COUNT", "0")
|
||||
valueAsFloat, err := strconv.ParseFloat(valueAsString, 64)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
result := math.Floor(valueAsFloat)
|
||||
if valueAsFloat < 0 {
|
||||
panic(fmt.Errorf("invalid SIMULATED_FAILURE_COUNT value: %s", valueAsString))
|
||||
}
|
||||
|
||||
return uint(result)
|
||||
}
|
||||
68
off_chain_data/application-go/transact.go
Normal file
68
off_chain_data/application-go/transact.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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 := 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)
|
||||
}
|
||||
}
|
||||
39
off_chain_data/application-go/utils.go
Normal file
39
off_chain_data/application-go/utils.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"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)
|
||||
}
|
||||
|
||||
func assertDefined[T any](value T, message string) T {
|
||||
if any(value) == any(nil) {
|
||||
panic(errors.New(message))
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
Loading…
Reference in a new issue