Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/cursorPrompts/issue-analysis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@cursor Analyze this MetaMask issue and provide:

1. **Problem**: What's broken (2-3 sentences)
2. **Root Cause**: 1-2 likely causes based on evidence
3. **Target Repo(s)**: Which repo needs the fix (this repo/core/transaction-controller/other controllers in the core repo)
4. **Solutions**: 2-3 approaches with pros/cons, recommend the best one

If a solution is clear, suggest a code diff (highlight with + and - what have changed) that would resolve the issue, but do not open or create a pull request.
16 changes: 1 addition & 15 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -337,27 +337,13 @@ jobs:

# Flask E2E tests

build-android-flask-apks:
name: 'Build Android Flask APKs'
if: ${{ github.event_name != 'merge_group' && needs.needs_e2e_build.outputs.android_changed == 'true' }}
permissions:
contents: read
id-token: write
needs: [needs_e2e_build]
uses: ./.github/workflows/build-android-e2e.yml
with:
build_type: 'flask'
metamask_environment: 'e2e'
keystore_target: 'flask'
secrets: inherit

e2e-smoke-tests-android-flask:
name: 'Android Flask E2E Smoke Tests'
if: ${{ github.event_name != 'merge_group' && needs.needs_e2e_build.outputs.android_changed == 'true' }}
permissions:
contents: read
id-token: write
needs: [needs_e2e_build, build-android-flask-apks]
needs: [needs_e2e_build, build-android-apks]
uses: ./.github/workflows/run-e2e-smoke-tests-android-flask.yml
with:
changed_files: ${{ needs.needs_e2e_build.outputs.changed_files }}
Expand Down
57 changes: 57 additions & 0 deletions .github/workflows/cursor-issue-analysis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Cursor Issue Analysis

on:
issues:
types: [opened, labeled]

permissions:
issues: write
contents: read

jobs:
analyze-issue:
runs-on: ubuntu-latest
# Check if issue has team-confirmations AND (Sev1-high OR Sev2-normal)
if: |
contains(github.event.issue.labels.*.name, 'team-confirmations') &&
(contains(github.event.issue.labels.*.name, 'Sev1-high') || contains(github.event.issue.labels.*.name, 'Sev2-normal'))
steps:
- name: Check for existing @cursor comment
id: check-comment
uses: actions/github-script@v6
with:
script: |
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});

const hasCursorComment = comments.some(comment =>
comment.body.trim().startsWith('@cursor')
);

core.setOutput('exists', hasCursorComment);
return hasCursorComment;

- name: Checkout repository
if: steps.check-comment.outputs.exists != 'true'
uses: actions/checkout@v4
with:
sparse-checkout: .github/cursorPrompts
sparse-checkout-cone-mode: false

- name: Add @cursor analysis comment
if: steps.check-comment.outputs.exists != 'true'
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const body = fs.readFileSync('.github/cursorPrompts/issue-analysis.md', 'utf8');

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: body
});
116 changes: 116 additions & 0 deletions .github/workflows/run-e2e-smoke-tests-android-flask.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,123 @@ permissions:
id-token: write

jobs:
repack-android-flask-apps:
name: 'Repack Android Flask Apps'
runs-on: ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-lg
permissions:
contents: read
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'

- name: Install dependencies
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 30
command: yarn install --immutable

- name: Setup project
run: yarn setup:github-ci --no-build-ios

- name: Configure Keystore
uses: MetaMask/github-tools/.github/actions/configure-keystore@0259e8a920318b02a8860e178d79796eaa08de02
with:
aws-role-to-assume: ${{ secrets.METAMASK_MOBILE_BUILDER_SIGNER_QA }}
aws-region: us-east-2
platform: android
target: qa
env:
ANDROID_KEYSTORE_PATH: android/keystores/internalRelease.keystore

- name: Download Main APK artifacts
uses: actions/download-artifact@v4
with:
path: artifacts/
pattern: main-e2e-release*.apk

