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<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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,591评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,448评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,823评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,204评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,228评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,190评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,078评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,923评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,334评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,550评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,727评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,428评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,022评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,672评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,826评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,734评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,619评论 2 354

推荐阅读更多精彩内容