Skip to content

Commit ec46875

Browse files
authored
feat: fetch batched swap quotes (#8711)
## Explanation This PR implements quote polling for batched swap requests #### Changes - `updateBridgeQuoteRequest` now takes 2 extra params: the quote request’s index and the total quoteRequestCount within the batch. no changes in how we are using this handler for regular swaps - the new `selectBatchSellQuotes` selector returns the recommended quote for each quoteRequest, and aggregated amounts for display purposes - `isValidBatchSellQuoteRequest` #### Usage - the clients will need to maintain a list of requests, each one identified by an index. the index identifies the quoteRequest and its related side effects (quotes, metrics, submission etc) once it’s passed to updateBridgeQuoteRequest - to access quotes for a single quoteRequest, use the same index used during quoteRequest update #### Minimal client examples - extension: MetaMask/metamask-extension#42434. Checkout this branch and resolve the package locally to test - mobile: MetaMask/metamask-mobile#29831 <!-- 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-4443 ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > This is a breaking API/state-shape change (`quoteRequest` becomes an array) that touches quote polling/streaming, analytics properties, and exchange-rate lookup logic; regressions could affect quote fetching and refresh behavior across swaps/bridges. > > **Overview** > **Adds BatchSell (batched swap) quote support** by changing `quoteRequest` state and polling inputs from a single request to an array, allowing callers to update a specific request via new `updateBridgeQuoteRequestParams(…, quoteRequestIndex, quoteRequestCount)` parameters. > > Quote fetching/streaming is updated to accept multiple requests: SSE uses a new `POST /getBatchQuoteStream` path when batching, tags incoming quotes with `quoteRequestIndex`, traces via new Sentry trace name `Batch Sell Quotes Fetched`, and adjusts polling stop/refresh logic to continue as long as *any* request is sufficiently funded. > > Selectors and rate lookup are extended for batching: adds `selectBatchSellQuotes` (per-request recommended quotes plus aggregated received/fee totals), switches exchange-rate selection to `selectExchangeRateByAssetId`, and exports `isValidBatchSellQuoteRequest`; tests/snapshots are updated and a new SSE batch test is added. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 9e2a7fa. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent e006811 commit ec46875

18 files changed

Lines changed: 1839 additions & 594 deletions

packages/bridge-controller/CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Added
11+
12+
- **BREAKING:** Add support for BatchSell quotes ([#8711](https://github.com/MetaMask/core/pull/8711))
13+
- change `quoteRequest`'s type from `QuoteRequest` to `QuoteRequest[]`
14+
- allow callers to update specific quote requests within a batch by adding 2 optional parameters to `updateBridgeQuoteRequest`: quoteRequestIndex and quoteRequestCount
15+
- export `isValidBatchSellQuoteRequest` request validator
16+
- fetch multiple swap quotes through a single SSE stream and append `quoteRequestIndex` to link each one to its originating quoteRequest
17+
- implement `selectBatchSellQuotes` selector which returns the recommended quote for each batched quote, and their aggregated fees and received amounts
18+
- trace BatchSell quote fetch operations in Sentry using label `Batch Sell Quotes Fetched`
19+
1020
### Changed
1121

1222
- Bump `@metamask/gas-fee-controller` from `^26.1.1` to `^26.2.0` ([#8722](https://github.com/MetaMask/core/pull/8722))
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`BridgeController BatchSell (multiple quote requests) SSE should trigger quote polling if request is valid 1`] = `
4+
{
5+
"assetExchangeRates": {
6+
"eip155:10/erc20:0x1f9840a85d5af5bf1d1762f925bdaddc4201f984": {
7+
"exchangeRate": undefined,
8+
"usdExchangeRate": "100",
9+
},
10+
},
11+
"minimumBalanceForRentExemptionInLamports": "0",
12+
"quoteFetchError": null,
13+
"quoteRequest": [
14+
{
15+
"destChainId": "137",
16+
"destTokenAddress": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359",
17+
"destWalletAddress": "SolanaWalletAddres1234",
18+
"insufficientBal": false,
19+
"resetApproval": false,
20+
"slippage": 0.5,
21+
"srcChainId": "10",
22+
"srcTokenAddress": "0x0000000000000000000000000000000000000000",
23+
"srcTokenAmount": "100000000000000000",
24+
"walletAddress": "0x30E8ccaD5A980BDF30447f8c2C48e70989D9d294",
25+
},
26+
{
27+
"destChainId": "137",
28+
"destTokenAddress": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359",
29+
"destWalletAddress": "SolanaWalletAddres1234",
30+
"insufficientBal": false,
31+
"resetApproval": false,
32+
"slippage": 0.5,
33+
"srcChainId": "10",
34+
"srcTokenAddress": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
35+
"srcTokenAmount": "1000000000000000000",
36+
"walletAddress": "0x30E8ccaD5A980BDF30447f8c2C48e70989D9d294",
37+
},
38+
{
39+
"destChainId": "137",
40+
"destTokenAddress": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359",
41+
"destWalletAddress": "SolanaWalletAddres1234",
42+
"insufficientBal": false,
43+
"resetApproval": false,
44+
"slippage": 0.5,
45+
"srcChainId": "10",
46+
"srcTokenAddress": "0x0000000000000000000000000000000000000000",
47+
"srcTokenAmount": "1000000000000000000",
48+
"walletAddress": "0x30E8ccaD5A980BDF30447f8c2C48e70989D9d294",
49+
},
50+
],
51+
"quoteStreamComplete": null,
52+
"quotes": [],
53+
"quotesInitialLoadTime": null,
54+
"quotesLoadingStatus": 0,
55+
"quotesRefreshCount": 0,
56+
"tokenSecurityTypeDestination": null,
57+
"tokenWarnings": [],
58+
}
59+
`;
60+
61+
exports[`BridgeController BatchSell (multiple quote requests) SSE should trigger quote polling if request is valid 2`] = `
62+
[
63+
[
64+
"Unified SwapBridge Input Changed",
65+
{
66+
"action_type": "swapbridge-v1",
67+
"input": "chain_source",
68+
"input_value": "eip155:10",
69+
"location": "Main View",
70+
},
71+
],
72+
[
73+
"Unified SwapBridge Input Changed",
74+
{
75+
"action_type": "swapbridge-v1",
76+
"input": "chain_destination",
77+
"input_value": "eip155:137",
78+
"location": "Main View",
79+
},
80+
],
81+
[
82+
"Unified SwapBridge Input Changed",
83+
{
84+
"action_type": "swapbridge-v1",
85+
"input": "token_destination",
86+
"input_value": "eip155:137/erc20:0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
87+
"location": "Main View",
88+
},
89+
],
90+
[
91+
"Unified SwapBridge Input Changed",
92+
{
93+
"action_type": "swapbridge-v1",
94+
"input": "slippage",
95+
"input_value": 0.5,
96+
"location": "Main View",
97+
},
98+
],
99+
[
100+
"Unified SwapBridge Quotes Requested",
101+
{
102+
"account_hardware_type": null,
103+
"action_type": "swapbridge-v1",
104+
"chain_id_destination": "eip155:137",
105+
"chain_id_source": "eip155:10",
106+
"custom_slippage": true,
107+
"has_sufficient_funds": true,
108+
"is_hardware_wallet": false,
109+
"location": "Main View",
110+
"security_warnings": [],
111+
"slippage_limit": 0.5,
112+
"stx_enabled": true,
113+
"swap_type": "crosschain",
114+
"token_address_destination": "eip155:137/erc20:0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
115+
"token_address_source": "eip155:10/slip44:60",
116+
"token_security_type_destination": null,
117+
"token_symbol_destination": "USDC",
118+
"token_symbol_source": "ETH",
119+
"usd_amount_source": 100,
120+
"warnings": [],
121+
},
122+
],
123+
]
124+
`;

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

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ exports[`BridgeController SSE should publish validation failures 4`] = `
2424
"refresh_count": 1,
2525
"token_address_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:123d1",
2626
"token_address_source": "eip155:1/slip44:60",
27-
"token_security_type_destination": null,
27+
"token_security_type_destination": "test",
2828
},
2929
],
3030
[
@@ -40,7 +40,7 @@ exports[`BridgeController SSE should publish validation failures 4`] = `
4040
"refresh_count": 1,
4141
"token_address_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:123d1",
4242
"token_address_source": "eip155:1/slip44:60",
43-
"token_security_type_destination": null,
43+
"token_security_type_destination": "test",
4444
},
4545
],
4646
[
@@ -56,7 +56,7 @@ exports[`BridgeController SSE should publish validation failures 4`] = `
5656
"refresh_count": 1,
5757
"token_address_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:123d1",
5858
"token_address_source": "eip155:1/slip44:60",
59-
"token_security_type_destination": null,
59+
"token_security_type_destination": "test",
6060
},
6161
],
6262
]
@@ -99,7 +99,7 @@ exports[`BridgeController SSE should reset and refetch quotes after quote reques
9999
"chain_id_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
100100
"chain_id_source": "eip155:1",
101101
"custom_slippage": true,
102-
"has_sufficient_funds": true,
102+
"has_sufficient_funds": false,
103103
"is_hardware_wallet": false,
104104
"location": "Main View",
105105
"security_warnings": [],
@@ -181,18 +181,20 @@ exports[`BridgeController SSE should rethrow error from server 1`] = `
181181
},
182182
"minimumBalanceForRentExemptionInLamports": "0",
183183
"quoteFetchError": null,
184-
"quoteRequest": {
185-
"destChainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
186-
"destTokenAddress": "123d1",
187-
"destWalletAddress": "SolanaWalletAddres1234",
188-
"insufficientBal": false,
189-
"resetApproval": false,
190-
"slippage": 0.5,
191-
"srcChainId": "0x1",
192-
"srcTokenAddress": "0x0000000000000000000000000000000000000000",
193-
"srcTokenAmount": "1000000000000000000",
194-
"walletAddress": "0x30E8ccaD5A980BDF30447f8c2C48e70989D9d294",
195-
},
184+
"quoteRequest": [
185+
{
186+
"destChainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
187+
"destTokenAddress": "123d1",
188+
"destWalletAddress": "SolanaWalletAddres1234",
189+
"insufficientBal": false,
190+
"resetApproval": false,
191+
"slippage": 0.5,
192+
"srcChainId": "0x1",
193+
"srcTokenAddress": "0x0000000000000000000000000000000000000000",
194+
"srcTokenAmount": "1000000000000000000",
195+
"walletAddress": "0x30E8ccaD5A980BDF30447f8c2C48e70989D9d294",
196+
},
197+
],
196198
"quoteStreamComplete": null,
197199
"quotes": [],
198200
"quotesInitialLoadTime": null,
@@ -303,18 +305,20 @@ exports[`BridgeController SSE should trigger quote polling if request is valid 1
303305
},
304306
"minimumBalanceForRentExemptionInLamports": "0",
305307
"quoteFetchError": null,
306-
"quoteRequest": {
307-
"destChainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
308-
"destTokenAddress": "123d1",
309-
"destWalletAddress": "SolanaWalletAddres1234",
310-
"insufficientBal": false,
311-
"resetApproval": false,
312-
"slippage": 0.5,
313-
"srcChainId": "0x1",
314-
"srcTokenAddress": "0x0000000000000000000000000000000000000000",
315-
"srcTokenAmount": "1000000000000000000",
316-
"walletAddress": "0x30E8ccaD5A980BDF30447f8c2C48e70989D9d294",
317-
},
308+
"quoteRequest": [
309+
{
310+
"destChainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
311+
"destTokenAddress": "123d1",
312+
"destWalletAddress": "SolanaWalletAddres1234",
313+
"insufficientBal": false,
314+
"resetApproval": false,
315+
"slippage": 0.5,
316+
"srcChainId": "0x1",
317+
"srcTokenAddress": "0x0000000000000000000000000000000000000000",
318+
"srcTokenAmount": "1000000000000000000",
319+
"walletAddress": "0x30E8ccaD5A980BDF30447f8c2C48e70989D9d294",
320+
},
321+
],
318322
"quoteStreamComplete": null,
319323
"quotes": [],
320324
"quotesInitialLoadTime": null,

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

Lines changed: 53 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,18 @@ exports[`BridgeController should handle errors from fetchBridgeQuotes 1`] = `
1010
},
1111
"minimumBalanceForRentExemptionInLamports": "0",
1212
"quoteFetchError": null,
13-
"quoteRequest": {
14-
"destChainId": "0x1",
15-
"destTokenAddress": "0x0000000000000000000000000000000000000000",
16-
"insufficientBal": false,
17-
"resetApproval": false,
18-
"srcChainId": "0xa",
19-
"srcTokenAddress": "0x4200000000000000000000000000000000000006",
20-
"srcTokenAmount": "991250000000000000",
21-
"walletAddress": "eip:id/id:id/0x123",
22-
},
13+
"quoteRequest": [
14+
{
15+
"destChainId": "0x1",
16+
"destTokenAddress": "0x0000000000000000000000000000000000000000",
17+
"insufficientBal": false,
18+
"resetApproval": false,
19+
"srcChainId": "0xa",
20+
"srcTokenAddress": "0x4200000000000000000000000000000000000006",
21+
"srcTokenAmount": "991250000000000000",
22+
"walletAddress": "eip:id/id:id/0x123",
23+
},
24+
],
2325
"quoteStreamComplete": null,
2426
"quotesInitialLoadTime": 10000,
2527
"quotesLoadingStatus": 1,
@@ -39,16 +41,18 @@ exports[`BridgeController should handle errors from fetchBridgeQuotes 2`] = `
3941
},
4042
"minimumBalanceForRentExemptionInLamports": "0",
4143
"quoteFetchError": null,
42-
"quoteRequest": {
43-
"destChainId": "0x1",
44-
"destTokenAddress": "0x0000000000000000000000000000000000000000",
45-
"insufficientBal": false,
46-
"resetApproval": false,
47-
"srcChainId": "0xa",
48-
"srcTokenAddress": "0x4200000000000000000000000000000000000006",
49-
"srcTokenAmount": "991250000000000000",
50-
"walletAddress": "eip:id/id:id/0x123",
51-
},
44+
"quoteRequest": [
45+
{
46+
"destChainId": "0x1",
47+
"destTokenAddress": "0x0000000000000000000000000000000000000000",
48+
"insufficientBal": false,
49+
"resetApproval": false,
50+
"srcChainId": "0xa",
51+
"srcTokenAddress": "0x4200000000000000000000000000000000000006",
52+
"srcTokenAmount": "991250000000000000",
53+
"walletAddress": "eip:id/id:id/0x123",
54+
},
55+
],
5256
"quoteStreamComplete": null,
5357
"quotesInitialLoadTime": 10000,
5458
"quotesLoadingStatus": 1,
@@ -503,7 +507,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should only poll once i
503507
"chain_id_destination": "eip155:10",
504508
"chain_id_source": "eip155:1",
505509
"custom_slippage": true,
506-
"has_sufficient_funds": true,
510+
"has_sufficient_funds": false,
507511
"is_hardware_wallet": false,
508512
"location": "Main View",
509513
"security_warnings": [],
@@ -827,17 +831,20 @@ exports[`BridgeController updateBridgeQuoteRequestParams should trigger quote po
827831
"assetExchangeRates": {},
828832
"minimumBalanceForRentExemptionInLamports": "0",
829833
"quoteFetchError": null,
830-
"quoteRequest": {
831-
"destChainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
832-
"destTokenAddress": "123d1",
833-
"destWalletAddress": "SolanaWalletAddres1234",
834-
"insufficientBal": false,
835-
"slippage": 0.5,
836-
"srcChainId": "0x1",
837-
"srcTokenAddress": "0x0000000000000000000000000000000000000000",
838-
"srcTokenAmount": "10",
839-
"walletAddress": "0x123",
840-
},
834+
"quoteRequest": [
835+
{
836+
"destChainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
837+
"destTokenAddress": "123d1",
838+
"destWalletAddress": "SolanaWalletAddres1234",
839+
"insufficientBal": false,
840+
"resetApproval": false,
841+
"slippage": 0.5,
842+
"srcChainId": "0x1",
843+
"srcTokenAddress": "0x0000000000000000000000000000000000000000",
844+
"srcTokenAmount": "10",
845+
"walletAddress": "0x123",
846+
},
847+
],
841848
"quoteStreamComplete": null,
842849
"quotes": [],
843850
"quotesInitialLoadTime": null,
@@ -859,18 +866,20 @@ exports[`BridgeController updateBridgeQuoteRequestParams should trigger quote po
859866
},
860867
"minimumBalanceForRentExemptionInLamports": "0",
861868
"quoteFetchError": null,
862-
"quoteRequest": {
863-
"destChainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
864-
"destTokenAddress": "123d1",
865-
"destWalletAddress": "SolanaWalletAddres1234",
866-
"insufficientBal": false,
867-
"resetApproval": false,
868-
"slippage": 0.5,
869-
"srcChainId": "0x1",
870-
"srcTokenAddress": "0x0000000000000000000000000000000000000000",
871-
"srcTokenAmount": "10",
872-
"walletAddress": "0x123",
873-
},
869+
"quoteRequest": [
870+
{
871+
"destChainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
872+
"destTokenAddress": "123d1",
873+
"destWalletAddress": "SolanaWalletAddres1234",
874+
"insufficientBal": false,
875+
"resetApproval": false,
876+
"slippage": 0.5,
877+
"srcChainId": "0x1",
878+
"srcTokenAddress": "0x0000000000000000000000000000000000000000",
879+
"srcTokenAmount": "10",
880+
"walletAddress": "0x123",
881+
},
882+
],
874883
"quoteStreamComplete": null,
875884
"quotesInitialLoadTime": 10000,
876885
"quotesLoadingStatus": 1,

0 commit comments

Comments
 (0)