Android以太坊钱包全部功能-基于web3j实现


这段时间学习了利用web3j来实现Android以太坊钱包的功能。所以这里做一些相关记录,方便以后查看。

需要用到的工具

用工具的目的就是为了测试方便,可以切换到自己的私链或者第三方的测试链,因为不能把所有的操作都放在以太坊公网去实现,毕竟那是实打实的以太坊币。

Ganache

它的安装比较简单,一直下一步就行
安装好之后如图:
在这里插入图片描述

它会提供10个账户 每个有100个以太币,而且可以在里面轻松的看到一些交易的纪录和交易详情。

Metamask

Metamask是一个基于Chorme的插件,它最方便的是可以切换不同的测试链还可以添加自定义的私链,如图:
在这里插入图片描述

Main Ethereum :表示以太坊公网,也就是正式环境

Ropsten、Kovan和Rinkeby都是Infura提供的测试链,Infura的使用也很简单,注册登录创建项目之后就可以看到如图:
在这里插入图片描述

创建好之后就需要往里面充以太币目前我只用到了Ropsten和Kovan。
Ropsten可以在Metamask中完成充值,具体步骤如下:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

至此可以开心的在Ropsten测试环境进行转账和交易查询等操作了。

Kovan的充值有两种方式:
具体参考
:Kovan充值以太币

大概就是在这个网址https://gitter.im/kovan-testnet/faucet发送一个你需要充值的账户地址,然后就是等……

上面这几种工具基本就可以满足现目前的以太坊钱包开发的基本功能了,下面开始具体的钱包功能开发。

钱包功能的具体实现

引入依赖

需要用到的依赖不多,就一个web3j和一个用于生成助记词的库,之所以引入第二个库是因为web3j提供的生成助记词的MnemonicUtils.generateMnemonic()一直报错,目前还没找到解决的办法。

  implementation 'org.web3j:core:3.3.1-android'
  implementation 'io.github.novacrypto:BIP39:0.1.9'//用于生成助记词

按我现在的理解,各个环境创建的钱包都可以互相导入,只是里面的以太币不通用,所以在创建钱包和导入钱包的环节都还是使用以太坊主网。

创建钱包

创建钱包目前找到两种实现方式

第一种创建方式

可以获取到钱包的私钥、keystore、address等信息,但是没找到获取助记词的方法,实现方式如下:

    //钱包密码
  pwd = mEdPasswd.getText().toString().trim();
    try {
        File fileDir = new File(Environment.getExternalStorageDirectory().getPath() + "/MyWallet");
        if (!fileDir.exists()) {
            fileDir.mkdirs();
        }

        ECKeyPair ecKeyPair = Keys.createEcKeyPair();
        //keystore文件名
        String filename = WalletUtils.generateWalletFile(pwd, ecKeyPair, fileDir, false);
        //获取keystore内容
        File KeyStore = new File(Environment.getExternalStorageDirectory().getPath() + "/MyWallet/" + filename);
        Log.e("+++","keystore:"+getDatafromFile(KeyStore.getAbsolutePath()));

        String msg = "fileName:\n" + filename
                + "\nprivateKey:\n" + Numeric.encodeQuantity(ecKeyPair.getPrivateKey())
                + "\nPublicKey:\n" + Numeric.encodeQuantity(ecKeyPair.getPublicKey());
        Log.e("+++", "create:" + msg);
    } catch (Exception e) {
        e.printStackTrace();
    }

创建成功后打印信息:

    create:fileName:
    UTC--2018-09-18T22-06-27.233--01e2a527948f35514b1bd2ddbb6023c7757c3254.json
    privateKey:
    0x44fd9b00d0e17ccd0c414f0fe3d72c8a6c1b89a3e50d9ef0a267673d150ca1bb
    PublicKey:
    0xf91431f4deda321070ddb0b3836a8ed92caec4ec735bd4f71916f8de416b0b1361b95584a6042a121bad8bed8cab6e19e176fddbf763cf9c99e4ea4382513f54
第二种创建方式

