From 72b2d87cb4728e31e2d44628aa2d0d2e6ff292d1 Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:50:05 +0300 Subject: [PATCH 1/3] Open the asset behind a perpetual market A perpetual market shows the asset it tracks under Info (Bitcoin on BTC, PancakeSwap on BNB Chain on CAKE); tapping it opens the asset screen. The market refresh stores the asset's official association through update_asset, and both apps read it from storage with AssociatedAssetsQuery, so the row shows offline and updates when the refresh saves it. --- .../android/ui/navigation/WalletNavGraph.kt | 1 + .../ui/navigation/routes/Perpetuals.kt | 2 + .../services/gemstone/di/PerpetualModule.kt | 3 +- .../data/services/store/database/AssetsDao.kt | 3 + .../store/queries/AssociatedAssetsQuery.kt | 24 +++++++ .../integration/AssociatedAssetsQueryTest.kt | 64 +++++++++++++++++++ .../presents/perpetual/PerpetualAction.kt | 2 + .../presents/perpetual/PerpetualScene.kt | 15 +++++ .../presents/perpetual/PerpetualScreen.kt | 5 ++ .../viewmodels/PerpetualViewModel.kt | 10 +++ .../viewmodels/PerpetualViewModelTest.kt | 5 ++ core/gemstone/src/services/assets/rules.rs | 2 +- .../src/services/perpetual/details.rs | 21 +++++- core/gemstone/src/services/perpetual/model.rs | 40 +++++++++++- .../src/services/perpetual/testkit.rs | 5 +- docs/product/perpetuals.md | 3 +- .../Sources/Scenes/PerpetualScene.swift | 10 +++ .../ViewModels/PerpetualSceneViewModel.swift | 11 ++++ .../Perpetuals/PerpetualNavigationView.swift | 2 +- .../ViewModelFactory+Perpetuals.swift | 2 +- .../Queries/AssociatedAssetsQuery.swift | 29 +++++++++ .../Queries/AssociatedAssetsQueryTests.swift | 29 +++++++++ 22 files changed, 278 insertions(+), 10 deletions(-) create mode 100644 android/data/services/store/src/main/kotlin/com/gemwallet/android/data/services/store/queries/AssociatedAssetsQuery.kt create mode 100644 android/data/services/store/src/test/kotlin/com/gemwallet/android/data/services/store/integration/AssociatedAssetsQueryTest.kt create mode 100644 ios/Packages/Store/Sources/Queries/AssociatedAssetsQuery.swift create mode 100644 ios/Packages/Store/Tests/StoreTests/Queries/AssociatedAssetsQueryTests.swift diff --git a/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/WalletNavGraph.kt b/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/WalletNavGraph.kt index bb84a65489..caab4ba46e 100644 --- a/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/WalletNavGraph.kt +++ b/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/WalletNavGraph.kt @@ -326,6 +326,7 @@ fun WalletNavGraph( perpetualsScreen( onOpenPerpetual = navigator::openPerpetual, + onOpenAsset = navigator::openAsset, onOpenPortfolio = { navigator.openPortfolio(PortfolioType.Perpetuals) }, amountAction = AmountTransactionAction(navigator::openAmount), onSelectDepositAsset = navigator::openPerpetualDepositSelect, diff --git a/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/routes/Perpetuals.kt b/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/routes/Perpetuals.kt index abf019a37c..e2c5453f00 100644 --- a/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/routes/Perpetuals.kt +++ b/android/app/src/main/kotlin/com/gemwallet/android/ui/navigation/routes/Perpetuals.kt @@ -29,6 +29,7 @@ data object PerpetualDepositSelectRoute : NavKey fun EntryProviderScope.perpetualsScreen( onCancel: () -> Unit, onOpenPerpetual: AssetIdAction, + onOpenAsset: AssetIdAction, onOpenPortfolio: () -> Unit, amountAction: AmountTransactionAction, onSelectDepositAsset: () -> Unit, @@ -61,6 +62,7 @@ fun EntryProviderScope.perpetualsScreen( confirmAction = confirmAction, onClose = onCancel, onTransaction = onTransaction, + onOpenAsset = onOpenAsset, confirmContent = { input, finishAction, cancelAction, onOpenAddress -> ConfirmTransferScreen( input = input, diff --git a/android/data/services/gemstone/src/main/kotlin/com/gemwallet/android/data/services/gemstone/di/PerpetualModule.kt b/android/data/services/gemstone/src/main/kotlin/com/gemwallet/android/data/services/gemstone/di/PerpetualModule.kt index d1e7451c32..3d4e073564 100644 --- a/android/data/services/gemstone/src/main/kotlin/com/gemwallet/android/data/services/gemstone/di/PerpetualModule.kt +++ b/android/data/services/gemstone/src/main/kotlin/com/gemwallet/android/data/services/gemstone/di/PerpetualModule.kt @@ -80,9 +80,10 @@ object PerpetualModule { fun provideGemPerpetualDetailsService( perpetualService: GemPerpetualService, transactionsService: GemTransactionsService, + assetsService: GemAssetsService, preferencesService: GemPreferencesService, walletSessionService: GemWalletSessionService, - ): GemPerpetualDetailsServiceInterface = GemPerpetualDetailsService(perpetualService, transactionsService, preferencesService, walletSessionService) + ): GemPerpetualDetailsServiceInterface = GemPerpetualDetailsService(perpetualService, transactionsService, assetsService, preferencesService, walletSessionService) @Provides @Singleton diff --git a/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/services/store/database/AssetsDao.kt b/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/services/store/database/AssetsDao.kt index e893abf201..7e653a55ff 100644 --- a/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/services/store/database/AssetsDao.kt +++ b/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/services/store/database/AssetsDao.kt @@ -178,6 +178,9 @@ interface AssetsDao { @Query("SELECT * FROM asset WHERE id IN (:ids)") suspend fun getAssetsByIds(ids: List): List + @Query("SELECT * FROM asset WHERE id IN (:ids)") + fun getAssets(ids: List): Flow> + @Query("SELECT id FROM asset WHERE id IN (:ids)") suspend fun getAssetIds(ids: List): List diff --git a/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/services/store/queries/AssociatedAssetsQuery.kt b/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/services/store/queries/AssociatedAssetsQuery.kt new file mode 100644 index 0000000000..a226e4631b --- /dev/null +++ b/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/services/store/queries/AssociatedAssetsQuery.kt @@ -0,0 +1,24 @@ +package com.gemwallet.android.data.services.store.queries + +import com.gemwallet.android.data.services.store.database.AssetsDao +import com.gemwallet.android.data.services.store.database.entities.toDTO +import com.gemwallet.android.ext.toIdentifier +import com.wallet.core.primitives.Asset +import com.wallet.core.primitives.AssetAssociationType +import com.wallet.core.primitives.AssetId +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@OptIn(ExperimentalCoroutinesApi::class) +class AssociatedAssetsQuery @Inject constructor(private val assetsDao: AssetsDao) { + + operator fun invoke(assetId: AssetId, type: AssetAssociationType): Flow> = assetsDao.getAsset(assetId.toIdentifier()) + .map { asset -> asset?.associations.orEmpty().filter { it.type == type }.map { it.assetId.toIdentifier() } } + .distinctUntilChanged() + .flatMapLatest { assetIds -> assetsDao.getAssets(assetIds) } + .map { assets -> assets.mapNotNull { it.toDTO() } } +} diff --git a/android/data/services/store/src/test/kotlin/com/gemwallet/android/data/services/store/integration/AssociatedAssetsQueryTest.kt b/android/data/services/store/src/test/kotlin/com/gemwallet/android/data/services/store/integration/AssociatedAssetsQueryTest.kt new file mode 100644 index 0000000000..231d03304b --- /dev/null +++ b/android/data/services/store/src/test/kotlin/com/gemwallet/android/data/services/store/integration/AssociatedAssetsQueryTest.kt @@ -0,0 +1,64 @@ +package com.gemwallet.android.data.services.store.integration + +import androidx.room.Room +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.gemwallet.android.data.services.store.database.GemDatabase +import com.gemwallet.android.data.services.store.database.entities.DbAsset +import com.gemwallet.android.data.services.store.queries.AssociatedAssetsQuery +import com.gemwallet.android.ext.toIdentifier +import com.wallet.core.primitives.AssetAssociation +import com.wallet.core.primitives.AssetAssociationType +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.AssetType +import com.wallet.core.primitives.Chain +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class AssociatedAssetsQueryTest { + private lateinit var database: GemDatabase + private lateinit var query: AssociatedAssetsQuery + private val perpetual = AssetId(Chain.HyperCore, "perpetual::BTC") + private val bitcoin = AssetId(Chain.Bitcoin) + private val wrapped = AssetId(Chain.Ethereum, "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599") + + @Before + fun setUp() = runBlocking(Dispatchers.IO) { + database = Room.inMemoryDatabaseBuilder( + InstrumentationRegistry.getInstrumentation().targetContext, + GemDatabase::class.java, + ).build() + query = AssociatedAssetsQuery(database.assetsDao()) + database.assetsDao().insert( + listOf( + DbAsset( + id = perpetual.toIdentifier(), + chain = Chain.HyperCore, + name = "BTC", + symbol = "BTC", + decimals = 5, + type = AssetType.PERPETUAL, + associations = listOf(AssetAssociation(bitcoin, AssetAssociationType.Official), AssetAssociation(wrapped, AssetAssociationType.Wrapped)), + ), + DbAsset(id = bitcoin.toIdentifier(), chain = Chain.Bitcoin, name = "Bitcoin", symbol = "BTC", decimals = 8, type = AssetType.NATIVE), + DbAsset(id = wrapped.toIdentifier(), chain = Chain.Ethereum, name = "Wrapped BTC", symbol = "WBTC", decimals = 8, type = AssetType.ERC20), + ), + ) + } + + @After + fun tearDown() = database.close() + + @Test + fun assetsOfType() = runBlocking(Dispatchers.IO) { + assertEquals(listOf(bitcoin), query(perpetual, AssetAssociationType.Official).first().map { it.id }) + assertEquals(emptyList(), query(bitcoin, AssetAssociationType.Official).first().map { it.id }) + } +} diff --git a/android/features/perpetuals/presents/src/main/kotlin/com/gemwallet/android/features/perpetuals/presents/perpetual/PerpetualAction.kt b/android/features/perpetuals/presents/src/main/kotlin/com/gemwallet/android/features/perpetuals/presents/perpetual/PerpetualAction.kt index 63b3541ef4..5768ee8dd7 100644 --- a/android/features/perpetuals/presents/src/main/kotlin/com/gemwallet/android/features/perpetuals/presents/perpetual/PerpetualAction.kt +++ b/android/features/perpetuals/presents/src/main/kotlin/com/gemwallet/android/features/perpetuals/presents/perpetual/PerpetualAction.kt @@ -1,5 +1,6 @@ package com.gemwallet.android.features.perpetuals.presents.perpetual +import com.wallet.core.primitives.AssetId import com.wallet.core.primitives.ChartPeriod import com.wallet.core.primitives.PerpetualDirection import com.wallet.core.primitives.TransactionId @@ -17,4 +18,5 @@ internal sealed interface PerpetualAction { data class ZoomChart(val magnification: Float, val anchor: Float) : PerpetualAction data class PanChart(val fraction: Float) : PerpetualAction data class OpenTransaction(val transactionId: TransactionId) : PerpetualAction + data class OpenAsset(val assetId: AssetId) : PerpetualAction } diff --git a/android/features/perpetuals/presents/src/main/kotlin/com/gemwallet/android/features/perpetuals/presents/perpetual/PerpetualScene.kt b/android/features/perpetuals/presents/src/main/kotlin/com/gemwallet/android/features/perpetuals/presents/perpetual/PerpetualScene.kt index b0d1aa1da3..5502deef3a 100644 --- a/android/features/perpetuals/presents/src/main/kotlin/com/gemwallet/android/features/perpetuals/presents/perpetual/PerpetualScene.kt +++ b/android/features/perpetuals/presents/src/main/kotlin/com/gemwallet/android/features/perpetuals/presents/perpetual/PerpetualScene.kt @@ -7,13 +7,17 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import com.gemwallet.android.ext.toPrimitives import com.gemwallet.android.features.perpetuals.presents.components.PerpetualActions import com.gemwallet.android.features.perpetuals.presents.components.PerpetualChartSection import com.gemwallet.android.features.perpetuals.presents.components.PerpetualModifyBottomSheet import com.gemwallet.android.features.perpetuals.presents.components.positionProperties import com.gemwallet.android.ui.R +import com.gemwallet.android.ui.components.clickable +import com.gemwallet.android.ui.components.list_item.AssetListItem import com.gemwallet.android.ui.components.list_item.GemListRowView import com.gemwallet.android.ui.components.list_item.SubheaderItem +import com.gemwallet.android.ui.components.list_item.property.DataBadgeChevron import com.gemwallet.android.ui.components.list_item.property.itemsPositioned import com.gemwallet.android.ui.components.list_item.rememberDateSections import com.gemwallet.android.ui.components.list_item.transaction.transactionsList @@ -29,6 +33,7 @@ import uniffi.gemstone.GemInfoTopic import uniffi.gemstone.GemListRow import uniffi.gemstone.GemListRowTitle import uniffi.gemstone.GemLocalizedText +import uniffi.gemstone.GemPerpetualAssociatedAssetRow import uniffi.gemstone.GemPerpetualButton import uniffi.gemstone.GemPerpetualButtonRow import uniffi.gemstone.GemPerpetualDetails @@ -42,6 +47,7 @@ import uniffi.gemstone.GemValueTone internal fun PerpetualScene( details: GemPerpetualDetails?, transactions: List, + associatedAssets: List, chart: StateViewType, period: ChartPeriod, isRefreshing: Boolean, @@ -103,6 +109,14 @@ internal fun PerpetualScene( } } } + itemsPositioned(associatedAssets) { position, associatedAsset -> + AssetListItem( + row = associatedAsset.row, + listPosition = position, + modifier = Modifier.clickable { onAction(PerpetualAction.OpenAsset(associatedAsset.asset.toPrimitives().id)) }, + accessory = { DataBadgeChevron() }, + ) + } if (transactions.isNotEmpty()) { transactionsList(transactionSections) { onAction(PerpetualAction.OpenTransaction(it)) } } @@ -156,6 +170,7 @@ private fun PerpetualScenePreview() { positionRow = null, ), transactions = emptyList(), + associatedAssets = emptyList(), chart = StateViewType.Loading, period = ChartPeriod.Day, isRefreshing = false, diff --git a/android/features/perpetuals/presents/src/main/kotlin/com/gemwallet/android/features/perpetuals/presents/perpetual/PerpetualScreen.kt b/android/features/perpetuals/presents/src/main/kotlin/com/gemwallet/android/features/perpetuals/presents/perpetual/PerpetualScreen.kt index 609918eb14..6b3cfba621 100644 --- a/android/features/perpetuals/presents/src/main/kotlin/com/gemwallet/android/features/perpetuals/presents/perpetual/PerpetualScreen.kt +++ b/android/features/perpetuals/presents/src/main/kotlin/com/gemwallet/android/features/perpetuals/presents/perpetual/PerpetualScreen.kt @@ -19,6 +19,7 @@ import com.gemwallet.android.ui.components.screen.ModalBottomSheet import com.gemwallet.android.ui.components.screen.SheetExpansion import com.gemwallet.android.ui.components.screen.rememberSnackbarState import com.gemwallet.android.ui.models.actions.AmountTransactionAction +import com.gemwallet.android.ui.models.actions.AssetIdAction import com.gemwallet.android.ui.models.actions.ConfirmTransactionAction import com.gemwallet.android.ui.models.actions.FinishConfirmAction import com.wallet.core.primitives.TransactionId @@ -29,6 +30,7 @@ fun PerpetualScreen( confirmAction: ConfirmTransactionAction, onClose: () -> Unit, onTransaction: (TransactionId) -> Unit, + onOpenAsset: AssetIdAction, confirmContent: AutocloseConfirmContent, viewModel: PerpetualViewModel = hiltViewModel(), ) { @@ -46,6 +48,7 @@ fun PerpetualScreen( InfoBottomSheet(item = infoSheet?.infoSheet(), onClose = { viewModel.infoSheet.value = null }) val details by viewModel.details.collectAsStateWithLifecycle() val transactions by viewModel.transactions.collectAsStateWithLifecycle() + val associatedAssets by viewModel.associatedAssets.collectAsStateWithLifecycle() val chart by viewModel.chart.collectAsStateWithLifecycle() val period by viewModel.period.collectAsStateWithLifecycle() val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle() @@ -57,6 +60,7 @@ fun PerpetualScreen( PerpetualScene( details = details, transactions = transactions, + associatedAssets = associatedAssets, chart = chart, period = period, isRefreshing = isRefreshing, @@ -77,6 +81,7 @@ fun PerpetualScreen( is PerpetualAction.ZoomChart -> viewModel.onZoom(action.magnification, action.anchor) is PerpetualAction.PanChart -> viewModel.onPan(action.fraction) is PerpetualAction.OpenTransaction -> onTransaction(action.transactionId) + is PerpetualAction.OpenAsset -> onOpenAsset(action.assetId) } }, ) diff --git a/android/features/perpetuals/viewmodels/src/main/kotlin/com/gemwallet/android/features/perpetuals/viewmodels/PerpetualViewModel.kt b/android/features/perpetuals/viewmodels/src/main/kotlin/com/gemwallet/android/features/perpetuals/viewmodels/PerpetualViewModel.kt index 1fb81f6764..f76dd692bd 100644 --- a/android/features/perpetuals/viewmodels/src/main/kotlin/com/gemwallet/android/features/perpetuals/viewmodels/PerpetualViewModel.kt +++ b/android/features/perpetuals/viewmodels/src/main/kotlin/com/gemwallet/android/features/perpetuals/viewmodels/PerpetualViewModel.kt @@ -9,6 +9,7 @@ import com.gemwallet.android.application.IoDispatcher import com.gemwallet.android.application.perpetual.cases.PerpetualObserver import com.gemwallet.android.application.session.cases.GetSession import com.gemwallet.android.application.transactions.cases.GetTransactions +import com.gemwallet.android.data.services.store.queries.AssociatedAssetsQuery import com.gemwallet.android.data.services.store.queries.PerpetualPositionsQuery import com.gemwallet.android.data.services.store.queries.PerpetualQuery import com.gemwallet.android.domains.confirm.ConfirmTransferInput @@ -25,6 +26,7 @@ import com.gemwallet.android.ui.models.actions.AmountTransactionAction import com.gemwallet.android.ui.models.actions.ConfirmTransactionAction import com.gemwallet.android.ui.models.flatMap import com.gemwallet.android.ui.models.navigation.requireAssetId +import com.wallet.core.primitives.AssetAssociationType import com.wallet.core.primitives.ChartPeriod import com.wallet.core.primitives.PerpetualDirection import com.wallet.core.primitives.TransactionsFilter @@ -60,10 +62,12 @@ import kotlinx.coroutines.withContext import uniffi.gemstone.GemCandleChart import uniffi.gemstone.GemInfoTopic import uniffi.gemstone.GemLoadState +import uniffi.gemstone.GemPerpetualAssociatedAssetRow import uniffi.gemstone.GemPerpetualDetails import uniffi.gemstone.GemPerpetualDetailsServiceInterface import uniffi.gemstone.GemPerpetualPositionKind import uniffi.gemstone.candleSession +import uniffi.gemstone.perpetualAssociatedAssetRows import java.time.Instant import java.time.ZoneId import javax.inject.Inject @@ -73,6 +77,7 @@ import javax.inject.Inject class PerpetualViewModel @Inject constructor( private val perpetualQuery: PerpetualQuery, private val perpetualPositionsQuery: PerpetualPositionsQuery, + private val associatedAssetsQuery: AssociatedAssetsQuery, private val getTransactions: GetTransactions, private val perpetualObserver: PerpetualObserver, private val service: GemPerpetualDetailsServiceInterface, @@ -133,6 +138,11 @@ class PerpetualViewModel @Inject constructor( .flowOn(ioDispatcher) .stateIn(viewModelScope, SharingStarted.Eagerly, getTransactions.stored(transactionsFilter, GemConstants.transactionsListLimit)) + val associatedAssets: StateFlow> = associatedAssetsQuery(assetId, AssetAssociationType.Official) + .map { assets -> perpetualAssociatedAssetRows(assets.map { it.toGem() }) } + .flowOn(ioDispatcher) + .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) + private val candles = MutableStateFlow(candleSession(service.chartPeriod())) val period: StateFlow = candles.map { it.period.toPrimitives() } diff --git a/android/features/perpetuals/viewmodels/src/test/kotlin/com/gemwallet/android/features/perpetuals/viewmodels/PerpetualViewModelTest.kt b/android/features/perpetuals/viewmodels/src/test/kotlin/com/gemwallet/android/features/perpetuals/viewmodels/PerpetualViewModelTest.kt index 42d8e5f0a5..2cca8463c9 100644 --- a/android/features/perpetuals/viewmodels/src/test/kotlin/com/gemwallet/android/features/perpetuals/viewmodels/PerpetualViewModelTest.kt +++ b/android/features/perpetuals/viewmodels/src/test/kotlin/com/gemwallet/android/features/perpetuals/viewmodels/PerpetualViewModelTest.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import com.gemwallet.android.application.perpetual.cases.PerpetualObserver import com.gemwallet.android.application.session.cases.GetSession import com.gemwallet.android.application.transactions.cases.GetTransactions +import com.gemwallet.android.data.services.store.queries.AssociatedAssetsQuery import com.gemwallet.android.data.services.store.queries.PerpetualPositionsQuery import com.gemwallet.android.data.services.store.queries.PerpetualQuery import com.gemwallet.android.ext.toIdentifier @@ -97,9 +98,13 @@ class PerpetualViewModelTest { every { getTransactions(any(), any()) } returns emptyFlow() every { stored(any(), any()) } returns emptyList() } + val associatedAssets: AssociatedAssetsQuery = mockk { + every { this@mockk(any(), any()) } returns flowOf(emptyList()) + } return PerpetualViewModel( perpetual, positions, + associatedAssets, transactions, observer, service, diff --git a/core/gemstone/src/services/assets/rules.rs b/core/gemstone/src/services/assets/rules.rs index 245363b2ba..7c50fff803 100644 --- a/core/gemstone/src/services/assets/rules.rs +++ b/core/gemstone/src/services/assets/rules.rs @@ -238,7 +238,7 @@ pub fn asset_text(asset: &Asset) -> GemAssetText { } } -fn asset_row_text(asset: &Asset, style: GemAssetRowStyle) -> GemAssetRowText { +pub fn asset_row_text(asset: &Asset, style: GemAssetRowStyle) -> GemAssetRowText { let chain_asset = ChainAsset::from_chain(asset.chain()); let title = match style.title { GemAssetTitleStyle::Asset => asset.name.clone(), diff --git a/core/gemstone/src/services/perpetual/details.rs b/core/gemstone/src/services/perpetual/details.rs index 9ee3f5b754..8e1c7438f5 100644 --- a/core/gemstone/src/services/perpetual/details.rs +++ b/core/gemstone/src/services/perpetual/details.rs @@ -7,6 +7,7 @@ use super::model::{GemPerpetualDetails, GemPerpetualPositionAction, GemPerpetual use super::{GemPerpetualService, rules}; use crate::models::perpetual::GemPerpetualSubscription; use crate::models::state::GemLoadState; +use crate::services::assets::GemAssetsService; use crate::services::error::GemServiceError; use crate::services::failures::record_result; use crate::services::preferences::GemPreferencesService; @@ -18,6 +19,7 @@ use crate::services::wallet_session::GemWalletSessionService; pub struct GemPerpetualDetailsService { perpetuals: Arc, transactions: Arc, + assets: Arc, preferences: Arc, session: Arc, } @@ -25,10 +27,11 @@ pub struct GemPerpetualDetailsService { #[uniffi::export] impl GemPerpetualDetailsService { #[uniffi::constructor] - pub fn new(perpetuals: Arc, transactions: Arc, preferences: Arc, session: Arc) -> Self { + pub fn new(perpetuals: Arc, transactions: Arc, assets: Arc, preferences: Arc, session: Arc) -> Self { Self { perpetuals, transactions, + assets, preferences, session, } @@ -77,9 +80,10 @@ impl GemPerpetualDetailsService { pub async fn refresh(&self, asset_id: AssetId) -> Vec { let mut failures = Vec::new(); - let (positions, transactions) = futures::join!(self.sync_positions(), self.sync_transactions(asset_id)); + let (positions, transactions, asset) = futures::join!(self.sync_positions(), self.sync_transactions(asset_id.clone()), self.assets.update_asset(asset_id)); record_result(&mut failures, GemPerpetualRefreshStep::Positions, positions); record_result(&mut failures, GemPerpetualRefreshStep::Transactions, transactions); + record_result(&mut failures, GemPerpetualRefreshStep::Asset, asset); failures } } @@ -103,6 +107,7 @@ impl GemPerpetualDetailsService { #[cfg(test)] mod tests { use futures::executor::block_on; + use gem_hypercore::models::metadata::perpetual_asset_id; use primitives::Chain; use super::super::testkit::PerpetualTestkit; @@ -122,6 +127,18 @@ mod tests { }) } + #[test] + fn test_refresh() { + block_on(async { + let testkit = PerpetualTestkit::new().details_service(); + + testkit.service.refresh(perpetual_asset_id("CAKE")).await; + + let asset_paths: Vec = testkit.provider.requested_paths().into_iter().filter(|path| path.starts_with("/v1/assets/")).collect(); + assert_eq!(asset_paths, vec!["/v1/assets/hypercore_perpetual::CAKE"]); + }) + } + #[test] fn test_a_candle_load_remembers_the_period_it_was_asked_for() { block_on(async { diff --git a/core/gemstone/src/services/perpetual/model.rs b/core/gemstone/src/services/perpetual/model.rs index 411914130e..3d6c844685 100644 --- a/core/gemstone/src/services/perpetual/model.rs +++ b/core/gemstone/src/services/perpetual/model.rs @@ -3,7 +3,9 @@ use crate::formatted_number::{GemFormattedNumber, GemValueTone}; use crate::models::custom_types::GemBigInt; use crate::models::list::{GemListRow, GemListSection}; use crate::models::state::GemListPhase; -use crate::services::assets::model::{GemAssetItemRow, GemAssetItemTrailing, GemPriceRow, GemRowText, GemValueHeader}; +use crate::services::assets::icon::asset_icon; +use crate::services::assets::model::{GemAssetItemRow, GemAssetItemTrailing, GemAssetRowStyle, GemAssetSubtitleStyle, GemAssetTitleStyle, GemAssetTrailingStyle, GemPriceRow, GemRowText, GemValueHeader}; +use crate::services::assets::rules::asset_row_text; use crate::services::chart::candlestick_header; use crate::services::chart::model::{GemChartDateStyle, GemChartHeader, GemChartSelection}; use crate::services::chart::rules as chart_rules; @@ -95,6 +97,36 @@ pub struct GemPerpetualPositionRow { pub row: GemAssetItemRow, } +#[derive(Debug, Clone, PartialEq, uniffi::Record)] +pub struct GemPerpetualAssociatedAssetRow { + pub asset: Asset, + pub row: GemAssetItemRow, +} + +impl From for GemPerpetualAssociatedAssetRow { + fn from(asset: Asset) -> Self { + let style = GemAssetRowStyle { + title: GemAssetTitleStyle::Asset, + shows_symbol: true, + subtitle: GemAssetSubtitleStyle::Network, + trailing: GemAssetTrailingStyle::None, + }; + let text = asset_row_text(&asset, style); + Self { + row: GemAssetItemRow { + icon: asset_icon(&asset.id), + title: text.title, + title_extra: text.symbol, + subtitle: text.network.map(|network| GemRowText::neutral(GemLocalizedText::Text { text: network })), + subtitle_extra: None, + trailing: GemAssetItemTrailing::None, + masks_balance: false, + }, + asset, + } + } +} + #[derive(Debug, Clone, PartialEq)] pub struct PerpetualPositionLine { pub id: String, @@ -163,6 +195,11 @@ pub fn perpetual_position_rows(positions: Vec) -> Vec) -> Vec { + assets.into_iter().map(GemPerpetualAssociatedAssetRow::from).collect() +} + #[derive(Debug, Clone, PartialEq)] pub struct PerpetualMarketLine { pub asset_id: AssetId, @@ -335,6 +372,7 @@ pub enum GemPerpetualRefreshStep { Markets, DepositBalances, Transactions, + Asset, } #[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)] diff --git a/core/gemstone/src/services/perpetual/testkit.rs b/core/gemstone/src/services/perpetual/testkit.rs index 3dc96162bb..60af120209 100644 --- a/core/gemstone/src/services/perpetual/testkit.rs +++ b/core/gemstone/src/services/perpetual/testkit.rs @@ -203,9 +203,10 @@ impl PerpetualTestkit { pub fn details_service(self) -> DetailsTestkit { let device_api = Arc::new(GemDeviceApiClient::new(self.provider.clone(), Arc::new(GemDeviceKeyService::new(Arc::new(EmptyPreferences))))); + let assets = Arc::new(GemAssetsService::mock(self.provider.clone(), self.asset_store.clone())); let transactions = Arc::new(GemTransactionsService::new( device_api.clone(), - Arc::new(GemAssetsService::mock(self.provider.clone(), self.asset_store.clone())), + assets.clone(), self.balance.clone(), Arc::new(MemoryTransactionStateStore::default()), Arc::new(GemNameService::new(device_api, Arc::new(MemoryAddressStore::default()))), @@ -213,7 +214,7 @@ impl PerpetualTestkit { self.session.clone(), Arc::new(RecordingTransactionStatus::default()), )); - let service = GemPerpetualDetailsService::new(Arc::new(self.service), transactions, self.preferences.clone(), self.session.clone()); + let service = GemPerpetualDetailsService::new(Arc::new(self.service), transactions, assets, self.preferences.clone(), self.session.clone()); DetailsTestkit { service, session: self.session, diff --git a/docs/product/perpetuals.md b/docs/product/perpetuals.md index 9196a9bc92..c13171867d 100644 --- a/docs/product/perpetuals.md +++ b/docs/product/perpetuals.md @@ -10,7 +10,7 @@ flowchart LR 1. The user switches Perpetuals on in Settings or from the "Trade Perpetuals on Hyperliquid" banner; the wallet screen gains a Perpetuals section and the Portfolio a Perpetuals view. 2. The Perpetuals list shows the balance with Deposit and Withdraw, then Positions, Pinned and Markets; prices and their 24h change move live. -3. A market shows a candlestick chart with a period picker, Long and Short, Volume, Open Interest and Funding APR, and its activity. +3. A market shows a candlestick chart with a period picker, Long and Short, Volume, Open Interest and Funding APR, the asset it tracks, and its activity. 4. Long or Short takes the USDC margin, a leverage from `1x` to the market's cap, and an Auto Close prefilled from the default take profit and stop loss. 5. Confirm shows the position ("Long 5x"), the size, the price with `2%` slippage, and the take profit and stop loss prices. 6. After a position is opened, closed, increased, reduced or modified, a message confirms what was done ("Open Long", "Close position"). @@ -55,6 +55,7 @@ flowchart LR | The user taps Deposit on a unified Hyperliquid account | straight to the Arbitrum USDC amount | the spot USDC already is the perpetual balance | | The user taps Deposit with no USDC to deposit | straight to the Arbitrum USDC amount, which shows the zero balance | the Arbitrum USDC deposits on every account | | A perpetual is opened from search, recents, a transaction, a notification or a link | its market screen | one rule decides which screen an asset opens, for both apps | +| A market tracks an asset Gem lists as its official association (Bitcoin, CAKE on BNB Chain) | the asset shows under the market info, offline too, and opens its asset screen; a market without one shows nothing there; the link is checked at most once an hour | the user can buy, swap or receive the asset itself, which a perpetual cannot offer | Pinching and touching the chart follow the shared [Charts](../PRODUCT.md#charts) section. diff --git a/ios/Features/Perpetuals/Sources/Scenes/PerpetualScene.swift b/ios/Features/Perpetuals/Sources/Scenes/PerpetualScene.swift index 571a902a84..d541f3cee1 100644 --- a/ios/Features/Perpetuals/Sources/Scenes/PerpetualScene.swift +++ b/ios/Features/Perpetuals/Sources/Scenes/PerpetualScene.swift @@ -53,6 +53,16 @@ public struct PerpetualScene: View { } } + if !model.associatedAssets.isEmpty { + Section { + ForEach(model.associatedAssets, id: \.asset.id) { associatedAsset in + NavigationLink(value: Scenes.Asset(asset: associatedAsset.asset.toPrimitives())) { + ListAssetItemView(row: associatedAsset.row) + } + } + } + } + if !model.transactionSections.isEmpty { TransactionsList(sections: model.transactionSections) .listRowInsets(.assetListRowInsets) diff --git a/ios/Features/Perpetuals/Sources/ViewModels/PerpetualSceneViewModel.swift b/ios/Features/Perpetuals/Sources/ViewModels/PerpetualSceneViewModel.swift index e4cdc9df31..7f90fb1fa8 100644 --- a/ios/Features/Perpetuals/Sources/ViewModels/PerpetualSceneViewModel.swift +++ b/ios/Features/Perpetuals/Sources/ViewModels/PerpetualSceneViewModel.swift @@ -3,12 +3,14 @@ import Components import Foundation import enum Gemstone.GemInfoTopic +import struct Gemstone.GemPerpetualAssociatedAssetRow import enum Gemstone.GemPerpetualButton import struct Gemstone.GemPerpetualDetails import protocol Gemstone.GemPerpetualDetailsServiceProtocol import enum Gemstone.GemPerpetualPositionAction import enum Gemstone.GemPerpetualPositionKind import struct Gemstone.GemTransactionRow +import func Gemstone.perpetualAssociatedAssetRows import GemstonePrimitives import GemstoneServices import InfoSheet @@ -31,6 +33,7 @@ public final class PerpetualSceneViewModel { public let positionsQuery: ObservableQuery public let perpetualQuery: ObservableQuery public let transactionsQuery: ObservableQuery]>> + public let associatedAssetsQuery: ObservableQuery> public var positions: [PerpetualPositionData] { positionsQuery.value @@ -44,6 +47,10 @@ public final class PerpetualSceneViewModel { transactionsQuery.value } + var associatedAssets: [GemPerpetualAssociatedAssetRow] { + associatedAssetsQuery.value + } + let chart: PerpetualChartViewModel public var isPresentingInfoSheet: InfoSheetModel? @@ -75,6 +82,10 @@ public final class PerpetualSceneViewModel { ), initialValue: [], ) + associatedAssetsQuery = ObservableQuery( + MappedQuery(AssociatedAssetsQuery(assetId: asset.id, type: .official)) { perpetualAssociatedAssetRows(assets: $0.map { $0.toGem() }) }, + initialValue: [], + ) } var details: GemPerpetualDetails { diff --git a/ios/Gem/Navigation/Perpetuals/PerpetualNavigationView.swift b/ios/Gem/Navigation/Perpetuals/PerpetualNavigationView.swift index 99751a3a91..91fa84d743 100644 --- a/ios/Gem/Navigation/Perpetuals/PerpetualNavigationView.swift +++ b/ios/Gem/Navigation/Perpetuals/PerpetualNavigationView.swift @@ -24,7 +24,7 @@ public struct PerpetualNavigationView: View { onComplete: model.onAutocloseComplete, ) } - .bindQuery(model.positionsQuery, model.perpetualQuery, model.transactionsQuery) + .bindQuery(model.positionsQuery, model.perpetualQuery, model.transactionsQuery, model.associatedAssetsQuery) .onChange(of: isPresentingSheet) { oldValue, newValue in guard newValue == nil else { return } switch oldValue { diff --git a/ios/Gem/Services/ViewModelFactory+Perpetuals.swift b/ios/Gem/Services/ViewModelFactory+Perpetuals.swift index 19e9c77f5e..f0573f2281 100644 --- a/ios/Gem/Services/ViewModelFactory+Perpetuals.swift +++ b/ios/Gem/Services/ViewModelFactory+Perpetuals.swift @@ -42,7 +42,7 @@ public extension ViewModelFactory { PerpetualSceneViewModel( wallet: wallet, asset: asset, - service: GemPerpetualDetailsService(perpetuals: perpetualService, transactions: transactionsService, preferences: preferencesService, session: walletSessionService), + service: GemPerpetualDetailsService(perpetuals: perpetualService, transactions: transactionsService, assets: assetsService, preferences: preferencesService, session: walletSessionService), observerService: hyperliquidObserverService, onTransferData: onTransferData, onPerpetualPosition: onPerpetualPosition, diff --git a/ios/Packages/Store/Sources/Queries/AssociatedAssetsQuery.swift b/ios/Packages/Store/Sources/Queries/AssociatedAssetsQuery.swift new file mode 100644 index 0000000000..8a5f01e522 --- /dev/null +++ b/ios/Packages/Store/Sources/Queries/AssociatedAssetsQuery.swift @@ -0,0 +1,29 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import Foundation +import GRDB +import Primitives + +public struct AssociatedAssetsQuery: DatabaseQueryable { + public let assetId: AssetId + public let type: AssetAssociationType + + public init(assetId: AssetId, type: AssetAssociationType) { + self.assetId = assetId + self.type = type + } + + public func fetch(_ db: Database) throws -> [Asset] { + let associations = try AssetRecord + .filter(AssetRecord.Columns.id == assetId.identifier) + .fetchOne(db)? + .associations ?? [] + let assetIds = associations.filter { $0.type == type }.map(\.assetId.identifier) + return try AssetRecord + .filter(assetIds.contains(AssetRecord.Columns.id)) + .fetchAll(db) + .map { $0.mapToAsset() } + } +} + +extension AssociatedAssetsQuery: Equatable {} diff --git a/ios/Packages/Store/Tests/StoreTests/Queries/AssociatedAssetsQueryTests.swift b/ios/Packages/Store/Tests/StoreTests/Queries/AssociatedAssetsQueryTests.swift new file mode 100644 index 0000000000..6bfa25ef5d --- /dev/null +++ b/ios/Packages/Store/Tests/StoreTests/Queries/AssociatedAssetsQueryTests.swift @@ -0,0 +1,29 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import Primitives +import PrimitivesTestKit +import Store +import StoreTestKit +import Testing + +struct AssociatedAssetsQueryTests { + @Test + func assetsOfType() throws { + let perpetual = Asset.mock(id: AssetId(chain: .hyperCore, tokenId: "perpetual::BTC"), type: .perpetual) + let bitcoin = Asset.mock(id: AssetId(chain: .bitcoin)) + let wrapped = Asset.mock(id: AssetId(chain: .ethereum, tokenId: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"), type: .erc20) + let db = DB.mock(assets: [.mock(asset: perpetual), .mock(asset: bitcoin), .mock(asset: wrapped)]) + try AssetStore(db: db).updateAssociations(assetId: perpetual.id, associations: [ + AssetAssociation(assetId: bitcoin.id, type: .official), + AssetAssociation(assetId: wrapped.id, type: .wrapped), + ]) + + try db.dbQueue.read { db in + let result = try AssociatedAssetsQuery(assetId: perpetual.id, type: .official).fetch(db) + let notFound = try AssociatedAssetsQuery(assetId: bitcoin.id, type: .official).fetch(db) + + #expect(result == [bitcoin]) + #expect(notFound.isEmpty) + } + } +} From c94c5af9f465b38afb9f5f9036fb4c3027325e01 Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Tue, 13 Oct 2026 01:13:48 +0300 Subject: [PATCH 2/3] Read a perpetual's associated assets through its query PerpetualQuery already joins the perpetual's asset, so it reads the official associated assets with it; the separate AssociatedAssetsQuery is gone on both apps. --- .../store/queries/AssociatedAssetsQuery.kt | 24 ------- .../services/store/queries/PerpetualQuery.kt | 15 ++++- .../integration/AssociatedAssetsQueryTest.kt | 64 ------------------- .../store/integration/PerpetualQueryTest.kt | 32 +++++++++- .../viewmodels/PerpetualViewModel.kt | 5 +- .../viewmodels/PerpetualViewModelTest.kt | 6 +- .../ViewModels/PerpetualSceneViewModel.swift | 9 +-- .../Perpetuals/PerpetualNavigationView.swift | 2 +- .../Queries/AssociatedAssetsQuery.swift | 29 --------- .../Queries/Perpetuals/PerpetualQuery.swift | 30 +++++++-- .../Queries/AssociatedAssetsQueryTests.swift | 29 --------- .../Queries/PerpetualQueryTests.swift | 61 ++++++++++++------ 12 files changed, 114 insertions(+), 192 deletions(-) delete mode 100644 android/data/services/store/src/main/kotlin/com/gemwallet/android/data/services/store/queries/AssociatedAssetsQuery.kt delete mode 100644 android/data/services/store/src/test/kotlin/com/gemwallet/android/data/services/store/integration/AssociatedAssetsQueryTest.kt delete mode 100644 ios/Packages/Store/Sources/Queries/AssociatedAssetsQuery.swift delete mode 100644 ios/Packages/Store/Tests/StoreTests/Queries/AssociatedAssetsQueryTests.swift diff --git a/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/services/store/queries/AssociatedAssetsQuery.kt b/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/services/store/queries/AssociatedAssetsQuery.kt deleted file mode 100644 index a226e4631b..0000000000 --- a/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/services/store/queries/AssociatedAssetsQuery.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.gemwallet.android.data.services.store.queries - -import com.gemwallet.android.data.services.store.database.AssetsDao -import com.gemwallet.android.data.services.store.database.entities.toDTO -import com.gemwallet.android.ext.toIdentifier -import com.wallet.core.primitives.Asset -import com.wallet.core.primitives.AssetAssociationType -import com.wallet.core.primitives.AssetId -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map -import javax.inject.Inject - -@OptIn(ExperimentalCoroutinesApi::class) -class AssociatedAssetsQuery @Inject constructor(private val assetsDao: AssetsDao) { - - operator fun invoke(assetId: AssetId, type: AssetAssociationType): Flow> = assetsDao.getAsset(assetId.toIdentifier()) - .map { asset -> asset?.associations.orEmpty().filter { it.type == type }.map { it.assetId.toIdentifier() } } - .distinctUntilChanged() - .flatMapLatest { assetIds -> assetsDao.getAssets(assetIds) } - .map { assets -> assets.mapNotNull { it.toDTO() } } -} diff --git a/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/services/store/queries/PerpetualQuery.kt b/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/services/store/queries/PerpetualQuery.kt index f8e9aecd0c..642c17543f 100644 --- a/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/services/store/queries/PerpetualQuery.kt +++ b/android/data/services/store/src/main/kotlin/com/gemwallet/android/data/services/store/queries/PerpetualQuery.kt @@ -1,18 +1,31 @@ package com.gemwallet.android.data.services.store.queries +import com.gemwallet.android.data.services.store.database.AssetsDao import com.gemwallet.android.data.services.store.database.PerpetualDao import com.gemwallet.android.data.services.store.database.entities.toDTO import com.gemwallet.android.ext.toIdentifier +import com.wallet.core.primitives.Asset +import com.wallet.core.primitives.AssetAssociationType import com.wallet.core.primitives.AssetId import com.wallet.core.primitives.PerpetualData import com.wallet.core.primitives.PerpetualId +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import javax.inject.Inject -class PerpetualQuery @Inject constructor(private val perpetualDao: PerpetualDao) { +@OptIn(ExperimentalCoroutinesApi::class) +class PerpetualQuery @Inject constructor(private val perpetualDao: PerpetualDao, private val assetsDao: AssetsDao) { operator fun invoke(assetId: AssetId): Flow = perpetualDao.getPerpetualByAssetId(assetId.toIdentifier()).map { it?.toDTO() } operator fun invoke(perpetualId: PerpetualId): Flow = perpetualDao.getPerpetual(perpetualId.toIdentifier()).map { it?.toDTO() } + + fun associatedAssets(assetId: AssetId): Flow> = perpetualDao.getPerpetualByAssetId(assetId.toIdentifier()) + .map { perpetual -> perpetual?.asset?.associations.orEmpty().filter { it.type == AssetAssociationType.Official }.map { it.assetId.toIdentifier() } } + .distinctUntilChanged() + .flatMapLatest { assetIds -> assetsDao.getAssets(assetIds) } + .map { assets -> assets.mapNotNull { it.toDTO() } } } diff --git a/android/data/services/store/src/test/kotlin/com/gemwallet/android/data/services/store/integration/AssociatedAssetsQueryTest.kt b/android/data/services/store/src/test/kotlin/com/gemwallet/android/data/services/store/integration/AssociatedAssetsQueryTest.kt deleted file mode 100644 index 231d03304b..0000000000 --- a/android/data/services/store/src/test/kotlin/com/gemwallet/android/data/services/store/integration/AssociatedAssetsQueryTest.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.gemwallet.android.data.services.store.integration - -import androidx.room.Room -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import com.gemwallet.android.data.services.store.database.GemDatabase -import com.gemwallet.android.data.services.store.database.entities.DbAsset -import com.gemwallet.android.data.services.store.queries.AssociatedAssetsQuery -import com.gemwallet.android.ext.toIdentifier -import com.wallet.core.primitives.AssetAssociation -import com.wallet.core.primitives.AssetAssociationType -import com.wallet.core.primitives.AssetId -import com.wallet.core.primitives.AssetType -import com.wallet.core.primitives.Chain -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.runBlocking -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class AssociatedAssetsQueryTest { - private lateinit var database: GemDatabase - private lateinit var query: AssociatedAssetsQuery - private val perpetual = AssetId(Chain.HyperCore, "perpetual::BTC") - private val bitcoin = AssetId(Chain.Bitcoin) - private val wrapped = AssetId(Chain.Ethereum, "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599") - - @Before - fun setUp() = runBlocking(Dispatchers.IO) { - database = Room.inMemoryDatabaseBuilder( - InstrumentationRegistry.getInstrumentation().targetContext, - GemDatabase::class.java, - ).build() - query = AssociatedAssetsQuery(database.assetsDao()) - database.assetsDao().insert( - listOf( - DbAsset( - id = perpetual.toIdentifier(), - chain = Chain.HyperCore, - name = "BTC", - symbol = "BTC", - decimals = 5, - type = AssetType.PERPETUAL, - associations = listOf(AssetAssociation(bitcoin, AssetAssociationType.Official), AssetAssociation(wrapped, AssetAssociationType.Wrapped)), - ), - DbAsset(id = bitcoin.toIdentifier(), chain = Chain.Bitcoin, name = "Bitcoin", symbol = "BTC", decimals = 8, type = AssetType.NATIVE), - DbAsset(id = wrapped.toIdentifier(), chain = Chain.Ethereum, name = "Wrapped BTC", symbol = "WBTC", decimals = 8, type = AssetType.ERC20), - ), - ) - } - - @After - fun tearDown() = database.close() - - @Test - fun assetsOfType() = runBlocking(Dispatchers.IO) { - assertEquals(listOf(bitcoin), query(perpetual, AssetAssociationType.Official).first().map { it.id }) - assertEquals(emptyList(), query(bitcoin, AssetAssociationType.Official).first().map { it.id }) - } -} diff --git a/android/data/services/store/src/test/kotlin/com/gemwallet/android/data/services/store/integration/PerpetualQueryTest.kt b/android/data/services/store/src/test/kotlin/com/gemwallet/android/data/services/store/integration/PerpetualQueryTest.kt index 6705fc6605..3f14b5c205 100644 --- a/android/data/services/store/src/test/kotlin/com/gemwallet/android/data/services/store/integration/PerpetualQueryTest.kt +++ b/android/data/services/store/src/test/kotlin/com/gemwallet/android/data/services/store/integration/PerpetualQueryTest.kt @@ -4,11 +4,17 @@ import androidx.room.Room import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.gemwallet.android.data.services.store.database.GemDatabase +import com.gemwallet.android.data.services.store.database.entities.DbAsset import com.gemwallet.android.data.services.store.database.entities.toRecord import com.gemwallet.android.data.services.store.queries.PerpetualQuery +import com.gemwallet.android.ext.toIdentifier import com.gemwallet.android.testkit.mockAsset import com.gemwallet.android.testkit.mockAssetId import com.gemwallet.android.testkit.mockPerpetual +import com.wallet.core.primitives.AssetAssociation +import com.wallet.core.primitives.AssetAssociationType +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.AssetType import com.wallet.core.primitives.Chain import com.wallet.core.primitives.PerpetualData import com.wallet.core.primitives.PerpetualId @@ -30,7 +36,7 @@ class PerpetualQueryTest { InstrumentationRegistry.getInstrumentation().targetContext, GemDatabase::class.java, ).build() - private val query = PerpetualQuery(database.perpetualDao()) + private val query = PerpetualQuery(database.perpetualDao(), database.assetsDao()) private val bitcoinAsset = mockAsset(id = mockAssetId(chain = Chain.Bitcoin), name = "Bitcoin", symbol = "BTC", decimals = 8u) private val ethereumAsset = mockAsset(id = mockAssetId(chain = Chain.Ethereum), name = "Ethereum", symbol = "ETH", decimals = 18u) private val bitcoin = PerpetualData( @@ -70,6 +76,30 @@ class PerpetualQueryTest { assertEquals(ethereum, query(ethereum.perpetual.id).first()) } + @Test + fun theMarketListsItsOfficialAssociatedAssets() = runBlocking(Dispatchers.IO) { + val perpetualAssetId = AssetId(Chain.HyperCore, "perpetual::CAKE") + val wrapped = AssetId(Chain.Ethereum, "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599") + database.assetsDao().insert( + listOf( + DbAsset( + id = perpetualAssetId.toIdentifier(), + chain = Chain.HyperCore, + name = "CAKE", + symbol = "CAKE", + decimals = 1, + type = AssetType.PERPETUAL, + associations = listOf(AssetAssociation(ethereumAsset.id, AssetAssociationType.Official), AssetAssociation(wrapped, AssetAssociationType.Wrapped)), + ), + DbAsset(id = wrapped.toIdentifier(), chain = Chain.Ethereum, name = "Wrapped BTC", symbol = "WBTC", decimals = 8, type = AssetType.ERC20), + ), + ) + database.perpetualDao().insert(listOf(mockPerpetual().copy(id = PerpetualId(PerpetualProvider.Hypercore, "CAKE"), name = "CAKE", assetId = perpetualAssetId, identifier = "2").toRecord())) + + assertEquals(listOf(ethereumAsset.id), query.associatedAssets(perpetualAssetId).first().map { it.id }) + assertEquals(emptyList(), query.associatedAssets(ethereumAsset.id).first().map { it.id }) + } + @Test fun anUnknownAssetOrIdHasNoMarket() = runBlocking(Dispatchers.IO) { assertNull(query(mockAssetId(chain = Chain.Solana)).first()) diff --git a/android/features/perpetuals/viewmodels/src/main/kotlin/com/gemwallet/android/features/perpetuals/viewmodels/PerpetualViewModel.kt b/android/features/perpetuals/viewmodels/src/main/kotlin/com/gemwallet/android/features/perpetuals/viewmodels/PerpetualViewModel.kt index f76dd692bd..0b0fa869eb 100644 --- a/android/features/perpetuals/viewmodels/src/main/kotlin/com/gemwallet/android/features/perpetuals/viewmodels/PerpetualViewModel.kt +++ b/android/features/perpetuals/viewmodels/src/main/kotlin/com/gemwallet/android/features/perpetuals/viewmodels/PerpetualViewModel.kt @@ -9,7 +9,6 @@ import com.gemwallet.android.application.IoDispatcher import com.gemwallet.android.application.perpetual.cases.PerpetualObserver import com.gemwallet.android.application.session.cases.GetSession import com.gemwallet.android.application.transactions.cases.GetTransactions -import com.gemwallet.android.data.services.store.queries.AssociatedAssetsQuery import com.gemwallet.android.data.services.store.queries.PerpetualPositionsQuery import com.gemwallet.android.data.services.store.queries.PerpetualQuery import com.gemwallet.android.domains.confirm.ConfirmTransferInput @@ -26,7 +25,6 @@ import com.gemwallet.android.ui.models.actions.AmountTransactionAction import com.gemwallet.android.ui.models.actions.ConfirmTransactionAction import com.gemwallet.android.ui.models.flatMap import com.gemwallet.android.ui.models.navigation.requireAssetId -import com.wallet.core.primitives.AssetAssociationType import com.wallet.core.primitives.ChartPeriod import com.wallet.core.primitives.PerpetualDirection import com.wallet.core.primitives.TransactionsFilter @@ -77,7 +75,6 @@ import javax.inject.Inject class PerpetualViewModel @Inject constructor( private val perpetualQuery: PerpetualQuery, private val perpetualPositionsQuery: PerpetualPositionsQuery, - private val associatedAssetsQuery: AssociatedAssetsQuery, private val getTransactions: GetTransactions, private val perpetualObserver: PerpetualObserver, private val service: GemPerpetualDetailsServiceInterface, @@ -138,7 +135,7 @@ class PerpetualViewModel @Inject constructor( .flowOn(ioDispatcher) .stateIn(viewModelScope, SharingStarted.Eagerly, getTransactions.stored(transactionsFilter, GemConstants.transactionsListLimit)) - val associatedAssets: StateFlow> = associatedAssetsQuery(assetId, AssetAssociationType.Official) + val associatedAssets: StateFlow> = perpetualQuery.associatedAssets(assetId) .map { assets -> perpetualAssociatedAssetRows(assets.map { it.toGem() }) } .flowOn(ioDispatcher) .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) diff --git a/android/features/perpetuals/viewmodels/src/test/kotlin/com/gemwallet/android/features/perpetuals/viewmodels/PerpetualViewModelTest.kt b/android/features/perpetuals/viewmodels/src/test/kotlin/com/gemwallet/android/features/perpetuals/viewmodels/PerpetualViewModelTest.kt index 2cca8463c9..a80d826d12 100644 --- a/android/features/perpetuals/viewmodels/src/test/kotlin/com/gemwallet/android/features/perpetuals/viewmodels/PerpetualViewModelTest.kt +++ b/android/features/perpetuals/viewmodels/src/test/kotlin/com/gemwallet/android/features/perpetuals/viewmodels/PerpetualViewModelTest.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.viewModelScope import com.gemwallet.android.application.perpetual.cases.PerpetualObserver import com.gemwallet.android.application.session.cases.GetSession import com.gemwallet.android.application.transactions.cases.GetTransactions -import com.gemwallet.android.data.services.store.queries.AssociatedAssetsQuery import com.gemwallet.android.data.services.store.queries.PerpetualPositionsQuery import com.gemwallet.android.data.services.store.queries.PerpetualQuery import com.gemwallet.android.ext.toIdentifier @@ -93,18 +92,15 @@ class PerpetualViewModelTest { } val perpetual: PerpetualQuery = mockk { every { this@mockk(any()) } returns perpetuals + every { associatedAssets(any()) } returns flowOf(emptyList()) } val transactions: GetTransactions = mockk { every { getTransactions(any(), any()) } returns emptyFlow() every { stored(any(), any()) } returns emptyList() } - val associatedAssets: AssociatedAssetsQuery = mockk { - every { this@mockk(any(), any()) } returns flowOf(emptyList()) - } return PerpetualViewModel( perpetual, positions, - associatedAssets, transactions, observer, service, diff --git a/ios/Features/Perpetuals/Sources/ViewModels/PerpetualSceneViewModel.swift b/ios/Features/Perpetuals/Sources/ViewModels/PerpetualSceneViewModel.swift index 7f90fb1fa8..a183f1fb96 100644 --- a/ios/Features/Perpetuals/Sources/ViewModels/PerpetualSceneViewModel.swift +++ b/ios/Features/Perpetuals/Sources/ViewModels/PerpetualSceneViewModel.swift @@ -33,14 +33,13 @@ public final class PerpetualSceneViewModel { public let positionsQuery: ObservableQuery public let perpetualQuery: ObservableQuery public let transactionsQuery: ObservableQuery]>> - public let associatedAssetsQuery: ObservableQuery> public var positions: [PerpetualPositionData] { positionsQuery.value } var perpetualData: PerpetualData { - perpetualQuery.value + perpetualQuery.value.data } var transactionSections: [ListSection] { @@ -48,7 +47,7 @@ public final class PerpetualSceneViewModel { } var associatedAssets: [GemPerpetualAssociatedAssetRow] { - associatedAssetsQuery.value + perpetualAssociatedAssetRows(assets: perpetualQuery.value.associatedAssets.map { $0.toGem() }) } let chart: PerpetualChartViewModel @@ -82,10 +81,6 @@ public final class PerpetualSceneViewModel { ), initialValue: [], ) - associatedAssetsQuery = ObservableQuery( - MappedQuery(AssociatedAssetsQuery(assetId: asset.id, type: .official)) { perpetualAssociatedAssetRows(assets: $0.map { $0.toGem() }) }, - initialValue: [], - ) } var details: GemPerpetualDetails { diff --git a/ios/Gem/Navigation/Perpetuals/PerpetualNavigationView.swift b/ios/Gem/Navigation/Perpetuals/PerpetualNavigationView.swift index 91fa84d743..99751a3a91 100644 --- a/ios/Gem/Navigation/Perpetuals/PerpetualNavigationView.swift +++ b/ios/Gem/Navigation/Perpetuals/PerpetualNavigationView.swift @@ -24,7 +24,7 @@ public struct PerpetualNavigationView: View { onComplete: model.onAutocloseComplete, ) } - .bindQuery(model.positionsQuery, model.perpetualQuery, model.transactionsQuery, model.associatedAssetsQuery) + .bindQuery(model.positionsQuery, model.perpetualQuery, model.transactionsQuery) .onChange(of: isPresentingSheet) { oldValue, newValue in guard newValue == nil else { return } switch oldValue { diff --git a/ios/Packages/Store/Sources/Queries/AssociatedAssetsQuery.swift b/ios/Packages/Store/Sources/Queries/AssociatedAssetsQuery.swift deleted file mode 100644 index 8a5f01e522..0000000000 --- a/ios/Packages/Store/Sources/Queries/AssociatedAssetsQuery.swift +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c). Gem Wallet. All rights reserved. - -import Foundation -import GRDB -import Primitives - -public struct AssociatedAssetsQuery: DatabaseQueryable { - public let assetId: AssetId - public let type: AssetAssociationType - - public init(assetId: AssetId, type: AssetAssociationType) { - self.assetId = assetId - self.type = type - } - - public func fetch(_ db: Database) throws -> [Asset] { - let associations = try AssetRecord - .filter(AssetRecord.Columns.id == assetId.identifier) - .fetchOne(db)? - .associations ?? [] - let assetIds = associations.filter { $0.type == type }.map(\.assetId.identifier) - return try AssetRecord - .filter(assetIds.contains(AssetRecord.Columns.id)) - .fetchAll(db) - .map { $0.mapToAsset() } - } -} - -extension AssociatedAssetsQuery: Equatable {} diff --git a/ios/Packages/Store/Sources/Queries/Perpetuals/PerpetualQuery.swift b/ios/Packages/Store/Sources/Queries/Perpetuals/PerpetualQuery.swift index 475bcc845d..0bfb3ecebe 100644 --- a/ios/Packages/Store/Sources/Queries/Perpetuals/PerpetualQuery.swift +++ b/ios/Packages/Store/Sources/Queries/Perpetuals/PerpetualQuery.swift @@ -4,6 +4,18 @@ import Foundation import GRDB import Primitives +public struct PerpetualResult: Equatable, Sendable { + public let data: PerpetualData + public let associatedAssets: [Asset] + + public init(data: PerpetualData, associatedAssets: [Asset]) { + self.data = data + self.associatedAssets = associatedAssets + } + + public static let empty = PerpetualResult(data: .empty, associatedAssets: []) +} + public struct PerpetualQuery: DatabaseQueryable { public let assetId: AssetId @@ -11,15 +23,21 @@ public struct PerpetualQuery: DatabaseQueryable { self.assetId = assetId } - public func fetch(_ db: Database) throws -> PerpetualData { - let result = try PerpetualRecord + public func fetch(_ db: Database) throws -> PerpetualResult { + guard let info = try PerpetualRecord .including(required: PerpetualRecord.asset) .filter(PerpetualRecord.Columns.assetId == assetId.identifier) .asRequest(of: PerpetualInfo.self) - .fetchOne(db)? - .mapToPerpetualData() - - return result ?? .empty + .fetchOne(db) + else { + return .empty + } + let assetIds = info.asset.associations.filter { $0.type == .official }.map(\.assetId.identifier) + let associatedAssets = try AssetRecord + .filter(assetIds.contains(AssetRecord.Columns.id)) + .fetchAll(db) + .map { $0.mapToAsset() } + return PerpetualResult(data: info.mapToPerpetualData(), associatedAssets: associatedAssets) } } diff --git a/ios/Packages/Store/Tests/StoreTests/Queries/AssociatedAssetsQueryTests.swift b/ios/Packages/Store/Tests/StoreTests/Queries/AssociatedAssetsQueryTests.swift deleted file mode 100644 index 6bfa25ef5d..0000000000 --- a/ios/Packages/Store/Tests/StoreTests/Queries/AssociatedAssetsQueryTests.swift +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c). Gem Wallet. All rights reserved. - -import Primitives -import PrimitivesTestKit -import Store -import StoreTestKit -import Testing - -struct AssociatedAssetsQueryTests { - @Test - func assetsOfType() throws { - let perpetual = Asset.mock(id: AssetId(chain: .hyperCore, tokenId: "perpetual::BTC"), type: .perpetual) - let bitcoin = Asset.mock(id: AssetId(chain: .bitcoin)) - let wrapped = Asset.mock(id: AssetId(chain: .ethereum, tokenId: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"), type: .erc20) - let db = DB.mock(assets: [.mock(asset: perpetual), .mock(asset: bitcoin), .mock(asset: wrapped)]) - try AssetStore(db: db).updateAssociations(assetId: perpetual.id, associations: [ - AssetAssociation(assetId: bitcoin.id, type: .official), - AssetAssociation(assetId: wrapped.id, type: .wrapped), - ]) - - try db.dbQueue.read { db in - let result = try AssociatedAssetsQuery(assetId: perpetual.id, type: .official).fetch(db) - let notFound = try AssociatedAssetsQuery(assetId: bitcoin.id, type: .official).fetch(db) - - #expect(result == [bitcoin]) - #expect(notFound.isEmpty) - } - } -} diff --git a/ios/Packages/Store/Tests/StoreTests/Queries/PerpetualQueryTests.swift b/ios/Packages/Store/Tests/StoreTests/Queries/PerpetualQueryTests.swift index 55f9c620c8..700cffb983 100644 --- a/ios/Packages/Store/Tests/StoreTests/Queries/PerpetualQueryTests.swift +++ b/ios/Packages/Store/Tests/StoreTests/Queries/PerpetualQueryTests.swift @@ -45,11 +45,11 @@ struct PerpetualQueryTests { let result = try PerpetualQuery(assetId: eth).fetch(db) let notFound = try PerpetualQuery(assetId: AssetId(chain: .bitcoin)).fetch(db) - #expect(result.perpetual.id == perpetual.id) - #expect(result.perpetual.price == 2500.0) - #expect(result.perpetual.isIsolatedOnly == true) - #expect(result.asset.id == eth) - #expect(result.metadata.isPinned == true) + #expect(result.data.perpetual.id == perpetual.id) + #expect(result.data.perpetual.price == 2500.0) + #expect(result.data.perpetual.isIsolatedOnly == true) + #expect(result.data.asset.id == eth) + #expect(result.data.metadata.isPinned == true) #expect(notFound == .empty) } } @@ -87,18 +87,37 @@ struct PerpetualQueryTests { let result = try PerpetualQuery(assetId: newAssetId).fetch(db) let stale = try PerpetualQuery(assetId: oldAssetId).fetch(db) - #expect(result.perpetual.id == updated.id) - #expect(result.perpetual.assetId == newAssetId) - #expect(result.perpetual.name == "ETH-PERP") - #expect(result.perpetual.price == 3000.0) - #expect(result.perpetual.maxLeverage == 75) - #expect(result.perpetual.isIsolatedOnly == true) - #expect(result.asset.id == newAssetId) - #expect(result.metadata.isPinned == true) + #expect(result.data.perpetual.id == updated.id) + #expect(result.data.perpetual.assetId == newAssetId) + #expect(result.data.perpetual.name == "ETH-PERP") + #expect(result.data.perpetual.price == 3000.0) + #expect(result.data.perpetual.maxLeverage == 75) + #expect(result.data.perpetual.isIsolatedOnly == true) + #expect(result.data.asset.id == newAssetId) + #expect(result.data.metadata.isPinned == true) #expect(stale == .empty) } } + @Test + func associatedAssets() throws { + let perpetualAssetId = AssetId(chain: .hyperCore, tokenId: "perpetual::BTC") + let bitcoin = Asset.mock(id: AssetId(chain: .bitcoin)) + let wrapped = Asset.mock(id: AssetId(chain: .ethereum, tokenId: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"), type: .erc20) + let db = DB.mock(assets: [.mock(asset: .mock(id: perpetualAssetId, type: .perpetual)), .mock(asset: bitcoin), .mock(asset: wrapped)]) + try PerpetualStore(db: db).upsertPerpetuals([.mock(assetId: perpetualAssetId)]) + try AssetStore(db: db).updateAssociations(assetId: perpetualAssetId, associations: [ + AssetAssociation(assetId: bitcoin.id, type: .official), + AssetAssociation(assetId: wrapped.id, type: .wrapped), + ]) + + try db.dbQueue.read { db in + let result = try PerpetualQuery(assetId: perpetualAssetId).fetch(db) + + #expect(result.associatedAssets == [bitcoin]) + } + } + @Test func updatesMarketData() throws { let db = DB.mock(wallets: [wallet], assets: walletAssets, balances: walletBalances) @@ -123,11 +142,11 @@ struct PerpetualQueryTests { try db.dbQueue.read { db in let result = try PerpetualQuery(assetId: eth).fetch(db) - #expect(result.perpetual.price == 2236.45) - #expect(result.perpetual.pricePercentChange24h == 5.12) - #expect(result.perpetual.openInterest == 1_538_967.4595) - #expect(result.perpetual.volume24h == 1_169_046.29406) - #expect(result.perpetual.funding == 0.00125) + #expect(result.data.perpetual.price == 2236.45) + #expect(result.data.perpetual.pricePercentChange24h == 5.12) + #expect(result.data.perpetual.openInterest == 1_538_967.4595) + #expect(result.data.perpetual.volume24h == 1_169_046.29406) + #expect(result.data.perpetual.funding == 0.00125) } } @@ -151,9 +170,9 @@ struct PerpetualQueryTests { try db.dbQueue.read { db in let result = try PerpetualQuery(assetId: eth).fetch(db) - #expect(result.perpetual.price == 2236.45) - #expect(result.perpetual.pricePercentChange24h == 5.12) - #expect(result.perpetual.openInterest == 1_538_967.4595) + #expect(result.data.perpetual.price == 2236.45) + #expect(result.data.perpetual.pricePercentChange24h == 5.12) + #expect(result.data.perpetual.openInterest == 1_538_967.4595) } } } From 076cad911c4bab13dfcafd725c68d2169c1d82cd Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:23:27 +0300 Subject: [PATCH 3/3] Build a perpetual's associated asset rows in its details perpetual_associated_asset_rows is no longer exported: details() takes the market's associated assets and returns their rows, like the position row. --- .../presents/perpetual/PerpetualScene.kt | 6 ++---- .../presents/perpetual/PerpetualScreen.kt | 2 -- .../perpetuals/viewmodels/PerpetualViewModel.kt | 11 ++--------- .../gemwallet/android/testkit/GeneratedMocks.kt | 2 ++ core/gemstone/src/services/perpetual/details.rs | 4 ++-- core/gemstone/src/services/perpetual/model.rs | 6 +----- core/gemstone/src/services/perpetual/rules.rs | 17 ++++++++++------- .../Sources/Scenes/PerpetualScene.swift | 4 ++-- .../ViewModels/PerpetualSceneViewModel.swift | 13 ++++++------- .../TestKit/GemPerpetualServiceMocks.swift | 2 +- .../TestKit/GeneratedMocks.swift | 2 ++ 11 files changed, 30 insertions(+), 39 deletions(-) diff --git a/android/features/perpetuals/presents/src/main/kotlin/com/gemwallet/android/features/perpetuals/presents/perpetual/PerpetualScene.kt b/android/features/perpetuals/presents/src/main/kotlin/com/gemwallet/android/features/perpetuals/presents/perpetual/PerpetualScene.kt index 63c3c47f29..39b90e8c85 100644 --- a/android/features/perpetuals/presents/src/main/kotlin/com/gemwallet/android/features/perpetuals/presents/perpetual/PerpetualScene.kt +++ b/android/features/perpetuals/presents/src/main/kotlin/com/gemwallet/android/features/perpetuals/presents/perpetual/PerpetualScene.kt @@ -34,7 +34,6 @@ import uniffi.gemstone.GemInfoTopic import uniffi.gemstone.GemListRow import uniffi.gemstone.GemListRowTitle import uniffi.gemstone.GemLocalizedText -import uniffi.gemstone.GemPerpetualAssociatedAssetRow import uniffi.gemstone.GemPerpetualButton import uniffi.gemstone.GemPerpetualButtonRow import uniffi.gemstone.GemPerpetualDetails @@ -48,7 +47,6 @@ import uniffi.gemstone.GemValueTone internal fun PerpetualScene( details: GemPerpetualDetails?, transactions: List, - associatedAssets: List, chart: StateViewType, period: ChartPeriod, isRefreshing: Boolean, @@ -112,7 +110,7 @@ internal fun PerpetualScene( } } } - itemsPositioned(associatedAssets) { position, associatedAsset -> + itemsPositioned(details?.associatedAssets.orEmpty()) { position, associatedAsset -> AssetListItem( row = associatedAsset.row, listPosition = position, @@ -171,9 +169,9 @@ private fun PerpetualScenePreview() { ), position = null, positionRow = null, + associatedAssets = emptyList(), ), transactions = emptyList(), - associatedAssets = emptyList(), chart = StateViewType.Loading, period = ChartPeriod.Day, isRefreshing = false, diff --git a/android/features/perpetuals/presents/src/main/kotlin/com/gemwallet/android/features/perpetuals/presents/perpetual/PerpetualScreen.kt b/android/features/perpetuals/presents/src/main/kotlin/com/gemwallet/android/features/perpetuals/presents/perpetual/PerpetualScreen.kt index 6b3cfba621..4707a0c261 100644 --- a/android/features/perpetuals/presents/src/main/kotlin/com/gemwallet/android/features/perpetuals/presents/perpetual/PerpetualScreen.kt +++ b/android/features/perpetuals/presents/src/main/kotlin/com/gemwallet/android/features/perpetuals/presents/perpetual/PerpetualScreen.kt @@ -48,7 +48,6 @@ fun PerpetualScreen( InfoBottomSheet(item = infoSheet?.infoSheet(), onClose = { viewModel.infoSheet.value = null }) val details by viewModel.details.collectAsStateWithLifecycle() val transactions by viewModel.transactions.collectAsStateWithLifecycle() - val associatedAssets by viewModel.associatedAssets.collectAsStateWithLifecycle() val chart by viewModel.chart.collectAsStateWithLifecycle() val period by viewModel.period.collectAsStateWithLifecycle() val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle() @@ -60,7 +59,6 @@ fun PerpetualScreen( PerpetualScene( details = details, transactions = transactions, - associatedAssets = associatedAssets, chart = chart, period = period, isRefreshing = isRefreshing, diff --git a/android/features/perpetuals/viewmodels/src/main/kotlin/com/gemwallet/android/features/perpetuals/viewmodels/PerpetualViewModel.kt b/android/features/perpetuals/viewmodels/src/main/kotlin/com/gemwallet/android/features/perpetuals/viewmodels/PerpetualViewModel.kt index 0b0fa869eb..859e52e8d0 100644 --- a/android/features/perpetuals/viewmodels/src/main/kotlin/com/gemwallet/android/features/perpetuals/viewmodels/PerpetualViewModel.kt +++ b/android/features/perpetuals/viewmodels/src/main/kotlin/com/gemwallet/android/features/perpetuals/viewmodels/PerpetualViewModel.kt @@ -60,12 +60,10 @@ import kotlinx.coroutines.withContext import uniffi.gemstone.GemCandleChart import uniffi.gemstone.GemInfoTopic import uniffi.gemstone.GemLoadState -import uniffi.gemstone.GemPerpetualAssociatedAssetRow import uniffi.gemstone.GemPerpetualDetails import uniffi.gemstone.GemPerpetualDetailsServiceInterface import uniffi.gemstone.GemPerpetualPositionKind import uniffi.gemstone.candleSession -import uniffi.gemstone.perpetualAssociatedAssetRows import java.time.Instant import java.time.ZoneId import javax.inject.Inject @@ -122,8 +120,8 @@ class PerpetualViewModel @Inject constructor( .flowOn(ioDispatcher) .stateIn(viewModelScope, SharingStarted.Eagerly, null) - val details: StateFlow = combine(perpetual, position) { perpetual, position -> - perpetual?.let { service.details(it.perpetual.toGem(), it.asset.toGem(), listOfNotNull(position?.toGem())) } + val details: StateFlow = combine(perpetual, position, perpetualQuery.associatedAssets(assetId)) { perpetual, position, associatedAssets -> + perpetual?.let { service.details(it.perpetual.toGem(), it.asset.toGem(), listOfNotNull(position?.toGem()), associatedAssets.map { asset -> asset.toGem() }) } } .flowOn(ioDispatcher) .stateIn(viewModelScope, SharingStarted.Eagerly, null) @@ -135,11 +133,6 @@ class PerpetualViewModel @Inject constructor( .flowOn(ioDispatcher) .stateIn(viewModelScope, SharingStarted.Eagerly, getTransactions.stored(transactionsFilter, GemConstants.transactionsListLimit)) - val associatedAssets: StateFlow> = perpetualQuery.associatedAssets(assetId) - .map { assets -> perpetualAssociatedAssetRows(assets.map { it.toGem() }) } - .flowOn(ioDispatcher) - .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) - private val candles = MutableStateFlow(candleSession(service.chartPeriod())) val period: StateFlow = candles.map { it.period.toPrimitives() } diff --git a/android/gemcore/src/testFixtures/kotlin/com/gemwallet/android/testkit/GeneratedMocks.kt b/android/gemcore/src/testFixtures/kotlin/com/gemwallet/android/testkit/GeneratedMocks.kt index 6b746488c9..4eea86706b 100644 --- a/android/gemcore/src/testFixtures/kotlin/com/gemwallet/android/testkit/GeneratedMocks.kt +++ b/android/gemcore/src/testFixtures/kotlin/com/gemwallet/android/testkit/GeneratedMocks.kt @@ -1629,12 +1629,14 @@ fun mockGemPerpetualDetails( modifyButtons: List = emptyList(), position: uniffi.gemstone.PerpetualPosition? = null, positionRow: uniffi.gemstone.GemPerpetualPositionRow? = null, + associatedAssets: List = emptyList(), ) = uniffi.gemstone.GemPerpetualDetails( title = title, sections = sections, modifyButtons = modifyButtons, position = position, positionRow = positionRow, + associatedAssets = associatedAssets, ) fun mockGemPerpetualTransferData( diff --git a/core/gemstone/src/services/perpetual/details.rs b/core/gemstone/src/services/perpetual/details.rs index 8e1c7438f5..a024650a69 100644 --- a/core/gemstone/src/services/perpetual/details.rs +++ b/core/gemstone/src/services/perpetual/details.rs @@ -41,8 +41,8 @@ impl GemPerpetualDetailsService { self.perpetuals.is_available() } - pub fn details(&self, perpetual: Perpetual, asset: Asset, positions: Vec) -> GemPerpetualDetails { - rules::details(&perpetual, &asset, positions) + pub fn details(&self, perpetual: Perpetual, asset: Asset, positions: Vec, associated_assets: Vec) -> GemPerpetualDetails { + rules::details(&perpetual, &asset, positions, associated_assets) } pub fn position_action(&self, perpetual: Perpetual, asset: Asset, position: Option, kind: GemPerpetualPositionKind) -> Result { diff --git a/core/gemstone/src/services/perpetual/model.rs b/core/gemstone/src/services/perpetual/model.rs index 3d6c844685..b7b51891ef 100644 --- a/core/gemstone/src/services/perpetual/model.rs +++ b/core/gemstone/src/services/perpetual/model.rs @@ -195,11 +195,6 @@ pub fn perpetual_position_rows(positions: Vec) -> Vec) -> Vec { - assets.into_iter().map(GemPerpetualAssociatedAssetRow::from).collect() -} - #[derive(Debug, Clone, PartialEq)] pub struct PerpetualMarketLine { pub asset_id: AssetId, @@ -408,6 +403,7 @@ pub struct GemPerpetualDetails { pub modify_buttons: Vec, pub position: Option, pub position_row: Option, + pub associated_assets: Vec, } #[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)] diff --git a/core/gemstone/src/services/perpetual/rules.rs b/core/gemstone/src/services/perpetual/rules.rs index 22677776ea..76706a6f5b 100644 --- a/core/gemstone/src/services/perpetual/rules.rs +++ b/core/gemstone/src/services/perpetual/rules.rs @@ -13,10 +13,10 @@ use primitives::{ use strum::IntoEnumIterator; use super::model::{ - GemCandleTooltip, GemCandleTooltipCell, GemCandleTooltipRow, GemMarketsRefreshTrigger, GemPerpetualButton, GemPerpetualButtonRow, GemPerpetualChartLayout, GemPerpetualChartLine, GemPerpetualChartLineKind, GemPerpetualCloseInput, - GemPerpetualConfirmDetails, GemPerpetualConfirmDetailsSummary, GemPerpetualDepositTarget, GemPerpetualDetails, GemPerpetualMarketCounts, GemPerpetualMarketQuery, GemPerpetualMarketSection, GemPerpetualMarketView, - GemPerpetualOrderAction, GemPerpetualOrderInput, GemPerpetualPositionAction, GemPerpetualPositionDetail, GemPerpetualPositionDetailRow, GemPerpetualPositionKind, GemPerpetualPositionRow, GemPerpetualSection, GemPerpetualTransferData, - PerpetualMarketLine, PerpetualOpenLine, PerpetualPositionLine, + GemCandleTooltip, GemCandleTooltipCell, GemCandleTooltipRow, GemMarketsRefreshTrigger, GemPerpetualAssociatedAssetRow, GemPerpetualButton, GemPerpetualButtonRow, GemPerpetualChartLayout, GemPerpetualChartLine, + GemPerpetualChartLineKind, GemPerpetualCloseInput, GemPerpetualConfirmDetails, GemPerpetualConfirmDetailsSummary, GemPerpetualDepositTarget, GemPerpetualDetails, GemPerpetualMarketCounts, GemPerpetualMarketQuery, + GemPerpetualMarketSection, GemPerpetualMarketView, GemPerpetualOrderAction, GemPerpetualOrderInput, GemPerpetualPositionAction, GemPerpetualPositionDetail, GemPerpetualPositionDetailRow, GemPerpetualPositionKind, + GemPerpetualPositionRow, GemPerpetualSection, GemPerpetualTransferData, PerpetualMarketLine, PerpetualOpenLine, PerpetualPositionLine, }; use crate::formatted_number::{GemFormattedNumber, GemValueTone, value_tone}; use crate::models::custom_types::GemBigInt; @@ -762,7 +762,7 @@ fn position_line(perpetual: &Perpetual, asset: &Asset, position: &PerpetualPosit } } -pub fn details(perpetual: &Perpetual, asset: &Asset, positions: Vec) -> GemPerpetualDetails { +pub fn details(perpetual: &Perpetual, asset: &Asset, positions: Vec, associated_assets: Vec) -> GemPerpetualDetails { let position = positions.into_iter().next(); let market = market_row(perpetual, asset); GemPerpetualDetails { @@ -780,6 +780,7 @@ pub fn details(perpetual: &Perpetual, asset: &Asset, positions: Vec GemPerpetualDetails { + public func details(perpetual _: Gemstone.Perpetual, asset _: Gemstone.Asset, positions _: [Gemstone.PerpetualPosition], associatedAssets _: [Gemstone.Asset]) -> GemPerpetualDetails { detailsValue } diff --git a/ios/Packages/GemstonePrimitives/TestKit/GeneratedMocks.swift b/ios/Packages/GemstonePrimitives/TestKit/GeneratedMocks.swift index 15904ebb4b..8bd7cba2d3 100644 --- a/ios/Packages/GemstonePrimitives/TestKit/GeneratedMocks.swift +++ b/ios/Packages/GemstonePrimitives/TestKit/GeneratedMocks.swift @@ -903,6 +903,7 @@ public extension Gemstone.GemPerpetualDetails { modifyButtons: [Gemstone.GemPerpetualButtonRow] = [], position: Gemstone.PerpetualPosition? = nil, positionRow: Gemstone.GemPerpetualPositionRow? = nil, + associatedAssets: [Gemstone.GemPerpetualAssociatedAssetRow] = [], ) -> Gemstone.GemPerpetualDetails { Gemstone.GemPerpetualDetails( title: title, @@ -910,6 +911,7 @@ public extension Gemstone.GemPerpetualDetails { modifyButtons: modifyButtons, position: position, positionRow: positionRow, + associatedAssets: associatedAssets, ) } }