当前位置: 首页 > 工具软件 > dc-sdk > 使用案例 >

fabric-node-sdk 编写第一个应用

魏雅惠
2023-12-01

本文参考官网示例:https://hyperledgercn.github.io/hyperledgerDocs/write_first_app_zh/

测试环境linux

一、准备条件

准备条件与上篇相同。

本文用到的例子是fabric-simples中的fabcar,是一个与汽车属性相关的测试项目,后面的chaincode部分会讲。

二、启动网络

在fabcar目录下有个startFabric.sh脚本,这个脚本是用来启动网络和部署链码等一些准备条件,具体分为以下几步:

1)启动peer节点、Ordering节点、证书颁发机构以及CLI容器

2)创建一个通道,并将peer加入该通道

3)将智能合约(即链码)安装到peer节点的文件系统上,并在通道上实例化该链码;实例化会启动链码容器

4)调用initLedger功能来向通道账本写入10个不同的汽车

分析一下脚本的内容,

#!/bin/bash
#
# Copyright IBM Corp All Rights Reserved
#
# SPDX-License-Identifier: Apache-2.0
#
# Exit on first error
set -e

# don't rewrite paths for Windows Git Bash users
export MSYS_NO_PATHCONV=1
starttime=$(date +%s)
LANGUAGE=${1:-"golang"}
CC_SRC_PATH=github.com/fabcar/go
if [ "$LANGUAGE" = "node" -o "$LANGUAGE" = "NODE" ]; then
	CC_SRC_PATH=/opt/gopath/src/github.com/fabcar/node
fi

# clean the keystore
rm -rf ./hfc-key-store

# launch network; create channel and join peer to channel
cd ../basic-network
./start.sh

# Now launch the CLI container in order to install, instantiate chaincode
# and prime the ledger with our 10 cars
docker-compose -f ./docker-compose.yml up -d cli

docker exec -e "CORE_PEER_LOCALMSPID=Org1MSP" -e "CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp" cli peer chaincode install -n fabcar -v 1.0 -p "$CC_SRC_PATH" -l "$LANGUAGE"
docker exec -e "CORE_PEER_LOCALMSPID=Org1MSP" -e "CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp" cli peer chaincode instantiate -o orderer.example.com:7050 -C mychannel -n fabcar -l "$LANGUAGE" -v 1.0 -c '{"Args":[""]}' -P "OR ('Org1MSP.member','Org2MSP.member')"
sleep 10
docker exec -e "CORE_PEER_LOCALMSPID=Org1MSP" -e "CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp" cli peer chaincode invoke -o orderer.example.com:7050 -C mychannel -n fabcar -c '{"function":"initLedger","Args":[""]}'

printf "\nTotal setup execution time : $(($(date +%s) - starttime)) secs ...\n\n\n"
printf "Start by installing required packages run 'npm install'\n"
printf "Then run 'node enrollAdmin.js', then 'node registerUser'\n\n"
printf "The 'node invoke.js' will fail until it has been updated with valid arguments\n"
printf "The 'node query.js' may be run at anytime once the user has been registered\n\n"

首先启动网络,第23行,调用了另外一个脚本start.sh,在basic-network文件夹中,内容如下:

#!/bin/bash
#
# Copyright IBM Corp All Rights Reserved
#
# SPDX-License-Identifier: Apache-2.0
#
# Exit on first error, print all commands.
set -ev

# don't rewrite paths for Windows Git Bash users
export MSYS_NO_PATHCONV=1

docker-compose -f docker-compose.yml down

docker-compose -f docker-compose.yml up -d ca.example.com orderer.example.com peer0.org1.example.com couchdb

# wait for Hyperledger Fabric to start
# incase of errors when running later commands, issue export FABRIC_START_TIMEOUT=<larger number>
export FABRIC_START_TIMEOUT=10
#echo ${FABRIC_START_TIMEOUT}
sleep ${FABRIC_START_TIMEOUT}

