Skip to content

Commit 73cbb54

Browse files
authored
Merge pull request #292 from synonymdev/feat/rn-restore
Support restore from RN app
2 parents 6492c96 + c2896e2 commit 73cbb54

File tree

12 files changed

+1062
-76
lines changed

12 files changed

+1062
-76
lines changed

Bitkit.xcodeproj/project.pbxproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
Services/GeoService.swift,
9393
Services/LightningService.swift,
9494
Services/MigrationsService.swift,
95+
Services/RNBackupClient.swift,
9596
Services/ServiceQueue.swift,
9697
Services/VssStoreIdProvider.swift,
9798
Utilities/Crypto.swift,

Bitkit/AppScene.swift

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,16 +248,18 @@ struct AppScene: View {
248248

249249
if wallet.isRestoringWallet {
250250
Task {
251-
await BackupService.shared.performFullRestoreFromLatestBackup()
251+
await restoreFromMostRecentBackup()
252252

253253
await MainActor.run {
254254
widgets.loadSavedWidgets()
255255
widgets.objectWillChange.send()
256256
}
257+
258+
await startWallet()
257259
}
260+
} else {
261+
Task { await startWallet() }
258262
}
259-
260-
Task { await startWallet() }
261263
}
262264

263265
private func startWallet() async {
@@ -332,6 +334,44 @@ struct AppScene: View {
332334
}
333335
}
334336

