最近项目上需要做一个类似钱包相关的东西,需要的一些知识和以前不同,以前操作的一直是内部的全节点,私钥啥的全部存储在自己的节点上,节点相当于一个钱包,直接操作节点上的钱包就好。现在需要在移动端或者web端签名交易之后,然后再用节点进行广播,接触到了一些新知识,记录下来吧。
准备公共节点
以太坊比其它一些公链好的地方是尽然提供了免费的公共节点(有 mainnet
、ropsten
、kovan
、rinkeby
供选择) ,这个节点只要注册登录之后创建项目选择自己需要的网络类型就好,这里为了方便测试我选择的 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
的理解:在
ETH
上nonce
往往用来防止重复交易,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
网友评论