- name: Setup Main APK artifacts
run: |
mkdir -p android/app/build/outputs/apk/prod/release/
mkdir -p android/app/build/outputs/apk/androidTest/prod/release/
cp artifacts/main-e2e-release.apk/app-prod-release.apk android/app/build/outputs/apk/prod/release/app-prod-release.apk
cp artifacts/main-e2e-release-androidTest.apk/app-prod-release-androidTest.apk android/app/build/outputs/apk/androidTest/prod/release/app-prod-release-androidTest.apk

- name: Repack Main APK
run: node scripts/repack.js
env:
PLATFORM: android
METAMASK_ENVIRONMENT: e2e
METAMASK_BUILD_TYPE: flask
IS_TEST: 'true'
E2E: 'true'
IGNORE_BOXLOGS_DEVELOPMENT: 'true'
GITHUB_CI: 'true'
CI: 'true'
NODE_OPTIONS: '--max-old-space-size=8192'
BRIDGE_USE_DEV_APIS: 'true'
RAMP_INTERNAL_BUILD: 'true'
SEEDLESS_ONBOARDING_ENABLED: 'true'
MM_NOTIFICATIONS_UI_ENABLED: 'true'
MM_SECURITY_ALERTS_API_ENABLED: 'true'
MM_REMOVE_GLOBAL_NETWORK_SELECTOR: 'true'
FEATURES_ANNOUNCEMENTS_ACCESS_TOKEN: ${{ secrets.FEATURES_ANNOUNCEMENTS_ACCESS_TOKEN }}
FEATURES_ANNOUNCEMENTS_SPACE_ID: ${{ secrets.FEATURES_ANNOUNCEMENTS_SPACE_ID }}
SEGMENT_WRITE_KEY_QA: ${{ secrets.SEGMENT_WRITE_KEY_QA }}
SEGMENT_WRITE_KEY_FLASK: ${{ secrets.SEGMENT_WRITE_KEY_FLASK }}
SEGMENT_PROXY_URL_QA: ${{ secrets.SEGMENT_PROXY_URL_QA }}
SEGMENT_PROXY_URL_FLASK: ${{ secrets.SEGMENT_PROXY_URL_FLASK }}
SEGMENT_DELETE_API_SOURCE_ID_QA: ${{ secrets.SEGMENT_DELETE_API_SOURCE_ID_QA }}
SEGMENT_DELETE_API_SOURCE_ID_FLASK: ${{ secrets.SEGMENT_DELETE_API_SOURCE_ID_FLASK }}
SEGMENT_REGULATIONS_ENDPOINT_QA: ${{ secrets.SEGMENT_REGULATIONS_ENDPOINT_QA }}
SEGMENT_REGULATIONS_ENDPOINT_FLASK: ${{ secrets.SEGMENT_REGULATIONS_ENDPOINT_FLASK }}
MM_SENTRY_DSN_TEST: ${{ secrets.MM_SENTRY_DSN_TEST }}
MM_SENTRY_AUTH_TOKEN: ${{ secrets.MM_SENTRY_AUTH_TOKEN }}
MAIN_IOS_GOOGLE_CLIENT_ID_UAT: ${{ secrets.MAIN_IOS_GOOGLE_CLIENT_ID_UAT }}
FLASK_IOS_GOOGLE_CLIENT_ID_PROD: ${{ secrets.FLASK_IOS_GOOGLE_CLIENT_ID_PROD }}
MAIN_IOS_GOOGLE_REDIRECT_URI_UAT: ${{ secrets.MAIN_IOS_GOOGLE_REDIRECT_URI_UAT }}
FLASK_IOS_GOOGLE_REDIRECT_URI_PROD: ${{ secrets.FLASK_IOS_GOOGLE_REDIRECT_URI_PROD }}
MAIN_ANDROID_APPLE_CLIENT_ID_UAT: ${{ secrets.MAIN_ANDROID_APPLE_CLIENT_ID_UAT }}
FLASK_ANDROID_APPLE_CLIENT_ID_PROD: ${{ secrets.FLASK_ANDROID_APPLE_CLIENT_ID_PROD }}
MAIN_ANDROID_GOOGLE_CLIENT_ID_UAT: ${{ secrets.MAIN_ANDROID_GOOGLE_CLIENT_ID_UAT }}
FLASK_ANDROID_GOOGLE_CLIENT_ID_PROD: ${{ secrets.FLASK_ANDROID_GOOGLE_CLIENT_ID_PROD }}
MAIN_ANDROID_GOOGLE_SERVER_CLIENT_ID_UAT: ${{ secrets.MAIN_ANDROID_GOOGLE_SERVER_CLIENT_ID_UAT }}
FLASK_ANDROID_GOOGLE_SERVER_CLIENT_ID_PROD: ${{ secrets.FLASK_ANDROID_GOOGLE_SERVER_CLIENT_ID_PROD }}
GOOGLE_SERVICES_B64_IOS: ${{ secrets.GOOGLE_SERVICES_B64_IOS }}
GOOGLE_SERVICES_B64_ANDROID: ${{ secrets.GOOGLE_SERVICES_B64_ANDROID }}
MM_INFURA_PROJECT_ID: ${{ secrets.MM_INFURA_PROJECT_ID }}
ANDROID_KEYSTORE_PATH: android/keystores/internalRelease.keystore

- name: Prepare artifacts for upload
run: |
# Copy APKs to workspace root with correct names (no path structure)
cp android/app/build/outputs/apk/prod/release/app-prod-release.apk app-flask-release.apk
cp android/app/build/outputs/apk/androidTest/prod/release/app-prod-release-androidTest.apk app-flask-release-androidTest.apk

- name: Upload Repacked APK
uses: actions/upload-artifact@v4
with:
name: flask-e2e-release.apk
path: app-flask-release.apk
retention-days: 1

- name: Upload Test APK
uses: actions/upload-artifact@v4
with:
name: flask-e2e-release-androidTest.apk
path: app-flask-release-androidTest.apk
retention-days: 1

flask-android-smoke:
needs: [repack-android-flask-apps]
strategy:
matrix:
split: [1, 2, 3]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,13 +182,32 @@ jest.mock('react-redux', () => ({
useSelector: jest.fn(),
}));

// Mock usePerpsConnection hook directly to ensure all hooks that import it get the mock
jest.mock('../../hooks/usePerpsConnection', () => ({
usePerpsConnection: () => ({
isConnected: true,
isConnecting: false,
isInitialized: true,
error: null,
connect: jest.fn(),
disconnect: jest.fn(),
resetError: jest.fn(),
reconnectWithNewContext: jest.fn(),
}),
}));

jest.mock('../../providers/PerpsConnectionProvider', () => ({
PerpsConnectionProvider: ({ children }: { children: React.ReactNode }) =>
children,
usePerpsConnection: () => ({
isConnected: true,
isConnecting: false,
isInitialized: true,
error: null,
connect: jest.fn(),
disconnect: jest.fn(),
resetError: jest.fn(),
reconnectWithNewContext: jest.fn(),
}),
}));

Expand Down Expand Up @@ -266,12 +285,28 @@ jest.mock('../../hooks/usePerpsEventTracking', () => ({
})),
}));

jest.mock('../../hooks/usePerpsPrices', () => ({
usePerpsPrices: jest.fn(() => ({})),
}));

jest.mock('../../hooks/useIsPriceDeviatedAboveThreshold', () => ({
useIsPriceDeviatedAboveThreshold: jest.fn(() => ({
isDeviatedAboveThreshold: false,
isLoading: false,
})),
}));

