Browse Source

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

singleaddress
ncoelho 5 years ago
parent
commit
4e3ffd833e
  1. 2
      .flowconfig
  2. 4
      .gitignore
  3. 26
      App.js
  4. 28
      App.test.js
  5. 50
      App2.test.js
  6. 2
      BlueApp.js
  7. 127
      BlueComponents.js
  8. 129
      BlueElectrum.js
  9. 81
      Electrum.test.js
  10. 265
      HDBech32Wallet.test.js
  11. 24
      HDWallet.test.js
  12. 11
      LightningCustodianWallet.test.js
  13. 4
      MainBottomTabs.js
  14. 2
      README.md
  15. 138
      WatchConnectivity.js
  16. 114
      WatchOnlyWallet.test.js
  17. 1
      __mocks__/@react-native-community/async-storage.js
  18. 151
      android/app/app.iml
  19. 3
      android/app/build.gradle
  20. BIN
      android/app/src/main/assets/fonts/FontAwesome5_Brands.ttf
  21. BIN
      android/app/src/main/assets/fonts/FontAwesome5_Regular.ttf
  22. BIN
      android/app/src/main/assets/fonts/FontAwesome5_Solid.ttf
  23. BIN
      android/app/src/main/assets/fonts/MaterialCommunityIcons.ttf
  24. BIN
      android/app/src/main/assets/fonts/Octicons.ttf
  25. 2
      android/app/src/main/java/io/bluewallet/bluewallet/MainApplication.java
  26. 2
      android/settings.gradle
  27. 93
      class/abstract-hd-wallet.js
  28. 6
      class/abstract-wallet.js
  29. 28
      class/app-storage.js
  30. 492
      class/hd-segwit-bech32-wallet.js
  31. 35
      class/hd-segwit-p2sh-wallet.js
  32. 1
      class/index.js
  33. 3
      class/legacy-wallet.js
  34. 7
      class/lightning-custodian-wallet.js
  35. 1
      class/segwit-bech-wallet.js
  36. 4
      class/segwit-p2sh-wallet.js
  37. 8
      class/walletGradient.js
  38. 61
      class/watch-only-wallet.js
  39. 2
      currency.js
  40. 4
      edit-version-number.sh
  41. 2161
      ios/BlueWallet.xcodeproj/project.pbxproj
  42. 2
      ios/BlueWallet.xcodeproj/xcshareddata/xcschemes/BlueWallet-tvOS.xcscheme
  43. 32
      ios/BlueWallet.xcodeproj/xcshareddata/xcschemes/BlueWallet.xcscheme
  44. 131
      ios/BlueWallet.xcodeproj/xcshareddata/xcschemes/BlueWalletWatch (Notification).xcscheme
  45. 128
      ios/BlueWallet.xcodeproj/xcshareddata/xcschemes/BlueWalletWatch.xcscheme
  46. 47
      ios/BlueWallet.xcodeproj/xcuserdata/marcosrodriguez.xcuserdatad/xcschemes/xcschememanagement.plist
  47. 19
      ios/BlueWallet.xcworkspace/contents.xcworkspacedata
  48. 8
      ios/BlueWallet.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  49. 6
      ios/BlueWallet/AppDelegate.h
  50. 20
      ios/BlueWallet/AppDelegate.m
  51. 2
      ios/BlueWallet/Info.plist
  52. 56
      ios/BlueWalletWatch Extension/ExtensionDelegate.swift
  53. 38
      ios/BlueWalletWatch Extension/Info.plist
  54. 57
      ios/BlueWalletWatch Extension/InterfaceController.swift
  55. 38
      ios/BlueWalletWatch Extension/NotificationController.swift
  56. 155
      ios/BlueWalletWatch Extension/NumericKeypadInterfaceController.swift
  57. 39
      ios/BlueWalletWatch Extension/Objects/Transaction.swift
  58. 52
      ios/BlueWalletWatch Extension/Objects/TransactionTableRow.swift
  59. 51
      ios/BlueWalletWatch Extension/Objects/Wallet.swift
  60. 32
      ios/BlueWalletWatch Extension/Objects/WalletGradient.swift
  61. 36
      ios/BlueWalletWatch Extension/Objects/WalletInformation.swift
  62. 103
      ios/BlueWalletWatch Extension/Objects/WatchDataSource.swift
  63. 20
      ios/BlueWalletWatch Extension/PushNotificationPayload.apns
  64. 115
      ios/BlueWalletWatch Extension/ReceiveInterfaceController.swift
  65. 90
      ios/BlueWalletWatch Extension/SpecifyInterfaceController.swift
  66. 68
      ios/BlueWalletWatch Extension/WalletDetailsInterfaceController.swift
  67. BIN
      ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/1024.png
  68. BIN
      ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/58.png
  69. BIN
      ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/87.png
  70. 92
      ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/Contents.json
  71. BIN
      ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/Icon-172.png
  72. BIN
      ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/Icon-173.png
  73. BIN
      ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/Icon-196.png
  74. BIN
      ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/Icon-48.png
  75. BIN
      ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/Icon-55.png
  76. BIN
      ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/Icon-88.png
  77. BIN
      ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/group-copy-2@3x.png
  78. BIN
      ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/watch.png
  79. 6
      ios/BlueWalletWatch/Assets.xcassets/Contents.json
  80. 13
      ios/BlueWalletWatch/Assets.xcassets/loadingIndicator.imageset/Contents.json
  81. BIN
      ios/BlueWalletWatch/Assets.xcassets/loadingIndicator.imageset/group-copy-2@3x.png
  82. 13
      ios/BlueWalletWatch/Assets.xcassets/pendingConfirmation.imageset/Contents.json
  83. BIN
      ios/BlueWalletWatch/Assets.xcassets/pendingConfirmation.imageset/shape@3x.png
  84. 13
      ios/BlueWalletWatch/Assets.xcassets/qr-code.imageset/Contents.json
  85. BIN
      ios/BlueWalletWatch/Assets.xcassets/qr-code.imageset/qr-code@3x.png
  86. 13
      ios/BlueWalletWatch/Assets.xcassets/receivedArrow.imageset/Contents.json
  87. BIN
      ios/BlueWalletWatch/Assets.xcassets/receivedArrow.imageset/path-copy-3@2x.png
  88. 13
      ios/BlueWalletWatch/Assets.xcassets/sentArrow.imageset/Contents.json
  89. BIN
      ios/BlueWalletWatch/Assets.xcassets/sentArrow.imageset/path-copy@2x.png
  90. 23
      ios/BlueWalletWatch/Assets.xcassets/wallet.imageset/Contents.json
  91. BIN
      ios/BlueWalletWatch/Assets.xcassets/wallet.imageset/mask.png
  92. BIN
      ios/BlueWalletWatch/Assets.xcassets/wallet.imageset/mask@2x.png
  93. BIN
      ios/BlueWalletWatch/Assets.xcassets/wallet.imageset/mask@3x.png
  94. 23
      ios/BlueWalletWatch/Assets.xcassets/walletACINQ.imageset/Contents.json
  95. BIN
      ios/BlueWalletWatch/Assets.xcassets/walletACINQ.imageset/mask.png
  96. BIN
      ios/BlueWalletWatch/Assets.xcassets/walletACINQ.imageset/mask@2x.png
  97. BIN
      ios/BlueWalletWatch/Assets.xcassets/walletACINQ.imageset/mask@3x.png
  98. 13
      ios/BlueWalletWatch/Assets.xcassets/walletHD.imageset/Contents.json
  99. BIN
      ios/BlueWalletWatch/Assets.xcassets/walletHD.imageset/mask@3x.png
  100. 13
      ios/BlueWalletWatch/Assets.xcassets/walletLightningCustodial.imageset/Contents.json

