Skip to content

KasperLiu/gobcos

Repository files navigation

gobcos

Build Status

Golang Client For FISCO BCOS 2.0.0


FISCO BCOS Go语言版本的SDK,借助以太坊代码进行改进,主要实现的功能有:

  • FISCO BCOS 2.0.0 JSON-RPC的Golang API 服务
  • Solidity合约编译为Go文件
  • 部署、查询、写入智能合约
  • 控制台

gobcos的使用可以当做是一个package进行使用,亦可对项目代码进行编译,直接使用控制台通过配置文件来进行访问FISCO BCOS。

环境准备

控制台使用

在使用控制台需要先拉取代码或下载代码,然后对配置文件gobcos_config.yaml进行更改:

git clone https://github.com/KasperLiu/gobcos.git
cd gobcos

#nano gobcos_config.yaml

本项目使用了go module特性,可以在旧版本的$GOPATH路径之外直接运行go命令,如果项目仍然在$GOPATH路径之下,则需要显示开启GO111MODULE以支持该特性:

export GO111MODULE=on

编译代码后会在$GOBIN下生成控制台程序,请确保添加$GOBIN到系统路径$PATH中,关于$GOBIN等的设置可以参考这里,以便能正常执行go生成的程序:

go install gobcos.go

如果不能访问外网,则可以设置开源代理进行依赖下载(需使用go module的特性):

export GOPROXY=https://goproxy.io

