diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 29a627b6ee..01040d936b 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -657,9 +657,6 @@ } }, "packages/bridge-controller/src/utils/bridge.ts": { - "@typescript-eslint/explicit-function-return-type": { - "count": 9 - }, "id-denylist": { "count": 1 } diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index ddcf6710ed..a9d0c67daf 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -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)) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 970f952435..69daddcfbb 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -6,6 +6,8 @@ import { EthScope, SolAccountType, SolScope, + XlmAccountType, + XlmScope, } from '@metamask/keyring-api'; import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger'; import type { @@ -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 + ): ReturnType => { + 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(); diff --git a/packages/bridge-controller/src/constants/bridge.ts b/packages/bridge-controller/src/constants/bridge.ts index ac3dfffe6d..5cd04c2ee7 100644 --- a/packages/bridge-controller/src/constants/bridge.ts +++ b/packages/bridge-controller/src/constants/bridge.ts @@ -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 { @@ -24,6 +24,7 @@ export const ALLOWED_BRIDGE_CHAIN_IDS = [ CHAIN_IDS.MEGAETH, SolScope.Mainnet, BtcScope.Mainnet, + XlmScope.Pubnet, TrxScope.Mainnet, ] as const; @@ -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' }, diff --git a/packages/bridge-controller/src/constants/tokens.ts b/packages/bridge-controller/src/constants/tokens.ts index 1c0ec09894..0609865d87 100644 --- a/packages/bridge-controller/src/constants/tokens.ts +++ b/packages/bridge-controller/src/constants/tokens.ts @@ -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'; @@ -55,6 +55,7 @@ const CURRENCY_SYMBOLS = { SOL: 'SOL', SEI: 'SEI', BTC: 'BTC', + XLM: 'XLM', TRX: 'TRX', MON: 'MON', HYPE: 'HYPE', @@ -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', @@ -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; @@ -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', diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index eb42636685..d7754cb3b7 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -139,6 +139,7 @@ export { isNativeAddress, isSolanaChainId, isBitcoinChainId, + isStellarChainId, isTronChainId, isNonEvmChainId, getNativeAssetForChainId, diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index 5581bd6560..1902882dd8 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -317,6 +317,7 @@ export enum ChainId { LINEA = 59144, SOLANA = 1151111081099710, BTC = 20000000000001, + STELLAR = 20000000000002, TRON = 728126428, SEI = 1329, MONAD = 143, diff --git a/packages/bridge-controller/src/utils/bridge.test.ts b/packages/bridge-controller/src/utils/bridge.test.ts index 97680af1e2..a300dcf4c6 100644 --- a/packages/bridge-controller/src/utils/bridge.test.ts +++ b/packages/bridge-controller/src/utils/bridge.test.ts @@ -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 { @@ -15,6 +15,7 @@ import { isEthUsdt, isNonEvmChainId, isSolanaChainId, + isStellarChainId, isSwapsDefaultTokenAddress, isSwapsDefaultTokenSymbol, sumHexes, @@ -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); @@ -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); @@ -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', diff --git a/packages/bridge-controller/src/utils/bridge.ts b/packages/bridge-controller/src/utils/bridge.ts index 204b164d51..4908c21500 100644 --- a/packages/bridge-controller/src/utils/bridge.ts +++ b/packages/bridge-controller/src/utils/bridge.ts @@ -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'; @@ -41,7 +41,7 @@ import { export const isCrossChain = ( srcChainId: GenericQuoteRequest['srcChainId'], destChainId?: GenericQuoteRequest['destChainId'], -) => { +): boolean => { try { if (!destChainId) { return false; @@ -115,7 +115,7 @@ export const getNativeAssetForChainId = ( */ export const getEthUsdtResetData = ( destChainId: GenericQuoteRequest['destChainId'], -) => { +): string => { const spenderAddress = isCrossChain(CHAIN_IDS.MAINNET, destChainId) ? METABRIDGE_ETHEREUM_ADDRESS : SWAPS_CONTRACT_ADDRESSES[CHAIN_IDS.MAINNET]; @@ -132,7 +132,7 @@ export const getEthUsdtResetData = ( export const isEthUsdt = ( chainId: GenericQuoteRequest['srcChainId'], address: string, -) => +): boolean => formatChainIdToDec(chainId) === ChainId.ETH && address.toLowerCase() === ETH_USDT_ADDRESS.toLowerCase(); @@ -156,7 +156,7 @@ export const sumHexes = (...hexStrings: string[]): Hex => { export const isSwapsDefaultTokenAddress = ( address: string, chainId: Hex | CaipChainId, -) => { +): boolean => { if (!address || !chainId) { return false; } @@ -175,7 +175,7 @@ export const isSwapsDefaultTokenAddress = ( export const isSwapsDefaultTokenSymbol = ( symbol: string, chainId: Hex | CaipChainId, -) => { +): boolean => { if (!symbol || !chainId) { return false; } @@ -189,7 +189,7 @@ export const isSwapsDefaultTokenSymbol = ( * @param address - The address to check * @returns Whether the address is a native asset */ -export const isNativeAddress = (address?: string | null) => +export const isNativeAddress = (address?: string | null): boolean => 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 || @@ -207,7 +207,7 @@ export const isNativeAddress = (address?: string | null) => */ export const isSolanaChainId = ( chainId: Hex | number | CaipChainId | string, -) => { +): boolean => { if (isCaipChainId(chainId)) { return chainId === SolScope.Mainnet.toString(); } @@ -216,23 +216,34 @@ export const isSolanaChainId = ( export const isBitcoinChainId = ( chainId: Hex | number | CaipChainId | string, -) => { +): boolean => { if (isCaipChainId(chainId)) { return chainId === BtcScope.Mainnet.toString(); } return chainId.toString() === ChainId.BTC.toString(); }; -export const isTronChainId = (chainId: Hex | number | CaipChainId | string) => { +export const isTronChainId = ( + chainId: Hex | number | CaipChainId | string, +): boolean => { if (isCaipChainId(chainId)) { return chainId === TrxScope.Mainnet.toString(); } return chainId.toString() === ChainId.TRON.toString(); }; +export const isStellarChainId = ( + chainId: Hex | number | CaipChainId | string, +): boolean => { + 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 @@ -243,6 +254,7 @@ export const isNonEvmChainId = ( return ( isSolanaChainId(chainId) || isBitcoinChainId(chainId) || + isStellarChainId(chainId) || isTronChainId(chainId) ); }; diff --git a/packages/bridge-controller/src/utils/caip-formatters.test.ts b/packages/bridge-controller/src/utils/caip-formatters.test.ts index 6b39b96434..7d0233330e 100644 --- a/packages/bridge-controller/src/utils/caip-formatters.test.ts +++ b/packages/bridge-controller/src/utils/caip-formatters.test.ts @@ -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 { CHAIN_IDS } from '../constants/chains'; import { ChainId } from '../types'; @@ -41,6 +41,11 @@ describe('CAIP Formatters', () => { expect(formatChainIdToCaip(TrxScope.Mainnet)).toBe(TrxScope.Mainnet); }); + it('should convert Stellar chainId to XlmScope.Pubnet', () => { + expect(formatChainIdToCaip(ChainId.STELLAR)).toBe(XlmScope.Pubnet); + expect(formatChainIdToCaip(XlmScope.Pubnet)).toBe(XlmScope.Pubnet); + }); + it('should convert number to CAIP format', () => { expect(formatChainIdToCaip(1)).toBe('eip155:1'); }); @@ -68,6 +73,10 @@ describe('CAIP Formatters', () => { expect(formatChainIdToDec(TrxScope.Mainnet)).toBe(ChainId.TRON); }); + it('should handle Stellar pubnet', () => { + expect(formatChainIdToDec(XlmScope.Pubnet)).toBe(ChainId.STELLAR); + }); + it('should parse CAIP chainId to decimal', () => { expect(formatChainIdToDec('eip155:1')).toBe(1); }); @@ -111,6 +120,12 @@ describe('CAIP Formatters', () => { `Invalid cross-chain swaps chainId: ${SolScope.Mainnet}`, ); }); + + it('should throw error for Stellar chainId (non-EVM)', () => { + expect(() => formatChainIdToHex(XlmScope.Pubnet)).toThrow( + `Invalid cross-chain swaps chainId: ${XlmScope.Pubnet}`, + ); + }); }); describe('formatAddressToCaipReference', () => { @@ -133,6 +148,9 @@ describe('CAIP Formatters', () => { expect( formatAddressToCaipReference(`${BtcScope.Mainnet}/slip44:0`), ).toStrictEqual(AddressZero); + expect( + formatAddressToCaipReference(`${XlmScope.Pubnet}/slip44:148`), + ).toStrictEqual(AddressZero); }); it('should extract address from CAIP format', () => { @@ -193,6 +211,11 @@ describe('CAIP Formatters', () => { expect(result).toBe('bip122:000000000019d6689c085ae165831e93/slip44:0'); }); + it('should return native asset for chainId when address is Stellar native asset', () => { + const result = formatAddressToAssetId('148', XlmScope.Pubnet); + expect(result).toBe('stellar:pubnet/slip44:148'); + }); + it('should return native asset for chainId when address is BSC native asset', () => { const result = formatAddressToAssetId('714', '0x38'); expect(result).toBe('eip155:56/slip44:714'); @@ -277,5 +300,30 @@ describe('CAIP Formatters', () => { 'tron:728126428/trc20:TJ1234567890123456789012345678901234567890', ); }); + + it('should create Stellar classic asset type for CODE-ISSUER tokens', () => { + const tokenAddress = + 'VELO-GDM4RQUQQUVSKQA7S6EM7XBZP3FCGH4Q7CL6TABQ7B2BEJ5ERARM2M5M'; + expect(formatAddressToAssetId(tokenAddress, ChainId.STELLAR)).toBe( + `${XlmScope.Pubnet}/asset:${tokenAddress}`, + ); + }); + + it('should create Stellar SEP-41 asset type for Soroban contract IDs', () => { + const tokenAddress = + 'CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75'; + expect(formatAddressToAssetId(tokenAddress, ChainId.STELLAR)).toBe( + `${XlmScope.Pubnet}/sep41:${tokenAddress}`, + ); + }); + + it('should not create Stellar asset types for bare issuer account addresses', () => { + expect( + formatAddressToAssetId( + 'GDTVUGOC5UHFYH3Y3XECEG7UO7H5PRPI6WPMOMKN5UVBFZNQOKHM4C7I', + ChainId.STELLAR, + ), + ).toBeUndefined(); + }); }); }); diff --git a/packages/bridge-controller/src/utils/caip-formatters.ts b/packages/bridge-controller/src/utils/caip-formatters.ts index 450be976b0..a38dc53505 100644 --- a/packages/bridge-controller/src/utils/caip-formatters.ts +++ b/packages/bridge-controller/src/utils/caip-formatters.ts @@ -5,7 +5,7 @@ import { convertHexToDecimal, toChecksumHexAddress, } from '@metamask/controller-utils'; -import { BtcScope, SolScope, TrxScope } from '@metamask/keyring-api'; +import { BtcScope, SolScope, TrxScope, XlmScope } from '@metamask/keyring-api'; import { toEvmCaipChainId } from '@metamask/multichain-network-controller'; import { isCaipChainId, @@ -25,9 +25,13 @@ import { isBitcoinChainId, isNativeAddress, isSolanaChainId, + isStellarChainId, isTronChainId, } from './bridge'; +const STELLAR_CLASSIC_ASSET_REGEX = /^[a-zA-Z0-9]{1,12}-G[A-Z2-7]{55}$/u; +const STELLAR_SOROBAN_CONTRACT_REGEX = /^C[A-Z2-7]{55}$/u; + /** * Converts a chainId to a CaipChainId * @@ -49,6 +53,9 @@ export const formatChainIdToCaip = ( if (isBitcoinChainId(chainId)) { return BtcScope.Mainnet; } + if (isStellarChainId(chainId)) { + return XlmScope.Pubnet; + } if (isTronChainId(chainId)) { return TrxScope.Mainnet; } @@ -73,6 +80,9 @@ export const formatChainIdToDec = ( if (chainId === BtcScope.Mainnet) { return ChainId.BTC; } + if (chainId === XlmScope.Pubnet) { + return ChainId.STELLAR; + } if (chainId === TrxScope.Mainnet) { return ChainId.TRON; } @@ -170,6 +180,20 @@ export const formatAddressToAssetId = ( ); } + if (chainIdCaip === XlmScope.Pubnet) { + if (STELLAR_CLASSIC_ASSET_REGEX.test(addressOrAssetId)) { + return CaipAssetTypeStruct.create( + `${chainIdCaip}/asset:${addressOrAssetId}`, + ); + } + if (STELLAR_SOROBAN_CONTRACT_REGEX.test(addressOrAssetId)) { + return CaipAssetTypeStruct.create( + `${chainIdCaip}/sep41:${addressOrAssetId}`, + ); + } + return undefined; + } + // EVM assets if (!isStrictHexString(addressOrAssetId)) { return undefined; diff --git a/packages/bridge-controller/src/utils/feature-flags.test.ts b/packages/bridge-controller/src/utils/feature-flags.test.ts index b9b00d811f..14c244cc6d 100644 --- a/packages/bridge-controller/src/utils/feature-flags.test.ts +++ b/packages/bridge-controller/src/utils/feature-flags.test.ts @@ -46,6 +46,10 @@ describe('feature-flags', () => { isActiveSrc: true, isActiveDest: true, }, + '20000000000002': { + isActiveSrc: true, + isActiveDest: true, + }, }, chainRanking: [], }; @@ -87,6 +91,10 @@ describe('feature-flags', () => { isActiveSrc: true, isActiveDest: true, }, + 'stellar:pubnet': { + isActiveSrc: true, + isActiveDest: true, + }, }, }); }); @@ -223,6 +231,10 @@ describe('feature-flags', () => { isActiveSrc: true, isActiveDest: true, }, + '20000000000002': { + isActiveSrc: true, + isActiveDest: true, + }, }, }; @@ -312,6 +324,10 @@ describe('feature-flags', () => { isActiveDest: true, isActiveSrc: true, }, + 'stellar:pubnet': { + isActiveDest: true, + isActiveSrc: true, + }, }, }; diff --git a/packages/bridge-controller/src/utils/quote.test.ts b/packages/bridge-controller/src/utils/quote.test.ts index 821c86c4e1..91154cb997 100644 --- a/packages/bridge-controller/src/utils/quote.test.ts +++ b/packages/bridge-controller/src/utils/quote.test.ts @@ -2,6 +2,7 @@ import { AddressZero } from '@ethersproject/constants'; import { convertHexToDecimal } from '@metamask/controller-utils'; import { BigNumber } from 'bignumber.js'; +import { ChainId } from '../types'; import type { GenericQuoteRequest, QuoteResponse, @@ -149,6 +150,41 @@ describe('Quote Utils', () => { expect(isValidQuoteRequest(requestWithoutSlippage)).toBe(true); }); }); + + it('requires destWalletAddress when bridging to Stellar', () => { + expect( + isValidQuoteRequest({ + ...validRequest, + destChainId: ChainId.STELLAR.toString(), + }), + ).toBe(false); + + expect( + isValidQuoteRequest({ + ...validRequest, + destChainId: ChainId.STELLAR.toString(), + destWalletAddress: + 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN', + }), + ).toBe(true); + }); + + it('requires destWalletAddress when bridging from Stellar', () => { + expect( + isValidQuoteRequest({ + ...validRequest, + srcChainId: ChainId.STELLAR.toString(), + }), + ).toBe(false); + + expect( + isValidQuoteRequest({ + ...validRequest, + srcChainId: ChainId.STELLAR.toString(), + destWalletAddress: '0x789', + }), + ).toBe(true); + }); }); });