16. 预言机使用文档
16.1. 安装和配置
16.1.1. 服务依赖
16.1.1.1. 操作系统
预言机合约运行在Docker VM中,当前仅支持部署在Linux系统
16.1.1.2. docker方式
git
docker 18+
docker-compose 1.29.2
16.1.1.3. 源码方式
Mysql 5.7+
golang 1.16+
16.1.2. 源码下载
下载代码
$ git clone -b v1.0.0_alpha https://git.chainmaker.org.cn/chainmaker/chainmaker-oracle
16.1.3. 服务配置
16.1.3.1. 整体配置目录结构如下:
├── config_files # 配置文件
│ ├── crypto-config # 使用chainmaker-cryptogen 工具生成的证书文件
│ ├── sdk-config # 链sdk配置
│ │ ├── sdk_config_chain1.yml # 链1的sdk
│ │ └── sdk_config_chain2.yml # 链2的sdk
│ └── smart_oracle.yml # 预言机服务的配置文件
16.1.3.2. 预言机配置结构(smart_oracle.yml):
用户需要关注如下配置,详情参考完整的配置文件
# 链证书配置相关
sdks:
# 预言机数据库配置
oracle_mysql:
# 预言机提供的mysql数据源
datasource_mysql:
- datasource: local
user_name: test
password: 123456@
db_address: oracle_db:3306
database: chain_oracle
max_open: 2
max_idle: 1
max_idle_seconds: 180
max_life_seconds: 1800
16.1.4. 快速启动一条docker-vm引擎的链(已有链则跳过)
$ git clone https://git.chainmaker.org.cn/chainmaker/chainmaker-go.git
$ git clone -b v2.2.0 https://git.chainmaker.org.cn/chainmaker/chainmaker-cryptogen.git
$ cd chainmaker-cryptogen && make
$ cd ../chainmaker-go/tools/ && ln -s ../../chainmaker-cryptogen/ .
# 修改下链的grpc配置中消息大小,链的chainmaker.yml文件
$ cd ../config/config_tpl
$ sed -i 's/max_send_msg_size: 10/max_send_msg_size: 200/g' chainmaker.tpl
$ sed -i 's/max_recv_msg_size: 10/max_recv_msg_size: 200/g' chainmaker.tpl
$ cd ../../scripts
$ ./prepare.sh 4 2
1
INFO
YES
$ ./build_release.sh
$ ./cluster_quick_start.sh normal
16.1.5. 运行服务方式一:使用docker-compose启动
若需自定义则修改下方配置信息
修改文件
chainmaker-oralce/config_files/smart_oracle.yml
修改文件
chainmaker-oralce/config_files/sdk-config/sdk_config_chain1.yml
添加证书
chainmaker-oralce/config_files/crypto-config
执行docker-compose up即可启动服务
# 设置证书
$ cd chainmaker-oracle
$ rm -rf config_files/crypto-config/ && cp -rf ../chainmaker-go/build/crypto-config/ config_files/
# 修改链节点地址为局域网IP
$ vim config_files/sdk-config/sdk_config_chain1.yml
$ vim config_files/sdk-config/sdk_config_chain2.yml
# 启动服务
$ docker-compose up
# 看到如下日志表示成功启动:
http service start , port :10123
# 查看服务健康状态
$ curl -X GET localhost:10123/v1/health
$ curl -X GET localhost:10124/v1/health
# 查看日志
$ docker logs -f --tail 100 chainmaker-oracle-1
$ docker logs -f --tail 100 chainmaker-oracle-2
停止服务
$ docker-compse down
# 删除mysql中的数据
$ rm -rf ./mysql-data-volumes/data/
16.1.6. 运行服务方式二:使用源码启动
编译:
$ cd chainmaker-oracle && go build -o chainmaker-oracle
初始化数据库: 数据库上执行代码的scripts/init.sql脚本文件
$ mysql -uroot -p < scripts/init.sql
修改配置信息
修改文件chainmaker-oralce/config_files/smart_oracle.yml
修改文件chainmaker-oralce/config_files/sdk-config/sdk_config_chain1.yml
添加证书chainmaker-oralce/config_files/crypto-config
启动服务
# 直接启动
$ ./chainmaker-oracle
# 或指定配置文件启动
$ ./chainmaker-oracle -i config_files/smart_oracle.yml
# 看到如下日志表示成功启动:
http service start , port :10123
# 查看服务健康状态
$ curl -X GET localhost:10123/v1/health
# 查看日志
$ tail -100 log/system.log
# 停止服务
$ kill ${pid}
16.1.7. 预言机合约安装
预言机合约的安装是通过http接口的形式给出,可以直接调用接口
# admin_token 为配置中的admin_token
# address 为预言机监听,接收http请求地址
# chain_alias 为配置文件smart_oracle.yml中的chain_alias
# oracle-contract 为预言机合约二进制压缩文件(默认为.7z)
$ curl -H "admin_auth_token: ${admin_token}" -X POST ${address}/v1/install_contract -F "chain_alias=${chain_alias}" -F "contract_version=1" -F "runtime=DOCKER_GO" -F "contract_file=@${oracle-contract}"
# 示例:
$ cd chainmaker-oracle
$ 7z a oracle_contract_v1.7z standard_oracle_contract/oracle_contract_file/oracle_contract_v1
# 给chain1安装oracle合约
$ curl -H 'admin_auth_token: si!*dfji@12mnku' -X POST localhost:10123/v1/install_contract -F "chain_alias=chain1_alias" -F "contract_version=1" -F "runtime=DOCKER_GO" -F "contract_file=@./oracle_contract_v1.7z"
# 给chain2安装oracle合约
$ curl -H 'admin_auth_token: si!*dfji@12mnku' -X POST localhost:10123/v1/install_contract -F "chain_alias=chain2_alias" -F "contract_version=1" -F "runtime=DOCKER_GO" -F "contract_file=@./oracle_contract_v1.7z"
为了方便使用,提供了install-upgrade-oracle-contract.sh安装脚本,可以按照命令提示也可以安装
$ ./install-upgrade-oracle-contract.sh
16.1.8. 使用用户合约取数据(以cmc工具为例)
16.1.8.1. 编译CMC
$ git clone https://git.chainmaker.org.cn/chainmaker/chainmaker-go.git
$ cd chainmaker-go/
$ make cmc
$ cp bin/cmc ../chainmaker-oracle/ && cd ../chainmaker-oracle/
16.1.8.2. 使用cmc工具安装示例用户合约文件
# 链1上安装用户合约
./cmc client contract user create --contract-name=use_demo --runtime-type=DOCKER_GO --byte-code-path=./standard_oracle_contract/use_demo.7z --version=1 --sdk-conf-path=./config_files/sdk-config/sdk_config_chain1.yml --admin-key-file-paths=./config_files/crypto-config/wx-org1.chainmaker.org/user/admin1/admin1.tls.key,./config_files/crypto-config/wx-org2.chainmaker.org/user/admin1/admin1.tls.key,./config_files/crypto-config/wx-org3.chainmaker.org/user/admin1/admin1.tls.key,./config_files/crypto-config/wx-org4.chainmaker.org/user/admin1/admin1.tls.key --admin-crt-file-paths=./config_files/crypto-config/wx-org1.chainmaker.org/user/admin1/admin1.tls.crt,./config_files/crypto-config/wx-org2.chainmaker.org/user/admin1/admin1.tls.crt,./config_files/crypto-config/wx-org3.chainmaker.org/user/admin1/admin1.tls.crt,./config_files/crypto-config/wx-org4.chainmaker.org/user/admin1/admin1.tls.crt --sync-result=true --params="{\"chainId\":\"chain1\",\"version\":\"1\"}" --chain-id=chain1
# 链2上安装用户合约
./cmc client contract user create --contract-name=use_demo --runtime-type=DOCKER_GO --byte-code-path=./standard_oracle_contract/use_demo.7z --version=1 --sdk-conf-path=./config_files/sdk-config/sdk_config_chain2.yml --admin-key-file-paths=./config_files/crypto-config/wx-org1.chainmaker.org/user/admin1/admin1.tls.key,./config_files/crypto-config/wx-org2.chainmaker.org/user/admin1/admin1.tls.key,./config_files/crypto-config/wx-org3.chainmaker.org/user/admin1/admin1.tls.key,./config_files/crypto-config/wx-org4.chainmaker.org/user/admin1/admin1.tls.key --admin-crt-file-paths=./config_files/crypto-config/wx-org1.chainmaker.org/user/admin1/admin1.tls.crt,./config_files/crypto-config/wx-org2.chainmaker.org/user/admin1/admin1.tls.crt,./config_files/crypto-config/wx-org3.chainmaker.org/user/admin1/admin1.tls.crt,./config_files/crypto-config/wx-org4.chainmaker.org/user/admin1/admin1.tls.crt --sync-result=true --params="{\"chainId\":\"chain2\",\"version\":\"1\"}" --chain-id=chain2
16.1.8.3. 使用管理员身份(也即为安装预言机合约相同的签名)注册查询topic,使用普通用户身份根据topic查询
# 注册topic[event_state_query],查询数据源local的表event_state
./cmc client contract user invoke --contract-name=oracle_contract_v1 --method=invoke_contract --sdk-conf-path=./config_files/sdk-config/sdk_config_chain1.yml --params="{\"method\":\"registerQueryTopic\",\"topic\":\"event_state_query\",\"method_type\":\"queryMysql\",\"query_body\": \"{\\\"sql_sentence\\\":\\\"select * from event_state \\\",\\\"data_source\\\":\\\"local\\\",\\\"sql_type\\\":\\\"select\\\"}\" }" --sync-result=true --chain-id=chain1
# 使用topic[event_state_query]查询
./cmc client contract user invoke --contract-name=use_demo --method=invoke_contract --sdk-conf-path=./config_files/sdk-config/sdk_config_chain1.yml --params="{\"method\":\"query_topic\",\"topic\":\"event_state_query\"}" --sync-result=true --chain-id=chain1
16.1.8.4. 取json类型数据
./cmc client contract user invoke --contract-name=use_demo --method=invoke_contract --sdk-conf-path=./config_files/sdk-config/sdk_config_chain1.yml --params="{\"method\":\"query_http\",\"url\":\"https://v0.yiketianqi.com/api?unescape=1&version=v61&appid=88684831&appsecret=OsSX6jwW\",\"fetch_data_type\":\"json\",\"fetch_data_formular\":\"\/aqi\/no2_desc\",\"max_retry\":\"3\",\"connection_timeout\":\"20\",\"max_fetch_timeout\":\"20\"}" --sync-result=true --chain-id=chain1
结果:INVOKE contract resp, [code:0]/[msg:]/[contractResult:result:”status:200 payload:”{\”result\”:null,\”code\”:\”202\”,\”message\”:\”success\”,\”response_hash\”:\”b1ee8be6f122612f1bfeac335ba1f2f3\”}” “ message:”Success” gas_used:18510 contract_event:<topic:”oracle_query” tx_id:”16fc7e533eabd3d7ca52fdfc0721826594cce7e90d084fc890053e15aa8b479f” contract_name:”oracle_contract_v1” contract_version:”1” event_data:”1” event_data:”chain1_alias” event_data:”b1ee8be6f122612f1bfeac335ba1f2f3” event_data:”16fc7e533eabd3d7ca52fdfc0721826594cce7e90d084fc890053e15aa8b479f” event_data:”7” event_data:”{”method”:”GET”,”url”:”https://v0.yiketianqi.com/api?unescape=1\u0026version=v61\u0026appid=88684831\u0026appsecret=OsSX6jwW”,”http_header”:null,”http_body”:null,”connection_timeout”:30,”max_fetch_timeout”:30,”fetch_data_type”:”json”,”fetch_data_formula”:”/aqi/no2_desc”}” > ]/[txId:16fc7e533eabd3d7ca52fdfc0721826594cce7e90d084fc890053e15aa8b479f]
16.1.8.5. 取html数据
./cmc client contract user invoke --contract-name=use_demo --method=invoke_contract --sdk-conf-path=./config_files/sdk-config/sdk_config_chain1.yml --params="{\"method\":\"query_http\",\"url\":\"https://www.baidu.com\",\"http_body\":null,\"connection_timeout\":\"5\",\"max_retry\":\"3\",\"max_fetch_timeout\":\"10\",\"fetch_data_type\":\"html\",\"fetch_data_formular\":\"\/\/title\"}" --sync-result=true --chain-id=chain1
结果:INVOKE contract resp, [code:0]/[msg:]/[contractResult:result:”status:200 payload:”{\”result\”:null,\”code\”:\”202\”,\”message\”:\”success\”,\”response_hash\”:\”30a004cdc8b2f4e80876dbed381fd3e7\”}” “ message:”Success” gas_used:19115 contract_event:<topic:”oracle_query” tx_id:”16fc7e5c927ec90fca52fdfc07218265daa02eb765794c77bb2deceebd4a4b05” contract_name:”oracle_contract_v1” contract_version:”1” event_data:”1” event_data:”chain1_alias” event_data:”30a004cdc8b2f4e80876dbed381fd3e7” event_data:”16fc7e5c927ec90fca52fdfc07218265daa02eb765794c77bb2deceebd4a4b05” event_data:”9” event_data:”{”method”:”GET”,”url”:”https://www.baidu.com”,”http_header”:{”User-Agent”:”Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3776.0 Safari/537.36”},”http_body”:null,”connection_timeout”:30,”max_fetch_timeout”:30,”fetch_data_type”:”html”,”fetch_data_formula”:”//title”}” > ]/[txId:16fc7e5c927ec90fca52fdfc07218265daa02eb765794c77bb2deceebd4a4b05]
16.1.8.6. 取xml数据
./cmc client contract user invoke --contract-name=use_demo --method=invoke_contract --sdk-conf-path=./config_files/sdk-config/sdk_config_chain1.yml --params="{\"method\":\"query_http\",\"url\":\"https://www.w3school.com.cn/example/xmle/note.xml\",\"http_body\":null,\"connection_timeout\":\"5\",\"max_retry\":\"3\",\"max_fetch_timeout\":\"10\",\"fetch_data_type\":\"xml\",\"fetch_data_formular\":\"\/\/note\"}" --sync-result=true --chain-id=chain1
结果: INVOKE contract resp, [code:0]/[msg:]/[contractResult:result:”status:200 payload:”{\”result\”:null,\”code\”:\”202\”,\”message\”:\”success\”,\”response_hash\”:\”4ca4a8cb3f2907eb5dac3f6f673fec0e\”}” “ message:”Success” gas_used:17611 contract_event:<topic:”oracle_query” tx_id:”16fc7e62554560a5ca52fdfc072182656b74385f041e4da8aadf68e78727e06a” contract_name:”oracle_contract_v1” contract_version:”1” event_data:”1” event_data:”chain1_alias” event_data:”4ca4a8cb3f2907eb5dac3f6f673fec0e” event_data:”16fc7e62554560a5ca52fdfc072182656b74385f041e4da8aadf68e78727e06a” event_data:”11” event_data:”{”method”:”GET”,”url”:”https://www.w3school.com.cn/example/xmle/note.xml”,”http_header”:null,”http_body”:null,”connection_timeout”:30,”max_fetch_timeout”:30,”fetch_data_type”:”xml”,”fetch_data_formula”:”//note”}” > ]/[txId:16fc7e62554560a5ca52fdfc072182656b74385f041e4da8aadf68e78727e06a]
16.1.8.7. 取VRF数据
./cmc client contract user invoke --contract-name=use_demo --method=invoke_contract --sdk-conf-path=./config_files/sdk-config/sdk_config_chain1.yml --params="{\"method\":\"get_vrf\",\"alpha\":\"f12vvio\"}" --sync-result=true --chain-id=chain1
结果:INVOKE contract resp, [code:0]/[msg:]/[contractResult:result:”status:200 payload:”{\”result\”:null,\”code\”:\”202\”,\”message\”:\”success\”,\”response_hash\”:\”dce88bc54c62200bfbdc24f755535d54\”}” “ message:”Success” gas_used:14431 contract_event:<topic:”oracle_query” tx_id:”16fc7e662a606536ca52fdfc07218265bf42b16723af4d7db03dec55a11dea46” contract_name:”oracle_contract_v1” contract_version:”1” event_data:”3” event_data:”chain1_alias” event_data:”dce88bc54c62200bfbdc24f755535d54” event_data:”16fc7e662a606536ca52fdfc07218265bf42b16723af4d7db03dec55a11dea46” event_data:”13” event_data:”f12vvio” > ]/[txId:16fc7e662a606536ca52fdfc07218265bf42b16723af4d7db03dec55a11dea46]
16.1.8.8. 取mysql数据
./cmc client contract user invoke --contract-name=use_demo --method=invoke_contract --sdk-conf-path=./config_files/sdk-config/sdk_config_chain1.yml --params="{\"method\":\"query_mysql\",\"sql_type\":\"select\",\"page_num\":\"1\",\"page_size\":\"10\",\"sql_sentence\":\"select * from event_result \",\"data_source\":\"mysql\"}" --sync-result=true --chain-id=chain1
INVOKE contract resp, [code:0]/[msg:]/[contractResult:result:”{”result”:null,”code”:”202”,”message”:”success”,”response_hash”:”3896ca045ed94ff9c3d5add90f206b0b”}” message:”Success” gas_used:15706 contract_event:<topic:”oracle_query” tx_id:”16fc7e6902f78cafca52fdfc07218265dc4d346d146b4b08a69f088f842d5585” contract_name:”oracle_contract_v1” contract_version:”1” event_data:”2” event_data:”chain1_alias” event_data:”3896ca045ed94ff9c3d5add90f206b0b” event_data:”16fc7e6902f78cafca52fdfc07218265dc4d346d146b4b08a69f088f842d5585” event_data:”15” event_data:”{”sql_type”:”select”,”sql_sentence”:”select * from event_result “,”data_source”:”mysql”}” > ]/[txId:16fc7e6902f78cafca52fdfc07218265dc4d346d146b4b08a69f088f842d5585]
16.1.8.9. 使用hash查询数据(注意hash为预言机合约返回给用户合约的hash值)
从上方结果中任意取一个response_hash
填充到query_hash
中。
./cmc client contract user invoke --contract-name=use_demo --method=invoke_contract --sdk-conf-path=./config_files/sdk-config/sdk_config_chain1.yml --params="{\"method\":\"query_hash\",\"query_hash\":\"3896ca045ed94ff9c3d5add90f206b0b\"}" --sync-result=true --chain-id=chain1
结果:INVOKE contract resp, [code:0]/[msg:]/[contractResult:result:”{”result”:null,”code”:”201”,”message”:”query success, get no result”,”response_hash”:””}” message:”Success” gas_used:10259 ]/[txId:16fc7e6e0343ce7fca52fdfc07218265e1778052f320478c939d3c86a8c57cad]
16.1.8.10. 跨链查询数据(注意hash为预言机合约返回给用户合约的hash值)
示例中从上方结果中任意取一个response_hash
填充到query_hash
中(此为用户合约行为,可自定义)。
./cmc client contract user invoke --contract-name=use_demo --method=invoke_contract --sdk-conf-path=./config_files/sdk-config/sdk_config_chain1.yml --params="{\"method\":\"query_hash\",\"query_hash\":\"3896ca045ed94ff9c3d5add90f206b0b\"}" --sync-result=true --chain-id=chain1
# 注意query_hash 需要为查询返回的值
./cmc client contract user invoke --contract-name=use_demo --method=invoke_contract --sdk-conf-path=./config_files/sdk-config/sdk_config_chain1.yml --params="{\"method\":\"query_cross\",\"chain_alias\":\"chain1_alias\",\"contract_name\":\"use_demo\",\"method_name\":\"query_hash\", \"query_hash\":\"conf\"}" --sync-result=true --chain-id=chain2
结果:INVOKE contract resp, [code:0]/[msg:]/[contractResult:result:”QueryCross ok” message:”Success” gas_used:16445 contract_event:<topic:”oracle_query” tx_id:”16fc7e8add879fd8ca52fdfc0721826578760f8515c049599c29e3cadc3a382e” contract_name:”oracle_contract_v1” contract_version:”1” event_data:”5” event_data:”chain2_alias” event_data:”55193d721576a16e21b2a110d7f11bd3” event_data:”16fc7e8add879fd8ca52fdfc0721826578760f8515c049599c29e3cadc3a382e” event_data:”6” event_data:”{”chain_alias”:”chain1_alias”,”contract_name”:”use_demo”,”method_name”:”query_hash”,”params”:[{”key”:”query_hash”,”value”:”Y29uZg==”}]}” > ]/[txId:16fc7e8add879fd8ca52fdfc0721826578760f8515c049599c29e3cadc3a382e]
16.2. 通过开发智能合约来操作预言机取数据
预言机通过在链上部署预言机智能合约oracle_contract
,来给用户提供取数据功能。用户可以通过调用oracle_contract
相应接口即可。由于取数据是一个不确定运行时长的过程,因此我们采用的是调用->事件->回调的方式。用户合约告知预言机需要取的数据类型,预言机取到数据通过用户合约注册的回调函数来告知用户合约
16.2.1. 用户合约的安装,安装时候需要查询一下预言机合约的公钥签名,以便后续校验数据
func (o *UseDemo) getOralceContractPk(stub shim.CMStubInterface) (string, error) {
parameters := make(map[string][]byte)
parameters["method"] = []byte("queryOracleContractPK")
resp := stub.CallContract("oracle_contract_v1", "1", parameters)
if resp.Status != 200 {
stub.Log("getOralceContractPk error," + resp.String())
return "", errors.New(resp.String())
}
var responeT OracleQueryResult
uerr := json.Unmarshal(resp.Payload, &responeT)
if uerr != nil {
stub.Log("getOralceContractPk unmarshal error")
return "", errors.New("getOralceContractPk unmarshal error" + uerr.Error())
}
return string(responeT.ResultB), nil
}
func (o *UseDemo) InitContract(stub shim.CMStubInterface) protogo.Response {
// 首先获取一下预言机合约的公钥签名,缓存一下,后续做callback的时候可以校验
oraclePk, oraclePkErr := o.getOralceContractPk(stub)
if oraclePkErr != nil {
return shim.Error(oraclePkErr.Error())
}
storeErr := stub.PutStateFromKey(gOracleContractPkStr, oraclePk)
if storeErr != nil {
stub.Log("InitContract store oracle_contract_pk error")
return shim.Error(storeErr.Error())
}
version := string(stub.GetArgs()["version"])
if strings.TrimSpace(version) == "" {
version = "1"
}
err := stub.PutStateFromKey("version", version)
if err != nil {
stub.Log("InitContract fail to save version")
return shim.Error("fail to save version")
}
stub.Log(fmt.Sprintf("InitContract version(%s) ", version))
return shim.Success([]byte("Init Success"))
}
16.2.2. 取VRF功能开发
调用参数
parameters := make(map[string][]byte)
parameters["alpha"] = []byte(alpha) //用户输入信息,来计算随机数使用
parameters["method"] = []byte("get_vrf") //调用预言机的vrf功能
parameters["original_contract_name"] = []byte("use_demo") //用户自己合约的名称
parameters["original_contract_version"] = []byte(version) //用户自己合约的版本
返回参数:
type OracleQueryResult struct {
ResultB []byte `json:"result"` //查询结果的json序列化
Code string `json:"code"` //错误码
Message string `json:"message"` //错误信息
ResponseHash string `json:"response_hash"` //返回唯一hash值
}
取数据回调:
args["result"] //调用结果数据
args["response_hash"] //调用的唯一索引值,与上文的返回参数唯一索引值一样
args["code"] //错误码
args["message"] //错误信息
其中result中即为所取到的VRF信息,其为如下结构体的json序类化字符串
type Random struct {
RandData string `json:"rand_data"` //32 byte的bigint
Pi []byte `json:"pi"` //证据
Ratio float64 `json:"ratio"` //[0,1]之间的随机数
}
16.2.3. 取接口数据功能开发(xpath语法)
调用参数
type ModelHttp struct {
Method string `json:"method"` //GET
URL string `json:"url"`
HttpHeader map[string]string `json:"http_header"`
HttpBody []byte `json:"http_body"`
ConnectionTimeout int `json:"connection_timeout"` //最大连接超时时长,默认30-60s,单位秒级别
MaxFetchTimeout int `json:"max_fetch_timeout"` //最大读取超时时长,默认10s,可以选择为10-60s
FetchDataType string `json:"fetch_data_type"` //json,xml,html
FetchDataFormula string `json:"fetch_data_formula"` //默认为空,为空则不解析,原样返回。如果非空,则按照xpath来分隔取出来结构化的数据
}
parameters["http_query"] = httpBs // ModelHttp 序列化后的字符串
parameters["method"] = []byte("queryHttp") //调用预言机的接口数据功能
parameters["original_contract_name"] = []byte("use_demo") //用户自己合约的名称
parameters["original_contract_version"] = []byte(version) //用户自己合约的版本
parameters["use_chaindata"] = []byte(useCache) //如果链上已经有相同的查询参数做的查询,是否使用链上已有数据返回
parameters["is_persistence"] = []byte(isPersist) //查询结果是否保存到链上,便于后面可以根据查询参数做查询
16.2.4. 取mysql数据功能开发
调用参数:
type ModelSql struct {
SqlType string `json:"sql_type"` //select
SqlSentence string `json:"sql_sentence"`// sql语句
DataSource string `json:"data_source"` //数据源
}
parameters["mysql_query"] = sqlBs // ModelSql 序列化后的字符串
parameters["method"] = []byte("queryMysql") //调用预言机的查询mysql数据功能
parameters["original_contract_name"] = []byte("use_demo") //用户自己合约的名称
parameters["original_contract_version"] = []byte(version) //用户自己合约的版本
parameters["use_chaindata"] = []byte(useCache) //如果链上已经有相同的查询参数做的查询,是否使用链上已有数据返回
parameters["is_persistence"] = []byte(isPersist) //查询结果是否保存到链上,便于后面可以根据查询参数做查询
16.2.5. 根据hash来查询数据功能开发(这个是个同步方法,可以直接调用预言机合约直接返回)
调用参数:
parameters := make(map[string][]byte)
parameters["method"] = []byte("queryResultByHash")
parameters["original_contract_name"] = []byte("use_demo")
parameters["original_contract_version"] = []byte(version)
parameters["query_hash"] = []byte(qHash) //hash值
16.2.6. 跨链功能的开发
调用参数:
type CrossChainQuery struct {
ChainAlias string `json:"chain_alias"` //调用的链id
ContractName string `json:"contract_name"` //链上合约名称
MethodName string `json:"method_name"` //合约的方法
Params []KeyValuePair `json:"params"` //合约所需要参数
}
parameters := make(map[string][]byte)
parameters["method"] = []byte("queryCrossChain")
parameters["original_contract_name"] = []byte("use_demo")
parameters["original_contract_version"] = []byte(version)
parameters["cross_query"] = queryMBS //queryMBS为json序列化后的结果
parameters["use_chaindata"] = []byte(useCache) //如果链上已经有相同的查询参数做的查询,是否使用链上已有数据返回
parameters["is_persistence"] = []byte(isPersist) //查询结果是否保存到链上,便于后面可以根据查询参数做查询
16.2.7. 根据指定topic查询数据的开发
调用参数:
parameters := make(map[string][]byte)
parameters["method"] = []byte("queryDataByTopic")
parameters["original_contract_name"] = []byte("use_demo")
parameters["original_contract_version"] = []byte(version)
parameters["topic"] = topic
parameters["use_chaindata"] = []byte(useCache) //如果链上已经有相同的查询参数做的查询,是否使用链上已有数据返回
parameters["is_persistence"] = []byte(isPersist) //查询结果是否保存到链上,便于后面可以根据查询参数做查询
16.3. 一个完整的demo合约代码参考
/*
Copyright (C) BABEC. All rights reserved.
SPDX-License-Identifier: Apache-2.0
*/
package main
import (
"encoding/json"
"errors"
"fmt"
"strings"
"chainmaker.org/chainmaker/chainmaker-contract-sdk-docker-go/pb/protogo"
"chainmaker.org/chainmaker/chainmaker-contract-sdk-docker-go/shim"
)
const (
gOracleContractPkStr = "oracle_contract_pk"
)
type Random struct {
RandData string `json:"rand_data"`
Pi []byte `json:"pi"`
Ratio float64 `json:"ratio"`
}
type ModelHttp struct {
Method string `json:"method"` //GET,POST
URL string `json:"url"`
HttpHeader map[string]string `json:"http_header"`
HttpBody []byte `json:"http_body"`
ConnectionTimeout int `json:"connection_timeout"` //最大连接超时时长,默认30-60s,单位秒级别
MaxFetchTimeout int `json:"max_fetch_timeout"` //最大读取超时时长,默认10s,可以选择为10-60s
FetchDataType string `json:"fetch_data_type"` //json,xml,html
FetchDataFormula string `json:"fetch_data_formula"` //默认为空,为空则不解析,原样返回。如果非空,则按照. 来分隔取出来结构化的数据,https://goessner.net/articles/JsonPath/(json)
}
type ModelSql struct {
SqlType string `json:"sql_type"` //select ,update , insert ,delete
SqlSentence string `json:"sql_sentence"`
DataSource string `json:"data_source"` //数据源
}
type CrossChainQuery struct {
ChainAlias string `json:"chain_alias"` //调用的链id
ContractName string `json:"contract_name"` //链上合约名称
MethodName string `json:"method_name"` //合约的方法
Params []KeyValuePair `json:"params"` //合约所需要参数
}
type KeyValuePair struct {
Key string `json:"key"`
Value []byte `json:"value"`
}
type UseDemo struct {
}
type OracleQueryResult struct {
ResultB []byte `json:"result"` //查询结果的json序列化
Code string `json:"code"` //错误码
Message string `json:"message"` //错误信息
ResponseHash string `json:"response_hash"` //返回唯一hash值
}
func (o *UseDemo) Name(stub shim.CMStubInterface) protogo.Response {
return shim.Success([]byte("use_demo"))
}
func (o *UseDemo) Version(stub shim.CMStubInterface) protogo.Response {
version, versionErr := stub.GetStateFromKey("version")
if versionErr != nil {
stub.Log("Version failed")
return shim.Error(versionErr.Error())
}
return shim.Success([]byte(version))
}
func (o *UseDemo) getOralceContractPk(stub shim.CMStubInterface) (string, error) {
parameters := make(map[string][]byte)
parameters["method"] = []byte("queryOracleContractPK")
resp := stub.CallContract("oracle_contract_v1", "1", parameters)
if resp.Status != 200 {
stub.Log("getOralceContractPk error," + resp.String())
return "", errors.New(resp.String())
}
var responeT OracleQueryResult
uerr := json.Unmarshal(resp.Payload, &responeT)
if uerr != nil {
stub.Log("getOralceContractPk unmarshal error")
return "", errors.New("getOralceContractPk unmarshal error" + uerr.Error())
}
return string(responeT.ResultB), nil
}
func (o *UseDemo) InitContract(stub shim.CMStubInterface) protogo.Response {
// 首先获取一下预言机合约的公钥签名,缓存一下,后续做callback的时候可以校验
oraclePk, oraclePkErr := o.getOralceContractPk(stub)
if oraclePkErr != nil {
return shim.Error(oraclePkErr.Error())
}
storeErr := stub.PutStateFromKey(gOracleContractPkStr, oraclePk)
if storeErr != nil {
stub.Log("InitContract store oracle_contract_pk error")
return shim.Error(storeErr.Error())
}
version := string(stub.GetArgs()["version"])
if strings.TrimSpace(version) == "" {
version = "1"
}
err := stub.PutStateFromKey("version", version)
if err != nil {
stub.Log("InitContract fail to save version")
return shim.Error("fail to save version")
}
stub.Log(fmt.Sprintf("InitContract version(%s) ", version))
return shim.Success([]byte("Init Success"))
}
func (o *UseDemo) InvokeContract(stub shim.CMStubInterface) protogo.Response {
method := string(stub.GetArgs()["method"])
stub.Log("custom " + method)
switch method {
case "query_http":
return o.QueryHttp(stub)
case "query_mysql":
return o.QueryMysql(stub)
case "get_vrf":
return o.GetVRF(stub)
case "oracle_callback":
return o.OracleCallBack(stub)
case "name":
return o.Name(stub)
case "version":
return o.Version(stub)
case "query_hash":
return o.QueryUseHash(stub)
case "query_cross":
return o.QueryCross(stub)
// case "register_topic":
// return o.RegisterTopic(stub)
case "query_topic":
return o.QueryTopic(stub)
default:
stub.Log("InvokeContract invalid method, method is " + method)
return shim.Error("invalid method")
}
}
func (o *UseDemo) QueryTopic(stub shim.CMStubInterface) protogo.Response {
version, versionErr := stub.GetStateFromKey("version")
if versionErr != nil {
stub.Log("Version failed")
return shim.Error(versionErr.Error())
}
args := stub.GetArgs()
topic := args["topic"]
useCache := string(args["use_chaindata"])
isPersist := string(args["is_persist"])
parameters := make(map[string][]byte)
parameters["method"] = []byte("queryDataByTopic")
parameters["original_contract_name"] = []byte("use_demo")
parameters["original_contract_version"] = []byte(version)
parameters["use_chaindata"] = []byte(useCache)
parameters["is_persistence"] = []byte(isPersist)
parameters["topic"] = topic
resp := stub.CallContract("oracle_contract_v1", "1", parameters)
if resp.Status != 200 {
stub.Log("QueryTopic error," + resp.String())
return shim.Error(resp.String())
}
stub.Log("QueryTopic response," + resp.String())
var responeT OracleQueryResult
uerr := json.Unmarshal(resp.Payload, &responeT)
if uerr != nil {
stub.Log("QueryTopic unmarshal error")
}
if responeT.Code == "201" {
//同步调用
stub.Log("QueryTopic sync," + string(responeT.ResultB))
return shim.Success(responeT.ResultB)
}
stub.Log("QueryTopic " + string(responeT.ResponseHash))
stub.PutStateFromKey(string(responeT.ResponseHash), string(topic))
//stub.PutStateByte(string(responeT.ResponseHash), "parameter", sqlBs) //存根一下
return shim.Success([]byte("QueryTopic ok"))
}
func (o *UseDemo) GetVRF(stub shim.CMStubInterface) protogo.Response {
version, versionErr := stub.GetStateFromKey("version")
if versionErr != nil {
stub.Log("Version failed")
return shim.Error(versionErr.Error())
}
args := stub.GetArgs()
alpha := string(args["alpha"])
parameters := make(map[string][]byte)
parameters["alpha"] = []byte(alpha)
parameters["method"] = []byte("getVrf")
parameters["original_contract_name"] = []byte("use_demo")
parameters["original_contract_version"] = []byte(version)
//stub.EmitEvent("http_query", []string{url, "aac"})
resp := stub.CallContract("oracle_contract_v1", "1", parameters)
if resp.Status != 200 {
stub.Log("GetVRF error," + resp.String())
return shim.Error(resp.String())
}
stub.Log("GetVRF resp" + resp.String())
var responeT OracleQueryResult
uerr := json.Unmarshal(resp.Payload, &responeT)
if uerr != nil {
stub.Log("GetVRF unmarshal error")
}
stub.Log("get_vrf_responset " + string(responeT.ResponseHash))
stub.PutStateFromKey(string(responeT.ResponseHash), string(alpha))
//stub.PutStateByte(string(responeT.ResponseHash), "parameter", []byte(alpha)) //存根一下
return shim.Success([]byte(resp.String()))
}
func (o *UseDemo) QueryHttp(stub shim.CMStubInterface) protogo.Response {
version, versionErr := stub.GetStateFromKey("version")
if versionErr != nil {
stub.Log("Version failed")
return shim.Error(versionErr.Error())
}
args := stub.GetArgs()
useCache := string(args["use_chaindata"])
isPersist := string(args["is_persist"])
url := string(args["url"])
fetchType := string(args["fetch_data_type"])
fetchFormular := string(args["fetch_data_formular"])
parameters := make(map[string][]byte)
httpQuerys := ModelHttp{
Method: "GET",
URL: url, //"https://www.baidu.com/baidu?wd=chainmaker",,
ConnectionTimeout: 30,
MaxFetchTimeout: 30,
FetchDataType: fetchType,
FetchDataFormula: fetchFormular,
}
if fetchType == "html" {
httpQuerys.HttpHeader = map[string]string{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3776.0 Safari/537.36",
}
}
httpBs, _ := json.Marshal(httpQuerys)
parameters["http_query"] = httpBs
parameters["method"] = []byte("queryHttp")
parameters["original_contract_name"] = []byte("use_demo")
parameters["original_contract_version"] = []byte(version)
parameters["use_chaindata"] = []byte(useCache)
parameters["is_persistence"] = []byte(isPersist)
resp := stub.CallContract("oracle_contract_v1", "1", parameters)
if resp.Status != 200 {
stub.Log("query_http error," + resp.String())
return shim.Error(resp.String())
}
stub.Log("query_http resp," + resp.String())
var responeT OracleQueryResult
uerr := json.Unmarshal(resp.Payload, &responeT)
if uerr != nil {
stub.Log("GetVRF unmarshal error")
}
if responeT.Code == "201" {
//同步调用结果
stub.Log("QueryHttp sync," + string(responeT.ResultB))
return shim.Success(responeT.ResultB)
}
stub.Log("query_http_responset " + string(responeT.ResponseHash))
stub.PutStateFromKey(string(responeT.ResponseHash), string(httpBs))
//stub.PutStateByte(string(responeT.ResponseHash), "parameter", httpBs) //存根一下
return shim.Success([]byte(resp.String()))
}
func (o *UseDemo) QueryCross(stub shim.CMStubInterface) protogo.Response {
//订制化查询hash函数
version, versionErr := stub.GetStateFromKey("version")
if versionErr != nil {
stub.Log("Version failed")
return shim.Error(versionErr.Error())
}
args := stub.GetArgs()
chainA := string(args["chain_alias"])
name := string(args["contract_name"])
mName := string(args["method_name"])
useCache := string(args["use_chaindata"])
isPersist := string(args["is_persist"])
qHash := string(args["query_hash"])
var kvp []KeyValuePair
kvp = append(kvp, KeyValuePair{
Key: "query_hash",
Value: []byte(qHash),
})
var queryM CrossChainQuery
queryM.ChainAlias = chainA
queryM.ContractName = name
queryM.MethodName = mName
queryM.Params = kvp
queryMBS, _ := json.Marshal(queryM)
parameters := make(map[string][]byte)
parameters["method"] = []byte("queryCrossChain")
parameters["original_contract_name"] = []byte("use_demo")
parameters["original_contract_version"] = []byte(version)
parameters["use_chaindata"] = []byte(useCache)
parameters["is_persistence"] = []byte(isPersist)
parameters["cross_query"] = queryMBS
resp := stub.CallContract("oracle_contract_v1", "1", parameters)
if resp.Status != 200 {
stub.Log("QueryCross error," + resp.String())
return shim.Error(resp.String())
}
stub.Log("QueryCross response," + resp.String())
var responeT OracleQueryResult
uerr := json.Unmarshal(resp.Payload, &responeT)
if uerr != nil {
stub.Log("QueryCross unmarshal error")
}
if responeT.Code == "201" {
stub.Log("QueryCross sync," + string(responeT.ResultB))
return shim.Success(responeT.ResultB)
}
stub.Log("QueryCross " + string(responeT.ResponseHash))
stub.PutStateFromKey(string(responeT.ResponseHash), string(queryMBS))
//stub.PutStateByte(string(responeT.ResponseHash), "parameter", sqlBs) //存根一下
return shim.Success([]byte("QueryCross ok"))
}
func (o *UseDemo) QueryUseHash(stub shim.CMStubInterface) protogo.Response {
args := stub.GetArgs()
qHash := string(args["query_hash"])
version, versionErr := stub.GetStateFromKey("version")
if versionErr != nil {
stub.Log("Version failed")
return shim.Error(versionErr.Error())
}
parameters := make(map[string][]byte)
parameters["method"] = []byte("queryResultByHash")
parameters["original_contract_name"] = []byte("use_demo")
parameters["original_contract_version"] = []byte(version)
parameters["query_hash"] = []byte(qHash)
resp := stub.CallContract("oracle_contract_v1", "1", parameters)
if resp.Status != 200 {
stub.Log("QueryUseHash error," + resp.String())
return shim.Error(resp.String())
}
var responeT OracleQueryResult
uerr := json.Unmarshal(resp.Payload, &responeT)
if uerr != nil {
stub.Log("QueryCross unmarshal error")
}
if responeT.Code == "201" && responeT.Message == "success" {
stub.Log("QueryUseHash sync," + string(responeT.ResultB))
return shim.Success(responeT.ResultB)
} else {
return shim.Success(resp.Payload)
}
}
func (o *UseDemo) QueryMysql(stub shim.CMStubInterface) protogo.Response {
version, versionErr := stub.GetStateFromKey("version")
if versionErr != nil {
stub.Log("Version failed")
return shim.Error(versionErr.Error())
}
args := stub.GetArgs()
sqlType := string(args["sql_type"])
sqlSentence := string(args["sql_sentence"])
dataSource := string(args["data_source"])
useCache := string(args["use_chaindata"])
isPersist := string(args["is_persist"])
parameters := make(map[string][]byte)
sqlQuerys := ModelSql{
SqlType: sqlType,
SqlSentence: sqlSentence,
DataSource: dataSource,
}
sqlBs, _ := json.Marshal(sqlQuerys)
parameters["mysql_query"] = sqlBs
parameters["method"] = []byte("queryMysql")
parameters["original_contract_name"] = []byte("use_demo")
parameters["original_contract_version"] = []byte(version)
parameters["use_chaindata"] = []byte(useCache)
parameters["is_persistence"] = []byte(isPersist)
resp := stub.CallContract("oracle_contract_v1", "1", parameters)
if resp.Status != 200 {
stub.Log("QueryMysql error," + resp.String())
return shim.Error(resp.String())
}
stub.Log("QueryMysql response," + resp.String())
var responeT OracleQueryResult
uerr := json.Unmarshal(resp.Payload, &responeT)
if uerr != nil {
stub.Log("QueryMysql unmarshal error")
}
if responeT.Code == "201" { //同步返回结果
stub.Log("QueryMysql sync," + string(responeT.ResultB))
return shim.Success(responeT.ResultB)
}
stub.Log("query_mysql_responset " + string(responeT.ResponseHash))
stub.PutStateFromKey(string(responeT.ResponseHash), string(sqlBs))
//stub.PutStateByte(string(responeT.ResponseHash), "parameter", sqlBs) //存根一下
return shim.Success(resp.Payload)
}
func (o *UseDemo) OracleCallBack(stub shim.CMStubInterface) protogo.Response {
//首先需要做一下校验,校验一下调用这个函数的交易发起者pk是否是预言机合约;不是的话,不允许调用
senderPk, senderPkErr := stub.GetSenderPk()
if senderPkErr != nil {
stub.Log("OracleCallBack getSenderPk error , " + senderPkErr.Error())
return shim.Error(senderPkErr.Error())
}
storePk, storePkErr := stub.GetStateFromKey(gOracleContractPkStr)
if storePkErr != nil {
stub.Log("OracleCallBack getStateFromKey error , " + storePkErr.Error())
return shim.Error(storePkErr.Error())
}
if storePk != senderPk {
return shim.Error("OracleCallBack failed , tx senderpk not match")
}
//
args := stub.GetArgs()
results := string(args["result"])
originalMethod := string(args["original_method"])
responseHash := args["response_hash"]
code := string(args["code"])
message := string(args["message"])
value, valueErr := stub.GetStateFromKey(string(responseHash))
if valueErr != nil {
stub.Log("callback got key error ")
}
//parameters, _ := stub.GetStateByte(string(responseHash), "parameter") //寻找对应关系
stub.Log(fmt.Sprintf("originalMethod(%s) , callback result (%s) ,code(%s), message(%s), parameter(%s) , responseHash(%s) \n", originalMethod, results,
code, message, value, string(responseHash)))
if originalMethod == "get_vrf" {
var vrfR Random
json.Unmarshal(args["result"], &vrfR)
parameters := make(map[string][]byte)
parameters["pi"] = vrfR.Pi
parameters["alpha"] = []byte(value)
parameters["method"] = []byte("verifyVrf")
verifyResp := stub.CallContract("oracle_contract_v1", "1", parameters)
if verifyResp.Status != 200 {
stub.Log("verify vrf error, " + verifyResp.String())
return shim.Error("verify vrf error")
}
var verifyT OracleQueryResult
json.Unmarshal(verifyResp.Payload, &verifyT)
if string(verifyT.ResultB) != "true" {
stub.Log("verify vrf false")
return shim.Error("verify vrf false")
}
}
return shim.Success([]byte("callback done"))
}
func main() {
err := shim.Start(new(UseDemo))
if err != nil {
panic(err)
}
}
16.4. 通过接口对预言机进行管理和简化开发
16.4.1. 安装预言机合约接口
/v1/install_contract
方法: POST
格式: Form表单
header: admin_auth_token
参数:
1. chain_alias 链id
2. contract_name 合约名称
3. contract_version 合约版本(整形数字,从1开始)
4. contract_file 合约文件(7z文件)
5. runtime 合约格式(暂时只支持DOCKER_GO)
返回参数:
格式: json
code: 错误码(0 为成功)
message: 错误信息(默认为success)
data: 返回合约信息
16.4.2. 查询已安装合约接口
/v1/list_contracts
方法: GET
格式: query参数
header: assist_auth_token
参数:
1. chain_alias 链id
返回参数:
格式: json
code: 错误码(0 为成功)
message: 错误信息(默认为success)
data: 返回合约信息列表
16.4.3. 升级合约接口
/v1/upgrade_contract
方法: POST
header: admin_auth_token
格式: Form表单
参数:
1. chain_alias 链id
2. contract_name 合约名称
3. contract_version 合约版本(整形数字,从1开始)
4. contract_file 合约文件(7z文件)
5. runtime 合约格式(暂时只支持DOCKER_GO)
返回参数:
格式: json
code: 错误码(0 为成功)
message: 错误信息(默认为success)
data: 返回合约信息
16.4.4. 生成VRF接口
/v1/vrf/get_random
方法:POST
header: assist_auth_token
格式:Form表单
参数:
1. alpha 用户输入随机数字
返回参数:
格式: json
code: 错误码(0 为成功)
message: 错误信息(默认为success)
data: 返回VRF信息
16.4.5. 验证VRF接口
/v1/vrf/verify
方法:POST
header: assist_auth_token
格式:json
参数:
1. alpha 用户输入随机数字
2. pi 证据
返回参数:
格式: json
code: 错误码(0 为成功)
message: 错误信息(默认为success)
data: 是否验证通过(true/false)
16.4.6. 查询预言机公钥接口
/v1/vrf/get_public_key
方法:GET
header: assist_auth_token
返回参数:
格式: json
code: 错误码(0 为成功)
message: 错误信息(默认为success)
data: 返回公钥
16.4.7. 服务是否可触达接口
/v1/health
方法: GET
格式: query参数
header: assist_auth_token
返回参数:
格式: json
code: 错误码(0 为成功)
message: 错误信息(默认为success)
16.4.8. 查询mysql数据接口
/v1/query/query_mysql
方法: POST
格式: Json
header: assist_auth_token
参数:
type ModelSql struct {
SqlType string `json:"sql_type"` //select
SqlSentence string `json:"sql_sentence"`
DataSource string `json:"data_source"` //数据源
}的序列化json数据
返回参数:
格式: json
code: 错误码(0 为成功)
message: 错误信息(默认为success)
data: 返回合约信息
16.4.9. 查询http接口数据接口
/v1/query/query_http
方法: POST
格式: Json
header: assist_auth_token
参数:
type ModelHttp struct {
URL string `json:"url"`
HttpHeader map[string]string `json:"http_header"`
HttpBody []byte `json:"http_body"`
ConnectionTimeout int `json:"connection_timeout"` //最大连接超时时长,默认30s,单位秒级别
MaxRetry int `json:"max_retry"` //最大连接试错次数,默认3次
MaxFetchTimeout int `json:"max_fetch_timeout"` //最大读取超时时长,默认10s,可以选择为10-60s
FetchDataType string `json:"fetch_data_type"` //json,xml,html
FetchDataFormula string `json:"fetch_data_formula"` //默认为空,为空则不解析,原样返回。如果非空,则按照. 来分隔取出来结构化的数据
}的序列化json数据
返回参数:
格式: json
code: 错误码(0 为成功)
message: 错误信息(默认为success)
data: 返回合约信息
16.4.10. 跨链查询数据接口
/v1/query/query_cross
方法: POST
格式: Json
header: assist_auth_token
参数:
# CrossChainQuery 合约跨链查询模型
type CrossChainQuery struct {
ChainAlias string `json:"chain_alias"` //调用的链id
ContractName string `json:"contract_name"`
MethodName string `json:"method_name"`
Params []KeyValuePair `json:"params"`
}
# KeyValuePair 合约传参数模型
type KeyValuePair struct {
Key string `json:"key"`
Value []byte `json:"value"`
}的序列化json数据
返回参数:
格式: json
code: 错误码(0 为成功)
message: 错误信息(默认为success)
data: 返回合约信息