如若仍然无法解决依赖问题,则可以参考文章,使用手动下载的方式,但无法支持具体版本的依赖库 :(

最后,运行控制台查看可用指令:

gobcos help

Package功能使用

以下的示例是通过import的方式来使用gobcos,如引入RPC控制台库:

import "github.com/KasperLiu/gobcos/client"

RPC API 测试

此部分只对项目代码中的RPC API接口调用进行测试,以确定是否能顺利连接FISCO BCOS 2.0.0节点以获取区块链信息。

首先需要拉取代码:

git clone https://github.com/KasperLiu/gobcos.git

进行代码测试前,请先按照实际部署节点的RPC URL更改client/goclient_test.go中的默认的FISCO BCOS RPC连接以及群组ID:

func GetClient(t *testing.T) (*Client) {
    // RPC API
    groupID := uint(1)
    c, err := Dial("http://localhost:8545", groupID) // change it to your RPC IP & port, groupID that you want to connect
    if err != nil {
        t.Fatalf("can not dial to the RPC API: %v", err)
    }
    return c
}

测试代码默认开启的测试函数为GetClientVersion, GetBlockNumber, GetPBFTView,其余函数需去除注释并更改为实际存在的数据后才能执行。如:

// GetBlockHashByNumber returns the block hash by its block number
func TestBlockHshByNumber(t *testing.T) {
    c := GetClient(t)
    // provide a specific blocknumber
    bnum := "0x1"
    raw, err := c.GetBlockHashByNumber(context.Background(), bnum)
    if err != nil {
        t.Fatalf("block hash not found: %v", err)
    }

    t.Logf("block hash by number:\n%s", raw)
}

执行RPC client的测试代码命令为:

go test -v -count=1 ./client

JSON-RPC API调用

在测试成功后,可以在用户的工程项目中引用gobcos的RPC客户端,以调用RPC方法,所有的方法返回的是[]byte,用户可根据实际需要做进一步的JSON解析:

import "github.com/KasperLiu/gobcos/client"

下面假设有一个block.go文件需要获取FISCO BCOS 区块链的某一个区块的信息,则在引入客户端代码包后首先需要初始化客户端,提供需要连接的FISCO BCOS区块链的RPC URL及群组ID:

package main
import (
    "context"
    "github.com/KasperLiu/gobcos/client"
)

func main() {
    groupID := uint(1)
    client, err := client.Dial("http://localhost:8545", groupID) # change to your RPC URL and GroupID
    if err != nil {
    	// handle err
    }
}

然后可按照FISCO BCOS的RPC API文档进行区块链信息查询,需要注意的是,gobcos客服端的RPC方法调用需要将API文档里的方法首字母更改为大写字母Get

blockHash := "0xc0b21d064b97bafda716e07785fe8bb20cc23506bb980f12c7f7a4f4ef50ce30" # fake hash
includeTx := false # only display the transaction hash
block, err := client.GetBlockByHash(context.BackGround(), blockHash, includeTx) # invoke "getBlockByHash“
if err != nil {
    // handle err
}

若要在代码的后续使用中获取其他群组的区块信息,则可以直接调用客户端的SetGroupID方法进行动态切换,如:

// switch to other group
client.SetGroupID(otherGroupID)
client.GetBlockNumber(context.BackGround()) # get the lastest block number of the otherGroupID

Solidity合约编译为Go文件

在利用SDK进行项目开发时,对智能合约进行操作时需要将Solidity智能合约利用gobcos的abigen工具转换为Go文件代码。整体上主要包含了五个流程:

  • 准备需要编译的智能合约
  • 配置好相应版本的solc编译器
  • 构建gobcos的合约编译工具abigen
  • 编译生成go文件
  • 使用生成的go文件进行合约调用

下面的内容作为一个示例进行使用介绍。

1.提供一份简单的样例智能合约Store.sol如下:

pragma solidity ^0.4.25;

contract Store {
  event ItemSet(bytes32 key, bytes32 value);

  string public version;
  mapping (bytes32 => bytes32) public items;

  constructor(string _version) public {
    version = _version;
  }

  function setItem(bytes32 key, bytes32 value) external {
    items[key] = value;
    emit ItemSet(key, value);
  }
}

2.安装对应版本的solc编译器,目前FISCO BCOS默认的solc编译器版本为0.4.25

solc --version
# solc, the solidity compiler commandline interface
# Version: 0.4.25+commit.59dbf8f1.Linux.g++

3.构建gobcos的代码生成工具abigen

git clone https://github.com/KasperLiu/gobcos.git # 下载gobcos代码,如已下载请跳过
cd gobcos # 进入代码目录
go build ./cmd/abigen # 编译生成abigen工具

执行命令后,检查根目录下是否存在abigen,并将生成的abigen以及所准备的智能合约Store.sol放置在一个新的目录下:

mkdir ../newdir && cp ./abigen ../newdir && cd ../newdir
# cp your/Store.sol path/to/newdir

4.编译生成go文件,先利用solc将合约文件生成abibin文件,以前面所提供的Store.sol为例:

solc --bin -o ./ Store.sol && solc --abi -o ./ Store.sol

Store.sol目录下会生成Store.binStore.abi。此时利用abigen工具将Store.binStore.abi转换成Store.go

./abigen --bin=Store.bin --abi=Store.abi --pkg=store --out=Store.go

最后目录下面存在以下文件:

>>ls
abigen  Store.abi  Store.bin  Store.go  Store.sol

5.调用生成的Store.go文件进行合约调用

至此,合约已成功转换为go文件,用户可根据此文件在项目中利用SDK进行合约操作。具体的使用可参阅下一节。

部署、查询、写入智能合约

此部分承接上一节的内容,同时也简单涵盖了SDK的合约使用部分以及账户私钥的加载。

创建外部账户

SDK发送交易需要一个外部账户,导入gobcos的crypto包,该包提供用于生成随机私钥的GenerateKey方法:

privateKey, err := crypto.GenerateKey()
if err != nil {
    log.Fatal(err)
}

然后我们可以通过导入golangcrypto/ecdsa包并使用FromECDSA方法将其转换为字节:

privateKeyBytes := crypto.FromECDSA(privateKey)

我们现在可以使用gobcos的common/hexutil包将它转换为十六进制字符串,该包提供了一个带有字节切片的Encode方法。 然后我们在十六进制编码之后删除“0x”。

fmt.Println(hexutil.Encode(privateKeyBytes)[2:])

这就是用于签署交易的私钥,将被视为密码,永远不应该被共享给别人

由于公钥是从私钥派生的,加密私钥具有一个返回公钥的Public方法:

publicKey := privateKey.Public()

将其转换为十六进制的过程与我们使用转化私钥的过程类似。 我们剥离了0x和前2个字符04,它始终是EC前缀,不是必需的。

publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
    log.Fatal("cannot assert type: publicKey is not of type *ecdsa.PublicKey")
}