# Create the channel
docker exec -e "CORE_PEER_LOCALMSPID=Org1MSP" -e "CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/msp/users/Admin@org1.example.com/msp" peer0.org1.example.com peer channel create -o orderer.example.com:7050 -c mychannel -f /etc/hyperledger/configtx/channel.tx
# Join peer0.org1.example.com to the channel.
docker exec -e "CORE_PEER_LOCALMSPID=Org1MSP" -e "CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/msp/users/Admin@org1.example.com/msp" peer0.org1.example.com peer channel join -b mychannel.block

其中第13行是关闭正在运行的docker容器(如果之前这个项目开启过,或者还有残留的话)

第15行根据docker-compose.yaml配置文件启动多个容器,启动-d后面的选项指定要开启的容器,包括一个ca证书颁发机构、一个排序节点、一个peer节点,使用的数据库是couchdb。

第24行,这里是在peer节点根据通道配置文件channel.tx(这个是示例下载后自带的,也可以自己生成,见上篇文章)来创建一个通道。如果进入peer节点的容器中(docker exec -it peer0.org1.example.com bash),会发现多了一个mychannel.block,这是通道的创世区块。

第26行,将peer节点加入通道中。

现在start.sh执行完了,回到startFabric.sh中继续:

第28行,启动cli容器,这相当于一个客户端,可以和peer节点相连,将命令发送给peer节点。

第30行,在cli容器中,使用peer节点安装链码(其中链码的位置在容器中的github.com/fabcar/go文件夹中,由本地fabric-samples/chaincode映射过去,在docker-compose.yaml的cli的volumes配置中有,后面会有链码的主要内容)

第31行,在cli容器中,使用peer节点将链码在通道实例化,指定初始化参数,背书策略。

第32行,在cli容器中,使用peer节点对链码初始化。注意这一步的初始化和上一步的初始化不同,上一步是在将链码在通道上初始化,调用的是链码的Init方法;这一步是链码中实际的变量值等进行初始化,调用的是Invode方法。

目前为止,网络已经启动起来,并且链码已经部署好,后面可以使用nodejs和Fabric的链码进行交互了。

下面看一下chaincode的内容fabcar.go:

package main

/* Imports
 * 4 utility libraries for formatting, handling bytes, reading and writing JSON, and string manipulation
 * 2 specific Hyperledger Fabric specific libraries for Smart Contracts
 */
import (
	"bytes"
	"encoding/json"
	"fmt"
	"strconv"

	"github.com/hyperledger/fabric/core/chaincode/shim"
	sc "github.com/hyperledger/fabric/protos/peer"
)

// Define the Smart Contract structure
type SmartContract struct {
}

// Define the car structure, with 4 properties.  Structure tags are used by encoding/json library
type Car struct {
	Make   string `json:"make"`
	Model  string `json:"model"`
	Colour string `json:"colour"`
	Owner  string `json:"owner"`
}

/*
 * The Init method is called when the Smart Contract "fabcar" is instantiated by the blockchain network
 * Best practice is to have any Ledger initialization in separate function -- see initLedger()
 */
func (s *SmartContract) Init(APIstub shim.ChaincodeStubInterface) sc.Response {
	return shim.Success(nil)
}

/*
 * The Invoke method is called as a result of an application request to run the Smart Contract "fabcar"
 * The calling application program has also specified the particular smart contract function to be called, with arguments
 */
func (s *SmartContract) Invoke(APIstub shim.ChaincodeStubInterface) sc.Response {

	// Retrieve the requested Smart Contract function and arguments
	function, args := APIstub.GetFunctionAndParameters()
	// Route to the appropriate handler function to interact with the ledger appropriately
	if function == "queryCar" {
		return s.queryCar(APIstub, args)
	} else if function == "initLedger" {
		return s.initLedger(APIstub)
	} else if function == "createCar" {
		return s.createCar(APIstub, args)
	} else if function == "queryAllCars" {
		return s.queryAllCars(APIstub)
	} else if function == "changeCarOwner" {
		return s.changeCarOwner(APIstub, args)
	}

	return shim.Error("Invalid Smart Contract function name.")
}

