ru.valle.btc.MainActivityTest.java Source code

Java tutorial

Introduction

Here is the source code for ru.valle.btc.MainActivityTest.java

Source

/*
 * The MIT License (MIT)
 * <p/>
 * Copyright (c) 2013 Valentin Konovalov
 * <p/>
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * <p/>
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * <p/>
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package ru.valle.btc;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceManager;
import android.test.ActivityInstrumentationTestCase2;
import android.test.UiThreadTest;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import external.ExternalPrivateKeyStorage;

import org.json.JSONArray;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * This is a simple framework for a test of an Application.  See
 * {@link android.test.ApplicationTestCase ApplicationTestCase} for more information on
 * how to write and extend Application tests.
 * <p/>
 * To run this test, you can type:
 * adb shell am instrument -w \
 * -e class ru.valle.btc.MainActivityTest \
 * ru.valle.btc.tests/android.test.InstrumentationTestRunner
 */
@SuppressWarnings("TryWithIdenticalCatches")
@SuppressLint("SetTextI18n")
public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity> {

    private EditText addressView;
    private EditText privateKeyTextEdit;
    private View qrAddressButton;

    @TargetApi(Build.VERSION_CODES.FROYO)
    public MainActivityTest() {
        super(MainActivity.class);
    }

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        MainActivity mainActivity = getActivity();
        addressView = (EditText) mainActivity.findViewById(R.id.address_label);
        privateKeyTextEdit = (EditText) mainActivity.findViewById(R.id.private_key_label);
        qrAddressButton = mainActivity.findViewById(R.id.qr_address_button);
    }

    public void testAlwaysGenerateNewAddress() {
        Activity activity = getActivity();
        String address = waitForAddress(activity);
        assertNotNull(address);
        activity.finish();
        setActivity(null);
        assertFalse(getActivity().isFinishing());
        activity = getActivity();
        String anotherAddress = waitForAddress(activity);
        assertNotNull(anotherAddress);
        assertNotSame(address, anotherAddress);
    }

    public void testLayoutOnStart() {
        Activity activity = getActivity();
        assertTrue(activity.findViewById(R.id.send_layout).getVisibility() == View.GONE);
        assertTrue(activity.findViewById(R.id.spend_tx_description).getVisibility() == View.GONE);
        assertTrue(activity.findViewById(R.id.spend_tx).getVisibility() == View.GONE);
        activity.finish();
    }

    public void testAddressGenerateOnStartup() {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
        performGenerationTest(preferences, PreferencesActivity.PREF_PRIVATE_KEY_MINI);
        performGenerationTest(preferences, PreferencesActivity.PREF_PRIVATE_KEY_WIF_COMPRESSED);
        performGenerationTest(preferences, PreferencesActivity.PREF_PRIVATE_KEY_WIF_NOT_COMPRESSED);
    }

    private void performGenerationTest(SharedPreferences preferences, String privateKeyType) {
        preferences.edit().putString(PreferencesActivity.PREF_PRIVATE_KEY, privateKeyType).commit();
        getActivity().finish();
        setActivity(null);
        assertFalse(getActivity().isFinishing());
        preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
        assertEquals(privateKeyType, preferences.getString(PreferencesActivity.PREF_PRIVATE_KEY,
                PreferencesActivity.PREF_PRIVATE_KEY_WIF_COMPRESSED));
        checkIfGeneratedKeyIsValid(privateKeyType);
        final Activity activity = getActivity();
        activity.runOnUiThread(new Runnable() {
            public void run() {
                assertTrue(activity.findViewById(R.id.spend_tx_description).getVisibility() == View.GONE);
                assertTrue(activity.findViewById(R.id.spend_tx).getVisibility() == View.GONE);
            }
        });
    }

    private void checkIfGeneratedKeyIsValid(String privateKeyType) {
        String address = waitForAddress(getActivity());
        assertNotNull(address);
        assertTrue("Addresses must starts with '1', but generated address is '" + address + "'",
                address.startsWith("1"));
        String privateKey = getText(getActivity(), R.id.private_key_label);
        assertNotNull(privateKey);
        if (PreferencesActivity.PREF_PRIVATE_KEY_MINI.equals(privateKeyType)) {
            assertTrue("Private keys must starts with 'S', but generated key is '" + privateKey + "'",
                    privateKey.startsWith("S"));
            assertEquals("Private keys should have length 30 characters ", 30, privateKey.length());
        } else if (PreferencesActivity.PREF_PRIVATE_KEY_WIF_COMPRESSED.equals(privateKeyType)) {
            assertTrue("WIF private keys (compressed public) must starts with 'K' or 'L', but generated key is '"
                    + privateKey + "'", privateKey.startsWith("K") || privateKey.startsWith("L"));
            byte[] decoded = BTCUtils.decodeBase58(privateKey);
            assertNotNull(decoded);
            assertEquals("decoded private key (with compressed public key) should be 38 bytes length", 38,
                    decoded.length);
        } else if (PreferencesActivity.PREF_PRIVATE_KEY_WIF_NOT_COMPRESSED.equals(privateKeyType)) {
            assertTrue("WIF private keys (not compressed public) must starts with '5', but generated key is '"
                    + privateKey + "'", privateKey.startsWith("5"));
            byte[] decoded = BTCUtils.decodeBase58(privateKey);
            assertNotNull(decoded);
            assertTrue("decoded private key (with not compressed public key) should be 37 or 38 bytes length",
                    decoded.length == 37 || decoded.length == 38);
        }
    }

    public void testDecodeMiniKey() {
        getActivity().runOnUiThread(new Runnable() {
            public void run() {
                privateKeyTextEdit.setText("S6c56bnXQiBjk9mqSYE7ykVQ7NzrRy");
            }
        });
        getInstrumentation().waitForIdleSync();
        String decodedAddress = waitForAddress(getActivity());
        assertEquals("1CciesT23BNionJeXrbxmjc7ywfiyM4oLW", decodedAddress);
    }

    public void testDecodeUncompressedWIF() {
        getActivity().runOnUiThread(new Runnable() {
            public void run() {
                privateKeyTextEdit.setText("5Kb8kLf9zgWQnogidDA76MzPL6TsZZY36hWXMssSzNydYXYB9KF");
            }
        });
        getInstrumentation().waitForIdleSync();
        String decodedAddress = waitForAddress(getActivity());
        assertEquals("1CC3X2gu58d6wXUWMffpuzN9JAfTUWu4Kj", decodedAddress);
    }

    public void testDecodeCompressedWIF() {
        getActivity().runOnUiThread(new Runnable() {
            public void run() {
                privateKeyTextEdit.setText("KwntMbt59tTsj8xqpqYqRRWufyjGunvhSyeMo3NTYpFYzZbXJ5Hp");
            }
        });
        getInstrumentation().waitForIdleSync();
        String decodedAddress = waitForAddress(getActivity());
        assertEquals("1Q1pE5vPGEEMqRcVRMbtBK842Y6Pzo6nK9", decodedAddress);
    }

    @UiThreadTest
    public void testDecodeAddress() {
        checkDecodeAddress();
    }

    private void checkDecodeAddress() {
        getActivity().runOnUiThread(new Runnable() {
            public void run() {
                addressView.setText("weriufhwehfiow");
                assertEquals("Address qr code button should be visible when an invalid address entered", View.GONE,
                        qrAddressButton.getVisibility());
                addressView.setText("1CciesT23BNionJeXrbxmjc7ywfiyM4oLW");
                assertEquals("You may edit address field", "1CciesT23BNionJeXrbxmjc7ywfiyM4oLW",
                        getString(addressView));
                assertEquals("Typing in address field should clean private key", "", getString(privateKeyTextEdit));
                assertEquals("Address qr code button should be visible when a valid address entered", View.VISIBLE,
                        qrAddressButton.getVisibility());
            }
        });
    }

    public void testDecodeAddressAndWait() {
        checkDecodeAddress();
        getInstrumentation().waitForIdleSync();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        getActivity().runOnUiThread(new Runnable() {
            public void run() {
                assertEquals("You may edit address field and the change must persist",
                        "1CciesT23BNionJeXrbxmjc7ywfiyM4oLW", getString(addressView));
                assertEquals("Typing in address field should clean private key and the change must persist", "",
                        getString(privateKeyTextEdit));
            }
        });
    }

    public void testTxCreationFromUI() {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
        long extraFee = 7;
        preferences.edit().putLong(PreferencesActivity.PREF_EXTRA_FEE, extraFee).commit();
        checkTxCreationFromUI("L49guLBaJw8VSLnKGnMKVH5GjxTrkK4PBGc425yYwLqnU5cGpyxJ", null,
                "1NKkKeTDWWi5LQQdrSS7hghnbhfYtWiWHs",
                "0100000001ef9ea3e6b7a664ff910ed1177bfa81efa018df417fb1ee964b8165a05dc7ef5a000000008b4830450220385373efe509"
                        + "719e38cb63b86ca5d764be0f2bd2ffcfa03194978ca68488f57b0221009686e0b54d7831f9f06d36bfb81c5d2931a8ada079a3ff58c"
                        + "6109030ed0c4cd601410424161de67ec43e5bfd55f52d98d2a99a2131904b25aa08e70924d32ed44bfb4a71c94a7c4fdac886ca5bec7"
                        + "b7fac4209ab1443bc48ab6dec31656cd3e55b5dfcffffffff02707f0088000000001976a9143412c159747b9149e8f0726123e2939b68"
                        + "edb49e88ace0a6e001000000001976a914e9e64aae2d1e066db6c5ecb1a2781f418b18eef488ac00000000",
                "1AyyaMAyo5sbC73kdUjgBK9h3jDMoXzkcP", BTCUtils.MIN_FEE_PER_KB + extraFee,
                31500000 - BTCUtils.MIN_FEE_PER_KB - extraFee);

        checkTxCreationFromUI("L49guLBaJw8VSLnKGnMKVH5GjxTrkK4PBGc425yYwLqnU5cGpyxJ", null,
                "1NKkKeTDWWi5LQQdrSS7hghnbhfYtWiWHs",
                "{\n" + "\t \n" + "\t\"unspent_outputs\":[\n" + "\t\n" + "\t\t{\n"
                        + "\t\t\t\"tx_hash\":\"088676b3e6cfb2f25e35f903b812ddae897ac922653c6ad6b74a188a08ffd253\",\n"
                        + "\t\t\t\"tx_output_n\": 1,\t\n"
                        + "\t\t\t\"script\":\"76a914e9e64aae2d1e066db6c5ecb1a2781f418b18eef488ac\",\n"
                        + "\t\t\t\"value\": 31500000,\n" + "\t\t\t\"confirmations\":0\n" + "\t\t}\n" + "\t  \n"
                        + "\t]\n" + "}",
                "18D5fLcryBDf8Vgov6JTd9Taj81gNekrex", BTCUtils.MIN_FEE_PER_KB + extraFee,
                31500000 - BTCUtils.MIN_FEE_PER_KB - extraFee);

        checkTxCreationFromUI(ExternalPrivateKeyStorage.PRIVATE_KEY_FOR_1AuEGCuHeioQsvSuBYiX2cuNhoZVW7KfWK, null,
                "1AuEGCuHeioQsvSuBYiX2cuNhoZVW7KfWK",
                "\"unspent_outputs\":[\n" + "\t\n" + "\t\t{\n"
                        + "\t\t\t\"tx_hash\":\"ec875732e94898a294c7f83080b729a4d2d12f54aa357cb3edbb38c7ac26973a\",\n"
                        + "\t\t\t\"tx_hash_big_endian\":\"3a9726acc738bbedb37c35aa542fd1d2a429b78030f8c794a29848e9325787ec\",\n"
                        + "\t\t\t\"tx_index\":30464843,\n" + "\t\t\t\"tx_output_n\": 1,\n"
                        + "\t\t\t\"script\":\"76a9146c99d52fba48aaf56de0cc26497a01f00328dd8a88ac\",\n"
                        + "\t\t\t\"value\": 380000,\n" + "\t\t\t\"value_hex\": \"05cc60\",\n"
                        + "\t\t\t\"confirmations\":110025\n" + "\t\t}\n" + "\t  \n" + "\t]",
                "18D5fLcryBDf8Vgov6JTd9Taj81gNekrex", BTCUtils.MIN_FEE_PER_KB + extraFee,
                380000 - BTCUtils.MIN_FEE_PER_KB - extraFee);

    }

    public void testTxCreationFromUIUsingBIP38Key() {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
        long extraFee = 0;
        preferences.edit().putLong(PreferencesActivity.PREF_EXTRA_FEE, extraFee).commit();

        checkTxCreationFromUI(
                ExternalPrivateKeyStorage.ENCRYPTED_PRIVATE_KEY_FOR_1AuEGCuHeioQsvSuBYiX2cuNhoZVW7KfWK,
                ExternalPrivateKeyStorage.PASSWORD_FOR_1AuEGCuHeioQsvSuBYiX2cuNhoZVW7KfWK,
                "1AuEGCuHeioQsvSuBYiX2cuNhoZVW7KfWK",
                "\n" + "\"unspent_outputs\":[\n" + "\t\n" + "\t\t{\n"
                        + "\t\t\t\"tx_hash\":\"ec875732e94898a294c7f83080b729a4d2d12f54aa357cb3edbb38c7ac26973a\",\n"
                        + "\t\t\t\"tx_hash_big_endian\":\"3a9726acc738bbedb37c35aa542fd1d2a429b78030f8c794a29848e9325787ec\",\n"
                        + "\t\t\t\"tx_index\":30464843,\n" + "\t\t\t\"tx_output_n\": 1,\n"
                        + "\t\t\t\"script\":\"76a9146c99d52fba48aaf56de0cc26497a01f00328dd8a88ac\",\n"
                        + "\t\t\t\"value\": 380000,\n" + "\t\t\t\"value_hex\": \"05cc60\",\n"
                        + "\t\t\t\"confirmations\":110025\n" + "\t\t}\n" + "\t  \n" + "\t]",
                "18D5fLcryBDf8Vgov6JTd9Taj81gNekrex", BTCUtils.MIN_FEE_PER_KB, 380000 - BTCUtils.MIN_FEE_PER_KB);
    }

    private void checkTxCreationFromUI(final String privateKey, final String password,
            final String expectedAddressForTheKey, final String unspentTxInfo, final String recipientAddress,
            long expectedFee, long expectedAmountInFirstOutput) {
        getActivity().runOnUiThread(new Runnable() {
            public void run() {
                ((EditText) getActivity().findViewById(R.id.address_label)).setText("");
                ((EditText) getActivity().findViewById(R.id.private_key_label)).setText(privateKey);
                if (!TextUtils.isEmpty(password)) {
                    ((EditText) getActivity().findViewById(R.id.password_edit)).setText(password);

                }
            }
        });
        getInstrumentation().waitForIdleSync();

        String decodedAddress = null;
        if (!TextUtils.isEmpty(password)) {
            boolean readyForDecryption = false;
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                String generatedAddress = getText(getActivity(), R.id.address_label);
                if (!TextUtils.isEmpty(generatedAddress)) {
                    if (generatedAddress.startsWith("1")) {
                        decodedAddress = generatedAddress;
                        break;
                    } else if (getActivity().getString(R.string.not_decrypted_yet).equals(generatedAddress)) {
                        readyForDecryption = true;
                        break;
                    }
                }
            }
            if (readyForDecryption) {
                getActivity().runOnUiThread(new Runnable() {
                    public void run() {
                        Button button = (Button) getActivity().findViewById(R.id.password_button);
                        button.performClick();
                    }
                });
                getInstrumentation().waitForIdleSync();
                decodedAddress = waitForAddress(getActivity());
            }
        } else {
            decodedAddress = waitForAddress(getActivity());
        }

        assertEquals(expectedAddressForTheKey, decodedAddress);
        getActivity().runOnUiThread(new Runnable() {
            public void run() {
                ((EditText) getActivity().findViewById(R.id.amount)).setText("");
                ((EditText) getActivity().findViewById(R.id.raw_tx)).setText(unspentTxInfo);
                ((EditText) getActivity().findViewById(R.id.recipient_address)).setText(recipientAddress);
            }
        });
        String createdTx = null;
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            createdTx = getText(getActivity(), R.id.spend_tx);
            if (!TextUtils.isEmpty(createdTx)) {
                break;
            }
        }
        assertNotNull(createdTx);

        ArrayList<UnspentOutputInfo> unspentOutputs = new ArrayList<>();
        byte[] rawTx = BTCUtils.fromHex(unspentTxInfo);
        if (rawTx != null) {
            Transaction baseTx = null;
            try {
                baseTx = new Transaction(rawTx);
            } catch (BitcoinException ignored) {
            }
            assertNotNull(baseTx);
            byte[] rawTxReconstructed = baseTx.getBytes();
            if (!Arrays.equals(rawTxReconstructed, rawTx)) {
                throw new IllegalArgumentException("Unable to decode given transaction");
            }
            byte[] txHash = BTCUtils.reverse(BTCUtils.doubleSha256(rawTx));
            for (int outputIndex = 0; outputIndex < baseTx.outputs.length; outputIndex++) {
                Transaction.Output output = baseTx.outputs[outputIndex];
                unspentOutputs.add(new UnspentOutputInfo(txHash, output.script, output.value, outputIndex, -1));
            }
        } else {
            try {
                String jsonStr = unspentTxInfo.replace((char) 160, ' ').trim();//remove nbsp
                if (!jsonStr.startsWith("{")) {
                    jsonStr = "{" + jsonStr;
                }
                if (!jsonStr.endsWith("}")) {
                    jsonStr += "}";
                }
                JSONObject jsonObject = new JSONObject(jsonStr);
                JSONArray unspentOutputsArray = jsonObject.getJSONArray("unspent_outputs");
                for (int i = 0; i < unspentOutputsArray.length(); i++) {
                    JSONObject unspentOutput = unspentOutputsArray.getJSONObject(i);
                    byte[] txHash = BTCUtils.reverse(BTCUtils.fromHex(unspentOutput.getString("tx_hash")));
                    Transaction.Script script = new Transaction.Script(
                            BTCUtils.fromHex(unspentOutput.getString("script")));
                    long value = unspentOutput.getLong("value");
                    int outputIndex = unspentOutput.getInt("tx_output_n");
                    long confirmations = unspentOutput.has("confirmations") ? unspentOutput.getLong("confirmations")
                            : -1;
                    unspentOutputs.add(new UnspentOutputInfo(txHash, script, value, outputIndex, confirmations));
                }
            } catch (Exception e) {
                assertFalse(e.getMessage(), true);
            }
        }

        Transaction spendTx = null;
        try {
            spendTx = new Transaction(BTCUtils.fromHex(createdTx));
        } catch (BitcoinException ignored) {
        }
        assertNotNull(spendTx);
        long inValue = 0;
        for (Transaction.Input input : spendTx.inputs) {
            for (UnspentOutputInfo unspentOutput : unspentOutputs) {
                if (Arrays.equals(unspentOutput.txHash, input.outPoint.hash)
                        && unspentOutput.outputIndex == input.outPoint.index) {
                    inValue += unspentOutput.value;
                }
            }
        }
        long outValue = 0;
        for (Transaction.Output output : spendTx.outputs) {
            outValue += output.value;
        }
        long fee = inValue - outValue;
        assertEquals(expectedFee, fee);
        assertEquals(expectedAmountInFirstOutput, spendTx.outputs[0].value);

        try {
            Transaction.Script[] relatedScripts = new Transaction.Script[spendTx.inputs.length];
            for (int i = 0; i < spendTx.inputs.length; i++) {
                Transaction.Input input = spendTx.inputs[i];
                for (UnspentOutputInfo unspentOutput : unspentOutputs) {
                    if (Arrays.equals(unspentOutput.txHash, input.outPoint.hash)
                            && unspentOutput.outputIndex == input.outPoint.index) {
                        relatedScripts[i] = unspentOutput.script;
                        break;
                    }
                }
                assertNotNull("and where is unspent output's script for this input?", relatedScripts[i]);
            }
            BTCUtils.verify(relatedScripts, spendTx);
        } catch (Transaction.Script.ScriptInvalidException e) {
            assertFalse(e.getMessage(), true);
        }
    }

    private String waitForAddress(Activity activity) {
        for (int i = 0; i < 150; i++) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String generatedAddress = getText(activity, R.id.address_label);
            if (!TextUtils.isEmpty(generatedAddress) && generatedAddress.startsWith("1")) {
                return generatedAddress;
            }
        }
        return null;
    }

    private String getText(final Activity activity, final int id) {
        FutureTask<String> task = new FutureTask<>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                TextView textView = ((TextView) activity.findViewById(id));
                return textView.getVisibility() == View.VISIBLE ? getString(textView) : null;
            }
        });
        activity.runOnUiThread(task);
        try {
            return task.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        return null;
    }

    private static String getString(TextView textView) {
        CharSequence charSequence = textView == null ? null : textView.getText();
        return charSequence == null ? "" : charSequence.toString();
    }

}