Merge branch 'master' of https://github.com/BlueWallet/BlueWallet into pr/450
|
@ -67,4 +67,4 @@ suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
|
||||||
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
|
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
|
||||||
|
|
||||||
[version]
|
[version]
|
||||||
^0.86.0
|
^0.97.0
|
||||||
|
|
4
.gitignore
vendored
|
@ -57,4 +57,6 @@ buck-out/
|
||||||
|
|
||||||
#BlueWallet
|
#BlueWallet
|
||||||
release-notes.json
|
release-notes.json
|
||||||
release-notes.txt
|
release-notes.txt
|
||||||
|
|
||||||
|
ios/Pods/
|
||||||
|
|
101
App.js
|
@ -1,11 +1,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Linking, AppState, Clipboard, StyleSheet, KeyboardAvoidingView, Platform, View } from 'react-native';
|
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 Modal from 'react-native-modal';
|
||||||
import { NavigationActions } from 'react-navigation';
|
import { NavigationActions } from 'react-navigation';
|
||||||
import MainBottomTabs from './MainBottomTabs';
|
import MainBottomTabs from './MainBottomTabs';
|
||||||
import NavigationService from './NavigationService';
|
import NavigationService from './NavigationService';
|
||||||
import { BlueTextCentered, BlueButton } from './BlueComponents';
|
import { BlueTextCentered, BlueButton } from './BlueComponents';
|
||||||
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
||||||
|
import url from 'url';
|
||||||
|
import { AppStorage, LightningCustodianWallet } from './class';
|
||||||
const bitcoin = require('bitcoinjs-lib');
|
const bitcoin = require('bitcoinjs-lib');
|
||||||
const bitcoinModalString = 'Bitcoin address';
|
const bitcoinModalString = 'Bitcoin address';
|
||||||
const lightningModalString = 'Lightning Invoice';
|
const lightningModalString = 'Lightning Invoice';
|
||||||
|
@ -56,7 +59,13 @@ export default class App extends React.Component {
|
||||||
hasSchema(schemaString) {
|
hasSchema(schemaString) {
|
||||||
if (typeof schemaString !== 'string' || schemaString.length <= 0) return false;
|
if (typeof schemaString !== 'string' || schemaString.length <= 0) return false;
|
||||||
const lowercaseString = schemaString.trim().toLowerCase();
|
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) {
|
isBitcoinAddress(address) {
|
||||||
|
@ -86,6 +95,12 @@ export default class App extends React.Component {
|
||||||
return isValidLightningInvoice;
|
return isValidLightningInvoice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isSafelloRedirect(event) {
|
||||||
|
let urlObject = url.parse(event.url, true) // eslint-disable-line
|
||||||
|
|
||||||
|
return !!urlObject.query['safello-state-token'];
|
||||||
|
}
|
||||||
|
|
||||||
handleOpenURL = event => {
|
handleOpenURL = event => {
|
||||||
if (event.url === null) {
|
if (event.url === null) {
|
||||||
return;
|
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 = () => {
|
renderClipboardContentModal = () => {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
onModalShow={() => ReactNativeHapticFeedback.trigger('impactLight', false)}
|
onModalShow={() => ReactNativeHapticFeedback.trigger('impactLight', { ignoreAndroidSystemSettings: false })}
|
||||||
isVisible={this.state.isClipboardContentModalVisible}
|
isVisible={this.state.isClipboardContentModalVisible}
|
||||||
style={styles.bottomModal}
|
style={styles.bottomModal}
|
||||||
onBackdropPress={() => {
|
onBackdropPress={() => {
|
||||||
|
|
28
App.test.js
|
@ -5,13 +5,11 @@ import TestRenderer from 'react-test-renderer';
|
||||||
import Settings from './screen/settings/settings';
|
import Settings from './screen/settings/settings';
|
||||||
import Selftest from './screen/selftest';
|
import Selftest from './screen/selftest';
|
||||||
import { BlueHeader } from './BlueComponents';
|
import { BlueHeader } from './BlueComponents';
|
||||||
import MockStorage from './MockStorage';
|
|
||||||
import { FiatUnit } from './models/fiatUnit';
|
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
|
global.crypto = require('crypto'); // shall be used by tests under nodejs CLI, but not in RN environment
|
||||||
let assert = require('assert');
|
let assert = require('assert');
|
||||||
jest.mock('react-native-qrcode-svg', () => 'Video');
|
jest.mock('react-native-qrcode-svg', () => 'Video');
|
||||||
const AsyncStorage = new MockStorage();
|
|
||||||
jest.setMock('AsyncStorage', AsyncStorage);
|
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
jest.mock('Picker', () => {
|
jest.mock('Picker', () => {
|
||||||
// eslint-disable-next-line import/no-unresolved
|
// eslint-disable-next-line import/no-unresolved
|
||||||
|
@ -105,7 +103,6 @@ it('Selftest work', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Appstorage - loadFromDisk works', async () => {
|
it('Appstorage - loadFromDisk works', async () => {
|
||||||
AsyncStorage.storageCache = {}; // cleanup from other tests
|
|
||||||
/** @type {AppStorage} */
|
/** @type {AppStorage} */
|
||||||
let Storage = new AppStorage();
|
let Storage = new AppStorage();
|
||||||
let w = new SegwitP2SHWallet();
|
let w = new SegwitP2SHWallet();
|
||||||
|
@ -125,16 +122,14 @@ it('Appstorage - loadFromDisk works', async () => {
|
||||||
|
|
||||||
// emulating encrypted storage (and testing flag)
|
// emulating encrypted storage (and testing flag)
|
||||||
|
|
||||||
AsyncStorage.storageCache.data = false;
|
await AsyncStorage.setItem('data', false);
|
||||||
AsyncStorage.storageCache.data_encrypted = '1'; // flag
|
await AsyncStorage.setItem(AppStorage.FLAG_ENCRYPTED, '1');
|
||||||
let Storage3 = new AppStorage();
|
let Storage3 = new AppStorage();
|
||||||
isEncrypted = await Storage3.storageIsEncrypted();
|
isEncrypted = await Storage3.storageIsEncrypted();
|
||||||
assert.ok(isEncrypted);
|
assert.ok(isEncrypted);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Appstorage - encryptStorage & load encrypted storage works', async () => {
|
it('Appstorage - encryptStorage & load encrypted storage works', async () => {
|
||||||
AsyncStorage.storageCache = {}; // cleanup from other tests
|
|
||||||
|
|
||||||
/** @type {AppStorage} */
|
/** @type {AppStorage} */
|
||||||
let Storage = new AppStorage();
|
let Storage = new AppStorage();
|
||||||
let w = new SegwitP2SHWallet();
|
let w = new SegwitP2SHWallet();
|
||||||
|
@ -228,6 +223,12 @@ it('Wallet can fetch UTXO', async () => {
|
||||||
assert.ok(w.utxo.length > 0, 'unexpected empty UTXO');
|
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 () => {
|
it('Wallet can fetch balance', async () => {
|
||||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000;
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000;
|
||||||
let w = new LegacyWallet();
|
let w = new LegacyWallet();
|
||||||
|
@ -236,7 +237,7 @@ it('Wallet can fetch balance', async () => {
|
||||||
assert.ok(w.getUnconfirmedBalance() === 0);
|
assert.ok(w.getUnconfirmedBalance() === 0);
|
||||||
assert.ok(w._lastBalanceFetch === 0);
|
assert.ok(w._lastBalanceFetch === 0);
|
||||||
await w.fetchBalance();
|
await w.fetchBalance();
|
||||||
assert.ok(w.getBalance() === 0.18262);
|
assert.ok(w.getBalance() === 18262000);
|
||||||
assert.ok(w.getUnconfirmedBalance() === 0);
|
assert.ok(w.getUnconfirmedBalance() === 0);
|
||||||
assert.ok(w._lastBalanceFetch > 0);
|
assert.ok(w._lastBalanceFetch > 0);
|
||||||
});
|
});
|
||||||
|
@ -302,19 +303,18 @@ it('Wallet can fetch TXs', async () => {
|
||||||
describe('currency', () => {
|
describe('currency', () => {
|
||||||
it('fetches exchange rate and saves to AsyncStorage', async () => {
|
it('fetches exchange rate and saves to AsyncStorage', async () => {
|
||||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000;
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000;
|
||||||
AsyncStorage.storageCache = {}; // cleanup from other tests
|
|
||||||
let currency = require('./currency');
|
let currency = require('./currency');
|
||||||
await currency.startUpdater();
|
await currency.startUpdater();
|
||||||
let cur = AsyncStorage.storageCache[AppStorage.EXCHANGE_RATES];
|
let cur = await AsyncStorage.getItem(AppStorage.EXCHANGE_RATES);
|
||||||
cur = JSON.parse(cur);
|
cur = JSON.parse(cur);
|
||||||
assert.ok(Number.isInteger(cur[currency.STRUCT.LAST_UPDATED]));
|
assert.ok(Number.isInteger(cur[currency.STRUCT.LAST_UPDATED]));
|
||||||
assert.ok(cur[currency.STRUCT.LAST_UPDATED] > 0);
|
assert.ok(cur[currency.STRUCT.LAST_UPDATED] > 0);
|
||||||
assert.ok(cur['BTC_USD'] > 0);
|
assert.ok(cur['BTC_USD'] > 0);
|
||||||
|
|
||||||
// now, setting other currency as default
|
// 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();
|
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);
|
assert.ok(cur['BTC_JPY'] > 0);
|
||||||
|
|
||||||
// now setting with a proper setter
|
// now setting with a proper setter
|
||||||
|
@ -322,7 +322,7 @@ describe('currency', () => {
|
||||||
await currency.startUpdater();
|
await currency.startUpdater();
|
||||||
let preferred = await currency.getPreferredCurrency();
|
let preferred = await currency.getPreferredCurrency();
|
||||||
assert.strictEqual(preferred.endPointKey, 'EUR');
|
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);
|
assert.ok(cur['BTC_EUR'] > 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
50
App2.test.js
|
@ -1,5 +1,4 @@
|
||||||
/* global it, describe, jasmine */
|
/* global it, jasmine */
|
||||||
import { WatchOnlyWallet } from './class';
|
|
||||||
let assert = require('assert');
|
let assert = require('assert');
|
||||||
|
|
||||||
it('bip38 decodes', async () => {
|
it('bip38 decodes', async () => {
|
||||||
|
@ -37,50 +36,3 @@ it('bip38 decodes slow', async () => {
|
||||||
'KxqRtpd9vFju297ACPKHrGkgXuberTveZPXbRDiQ3MXZycSQYtjc',
|
'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());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ let A = require('./analytics');
|
||||||
let BlueElectrum = require('./BlueElectrum'); // eslint-disable-line
|
let BlueElectrum = require('./BlueElectrum'); // eslint-disable-line
|
||||||
|
|
||||||
/** @type {AppStorage} */
|
/** @type {AppStorage} */
|
||||||
let BlueApp = new AppStorage();
|
const BlueApp = new AppStorage();
|
||||||
|
|
||||||
async function startAndDecrypt(retry) {
|
async function startAndDecrypt(retry) {
|
||||||
console.log('startAndDecrypt');
|
console.log('startAndDecrypt');
|
||||||
|
|
|
@ -25,7 +25,6 @@ import {
|
||||||
import LinearGradient from 'react-native-linear-gradient';
|
import LinearGradient from 'react-native-linear-gradient';
|
||||||
import { LightningCustodianWallet } from './class';
|
import { LightningCustodianWallet } from './class';
|
||||||
import Carousel from 'react-native-snap-carousel';
|
import Carousel from 'react-native-snap-carousel';
|
||||||
import DeviceInfo from 'react-native-device-info';
|
|
||||||
import { BitcoinUnit } from './models/bitcoinUnits';
|
import { BitcoinUnit } from './models/bitcoinUnits';
|
||||||
import NavigationService from './NavigationService';
|
import NavigationService from './NavigationService';
|
||||||
import ImagePicker from 'react-native-image-picker';
|
import ImagePicker from 'react-native-image-picker';
|
||||||
|
@ -36,6 +35,7 @@ let loc = require('./loc/');
|
||||||
let BlueApp = require('./BlueApp');
|
let BlueApp = require('./BlueApp');
|
||||||
const { height, width } = Dimensions.get('window');
|
const { height, width } = Dimensions.get('window');
|
||||||
const aspectRatio = height / width;
|
const aspectRatio = height / width;
|
||||||
|
const BigNumber = require('bignumber.js');
|
||||||
let isIpad;
|
let isIpad;
|
||||||
if (aspectRatio > 1.6) {
|
if (aspectRatio > 1.6) {
|
||||||
isIpad = false;
|
isIpad = false;
|
||||||
|
@ -93,32 +93,26 @@ export class BitcoinButton extends Component {
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
borderColor: (this.props.active && BlueApp.settings.foregroundColor) || BlueApp.settings.inputBorderColor,
|
borderColor: BlueApp.settings.hdborderColor,
|
||||||
borderWidth: 0.5,
|
borderWidth: 1,
|
||||||
borderRadius: 5,
|
borderRadius: 5,
|
||||||
backgroundColor: BlueApp.settings.inputBackgroundColor,
|
backgroundColor: (this.props.active && BlueApp.settings.hdbackgroundColor) || BlueApp.settings.brandingColor,
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
width: this.props.style.width,
|
width: this.props.style.width,
|
||||||
|
minWidth: this.props.style.width,
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
|
minHeight: this.props.style.height,
|
||||||
height: this.props.style.height,
|
height: this.props.style.height,
|
||||||
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={{ paddingTop: 30 }}>
|
<View style={{ marginTop: 16, marginLeft: 16, marginBottom: 16 }}>
|
||||||
<Icon
|
<Text style={{ color: BlueApp.settings.hdborderColor, fontWeight: 'bold' }}>{loc.wallets.add.bitcoin}</Text>
|
||||||
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>
|
</View>
|
||||||
|
<Image
|
||||||
|
style={{ width: 34, height: 34, marginRight: 8, marginBottom: 8, justifyContent: 'flex-end', alignSelf: 'flex-end' }}
|
||||||
|
source={require('./img/addWallet/bitcoin.png')}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
|
@ -137,32 +131,26 @@ export class LightningButton extends Component {
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
borderColor: (this.props.active && BlueApp.settings.foregroundColor) || BlueApp.settings.inputBorderColor,
|
borderColor: BlueApp.settings.lnborderColor,
|
||||||
borderWidth: 0.5,
|
borderWidth: 1,
|
||||||
borderRadius: 5,
|
borderRadius: 5,
|
||||||
backgroundColor: BlueApp.settings.inputBackgroundColor,
|
backgroundColor: (this.props.active && BlueApp.settings.lnbackgroundColor) || BlueApp.settings.brandingColor,
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
width: this.props.style.width,
|
width: this.props.style.width,
|
||||||
|
minWidth: this.props.style.width,
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
|
minHeight: this.props.style.height,
|
||||||
height: this.props.style.height,
|
height: this.props.style.height,
|
||||||
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={{ paddingTop: 30 }}>
|
<View style={{ marginTop: 16, marginLeft: 16, marginBottom: 16 }}>
|
||||||
<Icon
|
<Text style={{ color: BlueApp.settings.lnborderColor, fontWeight: 'bold' }}>{loc.wallets.add.lightning}</Text>
|
||||||
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>
|
</View>
|
||||||
|
<Image
|
||||||
|
style={{ width: 34, height: 34, marginRight: 8, marginBottom: 8, justifyContent: 'flex-end', alignSelf: 'flex-end' }}
|
||||||
|
source={require('./img/addWallet/lightning.png')}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
|
@ -241,6 +229,14 @@ export class BlueCopyTextToClipboard extends Component {
|
||||||
this.state = { hasTappedText: false, address: props.text };
|
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 = () => {
|
copyToClipboard = () => {
|
||||||
this.setState({ hasTappedText: true }, () => {
|
this.setState({ hasTappedText: true }, () => {
|
||||||
Clipboard.setString(this.props.text);
|
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 {
|
export class BlueHeader extends Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
|
@ -560,13 +533,6 @@ export class is {
|
||||||
static ipad() {
|
static ipad() {
|
||||||
return isIpad;
|
return isIpad;
|
||||||
}
|
}
|
||||||
|
|
||||||
static iphone8() {
|
|
||||||
if (Platform.OS !== 'ios') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return DeviceInfo.getDeviceId() === 'iPhone10,4';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BlueSpacing20 extends Component {
|
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 {
|
export class BlueList extends Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
|
@ -1733,7 +1705,7 @@ export class BlueAddressInput extends Component {
|
||||||
export class BlueBitcoinAmount extends Component {
|
export class BlueBitcoinAmount extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
amount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
amount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||||
onChangeText: PropTypes.func,
|
onChangeText: PropTypes.func,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
unit: PropTypes.string,
|
unit: PropTypes.string,
|
||||||
|
@ -1744,8 +1716,15 @@ export class BlueBitcoinAmount extends Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
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 (
|
return (
|
||||||
<TouchableWithoutFeedback disabled={this.props.pointerEvents === 'none'} onPress={() => this.textInput.focus()}>
|
<TouchableWithoutFeedback disabled={this.props.pointerEvents === 'none'} onPress={() => this.textInput.focus()}>
|
||||||
<View>
|
<View>
|
||||||
|
@ -1788,13 +1767,7 @@ export class BlueBitcoinAmount extends Component {
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={{ alignItems: 'center', marginBottom: 22, marginTop: 4 }}>
|
<View style={{ alignItems: 'center', marginBottom: 22, marginTop: 4 }}>
|
||||||
<Text style={{ fontSize: 18, color: '#d4d4d4', fontWeight: '600' }}>
|
<Text style={{ fontSize: 18, color: '#d4d4d4', fontWeight: '600' }}>{localCurrency}</Text>
|
||||||
{loc.formatBalance(
|
|
||||||
this.props.unit === BitcoinUnit.BTC ? amount || 0 : loc.formatBalanceWithoutSuffix(amount || 0, BitcoinUnit.BTC, false),
|
|
||||||
BitcoinUnit.LOCAL_CURRENCY,
|
|
||||||
false,
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
|
|
141
BlueElectrum.js
|
@ -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');
|
const ElectrumClient = require('electrum-client');
|
||||||
let bitcoin = require('bitcoinjs-lib');
|
let bitcoin = require('bitcoinjs-lib');
|
||||||
let reverse = require('buffer-reverse');
|
let reverse = require('buffer-reverse');
|
||||||
|
|
||||||
const storageKey = 'ELECTRUM_PEERS';
|
const storageKey = 'ELECTRUM_PEERS';
|
||||||
const defaultPeer = { host: 'electrum.coinucopia.io', tcp: 50001 };
|
const defaultPeer = { host: 'electrum1.bluewallet.io', tcp: '50001' };
|
||||||
const hardcodedPeers = [
|
const hardcodedPeers = [
|
||||||
{ host: 'noveltybobble.coinjoined.com', tcp: '50001' },
|
// { host: 'noveltybobble.coinjoined.com', tcp: '50001' }, // down
|
||||||
{ host: 'electrum.be', tcp: '50001' },
|
// { host: 'electrum.be', tcp: '50001' },
|
||||||
// { host: 'node.ispol.sk', tcp: '50001' }, // down
|
// { 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: 'electrum.coinucopia.io', tcp: '50001' }, // SLOW
|
||||||
{ host: 'Bitkoins.nl', tcp: '50001' },
|
// { host: 'Bitkoins.nl', tcp: '50001' }, // down
|
||||||
{ host: 'fullnode.coinkite.com', tcp: '50001' },
|
// { host: 'fullnode.coinkite.com', tcp: '50001' },
|
||||||
// { host: 'preperfect.eleCTruMioUS.com', tcp: '50001' }, // down
|
// { host: 'preperfect.eleCTruMioUS.com', tcp: '50001' }, // down
|
||||||
{ host: 'electrum1.bluewallet.io', tcp: '50001' },
|
{ 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;
|
let mainClient = false;
|
||||||
|
@ -26,7 +31,7 @@ async function connectMain() {
|
||||||
console.log('begin connection:', JSON.stringify(usingPeer));
|
console.log('begin connection:', JSON.stringify(usingPeer));
|
||||||
mainClient = new ElectrumClient(usingPeer.tcp, usingPeer.host, 'tcp');
|
mainClient = new ElectrumClient(usingPeer.tcp, usingPeer.host, 'tcp');
|
||||||
await mainClient.connect();
|
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();
|
let peers = await mainClient.serverPeers_subscribe();
|
||||||
if (peers && peers.length > 0) {
|
if (peers && peers.length > 0) {
|
||||||
console.log('connected to ', ver);
|
console.log('connected to ', ver);
|
||||||
|
@ -35,7 +40,7 @@ async function connectMain() {
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
mainConnected = false;
|
mainConnected = false;
|
||||||
console.log('bad connection:', JSON.stringify(usingPeer));
|
console.log('bad connection:', JSON.stringify(usingPeer), e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mainConnected) {
|
if (!mainConnected) {
|
||||||
|
@ -43,7 +48,7 @@ async function connectMain() {
|
||||||
mainClient.keepAlive = () => {}; // dirty hack to make it stop reconnecting
|
mainClient.keepAlive = () => {}; // dirty hack to make it stop reconnecting
|
||||||
mainClient.reconnect = () => {}; // dirty hack to make it stop reconnecting
|
mainClient.reconnect = () => {}; // dirty hack to make it stop reconnecting
|
||||||
mainClient.close();
|
mainClient.close();
|
||||||
setTimeout(connectMain, 5000);
|
setTimeout(connectMain, 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,23 +123,106 @@ async function getTransactionsByAddress(address) {
|
||||||
return history;
|
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}
|
* @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');
|
if (!mainClient) throw new Error('Electrum client is not connected');
|
||||||
let balance = 0;
|
let ret = { balance: 0, unconfirmed_balance: 0, addresses: {} };
|
||||||
let unconfirmedBalance = 0;
|
|
||||||
for (let addr of addresses) {
|
|
||||||
let b = await getBalanceByAddress(addr);
|
|
||||||
|
|
||||||
balance += b.confirmed;
|
let chunks = splitIntoChunks(addresses, batchsize);
|
||||||
unconfirmedBalance += b.unconfirmed_balance;
|
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() {
|
async function estimateFees() {
|
||||||
if (!mainClient) throw new Error('Electrum client is not connected');
|
if (!mainClient) throw new Error('Electrum client is not connected');
|
||||||
const fast = await mainClient.blockchainEstimatefee(1);
|
const fast = await mainClient.blockchainEstimatefee(1);
|
||||||
const medium = await mainClient.blockchainEstimatefee(6);
|
const medium = await mainClient.blockchainEstimatefee(5);
|
||||||
const slow = await mainClient.blockchainEstimatefee(12);
|
const slow = await mainClient.blockchainEstimatefee(10);
|
||||||
return { fast, medium, slow };
|
return { fast, medium, slow };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,9 +270,11 @@ async function broadcast(hex) {
|
||||||
module.exports.getBalanceByAddress = getBalanceByAddress;
|
module.exports.getBalanceByAddress = getBalanceByAddress;
|
||||||
module.exports.getTransactionsByAddress = getTransactionsByAddress;
|
module.exports.getTransactionsByAddress = getTransactionsByAddress;
|
||||||
module.exports.multiGetBalanceByAddress = multiGetBalanceByAddress;
|
module.exports.multiGetBalanceByAddress = multiGetBalanceByAddress;
|
||||||
|
module.exports.getTransactionsFullByAddress = getTransactionsFullByAddress;
|
||||||
module.exports.waitTillConnected = waitTillConnected;
|
module.exports.waitTillConnected = waitTillConnected;
|
||||||
module.exports.estimateFees = estimateFees;
|
module.exports.estimateFees = estimateFees;
|
||||||
module.exports.broadcast = broadcast;
|
module.exports.broadcast = broadcast;
|
||||||
|
module.exports.multiGetUtxoByAddress = multiGetUtxoByAddress;
|
||||||
|
|
||||||
module.exports.forceDisconnect = () => {
|
module.exports.forceDisconnect = () => {
|
||||||
mainClient.keepAlive = () => {}; // dirty hack to make it stop reconnecting
|
mainClient.keepAlive = () => {}; // dirty hack to make it stop reconnecting
|
||||||
|
@ -194,6 +284,15 @@ module.exports.forceDisconnect = () => {
|
||||||
|
|
||||||
module.exports.hardcodedPeers = hardcodedPeers;
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,13 @@
|
||||||
global.net = require('net');
|
global.net = require('net');
|
||||||
let BlueElectrum = require('./BlueElectrum');
|
let BlueElectrum = require('./BlueElectrum');
|
||||||
let assert = require('assert');
|
let assert = require('assert');
|
||||||
|
let bitcoin = require('bitcoinjs-lib');
|
||||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 150 * 1000;
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = 150 * 1000;
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
// after all tests we close socket so the test suite can actually terminate
|
// 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 () => {
|
beforeAll(async () => {
|
||||||
|
@ -14,8 +16,8 @@ beforeAll(async () => {
|
||||||
// while app starts up, but for tests we need to wait for it
|
// while app starts up, but for tests we need to wait for it
|
||||||
try {
|
try {
|
||||||
await BlueElectrum.waitTillConnected();
|
await BlueElectrum.waitTillConnected();
|
||||||
} catch (Err) {
|
} catch (err) {
|
||||||
console.log('failed to connect to Electrum:', Err);
|
console.log('failed to connect to Electrum:', err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -23,18 +25,17 @@ beforeAll(async () => {
|
||||||
describe('Electrum', () => {
|
describe('Electrum', () => {
|
||||||
it('ElectrumClient can connect and query', async () => {
|
it('ElectrumClient can connect and query', async () => {
|
||||||
const ElectrumClient = require('electrum-client');
|
const ElectrumClient = require('electrum-client');
|
||||||
let bitcoin = require('bitcoinjs-lib');
|
|
||||||
|
|
||||||
for (let peer of BlueElectrum.hardcodedPeers) {
|
for (let peer of BlueElectrum.hardcodedPeers) {
|
||||||
let mainClient = new ElectrumClient(peer.tcp, peer.host, 'tcp');
|
let mainClient = new ElectrumClient(peer.tcp, peer.host, 'tcp');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await mainClient.connect();
|
await mainClient.connect();
|
||||||
await mainClient.server_version('2.7.11', '1.2');
|
await mainClient.server_version('2.7.11', '1.4');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
mainClient.reconnect = mainClient.keepAlive = () => {}; // dirty hack to make it stop reconnecting
|
mainClient.reconnect = mainClient.keepAlive = () => {}; // dirty hack to make it stop reconnecting
|
||||||
mainClient.close();
|
mainClient.close();
|
||||||
throw new Error('bad connection: ' + JSON.stringify(peer));
|
throw new Error('bad connection: ' + JSON.stringify(peer) + ' ' + e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
let addr4elect = 'bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej';
|
let addr4elect = 'bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej';
|
||||||
|
@ -52,7 +53,6 @@ describe('Electrum', () => {
|
||||||
hash = bitcoin.crypto.sha256(script);
|
hash = bitcoin.crypto.sha256(script);
|
||||||
reversedHash = Buffer.from(hash.reverse());
|
reversedHash = Buffer.from(hash.reverse());
|
||||||
balance = await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex'));
|
balance = await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex'));
|
||||||
assert.ok(balance.confirmed === 51432);
|
|
||||||
|
|
||||||
// let peers = await mainClient.serverPeers_subscribe();
|
// let peers = await mainClient.serverPeers_subscribe();
|
||||||
// console.log(peers);
|
// 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 address = '3GCvDBAktgQQtsbN6x5DYiQCMmgZ9Yk8BK';
|
||||||
let balance = await BlueElectrum.getBalanceByAddress(address);
|
let balance = await BlueElectrum.getBalanceByAddress(address);
|
||||||
assert.strictEqual(balance.confirmed, 51432);
|
assert.strictEqual(balance.confirmed, 51432);
|
||||||
assert.strictEqual(balance.unconfirmed, 0);
|
assert.strictEqual(balance.unconfirmed, 0);
|
||||||
assert.strictEqual(balance.addr, address);
|
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.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) {
|
for (let tx of txs) {
|
||||||
assert.ok(tx.tx_hash);
|
assert.ok(tx.address === 'bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh');
|
||||||
assert.ok(tx.height);
|
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
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -9,7 +9,8 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 300 * 1000;
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
// after all tests we close socket so the test suite can actually terminate
|
// 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 () => {
|
beforeAll(async () => {
|
||||||
|
@ -79,18 +80,27 @@ it('HD (BIP49) can work with a gap', async function() {
|
||||||
// console.log('external', c, hd._getExternalAddressByIndex(c));
|
// console.log('external', c, hd._getExternalAddressByIndex(c));
|
||||||
// }
|
// }
|
||||||
await hd.fetchTransactions();
|
await hd.fetchTransactions();
|
||||||
console.log('hd.transactions.length=', hd.transactions.length);
|
|
||||||
assert.ok(hd.transactions.length >= 3);
|
assert.ok(hd.transactions.length >= 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Segwit HD (BIP49) can batch fetch many txs', async function() {
|
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();
|
let hd = new HDSegwitP2SHWallet();
|
||||||
hd._xpub = 'ypub6WZ2c7YJ1SQ1rBYftwMqwV9bBmybXzETFxWmkzMz25bCf6FkDdXjNgR7zRW8JGSnoddNdUH7ZQS7JeQAddxdGpwgPskcsXFcvSn1JdGVcPQ'; // cant fetch txs
|
hd._xpub = 'ypub6WZ2c7YJ1SQ1rBYftwMqwV9bBmybXzETFxWmkzMz25bCf6FkDdXjNgR7zRW8JGSnoddNdUH7ZQS7JeQAddxdGpwgPskcsXFcvSn1JdGVcPQ';
|
||||||
await hd.fetchBalance();
|
await hd.fetchBalance();
|
||||||
await hd.fetchTransactions();
|
await hd.fetchTransactions();
|
||||||
assert.ok(hd.transactions.length > 0);
|
assert.ok(hd.getTransactions().length === 153);
|
||||||
console.log('hd.transactions.length=', hd.transactions.length);
|
});
|
||||||
|
|
||||||
|
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() {
|
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();
|
let end = +new Date();
|
||||||
const took = (end - start) / 1000;
|
const took = (end - start) / 1000;
|
||||||
took > 15 && console.warn('took', took, "sec to fetch huge HD wallet's balance");
|
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();
|
await hd.fetchUtxo();
|
||||||
assert.ok(hd.utxo.length > 0);
|
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();
|
await hd.fetchTransactions();
|
||||||
assert.strictEqual(hd.getTransactions().length, 107);
|
assert.strictEqual(hd.getTransactions().length, 107);
|
||||||
|
|
|
@ -153,6 +153,9 @@ describe('LightningCustodianWallet', () => {
|
||||||
|
|
||||||
await l2.fetchTransactions();
|
await l2.fetchTransactions();
|
||||||
assert.strictEqual(l2.transactions_raw.length, txLen + 1);
|
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
|
// transactions became more after paying an invoice
|
||||||
|
|
||||||
// now, trying to pay duplicate invoice
|
// now, trying to pay duplicate invoice
|
||||||
|
@ -374,6 +377,14 @@ describe('LightningCustodianWallet', () => {
|
||||||
err = true;
|
err = true;
|
||||||
}
|
}
|
||||||
assert.ok(err);
|
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 () => {
|
it('cant pay negative free amount', async () => {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import LightningSettings from './screen/settings/lightningSettings';
|
||||||
import WalletsList from './screen/wallets/list';
|
import WalletsList from './screen/wallets/list';
|
||||||
import WalletTransactions from './screen/wallets/transactions';
|
import WalletTransactions from './screen/wallets/transactions';
|
||||||
import AddWallet from './screen/wallets/add';
|
import AddWallet from './screen/wallets/add';
|
||||||
|
import PleaseBackup from './screen/wallets/pleaseBackup';
|
||||||
import ImportWallet from './screen/wallets/import';
|
import ImportWallet from './screen/wallets/import';
|
||||||
import WalletDetails from './screen/wallets/details';
|
import WalletDetails from './screen/wallets/details';
|
||||||
import WalletExport from './screen/wallets/export';
|
import WalletExport from './screen/wallets/export';
|
||||||
|
@ -183,6 +184,9 @@ const CreateWalletStackNavigator = createStackNavigator({
|
||||||
ImportWallet: {
|
ImportWallet: {
|
||||||
screen: ImportWallet,
|
screen: ImportWallet,
|
||||||
},
|
},
|
||||||
|
PleaseBackup: {
|
||||||
|
screen: PleaseBackup,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const LightningScanInvoiceStackNavigator = createStackNavigator({
|
const LightningScanInvoiceStackNavigator = createStackNavigator({
|
||||||
|
|
|
@ -21,8 +21,6 @@ Community: [telegram group](https://t.me/bluewallet)
|
||||||
* Encryption. Plausible deniability
|
* Encryption. Plausible deniability
|
||||||
* And many more [features...](https://bluewallet.io/features.html)
|
* 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%">
|
<img src="https://i.imgur.com/hHYJnMj.png" width="100%">
|
||||||
|
|
||||||
|
|
138
WatchConnectivity.js
Normal 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
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
1
__mocks__/@react-native-community/async-storage.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export default from '@react-native-community/async-storage/jest/async-storage-mock'
|
|
@ -17,7 +17,7 @@
|
||||||
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
||||||
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
|
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
|
||||||
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
|
<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" />
|
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
|
||||||
</configuration>
|
</configuration>
|
||||||
</facet>
|
</facet>
|
||||||
|
@ -28,16 +28,16 @@
|
||||||
<exclude-output />
|
<exclude-output />
|
||||||
<content url="file://$MODULE_DIR$">
|
<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/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/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/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/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/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/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/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/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/res/resValues/androidTest/debug" type="java-test-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/apt/test/debug" isTestSource="true" generated="true" />
|
<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/java" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/rs" 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/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/res" type="java-test-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/resources" 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" />
|
<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/java" isTestSource="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/rs" 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/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/res" type="java-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" 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" />
|
<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/generated/source/r" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/annotation_processor_list" />
|
<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/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/blame" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/build-info" />
|
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundle_manifest" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/builds" />
|
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/check_manifest_result" />
|
||||||
<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/compatible_screen_manifest" />
|
<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" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-classes" />
|
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/instant_app_manifest" />
|
||||||
<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_run_merged_manifests" />
|
<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/javac" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/jniLibs" />
|
<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_aapt_derived_proguard_rules" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/legacy_multidex_main_dex_list" />
|
<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/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_assets" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/merged_manifests" />
|
<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/prebuild" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/processed_res" />
|
<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/res" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/resources" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
|
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/shader_assets" />
|
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/shader_assets" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/shaders" />
|
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/shaders" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/split-apk" />
|
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/signing_config" />
|
||||||
<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/symbols" />
|
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/transforms" />
|
<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/outputs" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
|
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="jdk" jdkName="Android API 27 Platform" jdkType="Android SDK" />
|
<orderEntry type="jdk" jdkName="Android API 27 Platform" jdkType="Android SDK" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
<orderEntry type="library" name="Gradle: org.webkit:android-jsc:r174650@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.android.support:support-vector-drawable:27.1.1@aar" level="project" />
|
|
||||||
<orderEntry type="library" name="Gradle: com.facebook.fresco:fresco:1.10.0@aar" 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: com.android.support:support-fragment:28.0.0@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:localbroadcastmanager:28.0.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.facebook.fresco:fbcore:1.10.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: com.android.support:animated-vector-drawable:28.0.0@aar" level="project" />
|
||||||
<orderEntry type="library" name="Gradle: android.arch.lifecycle:viewmodel:1.1.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.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: android.arch.lifecycle:viewmodel:1.1.1@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.squareup.okhttp3:okhttp:3.12.1@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:loader: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.1@aar" level="project" />
|
||||||
<orderEntry type="library" name="Gradle: android.arch.core:runtime:1.1.0@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.squareup.okhttp3:okhttp-urlconnection:3.11.0@jar" level="project" />
|
<orderEntry type="library" name="Gradle: com.android.support:cursoradapter:28.0.0@aar" level="project" />
|
||||||
<orderEntry type="library" name="Gradle: com.android.support:animated-vector-drawable:27.1.1@aar" level="project" />
|
<orderEntry type="library" name="Gradle: android.arch.lifecycle:runtime:1.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: 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: 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.google.zxing:core:3.3.3@jar" level="project" />
|
||||||
<orderEntry type="library" name="Gradle: com.squareup.okio:okio:1.14.0@jar" level="project" />
|
|
||||||
<orderEntry type="library" name="Gradle: javax.inject:javax.inject:1@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.facebook.soloader:soloader:0.6.0@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.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.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: 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:viewpager:28.0.0@aar" 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.facebook.react:react-native:0.59.6@aar" level="project" />
|
||||||
<orderEntry type="library" name="Gradle: com.google.code.findbugs:jsr305:3.0.2@jar" 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:support-fragment:27.1.1@aar" level="project" />
|
<orderEntry type="library" name="Gradle: com.android.support:coordinatorlayout:28.0.0@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:customview: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:swiperefreshlayout:28.0.0@aar" level="project" />
|
||||||
<orderEntry type="library" name="Gradle: com.android.support:appcompat-v7:27.1.1@aar" level="project" />
|
<orderEntry type="library" name="Gradle: com.android.support:multidex:1.0.3@aar" level="project" />
|
||||||
<orderEntry type="library" name="Gradle: android.arch.core:common:1.1.0@jar" level="project" />
|
<orderEntry type="library" name="Gradle: com.squareup.okio:okio:1.15.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.koushikdutta.async:androidasync:2.1.6@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="module" module-name="react-native-device-info" />
|
<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-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>
|
</component>
|
||||||
</module>
|
</module>
|
|
@ -102,7 +102,7 @@ android {
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "3.9.4"
|
versionName "4.0.3"
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters "armeabi-v7a", "x86"
|
abiFilters "armeabi-v7a", "x86"
|
||||||
}
|
}
|
||||||
|
@ -139,6 +139,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation project(':@react-native-community_async-storage')
|
||||||
implementation project(':@react-native-community_slider')
|
implementation project(':@react-native-community_slider')
|
||||||
implementation project(':react-native-obscure')
|
implementation project(':react-native-obscure')
|
||||||
implementation project(':react-native-tcp')
|
implementation project(':react-native-tcp')
|
||||||
|
|
|
@ -26,6 +26,9 @@
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data android:scheme="bitcoin" />
|
<data android:scheme="bitcoin" />
|
||||||
<data android:scheme="lightning" />
|
<data android:scheme="lightning" />
|
||||||
|
<data android:scheme="bluewallet" />
|
||||||
|
<data android:scheme="lapp" />
|
||||||
|
<data android:scheme="blue" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||||
|
|
|
@ -3,6 +3,7 @@ package io.bluewallet.bluewallet;
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
|
||||||
import com.facebook.react.ReactApplication;
|
import com.facebook.react.ReactApplication;
|
||||||
|
import com.reactnativecommunity.asyncstorage.AsyncStoragePackage;
|
||||||
import com.reactnativecommunity.slider.ReactSliderPackage;
|
import com.reactnativecommunity.slider.ReactSliderPackage;
|
||||||
import com.diegofhg.obscure.ObscurePackage;
|
import com.diegofhg.obscure.ObscurePackage;
|
||||||
import com.peel.react.TcpSocketsModule;
|
import com.peel.react.TcpSocketsModule;
|
||||||
|
@ -58,6 +59,7 @@ public class MainApplication extends Application implements ReactApplication {
|
||||||
protected List<ReactPackage> getPackages() {
|
protected List<ReactPackage> getPackages() {
|
||||||
return Arrays.<ReactPackage>asList(
|
return Arrays.<ReactPackage>asList(
|
||||||
new MainReactPackage(),
|
new MainReactPackage(),
|
||||||
|
new AsyncStoragePackage(),
|
||||||
new ReactSliderPackage(),
|
new ReactSliderPackage(),
|
||||||
new ObscurePackage(),
|
new ObscurePackage(),
|
||||||
new TcpSocketsModule(),
|
new TcpSocketsModule(),
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
rootProject.name = 'BlueWallet'
|
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'
|
include ':@react-native-community_slider'
|
||||||
project(':@react-native-community_slider').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/slider/android')
|
project(':@react-native-community_slider').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/slider/android')
|
||||||
include ':react-native-obscure'
|
include ':react-native-obscure'
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { LegacyWallet } from './legacy-wallet';
|
import { LegacyWallet } from './legacy-wallet';
|
||||||
import Frisbee from 'frisbee';
|
import Frisbee from 'frisbee';
|
||||||
const bip39 = require('bip39');
|
const bip39 = require('bip39');
|
||||||
const BigNumber = require('bignumber.js');
|
|
||||||
const bitcoin = require('bitcoinjs-lib');
|
const bitcoin = require('bitcoinjs-lib');
|
||||||
const BlueElectrum = require('../BlueElectrum');
|
const BlueElectrum = require('../BlueElectrum');
|
||||||
|
|
||||||
|
@ -18,7 +17,13 @@ export class AbstractHDWallet extends LegacyWallet {
|
||||||
this._xpub = ''; // cache
|
this._xpub = ''; // cache
|
||||||
this.usedAddresses = [];
|
this.usedAddresses = [];
|
||||||
this._address_to_wif_cache = {};
|
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() {
|
generate() {
|
||||||
|
@ -110,11 +115,16 @@ export class AbstractHDWallet extends LegacyWallet {
|
||||||
// looking for free external address
|
// looking for free external address
|
||||||
let freeAddress = '';
|
let freeAddress = '';
|
||||||
let c;
|
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;
|
if (this.next_free_address_index + c < 0) continue;
|
||||||
let address = this._getExternalAddressByIndex(this.next_free_address_index + c);
|
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
|
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) {
|
if (txs.length === 0) {
|
||||||
// found free address
|
// found free address
|
||||||
freeAddress = address;
|
freeAddress = address;
|
||||||
|
@ -143,11 +153,16 @@ export class AbstractHDWallet extends LegacyWallet {
|
||||||
// looking for free internal address
|
// looking for free internal address
|
||||||
let freeAddress = '';
|
let freeAddress = '';
|
||||||
let c;
|
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;
|
if (this.next_free_change_address_index + c < 0) continue;
|
||||||
let address = this._getInternalAddressByIndex(this.next_free_change_address_index + c);
|
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
|
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) {
|
if (txs.length === 0) {
|
||||||
// found free address
|
// found free address
|
||||||
freeAddress = 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
|
// 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);
|
let possibleAddress = this._getInternalAddressByIndex(c);
|
||||||
if (possibleAddress === address) {
|
if (possibleAddress === address) {
|
||||||
return (this._address_to_wif_cache[address] = this._getInternalWIFByIndex(c));
|
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);
|
let possibleAddress = this._getExternalAddressByIndex(c);
|
||||||
if (possibleAddress === address) {
|
if (possibleAddress === address) {
|
||||||
return (this._address_to_wif_cache[address] = this._getExternalWIFByIndex(c));
|
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
|
// wrong guess. will have to rescan
|
||||||
if (!completelyEmptyWallet) {
|
if (!completelyEmptyWallet) {
|
||||||
// so doing binary search for last used address:
|
// so doing binary search for last used address:
|
||||||
this.next_free_change_address_index = await binarySearchIterationForInternalAddress(100);
|
this.next_free_change_address_index = await binarySearchIterationForInternalAddress(1000);
|
||||||
this.next_free_address_index = await binarySearchIterationForExternalAddress(100);
|
this.next_free_address_index = await binarySearchIterationForExternalAddress(1000);
|
||||||
}
|
}
|
||||||
}
|
} // end rescanning fresh wallet
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
// finally fetching balance
|
// finally fetching balance
|
||||||
let balance = await BlueElectrum.multiGetBalanceByAddress(this.usedAddresses);
|
await this._fetchBalance();
|
||||||
this.balance = new BigNumber(balance.balance).dividedBy(100000000).toNumber();
|
|
||||||
this.unconfirmed_balance = new BigNumber(balance.unconfirmed_balance).dividedBy(100000000).toNumber();
|
|
||||||
this._lastBalanceFetch = +new Date();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async _fetchBalance() {
|
||||||
* @inheritDoc
|
// probing future addressess in hierarchy whether they have any transactions, in case
|
||||||
*/
|
// our 'next free addr' pointers are lagging behind
|
||||||
async fetchUtxo() {
|
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({
|
const api = new Frisbee({
|
||||||
baseURI: 'https://blockchain.info',
|
baseURI: 'https://blockchain.info',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.usedAddresses.length === 0) {
|
addresses = addresses.join('|');
|
||||||
// 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);
|
|
||||||
|
|
||||||
let utxos = [];
|
let utxos = [];
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
|
let uri;
|
||||||
try {
|
try {
|
||||||
|
uri = 'https://blockchain.info' + '/unspent?active=' + addresses + '&limit=1000';
|
||||||
response = await api.get('/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
|
// this endpoint does not support offset of some kind o_O
|
||||||
// so doing only one call
|
// so doing only one call
|
||||||
|
@ -469,10 +503,55 @@ export class AbstractHDWallet extends LegacyWallet {
|
||||||
utxos.push(unspent);
|
utxos.push(unspent);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} 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) {
|
weOwnAddress(addr) {
|
||||||
|
|
|
@ -42,6 +42,10 @@ export class AbstractWallet {
|
||||||
return this.label;
|
return this.label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns {number} Available to spend amount, int, in sats
|
||||||
|
*/
|
||||||
getBalance() {
|
getBalance() {
|
||||||
return this.balance;
|
return this.balance;
|
||||||
}
|
}
|
||||||
|
@ -95,7 +99,5 @@ export class AbstractWallet {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAddress() {}
|
|
||||||
|
|
||||||
// createTx () { throw Error('not implemented') }
|
// createTx () { throw Error('not implemented') }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { AsyncStorage } from 'react-native';
|
import AsyncStorage from '@react-native-community/async-storage';
|
||||||
import {
|
import {
|
||||||
HDLegacyBreadwalletWallet,
|
HDLegacyBreadwalletWallet,
|
||||||
HDSegwitP2SHWallet,
|
HDSegwitP2SHWallet,
|
||||||
|
@ -7,9 +7,11 @@ import {
|
||||||
LegacyWallet,
|
LegacyWallet,
|
||||||
SegwitP2SHWallet,
|
SegwitP2SHWallet,
|
||||||
SegwitBech32Wallet,
|
SegwitBech32Wallet,
|
||||||
|
HDSegwitBech32Wallet,
|
||||||
} from './';
|
} from './';
|
||||||
import { LightningCustodianWallet } from './lightning-custodian-wallet';
|
import { LightningCustodianWallet } from './lightning-custodian-wallet';
|
||||||
let encryption = require('../encryption');
|
import WatchConnectivity from '../WatchConnectivity';
|
||||||
|
const encryption = require('../encryption');
|
||||||
|
|
||||||
export class AppStorage {
|
export class AppStorage {
|
||||||
static FLAG_ENCRYPTED = 'data_encrypted';
|
static FLAG_ENCRYPTED = 'data_encrypted';
|
||||||
|
@ -17,6 +19,7 @@ export class AppStorage {
|
||||||
static EXCHANGE_RATES = 'currency';
|
static EXCHANGE_RATES = 'currency';
|
||||||
static LNDHUB = 'lndhub';
|
static LNDHUB = 'lndhub';
|
||||||
static PREFERRED_CURRENCY = 'preferredCurrency';
|
static PREFERRED_CURRENCY = 'preferredCurrency';
|
||||||
|
static ADVANCED_MODE_ENABLED = 'advancedmodeenabled';
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
/** {Array.<AbstractWallet>} */
|
/** {Array.<AbstractWallet>} */
|
||||||
|
@ -44,11 +47,14 @@ export class AppStorage {
|
||||||
failedColor: '#ff0000',
|
failedColor: '#ff0000',
|
||||||
shadowColor: '#000000',
|
shadowColor: '#000000',
|
||||||
inverseForegroundColor: '#ffffff',
|
inverseForegroundColor: '#ffffff',
|
||||||
|
hdborderColor: '#68BBE1',
|
||||||
|
hdbackgroundColor: '#ECF9FF',
|
||||||
|
lnborderColor: '#F7C056',
|
||||||
|
lnbackgroundColor: '#FFFAEF',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async storageIsEncrypted() {
|
async storageIsEncrypted() {
|
||||||
// await AsyncStorage.clear();
|
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
data = await AsyncStorage.getItem(AppStorage.FLAG_ENCRYPTED);
|
data = await AsyncStorage.getItem(AppStorage.FLAG_ENCRYPTED);
|
||||||
|
@ -118,8 +124,9 @@ export class AppStorage {
|
||||||
buckets = JSON.parse(buckets);
|
buckets = JSON.parse(buckets);
|
||||||
buckets.push(encryption.encrypt(JSON.stringify(data), fakePassword));
|
buckets.push(encryption.encrypt(JSON.stringify(data), fakePassword));
|
||||||
this.cachedPassword = fakePassword;
|
this.cachedPassword = fakePassword;
|
||||||
|
const bucketsString = JSON.stringify(buckets);
|
||||||
return AsyncStorage.setItem('data', JSON.stringify(buckets));
|
await AsyncStorage.setItem('data', bucketsString);
|
||||||
|
return (await AsyncStorage.getItem('data')) === bucketsString;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -156,6 +163,7 @@ export class AppStorage {
|
||||||
break;
|
break;
|
||||||
case WatchOnlyWallet.type:
|
case WatchOnlyWallet.type:
|
||||||
unserializedWallet = WatchOnlyWallet.fromJson(key);
|
unserializedWallet = WatchOnlyWallet.fromJson(key);
|
||||||
|
unserializedWallet.init();
|
||||||
break;
|
break;
|
||||||
case HDLegacyP2PKHWallet.type:
|
case HDLegacyP2PKHWallet.type:
|
||||||
unserializedWallet = HDLegacyP2PKHWallet.fromJson(key);
|
unserializedWallet = HDLegacyP2PKHWallet.fromJson(key);
|
||||||
|
@ -163,6 +171,9 @@ export class AppStorage {
|
||||||
case HDSegwitP2SHWallet.type:
|
case HDSegwitP2SHWallet.type:
|
||||||
unserializedWallet = HDSegwitP2SHWallet.fromJson(key);
|
unserializedWallet = HDSegwitP2SHWallet.fromJson(key);
|
||||||
break;
|
break;
|
||||||
|
case HDSegwitBech32Wallet.type:
|
||||||
|
unserializedWallet = HDSegwitBech32Wallet.fromJson(key);
|
||||||
|
break;
|
||||||
case HDLegacyBreadwalletWallet.type:
|
case HDLegacyBreadwalletWallet.type:
|
||||||
unserializedWallet = HDLegacyBreadwalletWallet.fromJson(key);
|
unserializedWallet = HDLegacyBreadwalletWallet.fromJson(key);
|
||||||
break;
|
break;
|
||||||
|
@ -199,11 +210,14 @@ export class AppStorage {
|
||||||
this.tx_metadata = data.tx_metadata;
|
this.tx_metadata = data.tx_metadata;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
WatchConnectivity.init();
|
||||||
|
await WatchConnectivity.shared.sendWalletsToWatch();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false; // failed loading data or loading/decryptin data
|
return false; // failed loading data or loading/decryptin data
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.warn(error.message);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -240,6 +254,7 @@ export class AppStorage {
|
||||||
let walletsToSave = [];
|
let walletsToSave = [];
|
||||||
for (let key of this.wallets) {
|
for (let key of this.wallets) {
|
||||||
if (typeof key === 'boolean') continue;
|
if (typeof key === 'boolean') continue;
|
||||||
|
if (key.prepareForSerialization) key.prepareForSerialization();
|
||||||
walletsToSave.push(JSON.stringify({ ...key, type: key.type }));
|
walletsToSave.push(JSON.stringify({ ...key, type: key.type }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,7 +284,8 @@ export class AppStorage {
|
||||||
} else {
|
} else {
|
||||||
await AsyncStorage.setItem(AppStorage.FLAG_ENCRYPTED, ''); // drop the flag
|
await AsyncStorage.setItem(AppStorage.FLAG_ENCRYPTED, ''); // drop the flag
|
||||||
}
|
}
|
||||||
|
WatchConnectivity.init();
|
||||||
|
WatchConnectivity.shared.sendWalletsToWatch();
|
||||||
return AsyncStorage.setItem('data', JSON.stringify(data));
|
return AsyncStorage.setItem('data', JSON.stringify(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
492
class/hd-segwit-bech32-wallet.js
Normal 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 };
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,13 @@
|
||||||
import { AbstractHDWallet } from './abstract-hd-wallet';
|
import { AbstractHDWallet } from './abstract-hd-wallet';
|
||||||
import Frisbee from 'frisbee';
|
import Frisbee from 'frisbee';
|
||||||
import { NativeModules } from 'react-native';
|
import { NativeModules } from 'react-native';
|
||||||
import bitcoin from 'bitcoinjs-lib';
|
|
||||||
import bip39 from 'bip39';
|
import bip39 from 'bip39';
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
import b58 from 'bs58check';
|
import b58 from 'bs58check';
|
||||||
import signer from '../models/signer';
|
import signer from '../models/signer';
|
||||||
|
const bitcoin = require('bitcoinjs-lib');
|
||||||
|
const bitcoin5 = require('bitcoinjs5');
|
||||||
|
const HDNode = require('bip32');
|
||||||
|
|
||||||
const { RNRandomBytes } = NativeModules;
|
const { RNRandomBytes } = NativeModules;
|
||||||
|
|
||||||
|
@ -28,13 +30,10 @@ function ypubToXpub(ypub) {
|
||||||
* @returns {String}
|
* @returns {String}
|
||||||
*/
|
*/
|
||||||
function nodeToP2shSegwitAddress(hdNode) {
|
function nodeToP2shSegwitAddress(hdNode) {
|
||||||
const pubkeyBuf = hdNode.keyPair.getPublicKeyBuffer();
|
const { address } = bitcoin5.payments.p2sh({
|
||||||
const hash = bitcoin.crypto.hash160(pubkeyBuf);
|
redeem: bitcoin5.payments.p2wpkh({ pubkey: hdNode.publicKey }),
|
||||||
const redeemScript = bitcoin.script.witnessPubKeyHash.output.encode(hash);
|
});
|
||||||
const hash2 = bitcoin.crypto.hash160(redeemScript);
|
return address;
|
||||||
const scriptPubkey = bitcoin.script.scriptHash.output.encode(hash2);
|
|
||||||
|
|
||||||
return bitcoin.address.fromOutputScript(scriptPubkey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -102,9 +101,12 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet {
|
||||||
index = index * 1; // cast to int
|
index = index * 1; // cast to int
|
||||||
if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
|
if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
|
||||||
|
|
||||||
const xpub = ypubToXpub(this.getXpub());
|
if (!this._node0) {
|
||||||
const hdNode = bitcoin.HDNode.fromBase58(xpub);
|
const xpub = ypubToXpub(this.getXpub());
|
||||||
const address = nodeToP2shSegwitAddress(hdNode.derive(0).derive(index));
|
const hdNode = HDNode.fromBase58(xpub);
|
||||||
|
this._node0 = hdNode.derive(0);
|
||||||
|
}
|
||||||
|
const address = nodeToP2shSegwitAddress(this._node0.derive(index));
|
||||||
|
|
||||||
return (this.external_addresses_cache[index] = address);
|
return (this.external_addresses_cache[index] = address);
|
||||||
}
|
}
|
||||||
|
@ -113,9 +115,12 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet {
|
||||||
index = index * 1; // cast to int
|
index = index * 1; // cast to int
|
||||||
if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit
|
if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit
|
||||||
|
|
||||||
const xpub = ypubToXpub(this.getXpub());
|
if (!this._node1) {
|
||||||
const hdNode = bitcoin.HDNode.fromBase58(xpub);
|
const xpub = ypubToXpub(this.getXpub());
|
||||||
const address = nodeToP2shSegwitAddress(hdNode.derive(1).derive(index));
|
const hdNode = HDNode.fromBase58(xpub);
|
||||||
|
this._node1 = hdNode.derive(1);
|
||||||
|
}
|
||||||
|
const address = nodeToP2shSegwitAddress(this._node1.derive(index));
|
||||||
|
|
||||||
return (this.internal_addresses_cache[index] = address);
|
return (this.internal_addresses_cache[index] = address);
|
||||||
}
|
}
|
||||||
|
@ -133,7 +138,7 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet {
|
||||||
// first, getting xpub
|
// first, getting xpub
|
||||||
const mnemonic = this.secret;
|
const mnemonic = this.secret;
|
||||||
const seed = bip39.mnemonicToSeed(mnemonic);
|
const seed = bip39.mnemonicToSeed(mnemonic);
|
||||||
const root = bitcoin.HDNode.fromSeedBuffer(seed);
|
const root = HDNode.fromSeed(seed);
|
||||||
|
|
||||||
const path = "m/49'/0'/0'";
|
const path = "m/49'/0'/0'";
|
||||||
const child = root.derivePath(path).neutered();
|
const child = root.derivePath(path).neutered();
|
||||||
|
|
|
@ -10,3 +10,4 @@ export * from './hd-legacy-p2pkh-wallet';
|
||||||
export * from './watch-only-wallet';
|
export * from './watch-only-wallet';
|
||||||
export * from './lightning-custodian-wallet';
|
export * from './lightning-custodian-wallet';
|
||||||
export * from './abstract-hd-wallet';
|
export * from './abstract-hd-wallet';
|
||||||
|
export * from './hd-segwit-bech32-wallet';
|
||||||
|
|
|
@ -114,8 +114,7 @@ export class LegacyWallet extends AbstractWallet {
|
||||||
throw new Error('Could not fetch balance from API: ' + response.err + ' ' + JSON.stringify(response.body));
|
throw new Error('Could not fetch balance from API: ' + response.err + ' ' + JSON.stringify(response.body));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.balance = new BigNumber(json.final_balance);
|
this.balance = Number(json.final_balance);
|
||||||
this.balance = this.balance.dividedBy(100000000).toString() * 1;
|
|
||||||
this.unconfirmed_balance = new BigNumber(json.unconfirmed_balance);
|
this.unconfirmed_balance = new BigNumber(json.unconfirmed_balance);
|
||||||
this.unconfirmed_balance = this.unconfirmed_balance.dividedBy(100000000).toString() * 1;
|
this.unconfirmed_balance = this.unconfirmed_balance.dividedBy(100000000).toString() * 1;
|
||||||
this._lastBalanceFetch = +new Date();
|
this._lastBalanceFetch = +new Date();
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { LegacyWallet } from './legacy-wallet';
|
import { LegacyWallet } from './legacy-wallet';
|
||||||
import Frisbee from 'frisbee';
|
import Frisbee from 'frisbee';
|
||||||
import { BitcoinUnit, Chain } from '../models/bitcoinUnits';
|
import { BitcoinUnit, Chain } from '../models/bitcoinUnits';
|
||||||
let BigNumber = require('bignumber.js');
|
|
||||||
|
|
||||||
export class LightningCustodianWallet extends LegacyWallet {
|
export class LightningCustodianWallet extends LegacyWallet {
|
||||||
static type = 'lightningCustodianWallet';
|
static type = 'lightningCustodianWallet';
|
||||||
|
@ -56,11 +55,11 @@ export class LightningCustodianWallet extends LegacyWallet {
|
||||||
}
|
}
|
||||||
|
|
||||||
timeToRefreshBalance() {
|
timeToRefreshBalance() {
|
||||||
return (+new Date() - this._lastBalanceFetch) / 1000 > 3600; // 1hr
|
return (+new Date() - this._lastBalanceFetch) / 1000 > 300; // 5 min
|
||||||
}
|
}
|
||||||
|
|
||||||
timeToRefreshTransaction() {
|
timeToRefreshTransaction() {
|
||||||
return (+new Date() - this._lastTxFetch) / 1000 > 3600; // 1hr
|
return (+new Date() - this._lastTxFetch) / 1000 > 300; // 5 min
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(param) {
|
static fromJson(param) {
|
||||||
|
@ -455,7 +454,7 @@ export class LightningCustodianWallet extends LegacyWallet {
|
||||||
}
|
}
|
||||||
|
|
||||||
getBalance() {
|
getBalance() {
|
||||||
return new BigNumber(this.balance).dividedBy(100000000).toString(10);
|
return this.balance;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchBalance(noRetry) {
|
async fetchBalance(noRetry) {
|
||||||
|
|
|
@ -29,7 +29,6 @@ export class SegwitBech32Wallet extends LegacyWallet {
|
||||||
}
|
}
|
||||||
|
|
||||||
static scriptPubKeyToAddress(scriptPubKey) {
|
static scriptPubKeyToAddress(scriptPubKey) {
|
||||||
const bitcoin = require('bitcoinjs-lib');
|
|
||||||
const scriptPubKey2 = Buffer.from(scriptPubKey, 'hex');
|
const scriptPubKey2 = Buffer.from(scriptPubKey, 'hex');
|
||||||
return bitcoin.address.fromOutputScript(scriptPubKey2, bitcoin.networks.bitcoin);
|
return bitcoin.address.fromOutputScript(scriptPubKey2, bitcoin.networks.bitcoin);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,10 @@ export class SegwitP2SHWallet extends LegacyWallet {
|
||||||
try {
|
try {
|
||||||
let keyPair = bitcoin.ECPair.fromWIF(this.secret);
|
let keyPair = bitcoin.ECPair.fromWIF(this.secret);
|
||||||
let pubKey = keyPair.getPublicKeyBuffer();
|
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 witnessScript = bitcoin.script.witnessPubKeyHash.output.encode(bitcoin.crypto.hash160(pubKey));
|
||||||
let scriptPubKey = bitcoin.script.scriptHash.output.encode(bitcoin.crypto.hash160(witnessScript));
|
let scriptPubKey = bitcoin.script.scriptHash.output.encode(bitcoin.crypto.hash160(witnessScript));
|
||||||
address = bitcoin.address.fromOutputScript(scriptPubKey);
|
address = bitcoin.address.fromOutputScript(scriptPubKey);
|
||||||
|
|
|
@ -4,9 +4,11 @@ import { LightningCustodianWallet } from './lightning-custodian-wallet';
|
||||||
import { HDLegacyBreadwalletWallet } from './hd-legacy-breadwallet-wallet';
|
import { HDLegacyBreadwalletWallet } from './hd-legacy-breadwallet-wallet';
|
||||||
import { HDLegacyP2PKHWallet } from './hd-legacy-p2pkh-wallet';
|
import { HDLegacyP2PKHWallet } from './hd-legacy-p2pkh-wallet';
|
||||||
import { WatchOnlyWallet } from './watch-only-wallet';
|
import { WatchOnlyWallet } from './watch-only-wallet';
|
||||||
|
import { HDSegwitBech32Wallet } from './hd-segwit-bech32-wallet';
|
||||||
|
|
||||||
export default class WalletGradient {
|
export default class WalletGradient {
|
||||||
static hdSegwitP2SHWallet = ['#65ceef', '#68bbe1'];
|
static hdSegwitP2SHWallet = ['#65ceef', '#68bbe1'];
|
||||||
|
static hdSegwitBech32Wallet = ['#68bbe1', '#3b73d4'];
|
||||||
static watchOnlyWallet = ['#7d7d7d', '#4a4a4a'];
|
static watchOnlyWallet = ['#7d7d7d', '#4a4a4a'];
|
||||||
static legacyWallet = ['#40fad1', '#15be98'];
|
static legacyWallet = ['#40fad1', '#15be98'];
|
||||||
static hdLegacyP2PKHWallet = ['#e36dfa', '#bd10e0'];
|
static hdLegacyP2PKHWallet = ['#e36dfa', '#bd10e0'];
|
||||||
|
@ -33,6 +35,9 @@ export default class WalletGradient {
|
||||||
case HDSegwitP2SHWallet.type:
|
case HDSegwitP2SHWallet.type:
|
||||||
gradient = WalletGradient.hdSegwitP2SHWallet;
|
gradient = WalletGradient.hdSegwitP2SHWallet;
|
||||||
break;
|
break;
|
||||||
|
case HDSegwitBech32Wallet.type:
|
||||||
|
gradient = WalletGradient.hdSegwitBech32Wallet;
|
||||||
|
break;
|
||||||
case LightningCustodianWallet.type:
|
case LightningCustodianWallet.type:
|
||||||
gradient = WalletGradient.lightningCustodianWallet;
|
gradient = WalletGradient.lightningCustodianWallet;
|
||||||
break;
|
break;
|
||||||
|
@ -64,6 +69,9 @@ export default class WalletGradient {
|
||||||
case HDSegwitP2SHWallet.type:
|
case HDSegwitP2SHWallet.type:
|
||||||
gradient = WalletGradient.hdSegwitP2SHWallet;
|
gradient = WalletGradient.hdSegwitP2SHWallet;
|
||||||
break;
|
break;
|
||||||
|
case HDSegwitBech32Wallet.type:
|
||||||
|
gradient = WalletGradient.hdSegwitBech32Wallet;
|
||||||
|
break;
|
||||||
case LightningCustodianWallet.type:
|
case LightningCustodianWallet.type:
|
||||||
gradient = WalletGradient.lightningCustodianWallet;
|
gradient = WalletGradient.lightningCustodianWallet;
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
import { LegacyWallet } from './legacy-wallet';
|
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');
|
const bitcoin = require('bitcoinjs-lib');
|
||||||
|
|
||||||
export class WatchOnlyWallet extends LegacyWallet {
|
export class WatchOnlyWallet extends LegacyWallet {
|
||||||
|
@ -18,6 +21,8 @@ export class WatchOnlyWallet extends LegacyWallet {
|
||||||
}
|
}
|
||||||
|
|
||||||
valid() {
|
valid() {
|
||||||
|
if (this.secret.startsWith('xpub') || this.secret.startsWith('ypub') || this.secret.startsWith('zpub')) return true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
bitcoin.address.toOutputScript(this.getAddress());
|
bitcoin.address.toOutputScript(this.getAddress());
|
||||||
return true;
|
return true;
|
||||||
|
@ -25,4 +30,60 @@ export class WatchOnlyWallet extends LegacyWallet {
|
||||||
return false;
|
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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Frisbee from 'frisbee';
|
import Frisbee from 'frisbee';
|
||||||
import { AsyncStorage } from 'react-native';
|
import AsyncStorage from '@react-native-community/async-storage';
|
||||||
import { AppStorage } from './class';
|
import { AppStorage } from './class';
|
||||||
import { FiatUnit } from './models/fiatUnit';
|
import { FiatUnit } from './models/fiatUnit';
|
||||||
let BigNumber = require('bignumber.js');
|
let BigNumber = require('bignumber.js');
|
||||||
|
|
4
edit-version-number.sh
Executable 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
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1010"
|
LastUpgradeVersion = "1020"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "NO"
|
parallelizeBuildables = "NO"
|
||||||
|
|
|
@ -1,25 +1,11 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1010"
|
LastUpgradeVersion = "1020"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "NO"
|
parallelizeBuildables = "YES"
|
||||||
buildImplicitDependencies = "YES">
|
buildImplicitDependencies = "YES">
|
||||||
<BuildActionEntries>
|
<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
|
<BuildActionEntry
|
||||||
buildForTesting = "YES"
|
buildForTesting = "YES"
|
||||||
buildForRunning = "YES"
|
buildForRunning = "YES"
|
||||||
|
@ -34,20 +20,6 @@
|
||||||
ReferencedContainer = "container:BlueWallet.xcodeproj">
|
ReferencedContainer = "container:BlueWallet.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</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>
|
</BuildActionEntries>
|
||||||
</BuildAction>
|
</BuildAction>
|
||||||
<TestAction
|
<TestAction
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -4,16 +4,59 @@
|
||||||
<dict>
|
<dict>
|
||||||
<key>SchemeUserState</key>
|
<key>SchemeUserState</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>BlueWallet-tvOS.xcscheme_^#shared#^_</key>
|
<key>BlueWallet for Apple Watch (Notification).xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>1</integer>
|
<integer>78</integer>
|
||||||
</dict>
|
</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>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>0</integer>
|
<integer>0</integer>
|
||||||
</dict>
|
</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>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
19
ios/BlueWallet.xcworkspace/contents.xcworkspacedata
generated
Normal 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>
|
|
@ -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>
|
|
@ -6,9 +6,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
#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) UIWindow *window;
|
||||||
|
@property(nonatomic, strong) WatchBridge *watchBridge;
|
||||||
|
@property(nonatomic, strong) WCSession *session;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
#else
|
#else
|
||||||
#import "RNSentry.h" // This is used for versions of react < 0.40
|
#import "RNSentry.h" // This is used for versions of react < 0.40
|
||||||
#endif
|
#endif
|
||||||
|
#import "WatchBridge.h"
|
||||||
|
|
||||||
@implementation AppDelegate
|
@implementation AppDelegate
|
||||||
|
|
||||||
|
@ -35,6 +36,11 @@
|
||||||
rootViewController.view = rootView;
|
rootViewController.view = rootView;
|
||||||
self.window.rootViewController = rootViewController;
|
self.window.rootViewController = rootViewController;
|
||||||
[self.window makeKeyAndVisible];
|
[self.window makeKeyAndVisible];
|
||||||
|
self.watchBridge = [WatchBridge shared];
|
||||||
|
self.session = self.watchBridge.session;
|
||||||
|
[self.session activateSession];
|
||||||
|
self.session.delegate = self;
|
||||||
|
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,4 +52,18 @@
|
||||||
return NO;
|
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
|
@end
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>3.9.4</string>
|
<string>4.0.3</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
|
@ -29,6 +29,9 @@
|
||||||
<array>
|
<array>
|
||||||
<string>bitcoin</string>
|
<string>bitcoin</string>
|
||||||
<string>lightning</string>
|
<string>lightning</string>
|
||||||
|
<string>bluewallet</string>
|
||||||
|
<string>lapp</string>
|
||||||
|
<string>blue</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
|
|
56
ios/BlueWalletWatch Extension/ExtensionDelegate.swift
Normal 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 you’re 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 you’re done.
|
||||||
|
connectivityTask.setTaskCompletedWithSnapshot(false)
|
||||||
|
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
|
||||||
|
// Be sure to complete the URL session task once you’re 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
38
ios/BlueWalletWatch Extension/Info.plist
Normal 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>
|
57
ios/BlueWalletWatch Extension/InterfaceController.swift
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
38
ios/BlueWalletWatch Extension/NotificationController.swift
Normal 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.
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
39
ios/BlueWalletWatch Extension/Objects/Transaction.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
51
ios/BlueWalletWatch Extension/Objects/Wallet.swift
Normal 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]()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
32
ios/BlueWalletWatch Extension/Objects/WalletGradient.swift
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
103
ios/BlueWalletWatch Extension/Objects/WatchDataSource.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
20
ios/BlueWalletWatch Extension/PushNotificationPayload.apns
Normal 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."
|
||||||
|
}
|
115
ios/BlueWalletWatch Extension/ReceiveInterfaceController.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
BIN
ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/1024.png
Normal file
After Width: | Height: | Size: 193 KiB |
BIN
ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/58.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/87.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 7.5 KiB |
After Width: | Height: | Size: 5 KiB |
After Width: | Height: | Size: 9 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 9.4 KiB |
BIN
ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/watch.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
6
ios/BlueWalletWatch/Assets.xcassets/Contents.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
13
ios/BlueWalletWatch/Assets.xcassets/loadingIndicator.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"filename" : "group-copy-2@3x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
BIN
ios/BlueWalletWatch/Assets.xcassets/loadingIndicator.imageset/group-copy-2@3x.png
vendored
Normal file
After Width: | Height: | Size: 14 KiB |
13
ios/BlueWalletWatch/Assets.xcassets/pendingConfirmation.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"filename" : "shape@3x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
BIN
ios/BlueWalletWatch/Assets.xcassets/pendingConfirmation.imageset/shape@3x.png
vendored
Normal file
After Width: | Height: | Size: 692 B |
13
ios/BlueWalletWatch/Assets.xcassets/qr-code.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"filename" : "qr-code@3x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
BIN
ios/BlueWalletWatch/Assets.xcassets/qr-code.imageset/qr-code@3x.png
vendored
Normal file
After Width: | Height: | Size: 112 KiB |
13
ios/BlueWalletWatch/Assets.xcassets/receivedArrow.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"filename" : "path-copy-3@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
BIN
ios/BlueWalletWatch/Assets.xcassets/receivedArrow.imageset/path-copy-3@2x.png
vendored
Normal file
After Width: | Height: | Size: 447 B |
13
ios/BlueWalletWatch/Assets.xcassets/sentArrow.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"filename" : "path-copy@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
BIN
ios/BlueWalletWatch/Assets.xcassets/sentArrow.imageset/path-copy@2x.png
vendored
Normal file
After Width: | Height: | Size: 471 B |
23
ios/BlueWalletWatch/Assets.xcassets/wallet.imageset/Contents.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
BIN
ios/BlueWalletWatch/Assets.xcassets/wallet.imageset/mask.png
vendored
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
ios/BlueWalletWatch/Assets.xcassets/wallet.imageset/mask@2x.png
vendored
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
ios/BlueWalletWatch/Assets.xcassets/wallet.imageset/mask@3x.png
vendored
Normal file
After Width: | Height: | Size: 25 KiB |
23
ios/BlueWalletWatch/Assets.xcassets/walletACINQ.imageset/Contents.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
BIN
ios/BlueWalletWatch/Assets.xcassets/walletACINQ.imageset/mask.png
vendored
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
ios/BlueWalletWatch/Assets.xcassets/walletACINQ.imageset/mask@2x.png
vendored
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
ios/BlueWalletWatch/Assets.xcassets/walletACINQ.imageset/mask@3x.png
vendored
Normal file
After Width: | Height: | Size: 25 KiB |
13
ios/BlueWalletWatch/Assets.xcassets/walletHD.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"filename" : "mask@3x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
BIN
ios/BlueWalletWatch/Assets.xcassets/walletHD.imageset/mask@3x.png
vendored
Normal file
After Width: | Height: | Size: 25 KiB |