简介
这篇文章主要是介绍ETH移动端(Android)钱包开发,核心功能包括创建钱包、导入钱包、钱包转账(收款)、交易查询等。
关于钱包的基本概念
钱包地址
以0x开头的42位的哈希值 (16进制) 字符串
keystore
明文私钥通过加密算法加密过后的 JSON 格式的字符串, 一般以文件形式存储
助记词
12 (或者 15、18、21) 单词构成, 用户可以通过助记词导入钱包, 但反过来讲, 如果他人得到了你的助记词, 不需要任何密码就可以轻而易举的转移你的资产, 所以要妥善保管自己的助记词
明文私钥
64位的16进制哈希值字符串, 用一句话阐述明文私钥的重要性 “谁掌握了私钥, 谁就掌握了该钱包的使用权!” 同样, 如果他人得到了你的明文私钥, 不需要任何密码就可以轻而易举的转移你的资产
和银行卡做个简单类比
地址=银行卡号
密码=银行卡密码
私钥=银行卡号+银行卡密码
助记词=银行卡号+银行卡密码
Keystore+密码=银行卡号+银行卡密码
Keystore ≠ 银行卡号
私钥通过椭圆曲线签名得到公钥 ,公钥经过哈希得到钱包地址 ,整个过程单向的(不可逆 )
私钥------->公钥------->钱包地址
关于BIP协议
这里先简单介绍一下BIP,后面再单独出一篇文章讲解。
BIP协议是比特币的一个改进协议,在钱包开发中主要用到BIP32、BIP39、BIP44
BIP32:定义了层级确定性钱包( Hierarchical Deterministic wallet ,简称 HD Wallet),是一个系统可以从单一个 seed 产生一树状结构储存多组 keypairs(私钥和公钥)。好处是可以方便的备份、转移到其他相容装置(因为都只需要 seed),以及分层的权限控制等。
BIP39:用于生成助记词,将 seed 用方便记忆和书写的单词表示,一般由 12 个单字组成,单词列表总共有2048个单词。
Wordlists
BIP44:基于 BIP32 的系统,赋予树状结构中的各层特殊的意义。让同一个 seed 可以支援多币种、多帐户等。各层定义如下:
m / purpose' / coin_type' / account' / change / address_index
- purporse': 固定值44', 代表是BIP44
- coin_type': 这个代表的是币种, 可以兼容很多种币, 比如BTC是0', ETH是60',btc一般是
m/44'/0'/0'/0
,eth一般是m/44'/60'/0'/0
- account’:账号
- change’: 0表示外部链(External Chain),用户接收比特币,1表示内部链(Internal Chain),用于接收找零
- address_index:钱包索引
准备工具
eth钱包开发需要借助2个第三方库
web3j:可以理解为eth API的java版本
bitcoinj:生成支持bip32
和bip44
的钱包。还有其他的一些库支持bip32和bip44,比如:Nova Crypto的系列包,包含bip32
,bip39
,bip44
,我就是使用的Nova Crypto系列包。
注意
:因为web3j不支持生成bip44的钱包,而市面上大多数钱包使用bip32,bip39,bip44标准结合生成,所以引用此包。
在创建完钱包之后,你可以使用下面这个工具去测试助记词, 和校验助记词生成的地址、公钥、私钥等。
https://iancoleman.io/bip39/
创建钱包
在了解BIP 后,我们开始以太坊钱包开发,创建的钱包的流程为:
1、随机生成一组助记词
2、生成 seed
3、生成 master key
4、生成 child key
5、我们取第一组child key即m/44'/60'/0'/0/0 得到私钥,keystore及地址
1、引用库:
web3j
implementation 'org.web3j:core:3.3.1-android'
创建钱包相关
全家桶的那个bip32
有点问题,用我这里给出的那个
// implementation 'org.bitcoinj:bitcoinj-core:0.14.7'
implementation 'io.github.novacrypto:BIP39:0.1.9' //用于生成助记词
implementation 'io.github.novacrypto:BIP44:0.0.3'
// implementation 'io.github.novacrypto:BIP32:0.0.9'
//使用这个bip32
implementation 'com.lhalcyon:bip32:1.0.0'
implementation 'com.lambdaworks:scrypt:1.4.0' //加密算法
2、生成随机助记词
/**
* generate a random group of mnemonics
* 生成一组随机的助记词
*/
public String generateMnemonics() {
StringBuilder sb = new StringBuilder();
byte[] entropy = new byte[Words.TWELVE.byteLength()];
new SecureRandom().nextBytes(entropy);
new MnemonicGenerator(English.INSTANCE)
.createMnemonic(entropy, sb::append);
return sb.toString();
}
3. 根据助记词计算出Seed,得到master key ,根据BIP44派生地址,获取KeyPair
/**
* generate key pair to create eth wallet_normal
* 生成KeyPair , 用于创建钱包(助记词生成私钥)
*/
public ECKeyPair generateKeyPair(String mnemonics) {
// 1. we just need eth wallet_normal for now
AddressIndex addressIndex = BIP44
.m()
.purpose44()
.coinType(60)
.account(0)
.external()
.address(0);
// 2. calculate seed from mnemonics , then get master/root key ; Note that the bip39 passphrase we set "" for common
byte[] seed = new SeedCalculator().calculateSeed(mnemonics, "");
ExtendedPrivateKey rootKey = ExtendedPrivateKey.fromSeed(seed, Bitcoin.MAIN_NET);
Log.i(TAG, "mnemonics:" + mnemonics);
String extendedBase58 = rootKey.extendedBase58();
Log.i(TAG, "extendedBase58:" + extendedBase58);
// 3. get child private key deriving from master/root key
ExtendedPrivateKey childPrivateKey = rootKey.derive(addressIndex, AddressIndex.DERIVATION);
String childExtendedBase58 = childPrivateKey.extendedBase58();
Log.i(TAG, "childExtendedBase58:" + childExtendedBase58);
// 4. get key pair
byte[] privateKeyBytes = childPrivateKey.getKey();
ECKeyPair keyPair = ECKeyPair.create(privateKeyBytes);
// we 've gotten what we need
String privateKey = childPrivateKey.getPrivateKey();
String publicKey = childPrivateKey.neuter().getPublicKey();
String address = Keys.getAddress(keyPair);
Log.i(TAG, "privateKey:" + privateKey);
Log.i(TAG, "publicKey:" + publicKey);
Log.i(TAG, "address:" + Constant.PREFIX_16 + address);
return keyPair;
}
这一步已经得到钱包公钥、私钥、地址了。
如果需要测试助记词, 和校验助记词生成的地址, 那么可以访问这个网站 : https://iancoleman.io/bip39/
4、通过keypair创建钱包
/**
* 创建钱包(助记词方式)
*
* @param context app context 上下文
* @param password the wallet_normal password(not the bip39 password) 钱包密码(而不是BIP39的密码)
* @param mnemonics 助记词
* @param walletName 钱包名称
* @return wallet_normal 钱包
*/
public Flowable generateWallet(Context context,
String password,
String mnemonics,
String walletName) {
Flowable flowable = Flowable.just(mnemonics);
return flowable
.map(s -> {
ECKeyPair keyPair = generateKeyPair(s);
WalletFile walletFile = Wallet.createLight(password, keyPair);
HLWallet hlWallet = new HLWallet(walletFile, walletName);
WalletManager.shared().saveWallet(context, hlWallet); //保存钱包信息
return hlWallet;
});
}
这样生成的就是符合bip32、bip39、bip44的钱包,也能和市面上包括imtoken在内的大多数钱包通用了。
HLWallet
public class HLWallet {
public WalletFile walletFile; //钱包文件,包含私钥、keystore、address等信息
public String walletName; //钱包名称
@JsonIgnore
public boolean isCurrent = false;
public HLWallet() {
}
public HLWallet(WalletFile walletFile) {
this.walletFile = walletFile;
}
public HLWallet(WalletFile walletFile, String walletName) {
this.walletFile = walletFile;
this.walletName = walletName;
}
public String getAddress(){
return Constant.PREFIX_16 + this.walletFile.getAddress();
}
public String getWalletName() {
return walletName;
}
public void setWalletName(String walletName) {
this.walletName = walletName;
}
}
导出钱包
导出私钥
通过解密获得ECKeyPair
通过ECKeyPair获得私钥,并转换成16进制,就是最后的私钥了。
/**
* 导出私钥
*
* @param password 创建钱包时的密码
* @param walletFile
* @return
*/
public String exportPrivateKey(String password, WalletFile walletFile) {
try {
// ECKeyPair ecKeyPair = Wallet.decrypt(password, walletFile); //可能出现OOM
ECKeyPair ecKeyPair = LWallet.decrypt(password, walletFile);
String privateKey = Numeric.toHexStringNoPrefix(ecKeyPair.getPrivateKey());
return privateKey;
} catch (CipherException e) {
e.printStackTrace();
return "error";
}
}
导出keystore
/**
* 导出Keystore
*
* @param password 创建钱包时的密码
* @param walletFile
* @return
*/
public String exportKeystore(String password, WalletFile walletFile) {
if (decrypt(password, walletFile)) {
return new Gson().toJson(walletFile);
} else {
return "decrypt failed";
}
}
/**
* 解密
* 如果方法没有抛出CipherException异常则表示解密成功,也就是说可以把Wallet相关信息展示给用户看
*
* @param password 创建钱包时的密码
* @param walletFile
* @return
*/
public boolean decrypt(String password, WalletFile walletFile) {
try {
// ECKeyPair ecKeyPair = Wallet.decrypt(password, walletFile); //可能出现OOM
ECKeyPair ecKeyPair = LWallet.decrypt(password, walletFile);
return true;
} catch (CipherException e) {
e.printStackTrace();
return false;
}
}
注意2点:
1、在导出私钥、keystore、助记词之前都需要先验证密码是否正确,也就是调用如下这个方法,如果没有抛出异常,则把信息展示给用户看。
Wallet.decrypt(password, walletFile);
2、使用web3j的这个decrypt
,经常会抛出OOM异常。关于解决方案,大家查看这里。https://www.jianshu.com/p/41d4a38754a3
导出助记词
助记词是没有办法根据私钥或者keystore推导出来的。一般的做法是在创建钱包的时候把助记词加密后在本地存储,导出时解密。
注意:使用IMToken导入私钥或者KeyStore创建的钱包,没有导出助记词的功能;如果是通过助记词创建的,就会有导出助记词的功能。而且助记词一旦备份之后,备份这个功能就会消失,也就是说从本地存储中删除。
/**
* 导出助记词
*
* @param password
* @param hlWallet
* @return
*/
public String exportMnemonics(String password, HLWallet hlWallet) {
WalletFile walletFile = hlWallet.walletFile;
if (decrypt(password, walletFile)) {
return hlWallet.getMnemonic();
} else {
return "decrypt failed";
}
}