From 1194c9ae610318c71079f91159d93a1cbf2bb9d2 Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Tue, 6 Jan 2026 10:00:10 +0100 Subject: [PATCH 1/4] test: highlight external usage for assets-controllers --- packages/assets-controllers/EXTERNAL_USAGE.md | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 packages/assets-controllers/EXTERNAL_USAGE.md diff --git a/packages/assets-controllers/EXTERNAL_USAGE.md b/packages/assets-controllers/EXTERNAL_USAGE.md new file mode 100644 index 00000000000..3ca7929be03 --- /dev/null +++ b/packages/assets-controllers/EXTERNAL_USAGE.md @@ -0,0 +1,61 @@ +# External Controller Dependencies + +The following controllers from other packages depend on `@metamask/assets-controllers` state. + +## Summary + +| External Controller | Assets-Controllers Consumed | Primary Use Case | +|--------------------|---------------------------|------------------| +| **`transaction-pay-controller`** | `TokenBalancesController`, `AccountTrackerController`, `TokensController`, `TokenRatesController`, `CurrencyRateController` | Pay gas fees with alternative tokens - needs balances, metadata, and rates | +| **`bridge-controller`** | `CurrencyRateController`, `TokenRatesController`, `MultichainAssetsRatesController` | Cross-chain bridging - needs exchange rates for EVM + non-EVM assets for quote calculations | +| **`subscription-controller`** | `MultichainBalancesController` | Crypto subscription payments - needs multi-chain balances to offer payment options | +| **`core-backend`** | `TokenBalancesController` | Real-time balance coordination - adjusts polling based on WebSocket connection state | + +## State Properties Used + +| Assets-Controller | Key State Properties | How It's Used | External Controller | +|-------------------|---------------------|---------------|---------------------| +| `TokenBalancesController` | `tokenBalances[account][chainId][token]` | Get ERC-20 token balances to check available funds for gas payment | `transaction-pay-controller` | +| `TokenBalancesController` | `updateChainPollingConfigs` action | Coordinate polling intervals based on WebSocket connection status | `core-backend` | +| `AccountTrackerController` | `accountsByChainId[chainId][account].balance` | Get native currency balance (ETH, MATIC) when paying with native token | `transaction-pay-controller` | +| `TokensController` | `allTokens[chainId][*]` → `decimals`, `symbol` | Get token metadata to format amounts and display token info | `transaction-pay-controller` | +| `TokenRatesController` | `marketData[chainId][token].price`, `currency` | Get token-to-native price for fiat calculations | `transaction-pay-controller`, `bridge-controller` | +| `CurrencyRateController` | `currencyRates[ticker].conversionRate` | Get native-to-fiat rate for USD/local currency display | `transaction-pay-controller`, `bridge-controller` | +| `CurrencyRateController` | `currencyRates[ticker].usdConversionRate` | Get native-to-USD rate for standardized value comparison | `transaction-pay-controller`, `bridge-controller` | +| `CurrencyRateController` | `currentCurrency` | Get user's selected fiat currency for fetching rates | `bridge-controller` | +| `MultichainAssetsRatesController` | `conversionRates[assetId].rate` | Get non-EVM asset prices (Solana, Bitcoin) for cross-chain quotes | `bridge-controller` | +| `MultichainBalancesController` | Full state via `getState()` | Check user's crypto balances across all chains for subscription payment options | `subscription-controller` | +| `MultichainBalancesController` | `AccountBalancesUpdatesEvent` | Monitor real-time balance changes to update payment options | `subscription-controller` | + +## Detailed Usage + +### `transaction-pay-controller` + +Handles gas fee payment with alternative tokens (pay for transactions with tokens other than the native currency). + +- **`TokenBalancesController`**: Queries `tokenBalances[account][chainId][tokenAddress]` to get ERC-20 token balances for determining available funds +- **`AccountTrackerController`**: Queries `accountsByChainId[chainId][account].balance` to get native currency balance when the payment token is native (ETH, MATIC, etc.) +- **`TokensController`**: Queries `allTokens[chainId][*]` to get token metadata (decimals, symbol) for proper amount formatting +- **`TokenRatesController`**: Queries `marketData[chainId][tokenAddress].price` to calculate token-to-native conversion for fiat display +- **`CurrencyRateController`**: Queries `currencyRates[ticker].conversionRate` and `usdConversionRate` to convert native amounts to fiat + +### `bridge-controller` + +Handles cross-chain token bridging and swapping, fetching quotes from bridge providers. + +- **`CurrencyRateController`**: Gets native currency rates for EVM chains and user's preferred currency via `currencyRates` and `currentCurrency` +- **`TokenRatesController`**: Gets EVM token prices relative to native currency via `marketData[chainId][tokenAddress]` +- **`MultichainAssetsRatesController`**: Gets non-EVM asset prices (Solana, Bitcoin, etc.) via `conversionRates[assetId]` for cross-chain quote calculations + +### `subscription-controller` + +Handles MetaMask subscription management, including crypto-based payments. + +- **`MultichainBalancesController`**: Queries full state to check user's crypto balances across all chains to determine available payment options. Subscribes to `AccountBalancesUpdatesEvent` to update payment options in real-time. + +### `core-backend` + +Provides real-time data delivery via WebSocket for account activity and balance updates. + +- **`TokenBalancesController`**: Calls `updateChainPollingConfigs` to coordinate polling intervals. When WebSocket is connected, reduces polling (10 min backup). When disconnected, increases polling frequency (30s) for HTTP fallback. + From 9ed0ba6b6a126dabe3cbc93071176aa0f1b36b87 Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Wed, 7 Jan 2026 15:44:08 +0100 Subject: [PATCH 2/4] test: migration strategy --- packages/assets-controllers/EXTERNAL_USAGE.md | 28 +- .../assets-controllers/MIGRATION_STRATEGY.md | 291 ++++++++++++++++++ 2 files changed, 314 insertions(+), 5 deletions(-) create mode 100644 packages/assets-controllers/MIGRATION_STRATEGY.md diff --git a/packages/assets-controllers/EXTERNAL_USAGE.md b/packages/assets-controllers/EXTERNAL_USAGE.md index 3ca7929be03..61cd62c0444 100644 --- a/packages/assets-controllers/EXTERNAL_USAGE.md +++ b/packages/assets-controllers/EXTERNAL_USAGE.md @@ -33,11 +33,29 @@ The following controllers from other packages depend on `@metamask/assets-contro Handles gas fee payment with alternative tokens (pay for transactions with tokens other than the native currency). -- **`TokenBalancesController`**: Queries `tokenBalances[account][chainId][tokenAddress]` to get ERC-20 token balances for determining available funds -- **`AccountTrackerController`**: Queries `accountsByChainId[chainId][account].balance` to get native currency balance when the payment token is native (ETH, MATIC, etc.) -- **`TokensController`**: Queries `allTokens[chainId][*]` to get token metadata (decimals, symbol) for proper amount formatting -- **`TokenRatesController`**: Queries `marketData[chainId][tokenAddress].price` to calculate token-to-native conversion for fiat display -- **`CurrencyRateController`**: Queries `currencyRates[ticker].conversionRate` and `usdConversionRate` to convert native amounts to fiat +**Call Chain to `TokenBalancesController`:** + +``` +TransactionPayController (constructor) + │ + └─► pollTransactionChanges() // subscribes to TransactionController events + │ + └─► onTransactionChange() // triggered when tx is new/updated + │ + └─► parseRequiredTokens() // in required-tokens.ts + │ + └─► getTokenBalance() // in token.ts + │ + └─► messenger.call('TokenBalancesController:getState') +``` + +**State accessed:** + +- **`TokenBalancesController`**: `tokenBalances[account][chainId][token]` → ERC-20 balances +- **`AccountTrackerController`**: `accountsByChainId[chainId][account].balance` → native balance (ETH, MATIC) +- **`TokensController`**: `allTokens[chainId][*]` → token metadata (decimals, symbol) +- **`TokenRatesController`**: `marketData[chainId][token].price` → token-to-native price +- **`CurrencyRateController`**: `currencyRates[ticker].conversionRate` → native-to-fiat rate ### `bridge-controller` diff --git a/packages/assets-controllers/MIGRATION_STRATEGY.md b/packages/assets-controllers/MIGRATION_STRATEGY.md new file mode 100644 index 00000000000..8bfd0469aa8 --- /dev/null +++ b/packages/assets-controllers/MIGRATION_STRATEGY.md @@ -0,0 +1,291 @@ +# Assets Controller State Migration Strategy: Balances + +## Overview + +This document outlines the migration strategy for consolidating **balance state** from `TokenBalancesController` and `AccountTrackerController` into the unified `AssetsController.assetsBalance` structure. + +> **Note:** This same migration pattern (dual-write → dual-read → gradual rollout → confidence period → cleanup) should be followed for migrating: +> - **Metadata** (`TokensController`, `TokenListController` → `assetsMetadata`) +> - **Prices** (`TokenRatesController`, `CurrencyRateController` → `assetsPrice`) +> +> Each migration should can be done **sequentially**, not in parallel, to reduce risk and simplify debugging. + +### Target State Structure + +```typescript +// assetsController.ts +export type AssetsControllerState = { + /** Shared metadata for all assets (stored once per asset) */ + assetsMetadata: { [assetId: string]: Json }; + /** Price data for assets (stored once per asset) */ + assetsPrice: { [assetId: string]: Json }; + /** Per-account balance data */ + assetsBalance: { [accountId: string]: { [assetId: string]: Json } }; +}; +``` + +### Current State Sources Being Migrated (This Document: Balances) + +| Current Controller | Current State Property | Target Property | +|-------------------|----------------------|-----------------| +| `TokenBalancesController` | `tokenBalances[account][chainId][token]` | `assetsBalance[accountId][assetId]` | +| `AccountTrackerController` | `accountsByChainId[chainId][account].balance` | `assetsBalance[accountId][assetId]` (native) | + +### Future Migrations (Same Pattern) + +| Current Controller | Current State Property | Target Property | +|-------------------|----------------------|-----------------| +| `TokensController` | `allTokens[chainId][account]` | `assetsMetadata[assetId]` | +| `TokenListController` | `tokenList`, `tokensChainsCache` | `assetsMetadata[assetId]` | +| `TokenRatesController` | `marketData[chainId][token].price` | `assetsPrice[assetId]` | +| `CurrencyRateController` | `currencyRates[ticker]` | `assetsPrice[assetId]` (native assets) | + +--- + +## Feature Flags + +Two feature flags control the entire migration: + +| Flag | Type | Purpose | +|------|------|---------| +| `assets_controller_dual_write` | `boolean` | When `true`, balance updates write to both legacy and new state. Keep enabled through Phase 4 to ensure rollback always has fresh data. | +| `assets_controller_read_percentage` | `number (0-100)` | Percentage of users reading from new state. `0` = all legacy, `100` = all new. | + +**Flag states by phase:** + +| Phase | `dual_write` | `read_percentage` | +|-------|--------------|-------------------| +| Phase 1: Dual-Write | `true` | `0` | +| Phase 2: Dual-Read (comparison) | `true` | `0` (logging enabled in code) | +| Phase 3: Gradual Rollout | `true` | `10 → 25 → 50 → 75 → 100` | +| Phase 4: Confidence Period | `true` | `100` | +| Phase 5: Cleanup | removed | removed | + +--- + +## Migration Phases (TokenBalancesController) + +### Phase 1: Dual-Write + +**Goal:** Write to both old and new state structures simultaneously. + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Phase 1: Dual-Write │ +│ │ +│ Balance Update │ +│ │ │ +│ ├──► TokenBalancesController.state.tokenBalances (WRITE) │ +│ │ │ +│ └──► AssetsController.state.assetsBalance (WRITE) │ +│ │ +│ External Controllers │ +│ │ │ +│ └──► TokenBalancesController.state.tokenBalances (READ) ◄── still │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +**Pseudo code:** + +``` +ON balance_update(account, chainId, token, balance): + + 1. WRITE to TokenBalancesController.state (legacy) + + 2. IF feature_flag("assets_controller_dual_write") THEN + WRITE to AssetsController.state (new) +``` + +--- + +### Phase 2: Dual-Read with Comparison + +**Goal:** Read from both sources, compare results, log discrepancies. + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Phase 2: Dual-Read + Compare │ +│ │ +│ External Controller Request │ +│ │ │ +│ ├──► TokenBalancesController.state (PRIMARY READ) │ +│ │ │ │ +│ │ └──► Return to caller │ +│ │ │ +│ └──► AssetsController.state (SHADOW READ) │ +│ │ │ +│ └──► Compare with primary, log discrepancies │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +**Pseudo code:** + +``` +ON get_balance(account, chainId, token): + + 1. legacyBalance = READ from TokenBalancesController.state + + 2. IF feature_flag("assets_controller_dual_write") AND read_percentage == 0 THEN + // Phase 2: Shadow read for comparison only + newBalance = READ from AssetsController.state + + IF legacyBalance != newBalance THEN + LOG discrepancy for investigation + + 3. RETURN legacyBalance // Still return legacy +``` + +--- + +### Phase 3: Gradual Read Migration + +**Goal:** Gradually shift reads to new state with percentage-based rollout. + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Phase 3: Percentage-Based Rollout │ +│ │ +│ Feature Flag: assets_controller_read_percentage = 10 │ +│ │ +│ Request 1-10: Read from AssetsController ────────┐ │ +│ Request 11-100: Read from TokenBalancesController ─┴──► Return │ +│ │ +│ Gradually increase: 10% → 25% → 50% → 75% → 100% │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +**Rollout:** Gradually increase `assets_controller_read_percentage`: 10% → 25% → 50% → 75% → 100% + +--- + +### Phase 4: Confidence Period (Keep Dual-Write Active) + +**Goal:** Maintain dual-write while 100% of reads use new state. This ensures rollback is always to fresh data. + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Phase 4: Confidence Period │ +│ │ +│ Balance Update (DUAL-WRITE CONTINUES) │ +│ │ │ +│ ├──► TokenBalancesController.state.tokenBalances (WRITE) ◄── fresh! │ +│ │ │ +│ └──► AssetsController.state.assetsBalance (WRITE) │ +│ │ +│ External Controllers │ +│ │ │ +│ └──► AssetsController.state (READ 100%) │ +│ │ +│ Rollback available: Set read_percentage=0 → instant switch to fresh │ +│ legacy data │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +**Why keep dual-write?** +- Rollback to stale data is worse than the original problem +- Storage cost is temporary +- Peace of mind during high-risk period + +**Duration:** 4+ weeks at 100% new state reads with zero issues + +--- + +### Phase 5: Legacy Removal + +**Goal:** Remove legacy state and controllers. This is a one-way door. + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Phase 5: Point of No Return │ +│ │ +│ BEFORE: Dual-write active, rollback possible │ +│ │ +│ AFTER: Legacy controllers removed, no rollback to old state │ +│ │ +│ Rollback strategy changes to: │ +│ - Fix forward (patch the new controller) │ +│ - Restore from backup (if catastrophic) │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +**Checklist before removal:** +- [ ] 100% reads from new state for 4+ weeks +- [ ] Zero rollbacks triggered during confidence period +- [ ] All external controllers migrated and tested +- [ ] Performance metrics stable + +--- + +## Rollback Strategy + +### During Phases 1-4: Instant Rollback (< 1 minute) + +Because dual-write is active, legacy state is always fresh. + +```typescript +// Set feature flag to disable new state reads +{ + "assets_controller_read_percentage": 0 +} +``` + +**Result:** All reads immediately use legacy state with fresh data (dual-write keeps it current). + + +--- + +## Monitoring & Alerts + +### Key Metrics + +| Metric | Threshold | Action | +|--------|-----------|--------| +| Balance discrepancy rate | > 0.1% | Pause rollout | +| Read latency increase | > 20ms | Investigate | +| State size increase | > 50% | Optimize | +| Error rate | > 0.01% | Rollback | + +### Logging + +```typescript +// Log all discrepancies during dual-read phase +interface DiscrepancyLog { + timestamp: number; + account: string; + assetId: string; + legacyValue: string; + newValue: string; + phase: 'dual_read' | 'percentage_rollout'; +} +``` + +--- + + +--- + +## Checklist + +### Pre-Migration +- [ ] New `AssetsController` implemented with new state structure +- [ ] Feature flags created in LaunchDarkly/remote config +- [ ] Monitoring dashboards set up +- [ ] Rollback runbook documented + +### During Migration +- [ ] Dual-write enabled and verified +- [ ] Discrepancy logging active +- [ ] Performance baseline established +- [ ] External team communication (if applicable) + +### Post-Migration +- [ ] Legacy state writes disabled +- [ ] Legacy controller deprecated +- [ ] Documentation updated +- [ ] Storage cleanup verified + From 6474ce4df97fce53566447f55eb67dca414bb608 Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Wed, 7 Jan 2026 15:47:24 +0100 Subject: [PATCH 3/4] test: migration strategy --- packages/assets-controllers/MIGRATION_STRATEGY.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/assets-controllers/MIGRATION_STRATEGY.md b/packages/assets-controllers/MIGRATION_STRATEGY.md index 8bfd0469aa8..fe5a3975dfd 100644 --- a/packages/assets-controllers/MIGRATION_STRATEGY.md +++ b/packages/assets-controllers/MIGRATION_STRATEGY.md @@ -190,8 +190,6 @@ ON get_balance(account, chainId, token): - Storage cost is temporary - Peace of mind during high-risk period -**Duration:** 4+ weeks at 100% new state reads with zero issues - --- ### Phase 5: Legacy Removal @@ -223,7 +221,7 @@ ON get_balance(account, chainId, token): ## Rollback Strategy -### During Phases 1-4: Instant Rollback (< 1 minute) +### During Phases 1-4: Instant Rollback Because dual-write is active, legacy state is always fresh. From e87fae98f6fe3e0715994e86c5ead022594b0345 Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Wed, 7 Jan 2026 15:48:57 +0100 Subject: [PATCH 4/4] test: migration strategy --- packages/assets-controllers/MIGRATION_STRATEGY.md | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/assets-controllers/MIGRATION_STRATEGY.md b/packages/assets-controllers/MIGRATION_STRATEGY.md index fe5a3975dfd..142a44dab08 100644 --- a/packages/assets-controllers/MIGRATION_STRATEGY.md +++ b/packages/assets-controllers/MIGRATION_STRATEGY.md @@ -237,16 +237,7 @@ Because dual-write is active, legacy state is always fresh. --- -## Monitoring & Alerts - -### Key Metrics - -| Metric | Threshold | Action | -|--------|-----------|--------| -| Balance discrepancy rate | > 0.1% | Pause rollout | -| Read latency increase | > 20ms | Investigate | -| State size increase | > 50% | Optimize | -| Error rate | > 0.01% | Rollback | +## Monitoring ### Logging