├── .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 | 
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 | 
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 |
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 |
--------------------------------------------------------------------------------