gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-kotlin] branch master updated: Finish prototype of adding/


From: gnunet
Subject: [taler-wallet-kotlin] branch master updated: Finish prototype of adding/updating an exchange
Date: Mon, 13 Jul 2020 22:09:30 +0200

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

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

The following commit(s) were added to refs/heads/master by this push:
     new 5f03889  Finish prototype of adding/updating an exchange
5f03889 is described below

commit 5f03889e649271347229af777ec1a025a6210f23
Author: Torsten Grote <t@grobox.de>
AuthorDate: Mon Jul 13 17:01:35 2020 -0300

    Finish prototype of adding/updating an exchange
---
 .idea/jarRepositories.xml                          |   5 +
 build.gradle                                       |  10 +-
 .../kotlin/net/taler/wallet/kotlin/Db.kt           |  34 +++-
 .../kotlin/net/taler/wallet/kotlin/Types.kt        |  23 ---
 .../kotlin/net/taler/wallet/kotlin/Utils.kt        |   6 +
 .../net/taler/wallet/kotlin/crypto/Signature.kt    |   2 +-
 .../net/taler/wallet/kotlin/exchange/Exchange.kt   | 108 +++++++++++--
 .../taler/wallet/kotlin/exchange/ExchangeRecord.kt |  24 +--
 .../net/taler/wallet/kotlin/exchange/Keys.kt       |  13 +-
 .../net/taler/wallet/kotlin/exchange/Wire.kt       |  79 ++++++++++
 .../taler/wallet/kotlin/crypto/SignatureTest.kt    |   2 +-
 .../exchange/{ExchangeTest.kt => KeysTest.kt}      |   5 +-
 .../taler/wallet/kotlin/exchange/UpdateTest.kt}    |  32 ++--
 .../net/taler/wallet/kotlin/exchange/WireTest.kt   | 175 +++++++++++++++++++++
 14 files changed, 442 insertions(+), 76 deletions(-)

diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
index b3e9cbd..b9b962c 100644
--- a/.idea/jarRepositories.xml
+++ b/.idea/jarRepositories.xml
@@ -21,5 +21,10 @@
       <option name="name" value="BintrayJCenter" />
       <option name="url" value="https://jcenter.bintray.com/"; />
     </remote-repository>
+    <remote-repository>
+      <option name="id" value="maven" />
+      <option name="name" value="maven" />
+      <option name="url" value="https://dl.bintray.com/terl/lazysodium-maven"; 
/>
+    </remote-repository>
   </component>
 </project>
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index c173b78..007a4bd 100644
--- a/build.gradle
+++ b/build.gradle
@@ -66,6 +66,7 @@ kotlin {
                 implementation kotlin('stdlib-common')
                 implementation 
"org.jetbrains.kotlinx:kotlinx-coroutines-core-common:$coroutines_version"
                 implementation "io.ktor:ktor-client-core:$ktor_version"
+                implementation "io.ktor:ktor-client-logging:$ktor_version"
                 implementation 
"io.ktor:ktor-client-serialization:$ktor_version"
                 implementation "com.soywiz.korlibs.klock:klock:1.11.12"
             }
@@ -82,10 +83,13 @@ kotlin {
                 implementation kotlin('stdlib-jdk8')
                 implementation 
"org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
                 implementation "io.ktor:ktor-client-android:$ktor_version"
+                implementation "io.ktor:ktor-client-logging-jvm:$ktor_version"
                 implementation 
"io.ktor:ktor-client-serialization-jvm:$ktor_version"
                 // TODO Android
 //                implementation 
"com.goterl.lazycode:lazysodium-android:4.1.1@aar"
-                implementation "com.goterl.lazycode:lazysodium-java:4.2.6"
+                implementation("com.goterl.lazycode:lazysodium-java:4.2.6") {
+                    exclude group: "org.slf4j"
+                }
                 implementation 'net.java.dev.jna:jna:5.5.0@aar'
             }
         }
