Skip to content

Conversation

@ben-kaufman
Copy link
Contributor

@ben-kaufman ben-kaufman commented Dec 29, 2025

This PR adds support for migrating an existing React Native Bitkit into the Bitkit native app.
The migration passes:

Balances
Channels
Activities
Settings
Widgets
Tags and notes
Closed Channels

It also adds support for restoring a wallet created in RN app which was later deleted. Ie. create wallet in RN, use it, delete the app, install native, recover using mnemonic.

What's still needed:

New design for the loading screen and success toast of migration

@jvsena42 jvsena42 requested a review from Copilot December 29, 2025 17:58
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements migration support for transitioning users from the React Native Bitkit app to the native Android version. The migration preserves critical wallet data including balances, channels, activities, settings, widgets, and tags/notes while maintaining a recovery path if issues occur.

Key changes:

  • Adds comprehensive RN data migration logic handling MMKV storage, keychain data, and LDK channel state
  • Implements migration loading UI with timeout protection and error handling
  • Coordinates migration flow across wallet initialization, node startup, and data synchronization phases

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
gradle/libs.versions.toml Updates ldk-node-android dependency to v0.7.0-rc.2
app/build.gradle.kts Bumps version code from 17 to 160
MigrationService.kt Core migration service handling RN data extraction, decryption, and native app data transformation
MmkvParser.kt Binary parser for React Native MMKV storage format
MigrationLoadingScreen.kt Loading screen UI displayed during migration process
WalletViewModel.kt Orchestrates migration check and execution during wallet initialization
AppViewModel.kt Handles migration completion, sync coordination, and timeout management
MainActivity.kt Integrates migration loading screen into app navigation flow
LightningService.kt Adds channel migration support to LDK node setup
LightningRepo.kt Extends node start/setup to accept channel migration data and adds restart capability
CoreService.kt Adds method to mark all unseen activities as seen during migration
ActivityRepo.kt Exposes payment sync and bulk activity marking methods for migration

walletIndex: Int,
customServerUrl: String? = null,
customRgsServerUrl: String? = null,
channelMigration: org.lightningdevkit.ldknode.ChannelDataMigration? = null,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could add org.lightningdevkit.ldknode to imports

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

val bitkitFiles = rnBackupClient.listFiles(fileGroup = "bitkit")?.list ?: emptyList()
retrieveAndApplyBitkitBackups(bitkitFiles)
markMigrationCompleted()
} catch (e: Exception) {

Check warning

Code scanning / detekt

The caught exception is too generic. Prefer catching specific exceptions to the case that is currently handled. Warning

The caught exception is too generic. Prefer catching specific exceptions to the case that is currently handled.
)

