gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-kotlin] 02/02: Add initial code for updating an exchange


From: gnunet
Subject: [taler-wallet-kotlin] 02/02: Add initial code for updating an exchange
Date: Thu, 09 Jul 2020 21:10:20 +0200

This is an automated email from the git hooks/post-receive script.

torsten-grote pushed a commit to branch master
in repository wallet-kotlin.

commit 4379d554c72d80328dff1e873d7cc007a1a5881a
Author: Torsten Grote <t@grobox.de>
AuthorDate: Wed Jul 8 12:00:53 2020 -0300

    Add initial code for updating an exchange
    
    This introduces a common Database interface that gets currently only
    implemented by a fake in-memmory database.
---
 build.gradle                                       |  23 +-
 settings.gradle                                    |   1 +
 .../kotlin/net/taler/wallet/kotlin/Db.kt}          |   9 +-
 .../net/taler/wallet/kotlin/TestUtilsAndroid.kt}   |   2 +
 .../net/taler/wallet/kotlin/crypto/RefreshTest.kt  |   4 +-
 .../kotlin/net/taler/wallet/kotlin/Db.kt           |  59 ++++
 .../kotlin/net/taler/wallet/kotlin/Timestamp.kt    |   8 +-
 .../kotlin/net/taler/wallet/kotlin/Types.kt        |  89 ------
 .../kotlin/net/taler/wallet/kotlin/Utils.kt}       |  19 +-
 .../net/taler/wallet/kotlin/crypto/Refresh.kt      |   2 +-
 .../net/taler/wallet/kotlin/crypto/Signature.kt    |   2 +-
 .../kotlin/{Timestamp.kt => exchange/Auditor.kt}   |  53 ++--
 .../net/taler/wallet/kotlin/exchange/Exchange.kt   | 142 +++++++++
 .../{Types.kt => exchange/ExchangeRecord.kt}       | 206 +++++++------
 .../net/taler/wallet/kotlin/exchange/Keys.kt       | 177 ++++++++++++
 .../net/taler/wallet/kotlin/operations/Withdraw.kt |   7 +-
 .../kotlin/net/taler/wallet/kotlin/DbTest.kt       |  67 +++++
 .../kotlin/net/taler/wallet/kotlin/TestUtils.kt    |  14 +-
 .../taler/wallet/kotlin/crypto/SignatureTest.kt    |   6 +-
 .../taler/wallet/kotlin/exchange/ExchangeTest.kt   | 317 +++++++++++++++++++++
 .../kotlin/net/taler/wallet/kotlin/Db.kt}          |   9 +-
 .../kotlin/{runCoroutine.kt => TestUtils.kt}       |   2 +
 .../kotlin/net/taler/wallet/kotlin/Db.kt}          |   9 +-
 .../kotlin/net/taler/wallet/kotlin/TestUtils.kt}   |   2 +
 24 files changed, 989 insertions(+), 240 deletions(-)

diff --git a/build.gradle b/build.gradle
index 342690d..c173b78 100644
--- a/build.gradle
+++ b/build.gradle
@@ -14,13 +14,19 @@
  * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
+buildscript {
+    ext.kotlin_version = '1.3.72'
+}
+
 plugins {
-    id 'org.jetbrains.kotlin.multiplatform' version '1.3.72'
-    id 'org.jetbrains.kotlin.plugin.serialization' version '1.3.72'
+    id 'org.jetbrains.kotlin.multiplatform' version "$kotlin_version"
+    id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version"
 }
+
 repositories {
     mavenCentral()
     jcenter()
+    maven { url "https://dl.bintray.com/terl/lazysodium-maven"; }
 }
 group 'net.taler'
 version '0.0.1'
