Merge pull request #4212 from shubhamkmr04/feat/ldk-24-word-seed

LDK: Add support for 24 word seed phrases
This commit is contained in:
Evan Kaloudis
2026-07-27 09:18:34 -04:00
committed by GitHub
12 changed files with 676 additions and 247 deletions
+15 -1
View File
@@ -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
}
/>
<Stack.Screen
name="LdkWalletRecoverySettings" // @ts-ignore:next-line
component={
LdkWalletRecoverySettings
}
/>
<Stack.Screen
name="LdkRecoveryVssServer" // @ts-ignore:next-line
component={
LdkRecoveryVssServer
}
/>
<Stack.Screen
name="SeedQRExport" // @ts-ignore:next-line
component={
+97
View File
@@ -0,0 +1,97 @@
import * as React from 'react';
import { Text, View } from 'react-native';
import DropdownSetting from './DropdownSetting';
import TextInput from './TextInput';
import { LDK_VSS_SERVER_KEYS } from '../stores/SettingsStore';
import { localeString } from '../utils/LocaleUtils';
import { themeColor } from '../utils/ThemeUtils';
import UrlUtils from '../utils/UrlUtils';
import { DEFAULT_VSS_SERVER } from '../utils/LdkNodeUtils';
export const VSS_CUSTOM_VALUE = 'custom';
interface VssServerPickerProps {
selectedValue: string;
customServer: string;
onChange: (selectedValue: string, customServer: string) => 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 (
<View>
<DropdownSetting
title={localeString(
'views.Settings.EmbeddedNode.VssServer.serverUrl'
)}
selectedValue={selectedValue}
onValueChange={(value: string) =>
onChange(
value,
value === VSS_CUSTOM_VALUE ? customServer : ''
)
}
values={LDK_VSS_SERVER_KEYS}
disabled={locked}
/>
{isCustom && (
<>
<TextInput
value={customServer}
placeholder={DEFAULT_VSS_SERVER}
onChangeText={(text: string) =>
onChange(selectedValue, text)
}
autoCapitalize="none"
autoCorrect={false}
locked={locked}
/>
{showInvalidUrlError && (
<Text
style={{
color: themeColor('error'),
fontFamily: 'PPNeueMontreal-Book',
fontSize: 12,
marginTop: 4
}}
>
{localeString(
'views.Settings.EmbeddedNode.invalidServerUrl'
)}
</Text>
)}
</>
)}
</View>
);
}
+7
View File
@@ -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!",
+16 -3
View File
@@ -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)
+5
View File
@@ -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',
+10 -6
View File
@@ -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;
+3 -1
View File
@@ -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({
+38 -50
View File
@@ -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 (
<Screen>
@@ -102,43 +113,17 @@ export default class VssServer extends React.Component<
</Text>
</View>
<Text
style={{
color: themeColor('secondaryText'),
fontFamily: 'PPNeueMontreal-Book'
}}
>
{localeString(
'views.Settings.EmbeddedNode.VssServer.serverUrl'
)}
</Text>
<TextInput
value={vssServer}
placeholder={DEFAULT_VSS_SERVER}
onChangeText={(text: string) => {
<VssServerPicker
selectedValue={selectedValue}
customServer={customServer}
onChange={(value, custom) =>
this.setState({
vssServer: text
});
}}
autoCapitalize="none"
autoCorrect={false}
selectedValue: value,
customServer: custom
})
}
/>
{showInvalidUrlError && (
<Text
style={{
color: themeColor('error'),
fontFamily: 'PPNeueMontreal-Book',
fontSize: 12,
marginTop: 4
}}
>
{localeString(
'views.Settings.EmbeddedNode.invalidServerUrl'
)}
</Text>
)}
<View style={{ marginTop: 10 }}>
<Text
style={{
@@ -160,7 +145,9 @@ export default class VssServer extends React.Component<
accessibilityLabel={localeString(
'general.save'
)}
onPress={() => this.saveSettings(vssServer)}
onPress={() =>
this.saveSettings(effectiveServer)
}
/>
</View>
)}
@@ -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
@@ -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<any, any>;
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 (
<Screen>
<Header
leftComponent="Back"
centerComponent={{
text: localeString(
'views.Settings.EmbeddedNode.VssServer.title'
),
style: {
color: themeColor('text'),
fontFamily: 'PPNeueMontreal-Book'
}
}}
navigation={navigation}
/>
<View style={styles.content}>
<Text
style={{
...styles.subtitle,
color: themeColor('secondaryText')
}}
>
{localeString(
'views.Settings.EmbeddedNode.VssServer.subtitle'
)}
</Text>
<VssServerPicker
selectedValue={selectedValue}
customServer={customServer}
onChange={(value, custom) =>
this.setState({
selectedValue: value,
customServer: custom
})
}
/>
<View style={styles.button}>
<Button
title={localeString('general.next')}
onPress={this.continueToRecovery}
disabled={
!isVssServerValid(selectedValue, customServer)
}
/>
</View>
</View>
</Screen>
);
}
}
const styles = StyleSheet.create({
content: {
flex: 1,
paddingHorizontal: 20,
paddingTop: 20
},
subtitle: {
fontFamily: 'PPNeueMontreal-Book',
fontSize: 15,
marginBottom: 24
},
button: {
marginTop: 24
}
});
@@ -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<any, any>;
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 (
<Screen>
<Header
leftComponent="Back"
centerComponent={{
text: localeString(
'views.Settings.LdkWalletRecoverySettings.title'
),
style: {
color: themeColor('text'),
fontFamily: 'PPNeueMontreal-Book'
}
}}
navigation={navigation}
/>
<View style={styles.content}>
<Text
style={{
...styles.title,
color: themeColor('text')
}}
>
{localeString(
'views.Settings.LdkWalletRecoverySettings.seedPhraseLength'
)}
</Text>
<Text
style={{
...styles.subtitle,
color: themeColor('secondaryText')
}}
>
{localeString(
'views.Settings.LdkWalletRecoverySettings.seedPhraseLength.subtitle'
)}
</Text>
<View style={styles.button}>
<Button
title={localeString(
'views.Settings.WalletConfiguration.seedPhraseLength.12'
)}
onPress={() => this.continueToVssServer(12)}
/>
</View>
<View style={styles.button}>
<Button
title={localeString(
'views.Settings.WalletConfiguration.seedPhraseLength.24'
)}
onPress={() => this.continueToVssServer(24)}
secondary
/>
</View>
</View>
</Screen>
);
}
}
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
}
});
+135 -177
View File
@@ -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' ? (
<>
<View
style={{
...styles.column,
alignSelf:
!selectedInputType
? 'center'
: undefined
}}
>
{[0, 1, 2, 3, 4, 5].map(
(i) => (
<RecoveryLabel
key={i}
type="mnemonicWord"
index={i}
text={
this.state
.seedArray[
i
]
}
/>
)
)}
</View>
<View
style={{
...styles.column,
alignSelf:
!selectedInputType
? 'center'
: undefined
}}
>
{[6, 7, 8, 9, 10, 11].map(
(i) => (
<RecoveryLabel
key={i}
type="mnemonicWord"
index={i}
text={
this.state
.seedArray[
i
]
}
/>
)
)}
</View>
</>
) : (
<>
<View
style={{
...styles.column,
alignSelf:
!selectedInputType
? 'center'
: undefined
}}
>
{[
0, 1, 2, 3, 4, 5, 6, 7,
8, 9, 10, 11
].map((index: number) => {
return (
<RecoveryLabel
key={index}
type="mnemonicWord"
index={index}
text={
seedArray[
index
]
}
/>
);
})}
</View>
<View
style={{
...styles.column,
alignSelf:
!selectedInputType
? 'center'
: undefined
}}
>
{[
12, 13, 14, 15, 16, 17,
18, 19, 20, 21, 22, 23
].map((index: number) => {
return (
<RecoveryLabel
key={index}
type="mnemonicWord"
index={index}
text={
seedArray[
index
]
}
/>
);
})}
</View>
</>
)}
{[
Array.from(
{ length: wordCount / 2 },
(_, i) => i
),
Array.from(
{ length: wordCount / 2 },
(_, i) => i + wordCount / 2
)
].map((columnIndices, columnKey) => (
<View
key={columnKey}
style={{
...styles.column,
alignSelf:
!selectedInputType
? 'center'
: undefined
}}
>
{columnIndices.map(
(index: number) => (
<RecoveryLabel
key={index}
type="mnemonicWord"
index={index}
text={
seedArray[index]
}
/>
)
)}
</View>
))}
</ScrollView>
)}
@@ -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()
)
)
}
/>
</View>
@@ -1616,18 +1564,28 @@ export default class SeedRecovery extends React.PureComponent<
<View style={{ marginBottom: 12 }}>
<Button
title={localeString('general.yes')}
onPress={() =>
onPress={() => {
const {
clipboardSeedArray,
implementation
} = this.state;
this.setState({
seedArray:
this.state.clipboardSeedArray.map(
(w) =>
w.toLowerCase().trim()
),
seedArray: clipboardSeedArray.map(
(w) => w.toLowerCase().trim()
),
showClipboardPrompt: false,
clipboardSeedArray: [],
showValidation: true
})
}
});
if (implementation === 'ldk-node') {
this.setState({
ldkWordCount:
clipboardSeedArray.length as
| 12
| 24
});
}
}}
tertiary
/>
</View>
+91 -9
View File
@@ -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}
/>
<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>
)}
@@ -2771,7 +2849,13 @@ export default class WalletConfiguration extends React.Component<
);
}}
tertiary
disabled={loading}
disabled={
loading ||
!isVssServerValid(
ldkVssServerSelected,
ldkVssServerCustom
)
}
/>
</View>
<View style={styles.button}>
@@ -2794,12 +2878,10 @@ export default class WalletConfiguration extends React.Component<
)}
onPress={() =>
navigation.navigate(
'SeedRecovery',
'LdkWalletRecoverySettings',
{
network:
ldkNetwork,
implementation:
'ldk-node',
nickname,
photo
}