4 Commits

12 changed files with 512 additions and 78 deletions

View File

@@ -1,12 +1,41 @@
<div align="center">
<img src="./docs/assets/satsprice-logo.png" alt="SatsPrice Logo" title="SatsPrice logo" width="256"/>
# SatsPrice
This app fetches the price of Bitcoin relative to common ISO currencies from multiple sources, and converts inputted amounts between Sats, BTC, and the selected ISO currency.
This app fetches the price of Bitcoin relative to common fiat currencies from multiple sources, and converts inputted amounts between Sats, BTC, and the selected fiat currency.
[![GitHub downloads](https://img.shields.io/github/downloads/tyiu/sats-price/total?label=Downloads&labelColor=27303D&color=0D1117&logo=github&logoColor=FFFFFF&style=flat)](https://github.com/tyiu/sats-price/releases)
[![Last Version](https://img.shields.io/github/release/tyiu/sats-price?maxAge=3600&label=Stable&labelColor=06599d&color=043b69)](https://github.com/tyiu/sats-price)
[![License: GPL-3.0](https://img.shields.io/github/license/tyiu/sats-price?labelColor=27303D&color=0877d2)](/LICENSE)
## Download and Install
[<img src="./docs/assets/download_on_apple.svg"
alt="Download on the Apple App Store"
height="70">](https://apps.apple.com/app/satsprice/id6478230475)
[<img src="./docs/assets/download_on_zapstore.svg"
alt="Get it on Zap Store"
height="70">](https://github.com/zapstore/zapstore/releases)
[<img src="./docs/assets/download_on_obtainium.png"
alt="Get it on Obtaininum"
height="70">](https://github.com/ImranR98/Obtainium)
[<img src="./docs/assets/download_on_github.svg" alt="Get it on GitHub"
height="70">](https://github.com/tyiu/sats-price/releases)
## Supported Platforms
iOS 16.0+ • macOS 13.0+ • Android 10.0+
</div>
## Building
This is a free [Skip](https://skip.tools) dual-platform app project.
It builds a native app for both iOS and Android.
## Building
This project is both a stand-alone Swift Package Manager module,
as well as an Xcode project that builds and transpiles the project
into a Kotlin Gradle project for Android using the Skip plugin.
@@ -48,3 +77,10 @@ Android Studio's logcat tab for the transpiled Kotlin app.
This project depends on [Skip](https://skip.tools) to build as a multi-platform app.
The [Bitcoin Calculator](https://www.flaticon.com/free-icons/bitcoin-calculator) icon was created by Icon home and licensed as free for personal and commercial use with attribution.
The following free APIs are used:
- Coinbase
- [Get Exchange Rates](https://docs.cdp.coinbase.com/coinbase-app/docs/api-exchange-rates#get-exchange-rates)
- [Get Spot Price](https://docs.cdp.coinbase.com/coinbase-app/docs/api-prices#get-spot-price)
- CoinGecko
- [Coin Price by IDs](https://docs.coingecko.com/reference/simple-price)

View File

@@ -33,17 +33,6 @@ public struct ContentView: View {
)
}
public func selectedCurrencyBinding(_ currency: Locale.Currency) -> Binding<String> {
Binding(
get: {
satsViewModel.currencyValueStrings[currency, default: ""]
},
set: { priceString in
satsViewModel.currencyValueStrings[currency] = priceString
}
)
}
public var body: some View {
NavigationStack {
Form {
@@ -96,7 +85,7 @@ public struct ContentView: View {
}
} footer: {
if satsViewModel.exceedsMaximum {
Text("21000000 BTC is the maximum.")
Text("\(SatsViewModel.MAXIMUM_BTC.formatBTCString()) BTC is the maximum.")
}
}

View File

@@ -51,11 +51,11 @@ class CoinbasePriceFetcher : PriceFetcher {
return nil
}
#if !SKIP
#if !SKIP
return Decimal(string: coinbasePrice.amount)
#else
#else
return Decimal(coinbasePrice.amount)
#endif
#endif
} catch {
return nil
}

View File

@@ -14,10 +14,10 @@
}
}
},
"1 BTC to %@" : {
"%@ BTC is the maximum." : {
},
"21000000 BTC is the maximum." : {
"1 BTC to %@" : {
},
"BTC" : {

View File

@@ -13,6 +13,10 @@ import Foundation
import SwiftUI
class SatsViewModel: ObservableObject {
static let MAXIMUM_BTC = Decimal(21000000)
private static let SATS_IN_BTC = Decimal(100000000)
let model: SatsPriceModel
@Published var lastUpdated: Date?
@@ -25,7 +29,8 @@ class SatsViewModel: ObservableObject {
@Published var selectedCurrencies = Set<Locale.Currency>()
@Published var currencyValueStrings: [Locale.Currency: String] = [:]
var currencyPrices: [Locale.Currency: Decimal] = [:]
@Published var currencyPrices: [Locale.Currency: Decimal] = [:]
@Published var currencyPriceStrings: [Locale.Currency: String] = [:]
let currentCurrency: Locale.Currency = Locale.current.currency ?? Locale.Currency("USD")
@@ -90,6 +95,7 @@ class SatsViewModel: ObservableObject {
let prices = try await priceFetcherDelegator.convertBTC(toCurrencies: Array(currencies))
currencyPrices = prices
updateCurrencyPriceStrings()
updateCurrencyValueStrings()
} catch {
clearCurrencyValueStrings()
@@ -97,24 +103,49 @@ class SatsViewModel: ObservableObject {
lastUpdated = Date.now
}
func updateCurrencyPriceStrings() {
currencyPriceStrings = Dictionary(
uniqueKeysWithValues: currencyPrices.map { ($0.key, $0.value.formatString(currency: $0.key)) }
)
}
private func priceWithoutGroupingSeparator(_ priceString: String) -> String {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
let decimalSeparator = numberFormatter.decimalSeparator
return priceString.filter {
$0.isDigit || String($0) == decimalSeparator
}
}
var satsString: String {
get {
satsStringInternal
}
set {
guard satsStringInternal != newValue else {
let oldPriceWithoutGroupingSeparator = priceWithoutGroupingSeparator(satsStringInternal)
let newPriceWithoutGroupingSeparator = priceWithoutGroupingSeparator(newValue)
guard oldPriceWithoutGroupingSeparator != newPriceWithoutGroupingSeparator else {
return
}
satsStringInternal = newValue
satsStringInternal = newPriceWithoutGroupingSeparator
if let sats {
#if !SKIP
let btc = sats / Decimal(100000000)
// Formatting the internal string after modifying it only if the platform is Apple.
// Apple does not seem to call get after set until after focus is moved to a different component.
// Android, on the other hand, does call get immediately after set,
// which causes text entry issues if the user keeps on entering input.
satsStringInternal = sats.formatSatsString()
let btc = sats / SatsViewModel.SATS_IN_BTC
#else
let btc = sats.divide(Decimal(100000000), 20, java.math.RoundingMode.DOWN)
let btc = sats.divide(SatsViewModel.SATS_IN_BTC, 20, java.math.RoundingMode.DOWN)
#endif
btcStringInternal = btc.formatString()
btcStringInternal = btc.formatBTCString()
updateCurrencyValueStrings()
} else {
@@ -129,15 +160,26 @@ class SatsViewModel: ObservableObject {
btcStringInternal
}
set {
guard btcStringInternal != newValue else {
let oldPriceWithoutGroupingSeparator = priceWithoutGroupingSeparator(btcStringInternal)
let newPriceWithoutGroupingSeparator = priceWithoutGroupingSeparator(newValue)
guard oldPriceWithoutGroupingSeparator != newPriceWithoutGroupingSeparator else {
return
}
btcStringInternal = newValue
btcStringInternal = newPriceWithoutGroupingSeparator
if let btc {
let sats = btc * Decimal(100000000)
satsStringInternal = sats.formatString()
#if !SKIP
// Formatting the internal string after modifying it only if the platform is Apple.
// Apple does not seem to call get after set until after focus is moved to a different component.
// Android, on the other hand, does call get immediately after set,
// which causes text entry issues if the user keeps on entering input.
btcStringInternal = btc.formatBTCString()
#endif
let sats = btc * SatsViewModel.SATS_IN_BTC
satsStringInternal = sats.formatSatsString()
updateCurrencyValueStrings()
} else {
@@ -154,7 +196,7 @@ class SatsViewModel: ObservableObject {
for currency in currencies {
if let btcToCurrency = btcToCurrency(for: currency) {
currencyValueStrings[currency] = (btc * btcToCurrency).formatString()
currencyValueStrings[currency] = (btc * btcToCurrency).formatString(currency: currency)
} else {
currencyValueStrings[currency] = ""
}
@@ -176,25 +218,36 @@ class SatsViewModel: ObservableObject {
self.currencyValueStrings[currency, default: ""]
},
set: { newValue in
guard self.currencyValueStrings[currency] != newValue else {
let oldPriceWithoutGroupingSeparator = self.priceWithoutGroupingSeparator(self.currencyValueStrings[currency] ?? "")
let newPriceWithoutGroupingSeparator = self.priceWithoutGroupingSeparator(newValue)
guard oldPriceWithoutGroupingSeparator != newPriceWithoutGroupingSeparator else {
return
}
self.currencyValueStrings[currency] = newValue
self.currencyValueStrings[currency] = newPriceWithoutGroupingSeparator
if let currencyValue = self.currencyValue(for: currency) {
if let btcToCurrency = self.currencyPrices[currency] {
#if !SKIP
#if !SKIP
let btc = currencyValue / btcToCurrency
#else
#else
let btc = currencyValue.divide(btcToCurrency, 20, java.math.RoundingMode.DOWN)
#endif
self.btcStringInternal = btc.formatString()
#endif
self.btcStringInternal = btc.formatBTCString()
let sats = btc * Decimal(100000000)
self.satsStringInternal = sats.formatString()
let sats = btc * SatsViewModel.SATS_IN_BTC
self.satsStringInternal = sats.formatSatsString()
#if !SKIP
// Formatting the internal string after modifying it only if the platform is Apple.
// Apple does not seem to call get after set until after focus is moved to a different component.
// Android, on the other hand, does call get immediately after set,
// which causes text entry issues if the user keeps on entering input.
self.updateCurrencyValueStrings(excludedCurrency: nil)
#else
self.updateCurrencyValueStrings(excludedCurrency: currency)
#endif
} else {
self.satsStringInternal = ""
self.btcStringInternal = ""
@@ -215,7 +268,7 @@ class SatsViewModel: ObservableObject {
}
#if !SKIP
return Decimal(string: currencyValueString)
return Decimal(string: priceWithoutGroupingSeparator(currencyValueString))
#else
do {
return Decimal(currencyValueString)
@@ -232,23 +285,38 @@ class SatsViewModel: ObservableObject {
func btcToCurrencyString(for currency: Locale.Currency) -> Binding<String> {
Binding<String>(
get: {
self.currencyPrices[currency]?.formatString() ?? ""
self.currencyPriceStrings[currency, default: ""]
},
set: { newValue in
let oldPriceWithoutGroupingSeparator = self.priceWithoutGroupingSeparator(self.currencyPriceStrings[currency, default: ""])
let newPriceWithoutGroupingSeparator = self.priceWithoutGroupingSeparator(newValue)
guard oldPriceWithoutGroupingSeparator != newPriceWithoutGroupingSeparator else {
return
}
self.currencyPriceStrings[currency] = newPriceWithoutGroupingSeparator
#if !SKIP
if let newPrice = Decimal(string: newValue), self.currencyPrices[currency] != newPrice {
self.currencyPrices[currency] = Decimal(string: newValue)
if let newPrice = Decimal(string: newPriceWithoutGroupingSeparator), self.currencyPrices[currency] != newPrice {
self.currencyPrices[currency] = newPrice
// Formatting the internal string after modifying it only if the platform is Apple.
// Apple does not seem to call get after set until after focus is moved to a different
// component. Android, on the other hand, does call get immediately after set,
// which causes text entry issues if the user keeps on entering input.
self.currencyPriceStrings[currency] = newPrice.formatString(currency: currency)
if let btc = self.btc {
self.currencyValueStrings[currency] = (btc * newPrice).formatString()
self.currencyValueStrings[currency] = (btc * newPrice).formatString(currency: currency)
} else {
self.currencyValueStrings[currency] = ""
}
}
#else
do {
if let newPrice = Decimal(newValue), self.currencyPrices[currency] != newPrice {
self.currencyPrices[currency] = Decimal(newValue)
if let newPrice = Decimal(newPriceWithoutGroupingSeparator), self.currencyPrices[currency] != newPrice {
self.currencyPrices[currency] = newPrice
if let btc = self.btc {
self.currencyValueStrings[currency] = (btc * newPrice).formatString()
@@ -265,11 +333,12 @@ class SatsViewModel: ObservableObject {
}
var sats: Decimal? {
let priceWithoutGroupingSeparator = priceWithoutGroupingSeparator(satsStringInternal)
#if !SKIP
return Decimal(string: satsStringInternal)
return Decimal(string: priceWithoutGroupingSeparator)
#else
do {
return Decimal(satsStringInternal)
return Decimal(priceWithoutGroupingSeparator)
} catch {
return nil
}
@@ -277,11 +346,12 @@ class SatsViewModel: ObservableObject {
}
var btc: Decimal? {
let priceWithoutGroupingSeparator = priceWithoutGroupingSeparator(btcStringInternal)
#if !SKIP
return Decimal(string: btcStringInternal)
return Decimal(string: priceWithoutGroupingSeparator)
#else
do {
return Decimal(btcStringInternal)
return Decimal(priceWithoutGroupingSeparator)
} catch {
return nil
}
@@ -289,7 +359,7 @@ class SatsViewModel: ObservableObject {
}
var exceedsMaximum: Bool {
if let btc, btc > Decimal(21000000) {
if let btc, btc > SatsViewModel.MAXIMUM_BTC {
return true
}
return false
@@ -302,6 +372,54 @@ extension Decimal {
return String(describing: self)
#else
return stripTrailingZeros().toPlainString()
#endif
}
func formatString(minimumFractionDigits: Int, maximumFractionDigits: Int, usesGroupingSeparator: Bool) -> String {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
numberFormatter.minimumFractionDigits = minimumFractionDigits
numberFormatter.maximumFractionDigits = maximumFractionDigits
numberFormatter.usesGroupingSeparator = usesGroupingSeparator
#if !SKIP
return numberFormatter.string(from: NSDecimalNumber(decimal: self)) ?? String(describing: self)
#else
return numberFormatter.string(from: android.icu.math.BigDecimal(self as java.math.BigDecimal) as NSNumber) ?? stripTrailingZeros().toPlainString()
#endif
}
func formatSatsString() -> String {
formatString(minimumFractionDigits: 0, maximumFractionDigits: 0, usesGroupingSeparator: true)
}
func formatBTCString() -> String {
formatString(minimumFractionDigits: 0, maximumFractionDigits: 8, usesGroupingSeparator: true)
}
func formatString(currency: Locale.Currency) -> String {
#if !SKIP
let currencyFormatter = NumberFormatter()
currencyFormatter.numberStyle = .currency
currencyFormatter.currencyCode = currency.identifier
return formatString(
minimumFractionDigits: currencyFormatter.minimumFractionDigits,
maximumFractionDigits: currencyFormatter.maximumFractionDigits,
usesGroupingSeparator: currencyFormatter.usesGroupingSeparator
)
#else
let javaCurrency = java.util.Currency.getInstance(currency.identifier)
return formatString(
minimumFractionDigits: javaCurrency.getDefaultFractionDigits(),
maximumFractionDigits: javaCurrency.getDefaultFractionDigits(),
usesGroupingSeparator: true
)
#endif
}
}
private extension Character {
var isDigit: Bool {
self >= "0" && self <= "9"
}
}

View File

@@ -16,8 +16,9 @@ final class SatsViewModelTests: XCTestCase {
let currency = Locale.Currency("USD")
func testSatsViewModel() {
let satsViewModel = SatsViewModel()
func testSatsViewModel() throws {
let satsPriceModel = try XCTUnwrap(SatsPriceModel(url: nil))
let satsViewModel = SatsViewModel(model: satsPriceModel)
satsViewModel.btcToCurrencyString(for: currency).wrappedValue = "54321"
// Test BTC updates.
@@ -32,8 +33,8 @@ final class SatsViewModelTests: XCTestCase {
XCTAssertEqual(satsViewModel.currencyValue(for: currency), Decimal("54321"))
#endif
XCTAssertEqual(satsViewModel.btcString, "1")
XCTAssertEqual(satsViewModel.satsString, "100000000")
XCTAssertEqual(satsViewModel.currencyValueString(for: currency).wrappedValue, "54321")
XCTAssertEqual(satsViewModel.satsString, "100,000,000")
XCTAssertEqual(satsViewModel.currencyValueString(for: currency).wrappedValue, "54,321.00")
// Test Sats updates.
satsViewModel.satsString = "200000000"
@@ -47,8 +48,8 @@ final class SatsViewModelTests: XCTestCase {
XCTAssertEqual(satsViewModel.currencyValue(for: currency), Decimal("108642"))
#endif
XCTAssertEqual(satsViewModel.btcString, "2")
XCTAssertEqual(satsViewModel.satsString, "200000000")
XCTAssertEqual(satsViewModel.currencyValueString(for: currency).wrappedValue, "108642")
XCTAssertEqual(satsViewModel.satsString, "200,000,000")
XCTAssertEqual(satsViewModel.currencyValueString(for: currency).wrappedValue, "108,642.00")
// Test currency value updates.
satsViewModel.currencyValueString(for: currency).wrappedValue = "162963"
@@ -62,44 +63,43 @@ final class SatsViewModelTests: XCTestCase {
XCTAssertEqual(satsViewModel.currencyValue(for: currency), Decimal("162963"))
#endif
XCTAssertEqual(satsViewModel.btcString, "3")
XCTAssertEqual(satsViewModel.satsString, "300000000")
XCTAssertEqual(satsViewModel.currencyValueString(for: currency).wrappedValue, "162963")
XCTAssertEqual(satsViewModel.satsString, "300,000,000")
XCTAssertEqual(satsViewModel.currencyValueString(for: currency).wrappedValue, "162,963.00")
// Test fractional amounts.
// Precision between platforms on this calculation is different so we have different assertions for each.
satsViewModel.currencyValueString(for: currency).wrappedValue = "1"
#if !SKIP
XCTAssertEqual(satsViewModel.btc, Decimal(string: "0.00001840908672520756245282671526665562"))
XCTAssertEqual(satsViewModel.btcString, "0.00001840908672520756245282671526665562")
XCTAssertEqual(satsViewModel.sats, Decimal(string: "1840.908672520756245282671526665562"))
XCTAssertEqual(satsViewModel.satsString, "1840.908672520756245282671526665562")
XCTAssertEqual(satsViewModel.btc, Decimal(string: "0.00001841"))
XCTAssertEqual(satsViewModel.btcString, "0.00001841")
XCTAssertEqual(satsViewModel.sats, Decimal(string: "1841"))
XCTAssertEqual(satsViewModel.satsString, "1,841")
XCTAssertEqual(satsViewModel.currencyValue(for: currency), Decimal(string: "1"))
#else
XCTAssertEqual(satsViewModel.btc, Decimal("0.00001840908672520756"))
XCTAssertEqual(satsViewModel.btcString, "0.00001840908672520756")
XCTAssertEqual(satsViewModel.sats, Decimal("1840.908672520756"))
XCTAssertEqual(satsViewModel.satsString, "1840.908672520756")
XCTAssertEqual(satsViewModel.btc, Decimal("0.00001841"))
XCTAssertEqual(satsViewModel.btcString, "0.00001841")
XCTAssertEqual(satsViewModel.sats, Decimal("1841"))
XCTAssertEqual(satsViewModel.satsString, "1,841")
XCTAssertEqual(satsViewModel.currencyValue(for: currency), Decimal("1"))
#endif
XCTAssertEqual(satsViewModel.currencyValueString(for: currency).wrappedValue, "1")
XCTAssertEqual(satsViewModel.currencyValueString(for: currency).wrappedValue, "1.00")
// Test large amounts that exceed the cap of 21M BTC.
// Precision between platforms on this calculation is different so we have different assertions for each.
satsViewModel.currencyValueString(for: currency).wrappedValue = "11407419999999"
#if !SKIP
XCTAssertEqual(satsViewModel.btc, Decimal(string: "210000184.09084884298889932070469983984"))
XCTAssertEqual(satsViewModel.btcString, "210000184.09084884298889932070469983984")
XCTAssertEqual(satsViewModel.sats, Decimal(string: "21000018409084884.298889932070469983984"))
XCTAssertEqual(satsViewModel.satsString, "21000018409084884.298889932070469983984")
XCTAssertEqual(satsViewModel.btc, Decimal(string: "210000184.09084884"))
XCTAssertEqual(satsViewModel.btcString, "210,000,184.09084884")
XCTAssertEqual(satsViewModel.sats, Decimal(string: "21000018409084884"))
XCTAssertEqual(satsViewModel.satsString, "21,000,018,409,084,884")
XCTAssertEqual(satsViewModel.currencyValue(for: currency), Decimal(string: "11407419999999"))
#else
XCTAssertEqual(satsViewModel.btc, Decimal("210000184.0908488429888993207"))
XCTAssertEqual(satsViewModel.btcString, "210000184.0908488429888993207")
XCTAssertEqual(satsViewModel.sats, Decimal("21000018409084884.29888993207"))
XCTAssertEqual(satsViewModel.satsString, "21000018409084884.29888993207")
XCTAssertEqual(satsViewModel.btc, Decimal("210000184.09084884"))
XCTAssertEqual(satsViewModel.btcString, "210000184.09084884")
XCTAssertEqual(satsViewModel.sats, Decimal("21000018409084884"))
XCTAssertEqual(satsViewModel.satsString, "21000018409084884")
XCTAssertEqual(satsViewModel.currencyValue, Decimal("11407419999999"))
#endif
XCTAssertEqual(satsViewModel.currencyValueString(for: currency).wrappedValue, "11407419999999")
XCTAssertEqual(satsViewModel.currencyValueString(for: currency).wrappedValue, "11,407,419,999,999.00")
}
}

View File

@@ -0,0 +1,46 @@
<svg id="livetype" xmlns="http://www.w3.org/2000/svg" width="119.66407" height="40" viewBox="0 0 119.66407 40">
<title>Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917</title>
<g>
<g>
<g>
<path d="M110.13477,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.43779,6.43779,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01515.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27445,6.27445,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H110.13477c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50643,13.50643,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.5071,13.5071,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76754,6.76754,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269-.3594-.002-.7246-.002-1.084-.002Z" style="fill: #a6a6a6"/>
<path d="M8.44483,39.125c-.30468,0-.602-.0039-.90429-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72186,5.72186,0,0,1-.543-1.6572,12.41351,12.41351,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37039,12.37039,0,0,1,.16553-1.87207,5.7555,5.7555,0,0,1,.54346-1.6621A5.37349,5.37349,0,0,1,2.61183,2.61768,5.56543,5.56543,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58589,12.58589,0,0,1,7.543.88721L8.44532.875H111.21387l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59374,5.59374,0,0,1,2.415,2.41993,5.76267,5.76267,0,0,1,.5352,1.64892,12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73126,12.73126,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.54,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z"/>
</g>
<g id="_Group_" data-name="&lt;Group&gt;">
<g id="_Group_2" data-name="&lt;Group&gt;">
<g id="_Group_3" data-name="&lt;Group&gt;">
<path id="_Path_" data-name="&lt;Path&gt;" d="M24.76888,20.30068a4.94881,4.94881,0,0,1,2.35656-4.15206,5.06566,5.06566,0,0,0-3.99116-2.15768c-1.67924-.17626-3.30719,1.00483-4.1629,1.00483-.87227,0-2.18977-.98733-3.6085-.95814a5.31529,5.31529,0,0,0-4.47292,2.72787c-1.934,3.34842-.49141,8.26947,1.3612,10.97608.9269,1.32535,2.01018,2.8058,3.42763,2.7533,1.38706-.05753,1.9051-.88448,3.5794-.88448,1.65876,0,2.14479.88448,3.591.8511,1.48838-.02416,2.42613-1.33124,3.32051-2.66914a10.962,10.962,0,0,0,1.51842-3.09251A4.78205,4.78205,0,0,1,24.76888,20.30068Z" style="fill: #fff"/>
<path id="_Path_2" data-name="&lt;Path&gt;" d="M22.03725,12.21089a4.87248,4.87248,0,0,0,1.11452-3.49062,4.95746,4.95746,0,0,0-3.20758,1.65961,4.63634,4.63634,0,0,0-1.14371,3.36139A4.09905,4.09905,0,0,0,22.03725,12.21089Z" style="fill: #fff"/>
</g>
</g>
<g>
<path d="M42.30227,27.13965h-4.7334l-1.13672,3.35645H34.42727l4.4834-12.418h2.083l4.4834,12.418H43.438ZM38.0591,25.59082h3.752l-1.84961-5.44727h-.05176Z" style="fill: #fff"/>
<path d="M55.15969,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H48.4302v1.50586h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C53.645,21.34766,55.15969,23.16406,55.15969,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C52.30227,29.01563,53.24953,27.81934,53.24953,25.96973Z" style="fill: #fff"/>
<path d="M65.12453,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H58.395v1.50586h.03418A3.21162,3.21162,0,0,1,61.312,21.34766C63.60988,21.34766,65.12453,23.16406,65.12453,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C62.26711,29.01563,63.21438,27.81934,63.21438,25.96973Z" style="fill: #fff"/>
<path d="M71.71047,27.03613c.1377,1.23145,1.334,2.04,2.96875,2.04,1.56641,0,2.69336-.80859,2.69336-1.91895,0-.96387-.67969-1.541-2.28906-1.93652l-1.60937-.3877c-2.28027-.55078-3.33887-1.61719-3.33887-3.34766,0-2.14258,1.86719-3.61426,4.51855-3.61426,2.624,0,4.42285,1.47168,4.4834,3.61426h-1.876c-.1123-1.23926-1.13672-1.9873-2.63379-1.9873s-2.52148.75684-2.52148,1.8584c0,.87793.6543,1.39453,2.25488,1.79l1.36816.33594c2.54785.60254,3.60645,1.626,3.60645,3.44238,0,2.32324-1.85059,3.77832-4.79395,3.77832-2.75391,0-4.61328-1.4209-4.7334-3.667Z" style="fill: #fff"/>
<path d="M83.34621,19.2998v2.14258h1.72168v1.47168H83.34621v4.99121c0,.77539.34473,1.13672,1.10156,1.13672a5.80752,5.80752,0,0,0,.61133-.043v1.46289a5.10351,5.10351,0,0,1-1.03223.08594c-1.833,0-2.54785-.68848-2.54785-2.44434V22.91406H80.16262V21.44238H81.479V19.2998Z" style="fill: #fff"/>
<path d="M86.065,25.96973c0-2.84863,1.67773-4.63867,4.29395-4.63867,2.625,0,4.29492,1.79,4.29492,4.63867,0,2.85645-1.66113,4.63867-4.29492,4.63867C87.72609,30.6084,86.065,28.82617,86.065,25.96973Zm6.69531,0c0-1.9541-.89551-3.10742-2.40137-3.10742s-2.40039,1.16211-2.40039,3.10742c0,1.96191.89453,3.10645,2.40039,3.10645S92.76027,27.93164,92.76027,25.96973Z" style="fill: #fff"/>
<path d="M96.18606,21.44238h1.77246v1.541h.043a2.1594,2.1594,0,0,1,2.17773-1.63574,2.86616,2.86616,0,0,1,.63672.06934v1.73828a2.59794,2.59794,0,0,0-.835-.1123,1.87264,1.87264,0,0,0-1.93652,2.083v5.37012h-1.8584Z" style="fill: #fff"/>
<path d="M109.3843,27.83691c-.25,1.64355-1.85059,2.77148-3.89844,2.77148-2.63379,0-4.26855-1.76465-4.26855-4.5957,0-2.83984,1.64355-4.68164,4.19043-4.68164,2.50488,0,4.08008,1.7207,4.08008,4.46582v.63672h-6.39453v.1123a2.358,2.358,0,0,0,2.43555,2.56445,2.04834,2.04834,0,0,0,2.09082-1.27344Zm-6.28223-2.70215h4.52637a2.1773,2.1773,0,0,0-2.2207-2.29785A2.292,2.292,0,0,0,103.10207,25.13477Z" style="fill: #fff"/>
</g>
</g>
</g>
<g id="_Group_4" data-name="&lt;Group&gt;">
<g>
<path d="M37.82619,8.731a2.63964,2.63964,0,0,1,2.80762,2.96484c0,1.90625-1.03027,3.002-2.80762,3.002H35.67092V8.731Zm-1.22852,5.123h1.125a1.87588,1.87588,0,0,0,1.96777-2.146,1.881,1.881,0,0,0-1.96777-2.13379h-1.125Z" style="fill: #fff"/>
<path d="M41.68068,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C44.57522,13.99463,45.01369,13.42432,45.01369,12.44434Z" style="fill: #fff"/>
<path d="M51.57326,14.69775h-.92187l-.93066-3.31641h-.07031l-.92676,3.31641h-.91309l-1.24121-4.50293h.90137l.80664,3.436h.06641l.92578-3.436h.85254l.92578,3.436h.07031l.80273-3.436h.88867Z" style="fill: #fff"/>
<path d="M53.85354,10.19482H54.709v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915h-.88867V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
<path d="M59.09377,8.437h.88867v6.26074h-.88867Z" style="fill: #fff"/>
<path d="M61.21779,12.44434a2.13346,2.13346,0,1,1,4.24756,0,2.1338,2.1338,0,1,1-4.24756,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C64.11232,13.99463,64.5508,13.42432,64.5508,12.44434Z" style="fill: #fff"/>
<path d="M66.4009,13.42432c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031v-.38867c0-.47559-.31445-.74414-.92187-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76563.562,1.76563,1.51318v3.07666h-.85547v-.63281h-.07031a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,66.4009,13.42432Zm2.89453-.38477v-.37646l-1.09961.07031c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,69.29543,13.03955Z" style="fill: #fff"/>
<path d="M71.34816,12.44434c0-1.42285.73145-2.32422,1.86914-2.32422a1.484,1.484,0,0,1,1.38086.79h.06641V8.437h.88867v6.26074h-.85156v-.71143h-.07031a1.56284,1.56284,0,0,1-1.41406.78564C72.0718,14.772,71.34816,13.87061,71.34816,12.44434Zm.918,0c0,.95508.4502,1.52979,1.20313,1.52979.749,0,1.21191-.583,1.21191-1.52588,0-.93848-.46777-1.52979-1.21191-1.52979C72.72121,10.91846,72.26613,11.49707,72.26613,12.44434Z" style="fill: #fff"/>
<path d="M79.23,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C82.12453,13.99463,82.563,13.42432,82.563,12.44434Z" style="fill: #fff"/>
<path d="M84.66945,10.19482h.85547v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915H87.605V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
<path d="M93.51516,9.07373v1.1416h.97559v.74854h-.97559V13.2793c0,.47168.19434.67822.63672.67822a2.96657,2.96657,0,0,0,.33887-.02051v.74023a2.9155,2.9155,0,0,1-.4834.04541c-.98828,0-1.38184-.34766-1.38184-1.21582v-2.543h-.71484v-.74854h.71484V9.07373Z" style="fill: #fff"/>
<path d="M95.70461,8.437h.88086v2.48145h.07031a1.3856,1.3856,0,0,1,1.373-.80664,1.48339,1.48339,0,0,1,1.55078,1.67871v2.90723H98.69v-2.688c0-.71924-.335-1.0835-.96289-1.0835a1.05194,1.05194,0,0,0-1.13379,1.1416v2.62988h-.88867Z" style="fill: #fff"/>
<path d="M104.76125,13.48193a1.828,1.828,0,0,1-1.95117,1.30273A2.04531,2.04531,0,0,1,100.73,12.46045a2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27V12.688h-3.17969v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,101.63527,12.03076Z" style="fill: #fff"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>

After

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

7
zapstore.yaml Normal file
View File

@@ -0,0 +1,7 @@
satsprice:
android:
name: SatsPrice
description: SatsPrice fetches the price of Bitcoin relative to common ISO currencies from multiple sources, and converts inputted amounts between Sats, BTC, and the selected ISO currency.
repository: https://github.com/tyiu/sats-price
artifacts:
- SatsPrice-v%v.apk