Skip to content

Commit

Permalink
Fix bugs and update tests (#2)
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
  • Loading branch information
vladmelnyk authored Dec 21, 2018
1 parent bfb22ee commit 7802b34
Show file tree
Hide file tree
Showing 3 changed files with 24 additions and 42 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.2'
version = '0.0.3'
buildscript {
ext.kotlin_version = '1.3.11'
ext.spring_boot_version = '2.0.6.RELEASE'
Expand Down
14 changes: 7 additions & 7 deletions src/main/kotlin/com.coinselection/BtcCoinSelectionProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class BtcCoinSelectionProvider : CoinSelectionProvider {
val cumulativeSum = selectedUtxoListSumAndFee.second
val cumulativeFee = selectedUtxoListSumAndFee.third
val improvedUtxoList = if (selectedUtxoList != null) {
improve(utxoList.subtract(selectedUtxoList).toList(), cumulativeSum, cumulativeFee, targetValue, feeRatePerByte, maxNumberOfInputs,
improve(utxoList.subtract(selectedUtxoList).toList(), cumulativeSum, cumulativeFee, targetValue, feeRatePerByte, maxNumberOfInputs - selectedUtxoList.size,
inputSize)
} else {
listOf()
Expand Down Expand Up @@ -44,6 +44,7 @@ class BtcCoinSelectionProvider : CoinSelectionProvider {
.asSequence()
.sortedByDescending { it.amount }
.takeWhile { cumulativeSum.get() < targetValue + cumulativeFee.get() }
.take(maxNumberOfInputs)
.onEach { append(atomicReference = cumulativeSum, with = it.amount) }
.onEach { append(atomicReference = cumulativeFee, with = costPerInput) }
.toList()
Expand All @@ -60,18 +61,17 @@ class BtcCoinSelectionProvider : CoinSelectionProvider {
val costPerInput = inputSize.toBigDecimal() * feeRatePerByte
val maxTargetValue = BigDecimal(3) * targetValue
val optimalTargetValue = BigDecimal(2) * targetValue
val delta = AtomicReference((currentCumulativeSum.get() - (optimalTargetValue + currentCumulativeFee.get())).abs())
val improvedUtxoList = remainingUtxoList
val delta = AtomicReference(BigDecimal.valueOf(Long.MAX_VALUE))
return remainingUtxoList
.shuffled()
.asSequence()
.takeWhile { currentCumulativeSum.get() < maxTargetValue + currentCumulativeFee.get() }
.take(maxNumOfInputs)
.takeWhile { currentCumulativeSum.get() < maxTargetValue + currentCumulativeFee.get() }
.onEach { delta.getAndSet((currentCumulativeSum.get() - (optimalTargetValue + currentCumulativeFee.get())).abs()) }
.takeWhile { (currentCumulativeSum.get() + it.amount - (optimalTargetValue + currentCumulativeFee.get() + costPerInput)).abs() < delta.get() }
.onEach { append(atomicReference = currentCumulativeSum, with = it.amount) }
.onEach { append(atomicReference = currentCumulativeFee, with = costPerInput) }
.takeWhile { (currentCumulativeSum.get() - (optimalTargetValue + currentCumulativeFee.get())).abs() < delta.get() }
.onEach { delta.getAndSet((currentCumulativeSum.get() - (optimalTargetValue + currentCumulativeFee.get())).abs()) }
.toList()
return improvedUtxoList
}

private fun append(atomicReference: AtomicReference<BigDecimal>, with: BigDecimal?): BigDecimal {
Expand Down
50 changes: 16 additions & 34 deletions src/test/kotlin/com/coinselection/BtcCoinSelectionProviderTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import org.junit.jupiter.api.Test
import java.math.BigDecimal
import java.util.*

private const val KB = 1000L
private val KB = 1000.toBigDecimal()

class BtcCoinSelectionProviderTest {

Expand All @@ -16,7 +16,7 @@ class BtcCoinSelectionProviderTest {
// 3. complex scenario: how does the UTXO pool behave over time and what is the average cost per transaction?

private val coinSelectionProvider: CoinSelectionProvider = BtcCoinSelectionProvider()
private val smartFee = BigDecimal(0.00000005)
private val smartFeePerByte = BigDecimal.ONE.movePointLeft(8)
private val random = Random()

@RepeatedTest(100)
Expand All @@ -25,48 +25,31 @@ class BtcCoinSelectionProviderTest {
val rangeMin = 0
val rangeMax = 1
val utxoList = (1..1000).map { rangeMin + (rangeMax - rangeMin) * random.nextDouble() }.map { createUnspentOutput(it) }
val coinSelectionResult = coinSelectionProvider.provide(utxoList, targetValue, smartFee)
val coinSelectionResult = coinSelectionProvider.provide(utxoList, targetValue, smartFeePerByte)
val sum = coinSelectionResult.selectedUtxos?.sumByBigDecimal { it.amount }
val count = coinSelectionResult.selectedUtxos?.size
val feeSimple = calculateTransactionFee(count!!, 2, smartFee)
val feeCalculated = coinSelectionResult.totalFee.movePointLeft(8)
val feeSimple = calculateTransactionFee(count!!, 2, smartFeePerByte)
val feeCalculated = coinSelectionResult.totalFee
Assertions.assertTrue(sum!! > targetValue + feeCalculated)
Assertions.assertTrue(feeSimple == feeCalculated.toLong())
}

@Test
fun `should fallback to largest first`() {
val targetValue = BigDecimal(5)
val rangeMin = 0
val rangeMax = 1
val maxNumOfInputs = 3
val utxoList = (1..1000).map { rangeMin + (rangeMax - rangeMin) * random.nextDouble() }.map { createUnspentOutput(it) }
val coinSelectionResult = coinSelectionProvider.provide(utxoList, targetValue, smartFee, maxNumberOfInputs = maxNumOfInputs)
val sum = coinSelectionResult.selectedUtxos?.sumByBigDecimal { it.amount }
val count = coinSelectionResult.selectedUtxos?.size
val feeSimple = calculateTransactionFee(count!!, 2, smartFee)
val feeCalculated = coinSelectionResult.totalFee.movePointLeft(8)

Assertions.assertTrue(sum!! > targetValue + feeCalculated)
Assertions.assertTrue(feeSimple == feeCalculated.toLong())
Assertions.assertTrue(coinSelectionResult.selectedUtxos!!.contains(utxoList.asSequence().sortedByDescending { it.amount }.first()))
Assertions.assertTrue(sum < targetValue * 3.toBigDecimal() + feeCalculated)
Assertions.assertTrue(feeSimple == feeCalculated)
}

@Test
fun `should trigger improve scenario`() {
val targetValue = BigDecimal(3)
val rangeMin = 1.1
val rangeMax = 1.2
val maxNumOfInputs = 3
val maxNumOfInputs = 6
val utxoList = (1..1000).map { rangeMin + (rangeMax - rangeMin) * random.nextDouble() }.map { createUnspentOutput(it) }
val coinSelectionResult = coinSelectionProvider.provide(utxoList, targetValue, smartFee, maxNumberOfInputs = maxNumOfInputs)
val coinSelectionResult = coinSelectionProvider.provide(utxoList, targetValue, smartFeePerByte, maxNumberOfInputs = maxNumOfInputs)
val sum = coinSelectionResult.selectedUtxos?.sumByBigDecimal { it.amount }
val count = coinSelectionResult.selectedUtxos?.size
val feeSimple = calculateTransactionFee(count!!, 2, smartFee)
val feeCalculated = coinSelectionResult.totalFee.movePointLeft(8)
val feeSimple = calculateTransactionFee(count!!, 2, smartFeePerByte)
val feeCalculated = coinSelectionResult.totalFee
Assertions.assertTrue(sum!! > targetValue + feeCalculated)
Assertions.assertTrue(feeSimple == feeCalculated.toLong())
Assertions.assertSame(maxNumOfInputs * 2 - 1, coinSelectionResult.selectedUtxos!!.size)
Assertions.assertTrue(feeSimple == feeCalculated)
Assertions.assertSame(maxNumOfInputs, coinSelectionResult.selectedUtxos!!.size)
}

@Test
Expand All @@ -75,7 +58,7 @@ class BtcCoinSelectionProviderTest {
val rangeMin = 1.1
val rangeMax = 1.2
val utxoList = (1..50).map { rangeMin + (rangeMax - rangeMin) * random.nextDouble() }.map { createUnspentOutput(it) }
val coinSelectionResult = coinSelectionProvider.provide(utxoList, targetValue, smartFee)
val coinSelectionResult = coinSelectionProvider.provide(utxoList, targetValue, smartFeePerByte)
Assertions.assertNull(coinSelectionResult.selectedUtxos)
}

Expand All @@ -87,10 +70,9 @@ class BtcCoinSelectionProviderTest {
return this.fold(BigDecimal.ZERO) { acc, e -> acc + transform.invoke(e) }
}

private fun calculateTransactionFee(inputsCount: Int, outputsCount: Int, smartFeePerKB: BigDecimal): Long {
private fun calculateTransactionFee(inputsCount: Int, outputsCount: Int, smartFeePerByte: BigDecimal): BigDecimal {
val size = inputsCount * 91 + outputsCount * 32 + 11
val smartFeePerByte = smartFeePerKB.div(BigDecimal(KB))
return smartFeePerByte.movePointLeft(8).toLong() * size
return (smartFeePerByte * size.toBigDecimal())
}

}

0 comments on commit 7802b34

Please sign in to comment.