337+
private func restoreFromMostRecentBackup() async {
338+
guard let mnemonicData = try? Keychain.load(key: .bip39Mnemonic(index: 0)),
339+
let mnemonic = String(data: mnemonicData, encoding: .utf8)
340+
else { return }
341+
342+
let passphrase: String? = {
343+
guard let data = try? Keychain.load(key: .bip39Passphrase(index: 0)) else { return nil }
344+
return String(data: data, encoding: .utf8)
345+
}()
346+
347+
// Check for RN backup and get its timestamp
348+
let hasRNBackup = await MigrationsService.shared.hasRNRemoteBackup(mnemonic: mnemonic, passphrase: passphrase)
349+
let rnTimestamp: UInt64? = await hasRNBackup ? (try? RNBackupClient.shared.getLatestBackupTimestamp()) : nil
350+
351+
// Get VSS backup timestamp
352+
let vssTimestamp = await BackupService.shared.getLatestBackupTime()
353+
354+
// Determine which backup is more recent
355+
let shouldRestoreRN: Bool = {
356+
guard hasRNBackup else { return false }
357+
guard let vss = vssTimestamp, vss > 0 else { return true } // No VSS, use RN
358+
guard let rn = rnTimestamp else { return false } // No RN timestamp, use VSS
359+
return rn >= vss // RN is same or newer
360+
}()
361+
362+
if shouldRestoreRN {
363+
do {
364+
try await MigrationsService.shared.restoreFromRNRemoteBackup(mnemonic: mnemonic, passphrase: passphrase)
365+
} catch {
366+
Logger.error("RN remote backup restore failed: \(error)", context: "AppScene")
367+
// Fall back to VSS
368+
await BackupService.shared.performFullRestoreFromLatestBackup()
369+
}
370+
} else {
371+
await BackupService.shared.performFullRestoreFromLatestBackup()
372+
}
373+
}
374+
335375
private func handleNodeLifecycleChange(_ state: NodeLifecycleState) {
336376
if state == .initializing {
337377
walletIsInitializing = true

Bitkit/Constants/Env.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,20 @@ enum Env {
204204
}
205205
}
206206

207+
static var rnBackupServerHost: String {
208+
switch network {
209+
case .bitcoin: "https://blocktank.synonym.to/backups-ldk"
210+
default: "https://bitkit.stag0.blocktank.to/backups-ldk"
211+
}
212+
}
213+
214+
static var rnBackupServerPubKey: String {
215+
switch network {
216+
case .bitcoin: "0236efd76e37f96cf2dced9d52ff84c97e5b3d4a75e7d494807291971783f38377"
217+
default: "02c03b8b8c1b5500b622646867d99bf91676fac0f38e2182c91a9ff0d053a21d6d"
218+
}
219+
}
220+
207221
static var blockExplorerUrl: String {
208222
switch network {
209223
case .bitcoin: "https://mempool.space"

Bitkit/Services/BackupService.swift

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -499,14 +499,53 @@ class BackupService {
499499
return statuses[category] ?? BackupItemStatus()
500500
}
501501

502-
func getLatestBackupTime() -> UInt64? {
503-
let statuses = getAllBackupStatuses()
504-
let syncedTimestamps = BackupCategory.allCases.compactMap { category -> UInt64? in
505-
let status = statuses[category] ?? BackupItemStatus()
506-
return status.synced > 0 ? status.synced : nil
502+
func getLatestBackupTime() async -> UInt64? {
503+
do {
504+
try await vssBackupClient.setup()
505+
506+
let timestamps = await withTaskGroup(of: UInt64?.self) { group in
507+
for category in BackupCategory.allCases where category != .lightningConnections {
508+
group.addTask {
509+
await self.getRemoteBackupTimestamp(category: category)
510+
}
511+
}
512+
513+
var results: [UInt64] = []
514+
for await timestamp in group {
515+
if let ts = timestamp, ts > 0 {
516+
results.append(ts)
517+
}
518+
}
519+
return results
520+
}
521+
522+
return timestamps.max()
523+
} catch {
524+
Logger.warn("Failed to get VSS backup timestamp: \(error)", context: "BackupService")
525+
return nil
507526
}
527+
}
528+
529+
private func getRemoteBackupTimestamp(category: BackupCategory) async -> UInt64? {
530+
do {
531+
guard let item = try await vssBackupClient.getObject(key: category.rawValue) else {
532+
return nil
533+
}
508534

509-
return syncedTimestamps.max()
535+
struct BackupWithCreatedAt: Codable {
536+
let createdAt: UInt64?
537+
}
538+
539+
let backup = try JSONDecoder().decode(BackupWithCreatedAt.self, from: item.value)
540+
guard let createdAtMillis = backup.createdAt, createdAtMillis > 0 else {
541+
return nil
542+
}
543+
// Convert from milliseconds to seconds (matching Android behavior)
544+
return createdAtMillis / 1000
545+
} catch {
546+
Logger.debug("Failed to get remote backup timestamp for \(category.rawValue): \(error)", context: "BackupService")
547+
return nil
548+
}
510549
}
511550

512551
func scheduleFullBackup() async {
@@ -576,7 +615,7 @@ class BackupService {
576615
let settingsDict = await SettingsViewModel.shared.getSettingsDictionary()
577616
let payload = SettingsBackupV1(
578617
version: 1,
579-
createdAt: UInt64(Date().timeIntervalSince1970),
618+
createdAt: UInt64(Date().timeIntervalSince1970 * 1000),
580619
settings: settingsDict
581620
)
582621
return try payload.encode()
@@ -592,7 +631,7 @@ class BackupService {
592631

593632
let payload = WidgetsBackupV1(
594633
version: 1,
595-
createdAt: UInt64(Date().timeIntervalSince1970),
634+
createdAt: UInt64(Date().timeIntervalSince1970 * 1000),
596635
widgets: androidWidgetsDict
597636
)
598637
let encoded = try payload.encode()
@@ -603,13 +642,13 @@ class BackupService {
603642
let transfers = try TransferStorage.shared.getAll()
604643
let payload = WalletBackupV1(
605644
version: 1,
606-
createdAt: UInt64(Date().timeIntervalSince1970),
645+
createdAt: UInt64(Date().timeIntervalSince1970 * 1000),
607646
transfers: transfers
608647
)
609648
return try JSONEncoder().encode(payload)
610649

611650
case .metadata:
612-
let currentTime = UInt64(Date().timeIntervalSince1970)
651+
let currentTime = UInt64(Date().timeIntervalSince1970 * 1000)
613652
let cache = await SettingsViewModel.shared.getAppCacheData()
614653

615654
let preActivityMetadata = try await CoreService.shared.activity.getAllPreActivityMetadata()
@@ -629,7 +668,7 @@ class BackupService {
629668

630669
let payload = BlocktankBackupV1(
631670
version: 1,
632-
createdAt: UInt64(Date().timeIntervalSince1970),
671+
createdAt: UInt64(Date().timeIntervalSince1970 * 1000),
633672
orders: orders,
634673
cjitEntries: cjitEntries,
635674
info: info
@@ -644,7 +683,7 @@ class BackupService {
644683

645684
let payload = ActivityBackupV1(
646685
version: 1,
647-
createdAt: UInt64(Date().timeIntervalSince1970),
686+
createdAt: UInt64(Date().timeIntervalSince1970 * 1000),
648687
activities: activities,
649688
activityTags: activityTags,
650689
closedChannels: closedChannels

Bitkit/Services/CoreService.swift

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -352,15 +352,16 @@ class ActivityService {
352352
let value = payment.amountSats ?? 0
353353

354354
// Determine confirmation status from payment's txStatus
355-
// Ensure confirmTimestamp is at least equal to paymentTimestamp when confirmed
356-
// This handles cases where payment.latestUpdateTimestamp is more recent than blockTimestamp
357-
let (isConfirmed, confirmedTimestamp): (Bool, UInt64?) =
358-
if case let .onchain(_, txStatus) = payment.kind,
359-
case let .confirmed(_, _, blockTimestamp) = txStatus {
360-
(true, max(blockTimestamp, paymentTimestamp))
361-
} else {
362-
(false, nil)
363-
}
355+
var blockTimestamp: UInt64?
356+
let isConfirmed: Bool
357+
if case let .onchain(_, txStatus) = payment.kind,
358+
case let .confirmed(_, _, bts) = txStatus
359+
{
360+
isConfirmed = true
361+
blockTimestamp = bts
362+
} else {
363+
isConfirmed = false
364+
}
364365

365366
// Extract existing activity data
366367
let existingOnchain: OnchainActivity? = {
@@ -412,6 +413,12 @@ class ActivityService {
412413
// Build and save the activity
413414
let finalDoesExist = isConfirmed ? true : doesExist
414415

416+
let activityTimestamp: UInt64 = if existingActivity == nil, let bts = blockTimestamp, bts < paymentTimestamp {
417+
bts
418+
} else {
419+
existingOnchain?.timestamp ?? paymentTimestamp
420+
}
421+
415422
let onchain = OnchainActivity(
416423
id: payment.id,
417424
txType: payment.direction == .outbound ? .sent : .received,
@@ -421,12 +428,12 @@ class ActivityService {
421428
feeRate: feeRate,
422429
address: address,
423430
confirmed: isConfirmed,
424-
timestamp: paymentTimestamp,
431+
timestamp: activityTimestamp,
425432
isBoosted: isBoosted,
426433
boostTxIds: boostTxIds,
427434
isTransfer: isTransfer,
428435
doesExist: finalDoesExist,
429-
confirmTimestamp: confirmedTimestamp,
436+
confirmTimestamp: blockTimestamp,
430437
channelId: channelId,
431438
transferTxId: transferTxId,
432439
createdAt: UInt64(payment.creationTime.timeIntervalSince1970),

0 commit comments

Comments
 (0)