Skip to content
Open
Show file tree
Hide file tree
Changes from all 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