Initial commit.
This commit is contained in:
commit
764baf371d
13 changed files with 667 additions and 0 deletions
44
src/main/kotlin/net/thauvin/erik/crypto/CryptoException.kt
Normal file
44
src/main/kotlin/net/thauvin/erik/crypto/CryptoException.kt
Normal 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
|
||||
}
|
||||
}
|
113
src/main/kotlin/net/thauvin/erik/crypto/CryptoPrice.kt
Normal file
113
src/main/kotlin/net/thauvin/erik/crypto/CryptoPrice.kt
Normal 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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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") }
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue