diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 00b53ce..81c4789 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -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 { @@ -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") diff --git a/lib/src/main/kotlin/me/tb/cashuclient/Wallet.kt b/lib/src/main/kotlin/me/tb/cashuclient/Wallet.kt index a8764fb..523eada 100644 --- a/lib/src/main/kotlin/me/tb/cashuclient/Wallet.kt +++ b/lib/src/main/kotlin/me/tb/cashuclient/Wallet.kt @@ -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") { @@ -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") { diff --git a/lib/src/main/kotlin/me/tb/cashuclient/db/CashuDB.kt b/lib/src/main/kotlin/me/tb/cashuclient/db/CashuDB.kt index 7390264..cb41864 100644 --- a/lib/src/main/kotlin/me/tb/cashuclient/db/CashuDB.kt +++ b/lib/src/main/kotlin/me/tb/cashuclient/db/CashuDB.kt @@ -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): List + /** * Deletes a proof from the database. * diff --git a/lib/src/main/kotlin/me/tb/cashuclient/db/DBBolt11Payment.kt b/lib/src/main/kotlin/me/tb/cashuclient/db/DBBolt11Payment.kt deleted file mode 100644 index e51810a..0000000 --- a/lib/src/main/kotlin/me/tb/cashuclient/db/DBBolt11Payment.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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.db - -import org.jetbrains.exposed.sql.Column -import org.jetbrains.exposed.sql.SchemaUtils -import org.jetbrains.exposed.sql.Table -import org.jetbrains.exposed.sql.select -import org.jetbrains.exposed.sql.transactions.transaction - -/** - * A table to store payment requests made by the mint. Once paid, these will later be used to request tokens. - */ -@OptIn(ExperimentalUnsignedTypes::class) -public object DBBolt11Payment : Table() { - public val pr: Column = varchar("pr", 400) - // The hash here is really just a secret payment ID used to identify the payment between the mint and the client. - public val hash: Column = varchar("hash", 100) - public val value: Column = ulong("value") -} - -/** - * Using a hash as the identifier for an amount requested and invoice paid, return said amount. - * - * @param hash The hash, used to identify the payment between the mint and the client. - */ -public fun getAmountByHash(hash: String): ULong { - DBSettings.db - // Database.connect("jdbc:sqlite:./cashu.sqlite3", "org.sqlite.JDBC") - return transaction { - SchemaUtils.create(DBBolt11Payment) - - val amount = DBBolt11Payment.select { - DBBolt11Payment.hash eq hash - }.singleOrNull() - - amount?.let { - it[DBBolt11Payment.value] - } ?: throw Exception("No amount found for hash $hash") - } -} diff --git a/lib/src/main/kotlin/me/tb/cashuclient/db/DBProof.kt b/lib/src/main/kotlin/me/tb/cashuclient/db/DBProof.kt deleted file mode 100644 index d0b3040..0000000 --- a/lib/src/main/kotlin/me/tb/cashuclient/db/DBProof.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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.db - -import org.jetbrains.exposed.sql.Column -import org.jetbrains.exposed.sql.Table - -@OptIn(ExperimentalUnsignedTypes::class) -public object DBProof : Table() { - public val amount: Column = ulong("amount") - public val secret: Column = varchar("secret", 100) - public val C: Column = varchar("C", 100) - public val id: Column = varchar("id", 100) - public val script: Column = varchar("script", 100).nullable() -} diff --git a/lib/src/main/kotlin/me/tb/cashuclient/db/DBSettings.kt b/lib/src/main/kotlin/me/tb/cashuclient/db/DBSettings.kt deleted file mode 100644 index 2ea6961..0000000 --- a/lib/src/main/kotlin/me/tb/cashuclient/db/DBSettings.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * 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.db - -import org.jetbrains.exposed.sql.Database - -public object DBSettings { - public val db: Database by lazy { - Database.connect("jdbc:sqlite:./cashu.sqlite3", "org.sqlite.JDBC") - } -} diff --git a/lib/src/main/kotlin/me/tb/cashuclient/db/SQLiteDB.kt b/lib/src/main/kotlin/me/tb/cashuclient/db/SQLiteDB.kt index 19fd739..4d2d52f 100644 --- a/lib/src/main/kotlin/me/tb/cashuclient/db/SQLiteDB.kt +++ b/lib/src/main/kotlin/me/tb/cashuclient/db/SQLiteDB.kt @@ -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): List { + 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 { - 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") +} diff --git a/lib/src/main/kotlin/me/tb/cashuclient/melt/PreMeltBundle.kt b/lib/src/main/kotlin/me/tb/cashuclient/melt/PreMeltBundle.kt index 4b59e8e..555282b 100644 --- a/lib/src/main/kotlin/me/tb/cashuclient/melt/PreMeltBundle.kt +++ b/lib/src/main/kotlin/me/tb/cashuclient/melt/PreMeltBundle.kt @@ -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. @@ -26,7 +23,7 @@ import org.jetbrains.exposed.sql.transactions.transaction public class PreMeltBundle private constructor( public val proofs: List, private val quoteId: String, - public val potentialChangeOutputs: List? = null + public val potentialChangeOutputs: List? = null, ) { /** * Builds a [MeltRequest] from the data in this bundle. This [MeltRequest] is the data structure that is then @@ -49,31 +46,16 @@ public class PreMeltBundle private constructor( */ public fun create( denominationsToUse: List, - quoteId: String + quoteId: String, + db: CashuDB, ): PreMeltBundle { - - DBSettings.db // Check if we have these amounts in the database - val proofs: List = 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, ) } } diff --git a/lib/src/main/kotlin/me/tb/cashuclient/swap/PreSwapBundle.kt b/lib/src/main/kotlin/me/tb/cashuclient/swap/PreSwapBundle.kt index c8cb7ce..12a95d4 100644 --- a/lib/src/main/kotlin/me/tb/cashuclient/swap/PreSwapBundle.kt +++ b/lib/src/main/kotlin/me/tb/cashuclient/swap/PreSwapBundle.kt @@ -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 @@ -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, requiredAmount: ULong, keysetId: KeysetId, @@ -78,7 +75,8 @@ public class PreSwapBundle private constructor( val requiredDenominations: List = decomposeAmount(requiredAmount) val overPayment: ULong = runningTotal - requiredAmount - val changeDenominations: List = if (overPayment > 0uL) decomposeAmount(overPayment) else emptyList() + val changeDenominations: List = + if (overPayment > 0uL) decomposeAmount(overPayment) else emptyList() val requestDenominations = requiredDenominations + changeDenominations @@ -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 = 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) } diff --git a/lib/src/main/sqldelight/me/tb/cashuclient/Bolt11Payment.sq b/lib/src/main/sqldelight/me/tb/cashuclient/Bolt11Payment.sq new file mode 100644 index 0000000..4e73083 --- /dev/null +++ b/lib/src/main/sqldelight/me/tb/cashuclient/Bolt11Payment.sq @@ -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 = ?; \ No newline at end of file diff --git a/lib/src/main/sqldelight/me/tb/cashuclient/Proof.sq b/lib/src/main/sqldelight/me/tb/cashuclient/Proof.sq new file mode 100644 index 0000000..6d403f5 --- /dev/null +++ b/lib/src/main/sqldelight/me/tb/cashuclient/Proof.sq @@ -0,0 +1,19 @@ +CREATE TABLE Proof( + id TEXT NOT NULL PRIMARY KEY, + amount INTEGER NOT NULL, + secret TEXT NOT NULL, + C TEXT NOT NULL, + script TEXT +); + +selectAll: +SELECT * FROM Proof; + +selectProofByAmount: +SELECT * FROM Proof WHERE amount = ?; + +insert: +INSERT INTO Proof(id, amount, secret, C, script) VALUES(?, ?, ?, ?, ?); + +deleteById: +DELETE FROM Proof WHERE id = ?; \ No newline at end of file diff --git a/lib/src/test/kotlin/me/tb/cashuclient/PreMeltBundleTest.kt b/lib/src/test/kotlin/me/tb/cashuclient/PreMeltBundleTest.kt deleted file mode 100644 index 73310e7..0000000 --- a/lib/src/test/kotlin/me/tb/cashuclient/PreMeltBundleTest.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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 file. - */ - -package me.tb.cashuclient - -import me.tb.cashuclient.db.DBProof -import me.tb.cashuclient.mockdb.buildMockDB -import org.jetbrains.exposed.sql.selectAll -import org.jetbrains.exposed.sql.transactions.transaction -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertEquals - -// class PreMeltBundleTest { -// @BeforeTest -// fun setUp() { -// buildMockDB() -// } -// -// @Test -// fun testMockDB() { -// transaction { -// val result = DBProof.selectAll() -// assertEquals(1, result.count()) -// } -// } -// } diff --git a/lib/src/test/kotlin/me/tb/cashuclient/db/InMemorySqlDriver.kt b/lib/src/test/kotlin/me/tb/cashuclient/db/InMemorySqlDriver.kt new file mode 100644 index 0000000..14e4741 --- /dev/null +++ b/lib/src/test/kotlin/me/tb/cashuclient/db/InMemorySqlDriver.kt @@ -0,0 +1,5 @@ +package me.tb.cashuclient.db + +import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver + +fun testSqlDriver() = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) diff --git a/lib/src/test/kotlin/me/tb/cashuclient/db/SQLiteDBTest.kt b/lib/src/test/kotlin/me/tb/cashuclient/db/SQLiteDBTest.kt new file mode 100644 index 0000000..bab6c90 --- /dev/null +++ b/lib/src/test/kotlin/me/tb/cashuclient/db/SQLiteDBTest.kt @@ -0,0 +1,73 @@ +package me.tb.cashuclient.db + +import me.tb.cashuclient.types.Proof +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertContentEquals +import kotlin.test.assertTrue + +class SQLiteDBTest { + val db = SQLiteDB(sqlDriver = testSqlDriver()) + + @Test + fun `spendable note sizes is empty when there are no proofs`() { + assertTrue(db.spendableNoteSizes().isEmpty()) + } + + @Test + fun `spendable note sizes is a list of proof amounts`() { + db.insertProof(Proof(amount = 1u, id = "id1", secret = "secret2", C = "C1")) + db.insertProof(Proof(amount = 2u, id = "id2", secret = "secret1", C = "C2")) + + val sizes = db.spendableNoteSizes() + assertContentEquals(listOf(1u, 2u), sizes) + } + + @Test + fun `spendable note sizes is updated when a proof is deleted`() { + val proof1 = Proof(amount = 1u, id = "id1", secret = "secret1", C = "C1") + val proof2 = Proof(amount = 2u, id = "id2", secret = "secret2", C = "C2") + + db.insertProof(proof1) + db.insertProof(proof2) + + assertContentEquals(listOf(1u, 2u), db.spendableNoteSizes()) + + db.deleteProof(proof1) + assertContentEquals(listOf(2u), db.spendableNoteSizes()) + + db.deleteProof(proof2) + + assertTrue(db.spendableNoteSizes().isEmpty()) + } + + @Test + fun `insert proofs`() { + // Populate proofs + val proof1 = Proof(amount = 1u, id = "id1", secret = "secret1", C = "C1") + val proof2 = Proof(amount = 2u, id = "id2", secret = "secret2", C = "C2") + db.insertProof(proof1) + db.insertProof(proof2) + + // Look up proofs by their amounts + val proofs = db.proofsForAmounts(listOf(proof1.amount, proof2.amount)) + assertContentEquals(listOf(proof1, proof2), proofs) + } + + @Test + fun `delete an existing proof`() { + // Populate proofs + val proof1 = Proof(amount = 1u, id = "id1", secret = "secret1", C = "C1") + val proof2 = Proof(amount = 2u, id = "id2", secret = "secret2", C = "C2") + db.insertProof(proof1) + db.insertProof(proof2) + + // Delete one of the proofs + db.deleteProof(proof1) + + // No longer able to look up a proof by its amount + assertThrows { + println(db.proofsForAmounts(listOf(proof1.amount))) + } + } +} \ No newline at end of file diff --git a/lib/src/test/kotlin/me/tb/cashuclient/mockdb/MockDB.kt b/lib/src/test/kotlin/me/tb/cashuclient/mockdb/MockDB.kt deleted file mode 100644 index 67c6575..0000000 --- a/lib/src/test/kotlin/me/tb/cashuclient/mockdb/MockDB.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 file. - */ - -package me.tb.cashuclient.mockdb - -import me.tb.cashuclient.db.DBProof -import org.jetbrains.exposed.sql.Database -import org.jetbrains.exposed.sql.SchemaUtils -import org.jetbrains.exposed.sql.batchInsert -import org.jetbrains.exposed.sql.transactions.transaction -import java.util.UUID - -fun buildMockDB() { - // Connect to a unique in-memory H2 database for each test - Database.connect("jdbc:h2:mem:test${UUID.randomUUID()};DB_CLOSE_DELAY=-1;", driver = "org.h2.Driver") - - // Create the table - transaction { - SchemaUtils.create(DBProof) - } - - // Insert some rows for testing - transaction { - DBProof.batchInsert( - listOf( - mapOf( - DBProof.amount to 16uL, - DBProof.secret to "secret1", - DBProof.C to "C1", - DBProof.id to "I2yN+iRYfkzT", - DBProof.script to null - ), - ) - ) {} - } -}