Skip to content

Commit

Permalink
refactor: expose coroutine APIs and remove unnecessary async
Browse files Browse the repository at this point in the history
- Using `runBlocking` at implementation level is generally not recommended (see https://www.billjings.net/posts/title/foot-marksmanship-with-runblocking/?up=). A more idiomatic to Kotlin approach is to simply expose coroutine APIs, make sure the coroutines run on an appropriate dispatcher and let the consumer use the API as needed.
- Remove unnecessary `async + await()`. Instead call the suspending coroutine directly, there's no functional difference here.
  • Loading branch information
kirillzh committed Jun 25, 2024
1 parent ed8fd8c commit b196265
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 76 deletions.
3 changes: 1 addition & 2 deletions lib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,7 @@ dependencies {
implementation("org.slf4j:slf4j-api:1.7.30")
runtimeOnly("ch.qos.logback:logback-classic:1.4.12")

// TODO: Why isn't this needed?
// testImplementation(kotlin("test"))
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")
}

testing {
Expand Down
115 changes: 48 additions & 67 deletions lib/src/main/kotlin/me/tb/cashuclient/Wallet.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ import io.ktor.http.HttpMethod
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import me.tb.cashuclient.db.CashuDB
import me.tb.cashuclient.db.SQLiteDB
Expand Down Expand Up @@ -100,17 +99,15 @@ public class Wallet(
/**
* Query the mint for the active [Keyset] and set it as the active keyset.
*/
public fun getActiveKeyset(): Unit = runBlocking(Dispatchers.IO) {
public suspend fun getActiveKeyset(): Unit = withContext(Dispatchers.IO) {
logger.info("Getting active keyset from mint.")

val client = createClient()
val keyset = async {
val response = client.get("$mintUrl$ACTIVE_KEYSET_ENDPOINT")
val activeKeysetsResponse = response.body<ActiveKeysetsResponse>()
val response = client.get("$mintUrl$ACTIVE_KEYSET_ENDPOINT")
val activeKeysetsResponse = response.body<ActiveKeysetsResponse>()

// TODO: I'm not sure why there can be multiple active keysets at the same time. Open issue on specs repo.
activeKeysetsResponse.keysets.first().toKeyset()
}.await()
// TODO: I'm not sure why there can be multiple active keysets at the same time. Open issue on specs repo.
val keyset = activeKeysetsResponse.keysets.first().toKeyset()
client.close()
addKeyset(keyset)
}
Expand All @@ -119,20 +116,18 @@ public class Wallet(
/**
* Query the mint for the [Keyset] associated with a given [KeysetId].
*/
public fun getSpecificKeyset(keysetId: KeysetId): Keyset = runBlocking(Dispatchers.IO) {
public suspend fun getSpecificKeyset(keysetId: KeysetId): Keyset = withContext(Dispatchers.IO) {
logger.info("Getting specific keyset from mint.")

val keysetIdHex: String = keysetId.value
val client = createClient()
val specificKeyset = async {
// val response = client.get("$mintUrl$SPECIFIC_KEYSET_PATH$keysetIdHex").bodyAsText()
// val mintResponse = Json.decodeFromString(ActiveKeysetsResponse.serializer(), response)
val response = client.get("$mintUrl$SPECIFIC_KEYSET_ENDPOINT$keysetIdHex")
val specificKeysetResponse: SpecificKeysetResponse = response.body<SpecificKeysetResponse>()

// TODO: There should only be one keyset in the response it feels odd that the spec requires an array
specificKeysetResponse.keysets.first().toKeyset()
}.await()
// val response = client.get("$mintUrl$SPECIFIC_KEYSET_PATH$keysetIdHex").bodyAsText()
// val mintResponse = Json.decodeFromString(ActiveKeysetsResponse.serializer(), response)
val response = client.get("$mintUrl$SPECIFIC_KEYSET_ENDPOINT$keysetIdHex")
val specificKeysetResponse: SpecificKeysetResponse = response.body<SpecificKeysetResponse>()

// TODO: There should only be one keyset in the response it feels odd that the spec requires an array
val specificKeyset = specificKeysetResponse.keysets.first().toKeyset()
client.close()
specificKeyset
}
Expand All @@ -152,7 +147,7 @@ public class Wallet(
*
* @param amount The total value to mint.
*/
public fun mint(amount: Satoshi): Unit = runBlocking(Dispatchers.IO) {
public suspend fun mint(amount: Satoshi): Unit = withContext(Dispatchers.IO) {
logger.info("Requesting a mint.")

val client = createClient()
Expand All @@ -166,13 +161,11 @@ public class Wallet(
)
val mintingRequest: MintRequest = preMintBundle.buildMintRequest()

val response = async {
client.post("$mintUrl$MINT_ENDPOINT/bolt11") {
method = HttpMethod.Post
contentType(ContentType.Application.Json)
setBody(mintingRequest)
}
}.await()
val response = client.post("$mintUrl$MINT_ENDPOINT/bolt11") {
method = HttpMethod.Post
contentType(ContentType.Application.Json)
setBody(mintingRequest)
}
client.close()

val mintResponse: MintResponse = response.body()
Expand All @@ -185,19 +178,17 @@ public class Wallet(
// keeping the quote in memory, otherwise simply request a new quote. This is not optimal because a client
// might pay an invoice and then get wiped out before calling the mint again, but if it doesn't know the quote
// id then it will not be able to prove to the mint that it paid the invoice.
public fun requestMintQuote(amount: Satoshi, paymentMethod: PaymentMethod): MintQuoteData = runBlocking(Dispatchers.IO) {
public suspend fun requestMintQuote(amount: Satoshi, paymentMethod: PaymentMethod): MintQuoteData = withContext(Dispatchers.IO) {
logger.info("Requesting a mint quote for $amount satoshis.")

val client = createClient()
val mintQuoteRequest = MintQuoteRequest(amount.sat.toULong(), unit.toString())

val response = async {
client.post("$mintUrl$MINT_QUOTE_ENDPOINT$paymentMethod") {
method = HttpMethod.Post
contentType(ContentType.Application.Json)
setBody(mintQuoteRequest)
}
}.await()
val response = client.post("$mintUrl$MINT_QUOTE_ENDPOINT$paymentMethod") {
method = HttpMethod.Post
contentType(ContentType.Application.Json)
setBody(mintQuoteRequest)
}
client.close()

val mintQuoteResponse: MintQuoteResponse = response.body<MintQuoteResponse>()
Expand All @@ -206,12 +197,10 @@ public class Wallet(
MintQuoteData.fromMintQuoteResponse(amount, mintQuoteResponse)
}

public fun checkMintQuoteStatus(quoteId: String): MintQuoteResponse = runBlocking(Dispatchers.IO) {
public suspend fun checkMintQuoteStatus(quoteId: String): MintQuoteResponse = withContext(Dispatchers.IO) {
val client = createClient()

val response = async {
client.get("$mintUrl$MINT_QUOTE_STATUS_ENDPOINT$quoteId")
}.await()
val response = client.get("$mintUrl$MINT_QUOTE_STATUS_ENDPOINT$quoteId")
client.close()
println("Response from the mint regarding quote status: ${response.bodyAsText()}")
val mintQuoteResponse: MintQuoteResponse = response.body<MintQuoteResponse>()
Expand All @@ -229,7 +218,7 @@ public class Wallet(
*
* @param paymentRequest The lightning payment request.
*/
public fun melt(paymentRequest: PaymentRequest): Unit = runBlocking(Dispatchers.IO) {
public suspend fun melt(paymentRequest: PaymentRequest): Unit = withContext(Dispatchers.IO) {
val client = createClient()

val quote: MeltQuoteResponse = requestMeltQuote(paymentRequest)
Expand Down Expand Up @@ -273,13 +262,11 @@ public class Wallet(
val preMeltBundle: PreMeltBundle = PreMeltBundle.create(finalListOfProofs, quote.quoteId)
val meltRequest: MeltRequest = preMeltBundle.buildMeltRequest()

val response = async {
client.post("$mintUrl/melt") {
method = HttpMethod.Post
contentType(ContentType.Application.Json)
setBody(meltRequest)
}
}.await()
val response = client.post("$mintUrl/melt") {
method = HttpMethod.Post
contentType(ContentType.Application.Json)
setBody(meltRequest)
}
client.close()

val responseString: String = response.body<String>()
Expand All @@ -295,19 +282,17 @@ public class Wallet(
}

// TODO: PaymentRequest.read() now returns a Try<PaymentRequest> so we need to handle the error case.
public fun requestMeltQuote(pr: PaymentRequest): MeltQuoteResponse = runBlocking(Dispatchers.IO) {
public suspend fun requestMeltQuote(pr: PaymentRequest): MeltQuoteResponse = withContext(Dispatchers.IO) {
logger.info("Requesting a melt quote.")

val client = createClient()
val meltQuoteRequest = MeltQuoteRequest(pr, EcashUnit.SAT)

val response = async {
client.post("$mintUrl${MELT_QUOTE_ENDPOINT}bolt11") {
method = HttpMethod.Post
contentType(ContentType.Application.Json)
setBody(meltQuoteRequest)
}
}.await()
val response = client.post("$mintUrl${MELT_QUOTE_ENDPOINT}bolt11") {
method = HttpMethod.Post
contentType(ContentType.Application.Json)
setBody(meltQuoteRequest)
}
client.close()
println("Response from mint: ${response.bodyAsText()}")
val meltQuoteResponse: MeltQuoteResponse = response.body<MeltQuoteResponse>()
Expand All @@ -330,7 +315,7 @@ public class Wallet(
// Swap
// ---------------------------------------------------------------------------------------------

private fun swap(availableForSwap: List<ULong>, requiredAmount: ULong): NewAvailableDenominations = runBlocking {
private suspend fun swap(availableForSwap: List<ULong>, requiredAmount: ULong): NewAvailableDenominations = withContext(Dispatchers.IO) {
logger.info("Requesting a swap.")

val client = createClient()
Expand All @@ -339,13 +324,11 @@ public class Wallet(
val preSwapRequestBundle = PreSwapBundle.create(availableForSwap, requiredAmount, scopedActiveKeyset.keysetId)
val swapRequest = preSwapRequestBundle.buildSwapRequest()

val response = async {
client.post("$mintUrl$SWAP_ENDPOINT") {
method = HttpMethod.Post
contentType(ContentType.Application.Json)
setBody(swapRequest)
}
}.await()
val response = client.post("$mintUrl$SWAP_ENDPOINT") {
method = HttpMethod.Post
contentType(ContentType.Application.Json)
setBody(swapRequest)
}
client.close()

val swapResponse: SwapResponse = response.body()
Expand Down Expand Up @@ -411,13 +394,11 @@ public class Wallet(
// Info
// ---------------------------------------------------------------------------------------------

public fun getInfo(): MintInfo = runBlocking(Dispatchers.IO) {
public suspend fun getInfo(): MintInfo = withContext(Dispatchers.IO) {
logger.info("Getting info from mint.")

val client = createClient()
val response = async {
client.get("$mintUrl$INFO_ENDPOINT")
}.await()
val response = client.get("$mintUrl$INFO_ENDPOINT")
client.close()

val mintInfo: InfoResponse = response.body<InfoResponse>()
Expand Down
15 changes: 8 additions & 7 deletions lib/src/test/kotlin/me/tb/cashuclient/WalletTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package me.tb.cashuclient

import fr.acinq.bitcoin.Satoshi
import fr.acinq.lightning.payment.PaymentRequest
import kotlinx.coroutines.test.runTest
import me.tb.cashuclient.mint.MintQuoteData
import me.tb.cashuclient.types.EcashUnit
import me.tb.cashuclient.types.Keyset
Expand Down Expand Up @@ -100,7 +101,7 @@ class WalletTest {
// }

@Test
fun `Wallet successfully updates active keyset from mint after requesting it`() {
fun `Wallet successfully updates active keyset from mint after requesting it`() = runTest {
val jsonString = """{"1":"03142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9"}"""
val smallKeyset = Keyset.fromJson(jsonString)
val wallet = Wallet(activeKeyset = smallKeyset, mintUrl = "https://testnut.cashu.space", unit = EcashUnit.SAT)
Expand All @@ -122,7 +123,7 @@ class WalletTest {
}

@Test
fun `Wallet adds old keyset to list of inactive keysets`() {
fun `Wallet adds old keyset to list of inactive keysets`() = runTest {
val jsonString = """{"1":"03142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9"}"""
val smallKeyset = Keyset.fromJson(jsonString)
val wallet = Wallet(activeKeyset = smallKeyset, mintUrl = "https://testnut.cashu.space", unit = EcashUnit.SAT)
Expand All @@ -137,7 +138,7 @@ class WalletTest {
}

@Test
fun `Wallet can request specific keyset from mint`() {
fun `Wallet can request specific keyset from mint`() = runTest {
val jsonString = """{"1":"03142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9"}"""
val smallKeyset = Keyset.fromJson(jsonString)
val wallet = Wallet(
Expand All @@ -156,7 +157,7 @@ class WalletTest {

@Ignore("There are no mainnet or testnet mints that implement the v1 endpoints yet")
@Test
fun `Wallet can request a mint quote`() {
fun `Wallet can request a mint quote`() = runTest {
// val wallet: Wallet = Wallet(mintUrl = "https://8333.space:3338", unit = EcashUnit.SAT)
// val wallet: Wallet = Wallet(mintUrl = "https://legend.lnbits.com/cashu/api", unit = EcashUnit.SAT)
val wallet = Wallet(mintUrl = "https://testnut.cashu.space", unit = EcashUnit.SAT)
Expand All @@ -169,7 +170,7 @@ class WalletTest {

@Ignore("There are no mainnet or testnet mints that implement the v1 endpoints yet")
@Test
fun `Wallet can check the status quote for mint`() {
fun `Wallet can check the status quote for mint`() = runTest {
// mutinynet.mocksha.cash uses signet, which the ACINQ library doesn't support yet
// val wallet: Wallet = Wallet(mintUrl = "https://mutinynet.moksha.cash:3338", unit = EcashUnit.SAT)

Expand All @@ -192,15 +193,15 @@ class WalletTest {

@Ignore("Find a way to produce a valid testnet payment request for unit tests")
@Test
fun `Wallet can request a melt quote`() {
fun `Wallet can request a melt quote`() = runTest {
val wallet: Wallet = Wallet(mintUrl = "https://testnut.cashu.space", unit = EcashUnit.SAT)
val paymentRequest = PaymentRequest.read("LNBC10U1P3PJ257PP5YZTKWJCZ5FTL5LAXKAV23ZMZEKAW37ZK6KMV80PK4XAEV5QHTZ7QDPDWD3XGER9WD5KWM36YPRX7U3QD36KUCMGYP282ETNV3SHJCQZPGXQYZ5VQSP5USYC4LK9CHSFP53KVCNVQ456GANH60D89REYKDNGSMTJ6YW3NHVQ9QYYSSQJCEWM5CJWZ4A6RFJX77C490YCED6PEMK0UPKXHY89CMM7SCT66K8GNEANWYKZGDRWRFJE69H9U5U0W57RRCSYSAS7GADWMZXC8C6T0SPJAZUP6").get()
val quote = wallet.requestMeltQuote(paymentRequest)
println(quote)
}

@Test
fun `Wallet can request mint info`() {
fun `Wallet can request mint info`() = runTest {
val wallet: Wallet = Wallet(mintUrl = "https://testnut.cashu.space", unit = EcashUnit.SAT)
val mintInfo = wallet.getInfo()
// println(mintInfo)
Expand Down

0 comments on commit b196265

Please sign in to comment.