Skip to content

Commit 1897f33

Browse files
davibrocclaude
andauthored
test(swap): enable Smart Transactions in swap and bridge E2E tests (#27836)
## Summary - Introduces `tests/helpers/swap/smart-transactions-mocks.ts` — a reusable helper that mocks the STX backend (`/getFees`, `/submitTransactions`, `/batchStatus`, `/getTxStatus`) and forwards signed transactions to Anvil so they get mined and receipts resolve correctly. - Removes `.withDisabledSmartTransactions()` from all affected test fixtures so tests run with the same STX-enabled configuration as production. - Applies the new helper to all swap/bridge regression and smoke specs: `swap-action-regression`, `swap-token-chart`, `swap-token-rwa`, `swap-action-smoke`, and `bridge-action-smoke`. ## How Smart Transactions work in tests When STX is enabled the swap publish hook intercepts the transaction before broadcast: 1. Calls `POST /getFees` → mock returns a static fee schedule so the controller can sign the tx locally. 2. Signs the tx locally and calls `POST /submitTransactions` → mock **forwards all `rawTxs` to Anvil** via `eth_sendRawTransaction` (sequentially, preserving nonce order for approval + swap batches), then returns a UUID. 3. Because `mobileReturnTxHashAsap: true` the hook resolves immediately with the locally-computed `txHash` — no polling needed. 4. `TransactionController` polls Anvil for `eth_getTransactionReceipt` using that hash → Anvil returns a valid receipt → tx is marked **Confirmed**. ### Key fix: ERC-20 → ETH batch submissions Previously the mock only forwarded `rawTxs[0]` (the approval tx) to Anvil. The swap tx (`rawTxs[1]`) was never mined, so `TransactionController` never found a receipt and the swap stayed **Pending**. The fix loops over all entries in `rawTxs` and forwards each one sequentially. ## Modified scripts - [x] `swap-action-regression` — ETH→WETH and WETH→ETH swaps confirm end-to-end - [x] `swap-token-chart` — ETH→DAI swap from token chart confirms - [x] `swap-token-rwa` — USDC→GOOGLON swap (CowSwap intent flow) - [x] `swap-action-smoke` — ETH→USDC and USDC→ETH swaps confirm - [x] `bridge-action-smoke` — ETH (Mainnet)→ETH (Base) bridge confirms 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk since changes are isolated to E2E test harnesses/mocks; main risk is increased flakiness if STX mock assumptions (fee schema, proxy matching, Anvil forwarding) diverge from app behavior. > > **Overview** > **Swap/bridge E2E tests now run with Smart Transactions enabled** by removing `.withDisabledSmartTransactions()` from affected fixtures. > > Adds `setupSmartTransactionsMocks` to mock the STX backend (`/getFees`, `/submitTransactions`, `/batchStatus`, and a low-priority `/getTxStatus` fallback) and, crucially, forwards all submitted `rawTxs` sequentially to Anvil via `eth_sendRawTransaction` so batched approval+swap flows get mined and receipts resolve. > > Updates swap and bridge regression/smoke specs to wrap existing `testSpecificMock` with the new STX mocks (and bumps timeouts where needed). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 8082888. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a2f8164 commit 1897f33

6 files changed

Lines changed: 225 additions & 15 deletions

File tree

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import { Mockttp } from 'mockttp';
2+
import { setupMockRequest } from '../../api-mocking/helpers/mockHelpers';
3+
import { getDecodedProxiedURL } from '../../smoke/notifications/utils/helpers';
4+
import PortManager, { ResourceType } from '../../framework/PortManager';
5+
6+
const STX_UUID = '0d506aaa-5e38-4cab-ad09-2039cb7a0f33';
7+
8+
/**
9+
* /getFees response body.
10+
* The STX controller reads `txs[0].fees` to obtain gas fee parameters
11+
* for signing the swap transaction locally before submitting to the backend.
12+
*/
13+
const GET_FEES_RESPONSE = {
14+
txs: [
15+
{
16+
cancelFees: [],
17+
return: '0x',
18+
status: 1,
19+
gasUsed: 190780,
20+
gasLimit: 239420,
21+
fees: [
22+
{
23+
maxFeePerGas: 4667609171,
24+
maxPriorityFeePerGas: 1000000004,
25+
gas: 239420,
26+
balanceNeeded: 1217518987960240,
27+
currentBalance: 751982303082919400,
28+
error: '',
29+
},
30+
],
31+
feeEstimate: 627603309182220,
32+
baseFeePerGas: 2289670348,
33+
maxFeeEstimate: 1117518987720820,
34+
},
35+
],
36+
};
37+
38+
/**
39+
* Sets up HTTP mocks that allow swap tests to execute with Smart Transactions enabled.
40+
*
41+
* ## How it works
42+
*
43+
* When Smart Transactions (STX) are enabled the publish hook intercepts every swap
44+
* transaction before it reaches the network:
45+
* 1. Calls `POST /getFees` → we return a static fee schedule.
46+
* 2. Signs the transaction locally using those fees.
47+
* 3. Calls `POST /submitTransactions` → we forward the raw signed tx to Anvil
48+
* via `eth_sendRawTransaction` so it actually gets mined, then return a UUID.
49+
* 4. Because `mobileReturnTxHashAsap: true` (set in the default remote feature-flag
50+
* mock), the hook immediately uses the locally-computed `txHash` without waiting
51+
* for `batchStatus` polling.
52+
* 5. TransactionController polls Anvil for `eth_getTransactionReceipt` using that
53+
* hash → Anvil returns a valid receipt → transaction is marked Confirmed.
54+
*
55+
* @param mockServer - The mockttp server instance.
56+
* @param anvilPort - The port Anvil is listening on (defaults to DEFAULT_ANVIL_PORT).
57+
*/
58+
export async function setupSmartTransactionsMocks(
59+
mockServer: Mockttp,
60+
anvilPort: number,
61+
): Promise<void> {
62+
// anvilPort is the fallback (DEFAULT_ANVIL_PORT = 8545).
63+
// On local dev the port manager allocates a random port, so we resolve
64+
// the actual port at request time to avoid ECONNREFUSED errors.
65+
const getActualAnvilRpcUrl = () => {
66+
const actualPort =
67+
PortManager.getInstance().getPort(ResourceType.ANVIL) ?? anvilPort;
68+
return `http://localhost:${actualPort}`;
69+
};
70+
71+
// Mock POST /getFees – returns a single fee tier so createSignedTransactions
72+
// produces a non-empty rawTxs array that can be forwarded to Anvil.
73+
await setupMockRequest(mockServer, {
74+
url: /transaction\.api\.cx\.metamask\.io\/networks\/\d+\/getFees/,
75+
response: GET_FEES_RESPONSE,
76+
requestMethod: 'POST',
77+
responseCode: 200,
78+
});
79+
80+
// Mock POST /submitTransactions – forward the signed transaction to Anvil so
81+
// eth_getTransactionReceipt resolves once the block is mined.
82+
await mockServer
83+
.forPost('/proxy')
84+
.matching((request) => {
85+
const url = getDecodedProxiedURL(request.url);
86+
return /transaction\.api\.cx\.metamask\.io\/networks\/\d+\/submitTransactions/.test(
87+
url,
88+
);
89+
})
90+
.asPriority(999)
91+
.thenCallback(async (request) => {
92+
let rawTxs: string[] = [];
93+
94+
try {
95+
const bodyText = await request.body.getText();
96+
const body = JSON.parse(bodyText ?? '{}');
97+
rawTxs = body?.rawTxs ?? [];
98+
} catch {
99+
// Ignore JSON-parse errors – we still return a valid UUID below.
100+
}
101+
102+
// Submit all signed transactions to Anvil so the locally-computed txHashes
103+
// are already on-chain when TransactionController polls for receipts.
104+
// When STX is enabled and a swap requires an approval, both the approval
105+
// tx and the swap tx are batched into a single submitTransactions call as
106+
// rawTxs = [approvalTx, swapTx]. We must forward both sequentially so
107+
// Anvil mines the approval before the swap (preserving nonce order).
108+
for (let i = 0; i < rawTxs.length; i++) {
109+
try {
110+
await fetch(getActualAnvilRpcUrl(), {
111+
method: 'POST',
112+
headers: { 'Content-Type': 'application/json' },
113+
body: JSON.stringify({
114+
jsonrpc: '2.0',
115+
method: 'eth_sendRawTransaction',
116+
params: [rawTxs[i]],
117+
id: i + 1,
118+
}),
119+
});
120+
} catch {
121+
// Non-fatal: the controller computes txHashes locally from rawTxs
122+
// regardless of whether this submission succeeds.
123+
}
124+
}
125+
126+
return {
127+
statusCode: 200,
128+
json: { uuid: STX_UUID },
129+
};
130+
});
131+
132+
// Mock GET /batchStatus – the STX controller polls this in the background
133+
// after submitTransactions. Mobile uses mobileReturnTxHashAsap: true so the
134+
// hook does not wait for this, but polling still runs in the background.
135+
// We always return SUCCESS so the background polling stops cleanly.
136+
const GET_BATCH_STATUS_SUCCESS = {
137+
[STX_UUID]: {
138+
cancellationFeeWei: 0,
139+
cancellationReason: 'not_cancelled',
140+
deadlineRatio: 0,
141+
isSettled: true,
142+
minedTx: 'success',
143+
wouldRevertMessage: null,
144+
minedHash:
145+
'0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5',
146+
timedOut: true,
147+
proxied: false,
148+
type: 'sentinel',
149+
},
150+
};
151+
152+
await setupMockRequest(mockServer, {
153+
url: /transaction\.api\.cx\.metamask\.io\/networks\/\d+\/batchStatus/,
154+
response: GET_BATCH_STATUS_SUCCESS,
155+
requestMethod: 'GET',
156+
responseCode: 200,
157+
});
158+
159+
// Mock GET /getTxStatus – fallback for same-chain swap tests.
160+
// Registered at priority 1 so bridge-mocks.ts (priority 999) always wins
161+
// for bridge tests, where src and dest tx hashes legitimately differ.
162+
// For same-chain swaps srcChainId == destChainId so reusing srcTxHash for
163+
// both chains is correct.
164+
await mockServer
165+
.forGet('/proxy')
166+
.matching((request) => {
167+
const url = getDecodedProxiedURL(request.url);
168+
return url.includes('getTxStatus');
169+
})
170+
.asPriority(1)
171+
.thenCallback((request) => {
172+
const decodedUrl = getDecodedProxiedURL(request.url);
173+
const urlObj = new URL(decodedUrl);
174+
const txHash = urlObj.searchParams.get('srcTxHash');
175+
const srcChainId = urlObj.searchParams.get('srcChainId');
176+
const destChainId = urlObj.searchParams.get('destChainId');
177+
178+
return {
179+
statusCode: 200,
180+
json: {
181+
status: 'COMPLETE',
182+
isExpectedToken: true,
183+
bridge: 'across',
184+
srcChain: {
185+
chainId: Number(srcChainId),
186+
txHash,
187+
},
188+
destChain: {
189+
chainId: Number(destChainId),
190+
txHash,
191+
},
192+
},
193+
};
194+
});
195+
}

tests/regression/swap/swap-action-regression.spec.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import { loginToApp } from '../../flows/wallet.flow';
1212
import { prepareSwapsTestEnvironment } from '../../helpers/swap/prepareSwapsTestEnvironment';
1313
import { AnvilPort } from '../../framework/fixtures/FixtureUtils';
1414
import { testSpecificMock } from '../../helpers/swap/swap-mocks';
15-
import { AnvilManager } from '../../seeder/anvil-manager';
15+
import { setupSmartTransactionsMocks } from '../../helpers/swap/smart-transactions-mocks';
16+
import { AnvilManager, DEFAULT_ANVIL_PORT } from '../../seeder/anvil-manager';
1617

1718
describe(RegressionTrade('Swap ETH <-> WETH from Actions'), (): void => {
1819
beforeEach(async (): Promise<void> => {
@@ -37,7 +38,6 @@ describe(RegressionTrade('Swap ETH <-> WETH from Actions'), (): void => {
3738
nickname: 'Localhost',
3839
ticker: 'ETH',
3940
})
40-
.withDisabledSmartTransactions()
4141
.build();
4242
},
4343
localNodeOptions: [
@@ -49,7 +49,10 @@ describe(RegressionTrade('Swap ETH <-> WETH from Actions'), (): void => {
4949
},
5050
},
5151
],
52-
testSpecificMock,
52+
testSpecificMock: async (mockServer) => {
53+
await testSpecificMock(mockServer);
54+
await setupSmartTransactionsMocks(mockServer, DEFAULT_ANVIL_PORT);
55+
},
5356
restartDevice: true,
5457
},
5558
async () => {

tests/regression/swap/swap-token-chart.spec.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ import ActivitiesView from '../../page-objects/Transactions/ActivitiesView';
1313
import { ActivitiesViewSelectorsText } from '../../../app/components/Views/ActivityView/ActivitiesView.testIds';
1414
import { submitSwapUnifiedUI } from '../../helpers/swap/swap-unified-ui';
1515
import { testSpecificMock } from '../../helpers/swap/swap-mocks';
16+
import { setupSmartTransactionsMocks } from '../../helpers/swap/smart-transactions-mocks';
1617
import { prepareSwapsTestEnvironment } from '../../helpers/swap/prepareSwapsTestEnvironment';
1718
import { AnvilPort } from '../../framework/fixtures/FixtureUtils';
18-
import { AnvilManager } from '../../seeder/anvil-manager';
19+
import { AnvilManager, DEFAULT_ANVIL_PORT } from '../../seeder/anvil-manager';
1920

2021
describe(RegressionTrade('Swap from Token view'), (): void => {
21-
jest.setTimeout(120000);
22+
jest.setTimeout(180000);
2223

2324
it('should complete a USDC to DAI swap from the token chart', async (): Promise<void> => {
2425
const FIRST_ROW: number = 0;
@@ -44,7 +45,6 @@ describe(RegressionTrade('Swap from Token view'), (): void => {
4445
nickname: 'Localhost',
4546
ticker: 'ETH',
4647
})
47-
.withDisabledSmartTransactions()
4848
.build();
4949
},
5050
localNodeOptions: [
@@ -55,7 +55,10 @@ describe(RegressionTrade('Swap from Token view'), (): void => {
5555
},
5656
},
5757
],
58-
testSpecificMock,
58+
testSpecificMock: async (mockServer) => {
59+
await testSpecificMock(mockServer);
60+
await setupSmartTransactionsMocks(mockServer, DEFAULT_ANVIL_PORT);
61+
},
5962
restartDevice: true,
6063
},
6164
async () => {

tests/regression/swap/swap-token-rwa.spec.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import {
1111
checkSwapActivity,
1212
} from '../../helpers/swap/swap-unified-ui';
1313
import { testSpecificMock } from '../../helpers/swap/swap-mocks';
14+
import { setupSmartTransactionsMocks } from '../../helpers/swap/smart-transactions-mocks';
1415
import { prepareSwapsTestEnvironment } from '../../helpers/swap/prepareSwapsTestEnvironment';
1516
import { AnvilPort } from '../../framework/fixtures/FixtureUtils';
16-
import { AnvilManager } from '../../seeder/anvil-manager';
17+
import { AnvilManager, DEFAULT_ANVIL_PORT } from '../../seeder/anvil-manager';
1718

1819
describe(RegressionTrade('Swap RWA'), (): void => {
1920
jest.setTimeout(120000);
@@ -41,7 +42,6 @@ describe(RegressionTrade('Swap RWA'), (): void => {
4142
nickname: 'Localhost',
4243
ticker: 'ETH',
4344
})
44-
.withDisabledSmartTransactions()
4545
.build();
4646
},
4747
localNodeOptions: [
@@ -53,7 +53,10 @@ describe(RegressionTrade('Swap RWA'), (): void => {
5353
},
5454
},
5555
],
56-
testSpecificMock,
56+
testSpecificMock: async (mockServer) => {
57+
await testSpecificMock(mockServer);
58+
await setupSmartTransactionsMocks(mockServer, DEFAULT_ANVIL_PORT);
59+
},
5760
restartDevice: true,
5861
},
5962
async () => {

tests/smoke/swap/bridge-action-smoke.spec.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import { prepareSwapsTestEnvironment } from '../../helpers/swap/prepareSwapsTest
1212
import { testSpecificMock } from '../../helpers/swap/bridge-mocks';
1313
import SoftAssert from '../../framework/SoftAssert';
1414
import { AnvilPort } from '../../framework/fixtures/FixtureUtils';
15-
import { AnvilManager } from '../../seeder/anvil-manager';
15+
import { AnvilManager, DEFAULT_ANVIL_PORT } from '../../seeder/anvil-manager';
16+
import { setupSmartTransactionsMocks } from '../../helpers/swap/smart-transactions-mocks';
1617
import { ActivitiesViewSelectorsText } from '../../../app/components/Views/ActivityView/ActivitiesView.testIds';
1718

1819
enum eventsToCheck {
@@ -57,7 +58,6 @@ describe(SmokeTrade('Bridge functionality'), () => {
5758
nickname: 'Localhost',
5859
ticker: 'ETH',
5960
})
60-
.withDisabledSmartTransactions()
6161
.build();
6262
},
6363
localNodeOptions: [
@@ -68,7 +68,10 @@ describe(SmokeTrade('Bridge functionality'), () => {
6868
},
6969
},
7070
],
71-
testSpecificMock,
71+
testSpecificMock: async (mockServer) => {
72+
await testSpecificMock(mockServer);
73+
await setupSmartTransactionsMocks(mockServer, DEFAULT_ANVIL_PORT);
74+
},
7275
restartDevice: true,
7376
},
7477
async () => {

tests/smoke/swap/swap-action-smoke.spec.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import { loginToApp } from '../../flows/wallet.flow';
1212
import { prepareSwapsTestEnvironment } from '../../helpers/swap/prepareSwapsTestEnvironment';
1313
import { testSpecificMock } from '../../helpers/swap/swap-mocks';
14+
import { setupSmartTransactionsMocks } from '../../helpers/swap/smart-transactions-mocks';
1415
import { DEFAULT_ANVIL_PORT } from '../../seeder/anvil-manager';
1516
import {
1617
EventPayload,
@@ -57,7 +58,6 @@ describe(SmokeTrade('Swap from Actions'), (): void => {
5758
nickname: 'Localhost',
5859
ticker: 'ETH',
5960
})
60-
.withDisabledSmartTransactions()
6161
.withMetaMetricsOptIn()
6262
.build(),
6363
localNodeOptions: [
@@ -71,7 +71,10 @@ describe(SmokeTrade('Swap from Actions'), (): void => {
7171
},
7272
},
7373
],
74-
testSpecificMock,
74+
testSpecificMock: async (mockServer) => {
75+
await testSpecificMock(mockServer);
76+
await setupSmartTransactionsMocks(mockServer, DEFAULT_ANVIL_PORT);
77+
},
7578
restartDevice: true,
7679
skipReactNativeReload: true,
7780
},

0 commit comments

Comments
 (0)