Skip to content

Commit

Permalink
Add check for utxo total amount and change return type that could be …
Browse files Browse the repository at this point in the history
…null if that check fails (#1)

* result now returns accumulated commission and selected utxo list

* check if total utxo amount is enough
  • Loading branch information
vladmelnyk authored Dec 21, 2018
1 parent ede5524 commit bfb22ee
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 30 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.1'
version = '0.0.2'
buildscript {
ext.kotlin_version = '1.3.10'
ext.kotlin_version = '1.3.11'
ext.spring_boot_version = '2.0.6.RELEASE'
repositories {
mavenCentral()
Expand Down
19 changes: 15 additions & 4 deletions src/main/kotlin/com.coinselection/BtcCoinSelectionProvider.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
package com.coinselection

import com.coinselection.dto.CoinSelectionResult
import com.coinselection.dto.UnspentOutput
import java.math.BigDecimal
import java.util.concurrent.atomic.AtomicReference

class BtcCoinSelectionProvider : CoinSelectionProvider {

override fun provide(utxoList: List<UnspentOutput>, targetValue: BigDecimal, feeRatePerByte: BigDecimal, maxNumberOfInputs: Int, numberOfDestinationAddress: Int, inputSize: Int, outputSize: Int, headerSize: Int): List<UnspentOutput> {
override fun provide(utxoList: List<UnspentOutput>, targetValue: BigDecimal, feeRatePerByte: BigDecimal, maxNumberOfInputs: Int, numberOfDestinationAddress: Int, inputSize: Int, outputSize: Int, headerSize: Int): CoinSelectionResult {
val selectedUtxoListSumAndFee = select(utxoList, targetValue, feeRatePerByte, maxNumberOfInputs, numberOfDestinationAddress, inputSize, outputSize, headerSize)
val selectedUtxoList = selectedUtxoListSumAndFee.first
val cumulativeSum = selectedUtxoListSumAndFee.second
val cumulativeFee = selectedUtxoListSumAndFee.third
val improvedUtxoList = improve(utxoList.subtract(selectedUtxoList).toList(), cumulativeSum, cumulativeFee, targetValue, feeRatePerByte, maxNumberOfInputs, inputSize)
return selectedUtxoList.union(improvedUtxoList).toList()
val improvedUtxoList = if (selectedUtxoList != null) {
improve(utxoList.subtract(selectedUtxoList).toList(), cumulativeSum, cumulativeFee, targetValue, feeRatePerByte, maxNumberOfInputs,
inputSize)
} else {
listOf()
}
return CoinSelectionResult(selectedUtxos = selectedUtxoList?.union(improvedUtxoList)?.toList(), totalFee = cumulativeFee.get())
}

private fun select(utxoList: List<UnspentOutput>, targetValue: BigDecimal, feeRatePerByte: BigDecimal, maxNumberOfInputs: Int, numberOfDestinationAddress: Int, inputSize: Int, outputSize: Int, headerSize: Int): Triple<List<UnspentOutput>, AtomicReference<BigDecimal>, AtomicReference<BigDecimal>> {
private fun select(utxoList: List<UnspentOutput>, targetValue: BigDecimal, feeRatePerByte: BigDecimal, maxNumberOfInputs: Int, numberOfDestinationAddress: Int, inputSize: Int, outputSize: Int, headerSize: Int): Triple<List<UnspentOutput>?, AtomicReference<BigDecimal>, AtomicReference<BigDecimal>> {
val cumulativeSum = AtomicReference<BigDecimal>(BigDecimal.ZERO)
val costPerInput = inputSize.toBigDecimal() * feeRatePerByte
val costPerOutput = outputSize.toBigDecimal() * feeRatePerByte
Expand All @@ -40,6 +47,10 @@ class BtcCoinSelectionProvider : CoinSelectionProvider {
.onEach { append(atomicReference = cumulativeSum, with = it.amount) }
.onEach { append(atomicReference = cumulativeFee, with = costPerInput) }
.toList()
// Return null utxo list if total amount is still not enough
if (cumulativeSum.get() < targetValue + cumulativeFee.get()) {
return Triple(null, cumulativeSum, cumulativeFee)
}
}
return Triple(selectedUtxoList, cumulativeSum, cumulativeFee)

Expand Down
4 changes: 3 additions & 1 deletion src/main/kotlin/com.coinselection/CoinSelectionProvider.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.coinselection

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

// Default sizes in bytes for Segwit legacy-compatible addresses (starting with 3...)
Expand All @@ -9,5 +11,5 @@ private const val HEADER_SIZE = 11
private const val MAX_INPUT = 60

interface CoinSelectionProvider {
fun provide(utxoList: List<UnspentOutput>, targetValue: BigDecimal, feeRatePerByte: BigDecimal, maxNumberOfInputs: Int = MAX_INPUT, numberOfDestinationAddress: Int = 1, inputSize: Int = INPUT_SIZE, outputSize: Int = OUTPUT_SIZE, headerSize: Int = HEADER_SIZE): List<UnspentOutput>
fun provide(utxoList: List<UnspentOutput>, targetValue: BigDecimal, feeRatePerByte: BigDecimal, maxNumberOfInputs: Int = MAX_INPUT, numberOfDestinationAddress: Int = 1, inputSize: Int = INPUT_SIZE, outputSize: Int = OUTPUT_SIZE, headerSize: Int = HEADER_SIZE): CoinSelectionResult
}
8 changes: 8 additions & 0 deletions src/main/kotlin/com.coinselection/dto/CoinSelectionResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.coinselection.dto

import java.math.BigDecimal

data class CoinSelectionResult(
val selectedUtxos: List<UnspentOutput>?,
val totalFee: BigDecimal
)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.coinselection
package com.coinselection.dto

import java.math.BigDecimal

Expand Down
59 changes: 37 additions & 22 deletions src/test/kotlin/com/coinselection/BtcCoinSelectionProviderTest.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package com.coinselection

import com.coinselection.dto.UnspentOutput
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.RepeatedTest
import org.junit.jupiter.api.Test
import java.math.BigDecimal
import java.util.*

private const val KB = 1024L
private const val KB = 1000L

class BtcCoinSelectionProviderTest {

Expand All @@ -24,12 +25,13 @@ class BtcCoinSelectionProviderTest {
val rangeMin = 0
val rangeMax = 1
val utxoList = (1..1000).map { rangeMin + (rangeMax - rangeMin) * random.nextDouble() }.map { createUnspentOutput(it) }
val selectedUtxos = coinSelectionProvider.provide(utxoList, targetValue, smartFee)
println(selectedUtxos.map { it.amount })
val sum = selectedUtxos.sumByBigDecimal { it.amount }
val count = selectedUtxos.size
val fee = calculateTransactionFee(count, 2, smartFee)
Assertions.assertTrue(sum > targetValue + BigDecimal(fee))
val coinSelectionResult = coinSelectionProvider.provide(utxoList, targetValue, smartFee)
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())
}

@Test
Expand All @@ -39,13 +41,15 @@ class BtcCoinSelectionProviderTest {
val rangeMax = 1
val maxNumOfInputs = 3
val utxoList = (1..1000).map { rangeMin + (rangeMax - rangeMin) * random.nextDouble() }.map { createUnspentOutput(it) }
val selectedUtxos = coinSelectionProvider.provide(utxoList, targetValue, smartFee, maxNumberOfInputs = maxNumOfInputs)
println(selectedUtxos.map { it.amount })
val sum = selectedUtxos.sumByBigDecimal { it.amount }
val count = selectedUtxos.size
val fee = calculateTransactionFee(count, 2, smartFee)
Assertions.assertTrue(sum > targetValue + BigDecimal(fee))
Assertions.assertTrue(selectedUtxos.contains(utxoList.asSequence().sortedByDescending { it.amount }.first()))
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()))
}

@Test
Expand All @@ -55,13 +59,24 @@ class BtcCoinSelectionProviderTest {
val rangeMax = 1.2
val maxNumOfInputs = 3
val utxoList = (1..1000).map { rangeMin + (rangeMax - rangeMin) * random.nextDouble() }.map { createUnspentOutput(it) }
val selectedUtxos = coinSelectionProvider.provide(utxoList, targetValue, smartFee, maxNumOfInputs)
println(selectedUtxos.map { it.amount })
val sum = selectedUtxos.sumByBigDecimal { it.amount }
val count = selectedUtxos.size
val fee = calculateTransactionFee(count, 2, smartFee)
Assertions.assertTrue(sum > targetValue + BigDecimal(fee))
Assertions.assertSame(maxNumOfInputs * 2 - 1, selectedUtxos.size)
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.assertSame(maxNumOfInputs * 2 - 1, coinSelectionResult.selectedUtxos!!.size)
}

@Test
fun `should return null utxoList if total accumulated value is not enough`() {
val targetValue = BigDecimal(100)
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)
Assertions.assertNull(coinSelectionResult.selectedUtxos)
}

private fun createUnspentOutput(value: Double): UnspentOutput {
Expand All @@ -75,7 +90,7 @@ class BtcCoinSelectionProviderTest {
private fun calculateTransactionFee(inputsCount: Int, outputsCount: Int, smartFeePerKB: BigDecimal): Long {
val size = inputsCount * 91 + outputsCount * 32 + 11
val smartFeePerByte = smartFeePerKB.div(BigDecimal(KB))
return (smartFeePerByte * BigDecimal(100000000)).toLong() * size
return smartFeePerByte.movePointLeft(8).toLong() * size
}

}

0 comments on commit bfb22ee

Please sign in to comment.