美文网首页
ETH(以太坊) 离线交易相关知识的学习总结(JAVA)

ETH(以太坊) 离线交易相关知识的学习总结(JAVA)

作者: 一个脱离高级趣味的人 | 来源:发表于2020-01-22 16:06 被阅读0次

    最近项目上需要做一个类似钱包相关的东西,需要的一些知识和以前不同,以前操作的一直是内部的全节点,私钥啥的全部存储在自己的节点上,节点相当于一个钱包,直接操作节点上的钱包就好。现在需要在移动端或者web端签名交易之后,然后再用节点进行广播,接触到了一些新知识,记录下来吧。

    准备公共节点

    以太坊比其它一些公链好的地方是尽然提供了免费的公共节点(有 mainnetropstenkovanrinkeby 供选择) ,这个节点只要注册登录之后创建项目选择自己需要的网络类型就好,这里为了方便测试我选择的 rinkeby

    生成的 url 像这个样子 https://rinkeby.infura.io/YOUR PROJECT SECRET

    创建项目

    这里使用的是一个Gradle 项目,在 build.gradle 下的 dependencies 加入以下依赖

    implementation 'org.bitcoinj:bitcoinj-core:0.15.6'
    implementation 'org.web3j:core:4.5.5'
    

    bitcoinj-core 是比特币开发的基础包,后面创建账号会用到,web3j 是Java版本的以太坊JSON RPC 接口协议封装实现。

    具体使用

    连接节点

    我们这里创建一个单例来连接节点

    public class Web3jClient {
    
        private static volatile Web3j instance;
    
        public static Web3j instance() {
            if (instance == null) {
                synchronized (Web3j.class) {
                    if (instance == null) {
                        instance = Web3j.build(new HttpService("https://rinkeby.infura.io/YOUR PROJECT SECRET"));
                    }
                }
            }
            return instance;
        }
    }
    
    创建账户
    /**
     * 生成地址和私钥
     */
    public void createAddress() {
        try {
            SecureRandom secureRandom = new SecureRandom();
            byte[] entropy = new byte[DeterministicSeed.DEFAULT_SEED_ENTROPY_BITS / 8];
            secureRandom.engineNextBytes(entropy);
            // 生成12位助记词
            List<String> mnemonics = MnemonicCode.INSTANCE.toMnemonic(entropy);
            // 使用助记词生成钱包种子
            byte[] seed = MnemonicCode.toSeed(mnemonics, "");
            DeterministicKey masterPrivateKey = HDKeyDerivation.createMasterPrivateKey(seed);
            DeterministicHierarchy deterministicHierarchy = new DeterministicHierarchy(masterPrivateKey);
            DeterministicKey deterministicKey = deterministicHierarchy.deriveChild(BIP44_ETH_ACCOUNT_ZERO_PATH, false, true, new ChildNumber(0));
            byte[] bytes = deterministicKey.getPrivKeyBytes();
            ECKeyPair keyPair = ECKeyPair.create(bytes);
            //通过公钥生成钱包地址
            String address = Keys.getAddress(keyPair.getPublicKey());
            log.info("Mnemonic:" + mnemonics);
            log.info("Address:0x" + address);
            log.info("PrivateKey:0x" + keyPair.getPrivateKey().toString(16));
            log.info("PublicKey:" + keyPair.getPublicKey().toString(16));
        } catch (Exception e) {
            log.error("create address error: ", e);
        }
    }
    

    首先生成12个助记词,然后根据助记词生成公私钥和地址。

    获取账户的 ETH 余额
    /**
     * 获取指定地址ETH余额
     *
     * @param address 地址
     * @return
     */
    public void getEthBalance(String address) {
        if (!validateAddress(address)) {
            log.error("address is incorrect.");
            return;
        }
        try {
            EthGetBalance ethGetBalance = Web3jClient.instance().ethGetBalance(address, DefaultBlockParameterName.LATEST).send();
            if (ethGetBalance.hasError()) {
                log.error("【获取账户 {} ETH余额失败】", address, ethGetBalance.getError().getMessage());
                return;
            }
            String balance = Convert.fromWei(new BigDecimal(ethGetBalance.getBalance()), Convert.Unit.ETHER).toPlainString();
            log.info("balance = " + balance);
        } catch (Exception e) {
            log.error("【获取账户 {} ETH余额失败】", address, e);
        }
    }
    
    获取代币余额
    /**
     * 获取地址代币余额
     *
     * @param address         ETH地址
     * @param contractAddress 代币合约地址
     * @return
     */
    public void getTokenBalance(String address, String contractAddress) {
        if (!validateAddress(address) || !validateAddress(contractAddress)) {
            log.error("address is incorrect.");
            return;
        }
        try {
            Function balanceOf = new Function("balanceOf", Arrays.asList(new Address(address)), Arrays.asList(new TypeReference<Uint256>() {
            }));
            EthCall ethCall = Web3jClient.instance().ethCall(Transaction.createEthCallTransaction(address, contractAddress, FunctionEncoder.encode(balanceOf)), DefaultBlockParameterName.PENDING).send();
            if (ethCall.hasError()) {
                log.error("【获取账户 {}, 合约 {contractAddress} 余额失败】", address, contractAddress, ethCall.getError().getMessage());
                return;
            }
            String value = ethCall.getValue();
            String balance = Numeric.toBigInt(value).toString();
            int decimal = getTokenDecimal(contractAddress);
            log.info("balance = " + EthAmountFormat.format(balance, decimal));
        } catch (Exception e) {
            log.error("【获取账户 {}, 合约 {contractAddress} 余额失败】", address, contractAddress, e);
        }
    }
    
    获取代币的精度
    /**
     * 获取代币精度
     *
     * @param contractAddress 代币合约地址
     * @return
     */
    public int getTokenDecimal(String contractAddress) throws Exception {
        Function function = new Function("decimals", Arrays.asList(), Arrays.asList(new TypeReference<Uint8>() {
        }));
        EthCall ethCall = Web3jClient.instance().ethCall(Transaction.createEthCallTransaction("0x0000000000000000000000000000000000000000", contractAddress, FunctionEncoder.encode(function)), DefaultBlockParameterName.LATEST).send();
        if (ethCall.hasError()) {
            log.error("【获取合约 {} Token 精度失败】", contractAddress, ethCall.getError().getMessage());
            throw new Exception(ethCall.getError().getMessage());
        }
        List<Type> decode = FunctionReturnDecoder.decode(ethCall.getValue(), function.getOutputParameters());
        int decimals = Integer.parseInt(decode.get(0).getValue().toString());
        log.info("decimals = " + decimals);
        return decimals;
    }
    
    获取代币符号
    /**
     * 获取代币符号
     *
     * @param contractAddress 代币合约地址
     * @return
     */
    public String getTokenSymbol(String contractAddress) throws Exception {
        Function function = new Function("symbol", Arrays.asList(), Arrays.asList(new TypeReference<Utf8String>() {
        }));
        EthCall ethCall = Web3jClient.instance().ethCall(Transaction.createEthCallTransaction("0x0000000000000000000000000000000000000000", contractAddress, FunctionEncoder.encode(function)), DefaultBlockParameterName.LATEST).send();
        if (ethCall.hasError()) {
            throw new Exception(ethCall.getError().getMessage());
        }
        List<Type> decode = FunctionReturnDecoder.decode(ethCall.getValue(), function.getOutputParameters());
        return decode.get(0).getValue().toString();
    }
    
    获取代币名称
    /**
     * 获取代币名称
     *
     * @param contractAddress 代币合约地址
     * @return
     */
    public String getTokenName(String contractAddress) throws Exception {
        Function function = new Function("name", Arrays.asList(), Arrays.asList(new TypeReference<Utf8String>() {
        }));
        EthCall ethCall = Web3jClient.instance().ethCall(Transaction.createEthCallTransaction("0x0000000000000000000000000000000000000000", contractAddress, FunctionEncoder.encode(function)), DefaultBlockParameterName.LATEST).send();
        if (ethCall.hasError()) {
            throw new Exception(ethCall.getError().getMessage());
        }
        List<Type> decode = FunctionReturnDecoder.decode(ethCall.getValue(), function.getOutputParameters());
        return decode.get(0).getValue().toString();
    }
    
    获取指定区块高度的交易信息
    /**
     * 获取指定区块高度的交易信息
     *
     * @param height 区块高度
     * @return
     */
    public List<org.web3j.protocol.core.methods.response.Transaction> getTransactionByHeight(BigInteger height) throws Exception {
        List<org.web3j.protocol.core.methods.response.Transaction> transactions = new ArrayList<>();
        EthBlock ethBlock = Web3jClient.instance().ethGetBlockByNumber(DefaultBlockParameter.valueOf(height), false).send();
        if (ethBlock.hasError()) {
            throw new Exception(ethBlock.getError().getMessage());
        }
        EthBlock.Block block = ethBlock.getBlock();
        for (EthBlock.TransactionResult transactionResult : block.getTransactions()) {
            try {
                org.web3j.protocol.core.methods.response.Transaction transaction = getTransactionByTxId((String) transactionResult.get());
                transactions.add(transaction);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        log.info("【获取区块交易数据成功】 区块高度: {}, 区块哈希: {}", block.getNumber(), block.getHash());
        return transactions;
    }
    
    获取指定交易ID的交易信息
    /**
     * 获取指定交易ID的交易信息
     *
     * @param txId 交易hash
     * @return
     */
    public org.web3j.protocol.core.methods.response.Transaction getTransactionByTxId(String txId) throws IOException {
        return Web3jClient.instance().ethGetTransactionByHash(txId).send().getTransaction().orElse(null);
    }
    
    获取合约交易估算gas值
    /**
     * 获取合约交易估算gas值
     *
     * @param from        发送者
     * @param to          发送目标地址
     * @param coinAddress 代币地址
     * @param value       发送金额(单位:代币最小单位)
     * @return
     */
    public BigInteger getTransactionGasLimit(String from, String to, String coinAddress, BigInteger value) throws Exception {
        Function transfer = new Function("transfer", Arrays.asList(new org.web3j.abi.datatypes.Address(to), new org.web3j.abi.datatypes.generated.Uint256(value)), Collections.emptyList());
        String data = FunctionEncoder.encode(transfer);
        EthEstimateGas ethEstimateGas = Web3jClient.instance().ethEstimateGas(new Transaction(from, null, null, null, coinAddress, BigInteger.ZERO, data)).send();
        if (ethEstimateGas.hasError()) {
            throw new Exception(ethEstimateGas.getError().getMessage());
        }
        return ethEstimateGas.getAmountUsed();
    }
    
    广播交易
    /**
     * 广播交易
     *
     * @param signData 签名数据
     * @return
     */
    public String broadcastTransaction(String signData) throws Exception {
        EthSendTransaction transaction = Web3jClient.instance().ethSendRawTransaction(signData).send();
        if (transaction.hasError()) {
            throw new Exception(transaction.getError().getMessage());
        }
        String txId = transaction.getTransactionHash();
        log.info("【发送交易交易成功】txId: {}", txId);
        return txId;
    }
    
    验证交易是否被打包进区块
    /**
     * 验证交易是否被打包进区块
     *
     * @param txId 交易id
     * @return
     */
    public boolean validateTransaction(String txId) throws Exception {
        org.web3j.protocol.core.methods.response.Transaction transaction = getTransactionByTxId(txId);
        String blockHash = transaction.getBlockHash();
        if (StringUtils.isEmpty(blockHash) || Numeric.toBigInt(blockHash).compareTo(BigInteger.valueOf(0)) == 0) {
            return false;
        }
        EthGetTransactionReceipt receipt = Web3jClient.instance().ethGetTransactionReceipt(txId).send();
        if (receipt.hasError()) {
            throw new Exception(receipt.getError().getMessage());
        }
        TransactionReceipt transactionReceipt = receipt.getTransactionReceipt().get();
        if (Numeric.toBigInt(transactionReceipt.getStatus()).compareTo(BigInteger.valueOf(1)) == 0) {
            return true;
        }
        return false;
    }
    
    验证地址是否有效
    public boolean isValidAddress(String input) {
        if (String.isEmpty(input) || !input.startsWith("0x")) {
            return false;
        }
        String cleanInput = Numeric.cleanHexPrefix(input);
        try {
            Numeric.toBigIntNoPrefix(cleanInput);
        } catch (NumberFormatException e) {
            return false;
        }
        return cleanInput.length() == ADDRESS_LENGTH_IN_HEX;
    }
    
    通过离线签名发送ETH
    @Test
    void sendEthTransaction() throws Exception {
        String from = "0x2C104AB32BEA7eCff8f37987AB1930bdF9FDb0ac";
        String to = "0xe6a974a4c020ba29a9acb6c2290175a4d8846760";
        String privateKey = "your privateKey";
        BigDecimal amount = Convert.toWei("1", Convert.Unit.WEI);
        BigInteger nonce = Web3jClient.instance().ethGetTransactionCount(address, DefaultBlockParameterName.PENDING).send().getTransactionCount();
        BigInteger amountWei = Convert.toWei(amount, Convert.Unit.ETHER).toBigInteger();
        BigInteger gasPrice = Web3jClient.instance().ethGasPrice().send().getGasPrice();
        //BigInteger gasLimit = Web3jClient.instance().ethEstimateGas(new Transaction(from, null, null, null, to, amount.toBigInteger(), null)).send().getAmountUsed();
        BigInteger gasLimit = BigInteger.valueOf(21000L);
        RawTransaction rawTransaction = RawTransaction.createTransaction(nonce, gasPrice, gasLimit, to, amountWei, "");
        // 签名交易
        byte[] signMessage = TransactionEncoder.signMessage(rawTransaction, Credentials.create(privateKey));
        // 广播交易
        broadcastTransaction(Numeric.toHexString(signMessage));
    }
    

    nonce 的理解:

    ETHnonce 往往用来防止重复交易,nonce 是指某个节点上以同一个身份发起交易的交易号,这个交易号默认从0开始,每次成功发起一笔交易后+1。一般我们用得最多的地方的就是发送一笔交易一直没有被打包,大部分情况都是 gasPrice 设置过低,这时候我们通常再发送一笔 gasPrice 更高的相同交易用来覆盖前面那笔一直没有被打包的交易。

    通过离线签名发送Token
    @Test
    void sendTokenTransaction() throws Exception {
        String from = address;
        String to = "0xe6a974a4c020ba29a9acb6c2290175a4d8846760";
        String privateKey = "your privateKey";
        String contractAddress = "0x064E6aC4deE25a101d535FcD91b35b9FcbA6ff31";
        BigInteger amount = new BigDecimal("1").multiply(BigDecimal.valueOf(Math.pow(10, ethService.getTokenDecimal(contractAddress)))).toBigInteger();
        BigInteger gasPrice = Web3jClient.instance().ethGasPrice().send().getGasPrice();
        BigInteger gasLimit = ethService.getTransactionGasLimit(from, to, contractAddress, amount);
        BigInteger nonce = Web3jClient.instance().ethGetTransactionCount(from, DefaultBlockParameterName.PENDING).send().getTransactionCount();
        Function function = new Function("transfer", Arrays.asList(new Address(to), new Uint256(amount)), Collections.emptyList());
        String data = FunctionEncoder.encode(function);
        RawTransaction rawTransaction = RawTransaction.createTransaction(nonce, gasPrice, gasLimit, contractAddress, data);
        byte[] signMessage = TransactionEncoder.signMessage(rawTransaction, Credentials.create(privateKey));
        broadcastTransaction(Numeric.toHexString(signMessage));
    }
    

    contractAddress 是你要转出 Token 的合约地址,0x064E6aC4deE25a101d535FcD91b35b9FcbA6ff31 这个地址是我自己在 rinkeby 上部署的简单合约,用 BNB 合约来改的,具体的合约可以点这里查看

    详细的代码已经提交到了 github

    相关文章

      网友评论

          本文标题:ETH(以太坊) 离线交易相关知识的学习总结(JAVA)

          本文链接:https://www.haomeiwen.com/subject/qxtpzctx.html