Merge branch 'master' of https://github.com/BlueWallet/BlueWallet into pr/450

This commit is contained in:
ncoelho 2019-06-05 18:25:24 +02:00
commit b3432b607f
259 changed files with 22750 additions and 3292 deletions

View file

@ -67,4 +67,4 @@ suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
[version]
^0.86.0
^0.97.0

4
.gitignore vendored
View file

@ -57,4 +57,6 @@ buck-out/
#BlueWallet
release-notes.json
release-notes.txt
release-notes.txt
ios/Pods/

101
App.js
View file

@ -1,11 +1,14 @@
import React from 'react';
import { Linking, AppState, Clipboard, StyleSheet, KeyboardAvoidingView, Platform, View } from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';
import Modal from 'react-native-modal';
import { NavigationActions } from 'react-navigation';
import MainBottomTabs from './MainBottomTabs';
import NavigationService from './NavigationService';
import { BlueTextCentered, BlueButton } from './BlueComponents';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import url from 'url';
import { AppStorage, LightningCustodianWallet } from './class';
const bitcoin = require('bitcoinjs-lib');
const bitcoinModalString = 'Bitcoin address';
const lightningModalString = 'Lightning Invoice';
@ -56,7 +59,13 @@ export default class App extends React.Component {
hasSchema(schemaString) {
if (typeof schemaString !== 'string' || schemaString.length <= 0) return false;
const lowercaseString = schemaString.trim().toLowerCase();
return lowercaseString.startsWith('bitcoin:') || lowercaseString.startsWith('lightning:');
return (
lowercaseString.startsWith('bitcoin:') ||
lowercaseString.startsWith('lightning:') ||
lowercaseString.startsWith('blue:') ||
lowercaseString.startsWith('bluewallet:') ||
lowercaseString.startsWith('lapp:')
);
}
isBitcoinAddress(address) {
@ -86,6 +95,12 @@ export default class App extends React.Component {
return isValidLightningInvoice;
}
isSafelloRedirect(event) {
let urlObject = url.parse(event.url, true) // eslint-disable-line
return !!urlObject.query['safello-state-token'];
}
handleOpenURL = event => {
if (event.url === null) {
return;
@ -113,13 +128,95 @@ export default class App extends React.Component {
},
}),
);
} else if (this.isSafelloRedirect(event)) {
let urlObject = url.parse(event.url, true) // eslint-disable-line
const safelloStateToken = urlObject.query['safello-state-token'];
this.navigator &&
this.navigator.dispatch(
NavigationActions.navigate({
routeName: 'BuyBitcoin',
params: {
uri: event.url,
safelloStateToken,
},
}),
);
} else {
let urlObject = url.parse(event.url, true); // eslint-disable-line
console.log('parsed', urlObject);
(async () => {
if (urlObject.protocol === 'bluewallet:' || urlObject.protocol === 'lapp:' || urlObject.protocol === 'blue:') {
switch (urlObject.host) {
case 'openlappbrowser':
console.log('opening LAPP', urlObject.query.url);
// searching for LN wallet:
let haveLnWallet = false;
for (let w of BlueApp.getWallets()) {
if (w.type === LightningCustodianWallet.type) {
haveLnWallet = true;
}
}
if (!haveLnWallet) {
// need to create one
let w = new LightningCustodianWallet();
w.setLabel(this.state.label || w.typeReadable);
try {
let lndhub = await AsyncStorage.getItem(AppStorage.LNDHUB);
if (lndhub) {
w.setBaseURI(lndhub);
w.init();
}
await w.createAccount();
await w.authorize();
} catch (Err) {
// giving up, not doing anything
return;
}
BlueApp.wallets.push(w);
await BlueApp.saveToDisk();
}
// now, opening lapp browser and navigating it to URL.
// looking for a LN wallet:
let lnWallet;
for (let w of BlueApp.getWallets()) {
if (w.type === LightningCustodianWallet.type) {
lnWallet = w;
break;
}
}
if (!lnWallet) {
// something went wrong
return;
}
this.navigator &&
this.navigator.dispatch(
NavigationActions.navigate({
routeName: 'LappBrowser',
params: {
fromSecret: lnWallet.getSecret(),
fromWallet: lnWallet,
url: urlObject.query.url,
},
}),
);
break;
}
}
})();
}
};
renderClipboardContentModal = () => {
return (
<Modal
onModalShow={() => ReactNativeHapticFeedback.trigger('impactLight', false)}
onModalShow={() => ReactNativeHapticFeedback.trigger('impactLight', { ignoreAndroidSystemSettings: false })}
isVisible={this.state.isClipboardContentModalVisible}
style={styles.bottomModal}
onBackdropPress={() => {

View file

@ -5,13 +5,11 @@ import TestRenderer from 'react-test-renderer';
import Settings from './screen/settings/settings';
import Selftest from './screen/selftest';
import { BlueHeader } from './BlueComponents';
import MockStorage from './MockStorage';
import { FiatUnit } from './models/fiatUnit';
import AsyncStorage from '@react-native-community/async-storage';
global.crypto = require('crypto'); // shall be used by tests under nodejs CLI, but not in RN environment
let assert = require('assert');
jest.mock('react-native-qrcode-svg', () => 'Video');
const AsyncStorage = new MockStorage();
jest.setMock('AsyncStorage', AsyncStorage);
jest.useFakeTimers();
jest.mock('Picker', () => {
// eslint-disable-next-line import/no-unresolved
@ -105,7 +103,6 @@ it('Selftest work', () => {
});
it('Appstorage - loadFromDisk works', async () => {
AsyncStorage.storageCache = {}; // cleanup from other tests
/** @type {AppStorage} */
let Storage = new AppStorage();
let w = new SegwitP2SHWallet();
@ -125,16 +122,14 @@ it('Appstorage - loadFromDisk works', async () => {
// emulating encrypted storage (and testing flag)
AsyncStorage.storageCache.data = false;
AsyncStorage.storageCache.data_encrypted = '1'; // flag
await AsyncStorage.setItem('data', false);
await AsyncStorage.setItem(AppStorage.FLAG_ENCRYPTED, '1');
let Storage3 = new AppStorage();
isEncrypted = await Storage3.storageIsEncrypted();
assert.ok(isEncrypted);
});
it('Appstorage - encryptStorage & load encrypted storage works', async () => {
AsyncStorage.storageCache = {}; // cleanup from other tests
/** @type {AppStorage} */
let Storage = new AppStorage();
let w = new SegwitP2SHWallet();
@ -228,6 +223,12 @@ it('Wallet can fetch UTXO', async () => {
assert.ok(w.utxo.length > 0, 'unexpected empty UTXO');
});
it('SegwitP2SHWallet can generate segwit P2SH address from WIF', () => {
let l = new SegwitP2SHWallet();
l.setSecret('Kxr9tQED9H44gCmp6HAdmemAzU3n84H3dGkuWTKvE23JgHMW8gct');
assert.ok(l.getAddress() === '34AgLJhwXrvmkZS1o5TrcdeevMt22Nar53', 'expected ' + l.getAddress());
});
it('Wallet can fetch balance', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000;
let w = new LegacyWallet();
@ -236,7 +237,7 @@ it('Wallet can fetch balance', async () => {
assert.ok(w.getUnconfirmedBalance() === 0);
assert.ok(w._lastBalanceFetch === 0);
await w.fetchBalance();
assert.ok(w.getBalance() === 0.18262);
assert.ok(w.getBalance() === 18262000);
assert.ok(w.getUnconfirmedBalance() === 0);
assert.ok(w._lastBalanceFetch > 0);
});
@ -302,19 +303,18 @@ it('Wallet can fetch TXs', async () => {
describe('currency', () => {
it('fetches exchange rate and saves to AsyncStorage', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000;
AsyncStorage.storageCache = {}; // cleanup from other tests
let currency = require('./currency');
await currency.startUpdater();
let cur = AsyncStorage.storageCache[AppStorage.EXCHANGE_RATES];
let cur = await AsyncStorage.getItem(AppStorage.EXCHANGE_RATES);
cur = JSON.parse(cur);
assert.ok(Number.isInteger(cur[currency.STRUCT.LAST_UPDATED]));
assert.ok(cur[currency.STRUCT.LAST_UPDATED] > 0);
assert.ok(cur['BTC_USD'] > 0);
// now, setting other currency as default
AsyncStorage.storageCache[AppStorage.PREFERRED_CURRENCY] = JSON.stringify(FiatUnit.JPY);
await AsyncStorage.setItem(AppStorage.PREFERRED_CURRENCY, JSON.stringify(FiatUnit.JPY));
await currency.startUpdater();
cur = JSON.parse(AsyncStorage.storageCache[AppStorage.EXCHANGE_RATES]);
cur = JSON.parse(await AsyncStorage.getItem(AppStorage.EXCHANGE_RATES));
assert.ok(cur['BTC_JPY'] > 0);
// now setting with a proper setter
@ -322,7 +322,7 @@ describe('currency', () => {
await currency.startUpdater();
let preferred = await currency.getPreferredCurrency();
assert.strictEqual(preferred.endPointKey, 'EUR');
cur = JSON.parse(AsyncStorage.storageCache[AppStorage.EXCHANGE_RATES]);
cur = JSON.parse(await AsyncStorage.getItem(AppStorage.EXCHANGE_RATES));
assert.ok(cur['BTC_EUR'] > 0);
});
});

View file

@ -1,5 +1,4 @@
/* global it, describe, jasmine */
import { WatchOnlyWallet } from './class';
/* global it, jasmine */
let assert = require('assert');
it('bip38 decodes', async () => {
@ -37,50 +36,3 @@ it('bip38 decodes slow', async () => {
'KxqRtpd9vFju297ACPKHrGkgXuberTveZPXbRDiQ3MXZycSQYtjc',
);
});
describe('Watch only wallet', () => {
it('can fetch balance', async () => {
let w = new WatchOnlyWallet();
w.setSecret('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa');
await w.fetchBalance();
assert.ok(w.getBalance() > 16);
});
it('can fetch tx', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 150 * 1000;
let w = new WatchOnlyWallet();
w.setSecret('167zK5iZrs1U6piDqubD3FjRqUTM2CZnb8');
await w.fetchTransactions();
assert.strictEqual(w.getTransactions().length, 233);
w = new WatchOnlyWallet();
w.setSecret('1BiJW1jyUaxcJp2JWwbPLPzB1toPNWTFJV');
await w.fetchTransactions();
assert.strictEqual(w.getTransactions().length, 2);
// fetch again and make sure no duplicates
await w.fetchTransactions();
assert.strictEqual(w.getTransactions().length, 2);
});
it('can fetch complex TXs', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 120 * 1000;
let w = new WatchOnlyWallet();
w.setSecret('3NLnALo49CFEF4tCRhCvz45ySSfz3UktZC');
await w.fetchTransactions();
for (let tx of w.getTransactions()) {
assert.ok(tx.value, 'incorrect tx.value');
}
});
it('can validate address', async () => {
let w = new WatchOnlyWallet();
w.setSecret('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG');
assert.ok(w.valid());
w.setSecret('3BDsBDxDimYgNZzsqszNZobqQq3yeUoJf2');
assert.ok(w.valid());
w.setSecret('not valid');
assert.ok(!w.valid());
});
});

View file

@ -10,7 +10,7 @@ let A = require('./analytics');
let BlueElectrum = require('./BlueElectrum'); // eslint-disable-line
/** @type {AppStorage} */
let BlueApp = new AppStorage();
const BlueApp = new AppStorage();
async function startAndDecrypt(retry) {
console.log('startAndDecrypt');

View file

@ -25,7 +25,6 @@ import {
import LinearGradient from 'react-native-linear-gradient';
import { LightningCustodianWallet } from './class';
import Carousel from 'react-native-snap-carousel';
import DeviceInfo from 'react-native-device-info';
import { BitcoinUnit } from './models/bitcoinUnits';
import NavigationService from './NavigationService';
import ImagePicker from 'react-native-image-picker';
@ -36,6 +35,7 @@ let loc = require('./loc/');
let BlueApp = require('./BlueApp');
const { height, width } = Dimensions.get('window');
const aspectRatio = height / width;
const BigNumber = require('bignumber.js');
let isIpad;
if (aspectRatio > 1.6) {
isIpad = false;
@ -93,32 +93,26 @@ export class BitcoinButton extends Component {
<View
style={{
// eslint-disable-next-line
borderColor: (this.props.active && BlueApp.settings.foregroundColor) || BlueApp.settings.inputBorderColor,
borderWidth: 0.5,
borderColor: BlueApp.settings.hdborderColor,
borderWidth: 1,
borderRadius: 5,
backgroundColor: BlueApp.settings.inputBackgroundColor,
backgroundColor: (this.props.active && BlueApp.settings.hdbackgroundColor) || BlueApp.settings.brandingColor,
// eslint-disable-next-line
width: this.props.style.width,
minWidth: this.props.style.width,
// eslint-disable-next-line
minHeight: this.props.style.height,
height: this.props.style.height,
flex: 1,
}}
>
<View style={{ paddingTop: 30 }}>
<Icon
name="btc"
size={32}
type="font-awesome"
color={(this.props.active && BlueApp.settings.foregroundColor) || BlueApp.settings.inputBorderColor}
/>
<Text
style={{
textAlign: 'center',
color: (this.props.active && BlueApp.settings.foregroundColor) || BlueApp.settings.inputBorderColor,
}}
>
{loc.wallets.add.bitcoin}
</Text>
<View style={{ marginTop: 16, marginLeft: 16, marginBottom: 16 }}>
<Text style={{ color: BlueApp.settings.hdborderColor, fontWeight: 'bold' }}>{loc.wallets.add.bitcoin}</Text>
</View>
<Image
style={{ width: 34, height: 34, marginRight: 8, marginBottom: 8, justifyContent: 'flex-end', alignSelf: 'flex-end' }}
source={require('./img/addWallet/bitcoin.png')}
/>
</View>
</TouchableOpacity>
);
@ -137,32 +131,26 @@ export class LightningButton extends Component {
<View
style={{
// eslint-disable-next-line
borderColor: (this.props.active && BlueApp.settings.foregroundColor) || BlueApp.settings.inputBorderColor,
borderWidth: 0.5,
borderColor: BlueApp.settings.lnborderColor,
borderWidth: 1,
borderRadius: 5,
backgroundColor: BlueApp.settings.inputBackgroundColor,
backgroundColor: (this.props.active && BlueApp.settings.lnbackgroundColor) || BlueApp.settings.brandingColor,
// eslint-disable-next-line
width: this.props.style.width,
minWidth: this.props.style.width,
// eslint-disable-next-line
minHeight: this.props.style.height,
height: this.props.style.height,
flex: 1,
}}
>
<View style={{ paddingTop: 30 }}>
<Icon
name="bolt"
size={32}
type="font-awesome"
color={(this.props.active && BlueApp.settings.foregroundColor) || BlueApp.settings.inputBorderColor}
/>
<Text
style={{
textAlign: 'center',
color: (this.props.active && BlueApp.settings.foregroundColor) || BlueApp.settings.inputBorderColor,
}}
>
{loc.wallets.add.lightning}
</Text>
<View style={{ marginTop: 16, marginLeft: 16, marginBottom: 16 }}>
<Text style={{ color: BlueApp.settings.lnborderColor, fontWeight: 'bold' }}>{loc.wallets.add.lightning}</Text>
</View>
<Image
style={{ width: 34, height: 34, marginRight: 8, marginBottom: 8, justifyContent: 'flex-end', alignSelf: 'flex-end' }}
source={require('./img/addWallet/lightning.png')}
/>
</View>
</TouchableOpacity>
);
@ -241,6 +229,14 @@ export class BlueCopyTextToClipboard extends Component {
this.state = { hasTappedText: false, address: props.text };
}
static getDerivedStateFromProps(props, state) {
if (state.hasTappedText) {
return { hasTappedText: state.hasTappedText, address: state.address };
} else {
return { hasTappedText: state.hasTappedText, address: props.text };
}
}
copyToClipboard = () => {
this.setState({ hasTappedText: true }, () => {
Clipboard.setString(this.props.text);
@ -404,29 +400,6 @@ export class BlueFormMultiInput extends Component {
}
}
export class BlueFormInputAddress extends Component {
render() {
return (
<FormInput
{...this.props}
inputStyle={{
maxWidth: width - 110,
color: BlueApp.settings.foregroundColor,
fontSize: (isIpad && 10) || ((is.iphone8() && 12) || 14),
}}
containerStyle={{
marginTop: 5,
borderColor: BlueApp.settings.inputBorderColor,
borderBottomColor: BlueApp.settings.inputBorderColor,
borderWidth: 0.5,
borderBottomWidth: 0.5,
backgroundColor: BlueApp.settings.inputBackgroundColor,
}}
/>
);
}
}
export class BlueHeader extends Component {
render() {
return (
@ -560,13 +533,6 @@ export class is {
static ipad() {
return isIpad;
}
static iphone8() {
if (Platform.OS !== 'ios') {
return false;
}
return DeviceInfo.getDeviceId() === 'iPhone10,4';
}
}
export class BlueSpacing20 extends Component {
@ -575,6 +541,12 @@ export class BlueSpacing20 extends Component {
}
}
export class BlueSpacing10 extends Component {
render() {
return <View {...this.props} style={{ height: 10, opacity: 0 }} />;
}
}
export class BlueList extends Component {
render() {
return (
@ -1733,7 +1705,7 @@ export class BlueAddressInput extends Component {
export class BlueBitcoinAmount extends Component {
static propTypes = {
isLoading: PropTypes.bool,
amount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
amount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
onChangeText: PropTypes.func,
disabled: PropTypes.bool,
unit: PropTypes.string,
@ -1744,8 +1716,15 @@ export class BlueBitcoinAmount extends Component {
};
render() {
const amount = typeof this.props.amount === 'number' ? this.props.amount.toString() : this.props.amount;
const amount = this.props.amount || 0;
let localCurrency = loc.formatBalanceWithoutSuffix(amount, BitcoinUnit.LOCAL_CURRENCY, false);
if (this.props.unit === BitcoinUnit.BTC) {
let sat = new BigNumber(amount);
sat = sat.multipliedBy(100000000).toString();
localCurrency = loc.formatBalanceWithoutSuffix(sat, BitcoinUnit.LOCAL_CURRENCY, false);
} else {
localCurrency = loc.formatBalanceWithoutSuffix(amount.toString(), BitcoinUnit.LOCAL_CURRENCY, false);
}
return (
<TouchableWithoutFeedback disabled={this.props.pointerEvents === 'none'} onPress={() => this.textInput.focus()}>
<View>
@ -1788,13 +1767,7 @@ export class BlueBitcoinAmount extends Component {
</Text>
</View>
<View style={{ alignItems: 'center', marginBottom: 22, marginTop: 4 }}>
<Text style={{ fontSize: 18, color: '#d4d4d4', fontWeight: '600' }}>
{loc.formatBalance(
this.props.unit === BitcoinUnit.BTC ? amount || 0 : loc.formatBalanceWithoutSuffix(amount || 0, BitcoinUnit.BTC, false),
BitcoinUnit.LOCAL_CURRENCY,
false,
)}
</Text>
<Text style={{ fontSize: 18, color: '#d4d4d4', fontWeight: '600' }}>{localCurrency}</Text>
</View>
</View>
</TouchableWithoutFeedback>

View file

@ -1,20 +1,25 @@
import { AsyncStorage } from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';
import { SegwitBech32Wallet } from './class';
const ElectrumClient = require('electrum-client');
let bitcoin = require('bitcoinjs-lib');
let reverse = require('buffer-reverse');
const storageKey = 'ELECTRUM_PEERS';
const defaultPeer = { host: 'electrum.coinucopia.io', tcp: 50001 };
const defaultPeer = { host: 'electrum1.bluewallet.io', tcp: '50001' };
const hardcodedPeers = [
{ host: 'noveltybobble.coinjoined.com', tcp: '50001' },
{ host: 'electrum.be', tcp: '50001' },
// { host: 'noveltybobble.coinjoined.com', tcp: '50001' }, // down
// { host: 'electrum.be', tcp: '50001' },
// { host: 'node.ispol.sk', tcp: '50001' }, // down
{ host: '139.162.14.142', tcp: '50001' },
// { host: '139.162.14.142', tcp: '50001' },
// { host: 'electrum.coinucopia.io', tcp: '50001' }, // SLOW
{ host: 'Bitkoins.nl', tcp: '50001' },
{ host: 'fullnode.coinkite.com', tcp: '50001' },
// { host: 'Bitkoins.nl', tcp: '50001' }, // down
// { host: 'fullnode.coinkite.com', tcp: '50001' },
// { host: 'preperfect.eleCTruMioUS.com', tcp: '50001' }, // down
{ host: 'electrum1.bluewallet.io', tcp: '50001' },
{ host: 'electrum1.bluewallet.io', tcp: '50001' }, // 2x weight
{ host: 'electrum2.bluewallet.io', tcp: '50001' },
{ host: 'electrum3.bluewallet.io', tcp: '50001' },
{ host: 'electrum3.bluewallet.io', tcp: '50001' }, // 2x weight
];
let mainClient = false;
@ -26,7 +31,7 @@ async function connectMain() {
console.log('begin connection:', JSON.stringify(usingPeer));
mainClient = new ElectrumClient(usingPeer.tcp, usingPeer.host, 'tcp');
await mainClient.connect();
const ver = await mainClient.server_version('2.7.11', '1.2');
const ver = await mainClient.server_version('2.7.11', '1.4');
let peers = await mainClient.serverPeers_subscribe();
if (peers && peers.length > 0) {
console.log('connected to ', ver);
@ -35,7 +40,7 @@ async function connectMain() {
}
} catch (e) {
mainConnected = false;
console.log('bad connection:', JSON.stringify(usingPeer));
console.log('bad connection:', JSON.stringify(usingPeer), e);
}
if (!mainConnected) {
@ -43,7 +48,7 @@ async function connectMain() {
mainClient.keepAlive = () => {}; // dirty hack to make it stop reconnecting
mainClient.reconnect = () => {}; // dirty hack to make it stop reconnecting
mainClient.close();
setTimeout(connectMain, 5000);
setTimeout(connectMain, 500);
}
}
@ -118,23 +123,106 @@ async function getTransactionsByAddress(address) {
return history;
}
async function getTransactionsFullByAddress(address) {
let txs = await this.getTransactionsByAddress(address);
let ret = [];
for (let tx of txs) {
let full = await mainClient.blockchainTransaction_get(tx.tx_hash, true);
full.address = address;
for (let input of full.vin) {
input.address = SegwitBech32Wallet.witnessToAddress(input.txinwitness[1]);
input.addresses = [input.address];
// now we need to fetch previous TX where this VIN became an output, so we can see its amount
let prevTxForVin = await mainClient.blockchainTransaction_get(input.txid, true);
if (prevTxForVin && prevTxForVin.vout && prevTxForVin.vout[input.vout]) {
input.value = prevTxForVin.vout[input.vout].value;
}
}
for (let output of full.vout) {
if (output.scriptPubKey && output.scriptPubKey.addresses) output.addresses = output.scriptPubKey.addresses;
}
full.inputs = full.vin;
full.outputs = full.vout;
delete full.vin;
delete full.vout;
delete full.hex; // compact
delete full.hash; // compact
ret.push(full);
}
return ret;
}
/**
*
* @param addresses {Array}
* @returns {Promise<{balance: number, unconfirmed_balance: number}>}
* @param batchsize {Number}
* @returns {Promise<{balance: number, unconfirmed_balance: number, addresses: object}>}
*/
async function multiGetBalanceByAddress(addresses) {
async function multiGetBalanceByAddress(addresses, batchsize) {
batchsize = batchsize || 100;
if (!mainClient) throw new Error('Electrum client is not connected');
let balance = 0;
let unconfirmedBalance = 0;
for (let addr of addresses) {
let b = await getBalanceByAddress(addr);
let ret = { balance: 0, unconfirmed_balance: 0, addresses: {} };
balance += b.confirmed;
unconfirmedBalance += b.unconfirmed_balance;
let chunks = splitIntoChunks(addresses, batchsize);
for (let chunk of chunks) {
let scripthashes = [];
let scripthash2addr = {};
for (let addr of chunk) {
let script = bitcoin.address.toOutputScript(addr);
let hash = bitcoin.crypto.sha256(script);
let reversedHash = Buffer.from(reverse(hash));
reversedHash = reversedHash.toString('hex');
scripthashes.push(reversedHash);
scripthash2addr[reversedHash] = addr;
}
let balances = await mainClient.blockchainScripthash_getBalanceBatch(scripthashes);
for (let bal of balances) {
ret.balance += +bal.result.confirmed;
ret.unconfirmed_balance += +bal.result.unconfirmed;
ret.addresses[scripthash2addr[bal.param]] = bal.result;
}
}
return { balance, unconfirmed_balance: unconfirmedBalance };
return ret;
}
async function multiGetUtxoByAddress(addresses, batchsize) {
batchsize = batchsize || 100;
if (!mainClient) throw new Error('Electrum client is not connected');
let ret = {};
let chunks = splitIntoChunks(addresses, batchsize);
for (let chunk of chunks) {
let scripthashes = [];
let scripthash2addr = {};
for (let addr of chunk) {
let script = bitcoin.address.toOutputScript(addr);
let hash = bitcoin.crypto.sha256(script);
let reversedHash = Buffer.from(reverse(hash));
reversedHash = reversedHash.toString('hex');
scripthashes.push(reversedHash);
scripthash2addr[reversedHash] = addr;
}
let results = await mainClient.blockchainScripthash_listunspentBatch(scripthashes);
for (let utxos of results) {
ret[scripthash2addr[utxos.param]] = utxos.result;
for (let utxo of ret[scripthash2addr[utxos.param]]) {
utxo.address = scripthash2addr[utxos.param];
utxo.txId = utxo.tx_hash;
utxo.vout = utxo.tx_pos;
delete utxo.tx_pos;
delete utxo.tx_hash;
}
}
}
return ret;
}
/**
@ -164,8 +252,8 @@ async function waitTillConnected() {
async function estimateFees() {
if (!mainClient) throw new Error('Electrum client is not connected');
const fast = await mainClient.blockchainEstimatefee(1);
const medium = await mainClient.blockchainEstimatefee(6);
const slow = await mainClient.blockchainEstimatefee(12);
const medium = await mainClient.blockchainEstimatefee(5);
const slow = await mainClient.blockchainEstimatefee(10);
return { fast, medium, slow };
}
@ -182,9 +270,11 @@ async function broadcast(hex) {
module.exports.getBalanceByAddress = getBalanceByAddress;
module.exports.getTransactionsByAddress = getTransactionsByAddress;
module.exports.multiGetBalanceByAddress = multiGetBalanceByAddress;
module.exports.getTransactionsFullByAddress = getTransactionsFullByAddress;
module.exports.waitTillConnected = waitTillConnected;
module.exports.estimateFees = estimateFees;
module.exports.broadcast = broadcast;
module.exports.multiGetUtxoByAddress = multiGetUtxoByAddress;
module.exports.forceDisconnect = () => {
mainClient.keepAlive = () => {}; // dirty hack to make it stop reconnecting
@ -194,6 +284,15 @@ module.exports.forceDisconnect = () => {
module.exports.hardcodedPeers = hardcodedPeers;
let splitIntoChunks = function(arr, chunkSize) {
let groups = [];
let i;
for (i = 0; i < arr.length; i += chunkSize) {
groups.push(arr.slice(i, i + chunkSize));
}
return groups;
};
/*

View file

@ -2,11 +2,13 @@
global.net = require('net');
let BlueElectrum = require('./BlueElectrum');
let assert = require('assert');
let bitcoin = require('bitcoinjs-lib');
jasmine.DEFAULT_TIMEOUT_INTERVAL = 150 * 1000;
afterAll(() => {
// after all tests we close socket so the test suite can actually terminate
return BlueElectrum.forceDisconnect();
BlueElectrum.forceDisconnect();
return new Promise(resolve => setTimeout(resolve, 10000)); // simple sleep to wait for all timeouts termination
});
beforeAll(async () => {
@ -14,8 +16,8 @@ beforeAll(async () => {
// while app starts up, but for tests we need to wait for it
try {
await BlueElectrum.waitTillConnected();
} catch (Err) {
console.log('failed to connect to Electrum:', Err);
} catch (err) {
console.log('failed to connect to Electrum:', err);
process.exit(1);
}
});
@ -23,18 +25,17 @@ beforeAll(async () => {
describe('Electrum', () => {
it('ElectrumClient can connect and query', async () => {
const ElectrumClient = require('electrum-client');
let bitcoin = require('bitcoinjs-lib');
for (let peer of BlueElectrum.hardcodedPeers) {
let mainClient = new ElectrumClient(peer.tcp, peer.host, 'tcp');
try {
await mainClient.connect();
await mainClient.server_version('2.7.11', '1.2');
await mainClient.server_version('2.7.11', '1.4');
} catch (e) {
mainClient.reconnect = mainClient.keepAlive = () => {}; // dirty hack to make it stop reconnecting
mainClient.close();
throw new Error('bad connection: ' + JSON.stringify(peer));
throw new Error('bad connection: ' + JSON.stringify(peer) + ' ' + e.message);
}
let addr4elect = 'bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej';
@ -52,7 +53,6 @@ describe('Electrum', () => {
hash = bitcoin.crypto.sha256(script);
reversedHash = Buffer.from(hash.reverse());
balance = await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex'));
assert.ok(balance.confirmed === 51432);
// let peers = await mainClient.serverPeers_subscribe();
// console.log(peers);
@ -61,18 +61,77 @@ describe('Electrum', () => {
}
});
it('BlueElectrum works', async function() {
it('BlueElectrum can do getBalanceByAddress()', async function() {
let address = '3GCvDBAktgQQtsbN6x5DYiQCMmgZ9Yk8BK';
let balance = await BlueElectrum.getBalanceByAddress(address);
assert.strictEqual(balance.confirmed, 51432);
assert.strictEqual(balance.unconfirmed, 0);
assert.strictEqual(balance.addr, address);
});
let txs = await BlueElectrum.getTransactionsByAddress(address);
it('BlueElectrum can do getTransactionsByAddress()', async function() {
let txs = await BlueElectrum.getTransactionsByAddress('bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh');
assert.strictEqual(txs.length, 1);
assert.strictEqual(txs[0].tx_hash, 'ad00a92409d8982a1d7f877056dbed0c4337d2ebab70b30463e2802279fb936d');
assert.strictEqual(txs[0].height, 563077);
});
it('BlueElectrum can do getTransactionsFullByAddress()', async function() {
let txs = await BlueElectrum.getTransactionsFullByAddress('bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh');
for (let tx of txs) {
assert.ok(tx.tx_hash);
assert.ok(tx.height);
assert.ok(tx.address === 'bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh');
assert.ok(tx.txid);
assert.ok(tx.confirmations);
assert.ok(!tx.vin);
assert.ok(!tx.vout);
assert.ok(tx.inputs);
assert.ok(tx.inputs[0].addresses.length > 0);
assert.ok(tx.inputs[0].value > 0);
assert.ok(tx.outputs);
assert.ok(tx.outputs[0].value > 0);
assert.ok(tx.outputs[0].scriptPubKey);
assert.ok(tx.outputs[0].addresses.length > 0);
}
});
it('BlueElectrum can do multiGetBalanceByAddress()', async function() {
let balances = await BlueElectrum.multiGetBalanceByAddress([
'bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh',
'bc1qvd6w54sydc08z3802svkxr7297ez7cusd6266p',
'bc1qwp58x4c9e5cplsnw5096qzdkae036ug7a34x3r',
'bc1qcg6e26vtzja0h8up5w2m7utex0fsu4v0e0e7uy',
]);
assert.strictEqual(balances.balance, 200000);
assert.strictEqual(balances.unconfirmed_balance, 0);
assert.strictEqual(balances.addresses['bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh'].confirmed, 50000);
assert.strictEqual(balances.addresses['bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh'].unconfirmed, 0);
assert.strictEqual(balances.addresses['bc1qvd6w54sydc08z3802svkxr7297ez7cusd6266p'].confirmed, 50000);
assert.strictEqual(balances.addresses['bc1qvd6w54sydc08z3802svkxr7297ez7cusd6266p'].unconfirmed, 0);
assert.strictEqual(balances.addresses['bc1qwp58x4c9e5cplsnw5096qzdkae036ug7a34x3r'].confirmed, 50000);
assert.strictEqual(balances.addresses['bc1qwp58x4c9e5cplsnw5096qzdkae036ug7a34x3r'].unconfirmed, 0);
assert.strictEqual(balances.addresses['bc1qcg6e26vtzja0h8up5w2m7utex0fsu4v0e0e7uy'].confirmed, 50000);
assert.strictEqual(balances.addresses['bc1qcg6e26vtzja0h8up5w2m7utex0fsu4v0e0e7uy'].unconfirmed, 0);
});
it('BlueElectrum can do multiGetUtxoByAddress()', async () => {
let utxos = await BlueElectrum.multiGetUtxoByAddress(
[
'bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh',
'bc1qvd6w54sydc08z3802svkxr7297ez7cusd6266p',
'bc1qwp58x4c9e5cplsnw5096qzdkae036ug7a34x3r',
'bc1qcg6e26vtzja0h8up5w2m7utex0fsu4v0e0e7uy',
],
3,
);
assert.strictEqual(Object.keys(utxos).length, 4);
assert.strictEqual(
utxos['bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh'][0].txId,
'ad00a92409d8982a1d7f877056dbed0c4337d2ebab70b30463e2802279fb936d',
);
assert.strictEqual(utxos['bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh'][0].vout, 1);
assert.strictEqual(utxos['bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh'][0].value, 50000);
assert.strictEqual(utxos['bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh'][0].address, 'bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh');
});
});

265
HDBech32Wallet.test.js Normal file
View file

@ -0,0 +1,265 @@
/* global it, describe, jasmine, afterAll, beforeAll */
import { HDSegwitBech32Wallet } from './class';
global.crypto = require('crypto'); // shall be used by tests under nodejs CLI, but not in RN environment
let assert = require('assert');
global.net = require('net'); // needed by Electrum client. For RN it is proviced in shim.js
let BlueElectrum = require('./BlueElectrum'); // so it connects ASAP
afterAll(async () => {
// after all tests we close socket so the test suite can actually terminate
BlueElectrum.forceDisconnect();
return new Promise(resolve => setTimeout(resolve, 10000)); // simple sleep to wait for all timeouts termination
});
beforeAll(async () => {
// awaiting for Electrum to be connected. For RN Electrum would naturally connect
// while app starts up, but for tests we need to wait for it
await BlueElectrum.waitTillConnected();
});
describe('Bech32 Segwit HD (BIP84)', () => {
it('can create', async function() {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30 * 1000;
let mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
let hd = new HDSegwitBech32Wallet();
hd.setSecret(mnemonic);
assert.strictEqual(true, hd.validateMnemonic());
assert.strictEqual(
'zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs',
hd.getXpub(),
);
assert.strictEqual(hd._getExternalWIFByIndex(0), 'KyZpNDKnfs94vbrwhJneDi77V6jF64PWPF8x5cdJb8ifgg2DUc9d');
assert.strictEqual(hd._getExternalWIFByIndex(1), 'Kxpf5b8p3qX56DKEe5NqWbNUP9MnqoRFzZwHRtsFqhzuvUJsYZCy');
assert.strictEqual(hd._getInternalWIFByIndex(0), 'KxuoxufJL5csa1Wieb2kp29VNdn92Us8CoaUG3aGtPtcF3AzeXvF');
assert.ok(hd._getInternalWIFByIndex(0) !== hd._getInternalWIFByIndex(1));
assert.strictEqual(hd._getExternalAddressByIndex(0), 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu');
assert.strictEqual(hd._getExternalAddressByIndex(1), 'bc1qnjg0jd8228aq7egyzacy8cys3knf9xvrerkf9g');
assert.strictEqual(hd._getInternalAddressByIndex(0), 'bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el');
assert.ok(hd._getInternalAddressByIndex(0) !== hd._getInternalAddressByIndex(1));
assert.ok(hd._lastBalanceFetch === 0);
await hd.fetchBalance();
assert.strictEqual(hd.getBalance(), 0);
assert.ok(hd._lastBalanceFetch > 0);
// checking that internal pointer and async address getter return the same address
let freeAddress = await hd.getAddressAsync();
assert.strictEqual(hd.next_free_address_index, 0);
assert.strictEqual(hd._getExternalAddressByIndex(hd.next_free_address_index), freeAddress);
let freeChangeAddress = await hd.getChangeAddressAsync();
assert.strictEqual(hd.next_free_change_address_index, 0);
assert.strictEqual(hd._getInternalAddressByIndex(hd.next_free_change_address_index), freeChangeAddress);
});
it('can fetch balance', async function() {
if (!process.env.HD_MNEMONIC) {
console.error('process.env.HD_MNEMONIC not set, skipped');
return;
}
jasmine.DEFAULT_TIMEOUT_INTERVAL = 90 * 1000;
let hd = new HDSegwitBech32Wallet();
hd.setSecret(process.env.HD_MNEMONIC);
assert.ok(hd.validateMnemonic());
assert.strictEqual(
'zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP',
hd.getXpub(),
);
assert.strictEqual(hd._getExternalAddressByIndex(0), 'bc1qvd6w54sydc08z3802svkxr7297ez7cusd6266p');
assert.strictEqual(hd._getExternalAddressByIndex(1), 'bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh');
assert.strictEqual(hd._getInternalAddressByIndex(0), 'bc1qcg6e26vtzja0h8up5w2m7utex0fsu4v0e0e7uy');
assert.strictEqual(hd._getInternalAddressByIndex(1), 'bc1qwp58x4c9e5cplsnw5096qzdkae036ug7a34x3r');
await hd.fetchBalance();
assert.strictEqual(hd.getBalance(), 200000);
assert.strictEqual(await hd.getAddressAsync(), hd._getExternalAddressByIndex(2));
assert.strictEqual(await hd.getChangeAddressAsync(), hd._getInternalAddressByIndex(2));
assert.strictEqual(hd.next_free_address_index, 2);
assert.strictEqual(hd.next_free_change_address_index, 2);
// now, reset HD wallet, and find free addresses from scratch:
hd = new HDSegwitBech32Wallet();
hd.setSecret(process.env.HD_MNEMONIC);
assert.strictEqual(await hd.getAddressAsync(), hd._getExternalAddressByIndex(2));
assert.strictEqual(await hd.getChangeAddressAsync(), hd._getInternalAddressByIndex(2));
assert.strictEqual(hd.next_free_address_index, 2);
assert.strictEqual(hd.next_free_change_address_index, 2);
});
it('can fetch transactions', async function() {
if (!process.env.HD_MNEMONIC) {
console.error('process.env.HD_MNEMONIC not set, skipped');
return;
}
jasmine.DEFAULT_TIMEOUT_INTERVAL = 90 * 1000;
let hd = new HDSegwitBech32Wallet();
hd.setSecret(process.env.HD_MNEMONIC);
assert.ok(hd.validateMnemonic());
assert.strictEqual(hd.timeToRefreshBalance(), true);
assert.ok(hd._lastTxFetch === 0);
assert.ok(hd._lastBalanceFetch === 0);
await hd.fetchBalance();
await hd.fetchTransactions();
assert.ok(hd._lastTxFetch > 0);
assert.ok(hd._lastBalanceFetch > 0);
assert.strictEqual(hd.timeToRefreshBalance(), false);
assert.strictEqual(hd.getTransactions().length, 4);
for (let tx of hd.getTransactions()) {
assert.ok(tx.hash);
assert.strictEqual(tx.value, 50000);
assert.ok(tx.received);
assert.ok(tx.confirmations > 1);
}
});
it('can fetch UTXO', async () => {
if (!process.env.HD_MNEMONIC) {
console.error('process.env.HD_MNEMONIC not set, skipped');
return;
}
jasmine.DEFAULT_TIMEOUT_INTERVAL = 90 * 1000;
let hd = new HDSegwitBech32Wallet();
hd.setSecret(process.env.HD_MNEMONIC);
assert.ok(hd.validateMnemonic());
await hd.fetchBalance();
await hd.fetchUtxo();
let utxo = hd.getUtxo();
assert.strictEqual(utxo.length, 4);
assert.ok(utxo[0].txId);
assert.ok(utxo[0].vout === 0 || utxo[0].vout === 1);
assert.ok(utxo[0].value);
assert.ok(utxo[0].address);
});
it('can generate addresses only via zpub', function() {
let zpub = 'zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs';
let hd = new HDSegwitBech32Wallet();
hd._xpub = zpub;
assert.strictEqual(hd._getExternalAddressByIndex(0), 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu');
assert.strictEqual(hd._getExternalAddressByIndex(1), 'bc1qnjg0jd8228aq7egyzacy8cys3knf9xvrerkf9g');
assert.strictEqual(hd._getInternalAddressByIndex(0), 'bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el');
assert.ok(hd._getInternalAddressByIndex(0) !== hd._getInternalAddressByIndex(1));
});
it('can generate', async () => {
let hd = new HDSegwitBech32Wallet();
let hashmap = {};
for (let c = 0; c < 1000; c++) {
await hd.generate();
let secret = hd.getSecret();
if (hashmap[secret]) {
throw new Error('Duplicate secret generated!');
}
hashmap[secret] = 1;
assert.ok(secret.split(' ').length === 12 || secret.split(' ').length === 24);
}
let hd2 = new HDSegwitBech32Wallet();
hd2.setSecret(hd.getSecret());
assert.ok(hd2.validateMnemonic());
});
it('can catch up with externally modified wallet', async () => {
if (!process.env.HD_MNEMONIC_BIP84) {
console.error('process.env.HD_MNEMONIC_BIP84 not set, skipped');
return;
}
jasmine.DEFAULT_TIMEOUT_INTERVAL = 90 * 1000;
let hd = new HDSegwitBech32Wallet();
hd.setSecret(process.env.HD_MNEMONIC_BIP84);
assert.ok(hd.validateMnemonic());
await hd.fetchBalance();
let oldBalance = hd.getBalance();
await hd.fetchTransactions();
let oldTransactions = hd.getTransactions();
// now, mess with internal state, make it 'obsolete'
hd._txs_by_external_index['2'].pop();
hd._txs_by_internal_index['16'].pop();
hd._txs_by_internal_index['17'] = [];
for (let c = 17; c < 100; c++) hd._balances_by_internal_index[c] = { c: 0, u: 0 };
hd._balances_by_external_index['2'].c = 1000000;
assert.ok(hd.getBalance() !== oldBalance);
assert.ok(hd.getTransactions().length !== oldTransactions.length);
// now, refetch! should get back to normal
await hd.fetchBalance();
assert.strictEqual(hd.getBalance(), oldBalance);
await hd.fetchTransactions();
assert.strictEqual(hd.getTransactions().length, oldTransactions.length);
});
it('can create transactions', async () => {
if (!process.env.HD_MNEMONIC_BIP84) {
console.error('process.env.HD_MNEMONIC_BIP84 not set, skipped');
return;
}
jasmine.DEFAULT_TIMEOUT_INTERVAL = 90 * 1000;
let hd = new HDSegwitBech32Wallet();
hd.setSecret(process.env.HD_MNEMONIC_BIP84);
assert.ok(hd.validateMnemonic());
let start = +new Date();
await hd.fetchBalance();
let end = +new Date();
end - start > 5000 && console.warn('fetchBalance took', (end - start) / 1000, 'sec');
start = +new Date();
await hd.fetchTransactions();
end = +new Date();
end - start > 15000 && console.warn('fetchTransactions took', (end - start) / 1000, 'sec');
let txFound = 0;
for (let tx of hd.getTransactions()) {
if (tx.hash === 'e9ef58baf4cff3ad55913a360c2fa1fd124309c59dcd720cdb172ce46582097b') {
assert.strictEqual(tx.value, -129545);
txFound++;
}
if (tx.hash === 'e112771fd43962abfe4e4623bf788d6d95ff1bd0f9b56a6a41fb9ed4dacc75f1') {
assert.strictEqual(tx.value, 1000000);
txFound++;
}
}
assert.ok(txFound === 2);
await hd.fetchUtxo();
let changeAddress = await hd.getChangeAddressAsync();
assert.ok(changeAddress && changeAddress.startsWith('bc1'));
let { tx, inputs, outputs, fee } = hd.createTransaction(
hd.getUtxo(),
[{ address: 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu', value: 101000 }],
13,
changeAddress,
);
assert.strictEqual(Math.round(fee / tx.byteLength()), 13);
let totalInput = 0;
for (let inp of inputs) {
totalInput += inp.value;
}
let totalOutput = 0;
for (let outp of outputs) {
totalOutput += outp.value;
}
assert.strictEqual(totalInput - totalOutput, fee);
assert.strictEqual(outputs[outputs.length - 1].address, changeAddress);
});
});

View file

@ -9,7 +9,8 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 300 * 1000;
afterAll(() => {
// after all tests we close socket so the test suite can actually terminate
return BlueElectrum.forceDisconnect();
BlueElectrum.forceDisconnect();
return new Promise(resolve => setTimeout(resolve, 10000)); // simple sleep to wait for all timeouts termination
});
beforeAll(async () => {
@ -79,18 +80,27 @@ it('HD (BIP49) can work with a gap', async function() {
// console.log('external', c, hd._getExternalAddressByIndex(c));
// }
await hd.fetchTransactions();
console.log('hd.transactions.length=', hd.transactions.length);
assert.ok(hd.transactions.length >= 3);
});
it('Segwit HD (BIP49) can batch fetch many txs', async function() {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 240 * 1000;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 300 * 1000;
let hd = new HDSegwitP2SHWallet();
hd._xpub = 'ypub6WZ2c7YJ1SQ1rBYftwMqwV9bBmybXzETFxWmkzMz25bCf6FkDdXjNgR7zRW8JGSnoddNdUH7ZQS7JeQAddxdGpwgPskcsXFcvSn1JdGVcPQ'; // cant fetch txs
hd._xpub = 'ypub6WZ2c7YJ1SQ1rBYftwMqwV9bBmybXzETFxWmkzMz25bCf6FkDdXjNgR7zRW8JGSnoddNdUH7ZQS7JeQAddxdGpwgPskcsXFcvSn1JdGVcPQ';
await hd.fetchBalance();
await hd.fetchTransactions();
assert.ok(hd.transactions.length > 0);
console.log('hd.transactions.length=', hd.transactions.length);
assert.ok(hd.getTransactions().length === 153);
});
it('Segwit HD (BIP49) can fetch more data if pointers to last_used_addr are lagging behind', async function() {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 300 * 1000;
let hd = new HDSegwitP2SHWallet();
hd._xpub = 'ypub6WZ2c7YJ1SQ1rBYftwMqwV9bBmybXzETFxWmkzMz25bCf6FkDdXjNgR7zRW8JGSnoddNdUH7ZQS7JeQAddxdGpwgPskcsXFcvSn1JdGVcPQ';
hd.next_free_change_address_index = 40;
hd.next_free_address_index = 50;
await hd.fetchBalance();
await hd.fetchTransactions();
assert.strictEqual(hd.getTransactions().length, 153);
});
it('Segwit HD (BIP49) can generate addressess only via ypub', function() {
@ -207,10 +217,13 @@ it('Segwit HD (BIP49) can fetch balance with many used addresses in hierarchy',
let end = +new Date();
const took = (end - start) / 1000;
took > 15 && console.warn('took', took, "sec to fetch huge HD wallet's balance");
assert.strictEqual(hd.getBalance(), 0.00051432);
assert.strictEqual(hd.getBalance(), 51432);
await hd.fetchUtxo();
assert.ok(hd.utxo.length > 0);
assert.ok(hd.utxo[0].txid);
assert.ok(hd.utxo[0].vout === 0);
assert.ok(hd.utxo[0].amount);
await hd.fetchTransactions();
assert.strictEqual(hd.getTransactions().length, 107);

View file

@ -153,6 +153,9 @@ describe('LightningCustodianWallet', () => {
await l2.fetchTransactions();
assert.strictEqual(l2.transactions_raw.length, txLen + 1);
let lastTx = l2.transactions_raw[l2.transactions_raw.length - 1];
assert.strictEqual(typeof lastTx.payment_preimage, 'string', 'preimage is present and is a string');
assert.strictEqual(lastTx.payment_preimage.length, 64, 'preimage is present and is a string of 32 hex-encoded bytes');
// transactions became more after paying an invoice
// now, trying to pay duplicate invoice
@ -374,6 +377,14 @@ describe('LightningCustodianWallet', () => {
err = true;
}
assert.ok(err);
err = false;
try {
await l1.addInvoice(NaN, 'zero amt inv');
} catch (_) {
err = true;
}
assert.ok(err);
});
it('cant pay negative free amount', async () => {

View file

@ -12,6 +12,7 @@ import LightningSettings from './screen/settings/lightningSettings';
import WalletsList from './screen/wallets/list';
import WalletTransactions from './screen/wallets/transactions';
import AddWallet from './screen/wallets/add';
import PleaseBackup from './screen/wallets/pleaseBackup';
import ImportWallet from './screen/wallets/import';
import WalletDetails from './screen/wallets/details';
import WalletExport from './screen/wallets/export';
@ -183,6 +184,9 @@ const CreateWalletStackNavigator = createStackNavigator({
ImportWallet: {
screen: ImportWallet,
},
PleaseBackup: {
screen: PleaseBackup,
},
});
const LightningScanInvoiceStackNavigator = createStackNavigator({

View file

@ -21,8 +21,6 @@ Community: [telegram group](https://t.me/bluewallet)
* Encryption. Plausible deniability
* And many more [features...](https://bluewallet.io/features.html)
Beta version, do not use to store large amounts!
<img src="https://i.imgur.com/hHYJnMj.png" width="100%">

138
WatchConnectivity.js Normal file
View file

@ -0,0 +1,138 @@
import * as watch from 'react-native-watch-connectivity';
import { InteractionManager } from 'react-native';
const loc = require('./loc');
export default class WatchConnectivity {
isAppInstalled = false;
BlueApp = require('./BlueApp');
constructor() {
this.getIsWatchAppInstalled();
}
getIsWatchAppInstalled() {
watch.getIsWatchAppInstalled((err, isAppInstalled) => {
if (!err) {
this.isAppInstalled = isAppInstalled;
this.sendWalletsToWatch();
}
});
watch.subscribeToMessages(async (err, message, reply) => {
if (!err) {
if (message.request === 'createInvoice') {
const createInvoiceRequest = await this.handleLightningInvoiceCreateRequest(
message.walletIndex,
message.amount,
message.description,
);
reply({ invoicePaymentRequest: createInvoiceRequest });
}
} else {
reply(err);
}
});
}
async handleLightningInvoiceCreateRequest(walletIndex, amount, description) {
const wallet = this.BlueApp.getWallets()[walletIndex];
if (wallet.allowReceive() && amount > 0 && description.trim().length > 0) {
try {
const invoiceRequest = await wallet.addInvoice(amount, description);
return invoiceRequest;
} catch (error) {
return error;
}
}
}
async sendWalletsToWatch() {
InteractionManager.runAfterInteractions(async () => {
if (this.isAppInstalled) {
const allWallets = this.BlueApp.getWallets();
let wallets = [];
for (const wallet of allWallets) {
let receiveAddress = '';
if (wallet.allowReceive()) {
if (wallet.getAddressAsync) {
receiveAddress = await wallet.getAddressAsync();
} else {
receiveAddress = wallet.getAddress();
}
}
let transactions = wallet.getTransactions(10);
let watchTransactions = [];
for (const transaction of transactions) {
let type = 'pendingConfirmation';
let memo = '';
let amount = 0;
if (transaction.hasOwnProperty('confirmations') && !transaction.confirmations > 0) {
type = 'pendingConfirmation';
} else if (transaction.type === 'user_invoice' || transaction.type === 'payment_request') {
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0;
const invoiceExpiration = transaction.timestamp + transaction.expire_time;
if (invoiceExpiration > now) {
type = 'pendingConfirmation';
} else if (invoiceExpiration < now) {
if (transaction.ispaid) {
type = 'received';
} else {
type = 'sent';
}
}
} else if (transaction.value / 100000000 < 0) {
type = 'sent';
} else {
type = 'received';
}
if (transaction.type === 'user_invoice' || transaction.type === 'payment_request') {
if (isNaN(transaction.value)) {
amount = '0';
}
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0;
const invoiceExpiration = transaction.timestamp + transaction.expire_time;
if (invoiceExpiration > now) {
amount = loc.formatBalance(transaction.value, wallet.getPreferredBalanceUnit(), true).toString();
} else if (invoiceExpiration < now) {
if (transaction.ispaid) {
amount = loc.formatBalance(transaction.value, wallet.getPreferredBalanceUnit(), true).toString();
} else {
amount = loc.lnd.expired;
}
} else {
amount = loc.formatBalance(transaction.value, wallet.getPreferredBalanceUnit(), true).toString();
}
} else {
amount = loc.formatBalance(transaction.value, wallet.getPreferredBalanceUnit(), true).toString();
}
if (this.BlueApp.tx_metadata[transaction.hash] && this.BlueApp.tx_metadata[transaction.hash]['memo']) {
memo = this.BlueApp.tx_metadata[transaction.hash]['memo'];
} else if (transaction.memo) {
memo = transaction.memo;
}
const watchTX = { type, amount, memo, time: loc.transactionTimeToReadable(transaction.received) };
watchTransactions.push(watchTX);
}
wallets.push({
label: wallet.getLabel(),
balance: loc.formatBalance(Number(wallet.getBalance()), wallet.getPreferredBalanceUnit(), true),
type: wallet.type,
preferredBalanceUnit: wallet.getPreferredBalanceUnit(),
receiveAddress: receiveAddress,
transactions: watchTransactions,
});
}
watch.updateApplicationContext({ wallets });
}
});
}
}
WatchConnectivity.init = function() {
if (WatchConnectivity.shared) return;
WatchConnectivity.shared = new WatchConnectivity();
};

114
WatchOnlyWallet.test.js Normal file
View file

@ -0,0 +1,114 @@
/* global it, describe, jasmine, afterAll, beforeAll */
import { WatchOnlyWallet } from './class';
let assert = require('assert');
global.net = require('net'); // needed by Electrum client. For RN it is proviced in shim.js
let BlueElectrum = require('./BlueElectrum'); // so it connects ASAP
afterAll(async () => {
// after all tests we close socket so the test suite can actually terminate
BlueElectrum.forceDisconnect();
return new Promise(resolve => setTimeout(resolve, 10000)); // simple sleep to wait for all timeouts termination
});
beforeAll(async () => {
// awaiting for Electrum to be connected. For RN Electrum would naturally connect
// while app starts up, but for tests we need to wait for it
await BlueElectrum.waitTillConnected();
});
describe('Watch only wallet', () => {
it('can fetch balance', async () => {
let w = new WatchOnlyWallet();
w.setSecret('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa');
await w.fetchBalance();
assert.ok(w.getBalance() > 16);
});
it('can fetch tx', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 150 * 1000;
let w = new WatchOnlyWallet();
w.setSecret('167zK5iZrs1U6piDqubD3FjRqUTM2CZnb8');
await w.fetchTransactions();
assert.strictEqual(w.getTransactions().length, 233);
w = new WatchOnlyWallet();
w.setSecret('1BiJW1jyUaxcJp2JWwbPLPzB1toPNWTFJV');
await w.fetchTransactions();
assert.strictEqual(w.getTransactions().length, 2);
// fetch again and make sure no duplicates
await w.fetchTransactions();
assert.strictEqual(w.getTransactions().length, 2);
});
it('can fetch complex TXs', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 120 * 1000;
let w = new WatchOnlyWallet();
w.setSecret('3NLnALo49CFEF4tCRhCvz45ySSfz3UktZC');
await w.fetchTransactions();
for (let tx of w.getTransactions()) {
assert.ok(tx.value, 'incorrect tx.value');
}
});
it('can validate address', async () => {
let w = new WatchOnlyWallet();
w.setSecret('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG');
assert.ok(w.valid());
w.setSecret('3BDsBDxDimYgNZzsqszNZobqQq3yeUoJf2');
assert.ok(w.valid());
w.setSecret('not valid');
assert.ok(!w.valid());
w.setSecret('xpub6CQdfC3v9gU86eaSn7AhUFcBVxiGhdtYxdC5Cw2vLmFkfth2KXCMmYcPpvZviA89X6DXDs4PJDk5QVL2G2xaVjv7SM4roWHr1gR4xB3Z7Ps');
assert.ok(w.valid());
w.setSecret('ypub6XRzrn3HB1tjhhvrHbk1vnXCecZEdXohGzCk3GXwwbDoJ3VBzZ34jNGWbC6WrS7idXrYjjXEzcPDX5VqnHEnuNf5VAXgLfSaytMkJ2rwVqy');
assert.ok(w.valid());
w.setSecret('zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP');
assert.ok(w.valid());
});
it('can fetch balance & transactions from zpub HD', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000;
let w = new WatchOnlyWallet();
w.setSecret('zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP');
await w.fetchBalance();
assert.strictEqual(w.getBalance(), 200000);
await w.fetchTransactions();
assert.strictEqual(w.getTransactions().length, 4);
assert.ok((await w.getAddressAsync()).startsWith('bc1'));
});
it('can fetch balance & transactions from ypub HD', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000;
let w = new WatchOnlyWallet();
w.setSecret('ypub6XRzrn3HB1tjhhvrHbk1vnXCecZEdXohGzCk3GXwwbDoJ3VBzZ34jNGWbC6WrS7idXrYjjXEzcPDX5VqnHEnuNf5VAXgLfSaytMkJ2rwVqy');
await w.fetchBalance();
assert.strictEqual(w.getBalance(), 52774);
await w.fetchTransactions();
assert.strictEqual(w.getTransactions().length, 3);
assert.ok((await w.getAddressAsync()).startsWith('3'));
});
it('can fetch balance & transactions from xpub HD', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000;
let w = new WatchOnlyWallet();
w.setSecret('xpub6CQdfC3v9gU86eaSn7AhUFcBVxiGhdtYxdC5Cw2vLmFkfth2KXCMmYcPpvZviA89X6DXDs4PJDk5QVL2G2xaVjv7SM4roWHr1gR4xB3Z7Ps');
await w.fetchBalance();
assert.strictEqual(w.getBalance(), 0);
await w.fetchTransactions();
assert.strictEqual(w.getTransactions().length, 4);
assert.ok((await w.getAddressAsync()).startsWith('1'));
});
it('can fetch large HD', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 500 * 1000;
let w = new WatchOnlyWallet();
w.setSecret('ypub6WnnYxkQCGeowv4BXq9Y9PHaXgHMJg9TkFaDJkunhcTAfbDw8z3LvV9kFNHGjeVaEoGdsSJgaMWpUBvYvpYGMJd43gTK5opecVVkvLwKttx');
await w.fetchBalance();
await w.fetchTransactions();
assert.ok(w.getTransactions().length >= 167);
});
});

View file

@ -0,0 +1 @@
export default from '@react-native-community/async-storage/jest/async-storage-mock'

View file

@ -17,7 +17,7 @@
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
<option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" />
<option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res;file://$MODULE_DIR$/build/generated/res/rs/debug;file://$MODULE_DIR$/build/generated/res/resValues/debug" />
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
</configuration>
</facet>
@ -28,16 +28,16 @@
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/apt/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debug/compileDebugAidl/out" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debug/compileDebugRenderscript/out" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/react/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/apt/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debugAndroidTest/compileDebugAndroidTestAidl/out" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debugAndroidTest/compileDebugAndroidTestRenderscript/out" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/androidTest/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/apt/test/debug" isTestSource="true" generated="true" />
@ -48,13 +48,6 @@
<sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/shaders" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/shaders" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/assets" type="java-test-resource" />
@ -62,6 +55,13 @@
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/shaders" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/shaders" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
@ -87,103 +87,106 @@
<excludeFolder url="file://$MODULE_DIR$/build/generated/source/r" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/annotation_processor_list" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/apk_list" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/blame" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/build-info" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/builds" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/check-libraries" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/check-manifest" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/checkDebugClasspath" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/checkReleaseClasspath" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundle_manifest" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/check_manifest_result" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/compatible_screen_manifest" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/external_libs_dex" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-runtime-classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-verifier" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/instant-run-apk" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/instant_run_main_apk_resources" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/instant_app_manifest" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/instant_run_merged_manifests" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/instant_run_split_apk_resources" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/javaPrecompile" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/javac" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/jniLibs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/legacy_multidex_aapt_derived_proguard_rules" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/legacy_multidex_main_dex_list" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/linked_res_for_bundle" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint_jar" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifest-checker" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/merged_assets" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/merged_manifests" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/module_bundle" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/metadata_feature_manifest" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/prebuild" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/processed_res" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/reload-dex" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/resources" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/shader_assets" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/shaders" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/split-apk" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/split_list" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/splits-support" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/signing_config" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/transforms" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/validate_signing_config" />
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
</content>
<orderEntry type="jdk" jdkName="Android API 27 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Gradle: org.webkit:android-jsc:r174650@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-vector-drawable:27.1.1@aar" level="project" />
<orderEntry type="library" name="Gradle: com.squareup.okhttp3:okhttp-urlconnection:3.12.1@jar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.fresco:fresco:1.10.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.react:react-native:0.57.8@aar" level="project" />
<orderEntry type="library" name="Gradle: android.arch.lifecycle:livedata-core:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-core-utils:27.1.1@aar" level="project" />
<orderEntry type="library" name="Gradle: android.arch.lifecycle:runtime:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-fragment:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:localbroadcastmanager:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.fresco:fbcore:1.10.0@aar" level="project" />
<orderEntry type="library" name="Gradle: android.arch.lifecycle:common:1.1.0@jar" level="project" />
<orderEntry type="library" name="Gradle: android.arch.lifecycle:viewmodel:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:animated-vector-drawable:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.fresco:drawee:1.10.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.squareup.okhttp3:okhttp:3.11.0@jar" level="project" />
<orderEntry type="library" name="Gradle: com.parse.bolts:bolts-tasks:1.4.0@jar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:exifinterface:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-v4:27.1.1@aar" level="project" />
<orderEntry type="library" name="Gradle: android.arch.core:runtime:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.squareup.okhttp3:okhttp-urlconnection:3.11.0@jar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:animated-vector-drawable:27.1.1@aar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.fresco:imagepipeline-okhttp3:1.10.0@aar" level="project" />
<orderEntry type="library" name="Gradle: android.arch.lifecycle:viewmodel:1.1.1@aar" level="project" />
<orderEntry type="library" name="Gradle: com.squareup.okhttp3:okhttp:3.12.1@jar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:loader:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: android.arch.core:runtime:1.1.1@aar" level="project" />
<orderEntry type="library" name="Gradle: android.arch.lifecycle:livedata-core:1.1.1@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:cursoradapter:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: android.arch.lifecycle:runtime:1.1.1@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-compat:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.fresco:imagepipeline-base:1.10.0@aar" level="project" />
<orderEntry type="library" name="Gradle: io.sentry:sentry:1.7.5@jar" level="project" />
<orderEntry type="library" name="Gradle: com.squareup.okio:okio:1.14.0@jar" level="project" />
<orderEntry type="library" name="Gradle: com.google.zxing:core:3.3.3@jar" level="project" />
<orderEntry type="library" name="Gradle: javax.inject:javax.inject:1@jar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-core-ui:27.1.1@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-compat:27.1.1@aar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.soloader:soloader:0.6.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.google.code.findbugs:jsr305:3.0.2@jar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-vector-drawable:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-core-utils:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-annotations:28.0.0@jar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:interpolator:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: android.arch.lifecycle:livedata:1.1.1@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:drawerlayout:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:documentfile:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:slidingpanelayout:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.parse.bolts:bolts-tasks:1.4.0@jar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:appcompat-v7:28.0.0@aar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: com.android.support:multidex-instrumentation:1.0.2@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:collections:28.0.0@jar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-core-ui:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.fresco:imagepipeline-okhttp3:1.10.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:asynclayoutinflater:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:print:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: android.arch.core:common:1.1.1@jar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:versionedparcelable:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.infer.annotation:infer-annotation:0.11.2@jar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.fresco:imagepipeline:1.10.0@aar" level="project" />
<orderEntry type="library" name="Gradle: org.slf4j:slf4j-api:1.7.24@jar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-media-compat:27.1.1@aar" level="project" />
<orderEntry type="library" name="Gradle: com.google.code.findbugs:jsr305:3.0.2@jar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-fragment:27.1.1@aar" level="project" />
<orderEntry type="library" name="Gradle: io.sentry:sentry-android:1.7.5@jar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-annotations:28.0.0@jar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:appcompat-v7:27.1.1@aar" level="project" />
<orderEntry type="library" name="Gradle: android.arch.core:common:1.1.0@jar" level="project" />
<orderEntry type="library" name="Gradle: com.fasterxml.jackson.core:jackson-core:2.8.7@jar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.soloader:soloader:0.5.1@aar" level="project" />
<orderEntry type="module" module-name="react-native-webview" />
<orderEntry type="module" module-name="react-native-linear-gradient" />
<orderEntry type="module" module-name="react-native-svg" />
<orderEntry type="module" module-name="react-native-sentry" />
<orderEntry type="module" module-name="react-native-google-analytics-bridge" />
<orderEntry type="module" module-name="react-native-haptic-feedback" />
<orderEntry type="module" module-name="react-native-gesture-handler" />
<orderEntry type="module" module-name="react-native-fs" />
<orderEntry type="module" module-name="react-native-prompt-android" />
<orderEntry type="module" module-name="react-native-vector-icons" />
<orderEntry type="library" name="Gradle: com.android.support:viewpager:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.react:react-native:0.59.6@aar" level="project" />
<orderEntry type="library" name="Gradle: android.arch.lifecycle:common:1.1.1@jar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:coordinatorlayout:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:customview:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:swiperefreshlayout:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:multidex:1.0.3@aar" level="project" />
<orderEntry type="library" name="Gradle: com.squareup.okio:okio:1.15.0@jar" level="project" />
<orderEntry type="library" name="Gradle: com.koushikdutta.async:androidasync:2.1.6@jar" level="project" />
<orderEntry type="module" module-name="react-native-device-info" />
<orderEntry type="module" module-name="react-native-randombytes" />
<orderEntry type="module" module-name="react-native-google-analytics-bridge" />
<orderEntry type="module" module-name="react-native-obscure" />
<orderEntry type="module" module-name="react-native-camera" />
<orderEntry type="module" module-name="react-native-webview" />
<orderEntry type="module" module-name="@react-native-community_slider" />
<orderEntry type="module" module-name="react-native-sentry" />
<orderEntry type="module" module-name="react-native-linear-gradient" />
<orderEntry type="module" module-name="react-native-image-picker" />
<orderEntry type="module" module-name="react-native-vector-icons" />
<orderEntry type="module" module-name="react-native-haptic-feedback" />
<orderEntry type="module" module-name="@react-native-community_async-storage" />
<orderEntry type="module" module-name="react-native-prompt-android" />
<orderEntry type="module" module-name="react-native-gesture-handler" />
<orderEntry type="module" module-name="react-native-randombytes" />
<orderEntry type="module" module-name="react-native-svg" />
<orderEntry type="module" module-name="@remobile_react-native-qrcode-local-image" />
<orderEntry type="module" module-name="react-native-fs" />
<orderEntry type="module" module-name="react-native-tcp" />
<orderEntry type="library" name="Gradle: android-android-27" level="project" />
</component>
</module>

View file

@ -102,7 +102,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "3.9.4"
versionName "4.0.3"
ndk {
abiFilters "armeabi-v7a", "x86"
}
@ -139,6 +139,7 @@ android {
}
dependencies {
implementation project(':@react-native-community_async-storage')
implementation project(':@react-native-community_slider')
implementation project(':react-native-obscure')
implementation project(':react-native-tcp')

View file

@ -26,6 +26,9 @@
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="bitcoin" />
<data android:scheme="lightning" />
<data android:scheme="bluewallet" />
<data android:scheme="lapp" />
<data android:scheme="blue" />
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />

View file

@ -3,6 +3,7 @@ package io.bluewallet.bluewallet;
import android.app.Application;
import com.facebook.react.ReactApplication;
import com.reactnativecommunity.asyncstorage.AsyncStoragePackage;
import com.reactnativecommunity.slider.ReactSliderPackage;
import com.diegofhg.obscure.ObscurePackage;
import com.peel.react.TcpSocketsModule;
@ -58,6 +59,7 @@ public class MainApplication extends Application implements ReactApplication {
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new AsyncStoragePackage(),
new ReactSliderPackage(),
new ObscurePackage(),
new TcpSocketsModule(),

View file

@ -1,4 +1,6 @@
rootProject.name = 'BlueWallet'
include ':@react-native-community_async-storage'
project(':@react-native-community_async-storage').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/async-storage/android')
include ':@react-native-community_slider'
project(':@react-native-community_slider').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/slider/android')
include ':react-native-obscure'

View file

@ -1,7 +1,6 @@
import { LegacyWallet } from './legacy-wallet';
import Frisbee from 'frisbee';
const bip39 = require('bip39');
const BigNumber = require('bignumber.js');
const bitcoin = require('bitcoinjs-lib');
const BlueElectrum = require('../BlueElectrum');
@ -18,7 +17,13 @@ export class AbstractHDWallet extends LegacyWallet {
this._xpub = ''; // cache
this.usedAddresses = [];
this._address_to_wif_cache = {};
this.gap_limit = 3;
this.gap_limit = 20;
}
prepareForSerialization() {
// deleting structures that cant be serialized
delete this._node0;
delete this._node1;
}
generate() {
@ -110,11 +115,16 @@ export class AbstractHDWallet extends LegacyWallet {
// looking for free external address
let freeAddress = '';
let c;
for (c = 0; c < Math.max(5, this.usedAddresses.length); c++) {
for (c = 0; c < this.gap_limit + 1; c++) {
if (this.next_free_address_index + c < 0) continue;
let address = this._getExternalAddressByIndex(this.next_free_address_index + c);
this.external_addresses_cache[this.next_free_address_index + c] = address; // updating cache just for any case
let txs = await BlueElectrum.getTransactionsByAddress(address);
let txs = [];
try {
txs = await BlueElectrum.getTransactionsByAddress(address);
} catch (Err) {
console.warn('BlueElectrum.getTransactionsByAddress()', Err.message);
}
if (txs.length === 0) {
// found free address
freeAddress = address;
@ -143,11 +153,16 @@ export class AbstractHDWallet extends LegacyWallet {
// looking for free internal address
let freeAddress = '';
let c;
for (c = 0; c < Math.max(5, this.usedAddresses.length); c++) {
for (c = 0; c < this.gap_limit + 1; c++) {
if (this.next_free_change_address_index + c < 0) continue;
let address = this._getInternalAddressByIndex(this.next_free_change_address_index + c);
this.internal_addresses_cache[this.next_free_change_address_index + c] = address; // updating cache just for any case
let txs = await BlueElectrum.getTransactionsByAddress(address);
let txs = [];
try {
txs = await BlueElectrum.getTransactionsByAddress(address);
} catch (Err) {
console.warn('BlueElectrum.getTransactionsByAddress()', Err.message);
}
if (txs.length === 0) {
// found free address
freeAddress = address;
@ -323,14 +338,14 @@ export class AbstractHDWallet extends LegacyWallet {
}
// no luck - lets iterate over all addresses we have up to first unused address index
for (let c = 0; c <= this.next_free_change_address_index + 3; c++) {
for (let c = 0; c <= this.next_free_change_address_index + this.gap_limit; c++) {
let possibleAddress = this._getInternalAddressByIndex(c);
if (possibleAddress === address) {
return (this._address_to_wif_cache[address] = this._getInternalWIFByIndex(c));
}
}
for (let c = 0; c <= this.next_free_address_index + 3; c++) {
for (let c = 0; c <= this.next_free_address_index + this.gap_limit; c++) {
let possibleAddress = this._getExternalAddressByIndex(c);
if (possibleAddress === address) {
return (this._address_to_wif_cache[address] = this._getExternalWIFByIndex(c));
@ -404,52 +419,71 @@ export class AbstractHDWallet extends LegacyWallet {
// wrong guess. will have to rescan
if (!completelyEmptyWallet) {
// so doing binary search for last used address:
this.next_free_change_address_index = await binarySearchIterationForInternalAddress(100);
this.next_free_address_index = await binarySearchIterationForExternalAddress(100);
this.next_free_change_address_index = await binarySearchIterationForInternalAddress(1000);
this.next_free_address_index = await binarySearchIterationForExternalAddress(1000);
}
}
this.usedAddresses = [];
// generating all involved addresses:
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
this.usedAddresses.push(this._getExternalAddressByIndex(c));
}
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
this.usedAddresses.push(this._getInternalAddressByIndex(c));
}
} // end rescanning fresh wallet
// finally fetching balance
let balance = await BlueElectrum.multiGetBalanceByAddress(this.usedAddresses);
this.balance = new BigNumber(balance.balance).dividedBy(100000000).toNumber();
this.unconfirmed_balance = new BigNumber(balance.unconfirmed_balance).dividedBy(100000000).toNumber();
this._lastBalanceFetch = +new Date();
await this._fetchBalance();
} catch (err) {
console.warn(err);
}
}
/**
* @inheritDoc
*/
async fetchUtxo() {
async _fetchBalance() {
// probing future addressess in hierarchy whether they have any transactions, in case
// our 'next free addr' pointers are lagging behind
let tryAgain = false;
let txs = await BlueElectrum.getTransactionsByAddress(
this._getExternalAddressByIndex(this.next_free_address_index + this.gap_limit - 1),
);
if (txs.length > 0) {
// whoa, someone uses our wallet outside! better catch up
this.next_free_address_index += this.gap_limit;
tryAgain = true;
}
txs = await BlueElectrum.getTransactionsByAddress(
this._getInternalAddressByIndex(this.next_free_change_address_index + this.gap_limit - 1),
);
if (txs.length > 0) {
this.next_free_change_address_index += this.gap_limit;
tryAgain = true;
}
// FIXME: refactor me ^^^ can be batched in single call
if (tryAgain) return this._fetchBalance();
// next, business as usuall. fetch balances
this.usedAddresses = [];
// generating all involved addresses:
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
this.usedAddresses.push(this._getExternalAddressByIndex(c));
}
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
this.usedAddresses.push(this._getInternalAddressByIndex(c));
}
let balance = await BlueElectrum.multiGetBalanceByAddress(this.usedAddresses);
this.balance = balance.balance;
this.unconfirmed_balance = balance.unconfirmed_balance;
this._lastBalanceFetch = +new Date();
}
async _fetchUtxoBatch(addresses) {
const api = new Frisbee({
baseURI: 'https://blockchain.info',
});
if (this.usedAddresses.length === 0) {
// just for any case, refresh balance (it refreshes internal `this.usedAddresses`)
await this.fetchBalance();
}
let addresses = this.usedAddresses.join('|');
addresses += '|' + this._getExternalAddressByIndex(this.next_free_address_index);
addresses += '|' + this._getInternalAddressByIndex(this.next_free_change_address_index);
addresses = addresses.join('|');
let utxos = [];
let response;
let uri;
try {
uri = 'https://blockchain.info' + '/unspent?active=' + addresses + '&limit=1000';
response = await api.get('/unspent?active=' + addresses + '&limit=1000');
// this endpoint does not support offset of some kind o_O
// so doing only one call
@ -469,10 +503,55 @@ export class AbstractHDWallet extends LegacyWallet {
utxos.push(unspent);
}
} catch (err) {
console.warn(err);
console.warn(err, { uri });
}
this.utxo = utxos;
return utxos;
}
/**
* @inheritDoc
*/
async fetchUtxo() {
if (this.usedAddresses.length === 0) {
// just for any case, refresh balance (it refreshes internal `this.usedAddresses`)
await this.fetchBalance();
}
this.utxo = [];
let addresses = this.usedAddresses;
addresses.push(this._getExternalAddressByIndex(this.next_free_address_index));
addresses.push(this._getInternalAddressByIndex(this.next_free_change_address_index));
let duplicateUtxos = {};
let batch = [];
for (let addr of addresses) {
batch.push(addr);
if (batch.length >= 75) {
let utxos = await this._fetchUtxoBatch(batch);
for (let utxo of utxos) {
let key = utxo.txid + utxo.vout;
if (!duplicateUtxos[key]) {
this.utxo.push(utxo);
duplicateUtxos[key] = 1;
}
}
batch = [];
}
}
// final batch
if (batch.length > 0) {
let utxos = await this._fetchUtxoBatch(batch);
for (let utxo of utxos) {
let key = utxo.txid + utxo.vout;
if (!duplicateUtxos[key]) {
this.utxo.push(utxo);
duplicateUtxos[key] = 1;
}
}
}
}
weOwnAddress(addr) {

View file

@ -42,6 +42,10 @@ export class AbstractWallet {
return this.label;
}
/**
*
* @returns {number} Available to spend amount, int, in sats
*/
getBalance() {
return this.balance;
}
@ -95,7 +99,5 @@ export class AbstractWallet {
return 0;
}
getAddress() {}
// createTx () { throw Error('not implemented') }
}

View file

@ -1,4 +1,4 @@
import { AsyncStorage } from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';
import {
HDLegacyBreadwalletWallet,
HDSegwitP2SHWallet,
@ -7,9 +7,11 @@ import {
LegacyWallet,
SegwitP2SHWallet,
SegwitBech32Wallet,
HDSegwitBech32Wallet,
} from './';
import { LightningCustodianWallet } from './lightning-custodian-wallet';
let encryption = require('../encryption');
import WatchConnectivity from '../WatchConnectivity';
const encryption = require('../encryption');
export class AppStorage {
static FLAG_ENCRYPTED = 'data_encrypted';
@ -17,6 +19,7 @@ export class AppStorage {
static EXCHANGE_RATES = 'currency';
static LNDHUB = 'lndhub';
static PREFERRED_CURRENCY = 'preferredCurrency';
static ADVANCED_MODE_ENABLED = 'advancedmodeenabled';
constructor() {
/** {Array.<AbstractWallet>} */
@ -44,11 +47,14 @@ export class AppStorage {
failedColor: '#ff0000',
shadowColor: '#000000',
inverseForegroundColor: '#ffffff',
hdborderColor: '#68BBE1',
hdbackgroundColor: '#ECF9FF',
lnborderColor: '#F7C056',
lnbackgroundColor: '#FFFAEF',
};
}
async storageIsEncrypted() {
// await AsyncStorage.clear();
let data;
try {
data = await AsyncStorage.getItem(AppStorage.FLAG_ENCRYPTED);
@ -118,8 +124,9 @@ export class AppStorage {
buckets = JSON.parse(buckets);
buckets.push(encryption.encrypt(JSON.stringify(data), fakePassword));
this.cachedPassword = fakePassword;
return AsyncStorage.setItem('data', JSON.stringify(buckets));
const bucketsString = JSON.stringify(buckets);
await AsyncStorage.setItem('data', bucketsString);
return (await AsyncStorage.getItem('data')) === bucketsString;
}
/**
@ -156,6 +163,7 @@ export class AppStorage {
break;
case WatchOnlyWallet.type:
unserializedWallet = WatchOnlyWallet.fromJson(key);
unserializedWallet.init();
break;
case HDLegacyP2PKHWallet.type:
unserializedWallet = HDLegacyP2PKHWallet.fromJson(key);
@ -163,6 +171,9 @@ export class AppStorage {
case HDSegwitP2SHWallet.type:
unserializedWallet = HDSegwitP2SHWallet.fromJson(key);
break;
case HDSegwitBech32Wallet.type:
unserializedWallet = HDSegwitBech32Wallet.fromJson(key);
break;
case HDLegacyBreadwalletWallet.type:
unserializedWallet = HDLegacyBreadwalletWallet.fromJson(key);
break;
@ -199,11 +210,14 @@ export class AppStorage {
this.tx_metadata = data.tx_metadata;
}
}
WatchConnectivity.init();
await WatchConnectivity.shared.sendWalletsToWatch();
return true;
} else {
return false; // failed loading data or loading/decryptin data
}
} catch (error) {
console.warn(error.message);
return false;
}
}
@ -240,6 +254,7 @@ export class AppStorage {
let walletsToSave = [];
for (let key of this.wallets) {
if (typeof key === 'boolean') continue;
if (key.prepareForSerialization) key.prepareForSerialization();
walletsToSave.push(JSON.stringify({ ...key, type: key.type }));
}
@ -269,7 +284,8 @@ export class AppStorage {
} else {
await AsyncStorage.setItem(AppStorage.FLAG_ENCRYPTED, ''); // drop the flag
}
WatchConnectivity.init();
WatchConnectivity.shared.sendWalletsToWatch();
return AsyncStorage.setItem('data', JSON.stringify(data));
}

View file

@ -0,0 +1,492 @@
import { AbstractHDWallet } from './abstract-hd-wallet';
import { NativeModules } from 'react-native';
import bip39 from 'bip39';
import BigNumber from 'bignumber.js';
import b58 from 'bs58check';
const BlueElectrum = require('../BlueElectrum');
const bitcoin5 = require('bitcoinjs5');
const HDNode = require('bip32');
const coinSelectAccumulative = require('coinselect/accumulative');
const coinSelectSplit = require('coinselect/split');
const { RNRandomBytes } = NativeModules;
/**
* Converts zpub to xpub
*
* @param {String} zpub
* @returns {String} xpub
*/
function _zpubToXpub(zpub) {
let data = b58.decode(zpub);
data = data.slice(4);
data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]);
return b58.encode(data);
}
/**
* Creates Segwit Bech32 Bitcoin address
*
* @param hdNode
* @returns {String}
*/
function _nodeToBech32SegwitAddress(hdNode) {
return bitcoin5.payments.p2wpkh({
pubkey: hdNode.publicKey,
}).address;
}
/**
* HD Wallet (BIP39).
* In particular, BIP84 (Bech32 Native Segwit)
* @see https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki
*/
export class HDSegwitBech32Wallet extends AbstractHDWallet {
static type = 'HDsegwitBech32';
static typeReadable = 'HD SegWit (BIP84 Bech32 Native)';
constructor() {
super();
this._balances_by_external_index = {}; // 0 => { c: 0, u: 0 } // confirmed/unconfirmed
this._balances_by_internal_index = {};
this._txs_by_external_index = {};
this._txs_by_internal_index = {};
this._utxo = [];
}
/**
* @inheritDoc
*/
getBalance() {
let ret = 0;
for (let bal of Object.values(this._balances_by_external_index)) {
ret += bal.c;
}
for (let bal of Object.values(this._balances_by_internal_index)) {
ret += bal.c;
}
return ret + this.getUnconfirmedBalance();
}
/**
* @inheritDoc
*/
timeToRefreshTransaction() {
for (let tx of this.getTransactions()) {
if (tx.confirmations < 7) return true;
}
return false;
}
getUnconfirmedBalance() {
let ret = 0;
for (let bal of Object.values(this._balances_by_external_index)) {
ret += bal.u;
}
for (let bal of Object.values(this._balances_by_internal_index)) {
ret += bal.u;
}
return ret;
}
allowSend() {
return true;
}
async generate() {
let that = this;
return new Promise(function(resolve) {
if (typeof RNRandomBytes === 'undefined') {
// CLI/CI environment
// crypto should be provided globally by test launcher
return crypto.randomBytes(32, (err, buf) => { // eslint-disable-line
if (err) throw err;
that.secret = bip39.entropyToMnemonic(buf.toString('hex'));
resolve();
});
}
// RN environment
RNRandomBytes.randomBytes(32, (err, bytes) => {
if (err) throw new Error(err);
let b = Buffer.from(bytes, 'base64').toString('hex');
that.secret = bip39.entropyToMnemonic(b);
resolve();
});
});
}
_getExternalWIFByIndex(index) {
return this._getWIFByIndex(false, index);
}
_getInternalWIFByIndex(index) {
return this._getWIFByIndex(true, index);
}
/**
* Get internal/external WIF by wallet index
* @param {Boolean} internal
* @param {Number} index
* @returns {*}
* @private
*/
_getWIFByIndex(internal, index) {
const mnemonic = this.secret;
const seed = bip39.mnemonicToSeed(mnemonic);
const root = HDNode.fromSeed(seed);
const path = `m/84'/0'/0'/${internal ? 1 : 0}/${index}`;
const child = root.derivePath(path);
return child.toWIF();
}
_getNodeAddressByIndex(node, index) {
index = index * 1; // cast to int
if (node === 0) {
if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
}
if (node === 1) {
if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit
}
if (node === 0 && !this._node0) {
const xpub = _zpubToXpub(this.getXpub());
const hdNode = HDNode.fromBase58(xpub);
this._node0 = hdNode.derive(node);
}
if (node === 1 && !this._node1) {
const xpub = _zpubToXpub(this.getXpub());
const hdNode = HDNode.fromBase58(xpub);
this._node1 = hdNode.derive(node);
}
let address;
if (node === 0) {
address = _nodeToBech32SegwitAddress(this._node0.derive(index));
}
if (node === 1) {
address = _nodeToBech32SegwitAddress(this._node1.derive(index));
}
if (node === 0) {
return (this.external_addresses_cache[index] = address);
}
if (node === 1) {
return (this.internal_addresses_cache[index] = address);
}
}
_getExternalAddressByIndex(index) {
return this._getNodeAddressByIndex(0, index);
}
_getInternalAddressByIndex(index) {
return this._getNodeAddressByIndex(1, index);
}
/**
* Returning zpub actually, not xpub. Keeping same method name
* for compatibility.
*
* @return {String} zpub
*/
getXpub() {
if (this._xpub) {
return this._xpub; // cache hit
}
// first, getting xpub
const mnemonic = this.secret;
const seed = bip39.mnemonicToSeed(mnemonic);
const root = HDNode.fromSeed(seed);
const path = "m/84'/0'/0'";
const child = root.derivePath(path).neutered();
const xpub = child.toBase58();
// bitcoinjs does not support zpub yet, so we just convert it from xpub
let data = b58.decode(xpub);
data = data.slice(4);
data = Buffer.concat([Buffer.from('04b24746', 'hex'), data]);
this._xpub = b58.encode(data);
return this._xpub;
}
/**
* @inheritDoc
*/
async fetchTransactions() {
// if txs are absent for some internal address in hierarchy - this is a sign
// we should fetch txs for that address
// OR if some address has unconfirmed balance - should fetch it's txs
// OR some tx for address is unconfirmed
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
// external addresses first
let hasUnconfirmed = false;
this._txs_by_external_index[c] = this._txs_by_external_index[c] || [];
for (let tx of this._txs_by_external_index[c]) hasUnconfirmed = hasUnconfirmed || (!tx.confirmations || tx.confirmations === 0);
if (hasUnconfirmed || this._txs_by_external_index[c].length === 0 || this._balances_by_external_index[c].u !== 0) {
this._txs_by_external_index[c] = await BlueElectrum.getTransactionsFullByAddress(this._getExternalAddressByIndex(c));
}
}
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
// next, internal addresses
let hasUnconfirmed = false;
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || [];
for (let tx of this._txs_by_internal_index[c]) hasUnconfirmed = hasUnconfirmed || (!tx.confirmations || tx.confirmations === 0);
if (hasUnconfirmed || this._txs_by_internal_index[c].length === 0 || this._balances_by_internal_index[c].u !== 0) {
this._txs_by_internal_index[c] = await BlueElectrum.getTransactionsFullByAddress(this._getInternalAddressByIndex(c));
}
}
this._lastTxFetch = +new Date();
}
getTransactions() {
let txs = [];
for (let addressTxs of Object.values(this._txs_by_external_index)) {
txs = txs.concat(addressTxs);
}
for (let addressTxs of Object.values(this._txs_by_internal_index)) {
txs = txs.concat(addressTxs);
}
let ret = [];
for (let tx of txs) {
tx.received = tx.blocktime * 1000;
if (!tx.blocktime) tx.received = +new Date() - 30 * 1000; // unconfirmed
tx.confirmations = tx.confirmations || 0; // unconfirmed
tx.hash = tx.txid;
tx.value = 0;
for (let vin of tx.inputs) {
// if input (spending) goes from our address - we are loosing!
if (vin.address && this.weOwnAddress(vin.address)) {
tx.value -= new BigNumber(vin.value).multipliedBy(100000000).toNumber();
}
}
for (let vout of tx.outputs) {
// when output goes to our address - this means we are gaining!
if (vout.addresses && vout.addresses[0] && this.weOwnAddress(vout.scriptPubKey.addresses[0])) {
tx.value += new BigNumber(vout.value).multipliedBy(100000000).toNumber();
}
}
ret.push(tx);
}
// now, deduplication:
let usedTxIds = {};
let ret2 = [];
for (let tx of ret) {
if (!usedTxIds[tx.txid]) ret2.push(tx);
usedTxIds[tx.txid] = 1;
}
return ret2.sort(function(a, b) {
return b.received - a.received;
});
}
async _fetchBalance() {
// probing future addressess in hierarchy whether they have any transactions, in case
// our 'next free addr' pointers are lagging behind
let tryAgain = false;
let txs = await BlueElectrum.getTransactionsByAddress(
this._getExternalAddressByIndex(this.next_free_address_index + this.gap_limit - 1),
);
if (txs.length > 0) {
// whoa, someone uses our wallet outside! better catch up
this.next_free_address_index += this.gap_limit;
tryAgain = true;
}
txs = await BlueElectrum.getTransactionsByAddress(
this._getInternalAddressByIndex(this.next_free_change_address_index + this.gap_limit - 1),
);
if (txs.length > 0) {
this.next_free_change_address_index += this.gap_limit;
tryAgain = true;
}
// FIXME: refactor me ^^^ can be batched in single call. plus not just couple of addresses, but all between [ next_free .. (next_free + gap_limit) ]
if (tryAgain) return this._fetchBalance();
// next, business as usuall. fetch balances
let addresses2fetch = [];
// generating all involved addresses.
// basically, refetch all from index zero to maximum. doesnt matter
// since we batch them 100 per call
// external
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
addresses2fetch.push(this._getExternalAddressByIndex(c));
}
// internal
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
addresses2fetch.push(this._getInternalAddressByIndex(c));
}
let balances = await BlueElectrum.multiGetBalanceByAddress(addresses2fetch);
// converting to a more compact internal format
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
let addr = this._getExternalAddressByIndex(c);
if (balances.addresses[addr]) {
// first, if balances differ from what we store - we delete transactions for that
// address so next fetchTransactions() will refetch everything
if (this._balances_by_external_index[c]) {
if (
this._balances_by_external_index[c].c !== balances.addresses[addr].confirmed ||
this._balances_by_external_index[c].u !== balances.addresses[addr].unconfirmed
) {
delete this._txs_by_external_index[c];
}
}
// update local representation of balances on that address:
this._balances_by_external_index[c] = {
c: balances.addresses[addr].confirmed,
u: balances.addresses[addr].unconfirmed,
};
}
}
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
let addr = this._getInternalAddressByIndex(c);
if (balances.addresses[addr]) {
// first, if balances differ from what we store - we delete transactions for that
// address so next fetchTransactions() will refetch everything
if (this._balances_by_internal_index[c]) {
if (
this._balances_by_internal_index[c].c !== balances.addresses[addr].confirmed ||
this._balances_by_internal_index[c].u !== balances.addresses[addr].unconfirmed
) {
delete this._txs_by_internal_index[c];
}
}
// update local representation of balances on that address:
this._balances_by_internal_index[c] = {
c: balances.addresses[addr].confirmed,
u: balances.addresses[addr].unconfirmed,
};
}
}
this._lastBalanceFetch = +new Date();
}
async fetchUtxo() {
// considering only confirmed balance
let addressess = [];
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
if (this._balances_by_external_index[c] && this._balances_by_external_index[c].c && this._balances_by_external_index[c].c > 0) {
addressess.push(this._getExternalAddressByIndex(c));
}
}
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
if (this._balances_by_internal_index[c] && this._balances_by_internal_index[c].c && this._balances_by_internal_index[c].c > 0) {
addressess.push(this._getInternalAddressByIndex(c));
}
}
this._utxo = [];
for (let arr of Object.values(await BlueElectrum.multiGetUtxoByAddress(addressess))) {
this._utxo = this._utxo.concat(arr);
}
}
getUtxo() {
return this._utxo;
}
weOwnAddress(address) {
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
if (this._getExternalAddressByIndex(c) === address) return true;
}
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
if (this._getInternalAddressByIndex(c) === address) return true;
}
return false;
}
createTx(utxos, amount, fee, address) {
throw new Error('Deprecated');
}
/**
*
* @param utxos {Array.<{vout: Number, value: Number, txId: String, address: String}>} List of spendable utxos
* @param targets {Array.<{value: Number, address: String}>} Where coins are going. If theres only 1 target and that target has no value - this will send MAX to that address (respecting fee rate)
* @param feeRate {Number} satoshi per byte
* @param changeAddress {String} Excessive coins will go back to that address
* @param sequence {Number} Used in RBF
* @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number}}
*/
createTransaction(utxos, targets, feeRate, changeAddress, sequence) {
if (!changeAddress) throw new Error('No change address provided');
sequence = sequence || 0;
let algo = coinSelectAccumulative;
if (targets.length === 1 && targets[0] && !targets[0].value) {
// we want to send MAX
algo = coinSelectSplit;
}
let { inputs, outputs, fee } = algo(utxos, targets, feeRate);
// .inputs and .outputs will be undefined if no solution was found
if (!inputs || !outputs) {
throw new Error('Not enough balance. Try sending smaller amount');
}
let txb = new bitcoin5.TransactionBuilder();
let c = 0;
let keypairs = {};
let values = {};
inputs.forEach(input => {
const keyPair = bitcoin5.ECPair.fromWIF(this._getWifForAddress(input.address));
keypairs[c] = keyPair;
values[c] = input.value;
c++;
if (!input.address || !this._getWifForAddress(input.address)) throw new Error('Internal error: no address or WIF to sign input');
const p2wpkh = bitcoin5.payments.p2wpkh({ pubkey: keyPair.publicKey });
txb.addInput(input.txId, input.vout, sequence, p2wpkh.output); // NOTE: provide the prevOutScript!
});
outputs.forEach(output => {
// if output has no address - this is change output
if (!output.address) {
output.address = changeAddress;
}
txb.addOutput(output.address, output.value);
});
for (let cc = 0; cc < c; cc++) {
txb.sign(cc, keypairs[cc], null, null, values[cc]); // NOTE: no redeem script
}
const tx = txb.build();
return { tx, inputs, outputs, fee };
}
}

View file

@ -1,11 +1,13 @@
import { AbstractHDWallet } from './abstract-hd-wallet';
import Frisbee from 'frisbee';
import { NativeModules } from 'react-native';
import bitcoin from 'bitcoinjs-lib';
import bip39 from 'bip39';
import BigNumber from 'bignumber.js';
import b58 from 'bs58check';
import signer from '../models/signer';
const bitcoin = require('bitcoinjs-lib');
const bitcoin5 = require('bitcoinjs5');
const HDNode = require('bip32');
const { RNRandomBytes } = NativeModules;
@ -28,13 +30,10 @@ function ypubToXpub(ypub) {
* @returns {String}
*/
function nodeToP2shSegwitAddress(hdNode) {
const pubkeyBuf = hdNode.keyPair.getPublicKeyBuffer();
const hash = bitcoin.crypto.hash160(pubkeyBuf);
const redeemScript = bitcoin.script.witnessPubKeyHash.output.encode(hash);
const hash2 = bitcoin.crypto.hash160(redeemScript);
const scriptPubkey = bitcoin.script.scriptHash.output.encode(hash2);
return bitcoin.address.fromOutputScript(scriptPubkey);
const { address } = bitcoin5.payments.p2sh({
redeem: bitcoin5.payments.p2wpkh({ pubkey: hdNode.publicKey }),
});
return address;
}
/**
@ -102,9 +101,12 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet {
index = index * 1; // cast to int
if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
const xpub = ypubToXpub(this.getXpub());
const hdNode = bitcoin.HDNode.fromBase58(xpub);
const address = nodeToP2shSegwitAddress(hdNode.derive(0).derive(index));
if (!this._node0) {
const xpub = ypubToXpub(this.getXpub());
const hdNode = HDNode.fromBase58(xpub);
this._node0 = hdNode.derive(0);
}
const address = nodeToP2shSegwitAddress(this._node0.derive(index));
return (this.external_addresses_cache[index] = address);
}
@ -113,9 +115,12 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet {
index = index * 1; // cast to int
if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit
const xpub = ypubToXpub(this.getXpub());
const hdNode = bitcoin.HDNode.fromBase58(xpub);
const address = nodeToP2shSegwitAddress(hdNode.derive(1).derive(index));
if (!this._node1) {
const xpub = ypubToXpub(this.getXpub());
const hdNode = HDNode.fromBase58(xpub);
this._node1 = hdNode.derive(1);
}
const address = nodeToP2shSegwitAddress(this._node1.derive(index));
return (this.internal_addresses_cache[index] = address);
}
@ -133,7 +138,7 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet {
// first, getting xpub
const mnemonic = this.secret;
const seed = bip39.mnemonicToSeed(mnemonic);
const root = bitcoin.HDNode.fromSeedBuffer(seed);
const root = HDNode.fromSeed(seed);
const path = "m/49'/0'/0'";
const child = root.derivePath(path).neutered();

View file

@ -10,3 +10,4 @@ export * from './hd-legacy-p2pkh-wallet';
export * from './watch-only-wallet';
export * from './lightning-custodian-wallet';
export * from './abstract-hd-wallet';
export * from './hd-segwit-bech32-wallet';

View file

@ -114,8 +114,7 @@ export class LegacyWallet extends AbstractWallet {
throw new Error('Could not fetch balance from API: ' + response.err + ' ' + JSON.stringify(response.body));
}
this.balance = new BigNumber(json.final_balance);
this.balance = this.balance.dividedBy(100000000).toString() * 1;
this.balance = Number(json.final_balance);
this.unconfirmed_balance = new BigNumber(json.unconfirmed_balance);
this.unconfirmed_balance = this.unconfirmed_balance.dividedBy(100000000).toString() * 1;
this._lastBalanceFetch = +new Date();

View file

@ -1,7 +1,6 @@
import { LegacyWallet } from './legacy-wallet';
import Frisbee from 'frisbee';
import { BitcoinUnit, Chain } from '../models/bitcoinUnits';
let BigNumber = require('bignumber.js');
export class LightningCustodianWallet extends LegacyWallet {
static type = 'lightningCustodianWallet';
@ -56,11 +55,11 @@ export class LightningCustodianWallet extends LegacyWallet {
}
timeToRefreshBalance() {
return (+new Date() - this._lastBalanceFetch) / 1000 > 3600; // 1hr
return (+new Date() - this._lastBalanceFetch) / 1000 > 300; // 5 min
}
timeToRefreshTransaction() {
return (+new Date() - this._lastTxFetch) / 1000 > 3600; // 1hr
return (+new Date() - this._lastTxFetch) / 1000 > 300; // 5 min
}
static fromJson(param) {
@ -455,7 +454,7 @@ export class LightningCustodianWallet extends LegacyWallet {
}
getBalance() {
return new BigNumber(this.balance).dividedBy(100000000).toString(10);
return this.balance;
}
async fetchBalance(noRetry) {

View file

@ -29,7 +29,6 @@ export class SegwitBech32Wallet extends LegacyWallet {
}
static scriptPubKeyToAddress(scriptPubKey) {
const bitcoin = require('bitcoinjs-lib');
const scriptPubKey2 = Buffer.from(scriptPubKey, 'hex');
return bitcoin.address.fromOutputScript(scriptPubKey2, bitcoin.networks.bitcoin);
}

View file

@ -26,6 +26,10 @@ export class SegwitP2SHWallet extends LegacyWallet {
try {
let keyPair = bitcoin.ECPair.fromWIF(this.secret);
let pubKey = keyPair.getPublicKeyBuffer();
if (!keyPair.compressed) {
console.warn('only compressed public keys are good for segwit');
return false;
}
let witnessScript = bitcoin.script.witnessPubKeyHash.output.encode(bitcoin.crypto.hash160(pubKey));
let scriptPubKey = bitcoin.script.scriptHash.output.encode(bitcoin.crypto.hash160(witnessScript));
address = bitcoin.address.fromOutputScript(scriptPubKey);

View file

@ -4,9 +4,11 @@ import { LightningCustodianWallet } from './lightning-custodian-wallet';
import { HDLegacyBreadwalletWallet } from './hd-legacy-breadwallet-wallet';
import { HDLegacyP2PKHWallet } from './hd-legacy-p2pkh-wallet';
import { WatchOnlyWallet } from './watch-only-wallet';
import { HDSegwitBech32Wallet } from './hd-segwit-bech32-wallet';
export default class WalletGradient {
static hdSegwitP2SHWallet = ['#65ceef', '#68bbe1'];
static hdSegwitBech32Wallet = ['#68bbe1', '#3b73d4'];
static watchOnlyWallet = ['#7d7d7d', '#4a4a4a'];
static legacyWallet = ['#40fad1', '#15be98'];
static hdLegacyP2PKHWallet = ['#e36dfa', '#bd10e0'];
@ -33,6 +35,9 @@ export default class WalletGradient {
case HDSegwitP2SHWallet.type:
gradient = WalletGradient.hdSegwitP2SHWallet;
break;
case HDSegwitBech32Wallet.type:
gradient = WalletGradient.hdSegwitBech32Wallet;
break;
case LightningCustodianWallet.type:
gradient = WalletGradient.lightningCustodianWallet;
break;
@ -64,6 +69,9 @@ export default class WalletGradient {
case HDSegwitP2SHWallet.type:
gradient = WalletGradient.hdSegwitP2SHWallet;
break;
case HDSegwitBech32Wallet.type:
gradient = WalletGradient.hdSegwitBech32Wallet;
break;
case LightningCustodianWallet.type:
gradient = WalletGradient.lightningCustodianWallet;
break;

View file

@ -1,4 +1,7 @@
import { LegacyWallet } from './legacy-wallet';
import { HDSegwitP2SHWallet } from './hd-segwit-p2sh-wallet';
import { HDLegacyP2PKHWallet } from './hd-legacy-p2pkh-wallet';
import { HDSegwitBech32Wallet } from './hd-segwit-bech32-wallet';
const bitcoin = require('bitcoinjs-lib');
export class WatchOnlyWallet extends LegacyWallet {
@ -18,6 +21,8 @@ export class WatchOnlyWallet extends LegacyWallet {
}
valid() {
if (this.secret.startsWith('xpub') || this.secret.startsWith('ypub') || this.secret.startsWith('zpub')) return true;
try {
bitcoin.address.toOutputScript(this.getAddress());
return true;
@ -25,4 +30,60 @@ export class WatchOnlyWallet extends LegacyWallet {
return false;
}
}
/**
* this method creates appropriate HD wallet class, depending on whether we have xpub, ypub or zpub
* as a property of `this`, and in case such property exists - it recreates it and copies data from old one.
* this is needed after serialization/save/load/deserialization procedure.
*/
init() {
let hdWalletInstance;
if (this.secret.startsWith('xpub')) hdWalletInstance = new HDLegacyP2PKHWallet();
else if (this.secret.startsWith('ypub')) hdWalletInstance = new HDSegwitP2SHWallet();
else if (this.secret.startsWith('zpub')) hdWalletInstance = new HDSegwitBech32Wallet();
else return;
hdWalletInstance._xpub = this.secret;
if (this._hdWalletInstance) {
// now, porting all properties from old object to new one
for (let k of Object.keys(this._hdWalletInstance)) {
hdWalletInstance[k] = this._hdWalletInstance[k];
}
}
this._hdWalletInstance = hdWalletInstance;
}
getBalance() {
if (this._hdWalletInstance) return this._hdWalletInstance.getBalance();
return super.getBalance();
}
getTransactions() {
if (this._hdWalletInstance) return this._hdWalletInstance.getTransactions();
return super.getTransactions();
}
async fetchBalance() {
if (this.secret.startsWith('xpub') || this.secret.startsWith('ypub') || this.secret.startsWith('zpub')) {
if (!this._hdWalletInstance) this.init();
return this._hdWalletInstance.fetchBalance();
} else {
// return LegacyWallet.prototype.fetchBalance.call(this);
return super.fetchBalance();
}
}
async fetchTransactions() {
if (this.secret.startsWith('xpub') || this.secret.startsWith('ypub') || this.secret.startsWith('zpub')) {
if (!this._hdWalletInstance) this.init();
return this._hdWalletInstance.fetchTransactions();
} else {
// return LegacyWallet.prototype.fetchBalance.call(this);
return super.fetchTransactions();
}
}
async getAddressAsync() {
if (this._hdWalletInstance) return this._hdWalletInstance.getAddressAsync();
throw new Error('Not initialized');
}
}

View file

@ -1,5 +1,5 @@
import Frisbee from 'frisbee';
import { AsyncStorage } from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';
import { AppStorage } from './class';
import { FiatUnit } from './models/fiatUnit';
let BigNumber = require('bignumber.js');

4
edit-version-number.sh Executable file
View file

@ -0,0 +1,4 @@
vim ios/BlueWallet/Info.plist
vim ios/BlueWalletWatch/Info.plist
vim "ios/BlueWalletWatch Extension/Info.plist"
vim android/app/build.gradle

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1010"
LastUpgradeVersion = "1020"
version = "1.3">
<BuildAction
parallelizeBuildables = "NO"

View file

@ -1,25 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1010"
LastUpgradeVersion = "1020"
version = "1.3">
<BuildAction
parallelizeBuildables = "NO"
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "83CBBA2D1A601D0E00E9B192"
BuildableName = "libReact.a"
BlueprintName = "React"
ReferencedContainer = "container:../node_modules/react-native/React/React.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
@ -34,20 +20,6 @@
ReferencedContainer = "container:BlueWallet.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
BuildableName = "BlueWalletTests.xctest"
BlueprintName = "BlueWalletTests"
ReferencedContainer = "container:BlueWallet.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction

View file

@ -0,0 +1,131 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B40D4E2F225841EC00428FCC"
BuildableName = "BlueWalletWatch.app"
BlueprintName = "BlueWalletWatch"
ReferencedContainer = "container:BlueWallet.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "BlueWallet.app"
BlueprintName = "BlueWallet"
ReferencedContainer = "container:BlueWallet.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B40D4E2F225841EC00428FCC"
BuildableName = "BlueWalletWatch.app"
BlueprintName = "BlueWalletWatch"
ReferencedContainer = "container:BlueWallet.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "8"
notificationPayloadFile = "BlueWalletWatch Extension/PushNotificationPayload.apns">
<RemoteRunnable
runnableDebuggingMode = "2"
BundleIdentifier = "com.apple.Carousel"
RemotePath = "/BlueWallet">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B40D4E2F225841EC00428FCC"
BuildableName = "BlueWalletWatch.app"
BlueprintName = "BlueWalletWatch"
ReferencedContainer = "container:BlueWallet.xcodeproj">
</BuildableReference>
</RemoteRunnable>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B40D4E2F225841EC00428FCC"
BuildableName = "BlueWalletWatch.app"
BlueprintName = "BlueWalletWatch"
ReferencedContainer = "container:BlueWallet.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
launchAutomaticallySubstyle = "8"
notificationPayloadFile = "BlueWalletWatch Extension/PushNotificationPayload.apns">
<RemoteRunnable
runnableDebuggingMode = "2"
BundleIdentifier = "com.apple.Carousel"
RemotePath = "/BlueWallet">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B40D4E2F225841EC00428FCC"
BuildableName = "BlueWalletWatch.app"
BlueprintName = "BlueWalletWatch"
ReferencedContainer = "container:BlueWallet.xcodeproj">
</BuildableReference>
</RemoteRunnable>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B40D4E2F225841EC00428FCC"
BuildableName = "BlueWalletWatch.app"
BlueprintName = "BlueWalletWatch"
ReferencedContainer = "container:BlueWallet.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -0,0 +1,128 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B40D4E2F225841EC00428FCC"
BuildableName = "BlueWalletWatch.app"
BlueprintName = "BlueWalletWatch"
ReferencedContainer = "container:BlueWallet.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "BlueWallet.app"
BlueprintName = "BlueWallet"
ReferencedContainer = "container:BlueWallet.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B40D4E2F225841EC00428FCC"
BuildableName = "BlueWalletWatch.app"
BlueprintName = "BlueWalletWatch"
ReferencedContainer = "container:BlueWallet.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
notificationPayloadFile = "BlueWalletWatch Extension/PushNotificationPayload.apns">
<RemoteRunnable
runnableDebuggingMode = "2"
BundleIdentifier = "com.apple.Carousel"
RemotePath = "/BlueWallet">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B40D4E2F225841EC00428FCC"
BuildableName = "BlueWalletWatch.app"
BlueprintName = "BlueWalletWatch"
ReferencedContainer = "container:BlueWallet.xcodeproj">
</BuildableReference>
</RemoteRunnable>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B40D4E2F225841EC00428FCC"
BuildableName = "BlueWalletWatch.app"
BlueprintName = "BlueWalletWatch"
ReferencedContainer = "container:BlueWallet.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<RemoteRunnable
runnableDebuggingMode = "2"
BundleIdentifier = "com.apple.Carousel"
RemotePath = "/BlueWallet">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B40D4E2F225841EC00428FCC"
BuildableName = "BlueWalletWatch.app"
BlueprintName = "BlueWalletWatch"
ReferencedContainer = "container:BlueWallet.xcodeproj">
</BuildableReference>
</RemoteRunnable>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B40D4E2F225841EC00428FCC"
BuildableName = "BlueWalletWatch.app"
BlueprintName = "BlueWalletWatch"
ReferencedContainer = "container:BlueWallet.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -4,16 +4,59 @@
<dict>
<key>SchemeUserState</key>
<dict>
<key>BlueWallet-tvOS.xcscheme_^#shared#^_</key>
<key>BlueWallet for Apple Watch (Notification).xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
<integer>78</integer>
</dict>
<key>BlueWallet.xcscheme_^#shared#^_</key>
<key>BlueWallet for Apple Watch.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>71</integer>
</dict>
<key>BlueWallet-tvOS.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
<key>BlueWallet.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
</dict>
<key>BlueWalletWatch (Glance).xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>14</integer>
</dict>
<key>BlueWalletWatch (Notification).xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>3</integer>
</dict>
<key>BlueWalletWatch.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>
<dict>
<key>00E356ED1AD99517003FC87E</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>13B07F861A680F5B00A75B9A</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>B40D4E2F225841EC00428FCC</key>
<dict>
<key>primary</key>
<true/>
</dict>
</dict>
</dict>
</plist>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:BlueWallet.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
<FileRef
location = "group:../node_modules/react-native-tcp/ios/TcpSockets.xcodeproj">
</FileRef>
<FileRef
location = "group:../node_modules/@remobile/react-native-qrcode-local-image/ios/RCTQRCodeLocalImage.xcodeproj">
</FileRef>
<FileRef
location = "group:../node_modules/react-native-privacy-snapshot/RCTPrivacySnapshot.xcodeproj">
</FileRef>
</Workspace>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View file

@ -6,9 +6,13 @@
*/
#import <UIKit/UIKit.h>
@import WatchConnectivity;
@class WatchBridge;
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@interface AppDelegate : UIResponder <UIApplicationDelegate, WCSessionDelegate>
@property (nonatomic, strong) UIWindow *window;
@property(nonatomic, strong) WatchBridge *watchBridge;
@property(nonatomic, strong) WCSession *session;
@end

View file

@ -15,6 +15,7 @@
#else
#import "RNSentry.h" // This is used for versions of react < 0.40
#endif
#import "WatchBridge.h"
@implementation AppDelegate
@ -35,6 +36,11 @@
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
self.watchBridge = [WatchBridge shared];
self.session = self.watchBridge.session;
[self.session activateSession];
self.session.delegate = self;
return YES;
}
@ -46,4 +52,18 @@
return NO;
}
- (void)sessionDidDeactivate:(WCSession *)session {
[session activateSession];
}
- (void)session:(nonnull WCSession *)session activationDidCompleteWithState:(WCSessionActivationState)activationState error:(nullable NSError *)error {
}
- (void)sessionDidBecomeInactive:(nonnull WCSession *)session {
}
@end

View file

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>3.9.4</string>
<string>4.0.3</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@ -29,6 +29,9 @@
<array>
<string>bitcoin</string>
<string>lightning</string>
<string>bluewallet</string>
<string>lapp</string>
<string>blue</string>
</array>
</dict>
</array>

View file

@ -0,0 +1,56 @@
//
// ExtensionDelegate.swift
// BlueWalletWatch Extension
//
// Created by Marcos Rodriguez on 3/6/19.
// Copyright © 2019 Facebook. All rights reserved.
//
import WatchKit
class ExtensionDelegate: NSObject, WKExtensionDelegate {
func applicationDidFinishLaunching() {
// Perform any final initialization of your application.
}
func applicationDidBecomeActive() {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillResignActive() {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, etc.
}
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
// Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one.
for task in backgroundTasks {
// Use a switch statement to check the task type
switch task {
case let backgroundTask as WKApplicationRefreshBackgroundTask:
// Be sure to complete the background task once youre done.
backgroundTask.setTaskCompletedWithSnapshot(false)
case let snapshotTask as WKSnapshotRefreshBackgroundTask:
// Snapshot tasks have a unique completion call, make sure to set your expiration date
snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil)
case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask:
// Be sure to complete the connectivity task once youre done.
connectivityTask.setTaskCompletedWithSnapshot(false)
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
// Be sure to complete the URL session task once youre done.
urlSessionTask.setTaskCompletedWithSnapshot(false)
case let relevantShortcutTask as WKRelevantShortcutRefreshBackgroundTask:
// Be sure to complete the relevant-shortcut task once you're done.
relevantShortcutTask.setTaskCompletedWithSnapshot(false)
case let intentDidRunTask as WKIntentDidRunRefreshBackgroundTask:
// Be sure to complete the intent-did-run task once you're done.
intentDidRunTask.setTaskCompletedWithSnapshot(false)
default:
// make sure to complete unhandled task types
task.setTaskCompletedWithSnapshot(false)
}
}
}
}

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>BlueWalletWatch Extension</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>4.0.3</string>
<key>CFBundleVersion</key>
<string>239</string>
<key>LSApplicationCategoryType</key>
<string></string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>WKAppBundleIdentifier</key>
<string>io.bluewallet.bluewallet.watch</string>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.watchkit</string>
</dict>
<key>WKExtensionDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).ExtensionDelegate</string>
</dict>
</plist>

View file

@ -0,0 +1,57 @@
//
// InterfaceController.swift
// BlueWalletWatch Extension
//
// Created by Marcos Rodriguez on 3/6/19.
// Copyright © 2019 Facebook. All rights reserved.
//
import WatchKit
import WatchConnectivity
import Foundation
class InterfaceController: WKInterfaceController {
@IBOutlet weak var walletsTable: WKInterfaceTable!
@IBOutlet weak var loadingIndicatorGroup: WKInterfaceGroup!
@IBOutlet weak var noWalletsAvailableLabel: WKInterfaceLabel!
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
WCSession.default.sendMessage(["message" : "sendApplicationContext"], replyHandler: nil, errorHandler: nil)
if (WatchDataSource.shared.wallets.isEmpty) {
loadingIndicatorGroup.setHidden(true)
noWalletsAvailableLabel.setHidden(false)
} else {
processWalletsTable()
}
NotificationCenter.default.addObserver(self, selector: #selector(processWalletsTable), name: WatchDataSource.NotificationName.dataUpdated, object: nil)
}
@objc private func processWalletsTable() {
loadingIndicatorGroup.setHidden(false)
walletsTable.setHidden(true)
walletsTable.setNumberOfRows(WatchDataSource.shared.wallets.count, withRowType: WalletInformation.identifier)
for index in 0..<walletsTable.numberOfRows {
guard let controller = walletsTable.rowController(at: index) as? WalletInformation else { continue }
let wallet = WatchDataSource.shared.wallets[index]
if wallet.identifier == nil {
WatchDataSource.shared.wallets[index].identifier = index
}
controller.name = wallet.label
controller.balance = wallet.balance
controller.type = WalletGradient(rawValue: wallet.type) ?? .SegwitHD
}
loadingIndicatorGroup.setHidden(true)
noWalletsAvailableLabel.setHidden(!WatchDataSource.shared.wallets.isEmpty)
walletsTable.setHidden(WatchDataSource.shared.wallets.isEmpty)
}
override func contextForSegue(withIdentifier segueIdentifier: String, in table: WKInterfaceTable, rowIndex: Int) -> Any? {
return rowIndex;
}
}

View file

@ -0,0 +1,38 @@
//
// NotificationController.swift
// BlueWalletWatch Extension
//
// Created by Marcos Rodriguez on 3/6/19.
// Copyright © 2019 Facebook. All rights reserved.
//
import WatchKit
import Foundation
import UserNotifications
class NotificationController: WKUserNotificationInterfaceController {
override init() {
// Initialize variables here.
super.init()
// Configure interface objects here.
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
override func didReceive(_ notification: UNNotification) {
// This method is called when a notification needs to be presented.
// Implement it if you use a dynamic notification interface.
// Populate your dynamic notification interface as quickly as possible.
}
}

View file

@ -0,0 +1,155 @@
//
// NumericKeypadInterfaceController.swift
// BlueWalletWatch Extension
//
// Created by Marcos Rodriguez on 3/23/19.
// Copyright © 2019 Facebook. All rights reserved.
//
import WatchKit
import Foundation
class NumericKeypadInterfaceController: WKInterfaceController {
static let identifier = "NumericKeypadInterfaceController"
private var amount: [String] = ["0"]
var keyPadType: NumericKeypadType = .BTC
struct NotificationName {
static let keypadDataChanged = Notification.Name(rawValue: "Notification.NumericKeypadInterfaceController.keypadDataChanged")
}
struct Notifications {
static let keypadDataChanged = Notification(name: NotificationName.keypadDataChanged)
}
enum NumericKeypadType: String {
case BTC = "BTC"
case SATS = "sats"
}
@IBOutlet weak var periodButton: WKInterfaceButton!
override func awake(withContext context: Any?) {
super.awake(withContext: context)
if let context = context as? SpecifyInterfaceController.SpecificQRCodeContent {
amount = context.amountStringArray
keyPadType = context.bitcoinUnit
}
periodButton.setEnabled(keyPadType == .SATS)
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
updateTitle()
}
private func updateTitle() {
var title = ""
for amount in self.amount {
let isValid = Double(amount)
if amount == "." || isValid != nil {
title.append(String(amount))
}
}
if title.isEmpty {
title = "0"
}
setTitle("< \(title) \(keyPadType)")
NotificationCenter.default.post(name: NotificationName.keypadDataChanged, object: amount)
}
private func append(value: String) {
guard amount.filter({$0 != "."}).count <= 9 && !(amount.contains(".") && value == ".") else {
return
}
switch keyPadType {
case .SATS:
if amount.first == "0" {
if value == "0" {
return
}
amount[0] = value
} else {
amount.append(value)
}
case .BTC:
if amount.isEmpty {
if (value == "0") {
amount.append("0")
} else if value == "." && !amount.contains(".") {
amount.append("0")
amount.append(".")
} else {
amount.append(value)
}
} else if let first = amount.first, first == "0" {
if amount.count > 1, amount[1] != "." {
amount.insert(".", at: 1)
} else if amount.count == 1, amount.first == "0" && value != "." {
amount.append(".")
amount.append(value)
} else {
amount.append(value)
}
} else {
amount.append(value)
}
}
updateTitle()
}
@IBAction func keypadNumberOneTapped() {
append(value: "1")
}
@IBAction func keypadNumberTwoTapped() {
append(value: "2")
}
@IBAction func keypadNumberThreeTapped() {
append(value: "3")
}
@IBAction func keypadNumberFourTapped() {
append(value: "4")
}
@IBAction func keypadNumberFiveTapped() {
append(value: "5")
}
@IBAction func keypadNumberSixTapped() {
append(value: "6")
}
@IBAction func keypadNumberSevenTapped() {
append(value: "7")
}
@IBAction func keypadNumberEightTapped() {
append(value: "8")
}
@IBAction func keypadNumberNineTapped() {
append(value: "9")
}
@IBAction func keypadNumberZeroTapped() {
append(value: "0")
}
@IBAction func keypadNumberDotTapped() {
guard !amount.contains("."), keyPadType == .BTC else { return }
append(value: ".")
}
@IBAction func keypadNumberRemoveTapped() {
guard !amount.isEmpty else {
setTitle("< 0 \(keyPadType)")
return
}
amount.removeLast()
updateTitle()
}
}

View file

@ -0,0 +1,39 @@
//
// Wallet.swift
// BlueWalletWatch Extension
//
// Created by Marcos Rodriguez on 3/13/19.
// Copyright © 2019 Facebook. All rights reserved.
//
import Foundation
class Transaction: NSObject, NSCoding {
static let identifier: String = "Transaction"
let time: String
let memo: String
let amount: String
let type: String
init(time: String, memo: String, type: String, amount: String) {
self.time = time
self.memo = memo
self.type = type
self.amount = amount
}
func encode(with aCoder: NSCoder) {
aCoder.encode(time, forKey: "time")
aCoder.encode(memo, forKey: "memo")
aCoder.encode(type, forKey: "type")
aCoder.encode(amount, forKey: "amount")
}
required init?(coder aDecoder: NSCoder) {
time = aDecoder.decodeObject(forKey: "time") as! String
memo = aDecoder.decodeObject(forKey: "memo") as! String
amount = aDecoder.decodeObject(forKey: "amount") as! String
type = aDecoder.decodeObject(forKey: "type") as! String
}
}

View file

@ -0,0 +1,52 @@
//
// TransactionTableRow.swift
// BlueWalletWatch Extension
//
// Created by Marcos Rodriguez on 3/10/19.
// Copyright © 2019 Facebook. All rights reserved.
//
import WatchKit
class TransactionTableRow: NSObject {
@IBOutlet private weak var transactionAmountLabel: WKInterfaceLabel!
@IBOutlet private weak var transactionMemoLabel: WKInterfaceLabel!
@IBOutlet private weak var transactionTimeLabel: WKInterfaceLabel!
@IBOutlet private weak var transactionTypeImage: WKInterfaceImage!
static let identifier: String = "TransactionTableRow"
var amount: String = "" {
willSet {
transactionAmountLabel.setText(newValue)
}
}
var memo: String = "" {
willSet {
transactionMemoLabel.setText(newValue)
}
}
var time: String = "" {
willSet {
transactionTimeLabel.setText(newValue)
}
}
var type: String = "" {
willSet {
if (newValue == "pendingConfirmation") {
transactionTypeImage.setImage(UIImage(named: "pendingConfirmation"))
} else if (newValue == "received") {
transactionTypeImage.setImage(UIImage(named: "receivedArrow"))
} else if (newValue == "sent") {
transactionTypeImage.setImage(UIImage(named: "sentArrow"))
} else {
transactionTypeImage.setImage(nil)
}
}
}
}

View file

@ -0,0 +1,51 @@
//
// Wallet.swift
// BlueWalletWatch Extension
//
// Created by Marcos Rodriguez on 3/13/19.
// Copyright © 2019 Facebook. All rights reserved.
//
import Foundation
class Wallet: NSObject, NSCoding {
static let identifier: String = "Wallet"
var identifier: Int?
let label: String
let balance: String
let type: String
let preferredBalanceUnit: String
let receiveAddress: String
let transactions: [Transaction]
init(label: String, balance: String, type: String, preferredBalanceUnit: String, receiveAddress: String, transactions: [Transaction], identifier: Int) {
self.label = label
self.balance = balance
self.type = type
self.preferredBalanceUnit = preferredBalanceUnit
self.receiveAddress = receiveAddress
self.transactions = transactions
self.identifier = identifier
}
func encode(with aCoder: NSCoder) {
aCoder.encode(label, forKey: "label")
aCoder.encode(balance, forKey: "balance")
aCoder.encode(type, forKey: "type")
aCoder.encode(receiveAddress, forKey: "receiveAddress")
aCoder.encode(preferredBalanceUnit, forKey: "preferredBalanceUnit")
aCoder.encode(transactions, forKey: "transactions")
aCoder.encode(identifier, forKey: "identifier")
}
required init?(coder aDecoder: NSCoder) {
label = aDecoder.decodeObject(forKey: "label") as! String
balance = aDecoder.decodeObject(forKey: "balance") as! String
type = aDecoder.decodeObject(forKey: "type") as! String
preferredBalanceUnit = aDecoder.decodeObject(forKey: "preferredBalanceUnit") as! String
receiveAddress = aDecoder.decodeObject(forKey: "receiveAddress") as! String
transactions = aDecoder.decodeObject(forKey: "transactions") as? [Transaction] ?? [Transaction]()
}
}

View file

@ -0,0 +1,32 @@
//
// WalletGradient.swift
// BlueWalletWatch Extension
//
// Created by Marcos Rodriguez on 3/23/19.
// Copyright © 2019 Facebook. All rights reserved.
//
import Foundation
enum WalletGradient: String {
case SegwitHD = "HDsegwitP2SH"
case Segwit = "segwitP2SH"
case LightningCustodial = "lightningCustodianWallet"
case ACINQStrike = "LightningACINQ"
case WatchOnly = "watchOnly"
var imageString: String{
switch self {
case .Segwit:
return "wallet"
case .ACINQStrike:
return "walletACINQ"
case .SegwitHD:
return "walletHD"
case .WatchOnly:
return "walletWatchOnly"
case .LightningCustodial:
return "walletLightningCustodial"
}
}
}

View file

@ -0,0 +1,36 @@
//
// WalletInformation.swift
// BlueWalletWatch Extension
//
// Created by Marcos Rodriguez on 3/10/19.
// Copyright © 2019 Facebook. All rights reserved.
//
import WatchKit
class WalletInformation: NSObject {
@IBOutlet private weak var walletBalanceLabel: WKInterfaceLabel!
@IBOutlet private weak var walletNameLabel: WKInterfaceLabel!
@IBOutlet private weak var walletGroup: WKInterfaceGroup!
static let identifier: String = "WalletInformation"
var name: String = "" {
willSet {
walletNameLabel.setText(newValue)
}
}
var balance: String = "" {
willSet {
walletBalanceLabel.setText(newValue)
}
}
var type: WalletGradient = .SegwitHD {
willSet {
walletGroup.setBackgroundImageNamed(newValue.imageString)
}
}
}

View file

@ -0,0 +1,103 @@
//
// WatchDataSource.swift
// BlueWalletWatch Extension
//
// Created by Marcos Rodriguez on 3/20/19.
// Copyright © 2019 Facebook. All rights reserved.
//
import Foundation
import WatchConnectivity
class WatchDataSource: NSObject, WCSessionDelegate {
struct NotificationName {
static let dataUpdated = Notification.Name(rawValue: "Notification.WalletDataSource.Updated")
}
struct Notifications {
static let dataUpdated = Notification(name: NotificationName.dataUpdated)
}
static let shared = WatchDataSource()
var wallets: [Wallet] = [Wallet]()
private let keychain = KeychainSwift()
override init() {
super.init()
if WCSession.isSupported() {
print("Activating watch session")
WCSession.default.delegate = self
WCSession.default.activate()
}
}
func processWalletsData(walletsInfo: [String: Any]) {
if let walletsToProcess = walletsInfo["wallets"] as? [[String: Any]] {
wallets.removeAll();
for (index, entry) in walletsToProcess.enumerated() {
guard let label = entry["label"] as? String, let balance = entry["balance"] as? String, let type = entry["type"] as? String, let preferredBalanceUnit = entry["preferredBalanceUnit"] as? String, let receiveAddress = entry["receiveAddress"] as? String, let transactions = entry["transactions"] as? [[String: Any]] else {
continue
}
var transactionsProcessed = [Transaction]()
for transactionEntry in transactions {
guard let time = transactionEntry["time"] as? String, let memo = transactionEntry["memo"] as? String, let amount = transactionEntry["amount"] as? String, let type = transactionEntry["type"] as? String else { continue }
let transaction = Transaction(time: time, memo: memo, type: type, amount: amount)
transactionsProcessed.append(transaction)
}
let wallet = Wallet(label: label, balance: balance, type: type, preferredBalanceUnit: preferredBalanceUnit, receiveAddress: receiveAddress, transactions: transactionsProcessed, identifier: index)
wallets.append(wallet)
}
if let walletsArchived = try? NSKeyedArchiver.archivedData(withRootObject: wallets, requiringSecureCoding: false) {
keychain.set(walletsArchived, forKey: Wallet.identifier)
}
WatchDataSource.postDataUpdatedNotification()
}
}
static func postDataUpdatedNotification() {
NotificationCenter.default.post(Notifications.dataUpdated)
}
static func requestLightningInvoice(walletIdentifier: Int, amount: Double, description: String?, responseHandler: @escaping (_ invoice: String) -> Void) {
guard WatchDataSource.shared.wallets.count > walletIdentifier else {
responseHandler("")
return
}
WCSession.default.sendMessage(["request": "createInvoice", "walletIndex": walletIdentifier, "amount": amount, "description": description ?? ""], replyHandler: { (reply: [String : Any]) in
if let invoicePaymentRequest = reply["invoicePaymentRequest"] as? String, !invoicePaymentRequest.isEmpty {
responseHandler(invoicePaymentRequest)
} else {
responseHandler("")
}
}) { (error) in
print(error)
responseHandler("")
}
}
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
WatchDataSource.shared.processWalletsData(walletsInfo: applicationContext)
}
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
WatchDataSource.shared.processWalletsData(walletsInfo: applicationContext)
}
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
// WatchDataSource.shared.processWalletsData(walletsInfo: userInfo)
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
if activationState == .activated {
WCSession.default.sendMessage([:], replyHandler: nil, errorHandler: nil)
if let existingData = keychain.getData(Wallet.identifier), let walletData = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(existingData) as? [Wallet] {
guard let walletData = walletData, walletData != self.wallets else { return }
wallets = walletData
WatchDataSource.postDataUpdatedNotification()
}
}
}
}

View file

@ -0,0 +1,20 @@
{
"aps": {
"alert": {
"body": "Test message",
"title": "Optional title",
"subtitle": "Optional subtitle"
},
"category": "myCategory",
"thread-id":"5280"
},
"WatchKit Simulator Actions": [
{
"title": "First Button",
"identifier": "firstButtonAction"
}
],
"customKey": "Use this file to define a testing payload for your notifications. The aps dictionary specifies the category, alert text and title. The WatchKit Simulator Actions array can provide info for one or more action buttons in addition to the standard Dismiss button. Any other top level keys are custom payload. If you have multiple such JSON files in your project, you'll be able to select them when choosing to debug the notification interface of your Watch App."
}

View file

@ -0,0 +1,115 @@
//
// ReceiveInterfaceController.swift
// BlueWalletWatch Extension
//
// Created by Marcos Rodriguez on 3/12/19.
// Copyright © 2019 Facebook. All rights reserved.
//
import WatchKit
import Foundation
import EFQRCode
class ReceiveInterfaceController: WKInterfaceController {
static let identifier = "ReceiveInterfaceController"
@IBOutlet weak var imageInterface: WKInterfaceImage!
private var wallet: Wallet?
private var isRenderingQRCode: Bool?
@IBOutlet weak var loadingIndicator: WKInterfaceGroup!
override func awake(withContext context: Any?) {
super.awake(withContext: context)
guard let identifier = context as? Int, WatchDataSource.shared.wallets.count > identifier else {
pop()
return
}
let wallet = WatchDataSource.shared.wallets[identifier]
self.wallet = wallet
NotificationCenter.default.addObserver(forName: SpecifyInterfaceController.NotificationName.createQRCode, object: nil, queue: nil) { [weak self] (notification) in
self?.isRenderingQRCode = true
if let wallet = self?.wallet, wallet.type == "lightningCustodianWallet", let object = notification.object as? SpecifyInterfaceController.SpecificQRCodeContent, let amount = object.amount {
self?.imageInterface.setHidden(true)
self?.loadingIndicator.setHidden(false)
WatchDataSource.requestLightningInvoice(walletIdentifier: identifier, amount: amount, description: object.description, responseHandler: { (invoice) in
DispatchQueue.main.async {
if (!invoice.isEmpty) {
guard let cgImage = EFQRCode.generate(
content: "lightning:\(invoice)") else {
return
}
let image = UIImage(cgImage: cgImage)
self?.loadingIndicator.setHidden(true)
self?.imageInterface.setHidden(false)
self?.imageInterface.setImage(nil)
self?.imageInterface.setImage(image)
} else {
self?.pop()
self?.presentAlert(withTitle: "Error", message: "Unable to create invoice. Please, make sure your iPhone is paired and nearby.", preferredStyle: .alert, actions: [WKAlertAction(title: "OK", style: .default, handler: { [weak self] in
self?.dismiss()
})])
}
}
})
} else {
guard let notificationObject = notification.object as? SpecifyInterfaceController.SpecificQRCodeContent, let walletContext = self?.wallet, !walletContext.receiveAddress.isEmpty, let receiveAddress = self?.wallet?.receiveAddress else { return }
var address = "bitcoin:\(receiveAddress)"
var hasAmount = false
if let amount = notificationObject.amount {
address.append("?amount=\(amount)&")
hasAmount = true
}
if let description = notificationObject.description {
if (!hasAmount) {
address.append("?")
}
address.append("label=\(description)")
}
DispatchQueue.main.async {
guard let cgImage = EFQRCode.generate(
content: address) else {
return
}
let image = UIImage(cgImage: cgImage)
self?.imageInterface.setImage(nil)
self?.imageInterface.setImage(image)
self?.imageInterface.setHidden(false)
self?.loadingIndicator.setHidden(true)
self?.isRenderingQRCode = false
}
}
}
guard !wallet.receiveAddress.isEmpty, let cgImage = EFQRCode.generate(
content: wallet.receiveAddress) else {
return
}
let image = UIImage(cgImage: cgImage)
imageInterface.setImage(image)
}
override func didAppear() {
super.didAppear()
if wallet?.type == "lightningCustodianWallet" {
if isRenderingQRCode == nil {
presentController(withName: SpecifyInterfaceController.identifier, context: wallet?.identifier)
isRenderingQRCode = false
} else if isRenderingQRCode == false {
pop()
}
}
}
override func didDeactivate() {
super.didDeactivate()
NotificationCenter.default.removeObserver(self, name: SpecifyInterfaceController.NotificationName.createQRCode, object: nil)
}
@IBAction func specifyMenuItemTapped() {
presentController(withName: SpecifyInterfaceController.identifier, context: wallet?.identifier)
}
}

View file

@ -0,0 +1,90 @@
//
// SpecifyInterfaceController.swift
// BlueWalletWatch Extension
//
// Created by Marcos Rodriguez on 3/23/19.
// Copyright © 2019 Facebook. All rights reserved.
//
import WatchKit
import Foundation
class SpecifyInterfaceController: WKInterfaceController {
static let identifier = "SpecifyInterfaceController"
@IBOutlet weak var descriptionButton: WKInterfaceButton!
@IBOutlet weak var amountButton: WKInterfaceButton!
struct SpecificQRCodeContent {
var amount: Double?
var description: String?
var amountStringArray: [String] = ["0"]
var bitcoinUnit: NumericKeypadInterfaceController.NumericKeypadType = .BTC
}
var specifiedQRContent: SpecificQRCodeContent = SpecificQRCodeContent(amount: nil, description: nil, amountStringArray: ["0"], bitcoinUnit: .BTC)
var wallet: Wallet?
struct NotificationName {
static let createQRCode = Notification.Name(rawValue: "Notification.SpecifyInterfaceController.createQRCode")
}
struct Notifications {
static let createQRCode = Notification(name: NotificationName.createQRCode)
}
override func awake(withContext context: Any?) {
super.awake(withContext: context)
guard let identifier = context as? Int, WatchDataSource.shared.wallets.count > identifier else {
return
}
let wallet = WatchDataSource.shared.wallets[identifier]
self.wallet = wallet
self.specifiedQRContent.bitcoinUnit = wallet.type == "lightningCustodianWallet" ? .SATS : .BTC
NotificationCenter.default.addObserver(forName: NumericKeypadInterfaceController.NotificationName.keypadDataChanged, object: nil, queue: nil) { [weak self] (notification) in
guard let amountObject = notification.object as? [String], !amountObject.isEmpty else { return }
if amountObject.count == 1 && (amountObject.first == "." || amountObject.first == "0") {
return
}
var title = ""
for amount in amountObject {
let isValid = Double(amount)
if amount == "." || isValid != nil {
title.append(String(amount))
}
}
self?.specifiedQRContent.amountStringArray = amountObject
if let amountDouble = Double(title), let keyPadType = self?.specifiedQRContent.bitcoinUnit {
self?.specifiedQRContent.amount = amountDouble
self?.amountButton.setTitle("\(title) \(keyPadType)")
}
}
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
NotificationCenter.default.removeObserver(self, name: NumericKeypadInterfaceController.NotificationName.keypadDataChanged, object: nil)
}
@IBAction func descriptionButtonTapped() {
presentTextInputController(withSuggestions: nil, allowedInputMode: .allowEmoji) { [weak self] (result: [Any]?) in
DispatchQueue.main.async {
if let result = result, let text = result.first as? String {
self?.specifiedQRContent.description = text
self?.descriptionButton.setTitle(nil)
self?.descriptionButton.setTitle(text)
}
}
}
}
@IBAction func createButtonTapped() {
NotificationCenter.default.post(name: NotificationName.createQRCode, object: specifiedQRContent)
dismiss()
}
override func contextForSegue(withIdentifier segueIdentifier: String) -> Any? {
if segueIdentifier == NumericKeypadInterfaceController.identifier {
return specifiedQRContent
}
return nil
}
}

View file

@ -0,0 +1,68 @@
//
// WalletDetailsInterfaceController.swift
// BlueWalletWatch Extension
//
// Created by Marcos Rodriguez on 3/11/19.
// Copyright © 2019 Facebook. All rights reserved.
//
import WatchKit
import Foundation
class WalletDetailsInterfaceController: WKInterfaceController {
var wallet: Wallet?
static let identifier = "WalletDetailsInterfaceController"
@IBOutlet weak var walletBasicsGroup: WKInterfaceGroup!
@IBOutlet weak var walletBalanceLabel: WKInterfaceLabel!
@IBOutlet weak var walletNameLabel: WKInterfaceLabel!
@IBOutlet weak var receiveButton: WKInterfaceButton!
@IBOutlet weak var noTransactionsLabel: WKInterfaceLabel!
@IBOutlet weak var transactionsTable: WKInterfaceTable!
override func awake(withContext context: Any?) {
super.awake(withContext: context)
guard let identifier = context as? Int else {
pop()
return
}
let wallet = WatchDataSource.shared.wallets[identifier]
self.wallet = wallet
walletBalanceLabel.setText(wallet.balance)
walletNameLabel.setText(wallet.label)
walletBasicsGroup.setBackgroundImageNamed(WalletGradient(rawValue: wallet.type)?.imageString)
processWalletsTable()
}
override func willActivate() {
super.willActivate()
transactionsTable.setHidden(wallet?.transactions.isEmpty ?? true)
noTransactionsLabel.setHidden(!(wallet?.transactions.isEmpty ?? false))
}
@IBAction func receiveMenuItemTapped() {
presentController(withName: ReceiveInterfaceController.identifier, context: wallet)
}
@objc private func processWalletsTable() {
transactionsTable.setNumberOfRows(wallet?.transactions.count ?? 0, withRowType: TransactionTableRow.identifier)
for index in 0..<transactionsTable.numberOfRows {
guard let controller = transactionsTable.rowController(at: index) as? TransactionTableRow, let transaction = wallet?.transactions[index] else { continue }
controller.amount = transaction.amount
controller.type = transaction.type
controller.memo = transaction.memo
controller.time = transaction.time
}
transactionsTable.setHidden(wallet?.transactions.isEmpty ?? true)
noTransactionsLabel.setHidden(!(wallet?.transactions.isEmpty ?? false))
}
override func contextForSegue(withIdentifier segueIdentifier: String) -> Any? {
return wallet?.identifier
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,92 @@
{
"images" : [
{
"size" : "24x24",
"idiom" : "watch",
"filename" : "Icon-48.png",
"scale" : "2x",
"role" : "notificationCenter",
"subtype" : "38mm"
},
{
"size" : "27.5x27.5",
"idiom" : "watch",
"filename" : "Icon-55.png",
"scale" : "2x",
"role" : "notificationCenter",
"subtype" : "42mm"
},
{
"size" : "29x29",
"idiom" : "watch",
"filename" : "58.png",
"role" : "companionSettings",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "watch",
"filename" : "87.png",
"role" : "companionSettings",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "watch",
"filename" : "watch.png",
"scale" : "2x",
"role" : "appLauncher",
"subtype" : "38mm"
},
{
"size" : "44x44",
"idiom" : "watch",
"filename" : "Icon-88.png",
"scale" : "2x",
"role" : "appLauncher",
"subtype" : "40mm"
},
{
"size" : "50x50",
"idiom" : "watch",
"filename" : "Icon-173.png",
"scale" : "2x",
"role" : "appLauncher",
"subtype" : "44mm"
},
{
"size" : "86x86",
"idiom" : "watch",
"filename" : "Icon-172.png",
"scale" : "2x",
"role" : "quickLook",
"subtype" : "38mm"
},
{
"size" : "98x98",
"idiom" : "watch",
"filename" : "Icon-196.png",
"scale" : "2x",
"role" : "quickLook",
"subtype" : "42mm"
},
{
"size" : "108x108",
"idiom" : "watch",
"filename" : "group-copy-2@3x.png",
"scale" : "2x",
"role" : "quickLook",
"subtype" : "44mm"
},
{
"size" : "1024x1024",
"idiom" : "watch-marketing",
"filename" : "1024.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -0,0 +1,13 @@
{
"images" : [
{
"idiom" : "watch",
"filename" : "group-copy-2@3x.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,13 @@
{
"images" : [
{
"idiom" : "watch",
"filename" : "shape@3x.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 B

View file

@ -0,0 +1,13 @@
{
"images" : [
{
"idiom" : "watch",
"filename" : "qr-code@3x.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View file

@ -0,0 +1,13 @@
{
"images" : [
{
"idiom" : "watch",
"filename" : "path-copy-3@2x.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 B

View file

@ -0,0 +1,13 @@
{
"images" : [
{
"idiom" : "watch",
"filename" : "path-copy@2x.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "mask.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "mask@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "mask@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "mask.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "mask@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "mask@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1,13 @@
{
"images" : [
{
"idiom" : "watch",
"filename" : "mask@3x.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Some files were not shown because too many files have changed in this diff Show more