From d9f11c67373425f6d31bb81e05f8fabbbf8bcffd Mon Sep 17 00:00:00 2001
From: Juanmi <95381763+juanmigdr@users.noreply.github.com>
Date: Fri, 12 Dec 2025 09:37:47 +0100
Subject: [PATCH 1/6] fix: O(n) API calls to bulk-scan (#23803)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
After a thorough profiling investigation, one of the issues I found is
that we are making O(n) API calls to the `bulk-scan` endpoint when NFT
auto-detection was being triggered, I have modified the code so that we
make 1 single API call per 250 urls.
Core changes: https://github.com/MetaMask/core/pull/7411
## **Changelog**
CHANGELOG entry: reduced number of calls to bulk-scan for NFT detection
## **Related issues**
Fixes: https://consensyssoftware.atlassian.net/browse/ASSETS-2068
## **Manual testing steps**
```gherkin
Feature: my feature name
Scenario: user [verb for user action]
Given [describe expected initial app state]
When user [verb for user action]
Then [describe expected outcome]
```
## **Screenshots/Recordings**
### **Before**
https://github.com/user-attachments/assets/d0232dec-b144-4f27-b085-0de417a9af20
### **After**
https://github.com/user-attachments/assets/053bca9b-9ee8-439b-bec7-4381fc6bc0b3
## **Pre-merge author checklist**
- [ ] I’ve followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **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]
> Switch NFT detection controller init from `addNft` to bulk `addNfts`
and upgrade `@metamask/assets-controllers` to ^94.0.0.
>
> - **Engine**:
> - Update
`app/core/Engine/controllers/nft-detection-controller-init.ts` to bind
`addNfts` instead of `addNft` when constructing
`NftDetectionController`.
> - Adjust tests in
`app/core/Engine/controllers/nft-detection-controller-init.test.ts` to
expect `addNfts`.
> - **Dependencies**:
> - Bump `@metamask/assets-controllers` to `^94.0.0` (with corresponding
lockfile updates).
>
> Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
8f4a00412d1841625b4b41f84f5b31b7bb3b1d67. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).
---
.../nft-detection-controller-init.test.ts | 4 +-
.../nft-detection-controller-init.ts | 2 +-
package.json | 2 +-
yarn.lock | 74 ++++++++++++++++---
4 files changed, 68 insertions(+), 14 deletions(-)
diff --git a/app/core/Engine/controllers/nft-detection-controller-init.test.ts b/app/core/Engine/controllers/nft-detection-controller-init.test.ts
index 1838c760c27..e940c112427 100644
--- a/app/core/Engine/controllers/nft-detection-controller-init.test.ts
+++ b/app/core/Engine/controllers/nft-detection-controller-init.test.ts
@@ -28,7 +28,7 @@ function getInitRequestMock(): jest.Mocked<
requestMock.getController.mockImplementation((name: string) => {
if (name === 'NftController') {
return {
- addNft: jest.fn(),
+ addNfts: jest.fn(),
state: {},
};
}
@@ -52,7 +52,7 @@ describe('NftDetectionControllerInit', () => {
expect(controllerMock).toHaveBeenCalledWith({
messenger: expect.any(Object),
disabled: false,
- addNft: expect.any(Function),
+ addNfts: expect.any(Function),
getNftState: expect.any(Function),
});
});
diff --git a/app/core/Engine/controllers/nft-detection-controller-init.ts b/app/core/Engine/controllers/nft-detection-controller-init.ts
index c60a493bb08..232341dbc36 100644
--- a/app/core/Engine/controllers/nft-detection-controller-init.ts
+++ b/app/core/Engine/controllers/nft-detection-controller-init.ts
@@ -20,7 +20,7 @@ export const nftDetectionControllerInit: ControllerInitFunction<
const controller = new NftDetectionController({
messenger: controllerMessenger,
disabled: false,
- addNft: nftController.addNft.bind(nftController),
+ addNfts: nftController.addNfts.bind(nftController),
getNftState: () => nftController.state,
});
diff --git a/package.json b/package.json
index 45263e0f298..2288b34da37 100644
--- a/package.json
+++ b/package.json
@@ -198,7 +198,7 @@
"@metamask/address-book-controller": "^7.0.0",
"@metamask/app-metadata-controller": "^2.0.0",
"@metamask/approval-controller": "^8.0.0",
- "@metamask/assets-controllers": "^93.0.0",
+ "@metamask/assets-controllers": "^94.0.0",
"@metamask/base-controller": "^9.0.0",
"@metamask/bitcoin-wallet-snap": "^1.8.0",
"@metamask/bridge-controller": "patch:@metamask/bridge-controller@npm%3A61.0.0#~/.yarn/patches/@metamask-bridge-controller-npm-61.0.0-8c413c463f.patch",
diff --git a/yarn.lock b/yarn.lock
index 7ff52b87dc7..fb0f467e13a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7132,7 +7132,7 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/assets-controllers@npm:^93.0.0, @metamask/assets-controllers@npm:^93.1.0":
+"@metamask/assets-controllers@npm:^93.1.0":
version: 93.1.0
resolution: "@metamask/assets-controllers@npm:93.1.0"
dependencies:
@@ -7186,6 +7186,60 @@ __metadata:
languageName: node
linkType: hard
+"@metamask/assets-controllers@npm:^94.0.0":
+ version: 94.0.0
+ resolution: "@metamask/assets-controllers@npm:94.0.0"
+ dependencies:
+ "@ethereumjs/util": "npm:^9.1.0"
+ "@ethersproject/abi": "npm:^5.7.0"
+ "@ethersproject/address": "npm:^5.7.0"
+ "@ethersproject/bignumber": "npm:^5.7.0"
+ "@ethersproject/contracts": "npm:^5.7.0"
+ "@ethersproject/providers": "npm:^5.7.0"
+ "@metamask/abi-utils": "npm:^2.0.3"
+ "@metamask/account-tree-controller": "npm:^4.0.0"
+ "@metamask/accounts-controller": "npm:^35.0.0"
+ "@metamask/approval-controller": "npm:^8.0.0"
+ "@metamask/base-controller": "npm:^9.0.0"
+ "@metamask/contract-metadata": "npm:^2.4.0"
+ "@metamask/controller-utils": "npm:^11.16.0"
+ "@metamask/core-backend": "npm:^5.0.0"
+ "@metamask/eth-query": "npm:^4.0.0"
+ "@metamask/keyring-api": "npm:^21.0.0"
+ "@metamask/keyring-controller": "npm:^25.0.0"
+ "@metamask/messenger": "npm:^0.3.0"
+ "@metamask/metamask-eth-abis": "npm:^3.1.1"
+ "@metamask/multichain-account-service": "npm:^4.0.1"
+ "@metamask/network-controller": "npm:^27.0.0"
+ "@metamask/permission-controller": "npm:^12.1.1"
+ "@metamask/phishing-controller": "npm:^16.1.0"
+ "@metamask/polling-controller": "npm:^16.0.0"
+ "@metamask/preferences-controller": "npm:^22.0.0"
+ "@metamask/profile-sync-controller": "npm:^27.0.0"
+ "@metamask/rpc-errors": "npm:^7.0.2"
+ "@metamask/snaps-controllers": "npm:^14.0.1"
+ "@metamask/snaps-sdk": "npm:^9.0.0"
+ "@metamask/snaps-utils": "npm:^11.0.0"
+ "@metamask/transaction-controller": "npm:^62.6.0"
+ "@metamask/utils": "npm:^11.8.1"
+ "@types/bn.js": "npm:^5.1.5"
+ "@types/uuid": "npm:^8.3.0"
+ async-mutex: "npm:^0.5.0"
+ bitcoin-address-validation: "npm:^2.2.3"
+ bn.js: "npm:^5.2.1"
+ immer: "npm:^9.0.6"
+ lodash: "npm:^4.17.21"
+ multiformats: "npm:^9.9.0"
+ reselect: "npm:^5.1.1"
+ single-call-balance-checker-abi: "npm:^1.0.0"
+ uuid: "npm:^8.3.2"
+ peerDependencies:
+ "@metamask/providers": ^22.0.0
+ webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0
+ checksum: 10/86324e75db4adffbfc7c4f93138de25242360578e3aa0fd26f78ef84d4390fb04042cb1582d64139754de60f315a9b8a8458850c65b0b764b95eb6435f3bb054
+ languageName: node
+ linkType: hard
+
"@metamask/auth-network-utils@npm:^0.3.0":
version: 0.3.1
resolution: "@metamask/auth-network-utils@npm:0.3.1"
@@ -8394,19 +8448,23 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/multichain-account-service@npm:^4.0.0":
- version: 4.0.0
- resolution: "@metamask/multichain-account-service@npm:4.0.0"
+"@metamask/multichain-account-service@npm:^4.0.0, @metamask/multichain-account-service@npm:^4.0.1":
+ version: 4.0.1
+ resolution: "@metamask/multichain-account-service@npm:4.0.1"
dependencies:
"@ethereumjs/util": "npm:^9.1.0"
+ "@metamask/accounts-controller": "npm:^35.0.0"
"@metamask/base-controller": "npm:^9.0.0"
+ "@metamask/error-reporting-service": "npm:^3.0.0"
"@metamask/eth-snap-keyring": "npm:^18.0.0"
"@metamask/key-tree": "npm:^10.1.1"
"@metamask/keyring-api": "npm:^21.0.0"
+ "@metamask/keyring-controller": "npm:^25.0.0"
"@metamask/keyring-internal-api": "npm:^9.0.0"
"@metamask/keyring-snap-client": "npm:^8.0.0"
"@metamask/keyring-utils": "npm:^3.1.0"
"@metamask/messenger": "npm:^0.3.0"
+ "@metamask/snaps-controllers": "npm:^14.0.1"
"@metamask/snaps-sdk": "npm:^9.0.0"
"@metamask/snaps-utils": "npm:^11.0.0"
"@metamask/superstruct": "npm:^3.1.0"
@@ -8414,13 +8472,9 @@ __metadata:
async-mutex: "npm:^0.5.0"
peerDependencies:
"@metamask/account-api": ^0.12.0
- "@metamask/accounts-controller": ^35.0.0
- "@metamask/error-reporting-service": ^3.0.0
- "@metamask/keyring-controller": ^25.0.0
"@metamask/providers": ^22.0.0
- "@metamask/snaps-controllers": ^14.0.0
webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0
- checksum: 10/b5e5cb6f7d4a8e077935a2a47e230f788ada79cc25829c781e3a26f9b80acaa93980f66bb9d931498400ae3873882e2040066cc83bdea36735029dacb39ad7db
+ checksum: 10/a664bed3b1f54c27c26f0eec2e07b666dbc09d80fb6cad6f081fecc40b6029971988cad0a9cc010ce97fea83b962d31809aae21e37792c19e94dce509eeb98e2
languageName: node
linkType: hard
@@ -34239,7 +34293,7 @@ __metadata:
"@metamask/address-book-controller": "npm:^7.0.0"
"@metamask/app-metadata-controller": "npm:^2.0.0"
"@metamask/approval-controller": "npm:^8.0.0"
- "@metamask/assets-controllers": "npm:^93.0.0"
+ "@metamask/assets-controllers": "npm:^94.0.0"
"@metamask/auto-changelog": "npm:^5.3.0"
"@metamask/base-controller": "npm:^9.0.0"
"@metamask/bitcoin-wallet-snap": "npm:^1.8.0"
From 1387d2b360a42597b65fdb270350af3859ec8807 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Patryk=20=C5=81ucka?=
<5708018+PatrykLucka@users.noreply.github.com>
Date: Fri, 12 Dec 2025 11:24:57 +0100
Subject: [PATCH 2/6] feat: update transition of DeFiProtocolPositionDetails
screen (#23911)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
The DeFi detail page (DeFiProtocolPositionDetails) was transitioning
into the viewport like a bottom sheet (sliding up from bottom) due to
the parent Stack.Navigator using mode={'modal'}.
This PR changes the transition to slide in from right to left (standard
push navigation) by adding a custom cardStyleInterpolator with
horizontal translation, matching the pattern used by other screens like
TrendingTokensFullView, ExploreSearchScreen, and PerpsScreenStack.
## **Changelog**
CHANGELOG entry: Changed DeFi protocol detail page to slide in from
right instead of from bottom
## **Related issues**
Fixes: https://consensyssoftware.atlassian.net/browse/MDP-263
## **Manual testing steps**
```gherkin
Feature: DeFi Protocol Position Details Navigation
Scenario: user opens DeFi protocol position details
Given user is on the Wallet view with DeFi positions visible
When user taps on a DeFi protocol position
Then the DeFi Protocol Position Details screen slides in from right to left
And user can swipe from left edge to go back (gesture navigation)
```
## **Screenshots/Recordings**
### **Before**
https://github.com/user-attachments/assets/f1437617-fac2-4fbb-aa97-6c9a18a03272
### **After**
https://github.com/user-attachments/assets/2521f279-8fd3-4bd1-a49a-c3270e511a70
## **Pre-merge author checklist**
- [x] I’ve followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **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]
> Switches `DeFiProtocolPositionDetails` to a right-to-left slide
transition via custom `cardStyleInterpolator`, updating snapshots
accordingly.
>
> - **Navigation**
> - `app/components/Nav/Main/MainNavigator.js`
> - `DeFiProtocolPositionDetails`: enable `animationEnabled` and add
horizontal `cardStyleInterpolator` (slides in from right), with
`headerShown: true`.
> - **Tests**
> - `app/components/Nav/Main/__snapshots__/MainNavigator.test.tsx.snap`:
update snapshot to include new screen options.
>
> Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
5a7a46fadde7427bd545bc114c37aa966127589c. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).
---
app/components/Nav/Main/MainNavigator.js | 13 +++++++++++++
.../Main/__snapshots__/MainNavigator.test.tsx.snap | 2 ++
2 files changed, 15 insertions(+)
diff --git a/app/components/Nav/Main/MainNavigator.js b/app/components/Nav/Main/MainNavigator.js
index 21428861aea..96a6bb4487a 100644
--- a/app/components/Nav/Main/MainNavigator.js
+++ b/app/components/Nav/Main/MainNavigator.js
@@ -1257,6 +1257,19 @@ const MainNavigator = () => {
component={DeFiProtocolPositionDetails}
options={{
headerShown: true,
+ animationEnabled: true,
+ cardStyleInterpolator: ({ current, layouts }) => ({
+ cardStyle: {
+ transform: [
+ {
+ translateX: current.progress.interpolate({
+ inputRange: [0, 1],
+ outputRange: [layouts.screen.width, 0],
+ }),
+ },
+ ],
+ },
+ }),
}}
/>
{
diff --git a/app/components/Nav/Main/__snapshots__/MainNavigator.test.tsx.snap b/app/components/Nav/Main/__snapshots__/MainNavigator.test.tsx.snap
index bf4ec4e82ea..4e1ad2ac041 100644
--- a/app/components/Nav/Main/__snapshots__/MainNavigator.test.tsx.snap
+++ b/app/components/Nav/Main/__snapshots__/MainNavigator.test.tsx.snap
@@ -265,6 +265,8 @@ exports[`MainNavigator matches rendered snapshot 1`] = `
name="DeFiProtocolPositionDetails"
options={
{
+ "animationEnabled": true,
+ "cardStyleInterpolator": [Function],
"headerShown": true,
}
}
From 0f86a83ca9455fbd97e03395651c6898b9a1c27b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Patryk=20=C5=81ucka?=
<5708018+PatrykLucka@users.noreply.github.com>
Date: Fri, 12 Dec 2025 11:25:02 +0100
Subject: [PATCH 3/6] feat: Update NFT details screen transitions (#23912)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
The NFT details pages (NftDetails and NftDetailsFullImage) were
transitioning into the viewport like a bottom sheet (sliding up from
bottom) due to the parent Stack.Navigator using mode={'modal'}.
This PR changes the transition to slide in from right to left (standard
push navigation) by adding a custom cardStyleInterpolator with
horizontal translation, matching the pattern used by other screens like
TrendingTokensFullView, ExploreSearchScreen, and PerpsScreenStack.
## **Changelog**
CHANGELOG entry: Updated NFT details pages to slide in from right
instead of from bottom
## **Related issues**
Fixes: https://consensyssoftware.atlassian.net/browse/MDP-271
## **Manual testing steps**
```gherkin
Feature: NFT Details Navigation
Scenario: user opens NFT details
Given user is on the Wallet view with NFTs visible
When user taps on an NFT
Then the NFT Details screen slides in from right to left
And user can swipe from left edge to go back (gesture navigation)
Scenario: user opens NFT full image
Given user is viewing NFT details
When user taps on the NFT image to view full screen
Then the NFT Full Image screen slides in from right to left
And user can swipe from left edge to go back (gesture navigation)
```
## **Screenshots/Recordings**
### **Before**
https://github.com/user-attachments/assets/80629490-2cff-4080-8feb-35d2929dfafa
### **After**
https://github.com/user-attachments/assets/8a635fac-4e98-42fe-b251-5b2880072c6b
## **Pre-merge author checklist**
- [x] I’ve followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **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]
> Switch NFT details screens to a right-to-left push animation with a
custom cardStyleInterpolator.
>
> - **Navigation (MainNavigator.js)**:
> - Add `options` to `NftDetails` and `NftDetailsFullImage` to enable
horizontal slide transition (`animationEnabled: true` + custom
`cardStyleInterpolator`).
> - **Tests**:
> - Update snapshot to reflect new `options` on `NftDetails` and
`NftDetailsFullImage`.
>
> Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
d47ecf951dbbb2fe7e82953c1f42321314b96585. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).
---
app/components/Nav/Main/MainNavigator.js | 35 ++++++++++++++++++-
.../__snapshots__/MainNavigator.test.tsx.snap | 12 +++++++
2 files changed, 46 insertions(+), 1 deletion(-)
diff --git a/app/components/Nav/Main/MainNavigator.js b/app/components/Nav/Main/MainNavigator.js
index 96a6bb4487a..a71c31b7200 100644
--- a/app/components/Nav/Main/MainNavigator.js
+++ b/app/components/Nav/Main/MainNavigator.js
@@ -1080,10 +1080,43 @@ const MainNavigator = () => {
component={NotificationsModeView}
/>
-
+ ({
+ cardStyle: {
+ transform: [
+ {
+ translateX: current.progress.interpolate({
+ inputRange: [0, 1],
+ outputRange: [layouts.screen.width, 0],
+ }),
+ },
+ ],
+ },
+ }),
+ }}
+ />
({
+ cardStyle: {
+ transform: [
+ {
+ translateX: current.progress.interpolate({
+ inputRange: [0, 1],
+ outputRange: [layouts.screen.width, 0],
+ }),
+ },
+ ],
+ },
+ }),
+ }}
/>
Date: Fri, 12 Dec 2025 11:25:22 +0100
Subject: [PATCH 4/6] feat: Update StakeScreens with custom navigation options
(#23913)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
When selecting "Deposit" from the Trade Menu, the Deposit page was
sliding in from the bottom (default modal behavior). Per design
requirements, it should slide in from the right, consistent with other
navigation flows.
Applied the same cardStyleInterpolator pattern already used by:
- Settings (Routes.SETTINGS_VIEW)
- TrendingTokensFullView
- ExploreSearchScreen
- SitesFullView
- Perps
## **Changelog**
CHANGELOG entry: Updated Deposit page transition to slide in from the
right instead of bottom
## **Related issues**
Fixes: https://consensyssoftware.atlassian.net/browse/MDP-235
## **Manual testing steps**
```gherkin
Feature: Deposit page navigation transition
Scenario: user opens Deposit page from Trade Menu
Given the user is on the Wallet home screen
And the user has Deposit enabled
When user taps on the Trade/Fund action button
And user selects "Deposit" from the menu
Then the Deposit page slides in from the right side of the screen
And user can swipe from left edge to go back
```
## **Screenshots/Recordings**
### **Before**
https://github.com/user-attachments/assets/fba1e102-92cc-4cf5-8bb5-d94bedbb48a4
### **After**
https://github.com/user-attachments/assets/d18e1e5b-5b89-451d-87d5-b18446180cc9
## **Pre-merge author checklist**
- [x] I’ve followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **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]
> Configures `StakeScreens` to hide the header and use a right-to-left
slide transition; updates snapshot accordingly.
>
> - **Navigation**:
> - **`MainNavigator.js`**: Add custom options to `Stack.Screen` for
`StakeScreens`:
> - `headerShown: false`
> - Enable right-to-left slide via `animationEnabled: true` and custom
`cardStyleInterpolator`.
> - **Tests**:
> - Update snapshot `MainNavigator.test.tsx.snap` to reflect new
`StakeScreens` options.
>
> Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
f185550c377ae047af18ad088d2bc35c00ff99d5. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).
---
app/components/Nav/Main/MainNavigator.js | 21 ++++++++++++++++++-
.../__snapshots__/MainNavigator.test.tsx.snap | 7 +++++++
2 files changed, 27 insertions(+), 1 deletion(-)
diff --git a/app/components/Nav/Main/MainNavigator.js b/app/components/Nav/Main/MainNavigator.js
index a71c31b7200..8eab51c3896 100644
--- a/app/components/Nav/Main/MainNavigator.js
+++ b/app/components/Nav/Main/MainNavigator.js
@@ -1141,7 +1141,26 @@ const MainNavigator = () => {
component={BridgeModalStack}
options={clearStackNavigatorOptions}
/>
-
+ ({
+ cardStyle: {
+ transform: [
+ {
+ translateX: current.progress.interpolate({
+ inputRange: [0, 1],
+ outputRange: [layouts.screen.width, 0],
+ }),
+ },
+ ],
+ },
+ }),
+ }}
+ />
Date: Fri, 12 Dec 2025 12:09:38 +0100
Subject: [PATCH 5/6] fix: perf test perps (#23863)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Fix perps performance e2e tests. Tasks done:
- Updated tests according UI changes
- Remove dead code
- Unified numeric keyboard component for different flows
- Added a patch in appwright to set geoLocation in BrowserStack and
avoid perps blocks -> from main finally
## **Changelog**
CHANGELOG entry:
## **Related issues**
Fixes:
## **Manual testing steps**
```gherkin
Feature: my feature name
Scenario: user [verb for user action]
Given [describe expected initial app state]
When user [verb for user action]
Then [describe expected outcome]
```
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [ ] I’ve followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **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]
> Updates perps performance tests to new UI, adds open/close position
flow, refactors numeric input via shared AmountScreen, and sets
BrowserStack geoLocation to FR.
>
> - **Tests (performance)**:
> - Add `appwright/tests/performance/login/perps-add-funds.spec.js` (add
funds flow + quote timings).
> - Add
`appwright/tests/performance/login/perps-position-management.spec.js`
(select market, set leverage, place order, close position with retry,
per-device account selection).
> - Remove outdated `perps-onboarding.spec.js`.
> - **Screen Objects (Perps)**:
> - New: `PerpsMarketDetailsView`, `PerpsOrderView` (leverage, keypad,
place order), `PerpsPositionDetailsView` (close with retry, state
check), `PerpsPositionsView`, `PerpsClosePositionView`.
> - Update `PerpsDepositScreen`: new getters (`backButton`,
`addFundsButton`, `totalText`), visibility checks, `tapBackButton`,
`isAddFundsVisible`, `isTotalVisible`.
> - Update `PerpsMarketListView`: header expect, `selectMarket(symbol)`.
> - Update `PerpsTabView`: new tab id, add `startTrading` action, expect
usage.
> - **Utilities/Infra**:
> - Add `selectAccountDevice` in `Flows.js` (map device to account);
adjust flows usage.
> - Remove `TimerHelper.withTimer` helper.
> - Set BrowserStack `geoLocation: 'FR'`.
> - `BridgeScreen.enterSourceTokenAmount` now delegates to
`AmountScreen`.
>
> Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
eb2abebca82b9c97ffb833394e5700e158d897fd. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).
---
.../performance/login/perps-add-funds.spec.js | 68 +++++++++++
.../login/perps-position-management.spec.js | 113 ++++++++++++++++++
.../onboarding/perps-onboarding.spec.js | 97 ---------------
appwright/utils/Flows.js | 49 ++++++++
appwright/utils/TimersHelper.js | 16 ---
.../browserstack/BrowserStackConfigBuilder.ts | 1 +
wdio/screen-objects/BridgeScreen.js | 25 +---
wdio/screen-objects/PerpsClosePositionView.js | 24 ++++
wdio/screen-objects/PerpsDepositScreen.js | 32 ++++-
wdio/screen-objects/PerpsMarketDetailsView.js | 31 +++++
wdio/screen-objects/PerpsMarketListView.js | 12 +-
wdio/screen-objects/PerpsOrderView.js | 64 ++++++++++
.../PerpsPositionDetailsView.js | 56 +++++++++
wdio/screen-objects/PerpsPositionsView.js | 23 ++++
wdio/screen-objects/PerpsTabView.js | 13 +-
15 files changed, 480 insertions(+), 144 deletions(-)
create mode 100644 appwright/tests/performance/login/perps-add-funds.spec.js
create mode 100644 appwright/tests/performance/login/perps-position-management.spec.js
delete mode 100644 appwright/tests/performance/onboarding/perps-onboarding.spec.js
create mode 100644 wdio/screen-objects/PerpsClosePositionView.js
create mode 100644 wdio/screen-objects/PerpsMarketDetailsView.js
create mode 100644 wdio/screen-objects/PerpsOrderView.js
create mode 100644 wdio/screen-objects/PerpsPositionDetailsView.js
create mode 100644 wdio/screen-objects/PerpsPositionsView.js
diff --git a/appwright/tests/performance/login/perps-add-funds.spec.js b/appwright/tests/performance/login/perps-add-funds.spec.js
new file mode 100644
index 00000000000..a604f901e00
--- /dev/null
+++ b/appwright/tests/performance/login/perps-add-funds.spec.js
@@ -0,0 +1,68 @@
+import { test } from '../../../fixtures/performance-test.js';
+
+import TimerHelper from '../../../utils/TimersHelper.js';
+import LoginScreen from '../../../../wdio/screen-objects/LoginScreen.js';
+import WalletMainScreen from '../../../../wdio/screen-objects/WalletMainScreen.js';
+import TabBarModal from '../../../../wdio/screen-objects/Modals/TabBarModal.js';
+import WalletActionModal from '../../../../wdio/screen-objects/Modals/WalletActionModal.js';
+import PerpsTutorialScreen from '../../../../wdio/screen-objects/PerpsTutorialScreen.js';
+import PerpsMarketListView from '../../../../wdio/screen-objects/PerpsMarketListView.js';
+import PerpsTabView from '../../../../wdio/screen-objects/PerpsTabView.js';
+import PerpsDepositScreen from '../../../../wdio/screen-objects/PerpsDepositScreen.js';
+import { login } from '../../../utils/Flows.js';
+
+async function screensSetup(device) {
+ const screens = [
+ LoginScreen,
+ WalletMainScreen,
+ TabBarModal,
+ WalletActionModal,
+ PerpsTutorialScreen,
+ PerpsMarketListView,
+ PerpsTabView,
+ PerpsDepositScreen,
+ ];
+ screens.forEach((screen) => {
+ screen.device = device;
+ });
+}
+
+/* Scenario 5: Perps add funds */
+test('Perps add funds', async ({ device, performanceTracker }, testInfo) => {
+ test.setTimeout(10 * 60 * 1000); // 10 minutes
+
+ const selectPerpsMainScreenTimer = new TimerHelper(
+ 'Select Perps Main Screen',
+ );
+ const openAddFundsTimer = new TimerHelper('Open Add Funds');
+ const getQuoteTimer = new TimerHelper('Get Quote');
+ await screensSetup(device);
+
+ await login(device);
+ await TabBarModal.tapActionButton();
+
+ // Open Perps Main Screen
+ selectPerpsMainScreenTimer.start();
+ await WalletActionModal.tapPerpsButton();
+ selectPerpsMainScreenTimer.stop();
+ performanceTracker.addTimer(selectPerpsMainScreenTimer);
+
+ // Skip tutorial
+ await PerpsTutorialScreen.tapSkip();
+
+ // Open Add Funds flow
+ openAddFundsTimer.start();
+ await PerpsTutorialScreen.tapAddFunds();
+ await PerpsDepositScreen.isAmountInputVisible();
+ openAddFundsTimer.stop();
+ performanceTracker.addTimer(openAddFundsTimer);
+
+ // Get quote
+ getQuoteTimer.start();
+ await PerpsDepositScreen.fillUsdAmount(5);
+ await PerpsDepositScreen.isAddFundsVisible();
+ await PerpsDepositScreen.isTotalVisible();
+ getQuoteTimer.stop();
+ performanceTracker.addTimer(getQuoteTimer);
+ await performanceTracker.attachToTest(testInfo);
+});
diff --git a/appwright/tests/performance/login/perps-position-management.spec.js b/appwright/tests/performance/login/perps-position-management.spec.js
new file mode 100644
index 00000000000..e98d1cac4e8
--- /dev/null
+++ b/appwright/tests/performance/login/perps-position-management.spec.js
@@ -0,0 +1,113 @@
+import { test } from '../../../fixtures/performance-test.js';
+
+import TimerHelper from '../../../utils/TimersHelper.js';
+import OnboardingSheet from '../../../../wdio/screen-objects/Onboarding/OnboardingSheet.js';
+import CreatePasswordScreen from '../../../../wdio/screen-objects/Onboarding/CreatePasswordScreen.js';
+import WalletMainScreen from '../../../../wdio/screen-objects/WalletMainScreen.js';
+import TabBarModal from '../../../../wdio/screen-objects/Modals/TabBarModal.js';
+import WalletActionModal from '../../../../wdio/screen-objects/Modals/WalletActionModal.js';
+import PerpsTutorialScreen from '../../../../wdio/screen-objects/PerpsTutorialScreen.js';
+import PerpsMarketListView from '../../../../wdio/screen-objects/PerpsMarketListView.js';
+import PerpsTabView from '../../../../wdio/screen-objects/PerpsTabView.js';
+import PerpsDepositScreen from '../../../../wdio/screen-objects/PerpsDepositScreen.js';
+import PerpsMarketDetailsView from '../../../../wdio/screen-objects/PerpsMarketDetailsView.js';
+import PerpsOrderView from '../../../../wdio/screen-objects/PerpsOrderView.js';
+import PerpsClosePositionView from '../../../../wdio/screen-objects/PerpsClosePositionView.js';
+import PerpsPositionDetailsView from '../../../../wdio/screen-objects/PerpsPositionDetailsView.js';
+import PerpsPositionsView from '../../../../wdio/screen-objects/PerpsPositionsView.js';
+import { login, selectAccountDevice } from '../../../utils/Flows.js';
+
+async function screensSetup(device) {
+ const screens = [
+ OnboardingSheet,
+ CreatePasswordScreen,
+ WalletMainScreen,
+ TabBarModal,
+ WalletActionModal,
+ PerpsTutorialScreen,
+ PerpsMarketListView,
+ PerpsTabView,
+ PerpsDepositScreen,
+ PerpsMarketDetailsView,
+ PerpsOrderView,
+ PerpsClosePositionView,
+ PerpsPositionDetailsView,
+ PerpsPositionsView,
+ ];
+ screens.forEach((screen) => {
+ screen.device = device;
+ });
+}
+
+/* Scenario 5: Perps onboarding + add funds 10 USD ARB.USDC + Open Position + Close Position */
+test('Perps open position and close it', async ({
+ device,
+ performanceTracker,
+}, testInfo) => {
+ test.setTimeout(10 * 60 * 1000); // 10 minutes
+
+ const selectPerpsMainScreenTimer = new TimerHelper(
+ 'Select Perps Main Screen',
+ );
+ const skipTutorialTimer = new TimerHelper('Skip Tutorial');
+ const selectMarketTimer = new TimerHelper('Select Market BTC');
+ const openOrderScreenTimer = new TimerHelper('Open Order Screen');
+ const openPositionTimer = new TimerHelper('Open Long Position');
+ const setLeverageTimer = new TimerHelper('Set Leverage');
+ const closePositionTimer = new TimerHelper('Close Position');
+ await screensSetup(device);
+ await login(device);
+
+ // Perps requires independent account for each device to avoid clashes when running tests in parallel
+ await selectAccountDevice(device, testInfo);
+
+ await TabBarModal.tapActionButton();
+
+ selectPerpsMainScreenTimer.start();
+ await WalletActionModal.tapPerpsButton();
+ selectPerpsMainScreenTimer.stop();
+ performanceTracker.addTimer(selectPerpsMainScreenTimer);
+
+ // Skip tutorial
+ skipTutorialTimer.start();
+ await PerpsTutorialScreen.tapSkip();
+ skipTutorialTimer.stop();
+ performanceTracker.addTimer(skipTutorialTimer);
+
+ selectMarketTimer.start();
+ // Selecting BTC market
+ await PerpsMarketListView.selectMarket('BTC');
+ selectMarketTimer.stop();
+ performanceTracker.addTimer(selectMarketTimer);
+
+ // TODO: Add a check to see if the position is open
+ // If position open, fail the test
+ if (await PerpsPositionDetailsView.isPositionOpen()) {
+ throw new Error('Position is already open');
+ }
+
+ // Open Position
+ openOrderScreenTimer.start();
+ await PerpsMarketDetailsView.tapLongButton();
+ openOrderScreenTimer.stop();
+ performanceTracker.addTimer(openOrderScreenTimer);
+
+ // Set leverage to 40x
+ setLeverageTimer.start();
+ await PerpsOrderView.setLeverage(40);
+ setLeverageTimer.stop();
+ performanceTracker.addTimer(setLeverageTimer);
+
+ openPositionTimer.start();
+ await PerpsOrderView.tapPlaceOrder();
+ openPositionTimer.stop();
+ performanceTracker.addTimer(openPositionTimer);
+
+ // Close Position
+ closePositionTimer.start();
+ await PerpsPositionDetailsView.closePositionWithRetry();
+ closePositionTimer.stop();
+ performanceTracker.addTimer(closePositionTimer);
+
+ await performanceTracker.attachToTest(testInfo);
+});
diff --git a/appwright/tests/performance/onboarding/perps-onboarding.spec.js b/appwright/tests/performance/onboarding/perps-onboarding.spec.js
deleted file mode 100644
index 195a3fc3493..00000000000
--- a/appwright/tests/performance/onboarding/perps-onboarding.spec.js
+++ /dev/null
@@ -1,97 +0,0 @@
-import { test } from '../../../fixtures/performance-test.js';
-
-import TimerHelper from '../../../utils/TimersHelper.js';
-import OnboardingSheet from '../../../../wdio/screen-objects/Onboarding/OnboardingSheet.js';
-import ImportFromSeedScreen from '../../../../wdio/screen-objects/Onboarding/ImportFromSeedScreen.js';
-import CreatePasswordScreen from '../../../../wdio/screen-objects/Onboarding/CreatePasswordScreen.js';
-import WalletMainScreen from '../../../../wdio/screen-objects/WalletMainScreen.js';
-import TabBarModal from '../../../../wdio/screen-objects/Modals/TabBarModal.js';
-import WalletActionModal from '../../../../wdio/screen-objects/Modals/WalletActionModal.js';
-import PerpsTutorialScreen from '../../../../wdio/screen-objects/PerpsTutorialScreen.js';
-import PerpsMarketListView from '../../../../wdio/screen-objects/PerpsMarketListView.js';
-import PerpsTabView from '../../../../wdio/screen-objects/PerpsTabView.js';
-import PerpsDepositScreen from '../../../../wdio/screen-objects/PerpsDepositScreen.js';
-import { onboardingFlowImportSRP } from '../../../utils/Flows.js';
-
-async function screensSetup(device) {
- const screens = [
- OnboardingSheet,
- ImportFromSeedScreen,
- CreatePasswordScreen,
- WalletMainScreen,
- TabBarModal,
- WalletActionModal,
- PerpsTutorialScreen,
- PerpsMarketListView,
- PerpsTabView,
- PerpsDepositScreen,
- ];
- screens.forEach((screen) => {
- screen.device = device;
- });
-}
-
-/* Scenario 5: Perps onboarding + add funds 10 USD ARB.USDC */
-// TODO: Fix this test: https://consensyssoftware.atlassian.net/browse/MMQA-1190
-test.skip('Perps onboarding + add funds 10 USD ARB.USDC', async ({
- device,
- performanceTracker,
-}, testInfo) => {
- test.setTimeout(10 * 60 * 1000); // 10 minutes
- await screensSetup(device);
-
- await onboardingFlowImportSRP(device, process.env.TEST_SRP_3);
- await WalletMainScreen.isTokenVisible('ETH');
- await TabBarModal.tapTradeButton();
-
- // Open Perps tab
- await TimerHelper.withTimer(
- performanceTracker,
- 'Open Perps tab',
- async () => {
- await PerpsTabView.tapPerpsTab();
- await PerpsTutorialScreen.expectFirstScreenVisible();
- },
- );
- // Open Tutorial flow
- await PerpsTutorialScreen.flowTapContinueTutorial(6);
-
- // Open Add Funds flow
- await TimerHelper.withTimer(
- performanceTracker,
- 'Open Add Funds',
- async () => {
- await PerpsTutorialScreen.tapAddFunds();
- await PerpsDepositScreen.isAmountInputVisible();
- },
- );
- // Select pay token
- await TimerHelper.withTimer(
- performanceTracker,
- 'Select pay token - 1 click USDC.arb',
- async () => {
- await PerpsDepositScreen.tapPayWith();
- await PerpsDepositScreen.selectPayTokenByText('USDC');
- },
- );
-
- // Fill amount
- await TimerHelper.withTimer(
- performanceTracker,
- 'Fill amount - 2 USD',
- async () => {
- await PerpsDepositScreen.fillUsdAmount('2');
- },
- );
-
- // Cancel
- await TimerHelper.withTimer(
- performanceTracker,
- 'Cancel - 1 click',
- async () => {
- await PerpsDepositScreen.checkTransactionFeeIsVisible();
- },
- );
-
- await performanceTracker.attachToTest(testInfo);
-});
diff --git a/appwright/utils/Flows.js b/appwright/utils/Flows.js
index 1ecd29d43b5..e1696f31bc2 100644
--- a/appwright/utils/Flows.js
+++ b/appwright/utils/Flows.js
@@ -19,6 +19,55 @@ import AppwrightGestures from '../../e2e/framework/AppwrightGestures.js';
import AppwrightSelectors from '../../e2e/framework/AppwrightSelectors.js';
import { expect } from 'appwright';
+export async function selectAccountDevice(device, testInfo) {
+ // Access device name from testInfo.project.use.device
+ const deviceName = testInfo.project.use.device.name;
+ console.log(`📱 Device executing the test: ${deviceName}`);
+
+ let accountName;
+
+ // Define account mapping based on device name
+ // The device names must match those in appwright.config.ts or device-matrix.json
+ switch (deviceName) {
+ case 'Samsung Galaxy S23 Ultra':
+ accountName = 'Account 3';
+ break;
+ case 'Google Pixel 8 Pro':
+ console.log(
+ `🔄 Account 1 is selected by default in the app for device: ${deviceName}`,
+ );
+ return;
+ case 'iPhone 16 Pro Max':
+ accountName = 'Account 4';
+ break;
+ case 'iPhone 12':
+ accountName = 'Account 5';
+ break;
+ default:
+ console.log(
+ `🔄 Account 1 is selected by default in the app for device: ${deviceName}`,
+ );
+ return;
+ }
+ // Account 2 is called stable and not used in this function
+
+ console.log(
+ `🔄 Switching to account: ${accountName} for device: ${deviceName}`,
+ );
+
+ // Set device for screen objects
+ WalletMainScreen.device = device;
+ AccountListComponent.device = device;
+
+ // Perform account switch
+ await WalletMainScreen.tapIdenticon();
+ await AccountListComponent.isComponentDisplayed();
+ await AccountListComponent.tapOnAccountByName(accountName);
+
+ // Verify we are back on main screen (tapping account usually closes modal)
+ await WalletMainScreen.isMainWalletViewVisible();
+}
+
export async function onboardingFlowImportSRP(device, srp) {
WelcomeScreen.device = device;
TermOfUseScreen.device = device;
diff --git a/appwright/utils/TimersHelper.js b/appwright/utils/TimersHelper.js
index 44620eea341..2fb62ff5a82 100644
--- a/appwright/utils/TimersHelper.js
+++ b/appwright/utils/TimersHelper.js
@@ -56,22 +56,6 @@ class TimerHelper {
get id() {
return this._id;
}
-
- // Runs the provided async function while timing it, and automatically
- // registers the timer with the given performanceTracker.
- // Usage:
- // await TimerHelper.withTimer(performanceTracker, 'Step name', async () => { /* ... */ });
- static async withTimer(performanceTracker, id, fn) {
- const timer = new TimerHelper(id);
- timer.start();
- try {
- const result = await fn();
- return result;
- } finally {
- timer.stop();
- performanceTracker.addTimer(timer);
- }
- }
}
export default TimerHelper;
diff --git a/e2e/framework/services/providers/browserstack/BrowserStackConfigBuilder.ts b/e2e/framework/services/providers/browserstack/BrowserStackConfigBuilder.ts
index e67d6c89acb..c9c00497e75 100644
--- a/e2e/framework/services/providers/browserstack/BrowserStackConfigBuilder.ts
+++ b/e2e/framework/services/providers/browserstack/BrowserStackConfigBuilder.ts
@@ -64,6 +64,7 @@ export class BrowserStackConfigBuilder {
appProfiling: 'true',
selfHeal: 'true',
networkProfile: '4g-lte-advanced-good',
+ geoLocation: 'FR',
},
'appium:autoGrantPermissions': true,
'appium:app': appBsUrl,
diff --git a/wdio/screen-objects/BridgeScreen.js b/wdio/screen-objects/BridgeScreen.js
index c29f50ece01..cc47d4da7b3 100644
--- a/wdio/screen-objects/BridgeScreen.js
+++ b/wdio/screen-objects/BridgeScreen.js
@@ -7,6 +7,7 @@ import { QuoteViewSelectorText } from '../../e2e/selectors/swaps/QuoteView.selec
import Selectors from '../helpers/Selectors.js';
import { LoginViewSelectors } from '../../e2e/selectors/wallet/LoginView.selectors';
import { splitAmountIntoDigits } from 'appwright/utils/Utils.js';
+import AmountScreen from './AmountScreen';
class BridgeScreen {
@@ -64,28 +65,8 @@ class BridgeScreen {
}
async enterSourceTokenAmount(amount) {
- // Split amount into digits
- const digits = splitAmountIntoDigits(amount);
- console.log('Amount digits:', digits);
- for (const digit of digits) {
- if (AppwrightSelectors.isAndroid(this._device)) {
- if (digit != '.') {
- const numberKey = await AppwrightSelectors.getElementByXpath(this._device, `//android.widget.Button[@content-desc='${digit}']`)
- await appwrightExpect(numberKey).toBeVisible({ timeout: 30000 });
- await AppwrightGestures.tap(numberKey);
- }
- else {
- const numberKey = await AppwrightSelectors.getElementByXpath(this._device, `//android.view.View[@text="."]`);
- await appwrightExpect(numberKey).toBeVisible({ timeout: 30000 });
- await AppwrightGestures.tap(numberKey);
- }
- }
- else {
- const numberKey = await AppwrightSelectors.getElementByXpath(this._device, `//XCUIElementTypeButton[@name="${digit}"]`);
- await appwrightExpect(numberKey).toBeVisible({ timeout: 30000 });
- await AppwrightGestures.tap(numberKey);
- }
- }
+ AmountScreen.device = this._device;
+ await AmountScreen.enterAmount(amount);
}
async selectNetworkAndTokenTo(network, token) {
diff --git a/wdio/screen-objects/PerpsClosePositionView.js b/wdio/screen-objects/PerpsClosePositionView.js
new file mode 100644
index 00000000000..bcbe90ba097
--- /dev/null
+++ b/wdio/screen-objects/PerpsClosePositionView.js
@@ -0,0 +1,24 @@
+import AppwrightSelectors from '../../e2e/framework/AppwrightSelectors';
+import AppwrightGestures from '../../e2e/framework/AppwrightGestures';
+
+class PerpsClosePositionView {
+ get device() {
+ return this._device;
+ }
+
+ set device(device) {
+ this._device = device;
+ }
+
+ get confirmButton() {
+ return AppwrightSelectors.getElementByID(this._device, 'close-position-confirm-button');
+ }
+
+ async tapConfirmButton() {
+ await AppwrightGestures.tap(this.confirmButton);
+ }
+}
+
+export default new PerpsClosePositionView();
+
+
diff --git a/wdio/screen-objects/PerpsDepositScreen.js b/wdio/screen-objects/PerpsDepositScreen.js
index 8edeb2180df..881ae6a1c17 100644
--- a/wdio/screen-objects/PerpsDepositScreen.js
+++ b/wdio/screen-objects/PerpsDepositScreen.js
@@ -1,7 +1,7 @@
import AppwrightSelectors from '../../e2e/framework/AppwrightSelectors';
import AppwrightGestures from '../../e2e/framework/AppwrightGestures';
import AmountScreen from './AmountScreen';
-import { expect } from 'appwright';
+import { expect as appwrightExpect } from 'appwright';
class PerpsDepositScreen {
@@ -26,6 +26,10 @@ class PerpsDepositScreen {
return AppwrightSelectors.getElementByID(this._device, 'custom-amount-input');
}
+ get backButton() {
+ return AppwrightSelectors.getElementByID(this._device, 'Add funds-navbar-back-button');
+ }
+
get payWithButton() {
return AppwrightSelectors.getElementByCatchAll(
this._device,
@@ -33,9 +37,17 @@ class PerpsDepositScreen {
);
}
+ get addFundsButton() {
+ return AppwrightSelectors.getElementByText(this._device, 'Add funds');
+ }
+
+ get totalText() {
+ return AppwrightSelectors.getElementByText(this._device, 'Total');
+ }
+
async isAmountInputVisible() {
const input = await this.amountInput;
- await input.isVisible({ timeout: 15000 });
+ await appwrightExpect(input).toBeVisible();
}
async selectPayTokenByText(token) {
@@ -61,9 +73,23 @@ class PerpsDepositScreen {
await AppwrightGestures.tap(this.cancelButton); // Use static tap method with retry logic
}
+ async tapBackButton() {
+ await AppwrightGestures.tap(this.backButton); // Use static tap method with retry logic
+ }
+
async checkTransactionFeeIsVisible() {
const transactionFee = await AppwrightSelectors.getElementByID(this._device, 'bridge-fee-row');
- await expect(transactionFee).toBeVisible();
+ await appwrightExpect(transactionFee).toBeVisible();
+ }
+
+ async isAddFundsVisible() {
+ const addFunds = await this.addFundsButton;
+ await appwrightExpect(addFunds).toBeVisible();
+ }
+
+ async isTotalVisible() {
+ const total = await AppwrightSelectors.getElementByText(this._device, 'Total');
+ await appwrightExpect(total).toBeVisible();
}
}
diff --git a/wdio/screen-objects/PerpsMarketDetailsView.js b/wdio/screen-objects/PerpsMarketDetailsView.js
new file mode 100644
index 00000000000..f3f85ef828b
--- /dev/null
+++ b/wdio/screen-objects/PerpsMarketDetailsView.js
@@ -0,0 +1,31 @@
+import AppwrightSelectors from '../../e2e/framework/AppwrightSelectors';
+import AppwrightGestures from '../../e2e/framework/AppwrightGestures';
+
+class PerpsMarketDetailsView {
+ get device() {
+ return this._device;
+ }
+
+ set device(device) {
+ this._device = device;
+ }
+
+ get longButton() {
+ return AppwrightSelectors.getElementByID(this._device, 'perps-market-details-long-button');
+ }
+
+ get shortButton() {
+ return AppwrightSelectors.getElementByID(this._device, 'perps-market-details-short-button');
+ }
+
+ async tapLongButton() {
+ await AppwrightGestures.tap(this.longButton);
+ }
+
+ async tapShortButton() {
+ await AppwrightGestures.tap(this.shortButton);
+ }
+}
+
+export default new PerpsMarketDetailsView();
+
diff --git a/wdio/screen-objects/PerpsMarketListView.js b/wdio/screen-objects/PerpsMarketListView.js
index 9ce4137e1d1..4b5d92b4723 100644
--- a/wdio/screen-objects/PerpsMarketListView.js
+++ b/wdio/screen-objects/PerpsMarketListView.js
@@ -1,5 +1,6 @@
import AppwrightSelectors from '../../e2e/framework/AppwrightSelectors';
import AppwrightGestures from '../../e2e/framework/AppwrightGestures';
+import { expect as appwrightExpect } from 'appwright';
class PerpsMarketListView {
@@ -22,15 +23,18 @@ class PerpsMarketListView {
async isHeaderVisible() {
const header = await this.listHeader;
- await header.isVisible({ timeout: 10000 });
+ await appwrightExpect(header).toBeVisible({ timeout: 10000 });
}
async tapBackButtonMarketList() {
await AppwrightGestures.tap(this.backButtonMarketList); // Use static tap method with retry logic
}
+
+ async selectMarket(symbol) {
+ // ID format from Perps.selectors.ts: `perps-market-row-item-${symbol}`
+ const marketRow = await AppwrightSelectors.getElementByID(this._device, `perps-market-row-item-${symbol}`);
+ await AppwrightGestures.tap(marketRow);
+ }
}
export default new PerpsMarketListView();
-
-
-
diff --git a/wdio/screen-objects/PerpsOrderView.js b/wdio/screen-objects/PerpsOrderView.js
new file mode 100644
index 00000000000..5df4e2eec74
--- /dev/null
+++ b/wdio/screen-objects/PerpsOrderView.js
@@ -0,0 +1,64 @@
+import AppwrightSelectors from '../../e2e/framework/AppwrightSelectors';
+import AppwrightGestures from '../../e2e/framework/AppwrightGestures';
+import AmountScreen from './AmountScreen';
+import { expect as appwrightExpect } from 'appwright';
+import { splitAmountIntoDigits } from 'appwright/utils/Utils';
+import PerpsPositionDetailsView from './PerpsPositionDetailsView';
+
+class PerpsOrderView {
+ get device() {
+ return this._device;
+ }
+
+ set device(device) {
+ this._device = device;
+ }
+
+ get placeOrderButton() {
+ return AppwrightSelectors.getElementByID(this._device, 'perps-order-view-place-order-button');
+ }
+
+ get keypad() {
+ return AppwrightSelectors.getElementByID(this._device, 'perps-order-view-keypad');
+ }
+
+ get leverageButton() {
+ return AppwrightSelectors.getElementByText(this._device, 'Leverage');
+ }
+
+ async leverageOption(leverage) {
+ return AppwrightSelectors.getElementByText(this._device, `${leverage}x`);
+ }
+
+ async confirmLeverageButton(leverage) {
+ return AppwrightSelectors.getElementByText(this._device, `Set ${leverage}x`);
+ }
+
+ async tapPlaceOrder() {
+ await AppwrightGestures.tap(this.placeOrderButton);
+ appwrightExpect(await PerpsPositionDetailsView.isPositionOpen()).toBe(true);
+ }
+
+ // Reuse logic from AmountScreen.js for Keypad interaction
+ async tapNumberKey(digit) {
+ AmountScreen.device = this._device;
+ await AmountScreen.tapNumberKey(digit);
+ }
+
+ async enterAmount(text) {
+ // Since PerpsOrderView likely only supports keypad input for amount in the UI flow being tested
+ const digits = splitAmountIntoDigits(text);
+ for (const digit of digits) {
+ console.log('Tapping digit:', digit);
+ await this.tapNumberKey(digit);
+ }
+ }
+
+ async setLeverage(leverage) {
+ await AppwrightGestures.tap(this.leverageButton);
+ await AppwrightGestures.tap(await this.leverageOption(leverage));
+ await AppwrightGestures.tap(await this.confirmLeverageButton(leverage));
+ }
+}
+
+export default new PerpsOrderView();
\ No newline at end of file
diff --git a/wdio/screen-objects/PerpsPositionDetailsView.js b/wdio/screen-objects/PerpsPositionDetailsView.js
new file mode 100644
index 00000000000..07fdc3b2cce
--- /dev/null
+++ b/wdio/screen-objects/PerpsPositionDetailsView.js
@@ -0,0 +1,56 @@
+import AppwrightSelectors from '../../e2e/framework/AppwrightSelectors';
+import AppwrightGestures from '../../e2e/framework/AppwrightGestures';
+import Utilities from '../../e2e/framework/Utilities';
+
+class PerpsPositionDetailsView {
+ get device() {
+ return this._device;
+ }
+
+ set device(device) {
+ this._device = device;
+ }
+
+ get closePositionButton() {
+ return AppwrightSelectors.getElementByID(this._device, 'perps-market-details-close-button');
+ }
+
+ get positionOpenButton() {
+ return AppwrightSelectors.getElementByID(this._device, 'position-open-button');
+ }
+
+ get confirmClosePositionButton() {
+ return AppwrightSelectors.getElementByID(this._device, 'close-position-confirm-button');
+ }
+
+ async tapClosePositionButton() {
+ await AppwrightGestures.tap(this.closePositionButton);
+ await AppwrightGestures.tap(this.confirmClosePositionButton);
+ }
+
+ async isPositionOpen() {
+ const closePositionButton = await this.closePositionButton;
+ return await closePositionButton.isVisible();
+ }
+
+ async closePositionWithRetry() {
+ await Utilities.executeWithRetry(async () => {
+ if (await this.isPositionOpen()) {
+ await this.tapClosePositionButton();
+ const closePositionButton = await this.closePositionButton;
+ await AppwrightSelectors.waitForElementToDisappear(
+ closePositionButton,
+ 'Close Position Button',
+ 5000,
+ );
+ }
+ }, {
+ description: 'close position',
+ elemDescription: 'Close Position Button',
+ });
+ }
+}
+
+export default new PerpsPositionDetailsView();
+
+
diff --git a/wdio/screen-objects/PerpsPositionsView.js b/wdio/screen-objects/PerpsPositionsView.js
new file mode 100644
index 00000000000..b9454c4dd65
--- /dev/null
+++ b/wdio/screen-objects/PerpsPositionsView.js
@@ -0,0 +1,23 @@
+import AppwrightSelectors from '../../e2e/framework/AppwrightSelectors';
+import AppwrightGestures from '../../e2e/framework/AppwrightGestures';
+
+class PerpsPositionsView {
+ get device() {
+ return this._device;
+ }
+
+ set device(device) {
+ this._device = device;
+ }
+
+ get positionItem() {
+ return AppwrightSelectors.getElementByID(this._device, 'perps-positions-item');
+ }
+
+ async tapPositionItem() {
+ await AppwrightGestures.tap(this.positionItem);
+ }
+}
+
+export default new PerpsPositionsView();
+
diff --git a/wdio/screen-objects/PerpsTabView.js b/wdio/screen-objects/PerpsTabView.js
index 27e5547167e..7ecbda41d3d 100644
--- a/wdio/screen-objects/PerpsTabView.js
+++ b/wdio/screen-objects/PerpsTabView.js
@@ -1,5 +1,6 @@
import AppwrightSelectors from '../../e2e/framework/AppwrightSelectors';
import AppwrightGestures from '../../e2e/framework/AppwrightGestures';
+import { expect as appwrightExpect } from 'appwright';
class PerpsTabView {
@@ -13,7 +14,7 @@ class PerpsTabView {
}
get perpsTabButton() {
- return AppwrightSelectors.getElementByID(this._device, 'wallet-perps-action');
+ return AppwrightSelectors.getElementByID(this._device, 'undefined-tab-1');
}
get addFundsButton() {
@@ -24,17 +25,25 @@ class PerpsTabView {
return AppwrightSelectors.getElementByID(this._device, 'perps-start-trading-button');
}
+ get startTradingButton() {
+ return AppwrightSelectors.getElementByText(this._device, 'Start trading');
+ }
+
async tapPerpsTab() {
await AppwrightGestures.tap(this.perpsTabButton); // Use static tap method with retry logic
}
+ async tapStartTradingButton() {
+ await AppwrightGestures.tap(this.startTradingButton); // Use static tap method with retry logic
+ }
+
async tapAddFunds() {
await AppwrightGestures.tap(this.addFundsButton); // Use static tap method with retry logic
}
async tapOnboardingButton() {
const button = await this.onboardingButton;
- await button.isVisible({ timeout: 5000 });
+ await appwrightExpect(button).toBeVisible({ timeout: 5000 });
await AppwrightGestures.tap(this.onboardingButton); // Use static tap method with retry logic
}
}
From 28a08fe35ec7ecb755403889c0e62a67ed6ec437 Mon Sep 17 00:00:00 2001
From: Jorge Carrasco
Date: Fri, 12 Dec 2025 12:30:04 +0100
Subject: [PATCH 6/6] chore(infra): optimize Android E2E build for lg runner
(16 vCPUs, 48GB) (#23869)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
This PR optimizes Android E2E builds to prevent "Gradle Daemon
disappeared unexpectedly" crashes on the LG runner (16 vCPUs, 48GB RAM).
### Root Cause Analysis
The "Daemon disappeared" symptom is consistent with **process
termination under memory pressure** (e.g., OS OOM kill). On **lg
(48GB)**, the previous GitHub Actions Gradle config (`-Xmx16g`,
`workers.max=6`, `daemon=true`) could overlap with Node/Metro memory
usage and native compilation spikes.
### Solution: Optimized Gradle Memory Settings
Following [Gradle 8.10.2 Performance Best
Practices](https://docs.gradle.org/8.10.2/userguide/performance.html),
we tuned `gradle.properties.github` for the 48GB runner.
#### Improved Gradle Logging for E2E
- E2E builds now run Gradle with **`--stacktrace --info`** via
`scripts/build.sh` to provide more actionable logs when the build fails.
- Android CI uses **JDK 17** via `setup-e2e-env` defaults:
[setup-e2e-env
action](https://github.com/MetaMask/github-tools/blob/v1/.github/actions/setup-e2e-env/action.yml)
#### JVM Memory Changes
| Setting | Before | After | Reason |
|---------|--------|-------|--------|
| **Heap (`-Xmx`)** | 16GB | **12GB** | Leave room for Node.js/Metro |
| **Initial Heap (`-Xms`)** | (none) | **4GB** | Set JVM initial heap to
reduce early heap resizing: [Java launcher
docs](https://docs.oracle.com/en/java/javase/17/docs/specs/man/java.html)
|
| **MaxGCPauseMillis** | (none) | **500ms** | Tune G1 pause-time goal
for CI throughput: [G1 GC tuning
guide](https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1.html)
|
| **ExitOnOutOfMemoryError** | (none) | **enabled** | Fail fast on JVM
OOM: [Java launcher
docs](https://docs.oracle.com/en/java/javase/17/docs/specs/man/java.html)
|
| **file.encoding** | (none) | **UTF-8** | Pin JVM default charset (JDK
17 default can depend on OS/locale):
[Charset.defaultCharset()](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/nio/charset/Charset.html#defaultCharset())
|
| **OptimizeStringConcat** | enabled | **removed** | Remove non-standard
`-XX` tuning flag from baseline config (prefer defaults): [Java launcher
docs](https://docs.oracle.com/en/java/javase/17/docs/specs/man/java.html)
|
#### Gradle Settings Changes
| Setting | Before | After | Reason |
|---------|--------|-------|--------|
| **Workers** | 6 | **2** | Prevent memory contention |
| **Daemon** | true | **false** | Disable persistent daemon between CI
builds: [Gradle
Daemon](https://docs.gradle.org/8.10.2/userguide/gradle_daemon.html#sec:disabling_the_daemon)
|
| **configureondemand** | true | **removed** | Not recommended for
modern Gradle + Android builds: [Gradle performance
guide](https://docs.gradle.org/8.10.2/userguide/performance.html);
[Android/AGP
compatibility](https://stackoverflow.com/questions/49990933/configuration-on-demand-is-not-supported-by-the-current-version-of-the-android-g)
|
#### Unchanged Settings (kept as-is)
| Setting | Value | Why Kept |
|---------|-------|----------|
| `parallel` | true | [Recommended for multi-project
builds](https://docs.gradle.org/8.10.2/userguide/performance.html#parallel_execution)
|
| `caching` | true | [Recommended build cache
usage](https://docs.gradle.org/8.10.2/userguide/build_cache.html) |
| `vfs.watch` | false | Already disabled in baseline config; we keep it
disabled for CI |
| `MaxMetaspaceSize` | 1g | Kept from baseline config |
| `UseG1GC` | enabled | Kept from baseline config |
| `G1HeapRegionSize` | 16m | Kept from baseline config |
| `UseStringDeduplication` | enabled | Kept from baseline config |
### Memory Budget (48GB Runner)
> Note: rough budgeting (actual usage varies by task and input changes)
```
┌─────────────────────────────────────────────────────────┐
│ Component │ Allocation │
├─────────────────────────┼───────────────────────────────┤
│ Gradle Heap │ 12GB │
│ Gradle Metaspace │ 1GB │
│ Node.js (Metro) │ 8GB (--max-old-space-size) │
│ OS + System + native │ remainder │
└─────────────────────────┴───────────────────────────────┘
```
### Additional Optimizations
- **Skip AAB bundle for E2E** - E2E tests only use APK files.
- **Removed AAB references** from the E2E build workflow.
- **Runner moved from xl → lg** for this workflow after tuning: lg
(48GB) is sufficient.
### Documentation References
**Gradle 8.10.2:**
- [Performance
Guide](https://docs.gradle.org/8.10.2/userguide/performance.html)
- [Gradle
Daemon](https://docs.gradle.org/8.10.2/userguide/gradle_daemon.html)
- [Build
Cache](https://docs.gradle.org/8.10.2/userguide/build_cache.html)
**Java 17:**
- [Java launcher docs (heap, -X/-XX, OOM
behavior)](https://docs.oracle.com/en/java/javase/17/docs/specs/man/java.html)
-
[Charset.defaultCharset()](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/nio/charset/Charset.html#defaultCharset())
- [G1 GC tuning
guide](https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1.html)
### Official Runner Specs
([source](https://cirrus-runners.app/setup/#__tabbed_3_2))
| Runner | vCPUs | RAM | Disk |
|--------|-------|-----|------|
| **lg** | 16 | 48 GB | 200 GB |
| xl | 32 | 96 GB | 400 GB |
## **Changelog**
CHANGELOG entry: null
## **Related issues**
Fixes:
[INFRA-3174](https://consensyssoftware.atlassian.net/browse/INFRA-3174)
## **Manual testing steps**
```gherkin
Feature: Android E2E Build Optimization
Scenario: Build completes without daemon disappearance
Given the PR uses tuned Gradle memory settings
And Gradle runs with --stacktrace --info for E2E
When the Android E2E build workflow runs
Then the build completes without the "Daemon disappeared" failure
And APK artifacts are uploaded successfully
```
## **Screenshots/Recordings**
### **Before**
Build failing with:
```
Gradle build daemon disappeared unexpectedly (it may have been killed or may have crashed)
```
### **After**
Build results will be visible in PR checks after workflow runs.
## **Pre-merge author checklist**
- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **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.
[INFRA-3174]:
https://consensyssoftware.atlassian.net/browse/INFRA-3174?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
---
.github/workflows/build-android-e2e.yml | 21 +--------------------
.github/workflows/run-e2e-workflow.yml | 4 ----
android/gradle.properties.github | 14 +++++++-------
scripts/build.sh | 11 +++++++++--
4 files changed, 17 insertions(+), 33 deletions(-)
diff --git a/.github/workflows/build-android-e2e.yml b/.github/workflows/build-android-e2e.yml
index 35d674bcee5..7019ff20104 100644
--- a/.github/workflows/build-android-e2e.yml
+++ b/.github/workflows/build-android-e2e.yml
@@ -9,9 +9,6 @@ on:
apk-uploaded:
description: 'Whether the APK was successfully uploaded'
value: ${{ jobs.build-android-apks.outputs.apk-uploaded }}
- aab-uploaded:
- description: 'Whether the AAB was successfully uploaded'
- value: ${{ jobs.build-android-apks.outputs.aab-uploaded }}
inputs:
build_type:
description: 'The type of build to perform'
@@ -32,17 +29,15 @@ on:
jobs:
build-android-apks:
name: Build Android E2E APKs
- runs-on: ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-xl # Bumped from lg to xl to prevent Daemon disappearance issue (Daemon OOM issue in CI)
+ runs-on: ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-lg # lg runner: 16 vCPUs, 48GB RAM
timeout-minutes: 40
env:
GRADLE_USER_HOME: /home/admin/_work/.gradle
CACHE_GENERATION: v1 # Increment this to bust the cache (v1, v2, v3, etc.)
outputs:
apk-uploaded: ${{ steps.upload-apk.outcome == 'success' }}
- aab-uploaded: ${{ steps.upload-aab.outcome == 'success' }}
apk-target-path: ${{ steps.determine-target-paths.outputs.apk-target-path }}
test-apk-target-path: ${{ steps.determine-target-paths.outputs.test-apk-target-path }}
- aab-target-path: ${{ steps.determine-target-paths.outputs.aab-target-path }}
artifact_name: ${{ steps.determine-target-paths.outputs.artifact_name }}
steps:
@@ -88,14 +83,12 @@ jobs:
{
echo "apk-target-path=android/app/build/outputs/apk/flask/release"
echo "test-apk-target-path=android/app/build/outputs/apk/androidTest/flask/release"
- echo "aab-target-path=android/app/build/outputs/bundle/flaskRelease"
echo "artifact_name=app-flask-release"
} >> "$GITHUB_OUTPUT"
elif [[ "${{ inputs.build_type }}" == "main" ]]; then
{
echo "apk-target-path=android/app/build/outputs/apk/prod/release"
echo "test-apk-target-path=android/app/build/outputs/apk/androidTest/prod/release"
- echo "aab-target-path=android/app/build/outputs/bundle/prodRelease"
echo "artifact_name=app-prod-release"
} >> "$GITHUB_OUTPUT"
else
@@ -110,7 +103,6 @@ jobs:
path: |
${{ steps.determine-target-paths.outputs.apk-target-path }}/${{ steps.determine-target-paths.outputs.artifact_name }}.apk
${{ steps.determine-target-paths.outputs.test-apk-target-path }}/${{ steps.determine-target-paths.outputs.artifact_name }}-androidTest.apk
- ${{ steps.determine-target-paths.outputs.aab-target-path }}/${{ steps.determine-target-paths.outputs.artifact_name }}.aab
# Include Gradle properties in key to force rebuild when properties change
# Keep the `hashFiles` call for Gradle config in-sync with these steps:
# - "Cache Gradle dependencies"
@@ -241,7 +233,6 @@ jobs:
path: |
${{ steps.determine-target-paths.outputs.apk-target-path }}/${{ steps.determine-target-paths.outputs.artifact_name }}.apk
${{ steps.determine-target-paths.outputs.test-apk-target-path }}/${{ steps.determine-target-paths.outputs.artifact_name }}-androidTest.apk
- ${{ steps.determine-target-paths.outputs.aab-target-path }}/${{ steps.determine-target-paths.outputs.artifact_name }}.aab
# Keep the `hashFiles` call for Gradle config in-sync with these steps:
# - "Check and restore cached APKs if Fingerprint is found"
# - "Cache Gradle dependencies"
@@ -264,13 +255,3 @@ jobs:
path: ${{ steps.determine-target-paths.outputs.test-apk-target-path }}/${{ steps.determine-target-paths.outputs.artifact_name }}-androidTest.apk
retention-days: 7
if-no-files-found: error
-
- - name: Upload Android AAB
- id: upload-aab
- uses: actions/upload-artifact@v4
- with:
- name: ${{ inputs.build_type }}-${{ inputs.metamask_environment }}-release.aab
- path: ${{ steps.determine-target-paths.outputs.aab-target-path }}/${{ steps.determine-target-paths.outputs.artifact_name }}.aab
- retention-days: 7
- if-no-files-found: warn
- continue-on-error: true
diff --git a/.github/workflows/run-e2e-workflow.yml b/.github/workflows/run-e2e-workflow.yml
index fcc4a9e05e6..6ec79db1012 100644
--- a/.github/workflows/run-e2e-workflow.yml
+++ b/.github/workflows/run-e2e-workflow.yml
@@ -56,7 +56,6 @@ jobs:
outputs:
apk-target-path: ${{ steps.determine-target-paths.outputs.apk-target-path }}
test-apk-target-path: ${{ steps.determine-target-paths.outputs.test-apk-target-path }}
- aab-target-path: ${{ steps.determine-target-paths.outputs.aab-target-path }}
env:
PREBUILT_IOS_APP_PATH: artifacts/MetaMask.app
@@ -131,14 +130,12 @@ jobs:
{
echo "apk-target-path=android/app/build/outputs/apk/flask/release"
echo "test-apk-target-path=android/app/build/outputs/apk/androidTest/flask/release"
- echo "aab-target-path=android/app/build/outputs/bundle/flaskRelease"
echo "artifact_name=app-flask-release"
} >> "$GITHUB_OUTPUT"
elif [[ "${{ inputs.build_type }}" == "main" ]]; then
{
echo "apk-target-path=android/app/build/outputs/apk/prod/release"
echo "test-apk-target-path=android/app/build/outputs/apk/androidTest/prod/release"
- echo "aab-target-path=android/app/build/outputs/bundle/prodRelease"
echo "artifact_name=app-prod-release"
} >> "$GITHUB_OUTPUT"
else
@@ -152,7 +149,6 @@ jobs:
echo "🏗 Setting up Android artifacts from build job..."
mkdir -p ${{ steps.determine-target-paths.outputs.apk-target-path }}
mkdir -p ${{ steps.determine-target-paths.outputs.test-apk-target-path }}
- mkdir -p ${{ steps.determine-target-paths.outputs.aab-target-path }}
- name: Download Android build artifacts
if: ${{ inputs.platform == 'android' }}
diff --git a/android/gradle.properties.github b/android/gradle.properties.github
index 768591f0851..f3582e40f7c 100644
--- a/android/gradle.properties.github
+++ b/android/gradle.properties.github
@@ -1,16 +1,16 @@
# GitHub Actions-specific Gradle settings
# Optimized for E2E builds on GitHub Actions runners
-# JVM configuration - balanced settings to avoid OOM while maintaining performance
-# Using 16GB heap to leave room for parallel workers and native memory
-org.gradle.jvmargs=-Xmx16g -XX:MaxMetaspaceSize=1g -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:+UseStringDeduplication -XX:+OptimizeStringConcat
+# JVM configuration - tuned for 48GB runner to avoid OOM while maintaining performance
+# Heap: 12GB to leave room for Node.js/Metro and native memory
+# ExitOnOutOfMemoryError: fail-fast on OOM for CI
+org.gradle.jvmargs=-Xmx12g -Xms4g -XX:MaxMetaspaceSize=1g -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:+UseStringDeduplication -XX:MaxGCPauseMillis=500 -XX:+ExitOnOutOfMemoryError -Dfile.encoding=UTF-8
# Enable performance optimizations but limit parallelism to prevent OOM
org.gradle.parallel=true
-org.gradle.configureondemand=true
org.gradle.caching=true
-org.gradle.daemon=true
-org.gradle.workers.max=6
+org.gradle.daemon=false
+org.gradle.workers.max=2
org.gradle.vfs.watch=false
# CI-specific optimizations - enabled for GitHub Actions
@@ -54,4 +54,4 @@ hermesEnabled=true
android.disableResourceValidation=true
# Use legacy packaging to compress native libraries in the resulting APK.
-expo.useLegacyPackaging=false
\ No newline at end of file
+expo.useLegacyPackaging=false
diff --git a/scripts/build.sh b/scripts/build.sh
index 54827d14451..7c0c54a341f 100755
--- a/scripts/build.sh
+++ b/scripts/build.sh
@@ -547,6 +547,8 @@ generateAndroidBinary() {
local reactNativeArchitecturesArg=""
# Define Test build type arg
local testBuildTypeArg=""
+ # Define Gradle debug flags
+ local gradleDebugFlags=""
# Check if configuration is valid
if [ "$configuration" != "Debug" ] && [ "$configuration" != "Release" ] ; then
@@ -572,14 +574,19 @@ generateAndroidBinary() {
if [ "$METAMASK_ENVIRONMENT" = "e2e" ] ; then
# Only build for x86_64 for E2E builds
reactNativeArchitecturesArg="-PreactNativeArchitectures=x86_64"
+ # Enable Gradle debugging flags for E2E builds to investigate Daemon disappearance issues
+ gradleDebugFlags="--stacktrace --info"
+ echo "📊 E2E build: Enabling Gradle debugging flags (--stacktrace --info)"
fi
fi
# Generate Android APKs
echo "Generating Android binary for ($flavor) flavor with ($configuration) configuration"
- ./gradlew $assembleApkTask $assembleTestApkTask $testBuildTypeArg $reactNativeArchitecturesArg
+ ./gradlew $assembleApkTask $assembleTestApkTask $testBuildTypeArg $reactNativeArchitecturesArg $gradleDebugFlags
- if [ "$configuration" = "Release" ] ; then
+ # Skip AAB bundle for E2E environments - AAB cannot be installed on emulators
+ # and is only needed for Play Store distribution
+ if [ "$configuration" = "Release" ] && [ "$METAMASK_ENVIRONMENT" != "e2e" ] ; then
# Generate AAB bundle (not needed for E2E)
bundleConfiguration="bundle${flavor}Release"
echo "Generating AAB bundle for ($flavor) flavor with ($configuration) configuration"