编写前的注意事项:
1、运行一条带有Doker_GoVM的链
2、建议直接用官方的在线IDE去写合约,因为写完可以直接测,缺点只是调试不方便。
3、自己拉环境在本地写合约,编译时注意编译环境,官方有提醒你去Linux下去编译。
本教程使用官方的在线IDE去写合约
教程是基于官方文档写的,只是会多写一些解析步骤
1、首先新建一个合约
2、打开main.go文件(这是新增工程的默认存证模板)
package main
import (
"encoding/json"
"log"
"strconv"
"chainmaker/pb/protogo"
"chainmaker/shim"
)
//FactContract 合约对象
type FactContract struct {
}
//Fact 存证对象,存证合约的数据内容
type Fact struct {
FileHash string
FileName string
Time int
}
//NewFact 新建存证对象
func NewFact(fileHash, fileName string, time int) *Fact {
return &Fact{
FileHash: fileHash,
FileName: fileName,
Time: time,
}
}
//InitContract 合约初始化方法
func (f *FactContract) InitContract(stub shim.CMStubInterface) protogo.Response {
return shim.Success([]byte("Init Success"))
}
// UpgradeContract 合约升级方法
func (f *FactContract) UpgradeContract(stub shim.CMStubInterface) protogo.Response {
return shim.Success([]byte("Upgrade Success"))
}
//InvokeContract 调用合约
func (f *FactContract) InvokeContract(stub shim.CMStubInterface) protogo.Response {
//获取调用合约哪个方法
method := string(stub.GetArgs()["method"])
// 这里必须写成 switch {case "a": ... [case "b": ...[...]] default:...} 形式
// 而且case后面的内容必须是字符串,不能是常量
// 这里必须写成 switch {case "a": ... [case "b": ...[...]] default:...} 形式
// 而且case后面的内容必须是字符串,不能是常量
// 这里必须写成 switch {case "a": ... [case "b": ...[...]] default:...} 形式
// 而且case后面的内容必须是字符串,不能是常量
// 如果 method == "save", 执行FactContract的save方法
// 如果 method == "findByFileHash", 执行FactContract的findByFileHash方法
// 如果没有对应的 case 语句,返回错误
switch method {
case "save":
return f.Save(stub)
case "findByFileHash":
return f.FindByFileHash(stub)
default:
return shim.Error("invalid method")
}
}
//save 存证,把数据存储到链上
func (f *FactContract) Save(stub shim.CMStubInterface) protogo.Response {
// 获取调用合约的全部参数
params := stub.GetArgs()
// 获取指定的参数
fileHash := string(params["file_hash"])
fileName := string(params["file_name"])
timeStr := string(params["time"])
if fileHash == "" || fileName == "" || timeStr == "" {
//返回合约执行错误,以及错误信息
return shim.Error("fileHash and fileName and time must not empty")
}
time, err := strconv.Atoi(timeStr)
if err != nil {
msg := "time is [" + timeStr + "] not int"
// 打印日志,使用 stub.Log 打印的日志会在控制台的输出中显示
stub.Log(msg + err.Error())
//返回合约执行错误,以及错误信息
return shim.Error(msg)
}
fact := NewFact(fileHash, fileName, time)
// 序列化
factBytes, err := json.Marshal(fact)
if err != nil {
msg := "marshal data fail"
stub.Log(msg + err.Error())
return shim.Error(msg)
}
//向链上发送事件,发送的事件会在控制台的事件中显示
stub.EmitEvent("topic_vx", []string{fact.FileHash, fact.FileName})
key := getHashKey(fact.FileHash)
//把数据存到链上
err = stub.PutStateFromKeyByte(key, factBytes)
if err != nil {
msg := "fail to save fact"
stub.Log(msg + err.Error())
return shim.Error(msg)
}
//打印日志,使用 stub.Log 打印的日志会在控制台的输出中显示
stub.Log("[save] file hash:" + fact.FileHash)
stub.Log("[save] file name:" + fact.FileName)
// 返回执行成功
return shim.Success([]byte(fact.FileName + fact.FileHash))
}
//findByFileHash 根据文件哈希从链上查找数据
func (f *FactContract) FindByFileHash(stub shim.CMStubInterface) protogo.Response {
// 获取调用合约的全部参数
params := stub.GetArgs()
// 获取指定参数
fileHash := string(params["file_hash"])
// 查询结果
key := getHashKey(fileHash)
result, err := stub.GetStateFromKeyByte(key)
if err != nil {
msg := "failed to call get_state"
// 打印日志,使用 stub.Log 打印的日志会在控制台的输出中显示
stub.Log(msg + err.Error())
//返回合约执行错误,以及错误信息
return shim.Error(msg)
}
// 反序列化
var fact Fact
err = json.Unmarshal(result, &fact)
if err != nil {
msg := "unmarshal data fail"
stub.Log(msg + err.Error())
return shim.Error(msg)
}
// 记录日志
stub.Log("[find_by_file_hash] file hash:" + fact.FileHash)
stub.Log("[find_by_file_hash] file name:" + fact.FileName)
// 返回执行成功
return shim.Success(result)
}
func getHashKey(hash string) string {
return "fact_hash" + hash
}
func main() {
//运行合约
err := shim.Start(new(FactContract))
if err != nil {
log.Fatal(err)
}
}
3、模板解析
17行:Fact结构体就是要存在区块链中的,根据你自己的需要去变更结构体的字段
//Fact 存证对象,存证合约的数据内容
type Fact struct {
FileHash string
FileName string
Time int
}
24行:新建存证对象,根据Fact 结构体的变化而变化
//NewFact 新建存证对象
func NewFact(fileHash, fileName string, time int) *Fact {
return &Fact{
FileHash: fileHash,
FileName: fileName,
Time: time,
}
}
InitContract、UpgradeContract、InvokeContract 三个方法解析
- InitContract、UpgradeContract:这是合约默认必须要有的方法,不要动。如果你在13行把对应合约对象的名称改了,对应你在方法名前的名称也要改成一致。
- InvokeContract:这里是合约方法、根据你写了几个方法,依葫芦画瓢,继续补充就行。
//InitContract 合约初始化方法
func (f *FactContract) InitContract(stub shim.CMStubInterface) protogo.Response {
return shim.Success([]byte("Init Success"))
}// UpgradeContract 合约升级方法
func (f *FactContract) UpgradeContract(stub shim.CMStubInterface) protogo.Response {
return shim.Success([]byte("Upgrade Success"))
}//InvokeContract 调用合约
func (f *FactContract) InvokeContract(stub shim.CMStubInterface) protogo.Response {//获取调用合约哪个方法
method := string(stub.GetArgs()["method"])
switch method {
case "save":
return f.Save(stub)
case "findByFileHash":
return f.FindByFileHash(stub)
default:
return shim.Error("invalid method")
}
}
存证方法
以下大部分依葫芦画瓢就好了,重点关注以下内容:
- 105行:key := getHashKey(fact.FileHash) 这是模板自定义的方法,在字符串前拼接一段字符,不必理会;
- 108行:err = stub.PutStateFromKeyByte(key, factBytes),这就是最重要的将数据上链的方法! 其中 key相当于id值,你之后查的话就要根据key去查。(别忘了模板中在105行给key前加了一段字符,你查的时候也要加上) 关于上链的方法官方还提供了其他几种。
- stub.Log :都是可以输出在控制台上的,方便调试排查错误,可写可不写;
- 120行,返回值要遵从官方的这个格式。
//save 存证,把数据存储到链上
func (f *FactContract) Save(stub shim.CMStubInterface) protogo.Response {
// 获取调用合约的全部参数
params := stub.GetArgs()// 获取指定的参数
fileHash := string(params["file_hash"])
fileName := string(params["file_name"])
timeStr := string(params["time"])if fileHash == "" || fileName == "" || timeStr == "" {
//返回合约执行错误,以及错误信息
return shim.Error("fileHash and fileName and time must not empty")
}time, err := strconv.Atoi(timeStr)
if err != nil {
msg := "time is [" + timeStr + "] not int"
// 打印日志,使用 stub.Log 打印的日志会在控制台的输出中显示
stub.Log(msg + err.Error())
//返回合约执行错误,以及错误信息
return shim.Error(msg)
}fact := NewFact(fileHash, fileName, time)
// 序列化
factBytes, err := json.Marshal(fact)
if err != nil {
msg := "marshal data fail"
stub.Log(msg + err.Error())
return shim.Error(msg)
}//向链上发送事件,发送的事件会在控制台的事件中显示
stub.EmitEvent("topic_vx", []string{fact.FileHash, fact.FileName})key := getHashKey(fact.FileHash)
//把数据存到链上
err = stub.PutStateFromKeyByte(key, factBytes)
if err != nil {
msg := "fail to save fact"
stub.Log(msg + err.Error())
return shim.Error(msg)
}//打印日志,使用 stub.Log 打印的日志会在控制台的输出中显示
stub.Log("[save] file hash:" + fact.FileHash)
stub.Log("[save] file name:" + fact.FileName)// 返回执行成功
return shim.Success([]byte(fact.FileName + fact.FileHash))}
取证方法
以下大部分依葫芦画瓢就好了,重点关注以下内容:
- 133行:这里就是去拼接了字符串
- 134行:取证的方法:stub.GetStateFromKeyByte(key) ,返回的result是byte[ ]类型
- 145行:反序列化
- 157行:返回值遵从官方规范。
//findByFileHash 根据文件哈希从链上查找数据
func (f *FactContract) FindByFileHash(stub shim.CMStubInterface) protogo.Response {
// 获取调用合约的全部参数
params := stub.GetArgs()// 获取指定参数
fileHash := string(params["file_hash"])// 查询结果
key := getHashKey(fileHash)
result, err := stub.GetStateFromKeyByte(key)
if err != nil {
msg := "failed to call get_state"
// 打印日志,使用 stub.Log 打印的日志会在控制台的输出中显示
stub.Log(msg + err.Error())
//返回合约执行错误,以及错误信息
return shim.Error(msg)
}// 反序列化
var fact Fact
err = json.Unmarshal(result, &fact)
if err != nil {
msg := "unmarshal data fail"
stub.Log(msg + err.Error())
return shim.Error(msg)
}// 记录日志
stub.Log("[find_by_file_hash] file hash:" + fact.FileHash)
stub.Log("[find_by_file_hash] file name:" + fact.FileName)// 返回执行成功
return shim.Success(result)}
其他
这就是前面说的拼接字符串的方法,他在hash前加了“fact_hash”
func getHashKey(hash string) string {
return "fact_hash" + hash
}
2、代码入口包名必须为main (注意事项在注释中)
// sdk代码中,有且仅有一个main()方法 func main() { // main()方法中,下面的代码为必须代码,不建议修改main()方法当中的代码 // 其中,FactContract为用户实现合约的具体名称 err := sandbox.Start(new(FactContract)) if err != nil { log.Fatal(err) } }
4、在官方模板上新增一个查询历史记录的方法
注意:
1、之前的合约是根据Key ,去进行存证、取证的。这个key和数据库中的唯一主键不同,在区块链中,你可以通过key 去多次存证,比如说你可以存 {key:1 ,value:2},{key:1 ,value:3},{key:1 ,value:4}
2、假设你用存证方法,存储了1中说的三条数据,你调用取证方法,只会返回最近最新的一条数据{key:1 ,value:4},这是因为联盟链中 有一个状态数据库和一个历史数据库,取证方法调用的是GetStateFromKeyByte()方法,他只会去状态数据库中查。
3、如果你想要查询出当时存证时输入key=1的所有存证记录,那么就要调用NewHistoryKvIterForKey()方法,官方的存证合约并没有这个方法,现在进行一步步补充。
我去,长安链时是分2.1.+和2.3.+两个版本,上面说的是2.1.+
算了,不写了,我用的是2.3.+ ,抽时间写个2.3.+的教程