func (s *SmartContract) queryCar(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {

	if len(args) != 1 {
		return shim.Error("Incorrect number of arguments. Expecting 1")
	}

	carAsBytes, _ := APIstub.GetState(args[0])
	return shim.Success(carAsBytes)
}

func (s *SmartContract) initLedger(APIstub shim.ChaincodeStubInterface) sc.Response {
	cars := []Car{
		Car{Make: "Toyota", Model: "Prius", Colour: "blue", Owner: "Tomoko"},
		Car{Make: "Ford", Model: "Mustang", Colour: "red", Owner: "Brad"},
		Car{Make: "Hyundai", Model: "Tucson", Colour: "green", Owner: "Jin Soo"},
		Car{Make: "Volkswagen", Model: "Passat", Colour: "yellow", Owner: "Max"},
		Car{Make: "Tesla", Model: "S", Colour: "black", Owner: "Adriana"},
		Car{Make: "Peugeot", Model: "205", Colour: "purple", Owner: "Michel"},
		Car{Make: "Chery", Model: "S22L", Colour: "white", Owner: "Aarav"},
		Car{Make: "Fiat", Model: "Punto", Colour: "violet", Owner: "Pari"},
		Car{Make: "Tata", Model: "Nano", Colour: "indigo", Owner: "Valeria"},
		Car{Make: "Holden", Model: "Barina", Colour: "brown", Owner: "Shotaro"},
	}

	i := 0
	for i < len(cars) {
		fmt.Println("i is ", i)
		carAsBytes, _ := json.Marshal(cars[i])
		APIstub.PutState("CAR"+strconv.Itoa(i), carAsBytes)
		fmt.Println("Added", cars[i])
		i = i + 1
	}

	return shim.Success(nil)
}

func (s *SmartContract) createCar(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {

	if len(args) != 5 {
		return shim.Error("Incorrect number of arguments. Expecting 5")
	}

	var car = Car{Make: args[1], Model: args[2], Colour: args[3], Owner: args[4]}

	carAsBytes, _ := json.Marshal(car)
	APIstub.PutState(args[0], carAsBytes)

	return shim.Success(nil)
}

func (s *SmartContract) queryAllCars(APIstub shim.ChaincodeStubInterface) sc.Response {

	startKey := "CAR0"
	endKey := "CAR999"

	resultsIterator, err := APIstub.GetStateByRange(startKey, endKey)
	if err != nil {
		return shim.Error(err.Error())
	}
	defer resultsIterator.Close()

	// buffer is a JSON array containing QueryResults
	var buffer bytes.Buffer
	buffer.WriteString("[")

	bArrayMemberAlreadyWritten := false
	for resultsIterator.HasNext() {
		queryResponse, err := resultsIterator.Next()
		if err != nil {
			return shim.Error(err.Error())
		}
		// Add a comma before array members, suppress it for the first array member
		if bArrayMemberAlreadyWritten == true {
			buffer.WriteString(",")
		}
		buffer.WriteString("{\"Key\":")
		buffer.WriteString("\"")
		buffer.WriteString(queryResponse.Key)
		buffer.WriteString("\"")

		buffer.WriteString(", \"Record\":")
		// Record is a JSON object, so we write as-is
		buffer.WriteString(string(queryResponse.Value))
		buffer.WriteString("}")
		bArrayMemberAlreadyWritten = true
	}
	buffer.WriteString("]")

	fmt.Printf("- queryAllCars:\n%s\n", buffer.String())

	return shim.Success(buffer.Bytes())
}

func (s *SmartContract) changeCarOwner(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {

	if len(args) != 2 {
		return shim.Error("Incorrect number of arguments. Expecting 2")
	}

	carAsBytes, _ := APIstub.GetState(args[0])
	car := Car{}

	json.Unmarshal(carAsBytes, &car)
	car.Owner = args[1]

	carAsBytes, _ = json.Marshal(car)
	APIstub.PutState(args[0], carAsBytes)

	return shim.Success(nil)
}

// The main function is only relevant in unit test mode. Only included here for completeness.
func main() {

	// Create a new Smart Contract
	err := shim.Start(new(SmartContract))
	if err != nil {
		fmt.Printf("Error creating new Smart Contract: %s", err)
	}
}

每个chaincode程序都必须实现 chiancode接口 ,接口中的方法会在响应传来的交易时被调用。特别地,Init(初始化)方法会在chaincode接收到instantiate(实例化)或者upgrade(升级)交易时被调用,进而使得chaincode顺利执行必要的初始化操作,包括初始化应用的状态;Invoke(调用)方法会在响应invoke(调用)交易时被调用以执行交易。

其他chaincode shim APIs中的接口被称为chaincode存根接口,用于访问及修改账本,并实现chaincode之间的互相调用。

从上面的代码中可以看到,对账本的操作主要有以下几个方法:

初始化账本,创建汽车、查询汽车、查询所有汽车、变更汽车主人。这些方法都是通过Invoke方法来调用的。

三、使用nodejs与链码交互

1、安装node依赖关系

npm install

会根据package.json文件中的配置自动将依赖下载并安装到本地目录中。

2、注册用户

node enrollAdmin.js 
node registerUser.js

这两步解释后续再补充,应该是用ca颁布证书之类。。。。

3、查询账本

1)运行查询及结果:

zhj@zhj-HP:~/project/test_fabric/fabric-samples/fabcar$ node query.js 
Store path:/home/zhj/project/test_fabric/fabric-samples/fabcar/hfc-key-store
Successfully loaded user1 from persistence
Query has completed, checking results
Response is  [{"Key":"CAR0", "Record":{"colour":"blue","make":"Toyota","model":"Prius","owner":"Tomoko"}},{"Key":"CAR1", "Record":{"colour":"red","make":"Ford","model":"Mustang","owner":"Brad"}},{"Key":"CAR2", "Record":{"colour":"green","make":"Hyundai","model":"Tucson","owner":"Jin Soo"}},{"Key":"CAR3", "Record":{"colour":"yellow","make":"Volkswagen","model":"Passat","owner":"Max"}},{"Key":"CAR4", "Record":{"colour":"black","make":"Tesla","model":"S","owner":"Adriana"}},{"Key":"CAR5", "Record":{"colour":"purple","make":"Peugeot","model":"205","owner":"Michel"}},{"Key":"CAR6", "Record":{"colour":"white","make":"Chery","model":"S22L","owner":"Aarav"}},{"Key":"CAR7", "Record":{"colour":"violet","make":"Fiat","model":"Punto","owner":"Pari"}},{"Key":"CAR8", "Record":{"colour":"indigo","make":"Tata","model":"Nano","owner":"Valeria"}},{"Key":"CAR9", "Record":{"colour":"brown","make":"Holden","model":"Barina","owner":"Shotaro"}}]

查询返回了所有的汽车的信息。分析一下:

query.js中的关于查询的参数:

const request = {
		//targets : --- letting this default to the peers assigned to the channel
		chaincodeId: 'fabcar',
		fcn: 'queryAllCars',
		args: ['']
	};

可以看到其调用chaincode里面的queryAllCars方法(截取部分代码):

func (s *SmartContract) queryAllCars(APIstub shim.ChaincodeStubInterface) sc.Response {

	startKey := "CAR0"
	endKey := "CAR999"

	resultsIterator, err := APIstub.GetStateByRange(startKey, endKey)
	.
	.
        .
	.    
}

可以看到其调用了Fabric的range查询。

2)现在进行修改query,查询一辆汽车的信息,将request参数改为下面:

const request = {
		//targets : --- letting this default to the peers assigned to the channel
		chaincodeId: 'fabcar',
		// txId: transaction_id,
		fcn: 'queryCar',
		args: ['CAR4']
	};

再次执行查询:

zhj@zhj-HP:~/project/test_fabric/fabric-samples/fabcar$ node query.js 
Store path:/home/zhj/project/test_fabric/fabric-samples/fabcar/hfc-key-store
Successfully loaded user1 from persistence
Query has completed, checking results
Response is  {"colour":"black","make":"Tesla","model":"S","owner":"Adriana"}

可以看到只返回了一辆车的信息。

4、更新账本

账本更新是从生成交易提案的应用程序开始的。就像查询一样,我们将会构造一个请求,用来识别要进行交易的通道ID、函数以及智能合约。该程序然后调用channel.SendTransactionProposalAPI将交易建议发送给peer(s)进行认证。

网络(即endorsing peer)返回一个提案答复,应用程序以此来创建和签署交易请求。该请求通过调用channel.sendTransaction API发送到排序服务器。排序服务器将把交易打包进区块,然后将区块“发送”到通道上的所有peers进行认证。(在我们的例子中,我们只有一个endorsing peer。)

最后,应用程序使用eh.setPeerAddr API连接到peer的事务监听端口,并调用eh.registerTxEvent注册与特定交易ID相关联的事务。该API使得应用程序获得事务的结果(即成功提交或不成功)。把它当作一个通知机制。

下面进行在账本登记一辆新汽车:

在invoke.js中可以看到新汽车的参数:

var request = {
		//targets: let default to the peer assigned to the client
		chaincodeId: 'fabcar',
		fcn: 'createCar',
		args: ['CAR10', 'Chevy', 'Volt', 'Red', 'Nick'],
		chainId: 'mychannel',
		txId: tx_id
	};

进行创建:

zhj@zhj-HP:~/project/test_fabric/fabric-samples/fabcar$ node invoke.js 
Store path:/home/zhj/project/test_fabric/fabric-samples/fabcar/hfc-key-store
Successfully loaded user1 from persistence
Assigning transaction_id:  e41e97f2647d899f8741dc568e731a01f05f725c83531ad7da711c1a282c702f
Transaction proposal was good
Successfully sent Proposal and received ProposalResponse: Status - 200, message - "OK"
The transaction has been committed on peer localhost:7053
Send transaction promise and event listener promise have completed
Successfully sent transaction to the orderer.
Successfully committed the change to the ledger by the peer

创建成功,然后下一步将这辆车的主人更换,将invoke.js的参数配置改为:

var request = {
		//targets: let default to the peer assigned to the client
		chaincodeId: 'fabcar',
		fcn: 'changeCarOwner',
		args: ['CAR10', 'Barry'],
		chainId: 'mychannel',
		txId: tx_id
	};

然后执行:

zhj@zhj-HP:~/project/test_fabric/fabric-samples/fabcar$ node invoke.js 
Store path:/home/zhj/project/test_fabric/fabric-samples/fabcar/hfc-key-store
Successfully loaded user1 from persistence
Assigning transaction_id:  5c37d72abe8b2ba49a1e00ebf5b4258c6189aaf96c06aff53a30cb03555b6523
Transaction proposal was good
Successfully sent Proposal and received ProposalResponse: Status - 200, message - "OK"
The transaction has been committed on peer localhost:7053
Send transaction promise and event listener promise have completed
Successfully sent transaction to the orderer.
Successfully committed the change to the ledger by the peer

现在再查询这辆车的车主:

修改query.js,将args中的CAR4改为CAR10,执行:

zhj@zhj-HP:~/project/test_fabric/fabric-samples/fabcar$ node query.js 
Store path:/home/zhj/project/test_fabric/fabric-samples/fabcar/hfc-key-store
Successfully loaded user1 from persistence
Query has completed, checking results
Response is  {"colour":"Red","make":"Chevy","model":"Volt","owner":"Barry"}

可以看到车主已经改变了。

所有操作都已执行完。除了nodejs外,Fabric还提供了和Java进行交互的sdk。

 类似资料: