Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
7f345c7
feat: updates snap methods to use new implementations in bridge contr…
ghgoodreau Sep 3, 2025
d488050
chore: prettier
ghgoodreau Sep 3, 2025
299d7f3
fix: test
ghgoodreau Sep 3, 2025
e6b2bd5
chore: lint
ghgoodreau Sep 3, 2025
9da14ea
test updates
ghgoodreau Sep 3, 2025
98af283
test updates
ghgoodreau Sep 3, 2025
5dc551b
update tests
ghgoodreau Sep 4, 2025
249acca
cleanup
ghgoodreau Sep 4, 2025
eadbd8f
more test coverage
ghgoodreau Sep 4, 2025
58cef05
fix final lint issue
ghgoodreau Sep 4, 2025
f35d3a2
update changelog
ghgoodreau Sep 4, 2025
ca9c3d4
Merge branch 'main' into SWAPS-2839-update-snap-methods-in-bridge-con…
ghgoodreau Sep 8, 2025
cae0407
update computeFee
ghgoodreau Sep 9, 2025
7651b43
snapshot update
ghgoodreau Sep 9, 2025
602645b
fix tests
ghgoodreau Sep 9, 2025
0c1028a
move signandsend to status controller
ghgoodreau Sep 10, 2025
f2d9198
Merge branch 'main' into SWAPS-2839-update-snap-methods-in-bridge-con…
ghgoodreau Sep 11, 2025
d2e73fc
PR comments
ghgoodreau Sep 11, 2025
45231de
remove keyring from lock
ghgoodreau Sep 11, 2025
30c24ec
more nonevm fee usage
ghgoodreau Sep 11, 2025
5ac13fa
update tests
ghgoodreau Sep 11, 2025
2e01ad7
update units
ghgoodreau Sep 11, 2025
b8815c6
bitcoin validation for quotes
ghgoodreau Sep 17, 2025
acc60f5
remove solana fee and rename to nonevmfees
ghgoodreau Sep 17, 2025
644f6d3
fix: merge conflicts
ghgoodreau Sep 17, 2025
81f4d09
yarn install
ghgoodreau Sep 17, 2025
ea57895
test fixes
ghgoodreau Sep 17, 2025
9e9ba9a
more test updates
ghgoodreau Sep 17, 2025
beb089d
changelog updates
ghgoodreau Sep 17, 2025
3917e27
remove tron changes
ghgoodreau Sep 19, 2025
1af634d
update function naming
ghgoodreau Sep 19, 2025
41ce980
add TODO for chainID handling in activity for bitcoin
ghgoodreau Sep 19, 2025
fb63f06
update test for new fallback
ghgoodreau Sep 19, 2025
959ddbd
removes redundant conversion of fees for solana
ghgoodreau Sep 22, 2025
0056aec
Merge branch 'main' into SWAPS-2839-update-snap-methods-in-bridge-con…
micaelae Sep 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions packages/bridge-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Add support for Bitcoin bridge transactions ([#6454](https://github.com/MetaMask/core/pull/6454))
- Handle Bitcoin PSBT (Partially Signed Bitcoin Transaction) format in trade data
- Add `BitcoinTradeDataSchema` and `BitcoinQuoteResponseSchema` validators
- Support Bitcoin chain ID (`ChainId.BTC = 20000000000001`) and CAIP format (`bip122:000000000019d6689c085ae165831e93`)
- Add support for Tron blockchain ([#6454](https://github.com/MetaMask/core/pull/6454))
- Add `ChainId.TRON = 728126428` and CAIP format (`tron:0x2b6653dc`)
- Add `isTronChainId` utility function
- Export `isNonEvmChainId` utility function to check for non-EVM chains (Solana, Bitcoin, Tron) ([#6454](https://github.com/MetaMask/core/pull/6454))
- Add `selectDefaultSlippagePercentage` that returns the default slippage for a chain and token combination ([#6616](https://github.com/MetaMask/core/pull/6616))
- Return `0.5` if requesting a bridge quote
- Return `undefined` (auto) if requesting a Solana swap
Expand All @@ -18,9 +26,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **BREAKING:** Rename fee handling for non-EVM chains ([#6454](https://github.com/MetaMask/core/pull/6454))
- Replace `SolanaFees` type with `NonEvmFees` type
- Replace `solanaFeesInLamports` field with `nonEvmFeesInNative` field
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unclear what is being renamed here, is this a property on a return value? The context made it seem like it was a package export or a method or something. More context would be appreciated here

- Update `#appendSolanaFees` to `#appendNonEvmFees` to support all non-EVM chains
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line seems like an internal-only change

- The `nonEvmFeesInNative` field stores fees in the smallest units for each chain (lamports for Solana, satoshis for Bitcoin, sun for Tron)
- Update Snap methods to use new unified interface for non-EVM chains ([#6454](https://github.com/MetaMask/core/pull/6454))
- Replace `getFeeForTransaction` with `computeFee` method that returns fees in native token units
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a breaking change as well. This is a change in the expectations we have of snaps this controller inter-operates with.

- Update fee calculation to handle different unit conversions per chain
- Support fee computation for Bitcoin, Solana, and Tron chains
- Update quote validation to support Bitcoin-specific trade data format ([#6454](https://github.com/MetaMask/core/pull/6454))
Copy link
Member

@Gudahtt Gudahtt Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This is fine, but maybe better to move these two entries ("Update quote validation" and "Update selectors and utilities") as sub-bullets of "Add support for Bitcoin bridge transactions". It seems like a detail of that addition, I don't think it's as helpful to frame it as a standalone change.

- Add separate validation for Bitcoin quotes that include `unsignedPsbtBase64` field
- Update selectors and utilities to use `isNonEvmChainId` instead of `isSolanaChainId` for generic non-EVM handling ([#6454](https://github.com/MetaMask/core/pull/6454))
- Bump `@metamask/controller-utils` from `^11.12.0` to `^11.14.0` ([#6620](https://github.com/MetaMask/core/pull/6620), [#6629](https://github.com/MetaMask/core/pull/6629))
- Bump `@metamask/base-controller` from `^8.3.0` to `^8.4.0` ([#6632](https://github.com/MetaMask/core/pull/6632))

### Removed

- **BREAKING:** Remove deprecated `SolanaFees` type - use `NonEvmFees` type instead ([#6454](https://github.com/MetaMask/core/pull/6454))
- **BREAKING:** Remove `solanaFeesInLamports` field from quotes - use `nonEvmFeesInNative` field instead ([#6454](https://github.com/MetaMask/core/pull/6454))

## [43.0.0]

### Added
Expand Down

Large diffs are not rendered by default.

186 changes: 181 additions & 5 deletions packages/bridge-controller/src/bridge-controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { Contract } from '@ethersproject/contracts';
import { deriveStateFromMetadata } from '@metamask/base-controller';
import {
BtcScope,
EthAccountType,
EthScope,
SolAccountType,
Expand Down Expand Up @@ -589,6 +590,26 @@ describe('BridgeController', function () {
resolve('5000');
}, 200);
}
if (
(params as { handler: string })?.handler ===
'onClientRequest' &&
(params as { request?: { method: string } })?.request
?.method === 'computeFee'
) {
return setTimeout(() => {
resolve([
{
type: 'base',
asset: {
unit: 'SOL',
type: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:11111111111111111111111111111111',
amount: '0.000000014', // 14 lamports in SOL
fungible: true,
},
},
]);
}, 100);
}
return setTimeout(() => {
resolve({ value: '14' });
}, 100);
Expand Down Expand Up @@ -669,9 +690,15 @@ describe('BridgeController', function () {
minimumBalanceForRentExemptionInLamports: '5000',
quotes: mockBridgeQuotesSolErc20.map((quote) => ({
...quote,
solanaFeesInLamports: '14',
nonEvmFeesInNative: '14',
})),
quotesLoadingStatus: RequestStatus.FETCHED,
quoteRequest: quoteParams,
quoteFetchError: null,
assetExchangeRates: {},
quotesRefreshCount: 1,
quotesInitialLoadTime: expect.any(Number),
quotesLastFetched: expect.any(Number),
}),
);
expect(consoleErrorSpy).not.toHaveBeenCalled();
Expand Down Expand Up @@ -725,9 +752,15 @@ describe('BridgeController', function () {
minimumBalanceForRentExemptionInLamports: '5000',
quotes: mockBridgeQuotesSolErc20.map((quote) => ({
...quote,
solanaFeesInLamports: '14',
nonEvmFeesInNative: '14',
})),
quotesLoadingStatus: RequestStatus.FETCHED,
quoteRequest: quoteParams,
quoteFetchError: null,
assetExchangeRates: {},
quotesRefreshCount: expect.any(Number),
quotesInitialLoadTime: expect.any(Number),
quotesLastFetched: expect.any(Number),
}),
);
expect(consoleErrorSpy).not.toHaveBeenCalled();
Expand Down Expand Up @@ -755,9 +788,15 @@ describe('BridgeController', function () {
minimumBalanceForRentExemptionInLamports: '0',
quotes: mockBridgeQuotesSolErc20.map((quote) => ({
...quote,
solanaFeesInLamports: '14',
nonEvmFeesInNative: '14',
})),
quotesLoadingStatus: RequestStatus.FETCHED,
quoteRequest: { ...quoteParams, srcTokenAmount: '11111' },
quoteFetchError: null,
assetExchangeRates: {},
quotesRefreshCount: expect.any(Number),
quotesInitialLoadTime: expect.any(Number),
quotesLastFetched: expect.any(Number),
}),
);

Expand Down Expand Up @@ -1679,6 +1718,28 @@ describe('BridgeController', function () {
resolve(expectedMinBalance);
}, 200);
}
if (
(params as { handler: string })?.handler ===
'onClientRequest' &&
(params as { request?: { method: string } })?.request
?.method === 'computeFee'
) {
return setTimeout(() => {
resolve([
{
type: 'base',
asset: {
unit: 'SOL',
type: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:11111111111111111111111111111111',
amount: expectedFees
? `0.${expectedFees.padStart(9, '0')}`
: '0', // Convert lamports to SOL
fungible: true,
},
},
]);
}, 100);
}
return setTimeout(() => {
resolve({ value: expectedFees });
}, 100);
Expand Down Expand Up @@ -1752,9 +1813,9 @@ describe('BridgeController', function () {
}),
);

// Verify Solana fees
// Verify non-EVM fees
quotes.forEach((quote) => {
expect(quote.solanaFeesInLamports).toBe(
expect(quote.nonEvmFeesInNative).toBe(
isSolanaChainId(quote.quote.srcChainId) ? expectedFees : undefined,
);
});
Expand All @@ -1781,6 +1842,121 @@ describe('BridgeController', function () {
},
);

it('should handle BTC chain fees correctly', async () => {
jest.useFakeTimers();
// Use the actual Solana mock which already has string trade type
const btcQuoteResponse = mockBridgeQuotesSolErc20.map((quote) => ({
...quote,
quote: {
...quote.quote,
srcChainId: ChainId.BTC,
},
})) as unknown as QuoteResponse[];

messengerMock.call.mockImplementation(
(
...args: Parameters<BridgeControllerMessenger['call']>
): ReturnType<BridgeControllerMessenger['call']> => {
const [actionType, params] = args;

if (actionType === 'AccountsController:getSelectedMultichainAccount') {
return {
type: 'btc:p2wpkh',
id: 'btc-account-1',
scopes: [BtcScope.Mainnet],
methods: [],
address: 'bc1q...',
metadata: {
name: 'BTC Account 1',
importTime: 1717334400,
keyring: {
type: 'Snap Keyring',
},
snap: {
id: 'btc-snap-id',
name: 'BTC Snap',
},
},
} as never;
}

if (actionType === 'SnapController:handleRequest') {
return new Promise((resolve) => {
if (
(params as { handler: string })?.handler === 'onClientRequest' &&
(params as { request?: { method: string } })?.request?.method ===
'computeFee'
) {
return setTimeout(() => {
resolve([
{
type: 'base',
asset: {
unit: 'BTC',
type: 'bip122:000000000019d6689c085ae165831e93/slip44:0',
amount: '0.00005', // BTC fee
fungible: true,
},
},
]);
}, 100);
}
return setTimeout(() => {
resolve('5000');
}, 200);
});
}

return {
provider: jest.fn() as never,
selectedNetworkClientId: 'selectedNetworkClientId',
} as never;
},
);

jest.spyOn(fetchUtils, 'fetchBridgeQuotes').mockResolvedValue({
quotes: btcQuoteResponse,
validationFailures: [],
});

const quoteParams = {
srcChainId: ChainId.BTC.toString(),
destChainId: '1',
srcTokenAddress: 'NATIVE',
destTokenAddress: '0x0000000000000000000000000000000000000000',
srcTokenAmount: '100000', // satoshis
walletAddress: 'bc1q...',
destWalletAddress: '0x5342',
slippage: 0.5,
};

await bridgeController.updateBridgeQuoteRequestParams(
quoteParams,
metricsContext,
);

// Wait for polling to start
jest.advanceTimersByTime(201);
await flushPromises();

// Wait for fetch to trigger
jest.advanceTimersByTime(295);
await flushPromises();

// Wait for fetch to complete
jest.advanceTimersByTime(2601);
await flushPromises();

// Final wait for fee calculation
jest.advanceTimersByTime(100);
await flushPromises();

const { quotes } = bridgeController.state;
expect(quotes).toHaveLength(2); // mockBridgeQuotesSolErc20 has 2 quotes
expect(quotes[0].nonEvmFeesInNative).toBe('0.00005'); // BTC fee as-is
expect(quotes[1].nonEvmFeesInNative).toBe('0.00005'); // BTC fee as-is
});

describe('trackUnifiedSwapBridgeEvent client-side calls', () => {
beforeEach(() => {
jest.clearAllMocks();
Expand Down
Loading
Loading