Skip to content

Conversation

@ben-kaufman
Copy link
Contributor

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
record.mp4

What's still needed:

  1. New design for the loading screen and success toast of migration
  2. 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.

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 adds support for migrating existing React Native Bitkit wallets to the native Swift app. The migration automatically detects RN wallet data on first launch and transfers wallet credentials, Lightning channels, settings, activities, widgets, and metadata to the native app format.

Key Changes:

  • Comprehensive RN wallet data detection and migration system with MMKV binary parsing
  • Channel migration support for Lightning Network state preservation
  • Migration loading UI with success/error toast notifications

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
Bitkit/Services/MigrationsService.swift Implements complete RN migration logic including MMKV parsing, keychain reading, and data transformation for settings, activities, channels, and widgets
Bitkit/AppScene.swift Adds migration detection flow on app startup and displays loading screen during migration
Bitkit/ViewModels/WalletViewModel.swift Passes pending channel migration data to Lightning service during wallet initialization
Bitkit/ViewModels/AppViewModel.swift Handles post-sync migration completion with metadata reapplication and success notification
Bitkit/Services/LightningService.swift Accepts and applies channel migration data during Lightning node setup
Bitkit/Services/CoreService.swift Adds utility to mark all unseen activities as seen after migration
Bitkit/Utilities/Keychain.swift Removes outdated TODO comment about RN keychain migration
Bitkit.xcodeproj/.../Package.resolved Updates ldk-node dependency revision for channel migration support

Comment on lines +532 to +535
var jsonString = rootJson
if let jsonStart = rootJson.firstIndex(of: "{") {
jsonString = String(rootJson[jsonStart...])
}
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

The JSON extraction logic is duplicated across multiple extract methods (extractRNMetadata, extractRNWidgets, extractRNActivities, extractRNClosedChannels). Consider extracting this into a shared helper method that takes a key name and returns the parsed JSON object.

Copilot uses AI. Check for mistakes.
Comment on lines +766 to +769
var activityId = txId
if let onchain = try? await CoreService.shared.activity.getOnchainActivityByTxId(txid: txId) {
activityId = onchain.id
}
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

This pattern of converting txId to activityId is duplicated in applyRNMetadata (lines 767-770, 787-790) and applyOnchainMetadata (lines 985-986). Extract this into a helper method to reduce duplication and improve maintainability.

Copilot uses AI. Check for mistakes.
Comment on lines +1018 to +1026
func getBool(from dict: [String: Any], key: String, fallbackKey: String? = nil, defaultValue: Bool) -> Bool {
let keys = fallbackKey != nil ? [key, fallbackKey!] : [key]
for k in keys {
if let val = dict[k] as? Bool { return val }
if let val = dict[k] as? Int { return val != 0 }
if let val = dict[k] as? NSNumber { return val.boolValue }
}
return defaultValue
}
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

This nested helper function is quite useful and handles different boolean representations. Consider extracting it as a private method of MigrationsService so it can be reused elsewhere if needed, and add a comment explaining why multiple type checks are necessary for RN data compatibility.

Copilot uses AI. Check for mistakes.
Comment on lines +554 to +560
try? await CoreService.shared.activity.syncLdkNodePayments(LightningService.shared.payments ?? [])
await CoreService.shared.activity.markAllUnseenActivitiesAsSeen()
await MigrationsService.shared.reapplyMetadataAfterSync()
try? await LightningService.shared.restart()

MigrationsService.shared.isShowingMigrationLoading = false
self.toast(type: .success, title: "Migration Complete", description: "Your wallet has been successfully migrated")
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

Errors from syncLdkNodePayments and LightningService.restart are silently ignored with try?. If these operations fail during migration, the user will see a success message even though the migration may be incomplete. Consider logging these errors or handling them appropriately, potentially showing a different message if critical steps fail.

Suggested change
try? await CoreService.shared.activity.syncLdkNodePayments(LightningService.shared.payments ?? [])
await CoreService.shared.activity.markAllUnseenActivitiesAsSeen()
await MigrationsService.shared.reapplyMetadataAfterSync()
try? await LightningService.shared.restart()
MigrationsService.shared.isShowingMigrationLoading = false
self.toast(type: .success, title: "Migration Complete", description: "Your wallet has been successfully migrated")
var hadError = false
do {
let payments = LightningService.shared.payments ?? []
try await CoreService.shared.activity.syncLdkNodePayments(payments)
} catch {
hadError = true
Logger.warn("Failed to sync LDK node payments during migration: \(error)", context: "AppViewModel")
}
await CoreService.shared.activity.markAllUnseenActivitiesAsSeen()
await MigrationsService.shared.reapplyMetadataAfterSync()
do {
try await LightningService.shared.restart()
} catch {
hadError = true
Logger.warn("Failed to restart LightningService after migration: \(error)", context: "AppViewModel")
}
MigrationsService.shared.isShowingMigrationLoading = false
if hadError {
self.toast(
type: .warning,
title: "Migration Completed With Issues",
description: "Your wallet migration completed, but some background tasks may not have finished correctly."
)
} else {
self.toast(
type: .success,
title: "Migration Complete",
description: "Your wallet has been successfully migrated"
)
}

Copilot uses AI. Check for mistakes.
var result: [String: Data] = [:]

func getBool(from dict: [String: Any], key: String, fallbackKey: String? = nil, defaultValue: Bool) -> Bool {
let keys = fallbackKey != nil ? [key, fallbackKey!] : [key]
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

Force unwrapping fallbackKey with ! is unsafe. Use optional unwrapping instead: let keys = [key] + (fallbackKey.map { [$0] } ?? []) or a more explicit check.

Suggested change
let keys = fallbackKey != nil ? [key, fallbackKey!] : [key]
let keys = [key] + (fallbackKey.map { [$0] } ?? [])

Copilot uses AI. Check for mistakes.
channelId: channel.channel_id,
counterpartyNodeId: channel.counterparty_node_id ?? "",
fundingTxoTxid: fundingTxid,
fundingTxoIndex: 0,
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

The fundingTxoIndex is hardcoded to 0, but RN channels may have used different output indices. The correct index should be extracted from the RN channel data or the funding transaction if available, otherwise channel identification may be incorrect.

Copilot uses AI. Check for mistakes.
Comment on lines +178 to +180
try await ServiceQueue.background(.core) {
try BitkitCore.markActivityAsSeen(activityId: id, seenAt: timestamp)
}
Copy link
Member

Choose a reason for hiding this comment

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

nit: maybe could apply some parallelization in case the wallet has many activities, but I'm not sure if it would make a big difference

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