Initial commit

This commit is contained in:
2024-02-19 01:58:22 -05:00
commit adac2bcd64
40 changed files with 2164 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

122
SatsPrice/ContentView.swift Normal file
View 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()
}

View 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
}
}
}

View 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
}
}
}

View 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?
}

View 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()
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View 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>

View 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>

View File

@@ -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>

View 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()
}
}
}

View 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))
}
}
}

View 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"
}
}
}