前言
這是我的區塊鏈專欄的第二篇,內容將圍繞web3j 以及springBoot與我們之前創建好的鏈進行交互來寫。怎麼創建一條私鏈,請看上一篇文章。
文章目錄
需求
重新講一下我們的需求:我想做的是把一部分的數據上鍊,以便之後必要的時候能做一定的驗證,你可以理解這是一個簡單地溯源項目。
既然如此,那麼我們的核心就是把數據上鍊,以及結合現實情況,讓一切能夠走得通。而完成這系列工作的核心就是:web3j
web3j
web3j是什麼?
Web3j是一個輕量級,響應式,類型安全的Java庫,用於與Ethereum網絡上的客戶端(節點)集成,核心就是這一點: 與Ethereum網絡上的客戶端(節點)集成
web3所提供的核心功能
-
通過HTTP和IPC完成對Ethereum客戶端API的實現
-
對於Ethereum錢包支持
-
使用過濾器的函數式編程功能的API
-
自動生成Java智能合約包裝器,以創建、部署、處理和調用來自本地Java代碼的智能合約
上一篇博客裏我們是基於本地環境下。利用geth客戶端與我們創建的私鏈進行交互,在實際開發中,我們不可能這樣的,我們肯定是需要藉助代碼去和鏈進行交互;而web3j說白了就是做這樣一件事情,需要記住的一點是: web3j不能進行智能合約編寫!
springBoot項目下進行web3j的整合
導入依賴
這裏只注入了web3j的依賴
<!-- web3j-->
<dependency>
<groupId>org.web3j</groupId>
<artifactId>core</artifactId>
<version>3.4.0</version>
</dependency>
開啓以太坊客戶端
我們現在還是基於本地進行測試,至於本地怎麼搭建相關環境,請看上一篇博客。這裏直接開啓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<String, Object> newAccounts(String walletPwd) throws Exception {
Map<String, Object> 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<String, Object> accountsBanlance(String address) throws Exception {
Web3j web3j = Web3j.build(new HttpService());
Map<String, Object> 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()方法;
Web3j.ethSendRawTransaction()
參數:
DATA - 簽名的交易數據
返回值:
DATA - 32字節,交易哈希,如果交易未生效則返回全0哈希。
當創建合約時,在交易生效後,使用ethGetTransactionReceipt獲取合約地址。
Web3j.ethSendTransaction()
其實還有一個很像的方法,也就是ethSendTransaction()方法,但是這個更多是用來進行合約部署以及相關操作
我的代碼實現:
接下來貼上我的代碼實現
/** 創建交易
*
*/
public Map<String, Object> newTransaction(Integer num, String from, BigInteger value, String passWord,
String to, String keyStoreKey, String input) throws Exception {
Web3j web3j = Web3j.build(new HttpService());
Map<String, Object> 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值,而根據這個hash值,藉助web3j的ethGetTransactionByHash()方法我們就可以拿來獲取交易的完整信息以及區塊信息。
實現代碼如下:
/**
* 通過交易hash獲取交易信息
*/
public Map<String,Object> getTransactionByHash(String transactionHash) throws IOException{
Map<String,Object> map = new HashMap<>();
Web3j web3j = Web3j.build(new HttpService());
Optional<Transaction> 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<String, Object> 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<EthBlock.TransactionResult> 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(
<startBlockNumber>, <endBlockNumber>, <fullTxObjects>)
.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;
}
}
RSA加密工具類:
public class RsaUtil {
/**
* 隨機生成密鑰對
* @throws NoSuchAlgorithmException
*/
private static Map<String, String> genKeyPair() throws NoSuchAlgorithmException {
Map<String, String> keyMap = new HashMap<String, String>();
// 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客戶端很完整的指令文檔
如果中間有問題或者其他情況可以留言,我們一起交流學習