publicKeyBytes := crypto.FromECDSAPub(publicKeyECDSA)
fmt.Println(hexutil.Encode(publicKeyBytes)[4:])

现在我们拥有公钥,就可以轻松生成你经常看到的公共地址。 加密包里有一个PubkeyToAddress方法,它接受一个ECDSA公钥,并返回公共地址。

address := crypto.PubkeyToAddress(*publicKeyECDSA).Hex()
fmt.Println(address) // 0x96216849c49358B10257cb55b28eA603c874b05E

公共地址可以查询合约信息。

整体的代码示例为:

package main

import (
    "crypto/ecdsa"
    "fmt"
    "log"
    "os"
    "github.com/KasperLiu/gobcos/crypto"
    "github.com/KasperLiu/gobcos/common/hexutil"
)

func main() {
    privateKey, err := crypto.GenerateKey()
    if err != nil {
        log.Fatal(err)
    }

    privateKeyBytes := crypto.FromECDSA(privateKey)
    fmt.Println("private key: ", hexutil.Encode(privateKeyBytes)[2:]) // privateKey in hex without "0x"

    publicKey := privateKey.Public()
    publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
    if !ok {
        log.Fatal("cannot assert type: publicKey is not of type *ecdsa.PublicKey")
    }

    publicKeyBytes := crypto.FromECDSAPub(publicKeyECDSA)
    fmt.Println("publick key: ", hexutil.Encode(publicKeyBytes)[4:])  // publicKey in hex without "0x"

    address := crypto.PubkeyToAddress(*publicKeyECDSA).Hex()
    fmt.Println("address: ", address)  // account address
}

部署智能合约

首先在利用abigen生成的Store.go文件下,创建一个新的contract_run.go文件用来调用Store.go文件,并创建一个新的文件夹来放置Store.go以方便调用,同时利用go mod进行包管理,初始化为一个contract包:

mkdir testfile
mv ./Store.go testfile
touch contract_run.go
go mod init contract

此时目录下会生成go.mod包管理文件。而在contract_deploy.go部署合约之前,需要先从gobcos中导入accounts/abi/bind包,然后调用传入私钥的NewKeyedTransactor

package main

import (
    "fmt"
    "log"
    "github.com/KasperLiu/gobcos/client"
    "github.com/KasperLiu/gobcos/accounts/abi/bind"
    "github.com/KasperLiu/gobcos/crypto"
    store "contract/testfile" // import Store.go
)

func main(){
    groupID := uint(1)
    client, err := client.Dial("http://localhost:8545", groupID)
    if err != nil {
        log.Fatal(err)
    }
    privateKey, err := crypto.HexToECDSA("input your privateKey in hex without \"0x\"") // 145e247e170ba3afd6ae97e88f00dbc976c2345d511b0f6713355d19d8b80b58
    if err != nil {
        log.Fatal(err)
    }
    auth := bind.NewKeyedTransactor(privateKey) // input your privateKey
    input := "Store deployment 1.0"
    address, tx, instance, err := store.DeployStore(auth, client, input)
    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
}

加载智能合约

在部署完智能合约后,可以获取到合约的地址,但在进行合约查询以及写入时,需要先加载智能合约,此时需要导入common包以获取正确的合约地址,新建contract_load.go以加载智能合约:

package main 

import (
    "fmt"
    "log"
    "github.com/KasperLiu/gobcos/common"
    "github.com/KasperLiu/gobcos/client"
    store "contract/testfile" // for demo
)