2
.flowconfig

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

4
.gitignore

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

26
App.js

@ -1,5 +1,6 @@
import React from 'react';
import { Linking, AppState, Clipboard, StyleSheet, KeyboardAvoidingView, Platform, View, AsyncStorage } 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 { NavigationActions } from 'react-navigation';
import MainBottomTabs from './MainBottomTabs';
@ -94,6 +95,12 @@ export default class App extends React.Component {
return isValidLightningInvoice;
}
isSafelloRedirect(event) {
let urlObject = url.parse(event.url, true) // eslint-disable-line
return !!urlObject.query['safello-state-token'];
}
handleOpenURL = event => {
if (event.url === null) {
return;
@ -121,6 +128,21 @@ 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);
@ -194,7 +216,7 @@ export default class App extends React.Component {
renderClipboardContentModal = () => {
return (
<Modal
onModalShow={() => ReactNativeHapticFeedback.trigger('impactLight', false)}
onModalShow={() => ReactNativeHapticFeedback.trigger('impactLight', { ignoreAndroidSystemSettings: false })}
isVisible={this.state.isClipboardContentModalVisible}
style={styles.bottomModal}
onBackdropPress={() => {

28
App.test.js

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

50
App2.test.js

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

2
BlueApp.js

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

127
BlueComponents.js

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

129
BlueElectrum.js

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

81
Electrum.test.js

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

265
HDBech32Wallet.test.js

@ -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);
});
});

24
HDWallet.test.js

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

11
LightningCustodianWallet.test.js

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

4
MainBottomTabs.js

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

2
README.md

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

138
WatchConnectivity.js

@ -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

@ -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

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

151
android/app/app.iml

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

3
android/app/build.gradle

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

BIN
android/app/src/main/assets/fonts/FontAwesome5_Brands.ttf

Binary file not shown.

BIN
android/app/src/main/assets/fonts/FontAwesome5_Regular.ttf

Binary file not shown.

BIN
android/app/src/main/assets/fonts/FontAwesome5_Solid.ttf

Binary file not shown.

BIN
android/app/src/main/assets/fonts/MaterialCommunityIcons.ttf

Binary file not shown.

BIN
android/app/src/main/assets/fonts/Octicons.ttf

Binary file not shown.

2
android/app/src/main/java/io/bluewallet/bluewallet/MainApplication.java

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

2
android/settings.gradle

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

93
class/abstract-hd-wallet.js

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

6
class/abstract-wallet.js

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

28
class/app-storage.js

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

492
class/hd-segwit-bech32-wallet.js

@ -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 };
}
}

