diff --git a/navigation/DetailViewScreensStack.tsx b/navigation/DetailViewScreensStack.tsx
index 575c7ed40..34156f1c9 100644
--- a/navigation/DetailViewScreensStack.tsx
+++ b/navigation/DetailViewScreensStack.tsx
@@ -50,7 +50,7 @@ import {
NotificationSettingsComponent,
PlausibleDeniabilityComponent,
ReleaseNotesComponent,
- SelftestComponent,
+ SelfTestComponent,
SettingsComponent,
SettingsPrivacyComponent,
ToolsComponent,
@@ -319,8 +319,8 @@ const DetailViewStackScreensStack = () => {
options={navigationStyle({ title: loc.settings.notifications })(theme)}
/>
import('../screen/settings/electrumSettings'
const EncryptStorage = lazy(() => import('../screen/settings/encryptStorage'));
const LightningSettings = lazy(() => import('../screen/settings/lightningSettings'));
const NotificationSettings = lazy(() => import('../screen/settings/notificationSettings'));
-const Selftest = lazy(() => import('../screen/selftest'));
+const SelfTest = lazy(() => import('../screen/settings/SelfTest'));
const ReleaseNotes = lazy(() => import('../screen/settings/ReleaseNotes'));
const Tools = lazy(() => import('../screen/settings/tools'));
const SettingsPrivacy = lazy(() => import('../screen/settings/SettingsPrivacy'));
@@ -84,9 +84,9 @@ export const NotificationSettingsComponent = () => (
);
-export const SelftestComponent = () => (
+export const SelfTestComponent = () => (
}>
-
+
);
diff --git a/screen/selftest.js b/screen/selftest.js
index 32e5f48e3..1563ec1bd 100644
--- a/screen/selftest.js
+++ b/screen/selftest.js
@@ -35,7 +35,7 @@ const styles = StyleSheet.create({
},
});
-export default class Selftest extends Component {
+export default class SelfTest extends Component {
constructor(props) {
super(props);
this.state = {
@@ -335,7 +335,7 @@ function assertStrictEqual(actual, expected, message) {
}
}
-Selftest.propTypes = {
+SelfTest.propTypes = {
navigation: PropTypes.shape({
navigate: PropTypes.func,
goBack: PropTypes.func,
diff --git a/screen/settings/SelfTest.js b/screen/settings/SelfTest.js
new file mode 100644
index 000000000..7eca83f95
--- /dev/null
+++ b/screen/settings/SelfTest.js
@@ -0,0 +1,345 @@
+import BIP32Factory from 'bip32';
+import bip38 from 'bip38';
+import * as bip39 from 'bip39';
+import * as bitcoin from 'bitcoinjs-lib';
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { Linking, ScrollView, StyleSheet, View } from 'react-native';
+import BlueCrypto from 'react-native-blue-crypto';
+import wif from 'wif';
+
+import * as BlueElectrum from '../../blue_modules/BlueElectrum';
+import * as encryption from '../../blue_modules/encryption';
+import * as fs from '../../blue_modules/fs';
+import ecc from '../../blue_modules/noble_ecc';
+import { BlueCard, BlueLoading, BlueSpacing20, BlueText } from '../../BlueComponents';
+import {
+ HDAezeedWallet,
+ HDSegwitBech32Wallet,
+ HDSegwitP2SHWallet,
+ LegacyWallet,
+ SegwitP2SHWallet,
+ SLIP39LegacyP2PKHWallet,
+} from '../../class';
+import presentAlert from '../../components/Alert';
+import Button from '../../components/Button';
+import SafeArea from '../../components/SafeArea';
+import SaveFileButton from '../../components/SaveFileButton';
+import loc from '../../loc';
+
+const bip32 = BIP32Factory(ecc);
+
+const styles = StyleSheet.create({
+ center: {
+ alignItems: 'center',
+ },
+});
+
+export default class SelfTest extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isLoading: true,
+ };
+ }
+
+ onPressImportDocument = async () => {
+ try {
+ fs.showFilePickerAndReadFile().then(file => {
+ if (file && file.data && file.data.length > 0) {
+ presentAlert({ message: file.data });
+ } else {
+ presentAlert({ message: 'Error reading file' });
+ }
+ });
+ } catch (err) {
+ console.log(err);
+ }
+ };
+
+ async componentDidMount() {
+ console.debug('SelfTest - componentDidMount');
+ let errorMessage = '';
+ let isOk = true;
+
+ try {
+ if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
+ const uniqs = {};
+ const w = new SegwitP2SHWallet();
+ for (let c = 0; c < 1000; c++) {
+ await w.generate();
+ if (uniqs[w.getSecret()]) {
+ throw new Error('failed to generate unique private key');
+ }
+ uniqs[w.getSecret()] = 1;
+ }
+ } else {
+ // skipping RN-specific test
+ }
+
+ //
+
+ if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
+ await BlueElectrum.ping();
+ await BlueElectrum.waitTillConnected();
+ const addr4elect = '3GCvDBAktgQQtsbN6x5DYiQCMmgZ9Yk8BK';
+ const electrumBalance = await BlueElectrum.getBalanceByAddress(addr4elect);
+ if (electrumBalance.confirmed !== 51432)
+ throw new Error('BlueElectrum getBalanceByAddress failure, got ' + JSON.stringify(electrumBalance));
+
+ const electrumTxs = await BlueElectrum.getTransactionsByAddress(addr4elect);
+ if (electrumTxs.length !== 1) throw new Error('BlueElectrum getTransactionsByAddress failure, got ' + JSON.stringify(electrumTxs));
+ } else {
+ // skipping RN-specific test'
+ }
+
+ if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
+ const aezeed = new HDAezeedWallet();
+ aezeed.setSecret(
+ 'abstract rhythm weird food attract treat mosquito sight royal actor surround ride strike remove guilt catch filter summer mushroom protect poverty cruel chaos pattern',
+ );
+ assertStrictEqual(await aezeed.validateMnemonicAsync(), true, 'Aezeed failed');
+ assertStrictEqual(aezeed._getExternalAddressByIndex(0), 'bc1qdjj7lhj9lnjye7xq3dzv3r4z0cta294xy78txn', 'Aezeed failed');
+ } else {
+ // skipping RN-specific test
+ }
+
+ let l = new LegacyWallet();
+ l.setSecret('L4ccWrPMmFDZw4kzAKFqJNxgHANjdy6b7YKNXMwB4xac4FLF3Tov');
+ assertStrictEqual(l.getAddress(), '14YZ6iymQtBVQJk6gKnLCk49UScJK7SH4M');
+ let utxos = [
+ {
+ txid: 'cc44e933a094296d9fe424ad7306f16916253a3d154d52e4f1a757c18242cec4',
+ vout: 0,
+ value: 100000,
+ txhex:
+ '0200000000010161890cd52770c150da4d7d190920f43b9f88e7660c565a5a5ad141abb6de09de00000000000000008002a0860100000000001976a91426e01119d265aa980390c49eece923976c218f1588ac3e17000000000000160014c1af8c9dd85e0e55a532a952282604f820746fcd02473044022072b3f28808943c6aa588dd7a4e8f29fad7357a2814e05d6c5d767eb6b307b4e6022067bc6a8df2dbee43c87b8ce9ddd9fe678e00e0f7ae6690d5cb81eca6170c47e8012102e8fba5643e15ab70ec79528833a2c51338c1114c4eebc348a235b1a3e13ab07100000000',
+ },
+ ];
+
+ let txNew = l.createTransaction(utxos, [{ value: 90000, address: '1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB' }], 1, l.getAddress());
+ const txBitcoin = bitcoin.Transaction.fromHex(txNew.tx.toHex());
+ assertStrictEqual(
+ txNew.tx.toHex(),
+ '0200000001c4ce4282c157a7f1e4524d153d3a251669f10673ad24e49f6d2994a033e944cc000000006b48304502210091e58bd2021f2eeea8d39d7f7b053c9ccc52a747b60f1c3584ba33285e2d150602205b2d35a2536cbe157015e8c54a26f5fc350cc7c72b5ca80b9e548917993f652201210337c09b3cb889801638078fd4e6998218b28c92d338ea2602720a88847aedceb3ffffffff02905f0100000000001976a914aa381cd428a4e91327fd4434aa0a08ff131f1a5a88ac2e260000000000001976a91426e01119d265aa980390c49eece923976c218f1588ac00000000',
+ );
+ assertStrictEqual(txBitcoin.ins.length, 1);
+ assertStrictEqual(txBitcoin.outs.length, 2);
+ assertStrictEqual('1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB', bitcoin.address.fromOutputScript(txBitcoin.outs[0].script)); // to address
+ assertStrictEqual(l.getAddress(), bitcoin.address.fromOutputScript(txBitcoin.outs[1].script)); // change address
+
+ //
+
+ l = new SegwitP2SHWallet();
+ l.setSecret('Kxr9tQED9H44gCmp6HAdmemAzU3n84H3dGkuWTKvE23JgHMW8gct');
+ if (l.getAddress() !== '34AgLJhwXrvmkZS1o5TrcdeevMt22Nar53') {
+ throw new Error('failed to generate segwit P2SH address from WIF');
+ }
+
+ //
+
+ const wallet = new SegwitP2SHWallet();
+ wallet.setSecret('Ky1vhqYGCiCbPd8nmbUeGfwLdXB1h5aGwxHwpXrzYRfY5cTZPDo4');
+ assertStrictEqual(wallet.getAddress(), '3CKN8HTCews4rYJYsyub5hjAVm5g5VFdQJ');
+
+ utxos = [
+ {
+ txid: 'a56b44080cb606c0bd90e77fcd4fb34c863e68e5562e75b4386e611390eb860c',
+ vout: 0,
+ value: 300000,
+ },
+ ];
+
+ txNew = wallet.createTransaction(utxos, [{ value: 90000, address: '1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB' }], 1, wallet.getAddress());
+ const tx = bitcoin.Transaction.fromHex(txNew.tx.toHex());
+ assertStrictEqual(
+ txNew.tx.toHex(),
+ '020000000001010c86eb9013616e38b4752e56e5683e864cb34fcd7fe790bdc006b60c08446ba50000000017160014139dc70d73097f9d775f8a3280ba3e3435515641ffffffff02905f0100000000001976a914aa381cd428a4e91327fd4434aa0a08ff131f1a5a88aca73303000000000017a914749118baa93fb4b88c28909c8bf0a8202a0484f4870248304502210080545d30e3d30dff272ab11c91fd6150170b603239b48c3d56a3fa66bf240085022003762404e1b45975adc89f61ec1569fa19d6d4a8d405e060897754c489ebeade012103a5de146762f84055db3202c1316cd9008f16047f4f408c1482fdb108217eda0800000000',
+ );
+ assertStrictEqual(tx.ins.length, 1);
+ assertStrictEqual(tx.outs.length, 2);
+ assertStrictEqual('1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB', bitcoin.address.fromOutputScript(tx.outs[0].script)); // to address
+ assertStrictEqual(bitcoin.address.fromOutputScript(tx.outs[1].script), wallet.getAddress()); // change address
+
+ //
+
+ const data2encrypt = 'really long data string';
+ const crypted = encryption.encrypt(data2encrypt, 'password');
+ const decrypted = encryption.decrypt(crypted, 'password');
+
+ if (decrypted !== data2encrypt) {
+ throw new Error('encryption lib is not ok');
+ }
+
+ //
+ const mnemonic =
+ 'honey risk juice trip orient galaxy win situate shoot anchor bounce remind horse traffic exotic since escape mimic ramp skin judge owner topple erode';
+ const seed = bip39.mnemonicToSeedSync(mnemonic);
+ const root = bip32.fromSeed(seed);
+
+ const path = "m/49'/0'/0'/0/0";
+ const child = root.derivePath(path);
+ const address = bitcoin.payments.p2sh({
+ redeem: bitcoin.payments.p2wpkh({
+ pubkey: child.publicKey,
+ network: bitcoin.networks.bitcoin,
+ }),
+ network: bitcoin.networks.bitcoin,
+ }).address;
+
+ if (address !== '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK') {
+ throw new Error('bip49 is not ok');
+ }
+
+ //
+ if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
+ const hd = new HDSegwitP2SHWallet();
+ const hashmap = {};
+ for (let c = 0; c < 1000; c++) {
+ await hd.generate();
+ const secret = hd.getSecret();
+ if (hashmap[secret]) {
+ throw new Error('Duplicate secret generated!');
+ }
+ hashmap[secret] = 1;
+ if (secret.split(' ').length !== 12 && secret.split(' ').length !== 24) {
+ throw new Error('mnemonic phrase not ok');
+ }
+ }
+
+ const hd2 = new HDSegwitP2SHWallet();
+ hd2.setSecret(hd.getSecret());
+ if (!hd2.validateMnemonic()) {
+ throw new Error('mnemonic phrase validation not ok');
+ }
+
+ //
+
+ const hd4 = new HDSegwitBech32Wallet();
+ hd4._xpub = 'zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP';
+ await hd4.fetchBalance();
+ if (hd4.getBalance() !== 200000) throw new Error('Could not fetch HD Bech32 balance');
+ await hd4.fetchTransactions();
+ if (hd4.getTransactions().length !== 4) throw new Error('Could not fetch HD Bech32 transactions');
+ } else {
+ // skipping RN-specific test
+ }
+
+ // BlueCrypto test
+ if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
+ const hex = await BlueCrypto.scrypt('717765727479', '4749345a22b23cf3', 64, 8, 8, 32); // using non-default parameters to speed it up (not-bip38 compliant)
+ if (hex.toUpperCase() !== 'F36AB2DC12377C788D61E6770126D8A01028C8F6D8FE01871CE0489A1F696A90')
+ throw new Error('react-native-blue-crypto is not ok');
+ }
+
+ // bip38 test
+ if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
+ let callbackWasCalled = false;
+ const decryptedKey = await bip38.decryptAsync(
+ '6PnU5voARjBBykwSddwCdcn6Eu9EcsK24Gs5zWxbJbPZYW7eiYQP8XgKbN',
+ 'qwerty',
+ () => (callbackWasCalled = true),
+ );
+ assertStrictEqual(
+ wif.encode(0x80, decryptedKey.privateKey, decryptedKey.compressed),
+ 'KxqRtpd9vFju297ACPKHrGkgXuberTveZPXbRDiQ3MXZycSQYtjc',
+ 'bip38 failed',
+ );
+ // bip38 with BlueCrypto doesn't support progress callback
+ assertStrictEqual(callbackWasCalled, false, "bip38 doesn't use BlueCrypto");
+ }
+
+ // slip39 test
+ if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
+ const w = new SLIP39LegacyP2PKHWallet();
+ w.setSecret(
+ 'shadow pistol academic always adequate wildlife fancy gross oasis cylinder mustang wrist rescue view short owner flip making coding armed\n' +
+ 'shadow pistol academic acid actress prayer class unknown daughter sweater depict flip twice unkind craft early superior advocate guest smoking',
+ );
+ assertStrictEqual(w._getExternalAddressByIndex(0), '18pvMjy7AJbCDtv4TLYbGPbR7SzGzjqUpj', 'SLIP39 failed');
+ }
+
+ //
+
+ if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
+ assertStrictEqual(await Linking.canOpenURL('https://github.com/BlueWallet/BlueWallet/'), true, 'Linking can not open https url');
+ } else {
+ // skipping RN-specific test'
+ }
+
+ //
+
+ assertStrictEqual(Buffer.from('00ff0f', 'hex').reverse().toString('hex'), '0fff00');
+
+ //
+ } catch (Err) {
+ console.log(Err);
+ errorMessage += Err;
+ isOk = false;
+ }
+
+ this.setState({
+ isLoading: false,
+ isOk,
+ errorMessage,
+ });
+ }
+
+ render() {
+ if (this.state.isLoading) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+ {(() => {
+ if (this.state.isOk) {
+ return (
+
+
+ OK
+
+
+ {loc.settings.about_selftest_ok}
+
+ );
+ } else {
+ return (
+
+
+ {this.state.errorMessage}
+
+
+ );
+ }
+ })()}
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+function assertStrictEqual(actual, expected, message) {
+ if (expected !== actual) {
+ if (message) throw new Error(message);
+ throw new Error('Assertion failed that ' + JSON.stringify(expected) + ' equals ' + JSON.stringify(actual));
+ }
+}
+
+SelfTest.propTypes = {
+ navigation: PropTypes.shape({
+ navigate: PropTypes.func,
+ goBack: PropTypes.func,
+ }),
+};
diff --git a/screen/settings/about.js b/screen/settings/about.js
index fdb17337d..9e6bedd13 100644
--- a/screen/settings/about.js
+++ b/screen/settings/about.js
@@ -86,7 +86,7 @@ const About = () => {
if (isElectrumDisabled) {
presentAlert({ message: loc.settings.about_selftest_electrum_disabled });
} else {
- navigate('Selftest');
+ navigate('SelfTest');
}
};
diff --git a/tests/integration/App.test.js b/tests/integration/App.test.js
index 0f370b42a..601a4e47f 100644
--- a/tests/integration/App.test.js
+++ b/tests/integration/App.test.js
@@ -3,7 +3,7 @@ import React from 'react';
import TestRenderer from 'react-test-renderer';
import { Header } from '../../components/Header';
-import Selftest from '../../screen/selftest';
+import SelfTest from '../../screen/selftest';
import Settings from '../../screen/settings/Settings';
jest.mock('../../blue_modules/BlueElectrum', () => {
@@ -23,8 +23,8 @@ it.skip('Settings work', () => {
expect(rendered).toBeTruthy();
});
-it('Selftest work', () => {
- const component = TestRenderer.create();
+it('SelfTest work', () => {
+ const component = TestRenderer.create();
const root = component.root;
const rendered = component.toJSON();
expect(rendered).toBeTruthy();