var latestTimestamp: ULong? = null
for (label in labels) {

Check warning

Code scanning / detekt

The loop contains more than one break or continue statement. The code should be refactored to increase readability. Warning

The loop contains more than one break or continue statement. The code should be refactored to increase readability.
val mnemonicBytes = mnemonic.toByteArray(Charsets.UTF_8)
val salt = ("mnemonic" + (passphrase ?: "")).toByteArray(Charsets.UTF_8)
val generator = PKCS5S2ParametersGenerator(SHA512Digest())
generator.init(mnemonicBytes, salt, 2048)

Check warning

Code scanning / detekt

Report magic numbers. Magic number is a numeric literal that is not defined as a constant and hence it's unclear what the purpose of this number is. It's better to declare such numbers as constants and give them a proper name. By default, -1, 0, 1, and 2 are not considered to be magic numbers. Warning

This expression contains a magic number. Consider defining it to a well named constant.
val salt = ("mnemonic" + (passphrase ?: "")).toByteArray(Charsets.UTF_8)
val generator = PKCS5S2ParametersGenerator(SHA512Digest())
generator.init(mnemonicBytes, salt, 2048)
return (generator.generateDerivedParameters(512) as KeyParameter).key

Check warning

Code scanning / detekt

Report magic numbers. Magic number is a numeric literal that is not defined as a constant and hence it's unclear what the purpose of this number is. It's better to declare such numbers as constants and give them a proper name. By default, -1, 0, 1, and 2 are not considered to be magic numbers. Warning

This expression contains a magic number. Consider defining it to a well named constant.
_restoreState.update { RestoreState.InProgress.Metadata }
try {
restoreFromMostRecentBackup()
} catch (e: Exception) {

Check warning

Code scanning / detekt

The caught exception is too generic. Prefer catching specific exceptions to the case that is currently handled. Warning

The caught exception is too generic. Prefer catching specific exceptions to the case that is currently handled.
import javax.inject.Singleton

private val Context.rnMigrationDataStore:
androidx.datastore.core.DataStore<androidx.datastore.preferences.core.Preferences> by preferencesDataStore(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import androidx.datastore.core.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment on lines 404 to 412
val words = mnemonic.split(" ").filter { it.isNotBlank() }
if (words.size != MNEMONIC_WORD_COUNT_12 && words.size != MNEMONIC_WORD_COUNT_24) {
throw to.bitkit.utils.AppError(
"Recovery phrase format is invalid. Please use your 12 or 24 word recovery phrase to restore manually."
)
}

keychain.saveString(Keychain.Key.BIP39_MNEMONIC.name, mnemonic)
}
Copy link
Member

@jvsena42 jvsena42 Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could validate the mnemonics phrase too instead of just the count

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

minSdk = 28
targetSdk = 36
versionCode = 17
versionCode = 160
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the last version of RN is 161, so should be 162 here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@jvsena42
Copy link
Member

jvsena42 commented Dec 31, 2025

Failed restoring the channels on local migration, I'll try again, just in case

Native data

Build variant: mainDebug

versionCode = 162
        create("mainnet") {
            dimension = "network"
            applicationIdSuffix = ""
            buildConfigField("String", "NETWORK", "\"REGTEST\"")
            resValue("string", "app_name", "Bitkit")
            manifestPlaceholders["app_icon"] = "@mipmap/ic_launcher_orange"
            manifestPlaceholders["app_icon_round"] = "@mipmap/ic_launcher_orange_round"
        }

RN data
versionCode = 161
network = regtest

logs.txt

@jvsena42
Copy link
Member

Failed restoring the channels on local migration, I'll try again, just in case

Native data

Build variant: mainDebug

versionCode = 162
       create("mainnet") {
           dimension = "network"
           applicationIdSuffix = ""
           buildConfigField("String", "NETWORK", "\"REGTEST\"")
           resValue("string", "app_name", "Bitkit")
           manifestPlaceholders["app_icon"] = "@mipmap/ic_launcher_orange"
           manifestPlaceholders["app_icon_round"] = "@mipmap/ic_launcher_orange_round"
       }

RN data versionCode = 161 network = regtest

logs.txt

Second attempt, still failed. also noticed that the PIN was not imported

)
}

private suspend fun completeMigration() {

Check warning

Code scanning / detekt

One method should have one responsibility. Long methods tend to handle many things at once. Prefer smaller methods to make them easier to understand. Warning

The function completeMigration is too long (66). The maximum length is 60.
import to.bitkit.repositories.SyncSource
import to.bitkit.repositories.WalletRepo
import to.bitkit.services.MigrationService
import to.bitkit.services.PendingChannelMigration

Check warning

Code scanning / detekt

Detects unused imports Warning

Unused import
companion object {
private const val TAG = "WalletViewModel"
private val RESTORE_WAIT_TIMEOUT = 30.seconds
private const val NODE_RESTART_DELAY_MS = 500L

Check warning

Code scanning / detekt

Property is unused and should be removed. Warning

Private property NODE_RESTART_DELAY_MS is unused.
@jvsena42
Copy link
Member

jvsena42 commented Dec 31, 2025

Tested:

On RN

  1. Create a fresh wallet
  2. Open a channel
  3. Send and receive lightning
  4. Open a second channel
  5. Enable PIN

On Android

  1. Install the app with the RN installed
  2. Migration triggered ✅
  3. Channels restored ✅
  4. Activities and balance restored ✅
  5. Pin restored ✅
  6. Disabled and re-enabled pin ✅

From the warnings, the PR needs a cleanup. After that I'll do a last review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants