From 549af94a79e28176acc0bc4215fa08a771f6733f Mon Sep 17 00:00:00 2001 From: Shubham Kumar Date: Wed, 8 Jul 2026 00:43:02 +0530 Subject: [PATCH 01/18] feat (ldk): thread wordCount through createLdkNodeWallet --- utils/LdkNodeUtils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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({ From adfc11d23dc074c8e938fb7dc9a33872c675c11f Mon Sep 17 00:00:00 2001 From: Shubham Kumar Date: Wed, 8 Jul 2026 00:45:06 +0530 Subject: [PATCH 02/18] feat (ldk): let users pick 12 or 24 word seed on wallet creation --- locales/en.json | 3 +++ views/Settings/WalletConfiguration.tsx | 34 +++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 9c7bca5e9..268cc1514 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", diff --git a/views/Settings/WalletConfiguration.tsx b/views/Settings/WalletConfiguration.tsx index d42161d18..31962dc19 100644 --- a/views/Settings/WalletConfiguration.tsx +++ b/views/Settings/WalletConfiguration.tsx @@ -150,6 +150,7 @@ interface WalletConfigurationState { ldkPassphrase?: string; ldkNodeDir?: string; ldkNetwork?: string; + ldkSeedWordCount: 12 | 24; ldkEsploraServer?: string; ldkRgsServer?: string; ldkScorerUrl?: string; @@ -231,6 +232,7 @@ export default class WalletConfiguration extends React.Component< ldkPassphrase: '', ldkNodeDir: '', ldkNetwork: 'mainnet', + ldkSeedWordCount: 12, ldkEsploraServer: '', ldkRgsServer: '', ldkScorerUrl: '', @@ -1081,7 +1083,8 @@ export default class WalletConfiguration extends React.Component< ldkEsploraServer, ldkRgsServer, ldkScorerUrl, - ldkMnemonic + ldkMnemonic, + ldkSeedWordCount } = this.state; this.setState({ @@ -1125,6 +1128,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: @@ -1221,6 +1225,7 @@ export default class WalletConfiguration extends React.Component< // LDK Node ldkMnemonic, ldkNetwork, + ldkSeedWordCount, ldkNodeInitialized, // NWC nostrWalletConnectUrl, @@ -1761,6 +1766,33 @@ 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 + } + ]} + /> )} From 6339801db8883c01039fda5f4db4da260c1c53e2 Mon Sep 17 00:00:00 2001 From: Shubham Kumar Date: Wed, 8 Jul 2026 11:12:58 +0530 Subject: [PATCH 03/18] feat (ldk): support 12- or 24-word seeds in wallet recovery --- locales/en.json | 1 + views/Settings/SeedRecovery.tsx | 279 +++++++++++++++++--------------- 2 files changed, 150 insertions(+), 130 deletions(-) diff --git a/locales/en.json b/locales/en.json index 268cc1514..2fc82f003 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1389,6 +1389,7 @@ "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.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/views/Settings/SeedRecovery.tsx b/views/Settings/SeedRecovery.tsx index 3a41bad2a..27c6cc5fc 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, @@ -136,6 +137,7 @@ interface SeedRecoveryState { ldkMnemonic: string; ldkPassphrase: string; ldkNodeDir: string; + ldkWordCount: 12 | 24; channelDbUri?: string; channelDbFileName?: string; olympusRestorePending: boolean; @@ -188,6 +190,7 @@ export default class SeedRecovery extends React.PureComponent< ldkMnemonic: '', ldkPassphrase: '', ldkNodeDir: '', + ldkWordCount: 12, olympusRestorePending: false, embeddedLndIsSqlite: false }; @@ -204,6 +207,46 @@ export default class SeedRecovery extends React.PureComponent< return shortMnemonicFlow ? 12 : 24; } + // Effective seed length for the current flow. Swaps/rescue keys are always + // 12 words; embedded LND is always a 24-word aezeed; LDK Node can be either, + // toggled by the user via `ldkWordCount`. + private currentWordCount(): 12 | 24 { + const { implementation, restoreSwaps, restoreRescueKey, ldkWordCount } = + this.state; + if (restoreSwaps || restoreRescueKey) { + return 12; + } + if (implementation === 'ldk-node') { + return ldkWordCount; + } + return 24; + } + + // Switch the LDK seed length. Shrinking to 12 drops any words entered in + // slots 13-24 and clears a now-out-of-range selection so the input and grid + // stay consistent. + private setLdkWordCount = (wordCount: 12 | 24) => { + this.setState((prevState) => { + const seedArray = prevState.seedArray.slice(0, wordCount); + const selectionOutOfRange = + prevState.selectedWordIndex != null && + prevState.selectedWordIndex >= wordCount; + return { + ...prevState, + ldkWordCount: wordCount, + seedArray, + ...(selectionOutOfRange + ? { + selectedWordIndex: null, + selectedInputType: null, + selectedText: '', + showSuggestions: false + } + : {}) + }; + }); + }; + async componentDidMount() { await this.initFromProps(this.props); @@ -213,13 +256,24 @@ 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 implementation = + route.params?.implementation ?? 'embedded-lnd'; - if (clipboardWords.length === expectedWords) { + const acceptedWordCounts = + implementation === 'ldk-node' + ? [12, 24] + : [this.expectedMnemonicWordCount(route.params)]; + + if (acceptedWordCounts.includes(clipboardWords.length)) { this.setState({ showClipboardPrompt: true, clipboardSeedArray: clipboardWords }); + if (implementation === 'ldk-node') { + this.setState({ + ldkWordCount: clipboardWords.length as 12 | 24 + }); + } } } } @@ -478,9 +532,13 @@ export default class SeedRecovery extends React.PureComponent< rescueHost, customRescueHost, showValidation, - implementation + implementation, + ldkWordCount } = 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 +576,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, @@ -716,9 +767,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 { @@ -1079,10 +1142,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) { @@ -1111,6 +1173,35 @@ export default class SeedRecovery extends React.PureComponent< {!showSuggestions && ( <> + {isLdkNode && selectedInputType !== 'scb' && ( + + + this.setLdkWordCount( + value as 12 | 24 + ) + } + values={[ + { + key: localeString( + 'views.Settings.WalletConfiguration.seedPhraseLength.12' + ), + value: 12 + }, + { + key: localeString( + 'views.Settings.WalletConfiguration.seedPhraseLength.24' + ), + value: 24 + } + ]} + /> + + )} {selectedInputType !== 'scb' && ( - {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) => ( + + ) + )} + + ))} )} @@ -1565,8 +1577,15 @@ export default class SeedRecovery extends React.PureComponent< ) ) : implementation === 'ldk-node' - ? seedArray.length !== 12 || - seedArray.some((seed) => !seed) + ? seedArray.length !== wordCount || + seedArray.some( + (seed) => + !BIP39_WORD_LIST.includes( + seed + ?.toLowerCase() + ?.trim() + ) + ) : seedArray.length !== 24 || seedArray.some( (seed) => From 2e9d564d86dda6f82ae759459467108c6e650231 Mon Sep 17 00:00:00 2001 From: Shubham Kumar Date: Wed, 8 Jul 2026 12:06:33 +0530 Subject: [PATCH 04/18] feat (swaps): only reuse LDK seed as rescue key for 12-word wallets --- stores/SwapStore.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) 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; From 2c584588102daccde50e855fafa77b793391cfde Mon Sep 17 00:00:00 2001 From: Shubham Kumar Date: Wed, 8 Jul 2026 13:03:34 +0530 Subject: [PATCH 05/18] feat (ldk): add recovery-options screen to pick seed length before restore --- App.tsx | 9 +- locales/en.json | 3 + views/Settings/LdkWalletRecoverySettings.tsx | 127 +++++++++++++++++++ views/Settings/SeedRecovery.tsx | 64 +--------- views/Settings/WalletConfiguration.tsx | 4 +- 5 files changed, 145 insertions(+), 62 deletions(-) create mode 100644 views/Settings/LdkWalletRecoverySettings.tsx diff --git a/App.tsx b/App.tsx index 6a5c64a8c..776d2e640 100644 --- a/App.tsx +++ b/App.tsx @@ -223,9 +223,10 @@ 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/LdkWalletRecoverySettings'; import SeedQRExport from './views/Settings/SeedQRExport'; import Sync from './views/Sync'; import SyncRecovery from './views/SyncRecovery'; @@ -1085,6 +1086,12 @@ export default class App extends React.PureComponent { SeedRecovery } /> + ; + route: Route< + 'LdkWalletRecoverySettings', + { + network: string; + nickname?: string; + photo?: string; + } + >; +} + +export default class LdkWalletRecoverySettings extends React.Component< + LdkWalletRecoverySettingsProps, + {} +> { + private continueToRecovery = (wordCount: 12 | 24) => { + const { navigation, route } = this.props; + const { network, nickname, photo } = route.params ?? {}; + + navigation.navigate('SeedRecovery', { + network, + implementation: 'ldk-node', + nickname, + photo, + wordCount + }); + }; + + render() { + const { navigation } = this.props; + + return ( + +
+ + + {localeString( + 'views.Settings.LdkWalletRecoverySettings.seedPhraseLength' + )} + + + {localeString( + 'views.Settings.LdkWalletRecoverySettings.seedPhraseLength.subtitle' + )} + + + +