Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Bitkit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
Services/GeoService.swift,
Services/LightningService.swift,
Services/MigrationsService.swift,
Services/RNBackupClient.swift,
Services/ServiceQueue.swift,
Services/VssStoreIdProvider.swift,
Utilities/Crypto.swift,
Expand Down
46 changes: 43 additions & 3 deletions Bitkit/AppScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -248,16 +248,18 @@ struct AppScene: View {

if wallet.isRestoringWallet {
Task {
await BackupService.shared.performFullRestoreFromLatestBackup()
await restoreFromMostRecentBackup()

await MainActor.run {
widgets.loadSavedWidgets()
widgets.objectWillChange.send()
}

await startWallet()
}
} else {
Task { await startWallet() }
}

Task { await startWallet() }
}

private func startWallet() async {
Expand Down Expand Up @@ -332,6 +334,44 @@ struct AppScene: View {
}
}

private func restoreFromMostRecentBackup() async {
guard let mnemonicData = try? Keychain.load(key: .bip39Mnemonic(index: 0)),
let mnemonic = String(data: mnemonicData, encoding: .utf8)
else { return }

let passphrase: String? = {
guard let data = try? Keychain.load(key: .bip39Passphrase(index: 0)) else { return nil }
return String(data: data, encoding: .utf8)
}()

// Check for RN backup and get its timestamp
let hasRNBackup = await MigrationsService.shared.hasRNRemoteBackup(mnemonic: mnemonic, passphrase: passphrase)
let rnTimestamp: UInt64? = await hasRNBackup ? (try? RNBackupClient.shared.getLatestBackupTimestamp()) : nil

// Get VSS backup timestamp
let vssTimestamp = BackupService.shared.getLatestBackupTime()

// Determine which backup is more recent
let shouldRestoreRN: Bool = {
guard hasRNBackup else { return false }
guard let vss = vssTimestamp, vss > 0 else { return true } // No VSS, use RN
Comment on lines +353 to +357
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

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

The vss > 0 check appears to be a magic number. Consider using a named constant or adding a comment explaining what a timestamp of 0 or less represents.

Suggested change
// Determine which backup is more recent
let shouldRestoreRN: Bool = {
guard hasRNBackup else { return false }
guard let vss = vssTimestamp, vss > 0 else { return true } // No VSS, use RN
// A VSS timestamp of 0 indicates that no VSS backup exists.
let vssNoBackupTimestamp: UInt64 = 0
// Determine which backup is more recent
let shouldRestoreRN: Bool = {
guard hasRNBackup else { return false }
guard let vss = vssTimestamp, vss > vssNoBackupTimestamp else { return true } // No VSS, use RN

Copilot uses AI. Check for mistakes.
guard let rn = rnTimestamp else { return false } // No RN timestamp, use VSS
return rn >= vss // RN is same or newer
}()

if shouldRestoreRN {
do {
try await MigrationsService.shared.restoreFromRNRemoteBackup(mnemonic: mnemonic, passphrase: passphrase)
} catch {
Logger.error("RN remote backup restore failed: \(error)", context: "AppScene")
// Fall back to VSS
await BackupService.shared.performFullRestoreFromLatestBackup()
}
} else {
await BackupService.shared.performFullRestoreFromLatestBackup()
}
}

private func handleNodeLifecycleChange(_ state: NodeLifecycleState) {
if state == .initializing {
walletIsInitializing = true
Expand Down
14 changes: 14 additions & 0 deletions Bitkit/Constants/Env.swift
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,20 @@ enum Env {
}
}

static var rnBackupServerHost: String {
switch network {
case .bitcoin: "https://blocktank.synonym.to/backups-ldk"
default: "https://bitkit.stag0.blocktank.to/backups-ldk"
}
}

static var rnBackupServerPubKey: String {
switch network {
case .bitcoin: "0236efd76e37f96cf2dced9d52ff84c97e5b3d4a75e7d494807291971783f38377"
default: "02c03b8b8c1b5500b622646867d99bf91676fac0f38e2182c91a9ff0d053a21d6d"
}
}

static var blockExplorerUrl: String {
switch network {
case .bitcoin: "https://mempool.space"
Expand Down
29 changes: 18 additions & 11 deletions Bitkit/Services/CoreService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
}
}
await MainActor.run {
self.cachedTxIdsInBoostTxIds = txIds

Check warning on line 57 in Bitkit/Services/CoreService.swift

View workflow job for this annotation

GitHub Actions / Run Tests

reference to captured var 'txIds' in concurrently-executing code; this is an error in the Swift 6 language mode
}
} catch {
Logger.error("Failed to refresh boostTxIds cache: \(error)", context: "ActivityService")
Expand Down Expand Up @@ -109,7 +109,7 @@

func isActivitySeen(id: String) async -> Bool {
do {
if let activity = try await getActivityById(activityId: id) {

Check warning on line 112 in Bitkit/Services/CoreService.swift

View workflow job for this annotation

GitHub Actions / Run Tests

no 'async' operations occur within 'await' expression
switch activity {
case let .onchain(onchain):
return onchain.seenAt != nil
Expand Down Expand Up @@ -352,15 +352,16 @@
let value = payment.amountSats ?? 0

// Determine confirmation status from payment's txStatus
// Ensure confirmTimestamp is at least equal to paymentTimestamp when confirmed
// This handles cases where payment.latestUpdateTimestamp is more recent than blockTimestamp
let (isConfirmed, confirmedTimestamp): (Bool, UInt64?) =
if case let .onchain(_, txStatus) = payment.kind,
case let .confirmed(_, _, blockTimestamp) = txStatus {
(true, max(blockTimestamp, paymentTimestamp))
} else {
(false, nil)
}
var blockTimestamp: UInt64?
let isConfirmed: Bool
if case let .onchain(_, txStatus) = payment.kind,
case let .confirmed(_, _, bts) = txStatus
{
isConfirmed = true
blockTimestamp = bts
} else {
isConfirmed = false
}

// Extract existing activity data
let existingOnchain: OnchainActivity? = {
Expand Down Expand Up @@ -412,6 +413,12 @@
// Build and save the activity
let finalDoesExist = isConfirmed ? true : doesExist

let activityTimestamp: UInt64 = if existingActivity == nil, let bts = blockTimestamp, bts < paymentTimestamp {
bts
} else {
existingOnchain?.timestamp ?? paymentTimestamp
}
Comment on lines +416 to +420
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

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

This timestamp selection logic is complex and would benefit from a comment explaining when block timestamp vs payment timestamp is used and why the condition checks for existingActivity == nil.

Copilot uses AI. Check for mistakes.

let onchain = OnchainActivity(
id: payment.id,
txType: payment.direction == .outbound ? .sent : .received,
Expand All @@ -421,12 +428,12 @@
feeRate: feeRate,
address: address,
confirmed: isConfirmed,
timestamp: paymentTimestamp,
timestamp: activityTimestamp,
isBoosted: isBoosted,
boostTxIds: boostTxIds,
isTransfer: isTransfer,
doesExist: finalDoesExist,
confirmTimestamp: confirmedTimestamp,
confirmTimestamp: blockTimestamp,
channelId: channelId,
transferTxId: transferTxId,
createdAt: UInt64(payment.creationTime.timeIntervalSince1970),
Expand Down Expand Up @@ -618,7 +625,7 @@
}

private func processLightningPayment(_ payment: PaymentDetails) async throws {
guard case let .bolt11(hash, preimage, secret, description, bolt11) = payment.kind else { return }

Check warning on line 628 in Bitkit/Services/CoreService.swift

View workflow job for this annotation

GitHub Actions / Run Tests

immutable value 'secret' was never used; consider replacing with '_' or removing it

Check warning on line 628 in Bitkit/Services/CoreService.swift

View workflow job for this annotation

GitHub Actions / Run Tests

immutable value 'hash' was never used; consider replacing with '_' or removing it

// Skip pending inbound payments - just means they created an invoice
guard !(payment.status == .pending && payment.direction == .inbound) else { return }
Expand Down Expand Up @@ -671,7 +678,7 @@

for payment in payments {
do {
let state: BitkitCore.PaymentState = switch payment.status {

Check warning on line 681 in Bitkit/Services/CoreService.swift

View workflow job for this annotation

GitHub Actions / Run Tests

immutable value 'state' was never used; consider replacing with '_' or removing it
case .failed:
.failed
case .pending:
Expand Down
Loading
Loading