├── .gitignore ├── BitcoinClient ├── Sources │ ├── MagicNumber.swift │ ├── Extensions │ │ ├── URLSessionStreamTask.swift │ │ ├── Data.swift │ │ └── InputStream.swift │ ├── NetworkAddress.swift │ ├── MessageHeader.swift │ ├── VersionMessage.swift │ └── BitcoinClient.swift └── Tests │ ├── BitcoinClientTests.swift │ ├── MessageHeaderTests.swift │ ├── NetworkAddressTests.swift │ ├── Extensions │ ├── DataTests.swift │ └── InputStreamTests.swift │ └── VersionMessageTests.swift ├── App ├── Assets.xcassets │ ├── Contents.json │ ├── AppIcon.appiconset │ │ ├── BitcoinWidgets_20px.png │ │ ├── BitcoinWidgets_29px.png │ │ ├── BitcoinWidgets_40px.png │ │ ├── BitcoinWidgets_58px.png │ │ ├── BitcoinWidgets_60px.png │ │ ├── BitcoinWidgets_76px.png │ │ ├── BitcoinWidgets_80px.png │ │ ├── BitcoinWidgets_87px.png │ │ ├── BitcoinWidgets_1024px.png │ │ ├── BitcoinWidgets_120px-1.png │ │ ├── BitcoinWidgets_120px.png │ │ ├── BitcoinWidgets_152px.png │ │ ├── BitcoinWidgets_167px.png │ │ ├── BitcoinWidgets_180px.png │ │ ├── BitcoinWidgets_40px-1.png │ │ ├── BitcoinWidgets_40px-2.png │ │ ├── BitcoinWidgets_58px-1.png │ │ ├── BitcoinWidgets_80px-1.png │ │ └── Contents.json │ ├── WatchIcon.appiconset │ │ ├── BitcoinWidgets_1024px.png │ │ └── Contents.json │ ├── Bitcoin.imageset │ │ ├── Contents.json │ │ └── Bitcoin.svg │ ├── AccentColor.colorset │ │ └── Contents.json │ └── LaunchScreenBackground.colorset │ │ └── Contents.json ├── Sources │ ├── BitcoinWidgetsApp.swift │ ├── LabelLink.swift │ └── ContentView.swift ├── Info.plist └── Localizable.xcstrings ├── Widgets ├── Assets.xcassets │ ├── Contents.json │ ├── MempoolColor.colorset │ │ └── Contents.json │ └── WidgetBackground.colorset │ │ └── Contents.json ├── Sources │ ├── CombinedStatus.swift │ ├── HalvingCountdown.swift │ ├── MoscowTime.swift │ ├── MempoolStatus.swift │ ├── BitcoinWidgets.swift │ ├── CombinedStatusWidget.swift │ ├── BitcoinBackground.swift │ ├── MoscowTimeFormatter.swift │ ├── NodeStatusWidget.swift │ ├── MoscowTimeWidget.swift │ ├── HalvingCountdownWidget.swift │ ├── MempoolStatusWidget.swift │ ├── NodeStatus.swift │ ├── CombinedStatusProvider.swift │ ├── MempoolStatusProvider.swift │ ├── CombinedStatusView.swift │ ├── HalvingCountdownProvider.swift │ ├── NodeStatusProvider.swift │ ├── MoscowTimeProvider.swift │ ├── HalvingCountdownView.swift │ ├── NodeStatusView.swift │ ├── MoscowTimeView.swift │ └── MempoolStatusView.swift ├── Info.plist ├── en.lproj │ └── Configuration.strings ├── cs.lproj │ └── Configuration.strings ├── Localizable.xcstrings └── Base.lproj │ └── Configuration.intentdefinition ├── BlockchainClient ├── Sources │ ├── Ticker.swift │ └── BlockchainClient.swift └── Tests │ └── BlockchainClientTests.swift ├── BitnodesClient ├── Sources │ ├── CheckNodeResponse.swift │ ├── NodeStatusResponse.swift │ └── BitnodesClient.swift └── Tests │ └── BitnodesClientTests.swift ├── BitcoinWidgets.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── Tests.xcscheme │ ├── BitcoinWidgets.xcscheme │ ├── watchOS App.xcscheme │ ├── iOS Widget Extension.xcscheme │ └── watchOS Widget Extension.xcscheme ├── MempoolClient ├── Sources │ ├── RecommendedFees.swift │ └── MempoolClient.swift └── Tests │ └── MempoolClientTests.swift ├── LICENSE ├── PRIVACY.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | xcuserdata/ 2 | -------------------------------------------------------------------------------- /BitcoinClient/Sources/MagicNumber.swift: -------------------------------------------------------------------------------- 1 | enum MagicNumber: UInt32 { 2 | case main = 0xD9B4BEF9 3 | } 4 | -------------------------------------------------------------------------------- /App/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Widgets/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /BlockchainClient/Sources/Ticker.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Ticker: Decodable { 4 | let symbol: String 5 | let last: Double 6 | } 7 | -------------------------------------------------------------------------------- /BitnodesClient/Sources/CheckNodeResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct CheckNodeResponse: Decodable { 4 | let userAgent: String 5 | let height: Int32 6 | } 7 | -------------------------------------------------------------------------------- /App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_20px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanascz/bitcoin-widgets-ios/HEAD/App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_20px.png -------------------------------------------------------------------------------- /App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_29px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanascz/bitcoin-widgets-ios/HEAD/App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_29px.png -------------------------------------------------------------------------------- /App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_40px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanascz/bitcoin-widgets-ios/HEAD/App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_40px.png -------------------------------------------------------------------------------- /App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_58px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanascz/bitcoin-widgets-ios/HEAD/App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_58px.png -------------------------------------------------------------------------------- /App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_60px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanascz/bitcoin-widgets-ios/HEAD/App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_60px.png -------------------------------------------------------------------------------- /App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_76px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanascz/bitcoin-widgets-ios/HEAD/App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_76px.png -------------------------------------------------------------------------------- /App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_80px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanascz/bitcoin-widgets-ios/HEAD/App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_80px.png -------------------------------------------------------------------------------- /App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_87px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanascz/bitcoin-widgets-ios/HEAD/App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_87px.png -------------------------------------------------------------------------------- /App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_1024px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanascz/bitcoin-widgets-ios/HEAD/App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_1024px.png -------------------------------------------------------------------------------- /App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_120px-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanascz/bitcoin-widgets-ios/HEAD/App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_120px-1.png -------------------------------------------------------------------------------- /App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_120px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanascz/bitcoin-widgets-ios/HEAD/App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_120px.png -------------------------------------------------------------------------------- /App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_152px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanascz/bitcoin-widgets-ios/HEAD/App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_152px.png -------------------------------------------------------------------------------- /App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_167px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanascz/bitcoin-widgets-ios/HEAD/App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_167px.png -------------------------------------------------------------------------------- /App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_180px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanascz/bitcoin-widgets-ios/HEAD/App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_180px.png -------------------------------------------------------------------------------- /App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_40px-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanascz/bitcoin-widgets-ios/HEAD/App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_40px-1.png -------------------------------------------------------------------------------- /App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_40px-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanascz/bitcoin-widgets-ios/HEAD/App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_40px-2.png -------------------------------------------------------------------------------- /App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_58px-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanascz/bitcoin-widgets-ios/HEAD/App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_58px-1.png -------------------------------------------------------------------------------- /App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_80px-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanascz/bitcoin-widgets-ios/HEAD/App/Assets.xcassets/AppIcon.appiconset/BitcoinWidgets_80px-1.png -------------------------------------------------------------------------------- /App/Assets.xcassets/WatchIcon.appiconset/BitcoinWidgets_1024px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanascz/bitcoin-widgets-ios/HEAD/App/Assets.xcassets/WatchIcon.appiconset/BitcoinWidgets_1024px.png -------------------------------------------------------------------------------- /BitcoinWidgets.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Widgets/Sources/CombinedStatus.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import WidgetKit 3 | 4 | struct CombinedStatus: TimelineEntry { 5 | 6 | let date: Date = Date() 7 | let nodeStatus: NodeStatus 8 | let mempoolStatus: MempoolStatus 9 | 10 | } 11 | -------------------------------------------------------------------------------- /Widgets/Sources/HalvingCountdown.swift: -------------------------------------------------------------------------------- 1 | import WidgetKit 2 | 3 | struct HalvingCountdown: TimelineEntry { 4 | 5 | let date: Date = Date() 6 | let showBitcoinLogo: Bool 7 | let blockCount: NSNumber 8 | let estimate: Date 9 | 10 | } 11 | -------------------------------------------------------------------------------- /App/Assets.xcassets/Bitcoin.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Bitcoin.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /MempoolClient/Sources/RecommendedFees.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct RecommendedFees: Decodable { 4 | let fastestFee: Int 5 | let halfHourFee: Int 6 | let hourFee: Int 7 | let economyFee: Int 8 | let minimumFee: Int 9 | } 10 | -------------------------------------------------------------------------------- /App/Sources/BitcoinWidgetsApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct BitcoinWidgetsApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | .preferredColorScheme(.dark) 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /BitcoinWidgets.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /App/Assets.xcassets/WatchIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "BitcoinWidgets_1024px.png", 5 | "idiom" : "universal", 6 | "platform" : "watchos", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Widgets/Sources/MoscowTime.swift: -------------------------------------------------------------------------------- 1 | import WidgetKit 2 | 3 | struct MoscowTime: TimelineEntry { 4 | 5 | let date: Date = Date() 6 | let showBitcoinLogo: Bool 7 | let format: MoscowTimeFormat 8 | let primaryPrice: NSNumber 9 | let primaryCurrencyCode: String 10 | let secondaryPrice: NSNumber 11 | let secondaryCurrencyCode: String 12 | 13 | } 14 | -------------------------------------------------------------------------------- /Widgets/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.widgetkit-extension 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Widgets/Sources/MempoolStatus.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import WidgetKit 3 | 4 | struct MempoolStatus: TimelineEntry { 5 | 6 | let date: Date = Date() 7 | let showBitcoinLogo: Bool 8 | let blockHeight: Int32 9 | let fastestFee: Int 10 | let halfHourFee: Int 11 | let hourFee: Int 12 | let economyFee: Int 13 | let minimumFee: Int 14 | 15 | } 16 | -------------------------------------------------------------------------------- /BlockchainClient/Tests/BlockchainClientTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | class BlockchainClientTests: XCTestCase { 4 | 5 | func testGetTickers() async throws { 6 | let client = BlockchainClient() 7 | let tickers = try await client.getTickers() 8 | 9 | XCTAssertNotNil(tickers) 10 | XCTAssertGreaterThan(tickers.count, 0) 11 | XCTAssertEqual(tickers["USD"]?.symbol, "USD") 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Widgets/Sources/BitcoinWidgets.swift: -------------------------------------------------------------------------------- 1 | import WidgetKit 2 | import SwiftUI 3 | 4 | @main 5 | struct BitcoinWidgets: WidgetBundle { 6 | 7 | @WidgetBundleBuilder 8 | var body: some Widget { 9 | #if !os(watchOS) 10 | NodeStatusWidget() 11 | #endif 12 | MempoolStatusWidget() 13 | #if !os(watchOS) 14 | CombinedStatusWidget() 15 | #endif 16 | MoscowTimeWidget() 17 | HalvingCountdownWidget() 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /App/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x1A", 9 | "green" : "0x93", 10 | "red" : "0xF7" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Widgets/Assets.xcassets/MempoolColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xDE", 9 | "green" : "0x52", 10 | "red" : "0xAF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /App/Assets.xcassets/LaunchScreenBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x00", 9 | "green" : "0x00", 10 | "red" : "0x00" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Widgets/Assets.xcassets/WidgetBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x00", 9 | "green" : "0x00", 10 | "red" : "0x00" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Widgets/Sources/CombinedStatusWidget.swift: -------------------------------------------------------------------------------- 1 | import WidgetKit 2 | import SwiftUI 3 | 4 | struct CombinedStatusWidget: Widget { 5 | 6 | var body: some WidgetConfiguration { 7 | IntentConfiguration(kind: "cz.yanas.bitcoin.CombinedStatusWidget", intent: NodeConfigurationIntent.self, provider: CombinedStatusProvider()) { entry in 8 | CombinedStatusView(combinedStatus: entry) 9 | } 10 | .configurationDisplayName("CombinedStatusWidget.displayName") 11 | .description("CombinedStatusWidget.description") 12 | .supportedFamilies([.systemMedium]) 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /BlockchainClient/Sources/BlockchainClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class BlockchainClient { 4 | 5 | private static let baseUrl = URL(string: "https://blockchain.info")! 6 | 7 | private let urlSession = URLSession.init(configuration: .ephemeral) 8 | private let decoder = JSONDecoder() 9 | 10 | func getTickers() async throws -> [String: Ticker] { 11 | let tickersUrl = Self.baseUrl.appendingPathComponent("/ticker") 12 | let (data, _) = try await urlSession.data(from: tickersUrl) 13 | 14 | return try decoder.decode([String: Ticker].self, from: data) 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Widgets/Sources/BitcoinBackground.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import WidgetKit 3 | 4 | struct BitcoinBackground: View { 5 | 6 | let family: WidgetFamily 7 | let showLogo: Bool 8 | 9 | var body: some View { 10 | ZStack { 11 | Color("WidgetBackground") 12 | if showLogo { 13 | Image("Bitcoin") 14 | .resizable() 15 | .opacity(0.07) 16 | .aspectRatio(contentMode: family == .systemSmall ? .fit : .fill) 17 | .padding(family == .systemSmall ? .all : .trailing) 18 | } 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Widgets/en.lproj/Configuration.strings: -------------------------------------------------------------------------------- 1 | "gpCwrM" = "Node Configuration"; 2 | "tVvJ9c" = "Configure host and port of your node"; 3 | "4L4D5m" = "Host"; 4 | "4uTPFk" = "Port"; 5 | "u1GuAn" = "Show Bitcoin Logo"; 6 | 7 | "tWGjwd" = "Mempool Configuration"; 8 | "JKy8Ff" = "Configure mempool.space display options"; 9 | "17ZqzB" = "Show Bitcoin Logo"; 10 | 11 | "CNIGff" = "Moscow Time Configuration"; 12 | "Mm0rjP" = "Configure Moscow Time format and currencies"; 13 | "Ld81XL" = "Moscow Time Format"; 14 | "GivfKe" = "Format"; 15 | "7TuOxV" = "Fiat Currency"; 16 | "jf1oAa" = "Primary Currency"; 17 | "ZRIceO" = "Secondary Currency"; 18 | "FDOABk" = "Show Bitcoin Logo"; 19 | -------------------------------------------------------------------------------- /Widgets/cs.lproj/Configuration.strings: -------------------------------------------------------------------------------- 1 | "gpCwrM" = "Konfigurace uzlu"; 2 | "tVvJ9c" = "Nastavte adresu and port svého uzlu"; 3 | "4L4D5m" = "Adresa"; 4 | "4uTPFk" = "Port"; 5 | "u1GuAn" = "Zobrazit Bitcoin logo"; 6 | 7 | "tWGjwd" = "Konfigurace mempool.space"; 8 | "JKy8Ff" = "Nastavte způsob zobrazení stavu mempool.space"; 9 | "17ZqzB" = "Zobrazit Bitcoin logo"; 10 | 11 | "CNIGff" = "Konfigurace Moskevského času"; 12 | "Mm0rjP" = "Nastavte formát a měny Moskevského času"; 13 | "Ld81XL" = "Formát Moskevského času"; 14 | "GivfKe" = "Formát"; 15 | "7TuOxV" = "Fiat měna"; 16 | "jf1oAa" = "Primární měna"; 17 | "ZRIceO" = "Sekundární měna"; 18 | "FDOABk" = "Zobrazit Bitcoin logo"; 19 | -------------------------------------------------------------------------------- /Widgets/Sources/MoscowTimeFormatter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class MoscowTimeFormatter: Formatter { 4 | 5 | var format: MoscowTimeFormat = .time 6 | 7 | override func string(for object: Any?) -> String { 8 | if let value = object as? Double { 9 | let satsPerUnit = Int(100_000_000 / value) 10 | 11 | switch format { 12 | case .time: 13 | return String(format: "%i:%02i", satsPerUnit / 100, satsPerUnit % 100) 14 | case .plain: 15 | return String(satsPerUnit) 16 | case .unknown: 17 | return "?" 18 | } 19 | } 20 | return "?" 21 | } 22 | 23 | } 24 | 25 | -------------------------------------------------------------------------------- /BitcoinClient/Sources/Extensions/URLSessionStreamTask.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension URLSessionStreamTask { 4 | 5 | private static let defaultTimeout: Double = 1 6 | 7 | func readData(length: Int) async throws -> Data? { 8 | return try await readData(ofMinLength: length, maxLength: length, timeout: Self.defaultTimeout).0 9 | } 10 | 11 | func write(_ message: VersionMessage) async throws { 12 | let messageData = message.data 13 | let header = MessageHeader( 14 | command: VersionMessage.command, 15 | payloadSize: UInt32(messageData.count), 16 | payloadChecksum: messageData.checksum 17 | ) 18 | 19 | try await write(header.data + messageData, timeout: Self.defaultTimeout) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /App/Sources/LabelLink.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct LabelLink: View { 4 | 5 | let titleKey: LocalizedStringKey 6 | let destination: URL 7 | let systemImage: String 8 | 9 | init (_ titleKey: LocalizedStringKey, url: String, systemImage: String) { 10 | self.titleKey = titleKey 11 | self.destination = URL(string: url)! 12 | self.systemImage = systemImage 13 | } 14 | 15 | var body: some View { 16 | Label { 17 | HStack { 18 | Link(titleKey, destination: destination) 19 | Spacer() 20 | Image(systemName: "link") 21 | .foregroundColor(.secondary) 22 | } 23 | } icon: { 24 | Image(systemName: systemImage) 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /BitcoinClient/Sources/NetworkAddress.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Network 3 | 4 | struct NetworkAddress { 5 | 6 | private static let addressSize = 16 7 | 8 | let services: UInt64 9 | let address: IPv6Address 10 | let port: UInt16 11 | 12 | init() { 13 | self.services = 0 14 | self.address = IPv6Address.any 15 | self.port = 0 16 | } 17 | 18 | init(from input: InputStream) { 19 | self.services = input.readUInt64() 20 | self.address = IPv6Address(input.readData(count: Self.addressSize))! 21 | self.port = input.readUInt16() 22 | } 23 | 24 | var data: Data { 25 | var data = Data() 26 | data.append(services) 27 | data.append(address.rawValue) 28 | data.append(port) 29 | 30 | return data 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /App/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ITSAppUsesNonExemptEncryption 6 | 7 | NSUserActivityTypes 8 | 9 | MempoolConfigurationIntent 10 | MoscowTimeConfigurationIntent 11 | NodeConfigurationIntent 12 | 13 | UIApplicationSceneManifest 14 | 15 | UIApplicationSupportsMultipleScenes 16 | 17 | 18 | UILaunchScreen 19 | 20 | UIColorName 21 | LaunchScreenBackground 22 | UIImageName 23 | Bitcoin 24 | UIImageRespectsSafeAreaInsets 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /BitcoinClient/Tests/BitcoinClientTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | class BitcoinClientTests: XCTestCase { 4 | 5 | func testGetVersion() async throws { 6 | let client = BitcoinClient(host: "127.0.0.1", port: 8333) 7 | let message = try await client.getVersionMessage() 8 | 9 | XCTAssertEqual(message.protocolVersion, 70016) 10 | XCTAssertEqual(message.services, 1033) 11 | XCTAssertGreaterThanOrEqual(message.timestamp, Date().advanced(by: -21)) 12 | XCTAssertLessThanOrEqual(message.timestamp, Date().advanced(by: 21)) 13 | XCTAssertNotNil(message.recipient) 14 | XCTAssertNotNil(message.sender) 15 | XCTAssertNotNil(message.nonce) 16 | XCTAssertEqual(message.userAgent, "/Satoshi:23.0.0/") 17 | XCTAssertGreaterThanOrEqual(message.blockHeight, 740597) 18 | XCTAssertTrue(message.relayTxs) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /BitnodesClient/Tests/BitnodesClientTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | class BitnodesClientTests: XCTestCase { 4 | 5 | private let host = "todo.onion" 6 | private let port = 8333 7 | 8 | func testCheckNode() async throws { 9 | let client = BitnodesClient() 10 | let response = try await client.checkNode(host: host, port: port) 11 | 12 | XCTAssertEqual(response.userAgent, "/Satoshi:23.0.0/") 13 | XCTAssertGreaterThanOrEqual(response.height, 740597) 14 | } 15 | 16 | func testGetNodeStatus() async throws { 17 | let client = BitnodesClient() 18 | let response = try await client.getNodeStatus(host: host, port: port) 19 | 20 | XCTAssertEqual(response.status, "UP") 21 | XCTAssertEqual(response.protocolVersion, 70016) 22 | XCTAssertEqual(response.userAgent, "/Satoshi:23.0.0/") 23 | XCTAssertGreaterThanOrEqual(response.height, 740597) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Widgets/Sources/NodeStatusWidget.swift: -------------------------------------------------------------------------------- 1 | import WidgetKit 2 | import SwiftUI 3 | 4 | struct NodeStatusWidget: Widget { 5 | 6 | let supportedFamilies: [WidgetFamily]; 7 | 8 | init() { 9 | if #available(iOSApplicationExtension 16.0, *) { 10 | self.supportedFamilies = [.systemSmall, .systemMedium, .accessoryRectangular, .accessoryInline] 11 | } else { 12 | self.supportedFamilies = [.systemSmall, .systemMedium] 13 | } 14 | } 15 | 16 | var body: some WidgetConfiguration { 17 | IntentConfiguration(kind: "cz.yanas.bitcoin.NodeStatusWidget", intent: NodeConfigurationIntent.self, provider: NodeStatusProvider()) { entry in 18 | NodeStatusView(nodeStatus: entry) 19 | } 20 | .configurationDisplayName("NodeStatusWidget.displayName") 21 | .description("NodeStatusWidget.description") 22 | .supportedFamilies(supportedFamilies) 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /BitnodesClient/Sources/NodeStatusResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct NodeStatusResponse: Decodable { 4 | 5 | let status: String 6 | let protocolVersion: Int32 7 | let userAgent: String 8 | let height: Int32 9 | 10 | init(from decoder: Decoder) throws { 11 | let container = try decoder.container(keyedBy: CodingKeys.self) 12 | var dataContainer = try container.nestedUnkeyedContainer(forKey: .data) 13 | 14 | self.status = try container.decode(String.self, forKey: .status) 15 | self.protocolVersion = try dataContainer.decode(Int32.self) 16 | self.userAgent = try dataContainer.decode(String.self) 17 | _ = try dataContainer.decode(Int.self) 18 | _ = try dataContainer.decode(Int.self) 19 | self.height = try dataContainer.decode(Int32.self) 20 | } 21 | 22 | private enum CodingKeys: String, CodingKey { 23 | case status 24 | case data 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Widgets/Sources/MoscowTimeWidget.swift: -------------------------------------------------------------------------------- 1 | import WidgetKit 2 | import SwiftUI 3 | 4 | struct MoscowTimeWidget: Widget { 5 | 6 | let supportedFamilies: [WidgetFamily]; 7 | 8 | init() { 9 | #if os(watchOS) 10 | self.supportedFamilies = [.accessoryRectangular, .accessoryInline] 11 | #else 12 | if #available(iOSApplicationExtension 16.0, *) { 13 | self.supportedFamilies = [.systemSmall, .systemMedium, .accessoryRectangular, .accessoryInline] 14 | } else { 15 | self.supportedFamilies = [.systemSmall, .systemMedium] 16 | } 17 | #endif 18 | } 19 | 20 | var body: some WidgetConfiguration { 21 | IntentConfiguration(kind: "cz.yanas.bitcoin.MoscowTimeWidget", intent: MoscowTimeConfigurationIntent.self, provider: MoscowTimeProvider()) { entry in 22 | MoscowTimeView(moscowTime: entry) 23 | } 24 | .configurationDisplayName("MoscowTimeWidget.displayName") 25 | .description("MoscowTimeWidget.description") 26 | .supportedFamilies(supportedFamilies) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Widgets/Sources/HalvingCountdownWidget.swift: -------------------------------------------------------------------------------- 1 | import WidgetKit 2 | import SwiftUI 3 | 4 | struct HalvingCountdownWidget: Widget { 5 | 6 | let supportedFamilies: [WidgetFamily]; 7 | 8 | init() { 9 | #if os(watchOS) 10 | self.supportedFamilies = [.accessoryRectangular] 11 | #else 12 | if #available(iOSApplicationExtension 16.0, *) { 13 | self.supportedFamilies = [.systemSmall, .systemMedium, .accessoryRectangular] 14 | } else { 15 | self.supportedFamilies = [.systemSmall, .systemMedium] 16 | } 17 | #endif 18 | } 19 | 20 | var body: some WidgetConfiguration { 21 | IntentConfiguration(kind: "cz.yanas.bitcoin.HalvingCountdownWidget", intent: MempoolConfigurationIntent.self, provider: HalvingCountdownProvider()) { entry in 22 | HalvingCountdownView(halvingCountdown: entry) 23 | } 24 | .configurationDisplayName("HalvingCountdownWidget.displayName") 25 | .description("HalvingCountdownWidget.description") 26 | .supportedFamilies(supportedFamilies) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Widgets/Sources/MempoolStatusWidget.swift: -------------------------------------------------------------------------------- 1 | import WidgetKit 2 | import SwiftUI 3 | 4 | struct MempoolStatusWidget: Widget { 5 | 6 | let supportedFamilies: [WidgetFamily]; 7 | 8 | init() { 9 | #if os(watchOS) 10 | self.supportedFamilies = [.accessoryRectangular, .accessoryInline] 11 | #else 12 | if #available(iOSApplicationExtension 16.0, *) { 13 | self.supportedFamilies = [.systemSmall, .systemMedium, .accessoryRectangular, .accessoryInline] 14 | } else { 15 | self.supportedFamilies = [.systemSmall, .systemMedium] 16 | } 17 | #endif 18 | } 19 | 20 | var body: some WidgetConfiguration { 21 | IntentConfiguration(kind: "cz.yanas.bitcoin.MempoolStatusWidget", intent: MempoolConfigurationIntent.self, provider: MempoolStatusProvider()) { entry in 22 | MempoolStatusView(mempoolStatus: entry) 23 | } 24 | .configurationDisplayName("MempoolStatusWidget.displayName") 25 | .description("MempoolStatusWidget.description") 26 | .supportedFamilies(supportedFamilies) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Martin Janík 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /BitcoinClient/Tests/MessageHeaderTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | class MessageHeaderTests: XCTestCase { 4 | 5 | func testInitFromData() throws { 6 | let data = Data([ 7 | 0xFA, 0xBF, 0xB5, 0xDA, 8 | 0x76, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x00, 0x00, 0x00, 0x00, 0x00, 9 | 0x65, 0x00, 0x00, 0x00, 10 | 0x35, 0x8D, 0x49, 0x32 11 | ]) 12 | 13 | let header = MessageHeader(from: data) 14 | 15 | XCTAssertEqual(header.magicNumber, 0xDAB5BFFA) 16 | XCTAssertEqual(header.command, "version") 17 | XCTAssertEqual(header.payloadSize, 101) 18 | XCTAssertEqual(header.payloadChecksum, 0x32498D35) 19 | } 20 | 21 | func testToData() throws { 22 | let header = MessageHeader(command: "version", payloadSize: 100, payloadChecksum: 0x37518C19) 23 | 24 | XCTAssertEqual(header.data, Data([ 25 | 0xF9, 0xBE, 0xB4, 0xD9, 26 | 0x76, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x00, 0x00, 0x00, 0x00, 0x00, 27 | 0x64, 0x00, 0x00, 0x00, 28 | 0x19, 0x8C, 0x51, 0x37 29 | ])) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /MempoolClient/Tests/MempoolClientTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | class MempoolClientTests: XCTestCase { 4 | 5 | func testGetBlockHeight() async throws { 6 | let client = MempoolClient() 7 | let blockHeight = try await client.getBlockHeight() 8 | 9 | XCTAssertGreaterThanOrEqual(blockHeight, 755237) 10 | } 11 | 12 | func testGetBlockHeightByDate() async throws { 13 | let client = MempoolClient() 14 | let blockHeight = try await client.getBlockHeightByDate(Date()) 15 | 16 | XCTAssertGreaterThanOrEqual(blockHeight, 826153) 17 | } 18 | 19 | func testGetRecommendedFees() async throws { 20 | let client = MempoolClient() 21 | let recommendedFees = try await client.getRecommendedFees() 22 | 23 | XCTAssertGreaterThanOrEqual(recommendedFees.fastestFee, 1) 24 | XCTAssertGreaterThanOrEqual(recommendedFees.halfHourFee, 1) 25 | XCTAssertGreaterThanOrEqual(recommendedFees.hourFee, 1) 26 | XCTAssertGreaterThanOrEqual(recommendedFees.economyFee, 1) 27 | XCTAssertGreaterThanOrEqual(recommendedFees.minimumFee, 1) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /BitcoinClient/Tests/NetworkAddressTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Network 3 | 4 | class NetworkAddressTests: XCTestCase { 5 | 6 | func testInitFromInputStream() throws { 7 | let data = Data([ 8 | 0x0D, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 9 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 10 | 0x20, 0x8D 11 | ]) 12 | 13 | let input = InputStream(data: data) 14 | input.open(); defer { input.close() } 15 | 16 | let networkAddress = NetworkAddress(from: input) 17 | 18 | XCTAssertEqual(networkAddress.services, 1037) 19 | XCTAssertEqual(networkAddress.address, IPv6Address.any) 20 | XCTAssertEqual(networkAddress.port, 8333) 21 | } 22 | 23 | func testToData() throws { 24 | let networkAddress = NetworkAddress() 25 | 26 | XCTAssertEqual(networkAddress.data, Data([ 27 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 28 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 29 | 0x00, 0x00 30 | ])) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Widgets/Sources/NodeStatus.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import WidgetKit 3 | import SwiftUI 4 | 5 | struct NodeStatus: TimelineEntry { 6 | 7 | let date: Date = Date() 8 | let showBitcoinLogo: Bool 9 | let blockHeight: Int32? 10 | let userAgent: String? 11 | let protocolVersion: Int32? 12 | let error: Error? 13 | 14 | init(showBitcoinLogo: Bool, blockHeight: Int32, userAgent: String, protocolVersion: Int32) { 15 | self.showBitcoinLogo = showBitcoinLogo 16 | self.blockHeight = blockHeight 17 | self.userAgent = userAgent 18 | self.protocolVersion = protocolVersion 19 | self.error = nil 20 | } 21 | 22 | init(showBitcoinLogo: Bool, error: Error) { 23 | self.showBitcoinLogo = showBitcoinLogo 24 | self.blockHeight = nil 25 | self.userAgent = nil 26 | self.protocolVersion = nil 27 | self.error = error 28 | } 29 | 30 | enum Error: String { 31 | 32 | case configurationRequired 33 | case nodeUnreachable 34 | 35 | var localizedDescription: LocalizedStringKey { 36 | return LocalizedStringKey("NodeStatus.Error." + self.rawValue); 37 | } 38 | 39 | } 40 | 41 | } 42 | 43 | -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | Bitcoin Widgets do not collect any personal data. Your data stays completely private, on your device. No personal data leaves your devices. 4 | 5 | ## Personally Identifiable Information 6 | 7 | Bitcoin Widgets do not collect or transmit any Personally Identifiable Information outside of your device. It only stores preferences about the user interface within its local database. 8 | 9 | ## Privacy Policy Changes 10 | 11 | Although most changes are likely to be minor, Bitcoin Widgets may change its Privacy Policy from time to time, and at Bitcoin Widgets's sole discretion. Your continued use of this application after any changes in this Privacy Policy will constitute your acceptance of such a change. 12 | 13 | ## Indemnity 14 | 15 | You hereby indemnify us and undertake to keep us indemnified against any losses, damages, costs, liabilities and expenses (including, without limitation, legal expenses and any amounts paid by us to a third party in settlement of a claim or dispute on the advice of our legal advisers) incurred or suffered by us arising out of use of this app or any breach by you of any provision of these terms of use, or arising out of any claim that you have breached any provision of these terms of use. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bitcoin Widgets 2 | 3 | ![Bitcoin Widgets](https://repository-images.githubusercontent.com/503792690/78980745-cce8-428e-ba02-c81f98f0a2e0) 4 | 5 | See status of your [full node](https://bitcoin.org/en/full-node) and/or [mempool.space](https://mempool.space) Bitcoin 6 | explorer on your iPhone, iPad or Mac with the Apple M1 chip. Some widgets may also be added to your Apple Watch. 7 | 8 | ## Installation 9 | 10 | * Download [Bitcoin Widgets](https://apps.apple.com/app/bitcoin-widgets/id1629041739) from the App Store. 11 | * Clone the repository and install the app manually via Xcode. 12 | 13 | ## Configuration 14 | 15 | Provide host and port of your full node in widget configuration. 16 | 17 | * Public nodes are accessed directly via the [Bitcoin protocol](https://en.bitcoin.it/wiki/Protocol_documentation). 18 | * Nodes running as Tor hidden services are accessed via the [Bitnodes API](https://bitnodes.io/api/). 19 | 20 | If your node runs behind NAT, consider setting up a [reverse SSH tunnel](https://github.com/yanascz/bitcoind-tunnel). 21 | 22 | ## Support 23 | 24 | If you find Bitcoin Widgets useful, please consider supporting its maintenance by sending some sats to ⚡widgets@bitcoinwidgets.app.\ 25 | ![Bitcoin Widgets LNURL-pay QR code](https://bitcoinwidgets.app/ln/pay/widgets/qr-code) 26 | -------------------------------------------------------------------------------- /BitcoinClient/Sources/MessageHeader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct MessageHeader { 4 | 5 | static let size = 24 6 | private static let commandSize = 12 7 | 8 | let magicNumber: UInt32 9 | let command: String 10 | let payloadSize: UInt32 11 | let payloadChecksum: UInt32 12 | 13 | init(command: String, payloadSize: UInt32, payloadChecksum: UInt32) { 14 | self.magicNumber = MagicNumber.main.rawValue 15 | self.command = command 16 | self.payloadSize = payloadSize 17 | self.payloadChecksum = payloadChecksum 18 | } 19 | 20 | init(from data: Data) { 21 | let input = InputStream(data: data) 22 | input.open(); defer { input.close() } 23 | 24 | self.magicNumber = input.readUInt32() 25 | self.command = String(bytes: input.readData(count: Self.commandSize).filter { $0 > 0 }, encoding: .ascii)! 26 | self.payloadSize = input.readUInt32() 27 | self.payloadChecksum = input.readUInt32() 28 | } 29 | 30 | var data: Data { 31 | var data = Data() 32 | data.append(magicNumber) 33 | data.append(command.data(using: .ascii)!) 34 | data.append(Data(count: Self.commandSize - command.count)) 35 | data.append(payloadSize) 36 | data.append(payloadChecksum) 37 | 38 | return data 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /BitnodesClient/Sources/BitnodesClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class BitnodesClient { 4 | 5 | private static let baseUrl = URL(string: "https://bitnodes.io/api/v1")! 6 | 7 | private let urlSession = URLSession.init(configuration: .ephemeral) 8 | private let decoder = JSONDecoder() 9 | 10 | init() { 11 | decoder.keyDecodingStrategy = .convertFromSnakeCase 12 | } 13 | 14 | func checkNode(host: String, port: Int) async throws -> CheckNodeResponse { 15 | var checkNodeRequest = URLRequest(url: Self.baseUrl.appendingPathComponent("/checknode/")) 16 | checkNodeRequest.httpMethod = "POST" 17 | checkNodeRequest.httpBody = "address=\(host)&port=\(port)".data(using: .utf8) 18 | checkNodeRequest.setValue("application/x-www-form-urlencoded; charset=UTF-8", forHTTPHeaderField: "Content-Type") 19 | 20 | let (data, _) = try await urlSession.data(for: checkNodeRequest) 21 | 22 | return try decoder.decode(CheckNodeResponse.self, from: data) 23 | } 24 | 25 | func getNodeStatus(host: String, port: Int) async throws -> NodeStatusResponse { 26 | let nodeStatusUrl = Self.baseUrl.appendingPathComponent("/nodes/\(host)-\(port)/") 27 | let (data, _) = try await urlSession.data(from: nodeStatusUrl) 28 | 29 | return try decoder.decode(NodeStatusResponse.self, from: data) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /App/Assets.xcassets/Bitcoin.imageset/Bitcoin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /BitcoinClient/Sources/Extensions/Data.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CryptoKit 3 | 4 | extension Data { 5 | 6 | mutating func append(_ value: Bool) { 7 | append(UInt8(value ? 0x01 : 0x00)) 8 | } 9 | 10 | mutating func append(_ value: UInt16) { 11 | append(Swift.withUnsafeBytes(of: value.bigEndian) { Data($0) }) 12 | } 13 | 14 | mutating func append(_ value: Int32) { 15 | append(Swift.withUnsafeBytes(of: value.littleEndian) { Data($0) }) 16 | } 17 | 18 | mutating func append(_ value: UInt32) { 19 | append(Swift.withUnsafeBytes(of: value.littleEndian) { Data($0) }) 20 | } 21 | 22 | mutating func append(_ value: Int64) { 23 | append(Swift.withUnsafeBytes(of: value.littleEndian) { Data($0) }) 24 | } 25 | 26 | mutating func append(_ value: UInt64) { 27 | append(Swift.withUnsafeBytes(of: value.littleEndian) { Data($0) }) 28 | } 29 | 30 | mutating func append(_ value: String) { 31 | // TODO: use variable length integer encoding 32 | append(UInt8(value.count)) 33 | append(value.data(using: .ascii)!) 34 | } 35 | 36 | var checksum: UInt32 { 37 | let hash: Data = Data(SHA256.hash(data: Data(SHA256.hash(data: self)))) 38 | let b1 = UInt32(hash[0]) 39 | let b2 = UInt32(hash[1]) << 8 40 | let b3 = UInt32(hash[2]) << 16 41 | let b4 = UInt32(hash[3]) << 24 42 | 43 | return b1 + b2 + b3 + b4 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /MempoolClient/Sources/MempoolClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class MempoolClient { 4 | 5 | private static let baseUrl = URL(string: "https://mempool.space/api")! 6 | 7 | private let urlSession = URLSession.init(configuration: .ephemeral) 8 | private let decoder = JSONDecoder() 9 | 10 | func getBlockHeight() async throws -> Int32 { 11 | let blockHeightUrl = Self.baseUrl.appendingPathComponent("/blocks/tip/height") 12 | let (data, _) = try await urlSession.data(from: blockHeightUrl) 13 | 14 | return try decoder.decode(Int32.self, from: data) 15 | } 16 | 17 | func getBlockHeightByDate(_ date: Date) async throws -> Int32 { 18 | struct BlockInfo: Decodable { 19 | let height: Int32 20 | } 21 | 22 | let blockInfoByTimestampUrl = Self.baseUrl.appendingPathComponent( 23 | "/v1/mining/blocks/timestamp/\(Int(date.timeIntervalSince1970))" 24 | ) 25 | let (data, _) = try await urlSession.data(from: blockInfoByTimestampUrl) 26 | let blockInfo = try decoder.decode(BlockInfo.self, from: data) 27 | 28 | return blockInfo.height 29 | } 30 | 31 | func getRecommendedFees() async throws -> RecommendedFees { 32 | let recommendedFeesUrl = Self.baseUrl.appendingPathComponent("/v1/fees/recommended") 33 | let (data, _) = try await urlSession.data(from: recommendedFeesUrl) 34 | 35 | return try decoder.decode(RecommendedFees.self, from: data) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /Widgets/Sources/CombinedStatusProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import WidgetKit 3 | 4 | struct CombinedStatusProvider: IntentTimelineProvider { 5 | 6 | private let nodeStatusProvider = NodeStatusProvider() 7 | private let mempoolStatusProvider = MempoolStatusProvider() 8 | 9 | func placeholder(in context: Context) -> CombinedStatus { 10 | return CombinedStatus( 11 | nodeStatus: nodeStatusProvider.placeholder(in: context), 12 | mempoolStatus: mempoolStatusProvider.placeholder(in: context) 13 | ) 14 | } 15 | 16 | func getSnapshot(for configuration: NodeConfigurationIntent, in context: Context, completion: @escaping (CombinedStatus) -> ()) { 17 | if context.isPreview { 18 | completion(placeholder(in: context)) 19 | return 20 | } 21 | 22 | Task.init { 23 | completion(try await getCombinedStatus(for: configuration)) 24 | } 25 | } 26 | 27 | func getTimeline(for configuration: NodeConfigurationIntent, in context: Context, completion: @escaping (Timeline) -> ()) { 28 | Task.init { 29 | let combinedStatus = try await getCombinedStatus(for: configuration) 30 | let nextUpdate = Calendar.current.date(byAdding: .minute, value: 1, to: Date())! 31 | completion(Timeline(entries: [combinedStatus], policy: .after(nextUpdate))) 32 | } 33 | } 34 | 35 | func getCombinedStatus(for configuration: NodeConfigurationIntent) async throws -> CombinedStatus { 36 | let nodeStatus = await nodeStatusProvider.getNodeStatus(for: configuration) 37 | let mempoolStatus = try await mempoolStatusProvider.getMempoolStatus(showBitcoinLogo: configuration.showBitcoinLogo) 38 | 39 | return CombinedStatus(nodeStatus: nodeStatus, mempoolStatus: mempoolStatus) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /BitcoinClient/Tests/Extensions/DataTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | class DataTests: XCTestCase { 4 | 5 | func testAppendBool() throws { 6 | var data = Data() 7 | data.append(true) 8 | data.append(false) 9 | 10 | XCTAssertEqual(data, Data([0x01, 0x00])) 11 | } 12 | 13 | func testAppendUInt16() throws { 14 | var data = Data() 15 | data.append(UInt16(0x0102)) 16 | 17 | XCTAssertEqual(data, Data([0x01, 0x02])) 18 | } 19 | 20 | func testAppendInt32() throws { 21 | var data = Data() 22 | data.append(Int32(0x04030201)) 23 | 24 | XCTAssertEqual(data, Data([0x01, 0x02, 0x03, 0x04])) 25 | } 26 | 27 | func testAppendUInt32() throws { 28 | var data = Data() 29 | data.append(UInt32(0x04030201)) 30 | 31 | XCTAssertEqual(data, Data([0x01, 0x02, 0x03, 0x04])) 32 | } 33 | 34 | func testAppendInt64() throws { 35 | var data = Data() 36 | data.append(Int64(0x0807060504030201)) 37 | 38 | XCTAssertEqual(data, Data([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08])) 39 | } 40 | 41 | func testAppendUInt64() throws { 42 | var data = Data() 43 | data.append(UInt64(0x0807060504030201)) 44 | 45 | XCTAssertEqual(data, Data([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08])) 46 | } 47 | 48 | func testAppendString() throws { 49 | var data = Data() 50 | data.append("foo") 51 | 52 | XCTAssertEqual(data, Data([0x03, 0x66, 0x6F, 0x6F])) 53 | } 54 | 55 | func testAppendEmptyString() throws { 56 | var data = Data() 57 | data.append("") 58 | 59 | XCTAssertEqual(data, Data([0x00])) 60 | } 61 | 62 | func testChecksum() throws { 63 | let data = "hello".data(using: .ascii)! 64 | 65 | XCTAssertEqual(data.checksum, UInt32(0xDFC99595)) 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /BitcoinClient/Sources/VersionMessage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct VersionMessage { 4 | 5 | static let command: String = "version" 6 | 7 | let protocolVersion: Int32 8 | let services: UInt64 9 | let timestamp: Date 10 | let recipient: NetworkAddress 11 | let sender: NetworkAddress 12 | let nonce: UInt64 13 | let userAgent: String 14 | let blockHeight: Int32 15 | let relayTxs: Bool 16 | 17 | init(protocolVersion: Int32, timestamp: Date, nonce: UInt64, userAgent: String) { 18 | self.protocolVersion = protocolVersion 19 | self.services = 0 20 | self.timestamp = timestamp 21 | self.recipient = NetworkAddress() 22 | self.sender = NetworkAddress() 23 | self.nonce = nonce 24 | self.userAgent = userAgent 25 | self.blockHeight = 0 26 | self.relayTxs = false 27 | } 28 | 29 | init(from data: Data) { 30 | let input = InputStream(data: data) 31 | input.open(); defer { input.close() } 32 | 33 | self.protocolVersion = input.readInt32() 34 | self.services = input.readUInt64() 35 | self.timestamp = Date(timeIntervalSince1970: TimeInterval(input.readInt64())) 36 | self.recipient = NetworkAddress(from: input) 37 | self.sender = NetworkAddress(from: input) 38 | self.nonce = input.readUInt64() 39 | self.userAgent = input.readString() 40 | self.blockHeight = input.readInt32() 41 | self.relayTxs = input.readBool() 42 | } 43 | 44 | var data: Data { 45 | var data = Data() 46 | data.append(protocolVersion) 47 | data.append(services) 48 | data.append(Int64(timestamp.timeIntervalSince1970)) 49 | data.append(recipient.data) 50 | data.append(sender.data) 51 | data.append(nonce) 52 | data.append(userAgent) 53 | data.append(blockHeight) 54 | data.append(relayTxs) 55 | 56 | return data 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /BitcoinClient/Sources/Extensions/InputStream.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension InputStream { 4 | 5 | func readBool() -> Bool { 6 | var value: UInt8 = 0 7 | read(to: &value, count: MemoryLayout.size) 8 | 9 | return value > 0 10 | } 11 | 12 | func readUInt16() -> UInt16 { 13 | var value: UInt16 = 0 14 | read(to: &value, count: MemoryLayout.size) 15 | 16 | return value.bigEndian 17 | } 18 | 19 | func readInt32() -> Int32 { 20 | var value: Int32 = 0 21 | read(to: &value, count: MemoryLayout.size) 22 | 23 | return value.littleEndian 24 | } 25 | 26 | func readUInt32() -> UInt32 { 27 | var value: UInt32 = 0 28 | read(to: &value, count: MemoryLayout.size) 29 | 30 | return value.littleEndian 31 | } 32 | 33 | func readInt64() -> Int64 { 34 | var value: Int64 = 0 35 | read(to: &value, count: MemoryLayout.size) 36 | 37 | return value.littleEndian 38 | } 39 | 40 | func readUInt64() -> UInt64 { 41 | var value: UInt64 = 0 42 | read(to: &value, count: MemoryLayout.size) 43 | 44 | return value.littleEndian 45 | } 46 | 47 | func readData(count: Int) -> Data { 48 | let buffer = UnsafeMutablePointer.allocate(capacity: count) 49 | read(buffer, maxLength: count) 50 | 51 | return Data(bytes: buffer, count: count) 52 | } 53 | 54 | func readString() -> String { 55 | // TODO: use variable length integer decoding 56 | var count: UInt8 = 0 57 | read(&count, maxLength: MemoryLayout.size) 58 | let data = readData(count: Int(count)) 59 | 60 | return String(bytes: data, encoding: .ascii)! 61 | } 62 | 63 | private func read(to target: inout T, count: Int) { 64 | _ = withUnsafeMutablePointer(to: &target) { 65 | self.read($0, maxLength: count) 66 | } 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /BitcoinClient/Sources/BitcoinClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Network 3 | 4 | class BitcoinClient { 5 | 6 | private let urlSession = URLSession.init(configuration: .ephemeral) 7 | 8 | private let host: String 9 | private let port: Int 10 | 11 | init(host: String, port: Int) { 12 | self.host = host 13 | self.port = port 14 | } 15 | 16 | func getVersionMessage() async throws -> VersionMessage { 17 | let streamTask = urlSession.streamTask(withHostName: host, port: port) 18 | streamTask.resume() 19 | 20 | try await streamTask.write(clientVersionMessage) 21 | 22 | guard let headerData = try await streamTask.readData(length: MessageHeader.size) else { 23 | throw BitcoinClientError.communicationFailure 24 | } 25 | 26 | let header = MessageHeader(from: headerData) 27 | 28 | guard header.magicNumber == MagicNumber.main.rawValue else { 29 | throw BitcoinClientError.invalidMagicNumber 30 | } 31 | guard header.command == VersionMessage.command else { 32 | throw BitcoinClientError.protocolViolation 33 | } 34 | 35 | guard let messageData = try await streamTask.readData(length: Int(header.payloadSize)) else { 36 | throw BitcoinClientError.communicationFailure 37 | } 38 | guard messageData.checksum == header.payloadChecksum else { 39 | throw BitcoinClientError.protocolViolation 40 | } 41 | 42 | return VersionMessage(from: messageData) 43 | } 44 | 45 | private var clientVersionMessage: VersionMessage { 46 | return VersionMessage( 47 | protocolVersion: 70001, 48 | timestamp: Date(), 49 | nonce: UInt64.random(in: UInt64.min...UInt64.max), 50 | userAgent: "/BitcoinClient:1.0.0/" 51 | ) 52 | } 53 | 54 | } 55 | 56 | enum BitcoinClientError: Error { 57 | case communicationFailure 58 | case invalidMagicNumber 59 | case protocolViolation 60 | } 61 | -------------------------------------------------------------------------------- /BitcoinWidgets.xcodeproj/xcshareddata/xcschemes/Tests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 16 | 18 | 24 | 25 | 26 | 27 | 28 | 38 | 39 | 45 | 46 | 48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /BitcoinClient/Tests/Extensions/InputStreamTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | class InputStreamTests: XCTestCase { 4 | 5 | func testReadBool() throws { 6 | let input = InputStream(data: Data([0x00, 0x01, 0xFA])) 7 | input.open(); defer { input.close() } 8 | 9 | XCTAssertFalse(input.readBool()) 10 | XCTAssertTrue(input.readBool()) 11 | XCTAssertTrue(input.readBool()) 12 | } 13 | 14 | func testReadUInt16() throws { 15 | let input = InputStream(data: Data([0x01, 0x02])) 16 | input.open(); defer { input.close() } 17 | 18 | XCTAssertEqual(input.readUInt16(), UInt16(0x0102)) 19 | } 20 | 21 | func testReadInt32() throws { 22 | let input = InputStream(data: Data([0x01, 0x02, 0x03, 0x04])) 23 | input.open(); defer { input.close() } 24 | 25 | XCTAssertEqual(input.readInt32(), Int32(0x04030201)) 26 | } 27 | 28 | func testReadUInt32() throws { 29 | let input = InputStream(data: Data([0x01, 0x02, 0x03, 0x04])) 30 | input.open(); defer { input.close() } 31 | 32 | XCTAssertEqual(input.readUInt32(), UInt32(0x04030201)) 33 | } 34 | 35 | func testReadInt64() throws { 36 | let input = InputStream(data: Data([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08])) 37 | input.open(); defer { input.close() } 38 | 39 | XCTAssertEqual(input.readInt64(), Int64(0x0807060504030201)) 40 | } 41 | 42 | func testReadUInt64() throws { 43 | let input = InputStream(data: Data([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08])) 44 | input.open(); defer { input.close() } 45 | 46 | XCTAssertEqual(input.readUInt64(), UInt64(0x0807060504030201)) 47 | } 48 | 49 | func testReadString() throws { 50 | let input = InputStream(data: Data([0x03, 0x66, 0x6F, 0x6F])) 51 | input.open(); defer { input.close() } 52 | 53 | XCTAssertEqual(input.readString(), "foo") 54 | } 55 | 56 | func testReadEmptyString() throws { 57 | let input = InputStream(data: Data([0x00])) 58 | input.open(); defer { input.close() } 59 | 60 | XCTAssertEqual(input.readString(), "") 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /Widgets/Sources/MempoolStatusProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import WidgetKit 3 | 4 | struct MempoolStatusProvider: IntentTimelineProvider { 5 | 6 | private let mempoolClient = MempoolClient() 7 | 8 | @available(iOSApplicationExtension 16.0, watchOS 9.0, *) 9 | func recommendations() -> [IntentRecommendation] { 10 | return [IntentRecommendation(intent: MempoolConfigurationIntent(), description: "MempoolStatusWidget.displayName")] 11 | } 12 | 13 | func placeholder(in context: Context) -> MempoolStatus { 14 | return MempoolStatus(showBitcoinLogo: true, blockHeight: 756569, fastestFee: 17, halfHourFee: 8, hourFee: 3, economyFee: 1, minimumFee: 1) 15 | } 16 | 17 | func getSnapshot(for configuration: MempoolConfigurationIntent, in context: Context, completion: @escaping (MempoolStatus) -> ()) { 18 | if context.isPreview { 19 | completion(placeholder(in: context)) 20 | return 21 | } 22 | 23 | Task.init { 24 | completion(try await getMempoolStatus(showBitcoinLogo: configuration.showBitcoinLogo)) 25 | } 26 | } 27 | 28 | func getTimeline(for configuration: MempoolConfigurationIntent, in context: Context, completion: @escaping (Timeline) -> ()) { 29 | Task.init { 30 | let mempoolStatus = try await getMempoolStatus(showBitcoinLogo: configuration.showBitcoinLogo) 31 | let nextUpdate = Calendar.current.date(byAdding: .minute, value: 1, to: Date())! 32 | completion(Timeline(entries: [mempoolStatus], policy: .after(nextUpdate))) 33 | } 34 | } 35 | 36 | func getMempoolStatus(showBitcoinLogo: NSNumber?) async throws -> MempoolStatus { 37 | let blockHeight = try await mempoolClient.getBlockHeight() 38 | let recommendedFees = try await mempoolClient.getRecommendedFees() 39 | 40 | return MempoolStatus( 41 | showBitcoinLogo: Bool(truncating: showBitcoinLogo ?? true), 42 | blockHeight: blockHeight, 43 | fastestFee: recommendedFees.fastestFee, 44 | halfHourFee: recommendedFees.halfHourFee, 45 | hourFee: recommendedFees.hourFee, 46 | economyFee: recommendedFees.economyFee, 47 | minimumFee: recommendedFees.minimumFee 48 | ) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Widgets/Sources/CombinedStatusView.swift: -------------------------------------------------------------------------------- 1 | import WidgetKit 2 | import SwiftUI 3 | import Intents 4 | 5 | struct CombinedStatusView: View { 6 | 7 | var combinedStatus: CombinedStatusProvider.Entry 8 | 9 | var body: some View { 10 | if #available(iOSApplicationExtension 17.0, *) { 11 | HStack(spacing: 42) { 12 | NodeStatusView(nodeStatus: combinedStatus.nodeStatus) 13 | .systemView(for: .systemSmall) 14 | MempoolStatusView(mempoolStatus: combinedStatus.mempoolStatus) 15 | .systemView(for: .systemSmall) 16 | }.containerBackground(for: .widget) { 17 | BitcoinBackground(family: .systemLarge, showLogo: combinedStatus.nodeStatus.showBitcoinLogo) 18 | } 19 | } else { 20 | ZStack { 21 | BitcoinBackground(family: .systemLarge, showLogo: combinedStatus.nodeStatus.showBitcoinLogo) 22 | HStack { 23 | NodeStatusView(nodeStatus: combinedStatus.nodeStatus) 24 | .systemView(for: .systemSmall).padding() 25 | MempoolStatusView(mempoolStatus: combinedStatus.mempoolStatus) 26 | .systemView(for: .systemSmall).padding() 27 | } 28 | } 29 | } 30 | } 31 | 32 | } 33 | 34 | struct CombinedStatusView_Previews: PreviewProvider { 35 | 36 | static var previews: some View { 37 | Group { 38 | CombinedStatusView( 39 | combinedStatus: CombinedStatus(nodeStatus: NodeStatus(showBitcoinLogo: true, blockHeight: 755237, userAgent: "/Satoshi:23.0.0/", protocolVersion: 70016), 40 | mempoolStatus: MempoolStatus(showBitcoinLogo: false, blockHeight: 755237, fastestFee: 17, halfHourFee: 8, hourFee: 3, economyFee: 1, minimumFee: 1)) 41 | ).previewContext(WidgetPreviewContext(family: .systemMedium)) 42 | CombinedStatusView( 43 | combinedStatus: CombinedStatus(nodeStatus: NodeStatus(showBitcoinLogo: false, blockHeight: 755237, userAgent: "/Satoshi:23.0.0/", protocolVersion: 70016), 44 | mempoolStatus: MempoolStatus(showBitcoinLogo: true, blockHeight: 755237, fastestFee: 2791, halfHourFee: 730, hourFee: 130, economyFee: 37, minimumFee: 19)) 45 | ).previewContext(WidgetPreviewContext(family: .systemMedium)) 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Widgets/Sources/HalvingCountdownProvider.swift: -------------------------------------------------------------------------------- 1 | import WidgetKit 2 | 3 | struct HalvingCountdownProvider: IntentTimelineProvider { 4 | 5 | private static let blocksPerEpoch: Int32 = 210000 6 | 7 | private let mempoolClient = MempoolClient() 8 | 9 | @available(iOSApplicationExtension 16.0, watchOS 9.0, *) 10 | func recommendations() -> [IntentRecommendation] { 11 | return [IntentRecommendation(intent: MempoolConfigurationIntent(), description: "HalvingCountdownWidget.displayName")] 12 | } 13 | 14 | func placeholder(in context: Context) -> HalvingCountdown { 15 | return HalvingCountdown(showBitcoinLogo: true, blockCount: 13842, estimate: Date(timeIntervalSince1970: 1713954060)) 16 | } 17 | 18 | func getSnapshot(for configuration: MempoolConfigurationIntent, in context: Context, completion: @escaping (HalvingCountdown) -> ()) { 19 | if context.isPreview { 20 | completion(placeholder(in: context)) 21 | return 22 | } 23 | 24 | Task.init { 25 | completion(try await getHalvingCountdown(showBitcoinLogo: configuration.showBitcoinLogo)) 26 | } 27 | } 28 | 29 | func getTimeline(for configuration: MempoolConfigurationIntent, in context: Context, completion: @escaping (Timeline) -> ()) { 30 | Task.init { 31 | let halvingCountdown = try await getHalvingCountdown(showBitcoinLogo: configuration.showBitcoinLogo) 32 | let nextUpdate = Calendar.current.date(byAdding: .minute, value: 1, to: Date())! 33 | completion(Timeline(entries: [halvingCountdown], policy: .after(nextUpdate))) 34 | } 35 | } 36 | 37 | func getHalvingCountdown(showBitcoinLogo: NSNumber?) async throws -> HalvingCountdown { 38 | let blockHeight = try await mempoolClient.getBlockHeight() 39 | let blockCount = Self.blocksPerEpoch - blockHeight % Self.blocksPerEpoch 40 | 41 | let secondsPerThreeWeeks = TimeInterval(21 * 24 * 60 * 60) 42 | let threeWeeksAgo = Date(timeIntervalSinceNow: -secondsPerThreeWeeks) 43 | let blockHeightThreeWeeksAgo = try await mempoolClient.getBlockHeightByDate(threeWeeksAgo) 44 | let secondsPerBlock = secondsPerThreeWeeks / Double(blockHeight - blockHeightThreeWeeksAgo + 1) 45 | 46 | return HalvingCountdown( 47 | showBitcoinLogo: Bool(truncating: showBitcoinLogo ?? true), 48 | blockCount: NSNumber(value: blockCount), 49 | estimate: Date(timeIntervalSinceNow: Double(blockCount) * secondsPerBlock) 50 | ) 51 | } 52 | 53 | } 54 | 55 | -------------------------------------------------------------------------------- /Widgets/Sources/NodeStatusProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import WidgetKit 3 | 4 | struct NodeStatusProvider: IntentTimelineProvider { 5 | 6 | private let bitnodesClient = BitnodesClient() 7 | 8 | func placeholder(in context: Context) -> NodeStatus { 9 | return NodeStatus(showBitcoinLogo: true, blockHeight: 756569, userAgent: "/Satoshi:23.0.0/", protocolVersion: 70016) 10 | } 11 | 12 | func getSnapshot(for configuration: NodeConfigurationIntent, in context: Context, completion: @escaping (NodeStatus) -> ()) { 13 | if context.isPreview { 14 | completion(placeholder(in: context)) 15 | return 16 | } 17 | 18 | Task.init { 19 | completion(await getNodeStatus(for: configuration)) 20 | } 21 | } 22 | 23 | func getTimeline(for configuration: NodeConfigurationIntent, in context: Context, completion: @escaping (Timeline) -> ()) { 24 | Task.init { 25 | let nodeStatus = await getNodeStatus(for: configuration) 26 | let nextUpdate = Calendar.current.date(byAdding: .minute, value: 1, to: Date())! 27 | completion(Timeline(entries: [nodeStatus], policy: .after(nextUpdate))) 28 | } 29 | } 30 | 31 | func getNodeStatus(for configuration: NodeConfigurationIntent) async -> NodeStatus { 32 | let showBitcoinLogo = Bool(truncating: configuration.showBitcoinLogo ?? true) 33 | 34 | if configuration.host == nil || configuration.port == nil { 35 | return NodeStatus(showBitcoinLogo: showBitcoinLogo, error: .configurationRequired) 36 | } 37 | 38 | let host = configuration.host! 39 | let port = Int(truncating: configuration.port!) 40 | 41 | if host.hasSuffix(".onion") { 42 | guard let cachedStatus = try? await bitnodesClient.getNodeStatus(host: host, port: port), 43 | let currentStatus = try? await bitnodesClient.checkNode(host: host, port: port) else { 44 | return NodeStatus(showBitcoinLogo: showBitcoinLogo, error: .nodeUnreachable) 45 | } 46 | 47 | return NodeStatus( 48 | showBitcoinLogo: showBitcoinLogo, 49 | blockHeight: currentStatus.height, 50 | userAgent: cachedStatus.userAgent, 51 | protocolVersion: cachedStatus.protocolVersion 52 | ) 53 | } 54 | 55 | let client = BitcoinClient(host: host, port: port) 56 | 57 | guard let versionMessage = try? await client.getVersionMessage() else { 58 | return NodeStatus(showBitcoinLogo: showBitcoinLogo, error: .nodeUnreachable) 59 | } 60 | 61 | return NodeStatus( 62 | showBitcoinLogo: showBitcoinLogo, 63 | blockHeight: versionMessage.blockHeight, 64 | userAgent: versionMessage.userAgent, 65 | protocolVersion: versionMessage.protocolVersion 66 | ) 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /Widgets/Sources/MoscowTimeProvider.swift: -------------------------------------------------------------------------------- 1 | import WidgetKit 2 | 3 | struct MoscowTimeProvider: IntentTimelineProvider { 4 | 5 | private let blockchainClient = BlockchainClient() 6 | 7 | @available(iOSApplicationExtension 16.0, watchOS 9.0, *) 8 | func recommendations() -> [IntentRecommendation] { 9 | let configuration = MoscowTimeConfigurationIntent() 10 | configuration.format = .time 11 | configuration.primaryCurrency = .usd 12 | configuration.secondaryCurrency = .eur 13 | 14 | return [IntentRecommendation(intent: configuration, description: "MoscowTimeWidget.displayName")] 15 | } 16 | 17 | func placeholder(in context: Context) -> MoscowTime { 18 | return MoscowTime(showBitcoinLogo: true, format: .time, primaryPrice: 51229.50, primaryCurrencyCode: "USD", secondaryPrice: 43546.19, secondaryCurrencyCode: "EUR") 19 | } 20 | 21 | func getSnapshot(for configuration: MoscowTimeConfigurationIntent, in context: Context, completion: @escaping (MoscowTime) -> ()) { 22 | if context.isPreview { 23 | completion(placeholder(in: context)) 24 | return 25 | } 26 | 27 | Task.init { 28 | completion(try await getMoscowTime(for: configuration)) 29 | } 30 | } 31 | 32 | func getTimeline(for configuration: MoscowTimeConfigurationIntent, in context: Context, completion: @escaping (Timeline) -> ()) { 33 | Task.init { 34 | let moscowTime = try await getMoscowTime(for: configuration) 35 | let nextUpdate = Calendar.current.date(byAdding: .minute, value: 1, to: Date())! 36 | completion(Timeline(entries: [moscowTime], policy: .after(nextUpdate))) 37 | } 38 | } 39 | 40 | func getMoscowTime(for configuration: MoscowTimeConfigurationIntent) async throws -> MoscowTime { 41 | let tickers = try await blockchainClient.getTickers() 42 | let primaryCurrencyCode = configuration.primaryCurrency.code 43 | let secondaryCurrencyCode = configuration.secondaryCurrency.code 44 | 45 | return MoscowTime( 46 | showBitcoinLogo: Bool(truncating: configuration.showBitcoinLogo ?? true), 47 | format: configuration.format, 48 | primaryPrice: NSNumber(value: tickers[primaryCurrencyCode]!.last), 49 | primaryCurrencyCode: primaryCurrencyCode, 50 | secondaryPrice: NSNumber(value: tickers[secondaryCurrencyCode]!.last), 51 | secondaryCurrencyCode: secondaryCurrencyCode 52 | ) 53 | } 54 | 55 | } 56 | 57 | extension FiatCurrency { 58 | 59 | var code: String { 60 | switch self { 61 | case .cad: 62 | return "CAD" 63 | case .chf: 64 | return "CHF" 65 | case .eur: 66 | return "EUR" 67 | case .gbp: 68 | return "GBP" 69 | case .usd: 70 | return "USD" 71 | default: 72 | return "?" 73 | } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /App/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "BitcoinWidgets_40px-2.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "BitcoinWidgets_60px.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "BitcoinWidgets_58px.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "BitcoinWidgets_87px.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "BitcoinWidgets_80px.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "BitcoinWidgets_120px-1.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "BitcoinWidgets_120px.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "BitcoinWidgets_180px.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "BitcoinWidgets_20px.png", 53 | "idiom" : "ipad", 54 | "scale" : "1x", 55 | "size" : "20x20" 56 | }, 57 | { 58 | "filename" : "BitcoinWidgets_40px-1.png", 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "BitcoinWidgets_29px.png", 65 | "idiom" : "ipad", 66 | "scale" : "1x", 67 | "size" : "29x29" 68 | }, 69 | { 70 | "filename" : "BitcoinWidgets_58px-1.png", 71 | "idiom" : "ipad", 72 | "scale" : "2x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "BitcoinWidgets_40px.png", 77 | "idiom" : "ipad", 78 | "scale" : "1x", 79 | "size" : "40x40" 80 | }, 81 | { 82 | "filename" : "BitcoinWidgets_80px-1.png", 83 | "idiom" : "ipad", 84 | "scale" : "2x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "BitcoinWidgets_76px.png", 89 | "idiom" : "ipad", 90 | "scale" : "1x", 91 | "size" : "76x76" 92 | }, 93 | { 94 | "filename" : "BitcoinWidgets_152px.png", 95 | "idiom" : "ipad", 96 | "scale" : "2x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "BitcoinWidgets_167px.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "83.5x83.5" 104 | }, 105 | { 106 | "filename" : "BitcoinWidgets_1024px.png", 107 | "idiom" : "ios-marketing", 108 | "scale" : "1x", 109 | "size" : "1024x1024" 110 | } 111 | ], 112 | "info" : { 113 | "author" : "xcode", 114 | "version" : 1 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /BitcoinWidgets.xcodeproj/xcshareddata/xcschemes/BitcoinWidgets.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 44 | 50 | 51 | 52 | 53 | 59 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /BitcoinClient/Tests/VersionMessageTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Network 3 | 4 | class VersionMessageTests: XCTestCase { 5 | 6 | private let dateFormatter = ISO8601DateFormatter() 7 | 8 | func testInitFromData() throws { 9 | let data = Data([ 10 | // protocolVersion 11 | 0x80, 0x11, 0x01, 0x00, 12 | // services 13 | 0x0D, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 14 | // timestamp 15 | 0x73, 0x59, 0xA7, 0x62, 0x00, 0x00, 0x00, 0x00, 16 | // recipient 17 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 18 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 19 | 0x00, 0x00, 20 | // sender 21 | 0x0D, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 22 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 23 | 0x20, 0x8D, 24 | // nonce 25 | 0x13, 0x57, 0xB4, 0x3A, 0x2C, 0x20, 0x9D, 0xDD, 26 | // userAgent 27 | 0x10, 0x2F, 0x53, 0x61, 0x74, 0x6F, 0x73, 0x68, 0x69, 0x3A, 0x32, 0x33, 0x2E, 0x30, 0x2E, 0x30, 0x2F, 28 | // blockHeight 29 | 0xF5, 0x4C, 0x0B, 0x00, 30 | // relayTxs 31 | 0x01 32 | ]) 33 | 34 | let message = VersionMessage(from: data) 35 | 36 | XCTAssertEqual(message.protocolVersion, 70016) 37 | XCTAssertEqual(message.services, 1037) 38 | XCTAssertEqual(message.timestamp, dateFormatter.date(from: "2022-06-13T15:36:19Z")) 39 | XCTAssertEqual(message.recipient.services, 0) 40 | XCTAssertEqual(message.recipient.address, IPv6Address.any) 41 | XCTAssertEqual(message.recipient.port, 0) 42 | XCTAssertEqual(message.sender.services, 1037) 43 | XCTAssertEqual(message.sender.address, IPv6Address.any) 44 | XCTAssertEqual(message.sender.port, 8333) 45 | XCTAssertEqual(message.nonce, 0xDD9D202C3AB45713) 46 | XCTAssertEqual(message.userAgent, "/Satoshi:23.0.0/") 47 | XCTAssertEqual(message.blockHeight, 740597) 48 | XCTAssertTrue(message.relayTxs) 49 | } 50 | 51 | func testToData() throws { 52 | let message = VersionMessage( 53 | protocolVersion: 70001, 54 | timestamp: dateFormatter.date(from: "2022-06-13T15:36:19Z")!, 55 | nonce: 0xDD9D202C3AB45713, 56 | userAgent: "/Satoshi:22.0.0/" 57 | ) 58 | 59 | XCTAssertEqual(message.data, Data([ 60 | // protocolVersion 61 | 0x71, 0x11, 0x01, 0x00, 62 | // services 63 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 64 | // timestamp 65 | 0x73, 0x59, 0xA7, 0x62, 0x00, 0x00, 0x00, 0x00, 66 | // recipient 67 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 68 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 69 | 0x00, 0x00, 70 | // sender 71 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 72 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 73 | 0x00, 0x00, 74 | // nonce 75 | 0x13, 0x57, 0xB4, 0x3A, 0x2C, 0x20, 0x9D, 0xDD, 76 | // userAgent 77 | 0x10, 0x2F, 0x53, 0x61, 0x74, 0x6F, 0x73, 0x68, 0x69, 0x3A, 0x32, 0x32, 0x2E, 0x30, 0x2E, 0x30, 0x2F, 78 | // blockHeight 79 | 0x00, 0x00, 0x00, 0x00, 80 | // relayTxs 81 | 0x00 82 | ])) 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /BitcoinWidgets.xcodeproj/xcshareddata/xcschemes/watchOS App.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 56 | 58 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Widgets/Sources/HalvingCountdownView.swift: -------------------------------------------------------------------------------- 1 | import WidgetKit 2 | import SwiftUI 3 | 4 | struct HalvingCountdownView: View { 5 | 6 | @Environment(\.widgetFamily) var family: WidgetFamily 7 | var halvingCountdown: HalvingCountdownProvider.Entry 8 | 9 | var body: some View { 10 | #if os(watchOS) 11 | accessoryView(for: family) 12 | #else 13 | if #available(iOSApplicationExtension 17.0, *) { 14 | if family == .accessoryRectangular || family == .accessoryInline { 15 | accessoryView(for: family).containerBackground(for: .widget) {} 16 | } else { 17 | systemView(for: family).containerBackground(for: .widget) { 18 | BitcoinBackground(family: family, showLogo: halvingCountdown.showBitcoinLogo) 19 | } 20 | } 21 | } else if #available(iOSApplicationExtension 16.0, *), family == .accessoryRectangular || family == .accessoryInline { 22 | accessoryView(for: family) 23 | } else { 24 | ZStack { 25 | BitcoinBackground(family: family, showLogo: halvingCountdown.showBitcoinLogo) 26 | systemView(for: family).padding() 27 | } 28 | } 29 | #endif 30 | } 31 | 32 | @available(iOSApplicationExtension 16.0, *) 33 | func accessoryView(for family: WidgetFamily) -> some View { 34 | VStack(alignment: .trailing) { 35 | Text(halvingCountdown.blockCount, formatter: numberFormatter()) 36 | .font(.system(size: 37)) 37 | Text(halvingCountdown.estimate, formatter: dateFormatter()) 38 | .font(.footnote) 39 | .padding(.trailing, 2) 40 | } 41 | } 42 | 43 | #if !os(watchOS) 44 | func systemView(for family: WidgetFamily) -> some View { 45 | VStack(alignment: family == .systemSmall ? .trailing : .center) { 46 | Text(halvingCountdown.blockCount, formatter: numberFormatter()) 47 | .font(family == .systemSmall ? .title : .largeTitle) 48 | .foregroundColor(.yellow) 49 | .padding(.bottom, 1) 50 | VStack(alignment: .trailing) { 51 | Text(halvingCountdown.estimate, formatter: dateFormatter()) 52 | .foregroundColor(.white) 53 | Text(verbatim: etaFormatter().string(from: Date(), to: halvingCountdown.estimate)!) 54 | .foregroundColor(.gray) 55 | }.font(family == .systemSmall ? .footnote : .body) 56 | .padding(.trailing, family == .systemSmall ? 2 : 0) 57 | } 58 | } 59 | #endif 60 | 61 | private func numberFormatter() -> NumberFormatter { 62 | let formatter = NumberFormatter() 63 | formatter.numberStyle = .decimal 64 | formatter.maximumFractionDigits = 0 65 | 66 | return formatter 67 | } 68 | 69 | private func dateFormatter() -> DateFormatter { 70 | let formatter = DateFormatter() 71 | formatter.dateStyle = .short 72 | formatter.timeStyle = .short 73 | 74 | return formatter 75 | } 76 | 77 | private func etaFormatter() -> DateComponentsFormatter { 78 | let formatter = DateComponentsFormatter() 79 | formatter.unitsStyle = .abbreviated 80 | formatter.allowedUnits = [.year, .month, .day, .hour, .minute] 81 | formatter.maximumUnitCount = 3 82 | 83 | return formatter 84 | } 85 | 86 | } 87 | 88 | struct HalvingCountdownView_Previews: PreviewProvider { 89 | 90 | static var previews: some View { 91 | Group { 92 | #if !os(watchOS) 93 | HalvingCountdownView(halvingCountdown: HalvingCountdown(showBitcoinLogo: true, blockCount: 13842, estimate: Date(timeIntervalSince1970: 1713954060))) 94 | .previewContext(WidgetPreviewContext(family: .systemMedium)) 95 | 96 | HalvingCountdownView(halvingCountdown: HalvingCountdown(showBitcoinLogo: false, blockCount: 13842, estimate: Date(timeIntervalSince1970: 1713954060))) 97 | .previewContext(WidgetPreviewContext(family: .systemSmall)) 98 | #endif 99 | 100 | if #available(iOSApplicationExtension 16.0, *) { 101 | HalvingCountdownView(halvingCountdown: HalvingCountdown(showBitcoinLogo: true, blockCount: 13842, estimate: Date(timeIntervalSince1970: 1713954060))) 102 | .previewContext(WidgetPreviewContext(family: .accessoryRectangular)) 103 | } 104 | } 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /BitcoinWidgets.xcodeproj/xcshareddata/xcschemes/iOS Widget Extension.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 58 | 60 | 66 | 67 | 68 | 69 | 73 | 74 | 78 | 79 | 83 | 84 | 85 | 86 | 93 | 95 | 101 | 102 | 103 | 104 | 106 | 107 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /App/Sources/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ContentView: View { 4 | 5 | var body: some View { 6 | if #available(iOS 16.0, watchOS 9.0, *) { 7 | NavigationStack { content } 8 | } else { 9 | NavigationView { content } 10 | } 11 | } 12 | 13 | private var content: some View { 14 | List { 15 | Section(header: Text("ContentView.AddTo.header").font(.title2)) { 16 | #if os(watchOS) 17 | watchFaceSteps 18 | #else 19 | if #available(iOS 16.0, *) { 20 | watchFaceSteps 21 | lockScreenSteps 22 | } 23 | homeScreenSteps 24 | todayViewSteps 25 | #endif 26 | } 27 | #if !os(watchOS) 28 | Section(header: Text("ContentView.Configure.header").font(.title2)) { 29 | Label("ContentView.Configure.step1", systemImage: "1.circle.fill") 30 | Label("ContentView.Configure.step2", systemImage: "2.circle.fill") 31 | } 32 | #endif 33 | Section(header: Text("ContentView.FindOutMore.header").font(.title2)) { 34 | LabelLink("ContentView.FindOutMore.sourceCode", url: "https://github.com/yanascz/bitcoin-widgets-ios", 35 | systemImage: "chevron.left.forwardslash.chevron.right") 36 | LabelLink("ContentView.FindOutMore.issueTracking", url: "https://github.com/yanascz/bitcoin-widgets-ios/issues", 37 | systemImage: "ladybug.fill") 38 | } 39 | Section(header: Text("ContentView.Support.header").font(.title2)) { 40 | LabelLink("widgets@bitcoinwidgets.app", url: "lightning:widgets@bitcoinwidgets.app", systemImage: "bolt.fill") 41 | } 42 | }.navigationTitle("ContentView.title").listStyle(.plain) 43 | } 44 | 45 | @available(iOS 16.0, *) 46 | private var watchFaceSteps: some View { 47 | NavigationLink { 48 | List { 49 | Label("ContentView.AddTo.WatchFace.step1", systemImage: "1.circle.fill") 50 | Label("ContentView.AddTo.WatchFace.step2", systemImage: "2.circle.fill") 51 | Label("ContentView.AddTo.WatchFace.step3", systemImage: "3.circle.fill") 52 | Label("ContentView.AddTo.WatchFace.step4", systemImage: "4.circle.fill") 53 | Label("ContentView.AddTo.WatchFace.step5", systemImage: "5.circle.fill") 54 | Label("ContentView.AddTo.WatchFace.step6", systemImage: "6.circle.fill") 55 | Label("ContentView.AddTo.WatchFace.step7", systemImage: "7.circle.fill") 56 | }.navigationTitle("ContentView.AddTo.WatchFace.header").listStyle(.plain) 57 | } label: { 58 | Label("ContentView.AddTo.WatchFace.link", systemImage: "plus.circle.fill") 59 | } 60 | } 61 | 62 | @available(iOS 16.0, *) 63 | private var lockScreenSteps: some View { 64 | NavigationLink { 65 | List { 66 | Label("ContentView.AddTo.LockScreen.step1", systemImage: "1.circle.fill") 67 | Label("ContentView.AddTo.LockScreen.step2", systemImage: "2.circle.fill") 68 | Label("ContentView.AddTo.LockScreen.step3", systemImage: "3.circle.fill") 69 | Label("ContentView.AddTo.LockScreen.step4", systemImage: "4.circle.fill") 70 | Label("ContentView.AddTo.LockScreen.step5", systemImage: "5.circle.fill") 71 | }.navigationTitle("ContentView.AddTo.LockScreen.header").listStyle(.plain) 72 | } label: { 73 | Label("ContentView.AddTo.LockScreen.link", systemImage: "plus.circle.fill") 74 | } 75 | } 76 | 77 | private var homeScreenSteps: some View { 78 | NavigationLink { 79 | List { 80 | Label("ContentView.AddTo.HomeScreen.step1", systemImage: "1.circle.fill") 81 | Label("ContentView.AddTo.HomeScreen.step2", systemImage: "2.circle.fill") 82 | Label("ContentView.AddTo.HomeScreen.step3", systemImage: "3.circle.fill") 83 | Label("ContentView.AddTo.HomeScreen.step4", systemImage: "4.circle.fill") 84 | }.navigationTitle("ContentView.AddTo.HomeScreen.header").listStyle(.plain) 85 | } label: { 86 | Label("ContentView.AddTo.HomeScreen.link", systemImage: "plus.circle.fill") 87 | } 88 | } 89 | 90 | private var todayViewSteps: some View { 91 | NavigationLink { 92 | List { 93 | Label("ContentView.AddTo.TodayView.step1", systemImage: "1.circle.fill") 94 | Label("ContentView.AddTo.TodayView.step2", systemImage: "2.circle.fill") 95 | Label("ContentView.AddTo.TodayView.step3", systemImage: "3.circle.fill") 96 | Label("ContentView.AddTo.TodayView.step4", systemImage: "4.circle.fill") 97 | }.navigationTitle("ContentView.AddTo.TodayView.header").listStyle(.plain) 98 | } label: { 99 | Label("ContentView.AddTo.TodayView.link", systemImage: "plus.circle.fill") 100 | } 101 | } 102 | 103 | } 104 | 105 | struct ContentView_Previews: PreviewProvider { 106 | static var previews: some View { 107 | ContentView() 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /BitcoinWidgets.xcodeproj/xcshareddata/xcschemes/watchOS Widget Extension.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 59 | 60 | 72 | 74 | 80 | 81 | 82 | 83 | 87 | 88 | 92 | 93 | 97 | 98 | 99 | 100 | 106 | 108 | 114 | 115 | 116 | 117 | 119 | 120 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /Widgets/Sources/NodeStatusView.swift: -------------------------------------------------------------------------------- 1 | import WidgetKit 2 | import SwiftUI 3 | import Intents 4 | 5 | struct NodeStatusView: View { 6 | 7 | @Environment(\.widgetFamily) var family: WidgetFamily 8 | var nodeStatus: NodeStatusProvider.Entry 9 | 10 | var body: some View { 11 | if #available(iOSApplicationExtension 17.0, *) { 12 | if family == .accessoryRectangular || family == .accessoryInline { 13 | accessoryView(for: family).containerBackground(for: .widget) {} 14 | } else { 15 | systemView(for: family).containerBackground(for: .widget) { 16 | BitcoinBackground(family: family, showLogo: nodeStatus.showBitcoinLogo) 17 | } 18 | } 19 | } else if #available(iOSApplicationExtension 16.0, *), family == .accessoryRectangular || family == .accessoryInline { 20 | accessoryView(for: family) 21 | } else { 22 | ZStack { 23 | BitcoinBackground(family: family, showLogo: nodeStatus.showBitcoinLogo) 24 | systemView(for: family).padding() 25 | } 26 | } 27 | } 28 | 29 | @available(iOSApplicationExtension 16.0, *) 30 | func accessoryView(for family: WidgetFamily) -> some View { 31 | VStack(alignment: .trailing) { 32 | if let blockHeight = nodeStatus.blockHeight, 33 | let userAgent = nodeStatus.userAgent, 34 | let protocolVersion = nodeStatus.protocolVersion { 35 | if family == .accessoryInline { 36 | Label(String(blockHeight), systemImage: "bitcoinsign.square.fill") 37 | } else { 38 | Text(verbatim: String(blockHeight)) 39 | .font(.system(size: 37)) 40 | Text(verbatim: "\(userAgent)\(protocolVersion)") 41 | .font(.footnote) 42 | .foregroundColor(.secondary) 43 | .padding(.trailing, 2) 44 | } 45 | } else if let error = nodeStatus.error { 46 | if family == .accessoryInline { 47 | Label(error.localizedDescription, systemImage: "exclamationmark.triangle.fill") 48 | } else { 49 | Text(error.localizedDescription) 50 | } 51 | } 52 | } 53 | } 54 | 55 | func systemView(for family: WidgetFamily) -> some View { 56 | VStack(alignment: family == .systemSmall ? .trailing : .center) { 57 | if let blockHeight = nodeStatus.blockHeight, 58 | let userAgent = nodeStatus.userAgent, 59 | let protocolVersion = nodeStatus.protocolVersion { 60 | Text(verbatim: String(blockHeight)) 61 | .font(family == .systemSmall ? .title : .largeTitle) 62 | .foregroundColor(Color("AccentColor")) 63 | .padding(.bottom, 1) 64 | VStack(alignment: .trailing) { 65 | Text(verbatim: userAgent) 66 | Text(verbatim: "(\(protocolVersion))") 67 | }.font(family == .systemSmall ? .footnote : .body) 68 | .foregroundColor(.gray) 69 | .padding(.trailing, family == .systemSmall ? 2 : 0) 70 | } else if let error = nodeStatus.error { 71 | Text(error.localizedDescription) 72 | .foregroundColor(Color.white) 73 | } 74 | } 75 | } 76 | 77 | } 78 | 79 | struct NodeStatusView_Previews: PreviewProvider { 80 | 81 | static var previews: some View { 82 | Group { 83 | NodeStatusView(nodeStatus: NodeStatus(showBitcoinLogo: true, error: .configurationRequired)) 84 | .previewContext(WidgetPreviewContext(family: .systemMedium)) 85 | NodeStatusView(nodeStatus: NodeStatus(showBitcoinLogo: false, error: .nodeUnreachable)) 86 | .previewContext(WidgetPreviewContext(family: .systemMedium)) 87 | NodeStatusView(nodeStatus: NodeStatus(showBitcoinLogo: true, blockHeight: 754091, userAgent: "/Satoshi:23.0.0/", protocolVersion: 70016)) 88 | .previewContext(WidgetPreviewContext(family: .systemMedium)) 89 | 90 | NodeStatusView(nodeStatus: NodeStatus(showBitcoinLogo: true, error: .configurationRequired)) 91 | .previewContext(WidgetPreviewContext(family: .systemSmall)) 92 | NodeStatusView(nodeStatus: NodeStatus(showBitcoinLogo: false, error: .nodeUnreachable)) 93 | .previewContext(WidgetPreviewContext(family: .systemSmall)) 94 | NodeStatusView(nodeStatus: NodeStatus(showBitcoinLogo: true, blockHeight: 754091, userAgent: "/Satoshi:22.99.0/", protocolVersion: 70016)) 95 | .previewContext(WidgetPreviewContext(family: .systemSmall)) 96 | 97 | if #available(iOSApplicationExtension 16.0, *) { 98 | NodeStatusView(nodeStatus: NodeStatus(showBitcoinLogo: true, error: .configurationRequired)) 99 | .previewContext(WidgetPreviewContext(family: .accessoryRectangular)) 100 | NodeStatusView(nodeStatus: NodeStatus(showBitcoinLogo: false, error: .nodeUnreachable)) 101 | .previewContext(WidgetPreviewContext(family: .accessoryRectangular)) 102 | NodeStatusView(nodeStatus: NodeStatus(showBitcoinLogo: true, blockHeight: 754091, userAgent: "/Satoshi:23.0.0/", protocolVersion: 70016)) 103 | .previewContext(WidgetPreviewContext(family: .accessoryRectangular)) 104 | 105 | NodeStatusView(nodeStatus: NodeStatus(showBitcoinLogo: true, error: .configurationRequired)) 106 | .previewContext(WidgetPreviewContext(family: .accessoryInline)) 107 | NodeStatusView(nodeStatus: NodeStatus(showBitcoinLogo: false, error: .nodeUnreachable)) 108 | .previewContext(WidgetPreviewContext(family: .accessoryInline)) 109 | NodeStatusView(nodeStatus: NodeStatus(showBitcoinLogo: true, blockHeight: 754091, userAgent: "/Satoshi:23.0.0/", protocolVersion: 70016)) 110 | .previewContext(WidgetPreviewContext(family: .accessoryInline)) 111 | } 112 | } 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /Widgets/Sources/MoscowTimeView.swift: -------------------------------------------------------------------------------- 1 | import WidgetKit 2 | import SwiftUI 3 | 4 | struct MoscowTimeView: View { 5 | 6 | @Environment(\.widgetFamily) var family: WidgetFamily 7 | var moscowTime: MoscowTimeProvider.Entry 8 | 9 | var body: some View { 10 | #if os(watchOS) 11 | accessoryView(for: family) 12 | #else 13 | if #available(iOSApplicationExtension 17.0, *) { 14 | if family == .accessoryRectangular || family == .accessoryInline { 15 | accessoryView(for: family).containerBackground(for: .widget) {} 16 | } else { 17 | systemView(for: family).containerBackground(for: .widget) { 18 | BitcoinBackground(family: family, showLogo: moscowTime.showBitcoinLogo) 19 | } 20 | } 21 | } else if #available(iOSApplicationExtension 16.0, *), family == .accessoryRectangular || family == .accessoryInline { 22 | accessoryView(for: family) 23 | } else { 24 | ZStack { 25 | BitcoinBackground(family: family, showLogo: moscowTime.showBitcoinLogo) 26 | systemView(for: family).padding() 27 | } 28 | } 29 | #endif 30 | } 31 | 32 | @available(iOSApplicationExtension 16.0, *) 33 | func accessoryView(for family: WidgetFamily) -> some View { 34 | VStack(alignment: .trailing) { 35 | if family == .accessoryInline { 36 | Label(moscowTime(for: moscowTime.format).string(for: moscowTime.primaryPrice), systemImage: "bitcoinsign.square.fill") 37 | } else { 38 | Text(moscowTime.primaryPrice, formatter: moscowTime(for: moscowTime.format)) 39 | .font(.system(size: 37)) 40 | Text(moscowTime.primaryPrice, formatter: currency(for: moscowTime.primaryCurrencyCode)) 41 | .font(.footnote) 42 | .foregroundColor(.secondary) 43 | .padding(.trailing, 2) 44 | } 45 | } 46 | } 47 | 48 | #if !os(watchOS) 49 | func systemView(for family: WidgetFamily) -> some View { 50 | HStack(spacing: 42) { 51 | VStack(alignment: .trailing) { 52 | Text(moscowTime.primaryPrice, formatter: moscowTime(for: moscowTime.format)) 53 | .font(.largeTitle) 54 | .foregroundColor(.white) 55 | Text(moscowTime.primaryPrice, formatter: currency(for: moscowTime.primaryCurrencyCode)) 56 | .font(.body) 57 | .foregroundColor(.gray) 58 | .padding(.trailing, 2) 59 | } 60 | if family == .systemMedium { 61 | VStack(alignment: .trailing) { 62 | Text(moscowTime.secondaryPrice, formatter: moscowTime(for: moscowTime.format)) 63 | .font(.largeTitle) 64 | .foregroundColor(.white) 65 | Text(moscowTime.secondaryPrice, formatter: currency(for: moscowTime.secondaryCurrencyCode)) 66 | .font(.body) 67 | .foregroundColor(.gray) 68 | .padding(.trailing, 2) 69 | } 70 | } 71 | } 72 | } 73 | #endif 74 | 75 | private func moscowTime(for format: MoscowTimeFormat) -> MoscowTimeFormatter { 76 | let formatter = MoscowTimeFormatter(); 77 | formatter.format = format 78 | 79 | return formatter 80 | } 81 | 82 | private func currency(for currencyCode: String) -> NumberFormatter { 83 | let formatter = NumberFormatter() 84 | formatter.numberStyle = .currency 85 | formatter.currencyCode = currencyCode 86 | formatter.maximumFractionDigits = 0 87 | 88 | return formatter 89 | } 90 | 91 | } 92 | 93 | struct MoscowTimeView_Previews: PreviewProvider { 94 | 95 | static var previews: some View { 96 | Group { 97 | #if !os(watchOS) 98 | MoscowTimeView(moscowTime: MoscowTime(showBitcoinLogo: true, format: .time, primaryPrice: 51229.50, primaryCurrencyCode: "USD", secondaryPrice: 43546.19, secondaryCurrencyCode: "EUR")) 99 | .previewContext(WidgetPreviewContext(family: .systemMedium)) 100 | MoscowTimeView(moscowTime: MoscowTime(showBitcoinLogo: false, format: .plain, primaryPrice: 51229.50, primaryCurrencyCode: "USD", secondaryPrice: 43546.19, secondaryCurrencyCode: "EUR")) 101 | .previewContext(WidgetPreviewContext(family: .systemMedium)) 102 | 103 | MoscowTimeView(moscowTime: MoscowTime(showBitcoinLogo: true, format: .time, primaryPrice: 51229.50, primaryCurrencyCode: "USD", secondaryPrice: 43546.19, secondaryCurrencyCode: "EUR")) 104 | .previewContext(WidgetPreviewContext(family: .systemSmall)) 105 | MoscowTimeView(moscowTime: MoscowTime(showBitcoinLogo: false, format: .plain, primaryPrice: 51229.50, primaryCurrencyCode: "USD", secondaryPrice: 43546.19, secondaryCurrencyCode: "EUR")) 106 | .previewContext(WidgetPreviewContext(family: .systemSmall)) 107 | #endif 108 | 109 | if #available(iOSApplicationExtension 16.0, *) { 110 | MoscowTimeView(moscowTime: MoscowTime(showBitcoinLogo: true, format: .time, primaryPrice: 51229.50, primaryCurrencyCode: "USD", secondaryPrice: 43546.19, secondaryCurrencyCode: "EUR")) 111 | .previewContext(WidgetPreviewContext(family: .accessoryRectangular)) 112 | MoscowTimeView(moscowTime: MoscowTime(showBitcoinLogo: false, format: .plain, primaryPrice: 51229.50, primaryCurrencyCode: "USD", secondaryPrice: 43546.19, secondaryCurrencyCode: "EUR")) 113 | .previewContext(WidgetPreviewContext(family: .accessoryRectangular)) 114 | 115 | MoscowTimeView(moscowTime: MoscowTime(showBitcoinLogo: true, format: .time, primaryPrice: 51229.50, primaryCurrencyCode: "USD", secondaryPrice: 43546.19, secondaryCurrencyCode: "EUR")) 116 | .previewContext(WidgetPreviewContext(family: .accessoryInline)) 117 | MoscowTimeView(moscowTime: MoscowTime(showBitcoinLogo: false, format: .plain, primaryPrice: 51229.50, primaryCurrencyCode: "USD", secondaryPrice: 43546.19, secondaryCurrencyCode: "EUR")) 118 | .previewContext(WidgetPreviewContext(family: .accessoryInline)) 119 | } 120 | } 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /Widgets/Sources/MempoolStatusView.swift: -------------------------------------------------------------------------------- 1 | import WidgetKit 2 | import SwiftUI 3 | import Intents 4 | 5 | struct MempoolStatusView: View { 6 | 7 | private static let feeSeparator: String = " / " 8 | 9 | @Environment(\.widgetFamily) var family: WidgetFamily 10 | var mempoolStatus: MempoolStatusProvider.Entry 11 | 12 | var body: some View { 13 | #if os(watchOS) 14 | accessoryView(for: family) 15 | #else 16 | if #available(iOSApplicationExtension 17.0, *) { 17 | if family == .accessoryRectangular || family == .accessoryInline { 18 | accessoryView(for: family).containerBackground(for: .widget) {} 19 | } else { 20 | systemView(for: family).containerBackground(for: .widget) { 21 | BitcoinBackground(family: family, showLogo: mempoolStatus.showBitcoinLogo) 22 | } 23 | } 24 | } else if #available(iOSApplicationExtension 16.0, *), family == .accessoryRectangular || family == .accessoryInline { 25 | accessoryView(for: family) 26 | } else { 27 | ZStack { 28 | BitcoinBackground(family: family, showLogo: mempoolStatus.showBitcoinLogo) 29 | systemView(for: family).padding() 30 | } 31 | } 32 | #endif 33 | } 34 | 35 | @available(iOSApplicationExtension 16.0, *) 36 | func accessoryView(for family: WidgetFamily) -> some View { 37 | VStack(alignment: .trailing) { 38 | if family == .accessoryInline { 39 | Label(String(mempoolStatus.blockHeight), systemImage: "bitcoinsign.square.fill") 40 | } else { 41 | Text(verbatim: String(mempoolStatus.blockHeight)) 42 | .font(.system(size: 37)) 43 | VStack { 44 | Text(verbatim: String(mempoolStatus.economyFee)) 45 | + Text(verbatim: Self.feeSeparator).foregroundColor(.secondary) 46 | + Text(verbatim: String(mempoolStatus.hourFee)) 47 | + Text(verbatim: Self.feeSeparator).foregroundColor(.secondary) 48 | + Text(verbatim: String(mempoolStatus.halfHourFee)) 49 | + Text(verbatim: Self.feeSeparator).foregroundColor(.secondary) 50 | + Text(verbatim: String(mempoolStatus.fastestFee)) 51 | }.font(.footnote).padding(.trailing, 2) 52 | } 53 | } 54 | } 55 | 56 | #if !os(watchOS) 57 | func systemView(for family: WidgetFamily) -> some View { 58 | VStack(alignment: family == .systemSmall ? .trailing : .center) { 59 | Text(verbatim: String(mempoolStatus.blockHeight)) 60 | .font(family == .systemSmall ? .title : .largeTitle) 61 | .foregroundColor(Color("MempoolColor")) 62 | .padding(.bottom, 1) 63 | VStack(alignment: .trailing) { 64 | Text(verbatim: String(mempoolStatus.economyFee)).foregroundColor(.white) 65 | + Text(verbatim: Self.feeSeparator).foregroundColor(.gray) 66 | + Text(verbatim: String(mempoolStatus.hourFee)).foregroundColor(.white) 67 | + Text(verbatim: Self.feeSeparator).foregroundColor(.gray) 68 | + Text(verbatim: String(mempoolStatus.halfHourFee)).foregroundColor(.white) 69 | + Text(verbatim: Self.feeSeparator).foregroundColor(.gray) 70 | + Text(verbatim: String(mempoolStatus.fastestFee)).foregroundColor(.white) 71 | Text("MempoolStatusView.minimumFee").foregroundColor(.gray) 72 | + Text(verbatim: " ") 73 | + Text(verbatim: String(mempoolStatus.minimumFee)).foregroundColor(.white) 74 | }.font(family == .systemSmall ? .footnote : .body) 75 | .padding(.trailing, family == .systemSmall ? 2 : 0) 76 | .multilineTextAlignment(.trailing) 77 | } 78 | } 79 | #endif 80 | } 81 | 82 | struct MempoolStatusView_Previews: PreviewProvider { 83 | 84 | static var previews: some View { 85 | Group { 86 | #if !os(watchOS) 87 | MempoolStatusView(mempoolStatus: MempoolStatus(showBitcoinLogo: true, blockHeight: 755237, fastestFee: 17, halfHourFee: 8, hourFee: 3, economyFee: 1, minimumFee: 1)) 88 | .previewContext(WidgetPreviewContext(family: .systemMedium)) 89 | MempoolStatusView(mempoolStatus: MempoolStatus(showBitcoinLogo: false, blockHeight: 755237, fastestFee: 2791, halfHourFee: 730, hourFee: 130, economyFee: 37, minimumFee: 19)) 90 | .previewContext(WidgetPreviewContext(family: .systemMedium)) 91 | 92 | MempoolStatusView(mempoolStatus: MempoolStatus(showBitcoinLogo: true, blockHeight: 755237, fastestFee: 1, halfHourFee: 1, hourFee: 1, economyFee: 1, minimumFee: 1)) 93 | .previewContext(WidgetPreviewContext(family: .systemSmall)) 94 | MempoolStatusView(mempoolStatus: MempoolStatus(showBitcoinLogo: false, blockHeight: 755237, fastestFee: 2791, halfHourFee: 730, hourFee: 130, economyFee: 37, minimumFee: 19)) 95 | .previewContext(WidgetPreviewContext(family: .systemSmall)) 96 | #endif 97 | 98 | if #available(iOSApplicationExtension 16.0, *) { 99 | MempoolStatusView(mempoolStatus: MempoolStatus(showBitcoinLogo: true, blockHeight: 755237, fastestFee: 1, halfHourFee: 1, hourFee: 1, economyFee: 1, minimumFee: 1)) 100 | .previewContext(WidgetPreviewContext(family: .accessoryRectangular)) 101 | MempoolStatusView(mempoolStatus: MempoolStatus(showBitcoinLogo: false, blockHeight: 755237, fastestFee: 17, halfHourFee: 8, hourFee: 3, economyFee: 1, minimumFee: 1)) 102 | .previewContext(WidgetPreviewContext(family: .accessoryRectangular)) 103 | MempoolStatusView(mempoolStatus: MempoolStatus(showBitcoinLogo: true, blockHeight: 755237, fastestFee: 2791, halfHourFee: 730, hourFee: 130, economyFee: 37, minimumFee: 19)) 104 | .previewContext(WidgetPreviewContext(family: .accessoryRectangular)) 105 | 106 | MempoolStatusView(mempoolStatus: MempoolStatus(showBitcoinLogo: true, blockHeight: 755237, fastestFee: 2791, halfHourFee: 730, hourFee: 130, economyFee: 37, minimumFee: 19)) 107 | .previewContext(WidgetPreviewContext(family: .accessoryInline)) 108 | } 109 | } 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /Widgets/Localizable.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "CombinedStatusWidget.description" : { 5 | "extractionState" : "manual", 6 | "localizations" : { 7 | "cs" : { 8 | "stringUnit" : { 9 | "state" : "translated", 10 | "value" : "Sledujte stav svého Bitcoinového uzlu a prohlížeče mempool.space." 11 | } 12 | }, 13 | "en" : { 14 | "stringUnit" : { 15 | "state" : "translated", 16 | "value" : "See status of your full node and mempool.space Bitcoin explorer." 17 | } 18 | } 19 | } 20 | }, 21 | "CombinedStatusWidget.displayName" : { 22 | "extractionState" : "manual", 23 | "localizations" : { 24 | "cs" : { 25 | "stringUnit" : { 26 | "state" : "translated", 27 | "value" : "Kombinovaný stav" 28 | } 29 | }, 30 | "en" : { 31 | "stringUnit" : { 32 | "state" : "translated", 33 | "value" : "Combined Status" 34 | } 35 | } 36 | } 37 | }, 38 | "HalvingCountdownWidget.description" : { 39 | "localizations" : { 40 | "cs" : { 41 | "stringUnit" : { 42 | "state" : "translated", 43 | "value" : "Sledujte odpočet do nadcházejícího půlení." 44 | } 45 | }, 46 | "en" : { 47 | "stringUnit" : { 48 | "state" : "translated", 49 | "value" : "See countdown to the upcoming halving." 50 | } 51 | } 52 | } 53 | }, 54 | "HalvingCountdownWidget.displayName" : { 55 | "localizations" : { 56 | "cs" : { 57 | "stringUnit" : { 58 | "state" : "translated", 59 | "value" : "Odpočet půlení" 60 | } 61 | }, 62 | "en" : { 63 | "stringUnit" : { 64 | "state" : "translated", 65 | "value" : "Halving Countdown" 66 | } 67 | } 68 | } 69 | }, 70 | "MempoolStatusView.minimumFee" : { 71 | "localizations" : { 72 | "cs" : { 73 | "stringUnit" : { 74 | "state" : "translated", 75 | "value" : "minimum" 76 | } 77 | }, 78 | "en" : { 79 | "stringUnit" : { 80 | "state" : "translated", 81 | "value" : "minimum" 82 | } 83 | } 84 | } 85 | }, 86 | "MempoolStatusWidget.description" : { 87 | "localizations" : { 88 | "cs" : { 89 | "stringUnit" : { 90 | "state" : "translated", 91 | "value" : "Sledujte stav Bitcoinového prohlížeče mempool.space." 92 | } 93 | }, 94 | "en" : { 95 | "stringUnit" : { 96 | "state" : "translated", 97 | "value" : "See status of mempool.space Bitcoin explorer." 98 | } 99 | } 100 | } 101 | }, 102 | "MempoolStatusWidget.displayName" : { 103 | "localizations" : { 104 | "cs" : { 105 | "stringUnit" : { 106 | "state" : "translated", 107 | "value" : "Stav mempool.space" 108 | } 109 | }, 110 | "en" : { 111 | "stringUnit" : { 112 | "state" : "translated", 113 | "value" : "Mempool Status" 114 | } 115 | } 116 | } 117 | }, 118 | "MoscowTimeWidget.description" : { 119 | "localizations" : { 120 | "cs" : { 121 | "stringUnit" : { 122 | "state" : "translated", 123 | "value" : "Sledujte aktuální Moskevský čas (sats za dolar) pro vybrané měny." 124 | } 125 | }, 126 | "en" : { 127 | "stringUnit" : { 128 | "state" : "translated", 129 | "value" : "See current Moscow Time (sats per dollar) for selected currencies." 130 | } 131 | } 132 | } 133 | }, 134 | "MoscowTimeWidget.displayName" : { 135 | "localizations" : { 136 | "cs" : { 137 | "stringUnit" : { 138 | "state" : "translated", 139 | "value" : "Moskevský čas" 140 | } 141 | }, 142 | "en" : { 143 | "stringUnit" : { 144 | "state" : "translated", 145 | "value" : "Moscow Time" 146 | } 147 | } 148 | } 149 | }, 150 | "NodeStatus.Error.configurationRequired" : { 151 | "extractionState" : "manual", 152 | "localizations" : { 153 | "cs" : { 154 | "stringUnit" : { 155 | "state" : "translated", 156 | "value" : "Proveďte konfiguraci" 157 | } 158 | }, 159 | "en" : { 160 | "stringUnit" : { 161 | "state" : "translated", 162 | "value" : "Configuration required" 163 | } 164 | } 165 | } 166 | }, 167 | "NodeStatus.Error.nodeUnreachable" : { 168 | "extractionState" : "manual", 169 | "localizations" : { 170 | "cs" : { 171 | "stringUnit" : { 172 | "state" : "translated", 173 | "value" : "Uzel je nedostupný" 174 | } 175 | }, 176 | "en" : { 177 | "stringUnit" : { 178 | "state" : "translated", 179 | "value" : "Node unreachable" 180 | } 181 | } 182 | } 183 | }, 184 | "NodeStatusWidget.description" : { 185 | "extractionState" : "manual", 186 | "localizations" : { 187 | "cs" : { 188 | "stringUnit" : { 189 | "state" : "translated", 190 | "value" : "Sledujte stav svého Bitcoinového uzlu." 191 | } 192 | }, 193 | "en" : { 194 | "stringUnit" : { 195 | "state" : "translated", 196 | "value" : "See status of your full node." 197 | } 198 | } 199 | } 200 | }, 201 | "NodeStatusWidget.displayName" : { 202 | "extractionState" : "manual", 203 | "localizations" : { 204 | "cs" : { 205 | "stringUnit" : { 206 | "state" : "translated", 207 | "value" : "Stav uzlu" 208 | } 209 | }, 210 | "en" : { 211 | "stringUnit" : { 212 | "state" : "translated", 213 | "value" : "Node Status" 214 | } 215 | } 216 | } 217 | } 218 | }, 219 | "version" : "1.0" 220 | } -------------------------------------------------------------------------------- /App/Localizable.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "ContentView.AddTo.header" : { 5 | "localizations" : { 6 | "cs" : { 7 | "stringUnit" : { 8 | "state" : "translated", 9 | "value" : "Přidání" 10 | } 11 | }, 12 | "en" : { 13 | "stringUnit" : { 14 | "state" : "translated", 15 | "value" : "Add to" 16 | } 17 | } 18 | } 19 | }, 20 | "ContentView.AddTo.HomeScreen.header" : { 21 | "localizations" : { 22 | "cs" : { 23 | "stringUnit" : { 24 | "state" : "translated", 25 | "value" : "Hlavní plocha" 26 | } 27 | }, 28 | "en" : { 29 | "stringUnit" : { 30 | "state" : "translated", 31 | "value" : "Add to Home Screen" 32 | } 33 | } 34 | } 35 | }, 36 | "ContentView.AddTo.HomeScreen.link" : { 37 | "localizations" : { 38 | "cs" : { 39 | "stringUnit" : { 40 | "state" : "translated", 41 | "value" : "Na hlavní plochu" 42 | } 43 | }, 44 | "en" : { 45 | "stringUnit" : { 46 | "state" : "translated", 47 | "value" : "Home Screen" 48 | } 49 | } 50 | } 51 | }, 52 | "ContentView.AddTo.HomeScreen.step1" : { 53 | "localizations" : { 54 | "cs" : { 55 | "stringUnit" : { 56 | "state" : "translated", 57 | "value" : "Na ploše stiskněte a podržte widget nebo prázdnou oblast, dokud se aplikace nezačnou chvět." 58 | } 59 | }, 60 | "en" : { 61 | "stringUnit" : { 62 | "state" : "translated", 63 | "value" : "From the Home Screen, touch and hold a widget or an empty area until the apps jiggle." 64 | } 65 | } 66 | } 67 | }, 68 | "ContentView.AddTo.HomeScreen.step2" : { 69 | "localizations" : { 70 | "cs" : { 71 | "stringUnit" : { 72 | "state" : "translated", 73 | "value" : "Klepněte na „+“ v levém horním rohu displeje." 74 | } 75 | }, 76 | "en" : { 77 | "stringUnit" : { 78 | "state" : "translated", 79 | "value" : "Tap the Add button in the upper-left corner." 80 | } 81 | } 82 | } 83 | }, 84 | "ContentView.AddTo.HomeScreen.step3" : { 85 | "localizations" : { 86 | "cs" : { 87 | "stringUnit" : { 88 | "state" : "translated", 89 | "value" : "Najděte Bitcoin Widgets, pak přejížděním doleva/doprava vyberte požadovaný widget a velikost." 90 | } 91 | }, 92 | "en" : { 93 | "stringUnit" : { 94 | "state" : "translated", 95 | "value" : "Look up Bitcoin Widgets, then choose desired widget and size." 96 | } 97 | } 98 | } 99 | }, 100 | "ContentView.AddTo.HomeScreen.step4" : { 101 | "localizations" : { 102 | "cs" : { 103 | "stringUnit" : { 104 | "state" : "translated", 105 | "value" : "Klepněte na „Přidat widget“, pak na „Hotovo“." 106 | } 107 | }, 108 | "en" : { 109 | "stringUnit" : { 110 | "state" : "translated", 111 | "value" : "Tap Add Widget, then tap Done." 112 | } 113 | } 114 | } 115 | }, 116 | "ContentView.AddTo.LockScreen.header" : { 117 | "localizations" : { 118 | "cs" : { 119 | "stringUnit" : { 120 | "state" : "translated", 121 | "value" : "Uzamčená obrazovka" 122 | } 123 | }, 124 | "en" : { 125 | "stringUnit" : { 126 | "state" : "translated", 127 | "value" : "Add to Lock Screen" 128 | } 129 | } 130 | } 131 | }, 132 | "ContentView.AddTo.LockScreen.link" : { 133 | "localizations" : { 134 | "cs" : { 135 | "stringUnit" : { 136 | "state" : "translated", 137 | "value" : "Na uzamčenou obrazovku" 138 | } 139 | }, 140 | "en" : { 141 | "stringUnit" : { 142 | "state" : "translated", 143 | "value" : "Lock Screen" 144 | } 145 | } 146 | } 147 | }, 148 | "ContentView.AddTo.LockScreen.step1" : { 149 | "localizations" : { 150 | "cs" : { 151 | "stringUnit" : { 152 | "state" : "translated", 153 | "value" : "Stiskněte a podržte uzamčenou obrazovku, dokud se neobjeví tlačítko „Přizpůsobit“ u spodního okraje displeje." 154 | } 155 | }, 156 | "en" : { 157 | "stringUnit" : { 158 | "state" : "translated", 159 | "value" : "Touch and hold the Lock Screen until the Customize button appears at the bottom of the screen." 160 | } 161 | } 162 | } 163 | }, 164 | "ContentView.AddTo.LockScreen.step2" : { 165 | "localizations" : { 166 | "cs" : { 167 | "stringUnit" : { 168 | "state" : "translated", 169 | "value" : "Klepněte na „Přizpůsobit“, pak klepněte na možnost „Uzamčená obrazovka“." 170 | } 171 | }, 172 | "en" : { 173 | "stringUnit" : { 174 | "state" : "translated", 175 | "value" : "Tap the Customize button, then tap the Lock Screen option." 176 | } 177 | } 178 | } 179 | }, 180 | "ContentView.AddTo.LockScreen.step3" : { 181 | "localizations" : { 182 | "cs" : { 183 | "stringUnit" : { 184 | "state" : "translated", 185 | "value" : "Klepněte do oblasti nad nebo pod časem, kterou chcete přizpůsobit." 186 | } 187 | }, 188 | "en" : { 189 | "stringUnit" : { 190 | "state" : "translated", 191 | "value" : "Tap the box above or below the time you want to customize." 192 | } 193 | } 194 | } 195 | }, 196 | "ContentView.AddTo.LockScreen.step4" : { 197 | "localizations" : { 198 | "cs" : { 199 | "stringUnit" : { 200 | "state" : "translated", 201 | "value" : "Najděte Bitcoin Widgets, pak klepnutím nebo přetažením přidejte požadované widgety." 202 | } 203 | }, 204 | "en" : { 205 | "stringUnit" : { 206 | "state" : "translated", 207 | "value" : "Look up Bitcoin Widgets, then tap or drag the widgets you want to add." 208 | } 209 | } 210 | } 211 | }, 212 | "ContentView.AddTo.LockScreen.step5" : { 213 | "localizations" : { 214 | "cs" : { 215 | "stringUnit" : { 216 | "state" : "translated", 217 | "value" : "Zavřete nabídku widgetů, pak klepněte na „Hotovo“." 218 | } 219 | }, 220 | "en" : { 221 | "stringUnit" : { 222 | "state" : "translated", 223 | "value" : "Close the widget popup, then tap Done." 224 | } 225 | } 226 | } 227 | }, 228 | "ContentView.AddTo.TodayView.header" : { 229 | "localizations" : { 230 | "cs" : { 231 | "stringUnit" : { 232 | "state" : "translated", 233 | "value" : "Zobrazení Dnes" 234 | } 235 | }, 236 | "en" : { 237 | "stringUnit" : { 238 | "state" : "translated", 239 | "value" : "Add to Today View" 240 | } 241 | } 242 | } 243 | }, 244 | "ContentView.AddTo.TodayView.link" : { 245 | "localizations" : { 246 | "cs" : { 247 | "stringUnit" : { 248 | "state" : "translated", 249 | "value" : "Do zobrazení Dnes" 250 | } 251 | }, 252 | "en" : { 253 | "stringUnit" : { 254 | "state" : "translated", 255 | "value" : "Today View" 256 | } 257 | } 258 | } 259 | }, 260 | "ContentView.AddTo.TodayView.step1" : { 261 | "localizations" : { 262 | "cs" : { 263 | "stringUnit" : { 264 | "state" : "translated", 265 | "value" : "Na ploše přejíždějte prstem doprava, dokud neuvidíte zobrazení Dnes." 266 | } 267 | }, 268 | "en" : { 269 | "stringUnit" : { 270 | "state" : "translated", 271 | "value" : "From the Home Screen, swipe right to access the Today View." 272 | } 273 | } 274 | } 275 | }, 276 | "ContentView.AddTo.TodayView.step2" : { 277 | "localizations" : { 278 | "cs" : { 279 | "stringUnit" : { 280 | "state" : "translated", 281 | "value" : "Přejeďte dolů a klepněte na „Upravit“, pak klepněte na „+“ v levém horním rohu displeje." 282 | } 283 | }, 284 | "en" : { 285 | "stringUnit" : { 286 | "state" : "translated", 287 | "value" : "Scroll down and tap the Edit button, then tap the Add button in the upper-left corner." 288 | } 289 | } 290 | } 291 | }, 292 | "ContentView.AddTo.TodayView.step3" : { 293 | "localizations" : { 294 | "cs" : { 295 | "stringUnit" : { 296 | "state" : "translated", 297 | "value" : "Najděte Bitcoin Widgets, pak přejížděním doleva/doprava vyberte požadovaný widget a velikost." 298 | } 299 | }, 300 | "en" : { 301 | "stringUnit" : { 302 | "state" : "translated", 303 | "value" : "Look up Bitcoin Widgets, then choose desired widget and size." 304 | } 305 | } 306 | } 307 | }, 308 | "ContentView.AddTo.TodayView.step4" : { 309 | "localizations" : { 310 | "cs" : { 311 | "stringUnit" : { 312 | "state" : "translated", 313 | "value" : "Klepněte na „Přidat widget“, pak na „Hotovo“." 314 | } 315 | }, 316 | "en" : { 317 | "stringUnit" : { 318 | "state" : "translated", 319 | "value" : "Tap Add Widget, then tap Done." 320 | } 321 | } 322 | } 323 | }, 324 | "ContentView.AddTo.WatchFace.header" : { 325 | "localizations" : { 326 | "cs" : { 327 | "stringUnit" : { 328 | "state" : "translated", 329 | "value" : "Ciferník hodinek" 330 | } 331 | }, 332 | "en" : { 333 | "stringUnit" : { 334 | "state" : "translated", 335 | "value" : "Add to Watch Face" 336 | } 337 | } 338 | } 339 | }, 340 | "ContentView.AddTo.WatchFace.link" : { 341 | "localizations" : { 342 | "cs" : { 343 | "stringUnit" : { 344 | "state" : "translated", 345 | "value" : "Na ciferník hodinek" 346 | } 347 | }, 348 | "en" : { 349 | "stringUnit" : { 350 | "state" : "translated", 351 | "value" : "Watch Face" 352 | } 353 | } 354 | } 355 | }, 356 | "ContentView.AddTo.WatchFace.step1" : { 357 | "localizations" : { 358 | "cs" : { 359 | "stringUnit" : { 360 | "state" : "translated", 361 | "value" : "Stiskněte korunku Digital Crown na svých hodinkách pro zobrazení ciferníku." 362 | } 363 | }, 364 | "en" : { 365 | "stringUnit" : { 366 | "state" : "translated", 367 | "value" : "Press the Digital Crown on your watch to go to the watch face." 368 | } 369 | } 370 | } 371 | }, 372 | "ContentView.AddTo.WatchFace.step2" : { 373 | "localizations" : { 374 | "cs" : { 375 | "stringUnit" : { 376 | "state" : "translated", 377 | "value" : "Podržte prst na displeji s aktuálním ciferníkem, pak klepněte na „Upravit“." 378 | } 379 | }, 380 | "en" : { 381 | "stringUnit" : { 382 | "state" : "translated", 383 | "value" : "Touch and hold the display." 384 | } 385 | } 386 | } 387 | }, 388 | "ContentView.AddTo.WatchFace.step3" : { 389 | "localizations" : { 390 | "cs" : { 391 | "stringUnit" : { 392 | "state" : "translated", 393 | "value" : "Přejížděním doleva/doprava vyberte ciferník, pak klepněte na „Upravit“." 394 | } 395 | }, 396 | "en" : { 397 | "stringUnit" : { 398 | "state" : "translated", 399 | "value" : "Swipe left or right to choose a watch face, then tap Edit." 400 | } 401 | } 402 | } 403 | }, 404 | "ContentView.AddTo.WatchFace.step4" : { 405 | "localizations" : { 406 | "cs" : { 407 | "stringUnit" : { 408 | "state" : "translated", 409 | "value" : "Přejeďte úplně doleva k editaci komplikací, klepnutím nějakou vyberte." 410 | } 411 | }, 412 | "en" : { 413 | "stringUnit" : { 414 | "state" : "translated", 415 | "value" : "Swipe all the way to the left to edit complications, tap one to select it." 416 | } 417 | } 418 | } 419 | }, 420 | "ContentView.AddTo.WatchFace.step5" : { 421 | "localizations" : { 422 | "cs" : { 423 | "stringUnit" : { 424 | "state" : "translated", 425 | "value" : "Najděte Bitcoin Widgets, klepnutím vyberte požadovanou komplikaci." 426 | } 427 | }, 428 | "en" : { 429 | "stringUnit" : { 430 | "state" : "translated", 431 | "value" : "Look up Bitcoin Widgets, then tap the desired complication to select it." 432 | } 433 | } 434 | } 435 | }, 436 | "ContentView.AddTo.WatchFace.step6" : { 437 | "localizations" : { 438 | "cs" : { 439 | "stringUnit" : { 440 | "state" : "translated", 441 | "value" : "Uložte změny stisknutím korunky Digital Crown." 442 | } 443 | }, 444 | "en" : { 445 | "stringUnit" : { 446 | "state" : "translated", 447 | "value" : "Press the Digital Crown to save your changes." 448 | } 449 | } 450 | } 451 | }, 452 | "ContentView.AddTo.WatchFace.step7" : { 453 | "localizations" : { 454 | "cs" : { 455 | "stringUnit" : { 456 | "state" : "translated", 457 | "value" : "Klepnutím na ciferník jej nastavte jako aktuální." 458 | } 459 | }, 460 | "en" : { 461 | "stringUnit" : { 462 | "state" : "translated", 463 | "value" : "Tap the watch face to set it as your current face." 464 | } 465 | } 466 | } 467 | }, 468 | "ContentView.Configure.header" : { 469 | "localizations" : { 470 | "cs" : { 471 | "stringUnit" : { 472 | "state" : "translated", 473 | "value" : "Konfigurace" 474 | } 475 | }, 476 | "en" : { 477 | "stringUnit" : { 478 | "state" : "translated", 479 | "value" : "Configure" 480 | } 481 | } 482 | } 483 | }, 484 | "ContentView.Configure.step1" : { 485 | "extractionState" : "manual", 486 | "localizations" : { 487 | "cs" : { 488 | "stringUnit" : { 489 | "state" : "translated", 490 | "value" : "Stisknutím a podržením widgetu otevřete nabídku rychlých akcí, pak klepněte na „Upravit widget“." 491 | } 492 | }, 493 | "en" : { 494 | "stringUnit" : { 495 | "state" : "translated", 496 | "value" : "Touch and hold a widget until menu appears, then tap Edit Widget." 497 | } 498 | } 499 | } 500 | }, 501 | "ContentView.Configure.step2" : { 502 | "extractionState" : "manual", 503 | "localizations" : { 504 | "cs" : { 505 | "stringUnit" : { 506 | "state" : "translated", 507 | "value" : "Proveďte změny a dokončete je klepnutím mimo widget." 508 | } 509 | }, 510 | "en" : { 511 | "stringUnit" : { 512 | "state" : "translated", 513 | "value" : "Provide required properties, then tap anywhere when done." 514 | } 515 | } 516 | } 517 | }, 518 | "ContentView.FindOutMore.header" : { 519 | "localizations" : { 520 | "cs" : { 521 | "stringUnit" : { 522 | "state" : "translated", 523 | "value" : "Další informace" 524 | } 525 | }, 526 | "en" : { 527 | "stringUnit" : { 528 | "state" : "translated", 529 | "value" : "Find out more" 530 | } 531 | } 532 | } 533 | }, 534 | "ContentView.FindOutMore.issueTracking" : { 535 | "localizations" : { 536 | "cs" : { 537 | "stringUnit" : { 538 | "state" : "translated", 539 | "value" : "Hlášení chyb" 540 | } 541 | }, 542 | "en" : { 543 | "stringUnit" : { 544 | "state" : "translated", 545 | "value" : "Issue tracking" 546 | } 547 | } 548 | } 549 | }, 550 | "ContentView.FindOutMore.sourceCode" : { 551 | "localizations" : { 552 | "cs" : { 553 | "stringUnit" : { 554 | "state" : "translated", 555 | "value" : "Zdrojový kód" 556 | } 557 | }, 558 | "en" : { 559 | "stringUnit" : { 560 | "state" : "translated", 561 | "value" : "Source code" 562 | } 563 | } 564 | } 565 | }, 566 | "ContentView.Support.header" : { 567 | "localizations" : { 568 | "cs" : { 569 | "stringUnit" : { 570 | "state" : "translated", 571 | "value" : "Podpora" 572 | } 573 | }, 574 | "en" : { 575 | "stringUnit" : { 576 | "state" : "translated", 577 | "value" : "Support" 578 | } 579 | } 580 | } 581 | }, 582 | "ContentView.title" : { 583 | "localizations" : { 584 | "cs" : { 585 | "stringUnit" : { 586 | "state" : "translated", 587 | "value" : "Bitcoin Widgets" 588 | } 589 | }, 590 | "en" : { 591 | "stringUnit" : { 592 | "state" : "translated", 593 | "value" : "Bitcoin Widgets" 594 | } 595 | } 596 | } 597 | }, 598 | "widgets@bitcoinwidgets.app" : { 599 | 600 | } 601 | }, 602 | "version" : "1.0" 603 | } -------------------------------------------------------------------------------- /Widgets/Base.lproj/Configuration.intentdefinition: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | INEnums 6 | 7 | 8 | INEnumDisplayName 9 | Moscow Time Format 10 | INEnumDisplayNameID 11 | Ld81XL 12 | INEnumGeneratesHeader 13 | 14 | INEnumName 15 | MoscowTimeFormat 16 | INEnumType 17 | Regular 18 | INEnumValues 19 | 20 | 21 | INEnumValueDisplayName 22 | unknown 23 | INEnumValueDisplayNameID 24 | qZqe3X 25 | INEnumValueName 26 | unknown 27 | 28 | 29 | INEnumValueDisplayName 30 | 19:52 31 | INEnumValueDisplayNameID 32 | 6OBZPT 33 | INEnumValueIndex 34 | 1 35 | INEnumValueName 36 | time 37 | 38 | 39 | INEnumValueDisplayName 40 | 1952 41 | INEnumValueDisplayNameID 42 | 4MEyAY 43 | INEnumValueIndex 44 | 2 45 | INEnumValueName 46 | plain 47 | 48 | 49 | 50 | 51 | INEnumDisplayName 52 | Fiat Currency 53 | INEnumDisplayNameID 54 | 7TuOxV 55 | INEnumGeneratesHeader 56 | 57 | INEnumName 58 | FiatCurrency 59 | INEnumType 60 | Regular 61 | INEnumValues 62 | 63 | 64 | INEnumValueDisplayName 65 | unknown 66 | INEnumValueDisplayNameID 67 | sqZFfB 68 | INEnumValueName 69 | unknown 70 | 71 | 72 | INEnumValueDisplayName 73 | CAD 74 | INEnumValueDisplayNameID 75 | kt9tTn 76 | INEnumValueIndex 77 | 1 78 | INEnumValueName 79 | cad 80 | 81 | 82 | INEnumValueDisplayName 83 | CHF 84 | INEnumValueDisplayNameID 85 | W2B6Pj 86 | INEnumValueIndex 87 | 2 88 | INEnumValueName 89 | chf 90 | 91 | 92 | INEnumValueDisplayName 93 | EUR 94 | INEnumValueDisplayNameID 95 | GkyfO3 96 | INEnumValueIndex 97 | 3 98 | INEnumValueName 99 | eur 100 | 101 | 102 | INEnumValueDisplayName 103 | GBP 104 | INEnumValueDisplayNameID 105 | kc059I 106 | INEnumValueIndex 107 | 4 108 | INEnumValueName 109 | gbp 110 | 111 | 112 | INEnumValueDisplayName 113 | USD 114 | INEnumValueDisplayNameID 115 | mGyQWc 116 | INEnumValueIndex 117 | 5 118 | INEnumValueName 119 | usd 120 | 121 | 122 | 123 | 124 | INIntentDefinitionModelVersion 125 | 1.2 126 | INIntentDefinitionNamespace 127 | 88xZPY 128 | INIntentDefinitionSystemVersion 129 | 22C65 130 | INIntentDefinitionToolsBuildVersion 131 | 14C18 132 | INIntentDefinitionToolsVersion 133 | 14.2 134 | INIntents 135 | 136 | 137 | INIntentCategory 138 | information 139 | INIntentDescription 140 | Configure host and port of your node 141 | INIntentDescriptionID 142 | tVvJ9c 143 | INIntentEligibleForWidgets 144 | 145 | INIntentIneligibleForSuggestions 146 | 147 | INIntentLastParameterTag 148 | 7 149 | INIntentName 150 | NodeConfiguration 151 | INIntentParameters 152 | 153 | 154 | INIntentParameterConfigurable 155 | 156 | INIntentParameterDisplayName 157 | Host 158 | INIntentParameterDisplayNameID 159 | 4L4D5m 160 | INIntentParameterDisplayPriority 161 | 1 162 | INIntentParameterMetadata 163 | 164 | INIntentParameterMetadataCapitalization 165 | None 166 | INIntentParameterMetadataDefaultValueID 167 | uETxcu 168 | INIntentParameterMetadataDisableAutocorrect 169 | 170 | INIntentParameterMetadataDisableSmartDashes 171 | 172 | INIntentParameterMetadataDisableSmartQuotes 173 | 174 | 175 | INIntentParameterName 176 | host 177 | INIntentParameterPromptDialogs 178 | 179 | 180 | INIntentParameterPromptDialogCustom 181 | 182 | INIntentParameterPromptDialogType 183 | Configuration 184 | 185 | 186 | INIntentParameterPromptDialogCustom 187 | 188 | INIntentParameterPromptDialogType 189 | Primary 190 | 191 | 192 | INIntentParameterTag 193 | 5 194 | INIntentParameterType 195 | String 196 | 197 | 198 | INIntentParameterConfigurable 199 | 200 | INIntentParameterDisplayName 201 | Port 202 | INIntentParameterDisplayNameID 203 | 4uTPFk 204 | INIntentParameterDisplayPriority 205 | 2 206 | INIntentParameterMetadata 207 | 208 | INIntentParameterMetadataDefaultValue 209 | 8333 210 | INIntentParameterMetadataMaximumValue 211 | 65535 212 | INIntentParameterMetadataType 213 | Field 214 | 215 | INIntentParameterName 216 | port 217 | INIntentParameterPromptDialogs 218 | 219 | 220 | INIntentParameterPromptDialogCustom 221 | 222 | INIntentParameterPromptDialogType 223 | Configuration 224 | 225 | 226 | INIntentParameterPromptDialogCustom 227 | 228 | INIntentParameterPromptDialogType 229 | Primary 230 | 231 | 232 | INIntentParameterTag 233 | 3 234 | INIntentParameterType 235 | Integer 236 | 237 | 238 | INIntentParameterConfigurable 239 | 240 | INIntentParameterDisplayName 241 | Show Bitcoin Logo 242 | INIntentParameterDisplayNameID 243 | u1GuAn 244 | INIntentParameterDisplayPriority 245 | 3 246 | INIntentParameterMetadata 247 | 248 | INIntentParameterMetadataDefaultValue 249 | 250 | INIntentParameterMetadataFalseDisplayName 251 | false 252 | INIntentParameterMetadataFalseDisplayNameID 253 | SLFpM5 254 | INIntentParameterMetadataTrueDisplayName 255 | true 256 | INIntentParameterMetadataTrueDisplayNameID 257 | QjUgXC 258 | 259 | INIntentParameterName 260 | showBitcoinLogo 261 | INIntentParameterPromptDialogs 262 | 263 | 264 | INIntentParameterPromptDialogCustom 265 | 266 | INIntentParameterPromptDialogType 267 | Configuration 268 | 269 | 270 | INIntentParameterPromptDialogCustom 271 | 272 | INIntentParameterPromptDialogType 273 | Primary 274 | 275 | 276 | INIntentParameterTag 277 | 7 278 | INIntentParameterType 279 | Boolean 280 | 281 | 282 | INIntentResponse 283 | 284 | INIntentResponseCodes 285 | 286 | 287 | INIntentResponseCodeName 288 | success 289 | INIntentResponseCodeSuccess 290 | 291 | 292 | 293 | INIntentResponseCodeName 294 | failure 295 | 296 | 297 | 298 | INIntentTitle 299 | Node Configuration 300 | INIntentTitleID 301 | gpCwrM 302 | INIntentType 303 | Custom 304 | INIntentVerb 305 | View 306 | 307 | 308 | INIntentCategory 309 | information 310 | INIntentDescription 311 | Configure Moscow Time format and currencies 312 | INIntentDescriptionID 313 | Mm0rjP 314 | INIntentEligibleForWidgets 315 | 316 | INIntentIneligibleForSuggestions 317 | 318 | INIntentLastParameterTag 319 | 10 320 | INIntentName 321 | MoscowTimeConfiguration 322 | INIntentParameters 323 | 324 | 325 | INIntentParameterConfigurable 326 | 327 | INIntentParameterDisplayName 328 | Format 329 | INIntentParameterDisplayNameID 330 | GivfKe 331 | INIntentParameterDisplayPriority 332 | 1 333 | INIntentParameterEnumType 334 | MoscowTimeFormat 335 | INIntentParameterEnumTypeNamespace 336 | 88xZPY 337 | INIntentParameterMetadata 338 | 339 | INIntentParameterMetadataDefaultValue 340 | time 341 | 342 | INIntentParameterName 343 | format 344 | INIntentParameterPromptDialogs 345 | 346 | 347 | INIntentParameterPromptDialogCustom 348 | 349 | INIntentParameterPromptDialogType 350 | Configuration 351 | 352 | 353 | INIntentParameterPromptDialogCustom 354 | 355 | INIntentParameterPromptDialogType 356 | Primary 357 | 358 | 359 | INIntentParameterPromptDialogCustom 360 | 361 | INIntentParameterPromptDialogFormatString 362 | There are ${count} options matching ‘${format}’. 363 | INIntentParameterPromptDialogFormatStringID 364 | 3PK6NE 365 | INIntentParameterPromptDialogType 366 | DisambiguationIntroduction 367 | 368 | 369 | INIntentParameterPromptDialogCustom 370 | 371 | INIntentParameterPromptDialogFormatString 372 | Just to confirm, you wanted ‘${format}’? 373 | INIntentParameterPromptDialogFormatStringID 374 | vxdbSb 375 | INIntentParameterPromptDialogType 376 | Confirmation 377 | 378 | 379 | INIntentParameterTag 380 | 2 381 | INIntentParameterType 382 | Integer 383 | 384 | 385 | INIntentParameterConfigurable 386 | 387 | INIntentParameterDisplayName 388 | Primary Currency 389 | INIntentParameterDisplayNameID 390 | jf1oAa 391 | INIntentParameterDisplayPriority 392 | 2 393 | INIntentParameterEnumType 394 | FiatCurrency 395 | INIntentParameterEnumTypeNamespace 396 | 88xZPY 397 | INIntentParameterMetadata 398 | 399 | INIntentParameterMetadataDefaultValue 400 | usd 401 | 402 | INIntentParameterName 403 | primaryCurrency 404 | INIntentParameterPromptDialogs 405 | 406 | 407 | INIntentParameterPromptDialogCustom 408 | 409 | INIntentParameterPromptDialogType 410 | Configuration 411 | 412 | 413 | INIntentParameterPromptDialogCustom 414 | 415 | INIntentParameterPromptDialogType 416 | Primary 417 | 418 | 419 | INIntentParameterPromptDialogCustom 420 | 421 | INIntentParameterPromptDialogFormatString 422 | There are ${count} options matching ‘${primaryCurrency}’. 423 | INIntentParameterPromptDialogFormatStringID 424 | yAWyHT 425 | INIntentParameterPromptDialogType 426 | DisambiguationIntroduction 427 | 428 | 429 | INIntentParameterPromptDialogCustom 430 | 431 | INIntentParameterPromptDialogFormatString 432 | Just to confirm, you wanted ‘${primaryCurrency}’? 433 | INIntentParameterPromptDialogFormatStringID 434 | HvhS5A 435 | INIntentParameterPromptDialogType 436 | Confirmation 437 | 438 | 439 | INIntentParameterTag 440 | 6 441 | INIntentParameterType 442 | Integer 443 | 444 | 445 | INIntentParameterConfigurable 446 | 447 | INIntentParameterDisplayName 448 | Secondary Currency 449 | INIntentParameterDisplayNameID 450 | ZRIceO 451 | INIntentParameterDisplayPriority 452 | 3 453 | INIntentParameterEnumType 454 | FiatCurrency 455 | INIntentParameterEnumTypeNamespace 456 | 88xZPY 457 | INIntentParameterMetadata 458 | 459 | INIntentParameterMetadataDefaultValue 460 | eur 461 | 462 | INIntentParameterName 463 | secondaryCurrency 464 | INIntentParameterPromptDialogs 465 | 466 | 467 | INIntentParameterPromptDialogCustom 468 | 469 | INIntentParameterPromptDialogType 470 | Configuration 471 | 472 | 473 | INIntentParameterPromptDialogCustom 474 | 475 | INIntentParameterPromptDialogType 476 | Primary 477 | 478 | 479 | INIntentParameterPromptDialogCustom 480 | 481 | INIntentParameterPromptDialogFormatString 482 | There are ${count} options matching ‘${secondaryCurrency}’. 483 | INIntentParameterPromptDialogFormatStringID 484 | Ut2O3M 485 | INIntentParameterPromptDialogType 486 | DisambiguationIntroduction 487 | 488 | 489 | INIntentParameterPromptDialogCustom 490 | 491 | INIntentParameterPromptDialogFormatString 492 | Just to confirm, you wanted ‘${secondaryCurrency}’? 493 | INIntentParameterPromptDialogFormatStringID 494 | exWAN8 495 | INIntentParameterPromptDialogType 496 | Confirmation 497 | 498 | 499 | INIntentParameterTag 500 | 8 501 | INIntentParameterType 502 | Integer 503 | 504 | 505 | INIntentParameterConfigurable 506 | 507 | INIntentParameterDisplayName 508 | Show Bitcoin Logo 509 | INIntentParameterDisplayNameID 510 | FDOABk 511 | INIntentParameterDisplayPriority 512 | 4 513 | INIntentParameterMetadata 514 | 515 | INIntentParameterMetadataDefaultValue 516 | 517 | INIntentParameterMetadataFalseDisplayName 518 | false 519 | INIntentParameterMetadataFalseDisplayNameID 520 | CAA4rr 521 | INIntentParameterMetadataTrueDisplayName 522 | true 523 | INIntentParameterMetadataTrueDisplayNameID 524 | Ck5tPY 525 | 526 | INIntentParameterName 527 | showBitcoinLogo 528 | INIntentParameterPromptDialogs 529 | 530 | 531 | INIntentParameterPromptDialogCustom 532 | 533 | INIntentParameterPromptDialogType 534 | Configuration 535 | 536 | 537 | INIntentParameterPromptDialogCustom 538 | 539 | INIntentParameterPromptDialogType 540 | Primary 541 | 542 | 543 | INIntentParameterTag 544 | 10 545 | INIntentParameterType 546 | Boolean 547 | 548 | 549 | INIntentResponse 550 | 551 | INIntentResponseCodes 552 | 553 | 554 | INIntentResponseCodeName 555 | success 556 | INIntentResponseCodeSuccess 557 | 558 | 559 | 560 | INIntentResponseCodeName 561 | failure 562 | 563 | 564 | 565 | INIntentTitle 566 | Moscow Time Configuration 567 | INIntentTitleID 568 | CNIGff 569 | INIntentType 570 | Custom 571 | INIntentVerb 572 | View 573 | 574 | 575 | INIntentCategory 576 | generic 577 | INIntentDescription 578 | Configure mempool.space display options 579 | INIntentDescriptionID 580 | JKy8Ff 581 | INIntentEligibleForWidgets 582 | 583 | INIntentIneligibleForSuggestions 584 | 585 | INIntentLastParameterTag 586 | 2 587 | INIntentName 588 | MempoolConfiguration 589 | INIntentParameters 590 | 591 | 592 | INIntentParameterConfigurable 593 | 594 | INIntentParameterDisplayName 595 | Show Bitcoin Logo 596 | INIntentParameterDisplayNameID 597 | 17ZqzB 598 | INIntentParameterDisplayPriority 599 | 1 600 | INIntentParameterMetadata 601 | 602 | INIntentParameterMetadataDefaultValue 603 | 604 | INIntentParameterMetadataFalseDisplayName 605 | false 606 | INIntentParameterMetadataFalseDisplayNameID 607 | cWt4u0 608 | INIntentParameterMetadataTrueDisplayName 609 | true 610 | INIntentParameterMetadataTrueDisplayNameID 611 | RdaYfr 612 | 613 | INIntentParameterName 614 | showBitcoinLogo 615 | INIntentParameterPromptDialogs 616 | 617 | 618 | INIntentParameterPromptDialogCustom 619 | 620 | INIntentParameterPromptDialogType 621 | Configuration 622 | 623 | 624 | INIntentParameterPromptDialogCustom 625 | 626 | INIntentParameterPromptDialogType 627 | Primary 628 | 629 | 630 | INIntentParameterTag 631 | 2 632 | INIntentParameterType 633 | Boolean 634 | 635 | 636 | INIntentResponse 637 | 638 | INIntentResponseCodes 639 | 640 | 641 | INIntentResponseCodeName 642 | success 643 | INIntentResponseCodeSuccess 644 | 645 | 646 | 647 | INIntentResponseCodeName 648 | failure 649 | 650 | 651 | 652 | INIntentTitle 653 | Mempool Configuration 654 | INIntentTitleID 655 | tWGjwd 656 | INIntentType 657 | Custom 658 | INIntentVerb 659 | Do 660 | 661 | 662 | INTypes 663 | 664 | 665 | 666 | --------------------------------------------------------------------------------