Skip to content

Commit dc2d67d

Browse files
Battambangmcmire
andauthored
feat: add gasSponsored flag to bridgeController (#6687)
## Explanation ### What is the current state of things and why does it need to change? - Today, quotes can indicate when gas is embedded into the transaction itself via `gasIncluded` and when EIP‑7702 delegated execution can be used via `gasIncluded7702`, but there’s no explicit way to signal that a third party will sponsor the gas cost. - This prevents the UI/clients from differentiating between “quote includes gas fees” and “quote is gasless to the user because gas is sponsored,” which affects UX (badging, prioritization) and any downstream decision logic. ### What is the solution your changes offer and how does it work? - Added an optional boolean `quote.gasSponsored` to `QuoteSchema` in `@validators.ts` to allow the bridge API to flag gas‑sponsored quotes. - Ensured end‑to‑end preservation: - Validation now accepts payloads with or without `gasSponsored`. - `BridgeController.fetchQuotes` and the fee‑augmentation path preserve the field unchanged so the UI and metrics layers can consume it. - Tests: - Added a test in `bridge-controller.test.ts` verifying that `fetchQuotes` returns quotes where `quote.gasSponsored` is preserved when present, and remains undefined when omitted. - Backward compatibility: - The field is optional; existing payloads and flows continue to work unchanged. - No sorting or fee calculations are altered by this addition. ### Are there any changes whose purpose might not obvious to those unfamiliar with the domain? - Distinction between flags: - `gasIncluded`: Quote includes gas fees in the trade economics, but the user still submits and pays gas. - `gasIncluded7702`: Quote can use delegated execution under EIP‑7702. - `gasSponsored`: A third party sponsors the gas so the user doesn’t pay it; this is orthogonal to whether gas is “included” in pricing or delegated via 7702. ## References This PR is related to this BridgeAPI PR consensys-vertical-apps/va-mmcx-bridge-api#527. PR related for MM Extension: MetaMask/metamask-extension#36227. PR related for MM Mobile: MetaMask/metamask-mobile#20878. ## Checklist - [X] 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 - [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] > Adds optional `quote.gasSponsored` to validation schema and ensures `fetchQuotes` returns it unchanged; updates tests and changelog. > > - **Validation**: > - Add optional `quote.gasSponsored` to `QuoteSchema` in `utils/validators.ts`. > - **Controller Tests**: > - Add test verifying `fetchQuotes` preserves `quote.gasSponsored` when present. > - Introduce `makeQuoteRequest` helper for test requests. > - **Changelog**: > - Document new `gasSponsored` property in `fetchQuotes` results. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c9872a7. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Elliot Winkler <elliot.winkler@gmail.com>
1 parent f6d4f2e commit dc2d67d

File tree

3 files changed

+51
-0
lines changed

3 files changed

+51
-0
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+
- Quotes as returned by `fetchQuotes` now include a `gasSponsored` property ([#6687](https://github.com/MetaMask/core/pull/6687))
13+
1014
## [58.0.0]
1115

1216
### Changed

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
StatusTypes,
2727
type BridgeControllerMessenger,
2828
type QuoteResponse,
29+
type GenericQuoteRequest,
2930
} from './types';
3031
import * as balanceUtils from './utils/balance';
3132
import { getNativeAssetForChainId, isSolanaChainId } from './utils/bridge';
@@ -2615,6 +2616,21 @@ describe('BridgeController', function () {
26152616
const quotesByDecreasingProcessingTime = [...mockBridgeQuotesSolErc20];
26162617
quotesByDecreasingProcessingTime.reverse();
26172618

2619+
const makeQuoteRequest = (
2620+
overrides: Partial<GenericQuoteRequest> = {},
2621+
): GenericQuoteRequest => ({
2622+
walletAddress: '0x123',
2623+
srcChainId: 1,
2624+
destChainId: 10,
2625+
srcTokenAddress: '0x0000000000000000000000000000000000000000',
2626+
destTokenAddress: '0x0000000000000000000000000000000000000000',
2627+
srcTokenAmount: '1000',
2628+
slippage: 0.5,
2629+
gasIncluded: false,
2630+
gasIncluded7702: false,
2631+
...overrides,
2632+
});
2633+
26182634
beforeEach(() => {
26192635
jest.clearAllMocks();
26202636
jest
@@ -2845,6 +2861,33 @@ describe('BridgeController', function () {
28452861
expect(quotes).toStrictEqual(mockBridgeQuotesSolErc20);
28462862
expect(bridgeController.state).toStrictEqual(expectedControllerState);
28472863
});
2864+
2865+
it('should preserve gasSponsored flag on quotes', async () => {
2866+
const firstQuoteWithFlag: QuoteResponse = {
2867+
...mockBridgeQuotesNativeErc20Eth[0],
2868+
quote: {
2869+
...mockBridgeQuotesNativeErc20Eth[0].quote,
2870+
gasSponsored: true,
2871+
},
2872+
} as QuoteResponse;
2873+
const secondQuote: QuoteResponse =
2874+
mockBridgeQuotesNativeErc20Eth[1] as QuoteResponse;
2875+
const quotesWithFlag: QuoteResponse[] = [firstQuoteWithFlag, secondQuote];
2876+
2877+
const fetchBridgeQuotesSpy = jest
2878+
.spyOn(fetchUtils, 'fetchBridgeQuotes')
2879+
.mockResolvedValueOnce({
2880+
quotes: quotesWithFlag,
2881+
validationFailures: [],
2882+
});
2883+
2884+
const quotes = await bridgeController.fetchQuotes(makeQuoteRequest());
2885+
2886+
expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1);
2887+
expect(quotes).toHaveLength(2);
2888+
expect(quotes[0].quote.gasSponsored).toBe(true);
2889+
expect(quotes[1].quote.gasSponsored).toBeUndefined();
2890+
});
28482891
});
28492892

28502893
describe('metadata', () => {

packages/bridge-controller/src/utils/validators.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,10 @@ export const QuoteSchema = type({
244244
totalFeeAmountUsd: optional(string()),
245245
}),
246246
),
247+
/**
248+
* A third party sponsors the gas. If true, then gasIncluded7702 is also true.
249+
*/
250+
gasSponsored: optional(boolean()),
247251
});
248252

249253
export const TxDataSchema = type({

0 commit comments

Comments
 (0)