summaryrefslogtreecommitdiff
path: root/wallet/src/commonTest/kotlin/net/taler/lib/wallet
diff options
context:
space:
mode:
Diffstat (limited to 'wallet/src/commonTest/kotlin/net/taler/lib/wallet')
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/lib/wallet/AmountTest.kt64
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/lib/wallet/Base32CrockfordTest.kt123
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/lib/wallet/DbTest.kt101
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/lib/wallet/PaytoUriTest.kt58
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/lib/wallet/TestUtils.kt69
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/lib/wallet/TimestampTest.kt76
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/lib/wallet/WalletApiTest.kt94
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/DepositTest.kt90
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/EllipticCurveTest.kt156
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/KdfTest.kt213
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/RecoupTest.kt90
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/RefreshPlanchetTest.kt112
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/Sha256Test.kt69
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/Sha512Test.kt116
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/SignatureTest.kt493
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/lib/wallet/exchange/DenominationTest.kt91
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/lib/wallet/exchange/Denominations.kt119
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/lib/wallet/exchange/KeysTest.kt316
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/lib/wallet/exchange/UpdateTest.kt37
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/lib/wallet/exchange/WireTest.kt175
-rw-r--r--wallet/src/commonTest/kotlin/net/taler/lib/wallet/operations/WithdrawTest.kt140
21 files changed, 2802 insertions, 0 deletions
diff --git a/wallet/src/commonTest/kotlin/net/taler/lib/wallet/AmountTest.kt b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/AmountTest.kt
new file mode 100644
index 0000000..ccee992
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/AmountTest.kt
@@ -0,0 +1,64 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.lib.wallet
+
+import net.taler.lib.common.Amount
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class AmountTest {
+
+ @Test
+ fun testToByteArray() {
+ val vectors = listOf(
+ Pair("ceicWVf9GhJ:3902026702525079.40496378", "006XSQV3G899E0K9XKX66SB9CDBNCSHS8XM4M00"),
+ Pair("asYDLuK2A:3800267550024600.02072907", "006R0MNXBVHSG00ZM55P2WTS8H67AJSJ8400000"),
+ Pair("pV1m:1347558259914570.09786232", "002CK66VCNVMM04NADW70NHHDM0000000000000"),
+ Pair("geO82l:553744321840253.41004983", "000ZF855K627T0KHNYVPESAF70S6R0000000000"),
+ Pair("B9bWK7WPEO:3663912678613976.12122563", "006G8KS5P9HXG05RZ71M4EB2AX5KENTG8N7G000"),
+ Pair("X:1537372109907438.77850768", "002QCETPFYJYW153X285G000000000000000000"),
+ Pair("5:4271492725553118.39728399", "007JSSK6J4VXW0JY6M7KA000000000000000000"),
+ Pair("OSdV:801656289790342.08256189", "001DJ6H6CA4RC03XZAYMYMV4AR0000000000000"),
+ Pair("Y6:2908617536334646.94126271", "0055AQTB19NKC1CW82ZNJDG0000000000000000"),
+ Pair("kSHoOZj:2610656582865206.00292046", "004MCR6T828KC004EK76PMT8DX7NMTG00000000"),
+ Pair("GkhLXrlGES:4246330707533398.83874252", "007HC0Z9DFF5C17ZT764ETV89HC74V278N9G000"),
+ Pair("CNS09:738124490298524.71259462", "0019YMG01DA9R11ZAN346KJK60WG00000000000"),
+ Pair("sw0b1tKXZym:2132978464977419.28199478", "003S7VNZPZS0P0DE98V76XSGC8RQ8JTRB9WPT00"),
+ Pair("fC:1275322307696988.17178522", "0028FSGX3ZCNR0863YD6CGR0000000000000000"),
+ Pair("cRai6j:166032749022734.69444771", "0009E0C30V70W113MJHP6MK1D4V6M0000000000"),
+ Pair("KOADwTb3:3932974019564218.48282023", "006ZJ16ZB39BM0Q0Q6KMPKT18HVN8RHK0000000"),
+ Pair("9Fi9wcLgDe:1268366772151214.97268853", "002834N6WRHTW1EC6HTKJHK975VP6K378HJG000"),
+ Pair("SDN:3370670470236379.88943272", "005ZK6V0124DP1AD5AM56H2E000000000000000"),
+ Pair("zGCP5V:4010014441349620.76121145", "0073Y5HYA8GZ8149GGWQMHT3A0TNC0000000000"),
+ Pair("VsW1JjBLn:2037070181191907.99717275", "003KSD2WH18E61FHJ2DNCWTQ6556MGJCDR00000"),
+ Pair("A:1806895799429502.00887758", "0036PQ5P8NMQW00DHF742000000000000000000"),
+ Pair("njA8:4015261148004966.43708687", "00747PYPD116C0MTY47PWTJ1700000000000000"),
+ Pair("Bwq:3562876074139250.28829179", "006AGTNTRWF740DQWQXM4XVH000000000000000"),
+ Pair("8e75v8:3716241006992995.95213823", "006K7SP93WF661DCV3ZKGS9Q6NV3G0000000000"),
+ Pair("XrnbQTTn:3887603772953949.94721267", "006WZGA9X8ANT1D5AKSNGWKEC98N8N3E0000000"),
+ Pair("MIN:0.00000001", "0000000000000000000MTJAE000000000000000"),
+ Pair("MAX:4503599627370496.99999999", "00800000000001FNW3ZMTGAR000000000000000")
+ )
+ for (v in vectors) {
+ val amount = Amount.fromJSONString(v.first)
+ val encodedBytes =
+ Base32Crockford.encode(amount.toByteArray())
+ assertEquals(v.second, encodedBytes)
+ }
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/lib/wallet/Base32CrockfordTest.kt b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/Base32CrockfordTest.kt
new file mode 100644
index 0000000..0f16971
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/Base32CrockfordTest.kt
@@ -0,0 +1,123 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.lib.wallet
+
+import kotlin.test.Ignore
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+@ExperimentalStdlibApi
+class Base32CrockfordTest {
+
+ private class TestVector(val value: ByteArray, val encoding: List<String>)
+
+ private val vectors = listOf(
+ TestVector(byteArrayOf(0), listOf("00", "0O", "0o")),
+ TestVector(byteArrayOf(0), listOf("00", "0O", "0o")),
+ TestVector(byteArrayOf(1), listOf("04")),
+ TestVector(byteArrayOf(2), listOf("08")),
+ TestVector(byteArrayOf(3), listOf("0C")),
+ TestVector(byteArrayOf(4), listOf("0G")),
+ TestVector(byteArrayOf(5), listOf("0M")),
+ TestVector(byteArrayOf(6), listOf("0R")),
+ TestVector(byteArrayOf(7), listOf("0W")),
+ TestVector(byteArrayOf(8), listOf("10")),
+ TestVector(byteArrayOf(9), listOf("14")),
+ TestVector(byteArrayOf(10), listOf("18")),
+ TestVector(byteArrayOf(11), listOf("1C")),
+ TestVector(byteArrayOf(12), listOf("1G")),
+ TestVector(byteArrayOf(13), listOf("1M")),
+ TestVector(byteArrayOf(14), listOf("1R")),
+ TestVector(byteArrayOf(15), listOf("1W")),
+ TestVector(byteArrayOf(16), listOf("20")),
+ TestVector(byteArrayOf(17), listOf("24")),
+ TestVector(byteArrayOf(18), listOf("28")),
+ TestVector(byteArrayOf(19), listOf("2C")),
+ TestVector(byteArrayOf(20), listOf("2G")),
+ TestVector(byteArrayOf(21), listOf("2M")),
+ TestVector(byteArrayOf(22), listOf("2R")),
+ TestVector(byteArrayOf(23), listOf("2W")),
+ TestVector(byteArrayOf(24), listOf("30")),
+ TestVector(byteArrayOf(25), listOf("34")),
+ TestVector(byteArrayOf(26), listOf("38")),
+ TestVector(byteArrayOf(27), listOf("3C")),
+ TestVector(byteArrayOf(28), listOf("3G")),
+ TestVector(byteArrayOf(29), listOf("3M")),
+ TestVector(byteArrayOf(30), listOf("3R")),
+ TestVector(byteArrayOf(31), listOf("3W")),
+ TestVector(byteArrayOf(0, 0), listOf("0000", "oooo", "OOOO", "0oO0")),
+ TestVector(byteArrayOf(1, 0), listOf("0400", "o4oo", "O4OO", "04oO")),
+ TestVector(byteArrayOf(0, 1), listOf("000G", "ooog", "OOOG", "0oOg")),
+ TestVector(byteArrayOf(1, 1), listOf("040G", "o4og", "O4og", "04Og")),
+ TestVector(byteArrayOf(136.toByte(), 64), listOf("H100", "hio0", "HLOo")),
+ TestVector(byteArrayOf(139.toByte(), 188.toByte()), listOf("HEY0", "heyo", "HeYO")),
+ TestVector(byteArrayOf(54, 31, 127), listOf("6RFQY", "6rfqy")),
+ TestVector(
+ byteArrayOf(72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33),
+ listOf("91JPRV3F41BPYWKCCGGG", "91jprv3f41bpywkccggg", "9Ljprv3f4ibpywkccggg")
+ ),
+ TestVector(
+ byteArrayOf(139.toByte(), 130.toByte(), 16, 112, 24, 11, 64),
+ listOf("HE110W0R1D00", "helloworld00", "heiiOw0RidoO")
+ ),
+ TestVector(
+ byteArrayOf(139.toByte(), 130.toByte(), 16, 112, 24, 11),
+ listOf("HE110W0R1C", "helloworlc", "heiiOw0RiC")
+ ),
+ TestVector(
+ byteArrayOf(139.toByte(), 130.toByte(), 16, 112, 24, 11, 0),
+ listOf("HE110W0R1C00", "helloworlc00", "heiiOw0RiC00")
+ )
+ )
+
+ @Test
+ fun testEncode() {
+ for (vector in vectors) {
+ assertEquals(vector.encoding[0], Base32Crockford.encode(vector.value))
+ }
+ }
+
+ @Test
+ fun testDecode() {
+ for (vector in vectors) {
+ for (encoding in vector.encoding) {
+ assertTrue(vector.value contentEquals Base32Crockford.decode(encoding))
+ }
+ }
+ }
+
+ @Ignore // TODO
+ @Test
+ fun testDecodeFuck() {
+ val bytes = byteArrayOf(0x7e, 0xd9.toByte())
+ assertTrue(bytes contentEquals Base32Crockford.decode("FUCK"))
+ assertTrue(bytes contentEquals Base32Crockford.decode("FuCk"))
+ assertTrue(bytes contentEquals Base32Crockford.decode("fUcK"))
+ assertTrue(bytes contentEquals Base32Crockford.decode("FVCK"))
+ }
+
+ @Test
+ fun testEncodingDecoding() {
+ val input = "Hello, World"
+ val encoded = Base32Crockford.encode(input.encodeToByteArray())
+ val decoded = Base32Crockford.decode(encoded)
+ val output = decoded.decodeToString()
+ assertEquals(input, output)
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/lib/wallet/DbTest.kt b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/DbTest.kt
new file mode 100644
index 0000000..cdd0484
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/DbTest.kt
@@ -0,0 +1,101 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.lib.wallet
+
+import net.taler.lib.common.Timestamp
+import net.taler.lib.wallet.exchange.DenominationStatus.Unverified
+import net.taler.lib.wallet.exchange.DenominationStatus.VerifiedGood
+import net.taler.lib.wallet.exchange.Denominations.denomination10
+import net.taler.lib.wallet.exchange.Denominations.denomination5
+import net.taler.lib.wallet.exchange.ExchangeRecord
+import net.taler.lib.wallet.exchange.ExchangeUpdateReason.Initial
+import net.taler.lib.wallet.exchange.ExchangeUpdateStatus.FetchKeys
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class DbTest {
+
+ private val dbFactory = DbFactory()
+
+ private val exchange1 = ExchangeRecord(
+ baseUrl = "https://example1.org/",
+ timestampAdded = Timestamp.now(),
+ updateStatus = FetchKeys,
+ updateStarted = Timestamp.now(),
+ updateReason = Initial
+ )
+ private val exchange2 = ExchangeRecord(
+ baseUrl = "https://example2.org/",
+ timestampAdded = Timestamp.now(),
+ updateStatus = FetchKeys,
+ updateStarted = Timestamp.now(),
+ updateReason = Initial
+ )
+
+ @Test
+ fun testExchanges() = runCoroutine {
+ val db = dbFactory.openDb()
+ var exchanges = db.listExchanges()
+ assertEquals(0, exchanges.size)
+
+ db.put(exchange1)
+ exchanges = db.listExchanges()
+ assertEquals(1, exchanges.size)
+ assertEquals(exchange1, exchanges[0])
+
+ db.put(exchange2)
+ exchanges = db.listExchanges()
+ assertEquals(2, exchanges.size)
+ assertEquals(exchange1, db.getExchangeByBaseUrl(exchange1.baseUrl))
+ assertEquals(exchange2, db.getExchangeByBaseUrl(exchange2.baseUrl))
+
+ db.deleteExchangeByBaseUrl(exchange1.baseUrl)
+ exchanges = db.listExchanges()
+ assertEquals(1, exchanges.size)
+ assertEquals(exchange2, exchanges[0])
+ }
+
+ @Test
+ fun testDenominations() = runCoroutine {
+ val db = dbFactory.openDb()
+ val url = denomination10.exchangeBaseUrl
+
+ // no denominations should be in DB
+ var denominations = db.getDenominationsByBaseUrl(url)
+ assertEquals(0, denominations.size)
+
+ // add a denomination and check that it is really there
+ db.put(denomination10)
+ denominations = db.getDenominationsByBaseUrl(url)
+ assertEquals(1, denominations.size)
+ assertEquals(denomination10, denominations[0])
+
+ // modify existing denomination and check that it gets updated
+ assertEquals(Unverified, denomination10.status)
+ val newDenomination = denomination10.copy(status = VerifiedGood)
+ db.put(newDenomination)
+ denominations = db.getDenominationsByBaseUrl(url)
+ assertEquals(1, denominations.size)
+ assertEquals(newDenomination, denominations[0])
+
+ // add one more denomination
+ db.put(denomination5)
+ denominations = db.getDenominationsByBaseUrl(url)
+ assertEquals(2, denominations.size)
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/lib/wallet/PaytoUriTest.kt b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/PaytoUriTest.kt
new file mode 100644
index 0000000..cf3780a
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/PaytoUriTest.kt
@@ -0,0 +1,58 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.lib.wallet
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+
+class PaytoUriTest {
+
+ @Test
+ fun testFromString() {
+ // wrong scheme
+ var uri = "https://example.com/"
+ assertNull(PaytoUri.fromString(uri))
+
+ // incomplete scheme
+ uri = "payto:blabla"
+ assertNull(PaytoUri.fromString(uri))
+
+ // proper URI
+ uri = "payto://x-taler-bank/123"
+ var parsedUri = PaytoUri.fromString(uri)
+ assertNotNull(parsedUri)
+ assertEquals("x-taler-bank", parsedUri.targetType)
+ assertEquals("123", parsedUri.targetPath)
+
+ // proper URI with incomplete query
+ uri = "payto://x-taler-bank/123?foo"
+ parsedUri = PaytoUri.fromString(uri)
+ assertNotNull(parsedUri)
+ assertEquals(0, parsedUri.params.size)
+
+ // proper URI with two query param
+ uri = "payto://x-taler-bank/123?foo=bar&hip=hop"
+ parsedUri = PaytoUri.fromString(uri)
+ assertNotNull(parsedUri)
+ assertEquals(2, parsedUri.params.size)
+ assertEquals("bar", parsedUri.params["foo"])
+ assertEquals("hop", parsedUri.params["hip"])
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/lib/wallet/TestUtils.kt b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/TestUtils.kt
new file mode 100644
index 0000000..14c3076
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/TestUtils.kt
@@ -0,0 +1,69 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.lib.wallet
+
+import io.ktor.client.HttpClient
+import io.ktor.client.engine.mock.MockEngine
+import io.ktor.client.engine.mock.MockEngineConfig
+import io.ktor.client.engine.mock.respond
+import io.ktor.client.features.json.JsonFeature
+import io.ktor.client.features.json.serializer.KotlinxSerializer
+import io.ktor.http.ContentType.Application
+import io.ktor.http.Url
+import io.ktor.http.fullPath
+import io.ktor.http.headersOf
+import io.ktor.http.hostWithPort
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.serialization.json.Json
+
+enum class PlatformTarget {
+ ANDROID,
+ JS,
+ NATIVE,
+}
+
+/**
+ * Workaround to use suspending functions in unit tests
+ */
+expect fun runCoroutine(block: suspend (scope: CoroutineScope) -> Unit)
+
+expect fun getPlatformTarget(): PlatformTarget
+
+fun getMockHttpClient(): HttpClient = HttpClient(MockEngine) {
+ install(JsonFeature) {
+ serializer = KotlinxSerializer(Json(getJsonConfiguration()))
+ }
+ engine {
+ addHandler { error("No test handler added") }
+ }
+}
+
+fun HttpClient.giveJsonResponse(url: String, jsonProducer: () -> String) {
+ val httpConfig = engineConfig as MockEngineConfig
+ httpConfig.requestHandlers.removeAt(0)
+ httpConfig.requestHandlers.add { request ->
+ if (request.url.fullUrl == url) {
+ val headers = headersOf("Content-Type" to listOf(Application.Json.toString()))
+ respond(jsonProducer(), headers = headers)
+ } else {
+ error("Unexpected URL: ${request.url.fullUrl}")
+ }
+ }
+}
+
+private val Url.hostWithPortIfRequired: String get() = if (port == protocol.defaultPort) host else hostWithPort
+private val Url.fullUrl: String get() = "${protocol.name}://$hostWithPortIfRequired$fullPath"
diff --git a/wallet/src/commonTest/kotlin/net/taler/lib/wallet/TimestampTest.kt b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/TimestampTest.kt
new file mode 100644
index 0000000..b0f3b32
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/TimestampTest.kt
@@ -0,0 +1,76 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.lib.wallet
+
+import net.taler.lib.common.Timestamp
+import kotlin.random.Random
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class TimestampTest {
+
+ companion object {
+ fun getRandomTimestamp() = Timestamp(Random.nextLong(0, 9007199254740991))
+ }
+
+ @Test
+ fun testRoundedToByteArray() {
+ val vectors = listOf<Pair<Long, String>>(
+ Pair(0, "0000000000000"),
+ Pair(23, "0000000000000"),
+ Pair(3349, "000000005Q3C0"),
+ Pair(61227, "00000003MB4M0"),
+ Pair(143879, "00000008GR0W0"),
+ Pair(8879237, "000000GH7B4W0"),
+ Pair(16058145, "000000XX46H80"),
+ Pair(270909464, "00000FRKDX4M0"),
+ Pair(5500325225, "0000A054XBSM0"),
+ Pair(52631835363, "0002ZQJDTGYC0"),
+ Pair(567067373675, "00107FN9AKAM0"),
+ Pair(1036693403335, "001TXQFY0VEC0"),
+ Pair(88636710366804, "04XED7JKDJSR0"),
+ Pair(852207301364437, "1F9TC1M0SEJG0"),
+ Pair(8312646819781097, "EDE78FC4AEXM0"),
+ Pair(7473472692572260, "CYVHQMAQAR7G0"),
+ Pair(1148188526507363, "1ZQJYRD9M40C0"),
+ Pair(5418115526173127, "9CRG6QASJ80M0"),
+ Pair(4046218176532046, "70KGVZK7XCPG0"),
+ Pair(2421361923399585, "46D6FNS4VAFW0"),
+ Pair(7305555710693483, "CNH8RDJYNV1M0"),
+ Pair(2857858080018042, "4YMJDJ1XYFM80"),
+ Pair(2037218281967033, "3H2TEEYTJCCW0"),
+ Pair(7912348432268295, "DQ74XYJCEFXG0"),
+ Pair(6416777738213721, "B46FJPQRT81M0"),
+ Pair(6914097778740296, "BZSWYK0W3NTG0"),
+ Pair(7090360690428000, "C9K0AYTABAVG0"),
+ Pair(1998560202566445, "3EY4ZTJR5QER0"),
+ Pair(7896179665141956, "DPADV4EQCHKM0"),
+ Pair(6266851558322330, "AVW58JG1WB880"),
+ Pair(1878397422799871, "388PH07WY68W0"),
+ Pair(3767372253320333, "6H46A6MZSMS00"),
+ Pair(8065344266359580, "DZPXQR6KHZ5W0"),
+ Pair(7947440620360995, "DS5FP8RA25H00"),
+ Pair(3414000286898485, "5XGFEB1VMC880"),
+ Pair(9007199254740991, "FKZZZZZZY3EG0")
+ ) // TODO add more test vectors beyond 9007199254740991 (typescript max of wallet-core)
+ for (v in vectors) {
+ val encodedBytes = Base32Crockford.encode(Timestamp(v.first).roundedToByteArray())
+ assertEquals(v.second, encodedBytes)
+ }
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/lib/wallet/WalletApiTest.kt b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/WalletApiTest.kt
new file mode 100644
index 0000000..511eb79
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/WalletApiTest.kt
@@ -0,0 +1,94 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.lib.wallet
+
+import net.taler.lib.common.Amount
+import kotlin.test.Ignore
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+@Ignore // Live-Test which fails when test environment changes or is not available
+class WalletApiTest {
+
+ private val api = WalletApi()
+
+ private val exchangeBaseUrl = "https://exchange.test.taler.net/"
+ private val withdrawUri =
+ "taler://withdraw/bank.test.taler.net/8adabbf8-fe61-4d31-a2d1-89350b5be8fa"
+ private val paytoUris = listOf("payto://x-taler-bank/bank.test.taler.net/Exchange")
+ private val tosEtag = "0"
+
+ @Test
+ fun testGetVersions() {
+ api.getVersions()
+ }
+
+ @Test
+ fun testGetWithdrawalDetails() = runCoroutine {
+ val detailsForUri = api.getWithdrawalDetailsForUri(withdrawUri)
+ assertEquals(Amount("TESTKUDOS", 5, 0), detailsForUri.amount)
+ assertEquals(exchangeBaseUrl, detailsForUri.defaultExchangeBaseUrl)
+
+ val detailsForAmount = api.getWithdrawalDetailsForAmount(
+ detailsForUri.defaultExchangeBaseUrl!!,
+ detailsForUri.amount
+ )
+ assertEquals(Amount("TESTKUDOS", 5, 0), detailsForAmount.amountRaw)
+ assertEquals(Amount("TESTKUDOS", 4, 80000000), detailsForAmount.amountEffective)
+ assertEquals(false, detailsForAmount.tosAccepted)
+ }
+
+ @Test
+ fun testExchangeManagement() = runCoroutine {
+ // initially we have no exchanges
+ assertTrue(api.listExchanges().isEmpty())
+
+ // test exchange gets added correctly
+ val exchange = api.addExchange(exchangeBaseUrl)
+ assertEquals(exchangeBaseUrl, exchange.exchangeBaseUrl)
+ assertEquals("TESTKUDOS", exchange.currency)
+ assertEquals(paytoUris, exchange.paytoUris)
+
+ // added exchange got added to list
+ val exchanges = api.listExchanges()
+ assertEquals(1, exchanges.size)
+ assertEquals(exchange, exchanges[0])
+
+ // ToS are not accepted
+ val tosResult = api.getExchangeTos(exchangeBaseUrl)
+ assertEquals(null, tosResult.acceptedEtag)
+ assertEquals(tosEtag, tosResult.currentEtag)
+ assertTrue(tosResult.tos.length > 100)
+
+ // withdrawal details also show ToS as not accepted
+ val withdrawalDetails =
+ api.getWithdrawalDetailsForAmount(exchangeBaseUrl, Amount.zero("TESTKUDOS"))
+ assertEquals(false, withdrawalDetails.tosAccepted)
+
+ // accept ToS
+ api.setExchangeTosAccepted(exchangeBaseUrl, tosResult.currentEtag)
+ val newTosResult = api.getExchangeTos(exchangeBaseUrl)
+ assertEquals(newTosResult.currentEtag, newTosResult.acceptedEtag)
+
+ // withdrawal details now show ToS as accepted as well
+ val newWithdrawalDetails =
+ api.getWithdrawalDetailsForAmount(exchangeBaseUrl, Amount.zero("TESTKUDOS"))
+ assertEquals(true, newWithdrawalDetails.tosAccepted)
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/DepositTest.kt b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/DepositTest.kt
new file mode 100644
index 0000000..e3017a3
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/DepositTest.kt
@@ -0,0 +1,90 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.lib.wallet.crypto
+
+import net.taler.lib.common.Amount
+import net.taler.lib.common.Timestamp
+import net.taler.lib.wallet.crypto.Deposit.CoinDepositPermission
+import net.taler.lib.wallet.crypto.Deposit.DepositInfo
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class DepositTest {
+
+ private val crypto = CryptoFactory.getCrypto()
+ private val deposit = Deposit(crypto)
+
+ private class DepositVector(val depositInfo: DepositInfo, val permission: CoinDepositPermission)
+
+ @Test
+ fun test() {
+ val vectors = listOf(
+ DepositVector(
+ DepositInfo(
+ exchangeBaseUrl = "example.org",
+ contractTermsHash = "QS3RK40J3262QRAJG4SVZB0C60N45W7D71ZXFTP385E05PKR7RGH22QTPA109Z0EBD120SHA09WHA72K4PCFM6JTFMCZEGVW2T9FT2R",
+ coinPublicKey = "6DMP3NPY6DZD0QDMEW6955R6ZCX3T052PRJE0FYHTKRA837VMQT0",
+ coinPrivateKey = "XWVF454Q600NGSMF7RYXY369N2DJKC1YQZG0RFAF8EMRRY8DJE50",
+ spendAmount = Amount("TESTKUDOS", 8, 0),
+ timestamp = Timestamp(1593522039000),
+ refundDeadline = Timestamp(1593522939000),
+ merchantPublicKey = "RQSR35MJJDP5Y27XMBEV7QBG4HTE3JXR1JCE4NCYCAQHKJ8CB5T0",
+ feeDeposit = Amount("TESTKUDOS", 0, 2000000),
+ wireInfoHash = "5C3WXB9PVPXAGGQVRJGAMBJJQBBYPHZEZZYVHQ8RS97F9EZABPAWNWSGD1VH6YNFB58GPJWX1A17DFNCK0S9YPP7PED3ZXR76E2A0XR",
+ denomPublicKey = "020000YQX8JZNBMJ8PTVRZZT27TTSJN5HM0037K0BSY3YWP0VF8GFYYAN06J0PTQVQGQJGVJ91CHWD6QCH9MKADR1VH31P5WEMTMWWTX6KWA3KX0FD1RWQSPJ8W8ZXVWPVDJVVJ08W12VWV39A9W4SDCMWBSQ17QV4RP86JJFMTEWB540KKYHY3ZYJEQYVN9XD7CM7SCAMM8WV03P0VG2001",
+ denomSignature = "9F3M4AZ8BN3MJRH6T49TD9R4EVTF6C5NH365DHCC73F39N3SAK422NY9ZWN7WANGW3M4XZTJDDV1B1E7MSW03VCESKSF8889EDTRE1VHF0FT3E5CT0Q449JAQQ6DSDY4D9JMPP99TRKZX86VAXN45FBSBXTJZ2FN85Y67T9ADDNDXMV060J0HP7G5YXXJQ0V7KHACEZFVXFH6"
+ ),
+ CoinDepositPermission(
+ coinSignature = "CZX4HMBR6H3W1KAN1T0HZ11VBSERZ6JM715MKSDAJJSXWQTZ0GDCZX92MAKMH0DB7B389E24XGP6X7ZSAQVW540KTQZJ9RMR0R9CM08",
+ coinPublicKey = "6DMP3NPY6DZD0QDMEW6955R6ZCX3T052PRJE0FYHTKRA837VMQT0",
+ denomSignature = "9F3M4AZ8BN3MJRH6T49TD9R4EVTF6C5NH365DHCC73F39N3SAK422NY9ZWN7WANGW3M4XZTJDDV1B1E7MSW03VCESKSF8889EDTRE1VHF0FT3E5CT0Q449JAQQ6DSDY4D9JMPP99TRKZX86VAXN45FBSBXTJZ2FN85Y67T9ADDNDXMV060J0HP7G5YXXJQ0V7KHACEZFVXFH6",
+ denomPublicKey = "020000YQX8JZNBMJ8PTVRZZT27TTSJN5HM0037K0BSY3YWP0VF8GFYYAN06J0PTQVQGQJGVJ91CHWD6QCH9MKADR1VH31P5WEMTMWWTX6KWA3KX0FD1RWQSPJ8W8ZXVWPVDJVVJ08W12VWV39A9W4SDCMWBSQ17QV4RP86JJFMTEWB540KKYHY3ZYJEQYVN9XD7CM7SCAMM8WV03P0VG2001",
+ contribution = "TESTKUDOS:8",
+ exchangeBaseUrl = "example.org"
+ )
+ ),
+ DepositVector(
+ DepositInfo(
+ exchangeBaseUrl = "example.org",
+ contractTermsHash = "CWWDVCEX745A092KB3W7K98M7EVK4G5HJRHKR0RTPKAFR1VSK147ER131PT23P8ZWH2VMWAWENTVTAXP4KDRQ9YY0951N2G2JZFGEXG",
+ coinPublicKey = "ZR6BER43XSR3NK705HFKGC842Q3Q2R4G3T6VQ5JEK8EAC34JYW6G",
+ coinPrivateKey = "CP8WW83V239DTXD84M87TZ0DG1TGM4RC3D77NN936554B7GDVBRG",
+ spendAmount = Amount("TESTKUDOS", 1, 0),
+ timestamp = Timestamp(1593522635000),
+ refundDeadline = Timestamp(1593523535000),
+ merchantPublicKey = "S8WFGCK6CJGFWYWHAY56NTZTD84S31TGM244GTVKTCETKW5HDBQG",
+ feeDeposit = Amount("TESTKUDOS", 0, 2000000),
+ wireInfoHash = "8GQFCMQXHHTF7VG73WVJN8PFCPH4Y0ZRSWAQ01A6A9F0Y0HY59H3RDPMBPVNJJDJP1S3E8JJVE7MGAC9YDACWAVCTE75QZ8ZRNAH3X8",
+ denomPublicKey = "020000X1NCWC14MNXTAJ17W6HY5AMGXKGV392BESTESJJ8TQ41W9W2RBN69Q3WQYQQXS7KS5ZMDSSGHY0H7921X0RRA6ZNW2RSKJGXJNQM66KRDEFTQ50B6ZK60CSCY3KZ0RSGAYBG8VHC2A87Z6DQS361G8BAJS937J9YX89MGVMG896MKVEZ3H3NGRJTT89QNGN5KZWFSE6G5129GG2001",
+ denomSignature = "7XJDMABJHV01GX7S764ZY3XR3AX2KAK4AXWKZJHBRV8BBQ7KQ5FHC372GE8RF8M2NT128G85RWW87CNNVYNWWPEPSFN8QXTA2H6SGTR0EFRDF4CXAJRXHWPB450YMJM7MNNPJKXDDXCFN87RSHFZ4ESH06S0SBX6185DX2HD6JWQ3BESCK8PYCB6A09KP5ZD0EZSQKDGNGMEG"
+ ),
+ CoinDepositPermission(
+ coinSignature = "A8N1NSMSPZ4H1VR1YANYMAE74VAJ7W88EMRNDY74YXPK2WFEEHHM28VVQ3HAK6J0P9YX61XGNHRP08AEX59M0YGJ7AW8ZG2Y5FQD23G",
+ coinPublicKey = "ZR6BER43XSR3NK705HFKGC842Q3Q2R4G3T6VQ5JEK8EAC34JYW6G",
+ denomSignature = "7XJDMABJHV01GX7S764ZY3XR3AX2KAK4AXWKZJHBRV8BBQ7KQ5FHC372GE8RF8M2NT128G85RWW87CNNVYNWWPEPSFN8QXTA2H6SGTR0EFRDF4CXAJRXHWPB450YMJM7MNNPJKXDDXCFN87RSHFZ4ESH06S0SBX6185DX2HD6JWQ3BESCK8PYCB6A09KP5ZD0EZSQKDGNGMEG",
+ denomPublicKey = "020000X1NCWC14MNXTAJ17W6HY5AMGXKGV392BESTESJJ8TQ41W9W2RBN69Q3WQYQQXS7KS5ZMDSSGHY0H7921X0RRA6ZNW2RSKJGXJNQM66KRDEFTQ50B6ZK60CSCY3KZ0RSGAYBG8VHC2A87Z6DQS361G8BAJS937J9YX89MGVMG896MKVEZ3H3NGRJTT89QNGN5KZWFSE6G5129GG2001",
+ contribution = "TESTKUDOS:1",
+ exchangeBaseUrl = "example.org"
+ )
+ )
+ )
+ for (v in vectors) {
+ assertEquals(v.permission, deposit.signDepositPermission(v.depositInfo))
+ }
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/EllipticCurveTest.kt b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/EllipticCurveTest.kt
new file mode 100644
index 0000000..10fbcc1
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/EllipticCurveTest.kt
@@ -0,0 +1,156 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.lib.wallet.crypto
+
+import net.taler.lib.wallet.Base32Crockford
+import kotlin.random.Random
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class EllipticCurveTest {
+
+ private val crypto = CryptoFactory.getCrypto()
+
+ @Test
+ fun testExchangeTvgEddsaKey() {
+ val pri = "9TM70AKDTS57AWY9JK2J4TMBTMW6K62WHHGZWYDG0VM5ABPZKD40"
+ val pub = "8GSJZ649T2PXMKZC01Y4ANNBE7MF14QVK9SQEC4E46ZHKCVG8AS0"
+ val pubBytes = crypto.eddsaGetPublic(Base32Crockford.decode(pri))
+ assertEquals(pub, Base32Crockford.encode(pubBytes))
+
+ val pri2 = "C9C5X5JS2SWRTSX5AQBPFA4S2DFD6V9Q04XTTEZDQ5XSSQK2P39G"
+ val pub2 = "6WC3MPYM5XKPKRA2Z6SYB81CPFV3E7EC6S2GVE095X8XH63QTZCG"
+ val pub2Bytes = crypto.eddsaGetPublic(Base32Crockford.decode(pri2))
+ assertEquals(pub2, Base32Crockford.encode(pub2Bytes))
+ }
+
+ @Test
+ fun testExchangeTvgEcdheKey() {
+ val pri = "X4T4N0M8PVQXQEBW2BA7049KFSM7J437NSDFC6GDNM3N5J9367A0"
+ val pub = "M997P494MS6A95G1P0QYWW2VNPSHSX5Q6JBY5B9YMNYWP0B50X3G"
+ val pubBytes = crypto.ecdheGetPublic(Base32Crockford.decode(pri))
+ assertEquals(pub, Base32Crockford.encode(pubBytes))
+
+ val pri2 = "MB8ZSQFTVPX4V0MGT3BA1PQS12NJ7KH33DA8D22NCWTNA5BHH2YG"
+ val pub2 = "8T2CN1W8G3XZE9C9158A5ASS74117GK1XQ1XAX5SGBFDGHS8H5W0"
+ val pub2Bytes = crypto.ecdheGetPublic(Base32Crockford.decode(pri2))
+ assertEquals(pub2, Base32Crockford.encode(pub2Bytes))
+ }
+
+ @Test
+ fun testCreateEddsaKeyPair() {
+ val pair1 = crypto.createEddsaKeyPair()
+ val pair2 = crypto.createEddsaKeyPair()
+ assertFalse(pair1.privateKey contentEquals pair2.privateKey)
+ assertFalse(pair1.publicKey contentEquals pair2.publicKey)
+ }
+
+ @Test
+ fun testCreateEcdheKeyPair() {
+ val pair1 = crypto.createEcdheKeyPair()
+ val pair2 = crypto.createEcdheKeyPair()
+ assertFalse(pair1.privateKey contentEquals pair2.privateKey)
+ assertFalse(pair1.publicKey contentEquals pair2.publicKey)
+ }
+
+ @Test
+ @ExperimentalStdlibApi
+ fun testEddsaSignAndVerify() {
+ val msg = "Hallo world!".encodeToByteArray()
+ val pri = "9TM70AKDTS57AWY9JK2J4TMBTMW6K62WHHGZWYDG0VM5ABPZKD40"
+ val expectedSig =
+ "Z6H76JXPJFP3JBGSF54XBF0BVXDJ0CJBK4YT9GVR1AT916ZD57KP53YZN5G67A4YN95WGMZKQW7744483P5JDF06B6S7TMK195QGP20"
+ val sig = crypto.eddsaSign(msg, Base32Crockford.decode(pri))
+ assertEquals(expectedSig, Base32Crockford.encode(sig))
+
+ val pub = crypto.eddsaGetPublic(Base32Crockford.decode(pri))
+ assertTrue(crypto.eddsaVerify(msg, sig, pub))
+
+ val wrongSig = Random.nextBytes(64)
+ assertFalse(crypto.eddsaVerify(msg, wrongSig, pub))
+
+ val wrongPub = Random.nextBytes(32)
+ assertFalse(crypto.eddsaVerify(msg, sig, wrongPub))
+
+ val wrongMsg = Random.nextBytes(16)
+ assertFalse(crypto.eddsaVerify(wrongMsg, sig, pub))
+
+ val pri2 = "T4NK2VVZZ1ZF6EBGEQHRK7KY67PCEVAZC5YHYH612XG5R6NJXSB0"
+ val pub2 = "2X3PSPT7D6TEM97R98C0DHZREFVAVA3XTH11D5A2Z2K7GBKQ7AEG"
+ val data2 =
+ "00000J00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
+ val expectedSig2 =
+ "RFC59ZWJB88TD03F4GTQD3RWKH3KKRVEY2T1VN7CFEF51SCZ4BX8HGHC708GW1SN7Y4XFEDE3A5PFZEJ7PR7V09YZXX10DM608T1W20"
+ val sig2 = crypto.eddsaSign(Base32Crockford.decode(data2), Base32Crockford.decode(pri2))
+ assertEquals(expectedSig2, Base32Crockford.encode(sig2))
+ assertTrue(crypto.eddsaVerify(Base32Crockford.decode(data2), sig2, Base32Crockford.decode(pub2)))
+ }
+
+ @Test
+ fun testExchangeTvgEddsaEcdh() {
+ val ecdhePrivateKey = "4AFZWMSGTVCHZPQ0R81NWXDCK4N58G7SDBBE5KXE080Y50370JJG"
+ val ecdhePublicKey = "FXFN5GPAFTKVPWJDPVXQ87167S8T82T5ZV8CDYC0NH2AE14X0M30"
+ val ecdhePublicKeyBytes = crypto.ecdheGetPublic(Base32Crockford.decode(ecdhePrivateKey))
+ assertEquals(ecdhePublicKey, Base32Crockford.encode(ecdhePublicKeyBytes))
+
+ val eddsaPrivateKey = "1KG54M8T3X8BSFSZXCR3SQBSR7Y9P53NX61M864S7TEVMJ2XVPF0"
+ val eddsaPublicKey = "7BXWKG6N224C57RTDV8XEAHR108HG78NMA995BE8QAT5GC1S7E80"
+ val eddsaPublicKeyBytes = crypto.eddsaGetPublic(Base32Crockford.decode(eddsaPrivateKey))
+ assertEquals(eddsaPublicKey, Base32Crockford.encode(eddsaPublicKeyBytes))
+
+ val keyMaterial =
+ "PKZ42Z56SVK2796HG1QYBRJ6ZQM2T9QGA3JA4AAZ8G7CWK9FPX175Q9JE5P0ZAX3HWWPHAQV4DPCK10R9X3SAXHRV0WF06BHEC2ZTKR"
+ val keyMaterial1Bytes = crypto.keyExchangeEddsaEcdhe(
+ Base32Crockford.decode(eddsaPrivateKey),
+ Base32Crockford.decode(ecdhePublicKey)
+ )
+ assertEquals(keyMaterial, Base32Crockford.encode(keyMaterial1Bytes))
+
+ val keyMaterial2Bytes = crypto.keyExchangeEcdheEddsa(
+ Base32Crockford.decode(ecdhePrivateKey),
+ Base32Crockford.decode(eddsaPublicKey)
+ )
+ assertEquals(keyMaterial, Base32Crockford.encode(keyMaterial2Bytes))
+
+ val ecdhePrivateKey2 = "3AMRJ1KC87VWX9MVQMW8MB9YVX5DMS6P5V2SYZNCZ44XVMSDVEFG"
+ val ecdhePublicKey2 = "CFFVCRFTNP7701KZC7187BC42MGSVCMBWK38F23E7T9VVK9D41Q0"
+ val ecdhePublicKeyBytes2 = crypto.ecdheGetPublic(Base32Crockford.decode(ecdhePrivateKey2))
+ assertEquals(ecdhePublicKey2, Base32Crockford.encode(ecdhePublicKeyBytes2))
+
+ val eddsaPrivateKey2 = "8AZTVPCXNCVBKJVQ6XS7Z38KB9JB38WJ7Z80F9FTCX1WHZACTXJ0"
+ val eddsaPublicKey2 = "T3F1VYAVZX3DVCNJTB3BNQTRZGXN446QP2VD847CB6N5V75RT2B0"
+ val eddsaPublicKeyBytes2 = crypto.eddsaGetPublic(Base32Crockford.decode(eddsaPrivateKey2))
+ assertEquals(eddsaPublicKey2, Base32Crockford.encode(eddsaPublicKeyBytes2))
+
+ val keyMaterial2 =
+ "FPQSHC6MWWJGEPXXVF76Q8C4PBJFE59RSGRGFQ3Z6ESR67HMG7FAK6JTCQ4ZKHSVNNCF53DX8FY55EA2193N2A6KD510AEV5TBVZRJR"
+ val keyMaterial1Bytes2 = crypto.keyExchangeEddsaEcdhe(
+ Base32Crockford.decode(eddsaPrivateKey2),
+ Base32Crockford.decode(ecdhePublicKey2)
+ )
+ assertEquals(keyMaterial2, Base32Crockford.encode(keyMaterial1Bytes2))
+
+ val keyMaterial2Bytes2 = crypto.keyExchangeEcdheEddsa(
+ Base32Crockford.decode(ecdhePrivateKey2),
+ Base32Crockford.decode(eddsaPublicKey2)
+ )
+ assertEquals(keyMaterial2, Base32Crockford.encode(keyMaterial2Bytes2))
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/KdfTest.kt b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/KdfTest.kt
new file mode 100644
index 0000000..291930d
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/KdfTest.kt
@@ -0,0 +1,213 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.lib.wallet.crypto
+
+import net.taler.lib.wallet.Base32Crockford
+import net.taler.lib.wallet.crypto.Kdf.HMAC_SHA256_BLOCK_SIZE
+import net.taler.lib.wallet.crypto.Kdf.HMAC_SHA512_BLOCK_SIZE
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+class KdfTest {
+
+ private val crypto = CryptoFactory.getCrypto()
+
+ @Test
+ fun testHmacSha256() {
+ // key smaller than block size
+ val key1 = "EDJP6WK5EG"
+ val message1 = "91JPRV3F41BPYWKCCGGG"
+ val expectedOut1 = "DYKV9QN2HVHMHQRGZ6XNJPPSGQZHA2JAVZB1676ACXYSNKQ0FQ30"
+ val out1 = Kdf.hmacSha256(Base32Crockford.decode(key1), Base32Crockford.decode(message1)) { crypto.sha256(it) }
+ assertTrue(Base32Crockford.decode(key1).size < HMAC_SHA256_BLOCK_SIZE)
+ assertEquals(expectedOut1, Base32Crockford.encode(out1))
+
+ // key bigger than block size
+ val key2 =
+ "AHM6JWS089GQ6S9K68G7CRBJD5GPWX10EXGQ6833E9JP2X35CGG64Y908HQQASVCC5SJ0GVJDXHPPSKFE9J20X3F41GQCVV9CGG66VVECSTQ6TBFDRQ0"
+ val message2 = "89P62RVB4166JXK5ECG4TRBMEHJQ488"
+ val expectedOut2 = "QGEK8853DW3GSQ4RHDYXZ0KX9R6E356R5495Z10KDNFNZ98V9FQG"
+ val out2 = Kdf.hmacSha256(Base32Crockford.decode(key2), Base32Crockford.decode(message2)) { crypto.sha256(it) }
+ assertTrue(Base32Crockford.decode(key2).size > HMAC_SHA256_BLOCK_SIZE)
+ assertEquals(expectedOut2, Base32Crockford.encode(out2))
+
+ // key exactly block size
+ val key3 =
+ "AHM6JWS0EDJP6WK5EGG6PSBS41MQ6831ECG6RVVECWG62WS0EHM6A832DHQP6TS0EDMQMS90DXK20J2D851J0MT884S3ADH07MG3CD0"
+ val message3 = "89P62RVB4166JXK5ECG4TRBMEHJQ488"
+ val expectedOut3 = "HAEWRWXEWWPRMT4W6E32GS0P6QMW85MMZZA5CX2BMJW36QZQFJ2G"
+ val out3 = Kdf.hmacSha256(Base32Crockford.decode(key3), Base32Crockford.decode(message3)) { crypto.sha256(it) }
+ assertEquals(HMAC_SHA256_BLOCK_SIZE, Base32Crockford.decode(key3).size)
+ assertEquals(expectedOut3, Base32Crockford.encode(out3))
+ }
+
+ @Test
+ fun testHmacSha512() {
+ // key smaller than block size
+ val key1 = "EDJP6WK5EG"
+ val message1 = "91JPRV3F41BPYWKCCGGG"
+ val expectedOut1 =
+ "ZMVHDTWE341YSE6YQ4S0Y6XESW7RVFG4WK2CE7K4ZESQ10125JJ2VTRBCHPAQTTKW9MT4H350ZXN8GMP2SBFG03SB6YK63QMHQRE2FG"
+ val out1 = Kdf.hmacSha512(Base32Crockford.decode(key1), Base32Crockford.decode(message1)) { crypto.sha512(it) }
+ assertTrue(Base32Crockford.decode(key1).size < HMAC_SHA512_BLOCK_SIZE)
+ assertEquals(expectedOut1, Base32Crockford.encode(out1))
+
+ // key bigger than block size
+ val key2 =
+ "AHM6JWS089GQ6S9K68G7CRBJD5GPWX10EXGQ6833E9JP2X35CGG64Y908HQQASVCC5SJ0GVJDXHPPSKFE9J2W829EGG6AY33DHTP8SBK414JR82C5GG4Y83MDWG62XKFD5J20RVFDSK7AWV9DXQ20XV9EHM20S39CXMQ8WS0C5Q6882N41T6Y83GE9JQCSBEEGG62RV3D5J6AVKMC5P20VV2EDHPAVK9EHWJW"
+ val message2 = "89P62RVB4166JXK5ECG4TRBMEHJQ488"
+ val expectedOut2 =
+ "0N9GF90FES6HXS344WB396CF78SQDRZ7CS9MRAVCVZPJ3PJ97HYSB5A3C7RCXTTYYSS4CV11X3DANKQD71YMTA46R3NS1X8A99YVAE0"
+ val out2 = Kdf.hmacSha512(Base32Crockford.decode(key2), Base32Crockford.decode(message2)) { crypto.sha512(it) }
+ assertTrue(Base32Crockford.decode(key2).size > HMAC_SHA512_BLOCK_SIZE)
+ assertEquals(expectedOut2, Base32Crockford.encode(out2))
+
+ // key exactly block size
+ val key3 =
+ "AHM6JWS0ESJQ4Y90EDJP6WK5EGG6PSBS41MQ6835F1GP6X3CF4G62WS0DHQPWSS0C5SJ0X38CMG64V3FCDNJ0WV9F9JJ0VV641T6GS90916M2GS0AD44281N64S20RBCCXQQ4TBMD1PJ0XV8D5HPG839ECG32CHR41H7JX35ECG62VK441GJ0V3FEGG6YSH0EHJQGX1E5RQ2W"
+ val message3 = "89P62RVB4166JXK5ECG4TRBMEHJQ488"
+ val expectedOut3 =
+ "A3ZAKHA0EADSBP832KBCPDNVB4GP876PMQEEB2F55ERTQFQ7KMYNTJ3SEWJEQRNH3EV2H0HSA740JE4JQH7GM8FC708KHF4A5FG9QNG"
+ val out3 = Kdf.hmacSha512(Base32Crockford.decode(key3), Base32Crockford.decode(message3)) { crypto.sha512(it) }
+ assertEquals(HMAC_SHA512_BLOCK_SIZE, Base32Crockford.decode(key3).size)
+ assertEquals(expectedOut3, Base32Crockford.encode(out3))
+ }
+
+ @Test
+ fun testRfc4231() {
+ val key1 = ByteArray(20) { 0x0b }
+ val data1 = "91MJ0N38CNS6A"
+ assertEquals(
+ "P0T4RRERVCW56Q58NZ7AY2ZH5E41VGG0S61KV9S6X4VPRBHJSZVG",
+ Base32Crockford.encode(Kdf.hmacSha256(key1, Base32Crockford.decode(data1)) { crypto.sha256(it) })
+ )
+ assertEquals(
+ "GYN7SQN5XXGSTKZGPGJ1M7BCP0HQKX72SS7C4Y3TT2SGAHF1FKFDNA1KPZBBH9R20E5JEKNEMFTE9FMXJ57EPRFHE0Q6JV107896GN0",
+ Base32Crockford.encode(Kdf.hmacSha512(key1, Base32Crockford.decode(data1)) { crypto.sha512(it) })
+ )
+
+ val key2 = Base32Crockford.decode("99JPCS8")
+ val data2 = "EXM62X10CHQJ0YB141VP2VKM41K6YWH0DSQQ8T39DSKKY"
+ assertEquals(
+ "BFEC2HNZC1TMWTG44GK0H5BNRXD00FR8KMKKK0WXXHCBJS7C711G",
+ Base32Crockford.encode(Kdf.hmacSha256(key2, Base32Crockford.decode(data2)) { crypto.sha256(it) })
+ )
+ assertEquals(
+ "2S5QMYZWZ0CY5RWNZFKKPNQ0ME3VTS125T1HZNGG4W6DFTH50NA9EP5ZEQ05N6AADM1MYSFRY3KFVJQAP6HMTJKB9DHPW1RA72YEEDR",
+ Base32Crockford.encode(Kdf.hmacSha512(key2, Base32Crockford.decode(data2)) { crypto.sha512(it) })
+ )
+
+ val key3 = ByteArray(20) { 0xaa.toByte() }
+ val data3 = "VQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEXVQEX"
+ assertEquals(
+ "EWZAJ7HPG074D1ADQ3NX14C1MWMNJ2CB7VWC28PSCDAH9KPNCQZ0",
+ Base32Crockford.encode(Kdf.hmacSha256(key3, Base32Crockford.decode(data3)) { crypto.sha256(it) })
+ )
+ assertEquals(
+ "Z9SV024XATH89VXGY1TPS28BX6RVBPYXHVM1MDJNZ0Z37CH7KMWVYFM4G9WTE8P80TT8B93YCZ40FEA6MCVVXT4M4ST2F22SW4S95YR",
+ Base32Crockford.encode(Kdf.hmacSha512(key3, Base32Crockford.decode(data3)) { crypto.sha512(it) })
+ )
+
+ val key4 = Base32Crockford.decode("041061050R3GG28A1C60T3GF208H44RM2MB1E60S")
+ val data4 = "SQ6WVKEDSQ6WVKEDSQ6WVKEDSQ6WVKEDSQ6WVKEDSQ6WVKEDSQ6WVKEDSQ6WVKEDSQ6WVKEDSQ6WVKED"
+ assertEquals(
+ "G9ARME4T8GY0X96CG6C9KWG87A2Z1YN3WNWFG1VT5RZZ8SS9CSDG",
+ Base32Crockford.encode(Kdf.hmacSha256(key4, Base32Crockford.decode(data4)) { crypto.sha256(it) })
+ )
+ assertEquals(
+ "P2X4CNHQ8P66K475N32ZC7AAYZJQDPBZZ55REBF7DY050DGYWFDTJ755R4DA4QNMTSWJEQ65F20679FHJX0H432F5QHAVTZB22H9HQ8",
+ Base32Crockford.encode(Kdf.hmacSha512(key4, Base32Crockford.decode(data4)) { crypto.sha512(it) })
+ )
+
+ val key5 = ByteArray(20) { 0x0c.toByte() }
+ val data5 = "AHJQ6X10AXMQ8T10AHS7AVK3C5T6JVVE"
+ assertEquals(
+ "MEV1CX3K207E0VGCF5P2JNAN5C",
+ Base32Crockford.encode(Kdf.hmacSha256(key5, Base32Crockford.decode(data5)) { crypto.sha256(it) }
+ .copyOfRange(0, 16))
+ )
+ assertEquals(
+ "85FTTRKHB05567A1F6Y8J7C7MR",
+ Base32Crockford.encode(Kdf.hmacSha512(key5, Base32Crockford.decode(data5)) { crypto.sha512(it) }
+ .copyOfRange(0, 16))
+ )
+
+ val key6 = ByteArray(131) { 0xaa.toByte() }
+ val data6 = "AHJQ6X10ANSPJVK741662WK7CNS20N38C5Q20GKCDXHPPBAKD5X6A82BCNWJ0B9091GQ6T109DJQJ826D5S76X0"
+ assertEquals(
+ "C3J32P8YW2V7Y3CA4TNCQXDQFY70QHH16WMCA5058R20Y3Q3FXA0",
+ Base32Crockford.encode(Kdf.hmacSha256(key6, Base32Crockford.decode(data6)) { crypto.sha256(it) })
+ )
+ assertEquals(
+ "G2S44RY7R6HYQDRMJF0XTYZ8PJDMDMFM3D5EXG8J3C0KF0ZRYD96PNPG6ZG5Y9CRQM7X48AXD8F555F69XSZCFRAXJ5S2PMRBNW6B60",
+ Base32Crockford.encode(Kdf.hmacSha512(key6, Base32Crockford.decode(data6)) { crypto.sha512(it) })
+ )
+
+ val key7 = ByteArray(131) { 0xaa.toByte() }
+ val data7 =
+ "AHM6JWS0D5SJ0R90EHJQ6X10ENSPJVK741GJ0V31E9KPAWH0EHM62VH0C9P6YRVB5NSPJYK541NPAY90C5Q6883141P62WK7CNS20X38C5Q20RKCDXHPPBBKD5X6A834C5T62BH0AHM6A83BCNWJ0VK5CNJ7683MDWG64S90D1GQ6T35CGG64SB6DXS6A832CNMPWSS0ENSPAS10C9WJ0X38CMG4GKA18CG62V37DXS6JX38DMQ0"
+ assertEquals(
+ "KC4ZZ9RVJGQWP9V3BYYDBC798JZXRRV49W3H74WAFX8N6Q1T6QH0",
+ Base32Crockford.encode(Kdf.hmacSha256(key7, Base32Crockford.decode(data7)) { crypto.sha256(it) })
+ )
+ assertEquals(
+ "WDXPMXTXS1YVN96ZN7WPWQHZZQFBTWFRGSS8K1JXYPHJT86DS52BC0HCNGY4K0NH1NFEPNE3WKF1A4T6EVXPVR24C1JWJX20ZA66MP0",
+ Base32Crockford.encode(Kdf.hmacSha512(key7, Base32Crockford.decode(data7)) { crypto.sha512(it) })
+ )
+ }
+
+ @Test
+ fun testKdf() {
+ val salt = "94KPT83PCNS7J83KC5P78Y8"
+ val ikm = "94KPT83MD1JJ0WV5CDS6AX10D5Q70XBM41NPAY90DNGQ8SBJD5GPR"
+ val ctx = "94KPT83141HPYVKMCNW78833D1TPWTSC41GPRWVF41NPWVVQDRG62WS04XMPWSKF4WG6JVH0EHM6A82J8S1G"
+ val expectedOut =
+ "GTMR4QT05Z9WF5HKVG0WK9RPXGHSMHJNW377G9GJXCA8B0FEKPF4D27RJMSJZYWSQNTBJ5EYVV7ZW18B48Z0JVJJ80RHB706Y96Q358"
+ val out =
+ crypto.kdf(64, Base32Crockford.decode(ikm), Base32Crockford.decode(salt), Base32Crockford.decode(ctx))
+ assertEquals(expectedOut, Base32Crockford.encode(out))
+
+ val salt2 = "94KPT83141V6AWKS41SP2V3MF4G76VK1CDNG"
+ val ikm2 =
+ "94KPT83MD1JJ0WV5CDS6AX10D5Q70XBM41NPAY90DNGQ8SBJD5GPR83MD1GQ8838C5SJ0RK5CNQ20RV8C5Q6ESB441K6YWH08X75A82MC5P6AWG"
+ val ctx2 =
+ "94KPT83141HPYVKMCNW78833D1TPWTSC41GPRWVF41NPWVVQDRG62WS04XMPWSKF4WG6JVH0EHM6A82J8S1J0WVF41VPGY90DSQQ8833C5P6R83DCMG6JVK6DWZG"
+ val expectedOut2 =
+ "C6TXT6GMVZ8JNWZF27SPD6939FK035Y3FTWXYT0W4EZX9FNJ5KH24MQSK9D0MW3G7T1BBZ4ERVQC1RE24BNDPJQ68ZRQ8S3XN4GNHC8"
+ val out2 =
+ crypto.kdf(64, Base32Crockford.decode(ikm2), Base32Crockford.decode(salt2), Base32Crockford.decode(ctx2))
+ assertEquals(expectedOut2, Base32Crockford.encode(out2))
+
+ val salt3 = "C4G76RBCEG"
+ val ikm3 = "ESJQ4Y90EDJP6WK5EGG6PSBS"
+ val ctx3 = "D5Q6CVR"
+ val expectedOut3 = "ZYMG67TD51XYS0ZM3QZKH8HF8FW7CTVT91NAWD0PGZM2QGTNYKMXAA4ZJBH5V633TJW9E124CRYEY"
+ val out3 =
+ crypto.kdf(48, Base32Crockford.decode(ikm3), Base32Crockford.decode(salt3), Base32Crockford.decode(ctx3))
+ assertEquals(expectedOut3, Base32Crockford.encode(out3))
+
+ val salt4 = "84G76XV5CNT20WV1DHT20EH9"
+ val ikm4 = "9NWJ0SBREHS6AVB5DHWJ0WV5CDS6AX10DDJQJ"
+ val ctx4 = "CDQPWX35F1T20TBECSQKY88"
+ val expectedOut4 = "YM46SN0HYJ0RNGY1V5S7WY1SD2RQ1Y17G5Z37JD46E2Z8KY11PH5EV772RQ5NQ9SBMGG"
+ val out4 =
+ crypto.kdf(42, Base32Crockford.decode(ikm4), Base32Crockford.decode(salt4), Base32Crockford.decode(ctx4))
+ assertEquals(expectedOut4, Base32Crockford.encode(out4))
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/RecoupTest.kt b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/RecoupTest.kt
new file mode 100644
index 0000000..035b7d7
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/RecoupTest.kt
@@ -0,0 +1,90 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.lib.wallet.crypto
+
+import net.taler.lib.common.Amount
+import net.taler.lib.wallet.CoinRecord
+import net.taler.lib.wallet.CoinSourceType.REFRESH
+import net.taler.lib.wallet.CoinSourceType.WITHDRAW
+import net.taler.lib.wallet.CoinStatus.FRESH
+import net.taler.lib.wallet.crypto.Recoup.Request
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class RecoupTest {
+
+ private val crypto = CryptoFactory.getCrypto()
+ private val recoup = Recoup(crypto)
+
+ private class RecoupRequestVector(val record: CoinRecord, val request: Request)
+
+ @Test
+ fun test() {
+ val vectors = listOf(
+ RecoupRequestVector(
+ CoinRecord(
+ coinSource = WITHDRAW,
+ coinPub = "9YW99NYH54FWG87TP3SKCGR9MRWYSVR75X42FN4YAJC9579CQBJ0",
+ coinPriv = "EPPYWTDVWM4CXW75J8AAGWW620C7DCC3B45TM31KHKPYMT9VM7DG",
+ denomPub = "020000X3T40FNGSM3Y1QFKX9H4JY5EP70Y2CKDHD29B5BEZCTWRMT6AC9SA0G5YJ1XVYY580K6S93SFCKM5PFKP96H3KXDNP58EVQPTYDB5S0QY4V8B873NYA7EYRH25NJ8MR2VP6F7WWVMBK3NR3FSFP17PHPGF279NBSRTXWZSJZFX6RCTR6VS5WMSYFHZCR0P8R6MGHDCB3QW4M3G2001",
+ denomPubHash = "DG3114X57XKHQ1XM6AN0P7D2B6J96SVFG09S3SF43ZXCYYK9PGX84XP3ZY7WY3QD9JE1BWS2T8DGR78QXZZAVGED79HES10HAPTWBX8",
+ denomSig = "AHE8DGMTTKNWGCVQYTV56CBWA81DH10BQEBAM0A5YGRAZXRPVHMZ5FH0XW1523QXSTXT3WMS1X7FDMEZ3BR898YEDTXDTHEMX6RS11KCPBAZCGTNPHKYF6RH9414Q0PYT5BZKGKWJNAFPWQS715NXEFZBY1D6RPTAN520REJ4RTREC9PP5D8WVQ3B66Q4ARYQ3CK49K0ZDME0",
+ currentAmount = Amount("TESTKUDOS", 0, 0),
+ exchangeBaseUrl = "example.org",
+ suspended = false,
+ blindingKey = "1Y29A3ABERGYJR8Y9HS7XS8AYYDAKV6BZSXMZ0WS5VDTS150C100",
+ status = FRESH
+ ),
+ Request(
+ denomPubHash = "DG3114X57XKHQ1XM6AN0P7D2B6J96SVFG09S3SF43ZXCYYK9PGX84XP3ZY7WY3QD9JE1BWS2T8DGR78QXZZAVGED79HES10HAPTWBX8",
+ denomSig = "AHE8DGMTTKNWGCVQYTV56CBWA81DH10BQEBAM0A5YGRAZXRPVHMZ5FH0XW1523QXSTXT3WMS1X7FDMEZ3BR898YEDTXDTHEMX6RS11KCPBAZCGTNPHKYF6RH9414Q0PYT5BZKGKWJNAFPWQS715NXEFZBY1D6RPTAN520REJ4RTREC9PP5D8WVQ3B66Q4ARYQ3CK49K0ZDME0",
+ coinPub = "9YW99NYH54FWG87TP3SKCGR9MRWYSVR75X42FN4YAJC9579CQBJ0",
+ coinBlindKeySecret = "1Y29A3ABERGYJR8Y9HS7XS8AYYDAKV6BZSXMZ0WS5VDTS150C100",
+ coinSig = "GBN5MVEY6JATGGSTX5YF32G3G204Y1PF9ASVXQFN895DWN5ZK3CBY2NHC8ATB1E9JWSV1QD4ECM0XHP8Y6DFZ1S02MYD5NBKZ45B018",
+ refreshed = false
+ )
+ ),
+ RecoupRequestVector(
+ CoinRecord(
+ coinSource = REFRESH,
+ coinPub = "2YE003173JB6WNQ9HS73Z468F11KDHWWVGCPDHDTD6AY5AVJPQPG",
+ coinPriv = "GCR4R26XTCFNS109ZYC0G6M374K1ACNES1YH2CESWD86JBAE22WG",
+ denomPub = "020000X9M8MQVNH28D4J4YFA5ZZNKGNR0423BXQZV00RRN754XTDQMS5YKWQ3KSN8NV4V7CHDM22CRJ4WWQW05FDZC7VN0KK4S8VK9PYPPXNW6FJKHBSEZ2X1FCJKRC3T6PK2BKQ422Y2ASE76ZZAH6RRQT4SQGZTV3TRTSBC5AECJ5Z6C4RX7XFBERKVB45DA7H3V53YCYX1C41ZY5G2001",
+ denomPubHash = "J0G3G880JJJD09923AAWNQQZHJVRQT71ZK8KZGYW7T1P18PCPZ72FBAKDW3EFZ3QFZEW72EYJ9K0FG3RFZTFADQKZDDN9YT6BT2PE70",
+ denomSig = "8HVKAGMKRQRWB1HX9WCPX3FJ0SVE24DCAWQSHX4ZMXZ1KFZDNF4F0Z4K6ZCW142B2WDEH0W848W8WKH8P6A6EJR7J635QEF78CSJFF0EX1FRS5VY484GEX0HH3BDRDFGTHXNQRTTF1DD5ETMEG1QNKA3SAB24XZXZNQ6RDGTK02MRETP859NGMDD2F94F58JH4HYGXMAY0X32",
+ currentAmount = Amount("TESTKUDOS", 0, 0),
+ exchangeBaseUrl = "example.org",
+ suspended = false,
+ blindingKey = "C5VPT5F925ADJWK48PR07KV2W66EZQN4KYE146NY77DFM8GFCTXG",
+ status = FRESH
+ ),
+ Request(
+ denomPubHash = "J0G3G880JJJD09923AAWNQQZHJVRQT71ZK8KZGYW7T1P18PCPZ72FBAKDW3EFZ3QFZEW72EYJ9K0FG3RFZTFADQKZDDN9YT6BT2PE70",
+ denomSig = "8HVKAGMKRQRWB1HX9WCPX3FJ0SVE24DCAWQSHX4ZMXZ1KFZDNF4F0Z4K6ZCW142B2WDEH0W848W8WKH8P6A6EJR7J635QEF78CSJFF0EX1FRS5VY484GEX0HH3BDRDFGTHXNQRTTF1DD5ETMEG1QNKA3SAB24XZXZNQ6RDGTK02MRETP859NGMDD2F94F58JH4HYGXMAY0X32",
+ coinPub = "2YE003173JB6WNQ9HS73Z468F11KDHWWVGCPDHDTD6AY5AVJPQPG",
+ coinBlindKeySecret = "C5VPT5F925ADJWK48PR07KV2W66EZQN4KYE146NY77DFM8GFCTXG",
+ coinSig = "HGPAWTM2ZXVBZWYVSPS6S9DMSQWSVEJCQ78BN6WG2VND3PA7BQNHVE6142CGYX0VA82G5YP9SAV5YDNPNQJH2FTY5M6VM92QF6CB228",
+ refreshed = true
+ )
+ )
+ )
+ for (v in vectors) {
+ assertEquals(v.request, recoup.createRequest(v.record))
+ }
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/RefreshPlanchetTest.kt b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/RefreshPlanchetTest.kt
new file mode 100644
index 0000000..a56d191
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/RefreshPlanchetTest.kt
@@ -0,0 +1,112 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.lib.wallet.crypto
+
+import net.taler.lib.wallet.Base32Crockford
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class RefreshPlanchetTest {
+
+ private val crypto = CryptoFactory.getCrypto()
+
+ private val vectors = listOf(
+ TestVector(
+ "A9M953G6CJWBD21S82784Q3ZFEZYYWWKB9M6K4WQTTJZ8VTHPTTSHN8W3RMJQCCEKAFM04QD1PAQEN80DR6K0MNKYP404KMYW494D6R",
+ 27,
+ FreshCoin(
+ Base32Crockford.decode("JR124F1W9Q984Y01Y323CN88MVCJN7D8BJHYHCST5NWJTNCJMCQG"),
+ Base32Crockford.decode("YQS09P0FGBWS2FYY4NE5EDB93MKAYATNT8GZ33GSM34KF9PW6YWG"),
+ Base32Crockford.decode("KMWCX1W2HDKQM55ZRDZ630JE75YNDP8S8X8J02F9VSVZMNBWQ70G")
+ )
+ ),
+ TestVector(
+ "MRQ6KTFKJ5JMQS8QV5Z189W110V7F6BJJQMDHVKY17HDMTY7MYSB1MKRJY0ZGF0SC1TSZQNT7NJPJVJE17VBSBH05MJ26SQVPXCM9R8",
+ 65,
+ FreshCoin(
+ Base32Crockford.decode("W6BVA4JQKDJ294HFBDQGZK0VGGKRRXG820WWPJ1Y2TJ9SQYEXZWG"),
+ Base32Crockford.decode("FKH9KW7F73V8F4KEYME899RTKED2DVRM3A74MJJ5YT3P3CCGFS60"),
+ Base32Crockford.decode("BNDT4TPMV2R7HJVJAA8ZPF9J3Q0V1ZHY1JQ76723CBBQ99RX0GR0")
+ )
+ ),
+ TestVector(
+ "SCHMBND9ZMHDHTWQTSF1M41W4WPF9GM3FR5PJHC5JTZM25XEEC7CWC5SEN8FEWM54MSH27B9DJG91AKZ2YZ7ZMERG14QTBYJC02K21G",
+ 89,
+ FreshCoin(
+ Base32Crockford.decode("J75FF26KDZPBQW960SBG81W24BAY0YA04ZJ6PKQ1V0NQ7K62XNC0"),
+ Base32Crockford.decode("QS7BMWNG7SAEX7Z4H6QF66J5Q8GAX7JWHD4JTM9A656N43RH66C0"),
+ Base32Crockford.decode("JTNY89C0Y69X0NGMK2KB2JGGQZXX41GASPBN1YRS7KKV0MTP84CG")
+ )
+ ),
+ TestVector(
+ "S8DC2D6N1TW1TJQFRVDM5Q2HZ9G66MWVS4651GB2ST49TPFK2KWCRSADH51YNCMNGWK4JFT2SKQNWVA17XAR7EC18Z0X214PM1RF2MG",
+ 37,
+ FreshCoin(
+ Base32Crockford.decode("QWP8560H613GSNR2A1NGVQ023V2BP8ZYBVJJ0E2Y66ZGSHCX92K0"),
+ Base32Crockford.decode("XF58PHSGFDC30CBWQ55KDFWBDSZJMQ3AA6DF4V9SQ4E2DEFBS7N0"),
+ Base32Crockford.decode("KQT2K5HDKVVBHD188XS0KFBM63B4MAWM42HABT9FH806GME1QXHG")
+ )
+ ),
+ TestVector(
+ "THX83Y6G26CYMF186V6GPP5A4DQ6624D9EGCA6R7DVW6T32P6E6Q0TDW0S9MJSRG84SR2AMK1KKS6D627EJ02XSNH0BXDNWD1W35BWR",
+ 31,
+ FreshCoin(
+ Base32Crockford.decode("QWCD3DMV7NX87SPHYFWE3STBZQ02Q1NMXJ8GQGAA6WZ4GF7B3C80"),
+ Base32Crockford.decode("7J26RGBJFRGAYX8XDZZZ8PE9PCW3DH5KFA7564WZ6F5ADC159KB0"),
+ Base32Crockford.decode("M0A1EPG9WFB02Y7R6YYE6V6Q8DRJ466XCD6N99YFDAGN1QVA62NG")
+ )
+ ),
+ TestVector(
+ "D949HYZA6MWNKBKGTRRP1SGFA4X7AZK7HG5XX9YBVMWA05B884WAYY7MRDQNQYMRGA7AE01V6EEWZDF9JXMQKSYG0G1FVSZG4096JV8",
+ 26,
+ FreshCoin(
+ Base32Crockford.decode("BXRTK601B5NRJV9BAE88SAVPSMA20ZWDH5P0T6WFY7BSM3SZD930"),
+ Base32Crockford.decode("5N4WJWFR1J1SDNJGC8G9AV3ET4MPF6TGA4S1PJNQRJ5BRDHEEJN0"),
+ Base32Crockford.decode("P7XFE4W0H444XBYPRHB9VBTHKAZHNJE4E0R7P4D5FT90W3JJRE20")
+ )
+ ),
+ TestVector(
+ "7CXEEDABFD2FM802314FE91J2DS7ZHCHDGX27SJHJWWJWJZ1NGZBNZJ8T55W1P9ZEBPVYCHDNKQWRG68K3EJD069723857R06XDYE00",
+ 2147483647,
+ FreshCoin(
+ Base32Crockford.decode("QCAK6CA31KJ7HX4V3QEFK9CENT3E6DVE76M4VY9ZQ030CM8TZQJG"),
+ Base32Crockford.decode("2V7HAFK2ATBB9MDWRR8P9PSPXBWMGFXW5G75WVGYCRZB0G5PDG9G"),
+ Base32Crockford.decode("6C02KA6496FG5X0SDSWTS4JJHH87GYNSW9FZE6S34AWG5RJGPWQ0")
+ )
+ ),
+ TestVector(
+ "9F65N3N1BZW7TT6EB4X2GCDC5BN3J2R33R7H0NWPZENN6B4Y41XYTVC9SA5NP0E4WQFQEJ63WNC5R0C8KBMKDA5JCXEKEGJYVMTR7VG",
+ 0,
+ FreshCoin(
+ Base32Crockford.decode("S6RM093DHGG882T39KWC48807CFC36SRGGX54DBB1Z6NDAMT875G"),
+ Base32Crockford.decode("2DY3KVKA978S85ERF6QYF73Q95KQ0YVE89KKQ8JYR0FX161PV1S0"),
+ Base32Crockford.decode("PCY47EC1APBKBG9HWQCTJN6FWNJPGGC6XDJTYWBHTJ9A952AGCK0")
+ )
+ )
+ )
+
+ @Test
+ fun testRefreshPlanchets() {
+ for (v in vectors) {
+ val freshCoin = crypto.setupRefreshPlanchet(Base32Crockford.decode(v.seed), v.coinNumber)
+ assertEquals(v.freshCoin, freshCoin)
+ }
+ }
+
+ private class TestVector(val seed: String, val coinNumber: Int, val freshCoin: FreshCoin)
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/Sha256Test.kt b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/Sha256Test.kt
new file mode 100644
index 0000000..a71734a
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/Sha256Test.kt
@@ -0,0 +1,69 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.lib.wallet.crypto
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+@kotlin.ExperimentalStdlibApi
+class Sha256Test {
+
+ private val crypto = CryptoFactory.getCrypto()
+
+ @Test
+ fun testAbc() {
+ assertEquals(
+ "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ crypto.sha256("abc".encodeToByteArray()).toHexString()
+ )
+ }
+
+ @Test
+ fun testEmptyString() {
+ assertEquals(
+ "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ crypto.sha256("".encodeToByteArray()).toHexString()
+ )
+ }
+
+ @Test
+ fun testAbc448bits() {
+ assertEquals(
+ "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1",
+ crypto.sha256("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq".encodeToByteArray()).toHexString()
+ )
+ }
+
+ @Test
+ fun testAbc896bits() {
+ assertEquals(
+ "cf5b16a778af8380036ce59e7b0492370b249b11e8f07a51afac45037afee9d1",
+ crypto.sha256("abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu".encodeToByteArray())
+ .toHexString()
+ )
+ }
+
+ @Test
+ fun testAMillionAs() {
+ val input = ByteArray(1_000_000) { 0x61 }
+ assertEquals(
+ "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0",
+ crypto.sha256(input).toHexString()
+ )
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/Sha512Test.kt b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/Sha512Test.kt
new file mode 100644
index 0000000..59fd730
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/Sha512Test.kt
@@ -0,0 +1,116 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.lib.wallet.crypto
+
+import net.taler.lib.wallet.Base32Crockford
+import kotlin.random.Random
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class Sha512Test {
+
+ private val crypto = CryptoFactory.getCrypto()
+
+ @Test
+ fun testAbc() {
+ assertEquals(
+ "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f",
+ crypto.sha512("abc".encodeToByteArray()).toHexString()
+ )
+ }
+
+ @Test
+ fun testEmptyString() {
+ assertEquals(
+ "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e",
+ crypto.sha512("".encodeToByteArray()).toHexString()
+ )
+ }
+
+ @Test
+ fun testAbc448bits() {
+ assertEquals(
+ "204a8fc6dda82f0a0ced7beb8e08a41657c16ef468b228a8279be331a703c33596fd15c13b1b07f9aa1d3bea57789ca031ad85c7a71dd70354ec631238ca3445",
+ crypto.sha512("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq".encodeToByteArray()).toHexString()
+ )
+ }
+
+ @Test
+ fun testAbc896bits() {
+ assertEquals(
+ "8e959b75dae313da8cf4f72814fc143f8f7779c6eb9f7fa17299aeadb6889018501d289e4900f7e4331b99dec4b5433ac7d329eeb6dd26545e96e55b874be909",
+ crypto.sha512("abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu".encodeToByteArray())
+ .toHexString()
+ )
+ }
+
+ @Test
+ fun testAMillionAs() {
+ val input = ByteArray(1_000_000) { 0x61 }
+ assertEquals(
+ "e718483d0ce769644e2e42c7bc15b4638e1f98b13b2044285632a803afa973ebde0ff244877ea60a4cb0432ce577c31beb009c5c2c49aa2e4eadb217ad8cc09b",
+ crypto.sha512(input).toHexString()
+ )
+ }
+
+ @Test
+ fun testExchangeTvgHashCode() {
+ val input = "91JPRV3F5GG4EKJN41A62V35E8"
+ val output =
+ "CW96WR74JS8T53EC8GKSGD49QKH4ZNFTZXDAWMMV5GJ1E4BM6B8GPN5NVHDJ8ZVXNCW7Q4WBYCV61HCA3PZC2YJD850DT29RHHN7ESR"
+ assertEquals(output, Base32Crockford.encode(crypto.sha512(Base32Crockford.decode(input))))
+ }
+
+ @Test
+ fun testIncrementalHashing() {
+ val n = 1024
+ val d = Random.nextBytes(n)
+
+ val h1 = crypto.sha512(d)
+ val h2 = crypto.getHashSha512State().update(d).final()
+ assertEquals(Base32Crockford.encode(h1), Base32Crockford.encode(h2))
+
+ val s = crypto.getHashSha512State()
+ for (i in 0 until n) {
+ val b = ByteArray(1)
+ b[0] = d[i]
+ s.update(b)
+ }
+ val h3 = s.final()
+ assertEquals(Base32Crockford.encode(h1), Base32Crockford.encode(h3))
+ }
+
+ @Test
+ fun testIncrementalHashing2() {
+ val n = 10
+ val d = Random.nextBytes(n)
+
+ val h1 = crypto.sha512(d)
+ val h2 = crypto.getHashSha512State().update(d).final()
+ assertEquals(Base32Crockford.encode(h1), Base32Crockford.encode(h2))
+
+ val s = crypto.getHashSha512State()
+ for (i in 0 until n) {
+ val b = ByteArray(1)
+ b[0] = d[i]
+ s.update(b)
+ }
+ val h3 = s.final()
+ assertEquals(Base32Crockford.encode(h1), Base32Crockford.encode(h3))
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/SignatureTest.kt b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/SignatureTest.kt
new file mode 100644
index 0000000..b3228b9
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/crypto/SignatureTest.kt
@@ -0,0 +1,493 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.lib.wallet.crypto
+
+import net.taler.lib.common.Amount
+import net.taler.lib.common.Timestamp
+import net.taler.lib.wallet.Base32Crockford
+import net.taler.lib.wallet.crypto.Signature.PurposeBuilder
+import net.taler.lib.wallet.exchange.DenominationRecord
+import net.taler.lib.wallet.exchange.DenominationStatus.Unverified
+import net.taler.lib.wallet.exchange.DenominationStatus.VerifiedBad
+import net.taler.lib.wallet.exchange.WireFee
+import kotlin.random.Random
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class SignatureTest {
+
+ private val crypto = CryptoFactory.getCrypto()
+ private val signature = Signature(crypto)
+
+ private class PurposeBuilderVector(val purposeNum: Int, val chunks: List<String>, val result: String)
+
+ @Test
+ fun testSignaturePurposeBuilder() {
+ val vectors = listOf(
+ PurposeBuilderVector(42, listOf("46D6FNS4VAFW0"), "000004000002M8CTCZBJ9PMZR0"),
+ PurposeBuilderVector(
+ 1375883724,
+ listOf("7H2ENBHG1AVRD5QVKYCXKAZQQQGCSEAAZ4"),
+ "000007AJ098WRF24XAQ302NQGTBFQ7WSV6NZFFF0SJWMNY8"
+ ),
+ PurposeBuilderVector(251873401, listOf("KVSRVCV6J7QGYR0"), "0000048F0D47K7QKHPSPD4FF1XG0"),
+ PurposeBuilderVector(
+ 2126178285,
+ listOf("EJN927GRT1HNS0K53TBPWYB4", "QGSXW3NGHPJCVTBX3KHGW7GBC1SJWPK0", "XK7GA"),
+ "00000BKYQBKYTX5AJ4F1HM33BJ16A7MQDSWP9F1KVR7B13D4SQMQT7731RF0PR3K5SD61V6F0M"
+ ),
+ PurposeBuilderVector(
+ 871975602,
+ listOf("JC26GG7M", "D5GER58NSW3MX3AB2EA0", "HVB81PFV0YYQ2170H77TQPZTB21T314P9B1N3VSXEHZ88VEPHW"),
+ "00000E1KZ55B54R4D10F8TB0XGAHBKR79T6MP4WMHVB81PFV0YYQ2170H77TQPZTB21T314P9B1N3VSXEHZ88VEPHW"
+ ),
+ PurposeBuilderVector(
+ 366986877,
+ listOf("28", "48213TXMKY8H8EGRJ9XP00D9MHRHQG5N02W5GG0", "VR4CDW7WQ2FYQF3WBG60"),
+ "00000B8NVZ37T4H20G8YQD4ZJ4A3M64JFDG03AD4E4DW1D80Q1C41QG8RVRFSE4ZXEY7RQ0C"
+ ),
+ PurposeBuilderVector(
+ 1369171164,
+ listOf(
+ "SJTSKNX7W7CAP848GCESA09HQQQYQN50G1GY5AHPP12D4GMZJCXMCFPYECGEBF8",
+ "J70SVFRH8WN1DP7DZ60BZW0",
+ "275VB93D2MV6NN7V97QXC16VMX07JNCC5R3HBH1TSWTW5V72G1E66HP73ANWFXE6NG"
+ ),
+ "00000SJHKFJDSK5NK7BTFRERNCG8H0RXJM0K3FFFXFAA1031WAN3DC24T919Z4SV8RZDWWS0WPYS3GCXQW8MEAGPV3PZK05ZY08WQDD4DMAKCTPMZD4YZNG4VEKM0YANHGQ0E5E47B7KBGQCWA05RRT6RWDAQHZNRTP0"
+ ),
+ PurposeBuilderVector(
+ 2115762114,
+ listOf("Z7A3PQ7BV5G5D1P3F2EY9KN4GKVEG95KZD50", "SZ3PA4BSWF8T22TJMDZN5877Z9DQFC67K9E0"),
+ "00000D3Y3FVW5YEM7DEEQPB0AT3C6Y4XWK7A917PX0JB7YTASZ3PA4BSWF8T22TJMDZN5877Z9DQFC67K9E0"
+ ),
+ PurposeBuilderVector(
+ 1382722498,
+ listOf("0WEEY7GRA8Q8ZWS2NRP0", "FQ5YY3ZWJ64014A0SPWMCE4B58", "G03C6M9PZ45D99G4M2HZJ"),
+ "00000CAJDANW41RWXWF1GMHEHZSJ5BHCFQ5YY3ZWJ64014A0SPWMCE4B5A00DGTH6VWGNN560JGA7Y8"
+ ),
+ PurposeBuilderVector(
+ 1557782497,
+ listOf(
+ "CEB9E7Q87X12R172RZ8DAHSGGS60",
+ "M17JXQ290P209Q7EH759PW558FYZYTEHR40J18M81W",
+ "B918DJVWTERCHS54S0G93DFPVK9TV383DMRJR43ZTFZBAE3CVSFG"
+ ),
+ "00000MTWV7FY2RWPJWFEGFT25G2E5HYGTN3K11JCM17JXQ290P209Q7EH759PW558FYZYTEHR40J18M81XD451PBFK9V1J74MK4214DNYVED7BCD0DPK2B0GFZ9ZXD9RDKF5Y"
+ ),
+ PurposeBuilderVector(1662106597, listOf("621Y24CA3ZCHJJ8"), "000004B326XYAC43W48RM7YS354G"),
+ PurposeBuilderVector(
+ 393436430,
+ listOf(
+ "6WMDVK6XKGEAX152ZM4SHQ77DCRQB4RYJX0MGCZFVVK3XTG",
+ "8TKEY2F1PMAT396GC3E191182F34FXXR12MR7Z6WMG4S10GZX7RJG"
+ ),
+ "00000HGQEDEGWDS8VQ6DV70WNT2A5Z89K3EEETSHEP9HX5T190SYZQQ67VN4D9QF17GVA5D1MK861Q0MGGM17HJ7YYW0HAC3ZKEA82CGG8FYKW98"
+ ),
+ PurposeBuilderVector(
+ 1616746709,
+ listOf("ZQT1F901YWFDX6XZZB6R8PSQ1QJANCPF63T8RE09HRK2XQ7EE2K0F74GERH4GQ0"),
+ "00000BV0BPCDBZFM2YJ03XRYVTDVZYPDGHDKE3F4NASCYC7MHGW0K3H65VEEWW560YE90XH291E0"
+ ),
+ PurposeBuilderVector(
+ 691857007,
+ listOf("5JT5DM7KWVW7A7P764NTG0FJDWEQTQAGB253J547GXWX93204NM0"),
+ "00000A197KK6YB5MAV8F7SQREMFCEC9BN00Z4VRXFNEN0P4A74A8F1VSTJ6409B8"
+ ),
+ PurposeBuilderVector(
+ 582885064,
+ listOf("W5P2HP14MCNSKX4CZ4FZ0BDPSX2SV7MCE45V8PF7DW9EXAGK3TC0"),
+ "00000A12QRFCHRBC53C298SBK7T8SY8ZY0PVDKT5KPF8RW8BPHCYEVRJXTN167MR"
+ ),
+ PurposeBuilderVector(
+ 480383332,
+ listOf(
+ "49DZJY0K1WJFDK7P86Q1R2F150S50JQR8HQW059HBG9CR615N4V82",
+ "D0XXQKX9BB0HVJK34TKCZJ1E2JQKF06EQVXRRD2ZKX4QGXG",
+ "99JY6SS2HJ1J29GS1793CGE5BKSG"
+ ),
+ "00000NRWM88P88JVZ5W163S4YV6FCGDE3G4Y2A1JA15FGH3FR0AK2Q0JSGC2BA9PG5M3QPYFN5DC27EACCKADKY85RAAYDW0STZFQ31MBYFMJY3P99JY6SS2HJ1J29GS1793CGE5BKSG"
+ ),
+ PurposeBuilderVector(
+ 877078324,
+ listOf("RC", "K1PKX78BPGNQSSZCVRB3B6PJYR", "74"),
+ "000006HM8WKK9GWRDMZ9T2XM5DYEFV6Y2RTSNMQP74"
+ ),
+ PurposeBuilderVector(
+ 25488155,
+ listOf(
+ "FNG58X0W2A4HV4ZY5TKS73RGNHA8KZ3B4WWDJS2V1B27F3G6G08FNHM8NYBZ45Q8",
+ "NQGNDPDNW4G9AEHXB1BKXQXK84"
+ ),
+ "00000G01GKNHPZB0AHT1R4M93P9ZWBN7JE7H1B2MH7Y6P9SRV5J5P2P4EY70D00GZB38HBWQY8BEHBF1AVCVBR90JMX3TP2Q7VFV6G8"
+ ),
+ PurposeBuilderVector(
+ 749389285,
+ listOf(
+ "FVD8X18A3S2KHJ7143TCEHE3K083DRBG2CA0D766NMRVHX7MSN78NSYHZSWNH9V2DM",
+ "1TSPZ4VBKEBKCQEB2673H2FJHCZ6M19R2691NY79A0",
+ "P9VW044VY2TGKNNF4ZWZ1S2ACM4VJYCWP7ZGWRKR1264ZS2CQZGSXZ775AC41X5778"
+ ),
+ "00000X1CNB2YAZPTHT2GM7J5734E287MRX2W760G6VGQ04RM0TECDB9HQ3TF9KAEHBKX3ZKSB2KP4V8EPDQS6TWVJWV5VJRHHRW8KWMB7SN0AE0HJ8DFHTAGP9VW044VY2TGKNNF4ZWZ1S2ACM4VJYCWP7ZGWRKR1264ZS2CQZGSXZ775AC41X5778"
+ ),
+ PurposeBuilderVector(
+ 434249902,
+ listOf(
+ "VFERBNSH7X70",
+ "T5DWNBWMF3BPAGWSESKF7WGTTS3JHVKAAR3DR",
+ "SMCA0184DN3WE934BGFE4MCE14279NYCFF0PJDQ20JRHFZXSGXE6CP1BSMGR479HAC"
+ ),
+ "00000KRSW8GAXPYXGQBK2FTET5DWNBWMF3BPAGWSESKF7WGTTS3JHVKAAR3DSK8RM02G8VA7RWJ68Q0YW98RW284EKBWRYY1D4VE415H2ZZVK1TWCSC2QK91G8EK2MR"
+ ),
+ PurposeBuilderVector(
+ 1710327887,
+ listOf("S64XB965NEAQQ5287XHNT126BW"),
+ "00000635Y644ZJC9TPJCBAWNFEA4GFV3BM24CQR"
+ ),
+ PurposeBuilderVector(
+ 1247371119,
+ listOf("CSXX5W59E3S03412M29J0HG0R67BW4B3X946GS2T0TVYPH9HFP9FC0M7", "931KSGS0ZJG4A79BT55G0Q4Q"),
+ "00000EJAB5FPYSKVTBRAJW7J0682584K41301GCEQR8P7TJ8D1J5M1NQXD2K2ZCJYR18EJ637K1J1Z508MEJQMAB01E9E"
+ ),
+ PurposeBuilderVector(
+ 2024386767,
+ listOf(
+ "R2WZA0FKD57D418NB2KK84PJKHAVPEC6219X2KQJ78JFB76F0R",
+ "CNYD5TAC7Y3N58CN6VK9F7NBKD2NJJK3VJ91CZKBFZWDVVZB28",
+ "G7WQJVXXYT4JHA1NK22RXS45BM0W5H5JD7JNX3PWD2WZ27Y5Y1FB4"
+ ),
+ "00000SVRN6RCZG5SYM0Z6TAET82HAP576G9D572NQCWRC42KT57F4EH4YPECY1K5FK9EJK1ZGX9A359PWTBSXAWV8NCMMRYWJ8B7WTVZZ3EYZTRJG7WQJVXXYT4JHA1NK22RXS45BM0W5H5JD7JNX3PWD2WZ27Y5Y1FB4"
+ ),
+ PurposeBuilderVector(1182512118, listOf("KH32THSNNZGJPC0"), "000004A6FESZD7265N3KBBZ15CR0"),
+ PurposeBuilderVector(
+ 2029783771,
+ listOf("N7468QH02K49BTD16S1F6FB4NKNXB0Q410"),
+ "000007BRZG5DQAE8CHF20568JQMT2DJ2YCYP9B7BTP1E820"
+ ),
+ PurposeBuilderVector(
+ 1111140596,
+ listOf("6BN45PA2RCVRR8PJ6XKRJ9ZER2BR3MY9RQ290FADAG"),
+ "000008J27AMF8CQA8BCM5GSQHGHD4DV7H4KYXG4QG79WKHE4J0YMTN0"
+ )
+ )
+ for (v in vectors) {
+ val builder = PurposeBuilder(v.purposeNum)
+ for (chunk in v.chunks) builder.put(Base32Crockford.decode(chunk))
+ val encodedBytes = Base32Crockford.encode(builder.build())
+ assertEquals(v.result, encodedBytes)
+ }
+ }
+
+ private class PaymentSignatureVector(val publicKey: String, val hash: String, val signature: String)
+
+ @Test
+ fun testVerifyPaymentSignature() {
+ val vectors = listOf(
+ PaymentSignatureVector(
+ "8GSJZ649T2PXMKZC01Y4ANNBE7MF14QVK9SQEC4E46ZHKCVG8AS0",
+ "CW96WR74JS8T53EC8GKSGD49QKH4ZNFTZXDAWMMV5GJ1E4BM6B8GPN5NVHDJ8ZVXNCW7Q4WBYCV61HCA3PZC2YJD850DT29RHHN7ESR",
+ "JSNG99MX5W4AS7AEA8D4ADCHYTHFER0GX1N064E1XX48N513AXAEHDTG8ZT7ANWQK5HGCAXGEWN7TCBTYVG3RDPBTAS5HEP608KQ40R"
+ ),
+ PaymentSignatureVector(
+ "6WC3MPYM5XKPKRA2Z6SYB81CPFV3E7EC6S2GVE095X8XH63QTZCG",
+ "Z6H76JXPJFP3JBGSF54XBF0BVXDJ0CJBK4YT9GVR1AT916ZD57KP53YZN5G67A4YN95WGMZKQW7744483P5JDF06B6S7TMK195QGP20",
+ "T2Y4KJJPZ0F2DMNF5S81V042T20VHB5VRXQYX4RF8KRH6H2Z4JRBD05CCDJ6C625MHM5FQET00RDX2NF5QX63S9YDXEP0710VBYHY10"
+ ),
+ PaymentSignatureVector(
+ "2X3PSPT7D6TEM97R98C0DHZREFVAVA3XTH11D5A2Z2K7GBKQ7AEG",
+ "JSNG99MX5W4AS7AEA8D4ADCHYTHFER0GX1N064E1XX48N513AXAEHDTG8ZT7ANWQK5HGCAXGEWN7TCBTYVG3RDPBTAS5HEP608KQ40R",
+ "W0783H1KZ6GX58T5ZEH5VYMTXP7P7EA3KBKQ4Y8CN8M20GY8RNA4RX1AZG6TQ70NR4XG4EZ9D606P4RDAARD2SXTKA90BJMN9VEAC00"
+ )
+ )
+ for (v in vectors) {
+ // verification succeeds as expected
+ assertTrue(signature.verifyPayment(v.signature, v.hash, v.publicKey))
+ // verification fails which different signature
+ val size = Base32Crockford.calculateDecodedDataLength(v.signature.length)
+ val sig = Base32Crockford.encode(Random.nextBytes(size))
+ assertFalse(signature.verifyPayment(sig, v.hash, v.publicKey))
+ // verification fails which different hash
+ val hash = Base32Crockford.encode(Random.nextBytes(64))
+ assertFalse(signature.verifyPayment(v.signature, hash, v.publicKey))
+ // verification fails which different public key
+ val publicKey = Base32Crockford.encode(Random.nextBytes(32))
+ assertFalse(signature.verifyPayment(v.signature, v.hash, publicKey))
+ }
+ }
+
+ private class WireFeeSignatureVector(val wireFee: WireFee, val masterPub: String)
+
+ @Test
+ fun testVerifyWireFeeSignature() {
+ val type = "x-taler-bank"
+ val vectors = listOf(
+ WireFeeSignatureVector(
+ WireFee(
+ wireFee = Amount("TESTKUDOS", 0, 1000000),
+ closingFee = Amount("TESTKUDOS", 0, 1000000),
+ startStamp = Timestamp(1609470000000),
+ endStamp = Timestamp(1641006000000),
+ signature = "C77EZ56ZT4ACNPGP3PX881S42413N37NJVTRPNMM9BWRVGQBK2C157HHRF61RMYPAWW6QVWV5RVKEVBD3XAVJFXAEBM2HPNS5AMPY18"
+ ), "Y8JGNCPMM7XTF44FT7V1JRNNJQ6F4R7DZPXAKYYCJZJVEX2C45QG"
+ ),
+ WireFeeSignatureVector(
+ WireFee(
+ wireFee = Amount("TESTKUDOS", 0, 1000000),
+ closingFee = Amount("TESTKUDOS", 0, 1000000),
+ startStamp = Timestamp(1577847600000),
+ endStamp = Timestamp(1609470000000),
+ signature = "4F87Z12WQM7JXEKPRPGFZVGCZPNN6Q9RVP2NA0PZ57CYQP4QXV38EQW59X3K5WAQN82BX95X57775ZJGA5EB3VA5GV9S3JBA3RGX41G"
+ ), "0BJ4SX13W4Q64QDDSRBVTYSWT8C0X2HVB61QYSZM1B1W9JVCE0C0"
+ ),
+ WireFeeSignatureVector(
+ WireFee(
+ wireFee = Amount("TESTKUDOS", 0, 1000000),
+ closingFee = Amount("TESTKUDOS", 0, 1000000),
+ startStamp = Timestamp(1672542000000),
+ endStamp = Timestamp(1704078000000),
+ signature = "9YWES9YPR2W5ACCYE4ZE4S3PE62CVX01AXXSC9KT9PZ6B52D5GSBP4DG95Y024EDE5HFGP6FEZVPKQM8PA6J9WD2NXW3QBA34MEV61R"
+ ), "FWPD4E312RB8XZ22FGHCZGV2YPT7AX02XVRZEC7F53QKZJHY7H50"
+ )
+ )
+ for (v in vectors) {
+ // verification succeeds as expected
+ assertTrue(signature.verifyWireFee(type, v.wireFee, v.masterPub))
+ // different type fails verification
+ assertFalse(signature.verifyWireFee("foo", v.wireFee, v.masterPub))
+ // different WireFee wireFee fails verification
+ var wireFee = v.wireFee.copy(wireFee = Amount("TESTKUDOS", 0, 100))
+ assertFalse(signature.verifyWireFee(type, wireFee, v.masterPub))
+ // different WireFee closingFee fails verification
+ wireFee = v.wireFee.copy(closingFee = Amount("TESTKUDOS", 0, 100))
+ assertFalse(signature.verifyWireFee(type, wireFee, v.masterPub))
+ // different WireFee startStamp fails verification
+ wireFee = v.wireFee.copy(startStamp = Timestamp(v.wireFee.startStamp.ms + 1000))
+ assertFalse(signature.verifyWireFee(type, wireFee, v.masterPub))
+ // different WireFee endStamp fails verification
+ wireFee = v.wireFee.copy(endStamp = Timestamp(v.wireFee.endStamp.ms + 1000))
+ assertFalse(signature.verifyWireFee(type, wireFee, v.masterPub))
+ // different WireFee signature fails verification
+ val size = Base32Crockford.calculateDecodedDataLength(v.wireFee.signature.length)
+ wireFee = v.wireFee.copy(signature = Base32Crockford.encode(Random.nextBytes(size)))
+ assertFalse(signature.verifyWireFee(type, wireFee, v.masterPub))
+ // startStamp changes below one second don't affect verification
+ wireFee = v.wireFee.copy(startStamp = Timestamp(v.wireFee.startStamp.ms + 999))
+ assertTrue(signature.verifyWireFee(type, wireFee, v.masterPub))
+ // startStamp changes below one second don't affect verification
+ wireFee = v.wireFee.copy(endStamp = Timestamp(v.wireFee.endStamp.ms + 999))
+ assertTrue(signature.verifyWireFee(type, wireFee, v.masterPub))
+ // different masterPub fails verification
+ val masterPub = Base32Crockford.encode(Random.nextBytes(32))
+ assertFalse(signature.verifyWireFee(type, v.wireFee, masterPub))
+ }
+ }
+
+ private class DenominationRecordSignatureVector(val denominationRecord: DenominationRecord, val masterPub: String)
+
+ @Test
+ fun testVerifyDenominationRecordSignature() {
+ val vectors = listOf(
+ DenominationRecordSignatureVector(
+ DenominationRecord(
+ value = Amount("TESTKUDOS", 4, 0),
+ denomPub = "020000XX26WN5X7ECKERVGBWTFM3KJ7AT0N8T7RQB7W7G4Q5K0W1BT8QFQBMMTR925TC6RX4QGVVVXH2ZMJVWDRR58BRXNDCFBZTH7RNS1KVWZQGEWME1D6QX79R0V6V9S0NC9H8YP0W6MJD7YSV5VQZWCR1JXRTSS0HPHDV4V6AVX34TSMHGRDQXZYVTEBGVWNF8TNR2P5296TCZR0G2001",
+ denomPubHash = "MY54RXQ1WTZPFD3VZ4QJH6RHFPTYE78Y4DQ3GANTWK2Z0SQ99AK90K5H0P419EY4QWV6S4Q6QG52HYFCVRCPEQNG22RM3E7XW2YFJ9R",
+ feeWithdraw = Amount("TESTKUDOS", 0, 3000000),
+ feeDeposit = Amount("TESTKUDOS", 0, 3000000),
+ feeRefresh = Amount("TESTKUDOS", 0, 4000000),
+ feeRefund = Amount("TESTKUDOS", 0, 2000000),
+ stampStart = Timestamp(1593449948000),
+ stampExpireWithdraw = Timestamp(1594054748000),
+ stampExpireLegal = Timestamp(1688057948000),
+ stampExpireDeposit = Timestamp(1656521948000),
+ masterSig = "2PEF4EHTKE2R89KHQBZ6NAQW34YEMAP044DTJXJ9TFX224YW07BB2X1VHEYH425N2RNDZS9XFEAQK54GV6Q6CTNSE1Z038TZ4V3MM1R",
+ status = Unverified, isOffered = false, isRevoked = false, exchangeBaseUrl = "example.org"
+ ),
+ "C5K3YXPHSDVYPCB79D5AC4FYCKQ9D0DN3N3MBA5D54BG42SX0ZRG"
+ ),
+ DenominationRecordSignatureVector(
+ DenominationRecord(
+ value = Amount("TESTKUDOS", 0, 10000000),
+ denomPub = "020000XWWS2DFXM1G8YR6NXVF07R0DH7GRE1J7ZC2YENN7Q60ZHX9S7FEVQDBFP1041DN0GFASZR6A7RSJ3EYRV1YD5ACAZRDQNH5P5KEBTSK5XA76YYWTW5VA7N1V1VRVF0CF7VZ8GSJNQ91FSJGNKGR82DB7H82TPAGMR1B5DG5SY2WP3NB5YJD90H64E09C0YC8FCCWRGC09HFNPG2001",
+ denomPubHash = "J73HZJTR76GRZKBEKQC4X289056V5RMY4JS932XM8BAENQ83YAF9W2WYKRBN87TN8ENXP61JC7HMC7PYK9MZ8S289FCD07EM95AJGMR",
+ feeWithdraw = Amount("TESTKUDOS", 0, 1000000),
+ feeDeposit = Amount("TESTKUDOS", 0, 1000000),
+ feeRefresh = Amount("TESTKUDOS", 0, 3000000),
+ feeRefund = Amount("TESTKUDOS", 0, 1000000),
+ stampStart = Timestamp(1593450307000),
+ stampExpireWithdraw = Timestamp(1594055107000),
+ stampExpireLegal = Timestamp(1688058307000),
+ stampExpireDeposit = Timestamp(1656522307000),
+ masterSig = "HPBAY19C1B5H3FAYWKRQFS8VC693658SNSK3BB304BNAG950BS881GMVPVN0YBG67K9J9E4A9BFN7VAQEY1D5G6YAGVPFQWX0GRQ62G",
+ status = Unverified, isOffered = false, isRevoked = false, exchangeBaseUrl = "example.org"
+ ),
+ "NF8G14QFVWJSQPNFC3M2JKNMGJESS8ZWZX9V43PG40YXWRWA88VG"
+ )
+ )
+ for (v in vectors) {
+ // verification succeeds as expected
+ assertTrue(signature.verifyDenominationRecord(v.denominationRecord, v.masterPub))
+ // different masterPub fails verification
+ val masterPub = Base32Crockford.encode(Random.nextBytes(32))
+ assertFalse(signature.verifyDenominationRecord(v.denominationRecord, masterPub))
+ // different value fails verification
+ val value = v.denominationRecord.value + Amount.min(v.denominationRecord.value.currency)
+ assertFalse(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(value = value),
+ v.masterPub
+ )
+ )
+ // different denomPubHash fails verification
+ val calculatedDenomPubHash = crypto.sha512(Base32Crockford.decode(v.denominationRecord.denomPub))
+ assertEquals(v.denominationRecord.denomPubHash, Base32Crockford.encode(calculatedDenomPubHash))
+ val denomPubHash = Base32Crockford.encode(Random.nextBytes(calculatedDenomPubHash.size))
+ assertFalse(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(denomPubHash = denomPubHash),
+ v.masterPub
+ )
+ )
+ // different feeWithdraw fails verification
+ val feeWithdraw = v.denominationRecord.feeWithdraw + Amount.min(v.denominationRecord.feeWithdraw.currency)
+ assertFalse(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(feeWithdraw = feeWithdraw),
+ v.masterPub
+ )
+ )
+ // different feeDeposit fails verification
+ val feeDeposit = v.denominationRecord.feeDeposit + Amount.min(v.denominationRecord.feeDeposit.currency)
+ assertFalse(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(feeDeposit = feeDeposit),
+ v.masterPub
+ )
+ )
+ // different feeRefresh fails verification
+ val feeRefresh = v.denominationRecord.feeRefresh + Amount.min(v.denominationRecord.feeRefresh.currency)
+ assertFalse(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(feeRefresh = feeRefresh),
+ v.masterPub
+ )
+ )
+ // different feeRefund fails verification
+ val feeRefund = v.denominationRecord.feeRefund + Amount.min(v.denominationRecord.feeRefund.currency)
+ assertFalse(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(feeRefund = feeRefund),
+ v.masterPub
+ )
+ )
+ // different stampStart fails verification
+ val stampStart = Timestamp(v.denominationRecord.stampStart.ms + 1000)
+ assertFalse(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(stampStart = stampStart),
+ v.masterPub
+ )
+ )
+ // different stampExpireWithdraw fails verification
+ val stampExpireWithdraw = Timestamp(v.denominationRecord.stampExpireWithdraw.ms + 1000)
+ assertFalse(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(stampExpireWithdraw = stampExpireWithdraw),
+ v.masterPub
+ )
+ )
+ // different stampExpireLegal fails verification
+ val stampExpireLegal = Timestamp(v.denominationRecord.stampExpireLegal.ms + 1000)
+ assertFalse(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(stampExpireLegal = stampExpireLegal),
+ v.masterPub
+ )
+ )
+ // different stampExpireDeposit fails verification
+ val stampExpireDeposit = Timestamp(v.denominationRecord.stampExpireDeposit.ms + 1000)
+ assertFalse(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(stampExpireDeposit = stampExpireDeposit),
+ v.masterPub
+ )
+ )
+ // different masterPub fails verification
+ val size = Base32Crockford.calculateDecodedDataLength(v.denominationRecord.masterSig.length)
+ val masterSig = Base32Crockford.encode(Random.nextBytes(size))
+ assertFalse(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(masterSig = masterSig),
+ v.masterPub
+ )
+ )
+ // different status does not affect verification
+ assertTrue(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(status = VerifiedBad),
+ v.masterPub
+ )
+ )
+ // different exchangeBaseUrl does not affect verification
+ assertTrue(
+ signature.verifyDenominationRecord(
+ v.denominationRecord.copy(exchangeBaseUrl = "foo.bar"),
+ v.masterPub
+ )
+ )
+ }
+ }
+
+ private class WireAccountSignatureVector(val paytoUri: String, val signature: String, val masterPub: String)
+
+ @Test
+ fun testVerifyWireAccount() {
+ val paytoUri = "payto://x-taler-bank/localhost/Exchange"
+ val vectors = listOf(
+ WireAccountSignatureVector(
+ paytoUri,
+ "7THNYN5G12GXZABHP187XJZ3ACTDKAWGHWYM2ERA1VGE4JMGPADXT37ZM8D4DVAQTSP8CR61VAD4ZZSWKRPP5KQ12JCTVHCKDD3KA3R",
+ "QPJSEA4SM8E67106C6MN2TMG4308J20C1T0D411WED1FJ00VF8ZG"
+ ),
+ WireAccountSignatureVector(
+ paytoUri,
+ "6JJDJ0HG3AGRTEY1FFGHR89367GPE4FS7TC8Z26N34PHFAMSRXQ4FA7P96CDS6625SRNAN4DY1NK4TNBQXKRXQ9QR82HEVCB2FF1E18",
+ "8QBE0SRR84GWJ1FX2QCGGNRZTWA2FCV6YQC98Q26DRRJ0QBQE930"
+ ),
+ WireAccountSignatureVector(
+ paytoUri,
+ "X5EYQJ388P8AH1PPYD2GFQ9Y1NGA7HV0TXAW6GJ7C0D6R6GAY0059HCDBE98TKJJT6MB1S660FV13DV46JDKJ622MR961XVGP6DG618",
+ "802M037TN8GHBXEGBPC7J41HJC06TWBWXYH36AYQHRJ7NVHJBQQ0"
+ )
+ )
+ for (v in vectors) {
+ // verification succeeds as expected
+ assertTrue(signature.verifyWireAccount(v.paytoUri, v.signature, v.masterPub))
+ // different paytoUri fails verification
+ assertFalse(signature.verifyWireAccount("foo", v.signature, v.masterPub))
+ // different paytoUri fails verification
+ val size = Base32Crockford.calculateDecodedDataLength(v.signature.length)
+ val sig = Base32Crockford.encode(Random.nextBytes(size))
+ assertFalse(signature.verifyWireAccount(v.paytoUri, sig, v.masterPub))
+ // different paytoUri fails verification
+ val masterPub = Base32Crockford.encode(Random.nextBytes(32))
+ assertFalse(signature.verifyWireAccount(v.paytoUri, sig, masterPub))
+ }
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/lib/wallet/exchange/DenominationTest.kt b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/exchange/DenominationTest.kt
new file mode 100644
index 0000000..c52638b
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/exchange/DenominationTest.kt
@@ -0,0 +1,91 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.lib.wallet.exchange
+
+import net.taler.lib.common.Amount
+import net.taler.lib.common.Timestamp
+import net.taler.lib.common.Timestamp.Companion.NEVER
+import net.taler.lib.wallet.exchange.DenominationStatus.VerifiedBad
+import net.taler.lib.wallet.exchange.DenominationStatus.VerifiedGood
+import net.taler.lib.wallet.exchange.Denominations.denomination1
+import net.taler.lib.wallet.exchange.Denominations.denomination10
+import net.taler.lib.wallet.exchange.Denominations.denomination2
+import net.taler.lib.wallet.exchange.Denominations.denomination5
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class DenominationTest {
+
+ @Test
+ fun testGetEarliestDepositExpiry() {
+ // empty selection info never expires
+ val infoEmpty = DenominationSelectionInfo(
+ totalCoinValue = Amount.zero("TESTKUDOS"),
+ totalWithdrawCost = Amount.zero("TESTKUDOS"),
+ selectedDenominations = emptyList()
+ )
+ assertEquals(Timestamp(NEVER), infoEmpty.getEarliestDepositExpiry())
+
+ // earliest expiry of single denomination is that of the denomination
+ val info1 = infoEmpty.copy(
+ selectedDenominations = listOf(SelectedDenomination(1, denomination10))
+ )
+ assertEquals(denomination10.stampExpireDeposit, info1.getEarliestDepositExpiry())
+
+ // denomination that expires earlier gets selected
+ val info2 = infoEmpty.copy(
+ selectedDenominations = listOf(
+ SelectedDenomination(3, denomination5.copy(stampExpireDeposit = Timestamp(42))),
+ SelectedDenomination(2, denomination2.copy(stampExpireDeposit = Timestamp(2))),
+ SelectedDenomination(1, denomination1.copy(stampExpireDeposit = Timestamp(1)))
+ )
+ )
+ assertEquals(Timestamp(1), info2.getEarliestDepositExpiry())
+
+ // denomination that expires at all is earlier than the one that never expires
+ val info3 = infoEmpty.copy(
+ selectedDenominations = listOf(
+ SelectedDenomination(2, denomination2.copy(stampExpireDeposit = Timestamp(NEVER))),
+ SelectedDenomination(1, denomination1.copy(stampExpireDeposit = Timestamp(1)))
+ )
+ )
+ assertEquals(Timestamp(1), info3.getEarliestDepositExpiry())
+ }
+
+ @Test
+ fun testIsWithdrawableDenomination() {
+ // denomination is withdrawable
+ assertTrue(denomination1.isWithdrawable())
+ // denomination is withdrawable when VerifiedGood
+ assertTrue(denomination1.copy(status = VerifiedGood).isWithdrawable())
+ // fails with VerifiedBad
+ assertFalse(denomination1.copy(status = VerifiedBad).isWithdrawable())
+ // fails when revoked
+ assertFalse(denomination1.copy(isRevoked = true).isWithdrawable())
+ // fails when not started
+ assertFalse(denomination1.copy(stampStart = Timestamp(Timestamp.now().ms + 9999)).isWithdrawable())
+ // fails when expired
+ assertFalse(denomination1.copy(stampExpireWithdraw = Timestamp.now()).isWithdrawable())
+ // fails when almost expired
+ assertFalse(denomination1.copy(stampExpireWithdraw = Timestamp(Timestamp.now().ms + 5000)).isWithdrawable())
+ // succeeds when not quite expired
+ assertTrue(denomination1.copy(stampExpireWithdraw = Timestamp(Timestamp.now().ms + 51000)).isWithdrawable())
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/lib/wallet/exchange/Denominations.kt b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/exchange/Denominations.kt
new file mode 100644
index 0000000..10a2772
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/exchange/Denominations.kt
@@ -0,0 +1,119 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.lib.wallet.exchange
+
+import net.taler.lib.common.Amount
+import net.taler.lib.common.Timestamp
+import net.taler.lib.wallet.exchange.DenominationStatus.Unverified
+import net.taler.lib.wallet.exchange.DenominationStatus.VerifiedGood
+
+object Denominations {
+
+ private val validStart = Timestamp.now()
+ private val validExpireWithdraw =
+ Timestamp(Timestamp.now().ms + 1000L * 60L * 60L * 24L * 365L)
+ val denomination10 = DenominationRecord(
+ denomPub = "020000X0X3G1EBB22XJ4HD6R8545R294TMCMA13ZRW7R101KJENFGTNTZSPGA0XP898FJEVHY4SJTC0SM264K0Y7Q6E24S35JSFZXD6VAJDJX8FCERBTNFV5DZR8V4GV7DAD062CPZBEVGNDEJQCTHVFJP84QWVPYJFNZSS3EJEK3WKJVG5EM3X2JPM1C97AB26VSZXWNYNC2CNJN7KG2001",
+ denomPubHash = "7GB2YKDWKQ3DS2GA9XCVXVPMPJQA9M7Q0DFDHCX5M71J4E2PEHAJK3QF3KTJTWJA33KG0BX6XX0TTRMMZ8CEBM4GSE2N5FSV7GYRGH0",
+ exchangeBaseUrl = "https://example.org",
+ feeDeposit = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeRefresh = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ feeRefund = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeWithdraw = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ isOffered = true,
+ isRevoked = false,
+ masterSig = "N9ADAGY43VTDA000NMW9NFKY8HBTTNPEWP9W1A1ATCAFVA1A9F1MD7HAJ6M4QQ3SVYJXCV1S9Z1ZMKXP9YKD3PFGNK0VQD1ZZ502420",
+ stampExpireDeposit = validExpireWithdraw,
+ stampExpireLegal = validExpireWithdraw,
+ stampExpireWithdraw = validExpireWithdraw,
+ stampStart = validStart,
+ status = Unverified,
+ value = Amount(currency = "TESTKUDOS", fraction = 0, value = 10)
+ )
+ val denomination8 = denomination10.copy(
+ denomPub = "020000X61XTJPQHC9Z3GEF8BD0YTPGH819V6YQDW7MZ74PH9GC2WZGH1YPBHB6F6AJG7C91G19KTSSP9GA64SFZ26T06QK6XEQNREEFV6EMVRCQRYXV3YVA5ANJQRFJVMQG3DQG6Q0ANQWRBZGZSX43MNJQ0NGZY2X1VHJ0351RC83RMPYV1JFSZ2J1JZ2MN5AJF6QMCBBJN0V5TF3EG2001",
+ denomPubHash = "M496WWEBC8KN4MYB73V78F4DCXXMFWR2X33GWDA92X98MC04E120H9NVNKN70J3NP7ZZ91BE7CX65NYHQEG5EQK5Y78E7ZE3YV8E868",
+ feeDeposit = Amount(currency = "TESTKUDOS", fraction = 2000000, value = 0),
+ feeRefresh = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ feeRefund = Amount(currency = "TESTKUDOS", fraction = 4000000, value = 0),
+ feeWithdraw = Amount(currency = "TESTKUDOS", fraction = 5000000, value = 0),
+ masterSig = "GNWDZNQHFECF2SPVWZ5D0KV7YEGJ0J86ND815B512ZSBNKSCSN7PCKE5GJTXV0WZNS3AYTDJYER3W1HXSST4TMBMAR3FY3ETRNRS20G",
+ value = Amount(currency = "TESTKUDOS", fraction = 0, value = 8)
+ )
+ val denomination5 = denomination10.copy(
+ denomPub = "020000XFF9HD3GJXVA9ARQD76BW2G9K65F6CVDPWSRYVE5HY7EVVBK1PK1XX2HE2P3BA3A9MJT9ESY1XNKK7TTF8DRE33C22EHPNNBPPQC1D1MHEE3YJHNF8PG0W6DTE406R6VHCZ0VHEE5HNTEPWMAHJ5J0VVY1ESGAXWE1SGSY82FCQARWV45MNGYZMBN2M55CG3SQXJ8STRPHEM1G2001",
+ denomPubHash = "4HKTYHDDCJXJW7Z215E3TFQWBRYKZA4VS41CZKBE8HH1T97FV4X9WSM62Q7WEZTZX3Y60T4Y8M4R0YYA3PVSEND1MZCJBTD4QZDMCJR",
+ feeDeposit = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeRefresh = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ feeRefund = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeWithdraw = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ masterSig = "0TBJSTZTWMKBT71ET1B7RVFB1VT84E10G3SENPSMPRYTFBH07WSDS5VA9PW5T0EYEABDKGAYDRQ0XA62GYQXZ4SBV2CTHHFY0TFNC18",
+ value = Amount(currency = "TESTKUDOS", fraction = 0, value = 5)
+ )
+ val denomination4 = denomination10.copy(
+ denomPub = "020000Y3JVXB3XTC7JTK3CYEMSXHEX8956DN9B4Z6WB3J1H8D5Y3BVTY8EE5BC5JSRJKM0VAK6BXSHVRGQ6N43BF132DPNSJDG4CD8JA4A856HVCNFSNP0DY21TJYN8GJ36R1T0VKTVH2SRMT4QN1QQZC0VQ5ZV2EJJMCSVYVKV8MZC9NG5K9PGNQKBV64E34H1K9RSFEJE7306ZH07G2001",
+ denomPubHash = "XRKJ5750TW2ZNQYGQ87TESYF2X942RBCG7EKXB8QKMFGB433EK3567SDSE9EFNBYTEH3PHPTK22V8XNSJ090DHYX8EW9BE1Q8JCZW7G",
+ feeDeposit = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ feeRefresh = Amount(currency = "TESTKUDOS", fraction = 4000000, value = 0),
+ feeRefund = Amount(currency = "TESTKUDOS", fraction = 2000000, value = 0),
+ feeWithdraw = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ masterSig = "VFKWHPFNEKD36A0A0TWVTEN98P4TCVBTRMJFJF1E1Q2578J9DXAY53Y7Q2D3BX01WKPM7QBCVF9WBMB4GTXVFP08QPZP086RV9PAP2G",
+ value = Amount(currency = "TESTKUDOS", fraction = 0, value = 4)
+ )
+ val denomination2 = denomination10.copy(
+ denomPub = "020000XB8Y8WVSBM0WBY6NMS0MTNN71PEA6KGKTV5FF27RRR8V1V0GPWYAFDMD2XT6E265QPNH079KM25ZZ97NGFRDVHW0Y7JPWA9C8MJY8DB7APYBRMD9XYA0N1569VFW2NPP4FGQJ865RVE94F35NSG0M4W80CG6WXXWW1ERRM7F2AGRZE9FS049183ANEDFB7QN4H62GDWS7039GG2001",
+ denomPubHash = "CTH34KGWJKQ3C264DQJCRPX97G5SWGWQQ8EMBXVH4DKGTPJ78ZA5CWZB9SDT538Z6S118VQ3RNX3CVC1YXEFN0PXR0D896MDNCGZW3G",
+ feeDeposit = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ feeRefresh = Amount(currency = "TESTKUDOS", fraction = 4000000, value = 0),
+ feeRefund = Amount(currency = "TESTKUDOS", fraction = 2000000, value = 0),
+ feeWithdraw = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ masterSig = "BTZYYEYBZK1EJJ47MQHEY7Q8ARBK4PT0N92TYEEPEZ54P0NTN6FT50AGKCCQCWQ8J74D8MTZFAX3PRDWSH30JPVREAQKGVGEVYXN00G",
+ value = Amount(currency = "TESTKUDOS", fraction = 0, value = 2)
+ )
+ val denomination1 = denomination10.copy(
+ denomPub = "020000XBEVGYETENRYN30AMTT006NPFG5QNHHJXESR9YRZ0J1Y0V00ASMSK7SGKBZJW1GKW05AGNXZDGHAT8RP5M87H2QZTCZHNWW0W4SFWSHWEMRK57DQ3Z7EYQ3XMFX8E2QNZNT9TB4J0MMDP833Y0K7RCQ3X5A584ZBQ9M0T03KTF1QTS2Z7J0BRCEVMK7CCM5WYCAVDPP44Z23HG2001",
+ denomPubHash = "KHRCTNBZHY653V3WZBNMTGGM3MSS471EZ4F6X32HJMN17A47WBBM5WHCRNK8F27KB6Q45YMEN832BKYNKBK1GCRXKDP0XTYC3CYTRWR",
+ feeDeposit = Amount(currency = "TESTKUDOS", fraction = 2000000, value = 0),
+ feeRefresh = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ feeRefund = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeWithdraw = Amount(currency = "TESTKUDOS", fraction = 2000000, value = 0),
+ masterSig = "X029BKSB9PRKE831VDV1X3ND441E0NNQCDJ2S9NKPG7F17R0Y6QCGKWCSHCHE96NW5XS13RK880H90ZSFY7HNJ17F4NSR1EM5FR2M10",
+ value = Amount(currency = "TESTKUDOS", fraction = 0, value = 1)
+ )
+ val denomination0d1 = denomination10.copy(
+ denomPub = "020000YVVE91KXE9S8JV5P4PWZJWBJD39Y6S00FA3CV6RP77A8PTKJWNE3W0W8ZHDR3CSKA17FT56PMC97RWV3RNG753B1WQYXEWNJA76GD5T2PA33BN08CQ07KP4M2K9R6A3N9VD6W8D3DK55W18Y7TBKAHCJBQ3AZ50VHSF1ZPM2XVJ238SKK1379WNHMK4VDJQ35H20QSF3GPWPKG2001",
+ denomPubHash = "C26V15X3BESS1CZCSP4BNSRJ8BK8DSGFHNB0WG1GZ6ZF6FR37WGSVEQ85A61X6Z103P8MY7XGQZ60VAX78V3E5GERWJTJAP0Q5QB7A8",
+ feeDeposit = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeRefresh = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ feeRefund = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeWithdraw = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ masterSig = "JSJXWJXN4532C07CBH4DZZ6YC5S3TPH3H8FG8RMGX0Z647GX2NYEK02NRN3C4GDS9Q0ZT6QE7T8EGYGEF9RZ9FCV65TZWC1ZP83DT1R",
+ value = Amount(currency = "TESTKUDOS", fraction = 10000000, value = 0)
+ )
+ val denomination0d01 = denomination10.copy(
+ denomPub = "020000X67SMNYMCR1HFZW4KEATXGXRA983JE5VW1JE4108XB7Z40BTJC1MV59Y9K4Y35E3MPPF73BJQA3KVT0FBT89R6ZYNZ77TSC5DMFV5E55DT4JB4S9K4C2V7GM8Z8QZ0KMCH0YK4PAX1WSKCEQFNRVKD3VH9WTVQ0CNV7Z1JVRHBKCSZNX62TRQ6JRZ05JEANT3C41SQ6MKSQZKG2001",
+ denomPubHash = "VS0E7ABRZ9NZMTDAHVKQ4QH1S1Q2PYWXFXKF0P84VSHCDBM4S6QK1G1D495TABN4AXVX049P7JESNRRQVHW37BNER4XKZSQT3XA61DG",
+ feeDeposit = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeRefresh = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeRefund = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeWithdraw = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ masterSig = "CF3638K8QBG91D66JZRRGWV8J53A0ZGBYJDMKYK293DNAA2ASM1M346C0YG9V7HWH8E2A7FPVR0HH2QE7DHD9GW0EN19VSER4S5F23R",
+ status = VerifiedGood,
+ value = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0)
+ )
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/lib/wallet/exchange/KeysTest.kt b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/exchange/KeysTest.kt
new file mode 100644
index 0000000..a40c7cd
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/exchange/KeysTest.kt
@@ -0,0 +1,316 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.lib.wallet.exchange
+
+import net.taler.lib.common.Amount
+import net.taler.lib.common.Timestamp
+import net.taler.lib.wallet.getMockHttpClient
+import net.taler.lib.wallet.giveJsonResponse
+import net.taler.lib.wallet.runCoroutine
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class KeysTest {
+
+ private val httpClient = getMockHttpClient()
+
+ @Test
+ fun testFetchKeys() {
+ val expectedKeys = Keys(
+ denoms = listOf(
+ Denomination(
+ value = Amount.fromJSONString("TESTKUDOS:5"),
+ denom_pub = "040000Z9TH9RPTA1BXF6Z89HM7JGXTPD5G8NNBWQWF7RWQGNAATN84QBWME1TGSWZ79WPQ62S2W2VHG2XBH66JDJ0KM8Q2FQ3FGBZQGNJVFNA9F66E6S3P36KTMWMKWDXWM9EX1YHSHQ841AHRR8JVDY96CZ13AJF6JW95K59AE8CSTH5ZS9NVS0102X92GK8JW2QX2S4EE25QNHK6XMXH3944QMXPFS7SFCMV623BM62VNPVX8JM424YXPJ09TXZAH2CF3QM5HDVRSTDRDGVBF6KZVRFM852TMVMYPVGFA9YQF6HWNJ8H5VCQ3Z9WWNMQ3T76X4F1P6W2J266K8B3W9HKW2WJNK3XHRAVC4GCF07TC0ZNAT0EDAAKV429YAXWSK952BPTY98GVP5XZQG2SE0Q5CF3PV04002",
+ fee_withdraw = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_deposit = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_refresh = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_refund = Amount.fromJSONString("TESTKUDOS:0.01"),
+ stamp_start = Timestamp(1582484881000),
+ stamp_expire_withdraw = Timestamp(1677092881000),
+ stamp_expire_legal = Timestamp(1897844881000),
+ stamp_expire_deposit = Timestamp(1740164881000),
+ master_sig = "2SDD44VVBD52XEV0A9R878BC60J51VKK0H5ZS6CPJ7Z738A8V4KPXCF70KFZAY2567400C2GEWNNVXF6PYD7HKX3D2M63WCNPJSE010"
+ ),
+ Denomination(
+ value = Amount.fromJSONString("TESTKUDOS:2"),
+ denom_pub = "040000XV91V0M7H906Y7R371YX2XAK1V5B2TRFD8ZM9WYJ495TP08NCVEDNFXS2KZBJR4808VZ52PNNQSYVQ2T3J7867MZQY1QZ9N8YQWQWCKSYAY8A07E5SYAK0G0KRTCN5VZ7JXE2YCNT7Q3RT9TGAZBSK5V1ZRRK6HX4C1YFKPWWP4TBVJ8DJMS43WKR4CR4S9T02YXVGR6GSDMR7GHBD89JHCEQ8V2K58Y5XVDGRRQYNBG9Q5XWDMV7GKN24JCPCEKSZZP5XYPXYJX2Z2JZ179M9FQV0PEWFJ4DP7AP14XE54FH97YP9398KA31ECVDY7PHMKMZ6E79Z9FSCXH3WSCXMHBWFGWPRG5ZG1P6HR71VKH4F0Y998JNE4G40JH2VSXP3035AR7HAMJGB56CYHH60EWD904002",
+ fee_withdraw = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_deposit = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_refresh = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_refund = Amount.fromJSONString("TESTKUDOS:0.01"),
+ stamp_start = Timestamp(1582484881000),
+ stamp_expire_withdraw = Timestamp(1677092881000),
+ stamp_expire_legal = Timestamp(1897844881000),
+ stamp_expire_deposit = Timestamp(1740164881000),
+ master_sig = "WX1RDTW1W1K7FPFQQ9MCJ8V5CJP8ZX2SGHQG2YS34BY8AMSR3YQ92HE2HT1JMP4W06J63RZ0BR2MKDBX54GV6QKD4R00N9HXN2S283R"
+ ),
+ Denomination(
+ value = Amount.fromJSONString("TESTKUDOS:1"),
+ denom_pub = "040000XCBJE9TDDZATYSDR51D0DKMY5NW8FMJ8YQ1Y4F40SPPTKFMD3FWH38NSQZ1YB621TCFH5RBN5J3SFR5SG4789G27FA90E605GG9AXYTXXPJ9HYPAVAVS6V4XCSC17HKX2M2NSX5D0PPETDGKQD04G498VS36YY4WTB5SYG4SV9MKPVZ5WG2WNP3MA77TFZSHK5HBHZBEW0S1TRKGSCDNBRHYB240M84YM1Y7EJ7BXKJK4GRR1GS16DJ2RA1YEQ1AAXH0GP6RRAEJE8D2JFSH05P3KR1GB97NMX6VD8DCAX45416F888EYQR4M6R820FJVZ6FYV9CCMZ3M10B64N6G4QFNKFNAV2ENPVVG4A3R0AAA6STJ7E5Z05GEKW35SHM14HY9CEGM7D1ZEKHZJYA9P6WH504002",
+ fee_withdraw = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_deposit = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_refresh = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_refund = Amount.fromJSONString("TESTKUDOS:0.01"),
+ stamp_start = Timestamp(1582484881000),
+ stamp_expire_withdraw = Timestamp(1677092881000),
+ stamp_expire_legal = Timestamp(1897844881000),
+ stamp_expire_deposit = Timestamp(1740164881000),
+ master_sig = "65G9FWQPA4YKJEM7D37079D4MY81D47KD1280RG7BRH85XZQ2N13FJPV9N8AEASK9CTGNX1HKX0GTRBJ5C49H4YRY0E4CYVPNH06W18"
+ ),
+ Denomination(
+ value = Amount.fromJSONString("TESTKUDOS:10"),
+ denom_pub = "040000XZDZK4BPPPXR7MYKK2AF4WF95EH3VF8WEX7WDX4HEWXSB5XX10N4V5RHFSK0TSBKNC9CRNVGK3WJ42S3Z9SB4Q3M4DQQ7DKCGKED6WBKENHT8JX51K1VR5JKCMAFBNM6DR5MNRGKFC2MDRQ0Y4BCXHKEMRD65C6JPBKYW9HJH66FGT22WMBV0AV7P60CKR13MQG6FKWW3TZW3XXHVY2VX9MJN6VQFPS6NQGGTNXZV2SK2X5MJAJME7RN9BNZ5ZBTW1CYMVCHBSVGBFPRC68W78PW44VP402VD12KG2AWKPD4DRBAA85HM1DN1KADYQ498QHYGEB3T3HH990HRV8PSNBGYCHB87JTVYMJ4N2PSP2FCX0H6FRTW1FQY05EB7D8BFXM95DNRCHVQSHBZ9RP7NZFA304002",
+ fee_withdraw = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_deposit = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_refresh = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_refund = Amount.fromJSONString("TESTKUDOS:0.01"),
+ stamp_start = Timestamp(1582484881000),
+ stamp_expire_withdraw = Timestamp(1677092881000),
+ stamp_expire_legal = Timestamp(1897844881000),
+ stamp_expire_deposit = Timestamp(1740164881000),
+ master_sig = "M423J7CJPACTPBYCFVR87B44JAJKAB2ME8C263WGHJSA8V8444SX428MVC9NF4GD08CKS9HY0WB4B8SEZ3HJFWKXNSH80RBJXQC822G"
+ ),
+ Denomination(
+ value = Amount.fromJSONString("TESTKUDOS:0.1"),
+ denom_pub = "040000YKYFF6GX979JS10MEZ16BQ7TT6XBTE0TBX6VJ9KSG7K4D91SWJVDETNKQJXAFK9SAB3S31FZFA0Y0X22TKRXKCT7Z4GZCCRJJ12T1A5M4DWRTZDFRD3FE495NXHVPFM96KXMKH1HABTDDFZN0NWQ3NBJ6GNXD40NJ95E955X948JHBDJZWM3TEAK4XFJX8056XFDHVNXSF4VN14RR1WD1J5K7JPS61SKRNF3HT6NZA823PZW2KPV2KVBMMP615A922ZNJGVQDTW5TYWTK5DCBGG1YEKQRYF39NX9X722FZK98BTMHHH6WZFCKBT096G9BKSHSJW3VE8KKPCN8XGWYYPD3158HRKSA28BJQ9XJVVB6FDCGZ154WWGGSGW82BDYDH7ZHJBMS046AG0ND4ZCVR2JQ04002",
+ fee_withdraw = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_deposit = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_refresh = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_refund = Amount.fromJSONString("TESTKUDOS:0.01"),
+ stamp_start = Timestamp(1582484881000),
+ stamp_expire_withdraw = Timestamp(1677092881000),
+ stamp_expire_legal = Timestamp(1897844881000),
+ stamp_expire_deposit = Timestamp(1740164881000),
+ master_sig = "RKZKGR0KYKY0VZ26339DZKV8EZJ2HRRQMFSAJDHBG3YHEQNZFHKM8WPYCH9WHXTWBB10GQN9QJKFDJJF2H6D5FT801GF87G153PTJ18"
+ ),
+ Denomination(
+ value = Amount.fromJSONString("TESTKUDOS:1000"),
+ denom_pub = "040000Y9PBY1HPBDD4KSK9PBA86TRY13JQ4284T4E0G4SEREQ8YM88PZHKW1ACKT1RTWVTBXX83G54NFVYRJQX9PTDXDJ1CXSS42G8NYMW97NA6NNNASV69W1JX39NTS1NVKXPW4WMBASATSNBTXHRT92FFN2NAJFGK876BNN3TPTH57C76ADAQV43VFF7CYAWWNYZAYGQQ1XY1NK34FJD778VFGYCZ1G9J8XPNB92ZKJBZEZKSNBRNH27GM5A736AFSGP7B4JSCGD0F4FMD1PDVB26MM9ZK8C1TDKXQ5DJ09AQQ55P7Q3A133ASPGBH6SCJTJYH8C9A451B0SP4GDX2ZFRSX5FP93PY4VKEB36KCAQ5E2MRZNWFB6T0JK0W7Z7NXP5FW2VQ4PNV7B2NQ3WFMCVRSDSV04002",
+ fee_withdraw = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_deposit = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_refresh = Amount.fromJSONString("TESTKUDOS:0.01"),
+ fee_refund = Amount.fromJSONString("TESTKUDOS:0.01"),
+ stamp_start = Timestamp(1582484881000),
+ stamp_expire_withdraw = Timestamp(1677092881000),
+ stamp_expire_legal = Timestamp(1897844881000),
+ stamp_expire_deposit = Timestamp(1740164881000),
+ master_sig = "FJPQKECRKVQSTB9Y983KDGD65Z1JHQKNSCC6YPMBN3Z4VW0AGC5MQM9BPB0YYD1SCMETPD6QB4X80HWE0ZDGWNZB1KND5TP567T4G3G"
+ )
+ ),
+ master_public_key = "DY95EXAHQ2BKM2WK9YHZHYG1R7PPMMJPY14FNGP662DAKE35AKQG",
+ auditors = emptyList(),
+ list_issue_date = Timestamp(1592161681000),
+ recoup = emptyList(),
+ signkeys = listOf(
+ SigningKey(
+ stamp_start = Timestamp(1592161681000),
+ stamp_expire = Timestamp(1594580881000),
+ stamp_end = Timestamp(1655233681000),
+ key = "0FMRBH8FZYYMSQ2RHTYYGK2BV33JVSW6MTYCV7Y833GVNXFDYK10",
+ master_sig = "368HV41Z4FNDXQ7EP6TNAMBSKP44PJAZW27EPH7XJNVG2A6HZQM7ZPMCB6B30HG50S95YD1K2BAJVPEYMGF2DR7EEY0NFBQZZ1B8P1G"
+ ),
+ SigningKey(
+ stamp_start = Timestamp(1594580881000),
+ stamp_expire = Timestamp(1597000081000),
+ stamp_end = Timestamp(1657652881000),
+ key = "XMNYM62DQW0XDQACCYDMFTM5GY7SZST60NH7XS9GY18H8Q9N7QN0",
+ master_sig = "4HRJN36VVJ87ZC2HZXP7QDSZN30YQE8FCNWZS3RCA1HGNY9Q0JPMVJZ79RDHKS4GYXV29PM27DGCN0VB0BCZFF2FC6FMF3A6ZNKC238"
+ )
+ ),
+ version = "7:0:0"
+ )
+
+ httpClient.giveJsonResponse("https://exchange.test.taler.net/keys") {
+ """{
+ "version": "7:0:0",
+ "master_public_key": "DY95EXAHQ2BKM2WK9YHZHYG1R7PPMMJPY14FNGP662DAKE35AKQG",
+ "reserve_closing_delay": {
+ "d_ms": 2419200000
+ },
+ "signkeys": [
+ {
+ "stamp_start": {
+ "t_ms": 1592161681000
+ },
+ "stamp_expire": {
+ "t_ms": 1594580881000
+ },
+ "stamp_end": {
+ "t_ms": 1655233681000
+ },
+ "master_sig": "368HV41Z4FNDXQ7EP6TNAMBSKP44PJAZW27EPH7XJNVG2A6HZQM7ZPMCB6B30HG50S95YD1K2BAJVPEYMGF2DR7EEY0NFBQZZ1B8P1G",
+ "key": "0FMRBH8FZYYMSQ2RHTYYGK2BV33JVSW6MTYCV7Y833GVNXFDYK10"
+ },
+ {
+ "stamp_start": {
+ "t_ms": 1594580881000
+ },
+ "stamp_expire": {
+ "t_ms": 1597000081000
+ },
+ "stamp_end": {
+ "t_ms": 1657652881000
+ },
+ "master_sig": "4HRJN36VVJ87ZC2HZXP7QDSZN30YQE8FCNWZS3RCA1HGNY9Q0JPMVJZ79RDHKS4GYXV29PM27DGCN0VB0BCZFF2FC6FMF3A6ZNKC238",
+ "key": "XMNYM62DQW0XDQACCYDMFTM5GY7SZST60NH7XS9GY18H8Q9N7QN0"
+ }
+ ],
+ "recoup": [],
+ "denoms": [
+ {
+ "master_sig": "2SDD44VVBD52XEV0A9R878BC60J51VKK0H5ZS6CPJ7Z738A8V4KPXCF70KFZAY2567400C2GEWNNVXF6PYD7HKX3D2M63WCNPJSE010",
+ "stamp_start": {
+ "t_ms": 1582484881000
+ },
+ "stamp_expire_withdraw": {
+ "t_ms": 1677092881000
+ },
+ "stamp_expire_deposit": {
+ "t_ms": 1740164881000
+ },
+ "stamp_expire_legal": {
+ "t_ms": 1897844881000
+ },
+ "denom_pub": "040000Z9TH9RPTA1BXF6Z89HM7JGXTPD5G8NNBWQWF7RWQGNAATN84QBWME1TGSWZ79WPQ62S2W2VHG2XBH66JDJ0KM8Q2FQ3FGBZQGNJVFNA9F66E6S3P36KTMWMKWDXWM9EX1YHSHQ841AHRR8JVDY96CZ13AJF6JW95K59AE8CSTH5ZS9NVS0102X92GK8JW2QX2S4EE25QNHK6XMXH3944QMXPFS7SFCMV623BM62VNPVX8JM424YXPJ09TXZAH2CF3QM5HDVRSTDRDGVBF6KZVRFM852TMVMYPVGFA9YQF6HWNJ8H5VCQ3Z9WWNMQ3T76X4F1P6W2J266K8B3W9HKW2WJNK3XHRAVC4GCF07TC0ZNAT0EDAAKV429YAXWSK952BPTY98GVP5XZQG2SE0Q5CF3PV04002",
+ "value": "TESTKUDOS:5",
+ "fee_withdraw": "TESTKUDOS:0.01",
+ "fee_deposit": "TESTKUDOS:0.01",
+ "fee_refresh": "TESTKUDOS:0.01",
+ "fee_refund": "TESTKUDOS:0.01"
+ },
+ {
+ "master_sig": "WX1RDTW1W1K7FPFQQ9MCJ8V5CJP8ZX2SGHQG2YS34BY8AMSR3YQ92HE2HT1JMP4W06J63RZ0BR2MKDBX54GV6QKD4R00N9HXN2S283R",
+ "stamp_start": {
+ "t_ms": 1582484881000
+ },
+ "stamp_expire_withdraw": {
+ "t_ms": 1677092881000
+ },
+ "stamp_expire_deposit": {
+ "t_ms": 1740164881000
+ },
+ "stamp_expire_legal": {
+ "t_ms": 1897844881000
+ },
+ "denom_pub": "040000XV91V0M7H906Y7R371YX2XAK1V5B2TRFD8ZM9WYJ495TP08NCVEDNFXS2KZBJR4808VZ52PNNQSYVQ2T3J7867MZQY1QZ9N8YQWQWCKSYAY8A07E5SYAK0G0KRTCN5VZ7JXE2YCNT7Q3RT9TGAZBSK5V1ZRRK6HX4C1YFKPWWP4TBVJ8DJMS43WKR4CR4S9T02YXVGR6GSDMR7GHBD89JHCEQ8V2K58Y5XVDGRRQYNBG9Q5XWDMV7GKN24JCPCEKSZZP5XYPXYJX2Z2JZ179M9FQV0PEWFJ4DP7AP14XE54FH97YP9398KA31ECVDY7PHMKMZ6E79Z9FSCXH3WSCXMHBWFGWPRG5ZG1P6HR71VKH4F0Y998JNE4G40JH2VSXP3035AR7HAMJGB56CYHH60EWD904002",
+ "value": "TESTKUDOS:2",
+ "fee_withdraw": "TESTKUDOS:0.01",
+ "fee_deposit": "TESTKUDOS:0.01",
+ "fee_refresh": "TESTKUDOS:0.01",
+ "fee_refund": "TESTKUDOS:0.01"
+ },
+ {
+ "master_sig": "65G9FWQPA4YKJEM7D37079D4MY81D47KD1280RG7BRH85XZQ2N13FJPV9N8AEASK9CTGNX1HKX0GTRBJ5C49H4YRY0E4CYVPNH06W18",
+ "stamp_start": {
+ "t_ms": 1582484881000
+ },
+ "stamp_expire_withdraw": {
+ "t_ms": 1677092881000
+ },
+ "stamp_expire_deposit": {
+ "t_ms": 1740164881000
+ },
+ "stamp_expire_legal": {
+ "t_ms": 1897844881000
+ },
+ "denom_pub": "040000XCBJE9TDDZATYSDR51D0DKMY5NW8FMJ8YQ1Y4F40SPPTKFMD3FWH38NSQZ1YB621TCFH5RBN5J3SFR5SG4789G27FA90E605GG9AXYTXXPJ9HYPAVAVS6V4XCSC17HKX2M2NSX5D0PPETDGKQD04G498VS36YY4WTB5SYG4SV9MKPVZ5WG2WNP3MA77TFZSHK5HBHZBEW0S1TRKGSCDNBRHYB240M84YM1Y7EJ7BXKJK4GRR1GS16DJ2RA1YEQ1AAXH0GP6RRAEJE8D2JFSH05P3KR1GB97NMX6VD8DCAX45416F888EYQR4M6R820FJVZ6FYV9CCMZ3M10B64N6G4QFNKFNAV2ENPVVG4A3R0AAA6STJ7E5Z05GEKW35SHM14HY9CEGM7D1ZEKHZJYA9P6WH504002",
+ "value": "TESTKUDOS:1",
+ "fee_withdraw": "TESTKUDOS:0.01",
+ "fee_deposit": "TESTKUDOS:0.01",
+ "fee_refresh": "TESTKUDOS:0.01",
+ "fee_refund": "TESTKUDOS:0.01"
+ },
+ {
+ "master_sig": "M423J7CJPACTPBYCFVR87B44JAJKAB2ME8C263WGHJSA8V8444SX428MVC9NF4GD08CKS9HY0WB4B8SEZ3HJFWKXNSH80RBJXQC822G",
+ "stamp_start": {
+ "t_ms": 1582484881000
+ },
+ "stamp_expire_withdraw": {
+ "t_ms": 1677092881000
+ },
+ "stamp_expire_deposit": {
+ "t_ms": 1740164881000
+ },
+ "stamp_expire_legal": {
+ "t_ms": 1897844881000
+ },
+ "denom_pub": "040000XZDZK4BPPPXR7MYKK2AF4WF95EH3VF8WEX7WDX4HEWXSB5XX10N4V5RHFSK0TSBKNC9CRNVGK3WJ42S3Z9SB4Q3M4DQQ7DKCGKED6WBKENHT8JX51K1VR5JKCMAFBNM6DR5MNRGKFC2MDRQ0Y4BCXHKEMRD65C6JPBKYW9HJH66FGT22WMBV0AV7P60CKR13MQG6FKWW3TZW3XXHVY2VX9MJN6VQFPS6NQGGTNXZV2SK2X5MJAJME7RN9BNZ5ZBTW1CYMVCHBSVGBFPRC68W78PW44VP402VD12KG2AWKPD4DRBAA85HM1DN1KADYQ498QHYGEB3T3HH990HRV8PSNBGYCHB87JTVYMJ4N2PSP2FCX0H6FRTW1FQY05EB7D8BFXM95DNRCHVQSHBZ9RP7NZFA304002",
+ "value": "TESTKUDOS:10",
+ "fee_withdraw": "TESTKUDOS:0.01",
+ "fee_deposit": "TESTKUDOS:0.01",
+ "fee_refresh": "TESTKUDOS:0.01",
+ "fee_refund": "TESTKUDOS:0.01"
+ },
+ {
+ "master_sig": "RKZKGR0KYKY0VZ26339DZKV8EZJ2HRRQMFSAJDHBG3YHEQNZFHKM8WPYCH9WHXTWBB10GQN9QJKFDJJF2H6D5FT801GF87G153PTJ18",
+ "stamp_start": {
+ "t_ms": 1582484881000
+ },
+ "stamp_expire_withdraw": {
+ "t_ms": 1677092881000
+ },
+ "stamp_expire_deposit": {
+ "t_ms": 1740164881000
+ },
+ "stamp_expire_legal": {
+ "t_ms": 1897844881000
+ },
+ "denom_pub": "040000YKYFF6GX979JS10MEZ16BQ7TT6XBTE0TBX6VJ9KSG7K4D91SWJVDETNKQJXAFK9SAB3S31FZFA0Y0X22TKRXKCT7Z4GZCCRJJ12T1A5M4DWRTZDFRD3FE495NXHVPFM96KXMKH1HABTDDFZN0NWQ3NBJ6GNXD40NJ95E955X948JHBDJZWM3TEAK4XFJX8056XFDHVNXSF4VN14RR1WD1J5K7JPS61SKRNF3HT6NZA823PZW2KPV2KVBMMP615A922ZNJGVQDTW5TYWTK5DCBGG1YEKQRYF39NX9X722FZK98BTMHHH6WZFCKBT096G9BKSHSJW3VE8KKPCN8XGWYYPD3158HRKSA28BJQ9XJVVB6FDCGZ154WWGGSGW82BDYDH7ZHJBMS046AG0ND4ZCVR2JQ04002",
+ "value": "TESTKUDOS:0.1",
+ "fee_withdraw": "TESTKUDOS:0.01",
+ "fee_deposit": "TESTKUDOS:0.01",
+ "fee_refresh": "TESTKUDOS:0.01",
+ "fee_refund": "TESTKUDOS:0.01"
+ },
+ {
+ "master_sig": "FJPQKECRKVQSTB9Y983KDGD65Z1JHQKNSCC6YPMBN3Z4VW0AGC5MQM9BPB0YYD1SCMETPD6QB4X80HWE0ZDGWNZB1KND5TP567T4G3G",
+ "stamp_start": {
+ "t_ms": 1582484881000
+ },
+ "stamp_expire_withdraw": {
+ "t_ms": 1677092881000
+ },
+ "stamp_expire_deposit": {
+ "t_ms": 1740164881000
+ },
+ "stamp_expire_legal": {
+ "t_ms": 1897844881000
+ },
+ "denom_pub": "040000Y9PBY1HPBDD4KSK9PBA86TRY13JQ4284T4E0G4SEREQ8YM88PZHKW1ACKT1RTWVTBXX83G54NFVYRJQX9PTDXDJ1CXSS42G8NYMW97NA6NNNASV69W1JX39NTS1NVKXPW4WMBASATSNBTXHRT92FFN2NAJFGK876BNN3TPTH57C76ADAQV43VFF7CYAWWNYZAYGQQ1XY1NK34FJD778VFGYCZ1G9J8XPNB92ZKJBZEZKSNBRNH27GM5A736AFSGP7B4JSCGD0F4FMD1PDVB26MM9ZK8C1TDKXQ5DJ09AQQ55P7Q3A133ASPGBH6SCJTJYH8C9A451B0SP4GDX2ZFRSX5FP93PY4VKEB36KCAQ5E2MRZNWFB6T0JK0W7Z7NXP5FW2VQ4PNV7B2NQ3WFMCVRSDSV04002",
+ "value": "TESTKUDOS:1000",
+ "fee_withdraw": "TESTKUDOS:0.01",
+ "fee_deposit": "TESTKUDOS:0.01",
+ "fee_refresh": "TESTKUDOS:0.01",
+ "fee_refund": "TESTKUDOS:0.01"
+ }
+ ],
+ "auditors": [],
+ "list_issue_date": {
+ "t_ms": 1592161681000
+ },
+ "eddsa_pub": "0FMRBH8FZYYMSQ2RHTYYGK2BV33JVSW6MTYCV7Y833GVNXFDYK10",
+ "eddsa_sig": "2GB384567SZM9CM7RJT51N04D2ZK7NAHWZRT6BA0FFNXTAB71D4T1WVQTXZEPDM07X1MJ46ZBC189SCM4EG4V8TQJRP2WAZCKPAJJ2R"
+ }""".trimIndent()
+ }
+ runCoroutine {
+ val keys = Keys.fetch(httpClient, "https://exchange.test.taler.net/")
+ assertEquals(expectedKeys, keys)
+ }
+ }
+
+
+} \ No newline at end of file
diff --git a/wallet/src/commonTest/kotlin/net/taler/lib/wallet/exchange/UpdateTest.kt b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/exchange/UpdateTest.kt
new file mode 100644
index 0000000..15e6d80
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/exchange/UpdateTest.kt
@@ -0,0 +1,37 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.lib.wallet.exchange
+
+import net.taler.lib.wallet.runCoroutine
+import kotlin.test.Ignore
+import kotlin.test.Test
+import kotlin.test.assertTrue
+
+class UpdateTest {
+
+ private val exchange = Exchange()
+
+ @Ignore // live test that requires internet connectivity
+ @Test
+ fun testLiveUpdate() {
+ runCoroutine {
+ val record = exchange.updateFromUrl("http://exchange.test.taler.net/")
+ assertTrue(record.addComplete)
+ }
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/lib/wallet/exchange/WireTest.kt b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/exchange/WireTest.kt
new file mode 100644
index 0000000..16671a9
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/exchange/WireTest.kt
@@ -0,0 +1,175 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.lib.wallet.exchange
+
+import net.taler.lib.common.Amount
+import net.taler.lib.common.Timestamp
+import net.taler.lib.wallet.getMockHttpClient
+import net.taler.lib.wallet.giveJsonResponse
+import net.taler.lib.wallet.runCoroutine
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class WireTest {
+
+ private val httpClient = getMockHttpClient()
+
+ @Test
+ fun testFetchWire() {
+ val expectedWire = Wire(
+ accounts = listOf(
+ AccountInfo(
+ paytoUri = "payto://x-taler-bank/bank.test.taler.net/Exchange",
+ masterSig = "5DMYMQCEFWG7B21RAX8XQ585V689K8DSSR065F04E2JK6G9AF1WM8EVDCHHBMVWRY3P02EWEE4M6YVKPY6B43H2CPCWHDP13RM1WR10"
+ )
+ ),
+ fees = mapOf(
+ "x-taler-bank" to listOf(
+ WireFee(
+ wireFee = Amount.fromJSONString("TESTKUDOS:0.04"),
+ closingFee = Amount.fromJSONString("TESTKUDOS:0.01"),
+ startStamp = Timestamp(1577833200000),
+ endStamp = Timestamp(1609455600000),
+ signature = "9DS6TXPTM8ZBBTJS9VCRSD9FVS56ZY9EVWCF4HDA3Y2DNWSVMGS7XXPWE295EZ3E98KVV1SWDJ11CP0A0VDSRDZTM6RD2RPRG19ZA2G"
+ ),
+ WireFee(
+ wireFee = Amount.fromJSONString("TESTKUDOS:0.04"),
+ closingFee = Amount.fromJSONString("TESTKUDOS:0.01"),
+ startStamp = Timestamp(1609455600000),
+ endStamp = Timestamp(1640991600000),
+ signature = "81852REBNR3ZRQHKQ2FPT6CPACED0MA0CW4V9CPDS3NP2JX6X8BE5YE5W1AKR9XPASEXSKQH6FEXHP7VJB64XWA7FDGH5DKCD3Q700G"
+ ),
+ WireFee(
+ wireFee = Amount.fromJSONString("TESTKUDOS:0.05"),
+ closingFee = Amount.fromJSONString("TESTKUDOS:0.01"),
+ startStamp = Timestamp(1640991600000),
+ endStamp = Timestamp(1672527600000),
+ signature = "REYMSGH4QBNF339Q8TD5VJMMY6BV7KFTC1Y69YD69Y9E8Z5HXGNAKCQKT490MHBSF48894YADT1ATGDMSRZAQJJFVXF6HX9JEYDT61G"
+ ),
+ WireFee(
+ wireFee = Amount.fromJSONString("TESTKUDOS:0.06"),
+ closingFee = Amount.fromJSONString("TESTKUDOS:0.01"),
+ startStamp = Timestamp(1672527600000),
+ endStamp = Timestamp(1704063600000),
+ signature = "BXB47D936XT7XDHGA3VA3461CY1GMQWFPVMSBY01N5SN6PBCGYRS8HSY19FJ0P5HVX3TGS9TAHY9X7RP4BQHPM4DMMS30TJ0EKG5A3G"
+ ),
+ WireFee(
+ wireFee = Amount.fromJSONString("TESTKUDOS:0.07"),
+ closingFee = Amount.fromJSONString("TESTKUDOS:0.01"),
+ startStamp = Timestamp(1704063600000),
+ endStamp = Timestamp(1735686000000),
+ signature = "RFF1KV54BH9TJ8KBE8YEY8DM0R468PZYGW82G16P97EDHNN3XZVN4KK5E9CBZQ730WPJT0RKR3TTYPBWGTR0YQ064XZZDHJHHZN1418"
+ ),
+ WireFee(
+ wireFee = Amount.fromJSONString("TESTKUDOS:0.08"),
+ closingFee = Amount.fromJSONString("TESTKUDOS:0.01"),
+ startStamp = Timestamp(1735686000000),
+ endStamp = Timestamp(1767222000000),
+ signature = "Q89VKJ54MF3DVG0NKK4N6VB96NCT0PRSTNBJ0SSB42SQTHB10JC68XJSDM6PRBBPEJ8CHDE9VVRZWW20VFSZFDTRA332JKDSBBFWY1G"
+ )
+ )
+ )
+ )
+ httpClient.giveJsonResponse("https://exchange.test.taler.net/wire") {
+ """{
+ "accounts": [
+ {
+ "payto_uri": "payto://x-taler-bank/bank.test.taler.net/Exchange",
+ "master_sig": "5DMYMQCEFWG7B21RAX8XQ585V689K8DSSR065F04E2JK6G9AF1WM8EVDCHHBMVWRY3P02EWEE4M6YVKPY6B43H2CPCWHDP13RM1WR10"
+ }
+ ],
+ "fees": {
+ "x-taler-bank": [
+ {
+ "wire_fee": "TESTKUDOS:0.04",
+ "closing_fee": "TESTKUDOS:0.01",
+ "start_date": {
+ "t_ms": 1577833200000
+ },
+ "end_date": {
+ "t_ms": 1609455600000
+ },
+ "sig": "9DS6TXPTM8ZBBTJS9VCRSD9FVS56ZY9EVWCF4HDA3Y2DNWSVMGS7XXPWE295EZ3E98KVV1SWDJ11CP0A0VDSRDZTM6RD2RPRG19ZA2G"
+ },
+ {
+ "wire_fee": "TESTKUDOS:0.04",
+ "closing_fee": "TESTKUDOS:0.01",
+ "start_date": {
+ "t_ms": 1609455600000
+ },
+ "end_date": {
+ "t_ms": 1640991600000
+ },
+ "sig": "81852REBNR3ZRQHKQ2FPT6CPACED0MA0CW4V9CPDS3NP2JX6X8BE5YE5W1AKR9XPASEXSKQH6FEXHP7VJB64XWA7FDGH5DKCD3Q700G"
+ },
+ {
+ "wire_fee": "TESTKUDOS:0.05",
+ "closing_fee": "TESTKUDOS:0.01",
+ "start_date": {
+ "t_ms": 1640991600000
+ },
+ "end_date": {
+ "t_ms": 1672527600000
+ },
+ "sig": "REYMSGH4QBNF339Q8TD5VJMMY6BV7KFTC1Y69YD69Y9E8Z5HXGNAKCQKT490MHBSF48894YADT1ATGDMSRZAQJJFVXF6HX9JEYDT61G"
+ },
+ {
+ "wire_fee": "TESTKUDOS:0.06",
+ "closing_fee": "TESTKUDOS:0.01",
+ "start_date": {
+ "t_ms": 1672527600000
+ },
+ "end_date": {
+ "t_ms": 1704063600000
+ },
+ "sig": "BXB47D936XT7XDHGA3VA3461CY1GMQWFPVMSBY01N5SN6PBCGYRS8HSY19FJ0P5HVX3TGS9TAHY9X7RP4BQHPM4DMMS30TJ0EKG5A3G"
+ },
+ {
+ "wire_fee": "TESTKUDOS:0.07",
+ "closing_fee": "TESTKUDOS:0.01",
+ "start_date": {
+ "t_ms": 1704063600000
+ },
+ "end_date": {
+ "t_ms": 1735686000000
+ },
+ "sig": "RFF1KV54BH9TJ8KBE8YEY8DM0R468PZYGW82G16P97EDHNN3XZVN4KK5E9CBZQ730WPJT0RKR3TTYPBWGTR0YQ064XZZDHJHHZN1418"
+ },
+ {
+ "wire_fee": "TESTKUDOS:0.08",
+ "closing_fee": "TESTKUDOS:0.01",
+ "start_date": {
+ "t_ms": 1735686000000
+ },
+ "end_date": {
+ "t_ms": 1767222000000
+ },
+ "sig": "Q89VKJ54MF3DVG0NKK4N6VB96NCT0PRSTNBJ0SSB42SQTHB10JC68XJSDM6PRBBPEJ8CHDE9VVRZWW20VFSZFDTRA332JKDSBBFWY1G"
+ }
+ ]
+ },
+ "master_public_key": "DY95EXAHQ2BKM2WK9YHZHYG1R7PPMMJPY14FNGP662DAKE35AKQG"
+ }""".trimIndent()
+ }
+
+ runCoroutine {
+ val wire = Wire.fetch(httpClient, "https://exchange.test.taler.net/")
+ assertEquals(expectedWire, wire)
+ }
+ }
+
+}
diff --git a/wallet/src/commonTest/kotlin/net/taler/lib/wallet/operations/WithdrawTest.kt b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/operations/WithdrawTest.kt
new file mode 100644
index 0000000..634a9dd
--- /dev/null
+++ b/wallet/src/commonTest/kotlin/net/taler/lib/wallet/operations/WithdrawTest.kt
@@ -0,0 +1,140 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.lib.wallet.operations
+
+import net.taler.lib.common.Amount
+import net.taler.lib.wallet.exchange.DenominationSelectionInfo
+import net.taler.lib.wallet.exchange.Denominations.denomination0d01
+import net.taler.lib.wallet.exchange.Denominations.denomination0d1
+import net.taler.lib.wallet.exchange.Denominations.denomination1
+import net.taler.lib.wallet.exchange.Denominations.denomination10
+import net.taler.lib.wallet.exchange.Denominations.denomination2
+import net.taler.lib.wallet.exchange.Denominations.denomination4
+import net.taler.lib.wallet.exchange.Denominations.denomination5
+import net.taler.lib.wallet.exchange.Denominations.denomination8
+import net.taler.lib.wallet.exchange.SelectedDenomination
+import net.taler.lib.wallet.getMockHttpClient
+import net.taler.lib.wallet.giveJsonResponse
+import net.taler.lib.wallet.operations.Withdraw.BankDetails
+import net.taler.lib.wallet.runCoroutine
+import kotlin.test.Ignore
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+internal class WithdrawTest {
+
+ private val httpClient = getMockHttpClient()
+ private val withdraw = Withdraw(httpClient)
+
+ @Ignore // live test that requires internet connectivity and a working exchange
+ @Test
+ fun testLiveUpdate() {
+ runCoroutine {
+ val withdraw = Withdraw() // use own instance without mocked HTTP client
+ val url = "http://exchange.test.taler.net/"
+ val amount = Amount("TESTKUDOS", 5, 0)
+ val details = withdraw.getWithdrawalDetails(url, amount)
+ assertEquals(url, details.exchange.baseUrl)
+ }
+ }
+
+ @Test
+ fun getBankWithdrawalInfo() {
+ val bankDetails = BankDetails(
+ amount = Amount.fromJSONString("TESTKUDOS:5"),
+ confirmTransferUrl = "https://bank.test.taler.net/confirm-withdrawal/9b51c1dd-db41-4b5f-97d9-1071d5dd8091",
+ extractedStatusUrl = "https://bank.test.taler.net/api/withdraw-operation/9b51c1dd-db41-4b5f-97d9-1071d5dd8091",
+ selectionDone = false,
+ senderPaytoUri = "payto://x-taler-bank/bank.test.taler.net/test",
+ suggestedExchange = "https://exchange.test.taler.net/",
+ transferDone = false,
+ wireTypes = listOf("x-taler-bank")
+ )
+ httpClient.giveJsonResponse(bankDetails.extractedStatusUrl) {
+ """{
+ "selection_done": ${bankDetails.selectionDone},
+ "transfer_done": ${bankDetails.transferDone},
+ "amount": "${bankDetails.amount.toJSONString()}",
+ "wire_types": ["${bankDetails.wireTypes[0]}"],
+ "sender_wire": "${bankDetails.senderPaytoUri}",
+ "suggested_exchange": "${bankDetails.suggestedExchange}",
+ "confirm_transfer_url": "${bankDetails.confirmTransferUrl}"
+ }""".trimIndent()
+ }
+ runCoroutine {
+ val details =
+ withdraw.getBankInfo("taler://withdraw/bank.test.taler.net/9b51c1dd-db41-4b5f-97d9-1071d5dd8091")
+ assertEquals(bankDetails, details)
+ }
+ }
+
+ @Test
+ fun testGetDenominationSelection() {
+ val allDenominations = listOf(
+ denomination0d1,
+ denomination0d01, // unsorted list to test sort as well
+ denomination10,
+ denomination8,
+ denomination5,
+ denomination4,
+ denomination2,
+ denomination1
+ )
+ // select denominations for 10 TESTKUDOS
+ val value10 = Amount(currency = "TESTKUDOS", value = 10, fraction = 0)
+ val expectedSelectionInfo10 = DenominationSelectionInfo(
+ totalCoinValue = Amount(currency = "TESTKUDOS", value = 9, fraction = 82000000),
+ totalWithdrawCost = Amount(currency = "TESTKUDOS", value = 9, fraction = 99000000),
+ selectedDenominations = listOf(
+ SelectedDenomination(1, denomination8),
+ SelectedDenomination(1, denomination1),
+ SelectedDenomination(8, denomination0d1),
+ SelectedDenomination(2, denomination0d01)
+ )
+ )
+ assertEquals(expectedSelectionInfo10, withdraw.getDenominationSelection(value10, allDenominations))
+
+ // select denominations for 5.5 TESTKUDOS
+ val value5d5 = Amount(currency = "TESTKUDOS", value = 5, fraction = 50000000)
+ val expectedSelectionInfo5d5 = DenominationSelectionInfo(
+ totalCoinValue = Amount(currency = "TESTKUDOS", value = 5, fraction = 42000000),
+ totalWithdrawCost = Amount(currency = "TESTKUDOS", value = 5, fraction = 49000000),
+ selectedDenominations = listOf(
+ SelectedDenomination(1, denomination5),
+ SelectedDenomination(4, denomination0d1),
+ SelectedDenomination(2, denomination0d01)
+ )
+ )
+ assertEquals(expectedSelectionInfo5d5, withdraw.getDenominationSelection(value5d5, allDenominations))
+
+ // select denominations for 23.42 TESTKUDOS
+ val value23d42 = Amount(currency = "TESTKUDOS", value = 23, fraction = 42000000)
+ val expectedSelectionInfo23d42 = DenominationSelectionInfo(
+ totalCoinValue = Amount(currency = "TESTKUDOS", value = 23, fraction = 31000000),
+ totalWithdrawCost = Amount(currency = "TESTKUDOS", value = 23, fraction = 42000000),
+ selectedDenominations = listOf(
+ SelectedDenomination(2, denomination10),
+ SelectedDenomination(1, denomination2),
+ SelectedDenomination(1, denomination1),
+ SelectedDenomination(3, denomination0d1),
+ SelectedDenomination(1, denomination0d01)
+ )
+ )
+ assertEquals(expectedSelectionInfo23d42, withdraw.getDenominationSelection(value23d42, allDenominations))
+ }
+
+}