Skip to content

Commit

Permalink
* add factory for concise api (#8)
Browse files Browse the repository at this point in the history
* differentiate between random-improve and largest-first method
* enclose non-public methods
* add test for largest-first method
* bump version
  • Loading branch information
vladmelnyk authored Sep 5, 2019
1 parent 4871947 commit b6c9ec6
Show file tree
Hide file tree
Showing 9 changed files with 272 additions and 134 deletions.
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
group = 'com.coinselection'
version = '0.0.7'
version = '1.0.0'
buildscript {
ext.kotlin_version = '1.3.31'
ext.kotlin_version = '1.3.50'
ext.spring_boot_version = '2.0.6.RELEASE'
repositories {
mavenCentral()
Expand Down
123 changes: 0 additions & 123 deletions src/main/kotlin/com.coinselection/BtcCoinSelectionProvider.kt

This file was deleted.

15 changes: 15 additions & 0 deletions src/main/kotlin/com.coinselection/CoinSelectionFactory.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.coinselection

import com.coinselection.model.Algorithm
import com.coinselection.model.Algorithm.LARGEST_FIRST
import com.coinselection.model.Algorithm.RANDOM_IMPROVE

object CoinSelectionFactory {

fun create(algorithm: Algorithm): CoinSelectionProvider {
return when (algorithm) {
RANDOM_IMPROVE -> RandomImproveCoinSelectionProvider
LARGEST_FIRST -> LargestFirstCoinSelectionProvider
}
}
}
18 changes: 18 additions & 0 deletions src/main/kotlin/com.coinselection/CoinSelectionProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.coinselection

import com.coinselection.dto.CoinSelectionResult
import com.coinselection.dto.UnspentOutput
import java.math.BigDecimal

const val MAX_INPUT = 60

interface CoinSelectionProvider {

fun provide(utxoList: List<UnspentOutput>,
targetValue: BigDecimal,
feeRatePerByte: BigDecimal,
numberOfOutputs: Int = 1,
compulsoryUtxoList: List<UnspentOutput>? = null,
hasOpReturnOutput: Boolean = false
): CoinSelectionResult?
}
56 changes: 56 additions & 0 deletions src/main/kotlin/com.coinselection/DefaultCoinSelectionProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.coinselection

import com.coinselection.dto.CoinSelectionResult
import com.coinselection.dto.UnspentOutput
import com.coinselection.model.CumulativeHolder
import com.coinselection.model.TransactionSize
import com.coinselection.size.SegwitLegacyCompatibleSizeProvider
import java.math.BigDecimal

internal object DefaultCoinSelectionProvider : CoinSelectionProvider {
internal const val maxNumberOfInputs: Int = MAX_INPUT
internal val transactionSize: TransactionSize = SegwitLegacyCompatibleSizeProvider.provide()

override fun provide(utxoList: List<UnspentOutput>, targetValue: BigDecimal, feeRatePerByte: BigDecimal, numberOfOutputs: Int, compulsoryUtxoList: List<UnspentOutput>?, hasOpReturnOutput: Boolean): CoinSelectionResult? {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

class UtxoSumCalculationData(
val utxoList: List<UnspentOutput>,
val cumulativeHolder: CumulativeHolder
)


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


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

fun selectUntilSumIsLessThanTarget(utxoList: List<UnspentOutput>,
targetValue: BigDecimal,
costCalculator: CostCalculator,
compulsoryUtxoList: List<UnspentOutput>?): UtxoSumCalculationData? {
val cumulativeHolder = CumulativeHolder.defaultInit()
cumulativeHolder.appendFee(costCalculator.getBaseFee())
val selectedCompulsoryUtxoList = compulsoryUtxoList?.asSequence()
?.take(maxNumberOfInputs)
?.onEach { appendSumAndFee(cumulativeHolder, costCalculator, sum = it.amount) }
?.toList() ?: listOf()
val selectedUtxoList = utxoList
.asSequence()
.filter { it.amount >= costCalculator.getCostPerInput() }
.takeWhile { sumIsLessThanTarget(cumulativeHolder, targetValue) }
.take(maxNumberOfInputs)
.onEach { appendSumAndFee(cumulativeHolder, costCalculator, sum = it.amount) }
.toList()
if (sumIsLessThanTarget(cumulativeHolder, targetValue)) {
return null
}
return UtxoSumCalculationData(selectedCompulsoryUtxoList + selectedUtxoList, cumulativeHolder)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.coinselection

import com.coinselection.DefaultCoinSelectionProvider.UtxoSumCalculationData
import com.coinselection.DefaultCoinSelectionProvider.selectUntilSumIsLessThanTarget
import com.coinselection.DefaultCoinSelectionProvider.sumIsLessThanTarget
import com.coinselection.DefaultCoinSelectionProvider.transactionSize
import com.coinselection.dto.CoinSelectionResult
import com.coinselection.dto.UnspentOutput
import java.math.BigDecimal

internal object LargestFirstCoinSelectionProvider
: CoinSelectionProvider by DefaultCoinSelectionProvider {

override fun provide(utxoList: List<UnspentOutput>,
targetValue: BigDecimal,
feeRatePerByte: BigDecimal,
numberOfOutputs: Int,
compulsoryUtxoList: List<UnspentOutput>?,
hasOpReturnOutput: Boolean
): CoinSelectionResult? {

val costCalculator = CostCalculator(
transactionSize,
feeRatePerByte,
numberOfOutputs,
hasOpReturnOutput
)

val dataPair = largestFirstSelection(
utxoList = utxoList,
costCalculator = costCalculator,
targetValue = targetValue,
compulsoryUtxoList = compulsoryUtxoList
) ?: return null

val selectedUtxoList = dataPair.utxoList
val cumulativeHolder = dataPair.cumulativeHolder


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

internal fun largestFirstSelection(utxoList: List<UnspentOutput>,
costCalculator: CostCalculator,
targetValue: BigDecimal,
compulsoryUtxoList: List<UnspentOutput>?): UtxoSumCalculationData? {
val utxoListSorted = utxoList.sortedByDescending { it.amount }
val dataPair = selectUntilSumIsLessThanTarget(utxoListSorted, targetValue, costCalculator, compulsoryUtxoList)
return if (dataPair == null || sumIsLessThanTarget(dataPair.cumulativeHolder, targetValue)) {
null
} else {
val cumulativeHolder = dataPair.cumulativeHolder
UtxoSumCalculationData(dataPair.utxoList, cumulativeHolder)
}
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.coinselection

import com.coinselection.DefaultCoinSelectionProvider.UtxoSumCalculationData
import com.coinselection.DefaultCoinSelectionProvider.appendSumAndFee
import com.coinselection.DefaultCoinSelectionProvider.maxNumberOfInputs
import com.coinselection.DefaultCoinSelectionProvider.selectUntilSumIsLessThanTarget
import com.coinselection.DefaultCoinSelectionProvider.sumIsLessThanTarget
import com.coinselection.DefaultCoinSelectionProvider.transactionSize
import com.coinselection.dto.CoinSelectionResult
import com.coinselection.dto.UnspentOutput
import com.coinselection.model.CumulativeHolder
import java.math.BigDecimal
import java.util.concurrent.atomic.AtomicReference

internal object RandomImproveCoinSelectionProvider : CoinSelectionProvider by DefaultCoinSelectionProvider {

override fun provide(utxoList: List<UnspentOutput>,
targetValue: BigDecimal,
feeRatePerByte: BigDecimal,
numberOfOutputs: Int,
compulsoryUtxoList: List<UnspentOutput>?,
hasOpReturnOutput: Boolean
): CoinSelectionResult? {

val costCalculator = CostCalculator(
transactionSize,
feeRatePerByte,
numberOfOutputs,
hasOpReturnOutput
)

val utxoListShuffled = utxoList.shuffled()
val dataPair =
selectUntilSumIsLessThanTarget(
utxoListShuffled,
targetValue,
costCalculator,
compulsoryUtxoList
) ?: fallbackLargestFirstSelection(
utxoList = utxoList,
costCalculator = costCalculator,
targetValue = targetValue,
compulsoryUtxoList = compulsoryUtxoList
) ?: return null

val selectedUtxoList = dataPair.utxoList
val cumulativeHolder = dataPair.cumulativeHolder

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

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

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

private fun improve(remainingUtxoList: List<UnspentOutput>, targetValue: BigDecimal, costCalculator: CostCalculator, cumulativeHolder: CumulativeHolder): List<UnspentOutput> {
val maxTargetValue = BigDecimal(3) * targetValue
val optimalTargetValue = BigDecimal(2) * targetValue
val delta = AtomicReference(BigDecimal.valueOf(Long.MAX_VALUE))
return remainingUtxoList
.shuffled()
.asSequence()
.take(maxNumberOfInputs)
.filter { it.amount >= costCalculator.getCostPerInput() }
.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, costCalculator, sum = it.amount) }
.toList()
}

private fun fallbackLargestFirstSelection(utxoList: List<UnspentOutput>,
costCalculator: CostCalculator,
targetValue: BigDecimal,
compulsoryUtxoList: List<UnspentOutput>?): UtxoSumCalculationData? {
return LargestFirstCoinSelectionProvider.largestFirstSelection(
utxoList = utxoList,
costCalculator = costCalculator,
targetValue = targetValue,
compulsoryUtxoList = compulsoryUtxoList
)
}

}
6 changes: 6 additions & 0 deletions src/main/kotlin/com.coinselection/model/Algorithm.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.coinselection.model

enum class Algorithm {
LARGEST_FIRST,
RANDOM_IMPROVE
}
Loading

0 comments on commit b6c9ec6

Please sign in to comment.