Skip to content

Conversation

@gambinish
Copy link
Contributor

@gambinish gambinish commented Jan 8, 2026

Description

This PR implements a graceful degradation pattern for Perps asset icons, allowing the app to first attempt loading icons from MetaMask's contract-metadata repository and automatically fall back to HyperLiquid's CDN if the primary source fails.

We want to host curated Perps icons in our own contract-metadata repo for quality control. However, not all icons may be uploaded yet, and we need a reliable fallback. This approach allows gradual migration while maintaining full icon coverage

Solution:

  1. Re-added METAMASK_PERPS_ICONS_BASE_URL constant pointing to contract-metadata repo
  2. Created getAssetIconUrls() function that returns both primary and fallback URLs
  3. Updated PerpsTokenLogo component to try primary URL first, then fallback on error
  4. HIP-3 assets use the hip3:dex_SYMBOL.svg format for MetaMask URLs

In cases where both primary and fallback urls fail, we fallback to the default two letter icon.

Changelog

CHANGELOG entry: Perps icon fallback url mechanism

Related issues

Fixes: https://consensyssoftware.atlassian.net/browse/TAT-2329

Manual testing steps

Feature: Perps Token Icon Fallback

  Scenario: User views Perps market list with icons from MetaMask repo
    Given the user has opened the Perps tab
    When the market list loads
    Then icons load from MetaMask contract-metadata URL first
    And if MetaMask URL fails, icons fall back to HyperLiquid URL
    And if both URLs fail, a 2-letter text fallback is displayed

  Scenario: User views HIP-3 asset icons
    Given the user has opened a HIP-3 market (e.g., xyz:AAPL)
    When the icon loads
    Then the primary URL uses format hip3:xyz_AAPL.svg
    And the fallback URL uses format xyz:AAPL.svg

Screenshots/Recordings

Before

After

Pre-merge author checklist

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Note

Introduces dual-source Perps icon loading with automatic fallback and comprehensive tests.

  • Adds METAMASK_PERPS_ICONS_BASE_URL and implements getAssetIconUrls() to return { primary, fallback } for both regular and HIP-3 assets (MetaMask uses hip3:dex_SYMBOL.svg, HyperLiquid uses dex:SYMBOL.svg)
  • Updates PerpsTokenLogo to prefer iconUrls.primary, switch to iconUrls.fallback on onError, and revert to primary when the symbol changes; preserves 2-letter text fallback if both fail
  • Uses a dynamic imageKey including fallback state for proper re-rendering; retains loading and caching behaviors
  • Expands tests in PerpsTokenLogo.test.tsx and marketUtils.test.ts to cover primary→fallback switching, HIP-3 formatting, k-prefix handling, uppercase normalization, and symbol-change reset

Written by Cursor Bugbot for commit 63075a8. This will update automatically on new commits. Configure here.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 8, 2026

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

@metamaskbot metamaskbot added the team-perps Perps team label Jan 8, 2026
@github-actions github-actions bot added the size-M label Jan 8, 2026
@gambinish gambinish changed the title feat: initial implementation of asset url graceful fallback feat: Graceful Perps icon fallback Jan 8, 2026
@gambinish gambinish marked this pull request as ready for review January 9, 2026 00:20
@gambinish gambinish requested a review from a team as a code owner January 9, 2026 00:20
<!-- EAS Update configuration -->
<meta-data android:name="expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY" android:value="{&quot;expo-channel-name&quot;:&quot;rc&quot;}"/>
<meta-data android:name="expo.modules.updates.EXPO_RUNTIME_VERSION" android:value="7.62.0" />
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://u.expo.dev/" />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EAS Update URL missing Expo project ID

High Severity

The EAS Update URL https://u.expo.dev/ is incomplete - it's missing the required Expo project UUID. According to ota.config.js, the URL format is https://u.expo.dev/${PROJECT_ID} where PROJECT_ID comes from the EXPO_PROJECT_ID environment variable. This appears to be accidentally committed output from running scripts/update-expo-channel.js without the environment variable set. Without the project ID, OTA updates won't be able to locate the correct Expo project, causing update checks to fail. These configuration changes also appear unrelated to the PR's stated purpose of implementing Perps icon fallback.

