diff --git a/Navigation.js b/Navigation.js
index 36feb30e1..fb9c2c24b 100644
--- a/Navigation.js
+++ b/Navigation.js
@@ -34,6 +34,7 @@ import WalletExport from './screen/wallets/export';
import ExportMultisigCoordinationSetup from './screen/wallets/exportMultisigCoordinationSetup';
import ViewEditMultisigCosigners from './screen/wallets/viewEditMultisigCosigners';
import WalletXpub from './screen/wallets/xpub';
+import SignVerify from './screen/wallets/signVerify';
import BuyBitcoin from './screen/wallets/buyBitcoin';
import HodlHodl from './screen/wallets/hodlHodl';
import HodlHodlViewOffer from './screen/wallets/hodlHodlViewOffer';
@@ -385,6 +386,17 @@ const WalletXpubStackRoot = () => {
);
};
+const SignVerifyStack = createStackNavigator();
+const SignVerifyStackRoot = () => {
+ const theme = useTheme();
+
+ return (
+
+
+
+ );
+};
+
const WalletExportStack = createStackNavigator();
const WalletExportStackRoot = () => {
const theme = useTheme();
@@ -485,6 +497,7 @@ const Navigation = () => {
/>
+
diff --git a/class/wallets/lightning-custodian-wallet.js b/class/wallets/lightning-custodian-wallet.js
index 6a7c026fe..1efffad3a 100644
--- a/class/wallets/lightning-custodian-wallet.js
+++ b/class/wallets/lightning-custodian-wallet.js
@@ -605,6 +605,10 @@ export class LightningCustodianWallet extends LegacyWallet {
return true;
}
+ allowSignVerifyMessage() {
+ return false;
+ }
+
/**
* Example return:
* { destination: '03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f',
diff --git a/class/wallets/watch-only-wallet.js b/class/wallets/watch-only-wallet.js
index 9cff25b13..1cff6710f 100644
--- a/class/wallets/watch-only-wallet.js
+++ b/class/wallets/watch-only-wallet.js
@@ -35,6 +35,10 @@ export class WatchOnlyWallet extends LegacyWallet {
);
}
+ allowSignVerifyMessage() {
+ return false;
+ }
+
getAddress() {
if (this.isAddressValid(this.secret)) return this.secret; // handling case when there is an XPUB there
if (this._hdWalletInstance) throw new Error('Should not be used in watch-only HD wallets');
diff --git a/components/FloatButtons.js b/components/FloatButtons.js
index 08810b1d8..8aaf0d03a 100644
--- a/components/FloatButtons.js
+++ b/components/FloatButtons.js
@@ -9,16 +9,20 @@ const ICON_MARGIN = 7;
const cStyles = StyleSheet.create({
root: {
- position: 'absolute',
alignSelf: 'center',
height: '6.3%',
minHeight: 44,
},
+ rootAbsolute: {
+ position: 'absolute',
+ bottom: 30,
+ },
+ rootInline: {},
rootPre: {
+ position: 'absolute',
bottom: -1000,
},
rootPost: {
- bottom: 30,
borderRadius: BORDER_RADIUS,
flexDirection: 'row',
overflow: 'hidden',
@@ -42,7 +46,11 @@ export const FContainer = forwardRef((props, ref) => {
};
return (
-
+
{newWidth
? React.Children.toArray(props.children)
.filter(Boolean)
@@ -61,6 +69,7 @@ export const FContainer = forwardRef((props, ref) => {
FContainer.propTypes = {
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.element), PropTypes.element]),
+ inline: PropTypes.bool,
};
const buttonFontSize =
@@ -95,6 +104,9 @@ export const FButton = ({ text, icon, width, first, last, ...props }) => {
text: {
color: colors.buttonAlternativeTextColor,
},
+ textDisabled: {
+ color: colors.formBorder,
+ },
});
const style = {};
@@ -109,7 +121,7 @@ export const FButton = ({ text, icon, width, first, last, ...props }) => {
return (
{icon}
-
+
{text}
@@ -122,4 +134,5 @@ FButton.propTypes = {
width: PropTypes.number,
first: PropTypes.bool,
last: PropTypes.bool,
+ disabled: PropTypes.bool,
};
diff --git a/loc/en.json b/loc/en.json
index d1ced1732..525d77f1d 100644
--- a/loc/en.json
+++ b/loc/en.json
@@ -14,6 +14,7 @@
"no": "No",
"save": "Save",
"seed": "Seed",
+ "success": "Success",
"wallet_key": "Wallet key",
"invalid_animated_qr_code_fragment" : "Invalid animated QRCode fragment. Please try again.",
"file_saved": "File ({filePath}) has been saved in your Downloads folder.",
@@ -561,5 +562,16 @@
"MAX": "Max",
"sat_byte": "sat/byte",
"sats": "sats"
+ },
+ "addresses": {
+ "sign_title": "Sign/Verify message",
+ "sign_help": "Here you can create or verify a cryptographic signature based on a Bitcoin address",
+ "sign_sign": "Sign",
+ "sign_verify": "Verify",
+ "sign_signature_correct": "Verification Succeeded!",
+ "sign_signature_incorrect": "Verification Failed!",
+ "sign_placeholder_address": "Address",
+ "sign_placeholder_message": "Message",
+ "sign_placeholder_signature": "Signature"
}
}
diff --git a/screen/wallets/details.js b/screen/wallets/details.js
index 6b8373ba6..a9d06914e 100644
--- a/screen/wallets/details.js
+++ b/screen/wallets/details.js
@@ -231,6 +231,14 @@ const WalletDetails = () => {
secret: wallet.getSecret(),
},
});
+ const navigateToSignVerify = () =>
+ navigate('SignVerifyRoot', {
+ screen: 'SignVerify',
+ params: {
+ walletID: wallet.getID(),
+ address: wallet.getAllExternalAddresses()[0], // works for both single address and HD wallets
+ },
+ });
const renderMarketplaceButton = () => {
return Platform.select({
@@ -552,6 +560,12 @@ const WalletDetails = () => {
>
+ {wallet.allowSignVerifyMessage() && (
+ <>
+
+
+ >
+ )}
diff --git a/screen/wallets/signVerify.js b/screen/wallets/signVerify.js
new file mode 100644
index 000000000..e86b8f885
--- /dev/null
+++ b/screen/wallets/signVerify.js
@@ -0,0 +1,232 @@
+import React, { useEffect, useState, useContext } from 'react';
+import {
+ ActivityIndicator,
+ Alert,
+ Keyboard,
+ KeyboardAvoidingView,
+ Platform,
+ StyleSheet,
+ TextInput,
+ TouchableWithoutFeedback,
+ View,
+} from 'react-native';
+import { useRoute, useTheme } from '@react-navigation/native';
+import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
+
+import { BlueDoneAndDismissKeyboardInputAccessory, BlueFormLabel, BlueSpacing10, BlueSpacing20, SafeBlueArea } from '../../BlueComponents';
+import navigationStyle from '../../components/navigationStyle';
+import { FContainer, FButton } from '../../components/FloatButtons';
+import { BlueStorageContext } from '../../blue_modules/storage-context';
+import loc from '../../loc';
+
+const SignVerify = () => {
+ const { colors } = useTheme();
+ const { wallets, sleep } = useContext(BlueStorageContext);
+ const { params } = useRoute();
+ const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
+ const [address, setAddress] = useState(params.address);
+ const [message, setMessage] = useState('');
+ const [signature, setSignature] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [messageHasFocus, setMessageHasFocus] = useState(false);
+
+ const wallet = wallets.find(w => w.getID() === params.walletID);
+ const isToolbarVisibleForAndroid = Platform.OS === 'android' && messageHasFocus && isKeyboardVisible;
+
+ useEffect(() => {
+ Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', () => setIsKeyboardVisible(true));
+ Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', () => setIsKeyboardVisible(false));
+ return () => {
+ Keyboard.removeListener(Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide');
+ Keyboard.removeListener(Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow');
+ };
+ }, []);
+
+ const stylesHooks = StyleSheet.create({
+ root: {
+ backgroundColor: colors.elevated,
+ },
+ text: {
+ borderColor: colors.formBorder,
+ borderBottomColor: colors.formBorder,
+ backgroundColor: colors.inputBackgroundColor,
+ color: colors.foregroundColor,
+ },
+ });
+
+ const handleSign = async () => {
+ setLoading(true);
+ await sleep(10); // wait for loading indicator to appear
+ try {
+ const newSignature = wallet.signMessage(message, address);
+ setSignature(newSignature);
+ } catch (e) {
+ ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
+ Alert.alert(loc.errors.error, e.message);
+ }
+ setLoading(false);
+ };
+
+ const handleVerify = async () => {
+ setLoading(true);
+ await sleep(10); // wait for loading indicator to appear
+ try {
+ const res = wallet.verifyMessage(message, address, signature);
+ Alert.alert(
+ res ? loc._.success : loc.errors.error,
+ res ? loc.addresses.sign_signature_correct : loc.addresses.sign_signature_incorrect,
+ );
+ if (res) {
+ ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
+ }
+ } catch (e) {
+ ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
+ Alert.alert(loc.errors.error, e.message);
+ }
+ setLoading(false);
+ };
+
+ if (loading)
+ return (
+
+
+
+ );
+
+ return (
+
+
+
+ {!isKeyboardVisible && (
+ <>
+
+ {loc.addresses.sign_help}
+
+ >
+ )}
+
+ setAddress(t.replace('\n', ''))}
+ testID="Signature"
+ style={[styles.text, stylesHooks.text]}
+ autoCorrect={false}
+ autoCapitalize="none"
+ spellCheck={false}
+ editable={!loading}
+ />
+
+
+ setSignature(t.replace('\n', ''))}
+ testID="Signature"
+ style={[styles.text, stylesHooks.text]}
+ autoCorrect={false}
+ autoCapitalize="none"
+ spellCheck={false}
+ editable={!loading}
+ />
+
+
+ setMessageHasFocus(true)}
+ onBlur={() => setMessageHasFocus(false)}
+ />
+
+
+ {!isKeyboardVisible && (
+ <>
+
+
+
+
+
+ >
+ )}
+
+ {Platform.select({
+ ios: (
+ setMessage('')}
+ onPasteTapped={text => {
+ setMessage(text);
+ Keyboard.dismiss();
+ }}
+ />
+ ),
+ android: isToolbarVisibleForAndroid && (
+ {
+ setMessage('');
+ Keyboard.dismiss();
+ }}
+ onPasteTapped={text => {
+ setMessage(text);
+ Keyboard.dismiss();
+ }}
+ />
+ ),
+ })}
+
+
+
+ );
+};
+
+SignVerify.navigationOptions = navigationStyle({}, opts => ({ ...opts, title: loc.addresses.sign_title }));
+
+export default SignVerify;
+
+const styles = StyleSheet.create({
+ root: {
+ flex: 1,
+ },
+ text: {
+ paddingHorizontal: 8,
+ paddingVertical: 8,
+ marginTop: 5,
+ marginHorizontal: 20,
+ borderWidth: 1,
+ borderBottomWidth: 0.5,
+ borderRadius: 4,
+ textAlignVertical: 'top',
+ },
+ textMessage: {
+ minHeight: 50,
+ },
+ flex: {
+ flex: 1,
+ },
+ loading: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+});