Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement custom UnspentOutputs #641

Merged
merged 8 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ class MainViewModel : ViewModel(), BitcoinKit.Listener {

fun onMaxClick() {
try {
amountLiveData.value = bitcoinKit.maximumSpendableValue(address, feePriority.feeRate, getPluginData())
amountLiveData.value = bitcoinKit.maximumSpendableValue(address, feePriority.feeRate, null, getPluginData())
} catch (e: Exception) {
amountLiveData.value = 0
errorLiveData.value = when (e) {
Expand All @@ -197,15 +197,15 @@ class MainViewModel : ViewModel(), BitcoinKit.Listener {
private fun updateFee() {
try {
feeLiveData.value = amount?.let {
fee(it, address)
fee(it, address).fee
}
} catch (e: Exception) {
errorLiveData.value = e.message ?: e.javaClass.simpleName
}
}

private fun fee(value: Long, address: String? = null): Long {
return bitcoinKit.fee(value, address, feeRate = feePriority.feeRate, pluginData = getPluginData())
private fun fee(value: Long, address: String? = null): BitcoinSendInfo {
return bitcoinKit.sendInfo(value, address, feeRate = feePriority.feeRate, unspentOutputs = null, pluginData = getPluginData())
}

private fun getPluginData(): MutableMap<Byte, IPluginData> {
Expand Down
4 changes: 2 additions & 2 deletions bitcoincashkit/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ dependencies {
api project(':bitcoincore')

// Test helpers
testImplementation 'junit:junit:4.13'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.junit.jupiter:junit-jupiter:5.6.1'
testImplementation 'org.mockito:mockito-core:3.3.3'
testImplementation 'com.nhaarman:mockito-kotlin-kt1.1:1.6.0'
Expand All @@ -72,6 +72,6 @@ dependencies {

// Android Instrumentation Test
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito-inline:2.19.1'
androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito-inline:2.28.3'
androidTestImplementation 'com.nhaarman:mockito-kotlin-kt1.1:1.6.0'
}
6 changes: 4 additions & 2 deletions bitcoincore/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -80,17 +80,19 @@ dependencies {
api 'com.github.horizontalsystems:hd-wallet-kit-android:a74b51f'

// Test helpers
testImplementation 'junit:junit:4.13'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.junit.jupiter:junit-jupiter:5.6.1'
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
testImplementation "org.powermock:powermock-api-mockito2:2.0.7"
testImplementation "org.powermock:powermock-module-junit4:2.0.7"
testImplementation 'org.mockito:mockito-inline:4.4.0'
testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"

// Spek
testImplementation "org.spekframework.spek2:spek-dsl-jvm:2.0.9"
testRuntimeOnly "org.spekframework.spek2:spek-runner-junit5:2.0.9"
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"

testImplementation "com.linkedin.dexmaker:dexmaker-mockito-inline:2.19.1"
testImplementation "com.linkedin.dexmaker:dexmaker-mockito-inline:2.28.3"
testImplementation 'androidx.test.ext:junit:1.1.1'
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.horizontalsystems.bitcoincore

import io.horizontalsystems.bitcoincore.core.IPluginData
import io.horizontalsystems.bitcoincore.models.BitcoinPaymentData
import io.horizontalsystems.bitcoincore.models.BitcoinSendInfo
import io.horizontalsystems.bitcoincore.models.PublicKey
import io.horizontalsystems.bitcoincore.models.TransactionDataSortType
import io.horizontalsystems.bitcoincore.models.TransactionFilterType
Expand All @@ -10,6 +11,7 @@ import io.horizontalsystems.bitcoincore.models.UsedAddress
import io.horizontalsystems.bitcoincore.network.Network
import io.horizontalsystems.bitcoincore.storage.FullTransaction
import io.horizontalsystems.bitcoincore.storage.UnspentOutput
import io.horizontalsystems.bitcoincore.storage.UnspentOutputInfo
import io.horizontalsystems.bitcoincore.transactions.scripts.ScriptType
import io.reactivex.Single

Expand All @@ -18,6 +20,9 @@ abstract class AbstractKit {
protected abstract var bitcoinCore: BitcoinCore
protected abstract var network: Network

val unspentOutputs
get() = bitcoinCore.unspentOutputs

val balance
get() = bitcoinCore.balance

Expand Down Expand Up @@ -52,8 +57,6 @@ abstract class AbstractKit {
bitcoinCore.onEnterBackground()
}

fun getSpendableUtxo() = bitcoinCore.getSpendableUtxo()

fun transactions(fromUid: String? = null, type: TransactionFilterType? = null, limit: Int? = null): Single<List<TransactionInfo>> {
return bitcoinCore.transactions(fromUid, type, limit)
}
Expand All @@ -62,27 +65,34 @@ abstract class AbstractKit {
return bitcoinCore.getTransaction(hash)
}

fun fee(
unspentOutputs: List<UnspentOutput>,
fun sendInfo(
value: Long,
address: String? = null,
senderPay: Boolean = true,
feeRate: Int,
pluginData: Map<Byte, IPluginData>
): Long {
return bitcoinCore.fee(unspentOutputs, address, feeRate, pluginData)
}

fun fee(value: Long, address: String? = null, senderPay: Boolean = true, feeRate: Int, pluginData: Map<Byte, IPluginData> = mapOf()): Long {
return bitcoinCore.fee(value, address, senderPay, feeRate, pluginData)
unspentOutputs: List<UnspentOutputInfo>?,
pluginData: Map<Byte, IPluginData> = mapOf()
): BitcoinSendInfo {
return bitcoinCore.sendInfo(
value = value,
address = address,
senderPay = senderPay,
feeRate = feeRate,
unspentOutputs = unspentOutputs,
pluginData = pluginData
)
}

fun send(
address: String,
unspentOutputs: List<UnspentOutput>,
value: Long,
senderPay: Boolean = true,
feeRate: Int,
sortType: TransactionDataSortType,
pluginData: Map<Byte, IPluginData>
unspentOutputs: List<UnspentOutputInfo>? = null,
pluginData: Map<Byte, IPluginData> = mapOf()
): FullTransaction {
return bitcoinCore.send(address, unspentOutputs, feeRate, sortType, pluginData)
return bitcoinCore.send(address, value, senderPay, feeRate, sortType, unspentOutputs, pluginData)
}

fun send(
Expand All @@ -93,7 +103,7 @@ abstract class AbstractKit {
sortType: TransactionDataSortType,
pluginData: Map<Byte, IPluginData> = mapOf()
): FullTransaction {
return bitcoinCore.send(address, value, senderPay, feeRate, sortType, pluginData)
return bitcoinCore.send(address, value, senderPay, feeRate, sortType, null, pluginData)
}

fun send(
Expand All @@ -102,9 +112,21 @@ abstract class AbstractKit {
value: Long,
senderPay: Boolean = true,
feeRate: Int,
sortType: TransactionDataSortType
sortType: TransactionDataSortType,
unspentOutputs: List<UnspentOutputInfo>? = null,
): FullTransaction {
return bitcoinCore.send(hash, scriptType, value, senderPay, feeRate, sortType, unspentOutputs)
}

fun send(
hash: ByteArray,
scriptType: ScriptType,
value: Long,
senderPay: Boolean = true,
feeRate: Int,
sortType: TransactionDataSortType,
): FullTransaction {
return bitcoinCore.send(hash, scriptType, value, senderPay, feeRate, sortType)
return bitcoinCore.send(hash, scriptType, value, senderPay, feeRate, sortType, null)
}

fun redeem(unspentOutput: UnspentOutput, address: String, feeRate: Int, sortType: TransactionDataSortType): FullTransaction {
Expand All @@ -115,8 +137,8 @@ abstract class AbstractKit {
return bitcoinCore.receiveAddress()
}

fun usedAddresses(): List<UsedAddress> {
return bitcoinCore.usedAddresses()
fun usedAddresses(change: Boolean): List<UsedAddress> {
return bitcoinCore.usedAddresses(change)
}

fun receivePublicKey(): PublicKey {
Expand Down Expand Up @@ -151,8 +173,8 @@ abstract class AbstractKit {
bitcoinCore.watchTransaction(filter, listener)
}

fun maximumSpendableValue(address: String?, feeRate: Int, pluginData: Map<Byte, IPluginData>): Long {
return bitcoinCore.maximumSpendableValue(address, feeRate, pluginData)
fun maximumSpendableValue(address: String?, feeRate: Int, unspentOutputs: List<UnspentOutputInfo>?, pluginData: Map<Byte, IPluginData>): Long {
return bitcoinCore.maximumSpendableValue(address, feeRate, unspentOutputs, pluginData)
}

fun minimumSpendableValue(address: String?): Int {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import io.horizontalsystems.bitcoincore.managers.SyncManager
import io.horizontalsystems.bitcoincore.managers.UnspentOutputSelectorChain
import io.horizontalsystems.bitcoincore.models.BalanceInfo
import io.horizontalsystems.bitcoincore.models.BitcoinPaymentData
import io.horizontalsystems.bitcoincore.models.BitcoinSendInfo
import io.horizontalsystems.bitcoincore.models.BlockInfo
import io.horizontalsystems.bitcoincore.models.PublicKey
import io.horizontalsystems.bitcoincore.models.TransactionDataSortType
Expand All @@ -37,6 +38,7 @@ import io.horizontalsystems.bitcoincore.network.peer.PeerManager
import io.horizontalsystems.bitcoincore.network.peer.PeerTaskHandlerChain
import io.horizontalsystems.bitcoincore.storage.FullTransaction
import io.horizontalsystems.bitcoincore.storage.UnspentOutput
import io.horizontalsystems.bitcoincore.storage.UnspentOutputInfo
import io.horizontalsystems.bitcoincore.transactions.TransactionCreator
import io.horizontalsystems.bitcoincore.transactions.TransactionFeeCalculator
import io.horizontalsystems.bitcoincore.transactions.TransactionSyncer
Expand All @@ -49,6 +51,7 @@ import io.horizontalsystems.hdwalletkit.HDWallet.Purpose
import io.reactivex.Single
import java.util.Date
import java.util.concurrent.Executor
import kotlin.math.max
import kotlin.math.roundToInt

class BitcoinCore(
Expand Down Expand Up @@ -141,6 +144,11 @@ class BitcoinCore(
val watchAccount: Boolean
get() = transactionCreator == null

val unspentOutputs: List<UnspentOutputInfo>
get() = unspentOutputSelector.all.map {
UnspentOutputInfo.fromUnspentOutput(it)
}

//
// API methods
//
Expand Down Expand Up @@ -169,45 +177,52 @@ class BitcoinCore(
return dataProvider.transactions(fromUid, type, limit)
}

fun fee(value: Long, address: String? = null, senderPay: Boolean = true, feeRate: Int, pluginData: Map<Byte, IPluginData>): Long {
return transactionFeeCalculator?.fee(value, feeRate, senderPay, address, pluginData) ?: throw CoreError.ReadOnlyCore
}

fun fee(
unspentOutputs: List<UnspentOutput>,
fun sendInfo(
value: Long,
address: String? = null,
senderPay: Boolean = true,
feeRate: Int,
unspentOutputs: List<UnspentOutputInfo>?,
pluginData: Map<Byte, IPluginData>
): Long {
return transactionFeeCalculator?.fee(
unspentOutputs,
feeRate,
address,
pluginData
): BitcoinSendInfo {
val outputs = unspentOutputs?.mapNotNull {
unspentOutputSelector.all.firstOrNull { unspentOutput ->
unspentOutput.transaction.hash.contentEquals(it.transactionHash) && unspentOutput.output.index == it.outputIndex
}
}
return transactionFeeCalculator?.sendInfo(
value = value,
feeRate = feeRate,
senderPay = senderPay,
toAddress = address,
unspentOutputs = outputs,
pluginData = pluginData
) ?: throw CoreError.ReadOnlyCore
}

fun getSpendableUtxo() = dataProvider.getSpendableUtxo()

fun send(
address: String,
unspentOutputs: List<UnspentOutput>,
feeRate: Int,
sortType: TransactionDataSortType,
pluginData: Map<Byte, IPluginData>
): FullTransaction {
return transactionCreator?.create(address, unspentOutputs, feeRate, sortType, pluginData) ?: throw CoreError.ReadOnlyCore
}

fun send(
address: String,
value: Long,
senderPay: Boolean = true,
feeRate: Int,
sortType: TransactionDataSortType,
unspentOutputs: List<UnspentOutputInfo>?,
pluginData: Map<Byte, IPluginData>
): FullTransaction {
return transactionCreator?.create(address, value, feeRate, senderPay, sortType, pluginData) ?: throw CoreError.ReadOnlyCore
val outputs = unspentOutputs?.mapNotNull {
unspentOutputSelector.all.firstOrNull { unspentOutput ->
unspentOutput.transaction.hash.contentEquals(it.transactionHash) && unspentOutput.output.index == it.outputIndex
}
}
return transactionCreator?.create(
toAddress = address,
value = value,
feeRate = feeRate,
senderPay = senderPay,
sortType = sortType,
unspentOutputs = outputs,
pluginData = pluginData
) ?: throw CoreError.ReadOnlyCore
}

fun send(
Expand All @@ -216,10 +231,24 @@ class BitcoinCore(
value: Long,
senderPay: Boolean = true,
feeRate: Int,
sortType: TransactionDataSortType
sortType: TransactionDataSortType,
unspentOutputs: List<UnspentOutputInfo>?,
): FullTransaction {
val address = addressConverter.convert(hash, scriptType)
return transactionCreator?.create(address.stringValue, value, feeRate, senderPay, sortType, mapOf()) ?: throw CoreError.ReadOnlyCore
val outputs = unspentOutputs?.mapNotNull {
unspentOutputSelector.all.firstOrNull { unspentOutput ->
unspentOutput.transaction.hash.contentEquals(it.transactionHash) && unspentOutput.output.index == it.outputIndex
}
}
return transactionCreator?.create(
toAddress = address.stringValue,
value = value,
feeRate = feeRate,
senderPay = senderPay,
sortType = sortType,
unspentOutputs = outputs,
pluginData = mapOf()
) ?: throw CoreError.ReadOnlyCore
}

fun redeem(unspentOutput: UnspentOutput, address: String, feeRate: Int, sortType: TransactionDataSortType): FullTransaction {
Expand All @@ -230,8 +259,8 @@ class BitcoinCore(
return addressConverter.convert(publicKeyManager.receivePublicKey(), purpose.scriptType).stringValue
}

fun usedAddresses(): List<UsedAddress> {
return publicKeyManager.usedExternalPublicKeys().map {
fun usedAddresses(change: Boolean): List<UsedAddress> {
return publicKeyManager.usedExternalPublicKeys(change).map {
UsedAddress(
index = it.index,
address = addressConverter.convert(it, purpose.scriptType).stringValue
Expand Down Expand Up @@ -360,10 +389,32 @@ class BitcoinCore(
watchedTransactionManager.add(filter, listener)
}

fun maximumSpendableValue(address: String?, feeRate: Int, pluginData: Map<Byte, IPluginData>): Long {
return transactionFeeCalculator?.let { transactionFeeCalculator ->
balance.spendable - transactionFeeCalculator.fee(balance.spendable, feeRate, false, address, pluginData)
} ?: throw CoreError.ReadOnlyCore
fun maximumSpendableValue(
address: String?,
feeRate: Int,
unspentOutputs: List<UnspentOutputInfo>?,
pluginData: Map<Byte, IPluginData>
): Long {
if (transactionFeeCalculator == null) throw CoreError.ReadOnlyCore

val outputs = unspentOutputs?.mapNotNull {
unspentOutputSelector.all.firstOrNull { unspentOutput ->
unspentOutput.transaction.hash.contentEquals(it.transactionHash) && unspentOutput.output.index == it.outputIndex
}
}

val spendableBalance = outputs?.sumOf { it.output.value } ?: balance.spendable

val sendAllFee = transactionFeeCalculator.sendInfo(
value = spendableBalance,
feeRate = feeRate,
senderPay = false,
toAddress = address,
unspentOutputs = outputs,
pluginData = pluginData
).fee

return max(0L, spendableBalance - sendAllFee)
}

fun minimumSpendableValue(address: String?): Int {
Expand Down
Loading
Loading