gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-kotlin] branch master updated (bdfbb18 -> 4379d55)


From: gnunet
Subject: [taler-wallet-kotlin] branch master updated (bdfbb18 -> 4379d55)
Date: Thu, 09 Jul 2020 21:10:18 +0200

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

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

    from bdfbb18  Use custom serializer for Amount and improve withdrawal info 
test
     new 2ee6b0d  Add class to handle libtool-style semantic versioning with 
test
     new 4379d55  Add initial code for updating an exchange

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 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}       |  20 +-
 .../kotlin/net/taler/wallet/kotlin/Version.kt      |  71 +++++
 .../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 +-
 .../kotlin/net/taler/wallet/kotlin/VersionTest.kt  |  57 ++++
 .../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 +
 26 files changed, 1117 insertions(+), 241 deletions(-)
 copy src/{linuxTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt => 
androidMain/kotlin/net/taler/wallet/kotlin/Db.kt} (80%)
 copy src/{linuxTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt => 
androidTest/kotlin/net/taler/wallet/kotlin/TestUtilsAndroid.kt} (92%)
 create mode 100644 src/commonMain/kotlin/net/taler/wallet/kotlin/Db.kt
 copy src/{jsTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt => 
commonMain/kotlin/net/taler/wallet/kotlin/Utils.kt} (57%)
 create mode 100644 src/commonMain/kotlin/net/taler/wallet/kotlin/Version.kt
 copy src/commonMain/kotlin/net/taler/wallet/kotlin/{Timestamp.kt => 
exchange/Auditor.kt} (50%)
 create mode 100644 
src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Exchange.kt
 copy src/commonMain/kotlin/net/taler/wallet/kotlin/{Types.kt => 
exchange/ExchangeRecord.kt} (52%)
 create mode 100644 
src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Keys.kt
 create mode 100644 src/commonTest/kotlin/net/taler/wallet/kotlin/DbTest.kt
 create mode 100644 src/commonTest/kotlin/net/taler/wallet/kotlin/VersionTest.kt
 create mode 100644 
src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/ExchangeTest.kt
 copy src/{linuxTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt => 
jsMain/kotlin/net/taler/wallet/kotlin/Db.kt} (80%)
 rename src/jsTest/kotlin/net/taler/wallet/kotlin/{runCoroutine.kt => 
TestUtils.kt} (93%)
 rename src/{linuxTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt => 
linuxMain/kotlin/net/taler/wallet/kotlin/Db.kt} (80%)
 rename src/{androidTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt => 
linuxTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt} (92%)

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/jsTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/Utils.kt
similarity index 57%
copy from src/jsTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt
copy to src/commonMain/kotlin/net/taler/wallet/kotlin/Utils.kt
index da5a183..aa3fd91 100644
--- a/src/jsTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/Utils.kt
@@ -16,8 +16,20 @@
 
 package net.taler.wallet.kotlin
 
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.promise
+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): 
dynamic = GlobalScope.promise { 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/Version.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/Version.kt
new file mode 100644
index 0000000..457fb07
--- /dev/null
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/Version.kt
@@ -0,0 +1,71 @@
+/*
+ * 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 kotlin.math.sign
+
+/**
+ * Semantic versioning, but libtool-style.
+ * See 
https://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html
+ */
+
+/**
+ * Result of comparing two libtool versions.
+ */
+data class VersionMatchResult(
+    /**
+     * Is the first version compatible with the second?
+     */
+    val compatible: Boolean,
+    /**
+     * Is the first version older (-1), newer (+1) or identical (0)?
+     */
+    val currentCmp: Int
+)
+
+data class Version(
+    val current: Int,
+    val revision: Int,
+    val age: Int
+)
+
+/**
+ * Compare two libtool-style version strings.
+ */
+fun compareVersions(me: String,other: String): VersionMatchResult? {
+    val meVer = parseVersion (me)
+    val otherVer = parseVersion (other)
+    if (meVer == null || otherVer == null) return null
+
+    val compatible = meVer.current - meVer.age <= otherVer.current &&
+            meVer.current >= otherVer.current - otherVer.age
+
+    val currentCmp = sign((meVer.current - 
otherVer.current).toDouble()).toInt()
+
+    return VersionMatchResult(compatible, currentCmp)
+}
+
+fun parseVersion(v: String): Version? {
+    val elements = v.split(":")
+    if (elements.size != 3) return null
+    val (currentStr, revisionStr, ageStr) = elements
+    val current = currentStr.toIntOrNull()
+    val revision = revisionStr.toIntOrNull()
+    val age = ageStr.toIntOrNull()
+    if (current == null || revision == null || age == null) return null
+    return Version(current, revision, age)
+}
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/VersionTest.kt 
b/src/commonTest/kotlin/net/taler/wallet/kotlin/VersionTest.kt
new file mode 100644
index 0000000..d445ebc
--- /dev/null
+++ b/src/commonTest/kotlin/net/taler/wallet/kotlin/VersionTest.kt
@@ -0,0 +1,57 @@
+/*
+ * 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 kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+
+class VersionTest {
+
+    @Test
+    fun testComparision() {
+        assertNull(compareVersions("0:0:0", ""))
+        assertNull(compareVersions("", "0:0:0"))
+        assertNull(compareVersions("foo", "0:0:0"))
+        assertNull(compareVersions("0:0:0", "foo"))
+        assertEquals(
+            VersionMatchResult(true, 0),
+            compareVersions("0:0:0", "0:0:0")
+        )
+        assertEquals(
+            VersionMatchResult(true, -1),
+            compareVersions("0:0:0", "1:0:1")
+        )
+        assertEquals(
+            VersionMatchResult(true, -1),
+            compareVersions("0:0:0", "1:5:1")
+        )
+        assertEquals(
+            VersionMatchResult(false, -1),
+            compareVersions("0:0:0", "1:5:0")
+        )
+        assertEquals(
+            VersionMatchResult(false, 1),
+            compareVersions("1:0:0", "0:5:0")
+        )
+        assertEquals(
+            VersionMatchResult(true, 0),
+            compareVersions("1:0:1", "1:5:1")
+        )
+    }
+
+}
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]