diff --git a/lib/src/main/kotlin/me/tb/cashuclient/Wallet.kt b/lib/src/main/kotlin/me/tb/cashuclient/Wallet.kt index e8d5172..14ca2de 100644 --- a/lib/src/main/kotlin/me/tb/cashuclient/Wallet.kt +++ b/lib/src/main/kotlin/me/tb/cashuclient/Wallet.kt @@ -43,6 +43,7 @@ import me.tb.cashuclient.mint.MintRequest import me.tb.cashuclient.mint.MintResponse import me.tb.cashuclient.swap.PreSwapBundle import me.tb.cashuclient.swap.SwapResponse +import me.tb.cashuclient.types.ActiveKeysetsResponse import me.tb.cashuclient.types.BlindedSignaturesResponse import me.tb.cashuclient.types.EcashUnit import me.tb.cashuclient.types.Keyset @@ -91,9 +92,12 @@ public class Wallet( public fun getActiveKeyset(): Unit = runBlocking(Dispatchers.IO) { val keyset = async { val client = HttpClient(OkHttp) - val keysetJson = client.get("$mintUrl$ACTIVE_KEYSET_PATH").bodyAsText() + val response = client.get("$mintUrl$ACTIVE_KEYSET_PATH").bodyAsText() client.close() - Keyset.fromJson(keysetJson) + val mintResponse = Json.decodeFromString(ActiveKeysetsResponse.serializer(), response) + + // TODO: I'm not sure why there can be multiple active keysets at the same time. Open issue on specs repo. + mintResponse.keysets.first().toKeyset() } addKeyset(keyset.await()) } @@ -103,12 +107,15 @@ public class Wallet( * Query the mint for the [Keyset] associated with a given [KeysetId]. */ public fun getSpecificKeyset(keysetId: KeysetId): Keyset = runBlocking(Dispatchers.IO) { - val keysetId: String = keysetId.value + val keysetIdHex: String = keysetId.value val specificKeyset = async { val client = HttpClient(OkHttp) - val keysetJson = client.get("$mintUrl$SPECIFIC_KEYSET_PATH$keysetId").bodyAsText() + val response = client.get("$mintUrl$SPECIFIC_KEYSET_PATH$keysetIdHex").bodyAsText() client.close() - Keyset.fromJson(keysetJson) + val mintResponse = Json.decodeFromString(ActiveKeysetsResponse.serializer(), response) + + // TODO: There should only be one keyset in the response it feels odd that the spec requires an array + mintResponse.keysets.first().toKeyset() } specificKeyset.await() } diff --git a/lib/src/main/kotlin/me/tb/cashuclient/types/ActiveKeysetsResponse.kt b/lib/src/main/kotlin/me/tb/cashuclient/types/ActiveKeysetsResponse.kt new file mode 100644 index 0000000..e18a20a --- /dev/null +++ b/lib/src/main/kotlin/me/tb/cashuclient/types/ActiveKeysetsResponse.kt @@ -0,0 +1,13 @@ +package me.tb.cashuclient.types + +import kotlinx.serialization.Serializable + +/** + * The data structure the mint returns when we ask for the active keysets. + * + * @param keysets A list of serializable [KeysetJson] objects. + */ +@Serializable +public data class ActiveKeysetsResponse( + public val keysets: List +) diff --git a/lib/src/main/kotlin/me/tb/cashuclient/types/EcashUnit.kt b/lib/src/main/kotlin/me/tb/cashuclient/types/EcashUnit.kt index 141864e..0e2ec8b 100644 --- a/lib/src/main/kotlin/me/tb/cashuclient/types/EcashUnit.kt +++ b/lib/src/main/kotlin/me/tb/cashuclient/types/EcashUnit.kt @@ -1,5 +1,5 @@ package me.tb.cashuclient.types public enum class EcashUnit { - Satoshi, + SATOSHI, } diff --git a/lib/src/main/kotlin/me/tb/cashuclient/types/Keyset.kt b/lib/src/main/kotlin/me/tb/cashuclient/types/Keyset.kt index 1cd0c34..cf4b0dc 100644 --- a/lib/src/main/kotlin/me/tb/cashuclient/types/Keyset.kt +++ b/lib/src/main/kotlin/me/tb/cashuclient/types/Keyset.kt @@ -9,6 +9,7 @@ import com.google.gson.Gson import com.google.gson.reflect.TypeToken import fr.acinq.bitcoin.PublicKey import fr.acinq.bitcoin.crypto.Digest +import kotlinx.serialization.Serializable import java.util.SortedMap /** @@ -17,7 +18,9 @@ import java.util.SortedMap * * @param keyset A map of token values to public keys. */ -public class Keyset(keyset: Map) { +public class Keyset( + keyset: Map, +) { init { keyset.forEach { (value, publicKey) -> require(publicKey.isValid()) { "Invalid public key $publicKey (hex: ${publicKey.toHex()}) for value $value." } @@ -25,24 +28,23 @@ public class Keyset(keyset: Map) { } public val sortedKeyset: SortedMap = keyset.toSortedMap() - public val keysetId: KeysetId by lazy { - deriveKeysetId() - } + public val keysetId: KeysetId by lazy { deriveKeysetId() } + public val unit: EcashUnit = EcashUnit.SATOSHI /** * Derive the [KeysetId] for a given [Keyset]. Currently only derives 0x00 version keyset ids. */ @OptIn(ExperimentalStdlibApi::class) private fun deriveKeysetId(): KeysetId { - val allKeysConcatenated: String = buildString { - sortedKeyset.values.forEach { publicKey -> - append(publicKey) - } + // Add all compressed public keys together + val allKeysConcatenated = sortedKeyset.values.fold(ByteArray(0)) { acc, publicKey -> + acc + publicKey.value.toByteArray() } + val versionPrefix = byteArrayOf(0x00) val bytes: ByteArray = Digest .sha256() - .hash(allKeysConcatenated.toByteArray(Charsets.UTF_8)) + .hash(allKeysConcatenated) .sliceArray(0..<7) val keysetId = versionPrefix + bytes @@ -60,6 +62,16 @@ public class Keyset(keyset: Map) { return sortedKeyset[tokenValue] ?: throw Exception("No key found in keyset for token value $tokenValue") } + public fun toKeysetJson(): KeysetJson { + val keys: Map = sortedKeyset.map { (tokenValue, publicKey) -> + tokenValue to publicKey.toHex() + }.toMap() + val unit: String = "sat" + val id: String = keysetId.value + + return KeysetJson(id, unit, keys) + } + /** * This override allows us to compare two [Keyset]s for equality (not implementing this means leaving the default * implementation, which is to compare references). @@ -97,6 +109,27 @@ public class Keyset(keyset: Map) { } } +// TODO: This class should be removed in favour of a proper serialization of the Keyset type. +@Serializable +public data class KeysetJson( + public val id: String, + public val unit: String = "sat", + public val keys: Map, +) { + public fun toKeyset(): Keyset { + val keysetOnly: Map = keys.map { (tokenValue, publicKeyHex) -> + tokenValue to PublicKey.fromHex(publicKeyHex) + }.toMap() + // println("keysetOnly: $keysetOnly") + println("Newly created keyset id should be: $id") + val keyset: Keyset = Keyset(keysetOnly) + + require(keyset.keysetId.value == id) { "Keyset id mismatch: ${keyset.keysetId.value} != $id" } + + return keyset + } +} + // TODO: Write tests for this type /** * A [KeysetId] is a unique identifier for a [Keyset]. diff --git a/lib/src/test/kotlin/me/tb/cashuclient/KeysetTest.kt b/lib/src/test/kotlin/me/tb/cashuclient/KeysetTest.kt index 21c889e..c185922 100644 --- a/lib/src/test/kotlin/me/tb/cashuclient/KeysetTest.kt +++ b/lib/src/test/kotlin/me/tb/cashuclient/KeysetTest.kt @@ -35,7 +35,7 @@ class KeysetTest { @Test fun `Derive keyset id`() { val keyset: Keyset = Keyset.fromJson(TEST_KEYSET) - assertEquals(expected = "00236c8dfa24587e", actual = keyset.keysetId.value) + assertEquals(expected = "000f01df73ea149a", actual = keyset.keysetId.value) } @Test diff --git a/lib/src/test/kotlin/me/tb/cashuclient/WalletTest.kt b/lib/src/test/kotlin/me/tb/cashuclient/WalletTest.kt index b8bd8f5..ffb5344 100644 --- a/lib/src/test/kotlin/me/tb/cashuclient/WalletTest.kt +++ b/lib/src/test/kotlin/me/tb/cashuclient/WalletTest.kt @@ -1,5 +1,11 @@ +/* + * 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.types.EcashUnit import me.tb.cashuclient.types.Keyset import me.tb.cashuclient.types.KeysetId import kotlin.test.Test @@ -92,9 +98,7 @@ class WalletTest { fun `Wallet successfully updates active keyset from mint after requesting it`() { val jsonString = """{"1":"03142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9"}""" val smallKeyset = Keyset.fromJson(jsonString) - val wallet = Wallet(activeKeyset = smallKeyset, mintUrl = "https://testnut.cashu.space") - // val wallet = Wallet(activeKeyset = smallKeyset, mintUrl = "https://mutinynet-cashu.thesimplekid.space") - // val wallet = Wallet(activeKeyset = smallKeyset, mintUrl = "https://8333.space:3338") + val wallet = Wallet(activeKeyset = smallKeyset, mintUrl = "https://testnut.cashu.space", unit = EcashUnit.SATOSHI) println("The current wallet keyset is ${wallet.activeKeyset}") // At this point the wallet keyset should not be the same as the smallKeyset @@ -105,7 +109,6 @@ class WalletTest { // Now we request the active keyset from the mint wallet.getActiveKeyset() - println("The new wallet keyset is ${wallet.activeKeyset}") // At this point the wallet keyset should not be the same anymore assertEquals( @@ -118,11 +121,10 @@ class WalletTest { fun `Wallet adds old keyset to list of inactive keysets`() { val jsonString = """{"1":"03142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9"}""" val smallKeyset = Keyset.fromJson(jsonString) - val wallet = Wallet(activeKeyset = smallKeyset, mintUrl = "https://testnut.cashu.space") + val wallet = Wallet(activeKeyset = smallKeyset, mintUrl = "https://testnut.cashu.space", unit = EcashUnit.SATOSHI) // Now we request the active keyset from the mint wallet.getActiveKeyset() - println("The new wallet keyset ID is ${wallet.activeKeyset!!.keysetId}") assertEquals( expected = smallKeyset, @@ -134,10 +136,18 @@ class WalletTest { fun `Wallet can request specific keyset from mint`() { val jsonString = """{"1":"03142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9"}""" val smallKeyset = Keyset.fromJson(jsonString) - val wallet = Wallet(activeKeyset = smallKeyset, mintUrl = "https://testnut.cashu.space") + val wallet = Wallet( + activeKeyset = smallKeyset, + mintUrl = "https://testnut.cashu.space", + unit = EcashUnit.SATOSHI + ) val specificKeyset = wallet.getSpecificKeyset(KeysetId("009a1f293253e41e")) - println("The specific keyset 009a1f293253e41e is ${specificKeyset.sortedKeyset}") + + assertEquals( + expected = "009a1f293253e41e", + actual = specificKeyset.keysetId.value + ) } // TODO: Fix this test: from what I understand the test completes before the BD has finished its work,