合约开发样例¶
标签:go-sdk
合约开发
非国密样例¶
本开发样例使用标准单群组四节点区块链网络结构,搭建请参考:安装。
在利用SDK进行项目开发时,对智能合约进行操作需要利用go-sdk的abigen
工具将Solidity智能合约转换为Go
文件代码,会自动生成合约中事件监听的接口。整体上主要包含六个流程:
准备需要编译的智能合约
配置好相应版本的solc编译器
构建go-sdk的合约编译工具abigen
编译生成go文件
准备建立ssl连接需要的证书
使用生成的go文件进行合约部署、调用
HelloWorld样例¶
准备HelloWorld.sol合约文件¶
# 该指令在go-sdk目录中执行
mkdir helloworld && cd helloworld
在 go-sdk 主目录中新建 helloworld 文件夹,在该文件夹中创建 HelloWorld.sol 合约。该合约提供两个接口,分别是get()和set(),用于获取/设置合约变量name。合约内容如下
pragma solidity >=0.6.10 <0.8.20;
contract HelloWorld {
string value;
constructor() public {
value = "Hello, World!";
}
function get() public view returns (string memory) {
return value;
}
function set(string v) public {
value = v;
}
}
安装solc编译器¶
该编译器用于将 sol 合约文件编译成 abi 和 bin 文件,目前FISCO BCOS提供的solc
编译器有0.4.25/0.5.2/0.6.10,每个版本有国密和非国密两种。
# 该指令在helloworld文件夹中执行
bash ../tools/download_solc.sh -v 0.4.25
构建go-sdk的代码生成工具abigen¶
该工具用于将 abi 和 bin 文件转换为 go 文件
# 该指令在helloworld文件夹中执行,编译生成abigen工具
go build ../cmd/abigen
编译生成go文件¶
先利用solc编译合约文件HelloWorld.sol,生成abi和bin文件
# 该指令在helloworld文件夹中执行
./solc-0.4.25 --bin --abi -o ./ ./HelloWorld.sol
helloworld目录下会生成HelloWorld.bin和HelloWorld.abi。此时利用abigen工具将HelloWorld.bin和HelloWorld.abi转换成HelloWorld.go:
# 该指令在helloworld文件夹中执行
./abigen --bin ./HelloWorld.bin --abi ./HelloWorld.abi --pkg helloworld --type HelloWorld --out ./HelloWorld.go
最后helloworld文件夹下面存在以下6个文件:
HelloWorld.abi、HelloWorld.bin、HelloWorld.go、HelloWorld.sol、solc-0.4.25、abigen
准备建立ssl连接需要的证书¶
使用build_chain.sh脚本搭建区块链时会在./nodes/127.0.0.1/sdk文件夹中生成sdk证书、私钥以及ca证书,需要将这三个文件拷贝至config.toml
中配置的位置。
部署合约¶
在helloworld文件夹中创建cmd文件夹,在cmd文件夹中创建main.go文件,main.go的内容如下,在该文件中调用HelloWorld.go部署智能合约
package main
import (
"fmt"
"log"
"github.com/FISCO-BCOS/go-sdk/client"
"github.com/FISCO-BCOS/go-sdk/conf"
"github.com/FISCO-BCOS/go-sdk/helloworld" // import helloworld
)
func main(){
configs, err := conf.ParseConfigFile("config.toml")
if err != nil {
log.Fatal(err)
}
config := &configs[0]
client, err := client.Dial(config)
if err != nil {
log.Fatal(err)
}
address, tx, instance, err := helloworld.DeployHelloWorld(client.GetTransactOpts(), client) // deploy contract
if err != nil {
log.Fatal(err)
}
fmt.Println("contract address: ", address.Hex()) // the address should be saved
fmt.Println("transaction hash: ", tx.Hash().Hex())
_ = instance
}
构建并执行。
# 该指令在go-sdk目录中执行
go run helloworld/cmd/main.go
注解
合约地址需要手动保存,调用合约接口时使用
调用合约get/set接口¶
在contract文件夹中创建helloworld_get.go文件,调用合约get接口,获取智能合约中name变量存储的值
package main
import (
"fmt"
"log"
"github.com/FISCO-BCOS/go-sdk/client"
"github.com/FISCO-BCOS/go-sdk/conf"
"github.com/FISCO-BCOS/go-sdk/helloworld"
"github.com/ethereum/go-ethereum/common"
)
func main() {
configs, err := conf.ParseConfigFile("config.toml")
if err != nil {
log.Fatal(err)
}
config := &configs[0]
client, err := client.Dial(config)
if err != nil {
log.Fatal(err)
}
// load the contract
contractAddress := common.HexToAddress("contract address in hex") // 0x481D3A1dcD72cD618Ea768b3FbF69D78B46995b0
instance, err := helloworld.NewHelloWorld(contractAddress, client)
if err != nil {
log.Fatal(err)
}
helloworldSession := &helloworld.HelloWorldSession{Contract: instance, CallOpts: *client.GetCallOpts(), TransactOpts: *client.GetTransactOpts()}
value, err := helloworldSession.Get() // call Get API
if err != nil {
log.Fatal(err)
}
fmt.Println("value :", value)
value = "Hello, FISCO BCOS"
tx, receipt, err := helloworldSession.Set(value) // call set API
if err != nil {
log.Fatal(err)
}
fmt.Printf("tx sent: %s\n", tx.Hash().Hex())
fmt.Printf("transaction hash of receipt: %s\n", receipt.GetTransactionHash())
}
KVTableTest样例¶
准备Table.sol合约文件¶
在 go-sdk 主目录中新建 kvtabletest 文件夹,拷贝 Table.sol 合约。
# 创建 kvtabletest 文件夹,该指令在go-sdk目录中执行
mkdir kvtabletest && cd kvtabletest
# 拷贝 Table.sol KVTableTest 合约
cp ../.ci/Table/Table.sol ../.ci/Table/KVTableTest.sol ./
准备KVTableTest.sol合约文件¶
该合约调用 Table 合约,实现创建用户表 t_kvtest,并对 t_kvtest 表进行读写。
pragma solidity >=0.6.10 <0.8.20;
import "./Table.sol";
contract KVTableTest {
event SetResult(int256 count);
KVTableFactory tableFactory;
string constant TABLE_NAME = "t_kvtest";
constructor() public {
//The fixed address is 0x1010 for KVTableFactory
tableFactory = KVTableFactory(0x1010);
// the parameters of createTable are tableName,keyField,"vlaueFiled1,vlaueFiled2,vlaueFiled3,..."
tableFactory.createTable(TABLE_NAME, "id", "item_price,item_name");
}
//get record
function get(string memory id) public view returns (bool, int256, string memory) {
KVTable table = tableFactory.openTable(TABLE_NAME);
bool ok = false;
Entry entry;
(ok, entry) = table.get(id);
int256 item_price;
string memory item_name;
if (ok) {
item_price = entry.getInt("item_price");
item_name = entry.getString("item_name");
}
return (ok, item_price, item_name);
}
//set record
function set(string memory id, int256 item_price, string memory item_name)
public
returns (int256)
{
KVTable table = tableFactory.openTable(TABLE_NAME);
Entry entry = table.newEntry();
// the length of entry's field value should < 16MB
entry.set("id", id);
entry.set("item_price", item_price);
entry.set("item_name", item_name);
// the first parameter length of set should <= 255B
int256 count = table.set(id, entry);
emit SetResult(count);
return count;
}
}
准备环境与合约编译¶
下面的操作都在 kvtabletest 文件夹中执行
bash ../tools/download_solc.sh -v 0.5.2
./solc-0.5.2 --bin --abi -o ./ ./KVTableTest.sol
编译生成 go 文件¶
先利用 solc 编译合约文件 KVTableTest.sol,生成 abi 和 bin 文件
go run ../cmd/abigen --bin ./KVTableTest.bin --abi ./KVTableTest.abi --pkg kvtabletest --type KVTableTest --out ./KVTableTest.go
最后 kvtabletest 文件夹下面存在以下5个文件和其它若干文件:
KVTableTest.abi、KVTableTest.bin、KVTableTest.go、KVTableTest.sol、solc-0.5.2
部署合约¶
在 kvtabletest 文件夹中创建 cmd 文件夹,在 cmd 文件夹中创建 kvtabletest_main.go 文件,调用 KVTableTest.go 部署智能合约。合约将创建 t_kvtest 表,该表用于记录某公司仓库中物资,以唯一的物资编号作为主key,保存物资的名称和价格。使用build_chain.sh脚本搭建区块链时会在./nodes/127.0.0.1/sdk文件夹中生成sdk证书、私钥以及ca证书,需要将这三个文件拷贝至config.toml
中配置的位置。
package main
import (
"fmt"
"log"
"github.com/FISCO-BCOS/go-sdk/client"
"github.com/FISCO-BCOS/go-sdk/conf"
kvtable "github.com/FISCO-BCOS/go-sdk/kvtabletest" // import kvtabletest
)
func main(){
configs, err := conf.ParseConfigFile("config.toml")
if err != nil {
log.Fatal(err)
}
config := &configs[0]
client, err := client.Dial(config)
if err != nil {
log.Fatal(err)
}
address, tx, instance, err := kvtable.DeployKVTableTest(client.GetTransactOpts(), client)
if err != nil {
log.Fatal(err)
}
fmt.Println("contract address: ", address.Hex()) // the address should be saved
fmt.Println("transaction hash: ", tx.Hash().Hex())
_ = instance
}
# 该指令在go-sdk目录中执行
go run kvtabletest/cmd/kvtabletest_main.go
注解
合约地址需要手动保存,调用合约接口时使用
调用合约set/get接口¶
在 contract 文件夹中新建 kvtabletest_set.go 文件,该文件调用合约 set 接口,向 t_kvtest 表中插入一条数据:id=”100010001001”、item_name=”Laptop”、item_price=6000。然后调用get接口查询数据。
package main
import (
"fmt"
"log"
"math/big"
"strings"
"github.com/FISCO-BCOS/go-sdk/abi"
"github.com/FISCO-BCOS/go-sdk/client"
"github.com/FISCO-BCOS/go-sdk/conf"
kvtable "github.com/FISCO-BCOS/go-sdk/kvtabletest"
"github.com/ethereum/go-ethereum/common"
)
func main() {
configs, err := conf.ParseConfigFile("config.toml")
if err != nil {
log.Fatal(err)
}
config := &configs[0]
client, err := client.Dial(config)
if err != nil {
log.Fatal(err)
}
// load the contract
contractAddress := common.HexToAddress(contract address in hex string) // deploy contract to get address
instance, err := kvtable.NewKVTableTest(contractAddress, client)
if err != nil {
log.Fatal(err)
}
kvtabletestSession := &kvtable.KVTableTestSession{Contract: instance, CallOpts: *client.GetCallOpts(), TransactOpts: *client.GetTransactOpts()}
id := "100010001001"
item_name := "Laptop"
item_price := big.NewInt(6000)
tx, receipt, err := kvtabletestSession.Set(id, item_price, item_name) // call set API
if err != nil {
log.Fatal(err)
}
fmt.Printf("tx sent: %s\n", tx.Hash().Hex())
// 解析abi
kvtableTestABI, err := abi.JSON(strings.NewReader(kvtable.KVTableTestABI))
if err != nil {
fmt.Printf("parse abi failed, err: %v\n", err)
return
}
// kvtableTestABI 解析返回值
ret := big.NewInt(0)
err = kvtableTestABI.Unpack(&ret, "set", common.FromHex(receipt.Output))
if err != nil {
fmt.Printf("parse return value failed, err: %v\n", err)
return
}
fmt.Printf("seted lines: %v\n", ret.String())
success, item_price, item_name, err := kvtabletestSession.Get(id) // call get API
if err != nil {
log.Fatal(err)
}
if !success {
log.Fatalf("id:%v is not found \n", id)
}
fmt.Printf("id: %v, item_price: %v, item_name: %v \n", id, item_price, item_name)
}
异步接口使用样例¶
异步合约开发指的是调用编译生成的 go 文件中提供的异步接口部署合约、修改数据,可以极大提高交易并发量。以 KVTableTest 为例,编译生成 go 文件
的步骤 同上
异步部署、调用KVTableTest合约¶
利用生成的异步接口部署和调用合约。通过注入 handler 函数,处理交易回执,获取交易回执中的 output。
package main
import (
"encoding/hex"
"fmt"
"log"
"math/big"
"strings"
"github.com/ethereum/go-ethereum/common"
"github.com/FISCO-BCOS/go-sdk/abi"
"github.com/FISCO-BCOS/go-sdk/client"
"github.com/FISCO-BCOS/go-sdk/conf"
"github.com/FISCO-BCOS/go-sdk/core/types"
kvtable "github.com/FISCO-BCOS/go-sdk/examples" // import kvtabletest
)
var (
channel = make(chan int, 0)
contractAddress common.Address
)
func deployContractHandler(receipt *types.Receipt, err error) {
if err != nil {
fmt.Printf("%v\n", err)
return
}
fmt.Println("contract address: ", receipt.ContractAddress.Hex()) // the address should be saved
contractAddress = receipt.ContractAddress
channel <- 0
}
func invokeSetHandler(receipt *types.Receipt, err error) {
if err != nil {
fmt.Printf("%v\n", err)
return
}
setedLines, err := parseOutput(kvtable.KVTableTestABI, "set", receipt)
if err != nil {
log.Fatalf("error when transfer string to int: %v\n", err)
}
fmt.Printf("seted lines: %v\n", setedLines.Int64())
channel <- 0
}
func main() {
configs, err := conf.ParseConfigFile("config.toml")
if err != nil {
log.Fatal(err)
}
config := &configs[0]
// AsyncDeploy
fmt.Println("-------------------starting deploy contract-----------------------")
client, err := client.Dial(config)
if err != nil {
log.Fatal(err)
}
tx, err := kvtable.AsyncDeployKVTableTest(client.GetTransactOpts(), deployContractHandler, client)
if err != nil {
log.Fatal(err)
}
fmt.Println("transaction hash: ", tx.Hash().Hex())
<-channel
// invoke AsyncSet to insert info
fmt.Println("\n-------------------starting invoke Set to insert info-----------------------")
instance, err := kvtable.NewKVTableTest(contractAddress, client)
if err != nil {
log.Fatal(err)
}
kvtabletestSession := &kvtable.KVTableTestSession{Contract: instance, CallOpts: *client.GetCallOpts(), TransactOpts: *client.GetTransactOpts()}
id := "100010001001"
item_name := "Laptop"
item_price := big.NewInt(6000)
tx, err = kvtabletestSession.AsyncSet(invokeSetHandler, id, item_price, item_name) // call set API
if err != nil {
log.Fatal(err)
}
fmt.Printf("tx sent: %s\n", tx.Hash().Hex())
<-channel
// invoke Get to query info
fmt.Println("\n-------------------starting invoke Get to query info-----------------------")
bool, item_price, item_name, err := kvtabletestSession.Get(id) // call get API
if err != nil {
log.Fatal(err)
}
if !bool {
log.Fatalf("id:%v is not found \n", id)
}
fmt.Printf("id: %v, item_price: %v, item_name: %v \n", id, item_price, item_name)
}
func parseOutput(abiStr, name string, receipt *types.Receipt) (*big.Int, error) {
parsed, err := abi.JSON(strings.NewReader(abiStr))
if err != nil {
fmt.Printf("parse ABI failed, err: %v", err)
}
var ret *big.Int
b, err := hex.DecodeString(receipt.Output[2:])
if err != nil {
return nil, fmt.Errorf("decode receipt.Output[2:] failed, err: %v", err)
}
err = parsed.Unpack(&ret, name, b)
if err != nil {
return nil, fmt.Errorf("unpack %v failed, err: %v", name, err)
}
return ret, nil
}
异步部署、调用HelloWorld合约¶
package main
import (
"fmt"
"log"
"github.com/FISCO-BCOS/go-sdk/client"
"github.com/FISCO-BCOS/go-sdk/conf"
"github.com/FISCO-BCOS/go-sdk/helloworld"
"github.com/ethereum/go-ethereum/common"
"github.com/FISCO-BCOS/go-sdk/core/types"
)
func main() {
configs, err := conf.ParseConfigFile("config.toml")
if err != nil {
log.Fatalf("ParseConfigFile failed, err: %v", err)
}
client, err := client.Dial(&configs[0])
if err != nil {
fmt.Printf("Dial Client failed, err:%v", err)
return
}
var contractAddress common.Address
var channel = make(chan int, 0)
tx, err := helloworld.AsyncDeployHelloWorld(client.GetTransactOpts(), func(receipt *types.Receipt, err error) {
if err != nil {
fmt.Printf("%v\n", err)
return
}
fmt.Println("contract address: ", receipt.ContractAddress.Hex()) // the address should be saved
contractAddress = receipt.ContractAddress
channel <- 0
}, client)
fmt.Println("transaction hash: ", tx.Hash().Hex())
<-channel
instance, err := helloworld.NewHelloWorld(contractAddress, client)
if err != nil {
log.Fatal(err)
}
if err != nil {
fmt.Printf("Deploy failed, err:%v", err)
return
}
hello := &helloworld.HelloWorldSession{Contract: instance, CallOpts: *client.GetCallOpts(), TransactOpts: *client.GetTransactOpts()}
ret, err := hello.Get()
if err != nil {
fmt.Printf("hello.Get() failed: %v", err)
return
}
fmt.Printf("Get: %s\n", ret)
tx, err = hello.AsyncSet(func(receipt *types.Receipt, err error) {
if err != nil {
fmt.Printf("hello.AsyncSet failed: %v\n", err)
return
}
if receipt.Status != 0 {
fmt.Printf("hello.AsyncSet failed: %v\n", receipt.GetErrorMessage())
}
channel <- 0
}, "fisco")
<-channel
ret, err = hello.Get()
if err != nil {
fmt.Printf("hello.Get() failed: %v", err)
return
}
fmt.Printf("Get: %s\n", ret)
}
国密样例¶
使用国密特性的开发流程和非国密大致相同,不同点在于以下几部分:
搭建的 FISCO BCOS 区块链网络需要开启国密特性,可参考:国密支持
go-sdk 的 config.toml 配置文件中 KeyFile 配置项,需要将非国密私钥替换为国密私钥
go-sdk 的 config.toml 配置文件中 SMCrypto 配置项,需要修改为 true
安装 solc 编译器时需要添加 -g 选项,替换为国密版本
使用 abigen 工具将 bin 和 abi 转换为 go 文件时,需要添加参数 –smcrypto=true
HelloWorld样例¶
准备HelloWorld.sol合约文件¶
在 go-sdk 主目录中新建 helloworld 文件夹,在该文件夹中创建 HelloWorld.sol 合约。该合约提供两个接口,分别是get()和set(),用于获取/设置合约变量name。合约内容如下
pragma solidity >=0.6.10 <0.8.20;
contract HelloWorld {
string name;
constructor() public {
name = "Hello, World!";
}
function get() public view returns (string memory) {
return name;
}
function set(string memory n) public {
name = n;
}
}
安装国密solc编译器¶
该编译器用于将 sol 合约文件编译成 abi 和 bin 文件
# 该指令在helloworld文件夹中执行
bash ../tools/download_solc.sh -v 0.4.25 -g
构建go-sdk的代码生成工具abigen¶
该工具用于将 abi 和 bin 文件转换为 go 文件
# 该指令在helloworld文件夹中执行,编译生成abigen工具
go build ../cmd/abigen
编译生成go文件¶
先利用solc编译合约文件HelloWorld.sol,生成abi和bin文件
# 该指令在helloworld文件夹中执行
./solc-0.4.25-gm --bin --abi -o ./ ./HelloWorld.sol
helloworld目录下会生成HelloWorld.bin和HelloWorld.abi。此时利用abigen工具将HelloWorld.bin和HelloWorld.abi转换成HelloWorld.go:
# 该指令在helloworld文件夹中执行
./abigen --bin ./HelloWorld.bin --abi ./HelloWorld.abi --pkg helloworld --type HelloWorld --out ./HelloWorld.go --smcrypto=true
接下来的步骤同非国密,不占用多余篇幅