Merge branch 'master' of https://github.com/BlueWallet/BlueWallet into pr/461
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
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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,13 +2023,139 @@ 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class BlueBitcoinAmount extends Component {
|
||||
static propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
async function getTransactionsFullByAddress(address) {
|
||||
module.exports.ping = async function() {
|
||||
try {
|
||||
await mainClient.server_ping();
|
||||
} catch (_) {
|
||||
mainConnected = false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
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); */
|
||||
|
|
|
@ -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
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
bluewallet at bluewallet dot io
|
|
@ -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 = {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
vendored
|
@ -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
Normal file
|
@ -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>
|
|
@ -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
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"app_secret": "7a010505-cccc-4e40-aa6b-fbbe0624c8d9"
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
task wrapper(type: Wrapper) {
|
||||
gradleVersion = '4.7'
|
||||
distributionUrl = distributionUrl.replace("bin", "all")
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
./gradlew assembleRelease
|
|
@ -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
vendored
|
@ -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
vendored
|
@ -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
vendored
|
@ -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
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
keystore(
|
||||
name = "debug",
|
||||
properties = "debug.keystore.properties",
|
||||
store = "debug.keystore",
|
||||
visibility = [
|
||||
"PUBLIC",
|
||||
],
|
||||
)
|
|
@ -1,4 +0,0 @@
|
|||
key.store=debug.keystore
|
||||
key.alias=androiddebugkey
|
||||
key.store.password=android
|
||||
key.alias.password=android
|
|
@ -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
|
|
@ -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 +0,0 @@
|
|||
Initial Android release
|
|
@ -1 +0,0 @@
|
|||
Bug fixes and performance improvements
|
|
@ -1 +0,0 @@
|
|||
Bug fixes and performance improvements
|
|
@ -1 +0,0 @@
|
|||
Bug fixes and performance improvements
|
|
@ -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.
|
Before Width: | Height: | Size: 87 KiB |
Before Width: | Height: | Size: 4.1 KiB |
|
@ -1 +0,0 @@
|
|||
A Bitcoin & Lightning wallet for android. Easy to use and secure
|
|
@ -1 +0,0 @@
|
|||
BlueWallet - Bitcoin & Lightning Wallet
|
|
@ -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
|
|
@ -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 +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
|
|
@ -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
|
||||
|
|
|
@ -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
Normal file
|
@ -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 };
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,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
Normal file
BIN
img/close-white.png
Normal file
After Width: | Height: | Size: 548 B |
BIN
img/close-white@2x.png
Normal file
After Width: | Height: | Size: 669 B |
BIN
img/close-white@3x.png
Normal file
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
Normal file
|
@ -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
Normal file
|
@ -0,0 +1,4 @@
|
|||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
|
@ -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";
|
||||
};
|
||||
|
|
|
@ -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
Normal file
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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
Normal file
|
@ -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,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>
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
vendored
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.4 KiB |
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 7.5 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 12 KiB |
|
@ -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"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.9 KiB |
|
@ -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"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.9 KiB |
|
@ -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"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.6 KiB |
|
@ -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
vendored
Normal file
|
@ -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
vendored
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Modular.imageset/modular40mm@2x.png
vendored
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Modular.imageset/modular42mm@2x.png
vendored
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
ios/BlueWalletWatch/Assets.xcassets/Complication.complicationset/Modular.imageset/modular44mm@2x.png
vendored
Normal file
After Width: | Height: | Size: 2.4 KiB |
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 2 KiB |