钱包开发经验分享:ETH篇
# 钱包开发经验分享:ETH篇
[TOC]
## 开发前的准备
> 工欲善其事,必先利其器
一路开发过来,积累了一些钱包的开发利器和网站,与大家分享一下。这些东西在这行开发过的人都知道,只是给行外打算入行的人做个参考。
- 最好用的ETH钱包--MetaMask
下载:[MetaMask(谷歌插件)](https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn?hl=zh-CN)
简介:这是一款以太坊浏览器插件,他可以很方便的查看或操作以太坊、erc20代币余额,也方便配合remix之类的合约IDE来部署合约,支持自定义代币,支持多种测试网络和正式网络以及自定义网络节点。总而言之,这是一款十分便利好用的钱包。
- 最官方的区块链浏览器--etherscan
网址:[以太坊官方区块链浏览器](https://etherscan.com/)
简介:这是以太坊最最最官方的区块链浏览器了,对于开发者而言,它不仅仅只是查询区块交易那么简单,他还有更多有利于程序员开发的功能。它提供了众多api和小工具,它是所有测试网络的父域名,可以轻松地切换查看到所有测试网络的区块和交易,在部署合约时,它又协助你发布合约,因此对于开发者而言,这是一个不可缺少的网站。
- 获取测试币的网站--rinkeby、ropsten
网址:[rinkeby](https://faucet.rinkeby.io/)、[ropsten](https://faucet.metamask.io/)
简介:以太坊有很多共享的测试网络,[这篇博文](https://zhuanlan.zhihu.com/p/29010231)介绍了各个网络的区别和其区块链浏览器,其中开发者主要使用的区块链浏览器不外乎rinkeby和ropsten,上述两个网址则是这两种测试币的水龙头网站,获取测试币的教程如下:[获取rinkeby测试币](http://zhaozhiming.github.io/blog/2018/04/18/how-to-earn-eth-and-token-in-rinkeby/)、[获取ropsten测试币](https://blog.csdn.net/kevin198528/article/details/80575713)。
- 免费的第三方节点接入--王站
网址:[infura](https://infura.io/)
简介:对于ETH钱包开发而言,这是个不可或缺的网站,当然,可能也有其他第三方节点免费对用户开放,不过我一直用的是这个网站。这个网站的作用是,我们不用搭建ETH节点也可以正常地进行ETH的开发,我们只需要动动手指注册一个账户,创建我们的项目,就能拿到一个免费接入的ETH节点,而且他还包括了所有流行的测试网络。而我之所以称之为王站,是因为它的网站图标类似一个王字。
- 最便捷的以太坊IDE--remix
网址:[remix](http://remix.ethereum.org/)
简介:对于ETH钱包开发而言,合约开发和部署或许是必不可少的一部分,为什么我会这样说?那是因为在钱包开发中,总会需要对接各种erc20的代币,而我们虽然能够在获得ETH的测试币,但是其他的代币的测试币我们是很难获得的(或者说根本无法获得),而基于erc20协议的代币代码是通用的,所以接入代币钱包的时候,我们往往是考虑自己在测试网络部署一份erc20协议的合约,并自己铸币,以方便进行后续的开发,而结合remix和MetaMask来部署合约,那就是几个步骤的事情。部署合约的流程可以参考[这篇教程](https://blog.csdn.net/hopeztm/article/details/81515816)。
## ETH钱包代码参考
> 真正的知识就在经验中
### 生成钱包地址、公私钥和助记词/通过助记词恢复钱包地址、公私钥
- 导入依赖
```xml
org.bitcoinj
bitcoinj-core
0.14.7
org.web3j
core
4.5.5
```
- 初始化web3j
```java
private final static Web3j web3j = Web3j.build(new HttpService("https://mainnet.infura.io/v3/你自己从infura申请的id"));
```
- 参考代码
```java
public static Map ethWalletGenerate(String mnemonic, String mnemonicPath, String passWord) {
try {
DeterministicSeed deterministicSeed = null;
List mnemonicArray = null;
if (null == mnemonic || 0 == mnemonic.length()) {
deterministicSeed = new DeterministicSeed(new SecureRandom(), 128, "", System.currentTimeMillis() / 1000);
mnemonicArray = deterministicSeed.getMnemonicCode();// 助记词
} else {
deterministicSeed = new DeterministicSeed(mnemonic, null, "", System.currentTimeMillis() / 1000);
}
byte[] seedBytes = deterministicSeed.getSeedBytes();// 种子
if (null == seedBytes) {
logger.error("生成钱包失败");
return null;
}
//种子对象
DeterministicKey deterministicKey = HDKeyDerivation.createMasterPrivateKey(seedBytes);
String[] pathArray = mnemonicPath.split("/");// 助记词路径
for (int i = 1; i () {
private static final long serialVersionUID = -4960785990664709623L;
{
put("walletFile", walletFile);
put("eCKeyPair", eCKeyPair);
put("mnemonic", mnemonicCode.substring(0, mnemonicCode.length() - 1));
}
};
} else {
return new HashMap() {
private static final long serialVersionUID = -947886783923530545L;
{
put("walletFile", walletFile);
put("eCKeyPair", eCKeyPair);
}
};
}
} catch (CipherException e) {
return null;
} catch (UnreadableWalletException e) {
return null;
}
}
```
其中关于助记词路径(mnemonicPath)的解释请参考这篇文章:[关于钱包助记词](https://www.jianshu.com/p/1ed6c0daa85e)。erc20代币的钱包地址和ETH的钱包地址是通用的,所以这套代码可以用于生成ETH钱包地址,也可以用于生成erc20钱包地址。
- 测试代码
```java
/**
* 生成钱包地址、公私钥、助记词
*/
@Test
public void testGenerateEthWallet(){
Map wallet = AddrUtil.ethWalletGenerate(null, ETH_MNEMONI_PATH, "123456");
WalletFile walletFile = (WalletFile) wallet.get("walletFile");
String address = walletFile.getAddress();
ECKeyPair eCKeyPair = (ECKeyPair) wallet.get("eCKeyPair");
String privateKey = eCKeyPair.getPrivateKey().toString(16);
String publicKey = eCKeyPair.getPublicKey().toString(16);
String mnemonic = (String) wallet.get("mnemonic");
logger.warn("address: {}, privateKey: {}, publicKey: {}, mnemonic: {}", address, privateKey, publicKey, mnemonic);
}
/**
* 通过助记词恢复钱包地址、公私钥
*/
@Test
public void testGenerateEthWalletByMnemonic(){
Map wallet = AddrUtil.ethWalletGenerate("clown cat senior keep problem engine degree modify ritual machine syrup company", ETH_MNEMONI_PATH, "123456");
WalletFile walletFile = (WalletFile) wallet.get("walletFile");
String address = walletFile.getAddress();
ECKeyPair eCKeyPair = (ECKeyPair) wallet.get("eCKeyPair");
String privateKey = eCKeyPair.getPrivateKey().toString(16);
String publicKey = eCKeyPair.getPublicKey().toString(16);
String mnemonic = (String) wallet.get("mnemonic");
logger.warn("address: {}, privateKey: {}, publicKey: {}, mnemonic: {}", address, privateKey, publicKey, mnemonic);
}
```
进一步,我们或许希望能够从一个唯一的密钥或者助记词去推导出交易所所有的钱包地址和密钥,可以参考这面这套代码:
- 参考代码
```java
/**
* 通过助记词和id生成对应的子账户
*
* @param mnemonic 助记词
* @param id 派生子id
* @return 子账户key
*/
private static DeterministicKey generateKeyFromMnemonicAndUid(String mnemonic, int id) {
byte[] seed = MnemonicUtils.generateSeed(mnemonic, "");
DeterministicKey rootKey = HDKeyDerivation.createMasterPrivateKey(seed);
DeterministicHierarchy hierarchy = new DeterministicHierarchy(rootKey);
return hierarchy.deriveChild(BIP44_ETH_ACCOUNT_ZERO_PATH, false, true, new ChildNumber(id, false));
}
/**
* 生成地址
*
* @param id 用户id
* @return 地址
*/
public static String getEthAddress(String mnemonic, int id) {
DeterministicKey deterministicKey = generateKeyFromMnemonicAndUid(mnemonic, id);
ECKeyPair ecKeyPair = ECKeyPair.create(deterministicKey.getPrivKey());
return Keys.getAddress(ecKeyPair);
}
/**
* 生成私钥
*
* @param id 用户id
* @return 私钥
*/
public static BigInteger getPrivateKey(String mnemonic, int id) {
return generateKeyFromMnemonicAndUid(mnemonic, id).getPrivKey();
}
```
- 测试代码
```java
/**
* 通过助记词和用户id生成钱包地址和私钥
*/
@Test
public void testGenerateEthChildWallet(){
String ethAddress = EthUtil.getEthAddress("clown cat senior keep problem engine degree modify ritual machine syrup company", 1);
BigInteger privateKey = EthUtil.getPrivateKey("clown cat senior keep problem engine degree modify ritual machine syrup company", 1);
logger.warn("address: {}, privateKey: {}", ethAddress, privateKey);
}
```
### 获取余额/获取代币余额
- 参考代码
```java
/**
* 获取eth余额
*
* @param address 传入查询的地址
* @return String 余额
* @throws IOException
*/
public static String getEthBalance(String address) {
EthGetBalance ethGetBlance = null;
try {
ethGetBlance = web3j.ethGetBalance(address, DefaultBlockParameterName.LATEST).send();
} catch (IOException e) {
logger.error("【获取ETH余额失败】 错误信息: {}", e.getMessage());
}
// 格式转换 WEI(币种单位) --> ETHER
String balance = Convert.fromWei(new BigDecimal(ethGetBlance.getBalance()), Convert.Unit.ETHER).toPlainString();
return balance;
}
```
- 测试代码
```java
/**
* 获取ETH余额
*/
@Test
public void testGetETHBalance(){
String balance = EthUtil.getEthBalance("0x09f20ff67db2c5fabeb9a2c8dd5f6b4afab7887b");
logger.warn("balance: {}", balance);
}
```
- 参考代码
```java
/**
* 获取账户代币余额
*
* @param account 账户地址
* @param coinAddress 合约地址
* @return 代币余额 (单位:代币最小单位)
* @throws IOException
*/
public static String getTokenBalance(String account, String coinAddress) {
Function balanceOf = new Function("balanceOf",
Arrays.asList(new org.web3j.abi.datatypes.Address(account)),
Arrays.>asList(new TypeReference() {
}));
if (coinAddress == null) {
return null;
}
String value = null;
try {
value = web3j.ethCall(Transaction.createEthCallTransaction(account, coinAddress, FunctionEncoder.encode(balanceOf)), DefaultBlockParameterName.PENDING).send().getValue();
} catch (IOException e) {
logger.error("【获取合约代币余额失败】 错误信息: {}", e.getMessage());
return null;
}
int decimal = getTokenDecimal(coinAddress);
BigDecimal balance = new BigDecimal(new BigInteger(value.substring(2), 16).toString(10)).divide(BigDecimal.valueOf(Math.pow(10, decimal)));
return balance.toPlainString();
}
```
- 测试代码
```java
/**
* 获取代币余额
*/
@Test
public void testGetTokenBalance(){
String usdtBalance = EthUtil.getTokenBalance("0x09f20ff67db2c5fabeb9a2c8dd5f6b4afab7887b", "0xdac17f958d2ee523a2206206994597c13d831ec7");
logger.warn("usdtBalance: {}", usdtBalance);
}
```
ETH的地址分为两种,一种为普通的用户地址,另一种则是合约地址,所有代币类型的转账都是向合约地址发起转账,在输入中输入实际入账的信息(地址和数量),各种代币的合约地址可以查阅[以太坊最最最官方的区块浏览器](https://cn.etherscan.com/tokens)。上面参考代码中获取代币精度的代码可以继续参考下面的代码。
### 获取代币名称、精度和符号
- 参考代码
```java
/**
* 查询代币符号
*/
public static String getTokenSymbol(String contractAddress) {
String methodName = "symbol";
List inputParameters = new ArrayList();
List> outputParameters = new ArrayList();
TypeReference typeReference = new TypeReference() {
};
outputParameters.add(typeReference);
Function function = new Function(methodName, inputParameters, outputParameters);
String data = FunctionEncoder.encode(function);
Transaction transaction = Transaction.createEthCallTransaction("0x0000000000000000000000000000000000000000", contractAddress, data);
EthCall ethCall = null;
try {
ethCall = web3j.ethCall(transaction, DefaultBlockParameterName.LATEST).sendAsync().get();
} catch (InterruptedException e) {
logger.error("获取代币符号失败");
e.printStackTrace();
} catch (ExecutionException e) {
logger.error("获取代币符号失败");
e.printStackTrace();
}
List results = FunctionReturnDecoder.decode(ethCall.getValue(), function.getOutputParameters());
if (null == results || 0 == results.size()) {
return "";
}
return results.get(0).getValue().toString();
}
/**
* 查询代币名称
*/
public static String getTokenName(String contractAddr) {
String methodName = "name";
List inputParameters = new ArrayList();
List> outputParameters = new ArrayList();
TypeReference typeReference = new TypeReference() {
};
outputParameters.add(typeReference);
Function function = new Function(methodName, inputParameters, outputParameters);
String data = FunctionEncoder.encode(function);
Transaction transaction = Transaction.createEthCallTransaction("0x0000000000000000000000000000000000000000", contractAddr, data);
EthCall ethCall = null;
try {
ethCall = web3j.ethCall(transaction, DefaultBlockParameterName.LATEST).sendAsync().get();
} catch (InterruptedException e) {
logger.error("获取代币名称失败");
e.printStackTrace();
} catch (ExecutionException e) {
logger.error("获取代币名称失败");
e.printStackTrace();
}
List results = FunctionReturnDecoder.decode(ethCall.getValue(), function.getOutputParameters());
if (null == results || results.size() inputParameters = new ArrayList();
List> outputParameters = new ArrayList();
TypeReference typeReference = new TypeReference() {
};
outputParameters.add(typeReference);
Function function = new Function(methodName, inputParameters, outputParameters);
String data = FunctionEncoder.encode(function);
Transaction transaction = Transaction.createEthCallTransaction("0x0000000000000000000000000000000000000000", contractAddr, data);
EthCall ethCall = null;
try {
ethCall = web3j.ethCall(transaction, DefaultBlockParameterName.LATEST).sendAsync().get();
} catch (InterruptedException e) {
logger.error("获取代币精度失败");
e.printStackTrace();
} catch (ExecutionException e) {
logger.error("获取代币精度失败");
e.printStackTrace();
}
List results = FunctionReturnDecoder.decode(ethCall.getValue(), function.getOutputParameters());
if (null == results || 0 == results.size()) {
return 0;
}
return Integer.parseInt(results.get(0).getValue().toString());
}
```
- 测试代码
```java
/**
* 获取代币名称、符号和精度
*/
@Test
public void testGetTokenInfo(){
String usdtContractAddress = "0xdac17f958d2ee523a2206206994597c13d831ec7";
String tokenName = EthUtil.getTokenName(usdtContractAddress);
String tokenSymbol = EthUtil.getTokenSymbol(usdtContractAddress);
int tokenDecimal = EthUtil.getTokenDecimal(usdtContractAddress);
logger.warn("name: {}, symbol: {}, decimal: {}", tokenName, tokenSymbol, tokenDecimal);
}
```
### 获取交易
- 参考代码
```java
/**
* 根据区块高度获取区块交易
* @param height 区块高度
* @return
*/
public static List getTxByHeight(BigInteger height) {
List transactions = new ArrayList();
try {
EthBlock.Block block = web3j.ethGetBlockByNumber(DefaultBlockParameter.valueOf(height), false).send().getBlock();
for (EthBlock.TransactionResult transactionResult : block.getTransactions()) {
Transaction transaction = web3j.ethGetTransactionByHash((String) transactionResult.get()).send().getTransaction().get();
transactions.add(transaction);
}
logger.info("【获取交易数据成功】 区块哈希: {}, 区块高度: {}", block.getHash(), block.getNumber());
} catch (IOException e) {
logger.error("【获取交易数据失败】 错误信息: {}", e.getMessage());
return null;
}
return transactions;
}
/**
* 根据txid获取交易信息
* @param txid 交易哈希
* @return
*/
public static Transaction getTxByTxid(String txid) {
Transaction transaction = null;
try {
transaction = web3j.ethGetTransactionByHash(txid).send().getTransaction().orElse(null);
logger.info("【获取交易信息成功】 {} : {}", txid, new Gson().toJson(transaction));
} catch (IOException e) {
logger.info("【获取交易信息失败】 交易哈希: {}, 错误信息: {}", txid, e.getMessage());
return null;
}
return transaction;
}
/**
* 解析代币交易
* @param transaction 交易对象
* @return
*/
public static Map getTokenTxInfo(Transaction transaction){
Map result = new HashMap();
String input = transaction.getInput();
if(!Erc20Util.isTransferFunc(input)) {
return null;
}
result.put("to", Erc20Util.getToAddress(input));
result.put("amount", Erc20Util.getTransferValue(input).divide(BigDecimal.valueOf(Math.pow(10, getTokenDecimal(transaction.getTo())))));
result.put("txid", transaction.getHash());
result.put("from", transaction.getFrom());
result.put("height", transaction.getBlockNumber());
result.put("txFee", Convert.fromWei(transaction.getGasPrice().multiply(transaction.getGas()).toString(10), Convert.Unit.ETHER));
result.put("gas", transaction.getGas());
result.put("gasPrice", transaction.getGasPrice());
return result;
}
/**
* 解析ETH交易
* @param transaction 交易对象
* @return
*/
public static Map getEthTxInfo(Transaction transaction){
Map result = new HashMap();
result.put("to", transaction.getTo());
result.put("amount", Convert.fromWei(transaction.getValue().toString(10), Convert.Unit.ETHER));
result.put("txid", transaction.getHash());
result.put("from", transaction.getFrom());
result.put("height", transaction.getBlockNumber());
result.put("txFee", Convert.fromWei(transaction.getGasPrice().multiply(transaction.getGas()).toString(10), Convert.Unit.ETHER));
result.put("gas", transaction.getGas());
result.put("gasPrice", transaction.getGasPrice());
return result;
}
```
- 测试代码
```java
/**
* 根据txid获取ETH/代币交易信息
*/
@Test
public void testGetTransactionByTxid(){
Transaction ethTx = EthUtil.getTxByTxid("0xd05798408be19ec0adc5e0a7397b4e9d294b8e136eacc1eb606be45533eb97f1");
Map ethTxInfo = EthUtil.getEthTxInfo(ethTx);
Transaction usdtTx = EthUtil.getTxByTxid("0xd5443fad2feafd309f28d86d39af2e3f112b1ca1b8cdce8a2b6b9cdcdef5ad59");
Map usdtTxInfo = EthUtil.getTokenTxInfo(usdtTx);
logger.warn("txInfo: {}, usdtTxInfo: {}", new Gson().toJson(ethTxInfo), new Gson().toJson(usdtTxInfo));
}
/**
* 根据区块高度获取交易
*/
@Test
public void testGetTransactionByBlockHeight(){
List transactions = EthUtil.getTxByHeight(new BigInteger("9159698"));
logger.warn("txCount: {}", transactions.size());
}
```
### ETH/代币离线签名转账
- 参考代码
```java
/**
* 发送eth离线交易
*
* @param from eth持有地址
* @param to 发送目标地址
* @param amount 金额(单位:eth)
* @param credentials 秘钥对象
* @return 交易hash
*/
public static String sendEthTx(String from, String to, BigInteger gasLimit, BigInteger gasPrice, BigDecimal amount, Credentials credentials) {
try {
BigInteger nonce = web3j.ethGetTransactionCount(from, DefaultBlockParameterName.PENDING).send().getTransactionCount();
BigInteger amountWei = Convert.toWei(amount, Convert.Unit.ETHER).toBigInteger();
RawTransaction rawTransaction = RawTransaction.createTransaction(nonce, gasPrice, gasLimit, to, amountWei, "");
byte[] signMessage = TransactionEncoder.signMessage(rawTransaction, credentials);
return web3j.ethSendRawTransaction(Numeric.toHexString(signMessage)).sendAsync().get().getTransactionHash();
}catch (Exception e) {
logger.error("【ETH离线转账失败】 错误信息: {}", e.getMessage());
return null;
}
}
/**
* 发送代币离线交易
*
* @param from 代币持有地址
* @param to 代币目标地址
* @param value 金额(单位:代币最小单位)
* @param coinAddress 代币合约地址
* @param credentials 秘钥对象
* @return 交易hash
*/
public static String sendTokenTx(String from, String to, BigInteger gasLimit, BigInteger gasPrice, BigInteger value, String coinAddress, Credentials credentials) {
try {
BigInteger nonce = web3j.ethGetTransactionCount(from, DefaultBlockParameterName.PENDING).send().getTransactionCount();
Function function = 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(function);
RawTransaction rawTransaction = RawTransaction.createTransaction(nonce, gasPrice, gasLimit, coinAddress, data);
byte[] signMessage = TransactionEncoder.signMessage(rawTransaction, credentials);
return web3j.ethSendRawTransaction(Numeric.toHexString(signMessage)).sendAsync().get().getTransactionHash();
}catch (Exception e) {
logger.error("【代币离线转账失败】 错误信息: {}", e.getMessage());
return null;
}
}
```
- 测试代码
```java
/**
* 测试ETH转账
*/
@Test
public void testETHTransfer() throws Exception{
String from = "0xB7Cd09d73a1719b90469Edf7Aa1942d8f89Ba21f";
String to = "0xF0B8412C211261B68bc797f31F642Aa14fbDC007";
String privateKey = "密钥不可见";
BigDecimal value = Convert.toWei("1", Convert.Unit.WEI);
BigInteger gasPrice = EthUtil.web3j.ethGasPrice().send().getGasPrice();
BigInteger gasLimit = EthUtil.web3j.ethEstimateGas(new Transaction(from, null, null, null, to, value.toBigInteger(), null)).send().getAmountUsed();
String txid = EthUtil.sendEthTx(from, to, gasLimit, gasPrice, value, Credentials.create(privateKey));
logger.warn("txid: {}", txid);
}
/**
* 测试代币转账
*/
@Test
public void testTokenTransfer() throws Exception{
String from = "0xB7Cd09d73a1719b90469Edf7Aa1942d8f89Ba21f";
String to = "0xF0B8412C211261B68bc797f31F642Aa14fbDC007";
String contractAddress = "0x6a26797a73f558a09a47d2dd56fbe03227a31dbb";
String privateKey = "密钥不可见";
BigInteger value = BigDecimal.valueOf(Math.pow(10, EthUtil.getTokenDecimal(contractAddress))).toBigInteger();
BigInteger gasPrice = EthUtil.web3j.ethGasPrice().send().getGasPrice();
BigInteger gasLimit = EthUtil.getTransactionGasLimit(from, to, contractAddress, value);
String txid = EthUtil.sendTokenTx(from, to, gasLimit, gasPrice, gasLimit, contractAddress, Credentials.create(privateKey));
logger.warn("txid: {}", txid);
}
```
代码里的合约地址是我在[rinkeby测试网络](https://rinkeby.etherscan.io/)发布的一个测试币:[TestCoin Token(TCT)](https://rinkeby.etherscan.io/address/0x6a26797a73f558a09a47d2dd56fbe03227a31dbb),不想自己部署合约的同学可以关注我的公众号,发钱包地址给我,我会发一些测试币到你的钱包地址。在上面的代码里,比较玩味的是关于nonce值的管理,关于nonce值的解释可以参考[这篇文章](https://blog.csdn.net/wo541075754/article/details/78081478)。在上面的代码里,nonce值我们是通过RPC接口直接获取,这样的操作是相对简单但是却耗时最长,因为调用RPC存在网络上的开销。比较成熟的处理方式是在交易信息表中维护一个nonce字段,这样做一方面是发起一笔新的交易的时候可以更快的获取nonce值,另一方面,当交易发生错误(发起了一笔金额错误的交易)的时候,可以及时进行修改,因为以太坊的设计是:你可以发起一笔与之前nonce值一样的交易,去覆盖处于pending状态的交易。
另外,关于矿工费的计算,正常的以太坊交易是**矿工费=gasPrice\*gasLimit**,gasPrice是每一步计算消耗的费用,gasLimit是允许接受的最大计算次数,它们的单位是WEI,关于以太坊的单位,请参考[这篇文章](https://blog.csdn.net/wypeng2010/article/details/81115257)。而当你使用这个计算公式来计算代币的手续费时就不怎么精确了,因为代币交易消耗的gas并不是百分百消耗完的,在区块链浏览器交易页面,你能看到,每一笔代币交易都有一个gas使用率,而由于每种代币输入的脚本大小不同,因而无法确定gas的使用率。目前为止,我还没找到一个方法可以去精确计算代币的手续费。
**对我的文章感兴趣的话,请关注我的公众号**

钱包开发经验分享:ETH篇的更多相关文章
- JBOSS集群技术升级版解决方案分享(图示篇)
JBOSS集群技术升级版解决方案分享(实现篇) 前段时间,由于阿堂一直较忙,没有写点什么了,有空时一直在关注"web架构和性能,高并发,Cache层"技术领域的 ...
- 关于Java多线程的线程同步和线程通信的一些小问题(顺便分享几篇高质量的博文)
Java多线程的线程同步和线程通信的一些小问题(顺便分享几篇质量高的博文) 前言:在学习多线程时,遇到了一些问题,这里我将这些问题都分享出来,同时也分享了几篇其他博客主的博客,并且将我个人的理解也分享 ...
- 分享一篇 Git Web 开发流程
分享一篇 Git Web 开发流程 web 项目如何进行 git 多人协作开发 https://segmentfault.com/a/1190000018165757
- Nuxt开发经验分享
Nuxt开发经验分享 本文章基于starter-template模板进行讲解,面向有vue-cli开发经验的宝宝 vue init nuxt-community/starter-template ...
- 【Meetup回顾】Apache DolphinScheduler在联通的实践和二次开发经验分享
在由 openLooKeng 社区主办,Apahce DolphinScheduler社区.Apache Pulsar 社区.示说网协办的联合 Meetup 上,来自联通数字科技的王兴杰老师分享了Do ...
- 数百个 HT 工业互联网 2D 3D 可视化应用案例分享 - 2019 篇
继<分享数百个 HT 工业互联网 2D 3D 可视化应用案例>2018 篇,图扑软件定义 2018 为国内工业互联网可视化的元年后,2019 年里我们与各行业客户进行了更深度合作,拓展了H ...
- 分享一篇vue项目规范
最近 Vue 用的比较多,而且因为公司里有实习生,当几个人写一个项目的时候,会出现很多问题,最麻烦的就是规范不统一,之前我有一篇文章是说, vue 是比较有规范的一种框架了,但是也会出现很多问题,所以 ...
- SAS学习经验总结分享:篇一—数据的读取
第一篇:BASE SAS分为数据步的作用及生成数据集的方式 我是学经济相关专业毕业的,从事数据分析工作近一年,之前一直在用EXCEL,自认为EXCEL掌握的还不错. 今年5月份听说了SAS,便开始学习 ...
- 分享几篇文章(PDF版)
后天就要回老家了,除了团团年估计也没有其他好玩的,就琢磨下点经典文章带回去看看. Google一番找到了老赵很早以前的文章: http://www.cnblogs.com/JeffreyZhao/ar ...
随机推荐
- saltStack_template
模版使用 新建文件:vim dns.sls vim file/resolv.conf 执行 : [root@server_client base]# salt \* state.sls dns ...
- selenium webdriver学习(十)------------如何把一个元素拖放到另一个元素里面(转)
selenium webdriver学习(十)------------如何把一个元素拖放到另一个元素里面 博客分类: Selenium-webdriver 元素拖放drag and drop Q群里 ...
- 第三期 行为规划——4.形式化FSM
让我们考虑一个简单的自动售货机,其中一切花费20美分.假设这台自动售货机只需要镍和硬币,但没有更大或更小. 然后,我们可以模拟状态这台自动售货机以已存入的金额为准.起始状态将为零美分.有两种可能发生. ...
- H3C 路由器的作用
- WebService 基础知识点和用Postman调试
阅读连接:Retrofit 用Soap协议访问WebService 详解 参考 1.java发HTTP POST请求(内容为xml格式) 2. android解析XML总结(SAX.Pull.Dom三 ...
- Spark1.6.1 MLlib 特征抽取和变换
Spark1.6.1 MLlib 特征抽取和变换 1 TF-IDF TF-IDF是一种特征向量化方法,这种方法多用于文本挖掘,通过算法可以反应出词在语料库中某个文档中的重要性.文档中词记为t,文档记为 ...
- [android] eclipse里面的安卓模拟器起不来
提示信息可能是: The connection to adb is down, and a severe error has occured. 网上看了下,常见原因有两个: 1,系统里面另外有个叫ad ...
- linux加载和卸载模块
模块建立之后, 下一步是加载到内核. 如我们已指出的, insmod 为你完成这个工作. 这个 程序加载模块的代码段和数据段到内核, 接着, 执行一个类似 ld 的函数, 它连接模块中 任何未解决的符 ...
- jQuery 工具类函数-字符串操作函数
调用名为$.trim的工具函数,能删除字符串中左右两边的空格符,但该函数不能删除字符串中间的空格,调用格式为: $.trim (str); 参数str表示需要删除左右两边空格符的字符串. <bo ...
- Linux环境下安装mysql(远程连接),zookeeper,java,tomcat.
环境阿里云centos7.5 64位 + FinalShell + Navicat Permium 12 用到的压缩包(版本看后缀) 注意:安装均在/usr/local目录下,下面代码中#号不要复制上 ...