Files
app.zeusln.zeus/views/Tools/SignVerifyMessage.tsx
T

1136 lines
40 KiB
TypeScript

import * as React from 'react';
import {
StyleSheet,
Text,
View,
ScrollView,
Alert,
TouchableOpacity
} from 'react-native';
import { ButtonGroup } from '@rneui/themed';
import { inject, observer } from 'mobx-react';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { RouteProp } from '@react-navigation/native';
import Button from '../../components/Button';
import CopyButton from '../../components/CopyButton';
import CopyBox from '../../components/CopyBox';
import Header from '../../components/Header';
import LoadingIndicator from '../../components/LoadingIndicator';
import Screen from '../../components/Screen';
import {
SuccessMessage,
ErrorMessage
} from '../../components/SuccessErrorMessage';
import TextInput from '../../components/TextInput';
import { themeColor } from '../../utils/ThemeUtils';
import { localeString } from '../../utils/LocaleUtils';
import BackendUtils from '../../utils/BackendUtils';
import MessageSignStore from '../../stores/MessageSignStore';
interface SignVerifyMessageProps {
navigation: NativeStackNavigationProp<any, any>;
route: RouteProp<any, any>;
MessageSignStore: MessageSignStore;
}
interface SignVerifyMessageState {
messageToSign: string;
messageToVerify: string;
signatureToVerify: string;
pubkeyToVerify: string;
selectedIndex: number;
signingMethodIndex: number;
selectedAddress: string;
signingMode: string;
supportsAddressMessageSigning: boolean;
supportsMessageVerification: boolean;
requiresVerifyPubkey: boolean;
loading: boolean;
}
@inject('SettingsStore', 'MessageSignStore')
@observer
export default class SignVerifyMessage extends React.Component<
SignVerifyMessageProps,
SignVerifyMessageState
> {
navigationFocusListener: any;
state = {
messageToSign: '',
messageToVerify: '',
signatureToVerify: '',
pubkeyToVerify: '',
selectedIndex: 0,
signingMethodIndex: 0,
selectedAddress: '',
signingMode: 'lightning',
supportsAddressMessageSigning: false,
supportsMessageVerification: true,
requiresVerifyPubkey: false,
loading: false
};
componentDidMount = () => {
const { MessageSignStore, navigation, route } = this.props;
MessageSignStore.reset();
// Load addresses right away and also when the screen comes into focus
if (BackendUtils.supportsAddressMessageSigning()) {
this.setState({ supportsAddressMessageSigning: true });
MessageSignStore.loadAddresses();
// Also reload addresses whenever the screen comes into focus
this.navigationFocusListener = navigation.addListener(
'focus',
() => {
MessageSignStore.loadAddresses();
this.checkForAddressSelection();
}
);
}
if (!BackendUtils.supportsMessageVerification()) {
this.setState({ supportsMessageVerification: false });
}
if (BackendUtils.requiresVerifyPubkey()) {
this.setState({ requiresVerifyPubkey: true });
}
this.handleRouteParams(route?.params || {});
};
componentWillUnmount() {
if (this.navigationFocusListener) {
this.navigationFocusListener();
}
}
checkForAddressSelection = () => {
const { route } = this.props;
const params = route?.params || {};
this.handleRouteParams(params);
};
handleRouteParams = (params: any) => {
const { MessageSignStore } = this.props;
if (params.selectedIndex !== undefined) {
this.setState({ selectedIndex: params.selectedIndex });
}
if (params.message) {
this.setState({
messageToVerify: params.message,
selectedIndex: 1
});
}
if (params.address) {
this.setState({
selectedAddress: params.address
});
}
if (params.preserveMode === 'onchain') {
this.setState({ signingMode: 'onchain' });
MessageSignStore.setSigningMode('onchain');
}
if (params.selectedAddress) {
const selectedAddress = params.selectedAddress;
const mode = params.mode;
if (mode === 'sign') {
this.setState({
selectedAddress,
signingMethodIndex: 1
});
MessageSignStore.setSigningMode('onchain');
MessageSignStore.setSelectedAddress(selectedAddress);
} else if (mode === 'verify') {
this.setState({
selectedAddress,
signingMode: 'onchain'
});
MessageSignStore.setSigningMode('onchain');
}
}
if (params?.signingMode) {
this.setState({ signingMode: params.signingMode });
MessageSignStore.setSigningMode(params.signingMode);
}
if (params?.messageToSign !== undefined) {
this.setState({ messageToSign: params.messageToSign });
}
if (params?.messageToVerify !== undefined) {
this.setState({ messageToVerify: params.messageToVerify });
}
if (params?.signatureToVerify !== undefined) {
this.setState({ signatureToVerify: params.signatureToVerify });
}
if (
params?.selectedAddress ||
params?.selectedIndex !== undefined ||
params?.signingMode ||
params?.preserveMode ||
params?.messageToSign !== undefined ||
params?.messageToVerify !== undefined ||
params?.signatureToVerify !== undefined
) {
this.props.navigation.setParams({
selectedAddress: undefined,
mode: undefined,
selectedIndex: undefined,
signingMode: undefined,
preserveMode: undefined,
messageToSign: undefined,
messageToVerify: undefined,
signatureToVerify: undefined,
timestamp: undefined
});
}
};
componentDidUpdate(prevProps: SignVerifyMessageProps) {
const { addresses, selectedAddress } = this.props.MessageSignStore;
if (
addresses &&
addresses.length > 0 &&
(!prevProps.MessageSignStore.addresses ||
prevProps.MessageSignStore.addresses.length === 0)
) {
if (!this.state.selectedAddress) {
this.setState({ selectedAddress: addresses[0].address });
}
}
if (
selectedAddress !== prevProps.MessageSignStore.selectedAddress &&
!this.state.selectedAddress
) {
this.setState({ selectedAddress });
}
}
reset = () => {
const { MessageSignStore } = this.props;
const { signingMode } = this.state;
// Clear form fields but preserve the signing mode
this.setState({
messageToSign: '',
messageToVerify: '',
signatureToVerify: '',
pubkeyToVerify: '',
selectedAddress: ''
});
// Reset the store but preserve the signing mode and address for onchain
MessageSignStore.reset();
MessageSignStore.setSigningMode(signingMode as 'lightning' | 'onchain');
};
updateIndex = (selectedIndex: number) => {
const { signingMode } = this.state;
const { MessageSignStore } = this.props;
this.reset();
this.setState({
selectedIndex
});
MessageSignStore.reset();
MessageSignStore.setSigningMode(signingMode as 'lightning' | 'onchain');
MessageSignStore.setSelectedAddress('');
};
updateSigningMethod = (signingMethodIndex: number) => {
const { MessageSignStore } = this.props;
const signingMode = signingMethodIndex === 0 ? 'lightning' : 'onchain';
// Clear message field and set the appropriate signing mode
this.reset();
this.setState({
signingMethodIndex,
signingMode
});
// Update the store with the new signing mode
MessageSignStore.setSigningMode(signingMode);
// Clear the selected address in the store only when switching to lightning mode
MessageSignStore.setSelectedAddress('');
};
setSelectedAddress = (mode: 'sign' | 'verify', address: string) => {
const { MessageSignStore } = this.props;
this.setState({
selectedAddress: address
});
MessageSignStore.setSigningMode('onchain');
MessageSignStore.setSelectedAddress(address);
if (mode === 'sign') {
this.setState({
signingMethodIndex: 1
});
}
};
navigateToAddressPicker = (mode: 'sign' | 'verify') => {
const { navigation } = this.props;
const { selectedAddress } = this.state;
// callback function to handle address selection
const handleAddressSelected = (address: string) => {
this.setSelectedAddress(mode, address);
};
navigation.navigate('OnChainAddresses', {
selectionMode: true,
selectedAddress,
onAddressSelected: handleAddressSelected,
headerTitle: localeString(
'views.Settings.SignMessage.selectAddress'
)
});
};
clearSelectedAddress = () => {
const { MessageSignStore } = this.props;
this.setState({ selectedAddress: '' });
MessageSignStore.setSelectedAddress('');
};
renderAddressSelector = (mode: 'sign' | 'verify') => {
const { MessageSignStore } = this.props;
const { addresses } = MessageSignStore;
const { selectedAddress, loading } = this.state;
if (mode === 'verify') {
return (
<View style={styles.addressSelector}>
<Text
style={{
...styles.text,
color: themeColor('secondaryText')
}}
>
{localeString(
'views.Settings.SignMessage.selectAddressVerification'
)}
</Text>
<TextInput
value={selectedAddress}
onChangeText={(text: string) => {
this.setState({ selectedAddress: text });
MessageSignStore.setSelectedAddress(text);
if (MessageSignStore.error) {
MessageSignStore.reset();
MessageSignStore.setSigningMode(
this.state.signingMode as
| 'lightning'
| 'onchain'
);
MessageSignStore.setSelectedAddress(text);
}
}}
locked={loading}
/>
</View>
);
}
// For signing, use the address picker since you can only sign with your own addresses
const currentAddressDetails = addresses.find(
(addr) => addr.address === selectedAddress
);
const displayLabel = selectedAddress;
return (
<View style={styles.addressSelector}>
<Text
style={{
...styles.text,
color: themeColor('secondaryText')
}}
>
{localeString(
'views.Settings.SignMessage.selectAddressSigning'
)}
</Text>
<TouchableOpacity
style={[
styles.addressButton,
{
backgroundColor: themeColor('secondary'),
...(selectedAddress
? {
borderColor: themeColor('highlight'),
borderWidth: 1
}
: {
borderColor: themeColor('secondaryText'),
borderWidth: 1,
borderStyle: 'dashed'
})
}
]}
onPress={() => this.navigateToAddressPicker(mode)}
>
{!selectedAddress && (
<View
style={{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Text
style={{
color: themeColor('secondaryText'),
textAlign: 'center',
marginRight: 5
}}
>
{localeString(
'views.Settings.SignMessage.tapToSelectAddress'
)}
</Text>
</View>
)}
{selectedAddress && (
<Text
style={{
color: themeColor('highlight'),
textAlign: 'center'
}}
>
{displayLabel}
</Text>
)}
</TouchableOpacity>
{selectedAddress && (
<View
style={[
styles.addressInfoBox,
{
backgroundColor: themeColor('secondary'),
borderColor: themeColor('secondary')
}
]}
>
<Text
style={{
color: themeColor('secondaryText'),
fontSize: 12
}}
>
{localeString(
'views.Settings.SignMessage.fullAddress'
)}
:
</Text>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginTop: 3
}}
>
<Text
style={{
color: themeColor('text'),
fontSize: 12,
flex: 1
}}
>
{selectedAddress}
</Text>
<CopyButton
copyValue={selectedAddress}
iconOnly={true}
iconSize={16}
/>
</View>
{/* Show account info if available */}
{currentAddressDetails?.accountName && (
<>
<Text
style={{
color: themeColor('secondaryText'),
fontSize: 12,
marginTop: 10
}}
>
{localeString('general.accountName')}:
</Text>
<Text
style={{
color: themeColor('text'),
fontSize: 12,
marginTop: 3
}}
>
{currentAddressDetails.accountName}
</Text>
</>
)}
</View>
)}
</View>
);
};
renderSigningMethodSelector = () => {
const { signingMethodIndex } = this.state;
const { supportsAddressMessageSigning } = this.state;
if (!supportsAddressMessageSigning) {
return null;
}
const lightningButton = () => (
<React.Fragment>
<Text
style={{
color:
signingMethodIndex === 0
? themeColor('background')
: themeColor('text')
}}
>
{localeString('general.defaultNodeNickname')}
</Text>
</React.Fragment>
);
const onchainButton = () => (
<React.Fragment>
<Text
style={{
color:
signingMethodIndex === 1
? themeColor('background')
: themeColor('text')
}}
>
{localeString('general.onchain')}
</Text>
</React.Fragment>
);
const buttons = [
{ element: lightningButton },
{ element: onchainButton }
];
const buttonElements = buttons.map((btn) => btn.element());
return (
<View style={styles.form}>
<ButtonGroup
onPress={this.updateSigningMethod}
selectedIndex={signingMethodIndex}
buttons={buttonElements}
selectedButtonStyle={{
backgroundColor: themeColor('highlight'),
borderRadius: 12
}}
containerStyle={{
backgroundColor: themeColor('secondary'),
borderRadius: 12,
borderColor: themeColor('secondary'),
marginBottom: 10
}}
innerBorderStyle={{
color: themeColor('secondary')
}}
/>
</View>
);
};
handleTextInputChange = (
field: keyof SignVerifyMessageState,
value: string
) => {
this.setState({ [field]: value } as any);
if (this.props.MessageSignStore.error) {
this.props.MessageSignStore.reset();
this.props.MessageSignStore.setSigningMode(
this.state.signingMode as 'lightning' | 'onchain'
);
}
};
renderErrorMessage = (customCondition: boolean = true) => {
const { MessageSignStore } = this.props;
const { error, errorMessage } = MessageSignStore;
if (error && customCondition) {
return (
<View style={styles.result}>
<ErrorMessage
message={
errorMessage ||
localeString('views.Settings.SignMessage.error')
}
/>
</View>
);
}
return null;
};
renderVerificationResult = () => {
const { MessageSignStore } = this.props;
const { valid, error, errorMessage, pubkey } = MessageSignStore;
if (valid !== null || error) {
return (
<View style={styles.result}>
{valid ? (
<>
<SuccessMessage
message={localeString(
'views.Settings.SignMessage.success'
)}
/>
{pubkey && !this.state.requiresVerifyPubkey && (
<View style={styles.copyBoxContainer}>
<CopyBox
heading={localeString(
'views.NodeInfo.pubkey'
)}
headingCopied={localeString(
'components.CopyButton.itemCopied',
{
item: localeString(
'views.NodeInfo.pubkey'
)
}
)}
theme="dark"
URL={pubkey}
/>
</View>
)}
</>
) : (
<ErrorMessage
message={
error && errorMessage
? errorMessage
: localeString(
'views.Settings.SignMessage.error'
)
}
/>
)}
</View>
);
}
return null;
};
validateSigningInput = () => {
const { messageToSign } = this.state;
if (
this.state.signingMode === 'onchain' &&
!this.state.selectedAddress
) {
Alert.alert(
localeString('views.Settings.SignMessage.pleaseSelectAddress')
);
return false;
}
if (!messageToSign || messageToSign.trim() === '') {
Alert.alert(
localeString('views.Settings.SignMessage.pleaseEnterMessage')
);
return false;
}
return true;
};
validateVerificationInput = () => {
const {
messageToVerify,
signatureToVerify,
pubkeyToVerify,
selectedAddress,
signingMode,
requiresVerifyPubkey,
supportsAddressMessageSigning
} = this.state;
if (!messageToVerify || messageToVerify.trim() === '') {
Alert.alert(
localeString(
'views.Settings.SignMessage.pleaseEnterMessageVerify'
)
);
return false;
}
if (!signatureToVerify || signatureToVerify.trim() === '') {
Alert.alert(
localeString('views.Settings.SignMessage.pleaseEnterSignature')
);
return false;
}
if (
requiresVerifyPubkey &&
(!pubkeyToVerify || pubkeyToVerify.trim() === '')
) {
Alert.alert(
localeString('views.Settings.SignMessage.pleaseEnterPubkey')
);
return false;
}
if (
supportsAddressMessageSigning &&
signingMode === 'onchain' &&
(!selectedAddress || selectedAddress.trim() === '')
) {
Alert.alert(
localeString('views.Settings.SignMessage.pleaseSelectAddress')
);
return false;
}
return true;
};
render() {
const { navigation, MessageSignStore } = this.props;
const {
messageToSign,
messageToVerify,
signatureToVerify,
selectedIndex,
signingMode,
loading
} = this.state;
const { signMessage, signature } = MessageSignStore;
const { supportsAddressMessageSigning, supportsMessageVerification } =
this.state;
const signButton = () => (
<React.Fragment>
<Text
style={{
color:
selectedIndex === 1
? themeColor('text')
: themeColor('background')
}}
>
{localeString('views.Settings.SignMessage.sign')}
</Text>
</React.Fragment>
);
const verifyButton = () => (
<React.Fragment>
<Text
style={{
color:
selectedIndex === 0
? themeColor('text')
: themeColor('background')
}}
>
{localeString('views.Settings.SignMessage.verify')}
</Text>
</React.Fragment>
);
const buttons = [{ element: signButton }, { element: verifyButton }];
const buttonElements = buttons.map((btn) => btn.element());
return (
<Screen>
<Header
leftComponent="Back"
centerComponent={{
text: localeString('views.Settings.SignMessage.title'),
style: {
color: themeColor('text'),
fontFamily: 'PPNeueMontreal-Book'
}
}}
rightComponent={
loading ? (
<View
style={{
width: 24,
height: 24,
marginRight: 10
}}
>
<LoadingIndicator size={24} />
</View>
) : undefined
}
navigation={navigation}
/>
<ScrollView
style={styles.content}
keyboardShouldPersistTaps="handled"
>
{supportsMessageVerification && (
<ButtonGroup
onPress={this.updateIndex}
selectedIndex={selectedIndex}
buttons={buttonElements}
selectedButtonStyle={{
backgroundColor: themeColor('text'),
borderRadius: 12
}}
containerStyle={{
backgroundColor: themeColor('secondary'),
borderRadius: 12,
borderColor: themeColor('secondary')
}}
innerBorderStyle={{
color: themeColor('secondary')
}}
/>
)}
{supportsAddressMessageSigning && (
<Text
style={{
...styles.text,
color: themeColor('secondaryText'),
paddingTop: 20
}}
>
{selectedIndex === 0
? localeString(
'views.Settings.SignMessage.signingMethod'
)
: localeString(
'views.Settings.SignMessage.signatureType'
)}
</Text>
)}
<View style={styles.form}>
{supportsAddressMessageSigning ? (
<View style={{ marginBottom: 15 }}>
{this.renderSigningMethodSelector()}
</View>
) : null}
{supportsAddressMessageSigning &&
signingMode === 'onchain' &&
(selectedIndex === 1
? this.renderAddressSelector('verify')
: this.renderAddressSelector('sign'))}
</View>
{selectedIndex === 0 && (
<View>
<View style={styles.form}>
<Text
style={{
...styles.text,
color: themeColor('secondaryText')
}}
>
{localeString(
'views.Settings.SignMessage.messageToSign'
)}
</Text>
<TextInput
placeholder={localeString(
'views.Settings.SignMessage.placeHolder'
)}
value={messageToSign}
onChangeText={(text: string) => {
this.handleTextInputChange(
'messageToSign',
text
);
}}
autoCorrect={false}
autoCapitalize="none"
multiline
style={{
height: 100
}}
locked={loading}
/>
</View>
<View style={styles.button}>
<Button
title={localeString(
'views.Settings.signMessage.button'
)}
icon={{
name: 'create'
}}
onPress={() => {
if (this.validateSigningInput()) {
signMessage(messageToSign.trim());
}
}}
/>
</View>
{this.renderErrorMessage(selectedIndex === 0)}
{signature && (
<View>
<View style={styles.copyBoxContainer}>
<CopyBox
heading={localeString(
'general.signature'
)}
headingCopied={localeString(
'components.CopyButton.itemCopied',
{
item: localeString(
'general.signature'
)
}
)}
theme="dark"
URL={signature}
/>
</View>
</View>
)}
</View>
)}
{selectedIndex === 1 && (
<View>
<View style={styles.form}>
<Text
style={{
...styles.text,
color: themeColor('secondaryText')
}}
>
{localeString(
'views.Settings.SignMessage.messageToVerify'
)}
</Text>
<TextInput
placeholder={localeString(
'views.Settings.SignMessage.placeHolder'
)}
value={messageToVerify}
onChangeText={(text: string) => {
this.handleTextInputChange(
'messageToVerify',
text
);
}}
locked={loading}
autoCorrect={false}
autoCapitalize="none"
multiline
style={{
height: 100
}}
/>
</View>
<View style={styles.form}>
<Text
style={{
...styles.text,
color: themeColor('secondaryText')
}}
>
{localeString(
'views.Settings.SignMessage.signatureToVerify'
)}
</Text>
<TextInput
value={signatureToVerify}
onChangeText={(text: string) => {
this.handleTextInputChange(
'signatureToVerify',
text
);
}}
autoCorrect={false}
autoCapitalize="none"
multiline
style={{
height: 100
}}
locked={loading}
/>
</View>
{this.state.requiresVerifyPubkey && (
<View style={styles.form}>
<Text
style={{
...styles.text,
color: themeColor('secondaryText')
}}
>
{localeString(
'views.Settings.SignMessage.pubkeyToVerify'
)}
</Text>
<TextInput
value={this.state.pubkeyToVerify}
onChangeText={(text: string) => {
this.handleTextInputChange(
'pubkeyToVerify',
text
);
}}
autoCorrect={false}
autoCapitalize="none"
locked={loading}
/>
</View>
)}
{this.renderVerificationResult()}
<View style={styles.button}>
<Button
title={localeString(
'views.Settings.signMessage.buttonVerify'
)}
icon={{
name: 'check'
}}
onPress={this.verifyMessage}
/>
</View>
</View>
)}
<View style={[styles.button, { paddingBottom: 50 }]}>
<Button
title={localeString(
'views.Settings.SignMessage.clear'
)}
icon={{
name: 'clear'
}}
onPress={() => this.reset()}
tertiary
/>
</View>
</ScrollView>
</Screen>
);
}
verifyMessage = async () => {
const { MessageSignStore } = this.props;
const {
messageToVerify,
signatureToVerify,
pubkeyToVerify,
selectedAddress,
signingMode
} = this.state;
if (!this.validateVerificationInput()) {
return;
}
const { supportsAddressMessageSigning } = this.state;
const verifyRequest = {
msg: messageToVerify.trim(),
signature: signatureToVerify.trim(),
pubkey: pubkeyToVerify.trim() || undefined,
addr:
supportsAddressMessageSigning && signingMode === 'onchain'
? selectedAddress
: undefined
};
MessageSignStore.verifyMessage(verifyRequest);
};
}
const styles = StyleSheet.create({
content: {
padding: 20
},
text: {
fontFamily: 'PPNeueMontreal-Book'
},
form: {
paddingTop: 5
},
textInput: {
marginTop: 5,
borderWidth: 1,
borderRadius: 6,
padding: 10,
minHeight: 100,
fontFamily: 'PPNeueMontreal-Book'
},
button: {
paddingTop: 20,
paddingBottom: 10
},
result: {
paddingTop: 20
},
addressInfoBox: {
padding: 10,
borderRadius: 6,
borderWidth: 1,
marginTop: 10
},
addressSelector: {
marginBottom: 10,
marginTop: 6
},
addressButton: {
padding: 10,
borderRadius: 6,
marginTop: 10
},
copyBoxContainer: {
marginTop: 15
}
});