# 预言机工具 ## 安装及配置 ### 服务依赖 #### 操作系统 预言机合约运行在Docker VM中,当前仅支持部署在Linux系统 #### docker方式 - git - docker 18+ - docker-compose 1.29.2 #### 源码方式 - Mysql 5.7+ - golang 1.16+ ### 源码下载 - 下载代码 ```bash $ git clone -b v1.0.0_beta --depth=1 https://git.chainmaker.org.cn/chainmaker/chainmaker-oracle ``` ### 服务配置 #### 整体配置目录结构如下: ```bash ├── config_files # 配置文件 │ ├── crypto-config # 使用chainmaker-cryptogen 工具生成的证书文件 │ ├── sdk-config # 链sdk配置 │ │ ├── sdk_config_chain1.yml # 链1的sdk │ │ └── sdk_config_chain2.yml # 链2的sdk │ └── smart_oracle.yml # 预言机服务的配置文件 ``` #### 预言机配置结构(smart_oracle.yml): 用户需要关注如下配置,详情参考完整的配置文件 ```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 ``` ### 快速启动一条docker-vm引擎的链(已有链则跳过) ```sh $ git clone -b v2.3.1 --depth=1 https://git.chainmaker.org.cn/chainmaker/chainmaker-go.git $ git clone -b v2.2.0 --depth=1 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 ``` ### 运行服务方式一:使用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即可启动服务 ```bash # 设置证书 $ 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 ``` - 停止服务 ```bash $ docker-compse down # 删除mysql中的数据 $ rm -rf ./mysql-data-volumes/data/ ``` ### 运行服务方式二:使用源码启动 - 编译: ```bash $ cd chainmaker-oracle && go build -o chainmaker-oracle ``` - 初始化数据库: 数据库上执行代码的scripts/init.sql脚本文件 ```bash $ 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 - 启动服务 ```bash # 直接启动 $ ./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} ``` ### 预言机合约安装 - 预言机合约的安装是通过http接口的形式给出,可以直接调用接口 ```bash # 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安装脚本,可以按照命令提示也可以安装 ```bash $ ./install-upgrade-oracle-contract.sh ``` ### 使用用户合约取数据(以cmc工具为例) #### 编译CMC ```bash $ git clone --depth=1 hps://git.chainmaker.org.cn/chainmaker/chainmaker-go.git $ cd chainmaker-go/ $ make cmc $ cp bin/cmc ../chainmaker-oracle/ && cd ../chainmaker-oracle/ ``` #### 使用cmc工具安装示例用户合约文件 ```bash # 链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 ``` #### 使用管理员身份(也即为安装预言机合约相同的签名)注册查询topic,使用普通用户身份根据topic查询 ```bash # 注册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 ``` #### 取json类型数据 ```bash ./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: ]/[txId:16fc7e533eabd3d7ca52fdfc0721826594cce7e90d084fc890053e15aa8b479f] #### 取html数据 ```bash ./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: ]/[txId:16fc7e5c927ec90fca52fdfc07218265daa02eb765794c77bb2deceebd4a4b05] #### 取xml数据 ```bash ./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: ]/[txId:16fc7e62554560a5ca52fdfc072182656b74385f041e4da8aadf68e78727e06a] #### 取VRF数据 ```bash ./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: ]/[txId:16fc7e662a606536ca52fdfc07218265bf42b16723af4d7db03dec55a11dea46] #### 取mysql数据 ```bash ./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: ]/[txId:16fc7e6902f78cafca52fdfc07218265dc4d346d146b4b08a69f088f842d5585] #### 使用hash查询数据(注意hash为预言机合约返回给用户合约的hash值) 从上方结果中任意取一个`response_hash`填充到`query_hash`中。 ```bash ./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] #### 跨链查询数据(注意hash为预言机合约返回给用户合约的hash值) 示例中从上方结果中任意取一个`response_hash`填充到`query_hash`中(此为用户合约行为,可自定义)。 ```bash ./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 ``` ```bash # 注意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: ]/[txId:16fc7e8add879fd8ca52fdfc0721826578760f8515c049599c29e3cadc3a382e] ### 代码整体目录结构 ```bash . ├── alarms # 告警模块的代码包 ├── config # 读取配置模块的代码包 ├── config_files # 配置文件 │ ├── crypto-config # 使用chainmaker-cryptogen 工具生成的证书文件 │ ├── sdk-config # 链sdk配置 │ │ ├── sdk_config_chain1.yml # 链1的sdk │ │ └── sdk_config_chain2.yml # 链2的sdk │ └── smart_oracle.yml # 预言机服务的配置文件 ├── contract_process # 预言机服务核心业务逻辑的代码包 ├── custom.cnf # 可以参考这个配置来配置docker中的mysql数据库(5.7 及以后) ├── databases # 连接数据库模块的代码包 ├── docker-compose.yml # docker-compose 模板文件 ├── Dockerfile # docker 镜像文件 ├── fetch_data # 取数据模块代码包 ├── http_server_process # 预言机http服务接口代码包 ├── install-upgrade-oracle-contract.sh #预言机合约安装、升级脚本 ├── logger # 日志模块代码包 ├── main.go # 服务入口 ├── Makefile ├── models # 预言机数据模型包 ├── mysql-data-volumes # 默认的docker-compose启动mysql数据库的数据挂载点 │ └── data ├── readme.md ├── scripts # mysql建库脚本 │ └── init.sql ├── standard_oracle_contract # 系统自带的合约文件及代码 │ ├── oracle_contract_file │ │ └── oracle_contract_v1 # 编译过的预言机合约可执行文件 │ ├── oracle_contract_src.zip # 预言机合约源代码 │ └── use_demo_src.zip # 用户使用预言机合约的示例源代码 │ └── use_demo.7z # 编译压缩后的示例智能合约文件 └── ut_cover.sh # 单元测试脚本 ``` ## 使用及示例 ### 通过开发智能合约来操作预言机取数据 预言机通过在链上部署`预言机智能合约oracle_contract`,来给用户提供取数据功能。用户可以通过调用`oracle_contract`相应接口即可。由于取数据是一个不确定运行时长的过程,因此我们采用的是调用->事件->回调的方式。用户合约告知预言机需要取的数据类型,预言机取到数据通过用户合约注册的回调函数来告知用户合约 #### 用户合约的安装,安装时候需要查询一下预言机合约的公钥签名,以便后续校验数据 ```go func (o *UseDemo) getOralceContractPk() (string, error) { parameters := make(map[string][]byte) parameters["method"] = []byte("queryOracleContractPK") resp := sdk.Instance.CallContract("oracle_contract_v1", "queryOracleContractPK", parameters) if resp.Status != 0 { sdk.Instance.Errorf("getOralceContractPk error," + resp.String()) return "", errors.New(resp.String()) } var responeT OracleQueryResult uerr := json.Unmarshal(resp.Payload, &responeT) if uerr != nil { sdk.Instance.Errorf("getOralceContractPk unmarshal error") return "", errors.New("getOralceContractPk unmarshal error" + uerr.Error()) } return string(responeT.ResultB), nil } func (o *UseDemo) InitContract() protogo.Response { // 首先获取一下预言机合约的公钥签名,缓存一下,后续做callback的时候可以校验 oraclePk, oraclePkErr := o.getOralceContractPk() if oraclePkErr != nil { return sdk.Error(oraclePkErr.Error()) } storeErr := sdk.Instance.PutStateFromKey(gOracleContractPkStr, oraclePk) if storeErr != nil { sdk.Instance.Errorf("InitContract store oracle_contract_pk error") return sdk.Error(storeErr.Error()) } version := string(sdk.Instance.GetArgs()["version"]) if strings.TrimSpace(version) == "" { version = "1" } err := sdk.Instance.PutStateFromKey("version", version) if err != nil { sdk.Instance.Errorf("InitContract fail to save version") return sdk.Error("fail to save version") } sdk.Instance.Infof("InitContract version(%s) ", version) return sdk.Success([]byte("Init Success")) } func (o *UseDemo) UpgradeContract() protogo.Response { // 首先获取一下预言机合约的公钥签名,缓存一下,后续做callback的时候可以校验 oraclePk, oraclePkErr := o.getOralceContractPk() if oraclePkErr != nil { return sdk.Error(oraclePkErr.Error()) } storeErr := sdk.Instance.PutStateFromKey(gOracleContractPkStr, oraclePk) if storeErr != nil { sdk.Instance.Errorf("UpgradeContract store oracle_contract_pk error") return sdk.Error(storeErr.Error()) } version := string(sdk.Instance.GetArgs()["version"]) if strings.TrimSpace(version) == "" { version = "1" } err := sdk.Instance.PutStateFromKey("version", version) if err != nil { sdk.Instance.Errorf("UpgradeContract fail to save version") return sdk.Error("fail to save version") } sdk.Instance.Infof("UpgradeContract version(%s) ", version) return sdk.Success([]byte("UpgradeContract Success")) } ``` #### 取VRF功能开发 调用参数 ```go 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) //用户自己合约的版本 ``` 返回参数: ```go type OracleQueryResult struct { ResultB []byte `json:"result"` //查询结果的json序列化 Code string `json:"code"` //错误码 Message string `json:"message"` //错误信息 ResponseHash string `json:"response_hash"` //返回唯一hash值 } ``` 取数据回调: ```go args["result"] //调用结果数据 args["response_hash"] //调用的唯一索引值,与上文的返回参数唯一索引值一样 args["code"] //错误码 args["message"] //错误信息 ``` 其中result中即为所取到的VRF信息,其为如下结构体的json序类化字符串 ```go type Random struct { RandData string `json:"rand_data"` //32 byte的bigint Pi []byte `json:"pi"` //证据 Ratio float64 `json:"ratio"` //[0,1]之间的随机数 } ``` #### 取接口数据功能开发(xpath语法) 调用参数 ```go 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) //查询结果是否保存到链上,便于后面可以根据查询参数做查询 ``` #### 取mysql数据功能开发 调用参数: ```go 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) //查询结果是否保存到链上,便于后面可以根据查询参数做查询 ``` #### 根据hash来查询数据功能开发(这个是个同步方法,可以直接调用预言机合约直接返回) 调用参数: ```go 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值 ``` #### 跨链功能的开发 调用参数: ```go 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) //查询结果是否保存到链上,便于后面可以根据查询参数做查询 ``` #### 根据指定topic查询数据的开发 调用参数: ```go 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) //查询结果是否保存到链上,便于后面可以根据查询参数做查询 ``` ### 一个完整的demo合约代码参考 ```go /* Copyright (C) BABEC. All rights reserved. SPDX-License-Identifier: Apache-2.0 */ package main import ( "encoding/json" "errors" "strings" "chainmaker.org/chainmaker/contract-sdk-go/v2/pb/protogo" "chainmaker.org/chainmaker/contract-sdk-go/v2/sandbox" "chainmaker.org/chainmaker/contract-sdk-go/v2/sdk" ) 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() protogo.Response { return sdk.Success([]byte("use_demo")) } func (o *UseDemo) Version() protogo.Response { version, versionErr := sdk.Instance.GetStateFromKey("version") if versionErr != nil { sdk.Instance.Errorf("Version failed") return sdk.Error(versionErr.Error()) } return sdk.Success([]byte(version)) } func (o *UseDemo) getOralceContractPk() (string, error) { parameters := make(map[string][]byte) parameters["method"] = []byte("queryOracleContractPK") resp := sdk.Instance.CallContract("oracle_contract_v1", "queryOracleContractPK", parameters) if resp.Status != 0 { sdk.Instance.Errorf("getOralceContractPk error," + resp.String()) return "", errors.New(resp.String()) } var responeT OracleQueryResult uerr := json.Unmarshal(resp.Payload, &responeT) if uerr != nil { sdk.Instance.Errorf("getOralceContractPk unmarshal error") return "", errors.New("getOralceContractPk unmarshal error" + uerr.Error()) } return string(responeT.ResultB), nil } func (o *UseDemo) InitContract() protogo.Response { // 首先获取一下预言机合约的公钥签名,缓存一下,后续做callback的时候可以校验 oraclePk, oraclePkErr := o.getOralceContractPk() if oraclePkErr != nil { return sdk.Error(oraclePkErr.Error()) } storeErr := sdk.Instance.PutStateFromKey(gOracleContractPkStr, oraclePk) if storeErr != nil { sdk.Instance.Errorf("InitContract store oracle_contract_pk error") return sdk.Error(storeErr.Error()) } version := string(sdk.Instance.GetArgs()["version"]) if strings.TrimSpace(version) == "" { version = "1" } err := sdk.Instance.PutStateFromKey("version", version) if err != nil { sdk.Instance.Errorf("InitContract fail to save version") return sdk.Error("fail to save version") } sdk.Instance.Infof("InitContract version(%s) ", version) return sdk.Success([]byte("Init Success")) } func (o *UseDemo) UpgradeContract() protogo.Response { // 首先获取一下预言机合约的公钥签名,缓存一下,后续做callback的时候可以校验 oraclePk, oraclePkErr := o.getOralceContractPk() if oraclePkErr != nil { return sdk.Error(oraclePkErr.Error()) } storeErr := sdk.Instance.PutStateFromKey(gOracleContractPkStr, oraclePk) if storeErr != nil { sdk.Instance.Errorf("UpgradeContract store oracle_contract_pk error") return sdk.Error(storeErr.Error()) } version := string(sdk.Instance.GetArgs()["version"]) if strings.TrimSpace(version) == "" { version = "1" } err := sdk.Instance.PutStateFromKey("version", version) if err != nil { sdk.Instance.Errorf("UpgradeContract fail to save version") return sdk.Error("fail to save version") } sdk.Instance.Infof("UpgradeContract version(%s) ", version) return sdk.Success([]byte("UpgradeContract Success")) } func (o *UseDemo) InvokeContract(method string) protogo.Response { sdk.Instance.Infof("custom " + method) switch method { case "query_http": return o.QueryHttp() case "query_mysql": return o.QueryMysql() case "get_vrf": return o.GetVRF() case "oracle_callback": return o.OracleCallBack() case "name": return o.Name() case "version": return o.Version() case "query_hash": return o.QueryUseHash() case "query_cross": return o.QueryCross() // case "register_topic": // return o.RegisterTopic(stub) case "query_topic": return o.QueryTopic() default: sdk.Instance.Errorf("InvokeContract invalid method, method is " + method) return sdk.Error("invalid method") } } func (o *UseDemo) QueryTopic() protogo.Response { version, versionErr := sdk.Instance.GetStateFromKey("version") if versionErr != nil { sdk.Instance.Errorf("Version failed") return sdk.Error(versionErr.Error()) } args := sdk.Instance.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 := sdk.Instance.CallContract("oracle_contract_v1", "queryDataByTopic", parameters) if resp.Status != 0 { sdk.Instance.Errorf("QueryTopic error," + resp.String()) return sdk.Error(resp.String()) } sdk.Instance.Infof("QueryTopic response," + resp.String()) var responeT OracleQueryResult uerr := json.Unmarshal(resp.Payload, &responeT) if uerr != nil { sdk.Instance.Errorf("QueryTopic unmarshal error") } if responeT.Code == "201" { //同步调用 sdk.Instance.Errorf("QueryTopic sync," + string(responeT.ResultB)) return sdk.Success(responeT.ResultB) } sdk.Instance.Infof("QueryTopic " + string(responeT.ResponseHash)) sdk.Instance.PutStateFromKey(string(responeT.ResponseHash), string(topic)) //stub.PutStateByte(string(responeT.ResponseHash), "parameter", sqlBs) //存根一下 return sdk.Success([]byte("QueryTopic ok")) } func (o *UseDemo) GetVRF() protogo.Response { version, versionErr := sdk.Instance.GetStateFromKey("version") if versionErr != nil { sdk.Instance.Errorf("Version failed") return sdk.Error(versionErr.Error()) } args := sdk.Instance.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 := sdk.Instance.CallContract("oracle_contract_v1", "getVrf", parameters) if resp.Status != 0 { sdk.Instance.Errorf("GetVRF error," + resp.String()) return sdk.Error(resp.String()) } sdk.Instance.Infof("GetVRF resp" + resp.String()) var responeT OracleQueryResult uerr := json.Unmarshal(resp.Payload, &responeT) if uerr != nil { sdk.Instance.Errorf("GetVRF unmarshal error") } sdk.Instance.Infof("get_vrf_responset " + string(responeT.ResponseHash)) sdk.Instance.PutStateFromKey(string(responeT.ResponseHash), string(alpha)) //stub.PutStateByte(string(responeT.ResponseHash), "parameter", []byte(alpha)) //存根一下 return sdk.Success([]byte(resp.String())) } func (o *UseDemo) QueryHttp() protogo.Response { version, versionErr := sdk.Instance.GetStateFromKey("version") if versionErr != nil { sdk.Instance.Errorf("Version failed") return sdk.Error(versionErr.Error()) } args := sdk.Instance.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 := sdk.Instance.CallContract("oracle_contract_v1", "queryHttp", parameters) if resp.Status != 0 { sdk.Instance.Errorf("query_http error," + resp.String()) return sdk.Error(resp.String()) } sdk.Instance.Infof("query_http resp," + resp.String()) var responeT OracleQueryResult uerr := json.Unmarshal(resp.Payload, &responeT) if uerr != nil { sdk.Instance.Errorf("GetVRF unmarshal error") } if responeT.Code == "201" { //同步调用结果 sdk.Instance.Errorf("QueryHttp sync," + string(responeT.ResultB)) return sdk.Success(responeT.ResultB) } sdk.Instance.Infof("query_http_responset " + string(responeT.ResponseHash)) sdk.Instance.PutStateFromKey(string(responeT.ResponseHash), string(httpBs)) //stub.PutStateByte(string(responeT.ResponseHash), "parameter", httpBs) //存根一下 return sdk.Success([]byte(resp.String())) } func (o *UseDemo) QueryCross() protogo.Response { //订制化查询hash函数 version, versionErr := sdk.Instance.GetStateFromKey("version") if versionErr != nil { sdk.Instance.Errorf("Version failed") return sdk.Error(versionErr.Error()) } args := sdk.Instance.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 := sdk.Instance.CallContract("oracle_contract_v1", "queryCrossChain", parameters) if resp.Status != 0 { sdk.Instance.Errorf("QueryCross error," + resp.String()) return sdk.Error(resp.String()) } sdk.Instance.Infof("QueryCross response," + resp.String()) var responeT OracleQueryResult uerr := json.Unmarshal(resp.Payload, &responeT) if uerr != nil { sdk.Instance.Errorf("QueryCross unmarshal error") } if responeT.Code == "201" { sdk.Instance.Errorf("QueryCross sync," + string(responeT.ResultB)) return sdk.Success(responeT.ResultB) } sdk.Instance.Infof("QueryCross " + string(responeT.ResponseHash)) sdk.Instance.PutStateFromKey(string(responeT.ResponseHash), string(queryMBS)) //stub.PutStateByte(string(responeT.ResponseHash), "parameter", sqlBs) //存根一下 return sdk.Success([]byte("QueryCross ok")) } func (o *UseDemo) QueryUseHash() protogo.Response { args := sdk.Instance.GetArgs() qHash := string(args["query_hash"]) version, versionErr := sdk.Instance.GetStateFromKey("version") if versionErr != nil { sdk.Instance.Errorf("Version failed") return sdk.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 := sdk.Instance.CallContract("oracle_contract_v1", "queryResultByHash", parameters) if resp.Status != 0 { sdk.Instance.Errorf("QueryUseHash error," + resp.String()) return sdk.Error(resp.String()) } var responeT OracleQueryResult uerr := json.Unmarshal(resp.Payload, &responeT) if uerr != nil { sdk.Instance.Errorf("QueryCross unmarshal error") } if responeT.Code == "201" && responeT.Message == "success" { sdk.Instance.Errorf("QueryUseHash sync," + string(responeT.ResultB)) return sdk.Success(responeT.ResultB) } else { return sdk.Success(resp.Payload) } } func (o *UseDemo) QueryMysql() protogo.Response { version, versionErr := sdk.Instance.GetStateFromKey("version") if versionErr != nil { sdk.Instance.Errorf("Version failed") return sdk.Error(versionErr.Error()) } args := sdk.Instance.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 := sdk.Instance.CallContract("oracle_contract_v1", "queryMysql", parameters) if resp.Status != 0 { sdk.Instance.Errorf("QueryMysql error," + resp.String()) return sdk.Error(resp.String()) } sdk.Instance.Infof("QueryMysql response," + resp.String()) var responeT OracleQueryResult uerr := json.Unmarshal(resp.Payload, &responeT) if uerr != nil { sdk.Instance.Errorf("QueryMysql unmarshal error") } if responeT.Code == "201" { //同步返回结果 sdk.Instance.Errorf("QueryMysql sync," + string(responeT.ResultB)) return sdk.Success(responeT.ResultB) } sdk.Instance.Infof("query_mysql_responset " + string(responeT.ResponseHash)) sdk.Instance.PutStateFromKey(string(responeT.ResponseHash), string(sqlBs)) //stub.PutStateByte(string(responeT.ResponseHash), "parameter", sqlBs) //存根一下 return sdk.Success(resp.Payload) } func (o *UseDemo) OracleCallBack() protogo.Response { //首先需要做一下校验,校验一下调用这个函数的交易发起者pk是否是预言机合约;不是的话,不允许调用 senderPk, senderPkErr := sdk.Instance.GetSenderPk() if senderPkErr != nil { sdk.Instance.Errorf("OracleCallBack getSenderPk error , " + senderPkErr.Error()) return sdk.Error(senderPkErr.Error()) } storePk, storePkErr := sdk.Instance.GetStateFromKey(gOracleContractPkStr) if storePkErr != nil { sdk.Instance.Errorf("OracleCallBack getStateFromKey error , " + storePkErr.Error()) return sdk.Error(storePkErr.Error()) } if storePk != senderPk { return sdk.Error("OracleCallBack failed , tx senderpk not match") } // args := sdk.Instance.GetArgs() results := string(args["result"]) originalMethod := string(args["original_method"]) responseHash := args["response_hash"] code := string(args["code"]) message := string(args["message"]) value, valueErr := sdk.Instance.GetStateFromKey(string(responseHash)) if valueErr != nil { sdk.Instance.Errorf("callback got key error ") } //parameters, _ := stub.GetStateByte(string(responseHash), "parameter") //寻找对应关系 sdk.Instance.Infof("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 := sdk.Instance.CallContract("oracle_contract_v1", "verifyVrf", parameters) if verifyResp.Status != 0 { sdk.Instance.Errorf("verify vrf error, " + verifyResp.String()) return sdk.Error("verify vrf error") } var verifyT OracleQueryResult json.Unmarshal(verifyResp.Payload, &verifyT) if string(verifyT.ResultB) != "true" { sdk.Instance.Errorf("verify vrf false") return sdk.Error("verify vrf false") } } return sdk.Success([]byte("callback done")) } func main() { err := sandbox.Start(new(UseDemo)) if err != nil { panic(err) } } ``` ### 通过接口对预言机进行管理和简化开发 #### 安装预言机合约接口 ```bash /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: 返回合约信息 ``` #### 查询已安装合约接口 ```bash /v1/list_contracts 方法: GET 格式: query参数 header: assist_auth_token 参数: 1. chain_alias 链id 返回参数: 格式: json code: 错误码(0 为成功) message: 错误信息(默认为success) data: 返回合约信息列表 ``` #### 升级合约接口 ```bash /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: 返回合约信息 ``` #### 生成VRF接口 ```bash /v1/vrf/get_random 方法:POST header: assist_auth_token 格式:Form表单 参数: 1. alpha 用户输入随机数字 返回参数: 格式: json code: 错误码(0 为成功) message: 错误信息(默认为success) data: 返回VRF信息 ``` #### 验证VRF接口 ```bash /v1/vrf/verify 方法:POST header: assist_auth_token 格式:json 参数: 1. alpha 用户输入随机数字 2. pi 证据 返回参数: 格式: json code: 错误码(0 为成功) message: 错误信息(默认为success) data: 是否验证通过(true/false) ``` #### 查询预言机公钥接口 ```bash /v1/vrf/get_public_key 方法:GET header: assist_auth_token 返回参数: 格式: json code: 错误码(0 为成功) message: 错误信息(默认为success) data: 返回公钥 ``` #### 服务是否可触达接口 ```bash /v1/health 方法: GET 格式: query参数 header: assist_auth_token 返回参数: 格式: json code: 错误码(0 为成功) message: 错误信息(默认为success) ``` #### 查询mysql数据接口 ```bash /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: 返回合约信息 ``` #### 查询http接口数据接口 ```bash /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: 返回合约信息 ``` #### 跨链查询数据接口 ```bash /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: 返回合约信息 ```