Updated on 2026-08-14
This commit is contained in:
parent
b28711ef43
commit
ed26d23a78
9 changed files with 219 additions and 8 deletions
|
|
@ -67,7 +67,7 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor(
|
||||||
commonNetworkStatusFetcher.fetch(
|
commonNetworkStatusFetcher.fetch(
|
||||||
userWalletId = params.userWalletId,
|
userWalletId = params.userWalletId,
|
||||||
network = network,
|
network = network,
|
||||||
networkCurrencies = networksCurrencies[network].orEmpty().toSet(),
|
networkCurrencies = networksCurrencies[network].orEmpty().toSet() + params.extraTokens,
|
||||||
xpub = xpubByNetwork[network],
|
xpub = xpubByNetwork[network],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ internal class DefaultSingleNetworkStatusFetcher @Inject constructor(
|
||||||
params = MultiNetworkStatusFetcher.Params(
|
params = MultiNetworkStatusFetcher.Params(
|
||||||
userWalletId = params.userWalletId,
|
userWalletId = params.userWalletId,
|
||||||
networks = setOf(params.network),
|
networks = setOf(params.network),
|
||||||
|
extraTokens = params.extraTokens,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,9 @@ import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||||
import com.tangem.data.common.network.NetworkFactory
|
import com.tangem.data.common.network.NetworkFactory
|
||||||
import com.tangem.domain.card.common.visa.VisaUtilities
|
import com.tangem.domain.card.common.visa.VisaUtilities
|
||||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||||
import com.tangem.domain.common.wallets.requireUserWalletsSync
|
import com.tangem.domain.common.wallets.getSyncStrict
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
|
import com.tangem.domain.models.network.Network
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -23,9 +24,7 @@ internal class DefaultTangemPayCurrencyFactory @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun create(userWalletId: UserWalletId): CryptoCurrency.Token {
|
override fun create(userWalletId: UserWalletId): CryptoCurrency.Token {
|
||||||
val userWallet = userWalletsListRepository.requireUserWalletsSync()
|
val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)
|
||||||
.firstOrNull { it.walletId == userWalletId }
|
|
||||||
?: error("User wallet with id $userWalletId not found")
|
|
||||||
val network = networkFactory.create(
|
val network = networkFactory.create(
|
||||||
blockchain = VisaUtilities.visaBlockchain,
|
blockchain = VisaUtilities.visaBlockchain,
|
||||||
userWallet = userWallet,
|
userWallet = userWallet,
|
||||||
|
|
@ -40,4 +39,21 @@ internal class DefaultTangemPayCurrencyFactory @Inject constructor(
|
||||||
decimals = TangemPayCurrencyFactory.TOKEN_DECIMALS,
|
decimals = TangemPayCurrencyFactory.TOKEN_DECIMALS,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun createVirtualAccountToken(userWalletId: UserWalletId): CryptoCurrency.Token {
|
||||||
|
val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)
|
||||||
|
val network = networkFactory.create(
|
||||||
|
blockchain = VisaUtilities.visaBlockchain,
|
||||||
|
derivationPath = Network.DerivationPath.Custom(VisaUtilities.virtualAccountDerivationPath.rawPath),
|
||||||
|
userWallet = userWallet,
|
||||||
|
)
|
||||||
|
return cryptoCurrencyFactory.createToken(
|
||||||
|
network = requireNotNull(network),
|
||||||
|
rawId = TangemPayCurrencyFactory.TOKEN_ID,
|
||||||
|
name = TangemPayCurrencyFactory.TOKEN_NAME,
|
||||||
|
symbol = TangemPayCurrencyFactory.TOKEN_NAME,
|
||||||
|
contractAddress = TangemPayCurrencyFactory.TOKEN_CONTRACT_ADDRESS,
|
||||||
|
decimals = TangemPayCurrencyFactory.TOKEN_DECIMALS,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,19 +1,41 @@
|
||||||
package com.tangem.data.virtualaccount.flow
|
package com.tangem.data.virtualaccount.flow
|
||||||
|
|
||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
|
import arrow.core.left
|
||||||
|
import arrow.core.right
|
||||||
|
import com.tangem.data.common.network.NetworkFactory
|
||||||
import com.tangem.data.virtualaccount.store.VirtualAccountStatusesStore
|
import com.tangem.data.virtualaccount.store.VirtualAccountStatusesStore
|
||||||
|
import com.tangem.domain.card.common.visa.VisaUtilities
|
||||||
|
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||||
|
import com.tangem.domain.common.wallets.getSyncStrict
|
||||||
import com.tangem.domain.core.utils.catchOn
|
import com.tangem.domain.core.utils.catchOn
|
||||||
import com.tangem.domain.models.StatusSource
|
import com.tangem.domain.models.StatusSource
|
||||||
import com.tangem.domain.models.account.Account
|
import com.tangem.domain.models.account.Account
|
||||||
import com.tangem.domain.models.account.AccountStatus
|
import com.tangem.domain.models.account.AccountStatus
|
||||||
import com.tangem.domain.models.account.VirtualAccountStatusValue
|
import com.tangem.domain.models.account.VirtualAccountStatusValue
|
||||||
|
import com.tangem.domain.models.network.Network
|
||||||
|
import com.tangem.domain.models.network.NetworkStatus
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
|
||||||
|
import com.tangem.domain.networks.single.SingleNetworkStatusProducer
|
||||||
|
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
|
||||||
|
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
||||||
import com.tangem.domain.virtualaccount.flow.VirtualAccountStatusFetcher
|
import com.tangem.domain.virtualaccount.flow.VirtualAccountStatusFetcher
|
||||||
|
import com.tangem.domain.wallets.extension.hasDerivation
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
import com.tangem.utils.logging.TangemLogger
|
||||||
|
import java.math.BigDecimal
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@Suppress("LongParameterList")
|
||||||
internal class DefaultVirtualAccountStatusFetcher @Inject constructor(
|
internal class DefaultVirtualAccountStatusFetcher @Inject constructor(
|
||||||
private val virtualAccountStatusesStore: VirtualAccountStatusesStore,
|
private val virtualAccountStatusesStore: VirtualAccountStatusesStore,
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
|
private val networkFactory: NetworkFactory,
|
||||||
|
private val tangemPayCurrencyFactory: TangemPayCurrencyFactory,
|
||||||
|
private val userWalletsListRepository: UserWalletsListRepository,
|
||||||
|
private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
|
||||||
|
private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
|
||||||
) : VirtualAccountStatusFetcher {
|
) : VirtualAccountStatusFetcher {
|
||||||
|
|
||||||
override suspend fun invoke(params: VirtualAccountStatusFetcher.Params) = Either.catchOn(dispatchers.default) {
|
override suspend fun invoke(params: VirtualAccountStatusFetcher.Params) = Either.catchOn(dispatchers.default) {
|
||||||
|
|
@ -21,6 +43,7 @@ internal class DefaultVirtualAccountStatusFetcher @Inject constructor(
|
||||||
// TODO([REDACTED_TASK_KEY]): Replace with the real VA status fetch (provisioning state, balance and banking
|
// TODO([REDACTED_TASK_KEY]): Replace with the real VA status fetch (provisioning state, balance and banking
|
||||||
// details) from the backend once Virtual Account status endpoints are available. Until then the
|
// details) from the backend once Virtual Account status endpoints are available. Until then the
|
||||||
// account is surfaced as NotCreated so the entity flows through the app end-to-end.
|
// account is surfaced as NotCreated so the entity flows through the app end-to-end.
|
||||||
|
getBalance(params.userWalletId)
|
||||||
virtualAccountStatusesStore.store(
|
virtualAccountStatusesStore.store(
|
||||||
userWalletId = params.userWalletId,
|
userWalletId = params.userWalletId,
|
||||||
status = AccountStatus.Virtual(account = account, value = VirtualAccountStatusValue.NotCreated),
|
status = AccountStatus.Virtual(account = account, value = VirtualAccountStatusValue.NotCreated),
|
||||||
|
|
@ -31,4 +54,55 @@ internal class DefaultVirtualAccountStatusFetcher @Inject constructor(
|
||||||
source = StatusSource.ONLY_CACHE,
|
source = StatusSource.ONLY_CACHE,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun getBalance(userWalletId: UserWalletId): Either<VirtualAccountStatusValue, BigDecimal> {
|
||||||
|
val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)
|
||||||
|
|
||||||
|
val hasVirtualAccountDerivation = userWallet.hasDerivation(
|
||||||
|
blockchain = VisaUtilities.visaBlockchain,
|
||||||
|
derivationPath = VisaUtilities.virtualAccountDerivationPath.rawPath,
|
||||||
|
)
|
||||||
|
if (!hasVirtualAccountDerivation) {
|
||||||
|
// TODO: Doston(VA) Derive will be implemented in [REDACTED_TASK_KEY]
|
||||||
|
TangemLogger.withTag(TAG).d("Virtual account is not derived")
|
||||||
|
return VirtualAccountStatusValue.Error.NotSynced.left()
|
||||||
|
}
|
||||||
|
|
||||||
|
val network = networkFactory.create(
|
||||||
|
blockchain = VisaUtilities.visaBlockchain,
|
||||||
|
derivationPath =
|
||||||
|
Network.DerivationPath.Custom(VisaUtilities.virtualAccountDerivationPath.rawPath),
|
||||||
|
userWallet = userWallet,
|
||||||
|
)
|
||||||
|
if (network == null) {
|
||||||
|
TangemLogger.withTag(TAG).d("Can not create network for Virtual account")
|
||||||
|
return VirtualAccountStatusValue.Error.Unavailable.left()
|
||||||
|
}
|
||||||
|
val token = tangemPayCurrencyFactory.createVirtualAccountToken(userWalletId)
|
||||||
|
|
||||||
|
singleNetworkStatusFetcher(
|
||||||
|
SingleNetworkStatusFetcher.Params(
|
||||||
|
userWalletId = userWalletId,
|
||||||
|
network = network,
|
||||||
|
extraTokens = setOf(token),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val verifiedStatus = singleNetworkStatusSupplier
|
||||||
|
.getSyncOrNull(SingleNetworkStatusProducer.Params(userWalletId, network))
|
||||||
|
?.value as? NetworkStatus.Verified
|
||||||
|
val balance = (verifiedStatus?.amounts?.get(token.id) as? NetworkStatus.Amount.Loaded)?.value
|
||||||
|
|
||||||
|
return if (balance != null) {
|
||||||
|
TangemLogger.withTag(TAG).d("VA on-chain balance = $balance")
|
||||||
|
balance.right()
|
||||||
|
} else {
|
||||||
|
TangemLogger.withTag(TAG).d("Can not get VA balance")
|
||||||
|
VirtualAccountStatusValue.Error.Unavailable.left()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
private const val TAG = "VirtualAccountStatusFetcher"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
package com.tangem.data.virtualaccount.flow
|
||||||
|
|
||||||
|
import arrow.core.right
|
||||||
|
import com.tangem.blockchain.common.Blockchain
|
||||||
|
import com.tangem.data.common.network.NetworkFactory
|
||||||
|
import com.tangem.data.virtualaccount.store.VirtualAccountStatusesStore
|
||||||
|
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
|
import com.tangem.domain.models.network.Network
|
||||||
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
|
||||||
|
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
|
||||||
|
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
||||||
|
import com.tangem.domain.virtualaccount.flow.VirtualAccountStatusFetcher
|
||||||
|
import com.tangem.domain.wallets.extension.hasDerivation
|
||||||
|
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||||
|
import io.mockk.clearMocks
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.unmockkStatic
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.AfterEach
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
private const val USER_WALLET_EXTENSIONS = "com.tangem.domain.wallets.extension.UserWalletExtensionsKt"
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
internal class DefaultVirtualAccountStatusFetcherTest {
|
||||||
|
|
||||||
|
private val virtualAccountStatusesStore: VirtualAccountStatusesStore = mockk(relaxed = true)
|
||||||
|
private val dispatchers = TestingCoroutineDispatcherProvider()
|
||||||
|
private val networkFactory: NetworkFactory = mockk()
|
||||||
|
private val tangemPayCurrencyFactory: TangemPayCurrencyFactory = mockk()
|
||||||
|
private val userWalletsListRepository: UserWalletsListRepository = mockk()
|
||||||
|
private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher = mockk()
|
||||||
|
private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier = mockk(relaxed = true)
|
||||||
|
|
||||||
|
private val fetcher = DefaultVirtualAccountStatusFetcher(
|
||||||
|
virtualAccountStatusesStore = virtualAccountStatusesStore,
|
||||||
|
dispatchers = dispatchers,
|
||||||
|
networkFactory = networkFactory,
|
||||||
|
tangemPayCurrencyFactory = tangemPayCurrencyFactory,
|
||||||
|
userWalletsListRepository = userWalletsListRepository,
|
||||||
|
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
|
||||||
|
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val userWalletId = UserWalletId("011")
|
||||||
|
private val userWallet: UserWallet = mockk { every { walletId } returns userWalletId }
|
||||||
|
private val network: Network = mockk()
|
||||||
|
private val token: CryptoCurrency.Token = mockk()
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setUp() {
|
||||||
|
mockkStatic(USER_WALLET_EXTENSIONS)
|
||||||
|
clearMocks(networkFactory, tangemPayCurrencyFactory, singleNetworkStatusFetcher, userWalletsListRepository)
|
||||||
|
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(userWallet))
|
||||||
|
every {
|
||||||
|
networkFactory.create(any<Blockchain>(), any<Network.DerivationPath>(), any<UserWallet>())
|
||||||
|
} returns network
|
||||||
|
every { tangemPayCurrencyFactory.createVirtualAccountToken(userWalletId) } returns token
|
||||||
|
coEvery { singleNetworkStatusFetcher(any()) } returns Unit.right()
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
fun tearDown() {
|
||||||
|
unmockkStatic(USER_WALLET_EXTENSIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN VA derivation missing WHEN invoke THEN on-chain fetch skipped`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
every { userWallet.hasDerivation(any(), any()) } returns false
|
||||||
|
|
||||||
|
// Act
|
||||||
|
fetcher.invoke(VirtualAccountStatusFetcher.Params(userWalletId))
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
coVerify(exactly = 0) { singleNetworkStatusFetcher(any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN VA derivation present WHEN invoke THEN on-chain fetch performed`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
every { userWallet.hasDerivation(any(), any()) } returns true
|
||||||
|
|
||||||
|
// Act
|
||||||
|
fetcher.invoke(VirtualAccountStatusFetcher.Params(userWalletId))
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
coVerify(exactly = 1) { singleNetworkStatusFetcher(any()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,7 @@ object VisaUtilities {
|
||||||
val visaDefaultDerivationPath
|
val visaDefaultDerivationPath
|
||||||
get() = visaBlockchain.derivationPath(DerivationStyle.V3)
|
get() = visaBlockchain.derivationPath(DerivationStyle.V3)
|
||||||
val customDerivationPath = DerivationPath("m/44'/60'/999999'/0/0")
|
val customDerivationPath = DerivationPath("m/44'/60'/999999'/0/0")
|
||||||
|
val virtualAccountDerivationPath = DerivationPath("m/44'/60'/999998'/0/0")
|
||||||
val curve = EllipticCurve.Secp256k1
|
val curve = EllipticCurve.Secp256k1
|
||||||
|
|
||||||
fun signWithNonceMessage(nonce: String): String {
|
fun signWithNonceMessage(nonce: String): String {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.tangem.domain.networks.multi
|
package com.tangem.domain.networks.multi
|
||||||
|
|
||||||
import com.tangem.domain.core.flow.FlowFetcher
|
import com.tangem.domain.core.flow.FlowFetcher
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.network.Network
|
import com.tangem.domain.models.network.Network
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
|
||||||
|
|
@ -11,5 +12,16 @@ import com.tangem.domain.models.wallet.UserWalletId
|
||||||
*/
|
*/
|
||||||
interface MultiNetworkStatusFetcher : FlowFetcher<MultiNetworkStatusFetcher.Params> {
|
interface MultiNetworkStatusFetcher : FlowFetcher<MultiNetworkStatusFetcher.Params> {
|
||||||
|
|
||||||
data class Params(val userWalletId: UserWalletId, val networks: Set<Network>)
|
/**
|
||||||
|
* Params
|
||||||
|
*
|
||||||
|
* @property userWalletId user wallet id
|
||||||
|
* @property networks networks whose statuses are fetched
|
||||||
|
* @property extraTokens additional tokens to fetch balances for, beyond the wallet's added currencies
|
||||||
|
*/
|
||||||
|
data class Params(
|
||||||
|
val userWalletId: UserWalletId,
|
||||||
|
val networks: Set<Network>,
|
||||||
|
val extraTokens: Set<CryptoCurrency.Token> = emptySet(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.tangem.domain.networks.single
|
package com.tangem.domain.networks.single
|
||||||
|
|
||||||
import com.tangem.domain.core.flow.FlowFetcher
|
import com.tangem.domain.core.flow.FlowFetcher
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.network.Network
|
import com.tangem.domain.models.network.Network
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
|
||||||
|
|
@ -15,7 +16,12 @@ interface SingleNetworkStatusFetcher : FlowFetcher<SingleNetworkStatusFetcher.Pa
|
||||||
* Params
|
* Params
|
||||||
*
|
*
|
||||||
* @property userWalletId user wallet id
|
* @property userWalletId user wallet id
|
||||||
* @property network network
|
* @property network network whose status is fetched
|
||||||
|
* @property extraTokens additional tokens to fetch balances for, beyond the wallet's added currencies
|
||||||
*/
|
*/
|
||||||
data class Params(val userWalletId: UserWalletId, val network: Network)
|
data class Params(
|
||||||
|
val userWalletId: UserWalletId,
|
||||||
|
val network: Network,
|
||||||
|
val extraTokens: Set<CryptoCurrency.Token> = emptySet(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -17,6 +17,7 @@ interface TangemPayCurrencyFactory {
|
||||||
* @throws IllegalStateException if no wallet with [userWalletId] is currently loaded.
|
* @throws IllegalStateException if no wallet with [userWalletId] is currently loaded.
|
||||||
*/
|
*/
|
||||||
fun create(userWalletId: UserWalletId): CryptoCurrency.Token
|
fun create(userWalletId: UserWalletId): CryptoCurrency.Token
|
||||||
|
fun createVirtualAccountToken(userWalletId: UserWalletId): CryptoCurrency.Token
|
||||||
|
|
||||||
/** Hardcoded token metadata for the Tangem Pay currency (USDC on Polygon). */
|
/** Hardcoded token metadata for the Tangem Pay currency (USDC on Polygon). */
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue