Skip to content
Closed
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
8 changes: 5 additions & 3 deletions Bitkit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@
"Extensions/PaymentFailureReason+UserMessage.swift",
Models/BlocksWidgetOptions.swift,
Models/LnPeer.swift,
Models/PrimaryNode.swift,
Models/PubkyPublicKeyFormat.swift,
Models/Toast.swift,
Models/Transfer.swift,
Expand Down Expand Up @@ -199,6 +200,7 @@
Extensions/PaymentDetails.swift,
"Extensions/PaymentFailureReason+UserMessage.swift",
Models/LnPeer.swift,
Models/PrimaryNode.swift,
Models/PubkyPublicKeyFormat.swift,
Models/Toast.swift,
Services/CoreService.swift,
Expand Down Expand Up @@ -1221,10 +1223,10 @@
};
968FE13E2DFB016B0053CD7F /* XCRemoteSwiftPackageReference "ldk-node" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/synonymdev/ldk-node";
repositoryURL = "https://github.com/ovitrif/ldk-node";
requirement = {
kind = exactVersion;
version = "0.7.0-rc.78";
kind = revision;
revision = e09626aa88d43a8a7d9e969beb99920b72ad32b1;
};
};
96DEA0382DE8BBA1009932BF /* XCRemoteSwiftPackageReference "bitkit-core" */ = {
Expand Down

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

2 changes: 1 addition & 1 deletion Bitkit/Components/TabBar/TabBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ struct TabBar: View {

let hasPendingTransfersToSpending = wallet.balanceInTransferToSpending > 0

if Self.shouldOpenSpendingTransferUnavailable(
if PrimaryNodeService.shared.primaryNode == nil, Self.shouldOpenSpendingTransferUnavailable(
isSpendingWallet: navigation.currentRoute == .spendingWallet,
wallet: wallet,
hasPendingTransfersToSpending: hasPendingTransfersToSpending
Expand Down
3 changes: 2 additions & 1 deletion Bitkit/Components/WalletBalanceView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import SwiftUI
struct WalletBalanceView: View {
let type: WalletType
let sats: UInt64
var title: String?
var amountTestIdentifier: String?

@EnvironmentObject var currency: CurrencyViewModel

var body: some View {
WalletBalanceContent {
CaptionMText(type.title)
CaptionMText(title ?? type.title)
} icon: {
Image(type.imageAsset).resizable().scaledToFit().frame(width: 24, height: 24)
} amount: {
Expand Down
2 changes: 1 addition & 1 deletion Bitkit/Components/Widgets/Suggestions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ struct Suggestions: View {
case .hardware:
sheets.showSheet(.hardwareConnect)
case .transferToSpending:
sheets.showSheet(.transferUnavailable)
route = .primaryTransfer
}

if let route {
Expand Down
9 changes: 9 additions & 0 deletions Bitkit/Constants/Env.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,15 @@ enum Env {

// MARK: wallet services

/// Beignet primary run for the Lightning-first mainnet test, reached through a public TCP relay.
private static let primaryNodeUriMainnet = "024a6cc632544189896cca8e06e375bdb1d1f34a71a4c25d09a39cf0ed920a299c@bore.pub:39101"

/// The primary node a wallet uses until the user pairs another one in Settings.
/// A `PRIMARY_NODE_URI` config value overrides it, for regtest runs against a local primary.
static var defaultPrimaryNodeUri: String {
configValue("PRIMARY_NODE_URI") ?? (network == .bitcoin ? primaryNodeUriMainnet : "")
}

static let network: LDKNode.Network = {
if isUnitTest {
return .regtest
Expand Down
10 changes: 10 additions & 0 deletions Bitkit/Extensions/PaymentDetails.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,13 @@ extension PaymentDetails {
Date(timeIntervalSince1970: TimeInterval(latestUpdateTimestamp))
}
}

extension PaymentKind {
/// A payment for a BOLT 11 invoice, including one the primary node delivers through a just-in-time channel.
var isBolt11Invoice: Bool {
switch self {
case .bolt11, .bolt11Jit: true
default: false
}
}
}
2 changes: 2 additions & 0 deletions Bitkit/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
<string>$(E2E_HOMESERVER_PUBKY)</string>
<key>E2E_LOCAL_HOST</key>
<string>$(E2E_LOCAL_HOST)</string>
<key>PRIMARY_NODE_URI</key>
<string>$(PRIMARY_NODE_URI)</string>
<key>E2E_NETWORK</key>
<string>$(E2E_NETWORK)</string>
<key>TREZOR_BRIDGE</key>
Expand Down
2 changes: 2 additions & 0 deletions Bitkit/MainNavView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,8 @@ struct MainNavView: View {
case let .closeConnection(channel: channel): CloseConnectionConfirmation(channel: channel)
case .node: NodeStateView()
case .electrumSettings: ElectrumSettingsScreen()
case .primaryNodeSettings: PrimaryNodeSettingsScreen()
case .primaryTransfer: PrimaryTransferScreen()
case .rgsSettings: RgsSettingsScreen()
case .addressViewer: AddressViewer()
case .watchOnlyAccounts: WatchOnlyAccountsView()
Expand Down
10 changes: 10 additions & 0 deletions Bitkit/Managers/ScannerManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ enum ScannerContext {
case main
case send
case electrum
case primaryNode
}

extension Notification.Name {
/// Posted with the scanned node URI as its object, for the Primary node settings screen.
static let primaryNodeScanned = Notification.Name("primaryNodeScanned")
}

@MainActor
Expand Down Expand Up @@ -72,6 +78,10 @@ class ScannerManager: ObservableObject {
case .electrum:
playScanSuccess()
await handleElectrumScan(uri)
case .primaryNode:
playScanSuccess()
NotificationCenter.default.post(name: .primaryNodeScanned, object: uri)
navigation?.navigateBack()
}
}

Expand Down
82 changes: 82 additions & 0 deletions Bitkit/Models/PrimaryNode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import Foundation

/// The Lightning node that serves this wallet: it opens a channel just in time for the first receive,
/// grows it by splicing, and is the wallet's only Lightning peer.
struct PrimaryNode: Hashable {
static let uriKey = "primaryNodeUri"
static let fundingPausedKey = "primaryFundingPaused"

let nodeId: String
let address: String?

var uri: String {
address.map { "\(nodeId)@\($0)" } ?? nodeId
}

var peer: LnPeer? {
guard let address else { return nil }
return try? LnPeer(connection: "\(nodeId)@\(address)")
}

/// Parses a `pubkey` or `pubkey@host:port` node URI, as pasted or scanned.
static func parse(_ input: String) throws -> PrimaryNode {
var value = input.trimmingCharacters(in: .whitespacesAndNewlines)
for prefix in ["lightning://", "lightning:", "node://"] where value.lowercased().hasPrefix(prefix) {
value = String(value.dropFirst(prefix.count))
break
}
let parts = value.split(separator: "@", omittingEmptySubsequences: false).map(String.init)
guard (1 ... 2).contains(parts.count), !value.contains(where: \.isWhitespace) else {
throw PrimaryNodeError.invalidInput
}
let nodeId = parts[0].lowercased()
guard nodeId.range(of: "^(02|03)[0-9a-f]{64}$", options: .regularExpression) != nil else {
throw PrimaryNodeError.invalidInput
}
return try PrimaryNode(nodeId: nodeId, address: parts.count == 2 ? parseAddress(parts[1]) : nil)
}

static func parseOrNil(_ input: String) -> PrimaryNode? {
try? parse(input)
}

/// The primary the wallet uses: the paired one, else the network's default.
static func effective(savedUri: String?) -> PrimaryNode? {
let saved = savedUri ?? ""
return parseOrNil(saved.isEmpty ? Env.defaultPrimaryNodeUri : saved)
}

/// The primary saved in settings, else the network's default.
static var current: PrimaryNode? {
effective(savedUri: UserDefaults.standard.string(forKey: uriKey))
}

private static func parseAddress(_ value: String) throws -> String {
guard let separator = value.lastIndex(of: ":") else { throw PrimaryNodeError.invalidInput }
let host = String(value[..<separator]).lowercased()
guard let port = UInt16(value[value.index(after: separator)...]), port > 0,
host.range(of: "^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$", options: .regularExpression) != nil
else {
throw PrimaryNodeError.invalidInput
}
return "\(host):\(port)"
}
}

enum PrimaryNodeError: Error, Equatable {
case invalidInput
case addressUnknown
case connectionInUse
case notConfigured
}

/// Why the primary node could not provide a receive or a channel change, shown to the user.
enum PrimaryReceiveError: Error, Equatable {
case insufficientFunds
case rejected
case timedOut
case amountTooSmall
case offline
case busy
case spliceFailed
}
14 changes: 11 additions & 3 deletions Bitkit/Models/ReceiveLiquidityDecision.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
enum ReceiveAdditionalLiquidityAction: Equatable {
case none
case transferUnavailable
/// The primary node opens or grows the channel for this receive, once the user approves its fee.
case primaryReceive
}

enum ReceiveLiquiditySource: Equatable {
Expand Down Expand Up @@ -29,13 +31,19 @@ enum ReceiveLiquidityDecision {
static func additionalLiquidityAction(
source: ReceiveLiquiditySource,
invoiceAmountSats: UInt64,
inboundCapacitySats: UInt64?
inboundCapacitySats: UInt64?,
hasPrimaryNode: Bool = false
) -> ReceiveAdditionalLiquidityAction {
guard source == .spending, needsInboundLiquidity(invoiceAmountSats: invoiceAmountSats, inboundCapacitySats: inboundCapacitySats),
(inboundCapacitySats ?? 0) > 0
guard source != .savings, needsInboundLiquidity(invoiceAmountSats: invoiceAmountSats, inboundCapacitySats: inboundCapacitySats)
else {
return .none
}
if hasPrimaryNode, invoiceAmountSats > 0 {
return .primaryReceive
}
guard source == .spending, (inboundCapacitySats ?? 0) > 0 else {
return .none
}

return .transferUnavailable
}
Expand Down
2 changes: 2 additions & 0 deletions Bitkit/Models/SettingsBackupConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ enum SettingsBackupConfig {
"enableNotifications": .bool,
"quickpayAmount": .double(optional: false),
"quickpayDailyLimitMultiplier": .double(optional: false),
"primaryNodeUri": .string(optional: true),
"primaryFundingPaused": .bool,
]

static var settingsKeys: [String] {
Expand Down
36 changes: 36 additions & 0 deletions Bitkit/Resources/Localization/ar.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1565,3 +1565,39 @@
"subscriptions__allowance_auto_paid" = "مدفوع تلقائيًا";

"wallet__balance_updating" = "جارٍ تحديث الرصيد…";
"primary_node__address" = "عنوان URI للعقدة";
"primary_node__address_placeholder" = "publickey@host:port";
"primary_node__address_required" = "لا يوجد عنوان معروف لهذا المفتاح العام. امسح عنوان URI الكامل publickey@host:port ضوئيًا أو الصقه.";
"primary_node__busy" = "ما زال هناك تغيير معلق في قناتك مع عقدتك الأساسية. حاول مرة أخرى بعد تأكيده.";
"primary_node__connect" = "اتصال";
"primary_node__connected" = "تم اتصال العقدة الأساسية";
"primary_node__connection_in_use" = "حوّل رصيد الإنفاق إلى المدخرات وأغلق القناة مع عقدتك الأساسية الحالية قبل اختيار عقدة أخرى.";
"primary_node__current" = "العقدة الأساسية الحالية";
"primary_node__description" = "تفتح عقدتك الأساسية قناة الإنفاق الخاصة بك وتوسّعها عند الاستلام، بحيث يمكنك الإرسال والاستلام من رصيد واحد.";
"primary_node__error_amount_too_small" = "هذا المبلغ صغير جدًا لتغطية رسوم الاستلام. اطلب مبلغًا أكبر.";
"primary_node__error_generic" = "حدث خطأ ما في عقدتك الأساسية. حاول مرة أخرى.";
"primary_node__error_insufficient_funds" = "لا تملك عقدتك الأساسية أموالًا كافية لهذا المبلغ. جرّب مبلغًا أصغر أو استلم على السلسلة.";
"primary_node__error_offline" = "عقدتك الأساسية غير متصلة أو لا يمكن الوصول إليها. تحقق من اتصالك وحاول مرة أخرى، أو استلم على السلسلة.";
"primary_node__error_rejected" = "رفضت عقدتك الأساسية هذا المبلغ. جرّب مبلغًا مختلفًا.";
"primary_node__error_timed_out" = "لم تستجب عقدتك الأساسية. حاول مرة أخرى، أو استلم على السلسلة.";
"primary_node__funding_paused" = "التمويل التلقائي متوقف منذ آخر تحويل لك إلى المدخرات. حوّل إلى الإنفاق لاستئنافه.";
"primary_node__invalid" = "أدخل مفتاحًا عامًا صالحًا للعقدة أو عنوان URI بصيغة publickey@host:port.";
"primary_node__not_configured" = "لا توجد عقدة أساسية متصلة. عيّن واحدة في الإعدادات › متقدم › العقدة الأساسية.";
"primary_node__status_connected" = "متصلة";
"primary_node__status_offline" = "غير متصلة";
"primary_node__title" = "العقدة الأساسية";
"primary_node__trust" = "اختر عقدة تثق بها. فهي توفر سيولة قناتك وهي نظير Lightning الوحيد لمحفظتك.";
"primary_receive__capacity" = "تمت إضافة سعة الإنفاق";
"primary_receive__create" = "إنشاء طلب Lightning";
"primary_receive__fee" = "الحد الأقصى لرسوم الاستلام";
"primary_receive__minimum" = "ستستلم على الأقل";
"primary_receive__note" = "تخصم عقدتك الأساسية هذه الرسوم من الدفعة. أبقِ Bitkit مفتوحًا حتى تصل الدفعة.";
"primary_receive__title" = "مراجعة رسوم الاستلام";
"primary_transfer__confirm" = "تحويل";
"primary_transfer__fee" = "رسوم الشبكة";
"primary_transfer__note" = "ينقل Bitkit مدخراتك المؤكدة إلى قناتك مع عقدتك الأساسية، لتتمكن من إنفاقها فورًا.";
"primary_transfer__nothing" = "ليس لديك مدخرات مؤكدة لتحويلها بعد.";
"primary_transfer__started" = "يجري نقل مدخراتك إلى رصيد الإنفاق.";
"primary_transfer__started_title" = "بدأ التحويل";
"primary_transfer__title" = "التحويل إلى الإنفاق";
"primary_node__balance" = "بيتكوين";
36 changes: 36 additions & 0 deletions Bitkit/Resources/Localization/ca.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1749,3 +1749,39 @@
"subscriptions__allowance_auto_paid" = "Pagat automàticament";

"wallet__balance_updating" = "S’està actualitzant el saldo…";
"primary_node__address" = "URI del node";
"primary_node__address_placeholder" = "publickey@host:port";
"primary_node__address_required" = "Aquesta clau pública no té cap adreça coneguda. Escaneja o enganxa l'URI complet publickey@host:port.";
"primary_node__busy" = "Encara hi ha un canvi pendent al teu canal amb el node principal. Torna-ho a provar quan es confirmi.";
"primary_node__connect" = "Connecta";
"primary_node__connected" = "Node principal connectat";
"primary_node__connection_in_use" = "Transfereix el teu saldo de despesa als estalvis i tanca el canal amb el teu node principal actual abans de triar-ne un altre.";
"primary_node__current" = "Node principal actual";
"primary_node__description" = "El teu node principal obre i amplia el teu canal de despesa a mesura que reps, perquè puguis enviar i rebre des d'un únic saldo.";
"primary_node__error_amount_too_small" = "Aquest import és massa petit per cobrir la comissió de recepció. Sol·licita un import més gran.";
"primary_node__error_generic" = "Alguna cosa ha anat malament amb el teu node principal. Torna-ho a provar.";
"primary_node__error_insufficient_funds" = "El teu node principal no té prou fons per a aquest import. Prova un import més petit o rep on-chain.";
"primary_node__error_offline" = "El teu node principal està fora de línia o no es pot assolir. Comprova la connexió i torna-ho a provar, o rep on-chain.";
"primary_node__error_rejected" = "El teu node principal ha rebutjat aquest import. Prova un import diferent.";
"primary_node__error_timed_out" = "El teu node principal no ha respost. Torna-ho a provar, o rep on-chain.";
"primary_node__funding_paused" = "El finançament automàtic està en pausa des de la darrera transferència als estalvis. Transfereix a despesa per reprendre'l.";
"primary_node__invalid" = "Introdueix una clau pública de node vàlida o un URI publickey@host:port.";
"primary_node__not_configured" = "No hi ha cap node principal connectat. Defineix-ne un a Configuracions › Avançat › Node principal.";
"primary_node__status_connected" = "Connectat";
"primary_node__status_offline" = "Fora de línia";
"primary_node__title" = "Node principal";
"primary_node__trust" = "Tria un node de confiança. Proporciona la liquiditat del teu canal i és l'únic parell Lightning de la teva cartera.";
"primary_receive__capacity" = "Capacitat de despesa afegida";
"primary_receive__create" = "Crea una sol·licitud Lightning";
"primary_receive__fee" = "Comissió màxima de recepció";
"primary_receive__minimum" = "Reps almenys";
"primary_receive__note" = "El teu node principal descompta aquesta comissió del pagament. Mantén Bitkit obert fins que arribi el pagament.";
"primary_receive__title" = "Revisa la comissió de recepció";
"primary_transfer__confirm" = "Transferir";
"primary_transfer__fee" = "Tarifa de xarxa";
"primary_transfer__note" = "Bitkit mou els teus estalvis confirmats al teu canal amb el node principal, perquè els puguis gastar a l'instant.";
"primary_transfer__nothing" = "Encara no tens estalvis confirmats per transferir.";
"primary_transfer__started" = "Els teus estalvis s'estan movent al teu saldo de despesa.";
"primary_transfer__started_title" = "Transferència iniciada";
"primary_transfer__title" = "Transferir a despesa";
"primary_node__balance" = "Bitcoin";
Loading
Loading