Skip to content

Commit

Permalink
Configure transaction size (#9)
Browse files Browse the repository at this point in the history
* refactoring

* change factory contract to parametrize transaction size and replace delegation by inheritance for simplicity

* bump version
  • Loading branch information
vladmelnyk authored Sep 6, 2019
1 parent b6c9ec6 commit e3e6c4b
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 55 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 = '1.0.0'
version = '1.1.0'
buildscript {
ext.kotlin_version = '1.3.50'
ext.spring_boot_version = '2.0.6.RELEASE'
Expand Down
12 changes: 9 additions & 3 deletions src/main/kotlin/com.coinselection/CoinSelectionFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@ package com.coinselection
import com.coinselection.model.Algorithm
import com.coinselection.model.Algorithm.LARGEST_FIRST
import com.coinselection.model.Algorithm.RANDOM_IMPROVE
import com.coinselection.model.TransactionSize
import com.coinselection.size.SegwitLegacyCompatibleSizeProvider

object CoinSelectionFactory {

fun create(algorithm: Algorithm): CoinSelectionProvider {
fun create(
algorithm: Algorithm,
maxNumberOfInputs: Int = MAX_INPUT,
transactionSize: TransactionSize = SegwitLegacyCompatibleSizeProvider.provide()
): CoinSelectionProvider {
return when (algorithm) {
RANDOM_IMPROVE -> RandomImproveCoinSelectionProvider
LARGEST_FIRST -> LargestFirstCoinSelectionProvider
RANDOM_IMPROVE -> RandomImproveCoinSelectionProvider(maxNumberOfInputs, transactionSize)
LARGEST_FIRST -> LargestFirstCoinSelectionProvider(maxNumberOfInputs, transactionSize)
}
}
}
2 changes: 0 additions & 2 deletions src/main/kotlin/com.coinselection/CoinSelectionProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ 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>,
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/com.coinselection/CostCalculator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import java.math.BigDecimal

const val OP_RETURN_SIZE: Int = 40

class CostCalculator(
internal class CostCalculator(
private val transactionSize: TransactionSize,
private val feePerByte: BigDecimal,
private val numberOfOutputs: Int,
Expand Down
20 changes: 11 additions & 9 deletions src/main/kotlin/com.coinselection/DefaultCoinSelectionProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,20 @@ 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 com.coinselection.model.UtxoSumCalculationData
import java.math.BigDecimal

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

internal abstract class DefaultCoinSelectionProvider(
internal val maxNumberOfInputs: Int,
internal val transactionSize: TransactionSize
) : CoinSelectionProvider {

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)
Expand Down Expand Up @@ -53,4 +51,8 @@ internal object DefaultCoinSelectionProvider : CoinSelectionProvider {
}
return UtxoSumCalculationData(selectedCompulsoryUtxoList + selectedUtxoList, cumulativeHolder)
}

internal fun <T> Iterable<T>.sumByBigDecimal(transform: (T) -> BigDecimal): BigDecimal {
return this.fold(BigDecimal.ZERO) { acc, e -> acc + transform.invoke(e) }
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
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 com.coinselection.model.TransactionSize
import com.coinselection.model.UtxoSumCalculationData
import java.math.BigDecimal

internal object LargestFirstCoinSelectionProvider
: CoinSelectionProvider by DefaultCoinSelectionProvider {
internal class LargestFirstCoinSelectionProvider(maxNumberOfInputs: Int, transactionSize: TransactionSize)
: DefaultCoinSelectionProvider(maxNumberOfInputs, transactionSize) {

override fun provide(utxoList: List<UnspentOutput>,
targetValue: BigDecimal,
Expand Down Expand Up @@ -42,17 +40,22 @@ internal object LargestFirstCoinSelectionProvider
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)
private fun largestFirstSelection(utxoList: List<UnspentOutput>,
costCalculator: CostCalculator,
targetValue: BigDecimal,
compulsoryUtxoList: List<UnspentOutput>?): UtxoSumCalculationData? {
val dataPair = selectUntilSumIsLessThanTarget(
utxoList.sortedByDescending { it.amount },
targetValue,
costCalculator,
compulsoryUtxoList
)
return if (dataPair == null || sumIsLessThanTarget(dataPair.cumulativeHolder, targetValue)) {
null
} else {
val cumulativeHolder = dataPair.cumulativeHolder
UtxoSumCalculationData(dataPair.utxoList, cumulativeHolder)
UtxoSumCalculationData(
utxoList = dataPair.utxoList,
cumulativeHolder = dataPair.cumulativeHolder)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
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.Algorithm.LARGEST_FIRST
import com.coinselection.model.CumulativeHolder
import com.coinselection.model.TransactionSize
import com.coinselection.model.UtxoSumCalculationData
import java.math.BigDecimal
import java.util.concurrent.atomic.AtomicReference

internal object RandomImproveCoinSelectionProvider : CoinSelectionProvider by DefaultCoinSelectionProvider {
internal class RandomImproveCoinSelectionProvider(maxNumberOfInputs: Int, transactionSize: TransactionSize)
: DefaultCoinSelectionProvider(maxNumberOfInputs, transactionSize) {

override fun provide(utxoList: List<UnspentOutput>,
targetValue: BigDecimal,
Expand All @@ -29,31 +27,29 @@ internal object RandomImproveCoinSelectionProvider : CoinSelectionProvider by De
hasOpReturnOutput
)

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

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

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

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

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

private fun improve(remainingUtxoList: List<UnspentOutput>, targetValue: BigDecimal, costCalculator: CostCalculator, cumulativeHolder: CumulativeHolder): List<UnspentOutput> {
Expand All @@ -73,14 +69,30 @@ internal object RandomImproveCoinSelectionProvider : CoinSelectionProvider by De
}

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
feeRatePerByte: BigDecimal,
numberOfOutputs: Int,
compulsoryUtxoList: List<UnspentOutput>?,
hasOpReturnOutput: Boolean): UtxoSumCalculationData? {

val coinSelectionResult = CoinSelectionFactory.create(
algorithm = LARGEST_FIRST,
maxNumberOfInputs = maxNumberOfInputs,
transactionSize = transactionSize)
.provide(
utxoList = utxoList,
targetValue = targetValue,
compulsoryUtxoList = compulsoryUtxoList,
feeRatePerByte = feeRatePerByte,
numberOfOutputs = numberOfOutputs,
hasOpReturnOutput = hasOpReturnOutput
)

return UtxoSumCalculationData(
utxoList = coinSelectionResult?.selectedUtxos ?: return null,
cumulativeHolder = CumulativeHolder(
accumulatedSum = AtomicReference(utxoList.sumByBigDecimal { it.amount }),
accumulatedFee = AtomicReference(coinSelectionResult.totalFee))
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.coinselection.model

import com.coinselection.dto.UnspentOutput

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

0 comments on commit e3e6c4b

Please sign in to comment.