diff options
author | Torsten Grote <t@grobox.de> | 2020-07-14 16:56:56 -0300 |
---|---|---|
committer | Torsten Grote <t@grobox.de> | 2020-07-14 16:56:56 -0300 |
commit | 561eaa710b00a1f49a089aeb8aa5cab8d688f453 (patch) | |
tree | 9a8cd976eb3bc7fa2c9f299003d47baf81da0a31 /src | |
parent | 5f03889e649271347229af777ec1a025a6210f23 (diff) | |
download | wallet-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.kt | 18 | ||||
-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.kt | 69 | ||||
-rw-r--r-- | src/commonTest/kotlin/net/taler/wallet/kotlin/DbTest.kt | 37 | ||||
-rw-r--r-- | src/commonTest/kotlin/net/taler/wallet/kotlin/Denominations.kt | 117 | ||||
-rw-r--r-- | src/commonTest/kotlin/net/taler/wallet/kotlin/operations/WithdrawTest.kt | 90 |
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)) + } + } |