diff --git a/.github/workflows/ai-device-tests.yml b/.github/workflows/ai-device-tests.yml index 3c43c3f1b..18075d77b 100644 --- a/.github/workflows/ai-device-tests.yml +++ b/.github/workflows/ai-device-tests.yml @@ -107,7 +107,6 @@ jobs: E2E: "true" E2E_BACKEND: "local" E2E_NETWORK: "regtest" - GEO: "false" SIMULATOR_NAME: ${{ inputs.simulator_name }} SIMULATOR_OS: ${{ inputs.simulator_os }} run: | diff --git a/.github/workflows/e2e-staging.yml b/.github/workflows/e2e-staging.yml index af24b7b29..d4d97ac82 100644 --- a/.github/workflows/e2e-staging.yml +++ b/.github/workflows/e2e-staging.yml @@ -65,7 +65,6 @@ jobs: env: GITHUB_ACTOR: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GEO: false E2E_BACKEND: network E2E_NETWORK: regtest run: | diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index b0f739e28..e4efb6293 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -93,7 +93,6 @@ jobs: CHATWOOT_API: ${{ secrets.CHATWOOT_API }} SIMULATOR_NAME: "iPhone 17" OS_VERSION: "26.2" - GEO: false run: | echo "=== Building iOS app (local) ===" echo "Using simulator: $SIMULATOR_NAME (iOS $OS_VERSION)" diff --git a/.github/workflows/e2e_migration.yml b/.github/workflows/e2e_migration.yml index b115803e5..9153cbcc5 100644 --- a/.github/workflows/e2e_migration.yml +++ b/.github/workflows/e2e_migration.yml @@ -77,7 +77,6 @@ jobs: GITHUB_ACTOR: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CHATWOOT_API: ${{ secrets.CHATWOOT_API }} - GEO: false E2E_BACKEND: network E2E_NETWORK: regtest run: | diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 927863b3a..08f010e1a 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -104,8 +104,6 @@ jobs: -test-iterations 2 \ -only-testing:BitkitTests/UtxoSelectionTests \ -only-testing:BitkitTests/BlocktankTests \ - -only-testing:BitkitTests/PaymentFlowTests \ - -only-testing:BitkitTests/BlocktankRefundAddressLiveIntegrationTests \ -only-testing:BitkitTests/AddressTypeIntegrationTests \ -only-testing:BitkitTests/RNMigrationCleanupTests \ | xcbeautify --report junit diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 0a83d0d07..055d68e24 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -58,8 +58,6 @@ jobs: -parallel-testing-enabled NO \ -skip-testing:BitkitTests/UtxoSelectionTests \ -skip-testing:BitkitTests/BlocktankTests \ - -skip-testing:BitkitTests/PaymentFlowTests \ - -skip-testing:BitkitTests/BlocktankRefundAddressLiveIntegrationTests \ -skip-testing:BitkitTests/AddressTypeIntegrationTests \ -skip-testing:BitkitTests/RNMigrationCleanupTests \ | xcbeautify --report junit diff --git a/AGENTS.md b/AGENTS.md index b95606969..207166143 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -345,8 +345,8 @@ xcodebuildmcp simulator test ``` Separately from the test suites, `journeys/` holds XML walkthroughs of app behaviour that an agent -evaluates by driving a running simulator — number pad caps, notification permission, widget flows, -hardware wallet pairing and transfers. Nothing in `.github/workflows` runs them; reviewers drive the +evaluates by driving a running simulator — number pad caps, widget flows, hardware wallet pairing +and the Transfer Unavailable sheet. Nothing in `.github/workflows` runs them; reviewers drive the ones a PR lists. Read `journeys/README.md` before running or writing one, and see the Journeys section under Code Style & Conventions. @@ -443,7 +443,7 @@ While the project is transitioning away from traditional ViewModels, these still - `ActivityListViewModel`: Transaction/payment history - `TransferViewModel`: Transfer flows (send/receive) - `NavigationViewModel`, `SheetViewModel`: UI navigation state -- `BlocktankViewModel`: Lightning channel ordering via Blocktank +- `BlocktankViewModel`: Reads existing Blocktank channel orders and CJIT entries **Key Directories:** - `Components/`: Reusable UI components (buttons, sliders, widgets) @@ -561,7 +561,7 @@ Ensure accessibility modifiers and labels are added to custom components. two platforms stay diffable. Change only what the platform forces: `adb` becomes `xcodebuildmcp`, and Android `testTag`s become iOS `accessibilityIdentifier`s. - MATCH the Android identifier string when adding an `accessibilityIdentifier` for a journey — the - vocabulary is deliberately shared (`N9`, `NRemove`, `SpendingAmountContinue`, `HardwareTransferSign`). + vocabulary is deliberately shared (`N9`, `NRemove`, `TransferUnavailableContinue`, `HardwareTransferToSpending`). Record any name that cannot match in the table in `journeys/README.md`. - PAIR a container identifier with `.accessibilityElement(children: .contain)` so it is queryable. - ADAPT rather than transcribe when iOS genuinely behaves differently, and say so in the journey's diff --git a/Bitkit.xcodeproj/project.pbxproj b/Bitkit.xcodeproj/project.pbxproj index 6707e1a79..fa79ea01b 100644 --- a/Bitkit.xcodeproj/project.pbxproj +++ b/Bitkit.xcodeproj/project.pbxproj @@ -163,7 +163,6 @@ Extensions/PaymentDetails.swift, "Extensions/PaymentFailureReason+UserMessage.swift", Models/BlocksWidgetOptions.swift, - Models/BlocktankNotificationType.swift, Models/LnPeer.swift, Models/PubkyPublicKeyFormat.swift, Models/Toast.swift, @@ -171,7 +170,6 @@ Models/TransferType.swift, Services/BackupFieldMigration.swift, Services/CoreService.swift, - Services/GeoService.swift, Services/HwActivityTagBackup.swift, Services/HwSnapshotMerge.swift, Services/LightningService.swift, @@ -200,12 +198,10 @@ "Extensions/LDKNode+AddressType.swift", Extensions/PaymentDetails.swift, "Extensions/PaymentFailureReason+UserMessage.swift", - Models/BlocktankNotificationType.swift, Models/LnPeer.swift, Models/PubkyPublicKeyFormat.swift, Models/Toast.swift, Services/CoreService.swift, - Services/GeoService.swift, Services/HwActivityTagBackup.swift, Services/HwSnapshotMerge.swift, Services/LightningService.swift, @@ -886,7 +882,7 @@ MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -warnings-as-errors -Wwarning DeprecatedDeclaration"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG CHECK_GEOBLOCK $(inherited)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; @@ -941,7 +937,7 @@ MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -warnings-as-errors -Wwarning DeprecatedDeclaration"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "CHECK_GEOBLOCK $(inherited)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; SWIFT_COMPILATION_MODE = wholemodule; }; name = Release; diff --git a/Bitkit/AppScene.swift b/Bitkit/AppScene.swift index a0d486aff..b31300079 100644 --- a/Bitkit/AppScene.swift +++ b/Bitkit/AppScene.swift @@ -402,8 +402,7 @@ struct AppScene: View { let sheetViewModel = SheetViewModel() let navigationViewModel = NavigationViewModel() let transferService = TransferService( - lightningService: LightningService.shared, - blocktankService: CoreService.shared.blocktank + lightningService: LightningService.shared ) // Run app data migrations before any feature code loads migrated state @@ -442,15 +441,6 @@ struct AppScene: View { _transfer = StateObject(wrappedValue: TransferViewModel( transferService: transferService, sheetViewModel: sheetViewModel, - hwFunding: hwWalletManager, - hwConnecting: hwWalletManager, - hwFeeRateProvider: { - guard let rates = await feeEstimatesManager.getEstimates() else { return nil } - return UInt64(TransactionSpeed.fast.getFeeRate(from: rates)) - }, - hwAddressProvider: { - try await LightningService.shared.addressInfoForType(.nativeSegwit, atIndex: 0).address - }, onBalanceRefresh: { await walletVm.updateBalanceState() } )) _widgets = StateObject(wrappedValue: WidgetsViewModel()) @@ -1029,9 +1019,6 @@ struct AppScene: View { await retryPendingPaykitEndpointRemoval() } - // Start watching pending orders after wallet is ready - await blocktank.startWatchingPendingOrders(transferViewModel: transfer) - // Open the swap updates stream so any pending LN -> onchain swaps resume // and auto-claim once their lockup confirms. Retries until the stream starts. wallet.ensureSwapUpdatesRunning() @@ -1111,7 +1098,8 @@ struct AppScene: View { appViewModel: app, settingsViewModel: settings, walletViewModel: wallet, - currencyViewModel: currency + currencyViewModel: currency, + paykitPaymentRequestManager: paykitPaymentRequestManager ) } catch { app.toast(error) diff --git a/Bitkit/Assets.xcassets/Illustrations/coin-stack-x.imageset/Contents.json b/Bitkit/Assets.xcassets/Illustrations/coin-stack-x.imageset/Contents.json deleted file mode 100644 index 600f19a4b..000000000 --- a/Bitkit/Assets.xcassets/Illustrations/coin-stack-x.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "filename" : "coin-stack-x.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Bitkit/Assets.xcassets/Illustrations/coin-stack-x.imageset/coin-stack-x.png b/Bitkit/Assets.xcassets/Illustrations/coin-stack-x.imageset/coin-stack-x.png deleted file mode 100644 index 62388ddf9..000000000 Binary files a/Bitkit/Assets.xcassets/Illustrations/coin-stack-x.imageset/coin-stack-x.png and /dev/null differ diff --git a/Bitkit/Assets.xcassets/icons/device-tablet-speaker.imageset/Contents.json b/Bitkit/Assets.xcassets/icons/device-tablet-speaker.imageset/Contents.json new file mode 100644 index 000000000..9681774f8 --- /dev/null +++ b/Bitkit/Assets.xcassets/icons/device-tablet-speaker.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "device-tablet-speaker.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/Bitkit/Assets.xcassets/icons/device-tablet-speaker.imageset/device-tablet-speaker.svg b/Bitkit/Assets.xcassets/icons/device-tablet-speaker.imageset/device-tablet-speaker.svg new file mode 100644 index 000000000..a971dd446 --- /dev/null +++ b/Bitkit/Assets.xcassets/icons/device-tablet-speaker.imageset/device-tablet-speaker.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Bitkit/Components/NotificationPreview.swift b/Bitkit/Components/NotificationPreview.swift index b72ecdaf3..4e1ba06c3 100644 --- a/Bitkit/Components/NotificationPreview.swift +++ b/Bitkit/Components/NotificationPreview.swift @@ -2,15 +2,6 @@ import SwiftUI struct NotificationPreview: View { var disabled: Bool - var enableAmount: Bool - - var amountText: String { - if enableAmount { - return "₿ 21 000 ($21.00)" - } else { - return t("settings__notifications__settings__preview__text") - } - } var body: some View { HStack(alignment: .top, spacing: 8) { @@ -21,7 +12,7 @@ struct NotificationPreview: View { VStack(alignment: .leading, spacing: 2) { HStack { - Text(t("settings__notifications__settings__preview__title")) + Text(t("subscriptions__payment_due_title")) .font(.system(size: 15, weight: .semibold)) .foregroundColor(Color(hex: 0x222222)) Spacer() @@ -30,7 +21,7 @@ struct NotificationPreview: View { .foregroundColor(Color(hex: 0x3F3F3F).opacity(0.5)) } - Text(amountText) + Text(t("subscriptions__payment_due_description")) .font(.system(size: 15)) .foregroundColor(Color(hex: 0x3F3F3F)) } diff --git a/Bitkit/Components/ProgressSteps.swift b/Bitkit/Components/ProgressSteps.swift deleted file mode 100644 index 2772d2e67..000000000 --- a/Bitkit/Components/ProgressSteps.swift +++ /dev/null @@ -1,65 +0,0 @@ -import SwiftUI - -/// A view that displays a list of steps with circles and numbers. -struct ProgressSteps: View { - let steps: [String] - let currentStep: Int - - private let size: CGFloat = 32 - - var body: some View { - VStack(spacing: 0) { - // Steps with circles and separators - GeometryReader { geometry in - ZStack(alignment: .center) { - // Dashed line background - Path { path in - let y = geometry.size.height / 2 - let padding = 36.0 * 2.5 // Account for circle radius (16) + horizontal padding (20) - path.move(to: CGPoint(x: padding, y: y)) - path.addLine(to: CGPoint(x: geometry.size.width - padding, y: y)) - } - .stroke(style: StrokeStyle(lineWidth: 1, dash: [4, 4])) - .foregroundColor(Color.white32) - - // Circles with numbers - HStack(spacing: 0) { - ForEach(Array(steps.enumerated()), id: \.0) { index, _ in - // Circle with number or checkmark - ZStack { - Circle() - .fill(index < currentStep ? Color.purpleAccent : Color.black) - .frame(width: size, height: size) - - if index < currentStep { - // Checkmark for completed steps - Image("check-mark") - .foregroundColor(.black) - } else { - // Number for current and upcoming steps - Text("\(index + 1)") - .foregroundColor(index == currentStep ? Color.purpleAccent : .white32) - .font(.custom(Fonts.regular, size: 17)) - } - - // Border for uncompleted steps - if index >= currentStep { - Circle() - .stroke(index == currentStep ? Color.purpleAccent : Color.white32, lineWidth: 1) - .frame(width: size, height: size) - } - } - .padding(.horizontal, 16) - } - } - } - } - .frame(height: size) - - VStack { - BodySSBText(steps[currentStep], textColor: .white32) - } - .frame(height: 56) - } - } -} diff --git a/Bitkit/Components/SheetIntro.swift b/Bitkit/Components/SheetIntro.swift index f4b8b4eac..be8aa8ad1 100644 --- a/Bitkit/Components/SheetIntro.swift +++ b/Bitkit/Components/SheetIntro.swift @@ -12,6 +12,7 @@ struct SheetIntro: View { let testID: String? let cancelTestID: String? let continueTestID: String? + let showBackButton: Bool let onCancel: (() -> Void)? let onContinue: () -> Void private var baseTestID: String { @@ -30,6 +31,7 @@ struct SheetIntro: View { testID: String? = nil, cancelTestID: String? = nil, continueTestID: String? = nil, + showBackButton: Bool = false, onCancel: (() -> Void)? = nil, onContinue: @escaping () -> Void ) { @@ -44,13 +46,14 @@ struct SheetIntro: View { self.testID = testID self.cancelTestID = cancelTestID self.continueTestID = continueTestID + self.showBackButton = showBackButton self.onCancel = onCancel self.onContinue = onContinue } var body: some View { VStack(alignment: .leading, spacing: 0) { - SheetHeader(title: navTitle) + SheetHeader(title: navTitle, showBackButton: showBackButton) VStack(spacing: 0) { Spacer() @@ -66,6 +69,7 @@ struct SheetIntro: View { DisplayText(title, accentColor: accentColor) .padding(.bottom, 14) .frame(maxWidth: .infinity, alignment: .leading) + .accessibilityIdentifier("\(baseTestID)Title") BodyMText(description, accentFont: accentFont) .frame(maxWidth: .infinity, alignment: .leading) diff --git a/Bitkit/Components/TabBar/TabBar.swift b/Bitkit/Components/TabBar/TabBar.swift index e5779010e..b6d10a790 100644 --- a/Bitkit/Components/TabBar/TabBar.swift +++ b/Bitkit/Components/TabBar/TabBar.swift @@ -65,7 +65,7 @@ struct TabBar: View { sheets.showSheet( .receive, data: ReceiveConfig( - view: .qr(cjitInvoice: nil, tab: .hardware), + view: .qr(tab: .hardware), hardwareWalletId: walletId ) ) @@ -74,13 +74,13 @@ struct TabBar: View { let hasPendingTransfersToSpending = wallet.balanceInTransferToSpending > 0 - if Self.shouldOpenSpendingCjitEntry( + if Self.shouldOpenSpendingTransferUnavailable( isSpendingWallet: navigation.currentRoute == .spendingWallet, wallet: wallet, hasPendingTransfersToSpending: hasPendingTransfersToSpending ) { - // On spending wallet screen, show CJIT flow when user can't receive normally - sheets.showSheet(.receive, data: ReceiveConfig(view: .cjitAmount)) + // Spending can't receive without inbound capacity, and Blocktank no longer sells it. + sheets.showSheet(.receive, data: ReceiveConfig(view: .transferUnavailable)) } else { sheets.showSheet(.receive) } @@ -90,7 +90,7 @@ struct TabBar: View { sheets.showSheet(.scanner) } - static func shouldOpenSpendingCjitEntry( + static func shouldOpenSpendingTransferUnavailable( isSpendingWallet: Bool, wallet: WalletViewModel, hasPendingTransfersToSpending: Bool diff --git a/Bitkit/Components/Widgets/Suggestions.swift b/Bitkit/Components/Widgets/Suggestions.swift index 2ce057c0e..3936ba524 100644 --- a/Bitkit/Components/Widgets/Suggestions.swift +++ b/Bitkit/Components/Widgets/Suggestions.swift @@ -374,7 +374,7 @@ struct Suggestions: View { case .hardware: sheets.showSheet(.hardwareConnect) case .transferToSpending: - route = app.hasSeenTransferIntro ? .fundingOptions : .transferIntro + sheets.showSheet(.transferUnavailable) } if let route { diff --git a/Bitkit/Constants/Env.swift b/Bitkit/Constants/Env.swift index 768856962..68abd332c 100644 --- a/Bitkit/Constants/Env.swift +++ b/Bitkit/Constants/Env.swift @@ -138,14 +138,6 @@ enum Env { } } - static var isGeoblockingEnabled: Bool { - #if CHECK_GEOBLOCK - return true - #else - return ProcessInfo.processInfo.environment["GEO"] == "true" - #endif - } - /// The current execution context of the app static var currentExecutionContext: ExecutionContext { let isNotificationExtension = Bundle.main.bundleIdentifier?.lowercased().contains("notification") == true @@ -304,26 +296,6 @@ enum Env { } } - // TODO: remove this to load from BT API instead - static var trustedLnPeers: [LnPeer] { - switch network { - case .bitcoin: - return [ - .init(nodeId: "039b8b4dd1d88c2c5db374290cda397a8f5d79f312d6ea5d5bfdfc7c6ff363eae3", host: "34.65.111.104", port: 9735), - .init(nodeId: "03816141f1dce7782ec32b66a300783b1d436b19777e7c686ed00115bd4b88ff4b", host: "34.65.191.64", port: 9735), - .init(nodeId: "02a371038863605300d0b3fc9de0cf5ccb57728b7f8906535709a831b16e311187", host: "34.65.153.174", port: 9735), - ] - case .signet: - return [] - case .testnet: - return [] - case .regtest: - return [ - .init(nodeId: "028a8910b0048630d4eb17af25668cdd7ea6f2d8ae20956e7a06e2ae46ebcb69fc", host: "34.65.86.104", port: 9400), - ] - } - } - static var blocktankBaseUrl: String { switch network { case .bitcoin: "https://api1.blocktank.to" @@ -331,13 +303,6 @@ enum Env { } } - static var blocktankPushNotificationServer: String { - switch network { - case .bitcoin: "\(blocktankBaseUrl)/api/notifications" - default: "\(blocktankBaseUrl)/notifications/api" - } - } - static var blocktankClientServer: String { switch network { case .bitcoin: "\(blocktankBaseUrl)/api" @@ -359,14 +324,6 @@ enum Env { static let blocktankOrderRefreshInterval: TimeInterval = 2 * 60 // 2 minutes - static var pushNotificationFeatures: [BlocktankNotificationType] = [ - .incomingHtlc, - .mutualClose, - .orderPaymentConfirmed, - .cjitPaymentArrived, - .wakeToTimeout, - ] - static var vssStoreIdPrefix: String { "bitkit_v1_\(networkName)" } @@ -433,7 +390,7 @@ enum Env { static let termsOfServiceUrl = "https://www.bitkit.to/terms-of-use" static let privacyPolicyUrl = "https://www.bitkit.to/privacy-policy" - static let geoCheckUrl = "https://api1.blocktank.to/api/geocheck" + static let blocktankSunsetUrl = "https://blog.bitkit.to/" static let bitrefillRef = "AL6dyZYt" static let btcMapUrl = "https://btcmap.org/map" static let helpUrl = "https://help.bitkit.to" diff --git a/Bitkit/Extensions/FixedWidthInteger+Saturating.swift b/Bitkit/Extensions/FixedWidthInteger+Saturating.swift index e56e9968e..3a954b13a 100644 --- a/Bitkit/Extensions/FixedWidthInteger+Saturating.swift +++ b/Bitkit/Extensions/FixedWidthInteger+Saturating.swift @@ -5,9 +5,4 @@ extension FixedWidthInteger { let (sum, overflow) = addingReportingOverflow(other) return overflow ? Self.max : sum } - - func saturatingSub(_ other: Self) -> Self { - let (difference, overflow) = subtractingReportingOverflow(other) - return overflow ? Self.min : difference - } } diff --git a/Bitkit/Extensions/HwWalletVendor+UI.swift b/Bitkit/Extensions/HwWalletVendor+UI.swift index 64dc79c09..b377a8c71 100644 --- a/Bitkit/Extensions/HwWalletVendor+UI.swift +++ b/Bitkit/Extensions/HwWalletVendor+UI.swift @@ -46,13 +46,6 @@ extension HwWalletVendor { } } - var transferSignButtonTitle: String { - switch self { - case .trezor: t("lightning__transfer_hw__open_connect") - case .blockstream: t("hardware__send_open_connect_jade") - } - } - /// Passphrase (hidden) wallets are a Trezor feature; a Jade holds one wallet per device. var supportsPassphraseWallets: Bool { switch self { diff --git a/Bitkit/Extensions/IcJitEntry+Mock.swift b/Bitkit/Extensions/IcJitEntry+Mock.swift deleted file mode 100644 index fb12f9336..000000000 --- a/Bitkit/Extensions/IcJitEntry+Mock.swift +++ /dev/null @@ -1,43 +0,0 @@ -import BitkitCore -import Foundation - -extension IcJitEntry { - static func mock( - state: CJitStateEnum = .created, - channelSizeSat: UInt64 = 100_000, - feeSat: UInt64 = 1000, - channelExpiryWeeks: UInt32 = 6, - channel: IBtChannel? = nil - ) -> IcJitEntry { - return IcJitEntry( - id: "test-cjit-id", - state: state, - feeSat: feeSat, - networkFeeSat: 500, - serviceFeeSat: 500, - channelSizeSat: channelSizeSat, - channelExpiryWeeks: channelExpiryWeeks, - channelOpenError: nil, - nodeId: "node-id-123456", - invoice: IBtBolt11Invoice( - request: "lnbc100000...", - state: .pending, - expiresAt: "2024-10-28T12:00:00Z", - updatedAt: "2024-10-21T12:00:00Z" - ), - channel: channel, - lspNode: ILspNode( - alias: "Test LSP", - pubkey: "lsp-pubkey-123456", - connectionStrings: ["127.0.0.1:9735"], - readonly: nil - ), - couponCode: "", - source: "bitkit-ios", - discount: nil, - expiresAt: "2024-10-28T12:00:00Z", - updatedAt: "2024-10-21T12:00:00Z", - createdAt: "2024-10-21T12:00:00Z" - ) - } -} diff --git a/Bitkit/MainNavView.swift b/Bitkit/MainNavView.swift index 7b1da905a..64da4682a 100644 --- a/Bitkit/MainNavView.swift +++ b/Bitkit/MainNavView.swift @@ -80,12 +80,6 @@ struct MainNavView: View { NavigationStack(path: $navigation.path) { navigationContent } - .onChange(of: transfer.hwFundingComplete) { _, complete in - if complete { - transfer.consumeHwFundingComplete() - navigation.navigate(.spendingHwSigned) - } - } .onChange(of: pendingProfileSetupResumeState, initial: true) { _, resumeState in guard resumeState.shouldResume(didResume: &didResumePendingPubkyProfileSetup) else { return } navigation.navigate(.createProfile) @@ -116,20 +110,20 @@ struct MainNavView: View { config in BackupSheet(config: config) } .sheet( - item: $sheets.giftSheetItem, + item: $sheets.connectionClosedSheetItem, onDismiss: { - sheets.hideSheetIfActive(.gift, reason: "Gift sheet dismissed") + sheets.hideSheetIfActive(.connectionClosed, reason: "Connection closed sheet dismissed") } ) { - config in GiftSheet(config: config) + config in ConnectionClosedSheet(config: config) } .sheet( - item: $sheets.connectionClosedSheetItem, + item: $sheets.transferUnavailableSheetItem, onDismiss: { - sheets.hideSheetIfActive(.connectionClosed, reason: "Connection closed sheet dismissed") + sheets.hideSheetIfActive(.transferUnavailable, reason: "Transfer unavailable sheet dismissed") } ) { - config in ConnectionClosedSheet(config: config) + config in TransferUnavailableSheet(config: config) } .sheet( item: $sheets.highBalanceSheetItem, @@ -389,23 +383,6 @@ struct MainNavView: View { notificationManager.unregister() } } - .onChange(of: notificationManager.deviceToken) { _, token in - // Register with backend if device token changed and notifications are enabled - if let token, settings.enableNotifications { - Task { - do { - try await notificationManager.registerWithBackend(deviceToken: token) - } catch { - Logger.error("Failed to sync push notifications with backend: \(error)") - app.toast( - type: .error, - title: t("other__notification_registration_failed_title"), - description: t("other__notification_registration_failed_description") - ) - } - } - } - } .onChange(of: settings.enableNotifications) { _, newValue in Task { await paykitPaymentRequestManager.synchronizeSubscriptionNotifications(enabled: newValue) @@ -414,21 +391,6 @@ struct MainNavView: View { if newValue { // Request permission in case user was not prompted yet notificationManager.requestPermission() - - if let token = notificationManager.deviceToken { - Task { - do { - try await notificationManager.registerWithBackend(deviceToken: token) - } catch { - Logger.error("Failed to sync push notifications: \(error)") - app.toast( - type: .error, - title: t("other__notification_registration_failed_title"), - description: t("other__notification_registration_failed_description") - ) - } - } - } } else { // Disable notifications (unregister) notificationManager.unregister() @@ -547,19 +509,7 @@ struct MainNavView: View { case .scanner: ScannerScreen() // Transfer - case .transferIntro: TransferIntroView() case .fundingOptions: FundingOptions() - case .spendingIntro: SpendingIntroView() - case let .spendingIntroHw(walletId): SpendingIntroView(walletId: walletId) - case .spendingAmount: SpendingAmount() - case let .spendingAmountHw(walletId): SpendingAmountHw(walletId: walletId) - case let .spendingHwSign(walletId): SpendingHwSign(walletId: walletId) - case .spendingHwSigned: SpendingHwSigned() - case .spendingConfirm: SpendingConfirm() - case let .spendingAdvanced(walletId): SpendingAdvancedView(walletId: walletId) - case .transferLearnMore: TransferLearnMoreView() - case .settingUp: SettingUpView() - case .fundingAdvanced: FundAdvancedOptions() case let .fundManual(nodeUri): FundManualSetupView(initialNodeUri: nodeUri) case let .fundManualAmount(lnPeer): FundManualAmountView(lnPeer: lnPeer) case let .fundManualConfirm(lnPeer, amountSats): FundManualConfirmView(lnPeer: lnPeer, amountSats: amountSats) diff --git a/Bitkit/Managers/BalanceManager.swift b/Bitkit/Managers/BalanceManager.swift index f2d6e7c65..e54b635cf 100644 --- a/Bitkit/Managers/BalanceManager.swift +++ b/Bitkit/Managers/BalanceManager.swift @@ -35,11 +35,6 @@ class BalanceManager { let channels = await MainActor.run { lightningService.channels } ?? [] let activeTransfers = try transferService.getActiveTransfers() - let paidOrdersSats = await getOrderPaymentsSats( - activeTransfers: activeTransfers, - channels: channels, - balances: balanceDetails - ) let pendingChannelsSats = await getPendingChannelsSats( transfers: activeTransfers, channels: channels, @@ -61,15 +56,9 @@ class BalanceManager { ) let toSavingsAmount = pendingCloseAmount - let toSpendingAmount = paidOrdersSats + pendingChannelsSats - - let ordersOnchainToSubtract = getOrderPaymentOnchainToSubtract( - activeTransfers: activeTransfers, - currentOnchainSats: balanceDetails.totalOnchainBalanceSats - ) + let toSpendingAmount = pendingChannelsSats let totalOnchainSats = balanceDetails.totalOnchainBalanceSats - .minusOrZero(ordersOnchainToSubtract) let totalLightningSats = balanceDetails.totalLightningBalanceSats .minusOrZero(pendingChannelsSats) .minusOrZero(lightningToSubtract) @@ -92,7 +81,7 @@ class BalanceManager { "Balances in ldk-node: onchain=\(balanceDetails.totalOnchainBalanceSats) lightning=\(balanceDetails.totalLightningBalanceSats)" ) Logger.debug( - "Balances in state: onchain=\(totalOnchainSats) lightning=\(totalLightningSats) toSavings=\(toSavingsAmount) toSpending=\(toSpendingAmount) lnSubtract=\(lightningToSubtract) ordersOnchainSubtract=\(ordersOnchainToSubtract)" + "Balances in state: onchain=\(totalOnchainSats) lightning=\(totalLightningSats) toSavings=\(toSavingsAmount) toSpending=\(toSpendingAmount) lnSubtract=\(lightningToSubtract)" ) return balanceState @@ -100,69 +89,20 @@ class BalanceManager { // MARK: - Private Helper Methods - /// Calculates the total amount paid for LSP orders whose funds are not yet reflected in a - /// Lightning channel balance. The channel is resolved dynamically (via the order's funding tx), - /// so an order is dropped here as soon as LDK exposes its pending channel — where - /// getPendingChannelsSats then tracks it — preventing a double-count during the - /// paid-order → pending-channel transition (notably for hardware-wallet transfers, where LDK can - /// expose the pending channel before the transfer's channelId is persisted). - private func getOrderPaymentsSats( - activeTransfers: [Transfer], - channels: [ChannelDetails], - balances: BalanceDetails - ) async -> UInt64 { - var amount: UInt64 = 0 - let paidOrders = activeTransfers.filter { $0.type.isToSpending() && $0.lspOrderId != nil } - - for transfer in paidOrders { - let channelId = try? await transferService.resolveChannelId(for: transfer, channels: channels) - let channelBalance = channelId.flatMap { id in - balances.lightningBalances.first { $0.channelIdString == id } - } - if channelBalance == nil { - Logger.debug( - "Order payment transfer: id=\(transfer.id) orderId=\(transfer.lspOrderId ?? "nil") amount=\(transfer.amountSats)" - ) - amount += transfer.amountSats - } - } - - return amount - } - - /// Amount to subtract from displayed on-chain for order payments whose tx is not yet reflected in LDK. - private func getOrderPaymentOnchainToSubtract(activeTransfers: [Transfer], currentOnchainSats: UInt64) -> UInt64 { - let orderPayments = activeTransfers.filter { - $0.type.isToSpending() && $0.fundingTxId != nil && $0.lspOrderId != nil - } - var toSubtract: UInt64 = 0 - for t in orderPayments { - guard let txTotal = t.txTotalSats, let pre = t.preTransferOnchainSats else { continue } - if currentOnchainSats >= pre { - toSubtract += txTotal - } - } - return toSubtract - } - - /// Calculates the total balance in pending (not yet usable) channels. - /// - /// The channel is resolved dynamically (via the order's funding tx) rather than only from the - /// transfer's stored channelId, so a pending channel is subtracted from the Lightning total as - /// soon as LDK exposes it — even before syncTransferStates persists the channelId. + /// Calculates the total balance in pending (not yet usable) channels opened through Manual Setup. private func getPendingChannelsSats( transfers: [Transfer], channels: [ChannelDetails], balances: BalanceDetails ) async -> UInt64 { var amount: UInt64 = 0 - let pendingTransfers = transfers.filter { $0.type.isToSpending() } + let pendingTransfers = transfers.filter { $0.type.isToSpending() && $0.lspOrderId == nil } Logger.debug("Checking \(pendingTransfers.count) pending transfers to spending") Logger.debug("Available channels: \(channels.count), Lightning balances: \(balances.lightningBalances.count)") for transfer in pendingTransfers { - guard let channelId = try? await transferService.resolveChannelId(for: transfer, channels: channels) else { + guard let channelId = transfer.channelId else { Logger.debug("Transfer \(transfer.id) has no resolvable channelId") continue } diff --git a/Bitkit/Managers/PushNotificationManager.swift b/Bitkit/Managers/PushNotificationManager.swift index 6d9421b14..301afb44a 100644 --- a/Bitkit/Managers/PushNotificationManager.swift +++ b/Bitkit/Managers/PushNotificationManager.swift @@ -1,10 +1,6 @@ import Foundation import SwiftUI -enum PushNotificationError: Error { - case deviceTokenNotAvailable -} - final class PushNotificationManager: ObservableObject { static let shared = PushNotificationManager() @Published var deviceToken: String? = nil @@ -27,6 +23,19 @@ final class PushNotificationManager: ObservableObject { } } + /// Asks for notification permission once, where a feature needs it; an earlier answer is kept. + func requestPermissionIfUndecided() { + UNUserNotificationCenter.current().getNotificationSettings { settings in + guard settings.authorizationStatus == .notDetermined else { return } + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { _, error in + if let error { + Logger.error("Notification permission request failed: \(error)") + } + self.updateNotificationPermission() + } + } + } + func updateNotificationPermission() { UNUserNotificationCenter.current().getNotificationSettings { settings in DispatchQueue.main.async { @@ -43,107 +52,12 @@ final class PushNotificationManager: ObservableObject { Logger.debug("✅ Device token: \(token)") } - func registerWithBackend(deviceToken: String) async throws { - guard !Env.isE2E else { - Logger.info("Skipped push registration in E2E build") - return - } - - try await waitForNodeToBeReady() - - guard let nodeId = LightningService.shared.nodeId else { - throw AppError(serviceError: .nodeNotStarted) - } - - Logger.info("Registering device for notifications") - - let isoTimestamp = ISO8601DateFormatter().string(from: Date()) - let messageToSign = "bitkit-notifications\(deviceToken)\(isoTimestamp)" - let signature = try await LightningService.shared.sign(message: messageToSign) - let keypair = try Crypto.generateKeyPair() - - Logger.debug("Notification encryption public key: \(keypair.publicKey.hex)") - - // New keypair for each token registration - if try Keychain.exists(key: .pushNotificationPrivateKey) { - try? Keychain.delete(key: .pushNotificationPrivateKey) - } - - try Keychain.save(key: .pushNotificationPrivateKey, data: keypair.privateKey) - - let result = try await CoreService.shared.blocktank.registerDeviceForNotifications( - deviceToken: deviceToken, - publicKey: keypair.publicKey.hex, - features: Env.pushNotificationFeatures.map(\.feature), - nodeId: nodeId, - isoTimestamp: isoTimestamp, - signature: signature - ) - - Logger.debug("Registration result: \(result)") - Logger.debug("🔔 PushNotificationManager: Successfully registered device token with server") - } - func unregister() { // Unregister from remote notifications UIApplication.shared.unregisterForRemoteNotifications() - // Unregister from server - // TODO: This would require a server endpoint to unregister the device - Logger.debug("Would unregister device from notification server") - // try await CoreService.shared.blocktank.unregisterDeviceForNotifications() } func handleNotification(_ userInfo: [AnyHashable: Any]) { Logger.debug("📩 Notification received: \(userInfo)") } - - func sendTestNotification() async throws { - Logger.debug("Sending test notification to self") - - guard let deviceToken else { - throw PushNotificationError.deviceTokenNotAvailable - } - - do { - let response = try await CoreService.shared.blocktank.pushNotificationTest( - deviceToken: deviceToken, - secretMessage: "hello", - notificationType: BlocktankNotificationType.orderPaymentConfirmed.rawValue - ) - - Logger.debug("Test notification sent successfully: \(response)") - } catch { - Logger.error("Failed to send test notification: \(error)") - } - } - - // MARK: - Private Helpers - - /// Waits for the Lightning node to be ready before proceeding - /// - Parameter timeout: Maximum time to wait in seconds (default: 30) - /// - Parameter pollInterval: How often to check the status in seconds (default: 0.5) - private func waitForNodeToBeReady(timeout: TimeInterval = 30, pollInterval: TimeInterval = 0.5) async throws { - let startTime = Date() - let timeoutDate = startTime.addingTimeInterval(timeout) - - Logger.debug("⏳ Waiting for Lightning node to be ready...") - - while Date() < timeoutDate { - // Check if node is running via the status - let status = await MainActor.run { LightningService.shared.status } - if let status, status.isRunning { - let waitTime = Date().timeIntervalSince(startTime) - Logger.debug("✅ Node is ready (waited \(String(format: "%.2f", waitTime))s)") - return - } - - // Wait before checking again - try await Task.sleep(nanoseconds: UInt64(pollInterval * 1_000_000_000)) - } - - // Timeout reached - let waitTime = Date().timeIntervalSince(startTime) - Logger.error("❌ Node did not become ready within \(timeout)s (waited \(String(format: "%.2f", waitTime))s)") - throw AppError(message: "Lightning node did not start in time", debugMessage: "Timed out after \(timeout) seconds") - } } diff --git a/Bitkit/Managers/TimedSheets/NotificationsTimedSheet.swift b/Bitkit/Managers/TimedSheets/NotificationsTimedSheet.swift index 8a463921e..369c74980 100644 --- a/Bitkit/Managers/TimedSheets/NotificationsTimedSheet.swift +++ b/Bitkit/Managers/TimedSheets/NotificationsTimedSheet.swift @@ -9,28 +9,30 @@ struct NotificationsTimedSheet: TimedSheetItem { private let appViewModel: AppViewModel private let settingsViewModel: SettingsViewModel - private let walletViewModel: WalletViewModel + private let hasActiveSubscription: @MainActor () -> Bool init( appViewModel: AppViewModel, settingsViewModel: SettingsViewModel, - walletViewModel: WalletViewModel + hasActiveSubscription: @escaping @MainActor () -> Bool ) { self.appViewModel = appViewModel self.settingsViewModel = settingsViewModel - self.walletViewModel = walletViewModel + self.hasActiveSubscription = hasActiveSubscription } @MainActor func shouldShow() async -> Bool { - // Check if user has enabled notifications - let notificationsEnabled = settingsViewModel.enableNotifications - // Check if user hasn't seen this prompt - let hasSeenNotificationsIntro = appViewModel.hasSeenNotificationsIntro - // Check if user has spending balance - let hasSpendingBalance = walletViewModel.totalLightningSats > 0 + Self.shouldShow( + notificationsEnabled: settingsViewModel.enableNotifications, + hasSeenIntro: appViewModel.hasSeenNotificationsIntro, + hasActiveSubscription: hasActiveSubscription() + ) + } - return !notificationsEnabled && !hasSeenNotificationsIntro && hasSpendingBalance + /// Paykit subscription reminders are the notifications Bitkit posts, so the prompt waits for a subscription. + static func shouldShow(notificationsEnabled: Bool, hasSeenIntro: Bool, hasActiveSubscription: Bool) -> Bool { + !notificationsEnabled && !hasSeenIntro && hasActiveSubscription } func onShown() { diff --git a/Bitkit/Managers/TimedSheets/TimedSheetManager.swift b/Bitkit/Managers/TimedSheets/TimedSheetManager.swift index 30595ec31..f3ba85bdf 100644 --- a/Bitkit/Managers/TimedSheets/TimedSheetManager.swift +++ b/Bitkit/Managers/TimedSheets/TimedSheetManager.swift @@ -45,7 +45,8 @@ class TimedSheetManager: ObservableObject { appViewModel: AppViewModel, settingsViewModel: SettingsViewModel, walletViewModel: WalletViewModel, - currencyViewModel: CurrencyViewModel + currencyViewModel: CurrencyViewModel, + paykitPaymentRequestManager: PaykitPaymentRequestManager ) { // Configure the manager self.sheetViewModel = sheetViewModel @@ -67,7 +68,9 @@ class TimedSheetManager: ObservableObject { let notificationsSheet = NotificationsTimedSheet( appViewModel: appViewModel, settingsViewModel: settingsViewModel, - walletViewModel: walletViewModel + hasActiveSubscription: { + paykitPaymentRequestManager.subscriptions.contains { $0.isActive(at: Date()) } + } ) registerSheet(notificationsSheet) diff --git a/Bitkit/Managers/TransferTrackingManager.swift b/Bitkit/Managers/TransferTrackingManager.swift index d305b41dc..dc60294d7 100644 --- a/Bitkit/Managers/TransferTrackingManager.swift +++ b/Bitkit/Managers/TransferTrackingManager.swift @@ -36,8 +36,7 @@ class TransferTrackingManager: ObservableObject { type: TransferType, amountSats: UInt64, channelId: String? = nil, - fundingTxId: String? = nil, - lspOrderId: String? = nil + fundingTxId: String? = nil ) async throws -> String { isLoading = true defer { isLoading = false } @@ -47,8 +46,7 @@ class TransferTrackingManager: ObservableObject { type: type, amountSats: amountSats, channelId: channelId, - fundingTxId: fundingTxId, - lspOrderId: lspOrderId + fundingTxId: fundingTxId ) await loadActiveTransfers() // Refresh list error = nil diff --git a/Bitkit/Models/BackupPayloads.swift b/Bitkit/Models/BackupPayloads.swift index 0b99b5de6..b763bc3ad 100644 --- a/Bitkit/Models/BackupPayloads.swift +++ b/Bitkit/Models/BackupPayloads.swift @@ -62,19 +62,12 @@ extension PubkySessionBackupV1 { } } -struct BlocktankRefundAddress: Codable, Equatable { - let address: String - let index: UInt32 -} - struct AppCacheData: Codable { let hasSeenContactsIntro: Bool let hasSeenProfileIntro: Bool let hasSeenNotificationsIntro: Bool let hasSeenQuickpayIntro: Bool let hasSeenShopIntro: Bool - let hasSeenTransferIntro: Bool - let hasSeenTransferToSpendingIntro: Bool let hasSeenTransferToSavingsIntro: Bool let hasSeenWidgetsIntro: Bool let hasDismissedWidgetsOnboardingHint: Bool @@ -85,7 +78,6 @@ struct AppCacheData: Codable { let dismissedSuggestions: [String] let lastUsedTags: [String] let quickPayLedger: QuickPayLedger? - let blocktankRefundAddress: BlocktankRefundAddress? let detachedActivityContacts: Set init( @@ -94,8 +86,6 @@ struct AppCacheData: Codable { hasSeenNotificationsIntro: Bool, hasSeenQuickpayIntro: Bool, hasSeenShopIntro: Bool, - hasSeenTransferIntro: Bool, - hasSeenTransferToSpendingIntro: Bool, hasSeenTransferToSavingsIntro: Bool, hasSeenWidgetsIntro: Bool, hasDismissedWidgetsOnboardingHint: Bool, @@ -106,7 +96,6 @@ struct AppCacheData: Codable { dismissedSuggestions: [String], lastUsedTags: [String], quickPayLedger: QuickPayLedger? = nil, - blocktankRefundAddress: BlocktankRefundAddress? = nil, detachedActivityContacts: Set = [] ) { self.hasSeenContactsIntro = hasSeenContactsIntro @@ -114,8 +103,6 @@ struct AppCacheData: Codable { self.hasSeenNotificationsIntro = hasSeenNotificationsIntro self.hasSeenQuickpayIntro = hasSeenQuickpayIntro self.hasSeenShopIntro = hasSeenShopIntro - self.hasSeenTransferIntro = hasSeenTransferIntro - self.hasSeenTransferToSpendingIntro = hasSeenTransferToSpendingIntro self.hasSeenTransferToSavingsIntro = hasSeenTransferToSavingsIntro self.hasSeenWidgetsIntro = hasSeenWidgetsIntro self.hasDismissedWidgetsOnboardingHint = hasDismissedWidgetsOnboardingHint @@ -126,7 +113,6 @@ struct AppCacheData: Codable { self.dismissedSuggestions = dismissedSuggestions self.lastUsedTags = lastUsedTags self.quickPayLedger = quickPayLedger - self.blocktankRefundAddress = blocktankRefundAddress self.detachedActivityContacts = detachedActivityContacts } @@ -137,8 +123,6 @@ struct AppCacheData: Codable { hasSeenNotificationsIntro = try c.decodeIfPresent(Bool.self, forKey: .hasSeenNotificationsIntro) ?? false hasSeenQuickpayIntro = try c.decodeIfPresent(Bool.self, forKey: .hasSeenQuickpayIntro) ?? false hasSeenShopIntro = try c.decodeIfPresent(Bool.self, forKey: .hasSeenShopIntro) ?? false - hasSeenTransferIntro = try c.decodeIfPresent(Bool.self, forKey: .hasSeenTransferIntro) ?? false - hasSeenTransferToSpendingIntro = try c.decodeIfPresent(Bool.self, forKey: .hasSeenTransferToSpendingIntro) ?? false hasSeenTransferToSavingsIntro = try c.decodeIfPresent(Bool.self, forKey: .hasSeenTransferToSavingsIntro) ?? false hasSeenWidgetsIntro = try c.decodeIfPresent(Bool.self, forKey: .hasSeenWidgetsIntro) ?? false hasDismissedWidgetsOnboardingHint = try c.decodeIfPresent(Bool.self, forKey: .hasDismissedWidgetsOnboardingHint) ?? false @@ -149,18 +133,16 @@ struct AppCacheData: Codable { dismissedSuggestions = try c.decodeIfPresent([String].self, forKey: .dismissedSuggestions) ?? [] lastUsedTags = try c.decodeIfPresent([String].self, forKey: .lastUsedTags) ?? [] quickPayLedger = try c.decodeIfPresent(QuickPayLedger.self, forKey: .quickPayLedger) - blocktankRefundAddress = try c.decodeIfPresent(BlocktankRefundAddress.self, forKey: .blocktankRefundAddress) detachedActivityContacts = try c.decodeIfPresent(Set.self, forKey: .detachedActivityContacts) ?? [] } private enum CodingKeys: String, CodingKey { case hasSeenContactsIntro, hasSeenProfileIntro, hasSeenNotificationsIntro, hasSeenQuickpayIntro - case hasSeenShopIntro, hasSeenTransferIntro, hasSeenTransferToSpendingIntro, hasSeenTransferToSavingsIntro + case hasSeenShopIntro, hasSeenTransferToSavingsIntro case hasSeenWidgetsIntro, hasDismissedWidgetsOnboardingHint case appUpdateIgnoreTimestamp, backupIgnoreTimestamp, highBalanceIgnoreCount, highBalanceIgnoreTimestamp case dismissedSuggestions, lastUsedTags case quickPayLedger - case blocktankRefundAddress case detachedActivityContacts } } diff --git a/Bitkit/Models/BlocktankNotificationType.swift b/Bitkit/Models/BlocktankNotificationType.swift deleted file mode 100644 index 7a98d14db..000000000 --- a/Bitkit/Models/BlocktankNotificationType.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation - -enum BlocktankNotificationType: String { - case incomingHtlc - case mutualClose - case orderPaymentConfirmed - case cjitPaymentArrived - case wakeToTimeout - - var feature: String { - return "blocktank.\(rawValue)" - } -} diff --git a/Bitkit/Models/ReceiveLiquidityDecision.swift b/Bitkit/Models/ReceiveLiquidityDecision.swift index b999fa998..7e3528e6c 100644 --- a/Bitkit/Models/ReceiveLiquidityDecision.swift +++ b/Bitkit/Models/ReceiveLiquidityDecision.swift @@ -1,8 +1,6 @@ enum ReceiveAdditionalLiquidityAction: Equatable { case none - case chooseAmount - case createCjit(UInt64) - case geoBlocked + case transferUnavailable } enum ReceiveLiquiditySource: Equatable { @@ -31,49 +29,15 @@ enum ReceiveLiquidityDecision { static func additionalLiquidityAction( source: ReceiveLiquiditySource, invoiceAmountSats: UInt64, - inboundCapacitySats: UInt64?, - minCjitSats: UInt64?, - maxCjitAmountSats: UInt64?, - isGeoBlocked: Bool + inboundCapacitySats: UInt64? ) -> ReceiveAdditionalLiquidityAction { - guard source == .spending, needsInboundLiquidity(invoiceAmountSats: invoiceAmountSats, inboundCapacitySats: inboundCapacitySats) else { - return .none - } - - let inboundCapacitySats = inboundCapacitySats ?? 0 - if inboundCapacitySats == 0 { - return .none - } - - if isGeoBlocked { - return .geoBlocked - } - - let minCjitSats = minCjitSats ?? 0 - guard let maxCjitAmountSats, maxCjitAmountSats > 0 else { - return .chooseAmount - } - - if invoiceAmountSats == 0 || minCjitSats == 0 || invoiceAmountSats < minCjitSats || invoiceAmountSats > maxCjitAmountSats { - return .chooseAmount - } - - return .createCjit(invoiceAmountSats) - } - - static func needsCjitLimitsForAdditionalLiquidity( - source: ReceiveLiquiditySource, - invoiceAmountSats: UInt64, - inboundCapacitySats: UInt64?, - isGeoBlocked: Bool - ) -> Bool { guard source == .spending, needsInboundLiquidity(invoiceAmountSats: invoiceAmountSats, inboundCapacitySats: inboundCapacitySats), (inboundCapacitySats ?? 0) > 0 else { - return false + return .none } - return !isGeoBlocked + return .transferUnavailable } static func needsInboundLiquidity(invoiceAmountSats: UInt64, inboundCapacitySats: UInt64?) -> Bool { diff --git a/Bitkit/Models/SettingsBackupConfig.swift b/Bitkit/Models/SettingsBackupConfig.swift index ca9412d48..03a769836 100644 --- a/Bitkit/Models/SettingsBackupConfig.swift +++ b/Bitkit/Models/SettingsBackupConfig.swift @@ -21,8 +21,6 @@ enum SettingsBackupConfig { "hasSeenNotificationsIntro", "hasSeenQuickpayIntro", "hasSeenShopIntro", - "hasSeenTransferIntro", - "hasSeenTransferToSpendingIntro", "hasSeenTransferToSavingsIntro", "hasSeenWidgetsIntro", "hasDismissedWidgetsOnboardingHint", @@ -33,7 +31,6 @@ enum SettingsBackupConfig { "dismissedSuggestions", "lastUsedTags", "quickPayLedger", - BlocktankRefundAddressStore.key, ] static let settingsKeyTypes: [String: SettingKeyType] = [ diff --git a/Bitkit/Resources/Localization/ar.lproj/Localizable.strings b/Bitkit/Resources/Localization/ar.lproj/Localizable.strings index 0f917a710..6619e2f8a 100644 --- a/Bitkit/Resources/Localization/ar.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ar.lproj/Localizable.strings @@ -2,8 +2,8 @@ "cards__backupSeedPhrase__description" = "احفظ عملات Bitcoin الخاصة بك"; "cards__quickpay__title" = "QuickPay"; "cards__quickpay__description" = "امسح وادفع"; -"cards__notifications__description_alt" = "عندما يكون Bitkit مغلقًا"; -"cards__notifications__title" = "استلم الأموال"; +"cards__notifications__description_alt" = "تذكيرات الدفع"; +"cards__notifications__title" = "الإشعارات"; "cards__invite__title" = "دعوة"; "cards__invite__description" = "شارك Bitkit"; "cards__lightning__title" = "إنفاق"; @@ -136,9 +136,7 @@ "common__learn_more" = "اعرف المزيد"; "common__understood" = "مفهوم"; "common__connect" = "اتصال"; -"common__min" = "الحد الأدنى"; "common__max" = "الحد الأقصى"; -"common__default" = "افتراضي"; "common__preview" = "معاينة"; "common__show_all" = "عرض الكل"; "common__show_details" = "عرض التفاصيل"; @@ -163,7 +161,6 @@ "fee__slow__description" = "±1 ساعة"; "fee__slow__shortDescription" = "±1 ساعة"; "fee__slow__range" = "±1-2 ساعة"; -"fee__minimum__title" = "الحد الأدنى"; "fee__minimum__longTitle" = "الحد الأدنى"; "fee__minimum__description" = "+2 ساعة"; "fee__minimum__shortDescription" = "+2 ساعة"; @@ -173,64 +170,27 @@ "fee__custom__description" = "يعتمد على الرسوم"; "fee__custom__shortDescription" = "يعتمد على الرسوم"; "fee__custom__range" = "يعتمد على الرسوم"; -"lightning__transfer_intro__title" = "رصيد\nالإنفاق"; -"lightning__transfer_intro__text" = "موّل رصيد إنفاقك للاستمتاع بمعاملات فورية ورخيصة مع الأصدقاء والعائلة والتجار."; -"lightning__transfer_intro__button" = "ابدأ"; "lightning__funding__title" = "موّل رصيد إنفاقك"; -"lightning__funding__text" = "أسهل خيار هو استخدام مدخرات Bitkit الخاصة بك أو إرسال بيتكوين من محفظة مختلفة."; -"lightning__funding__text_blocked" = "لا يوفر Bitkit حاليًا خدمات Lightning في بلدك، ولكن لا يزال بإمكانك الاتصال بعقد أخرى مباشرة."; -"lightning__funding__text_blocked_cjit" = "لا يوفر Bitkit حاليًا خدمات Lightning في بلدك، ولكن لا يزال بإمكانك الاتصال بعقد أخرى مباشرة."; "lightning__funding__button1" = "تحويل من المدخرات"; "lightning__funding__button2" = "استخدام محفظة أخرى"; "lightning__funding__button3" = "إعداد يدوي"; -"lightning__funding_advanced__nav_title" = "رصيد الإنفاق"; -"lightning__funding_advanced__title" = "إعداد متقدم"; -"lightning__funding_advanced__text" = "امسح رمز QR للمطالبة بقناة LNURL من LSP آخر، أو اختر الإعداد اليدوي."; -"lightning__funding_advanced__button1" = "رمز QR لقناة LNURL"; -"lightning__funding_advanced__button2" = "إعداد يدوي"; "lightning__funding_advanced__button_short" = "إعداد متقدم"; -"lightning__no_funds__title" = "لا توجد أموال متاحة"; -"lightning__no_funds__description" = "قبل أن تتمكن من تحويل الأموال من رصيد مدخراتك، تحتاج إلى إرسال Bitcoin إلى محفظة Bitkit الخاصة بك."; -"lightning__no_funds__fund" = "تمويل المحفظة"; "lightning__transfer__nav_title" = "تحويل الأموال"; "lightning__transfer__confirm" = "يرجى\nالتأكيد"; "lightning__transfer__swipe" = "اسحب للتحويل"; "lightning__transfer_hw__broadcast_uncertain_title" = "ربما تم إرسال المعاملة"; "lightning__transfer_hw__broadcast_uncertain_description" = "ربما تم بث معاملة التمويل. يرجى التحقق من نشاطك قبل المحاولة مرة أخرى."; -"lightning__transfer_hw__open_connect" = "فتح Trezor Connect"; "lightning__transfer_hw__reconnect_error_description" = "يرجى إعادة توصيل جهازك، يبدو أنه غير متصل بهاتفك."; "lightning__transfer_hw__reconnect_error_title" = "إعادة توصيل الجهاز"; -"lightning__transfer_hw__sign_title" = "وقّع باستخدام\nجهازك"; -"lightning__transfer_hw__signed_title" = "تم توقيع\nالمعاملة"; "lightning__transfer_to_savings_button" = "تحويل إلى المدخرات"; "lightning__transfer_to_spending_button" = "تحويل إلى الإنفاق"; -"lightning__spending_intro__title" = "تحويل\nإلى الإنفاق"; -"lightning__spending_intro__text" = "حوّل الأموال إلى رصيد إنفاقك للاستمتاع بمعاملات فورية ورخيصة مع الأصدقاء والعائلة والتجار."; -"lightning__spending_intro__button" = "ابدأ"; -"lightning__spending_amount__title" = "تحويل\nإلى الإنفاق"; "lightning__spending_amount__quarter" = "25%"; -"lightning__spending_amount__error_balance__description" = "لا تكفي مدخراتك لتغطية هذا التحويل ورسومه. جرّب مبلغًا أصغر."; -"lightning__spending_amount__error_balance__title" = "المدخرات غير كافية"; "lightning__spending_amount__error_max__title" = "الحد الأقصى لرصيد الإنفاق"; "lightning__spending_amount__error_max__description" = "المبلغ الذي يمكنك تحويله إلى رصيد الإنفاق محدود حاليًا بـ ₿ {amount}."; "lightning__spending_confirm__network_fee" = "رسوم الشبكة"; "lightning__spending_confirm__lsp_fee" = "رسوم الخدمة"; "lightning__spending_confirm__amount" = "إلى الإنفاق"; -"lightning__spending_confirm__background_setup" = "إعداد في الخلفية"; "lightning__spending_confirm__total" = "الإجمالي"; -"lightning__spending_confirm__default" = "استخدام الإعدادات الافتراضية"; -"lightning__spending_confirm__fees_changed_title" = "تغيّرت الرسوم"; -"lightning__spending_confirm__fees_changed_service" = "الرسوم أعلى بمقدار {amount} الآن بعد إنشاء طلبك. اسحب مرة أخرى للتأكيد."; -"lightning__spending_confirm__fees_changed_network" = "الرسوم أعلى بمقدار {amount} لأن معدلات الرسوم أو رصيد مدخراتك تغيّر أثناء وجودك في هذه الشاشة. اسحب مرة أخرى للتأكيد."; -"lightning__spending_advanced__title" = "سعة\nالاستلام"; -"lightning__spending_advanced__fee" = "رسوم السيولة"; -"lightning__spending_advanced__error_balance__description" = "لا تكفي مدخراتك لتغطية رسوم السيولة لسعة الاستلام هذه. اختر مبلغًا أصغر."; -"lightning__spending_advanced__error_balance__title" = "الأموال غير كافية"; -"lightning__spending_advanced__error_max__title" = "الحد الأقصى لسعة الاستلام"; -"lightning__spending_advanced__error_max__description" = "سعة الاستلام محدودة حاليًا بـ ₿ {amount}."; -"lightning__liquidity__title" = "السيولة\nوالتوجيه"; -"lightning__liquidity__text" = "يستخدم رصيد إنفاقك Lightning Network لجعل مدفوعاتك أرخص وأسرع وأكثر خصوصية.\n\nيعمل هذا مثل الوصول إلى الإنترنت، لكنك تدفع مقابل السيولة والتوجيه بدلاً من النطاق الترددي.\n\nيتضمن هذا الإعداد بعض التكاليف لمرة واحدة."; -"lightning__liquidity__label" = "سيولة رصيد الإنفاق"; "lightning__savings_intro__title" = "تحويل\nإلى المدخرات"; "lightning__savings_intro__text" = "حوّل رصيد إنفاقك إلى مدخراتك لتخزين Bitcoin على المدى الطويل وتأمين مستقبلك."; "lightning__savings_intro__button" = "ابدأ"; @@ -254,9 +214,6 @@ "lightning__savings_interrupted__nav_title" = "تحويل\nمتقطع"; "lightning__savings_interrupted__title" = "أبقِ Bitkit\nمفتوحًا"; "lightning__savings_interrupted__text" = "لم تُحوَّل الأموال بعد. سيحاول Bitkit بدء التحويل خلال الـ 30 دقيقة القادمة. يرجى إبقاء التطبيق مفتوحًا."; -"lightning__transfer_success__nav_title" = "نجح التحويل"; -"lightning__transfer_success__title_spending" = "انتقلت الأموال\nإلى الإنفاق"; -"lightning__transfer_success__text_spending" = "تم تحويل أموالك ورصيد إنفاقك جاهز للاستخدام."; "lightning__transfer_success__title_savings" = "انتقلت الأموال\nإلى المدخرات"; "lightning__transfer_success__text_savings" = "تم تحويل أموالك. يمكنك استخدام هذه الأموال فورًا."; "lightning__external__nav_title" = "اتصال Lightning"; @@ -275,12 +232,6 @@ "lightning__savings" = "المدخرات"; "lightning__spending_label" = "رصيد الإنفاق"; "lightning__receiving_label" = "سعة الاستلام"; -"lightning__setting_up_text" = "يرجى الانتظار، تحويل أموالك قيد التنفيذ. يجب أن يستغرق هذا ±10 دقائق."; -"lightning__setting_up_step1" = "جارٍ معالجة الدفع"; -"lightning__setting_up_step2" = "نجح الدفع"; -"lightning__setting_up_step3" = "في قائمة الانتظار للفتح"; -"lightning__setting_up_step4" = "جارٍ فتح الاتصال"; -"lightning__setting_up_button" = "متابعة استخدام Bitkit"; "lightning__node_info" = "عقدة Lightning"; "lightning__connections" = "اتصالات Lightning"; "lightning__conn_pending" = "الاتصالات المعلقة"; @@ -338,9 +289,9 @@ "lightning__force_init_msg" = "ستكون أموالك متاحة خلال ±14 يومًا."; "lightning__force_failed_title" = "فشل التحويل الإجباري"; "lightning__force_failed_msg" = "تعذر تحويل أموالك إلى المدخرات. يرجى المحاولة مرة أخرى."; -"lightning__force_channels_skipped" = "تعذر إغلاق بعض الاتصالات."; "lightning__channel_opened_title" = "رصيد الإنفاق جاهز"; "lightning__channel_opened_msg" = "يمكنك الآن الدفع لأي شخص، في أي مكان، فورًا."; +"lightning__funding__button_hw" = "تحويل من محفظة الأجهزة"; "lightning__order_state__awaiting_payment" = "جارٍ معالجة الدفع"; "lightning__order_state__payment_canceled" = "تم إلغاء الدفع"; "lightning__order_state__paid" = "نجح الدفع"; @@ -351,6 +302,9 @@ "lightning__order_state__open" = "الاتصال مفتوح"; "lightning__order_state__inactive" = "الاتصال غير نشط"; "lightning__peer_disconnected" = "النظير غير متصل."; +"lightning__transfer_unavailable__nav_title" = "التحويل غير متاح"; +"lightning__transfer_unavailable__text" = "لم تعد التحويلات التلقائية إلى رصيد الإنفاق متاحة. استخدم عقدتك الخاصة، أو أنشئ اتصالًا مع مزوّد آخر."; +"lightning__transfer_unavailable__title" = "الإعداد اليدوي\nمطلوب"; "onboarding__tos_header" = "Bitkit\nشروط الاستخدام"; "onboarding__tos_subtitle" = "بالمتابعة، أنت تقر بأنك قد قرأت وتقبل شروط الاستخدام."; "onboarding__tos_title" = "شروط الاستخدام"; @@ -365,7 +319,6 @@ "onboarding__slide0_text_alt" = "يمنحك Bitkit المفاتيح للتحكم في أموالك وملفك الشخصي وجهات اتصالك. تحكم وكن بلا حدود."; "onboarding__slide1_header" = "مدفوعات\nفورية"; "onboarding__slide1_text_alt" = "استمتع بمدفوعات فورية ورخيصة مع الأصدقاء والعائلة والتجار على Lightning Network."; -"onboarding__slide1_note" = "*لا يوفر Bitkit حاليًا خدمات Lightning في بلدك، ولكن لا يزال بإمكانك الاتصال بعقد أخرى."; "onboarding__slide3_header" = "الخصوصية\nليست جريمة"; "onboarding__slide3_text_alt" = "أموالك، خصوصيتك. اسحب لإخفاء رصيدك وأنفق بخصوصية أكبر، بدون تتبع بيانات وبدون KYC."; "onboarding__slide4_header" = "مفاتيحك،\nعملاتك"; @@ -452,8 +405,6 @@ "other__lnurl_withdr_error_minmax" = "معلمات سحب LNURL غير صحيحة، الحد الأدنى/الأقصى غير محدد بشكل صحيح."; "other__lnurl_withdr_success_title" = "تم طلب السحب"; "other__lnurl_withdr_success_msg" = "تم طلب سحبك بنجاح. في انتظار الدفع."; -"other__notification_registration_failed_description" = "تعذر على Bitkit التسجيل لإشعارات الدفع."; -"other__notification_registration_failed_title" = "فشل تسجيل الإشعارات"; "other__earlier" = "سابقًا"; "other__update_nav_title" = "تحديث متاح"; "other__update_title" = "حدّث\nBitkit"; @@ -462,13 +413,6 @@ "other__try_again" = "يرجى المحاولة مرة أخرى."; "other__scan__error__generic" = "لا يستطيع Bitkit قراءة البيانات المقدمة."; "other__scan__error__expired" = "انتهت صلاحية فاتورة Lightning هذه."; -"other__gift__claimed__text" = "عذرًا، لقد تأخرت! تمت المطالبة بجميع الهدايا لهذا الرمز."; -"other__gift__claiming__text" = "جارٍ المطالبة برمز هدية Bitkit..."; -"other__gift__claiming__title" = "جارٍ المطالبة بالهدية"; -"other__gift__error__title" = "خطأ في رمز الهدية"; -"other__gift__error__text" = "تعذر على Bitkit المطالبة بالأموال. يرجى المحاولة لاحقًا أو الاتصال بنا."; -"other__gift__used__title" = "رمز مستخدم"; -"other__gift__used__text" = "تم استخدام رمز هدية Bitkit هذا بالفعل، وتم دفع الأموال."; "other__shop__intro__title" = "تسوق"; "other__shop__intro__description" = "اجعل حياتك على معيار Bitcoin. أنفق Bitcoin على بطاقات الهدايا الرقمية وبطاقات eSIM وشحن الهاتف والمزيد."; "other__shop__intro__button" = "ابدأ"; @@ -619,6 +563,7 @@ "pubky_auth__homeserver" = "الخادم الرئيسي"; "pubky_auth__signup_description" = "أنشئ هوية Pubky جديدة على هذا الخادم الرئيسي. تابع فقط إذا كنت تثق به."; "pubky_auth__approval_failed" = "فشل التفويض"; +"wallet__onchain_abandon_transfer_note" = "لم تعد التحويلات إلى رصيد الإنفاق متاحة. يتحقق Bitkit أولًا من الشبكة، ثم يعيد عملات هذا التحويل إلى محفظتك حتى لا يتأكد التحويل الأصلي أبدًا. يكلّف ذلك رسوم شبكة."; "watch_only_accounts__active_section" = "الحسابات النشطة"; "watch_only_accounts__copy_xpub" = "نسخ xpub"; "watch_only_accounts__description" = "يحصل كل خادم Paykit معتمد على حساب Bitcoin منفصل. أوقف التتبع لإلغاء تحميل حساب دون حذف سجل محفظته."; @@ -690,21 +635,17 @@ "settings__widgets__reset_widgets_dialog_title" = "إعادة تعيين الأدوات؟"; "settings__widgets__reset_widgets_dialog_description" = "هل أنت متأكد من رغبتك في إعادة تعيين الأدوات؟ سيتم عرض مجموعة الأدوات الافتراضية بالإعدادات الافتراضية."; "settings__widgets__reset_suggestions" = "إعادة تعيين بطاقات الاقتراحات"; -"settings__notifications__nav_title" = "المدفوعات في الخلفية"; -"settings__notifications__intro__title" = "استلم الأموال\nبشكل سلبي"; -"settings__notifications__intro__text" = "قم بتشغيل الإشعارات للحصول على المدفوعات، حتى عندما يكون تطبيق Bitkit مغلقًا."; +"settings__notifications__nav_title" = "الإشعارات"; +"settings__notifications__intro__title" = "ابقَ\nعلى اطلاع"; +"settings__notifications__intro__text" = "فعّل الإشعارات لتصلك تذكيرات باشتراكاتك وطلبات الدفع."; "settings__notifications__intro__button" = "تمكين"; -"settings__notifications__settings__toggle" = "استلام الأموال عندما يكون Bitkit مغلقًا"; -"settings__notifications__settings__enabled" = "المدفوعات في الخلفية ممكّنة. يمكنك استلام الأموال حتى عندما يكون التطبيق مغلقًا (إذا كان جهازك متصلاً بالإنترنت)."; -"settings__notifications__settings__disabled" = "قم بتمكين المدفوعات في الخلفية لاستلام الأموال حتى عندما يكون التطبيق مغلقًا (إذا كان جهازك متصلاً بالإنترنت)."; -"settings__notifications__settings__denied" = "المدفوعات في الخلفية معطلة، لأنك رفضت الإشعارات."; -"settings__notifications__settings__preview__title" = "تم استلام دفعة"; -"settings__notifications__settings__preview__text" = "افتح Bitkit لرؤية التفاصيل"; +"settings__notifications__settings__toggle" = "السماح بالإشعارات"; +"settings__notifications__settings__enabled" = "الإشعارات مفعّلة. يمكن لـ Bitkit تذكيرك بالاشتراكات وطلبات الدفع."; +"settings__notifications__settings__disabled" = "فعّل الإشعارات لتصلك تذكيرات باشتراكاتك وطلبات الدفع."; +"settings__notifications__settings__denied" = "الإشعارات متوقفة لأنك رفضتها."; "settings__notifications__settings__preview__time" = "منذ 3 دقائق"; -"settings__notifications__settings__privacy__label" = "الخصوصية"; -"settings__notifications__settings__privacy__text" = "تضمين المبلغ في الإشعارات"; "settings__notifications__settings__notifications__label" = "الإشعارات"; -"settings__notifications__settings__notifications__text" = "لتمكين المدفوعات في الخلفية، يرجى تمكين الإشعارات في إعدادات Bitkit على iOS."; +"settings__notifications__settings__notifications__text" = "لتلقي التذكيرات، يرجى تمكين الإشعارات في إعدادات Bitkit على iOS."; "settings__notifications__settings__button__enabled" = "تخصيص في إعدادات Bitkit على {platform}"; "settings__notifications__settings__button__disabled" = "تمكين في إعدادات Bitkit على {platform}"; "settings__quickpay__nav_title" = "QuickPay"; @@ -771,15 +712,9 @@ "wallet__receive_lightning_invoice" = "فاتورة Lightning"; "wallet__receive_note_placeholder" = "ملاحظة اختيارية للدافع"; "wallet__receive_show_qr" = "عرض رمز QR"; -"wallet__receive_background_setup_text" = "قم بتمكين الإعداد في الخلفية للخروج من Bitkit بأمان أثناء تكوين رصيدك."; -"wallet__receive_background_setup_toggle" = "تمكين الإعداد في الخلفية"; "wallet__receive_spending" = "استلام أموال Lightning"; "wallet__receive_spending_text" = "استمتع بمدفوعات Bitcoin فورية ورخيصة على شبكة Lightning."; "wallet__receive_spending_title" = "استلم على رصيد الإنفاق"; -"wallet__receive_connect_initial" = "لإعداد رصيد إنفاقك، سيتم خصم رسوم شبكة {networkFee} ورسوم مزود الخدمة {serviceFee}."; -"wallet__receive_connect_additional" = "لاستلام المزيد من Bitcoin الفوري، يتعين على Bitkit زيادة سيولتك. سيتم خصم رسوم شبكة {networkFee} ورسوم مزود الخدمة {serviceFee} من المبلغ الذي حددته."; -"wallet__receive_liquidity__nav_title" = "إعداد رصيد الإنفاق"; -"wallet__receive_liquidity__nav_title_additional" = "زيادة سعة الاستقبال"; "settings__backup__category_contacts" = "جهات الاتصال"; "settings__support__title" = "الدعم"; "settings__support__text" = "تحتاج مساعدة؟ أبلغ عن مشكلتك من داخل Bitkit، أو زُر مركز المساعدة، أو تحقق من الحالة، أو تواصل معنا عبر قنواتنا الاجتماعية."; @@ -1205,9 +1140,6 @@ "wallet__onchain_broadcast_rejected_note" = "ربما وصلت هذه الدفعة إلى الشبكة رغم ذلك. لا ترسلها مرة أخرى. {reason}"; "wallet__onchain_broadcast_unconfirmed" = "البث غير مؤكد"; "wallet__onchain_broadcast_unconfirmed_note" = "ربما تم إرسال الدفعة. لا ترسلها مرة أخرى."; -"wallet__onchain_funding_rejected" = "تم رفض معاملة التمويل. لا تحاول تمويل هذا الطلب مرة أخرى."; -"wallet__onchain_funding_unknown" = "ربما تم إرسال معاملة التمويل. لا تحاول تمويل هذا الطلب مرة أخرى."; -"wallet__onchain_funding_followup_failed" = "تم إرسال التمويل، لكن تعذر حفظ تتبع الطلب محليًا. لا تموّل هذا الطلب مرة أخرى."; "wallet__onchain_retry_original" = "إعادة محاولة الدفعة الأصلية"; "wallet__onchain_retry_fee" = "معدل الرسوم (sat/vB)"; "wallet__onchain_retry_note" = "إعادة محاولة إرسال {amount} sats إلى {address} باستخدام العملات الأصلية نفسها. يبقى مبلغ المستلم دون تغيير. أدخل معدل رسوم الشبكة بوحدة sat/vB. لا يمكن أن ينشئ هذا دفعة منفصلة."; @@ -1434,7 +1366,7 @@ "subscriptions__name_placeholder" = "اسم الاشتراك"; "subscriptions__ongoing" = "مستمر"; "subscriptions__overview" = "نظرة عامة"; -"subscriptions__payment_due_description" = "افتح Bitkit لمراجعة دفعة اشتراك."; +"subscriptions__payment_due_description" = "افتح Bitkit لمراجعة الدفعة"; "subscriptions__payment_due_title" = "دفعة اشتراك مستحقة"; "subscriptions__payments" = "المدفوعات"; "subscriptions__per_day" = "يوميًا"; @@ -1547,9 +1479,6 @@ "wallet__toast_payment_failed_title" = "فشلت الدفعة"; "wallet__payment_failed_description" = "فشل دفعك الفوري. يرجى المحاولة مرة أخرى."; -"wallet__receive_cjit_error_node_capacity__title" = "السعة الإضافية غير متاحة"; -"wallet__receive_cjit_error_node_capacity__description" = "لا يستطيع Bitkit إضافة المزيد من سعة الإنفاق إلى هذه المحفظة في الوقت الحالي."; -"wallet__receive_will" = "ستستلم"; "wallet__activity" = "النشاط"; "wallet__activity_no" = "لا يوجد نشاط بعد"; "wallet__activity_sent" = "مرسل"; @@ -1581,13 +1510,6 @@ "wallet__activity_status" = "الحالة"; "wallet__activity_date" = "التاريخ"; "wallet__activity_time" = "الوقت"; -"wallet__receive_liquidity__invalid_quote" = "تلقى Bitkit عرض سيولة غير صالح. يرجى المحاولة مرة أخرى."; -"wallet__receive_liquidity__text" = "يستخدم رصيد إنفاقك Lightning Network لجعل مدفوعاتك أرخص وأسرع وأكثر خصوصية.\n\nيعمل هذا مثل الوصول إلى الإنترنت، لكنك تدفع مقابل السيولة والتوجيه بدلاً من النطاق الترددي.\n\nيتضمن هذا الإعداد بعض التكاليف لمرة واحدة."; -"wallet__receive_liquidity__text_additional" = "يستخدم رصيد إنفاقك Lightning Network لجعل مدفوعاتك أرخص وأسرع وأكثر خصوصية.\n\nيعمل هذا مثل الوصول إلى الإنترنت، لكنك تدفع مقابل السيولة والتوجيه بدلاً من النطاق الترددي.\n\nيحتاج Bitkit إلى زيادة سعة استلام رصيد إنفاقك لمعالجة هذه الدفعة."; -"wallet__receive_liquidity__label" = "سيولة رصيد الإنفاق"; -"wallet__receive_liquidity__label_additional" = "سيولة رصيد إنفاق إضافية"; -"wallet__receive_cjit_error_max__title" = "الحد الأقصى لسعة الاستلام"; -"wallet__receive_cjit_error_max__description" = "الحد الأقصى الذي يمكنك استلامه في رصيد الإنفاق الآن هو ₿ {amount}."; "subscriptions__allowances" = "المخصصات"; "subscriptions__allowances_empty_headline" = "أعدّ\nالمخصصات"; "subscriptions__allowances_empty_description" = "يمكنك تعيين حدود إنفاق لتلبية طلبات الدفع من جهات الاتصال الموثوقة تلقائيًا."; diff --git a/Bitkit/Resources/Localization/ca.lproj/Localizable.strings b/Bitkit/Resources/Localization/ca.lproj/Localizable.strings index 0fe96ef4e..ed04dc57b 100644 --- a/Bitkit/Resources/Localization/ca.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ca.lproj/Localizable.strings @@ -4,8 +4,8 @@ "cards__discount__description" = "Saldo de despesa"; "cards__quickpay__title" = "QuickPay"; "cards__quickpay__description" = "Escaneja i paga"; -"cards__notifications__description_alt" = "Quan Bitkit està tancat"; -"cards__notifications__title" = "Rebre pagaments"; +"cards__notifications__description_alt" = "Recordatoris de pagament"; +"cards__notifications__title" = "Notificacions"; "cards__invite__title" = "Convidar"; "cards__invite__description" = "Comparteix Bitkit"; "cards__lightning__title" = "Gastar"; @@ -151,9 +151,7 @@ "common__learn_more" = "Aprèn més"; "common__understood" = "Entès"; "common__connect" = "Connecta"; -"common__min" = "Mín"; "common__max" = "Màx"; -"common__default" = "Predeterminat"; "common__preview" = "Previsualització"; "common__show_all" = "Mostra tot"; "common__show_details" = "Mostra detalls"; @@ -179,7 +177,6 @@ "fee__slow__shortRange" = "1-2h"; "fee__slow__shortDescription" = "±1h"; "fee__slow__range" = "±1-2 hores"; -"fee__minimum__title" = "Mínim"; "fee__minimum__longTitle" = "Mínim"; "fee__minimum__description" = "+2 hores"; "fee__minimum__shortRange" = "+2h"; @@ -198,66 +195,29 @@ "fee__slow__longRange" = "± 1-2 hores"; "fee__custom__longTitle" = "Personalitzat"; "fee__custom__longRange" = "Depèn de la tarifa"; -"lightning__transfer_intro__title" = "Saldo de\ndespesa"; -"lightning__transfer_intro__text" = "Finança el teu saldo de despesa per gaudir de transaccions instantànies i barates amb amics, família i comerciants."; -"lightning__transfer_intro__button" = "Començar"; "lightning__funding__nav_title" = "Saldo de despesa"; "lightning__funding__title" = "Finança el teu saldo de despesa"; -"lightning__funding__text" = "La forma més fàcil és utilitzar els teus estalvis de Bitkit o enviar bitcoin des d\'una altra cartera."; -"lightning__funding__text_blocked" = "Bitkit actualment no ofereix serveis Lightning al teu país, però encara pots connectar-te directament a altres nodes."; -"lightning__funding__text_blocked_cjit" = "Bitkit actualment no ofereix serveis Lightning al teu país, però encara pots connectar-te directament a altres nodes."; "lightning__funding__button1" = "Transferir des d\'estalvis"; "lightning__funding__button2" = "Utilitza un altre moneder"; "lightning__funding__button3" = "Configuració manual"; -"lightning__funding_advanced__nav_title" = "Saldo de despesa"; -"lightning__funding_advanced__title" = "Configuració avançada"; -"lightning__funding_advanced__text" = "Escaneja un QR per reclamar el teu canal LNURL d\'un altre LSP, o tria la configuració manual."; -"lightning__funding_advanced__button1" = "QR de canal LNURL"; -"lightning__funding_advanced__button2" = "Configuració manual"; "lightning__funding_advanced__button_short" = "Configuració avançada"; -"lightning__no_funds__title" = "No hi ha fons disponibles"; -"lightning__no_funds__description" = "Abans de poder transferir fons del teu saldo d'estalvis, has d'enviar bitcoin a la teva cartera Bitkit."; -"lightning__no_funds__fund" = "Finançar cartera"; "lightning__transfer__nav_title" = "Transferir fons"; "lightning__transfer__confirm" = "Si us plau\nconfirma"; "lightning__transfer__swipe" = "Llisca per transferir"; "lightning__transfer_hw__broadcast_uncertain_title" = "És possible que la transacció s'hagi enviat"; "lightning__transfer_hw__broadcast_uncertain_description" = "És possible que la teva transacció de finançament s'hagi difós. Comprova la teva activitat abans de tornar-ho a provar."; -"lightning__transfer_hw__open_connect" = "Obrir Trezor Connect"; "lightning__transfer_hw__reconnect_error_description" = "Torna a connectar el teu dispositiu de maquinari; sembla que s'ha desconnectat del telèfon."; "lightning__transfer_hw__reconnect_error_title" = "Torna a connectar el dispositiu"; -"lightning__transfer_hw__sign_title" = "Signa amb\nel teu dispositiu"; -"lightning__transfer_hw__signed_title" = "Transacció\nsignada"; "lightning__transfer_to_savings_button" = "Transferir a estalvis"; "lightning__transfer_to_spending_button" = "Transferir a despesa"; -"lightning__spending_intro__title" = "Transferir\na despesa"; -"lightning__spending_intro__text" = "Transfereix fons al teu saldo de despesa per gaudir de transaccions instantànies i barates amb amics, família i comerciants."; -"lightning__spending_intro__button" = "Començar"; "lightning__transfer_failed__continue" = "Torna a intentar"; -"lightning__spending_amount__title" = "Transferir\na despesa"; "lightning__spending_amount__quarter" = "25%"; -"lightning__spending_amount__error_balance__description" = "Els teus estalvis no poden cobrir aquesta transferència i les seves tarifes. Prova un import més petit."; -"lightning__spending_amount__error_balance__title" = "Estalvis insuficients"; "lightning__spending_amount__error_max__title" = "Màxim del saldo de despesa"; "lightning__spending_amount__error_max__description" = "L'import que pots transferir al teu saldo de despesa està limitat a ₿ {amount}."; "lightning__spending_confirm__network_fee" = "Comissions de xarxa"; "lightning__spending_confirm__lsp_fee" = "Comissions de servei"; "lightning__spending_confirm__amount" = "A despesa"; -"lightning__spending_confirm__background_setup" = "Configurar en segon pla"; "lightning__spending_confirm__total" = "Total"; -"lightning__spending_confirm__default" = "Utilitzar valors predeterminats"; -"lightning__spending_confirm__fees_changed_title" = "Les tarifes han canviat"; -"lightning__spending_confirm__fees_changed_service" = "Les tarifes són {amount} més altes ara que s'ha creat la teva comanda. Torna a desplaçar per confirmar."; -"lightning__spending_confirm__fees_changed_network" = "Les tarifes són {amount} més altes perquè les taxes de tarifa o el teu saldo d'estalvis han canviat mentre eres en aquesta pantalla. Torna a desplaçar per confirmar."; -"lightning__spending_advanced__title" = "Capacitat de\nrecepció"; -"lightning__spending_advanced__fee" = "Comissió de liquiditat"; -"lightning__spending_advanced__error_balance__description" = "Els teus estalvis no poden cobrir la tarifa de liquiditat per a aquesta capacitat de recepció. Tria un import més petit."; -"lightning__spending_advanced__error_balance__title" = "Fons insuficients"; -"lightning__spending_advanced__error_max__title" = "Capacitat de recepció màxima"; -"lightning__spending_advanced__error_max__description" = "La capacitat de recepció està limitada actualment a ₿ {amount}."; -"lightning__liquidity__title" = "Liquiditat\ni encaminament"; -"lightning__liquidity__text" = "El teu saldo de despesa utilitza Lightning Network per fer els teus pagaments més barats, ràpids i privats.\n\nAixò funciona com l\'accés a Internet, però pagues per liquiditat i encaminament en lloc d\'amplada de banda.\n\nAquesta configuració inclou alguns costos únics."; -"lightning__liquidity__label" = "Liquiditat del saldo de despesa"; "lightning__savings_intro__title" = "Transferir\na estalvis"; "lightning__savings_intro__text" = "Transfereix el teu saldo de despesa als teus estalvis per emmagatzemar els teus Bitcoin a llarg termini i assegurar el teu futur."; "lightning__savings_intro__button" = "Començar"; @@ -281,9 +241,6 @@ "lightning__savings_interrupted__nav_title" = "Transferència\ninterrompuda"; "lightning__savings_interrupted__title" = "Mantén Bitkit\nobert"; "lightning__savings_interrupted__text" = "Els fons encara no s\'han transferit. Bitkit intentarà iniciar la transferència durant els pròxims 30 minuts. Si us plau, mantén l\'aplicació oberta."; -"lightning__transfer_success__nav_title" = "Transferència realitzada"; -"lightning__transfer_success__title_spending" = "Fons transferits\na despesa"; -"lightning__transfer_success__text_spending" = "Els teus fons s\'han transferit i el teu saldo de despesa està llest per utilitzar."; "lightning__transfer_success__title_savings" = "Fons transferits\na estalvis"; "lightning__transfer_success__text_savings" = "Els teus fons s\'han transferit. Pots utilitzar aquests fons immediatament."; "lightning__external__nav_title" = "Connexió Lightning"; @@ -304,12 +261,6 @@ "lightning__savings" = "Estalvis"; "lightning__spending_label" = "Saldo de despesa"; "lightning__receiving_label" = "Capacitat de recepció"; -"lightning__setting_up_text" = "Si us plau, espera, la transferència de fons està en curs. Això hauria de trigar ±10 minuts."; -"lightning__setting_up_step1" = "Processant pagament"; -"lightning__setting_up_step2" = "Pagament realitzat amb èxit"; -"lightning__setting_up_step3" = "En cua per obrir"; -"lightning__setting_up_step4" = "Obrint connexió"; -"lightning__setting_up_button" = "Continua utilitzant Bitkit"; "lightning__node_info" = "Node Lightning"; "lightning__node_id" = "ID de Node LDK"; "lightning__node_disconnected" = "desconnectat"; @@ -375,9 +326,9 @@ "lightning__force_init_msg" = "Els vostres fons seran accessibles en ±14 dies."; "lightning__force_failed_title" = "Transferència forçada fallida"; "lightning__force_failed_msg" = "No es possible transferir els teus fons de tornada a estalvi. Si us plau, intenta-ho de nou."; -"lightning__force_channels_skipped" = "Algunes connexions no s\'han pogut tancar."; "lightning__channel_opened_title" = "Saldo de despesa llest"; "lightning__channel_opened_msg" = "Ara pots pagar a qualsevol persona, en qualsevol lloc, instantàniament."; +"lightning__funding__button_hw" = "Transferir des del moneder HW"; "lightning__order_given_up_title" = "Configuració de pagaments instantanis fallida"; "lightning__order_expired_title" = "Configuració de pagaments instantanis fallida"; "lightning__order_state__awaiting_payment" = "Processant pagament"; @@ -392,6 +343,9 @@ "lightning__order_state__open" = "Connexió oberta"; "lightning__order_state__inactive" = "Connexió inactiva"; "lightning__peer_disconnected" = "Node parell desconnectat."; +"lightning__transfer_unavailable__nav_title" = "Transferència no disponible"; +"lightning__transfer_unavailable__text" = "Les transferències automàtiques al teu saldo de despesa ja no estan disponibles. Fes servir el teu propi node o configura una connexió amb un altre proveïdor."; +"lightning__transfer_unavailable__title" = "Configuració manual\nnecessària"; "lightning__wait_text_top" = "Si us plau, espereu que Bitkit es connecti a la xarxa de pagament (±10 segons)."; "lightning__wait_text_bottom" = "Connectant-se i sincronitzant..."; "onboarding__tos_header" = "Termes d\'ús\nde Bitkit"; @@ -409,7 +363,6 @@ "onboarding__slide0_text_alt" = "Bitkit et dóna les claus per controlar els teus diners, perfil i contactes. Pren el control i elimina les fronteres."; "onboarding__slide1_header" = "Pagaments\ninstantanis"; "onboarding__slide1_text_alt" = "Gaudeix de pagaments instantanis i barats amb amics, família i comerciants a Lightning Network."; -"onboarding__slide1_note" = "*Bitkit actualment no ofereix serveis Lightning al teu país, però encara pots connectar-te a altres nodes."; "onboarding__slide3_header" = "La privadesa no\nés un crim"; "onboarding__slide3_text_alt" = "Els teus diners, la teva privadesa. Llisca per amagar el teu saldo i gasta de manera més privada, sense seguiment de dades i sense KYC."; "onboarding__slide4_header" = "Les teves claus,\nles teves monedes"; @@ -520,8 +473,6 @@ "other__lnurl_withdr_error_minmax" = "Paràmetres de retirada LNURL incorrectes, min/max no configurats correctament."; "other__lnurl_withdr_success_title" = "Retirada sol·licitada"; "other__lnurl_withdr_success_msg" = "La teva retirada s\'ha sol·licitat correctament. Esperant el pagament."; -"other__notification_registration_failed_description" = "Bitkit no ha pogut registrar-se per a notificacions push."; -"other__notification_registration_failed_title" = "Registre de notificacions fallit"; "other__phone_settings" = "Obre la configuració del telèfon"; "other__bt_error_retrieve" = "Transferència fallida"; "other__bt_channel_purchase_cost_error" = "Necessites {delta} més per completar aquesta transacció."; @@ -533,14 +484,7 @@ "other__try_again" = "Torna-ho a provar."; "other__scan__error__generic" = "Bitkit no pot llegir les dades proporcionades."; "other__scan__error__expired" = "Aquesta factura Lightning ha caducat."; -"other__gift__claimed__text" = "Ho sentim, arribes tard! Tots els regals per a aquest codi ja s\'han reclamat."; -"other__gift__claiming__text" = "Reclamant el teu codi de regal de Bitkit..."; -"other__gift__claiming__title" = "Reclamant regal"; -"other__gift__error__title" = "Error del codi de regal"; -"other__gift__error__text" = "Bitkit no ha pogut reclamar els fons. Si us plau, torna-ho a provar més tard o contacta amb nosaltres."; "other__gift__error__button" = "OK"; -"other__gift__used__title" = "Codi usat"; -"other__gift__used__text" = "Aquest codi de regal de Bitkit ja s\'ha utilitzat i els fons s\'han pagat."; "other__gift__used__button" = "OK"; "other__shop__intro__title" = "Botiga"; "other__shop__intro__description" = "Viu la teva vida amb l\'estàndard Bitcoin. Gasta els teus Bitcoin en targetes regal digitals, eSIM, recàrregues de telèfon i molt més."; @@ -703,6 +647,7 @@ "pubky_auth__homeserver" = "Homeserver"; "pubky_auth__signup_description" = "Crea una nova identitat Pubky en aquest homeserver. Continua només si hi confies."; "pubky_auth__approval_failed" = "Ha fallat l'autorització"; +"wallet__onchain_abandon_transfer_note" = "Les transferències al saldo de despesa ja no estan disponibles. Bitkit primer comprova la xarxa i després retorna les monedes d'aquesta transferència a la teva cartera perquè la transferència original no es pugui confirmar mai. Això té una comissió de xarxa."; "watch_only_accounts__active_section" = "Comptes actius"; "watch_only_accounts__copy_xpub" = "Copiar xpub"; "watch_only_accounts__description" = "Cada servidor Paykit aprovat obté un compte de Bitcoin independent. Desactiva el seguiment per descarregar un compte sense eliminar l'historial de la seva cartera."; @@ -779,21 +724,17 @@ "settings__widgets__reset_widgets_dialog_title" = "Restablir widgets?"; "settings__widgets__reset_widgets_dialog_description" = "Estàs segur que vols restablir els widgets? Es mostrarà el conjunt de widgets predeterminat amb les configuracions predeterminades."; "settings__widgets__reset_suggestions" = "Restablir targetes de suggeriments"; -"settings__notifications__nav_title" = "Pagaments en segon pla"; -"settings__notifications__intro__title" = "Rep pagaments\npassivament"; -"settings__notifications__intro__text" = "Activa les notificacions per rebre pagaments, fins i tot quan l\'aplicació Bitkit està tancada."; +"settings__notifications__nav_title" = "Notificacions"; +"settings__notifications__intro__title" = "Mantén-te\ninformat"; +"settings__notifications__intro__text" = "Activa les notificacions per rebre recordatoris de les teves subscripcions i sol·licituds de pagament."; "settings__notifications__intro__button" = "Activar"; -"settings__notifications__settings__toggle" = "Rep pagaments quan Bitkit està tancat"; -"settings__notifications__settings__enabled" = "Els pagaments en segon pla estan activats. Pots rebre fons fins i tot quan l\'aplicació està tancada (si el teu dispositiu està connectat a Internet)."; -"settings__notifications__settings__disabled" = "Activa els pagaments en segon pla per rebre fons fins i tot quan l\'aplicació està tancada (si el teu dispositiu està connectat a Internet)."; -"settings__notifications__settings__denied" = "Els pagaments en segon pla estan desactivats perquè has denegat les notificacions."; -"settings__notifications__settings__preview__title" = "Pagament rebut"; -"settings__notifications__settings__preview__text" = "Obre Bitkit per veure els detalls"; +"settings__notifications__settings__toggle" = "Permet les notificacions"; +"settings__notifications__settings__enabled" = "Les notificacions estan activades. Bitkit et pot recordar les subscripcions i sol·licituds de pagament."; +"settings__notifications__settings__disabled" = "Activa les notificacions per rebre recordatoris de les teves subscripcions i sol·licituds de pagament."; +"settings__notifications__settings__denied" = "Les notificacions estan desactivades perquè les has rebutjat."; "settings__notifications__settings__preview__time" = "Fa 3m"; -"settings__notifications__settings__privacy__label" = "Privadesa"; -"settings__notifications__settings__privacy__text" = "Inclou l\'import a les notificacions"; "settings__notifications__settings__notifications__label" = "Notificacions"; -"settings__notifications__settings__notifications__text" = "Per activar els pagaments en segon pla, si us plau, activa les notificacions a la configuració de Bitkit d\'iOS."; +"settings__notifications__settings__notifications__text" = "Per rebre recordatoris, si us plau, activa les notificacions a la configuració de Bitkit d\'iOS."; "settings__notifications__settings__button__enabled" = "Personalitza a la configuració de Bitkit de {platform}"; "settings__notifications__settings__button__disabled" = "Activa a la configuració de Bitkit de {platform}"; "settings__quickpay__nav_title" = "QuickPay"; @@ -1280,26 +1221,10 @@ "wallet__receive_lightning_invoice" = "Factura Lightning"; "wallet__receive_note_placeholder" = "Nota opcional per al pagador"; "wallet__receive_show_qr" = "Mostra el codi QR"; -"wallet__receive_background_setup_text" = "Activa la configuració en segon pla per sortir de Bitkit amb seguretat mentre el teu saldo s\'està configurant."; -"wallet__receive_background_setup_toggle" = "Activa la configuració en segon pla"; "wallet__receive_spending" = "Rebre fons Lightning"; "wallet__receive_spending_text" = "Gaudeix de pagaments de bitcoin instantanis i barats a la xarxa Lightning."; "wallet__receive_spending_title" = "Rebre al saldo de despesa"; -"wallet__receive_connect_initial" = "Per configurar el teu saldo de despesa, es descomptarà una comissió de xarxa de {networkFee} i una comissió de proveïdor de servei de {serviceFee}."; -"wallet__receive_connect_additional" = "Per rebre més Bitcoin instantani, Bitkit ha d\'augmentar la teva liquiditat. Una comissió de xarxa de {networkFee} i una comissió de proveïdor de servei de {serviceFee} es descomptaran de l\'import que has especificat."; -"wallet__receive_liquidity__nav_title" = "Configuració del saldo de despesa"; -"wallet__receive_liquidity__nav_title_additional" = "Augmentar capacitat de recepció"; -"wallet__receive_liquidity__invalid_quote" = "Bitkit ha rebut una cotització de liquiditat no vàlida. Torna-ho a provar."; -"wallet__receive_liquidity__text" = "El teu saldo de despesa utilitza Lightning Network per fer els teus pagaments més barats, ràpids i privats.\n\nAixò funciona com l\'accés a Internet, però pagues per liquiditat i encaminament en lloc d\'amplada de banda.\n\nAquesta configuració inclou alguns costos únics."; -"wallet__receive_liquidity__text_additional" = "El teu saldo de despesa utilitza Lightning Network per fer els teus pagaments més barats, ràpids i privats.\n\nAixò funciona com l\'accés a Internet, però pagues per liquiditat i encaminament en lloc d\'amplada de banda.\n\nBitkit necessita augmentar la capacitat de recepció del teu saldo de despesa per processar aquest pagament."; -"wallet__receive_liquidity__label" = "Liquiditat del saldo de despesa"; -"wallet__receive_liquidity__label_additional" = "Liquiditat addicional del saldo de despesa"; "wallet__receive_cjit_error" = "Transacció fallida"; -"wallet__receive_cjit_error_max__title" = "Capacitat de recepció màxima"; -"wallet__receive_cjit_error_max__description" = "El màxim que pots rebre al teu saldo de despesa ara mateix és ₿ {amount}."; -"wallet__receive_cjit_error_node_capacity__title" = "Capacitat addicional no disponible"; -"wallet__receive_cjit_error_node_capacity__description" = "Bitkit no pot afegir més capacitat de despesa a aquesta cartera ara mateix."; -"wallet__receive_will" = "Rebràs"; "wallet__minimum" = "MÍNIM"; "wallet__activity" = "Activitat"; "wallet__activity_show_all" = "Mostra Tota l\'Activitat"; @@ -1480,7 +1405,7 @@ "subscriptions__name_placeholder" = "Nom de la subscripció"; "subscriptions__ongoing" = "En curs"; "subscriptions__overview" = "Resum"; -"subscriptions__payment_due_description" = "Obre Bitkit per revisar un pagament de subscripció."; +"subscriptions__payment_due_description" = "Obre Bitkit per revisar el pagament"; "subscriptions__payment_due_title" = "Pagament de subscripció pendent"; "subscriptions__payments" = "Pagaments"; "subscriptions__per_day" = "per dia"; @@ -1635,9 +1560,6 @@ "wallet__onchain_broadcast_rejected_note" = "És possible que aquest pagament hagi arribat igualment a la xarxa. No el tornis a enviar. {reason}"; "wallet__onchain_broadcast_unconfirmed" = "Difusió no confirmada"; "wallet__onchain_broadcast_unconfirmed_note" = "És possible que el pagament s'hagi enviat. No el tornis a enviar."; -"wallet__onchain_funding_rejected" = "La transacció de finançament s'ha rebutjat. No intentis tornar a finançar aquesta comanda."; -"wallet__onchain_funding_unknown" = "És possible que la transacció de finançament s'hagi enviat. No intentis tornar a finançar aquesta comanda."; -"wallet__onchain_funding_followup_failed" = "El finançament s'ha enviat, però no s'ha pogut desar el seguiment local de la comanda. No tornis a finançar aquesta comanda."; "wallet__onchain_retry_original" = "Reintentar el pagament original"; "wallet__onchain_retry_fee" = "Taxa de tarifa (sat/vB)"; "wallet__onchain_retry_note" = "Reintentar {amount} sats a {address} amb les mateixes monedes originals. L'import del destinatari no canvia. Introdueix la taxa de tarifa de xarxa en sat/vB. Això no pot crear un pagament separat."; diff --git a/Bitkit/Resources/Localization/cs.lproj/Localizable.strings b/Bitkit/Resources/Localization/cs.lproj/Localizable.strings index d1e05620b..7fd94e491 100644 --- a/Bitkit/Resources/Localization/cs.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/cs.lproj/Localizable.strings @@ -4,8 +4,8 @@ "cards__discount__description" = "Dostupný zůstatek"; "cards__quickpay__title" = "QuickPay"; "cards__quickpay__description" = "Naskenuj a zaplať"; -"cards__notifications__description_alt" = "Když je Bitkit zavřený"; -"cards__notifications__title" = "Přijímat platby"; +"cards__notifications__description_alt" = "Připomínky plateb"; +"cards__notifications__title" = "Oznámení"; "cards__invite__title" = "Pozvat"; "cards__invite__description" = "Sdílet Bitkit"; "cards__lightning__title" = "Utratit"; @@ -151,9 +151,7 @@ "common__learn_more" = "Další informace"; "common__understood" = "Rozumím"; "common__connect" = "Připojit"; -"common__min" = "Min"; "common__max" = "Max"; -"common__default" = "Výchozí"; "common__preview" = "Náhled"; "common__show_all" = "Zobrazit vše"; "common__show_details" = "Zobrazit podrobnosti"; @@ -185,7 +183,6 @@ "fee__slow__range" = "+/- 1-2 hodiny"; "fee__slow__shortRange" = "1-2h"; "fee__slow__longRange" = "± 1-2 hodiny"; -"fee__minimum__title" = "Minimum"; "fee__minimum__longTitle" = "Minimum"; "fee__minimum__description" = "+2 hodiny"; "fee__minimum__shortRange" = "+2h"; @@ -198,74 +195,37 @@ "fee__custom__range" = "Odvíjí se od poplatku"; "fee__custom__shortRange" = "Odvíjí se od poplatku"; "fee__custom__longRange" = "Odvíjí se od poplatku"; -"lightning__transfer_intro__title" = "Disponibilní\nZůstatek"; -"lightning__transfer_intro__text" = "Navyšte svůj disponibilní zůstatek a užívejte si okamžité a levné transakce s přáteli, rodinou a obchodníky."; -"lightning__transfer_intro__button" = "Začít"; "lightning__funding__nav_title" = "Dostupný zůstatek"; "lightning__funding__title" = "Navyšte svůj disponibilní zůstatek"; -"lightning__funding__text" = "Nejjednodušší možností je použít úspory z Bitkitu nebo poslat bitcoin z jiné peněženky."; -"lightning__funding__text_blocked" = "Bitkit v současné době neposkytuje služby lightning ve vaší zemi, ale stále se můžete připojit k jiným uzlům přímo."; -"lightning__funding__text_blocked_cjit" = "Bitkit v současné době neposkytuje služby lightning ve vaší zemi, ale stále se můžete připojit k jiným uzlům přímo."; "lightning__funding__button1" = "Převod z úspor"; "lightning__funding__button2" = "Použijte jinou peněženku"; "lightning__funding__button3" = "Manuální nastavení"; -"lightning__funding_advanced__nav_title" = "Dostupný zůstatek"; -"lightning__funding_advanced__title" = "Pokročilé nastavení"; -"lightning__funding_advanced__text" = "Naskenujte QR kód pro nárokování svého LNURL kanálu od jiného LSP nebo si vyberte ruční nastavení."; -"lightning__funding_advanced__button1" = "LNURL kanál QR"; -"lightning__funding_advanced__button2" = "Manuální nastavení"; "lightning__funding_advanced__button_short" = "Pokročilé nastavení"; -"lightning__no_funds__title" = "Žádné dostupné prostředky"; -"lightning__no_funds__description" = "Než budete moci převést prostředky ze svého zůstatku na spořicím účtu, musíte poslat bitcoiny do peněženky Bitkit."; -"lightning__no_funds__fund" = "Nabít peněženku"; "lightning__transfer__nav_title" = "Převést prostředky"; "lightning__transfer__confirm" = "Prosím,\npotvrďte"; "lightning__transfer__custom_fee" = "Vlastní poplatek"; "lightning__transfer__swipe" = "Přejeďte prstem k provedení převodu"; "lightning__transfer_hw__broadcast_uncertain_title" = "Transakce mohla být odeslána"; "lightning__transfer_hw__broadcast_uncertain_description" = "Vaše financující transakce mohla být odeslána do sítě. Než to zkusíte znovu, zkontrolujte prosím svou aktivitu."; -"lightning__transfer_hw__open_connect" = "Otevřít Trezor Connect"; "lightning__transfer_hw__reconnect_error_description" = "Připojte prosím znovu své hardwarové zařízení, zdá se, že je od telefonu odpojené."; "lightning__transfer_hw__reconnect_error_title" = "Znovu připojte hardwarové zařízení"; -"lightning__transfer_hw__sign_title" = "Podepište\nsvým zařízením"; -"lightning__transfer_hw__signed_title" = "Transakce\npodepsána"; "lightning__transfer_to_savings_button" = "Převést do úspor"; "lightning__transfer_to_spending_button" = "Převést do útraty"; -"lightning__spending_intro__title" = "Převést\ndo výdajů"; -"lightning__spending_intro__text" = "Převeďte peníze na svůj disponibilní zůstatek a využívejte okamžité a levné transakce s přáteli, rodinou a obchodníky."; -"lightning__spending_intro__button" = "Začít"; "lightning__transfer_failed__nav_title" = "Selhání převodu"; "lightning__transfer_failed__title" = "Převod\nneúspěšný"; "lightning__transfer_failed__text" = "Omlouváme se, ale Bitkit se nepodařilo nastavit váš disponibilní zůstatek nebo dokončit převod. Zkuste to prosím znovu."; "lightning__transfer_failed__cancel" = "Zkontrolovat chybu"; "lightning__transfer_failed__continue" = "Zkusit znovu"; -"lightning__spending_amount__title" = "Převést\ndo výdajů"; "lightning__spending_amount__quarter" = "25%"; "lightning__spending_amount__error_min__title" = "Minimální zůstatek úspor"; "lightning__spending_amount__error_min__description" = "K vytvoření disponibilního zůstatku je zapotřebí minimálně ₿ {amount}."; -"lightning__spending_amount__error_balance__description" = "Vaše úspory nepokryjí tento převod a jeho poplatky. Zkuste menší částku."; -"lightning__spending_amount__error_balance__title" = "Nedostatečné úspory"; "lightning__spending_amount__error_max__title" = "Maximální disponibilní zůstatek"; "lightning__spending_amount__error_max__description" = "Částka, kterou můžete převést na svůj disponibilní zůstatek, je v současné době omezena na ₿ {amount}."; "lightning__spending_amount__error_max__description_zero" = "Váš převod do disponibilního zůstatku je omezen z důvodu pravidel likvidity. Podrobnosti naleznete v Centru nápovědy."; "lightning__spending_confirm__network_fee" = "Síťové poplatky"; "lightning__spending_confirm__lsp_fee" = "Poplatky za služby"; "lightning__spending_confirm__amount" = "Do výdajů"; -"lightning__spending_confirm__background_setup" = "Nastavit na pozadí"; "lightning__spending_confirm__total" = "Celkem"; -"lightning__spending_confirm__default" = "Použití výchozích nastavení"; -"lightning__spending_confirm__fees_changed_title" = "Poplatky se změnily"; -"lightning__spending_confirm__fees_changed_service" = "Poplatky jsou nyní, po vytvoření objednávky, o {amount} vyšší. Pro potvrzení přejeďte znovu."; -"lightning__spending_confirm__fees_changed_network" = "Poplatky jsou o {amount} vyšší, protože se během zobrazení této obrazovky změnily sazby poplatků nebo zůstatek vašich úspor. Pro potvrzení přejeďte znovu."; -"lightning__spending_advanced__title" = "Příjímací\nkapacita"; -"lightning__spending_advanced__fee" = "Poplatek za likviditu"; -"lightning__spending_advanced__error_balance__description" = "Vaše úspory nepokryjí poplatek za likviditu pro tuto kapacitu příjmu. Zvolte menší částku."; -"lightning__spending_advanced__error_balance__title" = "Nedostatek prostředků"; -"lightning__spending_advanced__error_max__title" = "Maximální kapacita příjmu"; -"lightning__spending_advanced__error_max__description" = "Kapacita příjmu je aktuálně omezena na ₿ {amount}."; -"lightning__liquidity__title" = "Likvidita\n& routing"; -"lightning__liquidity__text" = "Váš zůstatek na účtu využívá síť lightning network, aby byly vaše platby levnější, rychlejší a soukromější.\n\nFunguje to jako přístup k internetu, ale místo za šířku pásma platíte za likviditu a směrování.\n\nToto nastavení zahrnuje některé jednorázové náklady."; -"lightning__liquidity__label" = "Likvidita disponibilního zůstatku"; "lightning__savings_intro__title" = "Převést\ndo úspor"; "lightning__savings_intro__text" = "Převeďte si disponibilní zůstatek na spoření, abyste si Bitcoiny uložili na delší dobu a zajistili si budoucnost."; "lightning__savings_intro__button" = "Začít"; @@ -289,9 +249,6 @@ "lightning__savings_interrupted__nav_title" = "Převod\npřerušen"; "lightning__savings_interrupted__title" = "Ponechte Bitkit\notevřený"; "lightning__savings_interrupted__text" = "Prostředky dosud nebyly převedeny. Bitkit se pokusí převod iniciovat během následujících 30 minut. Nechte prosím aplikaci otevřenou."; -"lightning__transfer_success__nav_title" = "Přenos úspěšný"; -"lightning__transfer_success__title_spending" = "Prostředky převedeny\ndo výdajů"; -"lightning__transfer_success__text_spending" = "Vaše prostředky byly převedeny a disponibilní zůstatek je připraven k použití."; "lightning__transfer_success__title_savings" = "Prostředky převedeny\ndo úspor"; "lightning__transfer_success__text_savings" = "Vaše prostředky byly převedeny. Tyto prostředky můžete okamžitě použít."; "lightning__external__nav_title" = "Připojení lightning"; @@ -314,12 +271,6 @@ "lightning__spending_label" = "Disponibilní zůstatek"; "lightning__receiving_label" = "Kapacita příjmu"; "lightning__setting_up_header" = "V převodu"; -"lightning__setting_up_text" = "Počkejte prosím, probíhá převod finančních prostředků. To by mělo trvat ±10 minut."; -"lightning__setting_up_step1" = "Zpracování platby"; -"lightning__setting_up_step2" = "Platba úspěšně proběhla"; -"lightning__setting_up_step3" = "Ve frontě na otevření"; -"lightning__setting_up_step4" = "Otevírání spojení"; -"lightning__setting_up_button" = "Pokračujte v používání Bitkit"; "lightning__node_info" = "Lightning uzel"; "lightning__node_id" = "ID uzlu LDK"; "lightning__node_disconnected" = "Odpojeno"; @@ -391,9 +342,9 @@ "lightning__force_init_msg" = "Vaše prostředky budou dostupné do ±14 dnů."; "lightning__force_failed_title" = "Vynucení převodu selhalo."; "lightning__force_failed_msg" = "Nelze převést vaše prostředky zpět do úspor. Zkuste to prosím znovu."; -"lightning__force_channels_skipped" = "Některá připojení se nepodařilo zavřít."; "lightning__channel_opened_title" = "Disponibilní zůstatek připraven"; "lightning__channel_opened_msg" = "Nyní můžete platit komukoli, kdekoli a okamžitě."; +"lightning__funding__button_hw" = "Převod z HW peněženky"; "lightning__order_given_up_title" = "Nastavení okamžitých plateb se nezdařilo"; "lightning__order_given_up_msg" = "Proces nastavení vypršel. Zkuste to prosím znovu."; "lightning__order_expired_title" = "Nastavení okamžitých plateb se nezdařilo"; @@ -412,6 +363,9 @@ "lightning__order_state__open" = "Spojení otevřeno"; "lightning__order_state__inactive" = "Připojení neaktivní"; "lightning__peer_disconnected" = "Peer odpojen."; +"lightning__transfer_unavailable__nav_title" = "Převod není dostupný"; +"lightning__transfer_unavailable__text" = "Automatické převody na zůstatek útrat již nejsou k dispozici. Použijte vlastní uzel nebo si nastavte připojení u jiného poskytovatele."; +"lightning__transfer_unavailable__title" = "Vyžadováno ruční\nnastavení"; "lightning__wait_text_top" = "Prosím vyčkejte, až se Bitkit připojí k platební síti (±10 sekund)."; "lightning__wait_text_bottom" = "Připojování & synchronizace..."; "onboarding__tos_header" = "Bitkit\npodmínky používání"; @@ -433,7 +387,6 @@ "onboarding__slide1_header" = "Okamžité\nplatby"; "onboarding__slide1_text" = "Utrácejte bitcoiny rychleji než kdykoli předtím. Užívejte si okamžité a levné platby s přáteli, rodinou a obchodníky."; "onboarding__slide1_text_alt" = "Užívejte si okamžité a levné platby s přáteli, rodinou a obchodníky na Lightning Network."; -"onboarding__slide1_note" = "*Bitkit v současné době neposkytuje služby lightning ve vaší zemi, ale stále se můžete připojit k jiným uzlům."; "onboarding__slide2_header" = "Bitcoineři,\nbez hranic"; "onboarding__slide2_text" = "Převezměte kontrolu nad svým digitálním životem pomocí přenosných profilů a kontaktů k placení."; "onboarding__slide3_header" = "Soukromí\nnení zločin"; @@ -556,8 +509,6 @@ "other__lnurl_withdr_error_minmax" = "Nesprávné parametry pro LNURL výběr, min/max nejsou správně nastaveny."; "other__lnurl_withdr_success_title" = "Výběr vyžádán"; "other__lnurl_withdr_success_msg" = "Váš výběr byl úspěšně vyžádán. Čekání na platbu."; -"other__notification_registration_failed_description" = "Bitkitu se nepodařilo zaregistrovat pro push notifikace."; -"other__notification_registration_failed_title" = "Registrace notifikací selhala"; "other__phone_settings" = "Otevřít nastavení telefonu"; "other__bt_error_retrieve" = "Přenos se nezdařil"; "other__bt_error_connect" = "V tuto chvíli nelze přidat uzel LSP jako protistranu."; @@ -573,14 +524,7 @@ "other__transfer_notification__body" = "Otevřete Bitkit pro dokončení přenosu"; "other__scan__error__generic" = "Bitkit není schopen přečíst zadaná data."; "other__scan__error__expired" = "Platnost lightning faktury vypršela."; -"other__gift__claimed__text" = "Je nám líto, přišli jste pozdě! Všechny dary pro tento kód již byly vyzvednuty."; -"other__gift__claiming__text" = "Uplatnění dárkového kódu Bitkit..."; -"other__gift__claiming__title" = "Uplatnění daru"; -"other__gift__error__title" = "Chyba dárkového kódu"; -"other__gift__error__text" = "Bitkit nemohl nárokovat prostředky. Zkuste to prosím znovu později nebo nás kontaktujte."; "other__gift__error__button" = "OK"; -"other__gift__used__title" = "Použitý kód"; -"other__gift__used__text" = "Tento dárkový kód BitKitu již byl použit a prostředky byly vyplaceny."; "other__gift__used__button" = "OK"; "other__shop__intro__title" = "Nakoupit"; "other__shop__intro__description" = "Zařiď si život podle Bitcoin standardu. Utrácej své Bitcoiny za digitální dárkové karty, eSIMky, dobíjení kreditu a mnohem víc."; @@ -763,6 +707,7 @@ "pubky_auth__homeserver" = "Homeserver"; "pubky_auth__signup_description" = "Vytvořte si na tomto homeserveru novou identitu Pubky. Pokračujte, jen pokud mu důvěřujete."; "pubky_auth__approval_failed" = "Autorizace se nezdařila"; +"wallet__onchain_abandon_transfer_note" = "Převody na útratový zůstatek už nejsou dostupné. Bitkit nejprve zkontroluje síť a pak vrátí mince tohoto převodu do vaší peněženky, aby se původní převod nikdy nepotvrdil. Stojí to síťový poplatek."; "watch_only_accounts__active_section" = "Aktivní účty"; "watch_only_accounts__copy_xpub" = "Kopírovat xpub"; "watch_only_accounts__description" = "Každý schválený server Paykit získá samostatný bitcoinový účet. Vypnutím sledování účet uvolníte, aniž byste smazali historii jeho peněženky."; @@ -839,21 +784,17 @@ "settings__widgets__reset_widgets_dialog_title" = "Resetovat widgety?"; "settings__widgets__reset_widgets_dialog_description" = "Opravdu chcete resetovat widgety? Zobrazí se výchozí sada widgetů s výchozím nastavením."; "settings__widgets__reset_suggestions" = "Resetovat karty návrhů"; -"settings__notifications__nav_title" = "Platby na pozadí"; -"settings__notifications__intro__title" = "Přijímat platby\npasivně"; -"settings__notifications__intro__text" = "Zapněte oznámení, abyste mohli přijímat platby, i když je vaše aplikace Bitkit zavřená."; +"settings__notifications__nav_title" = "Oznámení"; +"settings__notifications__intro__title" = "Buďte\nv obraze"; +"settings__notifications__intro__text" = "Zapněte oznámení a dostávejte připomínky předplatných a žádostí o platbu."; "settings__notifications__intro__button" = "Povolit"; -"settings__notifications__settings__toggle" = "Přijímat platby, když je Bitkit zavřený"; -"settings__notifications__settings__enabled" = "Platby na pozadí jsou povoleny. Můžete přijímat prostředky, i když je aplikace zavřená (pokud je vaše zařízení připojeno k internetu)."; -"settings__notifications__settings__disabled" = "Povolte platby na pozadí pro příjem prostředků, i když je aplikace zavřená (pokud je vaše zařízení připojeno k internetu)."; -"settings__notifications__settings__denied" = "Platby na pozadí jsou zakázány, protože jste odmítli oznámení."; -"settings__notifications__settings__preview__title" = "Platba přijata"; -"settings__notifications__settings__preview__text" = "Otevřete Bitkit pro zobrazení podrobností"; +"settings__notifications__settings__toggle" = "Povolit oznámení"; +"settings__notifications__settings__enabled" = "Oznámení jsou zapnutá. Bitkit vám může připomínat předplatná a žádosti o platbu."; +"settings__notifications__settings__disabled" = "Zapněte oznámení a dostávejte připomínky předplatných a žádostí o platbu."; +"settings__notifications__settings__denied" = "Oznámení jsou vypnutá, protože jste je zamítli."; "settings__notifications__settings__preview__time" = "před 3m"; -"settings__notifications__settings__privacy__label" = "Soukromí"; -"settings__notifications__settings__privacy__text" = "Zahrnout částku v oznámeních"; "settings__notifications__settings__notifications__label" = "Oznámení"; -"settings__notifications__settings__notifications__text" = "Pro povolení plateb na pozadí povolte prosím oznámení v iOS nastavení Bitkitu."; +"settings__notifications__settings__notifications__text" = "Pro připomínky povolte prosím oznámení v iOS nastavení Bitkitu."; "settings__notifications__settings__button__enabled" = "Přizpůsobit v {platform} nastavení Bitkitu"; "settings__notifications__settings__button__disabled" = "Povolit v {platform} nastavení Bitkitu"; "settings__quickpay__nav_title" = "QuickPay"; @@ -1394,27 +1335,11 @@ "wallet__receive_note_placeholder" = "Nepovinná poznámka pro plátce"; "wallet__receive_show_qr" = "Zobrazit QR kód"; "wallet__receive_text_lnfunds" = "Chcete obdržet prostředky lightning?"; -"wallet__receive_background_setup_text" = "Povolte nastavení na pozadí pro bezpečné ukončení Bitkitu, zatímco se váš zůstatek konfiguruje."; -"wallet__receive_background_setup_toggle" = "Povolit nastavení na pozadí"; "wallet__receive_spending" = "Přijmout Lightning prostředky"; "wallet__receive_spending_text" = "Užívejte si okamžité a levné bitcoinové platby v síti Lightning."; "wallet__receive_spending_title" = "Přijmout na disponibilní zůstatek"; -"wallet__receive_connect_initial" = "Chcete-li nastavit disponibilní zůstatek, bude odečten síťový poplatek {networkFee} a {serviceFee} poplatek poskytovateli služeb."; -"wallet__receive_connect_additional" = "Chcete-li získat více okamžitých bitcoinů, musí Bitkit navýšit vaši likviditu. Od vámi zadané částky bude odečten síťový poplatek {networkFee} a {serviceFee} poplatek poskytovatele služeb ."; -"wallet__receive_liquidity__nav_title" = "Nastavení disponibilního zůstatku"; -"wallet__receive_liquidity__nav_title_additional" = "Zvýšení přijímací kapacity"; -"wallet__receive_liquidity__invalid_quote" = "Bitkit obdržel neplatnou nabídku likvidity. Zkuste to prosím znovu."; -"wallet__receive_liquidity__text" = "Váš zůstatek na účtu využívá síť lightning network, aby byly vaše platby levnější, rychlejší a soukromější.\n\nFunguje to jako přístup k internetu, ale místo za šířku pásma platíte za likviditu a směrování.\n\nToto nastavení zahrnuje některé jednorázové náklady."; -"wallet__receive_liquidity__text_additional" = "Váš zůstatek na účtu využívá síť lightning network, aby byly vaše platby levnější, rychlejší a soukromější.\n\nFunguje to jako přístup k internetu, ale místo za šířku pásma platíte za likviditu a směrování.\n\nBitkit potřebuje zvýšit přijímací kapacitu vašeho výdajového zůstatku, aby mohl tuto platbu zpracovat."; -"wallet__receive_liquidity__label" = "Likvidita disponibilního zůstatku"; -"wallet__receive_liquidity__label_additional" = "Dodatečná likvidita disponibilního zůstatku"; "wallet__receive_cjit_error" = "Transakce selhala"; "wallet__receive_cjit_error_msg" = "Nepodařilo se odeslat prostředky na váš disponibilní účet."; -"wallet__receive_cjit_error_max__title" = "Maximální kapacita příjmu"; -"wallet__receive_cjit_error_max__description" = "Maximum, které teď můžete přijmout do disponibilního zůstatku, je ₿ {amount}."; -"wallet__receive_cjit_error_node_capacity__title" = "Dodatečná kapacita není k dispozici"; -"wallet__receive_cjit_error_node_capacity__description" = "Bitkit teď nemůže do této peněženky přidat další disponibilní kapacitu."; -"wallet__receive_will" = "Obdržíte"; "wallet__receive_ldk_init" = "Inicializace disponibilního zůstatku..."; "wallet__minimum" = "MINIMUM"; "wallet__activity" = "Aktivita"; @@ -1603,7 +1528,7 @@ "subscriptions__name_placeholder" = "Název předplatného"; "subscriptions__ongoing" = "Průběžné"; "subscriptions__overview" = "Přehled"; -"subscriptions__payment_due_description" = "Otevřete Bitkit a zkontrolujte platbu předplatného."; +"subscriptions__payment_due_description" = "Otevřete Bitkit a zkontrolujte platbu"; "subscriptions__payment_due_title" = "Splatná platba předplatného"; "subscriptions__payments" = "Platby"; "subscriptions__per_day" = "za den"; @@ -1775,9 +1700,6 @@ "wallet__onchain_broadcast_rejected_note" = "Tato platba se mohla do sítě přesto dostat. Neposílejte ji znovu. {reason}"; "wallet__onchain_broadcast_unconfirmed" = "Odeslání do sítě nepotvrzeno"; "wallet__onchain_broadcast_unconfirmed_note" = "Platba mohla být odeslána. Neposílejte ji znovu."; -"wallet__onchain_funding_rejected" = "Financující transakce byla odmítnuta. Nepokoušejte se tuto objednávku financovat znovu."; -"wallet__onchain_funding_unknown" = "Financující transakce mohla být odeslána. Nepokoušejte se tuto objednávku financovat znovu."; -"wallet__onchain_funding_followup_failed" = "Financování bylo odesláno, ale místní sledování objednávky se nepodařilo uložit. Tuto objednávku znovu nefinancujte."; "wallet__onchain_retry_original" = "Zopakovat původní platbu"; "wallet__onchain_retry_fee" = "Sazba poplatku (sat/vB)"; "wallet__onchain_retry_note" = "Zopakovat {amount} sats na {address} se stejnými původními mincemi. Částka pro příjemce zůstane beze změny. Zadejte sazbu síťového poplatku v sat/vB. Tímto nelze vytvořit samostatnou platbu."; diff --git a/Bitkit/Resources/Localization/de.lproj/Localizable.strings b/Bitkit/Resources/Localization/de.lproj/Localizable.strings index 31df19fb4..6cc768ef3 100644 --- a/Bitkit/Resources/Localization/de.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/de.lproj/Localizable.strings @@ -4,8 +4,8 @@ "cards__discount__description" = "Spending Balance"; "cards__quickpay__title" = "QuickPay"; "cards__quickpay__description" = "Scanne und zahle"; -"cards__notifications__description_alt" = "Wenn Bitkit geschlossen ist"; -"cards__notifications__title" = "Zahlungen empfangen"; +"cards__notifications__description_alt" = "Zahlungserinnerungen"; +"cards__notifications__title" = "Benachrichtigungen"; "cards__invite__title" = "Einladen"; "cards__invite__description" = "Teile Bitkit"; "cards__lightning__title" = "Ausgeben"; @@ -151,9 +151,7 @@ "common__learn_more" = "Mehr erfahren"; "common__understood" = "Verstanden"; "common__connect" = "Verbinden"; -"common__min" = "Min"; "common__max" = "Max"; -"common__default" = "Standard"; "common__preview" = "Vorschau"; "common__show_all" = "Alle anzeigen"; "common__show_details" = "Details anzeigen"; @@ -185,7 +183,6 @@ "fee__slow__range" = "±1-2 Stunden"; "fee__slow__shortRange" = "1-2h"; "fee__slow__longRange" = "± 1-2 Stunden"; -"fee__minimum__title" = "Minimum"; "fee__minimum__longTitle" = "Minimum"; "fee__minimum__description" = "+2 Stunden"; "fee__minimum__shortRange" = "+2h"; @@ -198,72 +195,35 @@ "fee__custom__range" = "Abhängig von der Gebühr"; "fee__custom__shortRange" = "Abhängig von der Gebühr"; "fee__custom__longRange" = "Abhängig von der Gebühr"; -"lightning__transfer_intro__title" = "Spending\nBalance"; -"lightning__transfer_intro__text" = "Fülle dein Guthaben auf, um sofortige und günstige Transaktionen mit Freunden, Familie und Händlern zu genießen."; -"lightning__transfer_intro__button" = "Los geht\'s"; "lightning__funding__nav_title" = "Spending Balance"; "lightning__funding__title" = "Fund your spending balance"; -"lightning__funding__text" = "Am einfachsten ist es, deine Bitkit-Ersparnisse zu verwenden oder Bitcoin von einer anderen Wallet zu senden."; -"lightning__funding__text_blocked" = "Bitkit bietet derzeit keine Lightning-Dienste in deinem Land an, aber du kannst dich trotzdem direkt mit anderen Knotenpunkten verbinden."; -"lightning__funding__text_blocked_cjit" = "Bitkit bietet derzeit keine Lightning-Dienste in deinem Land an, aber du kannst dich trotzdem direkt mit anderen Knotenpunkten verbinden."; "lightning__funding__button1" = "Von Sparkonto übertragen"; "lightning__funding__button2" = "Anderes Wallet verwenden"; "lightning__funding__button3" = "Manuelle Einrichtung"; -"lightning__funding_advanced__nav_title" = "Ausgabenkonto"; -"lightning__funding_advanced__title" = "Fortgeschrittenesetup"; -"lightning__funding_advanced__text" = "Scannen Sie einen QR-Code, um Ihren LNURL-Kanal von einem anderen LSP zu beanspruchen, oder wählen Sie die manuelle Einrichtung."; -"lightning__funding_advanced__button1" = "LNURL Channel QR"; -"lightning__funding_advanced__button2" = "Manuelle Einrichtung"; "lightning__funding_advanced__button_short" = "Fortgeschrittenes Setup"; -"lightning__no_funds__title" = "Keine verfügbaren Mittel"; -"lightning__no_funds__description" = "Bevor du Geld von deinem Sparkonto übertragen kannst, musst du Bitcoin zu deiner Bitkit Wallet senden."; -"lightning__no_funds__fund" = "Wallet aufladen"; "lightning__transfer__nav_title" = "Übertrage Guthaben"; "lightning__transfer__confirm" = "Bitte\nbestätige"; "lightning__transfer__custom_fee" = "Benutzerdefiniert fee"; "lightning__transfer__swipe" = "Swipe zum Übertragen"; "lightning__transfer_hw__broadcast_uncertain_title" = "Transaktion wurde evtl. gesendet"; "lightning__transfer_hw__broadcast_uncertain_description" = "Deine Finanzierungstransaktion wurde möglicherweise bereits übertragen. Bitte überprüfe deine Aktivität, bevor du es erneut versuchst."; -"lightning__transfer_hw__open_connect" = "Trezor Connect öffnen"; "lightning__transfer_hw__reconnect_error_description" = "Bitte verbinde dein Hardware-Gerät erneut, es scheint von deinem Telefon getrennt zu sein."; "lightning__transfer_hw__reconnect_error_title" = "Hardware-Gerät neu verbinden"; -"lightning__transfer_hw__sign_title" = "Mit deinem\nGerät signieren"; -"lightning__transfer_hw__signed_title" = "Transaktion\nsigniert"; "lightning__transfer_to_savings_button" = "Zu Ersparnissen transferieren"; "lightning__transfer_to_spending_button" = "Zu Ausgaben transferieren"; -"lightning__spending_intro__title" = "zu den Ausgaben"; -"lightning__spending_intro__text" = "Überweisen Sie Geld auf Ihr Guthaben, um sofortige und günstige Transaktionen mit Freunden, Familie und Händlern zu tätigen."; -"lightning__spending_intro__button" = "Los geht\'s"; "lightning__transfer_failed__nav_title" = "Übertragung fehlgeschlagen"; "lightning__transfer_failed__title" = "Übertragung\nfehlgeschlagen"; "lightning__transfer_failed__text" = "Entschuldigung, Bitkit konnte dein Guthaben nicht einrichten oder deine Übertragung nicht abschließen. Bitte versuche es erneut."; "lightning__transfer_failed__cancel" = "Fehler überprüfen"; "lightning__transfer_failed__continue" = "Erneut versuchen"; -"lightning__spending_amount__title" = "zu den Ausgaben"; "lightning__spending_amount__quarter" = "25%"; "lightning__spending_amount__error_min__title" = "Minimales Sparguthaben"; -"lightning__spending_amount__error_balance__description" = "Dein Sparkonto reicht nicht aus, um diese Übertragung und ihre Gebühren zu decken. Versuche einen kleineren Betrag."; -"lightning__spending_amount__error_balance__title" = "Unzureichendes Sparguthaben"; "lightning__spending_amount__error_max__title" = "Maximaler Ausgabensaldo"; "lightning__spending_amount__error_max__description" = "Der Betrag, den Sie auf Ihr Ausgabenkonto übertragen können, ist derzeit auf ₿ {amount} begrenzt."; "lightning__spending_confirm__network_fee" = "Netzgebühren"; "lightning__spending_confirm__lsp_fee" = "Servicegebühren"; "lightning__spending_confirm__amount" = "Zu den Ausgaben"; -"lightning__spending_confirm__background_setup" = "Im Hintergrund einrichten"; "lightning__spending_confirm__total" = "Total"; -"lightning__spending_confirm__default" = "Standardwerte verwenden"; -"lightning__spending_confirm__fees_changed_title" = "Gebühren geändert"; -"lightning__spending_confirm__fees_changed_service" = "Die Gebühren sind um {amount} höher, da deine Bestellung jetzt erstellt ist. Wische erneut, um zu bestätigen."; -"lightning__spending_confirm__fees_changed_network" = "Die Gebühren sind um {amount} höher, weil sich die Gebührensätze oder dein Sparguthaben geändert haben, während du auf diesem Bildschirm warst. Wische erneut, um zu bestätigen."; -"lightning__spending_advanced__title" = "Empfang von\nKapazität"; -"lightning__spending_advanced__fee" = "Liquiditätsgebühr"; -"lightning__spending_advanced__error_balance__description" = "Dein Sparkonto reicht nicht aus, um die Liquiditätsgebühr für diese Empfangskapazität zu decken. Wähle einen kleineren Betrag."; -"lightning__spending_advanced__error_balance__title" = "Nicht genügend Guthaben"; -"lightning__spending_advanced__error_max__title" = "Maximale Empfangskapazität"; -"lightning__spending_advanced__error_max__description" = "Die Empfangskapazität ist derzeit auf ₿ {amount} begrenzt."; -"lightning__liquidity__title" = "Liquidität\n& Routenplanung"; -"lightning__liquidity__text" = "Your Spending Balance nutzt das Lightning Network, um Ihre Zahlungen günstiger, schneller und privater zu machen.\n\nDies funktioniert wie ein Internetzugang, aber Sie zahlen für Liquidität und Routing statt für Bandbreite.\n\nDiese Einrichtung umfasst einige einmalige Kosten."; -"lightning__liquidity__label" = "Ausgabensaldo Liquidität"; "lightning__savings_intro__title" = "Übertragung\nzu den Einsparungen"; "lightning__savings_intro__text" = "Übertragen Sie Ihr Ausgabensaldo auf Ihr Sparguthaben, um Ihre Bitcoin langfristig zu speichern und Ihre Zukunft zu sichern."; "lightning__savings_intro__button" = "Los geht\'s"; @@ -287,9 +247,6 @@ "lightning__savings_interrupted__nav_title" = "Transfer\nunterbrochen"; "lightning__savings_interrupted__title" = "Bitkit behalten\nopen"; "lightning__savings_interrupted__text" = "Die Gelder wurden noch nicht übertragen. Bitkit wird versuchen, die Übertragung in den nächsten 30 Minuten zu initiieren. Bitte halte deine App offen."; -"lightning__transfer_success__nav_title" = "Übertragung erfolgreich"; -"lightning__transfer_success__title_spending" = "Bewegte Mittel\nzu den Ausgaben"; -"lightning__transfer_success__text_spending" = "Deine Gelder wurden übertragen und dein Ausgabenguthaben ist einsatzbereit."; "lightning__transfer_success__title_savings" = "Bewegte Mittel\nzu den Einsparungen"; "lightning__transfer_success__text_savings" = "Ihr Geld wurde überwiesen. Sie können diese Mittel sofort verwenden."; "lightning__external__nav_title" = "Lightning-Verbindung"; @@ -312,12 +269,6 @@ "lightning__spending_label" = "Ausgabenguthaben"; "lightning__receiving_label" = "Empfangskapazität"; "lightning__setting_up_header" = "In Übertragung"; -"lightning__setting_up_text" = "Bitte warte, deine Übertragung wird bearbeitet. Dies sollte ±10 Minuten dauern."; -"lightning__setting_up_step1" = "Zahlung wird verarbeitet"; -"lightning__setting_up_step2" = "Zahlung erfolgreich"; -"lightning__setting_up_step3" = "In Warteschlange für Öffnung"; -"lightning__setting_up_step4" = "Verbindung wird geöffnet"; -"lightning__setting_up_button" = "Bitkit weiter verwenden"; "lightning__node_info" = "Lightning Node"; "lightning__node_id" = "LDK Node ID"; "lightning__node_disconnected" = "getrennt"; @@ -389,9 +340,9 @@ "lightning__force_init_msg" = "Dein Guthaben wird in ±14 Tagen zur Verfügung stehen."; "lightning__force_failed_title" = "Forcierte Übertragung fehlgeschlagen"; "lightning__force_failed_msg" = "Unable to transfer your funds back to savings. Please try again."; -"lightning__force_channels_skipped" = "Einige Verbindungen konnten nicht geschlossen werden."; "lightning__channel_opened_title" = "Spending Balance Ready"; "lightning__channel_opened_msg" = "Du kannst jetzt jedermann, überall, instant bezahlen."; +"lightning__funding__button_hw" = "Von HW-Wallet übertragen"; "lightning__order_given_up_title" = "Einrichtung der Instant-Zahlungen fehlgeschlagen"; "lightning__order_given_up_msg" = "The setup process expired. Please try again."; "lightning__order_expired_title" = "Einrichtung der Instant-Zahlungen fehlgeschlagen"; @@ -410,6 +361,9 @@ "lightning__order_state__open" = "Verbindung offen"; "lightning__order_state__inactive" = "Verbindung inaktiv"; "lightning__peer_disconnected" = "Peer getrennt."; +"lightning__transfer_unavailable__nav_title" = "Übertragung nicht verfügbar"; +"lightning__transfer_unavailable__text" = "Automatische Übertragungen auf dein Ausgabenkonto sind nicht mehr verfügbar. Nutze deinen eigenen Node oder richte eine Verbindung mit einem anderen Anbieter ein."; +"lightning__transfer_unavailable__title" = "Manuelle Einrichtung\nerforderlich"; "lightning__wait_text_top" = "Bitte warte bis sich Bitkit mit dem Zahlungsnetzwerk verbunden hat (±10 Sekunden)."; "lightning__wait_text_bottom" = "Verbinden & Synchronisieren…"; "onboarding__tos_header" = "Bitkit\nNutzungs-bedingungen"; @@ -431,7 +385,6 @@ "onboarding__slide1_header" = "Instant\npayments"; "onboarding__slide1_text" = "Gib bitcoin aus, schneller denn je zuvor. Genieße sofortige und günstige Zahlungen mit Freunden, Familie und Händlern."; "onboarding__slide1_text_alt" = "Genieße sofortige und günstige Zahlungen mit Freunden, Familie und Händlern im Lightning-Netzwerk."; -"onboarding__slide1_note" = "Bitkit bietet derzeit keine Lightning-Dienste in deinem Land an, aber du kannst dich trotzdem direkt mit anderen Knotenpunkten verbinden."; "onboarding__slide2_header" = "Bitcoiners,\nborderless"; "onboarding__slide2_text" = "Übernimm die Kontrolle über dein digitales Leben mit portablen Profilen und Kontakten, die du bezahlen kannst."; "onboarding__slide3_header" = "Privacy is\nnot a crime"; @@ -554,8 +507,6 @@ "other__lnurl_withdr_error_minmax" = "Falsche LNURL-Abhebungsparameter, Min/Max nicht korrekt eingestellt."; "other__lnurl_withdr_success_title" = "Abhebung angefordert"; "other__lnurl_withdr_success_msg" = "Ihre Abhebung wurde erfolgreich beantragt. Ich warte auf die Zahlung."; -"other__notification_registration_failed_description" = "Bitkit konnte sich nicht für Push-Benachrichtigungen registrieren."; -"other__notification_registration_failed_title" = "Benachrichtigungsregistrierung fehlgeschlagen"; "other__phone_settings" = "Telefoneinstellungen öffnen"; "other__bt_error_retrieve" = "Übertragung fehlgeschlagen"; "other__bt_error_connect" = "LSP-Node kann derzeit nicht als Peer hinzugefügt werden."; @@ -571,14 +522,7 @@ "other__transfer_notification__body" = "Öffnen Sie Bitkit, um Ihre Übertragung abzuschließen"; "other__scan__error__generic" = "Bitkit ist nicht in der Lage, die bereitgestellten Daten zu lesen."; "other__scan__error__expired" = "Diese Lightning-Rechnung ist abgelaufen."; -"other__gift__claimed__text" = "Leider kommst du zu spät! Alle Geschenke für diesen Code wurden bereits eingelöst."; -"other__gift__claiming__text" = "Dein Bitkit-Geschenkcode wird eingelöst..."; -"other__gift__claiming__title" = "Geschenk einlösen"; -"other__gift__error__title" = "Rabattcode-Fehler"; -"other__gift__error__text" = "Bitkit konnte die Gelder nicht einlösen. Bitte versuche es später erneut oder kontaktiere uns."; "other__gift__error__button" = "OK"; -"other__gift__used__title" = "Verwendeter Code"; -"other__gift__used__text" = "Dieser Bitkit-Rabattcode wurde bereits verwendet und der Betrag wurde ausbezahlt."; "other__gift__used__button" = "OK"; "other__shop__intro__title" = "Shop"; "other__shop__intro__description" = "Lebe nach dem Bitcoin-Standard. Gib dein Bitcoin für digitale Geschenkkarten, eSIMs, Handy-Aufladungen und mehr aus."; @@ -761,6 +705,7 @@ "pubky_auth__homeserver" = "Homeserver"; "pubky_auth__signup_description" = "Erstelle eine neue Pubky-Identität auf diesem Homeserver. Fahre nur fort, wenn du ihm vertraust."; "pubky_auth__approval_failed" = "Autorisierung fehlgeschlagen"; +"wallet__onchain_abandon_transfer_note" = "Überweisungen auf dein Ausgabenguthaben sind nicht mehr verfügbar. Bitkit prüft zuerst das Netzwerk und schickt die Coins dieser Überweisung dann zurück in deine Wallet, damit die ursprüngliche Überweisung nie bestätigt wird. Dafür fällt eine Netzwerkgebühr an."; "watch_only_accounts__active_section" = "Aktive Konten"; "watch_only_accounts__copy_xpub" = "xpub kopieren"; "watch_only_accounts__description" = "Jeder genehmigte Paykit-Server erhält ein eigenes Bitcoin-Konto. Deaktiviere die Verfolgung, um ein Konto zu entladen, ohne seinen Wallet-Verlauf zu löschen."; @@ -837,21 +782,17 @@ "settings__widgets__reset_widgets_dialog_title" = "Widgets zurücksetzen?"; "settings__widgets__reset_widgets_dialog_description" = "Bist du sicher, dass du die Widgets zurücksetzen möchtest? Die Standard-Widgets mit Standardkonfigurationen werden angezeigt."; "settings__widgets__reset_suggestions" = "Vorschlagskarten zurücksetzen"; -"settings__notifications__nav_title" = "Hintergrundzahlungen"; -"settings__notifications__intro__title" = "Passiv\nbezahlt werden"; -"settings__notifications__intro__text" = "Aktiviere Benachrichtigungen, um Zahlungen zu erhalten, auch wenn Bitkit geschlossen ist."; +"settings__notifications__nav_title" = "Benachrichtigungen"; +"settings__notifications__intro__title" = "Bleib\ninformiert"; +"settings__notifications__intro__text" = "Aktiviere Benachrichtigungen, um an deine Abonnements und Zahlungsanfragen erinnert zu werden."; "settings__notifications__intro__button" = "Aktivieren"; -"settings__notifications__settings__toggle" = "Zahlungen empfangen, wenn Bitkit geschlossen ist"; -"settings__notifications__settings__enabled" = "Hintergrundzahlungen sind aktiviert. Du kannst Gelder auch bei geschlossener App empfangen (wenn dein Gerät mit dem Internet verbunden ist)."; -"settings__notifications__settings__disabled" = "Aktiviere Hintergrundzahlungen, um Gelder auch bei geschlossener App zu empfangen (wenn dein Gerät mit dem Internet verbunden ist)."; -"settings__notifications__settings__denied" = "Hintergrundzahlungen sind deaktiviert, weil du Benachrichtigungen abgelehnt hast."; -"settings__notifications__settings__preview__title" = "Zahlung erhalten"; -"settings__notifications__settings__preview__text" = "Öffne Bitkit für Details"; +"settings__notifications__settings__toggle" = "Benachrichtigungen erlauben"; +"settings__notifications__settings__enabled" = "Benachrichtigungen sind aktiviert. Bitkit kann dich an Abonnements und Zahlungsanfragen erinnern."; +"settings__notifications__settings__disabled" = "Aktiviere Benachrichtigungen, um an deine Abonnements und Zahlungsanfragen erinnert zu werden."; +"settings__notifications__settings__denied" = "Benachrichtigungen sind deaktiviert, weil du sie abgelehnt hast."; "settings__notifications__settings__preview__time" = "vor 3 Min."; -"settings__notifications__settings__privacy__label" = "Privatsphäre"; -"settings__notifications__settings__privacy__text" = "Betrag in Benachrichtigungen anzeigen"; "settings__notifications__settings__notifications__label" = "Benachrichtigungen"; -"settings__notifications__settings__notifications__text" = "Um Hintergrundzahlungen zu aktivieren, aktiviere bitte Benachrichtigungen in den iOS Bitkit-Einstellungen."; +"settings__notifications__settings__notifications__text" = "Um Erinnerungen zu erhalten, aktiviere bitte Benachrichtigungen in den iOS Bitkit-Einstellungen."; "settings__notifications__settings__button__enabled" = "In {platform} Bitkit-Einstellungen anpassen"; "settings__notifications__settings__button__disabled" = "In {platform} Bitkit-Einstellungen aktivieren"; "settings__quickpay__nav_title" = "QuickPay"; @@ -1392,27 +1333,11 @@ "wallet__receive_note_placeholder" = "Optionale Notiz an den Zahler"; "wallet__receive_show_qr" = "QR-Code anzeigen"; "wallet__receive_text_lnfunds" = "Möchtest du Lightning-Gelder empfangen?"; -"wallet__receive_background_setup_text" = "Aktiviere die Hintergrundeinrichtung, um Bitkit sicher zu verlassen, während dein Guthaben konfiguriert wird."; -"wallet__receive_background_setup_toggle" = "Hintergrundeinrichtung aktivieren"; "wallet__receive_spending" = "Auf Ausgabenkonto empfangen"; "wallet__receive_spending_text" = "Genieße sofortige und günstige Bitcoin-Zahlungen im Lightning-Netzwerk."; "wallet__receive_spending_title" = "Auf Ausgabenguthaben empfangen"; -"wallet__receive_connect_initial" = "Um dein Ausgabenguthaben einzurichten, werden eine {networkFee} Netzwerkgebühr und {serviceFee} Dienstanbietergebühr abgezogen."; -"wallet__receive_connect_additional" = "Um mehr sofortiges Bitcoin zu empfangen, muss Bitkit deine Liquidität erhöhen. Eine {networkFee} Netzwerkgebühr und {serviceFee} Dienstanbietergebühr werden vom angegebenen Betrag abgezogen."; -"wallet__receive_liquidity__nav_title" = "Einrichtung des Ausgabensaldos"; -"wallet__receive_liquidity__nav_title_additional" = "Erhöhung der Empfangskapazität"; -"wallet__receive_liquidity__invalid_quote" = "Bitkit hat ein ungültiges Liquiditätsangebot erhalten. Bitte versuche es erneut."; -"wallet__receive_liquidity__text" = "Your Spending Balance nutzt das Lightning Network, um Ihre Zahlungen günstiger, schneller und privater zu machen.\n\nDies funktioniert wie ein Internetzugang, aber Sie zahlen für Liquidität und Routing statt für Bandbreite.\n\nDiese Einrichtung umfasst einige einmalige Kosten."; -"wallet__receive_liquidity__text_additional" = "Your Spending Balance nutzt das Lightning Network, um Ihre Zahlungen günstiger, schneller und privater zu machen.\n\nDies funktioniert wie ein Internetzugang, aber Sie zahlen für Liquidität und Routing statt für Bandbreite.\n\nBitkit muss die Aufnahmekapazität Ihres Guthabens erhöhen, um diese Zahlung zu verarbeiten."; -"wallet__receive_liquidity__label" = "Ausgabensaldo Liquidität"; -"wallet__receive_liquidity__label_additional" = "Zusätzliche Ausgaben Saldo Liquidität"; "wallet__receive_cjit_error" = "Transaktion fehlgeschlagen"; "wallet__receive_cjit_error_msg" = "Fehler beim Senden der Gelder auf dein Ausgabenkonto."; -"wallet__receive_cjit_error_max__title" = "Maximale Empfangskapazität"; -"wallet__receive_cjit_error_max__description" = "Das Maximum, das du derzeit auf dein Ausgabenkonto empfangen kannst, ist ₿ {amount}."; -"wallet__receive_cjit_error_node_capacity__title" = "Zusätzliche Kapazität nicht verfügbar"; -"wallet__receive_cjit_error_node_capacity__description" = "Bitkit kann dieser Wallet derzeit keine weitere Ausgabenkapazität hinzufügen."; -"wallet__receive_will" = "Du wirst empfangen"; "wallet__receive_ldk_init" = "Ausgabensaldo Initialisierung..."; "wallet__minimum" = "MINIMUM"; "wallet__activity" = "Aktivität"; @@ -1601,7 +1526,7 @@ "subscriptions__name_placeholder" = "Name des Abonnements"; "subscriptions__ongoing" = "Laufend"; "subscriptions__overview" = "Übersicht"; -"subscriptions__payment_due_description" = "Öffne Bitkit, um eine Abonnementzahlung zu prüfen."; +"subscriptions__payment_due_description" = "Öffne Bitkit, um die Zahlung zu prüfen"; "subscriptions__payment_due_title" = "Abonnementzahlung fällig"; "subscriptions__payments" = "Zahlungen"; "subscriptions__per_day" = "pro Tag"; @@ -1772,9 +1697,6 @@ "wallet__onchain_broadcast_rejected_note" = "Diese Zahlung hat das Netzwerk möglicherweise trotzdem erreicht. Sende sie nicht erneut. {reason}"; "wallet__onchain_broadcast_unconfirmed" = "Übertragung unbestätigt"; "wallet__onchain_broadcast_unconfirmed_note" = "Die Zahlung wurde möglicherweise gesendet. Sende sie nicht erneut."; -"wallet__onchain_funding_rejected" = "Die Finanzierungstransaktion wurde abgelehnt. Versuche nicht, diesen Auftrag erneut zu finanzieren."; -"wallet__onchain_funding_unknown" = "Die Finanzierungstransaktion wurde möglicherweise gesendet. Versuche nicht, diesen Auftrag erneut zu finanzieren."; -"wallet__onchain_funding_followup_failed" = "Die Finanzierung wurde gesendet, aber die lokale Auftragsverfolgung konnte nicht gespeichert werden. Finanziere diesen Auftrag nicht erneut."; "wallet__onchain_retry_original" = "Ursprüngliche Zahlung wiederholen"; "wallet__onchain_retry_fee" = "Gebührensatz (sat/vB)"; "wallet__onchain_retry_note" = "Sende {amount} sats erneut an {address} mit denselben ursprünglichen Coins. Der Empfängerbetrag bleibt unverändert. Gib den Netzwerkgebührensatz in sat/vB ein. Dadurch kann keine separate Zahlung entstehen."; diff --git a/Bitkit/Resources/Localization/el.lproj/Localizable.strings b/Bitkit/Resources/Localization/el.lproj/Localizable.strings index f84cff360..603b6e73c 100644 --- a/Bitkit/Resources/Localization/el.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/el.lproj/Localizable.strings @@ -3,8 +3,8 @@ "cards__discount__description" = "Υπόλοιπο Δαπανών"; "cards__quickpay__title" = "QuickPay"; "cards__quickpay__description" = "Σαρώστε και πληρώστε"; -"cards__notifications__description_alt" = "Όταν το Bitkit είναι κλειστό"; -"cards__notifications__title" = "Λήψη Πληρωμής"; +"cards__notifications__description_alt" = "Υπενθυμίσεις πληρωμών"; +"cards__notifications__title" = "Ειδοποιήσεις"; "cards__invite__title" = "Πρόσκληση"; "cards__invite__description" = "Μοιραστείτε το Bitkit"; "cards__lightning__title" = "Δαπάνες"; @@ -147,9 +147,7 @@ "common__learn_more" = "Μάθετε Περισσότερα"; "common__understood" = "Κατανοητό"; "common__connect" = "Σύνδεση"; -"common__min" = "Ελάχ."; "common__max" = "Μέγ."; -"common__default" = "Προεπιλογή"; "common__preview" = "Προεπισκόπηση"; "common__show_all" = "Εμφάνιση Όλων"; "common__show_details" = "Εμφάνιση Λεπτομερειών"; @@ -181,7 +179,6 @@ "fee__slow__range" = "±1-2 ώρες"; "fee__slow__shortRange" = "1-2 ώρες"; "fee__slow__longRange" = "± 1-2 ώρες"; -"fee__minimum__title" = "Ελάχιστο"; "fee__minimum__longTitle" = "Ελάχιστο"; "fee__minimum__description" = "+2 ώρες"; "fee__minimum__shortRange" = "+2 ώρες"; @@ -194,66 +191,29 @@ "fee__custom__range" = "Εξαρτάται από το τέλος"; "fee__custom__shortRange" = "Εξαρτάται από το τέλος"; "fee__custom__longRange" = "Εξαρτάται από την προμήθεια"; -"lightning__transfer_intro__title" = "Υπόλοιπο\nΔαπανών"; -"lightning__transfer_intro__text" = "Χρηματοδοτήστε το υπόλοιπο δαπανών σας για να απολαύσετε άμεσες και φθηνές συναλλαγές με φίλους, οικογένεια και εμπόρους."; -"lightning__transfer_intro__button" = "Ξεκινήστε"; "lightning__funding__nav_title" = "Υπόλοιπο Δαπανών"; "lightning__funding__title" = "Χρηματοδοτήστε το υπόλοιπο δαπανών σας"; -"lightning__funding__text" = "Ο ευκολότερος τρόπος είναι να χρησιμοποιήσεις τις αποταμιεύσεις Bitkit ή να στείλεις bitcoin από διαφορετικό πορτοφόλι."; -"lightning__funding__text_blocked" = "Το Bitkit δεν παρέχει αυτή τη στιγμή υπηρεσίες Lightning στη χώρα σας, αλλά μπορείτε να συνδεθείτε απευθείας σε άλλους κόμβους."; -"lightning__funding__text_blocked_cjit" = "Το Bitkit δεν παρέχει αυτή τη στιγμή υπηρεσίες Lightning στη χώρα σας, αλλά μπορείτε να συνδεθείτε απευθείας σε άλλους κόμβους."; "lightning__funding__button1" = "Μεταφορά από Αποταμιεύσεις"; "lightning__funding__button2" = "Χρήση Άλλου Πορτοφολιού"; "lightning__funding__button3" = "Χειροκίνητη Ρύθμιση"; -"lightning__funding_advanced__nav_title" = "Υπόλοιπο Δαπανών"; -"lightning__funding_advanced__title" = "Ρυθμίσεις για προχωρημένους"; -"lightning__funding_advanced__text" = "Σαρώστε ένα QR για να διεκδικήσετε το LNURL Channel σας από άλλο LSP, ή επιλέξτε χειροκίνητη ρύθμιση."; -"lightning__funding_advanced__button1" = "LNURL Channel QR"; -"lightning__funding_advanced__button2" = "Χειροκίνητη Ρύθμιση"; "lightning__funding_advanced__button_short" = "Ρύθμιση για Προχωρημένους"; -"lightning__no_funds__title" = "Δεν υπάρχουν διαθέσιμα κεφάλαια"; -"lightning__no_funds__description" = "Πριν μπορέσεις να μεταφέρεις κεφάλαια από το υπόλοιπο αποταμιεύσεων, πρέπει να στείλεις bitcoin στο πορτοφόλι Bitkit."; -"lightning__no_funds__fund" = "Χρηματοδότηση πορτοφολιού"; "lightning__transfer__nav_title" = "Μεταφορά Κεφαλαίων"; "lightning__transfer__confirm" = "Παρακαλώ\nεπιβεβαιώστε"; "lightning__transfer__swipe" = "Σύρετε για Μεταφορά"; "lightning__transfer_hw__broadcast_uncertain_title" = "Η συναλλαγή μπορεί να έχει σταλεί"; "lightning__transfer_hw__broadcast_uncertain_description" = "Η συναλλαγή χρηματοδότησης μπορεί να έχει μεταδοθεί. Ελέγξτε τη δραστηριότητά σας πριν δοκιμάσετε ξανά."; -"lightning__transfer_hw__open_connect" = "Άνοιγμα Trezor Connect"; "lightning__transfer_hw__reconnect_error_description" = "Σύνδεσε ξανά τη συσκευή υλικού σου, φαίνεται ότι αποσυνδέθηκε από το τηλέφωνό σου."; "lightning__transfer_hw__reconnect_error_title" = "Επανασύνδεση συσκευής υλικού"; -"lightning__transfer_hw__sign_title" = "Υπόγραψε με\nτη συσκευή σου"; -"lightning__transfer_hw__signed_title" = "Η συναλλαγή\nυπογράφηκε"; "lightning__transfer_to_savings_button" = "Μεταφορά στις Αποταμιεύσεις"; "lightning__transfer_to_spending_button" = "Μεταφορά στις Δαπάνες"; -"lightning__spending_intro__title" = "Μεταφορά\nστις δαπάνες"; -"lightning__spending_intro__text" = "Μεταφέρετε κεφάλαια στο υπόλοιπο δαπανών σας για να απολαύσετε άμεσες και φθηνές συναλλαγές με φίλους, οικογένεια και εμπόρους."; -"lightning__spending_intro__button" = "Ξεκινήστε"; "lightning__transfer_failed__continue" = "Προσπαθείστε Ξανά"; -"lightning__spending_amount__title" = "Μεταφορά\nστις δαπάνες"; "lightning__spending_amount__quarter" = "25%"; -"lightning__spending_amount__error_balance__description" = "Οι αποταμιεύσεις σου δεν επαρκούν για αυτή τη μεταφορά και τα τέλη της. Δοκίμασε μικρότερο ποσό."; -"lightning__spending_amount__error_balance__title" = "Ανεπαρκείς αποταμιεύσεις"; "lightning__spending_amount__error_max__title" = "Μέγιστο υπόλοιπο δαπανών"; "lightning__spending_amount__error_max__description" = "Το ποσό που μπορείς να μεταφέρεις στο υπόλοιπο δαπανών περιορίζεται προς το παρόν σε ₿ {amount}."; "lightning__spending_confirm__network_fee" = "Τέλη δικτύου"; "lightning__spending_confirm__lsp_fee" = "Τέλη υπηρεσίας"; "lightning__spending_confirm__amount" = "Στις δαπάνες"; -"lightning__spending_confirm__background_setup" = "Ρύθμιση στο παρασκήνιο"; "lightning__spending_confirm__total" = "Σύνολο"; -"lightning__spending_confirm__default" = "Χρήση Προεπιλογών"; -"lightning__spending_confirm__fees_changed_title" = "Τα τέλη άλλαξαν"; -"lightning__spending_confirm__fees_changed_service" = "Τα τέλη είναι {amount} υψηλότερα τώρα που δημιουργήθηκε η παραγγελία σου. Σύρε ξανά για επιβεβαίωση."; -"lightning__spending_confirm__fees_changed_network" = "Τα τέλη είναι {amount} υψηλότερα, επειδή τα ποσοστά τελών ή το υπόλοιπο αποταμιεύσεων άλλαξαν ενώ ήσουν σε αυτή την οθόνη. Σύρε ξανά για επιβεβαίωση."; -"lightning__spending_advanced__title" = "Χωρητικότητα\nλήψης"; -"lightning__spending_advanced__fee" = "Τέλος ρευστότητας"; -"lightning__spending_advanced__error_balance__description" = "Οι αποταμιεύσεις σου δεν επαρκούν για το τέλος ρευστότητας αυτής της χωρητικότητας λήψης. Επίλεξε μικρότερο ποσό."; -"lightning__spending_advanced__error_balance__title" = "Ανεπαρκή κεφάλαια"; -"lightning__spending_advanced__error_max__title" = "Μέγιστη χωρητικότητα λήψης"; -"lightning__spending_advanced__error_max__description" = "Η χωρητικότητα λήψης περιορίζεται προς το παρόν σε ₿ {amount}."; -"lightning__liquidity__title" = "Ρευστότητα\n& δρομολόγηση"; -"lightning__liquidity__text" = "Το Υπόλοιπο Δαπανών σας χρησιμοποιεί το Lightning Network για να κάνει τις πληρωμές σας φθηνότερες, ταχύτερες και πιο ιδιωτικές.\n\nΑυτό λειτουργεί όπως η πρόσβαση στο διαδίκτυο, αλλά πληρώνετε για ρευστότητα & δρομολόγηση αντί για εύρος ζώνης.\n\nΑυτή η ρύθμιση περιλαμβάνει ορισμένα εφάπαξ κόστη."; -"lightning__liquidity__label" = "Ρευστότητα Υπολοίπου Δαπανών"; "lightning__savings_intro__title" = "Μεταφορά\nστις αποταμιεύσεις"; "lightning__savings_intro__text" = "Μεταφέρετε το υπόλοιπο δαπανών σας στις αποταμιεύσεις για να αποθηκεύσετε τα Bitcoin σας μακροπρόθεσμα και να εξασφαλίσετε το μέλλον σας."; "lightning__savings_intro__button" = "Ξεκινήστε"; @@ -277,9 +237,6 @@ "lightning__savings_interrupted__nav_title" = "Μεταφορά\nδιακόπηκε"; "lightning__savings_interrupted__title" = "Κρατήστε το Bitkit\nανοιχτό"; "lightning__savings_interrupted__text" = "Τα κεφάλαια δεν έχουν μεταφερθεί ακόμα. Το Bitkit θα προσπαθήσει να ξεκινήσει τη μεταφορά τα επόμενα 30 λεπτά. Παρακαλώ κρατήστε την εφαρμογή ανοιχτή."; -"lightning__transfer_success__nav_title" = "Επιτυχής Μεταφορά"; -"lightning__transfer_success__title_spending" = "Τα κεφάλαια μεταφέρθηκαν\nστις δαπάνες"; -"lightning__transfer_success__text_spending" = "Τα κεφάλαιά σας έχουν μεταφερθεί και το υπόλοιπο δαπανών σας είναι έτοιμο για χρήση."; "lightning__transfer_success__title_savings" = "Τα κεφάλαια μεταφέρθηκαν\nστις αποταμιεύσεις"; "lightning__transfer_success__text_savings" = "Τα κεφάλαιά σας έχουν μεταφερθεί. Μπορείτε να χρησιμοποιήσετε αυτά τα κεφάλαια αμέσως."; "lightning__external__nav_title" = "Σύνδεση Lightning"; @@ -298,12 +255,6 @@ "lightning__savings" = "Αποταμιεύσεις"; "lightning__spending_label" = "Υπόλοιπο δαπανών"; "lightning__receiving_label" = "Χωρητικότητα λήψης"; -"lightning__setting_up_text" = "Παρακαλώ περιμένετε, η μεταφορά κεφαλαίων σας είναι σε εξέλιξη. Αυτό θα διαρκέσει ±10 λεπτά."; -"lightning__setting_up_step1" = "Επεξεργασία Πληρωμής"; -"lightning__setting_up_step2" = "Επιτυχής Πληρωμή"; -"lightning__setting_up_step3" = "Σε Ουρά για Άνοιγμα"; -"lightning__setting_up_step4" = "Άνοιγμα Σύνδεσης"; -"lightning__setting_up_button" = "Συνεχίστε να Χρησιμοποιείτε το Bitkit"; "lightning__node_info" = "Κόμβος Lightning"; "lightning__node_disconnected" = "αποσυνδεδεμένο"; "lightning__connections" = "Συνδέσεις Lightning"; @@ -362,9 +313,9 @@ "lightning__force_init_msg" = "Τα κεφάλαιά σας θα είναι προσβάσιμα σε ±14 ημέρες."; "lightning__force_failed_title" = "Η Αναγκαστική Μεταφορά Απέτυχε"; "lightning__force_failed_msg" = "Αδυναμία μεταφοράς των κεφαλαίων σας πίσω στις αποταμιεύσεις. Παρακαλώ δοκιμάστε ξανά."; -"lightning__force_channels_skipped" = "Ορισμένες συνδέσεις δεν μπόρεσαν να κλείσουν."; "lightning__channel_opened_title" = "Το Υπόλοιπο Δαπανών είναι Έτοιμο"; "lightning__channel_opened_msg" = "Τώρα μπορείτε να πληρώσετε οποιονδήποτε, οπουδήποτε, άμεσα."; +"lightning__funding__button_hw" = "Μεταφορά από πορτοφόλι HW"; "lightning__order_state__awaiting_payment" = "Επεξεργασία πληρωμής"; "lightning__order_state__payment_canceled" = "Η πληρωμή ακυρώθηκε"; "lightning__order_state__paid" = "Επιτυχής πληρωμή"; @@ -375,6 +326,9 @@ "lightning__order_state__open" = "Η σύνδεση είναι ανοιχτή"; "lightning__order_state__inactive" = "Η σύνδεση είναι ανενεργή"; "lightning__peer_disconnected" = "Ο ομότιμος αποσυνδέθηκε."; +"lightning__transfer_unavailable__nav_title" = "Μεταφορά μη διαθέσιμη"; +"lightning__transfer_unavailable__text" = "Οι αυτόματες μεταφορές στο υπόλοιπο δαπανών δεν είναι πλέον διαθέσιμες. Χρησιμοποιήστε τον δικό σας κόμβο ή ρυθμίστε σύνδεση με άλλον πάροχο."; +"lightning__transfer_unavailable__title" = "Απαιτείται χειροκίνητη\nρύθμιση"; "onboarding__tos_header" = "Όροι χρήσης\nBitkit"; "onboarding__tos_subtitle" = "Συνεχίζοντας δηλώνετε ότι έχετε διαβάσει και αποδέχεστε τους όρους χρήσης."; "onboarding__tos_title" = "Όροι Χρήσης"; @@ -389,7 +343,6 @@ "onboarding__slide0_text_alt" = "Το Bitkit σας δίνει τα κλειδιά για να ελέγχετε τα χρήματά σας, το προφίλ σας και τις επαφές σας. Αναλάβετε τον έλεγχο και γίνετε χωρίς σύνορα."; "onboarding__slide1_header" = "Άμεσες\nπληρωμές"; "onboarding__slide1_text_alt" = "Απολαύστε άμεσες και φθηνές πληρωμές με φίλους, οικογένεια και εμπόρους στο Lightning Network."; -"onboarding__slide1_note" = "*Το Bitkit δεν παρέχει αυτή τη στιγμή υπηρεσίες Lightning στη χώρα σας, αλλά μπορείτε να συνδεθείτε σε άλλους κόμβους."; "onboarding__slide3_header" = "Η ιδιωτικότητα δεν\nείναι έγκλημα"; "onboarding__slide3_text_alt" = "Τα χρήματά σας, η ιδιωτικότητά σας. Σύρετε για να κρύψετε το υπόλοιπό σας και ξοδέψτε πιο ιδιωτικά, χωρίς παρακολούθηση δεδομένων και μηδενικό KYC."; "onboarding__slide4_header" = "Τα κλειδιά σας,\nτα νομίσματά σας"; @@ -478,8 +431,6 @@ "other__lnurl_withdr_error_minmax" = "Λανθασμένες παράμετροι ανάληψης LNURL, το min/max δεν έχει οριστεί σωστά."; "other__lnurl_withdr_success_title" = "Ζητήθηκε Ανάληψη"; "other__lnurl_withdr_success_msg" = "Η ανάληψή σας ζητήθηκε επιτυχώς. Αναμονή πληρωμής."; -"other__notification_registration_failed_description" = "Το Bitkit δεν μπόρεσε να εγγραφεί για ειδοποιήσεις push."; -"other__notification_registration_failed_title" = "Η Εγγραφή Ειδοποιήσεων Απέτυχε"; "other__bt_error_retrieve" = "Η Μεταφορά Απέτυχε"; "other__earlier" = "ΝΩΡΙΤΕΡΑ"; "other__update_nav_title" = "Διαθέσιμη Ενημέρωση"; @@ -489,14 +440,7 @@ "other__try_again" = "Δοκιμάστε ξανά."; "other__scan__error__generic" = "Το Bitkit δεν μπορεί να διαβάσει τα παρεχόμενα δεδομένα."; "other__scan__error__expired" = "Αυτό το τιμολόγιο Lightning έχει λήξει."; -"other__gift__claimed__text" = "Λυπούμαστε, αργήσατε! Όλα τα δώρα για αυτόν τον κωδικό έχουν ήδη διεκδικηθεί."; -"other__gift__claiming__text" = "Διεκδίκηση του κωδικού δώρου Bitkit σας..."; -"other__gift__claiming__title" = "Διεκδίκηση Δώρου"; -"other__gift__error__title" = "Σφάλμα Κωδικού Δώρου"; -"other__gift__error__text" = "Το Bitkit δεν μπόρεσε να διεκδικήσει τα κεφάλαια. Παρακαλώ δοκιμάστε ξανά αργότερα ή επικοινωνήστε μαζί μας."; "other__gift__error__button" = "ΟΚ"; -"other__gift__used__title" = "Χρησιμοποιημένος Κωδικός"; -"other__gift__used__text" = "Αυτός ο κωδικός δώρου Bitkit έχει ήδη χρησιμοποιηθεί και τα κεφάλαια έχουν εκταμιευθεί."; "other__gift__used__button" = "ΟΚ"; "other__shop__intro__title" = "Κατάστημα"; "other__shop__intro__description" = "Ζήστε τη ζωή σας με το πρότυπο Bitcoin. Ξοδέψτε τα Bitcoin σας σε ψηφιακές δωροκάρτες, eSIMs, ανανεώσεις τηλεφώνου και άλλα."; @@ -650,6 +594,7 @@ "pubky_auth__homeserver" = "Homeserver"; "pubky_auth__signup_description" = "Δημιούργησε μια νέα ταυτότητα Pubky σε αυτόν τον homeserver. Συνέχισε μόνο αν τον εμπιστεύεσαι."; "pubky_auth__approval_failed" = "Η Εξουσιοδότηση Απέτυχε"; +"wallet__onchain_abandon_transfer_note" = "Οι μεταφορές στο υπόλοιπο δαπανών δεν είναι πλέον διαθέσιμες. Το Bitkit ελέγχει πρώτα το δίκτυο και μετά επιστρέφει τα νομίσματα αυτής της μεταφοράς στο πορτοφόλι σας, ώστε η αρχική μεταφορά να μην επιβεβαιωθεί ποτέ. Αυτό έχει χρέωση δικτύου."; "watch_only_accounts__active_section" = "Ενεργοί λογαριασμοί"; "watch_only_accounts__copy_xpub" = "Αντιγραφή xpub"; "watch_only_accounts__description" = "Κάθε εγκεκριμένος διακομιστής Paykit λαμβάνει ξεχωριστό λογαριασμό Bitcoin. Απενεργοποίησε την παρακολούθηση για να αποφορτώσεις έναν λογαριασμό χωρίς να διαγράψεις το ιστορικό του πορτοφολιού του."; @@ -726,21 +671,17 @@ "settings__widgets__reset_widgets_dialog_title" = "Επαναφορά widgets;"; "settings__widgets__reset_widgets_dialog_description" = "Είσαι σίγουρος ότι θέλεις να επαναφέρεις τα widgets; Θα εμφανιστεί το προεπιλεγμένο σετ widgets με τις προεπιλεγμένες ρυθμίσεις."; "settings__widgets__reset_suggestions" = "Επαναφορά καρτών προτάσεων"; -"settings__notifications__nav_title" = "Πληρωμές Παρασκηνίου"; -"settings__notifications__intro__title" = "Λάβετε Πληρωμή\nΠαθητικά"; -"settings__notifications__intro__text" = "Ενεργοποιήστε τις ειδοποιήσεις για να λαμβάνετε πληρωμές, ακόμα και όταν η εφαρμογή Bitkit είναι κλειστή."; +"settings__notifications__nav_title" = "Ειδοποιήσεις"; +"settings__notifications__intro__title" = "Μείνε\nενημερωμένος"; +"settings__notifications__intro__text" = "Ενεργοποιήστε τις ειδοποιήσεις για να λαμβάνετε υπενθυμίσεις για τις συνδρομές και τα αιτήματα πληρωμής σας."; "settings__notifications__intro__button" = "Ενεργοποίηση"; -"settings__notifications__settings__toggle" = "Λήψη πληρωμών όταν το Bitkit είναι κλειστό"; -"settings__notifications__settings__enabled" = "Οι πληρωμές παρασκηνίου είναι ενεργοποιημένες. Μπορείτε να λάβετε κεφάλαια ακόμα και όταν η εφαρμογή είναι κλειστή (αν η συσκευή σας είναι συνδεδεμένη στο διαδίκτυο)."; -"settings__notifications__settings__disabled" = "Ενεργοποιήστε τις πληρωμές παρασκηνίου για να λαμβάνετε κεφάλαια ακόμα και όταν η εφαρμογή είναι κλειστή (αν η συσκευή σας είναι συνδεδεμένη στο διαδίκτυο)."; -"settings__notifications__settings__denied" = "Οι πληρωμές παρασκηνίου είναι απενεργοποιημένες, επειδή έχετε αρνηθεί τις ειδοποιήσεις."; -"settings__notifications__settings__preview__title" = "Λήψη Πληρωμής"; -"settings__notifications__settings__preview__text" = "Ανοίξτε το Bitkit για λεπτομέρειες"; +"settings__notifications__settings__toggle" = "Να επιτρέπονται οι ειδοποιήσεις"; +"settings__notifications__settings__enabled" = "Οι ειδοποιήσεις είναι ενεργές. Το Bitkit μπορεί να σας υπενθυμίζει συνδρομές και αιτήματα πληρωμής."; +"settings__notifications__settings__disabled" = "Ενεργοποιήστε τις ειδοποιήσεις για να λαμβάνετε υπενθυμίσεις για τις συνδρομές και τα αιτήματα πληρωμής σας."; +"settings__notifications__settings__denied" = "Οι ειδοποιήσεις είναι απενεργοποιημένες, επειδή τις απορρίψατε."; "settings__notifications__settings__preview__time" = "πριν 3λ"; -"settings__notifications__settings__privacy__label" = "Απόρρητο"; -"settings__notifications__settings__privacy__text" = "Συμπερίληψη ποσού στις ειδοποιήσεις"; "settings__notifications__settings__notifications__label" = "Ειδοποιήσεις"; -"settings__notifications__settings__notifications__text" = "Για να ενεργοποιήσετε τις πληρωμές παρασκηνίου, παρακαλώ ενεργοποιήστε τις ειδοποιήσεις στις Ρυθμίσεις iOS του Bitkit."; +"settings__notifications__settings__notifications__text" = "Για να λαμβάνετε υπενθυμίσεις, ενεργοποιήστε τις ειδοποιήσεις στις Ρυθμίσεις iOS του Bitkit."; "settings__notifications__settings__button__enabled" = "Προσαρμογή στις Ρυθμίσεις Bitkit {platform}"; "settings__notifications__settings__button__disabled" = "Ενεργοποίηση στις Ρυθμίσεις Bitkit {platform}"; "settings__quickpay__nav_title" = "QuickPay"; @@ -1159,26 +1100,10 @@ "wallet__receive_lightning_invoice" = "Τιμολόγιο Lightning"; "wallet__receive_note_placeholder" = "Προαιρετική σημείωση στον πληρωτή"; "wallet__receive_show_qr" = "Εμφάνιση Κωδικού QR"; -"wallet__receive_background_setup_text" = "Ενεργοποιήστε τη ρύθμιση παρασκηνίου για να βγείτε με ασφάλεια από το Bitkit ενώ διαμορφώνεται το υπόλοιπό σας."; -"wallet__receive_background_setup_toggle" = "Ενεργοποίηση ρύθμισης παρασκηνίου"; "wallet__receive_spending" = "Λήψη κεφαλαίων Lightning"; "wallet__receive_spending_text" = "Απολαύστε άμεσες και φθηνές πληρωμές bitcoin στο Lightning Network."; "wallet__receive_spending_title" = "Λήψη στο υπόλοιπο δαπανών"; -"wallet__receive_connect_initial" = "Για τη ρύθμιση του υπολοίπου δαπανών σας, θα αφαιρεθεί τέλος δικτύου {networkFee} και τέλος παρόχου υπηρεσιών {serviceFee}."; -"wallet__receive_connect_additional" = "Για να λάβετε περισσότερα άμεσα Bitcoin, το Bitkit πρέπει να αυξήσει τη ρευστότητά σας. Τέλος δικτύου {networkFee} και τέλος παρόχου υπηρεσιών {serviceFee} θα αφαιρεθούν από το ποσό που καθορίσατε."; -"wallet__receive_liquidity__nav_title" = "Ρύθμιση Υπολοίπου Δαπανών"; -"wallet__receive_liquidity__nav_title_additional" = "Αύξηση χωρητικότητας λήψης"; -"wallet__receive_liquidity__invalid_quote" = "Το Bitkit έλαβε μη έγκυρη προσφορά ρευστότητας. Δοκιμάστε ξανά."; -"wallet__receive_liquidity__text" = "Το Υπόλοιπο Δαπανών σας χρησιμοποιεί το Lightning Network για να κάνει τις πληρωμές σας φθηνότερες, ταχύτερες και πιο ιδιωτικές.\n\nΑυτό λειτουργεί όπως η πρόσβαση στο διαδίκτυο, αλλά πληρώνετε για ρευστότητα & δρομολόγηση αντί για εύρος ζώνης.\n\nΑυτή η ρύθμιση περιλαμβάνει ορισμένα εφάπαξ κόστη."; -"wallet__receive_liquidity__text_additional" = "Το Υπόλοιπο Δαπανών σας χρησιμοποιεί το Lightning Network για να κάνει τις πληρωμές σας φθηνότερες, ταχύτερες και πιο ιδιωτικές.\n\nΑυτό λειτουργεί όπως η πρόσβαση στο διαδίκτυο, αλλά πληρώνετε για ρευστότητα & δρομολόγηση αντί για εύρος ζώνης.\n\nΤο Bitkit πρέπει να αυξήσει τη χωρητικότητα λήψης του υπολοίπου δαπανών σας για να επεξεργαστεί αυτή την πληρωμή."; -"wallet__receive_liquidity__label" = "Ρευστότητα Υπολοίπου Δαπανών"; -"wallet__receive_liquidity__label_additional" = "Πρόσθετη Ρευστότητα Υπολοίπου Δαπανών"; "wallet__receive_cjit_error" = "Η Συναλλαγή Απέτυχε"; -"wallet__receive_cjit_error_max__title" = "Μέγιστη χωρητικότητα λήψης"; -"wallet__receive_cjit_error_max__description" = "Το μέγιστο που μπορείς να λάβεις στο υπόλοιπο δαπανών αυτή τη στιγμή είναι ₿ {amount}."; -"wallet__receive_cjit_error_node_capacity__title" = "Η πρόσθετη χωρητικότητα δεν είναι διαθέσιμη"; -"wallet__receive_cjit_error_node_capacity__description" = "Το Bitkit δεν μπορεί να προσθέσει περισσότερη χωρητικότητα δαπανών σε αυτό το πορτοφόλι αυτήν τη στιγμή."; -"wallet__receive_will" = "Θα λάβετε"; "wallet__activity" = "Δραστηριότητα"; "wallet__activity_no" = "Δεν υπάρχει δραστηριότητα ακόμα"; "wallet__activity_no_explain" = "Λάβετε κεφάλαια για να ξεκινήσετε"; @@ -1352,7 +1277,7 @@ "subscriptions__name_placeholder" = "Όνομα συνδρομής"; "subscriptions__ongoing" = "Σε εξέλιξη"; "subscriptions__overview" = "Επισκόπηση"; -"subscriptions__payment_due_description" = "Άνοιξε το Bitkit για να ελέγξεις μια πληρωμή συνδρομής."; +"subscriptions__payment_due_description" = "Άνοιξε το Bitkit για να ελέγξεις την πληρωμή"; "subscriptions__payment_due_title" = "Οφειλόμενη πληρωμή συνδρομής"; "subscriptions__payments" = "Πληρωμές"; "subscriptions__per_day" = "ανά ημέρα"; @@ -1506,9 +1431,6 @@ "wallet__onchain_broadcast_rejected_note" = "Αυτή η πληρωμή μπορεί να έχει φτάσει στο δίκτυο. Μην τη στείλετε ξανά. {reason}"; "wallet__onchain_broadcast_unconfirmed" = "Η μετάδοση δεν επιβεβαιώθηκε"; "wallet__onchain_broadcast_unconfirmed_note" = "Η πληρωμή μπορεί να έχει σταλεί. Μην τη στείλετε ξανά."; -"wallet__onchain_funding_rejected" = "Η συναλλαγή χρηματοδότησης απορρίφθηκε. Μην προσπαθήσετε να χρηματοδοτήσετε ξανά αυτή την παραγγελία."; -"wallet__onchain_funding_unknown" = "Η συναλλαγή χρηματοδότησης μπορεί να έχει σταλεί. Μην προσπαθήσετε να χρηματοδοτήσετε ξανά αυτή την παραγγελία."; -"wallet__onchain_funding_followup_failed" = "Η χρηματοδότηση στάλθηκε, αλλά η τοπική παρακολούθηση της παραγγελίας δεν αποθηκεύτηκε. Μη χρηματοδοτήσετε ξανά αυτή την παραγγελία."; "wallet__onchain_retry_original" = "Επανάληψη αρχικής πληρωμής"; "wallet__onchain_retry_fee" = "Ποσοστό τέλους (sat/vB)"; "wallet__onchain_retry_note" = "Επανάληψη {amount} sats προς {address} με τα ίδια αρχικά νομίσματα. Το ποσό του παραλήπτη παραμένει ίδιο. Εισαγάγετε το ποσοστό τέλους δικτύου σε sat/vB. Αυτό δεν μπορεί να δημιουργήσει ξεχωριστή πληρωμή."; diff --git a/Bitkit/Resources/Localization/en.lproj/Localizable.strings b/Bitkit/Resources/Localization/en.lproj/Localizable.strings index 81f981b16..f1634ce0f 100644 --- a/Bitkit/Resources/Localization/en.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/en.lproj/Localizable.strings @@ -4,9 +4,9 @@ "cards__discount__description" = "Spending Balance"; "cards__quickpay__title" = "QuickPay"; "cards__quickpay__description" = "Scan and pay"; -"cards__notifications__description" = "Without Bitkit open"; -"cards__notifications__description_alt" = "When Bitkit is closed"; -"cards__notifications__title" = "Get Paid"; +"cards__notifications__description" = "Payment reminders"; +"cards__notifications__description_alt" = "Payment reminders"; +"cards__notifications__title" = "Notifications"; "cards__invite__title" = "Invite"; "cards__invite__description" = "Share Bitkit"; "cards__lightning__title" = "Spend"; @@ -153,9 +153,7 @@ "common__learn_more" = "Learn More"; "common__understood" = "Understood"; "common__connect" = "Connect"; -"common__min" = "Min"; "common__max" = "Max"; -"common__default" = "Default"; "common__preview" = "Preview"; "common__qr_code" = "QR Code"; "common__show_all" = "Show All"; @@ -181,7 +179,6 @@ "fee__slow__description" = "±1 hours"; "fee__slow__shortDescription" = "±1h"; "fee__slow__range" = "±1-2 hours"; -"fee__minimum__title" = "Minimum"; "fee__minimum__longTitle" = "Minimum"; "fee__minimum__description" = "+2 hours"; "fee__minimum__shortDescription" = "+2h"; @@ -191,74 +188,37 @@ "fee__custom__description" = "Depends on the fee"; "fee__custom__shortDescription" = "Depends on the fee"; "fee__custom__range" = "Depends on the fee"; -"lightning__transfer_intro__title" = "Spending\nBalance"; -"lightning__transfer_intro__text" = "Fund your spending balance to enjoy instant and cheap transactions with friends, family, and merchants."; -"lightning__transfer_intro__button" = "Get Started"; "lightning__funding__nav_title" = "Spending Balance"; "lightning__funding__title" = "Fund your spending balance"; -"lightning__funding__text" = "The easiest option is to use your Bitkit savings or send bitcoin from a different wallet."; -"lightning__funding__text_blocked" = "Bitkit does not currently provide Lightning services in your country, but you can still connect to other nodes directly."; -"lightning__funding__text_blocked_cjit" = "Bitkit does not currently provide Lightning services in your country, but you can still connect to other nodes directly."; "lightning__funding__button1" = "Transfer from Savings"; "lightning__funding__button2" = "Use Other Wallet"; "lightning__funding__button3" = "Manual Setup"; -"lightning__funding_advanced__nav_title" = "Spending Balance"; -"lightning__funding_advanced__title" = "Advanced setup"; -"lightning__funding_advanced__text" = "Scan a QR to claim your LNURL Channel from another LSP, or choose manual setup."; -"lightning__funding_advanced__button1" = "LNURL Channel QR"; -"lightning__funding_advanced__button2" = "Manual Setup"; "lightning__funding_advanced__button_short" = "Advanced Setup"; -"lightning__no_funds__title" = "No Available Funds"; -"lightning__no_funds__description" = "Before you can transfer funds from your savings balance, you need to send bitcoin to your Bitkit wallet."; -"lightning__no_funds__fund" = "Fund wallet"; "lightning__transfer__nav_title" = "Transfer Funds"; "lightning__transfer__confirm" = "Please\nconfirm"; "lightning__transfer__custom_fee" = "Custom fee"; "lightning__transfer__swipe" = "Swipe To Transfer"; "lightning__transfer_hw__broadcast_uncertain_title" = "Transaction may have been sent"; "lightning__transfer_hw__broadcast_uncertain_description" = "Your funding transaction may have been broadcast. Please check your activity before trying again."; -"lightning__transfer_hw__open_connect" = "Open Trezor Connect"; "lightning__transfer_hw__reconnect_error_description" = "Please reconnect your hardware device, it appears to be disconnected from your phone."; "lightning__transfer_hw__reconnect_error_title" = "Reconnect Hardware Device"; -"lightning__transfer_hw__sign_title" = "Sign with\nyour device"; -"lightning__transfer_hw__signed_title" = "Transaction\nsigned"; "lightning__transfer_to_savings_button" = "Transfer To Savings"; "lightning__transfer_to_spending_button" = "Transfer To Spending"; -"lightning__spending_intro__title" = "Transfer\nto spending"; -"lightning__spending_intro__text" = "Transfer funds to your spending balance to enjoy instant and cheap transactions with friends, family, and merchants."; -"lightning__spending_intro__button" = "Get Started"; "lightning__transfer_failed__nav_title" = "Transfer Failure"; "lightning__transfer_failed__title" = "Transfer\nfailed"; "lightning__transfer_failed__text" = "Sorry, Bitkit could not set up your spending balance or complete your transfer. Please try again."; "lightning__transfer_failed__cancel" = "Inspect Error"; "lightning__transfer_failed__continue" = "Try Again"; -"lightning__spending_amount__title" = "Transfer\nto spending"; "lightning__spending_amount__quarter" = "25%"; "lightning__spending_amount__error_min__title" = "Savings Balance Minimum"; "lightning__spending_amount__error_min__description" = "A minimum of ₿ {amount} is needed to set up your spending balance."; -"lightning__spending_amount__error_balance__description" = "Your savings cannot cover this transfer and its fees. Try a smaller amount."; -"lightning__spending_amount__error_balance__title" = "Insufficient Savings"; "lightning__spending_amount__error_max__title" = "Spending Balance Maximum"; "lightning__spending_amount__error_max__description" = "The amount you can transfer to your spending balance is currently limited to ₿ {amount}."; "lightning__spending_amount__error_max__description_zero" = "Your transfer to the spending balance is limited due to liquidity policy. For details, visit the Help Center."; "lightning__spending_confirm__network_fee" = "Network fees"; "lightning__spending_confirm__lsp_fee" = "Service fees"; "lightning__spending_confirm__amount" = "To spending"; -"lightning__spending_confirm__background_setup" = "Set up in background"; "lightning__spending_confirm__total" = "Total"; -"lightning__spending_confirm__default" = "Use Defaults"; -"lightning__spending_confirm__fees_changed_title" = "Fees changed"; -"lightning__spending_confirm__fees_changed_service" = "Fees are {amount} higher now that your order is created. Swipe again to confirm."; -"lightning__spending_confirm__fees_changed_network" = "Fees are {amount} higher because fee rates or your savings balance changed while you were on this screen. Swipe again to confirm."; -"lightning__spending_advanced__title" = "Receiving\ncapacity"; -"lightning__spending_advanced__fee" = "Liquidity fee"; -"lightning__spending_advanced__error_balance__description" = "Your savings cannot cover the liquidity fee for this receiving capacity. Choose a smaller amount."; -"lightning__spending_advanced__error_balance__title" = "Not Enough Funds"; -"lightning__spending_advanced__error_max__title" = "Receiving Capacity Maximum"; -"lightning__spending_advanced__error_max__description" = "The receiving capacity is currently limited to ₿ {amount}."; -"lightning__liquidity__title" = "Liquidity\n& routing"; -"lightning__liquidity__text" = "Your Spending Balance uses the Lightning Network to make your payments cheaper, faster, and more private.\n\nThis works like internet access, but you pay for liquidity & routing instead of bandwidth.\n\nThis setup includes some one-time costs."; -"lightning__liquidity__label" = "Spending Balance Liquidity"; "lightning__savings_intro__title" = "Transfer\nto savings"; "lightning__savings_intro__text" = "Transfer your spending balance to your savings to store your Bitcoin long-term and secure your future."; "lightning__savings_intro__button" = "Get Started"; @@ -282,9 +242,6 @@ "lightning__savings_interrupted__nav_title" = "Transfer\ninterrupted"; "lightning__savings_interrupted__title" = "Keep Bitkit\nopen"; "lightning__savings_interrupted__text" = "Funds were not transferred yet. Bitkit will try to initiate the transfer during the next 30 minutes. Please keep your app open."; -"lightning__transfer_success__nav_title" = "Transfer Successful"; -"lightning__transfer_success__title_spending" = "Funds moved\nto spending"; -"lightning__transfer_success__text_spending" = "Your funds have been transferred and your spending balance is ready to use."; "lightning__transfer_success__title_savings" = "Funds moved\nto savings"; "lightning__transfer_success__text_savings" = "Your funds have been transferred. You are able to use these funds immediately."; "lightning__external__nav_title" = "Lightning Connection"; @@ -307,12 +264,6 @@ "lightning__spending_label" = "Spending balance"; "lightning__receiving_label" = "Receiving capacity"; "lightning__setting_up_header" = "In transfer"; -"lightning__setting_up_text" = "Please wait, your funds transfer is in progress. This should take ±10 minutes."; -"lightning__setting_up_step1" = "Processing Payment"; -"lightning__setting_up_step2" = "Payment Successful"; -"lightning__setting_up_step3" = "Queued For Opening"; -"lightning__setting_up_step4" = "Opening Connection"; -"lightning__setting_up_button" = "Continue Using Bitkit"; "lightning__node_info" = "Lightning Node"; "lightning__node_id" = "LDK Node ID"; "lightning__node_disconnected" = "disconnected"; @@ -384,9 +335,9 @@ "lightning__force_init_msg" = "Your funds will be accessible in ±14 days."; "lightning__force_failed_title" = "Force Transfer Failed"; "lightning__force_failed_msg" = "Unable to transfer your funds back to savings. Please try again."; -"lightning__force_channels_skipped" = "Some connections could not be closed."; "lightning__channel_opened_title" = "Spending Balance Ready"; "lightning__channel_opened_msg" = "You can now pay anyone, anywhere, instantly."; +"lightning__funding__button_hw" = "Transfer from HW wallet"; "lightning__order_given_up_title" = "Instant Payments Setup Failed"; "lightning__order_given_up_msg" = "The setup process expired. Please try again."; "lightning__order_expired_title" = "Instant Payments Setup Failed"; @@ -405,6 +356,9 @@ "lightning__order_state__open" = "Connection open"; "lightning__order_state__inactive" = "Connection inactive"; "lightning__peer_disconnected" = "Peer disconnected."; +"lightning__transfer_unavailable__nav_title" = "Transfer Unavailable"; +"lightning__transfer_unavailable__text" = "Automatic transfers to your spending balance are no longer available. Use your own node, or set up a connection with another provider."; +"lightning__transfer_unavailable__title" = "Manual setup\nrequired"; "onboarding__tos_header" = "Bitkit\nterms of use"; "onboarding__tos_checkbox" = "Terms of use"; "onboarding__tos_checkbox_value" = "I declare that I have read and accept the terms of use."; @@ -424,7 +378,6 @@ "onboarding__slide1_header" = "Instant\npayments"; "onboarding__slide1_text" = "Spend bitcoin faster than ever. Enjoy instant and cheap payments with friends, family, and merchants."; "onboarding__slide1_text_alt" = "Enjoy instant and cheap payments with friends, family, and merchants on the Lightning Network."; -"onboarding__slide1_note" = "*Bitkit does not currently provide Lightning services in your country, but you can still connect to other nodes."; "onboarding__slide2_header" = "Bitcoiners,\nborderless"; "onboarding__slide2_text" = "Take charge of your digital life with portable profiles and payable contacts."; "onboarding__slide3_header" = "Privacy is\nnot a crime"; @@ -546,8 +499,6 @@ "other__lnurl_withdr_error_minmax" = "Incorrect LNURL withdraw params, min/max not set correctly."; "other__lnurl_withdr_success_title" = "Withdraw Requested"; "other__lnurl_withdr_success_msg" = "Your withdraw was successfully requested. Waiting for payment."; -"other__notification_registration_failed_description" = "Bitkit was unable to register for push notifications."; -"other__notification_registration_failed_title" = "Notification Registration Failed"; "other__phone_settings" = "Open Phone Settings"; "other__bt_error_retrieve" = "Transfer Failed"; "other__bt_error_connect" = "Unable to add LSP node as a peer at this time."; @@ -563,14 +514,7 @@ "other__transfer_notification__body" = "Open Bitkit to complete your transfer"; "other__scan__error__generic" = "Bitkit is unable to read the provided data."; "other__scan__error__expired" = "This Lightning invoice has expired."; -"other__gift__claimed__text" = "Sorry, you're too late! All gifts for this code have already been claimed."; -"other__gift__claiming__text" = "Claiming your Bitkit gift code..."; -"other__gift__claiming__title" = "Claiming Gift"; -"other__gift__error__title" = "Gift Code Error"; -"other__gift__error__text" = "Bitkit couldn\'t claim the funds. Please try again later or contact us."; "other__gift__error__button" = "OK"; -"other__gift__used__title" = "Used Code"; -"other__gift__used__text" = "This Bitkit gift code has already been used, and the funds have been paid out."; "other__gift__used__button" = "OK"; "other__shop__intro__title" = "Shop"; "other__shop__intro__description" = "Get your life on the Bitcoin standard. Spend your Bitcoin on digital gift cards, eSIMs, phone refills, and more."; @@ -823,21 +767,17 @@ "settings__widgets__reset_widgets_dialog_title" = "Reset Widgets?"; "settings__widgets__reset_widgets_dialog_description" = "Are you sure you want to reset the widgets? The default widget set with default configurations will be displayed."; "settings__widgets__reset_suggestions" = "Reset Suggestions Cards"; -"settings__notifications__nav_title" = "Background Payments"; -"settings__notifications__intro__title" = "Get Paid\nPassively"; -"settings__notifications__intro__text" = "Turn on notifications to get paid, even when your Bitkit app is closed."; +"settings__notifications__nav_title" = "Notifications"; +"settings__notifications__intro__title" = "Stay\nnotified"; +"settings__notifications__intro__text" = "Turn on notifications to get reminders for your subscriptions and payment requests."; "settings__notifications__intro__button" = "Enable"; -"settings__notifications__settings__toggle" = "Get paid when Bitkit is closed"; -"settings__notifications__settings__enabled" = "Background payments are enabled. You can receive funds even when the app is closed (if your device is connected to the internet)."; -"settings__notifications__settings__disabled" = "Enable background payments to receive funds even when the app is closed (if your device is connected to the internet)."; -"settings__notifications__settings__denied" = "Background payments are disabled, because you have denied notifications."; -"settings__notifications__settings__preview__title" = "Payment Received"; -"settings__notifications__settings__preview__text" = "Open Bitkit to see details"; +"settings__notifications__settings__toggle" = "Allow notifications"; +"settings__notifications__settings__enabled" = "Notifications are on. Bitkit can remind you about subscriptions and payment requests."; +"settings__notifications__settings__disabled" = "Turn on notifications to get reminders for your subscriptions and payment requests."; +"settings__notifications__settings__denied" = "Notifications are off, because you have denied them."; "settings__notifications__settings__preview__time" = "3m ago"; -"settings__notifications__settings__privacy__label" = "Privacy"; -"settings__notifications__settings__privacy__text" = "Include amount in notifications"; "settings__notifications__settings__notifications__label" = "Notifications"; -"settings__notifications__settings__notifications__text" = "To enable background payments, please enable notifications in iOS Bitkit Settings."; +"settings__notifications__settings__notifications__text" = "To get reminders, please enable notifications in iOS Bitkit Settings."; "settings__notifications__settings__button__enabled" = "Customize in {platform} Bitkit Settings"; "settings__notifications__settings__button__disabled" = "Enable in {platform} Bitkit Settings"; "settings__quickpay__nav_title" = "QuickPay"; @@ -1408,27 +1348,11 @@ "wallet__receive_note_placeholder" = "Optional note to payer"; "wallet__receive_show_qr" = "QR Code"; "wallet__receive_text_lnfunds" = "Want to receive Lightning funds?"; -"wallet__receive_background_setup_text" = "Enable background setup to safely exit Bitkit while your balance is being configured."; -"wallet__receive_background_setup_toggle" = "Enable background setup"; "wallet__receive_spending" = "Receive Lightning Funds"; "wallet__receive_spending_text" = "Enjoy instant and cheap bitcoin payments on the Lightning Network."; "wallet__receive_spending_title" = "Receive on spending balance"; -"wallet__receive_connect_initial" = "To set up your spending balance, a {networkFee} network fee and {serviceFee} service provider fee will be deducted."; -"wallet__receive_connect_additional" = "To receive more instant Bitcoin, Bitkit has to increase your liquidity. A {networkFee} network fee and {serviceFee} service provider fee will be deducted from the amount you specified."; -"wallet__receive_liquidity__nav_title" = "Spending Balance Setup"; -"wallet__receive_liquidity__nav_title_additional" = "Increase Receiving Capacity"; -"wallet__receive_liquidity__invalid_quote" = "Bitkit received an invalid liquidity quote. Please try again."; -"wallet__receive_liquidity__text" = "Your Spending Balance uses the Lightning Network to make your payments cheaper, faster, and more private.\n\nThis works like internet access, but you pay for liquidity & routing instead of bandwidth.\n\nThis setup includes some one-time costs."; -"wallet__receive_liquidity__text_additional" = "Your Spending Balance uses the Lightning Network to make your payments cheaper, faster, and more private.\n\nThis works like internet access, but you pay for liquidity & routing instead of bandwidth.\n\nBitkit needs to increase the receiving capacity of your spending balance to process this payment."; -"wallet__receive_liquidity__label" = "Spending Balance Liquidity"; -"wallet__receive_liquidity__label_additional" = "Additional Spending Balance Liquidity"; "wallet__receive_cjit_error" = "Transaction Failed"; "wallet__receive_cjit_error_msg" = "Failed to send funds to your spending account."; -"wallet__receive_cjit_error_max__title" = "Receiving Capacity Maximum"; -"wallet__receive_cjit_error_max__description" = "The maximum you can receive to your spending balance right now is ₿ {amount}."; -"wallet__receive_cjit_error_node_capacity__title" = "Additional Capacity Unavailable"; -"wallet__receive_cjit_error_node_capacity__description" = "Bitkit cannot add more spending capacity to this wallet right now."; -"wallet__receive_will" = "You will receive"; "wallet__receive_ldk_init" = "Spending Balance Initializing..."; "wallet__minimum" = "MINIMUM"; "wallet__activity" = "Activity"; @@ -1617,7 +1541,7 @@ "subscriptions__name_placeholder" = "Subscription name"; "subscriptions__ongoing" = "Ongoing"; "subscriptions__overview" = "Overview"; -"subscriptions__payment_due_description" = "Open Bitkit to review a subscription payment."; +"subscriptions__payment_due_description" = "Open Bitkit to review payment"; "subscriptions__payment_due_title" = "Subscription Payment Due"; "subscriptions__payments" = "Payments"; "subscriptions__per_day" = "per day"; @@ -1852,9 +1776,6 @@ "wallet__onchain_broadcast_rejected_note" = "This payment may still have reached the network. Do not send it again. {reason}"; "wallet__onchain_broadcast_unconfirmed" = "Broadcast unconfirmed"; "wallet__onchain_broadcast_unconfirmed_note" = "The payment may have been sent. Do not send it again."; -"wallet__onchain_funding_rejected" = "The funding transaction was rejected. Do not try to fund this order again."; -"wallet__onchain_funding_unknown" = "The funding transaction may have been sent. Do not try to fund this order again."; -"wallet__onchain_funding_followup_failed" = "Funding was sent, but local order tracking could not be saved. Do not fund this order again."; "wallet__onchain_retry_original" = "Retry original payment"; "wallet__onchain_retry_fee" = "Fee rate (sat/vB)"; @@ -1873,6 +1794,7 @@ "wallet__onchain_abandon_pending" = "The cancellation was sent but is not confirmed yet. Open this payment again later to finish it."; "wallet__onchain_abandon_release" = "Send this payment's coins back to your wallet? Network fee: {fee} sats."; "wallet__onchain_abandon_sync" = "Bitkit could not sync with the network. Check your connection and try again."; +"wallet__onchain_abandon_transfer_note" = "Transfers to spending are no longer available. Bitkit first checks the network, then sends this transfer's coins back to your wallet so the original transfer can never confirm. This costs a network fee."; // USDT "usdt__activity" = "Activity"; "usdt__address" = "Recipient address"; diff --git a/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings b/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings index 1ff18a81a..0d26e0338 100644 --- a/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings @@ -4,9 +4,9 @@ "cards__discount__description" = "Saldo para gastos"; "cards__quickpay__title" = "QuickPay"; "cards__quickpay__description" = "Escanear y pagar"; -"cards__notifications__description" = "Sin Bitkit abierto"; -"cards__notifications__description_alt" = "Cuando Bitkit está cerrado"; -"cards__notifications__title" = "Cobrar"; +"cards__notifications__description" = "Recordatorios de pago"; +"cards__notifications__description_alt" = "Recordatorios de pago"; +"cards__notifications__title" = "Notificaciones"; "cards__invite__title" = "Invitar"; "cards__invite__description" = "Compatir BitKit"; "cards__lightning__title" = "Gastar"; @@ -152,9 +152,7 @@ "common__learn_more" = "Más info."; "common__understood" = "Entendido"; "common__connect" = "Conectar"; -"common__min" = "Min"; "common__max" = "Max"; -"common__default" = "Default"; "common__preview" = "Vista Previa"; "common__show_all" = "Mostrar todo"; "common__show_details" = "Mostrar detalles"; @@ -186,7 +184,6 @@ "fee__slow__range" = "±1-2 horas"; "fee__slow__shortRange" = "1-2h"; "fee__slow__longRange" = "± 1-2 horas"; -"fee__minimum__title" = "Mínimo"; "fee__minimum__longTitle" = "Mínimo"; "fee__minimum__description" = "+2 horas"; "fee__minimum__shortRange" = "+2h"; @@ -199,74 +196,37 @@ "fee__custom__range" = "Depende de la tarifa"; "fee__custom__shortRange" = "Depende de la tarifa"; "fee__custom__longRange" = "Depende de la tasa"; -"lightning__transfer_intro__title" = "Saldo de\nGasto"; -"lightning__transfer_intro__text" = "Fondee el saldo de gastos para disfrutar de transacciones baratas e instantáneas con amigos y comercios."; -"lightning__transfer_intro__button" = "Empezar"; "lightning__funding__nav_title" = "Saldo para gastos"; "lightning__funding__title" = "Fondee su saldo de gastos"; -"lightning__funding__text" = "La forma más fácil es usar tus ahorros de Bitkit o enviar bitcoin desde una billetera diferente."; -"lightning__funding__text_blocked" = "Bitkit no tiene soporte Lightning en tu región, pero puedes usar nodos externos manualmente."; -"lightning__funding__text_blocked_cjit" = "Bitkit no tiene soporte Lightning en tu región, pero puedes usar nodos externos manualmente."; "lightning__funding__button1" = "Transferir desde Ahorros"; "lightning__funding__button2" = "Usar Otra Billetera"; "lightning__funding__button3" = "Configuración Manual"; -"lightning__funding_advanced__nav_title" = "Saldo de gastos"; -"lightning__funding_advanced__title" = "Avanzado configuracion"; -"lightning__funding_advanced__text" = "Escanea un código QR para obtener tu canal LNURL de otro LSP, o selecciona la configuración manual."; -"lightning__funding_advanced__button1" = "QR de canal LNURL"; -"lightning__funding_advanced__button2" = "Configuración Manual"; "lightning__funding_advanced__button_short" = "Configuración avanzada"; -"lightning__no_funds__title" = "Sin fondos disponibles"; -"lightning__no_funds__description" = "Para transferir fondos a su saldo de ahorros, primero tiene que enviar bitcoin a su billetera Bitkit."; -"lightning__no_funds__fund" = "Fondear billetera"; "lightning__transfer__nav_title" = "Transferir fondos"; "lightning__transfer__confirm" = "Por favor,\nconfirme"; "lightning__transfer__custom_fee" = "Tarifa manual"; "lightning__transfer__swipe" = "Desliza para enviar"; "lightning__transfer_hw__broadcast_uncertain_title" = "Es posible que la transacción se haya enviado"; "lightning__transfer_hw__broadcast_uncertain_description" = "Es posible que su transacción de financiamiento ya se haya transmitido. Revise su actividad antes de volver a intentarlo."; -"lightning__transfer_hw__open_connect" = "Abrir Trezor Connect"; "lightning__transfer_hw__reconnect_error_description" = "Vuelva a conectar su dispositivo de hardware; parece que está desconectado de su teléfono."; "lightning__transfer_hw__reconnect_error_title" = "Reconectar dispositivo de hardware"; -"lightning__transfer_hw__sign_title" = "Firme con\nsu dispositivo"; -"lightning__transfer_hw__signed_title" = "Transacción\nfirmada"; "lightning__transfer_to_savings_button" = "Transferir a ahorros"; "lightning__transfer_to_spending_button" = "Transferir a gastos"; -"lightning__spending_intro__title" = "Transferir\na gastos"; -"lightning__spending_intro__text" = "Transfiera fondos a su saldo de gastos para disfrutar de transacciones baratas e instantáneas con amigos y comercios."; -"lightning__spending_intro__button" = "Empezar"; "lightning__transfer_failed__nav_title" = "Falla en transfer"; "lightning__transfer_failed__title" = "Transfer\nfallido"; "lightning__transfer_failed__text" = "Lo sentimos, Bitkit no pudo configurar tu saldo para gastar ni completar la transferencia. Intenta de nuevo."; "lightning__transfer_failed__cancel" = "Revisar error"; "lightning__transfer_failed__continue" = "Intente de nuevo"; -"lightning__spending_amount__title" = "Transferir\na gastos"; "lightning__spending_amount__quarter" = "25%"; "lightning__spending_amount__error_min__title" = "Saldo mínimo de ahorros"; "lightning__spending_amount__error_min__description" = "Se necesita un mínimo de ₿ {amount} para configurar tu saldo para gastar."; -"lightning__spending_amount__error_balance__description" = "Sus ahorros no alcanzan para cubrir esta transferencia y sus comisiones. Intente con un monto menor."; -"lightning__spending_amount__error_balance__title" = "Ahorros insuficientes"; "lightning__spending_amount__error_max__title" = "Saldo máximo de gastos"; "lightning__spending_amount__error_max__description" = "El monto que puedes transferir a tu saldo para gastar está limitado a ₿ {amount}."; "lightning__spending_amount__error_max__description_zero" = "Tu transferencia al saldo para gastos está limitada por la política de liquidez. Visita el Centro de ayuda para más detalles."; "lightning__spending_confirm__network_fee" = "Comisiones de red"; "lightning__spending_confirm__lsp_fee" = "Comisión de servicio"; "lightning__spending_confirm__amount" = "Hacia Gastos"; -"lightning__spending_confirm__background_setup" = "Configurar en segundo plano"; "lightning__spending_confirm__total" = "Total"; -"lightning__spending_confirm__default" = "Usar default"; -"lightning__spending_confirm__fees_changed_title" = "Las comisiones cambiaron"; -"lightning__spending_confirm__fees_changed_service" = "Las comisiones son {amount} más altas ahora que se creó su pedido. Deslice de nuevo para confirmar."; -"lightning__spending_confirm__fees_changed_network" = "Las comisiones son {amount} más altas porque las tarifas o su saldo de ahorros cambiaron mientras estaba en esta pantalla. Deslice de nuevo para confirmar."; -"lightning__spending_advanced__title" = "Capacidad de\nRecibir"; -"lightning__spending_advanced__fee" = "Comisión por liquidez"; -"lightning__spending_advanced__error_balance__description" = "Sus ahorros no alcanzan para cubrir la comisión de liquidez de esta capacidad de recepción. Elija un monto menor."; -"lightning__spending_advanced__error_balance__title" = "Fondos insuficientes"; -"lightning__spending_advanced__error_max__title" = "Capacidad de recepción máxima"; -"lightning__spending_advanced__error_max__description" = "La capacidad de recepción está limitada actualmente a ₿ {amount}."; -"lightning__liquidity__title" = "Liquidez\ny ruteo"; -"lightning__liquidity__text" = "Tu saldo para gastar usa la Red Lightning para que tus pagos sean más baratos, rápidos y privados.\n⠀\nFunciona como el acceso a internet, pero pagas por liquidez y ruteo en lugar de por ancho de banda.\n⠀\nEsta configuración incluye algunos costos únicos."; -"lightning__liquidity__label" = "Liquidez del saldo para gasto"; "lightning__savings_intro__title" = "Transferir\na ahorros"; "lightning__savings_intro__text" = "Transfiere tu saldo de gastos al de ahorros para guardar tus Bitcoin a largo plazo y asegurar tu futuro."; "lightning__savings_intro__button" = "Empezar"; @@ -290,9 +250,6 @@ "lightning__savings_interrupted__nav_title" = "Transfer\nfallido"; "lightning__savings_interrupted__title" = "Manten Bitkit\nabierto"; "lightning__savings_interrupted__text" = "Los fondos todavía no se transfirieron. Bitkit intentará iniciar la transferencia durante los próximos 30 minutos. Por favor, mantenga la aplicación abierta."; -"lightning__transfer_success__nav_title" = "Transferencia exitosa"; -"lightning__transfer_success__title_spending" = "Fondos movidos\na gastos"; -"lightning__transfer_success__text_spending" = "Tus fondos han sido transferidos y tu saldo para gastar está listo para usarse."; "lightning__transfer_success__title_savings" = "Fondos movidos\na gastos"; "lightning__transfer_success__text_savings" = "Sus fondos han sido transferidos. Podrá utilizar estos fondos inmediatamente."; "lightning__external__nav_title" = "Conexión Lightning"; @@ -315,12 +272,6 @@ "lightning__spending_label" = "Saldo para gastos"; "lightning__receiving_label" = "Capacidad de recepción"; "lightning__setting_up_header" = "En proceso"; -"lightning__setting_up_text" = "Por favor, espere, su transferencia está en curso. Esto debería tardar ±10 minutos."; -"lightning__setting_up_step1" = "Procesando pago"; -"lightning__setting_up_step2" = "Pago Efectuado"; -"lightning__setting_up_step3" = "En Fila Para la Apertura"; -"lightning__setting_up_step4" = "Abriendo Conexión"; -"lightning__setting_up_button" = "Continuar Usando Bitkit"; "lightning__node_info" = "Nodo Lightning"; "lightning__node_id" = "ID del nodo LDK"; "lightning__node_disconnected" = "desconectado"; @@ -392,9 +343,9 @@ "lightning__force_init_msg" = "Sus fondos estarán accesibles en ±14 días."; "lightning__force_failed_title" = "Transferencia forzada falló"; "lightning__force_failed_msg" = "No se pudo transferir tus fondos a ahorros. Intenta de nuevo."; -"lightning__force_channels_skipped" = "Algunas conexiones no pudieron cerrarse."; "lightning__channel_opened_title" = "Saldo de gastos listo"; "lightning__channel_opened_msg" = "Ahora puede pagar a cualquier persona, en cualquier lugar y al instante."; +"lightning__funding__button_hw" = "Transferir desde billetera HW"; "lightning__order_given_up_title" = "Falló la Configuración de Pagos Instantáneos"; "lightning__order_given_up_msg" = "El proceso de configuración expiró. Intenta de nuevo."; "lightning__order_expired_title" = "Falló la Configuración de Pagos Instantáneos"; @@ -413,6 +364,9 @@ "lightning__order_state__open" = "Conexión abierta"; "lightning__order_state__inactive" = "Conexión inactiva"; "lightning__peer_disconnected" = "Par desconectado."; +"lightning__transfer_unavailable__nav_title" = "Transferencia no disponible"; +"lightning__transfer_unavailable__text" = "Las transferencias automáticas a tu saldo de gasto ya no están disponibles. Usa tu propio nodo o configura una conexión con otro proveedor."; +"lightning__transfer_unavailable__title" = "Configuración manual\nnecesaria"; "lightning__wait_text_top" = "Por favor, espere a que Bitkit se conecte a la red de pagos (±10 seg.)."; "lightning__wait_text_bottom" = "Conectado y sincronizando..."; "onboarding__tos_header" = "Bitkit\ntérminos de uso"; @@ -434,7 +388,6 @@ "onboarding__slide1_header" = "Pagos\nal instante"; "onboarding__slide1_text" = "Gasta bitcoin más rápido que nunca. Disfruta pagos instantáneos y baratos con amigos, familia y comercios."; "onboarding__slide1_text_alt" = "Disfruta de pagos instantáneos y económicos con amigos, familiares y comerciantes en la red Lightning."; -"onboarding__slide1_note" = "Bitkit no tiene soporte Lightning en tu región, pero puedes usar nodos externos manualmente."; "onboarding__slide2_header" = "Bitcoiners\nsin fronteras"; "onboarding__slide2_text" = "Toma el control de tu vida digital con perfiles portables y contactos con pago."; "onboarding__slide3_header" = "La privacidad\nno es un delito"; @@ -557,8 +510,6 @@ "other__lnurl_withdr_error_minmax" = "Parámetros de retiro LNURL incorrectos, mín/máx no ajustados correctamente."; "other__lnurl_withdr_success_title" = "Retiro solicitado"; "other__lnurl_withdr_success_msg" = "Su retiro ha sido solicitado con éxito. Esperando el pago."; -"other__notification_registration_failed_description" = "Bitkit no pudo registrarse para notificaciones push."; -"other__notification_registration_failed_title" = "Error en el registro de notificaciones"; "other__phone_settings" = "Abrir Ajustes del teléfono"; "other__bt_error_retrieve" = "Transferencia fallida"; "other__bt_error_connect" = "No se puede añadir el nodo LSP como par en este momento."; @@ -574,14 +525,7 @@ "other__transfer_notification__body" = "Abra Bitkit para completar su transferencia"; "other__scan__error__generic" = "Bitkit no puede leer los datos proporcionados."; "other__scan__error__expired" = "Esta factura lightning ha caducado."; -"other__gift__claimed__text" = "¡Lo sentimos, llegaste tarde! Todos los regalos para este código ya han sido reclamados."; -"other__gift__claiming__text" = "Canjeando su código de regalo Bitkit..."; -"other__gift__claiming__title" = "Canjeando regalo"; -"other__gift__error__title" = "Error de código de regalo"; -"other__gift__error__text" = "Bitkit no pudo recibir los fondos. Por favor, intente de nuevo más tarde o póngase en contacto con nosotros."; "other__gift__error__button" = "OK"; -"other__gift__used__title" = "Código usado"; -"other__gift__used__text" = "Este código de regalo de Bitkit ya ha sido utilizado y los fondos han sido pagados."; "other__gift__used__button" = "OK"; "other__shop__intro__title" = "Tienda"; "other__shop__intro__description" = "Adopta el estándar Bitcoin. Gasta tus Bitcoin en tarjetas de regalo digitales, eSIMs, recargas telefónicas y más."; @@ -764,6 +708,7 @@ "pubky_auth__homeserver" = "Homeserver"; "pubky_auth__signup_description" = "Cree una nueva identidad Pubky en este homeserver. Continúe solo si confía en él."; "pubky_auth__approval_failed" = "Falló la autorización"; +"wallet__onchain_abandon_transfer_note" = "Las transferencias al saldo de gasto ya no están disponibles. Bitkit primero revisa la red y luego devuelve las monedas de esta transferencia a tu billetera para que la transferencia original nunca se confirme. Esto tiene una comisión de red."; "watch_only_accounts__active_section" = "Cuentas activas"; "watch_only_accounts__copy_xpub" = "Copiar xpub"; "watch_only_accounts__description" = "Cada servidor de Paykit aprobado recibe una cuenta de Bitcoin separada. Desactive el seguimiento para descargar una cuenta sin eliminar el historial de su billetera."; @@ -840,21 +785,17 @@ "settings__widgets__reset_widgets_dialog_title" = "¿Restablecer widgets?"; "settings__widgets__reset_widgets_dialog_description" = "¿Está seguro de que desea restablecer los widgets? Se mostrará el conjunto de widgets predeterminado con las configuraciones predeterminadas."; "settings__widgets__reset_suggestions" = "Restablecer tarjetas de sugerencias"; -"settings__notifications__nav_title" = "Pagos en segundo plano"; -"settings__notifications__intro__title" = "Cobrar\nPasivamente"; -"settings__notifications__intro__text" = "Active las notificaciones para cobrar, incluso cuando su aplicación Bitkit esté cerrada."; +"settings__notifications__nav_title" = "Notificaciones"; +"settings__notifications__intro__title" = "Mantente\ninformado"; +"settings__notifications__intro__text" = "Activa las notificaciones para recibir recordatorios de tus suscripciones y solicitudes de pago."; "settings__notifications__intro__button" = "Habilitar"; -"settings__notifications__settings__toggle" = "Cobrar mientras Bitkit está cerrado"; -"settings__notifications__settings__enabled" = "Los pagos en segundo plano están habilitados. Podrá recibir fondos incluso cuando la aplicación esté cerrada (si su dispositivo está conectado a Internet)."; -"settings__notifications__settings__disabled" = "Habilite los pagos en segundo plano para recibir fondos incluso cuando la aplicación esté cerrada (si su dispositivo está conectado a Internet)."; -"settings__notifications__settings__denied" = "Los pagos en segundo plano están desactivados, porque usted ha denegado las notificaciones."; -"settings__notifications__settings__preview__title" = "Pago recibido"; -"settings__notifications__settings__preview__text" = "Abra Bitkit para ver los detalles"; +"settings__notifications__settings__toggle" = "Permitir notificaciones"; +"settings__notifications__settings__enabled" = "Las notificaciones están activadas. Bitkit puede recordarte tus suscripciones y solicitudes de pago."; +"settings__notifications__settings__disabled" = "Activa las notificaciones para recibir recordatorios de tus suscripciones y solicitudes de pago."; +"settings__notifications__settings__denied" = "Las notificaciones están desactivadas porque las rechazaste."; "settings__notifications__settings__preview__time" = "hace 3m"; -"settings__notifications__settings__privacy__label" = "Privacidad"; -"settings__notifications__settings__privacy__text" = "Incluir el importe en las notificaciones"; "settings__notifications__settings__notifications__label" = "Notificaciones"; -"settings__notifications__settings__notifications__text" = "Para habilitar los pagos en segundo plano, habilite las notificaciones en los Ajustes de Bitkit de iOS."; +"settings__notifications__settings__notifications__text" = "Para recibir recordatorios, habilita las notificaciones en los Ajustes de Bitkit de iOS."; "settings__notifications__settings__button__enabled" = "Personalice en {platform} Configuración de Bitkit"; "settings__notifications__settings__button__disabled" = "Habilitar en {platform} Configuración de Bitkit"; "settings__quickpay__nav_title" = "QuickPay"; @@ -1397,27 +1338,11 @@ "wallet__receive_note_placeholder" = "Nota opcional para el pagador"; "wallet__receive_show_qr" = "Mostrar código QR"; "wallet__receive_text_lnfunds" = "Quiero recibir"; -"wallet__receive_background_setup_text" = "Activa la configuración en segundo plano para salir de Bitkit de forma segura mientras se configura tu saldo."; -"wallet__receive_background_setup_toggle" = "Activar configuración en segundo plano"; "wallet__receive_spending" = "Recibir en Saldo de Gastos"; "wallet__receive_spending_text" = "Disfrute de pagos de bitcoin instantáneos y baratos en la red Lightning."; "wallet__receive_spending_title" = "Recibir en saldo de gastos"; -"wallet__receive_connect_initial" = "Para configurar tu saldo para gastar, se descontará una comisión de red de {networkFee} y una comisión del proveedor de servicio de {serviceFee}."; -"wallet__receive_connect_additional" = "Para recibir más Bitcoin al instante, Bitkit necesita aumentar tu liquidez. Se descontará una comisión de red de {networkFee} y una comisión del proveedor de servicio de {serviceFee} del monto que especificaste."; -"wallet__receive_liquidity__nav_title" = "Configuración del saldo de gastos"; -"wallet__receive_liquidity__nav_title_additional" = "Aumentar la capacidad de recepción"; -"wallet__receive_liquidity__invalid_quote" = "Bitkit recibió una cotización de liquidez no válida. Inténtalo de nuevo."; -"wallet__receive_liquidity__text" = "Tu saldo para gastar usa la red Lightning para que tus pagos sean más baratos, rápidos y privados.\n⠀\nFunciona como el acceso a internet, pero pagas por liquidez y ruteo en lugar de por ancho de banda.\n⠀\nEsta configuración incluye algunos costos únicos."; -"wallet__receive_liquidity__text_additional" = "Su saldo de gastos utiliza la Lightning Network para que sus pagos sean más baratos, más rápidos y más privados.\n\nFunciona como el acceso a Internet, pero usted paga por la liquidez y el enrutamiento en lugar del ancho de banda.\n\nBitkit necesita aumentar la capacidad de recepción de su saldo de gastos para procesar este pago."; -"wallet__receive_liquidity__label" = "Liquidez del saldo para gastar"; -"wallet__receive_liquidity__label_additional" = "Liquidez adicional del saldo para gastar"; "wallet__receive_cjit_error" = "Transacción fallida"; "wallet__receive_cjit_error_msg" = "No se han podido enviar fondos a su cuenta de gastos."; -"wallet__receive_cjit_error_max__title" = "Capacidad de recepción máxima"; -"wallet__receive_cjit_error_max__description" = "Lo máximo que puede recibir en su saldo de gastos en este momento es ₿ {amount}."; -"wallet__receive_cjit_error_node_capacity__title" = "Capacidad adicional no disponible"; -"wallet__receive_cjit_error_node_capacity__description" = "Bitkit no puede agregar más capacidad de gastos a esta billetera en este momento."; -"wallet__receive_will" = "Recibirás"; "wallet__receive_ldk_init" = "Saldo de gastos inicializando..."; "wallet__minimum" = "MÍNIMO"; "wallet__activity" = "Actividad"; @@ -1606,7 +1531,7 @@ "subscriptions__name_placeholder" = "Nombre de la suscripción"; "subscriptions__ongoing" = "En curso"; "subscriptions__overview" = "Resumen"; -"subscriptions__payment_due_description" = "Abra Bitkit para revisar un pago de suscripción."; +"subscriptions__payment_due_description" = "Abra Bitkit para revisar el pago"; "subscriptions__payment_due_title" = "Pago de suscripción pendiente"; "subscriptions__payments" = "Pagos"; "subscriptions__per_day" = "por día"; @@ -1778,9 +1703,6 @@ "wallet__onchain_broadcast_rejected_note" = "Es posible que este pago haya llegado a la red de todos modos. No lo envíe de nuevo. {reason}"; "wallet__onchain_broadcast_unconfirmed" = "Transmisión no confirmada"; "wallet__onchain_broadcast_unconfirmed_note" = "Es posible que el pago se haya enviado. No lo envíe de nuevo."; -"wallet__onchain_funding_rejected" = "La transacción de financiamiento fue rechazada. No intente financiar este pedido de nuevo."; -"wallet__onchain_funding_unknown" = "Es posible que la transacción de financiamiento se haya enviado. No intente financiar este pedido de nuevo."; -"wallet__onchain_funding_followup_failed" = "El financiamiento se envió, pero no se pudo guardar el seguimiento local del pedido. No vuelva a financiar este pedido."; "wallet__onchain_retry_original" = "Reintentar pago original"; "wallet__onchain_retry_fee" = "Tarifa (sat/vB)"; "wallet__onchain_retry_note" = "Reintentar {amount} sats a {address} con las mismas monedas originales. El monto del destinatario no cambia. Introduzca la tarifa de red en sat/vB. Esto no puede crear un pago aparte."; diff --git a/Bitkit/Resources/Localization/es.lproj/Localizable.strings b/Bitkit/Resources/Localization/es.lproj/Localizable.strings index 1156dd6af..d4f9d2624 100644 --- a/Bitkit/Resources/Localization/es.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/es.lproj/Localizable.strings @@ -3,8 +3,8 @@ "cards__discount__description" = "Saldo de gastos"; "cards__quickpay__title" = "QuickPay"; "cards__quickpay__description" = "Escanea y paga"; -"cards__notifications__description_alt" = "Cuando Bitkit está cerrado"; -"cards__notifications__title" = "Recibir Pagos"; +"cards__notifications__description_alt" = "Recordatorios de pago"; +"cards__notifications__title" = "Notificaciones"; "cards__invite__title" = "Invitar"; "cards__invite__description" = "Comparta Bitkit"; "cards__lightning__title" = "Gastar"; @@ -149,9 +149,7 @@ "common__learn_more" = "Aprender Más"; "common__understood" = "Entendido"; "common__connect" = "Conectar"; -"common__min" = "Min"; "common__max" = "Max"; -"common__default" = "Predeterminado"; "common__preview" = "Vista previa"; "common__show_all" = "Mostrar todo"; "common__show_details" = "Mostrar detalles"; @@ -183,7 +181,6 @@ "fee__slow__range" = "±1-2 horas"; "fee__slow__shortRange" = "1-2h"; "fee__slow__longRange" = "± 1-2 horas"; -"fee__minimum__title" = "Mínimo"; "fee__minimum__longTitle" = "Mínima"; "fee__minimum__description" = "+2 horas"; "fee__minimum__shortRange" = "+2h"; @@ -196,66 +193,29 @@ "fee__custom__range" = "Depende de la comisión"; "fee__custom__shortRange" = "Depende de la tarifa"; "fee__custom__longRange" = "Depende de la comisión"; -"lightning__transfer_intro__title" = "Saldo de\nGasto"; -"lightning__transfer_intro__text" = "Rellene el saldo de gasto para disfrutar de transacciones baratas e instantáneas con amigos, familia y comerciantes."; -"lightning__transfer_intro__button" = "Empezar"; "lightning__funding__nav_title" = "Saldo de gastos"; "lightning__funding__title" = "Rellene su saldo de gastos"; -"lightning__funding__text" = "La forma más fácil es usar tus ahorros de Bitkit o enviar bitcoin desde un monedero diferente."; -"lightning__funding__text_blocked" = "Bitkit actualmente no proporciona servicios Lightning en su país, pero aún puede conectarse a otros nodos directamente."; -"lightning__funding__text_blocked_cjit" = "Bitkit actualmente no proporciona servicios Lightning en su país, pero aún puede conectarse a otros nodos directamente."; "lightning__funding__button1" = "Transferir desde Ahorros"; "lightning__funding__button2" = "Usar Otro Monedero"; "lightning__funding__button3" = "Configuración Manual"; -"lightning__funding_advanced__nav_title" = "Saldo de gastos"; -"lightning__funding_advanced__title" = "Configuración avanzada"; -"lightning__funding_advanced__text" = "Escanee un QR para reclamar su Canal LNURL de otro LSP, o elija la configuración manual."; -"lightning__funding_advanced__button1" = "QR de Canal LNURL"; -"lightning__funding_advanced__button2" = "Configuración Manual"; "lightning__funding_advanced__button_short" = "Configuración avanzada"; -"lightning__no_funds__title" = "No hay fondos disponibles"; -"lightning__no_funds__description" = "Antes de que pueda transferir fondos a su saldo de ahorros, tiene que enviar bitcoin a su monedero Bitkit."; -"lightning__no_funds__fund" = "Añadir fondos"; "lightning__transfer__nav_title" = "Transferir fondos"; "lightning__transfer__confirm" = "Por favor,\nconfirme"; "lightning__transfer__swipe" = "Deslizar para transferir"; "lightning__transfer_hw__broadcast_uncertain_title" = "Puede que la transacción se haya enviado"; "lightning__transfer_hw__broadcast_uncertain_description" = "Es posible que su transacción de financiación se haya difundido. Revise su actividad antes de volver a intentarlo."; -"lightning__transfer_hw__open_connect" = "Abrir Trezor Connect"; "lightning__transfer_hw__reconnect_error_description" = "Vuelva a conectar su dispositivo de hardware; parece que se ha desconectado de su teléfono."; "lightning__transfer_hw__reconnect_error_title" = "Reconecte el dispositivo de hardware"; -"lightning__transfer_hw__sign_title" = "Firme con\nsu dispositivo"; -"lightning__transfer_hw__signed_title" = "Transacción\nfirmada"; "lightning__transfer_to_savings_button" = "Transferir a Ahorros"; "lightning__transfer_to_spending_button" = "Transferir a Gasto"; -"lightning__spending_intro__title" = "Transferir\na gasto"; -"lightning__spending_intro__text" = "Transfiera fondos a su saldo de gasto para disfrutar de transacciones baratas e instantáneas con amigos, familia y comerciantes."; -"lightning__spending_intro__button" = "Empezar"; "lightning__transfer_failed__continue" = "Inténtelo de nuevo"; -"lightning__spending_amount__title" = "Transferir\na gasto"; "lightning__spending_amount__quarter" = "25%"; -"lightning__spending_amount__error_balance__description" = "Sus ahorros no cubren esta transferencia y sus comisiones. Pruebe con una cantidad menor."; -"lightning__spending_amount__error_balance__title" = "Ahorros Insuficientes"; "lightning__spending_amount__error_max__title" = "Máximo del Saldo de Gastos"; "lightning__spending_amount__error_max__description" = "La cantidad que puedes transferir a tu saldo de gastos está actualmente limitada a ₿ {amount}."; "lightning__spending_confirm__network_fee" = "Comisión de red"; "lightning__spending_confirm__lsp_fee" = "Comisión de servicio"; "lightning__spending_confirm__amount" = "A gasto"; -"lightning__spending_confirm__background_setup" = "Configurar en segundo plano"; "lightning__spending_confirm__total" = "Total"; -"lightning__spending_confirm__default" = "Usar Predeterminados"; -"lightning__spending_confirm__fees_changed_title" = "Las comisiones han cambiado"; -"lightning__spending_confirm__fees_changed_service" = "Las comisiones son {amount} más altas ahora que se ha creado su pedido. Deslice de nuevo para confirmar."; -"lightning__spending_confirm__fees_changed_network" = "Las comisiones son {amount} más altas porque las tasas de comisión o su saldo de ahorro cambiaron mientras estaba en esta pantalla. Deslice de nuevo para confirmar."; -"lightning__spending_advanced__title" = "Capacidad de\nrecepción"; -"lightning__spending_advanced__fee" = "Comisión de liquidez"; -"lightning__spending_advanced__error_balance__description" = "Sus ahorros no cubren la comisión de liquidez de esta capacidad de recepción. Elija una cantidad menor."; -"lightning__spending_advanced__error_balance__title" = "Fondos insuficientes"; -"lightning__spending_advanced__error_max__title" = "Capacidad de recepción máxima"; -"lightning__spending_advanced__error_max__description" = "La capacidad de recepción está limitada actualmente a ₿ {amount}."; -"lightning__liquidity__title" = "Liquidez\ny enrutamiento"; -"lightning__liquidity__text" = "Su Saldo de Gasto usa la Red Lightning para hacer sus pagos más baratos, rápidos y privados.\n\nEsto funciona como el acceso a Internet, pero paga por liquidez y enrutamiento en lugar de ancho de banda.\n\nEsta configuración incluye algunos costos únicos."; -"lightning__liquidity__label" = "Liquidez del Saldo de Gasto"; "lightning__savings_intro__title" = "Transferir\na ahorros"; "lightning__savings_intro__text" = "Transfiera su saldo de gasto a sus ahorros para almacenar su Bitcoin a largo plazo y asegurar su futuro."; "lightning__savings_intro__button" = "Empezar"; @@ -279,9 +239,6 @@ "lightning__savings_interrupted__nav_title" = "Transferencia\ninterrumpida"; "lightning__savings_interrupted__title" = "Mantener Bitkit\nabierto"; "lightning__savings_interrupted__text" = "Los fondos todavía no se transfirieron. Bitkit intentará iniciar la transferencia durante los próximos 30 minutos. Por favor, mantenga la aplicación abierta."; -"lightning__transfer_success__nav_title" = "Transferencia exitosa"; -"lightning__transfer_success__title_spending" = "Fondos movidos\na gasto"; -"lightning__transfer_success__text_spending" = "Sus fondos han sido transferidos y su saldo de gasto está listo para usar."; "lightning__transfer_success__title_savings" = "Fondos movidos\na ahorros"; "lightning__transfer_success__text_savings" = "Sus fondos han sido transferidos. Puede usar estos fondos inmediatamente."; "lightning__external__nav_title" = "Conexión Lightning"; @@ -302,12 +259,6 @@ "lightning__savings" = "Ahorro"; "lightning__spending_label" = "Saldo para gastos"; "lightning__receiving_label" = "Capacidad de recepción"; -"lightning__setting_up_text" = "Por favor espere, su transferencia de fondos está en progreso. Esto debería tomar ±10 minutos."; -"lightning__setting_up_step1" = "Procesando Pago"; -"lightning__setting_up_step2" = "Pago Exitoso"; -"lightning__setting_up_step3" = "En Cola para Apertura"; -"lightning__setting_up_step4" = "Abriendo Conexión"; -"lightning__setting_up_button" = "Continuar Usando Bitkit"; "lightning__node_info" = "Nodo Lightning"; "lightning__node_id" = "ID del nodo LDK"; "lightning__node_disconnected" = "desconectado"; @@ -376,9 +327,9 @@ "lightning__force_init_msg" = "Sus fondos estarán accesibles en ±14 días."; "lightning__force_failed_title" = "Transferencia forzada falló"; "lightning__force_failed_msg" = "No se pudieron transferir los fondos de vuelta a ahorros. Por favor, inténtelo de nuevo."; -"lightning__force_channels_skipped" = "Algunas conexiones no se pudieron cerrar."; "lightning__channel_opened_title" = "Saldo de Gasto Listo"; "lightning__channel_opened_msg" = "Ahora puede pagar a cualquier persona, en cualquier lugar y al instante."; +"lightning__funding__button_hw" = "Transferir desde billetera HW"; "lightning__order_given_up_title" = "Error en la Configuración de Pago Instántaneo"; "lightning__order_expired_title" = "Error en la Configuración de Pago Instántaneo"; "lightning__order_state__awaiting_payment" = "Procesando pago"; @@ -392,6 +343,9 @@ "lightning__order_state__open" = "Conexión abierta"; "lightning__order_state__inactive" = "Conexión inactiva"; "lightning__peer_disconnected" = "Par desconectado."; +"lightning__transfer_unavailable__nav_title" = "Transferencia no disponible"; +"lightning__transfer_unavailable__text" = "Las transferencias automáticas a tu saldo de gasto ya no están disponibles. Usa tu propio nodo o configura una conexión con otro proveedor."; +"lightning__transfer_unavailable__title" = "Configuración manual\nnecesaria"; "lightning__wait_text_top" = "Por favor, espere a que Bitkit se conecte a la red de pagos (±10 segundos)."; "lightning__wait_text_bottom" = "Conectando y Sincronizando..."; "onboarding__tos_header" = "Términos de uso\nde Bitkit"; @@ -411,7 +365,6 @@ "onboarding__slide0_text_alt" = "Bitkit te da las llaves para controlar tu dinero, perfil y contactos. Toma el control y conviértete en ciudadano del mundo."; "onboarding__slide1_header" = "Pagos\ninstantáneos"; "onboarding__slide1_text_alt" = "Disfruta de pagos instantáneos y baratos con amigos, familia y comerciantes en la Red Lightning."; -"onboarding__slide1_note" = "*Bitkit actualmente no proporciona servicios Lightning en su país, pero aún puede conectarse a otros nodos."; "onboarding__slide2_header" = "Bitcoineros,\nsin fronteras"; "onboarding__slide3_header" = "La privacidad\nno es un crimen"; "onboarding__slide3_text_alt" = "Tu dinero, tu privacidad. Desliza para ocultar tu saldo y gasta de forma más privada, sin rastreo de datos y sin KYC."; @@ -519,8 +472,6 @@ "other__lnurl_withdr_error_minmax" = "Parámetros de retiro LNURL incorrectos, min/max no están configurados correctamente."; "other__lnurl_withdr_success_title" = "Retirada solicitada"; "other__lnurl_withdr_success_msg" = "Su retiro fue solicitado exitosamente. Esperando el pago."; -"other__notification_registration_failed_description" = "Bitkit no pudo registrarse para notificaciones push."; -"other__notification_registration_failed_title" = "Error en el Registro de Notificaciones"; "other__phone_settings" = "Abrir Ajustes del Teléfono"; "other__bt_error_retrieve" = "Transferencia Fallida"; "other__bt_channel_purchase_cost_error" = "Necesita {delta} más para completar esta transacción."; @@ -532,14 +483,7 @@ "other__try_again" = "Por favor, intente de nuevo."; "other__scan__error__generic" = "Bitkit no puede leer los datos proporcionados."; "other__scan__error__expired" = "Esta factura Lightning ha expirado."; -"other__gift__claimed__text" = "Lo sentimos, ¡llegas tarde! Todos los regalos para este código ya han sido reclamados."; -"other__gift__claiming__text" = "Reclamando tu código de regalo de Bitkit..."; -"other__gift__claiming__title" = "Reclamando Regalo"; -"other__gift__error__title" = "Error en el Código de Regalo"; -"other__gift__error__text" = "Bitkit no pudo reclamar los fondos. Por favor, inténtelo más tarde o contáctenos."; "other__gift__error__button" = "OK"; -"other__gift__used__title" = "Código Usado"; -"other__gift__used__text" = "Este código de regalo de Bitkit ya ha sido usado, y los fondos ya han sido pagados."; "other__gift__used__button" = "OK"; "other__shop__intro__title" = "Tienda"; "other__shop__intro__description" = "Vive tu vida con el estándar Bitcoin. Gasta tu Bitcoin en tarjetas regalo digitales, eSIMs, recargas de teléfono y más."; @@ -710,6 +654,7 @@ "pubky_auth__homeserver" = "Servidor principal"; "pubky_auth__signup_description" = "Cree una nueva identidad Pubky en este servidor principal. Continúe solo si confía en él."; "pubky_auth__approval_failed" = "Error de autorización"; +"wallet__onchain_abandon_transfer_note" = "Las transferencias al saldo de gasto ya no están disponibles. Bitkit primero comprueba la red y después devuelve las monedas de esta transferencia a tu billetera para que la transferencia original nunca se confirme. Esto tiene una comisión de red."; "watch_only_accounts__active_section" = "Cuentas activas"; "watch_only_accounts__copy_xpub" = "Copiar xpub"; "watch_only_accounts__description" = "Cada servidor de Paykit aprobado recibe una cuenta de Bitcoin independiente. Desactive el seguimiento para descargar una cuenta sin eliminar el historial de su monedero."; @@ -786,21 +731,17 @@ "settings__widgets__reset_widgets_dialog_title" = "¿Restablecer widgets?"; "settings__widgets__reset_widgets_dialog_description" = "¿Estás seguro de que deseas restablecer los widgets? Se mostrará el conjunto de widgets predeterminado con las configuraciones predeterminadas."; "settings__widgets__reset_suggestions" = "Restablecer tarjetas de sugerencias"; -"settings__notifications__nav_title" = "Pagos en Segundo Plano"; -"settings__notifications__intro__title" = "Recibe Pagos\nPasivamente"; -"settings__notifications__intro__text" = "Activa las notificaciones para recibir pagos, incluso cuando tu app Bitkit esté cerrada."; +"settings__notifications__nav_title" = "Notificaciones"; +"settings__notifications__intro__title" = "Mantente\ninformado"; +"settings__notifications__intro__text" = "Activa las notificaciones para recibir recordatorios de tus suscripciones y solicitudes de pago."; "settings__notifications__intro__button" = "Activar"; -"settings__notifications__settings__toggle" = "Recibe pagos cuando Bitkit esté cerrado"; -"settings__notifications__settings__enabled" = "Los pagos en segundo plano están activados. Puedes recibir fondos incluso cuando la app esté cerrada (si tu dispositivo está conectado a Internet)."; -"settings__notifications__settings__disabled" = "Activa los pagos en segundo plano para recibir fondos incluso cuando la app esté cerrada (si tu dispositivo está conectado a Internet)."; -"settings__notifications__settings__denied" = "Los pagos en segundo plano están desactivados porque has denegado las notificaciones."; -"settings__notifications__settings__preview__title" = "Pago Recibido"; -"settings__notifications__settings__preview__text" = "Abre Bitkit para ver los detalles"; +"settings__notifications__settings__toggle" = "Permitir notificaciones"; +"settings__notifications__settings__enabled" = "Las notificaciones están activadas. Bitkit puede recordarte tus suscripciones y solicitudes de pago."; +"settings__notifications__settings__disabled" = "Activa las notificaciones para recibir recordatorios de tus suscripciones y solicitudes de pago."; +"settings__notifications__settings__denied" = "Las notificaciones están desactivadas porque las rechazaste."; "settings__notifications__settings__preview__time" = "Hace 3m"; -"settings__notifications__settings__privacy__label" = "Privacidad"; -"settings__notifications__settings__privacy__text" = "Incluir cantidad en las notificaciones"; "settings__notifications__settings__notifications__label" = "Notificaciones"; -"settings__notifications__settings__notifications__text" = "Para activar los pagos en segundo plano, por favor activa las notificaciones en los Ajustes de Bitkit en iOS."; +"settings__notifications__settings__notifications__text" = "Para recibir recordatorios, activa las notificaciones en los Ajustes de Bitkit en iOS."; "settings__notifications__settings__button__enabled" = "Personalizar en Ajustes de Bitkit en {platform}"; "settings__notifications__settings__button__disabled" = "Activar en Ajustes de Bitkit en {platform}"; "settings__quickpay__nav_title" = "QuickPay"; @@ -1294,27 +1235,11 @@ "wallet__receive_lightning_invoice" = "Factura Lightning"; "wallet__receive_note_placeholder" = "Nota opcional para el pagador"; "wallet__receive_show_qr" = "Mostrar código QR"; -"wallet__receive_background_setup_text" = "Activa la configuración en segundo plano para salir de Bitkit de forma segura mientras tu saldo se está configurando."; -"wallet__receive_background_setup_toggle" = "Activar configuración en segundo plano"; "wallet__receive_spending" = "Recibir Fondos Lightning"; "wallet__receive_spending_text" = "Disfrute de pagos de bitcoin instantáneos y baratos en la red Lightning."; "wallet__receive_spending_title" = "Recibir en saldo de gasto"; -"wallet__receive_connect_initial" = "Para configurar tu saldo de gasto, se deducirá una comisión de red de {networkFee} y una comisión del proveedor de servicio de {serviceFee}."; -"wallet__receive_connect_additional" = "Para recibir más Bitcoin instantáneo, Bitkit tiene que aumentar tu liquidez. Una comisión de red de {networkFee} y una comisión del proveedor de servicio de {serviceFee} se deducirán de la cantidad que especificaste."; -"wallet__receive_liquidity__nav_title" = "Configuración del Saldo de Gasto"; -"wallet__receive_liquidity__nav_title_additional" = "Incrementar Capacidad de Recepción"; -"wallet__receive_liquidity__invalid_quote" = "Bitkit recibió una cotización de liquidez no válida. Inténtalo de nuevo."; -"wallet__receive_liquidity__text" = "Tu Saldo de Gasto usa la Red Lightning para hacer tus pagos más baratos, rápidos y privados.\n\nEsto funciona como el acceso a Internet, pero pagas por liquidez y enrutamiento en lugar de ancho de banda.\n\nEsta configuración incluye algunos costos únicos."; -"wallet__receive_liquidity__text_additional" = "Tu Saldo de Gasto usa la Red Lightning para hacer tus pagos más baratos, rápidos y privados.\n\nEsto funciona como el acceso a Internet, pero pagas por liquidez y enrutamiento en lugar de ancho de banda.\n\nBitkit necesita aumentar la capacidad de recepción de tu saldo de gasto para procesar este pago."; -"wallet__receive_liquidity__label" = "Liquidez del Saldo de Gasto"; -"wallet__receive_liquidity__label_additional" = "Liquidez Adicional del Saldo de Gasto"; "wallet__receive_cjit_error" = "Transacción ha fallado"; "wallet__receive_cjit_error_msg" = "Fallo al enviar fondos a la cuenta de gastos."; -"wallet__receive_cjit_error_max__title" = "Capacidad de recepción máxima"; -"wallet__receive_cjit_error_max__description" = "El máximo que puede recibir en su saldo de gasto en este momento es ₿ {amount}."; -"wallet__receive_cjit_error_node_capacity__title" = "Capacidad adicional no disponible"; -"wallet__receive_cjit_error_node_capacity__description" = "Bitkit no puede añadir más capacidad de gasto a esta cartera en este momento."; -"wallet__receive_will" = "Recibirás"; "wallet__minimum" = "MÍNIMO"; "wallet__activity" = "Actividad"; "wallet__activity_show_all" = "Mostrar toda la actividad"; @@ -1498,7 +1423,7 @@ "subscriptions__name_placeholder" = "Nombre de la suscripción"; "subscriptions__ongoing" = "En curso"; "subscriptions__overview" = "Resumen"; -"subscriptions__payment_due_description" = "Abra Bitkit para revisar un pago de suscripción."; +"subscriptions__payment_due_description" = "Abra Bitkit para revisar el pago"; "subscriptions__payment_due_title" = "Pago de suscripción pendiente"; "subscriptions__payments" = "Pagos"; "subscriptions__per_day" = "al día"; @@ -1655,9 +1580,6 @@ "wallet__onchain_broadcast_rejected_note" = "Es posible que este pago haya llegado a la red. No lo envíe de nuevo. {reason}"; "wallet__onchain_broadcast_unconfirmed" = "Difusión sin confirmar"; "wallet__onchain_broadcast_unconfirmed_note" = "Es posible que el pago se haya enviado. No lo envíe de nuevo."; -"wallet__onchain_funding_rejected" = "La transacción de financiación fue rechazada. No intente financiar este pedido de nuevo."; -"wallet__onchain_funding_unknown" = "Es posible que la transacción de financiación se haya enviado. No intente financiar este pedido de nuevo."; -"wallet__onchain_funding_followup_failed" = "La financiación se envió, pero no se pudo guardar el seguimiento local del pedido. No vuelva a financiar este pedido."; "wallet__onchain_retry_original" = "Reintentar el pago original"; "wallet__onchain_retry_fee" = "Tasa de comisión (sat/vB)"; "wallet__onchain_retry_note" = "Reintentar {amount} sats a {address} con las mismas monedas originales. La cantidad del destinatario no cambia. Introduzca la tasa de comisión de red en sat/vB. Esto no puede crear un pago aparte."; diff --git a/Bitkit/Resources/Localization/fr.lproj/Localizable.strings b/Bitkit/Resources/Localization/fr.lproj/Localizable.strings index a4340900c..49667134b 100644 --- a/Bitkit/Resources/Localization/fr.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/fr.lproj/Localizable.strings @@ -4,9 +4,9 @@ "cards__discount__description" = "Solde Dépenses"; "cards__quickpay__title" = "QuickPay"; "cards__quickpay__description" = "Scannez et payez"; -"cards__notifications__description" = "Sans ouvrir Bitkit"; -"cards__notifications__description_alt" = "Lorsque Bitkit est fermé"; -"cards__notifications__title" = "Soyez rémunéré"; +"cards__notifications__description" = "Rappels de paiement"; +"cards__notifications__description_alt" = "Rappels de paiement"; +"cards__notifications__title" = "Notifications"; "cards__invite__title" = "Inviter"; "cards__invite__description" = "Partager Bitkit"; "cards__lightning__title" = "Dépenser"; @@ -152,9 +152,7 @@ "common__learn_more" = "En savoir plus"; "common__understood" = "Compris"; "common__connect" = "Connecter"; -"common__min" = "Min"; "common__max" = "Max"; -"common__default" = "Défaut"; "common__preview" = "Aperçu"; "common__show_all" = "Tout afficher"; "common__show_details" = "Afficher les détails"; @@ -186,7 +184,6 @@ "fee__slow__range" = "±1-2 heures"; "fee__slow__shortRange" = "1-2h"; "fee__slow__longRange" = "± 1-2 heures"; -"fee__minimum__title" = "Minimum"; "fee__minimum__longTitle" = "Minimum"; "fee__minimum__description" = "+2 heures"; "fee__minimum__shortRange" = "+2h"; @@ -199,74 +196,37 @@ "fee__custom__range" = "Dépend des frais"; "fee__custom__shortRange" = "Dépend des frais"; "fee__custom__longRange" = "Dépend du montant des frais"; -"lightning__transfer_intro__title" = "Dépenses \nSolde"; -"lightning__transfer_intro__text" = "Alimentez votre solde de dépenses pour profiter de transactions instantanées et bon marché avec vos amis, votre famille et les commerçants."; -"lightning__transfer_intro__button" = "Commencer"; "lightning__funding__nav_title" = "Solde Dépenses"; "lightning__funding__title" = "Financez votre solde Dépenses"; -"lightning__funding__text" = "Le plus simple est d\'utiliser vos économies Bitkit ou d\'envoyer des bitcoins depuis un autre portefeuille."; -"lightning__funding__text_blocked" = "Bitkit ne fournit pas actuellement de services Lightning dans votre pays, mais vous pouvez toujours vous connecter directement à d\'autres nœuds."; -"lightning__funding__text_blocked_cjit" = "Bitkit ne fournit pas actuellement de services Lightning dans votre pays, mais vous pouvez toujours vous connecter directement à d\'autres nœuds."; "lightning__funding__button1" = "Transfert depuis l\'épargne"; "lightning__funding__button2" = "Utiliser un autre portefeuille"; "lightning__funding__button3" = "Configuration manuelle"; -"lightning__funding_advanced__nav_title" = "Solde Dépenses"; -"lightning__funding_advanced__title" = "Configuration avancée"; -"lightning__funding_advanced__text" = "Scannez un QR code pour réclamer votre canal LNURL à un autre LSP, ou choisissez la configuration manuelle."; -"lightning__funding_advanced__button1" = "LNURL Canal QR code"; -"lightning__funding_advanced__button2" = "Configuration manuelle"; "lightning__funding_advanced__button_short" = "Configuration avancée"; -"lightning__no_funds__title" = "Pas de fonds disponibles"; -"lightning__no_funds__description" = "Avant de pouvoir transférer des fonds depuis votre solde d\'épargne, vous devez envoyer des bitcoins à votre portefeuille Bitkit."; -"lightning__no_funds__fund" = "Alimentez votre portefeuille"; "lightning__transfer__nav_title" = "Transfert de fonds"; "lightning__transfer__confirm" = "Veuillez\nconfirmer"; "lightning__transfer__custom_fee" = "Frais sur mesure"; "lightning__transfer__swipe" = "Glissez pour transférer"; "lightning__transfer_hw__broadcast_uncertain_title" = "Transaction peut-être envoyée"; "lightning__transfer_hw__broadcast_uncertain_description" = "Votre transaction de financement a peut-être été diffusée. Veuillez vérifier votre activité avant de réessayer."; -"lightning__transfer_hw__open_connect" = "Ouvrir Trezor Connect"; "lightning__transfer_hw__reconnect_error_description" = "Veuillez reconnecter votre appareil matériel, il semble être déconnecté de votre téléphone."; "lightning__transfer_hw__reconnect_error_title" = "Reconnecter l'appareil matériel"; -"lightning__transfer_hw__sign_title" = "Signez avec\nvotre appareil"; -"lightning__transfer_hw__signed_title" = "Transaction\nsignée"; "lightning__transfer_to_savings_button" = "Transférer vers l\'épargne"; "lightning__transfer_to_spending_button" = "Transférer vers les dépenses"; -"lightning__spending_intro__title" = "Transfert\naux dépenses"; -"lightning__spending_intro__text" = "Transférez des fonds vers votre solde Dépenses pour profiter de transactions instantanées et moins chers avec vos amis, votre famille et les commerçants."; -"lightning__spending_intro__button" = "Commencer"; "lightning__transfer_failed__nav_title" = "Échec du transfert"; "lightning__transfer_failed__title" = "Transfert\néchoué"; "lightning__transfer_failed__text" = "Désolé, Bitkit n’a pas pu configurer votre solde Dépenses ou effectuer le transfert. Veuillez réessayer."; "lightning__transfer_failed__cancel" = "Inspecter l\'erreur"; "lightning__transfer_failed__continue" = "Réessayer"; -"lightning__spending_amount__title" = "Transfert \nDépenses"; "lightning__spending_amount__quarter" = "25%"; "lightning__spending_amount__error_min__title" = "Seuil Minimum Épargne"; "lightning__spending_amount__error_min__description" = "Un minimum de ₿ {amount} est nécessaire pour établir votre solde de Dépenses."; -"lightning__spending_amount__error_balance__description" = "Votre épargne ne couvre pas ce transfert et ses frais. Essayez un montant plus petit."; -"lightning__spending_amount__error_balance__title" = "Épargne insuffisante"; "lightning__spending_amount__error_max__title" = "Seuil Maximum Épargne"; "lightning__spending_amount__error_max__description" = "Le montant que vous pouvez transférer vers votre solde de dépenses est actuellement limité à ₿ {amount}."; "lightning__spending_amount__error_max__description_zero" = "Votre transfert vers le solde des dépenses est limité en raison de la politique de liquidité. Pour plus de détails, consultez le Centre d\'aide."; "lightning__spending_confirm__network_fee" = "Frais de réseau"; "lightning__spending_confirm__lsp_fee" = "Frais de service"; "lightning__spending_confirm__amount" = "Vers Dépenses"; -"lightning__spending_confirm__background_setup" = "Configurer en arrière-plan"; "lightning__spending_confirm__total" = "Total"; -"lightning__spending_confirm__default" = "Utiliser les valeurs par défaut"; -"lightning__spending_confirm__fees_changed_title" = "Frais modifiés"; -"lightning__spending_confirm__fees_changed_service" = "Les frais sont plus élevés de {amount} maintenant que votre commande est créée. Glissez à nouveau pour confirmer."; -"lightning__spending_confirm__fees_changed_network" = "Les frais sont plus élevés de {amount}, car les taux de frais ou votre solde d'épargne ont changé pendant que vous étiez sur cet écran. Glissez à nouveau pour confirmer."; -"lightning__spending_advanced__title" = "Recevoir \ncapacité"; -"lightning__spending_advanced__fee" = "Frais de liquidité"; -"lightning__spending_advanced__error_balance__description" = "Votre épargne ne couvre pas les frais de liquidité pour cette capacité de réception. Choisissez un montant plus petit."; -"lightning__spending_advanced__error_balance__title" = "Fonds insuffisants"; -"lightning__spending_advanced__error_max__title" = "Capacité de réception maximale"; -"lightning__spending_advanced__error_max__description" = "La capacité de réception est actuellement limitée à ₿ {amount}."; -"lightning__liquidity__title" = "Liquidité\n& routage"; -"lightning__liquidity__text" = "Votre solde Dépenses, utilise le Lightning Network pour rendre vos paiements moins chers, plus rapides et plus confidentiels.\n\nCela fonctionne comme l\'accès à Internet, mais vous payez pour la liquidité et le routage au lieu de la bande passante.\n\nCette configuration implique des coûts uniques."; -"lightning__liquidity__label" = "Liquidité du Solde des dépenses"; "lightning__savings_intro__title" = "Transfert \nvers l\'Épargne"; "lightning__savings_intro__text" = "Transférez le solde de votre compte Dépenses vers votre Épargne pour conserver vos bitcoins à long terme."; "lightning__savings_intro__button" = "Commencer"; @@ -290,9 +250,6 @@ "lightning__savings_interrupted__nav_title" = "Transfert \ninterrompu"; "lightning__savings_interrupted__title" = "Gardez Bitkit \nouvert"; "lightning__savings_interrupted__text" = "Les fonds n\'ont pas encore été transférés. Bitkit va essayer d\'initier le transfert au cours des 30 prochaines minutes. Veuillez garder votre application ouverte."; -"lightning__transfer_success__nav_title" = "Transfert réussi"; -"lightning__transfer_success__title_spending" = "Fonds transférés \nvers Dépenses"; -"lightning__transfer_success__text_spending" = "Vos fonds ont été transférés et votre solde Dépenses est prêt à être utilisé."; "lightning__transfer_success__title_savings" = "Fonds transférés \nà l\'épargne"; "lightning__transfer_success__text_savings" = "Vos fonds ont été transférés. Vous pouvez utiliser ces fonds immédiatement."; "lightning__external__nav_title" = "Connexion Lightning"; @@ -315,12 +272,6 @@ "lightning__spending_label" = "Solde Dépenses"; "lightning__receiving_label" = "Capacité de réception"; "lightning__setting_up_header" = "En transfert"; -"lightning__setting_up_text" = "Veuillez patienter, le transfert des fonds est en cours. Cela devrait prendre ±10 minutes."; -"lightning__setting_up_step1" = "Traitement des paiements"; -"lightning__setting_up_step2" = "Paiement réussi"; -"lightning__setting_up_step3" = "En attente d\'ouverture"; -"lightning__setting_up_step4" = "Ouverture de la connexion"; -"lightning__setting_up_button" = "Continuer à utiliser Bitkit"; "lightning__node_info" = "Nœud Lightning"; "lightning__node_id" = "ID du nœud LDK"; "lightning__node_disconnected" = "déconnecté"; @@ -392,9 +343,9 @@ "lightning__force_init_msg" = "Vos fonds seront accessibles dans un délai de ±14 jours."; "lightning__force_failed_title" = "Échec du transfert forcé"; "lightning__force_failed_msg" = "Impossible de renvoyer vos fonds sur l\'Épargne. Veuillez réessayer."; -"lightning__force_channels_skipped" = "Certaines connexions n'ont pas pu être fermées."; "lightning__channel_opened_title" = "Solde Dépenses"; "lightning__channel_opened_msg" = "Vous pouvez désormais payer n\'importe qui, n\'importe où, instantanément."; +"lightning__funding__button_hw" = "Transfert depuis le portefeuille HW"; "lightning__order_given_up_title" = "Échec de la configuration des paiements instantanés"; "lightning__order_given_up_msg" = "Le processus d\'installation a expiré. Veuillez réessayer."; "lightning__order_expired_title" = "Échec de la configuration des paiements instantanés"; @@ -413,6 +364,9 @@ "lightning__order_state__open" = "Connexion ouverte"; "lightning__order_state__inactive" = "Connexion inactive"; "lightning__peer_disconnected" = "Pair déconnecté."; +"lightning__transfer_unavailable__nav_title" = "Transfert indisponible"; +"lightning__transfer_unavailable__text" = "Les transferts automatiques vers votre solde de dépenses ne sont plus disponibles. Utilisez votre propre nœud ou configurez une connexion avec un autre fournisseur."; +"lightning__transfer_unavailable__title" = "Configuration manuelle\nrequise"; "lightning__wait_text_top" = "Veuillez attendre que Bitkit se connecte au réseau de paiement (±10 secondes)."; "lightning__wait_text_bottom" = "Connexion et synchronisation..."; "onboarding__tos_header" = "Bitkit \nConditions d\'utilisation"; @@ -434,7 +388,6 @@ "onboarding__slide1_header" = "Paiements \nInstantanés"; "onboarding__slide1_text" = "Dépensez des bitcoins plus rapidement que jamais. Profitez de paiements instantanés et bon marché avec vos amis, votre famille et les commerçants."; "onboarding__slide1_text_alt" = "Profitez de paiements instantanés et peu coûteux avec vos amis, votre famille et les commerçants sur le réseau Lightning."; -"onboarding__slide1_note" = "*Bitkit ne fournit pas actuellement de services Lightning dans votre pays, mais vous pouvez toujours vous connecter à d\'autres nœuds."; "onboarding__slide2_header" = "Bitcoiners, \nsans frontières"; "onboarding__slide2_text" = "Prenez en charge votre vie numérique avec des profils et des contacts."; "onboarding__slide3_header" = "La vie privée \nn\'est pas un crime"; @@ -557,8 +510,6 @@ "other__lnurl_withdr_error_minmax" = "Paramètres de retrait LNURL incorrects, min/max mal définis."; "other__lnurl_withdr_success_title" = "Retrait demandé"; "other__lnurl_withdr_success_msg" = "Votre retrait a été demandé avec succès. En attente de paiement."; -"other__notification_registration_failed_description" = "Bitkit n\'a pas pu s\'inscrire aux notifications push."; -"other__notification_registration_failed_title" = "Échec de l\'inscription aux notifications"; "other__phone_settings" = "Ouvrez les paramètres du téléphone"; "other__bt_error_retrieve" = "Échec du transfer "; "other__bt_error_connect" = "Impossible d\'ajouter un nœud LSP comme pair pour le moment."; @@ -574,14 +525,7 @@ "other__transfer_notification__body" = "Ouvrez Bitkit pour terminer votre transfert"; "other__scan__error__generic" = "Bitkit ne peut pas lire les données fournies."; "other__scan__error__expired" = "Cette facture Lightning a expiré."; -"other__gift__claimed__text" = "Désolé, vous arrivez trop tard ! Tous les cadeaux pour ce code ont déjà été réclamés."; -"other__gift__claiming__text" = "Réclamer votre code cadeau Bitkit..."; -"other__gift__claiming__title" = "Réclamer un cadeau"; -"other__gift__error__title" = "Erreur de code cadeau"; -"other__gift__error__text" = "Bitkit n\'a pas pu réclamer les fonds. Veuillez réessayer plus tard ou nous contacter."; "other__gift__error__button" = "OK"; -"other__gift__used__title" = "Code utilisé"; -"other__gift__used__text" = "Ce code cadeau Bitkit a déjà été utilisé et les fonds ont été versés."; "other__gift__used__button" = "OK"; "other__shop__intro__title" = "Boutique"; "other__shop__intro__description" = "Passez à la norme Bitcoin. Dépensez vos bitcoins en cartes-cadeaux numériques, eSIM, recharges téléphoniques, et plus encore."; @@ -764,6 +708,7 @@ "pubky_auth__homeserver" = "Serveur domestique"; "pubky_auth__signup_description" = "Créez une nouvelle identité Pubky sur ce serveur domestique. Ne continuez que si vous lui faites confiance."; "pubky_auth__approval_failed" = "Échec de l'autorisation"; +"wallet__onchain_abandon_transfer_note" = "Les transferts vers le solde de dépenses ne sont plus disponibles. Bitkit vérifie d'abord le réseau, puis renvoie les pièces de ce transfert vers votre portefeuille afin que le transfert d'origine ne soit jamais confirmé. Cela coûte des frais de réseau."; "watch_only_accounts__active_section" = "Comptes actifs"; "watch_only_accounts__copy_xpub" = "Copier la xpub"; "watch_only_accounts__description" = "Chaque serveur Paykit approuvé reçoit un compte Bitcoin distinct. Désactivez le suivi pour décharger un compte sans supprimer l'historique de son portefeuille."; @@ -840,21 +785,17 @@ "settings__widgets__reset_widgets_dialog_title" = "Réinitialiser les widgets ?"; "settings__widgets__reset_widgets_dialog_description" = "Êtes-vous sûr de vouloir réinitialiser les widgets ? L'ensemble de widgets par défaut avec les configurations par défaut sera affiché."; "settings__widgets__reset_suggestions" = "Réinitialiser les cartes de suggestions"; -"settings__notifications__nav_title" = "Paiements en arrière-plan"; -"settings__notifications__intro__title" = "Soyez rémunéré\nde manière passive"; -"settings__notifications__intro__text" = "Activez les notifications pour être payé, même lorsque votre application Bitkit est fermée."; +"settings__notifications__nav_title" = "Notifications"; +"settings__notifications__intro__title" = "Restez\ninformé"; +"settings__notifications__intro__text" = "Activez les notifications pour recevoir des rappels de vos abonnements et demandes de paiement."; "settings__notifications__intro__button" = "Activer"; -"settings__notifications__settings__toggle" = "Soyez rémunéré lorsque Bitkit est fermé"; -"settings__notifications__settings__enabled" = "Les paiements en arrière-plan sont activés. Vous pouvez recevoir des fonds même lorsque l'application est fermée (si votre appareil est connecté à Internet)."; -"settings__notifications__settings__disabled" = "Activez les paiements en arrière-plan pour recevoir des fonds même lorsque l'application est fermée (si votre appareil est connecté à Internet)."; -"settings__notifications__settings__denied" = "Les paiements en arrière-plan sont désactivés, car vous avez refusé les notifications."; -"settings__notifications__settings__preview__title" = "Paiement reçu"; -"settings__notifications__settings__preview__text" = "Ouvrez Bitkit pour voir les détails"; +"settings__notifications__settings__toggle" = "Autoriser les notifications"; +"settings__notifications__settings__enabled" = "Les notifications sont activées. Bitkit peut vous rappeler vos abonnements et demandes de paiement."; +"settings__notifications__settings__disabled" = "Activez les notifications pour recevoir des rappels de vos abonnements et demandes de paiement."; +"settings__notifications__settings__denied" = "Les notifications sont désactivées, car vous les avez refusées."; "settings__notifications__settings__preview__time" = "Il y a 3 min"; -"settings__notifications__settings__privacy__label" = "Confidentialité"; -"settings__notifications__settings__privacy__text" = "Inclure le montant dans les notifications"; "settings__notifications__settings__notifications__label" = "Notifications"; -"settings__notifications__settings__notifications__text" = "Pour activer les paiements en arrière-plan, veuillez activer les notifications dans les paramètres iOS Bitkit."; +"settings__notifications__settings__notifications__text" = "Pour recevoir des rappels, veuillez activer les notifications dans les paramètres iOS Bitkit."; "settings__notifications__settings__button__enabled" = "Personnaliser dans {platform} Paramètres Bitkit"; "settings__notifications__settings__button__disabled" = "Activer dans {platform} Paramètres Bitkit"; "settings__quickpay__nav_title" = "QuickPay"; @@ -1405,27 +1346,11 @@ "wallet__receive_note_placeholder" = "Note facultative à destination du payeur"; "wallet__receive_show_qr" = "Afficher le QR code"; "wallet__receive_text_lnfunds" = "Vous souhaitez recevoir des fonds Lightning?"; -"wallet__receive_background_setup_text" = "Activez la configuration en arrière-plan pour quitter Bitkit en toute sécurité pendant que votre solde est configuré."; -"wallet__receive_background_setup_toggle" = "Activer la configuration en arrière-plan"; "wallet__receive_spending" = "Recevoir sur le compte de dépenses"; "wallet__receive_spending_text" = "Profitez de paiements en bitcoins instantanés et bon marché sur le Lightning Network."; "wallet__receive_spending_title" = "Recevoir sur le solde de dépenses"; -"wallet__receive_connect_initial" = "Pour établir votre solde Dépenses, un {networkFee} frais de réseau et {serviceFee} frais de fournisseur de services seront déduits."; -"wallet__receive_connect_additional" = "Pour recevoir plus de bitcoins instantanés, Bitkit doit augmenter vos liquidités. Des frais de réseau de {networkFee} et des frais de fournisseur de services de {serviceFee} seront déduits du montant que vous avez spécifié."; -"wallet__receive_liquidity__nav_title" = "Configuration du solde compte Dépenses"; -"wallet__receive_liquidity__nav_title_additional" = "Augmenter la capacité de réception"; -"wallet__receive_liquidity__invalid_quote" = "Bitkit a reçu un devis de liquidité invalide. Veuillez réessayer."; -"wallet__receive_liquidity__text" = "Votre solde Dépenses utilise le Lightning Network pour rendre vos paiements moins chers, plus rapides et plus confidentiels.\n\nCela fonctionne comme l\'accès à Internet, mais vous payez pour la liquidité et le routage au lieu de la bande passante.\n\nCette configuration implique des coûts uniques."; -"wallet__receive_liquidity__text_additional" = "Votre solde Dépenses utilise le Lightning Network pour rendre vos paiements moins chers, plus rapides et plus confidentiels.\n\nCela fonctionne comme l\'accès à Internet, mais vous payez pour la liquidité et le routage au lieu de la bande passante.\n\nBitkit a besoin d\'augmenter la capacité de réception de votre solde du compte courant pour traiter ce paiement."; -"wallet__receive_liquidity__label" = "Liquidité disponible"; -"wallet__receive_liquidity__label_additional" = "Liquidité supplémentaire du solde Dépenses"; "wallet__receive_cjit_error" = "Échec de la transaction"; "wallet__receive_cjit_error_msg" = "Échec de l’envoi vers Dépenses."; -"wallet__receive_cjit_error_max__title" = "Capacité de réception maximale"; -"wallet__receive_cjit_error_max__description" = "Le maximum que vous pouvez recevoir sur votre solde Dépenses pour le moment est de ₿ {amount}."; -"wallet__receive_cjit_error_node_capacity__title" = "Capacité supplémentaire indisponible"; -"wallet__receive_cjit_error_node_capacity__description" = "Bitkit ne peut pas ajouter plus de capacité de dépenses à ce portefeuille pour le moment."; -"wallet__receive_will" = "Vous recevrez"; "wallet__receive_ldk_init" = "Initialisation du solde Dépenses…"; "wallet__minimum" = "MINIMUM"; "wallet__activity" = "Activité"; @@ -1614,7 +1539,7 @@ "subscriptions__name_placeholder" = "Nom de l'abonnement"; "subscriptions__ongoing" = "En cours"; "subscriptions__overview" = "Aperçu"; -"subscriptions__payment_due_description" = "Ouvrez Bitkit pour examiner un paiement d'abonnement."; +"subscriptions__payment_due_description" = "Ouvrez Bitkit pour examiner le paiement"; "subscriptions__payment_due_title" = "Paiement d'abonnement dû"; "subscriptions__payments" = "Paiements"; "subscriptions__per_day" = "par jour"; @@ -1786,9 +1711,6 @@ "wallet__onchain_broadcast_rejected_note" = "Ce paiement a peut-être quand même atteint le réseau. Ne l'envoyez pas à nouveau. {reason}"; "wallet__onchain_broadcast_unconfirmed" = "Diffusion non confirmée"; "wallet__onchain_broadcast_unconfirmed_note" = "Le paiement a peut-être été envoyé. Ne l'envoyez pas à nouveau."; -"wallet__onchain_funding_rejected" = "La transaction de financement a été rejetée. N'essayez pas de financer cette commande à nouveau."; -"wallet__onchain_funding_unknown" = "La transaction de financement a peut-être été envoyée. N'essayez pas de financer cette commande à nouveau."; -"wallet__onchain_funding_followup_failed" = "Le financement a été envoyé, mais le suivi local de la commande n'a pas pu être enregistré. Ne financez pas cette commande à nouveau."; "wallet__onchain_retry_original" = "Réessayer le paiement d'origine"; "wallet__onchain_retry_fee" = "Taux de frais (sat/vB)"; "wallet__onchain_retry_note" = "Réessayer l'envoi de {amount} sats à {address} avec les mêmes UTXO d'origine. Le montant du destinataire reste inchangé. Saisissez le taux de frais de réseau en sat/vB. Cela ne peut pas créer de paiement séparé."; diff --git a/Bitkit/Resources/Localization/it.lproj/Localizable.strings b/Bitkit/Resources/Localization/it.lproj/Localizable.strings index 3f352c0fe..a1a3ba8f1 100644 --- a/Bitkit/Resources/Localization/it.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/it.lproj/Localizable.strings @@ -4,8 +4,8 @@ "cards__discount__description" = "Conto di Spesa"; "cards__quickpay__title" = "QuickPay"; "cards__quickpay__description" = "Scansiona e paga"; -"cards__notifications__description_alt" = "Quando Bitkit è chiuso"; -"cards__notifications__title" = "Ricevi Pagamenti"; +"cards__notifications__description_alt" = "Promemoria di pagamento"; +"cards__notifications__title" = "Notifiche"; "cards__invite__title" = "Invita"; "cards__invite__description" = "Condividi Bitkit"; "cards__lightning__title" = "Spendi"; @@ -150,9 +150,7 @@ "common__learn_more" = "Scopri di Più"; "common__understood" = "Ho Capito"; "common__connect" = "Connetti"; -"common__min" = "Min"; "common__max" = "Max"; -"common__default" = "Predefinito"; "common__preview" = "Anteprima"; "common__show_all" = "Mostra Tutto"; "common__show_details" = "Mostra Dettagli"; @@ -184,7 +182,6 @@ "fee__slow__range" = "±1-2 ore"; "fee__slow__shortRange" = "1-2h"; "fee__slow__longRange" = "± 1-2 ore"; -"fee__minimum__title" = "Minimo"; "fee__minimum__longTitle" = "Minima"; "fee__minimum__description" = "+2 ore"; "fee__minimum__shortRange" = "+2h"; @@ -197,70 +194,33 @@ "fee__custom__range" = "Dipende dalla commissione"; "fee__custom__shortRange" = "Dipende dalle fee"; "fee__custom__longRange" = "Dipende dalla commissione"; -"lightning__transfer_intro__title" = "Conto\ndi Spesa"; -"lightning__transfer_intro__text" = "Manda fondi al tuo conto di spesa per usufruire di transazioni istantanee ed economiche con amici, familiari e commercianti."; -"lightning__transfer_intro__button" = "Inizia"; "lightning__funding__nav_title" = "Conto di Spesa"; "lightning__funding__title" = "Manda fondi al tuo conto di spesa"; -"lightning__funding__text" = "Il modo più semplice è usare i tuoi risparmi Bitkit o inviare bitcoin da un portafoglio diverso."; -"lightning__funding__text_blocked" = "Bitkit attualmente non fornisce servizi Lightning nel tuo paese, ma puoi comunque connetterti direttamente ad altri nodi."; -"lightning__funding__text_blocked_cjit" = "Bitkit attualmente non fornisce servizi Lightning nel tuo paese, ma puoi comunque connetterti direttamente ad altri nodi."; "lightning__funding__button1" = "Trasferimento da risparmio"; "lightning__funding__button2" = "Usa un altro portafoglio"; "lightning__funding__button3" = "Configurazione manuale"; -"lightning__funding_advanced__nav_title" = "Conto Spendibile"; -"lightning__funding_advanced__title" = "Configurazione avanzata"; -"lightning__funding_advanced__text" = "Scansiona un QR per richiedere il tuo Canale LNURL da un altro LSP, oppure scegli la configurazione manuale."; -"lightning__funding_advanced__button1" = "QR Canale LNURL"; -"lightning__funding_advanced__button2" = "Configurazione Manuale"; "lightning__funding_advanced__button_short" = "Configurazione Avanzata"; -"lightning__no_funds__title" = "Nessun fondo disponibile"; -"lightning__no_funds__description" = "Prima di poter trasferire fondi dal tuo conto di risparmio, devi inviare bitcoin al tuo portafoglio Bitkit."; -"lightning__no_funds__fund" = "Finanzia portafoglio"; "lightning__transfer__nav_title" = "Trasferimento Fondi"; "lightning__transfer__confirm" = "Per favore\nconferma"; "lightning__transfer__swipe" = "Swipe Per Trasferire"; "lightning__transfer_hw__broadcast_uncertain_title" = "La transazione potrebbe essere stata inviata"; "lightning__transfer_hw__broadcast_uncertain_description" = "La tua transazione di finanziamento potrebbe essere stata trasmessa. Controlla la tua attività prima di riprovare."; -"lightning__transfer_hw__open_connect" = "Apri Trezor Connect"; "lightning__transfer_hw__reconnect_error_description" = "Ricollega il tuo dispositivo hardware, sembra essere disconnesso dal telefono."; "lightning__transfer_hw__reconnect_error_title" = "Ricollega il Dispositivo Hardware"; -"lightning__transfer_hw__sign_title" = "Firma con\nil tuo dispositivo"; -"lightning__transfer_hw__signed_title" = "Transazione\nfirmata"; "lightning__transfer_to_savings_button" = "Trasferisci Ai Risparmi"; "lightning__transfer_to_spending_button" = "Trasferisci A Spesa"; -"lightning__spending_intro__title" = "Trasferisci\na spesa"; -"lightning__spending_intro__text" = "Trasferisci fondi al tuo conto di spesa per usufruire di transazioni istantanee ed economiche con amici, familiari e commercianti."; -"lightning__spending_intro__button" = "Inizia"; "lightning__transfer_failed__nav_title" = "Trasferimento fallito"; "lightning__transfer_failed__title" = "Trasferimento\nfallito"; "lightning__transfer_failed__text" = "Siamo spiacenti, Bitkit non è riuscito a impostare il conto di spesa o a completare il trasferimento. Per favore riprova."; "lightning__transfer_failed__cancel" = "Ispeziona errore"; "lightning__transfer_failed__continue" = "Riprova"; -"lightning__spending_amount__title" = "Trasferisci\na spesa"; "lightning__spending_amount__quarter" = "25%"; -"lightning__spending_amount__error_balance__description" = "I tuoi risparmi non coprono questo trasferimento e le relative commissioni. Prova un importo minore."; -"lightning__spending_amount__error_balance__title" = "Risparmi insufficienti"; "lightning__spending_amount__error_max__title" = "Massimo Conto di Spesa"; "lightning__spending_amount__error_max__description" = "L'importo che puoi trasferire al tuo conto di spesa e' attualmente limitato a ₿ {amount}."; "lightning__spending_confirm__network_fee" = "Commissioni di rete"; "lightning__spending_confirm__lsp_fee" = "Commissioni di servizio"; "lightning__spending_confirm__amount" = "A spesa"; -"lightning__spending_confirm__background_setup" = "Configura in background"; "lightning__spending_confirm__total" = "Totale"; -"lightning__spending_confirm__default" = "Usa Predefiniti"; -"lightning__spending_confirm__fees_changed_title" = "Commissioni cambiate"; -"lightning__spending_confirm__fees_changed_service" = "Le commissioni sono aumentate di {amount} ora che il tuo ordine è stato creato. Scorri di nuovo per confermare."; -"lightning__spending_confirm__fees_changed_network" = "Le commissioni sono aumentate di {amount} perché i tassi di commissione o il saldo dei tuoi risparmi sono cambiati mentre eri su questa schermata. Scorri di nuovo per confermare."; -"lightning__spending_advanced__title" = "Capacità\ndi ricezione"; -"lightning__spending_advanced__fee" = "Commissione di liquidità"; -"lightning__spending_advanced__error_balance__description" = "I tuoi risparmi non coprono la commissione di liquidità per questa capacità di ricezione. Scegli un importo minore."; -"lightning__spending_advanced__error_balance__title" = "Fondi Insufficienti"; -"lightning__spending_advanced__error_max__title" = "Capacità di Ricezione Massima"; -"lightning__spending_advanced__error_max__description" = "La capacità di ricezione è attualmente limitata a ₿ {amount}."; -"lightning__liquidity__title" = "Liquidità\ne routing"; -"lightning__liquidity__text" = "Il tuo Conto di Spesa utilizza la rete Lightning Network per rendere i tuoi pagamenti più economici, veloci e privati.\n\nFunziona come l'accesso a internet, ma paghi per la liquidità e il routing invece che per la banda.\n\nQuesta configurazione include alcuni costi una tantum."; -"lightning__liquidity__label" = "Liquidità Conto di Spesa"; "lightning__savings_intro__title" = "Trasferisci\nai risparmi"; "lightning__savings_intro__text" = "Trasferisci il tuo saldo di spesa ai tuoi risparmi per conservare i tuoi Bitcoin a lungo termine e garantire il tuo futuro."; "lightning__savings_intro__button" = "Inizia"; @@ -284,9 +244,6 @@ "lightning__savings_interrupted__nav_title" = "Trasferimento\ninterrotto"; "lightning__savings_interrupted__title" = "Mantieni Bitkit\naperto"; "lightning__savings_interrupted__text" = "I fondi non sono stati ancora trasferiti. Bitkit tenterà di avviare il trasferimento durante i prossimi 30 minuti. Per favore mantieni l'app aperta."; -"lightning__transfer_success__nav_title" = "Trasferimento Riuscito"; -"lightning__transfer_success__title_spending" = "Fondi trasferiti\na spesa"; -"lightning__transfer_success__text_spending" = "I tuoi fondi sono stati trasferiti e il tuo conto di spesa è pronto per essere utilizzato."; "lightning__transfer_success__title_savings" = "Fondi trasferiti\nai risparmi"; "lightning__transfer_success__text_savings" = "I tuoi fondi sono stati trasferiti. Puoi utilizzare questi fondi immediatamente."; "lightning__external__nav_title" = "Connessione Lightning"; @@ -309,12 +266,6 @@ "lightning__spending_label" = "Conto di spesa"; "lightning__receiving_label" = "Capacità di ricezione"; "lightning__setting_up_header" = "In trasferimento"; -"lightning__setting_up_text" = "Attendi, il trasferimento dei fondi è in corso. L'operazione dovrebbe richiedere ±10 minuti."; -"lightning__setting_up_step1" = "Elaborazione del pagamento"; -"lightning__setting_up_step2" = "Pagamento Riuscito"; -"lightning__setting_up_step3" = "In Coda Per l'Apertura"; -"lightning__setting_up_step4" = "Apertura della Connessione"; -"lightning__setting_up_button" = "Continua ad utilizzare Bitkit"; "lightning__node_info" = "Nodo Lightning"; "lightning__node_id" = "ID Nodo LDK"; "lightning__node_disconnected" = "disconnesso"; @@ -385,9 +336,9 @@ "lightning__force_init_msg" = "I tuoi fondi saranno accessibili in ±14 giorni."; "lightning__force_failed_title" = "Trasferimento Forzato Fallito"; "lightning__force_failed_msg" = "Impossibile trasferire nuovamente i fondi nei risparmi. Per favore riprova."; -"lightning__force_channels_skipped" = "Alcune connessioni non sono state chiuse."; "lightning__channel_opened_title" = "Conto di spesa Pronto"; "lightning__channel_opened_msg" = "Ora puoi pagare chiunque, ovunque, istantaneamente."; +"lightning__funding__button_hw" = "Trasferimento da wallet HW"; "lightning__order_given_up_title" = "Configurazione dei Pagamenti Istantanei Fallita"; "lightning__order_given_up_msg" = "Il processo di installazione è scaduto. Per favore riprova."; "lightning__order_expired_title" = "Configurazione dei Pagamenti Istantanei Fallita"; @@ -406,6 +357,9 @@ "lightning__order_state__open" = "Connessione aperta"; "lightning__order_state__inactive" = "Connessione inattiva"; "lightning__peer_disconnected" = "Peer disconnesso."; +"lightning__transfer_unavailable__nav_title" = "Trasferimento non disponibile"; +"lightning__transfer_unavailable__text" = "I trasferimenti automatici al saldo di spesa non sono più disponibili. Usa il tuo nodo o configura una connessione con un altro fornitore."; +"lightning__transfer_unavailable__title" = "Configurazione manuale\nrichiesta"; "lightning__wait_text_top" = "Attendi che Bitkit si connetta alla rete di pagamento (±10 secondi)."; "lightning__wait_text_bottom" = "Connessione e sincronizzazione..."; "onboarding__tos_header" = "Bitkit\ntermini di utilizzo"; @@ -427,7 +381,6 @@ "onboarding__slide1_header" = "Pagamenti\nistantanei"; "onboarding__slide1_text" = "Spendi bitcoin più velocemente che mai. Goditi pagamenti immediati ed economici con amici, familiari e commercianti."; "onboarding__slide1_text_alt" = "Goditi pagamenti istantanei ed economici con amici, familiari e commercianti sulla rete Lightning Network."; -"onboarding__slide1_note" = "*Bitkit attualmente non fornisce servizi Lightning nel tuo paese, ma puoi comunque connetterti ad altri nodi."; "onboarding__slide2_header" = "Bitcoiners,\nsenza confini"; "onboarding__slide2_text" = "Prendi il controllo della tua vita digitale con profili portatili e contatti a pagamento."; "onboarding__slide3_header" = "La privacy\nnon è un crimine"; @@ -550,8 +503,6 @@ "other__lnurl_withdr_error_minmax" = "Parametri di prelievo LNURL errati, minimo/massimo non impostati correttamente."; "other__lnurl_withdr_success_title" = "Prelievo Richiesto"; "other__lnurl_withdr_success_msg" = "Il tuo prelievo è stato richiesto con successo. In attesa del pagamento."; -"other__notification_registration_failed_description" = "Bitkit non è riuscito a registrarsi per le notifiche push."; -"other__notification_registration_failed_title" = "Registrazione Notifiche Fallita"; "other__phone_settings" = "Apri le Impostazioni del telefono"; "other__bt_error_retrieve" = "Trasferimento non riuscito"; "other__bt_error_connect" = "Impossibile aggiungere il nodo LSP come peer in questo momento."; @@ -565,14 +516,7 @@ "other__try_again" = "Per favore riprova."; "other__scan__error__generic" = "Bitkit non è in grado di leggere i dati forniti."; "other__scan__error__expired" = "Questa fattura Lightning è scaduta."; -"other__gift__claimed__text" = "Spiacente, sei arrivato troppo tardi! Tutti i regali per questo codice sono già stati riscattati."; -"other__gift__claiming__text" = "Riscatto del tuo codice regalo Bitkit..."; -"other__gift__claiming__title" = "Riscatto Regalo"; -"other__gift__error__title" = "Errore Codice Regalo"; -"other__gift__error__text" = "Bitkit non è riuscito a riscattare i fondi. Riprova più tardi o contattaci."; "other__gift__error__button" = "OK"; -"other__gift__used__title" = "Codice Usato"; -"other__gift__used__text" = "Questo codice regalo Bitkit è già stato utilizzato e i fondi sono stati erogati."; "other__gift__used__button" = "OK"; "other__shop__intro__title" = "Shop"; "other__shop__intro__description" = "Porta la tua vita sullo standard Bitcoin. Spendi i tuoi Bitcoin in gift card digitali, eSIM, ricariche telefoniche e altro."; @@ -744,6 +688,7 @@ "pubky_auth__homeserver" = "Homeserver"; "pubky_auth__signup_description" = "Crea una nuova identità Pubky su questo homeserver. Continua solo se ti fidi."; "pubky_auth__approval_failed" = "Autorizzazione Non Riuscita"; +"wallet__onchain_abandon_transfer_note" = "I trasferimenti al saldo di spesa non sono più disponibili. Bitkit controlla prima la rete, poi rimanda le monete di questo trasferimento al tuo wallet, così il trasferimento originale non potrà mai essere confermato. Questo costa una commissione di rete."; "watch_only_accounts__active_section" = "Account attivi"; "watch_only_accounts__copy_xpub" = "Copia xpub"; "watch_only_accounts__description" = "Ogni server Paykit approvato riceve un account Bitcoin separato. Disattiva il monitoraggio per scaricare un account senza eliminare lo storico del suo wallet."; @@ -820,21 +765,17 @@ "settings__widgets__reset_widgets_dialog_title" = "Reimpostare i widget?"; "settings__widgets__reset_widgets_dialog_description" = "Sei sicuro di voler reimpostare i widget? Verrà visualizzato il set di widget predefinito con le configurazioni predefinite."; "settings__widgets__reset_suggestions" = "Reimposta schede suggerimenti"; -"settings__notifications__nav_title" = "Pagamenti in Background"; -"settings__notifications__intro__title" = "Ricevi Pagamenti\nPassivamente"; -"settings__notifications__intro__text" = "Attiva le notifiche per ricevere pagamenti, anche quando l'app Bitkit è chiusa."; +"settings__notifications__nav_title" = "Notifiche"; +"settings__notifications__intro__title" = "Resta\naggiornato"; +"settings__notifications__intro__text" = "Attiva le notifiche per ricevere promemoria su abbonamenti e richieste di pagamento."; "settings__notifications__intro__button" = "Abilita"; -"settings__notifications__settings__toggle" = "Ricevi pagamenti quando Bitkit è chiuso"; -"settings__notifications__settings__enabled" = "I pagamenti in background sono abilitati. Puoi ricevere fondi anche quando l'app è chiusa (se il dispositivo è connesso a internet)."; -"settings__notifications__settings__disabled" = "Abilita i pagamenti in background per ricevere fondi anche quando l'app è chiusa (se il dispositivo è connesso a internet)."; -"settings__notifications__settings__denied" = "I pagamenti in background sono disabilitati perché hai negato le notifiche."; -"settings__notifications__settings__preview__title" = "Pagamento Ricevuto"; -"settings__notifications__settings__preview__text" = "Apri Bitkit per vedere i dettagli"; +"settings__notifications__settings__toggle" = "Consenti notifiche"; +"settings__notifications__settings__enabled" = "Le notifiche sono attive. Bitkit può ricordarti abbonamenti e richieste di pagamento."; +"settings__notifications__settings__disabled" = "Attiva le notifiche per ricevere promemoria su abbonamenti e richieste di pagamento."; +"settings__notifications__settings__denied" = "Le notifiche sono disattivate perché le hai rifiutate."; "settings__notifications__settings__preview__time" = "3 min fa"; -"settings__notifications__settings__privacy__label" = "Privacy"; -"settings__notifications__settings__privacy__text" = "Includi l'importo nelle notifiche"; "settings__notifications__settings__notifications__label" = "Notifiche"; -"settings__notifications__settings__notifications__text" = "Per abilitare i pagamenti in background, abilita le notifiche nelle impostazioni iOS di Bitkit."; +"settings__notifications__settings__notifications__text" = "Per ricevere promemoria, abilita le notifiche nelle impostazioni iOS di Bitkit."; "settings__notifications__settings__button__enabled" = "Personalizza nelle impostazioni {platform} di Bitkit"; "settings__notifications__settings__button__disabled" = "Abilita nelle impostazioni {platform} di Bitkit"; "settings__quickpay__nav_title" = "QuickPay"; @@ -1369,27 +1310,11 @@ "wallet__receive_note_placeholder" = "Nota opzionale per chi paga"; "wallet__receive_show_qr" = "Visualizza QR Code"; "wallet__receive_text_lnfunds" = "Vuoi ricevere fondi Lightning?"; -"wallet__receive_background_setup_text" = "Abilita la configurazione in background per uscire da Bitkit in sicurezza mentre il tuo saldo viene configurato."; -"wallet__receive_background_setup_toggle" = "Abilita configurazione in background"; "wallet__receive_spending" = "Ricevi fondi Lightning"; "wallet__receive_spending_text" = "Goditi pagamenti bitcoin istantanei ed economici sulla Lightning Network."; "wallet__receive_spending_title" = "Ricevi su saldo di spesa"; -"wallet__receive_connect_initial" = "Per configurare il tuo conto di spesa, verranno dedotte una commissione di rete di {networkFee} e una commissione del fornitore di servizi di {serviceFee}."; -"wallet__receive_connect_additional" = "Per ricevere altri Bitcoin istantanei, Bitkit deve aumentare la tua liquidità. Una commissione di rete di {networkFee} e una commissione del fornitore di servizi di {serviceFee} verranno dedotte dall'importo specificato."; -"wallet__receive_liquidity__nav_title" = "Configurazione Saldo di Spesa"; -"wallet__receive_liquidity__nav_title_additional" = "Aumenta Capacita' di Ricezione"; -"wallet__receive_liquidity__invalid_quote" = "Bitkit ha ricevuto un preventivo di liquidità non valido. Riprova."; -"wallet__receive_liquidity__text" = "Il tuo Saldo di Spesa utilizza la rete Lightning Network per rendere i tuoi pagamenti più economici, veloci e privati.\n\nFunziona come l'accesso a internet, ma paghi per la liquidità e il routing invece che per la banda.\n\nQuesta configurazione include alcuni costi una tantum."; -"wallet__receive_liquidity__text_additional" = "Il tuo Saldo di Spesa utilizza la rete Lightning Network per rendere i tuoi pagamenti più economici, veloci e privati.\n\nFunziona come l'accesso a internet, ma paghi per la liquidità e il routing invece che per la banda.\n\nBitkit deve aumentare la capacità di ricezione del tuo saldo di spesa per elaborare questo pagamento."; -"wallet__receive_liquidity__label" = "Liquidità Saldo di Spesa"; -"wallet__receive_liquidity__label_additional" = "Liquidità Aggiuntiva Saldo di Spesa"; "wallet__receive_cjit_error" = "Transazione Fallita"; "wallet__receive_cjit_error_msg" = "Impossibile inviare fondi al tuo conto di spesa."; -"wallet__receive_cjit_error_max__title" = "Capacità di Ricezione Massima"; -"wallet__receive_cjit_error_max__description" = "Il massimo che puoi ricevere sul tuo conto di spesa in questo momento è ₿ {amount}."; -"wallet__receive_cjit_error_node_capacity__title" = "Capacità aggiuntiva non disponibile"; -"wallet__receive_cjit_error_node_capacity__description" = "Bitkit non può aggiungere altra capacità di spesa a questo portafoglio in questo momento."; -"wallet__receive_will" = "Riceverai"; "wallet__minimum" = "MINIMO"; "wallet__activity" = "Attività"; "wallet__activity_show_all" = "Visualizza tutte le attività"; @@ -1576,7 +1501,7 @@ "subscriptions__name_placeholder" = "Nome abbonamento"; "subscriptions__ongoing" = "In corso"; "subscriptions__overview" = "Panoramica"; -"subscriptions__payment_due_description" = "Apri Bitkit per controllare un pagamento di abbonamento."; +"subscriptions__payment_due_description" = "Apri Bitkit per controllare il pagamento"; "subscriptions__payment_due_title" = "Pagamento Abbonamento Dovuto"; "subscriptions__payments" = "Pagamenti"; "subscriptions__per_day" = "al giorno"; @@ -1745,9 +1670,6 @@ "wallet__onchain_broadcast_rejected_note" = "Questo pagamento potrebbe aver comunque raggiunto la rete. Non inviarlo di nuovo. {reason}"; "wallet__onchain_broadcast_unconfirmed" = "Trasmissione non confermata"; "wallet__onchain_broadcast_unconfirmed_note" = "Il pagamento potrebbe essere stato inviato. Non inviarlo di nuovo."; -"wallet__onchain_funding_rejected" = "La transazione di finanziamento è stata rifiutata. Non provare a finanziare di nuovo questo ordine."; -"wallet__onchain_funding_unknown" = "La transazione di finanziamento potrebbe essere stata inviata. Non provare a finanziare di nuovo questo ordine."; -"wallet__onchain_funding_followup_failed" = "Il finanziamento è stato inviato, ma non è stato possibile salvare il monitoraggio locale dell'ordine. Non finanziare di nuovo questo ordine."; "wallet__onchain_retry_original" = "Riprova pagamento originale"; "wallet__onchain_retry_fee" = "Tasso di commissione (sat/vB)"; "wallet__onchain_retry_note" = "Riprova a inviare {amount} sats a {address} usando gli stessi coin originali. L'importo per il destinatario resta invariato. Inserisci il tasso di commissione di rete in sat/vB. Questo non può creare un pagamento separato."; diff --git a/Bitkit/Resources/Localization/nl.lproj/Localizable.strings b/Bitkit/Resources/Localization/nl.lproj/Localizable.strings index d2dd741a3..7f5861673 100644 --- a/Bitkit/Resources/Localization/nl.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/nl.lproj/Localizable.strings @@ -4,8 +4,8 @@ "cards__discount__description" = "Bestedingssaldo"; "cards__quickpay__title" = "QuickPay"; "cards__quickpay__description" = "Scan en betaal"; -"cards__notifications__description_alt" = "Wanneer Bitkit gesloten is"; -"cards__notifications__title" = "Ontvang"; +"cards__notifications__description_alt" = "Betaalherinneringen"; +"cards__notifications__title" = "Meldingen"; "cards__invite__title" = "Uitnodiging"; "cards__invite__description" = "Deel Bitkit"; "cards__lightning__title" = "Besteed"; @@ -151,9 +151,7 @@ "common__learn_more" = "Meer Weten"; "common__understood" = "Begrepen"; "common__connect" = "Verbind"; -"common__min" = "Min"; "common__max" = "Max"; -"common__default" = "Standaard"; "common__preview" = "Voorbeeld"; "common__show_all" = "Alles Tonen"; "common__show_details" = "Details Tonen"; @@ -185,7 +183,6 @@ "fee__slow__range" = "±1-2 uur"; "fee__slow__shortRange" = "1-2h"; "fee__slow__longRange" = "± 1-2 uur"; -"fee__minimum__title" = "Minimum"; "fee__minimum__longTitle" = "Minimum"; "fee__minimum__description" = "+2 uur"; "fee__minimum__shortRange" = "+2h"; @@ -198,72 +195,35 @@ "fee__custom__range" = "Afhankelijk van de vergoeding"; "fee__custom__shortRange" = "Afhankelijk van het tarief"; "fee__custom__longRange" = "Afhankelijk van de kosten"; -"lightning__transfer_intro__title" = "Bestedings\nSaldo"; -"lightning__transfer_intro__text" = "Activeer uw bestedingssaldo en profiteer van directe en goedkope transacties met vrienden, familie en winkeliers."; -"lightning__transfer_intro__button" = "Starten"; "lightning__funding__nav_title" = "Bestedingssaldo"; "lightning__funding__title" = "Vul uw bestedingssaldo"; -"lightning__funding__text" = "De makkelijkste optie is om je Bitkit-spaargeld te gebruiken of bitcoin te versturen vanuit een andere wallet."; -"lightning__funding__text_blocked" = "Bitkit biedt momenteel geen Lightning diensten aan in uw land, maar u kunt nog steeds rechtstreeks verbinding maken met andere nodes."; -"lightning__funding__text_blocked_cjit" = "Bitkit biedt momenteel geen Lightning diensten aan in uw land, maar u kunt nog steeds rechtstreeks verbinding maken met andere nodes."; "lightning__funding__button1" = "Overboeking van Spaargeld"; "lightning__funding__button2" = "Gebruik Andere Wallet"; "lightning__funding__button3" = "Handmatige configuratie"; -"lightning__funding_advanced__nav_title" = "Bestedingssaldo"; -"lightning__funding_advanced__title" = "Geavanceerde configuratie"; -"lightning__funding_advanced__text" = "Scan een QR om uw LNURL Kanaal van een andere LSP te claimen, of kies handmatige configuratie."; -"lightning__funding_advanced__button1" = "LNURL Kanaal QR"; -"lightning__funding_advanced__button2" = "Handmatige Configuratie"; "lightning__funding_advanced__button_short" = "Geavanceerde Configuratie"; -"lightning__no_funds__title" = "Geen Geld Beschikbaar"; -"lightning__no_funds__description" = "Voordat u geld van uw spaarsaldo kunt overboeken, moet u bitcoin naar uw Bitkit wallet sturen."; -"lightning__no_funds__fund" = "Wallet aufladen"; "lightning__transfer__nav_title" = "Geld Overboeken"; "lightning__transfer__confirm" = "Bevestig\nalstublieft"; "lightning__transfer__custom_fee" = "Benutzerdefiniert fee"; "lightning__transfer__swipe" = "Veeg om over te boeken"; "lightning__transfer_hw__broadcast_uncertain_title" = "Transactie mogelijk verzonden"; "lightning__transfer_hw__broadcast_uncertain_description" = "Uw financieringstransactie is mogelijk al verzonden. Controleer uw activiteit voordat u het opnieuw probeert."; -"lightning__transfer_hw__open_connect" = "Trezor Connect openen"; "lightning__transfer_hw__reconnect_error_description" = "Verbind uw hardwareapparaat opnieuw, de verbinding met uw telefoon lijkt verbroken."; "lightning__transfer_hw__reconnect_error_title" = "Hardwareapparaat opnieuw verbinden"; -"lightning__transfer_hw__sign_title" = "Onderteken met\nuw apparaat"; -"lightning__transfer_hw__signed_title" = "Transactie\nondertekend"; "lightning__transfer_to_savings_button" = "Overboeken Naar Spaargeld"; "lightning__transfer_to_spending_button" = "Overboeken Naar Bestedingssaldo"; -"lightning__spending_intro__title" = "Overboeking\nnaar bestedingssaldo"; -"lightning__spending_intro__text" = "Boek geld over naar uw bestedingssaldo om te genieten van directe en goedkope transacties met vrienden, familie en winkeliers."; -"lightning__spending_intro__button" = "Starten"; "lightning__transfer_failed__nav_title" = "Overboeking Mislukt"; "lightning__transfer_failed__title" = "Overboeking\nMislukt"; "lightning__transfer_failed__text" = "Sorry, Bitkit kon uw bestedingssaldo niet opzetten of uw overboeking niet voltooien. Probeer het alstublieft opnieuw."; "lightning__transfer_failed__cancel" = "Fehler überprüfen"; "lightning__transfer_failed__continue" = "Opnieuw proberen"; -"lightning__spending_amount__title" = "Overboeking\nnaar bestedingssaldo"; "lightning__spending_amount__quarter" = "25%"; "lightning__spending_amount__error_min__title" = "Minimales Sparguthaben"; -"lightning__spending_amount__error_balance__description" = "Uw spaargeld is niet voldoende voor deze overboeking en de vergoedingen. Probeer een kleiner bedrag."; -"lightning__spending_amount__error_balance__title" = "Onvoldoende spaargeld"; "lightning__spending_amount__error_max__title" = "Maximaler Ausgabensaldo"; "lightning__spending_amount__error_max__description" = "Der Betrag, den Sie auf Ihr Ausgabenkonto übertragen können, ist derzeit auf ₿ {amount} begrenzt."; "lightning__spending_confirm__network_fee" = "Netwerkkosten"; "lightning__spending_confirm__lsp_fee" = "Servicekosten"; "lightning__spending_confirm__amount" = "Naar bestedingssaldo"; -"lightning__spending_confirm__background_setup" = "Op achtergrond instellen"; "lightning__spending_confirm__total" = "Totaal"; -"lightning__spending_confirm__default" = "Gebruik Standaard"; -"lightning__spending_confirm__fees_changed_title" = "Vergoedingen gewijzigd"; -"lightning__spending_confirm__fees_changed_service" = "De vergoedingen zijn {amount} hoger nu uw bestelling is aangemaakt. Veeg opnieuw om te bevestigen."; -"lightning__spending_confirm__fees_changed_network" = "De vergoedingen zijn {amount} hoger, omdat de vergoedingstarieven of uw spaargeld zijn gewijzigd terwijl u op dit scherm was. Veeg opnieuw om te bevestigen."; -"lightning__spending_advanced__title" = "Ontvangst\ncapaciteit"; -"lightning__spending_advanced__fee" = "Liquiditeitskosten"; -"lightning__spending_advanced__error_balance__description" = "Uw spaargeld is niet voldoende voor de liquiditeitsvergoeding voor deze ontvangstcapaciteit. Kies een kleiner bedrag."; -"lightning__spending_advanced__error_balance__title" = "Onvoldoende saldo"; -"lightning__spending_advanced__error_max__title" = "Maximale ontvangstcapaciteit"; -"lightning__spending_advanced__error_max__description" = "De ontvangstcapaciteit is momenteel beperkt tot ₿ {amount}."; -"lightning__liquidity__title" = "Liquiditeit\n& routering"; -"lightning__liquidity__text" = "Uw Bestedingssaldo gebruikt het Lightning Network om uw betalingen goedkoper, sneller en meer privé te maken.\n\nDit werkt als internettoegang, maar u betaalt voor liquiditeit en routering in plaats van bandbreedte.\n\nDeze configuratie bevat eenmalige kosten."; -"lightning__liquidity__label" = "Bestedingssaldo Liquiditeit"; "lightning__savings_intro__title" = "Overboeking\nnaar spaargeld"; "lightning__savings_intro__text" = "Boek uw bestedingssaldo over naar uw spaargeld om uw Bitcoin langdurig te bewaren en uw toekomst veilig te stellen."; "lightning__savings_intro__button" = "Starten"; @@ -287,9 +247,6 @@ "lightning__savings_interrupted__nav_title" = "Overboeking\nonderbroken"; "lightning__savings_interrupted__title" = "Houd Bitkit\nopen"; "lightning__savings_interrupted__text" = "Het geld is nog niet overgeboekt. Bitkit zal proberen de overboeking gedurende 30 minuten te initiëren. Houd uw app open."; -"lightning__transfer_success__nav_title" = "Overboeking Voltooid"; -"lightning__transfer_success__title_spending" = "Geld verplaatst\nnaar bestedingssaldo"; -"lightning__transfer_success__text_spending" = "Uw geld is overgeboekt en uw bestedingssaldo is klaar voor gebruik."; "lightning__transfer_success__title_savings" = "Geld verplaatst\nnaar spaargeld"; "lightning__transfer_success__text_savings" = "Uw geld is overgeboekt. U kunt dit geld direct gebruiken."; "lightning__external__nav_title" = "Lightning-verbinding"; @@ -312,12 +269,6 @@ "lightning__spending_label" = "Bestedingssaldo"; "lightning__receiving_label" = "Ontvangstcapaciteit"; "lightning__setting_up_header" = "Onderweg"; -"lightning__setting_up_text" = "Even geduld, uw overboeking is onderweg. Dit zou ±10 minuten moeten duren."; -"lightning__setting_up_step1" = "Betaling Verwerken"; -"lightning__setting_up_step2" = "Betaling Voltooid"; -"lightning__setting_up_step3" = "In de Wacht om te Worden Geopend"; -"lightning__setting_up_step4" = "Verbinding Wordt Gereed Gemaakt"; -"lightning__setting_up_button" = "Ga Door Met Bitkit"; "lightning__node_info" = "Lightning Node"; "lightning__node_id" = "LDK Node ID"; "lightning__node_disconnected" = "verbinding verbroken"; @@ -389,9 +340,9 @@ "lightning__force_init_msg" = "Uw geld zal in ±14 dagen beschikbaar zijn."; "lightning__force_failed_title" = "Geforceerde Overboeking Mislukt"; "lightning__force_failed_msg" = "Het geld kon niet worden teruggeboekt naar uw spaarsaldo. Probeer het opnieuw."; -"lightning__force_channels_skipped" = "Sommige verbindingen konden niet worden gesloten."; "lightning__channel_opened_title" = "Bestedingssaldo Gereed"; "lightning__channel_opened_msg" = "U kunt nu directe betalingen maken aan wie u wilt, wanneer u wilt."; +"lightning__funding__button_hw" = "Overboeken van HW-wallet"; "lightning__order_given_up_title" = "Directe Betalingen Instellen Mislukt"; "lightning__order_given_up_msg" = "The setup process expired. Please try again."; "lightning__order_expired_title" = "Directe Betalingen Instellen Mislukt"; @@ -410,6 +361,9 @@ "lightning__order_state__open" = "Verbinding open"; "lightning__order_state__inactive" = "Verbinding inactief"; "lightning__peer_disconnected" = "Peer verbinding verbroken."; +"lightning__transfer_unavailable__nav_title" = "Overboeking niet beschikbaar"; +"lightning__transfer_unavailable__text" = "Automatische overboekingen naar je bestedingssaldo zijn niet meer beschikbaar. Gebruik je eigen node of stel een verbinding in met een andere aanbieder."; +"lightning__transfer_unavailable__title" = "Handmatige instelling\nvereist"; "lightning__wait_text_top" = "Bitte warte bis sich Bitkit mit dem Zahlungsnetzwerk verbunden hat (±10 Sekunden)."; "lightning__wait_text_bottom" = "Verbinden & Synchronisieren…"; "onboarding__tos_header" = "Bitkit\ngebruiksvoorwaarden"; @@ -431,7 +385,6 @@ "onboarding__slide1_header" = "Directe\nbetalingen"; "onboarding__slide1_text" = "Gib bitcoin aus, schneller denn je zuvor. Genieße sofortige und günstige Zahlungen mit Freunden, Familie und Händlern."; "onboarding__slide1_text_alt" = "Geniet van directe en goedkope betalingen met vrienden, familie en winkeliers op het Lightning Network."; -"onboarding__slide1_note" = "*Bitkit biedt momenteel geen Lightning diensten aan in uw land, maar u kunt nog steeds verbinding maken met andere nodes."; "onboarding__slide2_header" = "Bitcoiners,\nborderless"; "onboarding__slide2_text" = "Übernimm die Kontrolle über dein digitales Leben mit portablen Profilen und Kontakten, die du bezahlen kannst."; "onboarding__slide3_header" = "Privacy is\ngeen misdaad"; @@ -554,8 +507,6 @@ "other__lnurl_withdr_error_minmax" = "Onjuiste LNURL opname parameters, min/max niet correct ingesteld."; "other__lnurl_withdr_success_title" = "Opname Verzocht"; "other__lnurl_withdr_success_msg" = "Uw opname is succesvol aangevraagd. Wachten op betaling."; -"other__notification_registration_failed_description" = "Bitkit kon zich niet registreren voor push notificaties."; -"other__notification_registration_failed_title" = "Notificatie Registratie Mislukt"; "other__phone_settings" = "Telefoneinstellungen öffnen"; "other__bt_error_retrieve" = "Overboeking Mislukt"; "other__bt_error_connect" = "LSP-Node kann derzeit nicht als Peer hinzugefügt werden."; @@ -571,14 +522,7 @@ "other__transfer_notification__body" = "Öffnen Sie Bitkit, um Ihre Übertragung abzuschließen"; "other__scan__error__generic" = "Bitkit kan de verstrekte data niet lezen."; "other__scan__error__expired" = "Deze Lightning factuur is verlopen."; -"other__gift__claimed__text" = "Sorry, u bent te laat! Alle cadeaus voor deze code zijn al geclaimd."; -"other__gift__claiming__text" = "Uw Bitkit cadeau code wordt geclaimd..."; -"other__gift__claiming__title" = "Cadeau Claimen"; -"other__gift__error__title" = "Cadeau Code Fout"; -"other__gift__error__text" = "Bitkit kon het geld niet claimen. Probeer het later opnieuw of neem contact met ons op."; "other__gift__error__button" = "OK"; -"other__gift__used__title" = "Gebruikte Code"; -"other__gift__used__text" = "Deze Bitkit cadeau code is al gebruikt en het geld is uitbetaald."; "other__gift__used__button" = "OK"; "other__shop__intro__title" = "Winkel"; "other__shop__intro__description" = "Zet uw leven op de Bitcoin standaard. Besteed uw Bitcoin aan digitale cadeaukaarten, eSIMs, telefoon opwaarderingen en meer."; @@ -761,6 +705,7 @@ "pubky_auth__homeserver" = "Homeserver"; "pubky_auth__signup_description" = "Maak een nieuwe Pubky-identiteit aan op deze homeserver. Ga alleen verder als u deze vertrouwt."; "pubky_auth__approval_failed" = "Autorisatie mislukt"; +"wallet__onchain_abandon_transfer_note" = "Overboekingen naar je bestedingssaldo zijn niet meer beschikbaar. Bitkit controleert eerst het netwerk en stuurt de munten van deze overboeking dan terug naar je wallet, zodat de oorspronkelijke overboeking nooit bevestigd wordt. Dit kost netwerkkosten."; "watch_only_accounts__active_section" = "Actieve accounts"; "watch_only_accounts__copy_xpub" = "xpub kopiëren"; "watch_only_accounts__description" = "Elke goedgekeurde Paykit-server krijgt een apart Bitcoin-account. Schakel volgen uit om een account te ontladen zonder de walletgeschiedenis te verwijderen."; @@ -837,21 +782,17 @@ "settings__widgets__reset_widgets_dialog_title" = "Widgets resetten?"; "settings__widgets__reset_widgets_dialog_description" = "Weet je zeker dat je de widgets wilt resetten? De standaard widgetset met standaardconfiguraties wordt weergegeven."; "settings__widgets__reset_suggestions" = "Suggestiekaarten resetten"; -"settings__notifications__nav_title" = "Achtergrondbetalingen"; -"settings__notifications__intro__title" = "Ontvang\nPassief"; -"settings__notifications__intro__text" = "Schakel notificaties in om betaald te worden, zelfs wanneer uw Bitkit app gesloten is."; +"settings__notifications__nav_title" = "Meldingen"; +"settings__notifications__intro__title" = "Blijf\nop de hoogte"; +"settings__notifications__intro__text" = "Zet meldingen aan om herinneringen te krijgen voor je abonnementen en betaalverzoeken."; "settings__notifications__intro__button" = "Inschakelen"; -"settings__notifications__settings__toggle" = "Ontvang geld wanneer Bitkit gesloten is"; -"settings__notifications__settings__enabled" = "Achtergrondbetalingen zijn ingeschakeld. U kunt geld ontvangen zelfs wanneer de app gesloten is (als uw apparaat verbonden is met internet)."; -"settings__notifications__settings__disabled" = "Schakel achtergrondbetalingen in om geld te ontvangen zelfs wanneer de app gesloten is (als uw apparaat verbonden is met internet)."; -"settings__notifications__settings__denied" = "Achtergrondbetalingen zijn uitgeschakeld, omdat u notificaties heeft geweigerd."; -"settings__notifications__settings__preview__title" = "Betaling Ontvangen"; -"settings__notifications__settings__preview__text" = "Open Bitkit om details te bekijken"; +"settings__notifications__settings__toggle" = "Meldingen toestaan"; +"settings__notifications__settings__enabled" = "Meldingen staan aan. Bitkit kan je herinneren aan abonnementen en betaalverzoeken."; +"settings__notifications__settings__disabled" = "Zet meldingen aan om herinneringen te krijgen voor je abonnementen en betaalverzoeken."; +"settings__notifications__settings__denied" = "Meldingen staan uit, omdat je ze hebt geweigerd."; "settings__notifications__settings__preview__time" = "3m geleden"; -"settings__notifications__settings__privacy__label" = "Privacy"; -"settings__notifications__settings__privacy__text" = "Bedrag opnemen in notificaties"; "settings__notifications__settings__notifications__label" = "Notificaties"; -"settings__notifications__settings__notifications__text" = "Om achtergrondbetalingen in te schakelen, schakel notificaties in via iOS Bitkit Instellingen."; +"settings__notifications__settings__notifications__text" = "Om herinneringen te ontvangen, schakel meldingen in via iOS Bitkit Instellingen."; "settings__notifications__settings__button__enabled" = "Aanpassen in {platform} Bitkit Instellingen"; "settings__notifications__settings__button__disabled" = "Inschakelen in {platform} Bitkit Instellingen"; "settings__quickpay__nav_title" = "QuickPay"; @@ -1400,27 +1341,11 @@ "wallet__receive_note_placeholder" = "Desgewenst bericht aan de betaler"; "wallet__receive_show_qr" = "Toon QR Code"; "wallet__receive_text_lnfunds" = "Wilt u Lightning ₿ ontvangen?"; -"wallet__receive_background_setup_text" = "Schakel achtergrondinstallatie in om Bitkit veilig te verlaten terwijl uw saldo wordt geconfigureerd."; -"wallet__receive_background_setup_toggle" = "Achtergrondinstallatie inschakelen"; "wallet__receive_spending" = "Ontvang Lightning-geld"; "wallet__receive_spending_text" = "Geniet van directe en goedkope bitcoin-betalingen op het Lightning Network."; "wallet__receive_spending_title" = "Ontvang op bestedingssaldo"; -"wallet__receive_connect_initial" = "Om uw bestedingssaldo in te stellen, worden een {networkFee} netwerkvergoeding en {serviceFee} serviceprovidervergoeding afgetrokken."; -"wallet__receive_connect_additional" = "Om meer direct Bitcoin te ontvangen, moet Bitkit uw liquiditeit verhogen. Een {networkFee} netwerkvergoeding en {serviceFee} serviceprovidervergoeding worden afgetrokken van het bedrag dat u heeft opgegeven."; -"wallet__receive_liquidity__nav_title" = "Bestedingssaldo Instellen"; -"wallet__receive_liquidity__nav_title_additional" = "Erhöhung der Empfangskapazität"; -"wallet__receive_liquidity__invalid_quote" = "Bitkit heeft een ongeldig liquiditeitsvoorstel ontvangen. Probeer het opnieuw."; -"wallet__receive_liquidity__text" = "Uw Bestedingssaldo gebruikt het Lightning Network om uw betalingen goedkoper, sneller en meer privé te maken.\n\nDit werkt als internettoegang, maar u betaalt voor liquiditeit en routering in plaats van bandbreedte.\n\nDeze configuratie bevat eenmalige kosten."; -"wallet__receive_liquidity__text_additional" = "Uw Bestedingssaldo gebruikt het Lightning Network om uw betalingen goedkoper, sneller en meer privé te maken.\n\nDit werkt als internettoegang, maar u betaalt voor liquiditeit en routering in plaats van bandbreedte.\n\nBitkit moet de ontvangstcapaciteit van uw bestedingssaldo verhogen om deze betaling te verwerken."; -"wallet__receive_liquidity__label" = "Bestedingssaldo Liquiditeit"; -"wallet__receive_liquidity__label_additional" = "Extra Bestedingssaldo Liquiditeit"; "wallet__receive_cjit_error" = "Transactie Mislukt"; "wallet__receive_cjit_error_msg" = "Fehler beim Senden der Gelder auf dein Ausgabenkonto."; -"wallet__receive_cjit_error_max__title" = "Maximale ontvangstcapaciteit"; -"wallet__receive_cjit_error_max__description" = "Het maximum dat u nu op uw bestedingssaldo kunt ontvangen is ₿ {amount}."; -"wallet__receive_cjit_error_node_capacity__title" = "Extra capaciteit niet beschikbaar"; -"wallet__receive_cjit_error_node_capacity__description" = "Bitkit kan op dit moment geen extra bestedingscapaciteit aan deze wallet toevoegen."; -"wallet__receive_will" = "U ontvangt"; "wallet__receive_ldk_init" = "Ausgabensaldo Initialisierung..."; "wallet__minimum" = "MINIMUM"; "wallet__activity" = "Activiteit"; @@ -1609,7 +1534,7 @@ "subscriptions__name_placeholder" = "Naam abonnement"; "subscriptions__ongoing" = "Doorlopend"; "subscriptions__overview" = "Overzicht"; -"subscriptions__payment_due_description" = "Open Bitkit om een abonnementsbetaling te bekijken."; +"subscriptions__payment_due_description" = "Open Bitkit om de betaling te bekijken"; "subscriptions__payment_due_title" = "Abonnementsbetaling verschuldigd"; "subscriptions__payments" = "Betalingen"; "subscriptions__per_day" = "per dag"; @@ -1780,9 +1705,6 @@ "wallet__onchain_broadcast_rejected_note" = "Deze betaling kan het netwerk toch hebben bereikt. Verstuur hem niet opnieuw. {reason}"; "wallet__onchain_broadcast_unconfirmed" = "Verzending onbevestigd"; "wallet__onchain_broadcast_unconfirmed_note" = "De betaling is mogelijk verzonden. Verstuur hem niet opnieuw."; -"wallet__onchain_funding_rejected" = "De financieringstransactie is geweigerd. Probeer deze order niet opnieuw te financieren."; -"wallet__onchain_funding_unknown" = "De financieringstransactie is mogelijk verzonden. Probeer deze order niet opnieuw te financieren."; -"wallet__onchain_funding_followup_failed" = "De financiering is verzonden, maar de lokale ordertracking kon niet worden opgeslagen. Financier deze order niet opnieuw."; "wallet__onchain_retry_original" = "Oorspronkelijke betaling opnieuw proberen"; "wallet__onchain_retry_fee" = "Vergoedingstarief (sat/vB)"; "wallet__onchain_retry_note" = "Probeer {amount} sats naar {address} opnieuw met dezelfde oorspronkelijke coins. Het bedrag voor de ontvanger blijft ongewijzigd. Voer het netwerkvergoedingstarief in sat/vB in. Dit kan geen aparte betaling aanmaken."; diff --git a/Bitkit/Resources/Localization/pl.lproj/Localizable.strings b/Bitkit/Resources/Localization/pl.lproj/Localizable.strings index 2d32cb55b..013309e94 100644 --- a/Bitkit/Resources/Localization/pl.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pl.lproj/Localizable.strings @@ -4,8 +4,8 @@ "cards__discount__description" = "Saldo wydatków"; "cards__quickpay__title" = "QuickPay"; "cards__quickpay__description" = "Skanuj i zapłać"; -"cards__notifications__description_alt" = "Gdy Bitkit jest zamknięty"; -"cards__notifications__title" = "Otrzymuj płatności"; +"cards__notifications__description_alt" = "Przypomnienia o płatnościach"; +"cards__notifications__title" = "Powiadomienia"; "cards__invite__title" = "Zaproś"; "cards__invite__description" = "Udostępnij Bitkit"; "cards__lightning__title" = "Wydawaj"; @@ -151,9 +151,7 @@ "common__learn_more" = "Dowiedz się więcej"; "common__understood" = "Rozumiem"; "common__connect" = "Połącz"; -"common__min" = "Min"; "common__max" = "Max"; -"common__default" = "Domyślne"; "common__preview" = "Podgląd"; "common__show_all" = "Pokaż wszystko"; "common__show_details" = "Pokaż szczegóły"; @@ -185,7 +183,6 @@ "fee__slow__range" = "±1-2 godzin"; "fee__slow__shortRange" = "1-2h"; "fee__slow__longRange" = "± 1-2 godziny"; -"fee__minimum__title" = "Minimum"; "fee__minimum__longTitle" = "Minimum"; "fee__minimum__description" = "+2 godziny"; "fee__minimum__shortRange" = "+2h"; @@ -198,74 +195,37 @@ "fee__custom__range" = "Zależy od opłaty"; "fee__custom__shortRange" = "Zależy od opłaty"; "fee__custom__longRange" = "Zależy od opłaty transakcyjnej"; -"lightning__transfer_intro__title" = "Saldo \nWydatków"; -"lightning__transfer_intro__text" = "Zasil swoje saldo wydatków, aby cieszyć się natychmiastowymi i tanimi transakcjami z przyjaciółmi, rodziną i sprzedawcami."; -"lightning__transfer_intro__button" = "Rozpocznij"; "lightning__funding__nav_title" = "Saldo wydatków"; "lightning__funding__title" = "Zasil swoje saldo wydatków"; -"lightning__funding__text" = "Najłatwiejszą opcją jest użycie oszczędności Bitkit lub wysłanie bitcoinów z innego portfela."; -"lightning__funding__text_blocked" = "Bitkit nie świadczy obecnie usług Lightning w Twoim kraju, ale nadal możesz łączyć się z innymi węzłami."; -"lightning__funding__text_blocked_cjit" = "Bitkit nie świadczy obecnie usług Lightning w Twoim kraju, ale nadal możesz łączyć się bezpośrednio z innymi węzłami."; "lightning__funding__button1" = "Prześlij z Oszczędności"; "lightning__funding__button2" = "Użyj innego portfela"; "lightning__funding__button3" = "Ręczna konfiguracja"; -"lightning__funding_advanced__nav_title" = "Saldo do wydawania"; -"lightning__funding_advanced__title" = "Ustawienia zaawansowane"; -"lightning__funding_advanced__text" = "Zeskanuj kod QR, aby odebrać swój kanał LNURL od innego LSP, lub wybierz ręczną konfigurację."; -"lightning__funding_advanced__button1" = "Kod QR kanału LNURL"; -"lightning__funding_advanced__button2" = "Konfiguracja ręczna"; "lightning__funding_advanced__button_short" = "Ustawienia zaawansowane"; -"lightning__no_funds__title" = "Brak dostępnych środków"; -"lightning__no_funds__description" = "Zanim będzie można przelać środki z salda oszczędności, należy wysłać bitcoiny do portfela Bitkit."; -"lightning__no_funds__fund" = "Zasil portfel"; "lightning__transfer__nav_title" = "Prześlij środki"; "lightning__transfer__confirm" = "Proszę \npotwierdzić"; "lightning__transfer__custom_fee" = "Niestandardowa opłata transakcyjna"; "lightning__transfer__swipe" = "Przesuń, aby przesłać"; "lightning__transfer_hw__broadcast_uncertain_title" = "Transakcja mogła zostać wysłana"; "lightning__transfer_hw__broadcast_uncertain_description" = "Twoja transakcja zasilająca mogła zostać rozgłoszona. Sprawdź swoją aktywność, zanim spróbujesz ponownie."; -"lightning__transfer_hw__open_connect" = "Otwórz Trezor Connect"; "lightning__transfer_hw__reconnect_error_description" = "Połącz ponownie urządzenie sprzętowe, wygląda na to, że zostało odłączone od telefonu."; "lightning__transfer_hw__reconnect_error_title" = "Połącz ponownie urządzenie"; -"lightning__transfer_hw__sign_title" = "Podpisz\nurządzeniem"; -"lightning__transfer_hw__signed_title" = "Transakcja\npodpisana"; "lightning__transfer_to_savings_button" = "Prześlij na oszczędności"; "lightning__transfer_to_spending_button" = "Prześlij na wydatki"; -"lightning__spending_intro__title" = "Prześlij \nna saldo wydatków"; -"lightning__spending_intro__text" = "Przelewaj środki na swoje saldo wydatków, aby cieszyć się natychmiastowymi i tanimi transakcjami z przyjaciółmi, rodziną i sprzedawcami."; -"lightning__spending_intro__button" = "Rozpocznij"; "lightning__transfer_failed__nav_title" = "Błąd podczas przelewu środków"; "lightning__transfer_failed__title" = "Transfer środków \nniepowiódł się"; "lightning__transfer_failed__text" = "Przepraszamy, Bitkit nie mógł skonfigurować salda wydatków lub zakończyć transferu. Proszę spróbować ponownie."; "lightning__transfer_failed__cancel" = "Sprawdź błąd"; "lightning__transfer_failed__continue" = "Spróbuj ponownie"; -"lightning__spending_amount__title" = "Prześlij \nna saldo wydatków"; "lightning__spending_amount__quarter" = "25%"; "lightning__spending_amount__error_min__title" = "Minimalne saldo oszczędności"; "lightning__spending_amount__error_min__description" = "Do utworzenia salda wydatków wymagana jest minimalna kwota ₿ {amount}."; -"lightning__spending_amount__error_balance__description" = "Twoje oszczędności nie pokryją tego transferu i jego opłat. Spróbuj mniejszej kwoty."; -"lightning__spending_amount__error_balance__title" = "Niewystarczające oszczędności"; "lightning__spending_amount__error_max__title" = "Maksymalne saldo wydatków"; "lightning__spending_amount__error_max__description" = "Kwota, którą można przelać na saldo wydatków, jest obecnie ograniczona do ₿ {amount}."; "lightning__spending_amount__error_max__description_zero" = "Transfer do salda wydatków jest ograniczony ze względu na politykę płynności. Aby uzyskać szczegółowe informacje, proszę odwiedzić Centrum pomocy."; "lightning__spending_confirm__network_fee" = "Opłaty sieciowe"; "lightning__spending_confirm__lsp_fee" = "Opłaty serwisowe"; "lightning__spending_confirm__amount" = "Do salda wydatków"; -"lightning__spending_confirm__background_setup" = "Skonfiguruj w tle"; "lightning__spending_confirm__total" = "Razem"; -"lightning__spending_confirm__default" = "Użyj ustawień domyślnych"; -"lightning__spending_confirm__fees_changed_title" = "Opłaty się zmieniły"; -"lightning__spending_confirm__fees_changed_service" = "Po utworzeniu zamówienia opłaty są wyższe o {amount}. Przesuń ponownie, aby potwierdzić."; -"lightning__spending_confirm__fees_changed_network" = "Opłaty są wyższe o {amount}, ponieważ stawki opłat lub saldo oszczędności zmieniły się podczas wyświetlania tego ekranu. Przesuń ponownie, aby potwierdzić."; -"lightning__spending_advanced__title" = "Pojemność \nodbiorcza"; -"lightning__spending_advanced__fee" = "Opłata za płynność"; -"lightning__spending_advanced__error_balance__description" = "Twoje oszczędności nie pokryją opłaty za płynność dla tej zdolności odbioru. Wybierz mniejszą kwotę."; -"lightning__spending_advanced__error_balance__title" = "Za mało środków"; -"lightning__spending_advanced__error_max__title" = "Maksymalna zdolność odbioru"; -"lightning__spending_advanced__error_max__description" = "Zdolność odbioru jest obecnie ograniczona do ₿ {amount}."; -"lightning__liquidity__title" = "Płynność \ni routing"; -"lightning__liquidity__text" = "Saldo Wydatków wykorzystuje sieć Lightning, aby Twoje płatności były tańsze, szybsze i bardziej prywatne.\n\nDziała to podobnie jak dostęp do Internetu, ale płacą Państwo za płynność i routing zamiast za przepustowość.\n\nTa konfiguracja obejmuje pewne jednorazowe koszty."; -"lightning__liquidity__label" = "Płynność Salda Wydatków"; "lightning__savings_intro__title" = "Prześlij \nna saldo oszczędności"; "lightning__savings_intro__text" = "Przenieś saldo wydatków do oszczędności, aby przechowywać Bitcoiny długoterminowo i zabezpieczyć swoją przyszłość."; "lightning__savings_intro__button" = "Rozpocznij"; @@ -289,9 +249,6 @@ "lightning__savings_interrupted__nav_title" = "Transfer środków\nprzerwany"; "lightning__savings_interrupted__title" = "Pozostaw Bitkit\notwarty"; "lightning__savings_interrupted__text" = "Środki nie zostały jeszcze przelane. Bitkit spróbuje zainicjować transfer w ciągu najbliższych 30 minut. Proszę pozostawić aplikację otwartą."; -"lightning__transfer_success__nav_title" = "Transfer środków powiódł się"; -"lightning__transfer_success__title_spending" = "Środki przeniesione\nna saldo wydatków"; -"lightning__transfer_success__text_spending" = "Środki zostały przeniesione i saldo wydatków jest gotowe do użycia."; "lightning__transfer_success__title_savings" = "Środki przeniesione\nna saldo oszczędności"; "lightning__transfer_success__text_savings" = "Środki zostały przelane i są gotowe do natychmiastowego użycia."; "lightning__external__nav_title" = "Połączenie z Lightning"; @@ -314,12 +271,6 @@ "lightning__spending_label" = "Saldo wydatków"; "lightning__receiving_label" = "Zdolność odbioru"; "lightning__setting_up_header" = "W transferze"; -"lightning__setting_up_text" = "Proszę czekać, transfer środków jest w toku. Powinno to zająć ±10 minut."; -"lightning__setting_up_step1" = "Przetwarzanie płatności"; -"lightning__setting_up_step2" = "Płatność powiodła się"; -"lightning__setting_up_step3" = "W kolejce do otwarcia"; -"lightning__setting_up_step4" = "Otwieranie połączenia"; -"lightning__setting_up_button" = "Kontunuuj korzystanie z Bitkit\'a"; "lightning__node_info" = "Węzeł Lightning"; "lightning__node_id" = "Identyfikator węzła LDK"; "lightning__node_disconnected" = "odłączony"; @@ -391,9 +342,9 @@ "lightning__force_init_msg" = "Środki będą dostępne w ciągu ±14 dni."; "lightning__force_failed_title" = "Wymuszenie transferu nie powiodło się"; "lightning__force_failed_msg" = "Nie można przelać środków z powrotem na saldo oszczędności. Proszę spróbować ponownie."; -"lightning__force_channels_skipped" = "Niektórych połączeń nie udało się zamknąć."; "lightning__channel_opened_title" = "Saldo wydatków gotowe"; "lightning__channel_opened_msg" = "Teraz możesz zapłacić każdemu, gdziekolwiek, natychmiast."; +"lightning__funding__button_hw" = "Prześlij z portfela HW"; "lightning__order_given_up_title" = "Konfiguracja płatności natychmiastowych nie powiodła się"; "lightning__order_given_up_msg" = "Proces konfiguracji wygasł. Proszę spróbować ponownie."; "lightning__order_expired_title" = "Konfiguracja płatności natychmiastowych nie powiodła się"; @@ -412,6 +363,9 @@ "lightning__order_state__open" = "Połączenie otwarte"; "lightning__order_state__inactive" = "Połączenie nieaktywne"; "lightning__peer_disconnected" = "Peer rozłączony."; +"lightning__transfer_unavailable__nav_title" = "Przesłanie niedostępne"; +"lightning__transfer_unavailable__text" = "Automatyczne przesyłanie środków na saldo wydatków nie jest już dostępne. Użyj własnego węzła lub skonfiguruj połączenie z innym dostawcą."; +"lightning__transfer_unavailable__title" = "Wymagana ręczna\nkonfiguracja"; "lightning__wait_text_top" = "Proszę poczekać, aż Bitkit połączy się z siecią płatności (±10 sekund)."; "lightning__wait_text_bottom" = "Podłączanie i synchronizacja..."; "onboarding__tos_header" = "Bitkit\nwarunki użytkowania"; @@ -433,7 +387,6 @@ "onboarding__slide1_header" = "Płatności\nnatychmiastowe"; "onboarding__slide1_text" = "Wydawaj bitcoiny szybciej niż kiedykolwiek. Ciesz się natychmiastowymi i tanimi płatnościami z przyjaciółmi, rodziną i sprzedawcami."; "onboarding__slide1_text_alt" = "Ciesz się natychmiastowymi i tanimi płatnościami z przyjaciółmi, rodziną i sprzedawcami w sieci Lightning Network."; -"onboarding__slide1_note" = "Bitkit nie świadczy obecnie usług Lightning w Twoim kraju, ale nadal możesz łączyć się z innymi węzłami."; "onboarding__slide2_header" = "Bitcoinerzy,\nbez granic"; "onboarding__slide2_text" = "Przejmij kontrolę nad swoim cyfrowym życiem dzięki przenośnym profilom i płatnym kontaktom."; "onboarding__slide3_header" = "Prywatność nie jest\nprzestępstwem"; @@ -556,8 +509,6 @@ "other__lnurl_withdr_error_minmax" = "Nieprawidłowe parametry wypłaty LNURL, wartości min/max nie zostały poprawnie ustawione."; "other__lnurl_withdr_success_title" = "Złożono żądanie wypłaty"; "other__lnurl_withdr_success_msg" = "Wypłata została pomyślnie zlecona. Oczekiwanie na płatność."; -"other__notification_registration_failed_description" = "Bitkit nie mógł zarejestrować się do powiadomień push."; -"other__notification_registration_failed_title" = "Rejestracja powiadomień nie powiodła się"; "other__phone_settings" = "Otwórz ustawienia telefonu"; "other__bt_error_retrieve" = "Transfer nie powiódł się"; "other__bt_error_connect" = "Nie udało się dodać węzła LSP jako peera w tym momencie."; @@ -573,14 +524,7 @@ "other__transfer_notification__body" = "Otwórz Bitkit, aby dokończyć transfer"; "other__scan__error__generic" = "Bitkit nie może odczytać dostarczonych danych."; "other__scan__error__expired" = "Ta faktura Lightning wygasła."; -"other__gift__claimed__text" = "Przepraszamy, spóźniłeś się! Wszystkie prezenty dla tego kodu zostały już odebrane."; -"other__gift__claiming__text" = "Odbiór kodu podarunkowego Bitkit..."; -"other__gift__claiming__title" = "Odbiór prezentu"; -"other__gift__error__title" = "Błąd kodu podarunkowego"; -"other__gift__error__text" = "Bitkit nie mógł odebrać środków. Proszę spróbować ponownie później lub skontaktować się z nami."; "other__gift__error__button" = "OK"; -"other__gift__used__title" = "Użyty kod"; -"other__gift__used__text" = "Ten kod podarunkowy Bitkit został już wykorzystany, a środki zostały wypłacone."; "other__gift__used__button" = "OK"; "other__shop__intro__title" = "Sklep"; "other__shop__intro__description" = "Przestaw się na standard Bitcoina. Wydawaj swoje Bitcoiny na karty podarunkowe, eSIM-y, doładowania telefonu i inne usługi."; @@ -763,6 +707,7 @@ "pubky_auth__homeserver" = "Serwer domowy"; "pubky_auth__signup_description" = "Utwórz nową tożsamość Pubky na tym serwerze domowym. Kontynuuj tylko, jeśli mu ufasz."; "pubky_auth__approval_failed" = "Autoryzacja nie powiodła się"; +"wallet__onchain_abandon_transfer_note" = "Przelewy na saldo wydatków nie są już dostępne. Bitkit najpierw sprawdza sieć, a potem odsyła monety z tego przelewu do Twojego portfela, aby pierwotny przelew nigdy nie został potwierdzony. Wiąże się to z opłatą sieciową."; "watch_only_accounts__active_section" = "Aktywne konta"; "watch_only_accounts__copy_xpub" = "Kopiuj xpub"; "watch_only_accounts__description" = "Każdy zatwierdzony serwer Paykit otrzymuje osobne konto Bitcoin. Wyłącz śledzenie, aby przestać wczytywać konto bez usuwania historii jego portfela."; @@ -839,21 +784,17 @@ "settings__widgets__reset_widgets_dialog_title" = "Resetować widgety?"; "settings__widgets__reset_widgets_dialog_description" = "Czy na pewno chcesz zresetować widgety? Zostanie wyświetlony domyślny zestaw widgetów z domyślnymi konfiguracjami."; "settings__widgets__reset_suggestions" = "Resetuj karty sugestii"; -"settings__notifications__nav_title" = "Płatności w tle"; -"settings__notifications__intro__title" = "Otrzymuj płatności\npasywnie"; -"settings__notifications__intro__text" = "Włącz powiadomienia, aby otrzymywać płatności, nawet gdy aplikacja Bitkit jest zamknięta."; +"settings__notifications__nav_title" = "Powiadomienia"; +"settings__notifications__intro__title" = "Bądź\nna bieżąco"; +"settings__notifications__intro__text" = "Włącz powiadomienia, aby otrzymywać przypomnienia o subskrypcjach i prośbach o płatność."; "settings__notifications__intro__button" = "Włącz"; -"settings__notifications__settings__toggle" = "Otrzymuj płatności przy zamkniętym Bitkit"; -"settings__notifications__settings__enabled" = "Płatności w tle są włączone. Możesz otrzymywać środki nawet przy zamkniętej aplikacji (jeśli urządzenie jest połączone z Internetem)."; -"settings__notifications__settings__disabled" = "Włącz płatności w tle, aby otrzymywać środki nawet przy zamkniętej aplikacji (jeśli urządzenie jest połączone z Internetem)."; -"settings__notifications__settings__denied" = "Płatności w tle są wyłączone, ponieważ odmówiono powiadomień."; -"settings__notifications__settings__preview__title" = "Otrzymano płatność"; -"settings__notifications__settings__preview__text" = "Otwórz Bitkit, aby zobaczyć szczegóły"; +"settings__notifications__settings__toggle" = "Zezwól na powiadomienia"; +"settings__notifications__settings__enabled" = "Powiadomienia są włączone. Bitkit może przypominać o subskrypcjach i prośbach o płatność."; +"settings__notifications__settings__disabled" = "Włącz powiadomienia, aby otrzymywać przypomnienia o subskrypcjach i prośbach o płatność."; +"settings__notifications__settings__denied" = "Powiadomienia są wyłączone, ponieważ ich odmówiono."; "settings__notifications__settings__preview__time" = "3 min. temu"; -"settings__notifications__settings__privacy__label" = "Prywatność"; -"settings__notifications__settings__privacy__text" = "Uwzględnij kwotę w powiadomieniach"; "settings__notifications__settings__notifications__label" = "Powiadomienia"; -"settings__notifications__settings__notifications__text" = "Aby włączyć płatności w tle, włącz powiadomienia w ustawieniach Bitkit iOS."; +"settings__notifications__settings__notifications__text" = "Aby otrzymywać przypomnienia, włącz powiadomienia w ustawieniach Bitkit iOS."; "settings__notifications__settings__button__enabled" = "Dostosuj w ustawieniach Bitkit {platform}"; "settings__notifications__settings__button__disabled" = "Włącz w ustawieniach Bitkit {platform}"; "settings__quickpay__nav_title" = "QuickPay"; @@ -1404,27 +1345,11 @@ "wallet__receive_note_placeholder" = "Opcjonalna notatka dla płatnika"; "wallet__receive_show_qr" = "Skanuj kod QR"; "wallet__receive_text_lnfunds" = "Chcesz otrzymać fundusze Lightning?"; -"wallet__receive_background_setup_text" = "Włącz konfigurację w tle, aby bezpiecznie wyjść z Bitkit podczas konfigurowania salda."; -"wallet__receive_background_setup_toggle" = "Włącz konfigurację w tle"; "wallet__receive_spending" = "Odbierz środki Lightning"; "wallet__receive_spending_text" = "Korzystaj z natychmiastowych i tanich płatności bitcoin w sieci Lightning."; "wallet__receive_spending_title" = "Odbierz na saldo wydatków"; -"wallet__receive_connect_initial" = "Aby skonfigurować saldo wydatków, zostanie pobrana opłata sieciowa {networkFee} oraz opłata dostawcy usługi {serviceFee}"; -"wallet__receive_connect_additional" = "Bitkit musi zwiększyć Twoją płynność, abyś mógł otrzymywać więcej natychmiastowych płatności. Od wskazanej kwoty zostanie pobrana opłata sieciowa {networkFee} i serwisowa {serviceFee}."; -"wallet__receive_liquidity__nav_title" = "Konfiguracja salda wydatków"; -"wallet__receive_liquidity__nav_title_additional" = "Zwiększenie zdolności odbioru"; -"wallet__receive_liquidity__invalid_quote" = "Bitkit otrzymał nieprawidłową ofertę płynności. Spróbuj ponownie."; -"wallet__receive_liquidity__text" = "Saldo Wydatków wykorzystuje sieć Lightning, aby Twoje płatności były tańsze, szybsze i bardziej prywatne.\n\nDziała to podobnie jak dostęp do Internetu, ale płacą Państwo za płynność i routing zamiast za przepustowość.\n\nTa konfiguracja obejmuje pewne jednorazowe koszty."; -"wallet__receive_liquidity__text_additional" = "Saldo Wydatków wykorzystuje sieć Lightning, aby Twoje płatności były tańsze, szybsze i bardziej prywatne.\n\nDziała to podobnie jak dostęp do Internetu, ale płacą Państwo za płynność i routing zamiast za przepustowość.\n\nBitkit musi zwiększyć zdolność odbiorczą Państwa salda wydatków, aby przetworzyć tę płatność."; -"wallet__receive_liquidity__label" = "Saldo wydatków Płynność"; -"wallet__receive_liquidity__label_additional" = "Dodatkowa płynność salda wydatków"; "wallet__receive_cjit_error" = "Transakcja nie powiodła się"; "wallet__receive_cjit_error_msg" = "Nie udało się wysłać środków na Twoje saldo wydatków."; -"wallet__receive_cjit_error_max__title" = "Maksymalna zdolność odbioru"; -"wallet__receive_cjit_error_max__description" = "Maksymalna kwota, jaką możesz teraz otrzymać na saldo wydatków, to ₿ {amount}."; -"wallet__receive_cjit_error_node_capacity__title" = "Dodatkowa pojemność niedostępna"; -"wallet__receive_cjit_error_node_capacity__description" = "Bitkit nie może teraz dodać więcej pojemności wydatków do tego portfela."; -"wallet__receive_will" = "Otrzymasz"; "wallet__receive_ldk_init" = "Inicjalizacja salda wydatków..."; "wallet__minimum" = "MINIMUM"; "wallet__activity" = "Aktywność"; @@ -1613,7 +1538,7 @@ "subscriptions__name_placeholder" = "Nazwa subskrypcji"; "subscriptions__ongoing" = "W toku"; "subscriptions__overview" = "Przegląd"; -"subscriptions__payment_due_description" = "Otwórz Bitkit, aby przejrzeć płatność za subskrypcję."; +"subscriptions__payment_due_description" = "Otwórz Bitkit, aby przejrzeć płatność"; "subscriptions__payment_due_title" = "Termin płatności za subskrypcję"; "subscriptions__payments" = "Płatności"; "subscriptions__per_day" = "dziennie"; @@ -1785,9 +1710,6 @@ "wallet__onchain_broadcast_rejected_note" = "Ta płatność mogła mimo to dotrzeć do sieci. Nie wysyłaj jej ponownie. {reason}"; "wallet__onchain_broadcast_unconfirmed" = "Rozgłoszenie niepotwierdzone"; "wallet__onchain_broadcast_unconfirmed_note" = "Płatność mogła zostać wysłana. Nie wysyłaj jej ponownie."; -"wallet__onchain_funding_rejected" = "Transakcja zasilająca została odrzucona. Nie próbuj ponownie zasilać tego zamówienia."; -"wallet__onchain_funding_unknown" = "Transakcja zasilająca mogła zostać wysłana. Nie próbuj ponownie zasilać tego zamówienia."; -"wallet__onchain_funding_followup_failed" = "Zasilenie zostało wysłane, ale nie udało się zapisać lokalnego śledzenia zamówienia. Nie zasilaj ponownie tego zamówienia."; "wallet__onchain_retry_original" = "Ponów oryginalną płatność"; "wallet__onchain_retry_fee" = "Stawka opłaty (sat/vB)"; "wallet__onchain_retry_note" = "Ponów wysłanie {amount} sats na {address} przy użyciu tych samych oryginalnych monet. Kwota dla odbiorcy pozostaje bez zmian. Wprowadź stawkę opłaty sieciowej w sat/vB. Nie może to utworzyć osobnej płatności."; diff --git a/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings b/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings index d3b1e8b10..b57d1273a 100644 --- a/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings @@ -4,8 +4,8 @@ "cards__discount__description" = "Saldo de Gastos"; "cards__quickpay__title" = "QuickPay"; "cards__quickpay__description" = "Escanear e pagar"; -"cards__notifications__description_alt" = "Quando o Bitkit está fechado"; -"cards__notifications__title" = "Receba Pagamentos"; +"cards__notifications__description_alt" = "Lembretes de pagamento"; +"cards__notifications__title" = "Notificações"; "cards__invite__title" = "Convide"; "cards__invite__description" = "Compartilhe a Bitkit"; "cards__lightning__title" = "Pagar"; @@ -151,9 +151,7 @@ "common__learn_more" = "Saiba Mais"; "common__understood" = "Entendido"; "common__connect" = "Conectar"; -"common__min" = "Mín"; "common__max" = "Máx"; -"common__default" = "Padrão"; "common__preview" = "Pré-visualização"; "common__show_all" = "Mostrar tudo"; "common__show_details" = "Mostrar detalhes"; @@ -185,7 +183,6 @@ "fee__slow__range" = "±1-2 horas"; "fee__slow__shortRange" = "1-2h"; "fee__slow__longRange" = "± 1-2 horas"; -"fee__minimum__title" = "Mínimo"; "fee__minimum__longTitle" = "Mínima"; "fee__minimum__description" = "+2 horas"; "fee__minimum__shortDescription" = "+2h"; @@ -198,74 +195,37 @@ "fee__custom__range" = "Depende da taxa"; "fee__custom__shortRange" = "Depende da taxa"; "fee__custom__longRange" = "Depende da taxa"; -"lightning__transfer_intro__title" = "Saldo de\nGastos"; -"lightning__transfer_intro__text" = "Use seu saldo de gastos para desfrutar de transações instantâneas e baratas com amigos, familiares e comerciantes."; -"lightning__transfer_intro__button" = "Começar"; "lightning__funding__nav_title" = "Saldo de Gastos"; "lightning__funding__title" = "Transfira para seu saldo de gastos"; -"lightning__funding__text" = "A forma mais fácil é usar suas economias do Bitkit ou enviar bitcoin de uma carteira diferente."; -"lightning__funding__text_blocked" = "Bitkit não fornece serviços de Lightning no seu paús atualmente, mas você ainda pode se conectar diretamente a outros nodes"; -"lightning__funding__text_blocked_cjit" = "Bitkit não fornece serviços de Lightning no seu paús atualmente, mas você ainda pode se conectar diretamente a outros nodes"; "lightning__funding__button1" = "Transferir do Saldo de Poupança"; "lightning__funding__button2" = "Transferir de Outra Carteira"; "lightning__funding__button3" = "Configuração manual"; -"lightning__funding_advanced__nav_title" = "Saldo de Gastos"; -"lightning__funding_advanced__title" = "Configuração avançada"; -"lightning__funding_advanced__text" = "Escaneie um QR para reivindicar seu canal LNURL de outro LSP ou escolha o setup manual."; -"lightning__funding_advanced__button1" = "QR Canal LNURL"; -"lightning__funding_advanced__button2" = "Setup Manual"; "lightning__funding_advanced__button_short" = "Configuração Avançada"; -"lightning__no_funds__title" = "Não há Fundos Disponíveis"; -"lightning__no_funds__description" = "Antes de transferir fundos de seu saldo de poupança, receba bitcoin na sua carteira Bitkit."; -"lightning__no_funds__fund" = "Adicione fundos"; "lightning__transfer__nav_title" = "Transf. fundos"; "lightning__transfer__confirm" = "Por favor\nconfirme"; "lightning__transfer__custom_fee" = "Taxa personalizada"; "lightning__transfer__swipe" = "Deslizar Para Transferir"; "lightning__transfer_hw__broadcast_uncertain_title" = "A transação pode ter sido enviada"; "lightning__transfer_hw__broadcast_uncertain_description" = "Sua transação de financiamento pode ter sido transmitida. Verifique sua atividade antes de tentar novamente."; -"lightning__transfer_hw__open_connect" = "Abrir Trezor Connect"; "lightning__transfer_hw__reconnect_error_description" = "Reconecte seu dispositivo de hardware, ele parece estar desconectado do seu telefone."; "lightning__transfer_hw__reconnect_error_title" = "Reconectar Dispositivo de Hardware"; -"lightning__transfer_hw__sign_title" = "Assine com\nseu dispositivo"; -"lightning__transfer_hw__signed_title" = "Transação\nassinada"; "lightning__transfer_to_savings_button" = "Transferir para poupança"; "lightning__transfer_to_spending_button" = "Transferir para gastos"; -"lightning__spending_intro__title" = "Transferir\npara gastos"; -"lightning__spending_intro__text" = "Transfira fundos para seu saldo de gastos para desfrutar de transações instantâneas e baratas com amigos, familiares e comerciantes."; -"lightning__spending_intro__button" = "Começar"; "lightning__transfer_failed__nav_title" = "Falha na Transferência"; "lightning__transfer_failed__title" = "Transferência\nfalhou"; "lightning__transfer_failed__text" = "Desculpe, a Bitkit não conseguiu configurar seu saldo de gastos ou concluir sua transferência. Por favor, tente novamente."; "lightning__transfer_failed__cancel" = "Inspecionar Erro"; "lightning__transfer_failed__continue" = "Tente novamente"; -"lightning__spending_amount__title" = "Transferir\npara gastos"; "lightning__spending_amount__quarter" = "25%"; "lightning__spending_amount__error_min__title" = "Saldo mínimo de poupança"; "lightning__spending_amount__error_min__description" = "É necessário um mínimo de ₿ {amount} para estabelecer seu saldo de gastos."; -"lightning__spending_amount__error_balance__description" = "Sua poupança não cobre esta transferência e suas taxas. Tente um valor menor."; -"lightning__spending_amount__error_balance__title" = "Saldo de Poupança Insuficiente"; "lightning__spending_amount__error_max__title" = "Saldo de gastos Máximo"; "lightning__spending_amount__error_max__description" = "O valor que você pode transferir para seu saldo de gastos está atualmente limitado a ₿ {amount}."; "lightning__spending_amount__error_max__description_zero" = "Sua transferência para o saldo de gastos é limitada devido à política de liquidez. Para obter detalhes, visite a Central de Ajuda."; "lightning__spending_confirm__network_fee" = "Taxa da rede"; "lightning__spending_confirm__lsp_fee" = "Taxa de serviço"; "lightning__spending_confirm__amount" = "Para gastos"; -"lightning__spending_confirm__background_setup" = "Configurar em segundo plano"; "lightning__spending_confirm__total" = "Total"; -"lightning__spending_confirm__default" = "Usar padrões"; -"lightning__spending_confirm__fees_changed_title" = "Taxas alteradas"; -"lightning__spending_confirm__fees_changed_service" = "As taxas estão {amount} mais altas agora que seu pedido foi criado. Deslize novamente para confirmar."; -"lightning__spending_confirm__fees_changed_network" = "As taxas estão {amount} mais altas porque as taxas de rede ou seu saldo de poupança mudaram enquanto você estava nesta tela. Deslize novamente para confirmar."; -"lightning__spending_advanced__title" = "Capacidade de recebimento\ncapacidade"; -"lightning__spending_advanced__fee" = "Taxa de liquidez"; -"lightning__spending_advanced__error_balance__description" = "Sua poupança não cobre a taxa de liquidez para esta capacidade de recebimento. Escolha um valor menor."; -"lightning__spending_advanced__error_balance__title" = "Fundos Insuficientes"; -"lightning__spending_advanced__error_max__title" = "Capacidade Máxima de Recebimento"; -"lightning__spending_advanced__error_max__description" = "A capacidade de recebimento está atualmente limitada a ₿ {amount}."; -"lightning__liquidity__title" = "Liquidez\ne routing"; -"lightning__liquidity__text" = "Seu Saldo de Gastos usa a Lightning Network para tornar seus pagamentos mais baratos, mais rápidos e mais privados.\n\nIsso funciona como o acesso à Internet, mas você paga por liquidez e roteamento em vez de largura de banda.\n\nEssa configuração inclui alguns custos únicos."; -"lightning__liquidity__label" = "Liquidez do Saldo de Gastos"; "lightning__savings_intro__title" = "Transferir\npara poupança"; "lightning__savings_intro__text" = "Transfira seu saldo de gastos para sua poupança para armazenar seu Bitcoin a longo prazo e garantir seu futuro."; "lightning__savings_intro__button" = "Começar"; @@ -289,9 +249,6 @@ "lightning__savings_interrupted__nav_title" = "Transferência\ninterrompida"; "lightning__savings_interrupted__title" = "Mantenha a Bitkit\naberta"; "lightning__savings_interrupted__text" = "Os fundos ainda não foram transferidos. A Bitkit tentará iniciar a transferência durante os próximos 30 minutos. Por favor, mantenha o aplicativo aberto."; -"lightning__transfer_success__nav_title" = "Transferência bem-sucedida"; -"lightning__transfer_success__title_spending" = "Fundos transferidos\npara gastos"; -"lightning__transfer_success__text_spending" = "Seus fundos foram transferidos e seu saldo de gastos está pronto para ser usado."; "lightning__transfer_success__title_savings" = "Fundos transferidos\npara poupança"; "lightning__transfer_success__text_savings" = "Seus fundos foram transferidos. Você já pode usar esses fundos imediatamente."; "lightning__external__nav_title" = "Conexões Lightning"; @@ -314,12 +271,6 @@ "lightning__spending_label" = "Saldo de gastos"; "lightning__receiving_label" = "Capacidade de recebimento"; "lightning__setting_up_header" = "Em transferência"; -"lightning__setting_up_text" = "Aguarde, sua transferência de fundos está em andamento. Isso deve levar ±10 minutos."; -"lightning__setting_up_step1" = "Processando Pagamento"; -"lightning__setting_up_step2" = "Pagamento Realizado"; -"lightning__setting_up_step3" = "Na Fila Para Abertura"; -"lightning__setting_up_step4" = "Abrindo Conexão"; -"lightning__setting_up_button" = "Continuar Usando o Bitkit"; "lightning__node_info" = "Nó Lightning"; "lightning__node_id" = "ID do LDK Node "; "lightning__node_disconnected" = "desconectado"; @@ -391,9 +342,9 @@ "lightning__force_init_msg" = "Seus fundos estarão disponíveis em ±14 dias."; "lightning__force_failed_title" = "Falha na transferência forçada"; "lightning__force_failed_msg" = "Não foi possível transferir seus fundos de volta para sua poupança. Por favor, tente novamente."; -"lightning__force_channels_skipped" = "Algumas conexões não puderam ser fechadas."; "lightning__channel_opened_title" = "Saldo de Gastos Pronto"; "lightning__channel_opened_msg" = "Agora é possível pagar qualquer pessoa, em qualquer lugar, instantaneamente."; +"lightning__funding__button_hw" = "Transferir da carteira HW"; "lightning__order_given_up_title" = "Falha no setup dos Pagamentos Instantâneos"; "lightning__order_given_up_msg" = "O processo de configuração expirou. Por favor, tente novamente."; "lightning__order_expired_title" = "Falha no setup dos Pagamentos Instantâneos"; @@ -412,6 +363,9 @@ "lightning__order_state__open" = "Conexão aberta"; "lightning__order_state__inactive" = "Conexão inativa"; "lightning__peer_disconnected" = "Par desconectado."; +"lightning__transfer_unavailable__nav_title" = "Transferência indisponível"; +"lightning__transfer_unavailable__text" = "As transferências automáticas para seu saldo de gastos não estão mais disponíveis. Use seu próprio nó ou configure uma conexão com outro provedor."; +"lightning__transfer_unavailable__title" = "Configuração manual\nnecessária"; "lightning__wait_text_top" = "Por favor, espere que a Bitkit se conecte à rede de pagamento (±10 segundos)."; "lightning__wait_text_bottom" = "Conectando e Sincronizando..."; "onboarding__tos_header" = "Bitkit\ntermos de uso"; @@ -433,7 +387,6 @@ "onboarding__slide1_header" = "Pagamentos\ninstantâneos"; "onboarding__slide1_text" = "Envie bitcoin mais rápido do que nunca. Aproveite os pagamentos instantâneos e baratos com amigos, familiares e comerciantes."; "onboarding__slide1_text_alt" = "Aproveite pagamentos instantâneos e baratos com amigos, família e comerciantes na rede Lightning."; -"onboarding__slide1_note" = "*No momento, a Bitkit não fornece serviços Lightning em seu país, mas você ainda pode se conectar a outros nós."; "onboarding__slide2_header" = "Bitcoiners,\nsem fronteiras"; "onboarding__slide2_text" = "Assuma o controle de sua vida digital com perfis e contatos pagáveis."; "onboarding__slide3_header" = "Privacidade\nnão é crime"; @@ -556,8 +509,6 @@ "other__lnurl_withdr_error_minmax" = "Parâmetros de saque LNURL incorretos, mínimo/máximo não configurados corretamente."; "other__lnurl_withdr_success_title" = "Saque solicitado"; "other__lnurl_withdr_success_msg" = "Seu saque foi solicitado com sucesso. Aguardando pagamento."; -"other__notification_registration_failed_description" = "O Bitkit não conseguiu se registrar para notificações push."; -"other__notification_registration_failed_title" = "Falha no registro de notificações"; "other__phone_settings" = "Abrir as configurações do telefone"; "other__bt_error_retrieve" = "A Transferência Falhou"; "other__bt_error_connect" = "Não é possível adicionar o nó LSP no momento."; @@ -573,14 +524,7 @@ "other__transfer_notification__body" = "Abra a Bitkit para concluir sua transferência"; "other__scan__error__generic" = "Bitkit não consegue ler os dados fornecidos."; "other__scan__error__expired" = "Este invoice lightning expirou."; -"other__gift__claimed__text" = "Desculpe, você chegou tarde! Todos os presentes para este código já foram resgatados."; -"other__gift__claiming__text" = "Reivindicando seu gitf code Bitkit..."; -"other__gift__claiming__title" = "Reivindicando presente"; -"other__gift__error__title" = "Erro no código de presente"; -"other__gift__error__text" = "A Bitkit não pôde reivindicar os fundos. Tente novamente mais tarde ou entre em contato conosco."; "other__gift__error__button" = "OK"; -"other__gift__used__title" = "Código usado"; -"other__gift__used__text" = "Este gift code da Bitkit já foi usado e os fundos já foram pagos."; "other__gift__used__button" = "OK"; "other__shop__intro__title" = "Comprar"; "other__shop__intro__description" = "Coloque sua vida no padrão Bitcoin. Gaste seu Bitcoin em cartões-presente digitais, eSIMs, recargas de telefone e muito mais."; @@ -763,6 +707,7 @@ "pubky_auth__homeserver" = "Homeserver"; "pubky_auth__signup_description" = "Crie uma nova identidade Pubky neste homeserver. Só continue se você confiar nele."; "pubky_auth__approval_failed" = "Falha na Autorização"; +"wallet__onchain_abandon_transfer_note" = "As transferências para o saldo de gastos não estão mais disponíveis. O Bitkit primeiro verifica a rede e depois devolve as moedas desta transferência para sua carteira, para que a transferência original nunca seja confirmada. Isso tem uma taxa de rede."; "watch_only_accounts__active_section" = "Contas ativas"; "watch_only_accounts__copy_xpub" = "Copiar xpub"; "watch_only_accounts__description" = "Cada servidor Paykit aprovado recebe uma conta Bitcoin separada. Desative o acompanhamento para descarregar uma conta sem excluir o histórico da carteira dela."; @@ -839,21 +784,17 @@ "settings__widgets__reset_widgets_dialog_title" = "Redefinir widgets?"; "settings__widgets__reset_widgets_dialog_description" = "Tem certeza de que deseja redefinir os widgets? O conjunto padrão de widgets com configurações padrão será exibido."; "settings__widgets__reset_suggestions" = "Redefinir cartões de sugestões"; -"settings__notifications__nav_title" = "Pagar em segundo plano"; -"settings__notifications__intro__title" = "Receba\nPassivamente"; -"settings__notifications__intro__text" = "Ative as notificações para receber pagamentos, mesmo quando o Bitkit estiver fechado."; +"settings__notifications__nav_title" = "Notificações"; +"settings__notifications__intro__title" = "Fique\npor dentro"; +"settings__notifications__intro__text" = "Ative as notificações para receber lembretes das suas assinaturas e solicitações de pagamento."; "settings__notifications__intro__button" = "Ativar"; -"settings__notifications__settings__toggle" = "Receber pagamentos quando o Bitkit está fechado"; -"settings__notifications__settings__enabled" = "Pagamentos em segundo plano estão ativados. Você pode receber fundos mesmo com o app fechado (se seu dispositivo estiver conectado à internet)."; -"settings__notifications__settings__disabled" = "Ative pagamentos em segundo plano para receber fundos mesmo com o app fechado (se seu dispositivo estiver conectado à internet)."; -"settings__notifications__settings__denied" = "Pagamentos em segundo plano estão desativados porque você negou as notificações."; -"settings__notifications__settings__preview__title" = "Pagamento Recebido"; -"settings__notifications__settings__preview__text" = "Abra o Bitkit para ver detalhes"; +"settings__notifications__settings__toggle" = "Permitir notificações"; +"settings__notifications__settings__enabled" = "As notificações estão ativadas. O Bitkit pode lembrar você de assinaturas e solicitações de pagamento."; +"settings__notifications__settings__disabled" = "Ative as notificações para receber lembretes das suas assinaturas e solicitações de pagamento."; +"settings__notifications__settings__denied" = "As notificações estão desativadas porque você as recusou."; "settings__notifications__settings__preview__time" = "3m atrás"; -"settings__notifications__settings__privacy__label" = "Privacidade"; -"settings__notifications__settings__privacy__text" = "Incluir valor nas notificações"; "settings__notifications__settings__notifications__label" = "Notificações"; -"settings__notifications__settings__notifications__text" = "Para ativar pagamentos em segundo plano, ative as notificações nas Configurações do Bitkit no iOS."; +"settings__notifications__settings__notifications__text" = "Para receber lembretes, ative as notificações nas Configurações do Bitkit no iOS."; "settings__notifications__settings__button__enabled" = "Personalizar nas Configurações do Bitkit em {platform}"; "settings__notifications__settings__button__disabled" = "Ativar nas Configurações do Bitkit em {platform}"; "settings__quickpay__nav_title" = "QuickPay"; @@ -1405,27 +1346,11 @@ "wallet__receive_note_placeholder" = "Comentário opcional para o pagador"; "wallet__receive_show_qr" = "Mostrar código QR"; "wallet__receive_text_lnfunds" = "Deseja receber via Lightning?"; -"wallet__receive_background_setup_text" = "Ative a configuração em segundo plano para sair do Bitkit com segurança enquanto seu saldo está sendo configurado."; -"wallet__receive_background_setup_toggle" = "Ativar configuração em segundo plano"; "wallet__receive_spending" = "Receber no Saldo de Gastos"; "wallet__receive_spending_text" = "Aproveite pagamentos em bitcoin instantâneos e baratos na Lightning Network."; "wallet__receive_spending_title" = "Receber no saldo de gastos"; -"wallet__receive_connect_initial" = "Para configurar seu saldo de gastos, uma taxa de rede de {networkFee} e {serviceFee} de taxa do provedor de serviços serão deduzidas."; -"wallet__receive_connect_additional" = "Para receber mais Bitcoin instantâneo, a Bitkit precisa aumentar sua liquidez. A taxa de rede de {networkFee} e {serviceFee} de taxa do provedor de serviços serão deduzidas do valor que você especificou."; -"wallet__receive_liquidity__nav_title" = "Setup do saldo de gastos"; -"wallet__receive_liquidity__nav_title_additional" = "Aumentar a capacidade de recebimento"; -"wallet__receive_liquidity__invalid_quote" = "A Bitkit recebeu uma cotação de liquidez inválida. Tente novamente."; -"wallet__receive_liquidity__text" = "Seu Saldo de Gastos usa a Lightning Network para tornar seus pagamentos mais baratos, mais rápidos e mais privados.\n\nIsso funciona como o acesso à Internet, mas você paga por liquidez e roteamento em vez de largura de banda.\n\nEssa configuração inclui alguns custos únicos."; -"wallet__receive_liquidity__text_additional" = "Seu Saldo de Gastos usa a Lightning Network para tornar seus pagamentos mais baratos, mais rápidos e mais privados.\n\nIsso funciona como o acesso à Internet, mas você paga por liquidez e roteamento em vez de largura de banda.\n\nA Bitkit precisa aumentar a capacidade de recebimento de seu saldo de gastos para processar esse pagamento."; -"wallet__receive_liquidity__label" = "Liquidez do Saldo de Gastos"; -"wallet__receive_liquidity__label_additional" = "Liquidez adicional do saldo de gastos"; "wallet__receive_cjit_error" = "Falha na Transação"; "wallet__receive_cjit_error_msg" = "Falha ao enviar fundos para sua conta de gastos."; -"wallet__receive_cjit_error_max__title" = "Capacidade Máxima de Recebimento"; -"wallet__receive_cjit_error_max__description" = "O máximo que você pode receber no seu saldo de gastos agora é ₿ {amount}."; -"wallet__receive_cjit_error_node_capacity__title" = "Capacidade adicional indisponível"; -"wallet__receive_cjit_error_node_capacity__description" = "O Bitkit não pode adicionar mais capacidade de gastos a esta carteira no momento."; -"wallet__receive_will" = "Você receberá"; "wallet__receive_ldk_init" = "Carregando saldo de gastos..."; "wallet__minimum" = "MÍNIMO"; "wallet__activity" = "Atividade"; @@ -1614,7 +1539,7 @@ "subscriptions__name_placeholder" = "Nome da assinatura"; "subscriptions__ongoing" = "Em andamento"; "subscriptions__overview" = "Visão Geral"; -"subscriptions__payment_due_description" = "Abra a Bitkit para revisar um pagamento de assinatura."; +"subscriptions__payment_due_description" = "Abra a Bitkit para revisar o pagamento"; "subscriptions__payment_due_title" = "Pagamento de Assinatura Pendente"; "subscriptions__payments" = "Pagamentos"; "subscriptions__per_day" = "por dia"; @@ -1786,9 +1711,6 @@ "wallet__onchain_broadcast_rejected_note" = "Este pagamento ainda pode ter chegado à rede. Não o envie novamente. {reason}"; "wallet__onchain_broadcast_unconfirmed" = "Transmissão não confirmada"; "wallet__onchain_broadcast_unconfirmed_note" = "O pagamento pode ter sido enviado. Não o envie novamente."; -"wallet__onchain_funding_rejected" = "A transação de financiamento foi rejeitada. Não tente financiar este pedido novamente."; -"wallet__onchain_funding_unknown" = "A transação de financiamento pode ter sido enviada. Não tente financiar este pedido novamente."; -"wallet__onchain_funding_followup_failed" = "O financiamento foi enviado, mas o acompanhamento local do pedido não pôde ser salvo. Não financie este pedido novamente."; "wallet__onchain_retry_original" = "Tentar novamente o pagamento original"; "wallet__onchain_retry_fee" = "Taxa (sat/vB)"; "wallet__onchain_retry_note" = "Tentar novamente {amount} sats para {address} usando as mesmas moedas originais. O valor do destinatário permanece inalterado. Digite a taxa de rede em sat/vB. Isso não pode criar um pagamento separado."; diff --git a/Bitkit/Resources/Localization/pt.lproj/Localizable.strings b/Bitkit/Resources/Localization/pt.lproj/Localizable.strings index 3d0260b00..c58d78499 100644 --- a/Bitkit/Resources/Localization/pt.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pt.lproj/Localizable.strings @@ -1,4 +1,5 @@ "security__cp_text" = "Você pode alterar seu código PIN para uma nova\ncombinação de 4 dígitos. Primeiro, digite seu código PIN atual."; +"wallet__onchain_abandon_transfer_note" = "As transferências para o saldo de gastos já não estão disponíveis. A Bitkit verifica primeiro a rede e depois devolve as moedas desta transferência à sua carteira, para que a transferência original nunca seja confirmada. Isto tem uma taxa de rede."; "wallet__payment_recipient_rejected" = "O destinatário rejeitou este pagamento Lightning. Verifique a fatura e tente novamente."; "wallet__payment_abandoned" = "O pagamento Lightning foi interrompido antes de ser concluído."; "wallet__payment_expired" = "Este pagamento Lightning expirou. Peça uma nova fatura."; @@ -48,15 +49,12 @@ "widgets__weather__next_block" = "Inclusão no próximo bloco"; "wallet__payment_request" = "Pedido de pagamento"; -"wallet__receive_cjit_error_node_capacity__title" = "Capacidade adicional indisponível"; -"wallet__receive_cjit_error_node_capacity__description" = "O Bitkit não pode adicionar mais capacidade de gastos a esta carteira neste momento."; -"wallet__receive_liquidity__invalid_quote" = "A Bitkit recebeu uma cotação de liquidez inválida. Tente novamente."; "cards__backupSeedPhrase__title" = "Back up"; "cards__backupSeedPhrase__description" = "Guarde seu bitcoin"; "cards__quickpay__title" = "QuickPay"; "cards__quickpay__description" = "Escanear e pagar"; -"cards__notifications__description_alt" = "Quando a Bitkit estiver fechada"; -"cards__notifications__title" = "Receba pagamentos"; +"cards__notifications__description_alt" = "Lembretes de pagamento"; +"cards__notifications__title" = "Notificações"; "cards__invite__title" = "Convide"; "cards__invite__description" = "Compartilhe a Bitkit"; "cards__lightning__title" = "Pagar"; @@ -189,9 +187,7 @@ "common__learn_more" = "Saiba Mais"; "common__understood" = "Entendido"; "common__connect" = "Conectar"; -"common__min" = "Mín"; "common__max" = "Máx"; -"common__default" = "Padrão"; "common__preview" = "Pré-visualização"; "common__show_all" = "Mostrar Tudo"; "common__show_details" = "Mostrar Detalhes"; @@ -216,7 +212,6 @@ "fee__slow__description" = "±1 hora"; "fee__slow__shortDescription" = "±1h"; "fee__slow__range" = "±1-2 horas"; -"fee__minimum__title" = "Mínimo"; "fee__minimum__longTitle" = "Mínimo"; "fee__minimum__description" = "+2 horas"; "fee__minimum__shortDescription" = "+2h"; @@ -226,64 +221,27 @@ "fee__custom__description" = "Depende da taxa"; "fee__custom__shortDescription" = "Depende da taxa"; "fee__custom__range" = "Depende da taxa"; -"lightning__transfer_intro__title" = "Saldo de\nGastos"; -"lightning__transfer_intro__text" = "Use seu saldo de gastos para desfrutar de transações instantâneas e baratas com amigos, familiares e comerciantes."; -"lightning__transfer_intro__button" = "Começar"; "lightning__funding__title" = "Transfira para seu saldo de gastos"; -"lightning__funding__text" = "A forma mais fácil é usar as suas poupanças Bitkit ou enviar bitcoin de uma carteira diferente."; -"lightning__funding__text_blocked" = "Bitkit não fornece serviços de Lightning no seu paús atualmente, mas você ainda pode se conectar diretamente a outros nodes"; -"lightning__funding__text_blocked_cjit" = "Bitkit não fornece serviços de Lightning no seu paús atualmente, mas você ainda pode se conectar diretamente a outros nodes"; "lightning__funding__button1" = "Transferir do Saldo de Poupança"; "lightning__funding__button2" = "Transferir de Outra Carteira"; "lightning__funding__button3" = "Setup Manual"; -"lightning__funding_advanced__nav_title" = "Saldo de Gastos"; -"lightning__funding_advanced__title" = "Configuração avançada"; -"lightning__funding_advanced__text" = "Escaneie um QR para reivindicar seu canal LNURL de outro LSP ou escolha o setup manual."; -"lightning__funding_advanced__button1" = "QR Canal LNURL"; -"lightning__funding_advanced__button2" = "Setup Manual"; "lightning__funding_advanced__button_short" = "Configuração Avançada"; -"lightning__no_funds__title" = "Não há Fundos Disponíveis"; -"lightning__no_funds__description" = "Antes de transferir fundos de seu saldo de poupança, receba bitcoin na sua carteira Bitkit."; -"lightning__no_funds__fund" = "Financiar carteira"; "lightning__transfer__nav_title" = "Transf. fundos"; "lightning__transfer__confirm" = "Por favor\nconfirme"; "lightning__transfer__swipe" = "Deslizar Para Transferir"; "lightning__transfer_hw__broadcast_uncertain_title" = "A transação pode ter sido enviada"; "lightning__transfer_hw__broadcast_uncertain_description" = "A sua transação de financiamento pode ter sido difundida. Verifique a sua atividade antes de tentar novamente."; -"lightning__transfer_hw__open_connect" = "Abrir Trezor Connect"; "lightning__transfer_hw__reconnect_error_description" = "Volte a ligar o seu dispositivo de hardware; parece estar desligado do telemóvel."; "lightning__transfer_hw__reconnect_error_title" = "Voltar a ligar o dispositivo"; -"lightning__transfer_hw__sign_title" = "Assine com\no seu dispositivo"; -"lightning__transfer_hw__signed_title" = "Transação\nassinada"; "lightning__transfer_to_savings_button" = "Transferir Para Poupança"; "lightning__transfer_to_spending_button" = "Transferir Para Gastos"; -"lightning__spending_intro__title" = "Transferir\npara gastos"; -"lightning__spending_intro__text" = "Transfira fundos para seu saldo de gastos para desfrutar de transações instantâneas e baratas com amigos, familiares e comerciantes."; -"lightning__spending_intro__button" = "Começar"; -"lightning__spending_amount__title" = "Transferir\npara gastos"; "lightning__spending_amount__quarter" = "25%"; -"lightning__spending_amount__error_balance__description" = "A sua poupança não cobre esta transferência e as respetivas taxas. Tente um montante menor."; -"lightning__spending_amount__error_balance__title" = "Saldo de Poupança Insuficiente"; "lightning__spending_amount__error_max__title" = "Saldo de gastos Máximo"; "lightning__spending_amount__error_max__description" = "O valor que você pode transferir para seu saldo de gastos está atualmente limitado a ₿ {amount}."; "lightning__spending_confirm__network_fee" = "Taxa da rede"; "lightning__spending_confirm__lsp_fee" = "Taxa de serviço"; "lightning__spending_confirm__amount" = "Para gastos"; -"lightning__spending_confirm__background_setup" = "Configurar em segundo plano"; "lightning__spending_confirm__total" = "Total"; -"lightning__spending_confirm__default" = "Usar padrões"; -"lightning__spending_confirm__fees_changed_title" = "As taxas mudaram"; -"lightning__spending_confirm__fees_changed_service" = "As taxas estão {amount} mais altas agora que o pedido foi criado. Deslize novamente para confirmar."; -"lightning__spending_confirm__fees_changed_network" = "As taxas estão {amount} mais altas porque as taxas de rede ou o seu saldo de poupança mudaram enquanto estava neste ecrã. Deslize novamente para confirmar."; -"lightning__spending_advanced__title" = "Capacidade de recebimento\ncapacidade"; -"lightning__spending_advanced__fee" = "Taxa de liquidez"; -"lightning__spending_advanced__error_balance__description" = "A sua poupança não cobre a taxa de liquidez para esta capacidade de receção. Escolha um montante menor."; -"lightning__spending_advanced__error_balance__title" = "Fundos insuficientes"; -"lightning__spending_advanced__error_max__title" = "Capacidade de receção máxima"; -"lightning__spending_advanced__error_max__description" = "A capacidade de receção está atualmente limitada a ₿ {amount}."; -"lightning__liquidity__title" = "Liquidez\ne routing"; -"lightning__liquidity__text" = "Seu Saldo de Gastos usa a Lightning Network para tornar seus pagamentos mais baratos, mais rápidos e mais privados.\n\nIsso funciona como o acesso à Internet, mas você paga por liquidez e roteamento em vez de largura de banda.\n\nEssa configuração inclui alguns custos únicos."; -"lightning__liquidity__label" = "Liquidez do Saldo de Gastos"; "lightning__savings_intro__title" = "Transferir\npara poupança"; "lightning__savings_intro__text" = "Transfira seu saldo de gastos para sua poupança para armazenar seu Bitcoin a longo prazo e garantir seu futuro."; "lightning__savings_intro__button" = "Começar"; @@ -307,9 +265,6 @@ "lightning__savings_interrupted__nav_title" = "Transferência\ninterrompida"; "lightning__savings_interrupted__title" = "Mantenha a Bitkit\naberta"; "lightning__savings_interrupted__text" = "Os fundos ainda não foram transferidos. A Bitkit tentará iniciar a transferência durante os próximos 30 minutos. Por favor, mantenha o aplicativo aberto."; -"lightning__transfer_success__nav_title" = "Transferência bem-sucedida"; -"lightning__transfer_success__title_spending" = "Fundos transferidos\npara gastos"; -"lightning__transfer_success__text_spending" = "Seus fundos foram transferidos e seu saldo de gastos está pronto para ser usado."; "lightning__transfer_success__title_savings" = "Fundos transferidos\npara poupança"; "lightning__transfer_success__text_savings" = "Seus fundos foram transferidos. Você já pode usar esses fundos imediatamente."; "lightning__external__nav_title" = "Conexões Lightning"; @@ -328,12 +283,6 @@ "lightning__savings" = "Poupança"; "lightning__spending_label" = "Saldo de gastos"; "lightning__receiving_label" = "Capacidade de recebimento"; -"lightning__setting_up_text" = "Aguarde, sua transferência de fundos está em andamento. Isso deve levar ±10 minutos."; -"lightning__setting_up_step1" = "Processando Pagamento"; -"lightning__setting_up_step2" = "Pagamento Realizado"; -"lightning__setting_up_step3" = "Na Fila Para Abertura"; -"lightning__setting_up_step4" = "Abrindo Conexão"; -"lightning__setting_up_button" = "Continuar Usando o Bitkit"; "lightning__node_info" = "Nó Lightning"; "lightning__connections" = "Conexões Lightning"; "lightning__conn_pending" = "Conexões pendentes"; @@ -391,9 +340,9 @@ "lightning__force_init_msg" = "Seus fundos estarão disponíveis em ±14 dias."; "lightning__force_failed_title" = "Falha na transferência forçada"; "lightning__force_failed_msg" = "Não foi possível transferir seus fundos de volta para sua poupança. Por favor, tente novamente."; -"lightning__force_channels_skipped" = "Algumas conexões não puderam ser fechadas."; "lightning__channel_opened_title" = "Saldo de Gastos Pronto"; "lightning__channel_opened_msg" = "Agora é possível pagar qualquer pessoa, em qualquer lugar, instantaneamente."; +"lightning__funding__button_hw" = "Transferir da carteira HW"; "lightning__order_state__awaiting_payment" = "Processando pagamentos"; "lightning__order_state__payment_canceled" = "Pagamento cancelado"; "lightning__order_state__paid" = "Pagamento bem-sucedido"; @@ -404,6 +353,9 @@ "lightning__order_state__open" = "Conexão aberta"; "lightning__order_state__inactive" = "Conexão inativa"; "lightning__peer_disconnected" = "Par desconectado."; +"lightning__transfer_unavailable__nav_title" = "Transferência indisponível"; +"lightning__transfer_unavailable__text" = "As transferências automáticas para o seu saldo de gastos já não estão disponíveis. Use o seu próprio nó ou configure uma ligação com outro fornecedor."; +"lightning__transfer_unavailable__title" = "Configuração manual\nnecessária"; "onboarding__tos_header" = "Bitkit\ntermos de uso"; "onboarding__tos_subtitle" = "Ao continuar, você declara que leu e aceita os termos de uso."; "onboarding__tos_title" = "Termos de uso"; @@ -418,7 +370,6 @@ "onboarding__slide0_text_alt" = "A Bitkit dá-lhe as chaves para controlar o seu dinheiro, o seu perfil e os seus contactos. Assuma o controlo e torne-se sem fronteiras."; "onboarding__slide1_header" = "Pagamentos\ninstantâneos"; "onboarding__slide1_text_alt" = "Desfrute de pagamentos instantâneos e baratos com amigos, familiares e comerciantes na rede Lightning."; -"onboarding__slide1_note" = "*No momento, a Bitkit não fornece serviços Lightning em seu país, mas você ainda pode se conectar a outros nós."; "onboarding__slide3_header" = "Privacidade\nnão é crime"; "onboarding__slide3_text_alt" = "O seu dinheiro, a sua privacidade. Deslize para ocultar o seu saldo e gaste de forma mais privada, sem rastreamento de dados e com zero KYC."; "onboarding__slide4_header" = "Suas chaves,\nsuas moedas"; @@ -505,8 +456,6 @@ "other__lnurl_withdr_error_minmax" = "Parâmetros de saque LNURL incorretos, mínimo/máximo não configurados corretamente."; "other__lnurl_withdr_success_title" = "Saque solicitado"; "other__lnurl_withdr_success_msg" = "Seu saque foi solicitado com sucesso. Aguardando pagamento."; -"other__notification_registration_failed_description" = "A Bitkit não conseguiu registar-se para receber notificações push."; -"other__notification_registration_failed_title" = "Falha no registo de notificações"; "other__earlier" = "ANTERIORES"; "other__update_nav_title" = "Atualização disponível"; "other__update_title" = "Atualizar\nBitkit"; @@ -515,13 +464,6 @@ "other__try_again" = "Tente novamente."; "other__scan__error__generic" = "Bitkit não consegue ler os dados fornecidos."; "other__scan__error__expired" = "Este invoice lightning expirou."; -"other__gift__claimed__text" = "Desculpe, você chegou tarde demais! Todos os presentes para este código já foram resgatados."; -"other__gift__claiming__text" = "Reivindicando seu gitf code Bitkit..."; -"other__gift__claiming__title" = "Reivindicando presente"; -"other__gift__error__title" = "Erro no código de presente"; -"other__gift__error__text" = "A Bitkit não pôde reivindicar os fundos. Tente novamente mais tarde ou entre em contato conosco."; -"other__gift__used__title" = "Código usado"; -"other__gift__used__text" = "Este gift code da Bitkit já foi usado e os fundos já foram pagos."; "other__shop__intro__title" = "Comprar"; "other__shop__intro__description" = "Coloque sua vida no padrão Bitcoin. Gaste seu Bitcoin em cartões-presente digitais, eSIMs, recargas de telefone e muito mais."; "other__shop__intro__button" = "Começar"; @@ -740,21 +682,17 @@ "settings__widgets__reset_widgets_dialog_title" = "Repor widgets?"; "settings__widgets__reset_widgets_dialog_description" = "Tem certeza de que deseja repor os widgets? O conjunto padrão de widgets com configurações padrão será exibido."; "settings__widgets__reset_suggestions" = "Repor cartões de sugestões"; -"settings__notifications__nav_title" = "Pagar em segundo plano"; -"settings__notifications__intro__title" = "Receba pagamentos\npassivamente"; -"settings__notifications__intro__text" = "Ative as notificações para receber pagamentos, mesmo quando a Bitkit está fechada."; +"settings__notifications__nav_title" = "Notificações"; +"settings__notifications__intro__title" = "Mantenha-se\ninformado"; +"settings__notifications__intro__text" = "Ative as notificações para receber lembretes das suas subscrições e pedidos de pagamento."; "settings__notifications__intro__button" = "Ativar"; -"settings__notifications__settings__toggle" = "Receba pagamentos com a Bitkit fechada"; -"settings__notifications__settings__enabled" = "Pagamentos em segundo plano estão ativados. Você pode receber fundos mesmo quando o app está fechado (se seu dispositivo estiver conectado à internet)."; -"settings__notifications__settings__disabled" = "Ative os pagamentos em segundo plano para receber fundos mesmo com a app fechada (se o dispositivo estiver ligado à internet)."; -"settings__notifications__settings__denied" = "Os pagamentos em segundo plano estão desativados porque você negou as notificações."; -"settings__notifications__settings__preview__title" = "Pagamento Recebido"; -"settings__notifications__settings__preview__text" = "Abra a Bitkit para ver os detalhes"; +"settings__notifications__settings__toggle" = "Permitir notificações"; +"settings__notifications__settings__enabled" = "As notificações estão ativadas. A Bitkit pode lembrá-lo de subscrições e pedidos de pagamento."; +"settings__notifications__settings__disabled" = "Ative as notificações para receber lembretes das suas subscrições e pedidos de pagamento."; +"settings__notifications__settings__denied" = "As notificações estão desativadas porque as recusou."; "settings__notifications__settings__preview__time" = "há 3 min"; -"settings__notifications__settings__privacy__label" = "Privacidade"; -"settings__notifications__settings__privacy__text" = "Incluir valor nas notificações"; "settings__notifications__settings__notifications__label" = "Notificações"; -"settings__notifications__settings__notifications__text" = "Para ativar os pagamentos em segundo plano, ative as notificações nas Definições da Bitkit no iOS."; +"settings__notifications__settings__notifications__text" = "Para receber lembretes, ative as notificações nas Definições da Bitkit no iOS."; "settings__notifications__settings__button__enabled" = "Personalizar nas Definições da Bitkit no {platform}"; "settings__notifications__settings__button__disabled" = "Ativar nas Definições da Bitkit no {platform}"; "settings__quickpay__nav_title" = "QuickPay"; @@ -1125,22 +1063,9 @@ "wallet__receive_lightning_invoice" = "Invoice Lightning"; "wallet__receive_note_placeholder" = "Comentário opcional para o pagador"; "wallet__receive_show_qr" = "Mostrar código QR"; -"wallet__receive_background_setup_text" = "Ative a configuração em segundo plano para sair da Bitkit com segurança enquanto seu saldo está sendo configurado."; -"wallet__receive_background_setup_toggle" = "Ativar configuração em segundo plano"; "wallet__receive_spending" = "Receber fundos Lightning"; "wallet__receive_spending_text" = "Desfrute de pagamentos em bitcoin instantâneos e baratos na rede Lightning."; "wallet__receive_spending_title" = "Receba no saldo de gastos"; -"wallet__receive_connect_initial" = "Para configurar seu saldo de gastos, uma taxa de rede de {networkFee} e {serviceFee} de taxa do provedor de serviços serão deduzidas."; -"wallet__receive_connect_additional" = "Para receber mais Bitcoin instantâneo, a Bitkit precisa aumentar sua liquidez. A taxa de rede de {networkFee} e {serviceFee} de taxa do provedor de serviços serão deduzidas do valor que você especificou."; -"wallet__receive_liquidity__nav_title" = "Setup do saldo de gastos"; -"wallet__receive_liquidity__nav_title_additional" = "Aumentar a capacidade de recebimento"; -"wallet__receive_liquidity__text" = "Seu Saldo de Gastos usa a Lightning Network para tornar seus pagamentos mais baratos, mais rápidos e mais privados.\n\nIsso funciona como o acesso à Internet, mas você paga por liquidez e roteamento em vez de largura de banda.\n\nEssa configuração inclui alguns custos únicos."; -"wallet__receive_liquidity__text_additional" = "Seu Saldo de Gastos usa a Lightning Network para tornar seus pagamentos mais baratos, mais rápidos e mais privados.\n\nIsso funciona como o acesso à Internet, mas você paga por liquidez e roteamento em vez de largura de banda.\n\nA Bitkit precisa aumentar a capacidade de recebimento de seu saldo de gastos para processar esse pagamento."; -"wallet__receive_liquidity__label" = "Liquidez do Saldo de Gastos"; -"wallet__receive_liquidity__label_additional" = "Liquidez adicional do saldo de gastos"; -"wallet__receive_cjit_error_max__title" = "Capacidade de receção máxima"; -"wallet__receive_cjit_error_max__description" = "O máximo que pode receber no seu saldo de gastos neste momento é ₿ {amount}."; -"wallet__receive_will" = "Você receberá"; "wallet__activity" = "Atividade"; "wallet__activity_no" = "Nenhuma atividade ainda"; "wallet__activity_sent" = "Enviado"; @@ -1308,7 +1233,7 @@ "subscriptions__name_placeholder" = "Nome da subscrição"; "subscriptions__ongoing" = "Em curso"; "subscriptions__overview" = "Vista geral"; -"subscriptions__payment_due_description" = "Abra a Bitkit para rever um pagamento de subscrição."; +"subscriptions__payment_due_description" = "Abra a Bitkit para rever o pagamento"; "subscriptions__payment_due_title" = "Pagamento de subscrição pendente"; "subscriptions__payments" = "Pagamentos"; "subscriptions__per_day" = "por dia"; @@ -1432,9 +1357,6 @@ "wallet__onchain_broadcast_rejected_note" = "Este pagamento pode ter chegado à rede. Não o envie novamente. {reason}"; "wallet__onchain_broadcast_unconfirmed" = "Difusão não confirmada"; "wallet__onchain_broadcast_unconfirmed_note" = "O pagamento pode ter sido enviado. Não o envie novamente."; -"wallet__onchain_funding_rejected" = "A transação de financiamento foi rejeitada. Não tente financiar este pedido novamente."; -"wallet__onchain_funding_unknown" = "A transação de financiamento pode ter sido enviada. Não tente financiar este pedido novamente."; -"wallet__onchain_funding_followup_failed" = "O financiamento foi enviado, mas não foi possível guardar o acompanhamento local do pedido. Não volte a financiar este pedido."; "wallet__onchain_retry_original" = "Repetir pagamento original"; "wallet__onchain_retry_fee" = "Taxa (sat/vB)"; "wallet__onchain_retry_note" = "Repetir {amount} sats para {address} com as mesmas moedas originais. O montante do destinatário mantém-se. Introduza a taxa de rede em sat/vB. Isto não pode criar um pagamento separado."; diff --git a/Bitkit/Resources/Localization/ru.lproj/Localizable.strings b/Bitkit/Resources/Localization/ru.lproj/Localizable.strings index c313e0ad0..7a8c06930 100644 --- a/Bitkit/Resources/Localization/ru.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ru.lproj/Localizable.strings @@ -4,8 +4,8 @@ "cards__discount__description" = "Баланс Расходов"; "cards__quickpay__title" = "QuickPay"; "cards__quickpay__description" = "Сканируйте и платите"; -"cards__notifications__description_alt" = "Когда Bitkit закрыт"; -"cards__notifications__title" = "Получайте Платежи"; +"cards__notifications__description_alt" = "Напоминания об оплате"; +"cards__notifications__title" = "Уведомления"; "cards__invite__title" = "Пригласить"; "cards__invite__description" = "Поделитесь Bitkit"; "cards__lightning__title" = "Тратить"; @@ -151,9 +151,7 @@ "common__learn_more" = "Узнать больше"; "common__understood" = "Понятно"; "common__connect" = "Подключиться"; -"common__min" = "Мин"; "common__max" = "Макс"; -"common__default" = "По умолчанию"; "common__preview" = "Предпросмотр"; "common__show_all" = "Показать Все"; "common__show_details" = "Показать Детали"; @@ -185,7 +183,6 @@ "fee__slow__range" = "±1-2 часа"; "fee__slow__shortRange" = "1-2ч"; "fee__slow__longRange" = "± 1-2 часа"; -"fee__minimum__title" = "Минимум"; "fee__minimum__longTitle" = "Минимум"; "fee__minimum__description" = "+2 часа"; "fee__minimum__shortRange" = "+2ч"; @@ -198,70 +195,33 @@ "fee__custom__range" = "Зависит от комиссии"; "fee__custom__shortRange" = "Зависит от комиссии"; "fee__custom__longRange" = "Зависит от комиссии"; -"lightning__transfer_intro__title" = "Расходы\nБаланс"; -"lightning__transfer_intro__text" = "Пополните свой баланс расходов, чтобы наслаждаться мгновенными и недорогими транзакциями с друзьями, семьёй и продавцами."; -"lightning__transfer_intro__button" = "Начать"; "lightning__funding__nav_title" = "Баланс Расходов"; "lightning__funding__title" = "Пополните свой Баланс Расходов"; -"lightning__funding__text" = "Самый простой вариант — использовать ваши сбережения Bitkit или отправить биткоин с другого кошелька."; -"lightning__funding__text_blocked" = "Bitkit в настоящее время не предоставляет услуги Lightning в вашей стране, но вы всё равно можете подключаться к другим узлам напрямую."; -"lightning__funding__text_blocked_cjit" = "Bitkit в настоящее время не предоставляет услуги Lightning в вашей стране, но вы всё равно можете подключаться к другим узлам напрямую."; "lightning__funding__button1" = "Перевести из Сбережений"; "lightning__funding__button2" = "Использовать Другой Кошелёк"; "lightning__funding__button3" = "Ручная настройка"; -"lightning__funding_advanced__nav_title" = "Баланс Расходов"; -"lightning__funding_advanced__title" = "Расширенная Настройка"; -"lightning__funding_advanced__text" = "Сканируйте QR-код, чтобы получить свой LNURL Канал от другого LSP, или выберите ручную настройку."; -"lightning__funding_advanced__button1" = "QR LNURL Канала"; -"lightning__funding_advanced__button2" = "Ручная Настройка"; "lightning__funding_advanced__button_short" = "Расширенная Настройка"; -"lightning__no_funds__title" = "Недостаточно Средств"; -"lightning__no_funds__description" = "Прежде чем вы сможете перевести средства со своего баланса сбережений, вам нужно отправить биткойны в ваш кошелёк Bitkit."; -"lightning__no_funds__fund" = "Пополнить Кошелёк"; "lightning__transfer__nav_title" = "Перевод Средств"; "lightning__transfer__confirm" = "Пожалуйста,\nПодтвердите"; "lightning__transfer__swipe" = "Проведите для Перевода"; "lightning__transfer_hw__broadcast_uncertain_title" = "Транзакция могла быть отправлена"; "lightning__transfer_hw__broadcast_uncertain_description" = "Ваша транзакция пополнения могла быть отправлена. Проверьте активность, прежде чем пробовать снова."; -"lightning__transfer_hw__open_connect" = "Открыть Trezor Connect"; "lightning__transfer_hw__reconnect_error_description" = "Переподключите аппаратное устройство: похоже, оно отключилось от телефона."; "lightning__transfer_hw__reconnect_error_title" = "Переподключите Аппаратное Устройство"; -"lightning__transfer_hw__sign_title" = "Подпишите на\nустройстве"; -"lightning__transfer_hw__signed_title" = "Транзакция\nподписана"; "lightning__transfer_to_savings_button" = "Перевести в Сбережения"; "lightning__transfer_to_spending_button" = "Перевести на Расходы"; -"lightning__spending_intro__title" = "Перевод\nна Расходы"; -"lightning__spending_intro__text" = "Переведите средства на свой баланс расходов, чтобы наслаждаться мгновенными и недорогими транзакциями с друзьями, семьёй и продавцами."; -"lightning__spending_intro__button" = "Начать"; "lightning__transfer_failed__nav_title" = "Неудачный Перевод"; "lightning__transfer_failed__title" = "Перевод\nНе Удался"; "lightning__transfer_failed__text" = "Извините, Bitkit не смог настроить ваш баланс расходов или завершить ваш перевод. Пожалуйста, попробуйте снова."; "lightning__transfer_failed__cancel" = "Проверить Ошибку"; "lightning__transfer_failed__continue" = "Попробовать Снова"; -"lightning__spending_amount__title" = "Перевод\nна Расходы"; "lightning__spending_amount__quarter" = "25%"; -"lightning__spending_amount__error_balance__description" = "Ваших сбережений не хватает на этот перевод и его комиссии. Попробуйте меньшую сумму."; -"lightning__spending_amount__error_balance__title" = "Недостаточно Сбережений"; "lightning__spending_amount__error_max__title" = "Максимум Баланса Расходов"; "lightning__spending_amount__error_max__description" = "Сумма, которую вы можете перевести на баланс расходов, в настоящее время ограничена ₿ {amount}."; "lightning__spending_confirm__network_fee" = "Сетевые Комиссии"; "lightning__spending_confirm__lsp_fee" = "Сервисные Комиссии"; "lightning__spending_confirm__amount" = "На Баланс Расходов"; -"lightning__spending_confirm__background_setup" = "Настроить в фоновом режиме"; "lightning__spending_confirm__total" = "Итого"; -"lightning__spending_confirm__default" = "Использовать По Умолчанию"; -"lightning__spending_confirm__fees_changed_title" = "Комиссии изменились"; -"lightning__spending_confirm__fees_changed_service" = "После создания заказа комиссии выросли на {amount}. Проведите ещё раз для подтверждения."; -"lightning__spending_confirm__fees_changed_network" = "Комиссии выросли на {amount}, потому что ставки комиссий или баланс сбережений изменились, пока вы были на этом экране. Проведите ещё раз для подтверждения."; -"lightning__spending_advanced__title" = "Входящая\nЁмкость"; -"lightning__spending_advanced__fee" = "Комиссия за Ликвидность"; -"lightning__spending_advanced__error_balance__description" = "Ваших сбережений не хватает на комиссию за ликвидность для этой входящей ёмкости. Выберите меньшую сумму."; -"lightning__spending_advanced__error_balance__title" = "Недостаточно Средств"; -"lightning__spending_advanced__error_max__title" = "Максимум Входящей Ёмкости"; -"lightning__spending_advanced__error_max__description" = "Входящая ёмкость сейчас ограничена ₿ {amount}."; -"lightning__liquidity__title" = "Ликвидность\n& Маршрутизация"; -"lightning__liquidity__text" = "Ваш Баланс Расходов использует Lightning Network для того, чтобы сделать ваши платежи дешевле, быстрее и конфиденциальнее.\n\nЭто работает как интернет-доступ, но вы платите за ликвидность и маршрутизацию вместо пропускной способности.\n\nЭта настройка включает некоторые единоразовые расходы."; -"lightning__liquidity__label" = "Ликвидность Баланса Расходов"; "lightning__savings_intro__title" = "Перевод\nв Сбережения"; "lightning__savings_intro__text" = "Переведите свой баланс расходов в сбережения, чтобы хранить свой биткойн в долгосрочной перспективе и обеспечить своё будущее."; "lightning__savings_intro__button" = "Начать"; @@ -285,9 +245,6 @@ "lightning__savings_interrupted__nav_title" = "Перевод\nПрерван"; "lightning__savings_interrupted__title" = "Держите Bitkit\nОткрытым"; "lightning__savings_interrupted__text" = "Средства ещё не переведены. Bitkit попытается инициировать перевод в течение следующих 30 минут. Пожалуйста, держите приложение открытым."; -"lightning__transfer_success__nav_title" = "Перевод Выполнен Успешно"; -"lightning__transfer_success__title_spending" = "Средства Переведены\nна Расходы"; -"lightning__transfer_success__text_spending" = "Ваши средства переведены, и ваш баланс расходов готов к использованию."; "lightning__transfer_success__title_savings" = "Средства Переведены\nв Сбережения"; "lightning__transfer_success__text_savings" = "Ваши средства переведены. Вы можете использовать эти средства немедленно."; "lightning__external__nav_title" = "Lightning Соединение"; @@ -310,12 +267,6 @@ "lightning__spending_label" = "Баланс Расходов"; "lightning__receiving_label" = "Входящая Ёмкость"; "lightning__setting_up_header" = "В Процессе Перевода"; -"lightning__setting_up_text" = "Пожалуйста, подождите, ваш перевод средств выполняется. Это займёт ±10 минут."; -"lightning__setting_up_step1" = "Обработка Платежа"; -"lightning__setting_up_step2" = "Платеж Успешен"; -"lightning__setting_up_step3" = "В Очереди на Открытие"; -"lightning__setting_up_step4" = "Открытие Соединения"; -"lightning__setting_up_button" = "Продолжить Использование Bitkit"; "lightning__node_info" = "Lightning Узел"; "lightning__node_id" = "ID Узла LDK"; "lightning__node_disconnected" = "Отключено"; @@ -386,9 +337,9 @@ "lightning__force_init_msg" = "Ваши средства будут доступны через ±14 дней."; "lightning__force_failed_title" = "Неудачный Принудительный Перевод"; "lightning__force_failed_msg" = "Не удалось перевести ваши средства обратно в сбережения. Пожалуйста, попробуйте снова."; -"lightning__force_channels_skipped" = "Некоторые соединения не удалось закрыть."; "lightning__channel_opened_title" = "Баланс Расходов Готов"; "lightning__channel_opened_msg" = "Теперь вы можете платить кому угодно, где угодно, мгновенно."; +"lightning__funding__button_hw" = "Перевести с аппаратного кошелька"; "lightning__order_given_up_title" = "Ошибка Настройки Мгновенных Платежей"; "lightning__order_given_up_msg" = "Процесс настройки истёк. Пожалуйста, попробуйте снова."; "lightning__order_expired_title" = "Ошибка Настройки Мгновенных Платежей"; @@ -407,6 +358,9 @@ "lightning__order_state__open" = "Соединение Открыто"; "lightning__order_state__inactive" = "Соединение неактивно"; "lightning__peer_disconnected" = "Узел отключён."; +"lightning__transfer_unavailable__nav_title" = "Перевод недоступен"; +"lightning__transfer_unavailable__text" = "Автоматические переводы на баланс расходов больше недоступны. Используйте собственный узел или настройте подключение к другому провайдеру."; +"lightning__transfer_unavailable__title" = "Требуется ручная\nнастройка"; "lightning__wait_text_top" = "Пожалуйста, подождите, пока Bitkit подключается к платёжной сети (±10 секунд)."; "lightning__wait_text_bottom" = "Подключение и Синхронизация..."; "onboarding__tos_header" = "Bitkit\nУсловия использования"; @@ -428,7 +382,6 @@ "onboarding__slide1_header" = "Мгновенные\nплатежи"; "onboarding__slide1_text" = "Тратьте биткойны быстрее, чем когда-либо. Наслаждайтесь мгновенными и недорогими платежами с друзьями, семьёй и продавцами."; "onboarding__slide1_text_alt" = "Наслаждайтесь мгновенными и недорогими платежами с друзьями, семьёй и продавцами в Lightning Network."; -"onboarding__slide1_note" = "*Bitkit в настоящее время не предоставляет услуги Lightning в вашей стране, но вы всё равно можете подключаться к другим узлам."; "onboarding__slide2_header" = "Биткойнеры,\nбез границ"; "onboarding__slide2_text" = "Берите под контроль свою цифровую жизнь с помощью портативных профилей и оплачиваемых контактов."; "onboarding__slide3_header" = "Конфиденциальность — это\nне преступление"; @@ -551,8 +504,6 @@ "other__lnurl_withdr_error_minmax" = "Неправильные параметры LNURL вывода, min/max установлены неправильно."; "other__lnurl_withdr_success_title" = "Запрос на Вывод"; "other__lnurl_withdr_success_msg" = "Ваш запрос на вывод успешно отправлен. Ожидание платежа."; -"other__notification_registration_failed_description" = "Bitkit не удалось зарегистрироваться для push-уведомлений."; -"other__notification_registration_failed_title" = "Ошибка Регистрации Уведомлений"; "other__phone_settings" = "Открыть Настройки Телефона"; "other__bt_error_retrieve" = "Перевод Не Удался"; "other__bt_error_connect" = "На данный момент невозможно добавить узел LSP в качестве пира."; @@ -568,14 +519,7 @@ "other__transfer_notification__body" = "Откройте Bitkit, чтобы завершить перевод"; "other__scan__error__generic" = "Bitkit не может прочитать предоставленные данные."; "other__scan__error__expired" = "Срок действия этого Lightning инвойса истёк."; -"other__gift__claimed__text" = "Извините, вы опоздали! Все подарки по этому коду уже получены."; -"other__gift__claiming__text" = "Получение подарочного кода Bitkit..."; -"other__gift__claiming__title" = "Получение Подарка"; -"other__gift__error__title" = "Ошибка Подарочного Кода"; -"other__gift__error__text" = "Bitkit не удалось получить средства. Пожалуйста, попробуйте позже или свяжитесь с нами."; "other__gift__error__button" = "Ок"; -"other__gift__used__title" = "Использованный Код"; -"other__gift__used__text" = "Этот подарочный код Bitkit уже был использован, и средства были выплачены."; "other__gift__used__button" = "Ок"; "other__shop__intro__title" = "Магазин"; "other__shop__intro__description" = "Переходите на биткоин-стандарт. Тратьте свои Bitcoin на цифровые подарочные карты, eSIM, пополнение телефона и многое другое."; @@ -758,6 +702,7 @@ "pubky_auth__homeserver" = "Домашний сервер"; "pubky_auth__signup_description" = "Создайте новый идентификатор Pubky на этом домашнем сервере. Продолжайте, только если доверяете ему."; "pubky_auth__approval_failed" = "Ошибка Авторизации"; +"wallet__onchain_abandon_transfer_note" = "Переводы на баланс расходов больше недоступны. Bitkit сначала проверяет сеть, а затем возвращает монеты этого перевода в ваш кошелёк, чтобы исходный перевод никогда не подтвердился. За это взимается сетевая комиссия."; "watch_only_accounts__active_section" = "Активные аккаунты"; "watch_only_accounts__copy_xpub" = "Копировать xpub"; "watch_only_accounts__description" = "Каждый одобренный сервер Paykit получает отдельный аккаунт Bitcoin. Отключите отслеживание, чтобы выгрузить аккаунт, не удаляя историю его кошелька."; @@ -834,21 +779,17 @@ "settings__widgets__reset_widgets_dialog_title" = "Сбросить виджеты?"; "settings__widgets__reset_widgets_dialog_description" = "Вы уверены, что хотите сбросить виджеты? Будет отображён набор виджетов по умолчанию с настройками по умолчанию."; "settings__widgets__reset_suggestions" = "Сбросить карточки предложений"; -"settings__notifications__nav_title" = "Фоновые Платежи"; -"settings__notifications__intro__title" = "Получайте Платежи\nПассивно"; -"settings__notifications__intro__text" = "Включите уведомления, чтобы получать платежи, даже когда приложение Bitkit закрыто."; +"settings__notifications__nav_title" = "Уведомления"; +"settings__notifications__intro__title" = "Будьте\nв курсе"; +"settings__notifications__intro__text" = "Включите уведомления, чтобы получать напоминания о подписках и запросах на оплату."; "settings__notifications__intro__button" = "Включить"; -"settings__notifications__settings__toggle" = "Получать платежи при закрытом Bitkit"; -"settings__notifications__settings__enabled" = "Фоновые платежи включены. Вы можете получать средства, даже когда приложение закрыто (если ваше устройство подключено к интернету)."; -"settings__notifications__settings__disabled" = "Включите фоновые платежи, чтобы получать средства, даже когда приложение закрыто (если ваше устройство подключено к интернету)."; -"settings__notifications__settings__denied" = "Фоновые платежи отключены, потому что вы отказали в уведомлениях."; -"settings__notifications__settings__preview__title" = "Платёж Получен"; -"settings__notifications__settings__preview__text" = "Откройте Bitkit для просмотра деталей"; +"settings__notifications__settings__toggle" = "Разрешить уведомления"; +"settings__notifications__settings__enabled" = "Уведомления включены. Bitkit может напоминать о подписках и запросах на оплату."; +"settings__notifications__settings__disabled" = "Включите уведомления, чтобы получать напоминания о подписках и запросах на оплату."; +"settings__notifications__settings__denied" = "Уведомления отключены, потому что вы их запретили."; "settings__notifications__settings__preview__time" = "3 мин. назад"; -"settings__notifications__settings__privacy__label" = "Конфиденциальность"; -"settings__notifications__settings__privacy__text" = "Включать сумму в уведомления"; "settings__notifications__settings__notifications__label" = "Уведомления"; -"settings__notifications__settings__notifications__text" = "Чтобы включить фоновые платежи, включите уведомления в настройках iOS Bitkit."; +"settings__notifications__settings__notifications__text" = "Чтобы получать напоминания, включите уведомления в настройках iOS Bitkit."; "settings__notifications__settings__button__enabled" = "Настроить в настройках Bitkit для {platform}"; "settings__notifications__settings__button__disabled" = "Включить в настройках Bitkit для {platform}"; "settings__quickpay__nav_title" = "QuickPay"; @@ -1388,27 +1329,11 @@ "wallet__receive_note_placeholder" = "Опциональная заметка для плательщика"; "wallet__receive_show_qr" = "Показать QR-код"; "wallet__receive_text_lnfunds" = "Хотите получить Лайтнинг средства?"; -"wallet__receive_background_setup_text" = "Включите фоновую настройку, чтобы безопасно выйти из Bitkit пока настраивается ваш баланс."; -"wallet__receive_background_setup_toggle" = "Включить фоновую настройку"; "wallet__receive_spending" = "Получить Lightning средства"; "wallet__receive_spending_text" = "Наслаждайтесь мгновенными и дешёвыми биткоин-платежами в сети Lightning."; "wallet__receive_spending_title" = "Получить на баланс расходов"; -"wallet__receive_connect_initial" = "Для настройки вашего баланса расходов будет удержана сетевая комиссия {networkFee} и комиссия провайдера услуг {serviceFee}."; -"wallet__receive_connect_additional" = "Чтобы получить больше мгновенных Bitcoin, Bitkit должен увеличить вашу ликвидность. Из указанной суммы будет удержана сетевая комиссия {networkFee} и комиссия провайдера услуг {serviceFee}."; -"wallet__receive_liquidity__nav_title" = "Настройка Баланса Расходов"; -"wallet__receive_liquidity__nav_title_additional" = "Увеличить Приёмную Мощность"; -"wallet__receive_liquidity__invalid_quote" = "Bitkit получил недействительное предложение ликвидности. Повторите попытку."; -"wallet__receive_liquidity__text" = "Ваш Баланс Расходов использует Lightning Network для того, чтобы сделать ваши платежи дешевле, быстрее и конфиденциальнее.\n\nЭто работает как интернет-доступ, но вы платите за ликвидность и маршрутизацию вместо пропускной способности.\n\nЭта настройка включает некоторые единоразовые расходы."; -"wallet__receive_liquidity__text_additional" = "Ваш Баланс Расходов использует Lightning Network для того, чтобы сделать ваши платежи дешевле, быстрее и конфиденциальнее.\n\nЭто работает как интернет-доступ, но вы платите за ликвидность и маршрутизацию вместо пропускной способности.\n\nBitkit нужно увеличить входящую ёмкость вашего баланса расходов для обработки этого платежа."; -"wallet__receive_liquidity__label" = "Ликвидность Баланса Расходов"; -"wallet__receive_liquidity__label_additional" = "Дополнительная Ликвидность Баланса Расходов"; "wallet__receive_cjit_error" = "Транзакция не удалась"; "wallet__receive_cjit_error_msg" = "Не удалось отправить средства на ваш Баланс Расходов."; -"wallet__receive_cjit_error_max__title" = "Максимум Входящей Ёмкости"; -"wallet__receive_cjit_error_max__description" = "Максимум, который вы сейчас можете получить на баланс расходов, — ₿ {amount}."; -"wallet__receive_cjit_error_node_capacity__title" = "Дополнительная емкость недоступна"; -"wallet__receive_cjit_error_node_capacity__description" = "Bitkit сейчас не может добавить больше емкости для расходов в этот кошелек."; -"wallet__receive_will" = "Вы получите"; "wallet__receive_ldk_init" = "Инициализация Баланса Расходов..."; "wallet__minimum" = "МИНИМУМ"; "wallet__activity" = "Активность"; @@ -1597,7 +1522,7 @@ "subscriptions__name_placeholder" = "Название подписки"; "subscriptions__ongoing" = "Бессрочно"; "subscriptions__overview" = "Обзор"; -"subscriptions__payment_due_description" = "Откройте Bitkit, чтобы проверить платёж по подписке."; +"subscriptions__payment_due_description" = "Откройте Bitkit, чтобы проверить платёж"; "subscriptions__payment_due_title" = "Пора Оплатить Подписку"; "subscriptions__payments" = "Платежи"; "subscriptions__per_day" = "в день"; @@ -1766,9 +1691,6 @@ "wallet__onchain_broadcast_rejected_note" = "Этот платёж всё равно мог попасть в сеть. Не отправляйте его повторно. {reason}"; "wallet__onchain_broadcast_unconfirmed" = "Отправка не подтверждена"; "wallet__onchain_broadcast_unconfirmed_note" = "Платёж мог быть отправлен. Не отправляйте его повторно."; -"wallet__onchain_funding_rejected" = "Транзакция пополнения отклонена. Не пытайтесь пополнить этот заказ повторно."; -"wallet__onchain_funding_unknown" = "Транзакция пополнения могла быть отправлена. Не пытайтесь пополнить этот заказ повторно."; -"wallet__onchain_funding_followup_failed" = "Пополнение отправлено, но локальное отслеживание заказа не удалось сохранить. Не пополняйте этот заказ повторно."; "wallet__onchain_retry_original" = "Повторить исходный платёж"; "wallet__onchain_retry_fee" = "Ставка комиссии (sat/vB)"; "wallet__onchain_retry_note" = "Повторить {amount} sats на {address} с теми же исходными монетами. Сумма получателя не изменится. Введите ставку сетевой комиссии в sat/vB. Это не может создать отдельный платёж."; diff --git a/Bitkit/Services/BackupService.swift b/Bitkit/Services/BackupService.swift index f9e40f1e4..2b9e28fec 100644 --- a/Bitkit/Services/BackupService.swift +++ b/Bitkit/Services/BackupService.swift @@ -445,7 +445,7 @@ class BackupService { categoriesNeedingRewrite.insert(.metadata) } - try await SettingsViewModel.shared.restoreAppCacheData(payload.cache) + await SettingsViewModel.shared.restoreAppCacheData(payload.cache) do { try await PubkyProfileManager.restoreSessionBackupState(payload.pubkySession) @@ -934,7 +934,7 @@ class BackupService { case .metadata: let currentTime = UInt64(Date().timeIntervalSince1970 * 1000) - let cache = try await SettingsViewModel.shared.getAppCacheData() + let cache = await SettingsViewModel.shared.getAppCacheData() let pubkySession = try PubkyProfileManager.snapshotSessionBackupState() let pubkyContactProfileOverrides = ContactsManager.backupContactProfileOverrides() diff --git a/Bitkit/Services/BlocktankRefundAddressProvider.swift b/Bitkit/Services/BlocktankRefundAddressProvider.swift deleted file mode 100644 index 05eaaa438..000000000 --- a/Bitkit/Services/BlocktankRefundAddressProvider.swift +++ /dev/null @@ -1,275 +0,0 @@ -import Foundation -import LDKNode - -enum BlocktankRefundAddressError: LocalizedError, Equatable { - case invalidCache - case invalidAddress - case indexOutOfRange(UInt32) - case ownershipMismatch - case persistenceFailed - case allocationDidNotAdvance - case allocationAttemptsExhausted - - var errorDescription: String? { - switch self { - case .invalidCache: - "The saved Blocktank refund address is invalid." - case .invalidAddress: - "The Blocktank refund address is empty." - case let .indexOutOfRange(index): - "The Blocktank refund address index is out of range: \(index)." - case .ownershipMismatch: - "The saved Blocktank refund address does not belong to the active wallet and network." - case .persistenceFailed: - "The Blocktank refund address could not be saved." - case .allocationDidNotAdvance: - "The Blocktank refund address index did not advance." - case .allocationAttemptsExhausted: - "No unused Blocktank refund address was found." - } - } -} - -struct BlocktankRefundAddressStore { - static let legacyKey = "blocktankRefundAddress" - static var key: String { - key(for: Env.network) - } - - private let defaults: UserDefaults - private let network: LDKNode.Network - - private var key: String { - Self.key(for: network) - } - - init(defaults: UserDefaults = .standard, network: LDKNode.Network = Env.network) { - self.defaults = defaults - self.network = network - } - - static func key(for network: LDKNode.Network) -> String { - "\(legacyKey)_\(Env.networkName(for: network))" - } - - func load() throws -> BlocktankRefundAddress? { - if let value = try load(forKey: key) { - return value - } - - guard let legacy = try load(forKey: Self.legacyKey) else { return nil } - let matchingNetworks = Self.matchingNetworks(for: legacy.address) - guard !matchingNetworks.isEmpty else { - throw BlocktankRefundAddressError.invalidCache - } - guard matchingNetworks == [network] else { return nil } - - try save(legacy) - defaults.removeObject(forKey: Self.legacyKey) - return legacy - } - - private func load(forKey key: String) throws -> BlocktankRefundAddress? { - guard defaults.object(forKey: key) != nil else { return nil } - guard let data = defaults.data(forKey: key) else { - throw BlocktankRefundAddressError.invalidCache - } - - do { - return try JSONDecoder().decode(BlocktankRefundAddress.self, from: data) - } catch { - throw BlocktankRefundAddressError.invalidCache - } - } - - func save(_ value: BlocktankRefundAddress) throws { - let data = try JSONEncoder().encode(value) - let previousValue = defaults.object(forKey: key) - defaults.set(data, forKey: key) - - do { - guard try load(forKey: key) == value else { - throw BlocktankRefundAddressError.persistenceFailed - } - } catch { - if let previousValue { - defaults.set(previousValue, forKey: key) - } else { - defaults.removeObject(forKey: key) - } - throw BlocktankRefundAddressError.persistenceFailed - } - } - - func clear() { - defaults.removeObject(forKey: key) - - guard defaults.object(forKey: Self.legacyKey) != nil else { return } - guard let legacy = try? load(forKey: Self.legacyKey) else { - defaults.removeObject(forKey: Self.legacyKey) - return - } - let matchingNetworks = Self.matchingNetworks(for: legacy.address) - if matchingNetworks.isEmpty || matchingNetworks == [network] { - defaults.removeObject(forKey: Self.legacyKey) - } - } - - private static func matchingNetworks(for address: String) -> [LDKNode.Network] { - let trimmed = address.trimmingCharacters(in: .whitespaces) - if trimmed.hasPrefix("bc1q") { - return [.bitcoin] - } - if trimmed.hasPrefix("bcrt1q") { - return [.regtest] - } - if trimmed.hasPrefix("tb1q") { - return [.testnet, .signet] - } - return [] - } -} - -@MainActor -protocol BlocktankRefundAddressProviding: AnyObject { - func addressForOrder() async throws -> String -} - -@MainActor -final class BlocktankRefundAddressProvider: BlocktankRefundAddressProviding { - static let maximumExternalIndex = UInt32(Int32.max) - static let maximumAllocationAttempts = 20 - - typealias Load = () throws -> BlocktankRefundAddress? - typealias Save = (BlocktankRefundAddress) throws -> Void - typealias Lookup = (UInt32) async throws -> BlocktankRefundAddress - typealias Reveal = (UInt32) async throws -> Void - typealias IsUsed = (String) async throws -> Bool - typealias Allocate = () async throws -> BlocktankRefundAddress - - private let load: Load - private let save: Save - private let lookup: Lookup - private let reveal: Reveal - private let isUsed: IsUsed - private let allocate: Allocate - private var inFlight: Task? - - init( - load: @escaping Load, - save: @escaping Save, - lookup: @escaping Lookup, - reveal: @escaping Reveal, - isUsed: @escaping IsUsed, - allocate: @escaping Allocate - ) { - self.load = load - self.save = save - self.lookup = lookup - self.reveal = reveal - self.isUsed = isUsed - self.allocate = allocate - } - - convenience init( - lightningService: LightningService, - utilityService: UtilityService, - store: BlocktankRefundAddressStore = .init() - ) { - self.init( - load: { try store.load() }, - save: { try store.save($0) }, - lookup: { index in - let info = try await lightningService.addressInfoForType( - .nativeSegwit, - keychain: .external, - atIndex: index - ) - return BlocktankRefundAddress(address: info.address, index: info.index) - }, - reveal: { index in - try await lightningService.revealReceiveAddresses(to: index, forType: .nativeSegwit) - }, - isUsed: { address in - try await utilityService.isAddressUsed(address: address) - }, - allocate: { - let info = try await lightningService.newAddressInfoForType(.nativeSegwit) - return BlocktankRefundAddress(address: info.address, index: info.index) - } - ) - } - - func addressForOrder() async throws -> String { - if let inFlight { - return try await inFlight.value - } - - let operation = Task { @MainActor [load, save, lookup, reveal, isUsed, allocate] in - try await Self.resolve( - load: load, - save: save, - lookup: lookup, - reveal: reveal, - isUsed: isUsed, - allocate: allocate - ) - } - inFlight = operation - defer { inFlight = nil } - return try await operation.value - } - - private static func resolve( - load: Load, - save: Save, - lookup: Lookup, - reveal: Reveal, - isUsed: IsUsed, - allocate: Allocate - ) async throws -> String { - var previousIndex: UInt32? - - if let cached = try load() { - try validate(cached) - - let derived = try await lookup(cached.index) - guard derived == cached else { - throw BlocktankRefundAddressError.ownershipMismatch - } - - try await reveal(cached.index) - if try await isUsed(cached.address) == false { - return cached.address - } - previousIndex = cached.index - } - - for _ in 0 ..< maximumAllocationAttempts { - let generated = try await allocate() - try validate(generated) - if let previousIndex, generated.index <= previousIndex { - throw BlocktankRefundAddressError.allocationDidNotAdvance - } - previousIndex = generated.index - - if try await isUsed(generated.address) { - continue - } - - try save(generated) - return generated.address - } - - throw BlocktankRefundAddressError.allocationAttemptsExhausted - } - - private static func validate(_ value: BlocktankRefundAddress) throws { - guard !value.address.isEmpty else { - throw BlocktankRefundAddressError.invalidAddress - } - guard value.index <= maximumExternalIndex else { - throw BlocktankRefundAddressError.indexOutOfRange(value.index) - } - } -} diff --git a/Bitkit/Services/CoreService.swift b/Bitkit/Services/CoreService.swift index 54329c40b..4a2dbe334 100644 --- a/Bitkit/Services/CoreService.swift +++ b/Bitkit/Services/CoreService.swift @@ -2224,30 +2224,6 @@ class BlocktankService { try await info(refresh: refresh)?.onchain.feeRates } - func createCjit( - channelSizeSat: UInt64, - invoiceSat: UInt64, - invoiceDescription: String, - nodeId: String, - channelExpiryWeeks: UInt32, - options: CreateCjitOptions - ) async throws -> IcJitEntry { - Logger.info("Creating CJIT invoice with channel size: \(channelSizeSat) and invoice amount: \(invoiceSat)", context: "BlocktankService") - - return try await ServiceQueue.background(.core) { - let entry = try await createCjitEntry( - channelSizeSat: channelSizeSat, - invoiceSat: invoiceSat, - invoiceDescription: invoiceDescription, - nodeId: nodeId, - channelExpiryWeeks: channelExpiryWeeks, - options: options - ) - self.stateChangedSubject.send() - return entry - } - } - func cjitOrders(entryIds: [String]? = nil, filter: CJitStateEnum? = nil, refresh: Bool = true) async throws -> [IcJitEntry] { try await ServiceQueue.background(.core) { try await getCjitEntries(entryIds: entryIds, filter: filter, refresh: refresh) @@ -2265,36 +2241,6 @@ class BlocktankService { } } - func newOrder( - lspBalanceSat: UInt64, - channelExpiryWeeks: UInt32, - options: CreateOrderOptions - ) async throws -> IBtOrder { - try await ServiceQueue.background(.core) { - let order = try await createOrder( - lspBalanceSat: lspBalanceSat, - channelExpiryWeeks: channelExpiryWeeks, - options: options - ) - self.stateChangedSubject.send() - return order - } - } - - func estimateFee( - lspBalanceSat: UInt64, - channelExpiryWeeks: UInt32, - options: CreateOrderOptions? = nil - ) async throws -> IBtEstimateFeeResponse2 { - try await ServiceQueue.background(.core) { - try await estimateOrderFeeFull( - lspBalanceSat: lspBalanceSat, - channelExpiryWeeks: channelExpiryWeeks, - options: options - ) - } - } - func orders(orderIds: [String]? = nil, filter: BtOrderState2? = nil, refresh: Bool = true) async throws -> [IBtOrder] { try await ServiceQueue.background(.core) { try await getOrders(orderIds: orderIds, filter: filter, refresh: refresh) @@ -2324,55 +2270,6 @@ class BlocktankService { stateChangedSubject.send() } - @discardableResult - func open(orderId: String) async throws -> IBtOrder { - guard let nodeId = LightningService.shared.nodeId else { - throw AppError(serviceError: .nodeNotStarted) - } - - let latestOrder = try await ServiceQueue.background(.core) { - try await getOrders(orderIds: [orderId], filter: nil, refresh: true).first - } - - guard latestOrder?.state2 == .paid else { - throw AppError(message: "Order not paid", debugMessage: "Order state: \(String(describing: latestOrder?.state2))") - } - - return try await ServiceQueue.background(.core) { - try await openChannel(orderId: orderId, connectionString: nodeId) - } - } - - // MARK: Notifications - - func registerDeviceForNotifications( - deviceToken: String, publicKey: String, features: [String], nodeId: String, isoTimestamp: String, signature: String - ) async throws -> String { - try await ServiceQueue.background(.core) { - try await registerDevice( - deviceToken: deviceToken, - publicKey: publicKey, - features: features, - nodeId: nodeId, - isoTimestamp: isoTimestamp, - signature: signature, - isProduction: !Env.isDebug, - customUrl: Env.blocktankPushNotificationServer - ) - } - } - - func pushNotificationTest(deviceToken: String, secretMessage: String, notificationType: String?) async throws -> String { - try await ServiceQueue.background(.core) { - try await testNotification( - deviceToken: deviceToken, - secretMessage: secretMessage, - notificationType: notificationType, - customUrl: Env.blocktankPushNotificationServer - ) - } - } - // MARK: Regtest only methods private func executeWithRetry(maxRetries: Int = 6, operation: @escaping () async throws -> T) async throws -> T { @@ -2656,35 +2553,4 @@ class CoreService { } } } - - func checkGeoStatus() async throws -> Bool? { - 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 as Bool? - } - - let (_, response) = try await URLSession.shared.data(from: url) - if let httpResponse = response as? HTTPURLResponse { - Logger.debug("Received geo status response: \(httpResponse.statusCode)", context: "GeoCheck") - switch httpResponse.statusCode { - case 200: - Logger.info("Region allowed", context: "GeoCheck") - return false - case 403: - Logger.warn("Region blocked", context: "GeoCheck") - return true - default: - Logger.warn("Unexpected status code: \(httpResponse.statusCode)", context: "GeoCheck") - return nil as Bool? - } - } - return nil as Bool? - } - } } diff --git a/Bitkit/Services/CurrencyService.swift b/Bitkit/Services/CurrencyService.swift index d269614a2..b59aa90a5 100644 --- a/Bitkit/Services/CurrencyService.swift +++ b/Bitkit/Services/CurrencyService.swift @@ -52,24 +52,6 @@ class CurrencyService { } return rates } - - func convertFiatToSats(fiatValue: Decimal, rate: FxRate) -> UInt64 { - let btcAmount = fiatValue / rate.rate - let satsDecimal = btcAmount * 100_000_000 - - // Use NSDecimalNumber for rounding - let decimalNumber = NSDecimalNumber(decimal: satsDecimal) - let roundingBehavior = NSDecimalNumberHandler( - roundingMode: .plain, - scale: 0, - raiseOnExactness: false, - raiseOnOverflow: false, - raiseOnUnderflow: false, - raiseOnDivideByZero: false - ) - let roundedNumber = decimalNumber.rounding(accordingToBehavior: roundingBehavior) - return roundedNumber.uint64Value - } } // MARK: UI Helpers (Published via CurrencyViewModel) diff --git a/Bitkit/Services/GeoService.swift b/Bitkit/Services/GeoService.swift deleted file mode 100644 index 725643c8d..000000000 --- a/Bitkit/Services/GeoService.swift +++ /dev/null @@ -1,36 +0,0 @@ -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") - } - } -} diff --git a/Bitkit/Services/LightningService.swift b/Bitkit/Services/LightningService.swift index 9e5cf990d..f24601b98 100644 --- a/Bitkit/Services/LightningService.swift +++ b/Bitkit/Services/LightningService.swift @@ -109,22 +109,8 @@ class LightningService { currentWalletIndex = walletIndex - var config = defaultConfig() - let ldkStoragePath = Env.ldkStorage(walletIndex: walletIndex).path - config.storageDirPath = ldkStoragePath - config.network = Env.network - config.probingLiquidityLimitMultiplier = 1 - - Logger.debug("Using LDK storage path: \(ldkStoragePath)") - - let trustedPeersIds = Env.trustedLnPeers.map(\.nodeId) - - config.trustedPeers0conf = trustedPeersIds - config.anchorChannelsConfig = .init( - trustedPeersNoReserve: trustedPeersIds, - perChannelReserveSats: 1 - ) - config.includeUntrustedPendingInSpendable = true + var config = Self.nodeConfig(walletIndex: walletIndex) + Logger.debug("Using LDK storage path: \(config.storageDirPath)") let (selectedAddressType, monitoredTypes) = Self.addressTypeStateFromUserDefaults() config.addressType = selectedAddressType @@ -376,64 +362,56 @@ class LightningService { return modificationDate } - func connectToTrustedPeers(remotePeers: [LnPeer]? = nil) async throws { + /// Disconnects stored peers we have nothing pending with, which also drops them from the peer store. + /// Skips the run while any closed channel still awaits its sweep, as a sweep does not name its peer. + func disconnectIdlePeers() async throws { guard let node else { throw AppError(serviceError: .nodeNotSetup) } - let peers: [LnPeer] - let usingRemotePeers: Bool - if let remotePeers, !remotePeers.isEmpty { - Logger.info("Using \(remotePeers.count) trusted peers from Blocktank API") - peers = remotePeers - usingRemotePeers = true - } else { - Logger.warn("No remote peers available, falling back to preconfigured env peers") - peers = Env.trustedLnPeers - usingRemotePeers = false - } - try await ServiceQueue.background(.ldk) { - for peer in peers { + let balances = node.listBalances() + let idlePeerIds = Self.idlePeerIds( + peers: node.listPeers(), + channels: node.listChannels(), + lightningBalances: balances.lightningBalances, + pendingSweeps: balances.pendingBalancesFromChannelClosures + ) + for nodeId in idlePeerIds { do { - try node.connect(nodeId: peer.nodeId, address: peer.address, persist: true) - Logger.info("Connected to trusted peer: \(peer.nodeId)") + try node.disconnect(nodeId: nodeId) + Logger.info("Disconnected idle peer: \(nodeId)") } catch { - Logger.error(error, context: "Peer: \(peer.nodeId)") + Logger.warn("Failed to disconnect idle peer: \(nodeId)") } } - - if usingRemotePeers { - self.verifyTrustedPeersOrFallback(node: node, trustedPeers: peers) - } } } - private func verifyTrustedPeersOrFallback(node: Node, trustedPeers: [LnPeer]) { - let connectedPeerIds = Set(node.listPeers().filter(\.isConnected).map(\.nodeId)) - let trustedConnected = trustedPeers.filter { connectedPeerIds.contains($0.nodeId) }.count - let trustedPeerIds = Set(trustedPeers.map(\.nodeId)) - - if trustedConnected == 0, !trustedPeers.isEmpty { - let fallbackPeers = Env.trustedLnPeers.filter { !trustedPeerIds.contains($0.nodeId) } - if fallbackPeers.isEmpty { - Logger.warn("No trusted peers connected. All preconfigured env peers overlap with API peers (not retrying with stale addresses).") - } else { - Logger.warn("No trusted peers connected, trying \(fallbackPeers.count) preconfigured env peer(s) not in API list") - for peer in fallbackPeers { - do { - try node.connect(nodeId: peer.nodeId, address: peer.address, persist: true) - Logger.info("Connected to fallback peer: \(peer.nodeId)") - } catch { - Logger.error(error, context: "Fallback peer: \(peer.nodeId)") - } - } - } - } else { - Logger.info("Connected to \(trustedConnected)/\(trustedPeers.count) trusted peers") + private static func counterpartyNodeId(of balance: LightningBalance) -> PublicKey { + switch balance { + case let .claimableOnChannelClose(_, counterpartyNodeId, _, _, _, _, _, _), + let .claimableAwaitingConfirmations(_, counterpartyNodeId, _, _, _), + let .contentiousClaimable(_, counterpartyNodeId, _, _, _, _), + let .maybeTimeoutClaimableHtlc(_, counterpartyNodeId, _, _, _, _), + let .maybePreimageClaimableHtlc(_, counterpartyNodeId, _, _, _), + let .counterpartyRevokedOutputClaimable(_, counterpartyNodeId, _): + return counterpartyNodeId } } + /// Returns the stored peers with no channel and no claimable balance, or none while any sweep is pending. + static func idlePeerIds( + peers: [PeerDetails], + channels: [ChannelDetails], + lightningBalances: [LightningBalance], + pendingSweeps: [PendingSweepBalance] + ) -> [PublicKey] { + guard pendingSweeps.isEmpty else { return [] } + let busyPeerIds = Set(channels.map(\.counterpartyNodeId) + lightningBalances.map(counterpartyNodeId(of:))) + return peers.filter { $0.isPersisted && !busyPeerIds.contains($0.nodeId) }.map(\.nodeId) + } + func connectPeer(peer: LnPeer, persist: Bool = true) async throws { guard let node else { throw AppError(serviceError: .nodeNotSetup) @@ -914,17 +892,6 @@ class LightningService { Logger.debug("closeChannel called to channel=\(channel), force=\(force)", context: "LightningService") - // Prevent force closing channels with trusted peers (LSP nodes) - if force { - let trustedPeerIds = Set(getLspPeerNodeIds()) - if trustedPeerIds.contains(channel.counterpartyNodeId.description) { - throw AppError( - message: "Cannot force close channel with trusted peer", - debugMessage: "Force close is disabled for Blocktank LSP channels. Please use cooperative close instead." - ) - } - } - return try await closeChannel( userChannelId: channel.userChannelId, counterpartyNodeId: channel.counterpartyNodeId, @@ -1104,25 +1071,6 @@ class LightningService { sb += " Total nodes: \(allNodes.count)\n" sb += " Total channels: \(allChannels.count)\n" - // Check for trusted peers in graph - sb += "\n Checking for trusted peers in network graph:\n" - let trustedPeers = Env.trustedLnPeers - var foundTrustedNodes = 0 - let allNodeStrings = allNodes.map(\.description) - - for peer in trustedPeers { - let nodeId = peer.nodeId - if allNodeStrings.contains(nodeId) { - foundTrustedNodes += 1 - let nodeIdPreview = String(nodeId.prefix(nodeIdPreviewLength)) - sb += " OK: \(nodeIdPreview)... found in graph\n" - } else { - let nodeIdPreview = String(nodeId.prefix(nodeIdPreviewLength)) - sb += " MISSING: \(nodeIdPreview)... NOT in graph\n" - } - } - sb += " Summary: \(foundTrustedNodes)/\(trustedPeers.count) trusted peers found in graph\n" - // Show first 10 nodes let nodesToShow = min(10, allNodes.count) sb += "\n First \(nodesToShow) nodes:\n" @@ -1351,6 +1299,22 @@ extension LightningService { return totalFundable } + /// Builds the node config. LSP peers are not trusted for zero-conf channels or to broadcast our force + /// closes: Blocktank no longer serves channels, so our node must broadcast and fee-bump its own closes. + static func nodeConfig(walletIndex: Int) -> Config { + var config = defaultConfig() + config.storageDirPath = Env.ldkStorage(walletIndex: walletIndex).path + config.network = Env.network + config.probingLiquidityLimitMultiplier = 1 + config.trustedPeers0conf = [] + config.anchorChannelsConfig = .init( + trustedPeersNoReserve: [], + perChannelReserveSats: 1 + ) + config.includeUntrustedPendingInSpendable = true + return config + } + /// Reads selected and monitored address types from UserDefaults and keeps native SegWit enabled /// so delayed Blocktank refund payments remain detectable. static func addressTypeStateFromUserDefaults(_ defaults: UserDefaults = .standard) @@ -1366,23 +1330,6 @@ extension LightningService { return (selectedType, monitoredTypes) } - /// Returns LSP (Blocktank) peer node IDs - func getLspPeerNodeIds() -> [String] { - return Env.trustedLnPeers.map(\.nodeId) - } - - /// Separates channels into trusted (LSP) and non-trusted peers - func separateTrustedChannels(_ channels: [ChannelDetails]) -> (trusted: [ChannelDetails], nonTrusted: [ChannelDetails]) { - let trustedPeerIds = Set(getLspPeerNodeIds()) - let trusted = channels.filter { channel in - trustedPeerIds.contains(channel.counterpartyNodeId.description) - } - let nonTrusted = channels.filter { channel in - !trustedPeerIds.contains(channel.counterpartyNodeId.description) - } - return (trusted: trusted, nonTrusted: nonTrusted) - } - /// Get a channel by ID from the channel cache /// This is more reliable than using the cachedChannels array when handling events, /// as the channelCache is updated immediately when channel events occur diff --git a/Bitkit/Services/MigrationsService.swift b/Bitkit/Services/MigrationsService.swift index 729d5142d..46ba8c07f 100644 --- a/Bitkit/Services/MigrationsService.swift +++ b/Bitkit/Services/MigrationsService.swift @@ -110,8 +110,6 @@ struct RNSettings: Codable { var hideBalanceOnOpen: Bool? var quickpayIntroSeen: Bool? var shopIntroSeen: Bool? - var transferIntroSeen: Bool? - var spendingIntroSeen: Bool? var savingsIntroSeen: Bool? } @@ -377,12 +375,10 @@ class MigrationsService: ObservableObject { private static let rnPendingRemoteBoostsKey = "rnPendingRemoteBoosts" private static let rnPendingMetadataKey = "rnPendingMetadata" private static let rnRetryScheduleKey = "rnMigrationRetrySchedule" - private static let rnPendingRemotePaidOrdersKey = "rnPendingRemotePaidOrders" private static let rnPendingChannelMigrationKey = "rnPendingChannelMigration" private static let rnPendingBlocktankOrderIdsKey = "rnPendingBlocktankOrderIds" private static let rnDidAttemptPeerRecoveryKey = "rnDidAttemptMigrationPeerRecovery" private static let rnChannelRecoveryCheckedKey = "rnChannelRecoveryChecked" - private static let didCleanupInvalidTransfersKey = "didCleanupInvalidMigrationTransfers" @Published var isShowingMigrationLoading = false { didSet { @@ -473,12 +469,6 @@ class MigrationsService: ObservableObject { set { setCodable(newValue, forKey: Self.rnPendingMetadataKey) } } - /// Stored paid orders from RN backup for creating transfers after wallet starts (persisted) - var pendingRemotePaidOrders: [String: String]? { - get { userDefaults.dictionary(forKey: Self.rnPendingRemotePaidOrdersKey) as? [String: String] } - set { userDefaults.set(newValue, forKey: Self.rnPendingRemotePaidOrdersKey) } - } - /// Stored Blocktank order IDs that couldn't be fetched during migration (offline) (persisted) var pendingBlocktankOrderIds: [String]? { get { userDefaults.stringArray(forKey: Self.rnPendingBlocktankOrderIdsKey) } @@ -821,7 +811,7 @@ extension MigrationsService { var hasPendingMigrationRetries: Bool { pendingMetadata != nil || pendingRemoteActivityData != nil || pendingRemoteTransfers != nil || - pendingRemoteBoosts != nil || pendingBlocktankOrderIds != nil || pendingRemotePaidOrders != nil + pendingRemoteBoosts != nil || pendingBlocktankOrderIds != nil } private var retrySchedule: RNMigrationRetrySchedule? { @@ -842,7 +832,7 @@ extension MigrationsService { /// Returns true if all pending migration data has been processed and cleanup can proceed var canCleanupAfterMigration: Bool { // Don't cleanup if there's still pending Blocktank data that needs retry - if pendingBlocktankOrderIds != nil || pendingRemotePaidOrders != nil { + if pendingBlocktankOrderIds != nil { Logger.debug("Cannot cleanup: pending Blocktank data exists", context: "Migration") return false } @@ -856,45 +846,6 @@ extension MigrationsService { return true } - /// One-time cleanup for transfers created from unpaid/expired Blocktank orders during migration. - /// The RN backup's paidOrders map could contain orders that were never actually paid. - func cleanupInvalidMigrationTransfers() async { - guard !userDefaults.bool(forKey: Self.didCleanupInvalidTransfersKey), rnMigrationCompleted, - let transfers = try? TransferStorage.shared.getActiveTransfers() else { return } - let orderTransfers = transfers.filter { $0.type.isToSpending() && $0.lspOrderId != nil } - - guard !orderTransfers.isEmpty else { - userDefaults.set(true, forKey: Self.didCleanupInvalidTransfersKey) - return - } - - let orderIds = orderTransfers.compactMap(\.lspOrderId) - - guard let orders = try? await CoreService.shared.blocktank.orders(orderIds: orderIds, filter: nil, refresh: true) else { - // Don't mark as done if we can't reach Blocktank — retry next launch - Logger.warn("Cannot cleanup migration transfers: Blocktank unreachable", context: "Migration") - return - } - - let now = UInt64(Date().timeIntervalSince1970) - for transfer in orderTransfers { - guard let orderId = transfer.lspOrderId, - let order = orders.first(where: { $0.id == orderId }) - else { continue } - - if order.state2 != .paid { - try? TransferStorage.shared.markSettled(id: transfer.id, settledAt: now) - Logger.info( - "Cleanup: settled invalid migration transfer \(transfer.id) for order \(orderId) (state: \(String(describing: order.state2)))", - context: "Migration" - ) - } - } - - userDefaults.set(true, forKey: Self.didCleanupInvalidTransfersKey) - Logger.info("Migration transfer cleanup completed", context: "Migration") - } - /// Clears all persisted pending migration data from UserDefaults private func clearPendingMigrationData() { retrySchedule = nil @@ -903,7 +854,6 @@ extension MigrationsService { pendingRemoteTransfers = nil pendingRemoteBoosts = nil pendingMetadata = nil - pendingRemotePaidOrders = nil pendingBlocktankOrderIds = nil } } @@ -1355,12 +1305,6 @@ extension MigrationsService { if let seen = settings.shopIntroSeen { defaults.set(seen, forKey: "hasSeenShopIntro") } - if let seen = settings.transferIntroSeen { - defaults.set(seen, forKey: "hasSeenTransferIntro") - } - if let seen = settings.spendingIntroSeen { - defaults.set(seen, forKey: "hasSeenTransferToSpendingIntro") - } if let seen = settings.savingsIntroSeen { defaults.set(seen, forKey: "hasSeenTransferToSavingsIntro") } @@ -1558,17 +1502,10 @@ extension MigrationsService { try await CoreService.shared.blocktank.upsertOrdersList(fetchedOrders) Logger.info("Upserted \(fetchedOrders.count) Blocktank orders", context: "Migration") } - - if !paidOrders.isEmpty { - await createTransfersForPaidOrders(paidOrdersMap: paidOrders, orders: fetchedOrders) - } } catch { Logger.warn("Failed to fetch and upsert Blocktank orders: \(error)", context: "Migration") // Store order IDs for retry after sync completes (when online) pendingBlocktankOrderIds = allOrderIds - if !paidOrders.isEmpty { - pendingRemotePaidOrders = paidOrders - } Logger.info("Stored \(allOrderIds.count) Blocktank order IDs for retry", context: "Migration") } } @@ -1714,7 +1651,6 @@ extension MigrationsService { guard isRetryCurrent(generation) else { return } // Handle pending Blocktank orders that couldn't be fetched during migration (offline) - var blocktankFetchFailed = false if let orderIds = pendingBlocktankOrderIds, !orderIds.isEmpty { Logger.info("Retrying \(orderIds.count) pending Blocktank orders", context: "Migration") do { @@ -1724,31 +1660,12 @@ extension MigrationsService { try await CoreService.shared.blocktank.upsertOrdersList(fetchedOrders) guard isRetryCurrent(generation) else { return } Logger.info("Upserted \(fetchedOrders.count) Blocktank orders after retry", context: "Migration") - - // Also create transfers for paid orders using the fetched orders - if let paidOrders = pendingRemotePaidOrders, !paidOrders.isEmpty { - Logger.info("Creating transfers for \(paidOrders.count) paid orders", context: "Migration") - await createTransfersForPaidOrders(paidOrdersMap: paidOrders, orders: fetchedOrders) - guard isRetryCurrent(generation) else { return } - pendingRemotePaidOrders = nil - } } pendingBlocktankOrderIds = nil } catch { Logger.warn("Still unable to fetch Blocktank orders: \(error)", context: "Migration") - blocktankFetchFailed = true } } - - // Handle remote backup paid orders (create transfers for pending channel orders) - // This also handles paid orders from RN remote backup restore - // Skip if Blocktank fetch failed - paid orders depend on Blocktank order data - if !blocktankFetchFailed, let paidOrders = pendingRemotePaidOrders { - Logger.info("Applying \(paidOrders.count) remote paid orders", context: "Migration") - await applyRemotePaidOrders(paidOrders) - guard isRetryCurrent(generation) else { return } - pendingRemotePaidOrders = nil - } } @MainActor @@ -1786,22 +1703,6 @@ extension MigrationsService { return remaining } - private func applyRemotePaidOrders(_ paidOrders: [String: String]) async { - let orderIds = Array(paidOrders.keys) - guard !orderIds.isEmpty else { return } - - do { - let fetchedOrders = try await CoreService.shared.blocktank.orders(orderIds: orderIds, filter: nil, refresh: true) - if !fetchedOrders.isEmpty { - try await CoreService.shared.blocktank.upsertOrdersList(fetchedOrders) - Logger.info("Upserted \(fetchedOrders.count) Blocktank orders from remote backup", context: "Migration") - } - await createTransfersForPaidOrders(paidOrdersMap: paidOrders, orders: fetchedOrders) - } catch { - Logger.warn("Failed to fetch Blocktank orders: \(error)", context: "Migration") - } - } - @MainActor func applyBoostTransactions( _ boosts: [String: String], @@ -2328,8 +2229,7 @@ extension MigrationsService { return [] } - let trustedIds = Set(Env.trustedLnPeers.map(\.nodeId)) - let uris = peers.filter { !trustedIds.contains($0.pubKey) }.map { "\($0.pubKey)@\($0.address):\($0.port)" } + let uris = peers.map { "\($0.pubKey)@\($0.address):\($0.port)" } Logger.info("Migration peer recovery: fetched \(uris.count) peer(s) from remote backup", context: "Migration") return uris } catch { @@ -2538,59 +2438,17 @@ extension MigrationsService { } // paidOrders is a map of orderId -> txId - var paidOrdersMap: [String: String] = [:] if let rawDict = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let dataDict = rawDict["data"] as? [String: Any], let paidOrders = dataDict["paidOrders"] as? [String: String] { - paidOrdersMap = paidOrders orderIds.append(contentsOf: paidOrders.keys) Logger.info("Found \(paidOrders.count) paid orders in blocktank backup", context: "Migration") } - // Store paid orders for processing after wallet starts (CoreService not ready yet during restore) - if !paidOrdersMap.isEmpty { - pendingRemotePaidOrders = paidOrdersMap - } - } - - private func createTransfersForPaidOrders(paidOrdersMap: [String: String], orders: [IBtOrder]) async { - let now = UInt64(Date().timeIntervalSince1970) - var transfers: [Transfer] = [] - - for (orderId, txId) in paidOrdersMap { - guard let order = orders.first(where: { $0.id == orderId }) else { - Logger.warn("Paid order \(orderId) not found in fetched orders", context: "Migration") - continue - } - - // Only create transfers for orders actually paid and awaiting channel - guard order.state2 == .paid else { - Logger.debug("Skipping order \(orderId) with state \(String(describing: order.state2)) for transfer creation", context: "Migration") - continue - } - - let transfer = Transfer( - id: txId, - type: .toSpending, - amountSats: order.clientBalanceSat, - channelId: nil, - fundingTxId: nil, - lspOrderId: orderId, - isSettled: false, - createdAt: now, - settledAt: nil - ) - transfers.append(transfer) - } - - if !transfers.isEmpty { - do { - try TransferStorage.shared.upsertList(transfers) - Logger.info("Created \(transfers.count) transfers for paid Blocktank orders", context: "Migration") - } catch { - Logger.error("Failed to create transfers for paid orders: \(error)", context: "Migration") - } + // Fetch the orders after the wallet starts (CoreService is not ready yet during restore), so they stay readable. + if !orderIds.isEmpty { + pendingBlocktankOrderIds = Array(Set((pendingBlocktankOrderIds ?? []) + orderIds)) } } } diff --git a/Bitkit/Services/OnchainSendAttemptService.swift b/Bitkit/Services/OnchainSendAttemptService.swift index eefaac4f3..f222a4d85 100644 --- a/Bitkit/Services/OnchainSendAttemptService.swift +++ b/Bitkit/Services/OnchainSendAttemptService.swift @@ -316,8 +316,13 @@ struct OnchainSendAttempt: Codable, Equatable { /// A Paykit payment from the node wallet that the network never confirmed accepting. Once its request /// has ended and no candidate reached the network, it can be released instead of blocking every send. + /// A transfer to spending that pays a Blocktank order, which Bitkit no longer buys. + var isBlocktankFunding: Bool { + requestId == nil && orderId != nil + } + var canBeAbandoned: Bool { - requestId != nil && orderId == nil && status != .accepted && txid != nil && + (requestId != nil && orderId == nil || isBlocktankFunding) && status != .accepted && txid != nil && walletId.hasPrefix("node:") && recoveryContext?.inputs.isEmpty == false } @@ -516,7 +521,6 @@ actor OnchainSendAttemptService { private let localFollowup: any OnchainSendLocalFollowupHandling private let winningFee: (OnchainSendAttempt) async throws -> UInt64? private let store: any OnchainSendAttemptStoring - private let hasPaidOrder: (String) throws -> Bool private var knownAttempt: OnchainSendAttempt? private var nativeDispatchInProgress: UUID? private var requestFollowupInProgress: UUID? @@ -525,15 +529,11 @@ actor OnchainSendAttemptService { init( store: any OnchainSendAttemptStoring = OnchainSendAttemptStore(), localFollowup: any OnchainSendLocalFollowupHandling = OnchainSendLocalFollowup(), - winningFee: @escaping (OnchainSendAttempt) async throws -> UInt64? = OnchainSendLocalFollowup.observedFee, - hasPaidOrder: @escaping (String) throws -> Bool = { orderId in - try TransferStorage.shared.getAll().contains(where: { $0.lspOrderId == orderId }) - } + winningFee: @escaping (OnchainSendAttempt) async throws -> UInt64? = OnchainSendLocalFollowup.observedFee ) { self.localFollowup = localFollowup self.winningFee = winningFee self.store = store - self.hasPaidOrder = hasPaidOrder } func backupSnapshot(wallet: PaykitPaymentStateBackup.ActiveOnchainAttempt.Wallet, @@ -605,9 +605,6 @@ actor OnchainSendAttemptService { { return .accepted(txid: txid) } - if let orderId, try hasPaidOrder(orderId) { - throw OnchainSendAttemptError.duplicate - } let attemptId = try admit( walletId: Self.walletId(index: walletIndex), requestId: requestId, @@ -883,9 +880,6 @@ actor OnchainSendAttemptService { !(orderId != nil && previous.orderId == orderId) else { throw OnchainSendAttemptError.duplicate } } - if let orderId, try hasPaidOrder(orderId) { - throw OnchainSendAttemptError.duplicate - } let attempt = OnchainSendAttempt( id: UUID(), walletId: walletId, @@ -1063,27 +1057,20 @@ actor OnchainSendAttemptService { } @discardableResult - func resumeAcceptedTransfer(walletId: String, using transferService: TransferService) async throws -> Bool { + func resumeAcceptedTransfer(walletId: String) async throws -> Bool { guard nativeDispatchInProgress == nil, let attempt = try currentAttempt(), attempt.walletId == walletId, attempt.status == .accepted, attempt.requestId == nil, attempt.orderId != nil, attempt.txid != nil else { return false } - return try await restoreAcceptedTransfer(attempt, using: transferService) + return try await restoreAcceptedTransfer(attempt) } - @discardableResult - func resumeAcceptedTransfer(orderId: String, txid: String, using transferService: TransferService) async throws -> Bool { - guard nativeDispatchInProgress == nil, let attempt = try currentAttempt(), attempt.orderId == orderId, - attempt.status == .accepted, attempt.requestId == nil, - attempt.txid?.caseInsensitiveCompare(txid) == .orderedSame - else { return false } - return try await restoreAcceptedTransfer(attempt, using: transferService) - } - - private func restoreAcceptedTransfer(_ attempt: OnchainSendAttempt, using transferService: TransferService) async throws -> Bool { - guard let savedOrderId = attempt.orderId, let txid = attempt.txid else { return false } + /// Blocktank no longer opens channels for paid orders, so an accepted funding keeps only its sent activity: no + /// transfer record is created, and the attempt resolves so later sends are not blocked. + private func restoreAcceptedTransfer(_ attempt: OnchainSendAttempt) async throws -> Bool { + guard attempt.orderId != nil, let txid = attempt.txid else { return false } guard !attempt.localFollowupComplete else { return true } let attempt = try await winningFollowupAttempt(attempt) - guard let context = attempt.followupContext, let transfer = attempt.transferContext else { + guard let context = attempt.followupContext else { throw OnchainSendAttemptError.localFollowupNotSaved } try await CoreService.shared.activity.upsertPreActivityMetadata([BitkitCore.PreActivityMetadata( @@ -1091,12 +1078,6 @@ actor OnchainSendAttemptService { txId: txid, address: attempt.address, isReceive: false, feeRate: UInt64(context.feeRate), isTransfer: true, channelId: nil, createdAt: context.createdAt )]) - let isSuccessor = attempt.recoveryContext.map { $0.candidateTxids.first?.caseInsensitiveCompare(txid) != .orderedSame } ?? false - _ = try await transferService.createTransfer( - type: .toSpending, amountSats: transfer.clientBalanceSats, fundingTxId: txid, - lspOrderId: savedOrderId, txTotalSats: isSuccessor ? attempt.amountSats + context.feeSats : transfer.txTotalSats, - preTransferOnchainSats: transfer.preTransferOnchainSats - ) guard await CoreService.shared.activity.createSentOnchainActivityFromSendResult( txid: txid, address: attempt.address, amount: attempt.amountSats, fee: context.feeSats, feeRate: context.feeRate, isTransfer: true, @@ -1112,12 +1093,12 @@ actor OnchainSendAttemptService { return true } - func resolvedAcceptedTransfer(context: OnchainSendPendingContext, using transferService: TransferService) async throws + func resolvedAcceptedTransfer(context: OnchainSendPendingContext) async throws -> OnchainSendLocalResolution? { guard let attempt = try pendingAttempt(context: context), attempt.orderId != nil, attempt.requestId == nil, attempt.status == .accepted, let txid = attempt.txid, - try await restoreAcceptedTransfer(attempt, using: transferService), + try await restoreAcceptedTransfer(attempt), let activity = try await CoreService.shared.activity.getOnchainActivityByTxId(txid: txid), activity.txType == .sent, activity.isTransfer else { return nil } @@ -1197,12 +1178,6 @@ actor OnchainSendAttemptService { return .init(attemptId: attempt.id, walletId: attempt.walletId, txid: txid ?? attempt.txid) } - func orderPendingContext(orderId: String) throws -> OnchainSendPendingContext? { - guard let attempt = try currentAttemptForPending(), attempt.requestId == nil, - attempt.orderId == orderId, attempt.blocksNewSend else { return nil } - return .init(attemptId: attempt.id, walletId: attempt.walletId, txid: attempt.txid) - } - func pendingAttempt(context: OnchainSendPendingContext) throws -> OnchainSendAttempt? { try currentAttempt().flatMap { $0.id == context.attemptId && $0.walletId == context.walletId && $0.containsCandidate(context.txid) ? $0 : nil diff --git a/Bitkit/Services/PaykitPaymentProofService.swift b/Bitkit/Services/PaykitPaymentProofService.swift index 5163999ff..24f23e80c 100644 --- a/Bitkit/Services/PaykitPaymentProofService.swift +++ b/Bitkit/Services/PaykitPaymentProofService.swift @@ -557,6 +557,10 @@ actor PaykitPaymentProofService { /// Whether the request behind an unresolved payment can no longer be paid: the payee ended it, or its deadline /// passed. A record that is missing, carries a proof or needs recovery never counts as ended. func onchainRequestHasEnded(_ attempt: OnchainSendAttempt, now: Date = Date()) async throws -> Bool { + // Blocktank no longer sells channels, so an unpaid order has ended. + if attempt.isBlocktankFunding { + return attempt.canBeAbandoned + } guard attempt.canBeAbandoned, let requestId = attempt.requestId, let payer = attempt.recoveryContext?.paymentIdentity else { return false } try await requireRecoveryPayer(payer) @@ -661,6 +665,15 @@ actor PaykitPaymentProofService { /// Removes the started proof, then the attempt, so a crash in between leaves an attempt a later cancel finishes. private func finishRelease(_ attempt: OnchainSendAttempt, context: OnchainSendPendingContext) async throws { + if attempt.isBlocktankFunding { + // A Blocktank funding has no Shop proof to remove. + try await mutationLock.withLock { + guard try await attemptService.pendingAttempt(context: context) == attempt else { throw OnchainSendAttemptError.unresolved } + try await attemptService.abandonUnbroadcast(attempt) + } + logInfo("Released Blocktank funding \(attempt.txid ?? "") through \(attempt.releaseTxid ?? "")") + return + } guard let requestId = attempt.requestId, let payer = attempt.recoveryContext?.paymentIdentity else { throw OnchainSendAttemptError.unresolved } diff --git a/Bitkit/Services/TransferService.swift b/Bitkit/Services/TransferService.swift index 46e83b734..d75cd7f0d 100644 --- a/Bitkit/Services/TransferService.swift +++ b/Bitkit/Services/TransferService.swift @@ -4,24 +4,19 @@ import LDKNode /// Service for managing transfer operations class TransferService { + /// `TransferStorage.insert` rewrites the whole list, so concurrent creations must not interleave. private static let creationLock = NSLock() private let storage: TransferStorage private let lightningService: LightningService - private let blocktankService: BlocktankService - private let isGeoBlocked: () -> Bool private let coreService: CoreService init( storage: TransferStorage = TransferStorage.shared, lightningService: LightningService, - blocktankService: BlocktankService, - coreService: CoreService = .shared, - isGeoBlocked: @escaping () -> Bool = { GeoService.shared.isGeoBlocked } + coreService: CoreService = .shared ) { - self.isGeoBlocked = isGeoBlocked self.storage = storage self.lightningService = lightningService - self.blocktankService = blocktankService self.coreService = coreService } @@ -37,38 +32,10 @@ class TransferService { amountSats: UInt64, channelId: String? = nil, fundingTxId: String? = nil, - lspOrderId: String? = nil, claimableAtHeight: UInt32? = nil, txTotalSats: UInt64? = nil, preTransferOnchainSats: UInt64? = nil ) async throws -> String { - try createTransferRecord(type: type, amountSats: amountSats, channelId: channelId, - fundingTxId: fundingTxId, lspOrderId: lspOrderId, claimableAtHeight: claimableAtHeight, - txTotalSats: txTotalSats, preTransferOnchainSats: preTransferOnchainSats) - } - - private func createTransferRecord( - type: TransferType, amountSats: UInt64, channelId: String?, fundingTxId: String?, - lspOrderId: String?, claimableAtHeight: UInt32?, txTotalSats: UInt64?, preTransferOnchainSats: UInt64? - ) throws -> String { - // Lookup and insert belong to one original paid-order operation, including across service instances. - Self.creationLock.lock() - defer { Self.creationLock.unlock() } - if let lspOrderId, let existing = try storage.getAll().first(where: { $0.lspOrderId == lspOrderId }) { - guard existing.fundingTxId == fundingTxId else { throw OnchainSendAttemptError.duplicate } - return existing.id - } - // When geoblocked, block transfers to spending that involve LSP (Blocktank) - // toSpending with lspOrderId means it's a Blocktank LSP channel order - let isGeoblocked = 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) @@ -78,7 +45,6 @@ class TransferService { amountSats: amountSats, channelId: channelId, fundingTxId: fundingTxId, - lspOrderId: lspOrderId, isSettled: false, createdAt: createdAt, settledAt: nil, @@ -87,38 +53,12 @@ class TransferService { preTransferOnchainSats: preTransferOnchainSats ) - try storage.insert(transfer) + try Self.creationLock.withLock { try storage.insert(transfer) } Logger.info("Created transfer: id=\(id) type=\(type) channelId=\(channelId ?? "nil")", context: "TransferService") return id } - /// Create the pending on-chain activity for a hardware-wallet transfer to spending. The funding - /// tx is broadcast externally (on-device signing), so LDK's own activity sync won't surface it; - /// this makes it appear immediately, marked as a transfer and scoped to the hardware wallet that - /// funded it, so it reads as one hardware-owned transfer row. - func createPendingToSpendingActivity( - order: IBtOrder, - txId: String, - fee: UInt64, - feeRate: UInt64, - walletId: String = WalletScope.default - ) async { - guard let address = order.payment?.onchain?.address, !address.isEmpty else { - Logger.error("Order '\(order.id)' has no on-chain payment address", context: "TransferService") - return - } - await coreService.activity.createSentOnchainActivityFromSendResult( - txid: txId, - address: address, - amount: order.feeSat, - fee: fee, - feeRate: UInt32(feeRate), - isTransfer: true, - walletId: walletId - ) - } - /// Mark a transfer as settled func markSettled(id: String) async throws { let settledAt = UInt64(Date().timeIntervalSince1970) @@ -150,27 +90,12 @@ class TransferService { let toSpending = activeTransfers.filter { $0.type.isToSpending() } for transfer in toSpending { - if let channelId = try await resolveChannelId(for: transfer, channels: channels) { - // Update transfer with channelId if not set yet - if transfer.channelId == nil { - let updatedTransfer = Transfer( - id: transfer.id, - type: transfer.type, - amountSats: transfer.amountSats, - channelId: channelId, - fundingTxId: transfer.fundingTxId, - lspOrderId: transfer.lspOrderId, - isSettled: transfer.isSettled, - createdAt: transfer.createdAt, - settledAt: transfer.settledAt, - claimableAtHeight: transfer.claimableAtHeight, - txTotalSats: transfer.txTotalSats, - preTransferOnchainSats: transfer.preTransferOnchainSats - ) - try storage.update(updatedTransfer) - Logger.debug("Updated transfer \(transfer.id) with channelId: \(channelId)", context: "TransferService") - } - + // Blocktank no longer opens channels for paid orders, so an order's transfer is no longer tracked. + if transfer.lspOrderId != nil { + try await markSettled(id: transfer.id) + continue + } + if let channelId = transfer.channelId { // Associate the funding tx's on-chain activity with the resolved channel so it reads // as a transfer (e.g. the hardware-wallet funding tx created before the channel opened). if let fundingTxId = transfer.fundingTxId { @@ -186,24 +111,6 @@ class TransferService { } else { Logger.debug("Channel \(channelId) exists but not yet usable for transfer: \(transfer.id)", context: "TransferService") } - } else { - // No channel ID resolved - check if order is expired/terminal - if let orderId = transfer.lspOrderId, let orders = try? await blocktankService.orders( - orderIds: [orderId], - filter: nil, - refresh: false - ), - let order = orders.first, - order.state2 == .expired - { - try await markSettled(id: transfer.id) - Logger.info("Order \(orderId) expired, settled transfer: \(transfer.id)", context: "TransferService") - continue - } - Logger.debug( - "Could not resolve channel for transfer: \(transfer.id) orderId: \(transfer.lspOrderId ?? "none")", - context: "TransferService" - ) } } @@ -211,7 +118,7 @@ class TransferService { let toSavings = activeTransfers.filter { $0.type.isToSavings() } for transfer in toSavings { - if let channelId = try await resolveChannelId(for: transfer, channels: channels) { + if let channelId = transfer.channelId { let hasBalance = balances?.lightningBalances.contains(where: { balance in balance.channelIdString == channelId }) ?? false @@ -278,44 +185,4 @@ class TransferService { } } } - - /// Resolve channel ID for a transfer - /// For LSP orders: match via order->fundingTx, for manual: use directly - func resolveChannelId(for transfer: Transfer, channels: [ChannelDetails]) async throws -> String? { - // If there's an LSP order ID, resolve via Blocktank - if let orderId = transfer.lspOrderId { - // Get orders from Blocktank (returns [IBtOrder]) - var orders: [IBtOrder]? = nil - - do { - orders = try await blocktankService.orders(orderIds: [orderId], filter: nil, refresh: false) - } catch { - Logger.error("Failed to fetch Blocktank orders for orderId \(orderId): \(error)", context: "TransferService") - // Fall back to the persisted channel id (same BOLT `channelId.description` format the - // success path returns) so a transient Blocktank failure doesn't drop a known channel - // and double-count the transfer in the balance calc. - return transfer.channelId - } - - if let order = orders?.first { - if let fundingTxId = order.channel?.fundingTx.id { - // Find channel matching the funding transaction - if let channel = channels.first(where: { channel in - channel.fundingTxo?.txid.description == fundingTxId - }) { - return channel.channelId.description - } else { - Logger.debug("Order \(orderId) has fundingTx \(fundingTxId) but no matching channel found", context: "TransferService") - } - } else { - Logger.debug("Order \(orderId) exists but has no fundingTx yet (state: \(order.state))", context: "TransferService") - } - } else { - Logger.debug("Order \(orderId) not found in Blocktank response", context: "TransferService") - } - } - - // Otherwise use the channel ID directly - return transfer.channelId - } } diff --git a/Bitkit/Utilities/DustChangeHelper.swift b/Bitkit/Utilities/DustChangeHelper.swift deleted file mode 100644 index de86abfac..000000000 --- a/Bitkit/Utilities/DustChangeHelper.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Foundation - -/// Helper for detecting dust change during on-chain sends. -enum DustChangeHelper { - /// Returns true if the expected change would be dust (below dust limit). - /// - Parameters: - /// - totalInput: Total sats from selected UTXOs (or spendable balance) - /// - amountSats: Amount to send to recipient - /// - normalFee: Fee for a normal send (recipient + change outputs) - /// - dustLimit: Minimum non-dust amount (default: Env.dustLimit) - /// - Returns: true when change would be dust - static func wouldCreateDustChange( - totalInput: UInt64, - amountSats: UInt64, - normalFee: UInt64, - dustLimit: UInt64 = UInt64(Env.dustLimit) - ) -> Bool { - let expectedChange = Int64(totalInput) - Int64(amountSats) - Int64(normalFee) - return expectedChange >= 0 && expectedChange < Int64(dustLimit) - } - - /// Returns true only when the caller is allowed to use sendAll to avoid dust change. - static func shouldUseSendAllToAvoidDust( - totalInput: UInt64, - amountSats: UInt64, - normalFee: UInt64, - isMaxAmount: Bool, - dustLimit: UInt64 = UInt64(Env.dustLimit) - ) -> Bool { - isMaxAmount && wouldCreateDustChange( - totalInput: totalInput, - amountSats: amountSats, - normalFee: normalFee, - dustLimit: dustLimit - ) - } -} diff --git a/Bitkit/Utilities/Errors.swift b/Bitkit/Utilities/Errors.swift index 216acb575..0b71a8d85 100644 --- a/Bitkit/Utilities/Errors.swift +++ b/Bitkit/Utilities/Errors.swift @@ -11,9 +11,6 @@ enum CustomServiceError: LocalizedError { case onchainWalletStillRunning case invalidNodeSigningMessage case regtestOnlyMethod - case channelSizeExceedsMaximum - case cjitNodeCapacityExceeded - case invalidCjitQuote case currencyRateUnavailable var errorDescription: String? { @@ -36,12 +33,6 @@ enum CustomServiceError: LocalizedError { return "Invalid node signing message" case .regtestOnlyMethod: return "Method only available in regtest environment" - case .channelSizeExceedsMaximum: - return "Channel size exceeds maximum allowed size" - case .cjitNodeCapacityExceeded: - return "Additional spending capacity is unavailable right now." - case .invalidCjitQuote: - return NSLocalizedString("wallet__receive_liquidity__invalid_quote", comment: "") case .currencyRateUnavailable: return "Currency rate unavailable" } @@ -75,32 +66,6 @@ enum PaymentTimeoutError: Error { case timedOut } -extension Error { - var isChannelSizeExceedsMaximum: Bool { - if let serviceError = self as? CustomServiceError { - return serviceError == .channelSizeExceedsMaximum - } - - if let appError = self as? AppError, let underlyingError = appError.underlyingError { - return underlyingError.isChannelSizeExceedsMaximum - } - - return false - } - - var isCjitNodeCapacityExceeded: Bool { - if let serviceError = self as? CustomServiceError { - return serviceError == .cjitNodeCapacityExceeded - } - - if let appError = self as? AppError, let underlyingError = appError.underlyingError { - return underlyingError.isCjitNodeCapacityExceeded - } - - return false - } -} - /// Translates LDK and BDK error messages into translated messages that can be displayed to end users struct AppError: LocalizedError { static let genericMessage = "App Error" @@ -194,15 +159,6 @@ struct AppError: LocalizedError { case .regtestOnlyMethod: message = "Method only available in regtest environment" debugMessage = nil - case .channelSizeExceedsMaximum: - message = "Channel size exceeds maximum allowed size" - debugMessage = nil - case .cjitNodeCapacityExceeded: - message = "Additional spending capacity is unavailable right now." - debugMessage = nil - case .invalidCjitQuote: - message = "wallet__receive_liquidity__invalid_quote" - debugMessage = nil case .currencyRateUnavailable: message = "Currency rate unavailable" debugMessage = nil diff --git a/Bitkit/Utilities/SpendingConfirmTotal.swift b/Bitkit/Utilities/SpendingConfirmTotal.swift deleted file mode 100644 index e81e682ed..000000000 --- a/Bitkit/Utilities/SpendingConfirmTotal.swift +++ /dev/null @@ -1,41 +0,0 @@ -import Foundation - -/// Confirm total for the spending transfer screen: the amount of bitcoin that leaves the wallet. -enum SpendingConfirmTotal { - /// When send-all funds the order, the sweep spends `maxSendable + networkFee` (the spendable balance). - /// Otherwise the total is the order fee plus the miner fee. - static func leavingAmount( - orderFeeSat: UInt64, - networkFeeSat: UInt64, - shouldUseSendAll: Bool, - maxSendable: UInt64? - ) -> UInt64 { - if shouldUseSendAll, let maxSendable { - return maxSendable.saturatingAdd(networkFeeSat) - } - return orderFeeSat.saturatingAdd(networkFeeSat) - } -} - -/// The network fee and total the spending confirm screen shows. -struct SpendingConfirmAmounts: Equatable { - let networkFeeSat: UInt64 - let totalSat: UInt64 -} - -enum SpendingFeeIncrease: Equatable { - case service(amountSat: UInt64) - case network(amountSat: UInt64) - - /// The localized toast description for this increase, with `amount` already formatted in the user's primary display unit. - func toastDescription(formattingAmount format: (UInt64) -> String) -> String { - switch self { - case let .service(amountSat): - t("lightning__spending_confirm__fees_changed_service", variables: ["amount": format(amountSat)]) - case let .network(amountSat): - t("lightning__spending_confirm__fees_changed_network", variables: ["amount": format(amountSat)]) - } - } -} - -struct SpendingFeesIncreasedError: Error {} diff --git a/Bitkit/ViewModels/ActivityListViewModel.swift b/Bitkit/ViewModels/ActivityListViewModel.swift index 9fe8ccbd9..1a303bb6c 100644 --- a/Bitkit/ViewModels/ActivityListViewModel.swift +++ b/Bitkit/ViewModels/ActivityListViewModel.swift @@ -128,8 +128,7 @@ class ActivityListViewModel: ObservableObject { lightningService: LightningService = .shared ) { let transferService = TransferService( - lightningService: lightningService, - blocktankService: coreService.blocktank + lightningService: lightningService ) self.init( coreService: coreService, diff --git a/Bitkit/ViewModels/AppViewModel.swift b/Bitkit/ViewModels/AppViewModel.swift index d621a8f3a..65dfd0cf3 100644 --- a/Bitkit/ViewModels/AppViewModel.swift +++ b/Bitkit/ViewModels/AppViewModel.swift @@ -204,8 +204,6 @@ class AppViewModel: ObservableObject { @AppStorage("hasSeenNotificationsIntro") var hasSeenNotificationsIntro: Bool = false @AppStorage("hasSeenQuickpayIntro") var hasSeenQuickpayIntro: Bool = false @AppStorage("hasSeenShopIntro") var hasSeenShopIntro: Bool = false - @AppStorage("hasSeenTransferIntro") var hasSeenTransferIntro: Bool = false - @AppStorage("hasSeenTransferToSpendingIntro") var hasSeenTransferToSpendingIntro: Bool = false @AppStorage("hasSeenTransferToSavingsIntro") var hasSeenTransferToSavingsIntro: Bool = false @AppStorage("hasSeenWidgetsIntro") var hasSeenWidgetsIntro: Bool = false @@ -413,7 +411,6 @@ class AppViewModel: ObservableObject { setupManualEntryValidationDebounce() Task { - await checkGeoStatus() // Check for app updates on startup await AppUpdateService.shared.checkForAppUpdate() } @@ -552,18 +549,12 @@ class AppViewModel: ObservableObject { deinit {} - func checkGeoStatus() async { - // Delegate to GeoService singleton for centralized geo-blocking management - await GeoService.shared.checkGeoStatus() - } - func wipe() async throws { hasSeenContactsIntro = false hasSeenProfileIntro = false hasSeenNotificationsIntro = false hasSeenQuickpayIntro = false hasSeenShopIntro = false - hasSeenTransferToSpendingIntro = false hasSeenTransferToSavingsIntro = false hasSeenWidgetsIntro = false hasDismissedWidgetsOnboardingHint = false @@ -1124,8 +1115,13 @@ extension AppViewModel { return } await handlePubkyAuthApproval(sourceURI) - case let .gift(code, amount): - sheetViewModel.showSheet(.gift, data: GiftConfig(code: code, amount: Int(amount))) + case .gift: + toast( + type: .error, + title: t("other__scan_err_decoding"), + description: t("other__scan__error__generic"), + accessibilityIdentifier: "InvalidAddressToast" + ) default: Logger.warn("Unhandled invoice type: \(data)") toast(type: .error, title: "Unsupported", description: "This type of invoice is not supported yet") diff --git a/Bitkit/ViewModels/BlocktankViewModel.swift b/Bitkit/ViewModels/BlocktankViewModel.swift index 330e66cdf..3dc238235 100644 --- a/Bitkit/ViewModels/BlocktankViewModel.swift +++ b/Bitkit/ViewModels/BlocktankViewModel.swift @@ -1,90 +1,26 @@ import BitkitCore import SwiftUI -struct BlocktankOrderClient { - typealias Submit = (UInt64, UInt32, CreateOrderOptions) async throws -> IBtOrder - typealias Estimate = (UInt64, UInt32, CreateOrderOptions) async throws -> IBtEstimateFeeResponse2 - - let nodeId: () -> String? - let sign: (String) async throws -> String - let submit: Submit - let estimate: Estimate - - init(coreService: CoreService, lightningService: LightningService) { - nodeId = { lightningService.nodeId } - sign = { try await lightningService.sign(message: $0) } - submit = { lspBalanceSat, channelExpiryWeeks, options in - try await coreService.blocktank.newOrder( - lspBalanceSat: lspBalanceSat, - channelExpiryWeeks: channelExpiryWeeks, - options: options - ) - } - estimate = { lspBalanceSat, channelExpiryWeeks, options in - try await coreService.blocktank.estimateFee( - lspBalanceSat: lspBalanceSat, - channelExpiryWeeks: channelExpiryWeeks, - options: options - ) - } - } - - init( - nodeId: @escaping () -> String?, - sign: @escaping (String) async throws -> String, - submit: @escaping Submit, - estimate: @escaping Estimate - ) { - self.nodeId = nodeId - self.sign = sign - self.submit = submit - self.estimate = estimate - } -} - @MainActor class BlocktankViewModel: ObservableObject { @Published var orders: [IBtOrder]? = nil @Published var cJitEntries: [IcJitEntry]? = nil @Published var info: IBtInfo? = nil - /// Use -1 as a sentinel value to represent nil - @AppStorage("minCjitSats") private var minCjitSatsStorage: Int = -1 - - var minCjitSats: UInt64? { - get { minCjitSatsStorage == -1 ? nil : UInt64(minCjitSatsStorage) } - set { minCjitSatsStorage = newValue == nil ? -1 : Int(newValue!) } - } - - private let defaultChannelExpiryWeeks: UInt32 = 6 - private let defaultSource = "bitkit-ios" - @Published private(set) var isRefreshing = false private let coreService: CoreService private let lightningService: LightningService - private let currencyService: CurrencyService - private let orderClient: BlocktankOrderClient - private let refundAddressProvider: any BlocktankRefundAddressProviding private var refreshTimer: Timer? private var refreshTask: Task? init( coreService: CoreService = .shared, lightningService: LightningService = .shared, - currencyService: CurrencyService = .shared, - orderClient: BlocktankOrderClient? = nil, - refundAddressProvider: (any BlocktankRefundAddressProviding)? = nil, startPolling: Bool = true ) { self.coreService = coreService self.lightningService = lightningService - self.currencyService = currencyService - self.orderClient = orderClient ?? BlocktankOrderClient(coreService: coreService, lightningService: lightningService) - self.refundAddressProvider = refundAddressProvider ?? BlocktankRefundAddressProvider( - lightningService: lightningService, - utilityService: coreService.utility - ) if startPolling { Task { try? await refreshInfo() } @@ -165,328 +101,4 @@ class BlocktankViewModel: ObservableObject { coreService.blocktank.notifyStateChanged() return refreshedOrder } - - func createCjit(amountSats: UInt64, description: String) async throws -> IcJitEntry { - guard let nodeId = lightningService.nodeId else { - throw CustomServiceError.nodeNotStarted - } - - let maxChannelSizeSat = await freshMaxChannelSizeSat() - let lspBalance = try await getDefaultLspBalance(clientBalance: amountSats) - guard amountSats <= UInt64.max - lspBalance else { - throw CustomServiceError.channelSizeExceedsMaximum - } - - let channelSizeSat = amountSats + lspBalance - - if let maxChannelSizeSat, channelSizeSat > maxChannelSizeSat { - Logger.error("CJIT channel size exceeds maximum: \(channelSizeSat) > \(maxChannelSizeSat)") - throw CustomServiceError.channelSizeExceedsMaximum - } - - do { - let entry = try await coreService.blocktank.createCjit( - channelSizeSat: channelSizeSat, - invoiceSat: amountSats, - invoiceDescription: description, - nodeId: nodeId, - channelExpiryWeeks: defaultChannelExpiryWeeks, - options: .init(source: defaultSource, discountCode: nil) - ) - try Self.validateCjitEntry(entry, receiveAmountSats: amountSats) - return entry - } catch { - throw Self.normalizedCreateCjitError(error) - } - } - - nonisolated static func validateCjitEntry(_ entry: IcJitEntry, receiveAmountSats: UInt64) throws { - guard entry.feeSat < receiveAmountSats else { - throw CustomServiceError.invalidCjitQuote - } - - let userBalanceSats = receiveAmountSats - entry.feeSat - guard entry.channelSizeSat >= userBalanceSats else { - throw CustomServiceError.invalidCjitQuote - } - } - - nonisolated static func normalizedCreateCjitError(_ error: Error) -> Error { - if isNodeCapacityError(error) { - return CustomServiceError.cjitNodeCapacityExceeded - } - - if isMaxChannelSizeError(error) { - return CustomServiceError.channelSizeExceedsMaximum - } - - return error - } - - private func canCreateCjit(amountSats: UInt64, maxChannelSizeSat: UInt64) async throws -> Bool { - let lspBalance = try await getDefaultLspBalance(clientBalance: amountSats) - guard amountSats <= maxChannelSizeSat else { - return false - } - - return lspBalance <= maxChannelSizeSat - amountSats - } - - func maxCjitAmountSats() async throws -> UInt64? { - guard let maxChannelSizeSat = await freshMaxChannelSizeSat() else { - return nil - } - - var lowerBound: UInt64 = 0 - var upperBound = maxChannelSizeSat - - while lowerBound < upperBound { - let candidate = lowerBound + (upperBound - lowerBound + 1) / 2 - if try await canCreateCjit(amountSats: candidate, maxChannelSizeSat: maxChannelSizeSat) { - lowerBound = candidate - } else { - upperBound = candidate - 1 - } - } - - return lowerBound - } - - private func freshMaxChannelSizeSat() async -> UInt64? { - do { - try await refreshInfo() - } catch { - Logger.warn("Failed to refresh Blocktank info before CJIT max check; using cached info: \(error)") - } - - guard let maxChannelSizeSat = info?.options.maxChannelSizeSat, maxChannelSizeSat > 0 else { - return nil - } - - return maxChannelSizeSat - } - - private nonisolated static func isNodeCapacityError(_ error: Error) -> Bool { - if error.isCjitNodeCapacityExceeded { - return true - } - - return errorDescriptionCandidates(error).contains { - $0.localizedCaseInsensitiveContains("capacity is above our capacity limit") - } - } - - private nonisolated static func isMaxChannelSizeError(_ error: Error) -> Bool { - if error.isChannelSizeExceedsMaximum { - return true - } - - return errorDescriptionCandidates(error).contains { - $0.localizedCaseInsensitiveContains("Channel size is too big") - || $0.localizedCaseInsensitiveContains("channelSizeExceedsMaximum") - || $0.localizedCaseInsensitiveContains("maxChannelSizeSat") - } - } - - private nonisolated static func errorDescriptionCandidates(_ error: Error) -> [String] { - var candidates = [ - String(describing: error), - String(reflecting: error), - error.localizedDescription, - ] - - if let appError = error as? AppError { - candidates.append(appError.message) - if let debugMessage = appError.debugMessage { - candidates.append(debugMessage) - } - if let underlyingError = appError.underlyingError { - candidates.append(String(describing: underlyingError)) - candidates.append(underlyingError.localizedDescription) - } - } - - appendMirroredErrorDescriptions(from: error, to: &candidates) - - return candidates - } - - private nonisolated static func appendMirroredErrorDescriptions(from value: Any, to candidates: inout [String]) { - for child in Mirror(reflecting: value).children { - appendMirroredErrorDescription(from: child.value, to: &candidates) - } - } - - private nonisolated static func appendMirroredErrorDescription(from value: Any, to candidates: inout [String]) { - if let string = value as? String { - candidates.append(string) - return - } - - if let error = value as? Error { - candidates.append(String(describing: error)) - candidates.append(error.localizedDescription) - return - } - - let mirror = Mirror(reflecting: value) - guard mirror.displayStyle == .optional, let wrappedValue = mirror.children.first?.value else { - return - } - - appendMirroredErrorDescription(from: wrappedValue, to: &candidates) - } - - func estimateFundingAmount(clientBalance: UInt64, lspBalance: UInt64? = nil) async throws -> UInt64 { - let receivingBalance = lspBalance ?? (clientBalance * 2) - try validateChannelSize(clientBalance: clientBalance, lspBalance: receivingBalance) - let estimate = try await estimateOrderFee(clientBalance: clientBalance, lspBalance: receivingBalance) - return clientBalance.saturatingAdd(estimate.networkFeeSat.saturatingAdd(estimate.serviceFeeSat)) - } - - func createOrder(clientBalance: UInt64, lspBalance: UInt64? = nil) async throws -> IBtOrder { - let finalReceivingBalanceSats = lspBalance ?? (clientBalance * 2) - try validateChannelSize(clientBalance: clientBalance, lspBalance: finalReceivingBalanceSats) - - guard orderClient.nodeId() != nil else { - throw CustomServiceError.nodeNotStarted - } - try Task.checkCancellation() - let refundAddress = try await refundAddressProvider.addressForOrder() - let options = try await defaultCreateOrderOptions( - clientBalanceSat: clientBalance, - refundOnchainAddress: refundAddress - ) - - Logger.debug( - "Buying channel with lspBalanceSat: \(finalReceivingBalanceSats), clientBalanceSat: \(clientBalance), expiryWeeks: \(defaultChannelExpiryWeeks)" - ) - - try Task.checkCancellation() - return try await orderClient.submit(finalReceivingBalanceSats, defaultChannelExpiryWeeks, options) - } - - private func validateChannelSize(clientBalance: UInt64, lspBalance: UInt64) throws { - guard let btBOptions = info?.options else { - Logger.warn("Has not refreshed Blocktank info yet, skipping validation of limits") - return - } - if (clientBalance + lspBalance) > btBOptions.maxChannelSizeSat { - Logger.error("Channel size exceeds maximum: \(clientBalance + lspBalance) > \(btBOptions.maxChannelSizeSat)") - throw CustomServiceError.channelSizeExceedsMaximum - } - } - - func openChannel(orderId: String) async throws -> IBtOrder { - let order = try await coreService.blocktank.open(orderId: orderId) - - // Update the order in the published array if it exists - if let index = orders?.firstIndex(where: { $0.id == orderId }) { - orders?[index] = order - } - - coreService.blocktank.notifyStateChanged() - return order - } - - func estimateOrderFee(clientBalance: UInt64, lspBalance: UInt64) async throws -> ( - feeSat: UInt64, networkFeeSat: UInt64, serviceFeeSat: UInt64 - ) { - let options = try await defaultCreateOrderOptions(clientBalanceSat: clientBalance) - - let estimate = try await orderClient.estimate(lspBalance, defaultChannelExpiryWeeks, options) - - return ( - feeSat: estimate.feeSat, - networkFeeSat: estimate.networkFeeSat, - serviceFeeSat: estimate.serviceFeeSat - ) - } - - /// Creates default options for channel creation or fee estimation - private func defaultCreateOrderOptions( - clientBalanceSat: UInt64, - refundOnchainAddress: String? = nil - ) async throws -> CreateOrderOptions { - guard let nodeId = orderClient.nodeId() else { - throw CustomServiceError.nodeNotStarted - } - - let timestamp = Date().formatted(.iso8601) - let signature = try await orderClient.sign("channelOpen-\(timestamp)") - - return CreateOrderOptions( - clientBalanceSat: clientBalanceSat, - lspNodeId: nil, - couponCode: "", - source: defaultSource, - discountCode: nil, - zeroConf: true, - zeroConfPayment: false, - zeroReserve: true, - clientNodeId: nodeId, - signature: signature, - timestamp: timestamp, - refundOnchainAddress: refundOnchainAddress, - announceChannel: false - ) - } - - /// Calculates default LSP balance for CJIT channels using bitkit-core - private func getDefaultLspBalance(clientBalance: UInt64) async throws -> UInt64 { - if info == nil { - try await refreshInfo() - } - - guard let rates = currencyService.loadCachedRates(), - let eurRate = currencyService.getCurrentRate(for: "EUR", from: rates) - else { - Logger.error("Failed to get EUR rate for lspBalance calculation") - throw CustomServiceError.currencyRateUnavailable - } - - let satsPerEur = currencyService.convertFiatToSats(fiatValue: 1, rate: eurRate) - let params = DefaultLspBalanceParams( - clientBalanceSat: clientBalance, - maxChannelSizeSat: info?.options.maxChannelSizeSat ?? 0, - satsPerEur: satsPerEur - ) - - return BitkitCore.getDefaultLspBalance(params: params) - } - - func refreshMinCjitSats() async throws { - do { - let lspBalance = try await getDefaultLspBalance(clientBalance: 0) - - // Get fees and calculate minimum - let fees = try await estimateOrderFee(clientBalance: 0, lspBalance: lspBalance) - let minimum = UInt64(ceil(Double(fees.feeSat) * 1.1 / 1000) * 1000) - minCjitSats = minimum - Logger.debug("Updated minCjitSats to \(minimum)") - } catch { - Logger.error("Failed to refresh minCjitSats: \(error)") - throw error - } - } - - /// Checks for pending orders and notifies TransferViewModel to start watching them - /// This should be called on app startup to resume watching orders after app restart - func startWatchingPendingOrders(transferViewModel: TransferViewModel) async { - guard let orders else { return } - - let pendingOrders = orders.filter { order in - // Watch orders that are created or paid but not yet completed - order.state2 == .created || order.state2 == .paid - } - - if !pendingOrders.isEmpty { - Logger.info("Found \(pendingOrders.count) pending orders to watch: \(pendingOrders.map(\.id))") - - // Notify TransferViewModel to start watching each pending order - for order in pendingOrders { - await transferViewModel.startWatchingOrderFromRestart(order) - } - } - } } diff --git a/Bitkit/ViewModels/HwFundingSigner.swift b/Bitkit/ViewModels/HwFundingSigner.swift index b8fa7a4ea..1ae45d71c 100644 --- a/Bitkit/ViewModels/HwFundingSigner.swift +++ b/Bitkit/ViewModels/HwFundingSigner.swift @@ -71,26 +71,6 @@ struct HwFundingSigner { return balanceSats > reserve ? balanceSats - reserve : 0 } - /// Reconnects, composes and signs the funding transaction without broadcasting it. - /// `onConnectingDevice` brackets every reconnect, as in `prepareSignedPayment`. - func prepareSignedFunding( - order: IBtOrder, - walletId: String, - address: String, - onComposed: (HwFundingTransaction) -> Void = { _ in }, - onConnectingDevice: (Bool) -> Void = { _ in } - ) async throws -> HwFundingSignedTx { - let satsPerVByte = await resolvedSatsPerVByte() - return try await prepareSignedPayment( - walletId: walletId, - address: address, - sats: order.feeSat, - satsPerVByte: satsPerVByte, - onComposed: onComposed, - onConnectingDevice: onConnectingDevice - ) - } - /// Reconnects, composes and signs a normal on-chain payment without broadcasting it. /// `onConnectingDevice` brackets every reconnect (true before it, false once it returns or throws), /// including the one before a sign retry: the phases a caller may abandon because nothing is on the @@ -128,12 +108,6 @@ struct HwFundingSigner { ) } - /// Best-effort pre-connect of the device before signing (fire-and-forget). Delegates to the - /// device-session capability, which no-ops unless it's a known BLE device that isn't connected. - func warmUp(walletId: String) { - connecting.warmUpConnection(walletId: walletId) - } - /// Offline compose for the exact payment amount; does not require a connected device. func estimateOfflineFundingMiningFee(walletId: String, address: String, sats: UInt64) async throws -> UInt64 { let satsPerVByte = await resolvedSatsPerVByte() @@ -831,3 +805,87 @@ final class HwSendCoordinator { let connecting: HwTransferConnecting } } + +/// A recoverable failure surfaced by the hardware-wallet transfer flow. The Sign screen maps each +/// case to the matching localized toast. +enum HwTransferError: Error, Equatable { + /// Reconnect failed. `isBluetooth` selects the softer INFO toast for a known BLE device + /// (advertises intermittently — "check that it is unlocked and try again") vs the ERROR toast. + case reconnect(isBluetooth: Bool) + case signingTimeout + case broadcastUncertain + /// Signed tx is retained but Electrum/network is unreachable — retry broadcast later. + case broadcastConnectivity + /// The device is locked or otherwise busy before signing can start. Carries the vendor so the + /// toast can name the device. + case deviceBusy(HwWalletVendor) + /// Firmware error (code 99) — user must reconnect the device. + case firmwareReconnect + /// The entered passphrase opened a different wallet than the one being spent from. + case passphraseMismatch + /// The device holds another wallet than the one being spent from, so reconnecting cannot help. + case walletMismatch + case funding(String?) + case generic(String?) +} + +/// The hardware-wallet funding capability the transfer flow needs. Implemented by `HwWalletManager`; +/// declared as a protocol so the flow stays testable. +@MainActor +protocol HwTransferFunding: Sendable { + func getFundingAccount(walletId: String, addressType: AddressScriptType) throws -> HwFundingAccount + func maxSpendableFunding( + walletId: String, + destinationAddress: String, + satsPerVByte: UInt64, + addressType: AddressScriptType + ) async throws -> UInt64 + func composeFundingTransaction( + walletId: String, + address: String, + sats: UInt64, + satsPerVByte: UInt64, + addressType: AddressScriptType + ) async throws -> HwFundingTransaction + /// Offline coin-selection estimate for the exact funding amount (`fingerprint: nil`); fee only. + func estimateOfflineFundingMiningFee( + walletId: String, + address: String, + sats: UInt64, + satsPerVByte: UInt64, + addressType: AddressScriptType + ) async throws -> UInt64 + func signFunding(walletId: String, funding: HwFundingTransaction) async throws -> HwFundingSignedTx + func broadcastFunding( + serializedTx: String, + paymentDeadline: PaykitPreciseInstant?, + beforeDispatch: @escaping @MainActor @Sendable () async throws -> Void + ) async throws -> String +} + +/// The device-session capability the transfer flow needs for on-device signing, addressed by wallet +/// identity: a device holds one wallet open at a time, so reaching a given wallet is more than +/// reaching its transport. Implemented by `HwWalletManager`. +@MainActor +protocol HwTransferConnecting: Sendable { + func ensureConnected(walletId: String) async throws + /// How long `ensureConnected` may take for this wallet's device before the flow gives up. + func reconnectTimeout(walletId: String) -> Double + func disconnectStaleSession(walletId: String) async + func scheduleStaleSessionCleanup(walletId: String) + /// Whether the wallet is reachable over a known Bluetooth device, so a reconnect failure can show + /// the softer BLE "check that it is unlocked and try again" toast instead of the generic error. + func isKnownBluetoothDevice(walletId: String) -> Bool + /// Best-effort pre-connect when the sign screen appears, so tapping Open Trezor Connect is less + /// likely to hit a cold reconnect. Fire-and-forget. + func warmUpConnection(walletId: String) + /// Whether the wallet's passphrase has to be collected again before the device can sign for it. + func needsPassphrase(walletId: String) -> Bool + /// Reopens a hidden wallet for signing, refusing a session that resolves to a different wallet. + func reconnectWithPassphrase(walletId: String, passphrase: String) async throws +} + +// MARK: - Hardware transfer capability conformances + +extension HwWalletManager: HwTransferFunding {} +extension HwWalletManager: HwTransferConnecting {} diff --git a/Bitkit/ViewModels/NavigationViewModel.swift b/Bitkit/ViewModels/NavigationViewModel.swift index 5ee0826a3..54a6f2f3b 100644 --- a/Bitkit/ViewModels/NavigationViewModel.swift +++ b/Bitkit/ViewModels/NavigationViewModel.swift @@ -39,19 +39,7 @@ enum Route: Hashable { case subscriptions(showPayments: Bool) case paymentRequestDetail(PaykitPaymentRequest.ID) case subscriptionDetail(PaykitSubscription.ID) - case transferIntro case fundingOptions - case spendingIntro - case spendingIntroHw(walletId: String) - case spendingAmount - case spendingAmountHw(walletId: String) - case spendingHwSign(walletId: String) - case spendingHwSigned - case spendingConfirm - case spendingAdvanced(walletId: String? = nil) - case transferLearnMore - case settingUp - case fundingAdvanced case fundManual(nodeUri: String?) case fundManualAmount(lnPeer: LnPeer) case fundManualConfirm(lnPeer: LnPeer, amountSats: UInt64) diff --git a/Bitkit/ViewModels/SettingsViewModel.swift b/Bitkit/ViewModels/SettingsViewModel.swift index 8e431ec46..9e84a7e54 100644 --- a/Bitkit/ViewModels/SettingsViewModel.swift +++ b/Bitkit/ViewModels/SettingsViewModel.swift @@ -108,7 +108,6 @@ class SettingsViewModel: NSObject, ObservableObject { @AppStorage("quickpayAmount") var quickpayAmount: Double = 5 @AppStorage("quickpayDailyLimitMultiplier") var quickpayDailyLimitMultiplier: Double = 5 @AppStorage("enableNotifications") var enableNotifications: Bool = false - @AppStorage("enableNotificationsAmount") var enableNotificationsAmount: Bool = false @AppStorage("ignoresSwitchUnitToast") var ignoresSwitchUnitToast: Bool = false @AppStorage("ignoresHideBalanceToast") var ignoresHideBalanceToast: Bool = false @@ -227,7 +226,6 @@ class SettingsViewModel: NSObject, ObservableObject { quickpayAmount = 5 quickpayDailyLimitMultiplier = 5 enableNotifications = false - enableNotificationsAmount = false UserDefaults.standard.set(PaykitFeatureFlags.uiEnabledByDefault, forKey: PaykitFeatureFlags.uiEnabledKey) UserDefaults.standard.set(false, forKey: PrivatePaykitService.publishingEnabledKey) UserDefaults.standard.set(false, forKey: PublicPaykitService.publishingEnabledKey) @@ -245,7 +243,6 @@ class SettingsViewModel: NSObject, ObservableObject { _coinSelectionAlgorithm = CoinSelectionAlgorithm.branchAndBound.stringValue _selectedAddressType = "nativeSegwit" _addressTypesToMonitor = "nativeSegwit" - BlocktankRefundAddressStore().clear() pinEnabled = false isChangingAddressType = false lastAddressTypeError = nil @@ -960,17 +957,14 @@ class SettingsViewModel: NSObject, ObservableObject { } /// Gets the current app cache data for backup - func getAppCacheData() throws -> AppCacheData { + func getAppCacheData() -> AppCacheData { let spend = QuickPaySpendStore.shared.backupSnapshot() - let refundAddress = try BlocktankRefundAddressStore().load() return AppCacheData( hasSeenContactsIntro: defaults.bool(forKey: "hasSeenContactsIntro"), hasSeenProfileIntro: defaults.bool(forKey: "hasSeenProfileIntro"), hasSeenNotificationsIntro: defaults.bool(forKey: "hasSeenNotificationsIntro"), hasSeenQuickpayIntro: defaults.bool(forKey: "hasSeenQuickpayIntro"), hasSeenShopIntro: defaults.bool(forKey: "hasSeenShopIntro"), - hasSeenTransferIntro: defaults.bool(forKey: "hasSeenTransferIntro"), - hasSeenTransferToSpendingIntro: defaults.bool(forKey: "hasSeenTransferToSpendingIntro"), hasSeenTransferToSavingsIntro: defaults.bool(forKey: "hasSeenTransferToSavingsIntro"), hasSeenWidgetsIntro: defaults.bool(forKey: "hasSeenWidgetsIntro"), hasDismissedWidgetsOnboardingHint: defaults.bool(forKey: "hasDismissedWidgetsOnboardingHint"), @@ -981,20 +975,17 @@ class SettingsViewModel: NSObject, ObservableObject { dismissedSuggestions: defaults.stringArray(forKey: "dismissedSuggestions") ?? [], lastUsedTags: defaults.stringArray(forKey: "lastUsedTags") ?? [], quickPayLedger: spend, - blocktankRefundAddress: refundAddress, detachedActivityContacts: CoreService.shared.activity.detachedContactsBackupSnapshot() ) } /// Restores app cache data from backup - func restoreAppCacheData(_ cache: AppCacheData) throws { + func restoreAppCacheData(_ cache: AppCacheData) { defaults.set(cache.hasSeenContactsIntro, forKey: "hasSeenContactsIntro") defaults.set(cache.hasSeenProfileIntro, forKey: "hasSeenProfileIntro") defaults.set(cache.hasSeenNotificationsIntro, forKey: "hasSeenNotificationsIntro") defaults.set(cache.hasSeenQuickpayIntro, forKey: "hasSeenQuickpayIntro") defaults.set(cache.hasSeenShopIntro, forKey: "hasSeenShopIntro") - defaults.set(cache.hasSeenTransferIntro, forKey: "hasSeenTransferIntro") - defaults.set(cache.hasSeenTransferToSpendingIntro, forKey: "hasSeenTransferToSpendingIntro") defaults.set(cache.hasSeenTransferToSavingsIntro, forKey: "hasSeenTransferToSavingsIntro") defaults.set(cache.hasSeenWidgetsIntro, forKey: "hasSeenWidgetsIntro") defaults.set(cache.hasDismissedWidgetsOnboardingHint, forKey: "hasDismissedWidgetsOnboardingHint") @@ -1005,12 +996,6 @@ class SettingsViewModel: NSObject, ObservableObject { defaults.set(cache.dismissedSuggestions, forKey: "dismissedSuggestions") defaults.set(cache.lastUsedTags, forKey: "lastUsedTags") QuickPaySpendStore.shared.restoreFromBackup(ledger: cache.quickPayLedger) - let refundAddressStore = BlocktankRefundAddressStore() - if let refundAddress = cache.blocktankRefundAddress { - try refundAddressStore.save(refundAddress) - } else { - refundAddressStore.clear() - } CoreService.shared.activity.restoreDetachedContacts(cache.detachedActivityContacts) } } diff --git a/Bitkit/ViewModels/SheetViewModel.swift b/Bitkit/ViewModels/SheetViewModel.swift index 6cc694616..b94d45bfd 100644 --- a/Bitkit/ViewModels/SheetViewModel.swift +++ b/Bitkit/ViewModels/SheetViewModel.swift @@ -8,7 +8,6 @@ enum SheetID: String, CaseIterable { case connectionClosed case forceTransfer case forgotPin - case gift case highBalance case lnurlAuth case lnurlWithdraw @@ -25,6 +24,7 @@ enum SheetID: String, CaseIterable { case security case send case tagFilter + case transferUnavailable case dateRangeSelector case widgets case hardwareConnect @@ -257,10 +257,10 @@ class SheetViewModel: ObservableObject { } } - var connectionClosedSheetItem: ConnectionClosedSheetItem? { + var transferUnavailableSheetItem: TransferUnavailableSheetItem? { get { - guard let config = activeSheetConfiguration, config.id == .connectionClosed else { return nil } - return ConnectionClosedSheetItem() + guard let config = activeSheetConfiguration, config.id == .transferUnavailable else { return nil } + return TransferUnavailableSheetItem() } set { if newValue == nil { @@ -269,10 +269,10 @@ class SheetViewModel: ObservableObject { } } - var forgotPinSheetItem: ForgotPinSheetItem? { + var connectionClosedSheetItem: ConnectionClosedSheetItem? { get { - guard let config = activeSheetConfiguration, config.id == .forgotPin else { return nil } - return ForgotPinSheetItem() + guard let config = activeSheetConfiguration, config.id == .connectionClosed else { return nil } + return ConnectionClosedSheetItem() } set { if newValue == nil { @@ -281,12 +281,10 @@ class SheetViewModel: ObservableObject { } } - var giftSheetItem: GiftSheetItem? { + var forgotPinSheetItem: ForgotPinSheetItem? { get { - guard let config = activeSheetConfiguration, config.id == .gift else { return nil } - let giftConfig = config.data as? GiftConfig - guard let code = giftConfig?.code, let amount = giftConfig?.amount else { return nil } - return GiftSheetItem(code: code, amount: amount) + guard let config = activeSheetConfiguration, config.id == .forgotPin else { return nil } + return ForgotPinSheetItem() } set { if newValue == nil { @@ -413,7 +411,7 @@ class SheetViewModel: ObservableObject { get { guard let config = activeSheetConfiguration, config.id == .receive else { return nil } let receiveConfig = config.data as? ReceiveConfig - let initialRoute = receiveConfig?.initialRoute ?? .qr(cjitInvoice: nil, tab: nil) + let initialRoute = receiveConfig?.initialRoute ?? .qr(tab: nil) return ReceiveSheetItem(id: config.presentationID, initialRoute: initialRoute, hardwareWalletId: receiveConfig?.hardwareWalletId) } set { diff --git a/Bitkit/ViewModels/TransferViewModel.swift b/Bitkit/ViewModels/TransferViewModel.swift index 045feaffd..6bf25a561 100644 --- a/Bitkit/ViewModels/TransferViewModel.swift +++ b/Bitkit/ViewModels/TransferViewModel.swift @@ -3,155 +3,10 @@ import Combine import LDKNode import SwiftUI -/// Funding was dispatched but unresolved; confirmation must yield to its retained Pending sheet. -struct OnchainFundingPendingError: Error {} - -struct TransferUiState { - var clientBalanceSat: UInt64 = 0 - var lspBalanceSat: UInt64 = 0 - var feeSat: UInt64 = 0 - var isAdvanced = false - var isConfirming = false - var order: IBtOrder? - - var lspFeeSat: UInt64 { - feeSat.saturatingSub(clientBalanceSat) - } -} - -struct TransferValues { - var defaultLspBalance: UInt64 = 0 - var minLspBalance: UInt64 = 0 - var maxLspBalance: UInt64 = 0 - var maxClientBalance: UInt64 = 0 -} - -/// Limits/flags for the hardware-wallet transfer-to-spending flow, sourced from the device balance. -struct HwSpendingState: Equatable { - var isLoading = false - var isSigning = false - /// Reaching the device before anything is on it to sign, which may be abandoned. - var isConnectingDevice = false - var hasPendingBroadcast = false - var isPassphraseRequired = false - var isVerifyingPassphrase = false - var isCreatingOrder = false - var miningFeeSats: UInt64 = 0 - var maxAllowedToSend: UInt64 = 0 - var balanceAfterFee: UInt64 = 0 - var quarterAmount: UInt64 = 0 -} - -private struct PendingHwFundingBroadcast { - let orderId: String - let walletId: String - let address: String - let amountSats: UInt64 - let signedTx: HwFundingSignedTx - - func matches(order: IBtOrder, walletId: String, address: String) -> Bool { - orderId == order.id && - self.walletId == walletId && - self.address == address && - amountSats == order.feeSat - } -} - -/// A recoverable failure surfaced by the hardware-wallet transfer flow. The Sign screen maps each -/// case to the matching localized toast. -enum HwTransferError: Error, Equatable { - /// Reconnect failed. `isBluetooth` selects the softer INFO toast for a known BLE device - /// (advertises intermittently — "check that it is unlocked and try again") vs the ERROR toast. - case reconnect(isBluetooth: Bool) - case signingTimeout - case broadcastUncertain - /// Signed tx is retained but Electrum/network is unreachable — retry broadcast later. - case broadcastConnectivity - /// The device is locked or otherwise busy before signing can start. Carries the vendor so the - /// toast can name the device. - case deviceBusy(HwWalletVendor) - /// Firmware error (code 99) — user must reconnect the device. - case firmwareReconnect - /// The entered passphrase opened a different wallet than the one being spent from. - case passphraseMismatch - /// The device holds another wallet than the one being spent from, so reconnecting cannot help. - case walletMismatch - case funding(String?) - case generic(String?) -} - -/// The hardware-wallet funding capability the transfer flow needs. Implemented by `HwWalletManager`; -/// declared as a protocol so the flow stays testable. -@MainActor -protocol HwTransferFunding: Sendable { - func getFundingAccount(walletId: String, addressType: AddressScriptType) throws -> HwFundingAccount - func maxSpendableFunding( - walletId: String, - destinationAddress: String, - satsPerVByte: UInt64, - addressType: AddressScriptType - ) async throws -> UInt64 - func composeFundingTransaction( - walletId: String, - address: String, - sats: UInt64, - satsPerVByte: UInt64, - addressType: AddressScriptType - ) async throws -> HwFundingTransaction - /// Offline coin-selection estimate for the exact funding amount (`fingerprint: nil`); fee only. - func estimateOfflineFundingMiningFee( - walletId: String, - address: String, - sats: UInt64, - satsPerVByte: UInt64, - addressType: AddressScriptType - ) async throws -> UInt64 - func signFunding(walletId: String, funding: HwFundingTransaction) async throws -> HwFundingSignedTx - func broadcastFunding( - serializedTx: String, - paymentDeadline: PaykitPreciseInstant?, - beforeDispatch: @escaping @MainActor @Sendable () async throws -> Void - ) async throws -> String -} - -/// The device-session capability the transfer flow needs for on-device signing, addressed by wallet -/// identity: a device holds one wallet open at a time, so reaching a given wallet is more than -/// reaching its transport. Implemented by `HwWalletManager`. -@MainActor -protocol HwTransferConnecting: Sendable { - func ensureConnected(walletId: String) async throws - /// How long `ensureConnected` may take for this wallet's device before the flow gives up. - func reconnectTimeout(walletId: String) -> Double - func disconnectStaleSession(walletId: String) async - func scheduleStaleSessionCleanup(walletId: String) - /// Whether the wallet is reachable over a known Bluetooth device, so a reconnect failure can show - /// the softer BLE "check that it is unlocked and try again" toast instead of the generic error. - func isKnownBluetoothDevice(walletId: String) -> Bool - /// Best-effort pre-connect when the sign screen appears, so tapping Open Trezor Connect is less - /// likely to hit a cold reconnect. Fire-and-forget. - func warmUpConnection(walletId: String) - /// Whether the wallet's passphrase has to be collected again before the device can sign for it. - func needsPassphrase(walletId: String) -> Bool - /// Reopens a hidden wallet for signing, refusing a session that resolves to a different wallet. - func reconnectWithPassphrase(walletId: String, passphrase: String) async throws -} - @MainActor class TransferViewModel: ObservableObject { - private static let orderReuseMargin: TimeInterval = 60 - - @Published var uiState = TransferUiState() - private var fundedOrderId: String? - @Published private(set) var recoveredOnchainFundingOrderId: String? - private var pendingOnchainFunding: OnchainSendPendingContext? - private var fundingResolutionObservation: AnyCancellable? - @Published var lightningSetupStep: Int = 0 - @Published var transferValues = TransferValues() - - @Published var isSettlingAdvancedCapacity = false @Published var selectedChannelIds: [String] = [] @Published var channelsToClose: [ChannelDetails] = [] - @Published var transferUnavailable = false /// How the LN -> onchain "transfer to savings" is executed. Closing a channel is the /// default because it always works; swapping funds out keeps channels open and is used @@ -159,36 +14,10 @@ class TransferViewModel: ObservableObject { @Published var savingsTransferMode: SavingsTransferMode = .close @Published var savingsSwapState = SavingsSwapState() - /// Hardware-wallet transfer-to-spending state. - @Published var hwSpending = HwSpendingState() - /// Bumped when a hardware funding tx is signed + broadcast, so the Sign screen advances. - @Published var hwSignedEvent = 0 - /// Set when funding is paid and recorded; observed app-wide so navigation survives leaving the sign screen. - @Published var hwFundingComplete = false - /// A recoverable hardware transfer failure the Sign screen should toast, then clear. - @Published var hwTransferError: HwTransferError? - - private let coreService: CoreService private let lightningService: LightningService - private let onchainAttemptService: OnchainSendAttemptService - private let onchainSender: any OnchainSending - private let onchainBalanceProvider: () -> UInt64 - private let currencyService: CurrencyService private let transferService: TransferService private let sheetViewModel: SheetViewModel private let onBalanceRefresh: (() async -> Void)? - /// The device-signing orchestration for a hardware-wallet transfer; nil when the HW capabilities - /// aren't injected (previews/plain init). - private let hwSigner: HwFundingSigner? - private let hwConnecting: HwTransferConnecting? - - private var refreshTimer: Timer? - private var refreshTask: Task? - private var hwSignTask: Task? - private var hwPassphraseTask: Task? - private var hwOrderCreationToken: UUID? - private var pendingHwFundingBroadcast: PendingHwFundingBroadcast? - private var activeHwTransferWalletId: String? private let retryInterval: TimeInterval = 60 // 1 min private let giveUpInterval: TimeInterval = 30 * 60 // 30 min @@ -209,1200 +38,38 @@ class TransferViewModel: ObservableObject { private let swapQuoteTimeout: TimeInterval = 15 /// Minimum sats held back from a swap to cover Lightning routing fees. private static let minLnRoutingFeeReserveSats: UInt64 = 10 - private static let maxAffordabilityRounds = 2 init( - coreService: CoreService = .shared, lightningService: LightningService = .shared, - currencyService: CurrencyService = .shared, transferService: TransferService, sheetViewModel: SheetViewModel, - hwFunding: HwTransferFunding? = nil, - hwConnecting: HwTransferConnecting? = nil, - hwFeeRateProvider: (() async -> UInt64?)? = nil, - hwAddressProvider: (() async throws -> String)? = nil, - hwTimeouts: (compose: Double, sign: Double, broadcast: Double) = (compose: 45, sign: 120, broadcast: 120), - onBalanceRefresh: (() async -> Void)? = nil, - onchainAttemptService: OnchainSendAttemptService = .shared, - onchainSender: (any OnchainSending)? = nil, - onchainBalanceProvider: (() -> UInt64)? = nil + onBalanceRefresh: (() async -> Void)? = nil ) { - self.coreService = coreService self.lightningService = lightningService - self.onchainAttemptService = onchainAttemptService - self.onchainSender = onchainSender ?? lightningService - self.onchainBalanceProvider = onchainBalanceProvider ?? { lightningService.balances?.totalOnchainBalanceSats ?? 0 } - self.currencyService = currencyService self.transferService = transferService self.sheetViewModel = sheetViewModel self.onBalanceRefresh = onBalanceRefresh - if let hwFunding, let hwConnecting { - self.hwConnecting = hwConnecting - hwSigner = HwFundingSigner( - funding: hwFunding, - connecting: hwConnecting, - feeRateProvider: hwFeeRateProvider ?? { nil }, - addressProvider: hwAddressProvider ?? { throw AppError(message: "No address provider", debugMessage: "") }, - timeouts: hwTimeouts - ) - } else { - self.hwConnecting = nil - hwSigner = nil - } - fundingResolutionObservation = OnchainSendAttemptService.localResolutionPublisher.sink { [weak self] resolution in - Task { @MainActor [weak self] in - guard let self, let context = pendingOnchainFunding, - context.attemptId == resolution.attemptId, context.walletId == resolution.walletId else { return } - await resumeRecoveredOnchainFunding(context: context) - } - } } /// Convenience initializer for testing and previews. Leave `transferDefaults` nil to persist through /// `TransferStorage.shared`, whose change notifications drive backups; tests pass an isolated suite /// so mock transfers never reach the app's own store. convenience init( - coreService: CoreService = .shared, lightningService: LightningService = .shared, - currencyService: CurrencyService = .shared, sheetViewModel: SheetViewModel = SheetViewModel(), transferDefaults: UserDefaults? = nil ) { let transferService = TransferService( storage: transferDefaults.map { TransferStorage(defaults: $0) } ?? .shared, - lightningService: lightningService, - blocktankService: coreService.blocktank + lightningService: lightningService ) self.init( - coreService: coreService, lightningService: lightningService, - currencyService: currencyService, transferService: transferService, sheetViewModel: sheetViewModel ) } - /// Convenience initializer for hardware-wallet transfer tests. Builds the `TransferService` - /// inside the app module so callers don't construct cross-module service types — `transferDefaults` - /// is a `UserDefaults` for the same reason, since `TransferStorage` is compiled into both modules. - /// Leave it nil to persist through `TransferStorage.shared`; tests pass an isolated suite so mock - /// transfers never reach the app's own store. - convenience init( - hwFunding: HwTransferFunding?, - hwConnecting: HwTransferConnecting?, - hwFeeRateProvider: (() async -> UInt64?)? = nil, - hwAddressProvider: (() async throws -> String)? = nil, - hwTimeouts: (compose: Double, sign: Double, broadcast: Double) = (compose: 45, sign: 120, broadcast: 120), - coreService: CoreService = .shared, - lightningService: LightningService = .shared, - sheetViewModel: SheetViewModel = SheetViewModel(), - transferDefaults: UserDefaults? = nil - ) { - let transferService = TransferService( - storage: transferDefaults.map { TransferStorage(defaults: $0) } ?? .shared, - lightningService: lightningService, - blocktankService: coreService.blocktank - ) - self.init( - coreService: coreService, - lightningService: lightningService, - transferService: transferService, - sheetViewModel: sheetViewModel, - hwFunding: hwFunding, - hwConnecting: hwConnecting, - hwFeeRateProvider: hwFeeRateProvider, - hwAddressProvider: hwAddressProvider, - hwTimeouts: hwTimeouts - ) - } - - deinit { - Task { @MainActor [weak self] in - Logger.debug("Stopping poll for order") - self?.stopPolling() - } - } - - private func stopPolling() { - refreshTimer?.invalidate() - refreshTimer = nil - refreshTask?.cancel() - refreshTask = nil - } - - /// Calculates the total value of channels connected to Blocktank nodes - func totalBtChannelsValueSats(blocktankInfo: IBtInfo?) -> UInt64 { - guard let channels = lightningService.channels, let btNodeIds = blocktankInfo?.nodes.map(\.pubkey) else { return 0 } - - let btChannels = channels.filter { channel in - btNodeIds.contains(channel.counterpartyNodeId) - } - - return btChannels.reduce(0) { sum, channel in - sum + channel.channelValueSats - } - } - - var isSpendingBusy: Bool { - uiState.isConfirming || hwSpending.isSigning || hwSpending.isCreatingOrder - } - - /// Whether the hardware sign screen may be left. Reaching the device (a Jade may wait minutes for - /// its PIN) can be abandoned, and leaving cancels it; once the device is asked to sign it cannot. - var canLeaveHwSign: Bool { - !isSpendingBusy || hwSpending.isConnectingDevice - } - - func onEstimateReady(clientBalance: UInt64, lspBalance: UInt64, feeSat: UInt64, isAdvanced: Bool = false) { - guard !isSpendingBusy else { return } - clearPendingHwFundingBroadcast() - uiState.order = nil - hwSpending.miningFeeSats = 0 - uiState.clientBalanceSat = clientBalance - uiState.lspBalanceSat = lspBalance - uiState.feeSat = feeSat - uiState.isAdvanced = isAdvanced - } - - func onOrderCreated(order: IBtOrder) { - uiState.order = order - } - - func orderForConfirmation( - createOrder: (_ clientBalance: UInt64, _ lspBalance: UInt64) async throws -> IBtOrder, - isCurrent: () -> Bool = { true } - ) async throws -> IBtOrder? { - guard let order = try await currentOrder(createOrder: createOrder, isCurrent: isCurrent) else { return nil } - if pendingHwFundingBroadcast?.orderId == order.id { - return order - } - return orderForDisplayedFee(order) - } - - func orderForSwipe( - createOrder: (_ clientBalance: UInt64, _ lspBalance: UInt64) async throws -> IBtOrder - ) async throws -> IBtOrder { - guard let order = try await currentOrder(createOrder: createOrder, isCurrent: { true }) else { throw CancellationError() } - return order - } - - private func currentOrder( - createOrder: (_ clientBalance: UInt64, _ lspBalance: UInt64) async throws -> IBtOrder, - isCurrent: () -> Bool - ) async throws -> IBtOrder? { - guard isCurrent() else { return nil } - if let retained = try await onchainAttemptService.blockingAttempt(), - retained.requestId == nil, retained.orderId != nil - { - let context = OnchainSendPendingContext(attemptId: retained.id, walletId: retained.walletId, txid: retained.txid) - await showFundingPending(context) - throw OnchainFundingPendingError() - } - if let order = uiState.order { - if pendingHwFundingBroadcast?.orderId == order.id { - return order - } - if isReusableSpendingOrder(order) { - return order - } - } - uiState.order = nil - let order = try await createOrder(uiState.clientBalanceSat, uiState.lspBalanceSat) - guard isCurrent() else { return nil } - uiState.order = order - return order - } - - private func isReusableSpendingOrder(_ order: IBtOrder, now: Date = Date()) -> Bool { - guard order.state2 == .created, order.id != fundedOrderId else { return false } - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - guard let expiresAt = formatter.date(from: order.orderExpiresAt) ?? ISO8601DateFormatter().date(from: order.orderExpiresAt) - else { return false } - return expiresAt > now.addingTimeInterval(Self.orderReuseMargin) - } - - private func orderForDisplayedFee(_ order: IBtOrder) -> IBtOrder? { - uiState.order = order - guard order.feeSat <= uiState.feeSat else { - uiState.feeSat = order.feeSat - return nil - } - return order - } - - /// The increase to report when the funding rebuilt for the order costs more than the confirm screen showed at swipe time, or nil when - /// the swipe may pay. On an increase the screen moves to the order's fee and nothing is paid until the next swipe. - func feeIncrease( - order: IBtOrder, - displayedOrderFeeSat: UInt64, - displayed: SpendingConfirmAmounts, - rebuilt: SpendingConfirmAmounts - ) -> SpendingFeeIncrease? { - guard rebuilt.totalSat > displayed.totalSat else { return nil } - let amountSat = rebuilt.totalSat - displayed.totalSat - let isServiceIncrease = order.feeSat > displayedOrderFeeSat - uiState.feeSat = order.feeSat - return isServiceIncrease ? .service(amountSat: amountSat) : .network(amountSat: amountSat) - } - - /// The increase to report when the funding could not be rebuilt for the created order and its fee is higher than the confirm screen - /// showed, or nil when the failure is unrelated to the order fee. On an increase the screen moves to the order's fee, so it re-sizes - /// against the real cost instead of repeating the same failure. - func unfundableFeeIncrease(order: IBtOrder, displayedOrderFeeSat: UInt64) -> SpendingFeeIncrease? { - guard order.feeSat > displayedOrderFeeSat else { return nil } - uiState.feeSat = order.feeSat - return .service(amountSat: order.feeSat - displayedOrderFeeSat) - } - - func payOrder( - order: IBtOrder, - speed: TransactionSpeed, - txFee: UInt64, - satsPerVbyte: UInt32, - utxosToSpend: [SpendableUtxo]? = nil, - isMaxAmount: Bool = false, - maxSendableAmount: UInt64? = nil - ) async throws { - if let context = try await onchainAttemptService.orderPendingContext(orderId: order.id) { - await showFundingPending(context) - if recoveredOnchainFundingOrderId == order.id { - return - } - throw OnchainFundingPendingError() - } - guard let address = order.payment?.onchain?.address else { - throw AppError(message: "Order payment onchain address is nil", debugMessage: nil) - } - - let preTransferOnchainSats = onchainBalanceProvider() - - // Verify we can afford the transfer when using sendAll - if isMaxAmount, let maxSendable = maxSendableAmount, maxSendable < order.feeSat { - throw AppError( - message: t("other__pay_insufficient_savings"), - debugMessage: "Fee rate changed. Max sendable: \(maxSendable), order requires: \(order.feeSat)" - ) - } - - let txTotalSats = SpendingConfirmTotal.leavingAmount( - orderFeeSat: order.feeSat, - networkFeeSat: txFee, - shouldUseSendAll: isMaxAmount, - maxSendable: maxSendableAmount - ) - - // For sendAll (change would be dust), send entire balance - // Otherwise, send exact order.feeSat amount - let result = try await onchainAttemptService.send( - using: onchainSender, - address: address, - amountSats: order.feeSat, - satsPerVbyte: satsPerVbyte, - utxosToSpend: utxosToSpend, - isMaxAmount: isMaxAmount, - orderId: order.id, - followupContext: OnchainSendFollowupContext( - feeSats: txFee, feeRate: satsPerVbyte, tags: [], contact: nil, - createdAt: UInt64(Date().timeIntervalSince1970) - ), - transferContext: OnchainSendTransferContext( - clientBalanceSats: order.clientBalanceSat, - txTotalSats: txTotalSats, - preTransferOnchainSats: preTransferOnchainSats, - originalOrderFeeSats: order.feeSat - ) - ) - let txid: String - switch result { - case let .accepted(acceptedTxid): - txid = acceptedTxid - case let .rejected(rejectedTxid, reason): - if let context = try await onchainAttemptService.pendingContext(txid: rejectedTxid) { - await showFundingPending(context) - throw OnchainFundingPendingError() - } - throw AppError( - message: t("wallet__onchain_funding_rejected"), - debugMessage: "Broadcast rejected for \(rejectedTxid): \(reason)" - ) - case let .unknown(unknownTxid): - if let context = try await onchainAttemptService.pendingContext(txid: unknownTxid) { - await showFundingPending(context) - throw OnchainFundingPendingError() - } - throw AppError( - message: t("wallet__onchain_funding_unknown"), - debugMessage: "Broadcast outcome unknown for \(unknownTxid)" - ) - } - - do { - guard try await onchainAttemptService.resumeAcceptedTransfer( - orderId: order.id, txid: txid, using: transferService - ) else { throw OnchainSendAttemptError.localFollowupNotSaved } - } catch { - Logger.warn("Accepted funding local follow-up remains guarded: \(error)", context: "TransferViewModel") - throw AppError( - message: t("wallet__onchain_funding_followup_failed"), - debugMessage: "Accepted funding transaction \(txid) for order \(order.id)" - ) - } - fundedOrderId = order.id - lightningSetupStep = 0 - await onBalanceRefresh?() - watchOrder(orderId: order.id) - } - - private func showFundingPending(_ context: OnchainSendPendingContext) async { - pendingOnchainFunding = context - sheetViewModel.showSheet(.send, data: SendConfig(view: .onchainPending(context))) - do { - if let attempt = try await onchainAttemptService.pendingAttempt(context: context), - attempt.status == .accepted, !attempt.localFollowupComplete, - let orderId = attempt.orderId, let txid = attempt.txid - { - _ = try await onchainAttemptService.resumeAcceptedTransfer(orderId: orderId, txid: txid, using: transferService) - } - } catch { - Logger.warn("Accepted original funding follow-up remains Pending: \(error)", context: "TransferViewModel") - } - // Acceptance can precede the sheet and its subscription's next delivery. - await resumeRecoveredOnchainFunding(context: context) - } - - private func resumeRecoveredOnchainFunding(context: OnchainSendPendingContext) async { - do { - guard pendingOnchainFunding?.attemptId == context.attemptId, - pendingOnchainFunding?.walletId == context.walletId, - let attempt = try await onchainAttemptService.pendingAttempt(context: context), - attempt.status == .accepted, attempt.localFollowupComplete, - attempt.requestId == nil, let orderId = attempt.orderId, - fundedOrderId != orderId else { return } - pendingOnchainFunding = nil - fundedOrderId = orderId - lightningSetupStep = 0 - watchOrder(orderId: orderId) - recoveredOnchainFundingOrderId = orderId - await onBalanceRefresh?() - } catch { - Logger.warn("Recovered funding order remains guarded: \(error)", context: "TransferViewModel") - } - } - - @discardableResult - private func fundPaidOrder( - order: IBtOrder, - txId: String, - createTransferActivity: Bool = false, - fee: UInt64 = 0, - feeRate: UInt64 = 0, - txTotalSats: UInt64? = nil, - preTransferOnchainSats: UInt64? = nil, - activityWalletId: String = WalletScope.default - ) async -> Bool { - fundedOrderId = order.id - var trackingSaved = false - do { - let transferId = try await transferService.createTransfer( - type: .toSpending, - amountSats: order.clientBalanceSat, - fundingTxId: txId, - lspOrderId: order.id, - txTotalSats: txTotalSats, - preTransferOnchainSats: preTransferOnchainSats - ) - Logger.info("Created transfer tracking record: \(transferId)", context: "TransferViewModel") - trackingSaved = true - } catch { - Logger.error("Failed to create transfer tracking record", context: error.localizedDescription) - } - - if createTransferActivity { - await transferService.createPendingToSpendingActivity( - order: order, - txId: txId, - fee: fee, - feeRate: feeRate, - walletId: activityWalletId - ) - } - - lightningSetupStep = 0 - await onBalanceRefresh?() - watchOrder(orderId: order.id) - return trackingSaved - } - - func startWatchingOrderFromRestart(_ order: IBtOrder) async { - Logger.info("Starting to watch order from restart: \(order.id)") - - uiState.order = order - uiState.clientBalanceSat = order.clientBalanceSat - uiState.lspBalanceSat = order.lspBalanceSat - uiState.feeSat = order.feeSat - uiState.isAdvanced = false - - watchOrder(orderId: order.id) - } - - private func watchOrder(orderId: String, frequencyMs: Double = 15000) { - stopPolling() - - let frequencySecs = frequencyMs / 1000.0 - Logger.debug("Starting to watch order \(orderId)") - - refreshTimer = Timer.scheduledTimer(withTimeInterval: frequencySecs, repeats: true) { [weak self] _ in - Task { @MainActor [weak self] in - guard let self else { return } - refreshTask?.cancel() - refreshTask = Task { @MainActor [weak self] in - guard let self else { return } - - do { - Logger.debug("Refreshing order \(orderId)") - let orders = try await coreService.blocktank.orders(orderIds: [orderId], refresh: true) - guard let order = orders.first else { - Logger.error("Order not found \(orderId)", context: "TransferViewModel") - return - } - - let step = try await updateOrder(order: order) - lightningSetupStep = step - Logger.debug("LN setup step: \(step)") - - if order.state2 == .expired { - Logger.error("Order expired \(orderId)", context: "TransferViewModel") - stopPolling() - return - } - - if step > 2 { - Logger.debug("Order settled, stopping polling") - - // Sync transfer states when order completes - try? await transferService.syncTransferStates() - - stopPolling() - return - } - } catch { - Logger.error(error, context: "Failed to watch order") - stopPolling() - } - } - } - } - - refreshTask = Task { @MainActor [weak self] in - guard let self else { return } - - do { - let orders = try await coreService.blocktank.orders(orderIds: [orderId], refresh: true) - guard let order = orders.first else { - Logger.error("Order not found \(orderId)", context: "TransferViewModel") - return - } - - let step = try await updateOrder(order: order) - lightningSetupStep = step - } catch { - Logger.error(error, context: "Failed in initial order check") - stopPolling() - } - } - } - - private func updateOrder(order: IBtOrder) async throws -> Int { - var currentStep = 0 - - if order.channel != nil { - do { - try await transferService.syncTransferStates() - } catch { - Logger.error("Failed to sync transfer states after updateOrder", context: error.localizedDescription) - // Don't throw - we don't want to fail the entire sync if transfer sync fails - } - - return 3 - } - - switch order.state2 { - case .created: - currentStep = 0 - - case .paid: - currentStep = 1 - - do { - _ = try await coreService.blocktank.open(orderId: order.id) - } catch { - Logger.error("Error opening channel: \(error.localizedDescription)", context: "TransferViewModel") - } - - case .executed: - currentStep = 2 - - default: - break - } - - return currentStep - } - - func onDefaultClick( - lspBalance: UInt64, - estimateFundingAmount: (_ clientBalance: UInt64, _ lspBalance: UInt64) async throws -> UInt64 - ) async throws { - guard !isSpendingBusy else { return } - let clientBalance = uiState.clientBalanceSat - let feeSat = try await estimateFundingAmount(clientBalance, lspBalance) - onEstimateReady(clientBalance: clientBalance, lspBalance: lspBalance, feeSat: feeSat) - } - - /// Compute the available/MAX/quarter limits for a hardware-wallet transfer: the signer resolves - /// the device's native-segwit balance minus an on-chain fee reserve, then the shared - /// spending-limit calculation clamps it to the LSP receiving cap. - func updateHwLimits( - walletId: String, - blocktankInfo: IBtInfo?, - estimateOrderFee: @escaping (_ clientBalance: UInt64, _ lspBalance: UInt64) async throws - -> (networkFeeSat: UInt64, serviceFeeSat: UInt64) - ) async { - guard let hwSigner else { return } - clearPendingHwFundingBroadcast() - hwSpending.isLoading = true - - let availability: HwFundingSigner.Availability - do { - availability = try await hwSigner.availability(walletId: walletId) - } catch { - hwSpending = HwSpendingState(isLoading: false) - hwTransferError = .generic((error as? AppError)?.message ?? error.localizedDescription) - return - } - - do { - let (avail, maxAmount) = try await calculateSpendingLimits( - onchainAvailable: availability.available, - lspMaxClientBalance: blocktankInfo?.options.maxClientBalanceSat, - transferValues: { self.calculateTransferValues(clientBalanceSat: $0, blocktankInfo: blocktankInfo) }, - estimateOrderFee: estimateOrderFee - ) - hwSpending.balanceAfterFee = avail - hwSpending.maxAllowedToSend = maxAmount - hwSpending.quarterAmount = min(availability.balanceSats / 4, maxAmount) - } catch { - hwSpending.balanceAfterFee = 0 - hwSpending.maxAllowedToSend = 0 - hwSpending.quarterAmount = 0 - } - - hwSpending.isLoading = false - } - - func updateHwFundingFeeEstimate(walletId: String) async { - guard let hwSigner, !hwSpending.hasPendingBroadcast else { return } - do { - let address: String = if let orderAddress = uiState.order?.payment?.onchain?.address { - orderAddress - } else { - try await hwSigner.addressProvider() - } - hwSpending.miningFeeSats = try await hwSigner.estimateOfflineFundingMiningFee( - walletId: walletId, - address: address, - sats: uiState.feeSat - ) - } catch { - Logger.debug("Skipped offline hardware funding fee estimate for '\(walletId)'", context: "TransferViewModel") - } - } - - func onTransferToSpendingHwConfirm( - walletId: String, - createOrder: (_ clientBalance: UInt64, _ lspBalance: UInt64) async throws -> IBtOrder - ) async { - guard !isSpendingBusy else { return } - let token = UUID() - hwOrderCreationToken = token - hwSpending.isCreatingOrder = true - defer { - if hwOrderCreationToken == token { - hwOrderCreationToken = nil - hwSpending.isCreatingOrder = false - } - } - - do { - guard let order = try await orderForConfirmation( - createOrder: createOrder, - isCurrent: { self.hwOrderCreationToken == token } - ), hwOrderCreationToken == token else { return } - onTransferToSpendingHwConfirm(order: order, walletId: walletId) - } catch is OnchainFundingPendingError { - return - } catch { - hwTransferError = .generic((error as? AppError)?.message ?? error.localizedDescription) - } - } - - func onTransferToSpendingHwConfirm(order: IBtOrder, walletId: String) { - guard !hwSpending.isSigning else { return } - guard let hwSigner, let address = order.payment?.onchain?.address, !address.isEmpty else { - hwTransferError = .generic(t("common__error")) - return - } - // A hidden wallet whose session is gone can only be reopened with its passphrase, and the - // device would otherwise sign from whichever wallet the current session holds. A signed - // transaction awaiting a broadcast retry is the exception: broadcasting never reaches the - // device, so holding the retry behind a passphrase would strand funds the user already - // approved — the same reason `cancelHwSigning` leaves the device alone while one is pending. - let isBroadcastRetry = pendingHwFundingBroadcast?.matches(order: order, walletId: walletId, address: address) == true - if !isBroadcastRetry, hwConnecting?.needsPassphrase(walletId: walletId) == true { - hwSpending.isPassphraseRequired = true - return - } - - activeHwTransferWalletId = walletId - hwSpending.isSigning = true - hwTransferError = nil - - hwSignTask = Task { @MainActor [weak self] in - guard let self else { return } - defer { - self.hwSpending.isSigning = false - self.hwSpending.isConnectingDevice = false - self.hwSignTask = nil - } - - do { - let signedTx: HwFundingSignedTx - if let pending = pendingHwFundingBroadcast, pending.matches(order: order, walletId: walletId, address: address) { - signedTx = pending.signedTx - hwSpending.miningFeeSats = signedTx.miningFeeSats - } else { - signedTx = try await hwSigner.prepareSignedFunding( - order: order, - walletId: walletId, - address: address - ) { [weak self] funding in - self?.hwSpending.miningFeeSats = funding.miningFeeSats - } onConnectingDevice: { [weak self] isConnecting in - self?.hwSpending.isConnectingDevice = isConnecting - } - pendingHwFundingBroadcast = PendingHwFundingBroadcast( - orderId: order.id, - walletId: walletId, - address: address, - amountSats: order.feeSat, - signedTx: signedTx - ) - hwSpending.hasPendingBroadcast = true - } - let result = try await hwSigner.broadcastSignedFunding(signedTx) - clearPendingHwFundingBroadcast() - await fundPaidOrder( - order: order, - txId: result.txId, - createTransferActivity: true, - fee: result.miningFeeSats, - feeRate: result.feeRate, - // The wallet being spent from is the wallet the transfer is recorded against. - activityWalletId: walletId - ) - activeHwTransferWalletId = nil - hwFundingComplete = true - hwSignedEvent += 1 - } catch is CancellationError { - // User dismissed the flow — no toast. - } catch let error as HwTransferError { - self.handleHardwareTransferFailure(error, walletId: walletId) - } catch { - if error.isHwUserCancellation() { - Logger.info("Hardware transfer cancelled on device for '\(walletId)'", context: "TransferViewModel") - return - } - handleRawHardwareTransferFailure(error, walletId: walletId) - } - } - } - - /// Reopens the hidden wallet with the entered passphrase and, once its accounts prove it is the - /// wallet the transfer is for, continues into signing. The passphrase is passed straight through - /// to the device session; it is never kept in view state. - func onHwPassphraseSubmit(order: IBtOrder, walletId: String, passphrase: String) { - guard !passphrase.isEmpty, let hwConnecting, hwPassphraseTask == nil else { return } - - hwSpending.isVerifyingPassphrase = true - hwTransferError = nil - - // Tracked separately from `hwSignTask`: on success this hands over to the confirm below, - // which installs its own signing task that this one's cleanup must not tear down. - hwPassphraseTask = Task { @MainActor [weak self] in - guard let self else { return } - defer { - self.hwSpending.isVerifyingPassphrase = false - self.hwPassphraseTask = nil - } - do { - try await hwConnecting.reconnectWithPassphrase(walletId: walletId, passphrase: passphrase) - // The prompt can be dismissed while the device is still reopening the wallet, and - // the confirm below would start a signing task a late dismiss could not reach. - guard hwSpending.isPassphraseRequired else { return } - hwSpending.isPassphraseRequired = false - onTransferToSpendingHwConfirm(order: order, walletId: walletId) - } catch is CancellationError { - // User dismissed the prompt — no toast. - } catch HwPassphraseError.mismatch { - Logger.warn("Rejected wrong passphrase for hardware wallet '\(walletId)'", context: "TransferViewModel") - hwTransferError = .passphraseMismatch - } catch { - handleRawHardwareTransferFailure(error, walletId: walletId) - } - } - } - - /// Backing out of the prompt also drops the reopen it started, so no signature is requested. - func onHwPassphraseDismiss() { - hwPassphraseTask?.cancel() - hwPassphraseTask = nil - hwSpending.isPassphraseRequired = false - hwSpending.isVerifyingPassphrase = false - } - - /// Pre-connect the hardware device when the sign screen appears, mirroring Android's warm-up, so - /// tapping Open Trezor Connect is less likely to hit a cold reconnect. Best-effort no-op without - /// the HW capabilities. - func warmUpHardwareConnection(walletId: String) { - guard !hwSpending.hasPendingBroadcast else { return } - hwSigner?.warmUp(walletId: walletId) - } - - /// Cancel an in-flight hardware signing task when the user abandons the sign flow, so a later - /// on-device approval can't still sign/broadcast/record. Idempotent. No-op while a signed tx - /// is awaiting broadcast retry. - func cancelHwSigning() { - // The prompt and the reopen it started belong to the screen being left. Neither touches a - // signed transaction waiting to be broadcast, so they are dropped before the guard below — - // otherwise leaving mid-verify would leave the reopen running and the prompt set to reappear. - onHwPassphraseDismiss() - hwOrderCreationToken = nil - hwSpending.isCreatingOrder = false - - guard pendingHwFundingBroadcast == nil else { return } - let walletId = activeHwTransferWalletId - hwSignTask?.cancel() - hwSignTask = nil - hwSpending.isSigning = false - hwSpending.isConnectingDevice = false - activeHwTransferWalletId = nil - if let walletId, let hwConnecting { - Task { - await hwConnecting.disconnectStaleSession(walletId: walletId) - } - } - } - - func consumeHwFundingComplete() { - hwFundingComplete = false - } - - private func clearPendingHwFundingBroadcast() { - pendingHwFundingBroadcast = nil - hwSpending.hasPendingBroadcast = false - } - - private func handleHardwareTransferFailure(_ error: HwTransferError, walletId: String) { - switch error { - case .reconnect: - Logger.error("Failed to reconnect hardware device '\(walletId)'", context: "TransferViewModel") - case .signingTimeout: - Logger.warn("Timed out hardware transfer signing for '\(walletId)'", context: "TransferViewModel") - case .broadcastUncertain: - Logger.warn("Hardware funding broadcast timed out (uncertain) for '\(walletId)'", context: "TransferViewModel") - case .broadcastConnectivity: - Logger.warn("Hardware funding broadcast connectivity failure for '\(walletId)'", context: "TransferViewModel") - case .deviceBusy: - Logger.warn("Blocked hardware transfer for locked or busy device '\(walletId)'", context: "TransferViewModel") - case .firmwareReconnect: - Logger.warn("Received hardware firmware error for '\(walletId)'", context: "TransferViewModel") - case .passphraseMismatch: - Logger.warn("Rejected wrong passphrase for hardware wallet '\(walletId)'", context: "TransferViewModel") - case .walletMismatch: - Logger.warn("Rejected hardware device holding another wallet for '\(walletId)'", context: "TransferViewModel") - case let .funding(message): - Logger.warn("Failed to compose hardware funding for '\(walletId)': \(message ?? "")", context: "TransferViewModel") - case .generic: - break - } - hwTransferError = error - } - - private func handleRawHardwareTransferFailure(_ error: Error, walletId: String) { - // The device is open on another identity and only this wallet's passphrase reopens it, so - // raise the prompt instead of reporting a failure the user can do nothing about. - if case HwPassphraseError.required = error { - Logger.info("Asking for the passphrase to reopen hardware wallet '\(walletId)'", context: "TransferViewModel") - hwSpending.isPassphraseRequired = true - return - } - if case HwPassphraseError.mismatch = error { - hwTransferError = .passphraseMismatch - return - } - if error is HwWalletMismatchError { - Logger.warn("Rejected hardware device holding another wallet for '\(walletId)'", context: "TransferViewModel") - hwTransferError = .walletMismatch - return - } - if let vendor = error.hwBusyVendor { - hwTransferError = .deviceBusy(vendor) - return - } - if error.isHwFirmwareError() { - hwTransferError = .firmwareReconnect - return - } - if pendingHwFundingBroadcast != nil { - if error.isBroadcastConnectivityFailure() { - hwTransferError = .broadcastConnectivity - return - } - clearPendingHwFundingBroadcast() - } - hwTransferError = .generic(HwErrorPresenter.jadeMessage(from: error) ?? (error as? AppError)?.message ?? error.localizedDescription) - } - - // MARK: - Balance Calculation - - /// Calculates channel liquidity options using bitkit-core - func calculateTransferValues(clientBalanceSat: UInt64, blocktankInfo: IBtInfo?) -> TransferValues { - guard let blocktankInfo else { - return TransferValues() - } - - guard let rates = currencyService.loadCachedRates(), - let eurRate = currencyService.getCurrentRate(for: "EUR", from: rates) - else { - Logger.error("Failed to get rates for calculateTransferValues", context: "TransferViewModel") - return TransferValues() - } - - let satsPerEur = currencyService.convertFiatToSats(fiatValue: 1, rate: eurRate) - let existingChannelsTotalSat = totalBtChannelsValueSats(blocktankInfo: blocktankInfo) - - let params = ChannelLiquidityParams( - clientBalanceSat: clientBalanceSat, - existingChannelsTotalSat: existingChannelsTotalSat, - minChannelSizeSat: blocktankInfo.options.minChannelSizeSat, - maxChannelSizeSat: blocktankInfo.options.maxChannelSizeSat, - satsPerEur: satsPerEur - ) - - let options = BitkitCore.calculateChannelLiquidityOptions(params: params) - - return TransferValues( - defaultLspBalance: options.defaultLspBalanceSat, - minLspBalance: options.minLspBalanceSat, - maxLspBalance: options.maxLspBalanceSat, - maxClientBalance: options.maxClientBalanceSat - ) - } - - /// Liquidity options for the advanced screen, with the maximum receiving capacity settled on one - /// the budget can pay the order fee for. - /// - /// The LSP prices both sides of the channel, so a higher capacity costs more, and its advertised - /// maximum knows nothing of the client balance already committed. - func updateAdvancedTransferValues( - clientBalanceSat: UInt64, - budget: () async -> UInt64?, - transferValues: (_ clientBalanceSat: UInt64) -> TransferValues, - estimateOrderFee: (_ clientBalance: UInt64, _ lspBalance: UInt64) async throws -> (networkFeeSat: UInt64, serviceFeeSat: UInt64) - ) async { - isSettlingAdvancedCapacity = true - defer { isSettlingAdvancedCapacity = false } - - // Publish the advertised options before the budget is read: until they land the screen has no - // cap at all, and reading the budget is itself a round trip. - var values = transferValues(clientBalanceSat) - self.transferValues = values - - guard values.maxLspBalance > values.minLspBalance, let budget = await budget() else { return } - - let settled = await settleAdvancedLspBalance( - clientBalance: clientBalanceSat, - budget: budget, - minLspBalance: values.minLspBalance, - maxLspBalance: values.maxLspBalance, - estimateOrderFee: estimateOrderFee - ) - - guard let settled, settled < values.maxLspBalance else { return } - Logger.info("Settled max capacity '\(values.maxLspBalance)' on affordable '\(settled)'", context: "TransferViewModel") - values.maxLspBalance = settled - // The Default button must not hand back a capacity the settled max just excluded. - values.defaultLspBalance = min(values.defaultLspBalance, settled) - self.transferValues = values - } - - /// The highest receiving capacity `budget` can pay the order fee for, or nil when even - /// `minLspBalance` is out of reach — the confirm step does the rejecting rather than this - /// presenting a range with nothing valid in it. - func settleAdvancedLspBalance( - clientBalance: UInt64, - budget: UInt64, - minLspBalance: UInt64, - maxLspBalance: UInt64, - estimateOrderFee: (_ clientBalance: UInt64, _ lspBalance: UInt64) async throws -> (networkFeeSat: UInt64, serviceFeeSat: UInt64) - ) async -> UInt64? { - let headroom = budget.saturatingSub(clientBalance) - - guard let maxFee = await lspFeeQuote(clientBalance: clientBalance, lspBalance: maxLspBalance, estimateOrderFee: estimateOrderFee) else { - Logger.warn("Advertising unsettled max capacity '\(maxLspBalance)', fee quote unavailable", context: "TransferViewModel") - return maxLspBalance - } - if maxFee <= headroom { - return maxLspBalance - } - - guard let minFee = await lspFeeQuote(clientBalance: clientBalance, lspBalance: minLspBalance, estimateOrderFee: estimateOrderFee), - minFee <= headroom - else { return nil } - - return await settleCapacity( - clientBalance: clientBalance, - headroom: headroom, - affordable: minLspBalance, - affordableFee: minFee, - overBudget: maxLspBalance, - overBudgetFee: maxFee, - estimateOrderFee: estimateOrderFee - ) - } - - /// Nil when the LSP will not quote: callers skip the check rather than reject. - private func lspFeeQuote( - clientBalance: UInt64, - lspBalance: UInt64, - estimateOrderFee: (_ clientBalance: UInt64, _ lspBalance: UInt64) async throws -> (networkFeeSat: UInt64, serviceFeeSat: UInt64) - ) async -> UInt64? { - guard let fee = try? await estimateOrderFee(clientBalance, lspBalance) else { return nil } - return fee.networkFeeSat.saturatingAdd(fee.serviceFeeSat) - } - - /// Walks the affordable/over-budget bracket inward along the fee rate its two priced ends imply. - /// - /// A satoshi off the capacity only takes a fraction of a satoshi off the fee, so stepping down by - /// the shortfall would barely move; interpolating lands in a round or two. The invariant - /// `affordableFee <= headroom < overBudgetFee` keeps every candidate inside the bracket. - private func settleCapacity( - clientBalance: UInt64, - headroom: UInt64, - affordable: UInt64, - affordableFee: UInt64, - overBudget: UInt64, - overBudgetFee: UInt64, - estimateOrderFee: (_ clientBalance: UInt64, _ lspBalance: UInt64) async throws -> (networkFeeSat: UInt64, serviceFeeSat: UInt64) - ) async -> UInt64 { - var settled = affordable - var settledFee = affordableFee - var ceiling = overBudget - var ceilingFee = overBudgetFee - - for _ in 0 ..< Self.maxAffordabilityRounds { - let feeSpan = ceilingFee.saturatingSub(settledFee) - guard feeSpan > 0 else { return settled } - - let candidate = settled.saturatingAdd( - Self.scaledSpan( - span: ceiling.saturatingSub(settled), - numerator: headroom.saturatingSub(settledFee), - denominator: feeSpan - ) - ) - guard candidate > settled, - let candidateFee = await lspFeeQuote(clientBalance: clientBalance, lspBalance: candidate, estimateOrderFee: estimateOrderFee) - else { return settled } - - if candidateFee <= headroom { - settled = candidate - settledFee = candidateFee - } else { - ceiling = candidate - ceilingFee = candidateFee - } - } - - return settled - } - - /// `span * numerator / denominator` without overflowing the intermediate product. The caller's - /// bracket guarantees `numerator < denominator`; the guard keeps `dividingFullWidth` from - /// trapping if a misconfigured LSP breaks that. - private static func scaledSpan(span: UInt64, numerator: UInt64, denominator: UInt64) -> UInt64 { - guard denominator > 0 else { return 0 } - let product = span.multipliedFullWidth(by: numerator) - guard product.high < denominator else { return span } - return denominator.dividingFullWidth(product).quotient - } - - /// Backstop before a raised capacity is ordered. Like `canFundOrder`, only a quoted and - /// definitively unaffordable capacity is rejected. - func canFundAdvancedOrder( - clientBalance: UInt64, - receivingAmount: UInt64, - budget: UInt64?, - estimateOrderFee: (_ clientBalance: UInt64, _ lspBalance: UInt64) async throws -> (networkFeeSat: UInt64, serviceFeeSat: UInt64) - ) async -> Bool { - guard let budget else { - Logger.warn("Skipped capacity check for '\(receivingAmount)', no sized budget available", context: "TransferViewModel") - return true - } - guard let fee = try? await estimateOrderFee(clientBalance, receivingAmount) else { - Logger.warn("Skipped capacity check for '\(receivingAmount)', fee quote unavailable", context: "TransferViewModel") - return true - } - - let cost = clientBalance.saturatingAdd(fee.networkFeeSat.saturatingAdd(fee.serviceFeeSat)) - if cost > budget { - Logger.info("Priced capacity '\(receivingAmount)' at '\(cost)', over funding budget '\(budget)'", context: "TransferViewModel") - } - return cost <= budget - } - - /// The device's spendable balance, re-read at decision time. Never on-chain savings, which would - /// reject every hardware transfer. Nil without hardware capabilities, leaving the guards - /// non-blocking in previews and tests. - func hwFundingBudget(walletId: String) async -> UInt64? { - guard let hwSigner else { return nil } - return try? await hwSigner.availability(walletId: walletId).available - } - - /// Calculates the max amount transferable to spending and the value to display as "Available". - /// - /// The prospective client balance is clamped to the LSP's `maxClientBalanceSat` before - /// computing liquidity options: an on-chain balance larger than the LSP's max channel size - /// otherwise makes the liquidity calculation report `maxClientBalanceSat = 0` (the balance - /// already saturates the channel), collapsing the spendable amount to zero and stranding the - /// funds on-chain. - /// - /// - `transferValues`: liquidity options for a given client balance (prod: `calculateTransferValues`) - /// - `estimateOrderFee`: Blocktank order fee for a given client/LSP balance - func calculateSpendingLimits( - onchainAvailable: UInt64, - lspMaxClientBalance: UInt64?, - transferValues: (_ clientBalance: UInt64) -> TransferValues, - estimateOrderFee: (_ clientBalance: UInt64, _ lspBalance: UInt64) async throws -> (networkFeeSat: UInt64, serviceFeeSat: UInt64) - ) async rethrows -> (available: UInt64, max: UInt64) { - // First pass: estimate the LSP fee against the full on-chain balance. - let values1 = transferValues(onchainAvailable) - let lspBalance1 = max(values1.defaultLspBalance, values1.minLspBalance) - let fee1 = try await estimateOrderFee(onchainAvailable, lspBalance1) - let balanceAfterLspFee = onchainAvailable.saturatingSub(fee1.networkFeeSat.saturatingAdd(fee1.serviceFeeSat)) - - let cappedClientBalance: UInt64 = { - guard let cap = lspMaxClientBalance, cap > 0 else { return balanceAfterLspFee } - return min(balanceAfterLspFee, cap) - }() - - // Second pass with the clamped balance. - let values2 = transferValues(cappedClientBalance) - guard values2.maxClientBalance > 0 else { return (0, 0) } - let lspBalance2 = max(values2.defaultLspBalance, values2.minLspBalance) - let fee2 = try await estimateOrderFee(cappedClientBalance, lspBalance2) - - let affordable = await resolveAffordableClientBalance( - availableAmount: onchainAvailable, - quotedBalance: cappedClientBalance, - quotedFee: fee2.networkFeeSat.saturatingAdd(fee2.serviceFeeSat), - transferValues: transferValues, - estimateOrderFee: estimateOrderFee - ) - let result = min(values2.maxClientBalance, affordable) - return (result, result) - } - - /// Settles the advertised max on a client balance the LSP has actually priced. - /// - /// `availableAmount - fee` is a different balance from the one that fee priced, and the service - /// fee moves with the client/LSP split — up with the client balance in production, down on - /// staging and regtest — so an order built there can cost more than the wallet holds. Each round - /// re-quotes its own candidate. - private func resolveAffordableClientBalance( - availableAmount: UInt64, - quotedBalance: UInt64, - quotedFee: UInt64, - transferValues: (_ clientBalance: UInt64) -> TransferValues, - estimateOrderFee: (_ clientBalance: UInt64, _ lspBalance: UInt64) async throws -> (networkFeeSat: UInt64, serviceFeeSat: UInt64) - ) async -> UInt64 { - var candidate = quotedBalance - var fee = quotedFee - - for _ in 0 ..< Self.maxAffordabilityRounds { - if candidate.saturatingAdd(fee) <= availableAmount { - return candidate - } - candidate = availableAmount.saturatingSub(fee) - // Re-price against the split order creation will pick for this balance, not the earlier one. - let values = transferValues(candidate) - let lspBalance = max(values.defaultLspBalance, values.minLspBalance) - guard let requoted = await lspFeeQuote(clientBalance: candidate, lspBalance: lspBalance, estimateOrderFee: estimateOrderFee) else { - Logger.warn("Advertising unverified max '\(candidate)', fee quote unavailable", context: "TransferViewModel") - return candidate - } - fee = requoted - } - - if candidate.saturatingAdd(fee) <= availableAmount { - return candidate - } - let fallback = availableAmount.saturatingSub(fee) - Logger.warn( - "Max '\(candidate)' still over budget '\(availableAmount)' after \(Self.maxAffordabilityRounds) rounds, " - + "advertising unverified '\(fallback)'", - context: "TransferViewModel" - ) - return fallback - } - - /// Backstop before an order is created: re-quote the fee and confirm the funding source still - /// covers it and the balance. - /// - /// A missing budget or quote does not block — that would lock people out whenever the node is - /// briefly unready, and the confirm step stays the authority. Both are logged. - func canFundOrder( - clientBalance: UInt64, - budget: UInt64?, - transferValues: (_ clientBalance: UInt64) -> TransferValues, - estimateOrderFee: (_ clientBalance: UInt64, _ lspBalance: UInt64) async throws -> (networkFeeSat: UInt64, serviceFeeSat: UInt64) - ) async -> Bool { - guard let budget else { - Logger.warn("Skipped funding check for '\(clientBalance)', no sized budget available", context: "TransferViewModel") - return true - } - - let values = transferValues(clientBalance) - let lspBalance = max(values.defaultLspBalance, values.minLspBalance) - guard let fee = try? await estimateOrderFee(clientBalance, lspBalance) else { - Logger.warn("Skipped funding check for '\(clientBalance)', fee quote unavailable", context: "TransferViewModel") - return true - } - - let cost = clientBalance.saturatingAdd(fee.networkFeeSat.saturatingAdd(fee.serviceFeeSat)) - if cost > budget { - Logger.info("Priced amount '\(clientBalance)' at '\(cost)', over funding budget '\(budget)'", context: "TransferViewModel") - } - return cost <= budget - } - - /// Calculates max client balance accounting for LDK reserve requirement - func getMaxClientBalance(maxChannelSize: UInt64) -> UInt64 { - let minRemoteBalance = UInt64(Double(maxChannelSize) * 0.025) - return maxChannelSize - minRemoteBalance - } - // MARK: - Manual Channel Opening /// Opens a manual channel and tracks the transfer @@ -1670,50 +337,25 @@ class TransferViewModel: ObservableObject { try? await Task.sleep(nanoseconds: UInt64(retryInterval * 1_000_000_000)) } - Logger.info("Giving up on coop close. Checking if force close is possible.") - - // Check if any channels can be force closed (filter out trusted peers) - let (_, nonTrustedChannels) = lightningService.separateTrustedChannels(channelsToClose) - - if !nonTrustedChannels.isEmpty { - sheetViewModel.showSheet(.forceTransfer) - } else { - Logger.warn("All channels are with trusted peers. Cannot force close.") - channelsToClose.removeAll() - transferUnavailable = true - } + Logger.info("Giving up on coop close.") + sheetViewModel.showSheet(.forceTransfer) } } /// Force close all channels that failed to cooperatively close - /// Returns the number of trusted peer channels that were skipped - func forceCloseChannel() async throws -> Int { + func forceCloseChannel() async throws { guard !channelsToClose.isEmpty else { Logger.warn("No channels to force close") - return 0 - } - - // Filter out trusted peer channels (cannot force close LSP channels) - let (trustedChannels, nonTrustedChannels) = lightningService.separateTrustedChannels(channelsToClose) - - if !trustedChannels.isEmpty { - Logger.warn("Skipping \(trustedChannels.count) trusted peer channel(s)") - } - - guard !nonTrustedChannels.isEmpty else { - channelsToClose.removeAll() - throw AppError( - message: "Cannot force close channels with trusted peer", - debugMessage: "All channels are with trusted peers (LSP). Force close is disabled." - ) + return } - Logger.info("Force closing \(nonTrustedChannels.count) channel(s)") + let channels = channelsToClose + Logger.info("Force closing \(channels.count) channel(s)") var errors: [(channelId: String, error: Error)] = [] var successfulChannels: [ChannelDetails] = [] - for channel in nonTrustedChannels { + for channel in channels { do { // Force close the channel first try await lightningService.closeChannel( @@ -1745,10 +387,8 @@ class TransferViewModel: ObservableObject { } } - // Remove successfully closed channels and trusted peer channels from the list channelsToClose.removeAll { channel in - successfulChannels.contains { $0.channelId == channel.channelId } || - trustedChannels.contains { $0.channelId == channel.channelId } + successfulChannels.contains { $0.channelId == channel.channelId } } try? await transferService.syncTransferStates() @@ -1763,8 +403,6 @@ class TransferViewModel: ObservableObject { debugMessage: errorMessages ) } - - return trustedChannels.count } // MARK: - Savings Swap (Boltz reverse swap) @@ -2089,8 +727,3 @@ actor ChannelPendingCapture { return channelData } } - -// MARK: - Hardware transfer capability conformances - -extension HwWalletManager: HwTransferFunding {} -extension HwWalletManager: HwTransferConnecting {} diff --git a/Bitkit/ViewModels/WalletViewModel.swift b/Bitkit/ViewModels/WalletViewModel.swift index 5e9b859c8..1d30a3e4c 100644 --- a/Bitkit/ViewModels/WalletViewModel.swift +++ b/Bitkit/ViewModels/WalletViewModel.swift @@ -96,14 +96,6 @@ class WalletViewModel: ObservableObject { private let feeEstimatesManager: FeeEstimatesManager private let pubkyProfile: PubkyProfileManager? - enum BlocktankPeerSimulation: String, CaseIterable { - case none = "None" - case apiFailure = "API Failure" - case unreachablePeers = "Unreachable Peers" - } - - static var peerSimulation: BlocktankPeerSimulation = .none - @Published var isRestoringWallet = false @Published var balanceInTransferToSavings: Int = 0 @Published var balanceInTransferToSpending: Int = 0 @@ -141,7 +133,7 @@ class WalletViewModel: ObservableObject { Task { do { _ = try await onchainAttemptService.resumeAcceptedTransfer( - walletId: OnchainSendAttemptService.walletId(index: lightningService.currentWalletIndex), using: transferService + walletId: OnchainSendAttemptService.walletId(index: lightningService.currentWalletIndex) ) } catch { Logger.warn("Accepted order local follow-up remains guarded: \(error)", context: "WalletViewModel") @@ -152,7 +144,7 @@ class WalletViewModel: ObservableObject { do { guard try await onchainAttemptService.observeTransaction(txid: txid, walletId: walletId, isConfirmed: isConfirmed) else { return } _ = try await onchainAttemptService.resumeAcceptedOrdinarySend(walletId: walletId, observedTxid: txid) - _ = try await onchainAttemptService.resumeAcceptedTransfer(walletId: walletId, using: transferService) + _ = try await onchainAttemptService.resumeAcceptedTransfer(walletId: walletId) await PaykitPaymentProofService.shared.reconcile() } catch { Logger.warn("Observed payment local follow-up remains guarded: \(error)", context: "WalletViewModel") @@ -165,8 +157,7 @@ class WalletViewModel: ObservableObject { /// Convenience initializer for previews and testing convenience init(pubkyProfile: PubkyProfileManager? = nil, usdtWallet: UsdtWalletManager? = nil) { let transferService = TransferService( - lightningService: .shared, - blocktankService: CoreService.shared.blocktank + lightningService: .shared ) self.init( transferService: transferService, sheetViewModel: SheetViewModel(), @@ -270,7 +261,6 @@ class WalletViewModel: ObservableObject { Task { await self.refreshAndSyncState() try? await self.refreshBip21(reportsError: false) - await self.reconnectTrustedPeers() await self.refreshPaykitEndpointsAfterChannelAvailabilityChanged(reason: "channel-ready refresh") try? await Task.sleep(nanoseconds: Self.paykitChannelUsabilityRefreshDelay) guard !Task.isCancelled else { return } @@ -362,7 +352,11 @@ class WalletViewModel: ObservableObject { syncState() - await reconnectTrustedPeers() + do { + try await lightningService.disconnectIdlePeers() + } catch { + Logger.error("Failed to disconnect idle peers: \(error)") + } // Migration only: fetch peers from remote backup (once) and persist in ldk-node let peerUris = await MigrationsService.shared.tryFetchMigrationPeersFromBackup(walletIndex: walletIndex) @@ -422,54 +416,6 @@ class WalletViewModel: ObservableObject { } } - private func fetchTrustedPeersFromBlocktank() async -> [LnPeer]? { - switch Self.peerSimulation { - case .apiFailure: - Logger.warn("⚠️ [DEBUG] Simulating Blocktank API failure — returning nil") - return nil - case .unreachablePeers: - Logger.warn("⚠️ [DEBUG] Simulating unreachable API peers") - return [ - LnPeer( - nodeId: "000000000000000000000000000000000000000000000000000000000000000001", - host: "192.0.2.1", - port: 9735 - ), - ] - case .none: - break - } - - var info: IBtInfo? - do { - info = try await coreService.blocktank.info(refresh: true) - } catch { - Logger.warn("Blocktank API refresh failed, trying cache: \(error)") - } - if info == nil { - info = try? await coreService.blocktank.info(refresh: false) - } - guard let nodes = info?.nodes, !nodes.isEmpty else { return nil } - let peers = nodes.compactMap { node -> LnPeer? in - guard let connString = node.connectionStrings.first else { return nil } - let address = connString.contains("@") ? String(connString.split(separator: "@").last ?? "") : connString - let parts = address.split(separator: ":") - guard parts.count == 2, let port = UInt16(parts[1]) else { return nil } - return LnPeer(nodeId: node.pubkey, host: String(parts[0]), port: port) - } - Logger.info("Fetched \(peers.count) trusted peers from Blocktank API") - return peers.isEmpty ? nil : peers - } - - func reconnectTrustedPeers() async { - do { - let remotePeers = await fetchTrustedPeersFromBlocktank() - try await lightningService.connectToTrustedPeers(remotePeers: remotePeers) - } catch { - Logger.error("Failed to connect to trusted peers") - } - } - func stopLightningNode(clearEventCallback: Bool = false) async throws { nodeLifecycleState = .stopping // Stop the swap updates stream with the node; it restarts on the next wallet start. @@ -692,14 +638,10 @@ class WalletViewModel: ObservableObject { return result } - func resumeAcceptedOnchainTransfer(walletId: String, attempts: OnchainSendAttemptService) async throws -> Bool { - try await attempts.resumeAcceptedTransfer(walletId: walletId, using: transferService) - } - func resolvedAcceptedOnchainTransfer(context: OnchainSendPendingContext, attempts: OnchainSendAttemptService) async throws -> OnchainSendLocalResolution? { - try await attempts.resolvedAcceptedTransfer(context: context, using: transferService) + try await attempts.resolvedAcceptedTransfer(context: context) } /// Sets the fee rate for the send flow @@ -1294,7 +1236,6 @@ class WalletViewModel: ObservableObject { await lightningService.refreshCache() do { - await MigrationsService.shared.cleanupInvalidMigrationTransfers() try? await transferService.syncTransferStates() let state = try await balanceManager.deriveBalanceState() balanceInTransferToSavings = Int(state.balanceInTransferToSavings) @@ -1355,10 +1296,6 @@ class WalletViewModel: ObservableObject { return channels?.contains(where: \.isChannelReady) ?? false } - var hasExistingChannels: Bool { - channelCount > 0 || channels?.isEmpty == false - } - /// Returns true if there's at least one usable channel (ready AND peer connected) var hasUsableChannels: Bool { return channels?.contains(where: \.isUsable) ?? false diff --git a/Bitkit/Views/Gift/GiftError.swift b/Bitkit/Views/Gift/GiftError.swift deleted file mode 100644 index 111dbedfa..000000000 --- a/Bitkit/Views/Gift/GiftError.swift +++ /dev/null @@ -1,38 +0,0 @@ -import SwiftUI - -struct GiftFailed: View { - @Binding var navigationPath: [GiftRoute] - @EnvironmentObject private var sheets: SheetViewModel - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - SheetHeader(title: t("other__gift__error__title")) - - VStack(spacing: 0) { - BodyMText(t("other__gift__error__text")) - .frame(maxWidth: .infinity, alignment: .leading) - - Spacer() - - Image("exclamation-mark") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: UIScreen.main.bounds.width * 0.8) - .frame(maxHeight: 256) - - Spacer() - } - - CustomButton( - title: t("common__ok") - ) { - sheets.hideSheet() - } - } - .navigationBarHidden(true) - .allowSwipeBack(false) - .padding(.horizontal, 16) - .sheetBackground() - .accessibilityIdentifier("GiftError") - } -} diff --git a/Bitkit/Views/Gift/GiftLoading.swift b/Bitkit/Views/Gift/GiftLoading.swift deleted file mode 100644 index bc7c04a83..000000000 --- a/Bitkit/Views/Gift/GiftLoading.swift +++ /dev/null @@ -1,149 +0,0 @@ -import BitkitCore -import SwiftUI - -struct GiftLoading: View { - @Binding var navigationPath: [GiftRoute] - @EnvironmentObject private var sheets: SheetViewModel - @EnvironmentObject private var wallet: WalletViewModel - - let code: String - let amount: Int - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - SheetHeader(title: t("other__gift__claiming__title")) - - VStack(spacing: 0) { - MoneyStack(sats: amount, showSymbol: true) - - BodyMText(t("other__gift__claiming__text")) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.top, 32) - - Spacer() - - Image("gift-figure") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: UIScreen.main.bounds.width * 0.8) - .frame(maxHeight: 256) - - Spacer() - - ActivityIndicator() - .padding(.bottom, 32) - } - } - .padding(.horizontal, 16) - .accessibilityIdentifier("GiftLoading") - .task { - await claimGift() - } - } - - private func claimGift() async { - // Wait for peers to be connected (equivalent to waitForLdkPeers) - await waitForPeers() - - let maxInboundCapacity = wallet.totalInboundLightningSats ?? 0 - - Logger.debug("Max inbound capacity: \(maxInboundCapacity)") - - if maxInboundCapacity >= UInt64(amount) { - // User has sufficient inbound capacity, use existing channels - await claimWithLiquidity() - } else { - // User needs new channel, create order - await claimWithoutLiquidity() - } - } - - private func waitForPeers() async { - // Wait a bit for peers to connect if node is starting - guard wallet.nodeLifecycleState == .running else { - // Wait for node to be running - await wallet.waitForNodeToRun(timeoutSeconds: 30.0) - return - } - - // Give some time for peer connections - try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds - } - - private func claimWithLiquidity() async { - do { - // Create a zero amount lightning invoice - let invoice = try await wallet.createInvoice(note: "blocktank-gift-code:\(code)") - _ = try await giftPay(invoice: invoice) - } catch { - handleGiftClaimError(error) - } - } - - private func claimWithoutLiquidity() async { - do { - guard let nodeId = LightningService.shared.nodeId else { - throw AppError(serviceError: .nodeNotStarted) - } - - // Create an order for the gift using BitkitCore - let order = try await giftOrder(clientNodeId: nodeId, code: "blocktank-gift-code:\(code)") - - guard let orderId = order.orderId else { - throw AppError(message: "Order ID is nil", debugMessage: "Order ID is nil") - } - - // Open the channel - let openedOrder = try await CoreService.shared.blocktank.open(orderId: orderId) - - let nowTimestamp = UInt64(Date().timeIntervalSince1970) - - // Create activity item for the received gift - let lightningActivity = LightningActivity( - walletId: WalletScope.default, - id: openedOrder.channel?.fundingTx.id ?? orderId, - txType: .received, - status: .succeeded, - value: UInt64(amount), - fee: 0, - // invoice can't be empty, so use the invoice from the order - invoice: openedOrder.payment?.bolt11Invoice?.request ?? "", - message: code, - timestamp: nowTimestamp, - preimage: nil, - contact: nil, - createdAt: nowTimestamp, - updatedAt: nil, - seenAt: nil - ) - - // Add to activity list - try await CoreService.shared.activity.insert(.lightning(lightningActivity)) - - // Mark the activity as seen before showing the sheet - await CoreService.shared.activity.markActivityAsSeen(id: lightningActivity.id) - - // Trigger haptic feedback - Haptics.notify(.success) - - // Close gift sheet and show received transaction sheet - sheets.showSheet(.receivedTx, data: ReceivedTxSheetDetails(type: .lightning, sats: UInt64(amount))) - } catch { - handleGiftClaimError(error) - } - } - - private func handleGiftClaimError(_ error: Error) { - Logger.error("Gift claim failed: \(error)") - - // Check the full error description for gift code error types - let errorDescription = String(describing: error) - if errorDescription.contains("GIFT_CODE_ALREADY_USED") { - navigationPath.append(.used) - } else if errorDescription.contains("GIFT_CODE_USED_UP") { - navigationPath.append(.usedUp) - } else { - navigationPath.append(.failed) - } - } -} diff --git a/Bitkit/Views/Gift/GiftUsed.swift b/Bitkit/Views/Gift/GiftUsed.swift deleted file mode 100644 index e287c051e..000000000 --- a/Bitkit/Views/Gift/GiftUsed.swift +++ /dev/null @@ -1,38 +0,0 @@ -import SwiftUI - -struct GiftUsed: View { - @Binding var navigationPath: [GiftRoute] - @EnvironmentObject private var sheets: SheetViewModel - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - SheetHeader(title: t("other__gift__used__title")) - - VStack(spacing: 0) { - BodyMText(t("other__gift__used__text")) - .frame(maxWidth: .infinity, alignment: .leading) - - Spacer() - - Image("exclamation-mark") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: UIScreen.main.bounds.width * 0.8) - .frame(maxHeight: 256) - - Spacer() - } - - CustomButton( - title: t("common__ok") - ) { - sheets.hideSheet() - } - } - .navigationBarHidden(true) - .allowSwipeBack(false) - .padding(.horizontal, 16) - .sheetBackground() - .accessibilityIdentifier("GiftUsed") - } -} diff --git a/Bitkit/Views/Gift/GiftUsedUp.swift b/Bitkit/Views/Gift/GiftUsedUp.swift deleted file mode 100644 index a0b521df2..000000000 --- a/Bitkit/Views/Gift/GiftUsedUp.swift +++ /dev/null @@ -1,38 +0,0 @@ -import SwiftUI - -struct GiftUsedUp: View { - @Binding var navigationPath: [GiftRoute] - @EnvironmentObject private var sheets: SheetViewModel - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - SheetHeader(title: t("Out of Gifts")) - - VStack(spacing: 0) { - BodyMText(t("other__gift__claimed__text")) - .frame(maxWidth: .infinity, alignment: .leading) - - Spacer() - - Image("exclamation-mark") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: UIScreen.main.bounds.width * 0.8) - .frame(maxHeight: 256) - - Spacer() - } - - CustomButton( - title: t("common__ok") - ) { - sheets.hideSheet() - } - } - .navigationBarHidden(true) - .allowSwipeBack(false) - .padding(.horizontal, 16) - .sheetBackground() - .accessibilityIdentifier("GiftUsedUp") - } -} diff --git a/Bitkit/Views/Onboarding/OnboardingSlider.swift b/Bitkit/Views/Onboarding/OnboardingSlider.swift index 718fdfa16..26afa33ad 100644 --- a/Bitkit/Views/Onboarding/OnboardingSlider.swift +++ b/Bitkit/Views/Onboarding/OnboardingSlider.swift @@ -59,7 +59,6 @@ struct OnboardingSlider: View { imageName: "lightning", title: t("onboarding__slide1_header"), text: t("onboarding__slide1_text_alt"), - disclaimerText: GeoService.shared.isGeoBlocked ? t("onboarding__slide1_note") : nil, accentColor: .purpleAccent ) .tag(1) diff --git a/Bitkit/Views/Settings/Advanced/CloseConnectionConfirmation.swift b/Bitkit/Views/Settings/Advanced/CloseConnectionConfirmation.swift index 11ee63fea..f6071d3ab 100644 --- a/Bitkit/Views/Settings/Advanced/CloseConnectionConfirmation.swift +++ b/Bitkit/Views/Settings/Advanced/CloseConnectionConfirmation.swift @@ -70,30 +70,17 @@ struct CloseConnectionConfirmation: View { DispatchQueue.main.async { dismiss() - // Check if failed channels are trusted peers (cannot force close) - let (_, nonTrustedFailedChannels) = LightningService.shared.separateTrustedChannels(failedChannels) - - if !nonTrustedFailedChannels.isEmpty { - // Show error toast - app.toast( - type: .error, - title: t("lightning__close_error"), - description: t("lightning__close_error_msg") - ) - - // Store the failed non-trusted channels for force close - transfer.channelsToClose = nonTrustedFailedChannels - - // Show force transfer sheet - sheets.showSheet(.forceTransfer) - } else { - // All failed channels are trusted peers - cannot force close - app.toast( - type: .error, - title: t("lightning__close_error"), - description: t("lightning__close_error_msg") - ) - } + app.toast( + type: .error, + title: t("lightning__close_error"), + description: t("lightning__close_error_msg") + ) + + // Store the failed channels for force close + transfer.channelsToClose = failedChannels + + // Show force transfer sheet + sheets.showSheet(.forceTransfer) } } } catch { @@ -103,31 +90,17 @@ struct CloseConnectionConfirmation: View { DispatchQueue.main.async { dismiss() - // Check if channel is a trusted peer (cannot force close) - let (trustedChannels, _) = LightningService.shared.separateTrustedChannels([channel]) - let isTrustedPeer = !trustedChannels.isEmpty + app.toast( + type: .error, + title: t("lightning__close_error"), + description: error.localizedDescription + ) - if isTrustedPeer { - // Cannot force close trusted peer channel - app.toast( - type: .error, - title: t("lightning__close_error"), - description: t("lightning__close_error_msg") - ) - } else { - // Show error toast - app.toast( - type: .error, - title: t("lightning__close_error"), - description: error.localizedDescription - ) - - // Store the channel for force close - transfer.channelsToClose = [channel] + // Store the channel for force close + transfer.channelsToClose = [channel] - // Show force transfer sheet - sheets.showSheet(.forceTransfer) - } + // Show force transfer sheet + sheets.showSheet(.forceTransfer) } } diff --git a/Bitkit/Views/Settings/ChannelOrders.swift b/Bitkit/Views/Settings/ChannelOrders.swift index da6fc19a9..627436013 100644 --- a/Bitkit/Views/Settings/ChannelOrders.swift +++ b/Bitkit/Views/Settings/ChannelOrders.swift @@ -223,22 +223,6 @@ struct OrderDetailView: View { DetailRow(label: "Created", value: order.createdAt) DetailRow(label: "Updated", value: order.updatedAt) } - - if order.state2 == .paid { - Button("Open Channel") { - Task { - Logger.info("Opening channel for order \(order.id)") - - do { - try await CoreService.shared.blocktank.open(orderId: order.id) - Logger.info("Channel opened for order \(order.id)") - } catch { - Logger.error("Error opening channel for order \(order.id): \(error)") - LightningService.shared.dumpLdkLogs() - } - } - } - } } .navigationTitle("Order Details") } diff --git a/Bitkit/Views/Settings/DevSettingsView.swift b/Bitkit/Views/Settings/DevSettingsView.swift index 0c2510135..6e0562b15 100644 --- a/Bitkit/Views/Settings/DevSettingsView.swift +++ b/Bitkit/Views/Settings/DevSettingsView.swift @@ -10,7 +10,6 @@ struct DevSettingsView: View { @EnvironmentObject var app: AppViewModel @EnvironmentObject var activity: ActivityListViewModel @EnvironmentObject var feeEstimatesManager: FeeEstimatesManager - @EnvironmentObject var notificationManager: PushNotificationManager @EnvironmentObject var pubkyProfile: PubkyProfileManager @EnvironmentObject var session: SessionManager @EnvironmentObject var wallet: WalletViewModel @@ -178,23 +177,6 @@ struct DevSettingsView: View { SettingsRow(title: "Export Logs", rightIcon: nil) } - Button { - Task { - do { - try await notificationManager.sendTestNotification() - } catch { - Logger.error(error, context: "failed to test push notification") - app.toast( - type: .error, - title: "Error", - description: "Failed to send test notification: \(error)" - ) - } - } - } label: { - SettingsRow(title: "Test Push Notification", rightIcon: nil) - } - Button { fatalError("Simulate Crash") } label: { diff --git a/Bitkit/Views/Settings/LdkDebugScreen.swift b/Bitkit/Views/Settings/LdkDebugScreen.swift index e1076b378..f33374694 100644 --- a/Bitkit/Views/Settings/LdkDebugScreen.swift +++ b/Bitkit/Views/Settings/LdkDebugScreen.swift @@ -66,21 +66,6 @@ struct LdkDebugScreen: View { } } } - - // Peer Simulation - VStack(alignment: .leading, spacing: 8) { - SettingsSectionHeader("Peer Simulation") - - Picker("Peer Simulation", selection: Binding( - get: { WalletViewModel.peerSimulation }, - set: { WalletViewModel.peerSimulation = $0 } - )) { - ForEach(WalletViewModel.BlocktankPeerSimulation.allCases, id: \.self) { mode in - Text(mode.rawValue).tag(mode) - } - } - .pickerStyle(.segmented) - } } } } @@ -124,7 +109,6 @@ struct LdkDebugScreen: View { isRestartingNode = true let lightningService = LightningService.shared try await lightningService.restart() - await wallet.reconnectTrustedPeers() app.toast(type: .success, title: "Node Restarted", description: "Node restarted successfully") } catch { Logger.error("Failed to restart node: \(error)") diff --git a/Bitkit/Views/Settings/Notifications/NotificationsSettings.swift b/Bitkit/Views/Settings/Notifications/NotificationsSettings.swift index c6aec03c0..a8ffd9bdd 100644 --- a/Bitkit/Views/Settings/Notifications/NotificationsSettings.swift +++ b/Bitkit/Views/Settings/Notifications/NotificationsSettings.swift @@ -44,19 +44,9 @@ struct NotificationsSettings: View { .padding(.top, 16) } - NotificationPreview(disabled: !settings.enableNotifications, enableAmount: settings.enableNotificationsAmount) + NotificationPreview(disabled: !settings.enableNotifications) .padding(.top, 16) - if !isDenied { - CaptionMText(t("settings__notifications__settings__privacy__label")) - .padding(.top, 32) - - SettingsRow( - title: t("settings__notifications__settings__privacy__text"), - toggle: $settings.enableNotificationsAmount - ) - } - CaptionMText(t("settings__notifications__settings__notifications__label")) .padding(.top, 32) diff --git a/Bitkit/Views/Sheets/ForceTransferSheet.swift b/Bitkit/Views/Sheets/ForceTransferSheet.swift index 32b373e23..6381a16d5 100644 --- a/Bitkit/Views/Sheets/ForceTransferSheet.swift +++ b/Bitkit/Views/Sheets/ForceTransferSheet.swift @@ -42,13 +42,9 @@ struct ForceTransferSheet: View { Task { @MainActor in do { - let skippedCount = try await transfer.forceCloseChannel() + try await transfer.forceCloseChannel() sheets.hideSheet() - - let description = skippedCount > 0 - ? "\(t("lightning__force_init_msg")) \(t("lightning__force_channels_skipped"))" - : t("lightning__force_init_msg") - app.toast(type: .success, title: t("lightning__force_init_title"), description: description) + app.toast(type: .success, title: t("lightning__force_init_title"), description: t("lightning__force_init_msg")) } catch { Logger.error("Force transfer failed", context: error.localizedDescription) app.toast( diff --git a/Bitkit/Views/Sheets/GiftSheet.swift b/Bitkit/Views/Sheets/GiftSheet.swift deleted file mode 100644 index 5e79083d8..000000000 --- a/Bitkit/Views/Sheets/GiftSheet.swift +++ /dev/null @@ -1,46 +0,0 @@ -import SwiftUI - -enum GiftRoute: Hashable { - case loading(code: String, amount: Int) - case used - case usedUp - case failed -} - -struct GiftConfig { - let code: String - let amount: Int -} - -struct GiftSheetItem: SheetItem { - let id: SheetID = .gift - let size: SheetSize = .large - let code: String - let amount: Int -} - -struct GiftSheet: View { - @EnvironmentObject private var sheets: SheetViewModel - @State private var navigationPath: [GiftRoute] = [] - let config: GiftSheetItem - - var body: some View { - Sheet(id: .gift, data: config) { - NavigationStack(path: $navigationPath) { - GiftLoading(navigationPath: $navigationPath, code: config.code, amount: config.amount) - .navigationDestination(for: GiftRoute.self) { route in - switch route { - case .loading: - GiftLoading(navigationPath: $navigationPath, code: config.code, amount: config.amount) - case .used: - GiftUsed(navigationPath: $navigationPath) - case .usedUp: - GiftUsedUp(navigationPath: $navigationPath) - case .failed: - GiftFailed(navigationPath: $navigationPath) - } - } - } - } - } -} diff --git a/Bitkit/Views/Sheets/TransferUnavailableSheet.swift b/Bitkit/Views/Sheets/TransferUnavailableSheet.swift new file mode 100644 index 000000000..ab722797c --- /dev/null +++ b/Bitkit/Views/Sheets/TransferUnavailableSheet.swift @@ -0,0 +1,54 @@ +import SwiftUI + +struct TransferUnavailableSheetItem: SheetItem { + let id: SheetID = .transferUnavailable + let size: SheetSize = .large +} + +/// Shown instead of the Blocktank transfer to spending, which is no longer available. +struct TransferUnavailableSheet: View { + @EnvironmentObject private var sheets: SheetViewModel + @EnvironmentObject private var navigation: NavigationViewModel + let config: TransferUnavailableSheetItem + + var body: some View { + Sheet(id: .transferUnavailable, data: config) { + TransferUnavailableContent( + navTitle: t("lightning__transfer_unavailable__nav_title"), + description: t("lightning__transfer_unavailable__text") + ) { + sheets.hideSheet() + navigation.navigate(.fundManual(nodeUri: nil)) + } + } + } +} + +struct TransferUnavailableContent: View { + let navTitle: String + let description: String + var showBackButton = false + let onContinue: () -> Void + + var body: some View { + SheetIntro( + navTitle: navTitle, + title: t("lightning__transfer_unavailable__title"), + description: description, + image: "lightning", + continueText: t("common__continue"), + cancelText: t("common__learn_more"), + accentColor: .purpleAccent, + testID: "TransferUnavailable", + cancelTestID: "TransferUnavailableLearnMore", + continueTestID: "TransferUnavailableContinue", + showBackButton: showBackButton, + onCancel: { + if let url = URL(string: Env.blocktankSunsetUrl) { + UIApplication.shared.open(url) + } + }, + onContinue: onContinue + ) + } +} diff --git a/Bitkit/Views/Subscriptions/AllowancesView.swift b/Bitkit/Views/Subscriptions/AllowancesView.swift index e3f370873..8a2c471c6 100644 --- a/Bitkit/Views/Subscriptions/AllowancesView.swift +++ b/Bitkit/Views/Subscriptions/AllowancesView.swift @@ -678,6 +678,7 @@ struct AllowanceReviewView: View { do { if accept { try await allowances.accept(entry) + PushNotificationManager.shared.requestPermissionIfUndecided() } else { try await allowances.reject(entry) } diff --git a/Bitkit/Views/Subscriptions/CreateSubscriptionView.swift b/Bitkit/Views/Subscriptions/CreateSubscriptionView.swift index d27f63657..edb0a4220 100644 --- a/Bitkit/Views/Subscriptions/CreateSubscriptionView.swift +++ b/Bitkit/Views/Subscriptions/CreateSubscriptionView.swift @@ -329,6 +329,7 @@ struct SubscriptionRecipientView: View { do { let subscription = try await paymentRequests.proposeSubscription(draft, to: selectedTarget) guard paymentRequests.subscriptions.contains(where: { $0.id == subscription.id }) else { return } + PushNotificationManager.shared.requestPermissionIfUndecided() onSent(subscription) } catch { app.toast(error) diff --git a/Bitkit/Views/Subscriptions/SubscriptionsView.swift b/Bitkit/Views/Subscriptions/SubscriptionsView.swift index 90612ff6a..0bbc06cfd 100644 --- a/Bitkit/Views/Subscriptions/SubscriptionsView.swift +++ b/Bitkit/Views/Subscriptions/SubscriptionsView.swift @@ -719,7 +719,9 @@ struct SubscriptionSheet: View { isAccepting = true defer { isAccepting = false } - guard let dueRequest = try await paymentRequests.accept(subscription) else { + let dueRequest = try await paymentRequests.accept(subscription) + PushNotificationManager.shared.requestPermissionIfUndecided() + guard let dueRequest else { guard paymentRequests.subscriptions.contains(where: { $0.id == subscription.id && $0.lifecycleState == .activeRecurring }) else { diff --git a/Bitkit/Views/Transfer/FundAdvancedOptions.swift b/Bitkit/Views/Transfer/FundAdvancedOptions.swift deleted file mode 100644 index a9719ace2..000000000 --- a/Bitkit/Views/Transfer/FundAdvancedOptions.swift +++ /dev/null @@ -1,55 +0,0 @@ -import SwiftUI - -struct FundAdvancedOptions: View { - @EnvironmentObject private var navigation: NavigationViewModel - - var body: some View { - VStack(spacing: 0) { - NavigationBar(title: t("lightning__funding_advanced__nav_title")) - .padding(.bottom, 16) - - ScrollView(showsIndicators: false) { - VStack(spacing: 0) { - DisplayText( - t("lightning__funding_advanced__title"), - accentColor: .purpleAccent - ) - .padding(.bottom, 8) - - BodyMText(t("lightning__funding_advanced__text")) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.bottom, 32) - - VStack(spacing: 8) { - RectangleButton( - icon: "scan", - title: t("lightning__funding_advanced__button1"), - testID: "FundLnUrl" - ) { - navigation.navigate(.scanner) - } - - RectangleButton( - icon: "pencil", - title: t("lightning__funding_advanced__button2"), - testID: "FundManual" - ) { - navigation.navigate(.fundManual(nodeUri: nil)) - } - } - - Spacer() - } - } - } - .navigationBarHidden(true) - .padding(.horizontal, 16) - } -} - -#Preview { - NavigationStack { - FundAdvancedOptions() - .preferredColorScheme(.dark) - } -} diff --git a/Bitkit/Views/Transfer/FundManualSetupView.swift b/Bitkit/Views/Transfer/FundManualSetupView.swift index 1cf60dabe..104bb2878 100644 --- a/Bitkit/Views/Transfer/FundManualSetupView.swift +++ b/Bitkit/Views/Transfer/FundManualSetupView.swift @@ -20,7 +20,6 @@ struct FundManualSetupView: View { self.initialNodeUri = initialNodeUri } - /// Test URI: 028a8910b0048630d4eb17af25668cdd7ea6f2d8ae20956e7a06e2ae46ebcb69fc@34.65.86.104:9400 func pasteLightningNodeUri() { guard let pastedText = UIPasteboard.general.string?.trimmingCharacters(in: .whitespacesAndNewlines) else { alertTitle = t("wallet__send_clipboard_empty_title") diff --git a/Bitkit/Views/Transfer/FundReceiveView.swift b/Bitkit/Views/Transfer/FundReceiveView.swift deleted file mode 100644 index bd3b0382d..000000000 --- a/Bitkit/Views/Transfer/FundReceiveView.swift +++ /dev/null @@ -1,20 +0,0 @@ -import SwiftUI - -struct FundReceiveView: View { - @EnvironmentObject var wallet: WalletViewModel - @EnvironmentObject var app: AppViewModel - - var body: some View { - Text("FundReceiveView") - .navigationBarTitleDisplayMode(.inline) - } -} - -#Preview { - NavigationStack { - FundReceiveView() - .environmentObject(WalletViewModel()) - .environmentObject(AppViewModel()) - .preferredColorScheme(.dark) - } -} diff --git a/Bitkit/Views/Transfer/FundingOptions.swift b/Bitkit/Views/Transfer/FundingOptions.swift index 9b12354c0..2cf7144c6 100644 --- a/Bitkit/Views/Transfer/FundingOptions.swift +++ b/Bitkit/Views/Transfer/FundingOptions.swift @@ -1,24 +1,11 @@ import SwiftUI struct FundingOptions: View { - @EnvironmentObject var app: AppViewModel @EnvironmentObject var navigation: NavigationViewModel - @EnvironmentObject var sheets: SheetViewModel - @EnvironmentObject var wallet: WalletViewModel - - @State private var showNoFundsAlert = false - - var text: String { - if GeoService.shared.isGeoBlocked { - return t("lightning__funding__text_blocked") - } else { - return t("lightning__funding__text") - } - } var body: some View { VStack(spacing: 0) { - NavigationBar(title: t("lightning__transfer__nav_title")) + NavigationBar(title: t("lightning__funding__nav_title")) .padding(.bottom, 16) DisplayText( @@ -27,36 +14,11 @@ struct FundingOptions: View { ) .padding(.bottom, 8) - BodyMText(text) + BodyMText(t("lightning__transfer_unavailable__text")) .frame(maxWidth: .infinity, alignment: .leading) .padding(.bottom, 32) VStack(spacing: 8) { - RectangleButton( - icon: "transfer", - title: t("lightning__funding__button1"), - isDisabled: GeoService.shared.isGeoBlocked, - testID: "FundTransfer" - ) { - if wallet.totalOnchainSats == 0 { - showNoFundsAlert = true - } else if app.hasSeenTransferToSpendingIntro { - navigation.navigate(.spendingAmount) - } else { - navigation.navigate(.spendingIntro) - } - } - - RectangleButton( - icon: "qr", - title: t("lightning__funding__button2"), - isDisabled: GeoService.shared.isGeoBlocked, - testID: "FundReceive" - ) { - navigation.reset() - sheets.showSheet(.receive, data: ReceiveConfig(view: .cjitAmount)) - } - RectangleButton( icon: "pencil", title: t("lightning__funding__button3"), @@ -64,27 +26,28 @@ struct FundingOptions: View { ) { navigation.navigate(.fundManual(nodeUri: nil)) } + + RectangleButton(icon: "transfer", title: t("lightning__funding__button1"), isDisabled: true, testID: "FundTransfer") {} + RectangleButton( + icon: "device-tablet-speaker", + title: t("lightning__funding__button_hw"), + isDisabled: true, + testID: "FundHardware" + ) {} + RectangleButton(icon: "qr", title: t("lightning__funding__button2"), isDisabled: true, testID: "FundReceive") {} } Spacer() + + CustomButton(title: t("common__learn_more"), variant: .secondary) { + if let url = URL(string: Env.blocktankSunsetUrl) { + UIApplication.shared.open(url) + } + } + .accessibilityIdentifier("FundLearnMore") + .padding(.bottom, 16) } .navigationBarHidden(true) .padding(.horizontal, 16) - .alert( - t("lightning__no_funds__title"), - isPresented: $showNoFundsAlert - ) { - Button(t("lightning__no_funds__fund")) { - navigation.reset() - sheets.showSheet(.receive, data: ReceiveConfig(view: .qr(cjitInvoice: nil, tab: .savings))) - } - Button(t("common__cancel"), role: .cancel) {} - } message: { - Text(t("lightning__no_funds__description")) - } - .tint(.purpleAccent) - .task { - await app.checkGeoStatus() - } } } diff --git a/Bitkit/Views/Transfer/Hardware/SpendingAmountHw.swift b/Bitkit/Views/Transfer/Hardware/SpendingAmountHw.swift deleted file mode 100644 index 9175f9a46..000000000 --- a/Bitkit/Views/Transfer/Hardware/SpendingAmountHw.swift +++ /dev/null @@ -1,221 +0,0 @@ -import BitkitCore -import LDKNode -import SwiftUI - -/// Amount entry for a transfer to spending funded from a paired hardware wallet. Mirrors -/// `SpendingAmount`, but the available/MAX/quarter limits come from the device's native-segwit -/// balance via `TransferViewModel.updateHwLimits`, and Continue advances to the on-device Sign step. -struct SpendingAmountHw: View { - let walletId: String - - @EnvironmentObject var app: AppViewModel - @EnvironmentObject var blocktank: BlocktankViewModel - @EnvironmentObject var currency: CurrencyViewModel - @EnvironmentObject var navigation: NavigationViewModel - @EnvironmentObject var transfer: TransferViewModel - @EnvironmentObject var wallet: WalletViewModel - - @State private var amountViewModel = AmountInputViewModel() - @State private var isLoading = false - - private var amountSats: UInt64 { - amountViewModel.amountSats - } - - private var maxAllowed: UInt64 { - transfer.hwSpending.maxAllowedToSend - } - - /// Inputs the limit calculation depends on. Keying the `.task` on this reruns `updateHwLimits` - /// when Blocktank info arrives after the screen opens (the LSP caps are otherwise nil → 0 limits). - private struct HwLimitInputs: Equatable { - let walletId: String - let maxChannelSizeSat: UInt64? - let maxClientBalanceSat: UInt64? - } - - private var hwLimitInputs: HwLimitInputs { - HwLimitInputs( - walletId: walletId, - maxChannelSizeSat: blocktank.info?.options.maxChannelSizeSat, - maxClientBalanceSat: blocktank.info?.options.maxClientBalanceSat - ) - } - - private var isValidAmount: Bool { - !transfer.hwSpending.isLoading && maxAllowed > 0 && amountSats <= maxAllowed - } - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - NavigationBar(title: t("lightning__transfer__nav_title")) - .padding(.bottom, 16) - - DisplayText(t("lightning__spending_amount__title"), accentColor: .purpleAccent) - .fixedSize(horizontal: false, vertical: true) - - NumberPadTextField(viewModel: amountViewModel, showConversion: false) - .onTapGesture { - amountViewModel.togglePrimaryDisplay(currency: currency) - } - .padding(.top, 32) - - Spacer() - - HStack(alignment: .bottom) { - if transfer.hwSpending.isLoading { - HStack(spacing: 4) { - CaptionMText(t("wallet__send_available")) - ProgressView() - .scaleEffect(0.7) - } - } else { - AvailableAmount( - label: t("wallet__send_available"), - amount: Int(transfer.hwSpending.balanceAfterFee), - testIdentifier: "HardwareTransferAmountAvailable" - ) - } - - Spacer() - - actionButtons - } - .padding(.bottom, 12) - - Divider() - - NumberPad( - type: amountViewModel.getNumberPadType(currency: currency), - errorKey: amountViewModel.errorKey, - isDisabled: transfer.hwSpending.isLoading - ) { key in - amountViewModel.handleNumberPadInput(key, currency: currency) - } - - CustomButton( - title: t("common__continue"), - isDisabled: !isValidAmount, - isLoading: isLoading - ) { - await onContinue() - } - .accessibilityIdentifier("HardwareTransferAmountContinue") - } - .accessibilityElement(children: .contain) - .accessibilityIdentifier("HardwareTransferAmount") - .navigationBarHidden(true) - .padding(.horizontal, 16) - .bottomSafeAreaPadding() - .offlineOverlay(title: t("lightning__transfer__nav_title")) - .task(id: hwLimitInputs) { - await transfer.updateHwLimits( - walletId: walletId, - blocktankInfo: blocktank.info, - estimateOrderFee: { clientBalance, lspBalance in - let estimate = try await blocktank.estimateOrderFee(clientBalance: clientBalance, lspBalance: lspBalance) - return (estimate.networkFeeSat, estimate.serviceFeeSat) - } - ) - } - // `initial: true` because `maxAllowed` outlives this screen: arriving with the limits - // already computed leaves no change to observe, and the input would go uncapped. - .onChange(of: maxAllowed, initial: true) { updateInputCap() } - .onChange(of: amountViewModel.maxExceededCount) { onMaxExceeded() } - .onChange(of: transfer.hwTransferError) { _, error in - guard let error else { return } - app.toast(error) - transfer.hwTransferError = nil - } - } - - private func updateInputCap() { - amountViewModel.maxAmountOverride = maxAllowed > 0 ? maxAllowed : nil - } - - private func onMaxExceeded() { - amountViewModel.updateFromSats(maxAllowed, currency: currency) - app.toast( - type: .warning, - title: t("lightning__spending_amount__error_max__title"), - description: t( - "lightning__spending_amount__error_max__description", - variables: ["amount": CurrencyFormatter.formatSats(maxAllowed)] - ), - visibilityTime: Toast.visibilityTimeShort, - accessibilityIdentifier: "HardwareTransferAmountExceededToast" - ) - } - - private var actionButtons: some View { - HStack(spacing: 8) { - NumberPadActionButton( - text: currency.primaryDisplay == .bitcoin ? "Bitcoin" : currency.selectedCurrency, - imageName: "arrow-up-down" - ) { - withAnimation { - amountViewModel.togglePrimaryDisplay(currency: currency) - } - } - - NumberPadActionButton(text: t("lightning__spending_amount__quarter")) { - amountViewModel.updateFromSats(transfer.hwSpending.quarterAmount, currency: currency) - } - .accessibilityIdentifier("HardwareTransferAmountQuarter") - - NumberPadActionButton(text: t("common__max")) { - amountViewModel.updateFromSats(maxAllowed, currency: currency) - } - .accessibilityIdentifier("HardwareTransferAmountMax") - } - } - - private func onContinue() async { - guard !transfer.isSpendingBusy else { return } - isLoading = true - defer { isLoading = false } - - if wallet.nodeLifecycleState != .running { - let isReady = await wallet.waitForNodeToRun(timeoutSeconds: 30.0) - guard isReady else { - app.toast( - type: .error, - title: "Lightning node not ready", - description: "Please wait for the Lightning node to start and try again." - ) - return - } - } - - do { - let canFund = await transfer.canFundOrder( - clientBalance: amountSats, - budget: transfer.hwFundingBudget(walletId: walletId), - transferValues: { transfer.calculateTransferValues(clientBalanceSat: $0, blocktankInfo: blocktank.info) }, - estimateOrderFee: { clientBalance, lspBalance in - let estimate = try await blocktank.estimateOrderFee(clientBalance: clientBalance, lspBalance: lspBalance) - return (estimate.networkFeeSat, estimate.serviceFeeSat) - } - ) - guard canFund else { - app.toast( - type: .warning, - title: t("lightning__spending_amount__error_balance__title"), - description: t("lightning__spending_amount__error_balance__description"), - visibilityTime: Toast.visibilityTimeShort - ) - return - } - - let values = transfer.calculateTransferValues(clientBalanceSat: amountSats, blocktankInfo: blocktank.info) - let lspBalance = max(values.defaultLspBalance, values.minLspBalance) - let feeSat = try await blocktank.estimateFundingAmount(clientBalance: amountSats, lspBalance: lspBalance) - - transfer.onEstimateReady(clientBalance: amountSats, lspBalance: lspBalance, feeSat: feeSat) - navigation.navigate(.spendingHwSign(walletId: walletId)) - } catch { - let appError = AppError(error: error) - app.toast(type: .error, title: appError.message, description: appError.debugMessage) - } - } -} diff --git a/Bitkit/Views/Transfer/Hardware/SpendingHwSign.swift b/Bitkit/Views/Transfer/Hardware/SpendingHwSign.swift deleted file mode 100644 index 7bf19db23..000000000 --- a/Bitkit/Views/Transfer/Hardware/SpendingHwSign.swift +++ /dev/null @@ -1,227 +0,0 @@ -import BitkitCore -import SwiftUI - -struct SpendingHwSign: View { - let walletId: String - - @EnvironmentObject var app: AppViewModel - @EnvironmentObject var blocktank: BlocktankViewModel - @EnvironmentObject var navigation: NavigationViewModel - @EnvironmentObject var transfer: TransferViewModel - @Environment(HwWalletManager.self) private var hwWalletManager - - var body: some View { - if transfer.uiState.feeSat > 0 { - content() - } else { - Color.clear.onAppear { navigation.reset() } - } - } - - private let illustrationWidthRatio = 256.0 / 375.0 - private let illustrationTopRatio = (488.0 - 92.0) / (812.0 - 92.0 - 34.0) - - private func content() -> some View { - VStack(alignment: .leading, spacing: 0) { - NavigationBar(title: t("lightning__transfer__nav_title")) - .disabled(!transfer.canLeaveHwSign) - .padding(.bottom, 16) - - ZStack(alignment: .top) { - deviceIllustration - - belowNav() - } - } - .accessibilityElement(children: .contain) - .accessibilityIdentifier("HardwareTransferSign") - .navigationBarHidden(true) - .padding(.horizontal, 16) - .bottomSafeAreaPadding() - .task(id: transfer.uiState.feeSat) { - transfer.warmUpHardwareConnection(walletId: walletId) - await transfer.updateHwFundingFeeEstimate(walletId: walletId) - } - .onChange(of: transfer.hwSignedEvent) { - navigation.navigate(.spendingHwSigned) - } - .onChange(of: transfer.hwTransferError) { _, error in - guard let error else { return } - app.toast(error) - transfer.hwTransferError = nil - } - .sheet(isPresented: passphrasePromptBinding) { - HwPassphrasePromptSheet( - isVerifying: transfer.hwSpending.isVerifyingPassphrase, - onSubmit: { passphrase in - guard let order = transfer.uiState.order else { return } - transfer.onHwPassphraseSubmit(order: order, walletId: walletId, passphrase: passphrase) - }, - onCancel: { transfer.onHwPassphraseDismiss() } - ) - } - .onDisappear { - let stillInFlow = navigation.path.contains { - if case .spendingHwSign = $0 { - return true - } else { - return false - } - } - if !stillInFlow { - transfer.cancelHwSigning() - } - } - } - - private var passphrasePromptBinding: Binding { - Binding( - get: { transfer.hwSpending.isPassphraseRequired }, - set: { - if !$0 { - transfer.onHwPassphraseDismiss() - } - } - ) - } - - private var isBusy: Bool { - transfer.isSpendingBusy - } - - private func belowNav() -> some View { - VStack(alignment: .leading, spacing: 0) { - DisplayText( - t( - transfer.hwSpending.hasPendingBroadcast - ? "lightning__transfer_hw__signed_title" - : "lightning__transfer_hw__sign_title" - ), - accentColor: .purpleAccent - ) - - SpendingHwFeeGrid(state: transfer.uiState, miningFeeSats: transfer.hwSpending.miningFeeSats) - .padding(.top, 16) - - HStack(spacing: 16) { - CustomButton( - title: t("common__learn_more"), - size: .small, - isDisabled: isBusy || transfer.hwSpending.hasPendingBroadcast - ) { - navigation.navigate(.transferLearnMore) - } - .accessibilityIdentifier("HardwareTransferSignLearnMore") - - if transfer.uiState.isAdvanced { - CustomButton( - title: t("lightning__spending_confirm__default"), - size: .small, - isDisabled: isBusy || transfer.hwSpending.hasPendingBroadcast - ) { - do { - let values = transfer.calculateTransferValues( - clientBalanceSat: transfer.uiState.clientBalanceSat, - blocktankInfo: blocktank.info - ) - try await transfer.onDefaultClick(lspBalance: max(values.defaultLspBalance, values.minLspBalance)) { - try await blocktank.estimateFundingAmount(clientBalance: $0, lspBalance: $1) - } - } catch { - app.toast(error) - } - } - .accessibilityIdentifier("HardwareTransferSignDefault") - } else { - CustomButton( - title: t("common__advanced"), - size: .small, - isDisabled: isBusy || transfer.hwSpending.hasPendingBroadcast - ) { - navigation.navigate(.spendingAdvanced(walletId: walletId)) - } - .accessibilityIdentifier("HardwareTransferSignAdvanced") - } - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.top, 24) - - Spacer() - - CustomButton( - title: transfer.hwSpending.hasPendingBroadcast ? t("common__retry") : vendor.transferSignButtonTitle, - isDisabled: isBusy, - isLoading: isBusy - ) { - await transfer.onTransferToSpendingHwConfirm(walletId: walletId) { clientBalance, lspBalance in - try await blocktank.createOrder(clientBalance: clientBalance, lspBalance: lspBalance) - } - } - .accessibilityIdentifier("HardwareTransferOpenTrezorConnect") - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - } - - private var vendor: HwWalletVendor { - hwWalletManager.wallets.first { $0.id == walletId }?.vendor ?? .trezor - } - - private var deviceIllustration: some View { - GeometryReader { geo in - let side = geo.size.width * illustrationWidthRatio - Image(vendor.signImageName) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: side, height: side) - .position(x: geo.size.width / 2, y: geo.size.height * illustrationTopRatio + side / 2) - } - .padding(.horizontal, -16) - .allowsHitTesting(false) - .accessibilityHidden(true) - } -} - -struct SpendingHwFeeGrid: View { - let state: TransferUiState - var miningFeeSats: UInt64 = 0 - - private var lspFee: UInt64 { - state.lspFeeSat - } - - private var total: UInt64 { - state.feeSat + miningFeeSats - } - - var body: some View { - VStack(spacing: 16) { - HStack { - FeeDisplayRow( - label: t("lightning__spending_confirm__network_fee"), - amount: miningFeeSats - ) - .frame(maxWidth: .infinity) - - FeeDisplayRow( - label: t("lightning__spending_confirm__lsp_fee"), - amount: lspFee - ) - .frame(maxWidth: .infinity) - } - - HStack { - FeeDisplayRow( - label: t("lightning__spending_confirm__amount"), - amount: state.clientBalanceSat - ) - .frame(maxWidth: .infinity) - - FeeDisplayRow( - label: t("lightning__spending_confirm__total"), - amount: total - ) - .frame(maxWidth: .infinity) - } - } - } -} diff --git a/Bitkit/Views/Transfer/Hardware/SpendingHwSigned.swift b/Bitkit/Views/Transfer/Hardware/SpendingHwSigned.swift deleted file mode 100644 index e3d58338e..000000000 --- a/Bitkit/Views/Transfer/Hardware/SpendingHwSigned.swift +++ /dev/null @@ -1,51 +0,0 @@ -import BitkitCore -import SwiftUI - -/// "Transaction signed" — a brief confirmation of the signed funding tx before auto-forwarding to -/// the shared "Funds in Transfer" progress screen. -struct SpendingHwSigned: View { - @EnvironmentObject var navigation: NavigationViewModel - @EnvironmentObject var transfer: TransferViewModel - - /// Figma handoff delay before forwarding from the signed confirmation. - private let autoForwardDelay: UInt64 = 1_000_000_000 - - var body: some View { - if transfer.uiState.feeSat > 0 { - content() - } else { - Color.clear.onAppear { navigation.reset() } - } - } - - private func content() -> some View { - VStack(alignment: .leading, spacing: 0) { - NavigationBar(title: t("lightning__transfer__nav_title")) - .disabled(transfer.isSpendingBusy) - .padding(.bottom, 16) - - DisplayText(t("lightning__transfer_hw__signed_title"), accentColor: .purpleAccent) - - SpendingHwFeeGrid(state: transfer.uiState, miningFeeSats: transfer.hwSpending.miningFeeSats) - .padding(.top, 16) - - Image("check") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 256, height: 256) - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity) - .accessibilityIdentifier("HardwareTransferSigned") - - Spacer() - } - .navigationBarHidden(true) - .padding(.horizontal, 16) - .bottomSafeAreaPadding() - .task { - try? await Task.sleep(nanoseconds: autoForwardDelay) - guard !Task.isCancelled else { return } - navigation.navigate(.settingUp) - } - } -} diff --git a/Bitkit/Views/Transfer/SavingsProgressView.swift b/Bitkit/Views/Transfer/SavingsProgressView.swift index 7aef3f6d5..b13b8e17c 100644 --- a/Bitkit/Views/Transfer/SavingsProgressView.swift +++ b/Bitkit/Views/Transfer/SavingsProgressView.swift @@ -148,16 +148,6 @@ struct SavingsProgressView: View { // Ensure we re-enable screen timeout when view disappears UIApplication.shared.isIdleTimerDisabled = false } - .onChange(of: transfer.transferUnavailable) { _, unavailable in - if unavailable { - transfer.transferUnavailable = false - app.toast( - type: .error, - title: t("lightning__close_error"), - description: t("lightning__close_error_msg") - ) - } - } } /// Swaps spending funds out to on-chain savings. A pending claim is shown as "settling" @@ -210,26 +200,11 @@ struct SavingsProgressView: View { progressState = .success } } else { - // Check if any channels can be retried (filter out trusted peers) - let (_, nonTrustedChannels) = LightningService.shared.separateTrustedChannels(channelsFailedToCoopClose) - - if nonTrustedChannels.isEmpty { - // All channels are trusted peers - show error and navigate back - UIApplication.shared.isIdleTimerDisabled = false - app.toast( - type: .error, - title: t("lightning__close_error"), - description: t("lightning__close_error_msg") - ) - navigation.reset() - } else { - withAnimation { - progressState = .failed - } - - // Start retrying the cooperative close for non-trusted channels - transfer.startCoopCloseRetries(channels: nonTrustedChannels) + withAnimation { + progressState = .failed } + + transfer.startCoopCloseRetries(channels: channelsFailedToCoopClose) } } catch { app.toast(error) diff --git a/Bitkit/Views/Transfer/SettingUpView.swift b/Bitkit/Views/Transfer/SettingUpView.swift deleted file mode 100644 index b710b3931..000000000 --- a/Bitkit/Views/Transfer/SettingUpView.swift +++ /dev/null @@ -1,165 +0,0 @@ -import BitkitCore -import SwiftUI - -struct SettingUpView: View { - @EnvironmentObject var app: AppViewModel - @EnvironmentObject var navigation: NavigationViewModel - @EnvironmentObject var transfer: TransferViewModel - - /// Keep in state so we don't get a new random text on each render - @State private var randomOkText: String = localizedRandom("common__ok_random") - - var isTransferring: Bool { - return transfer.lightningSetupStep < 3 - } - - var navTitle: String { - return isTransferring ? t("lightning__transfer__nav_title") : t("lightning__transfer_success__nav_title") - } - - var title: String { - return isTransferring ? t("lightning__savings_progress__title") : t("lightning__transfer_success__title_spending") - } - - var text: String { - return isTransferring ? t("lightning__setting_up_text") : t("lightning__transfer_success__text_spending") - } - - var buttonTitle: String { - return isTransferring ? t("lightning__setting_up_button") : randomOkText - } - - let steps = [ - t("lightning__setting_up_step1"), // Processing Payment - t("lightning__setting_up_step2"), // Payment Successful - t("lightning__setting_up_step3"), // Queued For Opening - t("lightning__setting_up_step4"), // Opening Connection - ] - - var body: some View { - VStack(spacing: 0) { - NavigationBar(title: navTitle, showBackButton: false) - .padding(.bottom, 16) - - VStack(alignment: .leading, spacing: 0) { - DisplayText(title, accentColor: .purpleAccent) - .padding(.bottom, 14) - .layoutPriority(1) - - BodyMText(text, accentColor: .white, accentFont: Fonts.bold) - .layoutPriority(1) - - if isTransferring { - EllipseLoader(variant: .transfer) - .padding(.top, 32) - .padding(.horizontal, 16) - .accessibilityIdentifier("LightningSettingUp") - } else { - Image("check") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 256, height: 256) - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity) - .accessibilityIdentifier("TransferSuccess") - } - - Spacer() - - if isTransferring { - ProgressSteps(steps: steps, currentStep: transfer.lightningSetupStep) - .padding(.vertical, 16) - } - - CustomButton(title: buttonTitle) { - navigation.reset() - } - .accessibilityIdentifier("TransferSuccess-button") - } - } - .navigationBarHidden(true) - .allowSwipeBack(false) - .padding(.horizontal, 16) - .bottomSafeAreaPadding() - .onAppear { - // Auto-mine a block in regtest mode after a 5-second delay - if Env.network == .regtest { - Task { - try? await Task.sleep(nanoseconds: 5_000_000_000) - - do { - Logger.debug("Auto-mining a block", context: "SettingUpView") - try await CoreService.shared.blocktank.regtestMineBlocks(1) - Logger.debug("Successfully mined a block", context: "SettingUpView") - } catch { - Logger.error("Failed to mine block: \(error.localizedDescription)", context: "SettingUpView") - } - } - } - } - } -} - -#Preview("Created") { - NavigationStack { - SettingUpView() - .environmentObject(AppViewModel()) - .environmentObject( - { - let vm = TransferViewModel() - vm.onOrderCreated(order: IBtOrder.mock(state2: .created)) - vm.lightningSetupStep = 0 - return vm - }() - ) - } - .preferredColorScheme(.dark) -} - -#Preview("Paid") { - NavigationStack { - SettingUpView() - .environmentObject(AppViewModel()) - .environmentObject( - { - let vm = TransferViewModel() - vm.onOrderCreated(order: IBtOrder.mock(state2: .paid)) - vm.lightningSetupStep = 1 - return vm - }() - ) - } - .preferredColorScheme(.dark) -} - -#Preview("Executed") { - NavigationStack { - SettingUpView() - .environmentObject(AppViewModel()) - .environmentObject( - { - let vm = TransferViewModel() - vm.onOrderCreated(order: IBtOrder.mock(state2: .executed)) - vm.lightningSetupStep = 2 - return vm - }() - ) - } - .preferredColorScheme(.dark) -} - -#Preview("Opened") { - NavigationStack { - SettingUpView() - .environmentObject(AppViewModel()) - .environmentObject( - { - let vm = TransferViewModel() - vm.onOrderCreated(order: IBtOrder.mock(state2: .executed, channel: .mock())) - vm.lightningSetupStep = 4 - return vm - }() - ) - } - .preferredColorScheme(.dark) -} diff --git a/Bitkit/Views/Transfer/SpendingAdvancedView.swift b/Bitkit/Views/Transfer/SpendingAdvancedView.swift deleted file mode 100644 index 19aa7dd94..000000000 --- a/Bitkit/Views/Transfer/SpendingAdvancedView.swift +++ /dev/null @@ -1,275 +0,0 @@ -import BitkitCore -import SwiftUI - -struct SpendingAdvancedView: View { - var walletId: String? - - @EnvironmentObject var app: AppViewModel - @EnvironmentObject var blocktank: BlocktankViewModel - @EnvironmentObject var currency: CurrencyViewModel - @EnvironmentObject var feeEstimatesManager: FeeEstimatesManager - @EnvironmentObject var transfer: TransferViewModel - @EnvironmentObject var wallet: WalletViewModel - @Environment(\.dismiss) var dismiss - - @State private var amountViewModel = AmountInputViewModel() - @State private var feeEstimate: UInt64? - @State private var isLoading = false - @State private var feeEstimateTask: Task? - /// Reserved once and reused, so re-reading the budget doesn't burn a receive index. - @State private var fundingAddress: String? - - var lspBalance: UInt64 { - amountViewModel.amountSats - } - - private var isValid: Bool { - let values = transfer.transferValues - guard !transfer.isSettlingAdvancedCapacity, lspBalance > 0, values.maxLspBalance > 0 else { return false } - return lspBalance >= values.minLspBalance && lspBalance <= values.maxLspBalance - } - - var body: some View { - VStack(spacing: 0) { - NavigationBar(title: t("lightning__transfer__nav_title")) - .padding(.bottom, 16) - - VStack(alignment: .leading, spacing: 0) { - DisplayText(t("lightning__spending_advanced__title"), accentColor: .purpleAccent) - .fixedSize(horizontal: false, vertical: true) - - NumberPadTextField( - viewModel: amountViewModel, - showConversion: false, - testIdentifier: "SpendingAdvancedNumberField" - ) - .onTapGesture { - amountViewModel.togglePrimaryDisplay(currency: currency) - } - .padding(.top, 32) - - // Fee estimate - HStack(spacing: 4) { - CaptionMText(t("lightning__spending_advanced__fee")) - - if let feeEstimate { - MoneyText(sats: Int(feeEstimate), size: .bodySSB, symbol: true) - } else { - CaptionMText("—") - } - } - .frame(height: 20) - .padding(.top, 16) - - Spacer() - - // Action buttons - HStack(alignment: .bottom) { - Spacer() - - actionButtons - } - .padding(.vertical, 8) - - Divider() - - NumberPad( - type: amountViewModel.getNumberPadType(currency: currency), - errorKey: amountViewModel.errorKey, - isDisabled: transfer.isSettlingAdvancedCapacity - ) { key in - amountViewModel.handleNumberPadInput(key, currency: currency) - } - - CustomButton( - title: t("common__continue"), - isDisabled: !isValid, - isLoading: isLoading - ) { - isLoading = true - defer { isLoading = false } - - do { - let canFund = await transfer.canFundAdvancedOrder( - clientBalance: transfer.uiState.clientBalanceSat, - receivingAmount: lspBalance, - budget: fundingBudget(), - estimateOrderFee: estimateOrderFee - ) - guard canFund else { - app.toast( - type: .warning, - title: t("lightning__spending_advanced__error_balance__title"), - description: t("lightning__spending_advanced__error_balance__description"), - visibilityTime: Toast.visibilityTimeShort - ) - return - } - - let feeSat = try await blocktank.estimateFundingAmount( - clientBalance: transfer.uiState.clientBalanceSat, - lspBalance: lspBalance - ) - transfer.onEstimateReady( - clientBalance: transfer.uiState.clientBalanceSat, - lspBalance: lspBalance, - feeSat: feeSat, - isAdvanced: true - ) - dismiss() - } catch { - app.toast(error) - } - } - .accessibilityIdentifier("SpendingAdvancedContinue") - } - } - .accessibilityElement(children: .contain) - .accessibilityIdentifier("SpendingAdvanced") - .navigationBarHidden(true) - .padding(.horizontal, 16) - .bottomSafeAreaPadding() - .task { - await transfer.updateAdvancedTransferValues( - clientBalanceSat: transfer.uiState.clientBalanceSat, - budget: { await fundingBudget() }, - transferValues: { transfer.calculateTransferValues(clientBalanceSat: $0, blocktankInfo: blocktank.info) }, - estimateOrderFee: estimateOrderFee - ) - - updateFeeEstimate() - } - .onChange(of: lspBalance) { - if isValid { - updateFeeEstimate() - } else { - feeEstimate = nil - } - } - .onChange(of: transfer.transferValues.maxLspBalance, initial: true) { updateInputCap() } - .onChange(of: amountViewModel.maxExceededCount) { onMaxExceeded() } - } - - private var estimateOrderFee: (UInt64, UInt64) async throws -> (networkFeeSat: UInt64, serviceFeeSat: UInt64) { - { clientBalance, lspBalance in - let estimate = try await blocktank.estimateOrderFee(clientBalance: clientBalance, lspBalance: lspBalance) - return (estimate.networkFeeSat, estimate.serviceFeeSat) - } - } - - /// The device account for a hardware transfer, this wallet's on-chain savings otherwise. - private func fundingBudget() async -> UInt64? { - if let walletId { - return await transfer.hwFundingBudget(walletId: walletId) - } - - do { - let address: String - if let fundingAddress { - address = fundingAddress - } else { - address = try await TransferFundingBudget.reserveSizingAddress() - fundingAddress = address - } - return await TransferFundingBudget.onchainBudget( - address: address, - feeEstimatesManager: feeEstimatesManager, - wallet: wallet - ) - } catch { - Logger.warn("Failed to resolve advanced funding budget: \(error)", context: "SpendingAdvancedView") - return nil - } - } - - private func updateInputCap() { - let maxLspBalance = transfer.transferValues.maxLspBalance - amountViewModel.maxAmountOverride = maxLspBalance > 0 ? maxLspBalance : nil - - // Settling can land the max below what is already entered. - if maxLspBalance > 0, maxLspBalance < amountViewModel.amountSats { - amountViewModel.updateFromSats(maxLspBalance, currency: currency) - } - } - - private func onMaxExceeded() { - // Snap the input to the max so the user lands on the highest allowed amount. - let maxLspBalance = transfer.transferValues.maxLspBalance - if maxLspBalance > 0 { - amountViewModel.updateFromSats(maxLspBalance, currency: currency) - } - showMaxExceededToast() - } - - private func showMaxExceededToast() { - app.toast( - type: .warning, - title: t("lightning__spending_advanced__error_max__title"), - description: t( - "lightning__spending_advanced__error_max__description", - variables: ["amount": CurrencyFormatter.formatSats(transfer.transferValues.maxLspBalance)] - ), - visibilityTime: Toast.visibilityTimeShort, - accessibilityIdentifier: "SpendingAdvancedExceededToast" - ) - } - - private var actionButtons: some View { - HStack(spacing: 16) { - NumberPadActionButton(text: t("common__min")) { - amountViewModel.updateFromSats(transfer.transferValues.minLspBalance, currency: currency) - } - .accessibilityIdentifier("SpendingAdvancedMin") - - Spacer() - - NumberPadActionButton(text: t("common__default")) { - amountViewModel.updateFromSats(transfer.transferValues.defaultLspBalance, currency: currency) - } - .accessibilityIdentifier("SpendingAdvancedDefault") - - Spacer() - - NumberPadActionButton(text: t("common__max")) { - amountViewModel.updateFromSats(transfer.transferValues.maxLspBalance, currency: currency) - } - .accessibilityIdentifier("SpendingAdvancedMax") - } - } - - private func updateFeeEstimate() { - guard lspBalance > 0 else { return } - - feeEstimateTask?.cancel() - feeEstimate = nil - - feeEstimateTask = Task { - do { - let estimate = try await blocktank.estimateOrderFee( - clientBalance: transfer.uiState.clientBalanceSat, - lspBalance: lspBalance - ) - guard !Task.isCancelled else { return } - await MainActor.run { - feeEstimate = estimate.feeSat - } - } catch { - guard !Task.isCancelled else { return } - Logger.debug("Fee estimation failed: \(error.localizedDescription)") - } - } - } -} - -#Preview { - NavigationStack { - SpendingAdvancedView() - .environmentObject(AppViewModel()) - .environmentObject(CurrencyViewModel()) - .environmentObject(BlocktankViewModel()) - .environmentObject(FeeEstimatesManager()) - .environmentObject(TransferViewModel()) - .environmentObject(WalletViewModel()) - } - .preferredColorScheme(.dark) -} diff --git a/Bitkit/Views/Transfer/SpendingAmount.swift b/Bitkit/Views/Transfer/SpendingAmount.swift deleted file mode 100644 index db3f96534..000000000 --- a/Bitkit/Views/Transfer/SpendingAmount.swift +++ /dev/null @@ -1,295 +0,0 @@ -import BitkitCore -import SwiftUI - -struct SpendingAmount: View { - @EnvironmentObject var app: AppViewModel - @EnvironmentObject var blocktank: BlocktankViewModel - @EnvironmentObject var currency: CurrencyViewModel - @EnvironmentObject var feeEstimatesManager: FeeEstimatesManager - @EnvironmentObject var navigation: NavigationViewModel - @EnvironmentObject var transfer: TransferViewModel - @EnvironmentObject var wallet: WalletViewModel - - @State private var amountViewModel = AmountInputViewModel() - @State private var isLoading = false - @State private var isCalculatingMax = true - @State private var availableAmount: UInt64? - @State private var maxTransferAmount: UInt64? - /// Reserved once and reused, so re-reading the budget doesn't burn a receive index. - @State private var fundingAddress: String? - - private var amountSats: UInt64 { - amountViewModel.amountSats - } - - /// Inputs the max calculation depends on. A single `.task(id:)` keyed on this restarts the - /// calculation (with structured cancellation) whenever either input changes, so concurrent - /// reloads can neither overlap nor leak the loading flag. - private struct MaxCalcInputs: Equatable { - let maxChannelSizeSat: UInt64? - let maxClientBalanceSat: UInt64? - let spendableOnchainBalanceSats: Int - } - - private var maxCalcInputs: MaxCalcInputs { - MaxCalcInputs( - maxChannelSizeSat: blocktank.info?.options.maxChannelSizeSat, - maxClientBalanceSat: blocktank.info?.options.maxClientBalanceSat, - spendableOnchainBalanceSats: wallet.spendableOnchainBalanceSats - ) - } - - private var isValidAmount: Bool { - guard let max = maxTransferAmount else { return false } - return amountSats <= max - } - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - NavigationBar(title: t("lightning__transfer__nav_title")) - .padding(.bottom, 16) - - DisplayText(t("lightning__spending_amount__title"), accentColor: .purpleAccent) - .fixedSize(horizontal: false, vertical: true) - - NumberPadTextField( - viewModel: amountViewModel, - showConversion: false, - testIdentifier: "SpendingAmountNumberField" - ) - .onTapGesture { - amountViewModel.togglePrimaryDisplay(currency: currency) - } - .padding(.top, 32) - - Spacer() - - HStack(alignment: .bottom) { - if let available = availableAmount { - AvailableAmount( - label: t("wallet__send_available"), - amount: Int(available), - testIdentifier: "SpendingAmountAvailable" - ) - } else { - HStack(spacing: 4) { - CaptionMText(t("wallet__send_available")) - ProgressView() - .scaleEffect(0.7) - } - } - - Spacer() - - actionButtons - } - .padding(.bottom, 12) - - Divider() - - NumberPad( - type: amountViewModel.getNumberPadType(currency: currency), - errorKey: amountViewModel.errorKey, - isDisabled: isCalculatingMax - ) { key in - amountViewModel.handleNumberPadInput(key, currency: currency) - } - - CustomButton( - title: t("common__continue"), - isDisabled: !isValidAmount, - isLoading: isLoading - ) { - await onContinue() - } - .accessibilityIdentifier("SpendingAmountContinue") - } - .accessibilityElement(children: .contain) - .accessibilityIdentifier("SpendingAmount") - .navigationBarHidden(true) - .padding(.horizontal, 16) - .bottomSafeAreaPadding() - .offlineOverlay(title: t("lightning__transfer__nav_title")) - .task(id: maxCalcInputs) { - await MainActor.run { isCalculatingMax = true } - await calculateMaxTransferAmount() - if !Task.isCancelled { - await MainActor.run { isCalculatingMax = false } - } - } - .onChange(of: maxTransferAmount) { updateInputCap() } - .onChange(of: amountViewModel.maxExceededCount) { onMaxExceeded() } - } - - private func updateInputCap() { - amountViewModel.maxAmountOverride = (maxTransferAmount ?? 0) > 0 ? maxTransferAmount : nil - } - - private func onMaxExceeded() { - if let max = maxTransferAmount { - amountViewModel.updateFromSats(max, currency: currency) - } - showMaxExceededToast() - } - - private func showMaxExceededToast() { - app.toast( - type: .warning, - title: t("lightning__spending_amount__error_max__title"), - description: t( - "lightning__spending_amount__error_max__description", - variables: ["amount": CurrencyFormatter.formatSats(maxTransferAmount ?? 0)] - ), - visibilityTime: Toast.visibilityTimeShort, - accessibilityIdentifier: "SpendingAmountExceededToast" - ) - } - - private var actionButtons: some View { - HStack(spacing: 8) { - NumberPadActionButton( - text: currency.primaryDisplay == .bitcoin ? "Bitcoin" : currency.selectedCurrency, - imageName: "arrow-up-down" - ) { - withAnimation { - amountViewModel.togglePrimaryDisplay(currency: currency) - } - } - - NumberPadActionButton(text: t("lightning__spending_amount__quarter")) { - guard let max = maxTransferAmount else { return } - let quarter = UInt64(wallet.spendableOnchainBalanceSats) / 4 - amountViewModel.updateFromSats(min(quarter, max), currency: currency) - } - .accessibilityIdentifier("SpendingAmountQuarter") - - NumberPadActionButton(text: t("common__max")) { - guard let max = maxTransferAmount else { return } - amountViewModel.updateFromSats(max, currency: currency) - } - .accessibilityIdentifier("SpendingAmountMax") - } - } - - private func onContinue() async { - guard !transfer.isSpendingBusy else { return } - isLoading = true - defer { isLoading = false } - - if wallet.nodeLifecycleState != .running { - let isReady = await wallet.waitForNodeToRun(timeoutSeconds: 30.0) - guard isReady else { - app.toast( - type: .error, - title: "Lightning node not ready", - description: "Please wait for the Lightning node to start and try again." - ) - return - } - } - - do { - let canFund = await transfer.canFundOrder( - clientBalance: amountSats, - budget: fundingBudget(), - transferValues: { transfer.calculateTransferValues(clientBalanceSat: $0, blocktankInfo: blocktank.info) }, - estimateOrderFee: { clientBalance, lspBalance in - let estimate = try await blocktank.estimateOrderFee(clientBalance: clientBalance, lspBalance: lspBalance) - return (estimate.networkFeeSat, estimate.serviceFeeSat) - } - ) - guard canFund else { - app.toast( - type: .warning, - title: t("lightning__spending_amount__error_balance__title"), - description: t("lightning__spending_amount__error_balance__description"), - visibilityTime: Toast.visibilityTimeShort - ) - return - } - - let values = transfer.calculateTransferValues(clientBalanceSat: amountSats, blocktankInfo: blocktank.info) - let lspBalance = max(values.defaultLspBalance, values.minLspBalance) - let feeSat = try await blocktank.estimateFundingAmount(clientBalance: amountSats, lspBalance: lspBalance) - - transfer.onEstimateReady(clientBalance: amountSats, lspBalance: lspBalance, feeSat: feeSat) - navigation.navigate(.spendingConfirm) - } catch { - let appError = AppError(error: error) - app.toast(type: .error, title: appError.message, description: appError.debugMessage) - } - } - - private func fundingBudget() async -> UInt64? { - do { - let address: String - if let fundingAddress { - address = fundingAddress - } else { - address = try await TransferFundingBudget.reserveSizingAddress() - fundingAddress = address - } - return await TransferFundingBudget.onchainBudget( - address: address, - feeEstimatesManager: feeEstimatesManager, - wallet: wallet - ) - } catch { - Logger.warn("Failed to resolve transfer funding budget: \(error)", context: "SpendingAmount") - return nil - } - } - - private func calculateMaxTransferAmount() async { - guard let info = blocktank.info else { - await MainActor.run { - availableAmount = 0 - maxTransferAmount = 0 - } - return - } - - do { - guard let calculatedAvailableAmount = await fundingBudget() else { - await MainActor.run { - let fallback = fallbackMaxTransferAmount(info: info) - availableAmount = fallback - maxTransferAmount = fallback - } - return - } - - let (available, maxAmount) = try await transfer.calculateSpendingLimits( - onchainAvailable: calculatedAvailableAmount, - lspMaxClientBalance: info.options.maxClientBalanceSat, - transferValues: { transfer.calculateTransferValues(clientBalanceSat: $0, blocktankInfo: info) }, - estimateOrderFee: { clientBalance, lspBalance in - let estimate = try await blocktank.estimateOrderFee(clientBalance: clientBalance, lspBalance: lspBalance) - return (estimate.networkFeeSat, estimate.serviceFeeSat) - } - ) - - await MainActor.run { - availableAmount = available - maxTransferAmount = maxAmount - } - } catch { - Logger.error("Failed to calculate max transfer amount: \(error)") - await MainActor.run { - let fallback = fallbackMaxTransferAmount(info: info) - availableAmount = fallback - maxTransferAmount = fallback - } - } - } - - /// Fallback max when fee estimates are unavailable: clamp the client balance to the LSP's max - /// client balance so the liquidity calculation doesn't collapse to zero on a saturating balance. - private func fallbackMaxTransferAmount(info: IBtInfo) -> UInt64 { - let balance = UInt64(wallet.spendableOnchainBalanceSats) - let lspMaxClientBalance = info.options.maxClientBalanceSat - let clientBalance = lspMaxClientBalance > 0 ? min(balance, lspMaxClientBalance) : balance - let values = transfer.calculateTransferValues(clientBalanceSat: clientBalance, blocktankInfo: info) - return min(values.maxClientBalance, balance) - } -} diff --git a/Bitkit/Views/Transfer/SpendingConfirm.swift b/Bitkit/Views/Transfer/SpendingConfirm.swift deleted file mode 100644 index 57dcea4f0..000000000 --- a/Bitkit/Views/Transfer/SpendingConfirm.swift +++ /dev/null @@ -1,370 +0,0 @@ -import BitkitCore -import LDKNode -import SwiftUI - -struct SpendingConfirm: View { - @EnvironmentObject var app: AppViewModel - @EnvironmentObject var blocktank: BlocktankViewModel - @EnvironmentObject var currency: CurrencyViewModel - @EnvironmentObject var feeEstimatesManager: FeeEstimatesManager - @EnvironmentObject var navigation: NavigationViewModel - @EnvironmentObject var settings: SettingsViewModel - @EnvironmentObject var transfer: TransferViewModel - @EnvironmentObject var wallet: WalletViewModel - - private var isPaying: Bool { - transfer.isSpendingBusy - } - - @State private var hideSwipeButton = false - @State private var transactionFee: UInt64 = 0 - @State private var selectedUtxos: [SpendableUtxo]? - @State private var satsPerVbyte: UInt32? - @State private var maxSendableAmount: UInt64? - @State private var shouldUseSendAll = false - var lspFee: UInt64 { - transfer.uiState.lspFeeSat - } - - var total: UInt64 { - SpendingConfirmTotal.leavingAmount( - orderFeeSat: transfer.uiState.feeSat, - networkFeeSat: transactionFee, - shouldUseSendAll: shouldUseSendAll, - maxSendable: maxSendableAmount - ) - } - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - NavigationBar(title: t("lightning__transfer__nav_title")) - .disabled(isPaying) - .padding(.bottom, 16) - - DisplayText(t("lightning__transfer__confirm"), accentColor: .purpleAccent) - - VStack(spacing: 16) { - HStack { - FeeDisplayRow( - label: t("lightning__spending_confirm__network_fee"), - amount: transactionFee - ) - .frame(maxWidth: .infinity) - - FeeDisplayRow( - label: t("lightning__spending_confirm__lsp_fee"), - amount: lspFee - ) - .frame(maxWidth: .infinity) - } - - HStack { - FeeDisplayRow( - label: t("lightning__spending_confirm__amount"), - amount: transfer.uiState.clientBalanceSat - ) - .frame(maxWidth: .infinity) - - FeeDisplayRow( - label: t("lightning__spending_confirm__total"), - amount: total - ) - .frame(maxWidth: .infinity) - } - } - .padding(.top, 32) - - if transfer.uiState.isAdvanced { - LightningChannel( - capacity: transfer.uiState.lspBalanceSat + transfer.uiState.clientBalanceSat, - localBalance: transfer.uiState.clientBalanceSat, - remoteBalance: transfer.uiState.lspBalanceSat, - status: .open, - showLabels: true - ) - .accessibilityElement(children: .ignore) - .accessibilityIdentifier("SpendingConfirmChannel") - .padding(.vertical, 16) - } - - HStack(alignment: .center, spacing: 0) { - BodyMText(t("lightning__spending_confirm__background_setup"), textColor: .textPrimary) - - Spacer() - - Toggle("", isOn: $settings.enableNotifications) - .toggleStyle(SwitchToggleStyle(tint: .purpleAccent)) - .labelsHidden() - .accessibilityIdentifier("SpendingConfirmNotificationSwitch") - } - .frame(height: 50) - - Divider() - .padding(.bottom, 16) - - HStack(spacing: 16) { - CustomButton(title: t("common__learn_more"), size: .small) { - navigation.navigate(.transferLearnMore) - } - .accessibilityIdentifier("SpendingConfirmMore") - - if transfer.uiState.isAdvanced { - CustomButton(title: t("lightning__spending_confirm__default"), size: .small) { - do { - let values = transfer.calculateTransferValues( - clientBalanceSat: transfer.uiState.clientBalanceSat, - blocktankInfo: blocktank.info - ) - try await transfer.onDefaultClick(lspBalance: max(values.defaultLspBalance, values.minLspBalance)) { - try await blocktank.estimateFundingAmount(clientBalance: $0, lspBalance: $1) - } - } catch { - app.toast(error) - } - } - .accessibilityIdentifier("SpendingConfirmDefault") - } else { - CustomButton(title: t("common__advanced"), size: .small) { - navigation.navigate(.spendingAdvanced()) - } - .accessibilityIdentifier("SpendingConfirmAdvanced") - } - } - .frame(maxWidth: .infinity, alignment: .leading) - .disabled(isPaying) - - Spacer() - - if !hideSwipeButton { - SwipeButton( - title: t("lightning__transfer__swipe"), - accentColor: .purpleAccent, - isLoading: isPaying - ) { - try await onConfirm() - } - .disabled(isPaying || selectedUtxos == nil || transactionFee == 0 || satsPerVbyte == nil) - } - } - .navigationBarHidden(true) - .padding(.horizontal, 16) - .bottomSafeAreaPadding() - .offlineOverlay(title: t("lightning__transfer__nav_title")) - .onChange(of: transfer.recoveredOnchainFundingOrderId) { _, orderId in - guard let orderId, orderId == transfer.uiState.order?.id else { return } - navigation.navigate(.settingUp) - } - .task(id: transfer.uiState.feeSat) { - await sizeFunding() - } - } - - private func sizeFunding() async { - do { - let address: String = if let orderAddress = transfer.uiState.order?.payment?.onchain?.address { - orderAddress - } else { - try await LightningService.shared.addressInfoForType(.nativeSegwit, atIndex: 0).address - } - try await calculateTransactionFee(address: address, amountSats: transfer.uiState.feeSat) - } catch { - app.toast(error) - } - } - - private func onConfirm() async throws { - guard let confirmedFeeRate = satsPerVbyte, !transfer.isSpendingBusy else { return } - transfer.uiState.isConfirming = true - defer { transfer.uiState.isConfirming = false } - - let displayed = SpendingConfirmAmounts(networkFeeSat: transactionFee, totalSat: total) - let displayedOrderFeeSat = transfer.uiState.feeSat - let displayedFunding = FundingState( - transactionFee: transactionFee, - selectedUtxos: selectedUtxos, - satsPerVbyte: satsPerVbyte, - maxSendableAmount: maxSendableAmount, - shouldUseSendAll: shouldUseSendAll - ) - - do { - let order = try await transfer.orderForSwipe { clientBalance, lspBalance in - try await blocktank.createOrder(clientBalance: clientBalance, lspBalance: lspBalance) - } - guard let address = order.payment?.onchain?.address else { - throw AppError(message: "Order payment onchain address is nil", debugMessage: nil) - } - do { - try await calculateTransactionFee(address: address, amountSats: order.feeSat, feeRate: confirmedFeeRate) - guard transactionFee > 0 else { - throw AppError(message: t("other__try_again"), debugMessage: "Rebuilt network fee is zero") - } - } catch { - if let increase = transfer.unfundableFeeIncrease(order: order, displayedOrderFeeSat: displayedOrderFeeSat) { - showFeesIncreasedToast(increase) - throw SpendingFeesIncreasedError() - } - restore(displayedFunding) - throw error - } - let rebuilt = SpendingConfirmAmounts( - networkFeeSat: transactionFee, - totalSat: SpendingConfirmTotal.leavingAmount( - orderFeeSat: order.feeSat, - networkFeeSat: transactionFee, - shouldUseSendAll: shouldUseSendAll, - maxSendable: maxSendableAmount - ) - ) - if let increase = transfer.feeIncrease( - order: order, - displayedOrderFeeSat: displayedOrderFeeSat, - displayed: displayed, - rebuilt: rebuilt - ) { - showFeesIncreasedToast(increase) - throw SpendingFeesIncreasedError() - } - guard let rate = satsPerVbyte else { return } - try await transfer.payOrder( - order: order, - speed: .fast, - txFee: transactionFee, - satsPerVbyte: rate, - utxosToSpend: selectedUtxos, - isMaxAmount: shouldUseSendAll, - maxSendableAmount: maxSendableAmount - ) - await wallet.updateBalanceState() - try await Task.sleep(nanoseconds: 1_000_000_000) - navigation.navigate(.settingUp) - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - hideSwipeButton = true - } - } catch let error as OnchainFundingPendingError { - // Reset the underlying swipe while the exact operation remains in Pending. - throw error - } catch { - if !(error is SpendingFeesIncreasedError) { - app.toast(error) - } - throw error - } - } - - private struct FundingState { - let transactionFee: UInt64 - let selectedUtxos: [SpendableUtxo]? - let satsPerVbyte: UInt32? - let maxSendableAmount: UInt64? - let shouldUseSendAll: Bool - } - - private func restore(_ funding: FundingState) { - transactionFee = funding.transactionFee - selectedUtxos = funding.selectedUtxos - satsPerVbyte = funding.satsPerVbyte - maxSendableAmount = funding.maxSendableAmount - shouldUseSendAll = funding.shouldUseSendAll - } - - private func showFeesIncreasedToast(_ increase: SpendingFeeIncrease) { - app.toast( - type: .info, - title: t("lightning__spending_confirm__fees_changed_title"), - description: increase.toastDescription { currency.primaryAmountText(sats: $0) } - ) - } - - private func calculateTransactionFee(address: String, amountSats: UInt64, feeRate: UInt32? = nil) async throws { - do { - let lightningService = LightningService.shared - - let fastFeeRate: UInt32 - if let feeRate { - fastFeeRate = feeRate - } else { - guard let feeEstimates = await feeEstimatesManager.getEstimates(refresh: true) else { - Logger.error("SpendingConfirm: feeEstimates is nil") - throw AppError(message: t("other__try_again"), debugMessage: nil) - } - fastFeeRate = TransactionSpeed.fast.getFeeRate(from: feeEstimates) - } - - let balance = UInt64(wallet.spendableOnchainBalanceSats) - let allUtxos = try await lightningService.listSpendableOutputs() - - var useSendAll = false - var normalFee: UInt64 = 0 - var normalUtxos: [SpendableUtxo]? - - do { - let utxos = try await lightningService.selectUtxosWithAlgorithm( - targetAmountSats: amountSats, - satsPerVbyte: fastFeeRate, - coinSelectionAlgorythm: .largestFirst, - utxos: nil - ) - normalFee = try await wallet.calculateTotalFee( - address: address, - amountSats: amountSats, - satsPerVByte: fastFeeRate, - utxosToSpend: utxos - ) - normalUtxos = utxos - - let totalInput = utxos.reduce(UInt64(0)) { $0 + $1.valueSats } - useSendAll = DustChangeHelper.shouldUseSendAllToAvoidDust( - totalInput: totalInput, - amountSats: amountSats, - normalFee: normalFee, - isMaxAmount: true - ) - } catch { - Logger.info("Normal coin selection failed, using sendAll: \(error)") - useSendAll = true - } - - if useSendAll { - let sendAllFee = try await wallet.estimateSendAllFee( - address: address, - satsPerVByte: fastFeeRate - ) - let maxSendable = balance >= sendAllFee ? balance - sendAllFee : 0 - - if maxSendable < amountSats { - Logger.error( - "Insufficient balance for transfer: maxSendable=\(maxSendable), orderFee=\(amountSats)", - context: "SpendingConfirm" - ) - throw AppError(message: t("other__pay_insufficient_savings"), debugMessage: nil) - } - - await MainActor.run { - transactionFee = sendAllFee - selectedUtxos = allUtxos - satsPerVbyte = fastFeeRate - maxSendableAmount = maxSendable - shouldUseSendAll = true - } - } else { - await MainActor.run { - transactionFee = normalFee - selectedUtxos = normalUtxos - satsPerVbyte = fastFeeRate - shouldUseSendAll = false - } - } - } catch { - Logger.error("Failed to calculate actual fee: \(error)", context: "SpendingConfirm") - await MainActor.run { - transactionFee = 0 - selectedUtxos = nil - satsPerVbyte = nil - maxSendableAmount = nil - shouldUseSendAll = false - } - throw error - } - } -} diff --git a/Bitkit/Views/Transfer/SpendingIntroView.swift b/Bitkit/Views/Transfer/SpendingIntroView.swift deleted file mode 100644 index 5b9ea5f00..000000000 --- a/Bitkit/Views/Transfer/SpendingIntroView.swift +++ /dev/null @@ -1,40 +0,0 @@ -import SwiftUI - -struct SpendingIntroView: View { - /// When set, this intro is for a hardware-wallet transfer; Continue routes to the HW amount flow. - var walletId: String? - - @EnvironmentObject var app: AppViewModel - @EnvironmentObject var navigation: NavigationViewModel - - var body: some View { - OnboardingView( - navTitle: t("lightning__transfer__nav_title"), - title: t("lightning__spending_intro__title"), - description: t("lightning__spending_intro__text"), - imageName: "coin-stack-x", - buttonText: t("lightning__spending_intro__button"), - onButtonPress: { - app.hasSeenTransferToSpendingIntro = true - if let walletId { - navigation.navigate(.spendingAmountHw(walletId: walletId)) - } else { - navigation.navigate(.spendingAmount) - } - }, - accentColor: .purpleAccent, - imagePosition: .center, - testID: "SpendingIntro" - ) - .navigationBarHidden(true) - } -} - -#Preview { - NavigationStack { - SpendingIntroView() - .environmentObject(AppViewModel()) - .environmentObject(NavigationViewModel()) - .preferredColorScheme(.dark) - } -} diff --git a/Bitkit/Views/Transfer/TransferFundingBudget.swift b/Bitkit/Views/Transfer/TransferFundingBudget.swift deleted file mode 100644 index f8ce0a6a4..000000000 --- a/Bitkit/Views/Transfer/TransferFundingBudget.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Foundation -import LDKNode - -/// The on-chain ceiling a transfer-to-spending order has to fit under: the spendable balance minus -/// the fee to sweep it at the fast rate. Shared by the amount and advanced screens so sizing and the -/// pre-order re-check use the same calculation. -@MainActor -enum TransferFundingBudget { - /// Reserved once per screen and reused: each call advances LDK's receive index, and this address - /// only ever prices a sweep, never receives. - static func reserveSizingAddress() async throws -> String { - let addressType = LDKNode.AddressType.fromStorage(UserDefaults.standard.string(forKey: "selectedAddressType")) - return try await PrivatePaykitAddressReservationStore.shared.nextNonReservedReceiveAddress(addressType: addressType) - } - - /// Re-read on every Continue, so it uses cached fee rates rather than forcing a refresh that - /// would sit between the tap and the confirm screen. - /// - /// Nil when the fee estimates or the sweep fee are unavailable: sizing then falls back to a - /// cheaper estimate, while a funding check skips rather than blocks. - static func onchainBudget( - address: String, - feeEstimatesManager: FeeEstimatesManager, - wallet: WalletViewModel - ) async -> UInt64? { - guard let feeEstimates = await feeEstimatesManager.getEstimates() else { return nil } - let fastFeeRate = TransactionSpeed.fast.getFeeRate(from: feeEstimates) - let spendable = UInt64(max(0, wallet.spendableOnchainBalanceSats)) - - guard let sweepFee = try? await LightningService.shared.estimateSendAllFee( - address: address, - satsPerVByte: fastFeeRate - ) else { return nil } - - return spendable.saturatingSub(sweepFee) - } -} diff --git a/Bitkit/Views/Transfer/TransferIntroView.swift b/Bitkit/Views/Transfer/TransferIntroView.swift deleted file mode 100644 index d5d077c08..000000000 --- a/Bitkit/Views/Transfer/TransferIntroView.swift +++ /dev/null @@ -1,33 +0,0 @@ -import SwiftUI - -struct TransferIntroView: View { - @EnvironmentObject var app: AppViewModel - @EnvironmentObject var navigation: NavigationViewModel - - var body: some View { - OnboardingView( - navTitle: t("lightning__transfer__nav_title"), - title: t("lightning__transfer_intro__title"), - description: t("lightning__transfer_intro__text"), - imageName: "lightning", - buttonText: t("lightning__transfer_intro__button"), - onButtonPress: { - app.hasSeenTransferIntro = true - navigation.navigate(.fundingOptions) - }, - accentColor: .purpleAccent, - imagePosition: .center, - testID: "TransferIntro" - ) - .navigationBarHidden(true) - } -} - -#Preview { - NavigationStack { - TransferIntroView() - .environmentObject(AppViewModel()) - .environmentObject(NavigationViewModel()) - .preferredColorScheme(.dark) - } -} diff --git a/Bitkit/Views/Transfer/TransferLearnMoreView.swift b/Bitkit/Views/Transfer/TransferLearnMoreView.swift deleted file mode 100644 index 5c762c1e3..000000000 --- a/Bitkit/Views/Transfer/TransferLearnMoreView.swift +++ /dev/null @@ -1,56 +0,0 @@ -import BitkitCore -import SwiftUI - -struct TransferLearnMoreView: View { - @EnvironmentObject var transfer: TransferViewModel - - @EnvironmentObject var app: AppViewModel - @EnvironmentObject var currency: CurrencyViewModel - @EnvironmentObject var wallet: WalletViewModel - @Environment(\.dismiss) var dismiss - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - NavigationBar(title: t("lightning__transfer__nav_title")) - .padding(.bottom, 16) - - DisplayText(t("lightning__liquidity__title"), accentColor: .purpleAccent) - .padding(.bottom, 16) - - BodyMText(t("lightning__liquidity__text")) - - Spacer() - - VStack(alignment: .leading, spacing: 16) { - SubtitleText(t("lightning__liquidity__label")) - LightningChannel( - capacity: transfer.uiState.lspBalanceSat + transfer.uiState.clientBalanceSat, - localBalance: transfer.uiState.clientBalanceSat, - remoteBalance: transfer.uiState.lspBalanceSat, - status: .open, - showLabels: true - ) - } - - CustomButton(title: t("common__understood")) { - dismiss() - } - .padding(.top, 32) - .accessibilityIdentifier("LiquidityContinue") - } - .navigationBarHidden(true) - .padding(.horizontal, 16) - .bottomSafeAreaPadding() - } -} - -#Preview { - NavigationStack { - TransferLearnMoreView() - .environmentObject(TransferViewModel()) - .environmentObject(WalletViewModel()) - .environmentObject(AppViewModel()) - .environmentObject(CurrencyViewModel()) - } - .preferredColorScheme(.dark) -} diff --git a/Bitkit/Views/Wallets/HardwareWalletScreen.swift b/Bitkit/Views/Wallets/HardwareWalletScreen.swift index 2af1c9f82..9e03b22d8 100644 --- a/Bitkit/Views/Wallets/HardwareWalletScreen.swift +++ b/Bitkit/Views/Wallets/HardwareWalletScreen.swift @@ -11,6 +11,7 @@ struct HardwareWalletScreen: View { @EnvironmentObject var activity: ActivityListViewModel @EnvironmentObject var app: AppViewModel @EnvironmentObject var navigation: NavigationViewModel + @EnvironmentObject var sheets: SheetViewModel @Environment(HwWalletManager.self) private var hwWalletManager @Environment(TrezorManager.self) private var trezorManager @@ -41,9 +42,6 @@ struct HardwareWalletScreen: View { .task(id: wallet?.walletId) { await loadActivities() } - .task { - await app.checkGeoStatus() - } .onReceive(activity.activitiesChangedPublisher) { _ in Task { await loadActivities() } } @@ -127,14 +125,9 @@ struct HardwareWalletScreen: View { .resizable() .scaledToFit() .frame(width: 16, height: 16) - .foregroundColor(.white80), - isDisabled: GeoService.shared.isGeoBlocked + .foregroundColor(.white80) ) { - if app.hasSeenTransferToSpendingIntro { - navigation.navigate(.spendingAmountHw(walletId: walletId)) - } else { - navigation.navigate(.spendingIntroHw(walletId: walletId)) - } + sheets.showSheet(.transferUnavailable) } .accessibilityIdentifier("HardwareTransferToSpending") } diff --git a/Bitkit/Views/Wallets/Receive/ReceiveCjitAmount.swift b/Bitkit/Views/Wallets/Receive/ReceiveCjitAmount.swift deleted file mode 100644 index 4962c9091..000000000 --- a/Bitkit/Views/Wallets/Receive/ReceiveCjitAmount.swift +++ /dev/null @@ -1,206 +0,0 @@ -import BitkitCore -import SwiftUI - -struct ReceiveCjitAmount: View { - @EnvironmentObject private var app: AppViewModel - @EnvironmentObject private var blocktank: BlocktankViewModel - @EnvironmentObject private var currency: CurrencyViewModel - @EnvironmentObject private var wallet: WalletViewModel - - @Binding var navigationPath: [ReceiveRoute] - - @State private var amountViewModel = AmountInputViewModel() - @State private var maxCjitAmount: UInt64? - @State private var isCreatingCjit = false - - var minimumAmount: UInt64 { - blocktank.minCjitSats ?? 0 - } - - var amountSats: UInt64 { - amountViewModel.amountSats - } - - private var canContinue: Bool { - guard blocktank.minCjitSats != nil else { - return false - } - - return amountSats >= minimumAmount - } - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - SheetHeader(title: t("wallet__receive_bitcoin"), showBackButton: true) - - VStack(alignment: .leading, spacing: 0) { - NumberPadTextField(viewModel: amountViewModel, testIdentifier: "ReceiveCjitAmountNumberField") - .onTapGesture { - amountViewModel.togglePrimaryDisplay(currency: currency) - } - - Spacer() - - HStack(alignment: .bottom) { - AvailableAmount( - label: t("fee__minimum__title"), - amount: Int(minimumAmount) - ) - .onTapGesture { - amountViewModel.updateFromSats(minimumAmount, currency: currency) - } - - Spacer() - - NumberPadActionButton( - text: currency.primaryDisplay == .bitcoin ? "Bitcoin" : currency.selectedCurrency, - imageName: "transfer", - color: .brandAccent - ) { - withAnimation { - amountViewModel.togglePrimaryDisplay(currency: currency) - } - } - } - .padding(.bottom, 8) - - Divider() - - NumberPad( - type: amountViewModel.getNumberPadType(currency: currency), - errorKey: amountViewModel.errorKey - ) { key in - amountViewModel.handleNumberPadInput(key, currency: currency) - } - } - - CustomButton(title: t("common__continue"), isDisabled: !canContinue, isLoading: isCreatingCjit) { - Task { - await onContinue() - } - } - .accessibilityIdentifier("ReceiveCjitAmountContinue") - } - .accessibilityElement(children: .contain) - .accessibilityIdentifier("ReceiveCjitAmount") - .navigationBarHidden(true) - .padding(.horizontal, 16) - .sheetBackground() - .task { - do { - try await blocktank.refreshMinCjitSats() - } catch { - app.toast(error) - } - await refreshMaxCjitAmount() - updateInputCap() - } - .onChange(of: blocktank.info?.options.maxChannelSizeSat) { - Task { - await refreshMaxCjitAmount() - } - } - .onChange(of: maxCjitAmount) { - updateInputCap() - } - .onChange(of: amountViewModel.maxExceededCount) { - showMaxExceededToast() - } - } - - private func onContinue() async { - guard !isCreatingCjit, canContinue else { - return - } - - isCreatingCjit = true - defer { isCreatingCjit = false } - - if maxCjitAmount == nil { - await refreshMaxCjitAmount() - updateInputCap() - } - - guard isWithinMaxCjitAmount else { - showMaxExceededToast() - return - } - - // Wait until node is running if it's in starting state - if await wallet.waitForNodeToRun() { - // Only proceed if node is running - do { - let entry = try await blocktank.createCjit(amountSats: amountSats, description: "Bitkit") - navigationPath.append(.cjitConfirm(entry: entry, receiveAmountSats: amountSats, isAdditional: false)) - } catch { - if error.isCjitNodeCapacityExceeded { - showNodeCapacityExceededToast() - Logger.error(error) - return - } - - if isMaxCjitAmountError(error) { - if maxCjitAmount == nil { - await refreshMaxCjitAmount() - updateInputCap() - } - showMaxExceededToast() - Logger.error(error) - return - } - - app.toast(error) - Logger.error(error) - } - } else { - // Show error if node is not running or timed out - app.toast(type: .warning, title: "Lightning not ready", description: "Lightning node must be running to create an invoice") - } - } - - private var isWithinMaxCjitAmount: Bool { - guard let maxCjitAmount, maxCjitAmount > 0 else { - return true - } - - return amountSats <= maxCjitAmount - } - - private func updateInputCap() { - amountViewModel.maxAmountOverride = (maxCjitAmount ?? 0) > 0 ? maxCjitAmount : nil - } - - private func refreshMaxCjitAmount() async { - do { - maxCjitAmount = try await blocktank.maxCjitAmountSats() - } catch { - Logger.error("Failed to calculate max CJIT amount: \(error)") - maxCjitAmount = nil - } - } - - private func showMaxExceededToast() { - app.toast( - type: .warning, - title: t("wallet__receive_cjit_error_max__title"), - description: t( - "wallet__receive_cjit_error_max__description", - variables: ["amount": CurrencyFormatter.formatSats(maxCjitAmount ?? 0)] - ), - accessibilityIdentifier: "ReceiveCjitAmountExceededToast" - ) - } - - private func showNodeCapacityExceededToast() { - app.toast( - type: .warning, - title: t("wallet__receive_cjit_error_node_capacity__title"), - description: t("wallet__receive_cjit_error_node_capacity__description"), - accessibilityIdentifier: "ReceiveCjitNodeCapacityExceededToast" - ) - } - - private func isMaxCjitAmountError(_ error: Error) -> Bool { - error.isChannelSizeExceedsMaximum - } -} diff --git a/Bitkit/Views/Wallets/Receive/ReceiveCjitConfirmation.swift b/Bitkit/Views/Wallets/Receive/ReceiveCjitConfirmation.swift deleted file mode 100644 index c3a37863c..000000000 --- a/Bitkit/Views/Wallets/Receive/ReceiveCjitConfirmation.swift +++ /dev/null @@ -1,98 +0,0 @@ -import BitkitCore -import SwiftUI - -struct ReceiveCjitConfirmation: View { - @Binding var navigationPath: [ReceiveRoute] - let entry: IcJitEntry - let receiveAmountSats: UInt64 - let isAdditional: Bool - - @EnvironmentObject private var currency: CurrencyViewModel - @EnvironmentObject private var settings: SettingsViewModel - @EnvironmentObject private var wallet: WalletViewModel - - private var isAdditionalFlow: Bool { - isAdditional || wallet.hasExistingChannels - } - - private func formattedNetworkFee() -> String { - guard let converted = currency.convert(sats: entry.networkFeeSat) else { - return String(entry.networkFeeSat) - } - return converted.formattedWithSymbol() - } - - private func formattedServiceFee() -> String { - guard let converted = currency.convert(sats: entry.serviceFeeSat) else { - return String(entry.serviceFeeSat) - } - return converted.formattedWithSymbol() - } - - var receiveAmount: Int { - Int(receiveAmountSats - entry.feeSat) - } - - var body: some View { - VStack(spacing: 0) { - SheetHeader(title: t("wallet__receive_bitcoin"), showBackButton: true) - - VStack(alignment: .leading, spacing: 0) { - MoneyStack(sats: Int(receiveAmountSats), showSymbol: true) - .padding(.bottom, 32) - - BodyMText( - t( - isAdditionalFlow ? "wallet__receive_connect_additional" : "wallet__receive_connect_initial", - variables: [ - "networkFee": formattedNetworkFee(), - "serviceFee": formattedServiceFee(), - ] - ), - accentColor: .white, - accentFont: Fonts.bold - ) - .padding(.bottom, 32) - - VStack(alignment: .leading, spacing: 4) { - CaptionMText(t("wallet__receive_will")) - MoneyText(sats: receiveAmount, size: .title, symbol: true) - } - } - - Spacer() - - HStack(alignment: .center, spacing: 0) { - BodyMText(t("wallet__receive_background_setup_toggle"), textColor: .textPrimary) - - Spacer() - - Toggle("", isOn: $settings.enableNotifications) - .toggleStyle(SwitchToggleStyle(tint: .brandAccent)) - .labelsHidden() - .accessibilityIdentifier("ReceiveConfirmNotificationSwitch") - } - .frame(height: 50) - .padding(.bottom, 8) - - Divider() - .padding(.bottom, 22) - - HStack(spacing: 16) { - CustomButton(title: t("common__learn_more"), variant: .secondary) { - navigationPath.append(.cjitLearnMore(entry: entry, receiveAmountSats: receiveAmountSats, isAdditional: isAdditionalFlow)) - } - - CustomButton(title: t("common__continue")) { - navigationPath.append(.qr(cjitInvoice: entry.invoice.request, tab: .spending)) - } - } - } - .accessibilityElement(children: .contain) - .accessibilityIdentifier("ReceiveCjitConfirm") - .navigationBarHidden(true) - .padding(.horizontal, 16) - .sheetBackground() - .frame(maxWidth: .infinity, maxHeight: .infinity) - } -} diff --git a/Bitkit/Views/Wallets/Receive/ReceiveCjitGeoBlocked.swift b/Bitkit/Views/Wallets/Receive/ReceiveCjitGeoBlocked.swift deleted file mode 100644 index fbc4827f5..000000000 --- a/Bitkit/Views/Wallets/Receive/ReceiveCjitGeoBlocked.swift +++ /dev/null @@ -1,33 +0,0 @@ -import SwiftUI - -struct ReceiveCjitGeoBlocked: View { - @EnvironmentObject private var navigation: NavigationViewModel - @EnvironmentObject private var sheets: SheetViewModel - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - SheetHeader(title: t("wallet__receive_bitcoin"), showBackButton: true) - - BodyMText(t("lightning__funding__text_blocked_cjit")) - - Spacer() - - Image("globe-sphere") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 256, height: 256) - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity) - - Spacer() - - CustomButton(title: t("lightning__funding_advanced__button_short")) { - sheets.hideSheet() - navigation.navigate(.fundingAdvanced) - } - } - .navigationBarHidden(true) - .padding(.horizontal, 16) - .sheetBackground() - } -} diff --git a/Bitkit/Views/Wallets/Receive/ReceiveCjitLearnMore.swift b/Bitkit/Views/Wallets/Receive/ReceiveCjitLearnMore.swift deleted file mode 100644 index 17d99523e..000000000 --- a/Bitkit/Views/Wallets/Receive/ReceiveCjitLearnMore.swift +++ /dev/null @@ -1,84 +0,0 @@ -import BitkitCore -import SwiftUI - -struct ReceiveCjitLearnMore: View { - let entry: IcJitEntry - let receiveAmountSats: UInt64 - let isAdditional: Bool - - @Environment(\.dismiss) var dismiss - @EnvironmentObject private var settings: SettingsViewModel - @EnvironmentObject private var wallet: WalletViewModel - - private var isAdditionalFlow: Bool { - isAdditional || wallet.hasExistingChannels - } - - var navTitle: String { - isAdditionalFlow - ? t("wallet__receive_liquidity__nav_title_additional") - : t("wallet__receive_liquidity__nav_title") - } - - var text: String { - isAdditionalFlow - ? t("wallet__receive_liquidity__text_additional") - : t("wallet__receive_liquidity__text") - } - - var label: String { - isAdditionalFlow - ? t("wallet__receive_liquidity__label_additional") - : t("wallet__receive_liquidity__label") - } - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - SheetHeader(title: navTitle, showBackButton: true) - - BodyMText(text) - - VStack(alignment: .leading, spacing: 16) { - SubtitleText(label) - LightningChannel( - capacity: entry.channelSizeSat, - localBalance: receiveAmountSats - entry.feeSat, - remoteBalance: entry.channelSizeSat - (receiveAmountSats - entry.feeSat), - status: .open, - showLabels: true - ) - } - .padding(.top, 32) - - Spacer() - - BodyMText(t("wallet__receive_background_setup_text")) - .padding(.bottom, 16) - - HStack(alignment: .center, spacing: 0) { - BodyMText(t("wallet__receive_background_setup_toggle"), textColor: .textPrimary) - - Spacer() - - Toggle("", isOn: $settings.enableNotifications) - .toggleStyle(SwitchToggleStyle(tint: .brandAccent)) - .labelsHidden() - .accessibilityIdentifier("ReceiveLiquidityNotificationSwitch") - } - .frame(height: 50) - .padding(.bottom, 8) - - Divider() - .padding(.bottom, 22) - - CustomButton(title: t("common__understood")) { - dismiss() - } - } - .accessibilityElement(children: .contain) - .accessibilityIdentifier("ReceiveCjitLiquidity") - .navigationBarHidden(true) - .padding(.horizontal, 16) - .sheetBackground() - } -} diff --git a/Bitkit/Views/Wallets/Receive/ReceiveEdit.swift b/Bitkit/Views/Wallets/Receive/ReceiveEdit.swift index de38d29e8..61433e842 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveEdit.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveEdit.swift @@ -3,7 +3,6 @@ import SwiftUI struct ReceiveEdit: View { @EnvironmentObject private var app: AppViewModel - @EnvironmentObject private var blocktank: BlocktankViewModel @EnvironmentObject private var currency: CurrencyViewModel @EnvironmentObject private var wallet: WalletViewModel @EnvironmentObject private var tagManager: TagManager @@ -15,7 +14,6 @@ struct ReceiveEdit: View { @Binding var navigationPath: [ReceiveRoute] let sourceTab: ReceiveQr.ReceiveTab let onchainOnly: Bool - let replacesCurrentQr: Bool let onSendPaymentRequest: (PaykitPaymentRequestDraft) -> Void @State private var amountViewModel = AmountInputViewModel() @@ -196,68 +194,18 @@ struct ReceiveEdit: View { await prepareLightningInvoice() } - static func replaceEditedQrRoute(in navigationPath: inout [ReceiveRoute], with route: ReceiveRoute) { - guard let currentRouteIndex = navigationPath.indices.last else { - navigationPath.append(route) - return - } - - var replacementStartIndex = currentRouteIndex - if let qrIndex = navigationPath[.. ReceiveAdditionalLiquidityAction { + private func additionalLiquidityAction() -> ReceiveAdditionalLiquidityAction { ReceiveLiquidityDecision.additionalLiquidityAction( source: liquiditySource, invoiceAmountSats: amountViewModel.amountSats, - inboundCapacitySats: wallet.totalReadyInboundLightningSats, - minCjitSats: blocktank.minCjitSats, - maxCjitAmountSats: maxCjitAmountSats, - isGeoBlocked: GeoService.shared.isGeoBlocked - ) - } - - private func needsCjitLimitsForAdditionalLiquidity() -> Bool { - ReceiveLiquidityDecision.needsCjitLimitsForAdditionalLiquidity( - source: liquiditySource, - invoiceAmountSats: amountViewModel.amountSats, - inboundCapacitySats: wallet.totalReadyInboundLightningSats, - isGeoBlocked: GeoService.shared.isGeoBlocked + inboundCapacitySats: wallet.totalReadyInboundLightningSats ) } diff --git a/Bitkit/Views/Wallets/Receive/ReceiveQr.swift b/Bitkit/Views/Wallets/Receive/ReceiveQr.swift index 85cd92dd6..e666abb9f 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveQr.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveQr.swift @@ -11,7 +11,6 @@ struct ReceiveQr: View { @Environment(HwWalletManager.self) private var hwWalletManager @Environment(PaykitPaymentRequestManager.self) private var paymentRequests @Binding var navigationPath: [ReceiveRoute] - let cjitInvoice: String? let tab: ReceiveTab? let hardwareWalletId: String? let onUsdtSelectionChange: ((Bool) -> Void)? @@ -34,13 +33,11 @@ struct ReceiveQr: View { init( navigationPath: Binding<[ReceiveRoute]>, - cjitInvoice: String? = nil, tab: ReceiveTab? = nil, hardwareWalletId: String? = nil, onUsdtSelectionChange: ((Bool) -> Void)? = nil ) { _navigationPath = navigationPath - self.cjitInvoice = cjitInvoice self.tab = tab self.hardwareWalletId = hardwareWalletId self.onUsdtSelectionChange = onUsdtSelectionChange @@ -89,7 +86,7 @@ struct ReceiveQr: View { if selectedHardwareWalletId != nil { items.insert(TabItem(.hardware, label: selectedHardwareWallet?.vendor.modelName), at: 0) } - if settings.isUsdtEnabled, cjitInvoice == nil { + if settings.isUsdtEnabled { items.append(TabItem(.usdt)) } return items @@ -114,7 +111,7 @@ struct ReceiveQr: View { } private var canShowUnifiedReceive: Bool { - (wallet.canCreateReceiveLightningInvoice || wallet.hasCoherentReceiveInvoice) && cjitInvoice == nil + wallet.canCreateReceiveLightningInvoice || wallet.hasCoherentReceiveInvoice } private var selectedTabBinding: Binding { @@ -159,8 +156,8 @@ struct ReceiveQr: View { ) } - var showingCjitOnboarding: Bool { - return !wallet.canCreateReceiveLightningInvoice && cjitInvoice == nil && selectedTab == .spending + var showingSpendingOnboarding: Bool { + return !wallet.canCreateReceiveLightningInvoice && selectedTab == .spending } var body: some View { @@ -201,7 +198,7 @@ struct ReceiveQr: View { } tabContent(for: .spending) - if settings.isUsdtEnabled, cjitInvoice == nil { + if settings.isUsdtEnabled { Color.clear.tag(ReceivePage.tab(.usdt)) } @@ -215,7 +212,7 @@ struct ReceiveQr: View { Spacer() Group { - if showingCjitOnboarding { + if showingSpendingOnboarding { CustomButton( title: t("wallet__receive_spending"), icon: Image("bolt") @@ -224,11 +221,7 @@ struct ReceiveQr: View { .foregroundColor(.purpleAccent), isDisabled: wallet.nodeLifecycleState != .running ) { - if GeoService.shared.isGeoBlocked { - navigationPath.append(.cjitGeoBlocked) - } else if !wallet.canCreateReceiveLightningInvoice { - navigationPath.append(.cjitAmount) - } + navigationPath.append(.transferUnavailable) } } else if showDetails { VStack(spacing: 16) { @@ -316,7 +309,6 @@ struct ReceiveQr: View { } catch { app.toast(error) } - await app.checkGeoStatus() } .onChange(of: wallet.nodeLifecycleState) { _, newState in // They may open this view before node has started @@ -398,7 +390,7 @@ struct ReceiveQr: View { } else { ProgressView() } - } else if tab == .spending && !wallet.canCreateReceiveLightningInvoice && cjitInvoice == nil { + } else if tab == .spending && !wallet.canCreateReceiveLightningInvoice { cjitOnboarding } else if receiveUnavailable(for: tab) { receiveRefreshStatus(for: tab) @@ -463,7 +455,7 @@ struct ReceiveQr: View { } private func receiveUnavailable(for tab: ReceiveTab) -> Bool { - guard cjitInvoice == nil, tab == .unified || tab == .spending else { return false } + guard tab == .unified || tab == .spending else { return false } return wallet.isReceiveInvoiceUpdating || wallet.receiveInvoiceError != nil || (tab == .unified && !wallet.hasCoherentReceiveInvoice) } @@ -504,7 +496,7 @@ struct ReceiveQr: View { ) case .spending: return ( - uri: cjitInvoice ?? wallet.bolt11, + uri: wallet.bolt11, imageAsset: "ln", accentColor: .purpleAccent ) @@ -566,18 +558,7 @@ struct ReceiveQr: View { ) } case .spending: - // Spending: cjitInvoice or bolt11 - if let cjitInvoice { - pairs.append( - CopyAddressPair( - title: t("wallet__receive_lightning_invoice"), - address: cjitInvoice, - type: .lightning - ) - ) - break - } - + // Spending: bolt11 if !wallet.bolt11.isEmpty { pairs.append( CopyAddressPair( @@ -639,7 +620,7 @@ struct ReceiveQr: View { } private func editRoute(for tab: ReceiveTab) -> ReceiveRoute? { - .edit(tab: tab, onchainOnly: tab == .hardware, replacesCurrentQr: tab == .spending && cjitInvoice != nil) + .edit(tab: tab, onchainOnly: tab == .hardware) } private struct ImageConfig { diff --git a/Bitkit/Views/Wallets/Receive/ReceiveSheet.swift b/Bitkit/Views/Wallets/Receive/ReceiveSheet.swift index 239ccdae6..23daaa0cc 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveSheet.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveSheet.swift @@ -3,13 +3,10 @@ import SwiftUI enum ReceiveRoute: Hashable { case usdt - case qr(cjitInvoice: String?, tab: ReceiveQr.ReceiveTab?) - case edit(tab: ReceiveQr.ReceiveTab, onchainOnly: Bool, replacesCurrentQr: Bool = false) + case qr(tab: ReceiveQr.ReceiveTab?) + case edit(tab: ReceiveQr.ReceiveTab, onchainOnly: Bool) case tag - case cjitAmount - case cjitConfirm(entry: IcJitEntry, receiveAmountSats: UInt64, isAdditional: Bool) - case cjitLearnMore(entry: IcJitEntry, receiveAmountSats: UInt64, isAdditional: Bool) - case cjitGeoBlocked + case transferUnavailable case requestOrPay(publicKey: String) case paymentRequestRecipient(PaykitPaymentRequestDraft) case paymentRequestAmount(PaykitPaymentRequestDraft, PaykitPaymentRequestTarget) @@ -18,7 +15,7 @@ enum ReceiveRoute: Hashable { var requiresInvoiceRefresh: Bool { switch self { - case .qr, .edit, .tag, .cjitAmount, .cjitConfirm, .cjitLearnMore, .cjitGeoBlocked: + case .qr, .edit, .tag, .transferUnavailable: return true case .usdt, .requestOrPay, .paymentRequestRecipient, .paymentRequestAmount, .paymentRequestDetails, .paymentRequestSent: return false @@ -30,7 +27,7 @@ struct ReceiveConfig { let initialRoute: ReceiveRoute let hardwareWalletId: String? - init(view: ReceiveRoute = .qr(cjitInvoice: nil, tab: nil), hardwareWalletId: String? = nil) { + init(view: ReceiveRoute = .qr(tab: nil), hardwareWalletId: String? = nil) { initialRoute = view self.hardwareWalletId = hardwareWalletId } @@ -42,7 +39,7 @@ struct ReceiveSheetItem: SheetItem { let initialRoute: ReceiveRoute let hardwareWalletId: String? - init(id: UUID = UUID(), initialRoute: ReceiveRoute = .qr(cjitInvoice: nil, tab: nil), hardwareWalletId: String? = nil) { + init(id: UUID = UUID(), initialRoute: ReceiveRoute = .qr(tab: nil), hardwareWalletId: String? = nil) { self.id = id self.initialRoute = initialRoute self.hardwareWalletId = hardwareWalletId @@ -62,7 +59,7 @@ struct ReceiveSheet: View { private var usesUsdtHeader: Bool { switch navigationPath.last ?? config.initialRoute { case .usdt: isUsdtSelected ?? true - case let .qr(_, tab): isUsdtSelected ?? (tab == .usdt) + case let .qr(tab): isUsdtSelected ?? (tab == .usdt) default: false } } @@ -112,28 +109,21 @@ struct ReceiveSheet: View { case .usdt: ReceiveQr(navigationPath: $navigationPath, tab: .usdt, hardwareWalletId: config.hardwareWalletId, onUsdtSelectionChange: { isUsdtSelected = $0 }) - case let .qr(cjitInvoice, tab): + case let .qr(tab): ReceiveQr( navigationPath: $navigationPath, - cjitInvoice: cjitInvoice, tab: tab, hardwareWalletId: config.hardwareWalletId, onUsdtSelectionChange: { isUsdtSelected = $0 } ) - case let .edit(tab, onchainOnly, replacesCurrentQr): - ReceiveEdit(navigationPath: $navigationPath, sourceTab: tab, onchainOnly: onchainOnly, replacesCurrentQr: replacesCurrentQr) { draft in + case let .edit(tab, onchainOnly): + ReceiveEdit(navigationPath: $navigationPath, sourceTab: tab, onchainOnly: onchainOnly) { draft in navigationPath.append(.paymentRequestRecipient(draft)) } case .tag: ReceiveTag(navigationPath: $navigationPath) - case .cjitAmount: - ReceiveCjitAmount(navigationPath: $navigationPath) - case let .cjitConfirm(entry, receiveAmountSats, isAdditional): - ReceiveCjitConfirmation(navigationPath: $navigationPath, entry: entry, receiveAmountSats: receiveAmountSats, isAdditional: isAdditional) - case let .cjitLearnMore(entry, receiveAmountSats, isAdditional): - ReceiveCjitLearnMore(entry: entry, receiveAmountSats: receiveAmountSats, isAdditional: isAdditional) - case .cjitGeoBlocked: - ReceiveCjitGeoBlocked() + case .transferUnavailable: + ReceiveTransferUnavailable() case let .requestOrPay(publicKey): RequestOrPayView(publicKey: publicKey) { target in navigationPath.append(.paymentRequestAmount(Self.defaultPaymentRequestDraft, target)) diff --git a/Bitkit/Views/Wallets/Receive/ReceiveTransferUnavailable.swift b/Bitkit/Views/Wallets/Receive/ReceiveTransferUnavailable.swift new file mode 100644 index 000000000..98bc43786 --- /dev/null +++ b/Bitkit/Views/Wallets/Receive/ReceiveTransferUnavailable.swift @@ -0,0 +1,19 @@ +import SwiftUI + +struct ReceiveTransferUnavailable: View { + @EnvironmentObject private var navigation: NavigationViewModel + @EnvironmentObject private var sheets: SheetViewModel + + var body: some View { + TransferUnavailableContent( + navTitle: t("lightning__transfer_unavailable__nav_title"), + description: t("lightning__transfer_unavailable__text"), + showBackButton: true + ) { + sheets.hideSheet() + navigation.navigate(.fundManual(nodeUri: nil)) + } + .navigationBarHidden(true) + .sheetBackground() + } +} diff --git a/Bitkit/Views/Wallets/SavingsWalletScreen.swift b/Bitkit/Views/Wallets/SavingsWalletScreen.swift index e3cdce22b..d757856c3 100644 --- a/Bitkit/Views/Wallets/SavingsWalletScreen.swift +++ b/Bitkit/Views/Wallets/SavingsWalletScreen.swift @@ -4,6 +4,7 @@ struct SavingsWalletScreen: View { @EnvironmentObject var activity: ActivityListViewModel @EnvironmentObject var app: AppViewModel @EnvironmentObject var navigation: NavigationViewModel + @EnvironmentObject var sheets: SheetViewModel @EnvironmentObject var wallet: WalletViewModel private var shouldShowOnboarding: Bool { @@ -49,11 +50,9 @@ struct SavingsWalletScreen: View { } if !shouldShowOnboarding { - if !GeoService.shared.isGeoBlocked { - transferButton - .transition(.move(edge: .leading).combined(with: .opacity)) - .padding(.top, 28) - } + transferButton + .transition(.move(edge: .leading).combined(with: .opacity)) + .padding(.top, 28) ActivityList(viewType: .onchain) @@ -123,11 +122,7 @@ struct SavingsWalletScreen: View { .frame(width: 16, height: 16) .foregroundColor(.white80) ) { - if app.hasSeenTransferToSpendingIntro { - navigation.navigate(.spendingAmount) - } else { - navigation.navigate(.spendingIntro) - } + sheets.showSheet(.transferUnavailable) } .accessibilityIdentifier("TransferToSpending") } diff --git a/Bitkit/Views/Wallets/Send/SendPendingScreen.swift b/Bitkit/Views/Wallets/Send/SendPendingScreen.swift index a76c62a4b..3e80b6374 100644 --- a/Bitkit/Views/Wallets/Send/SendPendingScreen.swift +++ b/Bitkit/Views/Wallets/Send/SendPendingScreen.swift @@ -228,7 +228,7 @@ struct SendPendingScreen: View { Task { await abandonExpiredPayment() } } } message: { - Text(t("wallet__onchain_abandon_note")) + Text(t(abandonableAttempt?.isBlocktankFunding == true ? "wallet__onchain_abandon_transfer_note" : "wallet__onchain_abandon_note")) } .onChange(of: isReviewingRecovery, initial: true) { _, reviewing in updatePaymentReview(reviewing) @@ -328,7 +328,7 @@ struct SendPendingScreen: View { // The request can end, or a failed check recover, while this screen stays open. while paymentHash == nil, abandonableAttempt == nil, !Task.isCancelled { if pendingHardwareWalletId == nil { - await loadAbandonableAttempt() + guard await loadAbandonableAttempt() else { return } } guard abandonableAttempt == nil else { return } do { try await Task.sleep(for: .seconds(30)) } @@ -356,6 +356,12 @@ struct SendPendingScreen: View { defer { retryingOnchain = false } let context = OnchainSendPendingContext(attemptId: original.id, walletId: original.walletId, txid: original.txid) do { + // Blocktank no longer sells channels, so an unpaid order is never paid again. + if original.orderId != nil { + Logger.warn("Stopped a retry of a transfer to spending: transfers are unavailable", context: "SendPendingScreen") + sheets.showSheet(.transferUnavailable) + return + } let feeLimits = await wallet.getFeeLimits() guard feeRate > 0, feeRate <= feeLimits.maxFee else { app.toast(type: .warning, title: t("wallet__onchain_retry_invalid_fee")) @@ -396,18 +402,6 @@ struct SendPendingScreen: View { guard authorization.paymentDeadline == paymentDeadline, let payer = attempt.recoveryContext?.paymentIdentity else { throw PaykitPaymentRequestError.requestUnavailable } try await proofService.requireRecoveryPayer(payer) - } else if let orderId = attempt.orderId { - let orders = try await CoreService.shared.blocktank.orders(orderIds: [orderId], refresh: true) - guard let order = orders.first(where: { $0.id == orderId }), order.state2 == .created, - order.payment?.onchain?.address == attempt.address, - order.clientBalanceSat == attempt.transferContext?.clientBalanceSats, - order.feeSat <= attempt.amountSats - else { throw OnchainSendAttemptError.unresolved } - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - guard let expiry = formatter.date(from: order.orderExpiresAt) ?? ISO8601DateFormatter().date(from: order.orderExpiresAt), - expiry > Date() - else { throw OnchainSendAttemptError.unresolved } } } ) @@ -427,11 +421,14 @@ struct SendPendingScreen: View { } } + /// Returns whether to check again later. Transfers do not come back once unavailable, so one check decides a + /// Blocktank funding. @MainActor - private func loadAbandonableAttempt() async { - guard let attempt = try? await attemptService.blockingAttempt(), attempt.canBeAbandoned else { return } + private func loadAbandonableAttempt() async -> Bool { + guard let attempt = try? await attemptService.blockingAttempt(), attempt.canBeAbandoned else { return true } let hasEnded = await (try? proofService.onchainRequestHasEnded(attempt)) ?? false abandonableAttempt = hasEnded ? attempt : nil + return !attempt.isBlocktankFunding } private func abandonExpiredPayment() async { diff --git a/Bitkit/Views/Wallets/SpendingWalletScreen.swift b/Bitkit/Views/Wallets/SpendingWalletScreen.swift index e907bee69..bae55e69a 100644 --- a/Bitkit/Views/Wallets/SpendingWalletScreen.swift +++ b/Bitkit/Views/Wallets/SpendingWalletScreen.swift @@ -4,6 +4,7 @@ struct SpendingWalletScreen: View { @EnvironmentObject var activity: ActivityListViewModel @EnvironmentObject var app: AppViewModel @EnvironmentObject var navigation: NavigationViewModel + @EnvironmentObject var sheets: SheetViewModel @EnvironmentObject var wallet: WalletViewModel private var shouldShowOnboarding: Bool { @@ -33,7 +34,7 @@ struct SpendingWalletScreen: View { .padding(.top, 16) } - if shouldShowOnboarding && wallet.totalOnchainSats > 0 && !GeoService.shared.isGeoBlocked { + if shouldShowOnboarding && wallet.totalOnchainSats > 0 { transferFromSavingsButton .transition(.move(edge: .leading).combined(with: .opacity)) .padding(.top, 28) @@ -114,11 +115,7 @@ struct SpendingWalletScreen: View { .frame(width: 16, height: 16) .foregroundColor(.white80) ) { - if app.hasSeenTransferToSpendingIntro { - navigation.navigate(.spendingAmount) - } else { - navigation.navigate(.spendingIntro) - } + sheets.showSheet(.transferUnavailable) } .accessibilityIdentifier("TransferFromSavings") } diff --git a/BitkitNotification/NotificationService.swift b/BitkitNotification/NotificationService.swift index 273c8e34e..ac90b2433 100644 --- a/BitkitNotification/NotificationService.swift +++ b/BitkitNotification/NotificationService.swift @@ -2,15 +2,9 @@ import Foundation import os.log import UserNotifications -/// Lightweight notification service extension that handles incoming push notifications. +/// Notification service extension that delivers incoming push notifications unchanged. /// -/// IMPORTANT: This extension does NOT start the LDK node due to iOS memory (~24MB) and time (~30s) constraints. -/// Instead, it: -/// 1. Decrypts the notification payload -/// 2. Displays a time-sensitive notification with urgency messaging -/// 3. Saves payment info for the main app to process when opened -/// -/// The main app handles actual Lightning payment processing when the user opens it. +/// Blocktank's encrypted Lightning pushes are gone, so there is no payload to decrypt or rewrite. class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? @@ -21,191 +15,21 @@ class NotificationService: UNNotificationServiceExtension { }() override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { - os_log("🚨 Push received! %{public}@", log: logger, type: .error, request.identifier) - os_log("🔔 UserInfo: %{public}@", log: logger, type: .error, request.content.userInfo) + os_log("🔔 Push received: %{public}@", log: logger, type: .info, request.identifier) self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) - - guard !StateLocker.isLocked(.lightning) else { - os_log("🔔 LDK-node process already locked, app likely in foreground", log: logger, type: .error) - return - } - - Task { - do { - let (notificationType, payload) = try await self.decryptPayload(request) - - // Configure notification content based on type - configureNotificationContent(for: notificationType, payload: payload) - deliver() - } catch { - // Fallback notification if decryption fails - os_log( - "🔔 Failed to decrypt notification payload: %{public}@", - log: logger, - type: .error, - error.localizedDescription - ) - configureFallbackNotification() - deliver() - } - } - } - - func decryptPayload(_ request: UNNotificationRequest) async throws -> (BlocktankNotificationType, [String: Any]) { - guard let aps = request.content.userInfo["aps"] as? AnyObject else { - os_log("🔔 Failed to decrypt payload: missing aps payload", log: logger, type: .error) - throw NSError(domain: "NotificationService", code: 1, userInfo: [NSLocalizedDescriptionKey: "Missing aps payload"]) - } - - guard let alert = aps["alert"] as? AnyObject, - let payload = alert["payload"] as? AnyObject, - let cipher = payload["cipher"] as? String, - let iv = payload["iv"] as? String, - let publicKey = payload["publicKey"] as? String, - let tag = payload["tag"] as? String - else { - os_log("🔔 Failed to decrypt payload: missing details", log: logger, type: .error) - throw NSError(domain: "NotificationService", code: 2, userInfo: [NSLocalizedDescriptionKey: "Missing payload details"]) - } - - guard let ciphertext = Data(base64Encoded: cipher) else { - os_log("🔔 Failed to decrypt payload: failed to decode cipher", log: logger, type: .error) - throw NSError(domain: "NotificationService", code: 3, userInfo: [NSLocalizedDescriptionKey: "Failed to decode cipher"]) - } - - guard let privateKey = try Keychain.load(key: .pushNotificationPrivateKey) else { - os_log("🔔 Failed to decrypt payload: missing pushNotificationPrivateKey", log: logger, type: .error) - throw NSError(domain: "NotificationService", code: 4, userInfo: [NSLocalizedDescriptionKey: "Missing pushNotificationPrivateKey"]) - } - - let password = try Crypto.generateSharedSecret(privateKey: privateKey, nodePubkey: publicKey, derivationName: "bitkit-notifications") - let decrypted = try Crypto.decrypt(.init(cipher: ciphertext, iv: iv.hexaData, tag: tag.hexaData), secretKey: password) - - guard let jsonData = try JSONSerialization.jsonObject(with: decrypted, options: []) as? [String: Any] else { - os_log("🔔 Failed to decrypt payload: failed to convert decrypted data to utf8", log: logger, type: .error) - throw NSError(domain: "NotificationService", code: 5, userInfo: [NSLocalizedDescriptionKey: "Failed to convert decrypted data to utf8"]) - } - - guard let payload = jsonData["payload"] as? [String: Any] else { - os_log("🔔 Failed to decrypt payload: missing payload", log: logger, type: .error) - throw NSError(domain: "NotificationService", code: 6, userInfo: [NSLocalizedDescriptionKey: "Missing payload"]) - } - - guard let typeStr = jsonData["type"] as? String, let type = BlocktankNotificationType(rawValue: typeStr) else { - os_log("🔔 Failed to decrypt payload: missing type", log: logger, type: .error) - throw NSError(domain: "NotificationService", code: 7, userInfo: [NSLocalizedDescriptionKey: "Missing type"]) - } - - os_log("🔔 Decrypted payload: type=%{public}@, payload=%{public}@", log: logger, type: .info, typeStr, payload) - - return (type, payload) - } - - /// Configures notification content based on the notification type - /// - Parameters: - /// - notificationType: The type of notification received - /// - payload: Optional payload data containing additional information (amount, payment hash, etc.) - private func configureNotificationContent(for notificationType: BlocktankNotificationType, payload: [String: Any]?) { - guard let content = bestAttemptContent else { return } - - switch notificationType { - case .incomingHtlc: - content.title = "Incoming Payment" - content.body = "Open Bitkit now to receive your payment" - - case .cjitPaymentArrived: - content.title = "Incoming Payment" - content.body = "Open Bitkit now to receive your payment" - - case .orderPaymentConfirmed: - content.title = "Spending Balance Ready" - content.body = "Open Bitkit to start paying anyone, anywhere." - - case .mutualClose: - content.title = "Spending Balance Expired" - content.body = "Open Bitkit to move funds from spending to savings" - - case .wakeToTimeout: - content.title = "Payment Pending" - content.body = "Open Bitkit to process pending payment" - - @unknown default: - content.title = "Bitkit" - content.body = "Open Bitkit to check for new activity" - } - - // Set time-sensitive interruption level for urgent notifications - content.interruptionLevel = .timeSensitive - content.relevanceScore = 1.0 - - // Store notification type in userInfo for the app to reference - var userInfo = content.userInfo - userInfo["notificationType"] = notificationType.rawValue - content.userInfo = userInfo - - os_log("🔔 Configured notification: type=%{public}@, title=%{public}@", log: logger, type: .info, notificationType.rawValue, content.title) - } - - /// Configures a fallback notification when decryption fails - private func configureFallbackNotification() { - guard let content = bestAttemptContent else { return } - - content.title = "Bitkit" - content.body = "Open Bitkit to check for new activity" - content.sound = .default - content.interruptionLevel = .timeSensitive - - os_log("🔔 Using fallback notification content", log: logger, type: .info) + deliver() } func deliver() { - Task { - if let contentHandler, let bestAttemptContent { - contentHandler(bestAttemptContent) - os_log("🔔 Notification delivered successfully", log: logger, type: .error) - } else { - os_log("🔔 Missing contentHandler or bestAttemptContent", log: logger, type: .error) - } - } - } - - override func serviceExtensionTimeWillExpire() { - os_log("🔔 NotificationService: Delivering notification before timeout", log: logger, type: .error) - - // Called just before the extension will be terminated by the system. - // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. if let contentHandler, let bestAttemptContent { contentHandler(bestAttemptContent) } } -} - -// MARK: - String Extension for Hex Conversion - -private extension String { - var hexaData: Data { - var data = Data() - var hex = self - // Remove any spaces or non-hex characters - hex = hex.replacingOccurrences(of: " ", with: "") - - // Ensure even number of characters - if hex.count % 2 != 0 { - hex = "0" + hex - } - - var index = hex.startIndex - while index < hex.endIndex { - let nextIndex = hex.index(index, offsetBy: 2) - if let byte = UInt8(hex[index ..< nextIndex], radix: 16) { - data.append(byte) - } - index = nextIndex - } - - return data + override func serviceExtensionTimeWillExpire() { + // Called just before the extension will be terminated by the system. + deliver() } } diff --git a/BitkitTests/ActivityListSyncTests.swift b/BitkitTests/ActivityListSyncTests.swift index 4b9ac3a0a..466980a2b 100644 --- a/BitkitTests/ActivityListSyncTests.swift +++ b/BitkitTests/ActivityListSyncTests.swift @@ -29,8 +29,7 @@ final class ActivityListSyncTests: XCTestCase { var loads = 0 let model = Bitkit.ActivityListViewModel( transferService: Bitkit.TransferService( - lightningService: .shared, - blocktankService: Bitkit.CoreService.shared.blocktank + lightningService: .shared ), listPayments: { loads += 1 diff --git a/BitkitTests/AppCacheDataTests.swift b/BitkitTests/AppCacheDataTests.swift new file mode 100644 index 000000000..3b8061c75 --- /dev/null +++ b/BitkitTests/AppCacheDataTests.swift @@ -0,0 +1,21 @@ +@testable import Bitkit +import XCTest + +final class AppCacheDataTests: XCTestCase { + func testBackupWithRemovedBlocktankKeysStillDecodes() throws { + let json = #""" + { + "hasSeenShopIntro": true, + "hasSeenTransferIntro": true, + "hasSeenTransferToSpendingIntro": true, + "blocktankRefundAddress": {"address": "bcrt1refund", "index": 7}, + "lastUsedTags": ["coffee"] + } + """# + + let cache = try JSONDecoder().decode(AppCacheData.self, from: Data(json.utf8)) + + XCTAssertTrue(cache.hasSeenShopIntro) + XCTAssertEqual(cache.lastUsedTags, ["coffee"]) + } +} diff --git a/BitkitTests/BlocktankRefundAddressLiveIntegrationTests.swift b/BitkitTests/BlocktankRefundAddressLiveIntegrationTests.swift deleted file mode 100644 index 3dab3d1b3..000000000 --- a/BitkitTests/BlocktankRefundAddressLiveIntegrationTests.swift +++ /dev/null @@ -1,186 +0,0 @@ -@testable import Bitkit -import BitkitCore -import LDKNode -import XCTest - -@MainActor -final class BlocktankRefundAddressLiveIntegrationTests: XCTestCase { - private let walletIndex = 0 - private let lightning = Bitkit.LightningService.shared - private let core = Bitkit.CoreService.shared - - override func setUp() async throws { - try await super.setUp() - // `resetToDefaults()` in both hooks writes ~30 real keys, and `clear()` drops the user's own - // refund address. The keychain wipe and LDK storage are namespaced under test; these are not. - snapshotAppDefaultsDomain() - // A running node persists address-search indexes as transactions arrive. Teardown blocks run - // last-in, first-out, so stopping it here lands before the restore above; `tearDown()` runs - // after both and would stop it too late. - addTeardownBlock { [lightning] in - let isRunning = await MainActor.run { lightning.status?.isRunning == true } - if isRunning { - try? await lightning.stop() - } - } - try Bitkit.Keychain.wipeEntireKeychain() - Bitkit.SettingsViewModel.shared.resetToDefaults() - Bitkit.BlocktankRefundAddressStore().clear() - } - - override func tearDown() async throws { - lightning.dumpLdkLogs() - if lightning.status?.isRunning == true { - try? await lightning.stop() - } - try? await lightning.wipeStorage(walletIndex: walletIndex) - try Bitkit.Keychain.wipeEntireKeychain() - try await super.tearDown() - } - - func testRefundAddressLifecycleAcrossOrdersRestartAndVssRestore() async throws { - try XCTSkipIf(true, "Blocktank staging no longer creates orders or CJIT entries (synonymdev/blocktank-lsp-http#168)") - guard Bitkit.Env.network == .regtest else { - throw XCTSkip("Blocktank refund address integration requires regtest") - } - - let settings = Bitkit.SettingsViewModel.shared - settings.selectedAddressType = .nativeSegwit - settings.addressTypesToMonitor = [.nativeSegwit] - - let mnemonic = try Bitkit.StartupHandler.createNewWallet(bip39Passphrase: nil, walletIndex: walletIndex) - XCTAssertFalse(mnemonic.isEmpty) - try await lightning.setup(walletIndex: walletIndex) - try await lightning.start() - try await lightning.sync() - XCTAssertNotNil(lightning.nodeId) - - var submittedOptions: [CreateOrderOptions] = [] - var estimatedOptions: [CreateOrderOptions] = [] - let makeViewModel = { - let client = Bitkit.BlocktankOrderClient( - nodeId: { self.lightning.nodeId }, - sign: { try await self.lightning.sign(message: $0) }, - submit: { lspBalanceSat, expiryWeeks, options in - submittedOptions.append(options) - return try await self.core.blocktank.newOrder( - lspBalanceSat: lspBalanceSat, - channelExpiryWeeks: expiryWeeks, - options: options - ) - }, - estimate: { lspBalanceSat, expiryWeeks, options in - estimatedOptions.append(options) - return try await self.core.blocktank.estimateFee( - lspBalanceSat: lspBalanceSat, - channelExpiryWeeks: expiryWeeks, - options: options - ) - } - ) - return Bitkit.BlocktankViewModel( - coreService: self.core, - lightningService: self.lightning, - orderClient: client, - startPolling: false - ) - } - - var viewModel = makeViewModel() - for _ in 0 ..< 5 { - _ = try await viewModel.estimateOrderFee(clientBalance: 0, lspBalance: 100_000) - } - XCTAssertEqual(estimatedOptions.count, 5) - XCTAssertTrue(estimatedOptions.allSatisfy { $0.refundOnchainAddress == nil }) - XCTAssertNil(try Bitkit.BlocktankRefundAddressStore().load(), "Fee estimates must not allocate refund addresses") - - let firstOrder = try await viewModel.createOrder(clientBalance: 0, lspBalance: 100_000) - let firstRefund = try XCTUnwrap(Bitkit.BlocktankRefundAddressStore().load()) - XCTAssertEqual(submittedOptions.last?.refundOnchainAddress, firstRefund.address) - XCTAssertTrue(firstRefund.address.hasPrefix("bcrt1q")) - - let clientFundedOrder = try await viewModel.createOrder(clientBalance: 25000, lspBalance: 100_000) - XCTAssertEqual(submittedOptions.last?.refundOnchainAddress, firstRefund.address) - let derivedFirst = try await lightning.addressInfoForType( - .nativeSegwit, - keychain: .external, - atIndex: firstRefund.index - ) - XCTAssertEqual(derivedFirst.address, firstRefund.address, "Client-funded orders must refund to the internal Bitkit wallet") - - do { - _ = try await viewModel.createOrder(clientBalance: 0, lspBalance: 1) - XCTFail("Expected the staging backend to reject an undersized order") - } catch { - XCTAssertEqual(try Bitkit.BlocktankRefundAddressStore().load(), firstRefund) - } - - try await lightning.stop() - try await lightning.setup(walletIndex: walletIndex) - try await lightning.start() - try await lightning.sync() - viewModel = makeViewModel() - - let restartedOrder = try await viewModel.createOrder(clientBalance: 0, lspBalance: 100_000) - XCTAssertEqual(submittedOptions.last?.refundOnchainAddress, firstRefund.address) - XCTAssertEqual(try Bitkit.BlocktankRefundAddressStore().load(), firstRefund) - - let appCache = try settings.getAppCacheData() - try await lightning.stop() - try await lightning.wipeStorage(walletIndex: walletIndex) - Bitkit.BlocktankRefundAddressStore().clear() - settings.selectedAddressType = .taproot - settings.addressTypesToMonitor = [.taproot] - try settings.restoreAppCacheData(appCache) - XCTAssertEqual(try Bitkit.BlocktankRefundAddressStore().load(), firstRefund) - - try await lightning.setup(walletIndex: walletIndex) - try await lightning.start() - try await lightning.sync() - await settings.pruneEmptyAddressTypesAfterRestore() - XCTAssertTrue(settings.addressTypesToMonitor.contains(.nativeSegwit)) - - viewModel = makeViewModel() - let restoredOrder = try await viewModel.createOrder(clientBalance: 0, lspBalance: 100_000) - XCTAssertEqual(submittedOptions.last?.refundOnchainAddress, firstRefund.address) - XCTAssertEqual(try Bitkit.BlocktankRefundAddressStore().load(), firstRefund) - let addressWasUsedBeforeDeposit = try await core.utility.isAddressUsed(address: firstRefund.address) - XCTAssertFalse(addressWasUsedBeforeDeposit) - - let refundDepositTxId = try await core.blocktank.regtestDepositFunds(address: firstRefund.address, amountSat: 10000) - XCTAssertFalse(refundDepositTxId.isEmpty) - try await core.blocktank.regtestMineBlocks(3) - - let activityDeadline = Date().addingTimeInterval(120) - var recordedPayment = false - repeat { - try await lightning.sync() - if let payments = await lightning.listPayments() { - try await core.activity.syncLdkNodePayments(payments) - } - recordedPayment = try await core.utility.isAddressUsed(address: firstRefund.address) - if !recordedPayment { - try await Task.sleep(nanoseconds: 3_000_000_000) - } - } while !recordedPayment && Date() < activityDeadline - XCTAssertTrue(recordedPayment, "Incoming refund-address payment must be recorded in the local activity database") - - let rotatedOrder = try await viewModel.createOrder(clientBalance: 0, lspBalance: 100_000) - let rotatedRefund = try XCTUnwrap(Bitkit.BlocktankRefundAddressStore().load()) - XCTAssertEqual(rotatedRefund.index, firstRefund.index + 1) - XCTAssertNotEqual(rotatedRefund.address, firstRefund.address) - XCTAssertEqual(submittedOptions.last?.refundOnchainAddress, rotatedRefund.address) - - let reusedRotatedOrder = try await viewModel.createOrder(clientBalance: 0, lspBalance: 100_000) - XCTAssertEqual(try Bitkit.BlocktankRefundAddressStore().load(), rotatedRefund) - XCTAssertEqual(submittedOptions.last?.refundOnchainAddress, rotatedRefund.address) - - Bitkit.Logger.test( - "Refund lifecycle orders: first=\(firstOrder.id), clientFunded=\(clientFundedOrder.id), restart=\(restartedOrder.id), " + - "restore=\(restoredOrder.id), rotated=\(rotatedOrder.id), rotatedReuse=\(reusedRotatedOrder.id); " + - "refundAddress=\(firstRefund.address), refundIndex=\(firstRefund.index), refundDepositTxId=\(refundDepositTxId), " + - "rotatedAddress=\(rotatedRefund.address), rotatedIndex=\(rotatedRefund.index)", - context: "BlocktankRefundAddressLiveIntegrationTests" - ) - } -} diff --git a/BitkitTests/BlocktankRefundAddressProviderTests.swift b/BitkitTests/BlocktankRefundAddressProviderTests.swift deleted file mode 100644 index 945332c8e..000000000 --- a/BitkitTests/BlocktankRefundAddressProviderTests.swift +++ /dev/null @@ -1,538 +0,0 @@ -@testable import Bitkit -import BitkitCore -import LDKNode -import XCTest - -@MainActor -final class BlocktankRefundAddressProviderTests: XCTestCase { - private enum StubError: Error { - case lookup - case reveal - case persist - case usage - case estimate - case submit - } - - private final class State { - var cached: BlocktankRefundAddress? - var usedAddresses: Set = [] - var allocationCount = 0 - var lookupCount = 0 - var revealCount = 0 - } - - private final class StubRefundProvider: BlocktankRefundAddressProviding { - var callCount = 0 - var result: Result - - init(result: Result) { - self.result = result - } - - func addressForOrder() async throws -> String { - callCount += 1 - return try result.get() - } - } - - override func setUp() { - super.setUp() - // `clear()` here and in tearDown, `restoreAppCacheData`, `resetToDefaults()` and the corrupt - // cache fixture all write UserDefaults.standard — the host app's own preferences. - snapshotAppDefaultsDomain() - BlocktankRefundAddressStore().clear() - } - - private func makeProvider( - state: State, - lookup: ((UInt32) async throws -> BlocktankRefundAddress)? = nil, - reveal: ((UInt32) async throws -> Void)? = nil, - save: ((BlocktankRefundAddress) throws -> Void)? = nil, - isUsed: ((String) async throws -> Bool)? = nil, - allocate: (() async throws -> BlocktankRefundAddress)? = nil, - allocationDelayNanoseconds: UInt64 = 0 - ) -> BlocktankRefundAddressProvider { - BlocktankRefundAddressProvider( - load: { state.cached }, - save: save ?? { state.cached = $0 }, - lookup: lookup ?? { index in - state.lookupCount += 1 - guard let cached = state.cached else { throw StubError.lookup } - return BlocktankRefundAddress(address: cached.address, index: index) - }, - reveal: reveal ?? { _ in state.revealCount += 1 }, - isUsed: isUsed ?? { state.usedAddresses.contains($0) }, - allocate: allocate ?? { - if allocationDelayNanoseconds > 0 { - try await Task.sleep(nanoseconds: allocationDelayNanoseconds) - } - let index = UInt32(state.allocationCount) - state.allocationCount += 1 - return BlocktankRefundAddress(address: "refund-\(index)", index: index) - } - ) - } - - func testUnpaidAndFailedOrdersReuseOneAddressAcrossProviderRestarts() async throws { - let state = State() - - for _ in 0 ..< 8 { - let provider = makeProvider(state: state) - let address = try await provider.addressForOrder() - XCTAssertEqual(address, "refund-0") - } - - XCTAssertEqual(state.allocationCount, 1) - XCTAssertEqual(state.cached, BlocktankRefundAddress(address: "refund-0", index: 0)) - } - - func testRecordedPaymentRotatesExactlyOnce() async throws { - let state = State() - let provider = makeProvider(state: state) - - let firstAddress = try await provider.addressForOrder() - XCTAssertEqual(firstAddress, "refund-0") - state.usedAddresses.insert("refund-0") - - let rotatedAddress = try await provider.addressForOrder() - let reusedAddress = try await provider.addressForOrder() - XCTAssertEqual(rotatedAddress, "refund-1") - XCTAssertEqual(reusedAddress, "refund-1") - XCTAssertEqual(state.allocationCount, 2) - XCTAssertEqual(state.cached, BlocktankRefundAddress(address: "refund-1", index: 1)) - } - - func testAllocationSkipsUsedCandidatesUntilFirstUnusedAddress() async throws { - let state = State() - state.usedAddresses = ["refund-0", "refund-1"] - let provider = makeProvider(state: state) - - let address = try await provider.addressForOrder() - - XCTAssertEqual(address, "refund-2") - XCTAssertEqual(state.allocationCount, 3) - XCTAssertEqual(state.cached, BlocktankRefundAddress(address: "refund-2", index: 2)) - } - - func testAllocationStopsAfterTwentyUsedCandidatesAndPreservesCache() async { - let state = State() - state.cached = BlocktankRefundAddress(address: "refund-0", index: 0) - state.allocationCount = 1 - state.usedAddresses = Set((0 ... 20).map { "refund-\($0)" }) - let provider = makeProvider(state: state) - - await XCTAssertThrowsErrorAsync({ try await provider.addressForOrder() }) { error in - XCTAssertEqual(error as? BlocktankRefundAddressError, .allocationAttemptsExhausted) - } - XCTAssertEqual(state.allocationCount, 21) - XCTAssertEqual(state.cached, BlocktankRefundAddress(address: "refund-0", index: 0)) - } - - func testAllocationUsageCheckFailurePreservesCache() async { - let state = State() - state.cached = BlocktankRefundAddress(address: "refund-0", index: 0) - state.allocationCount = 1 - let provider = makeProvider(state: state, isUsed: { address in - if address == "refund-0" { - return true - } - throw StubError.usage - }) - - await XCTAssertThrowsErrorAsync({ try await provider.addressForOrder() }) { error in - XCTAssertTrue(error is StubError) - } - XCTAssertEqual(state.allocationCount, 2) - XCTAssertEqual(state.cached, BlocktankRefundAddress(address: "refund-0", index: 0)) - } - - func testAllocationRejectsNonAdvancingIndex() async { - let state = State() - var candidates = [ - BlocktankRefundAddress(address: "refund-used", index: 7), - BlocktankRefundAddress(address: "refund-unused", index: 7), - ] - let provider = makeProvider( - state: state, - isUsed: { $0 == "refund-used" }, - allocate: { candidates.removeFirst() } - ) - - await XCTAssertThrowsErrorAsync({ try await provider.addressForOrder() }) { error in - XCTAssertEqual(error as? BlocktankRefundAddressError, .allocationDidNotAdvance) - } - XCTAssertNil(state.cached) - } - - func testConcurrentCallsCoalesceOneAllocation() async throws { - let state = State() - let provider = makeProvider(state: state, allocationDelayNanoseconds: 50_000_000) - - async let first = provider.addressForOrder() - async let second = provider.addressForOrder() - async let third = provider.addressForOrder() - let addresses = try await [first, second, third] - - XCTAssertEqual(addresses, ["refund-0", "refund-0", "refund-0"]) - XCTAssertEqual(state.allocationCount, 1) - } - - func testCachedAddressIsLookedUpAndRevealedBeforeReuse() async throws { - let state = State() - state.cached = BlocktankRefundAddress(address: "refund-7", index: 7) - let provider = makeProvider(state: state) - - let address = try await provider.addressForOrder() - XCTAssertEqual(address, "refund-7") - XCTAssertEqual(state.lookupCount, 1) - XCTAssertEqual(state.revealCount, 1) - XCTAssertEqual(state.allocationCount, 0) - } - - func testOwnershipMismatchIsRejected() async { - let state = State() - state.cached = BlocktankRefundAddress(address: "other-wallet", index: 7) - let provider = makeProvider(state: state, lookup: { index in - BlocktankRefundAddress(address: "active-wallet", index: index) - }) - - await XCTAssertThrowsErrorAsync({ try await provider.addressForOrder() }) { error in - XCTAssertEqual(error as? BlocktankRefundAddressError, .ownershipMismatch) - } - XCTAssertEqual(state.allocationCount, 0) - } - - func testHardenedIndexIsRejected() async { - let state = State() - state.cached = BlocktankRefundAddress(address: "refund", index: UInt32(Int32.max) + 1) - let provider = makeProvider(state: state) - - await XCTAssertThrowsErrorAsync({ try await provider.addressForOrder() }) { error in - XCTAssertEqual(error as? BlocktankRefundAddressError, .indexOutOfRange(UInt32(Int32.max) + 1)) - } - XCTAssertEqual(state.lookupCount, 0) - XCTAssertEqual(state.allocationCount, 0) - } - - func testLookupFailureDoesNotAllocate() async { - let state = State() - state.cached = BlocktankRefundAddress(address: "refund", index: 1) - let provider = makeProvider(state: state, lookup: { _ in throw StubError.lookup }) - - await XCTAssertThrowsErrorAsync { try await provider.addressForOrder() } - XCTAssertEqual(state.allocationCount, 0) - } - - func testRevealFailureDoesNotAllocate() async { - let state = State() - state.cached = BlocktankRefundAddress(address: "refund", index: 1) - let provider = makeProvider(state: state, reveal: { _ in throw StubError.reveal }) - - await XCTAssertThrowsErrorAsync { try await provider.addressForOrder() } - XCTAssertEqual(state.allocationCount, 0) - } - - func testCachePersistenceFailureReturnsNoAddress() async { - let state = State() - let provider = makeProvider(state: state, save: { _ in throw StubError.persist }) - - await XCTAssertThrowsErrorAsync { try await provider.addressForOrder() } - XCTAssertEqual(state.allocationCount, 1) - XCTAssertNil(state.cached) - } - - func testAppCacheSerializationMatchesSharedOptionalShape() throws { - let json = #"{"blocktankRefundAddress":{"address":"bcrt1refund","index":7}}"# - let cache = try JSONDecoder().decode(AppCacheData.self, from: Data(json.utf8)) - - XCTAssertEqual(cache.blocktankRefundAddress, BlocktankRefundAddress(address: "bcrt1refund", index: 7)) - - let encoded = try JSONEncoder().encode(cache) - let object = try XCTUnwrap(JSONSerialization.jsonObject(with: encoded) as? [String: Any]) - let refund = try XCTUnwrap(object["blocktankRefundAddress"] as? [String: Any]) - XCTAssertEqual(refund["address"] as? String, "bcrt1refund") - XCTAssertEqual(refund["index"] as? Int, 7) - } - - func testOlderAppCacheWithoutRefundAddressDecodesAsNil() throws { - let cache = try JSONDecoder().decode(AppCacheData.self, from: Data("{}".utf8)) - - XCTAssertNil(cache.blocktankRefundAddress) - } - - func testStoreKeepsRefundAddressesSeparateByNetwork() throws { - let defaults = try XCTUnwrap(UserDefaults(suiteName: #function)) - defer { defaults.removePersistentDomain(forName: #function) } - let regtestStore = BlocktankRefundAddressStore(defaults: defaults, network: .regtest) - let bitcoinStore = BlocktankRefundAddressStore(defaults: defaults, network: .bitcoin) - let regtestAddress = BlocktankRefundAddress(address: "bcrt1qrefund", index: 7) - let bitcoinAddress = BlocktankRefundAddress(address: "bc1qrefund", index: 9) - - try regtestStore.save(regtestAddress) - XCTAssertEqual(try regtestStore.load(), regtestAddress) - XCTAssertNil(try bitcoinStore.load()) - - try bitcoinStore.save(bitcoinAddress) - XCTAssertEqual(try bitcoinStore.load(), bitcoinAddress) - XCTAssertEqual(try regtestStore.load(), regtestAddress) - } - - func testLegacyCacheMigratesOnlyWhenPrefixIdentifiesNetwork() throws { - let cases: [(LDKNode.Network, String)] = [ - (.bitcoin, " bc1qrefund "), - (.regtest, " bcrt1qrefund "), - ] - - for (offset, testCase) in cases.enumerated() { - let suiteName = "\(#function)-\(offset)" - let defaults = try XCTUnwrap(UserDefaults(suiteName: suiteName)) - defer { defaults.removePersistentDomain(forName: suiteName) } - let legacy = BlocktankRefundAddress(address: testCase.1, index: 7) - try defaults.set(JSONEncoder().encode(legacy), forKey: BlocktankRefundAddressStore.legacyKey) - - let store = BlocktankRefundAddressStore(defaults: defaults, network: testCase.0) - XCTAssertEqual(try store.load(), legacy) - XCTAssertNil(defaults.object(forKey: BlocktankRefundAddressStore.legacyKey)) - let migrated = try XCTUnwrap( - defaults.data(forKey: BlocktankRefundAddressStore.key(for: testCase.0)) - ) - XCTAssertEqual( - try JSONDecoder().decode(BlocktankRefundAddress.self, from: migrated), - legacy - ) - } - } - - func testClearPreservesLegacyCacheFromAnotherNetwork() throws { - let defaults = try XCTUnwrap(UserDefaults(suiteName: #function)) - defer { defaults.removePersistentDomain(forName: #function) } - let legacy = BlocktankRefundAddress(address: "bcrt1qrefund", index: 7) - try defaults.set(JSONEncoder().encode(legacy), forKey: BlocktankRefundAddressStore.legacyKey) - - BlocktankRefundAddressStore(defaults: defaults, network: .bitcoin).clear() - XCTAssertNotNil(defaults.data(forKey: BlocktankRefundAddressStore.legacyKey)) - - BlocktankRefundAddressStore(defaults: defaults, network: .regtest).clear() - XCTAssertNil(defaults.object(forKey: BlocktankRefundAddressStore.legacyKey)) - } - - func testAmbiguousTestnetLegacyCacheIsPreserved() throws { - let defaults = try XCTUnwrap(UserDefaults(suiteName: #function)) - defer { defaults.removePersistentDomain(forName: #function) } - let legacy = BlocktankRefundAddress(address: "tb1qrefund", index: 7) - try defaults.set(JSONEncoder().encode(legacy), forKey: BlocktankRefundAddressStore.legacyKey) - - XCTAssertNil(try BlocktankRefundAddressStore(defaults: defaults, network: .testnet).load()) - XCTAssertNil(try BlocktankRefundAddressStore(defaults: defaults, network: .signet).load()) - BlocktankRefundAddressStore(defaults: defaults, network: .testnet).clear() - BlocktankRefundAddressStore(defaults: defaults, network: .signet).clear() - XCTAssertNotNil(defaults.data(forKey: BlocktankRefundAddressStore.legacyKey)) - } - - func testScopedCacheTakesPrecedenceOverLegacyCache() throws { - let defaults = try XCTUnwrap(UserDefaults(suiteName: #function)) - defer { defaults.removePersistentDomain(forName: #function) } - let store = BlocktankRefundAddressStore(defaults: defaults, network: .regtest) - let scoped = BlocktankRefundAddress(address: "bcrt1qscoped", index: 8) - let legacy = BlocktankRefundAddress(address: "bc1qlegacy", index: 7) - try store.save(scoped) - try defaults.set(JSONEncoder().encode(legacy), forKey: BlocktankRefundAddressStore.legacyKey) - - XCTAssertEqual(try store.load(), scoped) - XCTAssertNotNil(defaults.data(forKey: BlocktankRefundAddressStore.legacyKey)) - } - - func testUnknownLegacyCacheFailsClosedAndIsCleared() throws { - let defaults = try XCTUnwrap(UserDefaults(suiteName: #function)) - defer { defaults.removePersistentDomain(forName: #function) } - let legacy = BlocktankRefundAddress(address: "unknown", index: 7) - try defaults.set(JSONEncoder().encode(legacy), forKey: BlocktankRefundAddressStore.legacyKey) - let store = BlocktankRefundAddressStore(defaults: defaults, network: .regtest) - - XCTAssertThrowsError(try store.load()) { error in - XCTAssertEqual(error as? BlocktankRefundAddressError, .invalidCache) - } - store.clear() - XCTAssertNil(defaults.object(forKey: BlocktankRefundAddressStore.legacyKey)) - } - - func testSettingsCacheRestoreRoundTripAndWipe() throws { - let json = #"{"blocktankRefundAddress":{"address":"bcrt1refund","index":7}}"# - let restored = try JSONDecoder().decode(AppCacheData.self, from: Data(json.utf8)) - - try SettingsViewModel.shared.restoreAppCacheData(restored) - - XCTAssertEqual( - try SettingsViewModel.shared.getAppCacheData().blocktankRefundAddress, - BlocktankRefundAddress(address: "bcrt1refund", index: 7) - ) - XCTAssertTrue(SettingsBackupConfig.appStateKeys.contains(BlocktankRefundAddressStore.key)) - - SettingsViewModel.shared.resetToDefaults() - XCTAssertNil(try BlocktankRefundAddressStore().load()) - } - - func testCorruptLocalCacheIsRejected() throws { - UserDefaults.standard.set(Data("not-json".utf8), forKey: BlocktankRefundAddressStore.key) - - XCTAssertThrowsError(try BlocktankRefundAddressStore().load()) { error in - XCTAssertEqual(error as? BlocktankRefundAddressError, .invalidCache) - } - } - - func testEstimatesNeverResolveRefundAddress() async { - let refundProvider = StubRefundProvider(result: .success("refund")) - var estimateCount = 0 - let viewModel = makeViewModel(refundProvider: refundProvider, estimate: { _, _, _ in - estimateCount += 1 - throw StubError.estimate - }) - - for _ in 0 ..< 8 { - await XCTAssertThrowsErrorAsync { - try await viewModel.estimateOrderFee(clientBalance: 1, lspBalance: 2) - } - } - - XCTAssertEqual(refundProvider.callCount, 0) - XCTAssertEqual(estimateCount, 8) - } - - func testCreateOrderSubmitsResolvedRefundAddress() async { - let refundProvider = StubRefundProvider(result: .success("refund-address")) - var submittedOptions: CreateOrderOptions? - let viewModel = makeViewModel(refundProvider: refundProvider, submit: { _, _, options in - submittedOptions = options - throw StubError.submit - }) - - await XCTAssertThrowsErrorAsync { - try await viewModel.createOrder(clientBalance: 1, lspBalance: 2) - } - - XCTAssertEqual(refundProvider.callCount, 1) - XCTAssertEqual(submittedOptions?.refundOnchainAddress, "refund-address") - } - - func testRefundResolutionFailureBlocksSubmission() async { - let refundProvider = StubRefundProvider(result: .failure(StubError.lookup)) - var submitCount = 0 - let viewModel = makeViewModel(refundProvider: refundProvider, submit: { _, _, _ in - submitCount += 1 - throw StubError.submit - }) - - await XCTAssertThrowsErrorAsync { - try await viewModel.createOrder(clientBalance: 1, lspBalance: 2) - } - - XCTAssertEqual(submitCount, 0) - } - - func testMissingNodeBlocksResolutionAndSubmission() async { - let refundProvider = StubRefundProvider(result: .success("refund")) - var submitCount = 0 - let orderClient = BlocktankOrderClient( - nodeId: { nil }, - sign: { _ in "signature" }, - submit: { _, _, _ in - submitCount += 1 - throw StubError.submit - }, - estimate: { _, _, _ in throw StubError.estimate } - ) - let viewModel = BlocktankViewModel( - orderClient: orderClient, - refundAddressProvider: refundProvider, - startPolling: false - ) - - await XCTAssertThrowsErrorAsync { - try await viewModel.createOrder(clientBalance: 1, lspBalance: 2) - } - - XCTAssertEqual(refundProvider.callCount, 0) - XCTAssertEqual(submitCount, 0) - } - - func testCancellationBeforeResolutionBlocksAllocationAndSubmission() async { - let refundProvider = StubRefundProvider(result: .success("refund")) - var submitCount = 0 - let viewModel = makeViewModel(refundProvider: refundProvider, submit: { _, _, _ in - submitCount += 1 - throw StubError.submit - }) - - let task = Task { @MainActor in - withUnsafeCurrentTask { $0?.cancel() } - return try await viewModel.createOrder(clientBalance: 1, lspBalance: 2) - } - - await XCTAssertThrowsErrorAsync({ try await task.value }) { error in - XCTAssertTrue(error is CancellationError) - } - XCTAssertEqual(refundProvider.callCount, 0) - XCTAssertEqual(submitCount, 0) - } - - func testCancelledWaiterDoesNotSubmitWhileOtherWaiterCompletesResolution() async { - let state = State() - let provider = makeProvider(state: state, allocationDelayNanoseconds: 100_000_000) - var submitCount = 0 - let viewModel = makeViewModel(refundProvider: provider, submit: { _, _, _ in - submitCount += 1 - throw StubError.submit - }) - - let cancelled = Task { @MainActor in - try await viewModel.createOrder(clientBalance: 1, lspBalance: 2) - } - await Task.yield() - let active = Task { @MainActor in - try await viewModel.createOrder(clientBalance: 1, lspBalance: 2) - } - cancelled.cancel() - - await XCTAssertThrowsErrorAsync({ try await cancelled.value }) { error in - XCTAssertTrue(error is CancellationError) - } - await XCTAssertThrowsErrorAsync({ try await active.value }) { error in - XCTAssertTrue(error is StubError) - } - XCTAssertEqual(state.allocationCount, 1) - XCTAssertEqual(submitCount, 1) - } - - private func makeViewModel( - refundProvider: any BlocktankRefundAddressProviding, - submit: @escaping BlocktankOrderClient.Submit = { _, _, _ in throw StubError.submit }, - estimate: @escaping BlocktankOrderClient.Estimate = { _, _, _ in throw StubError.estimate } - ) -> BlocktankViewModel { - let orderClient = BlocktankOrderClient( - nodeId: { "node-id" }, - sign: { _ in "signature" }, - submit: submit, - estimate: estimate - ) - return BlocktankViewModel( - orderClient: orderClient, - refundAddressProvider: refundProvider, - startPolling: false - ) - } - - private func XCTAssertThrowsErrorAsync( - _ expression: () async throws -> some Any, - _ errorHandler: (Error) -> Void = { _ in }, - file: StaticString = #filePath, - line: UInt = #line - ) async { - do { - _ = try await expression() - XCTFail("Expected error", file: file, line: line) - } catch { - errorHandler(error) - } - } -} diff --git a/BitkitTests/BlocktankTests.swift b/BitkitTests/BlocktankTests.swift index b8f95c24e..0a01bb857 100644 --- a/BitkitTests/BlocktankTests.swift +++ b/BitkitTests/BlocktankTests.swift @@ -57,85 +57,6 @@ final class BlocktankTests: XCTestCase { XCTAssertGreaterThan(info.onchain.feeRates.slow, 0, "Slow fee rate should be greater than 0") } - func testCreateCjitOrder() async throws { - try XCTSkipIf(true, "Blocktank staging no longer creates orders or CJIT entries (synonymdev/blocktank-lsp-http#168)") - // Test creating a CJIT order - let channelSizeSat: UInt64 = 100_000 // 100k sats - let invoiceSat: UInt64 = 10000 // 10k sats for the invoice - let invoiceDescription = "Test CJIT order" - let nodeId = "03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad" // Example node ID - let channelExpiryWeeks: UInt32 = 6 - let options = CreateCjitOptions(source: "bitkit-ios", discountCode: nil) - - let cjitEntry = try await service.createCjit( - channelSizeSat: channelSizeSat, - invoiceSat: invoiceSat, - invoiceDescription: invoiceDescription, - nodeId: nodeId, - channelExpiryWeeks: channelExpiryWeeks, - options: options - ) - - // Verify CJIT entry - XCTAssertNotNil(cjitEntry) - XCTAssertFalse(cjitEntry.id.isEmpty, "CJIT entry ID should not be empty") - XCTAssertEqual(cjitEntry.channelSizeSat, channelSizeSat, "Channel size should match requested amount") - XCTAssertEqual(cjitEntry.source, "bitkit-ios", "Source should be bitkit-ios") - XCTAssertNotNil(cjitEntry.lspNode, "LSP node should not be nil") - XCTAssertFalse(cjitEntry.lspNode.pubkey.isEmpty, "LSP node pubkey should not be empty") - XCTAssertEqual(cjitEntry.nodeId, nodeId, "Node ID should match requested ID") - XCTAssertEqual(cjitEntry.channelExpiryWeeks, channelExpiryWeeks, "Channel expiry weeks should match") - - // Test getting CJIT entries - let entries = try await service.cjitOrders(entryIds: [cjitEntry.id], filter: nil, refresh: true) - Logger.test("CjitEntries: \(entries.count)") - XCTAssertFalse(entries.isEmpty, "Should retrieve created CJIT entry") - XCTAssertEqual(entries.first?.id, cjitEntry.id, "Retrieved entry should match created entry") - } - - func testOrders() async throws { - try XCTSkipIf(true, "Blocktank staging no longer creates orders or CJIT entries (synonymdev/blocktank-lsp-http#168)") - // Test creating an order - let lspBalanceSat: UInt64 = 100_000 - let channelExpiryWeeks: UInt32 = 2 - let options = CreateOrderOptions( - clientBalanceSat: 0, - lspNodeId: nil, - couponCode: "", - source: "bitkit-ios", - discountCode: nil, - zeroConf: false, - zeroConfPayment: true, - zeroReserve: false, - clientNodeId: nil, - signature: nil, - timestamp: nil, - refundOnchainAddress: nil, - announceChannel: true - ) - - let order = try await service.newOrder( - lspBalanceSat: lspBalanceSat, - channelExpiryWeeks: channelExpiryWeeks, - options: options - ) - - // Verify order - XCTAssertNotNil(order) - XCTAssertFalse(order.id.isEmpty, "Order ID should not be empty") - XCTAssertEqual(order.state, .created, "Initial state should be created") - XCTAssertEqual(order.state2, .created, "Initial state2 should be created") - XCTAssertEqual(order.lspBalanceSat, lspBalanceSat, "LSP balance should match requested amount") - XCTAssertEqual(order.clientBalanceSat, 0, "Client balance should be zero") - XCTAssertEqual(order.channelExpiryWeeks, channelExpiryWeeks, "Channel expiry weeks should match") - XCTAssertEqual(order.source, "bitkit-ios", "Source should be bitkit-ios") - - // Test getting orders - let orders = try await service.orders(orderIds: [order.id], filter: nil, refresh: true) - XCTAssertFalse(orders.isEmpty, "Orders list should not be empty") - XCTAssertEqual(orders.first?.id, order.id, "Retrieved order should match created order") - } - func testPerformanceExample() { // This is an example of a performance test case. measure { diff --git a/BitkitTests/BlocktankViewModelCjitTests.swift b/BitkitTests/BlocktankViewModelCjitTests.swift deleted file mode 100644 index d6e9a8eff..000000000 --- a/BitkitTests/BlocktankViewModelCjitTests.swift +++ /dev/null @@ -1,57 +0,0 @@ -@testable import Bitkit -import BitkitCore -import XCTest - -final class BlocktankViewModelCjitTests: XCTestCase { - func testWrappedNodeCapacityErrorNormalizesToDistinctError() { - let wrappedError = AppError( - message: AppError.genericMessage, - debugMessage: "Failed to create CJIT entry: Node capacity is above our capacity limit" - ) - - let normalizedError = BlocktankViewModel.normalizedCreateCjitError(wrappedError) - - XCTAssertEqual(String(describing: normalizedError), String(describing: CustomServiceError.cjitNodeCapacityExceeded)) - XCTAssertFalse(normalizedError.isChannelSizeExceedsMaximum) - } - - func testMaxChannelSizeErrorDoesNotNormalizeToNodeCapacity() { - let wrappedError = AppError( - message: AppError.genericMessage, - debugMessage: "Failed to create CJIT entry: Channel size is too big. Checkout maxChannelSizeSat in the getInfo call." - ) - - let normalizedError = BlocktankViewModel.normalizedCreateCjitError(wrappedError) - - XCTAssertEqual(String(describing: normalizedError), String(describing: CustomServiceError.channelSizeExceedsMaximum)) - XCTAssertFalse(normalizedError.isCjitNodeCapacityExceeded) - } - - func testCjitEntryValidationRejectsFeeGreaterThanReceiveAmount() { - let entry = IcJitEntry.mock(feeSat: 5001) - - XCTAssertThrowsError( - try BlocktankViewModel.validateCjitEntry(entry, receiveAmountSats: 5000) - ) { error in - XCTAssertEqual(String(describing: error), String(describing: CustomServiceError.invalidCjitQuote)) - } - } - - func testCjitEntryValidationRejectsUserBalanceAboveChannelSize() { - let entry = IcJitEntry.mock(channelSizeSat: 3000, feeSat: 1000) - - XCTAssertThrowsError( - try BlocktankViewModel.validateCjitEntry(entry, receiveAmountSats: 5000) - ) { error in - XCTAssertEqual(String(describing: error), String(describing: CustomServiceError.invalidCjitQuote)) - } - } - - func testCjitEntryValidationAllowsValidQuote() { - let entry = IcJitEntry.mock(channelSizeSat: 10000, feeSat: 1000) - - XCTAssertNoThrow( - try BlocktankViewModel.validateCjitEntry(entry, receiveAmountSats: 5000) - ) - } -} diff --git a/BitkitTests/ChannelPurchaseFlow.swift b/BitkitTests/ChannelPurchaseFlow.swift deleted file mode 100644 index 47b585c65..000000000 --- a/BitkitTests/ChannelPurchaseFlow.swift +++ /dev/null @@ -1,280 +0,0 @@ -@testable import Bitkit -import BitkitCore -import XCTest - -final class PaymentFlowTests: XCTestCase { - let walletIndex = 0 - let blocktank = CoreService.shared.blocktank - let lightning = LightningService.shared - - override func setUp() async throws { - try await super.setUp() - // `StartupHandler.createNewWallet` resets `selectedAddressType` and `addressTypesToMonitor` for a - // new wallet, and the running node persists address-search indexes. Teardown blocks run - // last-in, first-out, so the node stop registered below runs before this restore. - snapshotAppDefaultsDomain() - addTeardownBlock { [lightning] in - let isRunning = await MainActor.run { lightning.status?.isRunning == true } - if isRunning { - try? await lightning.stop() - } - } - Logger.test("Starting payment flow test setup", context: "PaymentFlowTests") - - // Wipe the keychain before starting tests - Logger.test("Wiping keychain before test", context: "PaymentFlowTests") - try Keychain.wipeEntireKeychain() - Logger.test("Keychain wiped successfully", context: "PaymentFlowTests") - } - - override func tearDown() async throws { - // Dump LDK logs for debugging - lightning.dumpLdkLogs() - - // Clean up after each test - Logger.test("Tearing down payment flow test", context: "PaymentFlowTests") - - // Wipe the keychain after test completion - Logger.test("Wiping keychain after test", context: "PaymentFlowTests") - try Keychain.wipeEntireKeychain() - Logger.test("Keychain wiped successfully", context: "PaymentFlowTests") - - let isRunning = await MainActor.run { lightning.status?.isRunning == true } - if isRunning { - try? await lightning.stop() - } - try? await lightning.wipeStorage(walletIndex: walletIndex) - try await super.tearDown() - } - - func testchannelPurchaseFlow() async throws { - try XCTSkipIf(true, "Blocktank staging no longer creates orders or CJIT entries (synonymdev/blocktank-lsp-http#168)") - Logger.test("Starting payment flow test", context: "PaymentFlowTests") - - // Create a new wallet using StartupHandler - Logger.test("Creating new wallet", context: "PaymentFlowTests") - let mnemonic = try StartupHandler.createNewWallet(bip39Passphrase: nil, walletIndex: walletIndex) - XCTAssertFalse(mnemonic.isEmpty, "Mnemonic should not be empty") - Logger.test("Wallet created successfully", context: "PaymentFlowTests") - - // Set up the lightning service with the wallet index - Logger.test("Setting up lightning service with wallet index \(walletIndex)", context: "PaymentFlowTests") - try await lightning.setup(walletIndex: walletIndex) - Logger.test("Lightning service setup complete", context: "PaymentFlowTests") - - // Start the node - Logger.test("Starting lightning node", context: "PaymentFlowTests") - try await lightning.start() - Logger.test("Lightning node started successfully", context: "PaymentFlowTests") - - // Verify node status - let status = await MainActor.run { lightning.status } - XCTAssertNotNil(status, "Node status should not be nil") - Logger.test("Node status: \(String(describing: status))", context: "PaymentFlowTests") - - // Verify node ID - let nodeId = lightning.nodeId - XCTAssertNotNil(nodeId, "Node ID should not be nil") - XCTAssertFalse(nodeId?.isEmpty ?? true, "Node ID should not be empty") - Logger.test("Node ID: \(String(describing: nodeId))", context: "PaymentFlowTests") - - // Test wallet sync - Logger.test("Syncing wallet", context: "PaymentFlowTests") - try await lightning.sync() - Logger.test("Wallet sync complete", context: "PaymentFlowTests") - - // Verify balances - let balances = await MainActor.run { lightning.balances } - XCTAssertNotNil(balances, "Balances should not be nil") - let initialTotal = balances?.totalOnchainBalanceSats ?? 0 - XCTAssertEqual(initialTotal, 0, "Initial balance should be zero") - Logger.test("Initial balance: \(initialTotal) sats", context: "PaymentFlowTests") - - // Generate a new address - Logger.test("Generating new address", context: "PaymentFlowTests") - let address = try await lightning.newAddress() - XCTAssertFalse(address.isEmpty, "Address should not be empty") - Logger.test("Generated address: \(address)", context: "PaymentFlowTests") - - // Test connecting to trusted peers - Logger.test("Connecting to trusted peers", context: "PaymentFlowTests") - try await lightning.connectToTrustedPeers() - Logger.test("Connected to trusted peers", context: "PaymentFlowTests") - - // Verify peers - let peers = await MainActor.run { lightning.peers } - XCTAssertNotNil(peers, "Peers should not be nil") - Logger.test("Connected peers: \(String(describing: peers))", context: "PaymentFlowTests") - - // Generate an address to receive funds - Logger.test("Generating deposit address", context: "PaymentFlowTests") - let depositAddress = try await lightning.newAddress() - Logger.test("Deposit address: \(depositAddress)", context: "PaymentFlowTests") - - // Fund the wallet using regtest deposit function - let depositAmount: UInt64 = 1_000_000 // 1M sats - Logger.test("Depositing \(depositAmount) sats to wallet", context: "PaymentFlowTests") - let txId = try await blocktank.regtestDepositFunds(address: depositAddress, amountSat: depositAmount) - XCTAssertFalse(txId.isEmpty, "Transaction ID should not be empty") - Logger.test("Deposit transaction ID: \(txId)", context: "PaymentFlowTests") - - // Mine some blocks to confirm the transaction - Logger.test("Mining 1 block to confirm transaction", context: "PaymentFlowTests") - try await blocktank.regtestMineBlocks(1) - Logger.test("Blocks mined successfully", context: "PaymentFlowTests") - - // Sleep 10 seconds to ensure the blocks are mined - Logger.test("Waiting 10 seconds for blocks to be processed", context: "PaymentFlowTests") - try await Task.sleep(nanoseconds: 10_000_000_000) - Logger.test("Wait completed", context: "PaymentFlowTests") - - // Sync the wallet to see the new balance - Logger.test("Syncing wallet to update balances", context: "PaymentFlowTests") - try await lightning.sync() - Logger.test("Wallet sync complete", context: "PaymentFlowTests") - - // Verify updated balances after funding - let updatedBalances = await MainActor.run { lightning.balances } - XCTAssertNotNil(updatedBalances, "Updated balances should not be nil") - let fundedTotal = updatedBalances?.totalOnchainBalanceSats ?? 0 - XCTAssertGreaterThan(fundedTotal, initialTotal, "Balance should have increased after funding") - XCTAssertGreaterThanOrEqual(fundedTotal, depositAmount, "Balance should be at least the deposit amount") - Logger.test("Updated balance: \(fundedTotal) sats", context: "PaymentFlowTests") - - // Create an order for 100k sats - Logger.test("Creating order for 100k sats", context: "PaymentFlowTests") - let lspBalanceSat: UInt64 = 100_000 // LSP balance (receiving capacity) - let clientBalanceSat: UInt64 = 0 // Client balance (sending capacity) - let channelExpiryWeeks: UInt32 = 2 - - // Create order options with necessary parameters - let options = CreateOrderOptions( - clientBalanceSat: clientBalanceSat, - lspNodeId: nil, - couponCode: "", - source: "bitkit-ios-test", - discountCode: nil, - zeroConf: true, - zeroConfPayment: false, - zeroReserve: true, - clientNodeId: nodeId, - signature: nil, - timestamp: nil, - refundOnchainAddress: nil, - announceChannel: false - ) - - let order = try await blocktank.newOrder( - lspBalanceSat: lspBalanceSat, - channelExpiryWeeks: channelExpiryWeeks, - options: options - ) - - XCTAssertNotNil(order, "Order should not be nil") - XCTAssertFalse(order.id.isEmpty, "Order ID should not be empty") - XCTAssertEqual(order.state2, .created, "Initial order state should be created") - XCTAssertGreaterThan(order.feeSat, 0, "Fee should be greater than 0") - Logger.test("Order created successfully with ID: \(order.id)", context: "PaymentFlowTests") - Logger.test("Order fee: \(order.feeSat) sats", context: "PaymentFlowTests") - - // Pay the order - Logger.test("Paying order with fee \(order.feeSat) sats to address \(order.payment?.onchain?.address ?? "")", context: "PaymentFlowTests") - let paymentResult = try await lightning.send(address: order.payment?.onchain?.address ?? "", sats: order.feeSat, satsPerVbyte: 1) - guard case let .accepted(paymentTxId) = paymentResult else { - XCTFail("Order funding broadcast was not accepted: \(paymentResult)") - return - } - XCTAssertFalse(paymentTxId.isEmpty, "Payment transaction ID should not be empty") - Logger.test("Payment sent with transaction ID: \(paymentTxId)", context: "PaymentFlowTests") - - // Mine 1 block to confirm the payment transaction - try await Task.sleep(nanoseconds: 500_000_000) - try await blocktank.regtestMineBlocks(1) - Logger.test("Block mined successfully", context: "PaymentFlowTests") - - // Sleep 10 seconds to ensure the order is processed and then mine blocks again - Logger.test("Waiting 10 seconds for payment to be processed", context: "PaymentFlowTests") - try await Task.sleep(nanoseconds: 10_000_000_000) - Logger.test("Wait completed", context: "PaymentFlowTests") - - // Mine blocks to confirm the payment transaction - Logger.test("Mining blocks to confirm payment", context: "PaymentFlowTests") - try await blocktank.regtestMineBlocks(1) - Logger.test("Blocks mined successfully", context: "PaymentFlowTests") - - // Sync wallet again - Logger.test("Syncing wallet after payment", context: "PaymentFlowTests") - try await lightning.sync() - - try await Task.sleep(nanoseconds: 5_000_000_000) - - Logger.test("Wallet sync complete", context: "PaymentFlowTests") - - // Check the order status - Logger.test("Checking order status", context: "PaymentFlowTests") - var updatedOrders = try await blocktank.orders(orderIds: [order.id], filter: nil, refresh: true) - XCTAssertFalse(updatedOrders.isEmpty, "Updated orders should not be empty") - - if updatedOrders.first?.state2 != .paid { - try await Task.sleep(nanoseconds: 20_000_000_000) - updatedOrders = try await blocktank.orders(orderIds: [order.id], filter: nil, refresh: true) - } - - Logger.test("Updated order state: \(String(describing: updatedOrders.first?.state2))", context: "PaymentFlowTests") - XCTAssertEqual(updatedOrders.first?.state2, .paid, "Order state should be paid after payment confirmation") - - // Request channel to be opened - Logger.test("Requesting channel to be opened", context: "PaymentFlowTests") - let openedOrder = try await blocktank.open(orderId: order.id) - XCTAssertEqual(openedOrder.state2, .executed, "Order state should be executed after opening channel") - Logger.test("Channel opening requested successfully", context: "PaymentFlowTests") - - // Mine blocks to confirm the channel opening - Logger.test("Mining blocks to confirm channel opening", context: "PaymentFlowTests") - try await blocktank.regtestMineBlocks(1) - Logger.test("Blocks mined successfully", context: "PaymentFlowTests") - - // Wait for the channel to be established - Logger.test("Waiting for channel to be established", context: "PaymentFlowTests") - try await Task.sleep(nanoseconds: 10_000_000_000) - - // Sync wallet to see the new channel - Logger.test("Syncing wallet to see new channel", context: "PaymentFlowTests") - try await lightning.sync() - - // Verify channel exists - let channels = await MainActor.run { lightning.channels } - XCTAssertNotNil(channels, "Channels should not be nil") - XCTAssertGreaterThan(channels?.count ?? 0, 0, "Should have at least one channel") - - // Check if we have at least one usable channel - let usableChannels = channels?.filter(\.isUsable) ?? [] - XCTAssertGreaterThan(usableChannels.count, 0, "Should have at least one usable channel") - - if let firstUsableChannel = usableChannels.first { - Logger.test("Usable channel established with capacity: \(firstUsableChannel.channelValueSats) sats", context: "PaymentFlowTests") - Logger.test("Channel counterparty: \(firstUsableChannel.counterpartyNodeId)", context: "PaymentFlowTests") - - // Verify channel properties - XCTAssertTrue(firstUsableChannel.isUsable, "Channel should be usable") - XCTAssertTrue(firstUsableChannel.isChannelReady, "Channel should be ready") - XCTAssertEqual(firstUsableChannel.channelValueSats, lspBalanceSat, "Channel capacity should match requested amount") - } else if let firstChannel = channels?.first { - Logger.test( - "Channel exists but is not yet usable. State: isChannelReady=\(firstChannel.isChannelReady), isUsable=\(firstChannel.isUsable)", - context: "PaymentFlowTests" - ) - Logger.test("Channel capacity: \(firstChannel.channelValueSats) sats", context: "PaymentFlowTests") - Logger.test("Channel counterparty: \(firstChannel.counterpartyNodeId)", context: "PaymentFlowTests") - XCTFail("Channel exists but is not usable yet") - } - - // TODO: actually try route some payments - - // Clean up by removing the test wallet - Logger.test("Stopping lightning node", context: "PaymentFlowTests") - try await lightning.stop() - Logger.test("Lightning node stopped successfully", context: "PaymentFlowTests") - Logger.test("Payment flow test completed successfully", context: "PaymentFlowTests") - } -} diff --git a/BitkitTests/CurrencyTests.swift b/BitkitTests/CurrencyTests.swift index c95409c83..6bd8a88e8 100644 --- a/BitkitTests/CurrencyTests.swift +++ b/BitkitTests/CurrencyTests.swift @@ -141,48 +141,4 @@ final class CurrencyTests: XCTestCase { let text = CurrencyViewModel.primaryAmountText(sats: 204, converted: nil, primaryDisplay: .fiat, displayUnit: .classic) XCTAssertEqual(text, "₿ 204") } - - // MARK: - Fees changed toast - - private func feeToastAmount(fiat: Bool, unit: BitcoinDisplayUnit, sats: UInt64) -> String { - CurrencyViewModel.primaryAmountText( - sats: sats, converted: makeConverted(sats: sats), primaryDisplay: fiat ? .fiat : .bitcoin, displayUnit: unit - ) - } - - func testFeesChangedToast_NetworkIncreaseShowsFiatAmount() { - let text = SpendingFeeIncrease.network(amountSat: 204).toastDescription { _ in "$0.95" } - XCTAssertTrue(text.contains("$0.95"), text) - XCTAssertFalse(text.contains("{amount}"), text) - XCTAssertTrue(text.contains("fee rates or your savings balance changed"), text) - } - - func testFeesChangedToast_ServiceIncreaseShowsClassicAmount() { - let text = SpendingFeeIncrease.service(amountSat: 204).toastDescription { _ in "₿ 0.00000204" } - XCTAssertTrue(text.contains("₿ 0.00000204"), text) - XCTAssertFalse(text.contains("{amount}"), text) - XCTAssertTrue(text.contains("now that your order is created"), text) - } - - func testFeesChangedToast_UsesThePrimaryDisplayUnitForEachIncrease() { - let cases: [(fiat: Bool, unit: BitcoinDisplayUnit, expected: String)] = [ - (true, .modern, "$0.12"), - (false, .modern, "₿ 204"), - (false, .classic, "₿ 0.00000204"), - ] - for testCase in cases { - for increase in [SpendingFeeIncrease.network(amountSat: 204), .service(amountSat: 204)] { - let text = increase.toastDescription { feeToastAmount(fiat: testCase.fiat, unit: testCase.unit, sats: $0) } - XCTAssertTrue(text.contains("Fees are \(testCase.expected) higher"), text) - XCTAssertFalse(text.contains("{amount}"), text) - } - } - } - - func testFeesChangedToast_PassesTheIncreaseAmountToTheFormatter() { - var formatted: [UInt64] = [] - _ = SpendingFeeIncrease.network(amountSat: 77).toastDescription { formatted.append($0); return "x" } - _ = SpendingFeeIncrease.service(amountSat: 88).toastDescription { formatted.append($0); return "x" } - XCTAssertEqual(formatted, [77, 88]) - } } diff --git a/BitkitTests/DustChangeHelperTests.swift b/BitkitTests/DustChangeHelperTests.swift deleted file mode 100644 index d1c860475..000000000 --- a/BitkitTests/DustChangeHelperTests.swift +++ /dev/null @@ -1,119 +0,0 @@ -@testable import Bitkit -import XCTest - -final class DustChangeHelperTests: XCTestCase { - private let dustLimit: UInt64 = 547 - - // MARK: - Change would be dust - - func testReportedNonMaxDustChange_ShouldNotUseSendAll() { - XCTAssertTrue(DustChangeHelper.wouldCreateDustChange( - totalInput: 5450, - amountSats: 5000, - normalFee: 181, - dustLimit: dustLimit - )) - - XCTAssertFalse(DustChangeHelper.shouldUseSendAllToAvoidDust( - totalInput: 5450, - amountSats: 5000, - normalFee: 181, - isMaxAmount: false, - dustLimit: dustLimit - )) - } - - func testChangeBelowDustLimit_ShouldUseSendAll() { - // totalInput: 100_000, amount: 99_500, fee: 500 -> change = 0 (explicit max case) - XCTAssertTrue(DustChangeHelper.shouldUseSendAllToAvoidDust( - totalInput: 100_000, - amountSats: 99500, - normalFee: 500, - isMaxAmount: true, - dustLimit: dustLimit - )) - - // totalInput: 100_000, amount: 99_000, fee: 500 -> change = 500 (below 547) - XCTAssertTrue(DustChangeHelper.shouldUseSendAllToAvoidDust( - totalInput: 100_000, - amountSats: 99000, - normalFee: 500, - isMaxAmount: true, - dustLimit: dustLimit - )) - - // totalInput: 100_000, amount: 98_954, fee: 500 -> change = 546 (just below dust) - XCTAssertTrue(DustChangeHelper.shouldUseSendAllToAvoidDust( - totalInput: 100_000, - amountSats: 98954, - normalFee: 500, - isMaxAmount: true, - dustLimit: dustLimit - )) - } - - func testChangeAtDustLimit_ShouldNotUseSendAll() { - // change = 547 is at the limit; < 547 means dust. So 547 is NOT dust. - // totalInput: 100_000, amount: 98_953, fee: 500 -> change = 547 (at limit, NOT dust) - XCTAssertFalse(DustChangeHelper.shouldUseSendAllToAvoidDust( - totalInput: 100_000, - amountSats: 98953, - normalFee: 500, - isMaxAmount: true, - dustLimit: dustLimit - )) - } - - func testChangeAboveDustLimit_ShouldNotUseSendAll() { - // totalInput: 100_000, amount: 98_000, fee: 500 -> change = 1_500 - XCTAssertFalse(DustChangeHelper.shouldUseSendAllToAvoidDust( - totalInput: 100_000, - amountSats: 98000, - normalFee: 500, - isMaxAmount: true, - dustLimit: dustLimit - )) - - // totalInput: 100_000, amount: 98_954, fee: 495 -> change = 551 - XCTAssertFalse(DustChangeHelper.shouldUseSendAllToAvoidDust( - totalInput: 100_000, - amountSats: 98954, - normalFee: 495, - isMaxAmount: true, - dustLimit: dustLimit - )) - } - - func testMaxSend_ChangeZero_ShouldUseSendAll() { - // totalInput: 100_000, amount: 99_500, fee: 500 -> change = 0 (max case) - XCTAssertTrue(DustChangeHelper.shouldUseSendAllToAvoidDust( - totalInput: 100_000, - amountSats: 99500, - normalFee: 500, - isMaxAmount: true, - dustLimit: dustLimit - )) - } - - func testInsufficientFunds_NegativeChange_ShouldNotUseSendAll() { - // totalInput: 100_000, amount: 100_000, fee: 500 -> change = -500 - XCTAssertFalse(DustChangeHelper.shouldUseSendAllToAvoidDust( - totalInput: 100_000, - amountSats: 100_000, - normalFee: 500, - isMaxAmount: true, - dustLimit: dustLimit - )) - } - - func testUsesEnvDustLimit_WhenNotSpecified() { - // Verify default uses Env.dustLimit (547): change 546 is dust - XCTAssertTrue(DustChangeHelper.shouldUseSendAllToAvoidDust( - totalInput: 100_000, - amountSats: 99454, - normalFee: 0, - isMaxAmount: true - // dustLimit omitted -> uses Env.dustLimit - )) - } -} diff --git a/BitkitTests/HwFundingSignerTests.swift b/BitkitTests/HwFundingSignerTests.swift index 7ad8840e6..f11ea1c79 100644 --- a/BitkitTests/HwFundingSignerTests.swift +++ b/BitkitTests/HwFundingSignerTests.swift @@ -1818,113 +1818,6 @@ final class HwFundingSignerTests: XCTestCase { // MARK: - Sign orchestration - func testHappyPathComposesFinalOrderFeeAndReturnsBroadcast() async throws { - let funding = MockHwFunding() - let signer = makeSigner(funding: funding, connecting: MockHwConnecting()) - let order = IBtOrder.mock() // feeSat = 1000, address = "bc1q..." - var composedMiningFee: UInt64? - - let signed = try await signer.prepareSignedFunding( - order: order, - walletId: "trezor:wallet", - address: XCTUnwrap(order.payment?.onchain?.address), - onComposed: { composedMiningFee = $0.miningFeeSats } - ) - let result = try await signer.broadcastSignedFunding(signed) - - XCTAssertEqual(result.txId, "txid") - XCTAssertEqual(composedMiningFee, funding.funding.miningFeeSats) - XCTAssertEqual(funding.composeCalls.count, 1) - XCTAssertEqual(funding.composeCalls.first?.sats, order.feeSat) - XCTAssertEqual(funding.composeCalls.first?.address, order.payment?.onchain?.address) - XCTAssertEqual(funding.composeCalls.first?.satsPerVByte, 2) - } - - func testReconnectFailureThrowsReconnectAndSkipsCompose() async { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - connecting.connectError = MockHwFunding.TestError() - let signer = makeSigner(funding: funding, connecting: connecting) - - await assertThrowsAsync { - _ = try await signer.prepareSignedFunding(order: .mock(), walletId: "trezor:wallet", address: "bc1q...") - } _: { error in - XCTAssertEqual(error as? HwTransferError, .reconnect(isBluetooth: false)) - } - XCTAssertTrue(funding.composeCalls.isEmpty) - XCTAssertEqual(funding.signCalls, 0) - } - - /// The reconnect deadline belongs to the wallet's device: a Jade may be waiting for its PIN. - func testReconnectUsesTheWalletsTimeout() async { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - connecting.connectDelay = 0.4 - connecting.reconnectTimeoutSeconds = 0.05 - let signer = makeSigner(funding: funding, connecting: connecting) - - await assertThrowsAsync { - _ = try await signer.prepareSignedFunding(order: .mock(), walletId: "jade:wallet", address: "bc1q...") - } _: { error in - XCTAssertEqual(error as? HwTransferError, .reconnect(isBluetooth: false)) - } - XCTAssertEqual(connecting.staleDisconnects, ["jade:wallet"], "the timed-out session is cleaned up") - XCTAssertTrue(funding.composeCalls.isEmpty) - } - - func testComposeFailureThrowsFundingError() async { - let funding = MockHwFunding() - funding.composeError = MockHwFunding.TestError() - let signer = makeSigner(funding: funding, connecting: MockHwConnecting()) - - await assertThrowsAsync { - _ = try await signer.prepareSignedFunding(order: .mock(), walletId: "trezor:wallet", address: "bc1q...") - } _: { error in - if case .funding = error as? HwTransferError {} else { - XCTFail("expected .funding, got \(error)") - } - } - XCTAssertEqual(funding.signCalls, 0) - } - - func testSigningTimeoutThrowsTimeoutAndClearsStaleSession() async { - let funding = MockHwFunding() - funding.signDelay = 0.4 - let connecting = MockHwConnecting() - let signer = makeSigner(funding: funding, connecting: connecting, timeouts: (compose: 5, sign: 0.05, broadcast: 5)) - - await assertThrowsAsync { - _ = try await signer.prepareSignedFunding(order: .mock(), walletId: "trezor:wallet", address: "bc1q...") - } _: { error in - XCTAssertEqual(error as? HwTransferError, .signingTimeout) - } - await Task.yield() - XCTAssertEqual(connecting.staleDisconnects, ["trezor:wallet"]) - XCTAssertEqual(funding.signCalls, 1) - } - - func testSigningTimeoutDoesNotWaitForCancellationIgnoringOperation() async { - let funding = MockHwFunding() - funding.cancellationIgnoringSignDelay = 0.5 - let connecting = MockHwConnecting() - let signer = makeSigner( - funding: funding, - connecting: connecting, - timeouts: (compose: 5, sign: 0.05, broadcast: 5) - ) - let start = ContinuousClock.now - - await assertThrowsAsync { - _ = try await signer.prepareSignedFunding(order: .mock(), walletId: "trezor:wallet", address: "bc1q...") - } _: { error in - XCTAssertEqual(error as? HwTransferError, .signingTimeout) - } - - XCTAssertLessThan(start.duration(to: .now), .milliseconds(250)) - await Task.yield() - XCTAssertEqual(connecting.staleDisconnects, ["trezor:wallet"]) - } - func testBroadcastTimeoutThrowsBroadcastUncertainWithoutClearingSession() async { let funding = MockHwFunding() funding.broadcastDelay = 0.4 @@ -1972,199 +1865,7 @@ final class HwFundingSignerTests: XCTestCase { XCTAssertEqual(result.txId, "core-derived-txid") } - func testComposeTimeoutClearsStaleSessionAndThrowsTimeout() async { - let funding = MockHwFunding() - funding.composeDelay = 0.4 - let connecting = MockHwConnecting() - let signer = makeSigner(funding: funding, connecting: connecting, timeouts: (compose: 0.05, sign: 5, broadcast: 5)) - - await assertThrowsAsync { - _ = try await signer.prepareSignedFunding(order: .mock(), walletId: "trezor:wallet", address: "bc1q...") - } _: { error in - XCTAssertEqual(error as? HwTransferError, .signingTimeout) - } - await Task.yield() - XCTAssertEqual(connecting.staleDisconnects, ["trezor:wallet"], "a compose timeout must tear down the stale session") - XCTAssertEqual(funding.signCalls, 0, "signing must not run after a compose timeout") - } - - func testRawSignErrorPropagatesUnwrapped() async { - let funding = MockHwFunding() - funding.signError = MockHwFunding.TestError() - let connecting = MockHwConnecting() - let signer = makeSigner(funding: funding, connecting: connecting) - - await assertThrowsAsync { - _ = try await signer.prepareSignedFunding(order: .mock(), walletId: "trezor:wallet", address: "bc1q...") - } _: { error in - XCTAssertTrue(error is MockHwFunding.TestError, "a real signing error must propagate unwrapped") - XCTAssertNil(error as? HwTransferError) - } - XCTAssertTrue(connecting.staleDisconnects.isEmpty, "a non-timeout error must not clear the session") - } - - func testBrokenThpSessionReconnectsAndRetriesSigningOnce() async throws { - let funding = MockHwFunding() - funding.signErrors = [ - Bitkit.AppError(error: TrezorError.ProtocolError(errorDetails: "THP decryption error: aead::Error")), - ] - let connecting = MockHwConnecting() - let signer = makeSigner(funding: funding, connecting: connecting) - - let result = try await signer.prepareSignedFunding( - order: .mock(), - walletId: "trezor:wallet", - address: "bc1q..." - ) - - XCTAssertEqual(result, funding.signedTx) - XCTAssertEqual(funding.signCalls, 2) - XCTAssertEqual(connecting.staleDisconnects, ["trezor:wallet"]) - XCTAssertEqual(connecting.ensureCalls, 2) - } - // MARK: - Vendor errors - - func testBusyJadeReportsJadeVendor() async { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - connecting.connectError = Bitkit.AppError(error: JadeError.DeviceLocked) - let signer = makeSigner(funding: funding, connecting: connecting) - - await assertThrowsAsync { - _ = try await signer.prepareSignedFunding(order: .mock(), walletId: "jade:wallet", address: "bc1q...") - } _: { error in - XCTAssertEqual(error as? HwTransferError, .deviceBusy(.blockstream)) - } - XCTAssertTrue(funding.composeCalls.isEmpty) - } - - func testADeviceHoldingAnotherWalletReportsTheWalletMismatch() async { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - connecting.connectError = HwWalletMismatchError() - let signer = makeSigner(funding: funding, connecting: connecting) - - await assertThrowsAsync { - _ = try await signer.prepareSignedFunding(order: .mock(), walletId: "jade:wallet", address: "bc1q...") - } _: { error in - XCTAssertEqual(error as? HwTransferError, .walletMismatch) - } - XCTAssertTrue(funding.composeCalls.isEmpty) - } - - func testBusyTrezorReportsTrezorVendor() async { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - connecting.connectError = Bitkit.AppError(error: TrezorError.DeviceBusy) - let signer = makeSigner(funding: funding, connecting: connecting) - - await assertThrowsAsync { - _ = try await signer.prepareSignedFunding(order: .mock(), walletId: "trezor:wallet", address: "bc1q...") - } _: { error in - XCTAssertEqual(error as? HwTransferError, .deviceBusy(.trezor)) - } - XCTAssertTrue(funding.composeCalls.isEmpty) - } - - func testJadeWrongPinDuringReconnectShowsJadeCopy() async { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - connecting.isBluetooth = true - let signer = makeSigner(funding: funding, connecting: connecting) - - for (error, key) in [ - (JadeError.InvalidPin, "hardware__jade_invalid_pin"), - (JadeError.PinServerError(errorDetails: "unreachable"), "hardware__jade_pinserver_error"), - ] { - connecting.connectError = Bitkit.AppError(error: error) - await assertThrowsAsync { - _ = try await signer.prepareSignedFunding(order: .mock(), walletId: "jade:wallet", address: "bc1q...") - } _: { thrown in - XCTAssertEqual(thrown as? HwTransferError, .generic(t(key)), "\(error)") - } - } - XCTAssertTrue(funding.composeCalls.isEmpty) - } - - func testAJadeLinkFailureDuringReconnectStillReportsAReconnect() async { - let connecting = MockHwConnecting() - connecting.connectError = Bitkit.AppError(error: JadeError.DeviceDisconnected) - connecting.isBluetooth = true - let signer = makeSigner(funding: MockHwFunding(), connecting: connecting) - - await assertThrowsAsync { - _ = try await signer.prepareSignedFunding(order: .mock(), walletId: "jade:wallet", address: "bc1q...") - } _: { error in - XCTAssertEqual(error as? HwTransferError, .reconnect(isBluetooth: true)) - } - } - - func testAJadeComposeFailureKeepsTheJadeCopy() async { - let funding = MockHwFunding() - funding.composeError = Bitkit.AppError(error: JadeError.InvalidPin) - let signer = makeSigner(funding: funding, connecting: MockHwConnecting()) - - await assertThrowsAsync { - _ = try await signer.prepareSignedFunding(order: .mock(), walletId: "jade:wallet", address: "bc1q...") - } _: { error in - XCTAssertEqual(error as? HwTransferError, .generic(t("hardware__jade_invalid_pin"))) - } - - funding.composeError = Bitkit.AppError(error: JadeError.DeviceBusy) - await assertThrowsAsync { - _ = try await signer.prepareSignedFunding(order: .mock(), walletId: "jade:wallet", address: "bc1q...") - } _: { error in - XCTAssertEqual(error as? HwTransferError, .deviceBusy(.blockstream)) - } - - funding.composeError = Bitkit.AppError(error: JadeError.Timeout) - await assertThrowsAsync { - _ = try await signer.prepareSignedFunding(order: .mock(), walletId: "jade:wallet", address: "bc1q...") - } _: { error in - XCTAssertEqual(error as? HwTransferError, .funding(t("hardware__connect_error"))) - } - XCTAssertEqual(funding.signCalls, 0) - } - - func testAJadeSessionFailureRetriesSigningOnce() async throws { - let funding = MockHwFunding() - funding.signErrors = [Bitkit.AppError(error: JadeError.DeviceDisconnected)] - let connecting = MockHwConnecting() - let signer = makeSigner(funding: funding, connecting: connecting) - - let result = try await signer.prepareSignedFunding(order: .mock(), walletId: "jade:wallet", address: "bc1q...") - - XCTAssertEqual(result, funding.signedTx) - XCTAssertEqual(funding.signCalls, 2) - XCTAssertEqual(connecting.staleDisconnects, ["jade:wallet"]) - XCTAssertEqual(connecting.ensureCalls, 2) - } - - func testAJadeCancellationOnDeviceIsRethrown() async { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - connecting.connectError = JadeError.UserCancelled - let signer = makeSigner(funding: funding, connecting: connecting) - - await assertThrowsAsync { - _ = try await signer.prepareSignedFunding(order: .mock(), walletId: "jade:wallet", address: "bc1q...") - } _: { error in - XCTAssertEqual(error as? JadeError, .UserCancelled, "a cancel on the Jade must not become a reconnect failure") - } - XCTAssertTrue(funding.composeCalls.isEmpty) - - connecting.connectError = nil - funding.signError = Bitkit.AppError(error: JadeError.UserCancelled) - await assertThrowsAsync { - _ = try await signer.prepareSignedFunding(order: .mock(), walletId: "jade:wallet", address: "bc1q...") - } _: { error in - XCTAssertTrue(error.isJadeUserCancellation()) - XCTAssertNil(error as? HwTransferError) - } - XCTAssertEqual(funding.signCalls, 1, "a cancel on the Jade is not retried") - XCTAssertTrue(connecting.staleDisconnects.isEmpty) - } } /// Async variant of `XCTAssertThrowsError` using a plain (non-autoclosure) operation closure, so the diff --git a/BitkitTests/HwTransferMocks.swift b/BitkitTests/HwTransferMocks.swift index d0fa59cc9..92cf101c1 100644 --- a/BitkitTests/HwTransferMocks.swift +++ b/BitkitTests/HwTransferMocks.swift @@ -2,8 +2,7 @@ import BitkitCore import Foundation -/// Shared mocks for the hardware-wallet transfer tests (`HwFundingSignerTests`, -/// `TransferViewModelHwTests`). +/// Shared mocks for the hardware-wallet funding tests (`HwFundingSignerTests`). @MainActor final class MockHwFunding: HwTransferFunding { struct TestError: Error {} diff --git a/BitkitTests/HwWalletVendorPresentationTests.swift b/BitkitTests/HwWalletVendorPresentationTests.swift index e1c1b3ac3..40eb06661 100644 --- a/BitkitTests/HwWalletVendorPresentationTests.swift +++ b/BitkitTests/HwWalletVendorPresentationTests.swift @@ -36,7 +36,6 @@ final class HwWalletVendorPresentationTests: XCTestCase { XCTAssertEqual(vendor.foundHeader, t("hardware__found_header")) XCTAssertEqual(vendor.pairedHeader, t("hardware__paired_header")) XCTAssertEqual(vendor.sendSignButtonTitle, t("hardware__send_open_connect")) - XCTAssertEqual(vendor.transferSignButtonTitle, t("lightning__transfer_hw__open_connect")) XCTAssertTrue(vendor.supportsPassphraseWallets) } @@ -50,13 +49,12 @@ final class HwWalletVendorPresentationTests: XCTestCase { XCTAssertEqual(vendor.foundHeader, t("hardware__found_header_jade")) XCTAssertEqual(vendor.pairedHeader, t("hardware__paired_header_jade")) XCTAssertEqual(vendor.sendSignButtonTitle, t("hardware__send_open_connect_jade")) - XCTAssertEqual(vendor.transferSignButtonTitle, t("hardware__send_open_connect_jade")) XCTAssertFalse(vendor.supportsPassphraseWallets) } func testNoJadeCopyMentionsTrezor() { let vendor = HwWalletVendor.blockstream - let copy = [vendor.modelName, vendor.foundHeader, vendor.pairedHeader, vendor.sendSignButtonTitle, vendor.transferSignButtonTitle] + let copy = [vendor.modelName, vendor.foundHeader, vendor.pairedHeader, vendor.sendSignButtonTitle] for text in copy { XCTAssertFalse(text.contains("Trezor"), text) diff --git a/BitkitTests/LightningServiceConfigTests.swift b/BitkitTests/LightningServiceConfigTests.swift new file mode 100644 index 000000000..5dae72e5d --- /dev/null +++ b/BitkitTests/LightningServiceConfigTests.swift @@ -0,0 +1,48 @@ +@testable import Bitkit +import LDKNode +import XCTest + +final class LightningServiceConfigTests: XCTestCase { + func testNodeConfigTrustsNoPeer() { + let config = LightningService.nodeConfig(walletIndex: 0) + + XCTAssertTrue(config.trustedPeers0conf.isEmpty) + XCTAssertEqual(config.anchorChannelsConfig?.trustedPeersNoReserve, []) + XCTAssertEqual(config.anchorChannelsConfig?.perChannelReserveSats, 1) + } + + func testIdlePeersAreStoredPeersWithNoChannelAndNoClaimableBalance() { + let channel = ChannelDetails.mock() + let peers = [ + peer("idle"), + peer(channel.counterpartyNodeId), + peer("withClaim"), + peer("notStored", isPersisted: false), + ] + let claims: [LightningBalance] = [ + .claimableAwaitingConfirmations( + channelId: "closed", + counterpartyNodeId: "withClaim", + amountSatoshis: 1000, + confirmationHeight: 100, + source: .holderForceClosed + ), + ] + + let idle = LightningService.idlePeerIds(peers: peers, channels: [channel], lightningBalances: claims, pendingSweeps: []) + + XCTAssertEqual(idle, ["idle"]) + } + + func testNoPeerIsIdleWhileAClosedChannelAwaitsItsSweep() { + let sweeps: [PendingSweepBalance] = [.pendingBroadcast(channelId: "closed", amountSatoshis: 1000)] + + let idle = LightningService.idlePeerIds(peers: [peer("idle")], channels: [], lightningBalances: [], pendingSweeps: sweeps) + + XCTAssertTrue(idle.isEmpty) + } + + private func peer(_ nodeId: String, isPersisted: Bool = true) -> PeerDetails { + PeerDetails(nodeId: nodeId, address: "127.0.0.1:9735", isPersisted: isPersisted, isConnected: true) + } +} diff --git a/BitkitTests/NotificationsTimedSheetTests.swift b/BitkitTests/NotificationsTimedSheetTests.swift new file mode 100644 index 000000000..ced2321d7 --- /dev/null +++ b/BitkitTests/NotificationsTimedSheetTests.swift @@ -0,0 +1,20 @@ +@testable import Bitkit +import XCTest + +final class NotificationsTimedSheetTests: XCTestCase { + func testShownWhenSubscriptionActiveAndNotificationsOff() { + XCTAssertTrue(NotificationsTimedSheet.shouldShow(notificationsEnabled: false, hasSeenIntro: false, hasActiveSubscription: true)) + } + + func testHiddenWithoutActiveSubscription() { + XCTAssertFalse(NotificationsTimedSheet.shouldShow(notificationsEnabled: false, hasSeenIntro: false, hasActiveSubscription: false)) + } + + func testHiddenWhenNotificationsAlreadyOn() { + XCTAssertFalse(NotificationsTimedSheet.shouldShow(notificationsEnabled: true, hasSeenIntro: false, hasActiveSubscription: true)) + } + + func testHiddenAfterIntroSeen() { + XCTAssertFalse(NotificationsTimedSheet.shouldShow(notificationsEnabled: false, hasSeenIntro: true, hasActiveSubscription: true)) + } +} diff --git a/BitkitTests/OnchainSendAttemptServiceTests.swift b/BitkitTests/OnchainSendAttemptServiceTests.swift index fe3c45d91..1fb261c21 100644 --- a/BitkitTests/OnchainSendAttemptServiceTests.swift +++ b/BitkitTests/OnchainSendAttemptServiceTests.swift @@ -126,7 +126,7 @@ final class OnchainSendAttemptServiceTests: XCTestCase { ) for orderId in [nil, "original-order"] as [String?] { let store = MemoryAttemptStore() - let service = OnchainSendAttemptService(store: store, hasPaidOrder: { _ in false }) + let service = OnchainSendAttemptService(store: store) let id = try await service.admit( walletId: XCTUnwrap(wallet.originalWalletId), requestId: nil, orderId: orderId, address: "bcrt1qoriginal", amountSats: 1000, isMaxAmount: false, @@ -1340,24 +1340,6 @@ final class OnchainSendAttemptServiceTests: XCTestCase { XCTAssertEqual(node.calls, 1) } - func testPaidOrderStoreProtectsOlderOrderAfterCurrentAttemptIsReplaced() async throws { - let store = MemoryAttemptStore() - let service = OnchainSendAttemptService(store: store, hasPaidOrder: { $0 == "paid-order" }) - let id = try await admit(service) - try await service.record(.accepted(txid: txid), attemptId: id) - try await service.acknowledgeLocalFollowup(txid: txid) - _ = try await admit(service) - let node = AttemptNodeMock(result: .unknown(txid: txid)) - do { - _ = try await send(service, node: node, orderId: "paid-order") - XCTFail("Older paid order was dispatched again") - } catch let error as OnchainSendAttemptError { - guard case .duplicate = error else { return XCTFail("Expected paid-order guard") } - } - XCTAssertEqual(node.calls, 0) - XCTAssertEqual(store.snapshot().first?.status, .pending) - } - @MainActor func testActualSendSerializesFixedMaxAndTransferBeforeNodeDispatch() async { let store = MemoryAttemptStore() @@ -1566,6 +1548,23 @@ final class OnchainSendAttemptServiceTests: XCTestCase { XCTAssertEqual(store.snapshot(), [releasing]) } + func testRefusedBlocktankFundingCanBeReleased() async throws { + let store = MemoryAttemptStore() + let service = OnchainSendAttemptService(store: store) + let sender = PreparedAttemptNodeMock() + sender.result = .rejected(txid: sender.txid, reason: "min relay fee not met") + _ = try? await service.send(using: sender, address: "bcrt1qfunding", amountSats: sender.amount, satsPerVbyte: 2, + utxosToSpend: nil, isMaxAmount: false, orderId: "order-1") + let attempt = try XCTUnwrap(store.snapshot().first) + XCTAssertTrue(attempt.isBlocktankFunding) + // Retry original is refused while transfers are unavailable, so release is the only exit. + XCTAssertTrue(attempt.canBeAbandoned) + let approved = try await service.approveRelease(attempt) + let releasing = try await service.recordRelease(approved, txid: String(repeating: "77", count: 32)) + try await service.abandonUnbroadcast(releasing) + XCTAssertTrue(store.snapshot().isEmpty) + } + func testOrdinaryPaymentIsNeverAbandoned() async throws { let store = MemoryAttemptStore() let service = OnchainSendAttemptService(store: store) diff --git a/BitkitTests/PaykitReceivedPaymentContactsTests.swift b/BitkitTests/PaykitReceivedPaymentContactsTests.swift index 333604885..2d5ed36a5 100644 --- a/BitkitTests/PaykitReceivedPaymentContactsTests.swift +++ b/BitkitTests/PaykitReceivedPaymentContactsTests.swift @@ -214,7 +214,7 @@ final class PaykitReceivedPaymentContactsTests: XCTestCase { service.setContactDetached(false, activityId: activity.activityId, walletId: WalletScope.default) service.setContactDetached(false, activityId: activity.activityId, walletId: hardwareWalletId) XCTAssertFalse(service.isContactDetached(activityId: activity.activityId, walletId: WalletScope.default)) - try await MainActor.run { try SettingsViewModel.shared.restoreAppCacheData(restored) } + await MainActor.run { SettingsViewModel.shared.restoreAppCacheData(restored) } XCTAssertTrue(service.isContactDetached(activityId: activity.activityId, walletId: WalletScope.default)) XCTAssertTrue(service.isContactDetached(activityId: activity.activityId, walletId: hardwareWalletId)) let restoredCache = PaykitReceivedPaymentBackfillCache(activityChanges: service.activitiesChangedPublisher) diff --git a/BitkitTests/PubkyProfileManagerTests.swift b/BitkitTests/PubkyProfileManagerTests.swift index 91465c895..5bb8703de 100644 --- a/BitkitTests/PubkyProfileManagerTests.swift +++ b/BitkitTests/PubkyProfileManagerTests.swift @@ -4075,8 +4075,6 @@ final class PubkyProfileManagerTests: XCTestCase { hasSeenNotificationsIntro: false, hasSeenQuickpayIntro: false, hasSeenShopIntro: false, - hasSeenTransferIntro: false, - hasSeenTransferToSpendingIntro: false, hasSeenTransferToSavingsIntro: false, hasSeenWidgetsIntro: false, hasDismissedWidgetsOnboardingHint: false, diff --git a/BitkitTests/QuickPaySpendStoreTests.swift b/BitkitTests/QuickPaySpendStoreTests.swift index 00c846028..6caa1de76 100644 --- a/BitkitTests/QuickPaySpendStoreTests.swift +++ b/BitkitTests/QuickPaySpendStoreTests.swift @@ -212,8 +212,6 @@ final class QuickPaySpendStoreTests: XCTestCase { hasSeenNotificationsIntro: false, hasSeenQuickpayIntro: false, hasSeenShopIntro: false, - hasSeenTransferIntro: false, - hasSeenTransferToSpendingIntro: false, hasSeenTransferToSavingsIntro: false, hasSeenWidgetsIntro: false, hasDismissedWidgetsOnboardingHint: false, diff --git a/BitkitTests/ReceiveEditTests.swift b/BitkitTests/ReceiveEditTests.swift index 92664516d..a35ba7f75 100644 --- a/BitkitTests/ReceiveEditTests.swift +++ b/BitkitTests/ReceiveEditTests.swift @@ -16,35 +16,4 @@ final class ReceiveEditTests: XCTestCase { XCTAssertTrue(didDismiss) XCTAssertFalse(didPrepareLightningInvoice) } - - func testReplacingEditedQrRouteRemovesStaleCjitQr() { - var navigationPath: [ReceiveRoute] = [ - .tag, - .edit(tab: .spending, onchainOnly: false), - .cjitGeoBlocked, - .qr(cjitInvoice: "stale-cjit-invoice", tab: .spending), - .edit(tab: .spending, onchainOnly: false, replacesCurrentQr: true), - ] - - ReceiveEdit.replaceEditedQrRoute(in: &navigationPath, with: .qr(cjitInvoice: nil, tab: .spending)) - - XCTAssertEqual( - navigationPath, - [ - .tag, - .qr(cjitInvoice: nil, tab: .spending), - ] - ) - } - - func testReplacingEditedQrRouteCanNavigateToFreshCjitFlow() { - var navigationPath: [ReceiveRoute] = [ - .qr(cjitInvoice: "stale-cjit-invoice", tab: .spending), - .edit(tab: .spending, onchainOnly: false, replacesCurrentQr: true), - ] - - ReceiveEdit.replaceEditedQrRoute(in: &navigationPath, with: .cjitAmount) - - XCTAssertEqual(navigationPath, [.cjitAmount]) - } } diff --git a/BitkitTests/ReceiveLiquidityDecisionTests.swift b/BitkitTests/ReceiveLiquidityDecisionTests.swift index 06ddf67fa..de2dfcdf7 100644 --- a/BitkitTests/ReceiveLiquidityDecisionTests.swift +++ b/BitkitTests/ReceiveLiquidityDecisionTests.swift @@ -48,155 +48,33 @@ final class ReceiveLiquidityDecisionTests: XCTestCase { ) } - func testZeroInboundDoesNotRouteToCjit() { + func testZeroInboundKeepsTheQr() { XCTAssertEqual( - ReceiveLiquidityDecision.additionalLiquidityAction( - source: .spending, - invoiceAmountSats: 10000, - inboundCapacitySats: 0, - minCjitSats: 5000, - maxCjitAmountSats: 100_000, - isGeoBlocked: false - ), + ReceiveLiquidityDecision.additionalLiquidityAction(source: .spending, invoiceAmountSats: 10000, inboundCapacitySats: 0), .none ) } - func testSavingsAndAutoEditsDoNotRouteToCjit() { + func testAmountWithinInboundKeepsTheQr() { + XCTAssertEqual( + ReceiveLiquidityDecision.additionalLiquidityAction(source: .spending, invoiceAmountSats: 5000, inboundCapacitySats: 5000), + .none + ) + } + + func testOnlySpendingNeedsMoreCapacity() { for source in [ReceiveLiquiditySource.savings, .auto] { XCTAssertEqual( - ReceiveLiquidityDecision.additionalLiquidityAction( - source: source, - invoiceAmountSats: 10000, - inboundCapacitySats: 1000, - minCjitSats: 5000, - maxCjitAmountSats: 100_000, - isGeoBlocked: false - ), + ReceiveLiquidityDecision.additionalLiquidityAction(source: source, invoiceAmountSats: 10000, inboundCapacitySats: 5000), .none ) } } - func testBelowCjitMinimumRoutesToAmountPicker() { - XCTAssertEqual( - ReceiveLiquidityDecision.additionalLiquidityAction( - source: .spending, - invoiceAmountSats: 4000, - inboundCapacitySats: 1000, - minCjitSats: 5000, - maxCjitAmountSats: 100_000, - isGeoBlocked: false - ), - .chooseAmount - ) - } - - func testAtCjitMinimumCreatesCjit() { - XCTAssertEqual( - ReceiveLiquidityDecision.additionalLiquidityAction( - source: .spending, - invoiceAmountSats: 5000, - inboundCapacitySats: 1000, - minCjitSats: 5000, - maxCjitAmountSats: 100_000, - isGeoBlocked: false - ), - .createCjit(5000) - ) - } - - func testOverMaxCjitAmountRoutesToAmountPicker() { - XCTAssertEqual( - ReceiveLiquidityDecision.additionalLiquidityAction( - source: .spending, - invoiceAmountSats: 100_001, - inboundCapacitySats: 1000, - minCjitSats: 5000, - maxCjitAmountSats: 100_000, - isGeoBlocked: false - ), - .chooseAmount - ) - } - - func testUnknownMaxCjitAmountRoutesToAmountPicker() { - XCTAssertEqual( - ReceiveLiquidityDecision.additionalLiquidityAction( - source: .spending, - invoiceAmountSats: 10000, - inboundCapacitySats: 1000, - minCjitSats: 5000, - maxCjitAmountSats: nil, - isGeoBlocked: false - ), - .chooseAmount - ) - } - - func testGeoBlockedRoutesToGeoBlock() { - XCTAssertEqual( - ReceiveLiquidityDecision.additionalLiquidityAction( - source: .spending, - invoiceAmountSats: 10000, - inboundCapacitySats: 1000, - minCjitSats: 5000, - maxCjitAmountSats: 100_000, - isGeoBlocked: true - ), - .geoBlocked - ) - } - - func testNoAdditionalLiquidityNeededReturnsNone() { + func testSpendingAmountOverInboundShowsTransferUnavailable() { XCTAssertEqual( - ReceiveLiquidityDecision.additionalLiquidityAction( - source: .spending, - invoiceAmountSats: 1000, - inboundCapacitySats: 1000, - minCjitSats: 5000, - maxCjitAmountSats: 100_000, - isGeoBlocked: false - ), - .none - ) - } - - func testCjitLimitsAreFetchedOnlyWhenAdditionalLiquidityCanUseThem() { - XCTAssertFalse( - ReceiveLiquidityDecision.needsCjitLimitsForAdditionalLiquidity( - source: .auto, - invoiceAmountSats: 10000, - inboundCapacitySats: 1000, - isGeoBlocked: false - ) - ) - - XCTAssertFalse( - ReceiveLiquidityDecision.needsCjitLimitsForAdditionalLiquidity( - source: .spending, - invoiceAmountSats: 10000, - inboundCapacitySats: 0, - isGeoBlocked: false - ) - ) - - XCTAssertFalse( - ReceiveLiquidityDecision.needsCjitLimitsForAdditionalLiquidity( - source: .spending, - invoiceAmountSats: 10000, - inboundCapacitySats: 1000, - isGeoBlocked: true - ) - ) - - XCTAssertTrue( - ReceiveLiquidityDecision.needsCjitLimitsForAdditionalLiquidity( - source: .spending, - invoiceAmountSats: 10000, - inboundCapacitySats: 1000, - isGeoBlocked: false - ) + ReceiveLiquidityDecision.additionalLiquidityAction(source: .spending, invoiceAmountSats: 10000, inboundCapacitySats: 5000), + .transferUnavailable ) } } diff --git a/BitkitTests/ReceiveSheetSessionTests.swift b/BitkitTests/ReceiveSheetSessionTests.swift index 757ee16e3..99c9a04db 100644 --- a/BitkitTests/ReceiveSheetSessionTests.swift +++ b/BitkitTests/ReceiveSheetSessionTests.swift @@ -42,16 +42,13 @@ final class ReceiveSheetSessionTests: XCTestCase { func testReceiveRoutesRequireInvoiceRefresh() { let routes: [ReceiveRoute] = [ - .qr(cjitInvoice: nil, tab: nil), - .qr(cjitInvoice: "cjit-invoice", tab: .spending), - .qr(cjitInvoice: nil, tab: .hardware), + .qr(tab: nil), + .qr(tab: .spending), + .qr(tab: .hardware), .edit(tab: .spending, onchainOnly: false), .edit(tab: .hardware, onchainOnly: true), .tag, - .cjitAmount, - .cjitConfirm(entry: .mock(), receiveAmountSats: 1000, isAdditional: false), - .cjitLearnMore(entry: .mock(), receiveAmountSats: 1000, isAdditional: false), - .cjitGeoBlocked, + .transferUnavailable, ] for route in routes { diff --git a/BitkitTests/ScannerManagerTests.swift b/BitkitTests/ScannerManagerTests.swift index 94fa92dcc..6e1e93706 100644 --- a/BitkitTests/ScannerManagerTests.swift +++ b/BitkitTests/ScannerManagerTests.swift @@ -328,6 +328,21 @@ final class ScannerManagerTests: XCTestCase { } } + func testGiftCodeScanShowsTheInvalidCodeToastAndNoSheet() async throws { + snapshotAppDefaultsDomain() + UserDefaults.standard.set(false, forKey: ToastWindowManager.disableAllToastsKey) + let previousToast = ToastWindowManager.shared.currentToast + defer { ToastWindowManager.shared.currentToast = previousToast } + ToastWindowManager.shared.currentToast = nil + let sheets = SheetViewModel() + let app = configure(ScannerManager {}, sheets: sheets, decodeInvoice: { _ in .gift(code: "gift-code", amount: 1000) }) + + try await app.handleScannedData("bitkit://gift-code-1000") + + XCTAssertEqual(ToastWindowManager.shared.currentToast?.accessibilityIdentifier, "InvalidAddressToast") + XCTAssertNil(sheets.activeSheetConfiguration) + } + private func imageTestLnurl(amount: UInt64) -> LnurlPayData { LnurlPayData(uri: "https://example.com/pay", callback: "https://example.com/callback", minSendable: amount, maxSendable: amount, diff --git a/BitkitTests/SendConfirmationLifecycleTests.swift b/BitkitTests/SendConfirmationLifecycleTests.swift index 4f23ba88f..dc513c0a3 100644 --- a/BitkitTests/SendConfirmationLifecycleTests.swift +++ b/BitkitTests/SendConfirmationLifecycleTests.swift @@ -609,7 +609,7 @@ private final class ControlledSendWallet: WalletViewModel { self.fails = fails self.suspendsResumedPreparation = suspendsResumedPreparation super.init( - transferService: TransferService(lightningService: .shared, blocktankService: Bitkit.CoreService.shared.blocktank), + transferService: TransferService(lightningService: .shared), sheetViewModel: SheetViewModel(), feeEstimatesManager: FeeEstimatesManager() ) diff --git a/BitkitTests/SpendingConfirmTotalTests.swift b/BitkitTests/SpendingConfirmTotalTests.swift deleted file mode 100644 index 26ba017d5..000000000 --- a/BitkitTests/SpendingConfirmTotalTests.swift +++ /dev/null @@ -1,77 +0,0 @@ -@testable import Bitkit -import XCTest - -final class SpendingConfirmTotalTests: XCTestCase { - func testNormalPath_UsesOrderFeePlusNetworkFee() { - XCTAssertEqual( - SpendingConfirmTotal.leavingAmount( - orderFeeSat: 50000, - networkFeeSat: 250, - shouldUseSendAll: false, - maxSendable: nil - ), - 50250 - ) - } - - func testNormalPath_IgnoresStaleMaxSendable() { - XCTAssertEqual( - SpendingConfirmTotal.leavingAmount( - orderFeeSat: 50000, - networkFeeSat: 250, - shouldUseSendAll: false, - maxSendable: 99750 - ), - 50250 - ) - } - - func testSendAllPath_UsesMaxSendablePlusNetworkFee() { - // balance 100_000, send-all fee 250 → maxSendable 99_750; amount leaving = 100_000 - XCTAssertEqual( - SpendingConfirmTotal.leavingAmount( - orderFeeSat: 50000, - networkFeeSat: 250, - shouldUseSendAll: true, - maxSendable: 99750 - ), - 100_000 - ) - } - - func testNormalPath_SaturatesOnOverflow() { - XCTAssertEqual( - SpendingConfirmTotal.leavingAmount( - orderFeeSat: UInt64.max, - networkFeeSat: 1, - shouldUseSendAll: false, - maxSendable: nil - ), - UInt64.max - ) - } - - func testSendAllPath_SaturatesOnOverflow() { - XCTAssertEqual( - SpendingConfirmTotal.leavingAmount( - orderFeeSat: 1, - networkFeeSat: 1, - shouldUseSendAll: true, - maxSendable: UInt64.max - ), - UInt64.max - ) - } - - func testSendAllPath_WithoutMaxSendable_FallsBackToOrderPlusFee() { - XCTAssertEqual( - SpendingConfirmTotal.leavingAmount( - orderFeeSat: 50000, - networkFeeSat: 250, - shouldUseSendAll: true, - maxSendable: nil - ), - 50250 - ) - } -} diff --git a/BitkitTests/TabBarReceiveTests.swift b/BitkitTests/TabBarReceiveTests.swift index 470cff36b..7dc0eab9d 100644 --- a/BitkitTests/TabBarReceiveTests.swift +++ b/BitkitTests/TabBarReceiveTests.swift @@ -13,7 +13,7 @@ final class TabBarReceiveTests: XCTestCase { XCTAssertFalse(wallet.canCreateReceiveLightningInvoice) XCTAssertTrue(wallet.canCreateReceiveLightningInvoice(amountSats: nil)) XCTAssertFalse( - TabBar.shouldOpenSpendingCjitEntry( + TabBar.shouldOpenSpendingTransferUnavailable( isSpendingWallet: true, wallet: wallet, hasPendingTransfersToSpending: false @@ -25,7 +25,7 @@ final class TabBarReceiveTests: XCTestCase { let wallet = WalletViewModel() XCTAssertTrue( - TabBar.shouldOpenSpendingCjitEntry( + TabBar.shouldOpenSpendingTransferUnavailable( isSpendingWallet: true, wallet: wallet, hasPendingTransfersToSpending: false @@ -37,7 +37,7 @@ final class TabBarReceiveTests: XCTestCase { ] XCTAssertFalse( - TabBar.shouldOpenSpendingCjitEntry( + TabBar.shouldOpenSpendingTransferUnavailable( isSpendingWallet: true, wallet: wallet, hasPendingTransfersToSpending: false @@ -49,7 +49,7 @@ final class TabBarReceiveTests: XCTestCase { let wallet = WalletViewModel() XCTAssertFalse( - TabBar.shouldOpenSpendingCjitEntry( + TabBar.shouldOpenSpendingTransferUnavailable( isSpendingWallet: false, wallet: wallet, hasPendingTransfersToSpending: false @@ -57,7 +57,7 @@ final class TabBarReceiveTests: XCTestCase { ) XCTAssertFalse( - TabBar.shouldOpenSpendingCjitEntry( + TabBar.shouldOpenSpendingTransferUnavailable( isSpendingWallet: true, wallet: wallet, hasPendingTransfersToSpending: true diff --git a/BitkitTests/TransferServiceActivityTests.swift b/BitkitTests/TransferServiceActivityTests.swift index 5db3f4c34..16492304e 100644 --- a/BitkitTests/TransferServiceActivityTests.swift +++ b/BitkitTests/TransferServiceActivityTests.swift @@ -90,9 +90,7 @@ final class TransferServiceActivityTests: XCTestCase { private func makeService() -> Bitkit.TransferService { Bitkit.TransferService( storage: Bitkit.TransferStorage(defaults: transferDefaults), - lightningService: .shared, - blocktankService: Bitkit.CoreService.shared.blocktank, - isGeoBlocked: { false } + lightningService: .shared ) } @@ -104,6 +102,26 @@ final class TransferServiceActivityTests: XCTestCase { ) } + func testConcurrentTransferCreationKeepsEveryRecord() async throws { + let service = makeService() + let ids = try await withThrowingTaskGroup(of: String.self) { group in + for index in 0 ..< 20 { + group.addTask { + try await service.createTransfer( + type: .coopClose, + amountSats: 1000, + channelId: "channel-\(index)" + ) + } + } + return try await group.reduce(into: Set()) { $0.insert($1) } + } + + let stored = try Bitkit.TransferStorage(defaults: transferDefaults).getAll() + XCTAssertEqual(Set(stored.map(\.id)), ids) + XCTAssertEqual(stored.count, 20) + } + func testOrdinaryResolutionBeforePendingInitializationRestoresExactDurableResult() async throws { for rejected in [false, true] { let store = MemoryAttemptStore() @@ -324,9 +342,9 @@ final class TransferServiceActivityTests: XCTestCase { let saved = try await activity.getOnchainActivityByTxId(txid: txid) XCTAssertEqual(saved?.txId, txid) XCTAssertEqual(saved?.isTransfer, true) - let records = try makeService().getActiveTransfers() - XCTAssertEqual(records.count, 1) - XCTAssertEqual(records.first?.lspOrderId, "original-lost-event-order") + XCTAssertTrue(try makeService().getActiveTransfers().isEmpty, "Blocktank opens no channel, so no transfer is tracked") + let blocking = try await attempts.blockingAttempt() + XCTAssertNil(blocking, "An accepted funding must not block later sends") XCTAssertTrue(path.isEmpty, "Funding recovery must not mark a new unsent payment successful") XCTAssertFalse(PaykitPaymentActivity.shared.shouldDeferRefresh) } @@ -826,7 +844,7 @@ final class TransferServiceActivityTests: XCTestCase { let request = PaykitPaymentRequest.ID(paymentRequestId: UUID().uuidString, counterparty: "pubky" + String(repeating: "y", count: 52), billingPeriodStartsAt: nil) - let service = OnchainSendAttemptService(store: store, hasPaidOrder: { _ in false }) + let service = OnchainSendAttemptService(store: store) _ = try await service.send(using: sender, address: "original", amountSats: 4321, satsPerVbyte: 2, utxosToSpend: nil, isMaxAmount: false, requestId: isOrder ? nil : request, orderId: isOrder ? "original-order" : nil, @@ -848,10 +866,7 @@ final class TransferServiceActivityTests: XCTestCase { XCTAssertEqual(store.snapshot().first?.txid, txid) XCTAssertEqual(store.snapshot().first?.walletId, OnchainSendAttemptService.walletId(index: sender.currentWalletIndex)) if isOrder { - let tracking = try Bitkit.TransferStorage(defaults: transferDefaults).getAll().first - XCTAssertEqual(tracking?.fundingTxId, txid) - XCTAssertEqual(tracking?.txTotalSats, 4444) - XCTAssertEqual(tracking?.preTransferOnchainSats, 10000) + XCTAssertTrue(try Bitkit.TransferStorage(defaults: transferDefaults).getAll().isEmpty) XCTAssertEqual(store.snapshot().first?.localFollowupComplete, true) } XCTAssertEqual(sender.calls, 1) @@ -969,338 +984,6 @@ final class TransferServiceActivityTests: XCTestCase { // There is no OnchainSending/native broadcast dependency in restore or local completion. } - @MainActor - func testReceivedTransferPublishesExactResolutionForVisiblePending() async throws { - let confirmed = Bitkit.LightningService.shared.onchainTransactionConfirmed - let received = Bitkit.LightningService.shared.onchainTransactionReceived - defer { - Bitkit.LightningService.shared.onchainTransactionConfirmed = confirmed - Bitkit.LightningService.shared.onchainTransactionReceived = received - } - let store = MemoryAttemptStore() - let txid = String(repeating: "ab", count: 32) - let node = AttemptNodeMock(result: .unknown(txid: txid)) - let service = OnchainSendAttemptService(store: store, hasPaidOrder: { _ in false }) - let order = IBtOrder.mock(id: "original-order") - let recoveryReady = expectation(description: "Recovered original order resumes funding setup") - var balanceRefreshes = 0 - let vm = TransferViewModel( - transferService: makeService(), sheetViewModel: SheetViewModel(), - onBalanceRefresh: { balanceRefreshes += 1; recoveryReady.fulfill() }, - onchainAttemptService: service, onchainSender: node, onchainBalanceProvider: { 10000 } - ) - vm.onOrderCreated(order: order) - do { try await vm.payOrder(order: order, speed: .normal, txFee: 123, satsPerVbyte: 2) } catch {} - XCTAssertNil(vm.recoveredOnchainFundingOrderId) - XCTAssertEqual(balanceRefreshes, 0, "Unknown funding must not advance setup") - let unrelatedStore = MemoryAttemptStore() - let unrelatedNode = AttemptNodeMock(result: .unknown(txid: String(repeating: "cd", count: 32))) - var unrelatedRefreshes = 0 - let unrelatedVM = TransferViewModel( - transferService: makeService(), sheetViewModel: SheetViewModel(), - onBalanceRefresh: { unrelatedRefreshes += 1 }, - onchainAttemptService: OnchainSendAttemptService(store: unrelatedStore, hasPaidOrder: { _ in false }), - onchainSender: unrelatedNode, onchainBalanceProvider: { 10000 } - ) - do { - try await unrelatedVM.payOrder(order: .mock(id: "other-order"), speed: .normal, txFee: 123, satsPerVbyte: 2) - } catch {} - let original = try XCTUnwrap(store.snapshot().first) - let wallet = makeWallet(attempts: service) - let resolutionReady = expectation(description: "Visible Pending receives exact durable transfer resolution") - var resolved: OnchainSendLocalResolution? - let observation = OnchainSendAttemptService.localResolutionPublisher.sink { resolution in - if resolution.attemptId == original.id, resolution.walletId == original.walletId, resolution.txid == txid, resolved == nil { - resolved = resolution - resolutionReady.fulfill() - } - } - defer { observation.cancel() } - await Bitkit.LightningService.shared.onchainTransactionReceived?(txid) - await fulfillment(of: [resolutionReady, recoveryReady], timeout: 2) - XCTAssertEqual(vm.recoveredOnchainFundingOrderId, order.id) - XCTAssertEqual(balanceRefreshes, 1) - XCTAssertNil(unrelatedVM.recoveredOnchainFundingOrderId) - XCTAssertEqual(unrelatedRefreshes, 0) - XCTAssertEqual(unrelatedNode.calls, 1) - await Bitkit.LightningService.shared.onchainTransactionReceived?(txid) - XCTAssertEqual(balanceRefreshes, 1, "A delayed repeat event must not resume order setup twice") - XCTAssertEqual(store.snapshot().first?.localFollowupComplete, true) - XCTAssertEqual(resolved?.activity.txId, txid, "Pending Details must refer to the actual winning funding transaction") - XCTAssertEqual(resolved?.activity.isTransfer, true) - if let resolved { - let context = OnchainSendPendingContext(attemptId: original.id, walletId: original.walletId, txid: original.txid) - XCTAssertTrue(SendPendingScreen.matchesLocalResolution(resolved, attempt: original, context: context, walletId: "new-selected-wallet")) - XCTAssertFalse(SendPendingScreen.matchesLocalResolution(resolved, attempt: original, - context: .init(attemptId: UUID(), walletId: original.walletId, txid: txid), - walletId: original.walletId)) - XCTAssertFalse(SendPendingScreen.matchesLocalResolution(resolved, attempt: original, - context: .init(attemptId: original.id, walletId: "other-wallet", txid: txid), - walletId: original.walletId)) - let reopened = try await service.resolvedAcceptedTransfer(context: context, using: makeService()) - XCTAssertEqual(reopened?.txid, txid, "Resolution preceding Pending initialization must still enable exact Details") - } - XCTAssertEqual(node.calls, 1) - withExtendedLifetime(wallet) {} - } - - @MainActor - func testUnknownOrderFundingOpensExactPendingWithoutFinishingSetup() async throws { - let store = MemoryAttemptStore() - let txid = String(repeating: "ab", count: 32) - let node = AttemptNodeMock(result: .unknown(txid: txid)) - let sheets = SheetViewModel() - let order = IBtOrder.mock() - let vm = TransferViewModel( - transferService: makeService(), sheetViewModel: sheets, - onchainAttemptService: OnchainSendAttemptService(store: store, hasPaidOrder: { _ in false }), - onchainSender: node, onchainBalanceProvider: { 50000 } - ) - do { - try await vm.payOrder(order: order, speed: .normal, txFee: 123, satsPerVbyte: 2) - XCTFail("Unknown funding must not finish setup") - } catch {} - let original = try XCTUnwrap(store.snapshot().first) - XCTAssertEqual(original.orderId, order.id) - XCTAssertTrue(original.blocksNewSend) - XCTAssertTrue(try Bitkit.TransferStorage(defaults: transferDefaults).getAll().isEmpty) - XCTAssertEqual(node.calls, 1) - XCTAssertEqual(sheets.activeSheetConfiguration?.id, .send) - let config = try XCTUnwrap(sheets.activeSheetConfiguration?.data as? SendConfig) - guard case let .onchainPending(context) = config.initialRoute else { - return XCTFail("Funding lost the exact original Pending route") - } - XCTAssertEqual(context.attemptId, original.id) - XCTAssertEqual(context.walletId, original.walletId) - XCTAssertEqual(context.txid, txid) - } - - @MainActor - func testDismissedFundingPendingReopensWithoutCreatingOrSendingAnotherOrder() async throws { - let store = MemoryAttemptStore() - let txid = String(repeating: "ab", count: 32) - let node = AttemptNodeMock(result: .unknown(txid: txid)) - let sheets = SheetViewModel() - let order = IBtOrder.mock() - let vm = TransferViewModel( - transferService: makeService(), sheetViewModel: sheets, - onchainAttemptService: OnchainSendAttemptService(store: store, hasPaidOrder: { _ in false }), - onchainSender: node, onchainBalanceProvider: { 50000 } - ) - vm.onOrderCreated(order: order) - do { try await vm.payOrder(order: order, speed: .normal, txFee: 123, satsPerVbyte: 2) } catch {} - let original = try XCTUnwrap(store.snapshot().first) - sheets.hideSheet() - vm.onEstimateReady(clientBalance: 20000, lspBalance: 10000, feeSat: 21000) - XCTAssertNil(vm.uiState.order, "A new estimate drops the screen order, not the global payment guard") - do { - _ = try await vm.orderForConfirmation { _, _ in - XCTFail("Confirmation must route retained funding before creating another order") - return IBtOrder.mock(id: "replacement-confirmation-order") - } - XCTFail("A new confirmation must yield to original Pending") - } catch is OnchainFundingPendingError {} catch { XCTFail("Wrong confirmation re-entry error: \(error)") } - sheets.hideSheet() - do { - _ = try await vm.orderForSwipe { _, _ in - XCTFail("An unresolved funding operation must not create a new order") - return IBtOrder.mock(id: "replacement-order") - } - XCTFail("The reset swipe must yield to original Pending") - } catch is OnchainFundingPendingError {} catch { XCTFail("Wrong funding re-entry error: \(error)") } - sheets.hideSheet() - await vm.onTransferToSpendingHwConfirm(walletId: "hardware-wallet") { _, _ in - XCTFail("Hardware confirmation must not create a replacement order") - return IBtOrder.mock(id: "replacement-hardware-order") - } - XCTAssertNil(vm.hwTransferError, "Routing Pending must not add a generic hardware error") - let config = try XCTUnwrap(sheets.activeSheetConfiguration?.data as? SendConfig) - guard case let .onchainPending(context) = config.initialRoute else { return XCTFail("Missing Pending") } - XCTAssertEqual(context.attemptId, original.id) - XCTAssertEqual(context.walletId, original.walletId) - XCTAssertEqual(context.txid, txid) - XCTAssertEqual(node.calls, 1) - } - - @MainActor - func testRejectedOrderFundingOpensExactPendingWithoutFinishingSetup() async throws { - let store = MemoryAttemptStore() - let txid = String(repeating: "ab", count: 32) - let node = AttemptNodeMock(result: .rejected(txid: txid, reason: "fixture refusal")) - let sheets = SheetViewModel() - let order = IBtOrder.mock() - let vm = TransferViewModel( - transferService: makeService(), sheetViewModel: sheets, - onchainAttemptService: OnchainSendAttemptService(store: store, hasPaidOrder: { _ in false }), - onchainSender: node, onchainBalanceProvider: { 50000 } - ) - do { - try await vm.payOrder(order: order, speed: .normal, txFee: 123, satsPerVbyte: 2) - XCTFail("Rejected funding must not finish setup") - } catch {} - let original = try XCTUnwrap(store.snapshot().first) - XCTAssertEqual(original.orderId, order.id) - XCTAssertTrue(original.blocksNewSend) - XCTAssertTrue(try Bitkit.TransferStorage(defaults: transferDefaults).getAll().isEmpty) - XCTAssertEqual(node.calls, 1) - XCTAssertEqual(sheets.activeSheetConfiguration?.id, .send) - let config = try XCTUnwrap(sheets.activeSheetConfiguration?.data as? SendConfig) - guard case let .onchainPending(context) = config.initialRoute else { - return XCTFail("Funding lost the exact original Pending route") - } - XCTAssertEqual(context.attemptId, original.id) - XCTAssertEqual(context.walletId, original.walletId) - XCTAssertEqual(context.txid, txid) - } - - @MainActor - func testTransferResumeRetainsOriginalBalanceMetadataAfterTrackingFailure() async throws { - for isMax in [false, true] { - transferDefaults.removeObject(forKey: "transfers") - let store = MemoryAttemptStore() - let txid = String(repeating: isMax ? "cd" : "ab", count: 32) - let node = AttemptNodeMock(result: .accepted(txid: txid)) - let attempts = OnchainSendAttemptService(store: store, hasPaidOrder: { _ in false }) - let order = IBtOrder.mock() - let initialTotal = isMax ? UInt64(20000) : order.feeSat + 123 - if isMax { - node.preparedMiningFeeSats = 123 - node.preparedRecipientAmountSats = initialTotal - 123 - } - let vm = TransferViewModel( - transferService: makeService(), sheetViewModel: SheetViewModel(), - onchainAttemptService: attempts, onchainSender: node, onchainBalanceProvider: { 50000 } - ) - transferDefaults.set(Data("broken-test-transfer-store".utf8), forKey: "transfers") - do { - try await vm.payOrder( - order: order, speed: .normal, txFee: 123, satsPerVbyte: 2, - isMaxAmount: isMax, maxSendableAmount: isMax ? initialTotal - 123 : nil - ) - XCTFail("Tracking failure was not surfaced") - } catch {} - XCTAssertEqual(node.calls, 1) - transferDefaults.removeObject(forKey: "transfers") - let restarted = TransferViewModel( - transferService: makeService(), sheetViewModel: SheetViewModel(), - onchainAttemptService: OnchainSendAttemptService(store: store, hasPaidOrder: { _ in false }), - onchainSender: node, onchainBalanceProvider: { 4000 } - ) - try await restarted.payOrder( - order: order, speed: .normal, txFee: 999, satsPerVbyte: 9, - isMaxAmount: false, maxSendableAmount: nil - ) - let tracking = try XCTUnwrap(Bitkit.TransferStorage(defaults: transferDefaults).getAll().first) - XCTAssertEqual(tracking.txTotalSats, initialTotal) - XCTAssertEqual(tracking.preTransferOnchainSats, 50000) - XCTAssertEqual(tracking.fundingTxId, txid) - XCTAssertEqual(node.calls, 1, "Transfer local resume dispatched funding twice") - } - } - - @MainActor - func testAcceptedIncompleteFundingReopensExpiredOriginalAndRepairsWithoutBroadcast() async throws { - let store = MemoryAttemptStore() - let txid = String(repeating: "ab", count: 32) - let node = AttemptNodeMock(result: .accepted(txid: txid)) - let attempts = OnchainSendAttemptService(store: store, hasPaidOrder: { _ in false }) - let sheets = SheetViewModel() - let order = IBtOrder.mock() - let vm = TransferViewModel(transferService: makeService(), sheetViewModel: sheets, - onchainAttemptService: attempts, onchainSender: node, onchainBalanceProvider: { 50000 }) - vm.onOrderCreated(order: order) - transferDefaults.set(Data("broken-transfer-store".utf8), forKey: "transfers") - do { try await vm.payOrder(order: order, speed: .normal, txFee: 123, satsPerVbyte: 2) } catch {} - let original = try XCTUnwrap(store.snapshot().first) - XCTAssertEqual(original.status, .accepted) - XCTAssertFalse(original.localFollowupComplete) - sheets.hideSheet() - do { - _ = try await vm.orderForSwipe { _, _ in - XCTFail("Accepted incomplete funding must retain the expired original order") - return IBtOrder.mock(id: "replacement-order") - } - XCTFail("Incomplete accepted funding must reopen Pending") - } catch is OnchainFundingPendingError {} catch { XCTFail("Wrong error: \(error)") } - let config = try XCTUnwrap(sheets.activeSheetConfiguration?.data as? SendConfig) - guard case let .onchainPending(context) = config.initialRoute else { return XCTFail("Missing Pending") } - XCTAssertEqual(context.attemptId, original.id) - XCTAssertEqual(context.walletId, original.walletId) - XCTAssertEqual(context.txid, txid) - XCTAssertEqual(node.calls, 1) - transferDefaults.removeObject(forKey: "transfers") - try await vm.payOrder(order: order, speed: .normal, txFee: 999, satsPerVbyte: 9) - XCTAssertTrue(try XCTUnwrap(store.snapshot().first).localFollowupComplete) - XCTAssertEqual(vm.recoveredOnchainFundingOrderId, order.id) - XCTAssertEqual(node.calls, 1) - } - - @MainActor - func testAcceptedOrderFollowupKeepsAdmissionContextWhenWalletChangesAfterDispatch() async throws { - let store = MemoryAttemptStore() - let txid = String(repeating: "ab", count: 32) - let node = AttemptNodeMock(result: .accepted(txid: txid)) - let order = IBtOrder.mock() - node.onSend = { - XCTAssertEqual(store.snapshot().first?.walletId, OnchainSendAttemptService.walletId(index: 0)) - node.currentWalletIndex = 1 - node.dispatchNode = NSObject() - } - let vm = TransferViewModel( - transferService: makeService(), sheetViewModel: SheetViewModel(), - onchainAttemptService: OnchainSendAttemptService(store: store, hasPaidOrder: { _ in false }), - onchainSender: node, onchainBalanceProvider: { node.currentWalletIndex == 0 ? 50000 : 4000 } - ) - do { - try await vm.payOrder(order: order, speed: .normal, txFee: 123, satsPerVbyte: 2) - } catch { - XCTFail("Accepted original order used the selected wallet after dispatch: \(error)") - } - let original = store.snapshot().first - XCTAssertEqual(original?.walletId, OnchainSendAttemptService.walletId(index: 0)) - XCTAssertEqual(original?.txid, txid) - XCTAssertEqual(original?.localFollowupComplete, true) - let tracking = try Bitkit.TransferStorage(defaults: transferDefaults).getAll().first - XCTAssertEqual(tracking?.fundingTxId, txid) - XCTAssertEqual(tracking?.txTotalSats, order.feeSat + 123) - XCTAssertEqual(tracking?.preTransferOnchainSats, 50000) - XCTAssertEqual(node.calls, 1) - } - - @MainActor - func testConfirmedAcceptedOrderRestoresTrackingAndAckWithoutRepayment() async throws { - let store = MemoryAttemptStore() - let txid = String(repeating: "ab", count: 32) - let node = AttemptNodeMock(result: .accepted(txid: txid)) - let order = IBtOrder.mock() - let vm = TransferViewModel( - transferService: makeService(), sheetViewModel: SheetViewModel(), - onchainAttemptService: OnchainSendAttemptService(store: store, hasPaidOrder: { _ in false }), - onchainSender: node, onchainBalanceProvider: { 50000 } - ) - transferDefaults.set(Data("broken-test-transfer-store".utf8), forKey: "transfers") - do { - try await vm.payOrder(order: order, speed: .normal, txFee: 123, satsPerVbyte: 2) - XCTFail("Tracking failure was not surfaced") - } catch {} - transferDefaults.removeObject(forKey: "transfers") - let confirmedCallback = Bitkit.LightningService.shared.onchainTransactionConfirmed - let receivedCallback = Bitkit.LightningService.shared.onchainTransactionReceived - defer { - Bitkit.LightningService.shared.onchainTransactionConfirmed = confirmedCallback - Bitkit.LightningService.shared.onchainTransactionReceived = receivedCallback - } - let wallet = makeWallet(attempts: OnchainSendAttemptService(store: store, hasPaidOrder: { _ in false })) - await Bitkit.LightningService.shared.onchainTransactionConfirmed?(txid) - let tracking = try Bitkit.TransferStorage(defaults: transferDefaults).getAll().first - XCTAssertEqual(tracking?.fundingTxId, txid) - XCTAssertEqual(tracking?.txTotalSats, order.feeSat + 123) - XCTAssertEqual(tracking?.preTransferOnchainSats, 50000) - XCTAssertEqual(store.snapshot().first?.localFollowupComplete, true) - XCTAssertEqual(node.calls, 1) - withExtendedLifetime(wallet) {} - } - func testAcceptedOrdinaryRecoveryAssociatesSyncCreatedSentRowWithoutDetachedContact() async throws { let store = MemoryAttemptStore() let txid = String(repeating: "19", count: 32) @@ -1472,33 +1155,6 @@ final class TransferServiceActivityTests: XCTestCase { XCTAssertNil(loaded.resolution) } - func testConcurrentPaidOrderFollowupCreatesOneOriginalRecord() async throws { - let storage = ConcurrentPaidOrderStorage(defaults: transferDefaults) - let service = Bitkit.TransferService( - storage: storage, lightningService: .shared, blocktankService: Bitkit.CoreService.shared.blocktank, - isGeoBlocked: { false } - ) - let txid = String(repeating: "ab", count: 32) - let first = Task.detached { - try await service.createTransfer(type: .toSpending, amountSats: 1000, fundingTxId: txid, - lspOrderId: "concurrent-paid-order", txTotalSats: 1100, preTransferOnchainSats: 9000) - } - await fulfillment(of: [storage.firstLookup], timeout: 5) - let second = Task.detached { - try await service.createTransfer(type: .toSpending, amountSats: 9999, fundingTxId: txid, - lspOrderId: "concurrent-paid-order", txTotalSats: 9999, preTransferOnchainSats: 9999) - } - let firstId = try await first.value - let secondId = try await second.value - XCTAssertEqual(firstId, secondId, "Concurrent original-operation follow-up created two record identities") - let records = try storage.getAll() - XCTAssertEqual(records.count, 1) - XCTAssertEqual(records.first?.fundingTxId, txid) - XCTAssertEqual(records.first?.amountSats, 1000) - XCTAssertEqual(records.first?.txTotalSats, 1100) - XCTAssertEqual(records.first?.preTransferOnchainSats, 9000) - } - @MainActor func testHardwareResolutionDoesNotReplayContactIntoSavingsOrOverwriteHardwareEdit() async throws { let identity = "pubky" + String(repeating: "z", count: 52) @@ -1530,39 +1186,6 @@ final class TransferServiceActivityTests: XCTestCase { } } - func testPaidOrderFollowupIsIdempotentAndRejectsDifferentFundingTxid() async throws { - let service = makeService() - let txid = String(repeating: "ab", count: 32) - let first = try await service.createTransfer(type: .toSpending, amountSats: 1000, fundingTxId: txid, lspOrderId: "paid-order") - let resumed = try await service.createTransfer(type: .toSpending, amountSats: 1000, fundingTxId: txid, lspOrderId: "paid-order") - XCTAssertEqual(first, resumed) - XCTAssertEqual(try Bitkit.TransferStorage(defaults: transferDefaults).getAll().count, 1) - do { - _ = try await service.createTransfer( - type: .toSpending, - amountSats: 1000, - fundingTxId: String(repeating: "cd", count: 32), - lspOrderId: "paid-order" - ) - XCTFail("Paid order was associated with another funding transaction") - } catch {} - XCTAssertEqual(try Bitkit.TransferStorage(defaults: transferDefaults).getAll().first?.fundingTxId, txid) - } - - func testPendingToSpendingActivityDoesNotStoreShortChannelId() async throws { - var channel = IBtChannel.mock() - channel.shortChannelId = "820100x5x0" - let order = IBtOrder.mock(channel: channel) - - await makeService().createPendingToSpendingActivity(order: order, txId: "hwtx1", fee: 141, feeRate: 2) - - let stored = try await activity.getOnchainActivityByTxId(txid: "hwtx1") - let onchain = try XCTUnwrap(stored) - XCTAssertNil(onchain.channelId, "The order's short channel id must not be stored as the activity channelId") - XCTAssertTrue(onchain.isTransfer, "A hardware funding activity must be marked as a transfer") - XCTAssertEqual(onchain.value, order.feeSat) - } - /// The atomic mark-as-transfer must only touch `isTransfer`/`channelId`, preserving confirmation /// and fee that a concurrent watcher sync may have written. func testMarkOnchainActivityAsTransferPreservesConfirmationAndFee() async throws { @@ -1615,25 +1238,3 @@ private final class HardwarePendingSavingsSpy: OnchainSendAttemptStoring, @unche func save(_: [OnchainSendAttempt]) {} } - -private final class ConcurrentPaidOrderStorage: Bitkit.TransferStorage { - let firstLookup = XCTestExpectation(description: "Original paid-order lookup captured") - private let lock = NSLock() - private let secondLookup = DispatchSemaphore(value: 0) - private var lookupCount = 0 - - override func getAll() throws -> [Bitkit.Transfer] { - let snapshot = try super.getAll() - lock.lock() - lookupCount += 1 - let index = lookupCount - lock.unlock() - if index == 1 { - firstLookup.fulfill() - _ = secondLookup.wait(timeout: .now() + 2) - } else if index == 2 { - secondLookup.signal() - } - return snapshot - } -} diff --git a/BitkitTests/TransferViewModelHwTests.swift b/BitkitTests/TransferViewModelHwTests.swift deleted file mode 100644 index f04a7c7b9..000000000 --- a/BitkitTests/TransferViewModelHwTests.swift +++ /dev/null @@ -1,920 +0,0 @@ -@testable import Bitkit -import BitkitCore -import XCTest - -/// Coordination coverage for the hardware-wallet transfer path in `TransferViewModel`: it drives the -/// `HwFundingSigner` (built from the injected capabilities), maps signer failures to published state, -/// and guards against re-entry. The device orchestration itself is covered by `HwFundingSignerTests`. -@MainActor -final class TransferViewModelHwTests: XCTestCase { - /// A successful mock broadcast reaches `fundPaidOrder`, which persists a transfer record. Without - /// an isolated suite that record lands in the app's own preferences and never settles, because - /// the mock order id is not a real Blocktank order (#733). - private var transferDefaults: UserDefaults! - - override func setUpWithError() throws { - try super.setUpWithError() - transferDefaults = try makeIsolatedDefaults() - guardAppDefaults("transfers") - } - - private func makeViewModel( - funding: MockHwFunding, - connecting: MockHwConnecting, - feeRate: UInt64? = 2, - sizingAddress: String? = nil, - timeouts: (compose: Double, sign: Double, broadcast: Double) = (compose: 5, sign: 5, broadcast: 5) - ) -> TransferViewModel { - TransferViewModel( - hwFunding: funding, - hwConnecting: connecting, - hwFeeRateProvider: { feeRate }, - hwAddressProvider: sizingAddress.map { address in { address } }, - hwTimeouts: timeouts, - transferDefaults: transferDefaults - ) - } - - private func awaitSigningComplete(_ vm: TransferViewModel, timeout: Double = 3) async { - let deadline = Date().addingTimeInterval(timeout) - while vm.hwSpending.isSigning, Date() < deadline { - try? await Task.sleep(nanoseconds: 10_000_000) - } - } - - // MARK: - Passphrase (hidden) wallets - - func testConfirmAsksForThePassphraseWhenTheSessionIsGone() async { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - connecting.walletsNeedingPassphrase = ["trezor:wallet"] - let vm = makeViewModel(funding: funding, connecting: connecting) - - vm.onTransferToSpendingHwConfirm(order: .mock(), walletId: "trezor:wallet") - await awaitSigningComplete(vm) - - XCTAssertTrue(vm.hwSpending.isPassphraseRequired) - XCTAssertFalse(vm.hwSpending.isSigning) - XCTAssertEqual(connecting.ensureCalls, 0, "the device is not reached before the passphrase is known") - XCTAssertEqual(funding.signCalls, 0) - } - - /// The reopen can also be demanded mid-flight, when the session turns out to hold another wallet. - func testAPassphraseDemandedDuringSigningRaisesThePromptInsteadOfToasting() async { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - connecting.connectError = HwPassphraseError.required - let vm = makeViewModel(funding: funding, connecting: connecting) - - vm.onTransferToSpendingHwConfirm(order: .mock(), walletId: "trezor:wallet") - await awaitSigningComplete(vm) - - XCTAssertTrue(vm.hwSpending.isPassphraseRequired) - XCTAssertNil(vm.hwTransferError, "the user can act on this, so it is a prompt and not an error") - XCTAssertEqual(funding.signCalls, 0) - } - - func testSubmittingTheRightPassphraseReopensTheWalletAndSigns() async { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - connecting.walletsNeedingPassphrase = ["trezor:wallet"] - let vm = makeViewModel(funding: funding, connecting: connecting) - let order = IBtOrder.mock() - vm.onTransferToSpendingHwConfirm(order: order, walletId: "trezor:wallet") - - vm.onHwPassphraseSubmit(order: order, walletId: "trezor:wallet", passphrase: "correct horse") - await awaitPassphraseVerified(vm) - await awaitSigningComplete(vm) - - XCTAssertEqual(connecting.reconnectCalls.first?.passphrase, "correct horse") - XCTAssertFalse(vm.hwSpending.isPassphraseRequired) - XCTAssertEqual(funding.signCalls, 1) - XCTAssertEqual(funding.broadcastCalls, 1) - } - - func testAPassphraseThatOpensAnotherWalletIsRejectedWithoutSigning() async { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - connecting.walletsNeedingPassphrase = ["trezor:wallet"] - connecting.reconnectError = HwPassphraseError.mismatch - let vm = makeViewModel(funding: funding, connecting: connecting) - let order = IBtOrder.mock() - vm.onTransferToSpendingHwConfirm(order: order, walletId: "trezor:wallet") - - vm.onHwPassphraseSubmit(order: order, walletId: "trezor:wallet", passphrase: "wrong") - await awaitPassphraseVerified(vm) - - XCTAssertEqual(vm.hwTransferError, .passphraseMismatch) - XCTAssertEqual(funding.signCalls, 0) - XCTAssertEqual(funding.broadcastCalls, 0) - XCTAssertTrue(vm.hwSpending.isPassphraseRequired, "the prompt stays up so the user can retry") - } - - /// The prompt can be swiped away while the device is still reopening the wallet. - func testDismissingThePromptStopsTheReopenFromRequestingASignature() async { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - connecting.walletsNeedingPassphrase = ["trezor:wallet"] - let vm = makeViewModel(funding: funding, connecting: connecting) - let order = IBtOrder.mock() - vm.onTransferToSpendingHwConfirm(order: order, walletId: "trezor:wallet") - - vm.onHwPassphraseSubmit(order: order, walletId: "trezor:wallet", passphrase: "correct horse") - vm.onHwPassphraseDismiss() - await awaitPassphraseVerified(vm) - await awaitSigningComplete(vm) - - XCTAssertFalse(vm.hwSpending.isPassphraseRequired) - XCTAssertEqual(funding.signCalls, 0, "nothing is signed after the user backs out") - XCTAssertEqual(funding.broadcastCalls, 0) - } - - /// Broadcasting never reaches the device, so a signed transaction awaiting retry must not be held - /// behind a passphrase — the session it would reopen is not needed to send it. - func testBroadcastRetryIsNotBlockedByTheRequiredPassphrase() async { - let funding = MockHwFunding() - funding.broadcastError = BroadcastError.ElectrumError(errorDetails: "offline") - let connecting = MockHwConnecting() - let vm = makeViewModel(funding: funding, connecting: connecting) - let order = IBtOrder.mock() - - vm.onTransferToSpendingHwConfirm(order: order, walletId: "trezor:wallet") - await awaitSigningComplete(vm) - XCTAssertTrue(vm.hwSpending.hasPendingBroadcast) - - // The device session is gone by the time the user retries. - connecting.walletsNeedingPassphrase = ["trezor:wallet"] - funding.broadcastError = nil - vm.onTransferToSpendingHwConfirm(order: order, walletId: "trezor:wallet") - await awaitSigningComplete(vm) - - XCTAssertFalse(vm.hwSpending.isPassphraseRequired) - XCTAssertEqual(funding.signCalls, 1, "the retry reuses the signed transaction") - XCTAssertEqual(funding.broadcastCalls, 2) - XCTAssertTrue(vm.hwFundingComplete) - } - - /// Only the transaction already signed for this order is exempt; anything else needs the device. - func testADifferentOrderStillAsksForThePassphraseWhileABroadcastIsPending() async { - let funding = MockHwFunding() - funding.broadcastError = BroadcastError.ElectrumError(errorDetails: "offline") - let connecting = MockHwConnecting() - let vm = makeViewModel(funding: funding, connecting: connecting) - - vm.onTransferToSpendingHwConfirm(order: .mock(), walletId: "trezor:wallet") - await awaitSigningComplete(vm) - XCTAssertTrue(vm.hwSpending.hasPendingBroadcast) - - connecting.walletsNeedingPassphrase = ["trezor:wallet"] - var other = IBtOrder.mock() - other.id = "another-order" - vm.onTransferToSpendingHwConfirm(order: other, walletId: "trezor:wallet") - await awaitSigningComplete(vm) - - XCTAssertTrue(vm.hwSpending.isPassphraseRequired) - XCTAssertEqual(funding.signCalls, 1, "nothing new is signed while the passphrase is unknown") - } - - /// Leaving the sign flow has to drop the reopen too, or a device answering afterwards would run - /// into a signature request for a screen the user already left. - func testLeavingTheSignFlowCancelsAnInFlightReopen() async { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - connecting.walletsNeedingPassphrase = ["trezor:wallet"] - let vm = makeViewModel(funding: funding, connecting: connecting) - let order = IBtOrder.mock() - vm.onTransferToSpendingHwConfirm(order: order, walletId: "trezor:wallet") - vm.onHwPassphraseSubmit(order: order, walletId: "trezor:wallet", passphrase: "correct horse") - - vm.cancelHwSigning() - await awaitPassphraseVerified(vm) - await awaitSigningComplete(vm) - - XCTAssertFalse(vm.hwSpending.isPassphraseRequired) - XCTAssertFalse(vm.hwSpending.isVerifyingPassphrase) - XCTAssertEqual(funding.signCalls, 0) - XCTAssertEqual(funding.broadcastCalls, 0) - } - - /// The prompt is dropped even while a signed transaction is held for retry, which the guard in - /// `cancelHwSigning` otherwise returns before reaching. - func testLeavingTheSignFlowClearsThePromptWhileABroadcastIsPending() async { - let funding = MockHwFunding() - funding.broadcastError = BroadcastError.ElectrumError(errorDetails: "offline") - let connecting = MockHwConnecting() - let vm = makeViewModel(funding: funding, connecting: connecting) - - vm.onTransferToSpendingHwConfirm(order: .mock(), walletId: "trezor:wallet") - await awaitSigningComplete(vm) - connecting.walletsNeedingPassphrase = ["trezor:wallet"] - var other = IBtOrder.mock() - other.id = "another-order" - vm.onTransferToSpendingHwConfirm(order: other, walletId: "trezor:wallet") - XCTAssertTrue(vm.hwSpending.isPassphraseRequired) - - vm.cancelHwSigning() - - XCTAssertFalse(vm.hwSpending.isPassphraseRequired) - XCTAssertTrue(vm.hwSpending.hasPendingBroadcast, "the signed transaction is still retained") - } - - func testAnEmptyPassphraseIsNotSubmitted() { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - let vm = makeViewModel(funding: funding, connecting: connecting) - - vm.onHwPassphraseSubmit(order: .mock(), walletId: "trezor:wallet", passphrase: "") - - XCTAssertTrue(connecting.reconnectCalls.isEmpty) - XCTAssertFalse(vm.hwSpending.isVerifyingPassphrase) - } - - private func awaitPassphraseVerified(_ vm: TransferViewModel, timeout: Double = 3) async { - let deadline = Date().addingTimeInterval(timeout) - while vm.hwSpending.isVerifyingPassphrase, Date() < deadline { - try? await Task.sleep(nanoseconds: 10_000_000) - } - } - - func testConfirmWithoutHwCapabilitiesSurfacesGenericError() { - let vm = TransferViewModel(transferDefaults: transferDefaults) // no signer injected - vm.onTransferToSpendingHwConfirm(order: .mock(), walletId: "trezor:wallet") - if case .generic = vm.hwTransferError {} else { - XCTFail("expected .generic error") - } - XCTAssertFalse(vm.hwSpending.isSigning) - } - - func testUpdateHwLimitsSurfacesErrorWhenAvailabilityFails() async { - let funding = MockHwFunding() - funding.accountError = MockHwFunding.TestError() - let vm = makeViewModel(funding: funding, connecting: MockHwConnecting()) - - await vm.updateHwLimits(walletId: "trezor:wallet", blocktankInfo: nil, estimateOrderFee: { _, _ in (0, 0) }) - - if case .generic = vm.hwTransferError {} else { - XCTFail("expected .generic error") - } - XCTAssertFalse(vm.hwSpending.isLoading) - } - - /// Regression: the funding guards must price a hardware transfer against the device account. - /// Reading on-chain savings here would reject every one of them — those funds never sit in this - /// wallet. - func testHwFundingBudgetReadsTheDeviceAccount() async { - let funding = MockHwFunding() - funding.account = HwFundingAccount(xpub: "zpubNS", addressType: .nativeSegwit, balanceSats: 1_000_000) - funding.maxSpendable = 990_000 - let vm = TransferViewModel( - hwFunding: funding, - hwConnecting: MockHwConnecting(), - hwFeeRateProvider: { 2 }, - hwAddressProvider: { "bcrt1qtest" }, - transferDefaults: transferDefaults - ) - - let budget = await vm.hwFundingBudget(walletId: "trezor:wallet") - - XCTAssertEqual(budget, 990_000) - XCTAssertEqual(funding.maxSpendableCalls.count, 1) - } - - /// Without an address to compose against, the budget still comes from the device balance — via - /// the conservative reserve clamp rather than an exact `sendMax`. - func testHwFundingBudgetFallsBackToTheDeviceReserveClamp() async { - let funding = MockHwFunding() - funding.account = HwFundingAccount(xpub: "zpubNS", addressType: .nativeSegwit, balanceSats: 1_000_000) - let vm = makeViewModel(funding: funding, connecting: MockHwConnecting()) - - let budget = await vm.hwFundingBudget(walletId: "trezor:wallet") - - XCTAssertEqual(funding.maxSpendableCalls.count, 0) - let clamped = try? XCTUnwrap(budget) - XCTAssertNotNil(clamped) - XCTAssertGreaterThan(clamped ?? 0, 0) - XCTAssertLessThan(clamped ?? 0, funding.account.balanceSats) - } - - func testHwFundingBudgetIsNilWhenTheDeviceIsUnreachable() async { - let funding = MockHwFunding() - funding.accountError = MockHwFunding.TestError() - let vm = makeViewModel(funding: funding, connecting: MockHwConnecting()) - - let budget = await vm.hwFundingBudget(walletId: "trezor:wallet") - - // An unreadable device balance leaves the guard non-blocking rather than rejecting. - XCTAssertNil(budget) - } - - func testUpdateHwLimitsClearsStalePreviousDeviceCap() async { - let funding = MockHwFunding() - funding.accountError = MockHwFunding.TestError() - let vm = makeViewModel(funding: funding, connecting: MockHwConnecting()) - vm.hwSpending.maxAllowedToSend = 999_999 // stale cap from a previously-selected device - - await vm.updateHwLimits(walletId: "trezor:wallet", blocktankInfo: nil, estimateOrderFee: { _, _ in (0, 0) }) - - XCTAssertEqual(vm.hwSpending.maxAllowedToSend, 0, "a reload must not keep the previous device's cap") - } - - func testReconnectFailureMapsToReconnectErrorAndResetsSigning() async { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - connecting.connectError = MockHwFunding.TestError() - let vm = makeViewModel(funding: funding, connecting: connecting) - - vm.onTransferToSpendingHwConfirm(order: .mock(), walletId: "trezor:wallet") - await awaitSigningComplete(vm) - - XCTAssertEqual(vm.hwTransferError, .reconnect(isBluetooth: false)) - XCTAssertFalse(vm.hwSpending.isSigning) - } - - func testReconnectFailureCarriesBluetoothFlagForKnownBleDevice() async { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - connecting.connectError = MockHwFunding.TestError() - connecting.isBluetooth = true - let vm = makeViewModel(funding: funding, connecting: connecting) - - vm.onTransferToSpendingHwConfirm(order: .mock(), walletId: "trezor:wallet") - await awaitSigningComplete(vm) - - XCTAssertEqual(vm.hwTransferError, .reconnect(isBluetooth: true), "a known BLE device gets the softer INFO reconnect toast") - } - - func testRawSignErrorMapsToGenericError() async { - let funding = MockHwFunding() - funding.signError = MockHwFunding.TestError() - let connecting = MockHwConnecting() - let vm = makeViewModel(funding: funding, connecting: connecting) - - vm.onTransferToSpendingHwConfirm(order: .mock(), walletId: "trezor:wallet") - await awaitSigningComplete(vm) - - if case .generic = vm.hwTransferError {} else { - XCTFail("expected .generic error, got \(String(describing: vm.hwTransferError))") - } - XCTAssertTrue(connecting.staleDisconnects.isEmpty) - } - - func testBroadcastFailureRetainsSignedTransactionForRetry() async { - let funding = MockHwFunding() - funding.broadcastError = BroadcastError.ElectrumError(errorDetails: "offline") - let connecting = MockHwConnecting() - let vm = makeViewModel(funding: funding, connecting: connecting) - let order = IBtOrder.mock() - - vm.onTransferToSpendingHwConfirm(order: order, walletId: "trezor:wallet") - await awaitSigningComplete(vm) - - XCTAssertTrue(vm.hwSpending.hasPendingBroadcast) - XCTAssertEqual(vm.hwSpending.miningFeeSats, funding.funding.miningFeeSats) - XCTAssertEqual(funding.signCalls, 1) - XCTAssertEqual(funding.broadcastCalls, 1) - XCTAssertEqual(vm.hwTransferError, .broadcastConnectivity) - - vm.onTransferToSpendingHwConfirm(order: order, walletId: "trezor:wallet") - await awaitSigningComplete(vm) - - XCTAssertTrue(vm.hwSpending.hasPendingBroadcast) - XCTAssertEqual(connecting.ensureCalls, 1) - XCTAssertEqual(funding.composeCalls.count, 1) - XCTAssertEqual(funding.signCalls, 1, "retry must reuse the signed transaction") - XCTAssertEqual(funding.broadcastCalls, 2) - - vm.cancelHwSigning() - XCTAssertTrue(vm.hwSpending.hasPendingBroadcast, "leaving must retain an uncertain signed transaction") - vm.onEstimateReady(clientBalance: 100_000, lspBalance: 200_000, feeSat: 101_000) - XCTAssertFalse(vm.hwSpending.hasPendingBroadcast, "starting a new transfer discards the previous retry state") - } - - func testBroadcastRetryReusesThePendingOrderAfterItExpires() async { - let funding = MockHwFunding() - funding.broadcastError = BroadcastError.ElectrumError(errorDetails: "offline") - let vm = makeViewModel(funding: funding, connecting: MockHwConnecting()) - var order = IBtOrder.mock() - order.orderExpiresAt = "2000-01-01T00:00:00.175Z" - vm.onOrderCreated(order: order) - - vm.onTransferToSpendingHwConfirm(order: order, walletId: "trezor:wallet") - await awaitSigningComplete(vm) - XCTAssertTrue(vm.hwSpending.hasPendingBroadcast) - - funding.broadcastError = nil - var createCalls = 0 - await vm.onTransferToSpendingHwConfirm(walletId: "trezor:wallet") { _, _ in - createCalls += 1 - var replacement = IBtOrder.mock() - replacement.id = "replacement" - return replacement - } - await awaitSigningComplete(vm) - - XCTAssertEqual(createCalls, 0) - XCTAssertEqual(funding.signCalls, 1, "the retry reuses the signed transaction") - XCTAssertEqual(funding.broadcastCalls, 2) - } - - func testBroadcastRetryDoesNotReuseSignedTransactionAfterOrderAddressChanges() async { - let funding = MockHwFunding() - funding.broadcastError = BroadcastError.ElectrumError(errorDetails: "offline") - let vm = makeViewModel(funding: funding, connecting: MockHwConnecting()) - var order = IBtOrder.mock() - - vm.onTransferToSpendingHwConfirm(order: order, walletId: "trezor:wallet") - await awaitSigningComplete(vm) - - order.payment?.onchain?.address = "bc1qnewdestination" - vm.onTransferToSpendingHwConfirm(order: order, walletId: "trezor:wallet") - await awaitSigningComplete(vm) - - XCTAssertEqual(funding.signCalls, 2) - XCTAssertEqual(funding.composeCalls.last?.address, "bc1qnewdestination") - } - - func testDeviceCancelDuringSigningIsSilent() async { - let funding = MockHwFunding() - funding.signError = TrezorError.UserCancelled - let connecting = MockHwConnecting() - let vm = makeViewModel(funding: funding, connecting: connecting) - - vm.onTransferToSpendingHwConfirm(order: .mock(), walletId: "trezor:wallet") - await awaitSigningComplete(vm) - - XCTAssertNil(vm.hwTransferError, "a device cancel must not surface a toast") - XCTAssertFalse(vm.hwSpending.isSigning) - XCTAssertEqual(vm.hwSignedEvent, 0, "a cancelled transfer must not advance the flow") - XCTAssertTrue(connecting.staleDisconnects.isEmpty, "a device cancel must not tear down the session") - } - - /// On device, `ServiceQueue` boxes the `TrezorError` into an `AppError` before it reaches the view - /// model, so the silent-cancel path must survive the wrapping (raw `TrezorError` is covered above). - /// `AppError` is qualified as `Bitkit.AppError` because `Errors.swift` is also compiled into this - /// test target — an unqualified `AppError` would be the test-target copy, not what production throws. - func testWrappedDeviceCancelDuringSigningIsSilent() async { - let funding = MockHwFunding() - funding.signError = Bitkit.AppError(error: TrezorError.UserCancelled) - let connecting = MockHwConnecting() - let vm = makeViewModel(funding: funding, connecting: connecting) - - vm.onTransferToSpendingHwConfirm(order: .mock(), walletId: "trezor:wallet") - await awaitSigningComplete(vm) - - XCTAssertNil(vm.hwTransferError, "a wrapped device cancel must not surface a toast") - XCTAssertFalse(vm.hwSpending.isSigning) - XCTAssertEqual(vm.hwSignedEvent, 0, "a cancelled transfer must not advance the flow") - } - - func testDeviceCancelDuringReconnectIsSilent() async { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - connecting.connectError = TrezorError.UserCancelled - let vm = makeViewModel(funding: funding, connecting: connecting) - - vm.onTransferToSpendingHwConfirm(order: .mock(), walletId: "trezor:wallet") - await awaitSigningComplete(vm) - - XCTAssertNil(vm.hwTransferError, "a reconnect-step cancel must not surface a toast") - XCTAssertFalse(vm.hwSpending.isSigning) - XCTAssertTrue(funding.composeCalls.isEmpty, "compose must not run after a reconnect cancel") - } - - func testWarmUpHardwareConnectionDelegatesToConnecting() { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - let vm = makeViewModel(funding: funding, connecting: connecting) - - vm.warmUpHardwareConnection(walletId: "trezor:wallet") - - XCTAssertEqual(connecting.warmUpCalls, ["trezor:wallet"]) - } - - func testCancelHwSigningStopsInFlightSign() async { - let funding = MockHwFunding() - funding.signDelay = 1.0 // keep signing in-flight so cancel interrupts it - let connecting = MockHwConnecting() - let vm = makeViewModel(funding: funding, connecting: connecting) - - vm.onTransferToSpendingHwConfirm(order: .mock(), walletId: "trezor:wallet") - while !vm.hwSpending.isSigning { - await Task.yield() - } - vm.cancelHwSigning() - for _ in 0 ..< 50 where connecting.staleDisconnects.isEmpty { - try? await Task.sleep(nanoseconds: 10_000_000) - } - await awaitSigningComplete(vm) - - XCTAssertFalse(vm.hwSpending.isSigning) - XCTAssertEqual(vm.hwSignedEvent, 0, "a cancelled sign must not advance the flow") - XCTAssertEqual(connecting.staleDisconnects, ["trezor:wallet"], "cancelling during sign must tear down the stale session") - } - - func testTheSignScreenCanBeLeftOnlyWhileTheDeviceIsBeingReached() async { - let funding = MockHwFunding() - funding.signDelay = 1.0 - let connecting = MockHwConnecting() - let gate = AsyncGate() - connecting.connectGate = gate - let vm = makeViewModel(funding: funding, connecting: connecting) - XCTAssertTrue(vm.canLeaveHwSign) - - vm.onTransferToSpendingHwConfirm(order: .mock(), walletId: "jade:wallet") - while !vm.hwSpending.isConnectingDevice { - await Task.yield() - } - XCTAssertTrue(vm.isSpendingBusy) - XCTAssertTrue(vm.canLeaveHwSign, "a device waiting for its PIN must not trap the user") - - gate.open() - while funding.signCalls == 0 { - await Task.yield() - } - XCTAssertFalse(vm.hwSpending.isConnectingDevice) - XCTAssertFalse(vm.canLeaveHwSign, "the device has been asked to sign") - - vm.cancelHwSigning() - await awaitSigningComplete(vm) - XCTAssertTrue(vm.canLeaveHwSign) - } - - func testLeavingWhileTheDeviceIsBeingReachedReleasesIt() async { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - let gate = AsyncGate() - connecting.connectGate = gate - let vm = makeViewModel(funding: funding, connecting: connecting) - - vm.onTransferToSpendingHwConfirm(order: .mock(), walletId: "jade:wallet") - while !vm.hwSpending.isConnectingDevice { - await Task.yield() - } - vm.cancelHwSigning() - for _ in 0 ..< 50 where connecting.staleDisconnects.isEmpty { - try? await Task.sleep(nanoseconds: 10_000_000) - } - gate.open() - await awaitSigningComplete(vm) - - XCTAssertFalse(vm.hwSpending.isConnectingDevice) - XCTAssertEqual(connecting.staleDisconnects, ["jade:wallet"]) - XCTAssertEqual(funding.signCalls, 0, "an abandoned connect never reaches signing") - XCTAssertEqual(vm.hwSignedEvent, 0) - } - - func testDeviceBusyMapsToDeviceBusyError() async { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - connecting.connectError = TrezorError.DeviceBusy - let vm = makeViewModel(funding: funding, connecting: connecting) - - vm.onTransferToSpendingHwConfirm(order: .mock(), walletId: "trezor:wallet") - await awaitSigningComplete(vm) - - XCTAssertEqual(vm.hwTransferError, .deviceBusy(.trezor)) - } - - func testADeviceHoldingAnotherWalletShowsTheWalletMismatch() async { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - connecting.connectError = HwWalletMismatchError() - let vm = makeViewModel(funding: funding, connecting: connecting) - - vm.onTransferToSpendingHwConfirm(order: .mock(), walletId: "jade:wallet") - await awaitSigningComplete(vm) - - XCTAssertEqual(vm.hwTransferError, .walletMismatch) - XCTAssertTrue(funding.composeCalls.isEmpty) - } - - func testASigningSessionHoldingAnotherWalletShowsTheWalletMismatch() async { - let funding = MockHwFunding() - funding.signError = HwWalletMismatchError() - let connecting = MockHwConnecting() - let vm = makeViewModel(funding: funding, connecting: connecting) - - vm.onTransferToSpendingHwConfirm(order: .mock(), walletId: "jade:wallet") - await awaitSigningComplete(vm) - - XCTAssertEqual(vm.hwTransferError, .walletMismatch) - } - - func testJadeBusyShowsTheJadeCopy() async { - let funding = MockHwFunding() - funding.signError = Bitkit.AppError(error: JadeError.DeviceLocked) - let connecting = MockHwConnecting() - let vm = makeViewModel(funding: funding, connecting: connecting) - - vm.onTransferToSpendingHwConfirm(order: .mock(), walletId: "jade:wallet") - await awaitSigningComplete(vm) - - XCTAssertEqual(vm.hwTransferError, .deviceBusy(.blockstream)) - XCTAssertEqual(HwErrorPresenter.deviceBusyMessage(for: .blockstream), t("hardware__jade_device_busy")) - XCTAssertTrue(connecting.staleDisconnects.isEmpty, "a busy device keeps its session") - } - - func testJadeCancellationOnDeviceIsSilent() async { - let funding = MockHwFunding() - funding.signError = Bitkit.AppError(error: JadeError.UserCancelled) - let connecting = MockHwConnecting() - let vm = makeViewModel(funding: funding, connecting: connecting) - - vm.onTransferToSpendingHwConfirm(order: .mock(), walletId: "jade:wallet") - await awaitSigningComplete(vm) - - XCTAssertNil(vm.hwTransferError, "a cancel on the Jade must not surface a toast") - XCTAssertFalse(vm.hwSpending.isSigning) - XCTAssertEqual(vm.hwSignedEvent, 0, "a cancelled transfer must not advance the flow") - XCTAssertTrue(connecting.staleDisconnects.isEmpty, "a device cancel must not tear down the session") - } - - func testJadeSigningFailureShowsTheJadeCopy() async { - let funding = MockHwFunding() - funding.signError = Bitkit.AppError(error: JadeError.PsbtTooLarge(size: 20000, max: 16384)) - let vm = makeViewModel(funding: funding, connecting: MockHwConnecting()) - - vm.onTransferToSpendingHwConfirm(order: .mock(), walletId: "jade:wallet") - await awaitSigningComplete(vm) - - XCTAssertEqual(vm.hwTransferError, .generic(t("hardware__jade_psbt_too_large"))) - } - - func testFirmwareErrorMapsToFirmwareReconnectError() async { - let funding = MockHwFunding() - funding.signError = Bitkit.AppError( - message: "Firmware error", - debugMessage: "Device error (code 99): Firmware error" - ) - let connecting = MockHwConnecting() - let vm = makeViewModel(funding: funding, connecting: connecting) - - vm.onTransferToSpendingHwConfirm(order: .mock(), walletId: "trezor:wallet") - await awaitSigningComplete(vm) - - XCTAssertEqual(vm.hwTransferError, .firmwareReconnect) - } - - func testElectrumBroadcastFailureKeepsPendingAndSurfacesConnectivityToast() async { - let funding = MockHwFunding() - funding.broadcastError = BroadcastError.ElectrumError(errorDetails: "offline") - let vm = makeViewModel(funding: funding, connecting: MockHwConnecting()) - let order = IBtOrder.mock() - - vm.onTransferToSpendingHwConfirm(order: order, walletId: "trezor:wallet") - await awaitSigningComplete(vm) - - XCTAssertTrue(vm.hwSpending.hasPendingBroadcast) - XCTAssertEqual(vm.hwTransferError, .broadcastConnectivity) - } - - func testPermanentBroadcastFailureClearsPendingState() async { - let funding = MockHwFunding() - funding.broadcastError = Bitkit.AppError(message: "rejected", debugMessage: "invalid tx") - let vm = makeViewModel(funding: funding, connecting: MockHwConnecting()) - - vm.onTransferToSpendingHwConfirm(order: .mock(), walletId: "trezor:wallet") - await awaitSigningComplete(vm) - - XCTAssertFalse(vm.hwSpending.hasPendingBroadcast) - if case .generic = vm.hwTransferError {} else { - XCTFail("expected .generic error") - } - } - - func testElectrumBroadcastRejectionClearsPendingState() async { - let funding = MockHwFunding() - funding.broadcastError = BroadcastError.ElectrumError( - errorDetails: "Broadcast failed: bad-txns-inputs-missingorspent" - ) - let vm = makeViewModel(funding: funding, connecting: MockHwConnecting()) - - vm.onTransferToSpendingHwConfirm(order: .mock(), walletId: "trezor:wallet") - await awaitSigningComplete(vm) - - XCTAssertFalse(vm.hwSpending.hasPendingBroadcast) - if case .generic = vm.hwTransferError {} else { - XCTFail("expected .generic error") - } - XCTAssertEqual(funding.signCalls, 1) - } - - func testUpdateHwLimitsClearsDesyncedPendingBroadcast() async { - let funding = MockHwFunding() - let vm = makeViewModel(funding: funding, connecting: MockHwConnecting()) - funding.broadcastError = BroadcastError.ElectrumError(errorDetails: "offline") - let order = IBtOrder.mock() - - vm.onTransferToSpendingHwConfirm(order: order, walletId: "trezor:wallet") - await awaitSigningComplete(vm) - XCTAssertTrue(vm.hwSpending.hasPendingBroadcast) - - await vm.updateHwLimits(walletId: "trezor:wallet", blocktankInfo: nil, estimateOrderFee: { _, _ in (0, 0) }) - - XCTAssertFalse(vm.hwSpending.hasPendingBroadcast) - vm.cancelHwSigning() - XCTAssertEqual(funding.signCalls, 1, "cancel after limits reload must not be blocked by stale pending tx") - } - - func testUpdateHwFundingFeeEstimateSetsMiningFeeBeforeSigning() async { - let funding = MockHwFunding() - let vm = makeViewModel(funding: funding, connecting: MockHwConnecting(), sizingAddress: "bcrt1qsizing") - let estimate = IBtOrder.mock(feeSat: 101_000, lspBalanceSat: 200_000, clientBalanceSat: 100_000) - - vm.onEstimateReady(clientBalance: estimate.clientBalanceSat, lspBalance: estimate.lspBalanceSat, feeSat: estimate.feeSat) - await vm.updateHwFundingFeeEstimate(walletId: "trezor:wallet") - - XCTAssertEqual(vm.hwSpending.miningFeeSats, funding.funding.miningFeeSats) - XCTAssertEqual(funding.estimateCalls.count, 1) - XCTAssertEqual(funding.estimateCalls.first?.address, "bcrt1qsizing", "sized against an app address; the order does not exist yet") - XCTAssertEqual(funding.estimateCalls.first?.sats, estimate.feeSat) - XCTAssertTrue(funding.composeCalls.isEmpty) - } - - func testUpdateHwFundingFeeEstimateSkipsWithoutASizingAddress() async { - let funding = MockHwFunding() - let vm = makeViewModel(funding: funding, connecting: MockHwConnecting()) - - await vm.updateHwFundingFeeEstimate(walletId: "trezor:wallet") - - XCTAssertEqual(vm.hwSpending.miningFeeSats, 0) - XCTAssertTrue(funding.estimateCalls.isEmpty) - } - - func testConfirmCreatesTheOrderOnceThenSigns() async { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - let vm = makeViewModel(funding: funding, connecting: connecting) - let estimate = IBtOrder.mock(feeSat: 101_000, lspBalanceSat: 200_000, clientBalanceSat: 100_000) - vm.onEstimateReady(clientBalance: estimate.clientBalanceSat, lspBalance: estimate.lspBalanceSat, feeSat: estimate.feeSat) - var createCalls = 0 - - await vm.onTransferToSpendingHwConfirm(walletId: "trezor:wallet") { clientBalance, lspBalance in - createCalls += 1 - return IBtOrder.mock(feeSat: estimate.feeSat, lspBalanceSat: lspBalance, clientBalanceSat: clientBalance) - } - await awaitSigningComplete(vm) - - XCTAssertEqual(createCalls, 1) - XCTAssertEqual(vm.uiState.order?.id, "order123") - XCTAssertEqual(funding.composeCalls.first?.sats, estimate.feeSat) - XCTAssertEqual(funding.signCalls, 1) - XCTAssertEqual(funding.broadcastCalls, 1) - XCTAssertEqual(vm.hwSignedEvent, 1) - - vm.onEstimateReady(clientBalance: estimate.clientBalanceSat, lspBalance: estimate.lspBalanceSat, feeSat: estimate.feeSat) - XCTAssertNil(vm.uiState.order) - } - - func testConfirmSurfacesAFailedOrderCreationWithoutSigning() async { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - let vm = makeViewModel(funding: funding, connecting: connecting) - let estimate = IBtOrder.mock(feeSat: 101_000, lspBalanceSat: 200_000, clientBalanceSat: 100_000) - vm.onEstimateReady(clientBalance: estimate.clientBalanceSat, lspBalance: estimate.lspBalanceSat, feeSat: estimate.feeSat) - - await vm.onTransferToSpendingHwConfirm(walletId: "trezor:wallet") { _, _ in - throw MockHwFunding.TestError() - } - await awaitSigningComplete(vm) - - if case .generic = vm.hwTransferError {} else { - XCTFail("expected .generic error, got \(String(describing: vm.hwTransferError))") - } - XCTAssertNil(vm.uiState.order) - XCTAssertEqual(connecting.ensureCalls, 0) - XCTAssertEqual(funding.signCalls, 0) - XCTAssertFalse(vm.hwSpending.isCreatingOrder) - } - - func testCancellingWhileCreatingTheOrderDoesNotStartSigning() async { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - let vm = makeViewModel(funding: funding, connecting: connecting) - let order = IBtOrder.mock(feeSat: 101_000, lspBalanceSat: 200_000, clientBalanceSat: 100_000) - vm.onEstimateReady(clientBalance: order.clientBalanceSat, lspBalance: order.lspBalanceSat, feeSat: order.feeSat) - - let confirmTask = Task { @MainActor in - await vm.onTransferToSpendingHwConfirm(walletId: "trezor:wallet") { _, _ in - try await Task.sleep(nanoseconds: 200_000_000) - return order - } - } - let deadline = Date().addingTimeInterval(1) - while !vm.hwSpending.isCreatingOrder, Date() < deadline { - try? await Task.sleep(nanoseconds: 10_000_000) - } - XCTAssertTrue(vm.hwSpending.isCreatingOrder) - - vm.cancelHwSigning() - await confirmTask.value - - XCTAssertFalse(vm.hwSpending.isCreatingOrder) - XCTAssertFalse(vm.hwSpending.isSigning) - XCTAssertEqual(connecting.ensureCalls, 0) - XCTAssertEqual(funding.composeCalls.count, 0) - XCTAssertEqual(funding.signCalls, 0) - } - - func testAnOlderOrderCreationCannotReplaceANewerConfirmation() async { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - let vm = makeViewModel(funding: funding, connecting: connecting) - let oldOrder = IBtOrder.mock(feeSat: 101_000, lspBalanceSat: 200_000, clientBalanceSat: 100_000) - var newOrder = IBtOrder.mock(feeSat: 201_000, lspBalanceSat: 300_000, clientBalanceSat: 200_000) - newOrder.id = "new-order" - newOrder.payment?.onchain?.address = "bc1qneworder" - vm.onEstimateReady( - clientBalance: oldOrder.clientBalanceSat, - lspBalance: oldOrder.lspBalanceSat, - feeSat: oldOrder.feeSat - ) - - let oldConfirmation = Task { @MainActor in - await vm.onTransferToSpendingHwConfirm(walletId: "trezor:wallet") { _, _ in - try await Task.sleep(nanoseconds: 150_000_000) - return oldOrder - } - } - let oldCreationDeadline = Date().addingTimeInterval(1) - while !vm.hwSpending.isCreatingOrder, Date() < oldCreationDeadline { - await Task.yield() - } - XCTAssertTrue(vm.hwSpending.isCreatingOrder) - vm.cancelHwSigning() - vm.onEstimateReady( - clientBalance: newOrder.clientBalanceSat, - lspBalance: newOrder.lspBalanceSat, - feeSat: newOrder.feeSat - ) - - let newConfirmation = Task { @MainActor in - await vm.onTransferToSpendingHwConfirm(walletId: "trezor:wallet") { _, _ in - try await Task.sleep(nanoseconds: 300_000_000) - return newOrder - } - } - let newCreationDeadline = Date().addingTimeInterval(1) - while !vm.hwSpending.isCreatingOrder, Date() < newCreationDeadline { - await Task.yield() - } - XCTAssertTrue(vm.hwSpending.isCreatingOrder) - await oldConfirmation.value - XCTAssertTrue(vm.hwSpending.isCreatingOrder, "the older request must not clear the newer request's state") - - await newConfirmation.value - await awaitSigningComplete(vm) - - XCTAssertEqual(vm.uiState.order?.id, newOrder.id) - XCTAssertEqual(funding.composeCalls.last?.address, "bc1qneworder") - XCTAssertEqual(funding.signCalls, 1) - } - - func testConfirmRaisesThePassphrasePromptAfterTheOrderExists() async { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - connecting.walletsNeedingPassphrase = ["trezor:wallet"] - let vm = makeViewModel(funding: funding, connecting: connecting) - let estimate = IBtOrder.mock(feeSat: 101_000, lspBalanceSat: 200_000, clientBalanceSat: 100_000) - vm.onEstimateReady(clientBalance: estimate.clientBalanceSat, lspBalance: estimate.lspBalanceSat, feeSat: estimate.feeSat) - - await vm.onTransferToSpendingHwConfirm(walletId: "trezor:wallet") { clientBalance, lspBalance in - IBtOrder.mock(feeSat: estimate.feeSat, lspBalanceSat: lspBalance, clientBalanceSat: clientBalance) - } - - XCTAssertTrue(vm.hwSpending.isPassphraseRequired) - XCTAssertNotNil(vm.uiState.order, "the prompt's submit signs the created order") - XCTAssertEqual(funding.signCalls, 0) - } - - func testReentrancyGuardIgnoresConcurrentConfirm() async { - let funding = MockHwFunding() - funding.composeError = MockHwFunding.TestError() - let connecting = MockHwConnecting() - let vm = makeViewModel(funding: funding, connecting: connecting) - - vm.onTransferToSpendingHwConfirm(order: .mock(), walletId: "trezor:wallet") - // Second call while the first is still signing must be ignored. - vm.onTransferToSpendingHwConfirm(order: .mock(), walletId: "trezor:wallet") - await awaitSigningComplete(vm) - - XCTAssertEqual(connecting.ensureCalls, 1, "only the first confirm should run") - XCTAssertEqual(funding.composeCalls.count, 1) - } - - func testMissingOrderAddressSurfacesGenericErrorWithoutSigning() { - let funding = MockHwFunding() - let connecting = MockHwConnecting() - let vm = makeViewModel(funding: funding, connecting: connecting) - var order = IBtOrder.mock() - order.payment?.onchain?.address = "" - - vm.onTransferToSpendingHwConfirm(order: order, walletId: "trezor:wallet") - - if case .generic = vm.hwTransferError {} else { - XCTFail("expected .generic error") - } - XCTAssertFalse(vm.hwSpending.isSigning) - XCTAssertEqual(connecting.ensureCalls, 0) - } -} diff --git a/BitkitTests/TransferViewModelTests.swift b/BitkitTests/TransferViewModelTests.swift deleted file mode 100644 index 7a4129d56..000000000 --- a/BitkitTests/TransferViewModelTests.swift +++ /dev/null @@ -1,906 +0,0 @@ -@testable import Bitkit -import BitkitCore -import XCTest - -final class TransferViewModelTests: XCTestCase { - /// The convenience initializer builds a real `TransferService`; an isolated suite keeps these - /// tests off the host app's own transfer store, and `guardAppDefaults` keeps it that way (#733). - private var transferDefaults: UserDefaults! - - override func setUpWithError() throws { - try super.setUpWithError() - transferDefaults = try makeIsolatedDefaults() - guardAppDefaults("transfers") - } - - @MainActor - private func makeViewModel() -> TransferViewModel { - TransferViewModel(transferDefaults: transferDefaults) - } - - @MainActor - func testEstimatesChangeBalancesWithoutCreatingAnOrder() async throws { - let vm = makeViewModel() - vm.onEstimateReady(clientBalance: 100_000, lspBalance: 50000, feeSat: 101_000) - XCTAssertNil(vm.uiState.order) - vm.onEstimateReady(clientBalance: 100_000, lspBalance: 150_000, feeSat: 102_000, isAdvanced: true) - XCTAssertTrue(vm.uiState.isAdvanced) - XCTAssertNil(vm.uiState.order) - - try await vm.onDefaultClick(lspBalance: 50000) { client, lsp in - XCTAssertEqual(client, 100_000) - XCTAssertEqual(lsp, 50000) - return 101_500 - } - XCTAssertEqual(vm.uiState.lspBalanceSat, 50000) - XCTAssertEqual(vm.uiState.feeSat, 101_500) - XCTAssertFalse(vm.uiState.isAdvanced) - XCTAssertNil(vm.uiState.order) - } - - @MainActor - func testConfirmationReusesTheOrderUntilANewQuoteStarts() async throws { - let vm = makeViewModel() - vm.onEstimateReady(clientBalance: 100_000, lspBalance: 50000, feeSat: 101_000) - var calls = 0 - let create: (UInt64, UInt64) async throws -> IBtOrder = { client, lsp in - calls += 1 - return self.makeOrder(id: "order-\(calls)", clientBalanceSat: client, lspBalanceSat: lsp, feeSat: client + 1000) - } - let firstValue = try await vm.orderForConfirmation(createOrder: create) - let first = try XCTUnwrap(firstValue) - let retryValue = try await vm.orderForConfirmation(createOrder: create) - let retry = try XCTUnwrap(retryValue) - XCTAssertEqual(first.id, retry.id) - XCTAssertEqual(calls, 1) - - vm.onEstimateReady(clientBalance: 100_000, lspBalance: 50000, feeSat: 101_000) - XCTAssertNil(vm.uiState.order) - let refreshedValue = try await vm.orderForConfirmation(createOrder: create) - let refreshed = try XCTUnwrap(refreshedValue) - XCTAssertNotEqual(refreshed.id, first.id) - XCTAssertEqual(calls, 2) - } - - @MainActor - func testHardwareConfirmationStopsWhenTheCreatedOrderCostsMore() async throws { - let vm = makeViewModel() - vm.onEstimateReady(clientBalance: 100_000, lspBalance: 50000, feeSat: 101_000) - let createdOrder = makeOrder(id: "created", clientBalanceSat: 100_000, lspBalanceSat: 50000, feeSat: 102_000) - var calls = 0 - - let first = try await vm.orderForConfirmation { _, _ in - calls += 1 - return createdOrder - } - - XCTAssertNil(first) - XCTAssertEqual(vm.uiState.order?.id, createdOrder.id) - XCTAssertEqual(vm.uiState.feeSat, createdOrder.feeSat) - - let confirmed = try await vm.orderForConfirmation { _, _ in - calls += 1 - return createdOrder - } - - XCTAssertEqual(confirmed?.id, createdOrder.id) - XCTAssertEqual(calls, 1) - } - - @MainActor - func testSwipeReportsAServiceIncreaseWhenTheCreatedOrderCostsMore() async throws { - let vm = makeViewModel() - vm.onEstimateReady(clientBalance: 100_000, lspBalance: 50000, feeSat: 101_000) - let createdOrder = makeOrder(id: "created", clientBalanceSat: 100_000, lspBalanceSat: 50000, feeSat: 102_000) - var calls = 0 - let create: (UInt64, UInt64) async throws -> IBtOrder = { _, _ in - calls += 1 - return createdOrder - } - let displayed = SpendingConfirmAmounts(networkFeeSat: 500, totalSat: 101_500) - let rebuilt = SpendingConfirmAmounts(networkFeeSat: 500, totalSat: 102_500) - - let first = try await vm.orderForSwipe(createOrder: create) - XCTAssertEqual(first.id, createdOrder.id) - XCTAssertEqual(vm.uiState.feeSat, 101_000) - XCTAssertEqual(vm.feeIncrease(order: first, displayedOrderFeeSat: 101_000, displayed: displayed, rebuilt: rebuilt), .service(amountSat: 1000)) - XCTAssertEqual(vm.uiState.feeSat, createdOrder.feeSat) - - let second = try await vm.orderForSwipe(createOrder: create) - XCTAssertEqual(second.id, createdOrder.id) - XCTAssertEqual(calls, 1) - XCTAssertNil(vm.feeIncrease(order: second, displayedOrderFeeSat: vm.uiState.feeSat, displayed: rebuilt, rebuilt: rebuilt)) - } - - @MainActor - func testUnfundableRebuildReportsTheServiceIncreaseAndMovesToTheOrderFee() { - let vm = makeViewModel() - vm.onEstimateReady(clientBalance: 100_000, lspBalance: 50000, feeSat: 101_000) - let order = makeOrder(id: "created", clientBalanceSat: 100_000, lspBalanceSat: 50000, feeSat: 102_500) - - XCTAssertEqual(vm.unfundableFeeIncrease(order: order, displayedOrderFeeSat: 101_000), .service(amountSat: 1500)) - XCTAssertEqual(vm.uiState.feeSat, 102_500) - } - - @MainActor - func testUnfundableRebuildReportsNothingWhenTheOrderFeeIsNotHigher() { - let vm = makeViewModel() - vm.onEstimateReady(clientBalance: 100_000, lspBalance: 50000, feeSat: 101_000) - let equal = makeOrder(id: "equal", clientBalanceSat: 100_000, lspBalanceSat: 50000, feeSat: 101_000) - let lower = makeOrder(id: "lower", clientBalanceSat: 100_000, lspBalanceSat: 50000, feeSat: 100_000) - - XCTAssertNil(vm.unfundableFeeIncrease(order: equal, displayedOrderFeeSat: 101_000)) - XCTAssertNil(vm.unfundableFeeIncrease(order: lower, displayedOrderFeeSat: 101_000)) - XCTAssertEqual(vm.uiState.feeSat, 101_000) - } - - @MainActor - func testSwipeReportsANetworkIncreaseThenPaysOnTheNextSwipe() async throws { - let vm = makeViewModel() - vm.onEstimateReady(clientBalance: 100_000, lspBalance: 50000, feeSat: 101_000) - let createdOrder = makeOrder(id: "created", clientBalanceSat: 100_000, lspBalanceSat: 50000, feeSat: 101_000) - var calls = 0 - let create: (UInt64, UInt64) async throws -> IBtOrder = { _, _ in - calls += 1 - return createdOrder - } - let displayed = SpendingConfirmAmounts(networkFeeSat: 500, totalSat: 101_500) - let rebuilt = SpendingConfirmAmounts(networkFeeSat: 700, totalSat: 101_700) - - let first = try await vm.orderForSwipe(createOrder: create) - XCTAssertEqual(vm.feeIncrease(order: first, displayedOrderFeeSat: 101_000, displayed: displayed, rebuilt: rebuilt), .network(amountSat: 200)) - XCTAssertEqual(vm.uiState.feeSat, 101_000) - - let second = try await vm.orderForSwipe(createOrder: create) - XCTAssertEqual(second.id, createdOrder.id) - XCTAssertEqual(calls, 1) - XCTAssertNil(vm.feeIncrease(order: second, displayedOrderFeeSat: vm.uiState.feeSat, displayed: rebuilt, rebuilt: rebuilt)) - } - - @MainActor - func testSwipeDecidesFromTheSnapshotTakenBeforeTheOrderIsCreated() async throws { - let vm = makeViewModel() - vm.onEstimateReady(clientBalance: 100_000, lspBalance: 50000, feeSat: 101_000) - let snapshot = SpendingConfirmAmounts(networkFeeSat: 500, totalSat: 101_500) - let snapshotOrderFeeSat = vm.uiState.feeSat - let createdOrder = makeOrder(id: "created", clientBalanceSat: 100_000, lspBalanceSat: 50000, feeSat: 102_000) - let order = try await vm.orderForSwipe { _, _ in createdOrder } - vm.uiState.feeSat = createdOrder.feeSat - let rebuilt = SpendingConfirmAmounts(networkFeeSat: 500, totalSat: 102_500) - - XCTAssertEqual( - vm.feeIncrease(order: order, displayedOrderFeeSat: snapshotOrderFeeSat, displayed: snapshot, rebuilt: rebuilt), - .service(amountSat: 1000) - ) - } - - @MainActor - func testSwipePaysOnTheFirstSwipeWhenTheRebuiltTotalIsLower() async throws { - let vm = makeViewModel() - vm.onEstimateReady(clientBalance: 100_000, lspBalance: 50000, feeSat: 101_000) - let createdOrder = makeOrder(id: "created", clientBalanceSat: 100_000, lspBalanceSat: 50000, feeSat: 101_000) - let order = try await vm.orderForSwipe { _, _ in createdOrder } - let displayed = SpendingConfirmAmounts(networkFeeSat: 500, totalSat: 101_500) - let rebuilt = SpendingConfirmAmounts(networkFeeSat: 300, totalSat: 101_300) - - XCTAssertNil(vm.feeIncrease(order: order, displayedOrderFeeSat: 101_000, displayed: displayed, rebuilt: rebuilt)) - XCTAssertEqual(vm.uiState.feeSat, 101_000) - } - - @MainActor - func testSwipePaysOnTheFirstSwipeWhenTheCreatedOrderCostsLess() async throws { - let vm = makeViewModel() - vm.onEstimateReady(clientBalance: 100_000, lspBalance: 50000, feeSat: 101_000) - let createdOrder = makeOrder(id: "cheaper", clientBalanceSat: 100_000, lspBalanceSat: 50000, feeSat: 100_500) - let order = try await vm.orderForSwipe { _, _ in createdOrder } - let displayed = SpendingConfirmAmounts(networkFeeSat: 500, totalSat: 101_500) - let rebuilt = SpendingConfirmAmounts(networkFeeSat: 500, totalSat: 101_000) - - XCTAssertNil(vm.feeIncrease(order: order, displayedOrderFeeSat: 101_000, displayed: displayed, rebuilt: rebuilt)) - } - - @MainActor - func testSwipePaysOnTheFirstSwipeWhenTheRebuiltFundingMatches() async throws { - let vm = makeViewModel() - vm.onEstimateReady(clientBalance: 100_000, lspBalance: 50000, feeSat: 101_000) - let createdOrder = makeOrder(id: "created", clientBalanceSat: 100_000, lspBalanceSat: 50000, feeSat: 101_000) - let order = try await vm.orderForSwipe { _, _ in createdOrder } - let amounts = SpendingConfirmAmounts(networkFeeSat: 500, totalSat: 101_500) - - XCTAssertNil(vm.feeIncrease(order: order, displayedOrderFeeSat: 101_000, displayed: amounts, rebuilt: amounts)) - XCTAssertEqual(vm.uiState.feeSat, 101_000) - } - - @MainActor - func testConfirmationReplacesAnExpiredRetainedOrder() async throws { - let vm = makeViewModel() - vm.onEstimateReady(clientBalance: 100_000, lspBalance: 50000, feeSat: 101_000) - vm.onOrderCreated( - order: makeOrder( - id: "expired", - clientBalanceSat: 100_000, - lspBalanceSat: 50000, - feeSat: 101_000, - expiresAt: "2000-01-01T00:00:00Z" - ) - ) - let replacement = makeOrder(id: "replacement", clientBalanceSat: 100_000, lspBalanceSat: 50000, feeSat: 101_000) - var calls = 0 - - let confirmed = try await vm.orderForConfirmation { _, _ in - calls += 1 - return replacement - } - - XCTAssertEqual(confirmed?.id, replacement.id) - XCTAssertEqual(vm.uiState.order?.id, replacement.id) - XCTAssertEqual(calls, 1) - } - - @MainActor - func testConfirmationReusesAnUnexpiredOrderWithFractionalSeconds() async throws { - let vm = makeViewModel() - vm.onEstimateReady(clientBalance: 100_000, lspBalance: 50000, feeSat: 101_000) - let retained = makeOrder( - id: "retained", - clientBalanceSat: 100_000, - lspBalanceSat: 50000, - feeSat: 101_000, - expiresAt: "2099-01-01T00:00:00.175Z" - ) - vm.onOrderCreated(order: retained) - var calls = 0 - - let confirmed = try await vm.orderForConfirmation { _, _ in - calls += 1 - return self.makeOrder(id: "replacement", clientBalanceSat: 100_000, lspBalanceSat: 50000, feeSat: 101_000) - } - - XCTAssertEqual(confirmed?.id, retained.id) - XCTAssertEqual(calls, 0) - } - - @MainActor - func testConfirmationBlocksReplacingTheTransfer() { - let vm = makeViewModel() - vm.onEstimateReady(clientBalance: 100_000, lspBalance: 50000, feeSat: 101_000) - vm.uiState.isConfirming = true - vm.onEstimateReady(clientBalance: 200_000, lspBalance: 150_000, feeSat: 202_000) - XCTAssertEqual(vm.uiState.clientBalanceSat, 100_000) - XCTAssertEqual(vm.uiState.lspBalanceSat, 50000) - XCTAssertEqual(vm.uiState.feeSat, 101_000) - } - - @MainActor - func testFailedCreationPreservesTheEstimate() async { - let vm = makeViewModel() - vm.onEstimateReady(clientBalance: 100_000, lspBalance: 50000, feeSat: 101_000) - struct CreateFailed: Error {} - do { - _ = try await vm.orderForConfirmation { _, _ in throw CreateFailed() } - XCTFail("Expected creation to fail") - } catch { - XCTAssertTrue(error is CreateFailed) - } - XCTAssertNil(vm.uiState.order) - XCTAssertEqual(vm.uiState.feeSat, 101_000) - } - - @MainActor - func testSpendingLimitsCapsAtLspMaxClientBalanceWhenOnchainExceedsIt() async { - let viewModel = makeViewModel() - var feeCallBalances: [UInt64] = [] - // The liquidity calc reports no receiving room (maxLspBalance = 0) because the client - // balance saturates the channel — the regression this guards against. - let values = TransferValues( - defaultLspBalance: Self.lspBalance, - minLspBalance: Self.lspBalance, - maxLspBalance: 0, - maxClientBalance: Self.optionMaxClientBalance - ) - - let result = await viewModel.calculateSpendingLimits( - onchainAvailable: Self.onChainBalance, - lspMaxClientBalance: Self.lspMaxClientBalance, - transferValues: { _ in values }, - estimateOrderFee: { clientBalance, _ in - feeCallBalances.append(clientBalance) - return (Self.networkFee, Self.serviceFee) - } - ) - - XCTAssertEqual(result.max, Self.optionMaxClientBalance) - XCTAssertEqual(result.available, result.max) - // The order fee must be estimated against the clamped client balance, not the full balance. - XCTAssertEqual(feeCallBalances.last, Self.lspMaxClientBalance) - } - - @MainActor - func testSpendingLimitsUsesFullBalanceWhenLspInfoUnavailable() async { - let viewModel = makeViewModel() - var feeCallBalances: [UInt64] = [] - let values = TransferValues( - defaultLspBalance: Self.lspBalance, - minLspBalance: Self.lspBalance, - maxLspBalance: 0, - maxClientBalance: Self.optionMaxClientBalance - ) - - let result = await viewModel.calculateSpendingLimits( - onchainAvailable: Self.onChainBalance, - lspMaxClientBalance: nil, - transferValues: { _ in values }, - estimateOrderFee: { clientBalance, _ in - feeCallBalances.append(clientBalance) - return (Self.networkFee, Self.serviceFee) - } - ) - - XCTAssertEqual(result.max, Self.optionMaxClientBalance) - // Without an LSP cap the order fee is estimated against the balance after the LSP fee. - XCTAssertEqual(feeCallBalances.last, Self.onChainBalance - Self.lspFee) - } - - @MainActor - func testSpendingLimitsIsZeroWhenLiquidityReportsZeroClientBalance() async { - let viewModel = makeViewModel() - let values = TransferValues( - defaultLspBalance: Self.lspBalance, - minLspBalance: Self.lspBalance, - maxLspBalance: 0, - maxClientBalance: 0 - ) - - let result = await viewModel.calculateSpendingLimits( - onchainAvailable: Self.onChainBalance, - lspMaxClientBalance: Self.lspMaxClientBalance, - transferValues: { _ in values }, - estimateOrderFee: { _, _ in (Self.networkFee, Self.serviceFee) } - ) - - XCTAssertEqual(result.max, 0) - XCTAssertEqual(result.available, 0) - } - - // MARK: - calculateSpendingLimits affordability (bitkit-android #1179) - - /// Production LSP: the service fee grows with the client balance, so `available - fee` sits above - /// the balance that fee priced. These are the quotes from the reported failure, where the order - /// came to 265,727 against 265,726 available. - @MainActor - func testSpendingMaxIsAffordableWhenTheServiceFeeRisesWithTheClientBalance() async throws { - let viewModel = makeViewModel() - let available: UInt64 = 265_726 - let quotes: [UInt64: UInt64] = [available: 4165, 261_561: 4128] - var feeCalls: [UInt64] = [] - - let result = try await viewModel.calculateSpendingLimits( - onchainAvailable: available, - lspMaxClientBalance: nil, - transferValues: { _ in Self.values(maxClientBalance: Self.optionMaxClientBalance) }, - estimateOrderFee: { clientBalance, _ in - feeCalls.append(clientBalance) - return try (XCTUnwrap(quotes[clientBalance]), 0) - } - ) - - XCTAssertEqual(result.max, 261_561) - // The order the user can build at this max must stay within what they can actually pay. - XCTAssertLessThanOrEqual(result.max + (quotes[result.max] ?? 0), available) - // The old derivation: `available - fee(261_561)`, a balance that quote never priced. - XCTAssertNotEqual(result.max, available - 4128) - // Already affordable, so the common path costs no extra round trip. - XCTAssertEqual(feeCalls.count, 2) - } - - /// Staging/regtest LSP: it charges the LSP side harder than the client side, so the second quote - /// is dearer than the first and no ordering assumption holds. Capping alone would not fix this. - @MainActor - func testSpendingMaxIsAffordableWhenTheServiceFeeFallsWithTheClientBalance() async throws { - let viewModel = makeViewModel() - let available: UInt64 = 266_478 - let quotes: [UInt64: UInt64] = [available: 1798, 264_680: 1800, 264_678: 1801, 264_677: 1801] - var feeCalls: [UInt64] = [] - - let result = try await viewModel.calculateSpendingLimits( - onchainAvailable: available, - lspMaxClientBalance: nil, - transferValues: { _ in Self.values(maxClientBalance: Self.optionMaxClientBalance) }, - estimateOrderFee: { clientBalance, _ in - feeCalls.append(clientBalance) - return try (XCTUnwrap(quotes[clientBalance]), 0) - } - ) - - XCTAssertEqual(result.max, 264_677) - // The settled max funds its own order rather than merely undercutting the first quote. - XCTAssertLessThanOrEqual(result.max + (quotes[result.max] ?? 0), available) - XCTAssertEqual(feeCalls, [available, 264_680, 264_678, 264_677]) - } - - /// Order creation recomputes the LSP balance from the chosen amount, so a re-quote priced against - /// an earlier balance would verify an order that is never created. - @MainActor - func testSpendingMaxRequotePricesTheSplitTheOrderWillUse() async throws { - let viewModel = makeViewModel() - let available: UInt64 = 266_478 - let maxChannel: UInt64 = 1_403_872 - let quotes: [UInt64: UInt64] = [available: 1798, 264_680: 1800, 264_678: 1801, 264_677: 1801] - var feeCalls: [(clientBalance: UInt64, lspBalance: UInt64)] = [] - - _ = try await viewModel.calculateSpendingLimits( - onchainAvailable: available, - lspMaxClientBalance: nil, - transferValues: { clientBalance in - // Each client balance gets its own LSP side, mirroring maxChannelSize - clientBalance. - TransferValues( - defaultLspBalance: maxChannel - clientBalance, - minLspBalance: 50000, - maxLspBalance: maxChannel - clientBalance, - maxClientBalance: Self.optionMaxClientBalance - ) - }, - estimateOrderFee: { clientBalance, lspBalance in - feeCalls.append((clientBalance, lspBalance)) - return try (XCTUnwrap(quotes[clientBalance]), 0) - } - ) - - XCTAssertEqual(feeCalls.count, 4) - for call in feeCalls { - XCTAssertEqual(call.lspBalance, maxChannel - call.clientBalance, "quote for \(call.clientBalance) priced the wrong split") - } - } - - @MainActor - func testSpendingMaxKeepsTheLastCandidateWhenTheRequoteFails() async throws { - let viewModel = makeViewModel() - let available: UInt64 = 266_478 - let quotes: [UInt64: UInt64] = [available: 1798, 264_680: 1800] - - let result = try await viewModel.calculateSpendingLimits( - onchainAvailable: available, - lspMaxClientBalance: nil, - transferValues: { _ in Self.values(maxClientBalance: Self.optionMaxClientBalance) }, - estimateOrderFee: { clientBalance, _ in - guard let fee = quotes[clientBalance] else { throw AppError(message: "lsp unreachable", debugMessage: nil) } - return (fee, 0) - } - ) - - // The step-down candidate is still published rather than the unaffordable quoted balance. - XCTAssertEqual(result.max, 264_678) - } - - @MainActor - func testSpendingMaxFallsBackWhenTheRoundsAreExhausted() async throws { - let viewModel = makeViewModel() - let available: UInt64 = 266_478 - // The fee rises as fast as the balance steps down, so no candidate ever becomes affordable. - let quotes: [UInt64: UInt64] = [available: 1800, 264_678: 2000, 264_478: 2200, 264_278: 2400] - var feeCalls: [UInt64] = [] - - let result = try await viewModel.calculateSpendingLimits( - onchainAvailable: available, - lspMaxClientBalance: nil, - transferValues: { _ in Self.values(maxClientBalance: Self.optionMaxClientBalance) }, - estimateOrderFee: { clientBalance, _ in - feeCalls.append(clientBalance) - return try (XCTUnwrap(quotes[clientBalance]), 0) - } - ) - - // The exhausted loop advertises availableAmount minus the last quote, not the last candidate. - XCTAssertEqual(result.max, available - 2400) - XCTAssertEqual(feeCalls.count, 4) - } - - // MARK: - Advanced receiving capacity (bitkit-android #1180) - - @MainActor - func testAdvancedCapacityKeepsTheLspMaxWhenTheBudgetCoversIt() async { - let viewModel = makeViewModel() - var quoteCount = 0 - - let settled = await viewModel.settleAdvancedLspBalance( - clientBalance: Self.advancedClientBalance, - budget: Self.advancedBudget, - minLspBalance: 50000, - maxLspBalance: 400_000, - estimateOrderFee: { _, lspBalance in - quoteCount += 1 - return (Self.capacityPricedFee(lspBalance), 0) - } - ) - - XCTAssertEqual(settled, 400_000) - XCTAssertEqual(quoteCount, 1) - } - - @MainActor - func testAdvancedCapacitySettlesBelowTheLspMaxWhenTheFeeOutgrowsTheBudget() async throws { - let viewModel = makeViewModel() - var quotedCapacities: [UInt64] = [] - - let resolved = await viewModel.settleAdvancedLspBalance( - clientBalance: Self.advancedClientBalance, - budget: Self.advancedBudget, - minLspBalance: 50000, - maxLspBalance: 2_000_000, - estimateOrderFee: { _, lspBalance in - quotedCapacities.append(lspBalance) - return (Self.capacityPricedFee(lspBalance), 0) - } - ) - let settled = try XCTUnwrap(resolved) - - // The fee is 1_000 + 1% of the capacity, and the budget leaves 10_000 over the client balance. - XCTAssertEqual(settled, 900_000) - XCTAssertLessThanOrEqual(Self.capacityPricedFee(settled), Self.advancedHeadroom) - XCTAssertTrue(quotedCapacities.contains(settled), "the offered max must itself have been priced") - } - - @MainActor - func testAdvancedCapacityIsNilWhenEvenTheMinimumIsUnaffordable() async { - let viewModel = makeViewModel() - - let settled = await viewModel.settleAdvancedLspBalance( - clientBalance: Self.advancedClientBalance, - budget: Self.advancedClientBalance + 500, - minLspBalance: 50000, - maxLspBalance: 2_000_000, - estimateOrderFee: { _, lspBalance in (Self.capacityPricedFee(lspBalance), 0) } - ) - - // Rejection is left to the confirm step rather than offering a range with nothing valid in it. - XCTAssertNil(settled) - } - - @MainActor - func testAdvancedCapacityAdvertisesTheLspMaxWhenTheQuoteIsUnavailable() async { - let viewModel = makeViewModel() - - let settled = await viewModel.settleAdvancedLspBalance( - clientBalance: Self.advancedClientBalance, - budget: Self.advancedBudget, - minLspBalance: 50000, - maxLspBalance: 2_000_000, - estimateOrderFee: { _, _ in throw AppError(message: "lsp unreachable", debugMessage: nil) } - ) - - XCTAssertEqual(settled, 2_000_000) - } - - @MainActor - func testAdvancedCapacityStopsAtTheLastAffordableCapacityWhenARequoteFails() async { - let viewModel = makeViewModel() - - let settled = await viewModel.settleAdvancedLspBalance( - clientBalance: Self.advancedClientBalance, - budget: Self.advancedBudget, - minLspBalance: 50000, - maxLspBalance: 2_000_000, - estimateOrderFee: { _, lspBalance in - // Only the two bracketing quotes succeed; every interpolated candidate fails. - guard lspBalance == 50000 || lspBalance == 2_000_000 else { - throw AppError(message: "lsp unreachable", debugMessage: nil) - } - return (Self.capacityPricedFee(lspBalance), 0) - } - ) - - XCTAssertEqual(settled, 50000) - } - - /// A concave fee curve makes the interpolation overshoot, so the candidate becomes the new - /// ceiling instead of being advertised. Whatever comes back must still be affordable. - @MainActor - func testAdvancedCapacityNeverAdvertisesAnOverBudgetCandidate() async throws { - let viewModel = makeViewModel() - // Steep to 200k, then near-flat — the linear guess between the two ends underestimates the fee. - let fee: (UInt64) -> UInt64 = { 1000 + min($0, 200_000) / 20 + $0.saturatingSub(200_000) / 1000 } - var quotedCapacities: [UInt64] = [] - - let resolved = await viewModel.settleAdvancedLspBalance( - clientBalance: Self.advancedClientBalance, - budget: Self.advancedBudget, - minLspBalance: 50000, - maxLspBalance: 1_000_000, - estimateOrderFee: { _, lspBalance in - quotedCapacities.append(lspBalance) - return (fee(lspBalance), 0) - } - ) - let settled = try XCTUnwrap(resolved) - - XCTAssertLessThanOrEqual(fee(settled), Self.advancedHeadroom) - XCTAssertLessThan(settled, 1_000_000) - // Candidates that priced over the headroom were rejected, not returned. - XCTAssertTrue(quotedCapacities.contains { fee($0) > Self.advancedHeadroom && $0 != 1_000_000 }) - } - - @MainActor - func testUpdateAdvancedTransferValuesSettlesTheMaxAndClearsTheFlag() async { - let viewModel = makeViewModel() - let values = TransferValues( - defaultLspBalance: 1_500_000, - minLspBalance: 50000, - maxLspBalance: 2_000_000, - maxClientBalance: Self.optionMaxClientBalance - ) - - await viewModel.updateAdvancedTransferValues( - clientBalanceSat: Self.advancedClientBalance, - budget: { Self.advancedBudget }, - transferValues: { _ in values }, - estimateOrderFee: { _, lspBalance in (Self.capacityPricedFee(lspBalance), 0) } - ) - - XCTAssertEqual(viewModel.transferValues.maxLspBalance, 900_000) - // Default must not hand back a capacity the settled max just excluded. - XCTAssertEqual(viewModel.transferValues.defaultLspBalance, 900_000) - XCTAssertFalse(viewModel.isSettlingAdvancedCapacity) - } - - @MainActor - func testUpdateAdvancedTransferValuesLeavesAnAffordableMaxUntouched() async { - let viewModel = makeViewModel() - let values = TransferValues( - defaultLspBalance: 100_000, - minLspBalance: 50000, - maxLspBalance: 400_000, - maxClientBalance: Self.optionMaxClientBalance - ) - - await viewModel.updateAdvancedTransferValues( - clientBalanceSat: Self.advancedClientBalance, - budget: { Self.advancedBudget }, - transferValues: { _ in values }, - estimateOrderFee: { _, lspBalance in (Self.capacityPricedFee(lspBalance), 0) } - ) - - XCTAssertEqual(viewModel.transferValues.maxLspBalance, 400_000) - XCTAssertEqual(viewModel.transferValues.defaultLspBalance, 100_000) - } - - /// Reading the budget is itself a round trip. Taking it as a value would resolve it before the - /// call, leaving the number pad live against the previous screen's cap — or none at all on a - /// first entry, where `transferValues` is still zeroed. - @MainActor - func testUpdateAdvancedTransferValuesHoldsTheFlagWhileTheBudgetIsRead() async { - let viewModel = makeViewModel() - let values = TransferValues( - defaultLspBalance: 1_500_000, - minLspBalance: 50000, - maxLspBalance: 2_000_000, - maxClientBalance: Self.optionMaxClientBalance - ) - var settlingWhileBudgetRead: Bool? - var maxWhileBudgetRead: UInt64? - - await viewModel.updateAdvancedTransferValues( - clientBalanceSat: Self.advancedClientBalance, - budget: { - settlingWhileBudgetRead = viewModel.isSettlingAdvancedCapacity - maxWhileBudgetRead = viewModel.transferValues.maxLspBalance - return Self.advancedBudget - }, - transferValues: { _ in values }, - estimateOrderFee: { _, lspBalance in (Self.capacityPricedFee(lspBalance), 0) } - ) - - XCTAssertEqual(settlingWhileBudgetRead, true) - // The advertised max is published first, so an early Min/Default/Max tap has a cap to land on. - XCTAssertEqual(maxWhileBudgetRead, 2_000_000) - XCTAssertFalse(viewModel.isSettlingAdvancedCapacity) - } - - /// No range to settle means no reason to pay for the budget round trip. - @MainActor - func testUpdateAdvancedTransferValuesSkipsTheBudgetReadWithoutARange() async { - let viewModel = makeViewModel() - let values = TransferValues( - defaultLspBalance: 50000, - minLspBalance: 50000, - maxLspBalance: 50000, - maxClientBalance: Self.optionMaxClientBalance - ) - var budgetReads = 0 - - await viewModel.updateAdvancedTransferValues( - clientBalanceSat: Self.advancedClientBalance, - budget: { - budgetReads += 1 - return Self.advancedBudget - }, - transferValues: { _ in values }, - estimateOrderFee: { _, lspBalance in (Self.capacityPricedFee(lspBalance), 0) } - ) - - XCTAssertEqual(budgetReads, 0) - XCTAssertEqual(viewModel.transferValues.maxLspBalance, 50000) - } - - // MARK: - Funding guards - - @MainActor - func testCanFundOrderRejectsAnAmountOverTheBudget() async { - let viewModel = makeViewModel() - - let canFund = await viewModel.canFundOrder( - clientBalance: 260_000, - budget: 265_000, - transferValues: { _ in Self.values(maxClientBalance: Self.optionMaxClientBalance) }, - estimateOrderFee: { _, _ in (6000, 0) } // 260_000 + 6_000 is over the budget - ) - - XCTAssertFalse(canFund) - } - - @MainActor - func testCanFundOrderAcceptsAnAmountThatFitsTheBudget() async { - let viewModel = makeViewModel() - - let canFund = await viewModel.canFundOrder( - clientBalance: 260_000, - budget: 265_000, - transferValues: { _ in Self.values(maxClientBalance: Self.optionMaxClientBalance) }, - estimateOrderFee: { _, _ in (1000, 0) } - ) - - XCTAssertTrue(canFund) - } - - @MainActor - func testCanFundOrderDoesNotBlockWhenTheBudgetIsUnknown() async { - let viewModel = makeViewModel() - - let canFund = await viewModel.canFundOrder( - clientBalance: 260_000, - budget: nil, - transferValues: { _ in Self.values(maxClientBalance: Self.optionMaxClientBalance) }, - estimateOrderFee: { _, _ in (6000, 0) } - ) - - // An unreadable balance must not block the flow; confirm stays the authority. - XCTAssertTrue(canFund) - } - - @MainActor - func testCanFundOrderDoesNotBlockWhenTheQuoteFails() async { - let viewModel = makeViewModel() - - let canFund = await viewModel.canFundOrder( - clientBalance: 260_000, - budget: 265_000, - transferValues: { _ in Self.values(maxClientBalance: Self.optionMaxClientBalance) }, - estimateOrderFee: { _, _ in throw AppError(message: "lsp unreachable", debugMessage: nil) } - ) - - // A quote the LSP will not give must not block the user; confirm stays the authority. - XCTAssertTrue(canFund) - } - - @MainActor - func testCanFundAdvancedOrderRejectsACapacityOverTheBudget() async { - let viewModel = makeViewModel() - - let canFund = await viewModel.canFundAdvancedOrder( - clientBalance: 260_000, - receivingAmount: 900_000, - budget: 265_000, - estimateOrderFee: { _, _ in (6000, 0) } - ) - - XCTAssertFalse(canFund) - } - - @MainActor - func testCanFundAdvancedOrderDoesNotBlockWhenTheBudgetIsUnknownOrUnquoted() async { - let viewModel = makeViewModel() - - let unsizedBudget = await viewModel.canFundAdvancedOrder( - clientBalance: 260_000, - receivingAmount: 900_000, - budget: nil, - estimateOrderFee: { _, _ in (6000, 0) } - ) - let failedQuote = await viewModel.canFundAdvancedOrder( - clientBalance: 260_000, - receivingAmount: 900_000, - budget: 265_000, - estimateOrderFee: { _, _ in throw AppError(message: "lsp unreachable", debugMessage: nil) } - ) - - XCTAssertTrue(unsizedBudget) - XCTAssertTrue(failedQuote) - } - - @MainActor - func testHwFundingBudgetIsNilWithoutDeviceCapabilities() async { - let viewModel = makeViewModel() - - // No hardware capabilities injected, so the funding guards degrade to non-blocking. - let budget = await viewModel.hwFundingBudget(walletId: "wallet-1") - - XCTAssertNil(budget) - } - - private static let advancedClientBalance: UInt64 = 100_000 - private static let advancedBudget: UInt64 = 110_000 - private static let advancedHeadroom: UInt64 = advancedBudget - advancedClientBalance - - /// Prices an order at a flat 1_000 plus 1% of the receiving capacity, as the LSP charges both sides. - private static func capacityPricedFee(_ lspBalance: UInt64) -> UInt64 { - 1000 + lspBalance / 100 - } - - private static func values(maxClientBalance: UInt64) -> TransferValues { - TransferValues( - defaultLspBalance: lspBalance, - minLspBalance: lspBalance, - maxLspBalance: 0, - maxClientBalance: maxClientBalance - ) - } - - private static let onChainBalance: UInt64 = 10_000_000 - private static let lspMaxClientBalance: UInt64 = 1_766_193 - private static let optionMaxClientBalance: UInt64 = 1_687_598 - private static let lspBalance: UInt64 = 252_368 - private static let networkFee: UInt64 = 2112 - private static let serviceFee: UInt64 = 286 - private static let lspFee: UInt64 = 2398 - - private func makeOrder( - id: String, - clientBalanceSat: UInt64, - lspBalanceSat: UInt64, - feeSat: UInt64 = 1000, - expiresAt: String = "2099-01-01T00:00:00Z" - ) -> IBtOrder { - IBtOrder( - id: id, - state: .created, - state2: .created, - feeSat: feeSat, - networkFeeSat: 2483, - serviceFeeSat: 1520, - lspBalanceSat: lspBalanceSat, - clientBalanceSat: clientBalanceSat, - zeroConf: false, - zeroReserve: false, - clientNodeId: "node123", - channelExpiryWeeks: 52, - channelExpiresAt: "2025-03-14T10:30:00Z", - orderExpiresAt: expiresAt, - channel: nil, - lspNode: .init(alias: "", pubkey: "", connectionStrings: [], readonly: nil), - lnurl: nil, - payment: IBtPayment( - state: .created, - state2: .created, - paidSat: 0, - bolt11Invoice: IBtBolt11Invoice( - request: "lnbc...", - state: .pending, - expiresAt: "2024-03-21T15:45:00Z", - updatedAt: "2024-03-14T08:20:00Z" - ), - onchain: IBtOnchainTransactions( - address: "bc1q...", - confirmedSat: 0, - requiredConfirmations: 3, - transactions: [] - ), - isManuallyPaid: nil, - manualRefunds: nil - ), - couponCode: nil, - source: nil, - discount: nil, - updatedAt: "2024-03-14T08:20:00Z", - createdAt: "2024-03-14T08:15:00Z" - ) - } -} diff --git a/Docs/AI_DEVICE_TESTS.md b/Docs/AI_DEVICE_TESTS.md index 4b6fa7974..6992f68dd 100644 --- a/Docs/AI_DEVICE_TESTS.md +++ b/Docs/AI_DEVICE_TESTS.md @@ -20,7 +20,7 @@ TEST_TREZOR_RESET_STATE=1 \ TREZOR_BRIDGE=true \ TREZOR_BRIDGE_URL=http://127.0.0.1:21325 \ TREZOR_ELECTRUM_URL=tcp://127.0.0.1:60001 \ -E2E=true E2E_BACKEND=local E2E_NETWORK=regtest GEO=false \ +E2E=true E2E_BACKEND=local E2E_NETWORK=regtest \ xcodebuild test \ -workspace Bitkit.xcodeproj/project.xcworkspace \ -scheme BitkitAITests \ diff --git a/Docs/receive-liquidity.md b/Docs/receive-liquidity.md index 0dbe905f2..71825e46e 100644 --- a/Docs/receive-liquidity.md +++ b/Docs/receive-liquidity.md @@ -1,6 +1,6 @@ # Receive Liquidity Behavior -This document describes how the receive flow decides whether to show a normal Lightning invoice or route the user into CJIT liquidity setup. +This document describes how the receive flow decides whether to show a normal Lightning invoice or point the user to Manual Setup. Blocktank no longer sells Lightning channels, so Bitkit creates no CJIT invoice or channel order: when Spending needs more inbound liquidity than its channels have, the flow shows the Transfer Unavailable screen, whose Continue opens Manual Setup. ## Cases @@ -8,13 +8,12 @@ This document describes how the receive flow decides whether to show a normal Li - A new Receive sheet session starts from a fresh tab state. - If Auto is available, the default tab is Auto. - If Auto is unavailable, the default tab is Savings. - - Temporary receive-session state, such as selected tab, nested navigation, pending CJIT details, and CJIT invoice QR state, must not survive closing and reopening the Receive sheet. + - Temporary receive-session state, such as selected tab and nested navigation, must not survive closing and reopening the Receive sheet. - Editing from Savings or Auto: - Editing sets the amount for the receive request. - If the edited amount can be received over Lightning, the regenerated Spending invoice also includes that amount. - - If the edited amount cannot be received over Lightning, Auto falls back to the Savings tab and shows the onchain QR instead of routing to CJIT. - - The edit flow does not create CJIT or route to CJIT amount entry. + - If the edited amount cannot be received over Lightning, Auto falls back to the Savings tab and shows the onchain QR. - Editing from a hardware receive tab: - Editing sets the amount for the hardware/onchain receive request. @@ -24,10 +23,9 @@ This document describes how the receive flow decides whether to show a normal Li - Lightning receive unavailable because there is no ready channel or ready inbound liquidity is `0`: - No Lightning invoice is created. - The normal QR remains Savings/onchain only. - - The Spending tab shows CJIT onboarding. - - Tapping receive spending routes to CJIT amount entry, or the CJIT geo-block screen when geo-blocked. - - Editing from Savings or Auto updates the receive amount and returns to the normal QR; it does not create or route to CJIT. - - When a channel already exists, later CJIT confirmation and learn-more screens use additional-liquidity copy. + - The Spending tab shows the receive-on-spending onboarding. + - Tapping receive spending opens the Transfer Unavailable screen. + - Editing from Savings or Auto updates the receive amount and returns to the normal QR. - Ready channel, inbound liquidity greater than `0`, zero/variable amount: - A Lightning invoice is allowed. @@ -38,29 +36,13 @@ This document describes how the receive flow decides whether to show a normal Li - The unified QR includes Lightning. - The Spending tab shows the normal Lightning invoice. -- Ready channel, fixed amount greater than inbound liquidity but below CJIT minimum: +- Ready channel, fixed amount greater than inbound liquidity: - A normal Lightning invoice is not shown. - - Editing from Spending routes to CJIT amount entry. - - The user must choose at least the minimum CJIT amount. + - Editing from Spending opens the Transfer Unavailable screen. - Editing from Savings or Auto returns to the normal QR with Savings/onchain only. -- Ready channel, fixed amount greater than inbound liquidity and at or above CJIT minimum: - - If editing from Spending and the amount can be backed by a CJIT channel without exceeding Blocktank's maximum channel size, the edit flow creates additional CJIT. - - The user gets CJIT confirmation and then a CJIT Lightning invoice QR. - - The CJIT Lightning invoice is an invoice to the LSP and must be shown as Spending-only, not as Auto/unified receive. - - Editing from a CJIT Lightning invoice QR must replace the stale CJIT QR route before showing the updated receive result, because the previous LSP invoice is immutable. - - The direct additional CJIT path must not regenerate the normal receive invoice before creating CJIT. - - If editing from Spending and the amount is too large for CJIT, or the maximum cannot be calculated, the edit flow routes to CJIT amount entry. - - The CJIT amount screen enforces the real maximum receivable amount, calculated from `invoiceSat + defaultLspBalance(invoiceSat) <= maxChannelSizeSat`. - - If Blocktank rejects additional CJIT because the node is already at its total capacity limit, the app explains that additional spending capacity is unavailable instead of showing the per-channel maximum. - - Editing from Savings or Auto returns to the normal QR with Savings/onchain only. - -- Geo-blocked and liquidity is needed: - - The flow routes to the CJIT geo-block screen. - - No CJIT invoice is created. - ## Invariants - Auto tab availability and default tab selection are based on whether a normal Lightning invoice can be created for the current receive amount. - Ready channels alone do not imply Auto availability; fixed receive amounts must also fit within ready inbound liquidity. -- CJIT min and max limits are only needed when a Spending-origin edit needs additional inbound liquidity and the user is not geo-blocked. +- The receive flow makes no Blocktank request to decide any of the above. diff --git a/changelog.d/next/1127.changed.md b/changelog.d/next/1127.changed.md new file mode 100644 index 000000000..609f516a9 --- /dev/null +++ b/changelog.d/next/1127.changed.md @@ -0,0 +1 @@ +Transfers to spending now point to Manual Setup, since Blocktank no longer sells Lightning channels, and a forced close of a Blocktank connection no longer leaves funds stuck. diff --git a/journeys/README.md b/journeys/README.md index 32d16155e..28ad469a5 100644 --- a/journeys/README.md +++ b/journeys/README.md @@ -24,8 +24,8 @@ separately confirmed the app is wrong. What this proves, and the preconditions needed to prove it. - Tap the Spending balance card on the home screen - Verify the spending amount screen (id "SpendingAmount") is visible + Tap the Savings balance card on the home screen + Verify the savings wallet screen (id "TransferToSpending") is visible ``` @@ -48,8 +48,8 @@ xcodebuildmcp simulator build-and-run # build, install, launch, xcodebuildmcp simulator snapshot-ui # semantic snapshot with elementRef targets xcodebuildmcp ui-automation tap --element-ref e12 # tap one ref from the latest snapshot xcodebuildmcp ui-automation type-text --element-ref e8 --text "..." -xcodebuildmcp ui-automation wait-for-ui --predicate textContains --text "Spending Balance Maximum" -xcodebuildmcp ui-automation wait-for-ui --identifier SpendingAmount --predicate exists +xcodebuildmcp ui-automation wait-for-ui --predicate textContains --text "Manual Setup Required" +xcodebuildmcp ui-automation wait-for-ui --identifier TransferUnavailable --predicate exists xcodebuildmcp simulator screenshot # only when a snapshot cannot settle a question ``` @@ -182,13 +182,14 @@ Known naming differences: | Pubky Ring choice row | `PubkyChoiceIdentity` (the same tag on every row; tell rows apart by their key caption) | `PubkyChoiceRing_` (bare z32 key) | | Pubky Ring choice row lookup spinner | `PubkyChoiceIdentityLookup` (the same tag on every row) | `PubkyChoiceRingLookup_` (bare z32 key) | | Contact import overview profile and friend count | — *(no tag; read the "Import" title and the "N friends" text)* | `ContactImportOverviewProfile`, `ContactImportOverviewSummary` | +| Lightning Connections add connection | `add_connection_button` | `NavigationAction` | `SubscriptionRow-` matches Android exactly. `PaymentRequestRow` keeps that prefix but appends the billing period (`-one-time` for a one-off), because every recurring payment of one subscription carries the same `paymentRequestId` and they can list together — Android stops at the id and lets those rows collide. Match on the prefix when a journey needs to work on both platforms. -Everything else — `N0`–`N9`, `N000`, `NDecimal`, `NRemove`, `SpendingAmount*`, `SpendingAdvanced*`, +Everything else — `N0`–`N9`, `N000`, `NDecimal`, `NRemove`, `TransferUnavailable*`, `Fund*`, `External*`, `Hardware*`, `Widget*` — matches Android exactly. ## Capabilities @@ -206,10 +207,9 @@ journey PR, which is what made this file conflict on every merge. | USDT wallet navigation without funding | an onboarded build with USDT configured; wallet syncing remains silent while offline; payment-action errors remain available — [USDT QA](../Docs/USDT-QA.md) | | Controlled USDT quotes, provider history, funded submission and recovery | an explicitly configured local USDT gateway/RPC/bundler fixture with owned test accounts and funds; absent from the standard journey environment. Report these steps as blocked unless that setup and transfer authorization are supplied — [USDT QA](../Docs/USDT-QA.md) | | On-chain funds and blocks on regtest | `../bitkit-android/lsp` deposit and mine, borrowed from the sibling Android checkout until #694 lands an iOS copy — [Backend preconditions](#backend-preconditions) | -| Lightning channels, CJIT orders and quoted maxima | the LSP the `E2E_BUILD` app targets, plus its node as an external LN peer — [Backend preconditions](#backend-preconditions), [amount-limits](amount-limits/README.md) | +| Lightning channels | a channel opened through Manual Setup to the node of the LSP the `E2E_BUILD` app targets, as an external LN peer — [Backend preconditions](#backend-preconditions) | | A hardware wallet to pair, watch and sign with | the deterministic Trezor emulator from `bitkit-docker`, reached through Trezor Bridge on the host; the simulator has no Bluetooth LE and iOS cannot do WebUSB, so BLE pairing and USB are not covered — [hardware-wallet](hardware-wallet/README.md) | -| Push notifications to a backgrounded or killed app | a real APNs round trip on an attached physical device, never the simulator, read back from Notification Center — [cjit-notifications](cjit-notifications/README.md) | -| The OS notification-permission dialog | the one-shot `UNUserNotificationCenter` alert, reset with `xcrun simctl uninstall to.bitkit` and a rebuild — [notification-permission](notification-permission/README.md) | +| The OS notification-permission dialog | the one-shot `UNUserNotificationCenter` alert, reset with `xcrun simctl uninstall to.bitkit` and a rebuild | | An incoming Payment Request from a linked issuer | the fixture issuer, saved as a contact and linked as identities — [payment-requests](payment-requests/README.md) | | A Pubky identity and a two-wallet marketplace purchase | a Bitkit-generated Pubky profile, plus the integration fixture runtime: Pubky testnet, Paykit Server, regtest bitcoind and Fulcrum — [pubky-auth](pubky-auth/README.md), [pubky-marketplace](pubky-marketplace/README.md) | | LNURL pay, withdraw, channel and auth, and Lightning Addresses | the `bitkit-docker` `lnurl-server` on local regtest, reached by a simulator app built with `E2E_BUILD`, whose default `E2E_BACKEND=local` targets the host at `127.0.0.1`; it issues memo invoices, so a check that needs a description-hash invoice needs another endpoint | @@ -237,9 +237,7 @@ required to run the suite. When you port an Android feature, port its journeys too — see the Journeys section in `AGENTS.md`. -Some suites are adapted rather than ported verbatim: `amount-limits` because iOS snaps to the -spending maximum differently, `cjit-notifications` because the notification copy differs, and -`pubky-marketplace` because the two-wallet payment runs on regtest here. In `pubky-auth`, the +Some suites are adapted rather than ported verbatim: `pubky-marketplace` because the two-wallet payment runs on regtest here. In `pubky-auth`, the watch-only OS handoff journey is iOS-only; `grant-signup.xml` and `grant-signup-existing-identity.xml` are shared with Android. diff --git a/journeys/amount-limits/README.md b/journeys/amount-limits/README.md index d353136ff..ac906a37a 100644 --- a/journeys/amount-limits/README.md +++ b/journeys/amount-limits/README.md @@ -1,11 +1,8 @@ # Amount-limit journeys These journeys exercise the "block number pad input exceeding the max/available amount" behaviour. -The same `AmountInputViewModel` cap + `maxExceededCount` effect path backs all four amount-entry -screens (Send, Transfer→Spending, Receiving capacity, External node). - -A fifth screen shares that path — the hardware Transfer To Spending amount. It needs a paired device, -so its journey lives in [`../hardware-wallet/transfer-to-spending-over-max.xml`](../hardware-wallet/transfer-to-spending-over-max.xml). +The same `AmountInputViewModel` cap + `maxExceededCount` effect path backs the amount-entry screens +for Send and the External node (Manual Setup) channel. `custom-fee-wallet-switch.xml` covers adjacent send preparation: a custom fee switches Spending to Savings, with manual or automatic coin selection after returning through the fee picker. It requires @@ -38,109 +35,37 @@ preparation. Do not submit a payment. then `../bitkit-android/lsp POST /regtest/chain/mine '{"count":3}'`. `mine` prints nothing on success — that is not a failure. There is no iOS copy of that helper yet — see #694 and the top-level README. -2. **Transfer/Spending and Receiving-capacity flows need the node connected to the LSP** so a real - max can be quoted. On the Spending amount screen the max starts at `0` behind a spinner — wait - for it to populate before typing. -3. **The external-node flow needs a reachable Lightning peer.** The staging LSP node works +2. **The external-node flow needs a reachable Lightning peer.** The staging LSP node works (`../bitkit-android/lsp GET /info` for its id/host/port). ## Gotchas -- **The cap can be lower than the visible "Available".** Fee and channel reserves mean e.g. Available - `99 890` but a spending max of `98 064`. Assert "does not exceed the **stated maximum**", not - "Available". +- **The cap can be lower than the visible "Available".** Fee and channel reserves mean the stated + maximum can sit below Available. Assert "does not exceed the **stated maximum**", not "Available". - **Do not assert Continue is disabled when over the max.** The input is *capped* rather than left in an over-max state, so the capped value is valid and Continue stays **enabled**. - Toasts are short-lived (`Toast.visibilityTimeShort`) and will have faded before a snapshot round-trip completes. Assert one with `xcodebuildmcp ui-automation wait-for-ui` on its identifier - (`SendAmountExceededToast`, `SpendingAmountExceededToast`, `SpendingAdvancedExceededToast`, - `ExternalAmountExceededToast`) immediately after the over-max keypress. + (`SendAmountExceededToast`, `ExternalAmountExceededToast`) immediately after the over-max keypress. - Prefer `snapshot-ui` and tap elements by identifier (`N9`, `NRemove`) over screen coordinates. ## Verified behaviour -All three walked on an iPhone 17 simulator against the stag0 regtest backend. **Send caps by -rejecting the keypress while the two transfer screens snap to the maximum, so assert "does not exceed -the stated maximum" — never a specific value:** +Walked on an iPhone 17 simulator against the stag0 regtest backend. **Send caps by rejecting the +keypress, so assert "does not exceed the stated maximum" — never a specific value:** | Screen | Cap | Nine taps on `N9` | After one `NRemove` | | --- | --- | --- | --- | | Send | 297 393 | **99 999** — the largest all-9s value still under the cap | 9 999 | -| Transfer → Spending | 296 522 | **296 522** — clamped to the max exactly | 29 652 | -| Transfer → Spending | 22 016 | **22 016** — same shape, a later run on a wallet with less LSP headroom | 2 201 | -| Receiving capacity | 564 | **564** — clamped to the max exactly | 56 | - -The cap column is whatever that screen enforces, not the wallet balance: for the two transfer rows it -is the settled maximum, and it moves with LSP headroom between runs. Assert the shape, never a value. -The receiving-capacity row is the behaviour PR #686 adds — Android leaves `99` there, because it -rejects the third keypress instead of snapping. - -On Send and Transfer → Spending, Continue stays enabled throughout: the cap is applied to the input -rather than leaving it in an invalid state. On the receiving-capacity screen Continue can still be -disabled at the cap, because it also enforces a minimum — see the `Min` > `Max` note below. The -exceeded toasts fire on the first rejected keypress and are gone well before a snapshot round-trip -returns — race a `wait-for-ui` on the toast identifier against the taps. - -The Transfer → Spending max populates from the on-chain balance and cached Blocktank info, so the -screen is reachable and cappable even with the regtest backend down. That is *not* a substitute for -a real LSP quote: without one the numbers are not the ones a real transfer would use. - -### Two things the walk turned up - -**The settling window is shorter than a snapshot round-trip.** `isSettlingAdvancedCapacity` disables -the number pad and Continue for exactly as long as the budget read and the LSP quotes take, and -against a warm stag0 LSP that is under a second — eight back-to-back snapshots raced against the -Advanced tap all came back with the pad already enabled. The gate is real (`NumberPad(isDisabled:)` -is wired to it, and disabled buttons drop out of the snapshot's Targets list, which is how the -Spending amount screen's own spinner is visible), but do not expect to catch it here. Treat -"already settled" as a pass. - -**`Min` can exceed `Max` once LSP headroom runs out.** On a wallet with two existing Blocktank -channels the walk found `SpendingAdvancedMin` at 77 984 and `SpendingAdvancedMax` at 564 — the -minimum is derived from the client balance, the maximum from what is left of the LSP's channel-size -cap, and nothing reconciles them. Continue then stays disabled at every capacity, so the journey's -tail (Continue → order created) cannot be walked. That is a property of the regtest LSP allocation, -not of this PR; close a channel to free headroom, or accept that the run covers the cap behaviour -only. - -## Transfer maximums are settled before they are offered - -[#686](https://github.com/synonymdev/bitkit-ios/pull/686) (porting -[bitkit-android#1179](https://github.com/synonymdev/bitkit-android/pull/1179) and -[#1180](https://github.com/synonymdev/bitkit-android/pull/1180)) settles both transfer maximums on an -amount the wallet can actually pay the LSP order fee for, rather than deriving one and hoping it -fits. Three behaviours it adds are covered by `transfer-spending-advanced-over-max.xml`: - -- **The number pad and Continue are disabled while the maximum settles.** Settling costs one or two - live fee quotes, so the advanced screen holds `NumberPad(isDisabled:)` until they return. Min, - Default and Max stay tappable — that is the iOS shape; Android disables the amount buttons too. -- **An entered capacity above the settled maximum snaps down to it**, with - `SpendingAdvancedExceededToast` naming the settled value. This is the snap - `SpendingAmount.onMaxExceeded()` already performed, now on the advanced screen as well. -- **Tapping Max before the maximum settles is corrected once it does.** `updateInputCap()` brings the - entered amount down when the settled maximum lands below it, so an early Max does not leave a - capacity selected that no longer exists. - -The journey sizes the transfer at **MAX** rather than 25% for this reason: settling only bites when -the client balance and the receiving capacity together crowd the funding budget, and at 25% the -settled maximum is simply the LSP's advertised maximum. -**Regtest is regression coverage here, not proof.** The staging/regtest service fee *falls* as the -client balance rises, where production's rises, which leaves a vulnerable window roughly 2 satoshis -wide against about 37 in production. Expect the settled maximum to equal the advertised one and the -two "comes down" steps to hold trivially. The unit tests in `BitkitTests/TransferViewModelTests.swift` -are the gate for the fix itself. +On Send, Continue stays enabled throughout: the cap is applied to the input rather than leaving it in +an invalid state. The exceeded toast fires on the first rejected keypress and is gone well before a +snapshot round-trip returns — race a `wait-for-ui` on the toast identifier against the taps. ## Identifiers used - Number pad keys: digits `N0`–`N9`, triple-zero `N000`, decimal `NDecimal`, delete `NRemove`. - Send: screen `SendAmount`, field `SendNumberField`, available `AvailableAmount`, continue `ContinueAmount`; recipient `RecipientManual` / `RecipientInput` / `AddressContinue`; home Send button `Send`. -- Transfer→Spending: screen `SpendingAmount`, field `SpendingAmountNumberField`, - available `SpendingAmountAvailable`, 25% `SpendingAmountQuarter`, max `SpendingAmountMax`, - continue `SpendingAmountContinue`. -- Receiving capacity: screen `SpendingAdvanced`, field `SpendingAdvancedNumberField`, - min/default/max `SpendingAdvancedMin`/`SpendingAdvancedDefault`/`SpendingAdvancedMax`, - continue `SpendingAdvancedContinue`. - External: funding `FundManual`; connection `NodeIdInput`/`HostInput`/`PortInput`/`ExternalContinue`; amount screen `ExternalAmount`, field `ExternalAmountNumberField`, available `ExternalAmountAvailable`, 25% `ExternalAmountQuarter`, max `ExternalAmountMax`, diff --git a/journeys/amount-limits/transfer-spending-advanced-over-max.xml b/journeys/amount-limits/transfer-spending-advanced-over-max.xml deleted file mode 100644 index 6fde01bec..000000000 --- a/journeys/amount-limits/transfer-spending-advanced-over-max.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - Verifies the receiving capacity (advanced) screen settles its maximum on a capacity the wallet - can pay the order fee for before offering it, holds the number pad disabled while that settles, - and snaps an entered capacity above the settled maximum down to it with the "Receiving Capacity - Maximum" warning toast — while still allowing deleting digits at the cap. - - Precondition: onboarded dev wallet with a POSITIVE on-chain Savings balance and a running node - connected to the LSP. Start on the wallet home screen. The advanced screen is reached by first - setting a spending amount and continuing to the confirm screen. - - Adapted from Android on purpose, for the behaviour PR #686 ports from bitkit-android#1179 and - bitkit-android#1180. Three deviations, all deliberate: - * The transfer is sized at MAX rather than 25%. Settling only bites when the receiving capacity - and the client balance together crowd the funding budget; at 25% there is enough headroom that - the settled maximum is the LSP's advertised maximum and every "comes down" step is a no-op. - * iOS snaps the entered capacity down to the settled maximum, the way `SpendingAmount` already - does; Android rejects the keypress and leaves the display at the largest all-9s value still - under the cap. Both satisfy "does not exceed the maximum". - * iOS disables the number pad and Continue while the maximum settles, but leaves Min, Default - and Max tappable — tapping Max early is corrected once the maximum settles. Android disables - the amount buttons too. - - Walked on an iPhone 17 simulator against the stag0 regtest LSP, where two findings shaped the - steps below. The settle finished faster than a single snapshot round-trip, so the disabled number - pad was never caught — the step is written to accept "already settled". And with the LSP headroom - already consumed by two channels, the offered minimum (77 984) sat above the offered maximum - (564), which leaves Continue disabled at every capacity: the cap steps still hold, the - order-creation tail cannot be walked. Both are properties of the regtest allocation rather than - of the app; `README.md` has the detail. Regtest is regression coverage for this journey, not - proof of the fix — the unit tests are that. - - - Tap the Savings balance card on the home screen (id "ActivitySavings") - Tap "Transfer To Spending" (id "TransferToSpending") - If a transfer intro screen appears, tap "Get Started" - Wait until the spending amount screen (id "SpendingAmount") has loaded a positive maximum (id "SpendingAmountAvailable") - Tap "MAX" (id "SpendingAmountMax") to size the transfer at the ceiling, where the least headroom is left for the receiving capacity - Tap Continue (id "SpendingAmountContinue") - On the confirm screen, tap "Advanced" (id "SpendingConfirmAdvanced") - Verify the receiving capacity screen (id "SpendingAdvanced") is visible - Verify the number pad and Continue are disabled while the maximum settles — a disabled button drops out of the snapshot's Targets list, so "N9" and "SpendingAdvancedContinue" missing from Targets is the signal, not an enabled flag. Against a warm LSP the settle finishes faster than one snapshot round-trip; report "already settled" rather than failing the step - Tap "MAX" (id "SpendingAdvancedMax") — unlike the number pad it stays tappable while the maximum settles - Wait for the number pad to be enabled (id "N9", predicate enabled), then verify the amount in the input field (id "SpendingAdvancedNumberField") does not exceed the maximum receiving capacity now offered - Tap the "9" key (id "N9") nine times to enter a capacity far larger than the maximum allowed - Verify a "Receiving Capacity Maximum" warning toast appears (id "SpendingAdvancedExceededToast") and names the settled maximum - Verify the amount in the input field (id "SpendingAdvancedNumberField") snapped to the maximum receiving capacity exactly, and does not exceed it - Tap the delete key (id "NRemove") once - Verify the amount in the input field (id "SpendingAdvancedNumberField") decreased after the delete - - diff --git a/journeys/amount-limits/transfer-spending-over-max.xml b/journeys/amount-limits/transfer-spending-over-max.xml deleted file mode 100644 index 8f9f00c23..000000000 --- a/journeys/amount-limits/transfer-spending-over-max.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - Verifies the "Transfer to Spending" amount number pad blocks input exceeding the maximum allowed - transfer amount, shows the "Spending Balance Maximum" warning toast, and still allows deleting - digits while at the cap. - - Precondition: onboarded dev wallet with a POSITIVE on-chain Savings balance and a running node - connected to the LSP (so a real max can be quoted). The max starts at 0 behind a spinner — wait - for it to populate. Note the cap may be lower than the visible "Available" due to fees. Start on - the wallet home screen. - - Enter via the Savings card, not the Spending card: the Spending screen only offers - "Transfer From Savings" while the spending balance is zero, and shows "Transfer To Savings" - once a balance exists. The Savings card's "Transfer To Spending" reaches the same screen in - both states. - - - Tap the Savings balance card on the home screen (id "ActivitySavings") - Tap "Transfer To Spending" (id "TransferToSpending") - If a transfer intro screen appears, tap "Get Started" - Verify the spending amount screen (id "SpendingAmount") is visible - Wait until the available/maximum amount (id "SpendingAmountAvailable") finishes loading and shows a positive value - Tap the "9" key (id "N9") nine times to enter an amount far larger than the maximum allowed - Verify a "Spending Balance Maximum" warning toast appears (id "SpendingAmountExceededToast") - Verify the amount in the input field (id "SpendingAmountNumberField") does not exceed the stated maximum - Tap the delete key (id "NRemove") once - Verify the amount in the input field (id "SpendingAmountNumberField") decreased after the delete - - diff --git a/journeys/cjit-notifications/README.md b/journeys/cjit-notifications/README.md deleted file mode 100644 index 2a0e40b06..000000000 --- a/journeys/cjit-notifications/README.md +++ /dev/null @@ -1,60 +0,0 @@ -# CJIT notification journeys - -Verify that a CJIT payment surfaces exactly one notification, and that a regular (non-CJIT) channel -opening is never reported as a received payment. - -## Adapted from Android — read this first - -The Android suite asserts notification **copy** that iOS does not produce. `BitkitNotification/NotificationService.swift` -maps a Blocktank push type to fixed title/body pairs and never formats an amount: - -| Blocktank type | iOS title | iOS body | -| --- | --- | --- | -| `cjitPaymentArrived` | Incoming Payment | Open Bitkit now to receive your payment | -| `incomingHtlc` | Incoming Payment | Open Bitkit now to receive your payment | -| `orderPaymentConfirmed` | Spending Balance Ready | Open Bitkit to start paying anyone, anywhere. | -| `mutualClose` | Spending Balance Expired | Open Bitkit to move funds from spending to savings | -| `wakeToTimeout` | Payment Pending | Open Bitkit to process pending payment | - -Consequences for the port: - -- **Android issue #1 (missing thousands separators) has no iOS counterpart.** iOS never puts an - amount in the notification, so there is no formatting to assert. Those actions are dropped rather - than rewritten into something the app was never meant to do. -- **Android issue #2 (double notification) does port**, as "exactly one notification". iOS has no - foreground service and no `WakeNodeWorker`; the Notification Service Extension is the only poster, - so the duplicate would have to come from a duplicate push. -- **Android issue #3 (channel open reported as a payment) ports directly** and is the strongest of - the three: `orderPaymentConfirmed` must render as "Spending Balance Ready", never "Incoming Payment". -- `cjit-foreground-service-notification.xml` becomes `cjit-background-notification.xml` — iOS has no - foreground service, so the equivalent state is simply "app backgrounded". - -## Preconditions -- **A physical device, not the simulator.** These journeys need a real APNs round trip: the app must - hold a device token Blocktank can push to, and the notification extension must decrypt a real - Blocktank payload. Nothing runs this suite automatically, and that is not a gap to - fill here — `ai-device-tests.yml` runs `TrezorBridgeDashboardUITests` on a simulator and is not a - journey runner. Run these by hand against an attached device. `test-push-server/` can drive a hand-built push at a known device token when you need to exercise - the extension without the LSP. -- Onboarded regtest wallet connected to the LSP, with notifications authorized and - "Get paid when Bitkit is closed" enabled in Settings → Notifications. -- A funded CJIT entry ready to pay, and no Lightning channel open yet for the CJIT journeys. - -## Inspecting notifications on iOS - -There is no `dumpsys notification`. Two options, in order of preference: - -1. **Read Notification Center on the device.** Swipe down from the top of the screen, then count the - Bitkit entries and read their text. This is the supported assertion path. - `xcodebuildmcp simulator snapshot-ui` does not apply — it drives a simulator, and this suite runs - on hardware. -2. **The extension's own log, if you can get at it.** `NotificationService` logs through `os_log` - (unlike the main app, which writes log files into the app group), at `.info` level, so any - predicate needs `--level info` to see it — the `🔔 Configured notification: type=…, title=…` line - names the type and title it posted. Note `/usr/bin/log stream` has no `--device` flag, so there is - no verified one-liner for an attached device here; use Console.app with the device selected, or - treat this as unavailable and rely on Notification Center. - -## Identifiers used -- In-app toast: `SpendingBalanceReadyToast`. -- Received payment sheet: `ReceivedTransactionButton`. diff --git a/journeys/cjit-notifications/cjit-background-notification.xml b/journeys/cjit-notifications/cjit-background-notification.xml deleted file mode 100644 index 81a644398..000000000 --- a/journeys/cjit-notifications/cjit-background-notification.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - iOS counterpart of the Android foreground-service journey (iOS has no foreground service, so the - equivalent state is the app simply being in the background). - - With the app backgrounded, a CJIT payment that opens a JIT channel must produce EXACTLY ONE - notification, posted by the Notification Service Extension with the cjitPaymentArrived copy: - title "Incoming Payment", body "Open Bitkit now to receive your payment". It must NOT be posted - twice, and must NOT use the orderPaymentConfirmed copy. - - Precondition: physical device with an onboarded regtest wallet connected to the LSP, - notifications authorized, "Get paid when Bitkit is closed" enabled. A funded CJIT entry is ready - to pay (see README.md). No Lightning channel open yet — this is the first, channel-opening payment. - - - Confirm "Get paid when Bitkit is closed" is enabled in Settings → Notifications, then go back to the wallet home screen - Send the app to the background by pressing Home, leaving no Bitkit screen in the foreground - Open Console.app with the device selected and filter on "🔔", including info-level messages — /usr/bin/log has no --device flag, so there is no verified one-liner here; if the log is not reachable, skip it and rely on Notification Center below - Pay the CJIT invoice using the sibling Android checkout's helper (no iOS copy yet, see #694): ../bitkit-android/lsp POST /regtest/lightning/pay '{"invoice":"<bolt11>"}' - Wait up to ~30s for the push to arrive and the notification to be delivered - Verify EXACTLY ONE Bitkit notification is posted for this channel opening, not two - Verify its title is "Incoming Payment" and its body is "Open Bitkit now to receive your payment" - Verify the streamed log shows a single "Configured notification" line with type=cjitPaymentArrived, and no orderPaymentConfirmed notification alongside it - Tap the notification, bring the app to the foreground, and verify the received-transaction sheet shows the correct amount - - diff --git a/journeys/cjit-notifications/cjit-push-single-notification.xml b/journeys/cjit-notifications/cjit-push-single-notification.xml deleted file mode 100644 index eff6be393..000000000 --- a/journeys/cjit-notifications/cjit-push-single-notification.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - The pure push path. With the app fully killed, a CJIT payment must still surface a single - notification, delivered by the Notification Service Extension — this is the wake-the-node-and- - open-the-channel path, so it must keep working. The notification must use the cjitPaymentArrived - copy: title "Incoming Payment", body "Open Bitkit now to receive your payment". - - Precondition: physical device with an onboarded regtest wallet connected to the LSP, - notifications authorized, "Get paid when Bitkit is closed" enabled. A funded CJIT entry is ready - to pay (see README.md), and the app force-quit from the app switcher so only the push and the - extension run. - - - Force-quit Bitkit from the app switcher so no Bitkit process is running - Open Console.app with the device selected and filter on "🔔", including info-level messages — /usr/bin/log has no --device flag, so there is no verified one-liner here; if the log is not reachable, skip it and rely on Notification Center below - Pay the CJIT invoice using the sibling Android checkout's helper (no iOS copy yet, see #694): ../bitkit-android/lsp POST /regtest/lightning/pay '{"invoice":"<bolt11>"}' - Wait up to ~30s for the push to wake the extension and deliver the notification - Verify EXACTLY ONE Bitkit notification is posted for this CJIT payment - Verify its title is "Incoming Payment" and its body is "Open Bitkit now to receive your payment" - Verify the streamed log shows exactly one "Configured notification" line, with type=cjitPaymentArrived - Tap the notification to open the app and verify the received amount in the activity list matches the CJIT payment - - diff --git a/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml b/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml deleted file mode 100644 index 619313922..000000000 --- a/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - Opening a regular (non-CJIT) Lightning channel must NOT be reported as a received payment: no - "Incoming Payment" notification, and never several duplicates. The correct notification for this - path is the orderPaymentConfirmed copy, "Spending Balance Ready". When the app is in the - foreground the only feedback is the in-app "Spending balance ready" toast. - - No extension log assertion here: with the app in the foreground the node holds the .lightning - process lock, and NotificationService returns on that lock before it decrypts or logs anything. - The observable evidence is the toast plus the absence of an "Incoming Payment" notification. - - Precondition: physical device with an onboarded regtest wallet connected to the LSP and a - positive on-chain balance to fund the transfer. No CJIT entry is involved — this is a manual - Transfer channel opening (orderPaymentConfirmed), not a CJIT receive. - - - Keep the app in the foreground on the wallet home screen - Open a regular spending channel via Transfer → Spending, funding some sats and confirming - Mine confirmations if required, using the sibling Android checkout's helper (no iOS copy yet, see #694): ../bitkit-android/lsp POST /regtest/chain/mine '{"count":3}' - Wait for the ChannelReady event (~5-10s) - Verify an in-app "Spending balance ready" toast appears (id "SpendingBalanceReadyToast") - Open Notification Center and inspect the Bitkit entries - Verify NO "Incoming Payment" notification was posted for this channel opening - Verify any notification posted for it uses the "Spending Balance Ready" copy and not the "Incoming Payment" copy - Verify there are not multiple duplicate channel-ready notifications stacked in Notification Center - - diff --git a/journeys/hardware-wallet/README.md b/journeys/hardware-wallet/README.md index 205c858a2..8916a48d2 100644 --- a/journeys/hardware-wallet/README.md +++ b/journeys/hardware-wallet/README.md @@ -1,7 +1,7 @@ # Hardware wallet journeys Cover pairing a Trezor, the home tile and detail screen, the Hardware Wallets settings surface, -passphrase (hidden) wallets, wallet-scoped activity, and the watch-only Transfer To Spending flow. +passphrase (hidden) wallets, wallet-scoped activity, and the Transfer To Spending button, which opens the Transfer Unavailable sheet. ## Transport: Bridge, not USB @@ -51,10 +51,8 @@ Several journeys mutate pairing state. Run them in this order, or re-pair betwee 1. `connect-home-tile.xml` — pairs the emulator; every other journey assumes it ran. 2. `settings-hardware-wallets.xml`, `connect-flow.xml`, `suggestion-intro-sheet.xml` — each forgets and re-pairs the device, ending paired. -3. `activity-blue-icons.xml`, `activity-detail-hw-tags.xml`, `transfer-to-spending*.xml`, `reconnect.xml`, - `send-onchain.xml`. -4. `passphrase-pairing.xml` → `passphrase-duplicate.xml` → `passphrase-transfer-to-spending.xml` → - `passphrase-settings-remove.xml`. +3. `activity-blue-icons.xml`, `activity-detail-hw-tags.xml`, `reconnect.xml`, `send-onchain.xml`. +4. `passphrase-pairing.xml` → `passphrase-duplicate.xml` → `passphrase-settings-remove.xml`. 5. `detail-overview.xml` **last** — its final step forgets the device. ## Approving prompts on the emulator @@ -108,16 +106,9 @@ Walked as far as a simulator allows, against a wallet with two Trezor identities - Row identifiers are inconsistent in the app and this is not a typo in the journeys: `HardwareWalletRowName` has **no** separator, while `HardwareWalletRowDelete_` and `HardwareWalletRow_` use an underscore. The `` is the full `trezor:` device id. -- **The amount number pad was ungated.** `SpendingAmountHw` observed `maxAllowed` without - `initial: true`, and that value lives on the shared `TransferViewModel`, so a screen that opens - with the limits already computed has no change to observe and the fresh `AmountInputViewModel` - keeps `maxAmountOverride` nil. Nine taps on `N9` entered **999 999 999** against an AVAILABLE of - 610 015, with no toast and no snap; Continue stayed disabled, so no unaffordable order was - reachable. Fixed in #686 and covered by `transfer-to-spending-over-max.xml`. - Confirmed resolving without the emulator: `HardwareWalletsSettings`, `HardwareWalletsScreen`, - `AddHardwareWallet`, `HardwareWalletIntroScreen`/`Continue`/`Cancel`, `HardwareWalletScreen`, - `HardwareTransferToSpending`, `HardwareTransferAmount` and its Available/25%/MAX/Continue controls, - and the number pad keys. + `AddHardwareWallet`, `HardwareWalletIntroScreen`/`Continue`/`Cancel`, `HardwareWalletScreen` and + `HardwareTransferToSpending`. ## Identifiers used - Dev Trezor screen: `TrezorScanButton`, `TrezorDevice-bridge`, `TrezorKnownDeviceConnect-bridge`, @@ -135,12 +126,7 @@ Walked as far as a simulator allows, against a wallet with two Trezor identities `RenameHardwareWalletInput`, `RenameHardwareWalletSave`. - Home and detail: tile `ActivityHardware`, screen `HardwareWalletScreen`, `RemoveHardwareWallet`, `RemoveHwWalletDialog`, `HwRemoveKeepBackupToggle`. -- Transfer: `HardwareTransferToSpending`, `HardwareTransferAmount`, - `HardwareTransferAmountAvailable`/`Quarter`/`Max`/`Continue`, - over-max toast `HardwareTransferAmountExceededToast`, `HardwareTransferSign`, - `HardwareTransferSignLearnMore`/`SignAdvanced`/`SignDefault`, - `HardwareTransferOpenTrezorConnect`, `HardwareTransferSigned`, - `HwTransferPassphraseSheet`/`Input`/`Continue`/`Cancel`. +- Transfer: `HardwareTransferToSpending` (opens the Transfer Unavailable sheet). - Activity: `ActivityShowAll` (home only). Rows use **two different schemes** — `ActivityShort-` in the home recent list, `Activity-` in All Activity and on the hardware wallet screen. Tabs `Tab-all`/`Tab-sent`/`Tab-received`/`Tab-other`, diff --git a/journeys/hardware-wallet/passphrase-transfer-to-spending.xml b/journeys/hardware-wallet/passphrase-transfer-to-spending.xml deleted file mode 100644 index 17cf31923..000000000 --- a/journeys/hardware-wallet/passphrase-transfer-to-spending.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - Drives Transfer To Spending from a passphrase (hidden) wallet. While the Trezor session that - holds the passphrase is still open the transfer signs straight away; after the session is - dropped Bitkit asks for the passphrase again, and a wrong one is refused instead of signing - from whichever wallet the device happens to have open. Requires the emulator started with - passphrase protection enabled, the hidden wallet from passphrase-pairing.xml paired, and its - native-segwit account holding spendable regtest funds. - - - Launch the Bitkit app and go to the wallet home screen - Tap the hidden wallet tile ("Hidden Trezor") and verify its detail screen opens (id "HardwareWalletScreen") - Tap "Transfer To Spending" (id "HardwareTransferToSpending"), and if the first-run intro is shown tap "Get Started" - Tap the "25%" quick button (id "HardwareTransferAmountQuarter"), then "Continue" (id "HardwareTransferAmountContinue") and wait for the Blocktank order - Verify the sign screen opens (id "HardwareTransferSign") titled "SIGN WITH YOUR DEVICE" - Tap "Open Trezor Connect" (id "HardwareTransferOpenTrezorConnect") and verify no passphrase sheet appears, because the session opened at pairing still holds the hidden wallet - Approve the Recipient, Amount, Locktime, and Summary prompts on the Bridge emulator in order, and verify the transaction signed screen appears (id "HardwareTransferSigned") - Wait for the Processing Payment screen, tap "Continue Using Bitkit", and verify the app returns to the wallet home screen - Force-quit Bitkit: xcodebuildmcp simulator stop --bundle-id to.bitkit - Relaunch it: xcodebuildmcp simulator launch-app --bundle-id to.bitkit - Once the app is back on the home screen, open the hidden wallet tile and start Transfer To Spending again, setting an amount with the "25%" quick button and continuing to the sign screen - Tap "Open Trezor Connect" (id "HardwareTransferOpenTrezorConnect") and verify the passphrase sheet opens (id "HwTransferPassphraseSheet"), because the session that held the passphrase is gone - Type a wrong passphrase "not-the-one" into the input (id "HwTransferPassphraseInput") and tap "Continue" (id "HwTransferPassphraseContinue") - - Confirm on the emulated device for every account the passphrase session reads: the device - prompts once per address type and the call blocks until it is acknowledged. Either tap the - emulator UI or run: ../bitkit-docker/scripts/trezor-emulator send-json '{"type":"emulator-press-yes","id":1}' - - Verify an error toast says the passphrase opens a different wallet, that no signing prompt was shown on the emulator, and that no new transfer appears in the activity list - Navigate to the wallet home screen and verify the hardware tile count is unchanged: the wallet the wrong passphrase opened must not be added - Reopen the sign screen, tap "Open Trezor Connect", type the correct passphrase "bitkit-hidden" and tap "Continue" - Approve the Recipient, Amount, Locktime, and Summary prompts on the Bridge emulator, and verify the transaction signed screen appears (id "HardwareTransferSigned") - Scan the app group for both passphrases, treating only "no match" as clean: UDID=<the simulator this journey is running on>; GROUP=$(xcrun simctl get_app_container "$UDID" to.bitkit groups | awk '{print $2}'); [ -d "$GROUP" ] || { echo "LEAK_CHECK_INVALID: container not found"; exit 1; }; grep -rl -e "bitkit-hidden" -e "not-the-one" "$GROUP"; s=$?; case $s in 1) echo NO_PASSPHRASE_LEAK;; 0) echo "LEAK FOUND";; *) echo "LEAK_CHECK_INVALID: grep exit $s";; esac - Verify the previous command printed NO_PASSPHRASE_LEAK. LEAK_CHECK_INVALID means the check did not scan anything and the journey cannot conclude either way — treat it as a failed step, not a pass - - diff --git a/journeys/hardware-wallet/transfer-to-spending-max-lsp-cap.xml b/journeys/hardware-wallet/transfer-to-spending-max-lsp-cap.xml deleted file mode 100644 index e8f7fdc1f..000000000 --- a/journeys/hardware-wallet/transfer-to-spending-max-lsp-cap.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - Verifies that a funded hardware wallet whose balance is larger than the current - Blocktank/LSP headroom is capped by the transferable spending limit, not by the - device balance. Requires a paired Bridge emulator or physical Trezor with a hardware - balance larger than the available transfer limit, and at least one existing channel or - pending order consuming most of the regtest LSP cap. - - - Launch the Bitkit app and go to the wallet home screen - Verify the hardware wallet tile shows a balance larger than the AVAILABLE amount expected in the transfer flow - Tap the hardware wallet tile (id "ActivityHardware") beneath the SAVINGS and SPENDING tiles, and verify the hardware wallet detail screen opens (id "HardwareWalletScreen") - Tap the "Transfer To Spending" button (id "HardwareTransferToSpending") - Verify the transfer amount screen opens (id "HardwareTransferAmount"), titled "TRANSFER TO SPENDING", and the AVAILABLE amount (id "HardwareTransferAmountAvailable") is lower than the hardware wallet balance because it is capped by LSP headroom - Tap the "MAX" quick button (id "HardwareTransferAmountMax") - Verify the amount field matches the AVAILABLE amount exactly and does not use the full hardware wallet balance - Tap "Continue" (id "HardwareTransferAmountContinue") and wait for the Blocktank order to be created - Verify the sign screen opens (id "HardwareTransferSign"), showing NETWORK FEES, SERVICE FEES, TO SPENDING and TOTAL, without an insufficient-funds toast - Tap "Open Trezor Connect" (id "HardwareTransferOpenTrezorConnect") and approve every hardware-wallet prompt - Verify the transaction signed screen appears (id "HardwareTransferSigned") and then advances to Processing Payment / setting-up progress - - diff --git a/journeys/hardware-wallet/transfer-to-spending-node-warmup.xml b/journeys/hardware-wallet/transfer-to-spending-node-warmup.xml deleted file mode 100644 index 0d633ad6b..000000000 --- a/journeys/hardware-wallet/transfer-to-spending-node-warmup.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - Verifies that starting a hardware-wallet transfer while the Lightning node is still - warming up does not fail or strand the CTA in loading. The flow should show the normal - loading/progress UI, continue once the node reaches the needed state, and land on the - sign screen. Requires a paired and funded hardware wallet. - - - Force-quit Bitkit: xcodebuildmcp simulator stop --bundle-id to.bitkit - Relaunch it: xcodebuildmcp simulator launch-app --bundle-id to.bitkit - As soon as the wallet home screen is visible, tap the hardware wallet tile (id "ActivityHardware") beneath the SAVINGS and SPENDING tiles - Tap the "Transfer To Spending" button (id "HardwareTransferToSpending") - Verify the transfer amount screen or loading/progress UI appears, and no reconnect, node-not-ready, or generic failure toast is shown while the node warms up - Tap the "25%" quick button (id "HardwareTransferAmountQuarter") if the amount screen is shown - Tap "Continue" (id "HardwareTransferAmountContinue") if the amount screen is shown, then wait for order creation and node warm-up to finish - Verify the sign screen opens (id "HardwareTransferSign"), titled "SIGN WITH YOUR DEVICE", and the Continue/Open Trezor Connect CTA is no longer loading - Navigate back without signing so this journey only covers node warm-up behavior - - diff --git a/journeys/hardware-wallet/transfer-to-spending-over-max.xml b/journeys/hardware-wallet/transfer-to-spending-over-max.xml deleted file mode 100644 index 4e078cf8e..000000000 --- a/journeys/hardware-wallet/transfer-to-spending-over-max.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - Verifies the hardware Transfer To Spending amount number pad enforces the transferable maximum: - an entry above it snaps down to the maximum with the "Spending Balance Maximum" warning toast, - and deleting digits still works while at the cap. - - iOS-only — there is no counterpart under `bitkit-android/journeys/hardware-wallet`. It was added - alongside the fix for the bug it covers. `SpendingAmountHw` observed `maxAllowed` without - `initial: true`, and since that value lives on the shared `TransferViewModel` and outlives the - screen, arriving with the limits already computed produced no change to observe. The freshly - created `AmountInputViewModel` then kept `maxAmountOverride` nil, leaving the pad bounded only by - the global 999 999 999 sat ceiling. Measured on an iPhone 17 simulator: nine taps on `N9` entered - 999 999 999 against an AVAILABLE of 610 015, with no toast and no snap. Continue stayed disabled, - so nothing unaffordable could be ordered — the gap was in the input, not in the order. - - Precondition: a paired Bridge emulator whose native-segwit account holds spendable regtest funds, - and a reachable Blocktank LSP so a real maximum can be quoted. Start on the wallet home screen. - - The screen is entered twice on purpose. A first entry that computes the limits from zero fires - the change either way, so it passes even with the bug; the second entry, where the limits are - already settled, is the one that regressed. - - - Tap the hardware wallet tile (id "ActivityHardware") beneath the SAVINGS and SPENDING tiles, and verify the hardware wallet detail screen opens (id "HardwareWalletScreen") - Tap the "Transfer To Spending" button (id "HardwareTransferToSpending") - If the first-run Transfer To Spending intro is shown, tap "Get Started" - Verify the transfer amount screen opens (id "HardwareTransferAmount") - Wait for the limits to load (id "N9", predicate enabled) — the number pad is held disabled behind the AVAILABLE spinner until the device balance and the LSP quote return - Verify the AVAILABLE amount (id "HardwareTransferAmountAvailable") shows a positive value - Tap the "9" key (id "N9") nine times to enter an amount far larger than the maximum allowed - Verify a "Spending Balance Maximum" warning toast appears (id "HardwareTransferAmountExceededToast") and names the maximum - Verify the amount in the input field snapped to the maximum and does not exceed the AVAILABLE amount — 999 999 999 here is the regression this journey exists for - Tap the delete key (id "NRemove") once, and verify the amount decreased - Tap back (id "NavigationBack") to return to the hardware wallet screen - Tap "Transfer To Spending" (id "HardwareTransferToSpending") again — this second entry arrives with the limits already computed, which is the case that regressed - Wait for the number pad (id "N9", predicate enabled), then tap the "9" key nine times again - Verify the toast appears again (id "HardwareTransferAmountExceededToast") and the amount snapped to the same maximum, not to 999 999 999 - - diff --git a/journeys/hardware-wallet/transfer-to-spending.xml b/journeys/hardware-wallet/transfer-to-spending.xml deleted file mode 100644 index c616e0676..000000000 --- a/journeys/hardware-wallet/transfer-to-spending.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - Drives the watch-only Transfer To Spending flow for a paired Trezor: Amount -> Sign With - Your Device -> Transaction Signed -> Processing Payment, then verifies the new transfer is - represented once in the hardware wallet's Core activity scope. Requires a paired Bridge - emulator whose native-segwit account holds spendable regtest funds and at least one older - hardware receive row with a distinct amount. - - - Launch the Bitkit app and go to the wallet home screen - Verify the recent activity list contains an older blue hardware received row with a distinct amount - Tap the hardware wallet tile (id "ActivityHardware") beneath the SAVINGS and SPENDING tiles, and verify the hardware wallet detail screen opens (id "HardwareWalletScreen") - Tap the "Transfer To Spending" button (id "HardwareTransferToSpending") - If the first-run Transfer To Spending intro is shown, tap "Get Started" - Verify the transfer amount screen opens (id "HardwareTransferAmount"), titled "TRANSFER TO SPENDING", showing an AVAILABLE row (id "HardwareTransferAmountAvailable"), the 25% and MAX quick buttons, and a number pad - Tap the "25%" quick button (id "HardwareTransferAmountQuarter") to set a valid amount below the available limit - Tap "Continue" (id "HardwareTransferAmountContinue") and wait for the fee estimate - Verify the sign screen opens (id "HardwareTransferSign"), titled "SIGN WITH YOUR DEVICE", showing the NETWORK FEES, SERVICE FEES, TO SPENDING and TOTAL cells, the Learn More and Advanced buttons, and the Trezor illustration - Tap "Open Trezor Connect" (id "HardwareTransferOpenTrezorConnect") to create the Blocktank order. If its fee is higher than the estimate, verify the fee cells refresh and tap "Open Trezor Connect" again; otherwise signing starts immediately - Approve the Recipient, Amount, Locktime, and Summary prompts on the Bridge emulator in order - Verify the transaction signed screen appears (id "HardwareTransferSigned"), titled "TRANSACTION SIGNED", showing the same fee cells and the checkmark illustration - Wait for the screen to auto-forward and verify the Processing Payment / setting-up progress screen appears - Tap "Continue Using Bitkit" and verify the app returns to the wallet home screen - Verify the recent activity list contains exactly one new blue hardware Transfer row with subtitle "From Savings", above the older blue received row - Tap the new blue Transfer row and verify its detail screen shows "TO SPENDING" - - diff --git a/journeys/notification-permission/README.md b/journeys/notification-permission/README.md deleted file mode 100644 index 6b7d18db1..000000000 --- a/journeys/notification-permission/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# Notification-permission journeys - -Verify that the background-setup toggle — shown on the Receive → CJIT confirm screen, the Receive → -CJIT liquidity screen and the Transfer → Spending confirm screen — drives the iOS notification -authorization prompt, and that the app's own notification settings own the route into the system -Settings app. - -**The label differs by screen.** The two Receive CJIT screens bind -`wallet__receive_background_setup_toggle` ("Enable background setup"); Transfer → Spending confirm -binds `lightning__spending_confirm__background_setup` ("Set up in background"), matching Android. -The switch identifiers are stable, so prefer those over the copy. - -## iOS vs Android - -Android requests `POST_NOTIFICATIONS` at the toggle and, once granted, re-tapping the toggle deep -links into system settings. iOS differs in two ways that these journeys are written around: - -- **The prompt is one-shot and system-owned.** `PushNotificationManager.requestPermission()` calls - `UNUserNotificationCenter.requestAuthorization`, which only shows the alert while the status is - `notDetermined`. Once the user has answered once, toggling on is silent. -- **Toggling off does not open system settings.** `MainNavView`'s `onChange(of:)` simply calls - `notificationManager.unregister()`. The route into the iOS Settings app is a dedicated button on - Settings → Notifications (id `NotificationsOpenSystemSettings`), which is what - `toggle-off-and-system-settings-route.xml` covers instead of Android's - `toggle-off-opens-system-settings.xml`. - -## Preconditions -- Onboarded dev wallet with the node connected to the LSP so a CJIT order can be quoted, and (for - the transfer journey) a positive on-chain Savings balance. -- **Notification authorization must be `notDetermined`** for the three "requests permission" - journeys. The alert is one-shot per install, so reset it by reinstalling the app: - `xcrun simctl uninstall to.bitkit` then `xcodebuildmcp simulator build-and-run`. - Re-onboarding the wallet is part of that reset. -- `settings.enableNotifications` must start **off** so the toggles read unchecked. - -## Gotcha: one setting, three toggles - -All three switches bind to the same `settings.enableNotifications`. Flipping one flips the other two, -so run these journeys one per app state — do not chain them expecting an unchecked toggle on the -second screen. - -## Identifiers used -- Receive CJIT: amount screen `ReceiveCjitAmount`, field `ReceiveCjitAmountNumberField`, - continue `ReceiveCjitAmountContinue`; confirm screen `ReceiveCjitConfirm` with switch - `ReceiveConfirmNotificationSwitch`; liquidity screen `ReceiveCjitLiquidity` with switch - `ReceiveLiquidityNotificationSwitch`. -- Transfer: `SpendingAmount`, `SpendingAmountAvailable`, `SpendingAmountContinue`, and the confirm - screen switch `SpendingConfirmNotificationSwitch`. -- Settings → Notifications: `NotificationsOpenSystemSettings`. diff --git a/journeys/notification-permission/receive-cjit-confirm-notification-toggle.xml b/journeys/notification-permission/receive-cjit-confirm-notification-toggle.xml deleted file mode 100644 index 9c7919a8c..000000000 --- a/journeys/notification-permission/receive-cjit-confirm-notification-toggle.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - Verifies that the "Enable background setup" toggle on the Receive → CJIT confirm screen - (id "ReceiveCjitConfirm") launches the iOS notification authorization alert, and that - allowing it checks the toggle. - - Precondition: onboarded dev wallet with the node connected to the LSP so a CJIT order can be - quoted, notification authorization still notDetermined (reinstall first — the alert is one-shot), - and "Enable background setup" starting off. Start on the wallet home screen. - - - Tap the "Receive" button (id "Receive") on the home screen - Tap the "Spending" tab in the Receive sheet - Tap "Receive Lightning funds" - On the amount screen (id "ReceiveCjitAmount"), enter an amount above the CJIT minimum (e.g. 100 000 sats) using the number pad - Tap "Continue" (id "ReceiveCjitAmountContinue") and wait for the CJIT order to be created and the confirm screen (id "ReceiveCjitConfirm") to appear - Verify the "Enable background setup" toggle (id "ReceiveConfirmNotificationSwitch") is visible and off - Tap the "Enable background setup" toggle (id "ReceiveConfirmNotificationSwitch") - Verify the iOS notification authorization alert appears (text like "Bitkit" Would Like to Send You Notifications") - Tap "Allow" - Verify the "Enable background setup" toggle is now on - - diff --git a/journeys/notification-permission/receive-cjit-liquidity-notification-toggle.xml b/journeys/notification-permission/receive-cjit-liquidity-notification-toggle.xml deleted file mode 100644 index 448813937..000000000 --- a/journeys/notification-permission/receive-cjit-liquidity-notification-toggle.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - Verifies that the "Enable background setup" toggle on the Receive → CJIT liquidity screen - (id "ReceiveCjitLiquidity", reached via "Learn More" from the confirm screen) launches the - iOS notification authorization alert, and that allowing it checks the toggle. - - Precondition: onboarded dev wallet with the node connected to the LSP so a CJIT order can be - quoted, notification authorization still notDetermined (reinstall first — the alert is one-shot), - and "Enable background setup" starting off. Start on the wallet home screen. - - - Tap the "Receive" button (id "Receive") on the home screen - Tap the "Spending" tab in the Receive sheet - Tap "Receive Lightning funds" - On the amount screen (id "ReceiveCjitAmount"), enter an amount above the CJIT minimum (e.g. 100 000 sats) using the number pad - Tap "Continue" (id "ReceiveCjitAmountContinue") and wait for the confirm screen to appear - Tap "Learn More" to open the liquidity screen - Verify the liquidity screen (id "ReceiveCjitLiquidity") is visible with the lightning channel illustration and the "Enable background setup" toggle (id "ReceiveLiquidityNotificationSwitch") off - Tap the "Enable background setup" toggle (id "ReceiveLiquidityNotificationSwitch") - Verify the iOS notification authorization alert appears (text like "Bitkit" Would Like to Send You Notifications") - Tap "Allow" - Verify the "Enable background setup" toggle is now on - - diff --git a/journeys/notification-permission/toggle-off-and-system-settings-route.xml b/journeys/notification-permission/toggle-off-and-system-settings-route.xml deleted file mode 100644 index 0683e3dfd..000000000 --- a/journeys/notification-permission/toggle-off-and-system-settings-route.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - iOS counterpart of the Android "toggle off opens system settings" journey. On iOS the - "Enable background setup" toggle does NOT deep link anywhere: switching it off just unregisters - push and leaves the user on the same screen, with no alert (the authorization alert is one-shot - and already answered). The route into the iOS Settings app lives on Settings → Notifications - instead, behind the "Customize in iOS Bitkit Settings" button. - - Precondition: onboarded dev wallet with the node connected to the LSP so a CJIT order can be - quoted, notification authorization ALREADY GRANTED, and "Enable background setup" already on so - the toggle starts checked. Start on the wallet home screen. - - - Tap the "Receive" button (id "Receive") on the home screen - Tap the "Spending" tab in the Receive sheet - Tap "Receive Lightning funds" - On the amount screen (id "ReceiveCjitAmount"), enter an amount above the CJIT minimum (e.g. 100 000 sats) using the number pad - Tap "Continue" (id "ReceiveCjitAmountContinue") and wait for the confirm screen (id "ReceiveCjitConfirm") to appear - Verify the "Enable background setup" toggle (id "ReceiveConfirmNotificationSwitch") is visible and on - Tap the "Enable background setup" toggle (id "ReceiveConfirmNotificationSwitch") - Verify the toggle turns off, no permission alert appears, the app does not leave the foreground, and the confirm screen is still shown - Dismiss the Receive sheet and return to the wallet home screen - Open the menu, tap "Settings" (id "DrawerSettings"), then open "Notifications" - Verify the "Get paid when Bitkit is closed" row reflects the toggle being off - Tap the "Customize in iOS Bitkit Settings" button (id "NotificationsOpenSystemSettings") - Verify the iOS Settings app opens on the Bitkit entry, with no in-app navigation and no permission alert - Return to Bitkit and verify the Notifications settings screen is still shown - - diff --git a/journeys/notification-permission/transfer-spending-confirm-notification-toggle.xml b/journeys/notification-permission/transfer-spending-confirm-notification-toggle.xml deleted file mode 100644 index 86013c37b..000000000 --- a/journeys/notification-permission/transfer-spending-confirm-notification-toggle.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - Verifies that the "Set up in background" toggle on the Transfer → Spending confirm screen - launches the iOS notification authorization alert, and that allowing it checks the toggle. - - Precondition: onboarded dev wallet with a POSITIVE on-chain Savings balance and the node - connected to the LSP (so a real max can be quoted), notification authorization still - notDetermined (reinstall first — the alert is one-shot), and "Set up in background" starting - off. The spending max starts at 0 behind a spinner — wait for it to populate. Start on the - wallet home screen. Enter via the Savings card, not the Spending card — the Spending screen - only offers "Transfer From Savings" while the spending balance is zero. - - - Tap the Savings balance card on the home screen (id "ActivitySavings") - Tap "Transfer To Spending" (id "TransferToSpending") - If a transfer intro screen appears, tap "Get Started" - On the spending amount screen (id "SpendingAmount"), wait until the available/maximum amount (id "SpendingAmountAvailable") finishes loading and shows a positive value - Enter an amount within the maximum using the number pad - Tap Continue (id "SpendingAmountContinue") and wait for the spending confirm screen to appear - Verify the "Set up in background" toggle (id "SpendingConfirmNotificationSwitch") is visible and off - Tap the "Set up in background" toggle (id "SpendingConfirmNotificationSwitch") - Verify the iOS notification authorization alert appears (text like "Bitkit" Would Like to Send You Notifications") - Tap "Allow" - Verify the "Set up in background" toggle is now on - - diff --git a/journeys/transfer/spending-confirm-amount-change.xml b/journeys/transfer/spending-confirm-amount-change.xml deleted file mode 100644 index 90728ae9a..000000000 --- a/journeys/transfer/spending-confirm-amount-change.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - Verifies that a transfer to spending never pays more than the total Spending Confirm shows - (synonymdev/bitkit-ios#783). After the swipe creates the order, the funding is rebuilt for it. A - plan that costs the same or less than the screen pays on that swipe. A plan that costs more pays - nothing: the screen shows the new amounts, a "Fees changed" toast explains the increase, and the - next swipe pays them. - - This journey covers the cheaper plan. Spending Confirm sizes the network fee for two inputs, then - a larger UTXO confirms before the swipe, so the plan rebuilt at swipe spends one input, costs less - and pays at once. The higher-total path (the "Fees changed" toast) is covered by unit tests and a - manual test, because spending the wallet's own UTXO outside the app is not a listed capability. - - iOS differs from Android in two steps: there is no app-log step, and closing the received sheet - re-sizes Spending Confirm, so the lower amounts can already be on screen before the swipe. The - swipe pays at once either way. - - Precondition: a freshly onboarded Debug build on the staging regtest backend (a plain Debug build, - or an E2E_BUILD with E2E_BACKEND=network; the default E2E_BUILD targets the local backend, which - cannot open a Blocktank channel) with no channel and exactly two - confirmed on-chain UTXOs of 150 000 sats each (two ../bitkit-android/lsp deposits to the - wallet's savings address, each followed by a mine). Start on the wallet home screen. - - - Tap Receive (id "Receive"), tap the "Savings" receive tab if one is shown, tap "Show Details" (id "ShowDetails"), read the address from id "ReceiveOnchainAddress", then close the sheet to return to the home screen - Tap the Savings balance card (id "ActivitySavings"), then "Transfer To Spending" (id "TransferToSpending") - If the spending intro appears, tap "Get Started" (id "SpendingIntroContinue") - Enter 200 000 on the number pad (ids "N2", "N0", "N0", "N0", "N0", "N0") and tap "Continue" (id "SpendingAmountContinue") - Verify the confirm screen is visible (id "SpendingConfirmMore") and wait until its network fee is shown - Note the amounts under "Network Fees" and "Total" - Run in one command: ../bitkit-android/lsp POST /regtest/chain/deposit '{"address":"<savings addr>","amountSat":500000}' && ../bitkit-android/lsp POST /regtest/chain/mine '{"count":1}' - Wait 30s for the wallet to sync the new UTXO - If the received sheet (id "ReceivedTransaction") is visible, tap its button (id "ReceivedTransactionButton") - Verify the confirm screen is visible; if the received sheet was shown, "Network Fees" and "Total" may already show lower amounts than noted - Swipe the "Swipe To Transfer" handle (id "GRAB") fully to the right - Wait up to 90s for the setting up screen (id "LightningSettingUp") - - diff --git a/journeys/transfer/transfer-unavailable-sheet.xml b/journeys/transfer/transfer-unavailable-sheet.xml new file mode 100644 index 000000000..a3ae0538a --- /dev/null +++ b/journeys/transfer/transfer-unavailable-sheet.xml @@ -0,0 +1,27 @@ + + + Verifies that Bitkit points users to Manual Setup now that Blocktank no longer sells Lightning channels. + Every Transfer To Spending entry opens the "Transfer Unavailable" sheet (testTag "TransferUnavailable") + at once, with no network check first. The + sheet shows "MANUAL SETUP REQUIRED" (testTag "TransferUnavailableTitle") and the buttons "Learn More" + (testTag "TransferUnavailableLearnMore"), which opens https://blog.bitkit.to/ in the browser, and "Continue" + (testTag "TransferUnavailableContinue"), which opens the existing Manual Setup screen. Receive's Lightning + path shows the same sheet content. + + Precondition: onboarded wallet with a positive on-chain Savings balance and no channels. Start on the + wallet home screen. + + + Tap the Savings balance card (testTag "ActivitySavings") and verify "Transfer To Spending" (testTag "TransferToSpending") is shown + Tap "Transfer To Spending" and verify the sheet titled "Transfer Unavailable" shows "MANUAL SETUP REQUIRED" and "Automatic transfers to your spending balance are no longer available. Use your own node, or set up a connection with another provider." + Tap "Learn More" and verify the browser opens blog.bitkit.to, then return to Bitkit + Tap "Continue" and verify the sheet closes and the "MANUAL SETUP" screen shows the node id (testTag "NodeIdInput"), host (testTag "HostInput") and port (testTag "PortInput") fields + Go back to the wallet home, tap the Spending balance card (testTag "ActivitySpending"), tap the transfer button (testTag "TransferFromSavings") and verify the same sheet opens; close it + Open Settings › Advanced › Lightning Connections, tap add connection (testTag "add_connection_button") and verify the screen text reads "Automatic transfers to your spending balance are no longer available. Use your own node, or set up a connection with another provider." + Verify "Manual Setup" (testTag "FundManual") is the first option and enabled, and "Transfer from Savings" (testTag "FundTransfer"), "Transfer from HW wallet" (testTag "FundHardware") and "Use Other Wallet" (testTag "FundReceive") below it are disabled; tap "Transfer from Savings" and verify nothing opens + Tap "Learn More" (testTag "FundLearnMore") at the bottom and verify the browser opens blog.bitkit.to, then return to Bitkit and go back to the wallet home + Tap "Receive" (testTag "Receive"), select the Spending tab (testTag "Tab-spending") and tap "Receive Lightning funds" + Verify the screen titled "Transfer Unavailable" shows "MANUAL SETUP REQUIRED" and "Automatic transfers to your spending balance are no longer available. Use your own node, or set up a connection with another provider.", with a back button + Tap "Continue" and verify the sheet closes and the "MANUAL SETUP" screen opens + +