Skip to content

Commit 3613508

Browse files
authored
fix: publish Unified SwapBridge Quotes Received event when tx is submitted (#7182)
## Explanation Publishes the `QuotesReceived` event when submitting a trade before all quotes load Extension PR: MetaMask/metamask-extension#37963 Mobilie PR: MetaMask/metamask-mobile#22905 <!-- Thanks for your contribution! Take a moment to answer these questions so that reviewers have the information they need to properly understand your changes: * What is the current state of things and why does it need to change? * What is the solution your changes offer and how does it work? * Are there any changes whose purpose might not obvious to those unfamiliar with the domain? * If your primary goal was to update one package but you found you had to update another one along the way, why did you do so? * If you had to upgrade a dependency, why did you do so? --> ## References <!-- Are there any issues that this pull request is tied to? Are there other links that reviewers should consult to understand these changes better? Are there client or consumer pull requests to adopt any breaking changes? For example: * Fixes #12345 * Related to #67890 --> Fixes https://consensyssoftware.atlassian.net/browse/SWAPS-3427 ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/contributing.md#updating-changelogs), highlighting breaking changes as necessary - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Publishes a QuotesReceived event on early trade submission and adds a helper and types to standardize its payload and warnings. > > - **Bridge Status Controller**: > - Change `submitTx` to accept optional `isLoading` and `warnings`; when `isLoading=true`, publish `Unified SwapBridge Quotes Received` using `getQuotesReceivedProperties` before stopping quote polling. > - Allow tracking of `QuotesReceived` in internal tracking helper. > - **Bridge Controller**: > - Add and export `getQuotesReceivedProperties` to build QuotesReceived metrics payload; re-export from `index.ts`. > - Introduce `QuoteWarning` type and use it for `warnings` across events/tests; update snapshots to standardized values (e.g., `low_return`, `insufficient_balance`). > - Selector/tests: refine `gasIncluded` vs `gasIncluded_7702` handling and add scenario where fees come from dest token under 7702. > - Tests: add BTC fee error handling (return `undefined` fees on failure) and validate BTC/SOL fee behaviors. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 89bb4b5. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 937b008 commit 3613508

File tree

12 files changed

+388
-23
lines changed

12 files changed

+388
-23
lines changed

packages/bridge-controller/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add and export `getQuotesReceivedProperties` utility to build the metrics payload for clients ([#7182](https://github.com/MetaMask/core/pull/7182))
13+
1014
## [61.0.0]
1115

1216
### Changed

packages/bridge-controller/src/__snapshots__/bridge-controller.test.ts.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,7 @@ Array [
403403
"usd_quoted_gas": 0,
404404
"usd_quoted_return": 100,
405405
"warnings": Array [
406-
"warning1",
406+
"insufficient_balance",
407407
],
408408
},
409409
],
@@ -494,7 +494,7 @@ Array [
494494
"usd_quoted_gas": 0,
495495
"usd_quoted_return": 100,
496496
"warnings": Array [
497-
"warning1",
497+
"low_return",
498498
],
499499
},
500500
],

packages/bridge-controller/src/bridge-controller.test.ts

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,7 +1050,7 @@ describe('BridgeController', function () {
10501050
bridgeController.trackUnifiedSwapBridgeEvent(
10511051
UnifiedSwapBridgeEventName.QuotesReceived,
10521052
{
1053-
warnings: ['warning1'],
1053+
warnings: ['low_return'],
10541054
usd_quoted_gas: 0,
10551055
gas_included: false,
10561056
gas_included_7702: false,
@@ -2101,6 +2101,99 @@ describe('BridgeController', function () {
21012101
expect(quotes[1].nonEvmFeesInNative).toBe('0.00005'); // BTC fee as-is
21022102
});
21032103

2104+
it('should catch BTC chain fees errors and return undefined fees', async () => {
2105+
jest.useFakeTimers();
2106+
// Use the actual Solana mock which already has string trade type
2107+
const btcQuoteResponse = mockBridgeQuotesSolErc20.map((quote) => ({
2108+
...quote,
2109+
quote: {
2110+
...quote.quote,
2111+
srcChainId: ChainId.BTC,
2112+
},
2113+
})) as unknown as QuoteResponse[];
2114+
2115+
messengerMock.call.mockImplementation(
2116+
(
2117+
...args: Parameters<BridgeControllerMessenger['call']>
2118+
): ReturnType<BridgeControllerMessenger['call']> => {
2119+
const [actionType] = args;
2120+
2121+
if (actionType === 'AccountsController:getAccountByAddress') {
2122+
return {
2123+
type: 'btc:p2wpkh',
2124+
id: 'btc-account-1',
2125+
scopes: [BtcScope.Mainnet],
2126+
methods: [],
2127+
address: 'bc1q...',
2128+
metadata: {
2129+
name: 'BTC Account 1',
2130+
importTime: 1717334400,
2131+
keyring: {
2132+
type: 'Snap Keyring',
2133+
},
2134+
snap: {
2135+
id: 'btc-snap-id',
2136+
name: 'BTC Snap',
2137+
},
2138+
},
2139+
} as never;
2140+
}
2141+
2142+
if (actionType === 'SnapController:handleRequest') {
2143+
return new Promise((_resolve, reject) => {
2144+
reject(new Error('Failed to compute fees'));
2145+
});
2146+
}
2147+
2148+
return {
2149+
provider: jest.fn() as never,
2150+
} as never;
2151+
},
2152+
);
2153+
2154+
jest.spyOn(fetchUtils, 'fetchBridgeQuotes').mockResolvedValue({
2155+
quotes: btcQuoteResponse,
2156+
validationFailures: [],
2157+
});
2158+
2159+
const quoteParams = {
2160+
srcChainId: ChainId.BTC.toString(),
2161+
destChainId: '1',
2162+
srcTokenAddress: 'NATIVE',
2163+
destTokenAddress: '0x0000000000000000000000000000000000000000',
2164+
srcTokenAmount: '100000', // satoshis
2165+
walletAddress: 'bc1q...',
2166+
destWalletAddress: '0x5342',
2167+
slippage: 0.5,
2168+
};
2169+
2170+
await bridgeController.updateBridgeQuoteRequestParams(
2171+
quoteParams,
2172+
metricsContext,
2173+
);
2174+
2175+
// Wait for polling to start
2176+
jest.advanceTimersByTime(201);
2177+
await flushPromises();
2178+
2179+
// Wait for fetch to trigger
2180+
jest.advanceTimersByTime(295);
2181+
await flushPromises();
2182+
2183+
// Wait for fetch to complete
2184+
jest.advanceTimersByTime(2601);
2185+
await flushPromises();
2186+
2187+
// Final wait for fee calculation
2188+
jest.advanceTimersByTime(100);
2189+
await flushPromises();
2190+
2191+
const { quotes } = bridgeController.state;
2192+
expect(quotes).toHaveLength(2); // mockBridgeQuotesSolErc20 has 2 quotes
2193+
expect(quotes[0].nonEvmFeesInNative).toBeUndefined();
2194+
expect(quotes[1].nonEvmFeesInNative).toBeUndefined();
2195+
});
2196+
21042197
describe('trackUnifiedSwapBridgeEvent client-side calls', () => {
21052198
beforeEach(async () => {
21062199
jest.clearAllMocks();
@@ -2267,7 +2360,7 @@ describe('BridgeController', function () {
22672360
bridgeController.trackUnifiedSwapBridgeEvent(
22682361
UnifiedSwapBridgeEventName.QuotesReceived,
22692362
{
2270-
warnings: ['warning1'],
2363+
warnings: ['insufficient_balance'],
22712364
usd_quoted_gas: 0,
22722365
gas_included: false,
22732366
gas_included_7702: false,
@@ -2569,7 +2662,7 @@ describe('BridgeController', function () {
25692662
bridgeController.trackUnifiedSwapBridgeEvent(
25702663
UnifiedSwapBridgeEventName.QuotesReceived,
25712664
{
2572-
warnings: ['warning1'],
2665+
warnings: ['low_return'],
25732666
usd_quoted_gas: 0,
25742667
gas_included: false,
25752668
gas_included_7702: false,

packages/bridge-controller/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type {
1313
RequestMetadata,
1414
TxStatusData,
1515
QuoteFetchData,
16+
QuoteWarning,
1617
} from './utils/metrics/types';
1718

1819
export {
@@ -21,6 +22,7 @@ export {
2122
getSwapType,
2223
isHardwareWallet,
2324
isCustomSlippage,
25+
getQuotesReceivedProperties,
2426
} from './utils/metrics/properties';
2527

2628
export type {

packages/bridge-controller/src/selectors.test.ts

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,7 @@ describe('Bridge Selectors', () => {
427427
amount: string;
428428
asset: Pick<BridgeAsset, 'address' | 'decimals' | 'assetId'>;
429429
},
430+
gasIncluded7702?: boolean,
430431
): BridgeAppState => {
431432
const chainId = 56;
432433
const currencyRates = {
@@ -446,6 +447,10 @@ describe('Bridge Selectors', () => {
446447
price: '1',
447448
currency: 'BNB',
448449
},
450+
'0x0000000000000000000000000000000000000001': {
451+
price: '1.5498387253001357',
452+
currency: 'BNB',
453+
},
449454
},
450455
} as unknown as Record<string, Record<string, MarketDataDetails>>;
451456
const srcTokenAmount = new BigNumber('10') // $10 worth of src token
@@ -473,8 +478,8 @@ describe('Bridge Selectors', () => {
473478
},
474479
txFee,
475480
},
476-
gasIncluded: Boolean(txFee),
477-
gasIncluded7702: false,
481+
gasIncluded: Boolean(txFee) && !gasIncluded7702,
482+
gasIncluded7702: Boolean(gasIncluded7702),
478483
srcTokenAmount,
479484
destTokenAmount: new BigNumber('9')
480485
.dividedBy(marketData['0x38'][destAsset.address].price)
@@ -875,6 +880,104 @@ describe('Bridge Selectors', () => {
875880
}
876881
`);
877882
});
883+
884+
it('when gasIncluded7702=true and is taken from dest token', () => {
885+
const newState = getMockSwapState(
886+
{
887+
address: '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d',
888+
decimals: 18,
889+
assetId:
890+
'eip155:1/erc20:0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d',
891+
},
892+
{
893+
address: '0x0000000000000000000000000000000000000001',
894+
decimals: 18,
895+
assetId:
896+
'eip155:1/erc20:0x0000000000000000000000000000000000000001',
897+
},
898+
{
899+
amount: '1000000000000000000',
900+
asset: {
901+
address:
902+
'eip155:1/erc20:0x0000000000000000000000000000000000000001',
903+
decimals: 18,
904+
assetId:
905+
'eip155:1/erc20:0x0000000000000000000000000000000000000001',
906+
},
907+
},
908+
true,
909+
);
910+
911+
const { sortedQuotes } = selectBridgeQuotes(newState, mockClientParams);
912+
913+
const {
914+
quote,
915+
trade,
916+
approval,
917+
estimatedProcessingTimeInSeconds,
918+
...quoteMetadata
919+
} = sortedQuotes[0];
920+
expect(quoteMetadata).toMatchInlineSnapshot(`
921+
Object {
922+
"adjustedReturn": Object {
923+
"usd": "10.518641979781876096240273601395823616",
924+
"valueInCurrency": "8.999999999999999949780980627632791914",
925+
},
926+
"cost": Object {
927+
"usd": "1.168737997753541853682403691190760358912",
928+
"valueInCurrency": "1.000000000000000050216414294183215375298",
929+
},
930+
"gasFee": Object {
931+
"effective": Object {
932+
"amount": "0.000008087",
933+
"usd": "0.00521708544",
934+
"valueInCurrency": "0.00446386226",
935+
},
936+
"max": Object {
937+
"amount": "0.000016174",
938+
"usd": "0.01043417088",
939+
"valueInCurrency": "0.00892772452",
940+
},
941+
"total": Object {
942+
"amount": "0.000008087",
943+
"usd": "0.00521708544",
944+
"valueInCurrency": "0.00446386226",
945+
},
946+
},
947+
"includedTxFees": Object {
948+
"amount": "1",
949+
"usd": "999.831958465623542784",
950+
"valueInCurrency": "855.479979591168903686",
951+
},
952+
"minToTokenAmount": Object {
953+
"amount": "0.009994389353314869",
954+
"usd": "9.992709880792782241436661998044855296",
955+
"valueInCurrency": "8.549999999999999909517932616692707134",
956+
},
957+
"sentAmount": Object {
958+
"amount": "11.689344272882887843",
959+
"usd": "11.687379977535417949922677292586583974912",
960+
"valueInCurrency": "9.999999999999999999997394921816007289298",
961+
},
962+
"swapRate": "0.00089999999999999999",
963+
"toTokenAmount": Object {
964+
"amount": "0.010520409845594599",
965+
"usd": "10.518641979781876096240273601395823616",
966+
"valueInCurrency": "8.999999999999999949780980627632791914",
967+
},
968+
"totalMaxNetworkFee": Object {
969+
"amount": "0.000016174",
970+
"usd": "0.01043417088",
971+
"valueInCurrency": "0.00892772452",
972+
},
973+
"totalNetworkFee": Object {
974+
"amount": "0.000008087",
975+
"usd": "0.00521708544",
976+
"valueInCurrency": "0.00446386226",
977+
},
978+
}
979+
`);
980+
});
878981
});
879982

880983
it('should only fetch quotes once if balance is insufficient', () => {

0 commit comments

Comments
 (0)