jest.mock('../../hooks', () => ({
usePerpsLiveAccount: () => mockUsePerpsAccount(),
usePerpsConnection: () => ({
isConnected: true,
isConnecting: false,
isInitialized: true,
error: null,
connect: jest.fn(),
disconnect: jest.fn(),
resetError: jest.fn(),
reconnectWithNewContext: jest.fn(),
}),
usePerpsOpenOrders: () => ({
orders: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {
usePerpsDataMonitor,
type DataMonitorParams,
} from '../../hooks/usePerpsDataMonitor';
import { useIsPriceDeviatedAboveThreshold } from '../../hooks/useIsPriceDeviatedAboveThreshold';
import { usePerpsMeasurement } from '../../hooks/usePerpsMeasurement';
import {
usePerpsLiveAccount,
Expand All @@ -81,6 +82,7 @@ import PerpsPositionCard from '../../components/PerpsPositionCard';
import PerpsMarketStatisticsCard from '../../components/PerpsMarketStatisticsCard';
import type { PerpsTooltipContentKey } from '../../components/PerpsBottomSheetTooltip/PerpsBottomSheetTooltip.types';
import PerpsOICapWarning from '../../components/PerpsOICapWarning';
import PerpsPriceDeviationWarning from '../../components/PerpsPriceDeviationWarning';
import PerpsNotificationTooltip from '../../components/PerpsNotificationTooltip';
import PerpsNavigationCard, {
type NavigationItem,
Expand Down Expand Up @@ -295,6 +297,12 @@ const PerpsMarketDetailsView: React.FC<PerpsMarketDetailsViewProps> = () => {
// Check if market is at open interest cap
const { isAtCap: isAtOICap } = usePerpsOICap(market?.symbol);

// Check if trading is halted due to price deviation
const {
isDeviatedAboveThreshold: isTradingHalted,
isLoading: isLoadingTradingHalted,
} = useIsPriceDeviatedAboveThreshold(market?.symbol);

// Handle data-driven monitoring when coming from order success
// Clear monitoringIntent after processing to allow fresh monitoring next time
const handleDataDetected = useCallback(() => {
Expand Down Expand Up @@ -894,21 +902,19 @@ const PerpsMarketDetailsView: React.FC<PerpsMarketDetailsViewProps> = () => {
)}

{hasHistoricalData ? (
<>
<TradingViewChart
ref={chartRef}
candleData={candleData}
height={PERPS_CHART_CONFIG.LAYOUT.DETAIL_VIEW_HEIGHT}
visibleCandleCount={visibleCandleCount}
tpslLines={tpslLines}
symbol={market?.symbol}
showOverlay={false}
coloredVolume
onOhlcDataChange={setOhlcData}
onNeedMoreHistory={fetchMoreHistory}
testID={`${PerpsMarketDetailsViewSelectorsIDs.CONTAINER}-tradingview-chart`}
/>
</>
<TradingViewChart
ref={chartRef}
candleData={candleData}
height={PERPS_CHART_CONFIG.LAYOUT.DETAIL_VIEW_HEIGHT}
visibleCandleCount={visibleCandleCount}
tpslLines={tpslLines}
symbol={market?.symbol}
showOverlay={false}
coloredVolume
onOhlcDataChange={setOhlcData}
onNeedMoreHistory={fetchMoreHistory}
testID={`${PerpsMarketDetailsViewSelectorsIDs.CONTAINER}-tradingview-chart`}
/>
) : (
<Skeleton
height={PERPS_CHART_CONFIG.LAYOUT.DETAIL_VIEW_HEIGHT}
Expand All @@ -925,6 +931,13 @@ const PerpsMarketDetailsView: React.FC<PerpsMarketDetailsViewProps> = () => {
onMorePress={handleMorePress}
testID={`${PerpsMarketDetailsViewSelectorsIDs.CONTAINER}-candle-period-selector`}
/>

{/* Price Deviation Warning - Shows when price has deviated too much from spot price */}
{market?.symbol && isTradingHalted && !isLoadingTradingHalted && (
<PerpsPriceDeviationWarning
testID={`${PerpsMarketDetailsViewSelectorsIDs.CONTAINER}-price-deviation-warning`}
/>
)}
</View>

{/* OI Cap Warning - Shows when market is at capacity */}
Expand Down Expand Up @@ -1035,7 +1048,7 @@ const PerpsMarketDetailsView: React.FC<PerpsMarketDetailsViewProps> = () => {
</View>

{/* Fixed Actions Footer */}
{(hasAddFundsButton || hasLongShortButtons) && (
{(hasAddFundsButton || hasLongShortButtons) && !isTradingHalted && (
<View style={styles.actionsFooter}>
{hasAddFundsButton && (
<View style={styles.singleActionContainer}>
Expand Down
Loading
Loading