3332 lines
151 KiB
TypeScript
3332 lines
151 KiB
TypeScript
import * as React from 'react';
|
|
import {
|
|
Alert,
|
|
KeyboardAvoidingView,
|
|
Modal,
|
|
StyleSheet,
|
|
Text,
|
|
View,
|
|
Image,
|
|
ScrollView,
|
|
TouchableOpacity,
|
|
Platform
|
|
} from 'react-native';
|
|
import Clipboard from '@react-native-clipboard/clipboard';
|
|
import { inject, observer } from 'mobx-react';
|
|
import differenceBy from 'lodash/differenceBy';
|
|
import { Route } from '@react-navigation/native';
|
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
|
|
import { hash, LNC_STORAGE_KEY } from '../../backends/LNC/credentialStore';
|
|
|
|
import AddressUtils, { CUSTODIAL_LNDHUBS } from '../../utils/AddressUtils';
|
|
import ConnectionFormatUtils from '../../utils/ConnectionFormatUtils';
|
|
import { localeString } from '../../utils/LocaleUtils';
|
|
import BackendUtils from '../../utils/BackendUtils';
|
|
import { themeColor } from '../../utils/ThemeUtils';
|
|
import ValidationUtils from '../../utils/ValidationUtils';
|
|
import { confirmAction } from '../../utils/ActionUtils';
|
|
|
|
import Storage from '../../storage';
|
|
|
|
import Button from '../../components/Button';
|
|
import CollapsedQR from '../../components/CollapsedQR';
|
|
import DropdownSetting from '../../components/DropdownSetting';
|
|
import Header from '../../components/Header';
|
|
import KeyValue from '../../components/KeyValue';
|
|
import LoadingIndicator from '../../components/LoadingIndicator';
|
|
import Pill from '../../components/Pill';
|
|
import Screen from '../../components/Screen';
|
|
import {
|
|
SuccessMessage,
|
|
ErrorMessage,
|
|
WarningMessage
|
|
} from '../../components/SuccessErrorMessage';
|
|
import Switch from '../../components/Switch';
|
|
import TextInput from '../../components/TextInput';
|
|
import Accordion from '../../components/Accordion';
|
|
import VssServerPicker, {
|
|
resolveVssServer,
|
|
isVssServerValid
|
|
} from '../../components/VssServerPicker';
|
|
import { Row } from '../../components/layout/Row';
|
|
import ShowHideToggle from '../../components/ShowHideToggle';
|
|
|
|
import BalanceStore from '../../stores/BalanceStore';
|
|
import ChannelsStore from '../../stores/ChannelsStore';
|
|
import SyncStore from '../../stores/SyncStore';
|
|
import SettingsStore, {
|
|
INTERFACE_KEYS,
|
|
LNC_MAILBOX_KEYS,
|
|
EMBEDDED_NODE_NETWORK_KEYS,
|
|
Settings,
|
|
Node,
|
|
Implementations,
|
|
getLspConfigForNetwork
|
|
} from '../../stores/SettingsStore';
|
|
|
|
import Scan from '../../assets/images/SVG/Scan.svg';
|
|
import AddIcon from '../../assets/images/SVG/Add.svg';
|
|
|
|
import { getPhoto } from '../../utils/PhotoUtils';
|
|
import {
|
|
optimizeNeutrinoPeers,
|
|
createLndWallet,
|
|
deleteLndWallet,
|
|
stopLnd
|
|
} from '../../utils/LndMobileUtils';
|
|
import {
|
|
createLdkNodeWallet,
|
|
stopLdkNode,
|
|
deleteLdkNodeWallet,
|
|
getDefaultEsploraServer,
|
|
getDefaultRgsServer,
|
|
DEFAULT_VSS_SERVER,
|
|
DEFAULT_SCORER_URL,
|
|
SupportedNetwork
|
|
} from '../../utils/LdkNodeUtils';
|
|
|
|
interface WalletConfigurationProps {
|
|
navigation: NativeStackNavigationProp<any, any>;
|
|
BalanceStore: BalanceStore;
|
|
ChannelsStore: ChannelsStore;
|
|
SettingsStore: SettingsStore;
|
|
SyncStore: SyncStore;
|
|
route: Route<
|
|
'WalletConfiguration',
|
|
{
|
|
node: Node;
|
|
index: any;
|
|
active: boolean;
|
|
tor: boolean;
|
|
saved: boolean;
|
|
newEntry: boolean;
|
|
photo: string;
|
|
}
|
|
>;
|
|
}
|
|
|
|
interface WalletConfigurationState {
|
|
node: Node | null;
|
|
nickname: string; //
|
|
host: string; // lnd
|
|
port: string; // lnd
|
|
macaroonHex: string; // lnd
|
|
rune: string; // CLN-rest
|
|
lndhubUrl: string; // lndhub
|
|
username: string | undefined; // lndhub
|
|
password: string | undefined; // lndhub
|
|
hidden: boolean;
|
|
existingAccount: boolean; // lndhub
|
|
dismissCustodialWarning: boolean;
|
|
implementation: Implementations;
|
|
certVerification: boolean;
|
|
saved: boolean;
|
|
active: boolean;
|
|
index: number | null;
|
|
newEntry: boolean;
|
|
suggestImport: string;
|
|
importError: string;
|
|
showLndHubModal: boolean;
|
|
showCertModal: boolean;
|
|
enableTor: boolean;
|
|
interfaceKeys: Array<any>;
|
|
photo?: string;
|
|
// lnc
|
|
pairingPhrase: string;
|
|
mailboxServer: string;
|
|
customMailboxServer?: string;
|
|
localKey?: string;
|
|
remoteKey?: string;
|
|
// embeded lnd
|
|
seedPhrase?: string[];
|
|
walletPassword?: string;
|
|
adminMacaroon?: string;
|
|
embeddedLndNetwork?: string;
|
|
lndDir?: string;
|
|
isSqlite?: boolean;
|
|
recoveryCipherSeed?: string;
|
|
channelBackupsBase64?: string;
|
|
creatingWallet: boolean;
|
|
errorCreatingWallet: boolean;
|
|
// embedded ldk node
|
|
ldkMnemonic?: string;
|
|
ldkPassphrase?: string;
|
|
ldkNodeDir?: string;
|
|
ldkNetwork?: string;
|
|
ldkSeedWordCount: 12 | 24;
|
|
ldkEsploraServer?: string;
|
|
ldkRgsServer?: string;
|
|
ldkScorerUrl?: string;
|
|
ldkVssServer?: string;
|
|
ldkVssServerSelected: string;
|
|
ldkVssServerCustom: string;
|
|
ldkNodeInitialized?: boolean;
|
|
// NWC
|
|
nostrWalletConnectUrl: string;
|
|
deletingWallet: boolean;
|
|
// Errors
|
|
lndhubUrlError: boolean;
|
|
usernameError: boolean;
|
|
hostError: boolean;
|
|
runeError: boolean;
|
|
macaroonHexError: boolean;
|
|
portError: boolean;
|
|
customMailboxServerError: boolean;
|
|
pairingPhraseError: boolean;
|
|
nostrWalletConnectUrlError: boolean;
|
|
}
|
|
|
|
const ScanBadge = ({ onPress }: { onPress: () => void }) => (
|
|
<TouchableOpacity onPress={onPress}>
|
|
<Scan fill={themeColor('text')} width={30} height={30} />
|
|
</TouchableOpacity>
|
|
);
|
|
|
|
@inject('BalanceStore', 'ChannelsStore', 'SettingsStore', 'SyncStore')
|
|
@observer
|
|
export default class WalletConfiguration extends React.Component<
|
|
WalletConfigurationProps,
|
|
WalletConfigurationState
|
|
> {
|
|
state: WalletConfigurationState = {
|
|
node: null,
|
|
nickname: '',
|
|
dismissCustodialWarning: false,
|
|
host: '',
|
|
port: '',
|
|
macaroonHex: '',
|
|
rune: '',
|
|
saved: false,
|
|
index: null as number | null,
|
|
active: false,
|
|
newEntry: false,
|
|
implementation: 'ldk-node',
|
|
certVerification: false,
|
|
enableTor: false,
|
|
existingAccount: false,
|
|
suggestImport: '',
|
|
importError: '',
|
|
lndhubUrl: '',
|
|
showLndHubModal: false,
|
|
showCertModal: false,
|
|
username: '',
|
|
password: '',
|
|
hidden: true,
|
|
photo: undefined,
|
|
// lnc
|
|
pairingPhrase: '',
|
|
mailboxServer: 'mailbox.terminal.lightning.today:443',
|
|
customMailboxServer: '',
|
|
localKey: '',
|
|
remoteKey: '',
|
|
// embedded lnd
|
|
seedPhrase: [],
|
|
walletPassword: '',
|
|
adminMacaroon: '',
|
|
embeddedLndNetwork: 'mainnet',
|
|
lndDir: '',
|
|
isSqlite: false,
|
|
interfaceKeys: INTERFACE_KEYS,
|
|
recoveryCipherSeed: '',
|
|
channelBackupsBase64: '',
|
|
creatingWallet: false,
|
|
errorCreatingWallet: false,
|
|
deletingWallet: false,
|
|
// embedded ldk node
|
|
ldkMnemonic: '',
|
|
ldkPassphrase: '',
|
|
ldkNodeDir: '',
|
|
ldkNetwork: 'mainnet',
|
|
ldkSeedWordCount: 12,
|
|
ldkEsploraServer: '',
|
|
ldkRgsServer: '',
|
|
ldkScorerUrl: '',
|
|
ldkVssServer: DEFAULT_VSS_SERVER,
|
|
ldkVssServerSelected: DEFAULT_VSS_SERVER,
|
|
ldkVssServerCustom: '',
|
|
ldkNodeInitialized: false,
|
|
// NWC
|
|
nostrWalletConnectUrl: '',
|
|
// Errors
|
|
lndhubUrlError: false,
|
|
usernameError: false,
|
|
hostError: false,
|
|
runeError: false,
|
|
macaroonHexError: false,
|
|
portError: false,
|
|
customMailboxServerError: false,
|
|
pairingPhraseError: false,
|
|
nostrWalletConnectUrlError: false
|
|
};
|
|
|
|
scrollViewRef = React.createRef<ScrollView>();
|
|
|
|
importClipboard = () => {
|
|
const { suggestImport } = this.state;
|
|
|
|
if (suggestImport.includes('lndconnect://')) {
|
|
const { host, port, macaroonHex } =
|
|
ConnectionFormatUtils.processLndConnectUrl(suggestImport);
|
|
|
|
this.setState({
|
|
host,
|
|
port,
|
|
macaroonHex,
|
|
suggestImport: '',
|
|
importError: '',
|
|
enableTor: host.includes('.onion')
|
|
});
|
|
} else if (
|
|
suggestImport.includes('lndhub://') ||
|
|
suggestImport.includes('bluewallet:')
|
|
) {
|
|
if (!AddressUtils.isValidLNDHubAddress(suggestImport)) {
|
|
this.setState({
|
|
suggestImport: '',
|
|
importError: localeString('utils.handleAnything.notValid')
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const { username, password, host } =
|
|
AddressUtils.processLNDHubAddress(suggestImport);
|
|
|
|
const existingAccount = !!username;
|
|
|
|
const stateUpdate: any = {
|
|
username,
|
|
password,
|
|
implementation: 'lndhub',
|
|
suggestImport: '',
|
|
importError: '',
|
|
existingAccount,
|
|
...(host && {
|
|
lndhubUrl: host,
|
|
enableTor: host.includes('.onion')
|
|
})
|
|
};
|
|
this.setState(stateUpdate);
|
|
} catch (error) {
|
|
this.setState({
|
|
suggestImport: '',
|
|
importError: localeString('utils.handleAnything.notValid')
|
|
});
|
|
return;
|
|
}
|
|
} else if (
|
|
suggestImport.includes('clnrest://') ||
|
|
suggestImport.includes('clnrest+')
|
|
) {
|
|
const {
|
|
host,
|
|
rune,
|
|
port,
|
|
enableTor,
|
|
implementation
|
|
}: {
|
|
host: string;
|
|
rune: string;
|
|
port: string;
|
|
enableTor: boolean;
|
|
implementation: Implementations;
|
|
} = ConnectionFormatUtils.processCLNRestConnectUrl(suggestImport);
|
|
|
|
this.setState({
|
|
host,
|
|
rune,
|
|
port,
|
|
enableTor,
|
|
implementation,
|
|
suggestImport: '',
|
|
importError: ''
|
|
});
|
|
}
|
|
|
|
Clipboard.setString('');
|
|
};
|
|
|
|
clearImportSuggestion = () => {
|
|
this.setState({
|
|
suggestImport: '',
|
|
importError: ''
|
|
});
|
|
};
|
|
|
|
async componentDidMount() {
|
|
const { SettingsStore } = this.props;
|
|
const { settings } = SettingsStore;
|
|
|
|
if (settings.privacy && settings.privacy.clipboard) {
|
|
const clipboard = await Clipboard.getString();
|
|
|
|
if (
|
|
clipboard.includes('lndconnect://') ||
|
|
clipboard.includes('lndhub://') ||
|
|
clipboard.includes('bluewallet:') ||
|
|
clipboard.includes('clnrest://') ||
|
|
clipboard.includes('clnrest+')
|
|
) {
|
|
this.setState({
|
|
suggestImport: clipboard
|
|
});
|
|
}
|
|
}
|
|
|
|
await this.initFromProps(this.props);
|
|
|
|
const { implementation, pairingPhrase } = this.state;
|
|
if (implementation === 'lightning-node-connect') {
|
|
const key = `${LNC_STORAGE_KEY}:${hash(pairingPhrase)}`;
|
|
const json: any = await Storage.getItem(key);
|
|
const parsed = JSON.parse(json);
|
|
if (parsed) {
|
|
if (parsed.localKey && parsed.remoteKey) {
|
|
this.setState({
|
|
localKey: parsed.localKey,
|
|
remoteKey: parsed.remoteKey
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async componentDidUpdate(prevProps: any) {
|
|
if (prevProps !== this.props) {
|
|
await this.initFromProps(this.props);
|
|
}
|
|
}
|
|
componentWillUnmount() {
|
|
const { SettingsStore } = this.props;
|
|
SettingsStore.createAccountError = '';
|
|
SettingsStore.createAccountSuccess = '';
|
|
}
|
|
|
|
async initFromProps(props: WalletConfigurationProps) {
|
|
const { route } = props;
|
|
|
|
// When returning from SetWalletPicture, only update the photo
|
|
// to avoid overwriting unsaved edits (e.g. nickname) from node
|
|
if (route.params.photo && !route.params.node) {
|
|
this.setState({
|
|
photo: route.params.photo,
|
|
saved: route.params.saved ?? this.state.saved
|
|
});
|
|
return;
|
|
}
|
|
|
|
const node = route.params.node ?? this.state.node;
|
|
const index = route.params.index ?? this.state.index;
|
|
const active = route.params.active ?? this.state.active;
|
|
const tor = route.params.tor ?? this.state.enableTor;
|
|
const saved = route.params.saved ?? this.state.saved;
|
|
const newEntry = route.params.newEntry ?? this.state.newEntry;
|
|
const newPhoto = route.params.photo ?? this.state.photo;
|
|
|
|
if (node) {
|
|
const {
|
|
nickname,
|
|
dismissCustodialWarning,
|
|
host,
|
|
port,
|
|
macaroonHex,
|
|
rune,
|
|
implementation,
|
|
certVerification,
|
|
enableTor,
|
|
photo,
|
|
// LNDHub
|
|
lndhubUrl,
|
|
existingAccount,
|
|
username,
|
|
password,
|
|
// LNC
|
|
pairingPhrase,
|
|
mailboxServer,
|
|
customMailboxServer,
|
|
// embedded LND
|
|
seedPhrase,
|
|
walletPassword,
|
|
adminMacaroon,
|
|
embeddedLndNetwork,
|
|
lndDir,
|
|
isSqlite,
|
|
// embedded LDK Node
|
|
ldkMnemonic,
|
|
ldkPassphrase,
|
|
ldkNodeDir,
|
|
ldkNetwork,
|
|
ldkEsploraServer,
|
|
ldkRgsServer,
|
|
ldkScorerUrl,
|
|
ldkVssServer,
|
|
// NWC
|
|
nostrWalletConnectUrl
|
|
} = node as any;
|
|
|
|
this.setState({
|
|
node,
|
|
nickname,
|
|
dismissCustodialWarning,
|
|
host,
|
|
port,
|
|
macaroonHex,
|
|
rune,
|
|
implementation: implementation || 'lnd',
|
|
certVerification,
|
|
index,
|
|
active,
|
|
saved,
|
|
newEntry,
|
|
enableTor: tor || enableTor,
|
|
photo: newPhoto || photo || '',
|
|
// LNDHub,
|
|
lndhubUrl,
|
|
existingAccount,
|
|
username,
|
|
password,
|
|
// LNC
|
|
pairingPhrase,
|
|
mailboxServer,
|
|
customMailboxServer,
|
|
// embedded LND
|
|
seedPhrase,
|
|
walletPassword,
|
|
adminMacaroon,
|
|
embeddedLndNetwork,
|
|
lndDir,
|
|
isSqlite,
|
|
// embedded LDK Node
|
|
ldkMnemonic,
|
|
ldkPassphrase,
|
|
ldkNodeDir,
|
|
ldkNetwork,
|
|
ldkEsploraServer,
|
|
ldkRgsServer,
|
|
ldkScorerUrl,
|
|
ldkVssServer,
|
|
ldkNodeInitialized: !!ldkMnemonic,
|
|
// NWC
|
|
nostrWalletConnectUrl
|
|
});
|
|
} else {
|
|
this.setState({
|
|
index,
|
|
active,
|
|
newEntry,
|
|
photo: newPhoto || ''
|
|
});
|
|
}
|
|
}
|
|
|
|
saveWalletConfiguration = (
|
|
recoveryCipherSeed?: string,
|
|
newEmbeddedLndWallet?: boolean
|
|
) => {
|
|
const { SettingsStore, navigation } = this.props;
|
|
const {
|
|
nickname,
|
|
dismissCustodialWarning,
|
|
host,
|
|
port,
|
|
enableTor,
|
|
lndhubUrl,
|
|
existingAccount,
|
|
macaroonHex,
|
|
rune,
|
|
username,
|
|
password,
|
|
implementation,
|
|
certVerification,
|
|
index,
|
|
pairingPhrase,
|
|
mailboxServer,
|
|
customMailboxServer,
|
|
seedPhrase,
|
|
walletPassword,
|
|
adminMacaroon,
|
|
embeddedLndNetwork,
|
|
lndDir,
|
|
isSqlite,
|
|
nostrWalletConnectUrl,
|
|
photo,
|
|
// embedded LDK Node
|
|
ldkMnemonic,
|
|
ldkPassphrase,
|
|
ldkNodeDir,
|
|
ldkNetwork,
|
|
ldkEsploraServer,
|
|
ldkRgsServer,
|
|
ldkScorerUrl,
|
|
ldkVssServer
|
|
} = this.state;
|
|
const { setConnectingStatus, updateSettings, settings } = SettingsStore;
|
|
|
|
if (
|
|
implementation === 'lndhub' &&
|
|
(!lndhubUrl || !username || !password)
|
|
) {
|
|
throw new Error('lndhub settings missing.');
|
|
}
|
|
|
|
const node = {
|
|
nickname,
|
|
dismissCustodialWarning,
|
|
host,
|
|
port,
|
|
lndhubUrl,
|
|
existingAccount,
|
|
macaroonHex,
|
|
rune,
|
|
username,
|
|
password,
|
|
implementation,
|
|
certVerification,
|
|
enableTor,
|
|
pairingPhrase,
|
|
mailboxServer,
|
|
customMailboxServer,
|
|
seedPhrase,
|
|
walletPassword,
|
|
adminMacaroon,
|
|
embeddedLndNetwork,
|
|
lndDir,
|
|
isSqlite,
|
|
nostrWalletConnectUrl,
|
|
photo,
|
|
// embedded LDK Node
|
|
ldkMnemonic,
|
|
ldkPassphrase,
|
|
ldkNodeDir,
|
|
ldkNetwork,
|
|
ldkEsploraServer,
|
|
ldkRgsServer,
|
|
ldkScorerUrl,
|
|
ldkVssServer
|
|
};
|
|
|
|
let nodes: Node[];
|
|
let originalNode: Node;
|
|
if (settings.nodes) {
|
|
nodes = [...settings.nodes];
|
|
if (index != null) {
|
|
originalNode = nodes[index];
|
|
nodes[index] = node;
|
|
} else {
|
|
nodes.push(node);
|
|
}
|
|
} else {
|
|
nodes = [node];
|
|
}
|
|
|
|
let update;
|
|
if (newEmbeddedLndWallet) {
|
|
update = {
|
|
nodes,
|
|
selectedNode: nodes.length - 1,
|
|
// Force Wallet view to restart connection for new wallet
|
|
justDeletedWallet: false,
|
|
// Ensure Express Graph Sync runs for new wallet
|
|
initialLoad: false
|
|
};
|
|
} else {
|
|
update = {
|
|
nodes
|
|
};
|
|
}
|
|
|
|
updateSettings(update).then(async () => {
|
|
if (recoveryCipherSeed) {
|
|
await updateSettings({
|
|
recovery: true
|
|
});
|
|
}
|
|
|
|
this.setState({
|
|
saved: true
|
|
});
|
|
|
|
const activeNodeIndex = settings.selectedNode || 0;
|
|
if (index === activeNodeIndex) {
|
|
// updating active node
|
|
if (originalNode != null) {
|
|
const diff = differenceBy(
|
|
Object.entries(originalNode),
|
|
Object.entries(node),
|
|
(entry) => entry[0] + entry[1]
|
|
).filter(
|
|
(entry) =>
|
|
entry[0] !== 'nickname' && entry[0] !== 'photo'
|
|
);
|
|
if (diff.length === 0) {
|
|
// only nickname or photo was edited - no reconnect necessary
|
|
navigation.goBack();
|
|
return;
|
|
}
|
|
}
|
|
if (implementation === 'lightning-node-connect') {
|
|
BackendUtils.disconnect();
|
|
}
|
|
setConnectingStatus(true);
|
|
navigation.popTo('Wallet');
|
|
} else {
|
|
if (newEmbeddedLndWallet) {
|
|
// New wallet created - trigger fresh connection
|
|
// LND was already stopped in createNewWallet(), just navigate
|
|
setConnectingStatus(true);
|
|
navigation.popTo('Wallet');
|
|
} else if (this.state.newEntry) {
|
|
navigation.navigate('Wallets');
|
|
} else {
|
|
navigation.goBack();
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
copyNodeConfig = () => {
|
|
const { SettingsStore, navigation } = this.props;
|
|
const { settings } = SettingsStore;
|
|
const {
|
|
nickname,
|
|
host,
|
|
port,
|
|
enableTor,
|
|
lndhubUrl,
|
|
existingAccount,
|
|
macaroonHex,
|
|
rune,
|
|
username,
|
|
password,
|
|
implementation,
|
|
certVerification,
|
|
pairingPhrase,
|
|
mailboxServer,
|
|
customMailboxServer,
|
|
photo,
|
|
nostrWalletConnectUrl
|
|
} = this.state;
|
|
const { nodes } = settings;
|
|
|
|
const node = {
|
|
nickname: `${nickname} copy`,
|
|
host,
|
|
port,
|
|
lndhubUrl,
|
|
existingAccount,
|
|
macaroonHex,
|
|
rune,
|
|
username,
|
|
password,
|
|
implementation,
|
|
certVerification,
|
|
enableTor,
|
|
pairingPhrase,
|
|
mailboxServer,
|
|
customMailboxServer,
|
|
photo,
|
|
nostrWalletConnectUrl
|
|
};
|
|
|
|
navigation.navigate('WalletConfiguration', {
|
|
node,
|
|
newEntry: true,
|
|
active: false,
|
|
saved: false,
|
|
index: nodes!.length
|
|
});
|
|
};
|
|
|
|
deleteNodeConfig = async () => {
|
|
this.setState({ deletingWallet: true });
|
|
const { SettingsStore, navigation } = this.props;
|
|
const { updateSettings, embeddedLndStarted, settings } = SettingsStore;
|
|
const { index, implementation, lndDir, ldkNodeDir, active } =
|
|
this.state;
|
|
const { nodes } = settings;
|
|
|
|
try {
|
|
const newNodes: any = [];
|
|
for (let i = 0; nodes && i < nodes.length; i++) {
|
|
if (index !== i) {
|
|
newNodes.push(nodes[i]);
|
|
}
|
|
}
|
|
|
|
// If deleting active embedded LND wallet, stop it first
|
|
if (
|
|
active &&
|
|
implementation === 'embedded-lnd' &&
|
|
embeddedLndStarted
|
|
) {
|
|
try {
|
|
console.log(
|
|
'Stopping active LND before deletion:',
|
|
lndDir || 'lnd'
|
|
);
|
|
this.props.SyncStore.reset(); // Stop sync loop before stopping LND
|
|
await stopLnd();
|
|
} catch (error) {
|
|
console.log('Error stopping LND before deletion:', error);
|
|
}
|
|
SettingsStore.embeddedLndStarted = false;
|
|
}
|
|
|
|
// If deleting active LDK Node wallet, stop it first
|
|
if (active && implementation === 'ldk-node') {
|
|
await stopLdkNode();
|
|
}
|
|
|
|
// Update settings first to clear node references before
|
|
// deleting files — prevents stale references if navigation
|
|
// triggers a reconnect
|
|
const newSelectedNodeIndex = this.getNewSelectedNodeIndex(
|
|
index,
|
|
settings
|
|
);
|
|
|
|
await updateSettings({
|
|
nodes: newNodes,
|
|
selectedNode: newSelectedNodeIndex,
|
|
justDeletedWallet: active
|
|
});
|
|
|
|
// Delete wallet data after settings are updated
|
|
if (implementation === 'embedded-lnd') {
|
|
await deleteLndWallet(lndDir || 'lnd');
|
|
}
|
|
if (implementation === 'ldk-node' && ldkNodeDir) {
|
|
await deleteLdkNodeWallet(ldkNodeDir);
|
|
}
|
|
|
|
if (newNodes.length === 0) {
|
|
navigation.navigate('IntroSplash');
|
|
} else {
|
|
navigation.popTo('Wallets');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting wallet:', error);
|
|
this.setState({ deletingWallet: false });
|
|
}
|
|
};
|
|
|
|
private handleDeletePress = () => {
|
|
confirmAction(
|
|
localeString('views.Settings.WalletConfiguration.deleteWallet'),
|
|
localeString(
|
|
'views.Settings.WalletConfiguration.deleteWallet.confirm'
|
|
),
|
|
{
|
|
text: localeString(
|
|
'views.Settings.WalletConfiguration.deleteWallet'
|
|
),
|
|
style: 'destructive',
|
|
onPress: this.performDelete
|
|
},
|
|
{
|
|
text: localeString('general.cancel'),
|
|
onPress: () => void 0,
|
|
isPreferred: true
|
|
}
|
|
);
|
|
};
|
|
|
|
private performDelete = () => {
|
|
const { implementation, active } = this.state;
|
|
const { ChannelsStore, BalanceStore, navigation } = this.props;
|
|
|
|
if (implementation === 'embedded-lnd' && !active) {
|
|
Alert.alert(
|
|
localeString('general.error'),
|
|
localeString(
|
|
'views.Settings.WalletConfiguration.deleteWallet.inactiveWarning'
|
|
),
|
|
[
|
|
{
|
|
text: localeString(
|
|
'views.Settings.WalletConfiguration.setWalletActive'
|
|
),
|
|
onPress: () => this.setWalletConfigurationAsActive(),
|
|
isPreferred: true
|
|
},
|
|
{
|
|
text: localeString('general.cancel'),
|
|
onPress: () => void 0
|
|
}
|
|
],
|
|
{ cancelable: false }
|
|
);
|
|
} else if (
|
|
implementation === 'embedded-lnd' &&
|
|
(ChannelsStore.channels.length > 0 ||
|
|
ChannelsStore.pendingChannels.length > 0)
|
|
) {
|
|
Alert.alert(
|
|
localeString('general.warning'),
|
|
localeString(
|
|
'views.Settings.WalletConfiguration.deleteWallet.channelsWarning'
|
|
),
|
|
[
|
|
{
|
|
text: localeString(
|
|
'views.Settings.WalletConfiguration.deleteWallet'
|
|
),
|
|
onPress: () => this.deleteNodeConfig()
|
|
},
|
|
{
|
|
text: localeString(
|
|
'views.Settings.WalletConfiguration.deleteWallet.exportChannels'
|
|
),
|
|
onPress: () =>
|
|
navigation.navigate('Seed', { skipWarning: true }),
|
|
isPreferred: true
|
|
},
|
|
{
|
|
text: localeString('general.cancel'),
|
|
onPress: () => void 0
|
|
}
|
|
],
|
|
{ cancelable: false }
|
|
);
|
|
} else if (
|
|
implementation === 'embedded-lnd' &&
|
|
(BalanceStore.confirmedBlockchainBalance !== 0 ||
|
|
BalanceStore.unconfirmedBlockchainBalance !== 0)
|
|
) {
|
|
Alert.alert(
|
|
localeString('general.warning'),
|
|
localeString(
|
|
'views.Settings.WalletConfiguration.deleteWallet.balanceWarning'
|
|
),
|
|
[
|
|
{
|
|
text: localeString(
|
|
'views.Settings.WalletConfiguration.deleteWallet'
|
|
),
|
|
onPress: () => this.deleteNodeConfig()
|
|
},
|
|
{
|
|
text: localeString('general.cancel'),
|
|
onPress: () => void 0,
|
|
isPreferred: true
|
|
}
|
|
],
|
|
{ cancelable: false }
|
|
);
|
|
} else {
|
|
this.deleteNodeConfig();
|
|
}
|
|
};
|
|
|
|
getNewSelectedNodeIndex(
|
|
indexOfDeletedNode: number | null,
|
|
settings: Settings
|
|
): number | undefined {
|
|
if (
|
|
settings.selectedNode == null ||
|
|
indexOfDeletedNode == null ||
|
|
indexOfDeletedNode > settings.selectedNode
|
|
) {
|
|
return settings.selectedNode;
|
|
}
|
|
if (indexOfDeletedNode < settings.selectedNode) {
|
|
return settings.selectedNode - 1;
|
|
}
|
|
return settings.nodes?.length != null && settings.nodes?.length > 0
|
|
? 0
|
|
: undefined;
|
|
}
|
|
|
|
setWalletConfigurationAsActive = async () => {
|
|
const { SettingsStore, navigation } = this.props;
|
|
const { updateSettings, setConnectingStatus, setInitialStart } =
|
|
SettingsStore;
|
|
const { index } = this.state;
|
|
|
|
await updateSettings({
|
|
selectedNode: index
|
|
});
|
|
|
|
this.setState({
|
|
active: true
|
|
});
|
|
|
|
setConnectingStatus(true);
|
|
setInitialStart(false);
|
|
|
|
navigation.popTo('Wallet');
|
|
};
|
|
|
|
createNewWallet = async (network: string = 'Mainnet') => {
|
|
const { recoveryCipherSeed, channelBackupsBase64, isSqlite } =
|
|
this.state;
|
|
const { SettingsStore, SyncStore } = this.props;
|
|
const { embeddedLndStarted } = SettingsStore;
|
|
|
|
this.setState({
|
|
creatingWallet: true,
|
|
errorCreatingWallet: false
|
|
});
|
|
|
|
// CRITICAL: Stop old LND instance completely before creating new wallet
|
|
if (embeddedLndStarted) {
|
|
try {
|
|
console.log(
|
|
'Stopping existing LND before creating new wallet...'
|
|
);
|
|
SyncStore.reset(); // Stop sync loop before stopping LND to prevent crash
|
|
await stopLnd();
|
|
console.log('LND stopped successfully');
|
|
} catch (error) {
|
|
console.log(
|
|
'Error stopping LND before creating new wallet:',
|
|
error
|
|
);
|
|
// Continue anyway - stopLnd handles errors gracefully
|
|
}
|
|
}
|
|
|
|
await optimizeNeutrinoPeers(network === 'Testnet');
|
|
|
|
const lndDir = uuidv4();
|
|
|
|
if (!embeddedLndStarted) {
|
|
SyncStore.reset();
|
|
}
|
|
let response;
|
|
try {
|
|
response = await createLndWallet({
|
|
lndDir,
|
|
seedMnemonic: recoveryCipherSeed,
|
|
isTestnet: network === 'Testnet',
|
|
channelBackupsBase64,
|
|
isSqlite: isSqlite ?? false
|
|
});
|
|
} catch (error) {
|
|
console.log('Error creating wallet:', error);
|
|
this.setState({
|
|
creatingWallet: false,
|
|
errorCreatingWallet: true
|
|
});
|
|
return;
|
|
}
|
|
|
|
const { wallet, seed, randomBase64 }: any = response;
|
|
|
|
if (wallet && wallet.admin_macaroon) {
|
|
this.setState(
|
|
{
|
|
adminMacaroon: wallet.admin_macaroon,
|
|
seedPhrase: seed.cipher_seed_mnemonic,
|
|
walletPassword: randomBase64,
|
|
embeddedLndNetwork: network,
|
|
lndDir,
|
|
isSqlite: this.state.isSqlite ?? false,
|
|
creatingWallet: false
|
|
},
|
|
() => {
|
|
this.saveWalletConfiguration(recoveryCipherSeed, true);
|
|
}
|
|
);
|
|
} else {
|
|
this.setState({
|
|
creatingWallet: false,
|
|
errorCreatingWallet: true
|
|
});
|
|
}
|
|
};
|
|
|
|
saveLdkNodeWalletConfiguration = (
|
|
mnemonic: string,
|
|
nodeDir: string,
|
|
network: string
|
|
) => {
|
|
const { SettingsStore, navigation } = this.props;
|
|
const { nickname, photo, ldkPassphrase, ldkVssServer } = this.state;
|
|
const { setConnectingStatus, updateSettings, settings } = SettingsStore;
|
|
|
|
const node = {
|
|
nickname,
|
|
photo,
|
|
implementation: 'ldk-node',
|
|
ldkNetwork: network,
|
|
ldkNodeDir: nodeDir,
|
|
ldkMnemonic: mnemonic,
|
|
ldkPassphrase,
|
|
ldkEsploraServer: getDefaultEsploraServer(
|
|
network as SupportedNetwork
|
|
),
|
|
ldkRgsServer: getDefaultRgsServer(network as SupportedNetwork),
|
|
ldkScorerUrl: DEFAULT_SCORER_URL,
|
|
ldkVssServer: ldkVssServer || DEFAULT_VSS_SERVER
|
|
};
|
|
|
|
let nodes: any;
|
|
if (settings.nodes) {
|
|
nodes = [...settings.nodes];
|
|
nodes.push(node);
|
|
} else {
|
|
nodes = [node];
|
|
}
|
|
|
|
updateSettings({
|
|
nodes,
|
|
selectedNode: nodes.length - 1
|
|
}).then(async () => {
|
|
this.setState({ saved: true });
|
|
setConnectingStatus(true);
|
|
|
|
if (nodes.length === 1) {
|
|
navigation.popTo('Wallet');
|
|
} else {
|
|
navigation.popTo('Wallets');
|
|
}
|
|
});
|
|
};
|
|
|
|
createLdkNodeNewWallet = async (network: string = 'mainnet') => {
|
|
const {
|
|
ldkPassphrase,
|
|
ldkEsploraServer,
|
|
ldkRgsServer,
|
|
ldkScorerUrl,
|
|
ldkVssServer,
|
|
ldkMnemonic,
|
|
ldkSeedWordCount
|
|
} = this.state;
|
|
|
|
this.setState({
|
|
creatingWallet: true
|
|
});
|
|
|
|
try {
|
|
await stopLdkNode();
|
|
|
|
const ldkNodeDir = uuidv4();
|
|
const networkType = network as
|
|
| 'mainnet'
|
|
| 'testnet'
|
|
| 'signet'
|
|
| 'regtest'
|
|
| 'mutinynet';
|
|
|
|
// Get LSPS1 config from settings based on network
|
|
const { SettingsStore } = this.props;
|
|
const { settings } = SettingsStore;
|
|
const lspConfig = getLspConfigForNetwork(settings, networkType);
|
|
const lsps1Token = settings.lsps1Token;
|
|
|
|
const lsps1Config =
|
|
lspConfig.lsps1Pubkey && lspConfig.lsps1Host
|
|
? {
|
|
nodeId: lspConfig.lsps1Pubkey,
|
|
address: lspConfig.lsps1Host,
|
|
token: lsps1Token || null
|
|
}
|
|
: undefined;
|
|
|
|
// Always include Flow LSP pubkey as trusted 0-conf peer
|
|
const flowLspPubkey = lspConfig.defaultPubkey;
|
|
|
|
const trustedPeers = [flowLspPubkey];
|
|
if (lsps1Config?.nodeId && lsps1Config.nodeId !== flowLspPubkey) {
|
|
trustedPeers.push(lsps1Config.nodeId);
|
|
}
|
|
|
|
const response = await createLdkNodeWallet({
|
|
nodeDir: ldkNodeDir,
|
|
seedMnemonic: ldkMnemonic || undefined,
|
|
wordCount: ldkSeedWordCount,
|
|
passphrase: ldkPassphrase || undefined,
|
|
network: networkType,
|
|
esploraServerUrl:
|
|
ldkEsploraServer || getDefaultEsploraServer(networkType),
|
|
rgsServerUrl: ldkRgsServer || getDefaultRgsServer(networkType),
|
|
scorerUrl:
|
|
ldkScorerUrl === undefined
|
|
? DEFAULT_SCORER_URL
|
|
: ldkScorerUrl,
|
|
lsps1Config,
|
|
trustedPeers0conf: trustedPeers,
|
|
vssServerUrl: ldkVssServer || DEFAULT_VSS_SERVER
|
|
});
|
|
|
|
// Node is already built — tell Wallet.tsx to skip re-init
|
|
SettingsStore.walletJustCreated = true;
|
|
|
|
if (response?.vssError) {
|
|
console.warn(
|
|
'LDK Node: VSS unavailable during wallet creation:',
|
|
response.vssError
|
|
);
|
|
}
|
|
|
|
if (response && response.mnemonic) {
|
|
this.setState({
|
|
ldkMnemonic: response.mnemonic,
|
|
ldkNodeDir,
|
|
ldkNetwork: network,
|
|
ldkNodeInitialized: true,
|
|
creatingWallet: false
|
|
});
|
|
|
|
this.saveLdkNodeWalletConfiguration(
|
|
response.mnemonic,
|
|
ldkNodeDir,
|
|
network
|
|
);
|
|
} else {
|
|
this.setState({
|
|
creatingWallet: false,
|
|
errorCreatingWallet: true
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error creating LDK Node wallet:', error);
|
|
this.setState({
|
|
creatingWallet: false,
|
|
errorCreatingWallet: true
|
|
});
|
|
}
|
|
};
|
|
|
|
render() {
|
|
const SERVER_ADDRESS_CHARS = "a-zA-Z0-9-._~!$&'()*+,;=";
|
|
|
|
const { navigation, SettingsStore } = this.props;
|
|
const {
|
|
node,
|
|
nickname,
|
|
host,
|
|
port,
|
|
lndhubUrl,
|
|
macaroonHex,
|
|
rune,
|
|
username,
|
|
password,
|
|
saved,
|
|
active,
|
|
newEntry,
|
|
implementation,
|
|
certVerification,
|
|
enableTor,
|
|
photo,
|
|
interfaceKeys,
|
|
existingAccount,
|
|
suggestImport,
|
|
importError,
|
|
showLndHubModal,
|
|
showCertModal,
|
|
pairingPhrase,
|
|
mailboxServer,
|
|
customMailboxServer,
|
|
localKey,
|
|
remoteKey,
|
|
adminMacaroon,
|
|
embeddedLndNetwork,
|
|
isSqlite,
|
|
recoveryCipherSeed,
|
|
channelBackupsBase64,
|
|
creatingWallet,
|
|
errorCreatingWallet,
|
|
deletingWallet,
|
|
// LDK Node
|
|
ldkMnemonic,
|
|
ldkNetwork,
|
|
ldkSeedWordCount,
|
|
ldkVssServerSelected,
|
|
ldkVssServerCustom,
|
|
ldkNodeInitialized,
|
|
// NWC
|
|
nostrWalletConnectUrl,
|
|
lndhubUrlError,
|
|
usernameError,
|
|
hostError,
|
|
runeError,
|
|
macaroonHexError,
|
|
portError,
|
|
customMailboxServerError,
|
|
pairingPhraseError,
|
|
nostrWalletConnectUrlError
|
|
} = this.state;
|
|
const {
|
|
loading,
|
|
createAccountError,
|
|
createAccountSuccess,
|
|
createAccount,
|
|
seedPhrase
|
|
} = SettingsStore;
|
|
|
|
const isLocalImpl =
|
|
implementation === 'embedded-lnd' || implementation === 'ldk-node';
|
|
const supportsTor =
|
|
implementation !== 'lightning-node-connect' &&
|
|
!isLocalImpl &&
|
|
implementation !== 'nostr-wallet-connect';
|
|
const supportsCertVerification =
|
|
implementation !== 'lightning-node-connect' &&
|
|
!isLocalImpl &&
|
|
implementation !== 'nostr-wallet-connect';
|
|
|
|
const CertInstallInstructions = () => (
|
|
<View style={styles.button}>
|
|
<Button
|
|
title={localeString(
|
|
'views.Settings.AddEditNode.certificateButton'
|
|
)}
|
|
onPress={() => {
|
|
this.setState({
|
|
showCertModal: false
|
|
});
|
|
navigation.navigate('CertInstallInstructions');
|
|
}}
|
|
secondary
|
|
/>
|
|
</View>
|
|
);
|
|
|
|
const WalletInterface = () => (
|
|
<DropdownSetting
|
|
title={localeString(
|
|
'views.Settings.WalletConfiguration.walletInterface'
|
|
)}
|
|
selectedValue={implementation}
|
|
onValueChange={(value: Implementations) => {
|
|
this.setState({
|
|
implementation: value,
|
|
saved: false,
|
|
certVerification: value === 'lndhub' ? true : false,
|
|
host: '',
|
|
lndhubUrl: ''
|
|
});
|
|
}}
|
|
values={interfaceKeys}
|
|
/>
|
|
);
|
|
|
|
const Mailbox = () => {
|
|
return (
|
|
<DropdownSetting
|
|
title={localeString(
|
|
'views.Settings.AddEditNode.mailboxServer'
|
|
)}
|
|
selectedValue={mailboxServer}
|
|
onValueChange={(value: string) => {
|
|
this.setState({
|
|
mailboxServer: value,
|
|
saved: false
|
|
});
|
|
}}
|
|
values={LNC_MAILBOX_KEYS}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const AddPhotos = () => (
|
|
<AddIcon
|
|
fill={themeColor('background')}
|
|
width="14"
|
|
height="14"
|
|
style={{ alignSelf: 'center' }}
|
|
/>
|
|
);
|
|
|
|
return (
|
|
<Screen>
|
|
<Header
|
|
leftComponent="Back"
|
|
centerComponent={{
|
|
text: localeString(
|
|
'views.Settings.WalletConfiguration.title'
|
|
),
|
|
style: { ...styles.text, color: themeColor('text') }
|
|
}}
|
|
rightComponent={
|
|
<Row>
|
|
{(loading || deletingWallet) && (
|
|
<View style={{ paddingRight: 15 }}>
|
|
<LoadingIndicator size={30} />
|
|
</View>
|
|
)}
|
|
<ScanBadge
|
|
onPress={() =>
|
|
navigation.navigate(
|
|
'HandleAnythingQRScanner'
|
|
)
|
|
}
|
|
/>
|
|
</Row>
|
|
}
|
|
navigation={navigation}
|
|
/>
|
|
{!!suggestImport && (
|
|
<View
|
|
style={{
|
|
padding: 10,
|
|
backgroundColor: themeColor('disabled')
|
|
}}
|
|
>
|
|
<Text style={styles.whiteText}>
|
|
{localeString(
|
|
'views.Settings.AddEditNode.connectionStringClipboard'
|
|
)}
|
|
:
|
|
</Text>
|
|
<Text
|
|
style={{
|
|
...styles.whiteText,
|
|
padding: 15
|
|
}}
|
|
>
|
|
{suggestImport.length > 100
|
|
? `${suggestImport.substring(0, 100)}...`
|
|
: suggestImport}
|
|
</Text>
|
|
<Text style={{ ...styles.whiteText, marginBottom: 10 }}>
|
|
{localeString(
|
|
'views.Settings.AddEditNode.importPrompt'
|
|
)}
|
|
</Text>
|
|
<View style={styles.button}>
|
|
<Button
|
|
title={localeString(
|
|
'views.Settings.AddEditNode.import'
|
|
)}
|
|
onPress={() => this.importClipboard()}
|
|
tertiary
|
|
/>
|
|
</View>
|
|
<View style={styles.button}>
|
|
<Button
|
|
title={localeString('general.cancel')}
|
|
onPress={() => this.clearImportSuggestion()}
|
|
tertiary
|
|
/>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
<Modal
|
|
animationType="slide"
|
|
transparent={true}
|
|
visible={showLndHubModal || showCertModal}
|
|
>
|
|
<View style={styles.centeredView}>
|
|
<View style={styles.modal}>
|
|
{showLndHubModal && (
|
|
<>
|
|
<Text
|
|
style={{
|
|
...styles.blackText,
|
|
fontSize: 40
|
|
}}
|
|
>
|
|
{localeString('general.warning')}
|
|
</Text>
|
|
<Text
|
|
style={{
|
|
paddingTop: 20,
|
|
color: 'black'
|
|
}}
|
|
>
|
|
{localeString(
|
|
'views.Settings.AddEditNode.lndhubWarning'
|
|
)}
|
|
</Text>
|
|
<Text
|
|
style={{
|
|
...styles.blackText,
|
|
paddingTop: 20,
|
|
paddingBottom: 20,
|
|
color: 'black'
|
|
}}
|
|
>
|
|
{localeString(
|
|
'views.Settings.AddEditNode.lndhubFriend'
|
|
)}
|
|
</Text>
|
|
<View style={styles.button}>
|
|
<Button
|
|
title={localeString(
|
|
'views.Settings.AddEditNode.lndhubUnderstand'
|
|
)}
|
|
onPress={() => {
|
|
createAccount(
|
|
lndhubUrl,
|
|
certVerification
|
|
).then((data: any) => {
|
|
if (
|
|
data.login &&
|
|
data.password
|
|
) {
|
|
this.setState({
|
|
username:
|
|
data.login,
|
|
password:
|
|
data.password,
|
|
existingAccount:
|
|
true
|
|
});
|
|
}
|
|
|
|
this.setState({
|
|
showLndHubModal: false
|
|
});
|
|
});
|
|
}}
|
|
secondary
|
|
/>
|
|
</View>
|
|
<View style={styles.button}>
|
|
<Button
|
|
title={localeString(
|
|
'general.cancel'
|
|
)}
|
|
onPress={() =>
|
|
this.setState({
|
|
showLndHubModal: false
|
|
})
|
|
}
|
|
/>
|
|
</View>
|
|
</>
|
|
)}
|
|
{showCertModal && (
|
|
<>
|
|
<Text
|
|
style={{
|
|
...styles.blackText,
|
|
fontSize: 40
|
|
}}
|
|
>
|
|
{localeString('general.warning')}
|
|
</Text>
|
|
<Text
|
|
style={{
|
|
...styles.blackText,
|
|
paddingTop: 20
|
|
}}
|
|
>
|
|
{localeString(
|
|
'views.Settings.AddEditNode.certificateWarning1'
|
|
)}
|
|
</Text>
|
|
<Text
|
|
style={{
|
|
...styles.blackText,
|
|
paddingTop: 20,
|
|
paddingBottom: 20,
|
|
color: 'black'
|
|
}}
|
|
>
|
|
{localeString(
|
|
'views.Settings.AddEditNode.certificateWarning2'
|
|
)}
|
|
</Text>
|
|
<CertInstallInstructions />
|
|
<View style={styles.button}>
|
|
<Button
|
|
title={localeString(
|
|
'views.Settings.WalletConfiguration.certificateUnderstand'
|
|
)}
|
|
onPress={() => {
|
|
this.saveWalletConfiguration();
|
|
this.setState({
|
|
showCertModal: false
|
|
});
|
|
}}
|
|
tertiary
|
|
/>
|
|
</View>
|
|
<View style={styles.button}>
|
|
<Button
|
|
title={localeString(
|
|
'general.cancel'
|
|
)}
|
|
onPress={() =>
|
|
this.setState({
|
|
showCertModal: false
|
|
})
|
|
}
|
|
/>
|
|
</View>
|
|
</>
|
|
)}
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
|
|
<KeyboardAvoidingView
|
|
style={{ flex: 1 }}
|
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
>
|
|
<ScrollView
|
|
ref={this.scrollViewRef}
|
|
style={{ flex: 1, paddingHorizontal: 20 }}
|
|
keyboardShouldPersistTaps="handled"
|
|
>
|
|
<View style={{ ...styles.form, marginBottom: 20 }}>
|
|
{!!importError && (
|
|
<ErrorMessage message={importError} />
|
|
)}
|
|
|
|
{!!createAccountError &&
|
|
implementation === 'lndhub' &&
|
|
!loading && (
|
|
<ErrorMessage
|
|
message={createAccountError}
|
|
/>
|
|
)}
|
|
|
|
{!!createAccountSuccess &&
|
|
implementation === 'lndhub' &&
|
|
!loading && (
|
|
<SuccessMessage
|
|
message={createAccountSuccess}
|
|
/>
|
|
)}
|
|
|
|
<View style={{ marginLeft: 64 }}>
|
|
<Text
|
|
style={{
|
|
...styles.text,
|
|
color: themeColor('text')
|
|
}}
|
|
>
|
|
{localeString(
|
|
'views.Settings.AddEditNode.nickname'
|
|
)}
|
|
</Text>
|
|
</View>
|
|
<View style={styles.container}>
|
|
<TouchableOpacity
|
|
onPress={
|
|
(!node || !node.photo) && !photo
|
|
? () =>
|
|
navigation.navigate(
|
|
'SetWalletPicture',
|
|
{ implementation }
|
|
)
|
|
: () => {
|
|
if (node) {
|
|
this.setState({
|
|
node: {
|
|
...node,
|
|
photo: undefined
|
|
}
|
|
});
|
|
}
|
|
this.setState(
|
|
{
|
|
photo: undefined,
|
|
saved: false
|
|
},
|
|
() => {
|
|
node
|
|
? (node.photo =
|
|
'')
|
|
: null;
|
|
}
|
|
);
|
|
}
|
|
}
|
|
>
|
|
<View
|
|
style={{
|
|
...styles.imageBackground,
|
|
backgroundColor:
|
|
themeColor('secondaryText')
|
|
}}
|
|
>
|
|
{node?.photo || photo ? (
|
|
<Image
|
|
source={{
|
|
uri: getPhoto(
|
|
node?.photo || photo
|
|
)
|
|
}}
|
|
style={styles.imageBackground}
|
|
/>
|
|
) : (
|
|
<AddPhotos />
|
|
)}
|
|
</View>
|
|
</TouchableOpacity>
|
|
|
|
<View
|
|
style={{
|
|
flex: 1,
|
|
marginLeft: 14
|
|
}}
|
|
>
|
|
<TextInput
|
|
placeholder={localeString(
|
|
'general.defaultNodeNickname'
|
|
)}
|
|
value={nickname}
|
|
onChangeText={(text: string) =>
|
|
this.setState({
|
|
nickname: text,
|
|
saved: false
|
|
})
|
|
}
|
|
locked={loading}
|
|
/>
|
|
</View>
|
|
</View>
|
|
|
|
{!adminMacaroon && <WalletInterface />}
|
|
|
|
{!adminMacaroon &&
|
|
implementation === 'embedded-lnd' && (
|
|
<View>
|
|
{!adminMacaroon && (
|
|
<DropdownSetting
|
|
title={localeString(
|
|
'general.network'
|
|
)}
|
|
selectedValue={
|
|
embeddedLndNetwork ||
|
|
'mainnet'
|
|
}
|
|
onValueChange={(
|
|
value: string
|
|
) => {
|
|
this.setState({
|
|
embeddedLndNetwork:
|
|
value
|
|
});
|
|
}}
|
|
values={EMBEDDED_NODE_NETWORK_KEYS.filter(
|
|
(k) =>
|
|
k.value !== 'mutinynet'
|
|
)}
|
|
/>
|
|
)}
|
|
<DropdownSetting
|
|
title={localeString(
|
|
'views.Settings.WalletConfiguration.database'
|
|
)}
|
|
selectedValue={
|
|
isSqlite ? 'sqlite' : 'bolt'
|
|
}
|
|
onValueChange={(value: string) => {
|
|
this.setState({
|
|
isSqlite: value === 'sqlite'
|
|
});
|
|
}}
|
|
values={[
|
|
{
|
|
key: 'Bolt',
|
|
value: 'bolt'
|
|
},
|
|
{
|
|
key: `SQLite (${localeString(
|
|
'general.experimental'
|
|
)})`,
|
|
value: 'sqlite'
|
|
}
|
|
]}
|
|
/>
|
|
</View>
|
|
)}
|
|
|
|
{!adminMacaroon &&
|
|
implementation === 'embedded-lnd' &&
|
|
recoveryCipherSeed && (
|
|
<View>
|
|
<Text
|
|
style={{
|
|
...styles.text,
|
|
color: themeColor('text')
|
|
}}
|
|
>
|
|
{`${localeString(
|
|
'views.Settings.AddEditNode.disasterRecoveryBase64'
|
|
)} (${localeString(
|
|
'general.optional'
|
|
)})`}
|
|
</Text>
|
|
<TextInput
|
|
value={channelBackupsBase64}
|
|
onChangeText={(text: string) =>
|
|
this.setState({
|
|
channelBackupsBase64: text
|
|
})
|
|
}
|
|
locked={loading}
|
|
/>
|
|
</View>
|
|
)}
|
|
|
|
{!ldkNodeInitialized &&
|
|
implementation === 'ldk-node' && (
|
|
<View>
|
|
<DropdownSetting
|
|
title={localeString(
|
|
'general.network'
|
|
)}
|
|
selectedValue={
|
|
ldkNetwork || 'mainnet'
|
|
}
|
|
onValueChange={(value: string) => {
|
|
this.setState({
|
|
ldkNetwork: value
|
|
});
|
|
}}
|
|
values={EMBEDDED_NODE_NETWORK_KEYS}
|
|
/>
|
|
|
|
<Accordion
|
|
headerLayout="form"
|
|
id="ldk-advanced-settings"
|
|
scrollRef={this.scrollViewRef}
|
|
title={localeString(
|
|
'general.advancedSettings'
|
|
)}
|
|
>
|
|
<DropdownSetting
|
|
title={localeString(
|
|
'views.Settings.WalletConfiguration.seedPhraseLength'
|
|
)}
|
|
selectedValue={ldkSeedWordCount}
|
|
onValueChange={(
|
|
value: number
|
|
) => {
|
|
this.setState({
|
|
ldkSeedWordCount:
|
|
value as 12 | 24
|
|
});
|
|
}}
|
|
values={[
|
|
{
|
|
key: localeString(
|
|
'views.Settings.WalletConfiguration.seedPhraseLength.12'
|
|
),
|
|
value: 12
|
|
},
|
|
{
|
|
key: localeString(
|
|
'views.Settings.WalletConfiguration.seedPhraseLength.24'
|
|
),
|
|
value: 24
|
|
}
|
|
]}
|
|
/>
|
|
|
|
<VssServerPicker
|
|
selectedValue={
|
|
ldkVssServerSelected
|
|
}
|
|
customServer={
|
|
ldkVssServerCustom
|
|
}
|
|
locked={loading}
|
|
onChange={(value, custom) =>
|
|
this.setState({
|
|
ldkVssServerSelected:
|
|
value,
|
|
ldkVssServerCustom:
|
|
custom,
|
|
ldkVssServer:
|
|
resolveVssServer(
|
|
value,
|
|
custom
|
|
)
|
|
})
|
|
}
|
|
/>
|
|
</Accordion>
|
|
</View>
|
|
)}
|
|
|
|
{implementation === 'nostr-wallet-connect' && (
|
|
<>
|
|
<Text
|
|
style={{
|
|
color: themeColor('secondaryText')
|
|
}}
|
|
>
|
|
{localeString(
|
|
'views.Settings.WalletConfiguration.nostrWalletConnectUrl'
|
|
)}
|
|
</Text>
|
|
<TextInput
|
|
placeholder={'nostr+walletconnect://'}
|
|
textColor={
|
|
nostrWalletConnectUrlError
|
|
? themeColor('error')
|
|
: themeColor('text')
|
|
}
|
|
value={nostrWalletConnectUrl}
|
|
autoCapitalize="none"
|
|
onChangeText={(text: string) =>
|
|
this.setState({
|
|
nostrWalletConnectUrl: text
|
|
.trim()
|
|
.replace(/\s+/g, ' '),
|
|
nostrWalletConnectUrlError:
|
|
!text.startsWith(
|
|
'nostr+walletconnect://'
|
|
),
|
|
saved: false
|
|
})
|
|
}
|
|
locked={loading}
|
|
autoCorrect={false}
|
|
/>
|
|
</>
|
|
)}
|
|
{implementation === 'lndhub' && (
|
|
<>
|
|
<Text
|
|
style={{
|
|
color: themeColor('secondaryText')
|
|
}}
|
|
>
|
|
{localeString(
|
|
'views.Settings.AddEditNode.serverAddress'
|
|
)}
|
|
</Text>
|
|
<TextInput
|
|
placeholder={'https://'}
|
|
textColor={
|
|
lndhubUrlError
|
|
? themeColor('error')
|
|
: themeColor('text')
|
|
}
|
|
value={lndhubUrl}
|
|
autoCorrect={false}
|
|
autoCapitalize="none"
|
|
onChangeText={(text: string) => {
|
|
this.setState({
|
|
lndhubUrlError: false
|
|
});
|
|
|
|
// Allow backspace/delete operations without validation
|
|
if (
|
|
text.length <
|
|
(lndhubUrl?.length || 0)
|
|
) {
|
|
this.setState({
|
|
lndhubUrl: text,
|
|
saved: false
|
|
});
|
|
return;
|
|
}
|
|
|
|
// For single character additions
|
|
if (
|
|
text.length ===
|
|
(lndhubUrl?.length || 0) + 1
|
|
) {
|
|
const cleanedText =
|
|
text.replace(
|
|
new RegExp(
|
|
`[^${SERVER_ADDRESS_CHARS}:/]`,
|
|
'g'
|
|
),
|
|
''
|
|
);
|
|
this.setState({
|
|
lndhubUrl: cleanedText,
|
|
saved: false
|
|
});
|
|
return;
|
|
}
|
|
|
|
// For pasted content
|
|
const trimmedText = text.trim();
|
|
this.setState({
|
|
lndhubUrl: trimmedText,
|
|
lndhubUrlError:
|
|
!ValidationUtils.isValidServerAddress(
|
|
trimmedText,
|
|
{ allowPort: true }
|
|
),
|
|
saved: false
|
|
});
|
|
}}
|
|
onBlur={() => {
|
|
if (lndhubUrl) {
|
|
this.setState({
|
|
lndhubUrlError:
|
|
!ValidationUtils.isValidServerAddress(
|
|
lndhubUrl,
|
|
{ allowPort: true }
|
|
)
|
|
});
|
|
}
|
|
}}
|
|
locked={loading}
|
|
/>
|
|
|
|
<>
|
|
<Text
|
|
style={{
|
|
top: 20,
|
|
color: themeColor(
|
|
'secondaryText'
|
|
)
|
|
}}
|
|
>
|
|
{localeString(
|
|
'views.Settings.AddEditNode.existingAccount'
|
|
)}
|
|
</Text>
|
|
<Switch
|
|
value={existingAccount}
|
|
onValueChange={() =>
|
|
this.setState({
|
|
existingAccount:
|
|
!existingAccount
|
|
})
|
|
}
|
|
/>
|
|
</>
|
|
|
|
{existingAccount && (
|
|
<>
|
|
<Text
|
|
style={{
|
|
color: themeColor(
|
|
'secondaryText'
|
|
)
|
|
}}
|
|
>
|
|
{localeString(
|
|
'views.Settings.AddEditNode.username'
|
|
)}
|
|
</Text>
|
|
<TextInput
|
|
placeholder={'...'}
|
|
textColor={
|
|
usernameError
|
|
? themeColor('error')
|
|
: themeColor('text')
|
|
}
|
|
value={username}
|
|
onFocus={() =>
|
|
this.setState({
|
|
usernameError: false
|
|
})
|
|
}
|
|
onChangeText={(
|
|
text: string
|
|
) => {
|
|
this.setState({
|
|
usernameError: false
|
|
});
|
|
|
|
// Allow backspace/delete operations without validation
|
|
if (
|
|
text.length <
|
|
(username?.length || 0)
|
|
) {
|
|
this.setState({
|
|
username: text,
|
|
saved: false
|
|
});
|
|
return;
|
|
}
|
|
|
|
// For single character additions
|
|
if (
|
|
text.length ===
|
|
(username?.length ||
|
|
0) +
|
|
1
|
|
) {
|
|
const cleanedText =
|
|
text.trim();
|
|
this.setState({
|
|
username:
|
|
cleanedText,
|
|
saved: false
|
|
});
|
|
return;
|
|
}
|
|
|
|
// For pasted content
|
|
const trimmedText =
|
|
text.trim();
|
|
this.setState({
|
|
username: trimmedText,
|
|
usernameError:
|
|
/\s/.test(
|
|
trimmedText
|
|
),
|
|
saved: false
|
|
});
|
|
}}
|
|
locked={loading}
|
|
autoCorrect={false}
|
|
autoCapitalize="none"
|
|
/>
|
|
|
|
<Text
|
|
style={{
|
|
color: themeColor(
|
|
'secondaryText'
|
|
)
|
|
}}
|
|
>
|
|
{localeString(
|
|
'views.Settings.AddEditNode.password'
|
|
)}
|
|
</Text>
|
|
<View
|
|
style={{
|
|
flexDirection: 'row',
|
|
alignItems: 'center'
|
|
}}
|
|
>
|
|
<TextInput
|
|
placeholder={'...'}
|
|
value={password}
|
|
onChangeText={(
|
|
text: string
|
|
) =>
|
|
this.setState({
|
|
password:
|
|
text.trim(),
|
|
saved: false
|
|
})
|
|
}
|
|
locked={loading}
|
|
secureTextEntry={
|
|
this.state.hidden
|
|
}
|
|
autoCorrect={false}
|
|
autoCapitalize="none"
|
|
style={{
|
|
flex: 1,
|
|
marginRight: 15
|
|
}}
|
|
/>
|
|
<ShowHideToggle
|
|
onPress={() =>
|
|
this.setState({
|
|
hidden: !this.state
|
|
.hidden
|
|
})
|
|
}
|
|
/>
|
|
</View>
|
|
|
|
{saved && (
|
|
<CollapsedQR
|
|
showText={localeString(
|
|
'views.Settings.AddEditNode.showAccountQR'
|
|
)}
|
|
collapseText={localeString(
|
|
'views.Settings.AddEditNode.hideAccountQR'
|
|
)}
|
|
value={
|
|
`lndhub://${username}:${password}` +
|
|
`@${lndhubUrl}`
|
|
}
|
|
hideText
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
{(implementation === 'lnd' ||
|
|
implementation === 'cln-rest') && (
|
|
<>
|
|
<Text
|
|
style={{
|
|
color: themeColor('secondaryText')
|
|
}}
|
|
>
|
|
{localeString(
|
|
'views.Settings.AddEditNode.serverAddress'
|
|
)}
|
|
</Text>
|
|
<TextInput
|
|
placeholder={'localhost'}
|
|
textColor={
|
|
hostError
|
|
? themeColor('error')
|
|
: themeColor('text')
|
|
}
|
|
autoCorrect={false}
|
|
autoCapitalize="none"
|
|
value={host}
|
|
onChangeText={(text: string) => {
|
|
this.setState({ hostError: false });
|
|
|
|
// Allow backspace/delete operations without validation
|
|
if (
|
|
text.length <
|
|
(host?.length || 0)
|
|
) {
|
|
this.setState({
|
|
host: text,
|
|
saved: false
|
|
});
|
|
return;
|
|
}
|
|
|
|
// For single character additions
|
|
if (
|
|
text.length ===
|
|
(host?.length || 0) + 1
|
|
) {
|
|
const cleanedText =
|
|
text.replace(
|
|
new RegExp(
|
|
`[^${SERVER_ADDRESS_CHARS}:/]`,
|
|
'g'
|
|
),
|
|
''
|
|
);
|
|
this.setState({
|
|
host: cleanedText,
|
|
saved: false
|
|
});
|
|
return;
|
|
}
|
|
|
|
// For pasted content
|
|
const trimmedText = text.trim();
|
|
this.setState({
|
|
host: trimmedText,
|
|
hostError:
|
|
!ValidationUtils.isValidServerAddress(
|
|
trimmedText
|
|
),
|
|
saved: false
|
|
});
|
|
}}
|
|
onBlur={() => {
|
|
if (host) {
|
|
this.setState({
|
|
hostError:
|
|
!ValidationUtils.isValidServerAddress(
|
|
host
|
|
)
|
|
});
|
|
}
|
|
}}
|
|
locked={loading}
|
|
/>
|
|
|
|
<Text
|
|
style={{
|
|
color: themeColor('secondaryText')
|
|
}}
|
|
>
|
|
{localeString(
|
|
'views.Settings.AddEditNode.restPort'
|
|
)}
|
|
</Text>
|
|
<TextInput
|
|
keyboardType="numeric"
|
|
placeholder={'443/8080'}
|
|
textColor={
|
|
portError
|
|
? themeColor('error')
|
|
: themeColor('text')
|
|
}
|
|
value={port}
|
|
onChangeText={(text: string) => {
|
|
this.setState({ portError: false });
|
|
|
|
// Allow backspace/delete operations without validation
|
|
if (
|
|
text.length <
|
|
(port?.length || 0)
|
|
) {
|
|
this.setState({
|
|
port: text,
|
|
saved: false
|
|
});
|
|
return;
|
|
}
|
|
|
|
// For single character additions
|
|
if (
|
|
text.length ===
|
|
(port?.length || 0) + 1
|
|
) {
|
|
const cleanedText = text
|
|
.replace(/[^0-9]/g, '')
|
|
.replace(/^0+/, '');
|
|
this.setState({
|
|
port: cleanedText,
|
|
saved: false
|
|
});
|
|
return;
|
|
}
|
|
|
|
// For pasted content
|
|
const trimmedText = text.trim();
|
|
this.setState({
|
|
port: trimmedText,
|
|
portError:
|
|
!ValidationUtils.isValidPort(
|
|
trimmedText
|
|
),
|
|
saved: false
|
|
});
|
|
}}
|
|
onBlur={() => {
|
|
if (port) {
|
|
this.setState({
|
|
portError:
|
|
!ValidationUtils.isValidPort(
|
|
port
|
|
)
|
|
});
|
|
}
|
|
}}
|
|
locked={loading}
|
|
/>
|
|
|
|
{implementation === 'cln-rest' ? (
|
|
<>
|
|
<Text
|
|
style={{
|
|
color: themeColor(
|
|
'secondaryText'
|
|
)
|
|
}}
|
|
>
|
|
{localeString(
|
|
'views.Settings.AddEditNode.rune'
|
|
)}
|
|
</Text>
|
|
<View
|
|
style={{
|
|
flexDirection: 'row',
|
|
alignItems: 'center'
|
|
}}
|
|
>
|
|
<TextInput
|
|
placeholder={'Lt1c...'}
|
|
textColor={
|
|
runeError
|
|
? themeColor(
|
|
'error'
|
|
)
|
|
: themeColor('text')
|
|
}
|
|
autoCorrect={false}
|
|
autoCapitalize="none"
|
|
value={rune}
|
|
secureTextEntry={
|
|
this.state.hidden
|
|
}
|
|
style={{
|
|
flex: 1,
|
|
marginRight: 15
|
|
}}
|
|
onChangeText={(
|
|
text: string
|
|
) => {
|
|
this.setState({
|
|
runeError: false
|
|
});
|
|
|
|
// Allow backspace/delete operations without validation
|
|
if (
|
|
text.length <
|
|
(rune?.length || 0)
|
|
) {
|
|
this.setState({
|
|
rune: text,
|
|
saved: false
|
|
});
|
|
return;
|
|
}
|
|
|
|
// For single character additions
|
|
if (
|
|
text.length ===
|
|
(rune?.length ||
|
|
0) +
|
|
1
|
|
) {
|
|
const cleanedText =
|
|
text.replace(
|
|
/[^A-Za-z0-9\-_=]/g,
|
|
''
|
|
);
|
|
this.setState({
|
|
rune: cleanedText,
|
|
saved: false
|
|
});
|
|
return;
|
|
}
|
|
|
|
// For pasted content
|
|
const trimmedText =
|
|
text.trim();
|
|
this.setState({
|
|
rune: trimmedText,
|
|
runeError:
|
|
!ValidationUtils.hasValidRuneChars(
|
|
trimmedText
|
|
),
|
|
saved: false
|
|
});
|
|
}}
|
|
locked={loading}
|
|
/>
|
|
<ShowHideToggle
|
|
onPress={() =>
|
|
this.setState({
|
|
hidden: !this.state
|
|
.hidden
|
|
})
|
|
}
|
|
/>
|
|
</View>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Text
|
|
style={{
|
|
color: themeColor(
|
|
'secondaryText'
|
|
)
|
|
}}
|
|
>
|
|
{localeString(
|
|
'views.Settings.AddEditNode.macaroon'
|
|
)}
|
|
</Text>
|
|
<View
|
|
style={{
|
|
flexDirection: 'row',
|
|
alignItems: 'center'
|
|
}}
|
|
>
|
|
<TextInput
|
|
placeholder={'0A...'}
|
|
textColor={
|
|
macaroonHexError
|
|
? themeColor(
|
|
'error'
|
|
)
|
|
: themeColor('text')
|
|
}
|
|
autoCorrect={false}
|
|
autoCapitalize="none"
|
|
value={macaroonHex}
|
|
secureTextEntry={
|
|
this.state.hidden
|
|
}
|
|
style={{
|
|
flex: 1,
|
|
marginRight: 15
|
|
}}
|
|
onChangeText={(
|
|
text: string
|
|
) => {
|
|
this.setState({
|
|
macaroonHexError:
|
|
false
|
|
});
|
|
|
|
// Allow backspace/delete operations without validation
|
|
if (
|
|
text.length <
|
|
(macaroonHex?.length ||
|
|
0)
|
|
) {
|
|
this.setState({
|
|
macaroonHex:
|
|
text,
|
|
saved: false
|
|
});
|
|
return;
|
|
}
|
|
|
|
// For single character additions
|
|
if (
|
|
text.length ===
|
|
(macaroonHex?.length ||
|
|
0) +
|
|
1
|
|
) {
|
|
const cleanedText =
|
|
text.replace(
|
|
/[^0-9a-fA-F]/g,
|
|
''
|
|
);
|
|
this.setState({
|
|
macaroonHex:
|
|
cleanedText,
|
|
saved: false
|
|
});
|
|
return;
|
|
}
|
|
|
|
// For pasted content
|
|
const trimmedText =
|
|
text.trim();
|
|
this.setState({
|
|
macaroonHex:
|
|
trimmedText,
|
|
macaroonHexError:
|
|
!ValidationUtils.hasValidMacaroonChars(
|
|
trimmedText
|
|
),
|
|
saved: false
|
|
});
|
|
}}
|
|
locked={loading}
|
|
/>
|
|
<ShowHideToggle
|
|
onPress={() =>
|
|
this.setState({
|
|
hidden: !this.state
|
|
.hidden
|
|
})
|
|
}
|
|
/>
|
|
</View>
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{implementation === 'lightning-node-connect' && (
|
|
<>
|
|
<Mailbox />
|
|
{mailboxServer === 'custom-defined' && (
|
|
<>
|
|
<Text
|
|
style={{
|
|
color: themeColor(
|
|
'secondaryText'
|
|
)
|
|
}}
|
|
>
|
|
{localeString(
|
|
'views.Settings.AddEditNode.customMailboxServer'
|
|
)}
|
|
</Text>
|
|
<TextInput
|
|
placeholder={
|
|
'my-custom.lnc.server:443'
|
|
}
|
|
textColor={
|
|
customMailboxServerError
|
|
? themeColor('error')
|
|
: themeColor('text')
|
|
}
|
|
autoCorrect={false}
|
|
autoCapitalize="none"
|
|
value={customMailboxServer}
|
|
onChangeText={(
|
|
text: string
|
|
) => {
|
|
this.setState({
|
|
customMailboxServerError:
|
|
false
|
|
});
|
|
|
|
// Allow backspace/delete operations without validation
|
|
if (
|
|
text.length <
|
|
(customMailboxServer?.length ||
|
|
0)
|
|
) {
|
|
this.setState({
|
|
customMailboxServer:
|
|
text,
|
|
saved: false
|
|
});
|
|
return;
|
|
}
|
|
|
|
// For single character additions
|
|
if (
|
|
text.length ===
|
|
(customMailboxServer?.length ||
|
|
0) +
|
|
1
|
|
) {
|
|
if (text.includes('::'))
|
|
return;
|
|
|
|
const cleanedText =
|
|
text.replace(
|
|
new RegExp(
|
|
`[^${SERVER_ADDRESS_CHARS}:/]`,
|
|
'g'
|
|
),
|
|
''
|
|
);
|
|
this.setState({
|
|
customMailboxServer:
|
|
cleanedText,
|
|
saved: false
|
|
});
|
|
return;
|
|
}
|
|
|
|
// For pasted content
|
|
const trimmedText =
|
|
text.trim();
|
|
this.setState({
|
|
customMailboxServer:
|
|
trimmedText,
|
|
customMailboxServerError:
|
|
!ValidationUtils.isValidServerAddress(
|
|
trimmedText,
|
|
{
|
|
allowPort:
|
|
true,
|
|
requireHttps:
|
|
true
|
|
}
|
|
),
|
|
saved: false
|
|
});
|
|
}}
|
|
onBlur={() => {
|
|
if (customMailboxServer) {
|
|
this.setState({
|
|
customMailboxServerError:
|
|
!ValidationUtils.isValidServerAddress(
|
|
customMailboxServer,
|
|
{
|
|
allowPort:
|
|
true,
|
|
requireHttps:
|
|
true
|
|
}
|
|
)
|
|
});
|
|
}
|
|
}}
|
|
locked={loading}
|
|
/>
|
|
</>
|
|
)}
|
|
<Text
|
|
style={{
|
|
color: themeColor('secondaryText')
|
|
}}
|
|
>
|
|
{localeString(
|
|
'views.Settings.AddEditNode.pairingPhrase'
|
|
)}
|
|
</Text>
|
|
<View
|
|
style={{
|
|
flexDirection: 'row',
|
|
alignItems: 'center'
|
|
}}
|
|
>
|
|
<TextInput
|
|
placeholder={
|
|
'cherry truth mask employ box silver mass bunker fiscal vote'
|
|
}
|
|
textColor={
|
|
pairingPhraseError
|
|
? themeColor('error')
|
|
: themeColor('text')
|
|
}
|
|
autoCapitalize="none"
|
|
value={pairingPhrase}
|
|
secureTextEntry={this.state.hidden}
|
|
style={{
|
|
flex: 1,
|
|
marginRight: 15
|
|
}}
|
|
onChangeText={(text: string) => {
|
|
this.setState({
|
|
pairingPhraseError: false
|
|
});
|
|
|
|
// Allow backspace/delete operations without validation
|
|
if (
|
|
text.length <
|
|
(pairingPhrase?.length || 0)
|
|
) {
|
|
this.setState({
|
|
pairingPhrase: text,
|
|
saved: false
|
|
});
|
|
return;
|
|
}
|
|
|
|
// For single character additions
|
|
if (
|
|
text.length ===
|
|
(pairingPhrase?.length ||
|
|
0) +
|
|
1
|
|
) {
|
|
if (text === ' ') return;
|
|
if (text.includes(' '))
|
|
return;
|
|
|
|
const cleanedText =
|
|
text.replace(
|
|
/[^a-zA-Z\s]/g,
|
|
''
|
|
);
|
|
this.setState({
|
|
pairingPhrase:
|
|
cleanedText,
|
|
saved: false
|
|
});
|
|
return;
|
|
}
|
|
|
|
// For pasted content
|
|
const normalizedPhrase = text
|
|
.trim()
|
|
.replace(/\s+/g, ' ');
|
|
this.setState({
|
|
pairingPhrase:
|
|
normalizedPhrase,
|
|
pairingPhraseError:
|
|
!ValidationUtils.hasValidPairingPhraseCharsAndWordcount(
|
|
normalizedPhrase
|
|
),
|
|
saved: false
|
|
});
|
|
}}
|
|
onBlur={() => {
|
|
const normalizedPhrase =
|
|
pairingPhrase
|
|
?.trim()
|
|
.replace(/\s+/g, ' ');
|
|
this.setState({
|
|
pairingPhrase:
|
|
normalizedPhrase,
|
|
pairingPhraseError:
|
|
!ValidationUtils.hasValidPairingPhraseCharsAndWordcount(
|
|
normalizedPhrase
|
|
),
|
|
saved: false
|
|
});
|
|
}}
|
|
locked={loading}
|
|
/>
|
|
<ShowHideToggle
|
|
onPress={() =>
|
|
this.setState({
|
|
hidden: !this.state.hidden
|
|
})
|
|
}
|
|
/>
|
|
</View>
|
|
|
|
{!!localKey && (
|
|
<>
|
|
<Text
|
|
style={{
|
|
color: themeColor(
|
|
'secondaryText'
|
|
)
|
|
}}
|
|
>
|
|
🔒{' '}
|
|
{localeString(
|
|
'views.Settings.AddEditNode.localKey'
|
|
)}
|
|
</Text>
|
|
<TextInput
|
|
value={localKey}
|
|
locked={true}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
{!!remoteKey && (
|
|
<>
|
|
<Text
|
|
style={{
|
|
color: themeColor(
|
|
'secondaryText'
|
|
)
|
|
}}
|
|
>
|
|
🔒{' '}
|
|
{localeString(
|
|
'views.Settings.AddEditNode.remoteKey'
|
|
)}
|
|
</Text>
|
|
<TextInput
|
|
value={remoteKey}
|
|
locked={true}
|
|
/>
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{supportsTor && (
|
|
<>
|
|
<Text
|
|
style={{
|
|
top: 20,
|
|
color: themeColor('secondaryText')
|
|
}}
|
|
>
|
|
{localeString(
|
|
'views.Settings.AddEditNode.useTor'
|
|
)}
|
|
</Text>
|
|
<Switch
|
|
value={enableTor}
|
|
onValueChange={() =>
|
|
this.setState({
|
|
enableTor: !enableTor,
|
|
saved: false
|
|
})
|
|
}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
{supportsCertVerification && !enableTor && (
|
|
<>
|
|
<Text
|
|
style={{
|
|
top: 20,
|
|
color: themeColor('secondaryText')
|
|
}}
|
|
>
|
|
{localeString(
|
|
'views.Settings.AddEditNode.certificateVerification'
|
|
)}
|
|
</Text>
|
|
<Switch
|
|
value={certVerification}
|
|
onValueChange={() =>
|
|
this.setState({
|
|
certVerification:
|
|
!certVerification,
|
|
saved: false
|
|
})
|
|
}
|
|
/>
|
|
</>
|
|
)}
|
|
</View>
|
|
|
|
{implementation === 'ldk-node' && (
|
|
<View style={{ ...styles.button }}>
|
|
{!ldkNodeInitialized &&
|
|
!creatingWallet &&
|
|
!saved && (
|
|
<>
|
|
<View style={styles.button}>
|
|
<Button
|
|
title={localeString(
|
|
'views.Settings.NodeConfiguration.createWallet',
|
|
{
|
|
network:
|
|
localeString(
|
|
EMBEDDED_NODE_NETWORK_KEYS.find(
|
|
(k) =>
|
|
k.value ===
|
|
(ldkNetwork ||
|
|
'mainnet')
|
|
)
|
|
?.translateKey ||
|
|
'network.mainnet'
|
|
)
|
|
}
|
|
)}
|
|
onPress={async () => {
|
|
await this.createLdkNodeNewWallet(
|
|
ldkNetwork ||
|
|
'mainnet'
|
|
);
|
|
}}
|
|
tertiary
|
|
disabled={
|
|
loading ||
|
|
!isVssServerValid(
|
|
ldkVssServerSelected,
|
|
ldkVssServerCustom
|
|
)
|
|
}
|
|
/>
|
|
</View>
|
|
<View style={styles.button}>
|
|
<Button
|
|
title={localeString(
|
|
'views.Settings.NodeConfiguration.restoreWallet',
|
|
{
|
|
network:
|
|
localeString(
|
|
EMBEDDED_NODE_NETWORK_KEYS.find(
|
|
(k) =>
|
|
k.value ===
|
|
(ldkNetwork ||
|
|
'mainnet')
|
|
)
|
|
?.translateKey ||
|
|
'network.mainnet'
|
|
)
|
|
}
|
|
)}
|
|
onPress={() =>
|
|
navigation.navigate(
|
|
'LdkWalletRecoverySettings',
|
|
{
|
|
network:
|
|
ldkNetwork,
|
|
nickname,
|
|
photo
|
|
}
|
|
)
|
|
}
|
|
secondary
|
|
disabled={loading}
|
|
/>
|
|
</View>
|
|
</>
|
|
)}
|
|
</View>
|
|
)}
|
|
|
|
{!existingAccount && implementation === 'lndhub' && (
|
|
<View style={{ ...styles.button }}>
|
|
<Button
|
|
title={localeString(
|
|
'views.Settings.AddEditNode.createLndhub'
|
|
)}
|
|
onPress={() => {
|
|
if (
|
|
CUSTODIAL_LNDHUBS.includes(
|
|
lndhubUrl
|
|
)
|
|
) {
|
|
this.setState({
|
|
showLndHubModal: true
|
|
});
|
|
} else {
|
|
createAccount(
|
|
lndhubUrl,
|
|
certVerification,
|
|
enableTor
|
|
).then((data: any) => {
|
|
if (data) {
|
|
this.setState({
|
|
username: data.login,
|
|
password: data.password,
|
|
existingAccount: true
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}}
|
|
disabled={loading || !lndhubUrl}
|
|
/>
|
|
</View>
|
|
)}
|
|
|
|
{implementation === 'embedded-lnd' && adminMacaroon && (
|
|
<KeyValue
|
|
keyValue={localeString('general.network')}
|
|
value={embeddedLndNetwork}
|
|
/>
|
|
)}
|
|
|
|
{implementation === 'embedded-lnd' && adminMacaroon && (
|
|
<KeyValue
|
|
keyValue={localeString(
|
|
'views.Settings.WalletConfiguration.database'
|
|
)}
|
|
value={
|
|
isSqlite
|
|
? `SQLite (${localeString(
|
|
'general.experimental'
|
|
)})`
|
|
: 'Bolt'
|
|
}
|
|
/>
|
|
)}
|
|
|
|
{implementation === 'embedded-lnd' &&
|
|
recoveryCipherSeed && (
|
|
<WarningMessage
|
|
message={localeString(
|
|
'views.Settings.NodeConfiguration.restoreWarning'
|
|
)}
|
|
/>
|
|
)}
|
|
|
|
{active && (
|
|
<View style={{ alignItems: 'center' }}>
|
|
<Pill
|
|
title={localeString(
|
|
'views.Settings.WalletConfiguration.walletActive'
|
|
)}
|
|
backgroundColor="transparent"
|
|
width={'100%'}
|
|
/>
|
|
</View>
|
|
)}
|
|
|
|
{((implementation === 'embedded-lnd' &&
|
|
adminMacaroon &&
|
|
seedPhrase) ||
|
|
(implementation === 'ldk-node' &&
|
|
ldkNodeInitialized &&
|
|
ldkMnemonic)) && (
|
|
<View style={styles.button}>
|
|
<Button
|
|
title={localeString(
|
|
'views.Settings.NodeConfiguration.backUpWallet'
|
|
)}
|
|
onPress={() =>
|
|
navigation.navigate(
|
|
'Seed',
|
|
implementation === 'ldk-node'
|
|
? {
|
|
implementation: 'ldk-node'
|
|
}
|
|
: undefined
|
|
)
|
|
}
|
|
secondary
|
|
disabled={loading}
|
|
/>
|
|
</View>
|
|
)}
|
|
|
|
{implementation === 'embedded-lnd' && (
|
|
<View style={{ ...styles.button }}>
|
|
{!adminMacaroon && !creatingWallet && (
|
|
<>
|
|
<View style={styles.button}>
|
|
<Button
|
|
title={localeString(
|
|
'views.Settings.NodeConfiguration.createWallet',
|
|
{
|
|
network: localeString(
|
|
EMBEDDED_NODE_NETWORK_KEYS.find(
|
|
(k) =>
|
|
k.value ===
|
|
(embeddedLndNetwork ||
|
|
'mainnet')
|
|
)?.translateKey ||
|
|
'network.mainnet'
|
|
)
|
|
}
|
|
)}
|
|
onPress={async () => {
|
|
await this.createNewWallet(
|
|
embeddedLndNetwork ===
|
|
'mainnet'
|
|
? undefined
|
|
: 'Testnet'
|
|
);
|
|
}}
|
|
tertiary
|
|
disabled={loading}
|
|
/>
|
|
</View>
|
|
<View style={styles.button}>
|
|
<Button
|
|
title={localeString(
|
|
'views.Settings.NodeConfiguration.restoreWallet',
|
|
{
|
|
network: localeString(
|
|
EMBEDDED_NODE_NETWORK_KEYS.find(
|
|
(k) =>
|
|
k.value ===
|
|
(embeddedLndNetwork ||
|
|
'mainnet')
|
|
)?.translateKey ||
|
|
'network.mainnet'
|
|
)
|
|
}
|
|
)}
|
|
onPress={() =>
|
|
navigation.navigate(
|
|
'SeedRecovery',
|
|
{
|
|
network:
|
|
embeddedLndNetwork,
|
|
nickname,
|
|
photo,
|
|
isSqlite:
|
|
isSqlite ??
|
|
false
|
|
}
|
|
)
|
|
}
|
|
secondary
|
|
disabled={loading}
|
|
/>
|
|
</View>
|
|
</>
|
|
)}
|
|
</View>
|
|
)}
|
|
|
|
{!creatingWallet &&
|
|
!(
|
|
(implementation === 'embedded-lnd' &&
|
|
!adminMacaroon) ||
|
|
(implementation === 'ldk-node' &&
|
|
!ldkNodeInitialized)
|
|
) &&
|
|
!saved && (
|
|
<View style={{ ...styles.button }}>
|
|
<Button
|
|
title={localeString(
|
|
'views.Settings.WalletConfiguration.saveWallet'
|
|
)}
|
|
onPress={() => {
|
|
if (
|
|
!saved &&
|
|
!certVerification &&
|
|
!enableTor &&
|
|
!isLocalImpl &&
|
|
implementation !==
|
|
'lightning-node-connect' &&
|
|
implementation !==
|
|
'nostr-wallet-connect'
|
|
) {
|
|
this.setState({
|
|
showCertModal: true
|
|
});
|
|
} else {
|
|
this.saveWalletConfiguration();
|
|
}
|
|
}}
|
|
// disable save button if loading, required input missing or input invalid
|
|
disabled={
|
|
loading ||
|
|
hostError ||
|
|
(host &&
|
|
!ValidationUtils.isValidServerAddress(
|
|
host
|
|
)) ||
|
|
portError ||
|
|
(port &&
|
|
!ValidationUtils.isValidPort(
|
|
port
|
|
)) ||
|
|
macaroonHexError ||
|
|
(macaroonHex &&
|
|
!ValidationUtils.hasValidMacaroonChars(
|
|
macaroonHex
|
|
)) ||
|
|
runeError ||
|
|
(rune &&
|
|
!ValidationUtils.hasValidRuneChars(
|
|
rune
|
|
)) ||
|
|
lndhubUrlError ||
|
|
(lndhubUrl &&
|
|
!ValidationUtils.isValidServerAddress(
|
|
lndhubUrl,
|
|
{ allowPort: true }
|
|
)) ||
|
|
customMailboxServerError ||
|
|
(customMailboxServer &&
|
|
!ValidationUtils.isValidServerAddress(
|
|
customMailboxServer,
|
|
{
|
|
allowPort: true,
|
|
requireHttps: true
|
|
}
|
|
)) ||
|
|
pairingPhraseError ||
|
|
(pairingPhrase &&
|
|
!ValidationUtils.hasValidPairingPhraseCharsAndWordcount(
|
|
pairingPhrase
|
|
)) ||
|
|
nostrWalletConnectUrlError ||
|
|
// Required input check
|
|
// Port is optional, it will fallback to 80 or 443
|
|
(implementation === 'lndhub' &&
|
|
!(
|
|
lndhubUrl &&
|
|
username &&
|
|
password
|
|
)) ||
|
|
(implementation === 'lnd' &&
|
|
!(host && macaroonHex)) ||
|
|
(implementation === 'cln-rest' &&
|
|
!(host && rune)) ||
|
|
(implementation ===
|
|
'lightning-node-connect' &&
|
|
(!pairingPhrase ||
|
|
(mailboxServer ===
|
|
'custom-defined' &&
|
|
!customMailboxServer)))
|
|
}
|
|
/>
|
|
</View>
|
|
)}
|
|
|
|
{creatingWallet && <LoadingIndicator />}
|
|
|
|
{errorCreatingWallet && (
|
|
<ErrorMessage
|
|
message={localeString(
|
|
'views.Intro.errorCreatingWallet'
|
|
)}
|
|
/>
|
|
)}
|
|
|
|
{!saved && certVerification && !enableTor && (
|
|
<CertInstallInstructions />
|
|
)}
|
|
|
|
{saved && !active && !newEntry && (
|
|
<View style={styles.button}>
|
|
<Button
|
|
title={localeString(
|
|
'views.Settings.WalletConfiguration.setWalletActive'
|
|
)}
|
|
onPress={() =>
|
|
this.setWalletConfigurationAsActive()
|
|
}
|
|
disabled={loading}
|
|
/>
|
|
</View>
|
|
)}
|
|
|
|
{saved &&
|
|
implementation !== 'embedded-lnd' &&
|
|
implementation !== 'ldk-node' && (
|
|
<View style={styles.button}>
|
|
<Button
|
|
title={localeString(
|
|
'views.Settings.WalletConfiguration.duplicateWallet'
|
|
)}
|
|
onPress={() => {
|
|
/**
|
|
* Scrolls to the top of the screen when going to the node config
|
|
* page for the copied node. Without this, the user would have to
|
|
* manually scroll to the top to edit the copied node properties.
|
|
*/
|
|
this.scrollViewRef.current?.scrollTo(
|
|
{
|
|
x: 0,
|
|
y: 0,
|
|
animated: true
|
|
}
|
|
);
|
|
this.copyNodeConfig();
|
|
}}
|
|
secondary
|
|
disabled={loading}
|
|
/>
|
|
</View>
|
|
)}
|
|
|
|
{saved && (
|
|
<View style={styles.button}>
|
|
<Button
|
|
title={localeString(
|
|
'views.Settings.WalletConfiguration.deleteWallet'
|
|
)}
|
|
onPress={this.handleDeletePress}
|
|
warning
|
|
disabled={loading}
|
|
/>
|
|
</View>
|
|
)}
|
|
|
|
{implementation === 'embedded-lnd' && !newEntry && (
|
|
<>
|
|
<Text
|
|
style={{
|
|
color: themeColor('text'),
|
|
textAlign: 'center',
|
|
marginTop: 20
|
|
}}
|
|
>
|
|
{localeString(
|
|
'views.Settings.NodeConfiguration.embeddedDelete2'
|
|
)}
|
|
</Text>
|
|
</>
|
|
)}
|
|
</ScrollView>
|
|
</KeyboardAvoidingView>
|
|
</Screen>
|
|
);
|
|
}
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
text: {
|
|
fontFamily: 'PPNeueMontreal-Book'
|
|
},
|
|
whiteText: {
|
|
color: 'white',
|
|
fontFamily: 'PPNeueMontreal-Book'
|
|
},
|
|
blackText: {
|
|
color: 'black',
|
|
fontFamily: 'PPNeueMontreal-Book'
|
|
},
|
|
form: {
|
|
marginTop: 5,
|
|
width: '100%'
|
|
},
|
|
pickerWrapper: {
|
|
paddingLeft: 5,
|
|
paddingRight: 5
|
|
},
|
|
picker: {
|
|
height: 50
|
|
},
|
|
button: {
|
|
paddingVertical: 10,
|
|
width: 350,
|
|
alignSelf: 'center'
|
|
},
|
|
modal: {
|
|
margin: 20,
|
|
backgroundColor: 'white',
|
|
borderRadius: 20,
|
|
padding: 35,
|
|
alignItems: 'center',
|
|
shadowColor: '#000',
|
|
shadowOffset: {
|
|
width: 0,
|
|
height: 2
|
|
},
|
|
shadowOpacity: 0.25,
|
|
shadowRadius: 3.84,
|
|
elevation: 5
|
|
},
|
|
centeredView: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
marginTop: 22
|
|
},
|
|
container: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center'
|
|
},
|
|
imageBackground: {
|
|
width: 50,
|
|
height: 50,
|
|
borderRadius: 25,
|
|
justifyContent: 'center'
|
|
},
|
|
photo: {
|
|
alignSelf: 'center',
|
|
width: 128,
|
|
height: 128,
|
|
borderRadius: 68
|
|
}
|
|
});
|