【ETH钱包开发01】创建、导出钱包

简介

这篇文章主要是介绍ETH移动端(Android)钱包开发,核心功能包括创建钱包、导入钱包、钱包转账(收款)、交易查询等。

关于钱包的基本概念

钱包地址
以0x开头的42位的哈希值 (16进制) 字符串

keystore
明文私钥通过加密算法加密过后的 JSON 格式的字符串, 一般以文件形式存储

助记词
12 (或者 15、18、21) 单词构成, 用户可以通过助记词导入钱包, 但反过来讲, 如果他人得到了你的助记词, 不需要任何密码就可以轻而易举的转移你的资产, 所以要妥善保管自己的助记词

明文私钥
64位的16进制哈希值字符串, 用一句话阐述明文私钥的重要性 “谁掌握了私钥, 谁就掌握了该钱包的使用权!” 同样, 如果他人得到了你的明文私钥, 不需要任何密码就可以轻而易举的转移你的资产

和银行卡做个简单类比
地址=银行卡号
密码=银行卡密码
私钥=银行卡号+银行卡密码
助记词=银行卡号+银行卡密码
Keystore+密码=银行卡号+银行卡密码
Keystore ≠ 银行卡号

私钥通过椭圆曲线签名得到公钥 ,公钥经过哈希得到钱包地址 ,整个过程单向的(不可逆 )
私钥------->公钥------->钱包地址

关于BIP协议

这里先简单介绍一下BIP,后面再单独出一篇文章讲解。

BIP协议是比特币的一个改进协议,在钱包开发中主要用到BIP32BIP39BIP44

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

推荐阅读更多精彩内容