Skip to content

Commit

Permalink
refactor: use SqlDelight for database implementation instead of Exposed
Browse files Browse the repository at this point in the history
Expose is a JVM only lib. SqlDelight provides KMP support out of the box.

This migrates the internal db implementation to SqlDelight.

Work towards resolving issue #3.
  • Loading branch information
kirillzh authored and thunderbiscuit committed Jun 29, 2024
1 parent 73ea1b0 commit dc44007
Show file tree
Hide file tree
Showing 15 changed files with 188 additions and 232 deletions.
19 changes: 11 additions & 8 deletions lib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ plugins {
id("org.gradle.java-library")
id("org.gradle.maven-publish")
id("org.jetbrains.dokka") version "1.9.10"
id("app.cash.sqldelight") version "2.0.2"
}

sqldelight {
databases {
create("Database") {
packageName.set("me.tb.cashulib")
}
}
}

repositories {
Expand All @@ -28,14 +37,8 @@ dependencies {
implementation("fr.acinq.secp256k1:secp256k1-kmp-jni-jvm-darwin:0.11.0")
implementation("fr.acinq.lightning:lightning-kmp:1.5.15")

// Exposed
implementation("org.jetbrains.exposed:exposed-core:0.40.1")
implementation("org.jetbrains.exposed:exposed-dao:0.40.1")
implementation("org.jetbrains.exposed:exposed-jdbc:0.40.1")
implementation("org.jetbrains.exposed:exposed-java-time:0.40.1")

// SQLite
implementation("org.xerial:sqlite-jdbc:3.42.0.0")
// SqlDelight
implementation("app.cash.sqldelight:sqlite-driver:2.0.2")

// Ktor
implementation("io.ktor:ktor-client-core-jvm:2.3.1")
Expand Down
4 changes: 2 additions & 2 deletions lib/src/main/kotlin/me/tb/cashuclient/Wallet.kt
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ public class Wallet(
"The sum of tokens to spend must be equal to the sum of the required tokens."
}

val preMeltBundle: PreMeltBundle = PreMeltBundle.create(finalListOfProofs, quote.quoteId)
val preMeltBundle: PreMeltBundle = PreMeltBundle.create(finalListOfProofs, quote.quoteId, db)
val meltRequest: MeltRequest = preMeltBundle.buildMeltRequest()

val response = client.post("$mintUrl/melt") {
Expand Down Expand Up @@ -320,7 +320,7 @@ public class Wallet(
val client = createClient()
val scopedActiveKeyset = activeKeyset ?: throw Exception("The wallet must have an active keyset for the mint when attempting a swap operation.")

val preSwapRequestBundle = PreSwapBundle.create(availableForSwap, requiredAmount, scopedActiveKeyset.keysetId)
val preSwapRequestBundle = PreSwapBundle.create(db, availableForSwap, requiredAmount, scopedActiveKeyset.keysetId)
val swapRequest = preSwapRequestBundle.buildSwapRequest()

val response = client.post("$mintUrl$SWAP_ENDPOINT") {
Expand Down
11 changes: 11 additions & 0 deletions lib/src/main/kotlin/me/tb/cashuclient/db/CashuDB.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ public interface CashuDB {
*/
public fun insertProof(proof: Proof): Unit

/**
* Returns proofs for requested amounts.
*
* This method is used to query the database for proofs associated with the specified amounts.
* Assumes that proofs are present in the database for the requested amounts, otherwise
* throws an exception.
*
* @param amounts - A list of [ULong] representing the amounts for which proofs are requested.
*/
public fun proofsForAmounts(amounts: List<ULong>): List<Proof>

/**
* Deletes a proof from the database.
*
Expand Down
44 changes: 0 additions & 44 deletions lib/src/main/kotlin/me/tb/cashuclient/db/DBBolt11Payment.kt

This file was deleted.

18 changes: 0 additions & 18 deletions lib/src/main/kotlin/me/tb/cashuclient/db/DBProof.kt

This file was deleted.

14 changes: 0 additions & 14 deletions lib/src/main/kotlin/me/tb/cashuclient/db/DBSettings.kt

This file was deleted.

68 changes: 41 additions & 27 deletions lib/src/main/kotlin/me/tb/cashuclient/db/SQLiteDB.kt
Original file line number Diff line number Diff line change
@@ -1,43 +1,57 @@
package me.tb.cashuclient.db

import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
import fr.acinq.lightning.utils.getValue
import me.tb.cashuclient.types.Proof
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import me.tb.cashulib.Database

/**
* The default implementation of the [CashuDB] interface, using SQLite.
*/
public class SQLiteDB : CashuDB {
override fun insertProof(proof: Proof) {
transaction(DBSettings.db) {
SchemaUtils.create(DBProof)

DBProof.insert {
it[amount] = proof.amount
it[secret] = proof.secret
it[C] = proof.C
it[id] = proof.id
it[script] = null
}
public class SQLiteDB(
private val sqlDriver: SqlDriver = sqlDriver()
) : CashuDB {

private val database by lazy {
Database.Schema.create(sqlDriver)
Database(sqlDriver)
}

override fun proofsForAmounts(amounts: List<ULong>): List<Proof> {
return amounts.map { amt ->
val record = database.proofQueries.selectProofByAmount(amt.toLong()).executeAsOne()
Proof(
amount = record.amount.toULong(),
secret = record.secret,
C = record.C,
id = record.id
)
}
}

override fun insertProof(proof: Proof) {
database.proofQueries.insert(
amount = proof.amount.toLong(),
secret = proof.secret,
C = proof.C,
id = proof.id,
script = proof.script
)
}

override fun deleteProof(proof: Proof) {
transaction(DBSettings.db) {
SchemaUtils.create(DBProof)
val secretOfProofToDelete = proof.secret
DBProof.deleteWhere { secret eq secretOfProofToDelete }
}
database.proofQueries.deleteById(proof.id)
}

override fun spendableNoteSizes(): List<ULong> {
return transaction(DBSettings.db) {
SchemaUtils.create(DBProof)
DBProof.selectAll().map { it[DBProof.amount] }
}
return database.proofQueries.selectAll().executeAsList().map { it.amount.toULong() }
}
}

/**
* Creates a new [SqlDriver] instance that connects to the SQLite database.
*/
private fun sqlDriver(): SqlDriver {
return JdbcSqliteDriver(url = "jdbc:sqlite:./cashu.sqlite3")
}
34 changes: 8 additions & 26 deletions lib/src/main/kotlin/me/tb/cashuclient/melt/PreMeltBundle.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,14 @@
* Copyright 2023 thunderbiscuit and contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the ./LICENSE.txt file.
*/

package me.tb.cashuclient.melt

import fr.acinq.lightning.payment.PaymentRequest
import me.tb.cashuclient.db.DBProof
import me.tb.cashuclient.db.DBSettings
import me.tb.cashuclient.db.CashuDB
import me.tb.cashuclient.melt.PreMeltBundle.Companion.create
import me.tb.cashuclient.types.BlindedMessage
import me.tb.cashuclient.types.Proof
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction

/**
* The data bundle Alice must create prior to communicating with the mint requesting a melt.
Expand All @@ -26,7 +23,7 @@ import org.jetbrains.exposed.sql.transactions.transaction
public class PreMeltBundle private constructor(
public val proofs: List<Proof>,
private val quoteId: String,
public val potentialChangeOutputs: List<BlindedMessage>? = null
public val potentialChangeOutputs: List<BlindedMessage>? = null,
) {
/**
* Builds a [MeltRequest] from the data in this bundle. This [MeltRequest] is the data structure that is then
Expand All @@ -49,31 +46,16 @@ public class PreMeltBundle private constructor(
*/
public fun create(
denominationsToUse: List<ULong>,
quoteId: String
quoteId: String,
db: CashuDB,
): PreMeltBundle {

DBSettings.db
// Check if we have these amounts in the database
val proofs: List<Proof> = denominationsToUse.map { amt ->
transaction {
SchemaUtils.create(DBProof)
val proof: Proof? = DBProof.select { DBProof.amount eq amt }.firstOrNull()?.let {
Proof(
amount = it[DBProof.amount],
id = it[DBProof.id],
secret = it[DBProof.secret],
C = it[DBProof.C],
script = it[DBProof.script]
)
}
proof ?: throw Exception("No proof found for amount $amt")
}
}
val proofs = db.proofsForAmounts(denominationsToUse)

return PreMeltBundle(
proofs = proofs,
quoteId = quoteId,
potentialChangeOutputs = null
potentialChangeOutputs = null,
)
}
}
Expand Down
32 changes: 6 additions & 26 deletions lib/src/main/kotlin/me/tb/cashuclient/swap/PreSwapBundle.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,20 @@
* Copyright 2023 thunderbiscuit and contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the ./LICENSE.txt file.
*/

package me.tb.cashuclient.swap

import fr.acinq.bitcoin.PrivateKey
import fr.acinq.bitcoin.PublicKey
import me.tb.cashuclient.db.CashuDB
import me.tb.cashuclient.types.KeysetId
import me.tb.cashuclient.types.Secret
import me.tb.cashuclient.db.DBProof
import me.tb.cashuclient.db.DBSettings
import me.tb.cashuclient.decomposeAmount
import me.tb.cashuclient.types.BlindedMessage
import me.tb.cashuclient.types.BlindingData
import me.tb.cashuclient.types.PreRequestBundle
import me.tb.cashuclient.types.Proof
import me.tb.cashuclient.types.createBlindingData
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction

/**
* The data bundle Alice must create prior to communicating with the mint requesting a swap. Once the mint sends a
Expand Down Expand Up @@ -60,6 +56,7 @@ public class PreSwapBundle private constructor(
* @param keysetId The [KeysetId] of the keyset the wallet expects will be signing the [BlindedMessage]s.
*/
public fun create(
db: CashuDB,
availableForSwap: List<ULong>,
requiredAmount: ULong,
keysetId: KeysetId,
Expand All @@ -78,7 +75,8 @@ public class PreSwapBundle private constructor(

val requiredDenominations: List<ULong> = decomposeAmount(requiredAmount)
val overPayment: ULong = runningTotal - requiredAmount
val changeDenominations: List<ULong> = if (overPayment > 0uL) decomposeAmount(overPayment) else emptyList()
val changeDenominations: List<ULong> =
if (overPayment > 0uL) decomposeAmount(overPayment) else emptyList()

val requestDenominations = requiredDenominations + changeDenominations

Expand All @@ -89,25 +87,7 @@ public class PreSwapBundle private constructor(
}

// Go get a proof in the database for the denominations required
DBSettings.db
val proofs: List<Proof> = transaction {
SchemaUtils.create(DBProof)

denominationsToSwap.map { denomination ->
DBProof.select { DBProof.amount eq denomination }
.limit(1)
.firstOrNull()
?.let {
Proof(
amount = it[DBProof.amount],
id = it[DBProof.id],
secret = it[DBProof.secret],
C = it[DBProof.C],
script = it[DBProof.script]
)
} ?: throw Exception("No proof found for denomination $denomination")
}
}
val proofs = db.proofsForAmounts(denominationsToSwap)

return PreSwapBundle(proofs, preSwapItem, keysetId)
}
Expand Down
12 changes: 12 additions & 0 deletions lib/src/main/sqldelight/me/tb/cashuclient/Bolt11Payment.sq
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- A table to store payment requests made by the mint.
-- Once paid, these will later be used to request tokens.
CREATE TABLE Bolt11Payment(
-- The hash here is really just a secret payment ID used to identify the payment between the mint and the client.
hash TEXT NOT NULL PRIMARY KEY,
pr TEXT NOT NULL,
value INTEGER NOT NULL
);

-- Using a hash as the identifier for an amount requested and invoice paid, return said amount
getAmountByHash:
SELECT value FROM Bolt11Payment WHERE hash = ?;
Loading

0 comments on commit dc44007

Please sign in to comment.