summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorTorsten Grote <t@grobox.de>2020-07-08 12:00:53 -0300
committerTorsten Grote <t@grobox.de>2020-07-08 12:00:53 -0300
commit4379d554c72d80328dff1e873d7cc007a1a5881a (patch)
tree354e4ef39c67a0e07e672bb0a09df850a2cc1561 /src
parent2ee6b0d4cc3871237d24e28d09d2f526d1304da1 (diff)
downloadwallet-kotlin-4379d554c72d80328dff1e873d7cc007a1a5881a.tar.gz
wallet-kotlin-4379d554c72d80328dff1e873d7cc007a1a5881a.tar.bz2
wallet-kotlin-4379d554c72d80328dff1e873d7cc007a1a5881a.zip
Add initial code for updating an exchange
This introduces a common Database interface that gets currently only implemented by a fake in-memmory database.
Diffstat (limited to 'src')
-rw-r--r--src/androidMain/kotlin/net/taler/wallet/kotlin/Db.kt23
-rw-r--r--src/androidTest/kotlin/net/taler/wallet/kotlin/TestUtilsAndroid.kt (renamed from src/androidTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt)2
-rw-r--r--src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/RefreshTest.kt4
-rw-r--r--src/commonMain/kotlin/net/taler/wallet/kotlin/Db.kt59
-rw-r--r--src/commonMain/kotlin/net/taler/wallet/kotlin/Timestamp.kt8
-rw-r--r--src/commonMain/kotlin/net/taler/wallet/kotlin/Types.kt89
-rw-r--r--src/commonMain/kotlin/net/taler/wallet/kotlin/Utils.kt35
-rw-r--r--src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Refresh.kt2
-rw-r--r--src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Signature.kt2
-rw-r--r--src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Auditor.kt56
-rw-r--r--src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Exchange.kt142
-rw-r--r--src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/ExchangeRecord.kt234
-rw-r--r--src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Keys.kt177
-rw-r--r--src/commonMain/kotlin/net/taler/wallet/kotlin/operations/Withdraw.kt7
-rw-r--r--src/commonTest/kotlin/net/taler/wallet/kotlin/DbTest.kt67
-rw-r--r--src/commonTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt14
-rw-r--r--src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/SignatureTest.kt6
-rw-r--r--src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/ExchangeTest.kt317
-rw-r--r--src/jsMain/kotlin/net/taler/wallet/kotlin/Db.kt23
-rw-r--r--src/jsTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt (renamed from src/jsTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt)2
-rw-r--r--src/linuxMain/kotlin/net/taler/wallet/kotlin/Db.kt23
-rw-r--r--src/linuxTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt (renamed from src/linuxTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt)2
22 files changed, 1189 insertions, 105 deletions
diff --git a/src/androidMain/kotlin/net/taler/wallet/kotlin/Db.kt b/src/androidMain/kotlin/net/taler/wallet/kotlin/Db.kt
new file mode 100644
index 0000000..45cbfc3
--- /dev/null
+++ b/src/androidMain/kotlin/net/taler/wallet/kotlin/Db.kt
@@ -0,0 +1,23 @@
+/*
+ * 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
+
+internal actual class DbFactory {
+ actual fun openDb(): Db {
+ return FakeDb()
+ }
+}
diff --git a/src/androidTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt b/src/androidTest/kotlin/net/taler/wallet/kotlin/TestUtilsAndroid.kt
index 524da15..a362874 100644
--- a/src/androidTest/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/commonMain/kotlin/net/taler/wallet/kotlin/Utils.kt b/src/commonMain/kotlin/net/taler/wallet/kotlin/Utils.kt
new file mode 100644
index 0000000..aa3fd91
--- /dev/null
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/Utils.kt
@@ -0,0 +1,35 @@
+/*
+ * 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 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
+
+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/exchange/Auditor.kt b/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Auditor.kt
new file mode 100644
index 0000000..4df0bdf
--- /dev/null
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Auditor.kt
@@ -0,0 +1,56 @@
+/*
+ * 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
+
+/**
+ * Auditor information as given by the exchange in /keys.
+ */
+@Serializable
+data class Auditor(
+ /**
+ * Auditor's public key.
+ */
+ val auditor_pub: String,
+
+ /**
+ * Base URL of the auditor.
+ */
+ val auditor_url: String,
+
+ /**
+ * List of signatures for denominations by the auditor.
+ */
+ val denomination_keys: List<AuditorDenomSig>
+)
+
+/**
+ * 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/exchange/ExchangeRecord.kt b/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/ExchangeRecord.kt
new file mode 100644
index 0000000..d882249
--- /dev/null
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/ExchangeRecord.kt
@@ -0,0 +1,234 @@
+/*
+ * 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.WireFee
+
+/**
+ * Exchange record as stored in the wallet's database.
+ */
+data class ExchangeRecord(
+ /**
+ * Base url of the exchange.
+ */
+ val baseUrl: String,
+
+ /**
+ * Was the exchange added as a built-in exchange?
+ */
+ val builtIn: Boolean = false,
+
+ /**
+ * Details, once known.
+ */
+ val details: ExchangeDetails? = null,
+
+ /**
+ * Mapping from wire method type to the wire fee.
+ */
+ val wireInfo: ExchangeWireInfo? = null,
+
+ /**
+ * When was the exchange added to the wallet?
+ */
+ 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.
+ */
+ 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
+}
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/jsMain/kotlin/net/taler/wallet/kotlin/Db.kt b/src/jsMain/kotlin/net/taler/wallet/kotlin/Db.kt
new file mode 100644
index 0000000..45cbfc3
--- /dev/null
+++ b/src/jsMain/kotlin/net/taler/wallet/kotlin/Db.kt
@@ -0,0 +1,23 @@
+/*
+ * 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
+
+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
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/linuxMain/kotlin/net/taler/wallet/kotlin/Db.kt b/src/linuxMain/kotlin/net/taler/wallet/kotlin/Db.kt
new file mode 100644
index 0000000..45cbfc3
--- /dev/null
+++ b/src/linuxMain/kotlin/net/taler/wallet/kotlin/Db.kt
@@ -0,0 +1,23 @@
+/*
+ * 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
+
+internal actual class DbFactory {
+ actual fun openDb(): Db {
+ return FakeDb()
+ }
+}
diff --git a/src/linuxTest/kotlin/net/taler/wallet/kotlin/runCoroutine.kt b/src/linuxTest/kotlin/net/taler/wallet/kotlin/TestUtils.kt
index 524da15..162ce4e 100644
--- a/src/linuxTest/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