@@ -94,6 +98,7 @@ kotlin {
                 implementation kotlin('test')
                 implementation kotlin('test-junit')
                 api "io.ktor:ktor-client-mock-jvm:$ktor_version"
+                implementation "org.slf4j:slf4j-simple:1.7.30"
             }
         }
         jsMain {
@@ -101,12 +106,14 @@ kotlin {
                 implementation kotlin('stdlib-js')
                 implementation 
"org.jetbrains.kotlinx:kotlinx-coroutines-core-js:$coroutines_version"
                 implementation "io.ktor:ktor-client-js:$ktor_version"
+                implementation "io.ktor:ktor-client-logging-js:$ktor_version"
                 implementation 
"io.ktor:ktor-client-serialization-js:$ktor_version"
                 // bug: https://github.com/ktorio/ktor/issues/1822
                 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("node-fetch", '2.6.0') // work-around for above bug
                 api npm("fs", '*') // work-around for above bug
 
                 implementation npm('tweetnacl', '1.0.3')
@@ -124,6 +131,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-logging-native:$ktor_version"
                 implementation 
"io.ktor:ktor-client-serialization-native:$ktor_version"
             }
         }
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/Db.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/Db.kt
index 303c526..14a2f98 100644
--- a/src/commonMain/kotlin/net/taler/wallet/kotlin/Db.kt
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/Db.kt
@@ -16,6 +16,8 @@
 
 package net.taler.wallet.kotlin
 
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
 import net.taler.wallet.kotlin.exchange.DenominationRecord
 import net.taler.wallet.kotlin.exchange.ExchangeRecord
 
@@ -25,35 +27,51 @@ internal interface Db {
     suspend fun getExchangeByBaseUrl(baseUrl: String): ExchangeRecord?
     suspend fun deleteExchangeByBaseUrl(baseUrl: String)
     suspend fun put(denomination: DenominationRecord)
+    suspend fun <T> transaction(function: suspend Db.() -> T): T
 }
 
 internal expect class DbFactory() {
     fun openDb(): Db
 }
 
