7.8 KiB
Swap Feature
Token-to-token exchange feature. Users select FROM and TO tokens, get quotes from providers (DEX/CEX), approve ERC-20 allowances if needed, and execute swaps.
Module Structure
features/swap/
api/ — Public contracts (SwapComponent, SwapEntryComponent, SwapFeatureToggles)
impl/ — UI, model, navigation, DI, token selection subfeature
domain/ — Business logic (SwapInteractor) + domain models
api/ — Domain interfaces
models/ — Domain model types (SwapPair, SwapProvider, SwapState, etc.)
data/ — Repository implementations, Retrofit APIs, Moshi DTOs
Package naming: API = com.tangem.features.swap, Impl = com.tangem.feature.swap (singular feature, legacy inconsistency).
Key Components
SwapComponent (API)
Entry point. Params requires currencyFrom, userWalletId, screenSource. Optional: currencyTo, isInitialReverseOrder, tangemPayInput, preselectedToToken, preselectedAccount.
SwapEntryComponent (API)
Gateway component with sealed Params: Story, Empty, Selected, Payment. Routes to stories or directly to swap based on input type. See entry/SwapEntryRoute.kt for route definitions.
DefaultSwapComponent (impl)
Decompose component. Creates SwapModel via getOrCreateModel(params).
Child navigation:
childStack(SwapRoute)for screen navigation —SwapRoute.Main,SwapRoute.Success,SwapRoute.SelectToken(isFromDirection)rendered viaChildrencomposable with fade animationSlotNavigation<Unit>for approval bottom sheet (GiveApprovalComponent)SlotNavigation<FeeSelectorConfig>for fee selector block
Injected factories: SwapFeeSelectorBlockComponent.Factory, GiveApprovalComponent.Factory, ChooseTokenComponent.Factory.
SwapModel (impl)
@ModelScoped, extends Model(). The central coordinator — ~1500 lines.
Key state:
dataStateStateFlow: MutableStateFlow<SwapProcessDataState>— reactive domain data (from/to tokens, pairs, providers, amounts, fees)uiState: SwapStateHolder by mutableStateOf()— Compose UI state built byStateBuilderfeeSelectorRepository: FeeSelectorRepository— fee state managementstackNavigation: StackNavigation<SwapRoute>— stack navigation exposed fromSwapRouterapprovalSlotNavigation: SlotNavigation<Unit>— approval bottom sheet
Navigation:
SwapRouterwrapsAppRouter+StackNavigation<SwapRoute>for screen switching and back navigationswapRouter.openScreen(SwapRoute.SelectToken(isFromDirection))to push token selectionswapRouter.openScreen(SwapRoute.Success)replaces current with success screenswapRouter.back()— pops local stack or exits swap via AppRouter
Initialization flow (init block):
- Subscribes to
chooseTokenBridge.onCurrencyChosen→onTokenSelect(result) - Subscribes to
chooseTokenBridge.onClose→ pops slot navigation - Checks
ShouldShowStoriesUseCase→ pushesAppRoute.Storiesif first-time swap - Resolves user country for FCA restrictions
- Loads primary account status, initial currencies, and starts swap pair loading
Token selection flow:
- User taps FROM or TO card →
onSelectTokenClick(direction)pushesSwapRoute.SelectToken(isFromDirection)to stack - Stack creates
ChooseTokenComponentwith appropriate bridge (FROM or TO) ChooseTokenBridgecommunicates selection result via ChannelonTokenSelect(result)assigns selected token to FROM or TO based onisFromDirection
Swap execution flow:
onSwapClick()— validates state, checks approval, initiates transaction- If approval needed →
approvalSlotNavigation.activate(Unit) - On approval done → reloads quotes
- On swap success →
swapRouter.openScreen(SwapRoute.Success)
StateBuilder (impl)
Pure transformation class. Takes UiActions + providers, builds SwapStateHolder from SwapProcessDataState.
Key methods: createInitialLoadingState, createQuotesLoadedState, createSuccessState, loadingPermissionState, updateSwapAmount, addNotification, dismissBottomSheet.
SwapRouter (impl)
Wraps AppRouter + StackNavigation<SwapRoute>. Handles openScreen(SwapRoute) to push/replace stack entries and back() with special logic: SelectToken pops local stack, Success exits to screen before SwapCrypto in app stack, Main pops AppRouter. openTokenDetails() navigates to AppRoute.CurrencyDetails.
Token Selection Subfeature (impl)
Self-contained within choosetoken/ package:
ChooseTokenComponent— API withParams(bridge, settings, analyticsPayload)ChooseTokenBridge— Channel-based communication:onCurrencyChosen,onClose,onTokenSelected(legacy),onNewTokenAdded(legacy). HassettingsStateFlowfor dynamic settings.ChooseTokenComponent.Settings—SwapFrom(no market block) vsSwapTo(with market block)ChooseTokenResult— ContainsCryptoCurrencyStatus,AccountStatus,UserWalletDefaultChooseTokenComponent— Has its ownChooseTokenModeland optionalAddToPortfolioComponentbottom sheet slot
Domain Layer
SwapInteractor
Central domain interface. Methods:
getPair(from, to, filterProviderTypes)→Either<ExpressError, List<SwapPairLeast>>findBestQuote(from, to, providers, amount, ...)→Map<SwapProvider, SwapState>onSwap(from, to, provider, swapData, amount, fee, ...)→SwapTransactionStateloadFeeForSwapTransaction(...)→Either<GetFeeError, TransactionFee/TransactionFeeExtended>getInitialCurrencyToSwap(accountStatusList, fromUserWallet, isReverse)→AccountCryptoCurrencyStatus?getTokenBalance(token)→SwapAmount
Key Domain Models
SwapPairLeast— from/to token info + providers listSwapProvider— providerId, name, type (DEX/CEX/DEX_BRIDGE), rates, slippage, TOS linksSwapState— sealed:QuotesLoadedState,SwapError,EmptyAmountStateSwapCurrencyStatus— wrapsCryptoCurrencyStatus+UserWallet+AccountSwapAmount— value + decimals pairSwapDataModel— quote result with transaction data
DI Modules
| Module | Scope | Bindings |
|---|---|---|
SwapFeatureModule |
Singleton | SwapComponent.Factory, SwapFeatureToggles |
SwapModelModule |
ModelComponent | SwapModel into model map |
SwapEntryModule |
Singleton + Model | SwapEntryComponent.Factory, SwapEntryModel |
ChooseTokenModule |
Singleton + Model | ChooseTokenComponent.Factory, ChooseTokenBridge.Factory, ChooseTokenModel |
SwapSingletonModule |
Singleton | AmountFormatter |
UI Layer
SwapScreen— main swap composable (send card, receive card, swap button, provider, notifications, fee)SwapSuccessScreen— post-swap success with transaction detailsSwapScreenContent— layout withConstraintLayoutfor card positioningTransactionCard/TransactionCardEmpty— token cards with amount input- Token cards pass
TokenSelectionDirection.FROM/.TOtoonSelectTokenClick
Navigation Summary
AppRouter (global)
└─ AppRoute.Swap → DefaultSwapComponent
├─ childStack(SwapRoute)
│ ├─ SwapRoute.Main → SwapMainChild (renders SwapScreen)
│ ├─ SwapRoute.Success → SwapSuccessChild (renders SwapSuccessScreen)
│ └─ SwapRoute.SelectToken → ChooseTokenComponent (FROM or TO bridge)
├─ SlotNavigation<Unit> (Approval)
│ └─ GiveApprovalComponent (bottom sheet)
└─ SlotNavigation<FeeSelectorConfig>
└─ SwapFeeSelectorBlockComponent (inline fee block)
Build Commands
./gradlew :features:swap:impl:compileDebugKotlin
./gradlew :features:swap:api:compileDebugKotlin
./gradlew :features:swap:domain:compileDebugKotlin
./gradlew :features:swap:impl:detekt