Hyperledger Fabric官方中文教程-改进笔记(十)-运行Fabric应用程序

本Fabric中文文档专栏的阅读前言前言



注意:在学习本章之前,应该熟悉Fabric Gateway服务以及它与应用程序交易流程的关系,详细信息可以在本专栏的Fabric Gateway文章中学习。

本教程介绍了Fabric应用程序与已部署区块链网络的交互方式。本教程使用使用Fabric Gateway客户端API构建的示例程序来调用智能合约, 使用智能合约API查询和更新账本, 详细信息在启动,部署与交互文章之中。

关于资产转移

资产转移(基本)样例展示了如何创建,更新和查询资产。它包括下面两个组件:

  1. 示例应用程序 调用区块链网络,调用智能合约中实现的交易。这个应用位于 fabric-samples目录里:
asset-transfer-basic/application-gateway-(typescript, go, java)
  1. 智能合约: 实现了涉及与账本交互的交易。智能合约位于下面fabric-samples目录里:
asset-transfer-basic/chaincode-(typescript, go, java)

本教程包含两个重要部分:

  1. 搭建链块网络. 我们的应用程序需要和区块链网络交互,所以我们启动基础网络,然后部署一个智能合约给我们的应用程序使用。
  2. 运行实例和智能合约互动. 我们的应用将使用assetTransfer智能合约在账本上创建、查询和更新资产。我们将逐步分析应用程序的代码以及它所调用的交易, 包括创建一些初始资产、查询一个资产、查询一系列资产、创建新资产以及将资产转让给新所有者。

完成本教程后,您应该对Fabric应用程序和智能合约如何协同工作来管理区块链网络的分布式账本数据有基本的理解.


开始之前

在你能运行样例应用前,你需要在你的环境里安装 Fabric Sampple. 根据 Getting Started - Install 的指令安装必要的软件。

本教程中的样例应用在节点上使用了 Fabric Gateway client API。要获取最新支持的编程语言运行时和依赖项的列表, 请参考 文档

确保您已安装适合您选择的运行环境的版本。有关安装说明见:

设置区块链网络

如果你已经学习了 使用Fabric的测试网络 而且已经运行起来了一个网络,本教程将在启动一个新网络之前关闭正在运行的网络。

启动区块链网络

导航到存储库的本地克隆中的子目录。

cd fabric-samples/test-network

如果您已经在运行测试网络,请将其关闭以确保环境干净。

./network.sh down

使用 shell 脚本启动 Fabric 测试网络。network.sh

./network.sh up createChannel -c mychannel -ca

此命令将部署具有两个对等节点、一个排序服务和三个证书颁发机构的 Fabric 测试网络 (排序节点,组织 1、组织 2)。我们通过证书颁发机构启动测试网络(而非使用cryptogen工具),因此需要添加此标志。另外,当证书颁发机构启动时,会自动完成组织管理员用户的注册。

部署智能合约

本教程演示了Asset Transfer智能合约和应用程序的Go版本, 但您可以将Go应用程序示例与任何智能合约语言示例一起使用 (例如Go应用程序调用TypeScript智能合约函数或Go应用程序调用TypeScript智能合约函数等)。