@@ -97,12 +103,11 @@ kotlin {
                 implementation "io.ktor:ktor-client-js:$ktor_version"
                 implementation 
"io.ktor:ktor-client-serialization-js:$ktor_version"
                 // bug: https://github.com/ktorio/ktor/issues/1822
-                implementation npm('abort-controller', '3.0.0') // work-around 
for above bug
-                implementation npm('utf-8-validate', '5.0.2') // work-around 
for above bug
-                implementation npm('text-encoding', '0.7.0') // work-around 
for above bug
-                implementation npm('node-fetch', '2.6.0') // work-around for 
above bug
-                implementation npm('bufferutil', '4.0.1') // work-around for 
above bug
-                implementation npm('fs', '*') // work-around for above bug
+                api npm("text-encoding", '0.7.0') // work-around for above bug
+                api npm("bufferutil", '4.0.1') // work-around for above bug
+                api npm("utf-8-validate", '5.0.2') // work-around for above bug
+                api npm("abort-controller", '3.0.0') // work-around for above 
bug
+                api npm("fs", '*') // work-around for above bug
 
                 implementation npm('tweetnacl', '1.0.3')
                 implementation npm('ed2curve', '0.3.0')
@@ -119,7 +124,7 @@ kotlin {
             dependencies {
                 implementation 
"org.jetbrains.kotlinx:kotlinx-coroutines-core-native:$coroutines_version"
                 implementation "io.ktor:ktor-client-curl:$ktor_version"
-                
implementation("io.ktor:ktor-client-serialization-native:$ktor_version")
+                implementation 
"io.ktor:ktor-client-serialization-native:$ktor_version"
             }
         }
         linuxTest {
diff --git a/settings.gradle b/settings.gradle
index 8a34848..bf5e773 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,2 +1,3 @@
 rootProject.name = 'wallet-kotlin'
 
+enableFeaturePreview("GRADLE_METADATA")
diff --git a/src/linuxTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt 
b/src/androidMain/kotlin/net/taler/wallet/kotlin/Db.kt
similarity index 80%
copy from src/linuxTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt
copy to src/androidMain/kotlin/net/taler/wallet/kotlin/Db.kt
index 524da15..45cbfc3 100644
--- a/src/linuxTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt
+++ b/src/androidMain/kotlin/net/taler/wallet/kotlin/Db.kt
@@ -16,7 +16,8 @@
 
 package net.taler.wallet.kotlin
 
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.runBlocking
-
-actual fun runCoroutine(block: suspend (scope : CoroutineScope) -> Unit) = 
runBlocking { block(this) }
+internal actual class DbFactory {
+    actual fun openDb(): Db {
+        return FakeDb()
+    }
+}
diff --git a/src/linuxTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt 
b/src/androidTest/kotlin/net/taler/wallet/kotlin/TestUtilsAndroid.kt
similarity index 92%
copy from src/linuxTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt
copy to src/androidTest/kotlin/net/taler/wallet/kotlin/TestUtilsAndroid.kt
index 524da15..a362874 100644
--- a/src/linuxTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt
+++ b/src/androidTest/kotlin/net/taler/wallet/kotlin/TestUtilsAndroid.kt
@@ -20,3 +20,5 @@ import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.runBlocking
 
 actual fun runCoroutine(block: suspend (scope : CoroutineScope) -> Unit) = 
runBlocking { block(this) }
+
+actual fun getPlatformTarget(): PlatformTarget = PlatformTarget.ANDROID
diff --git 
a/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/RefreshTest.kt 
b/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/RefreshTest.kt
index 37cf10f..4ed903e 100644
--- a/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/RefreshTest.kt
+++ b/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/RefreshTest.kt
@@ -21,13 +21,13 @@ import net.taler.wallet.kotlin.Base32Crockford
 import net.taler.wallet.kotlin.CoinRecord
 import net.taler.wallet.kotlin.CoinSourceType.WITHDRAW
 import net.taler.wallet.kotlin.CoinStatus.DORMANT
-import net.taler.wallet.kotlin.DenominationRecord
-import net.taler.wallet.kotlin.DenominationStatus
 import net.taler.wallet.kotlin.Timestamp
 import net.taler.wallet.kotlin.crypto.Refresh.DenominationSelectionInfo
 import net.taler.wallet.kotlin.crypto.Refresh.RefreshPlanchetRecord
 import net.taler.wallet.kotlin.crypto.Refresh.RefreshSessionRecord
 import net.taler.wallet.kotlin.crypto.Refresh.SelectedDenomination
+import net.taler.wallet.kotlin.exchange.DenominationRecord
+import net.taler.wallet.kotlin.exchange.DenominationStatus
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertTrue
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/Db.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/Db.kt
new file mode 100644
index 0000000..303c526
--- /dev/null
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/Db.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.wallet.kotlin
+
+import net.taler.wallet.kotlin.exchange.DenominationRecord
+import net.taler.wallet.kotlin.exchange.ExchangeRecord
+
+internal interface Db {
+    suspend fun put(exchange: ExchangeRecord)
+    suspend fun listExchanges(): List<ExchangeRecord>
+    suspend fun getExchangeByBaseUrl(baseUrl: String): ExchangeRecord?
+    suspend fun deleteExchangeByBaseUrl(baseUrl: String)
+    suspend fun put(denomination: DenominationRecord)
+}
+
+internal expect class DbFactory() {
+    fun openDb(): Db
+}
+
+internal class FakeDb: Db {
+
+    private val exchanges = HashMap<String, ExchangeRecord>()
+    private val denominations = HashMap<String, DenominationRecord>()
+
+    override suspend fun put(exchange: ExchangeRecord) {
+        exchanges[exchange.baseUrl] = exchange
+    }
+
+    override suspend fun listExchanges(): List<ExchangeRecord> {
+        return exchanges.values.toList()
+    }
+
+    override suspend fun getExchangeByBaseUrl(baseUrl: String): 
ExchangeRecord? {
+        return exchanges[baseUrl]
+    }
+
+    override suspend fun deleteExchangeByBaseUrl(baseUrl: String) {
+        exchanges.remove(baseUrl)
+    }
+
+    override suspend fun put(denomination: DenominationRecord) {
+        denominations[denomination.exchangeBaseUrl] = denomination
+    }
+
+}
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/Timestamp.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/Timestamp.kt
index d0260cf..b5c850f 100644
--- a/src/commonMain/kotlin/net/taler/wallet/kotlin/Timestamp.kt
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/Timestamp.kt
@@ -17,11 +17,13 @@
 package net.taler.wallet.kotlin
 
 import com.soywiz.klock.DateTime
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
 import net.taler.wallet.kotlin.crypto.CryptoImpl.Companion.toByteArray
 
-
-class Timestamp(
-    // @JsonProperty("t_ms")
+@Serializable
+data class Timestamp(
+    @SerialName("t_ms")
     val ms: Long
 ) {
 
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/Types.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/Types.kt
index 4702a19..2365795 100644
--- a/src/commonMain/kotlin/net/taler/wallet/kotlin/Types.kt
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/Types.kt
@@ -39,95 +39,6 @@ data class WireFee(
     val signature: String
 )
 
-data class DenominationRecord(
-    /**
-     * Value of one coin of the denomination.
-     */
-    val value: Amount,
-    /**
-     * The denomination public key.
-     */
-    val denomPub: String,
-    /**
-     * Hash of the denomination public key.
-     * Stored in the database for faster lookups.
-     */
-    val denomPubHash: String,
-    /**
-     * Fee for withdrawing.
-     */
-    val feeWithdraw: Amount,
-    /**
-     * Fee for depositing.
-     */
-    val feeDeposit: Amount,
-    /**
-     * Fee for refreshing.
-     */
-    val feeRefresh: Amount,
-    /**
-     * Fee for refunding.
-     */
-    val feeRefund: Amount,
-    /**
-     * Validity start date of the denomination.
-     */
-    val stampStart: Timestamp,
-    /**
-     * Date after which the currency can't be withdrawn anymore.
-     */
-    val stampExpireWithdraw: Timestamp,
-    /**
-     * Date after the denomination officially doesn't exist anymore.
-     */
-    val stampExpireLegal: Timestamp,
-    /**
-     * Data after which coins of this denomination can't be deposited anymore.
-     */
-    val stampExpireDeposit: Timestamp,
-    /**
-     * Signature by the exchange's master key over the denomination
-     * information.
-     */
-    val masterSig: String,
-    /**
-     * Did we verify the signature on the denomination?
-     */
-    val status: DenominationStatus,
-    /**
-     * Was this denomination still offered by the exchange the last time
-     * we checked?
-     * Only false when the exchange redacts a previously published 
denomination.
-     */
-    val isOffered: Boolean,
-    /**
-     * Did the exchange revoke the denomination?
-     * When this field is set to true in the database, the same transaction
-     * should also mark all affected coins as revoked.
-     */
-    val isRevoked: Boolean,
-    /**
-     * Base URL of the exchange.
-     */
-    val exchangeBaseUrl: String
-)
-
-enum class DenominationStatus {
-    /**
-     * Verification was delayed.
-     */
-    Unverified,
-
-    /**
-     * Verified as valid.
-     */
-    VerifiedGood,
-
-    /**
-     * Verified as invalid.
-     */
-    VerifiedBad
-}
 
 class CoinRecord(
     /**
diff --git a/src/linuxTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/Utils.kt
similarity index 57%
copy from src/linuxTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt
copy to src/commonMain/kotlin/net/taler/wallet/kotlin/Utils.kt
index 524da15..aa3fd91 100644
--- a/src/linuxTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/Utils.kt
@@ -16,7 +16,20 @@
 
 package net.taler.wallet.kotlin
 
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.runBlocking
+import io.ktor.client.HttpClient
+import io.ktor.client.features.json.JsonFeature
+import io.ktor.client.features.json.serializer.KotlinxSerializer
+import kotlinx.serialization.UnstableDefault
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonConfiguration
 
-actual fun runCoroutine(block: suspend (scope : CoroutineScope) -> Unit) = 
runBlocking { block(this) }
+fun getDefaultHttpClient(): HttpClient = HttpClient {
+    install(JsonFeature) {
+        serializer = KotlinxSerializer(Json(getJsonConfiguration()))
+    }
+}
+
+@OptIn(UnstableDefault::class)
+internal fun getJsonConfiguration() = JsonConfiguration(
+    ignoreUnknownKeys = true
+)
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Refresh.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Refresh.kt
index 4d6377a..602a1ab 100644
--- a/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Refresh.kt
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Refresh.kt
@@ -19,11 +19,11 @@ package net.taler.wallet.kotlin.crypto
 import net.taler.wallet.kotlin.Amount
 import net.taler.wallet.kotlin.Base32Crockford
 import net.taler.wallet.kotlin.CoinRecord
-import net.taler.wallet.kotlin.DenominationRecord
 import net.taler.wallet.kotlin.Timestamp
 import net.taler.wallet.kotlin.crypto.Signature.Companion.WALLET_COIN_LINK
 import net.taler.wallet.kotlin.crypto.Signature.Companion.WALLET_COIN_MELT
 import net.taler.wallet.kotlin.crypto.Signature.PurposeBuilder
+import net.taler.wallet.kotlin.exchange.DenominationRecord
 
 internal class Refresh(private val crypto: Crypto) {
 
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Signature.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Signature.kt
index 5e4a65e..ee1dff4 100644
--- a/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Signature.kt
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Signature.kt
@@ -17,9 +17,9 @@
 package net.taler.wallet.kotlin.crypto
 
 import net.taler.wallet.kotlin.Base32Crockford
-import net.taler.wallet.kotlin.DenominationRecord
 import net.taler.wallet.kotlin.WireFee
 import net.taler.wallet.kotlin.crypto.CryptoImpl.Companion.toByteArray
+import net.taler.wallet.kotlin.exchange.DenominationRecord
 
 internal class Signature(private val crypto: Crypto) {
 
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/Timestamp.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Auditor.kt
similarity index 50%
copy from src/commonMain/kotlin/net/taler/wallet/kotlin/Timestamp.kt
copy to src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Auditor.kt
index d0260cf..4df0bdf 100644
--- a/src/commonMain/kotlin/net/taler/wallet/kotlin/Timestamp.kt
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Auditor.kt
@@ -14,32 +14,43 @@
  * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-package net.taler.wallet.kotlin
+package net.taler.wallet.kotlin.exchange
 
-import com.soywiz.klock.DateTime
-import net.taler.wallet.kotlin.crypto.CryptoImpl.Companion.toByteArray
+import kotlinx.serialization.Serializable
 
+/**
+ * Auditor information as given by the exchange in /keys.
+ */
+@Serializable
+data class Auditor(
+    /**
+     * Auditor's public key.
+     */
+    val auditor_pub: String,
 
-class Timestamp(
-    // @JsonProperty("t_ms")
-    val ms: Long
-) {
-
-    companion object {
-        const val NEVER: Long = -1
-        fun now(): Timestamp = Timestamp(DateTime.now().unixMillisLong)
-    }
+    /**
+     * Base URL of the auditor.
+     */
+    val auditor_url: String,
 
     /**
-     * Returns a copy of this [Timestamp] rounded to seconds.
+     * List of signatures for denominations by the auditor.
      */
-    fun truncateSeconds(): Timestamp {
-        if (ms == NEVER) return Timestamp(ms)
-        return Timestamp((ms / 1000L) * 1000L)
-    }
+    val denomination_keys: List<AuditorDenomSig>
+)
 
-    fun roundedToByteArray(): ByteArray = ByteArray(8).apply {
-        (truncateSeconds().ms * 1000L).toByteArray().copyInto(this)
-    }
+/**
+ * Signature by the auditor that a particular denomination key is audited.
+ */
+@Serializable
+data class AuditorDenomSig(
+    /**
+     * Denomination public key's hash.
+     */
+    val denom_pub_h: String,
 
-}
+    /**
+     * The signature.
+     */
+    val auditor_sig: String
+)
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Exchange.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Exchange.kt
new file mode 100644
index 0000000..4124c30
--- /dev/null
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Exchange.kt
@@ -0,0 +1,142 @@
+/*
+ * 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.wallet.kotlin.exchange
+
+import io.ktor.client.HttpClient
+import io.ktor.client.request.get
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.Base32Crockford
+import net.taler.wallet.kotlin.Db
+import net.taler.wallet.kotlin.DbFactory
+import net.taler.wallet.kotlin.Timestamp
+import net.taler.wallet.kotlin.compareVersions
+import net.taler.wallet.kotlin.crypto.Crypto
+import net.taler.wallet.kotlin.crypto.CryptoFactory
+import net.taler.wallet.kotlin.exchange.DenominationStatus.Unverified
+import net.taler.wallet.kotlin.exchange.ExchangeUpdateReason.Initial
+import net.taler.wallet.kotlin.exchange.ExchangeUpdateStatus.FetchKeys
+import net.taler.wallet.kotlin.exchange.ExchangeUpdateStatus.FetchWire
+import net.taler.wallet.kotlin.getDefaultHttpClient
+
+internal class Exchange(
+    private val crypto: Crypto = CryptoFactory.getCrypto(),
+    private val httpClient: HttpClient = getDefaultHttpClient(),
+    private val db: Db = DbFactory().openDb()
+) {
+
+    companion object {
+        const val PROTOCOL_VERSION = "7:0:0"
+        fun normalizeUrl(exchangeBaseUrl: String): String {
+            var url = exchangeBaseUrl
+            if (!url.startsWith("http")) url = "http://$url";
+            if (!url.endsWith("/")) url = "$url/"
+            // TODO also remove query and hash
+            return url
+        }
+    }
+
+    suspend fun updateFromUrl(baseUrl: String): ExchangeRecord {
+        val now = Timestamp.now()
+        val url = normalizeUrl(baseUrl)
+        var record = db.getExchangeByBaseUrl(url) ?: ExchangeRecord(
+            baseUrl = url,
+            timestampAdded = now,
+            updateStatus = FetchKeys,
+            updateStarted = now,
+            updateReason = Initial
+        ).also { db.put(it) }
+        record = updateKeys(record)
+        // TODO update wire
+        // TODO update ToS
+        return record
+    }
+
+    /**
+     * Fetch the exchange's /keys and update database accordingly.
+     *
+     * Exceptions thrown in this method must be caught and reported in the 
pending operations.
+     */
+    internal suspend fun updateKeys(record: ExchangeRecord): ExchangeRecord {
+        val keys: Keys = fetchKeys(record.baseUrl)
+        // check if there are denominations offered
+        // TODO provide more error information for catcher
+        if (keys.denoms.isEmpty()) {
+            throw Error("Exchange doesn't offer any denominations")
+        }
+        // check if the exchange version is compatible
+        val versionMatch = compareVersions(PROTOCOL_VERSION, keys.version)
+        if (versionMatch == null || !versionMatch.compatible) {
+            throw Error("Exchange protocol version not compatible with wallet")
+        }
+        val currency = keys.denoms[0].value.currency
+        val newDenominations = keys.denoms.map { d ->
+            getDenominationRecord(record.baseUrl, currency, d)
+        }
+        // update exchange details
+        val details = ExchangeDetails(
+            auditors = keys.auditors,
+            currency = currency,
+            lastUpdateTime = keys.list_issue_date,
+            masterPublicKey = keys.master_public_key,
+            protocolVersion = keys.version,
+            signingKeys = keys.signkeys
+        )
+        val updatedRecord = record.copy(details = details, updateStatus = 
FetchWire)
+        for (newDenomination in newDenominations) {
+            // TODO check oldDenominations and do consistency checks
+            db.put(newDenomination)
+        }
+
+        // TODO handle keys.recoup
+
+        return updatedRecord
+    }
+
+    /**
+     * Fetch an exchange's /keys with the given normalized base URL.
+     *
+     * Visible for testing.
+     */
+    internal suspend fun fetchKeys(baseUrl: String): Keys {
+        return httpClient.get("${baseUrl}keys")
+    }
+
+    /**
+     * Turn an exchange's denominations from /keys into [DenominationRecord]s
+     *
+     * Visible for testing.
+     */
+    internal fun getDenominationRecord(baseUrl: String, currency: String, d: 
Denomination): DenominationRecord {
+        checkCurrency(currency, d.value)
+        checkCurrency(currency, d.fee_refund)
+        checkCurrency(currency, d.fee_withdraw)
+        checkCurrency(currency, d.fee_refresh)
+        checkCurrency(currency, d.fee_deposit)
+        return d.toDenominationRecord(
+            baseUrl = baseUrl,
+            denomPubHash = crypto.sha512(Base32Crockford.decode(d.denom_pub)),
+            isOffered = true,
+            isRevoked = false,
+            status = Unverified
+        )
+    }
+
+    private fun checkCurrency(currency: String, amount: Amount) {
+        if (currency != amount.currency) throw Error("Expected currency 
$currency, but found ${amount.currency}")
+    }
+
+}
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/Types.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/ExchangeRecord.kt
similarity index 52%
copy from src/commonMain/kotlin/net/taler/wallet/kotlin/Types.kt
copy to src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/ExchangeRecord.kt
index 4702a19..d882249 100644
--- a/src/commonMain/kotlin/net/taler/wallet/kotlin/Types.kt
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/ExchangeRecord.kt
@@ -14,31 +14,135 @@
  * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-package net.taler.wallet.kotlin
+package net.taler.wallet.kotlin.exchange
 
-data class WireFee(
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.Timestamp
+import net.taler.wallet.kotlin.WireFee
+
+/**
+ * Exchange record as stored in the wallet's database.
+ */
+data class ExchangeRecord(
     /**
-     * Fee for wire transfers.
+     * Base url of the exchange.
      */
-    val wireFee: Amount,
+    val baseUrl: String,
+
     /**
-     * Fees to close and refund a reserve.
+     * Was the exchange added as a built-in exchange?
      */
-    val closingFee: Amount,
+    val builtIn: Boolean = false,
+
     /**
-     * Start date of the fee.
+     * Details, once known.
      */
-    val startStamp: Timestamp,
+    val details: ExchangeDetails? = null,
+
     /**
-     * End date of the fee.
+     * Mapping from wire method type to the wire fee.
      */
-    val endStamp: Timestamp,
+    val wireInfo: ExchangeWireInfo? = null,
+
     /**
-     * Signature made by the exchange master key.
+     * When was the exchange added to the wallet?
      */
-    val signature: String
+    val timestampAdded: Timestamp,
+
+    /**
+     * Terms of service text or undefined if not downloaded yet.
+     */
+    val termsOfServiceText: String? = null,
+
+    /**
+     * ETag for last terms of service download.
+     */
+    val termsOfServiceLastEtag: String? = null,
+
+    /**
+     * ETag for last terms of service download.
+     */
+    val termsOfServiceAcceptedEtag: String? = null,
+
+    /**
+     * ETag for last terms of service download.
+     */
+    val termsOfServiceAcceptedTimestamp: Timestamp? = null,
+
+    /**
+     * Time when the update to the exchange has been started or
+     * undefined if no update is in progress.
+     */
+    val updateStarted: Timestamp? = null,
+
+    val updateStatus: ExchangeUpdateStatus,
+
+    val updateReason: ExchangeUpdateReason? = null
+) {
+    init {
+        check(baseUrl == Exchange.normalizeUrl(baseUrl)) { "Base URL was not 
normalized" }
+    }
+}
+
+/**
+ * Details about the exchange that we only know after querying /keys and /wire.
+ */
+data class ExchangeDetails(
+    /**
+     * Master public key of the exchange.
+     */
+    val masterPublicKey: String,
+
+    /**
+     * Auditors (partially) auditing the exchange.
+     */
+    val auditors: List<Auditor>,
+
+    /**
+     * Currency that the exchange offers.
+     */
+    val currency: String,
+
+    /**
+     * Last observed protocol version.
+     */
+    val protocolVersion: String,
+
+    /**
+     * Signing keys we got from the exchange, can also contain
+     * older signing keys that are not returned by /keys anymore.
+     */
+    val signingKeys: List<SigningKey>,
+
+    /**
+     * Timestamp for last update.
+     */
+    val lastUpdateTime: Timestamp
 )
 
+data class ExchangeWireInfo(
+    val feesForType: Map<String, List<WireFee>>,
+    val accounts: List<ExchangeBankAccount>
+)
+
+data class  ExchangeBankAccount(
+    val paytoUri: String
+)
+
+sealed class ExchangeUpdateStatus(val value: String) {
+    object FetchKeys: ExchangeUpdateStatus("fetch-keys")
+    object FetchWire: ExchangeUpdateStatus("fetch-wire")
+    object FetchTerms: ExchangeUpdateStatus("fetch-terms")
+    object FinalizeUpdate: ExchangeUpdateStatus("finalize-update")
+    object Finished: ExchangeUpdateStatus("finished")
+}
+
+sealed class ExchangeUpdateReason(val value: String) {
+    object Initial: ExchangeUpdateReason("initial")
+    object Forced: ExchangeUpdateReason("forced")
+    object Scheduled: ExchangeUpdateReason("scheduled")
+}
+
 data class DenominationRecord(
     /**
      * Value of one coin of the denomination.
@@ -128,81 +232,3 @@ enum class DenominationStatus {
      */
     VerifiedBad
 }
-
-class CoinRecord(
-    /**
-     * Where did the coin come from?  Used for recouping coins.
-     */
-    val coinSource: CoinSourceType,
-
-    /**
-     * Public key of the coin.
-     */
-    val coinPub: String,
-
-    /**
-     * Private key to authorize operations on the coin.
-     */
-    val coinPriv: String,
-
-    /**
-     * Key used by the exchange used to sign the coin.
-     */
-    val denomPub: String,
-
-    /**
-     * Hash of the public key that signs the coin.
-     */
-    val denomPubHash: String,
-
-    /**
-     * Unblinded signature by the exchange.
-     */
-    val denomSig: String,
-
-    /**
-     * Amount that's left on the coin.
-     */
-    val currentAmount: Amount,
-
-    /**
-     * Base URL that identifies the exchange from which we got the coin.
-     */
-    val exchangeBaseUrl: String,
-
-    /**
-     * The coin is currently suspended, and will not be used for payments.
-     */
-    val suspended: Boolean,
-
-    /**
-     * Blinding key used when withdrawing the coin.
-     * Potentially send again during payback.
-     */
-    val blindingKey: String,
-
-    /**
-     * Status of the coin.
-     */
-    val status: CoinStatus
-)
-
-enum class CoinSourceType(val value: String) {
-    WITHDRAW("withdraw"),
-    REFRESH("refresh"),
-    TIP("tip")
-}
-
-enum class CoinStatus(val value: String) {
-
-    /**
-     * Withdrawn and never shown to anybody.
-     */
-    FRESH("fresh"),
-
-    /**
-     * A coin that has been spent and refreshed.
-     */
-    DORMANT("dormant")
-
-}
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Keys.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Keys.kt
new file mode 100644
index 0000000..c7ea966
--- /dev/null
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Keys.kt
@@ -0,0 +1,177 @@
+/*
+ * 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.wallet.kotlin.exchange
+
+import kotlinx.serialization.Serializable
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.Base32Crockford
+import net.taler.wallet.kotlin.Timestamp
+
+/**
+ * Structure that the exchange gives us in /keys.
+ */
+@Serializable
+internal data class Keys(
+    /**
+     * List of offered denominations.
+     */
+    val denoms: List<Denomination>,
+
+    /**
+     * The exchange's master public key.
+     */
+    val master_public_key: String,
+
+    /**
+     * The list of auditors (partially) auditing the exchange.
+     */
+    val auditors: List<Auditor>,
+
+    /**
+     * Timestamp when this response was issued.
+     */
+    val list_issue_date: Timestamp,
+
+    /**
+     * List of revoked denominations.
+     */
+    val recoup: List<Recoup>?,
+
+    /**
+     * Short-lived signing keys used to sign online
+     * responses.
+     */
+    val signkeys: List<SigningKey>,
+
+    /**
+     * Protocol version.
+     */
+    val version: String
+)
+
+/**
+ * Structure of one exchange signing key in the /keys response.
+ */
+@Serializable
+data class SigningKey(
+    val stamp_start: Timestamp,
+    val stamp_expire: Timestamp,
+    val stamp_end: Timestamp,
+    val key: String,
+    val master_sig: String
+)
+
+/**
+ * Denomination as found in the /keys response from the exchange.
+ */
+@Serializable
+internal data class Denomination(
+    /**
+     * Value of one coin of the denomination.
+     */
+    val value: Amount,
+
+    /**
+     * Public signing key of the denomination.
+     */
+    val denom_pub: String,
+
+    /**
+     * Fee for withdrawing.
+     */
+    val fee_withdraw: Amount,
+
+    /**
+     * Fee for depositing.
+     */
+    val fee_deposit: Amount,
+
+    /**
+     * Fee for refreshing.
+     */
+    val fee_refresh: Amount,
+
+    /**
+     * Fee for refunding.
+     */
+    val fee_refund: Amount,
+
+    /**
+     * Start date from which withdraw is allowed.
+     */
+    val stamp_start: Timestamp,
+
+    /**
+     * End date for withdrawing.
+     */
+    val stamp_expire_withdraw: Timestamp,
+
+    /**
+     * Expiration date after which the exchange can forget about
+     * the currency.
+     */
+    val stamp_expire_legal: Timestamp,
+
+    /**
+     * Date after which the coins of this denomination can't be
+     * deposited anymore.
+     */
+    val stamp_expire_deposit: Timestamp,
+
+    /**
+     * Signature over the denomination information by the exchange's master
+     * signing key.
+     */
+    val master_sig: String
+) {
+    fun toDenominationRecord(
+        baseUrl: String,
+        denomPubHash: ByteArray,
+        isOffered: Boolean,
+        isRevoked: Boolean,
+        status: DenominationStatus
+    ): DenominationRecord = DenominationRecord(
+        denomPub = denom_pub,
+        denomPubHash = Base32Crockford.encode(denomPubHash),
+        exchangeBaseUrl = baseUrl,
+        feeDeposit = fee_deposit,
+        feeRefresh = fee_refresh,
+        feeRefund = fee_refund,
+        feeWithdraw = fee_withdraw,
+        isOffered = isOffered,
+        isRevoked = isRevoked,
+        masterSig = master_sig,
+        stampExpireDeposit = stamp_expire_deposit,
+        stampExpireLegal = stamp_expire_legal,
+        stampExpireWithdraw = stamp_expire_withdraw,
+        stampStart = stamp_start,
+        status = status,
+        value = value
+    )
+}
+
+/**
+ * Element of the payback list that the
+ * exchange gives us in /keys.
+ */
+@Serializable
+data class Recoup(
+    /**
+     * The hash of the denomination public key for which the payback is 
offered.
+     */
+    val h_denom_pub: String
+)
diff --git 
a/src/commonMain/kotlin/net/taler/wallet/kotlin/operations/Withdraw.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/operations/Withdraw.kt
index b002e32..f7064bf 100644
--- a/src/commonMain/kotlin/net/taler/wallet/kotlin/operations/Withdraw.kt
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/operations/Withdraw.kt
@@ -17,17 +17,14 @@
 package net.taler.wallet.kotlin.operations
 
 import io.ktor.client.HttpClient
-import io.ktor.client.features.json.JsonFeature
-import io.ktor.client.features.json.serializer.KotlinxSerializer
 import io.ktor.client.request.get
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
 import net.taler.wallet.kotlin.Amount
 import net.taler.wallet.kotlin.TalerUri.parseWithdrawUri
+import net.taler.wallet.kotlin.getDefaultHttpClient
 
-class Withdraw(
-    private val httpClient: HttpClient = HttpClient { install(JsonFeature) { 
serializer = KotlinxSerializer() } }
-) {
+class Withdraw(private val httpClient: HttpClient = getDefaultHttpClient()) {
 
     data class BankDetails(
         val amount: Amount,
diff --git a/src/commonTest/kotlin/net/taler/wallet/kotlin/DbTest.kt 
b/src/commonTest/kotlin/net/taler/wallet/kotlin/DbTest.kt
new file mode 100644
index 0000000..7acc2a5
--- /dev/null
+++ b/src/commonTest/kotlin/net/taler/wallet/kotlin/DbTest.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.wallet.kotlin
+
+import net.taler.wallet.kotlin.exchange.ExchangeRecord
+import net.taler.wallet.kotlin.exchange.ExchangeUpdateReason.Initial
+import net.taler.wallet.kotlin.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 test() = 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])
+    }
+
+}
\ No newline at end of file
diff --git a/src/commonTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt 
b/src/commonTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt
index 857c56c..72e4b4b 100644
--- a/src/commonTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt
+++ b/src/commonTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt
@@ -22,6 +22,7 @@ import io.ktor.client.engine.mock.MockEngineConfig
 import io.ktor.client.engine.mock.respond
 import io.ktor.client.engine.mock.respondError
 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.HttpStatusCode.Companion.InternalServerError
 import io.ktor.http.Url
@@ -29,14 +30,25 @@ 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_LINUX,
+}
 
 /**
  * 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)
+    install(JsonFeature) {
+        serializer = KotlinxSerializer(Json(getJsonConfiguration()))
+    }
     engine {
         addHandler { error("No test handler added") }
     }
diff --git 
a/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/SignatureTest.kt 
b/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/SignatureTest.kt
index 48cbc8d..02d9b1d 100644
--- a/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/SignatureTest.kt
+++ b/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/SignatureTest.kt
@@ -18,12 +18,12 @@ package net.taler.wallet.kotlin.crypto
 
 import net.taler.wallet.kotlin.Amount
 import net.taler.wallet.kotlin.Base32Crockford
-import net.taler.wallet.kotlin.DenominationRecord
-import net.taler.wallet.kotlin.DenominationStatus.Unverified
-import net.taler.wallet.kotlin.DenominationStatus.VerifiedBad
 import net.taler.wallet.kotlin.Timestamp
 import net.taler.wallet.kotlin.WireFee
 import net.taler.wallet.kotlin.crypto.Signature.PurposeBuilder
+import net.taler.wallet.kotlin.exchange.DenominationRecord
+import net.taler.wallet.kotlin.exchange.DenominationStatus.Unverified
+import net.taler.wallet.kotlin.exchange.DenominationStatus.VerifiedBad
 import kotlin.random.Random
 import kotlin.test.Test
 import kotlin.test.assertEquals
diff --git 
a/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/ExchangeTest.kt 
b/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/ExchangeTest.kt
new file mode 100644
index 0000000..e7a2c48
--- /dev/null
+++ b/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/ExchangeTest.kt
@@ -0,0 +1,317 @@
+/*
+ * 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.wallet.kotlin.exchange
+
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.Timestamp
+import net.taler.wallet.kotlin.getMockHttpClient
+import net.taler.wallet.kotlin.giveJsonResponse
+import net.taler.wallet.kotlin.runCoroutine
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class ExchangeTest {
+
+    private val httpClient = getMockHttpClient()
+    private val exchange = Exchange(httpClient = httpClient)
+
+    @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 = exchange.fetchKeys("https://exchange.test.taler.net/";)
+            assertEquals(expectedKeys, keys)
+        }
+    }
+
+
+}
\ No newline at end of file
diff --git a/src/linuxTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt 
b/src/jsMain/kotlin/net/taler/wallet/kotlin/Db.kt
similarity index 80%
copy from src/linuxTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt
copy to src/jsMain/kotlin/net/taler/wallet/kotlin/Db.kt
index 524da15..45cbfc3 100644
--- a/src/linuxTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt
+++ b/src/jsMain/kotlin/net/taler/wallet/kotlin/Db.kt
@@ -16,7 +16,8 @@
 
 package net.taler.wallet.kotlin
 
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.runBlocking
-
-actual fun runCoroutine(block: suspend (scope : CoroutineScope) -> Unit) = 
runBlocking { block(this) }
+internal actual class DbFactory {
+    actual fun openDb(): Db {
+        return FakeDb()
+    }
+}
diff --git a/src/jsTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt 
b/src/jsTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt
similarity index 93%
rename from src/jsTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt
rename to src/jsTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt
index da5a183..49466e0 100644
--- a/src/jsTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt
+++ b/src/jsTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt
@@ -21,3 +21,5 @@ import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.promise
 
 actual fun runCoroutine(block: suspend (scope : CoroutineScope) -> Unit): 
dynamic = GlobalScope.promise { block(this) }
+
+actual fun getPlatformTarget(): PlatformTarget = PlatformTarget.JS
diff --git a/src/linuxTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt 
b/src/linuxMain/kotlin/net/taler/wallet/kotlin/Db.kt
similarity index 80%
rename from src/linuxTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt
rename to src/linuxMain/kotlin/net/taler/wallet/kotlin/Db.kt
index 524da15..45cbfc3 100644
--- a/src/linuxTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt
+++ b/src/linuxMain/kotlin/net/taler/wallet/kotlin/Db.kt
@@ -16,7 +16,8 @@
 
 package net.taler.wallet.kotlin
 
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.runBlocking
-
-actual fun runCoroutine(block: suspend (scope : CoroutineScope) -> Unit) = 
runBlocking { block(this) }
+internal actual class DbFactory {
+    actual fun openDb(): Db {
+        return FakeDb()
+    }
+}
diff --git a/src/androidTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt 
b/src/linuxTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt
similarity index 92%
rename from src/androidTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt
rename to src/linuxTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt
index 524da15..162ce4e 100644
--- a/src/androidTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt
+++ b/src/linuxTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt
@@ -20,3 +20,5 @@ import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.runBlocking
 
 actual fun runCoroutine(block: suspend (scope : CoroutineScope) -> Unit) = 
runBlocking { block(this) }
+
+actual fun getPlatformTarget(): PlatformTarget = PlatformTarget.NATIVE_LINUX

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]