Browse Source

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

singleaddress
ncoelho 5 years ago
parent
commit
bdfb114d56
  1. 29
      App.js
  2. 2
      BlueApp.js
  3. 559
      BlueComponents.js
  4. 211
      BlueElectrum.js
  5. 25
      MainBottomTabs.js
  6. 5
      SECURITY.md
  7. 10
      WatchConnectivity.js
  8. 19
      analytics.js
  9. 19
      android/BlueWallet.iml
  10. 16
      android/android.iml
  11. 192
      android/app/app.iml
  12. 61
      android/app/build.gradle
  13. 7
      android/app/proguard-rules.pro
  14. 8
      android/app/src/debug/AndroidManifest.xml
  15. 4
      android/app/src/main/AndroidManifest.xml
  16. 3
      android/app/src/main/assets/appcenter-config.json
  17. 17
      android/app/src/main/java/io/bluewallet/bluewallet/MainActivity.java
  18. 66
      android/app/src/main/java/io/bluewallet/bluewallet/MainApplication.java
  19. 2
      android/app/src/main/res/values/strings.xml
  20. 1
      android/app/src/main/res/values/styles.xml
  21. 23
      android/build.gradle
  22. 1
      android/build.sh
  23. 3
      android/gradle.properties
  24. BIN
      android/gradle/wrapper/gradle-wrapper.jar
  25. 2
      android/gradle/wrapper/gradle-wrapper.properties
  26. 18
      android/gradlew
  27. 18
      android/gradlew.bat
  28. 8
      android/keystores/BUCK
  29. 4
      android/keystores/debug.keystore.properties
  30. 10
      android/metadata/en-US/changelogs/14.txt
  31. 9
      android/metadata/en-US/changelogs/15.txt
  32. 1
      android/metadata/en-US/changelogs/5.txt
  33. 1
      android/metadata/en-US/changelogs/7.txt
  34. 1
      android/metadata/en-US/changelogs/8.txt
  35. 1
      android/metadata/en-US/changelogs/9.txt
  36. 39
      android/metadata/en-US/full_description.txt
  37. BIN
      android/metadata/en-US/images/icon.png
  38. BIN
      android/metadata/en-US/images/promoGraphic.png
  39. 1
      android/metadata/en-US/short_description.txt
  40. 1
      android/metadata/en-US/title.txt
  41. 0
      android/metadata/en-US/video.txt
  42. 5
      android/sentry.properties
  43. 40
      android/settings.gradle
  44. 1
      android/upload.sh
  45. 17
      class/abstract-wallet.js
  46. 74
      class/app-storage.js
  47. 332
      class/hd-segwit-bech32-transaction.js
  48. 361
      class/hd-segwit-bech32-wallet.js
  49. 22
      class/hd-segwit-p2sh-wallet.js
  50. 1
      class/index.js
  51. 6
      class/lightning-custodian-wallet.js
  52. 6
      currency.js
  53. 2
      edit-version-number.sh
  54. 1
      img/bluewalletsplash.json
  55. BIN
      img/close-white.png
  56. BIN
      img/close-white@2x.png
  57. BIN
      img/close-white@3x.png
  58. 45
      index.js
  59. 8
      ios/AppCenter-Config.plist
  60. 4
      ios/BlueWallet-Bridging-Header.h
  61. 48
      ios/BlueWallet.xcodeproj/project.pbxproj
  62. 9
      ios/BlueWallet/AppDelegate.m
  63. 5
      ios/BlueWallet/BlueWallet.entitlements
  64. 4
      ios/BlueWallet/Info.plist
  65. 5
      ios/BlueWalletWatch Extension/BlueWalletWatch Extension.entitlements
  66. 56
      ios/BlueWalletWatch Extension/ComplicationController.swift
  67. 17
      ios/BlueWalletWatch Extension/Info.plist
  68. 6
      ios/BlueWalletWatch Extension/Objects/WalletGradient.swift
  69. 2
      ios/BlueWalletWatch Extension/ReceiveInterfaceController.swift
  70. 32
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json
  71. BIN
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Circular.imageset/circular38mm@2x.png
  72. BIN
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Circular.imageset/circular40mm@2x.png
  73. BIN
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Circular.imageset/circular42mm@2x.png
  74. BIN
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Circular.imageset/circular44mm@2x.png
  75. 48
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Contents.json
  76. 32
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json
  77. BIN
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Extra Large.imageset/extra-large38mm@2x.png
  78. BIN
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Extra Large.imageset/extra-large40mm@2x.png
  79. BIN
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Extra Large.imageset/extra-large42mm@2x.png
  80. BIN
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Extra Large.imageset/extra-large44mm@2x.png
  81. 30
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json
  82. BIN
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/graphic-bezel40mm@2x.png
  83. BIN
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/graphic-bezel44mm@2x.png
  84. 30
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json
  85. BIN
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/graphic-circular40mm@2x.png
  86. BIN
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/graphic-circular44mm@2x.png
  87. 30
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json
  88. BIN
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/graphic-corner40mm@2x.png
  89. BIN
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/graphic-corner44mm@2x.png
  90. 28
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json
  91. 32
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json
  92. BIN
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Modular.imageset/modular38mm@2x.png
  93. BIN
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Modular.imageset/modular40mm@2x.png
  94. BIN
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Modular.imageset/modular42mm@2x.png
  95. BIN
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Modular.imageset/modular44mm@2x.png
  96. 32
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json
  97. BIN
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/utility38mm@2x.png
  98. BIN
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/utility40mm@2x.png
  99. BIN
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/utility42mm@2x.png
  100. BIN
      ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/utility44mm@2x.png

29
App.js

