|
| 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 – mirrors the extension's mockGetTxStatus. |
| 160 | + // The BridgeStatusController polls this endpoint to determine if the swap |
| 161 | + // is complete. It must return status: 'COMPLETE' (uppercase) with srcChain |
| 162 | + // and destChain txHash fields so the controller marks the tx as finished. |
| 163 | + await mockServer |
| 164 | + .forGet('/proxy') |
| 165 | + .matching((request) => { |
| 166 | + const url = getDecodedProxiedURL(request.url); |
| 167 | + return url.includes('getTxStatus'); |
| 168 | + }) |
| 169 | + .asPriority(999) |
| 170 | + .thenCallback((request) => { |
| 171 | + const decodedUrl = getDecodedProxiedURL(request.url); |
| 172 | + const urlObj = new URL(decodedUrl); |
| 173 | + const txHash = urlObj.searchParams.get('srcTxHash'); |
| 174 | + const srcChainId = urlObj.searchParams.get('srcChainId'); |
| 175 | + const destChainId = urlObj.searchParams.get('destChainId'); |
| 176 | + |
| 177 | + return { |
| 178 | + statusCode: 200, |
| 179 | + json: { |
| 180 | + status: 'COMPLETE', |
| 181 | + isExpectedToken: true, |
| 182 | + bridge: 'across', |
| 183 | + srcChain: { |
| 184 | + chainId: Number(srcChainId), |
| 185 | + txHash, |
| 186 | + }, |
| 187 | + destChain: { |
| 188 | + chainId: Number(destChainId), |
| 189 | + txHash, |
| 190 | + }, |
| 191 | + }, |
| 192 | + }; |
| 193 | + }); |
| 194 | +} |
0 commit comments