Skip to content

Commit

Permalink
Dev (#4)
Browse files Browse the repository at this point in the history
* result now returns accumulated commission and selected utxo list

* check if total utxo amount is enough

* Fix bug in improve algorithm and refactor tests

* add test case

* refactor the number of params in coinselection method

* make model name more compact

* refactoring

* change function calls

* remove duplicate code

* shrink main methods

* improve readability

* rename method

* modify readme

* refactor and add compulsory list

* add test for compulsory list

* increment version
  • Loading branch information
vladmelnyk authored Feb 1, 2019
1 parent f95b318 commit a45b44b
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 34 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
group = 'com.coinselection'
version = '0.0.3'
version = '0.0.4'
buildscript {
ext.kotlin_version = '1.3.11'
ext.spring_boot_version = '2.0.6.RELEASE'
Expand Down
88 changes: 55 additions & 33 deletions src/main/kotlin/com.coinselection/BtcCoinSelectionProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,41 @@ class BtcCoinSelectionProvider(
private val transactionSize: TransactionSize = SegwitLegacyCompatibleSizeProvider.provide()
) {

fun provide(utxoList: List<UnspentOutput>, targetValue: BigDecimal, feeRatePerByte: BigDecimal, numberOfOutputs: Int = 1): CoinSelectionResult? {
val cumulativeHolder = CumulativeHolder.defaultInit()
val costCalculator = CostCalculator(transactionSize = transactionSize, feePerByte = feeRatePerByte, numberOfOutputs = numberOfOutputs)
val selectedUtxoList = select(utxoList, targetValue, costCalculator, cumulativeHolder)
private class UtxoSumCalculationData(
val utxoList: List<UnspentOutput>,
val cumulativeHolder: CumulativeHolder
)


fun provide(utxoList: List<UnspentOutput>,
targetValue: BigDecimal,
feeRatePerByte: BigDecimal,
numberOfOutputs: Int = 1,
compulsoryUtxoList: List<UnspentOutput> = listOf()
): CoinSelectionResult? {

val costCalculator = CostCalculator(transactionSize, feeRatePerByte, numberOfOutputs)

val dataPair = selectUntilSumIsLessThanTarget(
utxoList.shuffled(), targetValue, costCalculator, compulsoryUtxoList)
?: fallbackLargestFirstSelection(
utxoListRearanged = utxoList.sortedByDescending { it.amount },
costCalculator = costCalculator,
targetValue = targetValue,
compulsoryUtxoList = compulsoryUtxoList)
?: return null
val improvedUtxoList = improve(getRemainingUtxoList(selectedUtxoList = selectedUtxoList, originalUtxoList = utxoList), targetValue = targetValue, costCalculator = costCalculator, cumulativeHolder = cumulativeHolder)
return CoinSelectionResult(selectedUtxos = mergeUtxoLists(selectedUtxoList = selectedUtxoList, improvedUtxoList = improvedUtxoList), totalFee = cumulativeHolder.getFee())
}

private fun select(utxoList: List<UnspentOutput>, targetValue: BigDecimal, costCalculator: CostCalculator, cumulativeHolder: CumulativeHolder): List<UnspentOutput>? {
return selectUntilSumIsLessThanTarget(utxoListRearanged = utxoList.shuffled(), cumulativeHolder = cumulativeHolder, targetValue = targetValue, costCalculator = costCalculator)
?: fallbackLargestFirstSelection(utxoListRearanged = utxoList.sortedByDescending { it.amount }, cumulativeHolder = cumulativeHolder, costCalculator = costCalculator, targetValue = targetValue)
val selectedUtxoList = dataPair.utxoList
val cumulativeHolder = dataPair.cumulativeHolder

val remainingUtxoList = utxoList.subtract(selectedUtxoList).toList()
val improvedUtxoList = improve(remainingUtxoList, targetValue, costCalculator, cumulativeHolder)

val utxResult = selectedUtxoList.union(improvedUtxoList).toList()

return CoinSelectionResult(
selectedUtxos = utxResult,
totalFee = cumulativeHolder.getFee())
}

private fun improve(remainingUtxoList: List<UnspentOutput>, targetValue: BigDecimal, costCalculator: CostCalculator, cumulativeHolder: CumulativeHolder): List<UnspentOutput> {
Expand All @@ -37,53 +60,52 @@ class BtcCoinSelectionProvider(
.shuffled()
.asSequence()
.take(maxNumberOfInputs)
.takeWhile { sumIsLessThanTarget(cumulativeHolder = cumulativeHolder, targetValue = maxTargetValue) }
.takeWhile { sumIsLessThanTarget(cumulativeHolder, targetValue = maxTargetValue) }
.onEach { delta.getAndSet((cumulativeHolder.getSum() - (optimalTargetValue + cumulativeHolder.getFee())).abs()) }
.takeWhile { (cumulativeHolder.getSum() + it.amount - (optimalTargetValue + cumulativeHolder.getFee() + costCalculator.getCostPerInput())).abs() < delta.get() }
.onEach { appendSumAndFee(cumulativeHolder = cumulativeHolder, costCalculator = costCalculator, sum = it.amount) }
.onEach { appendSumAndFee(cumulativeHolder, costCalculator, sum = it.amount) }
.toList()
}

private fun sumIsLessThanTarget(cumulativeHolder: CumulativeHolder, targetValue: BigDecimal): Boolean {
return cumulativeHolder.getSum() < targetValue + cumulativeHolder.getFee()
}

private fun selectUntilSumIsLessThanTarget(utxoListRearanged: List<UnspentOutput>, cumulativeHolder: CumulativeHolder, targetValue: BigDecimal, costCalculator: CostCalculator): List<UnspentOutput>? {
private fun selectUntilSumIsLessThanTarget(utxoListRearanged: List<UnspentOutput>,
targetValue: BigDecimal,
costCalculator: CostCalculator,
compulsoryUtxoList: List<UnspentOutput>): UtxoSumCalculationData? {
val cumulativeHolder = CumulativeHolder.defaultInit()
cumulativeHolder.appendFee(costCalculator.getBaseFee())
val selectedUtxoList = utxoListRearanged
.asSequence()
.takeWhile { sumIsLessThanTarget(cumulativeHolder = cumulativeHolder, targetValue = targetValue) }
.takeWhile { sumIsLessThanTarget(cumulativeHolder, targetValue) }
.take(maxNumberOfInputs)
.onEach { appendSumAndFee(cumulativeHolder = cumulativeHolder, costCalculator = costCalculator, sum = it.amount) }
.onEach { appendSumAndFee(cumulativeHolder, costCalculator, sum = it.amount) }
.toList()
if (sumIsLessThanTarget(cumulativeHolder = cumulativeHolder, targetValue = targetValue)) {
if (sumIsLessThanTarget(cumulativeHolder, targetValue)) {
return null
}
return selectedUtxoList
return UtxoSumCalculationData(compulsoryUtxoList + selectedUtxoList, cumulativeHolder)
}

private fun fallbackLargestFirstSelection(utxoListRearanged: List<UnspentOutput>, cumulativeHolder: CumulativeHolder, costCalculator: CostCalculator, targetValue: BigDecimal): List<UnspentOutput>? {
cumulativeHolder.reset()
cumulativeHolder.appendFee(costCalculator.getBaseFee())
val selectedUtxoList = selectUntilSumIsLessThanTarget(utxoListRearanged = utxoListRearanged,
cumulativeHolder = cumulativeHolder, costCalculator = costCalculator, targetValue = targetValue)
// Return null utxo list if total amount is still not enough
if (sumIsLessThanTarget(cumulativeHolder = cumulativeHolder, targetValue = targetValue)) {
return null
private fun fallbackLargestFirstSelection(utxoListRearanged: List<UnspentOutput>,
costCalculator: CostCalculator,
targetValue: BigDecimal,
compulsoryUtxoList: List<UnspentOutput>): UtxoSumCalculationData? {
val dataPair = selectUntilSumIsLessThanTarget(utxoListRearanged, targetValue, costCalculator, compulsoryUtxoList)
return if (dataPair == null || sumIsLessThanTarget(dataPair.cumulativeHolder, targetValue)) {
null
} else {
val cumulativeHolder = dataPair.cumulativeHolder
cumulativeHolder.appendFee(costCalculator.getBaseFee())
UtxoSumCalculationData(compulsoryUtxoList + dataPair.utxoList, cumulativeHolder)
}
return selectedUtxoList
}

private fun appendSumAndFee(cumulativeHolder: CumulativeHolder, costCalculator: CostCalculator, sum: BigDecimal) {
cumulativeHolder.appendSum(sum)
cumulativeHolder.appendFee(costCalculator.getCostPerInput())
}

private fun getRemainingUtxoList(selectedUtxoList: List<UnspentOutput>, originalUtxoList: List<UnspentOutput>): List<UnspentOutput> {
return originalUtxoList.subtract(selectedUtxoList).toList()
}

private fun mergeUtxoLists(selectedUtxoList: List<UnspentOutput>, improvedUtxoList: List<UnspentOutput>): List<UnspentOutput> {
return selectedUtxoList.union(improvedUtxoList).toList()
}
}
12 changes: 12 additions & 0 deletions src/test/kotlin/com/coinselection/BtcCoinSelectionProviderTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ class BtcCoinSelectionProviderTest {
Assertions.assertNull(coinSelectionResult)
}

@Test
fun `should include all compulsory utxos`() {
val targetValue = BigDecimal(5)
val rangeMin = 1.1
val rangeMax = 1.2
val utxoList = (1..50).map { rangeMin + (rangeMax - rangeMin) * random.nextDouble() }.map { createUnspentOutput(it) }
val compulsoryUtxoList = (1..20).map { rangeMin + (rangeMax - rangeMin) * random.nextDouble() }.map { createUnspentOutput(it) }
val coinSelectionResult = coinSelectionProvider.provide(utxoList, targetValue, smartFeePerByte, compulsoryUtxoList = compulsoryUtxoList)
Assertions.assertNotNull(coinSelectionResult!!.selectedUtxos)
Assertions.assertTrue(coinSelectionResult.selectedUtxos!!.containsAll(compulsoryUtxoList))
}

private fun createUnspentOutput(value: Double): UnspentOutput {
return UnspentOutput(amount = BigDecimal(value))
}
Expand Down

0 comments on commit a45b44b

Please sign in to comment.