From 1c802e29282d22469030b8ebb8d0c49124e25334 Mon Sep 17 00:00:00 2001 From: abdrasulov Date: Tue, 9 Apr 2024 16:08:57 +0600 Subject: [PATCH] Add memo support --- .../bitcoinkit/demo/MainViewModel.kt | 12 ++++++-- .../bitcoinkit/demo/TransactionsFragment.kt | 4 ++- .../bitcoincore/AbstractKit.kt | 22 +++++++++----- .../bitcoincore/BitcoinCore.kt | 12 ++++++-- .../core/BaseTransactionInfoConverter.kt | 24 ++++++++++++++- .../bitcoincore/core/Interfaces.kt | 3 +- .../managers/IUnspentOutputSelector.kt | 30 +++++++++++++++++-- .../managers/UnspentOutputQueue.kt | 2 ++ .../managers/UnspentOutputSelector.kt | 2 ++ .../UnspentOutputSelectorSingleNoChange.kt | 2 ++ .../bitcoincore/models/TransactionInfo.kt | 19 +++++++----- .../transactions/TransactionCreator.kt | 5 +++- .../transactions/TransactionFeeCalculator.kt | 4 ++- .../transactions/TransactionSizeCalculator.kt | 24 +++++++++++++-- .../transactions/builder/InputSetter.kt | 7 +++-- .../builder/MutableTransaction.kt | 2 +- .../transactions/builder/OutputSetter.kt | 13 +++++++- .../transactions/builder/RecipientSetter.kt | 11 +++++-- .../builder/TransactionBuilder.kt | 14 +++++++-- ...UnspentOutputSelectorSingleNoChangeTest.kt | 16 +++++----- .../managers/UnspentOutputSelectorTest.kt | 12 ++++---- 21 files changed, 187 insertions(+), 53 deletions(-) diff --git a/app/src/main/java/io/horizontalsystems/bitcoinkit/demo/MainViewModel.kt b/app/src/main/java/io/horizontalsystems/bitcoinkit/demo/MainViewModel.kt index 586f61ab1..884130651 100644 --- a/app/src/main/java/io/horizontalsystems/bitcoinkit/demo/MainViewModel.kt +++ b/app/src/main/java/io/horizontalsystems/bitcoinkit/demo/MainViewModel.kt @@ -7,7 +7,12 @@ import io.horizontalsystems.bitcoincore.BitcoinCore.KitState import io.horizontalsystems.bitcoincore.core.IPluginData import io.horizontalsystems.bitcoincore.exceptions.AddressFormatException import io.horizontalsystems.bitcoincore.managers.SendValueErrors -import io.horizontalsystems.bitcoincore.models.* +import io.horizontalsystems.bitcoincore.models.BalanceInfo +import io.horizontalsystems.bitcoincore.models.BitcoinSendInfo +import io.horizontalsystems.bitcoincore.models.BlockInfo +import io.horizontalsystems.bitcoincore.models.TransactionDataSortType +import io.horizontalsystems.bitcoincore.models.TransactionFilterType +import io.horizontalsystems.bitcoincore.models.TransactionInfo import io.horizontalsystems.bitcoinkit.BitcoinKit import io.horizontalsystems.hdwalletkit.HDWallet.Purpose import io.horizontalsystems.hodler.HodlerData @@ -156,6 +161,7 @@ class MainViewModel : ViewModel(), BitcoinKit.Listener { try { val transaction = bitcoinKit.send( address!!, + null, amount!!, feeRate = feePriority.feeRate, sortType = TransactionDataSortType.Shuffle, @@ -182,7 +188,7 @@ class MainViewModel : ViewModel(), BitcoinKit.Listener { fun onMaxClick() { try { - amountLiveData.value = bitcoinKit.maximumSpendableValue(address, feePriority.feeRate, null, getPluginData()) + amountLiveData.value = bitcoinKit.maximumSpendableValue(address, null, feePriority.feeRate, null, getPluginData()) } catch (e: Exception) { amountLiveData.value = 0 errorLiveData.value = when (e) { @@ -206,7 +212,7 @@ class MainViewModel : ViewModel(), BitcoinKit.Listener { } private fun fee(value: Long, address: String? = null): BitcoinSendInfo { - return bitcoinKit.sendInfo(value, address, feeRate = feePriority.feeRate, unspentOutputs = null, pluginData = getPluginData()) + return bitcoinKit.sendInfo(value, address, null, feeRate = feePriority.feeRate, unspentOutputs = null, pluginData = getPluginData()) } private fun getPluginData(): MutableMap { diff --git a/app/src/main/java/io/horizontalsystems/bitcoinkit/demo/TransactionsFragment.kt b/app/src/main/java/io/horizontalsystems/bitcoinkit/demo/TransactionsFragment.kt index 87a0eaa26..8704d0bc5 100644 --- a/app/src/main/java/io/horizontalsystems/bitcoinkit/demo/TransactionsFragment.kt +++ b/app/src/main/java/io/horizontalsystems/bitcoinkit/demo/TransactionsFragment.kt @@ -22,7 +22,8 @@ import io.horizontalsystems.dashkit.models.DashTransactionInfo import io.horizontalsystems.hodler.HodlerOutputData import io.horizontalsystems.hodler.HodlerPlugin import java.text.DateFormat -import java.util.* +import java.util.Date +import java.util.Locale class TransactionsFragment : Fragment(), ViewHolderTransaction.Listener { @@ -165,6 +166,7 @@ class ViewHolderTransaction(val containerView: View, private val listener: Liste sb.append("\n value: ${it.value}") sb.append("\n mine: ${it.mine}") sb.append("\n change: ${it.changeOutput}") + sb.append("\n memo: ${it.memo}") if (it.pluginId == HodlerPlugin.id && it.pluginData != null) { (it.pluginData as? HodlerOutputData)?.let { hodlerData -> diff --git a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/AbstractKit.kt b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/AbstractKit.kt index 248519fe4..99cb6121d 100644 --- a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/AbstractKit.kt +++ b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/AbstractKit.kt @@ -71,6 +71,7 @@ abstract class AbstractKit { fun sendInfo( value: Long, address: String? = null, + memo: String?, senderPay: Boolean = true, feeRate: Int, unspentOutputs: List?, @@ -79,6 +80,7 @@ abstract class AbstractKit { return bitcoinCore.sendInfo( value = value, address = address, + memo = memo, senderPay = senderPay, feeRate = feeRate, unspentOutputs = unspentOutputs, @@ -88,6 +90,7 @@ abstract class AbstractKit { fun send( address: String, + memo: String?, value: Long, senderPay: Boolean = true, feeRate: Int, @@ -96,11 +99,12 @@ abstract class AbstractKit { pluginData: Map = mapOf(), rbfEnabled: Boolean, ): FullTransaction { - return bitcoinCore.send(address, value, senderPay, feeRate, sortType, unspentOutputs, pluginData, rbfEnabled) + return bitcoinCore.send(address, memo, value, senderPay, feeRate, sortType, unspentOutputs, pluginData, rbfEnabled) } fun send( address: String, + memo: String?, value: Long, senderPay: Boolean = true, feeRate: Int, @@ -108,11 +112,12 @@ abstract class AbstractKit { pluginData: Map = mapOf(), rbfEnabled: Boolean, ): FullTransaction { - return bitcoinCore.send(address, value, senderPay, feeRate, sortType, null, pluginData, rbfEnabled) + return bitcoinCore.send(address, memo, value, senderPay, feeRate, sortType, null, pluginData, rbfEnabled) } fun send( hash: ByteArray, + memo: String?, scriptType: ScriptType, value: Long, senderPay: Boolean = true, @@ -121,11 +126,12 @@ abstract class AbstractKit { unspentOutputs: List? = null, rbfEnabled: Boolean, ): FullTransaction { - return bitcoinCore.send(hash, scriptType, value, senderPay, feeRate, sortType, unspentOutputs, rbfEnabled) + return bitcoinCore.send(hash, memo, scriptType, value, senderPay, feeRate, sortType, unspentOutputs, rbfEnabled) } fun send( hash: ByteArray, + memo: String?, scriptType: ScriptType, value: Long, senderPay: Boolean = true, @@ -133,11 +139,11 @@ abstract class AbstractKit { sortType: TransactionDataSortType, rbfEnabled: Boolean, ): FullTransaction { - return bitcoinCore.send(hash, scriptType, value, senderPay, feeRate, sortType, null, rbfEnabled) + return bitcoinCore.send(hash, memo, scriptType, value, senderPay, feeRate, sortType, null, rbfEnabled) } - fun redeem(unspentOutput: UnspentOutput, address: String, feeRate: Int, sortType: TransactionDataSortType, rbfEnabled: Boolean): FullTransaction { - return bitcoinCore.redeem(unspentOutput, address, feeRate, sortType, rbfEnabled) + fun redeem(unspentOutput: UnspentOutput, address: String, memo: String?, feeRate: Int, sortType: TransactionDataSortType, rbfEnabled: Boolean): FullTransaction { + return bitcoinCore.redeem(unspentOutput, address, memo, feeRate, sortType, rbfEnabled) } fun receiveAddress(): String { @@ -180,8 +186,8 @@ abstract class AbstractKit { bitcoinCore.watchTransaction(filter, listener) } - fun maximumSpendableValue(address: String?, feeRate: Int, unspentOutputs: List?, pluginData: Map): Long { - return bitcoinCore.maximumSpendableValue(address, feeRate, unspentOutputs, pluginData) + fun maximumSpendableValue(address: String?, memo: String?, feeRate: Int, unspentOutputs: List?, pluginData: Map): Long { + return bitcoinCore.maximumSpendableValue(address, memo, feeRate, unspentOutputs, pluginData) } fun minimumSpendableValue(address: String?): Int { diff --git a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/BitcoinCore.kt b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/BitcoinCore.kt index 2599b0b09..2201e14d3 100644 --- a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/BitcoinCore.kt +++ b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/BitcoinCore.kt @@ -187,6 +187,7 @@ class BitcoinCore( fun sendInfo( value: Long, address: String? = null, + memo: String?, senderPay: Boolean = true, feeRate: Int, unspentOutputs: List?, @@ -202,6 +203,7 @@ class BitcoinCore( feeRate = feeRate, senderPay = senderPay, toAddress = address, + memo = memo, unspentOutputs = outputs, pluginData = pluginData ) ?: throw CoreError.ReadOnlyCore @@ -209,6 +211,7 @@ class BitcoinCore( fun send( address: String, + memo: String?, value: Long, senderPay: Boolean = true, feeRate: Int, @@ -224,6 +227,7 @@ class BitcoinCore( } return transactionCreator?.create( toAddress = address, + memo = memo, value = value, feeRate = feeRate, senderPay = senderPay, @@ -236,6 +240,7 @@ class BitcoinCore( fun send( hash: ByteArray, + memo: String?, scriptType: ScriptType, value: Long, senderPay: Boolean = true, @@ -252,6 +257,7 @@ class BitcoinCore( } return transactionCreator?.create( toAddress = address.stringValue, + memo = memo, value = value, feeRate = feeRate, senderPay = senderPay, @@ -262,8 +268,8 @@ class BitcoinCore( ) ?: throw CoreError.ReadOnlyCore } - fun redeem(unspentOutput: UnspentOutput, address: String, feeRate: Int, sortType: TransactionDataSortType, rbfEnabled: Boolean): FullTransaction { - return transactionCreator?.create(unspentOutput, address, feeRate, sortType, rbfEnabled) ?: throw CoreError.ReadOnlyCore + fun redeem(unspentOutput: UnspentOutput, address: String, memo: String?, feeRate: Int, sortType: TransactionDataSortType, rbfEnabled: Boolean): FullTransaction { + return transactionCreator?.create(unspentOutput, address, memo, feeRate, sortType, rbfEnabled) ?: throw CoreError.ReadOnlyCore } fun receiveAddress(): String { @@ -406,6 +412,7 @@ class BitcoinCore( fun maximumSpendableValue( address: String?, + memo: String?, feeRate: Int, unspentOutputs: List?, pluginData: Map @@ -425,6 +432,7 @@ class BitcoinCore( feeRate = feeRate, senderPay = false, toAddress = address, + memo = memo, unspentOutputs = outputs, pluginData = pluginData ).fee diff --git a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/core/BaseTransactionInfoConverter.kt b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/core/BaseTransactionInfoConverter.kt index 84bb60fbe..ce333a8b4 100644 --- a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/core/BaseTransactionInfoConverter.kt +++ b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/core/BaseTransactionInfoConverter.kt @@ -1,8 +1,19 @@ package io.horizontalsystems.bitcoincore.core import io.horizontalsystems.bitcoincore.extensions.toReversedHex -import io.horizontalsystems.bitcoincore.models.* +import io.horizontalsystems.bitcoincore.io.BitcoinInput +import io.horizontalsystems.bitcoincore.models.InvalidTransaction +import io.horizontalsystems.bitcoincore.models.Transaction +import io.horizontalsystems.bitcoincore.models.TransactionInfo +import io.horizontalsystems.bitcoincore.models.TransactionInputInfo +import io.horizontalsystems.bitcoincore.models.TransactionMetadata +import io.horizontalsystems.bitcoincore.models.TransactionOutput +import io.horizontalsystems.bitcoincore.models.TransactionOutputInfo +import io.horizontalsystems.bitcoincore.models.TransactionStatus +import io.horizontalsystems.bitcoincore.models.rbfEnabled import io.horizontalsystems.bitcoincore.storage.FullTransactionInfo +import io.horizontalsystems.bitcoincore.transactions.scripts.ScriptType +import java.io.ByteArrayInputStream class BaseTransactionInfoConverter(private val pluginManager: PluginManager) { @@ -37,6 +48,7 @@ class BaseTransactionInfoConverter(private val pluginManager: PluginManager) { changeOutput = output.changeOutput, value = output.value, address = output.address, + memo = parseMemo(output), pluginId = output.pluginId, pluginDataString = output.pluginData, pluginData = pluginManager.parsePluginData(output, transaction.timestamp)) @@ -63,6 +75,16 @@ class BaseTransactionInfoConverter(private val pluginManager: PluginManager) { ) } + private fun parseMemo(output: TransactionOutput): String? { + if (output.scriptType != ScriptType.NULL_DATA) return null + val payload = output.lockingScriptPayload ?: return null + if (payload.isEmpty()) return null + + val input = BitcoinInput(ByteArrayInputStream(payload)) + input.readByte() // op_return + return input.readString() + } + private fun getInvalidTransactionInfo( transaction: InvalidTransaction, metadata: TransactionMetadata diff --git a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/core/Interfaces.kt b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/core/Interfaces.kt index 6cf30f9e1..475ff2f07 100644 --- a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/core/Interfaces.kt +++ b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/core/Interfaces.kt @@ -195,7 +195,8 @@ interface IRecipientSetter { toAddress: String, value: Long, pluginData: Map, - skipChecking: Boolean + skipChecking: Boolean, + memo: String? ) } diff --git a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/managers/IUnspentOutputSelector.kt b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/managers/IUnspentOutputSelector.kt index 7f7bc03a3..3a44b162e 100644 --- a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/managers/IUnspentOutputSelector.kt +++ b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/managers/IUnspentOutputSelector.kt @@ -4,7 +4,15 @@ import io.horizontalsystems.bitcoincore.storage.UnspentOutput import io.horizontalsystems.bitcoincore.transactions.scripts.ScriptType interface IUnspentOutputSelector { - fun select(value: Long, feeRate: Int, outputScriptType: ScriptType = ScriptType.P2PKH, changeType: ScriptType = ScriptType.P2PKH, senderPay: Boolean, pluginDataOutputSize: Int): SelectedUnspentOutputInfo + fun select( + value: Long, + memo: String?, + feeRate: Int, + outputScriptType: ScriptType = ScriptType.P2PKH, + changeType: ScriptType = ScriptType.P2PKH, + senderPay: Boolean, + pluginDataOutputSize: Int + ): SelectedUnspentOutputInfo } data class SelectedUnspentOutputInfo( @@ -27,12 +35,28 @@ class UnspentOutputSelectorChain(private val unspentOutputProvider: IUnspentOutp val all: List get() = unspentOutputProvider.getSpendableUtxo() - override fun select(value: Long, feeRate: Int, outputScriptType: ScriptType, changeType: ScriptType, senderPay: Boolean, pluginDataOutputSize: Int): SelectedUnspentOutputInfo { + override fun select( + value: Long, + memo: String?, + feeRate: Int, + outputScriptType: ScriptType, + changeType: ScriptType, + senderPay: Boolean, + pluginDataOutputSize: Int + ): SelectedUnspentOutputInfo { var lastError: SendValueErrors? = null for (selector in concreteSelectors) { try { - return selector.select(value, feeRate, outputScriptType, changeType, senderPay, pluginDataOutputSize) + return selector.select( + value, + memo, + feeRate, + outputScriptType, + changeType, + senderPay, + pluginDataOutputSize + ) } catch (e: SendValueErrors) { lastError = e } diff --git a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/managers/UnspentOutputQueue.kt b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/managers/UnspentOutputQueue.kt index 58077e469..ec1717377 100644 --- a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/managers/UnspentOutputQueue.kt +++ b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/managers/UnspentOutputQueue.kt @@ -60,6 +60,7 @@ class UnspentOutputQueue( sizeCalculator.transactionSize( previousOutputs = selectedOutputs.map { it.output }, outputs = listOf(parameters.outputScriptType), + memo = parameters.memo, pluginDataOutputSize = parameters.pluginDataOutputSize ) * parameters.fee @@ -83,6 +84,7 @@ class UnspentOutputQueue( data class Parameters( val value: Long, val senderPay: Boolean, + val memo: String?, val fee: Int, val outputsLimit: Int?, val outputScriptType: ScriptType, diff --git a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/managers/UnspentOutputSelector.kt b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/managers/UnspentOutputSelector.kt index 9b563cfe6..7450380a6 100644 --- a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/managers/UnspentOutputSelector.kt +++ b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/managers/UnspentOutputSelector.kt @@ -18,6 +18,7 @@ class UnspentOutputSelector( @Throws(SendValueErrors::class) override fun select( value: Long, + memo: String?, feeRate: Int, outputScriptType: ScriptType, changeType: ScriptType, @@ -39,6 +40,7 @@ class UnspentOutputSelector( val params = UnspentOutputQueue.Parameters( value = value, senderPay = senderPay, + memo = memo, fee = feeRate, outputsLimit = outputsLimit, outputScriptType = outputScriptType, diff --git a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/managers/UnspentOutputSelectorSingleNoChange.kt b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/managers/UnspentOutputSelectorSingleNoChange.kt index 1903ed721..5a005d016 100644 --- a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/managers/UnspentOutputSelectorSingleNoChange.kt +++ b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/managers/UnspentOutputSelectorSingleNoChange.kt @@ -13,6 +13,7 @@ class UnspentOutputSelectorSingleNoChange( override fun select( value: Long, + memo: String?, feeRate: Int, outputScriptType: ScriptType, changeType: ScriptType, @@ -42,6 +43,7 @@ class UnspentOutputSelectorSingleNoChange( val params = UnspentOutputQueue.Parameters( value = value, senderPay = senderPay, + memo = memo, fee = feeRate, outputsLimit = null, outputScriptType = outputScriptType, diff --git a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/models/TransactionInfo.kt b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/models/TransactionInfo.kt index 3bea1fd6a..48f9ce37b 100644 --- a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/models/TransactionInfo.kt +++ b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/models/TransactionInfo.kt @@ -98,6 +98,7 @@ open class TransactionInfo { changeOutput = it.get("changeOutput").asBoolean(), value = it.get("value").asLong(), address = if (it.get("address")?.isNull == false) it.get("address")?.asString() else null, + memo = if (it.get("memo")?.isNull == false) it.get("memo")?.asString() else null, pluginId = if (it.get("pluginId")?.isNull == false) it.get("pluginId")?.asString()?.toByte() else null, pluginDataString = if (it.get("pluginDataString")?.isNull == false) it.get("pluginDataString")?.asString() else null) @@ -117,6 +118,7 @@ open class TransactionInfo { outputObj.add("address", it.address) outputObj.add("pluginId", it.pluginId?.toString()) outputObj.add("pluginDataString", it.pluginDataString) + outputObj.add("memo", it.memo) jsonArray.add(outputObj) } return jsonArray @@ -173,13 +175,16 @@ enum class TransactionStatus(val code: Int) { data class TransactionInputInfo(val mine: Boolean, val value: Long? = null, val address: String? = null) -data class TransactionOutputInfo(val mine: Boolean, - val changeOutput: Boolean, - val value: Long, - val address: String? = null, - val pluginId: Byte? = null, - val pluginData: IPluginOutputData? = null, - internal val pluginDataString: String? = null) +data class TransactionOutputInfo( + val mine: Boolean, + val changeOutput: Boolean, + val value: Long, + val address: String? = null, + val memo: String?, + val pluginId: Byte? = null, + val pluginData: IPluginOutputData? = null, + internal val pluginDataString: String? = null, +) data class BlockInfo( val headerHash: String, diff --git a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/TransactionCreator.kt b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/TransactionCreator.kt index 3b3d036b7..fd97e336e 100644 --- a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/TransactionCreator.kt +++ b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/TransactionCreator.kt @@ -20,6 +20,7 @@ class TransactionCreator( @Throws fun create( toAddress: String, + memo: String?, value: Long, feeRate: Int, senderPay: Boolean, @@ -30,6 +31,7 @@ class TransactionCreator( ): FullTransaction { val mutableTransaction = builder.buildTransaction( toAddress = toAddress, + memo = memo, value = value, feeRate = feeRate, senderPay = senderPay, @@ -46,11 +48,12 @@ class TransactionCreator( fun create( unspentOutput: UnspentOutput, toAddress: String, + memo: String?, feeRate: Int, sortType: TransactionDataSortType, rbfEnabled: Boolean ): FullTransaction { - val mutableTransaction = builder.buildTransaction(unspentOutput, toAddress, feeRate, sortType, rbfEnabled) + val mutableTransaction = builder.buildTransaction(unspentOutput, toAddress, memo, feeRate, sortType, rbfEnabled) return create(mutableTransaction) } diff --git a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/TransactionFeeCalculator.kt b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/TransactionFeeCalculator.kt index 1afdab919..edc3a5b4d 100644 --- a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/TransactionFeeCalculator.kt +++ b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/TransactionFeeCalculator.kt @@ -24,6 +24,7 @@ class TransactionFeeCalculator( feeRate: Int, senderPay: Boolean, toAddress: String?, + memo: String?, unspentOutputs: List?, pluginData: Map ): BitcoinSendInfo { @@ -34,7 +35,8 @@ class TransactionFeeCalculator( toAddress = toAddress ?: sampleAddress(), value = value, pluginData = pluginData, - skipChecking = true + skipChecking = true, + memo = memo ) val outputInfo = inputSetter.setInputs( diff --git a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/TransactionSizeCalculator.kt b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/TransactionSizeCalculator.kt index a91a1a5df..2e034337f 100644 --- a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/TransactionSizeCalculator.kt +++ b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/TransactionSizeCalculator.kt @@ -77,7 +77,18 @@ class TransactionSizeCalculator { return 32 + 4 + 1 + scriptSigLength + 4 // PreviousOutputHex + InputIndex + sigLength + scriptSig + sequence } - fun transactionSize(previousOutputs: List, outputs: List): Long { + private fun getMemoSize(memo: String?): Int { + if (memo == null) return 0 + + val memoData = memo.toByteArray(Charsets.UTF_8) + return outputSizeByScriptSize(memoData.size) * 4 + } + + fun transactionSize( + previousOutputs: List, + outputs: List, + memo: String? = null, + ): Long { val txIsWitness = previousOutputs.any { it.scriptType.isWitness } val txWeight = if (txIsWitness) witnessTx else legacyTx val inputWeight = previousOutputs.sumOf { inputSize(it) * 4 + if (txIsWitness) witnessSize(it.scriptType) else 0 } @@ -91,16 +102,25 @@ class TransactionSizeCalculator { } } + outputWeight += getMemoSize(memo) + return toBytes(txWeight + inputWeight + outputWeight).toLong() } - fun transactionSize(previousOutputs: List, outputs: List, pluginDataOutputSize: Int): Long { + fun transactionSize( + previousOutputs: List, + outputs: List, + memo: String?, + pluginDataOutputSize: Int, + ): Long { val txIsWitness = previousOutputs.any { it.scriptType.isWitness } val txWeight = if (txIsWitness) witnessTx else legacyTx val inputWeight = previousOutputs.map { inputSize(it) * 4 + if (txIsWitness) witnessSize(it.scriptType) else 0 }.sum() var outputWeight = outputs.map { outputSize(it) }.sum() * 4 // to vbytes + outputWeight += getMemoSize(memo) + if (pluginDataOutputSize > 0) { outputWeight += outputSizeByScriptSize(pluginDataOutputSize) * 4 } diff --git a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/builder/InputSetter.kt b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/builder/InputSetter.kt index e38d67252..53f9d8253 100644 --- a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/builder/InputSetter.kt +++ b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/builder/InputSetter.kt @@ -42,6 +42,7 @@ class InputSetter( transactionSizeCalculator.transactionSize( previousOutputs = listOf(unspentOutput.output), outputs = listOf(mutableTransaction.recipientAddress.scriptType), + memo = mutableTransaction.memo, pluginDataOutputSize = 0 ) @@ -68,6 +69,7 @@ class InputSetter( val params = UnspentOutputQueue.Parameters( value = mutableTransaction.recipientValue, senderPay = senderPay, + memo = mutableTransaction.memo, fee = feeRate, outputsLimit = null, outputScriptType = mutableTransaction.recipientAddress.scriptType, @@ -85,9 +87,10 @@ class InputSetter( val value = mutableTransaction.recipientValue unspentOutputInfo = unspentOutputSelector.select( value, + mutableTransaction.memo, feeRate, - mutableTransaction.recipientAddress.scriptType, - changeScriptType, // Assuming changeScriptType is defined somewhere + mutableTransaction.recipientAddress.scriptType, // Assuming changeScriptType is defined somewhere + changeScriptType, senderPay, mutableTransaction.getPluginDataOutputSize() ) diff --git a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/builder/MutableTransaction.kt b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/builder/MutableTransaction.kt index b07c65913..aaea497e5 100644 --- a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/builder/MutableTransaction.kt +++ b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/builder/MutableTransaction.kt @@ -14,7 +14,7 @@ class MutableTransaction(isOutgoing: Boolean = true) { lateinit var recipientAddress: Address var recipientValue = 0L - + var memo: String? = null var changeAddress: Address? = null var changeValue = 0L diff --git a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/builder/OutputSetter.kt b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/builder/OutputSetter.kt index b363b6d0e..60d03e23f 100644 --- a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/builder/OutputSetter.kt +++ b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/builder/OutputSetter.kt @@ -1,6 +1,7 @@ package io.horizontalsystems.bitcoincore.transactions.builder import io.horizontalsystems.bitcoincore.core.ITransactionDataSorterFactory +import io.horizontalsystems.bitcoincore.io.BitcoinOutput import io.horizontalsystems.bitcoincore.models.TransactionDataSortType import io.horizontalsystems.bitcoincore.models.TransactionOutput import io.horizontalsystems.bitcoincore.transactions.scripts.OP_RETURN @@ -28,7 +29,17 @@ class OutputSetter(private val transactionDataSorterFactory: ITransactionDataSor list.add(TransactionOutput(0, 0, data, ScriptType.NULL_DATA)) } - val sorted = transactionDataSorterFactory.sorter(sortType).sortOutputs(list) + val sorted = transactionDataSorterFactory.sorter(sortType).sortOutputs(list).toMutableList() + + transaction.memo?.let { memo -> + val data = BitcoinOutput() + .writeByte(OP_RETURN) + .writeString(memo) + .toByteArray() + + sorted.add(TransactionOutput(0, 0, data, ScriptType.NULL_DATA)) + } + sorted.forEachIndexed { index, transactionOutput -> transactionOutput.index = index } diff --git a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/builder/RecipientSetter.kt b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/builder/RecipientSetter.kt index 5797b4e70..8b8ca6019 100644 --- a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/builder/RecipientSetter.kt +++ b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/builder/RecipientSetter.kt @@ -3,7 +3,6 @@ package io.horizontalsystems.bitcoincore.transactions.builder import io.horizontalsystems.bitcoincore.core.IPluginData import io.horizontalsystems.bitcoincore.core.IRecipientSetter import io.horizontalsystems.bitcoincore.core.PluginManager -import io.horizontalsystems.bitcoincore.transactions.builder.MutableTransaction import io.horizontalsystems.bitcoincore.utils.IAddressConverter class RecipientSetter( @@ -11,9 +10,17 @@ class RecipientSetter( private val pluginManager: PluginManager ) : IRecipientSetter { - override fun setRecipient(mutableTransaction: MutableTransaction, toAddress: String, value: Long, pluginData: Map, skipChecking: Boolean) { + override fun setRecipient( + mutableTransaction: MutableTransaction, + toAddress: String, + value: Long, + pluginData: Map, + skipChecking: Boolean, + memo: String? + ) { mutableTransaction.recipientAddress = addressConverter.convert(toAddress) mutableTransaction.recipientValue = value + mutableTransaction.memo = memo pluginManager.processOutputs(mutableTransaction, pluginData, skipChecking) } diff --git a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/builder/TransactionBuilder.kt b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/builder/TransactionBuilder.kt index ec2b7ad4c..478f348fe 100644 --- a/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/builder/TransactionBuilder.kt +++ b/bitcoincore/src/main/kotlin/io/horizontalsystems/bitcoincore/transactions/builder/TransactionBuilder.kt @@ -3,7 +3,6 @@ package io.horizontalsystems.bitcoincore.transactions.builder import io.horizontalsystems.bitcoincore.core.IPluginData import io.horizontalsystems.bitcoincore.core.IRecipientSetter import io.horizontalsystems.bitcoincore.models.TransactionDataSortType -import io.horizontalsystems.bitcoincore.storage.FullTransaction import io.horizontalsystems.bitcoincore.storage.UnspentOutput class TransactionBuilder( @@ -15,6 +14,7 @@ class TransactionBuilder( fun buildTransaction( toAddress: String, + memo: String?, value: Long, feeRate: Int, senderPay: Boolean, @@ -25,7 +25,7 @@ class TransactionBuilder( ): MutableTransaction { val mutableTransaction = MutableTransaction() - recipientSetter.setRecipient(mutableTransaction, toAddress, value, pluginData, false) + recipientSetter.setRecipient(mutableTransaction, toAddress, value, pluginData, false, memo) inputSetter.setInputs(mutableTransaction, feeRate, senderPay, unspentOutputs, sortType, rbfEnabled) lockTimeSetter.setLockTime(mutableTransaction) @@ -37,13 +37,21 @@ class TransactionBuilder( fun buildTransaction( unspentOutput: UnspentOutput, toAddress: String, + memo: String?, feeRate: Int, sortType: TransactionDataSortType, rbfEnabled: Boolean ): MutableTransaction { val mutableTransaction = MutableTransaction(false) - recipientSetter.setRecipient(mutableTransaction, toAddress, unspentOutput.output.value, mapOf(), false) + recipientSetter.setRecipient( + mutableTransaction, + toAddress, + unspentOutput.output.value, + mapOf(), + false, + memo + ) inputSetter.setInputs(mutableTransaction, unspentOutput, feeRate, rbfEnabled) lockTimeSetter.setLockTime(mutableTransaction) diff --git a/bitcoincore/src/test/kotlin/io/horizontalsystems/bitcoincore/managers/UnspentOutputSelectorSingleNoChangeTest.kt b/bitcoincore/src/test/kotlin/io/horizontalsystems/bitcoincore/managers/UnspentOutputSelectorSingleNoChangeTest.kt index f78c2130a..1238528a8 100644 --- a/bitcoincore/src/test/kotlin/io/horizontalsystems/bitcoincore/managers/UnspentOutputSelectorSingleNoChangeTest.kt +++ b/bitcoincore/src/test/kotlin/io/horizontalsystems/bitcoincore/managers/UnspentOutputSelectorSingleNoChangeTest.kt @@ -35,7 +35,7 @@ class UnspentOutputSelectorSingleNoChangeTest { `when`(dustCalculator.dust(any())).thenReturn(dust) assertThrows(SendValueErrors.Dust::class.java) { - selector.select(value, 100, ScriptType.P2PKH, ScriptType.P2WPKH, false, 0) + selector.select(value, null, 100, ScriptType.P2PKH, ScriptType.P2WPKH, false, 0) } } @@ -46,7 +46,7 @@ class UnspentOutputSelectorSingleNoChangeTest { `when`(unspentOutputProvider.getSpendableUtxo()).thenReturn(emptyList()) assertThrows(SendValueErrors.EmptyOutputs::class.java) { - selector.select(10000, 100, ScriptType.P2PKH, ScriptType.P2WPKH, false, 0) + selector.select(10000, null, 100, ScriptType.P2PKH, ScriptType.P2WPKH, false, 0) } } @@ -64,7 +64,7 @@ class UnspentOutputSelectorSingleNoChangeTest { `when`(unspentOutputProvider.getSpendableUtxo()).thenReturn(outputs) `when`(dustCalculator.dust(any())).thenReturn(dust) `when`(calculator.inputSize(any())).thenReturn(10) - `when`(calculator.outputSize(any())).thenReturn(2) +// `when`(calculator.outputSize(any())).thenReturn(2) `when`(calculator.transactionSize( ArgumentMatchers.anyList(), ArgumentMatchers.anyList(), any())).thenReturn(30) @@ -72,7 +72,7 @@ class UnspentOutputSelectorSingleNoChangeTest { `when`(queueParams.fee).thenReturn(fee) assertThrows(SendValueErrors.NoSingleOutput::class.java) { - selector.select(value, 100, ScriptType.P2PKH, ScriptType.P2WPKH, false, 0) + selector.select(value, null, 100, ScriptType.P2PKH, ScriptType.P2WPKH, false, 0) } } @@ -91,7 +91,7 @@ class UnspentOutputSelectorSingleNoChangeTest { `when`(unspentOutputProvider.getSpendableUtxo()).thenReturn(outputs) `when`(dustCalculator.dust(any())).thenReturn(dust) `when`(calculator.inputSize(any())).thenReturn(10) - `when`(calculator.outputSize(any())).thenReturn(2) +// `when`(calculator.outputSize(any())).thenReturn(2) `when`(calculator.transactionSize( ArgumentMatchers.anyList(), ArgumentMatchers.anyList(), any())).thenReturn(30) @@ -99,7 +99,7 @@ class UnspentOutputSelectorSingleNoChangeTest { `when`(queueParams.fee).thenReturn(fee) val selectedInfo = - selector.select(value, feeRate, ScriptType.P2PKH, ScriptType.P2WPKH, false, 0) + selector.select(value, null, feeRate, ScriptType.P2PKH, ScriptType.P2WPKH, false, 0) Assert.assertEquals(null, selectedInfo.changeValue) Assert.assertEquals(1, selectedInfo.outputs.size) Assert.assertArrayEquals(arrayOf(outputs[1]), selectedInfo.outputs.toTypedArray()) @@ -119,7 +119,7 @@ class UnspentOutputSelectorSingleNoChangeTest { `when`(unspentOutputProvider.getSpendableUtxo()).thenReturn(outputs) `when`(dustCalculator.dust(any())).thenReturn(dust) `when`(calculator.inputSize(any())).thenReturn(10) - `when`(calculator.outputSize(any())).thenReturn(2) +// `when`(calculator.outputSize(any())).thenReturn(2) `when`(calculator.transactionSize( ArgumentMatchers.anyList(), ArgumentMatchers.anyList(), any())).thenReturn(30) @@ -127,7 +127,7 @@ class UnspentOutputSelectorSingleNoChangeTest { `when`(queueParams.fee).thenReturn(fee) assertThrows(SendValueErrors.HasOutputFailedToSpend::class.java) { - selector.select(value, 100, ScriptType.P2PKH, ScriptType.P2WPKH, false, 0) + selector.select(value, null, 100, ScriptType.P2PKH, ScriptType.P2WPKH, false, 0) } } diff --git a/bitcoincore/src/test/kotlin/io/horizontalsystems/bitcoincore/managers/UnspentOutputSelectorTest.kt b/bitcoincore/src/test/kotlin/io/horizontalsystems/bitcoincore/managers/UnspentOutputSelectorTest.kt index 69b768476..fa564f60c 100644 --- a/bitcoincore/src/test/kotlin/io/horizontalsystems/bitcoincore/managers/UnspentOutputSelectorTest.kt +++ b/bitcoincore/src/test/kotlin/io/horizontalsystems/bitcoincore/managers/UnspentOutputSelectorTest.kt @@ -36,7 +36,7 @@ class UnspentOutputSelectorTest { `when`(dustCalculator.dust(any())).thenReturn(dust) assertThrows(SendValueErrors.Dust::class.java) { - selector.select(value, 100, ScriptType.P2PKH, ScriptType.P2WPKH, false, 0) + selector.select(value, null, 100, ScriptType.P2PKH, ScriptType.P2WPKH, false, 0) } } @@ -47,7 +47,7 @@ class UnspentOutputSelectorTest { `when`(unspentOutputProvider.getSpendableUtxo()).thenReturn(emptyList()) assertThrows(SendValueErrors.InsufficientUnspentOutputs::class.java) { - selector.select(10000, 100, ScriptType.P2PKH, ScriptType.P2WPKH, false, 0) + selector.select(10000, null, 100, ScriptType.P2PKH, ScriptType.P2WPKH, false, 0) } } @@ -66,13 +66,13 @@ class UnspentOutputSelectorTest { `when`(unspentOutputProvider.getSpendableUtxo()).thenReturn(outputs) `when`(dustCalculator.dust(any())).thenReturn(dust) `when`(calculator.inputSize(any())).thenReturn(10) - `when`(calculator.outputSize(any())).thenReturn(2) +// `when`(calculator.outputSize(any())).thenReturn(2) `when`(calculator.transactionSize(anyList(), anyList(), any())).thenReturn(30) `when`(queueParams.value).thenReturn(value.toLong()) `when`(queueParams.fee).thenReturn(fee) val selectedInfo = - selector.select(value.toLong(), feeRate, ScriptType.P2PKH, ScriptType.P2WPKH, false, 0) + selector.select(value.toLong(), null, feeRate, ScriptType.P2PKH, ScriptType.P2WPKH, false, 0) assertEquals(outputs, selectedInfo.outputs) assertEquals(11850, selectedInfo.recipientValue) } @@ -97,13 +97,13 @@ class UnspentOutputSelectorTest { `when`(unspentOutputProvider.getSpendableUtxo()).thenReturn(outputs) `when`(dustCalculator.dust(any())).thenReturn(dust) `when`(calculator.inputSize(any())).thenReturn(10) - `when`(calculator.outputSize(any())).thenReturn(2) +// `when`(calculator.outputSize(any())).thenReturn(2) `when`(calculator.transactionSize(anyList(), anyList(), any())).thenReturn(30) `when`(queueParams.value).thenReturn(value.toLong()) `when`(queueParams.fee).thenReturn(fee) val selectedInfo = - selector.select(value.toLong(), feeRate, ScriptType.P2PKH, ScriptType.P2WPKH, false, 0) + selector.select(value.toLong(), null, feeRate, ScriptType.P2PKH, ScriptType.P2WPKH, false, 0) assertEquals(4, selectedInfo.outputs.size) assertEquals(10850, selectedInfo.recipientValue) }