35
class/hd-segwit-p2sh-wallet.js

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

1
class/index.js

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

3
class/legacy-wallet.js

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

7
class/lightning-custodian-wallet.js

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

1
class/segwit-bech-wallet.js

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

4
class/segwit-p2sh-wallet.js

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

8
class/walletGradient.js

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

61
class/watch-only-wallet.js

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

2
currency.js

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

4
edit-version-number.sh

@ -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

2161
ios/BlueWallet.xcodeproj/project.pbxproj

File diff suppressed because it is too large

2
ios/BlueWallet.xcodeproj/xcshareddata/xcschemes/BlueWallet-tvOS.xcscheme

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

32
ios/BlueWallet.xcodeproj/xcshareddata/xcschemes/BlueWallet.xcscheme

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

131
ios/BlueWallet.xcodeproj/xcshareddata/xcschemes/BlueWalletWatch (Notification).xcscheme

@ -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>

128
ios/BlueWallet.xcodeproj/xcshareddata/xcschemes/BlueWalletWatch.xcscheme

@ -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>

47
ios/BlueWallet.xcodeproj/xcuserdata/marcosrodriguez.xcuserdatad/xcschemes/xcschememanagement.plist

@ -4,15 +4,58 @@
<dict>
<key>SchemeUserState</key>
<dict>
<key>BlueWallet for Apple Watch (Notification).xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>78</integer>
</dict>
<key>BlueWallet for Apple Watch.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>71</integer>
</dict>
<key>BlueWallet-tvOS.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
<integer>0</integer>
</dict>
<key>BlueWallet.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
<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>

19
ios/BlueWallet.xcworkspace/contents.xcworkspacedata

@ -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>

8
ios/BlueWallet.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

@ -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
ios/BlueWallet/AppDelegate.h

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

20
ios/BlueWallet/AppDelegate.m

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

2
ios/BlueWallet/Info.plist

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>3.9.5</string>
<string>4.0.3</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>

56
ios/BlueWalletWatch Extension/ExtensionDelegate.swift

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

38
ios/BlueWalletWatch Extension/Info.plist

@ -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

@ -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

@ -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.
}
}

155
ios/BlueWalletWatch Extension/NumericKeypadInterfaceController.swift

@ -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

@ -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
}
}

52
ios/BlueWalletWatch Extension/Objects/TransactionTableRow.swift

@ -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

@ -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

@ -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"
}
}
}

36
ios/BlueWalletWatch Extension/Objects/WalletInformation.swift

@ -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

@ -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

@ -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

@ -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)
}
}

90
ios/BlueWalletWatch Extension/SpecifyInterfaceController.swift

@ -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
}
}

68
ios/BlueWalletWatch Extension/WalletDetailsInterfaceController.swift

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/58.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/87.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

92
ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/Contents.json

@ -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"
}
}

BIN
ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/Icon-172.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/Icon-173.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/Icon-196.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/Icon-48.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/Icon-55.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/Icon-88.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/group-copy-2@3x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/AppIcon.appiconset/watch.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

6
ios/BlueWalletWatch/Assets.xcassets/Contents.json

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

13
ios/BlueWalletWatch/Assets.xcassets/loadingIndicator.imageset/Contents.json

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

13
ios/BlueWalletWatch/Assets.xcassets/pendingConfirmation.imageset/Contents.json

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 B

13
ios/BlueWalletWatch/Assets.xcassets/qr-code.imageset/Contents.json

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

13
ios/BlueWalletWatch/Assets.xcassets/receivedArrow.imageset/Contents.json

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 B

13
ios/BlueWalletWatch/Assets.xcassets/sentArrow.imageset/Contents.json

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

23
ios/BlueWalletWatch/Assets.xcassets/wallet.imageset/Contents.json

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/wallet.imageset/mask@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/wallet.imageset/mask@3x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

23
ios/BlueWalletWatch/Assets.xcassets/walletACINQ.imageset/Contents.json

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/walletACINQ.imageset/mask@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/walletACINQ.imageset/mask@3x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

13
ios/BlueWalletWatch/Assets.xcassets/walletHD.imageset/Contents.json

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

13
ios/BlueWalletWatch/Assets.xcassets/walletLightningCustodial.imageset/Contents.json

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

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

Loading…
Cancel
Save