🔬 Verification Test

Test code:

// Verify the URL format expected by ota.config.js
const { UPDATE_URL, PROJECT_ID } = require('./ota.config.js');
console.log('PROJECT_ID:', PROJECT_ID || '(empty)');
console.log('UPDATE_URL:', UPDATE_URL);
console.log('URL is incomplete:', UPDATE_URL === 'https://u.expo.dev/');

Command run:

node -e "const { UPDATE_URL, PROJECT_ID } = require('./ota.config.js'); console.log('PROJECT_ID:', PROJECT_ID || '(empty)'); console.log('UPDATE_URL:', UPDATE_URL); console.log('URL is incomplete:', UPDATE_URL === 'https://u.expo.dev/');"

Output:

PROJECT_ID: (empty)
UPDATE_URL: https://u.expo.dev/
URL is incomplete: true

Why this proves the bug: The output shows that without the EXPO_PROJECT_ID environment variable, the URL defaults to https://u.expo.dev/ (incomplete), which matches what was committed to the Android and iOS config files. The Expo Update URL requires a project UUID suffix to function correctly.

Additional Locations (1)

Fix in Cursor Fix in Web

? useFallbackUrl
? iconUrls.fallback
: iconUrls.primary
: null;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale fallback state causes wrong URL on symbol change

Low Severity

When the symbol changes after a primary URL failure, useFallbackUrl retains its true value from the previous symbol during the first render cycle because the reset happens in a useEffect (which runs after render). This causes imageUri to incorrectly use iconUrls.fallback instead of iconUrls.primary for the new symbol. The component then re-renders correctly after the effect runs, but this creates an unnecessary network request to the fallback URL and could cause a brief visual inconsistency if the fallback loads quickly. The state reset could be made synchronous by calculating the initial URL based on symbol change detection rather than relying on a post-render effect.

🔬 Verification Test

Test code:

// This test demonstrates the bug - checking intermediate state
it('should use primary URL immediately when symbol changes (BUG: uses fallback first)', () => {
  // Simulate the render cycle manually
  let useFallbackUrl = true; // State from previous symbol's primary failure
  const newSymbol = 'ETH';
  const iconUrls = {
    primary: 'https://metamask/.../ETH.svg',
    fallback: 'https://hyperliquid/.../ETH.svg'
  };
  
  // First render with new symbol (before useEffect runs)
  const imageUri = iconUrls
    ? useFallbackUrl
      ? iconUrls.fallback  // <-- BUG: This branch is taken
      : iconUrls.primary
    : null;
  
  console.log('First render imageUri:', imageUri);
  console.log('Expected primary URL:', iconUrls.primary);
  console.log('Bug present:', imageUri === iconUrls.fallback);
});

Command run:

node -e "
let useFallbackUrl = true;
const iconUrls = { primary: 'https://metamask/ETH.svg', fallback: 'https://hyperliquid/ETH.svg' };
const imageUri = iconUrls ? (useFallbackUrl ? iconUrls.fallback : iconUrls.primary) : null;
console.log('First render uses:', imageUri.includes('hyperliquid') ? 'FALLBACK (bug)' : 'PRIMARY (correct)');
console.log('Expected: PRIMARY');
"

Output:

First render uses: FALLBACK (bug)
Expected: PRIMARY

Why this proves the bug: The output shows that when useFallbackUrl is true from a previous symbol's failure, the first render with a new symbol incorrectly selects the fallback URL instead of the primary URL. The useEffect that resets useFallbackUrl to false only runs after this initial render, causing one render cycle with the wrong URL.

Fix in Cursor Fix in Web

