diff --git a/App.tsx b/App.tsx
index 6a5c64a8c..8cce97358 100644
--- a/App.tsx
+++ b/App.tsx
@@ -223,9 +223,11 @@ import WalletSettings from './views/Onboarding/WalletSettings';
import EditFee from './views/EditFee';
-// Embedded LND
+// Embedded LND/LDK
import Seed from './views/Settings/Seed';
import SeedRecovery from './views/Settings/SeedRecovery';
+import LdkWalletRecoverySettings from './views/Settings/LdkRecovery/LdkWalletRecoverySettings';
+import LdkRecoveryVssServer from './views/Settings/LdkRecovery/LdkRecoveryVssServer';
import SeedQRExport from './views/Settings/SeedQRExport';
import Sync from './views/Sync';
import SyncRecovery from './views/SyncRecovery';
@@ -1085,6 +1087,18 @@ export default class App extends React.PureComponent {
SeedRecovery
}
/>
+
+
void;
+ locked?: boolean;
+}
+
+export const resolveVssServer = (
+ selectedValue: string,
+ customServer: string
+): string =>
+ selectedValue === VSS_CUSTOM_VALUE ? customServer.trim() : selectedValue;
+
+export const isVssServerValid = (
+ selectedValue: string,
+ customServer: string
+): boolean => {
+ if (selectedValue !== VSS_CUSTOM_VALUE) return true;
+ const trimmed = customServer.trim();
+ return trimmed !== '' && UrlUtils.isValidUrl(trimmed);
+};
+
+export default function VssServerPicker({
+ selectedValue,
+ customServer,
+ onChange,
+ locked
+}: VssServerPickerProps) {
+ const isCustom = selectedValue === VSS_CUSTOM_VALUE;
+ const showInvalidUrlError =
+ isCustom &&
+ customServer.trim() !== '' &&
+ !UrlUtils.isValidUrl(customServer);
+
+ return (
+
+
+ onChange(
+ value,
+ value === VSS_CUSTOM_VALUE ? customServer : ''
+ )
+ }
+ values={LDK_VSS_SERVER_KEYS}
+ disabled={locked}
+ />
+
+ {isCustom && (
+ <>
+
+ onChange(selectedValue, text)
+ }
+ autoCapitalize="none"
+ autoCorrect={false}
+ locked={locked}
+ />
+ {showInvalidUrlError && (
+
+ {localeString(
+ 'views.Settings.EmbeddedNode.invalidServerUrl'
+ )}
+
+ )}
+ >
+ )}
+
+ );
+}
diff --git a/locales/en.json b/locales/en.json
index b764cae68..dfd7df33a 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -359,6 +359,9 @@
"views.Settings.AddEditNode.certificateVerification": "Certificate Verification",
"views.Settings.AddEditNode.createLndhub": "Create LNDHub account",
"views.Settings.WalletConfiguration.database": "Database",
+ "views.Settings.WalletConfiguration.seedPhraseLength": "Seed phrase length",
+ "views.Settings.WalletConfiguration.seedPhraseLength.12": "12 words",
+ "views.Settings.WalletConfiguration.seedPhraseLength.24": "24 words",
"views.Settings.WalletConfiguration.saveWallet": "Save Wallet Config",
"views.Settings.WalletConfiguration.setWalletActive": "Set Wallet Config as Active",
"views.Settings.WalletConfiguration.walletActive": "Wallet Active",
@@ -1387,6 +1390,10 @@
"views.Settings.SeedRecovery.clipboardSeedWords": "Seed words detected in clipboard",
"views.Settings.SeedRecovery.clipboardSeedWordsPrompt": "Would you like to use them for wallet recovery?",
"views.Settings.SeedRecovery.duplicateWallet": "A wallet with the same seed phrase already exists",
+ "views.Settings.SeedRecovery.invalidChecksum": "Invalid seed phrase — checksum failed. Please check the word order and spelling.",
+ "views.Settings.LdkWalletRecoverySettings.title": "Recovery options",
+ "views.Settings.LdkWalletRecoverySettings.seedPhraseLength": "How many words is your seed phrase?",
+ "views.Settings.LdkWalletRecoverySettings.seedPhraseLength.subtitle": "Most wallets use 12 words. Choose 24 only if your backup has 24 words.",
"views.Settings.SeedQRExport.title": "Export HD node root keys",
"views.Settings.SeedQRExport.pleaseWait": "Generating yprv and zprv. This will take just a few moments.",
"views.Settings.SeedQRExport.warning": "WARNING: DO NOT bump a channel opening transaction. This can cause you to lose funds!",
diff --git a/stores/CashuStore.ts b/stores/CashuStore.ts
index a1d3750bf..0899cc52b 100644
--- a/stores/CashuStore.ts
+++ b/stores/CashuStore.ts
@@ -491,9 +491,22 @@ export default class CashuStore {
let cashuSeedPhrase: string;
if (ldkMnemonic) {
- // LDK Node uses a 12-word mnemonic - use it directly as the
- // cashu seed since it already has exactly 128 bits of entropy
- cashuSeedPhrase = ldkMnemonic;
+ const ldkWordCount = ldkMnemonic.trim().split(/\s+/).length;
+ if (ldkWordCount === 12) {
+ // A 12-word LDK mnemonic already has exactly 128 bits of
+ // entropy - use it directly as the cashu seed
+ cashuSeedPhrase = ldkMnemonic;
+ } else {
+ // Larger LDK mnemonics (e.g. 24 words) derive a 12-word cashu
+ // seed from bytes [48:64] of the BIP-39 seed (v2-bip39 style)
+ const seedFromMnemonic =
+ bip39scure.mnemonicToSeedSync(ldkMnemonic);
+ const entropy = seedFromMnemonic.slice(48, 64);
+ cashuSeedPhrase = bip39scure.entropyToMnemonic(
+ entropy,
+ BIP39_WORD_LIST
+ );
+ }
} else if (lndSeedPhrase && lndSeedPhrase.length > 0) {
// LND uses a 24-word mnemonic - derive a 12-word cashu seed
// from bytes [48:64] of the BIP-39 seed (v2-bip39 style)
diff --git a/stores/SettingsStore.ts b/stores/SettingsStore.ts
index 9ce8e201d..0e4e302db 100644
--- a/stores/SettingsStore.ts
+++ b/stores/SettingsStore.ts
@@ -427,6 +427,11 @@ export const EMBEDDED_NODE_NETWORK_KEYS = [
}
];
+export const LDK_VSS_SERVER_KEYS = [
+ { key: 'ZEUS', value: DEFAULT_VSS_SERVER },
+ { key: 'Custom', translateKey: 'general.custom', value: 'custom' }
+];
+
export const LNC_MAILBOX_KEYS = [
{
key: 'mailbox.terminal.lightning.today:443',
diff --git a/stores/SwapStore.ts b/stores/SwapStore.ts
index 941fc225b..2679f7f0b 100644
--- a/stores/SwapStore.ts
+++ b/stores/SwapStore.ts
@@ -777,13 +777,17 @@ export default class SwapStore {
@action
public generateRescueKey = async () => {
console.log('GENERATING RESCUE FILE...');
- // LDK Node already has a 12-word BIP-39 mnemonic - reuse it
- // instead of generating a separate rescue key
+ // A 12-word LDK Node wallet already has a 12-word BIP-39 mnemonic -
+ // reuse it instead of generating a separate rescue key. 24-word LDK
+ // wallets (and every other backend) get an independent 12-word key.
const { implementation, ldkMnemonic } = this.settingsStore;
- const mnemonic =
- implementation === 'ldk-node' && ldkMnemonic
- ? ldkMnemonic
- : generateMnemonic(BIP39_WORD_LIST);
+ const reuseLdkSeed =
+ implementation === 'ldk-node' &&
+ !!ldkMnemonic &&
+ ldkMnemonic.trim().split(/\s+/).length === 12;
+ const mnemonic = reuseLdkSeed
+ ? ldkMnemonic
+ : generateMnemonic(BIP39_WORD_LIST);
await Storage.setItem(SWAPS_RESCUE_KEY, mnemonic);
return mnemonic;
diff --git a/utils/LdkNodeUtils.ts b/utils/LdkNodeUtils.ts
index 8a5a18471..c8926b278 100644
--- a/utils/LdkNodeUtils.ts
+++ b/utils/LdkNodeUtils.ts
@@ -243,6 +243,7 @@ async function initNode({
export async function createLdkNodeWallet({
nodeDir,
seedMnemonic,
+ wordCount = 12,
passphrase,
network,
esploraServerUrl,
@@ -255,6 +256,7 @@ export async function createLdkNodeWallet({
}: {
nodeDir: string;
seedMnemonic?: string;
+ wordCount?: number;
passphrase?: string;
network: SupportedNetwork;
esploraServerUrl?: string;
@@ -279,7 +281,7 @@ export async function createLdkNodeWallet({
// Generate mnemonic if not provided (new wallet)
let mnemonic = seedMnemonic;
if (!mnemonic) {
- mnemonic = await generateMnemonic(12);
+ mnemonic = await generateMnemonic(wordCount);
}
const { vssError } = await initNode({
diff --git a/views/Settings/EmbeddedNode/VssServer.tsx b/views/Settings/EmbeddedNode/VssServer.tsx
index b6fc69c8f..9078f9a02 100644
--- a/views/Settings/EmbeddedNode/VssServer.tsx
+++ b/views/Settings/EmbeddedNode/VssServer.tsx
@@ -6,14 +6,19 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import Button from '../../../components/Button';
import Header from '../../../components/Header';
import Screen from '../../../components/Screen';
-import TextInput from '../../../components/TextInput';
+import VssServerPicker, {
+ VSS_CUSTOM_VALUE,
+ resolveVssServer,
+ isVssServerValid
+} from '../../../components/VssServerPicker';
-import SettingsStore from '../../../stores/SettingsStore';
+import SettingsStore, {
+ LDK_VSS_SERVER_KEYS
+} from '../../../stores/SettingsStore';
import { localeString } from '../../../utils/LocaleUtils';
import { restartNeeded } from '../../../utils/RestartUtils';
import { themeColor } from '../../../utils/ThemeUtils';
-import UrlUtils from '../../../utils/UrlUtils';
import { DEFAULT_VSS_SERVER } from '../../../utils/LdkNodeUtils';
interface VssServerProps {
@@ -22,7 +27,8 @@ interface VssServerProps {
}
interface VssServerState {
- vssServer: string;
+ selectedValue: string;
+ customServer: string;
savedVssServer: string;
}
@@ -35,9 +41,15 @@ export default class VssServer extends React.Component<
state = (() => {
const { settings } = this.props.SettingsStore;
const selectedNode = settings.selectedNode || 0;
- const saved = settings.nodes?.[selectedNode]?.ldkVssServer || '';
+ const saved =
+ settings.nodes?.[selectedNode]?.ldkVssServer || DEFAULT_VSS_SERVER;
+
+ const presetValues = LDK_VSS_SERVER_KEYS.map((s) => s.value);
+ const isPreset = presetValues.includes(saved);
+
return {
- vssServer: saved,
+ selectedValue: isPreset ? saved : VSS_CUSTOM_VALUE,
+ customServer: isPreset ? '' : saved,
savedVssServer: saved
};
})();
@@ -60,16 +72,15 @@ export default class VssServer extends React.Component<
render() {
const { navigation } = this.props;
- const { vssServer, savedVssServer } = this.state;
-
- const showReset = vssServer !== DEFAULT_VSS_SERVER;
-
- const vssServerTrimmed = vssServer.trim();
- const showInvalidUrlError =
- vssServerTrimmed !== '' && !UrlUtils.isValidUrl(vssServer);
+ const { selectedValue, customServer, savedVssServer } = this.state;
+ const effectiveServer = resolveVssServer(selectedValue, customServer);
+ const isValid = isVssServerValid(selectedValue, customServer);
const hasUnsavedChanges =
- vssServer !== savedVssServer && !showInvalidUrlError;
+ isValid &&
+ effectiveServer !== '' &&
+ effectiveServer !== savedVssServer;
+ const showReset = effectiveServer !== DEFAULT_VSS_SERVER;
return (
@@ -102,43 +113,17 @@ export default class VssServer extends React.Component<
-
- {localeString(
- 'views.Settings.EmbeddedNode.VssServer.serverUrl'
- )}
-
- {
+
this.setState({
- vssServer: text
- });
- }}
- autoCapitalize="none"
- autoCorrect={false}
+ selectedValue: value,
+ customServer: custom
+ })
+ }
/>
- {showInvalidUrlError && (
-
- {localeString(
- 'views.Settings.EmbeddedNode.invalidServerUrl'
- )}
-
- )}
-
this.saveSettings(vssServer)}
+ onPress={() =>
+ this.saveSettings(effectiveServer)
+ }
/>
)}
@@ -175,7 +162,8 @@ export default class VssServer extends React.Component<
secondary
onPress={async () => {
this.setState({
- vssServer: DEFAULT_VSS_SERVER
+ selectedValue: DEFAULT_VSS_SERVER,
+ customServer: ''
});
await this.saveSettings(
DEFAULT_VSS_SERVER
diff --git a/views/Settings/LdkRecovery/LdkRecoveryVssServer.tsx b/views/Settings/LdkRecovery/LdkRecoveryVssServer.tsx
new file mode 100644
index 000000000..6b64c27cd
--- /dev/null
+++ b/views/Settings/LdkRecovery/LdkRecoveryVssServer.tsx
@@ -0,0 +1,133 @@
+import * as React from 'react';
+import { View, StyleSheet } from 'react-native';
+import { NativeStackNavigationProp } from '@react-navigation/native-stack';
+import { Route } from '@react-navigation/native';
+
+import Button from '../../../components/Button';
+import Header from '../../../components/Header';
+import Screen from '../../../components/Screen';
+import Text from '../../../components/Text';
+import VssServerPicker, {
+ resolveVssServer,
+ isVssServerValid
+} from '../../../components/VssServerPicker';
+
+import { themeColor } from '../../../utils/ThemeUtils';
+import { localeString } from '../../../utils/LocaleUtils';
+import { DEFAULT_VSS_SERVER } from '../../../utils/LdkNodeUtils';
+
+interface LdkRecoveryVssServerProps {
+ navigation: NativeStackNavigationProp;
+ route: Route<
+ 'LdkRecoveryVssServer',
+ {
+ network: string;
+ nickname?: string;
+ photo?: string;
+ wordCount: 12 | 24;
+ }
+ >;
+}
+
+interface LdkRecoveryVssServerState {
+ selectedValue: string;
+ customServer: string;
+}
+
+export default class LdkRecoveryVssServer extends React.Component<
+ LdkRecoveryVssServerProps,
+ LdkRecoveryVssServerState
+> {
+ state = {
+ selectedValue: DEFAULT_VSS_SERVER,
+ customServer: ''
+ };
+
+ private continueToRecovery = () => {
+ const { navigation, route } = this.props;
+ const { network, nickname, photo, wordCount } = route.params ?? {};
+ const { selectedValue, customServer } = this.state;
+
+ navigation.navigate('SeedRecovery', {
+ network,
+ implementation: 'ldk-node',
+ nickname,
+ photo,
+ wordCount,
+ vssServer:
+ resolveVssServer(selectedValue, customServer) || undefined
+ });
+ };
+
+ render() {
+ const { navigation } = this.props;
+ const { selectedValue, customServer } = this.state;
+
+ return (
+
+
+
+
+ {localeString(
+ 'views.Settings.EmbeddedNode.VssServer.subtitle'
+ )}
+
+
+
+ this.setState({
+ selectedValue: value,
+ customServer: custom
+ })
+ }
+ />
+
+
+
+
+
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ content: {
+ flex: 1,
+ paddingHorizontal: 20,
+ paddingTop: 20
+ },
+ subtitle: {
+ fontFamily: 'PPNeueMontreal-Book',
+ fontSize: 15,
+ marginBottom: 24
+ },
+ button: {
+ marginTop: 24
+ }
+});
diff --git a/views/Settings/LdkRecovery/LdkWalletRecoverySettings.tsx b/views/Settings/LdkRecovery/LdkWalletRecoverySettings.tsx
new file mode 100644
index 000000000..64ce5cfcf
--- /dev/null
+++ b/views/Settings/LdkRecovery/LdkWalletRecoverySettings.tsx
@@ -0,0 +1,126 @@
+import * as React from 'react';
+import { View, StyleSheet } from 'react-native';
+import { NativeStackNavigationProp } from '@react-navigation/native-stack';
+import { Route } from '@react-navigation/native';
+
+import Button from '../../../components/Button';
+import Header from '../../../components/Header';
+import Screen from '../../../components/Screen';
+import Text from '../../../components/Text';
+
+import { themeColor } from '../../../utils/ThemeUtils';
+import { localeString } from '../../../utils/LocaleUtils';
+
+interface LdkWalletRecoverySettingsProps {
+ navigation: NativeStackNavigationProp;
+ route: Route<
+ 'LdkWalletRecoverySettings',
+ {
+ network: string;
+ nickname?: string;
+ photo?: string;
+ }
+ >;
+}
+
+export default class LdkWalletRecoverySettings extends React.Component<
+ LdkWalletRecoverySettingsProps,
+ {}
+> {
+ private continueToVssServer = (wordCount: 12 | 24) => {
+ const { navigation, route } = this.props;
+ const { network, nickname, photo } = route.params ?? {};
+
+ navigation.navigate('LdkRecoveryVssServer', {
+ network,
+ nickname,
+ photo,
+ wordCount
+ });
+ };
+
+ render() {
+ const { navigation } = this.props;
+
+ return (
+
+
+
+
+ {localeString(
+ 'views.Settings.LdkWalletRecoverySettings.seedPhraseLength'
+ )}
+
+
+ {localeString(
+ 'views.Settings.LdkWalletRecoverySettings.seedPhraseLength.subtitle'
+ )}
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ content: {
+ flex: 1,
+ justifyContent: 'center',
+ paddingHorizontal: 20
+ },
+ title: {
+ fontFamily: 'PPNeueMontreal-Book',
+ fontSize: 22,
+ textAlign: 'center',
+ marginBottom: 12
+ },
+ subtitle: {
+ fontFamily: 'PPNeueMontreal-Book',
+ fontSize: 16,
+ textAlign: 'center',
+ marginBottom: 40
+ },
+ button: {
+ marginVertical: 8
+ }
+});
diff --git a/views/Settings/SeedRecovery.tsx b/views/Settings/SeedRecovery.tsx
index 3a41bad2a..a44acf950 100644
--- a/views/Settings/SeedRecovery.tsx
+++ b/views/Settings/SeedRecovery.tsx
@@ -23,6 +23,7 @@ import { Route } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { v4 as uuidv4 } from 'uuid';
import RNRestart from 'react-native-restart';
+import { validateMnemonic } from '@scure/bip39';
import {
ErrorMessage,
@@ -100,6 +101,8 @@ interface SeedRecoveryProps {
nickname?: string;
photo?: string;
isSqlite?: boolean;
+ wordCount?: 12 | 24;
+ vssServer?: string;
}
>;
}
@@ -136,6 +139,7 @@ interface SeedRecoveryState {
ldkMnemonic: string;
ldkPassphrase: string;
ldkNodeDir: string;
+ ldkWordCount: 12 | 24;
channelDbUri?: string;
channelDbFileName?: string;
olympusRestorePending: boolean;
@@ -188,20 +192,41 @@ export default class SeedRecovery extends React.PureComponent<
ldkMnemonic: '',
ldkPassphrase: '',
ldkNodeDir: '',
+ ldkWordCount: 12,
olympusRestorePending: false,
embeddedLndIsSqlite: false
};
}
- private expectedMnemonicWordCount(
+ private acceptedWordCounts(
params: SeedRecoveryProps['route']['params']
- ): 12 | 24 {
+ ): (12 | 24)[] {
const implementation = params?.implementation ?? 'embedded-lnd';
- const shortMnemonicFlow =
- implementation === 'ldk-node' ||
+ if (
params?.restoreSwaps === true ||
- params?.restoreRescueKey === true;
- return shortMnemonicFlow ? 12 : 24;
+ params?.restoreRescueKey === true
+ ) {
+ return [12];
+ }
+ if (implementation === 'ldk-node') {
+ return [12, 24];
+ }
+ return [24];
+ }
+
+ // Effective seed length for the current flow, read from state for rendering.
+ // LDK's value comes from the `wordCount` chosen on the
+ // LdkWalletRecoverySettings screen
+ private currentWordCount(): 12 | 24 {
+ const { implementation, restoreSwaps, restoreRescueKey, ldkWordCount } =
+ this.state;
+ if (restoreSwaps || restoreRescueKey) {
+ return 12;
+ }
+ if (implementation === 'ldk-node') {
+ return ldkWordCount;
+ }
+ return 24;
}
async componentDidMount() {
@@ -213,9 +238,9 @@ export default class SeedRecovery extends React.PureComponent<
if (settings.privacy && settings.privacy.clipboard) {
const clipboard = await Clipboard.getString();
const clipboardWords = clipboard.trim().split(/\s+/);
- const expectedWords = this.expectedMnemonicWordCount(route.params);
+ const acceptedWordCounts = this.acceptedWordCounts(route.params);
- if (clipboardWords.length === expectedWords) {
+ if (acceptedWordCounts.includes(clipboardWords.length as 12 | 24)) {
this.setState({
showClipboardPrompt: true,
clipboardSeedArray: clipboardWords
@@ -237,12 +262,14 @@ export default class SeedRecovery extends React.PureComponent<
const restoreSwaps = props.route.params?.restoreSwaps ?? false;
const restoreRescueKey = props.route.params?.restoreRescueKey ?? false;
const isSqlite = props.route.params?.isSqlite ?? false;
+ const wordCount = props.route.params?.wordCount ?? 12;
this.setState({
embeddedLndIsSqlite: isSqlite,
network,
implementation,
restoreSwaps,
- restoreRescueKey
+ restoreRescueKey,
+ ldkWordCount: wordCount
});
}
@@ -434,7 +461,7 @@ export default class SeedRecovery extends React.PureComponent<
ldkPassphrase,
ldkEsploraServer: getDefaultEsploraServer(networkType),
ldkRgsServer: getDefaultRgsServer(networkType),
- ldkVssServer: DEFAULT_VSS_SERVER
+ ldkVssServer: route.params?.vssServer || DEFAULT_VSS_SERVER
};
let nodes: any;
@@ -481,6 +508,9 @@ export default class SeedRecovery extends React.PureComponent<
implementation
} = this.state;
+ const wordCount = this.currentWordCount();
+ const isLdkNode = implementation === 'ldk-node';
+
const invalidWordIndices: number[] = showValidation
? seedArray.reduce((acc: number[], word, i) => {
if (!BIP39_WORD_LIST.includes(word?.toLowerCase()?.trim())) {
@@ -518,14 +548,7 @@ export default class SeedRecovery extends React.PureComponent<
} else if (selectedWordIndex != null) {
seedArray[selectedWordIndex] = text || '';
this.setState({ seedArray } as any);
- const is12WordMode =
- restoreSwaps ||
- restoreRescueKey ||
- implementation === 'ldk-node';
- if (
- (is12WordMode && selectedWordIndex < 11) ||
- (!is12WordMode && selectedWordIndex < 23)
- ) {
+ if (selectedWordIndex < wordCount - 1) {
this.setState({
selectedWordIndex:
selectedWordIndex + 1,
@@ -688,21 +711,26 @@ export default class SeedRecovery extends React.PureComponent<
return;
}
- // Check for duplicate embedded-lnd wallet with same seed
+ // Check for a duplicate wallet with the same seed.
const { SettingsStore } = this.props;
const { settings } = SettingsStore;
const existingNodes = settings.nodes || [];
const seedWords = seedArray
.map((w: string) => w?.toLowerCase()?.trim())
.join(' ');
- const duplicateNode = existingNodes.find(
- (node: any) =>
- node.implementation === 'embedded-lnd' &&
- node.seedPhrase &&
- node.seedPhrase
- .map((w: string) => w?.toLowerCase()?.trim())
- .join(' ') === seedWords
- );
+ const duplicateNode = existingNodes.find((node: any) => {
+ if (node.implementation === 'embedded-lnd' && node.seedPhrase) {
+ return (
+ node.seedPhrase
+ .map((w: string) => w?.toLowerCase()?.trim())
+ .join(' ') === seedWords
+ );
+ }
+ if (node.implementation === 'ldk-node' && node.ldkMnemonic) {
+ return node.ldkMnemonic.toLowerCase().trim() === seedWords;
+ }
+ return false;
+ });
if (duplicateNode) {
this.setState({
errorMsg: localeString(
@@ -716,9 +744,21 @@ export default class SeedRecovery extends React.PureComponent<
if (implementation === 'ldk-node') {
// LDK Node restore
+ const mnemonic = seedArray.join(' ');
+
+ // LDK seeds are BIP39, so verify the checksum up front.
+ if (!validateMnemonic(mnemonic, BIP39_WORD_LIST)) {
+ this.setState({
+ loading: false,
+ errorMsg: localeString(
+ 'views.Settings.SeedRecovery.invalidChecksum'
+ )
+ });
+ return;
+ }
+
await stopLdkNode();
- const mnemonic = seedArray.join(' ');
const nodeDir = uuidv4();
try {
@@ -767,7 +807,9 @@ export default class SeedRecovery extends React.PureComponent<
scorerUrl: DEFAULT_SCORER_URL,
lsps1Config,
trustedPeers0conf: trustedPeers,
- vssServerUrl: DEFAULT_VSS_SERVER
+ vssServerUrl:
+ this.props.route.params?.vssServer ||
+ DEFAULT_VSS_SERVER
});
// Node is already built — tell Wallet.tsx to skip re-init
@@ -1079,10 +1121,9 @@ export default class SeedRecovery extends React.PureComponent<
if (
(restoreSwaps ||
restoreRescueKey ||
- implementation ===
- 'ldk-node') &&
+ isLdkNode) &&
(selectedWordIndex == null ||
- selectedWordIndex >= 12)
+ selectedWordIndex >= wordCount)
) {
return;
} else if (selectedWordIndex != null) {
@@ -1119,119 +1160,40 @@ export default class SeedRecovery extends React.PureComponent<
}}
keyboardShouldPersistTaps="handled"
>
- {restoreSwaps ||
- restoreRescueKey ||
- implementation === 'ldk-node' ? (
- <>
-
- {[0, 1, 2, 3, 4, 5].map(
- (i) => (
-
- )
- )}
-
-
- {[6, 7, 8, 9, 10, 11].map(
- (i) => (
-
- )
- )}
-
- >
- ) : (
- <>
-
- {[
- 0, 1, 2, 3, 4, 5, 6, 7,
- 8, 9, 10, 11
- ].map((index: number) => {
- return (
-
- );
- })}
-
-
- {[
- 12, 13, 14, 15, 16, 17,
- 18, 19, 20, 21, 22, 23
- ].map((index: number) => {
- return (
-
- );
- })}
-
- >
- )}
+ {[
+ Array.from(
+ { length: wordCount / 2 },
+ (_, i) => i
+ ),
+ Array.from(
+ { length: wordCount / 2 },
+ (_, i) => i + wordCount / 2
+ )
+ ].map((columnIndices, columnKey) => (
+
+ {columnIndices.map(
+ (index: number) => (
+
+ )
+ )}
+
+ ))}
)}
@@ -1552,30 +1514,16 @@ export default class SeedRecovery extends React.PureComponent<
)
}
disabled={
- restoreSwaps || restoreRescueKey
- ? (rescueHost === 'Custom' &&
- !customRescueHost) ||
- seedArray.length !== 12 ||
- seedArray.some(
- (seed) =>
- !BIP39_WORD_LIST.includes(
- seed
- ?.toLowerCase()
- ?.trim()
- )
- )
- : implementation === 'ldk-node'
- ? seedArray.length !== 12 ||
- seedArray.some((seed) => !seed)
- : seedArray.length !== 24 ||
- seedArray.some(
- (seed) =>
- !BIP39_WORD_LIST.includes(
- seed
- ?.toLowerCase()
- ?.trim()
- )
- )
+ ((restoreSwaps || restoreRescueKey) &&
+ rescueHost === 'Custom' &&
+ !customRescueHost) ||
+ seedArray.length !== wordCount ||
+ seedArray.some(
+ (seed) =>
+ !BIP39_WORD_LIST.includes(
+ seed?.toLowerCase()?.trim()
+ )
+ )
}
/>
@@ -1616,18 +1564,28 @@ export default class SeedRecovery extends React.PureComponent<
diff --git a/views/Settings/WalletConfiguration.tsx b/views/Settings/WalletConfiguration.tsx
index d42161d18..ba8e02ce6 100644
--- a/views/Settings/WalletConfiguration.tsx
+++ b/views/Settings/WalletConfiguration.tsx
@@ -45,6 +45,11 @@ import {
} 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';
@@ -150,10 +155,13 @@ interface WalletConfigurationState {
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;
@@ -231,10 +239,13 @@ export default class WalletConfiguration extends React.Component<
ldkPassphrase: '',
ldkNodeDir: '',
ldkNetwork: 'mainnet',
+ ldkSeedWordCount: 12,
ldkEsploraServer: '',
ldkRgsServer: '',
ldkScorerUrl: '',
- ldkVssServer: '',
+ ldkVssServer: DEFAULT_VSS_SERVER,
+ ldkVssServerSelected: DEFAULT_VSS_SERVER,
+ ldkVssServerCustom: '',
ldkNodeInitialized: false,
// NWC
nostrWalletConnectUrl: '',
@@ -1033,7 +1044,7 @@ export default class WalletConfiguration extends React.Component<
network: string
) => {
const { SettingsStore, navigation } = this.props;
- const { nickname, photo, ldkPassphrase } = this.state;
+ const { nickname, photo, ldkPassphrase, ldkVssServer } = this.state;
const { setConnectingStatus, updateSettings, settings } = SettingsStore;
const node = {
@@ -1049,7 +1060,7 @@ export default class WalletConfiguration extends React.Component<
),
ldkRgsServer: getDefaultRgsServer(network as SupportedNetwork),
ldkScorerUrl: DEFAULT_SCORER_URL,
- ldkVssServer: DEFAULT_VSS_SERVER
+ ldkVssServer: ldkVssServer || DEFAULT_VSS_SERVER
};
let nodes: any;
@@ -1081,7 +1092,9 @@ export default class WalletConfiguration extends React.Component<
ldkEsploraServer,
ldkRgsServer,
ldkScorerUrl,
- ldkMnemonic
+ ldkVssServer,
+ ldkMnemonic,
+ ldkSeedWordCount
} = this.state;
this.setState({
@@ -1125,6 +1138,7 @@ export default class WalletConfiguration extends React.Component<
const response = await createLdkNodeWallet({
nodeDir: ldkNodeDir,
seedMnemonic: ldkMnemonic || undefined,
+ wordCount: ldkSeedWordCount,
passphrase: ldkPassphrase || undefined,
network: networkType,
esploraServerUrl:
@@ -1136,7 +1150,7 @@ export default class WalletConfiguration extends React.Component<
: ldkScorerUrl,
lsps1Config,
trustedPeers0conf: trustedPeers,
- vssServerUrl: DEFAULT_VSS_SERVER
+ vssServerUrl: ldkVssServer || DEFAULT_VSS_SERVER
});
// Node is already built — tell Wallet.tsx to skip re-init
@@ -1221,6 +1235,9 @@ export default class WalletConfiguration extends React.Component<
// LDK Node
ldkMnemonic,
ldkNetwork,
+ ldkSeedWordCount,
+ ldkVssServerSelected,
+ ldkVssServerCustom,
ldkNodeInitialized,
// NWC
nostrWalletConnectUrl,
@@ -1761,6 +1778,67 @@ export default class WalletConfiguration extends React.Component<
}}
values={EMBEDDED_NODE_NETWORK_KEYS}
/>
+
+
+ {
+ 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
+ }
+ ]}
+ />
+
+
+ this.setState({
+ ldkVssServerSelected:
+ value,
+ ldkVssServerCustom:
+ custom,
+ ldkVssServer:
+ resolveVssServer(
+ value,
+ custom
+ )
+ })
+ }
+ />
+
)}
@@ -2771,7 +2849,13 @@ export default class WalletConfiguration extends React.Component<
);
}}
tertiary
- disabled={loading}
+ disabled={
+ loading ||
+ !isVssServerValid(
+ ldkVssServerSelected,
+ ldkVssServerCustom
+ )
+ }
/>
@@ -2794,12 +2878,10 @@ export default class WalletConfiguration extends React.Component<
)}
onPress={() =>
navigation.navigate(
- 'SeedRecovery',
+ 'LdkWalletRecoverySettings',
{
network:
ldkNetwork,
- implementation:
- 'ldk-node',
nickname,
photo
}