Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
245565c
chore: add geoblock env variable
jvsena42 Nov 18, 2025
a0e70f9
feat: add env variable check to checkGeoStatus
jvsena42 Nov 18, 2025
2a811e9
ci: add GEO env variable
jvsena42 Nov 18, 2025
2a0899a
Merge branch 'master' into chore/geoblocking-env
jvsena42 Nov 19, 2025
3754e0e
fix: add contextual type to nil return
jvsena42 Nov 19, 2025
d4cd419
chore: add swift compiler flags
jvsena42 Nov 19, 2025
986b6e3
chore: add geoblock helper methods
jvsena42 Nov 19, 2025
65c983e
fix: filter only non cLSP channels and fix milisats conversion
jvsena42 Nov 19, 2025
556212a
feat: add geoblock checks to lightning send
jvsena42 Nov 19, 2025
7218256
feat: update inbound calc
jvsena42 Nov 19, 2025
ca8d780
feat: display non LSP inbound
jvsena42 Nov 19, 2025
3361a0b
feat: pass geoblock status
jvsena42 Nov 19, 2025
e3148f8
feat: update canSend call
jvsena42 Nov 19, 2025
8127515
feat: block transfer to spending
jvsena42 Nov 19, 2025
fbbab2e
fix: error parameter
jvsena42 Nov 19, 2025
2a0576e
fix: set geoblocked parameter
jvsena42 Nov 19, 2025
ded5b69
fix: uniffy tab visibility
jvsena42 Nov 19, 2025
4f54600
feat: dont display qr spending if only has blocktank channels
jvsena42 Nov 19, 2025
b38db78
Merge branch 'master' into chore/geoblocking-env
jvsena42 Nov 19, 2025
91e64d8
chore: create a singleton service to geoblock
jvsena42 Nov 19, 2025
e04f979
refactor: replace geoblock parameters
jvsena42 Nov 20, 2025
3888486
chore: remove unused variable
jvsena42 Nov 20, 2025
f4f60a5
chore: remove comments
jvsena42 Nov 20, 2025
a0b7f7e
chore: geo build env name
jvsena42 Nov 20, 2025
b18e50b
fix: remove geoservice from ui tests
jvsena42 Nov 20, 2025
f93e748
Merge branch 'master' into chore/geoblocking-env
jvsena42 Nov 20, 2025
b3b53e8
Merge branch 'master' into chore/geoblocking-env
jvsena42 Nov 20, 2025
9bf556c
fic: change the Xcode project configuration to append CHECK_GEOBLOCK …
jvsena42 Nov 20, 2025
0ef51be
Merge branch 'master' into chore/geoblocking-env
jvsena42 Nov 20, 2025
a9c4c0e
Merge branch 'master' into chore/geoblocking-env
jvsena42 Nov 21, 2025
110304d
Merge branch 'master' into chore/geoblocking-env
jvsena42 Nov 24, 2025
5ced847
Merge branch 'master' into chore/geoblocking-env
jvsena42 Nov 24, 2025
ad91f6c
fix: refresh geoblocking on qr sheet display
jvsena42 Nov 24, 2025
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 .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ jobs:
CHATWOOT_API: ${{ secrets.CHATWOOT_API }}
SIMULATOR_NAME: "iPhone 17"
OS_VERSION: "latest"
GEO: false
run: |
echo "=== Building iOS app ==="
echo "Using simulator: $SIMULATOR_NAME (iOS $OS_VERSION)"
Expand Down
5 changes: 4 additions & 1 deletion Bitkit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
Models/LnPeer.swift,
Models/Toast.swift,
Services/CoreService.swift,
Services/GeoService.swift,
Services/LightningService.swift,
Services/MigrationsService.swift,
Services/ServiceQueue.swift,
Expand All @@ -113,6 +114,7 @@
Models/ReceivedTxSheetDetails.swift,
Models/Toast.swift,
Services/CoreService.swift,
Services/GeoService.swift,
Services/LightningService.swift,
Services/ServiceQueue.swift,
Services/VssStoreIdProvider.swift,
Expand Down Expand Up @@ -591,7 +593,7 @@
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG CHECK_GEOBLOCK $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
Expand Down Expand Up @@ -645,6 +647,7 @@
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "CHECK_GEOBLOCK $(inherited)";
SWIFT_COMPILATION_MODE = wholemodule;
};
name = Release;
Expand Down
6 changes: 6 additions & 0 deletions Bitkit/Constants/Env.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ enum Env {
#endif
static let dustLimit = 547

#if CHECK_GEOBLOCK
static let isGeoblockingEnabled = true
#else
static let isGeoblockingEnabled = ProcessInfo.processInfo.environment["GEO"] == "true"
#endif

/// The current execution context of the app
static var currentExecutionContext: ExecutionContext {
return Bundle.main.bundleIdentifier?.lowercased().contains("notification") == true ? .pushNotificationExtension : .foregroundApp
Expand Down
12 changes: 8 additions & 4 deletions Bitkit/Services/CoreService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@

var isConfirmed = false
var confirmedTimestamp: UInt64?
if case let .confirmed(blockHash, height, blockTimestamp) = txStatus {

Check warning on line 130 in Bitkit/Services/CoreService.swift

View workflow job for this annotation

GitHub Actions / Run Integration Tests

immutable value 'height' was never used; consider replacing with '_' or removing it

Check warning on line 130 in Bitkit/Services/CoreService.swift

View workflow job for this annotation

GitHub Actions / Run Integration Tests

immutable value 'blockHash' was never used; consider replacing with '_' or removing it

Check warning on line 130 in Bitkit/Services/CoreService.swift

View workflow job for this annotation

GitHub Actions / Run Tests

immutable value 'height' was never used; consider replacing with '_' or removing it

Check warning on line 130 in Bitkit/Services/CoreService.swift

View workflow job for this annotation

GitHub Actions / Run Tests

immutable value 'blockHash' was never used; consider replacing with '_' or removing it
isConfirmed = true
confirmedTimestamp = blockTimestamp
}
Expand Down Expand Up @@ -250,7 +250,7 @@
if !preservedDoesExist && isConfirmed {
try await self.markReplacementTransactionsAsRemoved(originalTxId: txid)
}
} else if case let .bolt11(hash, preimage, secret, description, bolt11) = payment.kind {

Check warning on line 253 in Bitkit/Services/CoreService.swift

View workflow job for this annotation

GitHub Actions / Run Integration Tests

immutable value 'secret' was never used; consider replacing with '_' or removing it

Check warning on line 253 in Bitkit/Services/CoreService.swift

View workflow job for this annotation

GitHub Actions / Run Integration Tests

immutable value 'hash' was never used; consider replacing with '_' or removing it

Check warning on line 253 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 { continue }

Expand Down Expand Up @@ -1378,11 +1378,15 @@
}

func checkGeoStatus() async throws -> Bool? {
try await ServiceQueue.background(.core) {
if !Env.isGeoblockingEnabled {
return false
}

return try await ServiceQueue.background(.core) {
Logger.info("Checking geo status...", context: "GeoCheck")
guard let url = URL(string: Env.geoCheckUrl) else {
Logger.error("Invalid geocheck URL: \(Env.geoCheckUrl)", context: "GeoCheck")
return nil
return nil as Bool?
}

let (_, response) = try await URLSession.shared.data(from: url)
Expand All @@ -1397,10 +1401,10 @@
return true
default:
Logger.warn("Unexpected status code: \(httpResponse.statusCode)", context: "GeoCheck")
return nil
return nil as Bool?
}
}
return nil
return nil as Bool?
}
}
}
36 changes: 36 additions & 0 deletions Bitkit/Services/GeoService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Foundation
import SwiftUI

