说在前头
Web3是一种新兴的网络概念,由于某些原因导致我们能够接触到的相关技术知识实在有限,每当我遇见技术瓶颈总是不能找到充足的资料,这也让我萌生了填补这片空白知识的冲动。“Hello Web3” 这个专栏会尽力将我掌握的web3 知识分享给大家。如果分享的知识能帮助到大家,希望能够 关注、点赞 支持作者!
本人已在github上发布Web3j工具,欢迎使用和star
Java与智能合约交互(Web3j)
之所以选择利用java与智能合约进行交互,完全是因为本人只会Java,并且Java是世界上最好的语言。
能干什么
- 监控合约状态,读取合约的关键参数,可作为后台数据源。
- 转账、授权等基础交互。
- 实现例如抢购、提挖买等复杂交互。
代码分享
- 引入依赖
<dependency>
<groupId>org.web3j</groupId>
<artifactId>core</artifactId>
<version>5.0.0</version>
</dependency>
- 新建Web3j对象
Web3j web3 = Web3j.build(new HttpService(rpcUrl));
- rpcUrl变量是区块链网络节点的url链接,这些节点会提供很多标准的api方法通过该url进行调用,web3j模块就是在此api上进行封装。
- 不同网络的rpcUrl可以在对应的区块链浏览器api文档上找到,百度关键字也很容易获取。
- 查询当前网络的gasPrice
// 获取gasPrice方法
BigInteger gasPrice = web3.ethGasPrice().send().getGasPrice();
/**
* 获取当期的gasPrice,如果超过最大的限制,取最大限制
*
* @return 在区间内的gasPrice
* @throws IOException 与节点交互出现异常
*/
public BigInteger getGasPriceWithLimit() throws IOException {
BigInteger gasPrice = web3.ethGasPrice().send().getGasPrice();
log.info("Gas price: {} Gwei, Min gas price: {} Gwei, Max gas price: {} Gwei",
Convert.fromWei(String.valueOf(gasPrice), Convert.Unit.GWEI),
Convert.fromWei(String.valueOf(minGasPrice), Convert.Unit.GWEI),
Convert.fromWei(String.valueOf(maxGasPrice), Convert.Unit.GWEI));
// 超过最大限制返回最大的gasPrice
if (maxGasPrice.compareTo(gasPrice) < 0) {
return maxGasPrice;
}
// 小于最小的限制返回最小的gasPrice
if (minGasPrice.compareTo(gasPrice) > 0) {
return minGasPrice;
}
return gasPrice;
}
- 注意,该方法会获取近几个区块的gasPrice均值,可能偏高。
- 参考作者的getGasPriceWithLimit方法,如果你不是抢购之类紧急的需求,建议不要直接使用这个方法的返回值作为gasPrice,不然亏哭你!
- 查询当前账户交易笔数用于作为交易nonce
/**
* 获取交易数量
*
* @return 账户交易次数
* @throws IOException 与节点交互失败
*/
public BigInteger getTransactionCount() throws IOException {
EthGetTransactionCount ethGetTransactionCount = web3.ethGetTransactionCount(
ownerAddress, DefaultBlockParameterName.LATEST).send();
return ethGetTransactionCount.getTransactionCount();
}
- 以太坊虚拟机用这个nonce字段作为顺序来处理你的多个交易,默认一般nonce取账户的交易笔数
- 本次交易nonce比上笔成功的交易还小的话会导致交易失败
- 查询当前区块链网络的链的chainId
long chainId = web3.ethChainId().send().getChainId().longValue();
- 在以太坊经典从以太坊分叉出来后,为了防止双花攻击,Chain ID在EIP155被引入。
- 通过在签名信息中加入Chain ID, 避免一个交易在签名之后被重复在不同的链上提交。
- 导入私钥(代码中的私钥切勿传到github公有仓库!!!)
Credentials credentials = Credentials.create(privateKey);
// 私钥对应的地址
String address = credentials.getAddress();
- 估算、签名、发送交易数据
/**
* 与合约交互
*
* @param contractAddress 交互合约地址
* @param functionName 交互函数名称
* @param value 携带的eth数量(单位Ether)
* @param input 输入参数 eg:Arrays.asList(new Address("0x6dF655480F465DC36347a5616E875D155804F0c5"), new Uint256(10000000));
* @param output 输出参数类型 eg: Arrays.asList(new TypeReference<Bool>(){});
* 类型映射关系
* boolean - bool
* BigInteger - uint/int
* byte[] - bytes
* String - string and address types
* List - dynamic/static array
* T - struct/tuple types
* @return 交易hash
* @throws Exception 与节点交互出现异常
*/
public String writeContract(String contractAddress, String functionName, String value, List<Type> input, List<TypeReference<?>> output) throws Exception {
// 转换value的单位
BigInteger valueWei = Convert.toWei(value, Convert.Unit.ETHER).toBigInteger();
// 生成需要调用函数的data
Function function = new Function(functionName, input, output);
String data = FunctionEncoder.encode(function);
// 估算gasLimit
BigInteger gasLimit = estimateGasLimit(contractAddress, data, valueWei);
// 获取gasPrice
BigInteger gasPrice = getGasPriceWithLimit();
// 获取chainId
long chainId = web3.ethChainId().send().getChainId().longValue();
// 正式请求
RawTransaction rawTransaction = RawTransaction.createTransaction(getNonce(), gasPrice, gasLimit, contractAddress, valueWei, data);
// 签名数据
byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, chainId, credentials);
String hexValue = Numeric.toHexString(signedMessage);
// 发送数据
EthSendTransaction response = web3.ethSendRawTransaction(hexValue).send();
// 查看是否有错误
if (response.hasError()) {
throw new Exception("trade hash: " + response.getTransactionHash() +
"\nerror: " + response.getError().getMessage());
}
log.info("function: {} data: {}", functionName, data);
log.info("Gas fee: {} ETH", Convert.fromWei(String.valueOf(gasLimit.multiply(gasPrice)), Convert.Unit.ETHER));
log.info("Trade Hash: {}", response.getTransactionHash());
return response.getTransactionHash();
}
/**
* 估算GasLimit
*
* @param to 发送的地址
* @param data 发送的数据
* @param value 携带的eth数量(单位wei)
* @return GasLimit
* @throws Exception 与节点交互失败
*/
public BigInteger estimateGasLimit(String to, String data, BigInteger value) throws Exception {
if (gasLimit.intValue() != 0) {
return gasLimit;
}
Transaction testTransaction = Transaction.createFunctionCallTransaction(ownerAddress, null, null, null, to, value, data);
EthEstimateGas response = web3.ethEstimateGas(testTransaction).send();
// 查看是否有错误
if (response.hasError()) {
throw new Exception("error: " + response.getError().getMessage());
}
return response.getAmountUsed();
}
- writeContract函数的相关参数说明请参考上一篇文章《read函数》
- estimateGasLimit函数用于计算本次执行的交易需要耗费的Gas数量,实际就是把你的交易数据传到链上模拟执行函数但不提交结果,将模拟执行花费的Gas作为请求结果返回
- 当拿到交易所需的所有数据,需要用私钥对数据进行签名操作方可执行
- 调用写函数执行转账操作
/**
* 转账操作
*
* @param contractAddress 交互合约地址
* @param recipient 接收转账地址
* @param amount 转账数量
* @return 交易hash
* @throws Exception 与节点交互失败
*/
public String transfer(String contractAddress, String recipient, String amount) throws Exception {
List input = Arrays.asList(new Address(recipient)
, new Uint256(new BigInteger(amount, 10)));
List output = Arrays.asList(new TypeReference<Bool>() {
});
return writeContract(contractAddress, "transfer", input, output);
}
掌握这些知识点最好的方法是自己将代码跑起来,去链上获取你想获取的信息
欢迎关注本专栏,作者知无不言~