diff --git a/build.gradle b/build.gradle index c7974dee1..a6a87b28b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'com.github.sherter.google-java-format' version '0.8' - id 'org.ajoberstar.grgit' version '4.0.1' +// id 'org.ajoberstar.grgit' version '4.0.1' } // Apply the java-library plugin to add support for Java Library @@ -137,9 +137,9 @@ List logger = [ dependencies { constraints { - compile group: 'io.netty', name: 'netty-all', version: '4.1.77.Final' + compile group: 'io.netty', name: 'netty-all', version: '4.1.53.Final' compile 'io.netty:netty-codec-haproxy:4.1.89.Final' - compile group: 'org.fisco-bcos', name: 'tcnative', version: '2.0.51.0' + //compile group: 'org.fisco-bcos', name: 'tcnative', version: '2.0.51.0' } compile 'org.quartz-scheduler:quartz:2.3.2' @@ -158,10 +158,22 @@ dependencies { compile 'org.bouncycastle:bcprov-jdk15on:1.69' compile 'commons-codec:commons-codec:1.14' compile 'javax.activation:activation:1.1.1' - compile 'org.fisco-bcos:tcnative' + //compile 'org.fisco-bcos:tcnative' compile 'org.apache.httpcomponents:httpclient:4.5.13' compile 'io.netty:netty-all' + compile 'org.springframework:spring-messaging:5.3.31' + compile ('com.rabbitmq:amqp-client:5.18.0') { + exclude group: "io.netty" + } + compile ('org.apache.kafka:kafka-clients:3.7.1') { + exclude group: "io.netty" + } + compile ('org.apache.rocketmq:rocketmq-client-java:5.0.5') { + exclude group: "io.netty" + } + compile 'com.alibaba.nacos:nacos-client:2.2.1' + // Use JUnit test framework testImplementation 'junit:junit:4.13.2' testImplementation 'org.springframework.boot:spring-boot-starter-test:2.7.9' @@ -169,9 +181,9 @@ dependencies { sourceSets { main { - resources { - exclude '/*' - } +// resources { +// exclude '/*' +// } } } @@ -222,6 +234,10 @@ jar { from configurations.runtime into 'dist/lib' } + copy { + from file('lib/') + into 'dist/lib' + } copy { from file('.').listFiles().findAll { File f -> (f.name.endsWith('.sh') || f.name.endsWith('.env')) } into 'dist' @@ -235,10 +251,9 @@ jar { } task makeStubJAR(type: org.gradle.api.tasks.bundling.Jar) { - baseName 'stub' - from 'build/classes/java/main/com/webank/wecross/stub' - from 'src/main/java/com/webank/wecross/stub' - into 'com/webank/wecross/stub/' + baseName 'wecross-java-stub' + into 'com/webank/wecross/stub', { from 'build/classes/java/main/com/webank/wecross/stub' } + into 'com/webank/wecross/exception', { from 'build/classes/java/main/com/webank/wecross/exception' } destinationDir file('dist/stub/') } diff --git a/docker/wecross-router/dockerfile b/docker/wecross-router/dockerfile index b6b274a38..785a10d4c 100644 --- a/docker/wecross-router/dockerfile +++ b/docker/wecross-router/dockerfile @@ -7,4 +7,4 @@ RUN apt-get update && \ tar -zxvf wecross-router.tar.gz && \ rm wecross-router.tar.gz -CMD [ "bash", "/wecross-router/start.sh"] +#CMD [ "bash", "/wecross-router/start.sh"] diff --git "a/docs/Stub \346\217\222\344\273\266\345\274\200\345\217\221.md" "b/docs/Stub \346\217\222\344\273\266\345\274\200\345\217\221.md" new file mode 100644 index 000000000..96f09d41e --- /dev/null +++ "b/docs/Stub \346\217\222\344\273\266\345\274\200\345\217\221.md" @@ -0,0 +1,2795 @@ +# Stub 插件开发 + +## 一、Stub 设计 + +## 二、Stub 开发 + +### 2.1 系统合约 + +系统合约包括代理合约(WeCrossProxy)和桥接合约(WeCrossHub),代理合约是WeCross调用该链其它合约的统一入口,桥接合约用于记录跨链调用请求,以配合跨链路由实现合约跨链调用 + +#### 2.1.1 代理合约 + +功能点: + +- 合约调用入口 +- 事务管理 + +- solidity 版本 + + ```solidity + /* + * v1.0.0 + * proxy contract for WeCross + * main entrance of all contract call + */ + + pragma solidity >=0.5.0 <0.6.0; + pragma experimental ABIEncoderV2; + + contract WeCrossProxy { + + string constant version = "v1.0.0"; + + // per step of xa transaction + struct XATransactionStep { + string accountIdentity; + uint256 timestamp; + string path; + address contractAddress; + string func; + bytes args; + } + + // information of xa transaction + struct XATransaction { + string accountIdentity; + string[] paths; // all paths related to this transaction + address[] contractAddresses; // locked addressed in current chain + string status; // processing | committed | rolledback + uint256 startTimestamp; + uint256 commitTimestamp; + uint256 rollbackTimestamp; + uint256[] seqs; // sequence of each step + uint256 stepNum; // step number + } + + struct ContractStatus { + bool locked; // isolation control, read-committed + string xaTransactionID; + } + + mapping(address => ContractStatus) lockedContracts; + + mapping(string => XATransaction) xaTransactions; // key: xaTransactionID + + mapping(string => XATransactionStep) xaTransactionSteps; // key: xaTransactionID || xaTransactionSeq + + /* + * record all xa transactionIDs + * head: point to the current xa transaction to be checked + * tail: point to the next position for added xa transaction + */ + uint256 head = 0; + uint256 tail = 0; + string[] xaTransactionIDs; + + string constant XA_STATUS_PROCESSING = "processing"; + string constant XA_STATUS_COMMITTED = "committed"; + string constant XA_STATUS_ROLLEDBACK = "rolledback"; + + string constant REVERT_FLAG = "_revert"; + string constant NULL_FLAG = "null"; + string constant SUCCESS_FLAG = "success"; + + byte constant SEPARATOR = '.'; + + uint256 constant ADDRESS_LEN = 42; + uint256 constant MAX_SETP = 1024; + + string[] pathCache; + + struct Transaction { + bool existed; + bytes result; + } + + mapping(string => Transaction) transactions; // key: uniqueID + + CNSPrecompiled cns; + constructor() public { + cns = CNSPrecompiled(0x1004); + } + + function getVersion() public pure + returns(string memory) + { + return version; + } + + function addPath(string memory _path) public + { + pathCache.push(_path); + } + + function getPaths() public view + returns (string[] memory) + { + return pathCache; + } + + function deletePathList() public + { + pathCache.length = 0; + } + + /* + * deploy contract by contract binary code + */ + function deployContract(bytes memory _bin) public returns(address addr) { + bool ok = false; + assembly { + addr := create(0,add(_bin,0x20), mload(_bin)) + ok := gt(extcodesize(addr),0) + } + if(!ok) { + revert("deploy contract failed"); + } + } + + /** + * deploy contract and register contract to cns + */ + function deployContractWithRegisterCNS(string memory _path, string memory _version, bytes memory _bin, string memory _abi) public returns(address) { + string memory name = getNameByPath(_path); + address addr = getAddressByName(name, false); + if((addr != address(0x0)) && lockedContracts[addr].locked) { + revert(string(abi.encodePacked(name, " is locked by unfinished xa transaction: ", lockedContracts[addr].xaTransactionID))); + } + + // deploy contract first + address deploy_addr = deployContract(_bin); + // register to cns + int ret = cns.insert(name, _version, addressToString(deploy_addr), _abi); + if(1 != ret) { + revert(string(abi.encodePacked(name, ":", _version, " unable register to cns, error: ", uint256ToString(uint256(ret > 0? ret : -ret))))); + } + pathCache.push(_path); + return deploy_addr; + } + + /** + * register contract to cns + */ + function registerCNS(string memory _path, string memory _version, string memory _addr, string memory _abi) public { + string memory name = getNameByPath(_path); + address addr = getAddressByName(name, false); + if((addr != address(0x0)) && lockedContracts[addr].locked) { + revert(string(abi.encodePacked(name, " is locked by unfinished xa transaction: ", lockedContracts[addr].xaTransactionID))); + } + + // check if version info exist ??? + int ret = cns.insert(name, _version, _addr, _abi); + if(1 != ret) { + revert(string(abi.encodePacked(name, ":", _version, " unable register to cns, error: ", uint256ToString(uint256(ret > 0 ? ret : - ret))))); + } + pathCache.push(_path); + } + + /** + * select cns by name + */ + function selectByName(string memory _name) public view returns(string memory) { + return cns.selectByName(_name); + } + + /** + * select cns by name and version + */ + function selectByNameAndVersion(string memory _name, string memory _version) public view returns(string memory) { + return cns.selectByNameAndVersion(_name, _version); + } + + // constant call with xaTransactionID + function constantCall(string memory _XATransactionID, string memory _path, string memory _func, bytes memory _args) public + returns(bytes memory) + { + address addr = getAddressByPath(_path); + + if(!isExistedXATransaction(_XATransactionID)) { + revert("xa transaction not found"); + } + + if(!sameString(lockedContracts[addr].xaTransactionID, _XATransactionID)) { + revert(string(abi.encodePacked(_path, " is unregistered in xa transaction: ", _XATransactionID))); + } + + return callContract(addr, _func, _args); + } + + // constant call without xaTransactionID + function constantCall(string memory _name, bytes memory _argsWithMethodId) public + returns(bytes memory) + { + // find address from abi cache first + address addr = getAddressByName(_name, true); + + if(lockedContracts[addr].locked) { + revert(string(abi.encodePacked("resource is locked by unfinished xa transaction: ", lockedContracts[addr].xaTransactionID))); + } + + return callContract(addr, _argsWithMethodId); + } + + // non-constant call with xaTransactionID + function sendTransaction(string memory _uid, string memory _XATransactionID, uint256 _XATransactionSeq, string memory _path, string memory _func, bytes memory _args) public + returns(bytes memory) + { + if(transactions[_uid].existed) { + return transactions[_uid].result; + } + + address addr = getAddressByPath(_path); + + if(!isExistedXATransaction(_XATransactionID)) { + revert("xa transaction not found"); + } + + if(sameString(xaTransactions[_XATransactionID].status, XA_STATUS_COMMITTED)) { + revert("xa transaction has been committed"); + } + + if(sameString(xaTransactions[_XATransactionID].status, XA_STATUS_ROLLEDBACK)) { + revert("xa transaction has been rolledback"); + } + + if(!sameString(lockedContracts[addr].xaTransactionID, _XATransactionID)) { + revert(string(abi.encodePacked(_path, " is unregistered in xa transaction ", _XATransactionID))); + } + + if(!isValidXATransactionSep(_XATransactionID, _XATransactionSeq)) { + revert("seq should be greater than before"); + } + + // recode step + xaTransactionSteps[getXATransactionStepKey(_XATransactionID, _XATransactionSeq)] = XATransactionStep( + addressToString(tx.origin), + block.timestamp / 1000, + _path, + addr, + _func, + _args + ); + + // recode seq + uint256 num = xaTransactions[_XATransactionID].stepNum; + xaTransactions[_XATransactionID].seqs[num] = _XATransactionSeq; + xaTransactions[_XATransactionID].stepNum = num + 1; + + bytes memory result = callContract(addr, _func, _args); + + // recode transaction + transactions[_uid] = Transaction(true, result); + return result; + } + + // non-constant call without xaTransactionID + function sendTransaction(string memory _uid, string memory _name, bytes memory _argsWithMethodId) public returns(bytes memory) { + if(transactions[_uid].existed) { + return transactions[_uid].result; + } + + // find address from abi cache first + address addr = getAddressByName(_name, true); + + if(lockedContracts[addr].locked) { + revert(string(abi.encodePacked(_name, " is locked by unfinished xa transaction: ", lockedContracts[addr].xaTransactionID))); + } + + bytes memory result = callContract(addr, _argsWithMethodId); + + // recode transaction + transactions[_uid] = Transaction(true, result); + return result; + } + + /* + * @param xaTransactionID + * @param selfPaths are related to current chain + * result: success + */ + function startXATransaction(string memory _xaTransactionID, string[] memory _selfPaths, string[] memory _otherPaths) public + returns(string memory) + { + if(isExistedXATransaction(_xaTransactionID)) { + revert(string(abi.encodePacked("xa transaction ", _xaTransactionID, " already exists"))); + } + + uint256 selfLen = _selfPaths.length; + uint256 otherLen = _otherPaths.length; + + address[] memory contracts = new address[](selfLen); + string[] memory allPaths = new string[](selfLen + otherLen); + + // recode ACL + for(uint256 i = 0; i < selfLen; i++) { + address addr = getAddressByPath(_selfPaths[i]); + contracts[i] = addr; + if(lockedContracts[addr].locked) { + revert(string(abi.encodePacked(_selfPaths[i], " is locked by unfinished xa transaction: ", lockedContracts[addr].xaTransactionID))); + } + lockedContracts[addr].locked = true; + lockedContracts[addr].xaTransactionID = _xaTransactionID; + allPaths[i] = _selfPaths[i]; + } + + for(uint256 i = 0; i < otherLen; i++) + { + allPaths[selfLen+i] = _otherPaths[i]; + } + + uint256[] memory seqs = new uint256[](MAX_SETP); + // recode xa transaction + xaTransactions[_xaTransactionID] = XATransaction( + addressToString(tx.origin), + allPaths, + contracts, + XA_STATUS_PROCESSING, + block.timestamp / 1000, + 0, + 0, + seqs, + 0 + ); + + addXATransaction(_xaTransactionID); + + return SUCCESS_FLAG; + } + + /* + * @param xaTransactionID + * result: success + */ + function commitXATransaction(string memory _xaTransactionID) public + returns(string memory) + { + if(!isExistedXATransaction(_xaTransactionID)) { + revert("xa transaction not found"); + } + + // has committed + if(sameString(xaTransactions[_xaTransactionID].status, XA_STATUS_COMMITTED)) { + revert("xa transaction has been committed"); + } + + // has rolledback + if(sameString(xaTransactions[_xaTransactionID].status, XA_STATUS_ROLLEDBACK)) { + revert("xa transaction has been rolledback"); + } + + xaTransactions[_xaTransactionID].commitTimestamp = block.timestamp / 1000; + xaTransactions[_xaTransactionID].status = XA_STATUS_COMMITTED; + deleteLockedContracts(_xaTransactionID); + + return SUCCESS_FLAG; + } + + /* + * @param xaTransactionID + * result: success | message + */ + function rollbackXATransaction(string memory _xaTransactionID) public + returns(string memory) + { + string memory result = SUCCESS_FLAG; + if(!isExistedXATransaction(_xaTransactionID)) { + revert("xa transaction not found"); + } + + // has committed + if(sameString(xaTransactions[_xaTransactionID].status, XA_STATUS_COMMITTED)) { + revert("xa transaction has been committed"); + } + + // has rolledback + if(sameString(xaTransactions[_xaTransactionID].status, XA_STATUS_ROLLEDBACK)) { + revert("xa transaction has been rolledback"); + } + + string memory message = 'warning:'; + uint256 stepNum = xaTransactions[_xaTransactionID].stepNum; + for(uint256 i = stepNum; i > 0; i--) { + uint256 seq = xaTransactions[_xaTransactionID].seqs[i-1]; + string memory key = getXATransactionStepKey(_xaTransactionID, seq); + + string memory func = xaTransactionSteps[key].func; + address contractAddress = xaTransactionSteps[key].contractAddress; + bytes memory args = xaTransactionSteps[key].args; + + // call revert function + bytes memory sig = abi.encodeWithSignature(getRevertFunc(func, REVERT_FLAG)); + bool success; + (success, ) = address(contractAddress).call(abi.encodePacked(sig, args)); + if(!success) { + message = string(abi.encodePacked(message, ' revert "', func, '" failed.')); + result = message; + } + } + + xaTransactions[_xaTransactionID].rollbackTimestamp = block.timestamp / 1000; + xaTransactions[_xaTransactionID].status = XA_STATUS_ROLLEDBACK; + deleteLockedContracts(_xaTransactionID); + return result; + } + + function getXATransactionNumber() public view + returns (string memory) + { + if(xaTransactionIDs.length == 0) { + return "0"; + } else { + return uint256ToString(xaTransactionIDs.length); + } + } + + /* + * traverse in reverse order + * outputs: + { + "total": 100, + "xaTransactions": + [ + { + "xaTransactionID": "001", + "accountIdentity": "0x11", + "status": "processing", + "timestamp": 123, + "paths": ["a.b.1","a.b.2"] + }, + { + "xaTransactionID": "002", + "accountIdentity": "0x11", + "status": "committed", + "timestamp": 123, + "paths": ["a.b.1","a.b.2"] + } + ] + } + */ + function listXATransactions(string memory _index, uint256 _size) public view + returns (string memory) + { + uint256 len = xaTransactionIDs.length; + if (len == 0) { + return '{"total":0,"xaTransactions":[]}'; + } + + uint256 index = sameString("-1", _index) ? (len - 1) : stringToUint256(_index); + + if (len <= index) { + return '{"total":0,"xaTransactions":[]}'; + } + + string memory jsonStr = '['; + for(uint256 i = 0; i < (_size - 1) && (index - i) > 0; i++) { + string memory xaTransactionID = xaTransactionIDs[index-i]; + jsonStr = string(abi.encodePacked(jsonStr, '{"xaTransactionID":"', xaTransactionID, '",', + '"accountIdentity":"', xaTransactions[xaTransactionID].accountIdentity, '",', + '"status":"', xaTransactions[xaTransactionID].status, '",', + '"paths":', pathsToJson(xaTransactionID), ',', + '"timestamp":', uint256ToString(xaTransactions[xaTransactionID].startTimestamp), '},') + ); + } + + uint256 lastIndex = (index + 1) >= _size ? (index + 1 - _size) : 0; + string memory xaTransactionID = xaTransactionIDs[lastIndex]; + jsonStr = string(abi.encodePacked(jsonStr, '{"xaTransactionID":"', xaTransactionID, '",', + '"accountIdentity":"', xaTransactions[xaTransactionID].accountIdentity, '",', + '"status":"', xaTransactions[xaTransactionID].status, '",', + '"paths":', pathsToJson(xaTransactionID), ',', + '"timestamp":', uint256ToString(xaTransactions[xaTransactionID].startTimestamp), '}]') + ); + + return string(abi.encodePacked('{"total":', uint256ToString(len),',"xaTransactions":', jsonStr, '}')); + } + + /* + * @param xaTransactionID + * result with json form + * example: + { + "xaTransactionID": "1", + "accountIdentity": "0x88", + "status": "processing", + "paths":["a.b.c1","a.b.c2","a.b1.c3"], + "startTimestamp": 123, + "commitTimestamp": 456, + "rollbackTimestamp": 0, + "xaTransactionSteps": [{ + "accountIdentity":"0x12", + "xaTransactionSeq": 233, + "path": "a.b.c1", + "timestamp": 233, + "method": "set", + "args": "0010101" + }, + { + "accountIdentity":"0x12", + "xaTransactionSeq": 244, + "path": "a.b.c2", + "timestamp": 244, + "method": "set", + "args": "0010101" + } + ] + } + */ + function getXATransaction(string memory _xaTransactionID) public view + returns(string memory) + { + if(!isExistedXATransaction(_xaTransactionID)) { + revert("xa transaction not found"); + } + + return string(abi.encodePacked('{"xaTransactionID":"', _xaTransactionID, '",', + '"accountIdentity":"', xaTransactions[_xaTransactionID].accountIdentity, '",', + '"status":"', xaTransactions[_xaTransactionID].status, '",', + '"paths":', pathsToJson(_xaTransactionID), ',', + '"startTimestamp":', uint256ToString(xaTransactions[_xaTransactionID].startTimestamp), ',', + '"commitTimestamp":', uint256ToString(xaTransactions[_xaTransactionID].commitTimestamp), ',', + '"rollbackTimestamp":', uint256ToString(xaTransactions[_xaTransactionID].rollbackTimestamp), ',', + '"xaTransactionSteps":', xaTransactionStepArrayToJson(_xaTransactionID, xaTransactions[_xaTransactionID].seqs, xaTransactions[_xaTransactionID].stepNum), "}") + ); + } + + // called by router to check xa transaction status + function getLatestXATransaction() public view + returns(string memory) + { + string memory xaTransactionID; + if(head == tail) { + return '{}'; + } else { + xaTransactionID = xaTransactionIDs[uint256(head)]; + } + return getXATransaction(xaTransactionID); + } + + // called by router to rollbach transaction + function rollbackAndDeleteXATransactionTask(string memory _xaTransactionID) public + returns (string memory) + { + rollbackXATransaction(_xaTransactionID); + return deleteXATransactionTask(_xaTransactionID); + } + + function getLatestXATransactionID() public view + returns (string memory) + { + if(head == tail) { + return NULL_FLAG; + } else { + return xaTransactionIDs[uint256(head)]; + } + } + + function getXATransactionState(string memory _path) public view + returns (string memory) + { + address addr = getAddressByPath(_path); + if(!lockedContracts[addr].locked) { + return NULL_FLAG; + } else { + string memory xaTransactionID = lockedContracts[addr].xaTransactionID; + uint256 index = xaTransactions[xaTransactionID].stepNum; + uint256 seq = index == 0 ? 0 : xaTransactions[xaTransactionID].seqs[index-1]; + return string(abi.encodePacked(xaTransactionID, " ", uint256ToString(seq))); + } + } + + function addXATransaction(string memory _xaTransactionID) internal + { + tail++; + xaTransactionIDs.push(_xaTransactionID); + } + + function deleteXATransactionTask(string memory _xaTransactionID) internal + returns (string memory) + { + if(head == tail) { + revert("delete nonexistent xa transaction"); + } + + if(!sameString(xaTransactionIDs[head], _xaTransactionID)) { + revert("delete unmatched xa transaction"); + } + + head++; + return SUCCESS_FLAG; + } + + // internal call + function callContract(address _contractAddress, string memory _sig, bytes memory _args) internal + returns(bytes memory result) + { + bytes memory sig = abi.encodeWithSignature(_sig); + bool success; + (success, result) = address(_contractAddress).call(abi.encodePacked(sig, _args)); + if(!success) { + revert(string(result)); + } + } + + // internal call + function callContract(address _contractAddress, bytes memory _argsWithMethodId) internal + returns(bytes memory result) + { + bool success; + (success, result) = address(_contractAddress).call(_argsWithMethodId); + if(!success) { + //(string memory error) = abi.decode(result, (string)); + revert(string(result)); + } + } + + + // retrive address from CNS + function getAddressByName(string memory _name, bool revertNotExist) internal view + returns (address) + { + string memory strJson = cns.selectByName(_name); + + bytes memory str = bytes(strJson); + uint256 len = str.length; + + uint256 index = newKMP(str, bytes("\"sserdda\"")); + if(index == 0) { + if(revertNotExist) { + revert("the name's address not exist."); + } + return address(0x0); + } + + bytes memory addr = new bytes(ADDRESS_LEN); + uint256 start = 0; + for(uint256 i = index; i < len; i++) { + if(str[i] == byte('0') && str[i+1] == byte('x')) { + start = i; + break; + } + } + + for(uint256 i = 0; i < ADDRESS_LEN; i++) { + addr[i] = str[start + i]; + } + + return bytesToAddress(addr); + } + + // retrive address from CNS + function getAddressByPath(string memory _path) internal view + returns (address) + { + string memory name = getNameByPath(_path); + return getAddressByName(name, true); + } + + // input must be a valid path like "zone.chain.resource" + function getNameByPath(string memory _path) internal pure + returns (string memory) + { + bytes memory path = bytes(_path); + uint256 len = path.length; + uint256 nameLen = 0; + uint256 index = 0; + for(uint256 i = len - 1; i > 0; i--) { + if(path[i] == SEPARATOR) { + index = i + 1; + break; + } else { + nameLen++; + } + } + + bytes memory name = new bytes(nameLen); + for(uint256 i = 0; i < nameLen; i++) { + name[i] = path[index++]; + } + + return string(name); + } + + /* + ["a.b.c1", "a.b.c2"] + */ + function pathsToJson(string memory _transactionID) internal view + returns(string memory) + { + uint256 len = xaTransactions[_transactionID].paths.length; + string memory paths = string(abi.encodePacked('["', xaTransactions[_transactionID].paths[0], '"')); + for(uint256 i = 1; i < len; i++) { + paths = string(abi.encodePacked(paths, ',"', xaTransactions[_transactionID].paths[i], '"')); + } + return string(abi.encodePacked(paths, ']')); + } + + /* + [ + { + "accountIdentity":"0x12", + "xaTransactionSeq": 233, + "path": "a.b.c1", + "timestamp": 233, + "method": "set", + "args": "0010101" + }, + { + "accountIdentity":"0x12", + "xaTransactionSeq": 233, + "path": "a.b.c1", + "timestamp": 233, + "method": "set", + "args": "0010101" + } + ] + */ + function xaTransactionStepArrayToJson(string memory _transactionID, uint256[] memory _seqs, uint256 _len) internal view + returns(string memory result) + { + if(_len == 0) { + return '[]'; + } + + result = string(abi.encodePacked('[', xatransactionStepToJson(xaTransactionSteps[getXATransactionStepKey(_transactionID, _seqs[0])], _seqs[0]))); + for(uint256 i = 1; i < _len; i++) { + result = string(abi.encodePacked(result, ',', xatransactionStepToJson(xaTransactionSteps[getXATransactionStepKey(_transactionID, _seqs[i])], _seqs[i]))); + } + + return string(abi.encodePacked(result, ']')); + } + + /* + { + "xaTransactionSeq": 233, + "accountIdentity":"0x12", + "path": "a.b.c1", + "timestamp": 233, + "method": "set", + "args": "0010101" + } + */ + function xatransactionStepToJson(XATransactionStep memory _xaTransactionStep, uint256 _XATransactionSeq) internal pure + returns(string memory) + { + return string(abi.encodePacked('{"xaTransactionSeq":', uint256ToString(_XATransactionSeq), ',', + '"accountIdentity":"', _xaTransactionStep.accountIdentity, '",', + '"path":"', _xaTransactionStep.path, '",', + '"timestamp":', uint256ToString(_xaTransactionStep.timestamp), ',', + '"method":"', getMethodFromFunc(_xaTransactionStep.func), '",', + '"args":"', bytesToHexString(_xaTransactionStep.args), '"}') + ); + } + + function isExistedXATransaction(string memory _xaTransactionID) internal view + returns (bool) + { + return xaTransactions[_xaTransactionID].startTimestamp != 0; + } + + function isValidXATransactionSep(string memory _xaTransactionID, uint256 _XATransactionSeq) internal view + returns(bool) + { + uint256 index = xaTransactions[_xaTransactionID].stepNum; + return (index == 0) || (_XATransactionSeq > xaTransactions[_xaTransactionID].seqs[index-1]); + } + + function deleteLockedContracts(string memory _xaTransactionID) internal + { + uint256 len = xaTransactions[_xaTransactionID].contractAddresses.length; + for(uint256 i = 0; i < len; i++) { + address contractAddress = xaTransactions[_xaTransactionID].contractAddresses[i]; + delete lockedContracts[contractAddress]; + } + } + + /* a famous algorithm for finding substring + match starts with tail, and the target must be "\"sserdda\"" + */ + function newKMP(bytes memory _str, bytes memory _target) internal pure + returns (uint256) + { + int256 strLen = int256(_str.length); + int256 tarLen = int256(_target.length); + + // next array for target "\"sserdda\"" + int8[9] memory nextArray = [-1,0,0,0,0,0,0,0,0]; + + int256 i = strLen; + int256 j = 0; + + while (i > 0 && j < tarLen) { + if (j == -1 || _str[uint256(i-1)] == _target[uint256(j)]) { + i--; + j++; + } else { + j = int256(nextArray[uint256(j)]); + } + } + + if ( j == tarLen) { + return uint256(i + tarLen); + } + + return 0; + } + + // func(string,uint256) => func_flag(string,uint256) + function getRevertFunc(string memory _func, string memory _revertFlag) internal pure + returns(string memory) + { + bytes memory funcBytes = bytes(_func); + bytes memory flagBytes = bytes(_revertFlag); + uint256 funcLen = funcBytes.length; + uint256 flagLen = flagBytes.length; + bytes memory newFunc = new bytes(funcLen + flagLen); + + byte c = byte('('); + uint256 index = 0; + uint256 point = 0; + + for(uint256 i = 0; i < funcLen; i++) { + if(funcBytes[i] != c) { + newFunc[index++] = funcBytes[i]; + } else { + point = i; + break; + } + } + + for(uint256 i = 0; i < flagLen; i++) { + newFunc[index++] = flagBytes[i]; + } + + for(uint256 i = point; i < funcLen; i++) { + newFunc[index++] = funcBytes[i]; + } + + return string(newFunc); + } + + // func(string,uint256) => func + function getMethodFromFunc(string memory _func) internal pure + returns(string memory) + { + bytes memory funcBytes = bytes(_func); + uint256 funcLen = funcBytes.length; + bytes memory temp = new bytes(funcLen); + + byte c = byte('('); + uint256 index = 0; + + for(uint256 i = 0; i < funcLen; i++) { + if(funcBytes[i] != c) { + temp[index++] = funcBytes[i]; + } else { + break; + } + } + + bytes memory result = new bytes(index); + for(uint256 i = 0; i < index; i++) { + result[i] = temp[i]; + } + + return string(result); + } + + function getXATransactionStepKey(string memory _transactionID, uint256 _transactionSeq) internal pure + returns(string memory) + { + return string(abi.encodePacked(_transactionID, uint256ToString(_transactionSeq))); + } + + function sameString(string memory _str1, string memory _str2) internal pure + returns (bool) + { + return keccak256(bytes(_str1)) == keccak256(bytes(_str2)); + } + + function hexStringToBytes(string memory _hexStr) internal pure + returns (bytes memory) + { + bytes memory bts = bytes(_hexStr); + require(bts.length%2 == 0); + bytes memory result = new bytes(bts.length/2); + uint len = bts.length/2; + for (uint i = 0; i < len; ++i) { + result[i] = byte(fromHexChar(uint8(bts[2*i])) * 16 + + fromHexChar(uint8(bts[2*i+1]))); + } + return result; + } + + function fromHexChar(uint8 _char) internal pure + returns (uint8) + { + if (byte(_char) >= byte('0') && byte(_char) <= byte('9')) { + return _char - uint8(byte('0')); + } + if (byte(_char) >= byte('a') && byte(_char) <= byte('f')) { + return 10 + _char - uint8(byte('a')); + } + if (byte(_char) >= byte('A') && byte(_char) <= byte('F')) { + return 10 + _char - uint8(byte('A')); + } + } + + function stringToUint256(string memory _str) public pure + returns (uint256) + { + bytes memory bts = bytes(_str); + uint256 result = 0; + uint256 len = bts.length; + for (uint256 i = 0; i < len; i++) { + if (uint8(bts[i]) >= 48 && uint8(bts[i]) <= 57) { + result = result * 10 + (uint8(bts[i]) - 48); + } + } + return result; + } + + function uint256ToString(uint256 _value) internal pure + returns (string memory) + { + bytes32 result; + if (_value == 0) { + return "0"; + } else { + while (_value > 0) { + result = bytes32(uint(result) / (2 ** 8)); + result |= bytes32(((_value % 10) + 48) * 2 ** (8 * 31)); + _value /= 10; + } + } + return bytes32ToString(result); + } + + function bytesToHexString(bytes memory _bts) internal pure + returns (string memory result) + { + uint256 len = _bts.length; + bytes memory s = new bytes(len * 2); + for (uint256 i = 0; i < len; i++) { + byte befor = byte(_bts[i]); + byte high = byte(uint8(befor) / 16); + byte low = byte(uint8(befor) - 16 * uint8(high)); + s[i*2] = convert(high); + s[i*2+1] = convert(low); + } + result = string(s); + } + + function bytes32ToString(bytes32 _bts32) internal pure + returns (string memory) + { + + bytes memory result = new bytes(_bts32.length); + + uint len = _bts32.length; + for(uint i = 0; i < len; i++) { + result[i] = _bts32[i]; + } + + return string(result); + } + + function bytesToAddress(bytes memory _address) internal pure + returns (address) + { + if(_address.length != 42) { + revert(string(abi.encodePacked("cannot covert ", _address, "to bcos address"))); + } + + uint160 result = 0; + uint160 b1; + uint160 b2; + for (uint i = 2; i < 2 + 2 * 20; i += 2) { + result *= 256; + b1 = uint160(uint8(_address[i])); + b2 = uint160(uint8(_address[i + 1])); + if ((b1 >= 97) && (b1 <= 102)) { + b1 -= 87; + } else if ((b1 >= 65) && (b1 <= 70)) { + b1 -= 55; + } else if ((b1 >= 48) && (b1 <= 57)) { + b1 -= 48; + } + + if ((b2 >= 97) && (b2 <= 102)) { + b2 -= 87; + } else if ((b2 >= 65) && (b2 <= 70)) { + b2 -= 55; + } else if ((b2 >= 48) && (b2 <= 57)) { + b2 -= 48; + } + result += (b1 * 16 + b2); + } + return address(result); + } + + function addressToString(address _addr) internal pure + returns (string memory) + { + bytes memory result = new bytes(40); + for (uint i = 0; i < 20; i++) { + byte temp = byte(uint8(uint(_addr) / (2 ** (8 * (19 - i))))); + byte b1 = byte(uint8(temp) / 16); + byte b2 = byte(uint8(temp) - 16 * uint8(b1)); + result[2 * i] = convert(b1); + result[2 * i + 1] = convert(b2); + } + return string(abi.encodePacked("0x", string(result))); + } + + function convert(byte _b) internal pure + returns (byte) + { + if (uint8(_b) < 10) { + return byte(uint8(_b) + 0x30); + } else { + return byte(uint8(_b) + 0x57); + } + } + } + + contract CNSPrecompiled { + function insert(string memory name, string memory version, string memory addr, string memory abiStr) public returns(int256); + function selectByName(string memory name) public view returns (string memory); + function selectByNameAndVersion(string memory name, string memory version) public view returns (string memory); + } + ``` + +- golang 版本 + + ```go + /* + * v1.0.0 + * proxy contract for WeCross + * main entrance of all contract call + */ + + package main + + import ( + "bytes" + "encoding/json" + "fmt" + "github.com/hyperledger/fabric/core/chaincode/shim" + "github.com/hyperledger/fabric/protos/peer" + "strconv" + "strings" + ) + + const ( + Version = "v1.0.0" + RevertFlag = "_revert" + Separator = "." + NullFlag = "null" + SuccessFlag = "success" + XAStatusProcessing = "processing" + XAStatusCommitted = "committed" + XAStatusRolledback = "rolledback" + + XATransactionListLenKey = "XATransactionLen" + XATaskHeadKey = "XATransactionTaskHead" + ChannelKey = "Channel" + LockContractKey = "Contract-%s" // %s: chaincode name + XATransactionKey = "XATransaction-%s-info" // %s: xa transaction id + XATransactionTaskKey = "XATransaction-%d-task" // %d: index + ) + + type XATransactionStep struct { + Seq uint64 `json:"xaTransactionSeq"` + Identity string `json:"accountIdentity"` + Path string `json:"path"` + Timestamp uint64 `json:"timestamp"` + Method string `json:"method"` + Args string `json:"args"` + } + + type XATransaction struct { + TransactionID string `json:"xaTransactionID"` + Identity string `json:"accountIdentity"` + Contracts []string `json:"contracts"` + Paths []string `json:"paths"` // all paths related to this transaction + Status string `json:"status"` + StartTimestamp uint64 `json:"startTimestamp"` + CommitTimestamp uint64 `json:"commitTimestamp"` + RollbackTimestamp uint64 `json:"rollbackTimestamp"` + Seqs []uint64 `json:"seqs"` + XATransactionSteps []XATransactionStep `json:"xaTransactionSteps"` + } + + type LockedContract struct { + //Path string `json:"path"` + XATransactionID string `json:"xaTransactionID"` + } + + type Proxy struct { + } + + func (p *Proxy) Init(stub shim.ChaincodeStubInterface) (res peer.Response) { + defer func() { + if r := recover(); r != nil { + res = shim.Error(fmt.Sprintf("%v", r)) + } + }() + fn, args := stub.GetFunctionAndParameters() + + switch fn { + case "init": + res = p.init(stub, args) + default: + res = shim.Success(nil) + } + return + } + + func (p *Proxy) Invoke(stub shim.ChaincodeStubInterface) (res peer.Response) { + defer func() { + if r := recover(); r != nil { + res = shim.Error(fmt.Sprintf("%v", r)) + } + }() + + fn, args := stub.GetFunctionAndParameters() + + switch fn { + case "init": + res = p.init(stub, args) + case "getVersion": + res = p.getVersion() + case "constantCall": + res = p.constantCall(stub, args) + case "sendTransaction": + res = p.sendTransaction(stub, args) + case "startXATransaction": + res = p.startXATransaction(stub, args) + case "commitXATransaction": + res = p.commitXATransaction(stub, args) + case "rollbackXATransaction": + res = p.rollbackXATransaction(stub, args) + case "getXATransactionNumber": + res = p.getXATransactionNumber(stub) + case "listXATransactions": + res = p.listXATransactions(stub, args) + case "getXATransaction": + res = p.getXATransaction(stub, args) + case "getLatestXATransaction": + res = p.getLatestXATransaction(stub) + case "rollbackAndDeleteXATransactionTask": + res = p.rollbackAndDeleteXATransactionTask(stub, args) + case "getXATransactionState": + res = p.getXATransactionState(stub, args) + default: + res = shim.Error("invalid function name") + } + + return + } + + // set channel + func (p *Proxy) init(stub shim.ChaincodeStubInterface, args []string) peer.Response { + if len(args) != 1 { + return shim.Error("invalid arguments, [channel] expected") + } + + channel := args[0] + err := stub.PutState(ChannelKey, []byte(channel)) + checkError(err) + err = stub.PutState(XATransactionListLenKey, []byte("0")) + checkError(err) + err = stub.PutState(XATaskHeadKey, []byte("0")) + checkError(err) + + return shim.Success([]byte(SuccessFlag)) + } + + func (p *Proxy) getVersion() peer.Response { + return shim.Success([]byte(Version)) + } + + // query + func (p *Proxy) constantCall(stub shim.ChaincodeStubInterface, args []string) peer.Response { + if len(args) != 4 { + return shim.Error("invalid arguments") + } + xaTransactionID, path, method, thisArgs := args[0], args[1], args[2], args[3] + + chaincodeName := getNameFromPath(path) + + var lockedContract LockedContract + isLocked := getLockedContract(stub, chaincodeName, &lockedContract) + + if xaTransactionID == "0" { + if isLocked { + return shim.Error("resource is locked by unfinished xa transaction: " + lockedContract.XATransactionID) + } + return callContract(stub, chaincodeName, method, thisArgs) + } + + if !isExistedXATransaction(stub, xaTransactionID) { + return shim.Error("xa transaction id not found") + } + + if lockedContract.XATransactionID != xaTransactionID { + return shim.Error(path + "is unregistered in xa transaction " + xaTransactionID) + } + + return callContract(stub, chaincodeName, method, thisArgs) + } + + // invoke + func (p *Proxy) sendTransaction(stub shim.ChaincodeStubInterface, args []string) peer.Response { + if len(args) != 6 { + return shim.Error("invalid arguments") + } + uniqueID, xaTransactionID, xaTransactionSeq, path, method, realArgs := args[0], args[1], stringToUint64(args[2]), args[3], args[4], args[5] + + res, err := stub.GetState(uniqueID) + checkError(err) + if res != nil { + return shim.Success(res) + } + + chaincodeName := getNameFromPath(path) + + var lockedContract LockedContract + isLocked := getLockedContract(stub, chaincodeName, &lockedContract) + + if xaTransactionID == "0" { + if isLocked { + return shim.Error(path + " is locked by unfinished xa transaction: " + lockedContract.XATransactionID) + } + return callContract(stub, chaincodeName, method, realArgs) + } + + if !isExistedXATransaction(stub, xaTransactionID) { + return shim.Error("xa transaction not found") + } + + var xaTransaction XATransaction + getXATransaction(stub, xaTransactionID, &xaTransaction) + if xaTransaction.Status == XAStatusCommitted { + return shim.Error("xa transaction has been committed") + } + + if xaTransaction.Status == XAStatusRolledback { + return shim.Error("xa transaction has been rolledback") + } + + if lockedContract.XATransactionID != xaTransactionID { + return shim.Error(path + "is unregistered in xa transaction " + xaTransactionID) + } + + if !isValidSeq(stub, xaTransactionID, xaTransactionSeq) { + return shim.Error("xaTransactionSeq should be greater than before") + } + + timeStamp, err := stub.GetTxTimestamp() + checkError(err) + + // recode transactionStep + var xaTransactionStep = XATransactionStep{ + Seq: xaTransactionSeq, + Identity: getIdentity(stub), + Path: path, + Timestamp: uint64(timeStamp.Seconds), + Method: method, + Args: realArgs, + } + + xaTransaction.Seqs = append(xaTransaction.Seqs, xaTransactionSeq) + xaTransaction.XATransactionSteps = append(xaTransaction.XATransactionSteps, xaTransactionStep) + + // recode xaTransaction + xa, err := json.Marshal(&xaTransaction) + checkError(err) + err = stub.PutState(getXATransactionKey(xaTransactionID), xa) + checkError(err) + + response := callContract(stub, chaincodeName, method, realArgs) + if response.Status == shim.OK { + err = stub.PutState(uniqueID, response.Payload) + checkError(err) + } + return response + } + + /* + * @args transactionID || selfPaths || otherPaths + * result: success + */ + func (p *Proxy) startXATransaction(stub shim.ChaincodeStubInterface, args []string) peer.Response { + argsLen := len(args) + if argsLen != 3 { + return shim.Error("invalid arguments") + } + + xaTransactionID := args[0] + if isExistedXATransaction(stub, xaTransactionID) { + return shim.Error("xa transaction " + xaTransactionID + " already exists") + } + + var selfPaths, otherPaths, allPaths, contracts []string + err := json.Unmarshal([]byte(args[1]), &selfPaths) + checkError(err) + err = json.Unmarshal([]byte(args[2]), &otherPaths) + checkError(err) + + for i := 0; i < len(selfPaths); i++ { + chaincodeName := getNameFromPath(selfPaths[i]) + contracts = append(contracts, chaincodeName) + var lockedContract LockedContract + isLocked := getLockedContract(stub, chaincodeName, &lockedContract) + // contract conflict + if isLocked { + return shim.Error(selfPaths[i] + " is locked by unfinished xa transaction: " + lockedContract.XATransactionID) + } + lockedContract = LockedContract{ + XATransactionID: xaTransactionID, + } + lc, err := json.Marshal(&lockedContract) + checkError(err) + err = stub.PutState(getLockContractKey(chaincodeName), lc) + checkError(err) + allPaths = append(allPaths, selfPaths[i]) + } + + for i := 0; i < len(otherPaths); i++ { + allPaths = append(allPaths, otherPaths[i]) + } + + timeStamp, err := stub.GetTxTimestamp() + checkError(err) + var xaTransaction = XATransaction{ + TransactionID: xaTransactionID, + Identity: getIdentity(stub), + Contracts: contracts, + Paths: allPaths, + Status: XAStatusProcessing, + StartTimestamp: uint64(timeStamp.Seconds), + CommitTimestamp: 0, + RollbackTimestamp: 0, + Seqs: []uint64{}, + XATransactionSteps: []XATransactionStep{}, + } + + xa, err := json.Marshal(&xaTransaction) + checkError(err) + err = stub.PutState(getXATransactionKey(xaTransactionID), xa) + checkError(err) + + addXATransaction(stub, xaTransactionID) + return shim.Success([]byte(SuccessFlag)) + } + + /* + * result: success + */ + func (p *Proxy) commitXATransaction(stub shim.ChaincodeStubInterface, args []string) peer.Response { + if len(args) != 1 { + return shim.Error("invalid arguments") + } + + xaTransactionID := args[0] + if !isExistedXATransaction(stub, xaTransactionID) { + return shim.Error("xa transaction not found") + } + + var xaTransaction XATransaction + getXATransaction(stub, xaTransactionID, &xaTransaction) + + if xaTransaction.Status == XAStatusCommitted { + return shim.Error("xa transaction has been committed") + } + + if xaTransaction.Status == XAStatusRolledback { + return shim.Error("xa transaction has been rolledback") + } + + timeStamp, err := stub.GetTxTimestamp() + checkError(err) + xaTransaction.Status = XAStatusCommitted + xaTransaction.CommitTimestamp = uint64(timeStamp.Seconds) + + xa, err := json.Marshal(&xaTransaction) + checkError(err) + err = stub.PutState(getXATransactionKey(xaTransactionID), xa) + checkError(err) + + deleteLockedContracts(stub, xaTransactionID) + return shim.Success([]byte(SuccessFlag)) + } + + /* + * result: success | warning message + */ + func (p *Proxy) rollbackXATransaction(stub shim.ChaincodeStubInterface, args []string) peer.Response { + if len(args) != 1 { + return shim.Error("invalid arguments") + } + + xaTransactionID := args[0] + if !isExistedXATransaction(stub, xaTransactionID) { + return shim.Error("xa transaction not found") + } + + var xaTransaction XATransaction + getXATransaction(stub, xaTransactionID, &xaTransaction) + + if xaTransaction.Status == XAStatusCommitted { + return shim.Error("xa transaction has been committed") + } + + if xaTransaction.Status == XAStatusRolledback { + return shim.Error("xa transaction has been rolledback") + } + + var res = SuccessFlag + var message = "warning:" + for i := len(xaTransaction.XATransactionSteps) - 1; i >= 0; i-- { + transactionStep := xaTransaction.XATransactionSteps[i] + newMethod := getRevertFunc(transactionStep.Method) + chaincodeName := getNameFromPath(transactionStep.Path) + + // call revert function + response := callContract(stub, chaincodeName, newMethod, transactionStep.Args) + if response.Status != shim.OK { + message = message + " revert \"" + transactionStep.Method + "\" failed." + res = message + } + } + + timeStamp, err := stub.GetTxTimestamp() + checkError(err) + xaTransaction.Status = XAStatusRolledback + xaTransaction.RollbackTimestamp = uint64(timeStamp.Seconds) + + xa, err := json.Marshal(&xaTransaction) + checkError(err) + err = stub.PutState(getXATransactionKey(xaTransactionID), xa) + checkError(err) + + deleteLockedContracts(stub, xaTransactionID) + + return shim.Success([]byte(res)) + } + + // return json string + func (p *Proxy) getXATransaction(stub shim.ChaincodeStubInterface, args []string) peer.Response { + if len(args) != 1 { + return shim.Error("invalid arguments") + } + + xaTransactionID := args[0] + if !isExistedXATransaction(stub, xaTransactionID) { + return shim.Error("xa transaction not found") + } + + xa, err := stub.GetState(getXATransactionKey(xaTransactionID)) + checkError(err) + + return shim.Success(xa) + } + + func (p *Proxy) getXATransactionNumber(stub shim.ChaincodeStubInterface) peer.Response { + num, err := stub.GetState(XATransactionListLenKey) + checkError(err) + + return shim.Success(num) + } + + // return all transaction ids + func (p *Proxy) listXATransactions(stub shim.ChaincodeStubInterface, args []string) peer.Response { + if len(args) != 2 { + return shim.Error("invalid arguments") + } + + xaLen, err := stub.GetState(XATransactionListLenKey) + checkError(err) + length := bytesToUint64(xaLen) + + var index uint64 + if "-1" == args[0] { + index = length - 1 + } else { + index = stringToUint64(args[0]) + } + + size := stringToInt(args[1]) + + if length == 0 || length < index { + return shim.Success([]byte("{\"total\":0,\"xaTransactions\":[]}")) + } + + type XAInfo struct { + TransactionID string `json:"xaTransactionID"` + Identity string `json:"accountIdentity"` + Status string `json:"status"` + Timestamp uint64 `json:"timestamp"` + Paths []string `json:"paths"` + } + + type XAList struct { + Total uint64 `json:"total"` + XATransactions []XAInfo `json:"xaTransactions"` + } + + var xaList XAList + var i int + + for i = 0; i < size && index >= uint64(i); i++ { + tid, err := stub.GetState(getTransactionTaskKey(index - uint64(i))) + checkError(err) + + var xaTransaction XATransaction + getXATransaction(stub, string(tid), &xaTransaction) + var info = XAInfo{ + TransactionID: string(tid), + Identity: getIdentity(stub), + Status: xaTransaction.Status, + Timestamp: xaTransaction.StartTimestamp, + Paths: xaTransaction.Paths, + } + xaList.XATransactions = append(xaList.XATransactions, info) + } + + xaList.Total = length + res, err := json.Marshal(&xaList) + checkError(err) + + return shim.Success(res) + } + + // called by router to check transaction status + func (p *Proxy) getLatestXATransaction(stub shim.ChaincodeStubInterface) peer.Response { + xaTransactionID := getLatestTransactionID(stub) + + if xaTransactionID == NullFlag { + return shim.Success([]byte(NullFlag)) + } + + return p.getXATransaction(stub, []string{xaTransactionID}) + } + + func (p *Proxy) rollbackAndDeleteXATransactionTask(stub shim.ChaincodeStubInterface, args []string) peer.Response { + if len(args) != 1 { + return shim.Error("invalid arguments") + } + + res := p.rollbackXATransaction(stub, args) + if res.Status == shim.ERROR { + return res + } + + return deleteLatestTransaction(stub, args[1]) + } + + func (p *Proxy) getXATransactionState(stub shim.ChaincodeStubInterface, args []string) peer.Response { + if len(args) != 1 { + return shim.Error("invalid arguments") + } + + path := args[0] + chaincodeName := getNameFromPath(path) + + var lockedContract LockedContract + isLocked := getLockedContract(stub, chaincodeName, &lockedContract) + + if !isLocked { + return shim.Success([]byte(NullFlag)) + } else { + seq := getCurrentSeq(stub, lockedContract.XATransactionID) + return shim.Success([]byte(lockedContract.XATransactionID + " " + strconv.FormatUint(seq, 10))) + } + } + + func callContract(stub shim.ChaincodeStubInterface, contract, method, jsonArgs string) peer.Response { + // parse args from json str + var args []string + err := json.Unmarshal([]byte(jsonArgs), &args) + checkError(err) + + var trans [][]byte + trans = append(trans, []byte(method)) + for _, param := range args { + trans = append(trans, []byte(param)) + } + + channel, err := stub.GetState(ChannelKey) + checkError(err) + + return stub.InvokeChaincode(contract, trans, string(channel)) + } + + func getIdentity(stub shim.ChaincodeStubInterface) string { + creator, err := stub.GetCreator() + checkError(err) + + certStart := bytes.IndexAny(creator, "-----BEGIN") + if certStart == -1 { + panic("no certificate found") + } + + return string(creator[certStart:]) + } + + func addXATransaction(stub shim.ChaincodeStubInterface, transactionID string) { + xaLen, err := stub.GetState(XATransactionListLenKey) + checkError(err) + + index := bytesToUint64(xaLen) + err = stub.PutState(getTransactionTaskKey(index), []byte(transactionID)) + checkError(err) + + err = stub.PutState(XATransactionListLenKey, uint64ToBytes(index+1)) + checkError(err) + } + + func getLatestTransactionID(stub shim.ChaincodeStubInterface) string { + taskLen, err := stub.GetState(XATransactionListLenKey) + checkError(err) + + head, err := stub.GetState(XATaskHeadKey) + checkError(err) + + if bytesToUint64(taskLen) == 0 || bytesToUint64(head) >= bytesToUint64(taskLen) { + return NullFlag + } + + id, err := stub.GetState(getTransactionTaskKey(bytesToUint64(head))) + checkError(err) + + return string(id) + } + + func deleteLatestTransaction(stub shim.ChaincodeStubInterface, transactionID string) peer.Response { + taskLen, err := stub.GetState(XATransactionListLenKey) + checkError(err) + + head, err := stub.GetState(XATaskHeadKey) + checkError(err) + + if bytesToUint64(taskLen) == 0 || bytesToUint64(head) >= bytesToUint64(taskLen) { + return shim.Error("delete nonexistent xa transaction") + } + + id, err := stub.GetState(getTransactionTaskKey(bytesToUint64(head))) + checkError(err) + + if string(id) != transactionID { + return shim.Error("delete unmatched transaction") + } + + err = stub.PutState(XATaskHeadKey, uint64ToBytes(bytesToUint64(head)+1)) + checkError(err) + + return shim.Success([]byte(SuccessFlag)) + } + + func getNameFromPath(path string) string { + strs := strings.Split(path, Separator) + if len(strs) != 3 { + panic(fmt.Errorf("invalid path: " + path)) + } + + return strs[2] + } + + func getRevertFunc(method string) string { + return method + RevertFlag + } + + func isExistedXATransaction(stub shim.ChaincodeStubInterface, xaTransactionID string) bool { + id, err := stub.GetState(getXATransactionKey(xaTransactionID)) + checkError(err) + + return id != nil + } + + func isValidSeq(stub shim.ChaincodeStubInterface, xaTransactionID string, seq uint64) bool { + var xaTransaction XATransaction + getXATransaction(stub, xaTransactionID, &xaTransaction) + index := len(xaTransaction.Seqs) + return (index == 0) || (seq > xaTransaction.Seqs[index-1]) + } + + func getCurrentSeq(stub shim.ChaincodeStubInterface, xaTransactionID string) uint64 { + var xaTransaction XATransaction + getXATransaction(stub, xaTransactionID, &xaTransaction) + index := len(xaTransaction.Seqs) + if index == 0 { + return 0 + } else { + return xaTransaction.Seqs[index-1] + } + } + + func getXATransaction(stub shim.ChaincodeStubInterface, xaTransactionID string, xa *XATransaction) { + data, err := stub.GetState(getXATransactionKey(xaTransactionID)) + checkError(err) + + err = json.Unmarshal(data, xa) + checkError(err) + } + + func getLockedContract(stub shim.ChaincodeStubInterface, contract string, lc *LockedContract) bool { + state, err := stub.GetState(getLockContractKey(contract)) + checkError(err) + + if state == nil { + return false + } else { + err = json.Unmarshal(state, lc) + checkError(err) + return true + } + } + + func deleteLockedContracts(stub shim.ChaincodeStubInterface, transactionID string) { + var xaTransaction XATransaction + getXATransaction(stub, transactionID, &xaTransaction) + + for _, contract := range xaTransaction.Contracts { + err := stub.DelState(getLockContractKey(contract)) + checkError(err) + } + } + + func getLockContractKey(contract string) string { + return fmt.Sprintf(LockContractKey, contract) + } + + func getXATransactionKey(transactionID string) string { + return fmt.Sprintf(XATransactionKey, transactionID) + } + + func getTransactionTaskKey(index uint64) string { + return fmt.Sprintf(XATransactionTaskKey, index) + } + + func stringToUint64(str string) uint64 { + i, e := strconv.Atoi(str) + if e != nil { + return 0 + } + return uint64(i) + } + + func stringToInt(str string) int { + i, e := strconv.Atoi(str) + if e != nil { + return 0 + } + return i + } + + func bytesToUint64(bts []byte) uint64 { + u, err := strconv.ParseUint(string(bts), 10, 64) + checkError(err) + + return u + } + + func uint64ToString(u uint64) string { + return strconv.FormatUint(u, 10) + } + + func uint64ToBytes(u uint64) []byte { + return []byte(uint64ToString(u)) + } + func checkError(err error) { + if err != nil { + panic(err) + } + } + + func main() { + err := shim.Start(new(Proxy)) + if err != nil { + fmt.Printf("Error: %s", err) + } + } + + ``` + +#### 2.1.2 桥接合约 + +功能点: + +- 注册跨链调用请求 + +- 查询跨链调用回调结果 + +- Solidity版 + + ```solidity + /* + * v1.0.0 + * hub contract for WeCross + * main entrance of interchain call + */ + + pragma solidity >=0.4.22 <0.6.0; + pragma experimental ABIEncoderV2; + + contract WeCrossHub { + + // string constant EVENT_TYPE = "INTERCHAIN"; + + string constant NULL_FLAG = "null"; + + string constant VERSION = "v1.0.0"; + + string constant CALL_TYPE_QUERY = "0"; + + string constant CALL_TYPE_INVOKE = "1"; + + string constant CALL_TYPE_GET_BLOCK = "2"; + + uint256 increment = 0; + + uint256 currentIndex = 0; + + mapping(uint256 => string) requests; + + mapping(string => string[]) callbackResults; + + function getVersion() public pure + returns(string memory) + { + return VERSION; + } + + // get current uid + function getIncrement() public view + returns(uint256) + { + return increment; + } + + // invoke other chain + function interchainInvoke(string memory _path, string memory _method, string[] memory _args, string memory _callbackPath, string memory _callbackMethod) public + returns(string memory uid) + { + return handleRequest(CALL_TYPE_INVOKE, _path, _method, _args, _callbackPath, _callbackMethod); + } + + // query other chain, not support right now + function interchainQuery(string memory _path, string memory _method, string[] memory _args, string memory _callbackPath, string memory _callbackMethod) public + returns(string memory uid) + { + return handleRequest(CALL_TYPE_QUERY, _path, _method, _args, _callbackPath, _callbackMethod); + } + + function interchainGetBlock(string memory _path, string memory _method, string[] memory _args, string memory _callbackPath, string memory _callbackMethod) public + returns(string memory uid) + { + return handleRequest(CALL_TYPE_GET_BLOCK, _path, _method, _args, _callbackPath, _callbackMethod); + } + + function handleRequest(string memory _callType, string memory _path, string memory _method, string[] memory _args, string memory _callbackPath, string memory _callbackMethod) private + returns(string memory uid) + { + uid = uint256ToString(++increment); + + string[] memory reuqest = new string[](8); + reuqest[0] = uid; + reuqest[1] = _callType; + reuqest[2] = _path; + reuqest[3] = _method; + reuqest[4] = serializeStringArray(_args); + reuqest[5] = _callbackPath; + reuqest[6] = _callbackMethod; + reuqest[7] = addressToString(tx.origin); + + requests[increment] = serializeStringArray(reuqest); + } + + function getInterchainRequests(uint256 _num) public view + returns(string memory) + { + if(currentIndex == increment) { + return NULL_FLAG; + } + + uint256 num = _num < (increment - currentIndex) ? _num : (increment - currentIndex); + + string[] memory tempRequests = new string[](num); + for(uint256 i = 0; i < num; i++){ + tempRequests[i] = requests[currentIndex+i+1]; + } + + return serializeStringArray(tempRequests); + } + + function updateCurrentRequestIndex(uint256 _index) public + { + if(currentIndex < _index) { + currentIndex = _index; + } + } + + // _result is json form of arrays + function registerCallbackResult(string memory _uid, string memory _tid, string memory _seq, string memory _errorCode, string memory _errorMsg, string[] memory _result) public + { + string[5] memory result = [_tid, _seq, _errorCode, _errorMsg, serializeStringArray(_result)]; + callbackResults[_uid] = result; + } + + function selectCallbackResult(string memory _uid) public view + returns(string[] memory) + { + return callbackResults[_uid]; + } + + function serializeStringArray(string[] memory _arr) internal pure + returns(string memory jsonStr) + { + uint len = _arr.length; + if(len == 0) { + return "[]"; + } + + jsonStr = '['; + for (uint i = 0; i < len - 1; i++) { + jsonStr = string(abi.encodePacked(jsonStr, '"')); + jsonStr = string(abi.encodePacked(jsonStr, jsonEscape(_arr[i]))); + jsonStr = string(abi.encodePacked(jsonStr, '",')); + } + + jsonStr = string(abi.encodePacked(jsonStr, '"')); + jsonStr = string(abi.encodePacked(jsonStr, jsonEscape(_arr[len - 1]))); + jsonStr = string(abi.encodePacked(jsonStr, '"')); + jsonStr = string(abi.encodePacked(jsonStr, ']')); + } + + function jsonEscape(string memory _str) internal pure + returns(string memory) + { + bytes memory bts = bytes(_str); + uint256 len = bts.length; + bytes memory temp = new bytes(len * 2); + uint256 i = 0; + uint256 j = 0; + for(; j 0) { + result = bytes32(uint(result) / (2 ** 8)); + result |= bytes32(((_value % 10) + 48) * 2 ** (8 * 31)); + _value /= 10; + } + return bytes32ToString(result); + } + + function bytes32ToString(bytes32 _bts32) internal pure + returns (string memory) + { + + bytes memory result = new bytes(_bts32.length); + + uint len = _bts32.length; + for(uint i = 0; i < len; i++) { + result[i] = _bts32[i]; + } + + return string(result); + } + + function addressToString(address _addr) internal pure + returns (string memory) + { + bytes memory result = new bytes(40); + for (uint i = 0; i < 20; i++) { + byte temp = byte(uint8(uint(_addr) / (2 ** (8 * (19 - i))))); + byte b1 = byte(uint8(temp) / 16); + byte b2 = byte(uint8(temp) - 16 * uint8(b1)); + result[2 * i] = convert(b1); + result[2 * i + 1] = convert(b2); + } + return string(abi.encodePacked("0x", string(result))); + } + + function convert(byte _b) internal pure + returns (byte) + { + if (uint8(_b) < 10) { + return byte(uint8(_b) + 0x30); + } else { + return byte(uint8(_b) + 0x57); + } + } + } + ``` + +- Golang版 + + ```go + /* + * v1.0.0 + * hub contract for WeCross + * main entrance of interchain call + */ + + package main + + import ( + "bytes" + "encoding/json" + "fmt" + "github.com/hyperledger/fabric/core/chaincode/shim" + "github.com/hyperledger/fabric/protos/peer" + "strconv" + ) + + const ( + Version = "v1.0.0" + NilFlag = "null" + CallTypeQuery = "0" + CallTypeInvoke = "1" + CallTypeGetBlock = "2" + + ChannelKey = "channel" + CurrentIndexKey = "current_index" + IncrementKey = "increment" + RequestsKey = "request_%s" // %s: uid + CallbackResultsKey = "callback_results_%s" // %s: uid + ) + + type Hub struct { + } + + func (h *Hub) Init(stub shim.ChaincodeStubInterface) (res peer.Response) { + defer func() { + if r := recover(); r != nil { + res = shim.Error(fmt.Sprintf("%v", r)) + } + }() + + _, args := stub.GetFunctionAndParameters() + if len(args) != 1 { + return shim.Error("invalid arguments, [channel] expected") + } + + err := stub.PutState(ChannelKey, []byte(args[0])) + checkError(err) + + err = stub.PutState(IncrementKey, []byte("0")) + checkError(err) + + err = stub.PutState(CurrentIndexKey, []byte("0")) + checkError(err) + + return shim.Success(nil) + } + + func (h *Hub) Invoke(stub shim.ChaincodeStubInterface) (res peer.Response) { + defer func() { + if r := recover(); r != nil { + res = shim.Error(fmt.Sprintf("%v", r)) + } + }() + + fcn, args := stub.GetFunctionAndParameters() + + switch fcn { + case "getVersion": + res = h.getVersion() + case "getIncrement": + res = h.getIncrement(stub) + case "getInterchainRequests": + res = h.getInterchainRequests(stub, args) + case "updateCurrentRequestIndex": + res = h.updateCurrentRequestIndex(stub, args) + case "interchainInvoke": + res = h.interchainInvoke(stub, args) + case "interchainQuery": + res = h.interchainQuery(stub, args) + case "interchainGetBlock": + res = h.interchainGetBlock(stub, args) + case "registerCallbackResult": + res = h.registerCallbackResult(stub, args) + case "selectCallbackResult": + res = h.selectCallbackResult(stub, args) + default: + res = shim.Error("invalid function name") + } + + return + } + + func (h *Hub) getVersion() peer.Response { + return shim.Success([]byte(Version)) + } + + func (h *Hub) getIncrement(stub shim.ChaincodeStubInterface) peer.Response { + increment, err := stub.GetState(IncrementKey) + checkError(err) + + return shim.Success(increment) + } + + /* + * invoke other chain + * @args path || method || args || callbackPath || callbackMethod + */ + func (h *Hub) interchainInvoke(stub shim.ChaincodeStubInterface, args []string) peer.Response { + if len(args) != 5 { + return shim.Error("incorrect number of arguments, expecting 5") + } + + uid := handleRequest(stub, CallTypeInvoke, args[0], args[1], args[2], args[3], args[4]) + + return shim.Success(uint64ToBytes(uid)) + } + + // query other chain, not support right now + func (h *Hub) interchainQuery(stub shim.ChaincodeStubInterface, args []string) peer.Response { + if len(args) != 5 { + return shim.Error("incorrect number of arguments, expecting 5") + } + + uid := handleRequest(stub, CallTypeQuery, args[0], args[1], args[2], args[3], args[4]) + + return shim.Success(uint64ToBytes(uid)) + } + + func (h *Hub) interchainGetBlock(stub shim.ChaincodeStubInterface, args []string) peer.Response { + if len(args) != 5 { + return shim.Error("incorrect number of arguments, expecting 5") + } + + uid := handleRequest(stub, CallTypeGetBlock, args[0], args[1], args[2], args[3], args[4]) + + return shim.Success(uint64ToBytes(uid)) + } + + func handleRequest(stub shim.ChaincodeStubInterface, callType, path, method, args, callbackPath, callbackMethod string) uint64 { + increment, err := stub.GetState(IncrementKey) + checkError(err) + + uid := bytesToUint64(increment) + 1 + creator, err := stub.GetCreator() + checkError(err) + + certStart := bytes.IndexAny(creator, "-----BEGIN") + if certStart == -1 { + panic("no certificate found") + } + + request := []string{string(uint64ToBytes(uid)), callType, path, method, args, callbackPath, callbackMethod, string(creator[certStart:])} + + requestData, err := json.Marshal(request) + checkError(err) + + err = stub.PutState(IncrementKey, uint64ToBytes(uid)) + checkError(err) + + err = stub.PutState(getRequestsKey(string(uint64ToBytes(uid))), requestData) + checkError(err) + + return uid + } + + /* + * @args uid || tid || seq || errorCOde || errorMsg || result + * result is json form of arrays + */ + func (h *Hub) registerCallbackResult(stub shim.ChaincodeStubInterface, args []string) peer.Response { + if len(args) != 6 { + return shim.Error("incorrect number of arguments, expecting 6") + } + + result := []string{args[1], args[2], args[3], args[4], args[5]} + + resultData, err := json.Marshal(result) + checkError(err) + + err = stub.PutState(getCallbackResultsKey(args[0]), resultData) + checkError(err) + + return shim.Success(nil) + } + + func (h *Hub) selectCallbackResult(stub shim.ChaincodeStubInterface, args []string) peer.Response { + if len(args) != 1 { + return shim.Error("incorrect number of arguments, expecting 1") + } + + uid := args[0] + result, err := stub.GetState(getCallbackResultsKey(uid)) + checkError(err) + + return shim.Success(result) + } + + func (h *Hub) getInterchainRequests(stub shim.ChaincodeStubInterface, args []string) peer.Response { + if len(args) != 1 { + return shim.Error("incorrect number of arguments, [num] expected") + } + + increment, err := stub.GetState(IncrementKey) + checkError(err) + + currentIndex, err := stub.GetState(CurrentIndexKey) + checkError(err) + + total := bytesToUint64(increment) + current := bytesToUint64(currentIndex) + if total == current { + return shim.Success([]byte(NilFlag)) + } + + num := bytesToUint64([]byte(args[0])) + var realNum uint64 + + if num < (total - current) { + realNum = num + } else { + realNum = total - current + } + + var tempRequests []string + var i uint64 + for i = 0; i < realNum; i++ { + request, err := stub.GetState(getRequestsKey(string(uint64ToBytes(current + i + 1)))) + checkError(err) + tempRequests = append(tempRequests, string(request)) + } + + requestsData, err := json.Marshal(tempRequests) + checkError(err) + + return shim.Success(requestsData) + } + + func (h *Hub) updateCurrentRequestIndex(stub shim.ChaincodeStubInterface, args []string) peer.Response { + if len(args) != 1 { + return shim.Error("incorrect number of arguments, [uid] expected") + } + + increment, err := stub.GetState(IncrementKey) + checkError(err) + + currentIndex, err := stub.GetState(CurrentIndexKey) + checkError(err) + + total := bytesToUint64(increment) + current := bytesToUint64(currentIndex) + index := bytesToUint64([]byte(args[0])) + + if current < index && index <= total { + err = stub.PutState(CurrentIndexKey, []byte(args[0])) + checkError(err) + } + return shim.Success(nil) + } + + func getRequestsKey(uid string) string { + return fmt.Sprintf(RequestsKey, uid) + } + + func getCallbackResultsKey(uid string) string { + return fmt.Sprintf(CallbackResultsKey, uid) + } + + func bytesToUint64(bts []byte) uint64 { + u, err := strconv.ParseUint(string(bts), 10, 64) + checkError(err) + + return u + } + + func uint64ToBytes(u uint64) []byte { + return []byte(strconv.FormatUint(u, 10)) + } + + func checkError(err error) { + if err != nil { + panic(err) + } + } + + func main() { + err := shim.Start(new(Hub)) + if err != nil { + fmt.Printf("Error: %s", err) + } + } + + ``` + +### 2.2 java 插件 + +#### 2.2.1 java 核心组件 + +#### Java核心组件 + +- StubFactory + - 组件实例化工厂类 +- Account + - 区块链账户,用于交易签名 +- Connection + - 调用区块链SDK接口,与区块链交互 +- Driver + - 交易、交易回执、区块等与区块链相关数据的编解码 + - 实现`Stub`的基础接口 + - 调用`Connection`对象的发送入口与区块链交互 + +##### StubFactory + +- 功能描述: + - 添加`@Stub`注解,定义插件类型 + - 提供`Account`、`Connection`、`Driver`实例化入口 + - @Stub定义了插件类型,添加@Stub注解的插件才能被Wecross Router识别加载 + +- 接口定义 + +``` +public interface StubFactory { + public Driver newDriver(); + public Connection newConnection(String path); + public Account newAccount(Map properties); +} +``` + +- 接口列表 + - newDriver + - 实例化`Driver`对象 + - newConnection + - 实例化`Connection`对象 + - `path`:配置文件路径,配置文件名称默认`stub.toml` + - newAccount + - 实例化`Account`对象 + - `properties`:`Account`对象实例化的参数 +- `FISCO-BCOS StubFactory`示例 + +``` +/** @Stub注解,插件类型: BCOS2.0 */ +@Stub("BCOS2.0") +public class BCOSStubFactory implements StubFactory { + @Override + public Driver newDriver() { + Driver driver = new BCOSDriver(); + /** 其他逻辑 */ + return driver; + } + @Override + public Connection newConnection(String path) { + Connection connection = new BCOSConnection(); + /** 解析配置文件,初始化 BCOSConnection */ + return connection; + } + @Override + public Account newAccount(Map properties) { + Account account = new BCOSAccount(); + /** 根据properties参数,初始化 BCOSAccount */ + return account; + } +} +``` + +#### 2.2.2 Account + +- 功能描述 + - 交易签名 + - 链账户签名、验签 +- 接口定义 + +``` +public interface Account { + String getName(); + String getType(); + String getIdentity(); + int getKeyID(); + boolean isDefault(); +} +``` + +- 接口列表 + - getName + - 链账户名称,自定义 + - getType + - 链账户类型,与Stub类型保持一致 + - getIdentify + - 链账户标记符,通常为链账户公钥 + - isDefault + - 当前账户是否为默认链账户 + - getKeyID + - 链账户KeyID + +#### 2.2.3 Connection + +- 功能描述 + - 解析配置文件,初始化区块链`JavaSDK`,参考下面`配置文件`小节 + - 为`Driver`提供统一的发送接口,与区块链进行交互 + - 获取链上的资源列表 +- 接口定义 + +``` +public interface Connection { + Response send(Request request); + List getResources(); +} +``` + +接口列表 + +- `getResources` + + - 获取区块链上的资源列表 + + ``` + /** 资源对象 */ + public class ResourceInfo { + /** 资源名称 */ + private String name; + /** 资源类型,用户自定义 */ + private String stubType; + /** 资源属性 */ + private Map properties = new HashMap(); + } + ``` + +- `send` + + - 发送接口 + + - `Request request`: 请求对象,包括请求类型、请求内容 + + ``` + public class Request { + // 请求类型,自定义类型 + private int type; + // 请求内容,序列化的请求参数 + private byte[] data; + } + ``` + + - `Response response`: 返回对象,包括返回状态、描述信息、返回内容 + + ``` + public class Response { + // 返回状态码 + private int errorCode; + // 描述信息 + private String errorMessage; + // 返回内容,序列化的返回参数 + private byte[] data; + } + ``` + +`FISCO-BCOS BCOSConnection`示例: + +``` +// Request type定义,自定义 +public class BCOSRequestType { + // 查询操作 + public static final int CALL = 1000; + // 发送交易 + public static final int SEND_TRANSACTION = 1001; + // 获取块高 + public static final int GET_BLOCK_NUMBER = 1002; + // 获取区块 + public static final int GET_BLOCK_BY_NUMBER = 1003; + // 获取交易证明 + public static final int GET_TRANSACTION_PROOF = 1004; +} + +// Connection定义各个类型消息的处理方式 +public class BCOSConnection implements Connection { + /** 发送入口,区分消息类型,调用区块链RPC接口 */ + @Override + public Response send(Request request) { + switch (request.getType()) { + /** 查询 */ + case BCOSRequestType.CALL: + /** call请求 */ + break; + /** 发送交易 */ + case BCOSRequestType.SEND_TRANSACTION: + /** sendTransaction请求 */ + break; + /** 获取区块头 */ + case BCOSRequestType.GET_BLOCK_NUMBER: + /** 获取区块高度请求 */ + break; + /** 获取块高 */ + case BCOSRequestType.GET_BLOCK_BY_NUMBER: + /** 获取区块 */ + break; + /** 获取交易证明 */ + case BCOSRequestType.GET_TRANSACTION_PROOF: + /** 获取交易证明 */ + break; + } + } +} +``` + +- 配置文件 配置文件主要包括区块链`JavaSDK`初始化需要的参数,也可以包含其他的一些附加信息,由用户自定义。配置默认位于`chains/`目录,可以配置多个stub,每个stub位于单独的子目录,配置文件名称`stub.toml`。 + + ``` + # 目录结构, conf/chains/stub名称/ + conf/chains/ + └── bcos # stub名称: bcos + └── stub.toml # stub.toml配置文件 + # 其他文件列表,比如:证书文件 + ``` + +`stub.toml`解析流程可以参考[FISCO-BCOS Stub stub.toml解析](https://github.com/WeBankBlockchain/WeCross-BCOS2-Stub/blob/dev/src/main/java/com/webank/wecross/stub/bcos/config/BCOSStubConfigParser.java) + +FISCO-BCOS stub.toml示例 + +``` +[common] # 通用配置 + name = 'bcos' # 名称,必须项 + type = 'BCOS2.0' # 必须项,插件类型,与插件@Stub注解定义的类型保持一致 + + +[chain] # FISCO-BCOS 属性 + groupId = 1 # default 1 + chainId = 1 # default 1 + +[channelService] # FISCO-BCOS JavaSDK配置 + caCert = 'ca.crt' + sslCert = 'sdk.crt' + sslKey = 'sdk.key' + timeout = 300000 # 超时时间 + connectionsStr = ['127.0.0.1:20200', '127.0.0.1:20201', '127.0.0.1:20202'] # 连接列表 +``` + +#### 2.2.4 Driver + +- 功能描述 + - 发送交易 + - 状态查询 + - 查询块高 + - 查询区块 + - 获取交易证明 + - 交易、区块编解码 + - 验证交易 + - 查询资源列表 +- 接口定义 + +``` +public interface Driver { + interface Callback { + void onTransactionResponse( + TransactionException transactionException, TransactionResponse transactionResponse); + } + + ImmutablePair decodeTransactionRequest(Request request); + + List getResources(Connection connection); + + void asyncCall( + TransactionContext context, + TransactionRequest request, + boolean byProxy, + Connection connection, + Driver.Callback callback); + + void asyncSendTransaction( + TransactionContext context, + TransactionRequest request, + boolean byProxy, + Connection connection, + Driver.Callback callback); + + interface GetBlockNumberCallback { + void onResponse(Exception e, long blockNumber); + } + + void asyncGetBlockNumber(Connection connection, GetBlockNumberCallback callback); + + interface GetBlockCallback { + void onResponse(Exception e, Block block); + } + + void asyncGetBlock( + long blockNumber, boolean onlyHeader, Connection connection, GetBlockCallback callback); + + interface GetTransactionCallback { + void onResponse(Exception e, Transaction transaction); + } + + void asyncGetTransaction( + String transactionHash, + long blockNumber, + BlockManager blockManager, + boolean isVerified, + Connection connection, + GetTransactionCallback callback); + + interface CustomCommandCallback { + void onResponse(Exception error, Object response); + } + + void asyncCustomCommand( + String command, + Path path, + Object[] args, + Account account, + BlockManager blockManager, + Connection connection, + CustomCommandCallback callback); + + byte[] accountSign(Account account, byte[] message); + + boolean accountVerify(String identity, byte[] signBytes, byte[] message); +} +``` + +- 接口列表: + - asyncCall + - asyncSendTransaction 状态查询/发送交易 + - TransactionContext context + - 请求上下文,交易的上下文,包含交易的附属信息 + - TransactionRequest request + - boolean byProxy + - 是否通过代理合约查询状态/发送交易 + - Connection connection + - 发送请求 + - Driver.Callback callback + - 回调返回 + - asyncGetBlockNumber 获取区块高度 + - Connection connection + - 发送请求 + - GetBlockNumberCallback callback + - 回调返回 + - asyncGetBlock 获取区块 + - long blockNumber + - 区块高度 + - boolean onlyHeader + - 是否只获取区块头 + - Connection connection + - 发送请求 + - GetBlockCallback callback + - 回调返回 + - asyncGetTransaction 获取交易,并且对交易进行合法性验证 + - String transactionHash + - 交易hash + - long blockNumber + - 区块高度 + - BlockManager blockManager + - 区块管理对象,用于获取区块信息,可以使用区块头部的状态信息校验交易是否合法 + - boolean isVerified + - 是否校验交易 + - Connection connection + - 发送请求 + - GetTransactionCallback callback + - 回调返回 + - asyncCustomCommand 用户自定义其他接口 + - String command + - 命令 + - Path path + - 资源 + - Object[] args + - 参数列表 + - Account account + - 账户 + - BlockManager blockManager + - 区块管理对象 + - Connection connection + - 发送请求 + - CustomCommandCallback callback + - 回调返回 + - accountSign 链账户`Account`对消息进行签名,返回序列化之后的签名对象 + - Account account + - 签名账户 + - byte[] message + - 代签名的消息 + - accountVerify 链账户验签 + - String identity + - byte[] signBytes + - 签名对象,`accountSign`的返回值 + - byte[] message + - 签名的原始消息 + +## 三、开发模版 + +跨链服务提供一个`Java`模板工程,加快用户开发 `Stub` 的速度,用户仅需要进行少量的修改。 + +**获取:** + +``` +git clone https://github.com/WeBankBlockchain/WeCross-Stub-Dev-Template.git +``` + +**目录结构:** + +``` +WeCross-Stub-Dev-Template +├── README.md +├── build.gradle +└── src + ├── main + │ ├── java + │ │ └── wecross + │ │ └── stub + │ │ └── demo ## Java核心组件,参考上文各个组件的介绍 + │ │ ├── DemoAccount.java # Account + │ │ ├── DemoConnection.java # Connection + │ │ ├── DemoDriver.java # Driver + │ │ └── DemoStubFactory.java # StubFactory + │ └── resources + └── test + ├── java + │ └── wecross + │ └── stub + │ └── demo + │ └── DemoStubTest.java + └── resources +``` + +**编译:** + +``` +cd WeCross-Stub-Dev-Template +bash gradlew build + +$ tree -L 1 dist/apps +dist/apps +└── WeCross-Stub-Dev-Template-1.0.0-SNAPSHOT.jar +``` + +## 四、参考链接 + +- [WeCross-BCOS3-Stub](https://github.com/WeBankBlockchain/WeCross-BCOS3-Stub) + +- [WeCross-BCOS2-Stub](https://github.com/WeBankBlockchain/WeCross-BCOS2-Stub) + +- [WeCross-Fabric-Stub](https://github.com/WeBankBlockchain/WeCross-Fabric1-Stub) + +## 五、注意事项 + +1. 国密版本的长安链插件和 Fisco Bcos 链插件,由于官方都对 netty 包做过适配改造,因此这两个插件如果部署在统一跨链服务会导致冲突。目前解决的方案是将两个插件部署在两个不同的跨链服务上; + +2. 跨链服务如果要同时支持国密版本的长安链和 Fisco Bcos 链插件,需要使用不同的分支管理编译,并且支持国密版本 Fisco Bcos 链插件需要在 `build.grandle` 里增加下面的依赖: + + ``` + dependencies { + constraints { + compile group: 'io.netty', name: 'netty-all', version: '4.1.77.Final' + compile 'io.netty:netty-codec-haproxy:4.1.89.Final' + compile group: 'org.fisco-bcos', name: 'tcnative', version: '2.0.51.0' + } + compile 'org.fisco-bcos:tcnative' + } + ``` + + \ No newline at end of file diff --git a/docs/images/bmsp-cross.jpg b/docs/images/bmsp-cross.jpg new file mode 100644 index 000000000..e73848eb5 Binary files /dev/null and b/docs/images/bmsp-cross.jpg differ diff --git "a/docs/\350\267\250\351\223\276\345\272\224\347\224\250\345\274\200\345\217\221.md" "b/docs/\350\267\250\351\223\276\345\272\224\347\224\250\345\274\200\345\217\221.md" new file mode 100644 index 000000000..4c6b7efb0 --- /dev/null +++ "b/docs/\350\267\250\351\223\276\345\272\224\347\224\250\345\274\200\345\217\221.md" @@ -0,0 +1,404 @@ +# 跨链应用开发 + +## 一、 Java SDK 开发应用 + +跨链路由向外部暴露了所有的UBI接口,开发者可以通过SDK实现这些接口的快速调用。 + +### 1.1 环境要求 + +- java 版本:[JDK8或以上](https://openjdk.java.net/) + +### 1.2 应用引入SDK + +通过 gradle 或 maven 引入 SDK 到 java 项目: + +- gradle + + ``` + compile('com.webank:wecross-java-sdk:1.3.0') + ``` + +- maven + + ``` + + com.webank + wecross-java-sdk + 1.3.0 + + ``` + +### 1.3 配置 SDK + +跨链 java SDK 主要包括以下的配置选项: + +- 路由网络配置 +- SSL 连接配置 + +#### 1.3.1 配置步骤 + +1. 将跨链路由`router-${zone}/cert/sdk/`目录下的证书和密钥拷贝到项目的`classpath`目录; + +2. 在项目的`classpath`目录下新建文件 `application.toml`,默认配置如下: + + ``` + [connection] + server = '127.0.0.1:8250' + sslKey = 'classpath:ssl.key' + sslCert = 'classpath:ssl.crt' + caCert = 'classpath:ca.crt' + sslSwitch = 2 # disable ssl:2, SSL without client auth:1 , SSL with client and server auth: 0 + ``` + + 修改`application.toml`中`server`,使用跨链路由实际监听的IP和端口。 + +#### 1.3.2 配置解读 + +在`application.toml`配置文件中,`[connection]`配置跨链路由的连接信息,具体包括以下配置项: + +- `server`: 跨链路由监听的IP和端口; +- `sslKey`: 客户端私钥路径 +- `sslCert`: 客户端证书路径 +- `caCert`: CA证书路径 +- `sslSwitch`: SSL的模式开关,0: 双向验证;1: 只验证服务端;2: 关闭SSL。 + +#### 1.3.3 使用方法: + +示例代码如下: + +```java +try { + // 初始化 Service + WeCrossRPCService weCrossRPCService = new WeCrossRPCService(); + + // 初始化Resource + WeCrossRPC weCrossRPC = WeCrossRPCFactory.build(weCrossRPCService); + + weCrossRPC.login("username", "password").send(); // 需要有登录态才能进一步操作 + Resource resource = ResourceFactory.build(weCrossRPC, "cross.fabric2.HelloWecross"); // RPC服务,资源的path + + // 用初始化好的resource进行调用 + String[] callRet = resource.call("get"); // call 接口函数名 参数列表 + System.out.println((Arrays.toString(callRet))); + + // 用初始化好的resource进行调用 + String[] sendTransactionRet = resource.sendTransaction("set", "Tom"); // sendTransaction 接口函数名 参数列表 + System.out.println((Arrays.toString(sendTransactionRet))); + +} catch (WeCrossSDKException e) { + System.out.println("Error: " + e); +} +``` + + + +## 二、 Java SDK API + +SDK API分为两大类型,一种是对跨链路由RPC接口调用的封装,一种是资源接口。 + +## 2.1 API 列表 + +- RPC 封装接口 + + ``` + // 支持的链类型 + RemoteCall supportedStubs(); + // 获取链账户列表 + RemoteCall listAccount(); + // 获取资源列表 + RemoteCall listResources(Boolean ignoreRemote); + // 获取资源详细信息 + RemoteCall detail(String path); + // 调用指定资源的某个方法(查询) + RemoteCall call(String path, String method, String... args); + // 调用指定资源的某个方法 + RemoteCall sendTransaction(String path, String method, String... args); + // 发送用户自定义指令 + RemoteCall customCommand(String command, String path, Object... args); + // 注册统一管理账户 + RemoteCall register(String name, String password) throws WeCrossSDKException; + // 登录跨链服务 + RemoteCall login(String name, String password); + // 登出跨链服务 + RemoteCall logout(); + // 添加链账户 + RemoteCall addChainAccount(String type, ChainAccount chainAccount); + // 设置某个链账户为默认操作账户 + RemoteCall setDefaultAccount(String type, ChainAccount chainAccount); + // 通过 keyID 设置默认操作链账户 + RemoteCall setDefaultAccount(String type, Integer keyID); + ``` + +- 资源接口 + + ``` + // 创建资源 + Resource ResourceFactory.build(WeCrossRPC weCrossRPC, String path, String account) + // 资源十分激活可用 + boolean isActive(); + // 资源详细信息 + ResourceDetail detail(); + // 调用资源中的某个方法(查询) + String[] call(String method); + // 调用资源中的某个方法(查询) + String[] call(String method, String... args); + // 调用资源中的某个方法 + String[] sendTransaction(String method); + // 调用资源中的某个方法 + String[] sendTransaction(String method, String... args); + ``` + +## 2.2 RPC 封装接口解析 + +#### 2.2.1 supportedStubs + +#### 参数 + +- 无 + +#### 返回值 + +- `StubResponse` - 响应包 + - `version`: `String` - 版本号 + - `errorCode`: `int` - 状态码 + - `message`: `String` - 错误消息 + - `data`: `Stubs` - 支持的插件列表 + +#### java示例 + +``` +// 初始化RPC实例 +WeCrossRPCService weCrossRPCService = new WeCrossRPCService(); +WeCrossRPC weCrossRPC = WeCrossRPCFactory.build(weCrossRPCService); + +// 调用RPC接口,目前只支持同步调用 +StubResponse response = weCrossRPC.supportedStubs().send(); +``` + +#### 2.2.2 listAccount + +查看当前全局账号的详细信息。 + +#### 参数 + +- 无 + +#### 返回值 + +- `AccountResponse` - 响应包 + - `version`: `String` - 版本号 + - `errorCode`: `int` - 状态码 + - `message`: `String` - 错误消息 + - `data`: `UniversalAccount` - 账号详细信息 + +#### java示例 + +``` +AccountResponse response = weCrossRPC.listAccounts().send(); +``` + +#### 2.2.3 listResources + +显示router配置的跨链资源。 + +#### 参数 + +- `ignoreRemote`: `Boolean` - 是否忽略远程资源 + +#### 返回值 + +- `ResourceResponse` - 响应包 + - `version`: `String` - 版本号 + - `errorCode`: `int` - 状态码 + - `message`: `String` - 错误消息 + - `data`: `Resources` - 配置的资源列表 + +#### java示例 + +``` +ResourceResponse response = weCrossRPC.listResources(true).send(); +``` + +#### 2.2.4 detail + +获取资源详情。 + +#### 参数 + +- `path`: `String` - 跨链资源标识 + +#### 返回值 + +- `ResourceDetailResponse` - 响应包 + - `version`: `String` - 版本号 + - `errorCode`: `int` - 状态码 + - `message`: `String` - 错误消息 + - `data`: `ResourceDetail` - 资源详情 + +#### java示例 + +``` +ResourceDetailResponse response = weCrossRPC.detail("payment.bcos.HelloWeCross").send(); +``` + +#### 2.2.5 call + +调用智能合约,不更改链状态,不发交易。 + +#### 参数 + +- `path`: `String` - 跨链资源标识 +- `method`: `String` - 调用的方法 +- `args` : `String...` - 可变参数列表 + +#### 返回值 + +- `TransactionResponse` - 响应包 + - `version`: `String` - 版本号 + - `errorCode`: `int` - 状态码 + - `message`: `String` - 错误消息 + - `data`: `Receipt` - 调用结果 + +#### java示例 + +``` +TransactionResponse transactionResponse = +weCrossRPC +.call( +"payment.bcos.HelloWeCross","get","key") +.send(); +``` + +#### 2.2.6 sendTransaction + +调用智能合约,会改变链状态,发交易。 + +#### 参数 + +- `path`: `String` - 跨链资源标识 +- `method`: `String` - 调用的方法 +- `args` : `String...` - 可变参数列表 + +#### 返回值 + +- `TransactionResponse` - 响应包 + - `version`: `String` - 版本号 + - `errorCode`: `int` - 状态码 + - `message`: `String` - 错误消息 + - `data`: `Receipt` - 调用结果 + +#### java示例 + +``` +TransactionResponse transactionResponse = + weCrossRPC + .invoke( + "payment.bcos.HelloWeCross","set","value") + .send(); +``` + +#### 2.2.7 customCommand + +自定义命令 + +#### 参数 + +- `command`: `String` - 命令名称 +- `path`: `String` - 跨链资源标识 +- `args`: `Object...` - 可变参数 + +#### 返回值 + +- `CommandResponse` - 响应包 + - `version`: `String` - 版本号 + - `errorCode`: `int` - 状态码 + - `message`: `String` - 错误消息 + - `data`: `String` - 调用结果 + +#### java示例 + +``` +CommandResponse commandResponse = + weCrossRPC + .customCommand( + "deploy", "payment.bcos.evidence", "Evidence") + .send(); +``` + +#### 2.2.8 register + +注册一个统一管理账号 + +#### 参数 + +- `name`: `String` - 账号名 +- `password`: `String` - 账号密码 + +#### 返回值 + +- `UAResponse` - 响应包 + - `version`: `String` - 版本号 + - `errorCode`: `int` - 状态码 + - `message`: `String` - 错误消息 + - `data`: `UAReceipt` - 注册账号信息 + +#### java示例 + +``` +UAResponse uaResponse = + weCrossRPC + .register( + "org1-admin", "123456").send(); +``` + +#### 2.2.9 login + +登录一个统一管理账号 + +#### 参数 + +- `name`: `String` - 账号名 +- `password`: `String` - 账号密码 + +#### 返回值 + +- `UAResponse` - 响应包 + - `version`: `String` - 版本号 + - `errorCode`: `int` - 状态码 + - `message`: `String` - 错误消息 + - `data`: `UAReceipt` - 登录账号信息 + +#### java示例 + +``` +UAResponse uaResponse = + weCrossRPC + .login( + "admin", "123456").send(); +``` + +#### 2.2.10 addChainAccount + +添加一个链账号。 + +#### 参数 + +- `type`: `String` - 链类型 +- `chainAccount`: `ChainAccount` - 链账号 + +#### 返回值 + +- `UAResponse` - 响应包 + - `version`: `String` - 版本号 + - `errorCode`: `int` - 状态码 + - `message`: `String` - 错误消息 + - `data`: `UAReceipt` - 添加结果 + +#### java示例 + +``` +UAResponse uaResponse = + weCrossRPC + .addChainAccount( + "GM_Fabric2.0", chainAccount).send(); +``` \ No newline at end of file diff --git "a/docs/\351\223\276\346\217\222\344\273\266\351\203\250\347\275\262.md" "b/docs/\351\223\276\346\217\222\344\273\266\351\203\250\347\275\262.md" new file mode 100644 index 000000000..c27623c64 --- /dev/null +++ "b/docs/\351\223\276\346\217\222\344\273\266\351\203\250\347\275\262.md" @@ -0,0 +1,144 @@ +# 1. 前提说明 + +当前部署的 `bmsp-cross` 的服务目录结构如下: + +![bmsp-cross](./images/bmsp-cross.jpg) + +目前跨链服务支持飞梭链、长安链和fabric2.0链 + + + +# 2. 部署现有链插件 + +以更新长安链为例说明。 + +1. 将编译的长安链插件拷贝至 `bmsp-cross/chainmaker/plugin` 目录下,使用下面的命令重新启动 `bmsp-cross` 服务。 + +```bash +$ docker compose stop bmsp-cross +$ docker compose up -d bmsp-cross +``` + +2. 进入数据可信服务系统,依次选择多链管理->链接入管理。点击接入区块链按钮 +3. 选择链类型长安链-国密 +4. 填写链名称,比如chainmaker_qchain +5. 链ID,需要和部署的链id保持一致,比如 qchain +6. 组织ID,需要和连接节点的组织id保持一致,比如digital +7. 上传组织证书 +8. 填写连接节点的地址,比如127.0.0.1:12301 +9. 输入用户ID,比如onehaha +10. 输入用户名称,比如admin +11. 上传签名证书、签名私钥、TLS证书和TLS私钥 + +# 3. 接入全新的链插件 + +如果开发的链插件不是飞梭链、长安链和fabric2.0链。 + +在开发的过程中,如果要支持国密版本的话,有很大的可能引用的 jar 和现有的 bcos、chainmaker链插件的 jar 有冲突。 + +下面分为有冲突和没有冲突的两种情况说明链插件的部署。 + +## 3.1 无冲突 + +1. 按照规范开发编译链插件,比如编译好的链插件名称为:`eth-gm-plugin.jar` + +2. 将 `eth-gm-plugin.jar` 包拷贝至 `fabirc2/plugin` 目录下(或`bcos2/plugin|chainmaker/plugin`) + +3. 重新启动 `bmsp-cross` 服务 + +4. 进入数据可信服务系统,依次选择系统管理->字典管理,点击字典类型为 `sys_chain_stub_type` 的标签 + +5. 点击增加按钮,增加插件类型 + + 数据标签:比如以太坊链 + + 数据键值:gm-eth + + 显示排序:2 + +6. 进入 `nacos` 配置中心(http://localhost:8848/nacos) + +7. 选择配置列表->prod,点击编辑`application-common.yml`,增加一下配置 + + ```yam + # 如果2步骤中将插件拷贝至 fabirc2/plugin 目录下 + wecross-router: + servers: + gm-eth: "bmsp-cross:8251" + + # 如果2步骤中将插件拷贝至 chainmaker/plugin 目录下 + wecross-router: + servers: + gm-eth: "bmsp-cross:8250" + + # 如果2步骤中将插件拷贝至 bcos2/plugin 目录下 + wecross-router: + servers: + gm-eth: "bmsp-cross:8252" + ``` + +8. 进入数据可信服务系统,依次选择多链管理->链接入管理。点击接入区块链按钮 + +9. 链类型选择以太坊链,以及添加以太坊链配置信息 + +## 3.2 有冲突 + +1. 按照规范开发编译链插件,比如编译好的链插件名称为:`eth-gm-plugin.jar` + +2. 在 `bmsp-cross` 目录下创建一个新目录,比如 `eth-chain` + +3. 将 `bmsp-cross/chainmaker` 相关目录拷贝至 `eth-chain` 目录下 + + ```bash + $ cp -r bmsp-cross/chainmaker/apps ./eth-chain + $ cp -r bmsp-cross/chainmaker/lib ./eth-chain/ + $ cp -r bmsp-cross/chainmaker/conf ./eth-chain/ # 不包括 bmsp-cross/chainmaker/conf/chains 目录下的所有文件和目录 + $ mkdir -p ./eth-chain/plugin + ``` + +4. 将 `eth-gm-plugin.jar` 拷贝至 `./eth-chain/plugin` 目录 + +5. 编辑 `./eth-chain/conf/wecross.toml` 配置文件,将 `rpc.port` 和 `p2p.listenPort` 分别更改为没有被系统占用的端口,比如 `8253` 和 `25503` + +6. 编辑 `docker-compose.yaml` 文件,增加下面的配置: + + ``` + bmsp-cross: + image: openjdk:8u342-jre + container_name: bmsp-cross + environment: + # 时区上海 + TZ: Asia/Shanghai + LANG: zh_CN.utf8 + ports: + - "8253:8253" + - "25503:25503" + volumes: + # 配置文件 + - ./bmsp-cross/eth-chain:/wecross-router/eth-chain + +7. 重启 `bmsp-cross` 服务 + +8. 进入数据可信服务系统,依次选择系统管理->字典管理,点击字典类型为 `sys_chain_stub_type` 的标签 + +9. 点击增加按钮,增加插件类型 + + 数据标签:比如以太坊链 + + 数据键值:gm-eth + + 显示排序:2 + +10. 进入 `nacos` 配置中心(http://localhost:8848/nacos) + +11. 选择配置列表->prod,点击编辑`application-common.yml`,增加一下配置 + +```yaml +wecross-router: + servers: + gm-eth: "bmsp-cross:8253" +``` + +12. 进入数据可信服务系统,依次选择多链管理->链接入管理。点击接入区块链按钮 + +13. 链类型选择以太坊链,以及添加以太坊链配置信息 \ No newline at end of file diff --git a/lib/netty-tcnative-openssl-static-2.0.39.Final.jar b/lib/netty-tcnative-openssl-static-2.0.39.Final.jar new file mode 100644 index 000000000..77f487e42 Binary files /dev/null and b/lib/netty-tcnative-openssl-static-2.0.39.Final.jar differ diff --git a/scripts/add_chain.sh b/scripts/add_chain.sh index 720557a29..17483d543 100644 --- a/scripts/add_chain.sh +++ b/scripts/add_chain.sh @@ -8,7 +8,7 @@ help() { echo $1 cat < [Required] type of chain: BCOS2.0, GM_BCOS2.0, Fabric1.4, Fabric2.0, BCOS3_ECDSA_EVM, BCOS3_GM_EVM, BCOS3_ECDSA_WASM, BCOS3_GM_WASM + -t [Required] type of chain: BCOS2.0, GM_BCOS2.0, Fabric1.4, Fabric2.0, BCOS3_ECDSA_EVM, BCOS3_GM_EVM, BCOS3_ECDSA_WASM, BCOS3_GM_WASM, ChainMakerWithCert, ChainMakerGMWithCert, ChainMakerWithPublic, ChainMakerGMWithPublic -n [Required] name of chain -d [Optional] generated target_directory, default conf/chains/ -h [Optional] Help @@ -19,6 +19,7 @@ e.g bash $0 -t BCOS3_GM_EVM -n my_gm_bcos3_chain bash $0 -t Fabric1.4 -n my_fabric_chain bash $0 -t Fabric2.0 -n my_fabric_chain + bash $0 -t ChainMakerWithCert -n chainMaker EOF exit 0 diff --git a/scripts/deploy_system_contract.sh b/scripts/deploy_system_contract.sh index ad488dae9..8b1a67083 100755 --- a/scripts/deploy_system_contract.sh +++ b/scripts/deploy_system_contract.sh @@ -14,7 +14,7 @@ help() { Usage: -c [Required] chain name -u [Optional] upgrade proxy/hub contract if proxy/hub contract has been deployed, default deploy proxy/hub contract - -t [Required] type of chain, support: BCOS2.0, GM_BCOS2.0, Fabric1.4, Fabric2.0, BCOS3_ECDSA_EVM, BCOS3_GM_EVM, BCOS3_ECDSA_WASM, BCOS3_GM_WASM + -t [Required] type of chain, support: BCOS2.0, GM_BCOS2.0, Fabric1.4, Fabric2.0, BCOS3_ECDSA_EVM, BCOS3_GM_EVM, ChainMakerWithCert, ChainMakerGMWithCert, ChainMakerWithPublic, ChainMakerGMWithPublic -P [Optional] upgrade/deploy operation on proxy contract -H [Optional] upgrade/deploy operation on hub contract -h [Optional] Help @@ -35,6 +35,10 @@ e.g bash $0 -t Fabric2.0 -c chains/fabric2 -H bash $0 -t Fabric2.0 -c chains/fabric2 -u -P bash $0 -t Fabric2.0 -c chains/fabric2 -u -H + bash $0 -t ChainMakerWithCert -c chains/chainmaker -P + bash $0 -t ChainMakerWithCert -c chains/chainmaker -H + bash $0 -t ChainMakerWithCert -c chains/chainmaker -u -P + bash $0 -t ChainMakerWithCert -c chains/chainmaker -u -H EOF exit 0 @@ -69,7 +73,7 @@ bcos_proxy_contract() { "GM_BCOS2.0") packageName="bcos.guomi" ;; - "BCOS3_ECDSA_EVM" | "BCOS3_GM_EVM" | "BCOS3_ECDSA_WASM" | "BCOS3_GM_WASM") + "BCOS3_ECDSA_EVM" | "BCOS3_GM_EVM") packageName="bcos3" ;; esac @@ -93,7 +97,7 @@ bcos_hub_contract() { "GM_BCOS2.0") packageName="bcos.guomi" ;; - "BCOS3_ECDSA_EVM" | "BCOS3_GM_EVM" | "BCOS3_ECDSA_WASM" | "BCOS3_GM_WASM") + "BCOS3_ECDSA_EVM" | "BCOS3_GM_EVM") packageName="bcos3" ;; esac @@ -185,6 +189,42 @@ update_fabric2_hub_contract() { java -Djdk.tls.client.protocols=TLSv1.2 -Djava.security.properties=${SECURIY_FILE} -Djdk.sunec.disableNative=false -Djdk.tls.namedGroups="SM2,secp256k1,x25519,secp256r1,secp384r1,secp521r1,x448" -cp conf/:lib/*:plugin/* com.webank.wecross.stub.fabric2.hub.HubChaincodeDeployment upgrade "${chainName}" } +deploy_or_update_chainmaker_proxy_contract() { + local chainName="$1" + local deploy="$2" + local type="$3" + local packageName="" + case $type in + "ChainMakerWithCert" | "ChainMakerGMWithCert" | "ChainMakerWithPublic" | "ChainMakerGMWithPublic") + packageName="chainmaker" + ;; + esac + + local op="deploy" + if [[ "${deploy}" == "false" ]]; then + op="upgrade" + fi + java -Djava.security.properties=${SECURIY_FILE} -Djdk.sunec.disableNative=false -Djdk.tls.namedGroups="SM2,secp256k1,x25519,secp256r1,secp384r1,secp521r1,x448" -cp conf/:lib/*:plugin/* com.webank.wecross.stub."${packageName}".preparation.ProxyDeployContract "${op}" "${chainName}" +} + +deploy_or_update_chainmaker_hub_contract() { + local chainName="$1" + local deploy="$2" + local type="$3" + local packageName="" + case $type in + "ChainMakerWithCert" | "ChainMakerGMWithCert" | "ChainMakerWithPublic" | "ChainMakerGMWithPublic") + packageName="chainmaker" + ;; + esac + + local op="deploy" + if [[ "${deploy}" == "false" ]]; then + op="upgrade" + fi + java -Djava.security.properties=${SECURIY_FILE} -Djdk.sunec.disableNative=false -Djdk.tls.namedGroups="SM2,secp256k1,x25519,secp256r1,secp384r1,secp521r1,x448" -cp conf/:lib/*:plugin/* com.webank.wecross.stub."${packageName}".preparation.HubDeployContract "${op}" "${chainName}" +} + main() { local type="$1" local chain="$2" @@ -201,7 +241,7 @@ main() { LOG_INFO " deploy_system_contract, type: ${type}, chain: ${chain}, deploy: ${deploy}, contract: ${contract}" case $type in - "BCOS2.0" | "BCOS3_ECDSA_EVM" | "GM_BCOS2.0" | "BCOS3_GM_EVM" | "BCOS3_ECDSA_WASM" | "BCOS3_GM_WASM") + "BCOS2.0" | "BCOS3_ECDSA_EVM" | "GM_BCOS2.0" | "BCOS3_GM_EVM") if [[ "${contract}" == "proxy" ]]; then bcos_proxy_contract "${chain}" "${deploy}" "${type}" elif [[ "${contract}" == "hub" ]]; then @@ -230,6 +270,13 @@ main() { update_fabric2_hub_contract "${chain}" fi ;; + "ChainMakerWithCert" | "ChainMakerGMWithCert" | "ChainMakerWithPublic" | "ChainMakerGMWithPublic") + if [[ "${contract}" == "proxy" ]]; then + deploy_or_update_chainmaker_proxy_contract "${chain}" "${deploy}" "${type}" + elif [[ "${contract}" == "hub" ]]; then + deploy_or_update_chainmaker_hub_contract "${chain}" "${deploy}" "${type}" + fi + ;; *) echo "Unrecognized type" && help ;; diff --git a/scripts/start.sh b/scripts/start.sh index 23ba9247e..b533185c1 100644 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -1,7 +1,7 @@ #!/bin/bash dirpath="$(cd "$(dirname "$0")" && pwd)" cd ${dirpath} -export LANG='zh_CN.utf8' +export LANG='en_US.UTF-8' APPS_FOLDER=$(pwd)/apps PLUGLIN_FOLDER=$(pwd)/plugin diff --git a/settings.gradle b/settings.gradle index 6d282df82..4e81e7675 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'wecross' +rootProject.name = 'WeCross' diff --git a/src/main/java/com/webank/wecross/Generator.java b/src/main/java/com/webank/wecross/Generator.java index 8cd36fc4d..389b04b96 100644 --- a/src/main/java/com/webank/wecross/Generator.java +++ b/src/main/java/com/webank/wecross/Generator.java @@ -10,9 +10,21 @@ public class Generator { private static final int ARGS_LENGTH = 3; private static ApplicationContext context; - public static void main(String[] args) { + public static void connectionChain(String type, String path, String[] args) throws Exception { + context = new AnnotationConfigApplicationContext(StubManagerConfig.class); + StubManager stubManager = context.getBean(StubManager.class); + StubFactory stubFactory = stubManager.getStubFactory(type); + stubFactory.generateConnection(path, args); + } + + public static void addChainAccount(String type, String path) throws Exception { context = new AnnotationConfigApplicationContext(StubManagerConfig.class); + StubManager stubManager = context.getBean(StubManager.class); + StubFactory stubFactory = stubManager.getStubFactory(type); + stubFactory.generateAccount(path, new String[] {}); + } + public static void main(String[] args) { if (args.length < ARGS_LENGTH) { System.out.println("Usage: connection/account "); return; @@ -23,14 +35,11 @@ public static void main(String[] args) { String path = args[2]; System.out.println(String.format("operator: " + op + " type: " + type + " path: " + path)); - StubManager stubManager = context.getBean(StubManager.class); try { - StubFactory stubFactory = stubManager.getStubFactory(type); - if (op.equals("connection")) { - stubFactory.generateConnection(path, new String[] {}); + connectionChain(type, path, new String[] {}); } else if (op.equals("account")) { - stubFactory.generateAccount(path, new String[] {}); + addChainAccount(type, path); } else { System.err.println("Unknown operation: " + op); } diff --git a/src/main/java/com/webank/wecross/account/UniversalAccount.java b/src/main/java/com/webank/wecross/account/UniversalAccount.java index 23e314d8a..02f9be2c4 100644 --- a/src/main/java/com/webank/wecross/account/UniversalAccount.java +++ b/src/main/java/com/webank/wecross/account/UniversalAccount.java @@ -31,8 +31,8 @@ public class UniversalAccount { private Map type2DefaultAccount = new HashMap<>(); - public Account getAccount(String type) { - return type2DefaultAccount.get(type); + public Account getAccount(String type, String chainName) { + return type2DefaultAccount.get(type + '-' + chainName); } public void setDefaultAccount(String type, Account account) { diff --git a/src/main/java/com/webank/wecross/account/UniversalAccountFactory.java b/src/main/java/com/webank/wecross/account/UniversalAccountFactory.java index 6abf9304a..501031652 100644 --- a/src/main/java/com/webank/wecross/account/UniversalAccountFactory.java +++ b/src/main/java/com/webank/wecross/account/UniversalAccountFactory.java @@ -37,8 +37,17 @@ public UniversalAccount buildUA(UADetails uaDetails) throws WeCrossException { for (ChainAccountDetails details : chainAccountDetailsMap.values()) { String type = details.getType(); + // type 可能是这样的格式: stubType-chainName + String[] splits = type.split("-"); + String stubType = splits[0]; + details.setType(stubType); - Account account = stubManager.newStubAccount(type, details.toProperties()); + // 如果没有加载相关插件,就不创建该插件的链账户 + if (!stubManager.hasFactory(stubType)) { + continue; + } + + Account account = stubManager.newStubAccount(stubType, details.toProperties()); if (account == null) { logger.error( diff --git a/src/main/java/com/webank/wecross/common/NetworkQueryStatus.java b/src/main/java/com/webank/wecross/common/NetworkQueryStatus.java index a42862713..9493f7873 100644 --- a/src/main/java/com/webank/wecross/common/NetworkQueryStatus.java +++ b/src/main/java/com/webank/wecross/common/NetworkQueryStatus.java @@ -34,6 +34,11 @@ public class NetworkQueryStatus { /** 70000+ is HTLC error real_state = 70000 + HTLCErrorCode */ public static final int HTLC_ERROR = 70000; + /** 80000+ are errors for uploading a resource */ + public static final int UPLOAD_RESOURCE_ERROR = 80000; + + public static final int UPLOAD_RESOURCE_EXIST = 80001; + public static String getStatusMessage(int status) { return getStatusMessage(status, "Error code: " + status); } diff --git a/src/main/java/com/webank/wecross/common/WeCrossDefault.java b/src/main/java/com/webank/wecross/common/WeCrossDefault.java index d826fd5fb..979c1bf37 100644 --- a/src/main/java/com/webank/wecross/common/WeCrossDefault.java +++ b/src/main/java/com/webank/wecross/common/WeCrossDefault.java @@ -27,5 +27,6 @@ public class WeCrossDefault { "BCOS2.0", "GM_BCOS2.0", "Fabric1.4", - "Fabric2.0"); + "Fabric2.0", + "CHAINMAKER"); } diff --git a/src/main/java/com/webank/wecross/config/AuthFilterConfig.java b/src/main/java/com/webank/wecross/config/AuthFilterConfig.java index 93c8450f0..503958ac5 100644 --- a/src/main/java/com/webank/wecross/config/AuthFilterConfig.java +++ b/src/main/java/com/webank/wecross/config/AuthFilterConfig.java @@ -23,6 +23,8 @@ public AuthFilter newAuthFilter() { remoteAuthFilter.setClientConnection(clientConnection); remoteAuthFilter.registerAuthUri("/auth/register"); remoteAuthFilter.registerAuthUri("/auth/login"); + remoteAuthFilter.registerAuthUri("/admin/auth/loginWithoutPwd"); + remoteAuthFilter.registerAuthUri("/auth/routerLogin"); remoteAuthFilter.registerAuthUri("/auth/logout"); remoteAuthFilter.registerAuthUri("/auth/addChainAccount"); remoteAuthFilter.registerAuthUri("/auth/removeChainAccount"); diff --git a/src/main/java/com/webank/wecross/config/ZoneManagerConfig.java b/src/main/java/com/webank/wecross/config/ZoneManagerConfig.java index 7583b7206..9c2f3694f 100644 --- a/src/main/java/com/webank/wecross/config/ZoneManagerConfig.java +++ b/src/main/java/com/webank/wecross/config/ZoneManagerConfig.java @@ -52,6 +52,11 @@ public void onResourcesChange(List resourceInfos) { chain.updateLocalResources(resourceInfos); zoneManager.newSeq(); } + + @Override + public void onANewResource(ResourceInfo resourceInfo) { + chain.addResource(resourceInfo); + } }); } } diff --git a/src/main/java/com/webank/wecross/config/ZonesConfig.java b/src/main/java/com/webank/wecross/config/ZonesConfig.java index c0600d50a..e8dcf5ef2 100644 --- a/src/main/java/com/webank/wecross/config/ZonesConfig.java +++ b/src/main/java/com/webank/wecross/config/ZonesConfig.java @@ -1,5 +1,8 @@ package com.webank.wecross.config; +import com.alibaba.nacos.api.NacosFactory; +import com.alibaba.nacos.api.PropertyKeyConst; +import com.alibaba.nacos.api.naming.NamingService; import com.fasterxml.jackson.core.JsonProcessingException; import com.moandjiezana.toml.Toml; import com.webank.wecross.common.WeCrossDefault; @@ -11,6 +14,7 @@ import com.webank.wecross.stubmanager.MemoryBlockManagerFactory; import com.webank.wecross.stubmanager.StubManager; import com.webank.wecross.utils.ConfigUtils; +import com.webank.wecross.utils.NetworkUtils; import com.webank.wecross.zone.Chain; import com.webank.wecross.zone.ChainInfo; import com.webank.wecross.zone.Zone; @@ -19,6 +23,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Properties; import javax.annotation.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -82,17 +87,37 @@ public Map newZoneMap() { result.put(network, networkBean); } else { logger.error("No stubs found in {}", network); - System.exit(1); } } catch (WeCrossException e) { logger.error(e.getMessage()); - System.exit(1); } return result; } + private void registerService() { + try { + String serviceAddr = toml.getString("nacos.serviceAddr"); + if(serviceAddr == null || serviceAddr.isEmpty()) { + return; + } + Properties properties = new Properties(); + properties.setProperty( + PropertyKeyConst.SERVER_ADDR, serviceAddr); + properties.setProperty(PropertyKeyConst.NAMESPACE, toml.getString("nacos.nameSpace")); + NamingService namingService = NacosFactory.createNamingService(properties); + String dubboIpToRegistry = toml.getString("nacos.dubboIpToRegistry"); + if (dubboIpToRegistry == null || dubboIpToRegistry.isEmpty()) { + dubboIpToRegistry = NetworkUtils.getLocalIP(); + } + namingService.registerInstance( + "bmsp-cross", toml.getString("nacos.groupName"), dubboIpToRegistry, 8251); + } catch (Exception e) { + logger.warn("注册服务至 Nacos 失败。 {}", e.getMessage()); + } + } + public Map getChains(String zone, Map chainsDir) throws WeCrossException { Map stubMap = new HashMap<>(); @@ -127,11 +152,25 @@ public Map getChains(String zone, Map chainsDir) throw new WeCrossException(WeCrossException.ErrorCode.FIELD_MISSING, errorMessage); } - Connection localConnection = stubManager.newStubConnection(type, stubPath); - if (localConnection == null) { - logger.error("Init localConnection: {}-{} failed", stubPath, type); + Connection localConnection = null; + try { + localConnection = stubManager.newStubConnection(type, stubPath); + registerService(); + } catch (WeCrossException e) { + logger.warn( + "Init {}-{} connection is unsuccessful. {}", + type, + chainName, + e.getMessage()); + } - throw new WeCrossException(-1, "Init localConnection failed"); + if (localConnection == null) { + logger.error( + "Init {}-{} connection is unsuccessful. please check stub config. {}", + type, + chainName, + stubPath); + continue; } Driver driver = stubManager.getStubDriver(type); diff --git a/src/main/java/com/webank/wecross/host/WeCrossHost.java b/src/main/java/com/webank/wecross/host/WeCrossHost.java index d64a4db43..3f8db678c 100644 --- a/src/main/java/com/webank/wecross/host/WeCrossHost.java +++ b/src/main/java/com/webank/wecross/host/WeCrossHost.java @@ -74,7 +74,7 @@ public void mainLoop() { boolean flag = true; while (flag) { try { - Thread.sleep(1000); + Thread.sleep(10000); broadcastStatus(); dumpStatus(); diff --git a/src/main/java/com/webank/wecross/interchain/InterchainJob.java b/src/main/java/com/webank/wecross/interchain/InterchainJob.java index 1ea6ae9cc..ddf760596 100644 --- a/src/main/java/com/webank/wecross/interchain/InterchainJob.java +++ b/src/main/java/com/webank/wecross/interchain/InterchainJob.java @@ -130,12 +130,12 @@ public String[] getInterchainRequests(SystemResource systemResource, UniversalAc if (transactionException.getErrorCode() == TransactionException.ErrorCode.ACCOUNT_ERRPR) { /* if has not config chain account for router */ - logger.warn( + logger.debug( "Failed to get interchain requests, path: {}, errorMessage: {}", hubResource.getPath(), transactionException.getMessage()); } else { - logger.error( + logger.debug( "Failed to get interchain requests, path: {}, errorMessage: {}", hubResource.getPath(), transactionException.getMessage()); @@ -145,7 +145,7 @@ public String[] getInterchainRequests(SystemResource systemResource, UniversalAc future.complete(InterchainDefault.NULL_FLAG); } } else if (transactionResponse.getErrorCode() != 0) { - logger.error( + logger.debug( "Failed to get interchain requests, path: {}, errorMessage: {}", hubResource.getPath(), transactionResponse.getMessage()); @@ -172,7 +172,9 @@ public String[] getInterchainRequests(SystemResource systemResource, UniversalAc String result = null; try { result = future.get(RoutineDefault.CALLBACK_TIMEOUT, TimeUnit.MILLISECONDS); - if (Objects.isNull(result) || RoutineDefault.NULL_FLAG.equals(result)) { + if (Objects.isNull(result) + || RoutineDefault.NULL_FLAG.equals(result) + || result.isEmpty()) { return new String[] {}; } diff --git a/src/main/java/com/webank/wecross/mq/config/MqConfig.java b/src/main/java/com/webank/wecross/mq/config/MqConfig.java new file mode 100644 index 000000000..b3fae07c7 --- /dev/null +++ b/src/main/java/com/webank/wecross/mq/config/MqConfig.java @@ -0,0 +1,98 @@ +package com.webank.wecross.mq.config; + +public class MqConfig { + // Getter and Setter 方法 + private String mqType; // kafka/rabbitmq/rocketmq + private String host; + private int port; + private String username; + private String password; + private String group; + private String topic; + + public MqConfig setHost(String host) { + if (host == null || host.trim().isEmpty()) { + throw new IllegalArgumentException("Host cannot be empty"); + } + this.host = host; + return this; + } + + public String getHost() { + return host; + } + + public MqConfig setUsername(String username) { + this.username = username; + return this; + } + + public String getUsername() { + return this.username; + } + + public MqConfig setPassword(String password) { + this.password = password; + return this; + } + + public String getPassword() { + return this.password; + } + + public MqConfig setGroup(String group) { + if (group == null || group.trim().isEmpty()) { + throw new IllegalArgumentException("Group cannot be empty"); + } + this.group = group; + return this; + } + + public String getGroup() { + return this.group; + } + + public MqConfig setTopic(String topic) { + if (topic == null || topic.trim().isEmpty()) { + throw new IllegalArgumentException("Topic cannot be empty"); + } + this.topic = topic; + return this; + } + + public String getTopic() { + return this.topic; + } + + public MqConfig setMqType(String mqType) { + this.mqType = mqType; + return this; + } + + public String getMqType() { + return this.mqType; + } + + public MqConfig setPort(Long port) { + this.port = Math.toIntExact(port); + return this; + } + + public int getPort() { + return this.port; + } + + @Override + public String toString() { + return "MqConfig{" + + "type=" + + mqType + + ", host=" + + host + + ", port=" + + port + + ", topic=" + + topic + + '}'; + } +} diff --git a/src/main/java/com/webank/wecross/mq/core/MqConnector.java b/src/main/java/com/webank/wecross/mq/core/MqConnector.java new file mode 100644 index 000000000..c41a1c2c0 --- /dev/null +++ b/src/main/java/com/webank/wecross/mq/core/MqConnector.java @@ -0,0 +1,14 @@ +package com.webank.wecross.mq.core; + +import org.springframework.messaging.Message; + +public interface MqConnector extends AutoCloseable { + void send(Message message) throws MqException; + + void subscribe(MessageHandler handler) throws MqException; + + @FunctionalInterface + interface MessageHandler { + void handle(Object payload); + } +} diff --git a/src/main/java/com/webank/wecross/mq/core/MqException.java b/src/main/java/com/webank/wecross/mq/core/MqException.java new file mode 100644 index 000000000..8525b3518 --- /dev/null +++ b/src/main/java/com/webank/wecross/mq/core/MqException.java @@ -0,0 +1,11 @@ +package com.webank.wecross.mq.core; + +public class MqException extends Exception { + public MqException(String message) { + super(message); + } + + public MqException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/webank/wecross/mq/factory/MqConnectorFactory.java b/src/main/java/com/webank/wecross/mq/factory/MqConnectorFactory.java new file mode 100644 index 000000000..4b901406f --- /dev/null +++ b/src/main/java/com/webank/wecross/mq/factory/MqConnectorFactory.java @@ -0,0 +1,23 @@ +package com.webank.wecross.mq.factory; + +import com.webank.wecross.mq.config.MqConfig; +import com.webank.wecross.mq.core.MqConnector; +import com.webank.wecross.mq.core.MqException; +import com.webank.wecross.mq.impl.KafkaConnector; +import com.webank.wecross.mq.impl.RabbitMQConnector; +import com.webank.wecross.mq.impl.RocketMQConnector; + +public class MqConnectorFactory { + public static MqConnector createConnector(MqConfig config) throws MqException { + switch (config.getMqType().toLowerCase()) { + case "kafka": + return new KafkaConnector(config); + case "rabbitmq": + return new RabbitMQConnector(config); + case "rocketmq": + return new RocketMQConnector(config); + default: + throw new IllegalArgumentException("Unsupported MQ type: " + config.getMqType()); + } + } +} diff --git a/src/main/java/com/webank/wecross/mq/impl/KafkaConnector.java b/src/main/java/com/webank/wecross/mq/impl/KafkaConnector.java new file mode 100644 index 000000000..4fbdfeb88 --- /dev/null +++ b/src/main/java/com/webank/wecross/mq/impl/KafkaConnector.java @@ -0,0 +1,108 @@ +package com.webank.wecross.mq.impl; + +import com.webank.wecross.mq.config.MqConfig; +import com.webank.wecross.mq.core.MqConnector; +import com.webank.wecross.mq.core.MqException; +import java.time.Duration; +import java.util.Collections; +import java.util.Properties; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.messaging.Message; + +public class KafkaConnector implements MqConnector { + private final KafkaProducer producer; + private final MqConfig config; + private volatile boolean consuming = false; + private Thread consumeThread; + + public KafkaConnector(MqConfig config) { + this.config = config; + Properties props = new Properties(); + props.put("bootstrap.servers", config.getHost() + ":" + config.getPort()); + // 设置Kafka生产者的消息键和值的序列化方式 + props.put("key.serializer", StringSerializer.class.getName()); + props.put("value.serializer", StringSerializer.class.getName()); + this.producer = new KafkaProducer<>(props); + } + + @Override + public void send(Message message) throws MqException { + try { + producer.send(new ProducerRecord<>(config.getTopic(), message.getPayload().toString())); + } catch (Exception e) { + throw new MqException("Kafka message send failed", e); + } + } + + /** + * 订阅消息队列服务 + * + * @param handler 消息处理接口,用于处理接收到的消 该方法通过创建一个Kafka消费者来订阅指定的主题,并在接收到消息时使用提供的处理器进行处理 + * 它配置了消费者属性,启动了一个消费线程,在该线程中创建消费者实例并开始消费循环 + */ + @Override + public void subscribe(MessageHandler handler) { + try { + // 初始化消费者配置属性 + Properties props = new Properties(); + props.put("bootstrap.servers", config.getHost() + ":" + config.getPort()); + props.put("group.id", config.getGroup()); + props.put( + "key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); + props.put( + "value.deserializer", + "org.apache.kafka.common.serialization.StringDeserializer"); + + // 设置消费状态为正在消费,并创建一个新的线程用于消息消费 + consuming = true; + consumeThread = + new Thread( + () -> { + try (org.apache.kafka.clients.consumer.KafkaConsumer + consumer = + new org.apache.kafka.clients.consumer + .KafkaConsumer<>(props)) { + + // 订阅配置中指定的主题 + consumer.subscribe(Collections.singleton(config.getTopic())); + + // 消费循环,直到消费状态变为false + while (consuming) { + ConsumerRecords records = + consumer.poll(Duration.ofMillis(100)); + for (ConsumerRecord record : records) { + // 调用消息处理器处理接收到的消息 + handler.handle(record.value()); + } + } + } catch (Exception e) { + // 抛出运行时异常,包含原始异常,用于处理Kafka消费过程中出现的异常 + throw new RuntimeException("Kafka消费异常", e); + } + }); + // 启动消费线程 + consumeThread.start(); + } catch (Exception e) { + // 抛出运行时异常,包含原始异常,用于处理Kafka订阅过程中出现的异常 + throw new RuntimeException("Kafka订阅失败", e); + } + } + + @Override + public void close() { + consuming = false; + if (producer != null) { + producer.close(); + } + if (consumeThread != null) { + try { + consumeThread.join(1000); + } catch (InterruptedException ignored) { + } + } + } +} diff --git a/src/main/java/com/webank/wecross/mq/impl/RabbitMQConnector.java b/src/main/java/com/webank/wecross/mq/impl/RabbitMQConnector.java new file mode 100644 index 000000000..909686da1 --- /dev/null +++ b/src/main/java/com/webank/wecross/mq/impl/RabbitMQConnector.java @@ -0,0 +1,130 @@ +package com.webank.wecross.mq.impl; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.webank.wecross.mq.config.MqConfig; +import com.webank.wecross.mq.core.MqConnector; +import com.webank.wecross.mq.core.MqException; +import java.nio.charset.StandardCharsets; +import org.springframework.messaging.Message; + +public class RabbitMQConnector implements MqConnector { + private final Connection connection; + private final Channel channel; + private final MqConfig config; + private volatile boolean consuming = false; + private Thread consumeThread; + + /** + * 构造函数:初始化RabbitMQ连接和通道 + * + * @param config MQ配置对象,包含连接RabbitMQ所需的信息(主机地址、端口、用户名、密码、主题) + * @throws MqException 如果连接创建失败,抛出自定义的MQ异常 + */ + public RabbitMQConnector(MqConfig config) throws MqException { + this.config = config; + try { + // 创建RabbitMQ连接工厂,并配置连接属性 + ConnectionFactory factory = new ConnectionFactory(); + factory.setHost(config.getHost()); + factory.setPort(config.getPort()); + factory.setUsername(config.getUsername()); + factory.setPassword(config.getPassword()); + + // 建立到RabbitMQ的连接 + this.connection = factory.newConnection(); + + // 创建通道,这是执行RabbitMQ操作的主要入口 + this.channel = connection.createChannel(); + + // 声明一个队列,参数分别为队列名称、是否持久化、是否独占、是否自动删除、其他属性 + // 此处的队列名称从配置对象中获取 + channel.queueDeclare(config.getTopic(), false, false, false, null); + } catch (Exception e) { + // 捕获连接或声明队列过程中可能发生的任何异常,并抛出自定义的MQ异常 + throw new MqException("RabbitMQ connection failed", e); + } + } + + @Override + public void send(Message message) throws MqException { + try { + channel.basicPublish( + "", config.getTopic(), null, message.getPayload().toString().getBytes()); + } catch (Exception e) { + throw new MqException("RabbitMQ message send failed", e); + } + } + + /** + * 订阅消息队列中的消息 当有新消息到达时,将调用提供的消息处理程序来处理消息 + * + * @param handler 消息处理程序,用于处理接收到的消息 + */ + @Override + public void subscribe(MessageHandler handler) { + try { + // 设置消费状态为true,表示开始消费消息 + consuming = true; + + // 创建一个新的线程来处理消息消费,以避免阻塞主线程 + consumeThread = + new Thread( + () -> { + try { + // 调用RabbitMQ的basicConsume方法来消费消息 + // 配置消费的队列、自动确认消息、消息交付确认回调和取消消费的回调 + channel.basicConsume( + config.getTopic(), + true, + (consumerTag, delivery) -> { + // 将接收到的消息体转换为字符串,并调用消息处理程序来处理消息 + String payload = + new String( + delivery.getBody(), + StandardCharsets.UTF_8); + handler.handle(payload); + }, + consumerTag -> {}); + + // 持续运行,保持消费状态,直到消费状态被设置为false + while (consuming) { + // 短暂休眠,减少CPU占用 + Thread.sleep(100); + } + } catch (Exception e) { + // 捕获并处理消息消费过程中的异常 + throw new RuntimeException("RabbitMQ消费异常", e); + } + }); + + // 启动消费线程 + consumeThread.start(); + } catch (Exception e) { + // 捕获并处理订阅过程中的异常 + throw new RuntimeException("RabbitMQ订阅失败", e); + } + } + + @Override + public void close() throws MqException { + consuming = false; + try { + if (channel != null && channel.isOpen()) { + channel.close(); + } + if (connection != null && connection.isOpen()) { + connection.close(); + } + } catch (Exception e) { + throw new MqException("RabbitMQ message close failed", e); + } + if (consumeThread != null) { + try { + consumeThread.join(1000); + } catch (InterruptedException ignored) { + } + } + } +} diff --git a/src/main/java/com/webank/wecross/mq/impl/RocketMQConnector.java b/src/main/java/com/webank/wecross/mq/impl/RocketMQConnector.java new file mode 100644 index 000000000..0b6063907 --- /dev/null +++ b/src/main/java/com/webank/wecross/mq/impl/RocketMQConnector.java @@ -0,0 +1,154 @@ +package com.webank.wecross.mq.impl; + +import com.webank.wecross.mq.config.MqConfig; +import com.webank.wecross.mq.core.MqConnector; +import com.webank.wecross.mq.core.MqException; +import java.util.Collections; +import org.apache.rocketmq.client.apis.ClientConfiguration; +import org.apache.rocketmq.client.apis.ClientServiceProvider; +import org.apache.rocketmq.client.apis.consumer.ConsumeResult; +import org.apache.rocketmq.client.apis.consumer.FilterExpression; +import org.apache.rocketmq.client.apis.consumer.PushConsumer; +import org.apache.rocketmq.client.apis.producer.Producer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RocketMQConnector implements MqConnector { + private Logger logger = LoggerFactory.getLogger(RocketMQConnector.class); + private final Producer producer; + private final MqConfig config; + private final ClientServiceProvider provider; + private PushConsumer consumer; + private volatile boolean consuming = false; + private Thread consumeThread; + + /** + * 构造函数,初始化RocketMQ连接器 + * + * @param config MQ配置对象,包含连接RocketMQ所需的信息 + * @throws MqException 如果连接RocketMQ失败,则抛出此异常 + */ + public RocketMQConnector(MqConfig config) throws MqException { + logger.info("RocketMQConnector: {}", config); + // 保存配置信息 + this.config = config; + try { + // 加载RocketMQ服务提供者 + provider = ClientServiceProvider.loadService(); + // 根据配置信息构建客户端配置 + ClientConfiguration clientConfiguration = + ClientConfiguration.newBuilder() + .setEndpoints(config.getHost() + ":" + config.getPort()) + .build(); + // 创建生产者对象 + producer = + provider.newProducerBuilder() + .setClientConfiguration(clientConfiguration) + .setTopics(config.getTopic()) + .build(); + } catch (Exception e) { + logger.error("RocketMQConnector catch an exception: {}", e.getMessage()); + // 如果连接过程中发生异常,抛出自定义的MqException + throw new MqException("RocketMQ connection failed", e); + } + } + + @Override + public void send(org.springframework.messaging.Message message) throws MqException { + try { + // 修正无法解析MessageBuilder类型的问题,引入正确的MessageBuilder类型 + org.apache.rocketmq.client.apis.message.MessageBuilder builder = + provider.newMessageBuilder() + .setTopic(config.getTopic()) + .setBody(message.getPayload().toString().getBytes()); + // 处理消息标签 + Object tag = message.getHeaders().getOrDefault("TAG", ""); + if (tag != null && !tag.toString().isEmpty()) { + builder.setTag(tag.toString()); + } + Object key = message.getHeaders().getOrDefault("KEY", ""); + if (key != null && !key.toString().isEmpty()) { + builder.setKeys(key.toString()); + } + producer.send(builder.build()); + } catch (Exception e) { + throw new MqException("RocketMQ message send failed", e); + } + } + + /** + * 订阅消息队列服务 + * + * @param handler 消息处理回调接口,用于处理接收到的消息 + * @throws MqException 如果订阅过程中发生错误,则抛出此异常 + */ + @Override + public void subscribe(MessageHandler handler) throws MqException { + try { + // 根据配置信息构建消费者客户端配置 + ClientConfiguration consumerConfig = + ClientConfiguration.newBuilder() + .setEndpoints(config.getHost() + ":" + config.getPort()) + .build(); + // 设置消费状态为true + consuming = true; + consumeThread = + new Thread( + () -> { + try { + // 创建一个推送式消费者构建器,并配置相关参数 + consumer = + provider.newPushConsumerBuilder() + .setClientConfiguration(consumerConfig) + .setConsumerGroup(config.getGroup()) // 添加消费者组配置 + .setSubscriptionExpressions( + Collections.singletonMap( + config.getTopic(), + FilterExpression.SUB_ALL)) + .setMessageListener( + messageView -> { + // 处理接收到的消息 + byte[] bytes = + new byte + [messageView + .getBody() + .remaining()]; + messageView.getBody().get(bytes); + handler.handle(new String(bytes)); + return ConsumeResult.SUCCESS; + }) + .build(); + + // 保持线程运行,以便持续消费消息 + while (consuming) { + Thread.sleep(1000); + } + } catch (Exception e) { + throw new RuntimeException("RocketMQ消费异常", e); + } + }); + // 启动消费线程 + consumeThread.start(); + } catch (Exception e) { + throw new MqException("RocketMQ订阅失败", e); + } + } + + @Override + public void close() throws MqException { + consuming = false; + try { + if (consumer != null) { + consumer.close(); + } + if (producer != null) { + producer.close(); + } + if (consumeThread != null) { + consumeThread.join(2000); + } + } catch (Exception e) { + throw new MqException("RocketMQ关闭失败", e); + } + } +} diff --git a/src/main/java/com/webank/wecross/network/client/NettyAsyncHttpClientEngine.java b/src/main/java/com/webank/wecross/network/client/NettyAsyncHttpClientEngine.java index 856138500..d8500f019 100644 --- a/src/main/java/com/webank/wecross/network/client/NettyAsyncHttpClientEngine.java +++ b/src/main/java/com/webank/wecross/network/client/NettyAsyncHttpClientEngine.java @@ -110,7 +110,7 @@ public void onFailed(WeCrossException e) { public void asyncSend( Request request, TypeReference typeReference, Callback callback) { try { - String url = "https://" + clientConnection.getServer() + request.getMethod(); + String url = "http://" + clientConnection.getServer() + request.getMethod(); if (logger.isDebugEnabled()) { logger.debug("request: {}; url: {}", request.toString(), url); } @@ -187,7 +187,7 @@ public void asyncSendInternal( Iterable> headers, String body, Handler handler) { - String url = "https://" + clientConnection.getServer() + uri; + String url = "http://" + clientConnection.getServer() + uri; BoundRequestBuilder requestBuilder = httpClient.prepare(method, url).setBody(body); for (Map.Entry header : headers) { diff --git a/src/main/java/com/webank/wecross/network/rpc/URIHandlerDispatcher.java b/src/main/java/com/webank/wecross/network/rpc/URIHandlerDispatcher.java index f95b8dac2..a5a09da9c 100644 --- a/src/main/java/com/webank/wecross/network/rpc/URIHandlerDispatcher.java +++ b/src/main/java/com/webank/wecross/network/rpc/URIHandlerDispatcher.java @@ -82,6 +82,7 @@ public void initializeRequestMapper(WeCrossHost host) { registerURIHandler(new URIMethod("GET", "/conn/listChains"), connectionURIHandler); registerURIHandler(new URIMethod("GET", "/conn/listZones"), connectionURIHandler); registerURIHandler(new URIMethod("POST", "/conn/addChain"), connectionURIHandler); + registerURIHandler(new URIMethod("POST", "/conn/connectChain"), connectionURIHandler); registerURIHandler(new URIMethod("POST", "/conn/updateChain"), connectionURIHandler); registerURIHandler(new URIMethod("POST", "/conn/removeChain"), connectionURIHandler); registerURIHandler(new URIMethod("GET", "/conn/listPeers"), connectionURIHandler); @@ -102,6 +103,9 @@ public void initializeRequestMapper(WeCrossHost host) { ResourceURIHandler resourceURIHandler = new ResourceURIHandler(host); registerURIHandler(RESOURCE_URIMETHOD, resourceURIHandler); + UpLoadFileURIHandler upLoadFileURIHandler = new UpLoadFileURIHandler(); + registerURIHandler(new URIMethod("POST", "/upload/chain"), upLoadFileURIHandler); + logger.info(" initialize size: {}", requestURIMapper.size()); logger.info(" URIMethod: {} ", requestURIMapper.keySet()); } diff --git a/src/main/java/com/webank/wecross/network/rpc/handler/ConnectionURIHandler.java b/src/main/java/com/webank/wecross/network/rpc/handler/ConnectionURIHandler.java index b72b91b73..a5f63fbb6 100644 --- a/src/main/java/com/webank/wecross/network/rpc/handler/ConnectionURIHandler.java +++ b/src/main/java/com/webank/wecross/network/rpc/handler/ConnectionURIHandler.java @@ -1,33 +1,41 @@ package com.webank.wecross.network.rpc.handler; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import com.moandjiezana.toml.Toml; +import com.webank.wecross.Generator; import com.webank.wecross.account.AccountManager; import com.webank.wecross.account.UniversalAccount; import com.webank.wecross.account.UserContext; import com.webank.wecross.common.NetworkQueryStatus; +import com.webank.wecross.common.WeCrossDefault; +import com.webank.wecross.config.WeCrossTomlConfig; import com.webank.wecross.exception.WeCrossException; import com.webank.wecross.network.UriDecoder; import com.webank.wecross.network.p2p.P2PService; import com.webank.wecross.peer.PeerManager; import com.webank.wecross.peer.PeerManager.PeerDetails; +import com.webank.wecross.resource.Resource; import com.webank.wecross.restserver.RestRequest; import com.webank.wecross.restserver.RestResponse; +import com.webank.wecross.stub.*; +import com.webank.wecross.utils.ToolUtils; import com.webank.wecross.zone.Chain; +import com.webank.wecross.zone.ChainInfo; import com.webank.wecross.zone.Zone; import com.webank.wecross.zone.ZoneManager; -import java.util.Collection; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import io.netty.handler.codec.http.HttpRequest; +import java.io.File; +import java.util.*; import java.util.concurrent.atomic.AtomicLong; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; public class ConnectionURIHandler implements URIHandler { private Logger logger = LoggerFactory.getLogger(ConnectionURIHandler.class); + private Toml toml = null; private ObjectMapper objectMapper = new ObjectMapper(); private P2PService p2PService; @@ -67,7 +75,12 @@ public void setData(Object data) { @Override public void handle( - UserContext userContext, String uri, String method, String content, Callback callback) { + UserContext userContext, + HttpRequest httpRequest, + String uri, + String method, + String content, + Callback callback) { logger.debug( "Handle rpc connection request: {} {} {} {}", userContext, uri, method, content); try { @@ -108,12 +121,19 @@ public void onResponse(Exception e, Object response) { case "listZones": handleListZones(userContext, uri, method, content, handleCallback); break; - /* - * case "addChain": data = handleAddChain(userContext, uri, method, content); - * break; case "updateChain": data = handleUpdateChain(userContext, uri, method, - * content); break; case "removeChain": data = handleRemoveChain(userContext, - * uri, method, content); break; - */ + case "addChain": + handleAddChain(userContext, uri, method, content, handleCallback); + break; + case "connectChain": + handleConnectionChain(userContext, uri, method, content, handleCallback); + break; + case "updateChain": + handleUpdateChain(userContext, uri, method, content, handleCallback); + break; + case "removeChain": + handleRemoveChain(userContext, uri, method, content, handleCallback); + break; + case "listPeers": handleListPeers(userContext, uri, method, content, handleCallback); break; @@ -326,19 +346,380 @@ private void handleListZones( callback.onResponse(null, new ListData(zoneManager.getZones().size(), zones)); } - private Object handleAddChain( - UserContext userContext, String uri, String method, String content) { - return null; + private String getChainRootPath() throws Exception { + if (this.toml == null) { + WeCrossTomlConfig weCrossTomlConfig = new WeCrossTomlConfig(); + this.toml = weCrossTomlConfig.newToml(); + } + + String stubsPath = this.toml.getString("chains.path"); + if (stubsPath == null || stubsPath.isEmpty()) { + stubsPath = this.toml.getString("stubs.path"); // To support old version + } + + if (stubsPath == null || stubsPath.isEmpty()) { + String errorMessage = + "\"path\" in [chains] item not found, please check " + + WeCrossDefault.MAIN_CONFIG_FILE; + throw new Exception(errorMessage); + } + return stubsPath; + } + + public static class AddChain { + public String chainType; + public String chainName; + public Object stubConfig; + + @Override + public String toString() { + String stubConfigStr = "writeValueAsString(stubConfig)"; + try { + ObjectMapper objectMapper = new ObjectMapper(); + stubConfigStr = objectMapper.writeValueAsString(stubConfig); + } catch (JsonProcessingException e) { + + } + + return "AddChain {" + + "chainType='" + + chainType + + "'," + + "chainName='" + + chainName + + "'," + + "stubConfig='" + + stubConfigStr + + "}"; + } + } + + private void handleAddChain( + UserContext userContext, + String uri, + String method, + String content, + HandleCallback callback) { + + AddChain data = null; + try { + String stubsPath = getChainRootPath(); + RestRequest restRequest = + objectMapper.readValue(content, new TypeReference>() {}); + data = restRequest.getData(); + + if (data.chainType.isEmpty()) { + callback.onResponse(new Exception("chainType is missing"), null); + return; + } + if (data.chainName.isEmpty()) { + callback.onResponse(new Exception("chainName is missing"), null); + return; + } + + PathMatchingResourcePatternResolver resolver = + new PathMatchingResourcePatternResolver(); + File dir = resolver.getResource(stubsPath).getFile(); + File addChainDir = new File(dir + File.separator + data.chainName); + if (!addChainDir.exists()) { + addChainDir.mkdirs(); + } else { + callback.onResponse( + new Exception(String.format("%s has existed.", data.chainName)), null); + return; + } + + String stubConfig = objectMapper.writeValueAsString(data.stubConfig); + Map mq = new HashMap<>(); + mq.put("type", this.toml.getString("mq.type")); + mq.put("host", this.toml.getString("mq.host")); + mq.put("port", this.toml.getLong("mq.port")); + mq.put("topic", this.toml.getString("mq.topic")); + mq.put("group", this.toml.getString("mq.group")); + Map mqConfigObject = new HashMap<>(); + mqConfigObject.put("mq", mq); + String mqConfig = objectMapper.writeValueAsString(mqConfigObject); + + String[] args = new String[] {data.chainType, data.chainName, stubConfig, mqConfig}; + // 执行 connection 操作 + Generator.connectionChain( + data.chainType, dir.getPath() + File.separator + data.chainName, args); + + logger.info( + "connection {} was successfully on {}", + data, + dir.getPath() + File.separator + data.chainName); + + callback.onResponse( + null, String.format("connection %s was successfully.", data.chainName)); + + } catch (Exception e) { + String chainName = data != null ? data.chainName : "#unknownChainName#"; + logger.error("add a chain names {} was failure. {}", chainName, e.getMessage()); + callback.onResponse(e, null); + } + } + + private boolean deploySystemContract(String chainType, String chainName) throws Exception { + // 部署系统合约 WeCrossHub 和 WeCrossProxy + boolean completed = + ToolUtils.executeShell( + 60, + "/bin/bash", + "deploy_system_contract.sh", + "-t", + chainType, + "-c", + "chains" + File.separator + chainName, + "-P"); + if (!completed) { + return false; + } + + completed = + ToolUtils.executeShell( + 60, + "/bin/bash", + "deploy_system_contract.sh", + "-t", + chainType, + "-c", + "chains" + File.separator + chainName, + "-H"); + if (!completed) { + return false; + } + return true; } - private Object handleUpdateChain( - UserContext userContext, String uri, String method, String content) { - return null; + public static class ConnectionChain { + public String chainType; + public String chainName; + public boolean ifDeploySystemContract; } - private Object handleRemoveChain( - UserContext userContext, String uri, String method, String content) { - return null; + private void handleConnectionChain( + UserContext userContext, + String uri, + String method, + String content, + HandleCallback callback) { + ConnectionChain data = null; + try { + String stubsPath = getChainRootPath(); + RestRequest restRequest = + objectMapper.readValue( + content, new TypeReference>() {}); + data = restRequest.getData(); + + PathMatchingResourcePatternResolver resolver = + new PathMatchingResourcePatternResolver(); + File dir = resolver.getResource(stubsPath).getFile(); + File connectingChainDir = new File(dir + File.separator + data.chainName); + if (!connectingChainDir.exists()) { + logger.error( + "connecting a chain names {}, but {} doesn't exist.", + data.chainName, + connectingChainDir.getPath()); + callback.onResponse( + new Exception( + String.format( + "connecting a chain named %s doesn't exist.", + data.chainName)), + null); + return; + } + + if (data.ifDeploySystemContract + && !deploySystemContract(data.chainType, data.chainName)) { + // 部署系统合约 WeCrossHub 和 WeCrossProxy + callback.onResponse( + new Exception("deploy WeCrossProxy contract was failure."), null); + return; + } + + // 1. 判断链的 stub 是否已经启动 + String zoneName = this.toml.getString("common.zone"); + Zone zone = zoneManager.getZone(zoneName); + if (zone.getChain(data.chainName) != null) { + callback.onResponse( + new Exception( + String.format("The chain names %s has existed.", data.chainName)), + null); + return; + } + + // 2. 连接 stub connection + String stubPath = String.format("classpath:%s/%s", dir.getName(), data.chainName); + Connection localConnection = + zoneManager.getStubManager().newStubConnection(data.chainType, stubPath); + if (localConnection == null) { + callback.onResponse( + new Exception(String.format("Init %s connection failed.", data.chainName)), + null); + return; + } + + // 3. + Driver driver = zoneManager.getStubManager().getStubDriver(data.chainType); + if (driver == null) { + callback.onResponse( + new Exception( + String.format( + "Stub driver type is %s doesn't exist.", data.chainType)), + null); + } + List resources = driver.getResources(localConnection); + Map properties = localConnection.getProperties(); + String checkSum = ChainInfo.buildChecksum(driver, localConnection); + ChainInfo chainInfo = new ChainInfo(); + chainInfo.setName(data.chainName); + chainInfo.setProperties(properties); + chainInfo.setStubType(data.chainType); + chainInfo.setResources(resources); + chainInfo.setChecksum(checkSum); + + Chain chain = new Chain(zoneName, chainInfo, driver, localConnection); + chain.setDriver(driver); + chain.setBlockManager(zoneManager.getMemoryBlockManagerFactory().build(chain)); + chain.setStubType(data.chainType); + + for (ResourceInfo resourceInfo : resources) { + com.webank.wecross.resource.Resource resource = + new com.webank.wecross.resource.Resource(); + Path path = new Path(); + path.setZone(zoneName); + path.setChain(chainInfo.getName()); + path.setResource(resourceInfo.getName()); + resource.setPath(path); + resource.setDriver(chain.getDriver()); + resource.addConnection(null, localConnection); + resource.setStubType(data.chainType); + resource.setResourceInfo(resourceInfo); + + resource.setBlockManager(chain.getBlockManager()); + + chain.getResources().put(resourceInfo.getName(), resource); + logger.info( + "Load local resource({}.{}.{}): {}", + zone, + data.chainName, + resource.getResourceInfo().getName(), + resource.getResourceInfo()); + } + zone.getChains().put(data.chainName, chain); + chain.start(); + localConnection.setConnectionEventHandler( + new Connection.ConnectionEventHandler() { + @Override + public void onResourcesChange(List resourceInfos) { + chain.updateLocalResources(resourceInfos); + zoneManager.newSeq(); + } + + @Override + public void onANewResource(ResourceInfo resourceInfo) { + chain.addResource(resourceInfo); + } + }); + + callback.onResponse(null, stubPath); + } catch (Exception e) { + String chainName = data != null ? data.chainName : "#unknownChainName#"; + logger.error("connecting a chain names {} was failure. {}", chainName, e.getMessage()); + callback.onResponse(e, null); + } + } + + private void handleUpdateChain( + UserContext userContext, + String uri, + String method, + String content, + HandleCallback callback) {} + + public static class RemoveChain { + public String chainType; + public String chainName; + } + + private void stopRunningChain(String chainName) throws Exception { + String zoneName = this.toml.getString("common.zone"); + Zone zone = zoneManager.getZone(zoneName); + if (zone == null) { + throw new Exception(String.format("Zone names %s doesn't exist.", zoneName)); + } + + Chain chain = zone.getChain(chainName); + if (chain == null) { + throw new Exception(String.format("Chain stub names %s doesn't exist.", chainName)); + } + ChainInfo chainInfo = chain.getChainInfo(); + List resourceInfos = chainInfo.getResources(); + for (ResourceInfo resourceInfo : resourceInfos) { + Resource resource = chain.getResource(resourceInfo.getName()); + if (resource == null) { + continue; + } + if (!resource.isTemporary() && resource.isConnectionEmpty()) { + chain.removeResource(resourceInfo.getName(), false); + } + } + + if (!chain.getPeers().isEmpty()) { + throw new Exception(String.format("Chain stub names %s has peers.", chainName)); + } + + chain.stop(); + zone.getChains().remove(chainName); + } + + private void handleRemoveChain( + UserContext userContext, + String uri, + String method, + String content, + HandleCallback callback) { + RemoveChain data = null; + try { + String chainRootPath = getChainRootPath(); + RestRequest restRequest = + objectMapper.readValue( + content, new TypeReference>() {}); + data = restRequest.getData(); + + // 停止 chain stub + try { + stopRunningChain(data.chainName); + StubFactory factory = zoneManager.getStubManager().getStubFactory(data.chainType); + if (factory != null) { + factory.releaseConnection(); + } + } catch (Exception e) { + logger.warn("Stop a running chain is unsuccessfully. {}", e); + } + + PathMatchingResourcePatternResolver resolver = + new PathMatchingResourcePatternResolver(); + File chainRootDir = resolver.getResource(chainRootPath).getFile(); + File removingChainDir = new File(chainRootDir + File.separator + data.chainName); + if (removingChainDir.exists() && removingChainDir.isDirectory()) { + ToolUtils.deleteDirectory(removingChainDir); + callback.onResponse(null, data); + } else { + callback.onResponse( + new Exception( + String.format( + "removing a chain names %s doesn't exist.", + data.chainName)), + null); + } + + } catch (Exception e) { + String chainName = data != null ? data.chainName : "#unknownChainName#"; + logger.error("remove a chain names {} was failure. {}", chainName, e.getMessage()); + callback.onResponse(e, null); + } } private void handleListPeers( diff --git a/src/main/java/com/webank/wecross/network/rpc/handler/ListResourcesURIHandler.java b/src/main/java/com/webank/wecross/network/rpc/handler/ListResourcesURIHandler.java index aa91170d9..4f5389c6e 100644 --- a/src/main/java/com/webank/wecross/network/rpc/handler/ListResourcesURIHandler.java +++ b/src/main/java/com/webank/wecross/network/rpc/handler/ListResourcesURIHandler.java @@ -17,6 +17,7 @@ import com.webank.wecross.restserver.response.ResourceResponse; import com.webank.wecross.stub.ObjectMapperFactory; import com.webank.wecross.stub.Path; +import io.netty.handler.codec.http.HttpRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,7 +50,12 @@ public void setAccountManager(AccountManager accountManager) { @Override public void handle( - UserContext userContext, String uri, String method, String content, Callback callback) { + UserContext userContext, + HttpRequest httpRequest, + String uri, + String method, + String content, + Callback callback) { RestResponse restResponse = new RestResponse<>(); if (logger.isDebugEnabled()) { diff --git a/src/main/java/com/webank/wecross/network/rpc/handler/ResourceURIHandler.java b/src/main/java/com/webank/wecross/network/rpc/handler/ResourceURIHandler.java index 42e9cbd12..89bcd77a6 100644 --- a/src/main/java/com/webank/wecross/network/rpc/handler/ResourceURIHandler.java +++ b/src/main/java/com/webank/wecross/network/rpc/handler/ResourceURIHandler.java @@ -18,10 +18,12 @@ import com.webank.wecross.routine.htlc.HTLCManager; import com.webank.wecross.stub.ObjectMapperFactory; import com.webank.wecross.stub.Path; +import com.webank.wecross.stub.SubscribeRequest; import com.webank.wecross.stub.TransactionRequest; import com.webank.wecross.zone.Chain; import com.webank.wecross.zone.Zone; import com.webank.wecross.zone.ZoneManager; +import io.netty.handler.codec.http.HttpRequest; import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -67,12 +69,12 @@ private Resource getResource(Path path) { @Override public void handle( UserContext userContext, + HttpRequest httpRequest, String uri, String httpMethod, String content, Callback callback) { RestResponse restResponse = new RestResponse<>(); - UniversalAccount ua; Path path = new Path(); try { @@ -262,6 +264,47 @@ public void handle( }); return; } + case "subscribeevent": + { + Resource resourceObj = getResource(path); + if (resourceObj == null) { + restResponse.setErrorCode(NetworkQueryStatus.URI_PATH_ERROR); + restResponse.setMessage("Resource not found"); + } + + RestRequest restRequest = + objectMapper.readValue( + content, + new TypeReference>() {}); + + restRequest.checkRestRequest(); + SubscribeRequest subscribeRequest = restRequest.getData(); + resourceObj.subscribeEvent( + subscribeRequest, + ua, + (transactionException, transactionResponse) -> { + if (logger.isDebugEnabled()) { + logger.debug( + " TransactionResponse: {}, TransactionException, ", + transactionResponse, + transactionException); + } + + if (transactionException != null + && !transactionException.isSuccess()) { + restResponse.setErrorCode( + NetworkQueryStatus.RESOURCE_ERROR + + transactionException.getErrorCode()); + restResponse.setMessage(transactionException.getMessage()); + } else { + restResponse.setData(transactionResponse); + } + + callback.onResponse(restResponse); + }); + // Response Will be returned in the callback + return; + } default: { logger.warn("Unsupported method: {}", method); diff --git a/src/main/java/com/webank/wecross/network/rpc/handler/StateURIHandler.java b/src/main/java/com/webank/wecross/network/rpc/handler/StateURIHandler.java index e0e255df2..a9fce57df 100644 --- a/src/main/java/com/webank/wecross/network/rpc/handler/StateURIHandler.java +++ b/src/main/java/com/webank/wecross/network/rpc/handler/StateURIHandler.java @@ -5,6 +5,7 @@ import com.webank.wecross.restserver.RestResponse; import com.webank.wecross.restserver.request.StateRequest; import com.webank.wecross.restserver.response.StateResponse; +import io.netty.handler.codec.http.HttpRequest; /** GET /sys/state */ public class StateURIHandler implements URIHandler { @@ -27,7 +28,12 @@ public void setHost(WeCrossHost host) { @Override public void handle( - UserContext userContext, String uri, String method, String content, Callback callback) { + UserContext userContext, + HttpRequest httpRequest, + String uri, + String method, + String content, + Callback callback) { StateResponse stateResponse = host.getState(new StateRequest()); RestResponse restResponse = new RestResponse<>(); diff --git a/src/main/java/com/webank/wecross/network/rpc/handler/SystemInfoHandler.java b/src/main/java/com/webank/wecross/network/rpc/handler/SystemInfoHandler.java index 611da1d9f..41a4e2df2 100644 --- a/src/main/java/com/webank/wecross/network/rpc/handler/SystemInfoHandler.java +++ b/src/main/java/com/webank/wecross/network/rpc/handler/SystemInfoHandler.java @@ -9,6 +9,7 @@ import com.webank.wecross.restserver.response.StubResponse; import com.webank.wecross.stubmanager.StubManager; import com.webank.wecross.zone.ZoneManager; +import io.netty.handler.codec.http.HttpRequest; import java.security.Provider; import java.security.Security; import java.util.stream.Collectors; @@ -34,7 +35,12 @@ public void setHost(WeCrossHost host) { @Override public void handle( - UserContext userContext, String uri, String method, String content, Callback callback) { + UserContext userContext, + HttpRequest httpRequest, + String uri, + String method, + String content, + Callback callback) { UriDecoder uriDecoder = new UriDecoder(uri); String operation = uriDecoder.getMethod(); switch (operation) { diff --git a/src/main/java/com/webank/wecross/network/rpc/handler/TestURIHandler.java b/src/main/java/com/webank/wecross/network/rpc/handler/TestURIHandler.java index bae82eae7..0b66c2d97 100644 --- a/src/main/java/com/webank/wecross/network/rpc/handler/TestURIHandler.java +++ b/src/main/java/com/webank/wecross/network/rpc/handler/TestURIHandler.java @@ -2,13 +2,19 @@ import com.webank.wecross.account.UserContext; import com.webank.wecross.restserver.RestResponse; +import io.netty.handler.codec.http.HttpRequest; /** GET /sys/test */ public class TestURIHandler implements URIHandler { @Override public void handle( - UserContext userContext, String uri, String method, String content, Callback callback) { + UserContext userContext, + HttpRequest httpRequest, + String uri, + String method, + String content, + Callback callback) { RestResponse restResponse = new RestResponse<>(); restResponse.setData("OK!"); diff --git a/src/main/java/com/webank/wecross/network/rpc/handler/TransactionURIHandler.java b/src/main/java/com/webank/wecross/network/rpc/handler/TransactionURIHandler.java index be8748bea..aa680feef 100644 --- a/src/main/java/com/webank/wecross/network/rpc/handler/TransactionURIHandler.java +++ b/src/main/java/com/webank/wecross/network/rpc/handler/TransactionURIHandler.java @@ -10,6 +10,7 @@ import com.webank.wecross.restserver.RestResponse; import com.webank.wecross.restserver.fetcher.TransactionFetcher; import com.webank.wecross.stub.*; +import io.netty.handler.codec.http.HttpRequest; import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,6 +41,7 @@ public void setTransactionFetcher(TransactionFetcher transactionFetcher) { @Override public void handle( UserContext userContext, + HttpRequest httpRequest, String uri, String httpMethod, String content, diff --git a/src/main/java/com/webank/wecross/network/rpc/handler/URIHandler.java b/src/main/java/com/webank/wecross/network/rpc/handler/URIHandler.java index d7458f193..6012ae08a 100644 --- a/src/main/java/com/webank/wecross/network/rpc/handler/URIHandler.java +++ b/src/main/java/com/webank/wecross/network/rpc/handler/URIHandler.java @@ -3,6 +3,7 @@ import com.webank.wecross.account.UserContext; import com.webank.wecross.restserver.RestResponse; import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpRequest; import java.io.File; /** */ @@ -18,5 +19,10 @@ interface Callback { } void handle( - UserContext userContext, String uri, String method, String content, Callback callback); + UserContext userContext, + HttpRequest httpRequest, + String uri, + String method, + String content, + Callback callback); } diff --git a/src/main/java/com/webank/wecross/network/rpc/handler/UpLoadFileURIHandler.java b/src/main/java/com/webank/wecross/network/rpc/handler/UpLoadFileURIHandler.java new file mode 100644 index 000000000..8cb8a673a --- /dev/null +++ b/src/main/java/com/webank/wecross/network/rpc/handler/UpLoadFileURIHandler.java @@ -0,0 +1,228 @@ +package com.webank.wecross.network.rpc.handler; + +import com.moandjiezana.toml.Toml; +import com.webank.wecross.account.UserContext; +import com.webank.wecross.common.NetworkQueryStatus; +import com.webank.wecross.common.WeCrossDefault; +import com.webank.wecross.config.WeCrossTomlConfig; +import com.webank.wecross.restserver.RestResponse; +import io.netty.handler.codec.http.*; +import io.netty.handler.codec.http.multipart.*; +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.StringJoiner; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; + +public class UpLoadFileURIHandler implements URIHandler { + private Logger logger = LoggerFactory.getLogger(UpLoadFileURIHandler.class); + private Toml toml = null; + + private enum FileType { + // 根证书 + ROOT_CERT, + // 根私钥 + ROOT_KEY, + // 用户签名证书 + USER_SIGN_CERT, + // 用户签名私钥 + USER_SIGN_KEY, + // 用户 TLS 通信证书 + USER_TLS_CERT, + // 用户 TLS 通信私钥 + USER_TLS_KEY, + // 节点签名证书 + NODE_SIGN_CERT, + // 节点 TLS 通信证书 + NODE_SIGN_KEY, + // 节点 TLS 通信证书 + NODE_TLS_CERT, + // 节点 TLS 通信私钥 + NODE_TLS_KEY + } + + @Override + public void handle( + UserContext userContext, + HttpRequest httpRequest, + String uri, + String method, + String content, + Callback callback) { + String chainType = ""; + String chainName = ""; + String stubsPath = ""; + String chainId = ""; + String orgId = ""; + String userName = ""; + int fileType = -1; + RestResponse response = new RestResponse<>(); + HttpPostRequestDecoder decoder = null; + List fileUploads = new ArrayList<>(); + try { + decoder = new HttpPostRequestDecoder(new DefaultHttpDataFactory(false), httpRequest); + + while (decoder.hasNext()) { + InterfaceHttpData data = decoder.next(); + if (data.getHttpDataType() == InterfaceHttpData.HttpDataType.Attribute) { + Attribute attribute = (Attribute) data; + try { + if ("chainType".equals(attribute.getName())) { + chainType = attribute.getValue(); + } else if ("chainName".equals(attribute.getName())) { + chainName = attribute.getValue(); + } else if ("chainId".equals(attribute.getName())) { + chainId = attribute.getValue(); + } else if ("orgId".equals(attribute.getName())) { + orgId = attribute.getValue(); + } else if ("fileType".equals(attribute.getName())) { + fileType = Integer.valueOf(attribute.getValue()); + } else if ("userName".equals(attribute.getName())) { + userName = attribute.getValue(); + } + } finally { + attribute.release(); + } + } else if (data.getHttpDataType() == InterfaceHttpData.HttpDataType.FileUpload) { + FileUpload fileUpload = (FileUpload) data; + if (fileUpload.isCompleted()) { + fileUploads.add(fileUpload); + } else { + fileUpload.release(); + } + } + } + if (chainType.isEmpty()) { + response.setErrorCode(NetworkQueryStatus.UPLOAD_RESOURCE_ERROR); + response.setMessage("chainType is missing."); + } else if (chainName.isEmpty()) { + response.setErrorCode(NetworkQueryStatus.UPLOAD_RESOURCE_ERROR); + response.setMessage("chainName is missing."); + } else if (fileType == -1) { + response.setErrorCode(NetworkQueryStatus.UPLOAD_RESOURCE_ERROR); + response.setMessage("fileType is missing."); + } else { + if (this.toml == null) { + WeCrossTomlConfig weCrossTomlConfig = new WeCrossTomlConfig(); + this.toml = weCrossTomlConfig.newToml(); + } + stubsPath = this.toml.getString("chains.path"); + if (stubsPath == null || stubsPath.isEmpty()) { + stubsPath = this.toml.getString("stubs.path"); // To support old version + } + + if (stubsPath == null || stubsPath.isEmpty()) { + response.setErrorCode(NetworkQueryStatus.INTERNAL_ERROR); + String errorMessage = + "\"path\" in [chains] item not found, please check " + + WeCrossDefault.MAIN_CONFIG_FILE; + response.setMessage(errorMessage); + } else { + List responseData = new ArrayList<>(); + for (FileUpload fileUpload : fileUploads) { + processUploadChain( + chainType, + chainName, + stubsPath, + chainId, + orgId, + userName, + fileType, + fileUpload); + responseData.add(fileUpload.getFilename()); + } + response.setErrorCode(NetworkQueryStatus.SUCCESS); + response.setData(responseData); + } + } + } catch (Exception e) { + logger.error("handling an uploaded file was failure. {}", e.getMessage()); + response.setErrorCode(NetworkQueryStatus.UPLOAD_RESOURCE_ERROR); + response.setMessage(e.getMessage()); + } finally { + if (decoder != null) { + decoder.destroy(); + } + + for (FileUpload fileUpload : fileUploads) { + fileUpload.release(); + } + } + + callback.onResponse(response); + } + + private void processUploadChain( + String chainType, + String chainName, + String stubsPath, + String chainId, + String orgId, + String userName, + int fileType, + FileUpload fileUpload) + throws Exception { + try { + PathMatchingResourcePatternResolver resolver = + new PathMatchingResourcePatternResolver(); + File dir = resolver.getResource(stubsPath).getFile(); + File newChainDir = new File(dir + File.separator + chainName); + if (!newChainDir.exists()) { + newChainDir.mkdirs(); + } + StringJoiner filePath = new StringJoiner(File.separator); + filePath.add(newChainDir.getPath()); + filePath.add("certs"); + if (!orgId.isEmpty()) { + filePath.add(orgId); + } + if (fileType == FileType.ROOT_CERT.ordinal() + || fileType == FileType.ROOT_KEY.ordinal()) { + filePath.add("ca"); + } else if (fileType == FileType.USER_SIGN_CERT.ordinal() + || fileType == FileType.USER_SIGN_KEY.ordinal()) { + filePath.add("user"); + if (!userName.isEmpty()) { + filePath.add(userName); + } + filePath.add("sign"); + } else if (fileType == FileType.USER_TLS_CERT.ordinal() + || fileType == FileType.USER_TLS_KEY.ordinal()) { + filePath.add("user"); + if (!userName.isEmpty()) { + filePath.add(userName); + } + filePath.add("tls"); + } else if (fileType == FileType.NODE_SIGN_CERT.ordinal() + || fileType == FileType.NODE_SIGN_KEY.ordinal()) { + filePath.add("node"); + if (!userName.isEmpty()) { + filePath.add(userName); + } + filePath.add("sign"); + } else if (fileType == FileType.NODE_TLS_CERT.ordinal() + || fileType == FileType.NODE_TLS_KEY.ordinal()) { + filePath.add("node"); + if (!userName.isEmpty()) { + filePath.add(userName); + } + filePath.add("tls"); + } + File localPath = new File(filePath.toString()); + if (localPath.exists() == false) { + localPath.mkdirs(); + } + filePath.add(fileUpload.getFilename()); + File outputFile = new File(filePath.toString()); + OutputStream outputStream = new FileOutputStream(outputFile); + outputStream.write(fileUpload.get()); + + } catch (Exception e) { + throw e; + } + } +} diff --git a/src/main/java/com/webank/wecross/network/rpc/handler/XATransactionHandler.java b/src/main/java/com/webank/wecross/network/rpc/handler/XATransactionHandler.java index 4e5ddb560..360128062 100644 --- a/src/main/java/com/webank/wecross/network/rpc/handler/XATransactionHandler.java +++ b/src/main/java/com/webank/wecross/network/rpc/handler/XATransactionHandler.java @@ -15,6 +15,7 @@ import com.webank.wecross.routine.xa.XATransactionManager; import com.webank.wecross.stub.ObjectMapperFactory; import com.webank.wecross.stub.Path; +import io.netty.handler.codec.http.HttpRequest; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -80,6 +81,7 @@ public void setOffsets(Map offsets) { @Override public void handle( UserContext userContext, + HttpRequest httpRequest, String uri, String httpMethod, String content, diff --git a/src/main/java/com/webank/wecross/network/rpc/netty/handler/HttpServerHandler.java b/src/main/java/com/webank/wecross/network/rpc/netty/handler/HttpServerHandler.java index 6e634f4ea..892d432b7 100644 --- a/src/main/java/com/webank/wecross/network/rpc/netty/handler/HttpServerHandler.java +++ b/src/main/java/com/webank/wecross/network/rpc/netty/handler/HttpServerHandler.java @@ -16,17 +16,8 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.socket.SocketChannel; -import io.netty.handler.codec.http.DefaultFullHttpResponse; -import io.netty.handler.codec.http.FullHttpRequest; -import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.handler.codec.http.HttpHeaderNames; -import io.netty.handler.codec.http.HttpHeaderValues; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpMethod; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.handler.codec.http.HttpUtil; -import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.*; +import io.netty.handler.codec.http.multipart.*; import io.netty.handler.ssl.SslCloseCompletionEvent; import io.netty.handler.ssl.SslHandshakeCompletionEvent; import io.netty.handler.timeout.IdleStateEvent; @@ -145,6 +136,31 @@ protected void doRouterRead(ChannelHandlerContext ctx, HttpRequest httpRequest) return; } + // TODO + // Avoid throw an exception names io.netty.util.IllegalReferenceCountException in thread + // pool(threadPoolTaskExecutor.execute). + // A better way may be fix this an issue later. + try { + if (HttpPostRequestDecoder.isMultipart(httpRequest)) { + HttpPostRequestDecoder decoder = + new HttpPostRequestDecoder(new DefaultHttpDataFactory(false), httpRequest); + decoder.destroy(); + } + } catch (Exception e) { + FullHttpResponse response = + new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.NOT_ACCEPTABLE, + Unpooled.wrappedBuffer(e.getMessage().getBytes())); + + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain"); + response.headers() + .set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes()); + // close connection + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + return; + } + threadPoolTaskExecutor.execute( () -> { UserContext userContext = new UserContext(); @@ -167,6 +183,7 @@ protected void doRouterRead(ChannelHandlerContext ctx, HttpRequest httpRequest) uriHandler.handle( userContext, + httpRequest, uri, method.toString(), content, @@ -403,6 +420,10 @@ public static boolean shouldLogin(String uri) { || uriMethod.startsWith("supportedStubs") || uriMethod.startsWith("login") || uriMethod.startsWith("register") + || uriMethod.startsWith("chain") + || uriMethod.startsWith("addChain") + || uriMethod.startsWith("removeChain") + || uriMethod.startsWith("connectChain") || splits[1].equalsIgnoreCase("s")) { return false; } diff --git a/src/main/java/com/webank/wecross/network/rpc/web/WebURIHandler.java b/src/main/java/com/webank/wecross/network/rpc/web/WebURIHandler.java index 23419287f..c0afe7e0d 100644 --- a/src/main/java/com/webank/wecross/network/rpc/web/WebURIHandler.java +++ b/src/main/java/com/webank/wecross/network/rpc/web/WebURIHandler.java @@ -3,10 +3,7 @@ import com.webank.wecross.account.UserContext; import com.webank.wecross.exception.WeCrossException; import com.webank.wecross.network.rpc.handler.URIHandler; -import io.netty.handler.codec.http.DefaultFullHttpResponse; -import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.*; import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; @@ -20,7 +17,12 @@ public class WebURIHandler implements URIHandler { @Override public void handle( - UserContext userContext, String uri, String method, String content, Callback callback) { + UserContext userContext, + HttpRequest httpRequest, + String uri, + String method, + String content, + Callback callback) { try { String filePath = toLocalPath(uri); diff --git a/src/main/java/com/webank/wecross/resource/Resource.java b/src/main/java/com/webank/wecross/resource/Resource.java index 917d5ae9e..4afe8f564 100644 --- a/src/main/java/com/webank/wecross/resource/Resource.java +++ b/src/main/java/com/webank/wecross/resource/Resource.java @@ -1,6 +1,12 @@ package com.webank.wecross.resource; +import com.moandjiezana.toml.Toml; import com.webank.wecross.account.UniversalAccount; +import com.webank.wecross.config.WeCrossTomlConfig; +import com.webank.wecross.mq.config.MqConfig; +import com.webank.wecross.mq.core.MqConnector; +import com.webank.wecross.mq.core.MqException; +import com.webank.wecross.mq.factory.MqConnectorFactory; import com.webank.wecross.peer.Peer; import com.webank.wecross.stub.*; import java.security.SecureRandom; @@ -12,6 +18,8 @@ import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.MessageBuilder; public class Resource { private Logger logger = LoggerFactory.getLogger(Response.class); @@ -24,6 +32,7 @@ public class Resource { boolean hasLocalConnection = false; boolean isTemporary = false; private Random random = new SecureRandom(); + private Map mqConnectors = new HashMap<>(); public static final String RAW_TRANSACTION = "RAW_TRANSACTION"; @@ -87,7 +96,7 @@ public void asyncCall( return; } - Account account = ua.getAccount(stubType); + Account account = ua.getAccount(stubType, path.getChain()); TransactionContext context = new TransactionContext(account, this.path, this.resourceInfo, this.blockManager); boolean isRawTransaction = @@ -134,7 +143,7 @@ public void asyncSendTransaction( return; } - Account account = ua.getAccount(stubType); + Account account = ua.getAccount(stubType, path.getChain()); TransactionContext context = new TransactionContext(account, this.path, this.resourceInfo, this.blockManager); boolean isRawTransaction = @@ -172,6 +181,83 @@ public void asyncSendTransaction( } } + private MqConnector getOrCreateMqConnector(String contract) throws Exception { + MqConnector connector = mqConnectors.get(contract); + if (connector == null) { + WeCrossTomlConfig tomlConfig = new WeCrossTomlConfig(); + Toml toml = tomlConfig.newToml(); + MqConfig mqConfig = new MqConfig(); + mqConfig.setMqType(toml.getString("mq.type")); + mqConfig.setHost(toml.getString("mq.host")); + mqConfig.setPort(toml.getLong("mq.port")); + mqConfig.setTopic(toml.getString("mq.topic")); + connector = MqConnectorFactory.createConnector(mqConfig); + mqConnectors.put(contract, connector); + } + return connector; + } + + private void pushSubscribeEvent(String contract, String topic, Object event) { + try { + Map values = new HashMap<>(); + values.put("TAG", topic); + values.put("KEY", contract); + MessageHeaders headers = new MessageHeaders(values); + MqConnector connector = getOrCreateMqConnector(contract); + connector.send(MessageBuilder.createMessage(event, headers)); + logger.info( + "push subscribe event success. contract: {}, topic: {}, data: {}", + contract, + topic, + event); + } catch (MqException e) { + logger.error("push subscribe event was failure. MqException: {}", e.getMessage()); + } catch (Exception e) { + logger.error("push subscribe event was failure. Exception: {}", e.getMessage()); + } + } + + public void subscribeEvent( + SubscribeRequest request, UniversalAccount ua, Resource.Callback callback) { + try { + checkAccount(ua); + } catch (TransactionException e) { + callback.onTransactionResponse(e, null); + return; + } + + Account account = ua.getAccount(stubType, path.getChain()); + TransactionContext context = + new TransactionContext(account, this.path, this.resourceInfo, this.blockManager); + + context.setCallback( + new TransactionContext.Callback() { + @Override + public void onSubscribe(String contract, String topic, Object event) { + logger.info( + "push subscribe event(contract: {}, topic: {}, data: {})", + contract, + topic, + event); + pushSubscribeEvent(contract, topic, event); + } + }); + + driver.subscribeEvent( + context, + request, + chooseConnection(), + (transactionException, transactionResponse) -> { + if (logger.isDebugEnabled()) { + logger.debug( + "subscribeEvent response: {}, exception: ", + transactionResponse, + transactionException); + } + callback.onTransactionResponse(transactionException, transactionResponse); + }); + } + public void onRemoteTransaction(Request request, Connection.Callback callback) { request.setResourceInfo(resourceInfo); chooseConnection().asyncSend(request, callback); @@ -199,7 +285,7 @@ private void checkAccount(UniversalAccount ua) throws TransactionException { TransactionException.ErrorCode.ACCOUNT_ERRPR, "UniversalAccount is null"); } - if (Objects.isNull(ua.getAccount(stubType))) { + if (Objects.isNull(ua.getAccount(stubType, path.getChain()))) { throw new TransactionException( TransactionException.ErrorCode.ACCOUNT_ERRPR, "Account with type '" + stubType + "' not found for " + ua.getName()); diff --git a/src/main/java/com/webank/wecross/stub/BlockHeader.java b/src/main/java/com/webank/wecross/stub/BlockHeader.java index bd5cc0018..b1cbb3a07 100644 --- a/src/main/java/com/webank/wecross/stub/BlockHeader.java +++ b/src/main/java/com/webank/wecross/stub/BlockHeader.java @@ -7,6 +7,7 @@ public class BlockHeader { private String stateRoot; private String transactionRoot; private String receiptRoot; + private long timestamp; public long getNumber() { return number; @@ -56,6 +57,14 @@ public void setReceiptRoot(String receiptRoot) { this.receiptRoot = receiptRoot; } + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + @Override public String toString() { return "BlockHeader{" diff --git a/src/main/java/com/webank/wecross/stub/Connection.java b/src/main/java/com/webank/wecross/stub/Connection.java index 3dd3aae1b..ba6052636 100644 --- a/src/main/java/com/webank/wecross/stub/Connection.java +++ b/src/main/java/com/webank/wecross/stub/Connection.java @@ -22,6 +22,8 @@ interface Callback { // Callback for setConnectionEventHandler interface ConnectionEventHandler { void onResourcesChange(List resourceInfos); + + void onANewResource(ResourceInfo resourceInfo); } /** * set the callback of connection events diff --git a/src/main/java/com/webank/wecross/stub/Driver.java b/src/main/java/com/webank/wecross/stub/Driver.java index 34164a82d..7d44745ce 100644 --- a/src/main/java/com/webank/wecross/stub/Driver.java +++ b/src/main/java/com/webank/wecross/stub/Driver.java @@ -58,6 +58,21 @@ void asyncSendTransaction( Connection connection, Driver.Callback callback); + /** + * subscribe contract event the interface of contract or chaincode Just fake async for + * compatibility, you need to override this function + * + * @param request the transaction request + * @param connection the connection of a chain + * @param callback the callback class for async sendTransaction + * @return the transaction response + */ + void subscribeEvent( + TransactionContext context, + SubscribeRequest request, + Connection connection, + Driver.Callback callback); + /** * Get block number * diff --git a/src/main/java/com/webank/wecross/stub/StubFactory.java b/src/main/java/com/webank/wecross/stub/StubFactory.java index ab9233f75..8b2902585 100644 --- a/src/main/java/com/webank/wecross/stub/StubFactory.java +++ b/src/main/java/com/webank/wecross/stub/StubFactory.java @@ -26,6 +26,8 @@ public interface StubFactory { */ public Connection newConnection(String path); + public void releaseConnection(); + /** * use sec and cert to new account * diff --git a/src/main/java/com/webank/wecross/stub/SubscribeRequest.java b/src/main/java/com/webank/wecross/stub/SubscribeRequest.java new file mode 100644 index 000000000..9473e08cd --- /dev/null +++ b/src/main/java/com/webank/wecross/stub/SubscribeRequest.java @@ -0,0 +1,55 @@ +package com.webank.wecross.stub; + +import java.util.List; + +public class SubscribeRequest { + // 起始区块 + private long fromBlockNumber; + private long toBlockNumber; + List topics; + + public SubscribeRequest() {} + + public SubscribeRequest(long fromBlockNumber, long toBlockNumber, List topics) { + this.fromBlockNumber = fromBlockNumber; + this.toBlockNumber = toBlockNumber; + this.topics = topics; + } + + public long getFromBlockNumber() { + return fromBlockNumber; + } + + public void setFromBlockNumber(long fromBlockNumber) { + this.fromBlockNumber = fromBlockNumber; + } + + public long getToBlockNumber() { + return toBlockNumber; + } + + public void setToBlockNumber(long toBlockNumber) { + this.toBlockNumber = toBlockNumber; + } + + public List getTopics() { + return topics; + } + + public void setTopics(List topics) { + this.topics = topics; + } + + @Override + public String toString() { + return "SubscribeRequest{" + + "fromBlockNumber = " + + fromBlockNumber + + ", toBlockNumber = " + + toBlockNumber + + ", topics = '" + + topics.toString() + + "\'" + + "}"; + } +} diff --git a/src/main/java/com/webank/wecross/stub/TransactionContext.java b/src/main/java/com/webank/wecross/stub/TransactionContext.java index d7e343cac..e8d88f0b6 100644 --- a/src/main/java/com/webank/wecross/stub/TransactionContext.java +++ b/src/main/java/com/webank/wecross/stub/TransactionContext.java @@ -47,6 +47,8 @@ public String toString() { + resourceInfo.toString() + ", blockManager=" + blockManager + + ", callback=" + + callback + '}'; } @@ -57,4 +59,18 @@ public Path getPath() { public void setPath(Path path) { this.path = path; } + + public interface Callback { + void onSubscribe(String contract, String topic, Object event); + } + + private Callback callback; + + public void setCallback(Callback callback) { + this.callback = callback; + } + + public Callback getCallback() { + return callback; + } } diff --git a/src/main/java/com/webank/wecross/stubmanager/StubManager.java b/src/main/java/com/webank/wecross/stubmanager/StubManager.java index e99f6a80d..566667fa9 100644 --- a/src/main/java/com/webank/wecross/stubmanager/StubManager.java +++ b/src/main/java/com/webank/wecross/stubmanager/StubManager.java @@ -42,7 +42,7 @@ public Account newStubAccount(String type, Map properties) { try { return getStubFactory(type).newAccount(properties); } catch (Exception e) { - logger.info("newStubAccount exception: ", e); + // logger.info("newStubAccount exception: ", e); return null; } } diff --git a/src/main/java/com/webank/wecross/utils/NetworkUtils.java b/src/main/java/com/webank/wecross/utils/NetworkUtils.java new file mode 100644 index 000000000..82397061c --- /dev/null +++ b/src/main/java/com/webank/wecross/utils/NetworkUtils.java @@ -0,0 +1,34 @@ +package com.webank.wecross.utils; + +import java.net.*; +import java.util.*; + +public class NetworkUtils { + /** 获取本地主机的第一个非回环IPv4地址 */ + public static String getLocalIP() { + try { + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + + while (interfaces.hasMoreElements()) { + NetworkInterface networkInterface = interfaces.nextElement(); + + if (networkInterface.isLoopback() || !networkInterface.isUp()) { + continue; + } + + Enumeration addresses = networkInterface.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress address = addresses.nextElement(); + + // 返回第一个IPv4地址 + if (address.getAddress().length == 4) { + return address.getHostAddress(); + } + } + } + } catch (SocketException e) { + e.printStackTrace(); + } + return "127.0.0.1"; // 默认返回localhost + } +} diff --git a/src/main/java/com/webank/wecross/utils/ToolUtils.java b/src/main/java/com/webank/wecross/utils/ToolUtils.java new file mode 100644 index 000000000..dc6c22639 --- /dev/null +++ b/src/main/java/com/webank/wecross/utils/ToolUtils.java @@ -0,0 +1,78 @@ +package com.webank.wecross.utils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.util.Enumeration; +import java.util.concurrent.TimeUnit; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +public class ToolUtils { + + public static String getFileExtension(File file) { + return getFileExtension(file.getName()); + } + + public static String getFileExtension(String fileName) { + int lastIndexOf = fileName.lastIndexOf("."); + if (lastIndexOf == -1) { + return ""; + } + return fileName.substring(lastIndexOf + 1); + } + + public static void unZip(String zipFilePath, String destDirectory) throws Exception { + ZipFile zipFile = new ZipFile(zipFilePath); + Enumeration entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + String entryName = entry.getName(); + File entryFile = new File(destDirectory, entryName); + if (entry.isDirectory()) { + entryFile.mkdirs(); + } else { + File parent = entryFile.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + + try (InputStream inputStream = zipFile.getInputStream(entry)) { + FileOutputStream outputStream = new FileOutputStream(entryFile); + byte[] buffer = new byte[1024]; + int length; + while ((length = inputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, length); + } + } + } + } + zipFile.close(); + } + + public static boolean executeShell(long waitSeconds, String... command) throws Exception { + ProcessBuilder processBuilder = new ProcessBuilder(command); + Process process = processBuilder.start(); + boolean completed = process.waitFor(waitSeconds, TimeUnit.SECONDS); + if (!completed) { + process.destroy(); + } + return completed; + } + + public static void deleteDirectory(File directory) { + if (directory.exists()) { + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + deleteDirectory(file); + } else { + file.delete(); + } + } + } + directory.delete(); + } + } +} diff --git a/src/main/java/com/webank/wecross/zone/Chain.java b/src/main/java/com/webank/wecross/zone/Chain.java index e2e49fdbb..6689efbbd 100644 --- a/src/main/java/com/webank/wecross/zone/Chain.java +++ b/src/main/java/com/webank/wecross/zone/Chain.java @@ -208,6 +208,34 @@ public void addResource(String name, Resource resource, boolean replaceIfExist) } } + public void addResource(ResourceInfo resourceInfo) { + lock.writeLock().lock(); + try { + String resourceName = resourceInfo.getName(); + Resource resource = resources.get(resourceName); + if (resource != null) { + return; + } + resource = new Resource(); + Path path = new Path(); + path.setZone(zoneName); + path.setChain(name); + path.setResource(resourceName); + resource.setPath(path); + resource.setStubType(stubType); + resource.setTemporary(false); + resource.setResourceInfo(resourceInfo); + resource.setBlockManager(blockManager); + resource.setDriver(driver); + resource.addConnection(null, localConnection); + resources.put(resourceName, resource); + } catch (Exception e) { + logger.debug("Exception: " + e); + } finally { + lock.writeLock().unlock(); + } + } + public void removeResource(Path path, boolean ignoreRemote) { removeResource(path.getResource(), ignoreRemote); } @@ -400,7 +428,7 @@ public void asyncCustomCommand( return; } - Account account = ua.getAccount(getStubType()); + Account account = ua.getAccount(getStubType(), getName()); if (Objects.isNull(account)) { callback.onResponse( new TransactionException( diff --git a/src/main/resources/chains-sample/chainmaker/stub-sample.toml b/src/main/resources/chains-sample/chainmaker/stub-sample.toml new file mode 100644 index 000000000..38016a85c --- /dev/null +++ b/src/main/resources/chains-sample/chainmaker/stub-sample.toml @@ -0,0 +1,25 @@ +[common] + name = 'chainmaker' + # ChainMakerWithCert/ChainMakerGMWithCert/ChainMakerWithPublic/ChainMakerGMWithPublic + type = 'ChainMakerWithCert' + +[endorsement] + entries = ['qctc003-org-id-001', 'qctc003-org-id-002', 'qctc003-org-id-003'] + +[[qctc003-org-id-001]] + user_key_file_path = 'crypto-config/qctc003-org-id-001/user/qctc003-org-admin-001/qctc003-org-admin-001.tls.key' + user_crt_file_path = 'crypto-config/qctc003-org-id-001/user/qctc003-org-admin-001/qctc003-org-admin-001.tls.crt' + user_sign_key_file_path = 'crypto-config/qctc003-org-id-001/user/qctc003-org-admin-001/qctc003-org-admin-001.sign.key' + user_sign_crt_file_path = 'crypto-config/qctc003-org-id-001/user/qctc003-org-admin-001/qctc003-org-admin-001.sign.crt' + +[[qctc003-org-id-002]] + user_key_file_path = 'crypto-config/qctc003-org-id-002/user/qctc003-org-admin-002/qctc003-org-admin-002.tls.key' + user_crt_file_path = 'crypto-config/qctc003-org-id-002/user/qctc003-org-admin-002/qctc003-org-admin-002.tls.crt' + user_sign_key_file_path = 'crypto-config/qctc003-org-id-002/user/qctc003-org-admin-002/qctc003-org-admin-002.sign.key' + user_sign_crt_file_path = 'crypto-config/qctc003-org-id-002/user/qctc003-org-admin-002/qctc003-org-admin-002.sign.crt' + +[[qctc003-org-id-003]] + user_key_file_path = 'crypto-config/qctc003-org-id-003/user/qctc003-org-admin-003/qctc003-org-admin-003.tls.key' + user_crt_file_path = 'crypto-config/qctc003-org-id-003/user/qctc003-org-admin-003/qctc003-org-admin-003.tls.crt' + user_sign_key_file_path = 'crypto-config/qctc003-org-id-003/user/qctc003-org-admin-003/qctc003-org-admin-003.sign.key' + user_sign_crt_file_path = 'crypto-config/qctc003-org-id-003/user/qctc003-org-admin-003/qctc003-org-admin-003.sign.crt' \ No newline at end of file diff --git a/src/main/resources/wecross-sample.toml b/src/main/resources/wecross-sample.toml index 06f7268a8..76bc72fe6 100644 --- a/src/main/resources/wecross-sample.toml +++ b/src/main/resources/wecross-sample.toml @@ -37,6 +37,21 @@ maxTotal = 200 maxPerRoute = 8 +[mq] + # kafka/rabbitmq/rocketmq + type = 'rocketmq' + host = '127.0.0.1' + port = 8081 + userName = '' + password = '' + topic = 'wecross' + group = '' + +[nacos] + serviceAddr = 'nacos:8848' + nameSpace = 'prod' + groupName = 'DEFAULT_GROUP' + dubboIpToRegistry = '127.0.0.1' #[[htlc]] # selfPath = 'payment.bcos.htlc' diff --git a/src/test/java/com/webank/wecross/test/resource/ResourceTest.java b/src/test/java/com/webank/wecross/test/resource/ResourceTest.java index 444366bed..19b3dc1cf 100644 --- a/src/test/java/com/webank/wecross/test/resource/ResourceTest.java +++ b/src/test/java/com/webank/wecross/test/resource/ResourceTest.java @@ -86,7 +86,7 @@ public void asyncCallAndTransactionTest() throws Exception { UniversalAccount ua = Mockito.mock(UniversalAccount.class); Account account = Mockito.mock(Account.class); - Mockito.when(ua.getAccount(Mockito.any())).thenReturn(account); + Mockito.when(ua.getAccount(Mockito.any(), Mockito.any())).thenReturn(account); resource.asyncCall( request, diff --git a/src/test/java/com/webank/wecross/test/rocketmq/producer/MQProducerTest.java b/src/test/java/com/webank/wecross/test/rocketmq/producer/MQProducerTest.java new file mode 100644 index 000000000..ae5a339f7 --- /dev/null +++ b/src/test/java/com/webank/wecross/test/rocketmq/producer/MQProducerTest.java @@ -0,0 +1,33 @@ +package com.webank.wecross.test.rocketmq.producer; + +import com.webank.wecross.mq.config.MqConfig; +import com.webank.wecross.mq.core.MqConnector; +import com.webank.wecross.mq.core.MqException; +import com.webank.wecross.mq.factory.MqConnectorFactory; +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.MessageBuilder; + +public class MQProducerTest { + @Test + public void produceTest() { + try { + // 参考: https://rocketmq.apache.org/zh/docs/quickStart/03quickstartWithDockercompose + MqConfig config = new MqConfig(); + config.setMqType("rocketmq"); + config.setHost("192.168.1.45"); + config.setPort(8081L); + config.setTopic("wecross"); + config.setGroup("wecross"); + MqConnector connector = MqConnectorFactory.createConnector(config); + Map values = new HashMap<>(); + values.put("TAG", "set_event"); + MessageHeaders headers = new MessageHeaders(values); + connector.send(MessageBuilder.createMessage("set_event(100,10)", headers)); + } catch (MqException e) { + System.out.println(e.getMessage()); + } + } +} diff --git a/src/test/java/com/webank/wecross/test/rpc/ConnectionURIHandlerTest.java b/src/test/java/com/webank/wecross/test/rpc/ConnectionURIHandlerTest.java index ec7d3c2c8..c14fc07fa 100644 --- a/src/test/java/com/webank/wecross/test/rpc/ConnectionURIHandlerTest.java +++ b/src/test/java/com/webank/wecross/test/rpc/ConnectionURIHandlerTest.java @@ -77,6 +77,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { AtomicInteger hit = new AtomicInteger(0); connectionURIHandler.handle( + null, null, "/listChains", "GET", @@ -113,6 +114,7 @@ public void onResponse(FullHttpResponse fullHttpResponse) { Assert.assertEquals(1, hit.intValue()); connectionURIHandler.handle( + null, null, "/listChains?zone=test-zone&offset=1000&size=100", "GET", @@ -149,6 +151,7 @@ public void onResponse(FullHttpResponse fullHttpResponse) { Set paths = new HashSet(); connectionURIHandler.handle( + null, null, "/listChains?zone=test-zone&offset=0&size=0", "GET", @@ -195,6 +198,7 @@ public void onResponse(FullHttpResponse fullHttpResponse) { final int index = i; connectionURIHandler.handle( + null, null, "/listChains?" + queryString, "GET", @@ -237,6 +241,7 @@ public void onResponse(FullHttpResponse fullHttpResponse) { Assert.assertEquals(13, hit.intValue()); connectionURIHandler.handle( + null, null, "/listChains?zone=test-zone2", "GET", @@ -269,6 +274,7 @@ public void onResponse(FullHttpResponse fullHttpResponse) { Assert.assertEquals(14, hit.intValue()); connectionURIHandler.handle( + null, null, "/listChains?zone=test-zone&offset=0&size=0", "GET", @@ -302,6 +308,7 @@ public void onResponse(FullHttpResponse fullHttpResponse) { paths.clear(); connectionURIHandler.handle( + null, null, "/listChains?zone=test-zone&offset=95&size=50", "GET", @@ -364,6 +371,7 @@ public void testListZones() throws Exception { final int index = i; connectionURIHandler.handle( + null, null, "/listZones?" + queryString, "GET", @@ -402,6 +410,7 @@ public void onResponse(FullHttpResponse fullHttpResponse) { paths.clear(); connectionURIHandler.handle( + null, null, "/listZones?offset=95&size=50", "GET", diff --git a/src/test/java/com/webank/wecross/test/rpc/URIHandlerDispatcherTest.java b/src/test/java/com/webank/wecross/test/rpc/URIHandlerDispatcherTest.java index 175080ee6..7b5aabce5 100644 --- a/src/test/java/com/webank/wecross/test/rpc/URIHandlerDispatcherTest.java +++ b/src/test/java/com/webank/wecross/test/rpc/URIHandlerDispatcherTest.java @@ -26,7 +26,7 @@ public void URIHandlerDispatcherTest() throws Exception { URIHandlerDispatcher uriHandlerDispatcher = new URIHandlerDispatcher(); uriHandlerDispatcher.initializeRequestMapper(host); - Assert.assertTrue(uriHandlerDispatcher.getRequestURIMapper().size() == 24); + Assert.assertTrue(uriHandlerDispatcher.getRequestURIMapper().size() == 26); Assert.assertTrue( Objects.nonNull( @@ -94,6 +94,7 @@ public void TestURIHandlerTest() throws Exception { null, null, null, + null, new URIHandler.Callback() { @Override public void onResponse(String restResponse) {