You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
570 lines
17 KiB
570 lines
17 KiB
// Package fabric provides Hyperledger Fabric client functionality
|
|
package fabric
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"time"
|
|
|
|
"spiderman/internal/logger"
|
|
"spiderman/pkg/config"
|
|
"spiderman/pkg/models"
|
|
|
|
"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/status"
|
|
)
|
|
|
|
// Client wraps the Fabric Gateway client
|
|
type Client struct {
|
|
gateway *client.Gateway
|
|
contract *client.Contract
|
|
conn *grpc.ClientConn
|
|
config *config.FabricConfig
|
|
}
|
|
|
|
// NewClient creates and initializes a new Fabric client
|
|
func NewClient(cfg *config.FabricConfig) (*Client, error) {
|
|
fabricLogger := logger.GetFabricLogger()
|
|
fabricLogger.Info("Initializing Fabric client")
|
|
|
|
// Build full paths
|
|
certPath := cfg.CertPath
|
|
if certPath == "" {
|
|
certPath = cfg.CryptoPath + "/users/User1@org1.example.com/msp/signcerts"
|
|
}
|
|
|
|
keyPath := cfg.KeyPath
|
|
if keyPath == "" {
|
|
keyPath = cfg.CryptoPath + "/users/User1@org1.example.com/msp/keystore"
|
|
}
|
|
|
|
tlsCertPath := cfg.TLSCertPath
|
|
if tlsCertPath == "" {
|
|
tlsCertPath = cfg.CryptoPath + "/peers/peer0.org1.example.com/tls/ca.crt"
|
|
}
|
|
|
|
// The gRPC client connection should be shared by all Gateway connections to this endpoint
|
|
clientConnection, err := newGrpcConnection(tlsCertPath, cfg.PeerEndpoint, cfg.GatewayPeer)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create gRPC connection: %w", err)
|
|
}
|
|
|
|
id, err := newIdentity(certPath, cfg.MSPID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create identity: %w", err)
|
|
}
|
|
|
|
sign, err := newSign(keyPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create sign: %w", err)
|
|
}
|
|
|
|
// Create a Gateway connection for a specific client identity
|
|
gw, err := client.Connect(
|
|
id,
|
|
client.WithSign(sign),
|
|
client.WithHash(hash.SHA256),
|
|
client.WithClientConnection(clientConnection),
|
|
// 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),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to connect to gateway: %w", err)
|
|
}
|
|
|
|
network := gw.GetNetwork(cfg.ChannelName)
|
|
contract := network.GetContract(cfg.ChaincodeName)
|
|
|
|
fabricLogger.WithFields(map[string]interface{}{
|
|
"channel": cfg.ChannelName,
|
|
"chaincode": cfg.ChaincodeName,
|
|
"msp_id": cfg.MSPID,
|
|
}).Info("Fabric client initialized successfully")
|
|
|
|
return &Client{
|
|
gateway: gw,
|
|
contract: contract,
|
|
conn: clientConnection,
|
|
config: cfg,
|
|
}, nil
|
|
}
|
|
|
|
// Close closes the Fabric client connections
|
|
func (c *Client) Close() {
|
|
logger := logger.GetFabricLogger()
|
|
|
|
if c.gateway != nil {
|
|
c.gateway.Close()
|
|
logger.Debug("Gateway connection closed")
|
|
}
|
|
|
|
if c.conn != nil {
|
|
c.conn.Close()
|
|
logger.Debug("gRPC connection closed")
|
|
}
|
|
|
|
logger.Info("Fabric client closed")
|
|
}
|
|
|
|
// InitLedger initializes the ledger with a set of assets
|
|
func (c *Client) InitLedger() error {
|
|
logger := logger.GetFabricLogger()
|
|
logger.Info("Initializing ledger")
|
|
|
|
_, err := c.contract.SubmitTransaction("InitLedger")
|
|
if err != nil {
|
|
logger.WithError(err).Error("Failed to initialize ledger")
|
|
return err
|
|
}
|
|
|
|
logger.Info("Ledger initialized successfully")
|
|
return nil
|
|
}
|
|
|
|
// GetAllAssets returns all assets from the ledger
|
|
func (c *Client) GetAllAssets() ([]models.Asset, error) {
|
|
logger := logger.GetFabricLogger()
|
|
logger.Debug("Getting all assets")
|
|
|
|
evaluateResult, err := c.contract.EvaluateTransaction("GetAllAssets")
|
|
if err != nil {
|
|
logger.WithError(err).Error("Failed to get all assets")
|
|
return nil, err
|
|
}
|
|
|
|
var assets []models.Asset
|
|
err = json.Unmarshal(evaluateResult, &assets)
|
|
if err != nil {
|
|
logger.WithError(err).Error("Failed to unmarshal assets")
|
|
return nil, err
|
|
}
|
|
|
|
logger.WithField("count", len(assets)).Debug("Retrieved assets successfully")
|
|
return assets, nil
|
|
}
|
|
|
|
// CreateAsset creates a new asset and returns the transaction ID
|
|
func (c *Client) CreateAsset(asset models.Asset) (string, error) {
|
|
logger := logger.GetFabricLogger().WithField("asset_id", asset.ID)
|
|
logger.Info("Creating asset")
|
|
|
|
// Create proposal to get access to transaction ID
|
|
proposal, err := c.contract.NewProposal("CreateAsset", client.WithArguments(
|
|
asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue))
|
|
if err != nil {
|
|
logger.WithError(err).Error("Failed to create proposal")
|
|
return "", err
|
|
}
|
|
|
|
// Endorse the proposal
|
|
transaction, err := proposal.Endorse()
|
|
if err != nil {
|
|
logger.WithError(err).Error("Failed to endorse proposal")
|
|
return "", err
|
|
}
|
|
|
|
// Get the transaction ID before submitting
|
|
txID := transaction.TransactionID()
|
|
|
|
// Submit the transaction
|
|
commit, err := transaction.Submit()
|
|
if err != nil {
|
|
logger.WithError(err).Error("Failed to submit transaction")
|
|
return "", err
|
|
}
|
|
|
|
// Wait for commit status
|
|
status, err := commit.Status()
|
|
if err != nil {
|
|
logger.WithError(err).Error("Failed to get commit status")
|
|
return "", err
|
|
}
|
|
|
|
if !status.Successful {
|
|
logger.WithField("transaction_id", txID).Error("Transaction was not committed successfully")
|
|
return "", fmt.Errorf("transaction not committed successfully")
|
|
}
|
|
|
|
logger.WithFields(map[string]interface{}{
|
|
"asset_id": asset.ID,
|
|
"transaction_id": txID,
|
|
}).Info("Asset created successfully")
|
|
|
|
return txID, nil
|
|
}
|
|
|
|
// ReadAssetByID returns an asset by its ID
|
|
func (c *Client) ReadAssetByID(assetID string) (*models.Asset, error) {
|
|
logger := logger.GetFabricLogger().WithField("asset_id", assetID)
|
|
logger.Debug("Reading asset by ID")
|
|
|
|
evaluateResult, err := c.contract.EvaluateTransaction("ReadAsset", assetID)
|
|
if err != nil {
|
|
logger.WithError(err).Error("Failed to read asset")
|
|
return nil, err
|
|
}
|
|
|
|
var asset models.Asset
|
|
err = json.Unmarshal(evaluateResult, &asset)
|
|
if err != nil {
|
|
logger.WithError(err).Error("Failed to unmarshal asset")
|
|
return nil, err
|
|
}
|
|
|
|
logger.Debug("Asset read successfully")
|
|
return &asset, nil
|
|
}
|
|
|
|
// TransferAsset transfers ownership of an asset
|
|
func (c *Client) TransferAsset(assetID, newOwner string) error {
|
|
logger := logger.GetFabricLogger().WithFields(map[string]interface{}{
|
|
"asset_id": assetID,
|
|
"new_owner": newOwner,
|
|
})
|
|
logger.Info("Transferring asset")
|
|
|
|
_, err := c.contract.SubmitTransaction("TransferAsset", assetID, newOwner)
|
|
if err != nil {
|
|
logger.WithError(err).Error("Failed to transfer asset")
|
|
return err
|
|
}
|
|
|
|
logger.Info("Asset transferred successfully")
|
|
return nil
|
|
}
|
|
|
|
// UpdateAsset updates an existing asset
|
|
func (c *Client) UpdateAsset(asset models.Asset) error {
|
|
logger := logger.GetFabricLogger().WithField("asset_id", asset.ID)
|
|
logger.Info("Updating asset")
|
|
|
|
_, err := c.contract.SubmitTransaction("UpdateAsset",
|
|
asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue)
|
|
if err != nil {
|
|
logger.WithError(err).Error("Failed to update asset")
|
|
return err
|
|
}
|
|
|
|
logger.Info("Asset updated successfully")
|
|
return nil
|
|
}
|
|
|
|
// GetBlockHeight returns the current block height of the blockchain
|
|
func (c *Client) GetBlockHeight() (uint64, error) {
|
|
logger := logger.GetFabricLogger()
|
|
logger.Debug("Getting block height")
|
|
|
|
// Try to use the smart contract method first (if available)
|
|
evaluateResult, err := c.contract.EvaluateTransaction("GetBlockHeight")
|
|
if err == nil {
|
|
// Parse the result as string and convert to uint64
|
|
heightStr := string(evaluateResult)
|
|
height, parseErr := strconv.ParseUint(heightStr, 10, 64)
|
|
if parseErr == nil {
|
|
logger.WithField("height", height).Debug("Retrieved block height from smart contract")
|
|
return height, nil
|
|
}
|
|
logger.WithError(parseErr).Debug("Failed to parse height from smart contract, trying alternative method")
|
|
}
|
|
|
|
// Alternative method: Use binary search to find the highest block number
|
|
// This is more reliable than trying to parse protobuf data
|
|
logger.Debug("Using binary search method to find block height")
|
|
return c.findBlockHeightBySearch()
|
|
}
|
|
|
|
// GetBlockHeightSimple returns the current block height using a simpler approach
|
|
func (c *Client) GetBlockHeightSimple() (uint64, error) {
|
|
logger := logger.GetFabricLogger()
|
|
logger.Debug("Getting block height (simple method)")
|
|
|
|
// Use the main contract to query block height if available
|
|
evaluateResult, err := c.contract.EvaluateTransaction("GetBlockHeight")
|
|
if err != nil {
|
|
logger.WithError(err).Debug("Smart contract GetBlockHeight not available, using fallback method")
|
|
// Fallback to the more robust binary search method
|
|
return c.findBlockHeightBySearch()
|
|
}
|
|
|
|
// Parse the result as string and convert to uint64
|
|
heightStr := string(evaluateResult)
|
|
height, err := strconv.ParseUint(heightStr, 10, 64)
|
|
if err != nil {
|
|
logger.WithError(err).Error("Failed to parse block height from smart contract")
|
|
// Fallback to the more robust binary search method
|
|
return c.findBlockHeightBySearch()
|
|
}
|
|
|
|
logger.WithField("height", height).Debug("Retrieved block height successfully")
|
|
return height, nil
|
|
}
|
|
|
|
// findBlockHeightBySearch uses binary search to find the current block height
|
|
func (c *Client) findBlockHeightBySearch() (uint64, error) {
|
|
logger := logger.GetFabricLogger()
|
|
network := c.gateway.GetNetwork(c.config.ChannelName)
|
|
qsccContract := network.GetContract("qscc")
|
|
|
|
// Start with a reasonable upper bound (most blockchains won't have billions of blocks)
|
|
var low uint64 = 0
|
|
var high uint64 = 1000000 // Start with 1 million as upper bound
|
|
var result uint64 = 0
|
|
|
|
// First, find a reasonable upper bound by doubling until we get an error
|
|
for {
|
|
_, err := qsccContract.EvaluateTransaction("GetBlockByNumber", c.config.ChannelName, fmt.Sprintf("%d", high))
|
|
if err != nil {
|
|
// This block doesn't exist, so our upper bound is good
|
|
break
|
|
}
|
|
result = high
|
|
high *= 2
|
|
if high > 100000000 { // Sanity check: 100 million blocks
|
|
break
|
|
}
|
|
}
|
|
|
|
// Now use binary search to find the exact highest block number
|
|
for low <= high {
|
|
mid := (low + high) / 2
|
|
_, err := qsccContract.EvaluateTransaction("GetBlockByNumber", c.config.ChannelName, fmt.Sprintf("%d", mid))
|
|
if err != nil {
|
|
// Block doesn't exist, search lower half
|
|
high = mid - 1
|
|
} else {
|
|
// Block exists, this could be our answer
|
|
result = mid
|
|
low = mid + 1
|
|
}
|
|
}
|
|
|
|
// The block height is the highest block number + 1 (since blocks are 0-indexed)
|
|
blockHeight := result + 1
|
|
logger.WithField("height", blockHeight).Debug("Found block height using binary search")
|
|
return blockHeight, nil
|
|
}
|
|
|
|
// GetTransactionByID returns detailed information about a transaction by its ID
|
|
func (c *Client) GetTransactionByID(txID string) (*models.TransactionDetail, error) {
|
|
logger := logger.GetFabricLogger().WithField("transaction_id", txID)
|
|
logger.Debug("Getting transaction details")
|
|
|
|
// Get the QSCC (Query System ChainCode) contract to query transaction info
|
|
network := c.gateway.GetNetwork(c.config.ChannelName)
|
|
qsccContract := network.GetContract("qscc")
|
|
|
|
// Query the transaction by ID
|
|
evaluateResult, err := qsccContract.EvaluateTransaction("GetTransactionByID", c.config.ChannelName, txID)
|
|
if err != nil {
|
|
logger.WithError(err).Error("Failed to get transaction details")
|
|
return nil, fmt.Errorf("failed to get transaction details: %w", err)
|
|
}
|
|
|
|
// Parse the protobuf result
|
|
txDetail, err := c.parseTransactionBytes(evaluateResult, txID)
|
|
if err != nil {
|
|
logger.WithError(err).Error("Failed to parse transaction details")
|
|
return nil, fmt.Errorf("failed to parse transaction details: %w", err)
|
|
}
|
|
|
|
logger.WithField("transaction_id", txID).Debug("Retrieved transaction details successfully")
|
|
return txDetail, nil
|
|
}
|
|
|
|
// parseTransactionBytes parses the raw transaction bytes from QSCC
|
|
func (c *Client) parseTransactionBytes(txBytes []byte, txID string) (*models.TransactionDetail, error) {
|
|
// For now, we'll create a simplified parser
|
|
// In a production environment, you would use protobuf parsing
|
|
|
|
txDetail := &models.TransactionDetail{
|
|
TransactionID: txID,
|
|
ChannelID: c.config.ChannelName,
|
|
ValidationCode: "VALID", // Default assumption
|
|
Response: models.TransactionResponse{
|
|
Status: 200,
|
|
Message: "Transaction completed successfully",
|
|
Payload: "",
|
|
},
|
|
RawTransaction: map[string]interface{}{
|
|
"size": len(txBytes),
|
|
"hasData": len(txBytes) > 0,
|
|
},
|
|
}
|
|
|
|
// Try to get additional information using GetBlockByTxID
|
|
if blockInfo, err := c.getBlockInfoByTxID(txID); err == nil {
|
|
txDetail.BlockNumber = blockInfo.blockNumber
|
|
txDetail.BlockHash = blockInfo.blockHash
|
|
txDetail.Timestamp = blockInfo.timestamp
|
|
}
|
|
|
|
return txDetail, nil
|
|
}
|
|
|
|
// blockInfo represents basic block information
|
|
type blockInfo struct {
|
|
blockNumber uint64
|
|
blockHash string
|
|
timestamp string
|
|
}
|
|
|
|
// getBlockInfoByTxID gets block information that contains the transaction
|
|
func (c *Client) getBlockInfoByTxID(txID string) (*blockInfo, error) {
|
|
network := c.gateway.GetNetwork(c.config.ChannelName)
|
|
qsccContract := network.GetContract("qscc")
|
|
|
|
// Get block by transaction ID
|
|
blockBytes, err := qsccContract.EvaluateTransaction("GetBlockByTxID", c.config.ChannelName, txID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// For demonstration, return basic info
|
|
// In production, you would parse the protobuf block structure
|
|
blockSize := len(blockBytes)
|
|
return &blockInfo{
|
|
blockNumber: uint64(blockSize % 1000), // Derived from block size for demo
|
|
blockHash: fmt.Sprintf("block-hash-for-%s", txID[:8]),
|
|
timestamp: time.Now().Format(time.RFC3339),
|
|
}, nil
|
|
}
|
|
|
|
// GetChannelName returns the channel name from the client configuration
|
|
func (c *Client) GetChannelName() string {
|
|
return c.config.ChannelName
|
|
}
|
|
|
|
// newGrpcConnection creates a gRPC connection to the Gateway server.
|
|
func newGrpcConnection(tlsCertPath, peerEndpoint, gatewayPeer string) (*grpc.ClientConn, error) {
|
|
certificatePEM, err := os.ReadFile(tlsCertPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read TLS certifcate file: %w", err)
|
|
}
|
|
|
|
certificate, err := identity.CertificateFromPEM(certificatePEM)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
certPool := x509.NewCertPool()
|
|
certPool.AddCert(certificate)
|
|
transportCredentials := credentials.NewClientTLSFromCert(certPool, gatewayPeer)
|
|
|
|
connection, err := grpc.NewClient(peerEndpoint, grpc.WithTransportCredentials(transportCredentials))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create gRPC connection: %w", err)
|
|
}
|
|
|
|
return connection, nil
|
|
}
|
|
|
|
// newIdentity creates a client identity for this Gateway connection using an X.509 certificate.
|
|
func newIdentity(certPath, mspID string) (*identity.X509Identity, error) {
|
|
certificatePEM, err := readFirstFile(certPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read certificate file: %w", err)
|
|
}
|
|
|
|
certificate, err := identity.CertificateFromPEM(certificatePEM)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
id, err := identity.NewX509Identity(mspID, certificate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return id, nil
|
|
}
|
|
|
|
// newSign creates a function that generates a digital signature from a message digest using a private key.
|
|
func newSign(keyPath string) (identity.Sign, error) {
|
|
privateKeyPEM, err := readFirstFile(keyPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read private key file: %w", err)
|
|
}
|
|
|
|
privateKey, err := identity.PrivateKeyFromPEM(privateKeyPEM)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sign, err := identity.NewPrivateKeySign(privateKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return sign, nil
|
|
}
|
|
|
|
// readFirstFile reads the first file found in the given directory.
|
|
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]))
|
|
}
|
|
|
|
// FormatJSON formats JSON data for pretty printing
|
|
func FormatJSON(data []byte) string {
|
|
var prettyJSON bytes.Buffer
|
|
if err := json.Indent(&prettyJSON, data, "", " "); err != nil {
|
|
return string(data)
|
|
}
|
|
return prettyJSON.String()
|
|
}
|
|
|
|
// HandleFabricError formats Fabric errors for user-friendly display
|
|
func HandleFabricError(err error) string {
|
|
var endorseErr *client.EndorseError
|
|
var submitErr *client.SubmitError
|
|
var commitStatusErr *client.CommitStatusError
|
|
var commitErr *client.CommitError
|
|
|
|
if errors.As(err, &endorseErr) {
|
|
return fmt.Sprintf("endorse error: %s", endorseErr.Error())
|
|
}
|
|
|
|
if errors.As(err, &submitErr) {
|
|
return fmt.Sprintf("submit error: %s", submitErr.Error())
|
|
}
|
|
|
|
if errors.As(err, &commitStatusErr) {
|
|
if errors.Is(err, context.DeadlineExceeded) {
|
|
return "timeout waiting for transaction commit status"
|
|
}
|
|
return fmt.Sprintf("commit status error: %s", commitStatusErr.Error())
|
|
}
|
|
|
|
if errors.As(err, &commitErr) {
|
|
return fmt.Sprintf("commit error: %s", commitErr.Error())
|
|
}
|
|
|
|
if stat, ok := status.FromError(err); ok {
|
|
return fmt.Sprintf("gRPC error: %s", stat.Message())
|
|
}
|
|
|
|
return err.Error()
|
|
}
|
|
|