From 0172c0ddb1997e70cdefa4f6b8bed6db5d5e3d3d Mon Sep 17 00:00:00 2001 From: Evgenii Kuzovkin Date: Tue, 10 Dec 2024 10:07:26 +0500 Subject: [PATCH] AND-9337, AND-9340, AND-9341, AND-9348, AND-9349, AND-9350, AND-9364: Fix KRC-20 issues --- .../kaspa/KaspaTransactionBuilder.kt | 46 +++-- .../blockchains/kaspa/KaspaWalletManager.kt | 174 +++++++++--------- .../krc20/KaspaKRC20TransactionExtras.kt | 8 - .../kaspa/krc20/model/CommitTransaction.kt | 3 +- 4 files changed, 114 insertions(+), 117 deletions(-) delete mode 100644 blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/KaspaKRC20TransactionExtras.kt diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/KaspaTransactionBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/KaspaTransactionBuilder.kt index 46ac2583..20dc618a 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/KaspaTransactionBuilder.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/KaspaTransactionBuilder.kt @@ -6,11 +6,13 @@ import com.tangem.blockchain.blockchains.kaspa.kaspacashaddr.KaspaCashAddr import com.tangem.blockchain.blockchains.kaspa.krc20.model.* import com.tangem.blockchain.blockchains.kaspa.network.* import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.datastorage.BlockchainSavedData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.network.moshi import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.isZero +import com.tangem.common.extensions.toCompressedPublicKey import com.tangem.common.extensions.toHexString import org.bitcoinj.core.* import org.bitcoinj.core.Transaction.SigHash @@ -102,7 +104,7 @@ class KaspaTransactionBuilder( return buildForSendInternal(transaction) } - internal fun buildKRC20RevealToSend( + internal fun buildToSendKRC20Reveal( signatures: ByteArray, redeemScript: RedeemScript, transaction: KaspaTransaction, @@ -122,7 +124,7 @@ class KaspaTransactionBuilder( } @Suppress("LongMethod", "MagicNumber") - internal fun buildKRC20CommitTransactionToSign( + internal fun buildToSignKRC20Commit( transactionData: TransactionData, dust: BigDecimal?, includeFee: Boolean = true, @@ -141,30 +143,26 @@ class KaspaTransactionBuilder( val transactionFeeAmountValue = transactionData.fee?.amount?.value ?: BigDecimal.ZERO - val change = calculateChange( - amount = requireNotNull(transactionData.amount.value) { "Transaction amount is null" }, - fee = transactionFeeAmountValue, - unspentOutputs = unspentsToSpend, - ) + val revealFeeAmount = (transactionData.fee as? Fee.Kaspa) + ?.revealTransactionFee + ?.takeIf { includeFee } + ?.value + ?: BigDecimal.ZERO - if (change < BigDecimal.ZERO) { // unspentsToSpend not enough to cover transaction amount - val maxAmount = transactionData.amount.value + change - return Result.Failure(BlockchainSdkError.Kaspa.UtxoAmountError(MAX_INPUT_COUNT, maxAmount)) + val commitFeeAmount = if (includeFee) { + transactionFeeAmountValue - revealFeeAmount + } else { + transactionFeeAmountValue } - // We determine the commission value for reveal transaction, - val revealFeeParams = (transactionData.fee as? Fee.Kaspa)?.revealTransactionFee?.takeIf { includeFee } - // if we don't know the commission, commission for reveal transaction will be set to zero - val feeEstimationRevealTransactionValue = revealFeeParams?.value ?: BigDecimal.ZERO - val targetOutputAmountValue = feeEstimationRevealTransactionValue + (dust ?: BigDecimal.ZERO) + val targetOutputAmountValue = revealFeeAmount + (dust ?: BigDecimal.ZERO) val resultChange = calculateChange( amount = targetOutputAmountValue, - fee = transactionFeeAmountValue, + fee = commitFeeAmount, unspentOutputs = getUnspentsToSpend(), ) - // The envelope will be used to create the RedeemScript and saved for use when building the Reveal transaction val envelope = Envelope( p = "krc-20", op = "transfer", @@ -174,7 +172,7 @@ class KaspaTransactionBuilder( ) val redeemScript = RedeemScript( - publicKey = publicKey.blockchainKey, + publicKey = publicKey.blockchainKey.toCompressedPublicKey(), envelope = envelope, ) @@ -205,9 +203,9 @@ class KaspaTransactionBuilder( hashes = getHashesForSign(transaction), redeemScript = redeemScript, sourceAddress = transactionData.sourceAddress, - params = IncompleteTokenTransactionParams( + params = BlockchainSavedData.KaspaKRC20IncompleteTokenTransaction( transactionId = transaction.transactionHash().toHexString(), - amountValue = transactionData.amount.value, + amountValue = transactionData.amount.value ?: BigDecimal.ZERO, feeAmountValue = targetOutputAmountValue, envelope = envelope, ), @@ -216,11 +214,11 @@ class KaspaTransactionBuilder( return Result.Success(commitTransaction) } - internal fun buildKRC20RevealTransactionToSign( + internal fun buildToSignKRC20Reveal( sourceAddress: String, redeemScript: RedeemScript, - params: IncompleteTokenTransactionParams, - feeAmountValue: BigDecimal, + params: BlockchainSavedData.KaspaKRC20IncompleteTokenTransaction, + revealFeeAmountValue: BigDecimal, ): Result { val utxo = listOf( KaspaUnspentOutput( @@ -233,7 +231,7 @@ class KaspaTransactionBuilder( val change = calculateChange( amount = BigDecimal.ZERO, - fee = feeAmountValue, + fee = revealFeeAmountValue, unspentOutputs = utxo, ) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/KaspaWalletManager.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/KaspaWalletManager.kt index 385612ee..1f4e3fe7 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/KaspaWalletManager.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/KaspaWalletManager.kt @@ -3,8 +3,6 @@ package com.tangem.blockchain.blockchains.kaspa import android.util.Log import com.tangem.blockchain.blockchains.kaspa.krc20.KaspaKRC20InfoResponse import com.tangem.blockchain.blockchains.kaspa.krc20.KaspaKRC20NetworkProvider -import com.tangem.blockchain.blockchains.kaspa.krc20.KaspaKRC20TransactionExtras -import com.tangem.blockchain.blockchains.kaspa.krc20.model.IncompleteTokenTransactionParams import com.tangem.blockchain.blockchains.kaspa.krc20.model.RedeemScript import com.tangem.blockchain.blockchains.kaspa.network.KaspaFeeBucketResponse import com.tangem.blockchain.blockchains.kaspa.network.KaspaInfoResponse @@ -19,6 +17,7 @@ import com.tangem.blockchain.common.trustlines.AssetRequirementsCondition import com.tangem.blockchain.common.trustlines.AssetRequirementsManager import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.blockchain.extensions.map import com.tangem.common.CompletionResult import com.tangem.common.extensions.toCompressedPublicKey import com.tangem.common.extensions.toHexString @@ -99,11 +98,20 @@ internal class KaspaWalletManager( ): Result { transactionData.requireUncompiled() - return when (transactionData.amount.type) { + return when (val type = transactionData.amount.type) { is AmountType.Coin -> sendCoinTransaction(transactionData, signer) is AmountType.Token -> { - if (transactionData.extras as? KaspaKRC20TransactionExtras != null) { - sendKRC20RevealOnlyTransaction(transactionData, signer) + updateUnspentOutputs() + val incompleteTokenTransaction = getIncompleteTokenTransaction(type.token) + if (incompleteTokenTransaction != null && + incompleteTokenTransaction.amountValue == transactionData.amount.value && + incompleteTokenTransaction.envelope.to == transactionData.destinationAddress + ) { + sendKRC20RevealOnlyTransaction( + transactionData = transactionData, + signer = signer, + incompleteTokenTransactionParams = incompleteTokenTransaction, + ) } else { sendKRC20Transaction(transactionData, signer) } @@ -130,9 +138,10 @@ internal class KaspaWalletManager( val buildTransactionResult = when (amount.type) { is AmountType.Coin -> transactionBuilder.buildToSign(transactionData) - is AmountType.Token -> transactionBuilder.buildKRC20CommitTransactionToSign( + is AmountType.Token -> transactionBuilder.buildToSignKRC20Commit( transactionData = transactionData, dust = dustValue, + includeFee = false, ).let { when (it) { is Result.Failure -> it @@ -157,11 +166,7 @@ internal class KaspaWalletManager( is Result.Failure -> sendResult is Result.Success -> { val data = sendResult.data - val mass = when (amount.type) { - is AmountType.Coin -> BigInteger.valueOf(data.mass) - is AmountType.Token -> REVEAL_TRANSACTION_MASS - else -> error("unknown amount type for fee estimation") - } + val mass = BigInteger.valueOf(data.mass) val allBuckets = ( listOf(data.priorityBucket) + @@ -231,12 +236,11 @@ internal class KaspaWalletManager( ?: return SimpleResult.Success val result = sendKRC20RevealOnlyTransaction( - transactionData = incompleteTokenTransaction - .toIncompleteTokenTransactionParams() - .toTransactionData( - type = AmountType.Token(currencyType.info), - ), + transactionData = incompleteTokenTransaction.toTransactionData( + type = AmountType.Token(currencyType.info), + ), signer = signer, + incompleteTokenTransactionParams = incompleteTokenTransaction, ) when (result) { @@ -294,18 +298,19 @@ internal class KaspaWalletManager( signer: TransactionSigner, ): Result { transactionData.requireUncompiled() + val token = (transactionData.amount.type as AmountType.Token).token return when ( - val commitTransaction = transactionBuilder.buildKRC20CommitTransactionToSign( + val commitTransaction = transactionBuilder.buildToSignKRC20Commit( transactionData = transactionData, dust = dustValue, ) ) { is Result.Success -> { - val revealTransaction = transactionBuilder.buildKRC20RevealTransactionToSign( + val revealTransaction = transactionBuilder.buildToSignKRC20Reveal( sourceAddress = transactionData.sourceAddress, redeemScript = commitTransaction.data.redeemScript, - feeAmountValue = transactionData.fee?.amount?.value!!, + revealFeeAmountValue = (transactionData.fee as Fee.Kaspa).revealTransactionFee?.value!!, params = commitTransaction.data.params, ) @@ -322,7 +327,7 @@ internal class KaspaWalletManager( signatures = commitSignatures.reduce { acc, bytes -> acc + bytes }, transaction = commitTransaction.data.transaction, ) - val revealTransactionToSend = transactionBuilder.buildKRC20RevealToSend( + val revealTransactionToSend = transactionBuilder.buildToSendKRC20Reveal( signatures = revealSignatures.reduce { acc, bytes -> acc + bytes }, redeemScript = commitTransaction.data.redeemScript, transaction = revealTransaction.data.transaction, @@ -334,9 +339,7 @@ internal class KaspaWalletManager( is Result.Success -> { storeIncompleteTokenTransaction( token = token, - data = commitTransaction.data - .params - .toBlockchainSavedData(), + data = commitTransaction.data.params, ) delay(REVEAL_TRANSACTION_DELAY) @@ -345,8 +348,12 @@ internal class KaspaWalletManager( revealTransactionToSend, ) ) { - is Result.Failure -> sendRevealResult + is Result.Failure -> { + updateUnspentOutputs() + sendRevealResult + } is Result.Success -> { + updateUnspentOutputs() val hash = sendRevealResult.data transactionData.hash = hash wallet.addOutgoingTransaction(transactionData) @@ -367,70 +374,41 @@ internal class KaspaWalletManager( } } - private fun IncompleteTokenTransactionParams.toTransactionData(type: AmountType.Token): TransactionData.Uncompiled { - val token = type.token - val tokenValue = BigDecimal(envelope.amt) - - val transactionAmount = tokenValue.movePointLeft(token.decimals) - val fee = feeAmountValue - dustValue - val feeAmount = Amount( - value = fee, - blockchain = blockchain, - ) - - return TransactionData.Uncompiled( - amount = Amount( - value = transactionAmount, - blockchain = blockchain, - type = type, - ), - fee = Fee.Kaspa( - amount = feeAmount, - mass = BigInteger.ZERO, // we need only amount - feeRate = BigInteger.ZERO, // we need only amount - revealTransactionFee = feeAmount, - ), - sourceAddress = wallet.address, - destinationAddress = envelope.to, - status = TransactionStatus.Unconfirmed, - extras = KaspaKRC20TransactionExtras( - incompleteTokenTransactionParams = this, - ), - contractAddress = token.contractAddress, - ) - } - private suspend fun sendKRC20RevealOnlyTransaction( transactionData: TransactionData, signer: TransactionSigner, + incompleteTokenTransactionParams: BlockchainSavedData.KaspaKRC20IncompleteTokenTransaction, ): Result { transactionData.requireUncompiled() + val token = (transactionData.amount.type as AmountType.Token).token - val incompleteTokenTransactionParams = - (transactionData.extras as KaspaKRC20TransactionExtras).incompleteTokenTransactionParams - val feeAmount = (transactionData.fee as Fee.Kaspa).revealTransactionFee + val revealFeeAmountValue = (transactionData.fee as Fee.Kaspa).revealTransactionFee?.value ?: BigDecimal.ZERO val redeemScript = RedeemScript( wallet.publicKey.blockchainKey.toCompressedPublicKey(), incompleteTokenTransactionParams.envelope, ) - val transaction = transactionBuilder.buildKRC20RevealTransactionToSign( + val transaction = transactionBuilder.buildToSignKRC20Reveal( sourceAddress = transactionData.sourceAddress, redeemScript = redeemScript, params = incompleteTokenTransactionParams, - feeAmountValue = feeAmount?.value!!, + revealFeeAmountValue = revealFeeAmountValue, ) return when (transaction) { is Result.Success -> { return when (val signerResult = signer.sign(transaction.data.hashes, wallet.publicKey)) { is CompletionResult.Success -> { - val transactionToSend = transactionBuilder.buildKRC20RevealToSend( + val transactionToSend = transactionBuilder.buildToSendKRC20Reveal( signatures = signerResult.data.reduce { acc, bytes -> acc + bytes }, redeemScript = redeemScript, transaction = transaction.data.transaction, ) when (val sendResult = networkProvider.sendTransaction(transactionToSend)) { - is Result.Failure -> sendResult + is Result.Failure -> { + updateUnspentOutputs() + sendResult + } is Result.Success -> { + updateUnspentOutputs() val hash = sendResult.data transactionData.hash = hash wallet.addOutgoingTransaction(transactionData) @@ -446,27 +424,71 @@ internal class KaspaWalletManager( } } + private fun BlockchainSavedData.KaspaKRC20IncompleteTokenTransaction.toTransactionData( + type: AmountType.Token, + ): TransactionData.Uncompiled { + val token = type.token + val tokenValue = BigDecimal(envelope.amt) + + val transactionAmount = tokenValue.movePointLeft(token.decimals) + val fee = feeAmountValue - dustValue + val feeAmount = Amount( + value = fee, + blockchain = blockchain, + ) + + return TransactionData.Uncompiled( + amount = Amount( + value = transactionAmount, + blockchain = blockchain, + type = type, + ), + fee = Fee.Kaspa( + amount = feeAmount, + mass = BigInteger.ZERO, // we need only amount + feeRate = BigInteger.ZERO, // we need only amount + revealTransactionFee = feeAmount, + ), + sourceAddress = wallet.address, + destinationAddress = envelope.to, + status = TransactionStatus.Unconfirmed, + contractAddress = token.contractAddress, + ) + } + private fun KaspaFeeBucketResponse.toFee(mass: BigInteger, type: AmountType): Fee.Kaspa { val feeRate = feeRate.toBigInteger() - val value = (mass * feeRate).toBigDecimal().movePointLeft(blockchain.decimals()) + val resultMass = if (type is AmountType.Token) { + mass + REVEAL_TRANSACTION_MASS + } else { + mass + } + val value = (resultMass * feeRate).toBigDecimal().movePointLeft(blockchain.decimals()) return Fee.Kaspa( amount = Amount( value = value, blockchain = blockchain, - type = type, ), mass = mass, feeRate = feeRate, revealTransactionFee = type.takeIf { it is AmountType.Token }.let { Amount( - value = (mass * feeRate).toBigDecimal().movePointLeft(blockchain.decimals()), + value = (REVEAL_TRANSACTION_MASS * feeRate).toBigDecimal().movePointLeft(blockchain.decimals()), blockchain = blockchain, - type = type, ) }, ) } + // we should update unspent outputs as soon as possible before create a new token transaction + private suspend fun updateUnspentOutputs() { + coroutineScope { + networkProvider.getInfo(wallet.address).map { + transactionBuilder.unspentOutputs = it.unspentOutputs + } + } + } + private suspend fun getIncompleteTokenTransaction( token: Token, ): BlockchainSavedData.KaspaKRC20IncompleteTokenTransaction? { @@ -488,22 +510,6 @@ internal class KaspaWalletManager( return "$symbol-${wallet.publicKey.blockchainKey.toCompressedPublicKey().toHexString()}" } - private fun IncompleteTokenTransactionParams.toBlockchainSavedData() = - BlockchainSavedData.KaspaKRC20IncompleteTokenTransaction( - transactionId = transactionId, - amountValue = amountValue, - feeAmountValue = feeAmountValue, - envelope = envelope, - ) - - private fun BlockchainSavedData.KaspaKRC20IncompleteTokenTransaction.toIncompleteTokenTransactionParams() = - IncompleteTokenTransactionParams( - transactionId = transactionId, - amountValue = amountValue, - feeAmountValue = feeAmountValue, - envelope = envelope, - ) - companion object { private val REVEAL_TRANSACTION_MASS: BigInteger = 4100.toBigInteger() private const val REVEAL_TRANSACTION_DELAY: Long = 2_000 diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/KaspaKRC20TransactionExtras.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/KaspaKRC20TransactionExtras.kt deleted file mode 100644 index fc864adb..00000000 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/KaspaKRC20TransactionExtras.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.blockchain.blockchains.kaspa.krc20 - -import com.tangem.blockchain.blockchains.kaspa.krc20.model.IncompleteTokenTransactionParams -import com.tangem.blockchain.common.TransactionExtras - -internal data class KaspaKRC20TransactionExtras( - val incompleteTokenTransactionParams: IncompleteTokenTransactionParams, -) : TransactionExtras diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/model/CommitTransaction.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/model/CommitTransaction.kt index 659a5ebb..b493fac6 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/model/CommitTransaction.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/model/CommitTransaction.kt @@ -1,11 +1,12 @@ package com.tangem.blockchain.blockchains.kaspa.krc20.model import com.tangem.blockchain.blockchains.kaspa.KaspaTransaction +import com.tangem.blockchain.common.datastorage.BlockchainSavedData internal data class CommitTransaction( val transaction: KaspaTransaction, val hashes: List, val redeemScript: RedeemScript, val sourceAddress: String, - val params: IncompleteTokenTransactionParams, + val params: BlockchainSavedData.KaspaKRC20IncompleteTokenTransaction, )