/// Service responsible for managing geoblocking state
class GeoService: ObservableObject {
static let shared = GeoService()

/// Current geoblocking status
/// - `false`: User is not geoblocked (default/fallback if check fails)
/// - `true`: User is geoblocked
@Published var isGeoBlocked: Bool = false

private let coreService: CoreService

private init(coreService: CoreService = .shared) {
self.coreService = coreService
}

/// Checks the current geoblocking status and updates the published state
func checkGeoStatus() async {
do {
let result = try await coreService.checkGeoStatus()

let newValue = result ?? false

await MainActor.run {
self.isGeoBlocked = newValue
}

Logger.info("Geo status check completed: isGeoBlocked=\(isGeoBlocked)", context: "GeoService")
} catch {
isGeoBlocked = false
Logger.error("Failed to check geo status: \(error)", context: "GeoService")
}
}
}
45 changes: 42 additions & 3 deletions Bitkit/Services/LightningService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -298,19 +298,23 @@
}

/// Checks if we have the correct outbound capacity to send the amount
/// - Parameter amountSats
/// - Parameter amountSats: Amount to send in satoshis
/// - Returns: True if we can send the amount
func canSend(amountSats: UInt64) -> Bool {
guard let channels else {
Logger.warn("Channels not available")
return false
}

// When geoblocked, only count non-LSP channels
let isGeoblocked = GeoService.shared.isGeoBlocked
let channelsToUse = isGeoblocked ? getNonLspChannels() : channels

let totalNextOutboundHtlcLimitSats =
channels
channelsToUse
.filter(\.isUsable)
.map(\.nextOutboundHtlcLimitMsat)
.reduce(0, +) * 1000
.reduce(0, +) / 1000

guard totalNextOutboundHtlcLimitSats > amountSats else {
Logger.warn("Insufficient outbound capacity: \(totalNextOutboundHtlcLimitSats) < \(amountSats)")
Expand Down Expand Up @@ -370,6 +374,16 @@
throw AppError(serviceError: .nodeNotSetup)
}

// When geoblocked, verify we have external (non-LSP) peers
let isGeoblocked = GeoService.shared.isGeoBlocked
if isGeoblocked && !hasExternalPeers() {
Logger.error("Cannot send Lightning payment when geoblocked without external peers")
throw AppError(
message: "Lightning send unavailable",
debugMessage: "You need channels with non-Blocktank nodes to send Lightning payments."
)
}

Logger.info("Paying bolt11: \(bolt11)")

do {
Expand Down Expand Up @@ -412,7 +426,7 @@
}

func closeChannel(_ channel: ChannelDetails, force: Bool = false, forceCloseReason: String? = nil) async throws {
guard let node else {

Check warning on line 429 in Bitkit/Services/LightningService.swift

View workflow job for this annotation

GitHub Actions / Run Integration Tests

value 'node' was defined but never used; consider replacing with boolean test

Check warning on line 429 in Bitkit/Services/LightningService.swift

View workflow job for this annotation

GitHub Actions / Run Tests

value 'node' was defined but never used; consider replacing with boolean test
throw AppError(serviceError: .nodeNotStarted)
}

Expand Down Expand Up @@ -546,6 +560,31 @@
var peers: [PeerDetails]? { node?.listPeers() }
var channels: [ChannelDetails]? { node?.listChannels() }
var payments: [PaymentDetails]? { node?.listPayments() }

/// Returns LSP (Blocktank) peer node IDs
func getLspPeerNodeIds() -> [String] {
return Env.trustedLnPeers.map(\.nodeId)
}

/// Checks if there are connected peers other than LSP peers
/// Used for geoblocking to determine if Lightning operations can proceed
func hasExternalPeers() -> Bool {
guard let peers else { return false }
let lspNodeIds = Set(getLspPeerNodeIds())
return peers.contains { peer in
!lspNodeIds.contains(peer.nodeId)
}
}

/// Filters channels to exclude LSP channels
/// Used for geoblocking to only allow operations through non-Blocktank channels
func getNonLspChannels() -> [ChannelDetails] {
guard let channels else { return [] }
let lspNodeIds = Set(getLspPeerNodeIds())
return channels.filter { channel in
!lspNodeIds.contains(channel.counterpartyNodeId)
}
}
}

// MARK: Events
Expand All @@ -571,15 +610,15 @@

// TODO: actual event handler
switch event {
case let .paymentSuccessful(paymentId, paymentHash, paymentPreimage, feePaidMsat):

Check warning on line 613 in Bitkit/Services/LightningService.swift

View workflow job for this annotation

GitHub Actions / Run Integration Tests

immutable value 'paymentPreimage' was never used; consider replacing with '_' or removing it

Check warning on line 613 in Bitkit/Services/LightningService.swift

View workflow job for this annotation

GitHub Actions / Run Tests

immutable value 'paymentPreimage' was never used; consider replacing with '_' or removing it
Logger.info("✅ Payment successful: paymentId: \(paymentId ?? "?") paymentHash: \(paymentHash) feePaidMsat: \(feePaidMsat ?? 0)")
case let .paymentFailed(paymentId, paymentHash, reason):
Logger.info(
"❌ Payment failed: paymentId: \(paymentId ?? "?") paymentHash: \(paymentHash ?? "") reason: \(reason.debugDescription)"
)
case let .paymentReceived(paymentId, paymentHash, amountMsat, feePaidMsat):

Check warning on line 619 in Bitkit/Services/LightningService.swift

View workflow job for this annotation

GitHub Actions / Run Integration Tests

immutable value 'feePaidMsat' was never used; consider replacing with '_' or removing it

Check warning on line 619 in Bitkit/Services/LightningService.swift

View workflow job for this annotation

GitHub Actions / Run Tests

immutable value 'feePaidMsat' was never used; consider replacing with '_' or removing it
Logger.info("🤑 Payment received: paymentId: \(paymentId ?? "?") paymentHash: \(paymentHash) amountMsat: \(amountMsat)")
case let .paymentClaimable(paymentId, paymentHash, claimableAmountMsat, claimDeadline, customRecords):

Check warning on line 621 in Bitkit/Services/LightningService.swift

View workflow job for this annotation

GitHub Actions / Run Integration Tests

immutable value 'customRecords' was never used; consider replacing with '_' or removing it

Check warning on line 621 in Bitkit/Services/LightningService.swift

View workflow job for this annotation

GitHub Actions / Run Integration Tests

immutable value 'claimDeadline' was never used; consider replacing with '_' or removing it

Check warning on line 621 in Bitkit/Services/LightningService.swift

View workflow job for this annotation

GitHub Actions / Run Tests

immutable value 'customRecords' was never used; consider replacing with '_' or removing it

Check warning on line 621 in Bitkit/Services/LightningService.swift

View workflow job for this annotation

GitHub Actions / Run Tests

immutable value 'claimDeadline' was never used; consider replacing with '_' or removing it
Logger.info(
"🫰 Payment claimable: paymentId: \(paymentId) paymentHash: \(paymentHash) claimableAmountMsat: \(claimableAmountMsat)"
)
Expand All @@ -604,7 +643,7 @@

if let channel {
await registerClosedChannel(channel: channel, reason: reasonString)
await MainActor.run {

Check warning on line 646 in Bitkit/Services/LightningService.swift

View workflow job for this annotation

GitHub Actions / Run Integration Tests

result of call to 'run(resultType:body:)' is unused

Check warning on line 646 in Bitkit/Services/LightningService.swift

View workflow job for this annotation

GitHub Actions / Run Tests

result of call to 'run(resultType:body:)' is unused
channelCache.removeValue(forKey: channelIdString)
}
} else {
Expand Down
11 changes: 11 additions & 0 deletions Bitkit/Services/TransferService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ class TransferService {
fundingTxId: String? = nil,
lspOrderId: String? = nil
) async throws -> String {
// When geoblocked, block transfers to spending that involve LSP (Blocktank)
// toSpending with lspOrderId means it's a Blocktank LSP channel order
let isGeoblocked = GeoService.shared.isGeoBlocked
if isGeoblocked && type.isToSpending() && lspOrderId != nil {
Logger.error("Cannot create LSP transfer when geoblocked", context: "TransferService")
throw AppError(
message: "Transfer unavailable",
debugMessage: "Transfer to spending via Blocktank is not available in your region."
)
}

let id = UUID().uuidString
let createdAt = UInt64(Date().timeIntervalSince1970)

Expand Down
9 changes: 2 additions & 7 deletions Bitkit/ViewModels/AppViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ class AppViewModel: ObservableObject {
@Published var lnurlPayData: LnurlPayData?
@Published var lnurlWithdrawData: LnurlWithdrawData?

@Published var isGeoBlocked: Bool? = nil

// Onboarding
@AppStorage("hasSeenContactsIntro") var hasSeenContactsIntro: Bool = false
@AppStorage("hasSeenProfileIntro") var hasSeenProfileIntro: Bool = false
Expand Down Expand Up @@ -91,11 +89,8 @@ class AppViewModel: ObservableObject {
deinit {}

func checkGeoStatus() async {
do {
isGeoBlocked = try await coreService.checkGeoStatus()
} catch {
Logger.error("Failed to check geo status: \(error)", context: "GeoCheck")
}
// Delegate to GeoService singleton for centralized geo-blocking management
await GeoService.shared.checkGeoStatus()
}

func wipe() async throws {
Expand Down
31 changes: 30 additions & 1 deletion Bitkit/ViewModels/WalletViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,27 @@ class WalletViewModel: ObservableObject {
return capacity
}

/// Total inbound Lightning capacity excluding LSP (Blocktank) channels
/// Used when geoblocked to show only non-Blocktank receiving capacity
var totalNonLspInboundLightningSats: UInt64? {
let nonLspChannels = lightningService.getNonLspChannels()
guard !nonLspChannels.isEmpty else {
return nil
}

var capacity: UInt64 = 0
for channel in nonLspChannels {
capacity += channel.inboundCapacityMsat / 1000
}
return capacity
}

/// Check if there are non-LSP (non-Blocktank) channels available
/// Used for geoblocking to determine if Lightning operations can proceed
func hasNonLspChannels() -> Bool {
return !lightningService.getNonLspChannels().isEmpty
}

func refreshBip21(forceRefreshBolt11: Bool = false) async throws {
// Get old payment ID and tags before refreshing (which may change payment ID)
let oldPaymentId = await paymentId()
Expand Down Expand Up @@ -524,7 +545,15 @@ class WalletViewModel: ObservableObject {

let amountSats = invoiceAmountSats > 0 ? invoiceAmountSats : nil

if channels?.count ?? 0 > 0 {
// When geoblocked, only create Lightning invoice if we have non-LSP channels
let isGeoblocked = GeoService.shared.isGeoBlocked
let hasUsableChannels: Bool = if isGeoblocked {
hasNonLspChannels()
} else {
channels?.count ?? 0 > 0
}

if hasUsableChannels {
if forceRefreshBolt11 || bolt11.isEmpty {
bolt11 = try await createInvoice(amountSats: amountSats, note: invoiceNote)
} else {
Expand Down
2 changes: 1 addition & 1 deletion Bitkit/Views/Onboarding/OnboardingSlider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ struct OnboardingSlider: View {
imageName: "lightning",
title: t("onboarding__slide1_header"),
text: tTodo("Enjoy instant and cheap payments with friends, family, and merchants on the Lightning Network."),
disclaimerText: app.isGeoBlocked == true ? t("onboarding__slide1_note") : nil,
disclaimerText: GeoService.shared.isGeoBlocked ? t("onboarding__slide1_note") : nil,
accentColor: .purpleAccent
)
.tag(1)
Expand Down
6 changes: 3 additions & 3 deletions Bitkit/Views/Transfer/FundingOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ struct FundingOptions: View {
@EnvironmentObject var wallet: WalletViewModel

var text: String {
if app.isGeoBlocked == true {
if GeoService.shared.isGeoBlocked {
return t("lightning__funding__text_blocked")
} else {
return t("lightning__funding__text")
Expand All @@ -33,7 +33,7 @@ struct FundingOptions: View {
RectangleButton(
icon: "transfer",
title: t("lightning__funding__button1"),
isDisabled: wallet.totalOnchainSats == 0 || app.isGeoBlocked == true,
isDisabled: wallet.totalOnchainSats == 0 || GeoService.shared.isGeoBlocked,
testID: "FundTransfer"
) {
if app.hasSeenTransferToSpendingIntro {
Expand All @@ -46,7 +46,7 @@ struct FundingOptions: View {
RectangleButton(
icon: "qr",
title: t("lightning__funding__button2"),
isDisabled: app.isGeoBlocked == true,
isDisabled: GeoService.shared.isGeoBlocked,
testID: "FundReceive"
) {
navigation.reset()
Expand Down
7 changes: 5 additions & 2 deletions Bitkit/Views/Wallets/Receive/ReceiveEdit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,12 @@ struct ReceiveEdit: View {
}

private func needsAdditionalCjit() -> Bool {
let isGeoBlocked = app.isGeoBlocked ?? false
let isGeoBlocked = GeoService.shared.isGeoBlocked
let minimumAmount = blocktank.minCjitSats ?? 0
let inboundCapacity = wallet.totalInboundLightningSats ?? 0
// When geoblocked, only count non-LSP inbound capacity
let inboundCapacity = isGeoBlocked
? (wallet.totalNonLspInboundLightningSats ?? 0)
: (wallet.totalInboundLightningSats ?? 0)
let invoiceAmount = amountViewModel.amountSats

// Calculate maxClientBalance using TransferViewModel
Expand Down
Loading
Loading