Initial commit
11
SatsPrice/Assets.xcassets/AccentColor.colorset/Contents.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
74
SatsPrice/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "bitcoin-calculator-1024 1.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "bitcoin-calculator-16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "bitcoin-calculator-32 1.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "bitcoin-calculator-32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "bitcoin-calculator-64.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "bitcoin-calculator-128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "bitcoin-calculator-256 1.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "bitcoin-calculator-256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "bitcoin-calculator-512 1.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "bitcoin-calculator-512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "bitcoin-calculator-1024.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 770 B |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
6
SatsPrice/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
122
SatsPrice/ContentView.swift
Normal file
@@ -0,0 +1,122 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// SatsPrice
|
||||
//
|
||||
// Created by Terry Yiu on 2/19/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import BigDecimal
|
||||
|
||||
struct ContentView: View {
|
||||
@ObservedObject private var satsViewModel = SatsViewModel()
|
||||
|
||||
@State private var spotPriceSource: SpotPriceSource = .coinbase
|
||||
|
||||
private let dateFormatter = DateFormatter()
|
||||
|
||||
private let spotPriceFetcherDelegator = SpotPriceFetcherDelegator()
|
||||
|
||||
init() {
|
||||
dateFormatter.dateStyle = .short
|
||||
dateFormatter.timeStyle = .short
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func updatePrice() async {
|
||||
do {
|
||||
guard let price = try await spotPriceFetcherDelegator.btcToUsd() else {
|
||||
satsViewModel.btcToUsdString = ""
|
||||
return
|
||||
}
|
||||
|
||||
satsViewModel.btcToUsdString = "\(price)"
|
||||
} catch {
|
||||
satsViewModel.btcToUsdString = ""
|
||||
}
|
||||
satsViewModel.lastUpdated = Date.now
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
Picker("Price Source", selection: $spotPriceSource) {
|
||||
ForEach(SpotPriceSource.allCases, id: \.self) {
|
||||
Text($0.description)
|
||||
}
|
||||
}
|
||||
.onChange(of: spotPriceSource) { newSpotPriceSource in
|
||||
spotPriceFetcherDelegator.spotPriceSource = newSpotPriceSource
|
||||
Task {
|
||||
await updatePrice()
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
TextField("", text: $satsViewModel.btcToUsdString)
|
||||
.disabled(true)
|
||||
Button(action: {
|
||||
Task {
|
||||
await updatePrice()
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("1 BTC to USD")
|
||||
} footer: {
|
||||
Text("Last updated: \(dateFormatter.string(from: satsViewModel.lastUpdated))")
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
Section {
|
||||
TextField("", text: $satsViewModel.satsString)
|
||||
.keyboardType(.numberPad)
|
||||
} header: {
|
||||
Text("Sats")
|
||||
}
|
||||
|
||||
Section {
|
||||
TextField("", text: $satsViewModel.btcString)
|
||||
.keyboardType(.decimalPad)
|
||||
} header: {
|
||||
Text("BTC")
|
||||
}
|
||||
|
||||
Section {
|
||||
TextField("", text: $satsViewModel.usdString)
|
||||
.keyboardType(.decimalPad)
|
||||
} header: {
|
||||
Text("USD")
|
||||
}
|
||||
#else
|
||||
Section {
|
||||
TextField("", text: $satsViewModel.satsString)
|
||||
} header: {
|
||||
Text("Sats")
|
||||
}
|
||||
|
||||
Section {
|
||||
TextField("", text: $satsViewModel.btcString)
|
||||
} header: {
|
||||
Text("BTC")
|
||||
}
|
||||
|
||||
Section {
|
||||
TextField("", text: $satsViewModel.usdString)
|
||||
} header: {
|
||||
Text("USD")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.task {
|
||||
await updatePrice()
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentView()
|
||||
}
|
||||
38
SatsPrice/Network/CoinGeckoSpotPriceFetcher.swift
Normal file
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// CoinGeckoSpotPriceFetcher.swift
|
||||
// SatsPrice
|
||||
//
|
||||
// Created by Terry Yiu on 2/19/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import BigDecimal
|
||||
|
||||
private struct CoinGeckoSpotPriceResponse: Codable {
|
||||
let bitcoin: CoinGeckoSpotPrice
|
||||
}
|
||||
|
||||
private struct CoinGeckoSpotPrice: Codable {
|
||||
let usd: Decimal
|
||||
}
|
||||
|
||||
class CoinGeckoSpotPriceFetcher : SpotPriceFetcher {
|
||||
private static let urlString = "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd&precision=18"
|
||||
|
||||
func btcToUsd() async throws -> BigDecimal? {
|
||||
do {
|
||||
guard let urlComponents = URLComponents(string: CoinGeckoSpotPriceFetcher.urlString), let url = urlComponents.url else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let (data, _) = try await URLSession.shared.data(from: url, delegate: nil)
|
||||
|
||||
let spotPriceResponse = try JSONDecoder().decode(CoinGeckoSpotPriceResponse.self, from: data)
|
||||
let spotPrice = spotPriceResponse.bitcoin
|
||||
|
||||
return BigDecimal(spotPrice.usd)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
46
SatsPrice/Network/CoinbaseSpotPriceFetcher.swift
Normal file
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// CoinbaseSpotPriceFetcher.swift
|
||||
// SatsPrice
|
||||
//
|
||||
// Created by Terry Yiu on 2/19/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import BigDecimal
|
||||
|
||||
private struct CoinbaseSpotPriceResponse: Codable {
|
||||
let data: CoinbaseSpotPrice
|
||||
}
|
||||
|
||||
private struct CoinbaseSpotPrice: Codable {
|
||||
let amount: String
|
||||
let base: String
|
||||
let currency: String
|
||||
}
|
||||
|
||||
class CoinbaseSpotPriceFetcher : SpotPriceFetcher {
|
||||
private static let urlString = "https://api.coinbase.com/v2/prices/BTC-USD/spot"
|
||||
private static let btc = "BTC"
|
||||
private static let usd = "USD"
|
||||
|
||||
func btcToUsd() async throws -> BigDecimal? {
|
||||
do {
|
||||
guard let urlComponents = URLComponents(string: CoinbaseSpotPriceFetcher.urlString), let url = urlComponents.url else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let (data, _) = try await URLSession.shared.data(from: url, delegate: nil)
|
||||
|
||||
let coinbaseSpotPriceResponse = try JSONDecoder().decode(CoinbaseSpotPriceResponse.self, from: data)
|
||||
let coinbaseSpotPrice = coinbaseSpotPriceResponse.data
|
||||
|
||||
guard coinbaseSpotPrice.base == CoinbaseSpotPriceFetcher.btc && coinbaseSpotPrice.currency == CoinbaseSpotPriceFetcher.usd else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return BigDecimal(coinbaseSpotPrice.amount)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
13
SatsPrice/Network/SpotPriceFetcher.swift
Normal file
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// SpotPriceFetcher.swift
|
||||
// SatsPrice
|
||||
//
|
||||
// Created by Terry Yiu on 2/19/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import BigDecimal
|
||||
|
||||
protocol SpotPriceFetcher {
|
||||
func btcToUsd() async throws -> BigDecimal?
|
||||
}
|
||||
29
SatsPrice/Network/SpotPriceFetcherDelegator.swift
Normal file
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// SpotPriceFetcherDelegator.swift
|
||||
// SatsPrice
|
||||
//
|
||||
// Created by Terry Yiu on 2/20/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import BigDecimal
|
||||
|
||||
class SpotPriceFetcherDelegator: SpotPriceFetcher {
|
||||
private let coinbaseSpotPriceFetcher = CoinbaseSpotPriceFetcher()
|
||||
private let coinGeckoSpotPriceFetcher = CoinGeckoSpotPriceFetcher()
|
||||
|
||||
var spotPriceSource: SpotPriceSource = .coinbase
|
||||
|
||||
private var delegate: SpotPriceFetcher {
|
||||
switch spotPriceSource {
|
||||
case .coinbase:
|
||||
coinbaseSpotPriceFetcher
|
||||
case .coingecko:
|
||||
coinGeckoSpotPriceFetcher
|
||||
}
|
||||
}
|
||||
|
||||
func btcToUsd() async throws -> BigDecimal? {
|
||||
return try await delegate.btcToUsd()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
10
SatsPrice/SatsPrice.entitlements
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
8
SatsPrice/SatsPrice.xcdatamodeld/.xccurrentversion
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>SatsPrice.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1" systemVersion="11A491" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="false" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Item" representedClassName="Item" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Item" positionX="-63" positionY="-18" width="128" height="44"/>
|
||||
</elements>
|
||||
</model>
|
||||
17
SatsPrice/SatsPriceApp.swift
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// SatsPriceApp.swift
|
||||
// SatsPrice
|
||||
//
|
||||
// Created by Terry Yiu on 2/19/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct SatsPriceApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
}
|
||||
67
SatsPrice/SatsViewModel.swift
Normal file
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// SatsViewModel.swift
|
||||
// SatsPrice
|
||||
//
|
||||
// Created by Terry Yiu on 2/19/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import BigDecimal
|
||||
|
||||
class SatsViewModel: ObservableObject {
|
||||
@Published private(set) var btcToUsd: BigDecimal = BigDecimal.nan
|
||||
@Published var lastUpdated: Date = Date.now
|
||||
|
||||
@Published private(set) var sats: BigDecimal = 0
|
||||
@Published private(set) var btc: BigDecimal = 0
|
||||
@Published private(set) var usd: BigDecimal = 0
|
||||
|
||||
var btcToUsdString: String {
|
||||
get { btcToUsd.asString(.plain) }
|
||||
set {
|
||||
self.btcToUsd = BigDecimal(newValue)
|
||||
self.usd = btc.multiply(btcToUsd, Rounding(.down, 20))
|
||||
}
|
||||
}
|
||||
|
||||
var satsString: String {
|
||||
get { sats.asString(.plain) }
|
||||
set {
|
||||
self.sats = BigDecimal(newValue)
|
||||
|
||||
let preciseDivide = sats.divide(100000000)
|
||||
if preciseDivide.isNaN {
|
||||
self.btc = sats.divide(100000000, Rounding(.down, 20))
|
||||
} else {
|
||||
self.btc = sats.divide(100000000)
|
||||
}
|
||||
|
||||
self.usd = btc.multiply(btcToUsd, Rounding(.down, 20))
|
||||
}
|
||||
}
|
||||
|
||||
var btcString: String {
|
||||
get { btc.asString(.plain) }
|
||||
set {
|
||||
self.btc = BigDecimal(newValue)
|
||||
self.sats = btc.multiply(100000000, Rounding(.down, 20))
|
||||
self.usd = btc.multiply(btcToUsd, Rounding(.down, 20))
|
||||
}
|
||||
}
|
||||
|
||||
var usdString: String {
|
||||
get { usd.asString(.plain) }
|
||||
set {
|
||||
self.usd = BigDecimal(newValue)
|
||||
|
||||
let preciseDivide = usd.divide(btcToUsd)
|
||||
if preciseDivide.isNaN {
|
||||
self.btc = usd.divide(btcToUsd, Rounding(.down, 20))
|
||||
} else {
|
||||
self.btc = usd.divide(btcToUsd)
|
||||
}
|
||||
|
||||
self.sats = btc.multiply(100000000, Rounding(.down, 20))
|
||||
}
|
||||
}
|
||||
}
|
||||
22
SatsPrice/SpotPriceSource.swift
Normal file
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// SpotPriceSource.swift
|
||||
// SatsPrice
|
||||
//
|
||||
// Created by Terry Yiu on 2/20/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum SpotPriceSource: CaseIterable, CustomStringConvertible {
|
||||
case coinbase
|
||||
case coingecko
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .coinbase:
|
||||
"Coinbase"
|
||||
case .coingecko:
|
||||
"CoinGecko"
|
||||
}
|
||||
}
|
||||
}
|
||||