这种创建方式可以获取到钱包的全部信息,代码如下:

    File fileDir = new File(Environment.getExternalStorageDirectory().getPath() + "/MyWallet");
    if (!fileDir.exists()) {
        fileDir.mkdirs();
    }

    StringBuilder sb = new StringBuilder();
    byte[] entropy = new byte[Words.TWELVE.byteLength()];
    new SecureRandom().nextBytes(entropy);
    new MnemonicGenerator(English.INSTANCE).createMnemonic(entropy, sb::append);
    String mnemonics = sb.toString();
    android.util.Log.e("+++","生成的助记词:"+mnemonics);
    
    //password为输入的钱包密码
    byte[] seed = MnemonicUtils.generateSeed(mnemonics, password);
    ECKeyPair privateKey = ECKeyPair.create(sha256(seed));

    String walletFile = null;
    try {
        walletFile = generateWalletFile(password, privateKey, fileDir, false);
    } catch (CipherException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
    Bip39Wallet bip39Wallet = new Bip39Wallet(walletFile, mnemonics);

    Log.e("+++",bip39Wallet.getFilename());

    keystore = getDatafromFile(new File(Environment.getExternalStorageDirectory().getPath() + "/MyWallet/"+bip39Wallet.getFilename()).getAbsolutePath());
    Log.e("+++","keystore:"+keystore);

    //导入助记词获取钱包地址
    Credentials credentials = WalletUtils.loadBip39Credentials(password,bip39Wallet.getMnemonic());
    Log.e("+++","导入助记词获取钱包地址:"+credentials.getAddress());

    String msg = "\n助记词:\n" + bip39Wallet.getMnemonic()
            +"\naddress:\n" + credentials.getAddress()
            + "\nprivateKey:\n" + Numeric.encodeQuantity(credentials.getEcKeyPair().getPrivateKey())
            + "\nPublicKey:\n" + Numeric.encodeQuantity(credentials.getEcKeyPair().getPublicKey());

    mTvMsg.setText(msg);
    Log.e("+++",msg);

打印信息 :

09-18 22:37:14.030 8963-8963/com.my.eth_demo E/+++: 助记词:
    shine creek tragic unable admit disease oil result various gun kingdom shop
    address:
    0xc5bbd2ea03eaa5e94a3af8bb4d8bfc178f837de3
    privateKey:
    0xa4756509c9c58cb5165f74797c50c4a2eddc160eade08989b1403755cfdf6e60
    PublicKey:
    0x8c7891f319582bf9bc653e3032be4406666e86938e4b860e28bc08b272f080306fcdbbbad5754b88a7dd90ad054b3ac4c9c40facda15c27b60180b27376f2ba9

注意上面两种都需要开启文件权限。

总结一下钱包提到的几个关键词语,钱包密码、助记词、私钥、地址、公钥,借助网上的一段描述:
若以银行账户为类比,这 5 个词分别对应内容如下:

地址=银行卡号
密码=银行卡密码
私钥=银行卡号+银行卡密码
助记词=银行卡号+银行卡密码
Keystore+密码=银行卡号+银行卡密码
Keystore ≠ 银行卡号

到这里两种钱包的创建方式都已经实现了,但还是有两点疑问
1、第一种方式创建的钱包为什么获取不到助记词?
2、第二种方式中的助记词生成方式是否能行?

keystore导入钱包

keystore导入钱包需要keystore和密码,代码如下:

       String password = mEdPasswd.getText().toString().trim();
        String keystore = mEdKeyStore.getText().toString().trim();
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        WalletFile walletFile = null;
        try {
            walletFile = objectMapper.readValue(keystore, WalletFile.class);
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            ECKeyPair keyPair = Wallet.decrypt(password, walletFile);
            WalletFile generateWalletFile = Wallet.createLight(password, keyPair);
            Log.e("+++","keyStoreImportAddress:"+generateWalletFile.getAddress());
            mTvMsg.setText("Address:"+generateWalletFile.getAddress());
            MyApplication.wallets.add(generateWalletFile.getAddress());
        } catch (CipherException e) {
            e.printStackTrace();
        }

打印信息:

09-18 22:47:19.046 18531-18531/com.my.eth_demo E/+++: keyStoreImportAddress:d62a21fb6771858f6bf0bd1a83b583ec3bbe08fa
    privatekey:77352659602195464805734571732795606172527138956341173026077776154531522435270

助记词导入钱包

助记词导入钱包需要助记词,这里提供的密码相当于重置钱包密码

        String menmory = mEdMemory.getText().toString().trim();
        String passwd = mEdPasswd.getText().toString().trim();

        List mnemonicList = Arrays.asList(menmory.split(" "));
        byte[] seed = new SeedCalculator()
                .withWordsFromWordList(English.INSTANCE)
                .calculateSeed(mnemonicList, passwd);
        ECKeyPair ecKeyPair = ECKeyPair.create(Sha256.sha256(seed));
        String privateKey = ecKeyPair.getPrivateKey().toString(16);
        String publicKey = ecKeyPair.getPublicKey().toString(16);
        String address = "0x" + Keys.getAddress(publicKey);
        //创建钱包地址与密钥
        String fileName = null;
        try {
            fileName = WalletUtils.generateWalletFile(passwd, ecKeyPair, new File(Environment.getExternalStorageDirectory().getPath() + "/MyWallet"), false);
        } catch (Exception e) {
            e.printStackTrace();
        }

        Log.e("+++","privateKey:"+privateKey);
        Log.e("+++","address:"+address);

打印信息:

09-19 21:09:34.218 24695-24695/com.my.eth_demo E/+++:  
    privateKey:2604bcf5fed76ce9bfd2056829594a5b77b702862f7effb1651f6b0218b6daeb
    address:0x92f1d2317c1353ad112dfb4c36ccabbd5fbb523a

私钥导入钱包

私钥导入只需要钱包私钥

        Credentials credentials = Credentials.create(privateKey);
        ECKeyPair ecKeyPair = credentials.getEcKeyPair();
        KeyStoreUtils.genKeyStore2Files(ecKeyPair);
        String msg = "address:\n" + credentials.getAddress()
                + "\nprivateKey:\n" + Numeric.encodeQuantity(ecKeyPair.getPrivateKey())
                + "\nPublicKey:\n" + Numeric.encodeQuantity(ecKeyPair.getPublicKey());

        Log.e("+++", "daoru:" + msg);

打印信息:

09-18 22:51:56.669 20830-20830/com.my.eth_demo E/+++: daoru:address:
    0x20bd719345dcc624dc43b30e893eac21099a2ab0
    privateKey:
    0xec8559f8e38ed088f56c328f71753b296d43958b4d64a0b906147b5bffca0279
    PublicKey:
    0x7591a2d625bc9d08ab9b161834f5055368082e7d40a894bf968ab7b07e7ace6ed418388fa608fe6d1c057b9fcac3a03467145edbcac5cae0b7249a4a05666901

Ganache环境

Ganache的安装很简单,安装完成后上面会有一个RPC SERVER,默认HTTP://127.0.0.1:7545,如果需要链接到这个环境只需要在web3j做如下配置:

//本机ip
Web3j web3j = Web3jFactory.build(new HttpService("http://xxx.xxx.xx.xxx:7545"))

链接infura第三方私链

Infura提供了多种测试链这里以Ropsten为例,在这个测试链上做一个0.01以太币的转账,然后在Etherscan上查看转账情况。

  //切换到Ropsten环境
  Web3j web3j = Web3jFactory.build(new HttpService("https://ropsten.infura.io/v3/b1a395a114ba485586c43d0fa227d443"));
  
  //目标账户
    String toAddress = "0x2B3e66B96924a170c4367e564C6638F28a620110";
    //转出账户
    Credentials credentials1  = Credentials.create("D3F293CC53D86F1B93A16E873FEAD44BA14F0E50987719307318A21A0A7C21D1");
    
    //通过提供的Transfer进行转账
    TransactionReceipt transactionReceipt = null;
    try {
        transactionReceipt = Transfer.sendFunds(
                web3j, credentials1, toAddress,
                BigDecimal.valueOf(0.01), Convert.Unit.ETHER).send();
    } catch (Exception e) {
        e.printStackTrace();
    }
    
    Log.e("+++","第三方私链转账Hash:"+transactionReceipt.getTransactionHash());

打印信息:

09-19 21:23:02.997 28951-30330/com.my.eth_demo E/+++: 第三方私链转账Hash:0xd5ddeaf77dad663493c90bd1f9f7f98813cdfae32d65e447d23ab8e89f12df08

然后在Etherscan上查看交易记录:

在这里插入图片描述

至此已经链接到Ropsten测试链上,并且进行了一笔以太币转账交易。

以太坊钱包转账

上面就已经成功链接到Ropsten测试链上并且进行了一笔交易,再记录一下另一种以太币转账方法,暂时无法区分二者的区别,先转3个以太币:


        EthGetTransactionCount ethGetTransactionCount = null;
        try {
            ethGetTransactionCount = web3j.ethGetTransactionCount(
                    fromAddress, DefaultBlockParameterName.LATEST).sendAsync().get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        BigInteger nonce = ethGetTransactionCount.getTransactionCount();

        RawTransaction rawTransaction = RawTransaction.createEtherTransaction(
                nonce, Convert.toWei("18", Convert.Unit.GWEI).toBigInteger(),
                Convert.toWei("45000", Convert.Unit.WEI).toBigInteger(), toAddress, new BigInteger("3000000000000000000"));
        byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, credentials);
        String hexValue = Numeric.toHexString(signedMessage);

        EthSendTransaction ethSendTransaction = null;
        try {
            ethSendTransaction = web3j.ethSendRawTransaction(hexValue).sendAsync().get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        if (ethSendTransaction.hasError()) {
            Log.e("+++transfer error:", ethSendTransaction.getError().getMessage());
        } else {
            String transactionHash = ethSendTransaction.getTransactionHash();
            Log.e("+++transactionHash:", ""+ transactionHash);
        }

一样到Etherscan上查看转账记录:
在这里插入图片描述

监听交易

当有交易发生时会触发,可以查看交易信息。

        //监听交易
        web3j.transactionObservable().subscribe(new Subscriber<Transaction>() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onNext(Transaction transaction) {
                Log.e("+++",""+transaction);
            }
        });

查询余额

        //查询余额
        DefaultBlockParameter defaultBlockParameter = new DefaultBlockParameterNumber(13);
        EthGetBalance ethGetBalance = null;
        try {
            ethGetBalance = web3j.ethGetBalance("0x38B4D9fe0aC062AC09Cc7a6FB45Ba9319c6B688e", DefaultBlockParameterName.LATEST).send();
            Log.e("+++","balance:"+Convert.fromWei(ethGetBalance.getBalance().toString(), Convert.Unit.ETHER));

        } catch (IOException e) {
            e.printStackTrace();
        }

查看交易详情

    //获取交易详情
    try {
        org.web3j.protocol.core.methods.response.Transaction transactionResult =  web3j.ethGetTransactionByHash(transactionReceipt.getTransactionHash()).send().getResult();
        Log.e("+++","交易详情:"+transactionResult.getFrom());
    } catch (IOException e) {
        e.printStackTrace();
    }

详情可以获取到的信息如图:
在这里插入图片描述

智能合约

可以通过web3j去写智能合约,也可以通过solidity写智能合约,然后通过web3j转换成java文件来实现,具体步骤如下:

1、下载web3j命令行
2、安装solc

npm install -g solc

3、编写智能合约

pragma solidity ^0.4.0;

contract SimpleStorage {
    uint storedData;

    function set(uint x) public {
        storedData = x;
    }

    function get() public view returns (uint) {
        return storedData;
    }
}

4、生成bin和abi文件

solcjs myContract.sol   --optimize  --bin --abi --output-dir C:\Users\wangc\Desktop\

5、切换到web3j命令行工具的bin目录下,生成java文件:

web3j solidity generate C:\Users\wangc\Desktop\myContract_sol_myContract.bin C:\Users\wangc\Desktop\myContract_sol_myContract.abi -o C:\Users\wangc\Desktop\ -p com.my.contract

6、将java文件放入项目中使用

        //智能合约调用
        try {
            MyContract_sol_myContract contract = MyContract_sol_myContract.deploy(web3j, credentials1, GAS_PRICE, GAS_LIMIT).send();
            Log.e("+++","智能合约地址:"+contract.getContractAddress());
            Object o2 = contract.set(new BigInteger("1")).send();
            Log.e("+++","智能合约获取:"+contract.get());

        } catch (Exception e) {
            e.printStackTrace();
        }

打印信息:

09-19 22:26:05.802 18270-19185/com.dajiabao.eth_demo E/+++: 智能合约地址:0xe36f4dd87295aa33364c268029d64330422bb5e6
09-19 22:27:40.441 18270-19185/com.dajiabao.eth_demo E/+++: 智能合约获取:org.web3j.protocol.core.RemoteCall@69ceb67

一个简单的智能合约调用也完成了。其实主要的还是solidity编写智能合约部分,后面的就是流程式的调用

查询交易记录

暂时还没做

Android以太坊钱包的大概功能就了解这么多,目前还需要了解的有如何进行代币的转账,以及前面提到的几个疑问。

主要还是要多花点事件研究solidity编写智能合约。


版权声明:本文为wangcheng_原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。