1. 程式人生 > >Android以太坊錢包全部功能-基於web3j實現

Android以太坊錢包全部功能-基於web3j實現


這段時間學習了利用web3j來實現Android以太坊錢包的功能。所以這裡做一些相關記錄,方便以後檢視。

需要用到的工具

用工具的目的就是為了測試方便,可以切換到自己的私鏈或者第三方的測試鏈,因為不能把所有的操作都放在以太坊公網去實現,畢竟那是實打實的以太坊幣。

Ganache

它的安裝比較簡單,一直下一步就行
安裝好之後如圖:
在這裡插入圖片描述

它會提供10個賬戶 每個有100個以太幣,而且可以在裡面輕鬆的看到一些交易的紀錄和交易詳情。

Metamask

Metamask是一個基於Chorme的外掛,它最方便的是可以切換不同的測試鏈還可以新增自定義的私鏈,如圖:
在這裡插入圖片描述

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

Ropsten、Kovan和Rinkeby都是Infura提供的測試鏈,Infura的使用也很簡單,註冊登入建立專案之後就可以看到如圖:
在這裡插入圖片描述

建立好之後就需要往裡面充以太幣目前我只用到了Ropsten和Kovan。
Ropsten可以在Metamask中完成充值,具體步驟如下:
在這裡插入圖片描述

在這裡插入圖片描述
在這裡插入圖片描述

在這裡插入圖片描述

至此可以開心的在Ropsten測試環境進行轉賬和交易查詢等操作了。

Kovan的充值有兩種方式:
具體參考
:Kovan充值以太幣

上面這幾種工具基本就可以滿足現目前的以太坊錢包開發的基本功能了,下面開始具體的錢包功能開發。

錢包功能的具體實現

引入依賴

需要用到的依賴不多,就一個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檔案來實現,具體步驟如下:

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/+++: 智慧合約獲取:[email protected]

一個簡單的智慧合約呼叫也完成了。其實主要的還是solidity編寫智慧合約部分,後面的就是流程式的呼叫

查詢交易記錄

暫時還沒做

Android以太坊錢包的大概功能就瞭解這麼多,目前還需要了解的有如何進行代幣的轉賬,以及前面提到的幾個疑問。

主要還是要多花點事件研究solidity編寫智慧合約。