diff options
Diffstat (limited to 'wallet/src/commonMain/kotlin/net/taler/lib/wallet/exchange')
6 files changed, 879 insertions, 0 deletions
diff --git a/wallet/src/commonMain/kotlin/net/taler/lib/wallet/exchange/Auditor.kt b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/exchange/Auditor.kt new file mode 100644 index 0000000..248da8d --- /dev/null +++ b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/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.lib.wallet.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/wallet/src/commonMain/kotlin/net/taler/lib/wallet/exchange/Denomination.kt b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/exchange/Denomination.kt new file mode 100644 index 0000000..fca9e3f --- /dev/null +++ b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/exchange/Denomination.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.lib.wallet.exchange + +import kotlinx.serialization.Serializable +import net.taler.lib.common.Amount +import net.taler.lib.common.Duration +import net.taler.lib.common.Timestamp +import net.taler.lib.wallet.Base32Crockford +import net.taler.lib.wallet.exchange.DenominationStatus.Unverified +import net.taler.lib.wallet.exchange.DenominationStatus.VerifiedGood + +/** + * 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 + ) +} + +enum class DenominationStatus { + /** + * Verification was delayed. + */ + Unverified, + + /** + * Verified as valid. + */ + VerifiedGood, + + /** + * Verified as invalid. + */ + VerifiedBad +} + +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 +) { + fun isWithdrawable(now: Timestamp = Timestamp.now()): Boolean { + if (isRevoked) return false // can not use revoked denomination + if (status != Unverified && status != VerifiedGood) return false // verified to be bad + if (now < stampStart) return false // denomination has not yet started + val lastPossibleWithdraw = stampExpireWithdraw - Duration(50 * 1000) + if ((lastPossibleWithdraw - now).ms == 0L) return false // denomination has expired + return true + } +} + +data class DenominationSelectionInfo( + val totalCoinValue: Amount, + val totalWithdrawCost: Amount, + val selectedDenominations: List<SelectedDenomination> +) { + fun getEarliestDepositExpiry(): Timestamp { + if (selectedDenominations.isEmpty()) return Timestamp( + Timestamp.NEVER + ) + var earliest = selectedDenominations[0].denominationRecord.stampExpireDeposit + for (i in 1 until selectedDenominations.size) { + val stampExpireDeposit = selectedDenominations[i].denominationRecord.stampExpireDeposit + if (stampExpireDeposit < earliest) earliest = stampExpireDeposit + } + return earliest + } +} + +data class SelectedDenomination(val count: Int, val denominationRecord: DenominationRecord) diff --git a/wallet/src/commonMain/kotlin/net/taler/lib/wallet/exchange/Exchange.kt b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/exchange/Exchange.kt new file mode 100644 index 0000000..4d89cd6 --- /dev/null +++ b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/exchange/Exchange.kt @@ -0,0 +1,262 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +package net.taler.lib.wallet.exchange + +import 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.lib.common.Amount +import net.taler.lib.common.Timestamp +import net.taler.lib.common.Version +import net.taler.lib.wallet.Base32Crockford +import net.taler.lib.wallet.Db +import net.taler.lib.wallet.DbFactory +import net.taler.lib.wallet.crypto.Crypto +import net.taler.lib.wallet.crypto.CryptoFactory +import net.taler.lib.wallet.crypto.Signature +import net.taler.lib.wallet.exchange.DenominationStatus.Unverified +import net.taler.lib.wallet.exchange.ExchangeUpdateReason.Initial +import net.taler.lib.wallet.exchange.ExchangeUpdateStatus.FetchKeys +import net.taler.lib.wallet.exchange.ExchangeUpdateStatus.FetchTerms +import net.taler.lib.wallet.exchange.ExchangeUpdateStatus.FetchWire +import net.taler.lib.wallet.exchange.ExchangeUpdateStatus.FinalizeUpdate +import net.taler.lib.wallet.exchange.ExchangeUpdateStatus.Finished +import net.taler.lib.wallet.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() +) { + + companion object { + private val PROTOCOL_VERSION = Version(8, 0, 0) + fun getVersionMatch(version: String) = PROTOCOL_VERSION.compare(Version.parse(version)) + 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 + } + } + + /** + * 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) + var record = db.getExchangeByBaseUrl(url) ?: ExchangeRecord( + baseUrl = url, + timestampAdded = now, + updateStatus = FetchKeys, + updateStarted = now, + updateReason = Initial + ).also { db.put(it) } + 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 + } + + /** + * 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 = Keys.fetch(httpClient, 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 = getVersionMatch(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 + } + + /** + * 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 + ) + } + + /** + * 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}") + } + +} + + +data class ExchangeListItem( + val exchangeBaseUrl: String, + val currency: String, + val paytoUris: List<String> +) { + companion object { + fun fromExchangeRecord(exchange: ExchangeRecord): ExchangeListItem? { + return if (exchange.details == null || exchange.wireInfo == null) null + else ExchangeListItem( + exchangeBaseUrl = exchange.baseUrl, + currency = exchange.details.currency, + paytoUris = exchange.wireInfo.accounts.map { + it.paytoUri + } + ) + } + } +} + +data class GetExchangeTosResult( + /** + * Markdown version of the current ToS. + */ + val tos: String, + + /** + * Version tag of the current ToS. + */ + val currentEtag: String, + + /** + * Version tag of the last ToS that the user has accepted, if any. + */ + val acceptedEtag: String? = null +) diff --git a/wallet/src/commonMain/kotlin/net/taler/lib/wallet/exchange/ExchangeRecord.kt b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/exchange/ExchangeRecord.kt new file mode 100644 index 0000000..bb8bbd1 --- /dev/null +++ b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/exchange/ExchangeRecord.kt @@ -0,0 +1,151 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +package net.taler.lib.wallet.exchange + +import net.taler.lib.common.Timestamp + +/** + * Exchange record as stored in the wallet's database. + */ +data class ExchangeRecord( + /** + * Base url of the exchange. + */ + val baseUrl: String, + + /** + * Did we finish adding the exchange? + */ + val addComplete: Boolean = false, + + /** + * 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" } + } + + val termsOfServiceAccepted: Boolean + get() = termsOfServiceAcceptedTimestamp != null && termsOfServiceAcceptedEtag == termsOfServiceLastEtag +} + +/** + * 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> +) + +// TODO is this class needed? +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") +} diff --git a/wallet/src/commonMain/kotlin/net/taler/lib/wallet/exchange/Keys.kt b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/exchange/Keys.kt new file mode 100644 index 0000000..12b29db --- /dev/null +++ b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/exchange/Keys.kt @@ -0,0 +1,97 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +package net.taler.lib.wallet.exchange + +import io.ktor.client.HttpClient +import io.ktor.client.request.get +import kotlinx.serialization.Serializable +import net.taler.lib.common.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 +) { + 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. + */ +@Serializable +data class SigningKey( + val stamp_start: Timestamp, + val stamp_expire: Timestamp, + val stamp_end: Timestamp, + val key: String, + val master_sig: String +) + +/** + * 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/wallet/src/commonMain/kotlin/net/taler/lib/wallet/exchange/Wire.kt b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/exchange/Wire.kt new file mode 100644 index 0000000..0dca4dd --- /dev/null +++ b/wallet/src/commonMain/kotlin/net/taler/lib/wallet/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.lib.wallet.exchange + +import io.ktor.client.HttpClient +import io.ktor.client.request.get +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import net.taler.lib.common.Amount +import net.taler.lib.common.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 +) |