Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Bitkit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1182,7 +1182,7 @@
repositoryURL = "https://github.com/pubky/paykit-rs";
requirement = {
kind = exactVersion;
version = "0.1.0-rc54";
version = "0.1.0-rc55";
};
};
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.

3 changes: 3 additions & 0 deletions Bitkit/AppScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,7 @@ struct AppScene: View {
private func pollIncomingPaykitPaymentRequests() async {
guard scenePhase == .active else { return }

if network.isConnected { await PubkyService.republishIdentityIfNeeded(publicKey: pubkyProfile.publicKey) }
var schedule = PaykitPaymentRequestPollingSchedule()
while !Task.isCancelled {
do {
Expand All @@ -1045,6 +1046,7 @@ struct AppScene: View {
}
let refreshMaintenance = schedule.takeMaintenanceIfDue()
if refreshMaintenance {
if network.isConnected { await PubkyService.republishIdentityIfNeeded(publicKey: pubkyProfile.publicKey) }
await PrivatePaykitService.shared.refreshKnownSavedContactEndpoints(
wallet: wallet,
reason: "payment request polling"
Expand Down Expand Up @@ -1368,6 +1370,7 @@ struct AppScene: View {
// to display balances (MoneyText returns "0" if rates are nil)
Task {
await currency.refresh()
if scenePhase == .active { await PubkyService.republishIdentityIfNeeded(publicKey: pubkyProfile.publicKey) }
if PaykitFeatureFlags.isUIEnabled {
let contactPublicKeys = contactsManager.contacts.map(\.publicKey)
await PrivatePaykitService.shared.startInitialLinkBurst(
Expand Down
103 changes: 89 additions & 14 deletions Bitkit/Services/PubkyService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ enum PubkyService {
try await PaykitSdkService.shared.initialize()
}

static func republishIdentityIfNeeded(publicKey: String? = nil) async {
await PaykitSdkService.shared.republishIdentityIfNeeded(publicKey: publicKey)
}

// MARK: - Session Management

/// Import a session secret into paykit and return the public key.
Expand Down Expand Up @@ -89,16 +93,26 @@ enum PubkyService {
}

/// Approve a pubkyauth:// request using the local secret key.
static func approveAuth(authUrl: String, expectedCapabilities: String, approvedClientID: String, secretKeyHex: String) async throws {
try await PaykitSdkService.shared.approveAuth(
static func approveAuth(
authUrl: String,
expectedCapabilities: String,
approvedClientID: String,
secretKeyHex: String,
sdkService: PaykitSdkService = .shared
) async throws {
try await sdkService.republishIdentityIfNeeded(publicKey: pubkyPublicKeyFromSecret(secretKeyHex: secretKeyHex))
try Task.checkCancellation()
try await sdkService.approveAuth(
authUrl: authUrl,
expectedCapabilities: expectedCapabilities,
approvedClientID: approvedClientID,
secretKeyHex: secretKeyHex
)
}

static func approveRingAuth(authUrl: String, secretKeyHex: String) async throws {
static func approveRingAuth(authUrl: String, secretKeyHex: String, sdkService: PaykitSdkService = .shared) async throws {
try await sdkService.republishIdentityIfNeeded(publicKey: pubkyPublicKeyFromSecret(secretKeyHex: secretKeyHex))
try Task.checkCancellation()
try await ServiceQueue.background(.core) {
try await BitkitCore.approvePubkyAuth(authUrl: authUrl, secretKeyHex: secretKeyHex)
}
Expand All @@ -108,9 +122,12 @@ enum PubkyService {
authUrl: String,
approvedClientID: String,
unsignedPayload: Data,
secretKeyHex: String
secretKeyHex: String,
sdkService: PaykitSdkService = .shared
) async throws {
try await PaykitSdkService.shared.approveAuthWithCompanionClaim(
try await sdkService.republishIdentityIfNeeded(publicKey: pubkyPublicKeyFromSecret(secretKeyHex: secretKeyHex))
try Task.checkCancellation()
try await sdkService.approveAuthWithCompanionClaim(
authUrl: authUrl,
expectedCapabilities: PubkyAuthClaim.watchOnlyAccountCapabilities,
approvedClientID: approvedClientID,
Expand Down Expand Up @@ -313,7 +330,7 @@ enum PubkyService {
// MARK: - Paykit SDK Runtime

actor PaykitSdkService {
typealias ApprovalBootstrapFactory = (String, PubkyClientConfig) throws -> PubkySessionBootstrap
typealias BootstrapFactory = (String, PubkyClientConfig) throws -> PubkySessionBootstrap

static let shared = PaykitSdkService()
private static let walletBackupDataChangedSubject = PassthroughSubject<Void, Never>()
Expand All @@ -327,18 +344,23 @@ actor PaykitSdkService {
private let paymentAdapter = PaykitSdkPaymentAdapter()
private let operationLock = PaykitSdkOperationLock()
private let pubkyClientConfig = PaykitSdkService.makePubkyClientConfig(localTestnetHost: Env.pubkyLocalTestnetHost)
private let approvalBootstrapFactory: ApprovalBootstrapFactory
private let bootstrapFactory: BootstrapFactory
private var cachedBootstrap: PubkySessionBootstrap?
private var isRepublishingIdentity = false
private var republishPublicKey: String?
private var nextIdentityRepublishAt = Date.distantPast
private var sdk: PaykitSdk?
private var activeAuthRequest: Paykit.PubkyAuthRequest?
private var activeAuthRequestID: UUID?

init(
approvalBootstrapFactory: @escaping ApprovalBootstrapFactory = PubkySessionBootstrap.withPubkyClientConfig(clientId:pubkyClient:)
bootstrapFactory: @escaping BootstrapFactory = PubkySessionBootstrap.withPubkyClientConfig(clientId:pubkyClient:)
) {
self.approvalBootstrapFactory = approvalBootstrapFactory
self.bootstrapFactory = bootstrapFactory
}

func initialize() async throws {
Task { await republishIdentityIfNeeded() }
Comment thread
jvsena42 marked this conversation as resolved.
try await operationLock.withLock {
var sdk = try handle()
do {
Expand All @@ -362,6 +384,58 @@ actor PaykitSdkService {
}
}

func republishIdentityIfNeeded(publicKey: String? = nil, now: Date = Date(), timeout: Duration = .seconds(5)) async {
guard !Task.isCancelled else { return }
// Swift FFI may ignore cancellation; keep the in-flight guard until publication actually finishes.
let (stream, continuation) = AsyncStream<Void>.makeStream()
let publication = Task {
await republishIdentity(publicKey: publicKey, now: now)
continuation.finish()
}
let deadline = Task {
do {
try await Task.sleep(for: timeout)
} catch {
return
}
Logger.warn("Stopped waiting for Pubky identity republishing", context: "PaykitSdkService")
continuation.finish()
}
defer {
publication.cancel()
deadline.cancel()
continuation.finish()
}
for await _ in stream {}
}

private func republishIdentity(publicKey: String?, now: Date) async {
guard !Task.isCancelled, !isRepublishingIdentity else { return }
isRepublishingIdentity = true
Comment thread
ben-kaufman marked this conversation as resolved.
defer { isRepublishingIdentity = false }

do {
let identity = try publicKey ?? sessionProvider.loadLocalSecretKey().map {
try Paykit.pubkyPublicKeyFromSecret(localSecretKey: $0)
}
guard let identity = identity.flatMap(PubkyPublicKeyFormat.normalized),
identity != republishPublicKey || now >= nextIdentityRepublishAt
else { return }

republishPublicKey = identity
nextIdentityRepublishAt = now.addingTimeInterval(60)
if try await bootstrap().republishIdentity(publicKey: identity) {
Comment thread
ben-kaufman marked this conversation as resolved.
nextIdentityRepublishAt = now.addingTimeInterval(30 * 60)
Logger.debug("Republished Pubky identity", context: "PaykitSdkService")
} else {
Logger.debug("Found no Pubky identity record to republish", context: "PaykitSdkService")
}
} catch is CancellationError {
} catch {
Logger.warn("Failed to republish Pubky identity: \(error)", context: "PaykitSdkService")
}
}

func currentPublicKey() async throws -> String? {
try await operationLock.withLock {
if let status = try await handle().identityStatus(), let publicKey = status.publicKey {
Expand Down Expand Up @@ -1118,6 +1192,7 @@ actor PaykitSdkService {
let sdk = try handle()
_ = try await sdk.initialize()
await publishReceiverMarkerIfLiveSessionAvailable(using: sdk)
await republishIdentityIfNeeded(publicKey: result.publicKey)
Comment thread
jvsena42 marked this conversation as resolved.
}

private func publishReceiverMarkerIfLiveSessionAvailable(using sdk: PaykitSdk) async {
Expand Down Expand Up @@ -1186,10 +1261,10 @@ actor PaykitSdkService {
}

private func bootstrap() throws -> PubkySessionBootstrap {
try PubkySessionBootstrap.withPubkyClientConfig(
clientId: Self.clientID,
pubkyClient: pubkyClientConfig
)
if let cachedBootstrap { return cachedBootstrap }
let bootstrap = try bootstrapFactory(Self.clientID, pubkyClientConfig)
cachedBootstrap = bootstrap
return bootstrap
}

func approvalBootstrap(authUrl: String, approvedClientID: String) throws -> PubkySessionBootstrap {
Expand All @@ -1200,7 +1275,7 @@ actor PaykitSdkService {
debugMessage: "Approved Pubky client ID does not match auth request"
)
}
return try approvalBootstrapFactory(requestClientID, pubkyClientConfig)
return try bootstrapFactory(requestClientID, pubkyClientConfig)
}

nonisolated static func makePubkyClientConfig(localTestnetHost: String?) -> PubkyClientConfig {
Expand Down
Loading
Loading