func main() {
    groupID := uint(1)
    client, err := client.Dial("http://localhost:8545", groupID)
    if err != nil {
        log.Fatal(err)
    }

    address := common.HexToAddress("contract addree in hex") // 0x0626918C51A1F36c7ad4354BB1197460A533a2B9
    instance, err := store.NewStore(address, client)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("contract is loaded")
    _ = instance
}

查询智能合约

在部署过程中设置的Store.sol合约中有一个名为version的全局变量。 因为它是公开的,这意味着它们将成为我们自动创建的getter函数。 常量和view函数也接受bind.CallOpts作为第一个参数,新建contract_read.go文件以查询合约:

package main

import (
    "fmt"
    "log"
    "github.com/KasperLiu/gobcos/common"
    "github.com/KasperLiu/gobcos/client"
    "github.com/KasperLiu/gobcos/accounts/abi/bind"
    store "contract/testfile" // for demo
)

func main() {
    groupID := uint(1)
    client, err := client.Dial("http://localhost:8545", groupID)
    if err != nil {
        log.Fatal(err)
    }

    // load the contract
    address := common.HexToAddress("contract addree in hex") // 0x0626918C51A1F36c7ad4354BB1197460A533a2B9
    instance, err := store.NewStore(address, client)
    if err != nil {
        log.Fatal(err)
    }

    opts := &bind.CallOpts{From: common.HexToAddress("account address")} //0xFbb18d54e9Ee57529cda8c7c52242EFE879f064F
    version, err := instance.Version(opts)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("version :", version) // "Store deployment 1.0"
}

写入智能合约

写入智能合约需要我们用私钥来对交易事务进行签名,我们创建的智能合约有一个名为SetItem的外部方法,它接受soliditybytes32类型的两个参数(key,value)。 这意味着在Go文件中需要传递一个长度为32个字节的字节数组。 调用SetItem方法需要我们传递我们之前创建的auth对象(keyed transactor)。 在幕后,此方法将使用它的参数对此函数调用进行编码,将其设置为事务的data属性,并使用私钥对其进行签名。 结果将是一个已签名的事务对象。新建contract_write.go来测试写入智能合约:

package main

import (
    "fmt"
    "log"
    "context"
    "github.com/KasperLiu/gobcos/common"
    "github.com/KasperLiu/gobcos/client"
    "github.com/KasperLiu/gobcos/accounts/abi/bind"
    "github.com/KasperLiu/gobcos/crypto"
    store "contract/testfile" // for demo
)

func main() {
    groupID := uint(8)
    client, err := client.Dial("http://localhost:8545", groupID)
    if err != nil {
        log.Fatal(err)
    }

    // load the contract
    address := common.HexToAddress("contract addree in hex") // 0x0626918C51A1F36c7ad4354BB1197460A533a2B9
    instance, err := store.NewStore(address, client)
    if err != nil {
        log.Fatal(err)
    }

    key := [32]byte{}
    value := [32]byte{}
    copy(key[:], []byte("foo"))
    copy(value[:], []byte("bar"))

    privateKey, err := crypto.HexToECDSA("input your privateKey in hex") // 145e247e170ba3afd6ae97e88f00dbc976c2345d511b0f6713355d19d8b80b58
    if err != nil {
        log.Fatal(err)
    }

    auth := bind.NewKeyedTransactor(privateKey)
    tx, err := instance.SetItem(auth, key, value)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("tx sent: %s\n", tx.Hash().Hex())

    // wait for the mining
    receipt, err := bind.WaitMined(context.Background(), client, tx)
    if err != nil {
        log.Fatalf("tx mining error:%v\n", err)
    }
    fmt.Printf("transaction hash of receipt: %s\n", receipt.GetTransactionHash())
    
    // read the result
    opts := &bind.CallOpts{From: common.HexToAddress("0xFbb18d54e9Ee57529cda8c7c52242EFE879f064F")} // 0xFbb18d54e9Ee57529cda8c7c52242EFE879f064F
    result, err := instance.Items(opts, key)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(string(result[:])) // "bar"
}

About

Golang Client For FISCO BCOS 2.0

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published