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

最近项目上需要做一个类似钱包相关的东西,需要的一些知识和以前不同,以前操作的一直是内部的全节点,私钥啥的全部存储在自己的节点上,节点相当于一个钱包,直接操作节点上的钱包就好。现在需要在移动端或者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 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() {
        }));
        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() {
    }));
    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 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() {
    }));
    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 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() {
    }));
    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 decode = FunctionReturnDecoder.decode(ethCall.getValue(), function.getOutputParameters());
    return decode.get(0).getValue().toString();
}
获取指定区块高度的交易信息
/**
 * 获取指定区块高度的交易信息
 *
 * @param height 区块高度
 * @return
 */
public List getTransactionByHeight(BigInteger height) throws Exception {
    List 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))