Files
TimandGitHub 39247a11fa feat(web): migrate chain config fetching to /v2/chains endpoint (#7275)
* feat(web): migrate chain config fetching to `/v2/chains` endpoint

Switch the chains list endpoint from `/v1/chains` to `/v2/chains` across
all callers in the web app and shared store package. The v2 endpoint
requires a serviceKey to scope chain features per service (e.g.
WALLET_WEB, MOBILE). The key is configurable via
NEXT_PUBLIC_CONFIG_SERVICE_KEY with a default of 'WALLET_WEB'.

Updated callers: useChains, useInitStaticChains, ofac API, swap order listener, build-time fetch-chains script, and service worker
gateway-utils. Also migrates the single-chain getChainConfig utility
to chainsGetChainV2.

* test: replace crypto UUID with faker UUID in dynamicBaseQuery tests

Updated the test cases in cgwClient.test.ts to use faker for generating UUIDs instead of crypto.randomUUID, ensuring consistent and controlled test data generation.

* refactor: update chain configuration hooks and API calls to use v2 endpoint

Replaced all instances of `getChainsConfig` with `getChainsConfigV2` in the web app and store, ensuring consistent usage of the new endpoint. This includes updates in hooks, API slices, and tests to align with the new service key requirements for fetching chain configurations.

* tests: restore v1/chains MSW handler for mobile test compatibility

The v1/chains handler was removed when migrating to v2, but mobile still queries getChainsConfig which hits /v1/chains. Without a handler, requests fell through to the real staging API, causing snapshot drift in mobile tests. Also extracts the shared mock data into a chainsConfig constant to keep v1
and v2 handlers in sync.

* test: replace hardcoded service key with constant in chains retry tests

Updated the chains retry functionality tests to use a constant for the service key instead of a hardcoded string.

* fix: add validation for serviceKey in getChainsConfigV2 query

Implemented a check to ensure that the serviceKey is provided when calling the getChainsConfigV2 query. If the serviceKey is missing, a custom error is returned, improving error handling and preventing potential issues during API calls.

* fix: add validation for results array in getChainsConfigs

Enhanced error handling in the getChainsConfigs function by adding a check to ensure the results array is present in the API response. If the results array is missing, a custom error is returned, improving robustness against invalid responses.

* fix: remove JSON.stringify from service key in getChainsConfigV2 query

Updated the getChainsConfigV2 query to directly use the CONFIG_SERVICE_KEY constant instead of stringifying it, simplifying the code and ensuring the correct value is passed.
2026-03-02 13:52:49 +01:00

335 lines
9.6 KiB
TypeScript

import { http, HttpResponse } from 'msw'
import type { Portfolio } from '@safe-global/store/gateway/AUTO_GENERATED/portfolios'
import type { Balances } from '@safe-global/store/gateway/AUTO_GENERATED/balances'
import type { Protocol } from '@safe-global/store/gateway/AUTO_GENERATED/positions'
import type { SafeState } from '@safe-global/store/gateway/AUTO_GENERATED/safes'
import type { Chain, ChainPage } from '@safe-global/store/gateway/AUTO_GENERATED/chains'
import type { SafeApp } from '@safe-global/store/gateway/AUTO_GENERATED/safe-apps'
import { FEATURES } from '@safe-global/utils/utils/chains'
import {
portfolioFixtures,
balancesFixtures,
positionsFixtures,
safeFixtures,
chainFixtures,
safeAppsFixtures,
type FixtureScenario,
} from '../fixtures'
/**
* Handlers from Fixtures
*
* Creates MSW handlers using real API response fixtures.
* This ensures mock data matches actual API shapes exactly.
*/
type ScenarioKey = Exclude<FixtureScenario, 'empty'>
/**
* Create all handlers for a specific fixture scenario
*
* @example
* // In a story:
* parameters: {
* msw: {
* handlers: createHandlersFromFixture('efSafe', GATEWAY_URL),
* },
* }
*/
export const createHandlersFromFixture = (
scenario: ScenarioKey,
GATEWAY_URL: string,
options: {
/** Override chain features */
features?: (FEATURES | string)[]
/** Include positions endpoint */
includePositions?: boolean
/** Include portfolio endpoint */
includePortfolio?: boolean
/** Include Safe Apps endpoint */
includeSafeApps?: boolean
} = {},
) => {
const { features, includePositions = true, includePortfolio = true, includeSafeApps = true } = options
const handlers = [
// Chain config (with optional feature overrides)
...createChainHandlersFromFixture(GATEWAY_URL, features),
// Safe info
...createSafeHandlersFromFixture(scenario, GATEWAY_URL),
// Balances
...createBalanceHandlersFromFixture(scenario, GATEWAY_URL),
]
if (includePositions) {
handlers.push(...createPositionHandlersFromFixture(scenario, GATEWAY_URL))
}
if (includePortfolio) {
handlers.push(...createPortfolioHandlersFromFixture(scenario, GATEWAY_URL))
}
if (includeSafeApps) {
handlers.push(...createSafeAppsHandlersFromFixture(GATEWAY_URL))
}
return handlers
}
/**
* Create chain handlers from fixtures with optional feature overrides
*/
export const createChainHandlersFromFixture = (GATEWAY_URL: string, featureOverrides?: (FEATURES | string)[]) => {
const chainData = { ...chainFixtures.mainnet }
// Override features if provided
if (featureOverrides) {
chainData.features = featureOverrides
}
return [
http.get<never, never, ChainPage>(`${GATEWAY_URL}/v2/chains`, () => {
return HttpResponse.json({
...chainFixtures.all,
results: [chainData],
})
}),
http.get<{ chainId: string }, never, Chain>(`${GATEWAY_URL}/v2/chains/:chainId`, () => {
return HttpResponse.json(chainData)
}),
]
}
/**
* Create safe info handlers from fixtures
*/
export const createSafeHandlersFromFixture = (scenario: ScenarioKey, GATEWAY_URL: string) => {
const safeData = safeFixtures[scenario]
return [
http.get<{ chainId: string; safeAddress: string }, never, SafeState>(
`${GATEWAY_URL}/v1/chains/:chainId/safes/:safeAddress`,
() => {
return HttpResponse.json(safeData)
},
),
]
}
/**
* Create balance handlers from fixtures
*/
export const createBalanceHandlersFromFixture = (scenario: ScenarioKey | 'empty', GATEWAY_URL: string) => {
const balancesData = balancesFixtures[scenario]
return [
http.get<{ chainId: string; safeAddress: string; currency: string }, never, Balances>(
`${GATEWAY_URL}/v1/chains/:chainId/safes/:safeAddress/balances/:currency`,
() => {
return HttpResponse.json(balancesData)
},
),
]
}
/**
* Create position handlers from fixtures
*/
export const createPositionHandlersFromFixture = (scenario: ScenarioKey | 'empty', GATEWAY_URL: string) => {
const positionsData = positionsFixtures[scenario]
return [
http.get<{ chainId: string; safeAddress: string; fiatCode: string }, never, Protocol[]>(
`${GATEWAY_URL}/v1/chains/:chainId/safes/:safeAddress/positions/:fiatCode`,
() => {
return HttpResponse.json(positionsData)
},
),
]
}
/**
* Create portfolio handlers from fixtures
*/
export const createPortfolioHandlersFromFixture = (scenario: ScenarioKey | 'empty', GATEWAY_URL: string) => {
const portfolioData = portfolioFixtures[scenario]
return [
http.get<{ address: string }, never, Portfolio>(`${GATEWAY_URL}/v1/portfolio/:address`, () => {
return HttpResponse.json(portfolioData)
}),
]
}
/**
* Create Safe Apps handlers from fixtures
*/
export const createSafeAppsHandlersFromFixture = (GATEWAY_URL: string, empty = false) => {
const safeAppsData = empty ? safeAppsFixtures.empty : safeAppsFixtures.mainnet
return [
http.get<{ chainId: string }, never, SafeApp[]>(`${GATEWAY_URL}/v1/chains/:chainId/safe-apps`, () => {
return HttpResponse.json(safeAppsData)
}),
]
}
/**
* Fixture scenario metadata for documentation and Storybook selectors
*/
export const FIXTURE_SCENARIOS = {
efSafe: {
id: 'efSafe',
name: 'EF Safe - DeFi Heavy',
description: '$142M in DeFi positions across 8 protocols. Tests position aggregation and double accounting.',
tokens: 32,
positions: '$142M',
defiApps: 8,
},
vitalik: {
id: 'vitalik',
name: 'Vitalik - Whale',
description: '1551 tokens, $675M in holdings. Tests large token lists and rendering performance.',
tokens: 1551,
positions: '$19M',
defiApps: 1,
},
spamTokens: {
id: 'spamTokens',
name: 'Spam Tokens',
description: 'Safe with many spam tokens. Tests spam filtering and token display.',
tokens: 26,
positions: '$1.7M',
defiApps: 2,
},
safeTokenHolder: {
id: 'safeTokenHolder',
name: 'Safe Token - Diverse DeFi',
description: '15 different DeFi protocols including locked/reward positions. Tests protocol diversity.',
tokens: 25,
positions: '$707',
defiApps: 15,
},
empty: {
id: 'empty',
name: 'Empty',
description: 'No tokens, no positions. Tests empty states and onboarding flows.',
tokens: 0,
positions: '$0',
defiApps: 0,
},
withoutPositions: {
id: 'withoutPositions',
name: 'Without Positions Feature',
description: 'POSITIONS feature flag disabled. Tests classic balances-only view.',
tokens: 32,
positions: 'N/A (feature disabled)',
defiApps: 0,
},
} as const
export type FixtureScenarioId = keyof typeof FIXTURE_SCENARIOS
/**
* Pre-configured handler sets for common scenarios
*
* @example
* // In Storybook story:
* export const DefiHeavy: Story = {
* parameters: {
* msw: { handlers: fixtureHandlers.efSafe(GATEWAY_URL) },
* },
* }
*
* @example
* // With Storybook select control:
* argTypes: {
* scenario: {
* control: 'select',
* options: Object.keys(FIXTURE_SCENARIOS),
* mapping: Object.fromEntries(
* Object.keys(FIXTURE_SCENARIOS).map(key => [key, fixtureHandlers[key](GATEWAY_URL)])
* ),
* },
* }
*/
export const fixtureHandlers = {
/**
* EF Safe - DeFi Heavy
*
* - $142M in DeFi positions across 8 protocols
* - 32 tokens ($4.5M)
* - Best for: Testing position aggregation, double accounting scenarios
*/
efSafe: (GATEWAY_URL: string) =>
createHandlersFromFixture('efSafe', GATEWAY_URL, {
features: [FEATURES.POSITIONS, FEATURES.PORTFOLIO_ENDPOINT],
}),
/**
* Vitalik Safe - Whale
*
* - 1551 tokens, $675M in holdings
* - Single DeFi protocol ($19M)
* - Best for: Testing large token lists, rendering performance, whale scenarios
*
* ⚠️ Large fixture (~540KB) - may impact story load time
*/
vitalik: (GATEWAY_URL: string) =>
createHandlersFromFixture('vitalik', GATEWAY_URL, {
features: [FEATURES.POSITIONS, FEATURES.PORTFOLIO_ENDPOINT],
}),
/**
* Spam Tokens Safe
*
* - 26 tokens with many obvious spam tokens
* - $85M total, $1.7M in positions
* - Best for: Testing spam filtering, token hiding
*/
spamTokens: (GATEWAY_URL: string) =>
createHandlersFromFixture('spamTokens', GATEWAY_URL, {
features: [FEATURES.POSITIONS, FEATURES.PORTFOLIO_ENDPOINT],
}),
/**
* Safe Token Holder - Diverse DeFi
*
* - 15 different DeFi protocols (Aave, Compound, Lido, etc.)
* - SAFE token with locked and reward positions
* - Best for: Testing protocol diversity, Zerion aggregation edge cases
*/
safeTokenHolder: (GATEWAY_URL: string) =>
createHandlersFromFixture('safeTokenHolder', GATEWAY_URL, {
features: [FEATURES.POSITIONS, FEATURES.PORTFOLIO_ENDPOINT],
}),
/**
* Empty State
*
* - No tokens, no positions
* - Best for: Testing empty states, onboarding flows, first-time user experience
*/
empty: (GATEWAY_URL: string) => [
...createChainHandlersFromFixture(GATEWAY_URL, [FEATURES.POSITIONS, FEATURES.PORTFOLIO_ENDPOINT]),
...createBalanceHandlersFromFixture('empty', GATEWAY_URL),
...createPositionHandlersFromFixture('empty', GATEWAY_URL),
...createPortfolioHandlersFromFixture('empty', GATEWAY_URL),
],
/**
* Without Positions Feature Flag
*
* - POSITIONS and PORTFOLIO_ENDPOINT features disabled
* - Shows classic balances-only view
* - Best for: Testing feature flag behavior, backwards compatibility
*/
withoutPositions: (GATEWAY_URL: string) =>
createHandlersFromFixture('efSafe', GATEWAY_URL, {
features: [], // No POSITIONS feature
includePositions: false,
includePortfolio: false,
}),
}