Skip to content
Open
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
29 changes: 25 additions & 4 deletions Bitkit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

/* Begin PBXBuildFile section */
182817C12F59A7F10055A441 /* Paykit in Frameworks */ = {isa = PBXBuildFile; productRef = 182817C02F59A7F10055A441 /* Paykit */; };
18B58A012F7A000100B58A01 /* Base58Swift in Frameworks */ = {isa = PBXBuildFile; productRef = 18B58A032F7A000100B58A01 /* Base58Swift */; };
18D65E002EB964B500252335 /* VssRustClientFfi in Frameworks */ = {isa = PBXBuildFile; productRef = 18D65DFF2EB964B500252335 /* VssRustClientFfi */; };
18D65E022EB964BD00252335 /* VssRustClientFfi in Frameworks */ = {isa = PBXBuildFile; productRef = 18D65E012EB964BD00252335 /* VssRustClientFfi */; };
3D76260F4C9C4A53B1E4A001 /* CoreBluetooth.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D76260E4C9C4A53B1E4A001 /* CoreBluetooth.framework */; };
Expand Down Expand Up @@ -173,6 +174,7 @@
Services/GeoService.swift,
Services/LightningService.swift,
Services/MigrationsService.swift,
Services/WatchOnlyAccountService.swift,
Services/RNBackupClient.swift,
Services/ServiceQueue.swift,
Services/TransferStorage.swift,
Expand All @@ -182,6 +184,7 @@
Utilities/Errors.swift,
Utilities/Keychain.swift,
Utilities/LightningAmountConversion.swift,
Utilities/LocalizeHelpers.swift,
Utilities/Logger.swift,
Utilities/StateLocker.swift,
);
Expand Down Expand Up @@ -256,6 +259,7 @@
buildActionMask = 2147483647;
files = (
182817C12F59A7F10055A441 /* Paykit in Frameworks */,
18B58A012F7A000100B58A01 /* Base58Swift in Frameworks */,
3D76260F4C9C4A53B1E4A001 /* CoreBluetooth.framework in Frameworks */,
968FDF162DFAFE230053CD7F /* LDKNode in Frameworks */,
18D65E002EB964B500252335 /* VssRustClientFfi in Frameworks */,
Expand Down Expand Up @@ -397,6 +401,7 @@
4AAB08C92E1FE77600BA63DF /* Lottie */,
18D65DFF2EB964B500252335 /* VssRustClientFfi */,
182817C02F59A7F10055A441 /* Paykit */,
18B58A032F7A000100B58A01 /* Base58Swift */,
);
productName = Bitkit;
productReference = 96FE1F612C2DE6AA006D0C8B /* Bitkit.app */;
Expand Down Expand Up @@ -507,6 +512,7 @@
4AAB08C82E1FE77600BA63DF /* XCRemoteSwiftPackageReference "lottie-ios" */,
18D65DFE2EB9649F00252335 /* XCRemoteSwiftPackageReference "vss-rust-client-ffi" */,
182817BF2F59A7F10055A441 /* XCRemoteSwiftPackageReference "paykit-rs" */,
18B58A022F7A000100B58A01 /* XCRemoteSwiftPackageReference "Base58Swift" */,
);
productRefGroup = 96FE1F622C2DE6AA006D0C8B /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -570,8 +576,8 @@
inputFileListPaths = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${WRAPPER_NAME}/Frameworks/LDKNodeFFI.framework/_CodeSignature/CodeResources",
"${TARGET_BUILD_DIR}/${WRAPPER_NAME}/Frameworks/vss_rust_client_ffiFFI.framework/_CodeSignature/CodeResources",
"${TARGET_BUILD_DIR}/${WRAPPER_NAME}/Frameworks/LDKNodeFFI.framework/_CodeSignature",
"${TARGET_BUILD_DIR}/${WRAPPER_NAME}/Frameworks/vss_rust_client_ffiFFI.framework/_CodeSignature",
);
name = "Remove Static Framework Stubs";
outputFileListPaths = (
Expand Down Expand Up @@ -765,6 +771,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) BITKIT_NOTIFICATION_EXTENSION";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
Expand Down Expand Up @@ -797,6 +804,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) BITKIT_NOTIFICATION_EXTENSION";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
Expand Down Expand Up @@ -1169,7 +1177,15 @@
repositoryURL = "https://github.com/pubky/paykit-rs";
requirement = {
kind = exactVersion;
version = "0.1.0-rc33";
version = "0.1.0-rc36";
};
};
18B58A022F7A000100B58A01 /* XCRemoteSwiftPackageReference "Base58Swift" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/keefertaylor/Base58Swift";
requirement = {
kind = exactVersion;
version = 2.1.14;
};
};
18D65DFE2EB9649F00252335 /* XCRemoteSwiftPackageReference "vss-rust-client-ffi" */ = {
Expand All @@ -1193,7 +1209,7 @@
repositoryURL = "https://github.com/synonymdev/ldk-node";
requirement = {
kind = exactVersion;
version = "0.7.0-rc.39";
version = "0.7.0-rc.56";
};
};
96DEA0382DE8BBA1009932BF /* XCRemoteSwiftPackageReference "bitkit-core" */ = {
Expand Down Expand Up @@ -1228,6 +1244,11 @@
package = 182817BF2F59A7F10055A441 /* XCRemoteSwiftPackageReference "paykit-rs" */;
productName = Paykit;
};
18B58A032F7A000100B58A01 /* Base58Swift */ = {
isa = XCSwiftPackageProductDependency;
package = 18B58A022F7A000100B58A01 /* XCRemoteSwiftPackageReference "Base58Swift" */;
productName = Base58Swift;
};
18D65DFF2EB964B500252335 /* VssRustClientFfi */ = {
isa = XCSwiftPackageProductDependency;
package = 18D65DFE2EB9649F00252335 /* XCRemoteSwiftPackageReference "vss-rust-client-ffi" */;
Expand Down

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

19 changes: 15 additions & 4 deletions Bitkit/Components/SettingsRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ enum SettingsRowRightIcon {

struct SettingsRow: View {
let title: String
let subtitle: String?
let iconName: String?
let iconColor: Color?
let rightText: String?
Expand All @@ -33,6 +34,7 @@ struct SettingsRow: View {

init(
title: String,
subtitle: String? = nil,
iconName: String? = nil,
iconColor: Color? = .brandAccent,
rightText: String? = nil,
Expand All @@ -42,6 +44,7 @@ struct SettingsRow: View {
testIdentifier: String? = nil
) {
self.title = title
self.subtitle = subtitle
self.iconName = iconName
self.iconColor = iconColor
self.rightText = rightText
Expand All @@ -59,9 +62,17 @@ struct SettingsRow: View {
.padding(.trailing, 8)
}

BodyMText(title, textColor: .textPrimary)
.lineLimit(1)
.truncationMode(.tail)
VStack(alignment: .leading, spacing: 4) {
BodyMText(title, textColor: .textPrimary)
.lineLimit(1)
.truncationMode(.tail)

if let subtitle {
BodySText(subtitle, textColor: .textSecondary)
.lineLimit(1)
.truncationMode(.middle)
}
}

Spacer()

Expand Down Expand Up @@ -98,7 +109,7 @@ struct SettingsRow: View {
}
}
}
.frame(height: 50)
.frame(minHeight: subtitle == nil ? 50 : 66)

CustomDivider()
}
Expand Down
1 change: 1 addition & 0 deletions Bitkit/MainNavView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,7 @@ struct MainNavView: View {
case .electrumSettings: ElectrumSettingsScreen()
case .rgsSettings: RgsSettingsScreen()
case .addressViewer: AddressViewer()
case .watchOnlyAccounts: WatchOnlyAccountsView()
case .devSettings: DevSettingsView()

// Dev settings
Expand Down
2 changes: 2 additions & 0 deletions Bitkit/Models/BackupPayloads.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ struct WalletBackupV1: Codable {
let transfers: [Transfer]
let privatePaykitHighestReservedReceiveIndexByAddressType: [String: UInt32]?
let paykitSdkBackupState: String?
let watchOnlyAccounts: [WatchOnlyAccountRecord]?
let watchOnlyAccountAllocationState: WatchOnlyAccountAllocationState?
}

struct MetadataBackupV1: Codable {
Expand Down
48 changes: 47 additions & 1 deletion Bitkit/Models/PubkyAuthRequest.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
import Foundation
import Paykit

enum PubkyAuthClaim: String, Equatable {
case watchOnlyAccountV1 = "watch-only-account-v1"

static let queryParameter = "x-bitkit-claim"
static let watchOnlyAccountCapabilities = "/pub/paykit/v0/bitkit/server/:rw"
}

enum PubkyAuthRequestError: Error, Equatable {
case invalidUrl
case missingBitkitClaim
case duplicateBitkitClaim
case unsupportedBitkitClaim(String)
case invalidBitkitClaimCapabilities
}

// MARK: - PubkyAuth Permission

struct PubkyAuthPermission {
Expand All @@ -24,22 +39,53 @@ struct PubkyAuthRequest {
let capabilities: String
let permissions: [PubkyAuthPermission]
let serviceNames: [String]
let bitkitClaim: PubkyAuthClaim?

static func parse(url: String) throws -> PubkyAuthRequest {
let details = try Paykit.parsePubkyAuthUrl(authUrl: url)
let capabilities = details.capabilities ?? ""
let permissions = parseCapabilities(capabilities)
let serviceNames = permissions.compactMap { extractServiceName($0.path) }
let bitkitClaim = try parseBitkitClaim(url: url, capabilities: capabilities)
return PubkyAuthRequest(
rawUrl: url,
kind: details.kind,
relay: details.relayUrl ?? "",
capabilities: capabilities,
permissions: permissions,
serviceNames: serviceNames
serviceNames: serviceNames,
bitkitClaim: bitkitClaim
)
}

static func parseBitkitClaim(url: String, capabilities: String) throws -> PubkyAuthClaim? {
guard let components = URLComponents(string: url) else {
throw PubkyAuthRequestError.invalidUrl
}

let claimValues = components.queryItems?
.filter { $0.name == PubkyAuthClaim.queryParameter }
.map { $0.value ?? "" } ?? []

guard claimValues.count <= 1 else {
throw PubkyAuthRequestError.duplicateBitkitClaim
}
guard let claimValue = claimValues.first else {
if capabilities == PubkyAuthClaim.watchOnlyAccountCapabilities {
throw PubkyAuthRequestError.missingBitkitClaim
}
return nil
}
guard let claim = PubkyAuthClaim(rawValue: claimValue) else {
throw PubkyAuthRequestError.unsupportedBitkitClaim(claimValue)
}
guard capabilities == PubkyAuthClaim.watchOnlyAccountCapabilities else {
throw PubkyAuthRequestError.invalidBitkitClaimCapabilities
}

return claim
}

static func parseCapabilities(_ caps: String) -> [PubkyAuthPermission] {
caps
.split(separator: ",")
Expand Down
27 changes: 27 additions & 0 deletions Bitkit/Resources/Localization/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,14 @@
"pubky_auth__description_prefix" = "A service is requesting permission to access and edit your ";
"pubky_auth__description_suffix" = " data.";
"pubky_auth__requested_permissions" = "REQUESTED PERMISSIONS";
"pubky_auth__watch_only_account_title" = "WATCH-ONLY BITCOIN ACCOUNT";
"pubky_auth__watch_only_account_description" = "Share a watch-only Bitcoin account. This lets the service generate receive addresses and view this account's transactions and balance, but not spend funds.";
"pubky_auth__watch_only_account_default_name" = "{service} account";
"pubky_auth__watch_only_account_fallback_name" = "Paykit server account";
"pubky_auth__watch_only_account_name_error" = "Enter an account name between 1 and 64 characters.";
"pubky_auth__watch_only_account_name_label" = "ACCOUNT NAME";
"pubky_auth__watch_only_account_name_placeholder" = "Name this account";
"pubky_auth__watch_only_account_xpub_error" = "Bitkit could not create a valid account xpub.";
"pubky_auth__trust_warning" = "Make sure you trust the service, browser, or device before authorizing with your pubky.";
"pubky_auth__authorizing" = "Authorizing...";
"pubky_auth__success_title" = "Authorization Successful";
Expand All @@ -677,6 +685,25 @@
"pubky_auth__use_ring_desc" = "Your identity was created with Pubky Ring. Open Ring to approve this request.";
"pubky_auth__invalid_request" = "Invalid auth request";
"pubky_auth__approval_failed" = "Authorization Failed";
"watch_only_accounts__active_section" = "Active accounts";
"watch_only_accounts__copy_xpub" = "Copy xpub";
"watch_only_accounts__description" = "Each approved Paykit server gets a separate Bitcoin account. Turn tracking off to unload an account without deleting its wallet history.";
"watch_only_accounts__details_title" = "Account details";
"watch_only_accounts__empty_description" = "Accounts appear here after you approve a Paykit server setup request.";
"watch_only_accounts__empty_title" = "No server accounts";
"watch_only_accounts__name" = "ACCOUNT NAME";
"watch_only_accounts__name_placeholder" = "Account name";
"watch_only_accounts__name_saved" = "Account name saved";
"watch_only_accounts__pending_description" = "These accounts were created locally, but setup did not finish. Retry the same authorization to reuse the account.";
"watch_only_accounts__pending_section" = "Incomplete setup";
"watch_only_accounts__save_name" = "Save name";
"watch_only_accounts__setup_not_confirmed" = "Setup not confirmed";
"watch_only_accounts__setup_not_finished" = "Setup did not finish. Retry the same authorization to use this account.";
"watch_only_accounts__title" = "Server Accounts";
"watch_only_accounts__tracking" = "Track account";
"watch_only_accounts__tracking_disabled" = "Account tracking disabled";
"watch_only_accounts__tracking_enabled" = "Account tracking enabled";
"watch_only_accounts__xpub" = "EXTENDED PUBLIC KEY";
"settings__settings" = "Settings";
"settings__dev_enabled_title" = "Dev Options Enabled";
"settings__dev_enabled_message" = "Developer options are now enabled throughout the app.";
Expand Down
17 changes: 16 additions & 1 deletion Bitkit/Services/BackupService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,10 @@ class BackupService {
let payload = try JSONDecoder().decode(WalletBackupV1.self, from: dataBytes)
try TransferStorage.shared.upsertList(payload.transfers)
await PrivatePaykitAddressReservationStore.shared.restoreBackup(payload.privatePaykitHighestReservedReceiveIndexByAddressType)
try await WatchOnlyAccountManager.shared.restore(
payload.watchOnlyAccounts,
allocationState: payload.watchOnlyAccountAllocationState
)
pendingPaykitSdkBackupState = payload.paykitSdkBackupState
didRestoreWalletBackup = true

Expand Down Expand Up @@ -369,6 +373,14 @@ class BackupService {
}
.store(in: &cancellables)

WatchOnlyAccountStore.walletBackupDataChangedPublisher
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.sink { [weak self] _ in
guard let self, !self.shouldSkipBackup() else { return }
markBackupRequired(category: .wallet)
}
.store(in: &cancellables)

// ACTIVITIES
CoreService.shared.activity.activitiesChangedPublisher
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
Expand Down Expand Up @@ -703,12 +715,15 @@ class BackupService {
let transfers = try TransferStorage.shared.getAll()
let privatePaykitHighestReservedReceiveIndexByAddressType = await PrivatePaykitAddressReservationStore.shared.backupSnapshot()
let paykitSdkBackupState = try await PrivatePaykitService.shared.backupSnapshot()
let watchOnlyAccountSnapshot = try WatchOnlyAccountStore.backupSnapshot()
let payload = WalletBackupV1(
version: 1,
createdAt: UInt64(Date().timeIntervalSince1970 * 1000),
transfers: transfers,
privatePaykitHighestReservedReceiveIndexByAddressType: privatePaykitHighestReservedReceiveIndexByAddressType,
paykitSdkBackupState: paykitSdkBackupState
paykitSdkBackupState: paykitSdkBackupState,
watchOnlyAccounts: watchOnlyAccountSnapshot.accounts,
watchOnlyAccountAllocationState: watchOnlyAccountSnapshot.allocationState
)
return try JSONEncoder().encode(payload)

Expand Down
Loading
Loading