Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions packages/bridge-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **BREAKING:** Add `quickBuy` and `dappSwap` FeatureIds for external swap quote consumers ([#8598](https://github.com/MetaMask/core/pull/8598))
- **BREAKING:** Add `market_closed` and `quote_expired` QuoteWarning ([#8598](https://github.com/MetaMask/core/pull/8598))
- Add `tokenSecurityTypeDestination: string | null` to `BridgeControllerState` (default `null`), set via `updateBridgeQuoteRequestParams` and reset by `resetState` ([#8595](https://github.com/MetaMask/core/pull/8595))
- Add Stellar network support for bridge quotes and non-EVM fee calculation (TODO: THIS PR)
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

### Changed

Expand Down
126 changes: 126 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 @@
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,130 @@
);
});

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[];

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') {
expect(

Check failure on line 2917 in packages/bridge-controller/src/bridge-controller.test.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (lint:eslint)

Avoid calling `expect` conditionally`
(params as { request?: { params?: { scope?: string } } }).request
?.params?.scope,
).toBe(XlmScope.Pubnet);
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');
});
});

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
14 changes: 12 additions & 2 deletions packages/bridge-controller/src/utils/bridge.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AddressZero } from '@ethersproject/constants';
import { Contract } from '@ethersproject/contracts';
import { BtcScope, SolScope, TrxScope } from '@metamask/keyring-api';
import { BtcScope, SolScope, TrxScope, XlmScope } from '@metamask/keyring-api';
import { abiERC20 } from '@metamask/metamask-eth-abis';
import { isCaipChainId, isStrictHexString } from '@metamask/utils';
import type { CaipAssetType, CaipChainId, Hex } from '@metamask/utils';
Expand Down Expand Up @@ -41,7 +41,7 @@
export const isCrossChain = (
srcChainId: GenericQuoteRequest['srcChainId'],
destChainId?: GenericQuoteRequest['destChainId'],
) => {

Check failure on line 44 in packages/bridge-controller/src/utils/bridge.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (lint:eslint)

Missing return type on function
try {
if (!destChainId) {
return false;
Expand Down Expand Up @@ -115,7 +115,7 @@
*/
export const getEthUsdtResetData = (
destChainId: GenericQuoteRequest['destChainId'],
) => {

Check failure on line 118 in packages/bridge-controller/src/utils/bridge.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (lint:eslint)

Missing return type on function
const spenderAddress = isCrossChain(CHAIN_IDS.MAINNET, destChainId)
? METABRIDGE_ETHEREUM_ADDRESS
: SWAPS_CONTRACT_ADDRESSES[CHAIN_IDS.MAINNET];
Expand All @@ -132,7 +132,7 @@
export const isEthUsdt = (
chainId: GenericQuoteRequest['srcChainId'],
address: string,
) =>

Check failure on line 135 in packages/bridge-controller/src/utils/bridge.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (lint:eslint)

Missing return type on function
formatChainIdToDec(chainId) === ChainId.ETH &&
address.toLowerCase() === ETH_USDT_ADDRESS.toLowerCase();

Expand All @@ -156,7 +156,7 @@
export const isSwapsDefaultTokenAddress = (
address: string,
chainId: Hex | CaipChainId,
) => {

Check failure on line 159 in packages/bridge-controller/src/utils/bridge.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (lint:eslint)

Missing return type on function
if (!address || !chainId) {
return false;
}
Expand All @@ -175,7 +175,7 @@
export const isSwapsDefaultTokenSymbol = (
symbol: string,
chainId: Hex | CaipChainId,
) => {

Check failure on line 178 in packages/bridge-controller/src/utils/bridge.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (lint:eslint)

Missing return type on function
if (!symbol || !chainId) {
return false;
}
Expand All @@ -189,7 +189,7 @@
* @param address - The address to check
* @returns Whether the address is a native asset
*/
export const isNativeAddress = (address?: string | null) =>

Check failure on line 192 in packages/bridge-controller/src/utils/bridge.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (lint:eslint)

Missing return type on function
address === AddressZero || // bridge and swap apis set the native asset address to zero
address === '' || // assets controllers set the native asset address to an empty string
!address ||
Expand All @@ -207,7 +207,7 @@
*/
export const isSolanaChainId = (
chainId: Hex | number | CaipChainId | string,
) => {

Check failure on line 210 in packages/bridge-controller/src/utils/bridge.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (lint:eslint)

Missing return type on function
if (isCaipChainId(chainId)) {
return chainId === SolScope.Mainnet.toString();
}
Expand All @@ -216,23 +216,32 @@

export const isBitcoinChainId = (
chainId: Hex | number | CaipChainId | string,
) => {

Check failure on line 219 in packages/bridge-controller/src/utils/bridge.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (lint:eslint)

Missing return type on function
if (isCaipChainId(chainId)) {
return chainId === BtcScope.Mainnet.toString();
}
return chainId.toString() === ChainId.BTC.toString();
};

export const isTronChainId = (chainId: Hex | number | CaipChainId | string) => {

Check failure on line 226 in packages/bridge-controller/src/utils/bridge.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (lint:eslint)

Missing return type on function
if (isCaipChainId(chainId)) {
return chainId === TrxScope.Mainnet.toString();
}
return chainId.toString() === ChainId.TRON.toString();
};

export const isStellarChainId = (
chainId: Hex | number | CaipChainId | string,
) => {
if (isCaipChainId(chainId)) {
return chainId === XlmScope.Pubnet.toString();
}
return chainId.toString() === ChainId.STELLAR.toString();
};

/**
* Checks if a chain ID represents a non-EVM blockchain supported by swaps
* Currently supports Solana, Bitcoin and Tron
* Currently supports Solana, Bitcoin, Stellar and Tron
*
* @param chainId - The chain ID to check
* @returns True if the chain is a supported non-EVM chain, false otherwise
Expand All @@ -243,6 +252,7 @@
return (
isSolanaChainId(chainId) ||
isBitcoinChainId(chainId) ||
isStellarChainId(chainId) ||
isTronChainId(chainId)
);
};
Expand Down
Loading
Loading