mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-17 15:35:09 +00:00
Compare commits
3 commits
2a4704930e
...
f90b1b5ef1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f90b1b5ef1 | ||
|
|
a2c40e6522 | ||
|
|
d42cc27a98 |
10 changed files with 774 additions and 1 deletions
|
|
@ -0,0 +1,15 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/hyperledger/fabric-gateway/pkg/client"
|
||||||
|
|
||||||
|
type Command func(gw *client.Gateway, args []string) error
|
||||||
|
|
||||||
|
var commands = map[string]Command{
|
||||||
|
"create": cmdCreate,
|
||||||
|
"delete": cmdDelete,
|
||||||
|
"getAllAssets": cmdGetAllAssets,
|
||||||
|
"listen": cmdListen,
|
||||||
|
"read": cmdRead,
|
||||||
|
"transact": cmdTransact,
|
||||||
|
"transfer": cmdTransfer,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,296 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/hyperledger/fabric-gateway/pkg/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
// cmdCreate creates a new asset on the ledger.
|
||||||
|
// Arguments: <assetId> <ownerName> <color>
|
||||||
|
func cmdCreate(gw *client.Gateway, args []string) error {
|
||||||
|
if len(args) < 3 {
|
||||||
|
return fmt.Errorf("arguments: <assetId> <ownerName> <color>")
|
||||||
|
}
|
||||||
|
|
||||||
|
network := gw.GetNetwork(channelName())
|
||||||
|
contract := network.GetContract(chaincodeName())
|
||||||
|
|
||||||
|
smartContract := NewAssetTransfer(contract)
|
||||||
|
return smartContract.CreateAsset(Asset{
|
||||||
|
ID: args[0],
|
||||||
|
Owner: args[1],
|
||||||
|
Color: args[2],
|
||||||
|
Size: 1,
|
||||||
|
AppraisedValue: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmdDelete deletes an asset from the ledger.
|
||||||
|
// Arguments: <assetId>
|
||||||
|
func cmdDelete(gw *client.Gateway, args []string) error {
|
||||||
|
if len(args) < 1 {
|
||||||
|
return fmt.Errorf("arguments: <assetId>")
|
||||||
|
}
|
||||||
|
|
||||||
|
network := gw.GetNetwork(channelName())
|
||||||
|
contract := network.GetContract(chaincodeName())
|
||||||
|
|
||||||
|
smartContract := NewAssetTransfer(contract)
|
||||||
|
return smartContract.DeleteAsset(args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmdGetAllAssets queries and prints all assets currently on the ledger.
|
||||||
|
func cmdGetAllAssets(gw *client.Gateway, _ []string) error {
|
||||||
|
network := gw.GetNetwork(channelName())
|
||||||
|
contract := network.GetContract(chaincodeName())
|
||||||
|
|
||||||
|
smartContract := NewAssetTransfer(contract)
|
||||||
|
assets, err := smartContract.GetAllAssets()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(assets, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal assets: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range strings.Split(string(data), "\n") {
|
||||||
|
fmt.Println(line)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const startBlock = uint64(0)
|
||||||
|
|
||||||
|
func cmdListen(gw *client.Gateway, _ []string) error {
|
||||||
|
network := gw.GetNetwork(channelName())
|
||||||
|
|
||||||
|
checkpointFile := os.Getenv("CHECKPOINT_FILE")
|
||||||
|
if checkpointFile == "" {
|
||||||
|
checkpointFile = "checkpoint.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulatedFailureCount, err := getSimulatedFailureCount()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
checkpointer, err := client.NewFileCheckpointer(checkpointFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create checkpointer: %w", err)
|
||||||
|
}
|
||||||
|
defer checkpointer.Close()
|
||||||
|
|
||||||
|
displayBlock := checkpointer.BlockNumber()
|
||||||
|
if displayBlock == 0 {
|
||||||
|
displayBlock = startBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Starting event listening from block", displayBlock)
|
||||||
|
fmt.Println("Last processed transaction ID within block:", checkpointer.TransactionID())
|
||||||
|
if simulatedFailureCount > 0 {
|
||||||
|
fmt.Println("Simulating a write failure every", simulatedFailureCount, "transactions")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
events, err := network.ChaincodeEvents(ctx, chaincodeName(),
|
||||||
|
client.WithStartBlock(startBlock),
|
||||||
|
client.WithCheckpoint(checkpointer),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to start chaincode event subscription: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
eventCount := 0
|
||||||
|
for event := range events {
|
||||||
|
if simulatedFailureCount > 0 {
|
||||||
|
eventCount++
|
||||||
|
if eventCount >= simulatedFailureCount {
|
||||||
|
eventCount = 0
|
||||||
|
return &ExpectedError{Message: "Simulated write failure"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf("Chaincode event: BlockNumber=%d TxID=%s Name=%s Payload=%s\n",
|
||||||
|
event.BlockNumber, event.TransactionID, event.EventName, string(event.Payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSimulatedFailureCount() (int, error) {
|
||||||
|
value := os.Getenv("SIMULATED_FAILURE_COUNT")
|
||||||
|
if value == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
count, err := strconv.Atoi(value)
|
||||||
|
if err != nil || count < 0 {
|
||||||
|
return 0, fmt.Errorf("invalid SIMULATED_FAILURE_COUNT value: %s", value)
|
||||||
|
}
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmdRead reads and prints a single asset from the ledger.
|
||||||
|
// Arguments: <assetId>
|
||||||
|
func cmdRead(gw *client.Gateway, args []string) error {
|
||||||
|
if len(args) < 1 {
|
||||||
|
return fmt.Errorf("arguments: <assetId>")
|
||||||
|
}
|
||||||
|
|
||||||
|
network := gw.GetNetwork(channelName())
|
||||||
|
contract := network.GetContract(chaincodeName())
|
||||||
|
|
||||||
|
smartContract := NewAssetTransfer(contract)
|
||||||
|
asset, err := smartContract.ReadAsset(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(asset, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal asset: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(string(data))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
colors = []string{"red", "green", "blue"}
|
||||||
|
maxInitialSize = 10
|
||||||
|
maxInitialVal = 1000
|
||||||
|
)
|
||||||
|
|
||||||
|
// cmdTransact runs a batch of concurrent create/update/delete transactions to demonstrate
|
||||||
|
func cmdTransact(gw *client.Gateway, _ []string) error {
|
||||||
|
network := gw.GetNetwork(channelName())
|
||||||
|
contract := network.GetContract(chaincodeName())
|
||||||
|
|
||||||
|
smartContract := NewAssetTransfer(contract)
|
||||||
|
app := &transactApp{smartContract: smartContract, batchSize: 6}
|
||||||
|
return app.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
type transactApp struct {
|
||||||
|
smartContract *AssetTransfer
|
||||||
|
batchSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *transactApp) run() error {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
errCh := make(chan error, a.batchSize)
|
||||||
|
|
||||||
|
for i := 0; i < a.batchSize; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := a.transact(); err != nil {
|
||||||
|
errCh <- err
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
close(errCh)
|
||||||
|
|
||||||
|
var failures []string
|
||||||
|
for err := range errCh {
|
||||||
|
failures = append(failures, err.Error())
|
||||||
|
}
|
||||||
|
if len(failures) > 0 {
|
||||||
|
return fmt.Errorf("%d failures:\n- %s", len(failures), strings.Join(failures, "\n- "))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *transactApp) transact() error {
|
||||||
|
asset := a.newAsset()
|
||||||
|
|
||||||
|
if err := a.smartContract.CreateAsset(asset); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("Created asset %s\n", asset.ID)
|
||||||
|
|
||||||
|
if randomInt(2) == 0 {
|
||||||
|
oldColor := asset.Color
|
||||||
|
asset.Color = differentElement(colors, oldColor)
|
||||||
|
if err := a.smartContract.UpdateAsset(asset); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("Updated color of asset %s from %s to %s\n", asset.ID, oldColor, asset.Color)
|
||||||
|
}
|
||||||
|
|
||||||
|
if randomInt(4) == 0 {
|
||||||
|
if err := a.smartContract.DeleteAsset(asset.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("Deleted asset %s\n", asset.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *transactApp) newAsset() Asset {
|
||||||
|
return Asset{
|
||||||
|
ID: randomHexString(8),
|
||||||
|
Color: randomElement(colors),
|
||||||
|
Size: randomInt(maxInitialSize) + 1,
|
||||||
|
AppraisedValue: float64(randomInt(maxInitialVal) + 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomHexString(length int) string {
|
||||||
|
b := make([]byte, (length+1)/2)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
panic(fmt.Sprintf("failed to generate random bytes: %v", err))
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(b)[:length]
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomInt(max int) int {
|
||||||
|
n, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("failed to generate random int: %v", err))
|
||||||
|
}
|
||||||
|
return int(n.Int64())
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomElement(values []string) string {
|
||||||
|
return values[randomInt(len(values))]
|
||||||
|
}
|
||||||
|
|
||||||
|
func differentElement(values []string, currentValue string) string {
|
||||||
|
var candidates []string
|
||||||
|
for _, v := range values {
|
||||||
|
if v != currentValue {
|
||||||
|
candidates = append(candidates, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return randomElement(candidates)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmdTransfer transfers ownership of an asset to a new owner in a different organisation.
|
||||||
|
// Arguments: <assetId> <ownerName> <ownerMspId>
|
||||||
|
func cmdTransfer(gw *client.Gateway, args []string) error {
|
||||||
|
if len(args) < 3 {
|
||||||
|
return fmt.Errorf("arguments: <assetId> <ownerName> <ownerMspId>")
|
||||||
|
}
|
||||||
|
|
||||||
|
network := gw.GetNetwork(channelName())
|
||||||
|
contract := network.GetContract(chaincodeName())
|
||||||
|
|
||||||
|
smartContract := NewAssetTransfer(contract)
|
||||||
|
return smartContract.TransferAsset(args[0], args[1], args[2])
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func gatewayEndpoint() (string, error) {
|
||||||
|
return requireEnv("ENDPOINT")
|
||||||
|
}
|
||||||
|
|
||||||
|
func mspID() (string, error) {
|
||||||
|
return requireEnv("MSP_ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
func clientCertPath() (string, error) {
|
||||||
|
return requireEnv("CERTIFICATE")
|
||||||
|
}
|
||||||
|
|
||||||
|
func privateKeyPath() (string, error) {
|
||||||
|
return requireEnv("PRIVATE_KEY")
|
||||||
|
}
|
||||||
|
|
||||||
|
func tlsCertPath() string {
|
||||||
|
return os.Getenv("TLS_CERT")
|
||||||
|
}
|
||||||
|
|
||||||
|
func channelName() string {
|
||||||
|
if v := os.Getenv("CHANNEL_NAME"); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return "mychannel"
|
||||||
|
}
|
||||||
|
|
||||||
|
func chaincodeName() string {
|
||||||
|
if v := os.Getenv("CHAINCODE_NAME"); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return "asset-transfer"
|
||||||
|
}
|
||||||
|
|
||||||
|
func hostAlias() string {
|
||||||
|
return os.Getenv("HOST_ALIAS")
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireEnv(name string) (string, error) {
|
||||||
|
v := os.Getenv(name)
|
||||||
|
if v == "" {
|
||||||
|
printEnvUsage()
|
||||||
|
return "", fmt.Errorf("environment variable %s not set", name)
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printEnvUsage() {
|
||||||
|
fmt.Fprintln(os.Stderr, "The following environment variables must be set:")
|
||||||
|
fmt.Fprintln(os.Stderr, " ENDPOINT - Endpoint address of the gateway service")
|
||||||
|
fmt.Fprintln(os.Stderr, " MSP_ID - User's organization Member Services Provider ID")
|
||||||
|
fmt.Fprintln(os.Stderr, " CERTIFICATE - User's certificate file")
|
||||||
|
fmt.Fprintln(os.Stderr, " PRIVATE_KEY - User's private key file")
|
||||||
|
fmt.Fprintln(os.Stderr, "")
|
||||||
|
fmt.Fprintln(os.Stderr, "The following environment variables are optional:")
|
||||||
|
fmt.Fprintln(os.Stderr, " CHANNEL_NAME - Channel to which the chaincode is deployed")
|
||||||
|
fmt.Fprintln(os.Stderr, " CHAINCODE_NAME - Chaincode deployed to the channel")
|
||||||
|
fmt.Fprintln(os.Stderr, " TLS_CERT - TLS CA root certificate (only if using TLS and private CA)")
|
||||||
|
fmt.Fprintln(os.Stderr, " HOST_ALIAS - TLS hostname override (only if TLS cert does not match endpoint)")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"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"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newGrpcConnection creates a gRPC connection to the Gateway server.
|
||||||
|
// If TLS_CERT is set, TLS is used; otherwise an insecure connection is established.
|
||||||
|
func newGrpcConnection() (*grpc.ClientConn, error) {
|
||||||
|
endpoint, err := gatewayEndpoint()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsCert := tlsCertPath()
|
||||||
|
if tlsCert != "" {
|
||||||
|
certPEM, err := os.ReadFile(tlsCert)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read TLS certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := identity.CertificateFromPEM(certPEM)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse TLS certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
certPool := x509.NewCertPool()
|
||||||
|
certPool.AddCert(cert)
|
||||||
|
|
||||||
|
transportCreds := credentials.NewClientTLSFromCert(certPool, "")
|
||||||
|
opts := []grpc.DialOption{grpc.WithTransportCredentials(transportCreds)}
|
||||||
|
|
||||||
|
// Override TLS server name if endpoint address doesn't match the certificate
|
||||||
|
if alias := hostAlias(); alias != "" {
|
||||||
|
opts = append(opts, grpc.WithAuthority(alias))
|
||||||
|
}
|
||||||
|
|
||||||
|
return grpc.NewClient(endpoint, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return grpc.NewClient(endpoint, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// newGatewayConnection creates a Fabric Gateway connection using the provided gRPC connection.
|
||||||
|
func newGatewayConnection(grpcConn *grpc.ClientConn) (*client.Gateway, error) {
|
||||||
|
id, err := newIdentity()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sign, err := newSigner()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.Connect(
|
||||||
|
id,
|
||||||
|
client.WithSign(sign),
|
||||||
|
client.WithHash(hash.SHA256),
|
||||||
|
client.WithClientConnection(grpcConn),
|
||||||
|
// Default timeouts for different gRPC calls
|
||||||
|
client.WithEvaluateTimeout(5*time.Second),
|
||||||
|
client.WithEndorseTimeout(15*time.Second),
|
||||||
|
client.WithSubmitTimeout(5*time.Second),
|
||||||
|
client.WithCommitStatusTimeout(1*time.Minute),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newIdentity creates a client X.509 identity from the certificate file.
|
||||||
|
func newIdentity() (*identity.X509Identity, error) {
|
||||||
|
certPath, err := clientCertPath()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
certPEM, err := os.ReadFile(certPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := identity.CertificateFromPEM(certPEM)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msp, err := mspID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return identity.NewX509Identity(msp, cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSigner creates a signing function from the private key file.
|
||||||
|
func newSigner() (identity.Sign, error) {
|
||||||
|
keyPath, err := privateKeyPath()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
keyPEM, err := os.ReadFile(keyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKey, err := identity.PrivateKeyFromPEM(keyPEM)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return identity.NewPrivateKeySign(privateKey)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hyperledger/fabric-gateway/pkg/client"
|
||||||
|
"github.com/hyperledger/fabric-protos-go-apiv2/peer"
|
||||||
|
)
|
||||||
|
|
||||||
|
const retries = 2
|
||||||
|
|
||||||
|
type Asset struct {
|
||||||
|
ID string `json:"ID"`
|
||||||
|
Color string `json:"Color"`
|
||||||
|
Size int `json:"Size"`
|
||||||
|
Owner string `json:"Owner"`
|
||||||
|
AppraisedValue float64 `json:"AppraisedValue"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type contractCaller interface {
|
||||||
|
Submit(name string, options ...client.ProposalOption) ([]byte, error)
|
||||||
|
Evaluate(name string, options ...client.ProposalOption) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AssetTransfer struct {
|
||||||
|
contract contractCaller
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAssetTransfer(contract *client.Contract) *AssetTransfer {
|
||||||
|
return &AssetTransfer{contract: contract}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AssetTransfer) CreateAsset(asset Asset) error {
|
||||||
|
data, err := json.Marshal(asset)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal asset: %w", err)
|
||||||
|
}
|
||||||
|
_, err = a.contract.Submit("CreateAsset", client.WithArguments(string(data)))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AssetTransfer) GetAllAssets() ([]Asset, error) {
|
||||||
|
result, err := a.contract.Evaluate("GetAllAssets")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(result) == 0 {
|
||||||
|
return []Asset{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var assets []Asset
|
||||||
|
if err := json.Unmarshal(result, &assets); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal assets: %w", err)
|
||||||
|
}
|
||||||
|
return assets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AssetTransfer) ReadAsset(id string) (*Asset, error) {
|
||||||
|
result, err := a.contract.Evaluate("ReadAsset", client.WithArguments(id))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var asset Asset
|
||||||
|
if err := json.Unmarshal(result, &asset); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal asset: %w", err)
|
||||||
|
}
|
||||||
|
return &asset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AssetTransfer) UpdateAsset(asset Asset) error {
|
||||||
|
data, err := json.Marshal(asset)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal asset: %w", err)
|
||||||
|
}
|
||||||
|
return submitWithRetry(func() error {
|
||||||
|
_, err := a.contract.Submit("UpdateAsset", client.WithArguments(string(data)))
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AssetTransfer) DeleteAsset(id string) error {
|
||||||
|
return submitWithRetry(func() error {
|
||||||
|
_, err := a.contract.Submit("DeleteAsset", client.WithArguments(id))
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AssetTransfer) AssetExists(id string) (bool, error) {
|
||||||
|
result, err := a.contract.Evaluate("AssetExists", client.WithArguments(id))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return string(result) == "true", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AssetTransfer) TransferAsset(id, newOwner, newOwnerOrg string) error {
|
||||||
|
fmt.Printf("transferring asset '%s' to %s, %s\n", id, newOwner, newOwnerOrg)
|
||||||
|
_, err := a.contract.Submit("TransferAsset", client.WithArguments(id, newOwner, newOwnerOrg))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func submitWithRetry(submit func() error) error {
|
||||||
|
var lastErr error
|
||||||
|
for i := 0; i < retries; i++ {
|
||||||
|
err := submit()
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
|
||||||
|
var commitErr *client.CommitError
|
||||||
|
if errors.As(err, &commitErr) && commitErr.Code == peer.TxValidationCode_MVCC_READ_CONFLICT {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return lastErr
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* Copyright IBM Corp. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
// ExpectedError represents a known, expected application error that should be
|
||||||
|
// displayed normally rather than treated as an unexpected failure.
|
||||||
|
type ExpectedError struct {
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ExpectedError) Error() string {
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
module trader-go
|
||||||
|
|
||||||
|
go 1.24.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/hyperledger/fabric-gateway v1.10.0
|
||||||
|
github.com/hyperledger/fabric-protos-go-apiv2 v0.3.7
|
||||||
|
google.golang.org/grpc v1.76.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||||
|
golang.org/x/net v0.42.0 // indirect
|
||||||
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
|
golang.org/x/text v0.28.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
|
||||||
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
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/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
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.10.0 h1:x5z/pofdVYIqgMo9QWejubfAZYCSt94WdUPj4Wipdeg=
|
||||||
|
github.com/hyperledger/fabric-gateway v1.10.0/go.mod h1:fSFS1vQkPZq6inNvzsnI/7PCaKSU+UZOZ6uAuau0Yq0=
|
||||||
|
github.com/hyperledger/fabric-protos-go-apiv2 v0.3.7 h1:sQ5qv8vQQfwewa1JlCiSCC8dLElmaU2/frLolpgibEY=
|
||||||
|
github.com/hyperledger/fabric-protos-go-apiv2 v0.3.7/go.mod h1:bJnwzfv03oZQeCc863pdGTDgf5nmCy6Za3RAE7d2XsQ=
|
||||||
|
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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
|
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||||
|
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||||
|
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||||
|
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||||
|
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||||
|
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||||
|
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||||
|
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||||
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||||
|
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||||
|
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||||
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := run(); err != nil {
|
||||||
|
var expectedErr *ExpectedError
|
||||||
|
if errors.As(err, &expectedErr) {
|
||||||
|
fmt.Println(err)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(os.Stderr, "\nUnexpected application error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() error {
|
||||||
|
args := os.Args[1:]
|
||||||
|
if len(args) == 0 {
|
||||||
|
printUsage()
|
||||||
|
return fmt.Errorf("no command specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
commandName := args[0]
|
||||||
|
commandArgs := args[1:]
|
||||||
|
|
||||||
|
command, ok := commands[commandName]
|
||||||
|
if !ok {
|
||||||
|
printUsage()
|
||||||
|
return fmt.Errorf("unknown command: %s", commandName)
|
||||||
|
}
|
||||||
|
|
||||||
|
grpcConn, err := newGrpcConnection()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create gRPC connection: %w", err)
|
||||||
|
}
|
||||||
|
defer grpcConn.Close()
|
||||||
|
|
||||||
|
gw, err := newGatewayConnection(grpcConn)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to gateway: %w", err)
|
||||||
|
}
|
||||||
|
defer gw.Close()
|
||||||
|
|
||||||
|
return command(gw, commandArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printUsage() {
|
||||||
|
names := make([]string, 0, len(commands))
|
||||||
|
for name := range commands {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
|
||||||
|
fmt.Println("Arguments: <command> [<arg1> ...]")
|
||||||
|
fmt.Println("Available commands:")
|
||||||
|
for _, name := range names {
|
||||||
|
fmt.Printf("\t%s\n", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -35,7 +35,7 @@ context CLUSTER_RUNTIME kind # or k3s for Rancher
|
||||||
context CONTAINER_CLI docker # or nerdctl for containerd
|
context CONTAINER_CLI docker # or nerdctl for containerd
|
||||||
context CONTAINER_NAMESPACE "" # or "--namespace k8s.io" for containerd / nerdctl
|
context CONTAINER_NAMESPACE "" # or "--namespace k8s.io" for containerd / nerdctl
|
||||||
|
|
||||||
context FABRIC_CONTAINER_REGISTRY hyperledger
|
context FABRIC_CONTAINER_REGISTRY ghcr.io/hyperledger
|
||||||
context FABRIC_PEER_IMAGE ${FABRIC_CONTAINER_REGISTRY}/fabric-peer:${FABRIC_VERSION}
|
context FABRIC_PEER_IMAGE ${FABRIC_CONTAINER_REGISTRY}/fabric-peer:${FABRIC_VERSION}
|
||||||
context COUCHDB_VERSION 3.4.2
|
context COUCHDB_VERSION 3.4.2
|
||||||
context NETWORK_NAME test-network
|
context NETWORK_NAME test-network
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue