Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-21 17:40:43 +03:00
parent 638cbc9d23
commit 32ae17c016
9 changed files with 31 additions and 66 deletions

View file

@ -9,15 +9,6 @@ import com.squareup.moshi.JsonClass
*/
@JsonClass(generateAdapter = true)
data class GaslessSignedTransactionResultDTO(
@Json(name = "signedTransaction")
val signedTransaction: String,
@Json(name = "gasLimit")
val gasLimit: String,
@Json(name = "maxFeePerGas")
val maxFeePerGas: String,
@Json(name = "maxPriorityFeePerGas")
val maxPriorityFeePerGas: String,
@Json(name = "txHash")
val txHash: String,
)

View file

@ -37,6 +37,9 @@ dependencies {
implementation(projects.domain.transaction)
implementation(projects.domain.demo)
/** Api */
implementation(projects.features.sendV2.api)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)

View file

@ -13,6 +13,7 @@ import com.tangem.domain.transaction.GaslessTransactionRepository
import com.tangem.domain.transaction.models.Eip7702Authorization
import com.tangem.domain.transaction.models.GaslessSignedTransactionResult
import com.tangem.domain.transaction.models.GaslessTransactionData
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
@ -25,6 +26,7 @@ class DefaultGaslessTransactionRepository(
private val gaslessTxServiceApi: GaslessTxServiceApi,
private val coroutineDispatcherProvider: CoroutineDispatcherProvider,
private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
private val sendFeatureToggles: SendFeatureToggles,
) : GaslessTransactionRepository {
private val supportedTokensState = MutableStateFlow<Map<Network.ID, Set<CryptoCurrency>>>(hashMapOf())
@ -120,6 +122,9 @@ class DefaultGaslessTransactionRepository(
}
override suspend fun getGaslessFeeAddresses(): Set<String> {
if (!sendFeatureToggles.isGaslessTransactionsEnabled) {
return EMPTY_ADDRESSES
}
return addressesMutex.withLock {
allFeeRecipientAddress.ifEmpty {
val allFeeAddresses = getAllFeeRecipientAddresses()
@ -136,5 +141,6 @@ class DefaultGaslessTransactionRepository(
private companion object {
val BASE_GAS_FOR_TRANSACTION: BigInteger = BigInteger("100000")
val EMPTY_ADDRESSES = emptySet<String>()
}
}

View file

@ -49,10 +49,7 @@ class MockedGaslessTransactionRepository(
network: Network,
eip7702Auth: Eip7702Authorization?,
): GaslessSignedTransactionResult = GaslessSignedTransactionResult(
signedTransaction = "0x000",
gasLimit = 100000.toBigInteger(),
maxFeePerGas = 21000.toBigInteger(),
maxPriorityFeePerGas = 21000.toBigInteger(),
txHash = "0x000",
)
override fun getBaseGasForTransaction(): BigInteger {

View file

@ -1,6 +1,6 @@
package com.tangem.data.transaction.convertes
import com.tangem.datasource.api.gasless.models.GaslessSignedTransactionResultDTO as GaslessSignedTransactionResultDTO
import com.tangem.datasource.api.gasless.models.GaslessSignedTransactionResultDTO
import com.tangem.domain.transaction.models.GaslessSignedTransactionResult
import com.tangem.utils.converter.Converter
@ -13,10 +13,7 @@ class GaslessSignedTransactionResultConverter :
override fun convert(value: GaslessSignedTransactionResultDTO): GaslessSignedTransactionResult {
return GaslessSignedTransactionResult(
signedTransaction = value.signedTransaction,
gasLimit = value.gasLimit.toBigInteger(),
maxFeePerGas = value.maxFeePerGas.toBigInteger(),
maxPriorityFeePerGas = value.maxPriorityFeePerGas.toBigInteger(),
txHash = value.txHash,
)
}
}

View file

@ -16,6 +16,7 @@ import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.transaction.WalletAddressServiceRepository
import com.tangem.domain.transaction.error.FeeErrorResolver
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -76,11 +77,13 @@ internal object TransactionDataModule {
responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
gaslessTxServiceApi: GaslessTxServiceApi,
coroutineDispatcherProvider: CoroutineDispatcherProvider,
sendFeatureToggles: SendFeatureToggles,
): GaslessTransactionRepository {
return DefaultGaslessTransactionRepository(
gaslessTxServiceApi,
coroutineDispatcherProvider,
responseCryptoCurrenciesFactory,
gaslessTxServiceApi = gaslessTxServiceApi,
coroutineDispatcherProvider = coroutineDispatcherProvider,
responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory,
sendFeatureToggles = sendFeatureToggles,
)
}
}

View file

@ -1,7 +1,5 @@
package com.tangem.domain.transaction.models
import java.math.BigInteger
/**
* Result of gasless transaction signing from the gasless service.
* Contains the fully signed transaction ready for broadcasting and gas parameters.
@ -12,26 +10,12 @@ import java.math.BigInteger
* 3. Constructs the final transaction
* 4. Returns this signed transaction with gas parameters
*
* @property signedTransaction complete signed transaction in RLP-encoded hex format (0x...)
* ready to be broadcast to the blockchain network
* @property gasLimit maximum amount of gas units allocated for the transaction
* @property maxFeePerGas maximum total fee per gas unit (base fee + priority fee) in wei
* @property maxPriorityFeePerGas maximum priority fee (tip) per gas unit in wei for EIP-1559
* @property txHash sent tx hash
*/
data class GaslessSignedTransactionResult(
val signedTransaction: String,
val gasLimit: BigInteger,
val maxFeePerGas: BigInteger,
val maxPriorityFeePerGas: BigInteger,
val txHash: String,
) {
init {
require(signedTransaction.isNotBlank()) { "Signed transaction must not be blank" }
require(signedTransaction.startsWith("0x")) { "Signed transaction must be in hex format with 0x prefix" }
require(gasLimit > BigInteger.ZERO) { "Gas limit must be positive" }
require(maxFeePerGas > BigInteger.ZERO) { "Max fee per gas must be positive" }
require(maxPriorityFeePerGas >= BigInteger.ZERO) { "Max priority fee per gas must be non-negative" }
require(maxPriorityFeePerGas <= maxFeePerGas) {
"Max priority fee per gas cannot exceed max fee per gas"
}
require(txHash.isNotBlank()) { "Tx hash must not be blank" }
}
}

View file

@ -26,7 +26,6 @@ import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.transaction.GaslessTransactionRepository
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.domain.transaction.models.Eip7702Authorization
import com.tangem.domain.transaction.models.GaslessSignedTransactionResult
import com.tangem.domain.transaction.models.GaslessTransactionData
import com.tangem.domain.transaction.models.TransactionFeeExtended
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -50,8 +49,7 @@ class CreateAndSendGaslessTransactionUseCase(
val uncompiledTxData = validateTransactionData(transactionData)
val context = prepareGaslessContext(userWallet, uncompiledTxData, fee)
val signedData = signGaslessTransactionByUser(userWallet, context, uncompiledTxData)
val remoteSignedData = signTransactionOnBackend(context, signedData, uncompiledTxData)
broadcastSignedTransaction(context, remoteSignedData)
signAndSendTransactionOnBackend(context, signedData, uncompiledTxData)
},
catch = {
raise(SendTransactionError.DataError(it.message))
@ -185,32 +183,18 @@ class CreateAndSendGaslessTransactionUseCase(
/**
* Sends gasless transaction to the service.
*/
private suspend fun signTransactionOnBackend(
private suspend fun signAndSendTransactionOnBackend(
context: GaslessContext,
signedData: SignedGaslessData,
transactionData: TransactionData.Uncompiled,
): GaslessSignedTransactionResult {
): String {
return gaslessTransactionRepository.signGaslessTransaction(
network = context.tokenForFeeStatus.currency.network,
gaslessTransactionData = context.gaslessTransactionData,
signature = signedData.eip712Signature,
userAddress = transactionData.sourceAddress,
eip7702Auth = signedData.eip7702Auth,
)
}
private suspend fun broadcastSignedTransaction(
context: GaslessContext,
remoteSignedData: GaslessSignedTransactionResult,
): String {
val transactionSender = context.walletManager as? TransactionSender ?: error(
"WalletManager for network ${context.tokenForFeeStatus.currency.network.id} " +
"does not support transaction sending",
)
return when (val result = transactionSender.broadcastTransaction(remoteSignedData.signedTransaction)) {
is Result.Failure -> throw result.error
is Result.Success -> result.data.hash
}
).txHash
}
private suspend fun signHashes(

View file

@ -425,8 +425,8 @@ internal class SendConfirmModel @Inject constructor(
),
)
},
ifRight = {
updateTransactionStatus(txData)
ifRight = { txHash ->
updateTransactionStatus(txData, txHash)
addTokenToWalletIfNeeded()
sendBalanceUpdater.scheduleUpdates()
sendAnalyticHelper.sendSuccessAnalytics(
@ -479,9 +479,9 @@ internal class SendConfirmModel @Inject constructor(
}
}
private fun updateTransactionStatus(txData: TransactionData.Uncompiled) {
private fun updateTransactionStatus(txData: TransactionData.Uncompiled, txHash: String) {
val txUrl = getExplorerTransactionUrlUseCase(
txHash = txData.hash.orEmpty(),
txHash = txHash.ifEmpty { txData.hash.orEmpty() },
networkId = cryptoCurrency.network.id,
).getOrNull().orEmpty()
_uiState.update(SendConfirmSentStateTransformer(txData, txUrl))