Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 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
10 changes: 8 additions & 2 deletions packages/bridge-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Export `isNonEvmChainId` utility function to check for non-EVM chains (Solana, Bitcoin, Tron) ([#6454](https://github.com/MetaMask/core/pull/6454))

### Changed

- **BREAKING:** Bump peer dependency `@metamask/assets-controller` from `^74.0.0` to `^75.0.0` ([#6570](https://github.com/MetaMask/core/pull/6570))
- Bump `@metamask/keyring-api` from `^20.1.0` to `^21.0.0` ([#6560](https://github.com/MetaMask/core/pull/6560))
- 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
- Update fee format to return native units (e.g., SOL) instead of smallest units (e.g., Lamports)
- Add support for Tron chain alongside existing Bitcoin and Solana support

## [42.0.0]

Expand Down

Large diffs are not rendered by default.

179 changes: 179 additions & 0 deletions packages/bridge-controller/src/bridge-controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/* eslint-disable jest/no-conditional-in-test */
import { Contract } from '@ethersproject/contracts';
import {
BtcScope,
EthAccountType,
EthScope,
SolAccountType,
Expand Down Expand Up @@ -588,6 +589,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,8 +690,15 @@ describe('BridgeController', function () {
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,8 +753,15 @@ describe('BridgeController', function () {
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,8 +790,15 @@ describe('BridgeController', function () {
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 @@ -1678,6 +1720,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 @@ -1780,6 +1844,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].solanaFeesInLamports).toBe('0.00005'); // BTC fee as-is
expect(quotes[1].solanaFeesInLamports).toBe('0.00005'); // BTC fee as-is
});

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