-internal class FakeDb: Db {
+internal class FakeDb : Db {
 
-    private val exchanges = HashMap<String, ExchangeRecord>()
-    private val denominations = HashMap<String, DenominationRecord>()
+    private data class Data(
+        val exchanges: HashMap<String, ExchangeRecord> = HashMap(),
+        val denominations: HashMap<String, DenominationRecord> = HashMap()
+    )
+
+    private var data = Data()
+    private val mutex = Mutex(false)
 
     override suspend fun put(exchange: ExchangeRecord) {
-        exchanges[exchange.baseUrl] = exchange
+        data.exchanges[exchange.baseUrl] = exchange
     }
 
     override suspend fun listExchanges(): List<ExchangeRecord> {
-        return exchanges.values.toList()
+        return data.exchanges.values.toList()
     }
 
     override suspend fun getExchangeByBaseUrl(baseUrl: String): 
ExchangeRecord? {
-        return exchanges[baseUrl]
+        return data.exchanges[baseUrl]
     }
 
     override suspend fun deleteExchangeByBaseUrl(baseUrl: String) {
-        exchanges.remove(baseUrl)
+        data.exchanges.remove(baseUrl)
     }
 
     override suspend fun put(denomination: DenominationRecord) {
-        denominations[denomination.exchangeBaseUrl] = denomination
+        data.denominations[denomination.exchangeBaseUrl] = denomination
+    }
+
+    override suspend fun <T> transaction(function: suspend Db.() -> T): T = 
mutex.withLock("transaction") {
+        val dataCopy = data.copy()
+        return@withLock try {
+            function()
+        } catch (e: Throwable) {
+            data = dataCopy
+            throw e
+        }
     }
 
 }
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/Types.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/Types.kt
index 2365795..04b17e7 100644
--- a/src/commonMain/kotlin/net/taler/wallet/kotlin/Types.kt
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/Types.kt
@@ -16,29 +16,6 @@
 
 package net.taler.wallet.kotlin
 
-data class WireFee(
-    /**
-     * Fee for wire transfers.
-     */
-    val wireFee: Amount,
-    /**
-     * Fees to close and refund a reserve.
-     */
-    val closingFee: Amount,
-    /**
-     * Start date of the fee.
-     */
-    val startStamp: Timestamp,
-    /**
-     * End date of the fee.
-     */
-    val endStamp: Timestamp,
-    /**
-     * Signature made by the exchange master key.
-     */
-    val signature: String
-)
-
 
 class CoinRecord(
     /**
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/Utils.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/Utils.kt
index aa3fd91..2549195 100644
--- a/src/commonMain/kotlin/net/taler/wallet/kotlin/Utils.kt
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/Utils.kt
@@ -19,6 +19,8 @@ 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 io.ktor.client.features.logging.LogLevel
+import io.ktor.client.features.logging.Logging
 import kotlinx.serialization.UnstableDefault
 import kotlinx.serialization.json.Json
 import kotlinx.serialization.json.JsonConfiguration
@@ -27,6 +29,10 @@ fun getDefaultHttpClient(): HttpClient = HttpClient {
     install(JsonFeature) {
         serializer = KotlinxSerializer(Json(getJsonConfiguration()))
     }
+    install(Logging) {
+//        level = LogLevel.HEADERS
+        level = LogLevel.NONE
+    }
 }
 
 @OptIn(UnstableDefault::class)
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 ee1dff4..9b06756 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.WireFee
 import net.taler.wallet.kotlin.crypto.CryptoImpl.Companion.toByteArray
 import net.taler.wallet.kotlin.exchange.DenominationRecord
+import net.taler.wallet.kotlin.exchange.WireFee
 
 internal class Signature(private val crypto: Crypto) {
 
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Exchange.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Exchange.kt
index 4124c30..c8a89ef 100644
--- a/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Exchange.kt
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Exchange.kt
@@ -17,7 +17,13 @@
 package net.taler.wallet.kotlin.exchange
 
 import io.ktor.client.HttpClient
+import io.ktor.client.request.accept
 import io.ktor.client.request.get
+import io.ktor.client.statement.HttpResponse
+import io.ktor.client.statement.readText
+import io.ktor.http.ContentType
+import io.ktor.http.HttpHeaders
+import io.ktor.http.HttpStatusCode
 import net.taler.wallet.kotlin.Amount
 import net.taler.wallet.kotlin.Base32Crockford
 import net.taler.wallet.kotlin.Db
@@ -26,15 +32,23 @@ 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.crypto.Signature
 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.FetchTerms
 import net.taler.wallet.kotlin.exchange.ExchangeUpdateStatus.FetchWire
+import net.taler.wallet.kotlin.exchange.ExchangeUpdateStatus.FinalizeUpdate
+import net.taler.wallet.kotlin.exchange.ExchangeUpdateStatus.Finished
 import net.taler.wallet.kotlin.getDefaultHttpClient
 
 internal class Exchange(
     private val crypto: Crypto = CryptoFactory.getCrypto(),
+    private val signature: Signature = Signature(crypto),
     private val httpClient: HttpClient = getDefaultHttpClient(),
+    // using the default Http client adds a json Accept header to each 
request, so we need a different one
+    // because the exchange is returning XML when it doesn't exactly match a 
mime type.
+    private val httpNoJsonClient: HttpClient = HttpClient(),
     private val db: Db = DbFactory().openDb()
 ) {
 
@@ -49,6 +63,9 @@ internal class Exchange(
         }
     }
 
+    /**
+     * Update or add exchange DB entry by fetching the /keys, /wire and /terms 
information.
+     */
     suspend fun updateFromUrl(baseUrl: String): ExchangeRecord {
         val now = Timestamp.now()
         val url = normalizeUrl(baseUrl)
@@ -59,9 +76,17 @@ internal class Exchange(
             updateStarted = now,
             updateReason = Initial
         ).also { db.put(it) }
-        record = updateKeys(record)
-        // TODO update wire
-        // TODO update ToS
+        val recordBeforeUpdate = record.copy()
+
+        record = updateKeys(record)  // TODO add denominations in transaction 
at the end
+        record = updateWireInfo(record)
+        record = updateTermsOfService(record)
+        record = finalizeUpdate(record)
+        db.transaction {
+            val dbRecord = getExchangeByBaseUrl(record.baseUrl)
+            if (dbRecord != recordBeforeUpdate) throw Error("Concurrent 
modification of $dbRecord")
+            put(record)
+        }
         return record
     }
 
@@ -71,7 +96,7 @@ internal class Exchange(
      * 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)
+        val keys: Keys = Keys.fetch(httpClient, record.baseUrl)
         // check if there are denominations offered
         // TODO provide more error information for catcher
         if (keys.denoms.isEmpty()) {
@@ -106,15 +131,6 @@ internal class Exchange(
         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
      *
@@ -135,6 +151,72 @@ internal class Exchange(
         )
     }
 
+    /**
+     * Fetch wire information for an exchange and store it in the database.
+     */
+    internal suspend fun updateWireInfo(record: ExchangeRecord): 
ExchangeRecord {
+        if (record.updateStatus != FetchWire) {
+            throw Error("Unexpected updateStatus: ${record.updateStatus}, 
expected: $FetchWire")
+        }
+        if (record.details == null) throw Error("Invalid exchange state")
+        val wire = Wire.fetch(httpClient, record.baseUrl)
+        // check account signatures
+        for (a in wire.accounts) {
+            val valid = signature.verifyWireAccount(
+                paytoUri = a.paytoUri,
+                signature = a.masterSig,
+                masterPub = record.details.masterPublicKey
+            )
+            if (!valid) throw Error("Exchange wire account signature invalid")
+        }
+        // check fee signatures
+        for (fee in wire.fees) {
+            val wireMethod = fee.key
+            val wireFees = fee.value
+            for (wireFee in wireFees) {
+                val valid = signature.verifyWireFee(
+                    type = wireMethod,
+                    wireFee = wireFee,
+                    masterPub = record.details.masterPublicKey
+                )
+                if (!valid) throw Error("Exchange wire fee signature invalid")
+                checkCurrency(record.details.currency, wireFee.wireFee)
+                checkCurrency(record.details.currency, wireFee.closingFee)
+            }
+        }
+        val wireInfo = ExchangeWireInfo(
+            accounts = wire.accounts.map { ExchangeBankAccount(it.paytoUri) },
+            feesForType = wire.fees
+        )
+        return record.copy(updateStatus = FetchTerms, wireInfo = wireInfo)
+    }
+
+    /**
+     * Fetch wire information for an exchange and store it in the database.
+     */
+    internal suspend fun updateTermsOfService(record: ExchangeRecord): 
ExchangeRecord {
+        if (record.updateStatus != FetchTerms) {
+            throw Error("Unexpected updateStatus: ${record.updateStatus}, 
expected: $FetchTerms")
+        }
+        val response: HttpResponse = 
httpNoJsonClient.get("${record.baseUrl}terms") {
+            accept(ContentType.Text.Plain)
+        }
+        if (response.status != HttpStatusCode.OK) {
+            throw Error("/terms response has unexpected status code 
(${response.status.value})")
+        }
+        val text = response.readText()
+        val eTag = response.headers[HttpHeaders.ETag]
+        return record.copy(updateStatus = FinalizeUpdate, termsOfServiceText = 
text, termsOfServiceLastEtag = eTag)
+    }
+
+    internal fun finalizeUpdate(record: ExchangeRecord): ExchangeRecord {
+        if (record.updateStatus != FinalizeUpdate) {
+            throw Error("Unexpected updateStatus: ${record.updateStatus}, 
expected: $FinalizeUpdate")
+        }
+        // TODO store an event log for this update (exchangeUpdatedEvents)
+        return record.copy(updateStatus = Finished, addComplete = true)
+    }
+
     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
index d882249..38d85ec 100644
--- a/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/ExchangeRecord.kt
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/ExchangeRecord.kt
@@ -18,7 +18,6 @@ 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.
@@ -29,6 +28,11 @@ data class ExchangeRecord(
      */
     val baseUrl: String,
 
+    /**
+     * Did we finish adding the exchange?
+     */
+    val addComplete: Boolean = false,
+
     /**
      * Was the exchange added as a built-in exchange?
      */
@@ -125,22 +129,22 @@ data class ExchangeWireInfo(
     val accounts: List<ExchangeBankAccount>
 )
 
-data class  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")
+    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")
+    object Initial : ExchangeUpdateReason("initial")
+    object Forced : ExchangeUpdateReason("forced")
+    object Scheduled : ExchangeUpdateReason("scheduled")
 }
 
 data class DenominationRecord(
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Keys.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Keys.kt
index c7ea966..016f957 100644
--- a/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Keys.kt
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Keys.kt
@@ -16,6 +16,8 @@
 
 package net.taler.wallet.kotlin.exchange
 
+import io.ktor.client.HttpClient
+import io.ktor.client.request.get
 import kotlinx.serialization.Serializable
 import net.taler.wallet.kotlin.Amount
 import net.taler.wallet.kotlin.Base32Crockford
@@ -61,7 +63,16 @@ internal data class Keys(
      * Protocol version.
      */
     val version: String
-)
+) {
+    companion object {
+        /**
+         * Fetch an exchange's /keys with the given normalized base URL.
+         */
+        suspend fun fetch(httpClient: HttpClient, exchangeBaseUrl: String): 
Keys {
+            return httpClient.get("${exchangeBaseUrl}keys")
+        }
+    }
+}
 
 /**
  * Structure of one exchange signing key in the /keys response.
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Wire.kt 
b/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Wire.kt
new file mode 100644
index 0000000..c8fae88
--- /dev/null
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/exchange/Wire.kt
@@ -0,0 +1,79 @@
+/*
+ * 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 kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.Timestamp
+
+@Serializable
+internal data class Wire(
+    val accounts: List<AccountInfo>,
+    val fees: Map<String, List<WireFee>>
+) {
+    companion object {
+        /**
+         * Fetch an exchange's /wire with the given normalized base URL.
+         */
+        suspend fun fetch(httpClient: HttpClient, exchangeBaseUrl: String): 
Wire {
+            return httpClient.get("${exchangeBaseUrl}wire")
+        }
+    }
+}
+
+@Serializable
+data class AccountInfo(
+    @SerialName("payto_uri")
+    val paytoUri: String,
+    @SerialName("master_sig")
+    val masterSig: String
+)
+
+/**
+ * Wire fees as announced by the exchange.
+ */
+@Serializable
+data class WireFee(
+    /**
+     * Fee for wire transfers.
+     */
+    @SerialName("wire_fee")
+    val wireFee: Amount,
+    /**
+     * Fees to close and refund a reserve.
+     */
+    @SerialName("closing_fee")
+    val closingFee: Amount,
+    /**
+     * Start date of the fee.
+     */
+    @SerialName("start_date")
+    val startStamp: Timestamp,
+    /**
+     * End date of the fee.
+     */
+    @SerialName("end_date")
+    val endStamp: Timestamp,
+    /**
+     * Signature made by the exchange master key.
+     */
+    @SerialName("sig")
+    val signature: String
+)
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 02d9b1d..1306c14 100644
--- a/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/SignatureTest.kt
+++ b/src/commonTest/kotlin/net/taler/wallet/kotlin/crypto/SignatureTest.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.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 net.taler.wallet.kotlin.exchange.WireFee
 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/KeysTest.kt
similarity index 99%
rename from 
src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/ExchangeTest.kt
rename to src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/KeysTest.kt
index e7a2c48..a6b0c98 100644
--- a/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/ExchangeTest.kt
+++ b/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/KeysTest.kt
@@ -24,10 +24,9 @@ import net.taler.wallet.kotlin.runCoroutine
 import kotlin.test.Test
 import kotlin.test.assertEquals
 
-class ExchangeTest {
+class KeysTest {
 
     private val httpClient = getMockHttpClient()
-    private val exchange = Exchange(httpClient = httpClient)
 
     @Test
     fun testFetchKeys() {
@@ -308,7 +307,7 @@ class ExchangeTest {
             }""".trimIndent()
         }
         runCoroutine {
-            val keys = exchange.fetchKeys("https://exchange.test.taler.net/";)
+            val keys = Keys.fetch(httpClient, 
"https://exchange.test.taler.net/";)
             assertEquals(expectedKeys, keys)
         }
     }
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/Utils.kt 
b/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/UpdateTest.kt
similarity index 55%
copy from src/commonMain/kotlin/net/taler/wallet/kotlin/Utils.kt
copy to src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/UpdateTest.kt
index aa3fd91..271dc09 100644
--- a/src/commonMain/kotlin/net/taler/wallet/kotlin/Utils.kt
+++ b/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/UpdateTest.kt
@@ -14,22 +14,24 @@
  * 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 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
+import net.taler.wallet.kotlin.runCoroutine
+import kotlin.test.Ignore
+import kotlin.test.Test
+import kotlin.test.assertTrue
 
-fun getDefaultHttpClient(): HttpClient = HttpClient {
-    install(JsonFeature) {
-        serializer = KotlinxSerializer(Json(getJsonConfiguration()))
+class UpdateTest {
+
+    private val exchange = Exchange()
+
+    @Ignore // live test that requires internet connectivity
+    @Test
+    fun testLiveUpdate() {
+        runCoroutine {
+            val record = 
exchange.updateFromUrl("http://exchange.test.taler.net/";)
+            assertTrue(record.addComplete)
+        }
     }
-}
 
-@OptIn(UnstableDefault::class)
-internal fun getJsonConfiguration() = JsonConfiguration(
-    ignoreUnknownKeys = true
-)
+}
diff --git a/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/WireTest.kt 
b/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/WireTest.kt
new file mode 100644
index 0000000..d09b44b
--- /dev/null
+++ b/src/commonTest/kotlin/net/taler/wallet/kotlin/exchange/WireTest.kt
@@ -0,0 +1,175 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under 
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
+ * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.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 WireTest {
+
+    private val httpClient = getMockHttpClient()
+
+    @Test
+    fun testFetchWire() {
+        val expectedWire = Wire(
+            accounts = listOf(
+                AccountInfo(
+                    paytoUri = 
"payto://x-taler-bank/bank.test.taler.net/Exchange",
+                    masterSig = 
"5DMYMQCEFWG7B21RAX8XQ585V689K8DSSR065F04E2JK6G9AF1WM8EVDCHHBMVWRY3P02EWEE4M6YVKPY6B43H2CPCWHDP13RM1WR10"
+                )
+            ),
+            fees = mapOf(
+                "x-taler-bank" to listOf(
+                    WireFee(
+                        wireFee = Amount.fromJSONString("TESTKUDOS:0.04"),
+                        closingFee = Amount.fromJSONString("TESTKUDOS:0.01"),
+                        startStamp = Timestamp(1577833200000),
+                        endStamp = Timestamp(1609455600000),
+                        signature = 
"9DS6TXPTM8ZBBTJS9VCRSD9FVS56ZY9EVWCF4HDA3Y2DNWSVMGS7XXPWE295EZ3E98KVV1SWDJ11CP0A0VDSRDZTM6RD2RPRG19ZA2G"
+                    ),
+                    WireFee(
+                        wireFee = Amount.fromJSONString("TESTKUDOS:0.04"),
+                        closingFee = Amount.fromJSONString("TESTKUDOS:0.01"),
+                        startStamp = Timestamp(1609455600000),
+                        endStamp = Timestamp(1640991600000),
+                        signature = 
"81852REBNR3ZRQHKQ2FPT6CPACED0MA0CW4V9CPDS3NP2JX6X8BE5YE5W1AKR9XPASEXSKQH6FEXHP7VJB64XWA7FDGH5DKCD3Q700G"
+                    ),
+                    WireFee(
+                        wireFee = Amount.fromJSONString("TESTKUDOS:0.05"),
+                        closingFee = Amount.fromJSONString("TESTKUDOS:0.01"),
+                        startStamp = Timestamp(1640991600000),
+                        endStamp = Timestamp(1672527600000),
+                        signature = 
"REYMSGH4QBNF339Q8TD5VJMMY6BV7KFTC1Y69YD69Y9E8Z5HXGNAKCQKT490MHBSF48894YADT1ATGDMSRZAQJJFVXF6HX9JEYDT61G"
+                    ),
+                    WireFee(
+                        wireFee = Amount.fromJSONString("TESTKUDOS:0.06"),
+                        closingFee = Amount.fromJSONString("TESTKUDOS:0.01"),
+                        startStamp = Timestamp(1672527600000),
+                        endStamp = Timestamp(1704063600000),
+                        signature = 
"BXB47D936XT7XDHGA3VA3461CY1GMQWFPVMSBY01N5SN6PBCGYRS8HSY19FJ0P5HVX3TGS9TAHY9X7RP4BQHPM4DMMS30TJ0EKG5A3G"
+                    ),
+                    WireFee(
+                        wireFee = Amount.fromJSONString("TESTKUDOS:0.07"),
+                        closingFee = Amount.fromJSONString("TESTKUDOS:0.01"),
+                        startStamp = Timestamp(1704063600000),
+                        endStamp = Timestamp(1735686000000),
+                        signature = 
"RFF1KV54BH9TJ8KBE8YEY8DM0R468PZYGW82G16P97EDHNN3XZVN4KK5E9CBZQ730WPJT0RKR3TTYPBWGTR0YQ064XZZDHJHHZN1418"
+                    ),
+                    WireFee(
+                        wireFee = Amount.fromJSONString("TESTKUDOS:0.08"),
+                        closingFee = Amount.fromJSONString("TESTKUDOS:0.01"),
+                        startStamp = Timestamp(1735686000000),
+                        endStamp = Timestamp(1767222000000),
+                        signature = 
"Q89VKJ54MF3DVG0NKK4N6VB96NCT0PRSTNBJ0SSB42SQTHB10JC68XJSDM6PRBBPEJ8CHDE9VVRZWW20VFSZFDTRA332JKDSBBFWY1G"
+                    )
+                )
+            )
+        )
+        httpClient.giveJsonResponse("https://exchange.test.taler.net/wire";) {
+            """{
+              "accounts": [
+                {
+                  "payto_uri": 
"payto://x-taler-bank/bank.test.taler.net/Exchange",
+                  "master_sig": 
"5DMYMQCEFWG7B21RAX8XQ585V689K8DSSR065F04E2JK6G9AF1WM8EVDCHHBMVWRY3P02EWEE4M6YVKPY6B43H2CPCWHDP13RM1WR10"
+                }
+              ],
+              "fees": {
+                "x-taler-bank": [
+                  {
+                    "wire_fee": "TESTKUDOS:0.04",
+                    "closing_fee": "TESTKUDOS:0.01",
+                    "start_date": {
+                      "t_ms": 1577833200000
+                    },
+                    "end_date": {
+                      "t_ms": 1609455600000
+                    },
+                    "sig": 
"9DS6TXPTM8ZBBTJS9VCRSD9FVS56ZY9EVWCF4HDA3Y2DNWSVMGS7XXPWE295EZ3E98KVV1SWDJ11CP0A0VDSRDZTM6RD2RPRG19ZA2G"
+                  },
+                  {
+                    "wire_fee": "TESTKUDOS:0.04",
+                    "closing_fee": "TESTKUDOS:0.01",
+                    "start_date": {
+                      "t_ms": 1609455600000
+                    },
+                    "end_date": {
+                      "t_ms": 1640991600000
+                    },
+                    "sig": 
"81852REBNR3ZRQHKQ2FPT6CPACED0MA0CW4V9CPDS3NP2JX6X8BE5YE5W1AKR9XPASEXSKQH6FEXHP7VJB64XWA7FDGH5DKCD3Q700G"
+                  },
+                  {
+                    "wire_fee": "TESTKUDOS:0.05",
+                    "closing_fee": "TESTKUDOS:0.01",
+                    "start_date": {
+                      "t_ms": 1640991600000
+                    },
+                    "end_date": {
+                      "t_ms": 1672527600000
+                    },
+                    "sig": 
"REYMSGH4QBNF339Q8TD5VJMMY6BV7KFTC1Y69YD69Y9E8Z5HXGNAKCQKT490MHBSF48894YADT1ATGDMSRZAQJJFVXF6HX9JEYDT61G"
+                  },
+                  {
+                    "wire_fee": "TESTKUDOS:0.06",
+                    "closing_fee": "TESTKUDOS:0.01",
+                    "start_date": {
+                      "t_ms": 1672527600000
+                    },
+                    "end_date": {
+                      "t_ms": 1704063600000
+                    },
+                    "sig": 
"BXB47D936XT7XDHGA3VA3461CY1GMQWFPVMSBY01N5SN6PBCGYRS8HSY19FJ0P5HVX3TGS9TAHY9X7RP4BQHPM4DMMS30TJ0EKG5A3G"
+                  },
+                  {
+                    "wire_fee": "TESTKUDOS:0.07",
+                    "closing_fee": "TESTKUDOS:0.01",
+                    "start_date": {
+                      "t_ms": 1704063600000
+                    },
+                    "end_date": {
+                      "t_ms": 1735686000000
+                    },
+                    "sig": 
"RFF1KV54BH9TJ8KBE8YEY8DM0R468PZYGW82G16P97EDHNN3XZVN4KK5E9CBZQ730WPJT0RKR3TTYPBWGTR0YQ064XZZDHJHHZN1418"
+                  },
+                  {
+                    "wire_fee": "TESTKUDOS:0.08",
+                    "closing_fee": "TESTKUDOS:0.01",
+                    "start_date": {
+                      "t_ms": 1735686000000
+                    },
+                    "end_date": {
+                      "t_ms": 1767222000000
+                    },
+                    "sig": 
"Q89VKJ54MF3DVG0NKK4N6VB96NCT0PRSTNBJ0SSB42SQTHB10JC68XJSDM6PRBBPEJ8CHDE9VVRZWW20VFSZFDTRA332JKDSBBFWY1G"
+                  }
+                ]
+              },
+              "master_public_key": 
"DY95EXAHQ2BKM2WK9YHZHYG1R7PPMMJPY14FNGP662DAKE35AKQG"
+            }""".trimIndent()
+        }
+
+        runCoroutine {
+            val wire = Wire.fetch(httpClient, 
"https://exchange.test.taler.net/";)
+            assertEquals(expectedWire, wire)
+        }
+    }
+
+}

-- 
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]