要尝试TypeScript或Java版本的智能合约,请将下面的./network.sh deployCC -ccl go命令中的 ‘’go参数更改为typescriptjava,并且将链码包chaincode-go`修改为对应语言的包,然后按照终端上的说明进行操作。

接下来,让我们通过调用脚本./network.sh来部署包含智能合约的链码包,链码名称和语言选项。

./network.sh deployCC -ccn basic -ccp ../asset-transfer-basic/chaincode-go/ -ccl go

此脚本使用链码生命周期来打包、安装、查询已安装的链码、为Org1和Org2批准链码,最后提交链码。

如果链码包成功部署,终端输出的末尾应该类似于以下内容:

Committed chaincode definition for chaincode 'basic' on channel 'mychannel':
Version: 1.0, Sequence: 1, Endorsement Plugin: escc, Validation Plugin: vscc, Approvals: [Org1MSP: true, Org2MSP: true]
Query chaincode definition successful on peer0.org2 on channel 'mychannel'
Chaincode initialization is not required

准备示例应用程序

现在,让我们准备将用于与已部署的智能合约交互的示例资产转移应用程序

打开一个新终端,然后导航到应用程序目录。此目录包含开发的示例应用程序 使用 Fabric Gateway 客户端 API。

应用程序依赖关系定义在go.mod文件中,其中最关键的是github.com/hyperledger/fabric-gateway模块:

  • 提供Fabric Gateway客户端API

  • 功能包括:

    • 连接Fabric Gateway
    • 使用特定客户端身份
    • 提交/评估交易
    • 接收事件通知

GO
下面我们将检视本教程使用的示例Go应用程序文件。

cd asset-transfer-basic/application-gateway-go

执行LS命令:

ls

你会看到这样的目录:

assetTransfer.go
go.mod
go.sum

其中assetTransfer.go所包含的就是应用程序的源码。


typescript

cd asset-transfer-basic/application-gateway-typescript

运行以下命令以安装依赖项并构建应用程序。可能需要一些时间才能完成:

npm install

这个过程会安装应用程序在package.json中定义的依赖项。其中最重要的是@hyperledger/fabric-gateway包;这提供了使用的 Fabric Gateway 客户端 API 连接 Fabric 网关,并使用特定的客户端标识提交和评估事务,并接收事件。

完成后,运行应用程序的一切都准备就绪。npm install

让我们看一下我们将在本教程中使用的示例 TypeScript 应用程序文件。运行以下命令 命令列出此目录中的文件:

ls

您应该会看到以下内容:

dist
node_modules
package-lock.json
package.json
src
tsconfig.json

src目录包含客户端应用程序源代码。从此源代码生成的 JavaScript 输出安装过程中的代码位于dist目录中,可以忽略。


Java

cd asset-transfer-basic/application-gateway-java

运行以下命令以安装依赖项并构建应用程序。可能需要一些时间才能完成:

./gradlew build

此过程将安装应用程序的依赖项.其中最重要的是 org.hyperledger.fabric:fabric-gateway 包;这提供了使用的 Fabric Gateway 客户端 API 连接 Fabric 网关,并使用特定的客户端标识提交和评估事务,并接收事件。
完成./gradlew build后,运行应用程序的一切都准备就绪。

让我们看一下我们将在本教程中使用的示例 Java 应用程序文件。运行以下命令 命令列出此目录中的文件:

ls

您应该会看到以下内容:

build
build.gradle
gradle
gradlew
gradlew.bat
settings.gradle
src

src/main/java目录包含客户端应用程序源代码。生成的编译后的 Java 类文件 在构建过程中,源代码位于目录build中,可以忽略。


运行示例应用程序

在本教程的前面部分,我们启动了Fabric测试网络,使用证书颁发机构创建了几个身份。其中包括每个组织的用户身份。 应用程序将使用这些用户身份中的一个来与区块链网络交互。

让我们运行该应用程序,然后逐步了解与智能合约函数的每次交互。
asset-transfer-basic/application-gateway-(typescript, go, java)目录,运行以下命令:

语音命令
GOgo run .
typescriptnpm start
java./gradlew run

:会用到的常量定义

const (
	mspID        = "Org1MSP"   //调用者所在组织
	cryptoPath   = "../../test-network/organizations/peerOrganizations/org1.example.com" //本地MSP证书目录
	certPath     = cryptoPath + "/users/User1@org1.example.com/msp/signcerts"  //用户`User1@org1.example.com`的签名证书路径,用于身份验证。
	keyPath      = cryptoPath + "/users/User1@org1.example.com/msp/keystore" //用户`User1@org1.example.com`的私钥存储路径,用于生成交易签名。
	tlsCertPath  = cryptoPath + "/peers/peer0.org1.example.com/tls/ca.crt"  //Peer节点`peer0.org1.example.com`的TLS CA证书路径,用于安全通信。
)

同时,笔者在最后一个章节提供了GO版本的完整代码,并附上了较为详细的注释方便大家参考

首先,建立与Gateway的gRPC连接

客户端应用程序建立了与将用于与区块链网络交互的Fabric Gateway服务的gRPC 连接。 为此,它只需要Fabric Gateway的终端地址,以及如果配置为使用TLS,则需要适当的TLS证书。 在这个示例中,网关终端地址是Peer的地址,它提供了Fabric Gateway服务。

建立gRPC连接涉及到相当大的开销,因此应用程序应该保留这个连接,并用它来进行与Fabric Gateway的所有交互。

在私有数据场景下,应用程序应优先连接到与客户身份所属组织相同、且启用了 Gateway 服务的 Peer 节点(同组织的 Peer 能访问到该组织的私有数据集合)。如果本组织没有任何启用 Gateway 的节点,则可以选择一个受信任的其他组织的 Gateway 节点来提交交易。

应用程序使用签名证书颁发机构的 TLS 证书创建 gRPC 连接,以便 可以验证网关的 TLS 证书的真实性。

为了成功建立TLS连接,客户端使用的终端地址必须与网关的TLS证书中的地址匹配。由于客户端访问网关的Docker容器时使用的是’localhost’地址, 因此需要指定一个gRPC选项,强制将此终端地址解释为网关的配置主机名。

typescript

const peerEndpoint = 'localhost:7051';

async function newGrpcConnection(): Promise<grpc.Client> {
    const tlsRootCert = await fs.readFile(tlsCertPath);
    const tlsCredentials = grpc.credentials.createSsl(tlsRootCert);
    return new grpc.Client(peerEndpoint, tlsCredentials, {
        'grpc.ssl_target_name_override': 'peer0.org1.example.com',
    });
}

GO

const (
    peerEndpoint = "dns:///localhost:7051"
    peerHostOverride = "peer0.org1.example.com"
)

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, peerHostOverride)

    connection, err := grpc.NewClient(peerEndpoint, grpc.WithTransportCredentials(transportCredentials))
    if err != nil {
        panic(fmt.Errorf("failed to create gRPC connection: %w", err))
    }

    return connection
}

Java

private static final String PEER_ENDPOINT = "localhost:7051";
private static final String PEER_HOST_OVERRIDE = "peer0.org1.example.com";

private static ManagedChannel newGrpcConnection() throws IOException {
    var credentials = TlsChannelCredentials.newBuilder()
        .trustManager(TLS_CERT_PATH.toFile())
        .build();
    return Grpc.newChannelBuilder(PEER_ENDPOINT, credentials)
        .overrideAuthority(PEER_HOST_OVERRIDE)
        .build();
}

其次,创建Gateway连接

然后,应用程序创建一个Gateway连接,用于访问Fabric Gateway可访问的任何Networks(类似于通道), 以及随后在这些网络上部署的智能’Contracts’。Gateway连接具有三个要求:

  1. 与Fabric Gateway的gRPC连接
  2. 用于与网络交互的客户身份
  3. 用于为客户身份生成数字签名的签名实现

示例应用程序使用Org1用户的X.509证书作为客户身份,以及基于该用户的私钥的签名实现。

typescript

const client = await newGrpcConnection();

const gateway = connect({
    client,
    identity: await newIdentity(),
    signer: await newSigner(),
    hash: hash.sha256,
});

async function newIdentity(): Promise<Identity> {
    const credentials = await fs.promises.readFile(certPath);
    return { mspId: 'Org1MSP', credentials };
}

async function newSigner(): Promise<Signer> {
    const privateKeyPem = await fs.promises.readFile(keyPath);
    const privateKey = crypto.createPrivateKey(privateKeyPem);
    return signers.newPrivateKeySigner(privateKey);
}

GO

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

gw, err := client.Connect(
    newIdentity(),
    client.WithSign(newSign()),
    client.WithHash(hash.SHA256),
    client.WithClientConnection(clientConnection),
)

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("Org1MSP", certificate)
    if err != nil {
        panic(err)
    }

    return id
}

func newSign() identity.Sign {
    privateKeyPEM, err := readFirstFile(keyPath)
    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
}

JAVA

var channel = newGrpcConnection();
var gateway = Gateway.newInstance()
    .identity(newIdentity())
    .signer(newSigner())
    .hash(Hash.SHA256)
    .connection(channel)
    .connect();

private static Identity newIdentity() throws IOException, CertificateException {
    try (var certReader = Files.newBufferedReader(CERT_PATH)) {
        var certificate = Identities.readX509Certificate(certReader);
        return new X509Identity("Org1MSP", certificate);
    }
}

private static Signer newSigner() throws IOException, InvalidKeyException {
    try (var keyReader = Files.newBufferedReader(KEY_PATH)) {
        var privateKey = Identities.readPrivateKey(keyReader);
        return Signers.newPrivateKeySigner(privateKey);
    }
}

第三,访问要调用的智能合约

示例应用程序使用Gateway连接获取对Network(:通道)的引用,然后获取该网络上部署的默认Contract

typescript

const network = gateway.getNetwork(channelName);
const contract = network.getContract(chaincodeName);

GO

network := gw.GetNetwork(channelName)
contract := network.GetContract(chaincodeName)

JAVA

var network = gateway.getNetwork(CHANNEL_NAME);
var contract = network.getContract(CHAINCODE_NAME);

当链码包包含多个智能合约时,您可以同时提供链码的名称和名称 作为调用getContract()的参数。例如:

语言代码
typescriptconst contract = network.getContract(chaincodeName, smartContractName);
GOcontract := network.GetContractWithName(chaincodeName, smartContractName)
JAVAvar contract = network.getContract(CHAINCODE_NAME, SMART_CONTRACT_NAME);

:链码交互 5 种范式

示例函数使用方法场景
EvaluateTransaction只读查询GetAllAssets, ReadAsset
SubmitTransaction同步写入;阻塞至 CommitInitLedger, CreateAsset
SubmitAsync + commit.Status()异步写入;先返回 TxID,再手动等 CommitTransferAssetAsync(UI 可先响应用户)
错误捕获示范捕获 EndorseError / SubmitError / CommitErrorexampleErrorHandling(演示错误处理)
JSON 结果格式化formatJSON()让控制台输出更易读

第四,使用示例资产填充账本

在初始部署链码包之后,账本是空的。应用程序使用submitTransaction()来调用InitLedger事务函数, 该函数将账本填充了一些样本资产。submitTransaction()将使用Fabric Gateway来执行以下操作:
typescript
await contract.submitTransaction('InitLedger');
GO

_, err := contract.SubmitTransaction("InitLedger")
if err != nil { 
panic(fmt.Errorf("failed to submit transaction: %w", err))
}

Java
contract.submitTransaction("InitLedger");

第五,调用事务函数以读取和写入资产

现在应用程序已准备好执行业务逻辑,通过调用智能合约上的事务函数来查询、创建额外资产以及修改账本上的资产。

查询所有资产

应用程序使用evaluateTransaction()通过执行只读事务调用来查询账本。evaluateTransaction() 将使用Fabric Gateway来调用事务函数并返回其结果。该事务不会被发送到订购服务,也不会导致账本更新。

以下是示例应用程序获取在之前的步骤中我们填充账本时创建的所有资产。

示例应用程序的GetAllAssets调用如下:
typescript

const resultBytes = await contract.evaluateTransaction('GetAllAssets');

const resultJson = utf8Decoder.decode(resultBytes);
const result = JSON.parse(resultJson);
console.log('*** Result:', result);

GO

evaluateResult, err := contract.EvaluateTransaction("GetAllAssets")
if err != nil {
    panic(fmt.Errorf("failed to evaluate transaction: %w", err))
}

result := formatJSON(evaluateResult)
fmt.Printf("*** Result:%s\n", result)

Java

var result = contract.evaluateTransaction("GetAllAssets");

System.out.println("*** Result: " + prettyJson(result));

事务函数的结果始终以字节返回,因为事务函数可以返回任何类型的数据。通常,事务函数返回字符串;或者在上面的情况下,返回JSON数据的UTF-8编码字符串。 应用程序有责任正确解析结果字节。

终端输出应该看起来如此:

Result: [
  {
    AppraisedValue: 300,
    Color: 'blue',
    ID: 'asset1',
    Owner: 'Tomoko',
    Size: 5,
    docType: 'asset'
  },
  {
    AppraisedValue: 400,
    Color: 'red',
    ID: 'asset2',
    Owner: 'Brad',
    Size: 5,
    docType: 'asset'
  },
  {
    AppraisedValue: 500,
    Color: 'green',
    ID: 'asset3',
    Owner: 'Jin Soo',
    Size: 10,
    docType: 'asset'
  },
  {
    AppraisedValue: 600,
    Color: 'yellow',
    ID: 'asset4',
    Owner: 'Max',
    Size: 10,
    docType: 'asset'
  },
  {
    AppraisedValue: 700,
    Color: 'black',
    ID: 'asset5',
    Owner: 'Adriana',
    Size: 15,
    docType: 'asset'
  },
  {
    AppraisedValue: 800,
    Color: 'white',
    ID: 'asset6',
    Owner: 'Michel',
    Size: 15,
    docType: 'asset'
  }
]

创建新资产

示例应用程序提交一个事务来创建新资产

示例应用程序的CreateAsset调用如下:

typescript

const assetId = `asset${String(Date.now())}`;

await contract.submitTransaction(
    'CreateAsset',
    assetId,
    'yellow',
    '5',
    'Tom',
    '1300',
);

GO

var assetId = fmt.Sprintf("asset%d", now.Unix()*1e3+int64(now.Nanosecond())/1e6)

_, err := contract.SubmitTransaction("CreateAsset", assetId, "yellow", "5", "Tom", "1300")
if err != nil {
    panic(fmt.Errorf("failed to submit transaction: %w", err))
}

Java

private final String assetId = "asset" + Instant.now().toEpochMilli();

contract.submitTransaction("CreateAsset", assetId, "yellow", "5", "Tom", "1300");

在上面的应用程序片段中,重要的是要注意,[](https://hyperledger-fabric.readthedocs.io/zh-cn/latest/write_first_app.html#id12)CreateAsset事务使用与链码期望的相同类型和数量的参数以及正确的顺序进行提交。
在这种情况下,正确排序的参数如下:assetId, "yellow", "5", "Tom", "1300"。相应的智能合约的CreateAsset事务函数期望以下顺序的参数来定义资产对象:ID, Color, Size, Owner, AppraisedValue

更新资产

示例应用程序提交一个事务来转移新创建资产的所有权。这次,使用submitAsync()调用事务, 该调用在成功提交已背书的事务给订购服务后返回,而不是等待事务提交到账本。这允许应用程序在等待事务提交时使用事务结果执行工作。

示例应用程序的TransferAsset调用如下:
typescript

const commit = await contract.submitAsync('TransferAsset', {
   arguments: [assetId, 'Saptha'],
});
const oldOwner = utf8Decoder.decode(commit.getResult());

console.log(`*** Successfully submitted transaction to transfer ownership from ${oldOwner} to Saptha`);
console.log('*** Waiting for transaction commit');

const status = await commit.getStatus();
if (!status.successful) {
    throw new Error(`Transaction ${status.transactionId} failed to commit with status code ${String(status.code)}`);
}

console.log('*** Transaction committed successfully');

GO

submitResult, commit, err := contract.SubmitAsync("TransferAsset", client.WithArguments(assetId, "Mark"))
if err != nil {
    panic(fmt.Errorf("failed to submit transaction asynchronously: %w", err))
}

fmt.Printf("\n*** Successfully submitted transaction to transfer ownership from %s to Mark. \n", string(submitResult))
fmt.Println("*** Waiting for transaction commit.")

if commitStatus, err := commit.Status(); err != nil {
    panic(fmt.Errorf("failed to get commit status: %w", err))
} else if !commitStatus.Successful {
    panic(fmt.Errorf("transaction %s failed to commit with status: %d", commitStatus.TransactionID, int32(commitStatus.Code)))
}

fmt.Printf("*** Transaction committed successfully\n")

Java

var commit = contract.newProposal("TransferAsset")
    .addArguments(assetId, "Saptha")
    .build()
    .endorse()
    .submitAsync();

var result = commit.getResult();
var oldOwner = new String(result, StandardCharsets.UTF_8);

System.out.println("*** Successfully submitted transaction to transfer ownership from " + oldOwner + " to Saptha");
System.out.println("*** Waiting for transaction commit");

var status = commit.getStatus();
if (!status.isSuccessful()) {
    throw new RuntimeException("Transaction " + status.getTransactionId() +
        " failed to commit with status code " + status.getCode());
}

System.out.println("*** Transaction committed successfully");

终端输出:

*** Successfully submitted transaction to transfer ownership from Tom to Saptha
*** Waiting for transaction commit
*** Transaction committed successfully

查询更新后的资产

示例应用程序然后评估了已转移资产的查询,显示它是如何根据描述创建的,然后随后转移到新所有者。

示例应用程序的ReadAsset调用如下:
typescript

const resultBytes = await contract.evaluateTransaction('ReadAsset', assetId);

const resultJson = utf8Decoder.decode(resultBytes);
const result = JSON.parse(resultJson);
console.log('*** Result:', result);

GO

evaluateResult, err := contract.EvaluateTransaction("ReadAsset", assetId)
if err != nil {
    panic(fmt.Errorf("failed to evaluate transaction: %w", err))
}

result := formatJSON(evaluateResult)
fmt.Printf("*** Result:%s\n", result)

Java

var evaluateResult = contract.evaluateTransaction("ReadAsset", assetId);

System.out.println("*** Result:" + prettyJson(evaluateResult));

终端输出:

 Result: {
    AppraisedValue: 1300,
    Color: 'yellow',
    ID: 'asset1639084597466',
    Owner: 'Saptha',
    Size: 5
}

处理事务错误

序列的最后部分演示了提交事务时发生错误。在这个示例中,应用程序尝试提交一个UpdateAsset事务, 但指定了一个不存在的资产ID。事务函数返回错误响应,submitTransaction()调用失败。

submitTransaction()的失败可能会生成多种不同类型的错误,指示错误发生在提交流程的哪个点, 并包含附加信息以使应用程序能够适当地响应。请参考API文档 以获取可能生成的不同错误类型的详细信息。

示例应用程序中失败的UpdateAsset调用如下:
typescript

try {
    await contract.submitTransaction(
        'UpdateAsset',
        'asset70',
        'blue',
        '5',
        'Tomoko',
        '300',
    );
    console.log('******** FAILED to return an error');
} catch (error) {
    console.log('*** Successfully caught the error: \n', error);
}

GO

_, err := contract.SubmitTransaction("UpdateAsset", "asset70", "blue", "5", "Tomoko", "300")
if err == nil {
    panic("******** FAILED to return an error")
}

fmt.Println("*** Successfully caught the error:")

var endorseErr *client.EndorseError
var submitErr *client.SubmitError
var commitStatusErr *client.CommitStatusError
var commitErr *client.CommitError

if errors.As(err, &endorseErr) {
    fmt.Printf("Endorse error for transaction %s with gRPC status %v: %s\n", endorseErr.TransactionID, status.Code(endorseErr), endorseErr)
} else if errors.As(err, &submitErr) {
    fmt.Printf("Submit error for transaction %s with gRPC status %v: %s\n", submitErr.TransactionID, status.Code(submitErr), submitErr)
} else if errors.As(err, &commitStatusErr) {
    if errors.Is(err, context.DeadlineExceeded) {
        fmt.Printf("Timeout waiting for transaction %s commit status: %s", commitStatusErr.TransactionID, commitStatusErr)
    } else {
        fmt.Printf("Error obtaining commit status for transaction %s with gRPC status %v: %s\n", commitStatusErr.TransactionID, status.Code(commitStatusErr), commitStatusErr)
    }
} else if errors.As(err, &commitErr) {
    fmt.Printf("Transaction %s failed to commit with status %d: %s\n", commitErr.TransactionID, int32(commitErr.Code), err)
} else {
    panic(fmt.Errorf("unexpected error type %T: %w", err, err))
}

statusErr := status.Convert(err)

details := statusErr.Details()
if len(details) > 0 {
    fmt.Println("Error Details:")

    for _, detail := range details {
        switch detail := detail.(type) {
        case *gateway.ErrorDetail:
            fmt.Printf("- address: %s; mspId: %s; message: %s\n", detail.Address, detail.MspId, detail.Message)
        }
    }
}

Java

try {
    contract.submitTransaction("UpdateAsset", "asset70", "blue", "5", "Tomoko", "300");
    System.out.println("******** FAILED to return an error");
} catch (EndorseException | SubmitException | CommitStatusException e) {
    System.out.println("*** Successfully caught the error:");
    e.printStackTrace(System.out);
    System.out.println("Transaction ID: " + e.getTransactionId());
} catch (CommitException e) {
    System.out.println("*** Successfully caught the error:");
    e.printStackTrace(System.out);
    System.out.println("Transaction ID: " + e.getTransactionId());
    System.out.println("Status code: " + e.getCode());
}

终端输出(为清楚起见删除了堆栈跟踪):
typescript

*** Successfully caught the error:
EndorseError: 10 ABORTED: failed to endorse transaction, see attached details for more info
    at ... {
  code: 10,
  details: [
    {
      address: 'peer0.org1.example.com:7051',
      message: 'chaincode response 500, the asset asset70 does not exist',
      mspId: 'Org1MSP'
    }
  ],
  cause: Error: 10 ABORTED: failed to endorse transaction, see attached details for more info
      at ... {
    code: 10,
    details: 'failed to endorse transaction, see attached details for more info',
    metadata: Metadata { internalRepr: [Map], options: {} }
  },
  transactionId: 'a92980d41eef1d6492d63acd5fbb6ef1db0f53252330ad28e548fedfdb9167fe'
}

GO

*** Successfully caught the error:
Endorse error for transaction 0a0bf1af9c53e0621d6dc98217fb882e0c6d5e174dc1a45f5cb4e07580528347 with gRPC status Aborted: rpc error: code = Aborted desc = failed to endorse transaction, see attached details for more info
Error Details:
- address: peer0.org1.example.com:7051; mspId: Org1MSP; message: chaincode response 500, the asset asset70 does not exist

Java

*** Successfully caught the error:
org.hyperledger.fabric.client.EndorseException: io.grpc.StatusRuntimeException: ABORTED: failed to endorse transaction, see attached details for more info
        at ...
Caused by: io.grpc.StatusRuntimeException: ABORTED: failed to endorse transaction, see attached details for more info
        at ...
Error details:
    address: peer0.org1.example.com:7051; mspId: Org1MSP; message: chaincode response 500, the asset asset70 does not exist
Transaction ID: 5dcc3576cbb851bfbd998f2413da7707761ad15911b7c7fba853e72ac1b3b002

EndorseError类型表示在认可过程中发生了故障,而ABORTED 表示应用程序成功调用了Fabric Gateway, 但在认可过程中发生了故障。UNAVAILABLEDEADLINE_EXCEEDED gRPC状态代码可能表明Fabric Gateway不可访问或未及时收到响应,因此可能需要重试该操作。


清理

当您完成使用资产转移示例后,可以使用network.sh 脚本关闭测试网络:

 ./network.sh down

该命令将关闭我们创建的区块链网络的证书颁发机构、对等节点和排序节点。 请注意,将丢失帐本上的所有数据。如果您想再次运行教程,您将从干净的初始状态开始。


总结

您已经学会了如何通过启动测试网络和部署智能合约来建立区块链网络。然后, 您运行了一个客户端应用程序,并检查了应用程序代码,以了解它如何使用Fabric Gateway 客户端API连接到Fabric Gateway并调用已部署的智能合约的事务功能来查询和更新帐本。 这个教程为您提供了与Hyperledger Fabric一起工作的亲身体验。


:application‑gateway‑go逐步解读

  • 适用版本:Fabric v2.4+(Gateway SDK)
  • 示例路径:fabric-samples/asset-transfer-basic/application-gateway-go

整体执行流程一览

Gateway (连接) → Network (通道) → Contract (链码) → Transaction (操作) → Commit (确认)

阶段主要函数作用
环境准备newGppConnection()与 Peer 建立安全 gRPC 连接;加载调用者身份与私钥;构造数字签名器
newIdentity()
newSign()
创建 Gateway 对象client.Connect(…)在客户端生成 Gateway 连接,封装后续所有链码调用
选择通道与链码gw.GetNetwork(channel)得到 Network 对象(通道)和 Contract 对象(链码)
业务调用initledger() 等 5 个演示函数展示不同交互模式:只读、同步写、异步写、错误捕获
退出清理defer 关闭连接/对象释放 gRPC 连接、Gateway 等资源

application‑gateway‑go示例代码

/*
Copyright 2021 IBM All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package main

import (
	"bytes"
	"context"
	"crypto/x509"
	"encoding/json"
	"errors"
	"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"
	"github.com/hyperledger/fabric-protos-go-apiv2/gateway"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/status"
)

const (
	mspID        = "Org1MSP"   //调用者所在组织
	cryptoPath   = "../../test-network/organizations/peerOrganizations/org1.example.com" //本地MSP证书目录
	certPath     = cryptoPath + "/users/User1@org1.example.com/msp/signcerts"  //用户`User1@org1.example.com`的签名证书路径,用于身份验证。
	keyPath      = cryptoPath + "/users/User1@org1.example.com/msp/keystore" //用户`User1@org1.example.com`的私钥存储路径,用于生成交易签名。
	tlsCertPath  = cryptoPath + "/peers/peer0.org1.example.com/tls/ca.crt"  //Peer节点`peer0.org1.example.com`的TLS CA证书路径,用于安全通信。
	peerEndpoint = "dns:///localhost:7051"  //Peer Gateway 服务端口
	gatewayPeer  = "peer0.org1.example.com"  //目标Peer节点的域名标识,用于Gateway连接时指定具体的Peer节点
)

var now = time.Now()
//动态生成待创建的资产 ID,避免重复。
var assetId = fmt.Sprintf("asset%d", now.Unix()*1e3+int64(now.Nanosecond())/1e6)

func main() {
	// The gRPC client connection should be shared by all Gateway connections to this endpoint
	// 创建 gRPC 连接
	clientConnection := newGrpcConnection()
	defer clientConnection.Close()
	//装载身份
	id := newIdentity()
	//装载签名器
	sign := newSign()

	// 为特定客户端身份创建GateWay连接
	gw, err := client.Connect(
		id,
		client.WithSign(sign),
		client.WithHash(hash.SHA256),
		client.WithClientConnection(clientConnection),
		// 针对不同FRPC连接的默认超时时间
		client.WithEvaluateTimeout(5*time.Second),
		client.WithEndorseTimeout(15*time.Second),
		client.WithSubmitTimeout(5*time.Second),
		client.WithCommitStatusTimeout(1*time.Minute),
	)
	if err != nil {
		panic(err)
	}
	defer gw.Close()

	// 覆盖链码和通道名称的默认值,因为它们在测试上下文中可能有所不同。
	chaincodeName := "basic"
	if ccname := os.Getenv("CHAINCODE_NAME"); ccname != "" {
		chaincodeName = ccname
	}

	channelName := "mychannel"
	if cname := os.Getenv("CHANNEL_NAME"); cname != "" {
		channelName = cname
	}
	//获取 Network / Contract
	network := gw.GetNetwork(channelName)
	contract := network.GetContract(chaincodeName)
	//下面为按顺序调用示例函数
	initLedger(contract)  //初始化账本 
	getAllAssets(contract) //查询所有资产
	createAsset(contract) //同步创建资产
	readAssetByID(contract) //查询刚创建的资产
	transferAssetAsync(contract) //异步转移资产
	exampleErrorHandling(contract) //演示链码业务错误捕获
}

//创建一个GRPC连接到Gateway服务
func newGrpcConnection() *grpc.ClientConn {
	//读取 tls/ca.crt → 构造根证书池 
	certificatePEM, err := os.ReadFile(tlsCertPath)
	if err != nil {
		panic(fmt.Errorf("failed to read TLS certifcate file: %w", err))
	}

	certificate, err := identity.CertificateFromPEM(certificatePEM)
	if err != nil {
		panic(err)
	}

	certPool := x509.NewCertPool()
	certPool.AddCert(certificate)
	transportCredentials := credentials.NewClientTLSFromCert(certPool, gatewayPeer)
	//建立双向TLS通道  
	connection, err := grpc.NewClient(peerEndpoint, grpc.WithTransportCredentials(transportCredentials))
	if err != nil {
		panic(fmt.Errorf("failed to create gRPC connection: %w", err))
	}

	return connection
}

// newIdentity 使用 X.509 证书为此网关连接创建客户端身份。读证书 (msp/signcerts),生成 X509Identity(代表谁在调用网络)
func newIdentity() *identity.X509Identity {
	certificatePEM, err := readFirstFile(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.NewX510Identity(mspID, certificate)
	if err != nil {
		panic(err)
	}

	return id
}

// newSign 创建一个函数,该函数使用私钥从消息摘要生成数字签名。
// 读私钥 (msp/keystore),构造签名函数,(用来对所有 Proposal/Commit请求签名,确保不可抵赖)  
func newSign() identity.Sign {
	privateKeyPEM, err := readFirstFile(keyPath)
	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]))
}

// 这种类型的事务通常只会在应用程序在其之后第一次启动时运行一次  
// 初始部署。稍后部署的新版本链码可能不需要运行“init”函数。
func initLedger(contract *client.Contract) {
	fmt.Printf("\n--> Submit Transaction: InitLedger, function creates the initial set of assets on the ledger \n")

	_, err := contract.SubmitTransaction("InitLedger")
	if err != nil {
		panic(fmt.Errorf("failed to submit transaction: %w", err))
	}

	fmt.Printf("*** Transaction committed successfully\n")
}

// 查询账本状态。
func getAllAssets(contract *client.Contract) {
	fmt.Println("\n--> Evaluate Transaction: GetAllAssets, function returns all the current assets on the ledger")

	evaluateResult, err := contract.EvaluateTransaction("GetAllAssets")
	if err != nil {
		panic(fmt.Errorf("failed to evaluate transaction: %w", err))
	}
	result := formatJSON(evaluateResult)

	fmt.Printf("*** Result:%s\n", result)
}

// 同步提交交易,阻塞直到它提交到账本。
func createAsset(contract *client.Contract) {
	fmt.Printf("\n--> Submit Transaction: CreateAsset, creates new asset with ID, Color, Size, Owner and AppraisedValue arguments \n")

	_, err := contract.SubmitTransaction("CreateAsset", assetId, "yellow", "5", "Tom", "1300")
	if err != nil {
		panic(fmt.Errorf("failed to submit transaction: %w", err))
	}

	fmt.Printf("*** Transaction committed successfully\n")
}

// 按 assetID 查询账本交易。
func readAssetByID(contract *client.Contract) {
	fmt.Printf("\n--> Evaluate Transaction: ReadAsset, function returns asset attributes\n")

	evaluateResult, err := contract.EvaluateTransaction("ReadAsset", assetId)
	if err != nil {
		panic(fmt.Errorf("failed to evaluate transaction: %w", err))
	}
	result := formatJSON(evaluateResult)

	fmt.Printf("*** Result:%s\n", result)
}

//异步提交交易,阻塞直到交易发送给排序器,并允许此线程来处理链码响应(例如更新 UI),而无需等待提交通知
func transferAssetAsync(contract *client.Contract) {
	fmt.Printf("\n--> Async Submit Transaction: TransferAsset, updates existing asset owner")

	submitResult, commit, err := contract.SubmitAsync("TransferAsset", client.WithArguments(assetId, "Mark"))
	if err != nil {
		panic(fmt.Errorf("failed to submit transaction asynchronously: %w", err))
	}

	fmt.Printf("\n*** Successfully submitted transaction to transfer ownership from %s to Mark. \n", string(submitResult))
	fmt.Println("*** Waiting for transaction commit.")

	if commitStatus, err := commit.Status(); err != nil {
		panic(fmt.Errorf("failed to get commit status: %w", err))
	} else if !commitStatus.Successful {
		panic(fmt.Errorf("transaction %s failed to commit with status: %d", commitStatus.TransactionID, int32(commitStatus.Code)))
	}

	fmt.Printf("*** Transaction committed successfully\n")
}

// 提交交易,传入错误数量的参数,预计会抛出一个错误,其中包含来自智能合约的任何错误响应的详细信息。
func exampleErrorHandling(contract *client.Contract) {
	fmt.Println("\n--> Submit Transaction: UpdateAsset asset70, asset70 does not exist and should return an error")

	_, err := contract.SubmitTransaction("UpdateAsset", "asset70", "blue", "5", "Tomoko", "300")
	if err == nil {
		panic("******** FAILED to return an error")
	}

	fmt.Println("*** Successfully caught the error:")

	var endorseErr *client.EndorseError
	var submitErr *client.SubmitError
	var commitStatusErr *client.CommitStatusError
	var commitErr *client.CommitError

	if errors.As(err, &endorseErr) {
		fmt.Printf("Endorse error for transaction %s with gRPC status %v: %s\n", endorseErr.TransactionID, status.Code(endorseErr), endorseErr)
	} else if errors.As(err, &submitErr) {
		fmt.Printf("Submit error for transaction %s with gRPC status %v: %s\n", submitErr.TransactionID, status.Code(submitErr), submitErr)
	} else if errors.As(err, &commitStatusErr) {
		if errors.Is(err, context.DeadlineExceeded) {
			fmt.Printf("Timeout waiting for transaction %s commit status: %s", commitStatusErr.TransactionID, commitStatusErr)
		} else {
			fmt.Printf("Error obtaining commit status for transaction %s with gRPC status %v: %s\n", commitStatusErr.TransactionID, status.Code(commitStatusErr), commitStatusErr)
		}
	} else if errors.As(err, &commitErr) {
		fmt.Printf("Transaction %s failed to commit with status %d: %s\n", commitErr.TransactionID, int32(commitErr.Code), err)
	} else {
		panic(fmt.Errorf("unexpected error type %T: %w", err, err))
	}

	//源自网关外部的对等节点或排序器节点的任何错误都将具有其详细信息嵌入在 gRPC 状态错误中。以下代码演示了如何提取它。
	statusErr := status.Convert(err)
	details := statusErr.Details()
	if len(details) > 0 {
		fmt.Println("Error Details:")

		for _, detail := range details {
			switch detail := detail.(type) {
			case *gateway.ErrorDetail:
				fmt.Printf("- address: %s; mspId: %s; message: %s\n", detail.Address, detail.MspId, detail.Message)
			}
		}
	}
}

// Format JSON data
func formatJSON(data []byte) string {
	var prettyJSON bytes.Buffer
	if err := json.Indent(&prettyJSON, data, "", "  "); err != nil {
		panic(fmt.Errorf("failed to parse JSON: %w", err))
	}
	return prettyJSON.String()
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值