Initial commit.

This commit is contained in:
Erik C. Thauvin 2021-05-08 01:35:55 -07:00
commit 764baf371d
13 changed files with 667 additions and 0 deletions

View file

@ -0,0 +1,44 @@
/*
* CryptoExtension.kt
*
* Copyright (c) 2021, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.crypto
@Suppress("EmptySecondaryConstructor")
class CryptoException : Exception {
constructor(message: String, cause: Throwable) : super(message, cause) {}
constructor(message: String) : super(message) {}
constructor(cause: Throwable) : super(cause) {}
companion object {
private const val serialVersionUID = 1L
}
}

View file

@ -0,0 +1,113 @@
/*
* CryptoPrice.kt
*
* Copyright (c) 2021, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.crypto
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import java.io.IOException
import java.net.URL
import java.time.LocalDate
data class Price(val base: String, val currency: String, val amount: Double)
open class CryptoPrice private constructor() {
companion object {
// Coinbase API URL
private const val COINBASE_API_URL = "https://api.coinbase.com/v2/"
/**
* Make an API call.
*/
@JvmStatic
@JvmOverloads
@Throws(CryptoException::class)
fun apiCall(paths: List<String>, params: Map<String, String> = emptyMap()): String {
val client = OkHttpClient()
val url = COINBASE_API_URL.toHttpUrl().newBuilder()
paths.forEach {
url.addPathSegment(it)
}
params.forEach {
url.addQueryParameter(it.key, it.value)
}
val request = Request.Builder().url(url.build()).build()
val response = client.newCall(request).execute()
val body = response.body?.string()
if (body != null) {
val json = JSONObject(body)
if (!response.isSuccessful) {
if (json.has("errors")) {
val data = json.getJSONArray("errors")
throw CryptoException(data.getJSONObject(0).getString("message"))
} else {
throw CryptoException("Invalid API response. (${response.code}")
}
} else {
return body
}
} else {
throw CryptoException("Empty API response.")
}
}
@JvmStatic
fun main(args: Array<String>) {
val price = marketPrice("BTC")
println("BTC: ${price.amount}")
}
/**
* Retrieves the current market price.
*/
@JvmStatic
@JvmOverloads
@Throws(CryptoException::class)
fun marketPrice(base: String, currency: String = "USD", date: LocalDate? = null): Price {
val params = mutableMapOf<String, String>()
if (date != null) {
params.put("date", "$date")
}
val body = apiCall(listOf("prices", "$base-$currency", "spot"), params)
val json = JSONObject(body)
if (json.has("data")) {
val data = json.getJSONObject("data")
return Price(data.getString("base"), data.getString("currency"), data.getString("amount").toDouble())
} else {
throw CryptoException("Missing JSON data.")
}
}
}
}

View file

@ -0,0 +1,65 @@
package net.thauvin.erik.crypto
import net.thauvin.erik.crypto.CryptoPrice.Companion.marketPrice
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
import java.time.LocalDate
/**
* The `CryptoPriceTest` class.
*/
class CryptoPriceTest {
@Test
@Throws(CryptoException::class)
fun testBTCPrice() {
val price = marketPrice("BTC")
assertEquals(price.base, "BTC", "BTC")
assertEquals(price.currency, "USD", "is USD")
assertTrue(price.amount > 0.00, "BTC > 0")
}
@Test
@Throws(CryptoException::class)
fun testETHPrice() {
val price = marketPrice("ETH", "EUR")
assertEquals(price.base, "ETH", "ETH")
assertEquals(price.currency, "EUR", "is EUR")
assertTrue(price.amount > 0.00, "ETH > 0")
}
@Test
@Throws(CryptoException::class)
fun testETH2Price() {
val price = marketPrice("ETH2", "GBP")
assertEquals(price.base, "ETH2", "ETH2")
assertEquals(price.currency, "GBP", "is GBP")
assertTrue(price.amount > 0.00, "GBP > 0")
}
@Test
@Throws(CryptoException::class)
fun testBCHPrice() {
val price = marketPrice("BCH", "GBP", LocalDate.now().minusDays(1))
assertEquals(price.base, "BCH", "ETH2")
assertEquals(price.currency, "GBP", "is GBP")
assertTrue(price.amount > 0.00, "BCH > 0")
}
@Test
@Throws(CryptoException::class)
fun testMarketPriceExceptions() {
assertFailsWith(
message = "FOO did not fail",
exceptionClass = CryptoException::class,
block = { marketPrice("FOO") }
)
assertFailsWith(
message = "BAR did not fail",
exceptionClass = CryptoException::class,
block = { marketPrice("BTC", "BAR") }
)
}
}