Updated on 2026-08-14
This commit is contained in:
parent
a9bca131c3
commit
b4ae0c1547
14 changed files with 533 additions and 465 deletions
19
.claude/rules/codestyle/drawable-naming.md
Normal file
19
.claude/rules/codestyle/drawable-naming.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Image Resources
|
||||
|
||||
## Naming
|
||||
|
||||
There are 3 types of icons:
|
||||
|
||||
1. Black or single color icon (naming: `ic_name_24`, where number is size)
|
||||
2. Icon with constant color, and tint could be applied (naming: `img_name_24`)
|
||||
3. Large image with different colors and shapes (naming: `ill_name`)
|
||||
|
||||
Examples:
|
||||
|
||||
1. `ic_chevron_24`
|
||||
2. `img_walletconnect_24`
|
||||
3. `ill_bussiness`
|
||||
|
||||
## Attention
|
||||
|
||||
For complex vector images (named with `ill_name`), you should use `.png` resources, because when the project is compiled, all complex vectors are converted to large, heavy PNGs for different dimensions.
|
||||
107
.claude/rules/domain/core-components.md
Normal file
107
.claude/rules/domain/core-components.md
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
# Domain Components
|
||||
|
||||
Key domain mechanisms that orchestrate data flow: suppliers, fetchers, and use cases.
|
||||
|
||||
## Retrieving Core Models
|
||||
|
||||
### UserWallet
|
||||
|
||||
#### UserWalletsListRepository
|
||||
|
||||
**Location:** `domain/common` — `com.tangem.domain.common.wallets.UserWalletsListRepository`
|
||||
|
||||
Repository for managing user wallets list. Provides `StateFlow<List<UserWallet>?>` for the wallets list and `StateFlow<UserWallet?>` for the selected wallet. Supports loading, selecting, saving, locking/unlocking (biometric, access code), deleting, and reordering wallets.
|
||||
|
||||
### Account / AccountList
|
||||
|
||||
#### SingleAccountSupplier
|
||||
|
||||
**Location:** `domain/account` — `com.tangem.domain.account.supplier.SingleAccountSupplier`
|
||||
|
||||
Supplier that provides a single `Account` by `AccountId`. Has convenience methods `filterPaymentAccount` and `filterCryptoPortfolioAccount` to filter by account subtype.
|
||||
|
||||
#### SingleAccountListSupplier
|
||||
|
||||
**Location:** `domain/account` — `com.tangem.domain.account.supplier.SingleAccountListSupplier`
|
||||
|
||||
Supplier that provides an `AccountList` for a specific user wallet by `UserWalletId`.
|
||||
|
||||
#### MultiAccountListSupplier
|
||||
|
||||
**Location:** `domain/account` — `com.tangem.domain.account.supplier.MultiAccountListSupplier`
|
||||
|
||||
Supplier that provides a list of `AccountList`s for all user wallets. Extends `FlowCachingSupplier`.
|
||||
|
||||
#### SingleAccountListFetcher
|
||||
|
||||
**Location:** `domain/account` — `com.tangem.domain.account.fetcher.SingleAccountListFetcher`
|
||||
|
||||
Fetcher that fetches a list of accounts for a single wallet by `UserWalletId`. Extends `FlowFetcher`.
|
||||
|
||||
### AccountStatus / AccountStatusList
|
||||
|
||||
#### SingleAccountStatusSupplier
|
||||
|
||||
**Location:** `domain/account/status` — `com.tangem.domain.account.status.supplier.SingleAccountStatusSupplier`
|
||||
|
||||
Supplier that provides a single `AccountStatus` by account identifier. Extends `FlowCachingSupplier`.
|
||||
|
||||
#### SingleAccountStatusListSupplier
|
||||
|
||||
**Location:** `domain/account/status` — `com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier`
|
||||
|
||||
Same as `SingleAccountListSupplier` but provides `AccountStatusList` (accounts with balances) for a specific user wallet.
|
||||
|
||||
#### MultiAccountStatusListSupplier
|
||||
|
||||
**Location:** `domain/account/status` — `com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier`
|
||||
|
||||
Same as `MultiAccountListSupplier` but provides a list of `AccountStatusList`s for all user wallets.
|
||||
|
||||
### Network / NetworkStatus
|
||||
|
||||
#### SingleNetworkStatusSupplier
|
||||
|
||||
**Location:** `domain/networks` — `com.tangem.domain.networks.single.SingleNetworkStatusSupplier`
|
||||
|
||||
Supplier of `NetworkStatus` for a specific network and wallet. Extends `FlowCachingSupplier`.
|
||||
|
||||
#### MultiNetworkStatusSupplier
|
||||
|
||||
**Location:** `domain/networks` — `com.tangem.domain.networks.multi.MultiNetworkStatusSupplier`
|
||||
|
||||
Supplier of all `NetworkStatus`es (as `Set<NetworkStatus>`) for a selected wallet. Extends `FlowCachingSupplier`.
|
||||
|
||||
#### SingleNetworkStatusFetcher
|
||||
|
||||
**Location:** `domain/networks` — `com.tangem.domain.networks.single.SingleNetworkStatusFetcher`
|
||||
|
||||
Fetcher of network status for a single `Network` by `UserWalletId`. Extends `FlowFetcher`.
|
||||
|
||||
#### MultiNetworkStatusFetcher
|
||||
|
||||
**Location:** `domain/networks` — `com.tangem.domain.networks.multi.MultiNetworkStatusFetcher`
|
||||
|
||||
Fetcher of network statuses for a set of `Network`s for a multi-currency wallet by `UserWalletId`. Extends `FlowFetcher`.
|
||||
|
||||
## Updating Balances
|
||||
|
||||
### WalletBalanceFetcher
|
||||
|
||||
**Location:** `domain/tokens` — `com.tangem.domain.tokens.wallet.WalletBalanceFetcher`
|
||||
|
||||
Fetcher of wallet balances by `UserWalletId`. Selects the appropriate fetching strategy based on wallet type (multi-wallet, single wallet with tokens, single wallet). Delegates to `BalanceFetchingOperations` for shared fetching logic.
|
||||
|
||||
### CryptoCurrencyBalanceFetcher
|
||||
|
||||
**Location:** `domain/account/status` — `com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher`
|
||||
|
||||
Fetches and refreshes balances for specific crypto currencies. Uses per-wallet mutexes to allow concurrent refreshes for different wallets while preventing concurrent refreshes for the same wallet. Delegates to `BalanceFetchingOperations`.
|
||||
|
||||
## Managing Portfolio (User Tokens)
|
||||
|
||||
### ManageCryptoCurrenciesUseCase
|
||||
|
||||
**Location:** `domain/account/status` — `com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase`
|
||||
|
||||
Use case for adding and removing crypto currencies in an account.
|
||||
142
.claude/rules/domain/core-models.md
Normal file
142
.claude/rules/domain/core-models.md
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
# Domain Models
|
||||
|
||||
Core business models used across the application. Models are defined in `domain/models/` and `domain/account/`.
|
||||
|
||||
## StatusSource
|
||||
|
||||
**Location:** `domain/models` — `com.tangem.domain.models.StatusSource`
|
||||
|
||||
Enum representing data loading/refresh status. Used across all status models (NetworkStatus, QuoteStatus, YieldBalance, CryptoCurrencyStatus.Sources):
|
||||
- `CACHE` — initial status, data loaded from cache
|
||||
- `ACTUAL` — terminal status, data successfully fetched from server
|
||||
- `ONLY_CACHE` — terminal status, data could not be refreshed (only cached data available)
|
||||
|
||||
## CryptoCurrency
|
||||
|
||||
**Location:** `domain/models` — `com.tangem.domain.models.currency.CryptoCurrency`
|
||||
|
||||
Sealed class representing a cryptocurrency — either a native coin (`Coin`) or a token (`Token`). Used throughout the application: portfolio, token search, swaps, buy/sell, staking, etc.
|
||||
|
||||
## CryptoCurrencyStatus
|
||||
|
||||
**Location:** `domain/models` — `com.tangem.domain.models.currency.CryptoCurrencyStatus`
|
||||
|
||||
Model representing a currency with its balance state. Primarily used to display user's coin balance in the portfolio. Wraps `CryptoCurrency` with a `Value` sealed interface:
|
||||
|
||||
| Value subtype | Description |
|
||||
|---|---|
|
||||
| `Loading` | First-time fetch; once data is loaded, subsequent updates use cache via StatusSource, bypassing Loading |
|
||||
| `Loaded` | Full data available |
|
||||
| `Custom` | Custom token in portfolio; some data may be missing (e.g., no balance if backend has no quotes for it) |
|
||||
| `NoQuote` | Balance known, no price data |
|
||||
| `NoAccount` | Account not created (e.g., Solana reserve) |
|
||||
| `Unreachable` | Network error |
|
||||
| `NoAmount` | Coin is added to portfolio but no blockchain data available for it |
|
||||
| `MissedDerivation` | Coin has no derivations — failed to obtain a blockchain network address |
|
||||
|
||||
All Value subtypes carry `sources: Sources` tracking data freshness per dimension: `networkSource`, `quoteSource`, `stakingBalanceSource`, and aggregated `total`.
|
||||
|
||||
## Network
|
||||
|
||||
**Location:** `domain/models` — `com.tangem.domain.models.network.Network`
|
||||
|
||||
Represents a blockchain network (e.g., Ethereum, Bitcoin). Contains network metadata: ID, name, currency symbol, derivation path, standard type (ERC20, TRC20, BEP20, etc.), and capabilities (token support, transaction extras, name resolving).
|
||||
|
||||
## NetworkStatus
|
||||
|
||||
**Location:** `domain/models` — `com.tangem.domain.models.network.NetworkStatus`
|
||||
|
||||
Blockchain balances for all tokens of a network at a specific address. Only `Verified` and `NoAccount` are cached.
|
||||
|
||||
| Value subtype | Description |
|
||||
|---|---|
|
||||
| `Verified` | Successful response from blockchain |
|
||||
| `Unreachable` | Failed response from blockchain |
|
||||
| `NoAccount` | Blockchain-specific status for chains that require a deposit to an address before it can be used |
|
||||
| `MissedDerivation` | Derivation failed — no blockchain network address |
|
||||
|
||||
## QuoteStatus
|
||||
|
||||
**Location:** `domain/models` — `com.tangem.domain.models.quote.QuoteStatus`
|
||||
|
||||
Exchange rate between the app's selected fiat currency and a coin's currency.
|
||||
|
||||
## YieldBalance
|
||||
|
||||
**Location:** `domain/models` — `com.tangem.domain.models.staking.YieldBalance`
|
||||
|
||||
Staking yield balance for a specific `StakingID` (integrationId + address).
|
||||
|
||||
## TotalFiatBalance
|
||||
|
||||
**Location:** `domain/models` — `com.tangem.domain.models.TotalFiatBalance`
|
||||
|
||||
Aggregate fiat balance across all tokens. Sealed interface with three states: `Loading`, `Failed`, `Loaded(amount, source)`.
|
||||
|
||||
## TokenList
|
||||
|
||||
**Location:** `domain/models` — `com.tangem.domain.models.tokenlist.TokenList`
|
||||
|
||||
List of cryptocurrency tokens for display in portfolio. Sealed interface with subtypes:
|
||||
- `GroupedByNetwork` — tokens grouped by `Network`, each group contains a list of `CryptoCurrencyStatus`
|
||||
- `Ungrouped` — flat list of `CryptoCurrencyStatus`
|
||||
- `Empty` — no tokens
|
||||
|
||||
All subtypes carry `totalFiatBalance: TotalFiatBalance` and `sortedBy: TokensSortType`.
|
||||
|
||||
## Account
|
||||
|
||||
**Location:** `domain/models` — `com.tangem.domain.models.account.Account`
|
||||
|
||||
Model representing a user account. Subtypes:
|
||||
- **`Account.CryptoPortfolio`** — crypto portfolio with coins. All tokens in the account share the account's derivation (main account is an exception). Has a `DerivationIndex`: `0` for main account, `1..19` for secondary
|
||||
- **`Account.Payment`** — account for Visa card integration
|
||||
|
||||
## AccountStatus
|
||||
|
||||
**Location:** `domain/models` — `com.tangem.domain.models.account.AccountStatus`
|
||||
|
||||
Model representing an account with balances. Has a similar structure to `Account`: `CryptoPortfolio` and `Payment` subtypes.
|
||||
|
||||
## AccountList
|
||||
|
||||
**Location:** `domain/account` — `com.tangem.domain.account.models.AccountList`
|
||||
|
||||
List of all accounts for a user wallet (`UserWallet`).
|
||||
|
||||
Business rules (enforced by factory returning `Either<Error, AccountList>`):
|
||||
- Accounts list cannot be empty
|
||||
- Exactly 1 main account
|
||||
- Max 20 active accounts (`MAX_ACCOUNTS_COUNT`), max 1000 archived
|
||||
- No duplicate AccountIds or custom AccountNames
|
||||
- `totalAccounts >= activeAccounts`
|
||||
|
||||
## AccountStatusList
|
||||
|
||||
**Location:** `domain/account` — `com.tangem.domain.account.models.AccountStatusList`
|
||||
|
||||
Same as `AccountList` but with balances (wraps `AccountStatus` instead of `Account`).
|
||||
|
||||
## UserWallet
|
||||
|
||||
**Location:** `domain/models` — `com.tangem.domain.models.wallet.UserWallet`
|
||||
|
||||
Top-level model representing a user's wallet stored in the app. Subtypes:
|
||||
- **`Cold`** — wallet backed by a physical Tangem card (NFC). Contains `ScanResponse`, card info, backup state
|
||||
- **`Hot`** — software (hot) wallet without a physical card
|
||||
|
||||
## Model Hierarchy
|
||||
|
||||
```
|
||||
UserWallet
|
||||
└─ AccountList / AccountStatusList
|
||||
└─ Account.CryptoPortfolio / AccountStatus.CryptoPortfolio
|
||||
├─ CryptoCurrency (Coin | Token)
|
||||
│ └─ CryptoCurrencyStatus (currency + Value state)
|
||||
│ ├─ built from NetworkStatus (per network)
|
||||
│ ├─ built from QuoteStatus (per rawCurrencyId)
|
||||
│ └─ built from YieldBalance (per stakingId)
|
||||
├─ AccountId (SHA-256 hash)
|
||||
├─ DerivationIndex (0 = main)
|
||||
└─ CryptoPortfolioIcon (icon + color)
|
||||
```
|
||||
22
.claude/rules/git-rules.md
Normal file
22
.claude/rules/git-rules.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Git Rules
|
||||
|
||||
## Branch Naming
|
||||
|
||||
| Type | Format | Example |
|
||||
|------|--------|---------|
|
||||
| Feature | `feature/AND-xxx_short_description` | `feature/AND-13391_balance_fetcher` |
|
||||
| Bugfix | `bugfix/AND-xxx_short_description` | `bugfix/AND-14000_fix_crash` |
|
||||
| Pre-release | `x.x_pre_release` | `5.36_pre_release` |
|
||||
|
||||
**Key branches:**
|
||||
- `develop` — main integration branch, all feature/bugfix branches merge here
|
||||
- `x.x_pre_release` — branched from `develop` on the last day of sprint for the upcoming release; receives regression bugfixes and additional release items
|
||||
- `release` — merging into this branch triggers appTester build and production artifacts; PRs come from `x.x_pre_release`
|
||||
|
||||
## Commit Messages
|
||||
|
||||
Format: `AND-xxx Description`
|
||||
|
||||
- Start with the Jira task number (AND-xxx)
|
||||
- Followed by a space and a short description in English
|
||||
- Example: `[REDACTED_TASK_KEY] Finalize CryptoCurrencyBalanceFetcher refactoring`
|
||||
16
.claude/rules/tangem-sdk.md
Normal file
16
.claude/rules/tangem-sdk.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Tangem SDK & Libraries
|
||||
|
||||
## In-house SDKs (via `tangem_dependencies.toml`)
|
||||
|
||||
- **Blockchain SDK** (`com.tangem:blockchain`) — multichain SDK for working with blockchains: creating/signing transactions, fetching balances, managing addresses. Wrapped in `libs/blockchain-sdk/`
|
||||
- **Card SDK** (`com.tangem.tangem-sdk-kotlin:core`, `:android`) — SDK for interacting with physical Tangem cards via NFC: scanning, wallet creation, key derivation, passcode management, backup. Wrapped in `libs/tangem-sdk-api/`
|
||||
- **Hot SDK** (`com.tangem.tangem-hot-sdk-kotlin:core`, `:android`) — SDK for hot (software) wallets
|
||||
- **Vico** (`com.tangem.vico`) — forked charting library Vico, adapted for project needs
|
||||
|
||||
## Wrapper Modules (`libs/`)
|
||||
|
||||
- `libs/blockchain-sdk/` — wrapper around Blockchain SDK, provides domain-level abstractions for blockchain operations
|
||||
- `libs/tangem-sdk-api/` — wrapper around Card SDK, exposes NFC card interaction API to the app
|
||||
- `libs/crypto/` — cryptographic utilities: derivation, address handling, blockchain-specific helpers
|
||||
- `libs/auth/` — API key provider interfaces for external services (Express, StakeKit)
|
||||
- `libs/visa/` — Visa integration: smart contracts, limits, balances via Web3j
|
||||
114
CLAUDE.md
Normal file
114
CLAUDE.md
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Build & Test Commands
|
||||
|
||||
```bash
|
||||
# Build debug APK (Google flavor)
|
||||
./gradlew :app:assembleGoogleDebug
|
||||
|
||||
# Run all unit tests (debug/googleDebug variants + JVM modules)
|
||||
./gradlew unitTest
|
||||
|
||||
# Run tests for a single module
|
||||
./gradlew :features:wallet:impl:testDebugUnitTest # Android library module
|
||||
./gradlew :app:testGoogleDebugUnitTest # App module
|
||||
./gradlew :domain:tokens:test # Pure JVM module
|
||||
|
||||
# Run a single test class
|
||||
./gradlew :core:ui:testDebugUnitTest --tests "com.tangem.core.ui.format.BigDecimalCryptoFormatTest"
|
||||
|
||||
# Detekt (static analysis) — runs automatically via convention plugin on applicable modules
|
||||
./gradlew detekt detektMain
|
||||
|
||||
# Build UI tests APKs (for Marathon)
|
||||
./gradlew :app:assembleGoogleMocked :app:assembleGoogleMockedAndroidTest
|
||||
# 1. :app:assembleGoogleMocked — main APK (mocked build)
|
||||
# 2. :app:assembleGoogleMockedAndroidTest — test APK with instrumented tests
|
||||
```
|
||||
|
||||
**Product flavors:** `google` and `huawei` (dimension: `service`). Default development flavor is `google`.
|
||||
|
||||
**Build types:** `debug`, `mocked`, `internal`, `external`, `release`.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Module Layers
|
||||
|
||||
The project is a heavily modularized Android app (~220 modules) organized in layers:
|
||||
|
||||
- **`app/`** — Application entry point, Hilt setup, navigation root
|
||||
- **`domain/`** — Business logic and models. Each domain area (e.g., `tokens`, `wallets`, `card`) has a `models` submodule for pure data types and a core module for use cases
|
||||
- **`data/`** — Repository implementations and data sources, mirrors domain structure
|
||||
- **`features/`** — UI features using **API/Impl split pattern**: `features:foo:api` defines the public contract, `features:foo:impl` contains the implementation. This enforces clean dependency boundaries
|
||||
- **`core/`** — Cross-cutting concerns: `ui`, `analytics`, `datasource`, `decompose`, `navigation`, `res`, `utils`, `security`, `pagination`
|
||||
- **`common/`** — Shared models, routing, UI components, test utilities
|
||||
- **`libs/`** — SDK wrappers: `blockchain-sdk`, `tangem-sdk-api`, `crypto`, `auth`, `visa`
|
||||
|
||||
### Component Architecture (Decompose)
|
||||
|
||||
The app uses [Decompose](https://github.com/arkivanov/Decompose) for lifecycle-aware components. Every feature screen follows this structure:
|
||||
|
||||
**API module** (`features/{name}/api/`):
|
||||
- `{Name}Component` interface implementing `ComposableContentComponent`
|
||||
- Inner `Params` data class for input parameters
|
||||
- Inner `Factory` interface: `fun create(context: AppComponentContext, params: Params): {Name}Component`
|
||||
|
||||
**Impl module** (`features/{name}/impl/`):
|
||||
- `Default{Name}Component` with `@AssistedInject` constructor taking `@Assisted appComponentContext: AppComponentContext` and `@Assisted params`
|
||||
- Delegates `AppComponentContext by appComponentContext`
|
||||
- Creates model via `getOrCreateModel(params)`
|
||||
- `@Composable Content(modifier)` collects model state via `collectAsStateWithLifecycle()`
|
||||
- Inner `@AssistedFactory` interface extending the public `Factory`
|
||||
|
||||
**Model** (`features/{name}/impl/.../model/`):
|
||||
- `{Name}Model` extending `Model` base class, annotated `@ModelScoped`, uses `@Inject` constructor
|
||||
- Receives params via `ParamsContainer.require<ParamsType>()`
|
||||
- Exposes `StateFlow<{Name}UM>` (UM = UI Model, state class in `ui/state/` subpackage)
|
||||
- Has `modelScope` (SupervisorJob + mainImmediate), auto-cancelled on destroy
|
||||
|
||||
**Child navigation within features:**
|
||||
- `childStack()` — stacked screen navigation (back stack)
|
||||
- `childSlot()` — optional overlays/bottom sheets (single or no child)
|
||||
- `InnerRouter` — feature-internal navigation that delegates unknown routes to parent router
|
||||
|
||||
### Feature Package Conventions
|
||||
|
||||
- API package: `com.tangem.features.{name}.api` (plural `features`)
|
||||
- Impl package: `com.tangem.feature.{name}.impl` (singular `feature` — legacy inconsistency, follow existing pattern per feature)
|
||||
- Component: `{Name}Component` (api), `Default{Name}Component` (impl)
|
||||
- Model: `{Name}Model` in `model/` subpackage
|
||||
- UI State: `{Name}UM` in `ui/state/` subpackage
|
||||
- UI Composable: in `ui/` subpackage
|
||||
|
||||
### Key Frameworks & Patterns
|
||||
|
||||
- **DI:** Hilt with `@SingletonComponent` scope and custom `@ModelScoped` scope for model-lifecycle dependencies
|
||||
- **UI:** Jetpack Compose with Material3. Image loading via Coil
|
||||
- **Navigation:** Custom `AppRouter` + `AppRoute` sealed classes with deep link support via `DeepLinkBuilder`
|
||||
- **Networking:** Retrofit + Moshi for API communication
|
||||
- **Local storage:** `AppPreferencesStore` for key-value pairs, `DataStore` for larger data
|
||||
- **Async:** Kotlin Coroutines + Flow. Inject `CoroutineDispatcherProvider` (from `core/utils`) instead of using `Dispatchers.*` directly — provides `main`, `mainImmediate`, `io`, `default`, `single`
|
||||
- **Error handling:** Arrow's `Either<Error, Success>` pattern throughout domain/data layers. `DataError` sealed hierarchy for domain errors. See `domain/core/CLAUDE.md` for the LCE pattern
|
||||
- **Analytics:** `AnalyticsEvent(category, event, params)` in `core/analytics/models/`. Feature events are sealed class hierarchies extending `AnalyticsEvent`. Send via injected `AnalyticsEventHandler`
|
||||
- **Feature toggles:** `FeatureTogglesManager` in `core/config-toggles/`. Toggles are defined in `core/config-toggles/src/main/assets/configs/feature_toggles_config.json` and auto-generated into a `FeatureToggles` enum by the convention plugin at build time. Each feature module exposes its own `XxxFeatureToggles` interface (in `api/`) with a `DefaultXxxFeatureToggles` implementation (in `impl/`) that delegates to `FeatureTogglesManager`
|
||||
- **Supported languages:** `SupportedLanguages` in `core/utils/` defines the app's supported locales: en, ru, de, fr, it, ja, uk, zh, es. `getCurrentSupportedLanguageCode()` returns the device locale if supported, otherwise falls back to English. Used by API calls that accept a language parameter
|
||||
|
||||
### Build System
|
||||
|
||||
- **Gradle 8.14.1**, AGP 8.10.1, Kotlin 2.1.10
|
||||
- **Version catalogs:** `gradle/dependencies.toml` (external/third-party dependencies) and `gradle/tangem_dependencies.toml` (in-house Tangem SDK dependencies)
|
||||
- **Convention plugin:** `plugins/configuration/` — applies Detekt, configures test settings, generates environment configs and feature toggles
|
||||
- **Custom Detekt rules:** `plugins/detekt-rules/`. Detekt configuration is in the `tangem-android-tools` git submodule. Key rule: `UnsafeStringResourceUsage` — prevents direct `stringResource()` / `pluralStringResource()` calls; use the `Safe`-suffixed variants instead
|
||||
- **Localization:** Managed via [Lokalise](https://lokalise.com). Update strings by running `python3 lokalize.py`
|
||||
- **GitHub Packages auth:** Requires `gpr.user` and `gpr.key` in `local.properties` for Tangem SDK dependencies
|
||||
|
||||
### Testing
|
||||
|
||||
- **JUnit 5** (Jupiter) for unit tests
|
||||
- **MockK** for mocking
|
||||
- **Turbine** for Flow testing
|
||||
- **Truth** for assertions
|
||||
- **Marathon** for UI tests (emulator-based, configured via `Marathonfile`)
|
||||
- Shared test utilities in `common:test` and `test/core/`
|
||||
|
|
@ -41,7 +41,7 @@ import javax.inject.Singleton
|
|||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object UserWalletsListManagerModule {
|
||||
internal object UserWalletsListRepositoryModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
30
core/datasource/CLAUDE.md
Normal file
30
core/datasource/CLAUDE.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# core/datasource
|
||||
|
||||
## API Integration Guide
|
||||
|
||||
### Config Structure
|
||||
|
||||
- `ApiConfig` — base API config with `id` (`ApiConfig.ID`), `defaultEnvironment` (`ApiEnvironment`), and `environmentConfigs` (list of `ApiEnvironmentConfig`)
|
||||
- `ApiEnvironmentConfig` — per-environment settings: `environment`, `baseUrl`, and `headers` (map of header name to `Provider<String>`)
|
||||
|
||||
### Config Management
|
||||
|
||||
- `ApiConfigsManager` — DI-available component for accessing configs via `getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig`
|
||||
- Two implementations: `ProdApiConfigsManager` (release) and `DevApiConfigsManager` (extends `MutableApiConfigsManager`, used when `BuildConfig.TESTER_MENU_ENABLED`)
|
||||
- `MutableApiConfigsManager` allows runtime environment switching via Tester Menu without app restart
|
||||
|
||||
### Adding a New API
|
||||
|
||||
1. Create `ApiConfig` subclass in `com.tangem.datasource.api.common.config` — override `defaultEnvironment` and `environmentConfigs`. DI dependencies can be injected via constructor
|
||||
2. Register the new config ID in `ApiConfig.initializeId(...)`
|
||||
3. Provide the config in `ApiConfigsModule` using `@Provides @IntoSet`
|
||||
4. Provide the API Retrofit service in `NetworkModule`:
|
||||
- Get environment config: `apiConfigsManager.getEnvironmentConfig(id)`
|
||||
- Use `environmentConfig.baseUrl` for Retrofit base URL
|
||||
- Apply headers via `OkHttpClient.Builder().applyApiConfig(id, apiConfigsManager)`
|
||||
|
||||
### Testing
|
||||
|
||||
- Add the new config to `API_CONFIGS` list in `ProdApiConfigsManagerTest`
|
||||
- Add a test model in the `data` method with expected `ApiEnvironmentConfig` values
|
||||
- If the config has constructor dependencies, mock them and set up behavior in `setup()`
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
package com.tangem.datasource.local.config.environment.converter
|
||||
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.datasource.local.config.environment.models.EnvironmentConfigModel
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converts [EnvironmentConfigModel] to [BlockchainSdkConfig]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object BlockchainSDKConfigConverter : Converter<EnvironmentConfigModel, BlockchainSdkConfig> {
|
||||
|
||||
override fun convert(value: EnvironmentConfigModel): BlockchainSdkConfig {
|
||||
return BlockchainSdkConfig(
|
||||
blockchairCredentials = BlockchairCredentials(
|
||||
apiKey = value.blockchairApiKeys,
|
||||
authToken = value.blockchairAuthorizationToken,
|
||||
),
|
||||
blockcypherTokens = value.blockcypherTokens,
|
||||
quickNodeSolanaCredentials = QuickNodeCredentials(
|
||||
apiKey = value.quiknodeApiKey,
|
||||
subdomain = value.quiknodeSubdomain,
|
||||
),
|
||||
quickNodeBscCredentials = QuickNodeCredentials(
|
||||
apiKey = value.bscQuiknodeApiKey,
|
||||
subdomain = value.bscQuiknodeSubdomain,
|
||||
),
|
||||
quickNodePlasmaCredentials = QuickNodeCredentials(
|
||||
apiKey = value.quiknodePlasmaApiKey,
|
||||
subdomain = value.quiknodePlasmaSubdomain,
|
||||
),
|
||||
quickNodeMonadCredentials = QuickNodeCredentials(
|
||||
apiKey = value.quiknodeMonadApiKey,
|
||||
subdomain = value.quiknodeMonadSubdomain,
|
||||
),
|
||||
infuraProjectId = value.infuraProjectId,
|
||||
tronGridApiKey = value.tronGridApiKey,
|
||||
nowNodeCredentials = NowNodeCredentials(value.nowNodesApiKey),
|
||||
getBlockCredentials = createGetBlockCredentials(value),
|
||||
kaspaSecondaryApiUrl = value.kaspaSecondaryApiUrl,
|
||||
tonCenterCredentials = TonCenterCredentials(
|
||||
mainnetApiKey = value.tonCenterKeys.mainnet,
|
||||
testnetApiKey = value.tonCenterKeys.testnet,
|
||||
),
|
||||
chiaFireAcademyApiKey = value.chiaFireAcademyApiKey,
|
||||
chiaTangemApiKey = value.chiaTangemApiKey,
|
||||
hederaArkhiaApiKey = value.hederaArkhiaKey,
|
||||
polygonScanApiKey = value.polygonScanApiKey,
|
||||
bittensorDwellirApiKey = value.bittensorDwellirApiKey,
|
||||
bittensorOnfinalityApiKey = value.bittensorOnfinalityKey,
|
||||
dwellirApiKey = value.dwellirApiKey,
|
||||
koinosProApiKey = value.koinosProApiKey,
|
||||
alephiumApiKey = value.alephiumTangemApiKey,
|
||||
moralisApiKey = value.moralisApiKey,
|
||||
etherscanApiKey = value.etherScanApiKey,
|
||||
blinkApiKey = value.blinkApiKey,
|
||||
tatumApiKey = value.tatumApiKey,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createGetBlockCredentials(configValues: EnvironmentConfigModel): GetBlockCredentials? {
|
||||
return configValues.getBlockAccessTokens?.let { accessTokens ->
|
||||
GetBlockCredentials(
|
||||
xrp = GetBlockAccessToken(jsonRpc = accessTokens.xrp?.jsonRPC),
|
||||
cardano = GetBlockAccessToken(rosetta = accessTokens.cardano?.rosetta),
|
||||
avalanche = GetBlockAccessToken(jsonRpc = accessTokens.avalanche?.jsonRPC),
|
||||
eth = GetBlockAccessToken(jsonRpc = accessTokens.eth?.jsonRPC),
|
||||
etc = GetBlockAccessToken(jsonRpc = accessTokens.etc?.jsonRPC),
|
||||
fantom = GetBlockAccessToken(jsonRpc = accessTokens.fantom?.jsonRPC),
|
||||
rsk = GetBlockAccessToken(jsonRpc = accessTokens.rsk?.jsonRPC),
|
||||
bsc = GetBlockAccessToken(jsonRpc = accessTokens.bsc?.jsonRPC),
|
||||
polygon = GetBlockAccessToken(jsonRpc = accessTokens.polygon?.jsonRPC),
|
||||
gnosis = GetBlockAccessToken(jsonRpc = accessTokens.gnosis?.jsonRPC),
|
||||
cronos = GetBlockAccessToken(jsonRpc = accessTokens.cronos?.jsonRPC),
|
||||
solana = GetBlockAccessToken(jsonRpc = accessTokens.solana?.jsonRPC),
|
||||
ton = GetBlockAccessToken(jsonRpc = accessTokens.ton?.jsonRPC),
|
||||
tron = GetBlockAccessToken(rest = accessTokens.tron?.rest),
|
||||
cosmos = GetBlockAccessToken(rest = accessTokens.cosmos?.rest),
|
||||
near = GetBlockAccessToken(jsonRpc = accessTokens.near?.jsonRPC),
|
||||
aptos = GetBlockAccessToken(rest = accessTokens.aptos?.rest),
|
||||
dogecoin = GetBlockAccessToken(
|
||||
jsonRpc = accessTokens.dogecoin?.jsonRPC,
|
||||
blockBookRest = accessTokens.dogecoin?.blockBookRest,
|
||||
),
|
||||
litecoin = GetBlockAccessToken(
|
||||
jsonRpc = accessTokens.litecoin?.jsonRPC,
|
||||
blockBookRest = accessTokens.litecoin?.blockBookRest,
|
||||
),
|
||||
dash = GetBlockAccessToken(
|
||||
jsonRpc = accessTokens.dash?.jsonRPC,
|
||||
blockBookRest = accessTokens.dash?.blockBookRest,
|
||||
),
|
||||
bitcoin = GetBlockAccessToken(
|
||||
jsonRpc = accessTokens.bitcoin?.jsonRPC,
|
||||
blockBookRest = accessTokens.bitcoin?.blockBookRest,
|
||||
),
|
||||
algorand = GetBlockAccessToken(rest = accessTokens.algorand?.rest),
|
||||
zkSyncEra = GetBlockAccessToken(jsonRpc = accessTokens.zksync?.jsonRPC),
|
||||
polygonZkEvm = GetBlockAccessToken(jsonRpc = accessTokens.polygonZkevm?.jsonRPC),
|
||||
base = GetBlockAccessToken(jsonRpc = accessTokens.base?.jsonRPC),
|
||||
blast = GetBlockAccessToken(jsonRpc = accessTokens.blast?.jsonRPC),
|
||||
filecoin = GetBlockAccessToken(jsonRpc = accessTokens.filecoin?.jsonRPC),
|
||||
arbitrum = GetBlockAccessToken(jsonRpc = accessTokens.arbitrum?.jsonRPC),
|
||||
bitcoinCash = GetBlockAccessToken(
|
||||
jsonRpc = accessTokens.bitcoinCash?.jsonRPC,
|
||||
blockBookRest = accessTokens.bitcoinCash?.blockBookRest,
|
||||
),
|
||||
kusama = GetBlockAccessToken(jsonRpc = accessTokens.kusama?.jsonRPC),
|
||||
moonbeam = GetBlockAccessToken(jsonRpc = accessTokens.moonbeam?.jsonRPC),
|
||||
optimism = GetBlockAccessToken(jsonRpc = accessTokens.optimism?.jsonRPC),
|
||||
polkadot = GetBlockAccessToken(jsonRpc = accessTokens.polkadot?.jsonRPC),
|
||||
shibarium = GetBlockAccessToken(jsonRpc = accessTokens.shibarium?.jsonRPC),
|
||||
sui = GetBlockAccessToken(jsonRpc = accessTokens.sui?.jsonRPC),
|
||||
telos = GetBlockAccessToken(jsonRpc = accessTokens.telos?.jsonRPC),
|
||||
tezos = GetBlockAccessToken(rest = accessTokens.tezos?.rest),
|
||||
monad = GetBlockAccessToken(rest = accessTokens.monad?.rest),
|
||||
stellar = GetBlockAccessToken(rest = accessTokens.stellar?.rest),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
package com.tangem.datasource.local.config.environment.converter
|
||||
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||
import com.tangem.datasource.local.config.environment.models.EnvironmentConfigModel
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converter from [EnvironmentConfigModel] to [EnvironmentConfig]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object EnvironmentConfigConverter : Converter<EnvironmentConfigModel, EnvironmentConfig> {
|
||||
|
||||
override fun convert(value: EnvironmentConfigModel): EnvironmentConfig {
|
||||
return EnvironmentConfig(
|
||||
moonPayApiKey = value.moonPayApiKey,
|
||||
moonPayApiSecretKey = value.moonPayApiSecretKey,
|
||||
mercuryoWidgetId = value.mercuryoWidgetId,
|
||||
mercuryoSecret = value.mercuryoSecret,
|
||||
blockchainSdkConfig = BlockchainSDKConfigConverter.convert(value = value),
|
||||
amplitudeApiKey = value.amplitudeApiKey,
|
||||
appsFlyerApiKey = value.appsFlyer.appsFlyerDevKey,
|
||||
appsAppId = value.appsFlyer.appsFlyerAppID,
|
||||
walletConnectProjectId = value.walletConnectProjectId,
|
||||
express = value.express,
|
||||
devExpress = value.devExpress,
|
||||
stakeKitApiKey = value.stakeKitApiKey,
|
||||
p2pApiKey = value.p2pApiKey,
|
||||
blockAidApiKey = value.blockaidApiKey,
|
||||
tangemApiKey = value.tangemApiKey,
|
||||
tangemApiKeyDev = value.tangemApiKeyDev,
|
||||
tangemApiKeyStage = value.tangemApiKeyStage,
|
||||
yieldModuleApiKey = value.yieldModuleApiKey,
|
||||
yieldModuleApiKeyDev = value.yieldModuleApiKeyDev,
|
||||
bffStaticToken = value.bffStaticToken,
|
||||
bffStaticTokenDev = value.bffStaticTokenDev,
|
||||
gaslessTxApiKeyDev = value.gaslessTxApiKeyDev,
|
||||
gaslessTxApiKey = value.gaslessTxApiKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
package com.tangem.datasource.local.config.environment.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@JsonClass(generateAdapter = true)
|
||||
class EnvironmentConfigModel(
|
||||
@Json(name = "mercuryoWidgetId") val mercuryoWidgetId: String,
|
||||
@Json(name = "mercuryoSecret") val mercuryoSecret: String,
|
||||
@Json(name = "moonPayApiKey") val moonPayApiKey: String,
|
||||
@Json(name = "moonPayApiSecretKey") val moonPayApiSecretKey: String,
|
||||
@Json(name = "blockchairApiKeys") val blockchairApiKeys: List<String>,
|
||||
@Json(name = "blockchairAuthorizationToken") val blockchairAuthorizationToken: String?,
|
||||
@Json(name = "quiknodeSubdomain") val quiknodeSubdomain: String,
|
||||
@Json(name = "quiknodeApiKey") val quiknodeApiKey: String,
|
||||
@Json(name = "bscQuiknodeSubdomain") val bscQuiknodeSubdomain: String,
|
||||
@Json(name = "bscQuiknodeApiKey") val bscQuiknodeApiKey: String,
|
||||
@Json(name = "quiknodePlasmaSubdomain") val quiknodePlasmaSubdomain: String,
|
||||
@Json(name = "quiknodePlasmaApiKey") val quiknodePlasmaApiKey: String,
|
||||
@Json(name = "quiknodeMonadSubdomain") val quiknodeMonadSubdomain: String,
|
||||
@Json(name = "quiknodeMonadApiKey") val quiknodeMonadApiKey: String,
|
||||
@Json(name = "nowNodesApiKey") val nowNodesApiKey: String,
|
||||
@Json(name = "getBlockAccessTokens") val getBlockAccessTokens: GetBlockAccessTokens?,
|
||||
@Json(name = "tonCenterApiKey") val tonCenterKeys: TonCenterKeys,
|
||||
@Json(name = "blockcypherTokens") val blockcypherTokens: Set<String>?,
|
||||
@Json(name = "infuraProjectId") val infuraProjectId: String?,
|
||||
@Json(name = "tronGridApiKey") val tronGridApiKey: String,
|
||||
@Json(name = "amplitudeApiKey") val amplitudeApiKey: String,
|
||||
@Json(name = "appsFlyer") val appsFlyer: AppsFlyerModel,
|
||||
@Json(name = "kaspaSecondaryApiUrl") val kaspaSecondaryApiUrl: String,
|
||||
@Json(name = "walletConnectProjectId") val walletConnectProjectId: String,
|
||||
@Json(name = "chiaFireAcademyApiKey") val chiaFireAcademyApiKey: String?,
|
||||
@Json(name = "chiaTangemApiKey") val chiaTangemApiKey: String?,
|
||||
@Json(name = "devExpress") val devExpress: ExpressModel?,
|
||||
@Json(name = "express") val express: ExpressModel?,
|
||||
@Json(name = "hederaArkhiaKey") val hederaArkhiaKey: String?,
|
||||
@Json(name = "polygonScanApiKey") val polygonScanApiKey: String?,
|
||||
@Json(name = "stakeKitApiKey") val stakeKitApiKey: String?,
|
||||
@Json(name = "p2pApiKey") val p2pApiKey: P2PKeys?,
|
||||
@Json(name = "bittensorDwellirKey") val bittensorDwellirApiKey: String?,
|
||||
@Json(name = "bittensorOnfinalityKey") val bittensorOnfinalityKey: String?,
|
||||
@Json(name = "dwellirApiKey") val dwellirApiKey: String?,
|
||||
@Json(name = "koinosProApiKey") val koinosProApiKey: String?,
|
||||
@Json(name = "alephiumTangemApiKey") val alephiumTangemApiKey: String?,
|
||||
@Json(name = "moralisApiKey") val moralisApiKey: String?,
|
||||
@Json(name = "nftScanApiKey") val nftScanApiKey: String?,
|
||||
@Json(name = "blockaidApiKey") val blockaidApiKey: String?,
|
||||
@Json(name = "tangemApiKey") val tangemApiKey: String?,
|
||||
@Json(name = "tangemApiKeyDev") val tangemApiKeyDev: String?,
|
||||
@Json(name = "tangemApiKeyStage") val tangemApiKeyStage: String?,
|
||||
@Json(name = "etherscanApiKey") val etherScanApiKey: String?,
|
||||
@Json(name = "yieldModuleApiKey") val yieldModuleApiKey: String?,
|
||||
@Json(name = "yieldModuleApiKeyDev") val yieldModuleApiKeyDev: String?,
|
||||
@Json(name = "blinkApiKey") val blinkApiKey: String?,
|
||||
@Json(name = "tatumApiKey") val tatumApiKey: String?,
|
||||
@Json(name = "bffStaticToken") val bffStaticToken: String?,
|
||||
@Json(name = "bffStaticTokenDev") val bffStaticTokenDev: String?,
|
||||
@Json(name = "gaslessTxApiKeyDev") val gaslessTxApiKeyDev: String?,
|
||||
@Json(name = "gaslessTxApiKey") val gaslessTxApiKey: String?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetBlockAccessTokens(
|
||||
@Json(name = "xrp") val xrp: GetBlockToken?,
|
||||
@Json(name = "cardano") val cardano: GetBlockToken?,
|
||||
@Json(name = "avalanche") val avalanche: GetBlockToken?,
|
||||
@Json(name = "ethereum") val eth: GetBlockToken?,
|
||||
@Json(name = "ethereumClassic") val etc: GetBlockToken?,
|
||||
@Json(name = "fantom") val fantom: GetBlockToken?,
|
||||
@Json(name = "rsk") val rsk: GetBlockToken?,
|
||||
@Json(name = "bsc") val bsc: GetBlockToken?,
|
||||
@Json(name = "polygon") val polygon: GetBlockToken?,
|
||||
@Json(name = "xdai") val gnosis: GetBlockToken?,
|
||||
@Json(name = "cronos") val cronos: GetBlockToken?,
|
||||
@Json(name = "solana") val solana: GetBlockToken?,
|
||||
@Json(name = "ton") val ton: GetBlockToken?,
|
||||
@Json(name = "tron") val tron: GetBlockToken?,
|
||||
@Json(name = "cosmos-hub") val cosmos: GetBlockToken?,
|
||||
@Json(name = "near") val near: GetBlockToken?,
|
||||
@Json(name = "aptos") val aptos: GetBlockToken?,
|
||||
@Json(name = "dogecoin") val dogecoin: GetBlockToken?,
|
||||
@Json(name = "litecoin") val litecoin: GetBlockToken?,
|
||||
@Json(name = "dash") val dash: GetBlockToken?,
|
||||
@Json(name = "bitcoin") val bitcoin: GetBlockToken?,
|
||||
@Json(name = "algorand") val algorand: GetBlockToken?,
|
||||
@Json(name = "polygon-zkevm") val polygonZkevm: GetBlockToken?,
|
||||
@Json(name = "zksync") val zksync: GetBlockToken?,
|
||||
@Json(name = "base") val base: GetBlockToken?,
|
||||
@Json(name = "blast") val blast: GetBlockToken?,
|
||||
@Json(name = "filecoin") val filecoin: GetBlockToken?,
|
||||
@Json(name = "arbitrum-one") val arbitrum: GetBlockToken?,
|
||||
@Json(name = "bitcoinCash") val bitcoinCash: GetBlockToken?,
|
||||
@Json(name = "kusama") val kusama: GetBlockToken?,
|
||||
@Json(name = "moonbeam") val moonbeam: GetBlockToken?,
|
||||
@Json(name = "optimism") val optimism: GetBlockToken?,
|
||||
@Json(name = "polkadot") val polkadot: GetBlockToken?,
|
||||
@Json(name = "shibarium") val shibarium: GetBlockToken?,
|
||||
@Json(name = "sui") val sui: GetBlockToken?,
|
||||
@Json(name = "telos") val telos: GetBlockToken?,
|
||||
@Json(name = "tezos") val tezos: GetBlockToken?,
|
||||
@Json(name = "monad") val monad: GetBlockToken?,
|
||||
@Json(name = "stellar") val stellar: GetBlockToken?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TonCenterKeys(
|
||||
@Json(name = "mainnet") val mainnet: String,
|
||||
@Json(name = "testnet") val testnet: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PKeys(
|
||||
@Json(name = "mainnet") val mainnet: String,
|
||||
@Json(name = "hoodi") val hoodi: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetBlockToken(
|
||||
@Json(name = "jsonRpc") val jsonRPC: String?,
|
||||
@Json(name = "blockBookRest") val blockBookRest: String?,
|
||||
@Json(name = "rest") val rest: String?,
|
||||
@Json(name = "rosetta") val rosetta: String?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ExpressModel(
|
||||
@Json(name = "apiKey")
|
||||
val apiKey: String,
|
||||
@Json(name = "signVerifierPublicKey")
|
||||
val signVerifierPublicKey: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class AppsFlyerModel(
|
||||
@Json(name = "appsFlyerDevKey")
|
||||
val appsFlyerDevKey: String,
|
||||
@Json(name = "appsFlyerAppID")
|
||||
val appsFlyerAppID: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.datasource.local.config.environment.models
|
||||
|
||||
data class ExpressModel(val apiKey: String, val signVerifierPublicKey: String)
|
||||
|
||||
data class P2PKeys(val mainnet: String, val hoodi: String)
|
||||
77
domain/core/CLAUDE.md
Normal file
77
domain/core/CLAUDE.md
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# domain/core
|
||||
|
||||
Cross-cutting domain utilities for async data loading, error handling, and reactive streams. Not business logic — foundational abstractions used across all domain modules.
|
||||
|
||||
## LCE (Loading-Content-Error) Pattern
|
||||
|
||||
`Lce<E, C>` — sealed class representing async operation state:
|
||||
- `Loading(partialContent?)` — in progress, may carry partial data
|
||||
- `Content(content)` — success
|
||||
- `Error(error)` — failure with typed error
|
||||
|
||||
Key APIs:
|
||||
- `lce { }` builder — executes block in `LceRaise` context with Arrow's Raise DSL for typed error handling
|
||||
- `lceFlow { }` builder — creates `LceFlow<E, C>` (alias for `Flow<Lce<E, C>>`) via channel-based producer DSL
|
||||
- `LceRaise.bind()` — extracts content from Lce/Either or short-circuits on error
|
||||
- Extensions: `fold()`, `map()`, `mapError()`, `toLce()`, `toEither()`
|
||||
|
||||
## Flow Packaging
|
||||
|
||||
A pattern for complex data streams where work on a single flow is split into three logically separate components: **Producer** (creation), **Supplier** (delivery/caching), and **Fetcher** (refresh). Use it only when you need flexibility in creating, reusing, fetching, and updating a data stream (e.g., network status). Do NOT use for simple cases like reading preferences.
|
||||
|
||||
### FlowProducer
|
||||
|
||||
`FlowProducer<Data>` — creates the data flow. Implement:
|
||||
- `fallback: Data` — emitted when the flow throws an exception
|
||||
- `produce(): Flow<Data>` — the actual flow creation logic
|
||||
|
||||
Built-in `produceWithFallback()` catches errors, emits `fallback`, waits 2s, then retries — keeping the flow alive for subscribers.
|
||||
|
||||
`FlowProducer.Factory<Params, Producer>` — creates a Producer from params. Typically implemented via Hilt `@AssistedFactory`.
|
||||
|
||||
**Implementation pattern:**
|
||||
1. Define interface extending `FlowProducer<Data>` with inner `Params` data class and `Factory` interface
|
||||
2. Create `Default*Producer` with `@AssistedInject` constructor taking `@Assisted params` + dependencies
|
||||
3. Override `fallback` and `produce()`
|
||||
4. Declare inner `@AssistedFactory` interface extending the Producer's Factory
|
||||
|
||||
### FlowSupplier / FlowCachingSupplier
|
||||
|
||||
`FlowSupplier<Params, Data>` — delivers a flow by params via `operator fun invoke(params): Flow<Data>`. Also provides `getSyncOrNull(params, timeout)` for one-shot access.
|
||||
|
||||
`FlowCachingSupplier<Producer, Params, Data>` — abstract implementation that caches flows by key. Implement:
|
||||
- `factory: FlowProducer.Factory` — to create producers
|
||||
- `keyCreator: (Params) -> String` — to generate cache keys
|
||||
|
||||
Behavior: returns cached flow if exists, otherwise creates via `factory.create(params).produceWithFallback()`, caches it, and auto-evicts on terminal exception.
|
||||
|
||||
**Implementation pattern:**
|
||||
1. Define abstract class extending `FlowCachingSupplier` with `factory` and `keyCreator` in constructor
|
||||
2. In DI module, create anonymous subclass providing the factory (injected) and keyCreator lambda
|
||||
|
||||
### FlowFetcher
|
||||
|
||||
`FlowFetcher<Params>` — triggers data refresh, returns `Either<Throwable, Unit>`. Typically updates a store/data source, causing the Producer's flow to re-emit.
|
||||
|
||||
**Implementation pattern:**
|
||||
1. Define interface extending `FlowFetcher<Params>` with inner `Params` data class
|
||||
2. Create `Default*Fetcher` with `@Inject` constructor, override `invoke` wrapping logic in `Either.catch { }`, handle errors with `.onLeft { }`
|
||||
|
||||
### Testing
|
||||
|
||||
- **FlowProducer**: test flow creation logic, params usage, emission behavior, exception handling
|
||||
- **FlowFetcher**: test successful update path and error path (exception thrown)
|
||||
|
||||
## Chain Processing
|
||||
|
||||
- `Chain<E, R>` / `ResultChain<E, R>` — single operation in a chain, works with `Either<E, R>`
|
||||
- `ChainProcessor<E, R>` — folds chains sequentially, stops on first error
|
||||
|
||||
## Error Types
|
||||
|
||||
- `DataError` — sealed domain error hierarchy: `NetworkError.NoInternetConnection`, `UserWalletError.WrongUserWallet`
|
||||
|
||||
## Either Extensions
|
||||
|
||||
- `Either.catchOn(dispatcher, block)` — executes on dispatcher, catches exceptions
|
||||
- `eitherOn(dispatcher, block)` — Raise DSL block on specified dispatcher
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
package com.tangem.domain.wallets.legacy
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface UserWalletsListManager {
|
||||
|
||||
/**
|
||||
* Indicates that the [UserWalletsListManager] is [UserWalletsListManager.Lockable]
|
||||
* */
|
||||
val isLockable: Boolean
|
||||
|
||||
/** [Flow] with all saved [UserWallet]s updates */
|
||||
val userWallets: Flow<List<UserWallet>>
|
||||
|
||||
/** Count saved wallets updates */
|
||||
val savedWalletsCount: Flow<Int>
|
||||
|
||||
/** [Flow] with selected [UserWallet] updates */
|
||||
@Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features")
|
||||
val selectedUserWallet: Flow<UserWallet>
|
||||
|
||||
/** [List] with all saved [UserWallet]s updates */
|
||||
val userWalletsSync: List<UserWallet>
|
||||
|
||||
/** Selected [UserWallet] */
|
||||
@Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features")
|
||||
val selectedUserWalletSync: UserWallet?
|
||||
|
||||
/** Indicates that the [UserWalletsListManager] contains at least one saved [UserWallet] */
|
||||
val hasUserWallets: Boolean
|
||||
|
||||
/** Count of saved user wallets */
|
||||
val walletsCount: Int
|
||||
|
||||
/**
|
||||
* Set [UserWallet] with provided [UserWalletId] as selected
|
||||
*
|
||||
* @param userWalletId [UserWalletId] of [UserWallet] which must be selected
|
||||
*
|
||||
* @return [CompletionResult.Success] with selected [UserWallet] or [CompletionResult.Failure] with
|
||||
* [NoSuchElementException] if [UserWallet] with [userWalletId] not found
|
||||
*/
|
||||
suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet>
|
||||
|
||||
/**
|
||||
* Save provided user wallet and set it as selected
|
||||
*
|
||||
* @param userWallet [UserWallet] to save
|
||||
* @param canOverride If false, then terminate with [UserWalletsListError.WalletAlreadySaved] when user tries
|
||||
* to save an already saved card
|
||||
*
|
||||
* @return [CompletionResult] of operation
|
||||
*/
|
||||
suspend fun save(userWallet: UserWallet, canOverride: Boolean = false): CompletionResult<Unit>
|
||||
|
||||
/**
|
||||
* Same as [save] but not change selected user wallet ID and not terminate with
|
||||
* [UserWalletsListError.WalletAlreadySaved] if [UserWallet] already saved.
|
||||
* Can terminate with [NoSuchElementException] if unable to find [UserWallet] with provided [UserWalletId].
|
||||
*
|
||||
* @param userWalletId update [UserWallet] with that [UserWalletId]
|
||||
* @param update lambda that receives stored [UserWallet] and returns updated [UserWallet]
|
||||
*
|
||||
* @return [CompletionResult.Success] with updated [UserWallet] or [CompletionResult.Failure] with
|
||||
* [NoSuchElementException] if [UserWallet] with [userWalletId] not found
|
||||
*/
|
||||
suspend fun update(
|
||||
userWalletId: UserWalletId,
|
||||
update: suspend (UserWallet) -> UserWallet,
|
||||
): CompletionResult<UserWallet>
|
||||
|
||||
/**
|
||||
* Delete saved [UserWallet]s with provided [UserWalletId]s.
|
||||
* Sets [isLocked] as true if [userWallets] is empty or if all [userWallets] are locked.
|
||||
*
|
||||
* @param userWalletIds [UserWalletId]s of [UserWallet]s which must be deleted
|
||||
*
|
||||
* @return [CompletionResult] of operation
|
||||
*/
|
||||
suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit>
|
||||
|
||||
/**
|
||||
* Clear all saved [UserWallet]s and set [isLocked] as true
|
||||
*
|
||||
* @return [CompletionResult] of operation
|
||||
*/
|
||||
suspend fun clear(): CompletionResult<Unit>
|
||||
|
||||
/**
|
||||
* Get [UserWallet] with provided [UserWalletId]
|
||||
*
|
||||
* @return [CompletionResult.Success] with found [UserWallet] or [CompletionResult.Failure] with
|
||||
* [NoSuchElementException] if [UserWallet] with [userWalletId] not found
|
||||
*/
|
||||
suspend fun get(userWalletId: UserWalletId): CompletionResult<UserWallet>
|
||||
|
||||
interface Lockable : UserWalletsListManager {
|
||||
|
||||
/**
|
||||
* Indicates that all [UserWallet]s is locked
|
||||
*
|
||||
* @see [isLocked]
|
||||
* @see [UserWallet.isLocked]
|
||||
*/
|
||||
val lockedState: Flow<Boolean>
|
||||
|
||||
/**
|
||||
* Indicates that all [UserWallet]s is locked. Sync version.
|
||||
*
|
||||
* @see [lockedState]
|
||||
* @see [UserWallet.isLocked]
|
||||
*/
|
||||
val isLocked: Boolean
|
||||
|
||||
/**
|
||||
* Receive saved [UserWallet]s, populate [userWallets] flow with it and set [lockedState] as false.
|
||||
*
|
||||
* @param type Defines the behavior of the operation.
|
||||
*
|
||||
* @return [CompletionResult] of operation, with selected [UserWallet]
|
||||
* or null if there is no selected [UserWallet]
|
||||
*/
|
||||
suspend fun unlock(type: UnlockType): CompletionResult<UserWallet>
|
||||
|
||||
/** Remove [UserWallet]s from [userWallets] and set [lockedState] as true */
|
||||
fun lock()
|
||||
|
||||
/**
|
||||
* Defines the behavior of the [unlock] operation.
|
||||
* */
|
||||
enum class UnlockType {
|
||||
/**
|
||||
* Ensures that all stored [UserWallet]s are unlocked,
|
||||
* or throws [UserWalletsListError.NotAllUserWalletsUnlocked].
|
||||
*
|
||||
* In this type [selectedUserWallet] is either a previously selected [UserWallet] or the first stored
|
||||
* [UserWallet].
|
||||
* */
|
||||
ALL,
|
||||
|
||||
/**
|
||||
* Ensures that at least one stored [UserWallet] is unlocked,
|
||||
* or throws [UserWalletsListError.NoUserWalletSelected].
|
||||
*
|
||||
* In this type [selectedUserWallet] is the first stored and unlocked [UserWallet].
|
||||
* */
|
||||
ANY,
|
||||
|
||||
/**
|
||||
* Same as [ALL] type, but this type can not change [selectedUserWallet] while unlocking.
|
||||
* */
|
||||
ALL_WITHOUT_SELECT,
|
||||
}
|
||||
}
|
||||
|
||||
// For provider
|
||||
companion object
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue