Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 0 additions & 3 deletions eslint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -657,9 +657,6 @@
}
},
"packages/bridge-controller/src/utils/bridge.ts": {
"@typescript-eslint/explicit-function-return-type": {
"count": 9
},
"id-denylist": {
"count": 1
}
Expand Down
4 changes: 4 additions & 0 deletions packages/bridge-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add Stellar network support for bridge quotes and non-EVM fee calculation ([#8625](https://github.com/MetaMask/core/pull/8625))

### Changed

- Bump `@metamask/messenger` from `^1.1.1` to `^1.2.0` ([#8632](https://github.com/MetaMask/core/pull/8632))
Expand Down
131 changes: 131 additions & 0 deletions packages/bridge-controller/src/bridge-controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
EthScope,
SolAccountType,
SolScope,
XlmAccountType,
XlmScope,
} from '@metamask/keyring-api';
import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger';
import type {
Expand Down Expand Up @@ -2833,6 +2835,135 @@ describe('BridgeController', function () {
);
});

it('should append Stellar fees for Stellar quotes', async () => {
await withController(async ({ controller: bridgeController }) => {
const stellarQuoteResponse = mockBridgeQuotesSolErc20.map((quote) => {
const stellarNativeAsset = getNativeAssetForChainId(ChainId.STELLAR);
return {
...quote,
quote: {
...quote.quote,
srcChainId: ChainId.STELLAR,
destChainId: ChainId.STELLAR,
srcAsset: stellarNativeAsset,
destAsset: {
...stellarNativeAsset,
address:
'CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75',
assetId:
'stellar:pubnet/sep41:CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75',
symbol: 'USDC',
name: 'USDC',
},
feeData: {
...quote.quote.feeData,
metabridge: {
...quote.quote.feeData.metabridge,
asset: stellarNativeAsset,
},
},
steps: quote.quote.steps.map((step) => ({
...step,
srcChainId: ChainId.STELLAR,
destChainId: ChainId.STELLAR,
srcAsset: stellarNativeAsset,
destAsset: stellarNativeAsset,
})),
},
};
}) as unknown as QuoteResponse[];
const snapRequestScopes: (string | undefined)[] = [];

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

if (actionType === 'AuthenticationController:getBearerToken') {
return 'AUTH_TOKEN';
}

if (actionType === 'RemoteFeatureFlagController:getState') {
return {
remoteFeatureFlags: {
bridgeConfig,
},
} as never;
}

if (actionType === 'AccountsController:getAccountByAddress') {
return {
type: XlmAccountType.Account,
id: 'xlm-account-1',
scopes: [XlmScope.Pubnet],
methods: [],
address:
'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN',
metadata: {
name: 'Stellar Account 1',
importTime: 1717334400,
keyring: {
type: 'Snap Keyring',
},
snap: {
id: 'npm:@metamask/stellar-snap',
name: 'Stellar Snap',
},
},
} as never;
}

if (actionType === 'SnapController:handleRequest') {
snapRequestScopes.push(
(params as { request?: { params?: { scope?: string } } }).request
?.params?.scope,
);
return Promise.resolve([
{
type: 'base',
asset: {
unit: 'XLM',
type: 'stellar:pubnet/slip44:148',
amount: '0.00001',
fungible: true,
},
},
]) as never;
}

return {} as never;
},
);

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

const quotes = await bridgeController.fetchQuotes({
srcChainId: ChainId.STELLAR,
destChainId: ChainId.STELLAR,
srcTokenAddress: '0x0000000000000000000000000000000000000000',
destTokenAddress:
'CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75',
srcTokenAmount: '300000000',
walletAddress:
'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN',
gasIncluded: false,
gasIncluded7702: false,
});

expect(quotes).toHaveLength(2);
expect(quotes[0].nonEvmFeesInNative).toBe('0.00001');
expect(quotes[1].nonEvmFeesInNative).toBe('0.00001');
expect(snapRequestScopes).toStrictEqual([
XlmScope.Pubnet,
XlmScope.Pubnet,
]);
});
});

describe('trackUnifiedSwapBridgeEvent client-side calls', () => {
beforeEach(() => {
jest.clearAllMocks();
Expand Down
4 changes: 3 additions & 1 deletion packages/bridge-controller/src/constants/bridge.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AddressZero } from '@ethersproject/constants';
import { BtcScope, SolScope, TrxScope } from '@metamask/keyring-api';
import { BtcScope, SolScope, TrxScope, XlmScope } from '@metamask/keyring-api';
import type { Hex } from '@metamask/utils';

import type {
Expand All @@ -24,6 +24,7 @@ export const ALLOWED_BRIDGE_CHAIN_IDS = [
CHAIN_IDS.MEGAETH,
SolScope.Mainnet,
BtcScope.Mainnet,
XlmScope.Pubnet,
TrxScope.Mainnet,
] as const;

Expand Down Expand Up @@ -55,6 +56,7 @@ export const DEFAULT_CHAIN_RANKING = [
{ chainId: 'eip155:56', name: 'BNB' },
{ chainId: 'bip122:000000000019d6689c085ae165831e93', name: 'BTC' },
{ chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', name: 'Solana' },
{ chainId: 'stellar:pubnet', name: 'Stellar' },
{ chainId: 'tron:728126428', name: 'Tron' },
{ chainId: 'eip155:8453', name: 'Base' },
{ chainId: 'eip155:42161', name: 'Arbitrum' },
Expand Down
13 changes: 12 additions & 1 deletion packages/bridge-controller/src/constants/tokens.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BtcScope, SolScope, TrxScope } from '@metamask/keyring-api';
import { BtcScope, SolScope, TrxScope, XlmScope } from '@metamask/keyring-api';

import type { AllowedBridgeChainIds } from './bridge';
import { CHAIN_IDS } from './chains';
Expand Down Expand Up @@ -55,6 +55,7 @@ const CURRENCY_SYMBOLS = {
SOL: 'SOL',
SEI: 'SEI',
BTC: 'BTC',
XLM: 'XLM',
TRX: 'TRX',
MON: 'MON',
HYPE: 'HYPE',
Expand Down Expand Up @@ -153,6 +154,14 @@ const BTC_SWAPS_TOKEN_OBJECT = {
iconUrl: '',
} as const;

const XLM_SWAPS_TOKEN_OBJECT = {
symbol: CURRENCY_SYMBOLS.XLM,
name: 'Stellar Lumens',
address: DEFAULT_TOKEN_ADDRESS,
decimals: 7,
iconUrl: '',
} as const;

const SEI_SWAPS_TOKEN_OBJECT = {
symbol: CURRENCY_SYMBOLS.SEI,
name: 'Sei',
Expand Down Expand Up @@ -209,6 +218,7 @@ export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = {
[SolScope.Mainnet]: SOLANA_SWAPS_TOKEN_OBJECT,
[SolScope.Devnet]: SOLANA_SWAPS_TOKEN_OBJECT,
[BtcScope.Mainnet]: BTC_SWAPS_TOKEN_OBJECT,
[XlmScope.Pubnet]: XLM_SWAPS_TOKEN_OBJECT,
[TrxScope.Mainnet]: TRX_SWAPS_TOKEN_OBJECT,
} as const;

Expand All @@ -227,6 +237,7 @@ export const SYMBOL_TO_SLIP44_MAP: Record<
> = {
SOL: 'slip44:501',
BTC: 'slip44:0',
XLM: 'slip44:148',
ETH: 'slip44:60',
POL: 'slip44:966',
BNB: 'slip44:714',
Expand Down
1 change: 1 addition & 0 deletions packages/bridge-controller/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export {
isNativeAddress,
isSolanaChainId,
isBitcoinChainId,
isStellarChainId,
isTronChainId,
isNonEvmChainId,
getNativeAssetForChainId,
Expand Down
1 change: 1 addition & 0 deletions packages/bridge-controller/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ export enum ChainId {
LINEA = 59144,
SOLANA = 1151111081099710,
BTC = 20000000000001,
STELLAR = 20000000000002,
TRON = 728126428,
SEI = 1329,
MONAD = 143,
Expand Down
35 changes: 34 additions & 1 deletion packages/bridge-controller/src/utils/bridge.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BtcScope, SolScope } from '@metamask/keyring-api';
import { BtcScope, SolScope, XlmScope } from '@metamask/keyring-api';
import type { Hex } from '@metamask/utils';

import {
Expand All @@ -15,6 +15,7 @@ import {
isEthUsdt,
isNonEvmChainId,
isSolanaChainId,
isStellarChainId,
isSwapsDefaultTokenAddress,
isSwapsDefaultTokenSymbol,
sumHexes,
Expand Down Expand Up @@ -185,6 +186,23 @@ describe('Bridge utils', () => {
});
});

describe('isStellarChainId', () => {
it('returns true for ChainId.STELLAR', () => {
expect(isStellarChainId(ChainId.STELLAR)).toBe(true);
expect(isStellarChainId('20000000000002')).toBe(true);
});

it('returns true for XlmScope.Pubnet', () => {
expect(isStellarChainId(XlmScope.Pubnet)).toBe(true);
});

it('returns false for other chainIds', () => {
expect(isStellarChainId(1)).toBe(false);
expect(isStellarChainId('0x0')).toBe(false);
expect(isStellarChainId(XlmScope.Testnet)).toBe(false);
});
});

describe('isNonEvmChainId', () => {
it('returns true for Solana chainIds', () => {
expect(isNonEvmChainId(ChainId.SOLANA)).toBe(true);
Expand All @@ -198,6 +216,12 @@ describe('Bridge utils', () => {
expect(isNonEvmChainId('20000000000001')).toBe(true);
});

it('returns true for Stellar chainIds', () => {
expect(isNonEvmChainId(ChainId.STELLAR)).toBe(true);
expect(isNonEvmChainId(XlmScope.Pubnet)).toBe(true);
expect(isNonEvmChainId('20000000000002')).toBe(true);
});

it('returns false for EVM chainIds', () => {
expect(isNonEvmChainId('0x1')).toBe(false);
expect(isNonEvmChainId(1)).toBe(false);
Expand Down Expand Up @@ -268,6 +292,15 @@ describe('Bridge utils', () => {
});
});

it('should return native asset for Stellar chainId', () => {
const result = getNativeAssetForChainId(XlmScope.Pubnet);
expect(result).toStrictEqual({
...SWAPS_CHAINID_DEFAULT_TOKEN_MAP[XlmScope.Pubnet],
chainId: 20000000000002,
assetId: 'stellar:pubnet/slip44:148',
});
});

it('should throw error for unsupported chainId', () => {
expect(() => getNativeAssetForChainId('999999')).toThrow(
'No XChain Swaps native asset found for chainId: 999999',
Expand Down
Loading
Loading