@ -9,6 +9,7 @@ import { BlueTextCentered, BlueButton } from './BlueComponents';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import url from 'url';
import { AppStorage, LightningCustodianWallet } from './class';
import { Chain } from './models/bitcoinUnits';
const bitcoin = require('bitcoinjs-lib');
const bitcoinModalString = 'Bitcoin address';
const lightningModalString = 'Lightning Invoice';
@ -47,7 +48,14 @@ export default class App extends React.Component {
if (BlueApp.getWallets().length > 0) {
if (this.state.appState.match(/inactive|background/) && nextAppState === 'active') {
const clipboard = await Clipboard.getString();
if (this.state.clipboardContent !== clipboard && (this.isBitcoinAddress(clipboard) || this.isLightningInvoice(clipboard))) {
const isAddressFromStoredWallet = BlueApp.getWallets().some(wallet =>
wallet.chain === Chain.ONCHAIN ? wallet.weOwnAddress(clipboard) : wallet.isInvoiceGeneratedByWallet(clipboard),
);
if (
!isAddressFromStoredWallet &&
this.state.clipboardContent !== clipboard &&
(this.isBitcoinAddress(clipboard) || this.isLightningInvoice(clipboard) || this.isLnUrl(clipboard))
) {
this.setState({ isClipboardContentModalVisible: true });
}
this.setState({ clipboardContent: clipboard });
@ -88,13 +96,20 @@ export default class App extends React.Component {
isLightningInvoice(invoice) {
let isValidLightningInvoice = false;
if (invoice.indexOf('lightning:lnb') === 0 || invoice.indexOf('LIGHTNING:lnb') === 0 || invoice.toLowerCase().startsWith('lnb')) {
if (invoice.toLowerCase().startsWith('lightning:lnb') || invoice.toLowerCase().startsWith('lnb')) {
this.setState({ clipboardContentModalAddressType: lightningModalString });
isValidLightningInvoice = true;
}
return isValidLightningInvoice;
}
isLnUrl(text) {
if (text.toLowerCase().startsWith('lightning:lnurl') || text.toLowerCase().startsWith('lnurl')) {
return true;
}
return false;
}
isSafelloRedirect(event) {
let urlObject = url.parse(event.url, true) // eslint-disable-line
@ -128,6 +143,16 @@ export default class App extends React.Component {
},
}),
);
} else if (this.isLnUrl(event.url)) {
this.navigator &&
this.navigator.dispatch(
NavigationActions.navigate({
routeName: 'LNDCreateInvoice',
params: {
uri: event.url,
},
}),
);
} else if (this.isSafelloRedirect(event)) {
let urlObject = url.parse(event.url, true) // eslint-disable-line

2
BlueApp.js

@ -6,7 +6,6 @@ let prompt = require('./prompt');
let EV = require('./events');
let currency = require('./currency');
let loc = require('./loc');
let A = require('./analytics');
let BlueElectrum = require('./BlueElectrum'); // eslint-disable-line
/** @type {AppStorage} */
@ -65,7 +64,6 @@ async function startAndDecrypt(retry) {
}
}
A(A.ENUM.INIT);
BlueApp.startAndDecrypt = startAndDecrypt;
currency.startUpdater();

559
BlueComponents.js

@ -11,12 +11,14 @@ import {
Animated,
ActivityIndicator,
View,
KeyboardAvoidingView,
UIManager,
StyleSheet,
Dimensions,
Image,
Keyboard,
SafeAreaView,
InteractionManager,
InputAccessoryView,
Clipboard,
Platform,
@ -29,6 +31,10 @@ import { BitcoinUnit } from './models/bitcoinUnits';
import NavigationService from './NavigationService';
import ImagePicker from 'react-native-image-picker';
import WalletGradient from './class/walletGradient';
import ToolTip from 'react-native-tooltip';
import { BlurView } from '@react-native-community/blur';
import showPopupMenu from 'react-native-popup-menu-android';
import NetworkTransactionFees, { NetworkTransactionFeeType } from './models/networkTransactionFees';
const LocalQRCode = require('@remobile/react-native-qrcode-local-image');
let loc = require('./loc/');
/** @type {AppStorage} */
@ -98,7 +104,6 @@ export class BitcoinButton extends Component {
borderRadius: 5,
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,
@ -136,7 +141,6 @@ export class LightningButton extends Component {
borderRadius: 5,
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,
@ -157,6 +161,180 @@ export class LightningButton extends Component {
}
}
export class BlueWalletNavigationHeader extends Component {
static propTypes = {
wallet: PropTypes.shape().isRequired,
onWalletUnitChange: PropTypes.func,
};
static getDerivedStateFromProps(props, _state) {
return { wallet: props.wallet, onWalletUnitChange: props.onWalletUnitChange };
}
constructor(props) {
super(props);
this.state = { wallet: props.wallet, walletPreviousPreferredUnit: props.wallet.getPreferredBalanceUnit() };
}
handleCopyPress = _item => {
Clipboard.setString(loc.formatBalance(this.state.wallet.getBalance(), this.state.wallet.getPreferredBalanceUnit()).toString());
};
handleBalanceVisibility = async _item => {
const wallet = this.state.wallet;
wallet.hideBalance = !wallet.hideBalance;
this.setState({ wallet });
await BlueApp.saveToDisk();
};
showAndroidTooltip = () => {
showPopupMenu(this.toolTipMenuOptions(), this.handleToolTipSelection, this.walletBalanceText);
};
handleToolTipSelection = item => {
if (item === loc.transactions.details.copy || item.id === loc.transactions.details.copy) {
this.handleCopyPress();
} else if (item === 'balancePrivacy' || item.id === 'balancePrivacy') {
this.handleBalanceVisibility();
}
};
toolTipMenuOptions() {
return Platform.select({
// NOT WORKING ATM.
// ios: [
// { text: this.state.wallet.hideBalance ? 'Show Balance' : 'Hide Balance', onPress: this.handleBalanceVisibility },
// { text: loc.transactions.details.copy, onPress: this.handleCopyPress },
// ],
android: this.state.wallet.hideBalance
? [{ id: 'balancePrivacy', label: this.state.wallet.hideBalance ? 'Show Balance' : 'Hide Balance' }]
: [
{ id: 'balancePrivacy', label: this.state.wallet.hideBalance ? 'Show Balance' : 'Hide Balance' },
{ id: loc.transactions.details.copy, label: loc.transactions.details.copy },
],
});
}
changeWalletBalanceUnit() {
let walletPreviousPreferredUnit = this.state.wallet.getPreferredBalanceUnit();
const wallet = this.state.wallet;
if (walletPreviousPreferredUnit === BitcoinUnit.BTC) {
wallet.preferredBalanceUnit = BitcoinUnit.SATS;
walletPreviousPreferredUnit = BitcoinUnit.BTC;
} else if (walletPreviousPreferredUnit === BitcoinUnit.SATS) {
wallet.preferredBalanceUnit = BitcoinUnit.LOCAL_CURRENCY;
walletPreviousPreferredUnit = BitcoinUnit.SATS;
} else if (walletPreviousPreferredUnit === BitcoinUnit.LOCAL_CURRENCY) {
wallet.preferredBalanceUnit = BitcoinUnit.BTC;
walletPreviousPreferredUnit = BitcoinUnit.BTC;
} else {
wallet.preferredBalanceUnit = BitcoinUnit.BTC;
walletPreviousPreferredUnit = BitcoinUnit.BTC;
}
this.setState({ wallet, walletPreviousPreferredUnit: walletPreviousPreferredUnit }, () => {
this.props.onWalletUnitChange(wallet);
});
}
render() {
return (
<LinearGradient
colors={WalletGradient.gradientsFor(this.state.wallet.type)}
style={{ padding: 15, minHeight: 140, justifyContent: 'center' }}
>
<Image
source={
(LightningCustodianWallet.type === this.state.wallet.type && require('./img/lnd-shape.png')) || require('./img/btc-shape.png')
}
style={{
width: 99,
height: 94,
position: 'absolute',
bottom: 0,
right: 0,
}}
/>
<Text
numberOfLines={1}
style={{
backgroundColor: 'transparent',
fontSize: 19,
color: '#fff',
}}
>
{this.state.wallet.getLabel()}
</Text>
{Platform.OS === 'ios' && (
<ToolTip
ref={tooltip => (this.tooltip = tooltip)}
actions={
this.state.wallet.hideBalance
? [{ text: this.state.wallet.hideBalance ? 'Show Balance' : 'Hide Balance', onPress: this.handleBalanceVisibility }]
: [
{ text: this.state.wallet.hideBalance ? 'Show Balance' : 'Hide Balance', onPress: this.handleBalanceVisibility },
{ text: loc.transactions.details.copy, onPress: this.handleCopyPress },
]
}
/>
)}
<TouchableOpacity
style={styles.balance}
onPress={() => this.changeWalletBalanceUnit()}
ref={ref => (this.walletBalanceText = ref)}
onLongPress={() => (Platform.OS === 'ios' ? this.tooltip.showMenu() : this.showAndroidTooltip())}
>
{this.state.wallet.hideBalance ? (
<BluePrivateBalance />
) : (
<Text
numberOfLines={1}
adjustsFontSizeToFit
style={{
backgroundColor: 'transparent',
fontWeight: 'bold',
fontSize: 36,
color: '#fff',
}}
>
{loc.formatBalance(this.state.wallet.getBalance(), this.state.wallet.getPreferredBalanceUnit(), true).toString()}
</Text>
)}
</TouchableOpacity>
{this.state.wallet.type === LightningCustodianWallet.type && (
<TouchableOpacity onPress={() => NavigationService.navigate('ManageFunds', { fromWallet: this.state.wallet })}>
<View
style={{
marginTop: 14,
marginBottom: 10,
backgroundColor: 'rgba(255,255,255,0.2)',
borderRadius: 9,
minWidth: 119,
minHeight: 39,
width: 119,
height: 39,
justifyContent: 'center',
alignItems: 'center',
}}
>
<Text
style={{
fontWeight: '500',
fontSize: 14,
color: '#FFFFFF',
}}
>
{loc.lnd.title}
</Text>
</View>
</TouchableOpacity>
)}
</LinearGradient>
);
}
}
export class BlueButtonLink extends Component {
render() {
return (
@ -204,6 +382,53 @@ export const BlueNavigationStyle = (navigation, withNavigationCloseButton = fals
headerBackTitle: null,
});
export const BlueCreateTxNavigationStyle = (navigation, withAdvancedOptionsMenuButton = false, advancedOptionsMenuButtonAction) => ({
headerStyle: {
backgroundColor: BlueApp.settings.brandingColor,
borderBottomWidth: 0,
elevation: 0,
},
headerTitleStyle: {
fontWeight: '600',
color: BlueApp.settings.foregroundColor,
},
headerTintColor: BlueApp.settings.foregroundColor,
headerLeft: (
<TouchableOpacity
style={{ minWwidth: 40, height: 40, padding: 14 }}
onPress={() => {
Keyboard.dismiss();
navigation.goBack(null);
}}
>
<Image style={{ alignSelf: 'center' }} source={require('./img/close.png')} />
</TouchableOpacity>
),
headerRight: withAdvancedOptionsMenuButton ? (
<TouchableOpacity style={{ minWidth: 40, height: 40, padding: 14 }} onPress={advancedOptionsMenuButtonAction}>
<Icon size={22} name="kebab-horizontal" type="octicon" color={BlueApp.settings.foregroundColor} />
</TouchableOpacity>
) : null,
headerBackTitle: null,
});
export const BluePrivateBalance = () => {
return Platform.select({
ios: (
<View style={{ flexDirection: 'row' }}>
<BlurView style={styles.balanceBlur} blurType="light" blurAmount={25} />
<Icon name="eye-slash" type="font-awesome" color="#FFFFFF" />
</View>
),
android: (
<View style={{ flexDirection: 'row' }}>
<View style={{ backgroundColor: '#FFFFFF', opacity: 0.5, height: 30, width: 100, marginRight: 8 }} />
<Icon name="eye-slash" type="font-awesome" color="#FFFFFF" />
</View>
),
});
};
export const BlueCopyToClipboardButton = ({ stringToCopy }) => {
return (
<TouchableOpacity {...this.props} onPress={() => Clipboard.setString(stringToCopy)}>
@ -366,10 +591,6 @@ export class BlueFormMultiInput extends Component {
};
}
onSelectionChange = ({ nativeEvent: { selection, text } }) => {
this.setState({ selection: { start: selection.end, end: selection.end } });
};
render() {
return (
<TextInput
@ -392,8 +613,6 @@ export class BlueFormMultiInput extends Component {
spellCheck={false}
{...this.props}
selectTextOnFocus={false}
onSelectionChange={this.onSelectionChange}
selection={this.state.selection}
keyboardType={Platform.OS === 'android' ? 'visible-password' : 'default'}
/>
);
@ -571,16 +790,71 @@ export class BlueUseAllFundsButton extends Component {
};
render() {
return (
<InputAccessoryView nativeID={BlueUseAllFundsButton.InputAccessoryViewID}>
<View style={{ flex: 1, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Text style={{ color: BlueApp.settings.alternativeTextColor, fontSize: 16, marginHorizontal: 8 }}>
Total: {this.props.wallet.getBalance()} {BitcoinUnit.BTC}
const inputView = (
<View
style={{
flex: 1,
flexDirection: 'row',
maxHeight: 44,
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: '#eef0f4',
}}
>
<View style={{ flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'flex-start' }}>
<Text
style={{
color: BlueApp.settings.alternativeTextColor,
fontSize: 16,
marginLeft: 8,
marginRight: 0,
paddingRight: 0,
paddingLeft: 0,
paddingTop: 12,
paddingBottom: 12,
}}
>
Total:
</Text>
<BlueButtonLink title="Use All" onPress={this.props.onUseAllPressed} />
{this.props.wallet.allowSendMax() && this.props.wallet.getBalance() > 0 ? (
<BlueButtonLink
onPress={this.props.onUseAllPressed}
style={{ marginLeft: 8, paddingRight: 0, paddingLeft: 0, paddingTop: 12, paddingBottom: 12 }}
title={`${loc.formatBalanceWithoutSuffix(this.props.wallet.getBalance(), BitcoinUnit.BTC, true).toString()} ${
BitcoinUnit.BTC
}`}
/>
) : (
<Text
style={{
color: BlueApp.settings.alternativeTextColor,
fontSize: 16,
marginLeft: 8,
marginRight: 0,
paddingRight: 0,
paddingLeft: 0,
paddingTop: 12,
paddingBottom: 12,
}}
>
{loc.formatBalanceWithoutSuffix(this.props.wallet.getBalance(), BitcoinUnit.BTC, true).toString()} {BitcoinUnit.BTC}
</Text>
)}
</View>
</InputAccessoryView>
<View style={{ flexDirection: 'row', justifyContent: 'flex-end', alignItems: 'flex-end' }}>
<BlueButtonLink
style={{ paddingRight: 8, paddingLeft: 0, paddingTop: 12, paddingBottom: 12 }}
title="Done"
onPress={() => Keyboard.dismiss()}
/>
</View>
</View>
);
if (Platform.OS === 'ios') {
return <InputAccessoryView nativeID={BlueUseAllFundsButton.InputAccessoryViewID}>{inputView}</InputAccessoryView>;
} else {
return <KeyboardAvoidingView style={{ height: 44 }}>{inputView}</KeyboardAvoidingView>;
}
}
}
@ -600,13 +874,47 @@ export class BlueDismissKeyboardInputAccessory extends Component {
alignItems: 'center',
}}
>
<BlueButtonLink title="Done" onPress={Keyboard.dismiss} />
<BlueButtonLink title="Done" onPress={() => Keyboard.dismiss()} />
</View>
</InputAccessoryView>
);
}
}
export class BlueDoneAndDismissKeyboardInputAccessory extends Component {
static InputAccessoryViewID = 'BlueDoneAndDismissKeyboardInputAccessory';
onPasteTapped = async () => {
const clipboard = await Clipboard.getString();
this.props.onPasteTapped(clipboard);
};
render() {
const inputView = (
<View
style={{
backgroundColor: '#eef0f4',
height: 44,
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
}}
>
<BlueButtonLink title="Clear" onPress={this.props.onClearTapped} />
<BlueButtonLink title="Paste" onPress={this.onPasteTapped} />
<BlueButtonLink title="Done" onPress={() => Keyboard.dismiss()} />
</View>
);
if (Platform.OS === 'ios') {
return <InputAccessoryView nativeID={BlueDoneAndDismissKeyboardInputAccessory.InputAccessoryViewID}>{inputView}</InputAccessoryView>;
} else {
return <KeyboardAvoidingView style={{ height: 44 }}>{inputView}</KeyboardAvoidingView>;
}
}
}
export class BlueLoading extends Component {
render() {
return (
@ -740,11 +1048,11 @@ export class BlueTransactionPendingIcon extends Component {
<View style={stylesBlueIcon.ball}>
<Icon
{...this.props}
name="ellipsis-h"
name="kebab-horizontal"
size={16}
type="font-awesome"
type="octicon"
color={BlueApp.settings.foregroundColor}
iconStyle={{ left: 0, top: 6 }}
iconStyle={{ left: 0, top: 7 }}
/>
</View>
</View>
@ -1084,6 +1392,8 @@ export class BlueTransactionListItem extends Component {
itemPriceUnit: BitcoinUnit.BTC,
};
state = { transactionTimeToReadable: '...', subtitleNumberOfLines: 1 };
txMemo = () => {
if (BlueApp.tx_metadata[this.props.item.hash] && BlueApp.tx_metadata[this.props.item.hash]['memo']) {
return BlueApp.tx_metadata[this.props.item.hash]['memo'];
@ -1222,7 +1532,7 @@ export class BlueTransactionListItem extends Component {
onPress = () => {
if (this.props.item.hash) {
NavigationService.navigate('TransactionDetails', { hash: this.props.item.hash });
NavigationService.navigate('TransactionStatus', { hash: this.props.item.hash });
} else if (
this.props.item.type === 'user_invoice' ||
this.props.item.type === 'payment_request' ||
@ -1245,13 +1555,28 @@ export class BlueTransactionListItem extends Component {
}
};
componentDidMount() {
InteractionManager.runAfterInteractions(() => {
const transactionTimeToReadable = loc.transactionTimeToReadable(this.props.item.received);
this.setState({ transactionTimeToReadable });
});
}
onLongPress = () => {
if (this.state.subtitleNumberOfLines === 1) {
this.setState({ subtitleNumberOfLines: 0 });
}
};
render() {
return (
<BlueListItem
avatar={this.avatar()}
title={loc.transactionTimeToReadable(this.props.item.received)}
title={this.state.transactionTimeToReadable}
subtitle={this.subtitle()}
subtitleNumberOfLines={this.state.subtitleNumberOfLines}
onPress={this.onPress}
onLongPress={this.onLongPress}
badge={{
value: 3,
textStyle: { color: 'orange' },
@ -1413,7 +1738,7 @@ export class BlueListTransactionItem extends Component {
onPress = () => {
if (this.props.item.hash) {
NavigationService.navigate('TransactionDetails', { hash: this.props.item.hash });
NavigationService.navigate('TransactionStatus', { hash: this.props.item.hash });
} else if (
this.props.item.type === 'user_invoice' ||
this.props.item.type === 'payment_request' ||
@ -1544,18 +1869,22 @@ export class WalletsCarousel extends Component {
>
{item.getLabel()}
</Text>
<Text
numberOfLines={1}
adjustsFontSizeToFit
style={{
backgroundColor: 'transparent',
fontWeight: 'bold',
fontSize: 36,
color: BlueApp.settings.inverseForegroundColor,
}}
>
{loc.formatBalance(Number(item.getBalance()), item.getPreferredBalanceUnit(), true)}
</Text>
{item.hideBalance ? (
<BluePrivateBalance />
) : (
<Text
numberOfLines={1}
adjustsFontSizeToFit
style={{
backgroundColor: 'transparent',
fontWeight: 'bold',
fontSize: 36,
color: BlueApp.settings.inverseForegroundColor,
}}
>
{loc.formatBalance(Number(item.getBalance()), item.getPreferredBalanceUnit(), true)}
</Text>
)}
<Text style={{ backgroundColor: 'transparent' }} />
<Text
numberOfLines={1}
@ -1651,7 +1980,7 @@ export class BlueAddressInput extends Component {
value={this.props.address}
style={{ flex: 1, marginHorizontal: 8, minHeight: 33 }}
editable={!this.props.isLoading}
onSubmitEditing={Keyboard.dismiss}
onSubmitEditing={() => Keyboard.dismiss()}
{...this.props}
/>
<TouchableOpacity
@ -1682,12 +2011,11 @@ export class BlueAddressInput extends Component {
);
}}
style={{
width: 75,
height: 36,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: '#bebebe',
backgroundColor: '#9AA0AA',
borderRadius: 4,
paddingVertical: 4,
paddingHorizontal: 8,
@ -1695,8 +2023,134 @@ export class BlueAddressInput extends Component {
}}
>
<Icon name="qrcode" size={22} type="font-awesome" color={BlueApp.settings.inverseForegroundColor} />
<Text style={{ color: BlueApp.settings.inverseForegroundColor }}>{loc.send.details.scan}</Text>
<Text style={{ marginLeft: 4, color: BlueApp.settings.inverseForegroundColor }}>{loc.send.details.scan}</Text>
</TouchableOpacity>
</View>
);
}
}
export class BlueReplaceFeeSuggestions extends Component {
static propTypes = {
onFeeSelected: PropTypes.func.isRequired,
transactionMinimum: PropTypes.number.isRequired,
};
static defaultProps = {
onFeeSelected: undefined,
transactionMinimum: 1,
};
state = { networkFees: undefined, selectedFeeType: NetworkTransactionFeeType.FAST, customFeeValue: 0 };
async componentDidMount() {
const networkFees = await NetworkTransactionFees.recommendedFees();
this.setState({ networkFees }, () => this.onFeeSelected(NetworkTransactionFeeType.FAST));
}
onFeeSelected = selectedFeeType => {
if (selectedFeeType !== NetworkTransactionFeeType.CUSTOM) {
Keyboard.dismiss();
}
if (selectedFeeType === NetworkTransactionFeeType.FAST) {
this.props.onFeeSelected(this.state.networkFees.fastestFee);
this.setState({ selectedFeeType }, () => this.props.onFeeSelected(this.state.networkFees.fastestFee));
} else if (selectedFeeType === NetworkTransactionFeeType.MEDIUM) {
this.setState({ selectedFeeType }, () => this.props.onFeeSelected(this.state.networkFees.halfHourFee));
} else if (selectedFeeType === NetworkTransactionFeeType.SLOW) {
this.setState({ selectedFeeType }, () => this.props.onFeeSelected(this.state.networkFees.hourFee));
} else if (selectedFeeType === NetworkTransactionFeeType.CUSTOM) {
this.props.onFeeSelected(this.state.customFeeValue);
}
};
onCustomFeeTextChange = customFee => {
this.setState({ customFeeValue: Number(customFee), selectedFeeType: NetworkTransactionFeeType.CUSTOM }, () => {
this.onFeeSelected(NetworkTransactionFeeType.CUSTOM);
});
};
render() {
return (
<View>
{this.state.networkFees && (
<>
<BlueText>Suggestions</BlueText>
<TouchableOpacity onPress={() => this.onFeeSelected(NetworkTransactionFeeType.FAST)}>
<BlueListItem
title={'Fast'}
rightTitle={`${this.state.networkFees.fastestFee} sat/b`}
{...(this.state.selectedFeeType === NetworkTransactionFeeType.FAST
? { rightIcon: <Icon name="check" type="font-awesome" color="#0c2550" /> }
: { hideChevron: true })}
/>
</TouchableOpacity>
<TouchableOpacity onPress={() => this.onFeeSelected(NetworkTransactionFeeType.MEDIUM)}>
<BlueListItem
title={'Medium'}
rightTitle={`${this.state.networkFees.halfHourFee} sat/b`}
{...(this.state.selectedFeeType === NetworkTransactionFeeType.MEDIUM
? { rightIcon: <Icon name="check" type="font-awesome" color="#0c2550" /> }
: { hideChevron: true })}
/>
</TouchableOpacity>
<TouchableOpacity onPress={() => this.onFeeSelected(NetworkTransactionFeeType.SLOW)}>
<BlueListItem
title={'Slow'}
rightTitle={`${this.state.networkFees.hourFee} sat/b`}
{...(this.state.selectedFeeType === NetworkTransactionFeeType.SLOW
? { rightIcon: <Icon name="check" type="font-awesome" color="#0c2550" /> }
: { hideChevron: true })}
/>
</TouchableOpacity>
</>
)}
<TouchableOpacity onPress={() => this.customTextInput.focus()}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginLeft: 18, marginRight: 18, alignItems: 'center' }}>
<Text style={{ color: BlueApp.settings.foregroundColor, fontSize: 16, fontWeight: '500' }}>Custom</Text>
<View
style={{
flexDirection: 'row',
minHeight: 44,
height: 44,
minWidth: 48,
alignItems: 'center',
justifyContent: 'flex-end',
marginVertical: 8,
}}
>
<TextInput
onChangeText={this.onCustomFeeTextChange}
keyboardType={'numeric'}
value={this.state.customFeeValue}
ref={ref => (this.customTextInput = ref)}
maxLength={9}
style={{
borderColor: '#d2d2d2',
borderBottomColor: '#d2d2d2',
borderWidth: 1.0,
borderBottomWidth: 0.5,
borderRadius: 4,
minHeight: 33,
maxWidth: 100,
minWidth: 44,
backgroundColor: '#f5f5f5',
textAlign: 'right',
}}
onFocus={() => this.onCustomFeeTextChange(this.state.customFeeValue)}
defaultValue={`${this.props.transactionMinimum}`}
placeholder="Custom sat/b"
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
/>
<Text style={{ color: BlueApp.settings.alternativeTextColor, marginHorizontal: 8 }}>sat/b</Text>
{this.state.selectedFeeType === NetworkTransactionFeeType.CUSTOM && <Icon name="check" type="font-awesome" color="#0c2550" />}
</View>
<BlueDismissKeyboardInputAccessory />
</View>
</TouchableOpacity>
<BlueText>
The total fee rate (satoshi per byte) you want to pay should be higher than {this.props.transactionMinimum} sat/byte
</BlueText>
</View>
);
}
@ -1725,22 +2179,41 @@ export class BlueBitcoinAmount extends Component {
} else {
localCurrency = loc.formatBalanceWithoutSuffix(amount.toString(), BitcoinUnit.LOCAL_CURRENCY, false);
}
if (amount === BitcoinUnit.MAX) localCurrency = ''; // we dont want to display NaN
return (
<TouchableWithoutFeedback disabled={this.props.pointerEvents === 'none'} onPress={() => this.textInput.focus()}>
<View>
<View style={{ flexDirection: 'row', justifyContent: 'center', paddingTop: 16, paddingBottom: 16 }}>
<View style={{ flexDirection: 'row', justifyContent: 'center', paddingTop: 16, paddingBottom: 2 }}>
<TextInput
{...this.props}
keyboardType="numeric"
onChangeText={text => {
text = text.trim();
text = text.replace(',', '.');
const split = text.split('.');
if (split.length >= 2) {
text = `${parseInt(split[0], 10)}.${split[1]}`;
} else {
text = `${parseInt(split[0], 10)}`;
}
text = this.props.unit === BitcoinUnit.BTC ? text.replace(/[^0-9.]/g, '') : text.replace(/[^0-9]/g, '');
text = text.replace(/(\..*)\./g, '$1');
if (text.startsWith('.')) {
text = '0.';
}
text = text.replace(/(0{1,}.)\./g, '$1');
if (this.props.unit !== BitcoinUnit.BTC) {
text = text.replace(/[^0-9.]/g, '');
}
this.props.onChangeText(text);
}}
onBlur={() => {
if (this.props.onBlur) this.props.onBlur();
}}
onFocus={() => {
if (this.props.onFocus) this.props.onFocus();
}}
placeholder="0"
maxLength={10}
ref={textInput => (this.textInput = textInput)}
@ -1774,3 +2247,11 @@ export class BlueBitcoinAmount extends Component {
);
}
}
const styles = StyleSheet.create({
balanceBlur: {
height: 30,
width: 100,
marginRight: 16,
},
});

211
BlueElectrum.js

@ -1,8 +1,9 @@
import AsyncStorage from '@react-native-community/async-storage';
import { SegwitBech32Wallet } from './class';
import { AppStorage } from './class';
const ElectrumClient = require('electrum-client');
let bitcoin = require('bitcoinjs-lib');
let reverse = require('buffer-reverse');
let BigNumber = require('bignumber.js');
const storageKey = 'ELECTRUM_PEERS';
const defaultPeer = { host: 'electrum1.bluewallet.io', tcp: '50001' };
@ -24,18 +25,29 @@ const hardcodedPeers = [
let mainClient = false;
let mainConnected = false;
let wasConnectedAtLeastOnce = false;
async function connectMain() {
let usingPeer = await getRandomHardcodedPeer();
let savedPeer = await getSavedPeer();
if (savedPeer && savedPeer.host && savedPeer.tcp) {
usingPeer = savedPeer;
}
try {
console.log('begin connection:', JSON.stringify(usingPeer));
mainClient = new ElectrumClient(usingPeer.tcp, usingPeer.host, 'tcp');
mainClient.onError = function(e) {
console.log('ElectrumClient error: ' + e);
mainConnected = false;
};
await mainClient.connect();
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);
mainConnected = true;
wasConnectedAtLeastOnce = true;
AsyncStorage.setItem(storageKey, JSON.stringify(peers));
}
} catch (e) {
@ -64,6 +76,12 @@ async function getRandomHardcodedPeer() {
return hardcodedPeers[(hardcodedPeers.length * Math.random()) | 0];
}
async function getSavedPeer() {
let host = await AsyncStorage.getItem(AppStorage.ELECTRUM_HOST);
let port = await AsyncStorage.getItem(AppStorage.ELECTRUM_TCP_PORT);
return { host, tcp: port };
}
/**
* Returns random electrum server out of list of servers
* previous electrum server told us. Nearly half of them is
@ -99,7 +117,7 @@ async function getRandomDynamicPeer() {
* @param address {String}
* @returns {Promise<Object>}
*/
async function getBalanceByAddress(address) {
module.exports.getBalanceByAddress = async function(address) {
if (!mainClient) throw new Error('Electrum client is not connected');
let script = bitcoin.address.toOutputScript(address);
let hash = bitcoin.crypto.sha256(script);
@ -107,35 +125,56 @@ async function getBalanceByAddress(address) {
let balance = await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex'));
balance.addr = address;
return balance;
}
};
module.exports.getConfig = async function() {
if (!mainClient) throw new Error('Electrum client is not connected');
return {
host: mainClient.host,
port: mainClient.port,
status: mainClient.status,
};
};
/**
*
* @param address {String}
* @returns {Promise<Array>}
*/
async function getTransactionsByAddress(address) {
module.exports.getTransactionsByAddress = async function(address) {
if (!mainClient) throw new Error('Electrum client is not connected');
let script = bitcoin.address.toOutputScript(address);
let hash = bitcoin.crypto.sha256(script);
let reversedHash = Buffer.from(reverse(hash));
let history = await mainClient.blockchainScripthash_getHistory(reversedHash.toString('hex'));
return history;
}
};
module.exports.ping = async function() {
try {
await mainClient.server_ping();
} catch (_) {
mainConnected = false;
return false;
}
return true;
};
async function getTransactionsFullByAddress(address) {
module.exports.getTransactionsFullByAddress = async function(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;
// also, we extract destination address from prev output:
if (prevTxForVin.vout[input.vout].scriptPubKey && prevTxForVin.vout[input.vout].scriptPubKey.addresses) {
input.addresses = prevTxForVin.vout[input.vout].scriptPubKey.addresses;
}
}
}
@ -152,7 +191,7 @@ async function getTransactionsFullByAddress(address) {
}
return ret;
}
};
/**
*
@ -160,7 +199,7 @@ async function getTransactionsFullByAddress(address) {
* @param batchsize {Number}
* @returns {Promise<{balance: number, unconfirmed_balance: number, addresses: object}>}
*/
async function multiGetBalanceByAddress(addresses, batchsize) {
module.exports.multiGetBalanceByAddress = async function(addresses, batchsize) {
batchsize = batchsize || 100;
if (!mainClient) throw new Error('Electrum client is not connected');
let ret = { balance: 0, unconfirmed_balance: 0, addresses: {} };
@ -188,9 +227,9 @@ async function multiGetBalanceByAddress(addresses, batchsize) {
}
return ret;
}
};
async function multiGetUtxoByAddress(addresses, batchsize) {
module.exports.multiGetUtxoByAddress = async function(addresses, batchsize) {
batchsize = batchsize || 100;
if (!mainClient) throw new Error('Electrum client is not connected');
let ret = {};
@ -223,7 +262,56 @@ async function multiGetUtxoByAddress(addresses, batchsize) {
}
return ret;
}
};
module.exports.multiGetHistoryByAddress = async function(addresses, batchsize) {
batchsize = batchsize || 100;
if (!mainClient) throw new Error('Electrum client is not connected');
let ret = {};
let chunks = splitIntoChunks(addresses, batchsize);
for (let chunk of chunks) {
let scripthashes = [];
let scripthash2addr = {};
for (let addr of chunk) {
let script = bitcoin.address.toOutputScript(addr);
let hash = bitcoin.crypto.sha256(script);
let reversedHash = Buffer.from(reverse(hash));
reversedHash = reversedHash.toString('hex');
scripthashes.push(reversedHash);
scripthash2addr[reversedHash] = addr;
}
let results = await mainClient.blockchainScripthash_getHistoryBatch(scripthashes);
for (let history of results) {
ret[scripthash2addr[history.param]] = history.result;
for (let hist of ret[scripthash2addr[history.param]]) {
hist.address = scripthash2addr[history.param];
}
}
}
return ret;
};
module.exports.multiGetTransactionByTxid = async function(txids, batchsize, verbose) {
batchsize = batchsize || 100;
verbose = verbose !== false;
if (!mainClient) throw new Error('Electrum client is not connected');
let ret = {};
let chunks = splitIntoChunks(txids, batchsize);
for (let chunk of chunks) {
let results = await mainClient.blockchainTransaction_getBatch(chunk, verbose);
for (let txdata of results) {
ret[txdata.param] = txdata.result;
}
}
return ret;
};
/**
* Simple waiter till `mainConnected` becomes true (which means
@ -232,7 +320,7 @@ async function multiGetUtxoByAddress(addresses, batchsize) {
*
* @returns {Promise<Promise<*> | Promise<*>>}
*/
async function waitTillConnected() {
module.exports.waitTillConnected = async function() {
let waitTillConnectedInterval = false;
let retriesCounter = 0;
return new Promise(function(resolve, reject) {
@ -241,23 +329,49 @@ async function waitTillConnected() {
clearInterval(waitTillConnectedInterval);
resolve(true);
}
if (wasConnectedAtLeastOnce && mainClient.status === 1) {
clearInterval(waitTillConnectedInterval);
mainConnected = true;
resolve(true);
}
if (retriesCounter++ >= 30) {
clearInterval(waitTillConnectedInterval);
reject(new Error('Waiting for Electrum connection timeout'));
}
}, 1000);
});
}
};
async function estimateFees() {
module.exports.estimateFees = async function() {
if (!mainClient) throw new Error('Electrum client is not connected');
const fast = await mainClient.blockchainEstimatefee(1);
const medium = await mainClient.blockchainEstimatefee(5);
const slow = await mainClient.blockchainEstimatefee(10);
return { fast, medium, slow };
}
};
async function broadcast(hex) {
/**
* Returns the estimated transaction fee to be confirmed within a certain number of blocks
*
* @param numberOfBlocks {number} The number of blocks to target for confirmation
* @returns {Promise<number>} Satoshis per byte
*/
module.exports.estimateFee = async function(numberOfBlocks) {
if (!mainClient) throw new Error('Electrum client is not connected');
numberOfBlocks = numberOfBlocks || 1;
let coinUnitsPerKilobyte = await mainClient.blockchainEstimatefee(numberOfBlocks);
if (coinUnitsPerKilobyte === -1) return 1;
return Math.round(
new BigNumber(coinUnitsPerKilobyte)
.dividedBy(1024)
.multipliedBy(100000000)
.toNumber(),
);
};
module.exports.broadcast = async function(hex) {
if (!mainClient) throw new Error('Electrum client is not connected');
try {
const broadcast = await mainClient.blockchainTransaction_broadcast(hex);
@ -265,16 +379,34 @@ async function broadcast(hex) {
} catch (error) {
return error;
}
}
};
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.broadcastV2 = async function(hex) {
if (!mainClient) throw new Error('Electrum client is not connected');
return mainClient.blockchainTransaction_broadcast(hex);
};
/**
*
* @param host
* @param tcpPort
* @returns {Promise<boolean>} Whether provided host:port is a valid electrum server
*/
module.exports.testConnection = async function(host, tcpPort) {
let client = new ElectrumClient(tcpPort, host, 'tcp');
try {
await client.connect();
await client.server_version('2.7.11', '1.4');
await client.server_ping();
client.keepAlive = () => {}; // dirty hack to make it stop reconnecting
client.reconnect = () => {}; // dirty hack to make it stop reconnecting
client.close();
return true;
} catch (_) {
return false;
}
};
module.exports.forceDisconnect = () => {
mainClient.keepAlive = () => {}; // dirty hack to make it stop reconnecting
@ -292,28 +424,3 @@ let splitIntoChunks = function(arr, chunkSize) {
}
return groups;
};
/*
let addr4elect = 'bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej';
let script = bitcoin.address.toOutputScript(addr4elect);
let hash = bitcoin.crypto.sha256(script);
let reversedHash = Buffer.from(hash.reverse());
console.log(addr4elect, ' maps to ', reversedHash.toString('hex'));
console.log(await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex')));
addr4elect = '1BWwXJH3q6PRsizBkSGm2Uw4Sz1urZ5sCj';
script = bitcoin.address.toOutputScript(addr4elect);
hash = bitcoin.crypto.sha256(script);
reversedHash = Buffer.from(hash.reverse());
console.log(addr4elect, ' maps to ', reversedHash.toString('hex'));
console.log(await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex')));
// let peers = await mainClient.serverPeers_subscribe();
// console.log(peers);
mainClient.keepAlive = () => {}; // dirty hack to make it stop reconnecting
mainClient.reconnect = () => {}; // dirty hack to make it stop reconnecting
mainClient.close();
// setTimeout(()=>process.exit(), 3000); */

25
MainBottomTabs.js

@ -9,6 +9,7 @@ import Currency from './screen/settings/currency';
import EncryptStorage from './screen/settings/encryptStorage';
import PlausibleDeniability from './screen/plausibledeniability';
import LightningSettings from './screen/settings/lightningSettings';
import ElectrumSettings from './screen/settings/electrumSettings';
import WalletsList from './screen/wallets/list';
import WalletTransactions from './screen/wallets/transactions';
import AddWallet from './screen/wallets/add';
@ -18,13 +19,18 @@ import WalletDetails from './screen/wallets/details';
import WalletExport from './screen/wallets/export';
import WalletXpub from './screen/wallets/xpub';
import BuyBitcoin from './screen/wallets/buyBitcoin';
import Marketplace from './screen/wallets/marketplace';
import scanQrWif from './screen/wallets/scanQrWif';
import ReorderWallets from './screen/wallets/reorderWallets';
import SelectWallet from './screen/wallets/selectWallet';
import details from './screen/transactions/details';
import TransactionStatus from './screen/transactions/transactionStatus';
import rbf from './screen/transactions/RBF';
import createrbf from './screen/transactions/RBF-create';
import cpfp from './screen/transactions/CPFP';
import rbfBumpFee from './screen/transactions/RBFBumpFee';
import rbfCancel from './screen/transactions/RBFCancel';
import receiveDetails from './screen/receive/details';
import setReceiveAmount from './screen/receive/receiveAmount';
@ -57,6 +63,9 @@ const WalletsStackNavigator = createStackNavigator(
WalletTransactions: {
screen: WalletTransactions,
},
TransactionStatus: {
screen: TransactionStatus,
},
TransactionDetails: {
screen: details,
},
@ -69,6 +78,15 @@ const WalletsStackNavigator = createStackNavigator(
CreateRBF: {
screen: createrbf,
},
CPFP: {
screen: cpfp,
},
RBFBumpFee: {
screen: rbfBumpFee,
},
RBFCancel: {
screen: rbfCancel,
},
Settings: {
screen: Settings,
path: 'Settings',
@ -111,6 +129,10 @@ const WalletsStackNavigator = createStackNavigator(
screen: LightningSettings,
path: 'LightningSettings',
},
ElectrumSettings: {
screen: ElectrumSettings,
path: 'ElectrumSettings',
},
LNDViewInvoice: {
screen: LNDViewInvoice,
swipeEnabled: false,
@ -228,6 +250,9 @@ const MainBottomTabs = createStackNavigator(
BuyBitcoin: {
screen: BuyBitcoin,
},
Marketplace: {
screen: Marketplace,
},
//
SendDetails: {
screen: CreateTransactionStackNavigator,

5
SECURITY.md

@ -0,0 +1,5 @@
# Security Policy
## Reporting a Vulnerability
bluewallet at bluewallet dot io

10
WatchConnectivity.js

@ -1,15 +1,18 @@
import * as watch from 'react-native-watch-connectivity';
import { InteractionManager } from 'react-native';
import { InteractionManager, Platform } from 'react-native';
const loc = require('./loc');
export default class WatchConnectivity {
isAppInstalled = false;
BlueApp = require('./BlueApp');
constructor() {
this.getIsWatchAppInstalled();
if (Platform.OS === 'ios') {
this.getIsWatchAppInstalled();
}
}
getIsWatchAppInstalled() {
if (Platform.OS !== 'ios') return;
watch.getIsWatchAppInstalled((err, isAppInstalled) => {
if (!err) {
this.isAppInstalled = isAppInstalled;
@ -45,6 +48,7 @@ export default class WatchConnectivity {
}
async sendWalletsToWatch() {
if (Platform.OS !== 'ios') return;
InteractionManager.runAfterInteractions(async () => {
if (this.isAppInstalled) {
const allWallets = this.BlueApp.getWallets();
@ -133,6 +137,6 @@ export default class WatchConnectivity {
}
WatchConnectivity.init = function() {
if (WatchConnectivity.shared) return;
if (WatchConnectivity.shared || Platform.OS !== 'ios') return;
WatchConnectivity.shared = new WatchConnectivity();
};

19
analytics.js

@ -1,12 +1,17 @@
// import Amplitude from 'react-native-amplitude-analytics';
import { GoogleAnalyticsTracker } from 'react-native-google-analytics-bridge';
import amplitude from 'amplitude-js';
import Analytics from 'appcenter-analytics';
// Amplitude.initialize('8b7cf19e8eea3cdcf16340f5fbf16330');
const analytics = new GoogleAnalyticsTracker('UA-121673546-1');
amplitude.getInstance().init('8b7cf19e8eea3cdcf16340f5fbf16330', null, {
useNativeDeviceInfo: true,
});
let A = function(event) {
// Amplitude.logEvent(event);
analytics.trackEvent(event, event);
let A = async event => {
amplitude.getInstance().logEvent(event, {});
try {
Analytics.trackEvent(event);
} catch (err) {
console.log(err);
}
};
A.ENUM = {

19
android/BlueWallet.iml

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id="BlueWallet" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="java-gradle" name="Java-Gradle">
<configuration>
<option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" />
<option name="BUILDABLE" value="false" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.gradle" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

16
android/android.iml

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.system.id="GRADLE" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android-gradle" name="Android-Gradle">
<configuration>
<option name="GRADLE_PROJECT_PATH" value=":" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="1.8" jdkType="JavaSDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

192
android/app/app.iml

@ -1,192 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id=":app" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android-gradle" name="Android-Gradle">
<configuration>
<option name="GRADLE_PROJECT_PATH" value=":app" />
</configuration>
</facet>
<facet type="android" name="Android">
<configuration>
<option name="SELECTED_BUILD_VARIANT" value="debug" />
<option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" />
<afterSyncTasks>
<task>generateDebugSources</task>
</afterSyncTasks>
<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;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>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7">
<output url="file://$MODULE_DIR$/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes" />
<output-test url="file://$MODULE_DIR$/build/intermediates/javac/debugUnitTest/compileDebugUnitTestJavaWithJavac/classes" />
<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/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/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/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/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" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/shaders" isTestSource="false" />
<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" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/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" />
<sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/shaders" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/shaders" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/shaders" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build/generated/not_namespaced_r_class_sources" />
<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/blame" />
<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/instant_app_manifest" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/instant_run_merged_manifests" />
<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/lint_jar" />
<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/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/res" />
<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/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: 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.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: 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: 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: 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.facebook.soloader:soloader:0.6.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.google.code.findbugs:jsr305:3.0.2@jar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-vector-drawable:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-core-utils:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-annotations:28.0.0@jar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:interpolator:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: android.arch.lifecycle:livedata:1.1.1@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:drawerlayout:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:documentfile:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:slidingpanelayout:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.parse.bolts:bolts-tasks:1.4.0@jar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:appcompat-v7:28.0.0@aar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: com.android.support:multidex-instrumentation:1.0.2@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:collections:28.0.0@jar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-core-ui:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.fresco:imagepipeline-okhttp3:1.10.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:asynclayoutinflater:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:print:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: android.arch.core:common:1.1.1@jar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:versionedparcelable:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.infer.annotation:infer-annotation:0.11.2@jar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.fresco:imagepipeline:1.10.0@aar" level="project" />
<orderEntry type="library" name="Gradle: 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-community_slider" />
<orderEntry type="module" module-name="react-native-sentry" />
<orderEntry type="module" module-name="react-native-linear-gradient" />
<orderEntry type="module" module-name="react-native-image-picker" />
<orderEntry type="module" module-name="react-native-vector-icons" />
<orderEntry type="module" module-name="react-native-haptic-feedback" />
<orderEntry type="module" module-name="@react-native-community_async-storage" />
<orderEntry type="module" module-name="react-native-prompt-android" />
<orderEntry type="module" module-name="react-native-gesture-handler" />
<orderEntry type="module" module-name="react-native-randombytes" />
<orderEntry type="module" module-name="react-native-svg" />
<orderEntry type="module" module-name="@remobile_react-native-qrcode-local-image" />
<orderEntry type="module" module-name="react-native-fs" />
<orderEntry type="module" module-name="react-native-tcp" />
<orderEntry type="library" name="Gradle: android-android-27" level="project" />
</component>
</module>

61
android/app/build.gradle

@ -18,6 +18,9 @@ import com.android.build.OutputFile
* // the entry file for bundle generation
* entryFile: "index.android.js",
*
* // https://facebook.github.io/react-native/docs/performance#enable-the-ram-format
* bundleCommand: "ram-bundle",
*
* // whether to bundle JS and assets in debug mode
* bundleInDebug: false,
*
@ -93,19 +96,29 @@ def enableSeparateBuildPerCPUArchitecture = false
*/
def enableProguardInReleaseBuilds = false
/**
* Use international variant JavaScriptCore
* International variant includes ICU i18n library and necessary data allowing to use
* e.g. Date.toLocaleString and String.localeCompare that give correct results
* when using with locales other than en-US.
* Note that this variant is about 6MiB larger per architecture than default.
*/
def useIntlJsc = false
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
applicationId "io.bluewallet.bluewallet"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "4.0.3"
ndk {
abiFilters "armeabi-v7a", "x86"
}
versionName "4.5.0"
multiDexEnabled true
missingDimensionStrategy 'react-native-camera', 'general'
}
@ -114,7 +127,7 @@ android {
reset()
enable enableSeparateBuildPerCPUArchitecture
universalApk false // If true, also generate a universal APK
include "armeabi-v7a", "x86"
include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}
}
buildTypes {
@ -127,8 +140,8 @@ android {
applicationVariants.all { variant ->
variant.outputs.each { output ->
// For each separate APK per architecture, set a unique version code as described here:
// http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits
def versionCodes = ["armeabi-v7a":1, "x86":2, "arm64-v8a": 3]
// https://developer.android.com/studio/build/configure-apk-splits.html
def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
def abi = output.getFilter(OutputFile.ABI)
if (abi != null) { // null for the universal-debug, universal-release variants
output.versionCodeOverride =
@ -139,29 +152,17 @@ 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')
implementation project(':@remobile_react-native-qrcode-local-image')
implementation project(':react-native-image-picker')
implementation project(':react-native-webview')
implementation project(':react-native-svg')
implementation project(':react-native-vector-icons')
implementation project(':react-native-sentry')
implementation project(':react-native-randombytes')
implementation project(':react-native-prompt-android')
implementation project(':react-native-linear-gradient')
implementation project(':react-native-haptic-feedback')
implementation project(':react-native-google-analytics-bridge')
implementation project(':react-native-gesture-handler')
implementation project(':react-native-fs')
implementation project(':react-native-device-info')
implementation project(':react-native-camera')
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
implementation "com.facebook.react:react-native:+" // From node_modules
implementation 'com.android.support:multidex:1.0.3'
def appCenterSdkVersion = '2.1.0'
implementation "com.microsoft.appcenter:appcenter-analytics:${appCenterSdkVersion}"
implementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
// JSC from node_modules
if (useIntlJsc) {
implementation 'org.webkit:android-jsc-intl:+'
} else {
implementation 'org.webkit:android-jsc:+'
}
}
// Run this once to be able to run the application with BUCK
@ -170,3 +171,5 @@ task copyDownloadableDepsToLibs(type: Copy) {
from configurations.compile
into 'libs'
}
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)

7
android/app/proguard-rules.pro

@ -8,10 +8,3 @@
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

8
android/app/src/debug/AndroidManifest.xml

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" />
</manifest>

4
android/app/src/main/AndroidManifest.xml

@ -1,15 +1,17 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.bluewallet.bluewallet">
package="io.bluewallet.bluewallet">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.CAMERA"/>
<application
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:usesCleartextTraffic="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"

3
android/app/src/main/assets/appcenter-config.json

@ -0,0 +1,3 @@
{
"app_secret": "7a010505-cccc-4e40-aa6b-fbbe0624c8d9"
}

17
android/app/src/main/java/io/bluewallet/bluewallet/MainActivity.java

@ -1,28 +1,15 @@
package io.bluewallet.bluewallet;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactRootView;
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
public class MainActivity extends ReactActivity {
/**
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "BlueWallet"; // this one too
return "BlueWallet";
}
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
return new ReactActivityDelegate(this, getMainComponentName()) {
@Override
protected ReactRootView createRootView() {
return new RNGestureHandlerEnabledRootView(MainActivity.this);
}
};
}
}

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

@ -2,49 +2,12 @@ package io.bluewallet.bluewallet;
import android.app.Application;
import com.facebook.react.PackageList;
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;
import com.remobile.qrcodeLocalImage.RCTQRCodeLocalImagePackage;
import com.imagepicker.ImagePickerPackage;
import com.reactnativecommunity.webview.RNCWebViewPackage;
import io.sentry.RNSentryPackage;
import com.bitgo.randombytes.RandomBytesPackage;
import im.shimo.react.prompt.RNPromptPackage;
import com.BV.LinearGradient.LinearGradientPackage;
import com.mkuczera.RNReactNativeHapticFeedbackPackage;
import com.idehub.GoogleAnalyticsBridge.GoogleAnalyticsBridgePackage;
import com.swmansion.gesturehandler.react.RNGestureHandlerPackage;
import com.rnfs.RNFSPackage;
import com.learnium.RNDeviceInfo.RNDeviceInfo;
import org.reactnative.camera.RNCameraPackage;
import io.sentry.RNSentryPackage;
import com.bitgo.randombytes.RandomBytesPackage;
import im.shimo.react.prompt.RNPromptPackage;
import com.BV.LinearGradient.LinearGradientPackage;
import com.mkuczera.RNReactNativeHapticFeedbackPackage;
import com.idehub.GoogleAnalyticsBridge.GoogleAnalyticsBridgePackage;
import com.reactnativecommunity.webview.RNCWebViewPackage;
import com.oblador.vectoricons.VectorIconsPackage;
import com.horcrux.svg.SvgPackage;
import io.sentry.RNSentryPackage;
import com.bitgo.randombytes.RandomBytesPackage;
import im.shimo.react.prompt.RNPromptPackage;
import com.BV.LinearGradient.LinearGradientPackage;
import com.mkuczera.RNReactNativeHapticFeedbackPackage;
import com.idehub.GoogleAnalyticsBridge.GoogleAnalyticsBridgePackage;
import com.learnium.RNDeviceInfo.RNDeviceInfo;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;
import org.reactnative.camera.RNCameraPackage;
import com.swmansion.gesturehandler.react.RNGestureHandlerPackage;
import com.rnfs.RNFSPackage;
import java.util.Arrays;
import java.util.List;
public class MainApplication extends Application implements ReactApplication {
@ -57,28 +20,11 @@ public class MainApplication extends Application implements ReactApplication {
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new AsyncStoragePackage(),
new ReactSliderPackage(),
new ObscurePackage(),
new TcpSocketsModule(),
new RCTQRCodeLocalImagePackage(),
new ImagePickerPackage(),
new RNCWebViewPackage(),
new RNSentryPackage(),
new RandomBytesPackage(),
new RNPromptPackage(),
new RNReactNativeHapticFeedbackPackage(),
new GoogleAnalyticsBridgePackage(),
new RNDeviceInfo(),
new LinearGradientPackage(),
new RNFSPackage() ,
new VectorIconsPackage(),
new SvgPackage(),
new RNCameraPackage(),
new RNGestureHandlerPackage()
);
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
return packages;
}
@Override

2
android/app/src/main/res/values/strings.xml

@ -1,3 +1,5 @@
<resources>
<string name="app_name">BlueWallet</string>
<string name="appCenterCrashes_whenToSendCrashes" moduleConfig="true" translatable="false">ASK_JAVASCRIPT</string>
<string name="appCenterAnalytics_whenToEnableAnalytics" moduleConfig="true" translatable="false">ALWAYS_SEND</string>
</resources>

1
android/app/src/main/res/values/styles.xml

@ -3,6 +3,7 @@
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:textColor">#000000</item>
</style>
</resources>

23
android/build.gradle

@ -5,15 +5,17 @@ buildscript {
buildToolsVersion = "28.0.3"
minSdkVersion = 16
compileSdkVersion = 28
targetSdkVersion = 27
targetSdkVersion = 28
supportLibVersion = "28.0.0"
googlePlayServicesVersion = "16.+"
firebaseVersion = "17.3.4"
}
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.2'
classpath("com.android.tools.build:gradle:3.4.1")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@ -23,17 +25,16 @@ buildscript {
allprojects {
repositories {
mavenLocal()
google()
jcenter()
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url "$rootDir/../node_modules/react-native/android"
url("$rootDir/../node_modules/react-native/android")
}
maven {
// Android JSC is installed from npm
url("$rootDir/../node_modules/jsc-android/dist")
}
}
}
task wrapper(type: Wrapper) {
gradleVersion = '4.7'
distributionUrl = distributionUrl.replace("bin", "all")
google()
jcenter()
}
}

1
android/build.sh

@ -1 +0,0 @@
./gradlew assembleRelease

3
android/gradle.properties

@ -16,3 +16,6 @@
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
android.useAndroidX=true
android.enableJetifier=true

BIN
android/gradle/wrapper/gradle-wrapper.jar

Binary file not shown.

2
android/gradle/wrapper/gradle-wrapper.properties

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip

18
android/gradlew

@ -1,5 +1,21 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
@ -28,7 +44,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"

18
android/gradlew.bat

@ -1,3 +1,19 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem http://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

8
android/keystores/BUCK

@ -1,8 +0,0 @@
keystore(
name = "debug",
properties = "debug.keystore.properties",
store = "debug.keystore",
visibility = [
"PUBLIC",
],
)

4
android/keystores/debug.keystore.properties

@ -1,4 +0,0 @@
key.store=debug.keystore
key.alias=androiddebugkey
key.store.password=android
key.alias.password=android

10
android/metadata/en-US/changelogs/14.txt

@ -1,10 +0,0 @@
v3.5.0
------
ADD: Create LND invoice
ADD: Ability to show wallet XPUB in options
ADD: translations for german (DE)
ADD: Set receive amount & label
ADD: Added more Fiat currencies
ADD: help text in lighning settings
ADD: CZ locale

9
android/metadata/en-US/changelogs/15.txt

@ -1,9 +0,0 @@
v3.5.5
------
ADD: pay zero-amount (tip) invoices
ADD: lightning withdrawal through zigzag
ADD: Thai translation
ADD: Dutch translation
ADD: Added Singapore Dollars
ADD: Added AUD, VEF, and ZAR fiats.

1
android/metadata/en-US/changelogs/5.txt

@ -1 +0,0 @@
Initial Android release

1
android/metadata/en-US/changelogs/7.txt

@ -1 +0,0 @@
Bug fixes and performance improvements

1
android/metadata/en-US/changelogs/8.txt

@ -1 +0,0 @@
Bug fixes and performance improvements

1
android/metadata/en-US/changelogs/9.txt

@ -1 +0,0 @@
Bug fixes and performance improvements

39
android/metadata/en-US/full_description.txt

@ -1,39 +0,0 @@
Store, send and receive bitcoin with the wallet focus on security and simplicity.
On BlueWallet you own you private keys.
You can instantly transact with anyone in the world and transform the financial system right from your pocket.
Create for free unlimited number of wallets or access your existing one on your Android device. It's simple and fast.
_____
Here's what you get:
1 - Security by design
Open Source
MIT licensed, you can build it and run it on your own! Made with ReactNative
Plausible deniability
Password which decrypts fake wallets if you are forced to disclose your access
Full encryption
You can protect and encrypt your wallet by defining a password
SegWit & HD wallets
SegWit supported and HD wallets enable
2 - Focused on your experience
Be in control
Private keys never leave your device. You control your private keys
Flexible fees
Starting from 1 Satoshi. Defined by you, the user
Replace-By-Fee
(RBF) Speed-up your transactions by increasing the fee (BIP125)
Sentinel wallets new
Watch-only wallets allow you to keep an eye on your cold storage without touching the hardware.

BIN
android/metadata/en-US/images/icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

BIN
android/metadata/en-US/images/promoGraphic.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

1
android/metadata/en-US/short_description.txt

@ -1 +0,0 @@
A Bitcoin & Lightning wallet for android. Easy to use and secure

1
android/metadata/en-US/title.txt

@ -1 +0,0 @@
BlueWallet - Bitcoin & Lightning Wallet

0
android/metadata/en-US/video.txt

5
android/sentry.properties

@ -1,5 +0,0 @@
defaults.url=https://sentry.io/
defaults.org=bluewallet
defaults.project=bluewallet
auth.token=0ee298bd4d3743819f710a5ed555f5429e4ffe64acbb41ac933f2745b0c163da
cli.executable=node_modules/@sentry/cli/bin/sentry-cli

40
android/settings.gradle

@ -1,41 +1,3 @@
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'
project(':react-native-obscure').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-obscure/android')
include ':react-native-tcp'
project(':react-native-tcp').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-tcp/android')
include ':@remobile_react-native-qrcode-local-image'
project(':@remobile_react-native-qrcode-local-image').projectDir = new File(rootProject.projectDir, '../node_modules/@remobile/react-native-qrcode-local-image/android')
include ':react-native-image-picker'
project(':react-native-image-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-image-picker/android')
include ':react-native-webview'
project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview/android')
include ':react-native-svg'
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
include ':react-native-vector-icons'
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
include ':react-native-sentry'
project(':react-native-sentry').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sentry/android')
include ':react-native-randombytes'
project(':react-native-randombytes').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-randombytes/android')
include ':react-native-prompt-android'
project(':react-native-prompt-android').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-prompt-android/android')
include ':react-native-linear-gradient'
project(':react-native-linear-gradient').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-linear-gradient/android')
include ':react-native-haptic-feedback'
project(':react-native-haptic-feedback').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-haptic-feedback/android')
include ':react-native-google-analytics-bridge'
project(':react-native-google-analytics-bridge').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-google-analytics-bridge/android')
include ':react-native-gesture-handler'
project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android')
include ':react-native-fs'
project(':react-native-fs').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fs/android')
include ':react-native-device-info'
project(':react-native-device-info').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-device-info/android')
include ':react-native-camera'
project(':react-native-camera').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-camera/android')
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
include ':app'

1
android/upload.sh

@ -1 +0,0 @@
fastlane supply --json_key ~/Documents/bluewalletsecure/api-8414470778596272879-665287-df921afdf533.json --package_name io.bluewallet.bluewallet --apk ~/Documents/BlueWallet/android/app/build/outputs/apk/release/app-release.apk

17
class/abstract-wallet.js

@ -28,6 +28,7 @@ export class AbstractWallet {
this._lastBalanceFetch = 0;
this.preferredBalanceUnit = BitcoinUnit.BTC;
this.chain = Chain.ONCHAIN;
this.hideBalance = false;
}
getTransactions() {
@ -42,6 +43,10 @@ export class AbstractWallet {
return this.label;
}
getXpub() {
return this._address;
}
/**
*
* @returns {number} Available to spend amount, int, in sats
@ -67,10 +72,22 @@ export class AbstractWallet {
return true;
}
allowSendMax(): boolean {
return false;
}
allowRBF() {
return false;
}
allowBatchSend() {
return false;
}
weOwnAddress(address) {
return this._address === address;
}
/**
* Returns delta of unconfirmed balance. For example, if theres no
* unconfirmed balance its 0

74
class/app-storage.js

@ -1,4 +1,5 @@
import AsyncStorage from '@react-native-community/async-storage';
import RNSecureKeyStore, { ACCESSIBLE } from 'react-native-secure-key-store';
import {
HDLegacyBreadwalletWallet,
HDSegwitP2SHWallet,
@ -18,6 +19,8 @@ export class AppStorage {
static LANG = 'lang';
static EXCHANGE_RATES = 'currency';
static LNDHUB = 'lndhub';
static ELECTRUM_HOST = 'electrum_host';
static ELECTRUM_TCP_PORT = 'electrum_tcp_port';
static PREFERRED_CURRENCY = 'preferredCurrency';
static ADVANCED_MODE_ENABLED = 'advancedmodeenabled';
@ -54,10 +57,41 @@ export class AppStorage {
};
}
/**
* Wrapper for storage call. Secure store works only in RN environment. AsyncStorage is
* used for cli/tests
*
* @param key
* @param value
* @returns {Promise<any>|Promise<any> | Promise<void> | * | Promise | void}
*/
setItem(key, value) {
if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
return RNSecureKeyStore.set(key, value, { accessible: ACCESSIBLE.WHEN_UNLOCKED });
} else {
return AsyncStorage.setItem(key, value);
}
}
/**
* Wrapper for storage call. Secure store works only in RN environment. AsyncStorage is
* used for cli/tests
*
* @param key
* @returns {Promise<any>|*}
*/
getItem(key) {
if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
return RNSecureKeyStore.get(key);
} else {
return AsyncStorage.getItem(key);
}
}
async storageIsEncrypted() {
let data;
try {
data = await AsyncStorage.getItem(AppStorage.FLAG_ENCRYPTED);
data = await this.getItem(AppStorage.FLAG_ENCRYPTED);
} catch (error) {
return false;
}
@ -93,7 +127,7 @@ export class AppStorage {
async encryptStorage(password) {
// assuming the storage is not yet encrypted
await this.saveToDisk();
let data = await AsyncStorage.getItem('data');
let data = await this.getItem('data');
// TODO: refactor ^^^ (should not save & load to fetch data)
let encrypted = encryption.encrypt(data, password);
@ -101,8 +135,8 @@ export class AppStorage {
data.push(encrypted); // putting in array as we might have many buckets with storages
data = JSON.stringify(data);
this.cachedPassword = password;
await AsyncStorage.setItem('data', data);
await AsyncStorage.setItem(AppStorage.FLAG_ENCRYPTED, '1');
await this.setItem('data', data);
await this.setItem(AppStorage.FLAG_ENCRYPTED, '1');
}
/**
@ -120,13 +154,13 @@ export class AppStorage {
tx_metadata: {},
};
let buckets = await AsyncStorage.getItem('data');
let buckets = await this.getItem('data');
buckets = JSON.parse(buckets);
buckets.push(encryption.encrypt(JSON.stringify(data), fakePassword));
this.cachedPassword = fakePassword;
const bucketsString = JSON.stringify(buckets);
await AsyncStorage.setItem('data', bucketsString);
return (await AsyncStorage.getItem('data')) === bucketsString;
await this.setItem('data', bucketsString);
return (await this.getItem('data')) === bucketsString;
}
/**
@ -138,7 +172,7 @@ export class AppStorage {
*/
async loadFromDisk(password) {
try {
let data = await AsyncStorage.getItem('data');
let data = await this.getItem('data');
if (password) {
data = this.decryptData(data, password);
if (data) {
@ -211,7 +245,7 @@ export class AppStorage {
}
}
WatchConnectivity.init();
await WatchConnectivity.shared.sendWalletsToWatch();
WatchConnectivity.shared && (await WatchConnectivity.shared.sendWalletsToWatch());
return true;
} else {
return false; // failed loading data or loading/decryptin data
@ -248,7 +282,7 @@ export class AppStorage {
* If cached password is saved - finds the correct bucket
* to save to, encrypts and then saves.
*
* @returns {Promise} Result of AsyncStorage save
* @returns {Promise} Result of storage save
*/
async saveToDisk() {
let walletsToSave = [];
@ -265,7 +299,7 @@ export class AppStorage {
if (this.cachedPassword) {
// should find the correct bucket, encrypt and then save
let buckets = await AsyncStorage.getItem('data');
let buckets = await this.getItem('data');
buckets = JSON.parse(buckets);
let newData = [];
for (let bucket of buckets) {
@ -277,16 +311,16 @@ export class AppStorage {
// decrypted ok, this is our bucket
// we serialize our object's data, encrypt it, and add it to buckets
newData.push(encryption.encrypt(JSON.stringify(data), this.cachedPassword));
await AsyncStorage.setItem(AppStorage.FLAG_ENCRYPTED, '1');
await this.setItem(AppStorage.FLAG_ENCRYPTED, '1');
}
}
data = newData;
} else {
await AsyncStorage.setItem(AppStorage.FLAG_ENCRYPTED, ''); // drop the flag
await this.setItem(AppStorage.FLAG_ENCRYPTED, ''); // drop the flag
}
WatchConnectivity.init();
WatchConnectivity.shared.sendWalletsToWatch();
return AsyncStorage.setItem('data', JSON.stringify(data));
WatchConnectivity.shared && WatchConnectivity.shared.sendWalletsToWatch();
return this.setItem('data', JSON.stringify(data));
}
/**
@ -411,4 +445,14 @@ export class AppStorage {
}
return finalBalance;
}
/**
* Simple async sleeper function
*
* @param ms {number} Milliseconds to sleep
* @returns {Promise<Promise<*> | Promise<*>>}
*/
async sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}

332
class/hd-segwit-bech32-transaction.js

@ -0,0 +1,332 @@
import { HDSegwitBech32Wallet, SegwitBech32Wallet } from './';
const bitcoin = require('bitcoinjs5');
const BlueElectrum = require('../BlueElectrum');
const reverse = require('buffer-reverse');
const BigNumber = require('bignumber.js');
/**
* Represents transaction of a BIP84 wallet.
* Helpers for RBF, CPFP etc.
*/
export class HDSegwitBech32Transaction {
/**
* @param txhex {string|null} Object is initialized with txhex
* @param txid {string|null} If txhex not present - txid whould be present
* @param wallet {HDSegwitBech32Wallet|null} If set - a wallet object to which transacton belongs
*/
constructor(txhex, txid, wallet) {
if (!txhex && !txid) throw new Error('Bad arguments');
this._txhex = txhex;
this._txid = txid;
if (wallet) {
if (wallet.type === HDSegwitBech32Wallet.type) {
/** @type {HDSegwitBech32Wallet} */
this._wallet = wallet;
} else {
throw new Error('Only HD Bech32 wallets supported');
}
}
if (this._txhex) this._txDecoded = bitcoin.Transaction.fromHex(this._txhex);
this._remoteTx = null;
}
/**
* If only txid present - we fetch hex
*
* @returns {Promise<void>}
* @private
*/
async _fetchTxhexAndDecode() {
let hexes = await BlueElectrum.multiGetTransactionByTxid([this._txid], 10, false);
this._txhex = hexes[this._txid];
if (!this._txhex) throw new Error("Transaction can't be found in mempool");
this._txDecoded = bitcoin.Transaction.fromHex(this._txhex);
}
/**
* Returns max used sequence for this transaction. Next RBF transaction
* should have this sequence + 1
*
* @returns {Promise<number>}
*/
async getMaxUsedSequence() {
if (!this._txDecoded) await this._fetchTxhexAndDecode();
let max = 0;
for (let inp of this._txDecoded.ins) {
max = Math.max(inp.sequence, max);
}
return max;
}
/**
* Basic check that Sequence num for this TX is replaceable
*
* @returns {Promise<boolean>}
*/
async isSequenceReplaceable() {
return (await this.getMaxUsedSequence()) < bitcoin.Transaction.DEFAULT_SEQUENCE;
}
/**
* If internal extended tx data not set - this is a method
* to fetch and set this data from electrum. Its different data from
* decoded hex - it contains confirmations etc.
*
* @returns {Promise<void>}
* @private
*/
async _fetchRemoteTx() {
let result = await BlueElectrum.multiGetTransactionByTxid([this._txid || this._txDecoded.getId()]);
this._remoteTx = Object.values(result)[0];
}
/**
* Fetches from electrum actual confirmations number for this tx
*
* @returns {Promise<Number>}
*/
async getRemoteConfirmationsNum() {
if (!this._remoteTx) await this._fetchRemoteTx();
return this._remoteTx.confirmations || 0; // stupid undefined
}
/**
* Checks that tx belongs to a wallet and also
* tx value is < 0, which means its a spending transaction
* definately initiated by us, can be RBF'ed.
*
* @returns {Promise<boolean>}
*/
async isOurTransaction() {
if (!this._wallet) throw new Error('Wallet required for this method');
let found = false;
for (let tx of this._wallet.getTransactions()) {
if (tx.txid === (this._txid || this._txDecoded.getId())) {
// its our transaction, and its spending transaction, which means we initiated it
if (tx.value < 0) found = true;
}
}
return found;
}
/**
* Checks that tx belongs to a wallet and also
* tx value is > 0, which means its a receiving transaction and thus
* can be CPFP'ed.
*
* @returns {Promise<boolean>}
*/
async isToUsTransaction() {
if (!this._wallet) throw new Error('Wallet required for this method');
let found = false;
for (let tx of this._wallet.getTransactions()) {
if (tx.txid === (this._txid || this._txDecoded.getId())) {
if (tx.value > 0) found = true;
}
}
return found;
}
/**
* Returns all the info about current transaction which is needed to do a replacement TX
* * fee - current tx fee
* * utxos - UTXOs current tx consumes
* * changeAmount - amount of satoshis that sent to change address (or addresses) we control
* * feeRate - sat/byte for current tx
* * targets - destination(s) of funds (outputs we do not control)
* * unconfirmedUtxos - UTXOs created by this transaction (only the ones we control)
*
* @returns {Promise<{fee: number, utxos: Array, unconfirmedUtxos: Array, changeAmount: number, feeRate: number, targets: Array}>}
*/
async getInfo() {
if (!this._wallet) throw new Error('Wallet required for this method');
if (!this._remoteTx) await this._fetchRemoteTx();
if (!this._txDecoded) await this._fetchTxhexAndDecode();
let prevInputs = [];
for (let inp of this._txDecoded.ins) {
let reversedHash = Buffer.from(reverse(inp.hash));
reversedHash = reversedHash.toString('hex');
prevInputs.push(reversedHash);
}
let prevTransactions = await BlueElectrum.multiGetTransactionByTxid(prevInputs);
// fetched, now lets count how much satoshis went in
let wentIn = 0;
let utxos = [];
for (let inp of this._txDecoded.ins) {
let reversedHash = Buffer.from(reverse(inp.hash));
reversedHash = reversedHash.toString('hex');
if (prevTransactions[reversedHash] && prevTransactions[reversedHash].vout && prevTransactions[reversedHash].vout[inp.index]) {
let value = prevTransactions[reversedHash].vout[inp.index].value;
value = new BigNumber(value).multipliedBy(100000000).toNumber();
wentIn += value;
let address = SegwitBech32Wallet.witnessToAddress(inp.witness[inp.witness.length - 1]);
utxos.push({ vout: inp.index, value: value, txId: reversedHash, address: address });
}
}
// counting how much went into actual outputs
let wasSpent = 0;
for (let outp of this._txDecoded.outs) {
wasSpent += +outp.value;
}
let fee = wentIn - wasSpent;
let feeRate = Math.floor(fee / (this._txhex.length / 2));
if (feeRate === 0) feeRate = 1;
// lets take a look at change
let changeAmount = 0;
let targets = [];
for (let outp of this._remoteTx.vout) {
let address = outp.scriptPubKey.addresses[0];
let value = new BigNumber(outp.value).multipliedBy(100000000).toNumber();
if (this._wallet.weOwnAddress(address)) {
changeAmount += value;
} else {
// this is target
targets.push({ value: value, address: address });
}
}
// lets find outputs we own that current transaction creates. can be used in CPFP
let unconfirmedUtxos = [];
for (let outp of this._remoteTx.vout) {
let address = outp.scriptPubKey.addresses[0];
let value = new BigNumber(outp.value).multipliedBy(100000000).toNumber();
if (this._wallet.weOwnAddress(address)) {
unconfirmedUtxos.push({
vout: outp.n,
value: value,
txId: this._txid || this._txDecoded.getId(),
address: address,
});
}
}
return { fee, feeRate, targets, changeAmount, utxos, unconfirmedUtxos };
}
/**
* Checks if all outputs belong to us, that
* means we already canceled this tx and we can only bump fees
*
* @returns {Promise<boolean>}
*/
async canCancelTx() {
if (!this._wallet) throw new Error('Wallet required for this method');
if (!this._txDecoded) await this._fetchTxhexAndDecode();
// if theres at least one output we dont own - we can cancel this transaction!
for (let outp of this._txDecoded.outs) {
if (!this._wallet.weOwnAddress(SegwitBech32Wallet.scriptPubKeyToAddress(outp.script))) return true;
}
return false;
}
/**
* Creates an RBF transaction that can replace previous one and basically cancel it (rewrite
* output to the one our wallet controls). Note, this cannot add more utxo in RBF transaction if
* newFeerate is too high
*
* @param newFeerate {number} Sat/byte. Should be greater than previous tx feerate
* @returns {Promise<{outputs: Array, tx: Transaction, inputs: Array, fee: Number}>}
*/
async createRBFcancelTx(newFeerate) {
if (!this._wallet) throw new Error('Wallet required for this method');
if (!this._remoteTx) await this._fetchRemoteTx();
let { feeRate, utxos } = await this.getInfo();
if (newFeerate <= feeRate) throw new Error('New feerate should be bigger than the old one');
let myAddress = await this._wallet.getChangeAddressAsync();
return this._wallet.createTransaction(
utxos,
[{ address: myAddress }],
newFeerate,
/* meaningless in this context */ myAddress,
(await this.getMaxUsedSequence()) + 1,
);
}
/**
* Creates an RBF transaction that can bumps fee of previous one. Note, this cannot add more utxo in RBF
* transaction if newFeerate is too high
*
* @param newFeerate {number} Sat/byte
* @returns {Promise<{outputs: Array, tx: Transaction, inputs: Array, fee: Number}>}
*/
async createRBFbumpFee(newFeerate) {
if (!this._wallet) throw new Error('Wallet required for this method');
if (!this._remoteTx) await this._fetchRemoteTx();
let { feeRate, targets, changeAmount, utxos } = await this.getInfo();
if (newFeerate <= feeRate) throw new Error('New feerate should be bigger than the old one');
let myAddress = await this._wallet.getChangeAddressAsync();
if (changeAmount === 0) delete targets[0].value;
// looks like this was sendMAX transaction (because there was no change), so we cant reuse amount in this
// target since fee wont change. removing the amount so `createTransaction` will sendMAX correctly with new feeRate
if (targets.length === 0) {
// looks like this was cancelled tx with single change output, so it wasnt included in `this.getInfo()` targets
// so we add output paying ourselves:
targets.push({ address: this._wallet._getInternalAddressByIndex(this._wallet.next_free_change_address_index) });
// not checking emptiness on purpose: it could unpredictably generate too far address because of unconfirmed tx.
}
return this._wallet.createTransaction(utxos, targets, newFeerate, myAddress, (await this.getMaxUsedSequence()) + 1);
}
/**
* Creates a CPFP transaction that can bumps fee of previous one (spends created but not confirmed outputs
* that belong to us). Note, this cannot add more utxo in CPFP transaction if newFeerate is too high
*
* @param newFeerate {number} sat/byte
* @returns {Promise<{outputs: Array, tx: Transaction, inputs: Array, fee: Number}>}
*/
async createCPFPbumpFee(newFeerate) {
if (!this._wallet) throw new Error('Wallet required for this method');
if (!this._remoteTx) await this._fetchRemoteTx();
let { feeRate, fee: oldFee, unconfirmedUtxos } = await this.getInfo();
if (newFeerate <= feeRate) throw new Error('New feerate should be bigger than the old one');
let myAddress = await this._wallet.getChangeAddressAsync();
// calculating feerate for CPFP tx so that average between current and CPFP tx will equal newFeerate.
// this works well if both txs are +/- equal size in bytes
const targetFeeRate = 2 * newFeerate - feeRate;
let add = 0;
while (add <= 128) {
var { tx, inputs, outputs, fee } = this._wallet.createTransaction(
unconfirmedUtxos,
[{ address: myAddress }],
targetFeeRate + add,
myAddress,
HDSegwitBech32Wallet.defaultRBFSequence,
);
let combinedFeeRate = (oldFee + fee) / (this._txhex.length / 2 + tx.toHex().length / 2); // avg
if (Math.round(combinedFeeRate) < newFeerate) {
add *= 2;
if (!add) add = 2;
} else {
// reached target feerate
break;
}
}
return { tx, inputs, outputs, fee };
}
}

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

@ -11,32 +11,6 @@ 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)
@ -45,6 +19,7 @@ function _nodeToBech32SegwitAddress(hdNode) {
export class HDSegwitBech32Wallet extends AbstractHDWallet {
static type = 'HDsegwitBech32';
static typeReadable = 'HD SegWit (BIP84 Bech32 Native)';
static defaultRBFSequence = 2147483648; // 1 << 31, minimum for replaceable transactions as per BIP68
constructor() {
super();
@ -57,6 +32,14 @@ export class HDSegwitBech32Wallet extends AbstractHDWallet {
this._utxo = [];
}
allowBatchSend() {
return true;
}
allowSendMax(): boolean {
return true;
}
/**
* @inheritDoc
*/
@ -68,7 +51,7 @@ export class HDSegwitBech32Wallet extends AbstractHDWallet {
for (let bal of Object.values(this._balances_by_internal_index)) {
ret += bal.c;
}
return ret + this.getUnconfirmedBalance();
return ret + (this.getUnconfirmedBalance() < 0 ? this.getUnconfirmedBalance() : 0);
}
/**
@ -155,24 +138,24 @@ export class HDSegwitBech32Wallet extends AbstractHDWallet {
}
if (node === 0 && !this._node0) {
const xpub = _zpubToXpub(this.getXpub());
const xpub = this.constructor._zpubToXpub(this.getXpub());
const hdNode = HDNode.fromBase58(xpub);
this._node0 = hdNode.derive(node);
}
if (node === 1 && !this._node1) {
const xpub = _zpubToXpub(this.getXpub());
const xpub = this.constructor._zpubToXpub(this.getXpub());
const hdNode = HDNode.fromBase58(xpub);
this._node1 = hdNode.derive(node);
}
let address;
if (node === 0) {
address = _nodeToBech32SegwitAddress(this._node0.derive(index));
address = this.constructor._nodeToBech32SegwitAddress(this._node0.derive(index));
}
if (node === 1) {
address = _nodeToBech32SegwitAddress(this._node1.derive(index));
address = this.constructor._nodeToBech32SegwitAddress(this._node1.derive(index));
}
if (node === 0) {
@ -228,15 +211,23 @@ export class HDSegwitBech32Wallet extends AbstractHDWallet {
// 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
// OR some tx has < 7 confirmations
// fetching transactions in batch: first, getting batch history for all addresses,
// then batch fetching all involved txids
// finally, batch fetching txids of all inputs (needed to see amounts & addresses of those inputs)
// then we combine it all together
let addresses2fetch = [];
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);
for (let tx of this._txs_by_external_index[c]) hasUnconfirmed = hasUnconfirmed || !tx.confirmations || tx.confirmations < 7;
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));
addresses2fetch.push(this._getExternalAddressByIndex(c));
}
}
@ -244,10 +235,153 @@ export class HDSegwitBech32Wallet extends AbstractHDWallet {
// 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);
for (let tx of this._txs_by_internal_index[c]) hasUnconfirmed = hasUnconfirmed || !tx.confirmations || tx.confirmations < 7;
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));
addresses2fetch.push(this._getInternalAddressByIndex(c));
}
}
// first: batch fetch for all addresses histories
let histories = await BlueElectrum.multiGetHistoryByAddress(addresses2fetch);
let txs = {};
for (let history of Object.values(histories)) {
for (let tx of history) {
txs[tx.tx_hash] = tx;
}
}
// next, batch fetching each txid we got
let txdatas = await BlueElectrum.multiGetTransactionByTxid(Object.keys(txs));
// now, tricky part. we collect all transactions from inputs (vin), and batch fetch them too.
// then we combine all this data (we need inputs to see source addresses and amounts)
let vinTxids = [];
for (let txdata of Object.values(txdatas)) {
for (let vin of txdata.vin) {
vinTxids.push(vin.txid);
}
}
let vintxdatas = await BlueElectrum.multiGetTransactionByTxid(vinTxids);
// fetched all transactions from our inputs. now we need to combine it.
// iterating all _our_ transactions:
for (let txid of Object.keys(txdatas)) {
// iterating all inputs our our single transaction:
for (let inpNum = 0; inpNum < txdatas[txid].vin.length; inpNum++) {
let inpTxid = txdatas[txid].vin[inpNum].txid;
let inpVout = txdatas[txid].vin[inpNum].vout;
// got txid and output number of _previous_ transaction we shoud look into
if (vintxdatas[inpTxid] && vintxdatas[inpTxid].vout[inpVout]) {
// extracting amount & addresses from previous output and adding it to _our_ input:
txdatas[txid].vin[inpNum].addresses = vintxdatas[inpTxid].vout[inpVout].scriptPubKey.addresses;
txdatas[txid].vin[inpNum].value = vintxdatas[inpTxid].vout[inpVout].value;
}
}
}
// now purge all unconfirmed txs from internal hashmaps, since some may be evicted from mempool because they became invalid
// or replaced. hashmaps are going to be re-populated anyways, since we fetched TXs for addresses with unconfirmed TXs
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
this._txs_by_external_index[c] = this._txs_by_external_index[c].filter(tx => !!tx.confirmations);
}
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
this._txs_by_internal_index[c] = this._txs_by_internal_index[c].filter(tx => !!tx.confirmations);
}
// now, we need to put transactions in all relevant `cells` of internal hashmaps: this._txs_by_internal_index && this._txs_by_external_index
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
for (let tx of Object.values(txdatas)) {
for (let vin of tx.vin) {
if (vin.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) {
// this TX is related to our address
this._txs_by_external_index[c] = this._txs_by_external_index[c] || [];
let clonedTx = Object.assign({}, tx);
clonedTx.inputs = tx.vin.slice(0);
clonedTx.outputs = tx.vout.slice(0);
delete clonedTx.vin;
delete clonedTx.vout;
// trying to replace tx if it exists already (because it has lower confirmations, for example)
let replaced = false;
for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) {
if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) {
replaced = true;
this._txs_by_external_index[c][cc] = clonedTx;
}
}
if (!replaced) this._txs_by_external_index[c].push(clonedTx);
}
}
for (let vout of tx.vout) {
if (vout.scriptPubKey.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) {
// this TX is related to our address
this._txs_by_external_index[c] = this._txs_by_external_index[c] || [];
let clonedTx = Object.assign({}, tx);
clonedTx.inputs = tx.vin.slice(0);
clonedTx.outputs = tx.vout.slice(0);
delete clonedTx.vin;
delete clonedTx.vout;
// trying to replace tx if it exists already (because it has lower confirmations, for example)
let replaced = false;
for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) {
if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) {
replaced = true;
this._txs_by_external_index[c][cc] = clonedTx;
}
}
if (!replaced) this._txs_by_external_index[c].push(clonedTx);
}
}
}
}
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
for (let tx of Object.values(txdatas)) {
for (let vin of tx.vin) {
if (vin.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) {
// this TX is related to our address
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || [];
let clonedTx = Object.assign({}, tx);
clonedTx.inputs = tx.vin.slice(0);
clonedTx.outputs = tx.vout.slice(0);
delete clonedTx.vin;
delete clonedTx.vout;
// trying to replace tx if it exists already (because it has lower confirmations, for example)
let replaced = false;
for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) {
if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) {
replaced = true;
this._txs_by_internal_index[c][cc] = clonedTx;
}
}
if (!replaced) this._txs_by_internal_index[c].push(clonedTx);
}
}
for (let vout of tx.vout) {
if (vout.scriptPubKey.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) {
// this TX is related to our address
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || [];
let clonedTx = Object.assign({}, tx);
clonedTx.inputs = tx.vin.slice(0);
clonedTx.outputs = tx.vout.slice(0);
delete clonedTx.vin;
delete clonedTx.vout;
// trying to replace tx if it exists already (because it has lower confirmations, for example)
let replaced = false;
for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) {
if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) {
replaced = true;
this._txs_by_internal_index[c][cc] = clonedTx;
}
}
if (!replaced) this._txs_by_internal_index[c].push(clonedTx);
}
}
}
}
@ -274,14 +408,14 @@ export class HDSegwitBech32Wallet extends AbstractHDWallet {
for (let vin of tx.inputs) {
// if input (spending) goes from our address - we are loosing!
if (vin.address && this.weOwnAddress(vin.address)) {
if ((vin.address && this.weOwnAddress(vin.address)) || (vin.addresses && vin.addresses[0] && this.weOwnAddress(vin.addresses[0]))) {
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])) {
if (vout.scriptPubKey.addresses && vout.scriptPubKey.addresses[0] && this.weOwnAddress(vout.scriptPubKey.addresses[0])) {
tx.value += new BigNumber(vout.value).multipliedBy(100000000).toNumber();
}
}
@ -301,6 +435,105 @@ export class HDSegwitBech32Wallet extends AbstractHDWallet {
});
}
async _binarySearchIterationForInternalAddress(index) {
const gerenateChunkAddresses = chunkNum => {
let ret = [];
for (let c = this.gap_limit * chunkNum; c < this.gap_limit * (chunkNum + 1); c++) {
ret.push(this._getInternalAddressByIndex(c));
}
return ret;
};
let lastChunkWithUsedAddressesNum = null;
let lastHistoriesWithUsedAddresses = null;
for (let c = 0; c < Math.round(index / this.gap_limit); c++) {
let histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c));
if (this.constructor._getTransactionsFromHistories(histories).length > 0) {
// in this particular chunk we have used addresses
lastChunkWithUsedAddressesNum = c;
lastHistoriesWithUsedAddresses = histories;
} else {
// empty chunk. no sense searching more chunks
break;
}
}
let lastUsedIndex = 0;
if (lastHistoriesWithUsedAddresses) {
// now searching for last used address in batch lastChunkWithUsedAddressesNum
for (
let c = lastChunkWithUsedAddressesNum * this.gap_limit;
c < lastChunkWithUsedAddressesNum * this.gap_limit + this.gap_limit;
c++
) {
let address = this._getInternalAddressByIndex(c);
if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) {
lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unsued
}
}
}
return lastUsedIndex;
}
async _binarySearchIterationForExternalAddress(index) {
const gerenateChunkAddresses = chunkNum => {
let ret = [];
for (let c = this.gap_limit * chunkNum; c < this.gap_limit * (chunkNum + 1); c++) {
ret.push(this._getExternalAddressByIndex(c));
}
return ret;
};
let lastChunkWithUsedAddressesNum = null;
let lastHistoriesWithUsedAddresses = null;
for (let c = 0; c < Math.round(index / this.gap_limit); c++) {
let histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c));
if (this.constructor._getTransactionsFromHistories(histories).length > 0) {
// in this particular chunk we have used addresses
lastChunkWithUsedAddressesNum = c;
lastHistoriesWithUsedAddresses = histories;
} else {
// empty chunk. no sense searching more chunks
break;
}
}
let lastUsedIndex = 0;
if (lastHistoriesWithUsedAddresses) {
// now searching for last used address in batch lastChunkWithUsedAddressesNum
for (
let c = lastChunkWithUsedAddressesNum * this.gap_limit;
c < lastChunkWithUsedAddressesNum * this.gap_limit + this.gap_limit;
c++
) {
let address = this._getExternalAddressByIndex(c);
if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) {
lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unsued
}
}
}
return lastUsedIndex;
}
async fetchBalance() {
try {
if (this.next_free_change_address_index === 0 && this.next_free_address_index === 0) {
// doing binary search for last used address:
this.next_free_change_address_index = await this._binarySearchIterationForInternalAddress(1000);
this.next_free_address_index = await this._binarySearchIterationForExternalAddress(1000);
} // end rescanning fresh wallet
// finally fetching balance
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
@ -427,6 +660,9 @@ export class HDSegwitBech32Wallet extends AbstractHDWallet {
return false;
}
/**
* @deprecated
*/
createTx(utxos, amount, fee, address) {
throw new Error('Deprecated');
}
@ -442,7 +678,7 @@ export class HDSegwitBech32Wallet extends AbstractHDWallet {
*/
createTransaction(utxos, targets, feeRate, changeAddress, sequence) {
if (!changeAddress) throw new Error('No change address provided');
sequence = sequence || 0;
sequence = sequence || HDSegwitBech32Wallet.defaultRBFSequence;
let algo = coinSelectAccumulative;
if (targets.length === 1 && targets[0] && !targets[0].value) {
@ -489,4 +725,53 @@ export class HDSegwitBech32Wallet extends AbstractHDWallet {
const tx = txb.build();
return { tx, inputs, outputs, fee };
}
/**
* Creates Segwit Bech32 Bitcoin address
*
* @param hdNode
* @returns {String}
*/
static _nodeToBech32SegwitAddress(hdNode) {
return bitcoin5.payments.p2wpkh({
pubkey: hdNode.publicKey,
}).address;
}
/**
* Converts zpub to xpub
*
* @param {String} zpub
* @returns {String} xpub
*/
static _zpubToXpub(zpub) {
let data = b58.decode(zpub);
data = data.slice(4);
data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]);
return b58.encode(data);
}
static _getTransactionsFromHistories(histories) {
let txs = [];
for (let history of Object.values(histories)) {
for (let tx of history) {
txs.push(tx);
}
}
return txs;
}
/**
* Broadcast txhex. Can throw an exception if failed
*
* @param {String} txhex
* @returns {Promise<boolean>}
*/
async broadcastTx(txhex) {
let broadcast = await BlueElectrum.broadcastV2(txhex);
console.log({ broadcast });
if (broadcast.indexOf('successfully') !== -1) return true;
return broadcast.length === 64; // this means return string is txid (precise length), so it was broadcasted ok
}
}

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

@ -5,6 +5,7 @@ import bip39 from 'bip39';
import BigNumber from 'bignumber.js';
import b58 from 'bs58check';
import signer from '../models/signer';
import { BitcoinUnit } from '../models/bitcoinUnits';
const bitcoin = require('bitcoinjs-lib');
const bitcoin5 = require('bitcoinjs5');
const HDNode = require('bip32');
@ -49,6 +50,10 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet {
return true;
}
allowSendMax(): boolean {
return true;
}
async generate() {
let that = this;
return new Promise(function(resolve) {
@ -255,12 +260,29 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet {
}
}
/**
*
* @param utxos
* @param amount Either float (BTC) or string 'MAX' (BitcoinUnit.MAX) to send all
* @param fee
* @param address
* @returns {string}
*/
createTx(utxos, amount, fee, address) {
for (let utxo of utxos) {
utxo.wif = this._getWifForAddress(utxo.address);
}
let amountPlusFee = parseFloat(new BigNumber(amount).plus(fee).toString(10));
if (amount === BitcoinUnit.MAX) {
amountPlusFee = new BigNumber(0);
for (let utxo of utxos) {
amountPlusFee = amountPlusFee.plus(utxo.amount);
}
amountPlusFee = amountPlusFee.dividedBy(100000000).toString(10);
}
return signer.createHDSegwitTransaction(
utxos,
address,

1
class/index.js

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

6
class/lightning-custodian-wallet.js

@ -199,6 +199,10 @@ export class LightningCustodianWallet extends LegacyWallet {
await this.getUserInvoices();
}
isInvoiceGeneratedByWallet(paymentRequest) {
return this.user_invoices_raw.some(invoice => invoice.payment_request === paymentRequest);
}
async addInvoice(amt, memo) {
let response = await this._api.post('/addinvoice', {
body: { amt: amt + '', memo: memo },
@ -362,7 +366,7 @@ export class LightningCustodianWallet extends LegacyWallet {
txs = txs.concat(this.pending_transactions_raw.slice(), this.transactions_raw.slice().reverse(), this.user_invoices_raw.slice()); // slice so array is cloned
// transforming to how wallets/list screen expects it
for (let tx of txs) {
tx.fromWallet = this.secret;
tx.fromWallet = this.getSecret();
if (tx.amount) {
// pending tx
tx.amt = tx.amount * -100000000;

6
currency.js

@ -48,6 +48,8 @@ async function updateExchangeRate() {
}
} catch (Err) {
console.warn(Err);
const lastSavedExchangeRate = JSON.parse(await AsyncStorage.getItem(AppStorage.EXCHANGE_RATES));
exchangeRates['BTC_' + preferredFiatCurrency.endPointKey] = lastSavedExchangeRate['BTC_' + preferredFiatCurrency.endPointKey] * 1;
return;
}
@ -69,7 +71,7 @@ async function startUpdater() {
}
function satoshiToLocalCurrency(satoshi) {
if (!exchangeRates['BTC_' + preferredFiatCurrency.endPointKey]) return satoshi;
if (!exchangeRates['BTC_' + preferredFiatCurrency.endPointKey]) return '...';
let b = new BigNumber(satoshi);
b = b
@ -109,7 +111,7 @@ function BTCToLocalCurrency(bitcoin) {
function satoshiToBTC(satoshi) {
let b = new BigNumber(satoshi);
b = b.dividedBy(100000000);
return b.toString(10) + ' BTC';
return b.toString(10);
}
module.exports.updateExchangeRate = updateExchangeRate;

2
edit-version-number.sh

@ -2,3 +2,5 @@ vim ios/BlueWallet/Info.plist
vim ios/BlueWalletWatch/Info.plist
vim "ios/BlueWalletWatch Extension/Info.plist"
vim android/app/build.gradle
vim package.json
vim package-lock.json

1
img/bluewalletsplash.json

File diff suppressed because one or more lines are too long

BIN
img/close-white.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 B

BIN
img/close-white@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 B

BIN
img/close-white@3x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 B

45
index.js

@ -7,8 +7,11 @@ import { Sentry } from 'react-native-sentry';
import { AppRegistry } from 'react-native';
import WalletMigrate from './screen/wallets/walletMigrate';
import { name as appName } from './app.json';
import LottieView from 'lottie-react-native';
/** @type {AppStorage} */
const BlueApp = require('./BlueApp');
let A = require('./analytics');
if (process.env.NODE_ENV !== 'development') {
Sentry.config('https://23377936131848ca8003448a893cb622@sentry.io/1295736').install();
}
@ -21,16 +24,54 @@ if (!Error.captureStackTrace) {
class BlueAppComponent extends React.Component {
constructor(props) {
super(props);
this.state = { isMigratingData: true };
this.state = { isMigratingData: true, onAnimationFinished: false };
}
componentDidMount() {
const walletMigrate = new WalletMigrate(this.setIsMigratingData);
walletMigrate.start();
}
setIsMigratingData = async () => {
await BlueApp.startAndDecrypt();
A(A.ENUM.INIT);
this.setState({ isMigratingData: false });
};
onAnimationFinish = () => {
if (this.state.isMigratingData) {
this.loadingSplash.play(0);
} else {
this.setState({ onAnimationFinished: true });
}
};
render() {
return this.state.isMigratingData ? <WalletMigrate onComplete={this.setIsMigratingData} /> : <App />;
if (this.state.isMigratingData) {
return (
<LottieView
ref={ref => (this.loadingSplash = ref)}
onAnimationFinish={this.onAnimationFinish}
source={require('./img/bluewalletsplash.json')}
autoPlay
loop={false}
/>
);
} else {
if (this.state.onAnimationFinished) {
return <App />;
} else {
return (
<LottieView
ref={ref => (this.loadingSplash = ref)}
onAnimationFinish={this.onAnimationFinish}
source={require('./img/bluewalletsplash.json')}
autoPlay
loop={false}
/>
);
}
}
}
}

8
ios/AppCenter-Config.plist

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AppSecret</key>
<string>e83710b1-61c2-497b-b0f7-c3b6ab79f2d8</string>
</dict>
</plist>

4
ios/BlueWallet-Bridging-Header.h

@ -0,0 +1,4 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//

48
ios/BlueWallet.xcodeproj/project.pbxproj

@ -22,6 +22,9 @@
2D02E4BF1E0B4AB3006451C7 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
2D16E6881FA4F8E400B85C8A /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2D16E6891FA4F8E400B85C8A /* libReact.a */; };
2DCD954D1E0B4F2C00145EB5 /* BlueWalletTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* BlueWalletTests.m */; };
3208E93922F63279007F5A27 /* AppCenter-Config.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3208E93822F63279007F5A27 /* AppCenter-Config.plist */; };
32B5A32A2334450100F8D608 /* Bridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B5A3292334450100F8D608 /* Bridge.swift */; };
32F0A29A2311DBB20095C559 /* ComplicationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F0A2992311DBB20095C559 /* ComplicationController.swift */; };
34582CAA4AD140F7B80C961A /* libTcpSockets.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9DF4E6C040764E4BA1ACC1EB /* libTcpSockets.a */; };
34CC55B441594DBB95AD1B50 /* Octicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E8E8CE89B3D142C6A8A56C34 /* Octicons.ttf */; };
398DED6337DF58F0ECFD8F2E /* libPods-BlueWalletTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 70089FECE936F9A0AC45B7CE /* libPods-BlueWalletTests.a */; };
@ -146,6 +149,12 @@
2D02E4901E0B4A5D006451C7 /* BlueWallet-tvOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "BlueWallet-tvOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
2D16E6891FA4F8E400B85C8A /* libReact.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libReact.a; sourceTree = BUILT_PRODUCTS_DIR; };
2FCC2CD6FF4448229D0CE0F3 /* MaterialCommunityIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = MaterialCommunityIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf"; sourceTree = "<group>"; };
3208E93822F63279007F5A27 /* AppCenter-Config.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "AppCenter-Config.plist"; sourceTree = "<group>"; };
32B5A3282334450100F8D608 /* BlueWallet-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BlueWallet-Bridging-Header.h"; sourceTree = "<group>"; };
32B5A3292334450100F8D608 /* Bridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bridge.swift; sourceTree = "<group>"; };
32F0A24F2310B0700095C559 /* BlueWalletWatch Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "BlueWalletWatch Extension.entitlements"; sourceTree = "<group>"; };
32F0A2502310B0910095C559 /* BlueWallet.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = BlueWallet.entitlements; path = BlueWallet/BlueWallet.entitlements; sourceTree = "<group>"; };
32F0A2992311DBB20095C559 /* ComplicationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComplicationController.swift; sourceTree = "<group>"; };
334051161886419EA186F4BA /* FontAwesome.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome.ttf; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf"; sourceTree = "<group>"; };
3703B10AAB374CF896CCC2EA /* libBVLinearGradient.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libBVLinearGradient.a; sourceTree = "<group>"; };
3F7F1B8332C6439793D55B45 /* EvilIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = EvilIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf"; sourceTree = "<group>"; };
@ -306,6 +315,8 @@
13B07FAE1A68108700A75B9A /* BlueWallet */ = {
isa = PBXGroup;
children = (
32F0A2502310B0910095C559 /* BlueWallet.entitlements */,
3208E93822F63279007F5A27 /* AppCenter-Config.plist */,
008F07F21AC5B25A0029DE68 /* main.jsbundle */,
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
13B07FB01A68108700A75B9A /* AppDelegate.m */,
@ -313,6 +324,8 @@
13B07FB61A68108700A75B9A /* Info.plist */,
13B07FB11A68108700A75B9A /* LaunchScreen.xib */,
13B07FB71A68108700A75B9A /* main.m */,
32B5A3292334450100F8D608 /* Bridge.swift */,
32B5A3282334450100F8D608 /* BlueWallet-Bridging-Header.h */,
);
name = BlueWallet;
sourceTree = "<group>";
@ -419,8 +432,10 @@
B40D4E40225841ED00428FCC /* BlueWalletWatch Extension */ = {
isa = PBXGroup;
children = (
32F0A24F2310B0700095C559 /* BlueWalletWatch Extension.entitlements */,
B43D03242258474500FBAA95 /* Objects */,
B40D4E672258426B00428FCC /* KeychainSwiftDistrib.swift */,
32F0A2992311DBB20095C559 /* ComplicationController.swift */,
B40D4E43225841ED00428FCC /* ExtensionDelegate.swift */,
B40D4E45225841ED00428FCC /* NotificationController.swift */,
B40D4E552258425400428FCC /* InterfaceController.swift */,
@ -614,7 +629,13 @@
};
13B07F861A680F5B00A75B9A = {
DevelopmentTeam = A7W54YZ4WU;
LastSwiftMigration = 1030;
ProvisioningStyle = Manual;
SystemCapabilities = {
com.apple.Keychain = {
enabled = 0;
};
};
};
2D02E47A1E0B4A5D006451C7 = {
CreatedOnToolsVersion = 8.2.1;
@ -636,6 +657,11 @@
CreatedOnToolsVersion = 10.2;
DevelopmentTeam = A7W54YZ4WU;
ProvisioningStyle = Manual;
SystemCapabilities = {
com.apple.Keychain = {
enabled = 0;
};
};
};
};
};
@ -686,6 +712,7 @@
854972E4A6134C14A1D3A5F9 /* FontAwesome5_Regular.ttf in Resources */,
D8E3A15E21994BC3AF6CEECE /* FontAwesome5_Solid.ttf in Resources */,
66AB95FA29464B0BA106AA67 /* Foundation.ttf in Resources */,
3208E93922F63279007F5A27 /* AppCenter-Config.plist in Resources */,
CF81A1855609466D90635511 /* Ionicons.ttf in Resources */,
D5B495319D1B4542BE945CEA /* MaterialCommunityIcons.ttf in Resources */,
6C313BF9BC3E4BD2A65AA547 /* MaterialIcons.ttf in Resources */,
@ -829,10 +856,8 @@
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${SRCROOT}/Pods/Target Support Files/Pods-BlueWallet/Pods-BlueWallet-resources.sh",
"${PODS_ROOT}/Target Support Files/Pods-BlueWallet/Pods-BlueWallet-resources.sh",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf",
@ -841,6 +866,7 @@
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Brands.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Regular.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Solid.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Fontisto.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Foundation.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Ionicons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf",
@ -850,8 +876,6 @@
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Zocial.ttf",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
);
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AntDesign.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf",
@ -861,6 +885,7 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Brands.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Regular.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Solid.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Fontisto.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Foundation.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Ionicons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialCommunityIcons.ttf",
@ -871,7 +896,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-BlueWallet/Pods-BlueWallet-resources.sh\"\n";
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-BlueWallet/Pods-BlueWallet-resources.sh\"\n";
showEnvVarsInLog = 0;
};
910F5F6DC7F7ADF3C6EE8653 /* [CP] Check Pods Manifest.lock */ = {
@ -913,6 +938,7 @@
files = (
13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */,
13B07FC11A68108700A75B9A /* main.m in Sources */,
32B5A32A2334450100F8D608 /* Bridge.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -939,6 +965,7 @@
files = (
B43D037C225847C500FBAA95 /* Wallet.swift in Sources */,
B43D037A225847C500FBAA95 /* Transaction.swift in Sources */,
32F0A29A2311DBB20095C559 /* ComplicationController.swift in Sources */,
B40D4E602258425500428FCC /* SpecifyInterfaceController.swift in Sources */,
B43D0379225847C500FBAA95 /* WatchDataSource.swift in Sources */,
B40D4E46225841ED00428FCC /* NotificationController.swift in Sources */,
@ -1004,6 +1031,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = A1B6AA2DE9A6E425682F4F3C /* Pods-BlueWalletTests.debug.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
DEVELOPMENT_TEAM = A7W54YZ4WU;
GCC_PREPROCESSOR_DEFINITIONS = (
@ -1054,6 +1082,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 6AB6574CC4ECAAA359683D0F /* Pods-BlueWalletTests.release.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
COPY_PHASE_STRIP = NO;
DEVELOPMENT_TEAM = A7W54YZ4WU;
@ -1102,6 +1131,7 @@
baseConfigurationReference = 9B3A324B70BC8C6D9314FD4F /* Pods-BlueWallet.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
@ -1123,6 +1153,9 @@
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet;
PRODUCT_NAME = BlueWallet;
PROVISIONING_PROFILE_SPECIFIER = "io.bluewallet.bluewallet AppStore";
SWIFT_OBJC_BRIDGING_HEADER = "BlueWallet-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
};
@ -1133,6 +1166,7 @@
baseConfigurationReference = B459EE96941AE09BCB547DC0 /* Pods-BlueWallet.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
@ -1153,6 +1187,8 @@
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet;
PRODUCT_NAME = BlueWallet;
PROVISIONING_PROFILE_SPECIFIER = "io.bluewallet.bluewallet AppStore";
SWIFT_OBJC_BRIDGING_HEADER = "BlueWallet-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
};

9
ios/BlueWallet/AppDelegate.m

@ -16,11 +16,19 @@
#import "RNSentry.h" // This is used for versions of react < 0.40
#endif
#import "WatchBridge.h"
#import <AppCenterReactNativeShared/AppCenterReactNativeShared.h>
#import <AppCenterReactNative.h>
#import <AppCenterReactNativeAnalytics.h>
#import <AppCenterReactNativeCrashes.h>
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[AppCenterReactNative register];
[AppCenterReactNativeAnalytics registerWithInitiallyEnabled:true];
[AppCenterReactNativeCrashes registerWithAutomaticProcessing];
NSURL *jsCodeLocation;
jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
@ -40,7 +48,6 @@
self.session = self.watchBridge.session;
[self.session activateSession];
self.session.delegate = self;
return YES;
}

5
ios/BlueWallet/BlueWallet.entitlements

@ -0,0 +1,5 @@
<?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/>
</plist>

4
ios/BlueWallet/Info.plist

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>4.0.3</string>
<string>4.5.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@ -43,6 +43,8 @@
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>

5
ios/BlueWalletWatch Extension/BlueWalletWatch Extension.entitlements

@ -0,0 +1,5 @@
<?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/>
</plist>

56
ios/BlueWalletWatch Extension/ComplicationController.swift

@ -0,0 +1,56 @@
//
// ComplicationController.swift
// T WatchKit Extension
//
// Created by Marcos Rodriguez on 8/24/19.
// Copyright © 2019 Marcos Rodriguez. All rights reserved.
//
import ClockKit
class ComplicationController: NSObject, CLKComplicationDataSource {
// MARK: - Timeline Configuration
func getSupportedTimeTravelDirections(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimeTravelDirections) -> Void) {
handler([.forward, .backward])
}
func getTimelineStartDate(for complication: CLKComplication, withHandler handler: @escaping (Date?) -> Void) {
handler(nil)
}
func getTimelineEndDate(for complication: CLKComplication, withHandler handler: @escaping (Date?) -> Void) {
handler(nil)
}
func getPrivacyBehavior(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationPrivacyBehavior) -> Void) {
handler(.showOnLockScreen)
}
// MARK: - Timeline Population
func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) {
// Call the handler with the current timeline entry
handler(nil)
}
func getTimelineEntries(for complication: CLKComplication, before date: Date, limit: Int, withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void) {
// Call the handler with the timeline entries prior to the given date
handler(nil)
}
func getTimelineEntries(for complication: CLKComplication, after date: Date, limit: Int, withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void) {
// Call the handler with the timeline entries after to the given date
handler(nil)
}
// MARK: - Placeholder Templates
func getLocalizableSampleTemplate(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) {
// This method will be called once per supported complication, and the results will be cached
handler(nil)
}
}

17
ios/BlueWalletWatch Extension/Info.plist

@ -17,9 +17,24 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>4.0.3</string>
<string>4.5.0</string>
<key>CFBundleVersion</key>
<string>239</string>
<key>CLKComplicationPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).ComplicationController</string>
<key>CLKComplicationSupportedFamilies</key>
<array>
<string>CLKComplicationFamilyCircularSmall</string>
<string>CLKComplicationFamilyExtraLarge</string>
<string>CLKComplicationFamilyGraphicBezel</string>
<string>CLKComplicationFamilyGraphicCircular</string>
<string>CLKComplicationFamilyGraphicCorner</string>
<string>CLKComplicationFamilyModularLarge</string>
<string>CLKComplicationFamilyModularSmall</string>
<string>CLKComplicationFamilyUtilitarianLarge</string>
<string>CLKComplicationFamilyUtilitarianSmall</string>
<string>CLKComplicationFamilyUtilitarianSmallFlat</string>
</array>
<key>LSApplicationCategoryType</key>
<string></string>
<key>NSExtension</key>

6
ios/BlueWalletWatch Extension/Objects/WalletGradient.swift

@ -12,15 +12,15 @@ enum WalletGradient: String {
case SegwitHD = "HDsegwitP2SH"
case Segwit = "segwitP2SH"
case LightningCustodial = "lightningCustodianWallet"
case ACINQStrike = "LightningACINQ"
case SegwitNative = "HDsegwitBech32"
case WatchOnly = "watchOnly"
var imageString: String{
switch self {
case .Segwit:
return "wallet"
case .ACINQStrike:
return "walletACINQ"
case .SegwitNative:
return "walletHDSegwitNative"
case .SegwitHD:
return "walletHD"
case .WatchOnly:

2
ios/BlueWalletWatch Extension/ReceiveInterfaceController.swift

@ -35,7 +35,7 @@ class ReceiveInterfaceController: WKInterfaceController {
DispatchQueue.main.async {
if (!invoice.isEmpty) {
guard let cgImage = EFQRCode.generate(
content: "lightning:\(invoice)") else {
content: "lightning:\(invoice)", inputCorrectionLevel: .h, pointShape: .circle) else {
return
}
let image = UIImage(cgImage: cgImage)

32
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json

@ -0,0 +1,32 @@
{
"images" : [
{
"idiom" : "watch",
"screen-width" : "<=145",
"filename" : "circular38mm@2x.png",
"scale" : "2x"
},
{
"screen-width" : ">161",
"scale" : "2x",
"idiom" : "watch",
"filename" : "circular40mm@2x.png"
},
{
"scale" : "2x",
"idiom" : "watch",
"filename" : "circular42mm@2x.png",
"screen-width" : ">145"
},
{
"filename" : "circular44mm@2x.png",
"scale" : "2x",
"idiom" : "watch",
"screen-width" : ">183"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

BIN
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Circular.imageset/circular38mm@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Circular.imageset/circular40mm@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Circular.imageset/circular42mm@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Circular.imageset/circular44mm@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

48
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Contents.json

@ -0,0 +1,48 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"assets" : [
{
"idiom" : "watch",
"role" : "circular",
"filename" : "Circular.imageset"
},
{
"idiom" : "watch",
"filename" : "Modular.imageset",
"role" : "modular"
},
{
"idiom" : "watch",
"filename" : "Utilitarian.imageset",
"role" : "utilitarian"
},
{
"idiom" : "watch",
"role" : "extra-large",
"filename" : "Extra Large.imageset"
},
{
"role" : "graphic-corner",
"idiom" : "watch",
"filename" : "Graphic Corner.imageset"
},
{
"filename" : "Graphic Circular.imageset",
"role" : "graphic-circular",
"idiom" : "watch"
},
{
"idiom" : "watch",
"filename" : "Graphic Bezel.imageset",
"role" : "graphic-bezel"
},
{
"idiom" : "watch",
"role" : "graphic-large-rectangular",
"filename" : "Graphic Large Rectangular.imageset"
}
]
}

32
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json

@ -0,0 +1,32 @@
{
"images" : [
{
"filename" : "extra-large38mm@2x.png",
"screen-width" : "<=145",
"idiom" : "watch",
"scale" : "2x"
},
{
"screen-width" : ">161",
"filename" : "extra-large40mm@2x.png",
"idiom" : "watch",
"scale" : "2x"
},
{
"screen-width" : ">145",
"idiom" : "watch",
"filename" : "extra-large42mm@2x.png",
"scale" : "2x"
},
{
"idiom" : "watch",
"scale" : "2x",
"screen-width" : ">183",
"filename" : "extra-large44mm@2x.png"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

BIN
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Extra Large.imageset/extra-large38mm@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Extra Large.imageset/extra-large40mm@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Extra Large.imageset/extra-large42mm@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Extra Large.imageset/extra-large44mm@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

30
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json

@ -0,0 +1,30 @@
{
"images" : [
{
"idiom" : "watch",
"scale" : "2x",
"screen-width" : "<=145"
},
{
"idiom" : "watch",
"filename" : "graphic-bezel40mm@2x.png",
"screen-width" : ">161",
"scale" : "2x"
},
{
"idiom" : "watch",
"scale" : "2x",
"screen-width" : ">145"
},
{
"idiom" : "watch",
"filename" : "graphic-bezel44mm@2x.png",
"screen-width" : ">183",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

BIN
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/graphic-bezel40mm@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/graphic-bezel44mm@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

30
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json

@ -0,0 +1,30 @@
{
"images" : [
{
"idiom" : "watch",
"scale" : "2x",
"screen-width" : "<=145"
},
{
"idiom" : "watch",
"filename" : "graphic-circular40mm@2x.png",
"screen-width" : ">161",
"scale" : "2x"
},
{
"idiom" : "watch",
"scale" : "2x",
"screen-width" : ">145"
},
{
"idiom" : "watch",
"filename" : "graphic-circular44mm@2x.png",
"screen-width" : ">183",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

BIN
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/graphic-circular40mm@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/graphic-circular44mm@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

30
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json

@ -0,0 +1,30 @@
{
"images" : [
{
"idiom" : "watch",
"scale" : "2x",
"screen-width" : "<=145"
},
{
"idiom" : "watch",
"filename" : "graphic-corner40mm@2x.png",
"screen-width" : ">161",
"scale" : "2x"
},
{
"idiom" : "watch",
"scale" : "2x",
"screen-width" : ">145"
},
{
"idiom" : "watch",
"filename" : "graphic-corner44mm@2x.png",
"screen-width" : ">183",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

BIN
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/graphic-corner40mm@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/graphic-corner44mm@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

28
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json

@ -0,0 +1,28 @@
{
"images" : [
{
"idiom" : "watch",
"scale" : "2x",
"screen-width" : "<=145"
},
{
"idiom" : "watch",
"scale" : "2x",
"screen-width" : ">161"
},
{
"idiom" : "watch",
"scale" : "2x",
"screen-width" : ">145"
},
{
"idiom" : "watch",
"scale" : "2x",
"screen-width" : ">183"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

32
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json

@ -0,0 +1,32 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"images" : [
{
"screen-width" : "<=145",
"scale" : "2x",
"idiom" : "watch",
"filename" : "modular38mm@2x.png"
},
{
"screen-width" : ">161",
"scale" : "2x",
"filename" : "modular40mm@2x.png",
"idiom" : "watch"
},
{
"scale" : "2x",
"idiom" : "watch",
"filename" : "modular42mm@2x.png",
"screen-width" : ">145"
},
{
"filename" : "modular44mm@2x.png",
"screen-width" : ">183",
"idiom" : "watch",
"scale" : "2x"
}
]
}

BIN
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Modular.imageset/modular38mm@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Modular.imageset/modular40mm@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Modular.imageset/modular42mm@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Modular.imageset/modular44mm@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

32
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json

@ -0,0 +1,32 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"images" : [
{
"scale" : "2x",
"filename" : "utility38mm@2x.png",
"screen-width" : "<=145",
"idiom" : "watch"
},
{
"idiom" : "watch",
"scale" : "2x",
"screen-width" : ">161",
"filename" : "utility40mm@2x.png"
},
{
"scale" : "2x",
"idiom" : "watch",
"filename" : "utility42mm@2x.png",
"screen-width" : ">145"
},
{
"idiom" : "watch",
"screen-width" : ">183",
"filename" : "utility44mm@2x.png",
"scale" : "2x"
}
]
}

BIN
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/utility38mm@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/utility40mm@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/utility42mm@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/utility44mm@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

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

Loading…
Cancel
Save