Skip to content

Commit

Permalink
Merge pull request #396 from tangem/fix/AND-5492
Browse files Browse the repository at this point in the history
AND-5492 Retrieve only the necessary inputs for the UTXO transaction.
  • Loading branch information
kozarezvlad authored Dec 21, 2023
2 parents 6d235ac + 03e396a commit c28f089
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import java.math.BigInteger

open class BitcoinTransactionBuilder(
private val walletPublicKey: ByteArray, blockchain: Blockchain,
walletAddresses: Set<com.tangem.blockchain.common.address.Address> = emptySet()
walletAddresses: Set<com.tangem.blockchain.common.address.Address> = emptySet(),
) {
private val walletScripts =
walletAddresses.filterIsInstance<BitcoinScriptAddress>().map { it.script }
Expand All @@ -51,9 +51,10 @@ open class BitcoinTransactionBuilder(
return Result.Failure(BlockchainSdkError.CustomError("Unspent outputs are missing"))
}

val change: BigDecimal = calculateChange(transactionData, unspentOutputs!!)
val outputsToSend = getOutputsToSend(unspentOutputs!!, transactionData)
val change: BigDecimal = calculateChange(transactionData, outputsToSend)
transaction =
transactionData.toBitcoinJTransaction(networkParameters, unspentOutputs!!, change)
transactionData.toBitcoinJTransaction(networkParameters, outputsToSend, change)

val hashesToSign = MutableList(transaction.inputs.size) { byteArrayOf() }
for (input in transaction.inputs) {
Expand Down Expand Up @@ -81,7 +82,7 @@ open class BitcoinTransactionBuilder(
transaction.hashForWitnessSignature(
index,
scriptToSign,
Coin.parseCoin(unspentOutputs!![index].amount.toPlainString()),
Coin.parseCoin(outputsToSend[index].amount.toPlainString()),
Transaction.SigHash.ALL,
false
).bytes
Expand Down Expand Up @@ -156,7 +157,7 @@ open class BitcoinTransactionBuilder(

fun calculateChange(
transactionData: TransactionData,
unspentOutputs: List<BitcoinUnspentOutput>
unspentOutputs: List<BitcoinUnspentOutput>,
): BigDecimal {
val fullAmount = unspentOutputs.map { it.amount }.reduce { acc, number -> acc + number }
return fullAmount - (transactionData.amount.value!! + (transactionData.fee?.amount?.value
Expand All @@ -170,6 +171,23 @@ open class BitcoinTransactionBuilder(
return TransactionSignature(r, canonicalS)
}

private fun getOutputsToSend(
unspentOutputs: List<BitcoinUnspentOutput>,
transactionData: TransactionData,
): List<BitcoinUnspentOutput> {
val outputs = mutableListOf<BitcoinUnspentOutput>()
val amount = transactionData.amount.value!! + transactionData.fee?.amount?.value!!
var sum = BigDecimal.ZERO
var i = 0
while (sum <= amount && i < unspentOutputs.size) {
val output = unspentOutputs[i]
sum = sum.plus(output.amount)
outputs.add(output)
i++
}
return outputs
}

private fun findSpendingScript(scriptPubKey: Script): Script {
val scriptHash = ScriptPattern.extractHashFromP2SH(scriptPubKey)
return when (scriptHash.size) {
Expand All @@ -187,7 +205,7 @@ open class BitcoinTransactionBuilder(
internal fun TransactionData.toBitcoinJTransaction(
networkParameters: NetworkParameters?,
unspentOutputs: List<BitcoinUnspentOutput>,
change: BigDecimal
change: BigDecimal,
): Transaction {
val transaction = Transaction(networkParameters)
for (utxo in unspentOutputs) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.tangem.blockchain.blockchains.bitcoin

import com.google.common.truth.Truth
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.address.AddressType
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.extensions.Result
Expand Down Expand Up @@ -44,43 +47,44 @@ class BitcoinTransactionTest {
fee = fee
)

val expectedHashToSign1 = "000D25B43DE238A2FB30C00F138413B9E4E32F45B5F831EF4312D9A2CEBDD62F"
.hexToBytes().toList()
val expectedHashToSign2 = "26C937660455A32DB3CF79D16BBD1D835BA34A84A19381DEE4FAD05884DD4453"
.hexToBytes().toList()
val expectedSignedTransaction = "01000000000102B6A2673BDD04D57B5560F4E46CAC3C1F974E41463568F2A11E7D3175521D9C6D000000008B48304502210088E322D377878E83F25FD2E258344F0A7CC401654BF71C43DF96FC6B46766CAE022030E97BD9018E9B2E918EF79E15E2747D4E00C55D69FA0B8ADFAFD07F41144F81014104E3F3BE3CE3D8284DB3BA073AD0291040093D83C11A277B905D5555C9EC41073E103F4D9D299EDEA8285C51C3356A8681A545618C174251B984DF841F49D2376FFFFFFFFF3F86D67DC12F3E3E7EE47E3B02D30D476823B594CBCABF1123A8C272CC91F2AE4900000000FFFFFFFF02809698000000000017A91420C5D650F5A66352A0D07C526D5738586F09E2DF8780790CD4E8000000160014D95DCA4F06B1F60BEE334ECDDA515E51DD6593CF00024730440220337D7F3BD0798D66FDCE04B07C30984424B13B98BB2C3645744A696AD26ECC7802200157EA9D44DC41D0BCB420175A5D3F543079F4263AA2DBDE0EE2D33A877FC583012103E3F3BE3CE3D8284DB3BA073AD0291040093D83C11A277B905D5555C9EC41073E00000000"
val expectedHashToSign1 = "886A853A86E8A1DA678E1727D20E053A28CDE99DDC09767391C05C626B3FC4EF"
.hexToBytes().toList()
val expectedSignedTransaction =
"0100000001B6A2673BDD04D57B5560F4E46CAC3C1F974E41463568F2A11E7D3175521D9C6D000000008B48304502210088E322D377878E83F25FD2E258344F0A7CC401654BF71C43DF96FC6B46766CAE022030E97BD9018E9B2E918EF79E15E2747D4E00C55D69FA0B8ADFAFD07F41144F81014104E3F3BE3CE3D8284DB3BA073AD0291040093D83C11A277B905D5555C9EC41073E103F4D9D299EDEA8285C51C3356A8681A545618C174251B984DF841F49D2376FFFFFFFFF02809698000000000017A91420C5D650F5A66352A0D07C526D5738586F09E2DF874037FDD3E8000000160014D95DCA4F06B1F60BEE334ECDDA515E51DD6593CF00000000"
.hexToBytes()

// act
val buildToSignResult = transactionBuilder.buildToSign(transactionData) as Result.Success
val signedTransaction = transactionBuilder.buildToSend(signature)

// assert
Truth.assertThat(buildToSignResult.data.map { it.toList() })
.containsExactly(expectedHashToSign1, expectedHashToSign2)
Truth.assertThat(buildToSignResult.data.map { it.toList() }).containsExactly(expectedHashToSign1)
Truth.assertThat(signedTransaction).isEqualTo(expectedSignedTransaction)
}

@Test
fun buildCorrectMultisigTransaction() {
// arrange
val walletPublicKey = "04D2B9FB288540D54E5B32ECAF0381CD571F97F6F1ECD036B66BB11AA52FFE9981110D883080E2E255C6B1640586F7765E6FAA325D1340F49B56B83D9DE56BC7ED"
val walletPublicKey =
"04D2B9FB288540D54E5B32ECAF0381CD571F97F6F1ECD036B66BB11AA52FFE9981110D883080E2E255C6B1640586F7765E6FAA325D1340F49B56B83D9DE56BC7ED"
.hexToBytes()
val pairPublicKey = "0485D520C8B907F0BC5E03FCBBAC212CCD270764BBFF4990A28653A2FB0D656C342DF143C4D52C43582289E20A81D5D014C1384A1FFFEA1D121903AD7ED35A01EA"
val pairPublicKey =
"0485D520C8B907F0BC5E03FCBBAC212CCD270764BBFF4990A28653A2FB0D656C342DF143C4D52C43582289E20A81D5D014C1384A1FFFEA1D121903AD7ED35A01EA"
.hexToBytes()
val signature = "B8ADFAFD07F41144F81ECC780157EA9D44DC41D0BCB420175A5D3F543079F4263AA2DBDE0EE2D388E322D37784424B13B98BB2C3645744A696AD2696FC6B46766CAE30E97BD9018E9B2E918EF79E878E83F25FD2E258344F0A7CC401654BF71C43DF337D7F3BD0798D66FDCE04B07C30915E2747D4E00C55D69FA03A877FC583"
val signature =
"B8ADFAFD07F41144F81ECC780157EA9D44DC41D0BCB420175A5D3F543079F4263AA2DBDE0EE2D388E322D37784424B13B98BB2C3645744A696AD2696FC6B46766CAE30E97BD9018E9B2E918EF79E878E83F25FD2E258344F0A7CC401654BF71C43DF337D7F3BD0798D66FDCE04B07C30915E2747D4E00C55D69FA03A877FC583"
.hexToBytes()
val sendValue = "1.7".toBigDecimal()
val feeValue = "0.3".toBigDecimal()
val sendValue = "10000".toBigDecimal()
val feeValue = "0.01".toBigDecimal()
val destinationAddress = "1CM45rkJXtV9r8aUXeJnVKUh174EcKBQAJ"

val addresses = BitcoinAddressService(blockchain)
.makeMultisigAddresses(walletPublicKey, pairPublicKey)
.makeMultisigAddresses(walletPublicKey, pairPublicKey)
val legacyAddress = addresses.find { it.type == AddressType.Legacy }!!.value
val segwitAddress = addresses.find { it.type == AddressType.Default }!!.value
val transactionBuilder = BitcoinTransactionBuilder(walletPublicKey, blockchain, addresses)
transactionBuilder.unspentOutputs =
prepareTwoUnspentOutputs(listOf(legacyAddress, segwitAddress), networkParameters)
prepareTwoUnspentOutputs(listOf(legacyAddress, segwitAddress), networkParameters)

val amountToSend = Amount(sendValue, blockchain, AmountType.Coin)
val fee = Fee.Common(Amount(amountToSend, feeValue))
Expand All @@ -91,11 +95,12 @@ class BitcoinTransactionTest {
fee = fee
)

val expectedHashToSign1 = "21978B183F5371AA1D4CCF041E3EAE941E591C41D7258E6E3AA28788A712ECD6"
.hexToBytes().toList()
val expectedHashToSign2 = "F8B6EA97BE48DD5A1F7376546BFA916ABCFBE77593FE1FEC02C14FD2F75E0E27"
.hexToBytes().toList()
val expectedSignedTransaction = "01000000000102B6A2673BDD04D57B5560F4E46CAC3C1F974E41463568F2A11E7D3175521D9C6D000000009200483045022100B8ADFAFD07F41144F81ECC780157EA9D44DC41D0BCB420175A5D3F543079F42602203AA2DBDE0EE2D388E322D37784424B13B98BB2C3645744A696AD2696FC6B4676014751210285D520C8B907F0BC5E03FCBBAC212CCD270764BBFF4990A28653A2FB0D656C342103D2B9FB288540D54E5B32ECAF0381CD571F97F6F1ECD036B66BB11AA52FFE998152AEFFFFFFFF3F86D67DC12F3E3E7EE47E3B02D30D476823B594CBCABF1123A8C272CC91F2AE4900000000FFFFFFFF0280FE210A000000001976A9147C7446EC6D27F4093D51CAFE09BC5F29BF6C806388AC4090C8C8E80000002200204867181DE1B7DB7F100CEF2D09A97747C87FA92862FB5DBCA09B0394CDC0190B00030047304402206CAE30E97BD9018E9B2E918EF79E878E83F25FD2E258344F0A7CC401654BF71C022043DF337D7F3BD0798D66FDCE04B07C30915E2747D4E00C55D69FA03A877FC583014751210285D520C8B907F0BC5E03FCBBAC212CCD270764BBFF4990A28653A2FB0D656C342103D2B9FB288540D54E5B32ECAF0381CD571F97F6F1ECD036B66BB11AA52FFE998152AE00000000"
val expectedHashToSign1 = "53AC758A191610D1D0BE7FB962B8F58AB42031DDBE43011F01719DDA0A3FF634"
.hexToBytes().toList()
val expectedHashToSign2 = "3669F458B939AACDFCEB533F000EE3F9E7C326A636CBC0E45D89FC52E60D3540"
.hexToBytes().toList()
val expectedSignedTransaction =
"01000000000102B6A2673BDD04D57B5560F4E46CAC3C1F974E41463568F2A11E7D3175521D9C6D000000009200483045022100B8ADFAFD07F41144F81ECC780157EA9D44DC41D0BCB420175A5D3F543079F42602203AA2DBDE0EE2D388E322D37784424B13B98BB2C3645744A696AD2696FC6B4676014751210285D520C8B907F0BC5E03FCBBAC212CCD270764BBFF4990A28653A2FB0D656C342103D2B9FB288540D54E5B32ECAF0381CD571F97F6F1ECD036B66BB11AA52FFE998152AEFFFFFFFF3F86D67DC12F3E3E7EE47E3B02D30D476823B594CBCABF1123A8C272CC91F2AE4900000000FFFFFFFF010010A5D4E80000001976A9147C7446EC6D27F4093D51CAFE09BC5F29BF6C806388AC00030047304402206CAE30E97BD9018E9B2E918EF79E878E83F25FD2E258344F0A7CC401654BF71C022043DF337D7F3BD0798D66FDCE04B07C30915E2747D4E00C55D69FA03A877FC583014751210285D520C8B907F0BC5E03FCBBAC212CCD270764BBFF4990A28653A2FB0D656C342103D2B9FB288540D54E5B32ECAF0381CD571F97F6F1ECD036B66BB11AA52FFE998152AE00000000"
.hexToBytes()

// act
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class DogecoinTransactionTest {
val signature =
"88E322D377878E83F25FD2E258344F0A7CC401654BF71C43DF96FC6B46766CAE30E97BD9018E9B2E918EF79E15E2747D4E00C55D69FA0B8ADFAFD07F41144F81337D7F3BD0798D66FDCE04B07C30984424B13B98BB2C3645744A696AD26ECC780157EA9D44DC41D0BCB420175A5D3F543079F4263AA2DBDE0EE2D33A877FC583"
.hexToBytes()
val sendValue = "0.1".toBigDecimal()
val sendValue = "10000".toBigDecimal()
val feeValue = "0.01".toBigDecimal()
val destinationAddress = "DRgF4iLXRhnYeQEV9kHmkvvnz128uCFZXL"

Expand All @@ -48,12 +48,12 @@ class DogecoinTransactionTest {
fee = fee
)

val expectedHashToSign1 = "821AB220C94E463A312C5D1AA8F30D01EA20FAD896C077D0E539D7F21FD0AC77"
val expectedHashToSign1 = "D07A3B066782BAB7F1ACADC7615DF4F590D92DB2D6326B4BB98AF4617B641832"
.hexToBytes().toList()
val expectedHashToSign2 = "3613B882F3A09047E8BF7D37FF0E09A255B0D4293C2CE894D6EE6B4C8C487AD4"
val expectedHashToSign2 = "A42880C36572E169BCD0BA65850EB158E01AEFE00AA0AACA24496057B990BF2A"
.hexToBytes().toList()
val expectedSignedTransaction =
"0100000002B6A2673BDD04D57B5560F4E46CAC3C1F974E41463568F2A11E7D3175521D9C6D000000008B48304502210088E322D377878E83F25FD2E258344F0A7CC401654BF71C43DF96FC6B46766CAE022030E97BD9018E9B2E918EF79E15E2747D4E00C55D69FA0B8ADFAFD07F41144F81014104E3F3BE3CE3D8284DB3BA073AD0291040093D83C11A277B905D5555C9EC41073E103F4D9D299EDEA8285C51C3356A8681A545618C174251B984DF841F49D2376FFFFFFFFF3F86D67DC12F3E3E7EE47E3B02D30D476823B594CBCABF1123A8C272CC91F2AE490000008A4730440220337D7F3BD0798D66FDCE04B07C30984424B13B98BB2C3645744A696AD26ECC7802200157EA9D44DC41D0BCB420175A5D3F543079F4263AA2DBDE0EE2D33A877FC583014104E3F3BE3CE3D8284DB3BA073AD0291040093D83C11A277B905D5555C9EC41073E103F4D9D299EDEA8285C51C3356A8681A545618C174251B984DF841F49D2376FFFFFFFFF0280969800000000001976A914E14686E153D98A799BDD1DC973D949AF5541B74188AC80790CD4E80000001976A914C5C53741303B67E7FE2EA62CB5730B3DD32D75FF88AC00000000"
"0100000002B6A2673BDD04D57B5560F4E46CAC3C1F974E41463568F2A11E7D3175521D9C6D000000008B48304502210088E322D377878E83F25FD2E258344F0A7CC401654BF71C43DF96FC6B46766CAE022030E97BD9018E9B2E918EF79E15E2747D4E00C55D69FA0B8ADFAFD07F41144F81014104E3F3BE3CE3D8284DB3BA073AD0291040093D83C11A277B905D5555C9EC41073E103F4D9D299EDEA8285C51C3356A8681A545618C174251B984DF841F49D2376FFFFFFFFF3F86D67DC12F3E3E7EE47E3B02D30D476823B594CBCABF1123A8C272CC91F2AE490000008A4730440220337D7F3BD0798D66FDCE04B07C30984424B13B98BB2C3645744A696AD26ECC7802200157EA9D44DC41D0BCB420175A5D3F543079F4263AA2DBDE0EE2D33A877FC583014104E3F3BE3CE3D8284DB3BA073AD0291040093D83C11A277B905D5555C9EC41073E103F4D9D299EDEA8285C51C3356A8681A545618C174251B984DF841F49D2376FFFFFFFFF010010A5D4E80000001976A914E14686E153D98A799BDD1DC973D949AF5541B74188AC00000000"
.hexToBytes()

// act
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ class DucatusTransactionTest {
// arrange
val walletPublicKey = "04E3F3BE3CE3D8284DB3BA073AD0291040093D83C11A277B905D5555C9EC41073E103F4D9D299EDEA8285C51C3356A8681A545618C174251B984DF841F49D2376F"
.hexToBytes()
val signature = "88E322D377878E83F25FD2E258344F0A7CC401654BF71C43DF96FC6B46766CAE30E97BD9018E9B2E918EF79E15E2747D4E00C55D69FA0B8ADFAFD07F41144F81337D7F3BD0798D66FDCE04B07C30984424B13B98BB2C3645744A696AD26ECC780157EA9D44DC41D0BCB420175A5D3F543079F4263AA2DBDE0EE2D33A877FC583"
val signature =
"88E322D377878E83F25FD2E258344F0A7CC401654BF71C43DF96FC6B46766CAE30E97BD9018E9B2E918EF79E15E2747D4E00C55D69FA0B8ADFAFD07F41144F81337D7F3BD0798D66FDCE04B07C30984424B13B98BB2C3645744A696AD26ECC780157EA9D44DC41D0BCB420175A5D3F543079F4263AA2DBDE0EE2D33A877FC583"
.hexToBytes()
val sendValue = "0.1".toBigDecimal()
val sendValue = "10000".toBigDecimal()
val feeValue = "0.01".toBigDecimal()
val destinationAddress = "M6tZXSEVGErPo8TnmpPv8Zvp69uSmLwJmF"

Expand All @@ -45,11 +46,12 @@ class DucatusTransactionTest {
fee = fee
)

val expectedHashToSign1 = "F1C2ECA8E0C2AE8D04F5C58BD5FFC74DFD6171A2D33174CB93770F170BBBB603"
.hexToBytes().toList()
val expectedHashToSign2 = "C37B01690AF7C88C6813644D49D63D5EEBFA0512CD143EC10B5E538C4C578387"
.hexToBytes().toList()
val expectedSignedTransaction = "0100000002B6A2673BDD04D57B5560F4E46CAC3C1F974E41463568F2A11E7D3175521D9C6D000000008B48304502210088E322D377878E83F25FD2E258344F0A7CC401654BF71C43DF96FC6B46766CAE022030E97BD9018E9B2E918EF79E15E2747D4E00C55D69FA0B8ADFAFD07F41144F81014104E3F3BE3CE3D8284DB3BA073AD0291040093D83C11A277B905D5555C9EC41073E103F4D9D299EDEA8285C51C3356A8681A545618C174251B984DF841F49D2376FFFFFFFFF3F86D67DC12F3E3E7EE47E3B02D30D476823B594CBCABF1123A8C272CC91F2AE490000008A4730440220337D7F3BD0798D66FDCE04B07C30984424B13B98BB2C3645744A696AD26ECC7802200157EA9D44DC41D0BCB420175A5D3F543079F4263AA2DBDE0EE2D33A877FC583014104E3F3BE3CE3D8284DB3BA073AD0291040093D83C11A277B905D5555C9EC41073E103F4D9D299EDEA8285C51C3356A8681A545618C174251B984DF841F49D2376FFFFFFFFF0280969800000000001976A914F4EAE70648A8B0DFC935B73C7C021CAFDCBE32E788AC80790CD4E80000001976A914C5C53741303B67E7FE2EA62CB5730B3DD32D75FF88AC00000000"
val expectedHashToSign1 = "F33AB717ECCEBEEE078C6EC8AE9F9B0F5B57085F6331CE62CDC373A49EB00A2C"
.hexToBytes().toList()
val expectedHashToSign2 = "0E1A74D006640DA3121D43B4765D80E4028D7B0B80B17E0A64CB49D2DFD65607"
.hexToBytes().toList()
val expectedSignedTransaction =
"0100000002B6A2673BDD04D57B5560F4E46CAC3C1F974E41463568F2A11E7D3175521D9C6D000000008B48304502210088E322D377878E83F25FD2E258344F0A7CC401654BF71C43DF96FC6B46766CAE022030E97BD9018E9B2E918EF79E15E2747D4E00C55D69FA0B8ADFAFD07F41144F81014104E3F3BE3CE3D8284DB3BA073AD0291040093D83C11A277B905D5555C9EC41073E103F4D9D299EDEA8285C51C3356A8681A545618C174251B984DF841F49D2376FFFFFFFFF3F86D67DC12F3E3E7EE47E3B02D30D476823B594CBCABF1123A8C272CC91F2AE490000008A4730440220337D7F3BD0798D66FDCE04B07C30984424B13B98BB2C3645744A696AD26ECC7802200157EA9D44DC41D0BCB420175A5D3F543079F4263AA2DBDE0EE2D33A877FC583014104E3F3BE3CE3D8284DB3BA073AD0291040093D83C11A277B905D5555C9EC41073E103F4D9D299EDEA8285C51C3356A8681A545618C174251B984DF841F49D2376FFFFFFFFF010010A5D4E80000001976A914F4EAE70648A8B0DFC935B73C7C021CAFDCBE32E788AC00000000"
.hexToBytes()

// act
Expand Down
Loading

0 comments on commit c28f089

Please sign in to comment.