Comment on lines 193 to 195
<meta-data android:name="expo.modules.updates.CODE_SIGNING_CERTIFICATE" android:value="-----BEGIN CERTIFICATE-----&#xD;&#xA;MIIC0zCCAbugAwIBAgIJGH+Ulb6OVEeJMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV&#xD;&#xA;BAMTCE1ldGFNYXNrMB4XDTI1MTIxOTIzMTY1NFoXDTM1MTIxOTIzMTY1NFowEzER&#xD;&#xA;MA8GA1UEAxMITWV0YU1hc2swggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB&#xD;&#xA;AQDWvpseYyys+mFZrxGXMDD4VRGIs0u5mXgjwDf7fEOJWI7uldvVcVDPCO1v+/Ig&#xD;&#xA;0nlz/NgDdVvYNgKFJJq4JMLDJNzxNIcaqSCKlO9IU5qWvnnfEeyWx6Pv0NQssHOD&#xD;&#xA;Sc+WnvfCXub3akM9CE6Noy/KbIHpyUNwypux1eU5KGnZ704kNVsWmU7PeRn5Olnl&#xD;&#xA;6t5Q93sIe2fbESDvHYh0TKC9eQvgkvrCCKlMgyqnZb4fdylXGbWRjivp+AKh/DHz&#xD;&#xA;37bz9KtQO7YF8oZ1QvTiSVOb0hS1rzAE/YMatTbC214FQu5/w3vHCe7ejKcNCjzM&#xD;&#xA;xFm6nSd41ho/SpW4r92dt0xvAgMBAAGjKjAoMA4GA1UdDwEB/wQEAwIHgDAWBgNV&#xD;&#xA;HSUBAf8EDDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAQEAn8OKneCOuUcI&#xD;&#xA;wUGRVqWDcej4yYGugWWiIdEgy4as+vXUhMCk47uPwWuKNSILDuPeSxt2lo+AND8U&#xD;&#xA;4N3I87+oYbLktOyph5FtpwUEMC8R/YE8Q5bNmi0LHzzGteenfUhSc+MVhpVVwZ1I&#xD;&#xA;SbHGrj6/oet39FFFqJhWAU+RbMwSnYZKrZTfYELuEppP3WO03P9I8T3XR0d+CmMb&#xD;&#xA;YdeLUvYilZI+3VxKL6tg/UWlZOX8MH6JbtNTm+2YMi1fp/hE8mrd+rR2Re5d87ub&#xD;&#xA;srOY8HZvM0JX4RPddITpfSEwLwrPhdSFRwxyAVmBALYWVHeuiFn2pI3jfNeHF2Lt&#xD;&#xA;SS2hIbf10Q==&#xD;&#xA;-----END CERTIFICATE-----&#xD;&#xA;" />
<meta-data android:name="expo.modules.updates.CODE_SIGNING_METADATA" android:value="{&quot;keyid&quot;:&quot;rc&quot;,&quot;alg&quot;:&quot;rsa-v1_5-sha256&quot;}" />
<meta-data android:name="expo.modules.updates.ENABLED" android:value="true" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may need to be removed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sigh sorry about this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 9, 2026

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokePerps
  • Risk Level: low
  • AI Confidence: 95%
click to see 🤖 AI reasoning details

The changes are entirely contained within the Perps (Perpetuals trading) feature module at app/components/UI/Perps/. The modifications include:

  1. PerpsTokenLogo component: Added fallback URL support for token icons - when the primary MetaMask-hosted icon fails to load, it falls back to HyperLiquid CDN icons.

  2. hyperLiquidConfig.ts: Added a new constant for MetaMask's Perps icons base URL.

  3. marketUtils.ts: Added a new utility function getAssetIconUrls() that returns both primary and fallback URLs.

  4. Unit tests: Updated to cover the new fallback logic.

The PerpsTokenLogo component is used by 13 other components, all within the Perps module (PerpsCard, PerpsMarketHeader, PerpsOrderDetailsView, etc.). No changes affect core wallet functionality, controllers, Engine, or any other features outside of Perps.

This is a UI enhancement that improves resilience by adding fallback icon sources. The changes are well-tested with comprehensive unit tests. Only the SmokePerps tag is needed to verify this functionality.

View GitHub Actions results

@sonarqubecloud
Copy link

sonarqubecloud bot commented Jan 9, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants