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
44 changes: 22 additions & 22 deletions Bitkit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -61,27 +61,6 @@
};
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXShellScriptBuildPhase section */
96EMBED0012026012000FRAME /* Remove Static Framework Stubs */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Remove Static Framework Stubs";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# Remove static framework stubs from app bundle\\n# LDKNodeFFI is a static library - its code is linked into the main executable.\\n# The empty framework structure causes iOS install errors.\\nFRAMEWORK_PATH=\"${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/Frameworks/LDKNodeFFI.framework\"\\n\\nif [ -d \"$FRAMEWORK_PATH\" ]; then\\n echo \"Removing LDKNodeFFI static framework stub...\"\\n rm -rf \"$FRAMEWORK_PATH\"\\n echo \"Done.\"\\nfi\\n";
};
/* End PBXShellScriptBuildPhase section */

/* Begin PBXFileReference section */
961058DC2C355B5500E1F1D8 /* BitkitNotification.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = BitkitNotification.appex; sourceTree = BUILT_PRODUCTS_DIR; };
96FE1F612C2DE6AA006D0C8B /* Bitkit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Bitkit.app; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -443,6 +422,27 @@
};
/* End PBXResourcesBuildPhase section */

/* Begin PBXShellScriptBuildPhase section */
96EMBED0012026012000FRAME /* Remove Static Framework Stubs */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Remove Static Framework Stubs";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# Remove static framework stubs from app bundle\\n# LDKNodeFFI is a static library - its code is linked into the main executable.\\n# The empty framework structure causes iOS install errors.\\nFRAMEWORK_PATH=\"${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/Frameworks/LDKNodeFFI.framework\"\\n\\nif [ -d \"$FRAMEWORK_PATH\" ]; then\\n echo \"Removing LDKNodeFFI static framework stub...\"\\n rm -rf \"$FRAMEWORK_PATH\"\\n echo \"Done.\"\\nfi\\n";
};
/* End PBXShellScriptBuildPhase section */

/* Begin PBXSourcesBuildPhase section */
961058D82C355B5500E1F1D8 /* Sources */ = {
isa = PBXSourcesBuildPhase;
Expand Down Expand Up @@ -893,7 +893,7 @@
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/synonymdev/vss-rust-client-ffi";
requirement = {
branch = master;
branch = "master";
kind = branch;
};
};
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Bitkit/MainNavView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ struct MainNavView: View {
// Dev settings
case .blocktankRegtest: BlocktankRegtestView()
case .ldkDebug: LdkDebugScreen()
case .vssDebug: VssDebugScreen()
case .probingTool: ProbingToolScreen()
case .orders: ChannelOrders()
case .logs: LogView()
Expand Down
182 changes: 154 additions & 28 deletions Bitkit/Services/VssBackupClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,47 +56,49 @@ class VssBackupClient {
static let shared = VssBackupClient()

private let setupCoordinator = VssSetupCoordinator()
private let ldkSetupCoordinator = VssSetupCoordinator()

private init() {}

func reset() async {
await setupCoordinator.reset()
await ldkSetupCoordinator.reset()
}

/// Returns lnurl auth params when lnurl is configured; nil otherwise.
private func getLnurlAuthParams(walletIndex: Int) async throws
-> (vssUrl: String, storeId: String, mnemonic: String, passphrase: String?, lnurlAuthServerUrl: String)?
{
let lnurlAuthServerUrl = Env.lnurlAuthServerUrl
guard !lnurlAuthServerUrl.isEmpty else { return nil }
guard let mnemonic = try Keychain.loadString(key: .bip39Mnemonic(index: walletIndex)) else {
throw CustomServiceError.mnemonicNotFound
}
let passphraseRaw = try Keychain.loadString(key: .bip39Passphrase(index: walletIndex))
let passphrase = passphraseRaw?.isEmpty == true ? nil : passphraseRaw
let storeId = try await VssStoreIdProvider.shared.getVssStoreId(walletIndex: walletIndex)
return (Env.vssServerUrl, storeId, mnemonic, passphrase, lnurlAuthServerUrl)
}

private func setup(walletIndex: Int = 0) async throws {
do {
try await withTimeout(seconds: 30) {
try await withTimeout(seconds: 30) { [self] in
Logger.debug("VSS client setting up…", context: "VssBackupClient")

let vssUrl = Env.vssServerUrl
let lnurlAuthServerUrl = Env.lnurlAuthServerUrl
Logger.debug("Building VSS client with vssUrl: '\(vssUrl)'", context: "VssBackupClient")
Logger.debug("Building VSS client with lnurlAuthServerUrl: '\(lnurlAuthServerUrl)'", context: "VssBackupClient")

let storeId = try await VssStoreIdProvider.shared.getVssStoreId(walletIndex: walletIndex)

if !lnurlAuthServerUrl.isEmpty {
guard let mnemonic = try Keychain.loadString(key: .bip39Mnemonic(index: walletIndex)) else {
throw CustomServiceError.mnemonicNotFound
}
// Normalize empty strings to nil - empty passphrase should be treated as no passphrase
let passphraseRaw = try Keychain.loadString(key: .bip39Passphrase(index: walletIndex))
let passphrase = passphraseRaw?.isEmpty == true ? nil : passphraseRaw

if let params = try await getLnurlAuthParams(walletIndex: walletIndex) {
try await vssNewClientWithLnurlAuth(
baseUrl: vssUrl,
storeId: storeId,
mnemonic: mnemonic,
passphrase: passphrase,
lnurlAuthServerUrl: lnurlAuthServerUrl
baseUrl: params.vssUrl,
storeId: params.storeId,
mnemonic: params.mnemonic,
passphrase: params.passphrase,
lnurlAuthServerUrl: params.lnurlAuthServerUrl
)
} else {
try await vssNewClient(
baseUrl: vssUrl,
storeId: storeId
)
let storeId = try await VssStoreIdProvider.shared.getVssStoreId(walletIndex: walletIndex)
try await vssNewClient(baseUrl: vssUrl, storeId: storeId)
}

Logger.info("VSS client setup with server: '\(vssUrl)'", context: "VssBackupClient")
}
} catch {
Expand All @@ -105,6 +107,29 @@ class VssBackupClient {
}
}

/// Lazily initializes the LDK VSS client (used only by the debug screen). Only runs when lnurl auth is configured.
private func setupLdk(walletIndex: Int = 0) async throws {
guard let params = try await getLnurlAuthParams(walletIndex: walletIndex) else {
throw AppError(message: "LDK VSS requires lnurl auth", debugMessage: "lnurlAuthServerUrl is not set")
}
do {
try await withTimeout(seconds: 30) {
Logger.debug("VSS LDK client setting up…", context: "VssBackupClient")
try await vssNewLdkClientWithLnurlAuth(
baseUrl: params.vssUrl,
storeId: params.storeId,
mnemonic: params.mnemonic,
passphrase: params.passphrase,
lnurlAuthServerUrl: params.lnurlAuthServerUrl
)
Logger.info("VSS LDK client setup with server: '\(params.vssUrl)'", context: "VssBackupClient")
}
} catch {
Logger.error("VSS LDK client setup error: \(error)", context: "VssBackupClient")
throw error
}
}

func putObject(key: String, data: Data) async throws -> VssItem {
try await awaitSetup()

Expand All @@ -126,25 +151,126 @@ class VssBackupClient {
Logger.debug("VSS 'getObject' call for '\(key)'", context: "VssBackupClient")

do {
let item = try await vssGet(key: key)
if let item {
Logger.debug("VSS 'getObject' success for '\(key)'", context: "VssBackupClient")
if let item = try await vssGet(key: key) {
Logger.debug("VSS 'getObject' success for '\(key)' at version \(item.version)", context: "VssBackupClient")
return item
} else {
Logger.debug("VSS 'getObject' success null for '\(key)'", context: "VssBackupClient")
return nil
}
return item
} catch {
Logger.debug("VSS 'getObject' error for '\(key)': \(error)", context: "VssBackupClient")
throw error
}
}

func listKeys() async throws -> [String] {
let versions = try await listKeyVersions()
return versions.map(\.key)
}

/// Returns app-level keys with version info (for debug UI).
func listKeyVersions() async throws -> [KeyVersion] {
try await awaitSetup()
Logger.debug("VSS 'listKeyVersions' call", context: "VssBackupClient")
do {
let versions = try await vssListKeys(prefix: nil)
Logger.debug("VSS 'listKeyVersions' success: \(versions.count) key(s)", context: "VssBackupClient")
return versions
} catch {
Logger.debug("VSS 'listKeyVersions' error: \(error)", context: "VssBackupClient")
throw error
}
}

/// Deletes a single app-level key.
func deleteKey(_ key: String) async throws -> Bool {
try await awaitSetup()
Logger.debug("VSS 'deleteKey' call for '\(key)'", context: "VssBackupClient")
do {
let wasDeleted = try await vssDelete(key: key)
Logger.debug("VSS 'deleteKey' success for '\(key)': \(wasDeleted)", context: "VssBackupClient")
return wasDeleted
} catch {
Logger.debug("VSS 'deleteKey' error for '\(key)': \(error)", context: "VssBackupClient")
throw error
}
}

/// Deletes all app-level keys (lists then deletes each).
func deleteAllKeys() async throws {
let versions = try await listKeyVersions()
for kv in versions {
_ = try await deleteKey(kv.key)
}
}

// MARK: - LDK namespace keys (for debug; requires FFI with LdkNamespace support)

private static let ldkNamespacesForList: [LdkNamespace] = [
.default,
.monitors,
.archivedMonitors,
]

/// Returns all LDK keys across default, monitors, and archivedMonitors namespaces.
func listAllKeysTaggedLdk() async throws -> [(LdkNamespace, KeyVersion)] {
try await awaitLdkSetup()
Logger.debug("VSS 'listAllKeysTaggedLdk' call", context: "VssBackupClient")
var result: [(LdkNamespace, KeyVersion)] = []
for ns in Self.ldkNamespacesForList {
do {
let keys = try await vssLdkListKeys(namespace: ns)
result.append(contentsOf: keys.map { (ns, $0) })
} catch {
Logger.debug("VSS 'listAllKeysTaggedLdk' error for namespace \(ns): \(error)", context: "VssBackupClient")
throw error
}
}
Logger.debug("VSS 'listAllKeysTaggedLdk' success: \(result.count) key(s)", context: "VssBackupClient")
return result
}

/// Gets a single LDK key value by key and namespace.
func getObjectLdk(key: String, namespace: LdkNamespace) async throws -> VssItem? {
try await awaitLdkSetup()
Logger.debug("VSS 'getObjectLdk' call for '\(key)'", context: "VssBackupClient")
do {
let item = try await vssLdkGet(key: key, namespace: namespace)
return item
} catch {
Logger.debug("VSS 'getObjectLdk' error for '\(key)': \(error)", context: "VssBackupClient")
throw error
}
}

/// Deletes a single LDK key by key and namespace.
func deleteObjectLdk(key: String, namespace: LdkNamespace) async throws -> Bool {
try await awaitLdkSetup()
Logger.debug("VSS 'deleteObjectLdk' call for '\(key)'", context: "VssBackupClient")
do {
let wasDeleted = try await vssLdkDelete(key: key, namespace: namespace)
Logger.debug("VSS 'deleteObjectLdk' success for '\(key)': \(wasDeleted)", context: "VssBackupClient")
return wasDeleted
} catch {
Logger.debug("VSS 'deleteObjectLdk' error for '\(key)': \(error)", context: "VssBackupClient")
throw error
}
}

private func awaitSetup() async throws {
try await setupCoordinator.awaitSetup { [self] in
try await setup()
}
}

/// Lazily sets up the LDK client when first needed (debug screen). Independent of the app client.
private func awaitLdkSetup() async throws {
try await ldkSetupCoordinator.awaitSetup { [self] in
try await setupLdk()
}
}

private func withTimeout<T>(seconds: TimeInterval, operation: @escaping () async throws -> T) async throws -> T {
try await withThrowingTaskGroup(of: T.self) { group in
group.addTask {
Expand Down
1 change: 1 addition & 0 deletions Bitkit/ViewModels/NavigationViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ enum Route: Hashable {
// Dev settings
case blocktankRegtest
case ldkDebug
case vssDebug
case probingTool
case orders
case logs
Expand Down
Loading