Updated on 2026-08-14
This commit is contained in:
parent
9e3a352ff6
commit
8b69fb70dc
10 changed files with 56 additions and 25 deletions
|
|
@ -68,7 +68,7 @@
|
||||||
"version": "undefined"
|
"version": "undefined"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "SWAP_INTEGRATED_APPROVE",
|
"name": "AND_15120_SWAP_INTEGRATED_APPROVE",
|
||||||
"version": "undefined"
|
"version": "undefined"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@ internal class FeatureTogglesNamingConventionTest {
|
||||||
"SOLANA_TX_HISTORY_ENABLED",
|
"SOLANA_TX_HISTORY_ENABLED",
|
||||||
"STAKING_ETH_ENABLED",
|
"STAKING_ETH_ENABLED",
|
||||||
"SWAP_AB_ENABLED",
|
"SWAP_AB_ENABLED",
|
||||||
"SWAP_INTEGRATED_APPROVE",
|
|
||||||
"USEDESK_ENABLED",
|
"USEDESK_ENABLED",
|
||||||
"VIRTUAL_ACCOUNTS_ENABLED",
|
"VIRTUAL_ACCOUNTS_ENABLED",
|
||||||
"VISA_ONBOARDING_ENABLED",
|
"VISA_ONBOARDING_ENABLED",
|
||||||
|
|
|
||||||
|
|
@ -30,13 +30,13 @@ interface SwapTransferInteractor {
|
||||||
suspend fun loadFee(
|
suspend fun loadFee(
|
||||||
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
||||||
toSwapCurrencyStatus: SwapCurrencyStatus,
|
toSwapCurrencyStatus: SwapCurrencyStatus,
|
||||||
fromTokenAmount: String,
|
fromTokenAmount: BigDecimal,
|
||||||
): Either<GetFeeError, TransactionFee>
|
): Either<GetFeeError, TransactionFee>
|
||||||
|
|
||||||
suspend fun loadFeeExtended(
|
suspend fun loadFeeExtended(
|
||||||
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
||||||
toSwapCurrencyStatus: SwapCurrencyStatus,
|
toSwapCurrencyStatus: SwapCurrencyStatus,
|
||||||
fromTokenAmount: String,
|
fromTokenAmount: BigDecimal,
|
||||||
): Either<GetFeeError, TransactionFeeExtended>
|
): Either<GetFeeError, TransactionFeeExtended>
|
||||||
|
|
||||||
suspend fun sendTransfer(
|
suspend fun sendTransfer(
|
||||||
|
|
|
||||||
|
|
@ -205,15 +205,14 @@ class SwapTransferInteractorImpl @Inject constructor(
|
||||||
override suspend fun loadFee(
|
override suspend fun loadFee(
|
||||||
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
||||||
toSwapCurrencyStatus: SwapCurrencyStatus,
|
toSwapCurrencyStatus: SwapCurrencyStatus,
|
||||||
fromTokenAmount: String,
|
fromTokenAmount: BigDecimal,
|
||||||
): Either<GetFeeError, TransactionFee> {
|
): Either<GetFeeError, TransactionFee> {
|
||||||
val amount = fromTokenAmount.parseBigDecimalOrNull() ?: BigDecimal.ZERO
|
|
||||||
val destination = toSwapCurrencyStatus.destinationAddress() ?: return feeDataError(
|
val destination = toSwapCurrencyStatus.destinationAddress() ?: return feeDataError(
|
||||||
message = "Destination address is null",
|
message = "Destination address is null",
|
||||||
)
|
)
|
||||||
|
|
||||||
return getFeeUseCase(
|
return getFeeUseCase(
|
||||||
amount = amount,
|
amount = fromTokenAmount,
|
||||||
destination = destination,
|
destination = destination,
|
||||||
userWallet = fromSwapCurrencyStatus.userWallet,
|
userWallet = fromSwapCurrencyStatus.userWallet,
|
||||||
cryptoCurrency = fromSwapCurrencyStatus.currency,
|
cryptoCurrency = fromSwapCurrencyStatus.currency,
|
||||||
|
|
@ -223,9 +222,8 @@ class SwapTransferInteractorImpl @Inject constructor(
|
||||||
override suspend fun loadFeeExtended(
|
override suspend fun loadFeeExtended(
|
||||||
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
||||||
toSwapCurrencyStatus: SwapCurrencyStatus,
|
toSwapCurrencyStatus: SwapCurrencyStatus,
|
||||||
fromTokenAmount: String,
|
fromTokenAmount: BigDecimal,
|
||||||
): Either<GetFeeError, TransactionFeeExtended> {
|
): Either<GetFeeError, TransactionFeeExtended> {
|
||||||
val amount = fromTokenAmount.parseBigDecimalOrNull() ?: BigDecimal.ZERO
|
|
||||||
val destination = toSwapCurrencyStatus.destinationAddress() ?: return feeDataError(
|
val destination = toSwapCurrencyStatus.destinationAddress() ?: return feeDataError(
|
||||||
message = "Destination address is null",
|
message = "Destination address is null",
|
||||||
)
|
)
|
||||||
|
|
@ -233,7 +231,7 @@ class SwapTransferInteractorImpl @Inject constructor(
|
||||||
val currency = fromSwapCurrencyStatus.currency
|
val currency = fromSwapCurrencyStatus.currency
|
||||||
|
|
||||||
val transactionData = createTransferTransactionUseCase(
|
val transactionData = createTransferTransactionUseCase(
|
||||||
amount = amount.convertToSdkAmount(
|
amount = fromTokenAmount.convertToSdkAmount(
|
||||||
cryptoCurrencyStatus = fromSwapCurrencyStatus.status,
|
cryptoCurrencyStatus = fromSwapCurrencyStatus.status,
|
||||||
),
|
),
|
||||||
memo = null,
|
memo = null,
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,17 @@ internal class SwapTransferInteractorImplTest {
|
||||||
every { getBalanceHidingSettingsUseCase.isBalanceHidden() } returns flowOf(true)
|
every { getBalanceHidingSettingsUseCase.isBalanceHidden() } returns flowOf(true)
|
||||||
coEvery { isAccountsModeEnabledUseCase.invokeSync() } returns true
|
coEvery { isAccountsModeEnabledUseCase.invokeSync() } returns true
|
||||||
val currencyCheck = buildCurrencyCheck()
|
val currencyCheck = buildCurrencyCheck()
|
||||||
coEvery { getCurrencyCheckUseCase(any(), any(), any(), any(), any(), any(), any()) } returns currencyCheck
|
coEvery {
|
||||||
|
getCurrencyCheckUseCase(
|
||||||
|
userWalletId = any(),
|
||||||
|
currencyStatus = any(),
|
||||||
|
feeCurrencyStatus = any(),
|
||||||
|
amount = any(),
|
||||||
|
fee = any(),
|
||||||
|
feeCurrencyBalanceAfterTransaction = any(),
|
||||||
|
recipientAddress = any(),
|
||||||
|
)
|
||||||
|
} returns currencyCheck
|
||||||
coEvery {
|
coEvery {
|
||||||
isAmountSubtractAvailableUseCase(any(), any(), any())
|
isAmountSubtractAvailableUseCase(any(), any(), any())
|
||||||
} returns false.right()
|
} returns false.right()
|
||||||
|
|
@ -193,7 +203,17 @@ internal class SwapTransferInteractorImplTest {
|
||||||
every { getBalanceHidingSettingsUseCase.isBalanceHidden() } returns flowOf(true)
|
every { getBalanceHidingSettingsUseCase.isBalanceHidden() } returns flowOf(true)
|
||||||
coEvery { isAccountsModeEnabledUseCase.invokeSync() } returns true
|
coEvery { isAccountsModeEnabledUseCase.invokeSync() } returns true
|
||||||
val currencyCheck = buildCurrencyCheck()
|
val currencyCheck = buildCurrencyCheck()
|
||||||
coEvery { getCurrencyCheckUseCase(any(), any(), any(), any(), any(), any(), any()) } returns currencyCheck
|
coEvery {
|
||||||
|
getCurrencyCheckUseCase(
|
||||||
|
userWalletId = any(),
|
||||||
|
currencyStatus = any(),
|
||||||
|
feeCurrencyStatus = any(),
|
||||||
|
amount = any(),
|
||||||
|
fee = any(),
|
||||||
|
feeCurrencyBalanceAfterTransaction = any(),
|
||||||
|
recipientAddress = any(),
|
||||||
|
)
|
||||||
|
} returns currencyCheck
|
||||||
coEvery {
|
coEvery {
|
||||||
isAmountSubtractAvailableUseCase(any(), any(), any())
|
isAmountSubtractAvailableUseCase(any(), any(), any())
|
||||||
} returns false.right()
|
} returns false.right()
|
||||||
|
|
@ -259,7 +279,15 @@ internal class SwapTransferInteractorImplTest {
|
||||||
every { getBalanceHidingSettingsUseCase.isBalanceHidden() } returns flowOf(false)
|
every { getBalanceHidingSettingsUseCase.isBalanceHidden() } returns flowOf(false)
|
||||||
coEvery { isAccountsModeEnabledUseCase.invokeSync() } returns false
|
coEvery { isAccountsModeEnabledUseCase.invokeSync() } returns false
|
||||||
coEvery {
|
coEvery {
|
||||||
getCurrencyCheckUseCase(any(), any(), any(), any(), any(), any(), any())
|
getCurrencyCheckUseCase(
|
||||||
|
userWalletId = any(),
|
||||||
|
currencyStatus = any(),
|
||||||
|
feeCurrencyStatus = any(),
|
||||||
|
amount = any(),
|
||||||
|
fee = any(),
|
||||||
|
feeCurrencyBalanceAfterTransaction = any(),
|
||||||
|
recipientAddress = any(),
|
||||||
|
)
|
||||||
} returns buildCurrencyCheck()
|
} returns buildCurrencyCheck()
|
||||||
coEvery {
|
coEvery {
|
||||||
isAmountSubtractAvailableUseCase(any(), any(), any())
|
isAmountSubtractAvailableUseCase(any(), any(), any())
|
||||||
|
|
@ -309,7 +337,7 @@ internal class SwapTransferInteractorImplTest {
|
||||||
val result = sut.loadFee(
|
val result = sut.loadFee(
|
||||||
fromSwapCurrencyStatus = fromCurrencyStatus,
|
fromSwapCurrencyStatus = fromCurrencyStatus,
|
||||||
toSwapCurrencyStatus = toCurrencyStatus,
|
toSwapCurrencyStatus = toCurrencyStatus,
|
||||||
fromTokenAmount = "1.5",
|
fromTokenAmount = BigDecimal("1.5"),
|
||||||
)
|
)
|
||||||
|
|
||||||
assertThat(result).isEqualTo(transactionFee.right())
|
assertThat(result).isEqualTo(transactionFee.right())
|
||||||
|
|
@ -365,7 +393,7 @@ internal class SwapTransferInteractorImplTest {
|
||||||
val result = sut.loadFeeExtended(
|
val result = sut.loadFeeExtended(
|
||||||
fromSwapCurrencyStatus = fromCurrencyStatus,
|
fromSwapCurrencyStatus = fromCurrencyStatus,
|
||||||
toSwapCurrencyStatus = toCurrencyStatus,
|
toSwapCurrencyStatus = toCurrencyStatus,
|
||||||
fromTokenAmount = "2.0",
|
fromTokenAmount = BigDecimal("2.0"),
|
||||||
)
|
)
|
||||||
|
|
||||||
assertThat(result).isEqualTo(feeExtended.right())
|
assertThat(result).isEqualTo(feeExtended.right())
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ internal class DefaultSwapFeatureToggles @Inject constructor(
|
||||||
)
|
)
|
||||||
|
|
||||||
override val isSwapIntegratedApproveEnabled: Boolean = featureTogglesManager.isFeatureEnabled(
|
override val isSwapIntegratedApproveEnabled: Boolean = featureTogglesManager.isFeatureEnabled(
|
||||||
toggle = FeatureToggles.SWAP_INTEGRATED_APPROVE,
|
toggle = FeatureToggles.AND_15120_SWAP_INTEGRATED_APPROVE,
|
||||||
)
|
)
|
||||||
|
|
||||||
override val isSwapAbEnabled: Boolean = featureTogglesManager.isFeatureEnabled(
|
override val isSwapAbEnabled: Boolean = featureTogglesManager.isFeatureEnabled(
|
||||||
|
|
|
||||||
|
|
@ -2180,6 +2180,7 @@ internal class SwapModel @Inject constructor(
|
||||||
dataState.fromSwapCurrencyStatus ?: return Either.Left(GetFeeError.UnknownError)
|
dataState.fromSwapCurrencyStatus ?: return Either.Left(GetFeeError.UnknownError)
|
||||||
val toSwapCurrencyStatus =
|
val toSwapCurrencyStatus =
|
||||||
dataState.toSwapCurrencyStatus ?: return Either.Left(GetFeeError.UnknownError)
|
dataState.toSwapCurrencyStatus ?: return Either.Left(GetFeeError.UnknownError)
|
||||||
|
val amount = lastAmount.value.parseBigDecimalOrNull() ?: return Either.Left(GetFeeError.UnknownError)
|
||||||
val shouldTransferInsteadOfSwap = swapTransferInteractor.shouldTransferInsteadOfSwap(
|
val shouldTransferInsteadOfSwap = swapTransferInteractor.shouldTransferInsteadOfSwap(
|
||||||
fromSwapCurrencyStatus.currency,
|
fromSwapCurrencyStatus.currency,
|
||||||
toSwapCurrencyStatus.currency,
|
toSwapCurrencyStatus.currency,
|
||||||
|
|
@ -2188,7 +2189,7 @@ internal class SwapModel @Inject constructor(
|
||||||
return swapTransferInteractor.loadFee(
|
return swapTransferInteractor.loadFee(
|
||||||
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||||
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
||||||
fromTokenAmount = lastAmount.value,
|
fromTokenAmount = amount,
|
||||||
).onLeft {
|
).onLeft {
|
||||||
TangemLogger.e("loadFee[transfer]: Failed to load fee with error $it")
|
TangemLogger.e("loadFee[transfer]: Failed to load fee with error $it")
|
||||||
}.onRight {
|
}.onRight {
|
||||||
|
|
@ -2202,9 +2203,7 @@ internal class SwapModel @Inject constructor(
|
||||||
return Either.Left(GetFeeError.UnknownError)
|
return Either.Left(GetFeeError.UnknownError)
|
||||||
}
|
}
|
||||||
|
|
||||||
val amountDecimal = lastAmount.value.replace(",", ".").toBigDecimalOrNull()
|
val swapAmount = SwapAmount(amount, fromSwapCurrencyStatus.currency.decimals)
|
||||||
?: return Either.Left(GetFeeError.UnknownError)
|
|
||||||
val swapAmount = SwapAmount(amountDecimal, fromSwapCurrencyStatus.currency.decimals)
|
|
||||||
val swapDataForCall = when (quoteState.swapProvider.type) {
|
val swapDataForCall = when (quoteState.swapProvider.type) {
|
||||||
ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> {
|
ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> {
|
||||||
quoteState.swapDataModel ?: return Either.Left(GetFeeError.UnknownError)
|
quoteState.swapDataModel ?: return Either.Left(GetFeeError.UnknownError)
|
||||||
|
|
@ -2235,6 +2234,8 @@ internal class SwapModel @Inject constructor(
|
||||||
dataState.fromSwapCurrencyStatus ?: return Either.Left(GetFeeError.UnknownError)
|
dataState.fromSwapCurrencyStatus ?: return Either.Left(GetFeeError.UnknownError)
|
||||||
val toSwapCurrencyStatus =
|
val toSwapCurrencyStatus =
|
||||||
dataState.toSwapCurrencyStatus ?: return Either.Left(GetFeeError.UnknownError)
|
dataState.toSwapCurrencyStatus ?: return Either.Left(GetFeeError.UnknownError)
|
||||||
|
val amount = lastAmount.value.parseBigDecimalOrNull() ?: return Either.Left(GetFeeError.UnknownError)
|
||||||
|
|
||||||
val shouldTransferInsteadOfSwap = swapTransferInteractor.shouldTransferInsteadOfSwap(
|
val shouldTransferInsteadOfSwap = swapTransferInteractor.shouldTransferInsteadOfSwap(
|
||||||
fromSwapCurrencyStatus.currency,
|
fromSwapCurrencyStatus.currency,
|
||||||
toSwapCurrencyStatus.currency,
|
toSwapCurrencyStatus.currency,
|
||||||
|
|
@ -2243,7 +2244,7 @@ internal class SwapModel @Inject constructor(
|
||||||
return swapTransferInteractor.loadFeeExtended(
|
return swapTransferInteractor.loadFeeExtended(
|
||||||
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||||
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
||||||
fromTokenAmount = lastAmount.value,
|
fromTokenAmount = amount,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val quoteState = dataState.getCurrentLoadedSwapState() ?: return Either.Left(GetFeeError.UnknownError)
|
val quoteState = dataState.getCurrentLoadedSwapState() ?: return Either.Left(GetFeeError.UnknownError)
|
||||||
|
|
@ -2252,8 +2253,7 @@ internal class SwapModel @Inject constructor(
|
||||||
return Either.Left(GetFeeError.UnknownError)
|
return Either.Left(GetFeeError.UnknownError)
|
||||||
}
|
}
|
||||||
|
|
||||||
val amountDecimal = lastAmount.value.parseBigDecimalOrNull() ?: return Either.Left(GetFeeError.UnknownError)
|
val swapAmount = SwapAmount(amount, fromSwapCurrencyStatus.currency.decimals)
|
||||||
val swapAmount = SwapAmount(amountDecimal, fromSwapCurrencyStatus.currency.decimals)
|
|
||||||
|
|
||||||
// DEX path requires a SwapDataModel.
|
// DEX path requires a SwapDataModel.
|
||||||
val swapDataForCall = when (quoteState.swapProvider.type) {
|
val swapDataForCall = when (quoteState.swapProvider.type) {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
# https://github.com/tangem/tangem-sdk-android/
|
# https://github.com/tangem/tangem-sdk-android/
|
||||||
# https://github.com/tangem/vico
|
# https://github.com/tangem/vico
|
||||||
|
|
||||||
tangemBlockchainSdk = "develop-1532"
|
tangemBlockchainSdk = "develop-1535"
|
||||||
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
|
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
|
||||||
tangemCardSdk = "develop-620"
|
tangemCardSdk = "develop-620"
|
||||||
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
|
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import javax.inject.Inject
|
||||||
*
|
*
|
||||||
[REDACTED_AUTHOR]
|
[REDACTED_AUTHOR]
|
||||||
*/
|
*/
|
||||||
|
@Suppress("LongParameterList")
|
||||||
internal class WalletManagerFactoryCreator @Inject constructor(
|
internal class WalletManagerFactoryCreator @Inject constructor(
|
||||||
private val accountCreator: AccountCreator,
|
private val accountCreator: AccountCreator,
|
||||||
private val blockchainDataStorage: BlockchainDataStorage,
|
private val blockchainDataStorage: BlockchainDataStorage,
|
||||||
|
|
@ -41,6 +42,7 @@ internal class WalletManagerFactoryCreator @Inject constructor(
|
||||||
isSolanaTxHistoryEnabled = featureToggleValues.isSolanaTxHistoryEnabled,
|
isSolanaTxHistoryEnabled = featureToggleValues.isSolanaTxHistoryEnabled,
|
||||||
isSolanaScaledUiAmountEnabled = featureToggleValues.isSolanaScaledUiAmountEnabled,
|
isSolanaScaledUiAmountEnabled = featureToggleValues.isSolanaScaledUiAmountEnabled,
|
||||||
isHederaErc20Enabled = featureToggleValues.isHederaErc20Enabled,
|
isHederaErc20Enabled = featureToggleValues.isHederaErc20Enabled,
|
||||||
|
isStateOverrideGasEstimateEnabled = featureToggleValues.isStateOverrideGasEstimateEnabled,
|
||||||
),
|
),
|
||||||
blockchainDataStorage = blockchainDataStorage,
|
blockchainDataStorage = blockchainDataStorage,
|
||||||
loggers = listOf(blockchainSDKLogger),
|
loggers = listOf(blockchainSDKLogger),
|
||||||
|
|
@ -52,5 +54,6 @@ internal class WalletManagerFactoryCreator @Inject constructor(
|
||||||
val isSolanaScaledUiAmountEnabled: Boolean,
|
val isSolanaScaledUiAmountEnabled: Boolean,
|
||||||
val isYieldModeSwapEnabled: Boolean,
|
val isYieldModeSwapEnabled: Boolean,
|
||||||
val isHederaErc20Enabled: Boolean,
|
val isHederaErc20Enabled: Boolean,
|
||||||
|
val isStateOverrideGasEstimateEnabled: Boolean,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -23,8 +23,8 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||||
import com.tangem.datasource.di.NetworkMoshi
|
import com.tangem.datasource.di.NetworkMoshi
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
|
||||||
import com.tangem.libs.blockchain_sdk.BuildConfig
|
import com.tangem.libs.blockchain_sdk.BuildConfig
|
||||||
|
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
|
|
@ -110,6 +110,9 @@ internal object BlockchainSDKFactoryModule {
|
||||||
isHederaErc20Enabled = featureTogglesManager.isFeatureEnabled(
|
isHederaErc20Enabled = featureTogglesManager.isFeatureEnabled(
|
||||||
FeatureToggles.HEDERA_ERC20_ENABLED,
|
FeatureToggles.HEDERA_ERC20_ENABLED,
|
||||||
),
|
),
|
||||||
|
isStateOverrideGasEstimateEnabled = featureTogglesManager.isFeatureEnabled(
|
||||||
|
FeatureToggles.AND_15120_SWAP_INTEGRATED_APPROVE,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue