在之前的文章中抚岗,講解了創(chuàng)建或杠、導(dǎo)出、導(dǎo)入錢包宣蔚。
【ETH錢包開發(fā)01】創(chuàng)建向抢、導(dǎo)出錢包
【ETH錢包開發(fā)02】導(dǎo)入錢包
本文主要講解以太坊轉(zhuǎn)賬相關(guān)的一些知識(shí)。交易分為ETH轉(zhuǎn)賬和ERC-20 Token轉(zhuǎn)賬胚委,本篇先講一下ETH轉(zhuǎn)賬挟鸠。
發(fā)起交易的2種方式
1、解鎖賬戶發(fā)起交易亩冬。錢包keyStore文件保存在geth節(jié)點(diǎn)上艘希,用戶發(fā)起交易需要解鎖賬戶,適用于中心化的交易所硅急。
2覆享、錢包文件離線簽名發(fā)起交易。錢包keyStore文件保存在本地营袜,用戶使用密碼+keystore的方式做離線交易簽名來發(fā)起交易撒顿,適用于dapp,比如錢包荚板。
本文主要講一下第二種方式凤壁,也就是錢包離線簽名轉(zhuǎn)賬的方式。
錢包文件簽名的方式發(fā)起交易
交易流程
1跪另、通過keystore加載轉(zhuǎn)賬所需的憑證Credentials
2拧抖、創(chuàng)建一筆交易R(shí)awTransaction
3、使用Credentials對象對交易簽名
4免绿、發(fā)起交易
/**
* 發(fā)起一筆交易(自定義參數(shù))
*
* @param from 發(fā)起人錢包地址
* @param to 轉(zhuǎn)入的錢包地址
* @param value 轉(zhuǎn)賬金額唧席,單位是wei
* @param privateKey 錢包私鑰
* @param gasPrice 轉(zhuǎn)賬費(fèi)用
* @param gasLimit
* @param data 備注的信息
* @throws IOException
* @throws CipherException
* @throws ExecutionException
* @throws InterruptedException
*/
public EthSendTransaction transfer(String from,
String to,
BigInteger value,
String privateKey,
BigInteger gasPrice,
BigInteger gasLimit,
String data) throws IOException, CipherException, ExecutionException, InterruptedException {
//加載轉(zhuǎn)賬所需的憑證,用私鑰
Credentials credentials = Credentials.create(privateKey);
//獲取nonce,交易筆數(shù)
BigInteger nonce = getNonce(from);
//創(chuàng)建RawTransaction交易對象
RawTransaction rawTransaction = RawTransaction.createEtherTransaction(nonce, gasPrice, gasLimit, to, value);
//簽名Transaction袱吆,這里要對交易做簽名
byte[] signMessage = TransactionEncoder.signMessage(rawTransaction, credentials);
String hexValue = Numeric.toHexString(signMessage);
//發(fā)送交易
EthSendTransaction ethSendTransaction = web3j.ethSendRawTransaction(hexValue).sendAsync().get();
return ethSendTransaction;
}
/**
* 獲取nonce,交易筆數(shù)
*
* @param from
* @return
* @throws ExecutionException
* @throws InterruptedException
*/
private BigInteger getNonce(String from) throws ExecutionException, InterruptedException {
EthGetTransactionCount transactionCount = web3j.ethGetTransactionCount(from, DefaultBlockParameterName.LATEST).sendAsync().get();
BigInteger nonce = transactionCount.getTransactionCount();
Log.i(TAG, "transfer nonce : " + nonce);
return nonce;
}
注意以下幾點(diǎn):
1距淫、Credentials
這里绞绒,我是通過獲取私鑰的方式來加載Credentials
//加載轉(zhuǎn)賬所需的憑證,用私鑰
Credentials credentials = Credentials.create(privateKey);
還有另外一種方式榕暇,通過密碼+錢包文件keystore方式來加載Credentials
ECKeyPair ecKeyPair = LWallet.decrypt(password, walletFile);
Credentials credentials = Credentials.create(ecKeyPair);
2蓬衡、nonce
nonce是指發(fā)起交易的賬戶下的交易筆數(shù),每一個(gè)賬戶nonce都是從0開始彤枢,當(dāng)nonce為0的交易處理完之后狰晚,才會(huì)處理nonce為1的交易,并依次加1的交易才會(huì)被處理缴啡。
可以通過eth_gettransactioncount 獲取nonce
3壁晒、gasPrice和gasLimit
交易手續(xù)費(fèi)由gasPrice 和gasLimit來決定,實(shí)際花費(fèi)的交易手續(xù)費(fèi)是gasUsed * gasPrice
业栅。所有這兩個(gè)值你可以自定義秒咐,也可以使用系統(tǒng)參數(shù)獲取當(dāng)前兩個(gè)值
關(guān)于gas
,你可以參考我之前的一篇文章碘裕。
以太坊(ETH)GAS詳解
gasPrice和gasLimit影響的是轉(zhuǎn)賬的速度携取,如果gas過低,礦工會(huì)最后才打包你的交易帮孔。在app中雷滋,通常給定一個(gè)默認(rèn)值,并且允許用戶自己選擇手續(xù)費(fèi)文兢。
如果不需要自定義的話晤斩,還有一種方式來獲取。獲取以太坊網(wǎng)絡(luò)最新一筆交易的gasPrice
,轉(zhuǎn)賬的話姆坚,gasLimit
一般設(shè)置為21000就可以了尸昧。
/**
* 獲取當(dāng)前以太坊網(wǎng)絡(luò)中最近一筆交易的gasPrice
*/
public BigInteger requestCurrentGasPrice() throws Exception {
EthGasPrice ethGasPrice = web3j.ethGasPrice().sendAsync().get();
return ethGasPrice.getGasPrice();
}
Web3j還提供另外一種簡單的方式來轉(zhuǎn)賬以太幣,這種方式的好處是不需要管理nonce,不需要設(shè)置gasPrice和gasLimit旷偿,會(huì)自動(dòng)獲取最新一筆交易的gasPrice烹俗,gasLimit 為21000(轉(zhuǎn)賬一般設(shè)置成這個(gè)值就夠用了)。
/**
* 發(fā)起一筆交易
* 使用以太錢包文件發(fā)送以太幣給其他人萍程,不能設(shè)置nonce:(推薦)
*
* @param privateKey
*/
public String transfer(String toAddress, BigDecimal value, String privateKey) throws Exception {
//轉(zhuǎn)賬者私鑰
Credentials credentials = Credentials.create(privateKey);
TransactionReceipt transactionReceipt = Transfer.sendFunds(
web3j, credentials, toAddress,
value, Convert.Unit.ETHER).sendAsync().get();
String transactionHash = transactionReceipt.getTransactionHash();
Log.i(TAG, "transfer: " + transactionHash);
return transactionHash;
}
如何驗(yàn)證交易是否成功幢妄?
這個(gè)問題,我想是很多朋友所關(guān)心的吧茫负。但是到目前為止蕉鸳,我還沒有看到有講解這方面的博客。
之前問過一些朋友,他們說可以通過區(qū)塊號潮尝、區(qū)塊哈希來判斷榕吼,也可以通過Receipt日志來判斷。但是經(jīng)過我的一番嘗試勉失,只有BlockHash
是可行的羹蚣,在web3j中根據(jù)blocknumber
和transactionReceipt
都會(huì)報(bào)空指針異常。
原因大致是這樣的:在發(fā)起一筆交易之后乱凿,會(huì)返回txHash
顽素,然后我們可以根據(jù)這個(gè)txHash
去查詢這筆交易相關(guān)的信息。但是剛發(fā)起交易的時(shí)候徒蟆,由于手續(xù)費(fèi)問題或者以太網(wǎng)絡(luò)擁堵問題胁出,會(huì)導(dǎo)致你的這筆交易還沒有被礦工打包進(jìn)區(qū)塊,因此一開始是查不到的段审,通常需要幾十秒甚至更長的時(shí)間才能獲取到結(jié)果全蝶。我目前的解決方案是輪詢的去刷BlockHash
,一開始的時(shí)候BlockHash
的值為0x00000000000寺枉,等到打包成功的時(shí)候就不再是0了裸诽。
這里我使用的是rxjava的方式去輪詢刷的,5s刷新一次型凳。
/**
* 開啟輪詢
* 根據(jù)txhash查詢交易是否被打包進(jìn)區(qū)塊
*
* @param txHash
*/
public static void startPolling(String txHash) {
//5s刷新一次
disposable = Flowable.interval(0, 5, TimeUnit.SECONDS)
.compose(ScheduleCompat.apply())
// .takeUntil(Flowable.timer(120, TimeUnit.SECONDS))
.subscribe(new Consumer<Long>() {
@Override
public void accept(Long num) throws Exception {
Log.i(TAG, "第 : " + num + " 次輪詢");
//根據(jù)blockHash來判斷交易是否被打包
String blockHash = getBlockHash(txHash);
boolean isSuccess = Numeric.toBigInt(blockHash).compareTo(BigInteger.valueOf(0)) != 0;
if (isSuccess) {
getTransactionReceipt(txHash);
}
}
});
}
/**
* 停止輪詢
*
* @param disposable
*/
public static void stopPolling(Disposable disposable) {
if (!disposable.isDisposed()) {
disposable.dispose();
}
}
/**
* 獲取blockhash
* @param txHash
* @return
*/
public static String getBlockHash(String txHash) {
Web3j web3j = Web3jFactory.build(new HttpService("https://rinkeby.infura.io/v3/xxxx"));
try {
EthTransaction transaction = web3j.ethGetTransactionByHash(txHash).sendAsync().get();
Transaction result = transaction.getResult();
String blockHash = result.getBlockHash();
Log.i(TAG, "getTransactionResult blockHash : " + blockHash);
boolean isSuccess = Numeric.toBigInt(blockHash).compareTo(BigInteger.valueOf(0)) != 0;
if (isSuccess) {
getTransactionReceipt(txHash);
stopPolling(disposable);
}
return blockHash;
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
public static void getTransactionReceipt(String transactionHash) {
Web3j web3j = Web3jFactory.build(new HttpService("https://rinkeby.infura.io/v3/xxxx"));
try {
EthGetTransactionReceipt transactionReceipt = web3j.ethGetTransactionReceipt(transactionHash).sendAsync().get();
TransactionReceipt receipt = transactionReceipt.getTransactionReceipt();
String status = receipt.getStatus();
BigInteger gasUsed = receipt.getGasUsed();
BigInteger blockNumber = receipt.getBlockNumber();
String blockHash = receipt.getBlockHash();
Log.i(TAG, "getTransactionReceipt status : " + status);
Log.i(TAG, "getTransactionReceipt gasUsed : " + gasUsed);
Log.i(TAG, "getTransactionReceipt blockNumber : " + blockNumber);
Log.i(TAG, "getTransactionReceipt blockHash : " + blockHash);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
正常情況下丈冬,幾十秒內(nèi)就可以獲取到區(qū)塊信息了。
區(qū)塊確認(rèn)數(shù)
區(qū)塊確認(rèn)數(shù)=當(dāng)前區(qū)塊高度-交易被打包時(shí)的區(qū)塊高度甘畅。