Updated on 2026-08-14
This commit is contained in:
parent
5c5c31dfc0
commit
3e4e7d8da0
17 changed files with 120 additions and 34 deletions
|
|
@ -9,6 +9,7 @@ import com.tangem.domain.walletconnect.model.WcRequestError
|
|||
import com.tangem.domain.walletconnect.model.WcSession
|
||||
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
|
||||
import com.tangem.domain.walletconnect.usecase.method.WcAddNetworkUseCase
|
||||
import com.tangem.domain.walletconnect.usecase.method.WcNetworkDerivationState
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
|
@ -25,8 +26,10 @@ internal class WcEthAddNetworkUseCase @AssistedInject constructor(
|
|||
get() = context.rawSdkRequest
|
||||
override val network: Network
|
||||
get() = context.network
|
||||
override val walletAddress: String
|
||||
get() = context.accountAddress
|
||||
override val derivationState: WcNetworkDerivationState = when {
|
||||
context.networkDerivationsCount > 1 -> WcNetworkDerivationState.Multiple(walletAddress = context.accountAddress)
|
||||
else -> WcNetworkDerivationState.Single
|
||||
}
|
||||
|
||||
override suspend fun approve(): Either<WcRequestError, String> {
|
||||
return respondService.respond(rawSdkRequest, "")
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ internal class WcEthNetwork(
|
|||
rawSdkRequest = request,
|
||||
network = walletNetwork,
|
||||
accountAddress = accountAddress,
|
||||
networkDerivationsCount = networksConverter.filterWalletNetworkForRequest(chainId, wallet).size,
|
||||
)
|
||||
return when (method) {
|
||||
is WcEthMethod.MessageSign -> factories.messageSign.create(context, method)
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ internal class WcSolanaNetwork(
|
|||
rawSdkRequest = request,
|
||||
network = walletNetwork,
|
||||
accountAddress = accountAddress,
|
||||
networkDerivationsCount = networksConverter.filterWalletNetworkForRequest(chainId, wallet).size,
|
||||
)
|
||||
return when (method) {
|
||||
is WcSolanaMethod.SignMessage -> factories.messageSign.create(context, method)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.tangem.domain.walletconnect.WcAnalyticEvents
|
|||
import com.tangem.domain.walletconnect.model.WcSession
|
||||
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
|
||||
import com.tangem.domain.walletconnect.usecase.method.WcMethodUseCase
|
||||
import com.tangem.domain.walletconnect.usecase.method.WcNetworkDerivationState
|
||||
import com.tangem.domain.walletconnect.usecase.method.WcSignState
|
||||
import com.tangem.domain.walletconnect.usecase.method.WcSignUseCase
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
|
|
@ -23,8 +24,12 @@ internal abstract class BaseWcSignUseCase<MiddleAction, SignModel> :
|
|||
abstract val context: WcMethodUseCaseContext
|
||||
override val network: Network get() = context.network
|
||||
override val session: WcSession get() = context.session
|
||||
override val walletAddress: String get() = context.accountAddress
|
||||
override val rawSdkRequest: WcSdkSessionRequest get() = context.rawSdkRequest
|
||||
override val derivationState: WcNetworkDerivationState
|
||||
get() = when {
|
||||
context.networkDerivationsCount > 1 -> WcNetworkDerivationState.Multiple(context.accountAddress)
|
||||
else -> WcNetworkDerivationState.Single
|
||||
}
|
||||
|
||||
protected val delegate by lazy {
|
||||
WcSignUseCaseDelegate(
|
||||
|
|
@ -73,6 +78,7 @@ internal class WcMethodUseCaseContext(
|
|||
val rawSdkRequest: WcSdkSessionRequest,
|
||||
val network: Network,
|
||||
val accountAddress: String,
|
||||
val networkDerivationsCount: Int,
|
||||
)
|
||||
|
||||
internal typealias SignCollector<SignModel> = FlowCollector<WcSignState<SignModel>>
|
||||
|
|
@ -47,6 +47,7 @@ internal class WcSignUseCaseDelegateTest {
|
|||
network = MockCryptoCurrencyFactory().ethereum.network,
|
||||
accountAddress = "",
|
||||
rawSdkRequest = rawRequestMock,
|
||||
networkDerivationsCount = 1,
|
||||
session = WcSession(
|
||||
wallet = MockUserWalletFactory.create(),
|
||||
networks = setOf(),
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ package com.tangem.domain.walletconnect.usecase.method
|
|||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.walletconnect.model.WcMethod
|
||||
import com.tangem.domain.walletconnect.model.WcRequestError
|
||||
import com.tangem.domain.walletconnect.model.WcSession
|
||||
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface WcMethodUseCase
|
||||
|
|
@ -16,10 +16,15 @@ interface WcMethodContext {
|
|||
val rawSdkRequest: WcSdkSessionRequest
|
||||
val network: Network
|
||||
val method: WcMethod
|
||||
val walletAddress: String
|
||||
val derivationState: WcNetworkDerivationState
|
||||
val wallet: UserWallet get() = session.wallet
|
||||
}
|
||||
|
||||
sealed class WcNetworkDerivationState {
|
||||
data object Single : WcNetworkDerivationState()
|
||||
data class Multiple(val walletAddress: String) : WcNetworkDerivationState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Wc Methods that must be signed by Tangem card
|
||||
* [WcSignState] represents signing state
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.features.walletconnect.transaction.converter
|
||||
|
||||
import com.tangem.domain.walletconnect.usecase.method.WcNetworkDerivationState
|
||||
import com.tangem.features.walletconnect.transaction.entity.common.WcAddressUM
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
private const val ADDRESS_FIRST_PART_LENGTH = 7
|
||||
private const val ADDRESS_SECOND_PART_LENGTH = 4
|
||||
|
||||
internal object WcAddressConverter : Converter<WcNetworkDerivationState, WcAddressUM?> {
|
||||
|
||||
override fun convert(value: WcNetworkDerivationState): WcAddressUM? {
|
||||
return when (value) {
|
||||
is WcNetworkDerivationState.Single -> null
|
||||
is WcNetworkDerivationState.Multiple -> WcAddressUM(
|
||||
fullAddress = value.walletAddress,
|
||||
shortAddress = value.walletAddress.toShortAddressText(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toShortAddressText() =
|
||||
"${take(ADDRESS_FIRST_PART_LENGTH)}...${takeLast(ADDRESS_SECOND_PART_LENGTH)}"
|
||||
}
|
||||
|
|
@ -3,10 +3,7 @@ package com.tangem.features.walletconnect.transaction.converter
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.walletconnect.model.WcEthMethod
|
||||
import com.tangem.domain.walletconnect.model.WcSolanaMethod
|
||||
import com.tangem.domain.walletconnect.usecase.method.WcMutableFee
|
||||
import com.tangem.domain.walletconnect.usecase.method.WcSignState
|
||||
import com.tangem.domain.walletconnect.usecase.method.WcSignStep
|
||||
import com.tangem.domain.walletconnect.usecase.method.WcTransactionUseCase
|
||||
import com.tangem.domain.walletconnect.usecase.method.*
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.walletconnect.impl.R
|
||||
import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendReceiveTransactionCheckResultsUM
|
||||
|
|
@ -41,9 +38,9 @@ internal class WcSendTransactionUMConverter @Inject constructor(
|
|||
feeState = constructFeeState(useCase = value.useCase, actions = value.actions),
|
||||
walletName = value.useCase.session.wallet.name.takeIf { value.useCase.session.showWalletInfo },
|
||||
networkInfo = networkInfoUMConverter.convert(value.useCase.network),
|
||||
address = value.useCase.walletAddress.toShortAddressText(),
|
||||
estimatedWalletChanges = WcSendReceiveTransactionCheckResultsUM(),
|
||||
isLoading = value.signState.domainStep == WcSignStep.Signing,
|
||||
address = WcAddressConverter.convert(value.useCase.derivationState),
|
||||
),
|
||||
feeSelectorUM = value.feeSelectorUM ?: FeeSelectorUM.Loading,
|
||||
transactionRequestInfo = WcTransactionRequestInfoUM(
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ internal class WcSignTransactionUMConverter @Inject constructor(
|
|||
walletName = value.useCase.session.wallet.name.takeIf { value.useCase.session.showWalletInfo },
|
||||
networkInfo = networkInfoUMConverter.convert(value.useCase.network),
|
||||
isLoading = value.signState.domainStep == WcSignStep.Signing,
|
||||
address = WcAddressConverter.convert(value.useCase.derivationState),
|
||||
),
|
||||
transactionRequestInfo = WcTransactionRequestInfoUM(
|
||||
persistentListOf(
|
||||
|
|
|
|||
|
|
@ -17,9 +17,6 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
import kotlinx.collections.immutable.toImmutableList
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val ADDRESS_FIRST_PART_LENGTH = 7
|
||||
private const val ADDRESS_SECOND_PART_LENGTH = 4
|
||||
|
||||
internal class WcSignTypedDataUMConverter @Inject constructor(
|
||||
private val appInfoContentUMConverter: WcTransactionAppInfoContentUMConverter,
|
||||
private val networkInfoUMConverter: WcNetworkInfoUMConverter,
|
||||
|
|
@ -38,7 +35,7 @@ internal class WcSignTypedDataUMConverter @Inject constructor(
|
|||
),
|
||||
walletName = value.useCase.session.wallet.name.takeIf { value.useCase.session.showWalletInfo },
|
||||
networkInfo = networkInfoUMConverter.convert(value.useCase.network),
|
||||
addressText = value.useCase.walletAddress.toShortAddressText(),
|
||||
address = WcAddressConverter.convert(value.useCase.derivationState),
|
||||
isLoading = value.signState.domainStep == WcSignStep.Signing,
|
||||
),
|
||||
transactionRequestInfo = WcTransactionRequestInfoUM(
|
||||
|
|
@ -78,7 +75,4 @@ internal class WcSignTypedDataUMConverter @Inject constructor(
|
|||
val signModel: WcMessageSignUseCase.SignModel,
|
||||
val actions: WcTransactionActionsUM,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun String.toShortAddressText() =
|
||||
"${take(ADDRESS_FIRST_PART_LENGTH)}...${takeLast(ADDRESS_SECOND_PART_LENGTH)}"
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.tangem.features.walletconnect.transaction.entity.common
|
||||
|
||||
data class WcAddressUM(val fullAddress: String, val shortAddress: String)
|
||||
|
|
@ -4,6 +4,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
|||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM
|
||||
import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendReceiveTransactionCheckResultsUM
|
||||
import com.tangem.features.walletconnect.transaction.entity.common.WcAddressUM
|
||||
import com.tangem.features.walletconnect.transaction.entity.common.WcCommonTransactionUM
|
||||
import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM
|
||||
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM
|
||||
|
|
@ -25,6 +26,6 @@ internal data class WcSendTransactionItemUM(
|
|||
val estimatedWalletChanges: WcSendReceiveTransactionCheckResultsUM?,
|
||||
val walletName: String?,
|
||||
val networkInfo: WcNetworkInfoUM,
|
||||
val address: String? = null,
|
||||
val address: WcAddressUM?,
|
||||
val isLoading: Boolean = false,
|
||||
) : TangemBottomSheetConfigContent
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.walletconnect.transaction.entity.sign
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.features.walletconnect.transaction.entity.common.WcAddressUM
|
||||
import com.tangem.features.walletconnect.transaction.entity.common.WcCommonTransactionUM
|
||||
import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM
|
||||
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM
|
||||
|
|
@ -17,6 +18,6 @@ internal data class WcSignTransactionItemUM(
|
|||
val appInfo: WcTransactionAppInfoContentUM,
|
||||
val walletName: String?,
|
||||
val networkInfo: WcNetworkInfoUM,
|
||||
val addressText: String? = null,
|
||||
val address: WcAddressUM?,
|
||||
val isLoading: Boolean = false,
|
||||
) : TangemBottomSheetConfigContent
|
||||
|
|
@ -10,12 +10,14 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import com.tangem.core.ui.components.tooltip.TangemTooltip
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.walletconnect.impl.R
|
||||
import com.tangem.features.walletconnect.transaction.entity.common.WcAddressUM
|
||||
|
||||
@Composable
|
||||
internal fun WcAddressItem(addressText: String, modifier: Modifier = Modifier) {
|
||||
internal fun WcAddressItem(address: WcAddressUM, modifier: Modifier = Modifier) {
|
||||
Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size24),
|
||||
|
|
@ -31,14 +33,20 @@ internal fun WcAddressItem(addressText: String, modifier: Modifier = Modifier) {
|
|||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
TangemTooltip(
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing16)
|
||||
.weight(1f),
|
||||
text = addressText,
|
||||
textAlign = TextAlign.End,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
text = address.fullAddress,
|
||||
content = { contentModifier ->
|
||||
Text(
|
||||
modifier = contentModifier,
|
||||
text = address.shortAddress,
|
||||
textAlign = TextAlign.End,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import com.tangem.core.ui.components.divider.DividerWithPadding
|
|||
import com.tangem.core.ui.extensions.clickableSingle
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
|
||||
import com.tangem.features.walletconnect.transaction.entity.common.WcAddressUM
|
||||
import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM
|
||||
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionFeeState
|
||||
|
||||
|
|
@ -24,8 +25,8 @@ internal fun WcSendTransactionItems(
|
|||
networkInfo: WcNetworkInfoUM,
|
||||
feeState: WcTransactionFeeState,
|
||||
feeSelectorBlockComponent: FeeSelectorBlockComponent?,
|
||||
address: WcAddressUM?,
|
||||
modifier: Modifier = Modifier,
|
||||
address: String? = null,
|
||||
) {
|
||||
val onFeeBlockClicked = remember(feeState) {
|
||||
when (feeState) {
|
||||
|
|
@ -55,11 +56,11 @@ internal fun WcSendTransactionItems(
|
|||
modifier = itemsModifier,
|
||||
networkInfo = networkInfo,
|
||||
)
|
||||
if (!address.isNullOrEmpty()) {
|
||||
if (address != null) {
|
||||
DividerWithPadding(start = 40.dp, end = 12.dp)
|
||||
WcAddressItem(
|
||||
modifier = itemsModifier,
|
||||
addressText = address,
|
||||
address = address,
|
||||
)
|
||||
}
|
||||
if (feeState != WcTransactionFeeState.None) {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import com.tangem.features.walletconnect.transaction.components.PreviewFeeSelect
|
|||
import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangeUM
|
||||
import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangesUM
|
||||
import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendReceiveTransactionCheckResultsUM
|
||||
import com.tangem.features.walletconnect.transaction.entity.common.WcAddressUM
|
||||
import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM
|
||||
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM
|
||||
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionFeeState
|
||||
|
|
@ -191,11 +192,47 @@ private class WcSendTransactionStateProvider : CollectionPreviewParameterProvide
|
|||
),
|
||||
),
|
||||
),
|
||||
isLoading = false,
|
||||
),
|
||||
walletName = "Tangem 2.0",
|
||||
networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22),
|
||||
feeState = WcTransactionFeeState.Success(null, {}),
|
||||
address = "0x345FF...34FA",
|
||||
address = null,
|
||||
),
|
||||
WcSendTransactionItemUM(
|
||||
onDismiss = {},
|
||||
onSend = {},
|
||||
appInfo = WcTransactionAppInfoContentUM(
|
||||
appName = "React App",
|
||||
appIcon = "",
|
||||
verifiedState = VerifiedDAppState.Verified {},
|
||||
appSubtitle = "react-app.walletconnect.com",
|
||||
),
|
||||
estimatedWalletChanges = WcSendReceiveTransactionCheckResultsUM(
|
||||
notificationText = TextReference.Str(
|
||||
"The transaction approves erc20 tokens to a known malicious address",
|
||||
),
|
||||
estimatedWalletChanges = WcEstimatedWalletChangesUM(
|
||||
items = persistentListOf(
|
||||
WcEstimatedWalletChangeUM(
|
||||
iconRes = R.drawable.ic_send_new_24,
|
||||
title = resourceReference(R.string.common_send),
|
||||
description = "- 42 USDT",
|
||||
tokenIconUrl = "https://tangem.com",
|
||||
),
|
||||
WcEstimatedWalletChangeUM(
|
||||
iconRes = R.drawable.ic_receive_new_24,
|
||||
title = resourceReference(R.string.common_receive),
|
||||
description = "+ 1,131.46 MATIC",
|
||||
tokenIconUrl = "https://tangem.com",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
walletName = "Tangem 2.0",
|
||||
networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22),
|
||||
feeState = WcTransactionFeeState.Success(null, {}),
|
||||
address = WcAddressUM("0xdac17f958d2ee523a2206206994597c13d831ec7", "0x345FF...34FA"),
|
||||
),
|
||||
WcSendTransactionItemUM(
|
||||
onDismiss = {},
|
||||
|
|
@ -230,7 +267,7 @@ private class WcSendTransactionStateProvider : CollectionPreviewParameterProvide
|
|||
walletName = null,
|
||||
networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22),
|
||||
feeState = WcTransactionFeeState.None,
|
||||
address = "0x345FF...34FA",
|
||||
address = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -28,6 +28,7 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
import com.tangem.features.walletconnect.connections.entity.VerifiedDAppState
|
||||
import com.tangem.features.walletconnect.connections.ui.WcAppInfoItem
|
||||
import com.tangem.features.walletconnect.impl.R
|
||||
import com.tangem.features.walletconnect.transaction.entity.common.WcAddressUM
|
||||
import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM
|
||||
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM
|
||||
import com.tangem.features.walletconnect.transaction.entity.sign.WcSignTransactionItemUM
|
||||
|
|
@ -124,11 +125,11 @@ private fun WcSignTransactionItems(state: WcSignTransactionItemUM, modifier: Mod
|
|||
modifier = itemsModifier,
|
||||
networkInfo = state.networkInfo,
|
||||
)
|
||||
if (!state.addressText.isNullOrEmpty()) {
|
||||
if (state.address != null) {
|
||||
DividerWithPadding(start = 40.dp, end = 12.dp)
|
||||
WcAddressItem(
|
||||
modifier = itemsModifier,
|
||||
addressText = state.addressText,
|
||||
address = state.address,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -182,6 +183,7 @@ private class WcSignTransactionStateProvider : CollectionPreviewParameterProvide
|
|||
),
|
||||
walletName = "Tangem 2.0",
|
||||
networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22),
|
||||
address = null,
|
||||
),
|
||||
WcSignTransactionItemUM(
|
||||
onDismiss = {},
|
||||
|
|
@ -193,8 +195,8 @@ private class WcSignTransactionStateProvider : CollectionPreviewParameterProvide
|
|||
appSubtitle = "react-app.walletconnect.com",
|
||||
),
|
||||
walletName = null,
|
||||
addressText = "0x345FF...34FA",
|
||||
networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22),
|
||||
address = WcAddressUM("0xdac17f958d2ee523a2206206994597c13d831ec7", "0x345FF...34FA"),
|
||||
),
|
||||
),
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue