这是我的区块链专栏的第二篇,内容将围绕web3j 以及springBoot与我们之前创建好的链进行交互来写。怎么创建一条私链,请看上一篇文章。
重新讲一下我们的需求:我想做的是把一部分的数据上链,以便之后必要的时候能做一定的验证,你可以理解这是一个简单地溯源项目。
既然如此,那么我们的核心就是把数据上链,以及结合现实情况,让一切能够走得通。而完成这系列工作的核心就是:web3j
Web3j是一个轻量级,响应式,类型安全的Java库,用于与Ethereum网络上的客户端(节点)集成,核心就是这一点: 与Ethereum网络上的客户端(节点)集成
通过HTTP和IPC完成对Ethereum客户端API的实现
对于Ethereum钱包支持
使用过滤器的函数式编程功能的API
自动生成Java智能合约包装器,以创建、部署、处理和调用来自本地Java代码的智能合约
上一篇博客里我们是基于本地环境下。利用geth客户端与我们创建的私链进行交互,在实际开发中,我们不可能这样的,我们肯定是需要借助代码去和链进行交互;而web3j说白了就是做这样一件事情,需要记住的一点是: web3j不能进行智能合约编写!
这里只注入了web3j的依赖
org.web3j
core
3.4.0
我们现在还是基于本地进行测试,至于本地怎么搭建相关环境,请看上一篇博客。这里直接开启geth控制台,运行以下命令:
geth --identity "TestNode" --rpc --rpcport "8545" --datadir data --port "30303" --nodiscover --allow-insecure-unlock console
从上面指令我们知道,我们已经开启了8545端口;那么接下来我们就去和我们本机的8545端口通信,在web3j里是这样实现的:
//你可以这样:
Web3j web3j = Web3j.build(new HttpService());
//你也可以这样:
Web3j web3j = Web3j.build(new HttpService("http://localhost:8545"));
//你还可以这样:
Admin admin =Admin.build(new HttpService());
上面的方式都可以与我们本地客户端完成通信
与客户端进行通信之后,下面就是我们的代码实现部分,首先先完成创建链上账户:下面是我的完整代码
首先,创建一个以太坊钱包类:
public class ETHAccounts {
Integer id ;
//保存文件名
String keyStoreKey;
//12个单词的助记词
String memorizingWords;
//钱包公钥16进制字符串表示
String ethPublicKey;
//钱包私钥16进制字符串表示
String ethPrivateKey;
//钱包地址
String walletAddress;
//企业id
Integer terraceUserId;
//密码秘钥
String rsaPublicKey;
//密码公钥
String rsaPrivateKey;
//加密后密码
String walletPwd;
Integer status;
public String getWalletPwd() {
return walletPwd;
}
public void setWalletPwd(String walletPwd) {
this.walletPwd = walletPwd;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getKeyStoreKey() {
return keyStoreKey;
}
public void setKeyStoreKey(String keyStoreKey) {
this.keyStoreKey = keyStoreKey;
}
public String getMemorizingWords() {
return memorizingWords;
}
public void setMemorizingWords(String memorizingWords) {
this.memorizingWords = memorizingWords;
}
public String getEthPublicKey() {
return ethPublicKey;
}
public void setEthPublicKey(String ethPublicKey) {
this.ethPublicKey = ethPublicKey;
}
public String getEthPrivateKey() {
return ethPrivateKey;
}
public void setEthPrivateKey(String ethPrivateKey) {
this.ethPrivateKey = ethPrivateKey;
}
public String getWalletAddress() {
return walletAddress;
}
public void setWalletAddress(String walletAddress) {
this.walletAddress = walletAddress;
}
public Integer getTerraceUserId() {
return terraceUserId;
}
public void setTerraceUserId(Integer terraceUserId) {
this.terraceUserId = terraceUserId;
}
public String getRsaPublicKey() {
return rsaPublicKey;
}
public void setRsaPublicKey(String rsaPublicKey) {
this.rsaPublicKey = rsaPublicKey;
}
public String getRsaPrivateKey() {
return rsaPrivateKey;
}
public void setRsaPrivateKey(String rsaPrivateKey) {
this.rsaPrivateKey = rsaPrivateKey;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
@Override
public String toString() {
return "ETHAccounts{" +
"id=" + id +
", keyStoreKey='" + keyStoreKey + '\'' +
", memorizingWords='" + memorizingWords + '\'' +
", ethPublicKey='" + ethPublicKey + '\'' +
", ethPrivateKey='" + ethPrivateKey + '\'' +
", walletAddress='" + walletAddress + '\'' +
", terraceUserId=" + terraceUserId +
", rsaPublicKey='" + rsaPublicKey + '\'' +
", rsaPrivateKey='" + rsaPrivateKey + '\'' +
", walletPwd='" + walletPwd + '\'' +
'}';
}
}
上面的账户类有些字段现在用不上,但是可以先保存着以后用得上;接下来,通过web3j提供的方法进行钱包账户创建:
public Map newAccounts(String walletPwd) throws Exception {
Map maps = new HashMap<>();
ETHAccounts ethAccounts = new ETHAccounts();
Bip39Wallet wallet;
try {
//本地环境
wallet = WalletUtils.generateBip39Wallet(walletPwd, new File("D:/new/data/keystore/"));
} catch (Exception e) {
throw new Exception("创建以太坊钱包失败");
}
//通过钱包密码与助记词获得钱包地址、公钥及私钥信息
Credentials credentials = WalletUtils.loadBip39Credentials(walletPwd,
wallet.getMnemonic());
//钱包地址
ethAccounts.setWalletAddress(credentials.getAddress());
//钱包私钥16进制字符串表示
ethAccounts.setEthPrivateKey(credentials.getEcKeyPair().getPrivateKey().toString(16));
//钱包公钥16进制字符串表示
ethAccounts.setEthPublicKey(credentials.getEcKeyPair().getPublicKey().toString(16));
//保存文件名
ethAccounts.setKeyStoreKey(wallet.getFilename());
//12个单词的助记词
ethAccounts.setMemorizingWords(wallet.getMnemonic());
maps.put("ethAccounts",ethAccounts);
return maps;
}
这就完成了账户的创建,每次创建成功之后,你会在本地D:/new/data/keystore/目录下看到新增了一个文件。然后你在客户端上去运行eth.accounts也会发现账户增加了。
创建完账户余额之后,我们也可以通过web3j提供的ethGetBalance()方法来查看我们的账户余额,代码实现如下:
public Map accountsBanlance(String address) throws Exception {
Web3j web3j = Web3j.build(new HttpService());
Map maps = new HashMap<>();
try {
EthGetBalance ethGetBalance = web3j.ethGetBalance(address, DefaultBlockParameterName.LATEST).send();
if (ethGetBalance != null) {
maps.put("address",address);
maps.put("banlance",Convert.fromWei(ethGetBalance.getBalance().toString(), Convert.Unit.ETHER));
System.out.println("账号地址:" + address);
// 打印账户余额
System.out.println("账号余额:" + ethGetBalance.getBalance());
// 将单位转为以太
System.out.println("账号余额:" + Convert.fromWei(ethGetBalance.getBalance().toString(), Convert.Unit.ETHER)+"ETH");
}
}
catch (ConnectException e){
throw new ConnectException("################连接失败,客户端挂了");
} catch (SocketTimeoutException exception){
throw new SocketTimeoutException("###############连接超时,钱包地址有问题");
}
return maps;
}
这里只需要提供你的地址就可以了,而地址怎么查呢,你可以在geth客户端通过指令获取到你所有的账户以及地址。你也可以通过上面的创建账户方法,创建成功之后把返回的账户地址记录下来,然后就可以测试了,下面是我的java控制台输出:
账号地址:0x2bd7f1ca5fd6da34ca434923e579b1f12b77f47d
账号余额:115792089237316195423570985008687907853269984665640564039457584007913129639920
账号余额:115792089237316195423570985008687907853269984665640564039457.58400791312963992ETH
web3j提供了交易相关的许多方法,这也方便我们与我们搭建好的链去完成一系列相关的操作。
我的目的是数据上链,实现方法就是通过发起交易并且在交易的input字段上放上我想要上链的数据,只要交易成功发起,并且被矿工打包处理并上链,那么我的目的就达到了。而在这一步,核心就在于web3j的ethSendRawTransaction()方法;
参数:
DATA - 签名的交易数据
返回值:
DATA - 32字节,交易哈希,如果交易未生效则返回全0哈希。
当创建合约时,在交易生效后,使用ethGetTransactionReceipt获取合约地址。
其实还有一个很像的方法,也就是ethSendTransaction()方法,但是这个更多是用来进行合约部署以及相关操作
接下来贴上我的代码实现
/** 创建交易
*
*/
public Map newTransaction(Integer num, String from, BigInteger value, String passWord,
String to, String keyStoreKey, String input) throws Exception {
Web3j web3j = Web3j.build(new HttpService());
Map maps = new HashMap<>();
try {
//获取账户余额
EthGetBalance ethGetBalance = web3j.ethGetBalance(from, DefaultBlockParameterName.LATEST).send();
if (ethGetBalance != null&ðGetBalance.getBalance().compareTo(value) == 1) {
// 将单位转为以太,方便查看
System.out.println("账号余额:" + Convert.fromWei(ethGetBalance.getBalance().toString(), Convert.Unit.ETHER));
// 第一个变量填入账户的密码,第二个变量填入账户文件的 path,可以在私链数据文件夹中的 keystore 文件夹中找到,是一个UTC开头的文件
Credentials credentials = WalletUtils.loadCredentials(passWord, keyStoreKey);
/*也可以通过私钥的方式*/
/* Credentials credentials = Credentials.create("xxxxxxxxxxxxx");*/
//创建交易
RawTransaction rawTransaction = RawTransaction.createTransaction(nonce, Contract.GAS_PRICE,Contract.GAS_LIMIT,
to, new BigInteger("1"), input);
//签名
byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, credentials);
String hexValue = Numeric.toHexString(signedMessage);
//发起交易
EthSendTransaction ethSendTransaction =
web3j.ethSendRawTransaction(hexValue).send();
String transactionHash = ethSendTransaction.getTransactionHash();
maps.put("transactionHash",transactionHash);
logger.info("transactionHash" + transactionHash);
}else {
throw new Exception("钱包账户余额不足");
}
}catch (ConnectException e){
throw new ConnectException("################连接失败,客户端挂了");
}catch (SocketTimeoutException exception){
throw new SocketTimeoutException("###############连接超时,钱包地址有问题");
}
return maps;
}
其中,各参数意义如下:
num:链上交易数,类似id的存在,从0开始计数,只有前面的nonce的交易被处理了,才会处理后面的nonce;如果nonce太小,交易会被拒绝,如果nonce太大,那么交易会被放在等待队列中,等前面的nonce全部存在并且处理完之后,才处理你的交易。切记是存在且处理完。
from:交易发起方的地址
value:转账的资金
passWord:交易发起方的密码
to:交易接收方的地址
keyStoreKey:交易发起方的秘钥文件
input:该笔交易所插入的字段
而 RawTransaction.createTransaction()方法各参数所代表的意思如下:
gasPrice:这个可以理解是处理交易给矿工的费用,在以太坊上,gasPrice越大越早会被处理
gasLimit:这个是矿工费用的最大值。
上面代码里有讲到,如果你有你的账户私钥的话,那么你可以直接通过私钥的方式完成交易,执行完之后,会在geth客户端返回:
INFO [12-05|11:54:31.847] Submitted transaction fullhash=0xdb84d4827a0ed9c0f9d1fcc78c9b906f7cd199cc683f0333317e120497b83fc3 recipient=0x8DF95CC3bAEd5D10D3a27C2705E3726c9AaDf635
而在java控制台,则可以看到返回的map内容如下:
{"transactionHash":"0xdb84d4827a0ed9c0f9d1fcc78c9b906f7cd199cc683f0333317e120497b83fc3"}
这也就意味着你的请求成功了,接下来你可以在geth控制台运行:miner.start()进行挖矿,打包处理完成之后,也就意味着你的数据成功上链了,这个时候你可以在链上查看到你刚才放在input字段里的上链数据了
在这里,可能会存在的几个bug,如下:
首先就是可能回出现nonce太小,交易被拒绝的情况;
其次,gasPrice和gasLimit都不能太小,否则可能会报 Exceeds block gas limit 这样的错误;因为实际在数据上链的时候,你的费用会随你的数据量大小而改变。而且如果你创建你的私链的时候,你的genesis.json文件里面的gasLimit太小的话,你还需要修改并且重新创建私链重新来一遍。(上篇博客里讲到了按最大的设,如果你没私自修改的话那就没问题);如果报了“Insufficient funds for gas * price + value”这个bug的话,意思是交易所需手续费超过了你的余额,也就是你钱不够了,赶紧挖点矿处理一下就可以了。
还有一个,input字段需要的是16进制表示,你可以在 用我这个"0xE68891E788B1E4BDA0",有惊喜哦;后续我会给完整代码,包含字符串转换成16进制。
完成交易之后,我们就可以拿到我们的交易hash值,而根据这个hash值,借助web3j的ethGetTransactionByHash()方法我们就可以拿来获取交易的完整信息以及区块信息。
实现代码如下:
/**
* 通过交易hash获取交易信息
*/
public Map getTransactionByHash(String transactionHash) throws IOException{
Map map = new HashMap<>();
Web3j web3j = Web3j.build(new HttpService());
Optional et = web3j.ethGetTransactionByHash(transactionHash).send().getTransaction();
Transaction transaction = et.get();
map.put("transaction",transaction);
return map;
}
//通过交易hash获取区块信息
public EthBlock getBlockByHash(String transactionHash) throws IOException{
Web3j web3j = Web3j.build(new HttpService());
//为true返回完整区块信息,false只返回交易hash
EthBlock ethBlock = web3j.ethGetBlockByHash(transactionHash,true).send();
return ethBlock;
}
需要注意的一点是,只有在矿工打包处理完之后,你才可以通过交易hash值获取到区块信息
web3j官方给出的建议就是监听链上的日志,存到数据库里,然后在这个数据库中查询。也就是我们监听我们的日志,然后把相关交易信息保存到我们的中心数据库上,之后相关操作都对中心数据库去操作即可,没必要一直与链做交互。
下面是我的实现代码:
public Map listen() throws Exception {
Web3j web3j = Web3j.build(new HttpService());
Map maps = new HashMap<>();
Subscription subscription = web3j.transactionObservable().subscribe(tx -> {
try {
logger.info("New tx: id={}, blockHash={}, fromAddress={}, toAddress={}, value={},input={},nonce={}", tx.getHash(), tx.getBlockHash(),
tx.getFrom(), tx.getTo(), tx.getValue().intValue(), tx.getInput(),tx.getNonce());
//获取总条数
EthGetTransactionCount transactionCount = web3j.ethGetTransactionCount(tx.getFrom(), DefaultBlockParameterName.LATEST).send();
logger.info("Tx count: {}", transactionCount.getTransactionCount().intValue());
} catch (org.web3j.protocol.core.filters.FilterException ee) {
logger.error("这里有个bug:", ee);
} catch (IOException e) {
logger.error("这里有个bug:", e);
} catch (ParseException e) {
e.printStackTrace();
}
});
return maps;
}
还可以这样实现:
List txs = web3j.ethGetBlockByNumber(DefaultBlockParameterName.LATEST, true).send().getBlock().getTransactions();
txs.forEach(tx -> {
EthBlock.TransactionObject transaction = (EthBlock.TransactionObject) tx.get();
System.out.println(transaction.getFrom());
})
当然,你也可以借助私有链浏览器去看,但是,这种实际上,没太大意义。
上面代码里面,用到了一个东西:过滤器
过滤器提供了在以太坊网络中发生的某些事件的通知,这也是我们能实现监听的关键。以太坊支持三种类型的过滤器,分别是:块过滤器、
待处理的交易过滤器以及主题过滤器。虽然以太坊支持过滤器,但是会有一个问题,由于HTTP和IPC请求的同步性质,,除非我们使用WebSocket进行连接客户端,否则在实际情况上我们只能不断去轮询我们的geth客户端来达到监听的效果
web3j的托管过滤器解决了这些问题,因此您有一个完全异步的基于事件的API来处理过滤器。它使用RxJava的Observable,它提供了一个一致的API来处理事件,这有助于通过功能组合将JSON-RPC调用链接在一起。
在添加到区块链时接收所有新区块(false参数指定我们只需要区块,而不是嵌入式交易)
Subscription subscription = web3j.blockObservable(false).subscribe(block -> {
...
});
在添加到区块链时接收所有新交易:
Subscription subscription = web3j.transactionObservable().subscribe(tx -> {
...
});
重播某一系列的区块交易信息:
Subscription subscription = web3j.replayBlocksObservable(
, , )
.subscribe(block -> {
...
});
public final class HexBin {
//字符串转十六进制
public static String toChineseHex(String s)
{
String ss = s;
byte[] bt = new byte[0];
try {
bt = ss.getBytes("UTF-8");
}catch (Exception e){
e.printStackTrace();
}
String s1 = "";
for (int i = 0; i < bt.length; i++)
{
String tempStr = Integer.toHexString(bt[i]);
if (tempStr.length() > 2)
tempStr = tempStr.substring(tempStr.length() - 2);
s1 = s1 + tempStr + "";
}
return s1.toUpperCase();
}
// 转化十六进制编码为字符串
public static String toStringHex(String s) throws Exception {
byte[] baKeyword = new byte[s.length() / 2];
for (int i = 0; i < baKeyword.length; i++) {
baKeyword[i] = (byte) (0xff & Integer.parseInt(s.substring(
i * 2, i * 2 + 2), 16));
}
// UTF-16le:Not
s = new String(baKeyword, "utf-8");
return s;
}
}
public class RsaUtil {
/**
* 随机生成密钥对
* @throws NoSuchAlgorithmException
*/
private static Map genKeyPair() throws NoSuchAlgorithmException {
Map keyMap = new HashMap();
// KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
// 初始化密钥对生成器,密钥大小为96-1024位
keyPairGen.initialize(1024,new SecureRandom());
// 生成一个密钥对,保存在keyPair中
KeyPair keyPair = keyPairGen.generateKeyPair();
// 得到私钥
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
// 得到公钥
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
String publicKeyString = new String(Base64.encodeBase64(publicKey.getEncoded()));
// 得到私钥字符串
String privateKeyString = new String(Base64.encodeBase64((privateKey.getEncoded())));
// 将公钥和私钥保存到Map
keyMap.put("publicKeyString",publicKeyString);
keyMap.put("privateKeyString",privateKeyString);
return keyMap;
}
/**
* RSA公钥加密
* @param str 加密字符串
* @param publicKey 公钥
* @return 密文
* @throws Exception
* 加密过程中的异常信息
*/
public static String encrypt(String str, String publicKey ) throws Exception{
//base64编码的公钥
byte[] decoded = Base64.decodeBase64(publicKey);
RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded));
//RSA加密
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
String outStr = Base64.encodeBase64String(cipher.doFinal(str.getBytes("UTF-8")));
return outStr;
}
/**
* RSA私钥解密
* @param str 加密字符串
* @param privateKey 私钥
* @return 铭文
* @throws Exception
* 解密过程中的异常信息
*/
public static String decrypt(String str, String privateKey) throws Exception{
//64位解码加密后的字符串
byte[] inputByte = Base64.decodeBase64(str.getBytes("UTF-8"));
//base64编码的私钥
byte[] decoded = Base64.decodeBase64(privateKey);
RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded));
//RSA解密
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, priKey);
String outStr = new String(cipher.doFinal(inputByte));
return outStr;
}
}
public String generatePassword (int length) {
// 最终生成的密码
String password = "";
Random random = new Random();
for (int i = 0; i < length; i ++) {
// 随机生成0或1,用来确定是当前使用数字还是字母 (0则输出数字,1则输出字母)
int charOrNum = random.nextInt(2);
if (charOrNum == 1) {
// 随机生成0或1,用来判断是大写字母还是小写字母 (0则输出小写字母,1则输出大写字母)
int temp = random.nextInt(2) == 1 ? 65 : 97;
password += (char) (random.nextInt(26) + temp);
} else {
// 生成随机数字
password += random.nextInt(10);
}
}
return password;
}
// length表示密码长度
至此,你基本完成了所有的核心代码的开发,在这里我附上几个很有用的链接:
以太坊常见问题和错误
以太坊JSON RPC手册 :这个虽然像是在写geth客户端指令,但是仔细一看你会发现,web3j的核心方法和这个一模一样,可以通过这个查一些方法所需参数的意思。
Geth管理API文档: 这个是geth客户端很完整的指令文档
如果中间有问题或者其他情况可以留言,我们一起交流学习