Merge branch 'master' into ops

This commit is contained in:
marcosrdz 2020-10-08 18:38:29 -04:00
commit aa8234b1d6
118 changed files with 14305 additions and 6735 deletions

View file

@ -71,5 +71,5 @@ untyped-import
untyped-type-import
[version]
^0.113.0
^0.122.0

View file

@ -89,15 +89,3 @@ script:
- npm run e2e:release-test || npm run e2e:release-test
after_failure: ./tests/e2e/upload-artifacts.sh
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -r -f node_modules/
- curl "${GRAVIS}.clean_gradle_cache.sh" --output ~/.clean_gradle_cache.sh
- bash ~/.clean_gradle_cache.sh > /dev/null
cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/
- node_modules/

View file

@ -1,33 +1,35 @@
/* eslint react/prop-types: "off", react-native/no-inline-styles: "off" */
/* global alert */
import React, { Component, useState } from 'react';
import Ionicons from 'react-native-vector-icons/Ionicons';
import React, { Component, useState, useMemo, useCallback } from 'react';
import PropTypes from 'prop-types';
import { Icon, Input, Text, Header, ListItem } from 'react-native-elements';
import { Icon, Input, Text, Header, ListItem, Avatar } from 'react-native-elements';
import {
ActivityIndicator,
Alert,
Animated,
Dimensions,
FlatList,
Image,
InputAccessoryView,
Keyboard,
KeyboardAvoidingView,
PixelRatio,
Platform,
PlatformColor,
SafeAreaView,
StyleSheet,
Switch,
TextInput,
TouchableOpacity,
TouchableWithoutFeedback,
Animated,
Alert,
ActivityIndicator,
View,
KeyboardAvoidingView,
UIManager,
StyleSheet,
Dimensions,
Image,
Keyboard,
SafeAreaView,
InputAccessoryView,
Platform,
FlatList,
TextInput,
PixelRatio,
useWindowDimensions,
View,
} from 'react-native';
import Clipboard from '@react-native-community/clipboard';
import LinearGradient from 'react-native-linear-gradient';
import ActionSheet from './screen/ActionSheet';
import { LightningCustodianWallet, PlaceholderWallet } from './class';
import { LightningCustodianWallet, MultisigHDWallet, PlaceholderWallet } from './class';
import Carousel from 'react-native-snap-carousel';
import { BitcoinUnit } from './models/bitcoinUnits';
import * as NavigationService from './NavigationService';
@ -60,57 +62,25 @@ if (aspectRatio > 1.6) {
} else {
isIpad = true;
}
// eslint-disable-next-line no-unused-expressions
Platform.OS === 'android' ? (ActivityIndicator.defaultProps.color = PlatformColor('?attr/colorControlActivated')) : null;
export class BlueButton extends Component {
render() {
let backgroundColor = this.props.backgroundColor ? this.props.backgroundColor : BlueCurrentTheme.colors.mainColor;
let fontColor = BlueCurrentTheme.colors.buttonTextColor;
if (this.props.disabled === true) {
backgroundColor = BlueCurrentTheme.colors.buttonDisabledBackgroundColor;
fontColor = BlueCurrentTheme.colors.buttonDisabledTextColor;
}
let buttonWidth = this.props.width ? this.props.width : width / 1.5;
if ('noMinWidth' in this.props) {
buttonWidth = 0;
}
return (
<TouchableOpacity
style={{
flex: 1,
borderWidth: 0.7,
borderColor: 'transparent',
backgroundColor: backgroundColor,
minHeight: 45,
height: 45,
maxHeight: 45,
borderRadius: 25,
minWidth: buttonWidth,
justifyContent: 'center',
alignItems: 'center',
}}
{...this.props}
>
<View style={{ flexDirection: 'row', justifyContent: 'center', alignItems: 'center' }}>
{this.props.icon && <Icon name={this.props.icon.name} type={this.props.icon.type} color={this.props.icon.color} />}
{this.props.title && <Text style={{ marginHorizontal: 8, fontSize: 16, color: fontColor }}>{this.props.title}</Text>}
</View>
</TouchableOpacity>
);
}
}
export const BlueButtonHook = props => {
export const BlueButton = props => {
const { colors } = useTheme();
const { width } = useWindowDimensions();
let backgroundColor = props.backgroundColor ? props.backgroundColor : colors.mainColor;
let fontColor = colors.buttonTextColor;
if (props.disabled === true) {
backgroundColor = colors.buttonDisabledBackgroundColor;
fontColor = colors.buttonDisabledTextColor;
}
let buttonWidth = props.width ? props.width : width / 1.5;
if ('noMinWidth' in props) {
buttonWidth = 0;
}
return (
<TouchableOpacity
style={{
@ -136,6 +106,38 @@ export const BlueButtonHook = props => {
);
};
export const BlueButtonHook = props => {
const { colors } = useTheme();
let backgroundColor = props.backgroundColor ? props.backgroundColor : colors.mainColor;
let fontColor = colors.buttonTextColor;
if (props.disabled === true) {
backgroundColor = colors.buttonDisabledBackgroundColor;
fontColor = colors.buttonDisabledTextColor;
}
return (
<TouchableOpacity
style={{
flex: 1,
borderWidth: 0.7,
borderColor: 'transparent',
backgroundColor: backgroundColor,
minHeight: 45,
height: 45,
maxHeight: 45,
borderRadius: 25,
justifyContent: 'center',
alignItems: 'center',
}}
{...props}
>
<View style={{ flexDirection: 'row', justifyContent: 'center', alignItems: 'center' }}>
{props.icon && <Icon name={props.icon.name} type={props.icon.type} color={props.icon.color} />}
{props.title && <Text style={{ marginHorizontal: 8, fontSize: 16, color: fontColor }}>{props.title}</Text>}
</View>
</TouchableOpacity>
);
};
export class SecondButton extends Component {
render() {
let backgroundColor = this.props.backgroundColor ? this.props.backgroundColor : BlueCurrentTheme.colors.buttonBlueBackgroundColor;
@ -144,10 +146,10 @@ export class SecondButton extends Component {
backgroundColor = BlueCurrentTheme.colors.buttonDisabledBackgroundColor;
fontColor = BlueCurrentTheme.colors.buttonDisabledTextColor;
}
let buttonWidth = this.props.width ? this.props.width : width / 1.5;
if ('noMinWidth' in this.props) {
buttonWidth = 0;
}
// let buttonWidth = this.props.width ? this.props.width : width / 1.5;
// if ('noMinWidth' in this.props) {
// buttonWidth = 0;
// }
return (
<TouchableOpacity
style={{
@ -159,7 +161,6 @@ export class SecondButton extends Component {
height: 45,
maxHeight: 45,
borderRadius: 25,
minWidth: buttonWidth,
justifyContent: 'center',
alignItems: 'center',
}}
@ -190,7 +191,7 @@ export const BitcoinButton = props => {
flex: 1,
}}
>
<View style={{ marginTop: 16, marginLeft: 16, marginBottom: 16 }}>
<View style={{ margin: 16 }}>
<Text style={{ color: colors.hdborderColor, fontWeight: 'bold' }}>{loc.wallets.add_bitcoin}</Text>
</View>
<Image
@ -218,7 +219,7 @@ export const LightningButton = props => {
flex: 1,
}}
>
<View style={{ marginTop: 16, marginLeft: 16, marginBottom: 16 }}>
<View style={{ margin: 16 }}>
<Text style={{ color: colors.lnborderColor, fontWeight: 'bold' }}>{loc.wallets.add_lightning}</Text>
</View>
<Image
@ -339,9 +340,16 @@ export class BlueWalletNavigationHeader extends Component {
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')
}
source={(() => {
switch (this.state.wallet.type) {
case LightningCustodianWallet.type:
return require('./img/lnd-shape.png');
case MultisigHDWallet.type:
return require('./img/vault-shape.png');
default:
return require('./img/btc-shape.png');
}
})()}
style={{
width: 99,
height: 94,
@ -406,9 +414,9 @@ export class BlueWalletNavigationHeader extends Component {
marginBottom: 10,
backgroundColor: 'rgba(255,255,255,0.2)',
borderRadius: 9,
minWidth: 119,
minHeight: 39,
width: 119,
alignSelf: 'flex-start',
paddingHorizontal: 12,
height: 39,
justifyContent: 'center',
alignItems: 'center',
@ -699,60 +707,56 @@ export const BlueTextCenteredHooks = props => {
return <Text {...props} style={{ color: colors.foregroundColor, textAlign: 'center' }} />;
};
export const BlueListItem = React.memo(props => (
<ListItem
testID={props.testID}
bottomDivider
containerStyle={{
backgroundColor: 'transparent',
borderBottomColor: BlueCurrentTheme.colors.lightBorder,
paddingTop: 16,
paddingBottom: 16,
}}
titleStyle={{
color: props.disabled ? BlueCurrentTheme.colors.buttonDisabledTextColor : BlueCurrentTheme.colors.foregroundColor,
fontSize: 16,
fontWeight: '500',
}}
subtitleStyle={{ flexWrap: 'wrap', color: BlueCurrentTheme.colors.alternativeTextColor, fontWeight: '400', fontSize: 14 }}
subtitleNumberOfLines={1}
titleNumberOfLines={0}
Component={TouchableOpacity}
{...props}
/>
));
export const BlueListItemHooks = props => {
export const BlueListItem = React.memo(props => {
const { colors } = useTheme();
return (
<ListItem
testID={props.testID}
containerStyle={props.containerStyle ?? { backgroundColor: 'transparent' }}
Component={props.Component ?? TouchableOpacity}
bottomDivider
containerStyle={{
backgroundColor: 'transparent',
borderBottomColor: colors.lightBorder,
paddingTop: 16,
paddingBottom: 16,
}}
titleStyle={{
color: props.disabled ? colors.buttonDisabledTextColor : colors.foregroundColor,
fontSize: 16,
fontWeight: '500',
}}
rightTitleStyle={{ flexWrap: 'wrap', color: colors.alternativeTextColor, fontWeight: '400', fontSize: 14 }}
subtitleStyle={{ flexWrap: 'wrap', color: colors.alternativeTextColor, fontWeight: '400', fontSize: 14 }}
subtitleNumberOfLines={1}
titleNumberOfLines={0}
Component={TouchableOpacity}
{...props}
/>
testID={props.testID}
onPress={props.onPress}
>
{props.leftAvatar && <Avatar>{props.leftAvatar}</Avatar>}
{props.leftIcon && <Avatar icon={props.leftIcon} />}
<ListItem.Content>
<ListItem.Title
style={{
color: props.disabled ? colors.buttonDisabledTextColor : colors.foregroundColor,
fontSize: 16,
fontWeight: '500',
}}
numberOfLines={0}
>
{props.title}
</ListItem.Title>
{props.subtitle && (
<ListItem.Subtitle
numberOfLines={1}
style={{ flexWrap: 'wrap', color: colors.alternativeTextColor, fontWeight: '400', fontSize: 14 }}
>
{props.subtitle}
</ListItem.Subtitle>
)}
</ListItem.Content>
<ListItem.Content right>
{props.rightTitle && (
<ListItem.Title style={props.rightTitleStyle} numberOfLines={0} right>
{props.rightTitle}
</ListItem.Title>
)}
</ListItem.Content>
{props.chevron && <ListItem.Chevron />}
{props.rightIcon && <Avatar icon={props.rightIcon} />}
{props.switch && <Switch {...props.switch} />}
</ListItem>
);
};
});
export const BlueFormLabel = props => {
const { colors } = useTheme();
return <Text {...props} style={{ color: colors.foregroundColor, fontWeight: '400', marginLeft: 20 }} />;
return <Text {...props} style={{ color: colors.foregroundColor, fontWeight: '400', marginHorizontal: 20 }} />;
};
export class BlueFormInput extends Component {
@ -902,95 +906,37 @@ export const BlueHeaderDefaultSubHooks = props => {
);
};
export const BlueHeaderDefaultMainHooks = props => {
export const BlueHeaderDefaultMain = props => {
const { colors } = useTheme();
const { isDrawerList } = props;
return (
<Header
{...props}
leftComponent={{
text: props.leftText,
style: {
fontWeight: 'bold',
fontSize: 34,
color: colors.foregroundColor,
paddingHorizontal: 4,
},
}}
leftContainerStyle={{
minWidth: '70%',
height: 80,
placement="left"
containerStyle={{
borderTopColor: isDrawerList ? colors.elevated : colors.background,
borderBottomColor: isDrawerList ? colors.elevated : colors.background,
maxHeight: 44,
height: 44,
paddingTop: 0,
marginBottom: 8,
}}
bottomDivider={false}
topDivider={false}
containerStyle={{
height: 44,
flexDirection: 'row',
backgroundColor: colors.elevatated,
borderTopColor: colors.elevatated,
borderBottomColor: colors.elevatated,
borderBottomWidth: 0,
}}
rightComponent={
props.onNewWalletPress && (
<TouchableOpacity
onPress={props.onNewWalletPress}
style={{
height: 100,
}}
>
<BluePlusIcon />
</TouchableOpacity>
)
}
backgroundColor={isDrawerList ? colors.elevated : colors.background}
rightComponent={<BluePlusIcon onPress={props.onNewWalletPress} Component={TouchableOpacity} />}
/>
);
};
export class BlueHeaderDefaultMain extends Component {
render() {
return (
<SafeAreaView style={{ paddingVertical: 8, paddingHorizontal: 4, backgroundColor: BlueCurrentTheme.colors.background }}>
<Header
{...this.props}
leftComponent={{
text: this.props.leftText,
style: {
fontWeight: 'bold',
fontSize: 34,
color: BlueCurrentTheme.colors.foregroundColor,
},
}}
leftContainerStyle={{
minWidth: '70%',
height: 80,
}}
bottomDivider={false}
topDivider={false}
containerStyle={{
height: 44,
flexDirection: 'row',
backgroundColor: BlueCurrentTheme.colors.background,
borderTopColor: BlueCurrentTheme.colors.background,
borderBottomColor: BlueCurrentTheme.colors.background,
borderBottomWidth: 0,
}}
rightComponent={
this.props.onNewWalletPress && (
<TouchableOpacity
onPress={this.props.onNewWalletPress}
style={{
height: 100,
}}
>
<BluePlusIcon />
</TouchableOpacity>
)
}
/>
</SafeAreaView>
);
}
}
export class BlueSpacing extends Component {
render() {
return <View {...this.props} style={{ height: 60 }} />;
@ -1268,23 +1214,12 @@ export const BluePlusIcon = props => {
},
});
return (
<View {...props} style={stylesBlueIcon.container}>
<View style={stylesBlueIcon.box1}>
<View style={[stylesBlueIcon.ball, stylesBlueIconHooks.ball]}>
<Ionicons
{...props}
name="ios-add"
size={26}
style={{
color: colors.foregroundColor,
backgroundColor: 'transparent',
left: 8,
top: 1,
}}
/>
</View>
</View>
</View>
<Avatar
rounded
containerStyle={[stylesBlueIcon.ball, stylesBlueIconHooks.ball]}
icon={{ name: 'add', size: 22, type: 'ionicons', color: colors.foregroundColor }}
{...props}
/>
);
};
@ -1632,7 +1567,7 @@ export const NewWalletPanel = props => {
minHeight: Platform.OS === 'ios' ? 164 : 181,
justifyContent: 'center',
alignItems: 'flex-start',
backgroundColor: WalletGradient.createWallet,
backgroundColor: WalletGradient.createWallet(),
}}
>
<Text
@ -1673,15 +1608,27 @@ export const NewWalletPanel = props => {
export const BlueTransactionListItem = React.memo(({ item, itemPriceUnit = BitcoinUnit.BTC, timeElapsed }) => {
const [subtitleNumberOfLines, setSubtitleNumberOfLines] = useState(1);
const { colors } = useTheme();
const containerStyle = useMemo(
() => ({
backgroundColor: 'transparent',
borderBottomColor: colors.lightBorder,
paddingTop: 16,
paddingBottom: 16,
}),
[colors.lightBorder],
);
const txMemo = () => {
if (BlueApp.tx_metadata[item.hash] && BlueApp.tx_metadata[item.hash].memo) {
return BlueApp.tx_metadata[item.hash].memo;
}
return '';
};
const title = useMemo(() => transactionTimeToReadable(item.received), [item.received]);
const txMemo = BlueApp.tx_metadata[item.hash]?.memo ?? '';
const subtitle = useMemo(() => {
let sub = item.confirmations < 7 ? loc.formatString(loc.transactions.list_conf, { number: item.confirmations }) : '';
if (sub !== '') sub += ' ';
sub += txMemo;
if (item.memo) sub += item.memo;
return sub || null;
}, [txMemo, item.confirmations, item.memo]);
const rowTitle = () => {
const rowTitle = useMemo(() => {
if (item.type === 'user_invoice' || item.type === 'payment_request') {
if (isNaN(item.value)) {
item.value = '0';
@ -1702,9 +1649,9 @@ export const BlueTransactionListItem = React.memo(({ item, itemPriceUnit = Bitco
} else {
return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
}
};
}, [item, itemPriceUnit]);
const rowTitleStyle = () => {
const rowTitleStyle = useMemo(() => {
let color = colors.successColor;
if (item.type === 'user_invoice' || item.type === 'payment_request') {
@ -1726,15 +1673,15 @@ export const BlueTransactionListItem = React.memo(({ item, itemPriceUnit = Bitco
}
return {
fontWeight: '600',
color,
fontSize: 14,
color: color,
fontWeight: '600',
textAlign: 'right',
width: 96,
};
};
}, [item, colors.foregroundColor, colors.successColor]);
const avatar = () => {
const avatar = useMemo(() => {
// is it lightning refill tx?
if (item.category === 'receive' && item.confirmations < 3) {
return (
@ -1800,13 +1747,9 @@ export const BlueTransactionListItem = React.memo(({ item, itemPriceUnit = Bitco
</View>
);
}
};
}, [item]);
const subtitle = () => {
return (item.confirmations < 7 ? loc.transactions.list_conf + ': ' + item.confirmations + ' ' : '') + txMemo() + (item.memo || '');
};
const onPress = async () => {
const onPress = useCallback(async () => {
if (item.hash) {
NavigationService.navigate('TransactionStatus', { hash: item.hash });
} else if (item.type === 'user_invoice' || item.type === 'payment_request' || item.type === 'paid_invoice') {
@ -1829,7 +1772,7 @@ export const BlueTransactionListItem = React.memo(({ item, itemPriceUnit = Bitco
NavigationService.navigate('ScanLndInvoiceRoot', {
screen: 'LnurlPaySuccess',
params: {
paymentHash: paymentHash,
paymentHash,
justPaid: false,
fromWalletID: lightningWallet[0].getID(),
},
@ -1844,28 +1787,31 @@ export const BlueTransactionListItem = React.memo(({ item, itemPriceUnit = Bitco
});
}
}
};
}, [item]);
const onLongPress = () => {
const onLongPress = useCallback(() => {
if (subtitleNumberOfLines === 1) {
setSubtitleNumberOfLines(0);
}
};
}, [subtitleNumberOfLines]);
const subtitleProps = useMemo(() => ({ numberOfLines: subtitleNumberOfLines }), [subtitleNumberOfLines]);
return (
<View style={{ marginHorizontal: 4 }}>
<BlueListItem
leftAvatar={avatar()}
title={transactionTimeToReadable(item.received)}
leftAvatar={avatar}
title={title}
titleNumberOfLines={subtitleNumberOfLines}
subtitle={subtitle()}
subtitleProps={{ numberOfLines: subtitleNumberOfLines }}
subtitle={subtitle}
subtitleProps={subtitleProps}
onPress={onPress}
onLongPress={onLongPress}
chevron={false}
Component={TouchableOpacity}
rightTitle={rowTitle()}
rightTitleStyle={rowTitleStyle()}
rightTitle={rowTitle}
rightTitleStyle={rowTitleStyle}
containerStyle={containerStyle}
/>
</View>
);
@ -2005,7 +1951,16 @@ const WalletCarouselItem = ({ item, index, onPress, handleLongPress, isSelectedW
}}
>
<Image
source={(LightningCustodianWallet.type === item.type && require('./img/lnd-shape.png')) || require('./img/btc-shape.png')}
source={(() => {
switch (item.type) {
case LightningCustodianWallet.type:
return require('./img/lnd-shape.png');
case MultisigHDWallet.type:
return require('./img/vault-shape.png');
default:
return require('./img/btc-shape.png');
}
})()}
style={{
width: 99,
height: 94,
@ -2093,7 +2048,8 @@ export class WalletsCarousel extends Component {
};
snapToItem = item => {
this.walletsCarousel.current.snapToItem(item);
// eslint-disable-next-line no-unused-expressions
this.walletsCarousel?.current?.snapToItem(item);
};
onLayout = () => {
@ -2250,6 +2206,7 @@ export class BlueAddressInput extends Component {
{...this.props}
/>
<TouchableOpacity
testID="BlueAddressInputScanQrButton"
disabled={this.props.isLoading}
onPress={() => {
Keyboard.dismiss();
@ -2745,6 +2702,7 @@ const tabsStyles = StyleSheet.create({
borderBottomWidth: 1,
},
tabRoot: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
borderColor: 'white',
@ -2753,7 +2711,7 @@ const tabsStyles = StyleSheet.create({
});
export const BlueTabs = ({ active, onSwitch, tabs }) => (
<View style={tabsStyles.root}>
<View style={[tabsStyles.root, isIpad && { marginBottom: 30 }]}>
{tabs.map((Tab, i) => (
<TouchableOpacity
key={i}
@ -2764,7 +2722,6 @@ export const BlueTabs = ({ active, onSwitch, tabs }) => (
borderColor: BlueCurrentTheme.colors.buttonAlternativeTextColor,
borderBottomWidth: 2,
},
{ width: width / tabs.length },
]}
>
<Tab active={active === i} />

View file

@ -27,6 +27,7 @@ import PleaseBackupLNDHub from './screen/wallets/pleaseBackupLNDHub';
import ImportWallet from './screen/wallets/import';
import WalletDetails from './screen/wallets/details';
import WalletExport from './screen/wallets/export';
import ExportMultisigCoordinationSetup from './screen/wallets/exportMultisigCoordinationSetup';
import WalletXpub from './screen/wallets/xpub';
import BuyBitcoin from './screen/wallets/buyBitcoin';
import HodlHodl from './screen/wallets/hodlHodl';
@ -53,6 +54,7 @@ import ScanQRCode from './screen/send/ScanQRCode';
import SendCreate from './screen/send/create';
import Confirm from './screen/send/confirm';
import PsbtWithHardwareWallet from './screen/send/psbtWithHardwareWallet';
import PsbtMultisig from './screen/send/psbtMultisig';
import Success from './screen/send/success';
import Broadcast from './screen/send/broadcast';
@ -79,7 +81,11 @@ const defaultScreenOptions =
...TransitionPresets.ModalPresentationIOS,
gestureResponseDistance: { vertical: Dimensions.get('window').height, horizontal: 50 },
})
: undefined;
: {
gestureEnabled: true,
cardOverlayEnabled: true,
...TransitionPresets.ScaleFromCenterAndroid,
};
const defaultStackScreenOptions =
Platform.OS === 'ios'
? {
@ -88,11 +94,15 @@ const defaultStackScreenOptions =
cardStyle: { backgroundColor: '#FFFFFF' },
headerStatusBarHeight: 10,
}
: undefined;
: {
gestureEnabled: true,
cardOverlayEnabled: true,
...TransitionPresets.ScaleFromCenterAndroid,
};
const WalletsStack = createStackNavigator();
const WalletsRoot = () => (
<WalletsStack.Navigator>
<WalletsStack.Navigator {...(Platform.OS === 'android' ? { screenOptions: defaultScreenOptions } : null)}>
<WalletsStack.Screen name="WalletsList" component={WalletsList} options={WalletsList.navigationOptions} />
<WalletsStack.Screen name="WalletTransactions" component={WalletTransactions} options={WalletTransactions.navigationOptions} />
<WalletsStack.Screen name="WalletDetails" component={WalletDetails} options={WalletDetails.navigationOptions} />
@ -167,17 +177,8 @@ const SendDetailsRoot = () => (
component={PsbtWithHardwareWallet}
options={PsbtWithHardwareWallet.navigationOptions}
/>
<SendDetailsStack.Screen
name="CreateTransaction"
component={SendCreate}
options={{
headerStyle: {
backgroundColor: '#FFFFFF',
borderBottomWidth: 0,
},
headerTintColor: '#0c2550',
}}
/>
<SendDetailsStack.Screen name="CreateTransaction" component={SendCreate} options={SendCreate.navigationOptions} />
<SendDetailsStack.Screen name="PsbtMultisig" component={PsbtMultisig} options={PsbtMultisig.navigationOptions} />
<SendDetailsStack.Screen name="Success" component={Success} options={Success.navigationOptions} />
<SendDetailsStack.Screen name="SelectWallet" component={SelectWallet} options={SelectWallet.navigationOptions} />
</SendDetailsStack.Navigator>
@ -263,11 +264,12 @@ function DrawerRoot() {
const dimensions = useWindowDimensions();
const isLargeScreen = Platform.OS === 'android' ? isTablet() : dimensions.width >= Dimensions.get('screen').width / 3 && isTablet();
const drawerStyle = { width: '0%' };
return (
<Drawer.Navigator
drawerStyle={isLargeScreen ? null : drawerStyle}
drawerType={isLargeScreen ? 'permanent' : null}
drawerContent={props => <DrawerList {...props} />}
drawerContent={props => (isLargeScreen ? <DrawerList {...props} /> : null)}
>
<Drawer.Screen name="Navigation" component={Navigation} options={{ headerShown: false, gestureEnabled: false }} />
</Drawer.Navigator>
@ -304,6 +306,11 @@ const Navigation = () => (
{/* screens */}
<RootStack.Screen name="WalletExport" component={WalletExport} options={WalletExport.navigationOptions} />
<RootStack.Screen
name="ExportMultisigCoordinationSetup"
component={ExportMultisigCoordinationSetup}
options={ExportMultisigCoordinationSetup.navigationOptions}
/>
<RootStack.Screen name="WalletXpub" component={WalletXpub} options={WalletXpub.navigationOptions} />
<RootStack.Screen name="BuyBitcoin" component={BuyBitcoin} options={BuyBitcoin.navigationOptions} />
<RootStack.Screen name="Marketplace" component={Marketplace} options={Marketplace.navigationOptions} />
@ -315,7 +322,7 @@ const Navigation = () => (
name="ScanQRCodeRoot"
component={ScanQRCodeRoot}
options={{
...TransitionPresets.ModalTransition,
...(Platform.OS === 'ios' ? TransitionPresets.ModalTransition : TransitionPresets.ScaleFromCenterAndroid),
headerShown: false,
}}
/>

View file

@ -69,7 +69,7 @@ The above command will build the app and install it. Once you launch the app it
* To run on iOS:
```
npx podinstall
npx pod-install
npm start
```

View file

@ -127,12 +127,16 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
lintOptions {
abortOnError false
}
defaultConfig {
applicationId "io.bluewallet.bluewallet"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "5.6.0"
versionName "5.6.2"
multiDexEnabled true
missingDimensionStrategy 'react-native-camera', 'general'
testBuildType System.getProperty('testBuildType', 'debug') // This will later be used to control the test apk build type
@ -155,13 +159,6 @@ android {
}
}
packagingOptions {
pickFirst "lib/armeabi-v7a/libc++_shared.so"
pickFirst "lib/arm64-v8a/libc++_shared.so"
pickFirst "lib/x86/libc++_shared.so"
pickFirst "lib/x86_64/libc++_shared.so"
}
// applicationVariants are e.g. debug, release
applicationVariants.all { variant ->
variant.outputs.each { output ->

View file

@ -2,11 +2,11 @@
buildscript {
ext {
buildToolsVersion = "28.0.3"
minSdkVersion = 18
compileSdkVersion = 28
targetSdkVersion = 28
supportLibVersion = "28.0.0"
buildToolsVersion = "29.0.2"
compileSdkVersion = 29
targetSdkVersion = 29
googlePlayServicesVersion = "16.+"
firebaseVersion = "17.3.4"
firebaseMessagingVersion = "20.2.1"

View file

@ -31,4 +31,4 @@ org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx4g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# Version of flipper SDK to use with React Native
FLIPPER_VERSION=0.33.1
FLIPPER_VERSION=0.54.0

30
android/gradlew vendored
View file

@ -154,19 +154,19 @@ if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
i=`expr $i + 1`
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
@ -175,15 +175,9 @@ save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

View file

@ -521,15 +521,27 @@ module.exports.calculateBlockTime = function (height) {
*/
module.exports.testConnection = async function (host, tcpPort, sslPort) {
const client = new ElectrumClient(sslPort || tcpPort, host, sslPort ? 'tls' : 'tcp');
client.onError = () => {}; // mute
let timeoutId = false;
try {
await client.connect();
const rez = await Promise.race([
new Promise(resolve => {
timeoutId = setTimeout(() => resolve('timeout'), 3000);
}),
client.connect(),
]);
if (rez === 'timeout') return false;
await client.server_version('2.7.11', '1.4');
await client.server_ping();
client.close();
return true;
} catch (_) {
return false;
} finally {
if (timeoutId) clearTimeout(timeoutId);
client.close();
}
return false;
};
module.exports.forceDisconnect = () => {

109
blue_modules/fs.js Normal file
View file

@ -0,0 +1,109 @@
/* global alert */
import { PermissionsAndroid, Platform } from 'react-native';
import RNFS from 'react-native-fs';
import Share from 'react-native-share';
import loc from '../loc';
import { getSystemName } from 'react-native-device-info';
import DocumentPicker from 'react-native-document-picker';
const isDesktop = getSystemName() === 'Mac OS X';
const writeFileAndExport = async function (filename, contents) {
if (Platform.OS === 'ios') {
const filePath = RNFS.TemporaryDirectoryPath + `/${filename}`;
await RNFS.writeFile(filePath, contents);
Share.open({
url: 'file://' + filePath,
saveToFiles: isDesktop,
})
.catch(error => {
console.log(error);
// alert(error.message);
})
.finally(() => {
RNFS.unlink(filePath);
});
} else if (Platform.OS === 'android') {
const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, {
title: loc.send.permission_storage_title,
message: loc.send.permission_storage_message,
buttonNeutral: loc.send.permission_storage_later,
buttonNegative: loc._.cancel,
buttonPositive: loc._.ok,
});
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
console.log('Storage Permission: Granted');
const filePath = RNFS.DownloadDirectoryPath + `/${filename}`;
await RNFS.writeFile(filePath, contents);
alert(loc.formatString(loc._.file_saved, { filePath: filename }));
} else {
console.log('Storage Permission: Denied');
}
}
};
/**
* Opens & reads *.psbt files, and returns base64 psbt. FALSE if something went wrong (wont throw).
*
* @returns {Promise<string|boolean>} Base64 PSBT
*/
const openSignedTransaction = async function () {
try {
const res = await DocumentPicker.pick({
type: Platform.OS === 'ios' ? ['io.bluewallet.psbt', 'io.bluewallt.psbt.txn'] : [DocumentPicker.types.allFiles],
});
return await _readPsbtFileIntoBase64(res.uri);
} catch (err) {
if (!DocumentPicker.isCancel(err)) {
alert(loc.send.details_no_signed_tx);
}
}
return false;
};
const _readPsbtFileIntoBase64 = async function (uri) {
const base64 = await RNFS.readFile(uri, 'base64');
const stringData = Buffer.from(base64, 'base64').toString(); // decode from base64
if (stringData.startsWith('psbt')) {
// file was binary, but outer code expects base64 psbt, so we return base64 we got from rn-fs;
// most likely produced by Electrum-desktop
return base64;
} else {
// file was a text file, having base64 psbt in there. so we basically have double base64encoded string
// thats why we are returning string that was decoded once;
// most likely produced by Coldcard
return stringData;
}
};
const showFilePickerAndReadFile = async function () {
try {
const res = await DocumentPicker.pick({
type:
Platform.OS === 'ios'
? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn', DocumentPicker.types.plainText, 'public.json']
: [DocumentPicker.types.allFiles],
});
let file = false;
if (res.uri.toLowerCase().endsWith('.psbt')) {
// this is either binary file from ElectrumDesktop OR string file with base64 string in there
file = await _readPsbtFileIntoBase64(res.uri);
} else {
file = await RNFS.readFile(res.uri);
}
return { data: file, uri: res.uri };
} catch (err) {
if (!DocumentPicker.isCancel(err)) {
return { data: false, uri: false };
}
}
};
module.exports.writeFileAndExport = writeFileAndExport;
module.exports.openSignedTransaction = openSignedTransaction;
module.exports.showFilePickerAndReadFile = showFilePickerAndReadFile;

View file

@ -14,6 +14,7 @@ import {
LightningCustodianWallet,
HDLegacyElectrumSeedP2PKHWallet,
HDSegwitElectrumSeedP2WPKHWallet,
MultisigHDWallet,
} from './';
import DeviceQuickActions from './quick-actions';
import { AbstractHDElectrumWallet } from './wallets/abstract-hd-electrum-wallet';
@ -297,6 +298,9 @@ export class AppStorage {
case HDSegwitElectrumSeedP2WPKHWallet.type:
unserializedWallet = HDSegwitElectrumSeedP2WPKHWallet.fromJson(key);
break;
case MultisigHDWallet.type:
unserializedWallet = MultisigHDWallet.fromJson(key);
break;
case LightningCustodianWallet.type: {
/** @type {LightningCustodianWallet} */
unserializedWallet = LightningCustodianWallet.fromJson(key);
@ -433,7 +437,7 @@ export class AppStorage {
const realm = await this.getRealm();
for (const key of this.wallets) {
if (typeof key === 'boolean' || key.type === PlaceholderWallet.type) continue;
if (key.prepareForSerialization) key.prepareForSerialization();
key.prepareForSerialization();
const keyCloned = Object.assign({}, key); // stripped-down version of a wallet to save to secure keystore
if (key._hdWalletInstance) keyCloned._hdWalletInstance = Object.assign({}, key._hdWalletInstance);
this.offloadWalletToRealm(realm, key);

View file

@ -59,12 +59,12 @@ export default class Biometric {
static async unlockWithBiometrics() {
const isDeviceBiometricCapable = await Biometric.isDeviceBiometricCapable();
if (isDeviceBiometricCapable) {
try {
const isConfirmed = await FingerprintScanner.authenticate({ description: 'Please confirm your identity.', fallbackEnabled: true });
return isConfirmed;
} catch (_e) {
return false;
}
return new Promise(resolve => {
FingerprintScanner.authenticate({ description: 'Please confirm your identity.', fallbackEnabled: true })
.then(() => resolve(true))
.catch(() => resolve(false))
.finally(() => FingerprintScanner.release());
});
}
return false;
}

View file

@ -40,9 +40,8 @@ class DeeplinkSchemaMatch {
if (event.url.toLowerCase().startsWith('bluewallet:bitcoin:') || event.url.toLowerCase().startsWith('bluewallet:lightning:')) {
event.url = event.url.substring(11);
}
if (DeeplinkSchemaMatch.isPossiblyPSBTFile(event.url)) {
RNFS.readFile(event.url)
if (DeeplinkSchemaMatch.isPossiblySignedPSBTFile(event.url)) {
RNFS.readFile(decodeURI(event.url))
.then(file => {
if (file) {
completionHandler([
@ -203,13 +202,23 @@ class DeeplinkSchemaMatch {
}
static isTXNFile(filePath) {
return filePath.toLowerCase().startsWith('file:') && filePath.toLowerCase().endsWith('.txn');
return (
(filePath.toLowerCase().startsWith('file:') || filePath.toLowerCase().startsWith('content:')) &&
filePath.toLowerCase().endsWith('.txn')
);
}
static isPossiblySignedPSBTFile(filePath) {
return (
(filePath.toLowerCase().startsWith('file:') || filePath.toLowerCase().startsWith('content:')) &&
filePath.toLowerCase().endsWith('-signed.psbt')
);
}
static isPossiblyPSBTFile(filePath) {
return (
(filePath.toLowerCase().startsWith('file:') || filePath.toLowerCase().startsWith('content:')) &&
filePath.toLowerCase().endsWith('-signed.psbt')
filePath.toLowerCase().endsWith('.psbt')
);
}
@ -314,6 +323,31 @@ class DeeplinkSchemaMatch {
static bip21encode() {
return bip21.encode.apply(bip21, arguments);
}
static decodeBitcoinUri(uri) {
let amount = '';
let parsedBitcoinUri = null;
let address = uri || '';
let memo = '';
let payjoinUrl = '';
try {
parsedBitcoinUri = DeeplinkSchemaMatch.bip21decode(uri);
address = 'address' in parsedBitcoinUri ? parsedBitcoinUri.address : address;
if ('options' in parsedBitcoinUri) {
if ('amount' in parsedBitcoinUri.options) {
amount = parsedBitcoinUri.options.amount.toString();
amount = parsedBitcoinUri.options.amount;
}
if ('label' in parsedBitcoinUri.options) {
memo = parsedBitcoinUri.options.label || memo;
}
if ('pj' in parsedBitcoinUri.options) {
payjoinUrl = parsedBitcoinUri.options.pj;
}
}
} catch (_) {}
return { address, amount, memo, payjoinUrl };
}
}
export default DeeplinkSchemaMatch;

View file

@ -214,6 +214,32 @@ export class HDSegwitBech32Transaction {
return { fee, feeRate, targets, changeAmount, utxos, unconfirmedUtxos };
}
/**
* We get _all_ our UTXOs (even spent kek),
* and see if each input in this transaction's UTXO is in there. If its not there - its an unknown
* input, we dont own it (possibly a payjoin transaction), and we cant do RBF
*
* @returns {Promise<boolean>}
*/
async thereAreUnknownInputsInTx() {
if (!this._wallet) throw new Error('Wallet required for this method');
if (!this._txDecoded) await this._fetchTxhexAndDecode();
const spentUtxos = this._wallet.getDerivedUtxoFromOurTransaction(true);
for (const inp of this._txDecoded.ins) {
const txidInUtxo = reverse(inp.hash).toString('hex');
let found = false;
for (const spentU of spentUtxos) {
if (spentU.txid === txidInUtxo && spentU.vout === inp.index) found = true;
}
if (!found) {
return true;
}
}
}
/**
* Checks if all outputs belong to us, that
* means we already canceled this tx and we can only bump fees
@ -224,6 +250,8 @@ export class HDSegwitBech32Transaction {
if (!this._wallet) throw new Error('Wallet required for this method');
if (!this._txDecoded) await this._fetchTxhexAndDecode();
if (await this.thereAreUnknownInputsInTx()) return false;
// if theres at least one output we dont own - we can cancel this transaction!
for (const outp of this._txDecoded.outs) {
if (!this._wallet.weOwnAddress(SegwitBech32Wallet.scriptPubKeyToAddress(outp.script))) return true;
@ -232,6 +260,15 @@ export class HDSegwitBech32Transaction {
return false;
}
async canBumpTx() {
if (!this._wallet) throw new Error('Wallet required for this method');
if (!this._txDecoded) await this._fetchTxhexAndDecode();
if (await this.thereAreUnknownInputsInTx()) return false;
return true;
}
/**
* 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

View file

@ -14,3 +14,4 @@ export * from './hd-segwit-bech32-transaction';
export * from './wallets/placeholder-wallet';
export * from './wallets/hd-legacy-electrum-seed-p2pkh-wallet';
export * from './wallets/hd-segwit-electrum-seed-p2wpkh-wallet';
export * from './wallets/multisig-hd-wallet';

View file

@ -0,0 +1,85 @@
/* global alert */
import * as bitcoin from 'bitcoinjs-lib';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
const delay = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));
// Implements IPayjoinClientWallet
// https://github.com/bitcoinjs/payjoin-client/blob/master/ts_src/wallet.ts
export default class PayjoinTransaction {
constructor(psbt, broadcast, wallet) {
this._psbt = psbt;
this._broadcast = broadcast;
this._wallet = wallet;
this._payjoinPsbt = false;
}
async getPsbt() {
// Nasty hack to get this working for now
const unfinalized = this._psbt.clone();
unfinalized.data.inputs.forEach((input, index) => {
delete input.finalScriptWitness;
const address = bitcoin.address.fromOutputScript(input.witnessUtxo.script);
const wif = this._wallet._getWifForAddress(address);
const keyPair = bitcoin.ECPair.fromWIF(wif);
unfinalized.signInput(index, keyPair);
});
return unfinalized;
}
/**
* Doesnt conform to spec but needed for user-facing wallet software to find out txid of payjoined transaction
*
* @returns {boolean|Psbt}
*/
getPayjoinPsbt() {
return this._payjoinPsbt;
}
async signPsbt(payjoinPsbt) {
// Do this without relying on private methods
payjoinPsbt.data.inputs.forEach((input, index) => {
const address = bitcoin.address.fromOutputScript(input.witnessUtxo.script);
try {
const wif = this._wallet._getWifForAddress(address);
const keyPair = bitcoin.ECPair.fromWIF(wif);
payjoinPsbt.signInput(index, keyPair).finalizeInput(index);
} catch (e) {}
});
this._payjoinPsbt = payjoinPsbt;
return this._payjoinPsbt;
}
async broadcastTx(txHex) {
try {
const result = await this._broadcast(txHex);
if (!result) {
throw new Error(`Broadcast failed`);
}
return '';
} catch (e) {
return 'Error: ' + e.message;
}
}
async scheduleBroadcastTx(txHex, milliseconds) {
delay(milliseconds).then(async () => {
const result = await this.broadcastTx(txHex);
if (result === '') {
// TODO: Improve the wording of this error message
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
alert('Something was wrong with the payjoin transaction, the original transaction sucessfully broadcast.');
}
});
}
async isOwnOutputScript(outputScript) {
const address = bitcoin.address.fromOutputScript(outputScript);
return this._wallet.weOwnAddress(address);
}
}

View file

@ -9,7 +9,8 @@ import { PlaceholderWallet } from './wallets/placeholder-wallet';
import { SegwitBech32Wallet } from './wallets/segwit-bech32-wallet';
import { HDLegacyElectrumSeedP2PKHWallet } from './wallets/hd-legacy-electrum-seed-p2pkh-wallet';
import { HDSegwitElectrumSeedP2WPKHWallet } from './wallets/hd-segwit-electrum-seed-p2wpkh-wallet';
import { BlueCurrentTheme } from '../components/themes';
import { MultisigHDWallet } from './wallets/multisig-hd-wallet';
import { useTheme } from '@react-navigation/native';
export default class WalletGradient {
static hdSegwitP2SHWallet = ['#65ceef', '#68bbe1'];
@ -19,9 +20,15 @@ export default class WalletGradient {
static legacyWallet = ['#40fad1', '#15be98'];
static hdLegacyP2PKHWallet = ['#e36dfa', '#bd10e0'];
static hdLegacyBreadWallet = ['#fe6381', '#f99c42'];
static multisigHdWallet = ['#1ce6eb', '#296fc5', '#3500A2'];
static defaultGradients = ['#c65afb', '#9053fe'];
static lightningCustodianWallet = ['#f1be07', '#f79056'];
static createWallet = BlueCurrentTheme.colors.lightButton;
static createWallet = () => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { colors } = useTheme();
return colors.lightButton;
};
static gradientsFor(type) {
let gradient;
@ -55,6 +62,9 @@ export default class WalletGradient {
case SegwitBech32Wallet.type:
gradient = WalletGradient.segwitBech32Wallet;
break;
case MultisigHDWallet.type:
gradient = WalletGradient.multisigHdWallet;
break;
default:
gradient = WalletGradient.defaultGradients;
break;
@ -88,6 +98,9 @@ export default class WalletGradient {
case SegwitBech32Wallet.type:
gradient = WalletGradient.segwitBech32Wallet;
break;
case MultisigHDWallet.type:
gradient = WalletGradient.multisigHdWallet;
break;
case LightningCustodianWallet.type:
gradient = WalletGradient.lightningCustodianWallet;
break;

View file

@ -13,6 +13,7 @@ import {
SegwitBech32Wallet,
HDLegacyElectrumSeedP2PKHWallet,
HDSegwitElectrumSeedP2WPKHWallet,
MultisigHDWallet,
} from '.';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import loc from '../loc';
@ -34,7 +35,9 @@ export default class WalletImport {
*/
static async _saveWallet(w, additionalProperties) {
try {
const wallet = BlueApp.getWallets().some(wallet => wallet.getSecret() === w.secret && wallet.type !== PlaceholderWallet.type);
const wallet = BlueApp.getWallets().some(
wallet => (wallet.getSecret() === w.secret || wallet.getID() === w.getID()) && wallet.type !== PlaceholderWallet.type,
);
if (wallet) {
alert('This wallet has been previously imported.');
WalletImport.removePlaceholderWallet();
@ -97,6 +100,7 @@ export default class WalletImport {
const placeholderWallet = WalletImport.addPlaceholderWallet(importText);
// Plan:
// -2. check if BIP38 encrypted
// -1a. check if multisig
// -1. check lightning custodian
// 0. check if its HDSegwitBech32Wallet (BIP84)
// 1. check if its HDSegwitP2SHWallet (BIP49)
@ -125,6 +129,18 @@ export default class WalletImport {
}
}
// is it multisig?
try {
const ms = new MultisigHDWallet();
ms.setSecret(importText);
if (ms.getN() > 0 && ms.getM() > 0) {
await ms.fetchBalance();
return WalletImport._saveWallet(ms);
}
} catch (e) {
console.log(e);
}
// is it lightning custodian?
if (importText.indexOf('blitzhub://') !== -1 || importText.indexOf('lndhub://') !== -1) {
const lnd = new LightningCustodianWallet();

View file

@ -553,27 +553,33 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
async _fetchBalance() {
// probing future addressess in hierarchy whether they have any transactions, in case
// our 'next free addr' pointers are lagging behind
let tryAgain = false;
let txs = await BlueElectrum.getTransactionsByAddress(
this._getExternalAddressByIndex(this.next_free_address_index + this.gap_limit - 1),
);
if (txs.length > 0) {
// whoa, someone uses our wallet outside! better catch up
this.next_free_address_index += this.gap_limit;
tryAgain = true;
// for that we are gona batch fetch history for all addresses between last used and last used + gap_limit
const lagAddressesToFetch = [];
for (let c = this.next_free_address_index; c < this.next_free_address_index + this.gap_limit; c++) {
lagAddressesToFetch.push(this._getExternalAddressByIndex(c));
}
for (let c = this.next_free_change_address_index; c < this.next_free_change_address_index + this.gap_limit; c++) {
lagAddressesToFetch.push(this._getInternalAddressByIndex(c));
}
txs = await BlueElectrum.getTransactionsByAddress(
this._getInternalAddressByIndex(this.next_free_change_address_index + this.gap_limit - 1),
);
if (txs.length > 0) {
this.next_free_change_address_index += this.gap_limit;
tryAgain = true;
const txs = await BlueElectrum.multiGetHistoryByAddress(lagAddressesToFetch); // <------ electrum call
for (let c = this.next_free_address_index; c < this.next_free_address_index + this.gap_limit; c++) {
const address = this._getExternalAddressByIndex(c);
if (txs[address] && Array.isArray(txs[address]) && txs[address].length > 0) {
// whoa, someone uses our wallet outside! better catch up
this.next_free_address_index = c + 1;
}
}
// FIXME: refactor me ^^^ can be batched in single call. plus not just couple of addresses, but all between [ next_free .. (next_free + gap_limit) ]
if (tryAgain) return this._fetchBalance();
for (let c = this.next_free_change_address_index; c < this.next_free_change_address_index + this.gap_limit; c++) {
const address = this._getInternalAddressByIndex(c);
if (txs[address] && Array.isArray(txs[address]) && txs[address].length > 0) {
// whoa, someone uses our wallet outside! better catch up
this.next_free_change_address_index = c + 1;
}
}
// next, business as usuall. fetch balances
@ -674,8 +680,9 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
addressess = [...new Set(addressess)]; // deduplicate just for any case
const fetchedUtxo = await BlueElectrum.multiGetUtxoByAddress(addressess);
this._utxo = [];
for (const arr of Object.values(await BlueElectrum.multiGetUtxoByAddress(addressess))) {
for (const arr of Object.values(fetchedUtxo)) {
this._utxo = this._utxo.concat(arr);
}
@ -712,15 +719,26 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
return this._utxo;
}
getDerivedUtxoFromOurTransaction() {
getDerivedUtxoFromOurTransaction(returnSpentUtxoAsWell = false) {
const utxos = [];
// its faster to pre-build hashmap of owned addresses than to query `this.weOwnAddress()`, which in turn
// iterates over all addresses in hierarchy
const ownedAddressesHashmap = {};
for (let c = 0; c < this.next_free_address_index + 1; c++) {
ownedAddressesHashmap[this._getExternalAddressByIndex(c)] = true;
}
for (let c = 0; c < this.next_free_change_address_index + 1; c++) {
ownedAddressesHashmap[this._getInternalAddressByIndex(c)] = true;
}
for (const tx of this.getTransactions()) {
for (const output of tx.outputs) {
let address = false;
if (output.scriptPubKey && output.scriptPubKey.addresses && output.scriptPubKey.addresses[0]) {
address = output.scriptPubKey.addresses[0];
}
if (this.weOwnAddress(address)) {
if (ownedAddressesHashmap[address]) {
const value = new BigNumber(output.value).multipliedBy(100000000).toNumber();
utxos.push({
txid: tx.txid,
@ -730,13 +748,15 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
value,
amount: value,
confirmations: tx.confirmations,
wif: this._getWifForAddress(address),
wif: false,
height: BlueElectrum.estimateCurrentBlockheight() - tx.confirmations,
});
}
}
}
if (returnSpentUtxoAsWell) return utxos;
// got all utxos we ever had. lets filter out the ones that are spent:
const ret = [];
for (const utxo of utxos) {
@ -748,7 +768,11 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
}
if (!spent) ret.push(utxo);
if (!spent) {
// filling WIFs only for legit unspent UTXO, as it is a slow operation
utxo.wif = this._getWifForAddress(utxo.address);
ret.push(utxo);
}
}
return ret;

View file

@ -26,6 +26,10 @@ export class AbstractHDWallet extends LegacyWallet {
return this.next_free_address_index;
}
getNextFreeChangeAddressIndex() {
return this.next_free_change_address_index;
}
prepareForSerialization() {
// deleting structures that cant be serialized
delete this._node0;
@ -93,7 +97,7 @@ export class AbstractHDWallet extends LegacyWallet {
if (!freeAddress) {
// could not find in cycle above, give up
freeAddress = this._getExternalAddressByIndex(this.next_free_address_index + c); // we didnt check this one, maybe its free
this.next_free_address_index += c + 1; // now points to the one _after_
this.next_free_address_index += c; // now points to this one
}
this._address = freeAddress;
return freeAddress;
@ -130,8 +134,8 @@ export class AbstractHDWallet extends LegacyWallet {
if (!freeAddress) {
// could not find in cycle above, give up
freeAddress = this._getExternalAddressByIndex(this.next_free_address_index + c); // we didnt check this one, maybe its free
this.next_free_address_index += c + 1; // now points to the one _after_
freeAddress = this._getInternalAddressByIndex(this.next_free_change_address_index + c); // we didnt check this one, maybe its free
this.next_free_change_address_index += c; // now points to this one
}
this._address = freeAddress;
return freeAddress;

View file

@ -115,6 +115,10 @@ export class AbstractWallet {
return false;
}
allowPayJoin() {
return false;
}
weOwnAddress(address) {
throw Error('not implemented');
}
@ -215,6 +219,10 @@ export class AbstractWallet {
return new Promise(resolve => resolve(this.getAddress()));
}
async getChangeAddressAsync() {
return new Promise(resolve => resolve(this.getAddress()));
}
useWithHardwareWalletEnabled() {
return false;
}
@ -260,4 +268,6 @@ export class AbstractWallet {
return b58.encode(data);
}
prepareForSerialization() {}
}

View file

@ -71,6 +71,7 @@ export class HDLegacyBreadwalletWallet extends HDLegacyP2PKHWallet {
* @private
*/
_getWIFByIndex(internal, index) {
if (!this.secret) return false;
const mnemonic = this.secret;
const seed = bip39.mnemonicToSeed(mnemonic);
const root = bitcoinjs.bip32.fromSeed(seed);

View file

@ -61,6 +61,7 @@ export class HDLegacyElectrumSeedP2PKHWallet extends HDLegacyP2PKHWallet {
}
_getWIFByIndex(internal, index) {
if (!this.secret) return false;
const root = bitcoin.bip32.fromSeed(mn.mnemonicToSeedSync(this.secret, MNEMONIC_TO_SEED_OPTS));
const path = `m/${internal ? 1 : 0}/${index}`;
const child = root.derivePath(path);

View file

@ -48,6 +48,7 @@ export class HDLegacyP2PKHWallet extends AbstractHDElectrumWallet {
* @private
*/
_getWIFByIndex(internal, index) {
if (!this.secret) return false;
const mnemonic = this.secret;
const seed = bip39.mnemonicToSeed(mnemonic);

View file

@ -28,4 +28,8 @@ export class HDSegwitBech32Wallet extends AbstractHDElectrumWallet {
allowRBF() {
return true;
}
allowPayJoin() {
return true;
}
}

View file

@ -61,6 +61,7 @@ export class HDSegwitElectrumSeedP2WPKHWallet extends HDSegwitBech32Wallet {
}
_getWIFByIndex(internal, index) {
if (!this.secret) return false;
const root = bitcoin.bip32.fromSeed(mn.mnemonicToSeedSync(this.secret, MNEMONIC_TO_SEED_OPTS));
const path = `m/0'/${internal ? 1 : 0}/${index}`;
const child = root.derivePath(path);

View file

@ -29,6 +29,7 @@ export class HDSegwitP2SHWallet extends AbstractHDElectrumWallet {
* @private
*/
_getWIFByIndex(internal, index) {
if (!this.secret) return false;
const mnemonic = this.secret;
const seed = bip39.mnemonicToSeed(mnemonic);
const root = bitcoin.bip32.fromSeed(seed);

View file

@ -401,10 +401,4 @@ export class LegacyWallet extends AbstractWallet {
allowSendMax() {
return true;
}
async getChangeAddressAsync() {
return new Promise(resolve => {
resolve(this.getAddress());
});
}
}

View file

@ -0,0 +1,897 @@
import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet';
import bip39 from 'bip39';
import b58 from 'bs58check';
import { decodeUR } from 'bc-ur';
const BlueElectrum = require('../../blue_modules/BlueElectrum');
const coinSelectAccumulative = require('coinselect/accumulative');
const coinSelectSplit = require('coinselect/split');
const HDNode = require('bip32');
const bitcoin = require('bitcoinjs-lib');
const createHash = require('create-hash');
const reverse = require('buffer-reverse');
export class MultisigHDWallet extends AbstractHDElectrumWallet {
static type = 'HDmultisig';
static typeReadable = 'Multisig Vault';
constructor() {
super();
this._m = 0; // minimum required signatures so spend (m out of n)
this._cosigners = []; // array of xpubs or mnemonic seeds
this._cosignersFingerprints = []; // array of according fingerprints (if any provided)
this._cosignersCustomPaths = []; // array of according paths (if any provided)
this._derivationPath = '';
this._isNativeSegwit = false;
this._isWrappedSegwit = false;
this._isLegacy = false;
this.gap_limit = 10;
}
isLegacy() {
return this._isLegacy;
}
isNativeSegwit() {
return this._isNativeSegwit;
}
isWrappedSegwit() {
return this._isWrappedSegwit;
}
setWrappedSegwit() {
this._isWrappedSegwit = true;
}
setNativeSegwit() {
this._isNativeSegwit = true;
}
setLegacy() {
this._isLegacy = true;
}
setM(m) {
this._m = m;
}
/**
* @returns {number} How many minumim signatures required to authorize a spend
*/
getM() {
return this._m;
}
/**
* @returns {number} Total count of cosigners
*/
getN() {
return this._cosigners.length;
}
setDerivationPath(path) {
this._derivationPath = path;
switch (this._derivationPath) {
case "m/48'/0'/0'/2'":
this._isNativeSegwit = true;
break;
case "m/48'/0'/0'/1'":
this._isWrappedSegwit = true;
break;
case "m/45'":
this._isLegacy = true;
break;
case "m/44'":
this._isLegacy = true;
break;
}
}
getDerivationPath() {
return this._derivationPath;
}
getCustomDerivationPathForCosigner(index) {
if (index === 0) throw new Error('cosigners indexation starts from 1');
return this._cosignersCustomPaths[index - 1] || this.getDerivationPath();
}
getCosigner(index) {
if (index === 0) throw new Error('cosigners indexation starts from 1');
return this._cosigners[index - 1];
}
getFingerprint(index) {
if (index === 0) throw new Error('cosigners fingerprints indexation starts from 1');
return this._cosignersFingerprints[index - 1];
}
getCosignerForFingerprint(fp) {
const index = this._cosignersFingerprints.indexOf(fp);
return this._cosigners[index];
}
static isXpubValid(key) {
let xpub;
try {
xpub = super._zpubToXpub(key);
HDNode.fromBase58(xpub);
return true;
} catch (_) {}
return false;
}
/**
*
* @param key {string} Either xpub or mnemonic phrase
* @param fingerprint {string} Fingerprint for cosigner that is added as xpub
* @param path {string} Custom path (if any) for cosigner that is added as mnemonics
*/
addCosigner(key, fingerprint, path) {
if (MultisigHDWallet.isXpubString(key) && !fingerprint) {
throw new Error('fingerprint is required when adding cosigner as xpub (watch-only)');
}
if (path && !this.constructor.isPathValid(path)) {
throw new Error('path is not valid');
}
if (!MultisigHDWallet.isXpubString(key)) {
// mnemonics. lets derive fingerprint
if (!bip39.validateMnemonic(key)) throw new Error('Not a valid mnemonic phrase');
fingerprint = MultisigHDWallet.seedToFingerprint(key);
} else {
if (!MultisigHDWallet.isXpubValid(key)) throw new Error('Not a valid xpub: ' + key);
}
const index = this._cosigners.length;
this._cosigners[index] = key;
if (fingerprint) this._cosignersFingerprints[index] = fingerprint.toUpperCase();
if (path) this._cosignersCustomPaths[index] = path;
}
/**
* Stored cosigner can be EITHER xpub (or Zpub or smth), OR mnemonic phrase. This method converts it to xpub
*
* @param cosigner {string} Zpub (or similar) or mnemonic seed
* @returns {string} xpub
* @private
*/
_getXpubFromCosigner(cosigner) {
let xpub = cosigner;
if (!MultisigHDWallet.isXpubString(cosigner)) {
const index = this._cosigners.indexOf(cosigner);
xpub = MultisigHDWallet.seedToXpub(cosigner, this._cosignersCustomPaths[index] || this._derivationPath);
}
return this.constructor._zpubToXpub(xpub);
}
_getExternalAddressByIndex(index) {
if (!this._m) throw new Error('m is not set');
index = +index;
if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
const address = this._getAddressFromNode(0, index);
this.external_addresses_cache[index] = address;
return address;
}
_getAddressFromNode(nodeIndex, index) {
const pubkeys = [];
let cosignerIndex = 0;
for (const cosigner of this._cosigners) {
this._nodes = this._nodes || [];
this._nodes[nodeIndex] = this._nodes[nodeIndex] || [];
let _node;
if (!this._nodes[nodeIndex][cosignerIndex]) {
const xpub = this._getXpubFromCosigner(cosigner);
const hdNode = HDNode.fromBase58(xpub);
_node = hdNode.derive(nodeIndex);
} else {
_node = this._nodes[nodeIndex][cosignerIndex];
}
pubkeys.push(_node.derive(index).publicKey);
cosignerIndex++;
}
if (this.isWrappedSegwit()) {
const { address } = bitcoin.payments.p2sh({
redeem: bitcoin.payments.p2wsh({
redeem: bitcoin.payments.p2ms({ m: this._m, pubkeys: MultisigHDWallet.sortBuffers(pubkeys) }),
}),
});
return address;
} else if (this.isNativeSegwit()) {
const { address } = bitcoin.payments.p2wsh({
redeem: bitcoin.payments.p2ms({ m: this._m, pubkeys: MultisigHDWallet.sortBuffers(pubkeys) }),
});
return address;
} else if (this.isLegacy()) {
const { address } = bitcoin.payments.p2sh({
redeem: bitcoin.payments.p2ms({ m: this._m, pubkeys: MultisigHDWallet.sortBuffers(pubkeys) }),
});
return address;
} else {
throw new Error('Dont know how to make address');
}
}
_getInternalAddressByIndex(index) {
if (!this._m) throw new Error('m is not set');
index = +index;
if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit
const address = this._getAddressFromNode(1, index);
this.internal_addresses_cache[index] = address;
return address;
}
static seedToXpub(mnemonic, path) {
const seed = bip39.mnemonicToSeed(mnemonic);
const root = bitcoin.bip32.fromSeed(seed);
const child = root.derivePath(path).neutered();
this._xpub = child.toBase58();
return this._xpub;
}
/**
* @param mnemonic {string} Mnemonic seed phrase
* @returns {string} Hex string of fingerprint derived from mnemonics. Always has lenght of 8 chars and correct leading zeroes
*/
static seedToFingerprint(mnemonic) {
const seed = bip39.mnemonicToSeed(mnemonic);
const root = bitcoin.bip32.fromSeed(seed);
let hex = root.fingerprint.toString('hex');
while (hex.length < 8) hex = '0' + hex; // leading zeroes
return hex.toUpperCase();
}
/**
* Returns xpub with correct prefix accodting to this objects set derivation path, for example 'Zpub' (with
* capital Z) for bech32 multisig
* @see https://github.com/satoshilabs/slips/blob/master/slip-0132.md
*
* @param xpub {string} Any kind of xpub, including zpub etc since we are only swapping the prefix bytes
* @returns {string}
*/
convertXpubToMultisignatureXpub(xpub) {
let data = b58.decode(xpub);
data = data.slice(4);
if (this.isNativeSegwit()) {
return b58.encode(Buffer.concat([Buffer.from('02aa7ed3', 'hex'), data]));
} else if (this.isWrappedSegwit()) {
return b58.encode(Buffer.concat([Buffer.from('0295b43f', 'hex'), data]));
}
return xpub;
}
static isXpubString(xpub) {
return ['xpub', 'ypub', 'zpub', 'Ypub', 'Zpub'].includes(xpub.substring(0, 4));
}
/**
* Converts fingerprint that is stored as a deciman number to hex string (all caps)
*
* @param xfp {number} For example 64392470
* @returns {string} For example 168DD603
*/
static ckccXfp2fingerprint(xfp) {
let masterFingerprintHex = Number(xfp).toString(16);
while (masterFingerprintHex.length < 8) masterFingerprintHex = '0' + masterFingerprintHex; // conversion without explicit zero might result in lost byte
// poor man's little-endian conversion:
// ¯\_(ツ)_/¯
return (
masterFingerprintHex[6] +
masterFingerprintHex[7] +
masterFingerprintHex[4] +
masterFingerprintHex[5] +
masterFingerprintHex[2] +
masterFingerprintHex[3] +
masterFingerprintHex[0] +
masterFingerprintHex[1]
).toUpperCase();
}
getXpub() {
return this.getSecret(true);
}
getSecret(coordinationSetup = false) {
let ret = '# BlueWallet Multisig setup file\n';
if (coordinationSetup) ret += '# this file contains only public keys and is safe to\n# distribute among cosigners\n';
if (!coordinationSetup) ret += '# this file may contain private information\n';
ret += '#\n';
ret += 'Name: ' + this.getLabel() + '\n';
ret += 'Policy: ' + this.getM() + ' of ' + this.getN() + '\n';
let hasCustomPaths = 0;
for (let index = 0; index < this.getN(); index++) {
if (this._cosignersCustomPaths[index]) hasCustomPaths++;
}
let printedGlobalDerivation = false;
if (hasCustomPaths !== this.getN()) {
printedGlobalDerivation = true;
ret += 'Derivation: ' + this.getDerivationPath() + '\n';
}
if (this.isNativeSegwit()) {
ret += 'Format: P2WSH\n';
} else if (this.isWrappedSegwit()) {
ret += 'Format: P2WSH-P2SH\n';
} else if (this.isLegacy()) {
ret += 'Format: P2SH\n';
} else {
ret += 'Format: unknown\n';
}
ret += '\n';
for (let index = 0; index < this.getN(); index++) {
if (
this._cosignersCustomPaths[index] &&
((printedGlobalDerivation && this._cosignersCustomPaths[index] !== this.getDerivationPath()) || !printedGlobalDerivation)
) {
ret += '# derivation: ' + this._cosignersCustomPaths[index] + '\n';
// if we printed global derivation and this cosigned _has_ derivation and its different from global - we print it ;
// or we print it if cosigner _has_ some derivation set and we did not print global
}
if (this.constructor.isXpubString(this._cosigners[index])) {
ret += this._cosignersFingerprints[index] + ': ' + this._cosigners[index] + '\n';
} else {
if (coordinationSetup) {
const xpub = this.convertXpubToMultisignatureXpub(
MultisigHDWallet.seedToXpub(this._cosigners[index], this._cosignersCustomPaths[index] || this._derivationPath),
);
const fingerprint = MultisigHDWallet.seedToFingerprint(this._cosigners[index]);
ret += fingerprint + ': ' + xpub + '\n';
} else {
ret += 'seed: ' + this._cosigners[index] + '\n';
ret += '# warning! sensitive information, do not disclose ^^^ \n';
}
}
ret += '\n';
}
return ret;
}
setSecret(secret) {
if (secret.toUpperCase().startsWith('UR:BYTES')) {
const decoded = decodeUR([secret]);
const b = Buffer.from(decoded, 'hex');
secret = b.toString();
}
// is it Coldcard json file?
let json;
try {
json = JSON.parse(secret);
} catch (_) {}
if (json && json.xfp && json.p2wsh_deriv && json.p2wsh) {
this.addCosigner(json.p2wsh, json.xfp); // technically we dont need deriv (json.p2wsh_deriv), since cosigner is already an xpub
return;
}
// is it electrum json?
if (json && json.wallet_type) {
const mofn = json.wallet_type.split('of');
this.setM(parseInt(mofn[0].trim()));
const n = parseInt(mofn[1].trim());
for (let c = 1; c <= n; c++) {
const cosignerData = json['x' + c + '/'];
if (cosignerData) {
const fingerprint =
(cosignerData.ckcc_xfp
? MultisigHDWallet.ckccXfp2fingerprint(cosignerData.ckcc_xfp)
: cosignerData.root_fingerprint?.toUpperCase()) || '00000000';
if (cosignerData.seed) {
// TODO: support electrum's bip32
}
this.addCosigner(cosignerData.xpub, fingerprint, cosignerData.derivation);
}
}
if (this.getCosigner(1).startsWith('Zpub')) this.setNativeSegwit();
if (this.getCosigner(1).startsWith('Ypub')) this.setWrappedSegwit();
if (this.getCosigner(1).startsWith('xpub')) this.setLegacy();
}
// coldcard & cobo txt format:
let customPathForCurrentCosigner = false;
for (const line of secret.split('\n')) {
const [key, value] = line.split(':');
switch (key) {
case 'Name':
this.setLabel(value.trim());
break;
case 'Policy':
this.setM(parseInt(value.trim().split('of')[0].trim()));
break;
case 'Derivation':
this.setDerivationPath(value.trim());
break;
case 'Format':
switch (value.trim()) {
case 'P2WSH':
this.setNativeSegwit();
break;
case 'P2WSH-P2SH':
this.setWrappedSegwit();
break;
case 'P2SH':
this.setLegacy();
break;
}
break;
default:
if (key && value && MultisigHDWallet.isXpubString(value.trim())) {
this.addCosigner(value.trim(), key, customPathForCurrentCosigner);
} else if (key.replace('#', '').trim() === 'derivation') {
customPathForCurrentCosigner = value.trim();
} else if (key === 'seed') {
this.addCosigner(value.trim(), false, customPathForCurrentCosigner);
}
break;
}
}
// is it wallet descriptor?
// @see https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md
// @see https://github.com/Fonta1n3/FullyNoded/blob/master/Docs/Wallets/Wallet-Export-Spec.md
if (secret.indexOf('sortedmulti(') !== -1 && json.descriptor) {
if (json.label) this.setLabel(json.label);
if (json.descriptor.startsWith('wsh(')) {
this.setNativeSegwit();
}
if (json.descriptor.startsWith('sh(')) {
this.setLegacy();
}
if (json.descriptor.startsWith('sh(wsh(')) {
this.setLegacy();
}
const s2 = json.descriptor.substr(json.descriptor.indexOf('sortedmulti(') + 12);
const s3 = s2.split(',');
const m = parseInt(s3[0]);
if (m) this.setM(m);
for (let c = 1; c < s3.length; c++) {
const re = /\[([^\]]+)\](.*)/;
const m = s3[c].match(re);
if (m && m.length === 3) {
let hexFingerprint = m[1].split('/')[0];
if (hexFingerprint.length === 8) {
hexFingerprint = Buffer.from(hexFingerprint, 'hex').reverse().toString('hex');
}
const path = 'm/' + m[1].split('/').slice(1).join('/').replace(/[h]/g, "'");
let xpub = m[2];
if (xpub.indexOf('/') !== -1) {
xpub = xpub.substr(0, xpub.indexOf('/'));
}
// console.warn('m[2] = ', m[2], {hexFingerprint, path, xpub});
this.addCosigner(xpub, hexFingerprint.toUpperCase(), path);
}
}
}
// is it caravan?
if (json && json.network === 'mainnet' && json.quorum) {
this.setM(+json.quorum.requiredSigners);
if (json.name) this.setLabel(json.name);
switch (json.addressType.toLowerCase()) {
case 'P2SH':
this.setLegacy();
break;
case 'P2SH-P2WSH':
this.setWrappedSegwit();
break;
default:
case 'P2WSH':
this.setNativeSegwit();
break;
}
for (const pk of json.extendedPublicKeys) {
const path = this.constructor.isPathValid(json.bip32Path) ? json.bip32Path : "m/1'";
// wtf, where caravan stores fingerprints..?
this.addCosigner(pk.xpub, '00000000', path);
}
}
if (!this.getLabel()) this.setLabel('Multisig vault');
}
_getDerivationPathByAddressWithCustomPath(address, customPathPrefix) {
const path = customPathPrefix || this._derivationPath;
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
if (this._getExternalAddressByIndex(c) === address) return path + '/0/' + c;
}
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
if (this._getInternalAddressByIndex(c) === address) return path + '/1/' + c;
}
return false;
}
_getWifForAddress(address) {
return false;
}
_getPubkeyByAddress(address) {
throw new Error('Not applicable in multisig');
}
_getDerivationPathByAddress(address) {
throw new Error('Not applicable in multisig');
}
_addPsbtInput(psbt, input, sequence, masterFingerprintBuffer) {
const bip32Derivation = []; // array per each pubkey thats gona be used
const pubkeys = [];
for (let c = 0; c < this._cosigners.length; c++) {
const cosigner = this._cosigners[c];
const path = this._getDerivationPathByAddressWithCustomPath(input.address, this._cosignersCustomPaths[c] || this._derivationPath);
// ^^ path resembles _custom path_, if provided by user during setup, otherwise default path for wallet type gona be used
const masterFingerprint = Buffer.from(this._cosignersFingerprints[c], 'hex');
const xpub = this._getXpubFromCosigner(cosigner);
const hdNode0 = HDNode.fromBase58(xpub);
const splt = path.split('/');
const internal = +splt[splt.length - 2];
const index = +splt[splt.length - 1];
const _node0 = hdNode0.derive(internal);
const pubkey = _node0.derive(index).publicKey;
pubkeys.push(pubkey);
bip32Derivation.push({
masterFingerprint,
path,
pubkey,
});
}
if (this.isNativeSegwit()) {
const p2wsh = bitcoin.payments.p2wsh({
redeem: bitcoin.payments.p2ms({ m: this._m, pubkeys: MultisigHDWallet.sortBuffers(pubkeys) }),
});
const witnessScript = p2wsh.redeem.output;
psbt.addInput({
hash: input.txId,
index: input.vout,
sequence,
bip32Derivation,
witnessUtxo: {
script: p2wsh.output,
value: input.value,
},
witnessScript,
// hw wallets now require passing the whole previous tx as Buffer, as if it was non-segwit input, to mitigate
// some hw wallets attack vector
nonWitnessUtxo: Buffer.from(input.txhex, 'hex'),
});
} else if (this.isWrappedSegwit()) {
const p2shP2wsh = bitcoin.payments.p2sh({
redeem: bitcoin.payments.p2wsh({
redeem: bitcoin.payments.p2ms({ m: this._m, pubkeys: MultisigHDWallet.sortBuffers(pubkeys) }),
}),
});
const witnessScript = p2shP2wsh.redeem.redeem.output;
const redeemScript = p2shP2wsh.redeem.output;
psbt.addInput({
hash: input.txId,
index: input.vout,
sequence,
bip32Derivation,
witnessUtxo: {
script: p2shP2wsh.output,
value: input.value,
},
witnessScript,
redeemScript,
// hw wallets now require passing the whole previous tx as Buffer, as if it was non-segwit input, to mitigate
// some hw wallets attack vector
nonWitnessUtxo: Buffer.from(input.txhex, 'hex'),
});
} else if (this.isLegacy()) {
const p2sh = bitcoin.payments.p2sh({
redeem: bitcoin.payments.p2ms({ m: this._m, pubkeys: MultisigHDWallet.sortBuffers(pubkeys) }),
});
const redeemScript = p2sh.redeem.output;
psbt.addInput({
hash: input.txId,
index: input.vout,
sequence,
bip32Derivation,
redeemScript,
nonWitnessUtxo: Buffer.from(input.txhex, 'hex'),
});
} else {
throw new Error('Dont know how to add input');
}
return psbt;
}
_getOutputDataForChange(outputData) {
const bip32Derivation = []; // array per each pubkey thats gona be used
const pubkeys = [];
for (let c = 0; c < this._cosigners.length; c++) {
const cosigner = this._cosigners[c];
const path = this._getDerivationPathByAddressWithCustomPath(
outputData.address,
this._cosignersCustomPaths[c] || this._derivationPath,
);
// ^^ path resembles _custom path_, if provided by user during setup, otherwise default path for wallet type gona be used
const masterFingerprint = Buffer.from(this._cosignersFingerprints[c], 'hex');
const xpub = this._getXpubFromCosigner(cosigner);
const hdNode0 = HDNode.fromBase58(xpub);
const splt = path.split('/');
const internal = +splt[splt.length - 2];
const index = +splt[splt.length - 1];
const _node0 = hdNode0.derive(internal);
const pubkey = _node0.derive(index).publicKey;
pubkeys.push(pubkey);
bip32Derivation.push({
masterFingerprint,
path,
pubkey,
});
}
outputData.bip32Derivation = bip32Derivation;
if (this.isLegacy()) {
const p2sh = bitcoin.payments.p2ms({ m: this._m, pubkeys: MultisigHDWallet.sortBuffers(pubkeys) });
outputData.redeemScript = p2sh.output;
} else if (this.isWrappedSegwit()) {
const p2shP2wsh = bitcoin.payments.p2sh({
redeem: bitcoin.payments.p2wsh({
redeem: bitcoin.payments.p2ms({ m: this._m, pubkeys: MultisigHDWallet.sortBuffers(pubkeys) }),
}),
});
outputData.witnessScript = p2shP2wsh.redeem.redeem.output;
outputData.redeemScript = p2shP2wsh.redeem.output;
} else if (this.isNativeSegwit()) {
// not needed by coldcard, apparently..?
const p2wsh = bitcoin.payments.p2wsh({
redeem: bitcoin.payments.p2ms({ m: this._m, pubkeys: MultisigHDWallet.sortBuffers(pubkeys) }),
});
outputData.witnessScript = p2wsh.redeem.output;
} else {
throw new Error('dont know how to add change output');
}
return outputData;
}
howManySignaturesCanWeMake() {
let howManyPrivKeysWeGot = 0;
for (const cosigner of this._cosigners) {
if (!MultisigHDWallet.isXpubString(cosigner)) howManyPrivKeysWeGot++;
}
return howManyPrivKeysWeGot;
}
/**
* @inheritDoc
*/
createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false, masterFingerprint) {
if (targets.length === 0) throw new Error('No destination provided');
if (this.howManySignaturesCanWeMake() === 0) skipSigning = true;
if (!changeAddress) throw new Error('No change address provided');
sequence = sequence || AbstractHDElectrumWallet.defaultRBFSequence;
let algo = coinSelectAccumulative;
if (targets.length === 1 && targets[0] && !targets[0].value) {
// we want to send MAX
algo = coinSelectSplit;
}
const { inputs, outputs, fee } = algo(utxos, targets, feeRate);
// .inputs and .outputs will be undefined if no solution was found
if (!inputs || !outputs) {
throw new Error('Not enough balance. Try sending smaller amount');
}
let psbt = new bitcoin.Psbt();
let c = 0;
inputs.forEach(input => {
c++;
psbt = this._addPsbtInput(psbt, input, sequence);
});
outputs.forEach(output => {
// if output has no address - this is change output
let change = false;
if (!output.address) {
change = true;
output.address = changeAddress;
}
let outputData = {
address: output.address,
value: output.value,
};
if (change) {
outputData = this._getOutputDataForChange(outputData);
}
psbt.addOutput(outputData);
});
if (!skipSigning) {
for (let cc = 0; cc < c; cc++) {
for (const cosigner of this._cosigners) {
if (!MultisigHDWallet.isXpubString(cosigner)) {
// ok this is a mnemonic, lets try to sign
const seed = bip39.mnemonicToSeed(cosigner);
const hdRoot = bitcoin.bip32.fromSeed(seed);
psbt.signInputHD(cc, hdRoot);
}
}
}
}
let tx;
if (!skipSigning && this.howManySignaturesCanWeMake() >= this.getM()) {
tx = psbt.finalizeAllInputs().extractTransaction();
}
return { tx, inputs, outputs, fee, psbt };
}
/**
* @see https://github.com/bitcoin/bips/blob/master/bip-0067.mediawiki
*
* @param bufArr {Array.<Buffer>}
* @returns {Array.<Buffer>}
*/
static sortBuffers(bufArr) {
return bufArr.sort(Buffer.compare);
}
prepareForSerialization() {
// deleting structures that cant be serialized
delete this._nodes;
}
static isPathValid(path) {
const root = bitcoin.bip32.fromSeed(Buffer.alloc(32));
try {
root.derivePath(path);
return true;
} catch (_) {}
return false;
}
allowSend() {
return true;
}
async fetchUtxo() {
await super.fetchUtxo();
// now we need to fetch txhash for each input as required by PSBT
const txhexes = await BlueElectrum.multiGetTransactionByTxid(
this.getUtxo().map(x => x.txid),
50,
false,
);
const newUtxos = [];
for (const u of this.getUtxo()) {
if (txhexes[u.txid]) u.txhex = txhexes[u.txid];
newUtxos.push(u);
}
return newUtxos;
}
getID() {
const string2hash = [...this._cosigners].sort().join(',') + ';' + [...this._cosignersFingerprints].sort().join(',');
return createHash('sha256').update(string2hash).digest().toString('hex');
}
calculateFeeFromPsbt(psbt) {
let goesIn = 0;
const cacheUtxoAmounts = {};
for (const inp of psbt.data.inputs) {
if (inp.witnessUtxo && inp.witnessUtxo.value) {
// segwit input
goesIn += inp.witnessUtxo.value;
} else if (inp.nonWitnessUtxo) {
// non-segwit input
// lets parse this transaction and cache how much each input was worth
const inputTx = bitcoin.Transaction.fromHex(inp.nonWitnessUtxo);
let index = 0;
for (const out of inputTx.outs) {
cacheUtxoAmounts[inputTx.getId() + ':' + index] = out.value;
index++;
}
}
}
if (goesIn === 0) {
// means we failed to get amounts that go in previously, so lets use utxo amounts cache we've build
// from non-segwit inputs
for (const inp of psbt.txInputs) {
const cacheKey = reverse(inp.hash).toString('hex') + ':' + inp.index;
if (cacheUtxoAmounts[cacheKey]) goesIn += cacheUtxoAmounts[cacheKey];
}
}
let goesOut = 0;
for (const output of psbt.txOutputs) {
goesOut += output.value;
}
return goesIn - goesOut;
}
calculateHowManySignaturesWeHaveFromPsbt(psbt) {
let sigsHave = 0;
for (const inp of psbt.data.inputs) {
sigsHave = Math.max(sigsHave, inp.partialSig?.length || 0);
if (inp.finalScriptSig || inp.finalScriptWitness) sigsHave = this.getM(); // hacky, but it means we have enough
// He who knows that enough is enough will always have enough. Lao Tzu
}
return sigsHave;
}
/**
* Tries to signs passed psbt object (by reference). If there are enough signatures - tries to finalize psbt
* and returns Transaction (ready to extract hex)
*
* @param psbt {Psbt}
* @returns {{ tx: Transaction }}
*/
cosignPsbt(psbt) {
for (let cc = 0; cc < psbt.inputCount; cc++) {
for (const cosigner of this._cosigners) {
if (!MultisigHDWallet.isXpubString(cosigner)) {
// ok this is a mnemonic, lets try to sign
const seed = bip39.mnemonicToSeed(cosigner);
const hdRoot = bitcoin.bip32.fromSeed(seed);
try {
psbt.signInputHD(cc, hdRoot);
} catch (_) {} // protects agains duplicate cosignings
}
}
}
let tx = false;
if (this.calculateHowManySignaturesWeHaveFromPsbt(psbt) >= this.getM()) {
tx = psbt.finalizeAllInputs().extractTransaction();
}
return { tx };
}
}

View file

@ -9,6 +9,11 @@ export class PlaceholderWallet extends AbstractWallet {
this._isFailure = false;
}
setSecret(newSecret) {
// so TRY AGAIN when something goes wrong during import has more consistent prefilled text
this.secret = newSecret;
}
allowSend() {
return false;
}

View file

@ -120,16 +120,26 @@ export class WatchOnlyWallet extends LegacyWallet {
throw new Error('Not initialized');
}
async _getExternalAddressByIndex(index) {
_getExternalAddressByIndex(index) {
if (this._hdWalletInstance) return this._hdWalletInstance._getExternalAddressByIndex(index);
throw new Error('Not initialized');
}
_getInternalAddressByIndex(index) {
if (this._hdWalletInstance) return this._hdWalletInstance._getInternalAddressByIndex(index);
throw new Error('Not initialized');
}
getNextFreeAddressIndex() {
if (this._hdWalletInstance) return this._hdWalletInstance.next_free_address_index;
throw new Error('Not initialized');
}
getNextFreeChangeAddressIndex() {
if (this._hdWalletInstance) return this._hdWalletInstance.next_free_change_address_index;
throw new Error('Not initialized');
}
async getChangeAddressAsync() {
if (this._hdWalletInstance) return this._hdWalletInstance.getChangeAddressAsync();
throw new Error('Not initialized');

180
components/DynamicQRCode.js Normal file
View file

@ -0,0 +1,180 @@
/* eslint react/prop-types: "off", react-native/no-inline-styles: "off" */
import React, { Component } from 'react';
import { Text } from 'react-native-elements';
import { Dimensions, StyleSheet, TouchableOpacity, View } from 'react-native';
import { encodeUR } from 'bc-ur/dist';
import QRCode from 'react-native-qrcode-svg';
import { BlueCurrentTheme } from '../components/themes';
import { BlueSpacing20 } from '../BlueComponents';
import loc from '../loc';
const { height, width } = Dimensions.get('window');
export class DynamicQRCode extends Component {
constructor() {
super();
const qrCodeHeight = height > width ? width - 40 : width / 3;
const qrCodeMaxHeight = 370;
this.state = {
index: 0,
total: 0,
qrCodeHeight: Math.min(qrCodeHeight, qrCodeMaxHeight),
intervalHandler: null,
};
}
fragments = [];
componentDidMount() {
const { value, capacity = 800, hideControls = true } = this.props;
this.fragments = encodeUR(value, capacity);
this.setState(
{
total: this.fragments.length,
hideControls,
},
() => {
this.startAutoMove();
},
);
}
moveToNextFragment = () => {
const { index, total } = this.state;
if (index === total - 1) {
this.setState({
index: 0,
});
} else {
this.setState(state => ({
index: state.index + 1,
}));
}
};
startAutoMove = () => {
if (!this.state.intervalHandler)
this.setState(() => ({
intervalHandler: setInterval(this.moveToNextFragment, 500),
}));
};
stopAutoMove = () => {
clearInterval(this.state.intervalHandler);
this.setState(() => ({
intervalHandler: null,
}));
};
moveToPreviousFragment = () => {
const { index, total } = this.state;
if (index > 0) {
this.setState(state => ({
index: state.index - 1,
}));
} else {
this.setState(state => ({
index: total - 1,
}));
}
};
render() {
const currentFragment = this.fragments[this.state.index];
if (!currentFragment) {
return (
<View>
<Text>{loc.send.dynamic_init}</Text>
</View>
);
}
return (
<View style={animatedQRCodeStyle.container}>
<TouchableOpacity
style={animatedQRCodeStyle.qrcodeContainer}
onPress={() => {
this.setState(prevState => ({ hideControls: !prevState.hideControls }));
}}
>
<QRCode
value={currentFragment.toUpperCase()}
size={this.state.qrCodeHeight}
color="#000000"
logoBackgroundColor={BlueCurrentTheme.colors.brandingColor}
backgroundColor="#FFFFFF"
ecl="L"
/>
</TouchableOpacity>
{!this.state.hideControls && (
<View style={animatedQRCodeStyle.container}>
<BlueSpacing20 />
<View>
<Text style={animatedQRCodeStyle.text}>
{loc.formatString(loc._.of, { number: this.state.index + 1, total: this.state.total })}
</Text>
</View>
<BlueSpacing20 />
<View style={animatedQRCodeStyle.controller}>
<TouchableOpacity
style={[animatedQRCodeStyle.button, { width: '25%', alignItems: 'flex-start' }]}
onPress={this.moveToPreviousFragment}
>
<Text style={animatedQRCodeStyle.text}>{loc.send.dynamic_prev}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[animatedQRCodeStyle.button, { width: '50%' }]}
onPress={this.state.intervalHandler ? this.stopAutoMove : this.startAutoMove}
>
<Text style={animatedQRCodeStyle.text}>{this.state.intervalHandler ? loc.send.dynamic_stop : loc.send.dynamic_start}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[animatedQRCodeStyle.button, { width: '25%', alignItems: 'flex-end' }]}
onPress={this.moveToNextFragment}
>
<Text style={animatedQRCodeStyle.text}>{loc.send.dynamic_next}</Text>
</TouchableOpacity>
</View>
</View>
)}
</View>
);
}
}
const animatedQRCodeStyle = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
alignItems: 'center',
},
qrcodeContainer: {
alignItems: 'center',
justifyContent: 'center',
borderWidth: 6,
borderRadius: 8,
borderColor: '#FFFFFF',
margin: 6,
},
controller: {
width: '90%',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
borderRadius: 25,
height: 45,
paddingHorizontal: 18,
},
button: {
alignItems: 'center',
height: 45,
justifyContent: 'center',
},
text: {
fontSize: 14,
color: BlueCurrentTheme.colors.foregroundColor,
fontWeight: 'bold',
},
});

118
components/FloatButtons.js Normal file
View file

@ -0,0 +1,118 @@
import React, { useState, useRef } from 'react';
import PropTypes from 'prop-types';
import { View, Text, TouchableOpacity, StyleSheet, Dimensions, PixelRatio } from 'react-native';
import { BlueCurrentTheme } from './themes';
const BORDER_RADIUS = 30;
const PADDINGS = 8;
const ICON_MARGIN = 7;
const cStyles = StyleSheet.create({
root: {
position: 'absolute',
alignSelf: 'center',
height: '6.3%',
minHeight: 44,
},
rootPre: {
bottom: -1000,
},
rootPost: {
bottom: 30,
borderRadius: BORDER_RADIUS,
flexDirection: 'row',
overflow: 'hidden',
},
});
export const FContainer = ({ children }) => {
const [newWidth, setNewWidth] = useState();
const layoutCalculated = useRef(false);
const onLayout = event => {
if (layoutCalculated.current) return;
const maxWidth = Dimensions.get('window').width - BORDER_RADIUS - 20;
const { width } = event.nativeEvent.layout;
const withPaddings = Math.ceil(width + PADDINGS * 2);
const len = React.Children.toArray(children).filter(Boolean).length;
let newWidth = withPaddings * len > maxWidth ? Math.floor(maxWidth / len) : withPaddings;
if (len === 1 && newWidth < 90) newWidth = 90; // to add Paddings for lonely small button, like Scan on main screen
setNewWidth(newWidth);
layoutCalculated.current = true;
};
return (
<View onLayout={onLayout} style={[cStyles.root, newWidth ? cStyles.rootPost : cStyles.rootPre]}>
{newWidth
? React.Children.toArray(children)
.filter(Boolean)
.map((c, index, array) =>
React.cloneElement(c, {
width: newWidth,
key: index,
first: index === 0,
last: index === array.length - 1,
}),
)
: children}
</View>
);
};
FContainer.propTypes = {
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.element), PropTypes.element]),
};
const buttonFontSize =
PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26) > 22
? 22
: PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26);
const bStyles = StyleSheet.create({
root: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
backgroundColor: BlueCurrentTheme.colors.buttonBackgroundColor,
},
icon: {
alignItems: 'center',
},
text: {
color: BlueCurrentTheme.colors.buttonAlternativeTextColor,
fontSize: buttonFontSize,
fontWeight: '600',
marginLeft: ICON_MARGIN,
backgroundColor: 'transparent',
},
});
export const FButton = ({ text, icon, width, first, last, ...props }) => {
const style = {};
if (width) {
const paddingLeft = first ? BORDER_RADIUS / 2 : PADDINGS;
const paddingRight = last ? BORDER_RADIUS / 2 : PADDINGS;
style.paddingRight = paddingRight;
style.paddingLeft = paddingLeft;
style.width = width + paddingRight + paddingLeft;
}
return (
<TouchableOpacity style={[bStyles.root, style]} {...props}>
<View style={bStyles.icon}>{icon}</View>
<Text numberOfLines={1} style={bStyles.text}>
{text}
</Text>
</TouchableOpacity>
);
};
FButton.propTypes = {
text: PropTypes.string,
icon: PropTypes.element,
width: PropTypes.number,
first: PropTypes.bool,
last: PropTypes.bool,
};

View file

@ -0,0 +1,38 @@
/* eslint react/prop-types: "off", react-native/no-inline-styles: "off" */
import React from 'react';
import { TouchableOpacity, View, Text } from 'react-native';
import { Icon } from 'react-native-elements';
import { useTheme } from '@react-navigation/native';
export const SquareButton = props => {
const { colors } = useTheme();
let backgroundColor = props.backgroundColor ? props.backgroundColor : colors.buttonBlueBackgroundColor;
let fontColor = colors.buttonTextColor;
if (props.disabled === true) {
backgroundColor = colors.buttonDisabledBackgroundColor;
fontColor = colors.buttonDisabledTextColor;
}
return (
<TouchableOpacity
style={{
flex: 1,
borderWidth: 0.7,
borderColor: 'transparent',
backgroundColor: backgroundColor,
minHeight: 50,
height: 50,
maxHeight: 50,
borderRadius: 10,
justifyContent: 'center',
alignItems: 'center',
}}
{...props}
>
<View style={{ flexDirection: 'row', justifyContent: 'center', alignItems: 'center' }}>
{props.icon && <Icon name={props.icon.name} type={props.icon.type} color={props.icon.color} />}
{props.title && <Text style={{ marginHorizontal: 8, fontSize: 16, color: fontColor }}>{props.title}</Text>}
</View>
</TouchableOpacity>
);
};

View file

@ -54,6 +54,8 @@ export const BlueDefaultTheme = {
mainColor: '#CFDCF6',
success: '#ccddf9',
successCheck: '#0f5cc0',
msSuccessBG: '#37c0a1',
msSuccessCheck: '#ffffff',
},
};
@ -96,6 +98,8 @@ export const BlueDarkTheme = {
buttonBlueBackgroundColor: '#202020',
scanLabel: 'rgba(255,255,255,.2)',
labelText: '#ffffff',
msSuccessBG: '#8EFFE5',
msSuccessCheck: '#000000',
},
};

BIN
img/vault-shape.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -1,15 +1,7 @@
import React, { useEffect } from 'react';
import './shim.js';
import { AppRegistry, YellowBox } from 'react-native';
import { AppRegistry } from 'react-native';
import App from './App';
YellowBox.ignoreWarnings([
'Require cycle',
'Non-serializable values were',
"Can't perform a React state update",
'{"code":404',
'React has detected a change in the order of Hooks',
]);
const A = require('./blue_modules/analytics');
if (!Error.captureStackTrace) {

View file

@ -939,43 +939,12 @@
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${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",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Feather.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf",
"${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",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/MaterialIcons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Octicons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Zocial.ttf",
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-BlueWallet/Pods-BlueWallet-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AntDesign.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EvilIcons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Feather.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome.ttf",
"${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",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialIcons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Octicons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SimpleLineIcons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Zocial.ttf",
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-BlueWallet/Pods-BlueWallet-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
@ -1281,6 +1250,7 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 5.6.2;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@ -1320,6 +1290,7 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 5.6.2;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@ -1575,6 +1546,7 @@
CODE_SIGN_ENTITLEMENTS = "TodayExtension/BlueWallet - Bitcoin Price.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 239;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = A7W54YZ4WU;
GCC_C_LANGUAGE_STANDARD = gnu11;
@ -1611,6 +1583,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 239;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = A7W54YZ4WU;
GCC_C_LANGUAGE_STANDARD = gnu11;

View file

@ -13,6 +13,7 @@
#import "RNQuickActionManager.h"
#import <UserNotifications/UserNotifications.h>
#import <RNCPushNotificationIOS.h>
#ifdef FB_SONARKIT_ENABLED
#if DEBUG
#import <FlipperKit/FlipperClient.h>
#import <FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.h>
@ -31,13 +32,13 @@ static void InitializeFlipper(UIApplication *application) {
[client start];
}
#endif
#endif
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
#if DEBUG
#ifdef FB_SONARKIT_ENABLED
InitializeFlipper(application);
#endif

View file

@ -36,6 +36,34 @@
<string>io.bluewallet.psbt.txn</string>
</array>
</dict>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>TXT</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSItemContentTypes</key>
<array>
<string>io.bluewallet.txt</string>
</array>
</dict>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>JSON</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSItemContentTypes</key>
<array>
<string>io.bluewallet.json</string>
</array>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
@ -101,7 +129,7 @@
<key>NSCalendarsUsageDescription</key>
<string>This alert should not show up as we do not require this data</string>
<key>NSCameraUsageDescription</key>
<string>In order to quickly scan the recipient&apos;s address, we need your permission to use the camera to scan their QR Code.</string>
<string>In order to quickly scan the recipient's address, we need your permission to use the camera to scan their QR Code.</string>
<key>NSFaceIDUsageDescription</key>
<string>In order to use FaceID please confirm your permission.</string>
<key>NSLocationAlwaysUsageDescription</key>
@ -241,6 +269,44 @@
</array>
</dict>
</dict>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.plain-text</string>
</array>
<key>UTTypeDescription</key>
<string>Text File</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>io.bluewallet.txt</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>txt</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.json</string>
</array>
<key>UTTypeDescription</key>
<string>JSON File</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>io.bluewallet.json</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>json</string>
</array>
</dict>
</dict>
</array>
</dict>
</plist>

View file

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>5.6.0</string>
<string>5.6.2</string>
<key>CFBundleVersion</key>
<string>239</string>
<key>CLKComplicationPrincipalClass</key>

View file

@ -14,6 +14,7 @@ enum WalletGradient: String {
case LightningCustodial = "lightningCustodianWallet"
case SegwitNative = "HDsegwitBech32"
case WatchOnly = "watchOnly"
case MultiSig = "HDmultisig"
var imageString: String{
switch self {
@ -27,6 +28,8 @@ enum WalletGradient: String {
return "walletWatchOnly"
case .LightningCustodial:
return "walletLightningCustodial"
case .MultiSig:
return "watchMultisig"
}
}
}

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View file

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>5.6.0</string>
<string>5.6.2</string>
<key>CFBundleVersion</key>
<string>239</string>
<key>UISupportedInterfaceOrientations</key>

View file

@ -1,91 +1,18 @@
platform :ios, '10.0'
workspace 'BlueWallet'
require_relative '../node_modules/react-native/scripts/react_native_pods'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
def add_flipper_pods!(versions = {})
versions['Flipper'] ||= '~> 0.37.0'
versions['DoubleConversion'] ||= '1.1.7'
versions['Flipper-Folly'] ||= '~> 2.1'
versions['Flipper-Glog'] ||= '0.3.6'
versions['Flipper-PeerTalk'] ||= '~> 0.0.4'
versions['Flipper-RSocket'] ||= '~> 1.0'
pod 'FlipperKit', versions['Flipper'], :configuration => 'Debug'
pod 'FlipperKit/FlipperKitLayoutPlugin', versions['Flipper'], :configuration => 'Debug'
pod 'FlipperKit/SKIOSNetworkPlugin', versions['Flipper'], :configuration => 'Debug'
pod 'FlipperKit/FlipperKitUserDefaultsPlugin', versions['Flipper'], :configuration => 'Debug'
pod 'FlipperKit/FlipperKitReactPlugin', versions['Flipper'], :configuration => 'Debug'
# List all transitive dependencies for FlipperKit pods
# to avoid them being linked in Release builds
pod 'Flipper', versions['Flipper'], :configuration => 'Debug'
pod 'Flipper-DoubleConversion', versions['DoubleConversion'], :configuration => 'Debug'
pod 'Flipper-Folly', versions['Flipper-Folly'], :configuration => 'Debug'
pod 'Flipper-Glog', versions['Flipper-Glog'], :configuration => 'Debug'
pod 'Flipper-PeerTalk', versions['Flipper-PeerTalk'], :configuration => 'Debug'
pod 'Flipper-RSocket', versions['Flipper-RSocket'], :configuration => 'Debug'
pod 'FlipperKit/Core', versions['Flipper'], :configuration => 'Debug'
pod 'FlipperKit/CppBridge', versions['Flipper'], :configuration => 'Debug'
pod 'FlipperKit/FBCxxFollyDynamicConvert', versions['Flipper'], :configuration => 'Debug'
pod 'FlipperKit/FBDefines', versions['Flipper'], :configuration => 'Debug'
pod 'FlipperKit/FKPortForwarding', versions['Flipper'], :configuration => 'Debug'
pod 'FlipperKit/FlipperKitHighlightOverlay', versions['Flipper'], :configuration => 'Debug'
pod 'FlipperKit/FlipperKitLayoutTextSearchable', versions['Flipper'], :configuration => 'Debug'
pod 'FlipperKit/FlipperKitNetworkPlugin', versions['Flipper'], :configuration => 'Debug'
end
# Post Install processing for Flipper
def flipper_post_install(installer)
installer.pods_project.targets.each do |target|
if target.name == 'YogaKit'
target.build_configurations.each do |config|
config.build_settings['SWIFT_VERSION'] = '4.1'
end
end
end
end
target 'BlueWallet' do
# Pods for RnDiffApp
pod 'FBLazyVector', :path => "../node_modules/react-native/Libraries/FBLazyVector"
pod 'FBReactNativeSpec', :path => "../node_modules/react-native/Libraries/FBReactNativeSpec"
pod 'RCTRequired', :path => "../node_modules/react-native/Libraries/RCTRequired"
pod 'RCTTypeSafety', :path => "../node_modules/react-native/Libraries/TypeSafety"
pod 'React', :path => '../node_modules/react-native/'
pod 'React-Core', :path => '../node_modules/react-native/'
pod 'React-CoreModules', :path => '../node_modules/react-native/React/CoreModules'
pod 'React-Core/DevSupport', :path => '../node_modules/react-native/'
pod 'React-RCTActionSheet', :path => '../node_modules/react-native/Libraries/ActionSheetIOS'
pod 'React-RCTAnimation', :path => '../node_modules/react-native/Libraries/NativeAnimation'
pod 'React-RCTBlob', :path => '../node_modules/react-native/Libraries/Blob'
pod 'React-RCTImage', :path => '../node_modules/react-native/Libraries/Image'
pod 'React-RCTLinking', :path => '../node_modules/react-native/Libraries/LinkingIOS'
pod 'React-RCTNetwork', :path => '../node_modules/react-native/Libraries/Network'
pod 'React-RCTSettings', :path => '../node_modules/react-native/Libraries/Settings'
pod 'React-RCTText', :path => '../node_modules/react-native/Libraries/Text'
pod 'React-RCTVibration', :path => '../node_modules/react-native/Libraries/Vibration'
pod 'React-Core/RCTWebSocket', :path => '../node_modules/react-native/'
config = use_native_modules!
pod 'React-cxxreact', :path => '../node_modules/react-native/ReactCommon/cxxreact'
pod 'React-jsi', :path => '../node_modules/react-native/ReactCommon/jsi'
pod 'React-jsiexecutor', :path => '../node_modules/react-native/ReactCommon/jsiexecutor'
pod 'React-jsinspector', :path => '../node_modules/react-native/ReactCommon/jsinspector'
pod 'ReactCommon/callinvoker', :path => "../node_modules/react-native/ReactCommon"
pod 'ReactCommon/turbomodule/core', :path => "../node_modules/react-native/ReactCommon"
pod 'Yoga', :path => '../node_modules/react-native/ReactCommon/yoga', :modular_headers => true
pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec'
pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec'
pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'
use_native_modules!
use_react_native!(:path => config["reactNativePath"])
# Enables Flipper.
#
# Note that if you have use_frameworks! enabled, Flipper will not work and
# you should disable these next few lines.
add_flipper_pods!
use_flipper!
post_install do |installer|
flipper_post_install(installer)
end

View file

@ -5,15 +5,15 @@ PODS:
- CocoaAsyncSocket (7.6.4)
- CocoaLibEvent (1.0.0)
- DoubleConversion (1.1.6)
- FBLazyVector (0.62.2)
- FBReactNativeSpec (0.62.2):
- Folly (= 2018.10.22.00)
- RCTRequired (= 0.62.2)
- RCTTypeSafety (= 0.62.2)
- React-Core (= 0.62.2)
- React-jsi (= 0.62.2)
- ReactCommon/turbomodule/core (= 0.62.2)
- Flipper (0.37.0):
- FBLazyVector (0.63.3)
- FBReactNativeSpec (0.63.3):
- Folly (= 2020.01.13.00)
- RCTRequired (= 0.63.3)
- RCTTypeSafety (= 0.63.3)
- React-Core (= 0.63.3)
- React-jsi (= 0.63.3)
- ReactCommon/turbomodule/core (= 0.63.3)
- Flipper (0.54.0):
- Flipper-Folly (~> 2.2)
- Flipper-RSocket (~> 1.1)
- Flipper-DoubleConversion (1.1.7)
@ -27,44 +27,44 @@ PODS:
- Flipper-PeerTalk (0.0.4)
- Flipper-RSocket (1.1.0):
- Flipper-Folly (~> 2.2)
- FlipperKit (0.37.0):
- FlipperKit/Core (= 0.37.0)
- FlipperKit/Core (0.37.0):
- Flipper (~> 0.37.0)
- FlipperKit (0.54.0):
- FlipperKit/Core (= 0.54.0)
- FlipperKit/Core (0.54.0):
- Flipper (~> 0.54.0)
- FlipperKit/CppBridge
- FlipperKit/FBCxxFollyDynamicConvert
- FlipperKit/FBDefines
- FlipperKit/FKPortForwarding
- FlipperKit/CppBridge (0.37.0):
- Flipper (~> 0.37.0)
- FlipperKit/FBCxxFollyDynamicConvert (0.37.0):
- FlipperKit/CppBridge (0.54.0):
- Flipper (~> 0.54.0)
- FlipperKit/FBCxxFollyDynamicConvert (0.54.0):
- Flipper-Folly (~> 2.2)
- FlipperKit/FBDefines (0.37.0)
- FlipperKit/FKPortForwarding (0.37.0):
- FlipperKit/FBDefines (0.54.0)
- FlipperKit/FKPortForwarding (0.54.0):
- CocoaAsyncSocket (~> 7.6)
- Flipper-PeerTalk (~> 0.0.4)
- FlipperKit/FlipperKitHighlightOverlay (0.37.0)
- FlipperKit/FlipperKitLayoutPlugin (0.37.0):
- FlipperKit/FlipperKitHighlightOverlay (0.54.0)
- FlipperKit/FlipperKitLayoutPlugin (0.54.0):
- FlipperKit/Core
- FlipperKit/FlipperKitHighlightOverlay
- FlipperKit/FlipperKitLayoutTextSearchable
- YogaKit (~> 1.18)
- FlipperKit/FlipperKitLayoutTextSearchable (0.37.0)
- FlipperKit/FlipperKitNetworkPlugin (0.37.0):
- FlipperKit/FlipperKitLayoutTextSearchable (0.54.0)
- FlipperKit/FlipperKitNetworkPlugin (0.54.0):
- FlipperKit/Core
- FlipperKit/FlipperKitReactPlugin (0.37.0):
- FlipperKit/FlipperKitReactPlugin (0.54.0):
- FlipperKit/Core
- FlipperKit/FlipperKitUserDefaultsPlugin (0.37.0):
- FlipperKit/FlipperKitUserDefaultsPlugin (0.54.0):
- FlipperKit/Core
- FlipperKit/SKIOSNetworkPlugin (0.37.0):
- FlipperKit/SKIOSNetworkPlugin (0.54.0):
- FlipperKit/Core
- FlipperKit/FlipperKitNetworkPlugin
- Folly (2018.10.22.00):
- Folly (2020.01.13.00):
- boost-for-react-native
- DoubleConversion
- Folly/Default (= 2018.10.22.00)
- Folly/Default (= 2020.01.13.00)
- glog
- Folly/Default (2018.10.22.00):
- Folly/Default (2020.01.13.00):
- boost-for-react-native
- DoubleConversion
- glog
@ -81,169 +81,172 @@ PODS:
- OpenSSL-Universal/Static (1.0.2.19)
- PasscodeAuth (1.0.0):
- React
- RCTRequired (0.62.2)
- RCTTypeSafety (0.62.2):
- FBLazyVector (= 0.62.2)
- Folly (= 2018.10.22.00)
- RCTRequired (= 0.62.2)
- React-Core (= 0.62.2)
- React (0.62.2):
- React-Core (= 0.62.2)
- React-Core/DevSupport (= 0.62.2)
- React-Core/RCTWebSocket (= 0.62.2)
- React-RCTActionSheet (= 0.62.2)
- React-RCTAnimation (= 0.62.2)
- React-RCTBlob (= 0.62.2)
- React-RCTImage (= 0.62.2)
- React-RCTLinking (= 0.62.2)
- React-RCTNetwork (= 0.62.2)
- React-RCTSettings (= 0.62.2)
- React-RCTText (= 0.62.2)
- React-RCTVibration (= 0.62.2)
- React-Core (0.62.2):
- Folly (= 2018.10.22.00)
- RCTRequired (0.63.3)
- RCTTypeSafety (0.63.3):
- FBLazyVector (= 0.63.3)
- Folly (= 2020.01.13.00)
- RCTRequired (= 0.63.3)
- React-Core (= 0.63.3)
- React (0.63.3):
- React-Core (= 0.63.3)
- React-Core/DevSupport (= 0.63.3)
- React-Core/RCTWebSocket (= 0.63.3)
- React-RCTActionSheet (= 0.63.3)
- React-RCTAnimation (= 0.63.3)
- React-RCTBlob (= 0.63.3)
- React-RCTImage (= 0.63.3)
- React-RCTLinking (= 0.63.3)
- React-RCTNetwork (= 0.63.3)
- React-RCTSettings (= 0.63.3)
- React-RCTText (= 0.63.3)
- React-RCTVibration (= 0.63.3)
- React-callinvoker (0.63.3)
- React-Core (0.63.3):
- Folly (= 2020.01.13.00)
- glog
- React-Core/Default (= 0.62.2)
- React-cxxreact (= 0.62.2)
- React-jsi (= 0.62.2)
- React-jsiexecutor (= 0.62.2)
- React-Core/Default (= 0.63.3)
- React-cxxreact (= 0.63.3)
- React-jsi (= 0.63.3)
- React-jsiexecutor (= 0.63.3)
- Yoga
- React-Core/CoreModulesHeaders (0.62.2):
- Folly (= 2018.10.22.00)
- React-Core/CoreModulesHeaders (0.63.3):
- Folly (= 2020.01.13.00)
- glog
- React-Core/Default
- React-cxxreact (= 0.62.2)
- React-jsi (= 0.62.2)
- React-jsiexecutor (= 0.62.2)
- React-cxxreact (= 0.63.3)
- React-jsi (= 0.63.3)
- React-jsiexecutor (= 0.63.3)
- Yoga
- React-Core/Default (0.62.2):
- Folly (= 2018.10.22.00)
- React-Core/Default (0.63.3):
- Folly (= 2020.01.13.00)
- glog
- React-cxxreact (= 0.62.2)
- React-jsi (= 0.62.2)
- React-jsiexecutor (= 0.62.2)
- React-cxxreact (= 0.63.3)
- React-jsi (= 0.63.3)
- React-jsiexecutor (= 0.63.3)
- Yoga
- React-Core/DevSupport (0.62.2):
- Folly (= 2018.10.22.00)
- React-Core/DevSupport (0.63.3):
- Folly (= 2020.01.13.00)
- glog
- React-Core/Default (= 0.62.2)
- React-Core/RCTWebSocket (= 0.62.2)
- React-cxxreact (= 0.62.2)
- React-jsi (= 0.62.2)
- React-jsiexecutor (= 0.62.2)
- React-jsinspector (= 0.62.2)
- React-Core/Default (= 0.63.3)
- React-Core/RCTWebSocket (= 0.63.3)
- React-cxxreact (= 0.63.3)
- React-jsi (= 0.63.3)
- React-jsiexecutor (= 0.63.3)
- React-jsinspector (= 0.63.3)
- Yoga
- React-Core/RCTActionSheetHeaders (0.62.2):
- Folly (= 2018.10.22.00)
- React-Core/RCTActionSheetHeaders (0.63.3):
- Folly (= 2020.01.13.00)
- glog
- React-Core/Default
- React-cxxreact (= 0.62.2)
- React-jsi (= 0.62.2)
- React-jsiexecutor (= 0.62.2)
- React-cxxreact (= 0.63.3)
- React-jsi (= 0.63.3)
- React-jsiexecutor (= 0.63.3)
- Yoga
- React-Core/RCTAnimationHeaders (0.62.2):
- Folly (= 2018.10.22.00)
- React-Core/RCTAnimationHeaders (0.63.3):
- Folly (= 2020.01.13.00)
- glog
- React-Core/Default
- React-cxxreact (= 0.62.2)
- React-jsi (= 0.62.2)
- React-jsiexecutor (= 0.62.2)
- React-cxxreact (= 0.63.3)
- React-jsi (= 0.63.3)
- React-jsiexecutor (= 0.63.3)
- Yoga
- React-Core/RCTBlobHeaders (0.62.2):
- Folly (= 2018.10.22.00)
- React-Core/RCTBlobHeaders (0.63.3):
- Folly (= 2020.01.13.00)
- glog
- React-Core/Default
- React-cxxreact (= 0.62.2)
- React-jsi (= 0.62.2)
- React-jsiexecutor (= 0.62.2)
- React-cxxreact (= 0.63.3)
- React-jsi (= 0.63.3)
- React-jsiexecutor (= 0.63.3)
- Yoga
- React-Core/RCTImageHeaders (0.62.2):
- Folly (= 2018.10.22.00)
- React-Core/RCTImageHeaders (0.63.3):
- Folly (= 2020.01.13.00)
- glog
- React-Core/Default
- React-cxxreact (= 0.62.2)
- React-jsi (= 0.62.2)
- React-jsiexecutor (= 0.62.2)
- React-cxxreact (= 0.63.3)
- React-jsi (= 0.63.3)
- React-jsiexecutor (= 0.63.3)
- Yoga
- React-Core/RCTLinkingHeaders (0.62.2):
- Folly (= 2018.10.22.00)
- React-Core/RCTLinkingHeaders (0.63.3):
- Folly (= 2020.01.13.00)
- glog
- React-Core/Default
- React-cxxreact (= 0.62.2)
- React-jsi (= 0.62.2)
- React-jsiexecutor (= 0.62.2)
- React-cxxreact (= 0.63.3)
- React-jsi (= 0.63.3)
- React-jsiexecutor (= 0.63.3)
- Yoga
- React-Core/RCTNetworkHeaders (0.62.2):
- Folly (= 2018.10.22.00)
- React-Core/RCTNetworkHeaders (0.63.3):
- Folly (= 2020.01.13.00)
- glog
- React-Core/Default
- React-cxxreact (= 0.62.2)
- React-jsi (= 0.62.2)
- React-jsiexecutor (= 0.62.2)
- React-cxxreact (= 0.63.3)
- React-jsi (= 0.63.3)
- React-jsiexecutor (= 0.63.3)
- Yoga
- React-Core/RCTSettingsHeaders (0.62.2):
- Folly (= 2018.10.22.00)
- React-Core/RCTSettingsHeaders (0.63.3):
- Folly (= 2020.01.13.00)
- glog
- React-Core/Default
- React-cxxreact (= 0.62.2)
- React-jsi (= 0.62.2)
- React-jsiexecutor (= 0.62.2)
- React-cxxreact (= 0.63.3)
- React-jsi (= 0.63.3)
- React-jsiexecutor (= 0.63.3)
- Yoga
- React-Core/RCTTextHeaders (0.62.2):
- Folly (= 2018.10.22.00)
- React-Core/RCTTextHeaders (0.63.3):
- Folly (= 2020.01.13.00)
- glog
- React-Core/Default
- React-cxxreact (= 0.62.2)
- React-jsi (= 0.62.2)
- React-jsiexecutor (= 0.62.2)
- React-cxxreact (= 0.63.3)
- React-jsi (= 0.63.3)
- React-jsiexecutor (= 0.63.3)
- Yoga
- React-Core/RCTVibrationHeaders (0.62.2):
- Folly (= 2018.10.22.00)
- React-Core/RCTVibrationHeaders (0.63.3):
- Folly (= 2020.01.13.00)
- glog
- React-Core/Default
- React-cxxreact (= 0.62.2)
- React-jsi (= 0.62.2)
- React-jsiexecutor (= 0.62.2)
- React-cxxreact (= 0.63.3)
- React-jsi (= 0.63.3)
- React-jsiexecutor (= 0.63.3)
- Yoga
- React-Core/RCTWebSocket (0.62.2):
- Folly (= 2018.10.22.00)
- React-Core/RCTWebSocket (0.63.3):
- Folly (= 2020.01.13.00)
- glog
- React-Core/Default (= 0.62.2)
- React-cxxreact (= 0.62.2)
- React-jsi (= 0.62.2)
- React-jsiexecutor (= 0.62.2)
- React-Core/Default (= 0.63.3)
- React-cxxreact (= 0.63.3)
- React-jsi (= 0.63.3)
- React-jsiexecutor (= 0.63.3)
- Yoga
- React-CoreModules (0.62.2):
- FBReactNativeSpec (= 0.62.2)
- Folly (= 2018.10.22.00)
- RCTTypeSafety (= 0.62.2)
- React-Core/CoreModulesHeaders (= 0.62.2)
- React-RCTImage (= 0.62.2)
- ReactCommon/turbomodule/core (= 0.62.2)
- React-cxxreact (0.62.2):
- React-CoreModules (0.63.3):
- FBReactNativeSpec (= 0.63.3)
- Folly (= 2020.01.13.00)
- RCTTypeSafety (= 0.63.3)
- React-Core/CoreModulesHeaders (= 0.63.3)
- React-jsi (= 0.63.3)
- React-RCTImage (= 0.63.3)
- ReactCommon/turbomodule/core (= 0.63.3)
- React-cxxreact (0.63.3):
- boost-for-react-native (= 1.63.0)
- DoubleConversion
- Folly (= 2018.10.22.00)
- Folly (= 2020.01.13.00)
- glog
- React-jsinspector (= 0.62.2)
- React-jsi (0.62.2):
- React-callinvoker (= 0.63.3)
- React-jsinspector (= 0.63.3)
- React-jsi (0.63.3):
- boost-for-react-native (= 1.63.0)
- DoubleConversion
- Folly (= 2018.10.22.00)
- Folly (= 2020.01.13.00)
- glog
- React-jsi/Default (= 0.62.2)
- React-jsi/Default (0.62.2):
- React-jsi/Default (= 0.63.3)
- React-jsi/Default (0.63.3):
- boost-for-react-native (= 1.63.0)
- DoubleConversion
- Folly (= 2018.10.22.00)
- Folly (= 2020.01.13.00)
- glog
- React-jsiexecutor (0.62.2):
- React-jsiexecutor (0.63.3):
- DoubleConversion
- Folly (= 2018.10.22.00)
- Folly (= 2020.01.13.00)
- glog
- React-cxxreact (= 0.62.2)
- React-jsi (= 0.62.2)
- React-jsinspector (0.62.2)
- React-cxxreact (= 0.63.3)
- React-jsi (= 0.63.3)
- React-jsinspector (0.63.3)
- react-native-blue-crypto (1.0.0):
- React
- react-native-blur (0.8.0):
@ -262,8 +265,8 @@ PODS:
- React
- react-native-geolocation (2.0.2):
- React
- react-native-image-picker (2.3.3):
- React
- react-native-image-picker (2.3.4):
- React-Core
- react-native-randombytes (3.5.3):
- React
- react-native-safe-area-context (3.1.8):
@ -273,67 +276,68 @@ PODS:
- react-native-tcp-socket (3.7.1):
- CocoaAsyncSocket
- React
- react-native-webview (10.8.3):
- React
- React-RCTActionSheet (0.62.2):
- React-Core/RCTActionSheetHeaders (= 0.62.2)
- React-RCTAnimation (0.62.2):
- FBReactNativeSpec (= 0.62.2)
- Folly (= 2018.10.22.00)
- RCTTypeSafety (= 0.62.2)
- React-Core/RCTAnimationHeaders (= 0.62.2)
- ReactCommon/turbomodule/core (= 0.62.2)
- React-RCTBlob (0.62.2):
- FBReactNativeSpec (= 0.62.2)
- Folly (= 2018.10.22.00)
- React-Core/RCTBlobHeaders (= 0.62.2)
- React-Core/RCTWebSocket (= 0.62.2)
- React-jsi (= 0.62.2)
- React-RCTNetwork (= 0.62.2)
- ReactCommon/turbomodule/core (= 0.62.2)
- React-RCTImage (0.62.2):
- FBReactNativeSpec (= 0.62.2)
- Folly (= 2018.10.22.00)
- RCTTypeSafety (= 0.62.2)
- React-Core/RCTImageHeaders (= 0.62.2)
- React-RCTNetwork (= 0.62.2)
- ReactCommon/turbomodule/core (= 0.62.2)
- React-RCTLinking (0.62.2):
- FBReactNativeSpec (= 0.62.2)
- React-Core/RCTLinkingHeaders (= 0.62.2)
- ReactCommon/turbomodule/core (= 0.62.2)
- React-RCTNetwork (0.62.2):
- FBReactNativeSpec (= 0.62.2)
- Folly (= 2018.10.22.00)
- RCTTypeSafety (= 0.62.2)
- React-Core/RCTNetworkHeaders (= 0.62.2)
- ReactCommon/turbomodule/core (= 0.62.2)
- React-RCTSettings (0.62.2):
- FBReactNativeSpec (= 0.62.2)
- Folly (= 2018.10.22.00)
- RCTTypeSafety (= 0.62.2)
- React-Core/RCTSettingsHeaders (= 0.62.2)
- ReactCommon/turbomodule/core (= 0.62.2)
- React-RCTText (0.62.2):
- React-Core/RCTTextHeaders (= 0.62.2)
- React-RCTVibration (0.62.2):
- FBReactNativeSpec (= 0.62.2)
- Folly (= 2018.10.22.00)
- React-Core/RCTVibrationHeaders (= 0.62.2)
- ReactCommon/turbomodule/core (= 0.62.2)
- ReactCommon/callinvoker (0.62.2):
- react-native-webview (10.9.2):
- React-Core
- React-RCTActionSheet (0.63.3):
- React-Core/RCTActionSheetHeaders (= 0.63.3)
- React-RCTAnimation (0.63.3):
- FBReactNativeSpec (= 0.63.3)
- Folly (= 2020.01.13.00)
- RCTTypeSafety (= 0.63.3)
- React-Core/RCTAnimationHeaders (= 0.63.3)
- React-jsi (= 0.63.3)
- ReactCommon/turbomodule/core (= 0.63.3)
- React-RCTBlob (0.63.3):
- FBReactNativeSpec (= 0.63.3)
- Folly (= 2020.01.13.00)
- React-Core/RCTBlobHeaders (= 0.63.3)
- React-Core/RCTWebSocket (= 0.63.3)
- React-jsi (= 0.63.3)
- React-RCTNetwork (= 0.63.3)
- ReactCommon/turbomodule/core (= 0.63.3)
- React-RCTImage (0.63.3):
- FBReactNativeSpec (= 0.63.3)
- Folly (= 2020.01.13.00)
- RCTTypeSafety (= 0.63.3)
- React-Core/RCTImageHeaders (= 0.63.3)
- React-jsi (= 0.63.3)
- React-RCTNetwork (= 0.63.3)
- ReactCommon/turbomodule/core (= 0.63.3)
- React-RCTLinking (0.63.3):
- FBReactNativeSpec (= 0.63.3)
- React-Core/RCTLinkingHeaders (= 0.63.3)
- React-jsi (= 0.63.3)
- ReactCommon/turbomodule/core (= 0.63.3)
- React-RCTNetwork (0.63.3):
- FBReactNativeSpec (= 0.63.3)
- Folly (= 2020.01.13.00)
- RCTTypeSafety (= 0.63.3)
- React-Core/RCTNetworkHeaders (= 0.63.3)
- React-jsi (= 0.63.3)
- ReactCommon/turbomodule/core (= 0.63.3)
- React-RCTSettings (0.63.3):
- FBReactNativeSpec (= 0.63.3)
- Folly (= 2020.01.13.00)
- RCTTypeSafety (= 0.63.3)
- React-Core/RCTSettingsHeaders (= 0.63.3)
- React-jsi (= 0.63.3)
- ReactCommon/turbomodule/core (= 0.63.3)
- React-RCTText (0.63.3):
- React-Core/RCTTextHeaders (= 0.63.3)
- React-RCTVibration (0.63.3):
- FBReactNativeSpec (= 0.63.3)
- Folly (= 2020.01.13.00)
- React-Core/RCTVibrationHeaders (= 0.63.3)
- React-jsi (= 0.63.3)
- ReactCommon/turbomodule/core (= 0.63.3)
- ReactCommon/turbomodule/core (0.63.3):
- DoubleConversion
- Folly (= 2018.10.22.00)
- Folly (= 2020.01.13.00)
- glog
- React-cxxreact (= 0.62.2)
- ReactCommon/turbomodule/core (0.62.2):
- DoubleConversion
- Folly (= 2018.10.22.00)
- glog
- React-Core (= 0.62.2)
- React-cxxreact (= 0.62.2)
- React-jsi (= 0.62.2)
- ReactCommon/callinvoker (= 0.62.2)
- React-callinvoker (= 0.63.3)
- React-Core (= 0.63.3)
- React-cxxreact (= 0.63.3)
- React-jsi (= 0.63.3)
- ReactNativePrivacySnapshot (1.0.0):
- React
- RealmJS (6.1.0):
@ -341,28 +345,28 @@ PODS:
- React
- RemobileReactNativeQrcodeLocalImage (1.0.4):
- React
- RNCAsyncStorage (1.12.0):
- React
- RNCClipboard (1.2.3):
- React
- RNCAsyncStorage (1.12.1):
- React-Core
- RNCClipboard (1.4.0):
- React-Core
- RNCMaskedView (0.1.10):
- React
- RNCPushNotificationIOS (1.5.0):
- React
- RNDefaultPreference (1.4.3):
- React
- RNDeviceInfo (6.0.3):
- RNDeviceInfo (6.2.0):
- React-Core
- RNFS (2.16.6):
- React
- RNGestureHandler (1.7.0):
- RNGestureHandler (1.8.0):
- React
- RNHandoff (0.0.3):
- React
- RNInAppBrowser (3.4.0):
- React
- RNLocalize (1.4.0):
- React
- RNLocalize (1.4.2):
- React-Core
- RNQuickAction (0.3.13):
- React
- RNRate (1.2.4):
@ -375,11 +379,11 @@ PODS:
- React
- RNSecureKeyStore (1.0.0):
- React
- RNSentry (1.8.0):
- RNSentry (1.8.2):
- React
- Sentry (~> 5.2.0)
- RNShare (3.7.0):
- React
- RNShare (4.0.2):
- React-Core
- RNSVG (12.1.0):
- React
- RNVectorIcons (6.6.0):
@ -400,25 +404,25 @@ DEPENDENCIES:
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
- FBReactNativeSpec (from `../node_modules/react-native/Libraries/FBReactNativeSpec`)
- Flipper (~> 0.37.0)
- Flipper (~> 0.54.0)
- Flipper-DoubleConversion (= 1.1.7)
- Flipper-Folly (~> 2.1)
- Flipper-Folly (~> 2.2)
- Flipper-Glog (= 0.3.6)
- Flipper-PeerTalk (~> 0.0.4)
- Flipper-RSocket (~> 1.0)
- FlipperKit (~> 0.37.0)
- FlipperKit/Core (~> 0.37.0)
- FlipperKit/CppBridge (~> 0.37.0)
- FlipperKit/FBCxxFollyDynamicConvert (~> 0.37.0)
- FlipperKit/FBDefines (~> 0.37.0)
- FlipperKit/FKPortForwarding (~> 0.37.0)
- FlipperKit/FlipperKitHighlightOverlay (~> 0.37.0)
- FlipperKit/FlipperKitLayoutPlugin (~> 0.37.0)
- FlipperKit/FlipperKitLayoutTextSearchable (~> 0.37.0)
- FlipperKit/FlipperKitNetworkPlugin (~> 0.37.0)
- FlipperKit/FlipperKitReactPlugin (~> 0.37.0)
- FlipperKit/FlipperKitUserDefaultsPlugin (~> 0.37.0)
- FlipperKit/SKIOSNetworkPlugin (~> 0.37.0)
- Flipper-RSocket (~> 1.1)
- FlipperKit (~> 0.54.0)
- FlipperKit/Core (~> 0.54.0)
- FlipperKit/CppBridge (~> 0.54.0)
- FlipperKit/FBCxxFollyDynamicConvert (~> 0.54.0)
- FlipperKit/FBDefines (~> 0.54.0)
- FlipperKit/FKPortForwarding (~> 0.54.0)
- FlipperKit/FlipperKitHighlightOverlay (~> 0.54.0)
- FlipperKit/FlipperKitLayoutPlugin (~> 0.54.0)
- FlipperKit/FlipperKitLayoutTextSearchable (~> 0.54.0)
- FlipperKit/FlipperKitNetworkPlugin (~> 0.54.0)
- FlipperKit/FlipperKitReactPlugin (~> 0.54.0)
- FlipperKit/FlipperKitUserDefaultsPlugin (~> 0.54.0)
- FlipperKit/SKIOSNetworkPlugin (~> 0.54.0)
- Folly (from `../node_modules/react-native/third-party-podspecs/Folly.podspec`)
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
- lottie-ios (from `../node_modules/lottie-ios`)
@ -427,6 +431,7 @@ DEPENDENCIES:
- RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`)
- RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`)
- React (from `../node_modules/react-native/`)
- React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`)
- React-Core (from `../node_modules/react-native/`)
- React-Core/DevSupport (from `../node_modules/react-native/`)
- React-Core/RCTWebSocket (from `../node_modules/react-native/`)
@ -456,7 +461,6 @@ DEPENDENCIES:
- React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`)
- React-RCTText (from `../node_modules/react-native/Libraries/Text`)
- React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`)
- ReactCommon/callinvoker (from `../node_modules/react-native/ReactCommon`)
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- ReactNativePrivacySnapshot (from `../node_modules/react-native-privacy-snapshot`)
- RealmJS (from `../node_modules/realm`)
@ -528,6 +532,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/Libraries/TypeSafety"
React:
:path: "../node_modules/react-native/"
React-callinvoker:
:path: "../node_modules/react-native/ReactCommon/callinvoker"
React-Core:
:path: "../node_modules/react-native/"
React-CoreModules:
@ -644,84 +650,85 @@ SPEC CHECKSUMS:
BVLinearGradient: e3aad03778a456d77928f594a649e96995f1c872
CocoaAsyncSocket: 694058e7c0ed05a9e217d1b3c7ded962f4180845
CocoaLibEvent: 2fab71b8bd46dd33ddb959f7928ec5909f838e3f
DoubleConversion: 5805e889d232975c086db112ece9ed034df7a0b2
FBLazyVector: 4aab18c93cd9546e4bfed752b4084585eca8b245
FBReactNativeSpec: 5465d51ccfeecb7faa12f9ae0024f2044ce4044e
Flipper: 1670db365568191bd123a0c905b834e77ba9e3d3
DoubleConversion: cde416483dac037923206447da6e1454df403714
FBLazyVector: 878b59e31113e289e275165efbe4b54fa614d43d
FBReactNativeSpec: 7da9338acfb98d4ef9e5536805a0704572d33c2f
Flipper: be611d4b742d8c87fbae2ca5f44603a02539e365
Flipper-DoubleConversion: 38631e41ef4f9b12861c67d17cb5518d06badc41
Flipper-Folly: c12092ea368353b58e992843a990a3225d4533c3
Flipper-Glog: 1dfd6abf1e922806c52ceb8701a3599a79a200a6
Flipper-PeerTalk: 116d8f857dc6ef55c7a5a75ea3ceaafe878aadc9
Flipper-RSocket: 64e7431a55835eb953b0bf984ef3b90ae9fdddd7
FlipperKit: afd4259ef9eadeeb2d30250b37d95cb3b6b97a69
Folly: 30e7936e1c45c08d884aa59369ed951a8e68cf51
FlipperKit: ab353d41aea8aae2ea6daaf813e67496642f3d7d
Folly: b73c3869541e86821df3c387eb0af5f65addfab4
GCDWebServer: 2c156a56c8226e2d5c0c3f208a3621ccffbe3ce4
glog: 1f3da668190260b06b429bb211bfbee5cd790c28
glog: 40a13f7840415b9a77023fbcae0f1e6f43192af3
lottie-ios: 48fac6be217c76937e36e340e2d09cf7b10b7f5f
lottie-react-native: 1fb4ce21d6ad37dab8343eaff8719df76035bd93
OpenSSL-Universal: 8b48cc0d10c1b2923617dfe5c178aa9ed2689355
PasscodeAuth: 1cc99b13d8e4de4716d7e2b4069af2f1a9de30b2
RCTRequired: cec6a34b3ac8a9915c37e7e4ad3aa74726ce4035
RCTTypeSafety: 93006131180074cffa227a1075802c89a49dd4ce
React: 29a8b1a02bd764fb7644ef04019270849b9a7ac3
React-Core: b12bffb3f567fdf99510acb716ef1abd426e0e05
React-CoreModules: 4a9b87bbe669d6c3173c0132c3328e3b000783d0
React-cxxreact: e65f9c2ba0ac5be946f53548c1aaaee5873a8103
React-jsi: b6dc94a6a12ff98e8877287a0b7620d365201161
React-jsiexecutor: 1540d1c01bb493ae3124ed83351b1b6a155db7da
React-jsinspector: 512e560d0e985d0e8c479a54a4e5c147a9c83493
RCTRequired: 48884c74035a0b5b76dbb7a998bd93bcfc5f2047
RCTTypeSafety: edf4b618033c2f1c5b7bc3d90d8e085ed95ba2ab
React: f36e90f3ceb976546e97df3403e37d226f79d0e3
React-callinvoker: 18874f621eb96625df7a24a7dc8d6e07391affcd
React-Core: ac3d816b8e3493970153f4aaf0cff18af0bb95e6
React-CoreModules: 4016d3a4e518bcfc4f5a51252b5a05692ca6f0e1
React-cxxreact: ffc9129013b87cb36cf3f30a86695a3c397b0f99
React-jsi: df07aa95b39c5be3e41199921509bfa929ed2b9d
React-jsiexecutor: b56c03e61c0dd5f5801255f2160a815f4a53d451
React-jsinspector: 8e68ffbfe23880d3ee9bafa8be2777f60b25cbe2
react-native-blue-crypto: 23f1558ad3d38d7a2edb7e2f6ed1bc520ed93e56
react-native-blur: cad4d93b364f91e7b7931b3fa935455487e5c33c
react-native-camera: fc1296181b2d393ea698164869070c96d8625129
react-native-document-picker: c5752781fbc0c126c627c1549b037c139444a4cf
react-native-fingerprint-scanner: c68136ca57e3704d7bdf5faa554ea535ce15b1d0
react-native-geolocation: cbd9d6bd06bac411eed2671810f454d4908484a8
react-native-image-picker: a6c3d644751a388b0fc8b56822ff7cbd398a3008
react-native-image-picker: 32d1ad2c0024ca36161ae0d5c2117e2d6c441f11
react-native-randombytes: 991545e6eaaf700b4ee384c291ef3d572e0b2ca8
react-native-safe-area-context: 01158a92c300895d79dee447e980672dc3fb85a6
react-native-slider: b733e17fdd31186707146debf1f04b5d94aa1a93
react-native-tcp-socket: 96a4f104cdcc9c6621aafe92937f163d88447c5b
react-native-webview: 162c2f2b14555cb524ac0e3b422a9b66ebceefee
React-RCTActionSheet: f41ea8a811aac770e0cc6e0ad6b270c644ea8b7c
React-RCTAnimation: 49ab98b1c1ff4445148b72a3d61554138565bad0
React-RCTBlob: a332773f0ebc413a0ce85942a55b064471587a71
React-RCTImage: e70be9b9c74fe4e42d0005f42cace7981c994ac3
React-RCTLinking: c1b9739a88d56ecbec23b7f63650e44672ab2ad2
React-RCTNetwork: 73138b6f45e5a2768ad93f3d57873c2a18d14b44
React-RCTSettings: 6e3738a87e21b39a8cb08d627e68c44acf1e325a
React-RCTText: fae545b10cfdb3d247c36c56f61a94cfd6dba41d
React-RCTVibration: 4356114dbcba4ce66991096e51a66e61eda51256
ReactCommon: ed4e11d27609d571e7eee8b65548efc191116eb3
react-native-webview: c51f73be304c61d359ec3e7c5e4e8f2c977fd360
React-RCTActionSheet: 53ea72699698b0b47a6421cb1c8b4ab215a774aa
React-RCTAnimation: 1befece0b5183c22ae01b966f5583f42e69a83c2
React-RCTBlob: 0b284339cbe4b15705a05e2313a51c6d8b51fa40
React-RCTImage: d1756599ebd4dc2cb19d1682fe67c6b976658387
React-RCTLinking: 9af0a51c6d6a4dd1674daadafffc6d03033a6d18
React-RCTNetwork: 332c83929cc5eae0b3bbca4add1d668e1fc18bda
React-RCTSettings: d6953772cfd55f2c68ad72b7ef29efc7ec49f773
React-RCTText: 65a6de06a7389098ce24340d1d3556015c38f746
React-RCTVibration: 8e9fb25724a0805107fc1acc9075e26f814df454
ReactCommon: 4167844018c9ed375cc01a843e9ee564399e53c3
ReactNativePrivacySnapshot: cc295e45dc22810e9ff2c93380d643de20a77015
RealmJS: c8645e0d65b676780f7e6c393d327527a2eb15e8
RemobileReactNativeQrcodeLocalImage: 57aadc12896b148fb5e04bc7c6805f3565f5c3fa
RNCAsyncStorage: 3eea36d9460c5159b592f9ecbe5a77f8aca98006
RNCClipboard: 5f3218dcdc28405aa2ae72b78e388f150b826dd3
RNCAsyncStorage: cb9a623793918c6699586281f0b51cbc38f046f9
RNCClipboard: ce9b77f2881948e9e04af84bd70262ab37adb1c1
RNCMaskedView: f5c7d14d6847b7b44853f7acb6284c1da30a3459
RNCPushNotificationIOS: 8025ff0b610d7b28d29ddc1b619cd55814362e4c
RNDefaultPreference: 21816c0a6f61a2829ccc0cef034392e9b509ee5f
RNDeviceInfo: 910ef2129aff229d8ebd1214bc5f0148017b51d1
RNDeviceInfo: 980848feea8d74412b16f2e3e8758c8294d63ca2
RNFS: 2bd9eb49dc82fa9676382f0585b992c424cd59df
RNGestureHandler: b6b359bb800ae399a9c8b27032bdbf7c18f08a08
RNGestureHandler: 7a5833d0f788dbd107fbb913e09aa0c1ff333c39
RNHandoff: d3b0754cca3a6bcd9b25f544f733f7f033ccf5fa
RNInAppBrowser: 6097fbc6b09051b40a6a9ec22caf7af40b115ec0
RNLocalize: fc27ee5878ce5a3af73873fb2d8e866e0d1e6d84
RNLocalize: 4071198b59b461f3b74eebc5fee8c50f13e39e79
RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93
RNRate: 2b31dad120cd1b78e33c6034808561c386a3dddf
RNReactNativeHapticFeedback: 22c5ecf474428766c6b148f96f2ff6155cd7225e
RNReanimated: 89f5e0a04d1dd52fbf27e7e7030d8f80a646a3fc
RNScreens: 0e91da98ab26d5d04c7b59a9b6bd694124caf88c
RNSecureKeyStore: f1ad870e53806453039f650720d2845c678d89c8
RNSentry: 1e27aeee260eaa833d5c22a4401be47d7b15c4b0
RNShare: a1d5064df7a0ebe778d001869b3f0a124bf0a491
RNSentry: cff88174a0b2e0289b2efe5be1abba1f6c54269d
RNShare: 7a7277f3c313652422d9de072ac50714dff5e8a4
RNSVG: ce9d996113475209013317e48b05c21ee988d42e
RNVectorIcons: 0bb4def82230be1333ddaeee9fcba45f0b288ed4
RNWatch: d56d00be49131ee454bb5a4a574f18506c8949e4
Sentry: 8fa58a051237554f22507fb483b9a1de0171a2dc
ToolTipMenu: 4d89d95ddffd7539230bdbe02ee51bbde362e37e
Yoga: 3ebccbdd559724312790e7742142d062476b698e
Yoga: 7d13633d129fd179e01b8953d38d47be90db185a
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
PODFILE CHECKSUM: e9c5efd531ca5ac67a4b743a179eeefb322cf387
PODFILE CHECKSUM: 845139ceee01fe141fab93749d578abee11b11c9
COCOAPODS: 1.10.0.beta.2
COCOAPODS: 1.9.3

View file

@ -1,9 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="M4Y-Lb-cyx">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097.3" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="M4Y-Lb-cyx">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="Stack View standard spacing" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@ -15,90 +17,80 @@
<rect key="frame" x="0.0" y="0.0" width="320" height="100"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Bitcoin Price" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="aaf-Pc-Y9i">
<rect key="frame" x="16" y="8" width="288" height="21"/>
<constraints>
<constraint firstAttribute="height" constant="21" id="fON-Nf-oBQ"/>
</constraints>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" text="..." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bcB-MD-aJf">
<rect key="frame" x="104" y="73" width="200" height="15"/>
<rect key="frame" x="104" y="73.5" width="200" height="14.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Last Updated:" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vU4-uK-6ow">
<rect key="frame" x="16" y="73" width="80" height="15"/>
<rect key="frame" x="16" y="73.5" width="80" height="14.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="USD" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lkL-gv-1a1">
<rect key="frame" x="16" y="40" width="35" height="33"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="35" id="4Nq-zK-gi3"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" text="..." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bEQ-e6-Puo">
<rect key="frame" x="59" y="46.5" width="14" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="arrow.up" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="eST-DU-WIK">
<rect key="frame" x="221" y="47.5" width="17" height="18"/>
<constraints>
<constraint firstAttribute="width" constant="17" id="gkK-pz-TDJ"/>
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="..." textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="gm7-vT-KrH" userLabel="...">
<rect key="frame" x="290" y="40" width="14" height="33"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="14" id="KoT-51-551"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="from" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="aqr-Mt-cor">
<rect key="frame" x="246" y="40" width="36" height="33"/>
<constraints>
<constraint firstAttribute="height" constant="33" id="u9N-WC-Os2"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<stackView opaque="NO" contentMode="scaleToFill" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="U8g-sL-Cl4">
<rect key="frame" x="16" y="5.5" width="83" height="20.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="USD" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lkL-gv-1a1">
<rect key="frame" x="0.0" y="0.0" width="61" height="20.5"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="35" id="4Nq-zK-gi3"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" text="..." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bEQ-e6-Puo">
<rect key="frame" x="69" y="0.0" width="14" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" spacingType="standard" translatesAutoresizingMaskIntoConstraints="NO" id="2BS-g5-Fog">
<rect key="frame" x="16" y="34" width="83" height="33"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="arrow.up" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="eST-DU-WIK">
<rect key="frame" x="0.0" y="1" width="17" height="31"/>
<constraints>
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="17" id="gkK-pz-TDJ"/>
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="from" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="aqr-Mt-cor">
<rect key="frame" x="25" y="0.0" width="36" height="33"/>
<constraints>
<constraint firstAttribute="height" constant="33" id="u9N-WC-Os2"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="..." textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="gm7-vT-KrH" userLabel="...">
<rect key="frame" x="69" y="0.0" width="14" height="33"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="14" id="KoT-51-551"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
</subviews>
<constraints>
<constraint firstItem="aqr-Mt-cor" firstAttribute="top" secondItem="aaf-Pc-Y9i" secondAttribute="bottom" constant="11" id="0ca-1C-JqG"/>
<constraint firstItem="ssy-KU-ocm" firstAttribute="trailing" secondItem="aaf-Pc-Y9i" secondAttribute="trailing" constant="16" id="197-jr-Kn5"/>
<constraint firstItem="eST-DU-WIK" firstAttribute="centerY" secondItem="aqr-Mt-cor" secondAttribute="centerY" id="2yd-pY-y1Y"/>
<constraint firstItem="gm7-vT-KrH" firstAttribute="leading" secondItem="bEQ-e6-Puo" secondAttribute="leading" id="1nN-8G-TB7"/>
<constraint firstItem="bcB-MD-aJf" firstAttribute="leading" secondItem="vU4-uK-6ow" secondAttribute="trailing" constant="8" id="5bB-Zv-Yeq"/>
<constraint firstItem="lkL-gv-1a1" firstAttribute="leading" secondItem="ssy-KU-ocm" secondAttribute="leading" constant="16" id="Bgx-xM-CSS"/>
<constraint firstItem="ssy-KU-ocm" firstAttribute="bottom" secondItem="bcB-MD-aJf" secondAttribute="bottom" constant="12" id="EPP-OS-3b6"/>
<constraint firstItem="vU4-uK-6ow" firstAttribute="leading" secondItem="ssy-KU-ocm" secondAttribute="leading" constant="16" id="EkD-jp-arv"/>
<constraint firstItem="gm7-vT-KrH" firstAttribute="centerY" secondItem="aqr-Mt-cor" secondAttribute="centerY" id="Eo2-n1-zbi"/>
<constraint firstItem="U8g-sL-Cl4" firstAttribute="leading" secondItem="vU4-uK-6ow" secondAttribute="leading" id="INV-Y4-WjP"/>
<constraint firstItem="2BS-g5-Fog" firstAttribute="top" secondItem="U8g-sL-Cl4" secondAttribute="bottom" constant="8" id="JRm-Qe-7EW"/>
<constraint firstItem="ssy-KU-ocm" firstAttribute="bottom" secondItem="vU4-uK-6ow" secondAttribute="bottom" constant="12" id="JSh-ZE-k1H"/>
<constraint firstItem="bcB-MD-aJf" firstAttribute="centerY" secondItem="vU4-uK-6ow" secondAttribute="centerY" id="MUL-tE-LmX"/>
<constraint firstItem="bEQ-e6-Puo" firstAttribute="leading" secondItem="lkL-gv-1a1" secondAttribute="trailing" constant="8" id="Ml2-4o-Yqk"/>
<constraint firstItem="ssy-KU-ocm" firstAttribute="trailing" secondItem="gm7-vT-KrH" secondAttribute="trailing" constant="16" id="OLV-lQ-T8a"/>
<constraint firstItem="gm7-vT-KrH" firstAttribute="centerY" secondItem="bEQ-e6-Puo" secondAttribute="centerY" id="Rle-PT-j9m"/>
<constraint firstItem="gm7-vT-KrH" firstAttribute="leading" secondItem="aqr-Mt-cor" secondAttribute="trailing" constant="8" id="Rtu-ah-AvP"/>
<constraint firstItem="gm7-vT-KrH" firstAttribute="firstBaseline" secondItem="aqr-Mt-cor" secondAttribute="firstBaseline" id="YIV-xq-qlw"/>
<constraint firstItem="bEQ-e6-Puo" firstAttribute="centerY" secondItem="lkL-gv-1a1" secondAttribute="centerY" id="Ys3-7f-RIc"/>
<constraint firstItem="aaf-Pc-Y9i" firstAttribute="leading" secondItem="S3S-Oj-5AN" secondAttribute="leading" constant="16" id="a1b-Yq-aZb"/>
<constraint firstItem="aaf-Pc-Y9i" firstAttribute="top" secondItem="S3S-Oj-5AN" secondAttribute="top" constant="8" id="aIo-h1-w4F"/>
<constraint firstItem="lkL-gv-1a1" firstAttribute="top" secondItem="aaf-Pc-Y9i" secondAttribute="bottom" constant="11" id="dET-8J-W4K"/>
<constraint firstItem="2BS-g5-Fog" firstAttribute="leading" secondItem="vU4-uK-6ow" secondAttribute="leading" id="ThK-uE-6nD"/>
<constraint firstItem="vU4-uK-6ow" firstAttribute="top" secondItem="2BS-g5-Fog" secondAttribute="bottom" constant="6.5" id="Wib-ev-GFn"/>
<constraint firstItem="ssy-KU-ocm" firstAttribute="trailing" secondItem="bcB-MD-aJf" secondAttribute="trailing" constant="16" id="kkD-VZ-BAt"/>
<constraint firstItem="vU4-uK-6ow" firstAttribute="firstBaseline" secondItem="lkL-gv-1a1" secondAttribute="baseline" constant="16" symbolType="layoutAnchor" id="lml-Hc-8Sv"/>
<constraint firstItem="aqr-Mt-cor" firstAttribute="leading" secondItem="eST-DU-WIK" secondAttribute="trailing" constant="8" id="t4j-U9-sOm"/>
</constraints>
<viewLayoutGuide key="safeArea" id="ssy-KU-ocm"/>
</view>
@ -120,6 +112,6 @@
</scene>
</scenes>
<resources>
<image name="arrow.up" catalog="system" width="60" height="64"/>
<image name="arrow.up" catalog="system" width="120" height="128"/>
</resources>
</document>

View file

@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>5.6.0</string>
<string>5.6.2</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionMainStoryboard</key>

View file

@ -20,23 +20,28 @@ class TodayViewController: UIViewController, NCWidgetProviding {
@IBOutlet weak var lastPrice: UILabel!
@IBOutlet weak var lastPriceFromLabel: UILabel!
private var lastPriceNumber: NSNumber?
override func viewDidLoad() {
super.viewDidLoad()
setLastPriceOutletsHidden(isHidden: true)
if let lastStoredTodayStore = TodayData.getPriceRateAndLastUpdate() {
processRateAndLastUpdate(todayStore: lastStoredTodayStore)
} else {
setLastPriceOutletsHidden(isHidden: true)
}
if #available(iOSApplicationExtension 13.0, *) {
} else{
self.lastPriceArrowImage.removeFromSuperview()
}
}
func setLastPriceOutletsHidden(isHidden: Bool) {
lastPrice.isHidden = isHidden
lastPriceFromLabel.isHidden = isHidden
lastPriceArrowImage.isHidden = isHidden
lastPriceArrowImage?.isHidden = isHidden
}
func processRateAndLastUpdate(todayStore: TodayDataStore) {

View file

@ -1,40 +1,40 @@
A Bitcoin wallet that allows you to store, send Bitcoin, receive Bitcoin and buy Bitcoin with focus on security and simplicity.
Eine Bitcoin-Wallet, die das Speichern, Überweisen, Empfangen und Kaufen von Bitcoin erlaubt und dabei den Fokus auf Sicherheit und Einfachheit legt.
On BlueWallet, a bitcoin wallet you own your private keys. A Bitcoin wallet made by Bitcoin users for the community.
Auf BlueWallet bist Du im Besitz der privaten Schlüssel. Eine Bitcoin-Wallet, die von Bitcoin-Benutzern für die Gemeinschaft erstellt wurde.
You can instantly transact with anyone in the world and transform the financial system right from your pocket.
Du kannst sofort mit jedem in der Welt Geschäfte machen und das Finanzsystem direkt aus der Tasche heraus reformieren.
Create for free unlimited number of bitcoin wallets or import your existing wallet. It's simple and fast.
Erstelle kostenlos eine unbegrenzte Anzahl von Bitcoin-Wallets oder importiere Deine vorhandene Wallet. Es ist einfach und schnell.
_____
Folgendes ist enthalten:
1 - Security by design
1 - Konzeptionsintegrierte Sicherheit
Free/Libre Open Source Software
MIT licensed, you can build it and run it on your own! Made with ReactNative
Quelloffene Software
MIT lizenziert. Sie können es selbst kompilieren und betreiben! Erstellt mit ReactNative.
Plausible deniability
Password which decrypts fake bitcoin wallets if you are forced to disclose your access
Plausible Bestreitbarkeit
Passwort, das falsche Bitcoin-Wallet entschlüsselt, falls Du gezwungen bist, Deinen Zugang preiszugeben
Full encryption
On top of the iOS multi-layer encryption, we encrypt everything with added passwords
Vollständige Verschlüsselung
Zusätzlich zur mehrschichtigen iOS-Verschlüsselung verschlüsseln wir alles mit zusätzlichen Passwörtern
Full node
Connect to your Bitcoin full node through Electrum
Full Node
Verbindung zu Deinem Bitcoin Full Node über Electrum
Cold Storage
Connect to your hardware wallet and keep your coins in Cold storage
Verbinde Dich mit Deiner Hardware-Wallet und verwahre Deine Coins sicher im Cold Storage
2 - Focused on your experience
2 - Fokus auf einfacher Bedienbarkeit
Be in control
Private keys never leave your device.
You control your private keys
Behalte die Kontrolle
Private Schlüssel verlassen niemals Dein Gerät.
Du kontrollierst Deine privaten Schlüssel
Flexible fees
Definierbare Transaktionsgebühren
Starting from 1 Satoshi. Defined by you, the user
Replace-By-Fee
@ -50,4 +50,4 @@ Bitcoin kaufen
Enter in the open financial revolution with the ability to buy Bitcoin directly in your wallet.
Local Trader
A p2p Bitcoin Trading platform, that allows you to buy and sell bitcoin directly to other users without 3rd parties.
A p2p Bitcoin Trading platform, that allows you to buy and sell bitcoin directly to other users without 3rd parties.

View file

@ -1,3 +1,24 @@
v5.6.1
======
* ADD: payjoin support
* FIX: rare crash on startup (electrum server malformed response)
* FIX: rare freezes on send screen
* FIX: bitcoin price widget content overlap
* FIX: biometrics listener release for some devices
* FIX: locales pt_BR, pt_PT, ru, sl_SI, ja_JP
* FIX: add margin for RTL languages
* FIX: Missing (NT) before $ sign
v.5.6.0
=======
* FIX: some transactions displayed with 0 value
* FIX: PSBT with HW wallets flow
* FIX: rare crash on watch-only receive button
* FIX: RBF cancel style
* REF: updated languages sl_SI, de_DE, fi_FI, es_ES
v5.5.9
=======
@ -67,33 +88,3 @@ v5.5.7
* FIX: Background had wrong color during loading phase
* REF: speeded up large wallets (>3k txs)
* REF: speedup onchain wallet creation
v5.5.6
======
* ADD: Camera Permission authorization view
* FIX: recieve button for watch-only wallets
* FIX: could not scan animated QR signed psbt
* FIX: updated 'fi_FI' language.
v5.5.5
======
* FIX: scan Cobo vault signed transaction QR
v5.5.4
======
* ADD: handling push notification open
* ADD: View Wallet xPub (Apple Watch)
* ADD: COP Fiat
* FIX: Invoice were not being sent (Apple Watch)
* FIX: Disable some Watch app elements when app is not reachable
* FIX: Show loading indicator when processing file or qrcode image
* FIX: Button size for large devices
* FIX: better handling of electrum disconnect
* FIX: disable push notifications in settings
* FIX: Font-Color in Bump-Fee Input Field "Custom" is not adapted for dark mode
* FIX: QRCode border in LND Backup screen
* FIX: Animated QRCode border. Change save path to Downloads folder
* FIX: sk_SK language updates

View file

@ -293,7 +293,7 @@
"details_to": "Ausgehend",
"details_transaction_details": "Transaktionsdetails",
"enable_hw": "Dieses Wallet nutzt keine Hardware-Wallet. Möchtest Du die Verwendung einer Hardware-Wallet aktivieren?",
"list_conf": "Konf",
"list_conf": "Bestätigungen: {number}",
"list_title": "Transaktionen",
"rbf_explain": "BlueWallet ersetzt diese Transaktion zur Verringerung der Transaktionszeit durch eine mit höherer Gebühr. (RBF - Replace By Fee)",
"rbf_title": "TRX-Gebühr erhöhen (RBF)",

View file

@ -8,7 +8,10 @@
"of": "{number} of {total}",
"ok": "OK",
"storage_is_encrypted": "Your storage is encrypted. Password is required to decrypt it",
"yes": "Yes"
"yes": "Yes",
"no": "No",
"invalid_animated_qr_code_fragment" : "Invalid animated QRCode fragment, please try again",
"file_saved": "File ({filePath}) has been saved in your Downloads folder ."
},
"azteco": {
"codeIs": "Your voucher code is",
@ -170,13 +173,13 @@
"details_next": "Next",
"details_no_maximum": "The selected wallet does not support automatic maximum balance calculation. Are you sure to want to select this wallet?",
"details_no_multiple": "The selected wallet does not support sending Bitcoin to multiple recipients. Are you sure to want to select this wallet?",
"details_no_signed_tx": "The selected file does not contain a signed transaction that can be imported.",
"details_no_signed_tx": "The selected file does not contain a transaction that can be imported.",
"details_note_placeholder": "note to self",
"details_scan": "Scan",
"details_total_exceeds_balance": "The sending amount exceeds the available balance.",
"details_wallet_before_tx": "Before creating a transaction, you must first add a Bitcoin wallet.",
"details_wallet_selection": "Wallet Selection",
"dynamic_init": "Initialing",
"dynamic_init": "Initializing",
"dynamic_next": "Next",
"dynamic_prev": "Previous",
"dynamic_start": "Start",
@ -209,7 +212,8 @@
"qr_error_no_qrcode": "The selected image does not contain a QR Code.",
"qr_error_no_wallet": "The selected file does not contain a wallet that can be imported.",
"success_done": "Done",
"txSaved": "The transaction file ({filePath}) has been saved in your Downloads folder ."
"txSaved": "The transaction file ({filePath}) has been saved in your Downloads folder .",
"problem_with_psbt": "Problem with PSBT"
},
"settings": {
"about": "About",
@ -293,7 +297,7 @@
"details_to": "Output",
"details_transaction_details": "Transaction details",
"enable_hw": "This wallet is not being used in conjunction with a hardwarde wallet. Would you like to enable hardware wallet use?",
"list_conf": "conf",
"list_conf": "conf: {number}",
"list_title": "transactions",
"rbf_explain": "We will replace this transaction with the one with a higher fee, so it should be mined faster. This is called RBF - Replace By Fee.",
"rbf_title": "Bump fee (RBF)",
@ -371,5 +375,19 @@
"select_wallet": "Select Wallet",
"xpub_copiedToClipboard": "Copied to clipboard.",
"xpub_title": "wallet XPUB"
},
"multisig": {
"provide_signature": "Provide signature",
"vault_key": "Vault key {number}",
"fee": "Fee: {number}",
"fee_btc": "{number} BTC",
"confirm": "Confirm",
"header": "Send",
"share": "Share",
"how_many_signatures_can_bluewallet_make": "how many signatures can bluewallet make",
"scan_or_import_file": "Scan or import file",
"export_coordination_setup": "export coordination setup",
"cosign_this_transaction": "Co-sign this transaction?",
"co_sign_transaction": "Co-sign transaction"
}
}

View file

@ -94,7 +94,7 @@
"refill_external": "Recargar con una cartera externa",
"refill_lnd_balance": "Rellenar la cartera de Lightning",
"sameWalletAsInvoiceError": "No puedes pagar una factura con la misma cartera que usaste para crearla.",
"title": "manejar fondos"
"title": "Administrar fondos"
},
"lndViewInvoice": {
"additional_info": "Información adicional",
@ -170,7 +170,7 @@
"details_next": "Siguiente",
"details_no_maximum": "La cartera seleccionada no permite el cálculo automático del saldo máximo. ¿Estás seguro de querer seleccionar esta cartera?",
"details_no_multiple": "La cartera seleccionada no admite el envío de bitcoin a varios destinatarios. ¿Estás seguro de querer seleccionar esta cartera?",
"details_no_signed_tx": "El archivo seleccionado no contiene una transacción firmada que se pueda importar.",
"details_no_signed_tx": "El archivo seleccionado no contiene una transacción que se pueda importar.",
"details_note_placeholder": "nota personal",
"details_scan": "Escanear",
"details_total_exceeds_balance": "El monto excede el balance disponible.",
@ -184,20 +184,20 @@
"fee_10m": "10m",
"fee_1d": "1d",
"fee_3h": "3h",
"fee_custom": "Custom",
"fee_fast": "Fast",
"fee_medium": "Medium",
"fee_replace_min": "The total fee rate (satoshi per byte) you want to pay should be higher than {min} sat/byte",
"fee_satbyte": "in sat/byte",
"fee_slow": "Slow",
"fee_custom": "Personalizada",
"fee_fast": "Rápida",
"fee_medium": "Media",
"fee_replace_min": "La comisión (en satoshis por byte) tiene que ser mayor que {min} sat/byte",
"fee_satbyte": "en sat/bye",
"fee_slow": "Lenta",
"header": "Enviar",
"input_clear": "Borrar",
"input_done": "Completado",
"input_paste": "Pegar",
"input_total": "Total:",
"open_settings": "Abrir configuración",
"permission_camera_message": "Necesitamos permiso para usar la cámara",
"permission_camera_title": "Permiso para usar la cámara",
"open_settings": "Abrir configuración",
"permission_storage_later": "Pregúntame luego",
"permission_storage_message": "BlueWallet necesita permiso para acceder a su almacenamiento para poder guardar esta transacción.",
"permission_storage_title": "Permiso para acceder al almacenamiento",
@ -308,6 +308,7 @@
"add_entropy_provide": "Entropía mediante el lanzamiento de dados",
"add_entropy_remain": "{gen} bytes of entropía generada. Los {rem} bytes restantes serán obtenidos del generador de números aleatorios.",
"add_import_wallet": "Importar cartera",
"import_file": "Importar archivo",
"add_lightning": "Lightning",
"add_lndhub": "Conecta a tu LDNHub",
"add_lndhub_error": "La dirección proporcionada no es válida para un nodo LNDHub.",
@ -341,7 +342,6 @@
"import_do_import": "Importar",
"import_error": "Error al importar. Por favor, asegúrate de que los datos introducidos son correctos.",
"import_explanation": "Escriba aquí mnemotécnica, clave privada, WIF o cualquier cosa que tenga. BlueWallet hará todo lo posible para adivinar el formato correcto e importar su billetera.",
"import_file": "Importar archivo",
"import_imported": "Importado",
"import_scan_qr": "Escanear o importar un archivo",
"import_success": "Tu cartera ha sido importada.",
@ -353,7 +353,7 @@
"list_empty_txs1": "Tus transacciones aparecerán aquí",
"list_empty_txs1_lightning": "Usa carteras Lighting para tus transacciones diarias. Tienen tasas muy bajas y una velocidad de vértigo.",
"list_empty_txs2": "Empieza con tu cartera",
"list_empty_txs2_lightning": "\nPara comenzar a usarlo, toque \"manejar fondos\" y recargue su saldo.",
"list_empty_txs2_lightning": "\nPara comenzar a usarlo, toca en \"Administrar fondos\" y añade algo de salgo.",
"list_header": "Una cartera representa un par de llaves, una privada y una que puedes compartir para recibir fondos.",
"list_import_error": "Error al intentar importar esta cartera.",
"list_import_problem": "Ha ocurrido un problema al importar esta cartera",
@ -361,6 +361,7 @@
"list_long_choose": "Elegir foto",
"list_long_clipboard": "Copiar del portapapeles",
"list_long_scan": "Escanear código QR",
"take_photo": "Hacer una foto",
"list_tap_here_to_buy": "Tap here to buy Bitcoin",
"list_title": "Carteras",
"list_tryagain": "Inténtalo otra vez",
@ -368,7 +369,6 @@
"select_no_bitcoin": "No hay carteras de Bitcoin disponibles.",
"select_no_bitcoin_exp": "Es necesaria una cartera de Bitcoin para recargar una Cartera de Lighting. Por favor, cree o importe una.",
"select_wallet": "Selecciona una cartera",
"take_photo": "Hacer una foto",
"xpub_copiedToClipboard": "Copiado a portapapeles.",
"xpub_title": "XPUB de la cartera"
}

View file

@ -7,7 +7,7 @@
"never": "ei koskaan",
"of": "{number} / {total}",
"ok": "OK",
"storage_is_encrypted": "Tallennustilasi on salattu. Salasana vaaditaan sen purkamiseksi",
"storage_is_encrypted": "Tallennustilasi on salattu. Sen purkamiseksi vaaditaan salasana",
"yes": "Kyllä"
},
"azteco": {
@ -16,12 +16,12 @@
"errorSomething": "Jotain meni pieleen. Onko tämä kuponki edelleen voimassa?",
"redeem": "Lunastus lompakkoon",
"redeemButton": "Lunastus",
"success": "Onnistunut",
"success": "Onnistui",
"title": "Lunasta Azte.co kuponki"
},
"entropy": {
"save": "Tallenna",
"title": "Satunnaisuus",
"title": "Entropia",
"undo": "Kumoa"
},
"errors": {
@ -31,7 +31,7 @@
},
"hodl": {
"are_you_sure_you_want_to_logout": "Haluatko varmasti kirjautua ulos HodlHodl-palvelusta?",
"cont_address_escrow": "Sulkutili",
"cont_address_escrow": "Escrow",
"cont_address_to": "Vastaanottaja",
"cont_buying": "ostaminen",
"cont_cancel": "Peruuta sopimus",
@ -40,19 +40,19 @@
"cont_chat": "Avaa keskustelu vastapuolen kanssa",
"cont_how": "Kuinka maksaa",
"cont_no": "Sinulla ei ole meneillään sopimuksia",
"cont_paid": "Merkitse sopimus maksetuksi",
"cont_paid": "Merkitse sopimus Maksettu",
"cont_paid_e": "Tee tämä vain, jos lähetit varoja myyjälle sovitulla maksutavalla",
"cont_paid_q": "Haluatko varmasti merkitä tämän sopimuksen maksettuksi?",
"cont_paid_q": "Haluatko varmasti merkitä tämän sopimuksen maksetuksi?",
"cont_selling": "myynti",
"cont_st_completed": "Valmista!",
"cont_st_in_progress_buyer": "Kolikot ovat sulkutilillä, ole hyvä ja maksa myyjälle",
"cont_st_paid_enought": "Bitcoinit ovat sulkutilillä! Ole hyvä ja maksa myyjälle\nsovitun maksutavan kautta",
"cont_st_paid_waiting": "Odotetaan myyjän vapauttavan kolikot sulkutilistä",
"cont_st_waiting": "Odotetaan myyjän tallettavan bitcoineja sulkutiliin...",
"cont_title": "Minun sopimukset",
"cont_st_in_progress_buyer": "Kolikot ovat escrow:ssa, ole hyvä ja maksa myyjälle",
"cont_st_paid_enought": "Bitcoinit ovat escrow:ssa! Ole hyvä ja maksa myyjälle\nsovitun maksutavan kautta",
"cont_st_paid_waiting": "Odotetaan myyjän vapauttavan kolikot escrow:sta",
"cont_st_waiting": "Odotetaan myyjän tallettavan bitcoineja escrow:iin...",
"cont_title": "Sopimukseni",
"filter_any": "Mikä tahansa",
"filter_buying": "Ostaminen",
"filter_country_global": "Globaalit tarjoukset",
"filter_country_global": "Maailmanlaajuiset tarjoukset",
"filter_country_near": "Lähellä minua",
"filter_currency": "Valuutta",
"filter_detail": "Tiedot",
@ -63,17 +63,17 @@
"filter_search": "Etsi",
"filter_selling": "Myynti",
"item_minmax": "Minimi/Maximi",
"item_nooffers": "Ei tarjouksia. Yritä muuttaa \"Lähellä minua\" globaaleiksi tarjouksiksi!",
"item_rating": "{rating} kaupat",
"item_rating_no": "Ei arvosanaa",
"item_nooffers": "Ei tarjouksia. Yritä muuttaa \"Lähellä minua\" kansainvälisiksi tarjouksiksi!",
"item_rating": "{rating} vaihdot",
"item_rating_no": "Ei luokitusta",
"login": "Kirjaudu sisään",
"mycont": "Sopimukseni",
"offer_accept": "Hyväksy tarjous",
"offer_account_finish": "Näyttää siltä, että et vienyt loppuun tilin luomista HodlHodl-palvelussa, haluatko lopettaa asennuksen nyt?",
"offer_account_finish": "Näyttää siltä, että et vienyt loppuun tilin luomista HodlHodl-palvelussa, haluatko päättää asennuksen nyt?",
"offer_choosemethod": "Valitse maksutapa",
"offer_confirmations": "vahvistukset",
"offer_minmax": "minimi/maximi",
"offer_minutes": "minimi",
"offer_minmax": "min / max",
"offer_minutes": "min",
"offer_promt_fiat": "Kuinka paljon {currency} haluat ostaa?",
"offer_promt_fiat_e": "Esimerkiksi 100",
"offer_window": "ikkuna",
@ -92,7 +92,7 @@
"refill_card": "Täytä pankkikortilla",
"refill_create": "Jatka luomalla Bitcoin-lompakko, jolla voit täyttää sen.",
"refill_external": "Täytä ulkoisella lompakolla",
"refill_lnd_balance": "Täytä Lightning lompakon saldoa",
"refill_lnd_balance": "Täytä Lightning-lompakon saldoa",
"sameWalletAsInvoiceError": "Et voi maksaa laskua samalla lompakolla, jolla se on luotu.",
"title": "hallinnoi varoja"
},
@ -107,12 +107,12 @@
"wasnt_paid_and_expired": "Tätä laskua ei maksettu, ja se on vanhentunut"
},
"plausibledeniability": {
"create_fake_storage": "Luo väärennetty tallennustila",
"create_fake_storage": "Luo Salattu tallennustila",
"create_password": "Luo salasana",
"create_password_explanation": "Väärennetyn tallennustilan salasanan ei tule täsmätä oikean tallennustilan salasanan kanssa",
"help": "Joissain tilanteissa, saatat olla pakotettu kertomaan salasanasi. Pitääksesi kolikkosi turvassa, BlueWallet voi luoda toisen salatun tallennustilan, toisella salasanalla. Paineen alla, voit kertoa tämän salasanan kolmannelle osapuolelle. Annettaessa BlueWalletiin, se avaa uuden väärennetyn tallennustilan. Se näyttää oikealta kolmannelle osapuolelle, mutta pitää oikean tallennustilasi kolikkoineen turvassa.",
"create_password_explanation": "Väärennetyn tallennustilan salasana ei tule täsmätä oikean tallennustilan salasanan kanssa",
"help": "Joissain tilanteissa, saatat olla pakotettu kertomaan salasanasi. Pitääksesi kolikkosi turvassa, BlueWallet voi luoda toisen salatun tallennustilan, toisella salasanalla. Paineen alla, voit kertoa tämän salasanan kolmannelle osapuolelle. Jos se tulee sisään BlueWallet:iin, se avaa uuden \"väärennetyn\" tallennustilan. Se näyttää oikealta kolmannelle osapuolelle, mutta pitää oikean tallennustilasi kolikkoineen turvassa.",
"help2": "Uusi tallennustila näyttää täysin toimivalta, ja voit säilyttää pieniä summia siellä, jotta se näyttää uskottavalta.",
"password_should_not_match": "Väärennetyn tallennustilan salasanan ei tule täsmätä oikean tallennustilan salasanan kanssa",
"password_should_not_match": "Salasana on käytössä. Ole hyvä, ja kokeile toista salasanaa.",
"passwords_do_not_match": "Salasanat eivät täsmää, yritä uudelleen",
"retype_password": "Salasana uudelleen",
"success": "Onnistui",
@ -136,41 +136,41 @@
"header": "Vastaanota"
},
"send": {
"broadcastButton": "LÄHETÄ",
"broadcastButton": "LÄHETÄ",
"broadcastError": "virhe",
"broadcastNone": "Syötä siirtotapahtuman tiiviste",
"broadcastPending": "odottaa",
"broadcastSuccess": "onnistunut",
"broadcastSuccess": "onnistui",
"confirm_header": "Vahvista",
"confirm_sendNow": "Lähetä nyt",
"create_amount": "Summa",
"create_broadcast": "Kuuluta",
"create_broadcast": "Lähetä",
"create_copy": "Kopioi ja lähetä myöhemmin",
"create_details": "Tiedot",
"create_details": "Tarkemmat tiedot",
"create_fee": "Siirtokulu",
"create_memo": "Muistio",
"create_satoshi_per_byte": "Satoshia per tavu",
"create_this_is_hex": "Tämä on siirron hex, allekirjoitettu ja valmis lähetettävksi verkkoon.",
"create_this_is_hex": "Tämä on siirtotapahtuman hex, allekirjoitettu ja valmis lähetettävksi verkkoon.",
"create_to": "Vastaanottaja",
"create_tx_size": "TX koko",
"create_verify": "Varmenna coinb.in:ssä",
"create_verify": "Varmenna coinb.in :ssä",
"details_add_rec_add": "Lisää Vastaanottaja",
"details_add_rec_rem": "Poista Vastaanottaja",
"details_address": "osoite",
"details_address_field_is_not_valid": "Osoite ei kelpaa",
"details_adv_fee_bump": "Salli Siirtomaksun Nosto",
"details_adv_fee_bump": "Salli Siirtokulun Nosto",
"details_adv_full": "Käytä Koko Saldo",
"details_adv_full_remove": "Muut vastaanottajat poistetaan tästä siirtotapahtumasta.",
"details_adv_full_sure": "Haluatko varmasti käyttää lompakon koko saldoa tähän siirtotapahtumaan?",
"details_adv_full_sure": "Haluatko varmasti käyttää lompakon koko saldon tähän siirtotapahtumaan?",
"details_adv_import": "Tuo Siirtotapahtuma",
"details_amount_field_is_not_valid": "Määrä ei kelpaa",
"details_create": "Luo Lasku",
"details_error_decode": "Virhe: Bitcoin-osoitetta ei voi muuntaa",
"details_fee_field_is_not_valid": "Siirtomaksu ei kelpaa",
"details_error_decode": "Virhe: Bitcoin-osoitetta ei voida dekoodata",
"details_fee_field_is_not_valid": "Siirtokulukenttä ei ole pätevä",
"details_next": "Seuraava",
"details_no_maximum": "Valittu lompakko ei tue automaattista enimmäis-saldolaskelmaa. Haluatko varmasti valita tämän lompakon?",
"details_no_multiple": "Valittu lompakko ei tue Bitcoinin lähettämistä useille vastaanottajille. Haluatko varmasti valita tämän lompakon?",
"details_no_signed_tx": "Valittu tiedosto ei sisällä allekirjoitettua siirtotapahtumaa, joka voidaan tuoda.",
"details_no_signed_tx": "Valittu tiedosto ei sisällä tuotavaa siirtotapahtumaa.",
"details_note_placeholder": "muistiinpano itselle",
"details_scan": "Skannaa",
"details_total_exceeds_balance": "Lähetettävä summa ylittää katteen",
@ -181,31 +181,41 @@
"dynamic_prev": "Edellinen",
"dynamic_start": "Aloita",
"dynamic_stop": "Lopeta",
"fee_10m": "10 m",
"fee_1d": "1 p",
"fee_3h": "3 t",
"fee_custom": "Mukautettu",
"fee_fast": "Nopea",
"fee_medium": "Keskitaso",
"fee_replace_min": "Maksettavan kokonaiskulun (satoshia tavua kohti) tulisi olla korkeampi kuin {min} sat/tavu",
"fee_satbyte": "sat/tavu",
"fee_slow": "Hidas",
"header": "Lähetä",
"input_clear": "Tyhjää",
"input_done": "Valmis",
"input_paste": "Liitä",
"input_total": "Loppusumma:",
"permission_camera_message": "Tarvitsemme lupaasi käyttämään kameraasi",
"input_total": "Yhteensä:",
"permission_camera_message": "Tarvitsemme lupasi kameran käyttöön",
"permission_camera_title": "Kameran käyttölupa",
"open_settings": "Avaa Asetukset",
"permission_storage_later": "Kysy Minulta Myöhemmin",
"permission_storage_message": "BlueWallet tarvitsee luvan käyttää tallennustilaasi tämän siirtotapahtuman tallentamiseksi.",
"permission_storage_title": "BlueWallet-Tallennustilan Käyttöoikeus",
"psbt_clipboard": "Kopioi leikepöydälle",
"psbt_this_is_psbt": "Tämä on osittain allekirjoitettu bitcoin-tapahtuma (PSBT). Ole hyvä ja allekirjoittakaa se hardware lompakolla.",
"permission_storage_title": "BlueWallet Tallennustilan Käyttöoikeus",
"psbt_clipboard": "Kopioi Leikepöydälle",
"psbt_this_is_psbt": "Tämä on osittain allekirjoitettu bitcoin-siirtotapahtuma (PSBT). Ole hyvä ja allekirjoita se hardware-lompakolla.",
"psbt_tx_export": "Vie tiedostoon",
"psbt_tx_open": "Avaa allekirjoitettu siirtotapahtuma",
"psbt_tx_scan": "Skannaa allekirjoitettu siirtotapahtuma",
"psbt_tx_open": "Avaa Allekirjoitettu Siirtotapahtuma",
"psbt_tx_scan": "Skannaa Allekirjoitettu Siirtotapahtuma",
"qr_error_no_qrcode": "Valittu kuva ei sisällä QR-koodia.",
"qr_error_no_wallet": "Valittu tiedosto ei sisällä tuotavaa lompakkoa.",
"success_done": "Valmis",
"txSaved": "Siirtotapahtuma tiedosto ({filePath}) on tallennettu Lataukset-kansioon."
"txSaved": "Siirtotapahtumatiedosto ({filePath}) on tallennettu Lataukset-kansioon."
},
"settings": {
"about": "Tietoa",
"about_awesome": "Rakennettu mahtavasti",
"about_backup": "Varmuuskopioi avaimesi aina!",
"about_free": "BlueWallet on ilmainen ja avoimen lähdekoodin projekti. Bitcoin-käyttäjien tekemä.",
"about_awesome": "Mahtavasti rakennettu",
"about_backup": "Varmuuskopioi aina avaimesi!",
"about_free": "BlueWallet on ilmainen ja avoimen lähdekoodin projekti. Bitcoin käyttäjien tekemä.",
"about_release_notes": "Julkaisutiedot",
"about_review": "Jätä meille arvostelu",
"about_selftest": "Suorita itsetestaus",
@ -215,61 +225,61 @@
"advanced_options": "Lisäasetukset",
"currency": "Valuutta",
"currency_source": "Hinnat saadaan CoinDeskistä",
"default_desc": "Kun toiminto on poistettu käytöstä, BlueWallet avaa valitun lompakon heti käynnistettäessä.",
"default_info": "Oletus kohtaan",
"default_title": "Käynnistyksessä",
"default_desc": "Kun on pois käytöstä, BlueWallet avaa valitun lompakon heti käynnistettäessä.",
"default_info": "Oletustiedot",
"default_title": "Käynnistettäessä",
"default_wallets": "Näytä Kaikki Lompakot",
"electrum_connected": "Yhdistetty",
"electrum_connected_not": "Ei yhteyttä",
"electrum_error_connect": "Ei voi muodostaa yhteyttä toimitettuun Electrum-palvelimeen",
"electrum_error_connect": "Ei voida yhdistää tarjottuun Electrum-palvelimeen",
"electrum_host": "ylläpitäjä, esimerkiksi {example}",
"electrum_port": "TCP-portti, yleensä {example}",
"electrum_port_ssl": "SSL-portti, yleensä {example}",
"electrum_saved": "Muutoksesi on tallennettu onnistuneesti. Uudelleenkäynnistys voi olla tarpeen, jotta muutokset tulevat voimaan.",
"electrum_settings": "Electrum-asetukset",
"electrum_settings_explain": "Aseta tyhjäksi käyttääksesi oletusasetusta",
"electrum_status": "Status",
"electrum_settings": "Electrum-Asetukset",
"electrum_settings_explain": "Jätä tyhjäksi käyttääksesi oletusasetusta",
"electrum_status": "Tila",
"encrypt_decrypt": "Pura tallennustilan salaus",
"encrypt_decrypt_q": "Haluatko varmasti purkaa tallennustilan salauksen? Tämän avulla lompakoihisi pääsee käsiksi ilman salasanaa.",
"encrypt_decrypt_q": "Haluatko varmasti purkaa tallennustilan salauksen? Tämä mahdollistaa lompakkoihisi pääsyn ilman salasanaa.",
"encrypt_del_uninstall": "Poista, jos BlueWallet poistetaan",
"encrypt_enc_and_pass": "Salattu ja Salasanalla suojattu",
"encrypt_title": "Tietoturva",
"encrypt_tstorage": "tallennustila",
"encrypt_use": "Käytä {type}",
"encrypt_use_expl": "{type} -toimintoa käytetään henkilöllisyytesi vahvistamiseen ennen siirtotapahtuman tekemistä, lompakon lukituksen avaamista, vientiä tai poistamista. {type} ei käytetä salatun tallennustilan lukituksen avaamiseen.",
"encrypt_use_expl": "{type} käytetään henkilöllisyytesi vahvistamiseen ennen siirtotapahtuman tekemistä, lompakon lukituksen avaamista, vientiä tai poistamista. {type} ei käytetä salatun tallennustilan lukituksen avaamiseen.",
"general": "Yleinen",
"general_adv_mode": "Kehittynyt tila",
"general_adv_mode_e": "Kun tämä asetus on käytössä, näet lisäasetukset, kuten erilaiset lompakkotyypit, kyvyn määrittää LNDHub-ilmentymän, johon haluat muodostaa yhteyden, ja mukautetun entropian lompakon luomisen aikana.",
"general_adv_mode_e": "Kun tämä asetus on käytössä, näet lisäasetukset, kuten erilaiset lompakkotyypit, kyvyn määrittää LNDHub-instanssi, johon haluat muodostaa yhteyden, ja mukautetun entropian lompakon luomisen aikana.",
"general_continuity": "Jatkuvuus",
"general_continuity_e": "Kun tämä asetus on käytössä, voit tarkastella valittuja lompakoita ja siirtotapahtumia muilla Apple iCloud -laitteilla.",
"groundcontrol_explanation": "GroundControl on ilmainen avoimen lähdekoodin push-ilmoituspalvelin bitcoin-lompakoille. Voit asentaa oman GroundControl-palvelimen ja laittaa sen URL-osoitteen tähän, jotta et luota BlueWallet-infrastruktuuriin. Jätä tyhjäksi käyttääksesi oletusasetusta",
"header": "asetukset",
"language": "Kieli",
"language_restart": "Kun valitset uuden kielen, muutoksen voimaantulo edellyttää, että BlueWallet käynnistetään uudelleen.",
"lightning_error_lndhub_uri": "Ei kelvollinen LndHub-URI",
"lightning_error_lndhub_uri": "LndHub-URI ei kelpaa",
"lightning_saved": "Muutoksesi on tallennettu onnistuneesti",
"lightning_settings": "Lightning asetukset",
"lightning_settings": "Lightning-Asetukset",
"lightning_settings_explain": "Yhdistääksesi omaan LND-solmuun, asenna LndHub ja laita sen URL tänne. Jätä tyhjäksi käyttääksesi BlueWalletin LNDHubia (lndhub.io). Muutosten tallentamisen jälkeen luodut lompakot yhdistävät annettuun LNDHubiin.",
"network": "Verkko",
"network_broadcast": "Lähetä siirtotapahtuma",
"network_electrum": "Electrum-palvelin",
"not_a_valid_uri": "URI ei kelpaa",
"notifications": "Ilmoitukset",
"password": "Salasana",
"password_explain": "Luo salasana, jota käytät tallennustilan salauksen purkamiseen",
"passwords_do_not_match": "Salasanat eivät täsmää",
"plausible_deniability": "Uskottava kiistettävyys...",
"retype_password": "Salasana uudelleen",
"notifications": "Ilmoitukset",
"save": "Tallenna",
"saved": "Tallennettu",
"not_a_valid_uri": "Ei kelvollinen URI",
"plausible_deniability": "Uskottava kiistettävyys",
"push_notifications": "Push-ilmoitukset",
"groundcontrol_explanation": "GroundControl on ilmainen avoimen lähdekoodin push-ilmoituspalvelin bitcoin-lompakoille. Voit asentaa oman GroundControl-palvelimen ja laittaa sen URL-osoitteen tähän, jotta et luota BlueWallet-infrastruktuuriin. Jätä tyhjäksi käyttääksesi oletusasetusta"
"retype_password": "Salasana uudelleen",
"save": "Tallenna",
"saved": "Tallennettu"
},
"transactions": {
"cancel_explain": "Korvaamme tämän siirtotapahtuman sillä, joka maksaa sinulle ja jolla on korkeammat siirtokulut. Tämä peruuttaa siirtotapahtuman tehokkaasti. Tätä kutsutaan RBF - Korvattavissa korkeammalla kululla.",
"cancel_explain": "Korvaamme tämän siirtotapahtuman sillä, joka maksaa sinulle ja on korkeammat siirtokulut. Tämä peruuttaa siirtotapahtuman tehokkaasti. Tätä kutsutaan RBF - Replace By Fee - Korvaa korkeammilla kuluilla.",
"cancel_no": "Tämä siirtotapahtuma ei ole vaihdettavissa",
"cancel_title": "Peruuta tämä siirtotapahtuma (RBF)",
"cpfp_create": "Luo",
"cpfp_exp": "Luomme toisen siirtotapahtuman, joka kuluttaa vahvistamattoman siirtotapahtuman. Kokonaiskulu on suurempi kuin alkuperäinen siirtotapahtumakulu, joten sen pitäisi olla louhittu nopeammin. Tätä kutsutaan CPFP - lapsi maksaa vanhemmalle.",
"cpfp_exp": "Luomme toisen siirtotapahtuman, joka kuluttaa vahvistamattoman siirtotapahtuman. Kokonaiskulu on suurempi kuin alkuperäinen siirtokulu, joten sen pitäisi olla louhittu nopeammin. Tätä kutsutaan CPFP - Child Pays For Parent - Lapsi Maksaa Vanhemmalle.",
"cpfp_no_bump": "Tämä siirtotapahtuma ei ole nostettavissa",
"cpfp_title": "Nosta siirtokuluja (CPFP)",
"details_block": "Lohkon järjestysnumero",
@ -279,17 +289,17 @@
"details_outputs": "Ulostulot",
"details_received": "Vastaanotettu",
"details_show_in_block_explorer": "Näytä lohkoketjuselaimessa",
"details_title": "Siirto",
"details_title": "Siirtotapahtuma",
"details_to": "Ulostulo",
"details_transaction_details": "Siirtotapahtuman tiedot",
"enable_hw": "Tätä lompakkoa ei käytetä yhdessä hardware-lompakon kanssa. Haluatko ottaa hardware-lompakon käyttöön?",
"list_conf": "conf",
"list_title": "siirrot",
"transactions_count": "siirtotapahtumien määrä",
"rbf_explain": "Korvaamme tämän siirtotapahtuman toisella jossa on korkeammat siirtokulut, joten se pitäisi olla louhittu nopeammin. Tätä kutsutaan RBF - Korvattavissa korkeammalla kululla.",
"enable_hw": "Tätä lompakkoa ei ole käytetty yhdessä hardware-lompakon kanssa. Haluatko ottaa hardware-lompakon käyttöön?",
"list_conf": "conf: {number}",
"list_title": "siirtotapahtumat",
"rbf_explain": "Korvaamme tämän siirtotapahtuman toisella jossa on korkeammat siirtokulut, joten se pitäisi olla louhittu nopeammin. Tätä kutsutaan RBF - Replace By Fee - Korvattavissa korkeammilla kuluilla.",
"rbf_title": "Nosta siirtokuluja (RBF)",
"status_bump": "Nosta siirtokuluja",
"status_cancel": "Peruuta siirtotapahtuma"
"status_cancel": "Peruuta Siirtotapahtuma",
"transactions_count": "siirtotapahtumien määrä"
},
"wallets": {
"add_bitcoin": "Bitcoin",
@ -298,8 +308,9 @@
"add_entropy_provide": "Hanki entropia nopanheiton kautta",
"add_entropy_remain": "{gen} tavua luotua entropiaa. Jäljellä olevat {rem} tavut saadaan Järjestelmän satunnaislukugeneraattorilta.",
"add_import_wallet": "Tuo lompakko",
"add_lightning": "Salama",
"add_lndhub": "Yhdistä LNDHubiisi",
"import_file": "Tuo tiedosto",
"add_lightning": "Lightning",
"add_lndhub": "Yhdistä LNDHub:iisi",
"add_lndhub_error": "Annettu solmun osoite ei ole kelvollinen LNDHub-solmu.",
"add_lndhub_placeholder": "solmusi osoite",
"add_or": "tai",
@ -312,14 +323,14 @@
"details_connected_to": "Yhdistetty",
"details_del_wb": "Lompakon saldo",
"details_del_wb_err": "Annettu saldo ei vastaa tämän lompakon saldoa. Yritä uudelleen",
"details_del_wb_q": "Tällä lompakolla on saldo. Ennen kuin jatkat, huomaa, että et voi palauttaa varoja ilman tämän lompakon siemenlauseketta. Syötä lompakkosi saldo {saldo} satoshia välttääksesi tämän lompakon vahingossa tapahtuvan poistamisen.",
"details_del_wb_q": "Tällä lompakolla on saldoa. Ennen kuin jatkat, huomaa, että et voi palauttaa varoja ilman tämän lompakon siemenlauseketta. Syötä lompakkosi saldo {balance} satoshia välttääksesi vahingossa tämän lompakon poistamisen.",
"details_delete": "Poista",
"details_delete_wallet": "Poista lompakko",
"details_display": "näkyy lompakkolistassa",
"details_display": "näkyy lompakkojen listassa",
"details_export_backup": "Vie / varmuuskopioi",
"details_marketplace": "Kauppapaikka",
"details_master_fingerprint": "Isäntä sormenjälki",
"details_no_cancel": "En, peruuta",
"details_master_fingerprint": "ä sormenjälki",
"details_no_cancel": "Ei, peruuta",
"details_save": "Tallenna",
"details_show_xpub": "Näytä lompakon XPUB",
"details_title": "Lompakko",
@ -332,26 +343,26 @@
"import_error": "Tuonti epäonnistui. Varmista, että annettu tieto on oikein",
"import_explanation": "Kirjoita tähän muistisanasi, yksityinen avain, WIF tai jotain mitä sinulla on. BlueWallet tekee parhaansa arvatakseen oikean muodon ja tuo lompakkosi",
"import_imported": "Tuotu",
"import_scan_qr": "tai skannaa QR-koodi?",
"import_success": "Onnistui",
"import_scan_qr": "Skannaa tai tuo tiedosto",
"import_success": "Lompakkosi tuonti onnistui.",
"import_title": "tuo",
"list_create_a_button": "Lisää nyt",
"list_create_a_wallet": "Lisää lompakko",
"list_create_a_wallet1": "Se on ilmaista ja voit luoda",
"list_create_a_wallet2": "niin monta kuin haluat",
"list_empty_txs1": "Siirtosi näkyvät tässä,",
"list_empty_txs1_lightning": "Salama-lompakkoa tulisi käyttää päivittäisiin siirtotapahtumiin. Siirtokulut ovat kohtuuttoman halpoja ja nopeus on liekehtivän nopea.",
"list_empty_txs1": "Siirtotapahtumasi näkyvät tässä,",
"list_empty_txs1_lightning": "Lightning-lompakkoa tulisi käyttää päivittäisiin siirtotapahtumiin. Siirtokulut ovat kohtuuttoman halpoja ja nopeus on liekehtivän kova.",
"list_empty_txs2": "Aloita lompakostasi",
"list_empty_txs2_lightning": "Aloita sen käyttäminen napsauttamalla \"hallinnoi varoja\" ja lisää saldoasi.",
"list_header": "Lompakko edustaa salaista paria (yksityinen avain) ja osoitetta, jonka voit jakaa vastaanottaaksesi kolikoita.",
"list_empty_txs2_lightning": "Aloita sen käyttäminen napsauttamalla \"hallinnoi varoja\" ja lisää saldoasi.\n",
"list_header": "Lompakko edustaa avainparia, yhtä yksityistä ja yhtä, jonka voit jakaa vastaanottaaksesi kolikoita.",
"list_import_error": "Tämän lompakon tuomisessa tapahtui virhe.",
"list_import_problem": "Tämän lompakon tuonnissa oli ongelma",
"list_import_problem": "Tämän lompakon tuomisessa oli ongelma",
"list_latest_transaction": "viimeisin siirto",
"list_long_choose": "Valitse Kuva",
"list_long_clipboard": "Kopio leikepöydältä",
"list_long_clipboard": "Kopioi Leikepöydältä",
"list_long_scan": "Skannaa QR-koodi",
"take_photo": "Ota Kuva",
"list_tap_here_to_buy": "Napsauta tästä ostaaksesi Bitcoinia",
"list_tap_here_to_buy": "Osta Bitcoinia",
"list_title": "lompakot",
"list_tryagain": "Yritä uudelleen",
"reorder_title": "Järjestele Lompakot",

View file

@ -1,12 +1,12 @@
{
"_": {
"bad_password": "סיסמה שגויה, נסו שוב.",
"bad_password": "סיסמה שגויה, אנא נסו שוב.",
"cancel": "ביטול",
"continue": "המשך",
"enter_password": זינו סיסמה",
"enter_password": כניסו סיסמה",
"never": "אף פעם",
"of": "{number} של {total}",
"ok": וקיי",
"of": "{number} מתוך {total}",
"ok": ישור",
"storage_is_encrypted": "האחסון שלך מוצפן, נדרשת סיסמה לפתיחה",
"yes": "כן"
},
@ -33,35 +33,35 @@
"are_you_sure_you_want_to_logout": "האם אתם בטוחים שאתם רוצים להתנתק מ- HodlHodl?",
"cont_address_escrow": "פקדון",
"cont_address_to": "עבור",
"cont_buying": ונה",
"cont_buying": נייה",
"cont_cancel": "ביטול חוזה",
"cont_cancel_q": "האם אתם בטוחים שאתם רוצים לבטל חוזה זה?",
"cont_cancel_y": "כן, בטל חוזה.",
"cont_chat": "פתחו שיחה עם הצד השני",
"cont_how": "איך לשלם",
"cont_no": "אין לך שום חוזה פעיל.",
"cont_paid": "סמן חוזה כשולם",
"cont_paid_e": "עשה זאת רק אם שילמת למוכר עם אמצאי התשלום המוסכם",
"cont_paid": "סימון חוזה כשולם",
"cont_paid_e": "עשו זאת רק אם שילמתם למוכר עם אמצעי התשלום המוסכם",
"cont_paid_q": "האם אתם בטוחים שאתם רוצים לסמן חוזה זה כשולם?",
"cont_selling": וכר",
"cont_st_completed": "הכל בוצע",
"cont_selling": כירה",
"cont_st_completed": "הכל בוצע!",
"cont_st_in_progress_buyer": "המטבעות נעולים בפיקדון, אנא שלמו למוכר",
"cont_st_paid_enought": "הביטקוין נעול בפיקדון! אנא שלמו למוכר\nבאמצעי התשלום המוסכם",
"cont_st_paid_waiting": "מחכה לשחרור המטבעות מהפיקדון על-ידי המוכר ",
"cont_st_waiting": "מחכה להפקדת המטבעות בפיקדון על-ידי המוכר ...",
"cont_title": "החוזים שלי",
"filter_any": "הכל",
"filter_buying": ונה",
"filter_buying": נייה",
"filter_country_global": "הצעות גלובליות",
"filter_country_near": "לידי",
"filter_currency": "מטבע",
"filter_detail": "פרטים",
"filter_filters": "מסננים",
"filter_iambuying": "אני רוצה לקנות ביטקוין",
"filter_iamselling": "רוצה למכור ביטקוין",
"filter_iamselling": "אני רוצה למכור ביטקוין",
"filter_method": "אמצעי תשלום",
"filter_search": "חיפוש",
"filter_selling": וכר",
"filter_selling": כירה",
"item_minmax": "מינימום/מקסימום",
"item_nooffers": "אין הצעות. נסו לשנות \"לידי\" להצעות גלובליות!",
"item_rating": "{rating} החלפות",
@ -113,8 +113,8 @@
"help": "בנסיבות מסוימות, יתכן ותאולצו לחשוף את סיסמת הארנק. כדי לשמור על המטבעות בטוחים, BlueWallet מאפשר ליצור אחסון מוצפן נוסף, עם סיסמה שונה. תחת לחץ, תוכלו לחשוף את סיסמה זו לצד שלישי. אם הסיסמה תוכנס ל- BlueWallet, אחסון 'מזויף' חדש יפתח. מצב זה יראה לגיטימי לצד השלישי, בזמן שהאחסון הראשי ישמר בסודיות עם כשהמטבעות מוגנים.",
"help2": "האחסון החדש יתפקד באופן מלא, ותוכלו לאחסן בו סכומים מינימליים כך שיראה יותר מהימן.",
"password_should_not_match": "הסיסמה כבר בשימוש. אנא נסו סיסמה אחרת.",
"passwords_do_not_match": "סיסמה אינה תואמת, נסו שוב",
"retype_password": "סיסמה בשנית",
"passwords_do_not_match": "סיסמאות אינן תואמות, נסו שוב",
"retype_password": "הכניסו שוב סיסמה",
"success": "הצלחה",
"title": "הכחשה סבירה"
},
@ -129,16 +129,16 @@
"title": "ארנקך נוצר..."
},
"receive": {
"details_create": "צרו",
"details_create": "יצירה",
"details_label": "תיאור",
"details_setAmount": "בחר כמות",
"details_setAmount": "קבלה עם סכום",
"details_share": "שיתוף",
"header": "קבלה"
},
"send": {
"broadcastButton": "שדר",
"broadcastError": "שגיאה",
"broadcastNone": "גיבוב קלט העסקה",
"broadcastNone": "קלט גיבוב העברה",
"broadcastPending": "ממתין",
"broadcastSuccess": "הצלחה",
"confirm_header": "אישור",
@ -150,7 +150,7 @@
"create_fee": "עמלה",
"create_memo": "תזכיר",
"create_satoshi_per_byte": "סאטושי לבייט",
"create_this_is_hex": "זוהי העסקה החתומה שלך, מוכנה לשידור לרשת.",
"create_this_is_hex": "זוהי ההעברה שלך, חתומה ומוכנה לשידור לרשת.",
"create_to": "עבור",
"create_tx_size": "גודל ההעברה",
"create_verify": "אמתו ב- coinb.in",
@ -158,11 +158,11 @@
"details_add_rec_rem": "הסרת נמען",
"details_address": "כתובת",
"details_address_field_is_not_valid": "שדה כתובת לא תקין",
"details_adv_fee_bump": "אפשר העלאת עמלה",
"details_adv_fee_bump": "אפשר הקפצת עמלה",
"details_adv_full": "שימוש בכל היתרה",
"details_adv_full_remove": "שאר הנמענים ימחקו מהעברה זו.",
"details_adv_full_sure": "האם אתם בטוחים שתרצו להשתמש בכל יתרת הארנק בשביל עסקה זאת?",
"details_adv_import": "יבוא עסקה",
"details_adv_full_sure": "האם אתם בטוחים שתרצו להשתמש בכל יתרת הארנק בשביל העברה זאת?",
"details_adv_import": "יבוא העברה",
"details_amount_field_is_not_valid": "שדה סכום אינו תקין",
"details_create": "יצירת קבלה",
"details_error_decode": "שגיאה: לא ניתן לפענח כתובת ביטקוין",
@ -170,8 +170,8 @@
"details_next": "הבא",
"details_no_maximum": "הארנק הנבחר אינו תומך בחישוב יתרה מקסימלית אוטומטי. האם לבחור בארנק זה בכל זאת?",
"details_no_multiple": "הארנק הנבחר אינו תומך בשליחת ביטקוין לנמענים מרובים. האם לבחור בארנק זה בכל זאת?",
"details_no_signed_tx": "הקובץ הנבחר אינו מכיל העברה חתומה שניתן לייבא.",
"details_note_placeholder": "פתק לעצמך",
"details_no_signed_tx": "הקובץ הנבחר אינו מכיל העברה שניתן לייבא.",
"details_note_placeholder": "הערה לעצמך",
"details_scan": "סריקה",
"details_total_exceeds_balance": "הסכום לשליחה חורג מהיתרה הזמינה.",
"details_wallet_before_tx": "לפני יצירת העברה, עליך להוסיף ארנק ביטקוין.",
@ -181,16 +181,16 @@
"dynamic_prev": "הקודם",
"dynamic_start": "התחל",
"dynamic_stop": "עצור",
"fee_10m": "10m",
"fee_1d": "1d",
"fee_3h": "3h",
"fee_custom": "Custom",
"fee_fast": "Fast",
"fee_medium": "Medium",
"fee_replace_min": "The total fee rate (satoshi per byte) you want to pay should be higher than {min} sat/byte",
"fee_satbyte": "in sat/byte",
"fee_slow": "Slow",
"header": "שלח",
"fee_10m": "10 ד'",
"fee_1d": "1 י'",
"fee_3h": "3 ש'",
"fee_custom": "אחר",
"fee_fast": "מהיר",
"fee_medium": "בינוני",
"fee_replace_min": "סך כל העמלה (סאטושי לבייט) שתרצו לשלם צריך להיות גבוה מ- {min} סאט/בייט",
"fee_satbyte": "בסאטושי/בייט",
"fee_slow": "איטי",
"header": "שליחה",
"input_clear": "נקה",
"input_done": "בוצע",
"input_paste": "הדבק",
@ -201,11 +201,11 @@
"permission_storage_later": "שאל אותי מאוחר יותר",
"permission_storage_message": "BlueWallet צריך את הרשאתך לגשת לאחסון שלך כדי לשמור את ההעברה.",
"permission_storage_title": "הרשאת גישה לאחסון BlueWallet",
"psbt_clipboard": "העתק ללוח",
"psbt_clipboard": "העתקה ללוח",
"psbt_this_is_psbt": "זוהי העברת ביטקוין חתומה חלקית (PSBT). אנא סיימו את תהליך החתימה בארנק החומרה שלכם.",
"psbt_tx_export": "יצא לקובץ",
"psbt_tx_open": "פתחו עסקה חתומה",
"psbt_tx_scan": "סרקו עסקה חתומה",
"psbt_tx_open": "פתחו העברה חתומה",
"psbt_tx_scan": "סרקו העברה חתומה",
"qr_error_no_qrcode": "התמונה אינה מכילה קוד QR.",
"qr_error_no_wallet": "הקובץ הנבחר אינו מכיל ארנק שניתן לייבא.",
"success_done": "בוצע",
@ -215,7 +215,7 @@
"about": "אודות",
"about_awesome": "נבנה בעזרת",
"about_backup": "תמיד גבו את המפתחות שלכם!",
"about_free": "BlueWallet הינו פרויקט חופשי בקוד פתוח. נוצר על ידי קהילת ביטקוין.",
"about_free": "פרויקט BlueWallet הינו פרויקט חופשי בקוד פתוח. נוצר על ידי קהילת ביטקוין.",
"about_release_notes": "הערות שחרור",
"about_review": "השאירו לנו ביקורת",
"about_selftest": "הרץ בדיקה עצמית",
@ -252,7 +252,7 @@
"general_adv_mode_e": "כאשר מופעל, אפשרויות מתקדמות יוצגו כגון סוגי ארנק שונים, אפשרות להתחבר לצומת LNDHub לפי רצונך ואנטרופיה מותאמת בתהליך יצירת ארנק.",
"general_continuity": "המשכיות",
"general_continuity_e": "כאשר מופעל, תוכלו לצפות בארנקים והעברות נבחרים, באמצעות מכשירי Apple iCloud מחוברים אחרים.",
"groundcontrol_explanation": "GroundControl הינו שרת התראות חופשי בקוד פתוח בשביל ארנקי ביטקוין. באפשרותך להתקין שרת GroundControl אישי ולהכניס את ה- URL שלו כאן, כדי לא להסתמך על התשתית של BlueWallet. השאירו ריק כדי להשתמש בברירת המחדל",
"groundcontrol_explanation": "שרת GroundControl הינו שרת התראות חופשי בקוד פתוח בשביל ארנקי ביטקוין. באפשרותך להתקין שרת GroundControl אישי ולהכניס את ה- URL שלו כאן, כדי לא להסתמך על התשתית של BlueWallet. השאירו ריק כדי להשתמש בברירת המחדל",
"header": "הגדרות",
"language": "שפה",
"language_restart": "כאשר בוחרים שפה חדשה, יתכן ותדרש הפעלה מחדש של BlueWallet כדי שהשינוי ייכנס לתוקף.",
@ -261,7 +261,7 @@
"lightning_settings": "הגדרות ברק",
"lightning_settings_explain": "כדי להתחבר לצומת LND אישי, אנא התקינו LndHub והכניסו את כתובת ה- URL שלו כאן בהגדרות. השאירו ריק כדי להשתמש ב- LNDHub של BlueWallet (lndhub.io). ארנקים שנוצרו אחרי שמירת השינויים יתחברו ל- LNDHub שהוגדר.",
"network": "רשת",
"network_broadcast": "שידור עסקה",
"network_broadcast": "שידור העברה",
"network_electrum": "שרת אלקטרום",
"not_a_valid_uri": "URI לא תקני",
"notifications": "התראות",
@ -270,16 +270,16 @@
"passwords_do_not_match": "סיסמאות לא תואמות",
"plausible_deniability": "הכחשה סבירה",
"push_notifications": "התראות",
"retype_password": "סיסמה בשנית",
"retype_password": "הכניסו שוב סיסמה",
"save": "שמירה",
"saved": "נשמר"
},
"transactions": {
"cancel_explain": "אנו נחליף את ההעברה הזאת באחת עם עמלה גבוהה יותר. פעולה זאת למעשה מבטלת את העברה. פעולה זאת נקראת RBF - Replace By Fee.",
"cancel_no": "העברה זאת אינה ניתנת להחלפה",
"cancel_title": "בטל עסקה זאת (RBF)",
"cancel_title": "בטל העברה זאת (RBF)",
"cpfp_create": "צור",
"cpfp_exp": "אנו ניצור העברה נוספת שתשתמש בעודף שנשאר מההעברה הקודמת שבוצעה. סך כל העמלה יהיה גבוה יותר מעמלת העסקה המקורית, כך שמהירות קבלת האישור אמורה לעלות. פעולה זאת נקראת CPFP - Child Pays For Parent.",
"cpfp_exp": "אנו ניצור העברה נוספת שתשתמש בעודף שנשאר מההעברה הקודמת שבוצעה. סך כל העמלה יהיה גבוה יותר מעמלת ההעברה המקורית, כך שמהירות קבלת האישור אמורה לעלות. פעולה זאת נקראת CPFP - Child Pays For Parent.",
"cpfp_no_bump": "עמלת העברה זו אינה ניתנת להעלאה",
"cpfp_title": "הקפץ עמלה (CPFP)",
"details_block": "גובה הבלוק",
@ -289,23 +289,23 @@
"details_outputs": "פלטים",
"details_received": "התקבל",
"details_show_in_block_explorer": "צפייה בסייר בלוקים",
"details_title": "עסקה",
"details_title": "העברה",
"details_to": "פלט",
"details_transaction_details": "פרטי עסקה",
"details_transaction_details": "פרטי העברה",
"enable_hw": "ארנק זה אינו נמצא בשימוש בצירוף ארנק חומרה. האם תרצו לאפשר שימוש בארנק חומרה?",
"list_conf": "אישורים",
"list_title": "תנועות",
"rbf_explain": "אנו נחליף את העברה זו בהעברה עם עמלה גבוהה יותר, כך שמהירות קבלת האישור אמורה לעלות. פעולה זאת נקראת CPFP - Child Pays For Parent.",
"rbf_title": "העלאת עמלה (RBF)",
"status_bump": "העלאת עמלה",
"status_cancel": "ביטול עסקה",
"status_cancel": "ביטול העברה",
"transactions_count": "מספר תנועות"
},
"wallets": {
"add_bitcoin": "ביטקוין",
"add_create": "יצירה",
"add_entropy_generated": "{gen} בייטים של אנתרופיה",
"add_entropy_provide": "ספקו אנטרופיה בעזרת קוביות ",
"add_entropy_provide": "ספקו אנטרופיה על ידי הטלת קוביות ",
"add_entropy_remain": "{gen} בייטים של אנתרופיה. שאר {rem} בייטים יתקבלו ממחולל מספרים רנדומליים של המערכת.",
"add_import_wallet": "יבוא ארנק",
"import_file": "יבוא קובץ",
@ -341,7 +341,7 @@
"export_title": "יצוא ארנק",
"import_do_import": "יבוא",
"import_error": "היבוא כשל. אנא וודאו שהמידע שסופק תקין.",
"import_explanation": "כתבו כאן את מילות הגיבוי, המפתח הפרטי, WIF או כל דבר אחר שברשותך. BlueWallet ישתדל לנחש את הפורמט הנכון וייבא את ארנק.",
"import_explanation": "כתבו כאן את מילות הגיבוי, המפתח הפרטי, WIF או כל דבר אחר שברשותכם. BlueWallet ישתדל לנחש את הפורמט הנכון וייבא את ארנק.",
"import_imported": "יובא",
"import_scan_qr": "סריקה או יבוא קובץ",
"import_success": "ארנקך יובא בהצלחה.",
@ -352,14 +352,14 @@
"list_create_a_wallet2": "כמה שתרצו",
"list_empty_txs1": "התנועות שלך יופיעו פה",
"list_empty_txs1_lightning": "ארנק הברק משמש לתשלומים יומיומיים. העברות זולות בצורה לא הגיונית המתבצעות במהירות הבזק.",
"list_empty_txs2": "התחילו שימוש בארנק",
"list_empty_txs2": "עם תחילת השימוש בארנק",
"list_empty_txs2_lightning": "\nלהתחלת שימוש לחצו על \"ניהול כספים\" ומלאו את היתרה",
"list_header": "ארנק מייצר צמד מפתחות, מפתח פרטי וכתובת שאותה ניתן לשתף כדי לקבל מטבעות.",
"list_import_error": "התרחשה שגיאה בניסיון לייבא ארנק זה.",
"list_import_problem": "לא ניתן לייבא ארנק הזה",
"list_latest_transaction": "עסקה אחרונה",
"list_latest_transaction": "העברה אחרונה",
"list_long_choose": "בחר תמונה",
"list_long_clipboard": "העתק מלוח",
"list_long_clipboard": "העתקה מלוח",
"list_long_scan": "סריקת קוד QR",
"take_photo": "צילום תמונה",
"list_tap_here_to_buy": "קנו ביטקוין",

View file

@ -170,7 +170,7 @@
"details_next": "Következő",
"details_no_maximum": "A kiválasztott tárca nem támogatja az automatikus teljes egyenleg számítást. Biztosan ezt a tárcát választod?",
"details_no_multiple": "A kiválasztott tárca nem támogatja a Bitcoin küldést több kedvezményezettnek. Biztosan ezt a tárcát választod?",
"details_no_signed_tx": "A kiválasztott fájl nem tartalmaz importálható aláírt tranzakciót.",
"details_no_signed_tx": "A kiválasztott fájl nem tartalmaz importálható tranzakciót.",
"details_note_placeholder": "saját megjegyzés",
"details_scan": "Szkennelés",
"details_total_exceeds_balance": "A megadott összeg nagyobb, mint a tárca elérhető egyenlege",
@ -184,20 +184,20 @@
"fee_10m": "10m",
"fee_1d": "1d",
"fee_3h": "3h",
"fee_custom": "Custom",
"fee_fast": "Fast",
"fee_medium": "Medium",
"fee_replace_min": "The total fee rate (satoshi per byte) you want to pay should be higher than {min} sat/byte",
"fee_satbyte": "in sat/byte",
"fee_slow": "Slow",
"fee_custom": "beállított",
"fee_fast": "Gyors",
"fee_medium": "Közepes",
"fee_replace_min": "A teljes tranzakciós díj (satoshi / byte) amit fizetsz, magasabb lesz, mint a minimum díj.",
"fee_satbyte": "satoshi/byte",
"fee_slow": "Lassú",
"header": "Küldés",
"input_clear": "Törlés",
"input_done": "Kész",
"input_paste": "beillesztés",
"input_total": "Összesen:",
"open_settings": "Beállítások megnyitása",
"permission_camera_message": "Kamera használat engedélyezése",
"permission_camera_title": "Kamera használatának engedélyezése",
"open_settings": "Beállítások megnyitása",
"permission_storage_later": "Később",
"permission_storage_message": "A tranzakció elmentéséhez engedélyezned kell a BlueWallet hozzáférését a háttértárhoz.",
"permission_storage_title": "BlueWallet Tárhely Hozzáférés Engedélyezés",
@ -252,6 +252,7 @@
"general_adv_mode_e": "Ha engedélyezve van, további opciók is elérhetőek, mint különböző tárca típusok, Lightning LNDHub beállítások és entrópia beállítások tárca készítésénél. ",
"general_continuity": "Folytonosság",
"general_continuity_e": "Ha engedélyezve van, láthatod a kiválasztott tárcákat és tranzakciókat más, csatlakoztatott Apple iCloud eszközökön.",
"groundcontrol_explanation": "A GroundControl egy ingyenes, nyílt forráskodú, push üzenetküldő szerver Bitcoin tárcákhoz. Telepítheted a saját GroundControl szerveredet webcímed megadásával, ami függetlet lesz a BlueWallet infrastuktúrától. Hagyd üresen alapértelmezésben.",
"header": "beállítások",
"language": "Nyelv",
"language_restart": "Új nyelv kiválasztásánal, szügség lehet a BlueWallet újraindítására. ",
@ -307,6 +308,7 @@
"add_entropy_provide": "Entrópia megadása véletlenszerűen ",
"add_entropy_remain": "{gen} byte generálva entrópiával. A megmaradt {rem} byte a rendszer véletlenszám generátorával készül.",
"add_import_wallet": "Tárca importálása",
"import_file": "fájl importálása",
"add_lightning": "Lightning",
"add_lndhub": "Kapcsolódj az LNDHub-hoz",
"add_lndhub_error": "A megadott node cím hibás LBDHub node.",
@ -340,7 +342,6 @@
"import_do_import": "Importálás",
"import_error": "Importálás sikertelen. Ellenőrizd, hogy helyes adatokat adtál-e meg.",
"import_explanation": "Írd be a kulcsszavaidat, a titkos kulcsodat, WIF-et, vagy bármi mást. A BlueWallet megpróbálja kitalálni a helyes formátumot, és importálja a tárcádat",
"import_file": "fájl importálása",
"import_imported": "Importálva",
"import_scan_qr": "vagy QR-kód szkennelése?",
"import_success": "Sikeres importálás!",
@ -360,6 +361,7 @@
"list_long_choose": "Válassz fényképet",
"list_long_clipboard": "Másolás vágólapról",
"list_long_scan": "QR kód szkennelése",
"take_photo": "Fénykép készítése",
"list_tap_here_to_buy": "Bitcoin vásárláshoz kattints ide",
"list_title": "tárcák",
"list_tryagain": "Próbáld újra",
@ -367,7 +369,6 @@
"select_no_bitcoin": "Jelenleg nincs elérhető Bitcoin tárca.",
"select_no_bitcoin_exp": "A Lightning tárca feltöltéséhez Bitcoin tárcára van szükség. Készíts vagy importálj egy Bitcoin tárcát.",
"select_wallet": "Válassz tárcát",
"take_photo": "Fénykép készítése",
"xpub_copiedToClipboard": "Vágólapra másolva",
"xpub_title": "a tárca XPUB kulcsa"
}

View file

@ -170,13 +170,13 @@
"details_next": "Next",
"details_no_maximum": "The selected wallet does not support automatic maximum balance calculation. Are you sure to want to select this wallet?",
"details_no_multiple": "The selected wallet does not support sending Bitcoin to multiple recipients. Are you sure to want to select this wallet?",
"details_no_signed_tx": "The selected file does not contain a signed transaction that can be imported.",
"details_no_signed_tx": "The selected file does not contain a transaction that can be imported.",
"details_note_placeholder": "catatan pribadi",
"details_scan": "Pindai",
"details_total_exceeds_balance": "Jumlah yang dikirim melebihi saldo.",
"details_wallet_before_tx": "Before creating a transaction, you must first add a Bitcoin wallet.",
"details_wallet_selection": "Wallet Selection",
"dynamic_init": "Initialing",
"dynamic_init": "Initializing",
"dynamic_next": "Next",
"dynamic_prev": "Previous",
"dynamic_start": "Start",

View file

@ -5,17 +5,107 @@
"continue": "Continua",
"enter_password": "Inserisci password",
"never": "mai",
"of": "{number} su {total}",
"ok": "OK",
"storage_is_encrypted": "Il tuo archivio è criptato. È necessaria una password per decriptarlo"
"storage_is_encrypted": "Il tuo archivio è criptato. È necessaria una password per decriptarlo",
"yes": "Sì"
},
"azteco": {
"codeIs": "Your voucher code is",
"errorBeforeRefeem": "Before redeeming you must first add a Bitcoin wallet.",
"errorSomething": "Something went wrong. Is this voucher still valid?",
"redeem": "Redeem to wallet",
"redeemButton": "Redeem",
"success": "Fatto",
"title": "Redeem Azte.co voucher"
},
"entropy": {
"save": "Salva",
"title": "Entropy",
"undo": "Annulla"
},
"errors": {
"broadcast": "Broadcast failed",
"error": "Errore",
"network": "Errore di rete"
},
"hodl": {
"are_you_sure_you_want_to_logout": "Are you sure you want to logout from HodlHodl?",
"cont_address_escrow": "Escrow",
"cont_address_to": "A",
"cont_buying": "buying",
"cont_cancel": "Cancella contratto",
"cont_cancel_q": "Sei sicuro di voler cancellare questo contratto?",
"cont_cancel_y": "Sì, cancella il contratto",
"cont_chat": "Open chat with counterparty",
"cont_how": "Come pagare",
"cont_no": "You don't have any contracts in progress",
"cont_paid": "Mark contract as Paid",
"cont_paid_e": "Do this only if you sent funds to the seller via agreed payment method",
"cont_paid_q": "Are you sure you want to mark this contract as paid?",
"cont_selling": "selling",
"cont_st_completed": "All done!",
"cont_st_in_progress_buyer": "Coins are in escrow, please pay seller",
"cont_st_paid_enought": "Bitcoins are in escrow! Please pay seller\nvia agreed payment method",
"cont_st_paid_waiting": "Waiting for seller to release coins from escrow",
"cont_st_waiting": "Waiting for seller to deposit bitcoins to escrow...",
"cont_title": "I miei contratti",
"filter_any": "Any",
"filter_buying": "Buying",
"filter_country_global": "Global offers",
"filter_country_near": "Vicino a me",
"filter_currency": "Valuta",
"filter_detail": "Dettaglio",
"filter_filters": "Filtri",
"filter_iambuying": "Sto comprando bitcoin",
"filter_iamselling": "Sto vendendo bitcoin",
"filter_method": "Metodo di pagamento",
"filter_search": "Cerca",
"filter_selling": "Selling",
"item_minmax": "Min/Max",
"item_nooffers": "No offers. Try to change \"Near me\" to Global offers!",
"item_rating": "{rating} trades",
"item_rating_no": "Nessuna recensione",
"login": "Login",
"mycont": "I miei contratti",
"offer_accept": "Accept offer",
"offer_account_finish": "Looks like you didn't finish setting up account on HodlHodl, would you like to finish setup now?",
"offer_choosemethod": "Scegli il metodo di pagamento",
"offer_confirmations": "Conferme",
"offer_minmax": "min / max",
"offer_minutes": "min",
"offer_promt_fiat": "How much {currency} do you want to buy?",
"offer_promt_fiat_e": "For example 100",
"offer_window": "window",
"p2p": "A p2p exchange"
},
"lnd": {
"errorInvoiceExpired": "Invoice expired",
"exchange": "Exchange",
"expired": "Scaduto",
"expiredLow": "expired",
"expiresIn": "Expires: {time}",
"payButton": "Pay",
"placeholder": "Fattura",
"potentialFee": "Commissioni potenziali: {fee}",
"refill": "Ricarica",
"refill_card": "Ricarica con una carta",
"refill_create": "In order to proceed, please create a Bitcoin wallet to refill with.",
"refill_external": "Refill with External Wallet",
"refill_lnd_balance": "Ricarica saldo del portafoglio Lightning",
"sameWalletAsInvoiceError": "Non puoi pagare una fattura con lo stesso portafoglio utilizzato per crearla.",
"title": "Gestisci fondi"
},
"lndViewInvoice": {
"additional_info": "Additional Information",
"for": "For:",
"has_been_paid": "This invoice has been paid for",
"open_direct_channel": "Open direct channel with this node:",
"please_pay": "Please pay",
"preimage": "Preimage",
"sats": "sats",
"wasnt_paid_and_expired": "This invoice was not paid for and has expired"
},
"plausibledeniability": {
"create_fake_storage": "Crea archivio falso criptato",
"create_password": "Crea una password",
@ -28,6 +118,16 @@
"success": "Fatto",
"title": "Negazione Plausibile"
},
"pleasebackup": {
"ask": "Have you saved your wallet's backup phrase? This backup phrase is required to access your funds in case you lose this device. Without the backup phrase, your funds will be permanently lost.",
"ask_no": "No, I have not",
"ask_yes": "Yes, I have",
"ok": "OK, I wrote this down!",
"ok_lnd": "OK, I have saved it.",
"text": "Please take a moment to write down this mnemonic phrase on a piece of paper. It's your backup you can use to restore the wallet on other device.",
"text_lnd": "Please take a moment to save this LNDHub authentication. It's your backup you can use to restore the wallet on other device.",
"title": "Your wallet is created..."
},
"receive": {
"details_create": "Crea",
"details_label": "Descrizione",
@ -36,65 +136,207 @@
"header": "Ricevi"
},
"send": {
"broadcastButton": "BROADCAST",
"broadcastError": "error",
"broadcastNone": "Input transaction hash",
"broadcastPending": "pending",
"broadcastSuccess": "success",
"confirm_header": "Conferma",
"confirm_sendNow": "Invia ora",
"create_amount": "Importo",
"create_broadcast": "Trasmetti",
"create_copy": "Copy and broadcast later",
"create_details": "Dettagli",
"create_fee": "Commissione",
"create_memo": "Memo",
"create_satoshi_per_byte": "Satoshi per byte",
"create_this_is_hex": "Questo è l'hex della transazione, firmato e pronto per essere trasmesso sulla rete.",
"create_to": "A",
"create_tx_size": "Grandezza TX",
"create_verify": "Verify on coinb.in",
"details_add_rec_add": "Add Recipient",
"details_add_rec_rem": "Remove Recipient",
"details_address": "Indirizzo",
"details_address_field_is_not_valid": "Indirizzo non valido",
"details_adv_fee_bump": "Allow Fee Bump",
"details_adv_full": "Use Full Balance",
"details_adv_full_remove": "Your other recipients will be removed from this transaction.",
"details_adv_full_sure": "Are you sure you want to use your wallet's full balance for this transaction?",
"details_adv_import": "Import Transaction",
"details_amount_field_is_not_valid": "Importo non valido",
"details_create": "Crea",
"details_error_decode": "Error: Unable to decode Bitcoin address",
"details_fee_field_is_not_valid": "Commissione non valida",
"details_next": "Next",
"details_no_maximum": "The selected wallet does not support automatic maximum balance calculation. Are you sure to want to select this wallet?",
"details_no_multiple": "The selected wallet does not support sending Bitcoin to multiple recipients. Are you sure to want to select this wallet?",
"details_no_signed_tx": "The selected file does not contain a transaction that can be imported.",
"details_note_placeholder": "Nota",
"details_scan": "Scansiona",
"details_total_exceeds_balance": "L'importo da inviare eccede i fondi disponibili.",
"details_wallet_before_tx": "Before creating a transaction, you must first add a Bitcoin wallet.",
"details_wallet_selection": "Wallet Selection",
"dynamic_init": "Initializing",
"dynamic_next": "Next",
"dynamic_prev": "Previous",
"dynamic_start": "Start",
"dynamic_stop": "Stop",
"fee_10m": "10m",
"fee_1d": "1d",
"fee_3h": "3h",
"fee_custom": "Custom",
"fee_fast": "Fast",
"fee_medium": "Medium",
"fee_replace_min": "The total fee rate (satoshi per byte) you want to pay should be higher than {min} sat/byte",
"fee_satbyte": "in sat/byte",
"fee_slow": "Slow",
"header": "Invia",
"success_done": "Fatto"
"input_clear": "Clear",
"input_done": "Done",
"input_paste": "Paste",
"input_total": "Total:",
"permission_camera_message": "We need your permission to use your camera",
"permission_camera_title": "Permission to use camera",
"open_settings": "Open Settings",
"permission_storage_later": "Ask Me Later",
"permission_storage_message": "BlueWallet needs your permission to access your storage to save this transaction.",
"permission_storage_title": "BlueWallet Storage Access Permission",
"psbt_clipboard": "Copy to Clipboard",
"psbt_this_is_psbt": "This is a partially signed bitcoin transaction (PSBT). Please finish signing it with your hardware wallet.",
"psbt_tx_export": "Export to file",
"psbt_tx_open": "Open Signed Transaction",
"psbt_tx_scan": "Scan Signed Transaction",
"qr_error_no_qrcode": "The selected image does not contain a QR Code.",
"qr_error_no_wallet": "The selected file does not contain a wallet that can be imported.",
"success_done": "Fatto",
"txSaved": "The transaction file ({filePath}) has been saved in your Downloads folder ."
},
"settings": {
"about": "Informazioni",
"about_awesome": "Built with the awesome",
"about_backup": "Always backup your keys!",
"about_free": "BlueWallet is a free and open source project. Crafted by Bitcoin users.",
"about_release_notes": "Release notes",
"about_review": "Leave us a review",
"about_selftest": "Run self test",
"about_sm_github": "GitHub",
"about_sm_telegram": "Telegram chat",
"about_sm_twitter": "Follow us on Twitter",
"advanced_options": "Advanced Options",
"currency": "Valuta",
"currency_source": "Prices are obtained from CoinDesk",
"default_desc": "When disabled, BlueWallet will immediately open the selected wallet at launch.",
"default_info": "Default info",
"default_title": "On Launch",
"default_wallets": "View All Wallets",
"electrum_connected": "Connected",
"electrum_connected_not": "Not Connected",
"electrum_error_connect": "Can't connect to provided Electrum server",
"electrum_host": "host, for example {example}",
"electrum_port": "TCP port, usually {example}",
"electrum_port_ssl": "SSL port, usually {example}",
"electrum_saved": "Your changes have been saved successfully. Restart may be required for changes to take effect.",
"electrum_settings": "Electrum Settings",
"electrum_settings_explain": "Set to blank to use default",
"electrum_status": "Status",
"encrypt_decrypt": "Decrypt Storage",
"encrypt_decrypt_q": "Are you sure you want to decrypt your storage? This will allow your wallets to be accessed without a password.",
"encrypt_del_uninstall": "Delete if BlueWallet is uninstalled",
"encrypt_enc_and_pass": "Encrypted and Password protected",
"encrypt_title": "Security",
"encrypt_tstorage": "storage",
"encrypt_use": "Use {type}",
"encrypt_use_expl": "{type} will be used to confirm your identity prior to making a transaction, unlocking, exporting or deleting a wallet. {type} will not be used to unlock an encrypted storage.",
"general": "General",
"general_adv_mode": "Enable advanced mode",
"general_adv_mode_e": "When enabled, you will see advanced options such as different wallet types, the ability to specify the LNDHub instance you wish to connect to and custom entropy during wallet creation.",
"general_continuity": "Continuity",
"general_continuity_e": "When enabled, you will be able to view selected wallets, and transactions, using your other Apple iCloud connected devices.",
"groundcontrol_explanation": "GroundControl is a free opensource push notifications server for bitcoin wallets. You can install your own GroundControl server and put its URL here to not rely on BlueWallet's infrastructure. Leave blank to use default",
"header": "Impostazioni",
"language": "Lingua",
"language_restart": "When selecting a new language, restarting BlueWallet may be required for the change to take effect.",
"lightning_error_lndhub_uri": "Not a valid LndHub URI",
"lightning_saved": "Your changes have been saved successfully",
"lightning_settings": "Impostazioni Lightning",
"lightning_settings_explain": "Per connetterti al tuo nodo LND personale installa LndHub e inserisci il suo URL qui nelle impostazioni. Lascialo vuoto per utilizzare il nodo LndHub di default (lndhub.io)",
"network": "Network",
"network_broadcast": "Broadcast transaction",
"network_electrum": "Electrum server",
"not_a_valid_uri": "Not a valid URI",
"notifications": "Notifications",
"password": "Password",
"password_explain": "Crea la password che userai per decriptare l'archivio",
"passwords_do_not_match": "Le password non corrispondono",
"plausible_deniability": "Negazione plausibile...",
"push_notifications": "Push notifications",
"retype_password": "Reinserisci password",
"save": "Salva"
"save": "Salva",
"saved": "Saved"
},
"transactions": {
"cancel_explain": "We will replace this transaction with the one that pays you and has higher fees. This effectively cancels transaction. This is called RBF - Replace By Fee.",
"cancel_no": "This transaction is not replaceable",
"cancel_title": "Cancel this transaction (RBF)",
"cpfp_create": "Create",
"cpfp_exp": "We will create another transaction that spends your unconfirmed transaction. The total fee will be higher than the original transaction fee, so it should be mined faster. This is called CPFP - Child Pays For Parent.",
"cpfp_no_bump": "This transaction is not bumpable",
"cpfp_title": "Bump fee (CPFP)",
"details_block": "Block Height",
"details_copy": "Copia",
"details_from": "Da",
"details_inputs": "Inputs",
"details_outputs": "Outputs",
"details_received": "Received",
"details_show_in_block_explorer": "Mostra sul block explorer",
"details_title": "Transazione",
"details_to": "A",
"details_transaction_details": "Dettagli transazione",
"list_title": "Transazioni"
"enable_hw": "This wallet is not being used in conjunction with a hardwarde wallet. Would you like to enable hardware wallet use?",
"list_conf": "conf",
"list_title": "Transazioni",
"rbf_explain": "We will replace this transaction with the one with a higher fee, so it should be mined faster. This is called RBF - Replace By Fee.",
"rbf_title": "Bump fee (RBF)",
"status_bump": "Bump Fee",
"status_cancel": "Cancel Transaction",
"transactions_count": "transactions count"
},
"wallets": {
"add_bitcoin": "Bitcoin",
"add_create": "Crea",
"add_entropy_generated": "{gen} bytes of generated entropy",
"add_entropy_provide": "Provide entropy via dice rolls",
"add_entropy_remain": "{gen} bytes of generated entropy. Remaining {rem} bytes will be obtained from the System random number generator.",
"add_import_wallet": "Importa Portafoglio",
"import_file": "Import File",
"add_lightning": "Lightning",
"add_lndhub": "Connect to your LNDHub",
"add_lndhub_error": "The provided node address is not valid LNDHub node.",
"add_lndhub_placeholder": "your node address",
"add_or": "o",
"add_title": "Aggiungi Portafoglio",
"add_wallet_name": "Nome Portafoglio",
"add_wallet_type": "Tipo",
"details_address": "Indirizzo",
"details_advanced": "Advanced",
"details_are_you_sure": "Sei sicuro?",
"details_connected_to": "Connected to",
"details_del_wb": "Wallet Balance",
"details_del_wb_err": "The provided balance amount does not match this wallet's balance. Please, try again",
"details_del_wb_q": "This wallet has a balance. Before proceeding, please be aware that you will not be able to recover the funds without this wallet's seed phrase. In order to avoid accidental removal this wallet, please enter your wallet's balance of {balance} satoshis.",
"details_delete": "Elimina",
"details_delete_wallet": "Delete wallet",
"details_display": "display in wallets list",
"details_export_backup": "Esporta / Backup",
"details_marketplace": "Marketplace",
"details_master_fingerprint": "Master fingerprint",
"details_no_cancel": "No, annulla",
"details_save": "Salva",
"details_show_xpub": "Mostra XPUB del portafoglio",
"details_title": "Portafoglio",
"details_type": "Tipo",
"details_use_with_hardware_wallet": "Use with hardware wallet",
"details_wallet_updated": "Wallet updated",
"details_yes_delete": "Si, elimina",
"export_title": "Esporta portafoglio",
"import_do_import": "Importa",
@ -109,11 +351,23 @@
"list_create_a_wallet1": "È gratuito e puoi crearne",
"list_create_a_wallet2": "quanti ne vuoi",
"list_empty_txs1": "Le tue transazioni appariranno qui,",
"list_empty_txs1_lightning": "Lightning wallet should be used for your daily transactions. Fees are unfairly cheap and speed is blazing fast.",
"list_empty_txs2": "Nessuna transazione",
"list_empty_txs2_lightning": "\nTo start using it tap on \"manage funds\" and topup your balance.",
"list_header": "A wallet represents a pair of keys, one private and one you can share to receive coins.",
"list_import_error": "An error was encountered when attempting to import this wallet.",
"list_import_problem": "There was a problem importing this wallet",
"list_latest_transaction": "Transazioni recenti",
"list_long_choose": "Choose Photo",
"list_long_clipboard": "Copy from Clipboard",
"list_long_scan": "Scan QR Code",
"take_photo": "Take Photo",
"list_tap_here_to_buy": "Clicca qui per comprare Bitcoin",
"list_title": "Portafogli",
"list_tryagain": "Try Again",
"reorder_title": "Riordina Portafogli",
"select_no_bitcoin": "There are currently no Bitcoin wallets available.",
"select_no_bitcoin_exp": "A Bitcoin wallet is required to refill Lightning wallets. Please, create or import one.",
"select_wallet": "Seleziona Portafoglio",
"xpub_copiedToClipboard": "Copiata negli appunti.",
"xpub_title": "XPUB del Portafoglio"

View file

@ -32,7 +32,7 @@
"hodl": {
"are_you_sure_you_want_to_logout": "本当にHodlHodlからログアウトしますか",
"cont_address_escrow": "エスクロー",
"cont_address_to": "To",
"cont_address_to": "宛先",
"cont_buying": "買う",
"cont_cancel": "コントラクトをキャンセル",
"cont_cancel_q": "本当にこのコントラクトをキャンセルしますか?",
@ -50,8 +50,8 @@
"cont_st_paid_waiting": "販売者がエスクローからコインをリリースするのを待っています。",
"cont_st_waiting": "販売者がエスクローにビットコインを預けるのを待っています。",
"cont_title": "マイコントラクト",
"filter_any": "Any",
"filter_buying": "Buying",
"filter_any": "すべて",
"filter_buying": "買う",
"filter_country_global": "グローバル・オファー",
"filter_country_near": "近くで探す",
"filter_currency": "通貨",
@ -74,7 +74,7 @@
"offer_confirmations": "承認",
"offer_minmax": "最小 / 最大",
"offer_minutes": "分",
"offer_promt_fiat": "How much {currency} do you want to buy?",
"offer_promt_fiat": "いくら {currency} を購入しますか?",
"offer_promt_fiat_e": "例 100",
"offer_window": "ウィンドウ",
"p2p": "P2P エクスチェンジ"
@ -89,9 +89,9 @@
"placeholder": "入金依頼",
"potentialFee": "手数料推計: {fee}",
"refill": "送金",
"refill_card": "Refill with bank card",
"refill_create": "In order to proceed, please create a Bitcoin wallet to refill with.",
"refill_external": "Refill with External Wallet",
"refill_card": "銀行カードで補充する",
"refill_create": "先に進むためには、ビットコインウォレットを作成して補充してください。",
"refill_external": "外部ウォレットで補充",
"refill_lnd_balance": "Lightning ウォレットへ送金",
"sameWalletAsInvoiceError": "以前作成したウォレットと同じウォレットへの支払いはできません。",
"title": "資金の管理"
@ -125,7 +125,7 @@
"ok": "すべてのニモニックを書きとめました",
"ok_lnd": "はい、書きとめました",
"text": "すべてのニモニックを別紙に書きとめてください。他のデバイスへウォレットをリストアする際にニモニックが必要になります。デスクトップ用ウォレットの Electrum wallet (https://electrum.org/) へニモニックを使用してウォレットをリストアすることが可能です。",
"text_lnd": "Please take a moment to save this LNDHub authentication. It's your backup you can use to restore the wallet on other device.",
"text_lnd": "このLNDHub認証を保存しておいてください。これはあなたのバックアップであり、他のデバイス上でウォレットを復元するために使用できます。",
"title": "ウォレットを作成しています..."
},
"receive": {
@ -136,9 +136,9 @@
"header": "入金"
},
"send": {
"broadcastButton": "BROADCAST",
"broadcastButton": "ブロードキャスト",
"broadcastError": "エラー",
"broadcastNone": "Input transaction hash",
"broadcastNone": "インプットトランザクションハッシュ",
"broadcastPending": "試行中",
"broadcastSuccess": "成功",
"confirm_header": "確認",
@ -158,38 +158,38 @@
"details_add_rec_rem": "宛先を削除",
"details_address": "アドレス",
"details_address_field_is_not_valid": "アドレス欄が正しくありません",
"details_adv_fee_bump": "Allow Fee Bump",
"details_adv_full": "Use Full Balance",
"details_adv_full_remove": "Your other recipients will be removed from this transaction.",
"details_adv_full_sure": "Are you sure you want to use your wallet's full balance for this transaction?",
"details_adv_import": "Import Transaction",
"details_adv_fee_bump": "費用のバンプ(増加)を許可",
"details_adv_full": "全残高を使う",
"details_adv_full_remove": "BlueWalletがアンインストールされたら消去",
"details_adv_full_sure": "本当にこのウォレットの全残高をこのトランザクションに利用しますか?",
"details_adv_import": "トランザクションをインポート",
"details_amount_field_is_not_valid": "金額欄が正しくありません",
"details_create": "作成",
"details_error_decode": "Error: Unable to decode Bitcoin address",
"details_error_decode": "エラー:ビットコインアドレスを復号できません",
"details_fee_field_is_not_valid": "手数料欄が正しくありません",
"details_next": "次",
"details_no_maximum": "選択したウォレットは、最大残高の自動計算に対応していません。このウォレットを選択してもよろしいですか?",
"details_no_multiple": "選択したウォレットは、複数の受信者へのビットコインの送信をサポートしていません。このウォレットを選択してもよろしいですか?",
"details_no_signed_tx": "The selected file does not contain a signed transaction that can be imported.",
"details_no_signed_tx": "選択したファイルには、インポート可能なトランザクションが含まれていません。",
"details_note_placeholder": "ラベル",
"details_scan": "読取り",
"details_total_exceeds_balance": "送金額が利用可能残額を超えています。",
"details_wallet_before_tx": "Before creating a transaction, you must first add a Bitcoin wallet.",
"details_wallet_before_tx": "トランザクションを作成する前に、まずはビットコインウォレットを追加する必要があります。",
"details_wallet_selection": "ウォレット選択",
"dynamic_init": "インストール中",
"dynamic_next": "次",
"dynamic_prev": "前",
"dynamic_start": "スタート",
"dynamic_stop": "ストップ",
"fee_10m": "10m",
"fee_1d": "1d",
"fee_3h": "3h",
"fee_custom": "Custom",
"fee_fast": "Fast",
"fee_medium": "Medium",
"fee_replace_min": "The total fee rate (satoshi per byte) you want to pay should be higher than {min} sat/byte",
"fee_satbyte": "in sat/byte",
"fee_slow": "Slow",
"fee_10m": "10",
"fee_1d": "1",
"fee_3h": "3時間",
"fee_custom": "カスタム",
"fee_fast": "高速",
"fee_medium": "中速",
"fee_replace_min": "支払いたい手数料レート(1バイトあたりのsatoshi)は、{min} sat/byteよりも高い必要があります。",
"fee_satbyte": "sat/バイト",
"fee_slow": "低速",
"header": "送金",
"input_clear": "クリア",
"input_done": "完了",
@ -197,71 +197,71 @@
"input_total": "合計:",
"permission_camera_message": "カメラを使用するのに許可が必要です",
"permission_camera_title": "カメラの使用許可",
"open_settings": "Open Settings",
"permission_storage_later": "Ask Me Later",
"permission_storage_message": "BlueWallet needs your permission to access your storage to save this transaction.",
"permission_storage_title": "BlueWallet Storage Access Permission",
"open_settings": "設定を開く",
"permission_storage_later": "後で聞く",
"permission_storage_message": "BlueWalletはこのトランザクションを保存するためにストレージへのアクセス許可を必要としています。",
"permission_storage_title": "BlueWalletストレージアクセス許可",
"psbt_clipboard": "クリップボードにコピー",
"psbt_this_is_psbt": "これは部分的に署名されたビットコイントランザクションPSBTです。ハードウェアウォレットで署名を完了させてください。",
"psbt_tx_export": "ファイルにエクスポート",
"psbt_tx_open": "署名トランザクションを開く",
"psbt_tx_scan": "署名トランザクションをスキャン",
"qr_error_no_qrcode": "選択された画像はQRコードを含んでいません。",
"qr_error_no_wallet": "The selected file does not contain a wallet that can be imported.",
"qr_error_no_wallet": "選択されたファイルにはインポートできるウォレットが含まれていません。",
"success_done": "完了",
"txSaved": "The transaction file ({filePath}) has been saved in your Downloads folder ."
"txSaved": "トランザクションファイル ({filePath}) はダウンロードフォルダに保存されました。"
},
"settings": {
"about": "BlueWallet について",
"about_awesome": "Built with the awesome",
"about_backup": "Always backup your keys!",
"about_free": "BlueWallet is a free and open source project. Crafted by Bitcoin users.",
"about_release_notes": "Release notes",
"about_backup": "常に秘密鍵はバックアップしましょう!",
"about_free": "BlueWalletはフリーでオープンソースのプロジェクトです。ビットコインユーザーによって作られています。",
"about_release_notes": "リリースノート",
"about_review": "レビューを書く",
"about_selftest": "Run self test",
"about_selftest": "セルフテストを実行",
"about_sm_github": "GitHub",
"about_sm_telegram": "テレグラムチャット",
"about_sm_twitter": "Twitterでフォロー",
"advanced_options": "上級設定",
"currency": "通貨",
"currency_source": "CoinDeskから取得された価格",
"default_desc": "When disabled, BlueWallet will immediately open the selected wallet at launch.",
"default_info": "Default info",
"default_title": "On Launch",
"default_wallets": "View All Wallets",
"electrum_connected": "Connected",
"electrum_connected_not": "Not Connected",
"electrum_error_connect": "Can't connect to provided Electrum server",
"electrum_host": "host, for example {example}",
"electrum_port": "TCP port, usually {example}",
"electrum_port_ssl": "SSL port, usually {example}",
"electrum_saved": "Your changes have been saved successfully. Restart may be required for changes to take effect.",
"electrum_settings": "Electrum Settings",
"electrum_settings_explain": "Set to blank to use default",
"default_desc": "無効にすれば、BlueWalletは起動時に選択したウォレットをすぐに開きます。",
"default_info": "デフォルト情報",
"default_title": "起動時",
"default_wallets": "すべてのウォレットを確認",
"electrum_connected": "接続済",
"electrum_connected_not": "未接続",
"electrum_error_connect": "指定されたElectrumサーバーに接続できません",
"electrum_host": "ホスト 例 {example}",
"electrum_port": "TCP ポート 通常 {example}",
"electrum_port_ssl": "SSL ポート 通常 {example}",
"electrum_saved": "変更は正常に保存されました。変更の適用には、リスタートが必要な場合があります。",
"electrum_settings": "Electrum 設定",
"electrum_settings_explain": "ブランクに設定してデフォルトを使用",
"electrum_status": "ステータス",
"encrypt_decrypt": "ストレージ復号化",
"encrypt_decrypt_q": "Are you sure you want to decrypt your storage? This will allow your wallets to be accessed without a password.",
"encrypt_del_uninstall": "Delete if BlueWallet is uninstalled",
"encrypt_enc_and_pass": "Encrypted and Password protected",
"encrypt_decrypt_q": "本当にストレージを復号化しますか?これによりウォレットがパスワードなしでアクセス可能になります。",
"encrypt_del_uninstall": "BlueWalletをアンインストールしたら削除",
"encrypt_enc_and_pass": "暗号化されパスワードで保護されています",
"encrypt_title": "セキュリティ",
"encrypt_tstorage": "ストレージ",
"encrypt_use": "{type} を使う",
"encrypt_use_expl": "{type} は、トランザクションの実行、ウォレットのロック解除、エクスポート、または削除を行う前の本人確認に使用されます。{type} は暗号化されたストレージのロック解除には使用されません。",
"general": "一般情報",
"general_adv_mode": "Enable advanced mode",
"general_adv_mode_e": "When enabled, you will see advanced options such as different wallet types, the ability to specify the LNDHub instance you wish to connect to and custom entropy during wallet creation.",
"general_continuity": "Continuity",
"general_continuity_e": "When enabled, you will be able to view selected wallets, and transactions, using your other Apple iCloud connected devices.",
"groundcontrol_explanation": "GroundControl is a free opensource push notifications server for bitcoin wallets. You can install your own GroundControl server and put its URL here to not rely on BlueWallet's infrastructure. Leave blank to use default",
"general_adv_mode_e": "この機能を有効にすると、異なるウォレットタイプ、接続先の LNDHub インスタンスの指定、ウォレット作成時のカスタムエントロピーなどの高度なオプションが表示されます。",
"general_continuity": "継続性",
"general_continuity_e": "この機能を有効にすると、Apple iCloudに接続している他のデバイスを使用して、選択したウォレットやトランザクションを表示できるようになります。",
"groundcontrol_explanation": "GroundControlはビットコインウォレットのための無料のオープンソースのプッシュ通知サーバーです。独自のGroundControlサーバーをインストールし、BlueWalletのインフラに依存しないようにURLをここに入力することができます。デフォルトを使用するには空白のままにしてください。",
"header": "設定",
"language": "言語",
"language_restart": "When selecting a new language, restarting BlueWallet may be required for the change to take effect.",
"lightning_error_lndhub_uri": "Not a valid LndHub URI",
"lightning_saved": "Your changes have been saved successfully",
"language_restart": "新しい言語を選択した場合、変更を有効にするには BlueWallet の再起動が必要な場合があります。",
"lightning_error_lndhub_uri": "有効なLndHub URIではありません",
"lightning_saved": "変更は正常に保存されました",
"lightning_settings": "Lightning 設定",
"lightning_settings_explain": "他の LND ノードへ接続するには LndHub をインストール後、URL を入力してください。既定の設定を使用するには空欄にしますndHub\n (lndhub.io)",
"network": "Network",
"network_broadcast": "Broadcast transaction",
"network": "ネットワーク",
"network_broadcast": "ブロードキャストトランザクション",
"network_electrum": "Electrum サーバー",
"not_a_valid_uri": "有効なURIではありません",
"notifications": "通知",
@ -275,11 +275,11 @@
"saved": "保存済"
},
"transactions": {
"cancel_explain": "We will replace this transaction with the one that pays you and has higher fees. This effectively cancels transaction. This is called RBF - Replace By Fee.",
"cancel_explain": "このトランザクションを、最初の支払い時より高い手数料を持つものに置き換えます。これは事実上、最初のトランザクションをキャンセルします。これはReplace By Fee - RBFと呼ばれています。",
"cancel_no": "このトランザクションは交換可能ではありません",
"cancel_title": "このトランザクションをキャンセル (RBF)",
"cpfp_create": "作成",
"cpfp_exp": "We will create another transaction that spends your unconfirmed transaction. The total fee will be higher than the original transaction fee, so it should be mined faster. This is called CPFP - Child Pays For Parent.",
"cpfp_exp": "あなたの未承認トランザクションを消費する別のトランザクションを作成します。元のトランザクションの手数料よりも合計金額が高くなるため、より早くマイニングされます。これはCPFP - Child Pays For Parentと呼ばれています。",
"cpfp_no_bump": "このトランザクションはバンプ可能ではありません",
"cpfp_title": "バンプ費用 (CPFP)",
"details_block": "ブロック高",
@ -292,44 +292,44 @@
"details_title": "取引",
"details_to": "送り先",
"details_transaction_details": "取引詳細",
"enable_hw": "This wallet is not being used in conjunction with a hardwarde wallet. Would you like to enable hardware wallet use?",
"list_conf": "確認",
"enable_hw": "このウォレットはハードウォレットとの併用はされていません。ハードウェアウォレットの使用を有効にしますか?",
"list_conf": "コンファメーション: {number}",
"list_title": "取引",
"rbf_explain": "We will replace this transaction with the one with a higher fee, so it should be mined faster. This is called RBF - Replace By Fee.",
"rbf_title": "Bump fee (RBF)",
"status_bump": "Bump Fee",
"status_cancel": "Cancel Transaction",
"transactions_count": "transactions count"
"rbf_explain": "このトランザクションを手数料の高いものに置き換えるので、マイニングが早くなるはずです。これをRBF - Replace By Feeといいます。",
"rbf_title": "手数料をバンプ (RBF)",
"status_bump": "手数料をバンプ",
"status_cancel": "トランザクションをキャンセル",
"transactions_count": "トランザクションカウント"
},
"wallets": {
"add_bitcoin": "ビットコイン",
"add_create": "作成",
"add_entropy_generated": "{gen} bytes of generated entropy",
"add_entropy_generated": "生成されたエントロピーの {gen} バイト",
"add_entropy_provide": "サイコロを振ってエントロピーを提供",
"add_entropy_remain": "{gen} bytes of generated entropy. Remaining {rem} bytes will be obtained from the System random number generator.",
"add_entropy_remain": "生成されたエントロピーの{gen}バイト。残りの{rem}バイトはシステム乱数発生器から取得されます。",
"add_import_wallet": "ウォレットをインポート",
"import_file": "Import File",
"add_lightning": "Lightning",
"add_lndhub": "Connect to your LNDHub",
"add_lndhub_error": "The provided node address is not valid LNDHub node.",
"add_lndhub_placeholder": "your node address",
"import_file": "インポートファイル",
"add_lightning": "ライトニング",
"add_lndhub": "あなたのLNDHubに接続",
"add_lndhub_error": "指定されたードアドレスは有効なLNDHubードではありません。",
"add_lndhub_placeholder": "あなたのノードアドレス",
"add_or": "又は",
"add_title": "ウォレットの追加",
"add_wallet_name": "ウォレット名",
"add_wallet_type": "タイプ",
"details_address": "アドレス",
"details_advanced": "Advanced",
"details_advanced": "上級設定",
"details_are_you_sure": "実行しますか?",
"details_connected_to": "接続",
"details_del_wb": "ウォレット残高",
"details_del_wb_err": "The provided balance amount does not match this wallet's balance. Please, try again",
"details_del_wb_q": "This wallet has a balance. Before proceeding, please be aware that you will not be able to recover the funds without this wallet's seed phrase. In order to avoid accidental removal this wallet, please enter your wallet's balance of {balance} satoshis.",
"details_del_wb_err": "提供された残高は、このウォレットの残高と一致しません。もう一度お試しください。",
"details_del_wb_q": "このウォレットには残高があります。先に進む前に、このウォレットのシードフレーズがないと資金を回収できない点に注意してください。誤ってこのウォレットを削除しないようにするために、このウォレットの残高 {balance} satoshisを入力してください。",
"details_delete": "削除",
"details_delete_wallet": "ウォレット削除",
"details_display": "display in wallets list",
"details_display": "ウォレットリストで表示",
"details_export_backup": "エクスポート / バックアップ",
"details_marketplace": "Marketplace",
"details_master_fingerprint": "Master fingerprint",
"details_marketplace": "マーケットプレイス",
"details_master_fingerprint": "マスタフィンガープリント",
"details_no_cancel": "いいえ、中止します",
"details_save": "保存",
"details_show_xpub": "ウォレット XPUB の表示",
@ -354,8 +354,8 @@
"list_empty_txs1_lightning": "Lightning ウォレットを日常の取引にご利用ください。手数料は安く、送金はあっという間に完了します。",
"list_empty_txs2": "現在は何もありません",
"list_empty_txs2_lightning": "\n利用を開始するには\"資金の管理\"をタップしてウォレットへ送金してください。",
"list_header": "A wallet represents a pair of keys, one private and one you can share to receive coins.",
"list_import_error": "An error was encountered when attempting to import this wallet.",
"list_header": "ウォレットが表示する鍵のペアは、ひとつが秘密鍵、もうひとつはコインを受け取るために他人と共有することができる鍵です。",
"list_import_error": "ウォレットのインポート時にエラーが起こりました。",
"list_import_problem": "このウォレットのインポートに問題が生じました",
"list_latest_transaction": "最新の取引",
"list_long_choose": "写真選択",

View file

@ -170,13 +170,13 @@
"details_next": "Volgende",
"details_no_maximum": "The selected wallet does not support automatic maximum balance calculation. Are you sure to want to select this wallet?",
"details_no_multiple": "The selected wallet does not support sending Bitcoin to multiple recipients. Are you sure to want to select this wallet?",
"details_no_signed_tx": "The selected file does not contain a signed transaction that can be imported.",
"details_no_signed_tx": "The selected file does not contain a transaction that can be imported.",
"details_note_placeholder": "notitie voor mezelf",
"details_scan": "Scan",
"details_total_exceeds_balance": "Het verzendingsbedrag overschrijdt het beschikbare saldo.",
"details_wallet_before_tx": "Before creating a transaction, you must first add a Bitcoin wallet.",
"details_wallet_selection": "Wallet Selection",
"dynamic_init": "Initialing",
"dynamic_init": "Initializing",
"dynamic_next": "Volgende",
"dynamic_prev": "Vorige",
"dynamic_start": "Start",

View file

@ -170,7 +170,7 @@
"details_next": "Próximo",
"details_no_maximum": "A carteira selecionada não suporta cálculo automático de saldo máximo. Tem certeza que deseja selecionar esta carteira?",
"details_no_multiple": "A carteira selecionada não suporta o envio de Bitcoins para vários destinatários. Tem certeza que deseja selecionar esta carteira?",
"details_no_signed_tx": "O arquivo selecionado não contém uma transação assinada que possa ser importada.",
"details_no_signed_tx": "O arquivo selecionado não contém uma transação que possa ser importada.",
"details_note_placeholder": "Nota pessoal",
"details_scan": "Ler",
"details_total_exceeds_balance": "Valor total excede o saldo disponível",
@ -181,6 +181,15 @@
"dynamic_prev": "Anterior",
"dynamic_start": "Começar",
"dynamic_stop": "Parar",
"fee_10m": "10m",
"fee_1d": "1d",
"fee_3h": "3h",
"fee_custom": "Inserir",
"fee_fast": "Veloz",
"fee_medium": "Médio",
"fee_replace_min": "A taxa total (satoshi por byte) que você deseja pagar deve ser superior a {min} sat/byte",
"fee_satbyte": "em sat/byte",
"fee_slow": "Lento",
"header": "Enviar",
"input_clear": "Limpar",
"input_done": "Feito",
@ -217,7 +226,7 @@
"currency": "Moeda",
"currency_source": "Os preços são obtidos no CoinDesk",
"default_desc": "Quando desativado, o BlueWallet abrirá imediatamente a carteira selecionada no lançamento.",
"default_info": "Padrão em",
"default_info": "Carteira padrão",
"default_title": "No lançamento",
"default_wallets": "Ver todas as carteiras",
"electrum_connected": "Conectado",
@ -243,6 +252,7 @@
"general_adv_mode_e": "Quando ativado, você verá opções avançadas, como diferentes tipos de carteira, a capacidade de especificar a instância do LNDHub à qual deseja se conectar e a entropia personalizada durante a criação da carteira.",
"general_continuity": "Continuidade",
"general_continuity_e": "Quando ativado, você poderá visualizar carteiras selecionadas e transações, usando seus outros dispositivos conectados ao Apple iCloud.",
"groundcontrol_explanation": "GroundControl é um servidor de notificações push de código aberto gratuito para carteiras bitcoin. Você pode instalar seu próprio servidor GroundControl e colocar sua URL aqui para não depender da infraestrutura da BlueWallet. Deixe em branco para usar o padrão",
"header": "definições",
"language": "Idioma",
"language_restart": "Ao selecionar um novo idioma, pode ser necessário reiniciar a BlueWallet para que a alteração tenha efeito.",
@ -253,17 +263,16 @@
"network": "Rede",
"network_broadcast": "Divulgar transação",
"network_electrum": "Servidor Electrum",
"not_a_valid_uri": "Não é um URI válido",
"notifications": "Notificações",
"password": "Senha",
"password_explain": "Definir a senha para descriptografar os arquivos",
"passwords_do_not_match": "Senhas não conferem",
"plausible_deniability": "Negação plausível...",
"retype_password": "Inserir senha novamente",
"notifications": "Notificações",
"save": "Salvar",
"saved": "Salvo",
"not_a_valid_uri": "Não é um URI válido",
"push_notifications": "Notificações via push",
"groundcontrol_explanation": "GroundControl é um servidor de notificações push de código aberto gratuito para carteiras bitcoin. Você pode instalar seu próprio servidor GroundControl e colocar sua URL aqui para não depender da infraestrutura da BlueWallet. Deixe em branco para usar o padrão"
"retype_password": "Inserir senha novamente",
"save": "Salvar",
"saved": "Salvo"
},
"transactions": {
"cancel_explain": "Substituiremos esta transação por aquela que lhe paga e tem taxas mais altas. Isso efetivamente cancela a transação. Isso é chamado de RBF - Substituir por Taxa.",
@ -284,13 +293,13 @@
"details_to": "Para",
"details_transaction_details": "Detalhes",
"enable_hw": "Esta carteira não está sendo usada em conjunto com uma carteira de hardware. Gostaria de habilitar o uso de carteira de hardware?",
"list_conf": "conf",
"list_conf": "conf: {number}",
"list_title": "Transações",
"transactions_count": "contagem de transações",
"rbf_explain": "Substituiremos essa transação por outra com uma taxa mais alta, portanto, ela deve ser confirmada mais rapidamente. Isso é chamado de RBF - Substituir por Taxa.",
"rbf_title": "Aumento de taxa (RBF)",
"status_bump": "Aumento de taxa",
"status_cancel": "Cancelar Transação"
"status_cancel": "Cancelar Transação",
"transactions_count": "contagem de transações"
},
"wallets": {
"add_bitcoin": "Bitcoin",
@ -334,7 +343,7 @@
"import_error": "Erro. Por favor, confira se o formato que você passou é válido.",
"import_explanation": "Escreva aqui sua frase mnemônica, chave privada, WIF, ou o que você tiver. Faremos nosso melhor para adivinhar o formato e importat sua carteira",
"import_imported": "Importada",
"import_scan_qr": "ou ler um código QR?",
"import_scan_qr": "Ler um código QR ou arquivo",
"import_success": "Sucesso",
"import_title": "importar",
"list_create_a_button": "Criar agora",
@ -353,7 +362,7 @@
"list_long_clipboard": "Copiar da área de transferência",
"list_long_scan": "Ler QR Code",
"take_photo": "Registrar Foto",
"list_tap_here_to_buy": "Toque aqui para comprar Bitcoin",
"list_tap_here_to_buy": "Comprar Bitcoin",
"list_title": "carteiras",
"list_tryagain": "Tente Novamente",
"reorder_title": "Reordenar carteiras",

View file

@ -170,7 +170,7 @@
"details_next": "Próximo",
"details_no_maximum": "A carteira selecionada não suporta cálculo automático de saldo máximo. Tem a certeza que deseja selecionar esta carteira?",
"details_no_multiple": "A carteira selecionada não suporta o envio de Bitcoins para vários destinatários. Tem a certeza que deseja selecionar esta carteira?",
"details_no_signed_tx": "O ficheiro seleccionado não contém uma transacção assinada que possa ser importada.",
"details_no_signed_tx": "O ficheiro seleccionado não contém uma transacção que possa ser importada.",
"details_note_placeholder": "Nota pessoal",
"details_scan": "Scan",
"details_total_exceeds_balance": "O valor total excede o saldo disponível.",
@ -181,6 +181,15 @@
"dynamic_prev": "Anterior",
"dynamic_start": "Começar",
"dynamic_stop": "Parar",
"fee_10m": "10m",
"fee_1d": "1d",
"fee_3h": "3h",
"fee_custom": "Escolher",
"fee_fast": "Rápido",
"fee_medium": "Médio",
"fee_replace_min": "A taxa total (satoshi/byte) que deseja pagar deve ser superior a {min} sat/byte",
"fee_satbyte": "em sat/byte",
"fee_slow": "Lento",
"header": "Enviar",
"input_clear": "Limpar",
"input_done": "Feito",
@ -217,7 +226,7 @@
"currency": "Moeda",
"currency_source": "Os preços são obtidos no CoinDesk",
"default_desc": "Quando desactivado, a BlueWallet abrirá imediatamente a carteira seleccionada no lançamento.",
"default_info": "Padrão em",
"default_info": "Carteira padrão",
"default_title": "A abrir",
"default_wallets": "Ver todas as carteiras",
"electrum_connected": "Conectado",
@ -243,6 +252,7 @@
"general_adv_mode_e": "Quando activado, verá opções avançadas, como diferentes tipos de carteira, a capacidade de especificar a instância do LNDHub à qual se deseja conectar e a entropia personalizada durante a criação da carteira.",
"general_continuity": "Continuidade",
"general_continuity_e": "Quando activado, poderá visualizar carteiras seleccionadas e transacções, usando os seus outros dispositivos conectados ao Apple iCloud.",
"groundcontrol_explanation": "GroundControl é um servidor de notificações push de código aberto gratuito para carteiras bitcoin. Pode instalar seu próprio servidor GroundControl e colocar sua URL aqui para não depender da infraestrutura da BlueWallet. Deixe em branco para usar o padrão",
"header": "definições",
"language": "Idioma",
"language_restart": "Ao selecionar um novo idioma, pode ser necessário reiniciar a BlueWallet para que a alteração tenha efeito.",
@ -253,17 +263,16 @@
"network": "Rede",
"network_broadcast": "Transmitir transacção",
"network_electrum": "Electrum server",
"not_a_valid_uri": "Não é um URI válido",
"notifications": "Notificações",
"password": "Password",
"password_explain": "Definir a password para desencriptar o armazenamento",
"passwords_do_not_match": "Passwords não coincidem",
"plausible_deniability": "Negação plausível...",
"retype_password": "Inserir password novamente",
"notifications": "Notificações",
"save": "Guardar",
"saved": "Guardado",
"not_a_valid_uri": "Não é um URI válido",
"push_notifications": "Notificações via push",
"groundcontrol_explanation": "GroundControl é um servidor de notificações push de código aberto gratuito para carteiras bitcoin. Pode instalar seu próprio servidor GroundControl e colocar sua URL aqui para não depender da infraestrutura da BlueWallet. Deixe em branco para usar o padrão"
"retype_password": "Inserir password novamente",
"save": "Guardar",
"saved": "Guardado"
},
"transactions": {
"cancel_explain": "Substituiremos esta transacção por aquela que lhe paga e tem taxas mais altas. Isso efectivamente cancela a transacção. Isto é chamado de RBF - Substituir por Taxa.",
@ -286,11 +295,11 @@
"enable_hw": "Esta carteira não está a ser usada em conjunto com uma carteira de hardware. Gostaria de habilitar o uso de carteira de hardware?",
"list_conf": "conf",
"list_title": "transacções",
"transactions_count": "contagem de transações",
"rbf_explain": "Substituiremos esta transacção por outra com uma taxa mais alta, portanto, ela deve ser confirmada mais rapidamente. Isto é chamado de RBF - Substituir por Taxa.",
"rbf_title": "Aumento de taxa (RBF)",
"status_bump": "Aumento de taxa",
"status_cancel": "Cancelar transacção"
"status_cancel": "Cancelar transacção",
"transactions_count": "contagem de transações"
},
"wallets": {
"add_bitcoin": "Bitcoin",

View file

@ -170,9 +170,9 @@
"details_next": "Дальше",
"details_no_maximum": "Этот кошелёк не поддерживает отправку всех средств. Ты уверен что хочешь выбрать его?",
"details_no_multiple": "Этот кошелёк не поддерживает отправку нескольким получателям. Ты уверен что хочешь выбрать его?",
"details_no_signed_tx": "В файле нет подписаных транзакций которые можно импортировать",
"details_no_signed_tx": "В файле нет транзакций которые можно импортировать.",
"details_note_placeholder": "примечание платежа",
"details_scan": "Отсканировать QR",
"details_scan": "Скан",
"details_total_exceeds_balance": "Общая сумма превышает баланс.",
"details_wallet_before_tx": "Перед созданием транзакции нужно сделать Bitcoin кошелёк.",
"details_wallet_selection": "Выбор Кошелька",
@ -181,6 +181,15 @@
"dynamic_prev": "Предыдущий",
"dynamic_start": "Старт",
"dynamic_stop": "Стоп",
"fee_10m": "10м",
"fee_1d": "1д",
"fee_3h": "3ч",
"fee_custom": "Другое",
"fee_fast": "Быстро",
"fee_medium": "Средне",
"fee_replace_min": "Комиссия (сатоши за байт) которую нужно заплатить должна быть выше {min} сатоши/байт",
"fee_satbyte": "сатоши/байт",
"fee_slow": "Медленно",
"header": "Отправить",
"input_clear": "Очистить",
"input_done": "Готово",
@ -188,6 +197,7 @@
"input_total": "Всего:",
"permission_camera_message": "Нужно ваше разрешение на использование камеры",
"permission_camera_title": "Разрешите пользоваться камерой",
"open_settings": "Открыть настройки",
"permission_storage_later": "Спроси меня позже",
"permission_storage_message": "BlueWallet нужно ваше разрешение для доступа к хранилищу файлов, чтобы сохранить эту транзакцию.",
"permission_storage_title": "Разрешите сохранять файлы",
@ -242,6 +252,7 @@
"general_adv_mode_e": "При включении вы увидите расширенные параметры, такие как разные типы кошельков, возможность указать экземпляр LNDHub, к которому вы хотите подключиться, и пользовательскую энтропию при создании кошелька.",
"general_continuity": "Непрерывность",
"general_continuity_e": "Когда эта функция включена, вы сможете просматривать выбранные кошельки и транзакции, используя другие устройства, подключенные к Apple iCloud.",
"groundcontrol_explanation": "GroundControl - это бесплатный сервер push-уведомлений с открытым исходным кодом для биткойн-кошельков. Вы можете установить свой собственный сервер GroundControl и указать здесь его URL, чтобы не полагаться на инфраструктуру BlueWallet. Оставьте пустым, чтобы использовать по умолчанию",
"header": "Настройки",
"language": "Язык",
"language_restart": "При выборе нового языка может потребоваться перезапуск BlueWallet, чтобы изменения вступили в силу.",
@ -252,17 +263,16 @@
"network": "Сеть",
"network_broadcast": "Отправить транзакцию",
"network_electrum": "Electrum сервер",
"not_a_valid_uri": "Неверный URI",
"notifications": "Уведомления",
"password": "Пароль",
"password_explain": "Придумай пароль для расшифровки хранилища",
"passwords_do_not_match": "Пароли не совпадают",
"plausible_deniability": "Правдоподобная имитация...",
"retype_password": "Набери пароль повторно",
"notifications": "Уведомления",
"save": "Сохранить",
"saved": "Сохранено",
"not_a_valid_uri": "Неверный URI",
"push_notifications": "Push-уведомления",
"groundcontrol_explanation": "GroundControl - это бесплатный сервер push-уведомлений с открытым исходным кодом для биткойн-кошельков. Вы можете установить свой собственный сервер GroundControl и указать здесь его URL, чтобы не полагаться на инфраструктуру BlueWallet. Оставьте пустым, чтобы использовать по умолчанию"
"retype_password": "Набери пароль повторно",
"save": "Сохранить",
"saved": "Сохранено"
},
"transactions": {
"cancel_explain": "Мы заменим эту транзакцию другой, которая платит вам и имеет более высокую комиссию. Это отменит предыдущую транзакцию. Это называется RBF - Replace By Fee.",
@ -283,13 +293,13 @@
"details_to": "Кому",
"details_transaction_details": "Детали транзакции",
"enable_hw": "Кошелек не используется вместе с аппаратным. Вы хотите включить поддержку аппаратного кошелека?",
"list_conf": "подтв.",
"list_conf": "{number} подтв.",
"list_title": "Мои транзакции",
"transactions_count": "кол-во транзакций",
"rbf_explain": "Мы заменим эту транзакцию другой с более высокой комиссией, поэтому будет обработана быстрее. Это называется RBF - Replace By Fee.",
"rbf_title": "Повысить комиссию (RBF)",
"status_bump": "Повысить комиссию",
"status_cancel": "Отменить транзакцию"
"status_cancel": "Отменить транзакцию",
"transactions_count": "кол-во транзакций"
},
"wallets": {
"add_bitcoin": "Bitcoin",
@ -298,6 +308,7 @@
"add_entropy_provide": "Сгенерировать энторию с помощью игральных костей",
"add_entropy_remain": "{gen} байтов сгенерированной энтории. Оставшиеся {rem} байт будут получены из системного генератора случайных чисел.",
"add_import_wallet": "Импортировать кошелек",
"import_file": "Импортировать файл",
"add_lightning": "Lightning",
"add_lndhub": "Подключиться к своему LNDHub",
"add_lndhub_error": "Не верный адрес LNDHub.",
@ -343,7 +354,7 @@
"list_empty_txs1_lightning": "Lightning кошелек отлично подходит для ежедневных транзакций. Комиссия несправедливо мала, а скорость невероятно высока.",
"list_empty_txs2": "Пока транзакций нет ",
"list_empty_txs2_lightning": "\nДля начала использования нажми \"Мои средства\" и пополни баланс.",
"list_header": "Кошелек - это секретный (приватный) ключ и соответствующий ему адрес на который можно получать Bitcoin",
"list_header": "Кошелек - пара ключей, один приватный, другой - публичный используется, чтобы получить Bitcoin.",
"list_import_error": "При импорте этого кошелька возникла ошибка.",
"list_import_problem": "Ошибка при импорте кошелька",
"list_latest_transaction": "Последняя транзакция",

View file

@ -170,13 +170,13 @@
"details_next": "Ďalej",
"details_no_maximum": "The selected wallet does not support automatic maximum balance calculation. Are you sure to want to select this wallet?",
"details_no_multiple": "The selected wallet does not support sending Bitcoin to multiple recipients. Are you sure to want to select this wallet?",
"details_no_signed_tx": "The selected file does not contain a signed transaction that can be imported.",
"details_no_signed_tx": "The selected file does not contain a transaction that can be imported.",
"details_note_placeholder": "poznámka pre seba",
"details_scan": "Skenovať",
"details_total_exceeds_balance": "Čiastka, ktorú chcete poslať, presahuje dostupný zostatok.",
"details_wallet_before_tx": "Pred vytvorením transakcie potrebujete najprv pridať Bitcoinovú peňaženku.",
"details_wallet_selection": "Výber peňaženky",
"dynamic_init": "Initialing",
"dynamic_init": "Initializing",
"dynamic_next": "Next",
"dynamic_prev": "Previous",
"dynamic_start": "Start",

View file

@ -8,7 +8,9 @@
"of": "{number} od {total}",
"ok": "OK",
"storage_is_encrypted": "Shramba je šifrirana. Za dešifriranje je potrebno geslo",
"yes": "Da"
"yes": "Da",
"invalid_animated_qr_code_fragment" : "Neveljaven del animirane QR kode, prosimo poskusite ponovno",
"file_saved": "Datoteka ({filePath}) je bila shranjena v mapo Prenosi."
},
"azteco": {
"codeIs": "Koda vašega bona je",
@ -170,7 +172,7 @@
"details_next": "Naprej",
"details_no_maximum": "Izbrana denarnica ne podpira samodejnega izračuna največjega stanja. Ali ste prepričani, da želite izbrati to denarnico?",
"details_no_multiple": "Izbrana denarnica ne podpira pošiljanja več prejemnikom. Ali ste prepričani, da želite izbrati to denarnico?",
"details_no_signed_tx": "Izbrana datoteka ne vsebuje podpisane transakcije, ki jo je mogoče uvoziti.",
"details_no_signed_tx": "Izbrana datoteka ne vsebuje transakcije, ki jo je mogoče uvoziti.",
"details_note_placeholder": "lastna opomba",
"details_scan": "Skeniraj",
"details_total_exceeds_balance": "Znesek presega razpoložljivo stanje.",
@ -209,7 +211,8 @@
"qr_error_no_qrcode": "Izbrana slika ne vsebuje QR kode.",
"qr_error_no_wallet": "Izbrana datoteka ne vsebuje denarnice, ki jo je mogoče uvoziti.",
"success_done": "Končano",
"txSaved": "Transakcijska datoteka ({filePath}) je bila shranjena v mapo Prenosi."
"txSaved": "Transakcijska datoteka ({filePath}) je bila shranjena v mapo Prenosi.",
"problem_with_psbt": "Težava s PSBT"
},
"settings": {
"about": "O aplikaciji",
@ -293,7 +296,7 @@
"details_to": "Izhod",
"details_transaction_details": "Podrobnosti transakcije",
"enable_hw": "Ta denarnica se ne uporablja skupaj s strojno denarnico. Ali želite omogočiti uporabo strojne denarnice?",
"list_conf": "potrd",
"list_conf": "potrd: {number}",
"list_title": "transakcije",
"rbf_explain": "To transakcijo bomo nadomestili z novo, ki plača višjo omrežnino, zato bi morala biti potrditev hitrejša. To se imenuje RBF - Replace By Fee.",
"rbf_title": "Povečaj omrežnino (RBF)",
@ -371,5 +374,17 @@
"select_wallet": "Izberite Denarnico",
"xpub_copiedToClipboard": "Kopirano v odložišče.",
"xpub_title": "XPUB denarnice"
},
"multisig": {
"provide_signature": "Vnesite podpis",
"vault_key": "Ključ trezorja {number}",
"fee": "Omrežnina: {number}",
"fee_btc": "{number} BTC",
"confirm": "Potrditev",
"header": "Pošlji",
"share": "Deli",
"how_many_signatures_can_bluewallet_make": "koliko podpisov lahko naredi bluewallet",
"scan_or_import_file": "Skenirajte ali uvozite datoteko",
"export_coordination_setup": "izvoz koordinacijskih nastavitev"
}
}

View file

@ -170,13 +170,13 @@
"details_next": "Nästa",
"details_no_maximum": "The selected wallet does not support automatic maximum balance calculation. Are you sure to want to select this wallet?",
"details_no_multiple": "The selected wallet does not support sending Bitcoin to multiple recipients. Are you sure to want to select this wallet?",
"details_no_signed_tx": "The selected file does not contain a signed transaction that can be imported.",
"details_no_signed_tx": "The selected file does not contain a transaction that can be imported.",
"details_note_placeholder": "egen notering",
"details_scan": "Skanna",
"details_total_exceeds_balance": "Beloppet överstiger plånbokens tillgängliga belopp",
"details_wallet_before_tx": "Before creating a transaction, you must first add a Bitcoin wallet.",
"details_wallet_selection": "Wallet Selection",
"dynamic_init": "Initialing",
"dynamic_init": "Initializing",
"dynamic_next": "Nästa",
"dynamic_prev": "Föregående",
"dynamic_start": "Starta",

View file

@ -170,7 +170,7 @@
"details_next": "ถัดไป",
"details_no_maximum": "The selected wallet does not support automatic maximum balance calculation. Are you sure to want to select this wallet?",
"details_no_multiple": "The selected wallet does not support sending Bitcoin to multiple recipients. Are you sure to want to select this wallet?",
"details_no_signed_tx": "The selected file does not contain a signed transaction that can be imported.",
"details_no_signed_tx": "The selected file does not contain a transaction that can be imported.",
"details_note_placeholder": "หมายเหตุถึงตัวท่านเอง",
"details_scan": "สแกน",
"details_total_exceeds_balance": "จำนวนเงินที่จะส่งเกินเงินที่มี.",

View file

@ -170,13 +170,13 @@
"details_next": "Next",
"details_no_maximum": "The selected wallet does not support automatic maximum balance calculation. Are you sure to want to select this wallet?",
"details_no_multiple": "The selected wallet does not support sending Bitcoin to multiple recipients. Are you sure to want to select this wallet?",
"details_no_signed_tx": "The selected file does not contain a signed transaction that can be imported.",
"details_no_signed_tx": "The selected file does not contain a transaction that can be imported.",
"details_note_placeholder": "kendime not",
"details_scan": "Tara",
"details_total_exceeds_balance": "Gönderme miktarı mevcut bakiyeyi aşıyor.",
"details_wallet_before_tx": "Before creating a transaction, you must first add a Bitcoin wallet.",
"details_wallet_selection": "Wallet Selection",
"dynamic_init": "Initialing",
"dynamic_init": "Initializing",
"dynamic_next": "Next",
"dynamic_prev": "Previous",
"dynamic_start": "Start",

View file

@ -12,7 +12,7 @@
},
"azteco": {
"codeIs": "Your voucher code is",
"errorBeforeRefeem": "Before redeeming you must first add a Bitcoin wallet.",
"errorBeforeRefeem": "你需先添加一个Bitcoin钱包",
"errorSomething": "Something went wrong. Is this voucher still valid?",
"redeem": "Redeem to wallet",
"redeemButton": "Redeem",
@ -30,10 +30,10 @@
"network": "网络错误"
},
"hodl": {
"are_you_sure_you_want_to_logout": "Are you sure you want to logout from HodlHodl?",
"are_you_sure_you_want_to_logout": "您确定要注销HodlHodl吗",
"cont_address_escrow": "Escrow",
"cont_address_to": "To",
"cont_buying": "buying",
"cont_buying": "购买",
"cont_cancel": "Cancel contract",
"cont_cancel_q": "Are you sure you want to cancel this contract?",
"cont_cancel_y": "Yes, cancel contract",
@ -170,13 +170,13 @@
"details_next": "Next",
"details_no_maximum": "The selected wallet does not support automatic maximum balance calculation. Are you sure to want to select this wallet?",
"details_no_multiple": "The selected wallet does not support sending Bitcoin to multiple recipients. Are you sure to want to select this wallet?",
"details_no_signed_tx": "The selected file does not contain a signed transaction that can be imported.",
"details_no_signed_tx": "The selected file does not contain a transaction that can be imported.",
"details_note_placeholder": "消息",
"details_scan": "扫描",
"details_total_exceeds_balance": "余额不足",
"details_wallet_before_tx": "Before creating a transaction, you must first add a Bitcoin wallet.",
"details_wallet_selection": "Wallet Selection",
"dynamic_init": "Initialing",
"dynamic_init": "Initializing",
"dynamic_next": "Next",
"dynamic_prev": "Previous",
"dynamic_start": "Start",

View file

@ -27,7 +27,7 @@ export const FiatUnit = Object.freeze({
SGD: { endPointKey: 'SGD', symbol: 'S$', locale: 'zh-SG' },
SEK: { endPointKey: 'SEK', symbol: 'kr', locale: 'sv-SE' },
THB: { endPointKey: 'THB', symbol: '฿', locale: 'th-TH' },
TWD: { endPointKey: 'TWD', symbol: '$', locale: 'zh-Hant-TW' },
TWD: { endPointKey: 'TWD', symbol: 'NT$', locale: 'zh-Hant-TW' },
UAH: { endPointKey: 'UAH', symbol: '₴', locale: 'uk-UA' },
VEF: { endPointKey: 'VEF', symbol: 'Bs.', locale: 'es-VE' },
ZAR: { endPointKey: 'ZAR', symbol: 'R', locale: 'en-ZA' },

View file

@ -20,7 +20,7 @@ export class NetworkTransactionFee {
export default class NetworkTransactionFees {
static recommendedFees() {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
return new Promise(async resolve => {
try {
const response = await BlueElectrum.estimateFees();
if (typeof response === 'object') {
@ -28,12 +28,12 @@ export default class NetworkTransactionFees {
resolve(networkFee);
} else {
const networkFee = new NetworkTransactionFee(1, 1, 1);
reject(networkFee);
resolve(networkFee);
}
} catch (err) {
console.warn(err);
const networkFee = new NetworkTransactionFee(1, 1, 1);
reject(networkFee);
resolve(networkFee);
}
});
}

12081
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,25 +1,26 @@
{
"name": "bluewallet",
"version": "5.6.0",
"version": "5.6.2",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.9.6",
"@babel/runtime": "^7.9.6",
"@react-native-community/eslint-config": "^1.1.0",
"@babel/core": "^7.10.4",
"@babel/runtime": "^7.10.4",
"@jest/reporters": "^26.4.1",
"@react-native-community/eslint-config": "^2.0.0",
"babel-cli": "^6.26.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^26.0.1",
"babel-jest": "^26.1.0",
"babel-preset-flow": "^6.23.0",
"eslint": "^6.8.0",
"eslint-plugin-babel": "^5.3.0",
"eslint-plugin-import": "^2.20.2",
"eslint": "^7.5.0",
"eslint-plugin-babel": "^5.3.1",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-react": "^7.20.0",
"flow-bin": "^0.125.1",
"jest": "^24.9.0",
"eslint-plugin-react": "^7.20.3",
"flow-bin": "^0.134.0",
"jest": "^26.1.0",
"jetifier": "^1.6.3",
"react-test-renderer": "16.11.0"
"react-test-renderer": "16.13.1"
},
"engines": {
"node": ">=10.16.0",
@ -42,7 +43,7 @@
"e2e:debug": "(test -f android/app/build/outputs/apk/debug/app-debug.apk && test -f android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk) || npm run e2e:debug-build; npm run e2e:debug-test",
"e2e:release-build": "npx detox build -c android.emu.release",
"e2e:release-test": "detox test -c android.emu.release --record-videos all --take-screenshots all --headless",
"lint": "eslint *.js screen/**/*.js blue_modules/*.js class/**/*.js models/ loc/ tests/**/*.js",
"lint": "eslint *.js screen/**/*.js blue_modules/*.js class/**/*.js models/ loc/ tests/**/*.js components/**/*.js",
"lint:fix": "npm run lint -- --fix",
"lint:quickfix": "git status --porcelain | grep -v '\\.json' | grep '\\.js' --color=never | awk '{print $2}' | xargs eslint --fix; exit 0",
"unit": "jest tests/unit/*"
@ -55,24 +56,27 @@
"setupFiles": [
"./tests/setup.js"
],
"watchPathIgnorePatterns": [
"<rootDir>/node_modules"
],
"setupFilesAfterEnv": [
"./tests/setupAfterEnv.js"
]
},
"dependencies": {
"@babel/preset-env": "7.11.0",
"@react-native-community/async-storage": "1.12.0",
"@babel/preset-env": "7.11.5",
"@react-native-community/async-storage": "1.12.1",
"@react-native-community/blur": "3.6.0",
"@react-native-community/clipboard": "1.2.3",
"@react-native-community/clipboard": "1.4.0",
"@react-native-community/geolocation": "2.0.2",
"@react-native-community/masked-view": "0.1.10",
"@react-native-community/push-notification-ios": "1.5.0",
"@react-native-community/slider": "3.0.3",
"@react-navigation/drawer": "5.9.0",
"@react-navigation/native": "5.7.3",
"@react-navigation/stack": "5.9.0",
"@react-navigation/drawer": "5.9.2",
"@react-navigation/native": "5.7.5",
"@react-navigation/stack": "5.9.2",
"@remobile/react-native-qrcode-local-image": "git+https://github.com/BlueWallet/react-native-qrcode-local-image.git",
"@sentry/react-native": "1.8.0",
"@sentry/react-native": "1.8.2",
"amplitude-js": "5.11.0",
"assert": "1.5.0",
"bc-bech32": "file:blue_modules/bc-bech32",
@ -82,16 +86,16 @@
"bip21": "2.0.3",
"bip32": "2.0.5",
"bip39": "2.6.0",
"bitcoinjs-lib": "5.1.10",
"bitcoinjs-lib": "5.2.0",
"bolt11": "1.2.7",
"buffer": "5.6.0",
"buffer-reverse": "1.0.1",
"coinselect": "3.1.12",
"crypto-js": "3.1.9-1",
"dayjs": "1.8.33",
"dayjs": "1.8.35",
"detox": "17.5.6",
"ecurve": "1.0.6",
"electrum-client": "git+https://github.com/BlueWallet/rn-electrum-client.git#99c75385f99e82b87fbfed2abfb2cccd322fe3e9",
"electrum-client": "git+https://github.com/BlueWallet/rn-electrum-client.git#f9a827e724a5a2e578fdfdb483f83793af55b030",
"electrum-mnemonic": "2.0.0",
"eslint-config-prettier": "6.11.0",
"eslint-config-standard": "14.1.1",
@ -104,29 +108,30 @@
"lottie-react-native": "3.5.0",
"metro-react-native-babel-preset": "0.63.0",
"path-browserify": "1.0.1",
"payjoin-client": "git+https://github.com/bitcoinjs/payjoin-client.git#31d2118a4c0d00192d975f3a6da2a96238f8f7a5",
"pbkdf2": "3.1.1",
"prettier": "2.1.1",
"process": "0.11.10",
"prop-types": "15.7.2",
"react": "16.11.0",
"react": "16.13.1",
"react-localization": "1.0.15",
"react-native": "0.62.2",
"react-native": "0.63.3",
"react-native-blue-crypto": "git+https://github.com/Overtorment/react-native-blue-crypto.git",
"react-native-camera": "3.39.1",
"react-native-default-preference": "1.4.3",
"react-native-device-info": "6.0.3",
"react-native-device-info": "6.2.0",
"react-native-document-picker": "git+https://github.com/BlueWallet/react-native-document-picker.git#3684d4fcc2bc0b47c32be39024e4796004c3e428",
"react-native-elements": "2.2.1",
"react-native-elements": "2.3.1",
"react-native-fingerprint-scanner": "git+https://github.com/BlueWallet/react-native-fingerprint-scanner.git#ce644673681716335d786727bab998f7e632ab5e",
"react-native-fs": "2.16.6",
"react-native-gesture-handler": "1.7.0",
"react-native-gesture-handler": "1.8.0",
"react-native-handoff": "git+https://github.com/marcosrdz/react-native-handoff.git",
"react-native-haptic-feedback": "1.10.0",
"react-native-image-picker": "2.3.3",
"react-native-image-picker": "2.3.4",
"react-native-inappbrowser-reborn": "git+https://github.com/BlueWallet/react-native-inappbrowser.git#v3.4.0",
"react-native-level-fs": "3.0.1",
"react-native-linear-gradient": "2.5.6",
"react-native-localize": " 1.4.0",
"react-native-localize": "1.4.2",
"react-native-modal": "11.5.6",
"react-native-obscure": "1.2.1",
"react-native-passcode-auth": "git+https://github.com/BlueWallet/react-native-passcode-auth.git#a2ff977ba92b36f8d0a5567f59c05cc608e8bd12",
@ -142,7 +147,7 @@
"react-native-safe-area-context": "3.1.8",
"react-native-screens": "2.11.0",
"react-native-secure-key-store": "git+https://github.com/BlueWallet/react-native-secure-key-store.git#4ba25dedb3d5ae15c22fd0ea0555116055630966",
"react-native-share": "3.7.0",
"react-native-share": "4.0.2",
"react-native-snap-carousel": "3.9.1",
"react-native-sortable-list": "0.0.24",
"react-native-svg": "12.1.0",
@ -150,7 +155,7 @@
"react-native-tooltip": "git+https://github.com/BlueWallet/react-native-tooltip.git#d369e7ece09e4dec73873f1cfeac83e9d35294a6",
"react-native-vector-icons": "6.6.0",
"react-native-watch-connectivity": "1.0.2",
"react-native-webview": "10.8.3",
"react-native-webview": "10.9.2",
"react-test-render": "1.1.2",
"readable-stream": "3.6.0",
"realm": "6.1.0",

View file

@ -125,9 +125,8 @@ const ReceiveDetails = () => {
backgroundColor: BlueCurrentTheme.colors.elevated,
},
share: {
alignItems: 'center',
alignContent: 'flex-end',
marginBottom: 24,
marginHorizontal: 16,
},
modalButton: {
backgroundColor: BlueCurrentTheme.colors.modalButton,
@ -193,7 +192,7 @@ const ReceiveDetails = () => {
if (!address) {
// either sleep expired or getAddressAsync threw an exception
console.warn('either sleep expired or getAddressAsync threw an exception');
address = wallet._getExternalAddressByIndex(wallet.next_free_address_index);
address = wallet._getExternalAddressByIndex(wallet.getNextFreeAddressIndex());
} else {
BlueApp.saveToDisk(); // caching whatever getAddressAsync() generated internally
}

View file

@ -1,19 +1,19 @@
/* global alert */
import React, { useState } from 'react';
import { Image, View, TouchableOpacity, StatusBar, Platform, StyleSheet, Linking, Alert } from 'react-native';
import { Image, View, TouchableOpacity, StatusBar, Platform, StyleSheet, Linking, Alert, TextInput } from 'react-native';
import { RNCamera } from 'react-native-camera';
import { Icon } from 'react-native-elements';
import ImagePicker from 'react-native-image-picker';
import { useNavigation, useRoute, useIsFocused, useTheme } from '@react-navigation/native';
import DocumentPicker from 'react-native-document-picker';
import RNFS from 'react-native-fs';
import loc from '../../loc';
import { BlueLoadingHook, BlueTextHooks, BlueButtonHook, BlueSpacing40 } from '../../BlueComponents';
import { getSystemName } from 'react-native-device-info';
const prompt = require('../../blue_modules/prompt');
import { BlueCurrentTheme } from '../../components/themes';
import { decodeUR, extractSingleWorkload } from 'bc-ur';
const LocalQRCode = require('@remobile/react-native-qrcode-local-image');
const createHash = require('create-hash');
const isDesktop = getSystemName() === 'Mac OS X';
const fs = require('../../blue_modules/fs');
const styles = StyleSheet.create({
root: {
@ -65,12 +65,21 @@ const styles = StyleSheet.create({
backdoorButton: {
width: 40,
height: 40,
backgroundColor: 'rgba(0,0,0,0)',
justifyContent: 'center',
borderRadius: 0,
backgroundColor: 'rgba(0,0,0,0.1)',
position: 'absolute',
left: 0,
bottom: 0,
},
backdoorInputWrapper: { position: 'absolute', left: '5%', top: '0%', width: '90%', height: '70%', backgroundColor: 'white' },
backdoorInput: {
height: '50%',
marginTop: 5,
marginHorizontal: 20,
borderColor: BlueCurrentTheme.colors.formBorder,
borderBottomColor: BlueCurrentTheme.colors.formBorder,
borderWidth: 1,
borderRadius: 4,
backgroundColor: BlueCurrentTheme.colors.inputBackgroundColor,
color: BlueCurrentTheme.colors.foregroundColor,
textAlignVertical: 'top',
},
});
@ -85,6 +94,9 @@ const ScanQRCode = () => {
const isFocused = useIsFocused();
const [cameraStatus, setCameraStatus] = useState(RNCamera.Constants.CameraStatus.PENDING_AUTHORIZATION);
const [backdoorPressed, setBackdoorPressed] = useState(0);
const [backdoorText, setBackdoorText] = useState('');
const [backdoorVisible, setBackdoorVisible] = useState(false);
const [animatedQRCodeData, setAnimatedQRCodeData] = useState({});
const stylesHook = StyleSheet.create({
openSettingsContainer: {
backgroundColor: colors.brandingColor,
@ -94,6 +106,34 @@ const ScanQRCode = () => {
return createHash('sha256').update(s).digest().toString('hex');
};
const _onReadUniformResource = ur => {
try {
const [index, total] = extractSingleWorkload(ur);
animatedQRCodeData[index + 'of' + total] = ur;
if (Object.values(animatedQRCodeData).length === total) {
const payload = decodeUR(Object.values(animatedQRCodeData));
// lets look inside that data
let data = false;
if (Buffer.from(payload, 'hex').toString().startsWith('psbt')) {
// its a psbt, and whoever requested it expects it encoded in base64
data = Buffer.from(payload, 'hex').toString('base64');
} else {
// its something else. probably plain text is expected
data = Buffer.from(payload, 'hex').toString();
}
if (launchedBy) {
navigation.navigate(launchedBy);
}
onBarScanned({ data });
} else {
setAnimatedQRCodeData(animatedQRCodeData);
}
} catch (error) {
console.warn(error);
alert(loc._.invalid_animated_qr_code_fragment);
}
};
const onBarCodeRead = ret => {
const h = HashIt(ret.data);
if (scannedCache[h]) {
@ -102,6 +142,10 @@ const ScanQRCode = () => {
}
scannedCache[h] = +new Date();
if (ret.data.toUpperCase().startsWith('UR')) {
return _onReadUniformResource(ret.data);
}
if (!isLoading) {
setIsLoading(true);
try {
@ -121,26 +165,10 @@ const ScanQRCode = () => {
};
const showFilePicker = async () => {
try {
setIsLoading(true);
const res = await DocumentPicker.pick();
const file = await RNFS.readFile(res.uri);
const fileParsed = JSON.parse(file);
if (fileParsed.keystore.xpub) {
let masterFingerprint;
if (fileParsed.keystore.ckcc_xfp) {
masterFingerprint = Number(fileParsed.keystore.ckcc_xfp);
}
onBarCodeRead({ data: fileParsed.keystore.xpub, additionalProperties: { masterFingerprint, label: fileParsed.keystore.label } });
} else {
throw new Error();
}
} catch (err) {
if (!DocumentPicker.isCancel(err)) {
alert(loc.send.qr_error_no_wallet);
}
setIsLoading(false);
}
setIsLoading(true);
const { data } = await fs.showFilePickerAndReadFile();
if (data) onBarCodeRead({ data });
setIsLoading(false);
};
const showImagePicker = () => {
@ -218,6 +246,45 @@ const ScanQRCode = () => {
<TouchableOpacity style={styles.imagePickerTouch} onPress={showImagePicker}>
<Icon name="image" type="font-awesome" color="#ffffff" />
</TouchableOpacity>
{showFileImportButton && (
<TouchableOpacity style={styles.filePickerTouch} onPress={showFilePicker}>
<Icon name="file-import" type="material-community" color="#ffffff" />
</TouchableOpacity>
)}
{backdoorVisible && (
<View style={styles.backdoorInputWrapper}>
<BlueTextHooks>Provide QR code contents manually:</BlueTextHooks>
<TextInput
testID="scanQrBackdoorInput"
multiline
underlineColorAndroid="transparent"
style={styles.backdoorInput}
autoCorrect={false}
autoCapitalize="none"
spellCheck={false}
selectTextOnFocus={false}
keyboardType={Platform.OS === 'android' ? 'visible-password' : 'default'}
value={backdoorText}
onChangeText={setBackdoorText}
/>
<BlueButtonHook
title="OK"
testID="scanQrBackdoorOkButton"
onPress={() => {
setBackdoorVisible(false);
let data;
try {
data = JSON.parse(backdoorText);
// this might be a json string (for convenience - in case there are "\n" in there)
} catch (_) {
data = backdoorText;
}
if (data) onBarCodeRead({ data });
}}
/>
</View>
)}
<TouchableOpacity
testID="ScanQrBackdoorButton"
style={styles.backdoorButton}
@ -227,23 +294,10 @@ const ScanQRCode = () => {
// this allows to mock and test QR scanning in e2e tests
setBackdoorPressed(backdoorPressed + 1);
if (backdoorPressed < 10) return;
let data, userInput;
try {
userInput = await prompt('Provide QR code contents manually:', '', false, 'plain-text');
data = JSON.parse(userInput);
// this might be a json string (for convenience - in case there are "\n" in there)
} catch (_) {
data = userInput;
}
if (data) onBarCodeRead({ data });
setBackdoorPressed(0);
setBackdoorVisible(true);
}}
/>
{showFileImportButton && (
<TouchableOpacity style={styles.filePickerTouch} onPress={showFilePicker}>
<Icon name="file-import" type="material-community" color="#ffffff" />
</TouchableOpacity>
)}
</View>
);
};

View file

@ -1,7 +1,9 @@
/* global alert */
import React, { Component } from 'react';
import { ActivityIndicator, FlatList, TouchableOpacity, StyleSheet, View } from 'react-native';
import { ActivityIndicator, FlatList, TouchableOpacity, StyleSheet, Switch, View } from 'react-native';
import { Text } from 'react-native-elements';
import { PayjoinClient } from 'payjoin-client';
import PayjoinTransaction from '../../class/payjoin-transaction';
import { BlueButton, BlueText, SafeBlueArea, BlueCard, BlueSpacing40, BlueNavigationStyle } from '../../BlueComponents';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import PropTypes from 'prop-types';
@ -32,7 +34,10 @@ export default class Confirm extends Component {
this.state = {
isLoading: false,
fee: props.route.params.fee,
isPayjoinEnabled: false,
payjoinUrl: props.route.params.fromWallet.allowPayJoin() ? props.route.params?.payjoinUrl : false,
psbt: props.route.params?.psbt,
fee: props.route.params?.fee,
feeSatoshi: new Bignumber(props.route.params.fee).multipliedBy(100000000).toNumber(),
memo: props.route.params.memo,
recipients: props.route.params.recipients,
@ -50,59 +55,63 @@ export default class Confirm extends Component {
this.isBiometricUseCapableAndEnabled = await Biometric.isBiometricUseCapableAndEnabled();
}
broadcast() {
send() {
this.setState({ isLoading: true }, async () => {
try {
// await BlueElectrum.ping();
await BlueElectrum.waitTillConnected();
if (this.isBiometricUseCapableAndEnabled) {
if (!(await Biometric.unlockWithBiometrics())) {
this.setState({ isLoading: false });
return;
}
}
const result = await this.state.fromWallet.broadcastTx(this.state.tx);
if (!result) {
throw new Error(loc.errors.broadcast);
const txids2watch = [];
if (!this.state.isPayjoinEnabled) {
await this.broadcast(this.state.tx);
} else {
const txid = bitcoin.Transaction.fromHex(this.state.tx).getId();
notifications.majorTomToGroundControl([], [], [txid]);
EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED); // someone should fetch txs
let amount = 0;
const recipients = this.state.recipients;
if (recipients[0].amount === BitcoinUnit.MAX || (!recipients[0].amount && !recipients[0].value)) {
amount = this.state.fromWallet.getBalance() - this.state.feeSatoshi;
} else {
for (const recipient of recipients) {
amount += recipient.amount ? +recipient.amount : recipient.value;
}
}
// wallets that support new createTransaction() instead of deprecated createTx()
if (
[
HDSegwitBech32Wallet.type,
HDSegwitP2SHWallet.type,
HDLegacyP2PKHWallet.type,
HDLegacyBreadwalletWallet.type,
HDLegacyElectrumSeedP2PKHWallet.type,
LegacyWallet.type,
SegwitP2SHWallet.type,
SegwitBech32Wallet.type,
].includes(this.state.fromWallet.type)
) {
amount = formatBalanceWithoutSuffix(amount, BitcoinUnit.BTC, false);
}
this.props.navigation.navigate('Success', {
fee: Number(this.state.fee),
amount,
dismissModal: () => this.props.navigation.dangerouslyGetParent().pop(),
const wallet = new PayjoinTransaction(this.state.psbt, txHex => this.broadcast(txHex), this.state.fromWallet);
const payjoinClient = new PayjoinClient({
wallet,
payjoinUrl: this.state.payjoinUrl,
});
this.setState({ isLoading: false });
await payjoinClient.run();
const payjoinPsbt = wallet.getPayjoinPsbt();
if (payjoinPsbt) {
const tx = payjoinPsbt.extractTransaction();
txids2watch.push(tx.getId());
}
}
const txid = bitcoin.Transaction.fromHex(this.state.tx).getId();
txids2watch.push(txid);
notifications.majorTomToGroundControl([], [], txids2watch);
EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED); // someone should fetch txs
let amount = 0;
const recipients = this.state.recipients;
if (recipients[0].amount === BitcoinUnit.MAX || (!recipients[0].amount && !recipients[0].value)) {
amount = this.state.fromWallet.getBalance() - this.state.feeSatoshi;
} else {
for (const recipient of recipients) {
amount += recipient.amount ? +recipient.amount : recipient.value;
}
}
// wallets that support new createTransaction() instead of deprecated createTx()
if (
[
HDSegwitBech32Wallet.type,
HDSegwitP2SHWallet.type,
HDLegacyP2PKHWallet.type,
HDLegacyBreadwalletWallet.type,
HDLegacyElectrumSeedP2PKHWallet.type,
LegacyWallet.type,
SegwitP2SHWallet.type,
SegwitBech32Wallet.type,
].includes(this.state.fromWallet.type)
) {
amount = formatBalanceWithoutSuffix(amount, BitcoinUnit.BTC, false);
}
this.props.navigation.navigate('Success', {
fee: Number(this.state.fee),
amount,
dismissModal: () => this.props.navigation.dangerouslyGetParent().pop(),
});
this.setState({ isLoading: false });
} catch (error) {
ReactNativeHapticFeedback.trigger('notificationError', {
ignoreAndroidSystemSettings: false,
@ -113,6 +122,24 @@ export default class Confirm extends Component {
});
}
async broadcast(tx) {
await BlueElectrum.ping();
await BlueElectrum.waitTillConnected();
if (this.isBiometricUseCapableAndEnabled) {
if (!(await Biometric.unlockWithBiometrics())) {
return;
}
}
const result = await this.state.fromWallet.broadcastTx(tx);
if (!result) {
throw new Error(loc.errors.broadcast);
}
return result;
}
_renderItem = ({ index, item }) => {
return (
<>
@ -168,11 +195,17 @@ export default class Confirm extends Component {
{currency.satoshiToLocalCurrency(this.state.feeSatoshi)})
</Text>
<BlueSpacing40 />
{this.state.isLoading ? (
<ActivityIndicator />
) : (
<BlueButton onPress={() => this.broadcast()} title={loc.send.confirm_sendNow} />
{!!this.state.payjoinUrl && (
<View style={styles.payjoinWrapper}>
<Text style={styles.payjoinText}>Payjoin</Text>
<Switch
testID="PayjoinSwitch"
value={this.state.isPayjoinEnabled}
onValueChange={isPayjoinEnabled => this.setState({ isPayjoinEnabled })}
/>
</View>
)}
{this.state.isLoading ? <ActivityIndicator /> : <BlueButton onPress={() => this.send()} title={loc.send.confirm_sendNow} />}
<TouchableOpacity
testID="TransactionDetailsButton"
@ -287,11 +320,20 @@ const styles = StyleSheet.create({
fontWeight: '500',
alignSelf: 'center',
},
payjoinWrapper: {
flexDirection: 'row',
marginHorizontal: 20,
marginBottom: 10,
justifyContent: 'space-between',
alignItems: 'center',
},
payjoinText: { color: '#81868e', fontSize: 14 },
});
Confirm.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.func,
dismiss: PropTypes.func,
navigate: PropTypes.func,
dangerouslyGetParent: PropTypes.func,
}),

View file

@ -253,8 +253,8 @@ SendCreate.navigationOptions = ({ navigation, route }) => {
}
return {
...BlueNavigationStyle,
title: loc.send.create.details,
...BlueNavigationStyle(),
title: loc.send.create_details,
headerRight,
};
};

View file

@ -38,15 +38,17 @@ import * as bitcoin from 'bitcoinjs-lib';
import NetworkTransactionFees, { NetworkTransactionFee } from '../../models/networkTransactionFees';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import { AppStorage, HDSegwitBech32Wallet, LightningCustodianWallet, WatchOnlyWallet } from '../../class';
import { AppStorage, HDSegwitBech32Wallet, LightningCustodianWallet, MultisigHDWallet, WatchOnlyWallet } from '../../class';
import { BitcoinTransaction } from '../../models/bitcoinTransactionInfo';
import DocumentPicker from 'react-native-document-picker';
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
import loc from '../../loc';
import { BlueCurrentTheme } from '../../components/themes';
import { AbstractHDElectrumWallet } from '../../class/wallets/abstract-hd-electrum-wallet';
const currency = require('../../blue_modules/currency');
const BlueApp: AppStorage = require('../../BlueApp');
const prompt = require('../../blue_modules/prompt');
const fs = require('../../blue_modules/fs');
const btcAddressRx = /^[a-zA-Z0-9]{26,35}$/;
@ -131,7 +133,6 @@ const styles = StyleSheet.create({
createButton: {
marginHorizontal: 56,
marginVertical: 16,
alignItems: 'center',
minHeight: 44,
},
select: {
@ -309,12 +310,14 @@ export default class SendDetails extends Component {
units[this.state.recipientsScrollIndex] = BitcoinUnit.BTC; // also resetting current unit to BTC
recipients[[this.state.recipientsScrollIndex]].address = address;
recipients[[this.state.recipientsScrollIndex]].amount = options.amount;
recipients[[this.state.recipientsScrollIndex]].amountSats = new BigNumber(options.amount).multipliedBy(100000000).toNumber();
this.setState({
addresses: recipients,
memo: options.label || options.message,
isLoading: false,
amountUnit: BitcoinUnit.BTC,
units,
payjoinUrl: options.pj || '',
});
} else {
this.setState({ isLoading: false });
@ -332,10 +335,10 @@ export default class SendDetails extends Component {
if (this.props.route.params.uri) {
const uri = this.props.route.params.uri;
try {
const { address, amount, memo } = this.decodeBitcoinUri(uri);
const { address, amount, memo, payjoinUrl } = DeeplinkSchemaMatch.decodeBitcoinUri(uri);
addresses.push(new BitcoinTransaction(address, amount, currency.btcToSatoshi(amount)));
initialMemo = memo;
this.setState({ addresses, memo: initialMemo, isLoading: false, amountUnit: BitcoinUnit.BTC });
this.setState({ addresses, memo: initialMemo, isLoading: false, amountUnit: BitcoinUnit.BTC, payjoinUrl });
} catch (error) {
console.log(error);
alert(loc.send.details_error_decode);
@ -359,10 +362,10 @@ export default class SendDetails extends Component {
}
} catch (_) {}
await this.reCalcTx();
this.reCalcTx();
try {
const recommendedFees = await NetworkTransactionFees.recommendedFees();
const recommendedFees = await Promise.race([NetworkTransactionFees.recommendedFees(), BlueApp.sleep(2000)]);
if (recommendedFees && 'fastestFee' in recommendedFees) {
await AsyncStorage.setItem(NetworkTransactionFee.StorageKey, JSON.stringify(recommendedFees));
this.setState({
@ -370,21 +373,28 @@ export default class SendDetails extends Component {
networkTransactionFees: recommendedFees,
});
}
} catch (_) {}
} catch (_) {} // either sleep expired or recommendedFees threw an exception
if (this.props.route.params.uri) {
try {
const { address, amount, memo } = this.decodeBitcoinUri(this.props.route.params.uri);
this.setState({ address, amount, memo });
const { address, amount, memo, payjoinUrl } = DeeplinkSchemaMatch.decodeBitcoinUri(this.props.route.params.uri);
this.setState({ address, amount, memo, isLoading: false, payjoinUrl });
} catch (error) {
console.log(error);
this.setState({ isLoading: false });
alert(loc.send.details_error_decode);
}
}
await this.state.fromWallet.fetchUtxo();
try {
await Promise.race([this.state.fromWallet.fetchUtxo(), BlueApp.sleep(6000)]);
} catch (_) {
// either sleep expired or fetchUtxo threw an exception
}
this.setState({ isLoading: false });
await this.reCalcTx();
this.reCalcTx();
}
componentWillUnmount() {
@ -400,27 +410,6 @@ export default class SendDetails extends Component {
this.setState({ renderWalletSelectionButtonHidden: false, isAmountToolbarVisibleForAndroid: false });
};
decodeBitcoinUri(uri) {
let amount = '';
let parsedBitcoinUri = null;
let address = uri || '';
let memo = '';
try {
parsedBitcoinUri = DeeplinkSchemaMatch.bip21decode(uri);
address = 'address' in parsedBitcoinUri ? parsedBitcoinUri.address : address;
if ('options' in parsedBitcoinUri) {
if ('amount' in parsedBitcoinUri.options) {
amount = parsedBitcoinUri.options.amount.toString();
amount = parsedBitcoinUri.options.amount;
}
if ('label' in parsedBitcoinUri.options) {
memo = parsedBitcoinUri.options.label || memo;
}
}
} catch (_) {}
return { address, amount, memo };
}
async createTransaction() {
Keyboard.dismiss();
this.setState({ isLoading: true });
@ -484,13 +473,63 @@ export default class SendDetails extends Component {
}
}
getChangeAddressFast() {
if (this.state.changeAddress) return this.state.changeAddress; // cache
/** @type {AbstractHDElectrumWallet|WatchOnlyWallet} */
const wallet = this.state.fromWallet;
let changeAddress;
if (WatchOnlyWallet.type === wallet.type && !wallet.isHd()) {
// plain watchonly - just get the address
changeAddress = wallet.getAddress();
} else if (WatchOnlyWallet.type === wallet.type || wallet instanceof AbstractHDElectrumWallet) {
changeAddress = wallet._getInternalAddressByIndex(wallet.getNextFreeChangeAddressIndex());
} else {
// legacy wallets
changeAddress = wallet.getAddress();
}
return changeAddress;
}
async getChangeAddressAsync() {
if (this.state.changeAddress) return this.state.changeAddress; // cache
/** @type {AbstractHDElectrumWallet|WatchOnlyWallet} */
const wallet = this.state.fromWallet;
let changeAddress;
if (WatchOnlyWallet.type === wallet.type && !wallet.isHd()) {
// plain watchonly - just get the address
changeAddress = wallet.getAddress();
} else {
// otherwise, lets call widely-used getChangeAddressAsync()
try {
changeAddress = await Promise.race([BlueApp.sleep(2000), wallet.getChangeAddressAsync()]);
} catch (_) {}
if (!changeAddress) {
// either sleep expired or getChangeAddressAsync threw an exception
if (wallet instanceof AbstractHDElectrumWallet) {
changeAddress = wallet._getInternalAddressByIndex(wallet.getNextFreeChangeAddressIndex());
} else {
// legacy wallets
changeAddress = wallet.getAddress();
}
}
}
if (changeAddress) this.setState({ changeAddress }); // cache
return changeAddress;
}
/**
* Recalculating fee options by creating skeleton of future tx.
*/
reCalcTx = async (all = false) => {
reCalcTx = (all = false) => {
const wallet = this.state.fromWallet;
const fees = this.state.networkTransactionFees;
const changeAddress = await wallet.getChangeAddressAsync();
const changeAddress = this.getChangeAddressFast();
const requestedSatPerByte = Number(this.state.fee);
const feePrecalc = { ...this.state.feePrecalc };
@ -564,7 +603,7 @@ export default class SendDetails extends Component {
async createPsbtTransaction() {
/** @type {HDSegwitBech32Wallet} */
const wallet = this.state.fromWallet;
const changeAddress = await wallet.getChangeAddressAsync();
const changeAddress = await this.getChangeAddressAsync();
const requestedSatPerByte = Number(this.state.fee);
console.log({ requestedSatPerByte, utxo: wallet.getUtxo() });
@ -606,6 +645,16 @@ export default class SendDetails extends Component {
return;
}
if (wallet.type === MultisigHDWallet.type) {
this.props.navigation.navigate('PsbtMultisig', {
memo: this.state.memo,
psbtBase64: psbt.toBase64(),
walletId: wallet.getID(),
});
this.setState({ isLoading: false });
return;
}
BlueApp.tx_metadata = BlueApp.tx_metadata || {};
BlueApp.tx_metadata[tx.getId()] = {
txhex: tx.toHex(),
@ -619,6 +668,8 @@ export default class SendDetails extends Component {
tx: tx.toHex(),
recipients: targets,
satoshiPerByte: requestedSatPerByte,
payjoinUrl: this.state.payjoinUrl,
psbt,
});
this.setState({ isLoading: false });
}
@ -772,32 +823,54 @@ export default class SendDetails extends Component {
);
};
/**
* watch-only wallets with enabled HW wallet support have different flow. we have to show PSBT to user as QR code
* so he can scan it and sign it. then we have to scan it back from user (via camera and QR code), and ask
* user whether he wants to broadcast it.
* alternatively, user can export psbt file, sign it externally and then import it
*
* @returns {Promise<void>}
*/
importTransaction = async () => {
if (this.state.fromWallet.type !== WatchOnlyWallet.type) {
alert('Error: importing transaction in non-watchonly wallet (this should never happen)');
return;
}
try {
const res = await DocumentPicker.pick({
type: Platform.OS === 'ios' ? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn'] : [DocumentPicker.types.allFiles],
type:
Platform.OS === 'ios'
? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn', DocumentPicker.types.plainText, 'public.json']
: [DocumentPicker.types.allFiles],
});
if (DeeplinkSchemaMatch.isPossiblyPSBTFile(res.uri)) {
if (DeeplinkSchemaMatch.isPossiblySignedPSBTFile(res.uri)) {
// we assume that transaction is already signed, so all we have to do is get txhex and pass it to next screen
// so user can broadcast:
const file = await RNFS.readFile(res.uri, 'ascii');
const bufferDecoded = Buffer.from(file, 'ascii').toString('base64');
if (bufferDecoded) {
if (this.state.fromWallet.type === WatchOnlyWallet.type) {
// watch-only wallets with enabled HW wallet support have different flow. we have to show PSBT to user as QR code
// so he can scan it and sign it. then we have to scan it back from user (via camera and QR code), and ask
// user whether he wants to broadcast it.
// alternatively, user can export psbt file, sign it externally and then import it
this.props.navigation.navigate('PsbtWithHardwareWallet', {
memo: this.state.memo,
fromWallet: this.state.fromWallet,
psbt: file,
});
this.setState({ isLoading: false });
return;
}
} else {
throw new Error();
}
const psbt = bitcoin.Psbt.fromBase64(file);
const txhex = psbt.extractTransaction().toHex();
this.props.navigation.navigate('PsbtWithHardwareWallet', {
memo: this.state.memo,
fromWallet: this.state.fromWallet,
txhex,
});
this.setState({ isLoading: false, isAdvancedTransactionOptionsVisible: false });
} else if (DeeplinkSchemaMatch.isPossiblyPSBTFile(res.uri)) {
// looks like transaction is UNsigned, so we construct PSBT object and pass to next screen
// so user can do smth with it:
const file = await RNFS.readFile(res.uri, 'ascii');
const psbt = bitcoin.Psbt.fromBase64(file);
this.props.navigation.navigate('PsbtWithHardwareWallet', {
memo: this.state.memo,
fromWallet: this.state.fromWallet,
psbt,
});
this.setState({ isLoading: false, isAdvancedTransactionOptionsVisible: false });
} else if (DeeplinkSchemaMatch.isTXNFile(res.uri)) {
// plain text file with txhex ready to broadcast
const file = await RNFS.readFile(res.uri, 'ascii');
this.props.navigation.navigate('PsbtWithHardwareWallet', {
memo: this.state.memo,
@ -805,7 +878,8 @@ export default class SendDetails extends Component {
txhex: file,
});
this.setState({ isLoading: false, isAdvancedTransactionOptionsVisible: false });
return;
} else {
alert('Unrecognized file format');
}
} catch (err) {
if (!DocumentPicker.isCancel(err)) {
@ -814,6 +888,112 @@ export default class SendDetails extends Component {
}
};
askCosignThisTransaction = async () => {
return new Promise(resolve => {
Alert.alert(
loc.multisig.cosign_this_transaction,
'',
[
{
text: loc._.no,
style: 'cancel',
onPress: () => resolve(false),
},
{
text: loc._.yes,
onPress: () => resolve(true),
},
],
{ cancelable: false },
);
});
};
_importTransactionMultisig = async base64arg => {
try {
/** @type MultisigHDWallet */
const fromWallet = this.state.fromWallet;
const base64 = base64arg || (await fs.openSignedTransaction());
if (!base64) return;
const psbt = bitcoin.Psbt.fromBase64(base64); // if it doesnt throw - all good, its valid
if (fromWallet.howManySignaturesCanWeMake() > 0 && (await this.askCosignThisTransaction())) {
fromWallet.cosignPsbt(psbt);
}
this.props.navigation.navigate('PsbtMultisig', {
memo: this.state.memo,
psbtBase64: psbt.toBase64(),
walletId: fromWallet.getID(),
});
} catch (error) {
alert(loc.send.problem_with_psbt + ': ' + error.message);
}
this.setState({ isLoading: false, isAdvancedTransactionOptionsVisible: false });
};
importTransactionMultisig = async () => {
return this._importTransactionMultisig();
};
onBarScanned = ret => {
this.props.navigation.dangerouslyGetParent().pop();
if (!ret.data) ret = { data: ret };
if (ret.data.toUpperCase().startsWith('UR')) {
alert('BC-UR not decoded. This should never happen');
} else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
// we dont support it in this flow
} else {
// psbt base64?
return this._importTransactionMultisig(ret.data);
}
};
importTransactionMultisigScanQr = async () => {
this.setState({ isAdvancedTransactionOptionsVisible: false });
this.props.navigation.navigate('ScanQRCodeRoot', {
screen: 'ScanQRCode',
params: {
onBarScanned: this.onBarScanned,
showFileImportButton: true,
},
});
};
handleAddRecipient = () => {
const { addresses } = this.state;
addresses.push(new BitcoinTransaction());
this.setState(
{
addresses,
isAdvancedTransactionOptionsVisible: false,
},
() => {
this.scrollView.scrollToEnd();
if (this.state.addresses.length > 1) this.scrollView.flashScrollIndicators();
// after adding recipient it automatically scrolls to the last one
this.setState({ recipientsScrollIndex: this.state.addresses.length - 1 });
},
);
};
handleRemoveRecipient = () => {
const { addresses } = this.state;
addresses.splice(this.state.recipientsScrollIndex, 1);
this.setState(
{
addresses,
isAdvancedTransactionOptionsVisible: false,
},
() => {
if (this.state.addresses.length > 1) this.scrollView.flashScrollIndicators();
// after deletion it automatically scrolls to the last one
this.setState({ recipientsScrollIndex: this.state.addresses.length - 1 });
},
);
};
renderAdvancedTransactionOptionsModal = () => {
const isSendMaxUsed = this.state.addresses.some(element => element.amount === BitcoinUnit.MAX);
return (
@ -856,6 +1036,22 @@ export default class SendDetails extends Component {
onPress={this.importTransaction}
/>
)}
{this.state.fromWallet.type === MultisigHDWallet.type && (
<BlueListItem
title={loc.send.details_adv_import}
hideChevron
component={TouchableOpacity}
onPress={this.importTransactionMultisig}
/>
)}
{this.state.fromWallet.type === MultisigHDWallet.type && this.state.fromWallet.howManySignaturesCanWeMake() > 0 && (
<BlueListItem
title={loc.multisig.co_sign_transaction}
hideChevron
component={TouchableOpacity}
onPress={this.importTransactionMultisigScanQr}
/>
)}
{this.state.fromWallet.allowBatchSend() && (
<>
<BlueListItem
@ -863,43 +1059,14 @@ export default class SendDetails extends Component {
title={loc.send.details_add_rec_add}
hideChevron
component={TouchableOpacity}
onPress={() => {
const addresses = this.state.addresses;
addresses.push(new BitcoinTransaction());
this.setState(
{
addresses,
isAdvancedTransactionOptionsVisible: false,
},
() => {
this.scrollView.scrollToEnd();
if (this.state.addresses.length > 1) this.scrollView.flashScrollIndicators();
// after adding recipient it automatically scrolls to the last one
this.setState({ recipientsScrollIndex: this.state.addresses.length - 1 });
},
);
}}
onPress={this.handleAddRecipient}
/>
<BlueListItem
title={loc.send.details_add_rec_rem}
hideChevron
disabled={this.state.addresses.length < 2}
component={TouchableOpacity}
onPress={() => {
const addresses = this.state.addresses;
addresses.splice(this.state.recipientsScrollIndex, 1);
this.setState(
{
addresses,
isAdvancedTransactionOptionsVisible: false,
},
() => {
if (this.state.addresses.length > 1) this.scrollView.flashScrollIndicators();
// after deletion it automatically scrolls to the last one
this.setState({ recipientsScrollIndex: this.state.addresses.length - 1 });
},
);
}}
onPress={this.handleRemoveRecipient}
/>
</>
)}
@ -1033,7 +1200,7 @@ export default class SendDetails extends Component {
onChangeText={async text => {
text = text.trim();
const transactions = this.state.addresses;
const { address, amount, memo } = this.decodeBitcoinUri(text);
const { address, amount, memo, payjoinUrl } = DeeplinkSchemaMatch.decodeBitcoinUri(text);
item.address = address || text;
item.amount = amount || item.amount;
transactions[index] = item;
@ -1041,6 +1208,7 @@ export default class SendDetails extends Component {
addresses: transactions,
memo: memo || this.state.memo,
isLoading: false,
payjoinUrl,
});
this.reCalcTx();
}}
@ -1185,6 +1353,7 @@ SendDetails.propTypes = {
goBack: PropTypes.func,
navigate: PropTypes.func,
setParams: PropTypes.func,
dangerouslyGetParent: PropTypes.func,
}),
route: PropTypes.shape({
name: PropTypes.string,

491
screen/send/psbtMultisig.js Normal file
View file

@ -0,0 +1,491 @@
/* global alert */
import React, { useState } from 'react';
import { FlatList, Platform, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { BlueButton, BlueButtonLink, BlueCard, BlueNavigationStyle, BlueSpacing20, BlueText, SafeBlueArea } from '../../BlueComponents';
import { DynamicQRCode } from '../../components/DynamicQRCode';
import { SquareButton } from '../../components/SquareButton';
import { getSystemName } from 'react-native-device-info';
import loc from '../../loc';
import { Icon } from 'react-native-elements';
import ImagePicker from 'react-native-image-picker';
import ScanQRCode from './ScanQRCode';
import { useNavigation, useRoute, useTheme } from '@react-navigation/native';
import { BitcoinUnit } from '../../models/bitcoinUnits';
const BlueApp = require('../../BlueApp');
const bitcoin = require('bitcoinjs-lib');
const currency = require('../../blue_modules/currency');
const fs = require('../../blue_modules/fs');
const LocalQRCode = require('@remobile/react-native-qrcode-local-image');
const isDesktop = getSystemName() === 'Mac OS X';
const BigNumber = require('bignumber.js');
const shortenAddress = addr => {
return addr.substr(0, Math.floor(addr.length / 2) - 1) + '\n' + addr.substr(Math.floor(addr.length / 2) - 1, addr.length);
};
const PsbtMultisig = () => {
const navigation = useNavigation();
const route = useRoute();
const { colors } = useTheme();
const [flatListHeight, setFlatListHeight] = useState(0);
const walletId = route.params.walletId;
const psbtBase64 = route.params.psbtBase64;
const memo = route.params.memo;
const [psbt, setPsbt] = useState(bitcoin.Psbt.fromBase64(psbtBase64));
const [isModalVisible, setIsModalVisible] = useState(false);
const stylesHook = StyleSheet.create({
root: {
backgroundColor: colors.elevated,
},
textBtc: {
color: colors.buttonAlternativeTextColor,
},
textDestinationFirstFour: {
color: colors.buttonAlternativeTextColor,
},
textBtcUnitValue: {
color: colors.buttonAlternativeTextColor,
},
textDestination: {
color: colors.foregroundColor,
},
modalContentShort: {
backgroundColor: colors.elevated,
},
textFiat: {
color: colors.alternativeTextColor,
},
provideSignatureButton: {
backgroundColor: colors.buttonDisabledBackgroundColor,
},
exportButton: {
backgroundColor: colors.buttonDisabledBackgroundColor,
},
provideSignatureButtonText: {
color: colors.buttonTextColor,
},
vaultKeyCircle: {
backgroundColor: colors.buttonDisabledBackgroundColor,
},
vaultKeyText: {
color: colors.alternativeTextColor,
},
feeFiatText: {
color: colors.alternativeTextColor,
},
vaultKeyCircleSuccess: {
backgroundColor: colors.msSuccessBG,
},
vaultKeyTextSigned: {
color: colors.msSuccessBG,
},
});
/** @type MultisigHDWallet */
const wallet = BlueApp.getWallets().find(w => w.getID() === walletId);
let destination = [];
let totalSat = 0;
const targets = [];
for (const output of psbt.txOutputs) {
if (output.address && !wallet.weOwnAddress(output.address)) {
totalSat += output.value;
destination.push(output.address);
targets.push({ address: output.address, value: output.value });
}
}
destination = shortenAddress(destination.join(', '));
const totalBtc = new BigNumber(totalSat).dividedBy(100000000).toNumber();
const totalFiat = currency.satoshiToLocalCurrency(totalSat);
const fileName = `${Date.now()}.psbt`;
const howManySignaturesWeHave = () => {
return wallet.calculateHowManySignaturesWeHaveFromPsbt(psbt);
};
const getFee = () => {
return wallet.calculateFeeFromPsbt(psbt);
};
const _renderItem = el => {
if (el.index >= howManySignaturesWeHave()) return _renderItemUnsigned(el);
else return _renderItemSigned(el);
};
const _renderItemUnsigned = el => {
const renderProvideSignature = el.index === howManySignaturesWeHave();
return (
<View>
<View style={styles.itemUnsignedWrapper}>
<View style={[styles.vaultKeyCircle, stylesHook.vaultKeyCircle]}>
<Text style={[styles.vaultKeyText, stylesHook.vaultKeyText]}>{el.index + 1}</Text>
</View>
<View style={styles.vaultKeyTextWrapper}>
<Text style={[styles.vaultKeyText, stylesHook.vaultKeyText]}>
{loc.formatString(loc.multisig.vault_key, { number: el.index + 1 })}
</Text>
</View>
</View>
{renderProvideSignature && (
<View>
<TouchableOpacity
style={[styles.provideSignatureButton, stylesHook.provideSignatureButton]}
onPress={() => {
setIsModalVisible(true);
}}
>
<Text style={[styles.provideSignatureButtonText, stylesHook.provideSignatureButtonText]}>
{loc.multisig.provide_signature}
</Text>
</TouchableOpacity>
</View>
)}
</View>
);
};
const _renderItemSigned = el => {
return (
<View style={styles.flexDirectionRow}>
<View style={[styles.vaultKeyCircleSuccess, stylesHook.vaultKeyCircleSuccess]}>
<Icon size={24} name="check" type="ionicons" color={colors.msSuccessCheck} />
</View>
<View style={styles.vaultKeyTextSignedWrapper}>
<Text style={[styles.vaultKeyTextSigned, stylesHook.vaultKeyTextSigned]}>
{loc.formatString(loc.multisig.vault_key, { number: el.index + 1 })}
</Text>
</View>
</View>
);
};
const _combinePSBT = receivedPSBTBase64 => {
const receivedPSBT = bitcoin.Psbt.fromBase64(receivedPSBTBase64);
try {
const newPsbt = psbt.combine(receivedPSBT);
navigation.dangerouslyGetParent().pop();
setPsbt(newPsbt);
setIsModalVisible(false);
} catch (error) {
alert(error);
}
};
const onBarScanned = ret => {
if (!ret.data) ret = { data: ret };
if (ret.data.toUpperCase().startsWith('UR')) {
alert('BC-UR not decoded. This should never happen');
} else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
// we dont support it in this flow
} else {
// psbt base64?
_combinePSBT(ret.data);
}
};
const onConfirm = () => {
try {
psbt.finalizeAllInputs();
} catch (_) {} // ignore if it is already finalized
try {
const tx = psbt.extractTransaction().toHex();
const satoshiPerByte = Math.round(getFee() / (tx.length / 2));
navigation.navigate('Confirm', {
fee: new BigNumber(getFee()).dividedBy(100000000).toNumber(),
memo: memo,
fromWallet: wallet,
tx,
recipients: targets,
satoshiPerByte,
});
} catch (error) {
alert(error);
}
};
const openScanner = () => {
if (isDesktop) {
ImagePicker.launchCamera(
{
title: null,
mediaType: 'photo',
takePhotoButtonTitle: null,
},
response => {
if (response.uri) {
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.path.toString();
LocalQRCode.decode(uri, (error, result) => {
if (!error) {
onBarScanned(result);
} else {
alert(loc.send.qr_error_no_qrcode);
}
});
} else if (response.error) {
ScanQRCode.presentCameraNotAuthorizedAlert(response.error);
}
},
);
} else {
navigation.navigate('ScanQRCodeRoot', {
screen: 'ScanQRCode',
params: {
onBarScanned: onBarScanned,
showFileImportButton: true,
},
});
}
};
const exportPSBT = async () => {
await fs.writeFileAndExport(fileName, psbt.toBase64());
};
const isConfirmEnabled = () => {
return howManySignaturesWeHave() >= wallet.getM();
};
const renderDynamicQrCode = () => {
return (
<SafeBlueArea style={[styles.root, stylesHook.root]}>
<ScrollView centerContent contentContainerStyle={styles.scrollViewContent}>
<View style={[styles.modalContentShort, stylesHook.modalContentShort]}>
<DynamicQRCode value={psbt.toHex()} capacity={666} />
<BlueSpacing20 />
<SquareButton
style={[styles.exportButton, stylesHook.exportButton]}
onPress={openScanner}
title={loc.multisig.scan_or_import_file}
/>
<BlueSpacing20 />
<SquareButton style={[styles.exportButton, stylesHook.exportButton]} onPress={exportPSBT} title={loc.multisig.share} />
<BlueSpacing20 />
<BlueButtonLink title={loc._.cancel} onPress={() => setIsModalVisible(false)} />
</View>
</ScrollView>
</SafeBlueArea>
);
};
const destinationAddress = () => {
// eslint-disable-next-line prefer-const
let destinationAddressView = [];
const destinations = Object.entries(destination.split(','));
for (const [index, address] of destinations) {
if (index > 1) {
destinationAddressView.push(
<View style={styles.destionationTextContainer} key={`end-${index}`}>
<Text numberOfLines={0} style={[styles.textDestinationFirstFour, stylesHook.textFiat]}>
and {destinations.length - 2} more...
</Text>
</View>,
);
break;
} else {
const currentAddress = address;
const firstFour = currentAddress.substring(0, 5);
const lastFour = currentAddress.substring(currentAddress.length - 5, currentAddress.length);
const middle = currentAddress.split(firstFour)[1].split(lastFour)[0];
destinationAddressView.push(
<View style={styles.destionationTextContainer} key={`${currentAddress}-${index}`}>
<Text numberOfLines={2} style={[styles.textDestinationFirstFour, stylesHook.textBtc]}>
{firstFour}
<Text> </Text>
<Text style={[styles.textDestination, stylesHook.textFiat]}>{middle}</Text>
<Text> </Text>
<Text style={[styles.textDestinationFirstFour, stylesHook.textBtc]}>{lastFour}</Text>
</Text>
</View>,
);
}
}
return destinationAddressView;
};
const header = (
<View style={stylesHook.root}>
<View style={styles.containerText}>
<BlueText style={[styles.textBtc, stylesHook.textBtc]}>{totalBtc}</BlueText>
<View style={styles.textBtcUnit}>
<BlueText style={[styles.textBtcUnitValue, stylesHook.textBtcUnitValue]}> {BitcoinUnit.BTC}</BlueText>
</View>
</View>
<View style={styles.containerText}>
<BlueText style={[styles.textFiat, stylesHook.textFiat]}>{totalFiat}</BlueText>
</View>
<View>{destinationAddress()}</View>
</View>
);
const footer = (
<View style={styles.bottomWrapper}>
<View style={styles.bottomFeesWrapper}>
<BlueText style={[styles.feeFiatText, stylesHook.feeFiatText]}>
{loc.formatString(loc.multisig.fee, { number: currency.satoshiToLocalCurrency(getFee()) })} -{' '}
</BlueText>
<BlueText>{loc.formatString(loc.multisig.fee_btc, { number: currency.satoshiToBTC(getFee()) })}</BlueText>
</View>
<BlueButton disabled={!isConfirmEnabled()} title={loc.multisig.confirm} onPress={onConfirm} />
</View>
);
if (isModalVisible) return renderDynamicQrCode();
const onLayout = e => {
setFlatListHeight(e.nativeEvent.layout.height);
};
const data = new Array(wallet.getM());
return (
<SafeBlueArea style={[styles.root, stylesHook.root]}>
<View style={styles.container}>
<View style={styles.mstopcontainer}>
<View style={styles.mscontainer}>
<View style={[styles.msleft, { height: flatListHeight - 200 }]} />
</View>
<View style={styles.msright}>
<BlueCard>
<FlatList
data={data}
onLayout={onLayout}
renderItem={_renderItem}
keyExtractor={(_item, index) => `${index}`}
ListHeaderComponent={header}
scrollEnabled={false}
/>
</BlueCard>
</View>
</View>
{footer}
</View>
</SafeBlueArea>
);
};
const styles = StyleSheet.create({
root: {
flex: 1,
},
mstopcontainer: {
flex: 1,
flexDirection: 'row',
},
mscontainer: {
flex: 10,
},
msleft: {
width: 1,
borderStyle: 'dashed',
borderWidth: 0.8,
borderColor: '#c4c4c4',
marginLeft: 40,
marginTop: 185,
},
msright: {
flex: 90,
marginLeft: '-11%',
},
scrollViewContent: {
flexGrow: 1,
justifyContent: 'space-between',
},
container: {
flexDirection: 'column',
paddingTop: 24,
flex: 1,
},
containerText: {
flexDirection: 'row',
justifyContent: 'center',
},
destionationTextContainer: {
flexDirection: 'row',
marginBottom: 4,
paddingHorizontal: 60,
fontSize: 14,
justifyContent: 'center',
},
textFiat: {
fontSize: 16,
fontWeight: '500',
marginBottom: 30,
},
textBtc: {
fontWeight: 'bold',
fontSize: 30,
},
textDestinationFirstFour: {
fontSize: 14,
},
textDestination: {
paddingTop: 10,
paddingBottom: 40,
fontSize: 14,
flexWrap: 'wrap',
},
bottomModal: {
justifyContent: 'flex-end',
margin: 0,
},
modalContentShort: {
marginLeft: 20,
marginRight: 20,
},
copyToClipboard: {
justifyContent: 'center',
alignItems: 'center',
},
exportButton: {
height: 48,
borderRadius: 8,
flex: 1,
justifyContent: 'center',
paddingHorizontal: 16,
},
provideSignatureButton: {
marginTop: 24,
marginLeft: 40,
height: 48,
borderRadius: 8,
flex: 1,
justifyContent: 'center',
paddingHorizontal: 16,
marginBottom: 8,
},
provideSignatureButtonText: { fontWeight: '600', fontSize: 15 },
vaultKeyText: { fontSize: 18, fontWeight: 'bold' },
vaultKeyTextWrapper: { justifyContent: 'center', alignItems: 'center', paddingLeft: 16 },
vaultKeyCircle: {
width: 42,
height: 42,
borderRadius: 25,
justifyContent: 'center',
alignItems: 'center',
},
vaultKeyCircleSuccess: {
width: 42,
height: 42,
borderRadius: 25,
justifyContent: 'center',
alignItems: 'center',
},
itemUnsignedWrapper: { flexDirection: 'row', paddingTop: 16 },
textDestinationSpacingRight: { marginRight: 4 },
textDestinationSpacingLeft: { marginLeft: 4 },
vaultKeyTextSigned: { fontSize: 18, fontWeight: 'bold' },
vaultKeyTextSignedWrapper: { justifyContent: 'center', alignItems: 'center', paddingLeft: 16 },
flexDirectionRow: { flexDirection: 'row', paddingVertical: 12 },
textBtcUnit: { justifyContent: 'flex-end', bottom: 8 },
bottomFeesWrapper: { flexDirection: 'row', paddingBottom: 20 },
bottomWrapper: { justifyContent: 'center', alignItems: 'center', paddingVertical: 20 },
});
PsbtMultisig.navigationOptions = () => ({
...BlueNavigationStyle(null, false),
title: loc.multisig.header,
});
export default PsbtMultisig;

View file

@ -33,7 +33,6 @@ import { getSystemName } from 'react-native-device-info';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import RNFS from 'react-native-fs';
import DocumentPicker from 'react-native-document-picker';
import { decodeUR, extractSingleWorkload } from 'bc-ur/dist';
import loc from '../../loc';
import { BlueCurrentTheme } from '../../components/themes';
import ScanQRCode from './ScanQRCode';
@ -68,6 +67,7 @@ const styles = StyleSheet.create({
rootPadding: {
flex: 1,
paddingTop: 20,
backgroundColor: BlueCurrentTheme.colors.elevated,
},
closeCamera: {
width: 40,
@ -89,6 +89,7 @@ const styles = StyleSheet.create({
hexWrap: {
alignItems: 'center',
flex: 1,
backgroundColor: BlueCurrentTheme.colors.elevated,
},
hexLabel: {
color: BlueCurrentTheme.colors.foregroundColor,
@ -124,44 +125,6 @@ const styles = StyleSheet.create({
export default class PsbtWithHardwareWallet extends Component {
cameraRef = null;
_onReadUniformResource = ur => {
try {
const [index, total] = extractSingleWorkload(ur);
const { animatedQRCodeData } = this.state;
if (animatedQRCodeData.length > 0) {
const currentTotal = animatedQRCodeData[0].total;
if (total !== currentTotal) {
alert('invalid animated QRCode');
}
}
if (!animatedQRCodeData.find(i => i.index === index)) {
this.setState(
state => ({
animatedQRCodeData: [
...state.animatedQRCodeData,
{
index,
total,
data: ur,
},
],
}),
() => {
if (this.state.animatedQRCodeData.length === total) {
const payload = decodeUR(this.state.animatedQRCodeData.map(i => i.data));
const psbtB64 = Buffer.from(payload, 'hex').toString('base64');
const Tx = this._combinePSBT(psbtB64);
this.setState({ txhex: Tx.toHex() });
this.props.navigation.dangerouslyGetParent().pop();
}
},
);
}
} catch (Err) {
alert('invalid animated QRCode fragment, please try again');
}
};
_combinePSBT = receivedPSBT => {
return this.state.fromWallet.combinePsbt(this.state.psbt, receivedPSBT);
};
@ -169,11 +132,11 @@ export default class PsbtWithHardwareWallet extends Component {
onBarScanned = ret => {
if (ret && !ret.data) ret = { data: ret };
if (ret.data.toUpperCase().startsWith('UR')) {
return this._onReadUniformResource(ret.data);
alert('BC-UR not decoded. This should never happen');
}
if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
this.setState({ txhex: ret.data }, () => this.props.navigation.dangerouslyGetParent().pop());
this.setState({ txhex: ret.data });
return;
}
try {
@ -201,11 +164,20 @@ export default class PsbtWithHardwareWallet extends Component {
}
static getDerivedStateFromProps(nextProps, prevState) {
if (!prevState.psbt && !nextProps.route.params.txhex) {
alert('There is no transaction signing in progress');
return {
...prevState,
isLoading: true,
};
}
const deepLinkPSBT = nextProps.route.params.deepLinkPSBT;
const txhex = nextProps.route.params.txhex;
if (deepLinkPSBT) {
const psbt = bitcoin.Psbt.fromBase64(deepLinkPSBT);
try {
const Tx = prevState.fromWallet.combinePsbt(prevState.psbt, deepLinkPSBT);
const Tx = prevState.fromWallet.combinePsbt(prevState.psbt, psbt);
return {
...prevState,
txhex: Tx.toHex(),

View file

@ -1,19 +1,104 @@
import React, { Component } from 'react';
import React, { useEffect, useRef } from 'react';
import LottieView from 'lottie-react-native';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import { View, StyleSheet } from 'react-native';
import { Text } from 'react-native-elements';
import { BlueButton, SafeBlueArea, BlueCard } from '../../BlueComponents';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import PropTypes from 'prop-types';
import loc from '../../loc';
import { BlueCurrentTheme } from '../../components/themes';
import { useNavigation, useRoute, useTheme } from '@react-navigation/native';
const Success = () => {
const { colors } = useTheme();
const { dangerouslyGetParent } = useNavigation();
const { amount, fee = 0, amountUnit = BitcoinUnit.BTC, invoiceDescription = '' } = useRoute().params;
const animationRef = useRef();
const stylesHook = StyleSheet.create({
root: {
backgroundColor: colors.elevated,
},
amountValue: {
color: colors.alternativeTextColor2,
},
amountUnit: {
color: colors.alternativeTextColor2,
},
});
useEffect(() => {
console.log('send/success - useEffect');
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const pop = () => {
dangerouslyGetParent().pop();
};
useEffect(() => {
animationRef.current.reset();
animationRef.current.resume();
}, [colors]);
return (
<SafeBlueArea style={[styles.root, stylesHook.root]}>
<BlueCard style={styles.amout}>
{amount > 0 && (
<View style={styles.view}>
<Text style={[styles.amountValue, stylesHook.amountValue]}>{amount}</Text>
<Text style={[styles.amountUnit, stylesHook.amountUnit]}>{' ' + amountUnit}</Text>
</View>
)}
{fee > 0 && (
<Text style={styles.feeText}>
{loc.send.create_fee}: {fee} {BitcoinUnit.BTC}
</Text>
)}
{fee <= 0 && (
<Text numberOfLines={0} style={styles.feeText}>
{invoiceDescription}
</Text>
)}
</BlueCard>
<View style={styles.ready}>
<LottieView
style={styles.lottie}
source={require('../../img/bluenice.json')}
autoPlay
ref={animationRef}
loop={false}
colorFilters={[
{
keypath: 'spark',
color: colors.success,
},
{
keypath: 'circle',
color: colors.success,
},
{
keypath: 'Oval',
color: colors.successCheck,
},
]}
/>
</View>
<BlueCard>
<BlueButton onPress={pop} title={loc.send.success_done} />
</BlueCard>
</SafeBlueArea>
);
};
Success.navigationOptions = {
headerShown: false,
gesturesEnabled: false,
};
export default Success;
const styles = StyleSheet.create({
root: {
flex: 1,
paddingTop: 19,
backgroundColor: BlueCurrentTheme.colors.elevated,
},
amout: {
alignItems: 'center',
@ -26,12 +111,10 @@ const styles = StyleSheet.create({
paddingBottom: 16,
},
amountValue: {
color: BlueCurrentTheme.colors.alternativeTextColor2,
fontSize: 36,
fontWeight: '600',
},
amountUnit: {
color: BlueCurrentTheme.colors.alternativeTextColor2,
fontSize: 16,
marginHorizontal: 4,
paddingBottom: 6,
@ -61,92 +144,3 @@ const styles = StyleSheet.create({
height: 400,
},
});
export default class Success extends Component {
constructor(props) {
super(props);
console.log('send/success constructor');
this.state = {
amount: props.route.params.amount,
fee: props.route.params.fee || 0,
amountUnit: props.route.params.amountUnit || BitcoinUnit.BTC,
invoiceDescription: props.route.params.invoiceDescription || '',
};
}
async componentDidMount() {
console.log('send/success - componentDidMount');
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
}
render() {
return (
<SafeBlueArea style={styles.root}>
<BlueCard style={styles.amout}>
<View style={styles.view}>
<Text style={styles.amountValue}>{this.state.amount}</Text>
<Text style={styles.amountUnit}>{' ' + this.state.amountUnit}</Text>
</View>
{this.state.fee > 0 && (
<Text style={styles.feeText}>
{loc.send.create_fee}: {this.state.fee} {BitcoinUnit.BTC}
</Text>
)}
{this.state.fee <= 0 && (
<Text numberOfLines={0} style={styles.feeText}>
{this.state.invoiceDescription}
</Text>
)}
</BlueCard>
<View style={styles.ready}>
<LottieView
style={styles.lottie}
source={require('../../img/bluenice.json')}
autoPlay
loop={false}
colorFilters={[
{
keypath: 'spark',
color: BlueCurrentTheme.colors.success,
},
{
keypath: 'circle',
color: BlueCurrentTheme.colors.success,
},
{
keypath: 'Oval',
color: BlueCurrentTheme.colors.successCheck,
},
]}
/>
</View>
<BlueCard>
<BlueButton onPress={() => this.props.navigation.dangerouslyGetParent().pop()} title={loc.send.success_done} />
</BlueCard>
</SafeBlueArea>
);
}
}
Success.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.func,
navigate: PropTypes.func,
dangerouslyGetParent: PropTypes.func,
state: PropTypes.shape({
params: PropTypes.shape({
amount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
fee: PropTypes.number,
}),
}),
}),
route: PropTypes.shape({
params: PropTypes.object,
}),
};
Success.navigationOptions = {
headerShown: false,
gesturesEnabled: false,
};

View file

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import { ScrollView, Platform, TouchableWithoutFeedback, TouchableOpacity, StyleSheet } from 'react-native';
import { BlueLoading, BlueTextHooks, BlueSpacing20, BlueListItemHooks, BlueNavigationStyle, BlueCard } from '../../BlueComponents';
import { BlueLoading, BlueTextHooks, BlueSpacing20, BlueListItem, BlueNavigationStyle, BlueCard } from '../../BlueComponents';
import { AppStorage } from '../../class';
import { useNavigation, useTheme } from '@react-navigation/native';
import HandoffSettings from '../../class/handoff';
@ -58,17 +58,12 @@ const GeneralSettings = () => {
<ScrollView style={stylesWithThemeHook.scroll}>
{BlueApp.getWallets().length > 1 && (
<>
<BlueListItemHooks
component={TouchableOpacity}
onPress={() => navigate('DefaultView')}
title={loc.settings.default_title}
chevron
/>
<BlueListItem component={TouchableOpacity} onPress={() => navigate('DefaultView')} title={loc.settings.default_title} chevron />
</>
)}
{Platform.OS === 'ios' ? (
<>
<BlueListItemHooks
<BlueListItem
hideChevron
title={loc.settings.general_continuity}
Component={TouchableWithoutFeedback}
@ -80,7 +75,7 @@ const GeneralSettings = () => {
<BlueSpacing20 />
</>
) : null}
<BlueListItemHooks
<BlueListItem
Component={TouchableWithoutFeedback}
title={loc.settings.general_adv_mode}
switch={{ onValueChange: onAdvancedModeSwitch, value: isAdancedModeEnabled }}

View file

@ -1,6 +1,6 @@
import React from 'react';
import { ScrollView, StyleSheet } from 'react-native';
import { SafeBlueArea, BlueListItemHooks, BlueNavigationStyle } from '../../BlueComponents';
import { SafeBlueArea, BlueListItem, BlueNavigationStyle } from '../../BlueComponents';
import { useNavigation, useTheme } from '@react-navigation/native';
import loc from '../../loc';
@ -29,9 +29,9 @@ const NetworkSettings = () => {
return (
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={styles.root}>
<ScrollView>
<BlueListItemHooks title={loc.settings.network_electrum} onPress={navigateToElectrumSettings} chevron />
<BlueListItemHooks title={loc.settings.lightning_settings} onPress={navigateToLightningSettings} chevron />
<BlueListItemHooks title={loc.settings.network_broadcast} onPress={navigateToBroadcast} chevron />
<BlueListItem title={loc.settings.network_electrum} onPress={navigateToElectrumSettings} chevron />
<BlueListItem title={loc.settings.lightning_settings} onPress={navigateToLightningSettings} chevron />
<BlueListItem title={loc.settings.network_broadcast} onPress={navigateToBroadcast} chevron />
</ScrollView>
</SafeBlueArea>
);

View file

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { ScrollView, Linking, Dimensions, Image, View, Text, StyleSheet } from 'react-native';
import React from 'react';
import { ScrollView, Linking, Image, View, Text, StyleSheet, useWindowDimensions } from 'react-native';
import { useNavigation, useTheme } from '@react-navigation/native';
import {
BlueTextCentered,
@ -7,20 +7,17 @@ import {
BlueButton,
SafeBlueArea,
BlueCard,
BlueListItemHooks,
BlueListItem,
BlueNavigationStyle,
BlueLoadingHook,
} from '../../BlueComponents';
import { getApplicationName, getVersion, getBundleId, getBuildNumber } from 'react-native-device-info';
import Rate, { AndroidMarket } from 'react-native-rate';
import loc from '../../loc';
const { width, height } = Dimensions.get('window');
const About = () => {
const [isLoading, setIsLoading] = useState(true);
const { navigate } = useNavigation();
const { colors } = useTheme();
const { width, height } = useWindowDimensions();
const styles = StyleSheet.create({
root: {
flex: 1,
@ -58,10 +55,6 @@ const About = () => {
},
});
useEffect(() => {
setIsLoading(false);
}, []);
const handleOnReleaseNotesPress = () => {
navigate('ReleaseNotes');
};
@ -102,9 +95,7 @@ const About = () => {
});
};
return isLoading ? (
<BlueLoadingHook />
) : (
return (
<SafeBlueArea style={styles.root}>
<ScrollView testID="AboutScrollView">
<BlueCard>
@ -115,7 +106,7 @@ const About = () => {
<BlueButton onPress={handleOnRatePress} title={loc.settings.about_review + ' ⭐🙏'} />
</View>
</BlueCard>
<BlueListItemHooks
<BlueListItem
leftIcon={{
name: 'twitter',
type: 'font-awesome',
@ -124,7 +115,7 @@ const About = () => {
onPress={handleOnTwitterPress}
title={loc.settings.about_sm_twitter}
/>
<BlueListItemHooks
<BlueListItem
leftIcon={{
name: 'telegram',
type: 'font-awesome',
@ -133,7 +124,7 @@ const About = () => {
onPress={handleOnTelegramPress}
title={loc.settings.about_sm_telegram}
/>
<BlueListItemHooks
<BlueListItem
leftIcon={{
name: 'github',
type: 'font-awesome',
@ -154,7 +145,7 @@ const About = () => {
<BlueTextCentered>Electrum server</BlueTextCentered>
</View>
</BlueCard>
<BlueListItemHooks
<BlueListItem
leftIcon={{
name: 'book',
type: 'font-awesome',
@ -164,7 +155,7 @@ const About = () => {
onPress={handleOnReleaseNotesPress}
title={loc.settings.about_release_notes}
/>
<BlueListItemHooks
<BlueListItem
leftIcon={{
name: 'law',
type: 'octicon',
@ -174,7 +165,7 @@ const About = () => {
onPress={handleOnLicensingPress}
title="MIT License"
/>
<BlueListItemHooks
<BlueListItem
leftIcon={{
name: 'flask',
type: 'font-awesome',

View file

@ -1,8 +1,7 @@
import React, { useState, useEffect } from 'react';
import { FlatList, TouchableOpacity, ActivityIndicator, View, StyleSheet } from 'react-native';
import { SafeBlueArea, BlueListItemHooks, BlueTextHooks, BlueCard, BlueNavigationStyle } from '../../BlueComponents';
import { SafeBlueArea, BlueListItem, BlueTextHooks, BlueCard, BlueNavigationStyle } from '../../BlueComponents';
import PropTypes from 'prop-types';
import { Icon } from 'react-native-elements';
import { FiatUnit } from '../../models/fiatUnit';
import loc from '../../loc';
import { useTheme } from '@react-navigation/native';
@ -52,11 +51,11 @@ const Currency = () => {
extraData={data}
renderItem={({ item }) => {
return (
<BlueListItemHooks
<BlueListItem
disabled={isSavingNewPreferredCurrency}
title={`${item.endPointKey} (${item.symbol})`}
{...(selectedCurrency.endPointKey === item.endPointKey
? { rightIcon: <Icon name="check" type="octaicon" color="#0070FF" /> }
? { rightIcon: { name: 'check', type: 'octaicon', color: '#0070FF' } }
: { hideChevron: true })}
Component={TouchableOpacity}
onPress={async () => {

View file

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { View, TouchableWithoutFeedback, StyleSheet } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { SafeBlueArea, BlueCard, BlueNavigationStyle, BlueListItemHooks, BlueTextHooks } from '../../BlueComponents';
import { SafeBlueArea, BlueCard, BlueNavigationStyle, BlueListItem, BlueTextHooks } from '../../BlueComponents';
import OnAppLaunch from '../../class/on-app-launch';
import loc from '../../loc';
const BlueApp = require('../../BlueApp');
@ -57,7 +57,7 @@ const DefaultView = () => {
return (
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={styles.flex}>
<View>
<BlueListItemHooks
<BlueListItem
title={loc.settings.default_wallets}
Component={TouchableWithoutFeedback}
switch={{
@ -70,7 +70,7 @@ const DefaultView = () => {
<BlueTextHooks>{loc.settings.default_desc}</BlueTextHooks>
</BlueCard>
{!viewAllWalletsEnabled && (
<BlueListItemHooks title={loc.settings.default_info} onPress={selectWallet} rightTitle={defaultWalletLabel} chevron />
<BlueListItem title={loc.settings.default_info} onPress={selectWallet} rightTitle={defaultWalletLabel} chevron />
)}
</View>
</SafeBlueArea>

View file

@ -8,7 +8,7 @@ import {
SafeBlueArea,
BlueSpacing20,
BlueCard,
BlueListItemHooks,
BlueListItem,
BlueHeaderDefaultSubHooks,
BlueTextHooks,
BlueNavigationStyle,
@ -150,7 +150,7 @@ const EncryptStorage = () => {
{biometrics.isDeviceBiometricCapable && (
<>
<BlueHeaderDefaultSubHooks leftText="biometrics" rightComponent={null} />
<BlueListItemHooks
<BlueListItem
title={loc.formatString(loc.settings.encrypt_use, { type: biometrics.biometricsType })}
Component={TouchableWithoutFeedback}
switch={{ value: biometrics.isBiometricsEnabled, onValueChange: onUseBiometricSwitch }}
@ -162,7 +162,7 @@ const EncryptStorage = () => {
</>
)}
<BlueHeaderDefaultSubHooks leftText={loc.settings.encrypt_tstorage} rightComponent={null} />
<BlueListItemHooks
<BlueListItem
testID="EncyptedAndPasswordProtected"
hideChevron
title={loc.settings.encrypt_enc_and_pass}
@ -170,7 +170,7 @@ const EncryptStorage = () => {
switch={{ onValueChange: onEncryptStorageSwitch, value: storageIsEncrypted }}
/>
{Platform.OS === 'ios' && (
<BlueListItemHooks
<BlueListItem
hideChevron
title={loc.settings.encrypt_del_uninstall}
Component={TouchableWithoutFeedback}
@ -181,7 +181,7 @@ const EncryptStorage = () => {
/>
)}
{storageIsEncrypted && (
<BlueListItemHooks
<BlueListItem
onPress={navigateToPlausibleDeniability}
title={loc.settings.plausible_deniability}
chevron

View file

@ -1,7 +1,6 @@
import React, { useState, useEffect, useCallback } from 'react';
import { FlatList, StyleSheet } from 'react-native';
import { SafeBlueArea, BlueListItemHooks, BlueCard, BlueLoadingHook, BlueNavigationStyle, BlueTextHooks } from '../../BlueComponents';
import { Icon } from 'react-native-elements';
import { SafeBlueArea, BlueListItem, BlueCard, BlueLoadingHook, BlueNavigationStyle, BlueTextHooks } from '../../BlueComponents';
import { AvailableLanguages } from '../../loc/languages';
import loc from '../../loc';
@ -22,7 +21,7 @@ const Language = () => {
const renderItem = useCallback(
({ item }) => {
return (
<BlueListItemHooks
<BlueListItem
onPress={() => {
console.log('setLanguage', item.value);
loc.saveLanguage(item.value);
@ -31,7 +30,7 @@ const Language = () => {
title={item.label}
{...(language === item.value
? {
rightIcon: <Icon name="check" type="octaicon" color="#0070FF" />,
rightIcon: { name: 'check', type: 'octaicon', color: '#0070FF' },
}
: { hideChevron: true })}
/>

View file

@ -5,7 +5,7 @@ import {
BlueLoading,
BlueTextHooks,
BlueSpacing20,
BlueListItemHooks,
BlueListItem,
BlueNavigationStyle,
BlueCard,
BlueButton,
@ -101,7 +101,7 @@ const NotificationSettings = () => {
<BlueLoading />
) : (
<ScrollView style={stylesWithThemeHook.scroll}>
<BlueListItemHooks
<BlueListItem
Component={TouchableWithoutFeedback}
title={loc.settings.push_notifications}
switch={{ onValueChange: onNotificationsSwitch, value: isNotificationsEnabled }}

View file

@ -1,6 +1,6 @@
import React from 'react';
import { ScrollView, TouchableOpacity, StyleSheet, StatusBar } from 'react-native';
import { BlueListItemHooks, BlueHeaderDefaultSubHooks } from '../../BlueComponents';
import { BlueListItem, BlueHeaderDefaultSubHooks } from '../../BlueComponents';
import { useNavigation } from '@react-navigation/native';
import loc from '../../loc';
@ -17,24 +17,24 @@ const Settings = () => {
<ScrollView style={styles.root}>
<StatusBar barStyle="default" />
<BlueHeaderDefaultSubHooks leftText={loc.settings.header} rightComponent={null} />
<BlueListItemHooks title={loc.settings.general} component={TouchableOpacity} onPress={() => navigate('GeneralSettings')} chevron />
<BlueListItemHooks title={loc.settings.currency} component={TouchableOpacity} onPress={() => navigate('Currency')} chevron />
<BlueListItemHooks title={loc.settings.language} component={TouchableOpacity} onPress={() => navigate('Language')} chevron />
<BlueListItemHooks
<BlueListItem title={loc.settings.general} component={TouchableOpacity} onPress={() => navigate('GeneralSettings')} chevron />
<BlueListItem title={loc.settings.currency} component={TouchableOpacity} onPress={() => navigate('Currency')} chevron />
<BlueListItem title={loc.settings.language} component={TouchableOpacity} onPress={() => navigate('Language')} chevron />
<BlueListItem
title={loc.settings.encrypt_title}
onPress={() => navigate('EncryptStorage')}
component={TouchableOpacity}
testID="SecurityButton"
chevron
/>
<BlueListItemHooks title={loc.settings.network} component={TouchableOpacity} onPress={() => navigate('NetworkSettings')} chevron />
<BlueListItemHooks
<BlueListItem title={loc.settings.network} component={TouchableOpacity} onPress={() => navigate('NetworkSettings')} chevron />
<BlueListItem
title={loc.settings.notifications}
component={TouchableOpacity}
onPress={() => navigate('NotificationSettings')}
chevron
/>
<BlueListItemHooks
<BlueListItem
title={loc.settings.about}
component={TouchableOpacity}
onPress={() => navigate('About')}

View file

@ -215,7 +215,12 @@ export default class TransactionsStatus extends Component {
}
const tx = new HDSegwitBech32Transaction(null, this.state.tx.hash, this.state.wallet);
if ((await tx.isOurTransaction()) && (await tx.getRemoteConfirmationsNum()) === 0 && (await tx.isSequenceReplaceable())) {
if (
(await tx.isOurTransaction()) &&
(await tx.getRemoteConfirmationsNum()) === 0 &&
(await tx.isSequenceReplaceable()) &&
(await tx.canBumpTx())
) {
return this.setState({ isRBFBumpFeePossible: buttonStatus.possible });
} else {
return this.setState({ isRBFBumpFeePossible: buttonStatus.notPossible });

View file

@ -16,7 +16,7 @@ import AsyncStorage from '@react-native-community/async-storage';
import {
BlueTextCenteredHooks,
BlueTextHooks,
BlueListItemHooks,
BlueListItem,
LightningButton,
BitcoinButton,
BlueFormLabel,
@ -27,7 +27,6 @@ import {
} from '../../BlueComponents';
import { HDSegwitBech32Wallet, SegwitP2SHWallet, HDSegwitP2SHWallet, LightningCustodianWallet, AppStorage } from '../../class';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import { Icon } from 'react-native-elements';
import { useTheme, useNavigation } from '@react-navigation/native';
import { Chain } from '../../models/bitcoinUnits';
import loc from '../../loc';
@ -93,7 +92,6 @@ const styles = StyleSheet.create({
borderRadius: 4,
},
createButton: {
alignItems: 'center',
flex: 1,
marginTop: 32,
},
@ -312,36 +310,36 @@ const WalletsAdd = () => {
<View>
<BlueSpacing20 />
<Text style={[styles.advancedText, stylesHook.advancedText]}>{loc.settings.advanced_options}</Text>
<BlueListItemHooks
<BlueListItem
containerStyle={[styles.noPadding, stylesHook.noPadding]}
bottomDivider={false}
onPress={() => setSelectedIndex(0)}
title={HDSegwitBech32Wallet.typeReadable}
{...(selectedIndex === 0
? {
rightIcon: <Icon name="check" type="octaicon" color="#0070FF" />,
rightIcon: { name: 'check', type: 'octaicon', color: '#0070FF' },
}
: { hideChevron: true })}
/>
<BlueListItemHooks
<BlueListItem
containerStyle={[styles.noPadding, stylesHook.noPadding]}
bottomDivider={false}
onPress={() => setSelectedIndex(1)}
title={SegwitP2SHWallet.typeReadable}
{...(selectedIndex === 1
? {
rightIcon: <Icon name="check" type="octaicon" color="#0070FF" />,
rightIcon: { name: 'check', type: 'octaicon', color: '#0070FF' },
}
: { hideChevron: true })}
/>
<BlueListItemHooks
<BlueListItem
containerStyle={[styles.noPadding, stylesHook.noPadding]}
bottomDivider={false}
onPress={() => setSelectedIndex(2)}
title={HDSegwitP2SHWallet.typeReadable}
{...(selectedIndex === 2
? {
rightIcon: <Icon name="check" type="octaicon" color="#0070FF" />,
rightIcon: { name: 'check', type: 'octaicon', color: '#0070FF' },
}
: { hideChevron: true })}
/>
@ -351,7 +349,7 @@ const WalletsAdd = () => {
return (
<>
<BlueSpacing20 />
<Text style={styles.advancedText}>{loc.settings.advanced_options}</Text>
<Text style={[styles.advancedText, stylesHook.advancedText]}>{loc.settings.advanced_options}</Text>
<BlueSpacing20 />
<BlueTextHooks>Connect to your LNDHub</BlueTextHooks>
<View style={[styles.lndUri, stylesHook.lndUri]}>

View file

@ -14,6 +14,7 @@ import {
Linking,
StyleSheet,
StatusBar,
PermissionsAndroid,
} from 'react-native';
import { SecondButton, SafeBlueArea, BlueCard, BlueSpacing20, BlueNavigationStyle, BlueText, BlueLoadingHook } from '../../BlueComponents';
import { LightningCustodianWallet } from '../../class/wallets/lightning-custodian-wallet';
@ -22,14 +23,18 @@ import { HDLegacyP2PKHWallet } from '../../class/wallets/hd-legacy-p2pkh-wallet'
import { HDSegwitP2SHWallet } from '../../class/wallets/hd-segwit-p2sh-wallet';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import Biometric from '../../class/biometrics';
import { HDSegwitBech32Wallet, SegwitP2SHWallet, LegacyWallet, SegwitBech32Wallet, WatchOnlyWallet } from '../../class';
import { HDSegwitBech32Wallet, SegwitP2SHWallet, LegacyWallet, SegwitBech32Wallet, WatchOnlyWallet, MultisigHDWallet } from '../../class';
import { ScrollView } from 'react-native-gesture-handler';
import loc from '../../loc';
import { useTheme, useRoute, useNavigation } from '@react-navigation/native';
import RNFS from 'react-native-fs';
import Share from 'react-native-share';
import { getSystemName } from 'react-native-device-info';
const EV = require('../../blue_modules/events');
const prompt = require('../../blue_modules/prompt');
const BlueApp = require('../../BlueApp');
const notifications = require('../../blue_modules/notifications');
const isDesktop = getSystemName() === 'Mac OS X';
const styles = StyleSheet.create({
root: {
@ -94,6 +99,7 @@ const styles = StyleSheet.create({
const WalletDetails = () => {
const { wallet } = useRoute().params;
const [isLoading, setIsLoading] = useState(true);
const [backdoorPressed, setBackdoorPressed] = useState(0);
const [walletName, setWalletName] = useState(wallet.getLabel());
const [useWithHardwareWallet, setUseWithHardwareWallet] = useState(wallet.useWithHardwareWalletEnabled());
const [hideTransactionsInWalletsList, setHideTransactionsInWalletsList] = useState(!wallet.getHideTransactionsInWalletsList());
@ -148,27 +154,29 @@ const WalletDetails = () => {
const presentWalletHasBalanceAlert = useCallback(async () => {
ReactNativeHapticFeedback.trigger('notificationWarning', { ignoreAndroidSystemSettings: false });
const walletBalanceConfirmation = await prompt(
loc.wallets.details_del_wb,
loc.formatString(loc.wallets.details_del_wb_q, { balance: wallet.getBalance() }),
true,
'plain-text',
);
if (Number(walletBalanceConfirmation) === wallet.getBalance()) {
setParams({ isLoading: true });
setIsLoading(true);
notifications.unsubscribe(wallet.getAllExternalAddresses(), [], []);
BlueApp.deleteWallet(wallet);
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
await BlueApp.saveToDisk();
EV(EV.enum.TRANSACTIONS_COUNT_CHANGED);
EV(EV.enum.WALLETS_COUNT_CHANGED);
popToTop();
} else {
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
setIsLoading(false);
alert(loc.wallets.details_del_wb_err);
}
try {
const walletBalanceConfirmation = await prompt(
loc.wallets.details_del_wb,
loc.formatString(loc.wallets.details_del_wb_q, { balance: wallet.getBalance() }),
true,
'plain-text',
);
if (Number(walletBalanceConfirmation) === wallet.getBalance()) {
setParams({ isLoading: true });
setIsLoading(true);
notifications.unsubscribe(wallet.getAllExternalAddresses(), [], []);
BlueApp.deleteWallet(wallet);
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
await BlueApp.saveToDisk();
EV(EV.enum.TRANSACTIONS_COUNT_CHANGED);
EV(EV.enum.WALLETS_COUNT_CHANGED);
popToTop();
} else {
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
setIsLoading(false);
alert(loc.wallets.details_del_wb_err);
}
} catch (_) {}
}, [popToTop, setParams, wallet]);
const navigateToWalletExport = () => {
@ -176,6 +184,11 @@ const WalletDetails = () => {
wallet,
});
};
const navigateToMultisigCoordinationSetup = () => {
navigate('ExportMultisigCoordinationSetup', {
walletId: wallet.getID(),
});
};
const navigateToXPub = () =>
navigate('WalletXpub', {
secret: wallet.getSecret(),
@ -204,6 +217,65 @@ const WalletDetails = () => {
});
};
const exportInternals = async () => {
if (backdoorPressed < 10) return setBackdoorPressed(backdoorPressed + 1);
setBackdoorPressed(0);
if (wallet.type !== HDSegwitBech32Wallet.type) return;
const fileName = 'wallet-externals.json';
const contents = JSON.stringify(
{
_balances_by_external_index: wallet._balances_by_external_index,
_balances_by_internal_index: wallet._balances_by_internal_index,
_txs_by_external_index: wallet._txs_by_external_index,
_txs_by_internal_index: wallet._txs_by_internal_index,
_utxo: wallet._utxo,
next_free_address_index: wallet.next_free_address_index,
next_free_change_address_index: wallet.next_free_change_address_index,
internal_addresses_cache: wallet.internal_addresses_cache,
external_addresses_cache: wallet.external_addresses_cache,
_xpub: wallet._xpub,
gap_limit: wallet.gap_limit,
label: wallet.label,
_lastTxFetch: wallet._lastTxFetch,
_lastBalanceFetch: wallet._lastBalanceFetch,
},
null,
2,
);
if (Platform.OS === 'ios') {
const filePath = RNFS.TemporaryDirectoryPath + `/${fileName}`;
await RNFS.writeFile(filePath, contents);
Share.open({
url: 'file://' + filePath,
saveToFiles: isDesktop,
})
.catch(error => {
console.log(error);
alert(error.message);
})
.finally(() => {
RNFS.unlink(filePath);
});
} else if (Platform.OS === 'android') {
const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, {
title: loc.send.permission_storage_title,
message: loc.send.permission_storage_message,
buttonNeutral: loc.send.permission_storage_later,
buttonNegative: loc._.cancel,
buttonPositive: loc._.ok,
});
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
console.log('Storage Permission: Granted');
const filePath = RNFS.DownloadDirectoryPath + `/${fileName}`;
await RNFS.writeFile(filePath, contents);
alert(loc.formatString(loc.send.txSaved, { filePath: fileName }));
} else {
console.log('Storage Permission: Denied');
}
}
};
const navigateToBroadcast = () => {
navigate('Broadcast');
};
@ -295,6 +367,35 @@ const WalletDetails = () => {
<BlueSpacing20 />
<Text style={[styles.textLabel1, stylesHook.textLabel1]}>{loc.wallets.details_type.toLowerCase()}</Text>
<Text style={[styles.textValue, stylesHook.textValue]}>{wallet.typeReadable}</Text>
{wallet.type === MultisigHDWallet.type && (
<>
<Text style={[styles.textLabel2, stylesHook.textLabel2]}>multisig</Text>
<BlueText>
{wallet.getM()} of {wallet.getN()}{' '}
{wallet.isNativeSegwit()
? 'native segwit (p2wsh)'
: wallet.isWrappedSegwit()
? 'wrapped segwit (p2sh-p2wsh)'
: 'legacy (p2sh)'}
</BlueText>
</>
)}
{wallet.type === MultisigHDWallet.type && wallet.howManySignaturesCanWeMake() > 0 && (
<>
<Text style={[styles.textLabel2, stylesHook.textLabel2]}>{loc.multisig.how_many_signatures_can_bluewallet_make}</Text>
<BlueText>{wallet.howManySignaturesCanWeMake()}</BlueText>
</>
)}
{wallet.type === MultisigHDWallet.type && !!wallet.getDerivationPath() && (
<>
<Text style={[styles.textLabel2, stylesHook.textLabel2]}>derivation path</Text>
<BlueText>{wallet.getDerivationPath()}</BlueText>
</>
)}
{wallet.type === LightningCustodianWallet.type && (
<>
<Text style={[styles.textLabel1, stylesHook.textLabel1]}>{loc.wallets.details_connected_to.toLowerCase()}</Text>
@ -302,7 +403,9 @@ const WalletDetails = () => {
</>
)}
<>
<Text style={[styles.textLabel2, stylesHook.textLabel2]}>{loc.transactions.list_title.toLowerCase()}</Text>
<Text onPress={exportInternals} style={[styles.textLabel2, stylesHook.textLabel2]}>
{loc.transactions.list_title.toLowerCase()}
</Text>
<View style={styles.hardware}>
<BlueText>{loc.wallets.details_display}</BlueText>
<Switch value={hideTransactionsInWalletsList} onValueChange={setHideTransactionsInWalletsList} />
@ -333,6 +436,15 @@ const WalletDetails = () => {
<BlueSpacing20 />
{wallet.type === MultisigHDWallet.type && (
<>
<SecondButton
onPress={navigateToMultisigCoordinationSetup}
title={loc.multisig.export_coordination_setup.replace(/^\w/, c => c.toUpperCase())}
/>
</>
)}
{(wallet.type === HDLegacyBreadwalletWallet.type ||
wallet.type === HDLegacyP2PKHWallet.type ||
wallet.type === HDSegwitBech32Wallet.type ||

View file

@ -1,7 +1,7 @@
import React, { useRef, useState, useEffect } from 'react';
import { StatusBar, View, TouchableOpacity, InteractionManager, StyleSheet, Alert, useWindowDimensions } from 'react-native';
import React, { useRef } from 'react';
import { StatusBar, View, TouchableOpacity, StyleSheet, Alert, useWindowDimensions } from 'react-native';
import { DrawerContentScrollView } from '@react-navigation/drawer';
import { WalletsCarousel, BlueNavigationStyle, BlueHeaderDefaultMainHooks } from '../../BlueComponents';
import { WalletsCarousel, BlueNavigationStyle, BlueHeaderDefaultMain } from '../../BlueComponents';
import { Icon } from 'react-native-elements';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import PropTypes from 'prop-types';
@ -14,11 +14,11 @@ import { useTheme, useRoute } from '@react-navigation/native';
import { SafeAreaView } from 'react-native-safe-area-context';
const EV = require('../../blue_modules/events');
const BlueApp: AppStorage = require('../../BlueApp');
const BlueElectrum = require('../../blue_modules/BlueElectrum');
const DrawerList = props => {
console.log('drawerList rendering...');
const walletsCarousel = useRef();
const [wallets, setWallets] = useState(BlueApp.getWallets().concat(false));
const wallets = useRoute().params?.wallets || BlueApp.getWallets() || [];
const height = useWindowDimensions().height;
const { colors } = useTheme();
const { selectedWallet } = useRoute().params || '';
@ -27,83 +27,6 @@ const DrawerList = props => {
backgroundColor: colors.brandingColor,
},
});
let lastSnappedTo = 0;
const refreshTransactions = () => {
InteractionManager.runAfterInteractions(async () => {
let noErr = true;
try {
// await BlueElectrum.ping();
await BlueElectrum.waitTillConnected();
const balanceStart = +new Date();
await BlueApp.fetchWalletBalances(lastSnappedTo || 0);
const balanceEnd = +new Date();
console.log('fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec');
const start = +new Date();
await BlueApp.fetchWalletTransactions(lastSnappedTo || 0);
const end = +new Date();
console.log('fetch tx took', (end - start) / 1000, 'sec');
} catch (err) {
noErr = false;
console.warn(err);
}
if (noErr) await BlueApp.saveToDisk(); // caching
redrawScreen();
});
};
useEffect(() => {
EV(EV.enum.TRANSACTIONS_COUNT_CHANGED);
console.log('drawerList wallets changed');
}, [wallets]);
const redrawScreen = (scrollToEnd = false) => {
console.log('drawerList redrawScreen()');
const newWallets = BlueApp.getWallets().concat(false);
if (scrollToEnd) {
scrollToEnd = newWallets.length > wallets.length;
}
setWallets(newWallets);
if (scrollToEnd) {
// eslint-disable-next-line no-unused-expressions
walletsCarousel.current?.snapToItem(wallets.length - 2);
}
};
useEffect(() => {
// here, when we receive REMOTE_TRANSACTIONS_COUNT_CHANGED we fetch TXs and balance for current wallet.
// placing event subscription here so it gets exclusively re-subscribed more often. otherwise we would
// have to unsubscribe on unmount and resubscribe again on mount.
EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED, refreshTransactions, true);
EV(EV.enum.WALLETS_COUNT_CHANGED, () => redrawScreen(true));
console.log('drawerList useEffect');
// the idea is that upon wallet launch we will refresh
// all balances and all transactions here:
redrawScreen();
InteractionManager.runAfterInteractions(async () => {
try {
await BlueElectrum.waitTillConnected();
const balanceStart = +new Date();
await BlueApp.fetchWalletBalances();
const balanceEnd = +new Date();
console.log('fetch all wallet balances took', (balanceEnd - balanceStart) / 1000, 'sec');
const start = +new Date();
await BlueApp.fetchWalletTransactions();
const end = +new Date();
console.log('fetch all wallet txs took', (end - start) / 1000, 'sec');
redrawScreen();
await BlueApp.saveToDisk();
} catch (error) {
console.log(error);
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleClick = index => {
console.log('click', index);
@ -156,82 +79,13 @@ const DrawerList = props => {
}
};
const onSnapToItem = index => {
console.log('onSnapToItem', index);
lastSnappedTo = index;
if (index < BlueApp.getWallets().length) {
// not the last
}
if (wallets[index].type === PlaceholderWallet.type) {
return;
}
// now, lets try to fetch balance and txs for this wallet in case it has changed
lazyRefreshWallet(index);
};
/**
* Decides whether wallet with such index shoud be refreshed,
* refreshes if yes and redraws the screen
* @param index {Integer} Index of the wallet.
* @return {Promise.<void>}
*/
const lazyRefreshWallet = async index => {
/** @type {Array.<AbstractWallet>} wallets */
const wallets = BlueApp.getWallets();
if (!wallets[index]) {
return;
}
const oldBalance = wallets[index].getBalance();
let noErr = true;
let didRefresh = false;
try {
if (wallets[index] && wallets[index].type !== PlaceholderWallet.type && wallets[index].timeToRefreshBalance()) {
console.log('snapped to, and now its time to refresh wallet #', index);
await wallets[index].fetchBalance();
if (oldBalance !== wallets[index].getBalance() || wallets[index].getUnconfirmedBalance() !== 0) {
console.log('balance changed, thus txs too');
// balance changed, thus txs too
await wallets[index].fetchTransactions();
redrawScreen();
didRefresh = true;
} else if (wallets[index].timeToRefreshTransaction()) {
console.log(wallets[index].getLabel(), 'thinks its time to refresh TXs');
await wallets[index].fetchTransactions();
if (wallets[index].fetchPendingTransactions) {
await wallets[index].fetchPendingTransactions();
}
if (wallets[index].fetchUserInvoices) {
await wallets[index].fetchUserInvoices();
await wallets[index].fetchBalance(); // chances are, paid ln invoice was processed during `fetchUserInvoices()` call and altered user's balance, so its worth fetching balance again
}
redrawScreen();
didRefresh = true;
} else {
console.log('balance not changed');
}
}
} catch (Err) {
noErr = false;
console.warn(Err);
}
if (noErr && didRefresh) {
await BlueApp.saveToDisk(); // caching
}
};
const renderWalletsCarousel = () => {
return (
<WalletsCarousel
removeClippedSubviews={false}
data={wallets}
data={wallets.concat(false)}
onPress={handleClick}
handleLongPress={handleLongPress}
onSnapToItem={onSnapToItem}
ref={walletsCarousel}
testID="WalletsList"
vertical
@ -244,19 +98,16 @@ const DrawerList = props => {
);
};
const onNewWalletPress = () => {
return !BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type) ? props.navigation.navigate('AddWalletRoot') : null;
};
return (
<DrawerContentScrollView {...props} scrollEnabled={false}>
<View styles={[styles.root, stylesHook.root]}>
<StatusBar barStyle="default" />
<SafeAreaView style={styles.root}>
<BlueHeaderDefaultMainHooks
leftText={loc.wallets.list_title}
onNewWalletPress={
!BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type)
? () => props.navigation.navigate('AddWalletRoot')
: null
}
/>
<BlueHeaderDefaultMain leftText={loc.wallets.list_title} onNewWalletPress={onNewWalletPress} isDrawerList />
</SafeAreaView>
{renderWalletsCarousel()}
</View>

View file

@ -0,0 +1,128 @@
import React, { useCallback, useState } from 'react';
import { ActivityIndicator, InteractionManager, ScrollView, StatusBar, StyleSheet, useWindowDimensions, View } from 'react-native';
import QRCode from 'react-native-qrcode-svg';
import { BlueNavigationStyle, BlueSpacing20, BlueText, SafeBlueArea } from '../../BlueComponents';
import Privacy from '../../Privacy';
import Biometric from '../../class/biometrics';
import loc from '../../loc';
import { encodeUR } from '../../blue_modules/bc-ur/dist';
import { useFocusEffect, useNavigation, useRoute, useTheme } from '@react-navigation/native';
import { SquareButton } from '../../components/SquareButton';
const BlueApp = require('../../BlueApp');
const fs = require('../../blue_modules/fs');
const ExportMultisigCoordinationSetup = () => {
const walletId = useRoute().params.walletId;
const wallet = BlueApp.getWallets().find(w => w.getID() === walletId);
const qrCodeContents = encodeUR(Buffer.from(wallet.getXpub(), 'ascii').toString('hex'), 77777)[0];
const [isLoading, setIsLoading] = useState(true);
const { goBack } = useNavigation();
const { colors } = useTheme();
const { width, height } = useWindowDimensions();
const stylesHook = {
...styles,
loading: {
...styles.loading,
backgroundColor: colors.elevated,
},
root: {
...styles.root,
backgroundColor: colors.elevated,
},
type: { ...styles.type, color: colors.foregroundColor },
secret: { ...styles.secret, color: colors.foregroundColor },
};
const exportTxtFile = async () => {
await fs.writeFileAndExport(wallet.getLabel() + '.txt', wallet.getXpub());
};
useFocusEffect(
useCallback(() => {
Privacy.enableBlur();
const task = InteractionManager.runAfterInteractions(async () => {
if (wallet) {
const isBiometricsEnabled = await Biometric.isBiometricUseCapableAndEnabled();
if (isBiometricsEnabled) {
if (!(await Biometric.unlockWithBiometrics())) {
return goBack();
}
}
setIsLoading(false);
}
});
return () => {
task.cancel();
Privacy.disableBlur();
};
}, [goBack, wallet]),
);
return isLoading ? (
<View style={stylesHook.loading}>
<ActivityIndicator />
</View>
) : (
<SafeBlueArea style={stylesHook.root}>
<StatusBar barStyle="light-content" />
<ScrollView contentContainerStyle={styles.scrollViewContent}>
<View>
<BlueText style={stylesHook.type}>{wallet.getLabel()}</BlueText>
</View>
<BlueSpacing20 />
<View style={styles.activeQrcode}>
<QRCode
value={qrCodeContents}
size={height > width ? width - 40 : width / 2}
logoSize={70}
color="#000000"
logoBackgroundColor={colors.brandingColor}
backgroundColor="#FFFFFF"
ecl="H"
/>
<BlueSpacing20 />
<SquareButton backgroundColor="#EEF0F4" onPress={exportTxtFile} title={loc.multisig.share} />
</View>
<BlueSpacing20 />
<BlueText style={stylesHook.secret}>{wallet.getXpub()}</BlueText>
</ScrollView>
</SafeBlueArea>
);
};
const styles = StyleSheet.create({
loading: {
flex: 1,
paddingTop: 20,
},
root: {
flex: 1,
},
scrollViewContent: {
alignItems: 'center',
justifyContent: 'center',
flexGrow: 1,
},
activeQrcode: { borderWidth: 6, borderRadius: 8, borderColor: '#FFFFFF' },
type: {
fontSize: 17,
fontWeight: '700',
},
secret: {
alignItems: 'center',
paddingHorizontal: 16,
fontSize: 16,
lineHeight: 24,
},
});
ExportMultisigCoordinationSetup.navigationOptions = ({ navigation }) => ({
...BlueNavigationStyle(navigation, true),
title: loc.multisig.export_coordination_setup,
headerLeft: null,
});
export default ExportMultisigCoordinationSetup;

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