summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorTorsten Grote <t@grobox.de>2020-07-14 16:56:56 -0300
committerTorsten Grote <t@grobox.de>2020-07-14 16:56:56 -0300
commit561eaa710b00a1f49a089aeb8aa5cab8d688f453 (patch)
tree9a8cd976eb3bc7fa2c9f299003d47baf81da0a31 /src
parent5f03889e649271347229af777ec1a025a6210f23 (diff)
downloadwallet-kotlin-561eaa710b00a1f49a089aeb8aa5cab8d688f453.tar.gz
wallet-kotlin-561eaa710b00a1f49a089aeb8aa5cab8d688f453.tar.bz2
wallet-kotlin-561eaa710b00a1f49a089aeb8aa5cab8d688f453.zip
Select denominations for withdrawal (with tests)
Diffstat (limited to 'src')
-rw-r--r--src/commonMain/kotlin/net/taler/wallet/kotlin/Db.kt18
-rw-r--r--src/commonMain/kotlin/net/taler/wallet/kotlin/Time.kt (renamed from src/commonMain/kotlin/net/taler/wallet/kotlin/Timestamp.kt)42
-rw-r--r--src/commonMain/kotlin/net/taler/wallet/kotlin/operations/Withdraw.kt69
-rw-r--r--src/commonTest/kotlin/net/taler/wallet/kotlin/DbTest.kt37
-rw-r--r--src/commonTest/kotlin/net/taler/wallet/kotlin/Denominations.kt117
-rw-r--r--src/commonTest/kotlin/net/taler/wallet/kotlin/operations/WithdrawTest.kt90
6 files changed, 364 insertions, 9 deletions
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/Db.kt b/src/commonMain/kotlin/net/taler/wallet/kotlin/Db.kt
index 14a2f98..3a5ecd6 100644
--- a/src/commonMain/kotlin/net/taler/wallet/kotlin/Db.kt
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/Db.kt
@@ -27,6 +27,7 @@ internal interface Db {
suspend fun getExchangeByBaseUrl(baseUrl: String): ExchangeRecord?
suspend fun deleteExchangeByBaseUrl(baseUrl: String)
suspend fun put(denomination: DenominationRecord)
+ suspend fun getDenominationsByBaseUrl(baseUrl: String): List<DenominationRecord>
suspend fun <T> transaction(function: suspend Db.() -> T): T
}
@@ -38,7 +39,7 @@ internal class FakeDb : Db {
private data class Data(
val exchanges: HashMap<String, ExchangeRecord> = HashMap(),
- val denominations: HashMap<String, DenominationRecord> = HashMap()
+ val denominations: HashMap<String, ArrayList<DenominationRecord>> = HashMap()
)
private var data = Data()
@@ -60,8 +61,19 @@ internal class FakeDb : Db {
data.exchanges.remove(baseUrl)
}
- override suspend fun put(denomination: DenominationRecord) {
- data.denominations[denomination.exchangeBaseUrl] = denomination
+ override suspend fun put(denomination: DenominationRecord): Unit = mutex.withLock("transaction") {
+ val list = data.denominations[denomination.exchangeBaseUrl] ?: {
+ val newList = ArrayList<DenominationRecord>()
+ data.denominations[denomination.exchangeBaseUrl] = newList
+ newList
+ }()
+ val index = list.indexOfFirst { it.denomPub == denomination.denomPub }
+ if (index == -1) list.add(denomination)
+ else list[index] = denomination
+ }
+
+ override suspend fun getDenominationsByBaseUrl(baseUrl: String): List<DenominationRecord> {
+ return data.denominations[baseUrl] ?: emptyList()
}
override suspend fun <T> transaction(function: suspend Db.() -> T): T = mutex.withLock("transaction") {
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/Timestamp.kt b/src/commonMain/kotlin/net/taler/wallet/kotlin/Time.kt
index b5c850f..ebf445e 100644
--- a/src/commonMain/kotlin/net/taler/wallet/kotlin/Timestamp.kt
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/Time.kt
@@ -19,16 +19,18 @@ package net.taler.wallet.kotlin
import com.soywiz.klock.DateTime
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
+import net.taler.wallet.kotlin.Duration.Companion.FOREVER
import net.taler.wallet.kotlin.crypto.CryptoImpl.Companion.toByteArray
+import kotlin.math.max
@Serializable
data class Timestamp(
@SerialName("t_ms")
val ms: Long
-) {
+) : Comparable<Timestamp> {
companion object {
- const val NEVER: Long = -1
+ const val NEVER: Long = -1 // TODO or UINT64_MAX?
fun now(): Timestamp = Timestamp(DateTime.now().unixMillisLong)
}
@@ -44,4 +46,40 @@ data class Timestamp(
(truncateSeconds().ms * 1000L).toByteArray().copyInto(this)
}
+ operator fun minus(other: Timestamp): Duration = when {
+ ms == NEVER -> Duration(FOREVER)
+ other.ms == NEVER -> throw Error("Invalid argument for timestamp comparision")
+ ms < other.ms -> Duration(0)
+ else -> Duration(ms - other.ms)
+ }
+
+ operator fun minus(other: Duration): Timestamp = when {
+ ms == NEVER -> this
+ other.ms == FOREVER -> Timestamp(0)
+ else -> Timestamp(max(0, ms - other.ms))
+ }
+
+ override fun compareTo(other: Timestamp): Int {
+ return if (ms == NEVER) {
+ if (other.ms == NEVER) 0
+ else 1
+ } else {
+ if (other.ms == NEVER) -1
+ else ms.compareTo(other.ms)
+ }
+ }
+
+}
+
+@Serializable
+data class Duration(
+ /**
+ * Duration in milliseconds.
+ */
+ @SerialName("d_ms")
+ val ms: Long
+) {
+ companion object {
+ const val FOREVER: Long = -1
+ }
}
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 f7064bf..b73688c 100644
--- a/src/commonMain/kotlin/net/taler/wallet/kotlin/operations/Withdraw.kt
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/operations/Withdraw.kt
@@ -21,10 +21,25 @@ 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.Db
+import net.taler.wallet.kotlin.DbFactory
+import net.taler.wallet.kotlin.Duration
import net.taler.wallet.kotlin.TalerUri.parseWithdrawUri
+import net.taler.wallet.kotlin.Timestamp
+import net.taler.wallet.kotlin.crypto.CryptoFactory
+import net.taler.wallet.kotlin.crypto.Refresh.DenominationSelectionInfo
+import net.taler.wallet.kotlin.crypto.Refresh.SelectedDenomination
+import net.taler.wallet.kotlin.crypto.Signature
+import net.taler.wallet.kotlin.exchange.DenominationRecord
+import net.taler.wallet.kotlin.exchange.DenominationStatus.Unverified
+import net.taler.wallet.kotlin.exchange.DenominationStatus.VerifiedGood
import net.taler.wallet.kotlin.getDefaultHttpClient
-class Withdraw(private val httpClient: HttpClient = getDefaultHttpClient()) {
+internal class Withdraw(
+ private val httpClient: HttpClient = getDefaultHttpClient(),
+ private val db: Db = DbFactory().openDb(),
+ private val signature: Signature = Signature(CryptoFactory.getCrypto())
+) {
data class BankDetails(
val amount: Amount,
@@ -38,7 +53,7 @@ class Withdraw(private val httpClient: HttpClient = getDefaultHttpClient()) {
)
@Serializable
- private data class Response(
+ data class Response(
@SerialName("selection_done")
val selectionDone: Boolean,
@SerialName("transfer_done")
@@ -71,4 +86,54 @@ class Withdraw(private val httpClient: HttpClient = getDefaultHttpClient()) {
return response.toBankDetails(url)
}
+ /**
+ * Get a list of denominations (with repetitions possible)
+ * whose total value is as close as possible to the available amount, but never larger.
+ *
+ * Note that this algorithm does not try to optimize withdrawal fees.
+ */
+ fun getDenominationSelection(amount: Amount, denoms: List<DenominationRecord>): DenominationSelectionInfo {
+ val selectedDenominations = ArrayList<SelectedDenomination>()
+ var totalCoinValue = Amount.zero(amount.currency)
+ var totalWithdrawCost = Amount.zero(amount.currency)
+
+ // denominations need to be sorted, so we try the highest ones first
+ val denominations = denoms.filter(this::isWithdrawableDenomination).sortedByDescending { it.value }
+ var remainingAmount = amount.copy()
+ val zero = Amount.zero(amount.currency)
+ for (d in denominations) {
+ var count = 0
+ val totalCost = d.value + d.feeWithdraw
+ // keep adding this denomination as long as its total cost fits into remaining amount
+ while (remainingAmount >= totalCost) {
+ remainingAmount -= totalCost
+ count++
+ }
+ // calculate new totals based on count-many added denominations
+ if (count > 0) {
+ totalCoinValue += d.value * count
+ totalWithdrawCost += totalCost * count
+ selectedDenominations.add(SelectedDenomination(count, d))
+ }
+ // stop early if nothing is remaining
+ if (remainingAmount == zero) break
+ }
+ return DenominationSelectionInfo(
+ selectedDenominations = selectedDenominations,
+ totalCoinValue = totalCoinValue,
+ totalWithdrawCost = totalWithdrawCost
+ )
+ }
+
+ // TODO move into DenominationRecord?
+ fun isWithdrawableDenomination(d: DenominationRecord): Boolean {
+ if (d.isRevoked) return false // can not use revoked denomination
+ if (d.status != Unverified && d.status != VerifiedGood) return false // verified to be bad
+ val now = Timestamp.now()
+ if (now < d.stampStart) return false // denomination has not yet started
+ val lastPossibleWithdraw = d.stampExpireWithdraw - Duration(50 * 1000)
+ if ((lastPossibleWithdraw - now).ms == 0L) return false // denomination has expired
+ return true
+ }
+
}
diff --git a/src/commonTest/kotlin/net/taler/wallet/kotlin/DbTest.kt b/src/commonTest/kotlin/net/taler/wallet/kotlin/DbTest.kt
index 7acc2a5..32a8d88 100644
--- a/src/commonTest/kotlin/net/taler/wallet/kotlin/DbTest.kt
+++ b/src/commonTest/kotlin/net/taler/wallet/kotlin/DbTest.kt
@@ -16,6 +16,10 @@
package net.taler.wallet.kotlin
+import net.taler.wallet.kotlin.Denominations.denomination10
+import net.taler.wallet.kotlin.Denominations.denomination5
+import net.taler.wallet.kotlin.exchange.DenominationStatus.Unverified
+import net.taler.wallet.kotlin.exchange.DenominationStatus.VerifiedGood
import net.taler.wallet.kotlin.exchange.ExchangeRecord
import net.taler.wallet.kotlin.exchange.ExchangeUpdateReason.Initial
import net.taler.wallet.kotlin.exchange.ExchangeUpdateStatus.FetchKeys
@@ -42,7 +46,7 @@ class DbTest {
)
@Test
- fun test() = runCoroutine {
+ fun testExchanges() = runCoroutine {
val db = dbFactory.openDb()
var exchanges = db.listExchanges()
assertEquals(0, exchanges.size)
@@ -64,4 +68,33 @@ class DbTest {
assertEquals(exchange2, exchanges[0])
}
-} \ No newline at end of file
+ @Test
+ fun testDenominations() = runCoroutine {
+ val db = dbFactory.openDb()
+ val url = denomination10.exchangeBaseUrl
+
+ // no denominations should be in DB
+ var denominations = db.getDenominationsByBaseUrl(url)
+ assertEquals(0, denominations.size)
+
+ // add a denomination and check that it is really there
+ db.put(denomination10)
+ denominations = db.getDenominationsByBaseUrl(url)
+ assertEquals(1, denominations.size)
+ assertEquals(denomination10, denominations[0])
+
+ // modify existing denomination and check that it gets updated
+ assertEquals(Unverified, denomination10.status)
+ val newDenomination = denomination10.copy(status = VerifiedGood)
+ db.put(newDenomination)
+ denominations = db.getDenominationsByBaseUrl(url)
+ assertEquals(1, denominations.size)
+ assertEquals(newDenomination, denominations[0])
+
+ // add one more denomination
+ db.put(denomination5)
+ denominations = db.getDenominationsByBaseUrl(url)
+ assertEquals(2, denominations.size)
+ }
+
+}
diff --git a/src/commonTest/kotlin/net/taler/wallet/kotlin/Denominations.kt b/src/commonTest/kotlin/net/taler/wallet/kotlin/Denominations.kt
new file mode 100644
index 0000000..2049cf4
--- /dev/null
+++ b/src/commonTest/kotlin/net/taler/wallet/kotlin/Denominations.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.DenominationStatus.Unverified
+import net.taler.wallet.kotlin.exchange.DenominationStatus.VerifiedGood
+
+object Denominations {
+
+ private val validStart = Timestamp.now()
+ private val validExpireWithdraw = Timestamp(Timestamp.now().ms + 1000L * 60L * 60L * 24L * 365L)
+ val denomination10 = DenominationRecord(
+ denomPub = "020000X0X3G1EBB22XJ4HD6R8545R294TMCMA13ZRW7R101KJENFGTNTZSPGA0XP898FJEVHY4SJTC0SM264K0Y7Q6E24S35JSFZXD6VAJDJX8FCERBTNFV5DZR8V4GV7DAD062CPZBEVGNDEJQCTHVFJP84QWVPYJFNZSS3EJEK3WKJVG5EM3X2JPM1C97AB26VSZXWNYNC2CNJN7KG2001",
+ denomPubHash = "7GB2YKDWKQ3DS2GA9XCVXVPMPJQA9M7Q0DFDHCX5M71J4E2PEHAJK3QF3KTJTWJA33KG0BX6XX0TTRMMZ8CEBM4GSE2N5FSV7GYRGH0",
+ exchangeBaseUrl = "https://example.org",
+ feeDeposit = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeRefresh = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ feeRefund = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeWithdraw = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ isOffered = true,
+ isRevoked = false,
+ masterSig = "N9ADAGY43VTDA000NMW9NFKY8HBTTNPEWP9W1A1ATCAFVA1A9F1MD7HAJ6M4QQ3SVYJXCV1S9Z1ZMKXP9YKD3PFGNK0VQD1ZZ502420",
+ stampExpireDeposit = validExpireWithdraw,
+ stampExpireLegal = validExpireWithdraw,
+ stampExpireWithdraw = validExpireWithdraw,
+ stampStart = validStart,
+ status = Unverified,
+ value = Amount(currency = "TESTKUDOS", fraction = 0, value = 10)
+ )
+ val denomination8 = denomination10.copy(
+ denomPub = "020000X61XTJPQHC9Z3GEF8BD0YTPGH819V6YQDW7MZ74PH9GC2WZGH1YPBHB6F6AJG7C91G19KTSSP9GA64SFZ26T06QK6XEQNREEFV6EMVRCQRYXV3YVA5ANJQRFJVMQG3DQG6Q0ANQWRBZGZSX43MNJQ0NGZY2X1VHJ0351RC83RMPYV1JFSZ2J1JZ2MN5AJF6QMCBBJN0V5TF3EG2001",
+ denomPubHash = "M496WWEBC8KN4MYB73V78F4DCXXMFWR2X33GWDA92X98MC04E120H9NVNKN70J3NP7ZZ91BE7CX65NYHQEG5EQK5Y78E7ZE3YV8E868",
+ feeDeposit = Amount(currency = "TESTKUDOS", fraction = 2000000, value = 0),
+ feeRefresh = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ feeRefund = Amount(currency = "TESTKUDOS", fraction = 4000000, value = 0),
+ feeWithdraw = Amount(currency = "TESTKUDOS", fraction = 5000000, value = 0),
+ masterSig = "GNWDZNQHFECF2SPVWZ5D0KV7YEGJ0J86ND815B512ZSBNKSCSN7PCKE5GJTXV0WZNS3AYTDJYER3W1HXSST4TMBMAR3FY3ETRNRS20G",
+ value = Amount(currency = "TESTKUDOS", fraction = 0, value = 8)
+ )
+ val denomination5 = denomination10.copy(
+ denomPub = "020000XFF9HD3GJXVA9ARQD76BW2G9K65F6CVDPWSRYVE5HY7EVVBK1PK1XX2HE2P3BA3A9MJT9ESY1XNKK7TTF8DRE33C22EHPNNBPPQC1D1MHEE3YJHNF8PG0W6DTE406R6VHCZ0VHEE5HNTEPWMAHJ5J0VVY1ESGAXWE1SGSY82FCQARWV45MNGYZMBN2M55CG3SQXJ8STRPHEM1G2001",
+ denomPubHash = "4HKTYHDDCJXJW7Z215E3TFQWBRYKZA4VS41CZKBE8HH1T97FV4X9WSM62Q7WEZTZX3Y60T4Y8M4R0YYA3PVSEND1MZCJBTD4QZDMCJR",
+ feeDeposit = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeRefresh = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ feeRefund = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeWithdraw = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ masterSig = "0TBJSTZTWMKBT71ET1B7RVFB1VT84E10G3SENPSMPRYTFBH07WSDS5VA9PW5T0EYEABDKGAYDRQ0XA62GYQXZ4SBV2CTHHFY0TFNC18",
+ value = Amount(currency = "TESTKUDOS", fraction = 0, value = 5)
+ )
+ val denomination4 = denomination10.copy(
+ denomPub = "020000Y3JVXB3XTC7JTK3CYEMSXHEX8956DN9B4Z6WB3J1H8D5Y3BVTY8EE5BC5JSRJKM0VAK6BXSHVRGQ6N43BF132DPNSJDG4CD8JA4A856HVCNFSNP0DY21TJYN8GJ36R1T0VKTVH2SRMT4QN1QQZC0VQ5ZV2EJJMCSVYVKV8MZC9NG5K9PGNQKBV64E34H1K9RSFEJE7306ZH07G2001",
+ denomPubHash = "XRKJ5750TW2ZNQYGQ87TESYF2X942RBCG7EKXB8QKMFGB433EK3567SDSE9EFNBYTEH3PHPTK22V8XNSJ090DHYX8EW9BE1Q8JCZW7G",
+ feeDeposit = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ feeRefresh = Amount(currency = "TESTKUDOS", fraction = 4000000, value = 0),
+ feeRefund = Amount(currency = "TESTKUDOS", fraction = 2000000, value = 0),
+ feeWithdraw = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ masterSig = "VFKWHPFNEKD36A0A0TWVTEN98P4TCVBTRMJFJF1E1Q2578J9DXAY53Y7Q2D3BX01WKPM7QBCVF9WBMB4GTXVFP08QPZP086RV9PAP2G",
+ value = Amount(currency = "TESTKUDOS", fraction = 0, value = 4)
+ )
+ val denomination2 = denomination10.copy(
+ denomPub = "020000XB8Y8WVSBM0WBY6NMS0MTNN71PEA6KGKTV5FF27RRR8V1V0GPWYAFDMD2XT6E265QPNH079KM25ZZ97NGFRDVHW0Y7JPWA9C8MJY8DB7APYBRMD9XYA0N1569VFW2NPP4FGQJ865RVE94F35NSG0M4W80CG6WXXWW1ERRM7F2AGRZE9FS049183ANEDFB7QN4H62GDWS7039GG2001",
+ denomPubHash = "CTH34KGWJKQ3C264DQJCRPX97G5SWGWQQ8EMBXVH4DKGTPJ78ZA5CWZB9SDT538Z6S118VQ3RNX3CVC1YXEFN0PXR0D896MDNCGZW3G",
+ feeDeposit = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ feeRefresh = Amount(currency = "TESTKUDOS", fraction = 4000000, value = 0),
+ feeRefund = Amount(currency = "TESTKUDOS", fraction = 2000000, value = 0),
+ feeWithdraw = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ masterSig = "BTZYYEYBZK1EJJ47MQHEY7Q8ARBK4PT0N92TYEEPEZ54P0NTN6FT50AGKCCQCWQ8J74D8MTZFAX3PRDWSH30JPVREAQKGVGEVYXN00G",
+ value = Amount(currency = "TESTKUDOS", fraction = 0, value = 2)
+ )
+ val denomination1 = denomination10.copy(
+ denomPub = "020000XBEVGYETENRYN30AMTT006NPFG5QNHHJXESR9YRZ0J1Y0V00ASMSK7SGKBZJW1GKW05AGNXZDGHAT8RP5M87H2QZTCZHNWW0W4SFWSHWEMRK57DQ3Z7EYQ3XMFX8E2QNZNT9TB4J0MMDP833Y0K7RCQ3X5A584ZBQ9M0T03KTF1QTS2Z7J0BRCEVMK7CCM5WYCAVDPP44Z23HG2001",
+ denomPubHash = "KHRCTNBZHY653V3WZBNMTGGM3MSS471EZ4F6X32HJMN17A47WBBM5WHCRNK8F27KB6Q45YMEN832BKYNKBK1GCRXKDP0XTYC3CYTRWR",
+ feeDeposit = Amount(currency = "TESTKUDOS", fraction = 2000000, value = 0),
+ feeRefresh = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ feeRefund = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeWithdraw = Amount(currency = "TESTKUDOS", fraction = 2000000, value = 0),
+ masterSig = "X029BKSB9PRKE831VDV1X3ND441E0NNQCDJ2S9NKPG7F17R0Y6QCGKWCSHCHE96NW5XS13RK880H90ZSFY7HNJ17F4NSR1EM5FR2M10",
+ value = Amount(currency = "TESTKUDOS", fraction = 0, value = 1)
+ )
+ val denomination0d1 = denomination10.copy(
+ denomPub = "020000YVVE91KXE9S8JV5P4PWZJWBJD39Y6S00FA3CV6RP77A8PTKJWNE3W0W8ZHDR3CSKA17FT56PMC97RWV3RNG753B1WQYXEWNJA76GD5T2PA33BN08CQ07KP4M2K9R6A3N9VD6W8D3DK55W18Y7TBKAHCJBQ3AZ50VHSF1ZPM2XVJ238SKK1379WNHMK4VDJQ35H20QSF3GPWPKG2001",
+ denomPubHash = "C26V15X3BESS1CZCSP4BNSRJ8BK8DSGFHNB0WG1GZ6ZF6FR37WGSVEQ85A61X6Z103P8MY7XGQZ60VAX78V3E5GERWJTJAP0Q5QB7A8",
+ feeDeposit = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeRefresh = Amount(currency = "TESTKUDOS", fraction = 3000000, value = 0),
+ feeRefund = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeWithdraw = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ masterSig = "JSJXWJXN4532C07CBH4DZZ6YC5S3TPH3H8FG8RMGX0Z647GX2NYEK02NRN3C4GDS9Q0ZT6QE7T8EGYGEF9RZ9FCV65TZWC1ZP83DT1R",
+ value = Amount(currency = "TESTKUDOS", fraction = 10000000, value = 0)
+ )
+ val denomination0d01 = denomination10.copy(
+ denomPub = "020000X67SMNYMCR1HFZW4KEATXGXRA983JE5VW1JE4108XB7Z40BTJC1MV59Y9K4Y35E3MPPF73BJQA3KVT0FBT89R6ZYNZ77TSC5DMFV5E55DT4JB4S9K4C2V7GM8Z8QZ0KMCH0YK4PAX1WSKCEQFNRVKD3VH9WTVQ0CNV7Z1JVRHBKCSZNX62TRQ6JRZ05JEANT3C41SQ6MKSQZKG2001",
+ denomPubHash = "VS0E7ABRZ9NZMTDAHVKQ4QH1S1Q2PYWXFXKF0P84VSHCDBM4S6QK1G1D495TABN4AXVX049P7JESNRRQVHW37BNER4XKZSQT3XA61DG",
+ feeDeposit = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeRefresh = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeRefund = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ feeWithdraw = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0),
+ masterSig = "CF3638K8QBG91D66JZRRGWV8J53A0ZGBYJDMKYK293DNAA2ASM1M346C0YG9V7HWH8E2A7FPVR0HH2QE7DHD9GW0EN19VSER4S5F23R",
+ status = VerifiedGood,
+ value = Amount(currency = "TESTKUDOS", fraction = 1000000, value = 0)
+ )
+
+}
diff --git a/src/commonTest/kotlin/net/taler/wallet/kotlin/operations/WithdrawTest.kt b/src/commonTest/kotlin/net/taler/wallet/kotlin/operations/WithdrawTest.kt
index b5f8553..03b629e 100644
--- a/src/commonTest/kotlin/net/taler/wallet/kotlin/operations/WithdrawTest.kt
+++ b/src/commonTest/kotlin/net/taler/wallet/kotlin/operations/WithdrawTest.kt
@@ -17,12 +17,27 @@
package net.taler.wallet.kotlin.operations
import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.Denominations.denomination0d01
+import net.taler.wallet.kotlin.Denominations.denomination0d1
+import net.taler.wallet.kotlin.Denominations.denomination1
+import net.taler.wallet.kotlin.Denominations.denomination10
+import net.taler.wallet.kotlin.Denominations.denomination2
+import net.taler.wallet.kotlin.Denominations.denomination4
+import net.taler.wallet.kotlin.Denominations.denomination5
+import net.taler.wallet.kotlin.Denominations.denomination8
+import net.taler.wallet.kotlin.Timestamp
+import net.taler.wallet.kotlin.crypto.Refresh.DenominationSelectionInfo
+import net.taler.wallet.kotlin.crypto.Refresh.SelectedDenomination
+import net.taler.wallet.kotlin.exchange.DenominationStatus.VerifiedBad
+import net.taler.wallet.kotlin.exchange.DenominationStatus.VerifiedGood
import net.taler.wallet.kotlin.getMockHttpClient
import net.taler.wallet.kotlin.giveJsonResponse
import net.taler.wallet.kotlin.operations.Withdraw.BankDetails
import net.taler.wallet.kotlin.runCoroutine
import kotlin.test.Test
import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
internal class WithdrawTest {
@@ -59,4 +74,79 @@ internal class WithdrawTest {
}
}
+ @Test
+ fun testIsWithdrawableDenomination() {
+ // denomination is withdrawable
+ assertTrue(withdraw.isWithdrawableDenomination(denomination1))
+ // denomination is withdrawable when VerifiedGood
+ assertTrue(withdraw.isWithdrawableDenomination(denomination1.copy(status = VerifiedGood)))
+ // fails with VerifiedBad
+ assertFalse(withdraw.isWithdrawableDenomination(denomination1.copy(status = VerifiedBad)))
+ // fails when revoked
+ assertFalse(withdraw.isWithdrawableDenomination(denomination1.copy(isRevoked = true)))
+ // fails when not started
+ assertFalse(withdraw.isWithdrawableDenomination(denomination1.copy(stampStart = Timestamp(Timestamp.now().ms + 9999))))
+ // fails when expired
+ assertFalse(withdraw.isWithdrawableDenomination(denomination1.copy(stampExpireWithdraw = Timestamp.now())))
+ // fails when almost expired
+ assertFalse(withdraw.isWithdrawableDenomination(denomination1.copy(stampExpireWithdraw = Timestamp(Timestamp.now().ms + 5000))))
+ // succeeds when not quite expired
+ assertTrue(withdraw.isWithdrawableDenomination(denomination1.copy(stampExpireWithdraw = Timestamp(Timestamp.now().ms + 51000))))
+ }
+
+ @Test
+ fun testGetDenominationSelection() {
+ val allDenominations = listOf(
+ denomination0d1,
+ denomination0d01, // unsorted list to test sort as well
+ denomination10,
+ denomination8,
+ denomination5,
+ denomination4,
+ denomination2,
+ denomination1
+ )
+ // select denominations for 10 TESTKUDOS
+ val value10 = Amount(currency = "TESTKUDOS", value = 10, fraction = 0)
+ val expectedSelectionInfo10 = DenominationSelectionInfo(
+ totalCoinValue = Amount(currency = "TESTKUDOS", value = 9, fraction = 82000000),
+ totalWithdrawCost = Amount(currency = "TESTKUDOS", value = 9, fraction = 99000000),
+ selectedDenominations = listOf(
+ SelectedDenomination(1, denomination8),
+ SelectedDenomination(1, denomination1),
+ SelectedDenomination(8, denomination0d1),
+ SelectedDenomination(2, denomination0d01)
+ )
+ )
+ assertEquals(expectedSelectionInfo10, withdraw.getDenominationSelection(value10, allDenominations))
+
+ // select denominations for 5.5 TESTKUDOS
+ val value5d5 = Amount(currency = "TESTKUDOS", value = 5, fraction = 50000000)
+ val expectedSelectionInfo5d5 = DenominationSelectionInfo(
+ totalCoinValue = Amount(currency = "TESTKUDOS", value = 5, fraction = 42000000),
+ totalWithdrawCost = Amount(currency = "TESTKUDOS", value = 5, fraction = 49000000),
+ selectedDenominations = listOf(
+ SelectedDenomination(1, denomination5),
+ SelectedDenomination(4, denomination0d1),
+ SelectedDenomination(2, denomination0d01)
+ )
+ )
+ assertEquals(expectedSelectionInfo5d5, withdraw.getDenominationSelection(value5d5, allDenominations))
+
+ // select denominations for 23.42 TESTKUDOS
+ val value23d42 = Amount(currency = "TESTKUDOS", value = 23, fraction = 42000000)
+ val expectedSelectionInfo23d42 = DenominationSelectionInfo(
+ totalCoinValue = Amount(currency = "TESTKUDOS", value = 23, fraction = 31000000),
+ totalWithdrawCost = Amount(currency = "TESTKUDOS", value = 23, fraction = 42000000),
+ selectedDenominations = listOf(
+ SelectedDenomination(2, denomination10),
+ SelectedDenomination(1, denomination2),
+ SelectedDenomination(1, denomination1),
+ SelectedDenomination(3, denomination0d1),
+ SelectedDenomination(1, denomination0d01)
+ )
+ )
+ assertEquals(expectedSelectionInfo23d42, withdraw.getDenominationSelection(value23d42, allDenominations))
+ }
+
}