基於web3j和springBoot與鏈進行交互

前言

這是我的區塊鏈專欄的第二篇,內容將圍繞web3j 以及springBoot與我們之前創建好的鏈進行交互來寫。怎麼創建一條私鏈,請看上一篇文章。

需求

重新講一下我們的需求:我想做的是把一部分的數據上鍊,以便之後必要的時候能做一定的驗證,你可以理解這是一個簡單地溯源項目。
既然如此,那麼我們的核心就是把數據上鍊,以及結合現實情況,讓一切能夠走得通。而完成這系列工作的核心就是:web3j

web3j

web3j是什麼?

Web3j是一個輕量級,響應式,類型安全的Java庫,用於與Ethereum網絡上的客戶端(節點)集成,核心就是這一點: 與Ethereum網絡上的客戶端(節點)集成

web3所提供的核心功能

  1. 通過HTTP和IPC完成對Ethereum客戶端API的實現

  2. 對於Ethereum錢包支持

  3. 使用過濾器的函數式編程功能的API

  4. 自動生成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&&ethGetBalance.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客戶端很完整的指令文檔

如果中間有問題或者其他情況可以留言,我們一起交流學習

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章