Updated on 2026-08-14
This commit is contained in:
parent
85803abc7f
commit
095c755362
194 changed files with 830 additions and 473 deletions
|
|
@ -0,0 +1,744 @@
|
|||
package com.tangem.data.walletmanager
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.raise.ensureNotNull
|
||||
import arrow.core.right
|
||||
import com.tangem.blockchain.blockchains.solana.RentProvider
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.blockchain.common.address.AddressType
|
||||
import com.tangem.blockchain.common.address.EstimationFeeAddressFactory
|
||||
import com.tangem.blockchain.common.pagination.Page
|
||||
import com.tangem.blockchain.common.smartcontract.SmartContractCallDataProviderFactory
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.blockchain.common.trustlines.AssetRequirementsManager
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.blockchain.nft.models.NFTAsset
|
||||
import com.tangem.blockchain.nft.models.NFTCollection
|
||||
import com.tangem.blockchain.transactionhistory.models.TransactionHistoryRequest
|
||||
import com.tangem.blockchainsdk.BlockchainSDKFactory
|
||||
import com.tangem.blockchainsdk.models.UpdateWalletManagerResult
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.data.walletmanager.utils.*
|
||||
import com.tangem.datasource.asset.loader.AssetLoader
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.datasource.local.walletmanager.WalletManagersStore
|
||||
import com.tangem.domain.demo.models.DemoConfig
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.network.TxInfo
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.transaction.models.AssetRequirementsCondition
|
||||
import com.tangem.domain.txhistory.models.PaginationWrapper
|
||||
import com.tangem.domain.txhistory.models.TxHistoryState
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.walletmanager.model.RentData
|
||||
import com.tangem.domain.walletmanager.model.SmartContractMethod
|
||||
import com.tangem.domain.walletmanager.model.TokenInfo
|
||||
import com.tangem.domain.walletmanager.utils.SdkPageConverter
|
||||
import com.tangem.domain.wallets.extension.hasDerivation
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
import java.util.EnumSet
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LargeClass", "TooManyFunctions")
|
||||
internal class DefaultWalletManagersFacade @Inject constructor(
|
||||
private val walletManagersStore: WalletManagersStore,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val assetLoader: AssetLoader,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
blockchainSDKFactory: BlockchainSDKFactory,
|
||||
) : WalletManagersFacade {
|
||||
|
||||
private val demoConfig by lazy { DemoConfig() }
|
||||
private val resultFactory by lazy { UpdateWalletManagerResultFactory() }
|
||||
private val walletManagerFactory by lazy { WalletManagerFactory(blockchainSDKFactory) }
|
||||
private val sdkTokenConverter by lazy { SdkTokenConverter() }
|
||||
private val txHistoryStateConverter by lazy { SdkTransactionHistoryStateConverter() }
|
||||
private val sdkPageConverter by lazy { SdkPageConverter() }
|
||||
private val cryptoCurrencyTypeConverter by lazy { CryptoCurrencyTypeConverter() }
|
||||
private val requirementsConditionConverter by lazy { SdkRequirementsConditionConverter() }
|
||||
private val estimationFeeAddressFactory by lazy { EstimationFeeAddressFactory() }
|
||||
|
||||
private val initMutex = Mutex()
|
||||
|
||||
override suspend fun update(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
extraTokens: Set<CryptoCurrency.Token>,
|
||||
): UpdateWalletManagerResult {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
val blockchain = network.toBlockchain()
|
||||
val derivationPath = network.derivationPath.value
|
||||
|
||||
return getAndUpdateWalletManager(userWallet, blockchain, derivationPath, extraTokens)
|
||||
}
|
||||
|
||||
override suspend fun remove(userWalletId: UserWalletId, networks: Set<Network>) {
|
||||
if (networks.isEmpty()) return
|
||||
|
||||
val blockchainsToDerivationPaths = networks.map {
|
||||
it.toBlockchain() to it.derivationPath.value
|
||||
}
|
||||
|
||||
withContext(dispatchers.io) {
|
||||
walletManagersStore.remove(userWalletId) { walletManager ->
|
||||
val wallet = walletManager.wallet
|
||||
val blockchainToDerivationPath = wallet.blockchain to wallet.publicKey.derivationPath?.rawPath
|
||||
|
||||
blockchainToDerivationPath in blockchainsToDerivationPaths
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun removeTokens(userWalletId: UserWalletId, tokens: Set<CryptoCurrency.Token>) {
|
||||
if (tokens.isEmpty()) return
|
||||
|
||||
tokens
|
||||
.groupBy(CryptoCurrency.Token::network)
|
||||
.forEach { (network, networkTokens) ->
|
||||
removeTokens(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
networkTokens = sdkTokenConverter.convertList(networkTokens),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun removeTokensByTokenInfo(userWalletId: UserWalletId, tokenInfos: Set<TokenInfo>) {
|
||||
if (tokenInfos.isEmpty()) return
|
||||
|
||||
tokenInfos
|
||||
.groupBy { it.network }
|
||||
.forEach { (network, tokenInfoList) ->
|
||||
removeTokens(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
networkTokens = tokenInfoList.map {
|
||||
Token(
|
||||
name = it.name,
|
||||
symbol = it.symbol,
|
||||
contractAddress = it.contractAddress,
|
||||
decimals = it.decimals,
|
||||
id = it.id,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun removeTokens(userWalletId: UserWalletId, network: Network, networkTokens: List<Token>) {
|
||||
withContext(dispatchers.io) {
|
||||
val walletManager = walletManagersStore.getSyncOrNull(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = network.toBlockchain(),
|
||||
derivationPath = network.derivationPath.value,
|
||||
) ?: return@withContext
|
||||
|
||||
networkTokens.forEach { token ->
|
||||
walletManager.removeToken(token)
|
||||
}
|
||||
|
||||
walletManagersStore.store(userWalletId, walletManager)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updatePendingTransactions(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): UpdateWalletManagerResult {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
val blockchain = network.toBlockchain()
|
||||
val derivationPath = network.derivationPath.value
|
||||
|
||||
if (derivationPath != null &&
|
||||
!userWallet.hasDerivation(blockchain, derivationPath)
|
||||
) {
|
||||
Timber.w("Derivation missed for: $blockchain")
|
||||
return UpdateWalletManagerResult.MissedDerivation
|
||||
}
|
||||
|
||||
val walletManager = getOrCreateWalletManager(userWalletId, blockchain, derivationPath)
|
||||
if (walletManager == null || blockchain == Blockchain.Unknown) {
|
||||
Timber.w("Unable to get a wallet manager for blockchain: $blockchain")
|
||||
return UpdateWalletManagerResult.Unreachable()
|
||||
}
|
||||
|
||||
return getLastWalletManagerResult(walletManager)
|
||||
}
|
||||
|
||||
override suspend fun getExploreUrl(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
addressType: AddressType,
|
||||
contractAddress: String?,
|
||||
): String {
|
||||
val blockchain = network.toBlockchain()
|
||||
val walletManager = getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
)
|
||||
|
||||
requireNotNull(walletManager) {
|
||||
"Unable to get a wallet manager for blockchain: $blockchain"
|
||||
}
|
||||
|
||||
val address = walletManager
|
||||
.wallet
|
||||
.addresses
|
||||
.find { it.type == addressType }
|
||||
?.value ?: walletManager.wallet.address
|
||||
return blockchain.getExploreUrl(address, contractAddress)
|
||||
}
|
||||
|
||||
override suspend fun getTxHistoryState(userWalletId: UserWalletId, currency: CryptoCurrency): TxHistoryState {
|
||||
val walletManager = getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
network = currency.network,
|
||||
)
|
||||
|
||||
requireNotNull(walletManager) {
|
||||
"Unable to get a wallet manager for blockchain: ${currency.network}"
|
||||
}
|
||||
|
||||
return walletManager
|
||||
.getTransactionHistoryState(
|
||||
address = walletManager.wallet.address,
|
||||
filterType = when (currency) {
|
||||
is CryptoCurrency.Coin -> TransactionHistoryRequest.FilterType.Coin
|
||||
is CryptoCurrency.Token -> {
|
||||
val blockchainToken = Token(
|
||||
name = currency.name,
|
||||
symbol = currency.symbol,
|
||||
contractAddress = currency.contractAddress,
|
||||
decimals = currency.decimals,
|
||||
id = currency.id.rawCurrencyId?.value,
|
||||
)
|
||||
TransactionHistoryRequest.FilterType.Contract(blockchainToken)
|
||||
}
|
||||
},
|
||||
)
|
||||
.let(txHistoryStateConverter::convert)
|
||||
}
|
||||
|
||||
override suspend fun getTxHistoryItems(
|
||||
userWalletId: UserWalletId,
|
||||
currency: CryptoCurrency,
|
||||
page: Page,
|
||||
pageSize: Int,
|
||||
): PaginationWrapper<TxInfo> {
|
||||
val walletManager = getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
network = currency.network,
|
||||
)
|
||||
|
||||
requireNotNull(walletManager) {
|
||||
"Unable to get a wallet manager for blockchain: ${currency.network}"
|
||||
}
|
||||
|
||||
val itemsResult = walletManager.getTransactionsHistory(
|
||||
request = TransactionHistoryRequest(
|
||||
address = walletManager.wallet.address,
|
||||
decimals = currency.decimals,
|
||||
page = page,
|
||||
pageSize = pageSize,
|
||||
filterType = when (currency) {
|
||||
is CryptoCurrency.Coin -> TransactionHistoryRequest.FilterType.Coin
|
||||
is CryptoCurrency.Token -> {
|
||||
val blockchainToken = Token(
|
||||
name = currency.name,
|
||||
symbol = currency.symbol,
|
||||
contractAddress = currency.contractAddress,
|
||||
decimals = currency.decimals,
|
||||
id = currency.id.rawCurrencyId?.value,
|
||||
)
|
||||
TransactionHistoryRequest.FilterType.Contract(blockchainToken)
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
return when (itemsResult) {
|
||||
is Result.Success -> PaginationWrapper(
|
||||
currentPage = sdkPageConverter.convert(page),
|
||||
nextPage = sdkPageConverter.convert(itemsResult.data.nextPage),
|
||||
items = SdkTransactionHistoryItemConverter(smartContractMethods = readSmartContractMethods())
|
||||
.convertList(itemsResult.data.items),
|
||||
)
|
||||
is Result.Failure -> error(itemsResult.error.message ?: itemsResult.error.customMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUserWallet(userWalletId: UserWalletId) = userWalletsStore.getSyncStrict(userWalletId)
|
||||
|
||||
private suspend fun getAndUpdateWalletManager(
|
||||
userWallet: UserWallet,
|
||||
blockchain: Blockchain,
|
||||
derivationPath: String?,
|
||||
extraTokens: Set<CryptoCurrency.Token>,
|
||||
): UpdateWalletManagerResult {
|
||||
if (derivationPath != null && !userWallet.hasDerivation(blockchain, derivationPath)) {
|
||||
Timber.w("Derivation missed for: $blockchain")
|
||||
return UpdateWalletManagerResult.MissedDerivation
|
||||
}
|
||||
|
||||
val walletManager = getOrCreateWalletManager(
|
||||
userWalletId = userWallet.walletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
if (walletManager == null || blockchain == Blockchain.Unknown) {
|
||||
Timber.w("Unable to create or find a wallet manager for blockchain: $blockchain")
|
||||
return UpdateWalletManagerResult.Unreachable()
|
||||
}
|
||||
|
||||
updateWalletManagerTokensIfNeeded(walletManager, extraTokens)
|
||||
|
||||
return try {
|
||||
if (userWallet is UserWallet.Cold && demoConfig.isDemoCardId(userWallet.scanResponse.card.cardId)) {
|
||||
updateDemoWalletManager(walletManager)
|
||||
} else {
|
||||
updateWalletManager(walletManager)
|
||||
}
|
||||
} finally {
|
||||
walletManagersStore.store(userWallet.walletId, walletManager)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDemoWalletManager(walletManager: WalletManager): UpdateWalletManagerResult {
|
||||
val amount = demoConfig.getBalance(walletManager.wallet.blockchain)
|
||||
walletManager.wallet.setAmount(amount)
|
||||
|
||||
return resultFactory.getDemoResult(walletManager, amount)
|
||||
}
|
||||
|
||||
private suspend fun updateWalletManager(walletManager: WalletManager): UpdateWalletManagerResult {
|
||||
return try {
|
||||
walletManager.update()
|
||||
|
||||
resultFactory.getResult(walletManager)
|
||||
} catch (e: BlockchainSdkError.AccountNotFound) {
|
||||
resultFactory.getNoAccountResult(
|
||||
walletManager = walletManager,
|
||||
customMessage = e.customMessage,
|
||||
amountToCreateAccount = e.amountToCreateAccount,
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
Timber.w(e, "Unable to update a wallet manager for: ${walletManager.wallet.blockchain}")
|
||||
|
||||
resultFactory.getUnreachableResult(walletManager)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLastWalletManagerResult(walletManager: WalletManager): UpdateWalletManagerResult {
|
||||
return try {
|
||||
resultFactory.getResult(walletManager)
|
||||
} catch (e: BlockchainSdkError.AccountNotFound) {
|
||||
resultFactory.getNoAccountResult(
|
||||
walletManager = walletManager,
|
||||
customMessage = e.customMessage,
|
||||
amountToCreateAccount = e.amountToCreateAccount,
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
Timber.w(e, "Unable to update a wallet manager for: ${walletManager.wallet.blockchain}")
|
||||
|
||||
resultFactory.getUnreachableResult(walletManager)
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Will be removed in future")
|
||||
override suspend fun getOrCreateWalletManager(
|
||||
userWalletId: UserWalletId,
|
||||
blockchain: Blockchain,
|
||||
derivationPath: String?,
|
||||
): WalletManager? {
|
||||
initMutex.withLock {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
|
||||
var walletManager = walletManagersStore.getSyncOrNull(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
val path = derivationPath?.let { DerivationPath(rawPath = it) }
|
||||
|
||||
if (walletManager == null) {
|
||||
when (userWallet) {
|
||||
is UserWallet.Hot -> {
|
||||
walletManager = walletManagerFactory.createWalletManagerForHot(
|
||||
hotWallet = userWallet,
|
||||
blockchain = blockchain,
|
||||
derivationPath = path,
|
||||
)
|
||||
}
|
||||
is UserWallet.Cold -> {
|
||||
walletManager = walletManagerFactory.createWalletManager(
|
||||
scanResponse = userWallet.scanResponse,
|
||||
blockchain = blockchain,
|
||||
derivationPath = path,
|
||||
)
|
||||
}
|
||||
}
|
||||
walletManager ?: return null
|
||||
walletManagersStore.store(userWalletId, walletManager)
|
||||
}
|
||||
return walletManager
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Will be removed in future")
|
||||
override suspend fun getOrCreateWalletManager(userWalletId: UserWalletId, network: Network): WalletManager? {
|
||||
val blockchain = network.toBlockchain()
|
||||
return getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Deprecated("Will be removed in future")
|
||||
override suspend fun getStoredWalletManagers(userWalletId: UserWalletId): List<WalletManager> {
|
||||
return walletManagersStore.getAllSync(userWalletId)
|
||||
}
|
||||
|
||||
override suspend fun getDefaultAddress(userWalletId: UserWalletId, network: Network): String? {
|
||||
return getAddresses(userWalletId, network)
|
||||
.firstOrNull { it.type == AddressType.Default }
|
||||
?.value
|
||||
}
|
||||
|
||||
override suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set<Address> {
|
||||
val manager = getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
)
|
||||
|
||||
return manager?.wallet?.addresses.orEmpty()
|
||||
}
|
||||
|
||||
override suspend fun getRentInfo(userWalletId: UserWalletId, network: Network): RentData? {
|
||||
val manager = getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
)
|
||||
if (manager !is RentProvider) return null
|
||||
|
||||
return when (val result = manager.minimalBalanceForRentExemption()) {
|
||||
is Result.Success -> {
|
||||
RentData(manager.rentAmount(), result.data)
|
||||
}
|
||||
is Result.Failure -> null
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Will be removed in future")
|
||||
override fun getAll(userWalletId: UserWalletId): Flow<List<WalletManager>> {
|
||||
return walletManagersStore.getAll(userWalletId)
|
||||
}
|
||||
|
||||
override suspend fun validateSignatureCount(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
signedHashes: Int,
|
||||
): Either<Throwable, Unit> {
|
||||
return either {
|
||||
val walletManager = getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
)
|
||||
|
||||
val validator = ensureNotNull(walletManager as? SignatureCountValidator) {
|
||||
raise(IllegalStateException("Wallet manager is not a SignatureCountValidator"))
|
||||
}
|
||||
|
||||
when (val result = validator.validateSignatureCount(signedHashes)) {
|
||||
is SimpleResult.Failure -> raise(result.error)
|
||||
is SimpleResult.Success -> Unit.right()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Will be removed in future")
|
||||
override suspend fun getFee(
|
||||
amount: Amount,
|
||||
destination: String,
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): Result<TransactionFee>? = withContext(dispatchers.io) {
|
||||
val walletManager = getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
)
|
||||
(walletManager as? TransactionSender)?.getFee(
|
||||
amount = amount,
|
||||
destination = destination,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun estimateFee(
|
||||
amount: Amount,
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): Result<TransactionFee>? = withContext(dispatchers.io) {
|
||||
val blockchain = network.toBlockchain()
|
||||
val walletManager = getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
)
|
||||
|
||||
val destination = estimationFeeAddressFactory.makeAddress(blockchain)
|
||||
|
||||
val callData = if (amount.type is AmountType.Token) {
|
||||
SmartContractCallDataProviderFactory.getTokenTransferCallData(
|
||||
destinationAddress = destination,
|
||||
amount = amount,
|
||||
blockchain = blockchain,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
(walletManager as? TransactionSender)?.estimateFee(
|
||||
amount = amount,
|
||||
destination = destination,
|
||||
callData = callData,
|
||||
)
|
||||
}
|
||||
|
||||
@Deprecated("Will be removed in future")
|
||||
override suspend fun validateTransaction(
|
||||
amount: Amount,
|
||||
fee: Amount?,
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): EnumSet<TransactionError>? {
|
||||
val walletManager = getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
)
|
||||
return walletManager?.validateTransaction(amount, fee)
|
||||
}
|
||||
|
||||
@Deprecated("Will be removed in future")
|
||||
override suspend fun createTransaction(
|
||||
amount: Amount,
|
||||
fee: Fee,
|
||||
memo: String?,
|
||||
destination: String,
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): TransactionData? {
|
||||
val walletManager = getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
)
|
||||
|
||||
return walletManager?.createTransaction(amount, fee, destination)
|
||||
}
|
||||
|
||||
override suspend fun getRecentTransactions(userWalletId: UserWalletId, currency: CryptoCurrency): List<TxInfo> {
|
||||
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network)
|
||||
|
||||
if (walletManager == null) {
|
||||
Timber.e("Unable to get a wallet manager for blockchain: ${currency.network.id}")
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val transactionDataConverter = TransactionDataToTxHistoryItemConverter(
|
||||
walletAddresses = SdkAddressToAddressConverter.convertList(walletManager.wallet.addresses).toSet(),
|
||||
feePaidCurrency = walletManager.wallet.blockchain.feePaidCurrency(),
|
||||
)
|
||||
|
||||
return walletManager.wallet.recentTransactions
|
||||
.filter { transaction ->
|
||||
when (currency) {
|
||||
is CryptoCurrency.Coin -> transaction.amount.type is AmountType.Coin
|
||||
is CryptoCurrency.Token -> transaction.contractAddress == currency.contractAddress
|
||||
}
|
||||
}
|
||||
.mapNotNull(transactionDataConverter::convert)
|
||||
}
|
||||
|
||||
override suspend fun tokenBalance(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
name: String,
|
||||
symbol: String,
|
||||
contractAddress: String,
|
||||
decimals: Int,
|
||||
id: String?,
|
||||
): BigDecimal {
|
||||
val blockchain = network.toBlockchain()
|
||||
val walletManager = getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
)
|
||||
requireNotNull(walletManager) { "Unable to get a wallet manager for blockchain: $blockchain" }
|
||||
return walletManager.wallet.fundsAvailable(
|
||||
AmountType.Token(
|
||||
token = Token(
|
||||
name = name,
|
||||
symbol = symbol,
|
||||
contractAddress = contractAddress,
|
||||
decimals = decimals,
|
||||
id = id,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getAssetRequirements(
|
||||
userWalletId: UserWalletId,
|
||||
currency: CryptoCurrency,
|
||||
): AssetRequirementsCondition? {
|
||||
return withContext(dispatchers.io) {
|
||||
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network)
|
||||
val currencyType = cryptoCurrencyTypeConverter.convert(currency)
|
||||
if (walletManager !is AssetRequirementsManager) {
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
val condition = walletManager.requirementsCondition(currencyType) ?: return@withContext null
|
||||
requirementsConditionConverter.convert(condition)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun fulfillRequirements(
|
||||
userWalletId: UserWalletId,
|
||||
currency: CryptoCurrency,
|
||||
signer: TransactionSigner,
|
||||
): SimpleResult {
|
||||
return withContext(dispatchers.io) {
|
||||
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network)
|
||||
val currencyType = cryptoCurrencyTypeConverter.convert(currency)
|
||||
|
||||
if (walletManager !is AssetRequirementsManager) {
|
||||
return@withContext SimpleResult.Failure(
|
||||
BlockchainSdkError.CustomError("WalletManager is not implemented AssetRequirementsManager"),
|
||||
)
|
||||
}
|
||||
|
||||
walletManager.fulfillRequirements(currencyType, signer)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun discardRequirements(userWalletId: UserWalletId, currency: CryptoCurrency): SimpleResult {
|
||||
return withContext(dispatchers.io) {
|
||||
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network)
|
||||
val currencyType = cryptoCurrencyTypeConverter.convert(currency)
|
||||
|
||||
if (walletManager !is AssetRequirementsManager) {
|
||||
return@withContext SimpleResult.Failure(
|
||||
BlockchainSdkError.CustomError("WalletManager is not implemented AssetRequirementsManager"),
|
||||
)
|
||||
}
|
||||
|
||||
walletManager.discardRequirements(currencyType)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun checkUtxoConsolidationAvailability(userWalletId: UserWalletId, network: Network): Boolean {
|
||||
val blockchain = network.toBlockchain()
|
||||
val walletManager = getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
) ?: return false
|
||||
|
||||
return (walletManager as? UtxoBlockchainManager)?.allowConsolidation == true
|
||||
}
|
||||
|
||||
override suspend fun getNFTCollections(userWalletId: UserWalletId, network: Network): List<NFTCollection> {
|
||||
val blockchain = network.toBlockchain()
|
||||
val walletManager = getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
) ?: return emptyList()
|
||||
val address = walletManager.wallet.address
|
||||
return walletManager.getCollections(address)
|
||||
}
|
||||
|
||||
override suspend fun getNFTAssets(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
collectionIdentifier: NFTCollection.Identifier,
|
||||
): List<NFTAsset> {
|
||||
val blockchain = network.toBlockchain()
|
||||
val walletManager = getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
) ?: return emptyList()
|
||||
val address = walletManager.wallet.address
|
||||
return walletManager.getAssets(address, collectionIdentifier)
|
||||
}
|
||||
|
||||
override suspend fun getNFTAsset(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
collectionIdentifier: NFTCollection.Identifier,
|
||||
assetIdentifier: NFTAsset.Identifier,
|
||||
): NFTAsset? {
|
||||
val blockchain = network.toBlockchain()
|
||||
val walletManager = getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
) ?: return null
|
||||
return walletManager.getAsset(collectionIdentifier, assetIdentifier)
|
||||
}
|
||||
|
||||
override suspend fun getNFTSalePrice(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
collectionIdentifier: NFTCollection.Identifier,
|
||||
assetIdentifier: NFTAsset.Identifier,
|
||||
): NFTAsset.SalePrice? {
|
||||
val blockchain = network.toBlockchain()
|
||||
val walletManager = getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
) ?: return null
|
||||
return walletManager.getSalePrice(collectionIdentifier, assetIdentifier)
|
||||
}
|
||||
|
||||
override suspend fun getNFTExploreUrl(network: Network, assetIdentifier: NFTAsset.Identifier): String? {
|
||||
val blockchain = network.toBlockchain()
|
||||
return blockchain.getNFTExploreUrl(assetIdentifier)
|
||||
}
|
||||
|
||||
override suspend fun isAccountInitialized(userWalletId: UserWalletId, network: Network): Boolean {
|
||||
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = network)
|
||||
val initializableAccountWalletManger = walletManager as? InitializableAccount ?: return true
|
||||
return initializableAccountWalletManger.accountInitializationState == InitializableAccount.State.INITIALIZED
|
||||
}
|
||||
|
||||
private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set<CryptoCurrency.Token>) {
|
||||
if (tokens.isEmpty()) return
|
||||
|
||||
val tokensToAdd = sdkTokenConverter
|
||||
.convertList(tokens)
|
||||
.filter { it !in walletManager.cardTokens }
|
||||
|
||||
walletManager.addTokens(tokensToAdd)
|
||||
}
|
||||
|
||||
private suspend fun readSmartContractMethods(): Map<String, SmartContractMethod> {
|
||||
return assetLoader.loadMap<SmartContractMethod>(fileName = "contract_methods")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
package com.tangem.data.walletmanager
|
||||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.TransactionStatus
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.blockchainsdk.models.UpdateWalletManagerResult
|
||||
import com.tangem.blockchainsdk.utils.amountToCreateAccount
|
||||
import com.tangem.data.walletmanager.utils.SdkAddressToAddressConverter
|
||||
import com.tangem.data.walletmanager.utils.TransactionDataToTxHistoryItemConverter
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
/** Factory for creating [com.tangem.blockchainsdk.models.UpdateWalletManagerResult] */
|
||||
internal class UpdateWalletManagerResultFactory {
|
||||
|
||||
/** Get [com.tangem.blockchainsdk.models.UpdateWalletManagerResult.Verified] result for [walletManager] */
|
||||
fun getResult(walletManager: WalletManager): UpdateWalletManagerResult.Verified {
|
||||
val wallet = walletManager.wallet
|
||||
val addresses = getAvailableAddresses(wallet.addresses)
|
||||
val feePaidCurrency = wallet.blockchain.feePaidCurrency()
|
||||
val txHistoryItemConverter = TransactionDataToTxHistoryItemConverter(addresses, feePaidCurrency)
|
||||
|
||||
return UpdateWalletManagerResult.Verified(
|
||||
selectedAddress = wallet.address,
|
||||
addresses = addresses,
|
||||
currenciesAmounts = getTokensAmounts(wallet.amounts.values.toSet()),
|
||||
currentTransactions = getCurrentTransactions(txHistoryItemConverter, wallet.recentTransactions.toSet()),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get demo [UpdateWalletManagerResult.Verified] result
|
||||
*
|
||||
* @param walletManager wallet manager
|
||||
* @param demoAmount amount that will be used for demo result
|
||||
*/
|
||||
fun getDemoResult(walletManager: WalletManager, demoAmount: Amount): UpdateWalletManagerResult.Verified {
|
||||
val wallet = walletManager.wallet
|
||||
val addresses = getAvailableAddresses(wallet.addresses)
|
||||
val feePaidCurrency = wallet.blockchain.feePaidCurrency()
|
||||
val txHistoryItemConverter = TransactionDataToTxHistoryItemConverter(addresses, feePaidCurrency)
|
||||
|
||||
return UpdateWalletManagerResult.Verified(
|
||||
selectedAddress = wallet.address,
|
||||
addresses = addresses,
|
||||
currenciesAmounts = getDemoTokensAmounts(demoAmount, walletManager.cardTokens),
|
||||
currentTransactions = getCurrentTransactions(txHistoryItemConverter, wallet.recentTransactions.toSet()),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get [UpdateWalletManagerResult.NoAccount] result.
|
||||
* If unable to get required amount for creating account, [UpdateWalletManagerResult.Unreachable] result will be returned.
|
||||
*
|
||||
* @param walletManager wallet manager
|
||||
* @param customMessage custom error message
|
||||
* @param amountToCreateAccount amount to create account
|
||||
*/
|
||||
fun getNoAccountResult(
|
||||
walletManager: WalletManager,
|
||||
customMessage: String,
|
||||
amountToCreateAccount: BigDecimal?,
|
||||
): UpdateWalletManagerResult {
|
||||
val wallet = walletManager.wallet
|
||||
val blockchain = wallet.blockchain
|
||||
val firstWalletToken = wallet.getTokens().firstOrNull()
|
||||
val amount = amountToCreateAccount ?: blockchain.amountToCreateAccount(walletManager, firstWalletToken)
|
||||
|
||||
return if (amount == null) {
|
||||
Timber.w("Unable to get required amount to create account for: $blockchain")
|
||||
UpdateWalletManagerResult.Unreachable(
|
||||
selectedAddress = wallet.address,
|
||||
addresses = getAvailableAddresses(wallet.addresses),
|
||||
)
|
||||
} else {
|
||||
UpdateWalletManagerResult.NoAccount(
|
||||
selectedAddress = wallet.address,
|
||||
addresses = getAvailableAddresses(wallet.addresses),
|
||||
amountToCreateAccount = amount,
|
||||
errorMessage = customMessage,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Get [UpdateWalletManagerResult.Unreachable] result for [walletManager] */
|
||||
fun getUnreachableResult(walletManager: WalletManager): UpdateWalletManagerResult.Unreachable {
|
||||
val wallet = walletManager.wallet
|
||||
|
||||
return UpdateWalletManagerResult.Unreachable(
|
||||
selectedAddress = wallet.address,
|
||||
addresses = getAvailableAddresses(wallet.addresses),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getAvailableAddresses(addresses: Set<Address>): Set<UpdateWalletManagerResult.Address> {
|
||||
return SdkAddressToAddressConverter.convertList(addresses).toSet()
|
||||
}
|
||||
|
||||
private fun getTokensAmounts(amounts: Set<Amount>): Set<UpdateWalletManagerResult.CryptoCurrencyAmount> {
|
||||
return amounts.mapNotNullTo(hashSetOf(), ::createCurrencyAmount)
|
||||
}
|
||||
|
||||
private fun createCurrencyAmount(amount: Amount): UpdateWalletManagerResult.CryptoCurrencyAmount? {
|
||||
return when (val type = amount.type) {
|
||||
is AmountType.Token -> {
|
||||
val value = getCurrencyAmountValue(amount) ?: return null
|
||||
|
||||
UpdateWalletManagerResult.CryptoCurrencyAmount.Token(
|
||||
currencyRawId = type.token.id?.let(CryptoCurrency::RawID),
|
||||
contractAddress = type.token.contractAddress,
|
||||
value = value,
|
||||
)
|
||||
}
|
||||
is AmountType.Coin -> {
|
||||
val value = getCurrencyAmountValue(amount) ?: return null
|
||||
|
||||
UpdateWalletManagerResult.CryptoCurrencyAmount.Coin(value = value)
|
||||
}
|
||||
is AmountType.FeeResource,
|
||||
is AmountType.Reserve,
|
||||
-> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCurrencyAmountValue(amount: Amount): BigDecimal? {
|
||||
val value = amount.value
|
||||
|
||||
if (value == null) {
|
||||
Timber.w("Currency amount must not be null: ${amount.currencySymbol}")
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
private fun getDemoTokensAmounts(
|
||||
demoAmount: Amount,
|
||||
tokens: Set<Token>,
|
||||
): Set<UpdateWalletManagerResult.CryptoCurrencyAmount> {
|
||||
val amountValue = demoAmount.value ?: BigDecimal.ZERO
|
||||
val demoAmounts = hashSetOf<UpdateWalletManagerResult.CryptoCurrencyAmount>(
|
||||
UpdateWalletManagerResult.CryptoCurrencyAmount.Coin(amountValue),
|
||||
)
|
||||
|
||||
return tokens.mapTo(demoAmounts) { token ->
|
||||
UpdateWalletManagerResult.CryptoCurrencyAmount.Token(
|
||||
currencyRawId = token.id?.let(CryptoCurrency::RawID),
|
||||
contractAddress = token.contractAddress,
|
||||
value = amountValue,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCurrentTransactions(
|
||||
txHistoryItemConverter: TransactionDataToTxHistoryItemConverter,
|
||||
recentTransactions: Set<TransactionData.Uncompiled>,
|
||||
): Set<UpdateWalletManagerResult.CryptoCurrencyTransaction> {
|
||||
val unconfirmedTransactions = recentTransactions.filter { it.status == TransactionStatus.Unconfirmed }
|
||||
|
||||
return unconfirmedTransactions.mapNotNullTo(hashSetOf()) {
|
||||
createCurrencyTransaction(
|
||||
txHistoryItemConverter = txHistoryItemConverter,
|
||||
data = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCurrencyTransaction(
|
||||
txHistoryItemConverter: TransactionDataToTxHistoryItemConverter,
|
||||
data: TransactionData.Uncompiled,
|
||||
): UpdateWalletManagerResult.CryptoCurrencyTransaction? {
|
||||
return when (val type = data.amount.type) {
|
||||
is AmountType.Coin -> {
|
||||
val txHistoryItem = txHistoryItemConverter.convert(data) ?: return null
|
||||
|
||||
UpdateWalletManagerResult.CryptoCurrencyTransaction.Coin(txInfo = txHistoryItem)
|
||||
}
|
||||
is AmountType.Token -> {
|
||||
val txHistoryItem = txHistoryItemConverter.convert(data) ?: return null
|
||||
|
||||
UpdateWalletManagerResult.CryptoCurrencyTransaction.Token(
|
||||
tokenId = type.token.id,
|
||||
contractAddress = type.token.contractAddress,
|
||||
txInfo = txHistoryItem,
|
||||
)
|
||||
}
|
||||
is AmountType.FeeResource,
|
||||
is AmountType.Reserve,
|
||||
-> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package com.tangem.data.walletmanager
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.DerivationParams
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchainsdk.BlockchainSDKFactory
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.data.walletmanager.extensions.makePublicKey
|
||||
import com.tangem.data.walletmanager.extensions.makeWalletManagerForApp
|
||||
import com.tangem.domain.card.DerivationStyleProvider
|
||||
import com.tangem.domain.card.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import timber.log.Timber
|
||||
|
||||
internal class WalletManagerFactory(
|
||||
private val blockchainSDKFactory: BlockchainSDKFactory,
|
||||
) {
|
||||
|
||||
suspend fun createWalletManager(
|
||||
scanResponse: ScanResponse,
|
||||
blockchain: Blockchain,
|
||||
derivationPath: DerivationPath?,
|
||||
): WalletManager? {
|
||||
val derivationParams = getDerivationParams(derivationPath, scanResponse.derivationStyleProvider)
|
||||
|
||||
return try {
|
||||
blockchainSDKFactory.getWalletManagerFactorySync()?.makeWalletManagerForApp(
|
||||
scanResponse = scanResponse,
|
||||
blockchain = blockchain,
|
||||
derivationParams = derivationParams,
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
Timber.w(e, "Failed to create wallet manager for $blockchain")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createWalletManagerForHot(
|
||||
hotWallet: UserWallet.Hot,
|
||||
blockchain: Blockchain,
|
||||
derivationPath: DerivationPath?,
|
||||
): WalletManager? {
|
||||
val curve = blockchain.getSupportedCurves().first()
|
||||
val selectedWallet = hotWallet.wallets.orEmpty().firstOrNull { it.curve == curve }
|
||||
?: return null
|
||||
return try {
|
||||
val factory = blockchainSDKFactory.getWalletManagerFactorySync() ?: return null
|
||||
|
||||
if (derivationPath == null) {
|
||||
factory.createLegacyWalletManager(
|
||||
blockchain = blockchain,
|
||||
walletPublicKey = selectedWallet.publicKey,
|
||||
curve = selectedWallet.curve,
|
||||
)
|
||||
} else {
|
||||
factory.createWalletManager(
|
||||
blockchain = blockchain,
|
||||
publicKey = makePublicKey(
|
||||
seedKey = selectedWallet.publicKey,
|
||||
blockchain = blockchain,
|
||||
derivationPath = derivationPath,
|
||||
derivedWalletKeys = selectedWallet.derivedKeys,
|
||||
isWallet2 = true,
|
||||
) ?: return null,
|
||||
curve = selectedWallet.curve,
|
||||
)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Timber.w(e, "Failed to create wallet manager for $blockchain")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDerivationParams(
|
||||
derivationPath: DerivationPath?,
|
||||
derivationStyleProvider: DerivationStyleProvider,
|
||||
): DerivationParams? {
|
||||
val derivationStyle = derivationStyleProvider.getDerivationStyle() ?: return null
|
||||
|
||||
return if (derivationPath == null) {
|
||||
DerivationParams.Default(derivationStyle)
|
||||
} else {
|
||||
DerivationParams.Custom(derivationPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.data.walletmanager.di
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
import com.tangem.data.walletmanager.DefaultWalletManagersFacade
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface WalletManagerModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindWalletManagerFacade(impl: DefaultWalletManagersFacade): WalletManagersFacade
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
package com.tangem.data.walletmanager.extensions
|
||||
|
||||
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.common.derivation.DerivationStyle
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.card.common.TapWorkarounds.useOldStyleDerivation
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.card.configs.CardConfig
|
||||
import com.tangem.domain.card.configs.Wallet2CardConfig
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
|
||||
fun WalletManagerFactory.makeWalletManagerForApp(
|
||||
scanResponse: ScanResponse,
|
||||
blockchain: Blockchain,
|
||||
derivationParams: DerivationParams?,
|
||||
): WalletManager? {
|
||||
val card = scanResponse.card
|
||||
val cardConfig = CardConfig.createConfig(card)
|
||||
if (card.isTestCard && blockchain.getTestnetVersion() == null) return null
|
||||
val supportedCurves = blockchain.getSupportedCurves()
|
||||
|
||||
val wallets = card.wallets.filter { wallet -> supportedCurves.contains(wallet.curve) }
|
||||
val wallet = selectWallet(
|
||||
wallets = wallets,
|
||||
cardConfig = cardConfig,
|
||||
blockchain = blockchain,
|
||||
) ?: return null
|
||||
|
||||
val environmentBlockchain =
|
||||
if (card.isTestCard) blockchain.getTestnetVersion()!! else blockchain
|
||||
|
||||
val seedKey = wallet.extendedPublicKey
|
||||
return when {
|
||||
scanResponse.cardTypesResolver.isTangemTwins() && scanResponse.secondTwinPublicKey != null -> {
|
||||
createTwinWalletManager(
|
||||
walletPublicKey = wallet.publicKey,
|
||||
pairPublicKey = scanResponse.secondTwinPublicKey!!.hexToBytes(),
|
||||
blockchain = environmentBlockchain,
|
||||
curve = wallet.curve,
|
||||
)
|
||||
}
|
||||
scanResponse.card.settings.isHDWalletAllowed && seedKey != null && derivationParams != null -> {
|
||||
val derivedKeys = scanResponse.derivedKeys[wallet.publicKey.toMapKey()]
|
||||
val derivationPath = derivationParams.getPath(blockchain)
|
||||
|
||||
val publicKey = makePublicKey(
|
||||
seedKey = wallet.publicKey,
|
||||
blockchain = blockchain,
|
||||
derivationPath = derivationPath ?: return null,
|
||||
derivedWalletKeys = derivedKeys ?: return null,
|
||||
isWallet2 = scanResponse.cardTypesResolver.isWallet2(),
|
||||
) ?: return null
|
||||
|
||||
createWalletManager(
|
||||
blockchain = environmentBlockchain,
|
||||
publicKey = publicKey,
|
||||
curve = wallet.curve,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
createLegacyWalletManager(
|
||||
blockchain = environmentBlockchain,
|
||||
walletPublicKey = wallet.publicKey,
|
||||
curve = wallet.curve,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun makePublicKey(
|
||||
seedKey: ByteArray,
|
||||
blockchain: Blockchain,
|
||||
derivationPath: DerivationPath,
|
||||
derivedWalletKeys: Map<DerivationPath, ExtendedPublicKey>,
|
||||
isWallet2: Boolean,
|
||||
): Wallet.PublicKey? {
|
||||
val derivedKey = derivedWalletKeys[derivationPath] ?: return null
|
||||
|
||||
val derivationKey = Wallet.HDKey(
|
||||
path = derivationPath,
|
||||
extendedPublicKey = derivedKey,
|
||||
)
|
||||
|
||||
// we should generate second key for cardano
|
||||
// because cardano address generation for wallet2 requires keys from 2 derivations
|
||||
// https://developers.cardano.org/docs/get-started/cardano-serialization-lib/generating-keys/
|
||||
if (blockchain == Blockchain.Cardano && isWallet2) {
|
||||
val extendedDerivationPath = CardanoUtils.extendedDerivationPath(derivationPath)
|
||||
val secondDerivedKey = derivedWalletKeys[extendedDerivationPath] ?: error("No derivation found")
|
||||
|
||||
val secondDerivationKey = Wallet.HDKey(secondDerivedKey, extendedDerivationPath)
|
||||
|
||||
return Wallet.PublicKey(
|
||||
seedKey = seedKey,
|
||||
derivationType = Wallet.PublicKey.DerivationType.Double(derivationKey, secondDerivationKey),
|
||||
)
|
||||
}
|
||||
|
||||
return Wallet.PublicKey(
|
||||
seedKey = seedKey,
|
||||
derivationType = Wallet.PublicKey.DerivationType.Plain(derivationKey),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getDerivationParams(card: CardDTO): DerivationParams? {
|
||||
return if (!card.settings.isHDWalletAllowed) {
|
||||
null
|
||||
} else if (card.useOldStyleDerivation) {
|
||||
DerivationParams.Default(DerivationStyle.LEGACY)
|
||||
} else {
|
||||
DerivationParams.Default(DerivationStyle.NEW)
|
||||
}
|
||||
}
|
||||
|
||||
fun WalletManagerFactory.makePrimaryWalletManager(scanResponse: ScanResponse): WalletManager? {
|
||||
val blockchain = if (scanResponse.card.isTestCard) {
|
||||
scanResponse.cardTypesResolver.getBlockchain().getTestnetVersion() ?: return null
|
||||
} else {
|
||||
scanResponse.cardTypesResolver.getBlockchain()
|
||||
}
|
||||
val derivationParams = getDerivationParams(scanResponse.card)
|
||||
return makeWalletManagerForApp(
|
||||
scanResponse = scanResponse,
|
||||
blockchain = blockchain,
|
||||
derivationParams = derivationParams,
|
||||
)
|
||||
}
|
||||
|
||||
private fun selectWallet(
|
||||
wallets: List<CardDTO.Wallet>,
|
||||
cardConfig: CardConfig,
|
||||
blockchain: Blockchain,
|
||||
): CardDTO.Wallet? {
|
||||
return if (cardConfig is Wallet2CardConfig) {
|
||||
val primaryCurve = cardConfig.primaryCurve(blockchain)
|
||||
wallets.firstOrNull { it.curve == primaryCurve }
|
||||
} else {
|
||||
when (wallets.size) {
|
||||
0 -> null
|
||||
1 -> wallets[0]
|
||||
else -> wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: wallets[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.data.walletmanager.utils
|
||||
|
||||
import com.tangem.blockchain.common.CryptoCurrencyType
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class CryptoCurrencyTypeConverter : Converter<CryptoCurrency, CryptoCurrencyType> {
|
||||
override fun convert(value: CryptoCurrency): CryptoCurrencyType {
|
||||
return when (value) {
|
||||
is CryptoCurrency.Coin -> CryptoCurrencyType.Coin
|
||||
is CryptoCurrency.Token -> CryptoCurrencyType.Token(
|
||||
info = Token(
|
||||
name = value.name,
|
||||
symbol = value.symbol,
|
||||
contractAddress = value.contractAddress,
|
||||
decimals = value.decimals,
|
||||
id = value.id.rawCurrencyId?.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.data.walletmanager.utils
|
||||
|
||||
import com.tangem.blockchain.common.address.AddressType
|
||||
import com.tangem.blockchainsdk.models.UpdateWalletManagerResult.Address
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.blockchain.common.address.Address as SdkAddress
|
||||
|
||||
/**
|
||||
* Convert [SdkAddress] to [Address]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object SdkAddressToAddressConverter : Converter<SdkAddress, Address> {
|
||||
|
||||
override fun convert(value: SdkAddress): Address {
|
||||
return Address(
|
||||
value = value.value,
|
||||
type = when (value.type) {
|
||||
AddressType.Default -> Address.Type.Primary
|
||||
AddressType.Legacy -> Address.Type.Secondary
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.data.walletmanager.utils
|
||||
|
||||
import com.tangem.domain.transaction.models.AssetRequirementsCondition
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.blockchain.common.trustlines.AssetRequirementsCondition as SdkRequirementsCondition
|
||||
|
||||
internal class SdkRequirementsConditionConverter : Converter<SdkRequirementsCondition, AssetRequirementsCondition> {
|
||||
override fun convert(value: SdkRequirementsCondition): AssetRequirementsCondition {
|
||||
return when (value) {
|
||||
is SdkRequirementsCondition.PaidTransaction -> AssetRequirementsCondition.PaidTransaction
|
||||
is SdkRequirementsCondition.RequiredTrustline -> AssetRequirementsCondition.RequiredTrustline(
|
||||
requiredAmount = requireNotNull(value.amount.value),
|
||||
currencySymbol = value.amount.currencySymbol,
|
||||
decimals = value.amount.decimals,
|
||||
)
|
||||
is SdkRequirementsCondition.PaidTransactionWithFee -> AssetRequirementsCondition.PaidTransactionWithFee(
|
||||
feeAmount = requireNotNull(value.feeAmount.value),
|
||||
feeCurrencySymbol = value.feeAmount.currencySymbol,
|
||||
decimals = value.feeAmount.decimals,
|
||||
)
|
||||
is SdkRequirementsCondition.IncompleteTransaction -> AssetRequirementsCondition.IncompleteTransaction(
|
||||
amount = requireNotNull(value.amount.value),
|
||||
currencySymbol = value.amount.currencySymbol,
|
||||
currencyDecimals = value.amount.decimals,
|
||||
feeAmount = requireNotNull(value.feeAmount.value),
|
||||
feeCurrencySymbol = value.feeAmount.currencySymbol,
|
||||
feeCurrencyDecimals = value.feeAmount.decimals,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.data.walletmanager.utils
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.blockchain.common.Token as SdkToken
|
||||
|
||||
internal class SdkTokenConverter : Converter<CryptoCurrency.Token, SdkToken> {
|
||||
|
||||
override fun convert(value: CryptoCurrency.Token): SdkToken {
|
||||
return SdkToken(
|
||||
id = value.id.rawCurrencyId?.value,
|
||||
name = value.name,
|
||||
symbol = value.symbol,
|
||||
contractAddress = value.contractAddress,
|
||||
decimals = value.decimals,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package com.tangem.data.walletmanager.utils
|
||||
|
||||
import com.tangem.blockchain.transactionhistory.models.TransactionHistoryItem
|
||||
import com.tangem.domain.models.network.TxInfo
|
||||
import com.tangem.domain.walletmanager.model.SmartContractMethod
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.blockchain.transactionhistory.models.TransactionHistoryItem as SdkTransactionHistoryItem
|
||||
|
||||
internal class SdkTransactionHistoryItemConverter(
|
||||
smartContractMethods: Map<String, SmartContractMethod>,
|
||||
) : Converter<SdkTransactionHistoryItem, TxInfo> {
|
||||
|
||||
private val typeConverter by lazy { SdkTransactionTypeConverter(smartContractMethods) }
|
||||
|
||||
override fun convert(value: SdkTransactionHistoryItem): TxInfo = TxInfo(
|
||||
txHash = value.txHash,
|
||||
timestampInMillis = value.timestamp,
|
||||
isOutgoing = value.isOutgoing,
|
||||
destinationType = value.destinationType.toDomain(),
|
||||
sourceType = value.sourceType.toDomain(),
|
||||
interactionAddressType = value.extractInteractionAddressType(),
|
||||
status = when (value.status) {
|
||||
SdkTransactionHistoryItem.TransactionStatus.Confirmed -> TxInfo.TransactionStatus.Confirmed
|
||||
SdkTransactionHistoryItem.TransactionStatus.Failed -> TxInfo.TransactionStatus.Failed
|
||||
SdkTransactionHistoryItem.TransactionStatus.Unconfirmed -> TxInfo.TransactionStatus.Unconfirmed
|
||||
},
|
||||
type = typeConverter.convert(value.type),
|
||||
amount = requireNotNull(value.amount.value) { "Transaction amount value must not be null" },
|
||||
)
|
||||
|
||||
private fun SdkTransactionHistoryItem.SourceType.toDomain(): TxInfo.SourceType = when (this) {
|
||||
is TransactionHistoryItem.SourceType.Single -> TxInfo.SourceType.Single(address)
|
||||
is TransactionHistoryItem.SourceType.Multiple -> TxInfo.SourceType.Multiple(addresses)
|
||||
}
|
||||
|
||||
private fun SdkTransactionHistoryItem.DestinationType.toDomain(): TxInfo.DestinationType = when (this) {
|
||||
is SdkTransactionHistoryItem.DestinationType.Single -> TxInfo.DestinationType.Single(
|
||||
addressType.toDomain(),
|
||||
)
|
||||
is SdkTransactionHistoryItem.DestinationType.Multiple -> TxInfo.DestinationType.Multiple(
|
||||
addressTypes.map { it.toDomain() },
|
||||
)
|
||||
}
|
||||
|
||||
private fun SdkTransactionHistoryItem.AddressType.toDomain(): TxInfo.AddressType = when (this) {
|
||||
is SdkTransactionHistoryItem.AddressType.Contract -> TxInfo.AddressType.Contract(address)
|
||||
is SdkTransactionHistoryItem.AddressType.User -> TxInfo.AddressType.User(address)
|
||||
is SdkTransactionHistoryItem.AddressType.Validator -> TxInfo.AddressType.Validator(address)
|
||||
}
|
||||
|
||||
private fun SdkTransactionHistoryItem.extractInteractionAddressType(): TxInfo.InteractionAddressType? {
|
||||
return when (val transactionType = type) {
|
||||
SdkTransactionHistoryItem.TransactionType.Transfer -> if (isOutgoing) {
|
||||
mapToInteractionAddressType(destinationType = destinationType)
|
||||
} else {
|
||||
mapToInteractionAddressType(sourceType = sourceType)
|
||||
}
|
||||
|
||||
is SdkTransactionHistoryItem.TransactionType.ContractMethod,
|
||||
is SdkTransactionHistoryItem.TransactionType.ContractMethodName,
|
||||
-> mapToInteractionAddressType(destinationType = destinationType)
|
||||
|
||||
is SdkTransactionHistoryItem.TransactionType.TronStakingTransactionType.VoteWitnessContract -> {
|
||||
TxInfo.InteractionAddressType.Validator(address = transactionType.validatorAddress)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapToInteractionAddressType(
|
||||
destinationType: SdkTransactionHistoryItem.DestinationType,
|
||||
): TxInfo.InteractionAddressType {
|
||||
return when (destinationType) {
|
||||
is TransactionHistoryItem.DestinationType.Multiple -> TxInfo.InteractionAddressType.Multiple(
|
||||
destinationType.addressTypes.map { it.address },
|
||||
)
|
||||
is TransactionHistoryItem.DestinationType.Single -> when (destinationType.addressType) {
|
||||
is TransactionHistoryItem.AddressType.Contract -> TxInfo.InteractionAddressType.Contract(
|
||||
destinationType.addressType.address,
|
||||
)
|
||||
is TransactionHistoryItem.AddressType.User -> TxInfo.InteractionAddressType.User(
|
||||
destinationType.addressType.address,
|
||||
)
|
||||
is TransactionHistoryItem.AddressType.Validator -> TxInfo.InteractionAddressType.Validator(
|
||||
destinationType.addressType.address,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapToInteractionAddressType(
|
||||
sourceType: SdkTransactionHistoryItem.SourceType,
|
||||
): TxInfo.InteractionAddressType {
|
||||
return when (sourceType) {
|
||||
is TransactionHistoryItem.SourceType.Multiple -> TxInfo.InteractionAddressType.Multiple(
|
||||
sourceType.addresses,
|
||||
)
|
||||
is TransactionHistoryItem.SourceType.Single -> {
|
||||
TxInfo.InteractionAddressType.User(sourceType.address)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.data.walletmanager.utils
|
||||
|
||||
import com.tangem.blockchain.transactionhistory.TransactionHistoryState
|
||||
import com.tangem.domain.txhistory.models.TxHistoryState
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.blockchain.transactionhistory.TransactionHistoryState as SdkTransactionHistoryState
|
||||
|
||||
internal class SdkTransactionHistoryStateConverter : Converter<SdkTransactionHistoryState, TxHistoryState> {
|
||||
|
||||
override fun convert(value: TransactionHistoryState): TxHistoryState = when (value) {
|
||||
is TransactionHistoryState.Success.Empty -> TxHistoryState.Success.Empty
|
||||
is TransactionHistoryState.Success.HasTransactions -> TxHistoryState.Success.HasTransactions(value.txCount)
|
||||
is TransactionHistoryState.Failed.FetchError -> TxHistoryState.Failed.FetchError(value.exception)
|
||||
is TransactionHistoryState.NotImplemented -> TxHistoryState.NotImplemented
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.tangem.data.walletmanager.utils
|
||||
|
||||
import com.tangem.blockchain.transactionhistory.models.TransactionHistoryItem.TransactionType
|
||||
import com.tangem.domain.models.network.TxInfo
|
||||
import com.tangem.domain.walletmanager.model.SmartContractMethod
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class SdkTransactionTypeConverter(
|
||||
private val smartContractMethods: Map<String, SmartContractMethod>,
|
||||
) : Converter<TransactionType, TxInfo.TransactionType> {
|
||||
|
||||
override fun convert(value: TransactionType): TxInfo.TransactionType {
|
||||
return when (value) {
|
||||
is TransactionType.ContractMethod -> {
|
||||
getTransactionType(methodName = smartContractMethods[value.id]?.name)
|
||||
}
|
||||
is TransactionType.ContractMethodName -> {
|
||||
getTransactionType(methodName = value.name)
|
||||
}
|
||||
is TransactionType.Transfer -> {
|
||||
TxInfo.TransactionType.Transfer
|
||||
}
|
||||
is TransactionType.TronStakingTransactionType.FreezeBalanceV2Contract -> {
|
||||
TxInfo.TransactionType.Staking.Stake
|
||||
}
|
||||
is TransactionType.TronStakingTransactionType.UnfreezeBalanceV2Contract -> {
|
||||
TxInfo.TransactionType.Staking.Unstake
|
||||
}
|
||||
is TransactionType.TronStakingTransactionType.VoteWitnessContract -> {
|
||||
TxInfo.TransactionType.Staking.Vote(value.validatorAddress)
|
||||
}
|
||||
is TransactionType.TronStakingTransactionType.WithdrawBalanceContract -> {
|
||||
TxInfo.TransactionType.Staking.ClaimRewards
|
||||
}
|
||||
is TransactionType.TronStakingTransactionType.WithdrawExpireUnfreezeContract -> {
|
||||
TxInfo.TransactionType.Staking.Withdraw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTransactionType(methodName: String?): TxInfo.TransactionType {
|
||||
return when (methodName) {
|
||||
"transfer" -> TxInfo.TransactionType.Transfer
|
||||
"approve" -> TxInfo.TransactionType.Approve
|
||||
"swap" -> TxInfo.TransactionType.Swap
|
||||
"buyVoucher",
|
||||
"buyVoucherPOL",
|
||||
"delegate",
|
||||
-> TxInfo.TransactionType.Staking.Stake
|
||||
"sellVoucher_new",
|
||||
"sellVoucher_newPOL",
|
||||
"undelegate",
|
||||
-> TxInfo.TransactionType.Staking.Unstake
|
||||
"unstakeClaimTokens_new",
|
||||
"unstakeClaimTokens_newPOL",
|
||||
"claim",
|
||||
-> TxInfo.TransactionType.Staking.Withdraw
|
||||
"withdrawRewards",
|
||||
"withdrawRewardsPOL",
|
||||
-> TxInfo.TransactionType.Staking.ClaimRewards
|
||||
"redelegate" -> TxInfo.TransactionType.Staking.Restake
|
||||
null -> TxInfo.TransactionType.UnknownOperation
|
||||
else -> TxInfo.TransactionType.Operation(name = methodName.replaceFirstChar { it.titlecase() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package com.tangem.data.walletmanager.utils
|
||||
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchainsdk.models.UpdateWalletManagerResult.Address
|
||||
import com.tangem.domain.models.network.TxInfo
|
||||
import com.tangem.utils.converter.Converter
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Convert [TransactionData] to [TxInfo]
|
||||
*
|
||||
* @property walletAddresses wallet addresses
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class TransactionDataToTxHistoryItemConverter(
|
||||
private val walletAddresses: Set<Address>,
|
||||
private val feePaidCurrency: FeePaidCurrency,
|
||||
) : Converter<TransactionData.Uncompiled, TxInfo?> {
|
||||
|
||||
override fun convert(value: TransactionData.Uncompiled): TxInfo? {
|
||||
val hash = value.hash ?: return null
|
||||
val millis = value.date?.timeInMillis ?: return null
|
||||
val amount = getTransactionAmountValue(value.amount, value.fee?.amount) ?: return null
|
||||
val isOutgoing = value.sourceAddress in walletAddresses.map(Address::value)
|
||||
|
||||
return TxInfo(
|
||||
txHash = hash,
|
||||
timestampInMillis = millis,
|
||||
isOutgoing = isOutgoing,
|
||||
destinationType = TxInfo.DestinationType.Single(
|
||||
addressType = TxInfo.AddressType.User(value.destinationAddress),
|
||||
),
|
||||
sourceType = TxInfo.SourceType.Single(value.sourceAddress),
|
||||
interactionAddressType = TxInfo.InteractionAddressType.User(
|
||||
address = if (isOutgoing) value.destinationAddress else value.sourceAddress,
|
||||
),
|
||||
status = when (value.status) {
|
||||
TransactionStatus.Confirmed -> TxInfo.TransactionStatus.Confirmed
|
||||
TransactionStatus.Unconfirmed -> TxInfo.TransactionStatus.Unconfirmed
|
||||
},
|
||||
type = TxInfo.TransactionType.Transfer,
|
||||
amount = amount,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getTransactionAmountValue(amount: Amount, feeAmount: Amount?): BigDecimal? {
|
||||
val feeValue = feeAmount?.value ?: BigDecimal.ZERO
|
||||
val value = amount.value
|
||||
|
||||
if (value == null) {
|
||||
Timber.w("Transaction amount must not be null: ${amount.currencySymbol}")
|
||||
}
|
||||
|
||||
return when (feePaidCurrency) {
|
||||
FeePaidCurrency.SameCurrency -> value?.plus(feeValue)
|
||||
FeePaidCurrency.Coin -> {
|
||||
if (amount.type is AmountType.Coin) value?.plus(feeValue) else value
|
||||
}
|
||||
is FeePaidCurrency.Token -> {
|
||||
val token = (amount.type as? AmountType.Token)?.token ?: return value
|
||||
if (isSameToken(token, feePaidCurrency.token)) {
|
||||
value?.plus(feeValue)
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
is FeePaidCurrency.FeeResource -> value
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSameToken(amountToken: Token, feeToken: Token): Boolean {
|
||||
return amountToken.contractAddress.equals(feeToken.contractAddress, ignoreCase = true) &&
|
||||
amountToken.symbol.equals(feeToken.symbol, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue