diff --git a/packages/assets-controllers/src/MultichainAssetsRatesController/constant.ts b/packages/assets-controllers/src/MultichainAssetsRatesController/constant.ts index 2fef0e8155..e0610f49f3 100644 --- a/packages/assets-controllers/src/MultichainAssetsRatesController/constant.ts +++ b/packages/assets-controllers/src/MultichainAssetsRatesController/constant.ts @@ -26,7 +26,7 @@ export const MAP_CAIP_CURRENCIES: { // XRP mainnet xrp: 'xrpl:mainnet/slip44:144', - // Stellar Lumens mainnet + // Stellar Lumens (spot/fiat map uses pubnet; per-chain testnet balances use stellar:testnet/slip44:148 when wired) xlm: 'stellar:pubnet/slip44:148', // Chainlink (ERC20 on Ethereum mainnet) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index f3b55d98c5..c88ab76e82 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add Stellar support for bridge token flows: `isStellarChainId`, `ChainId.STELLAR`, native XLM metadata, CAIP/decimal formatting aligned with Bridge API, and Stellar pubnet/testnet in `isNonEvmChainId` ([#TODO](https://github.com/MetaMask/core/pull/TODO)) +- Add `StellarTradeDataSchema`, `StellarTradeData`, and `isStellarTrade`; extend `extractTradeData` to read Stellar XDR from `{ xdrBase64 }` or `{ xdr }` objects ([#TODO](https://github.com/MetaMask/core/pull/TODO)) + ### Changed - Bump `@metamask/transaction-controller` from `^64.2.0` to `^64.3.0` ([#8482](https://github.com/MetaMask/core/pull/8482)) diff --git a/packages/bridge-controller/src/constants/bridge.ts b/packages/bridge-controller/src/constants/bridge.ts index 14be143ec2..b72baf66ca 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 { @@ -25,6 +25,7 @@ export const ALLOWED_BRIDGE_CHAIN_IDS = [ SolScope.Mainnet, BtcScope.Mainnet, TrxScope.Mainnet, + XlmScope.Pubnet, ] as const; export type AllowedBridgeChainIds = (typeof ALLOWED_BRIDGE_CHAIN_IDS)[number]; @@ -56,6 +57,7 @@ export const DEFAULT_CHAIN_RANKING = [ { chainId: 'bip122:000000000019d6689c085ae165831e93', name: 'BTC' }, { chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', name: 'Solana' }, { chainId: 'tron:728126428', name: 'Tron' }, + { chainId: 'stellar:pubnet', name: 'Stellar' }, { chainId: 'eip155:8453', name: 'Base' }, { chainId: 'eip155:42161', name: 'Arbitrum' }, { chainId: 'eip155:59144', name: 'Linea' }, diff --git a/packages/bridge-controller/src/constants/tokens.ts b/packages/bridge-controller/src/constants/tokens.ts index 1c0ec09894..c4f6b85d9d 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'; @@ -59,6 +59,7 @@ const CURRENCY_SYMBOLS = { MON: 'MON', HYPE: 'HYPE', MEGAETH: 'ETH', + XLM: 'XLM', } as const; const ETH_SWAPS_TOKEN_OBJECT = { @@ -169,6 +170,14 @@ const TRX_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 MONAD_SWAPS_TOKEN_OBJECT = { symbol: CURRENCY_SYMBOLS.MON, name: 'Mon', @@ -210,6 +219,7 @@ export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = { [SolScope.Devnet]: SOLANA_SWAPS_TOKEN_OBJECT, [BtcScope.Mainnet]: BTC_SWAPS_TOKEN_OBJECT, [TrxScope.Mainnet]: TRX_SWAPS_TOKEN_OBJECT, + [XlmScope.Pubnet]: XLM_SWAPS_TOKEN_OBJECT, } as const; export type SupportedSwapsNativeCurrencySymbols = @@ -234,6 +244,7 @@ export const SYMBOL_TO_SLIP44_MAP: Record< TESTETH: 'slip44:60', SEI: 'slip44:19000118', TRX: 'slip44:195', + XLM: 'slip44:148', MON: 'slip44:268435779', HYPE: 'slip44:2457', }; diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index 5794866620..ed7739f1e1 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -48,6 +48,7 @@ export type { Intent, IntentOrderLike, BitcoinTradeData, + StellarTradeData, TronTradeData, BridgeControllerState, BridgeControllerAction, @@ -138,6 +139,7 @@ export { isSolanaChainId, isBitcoinChainId, isTronChainId, + isStellarChainId, isNonEvmChainId, getNativeAssetForChainId, getDefaultBridgeControllerState, @@ -164,6 +166,7 @@ export { export { extractTradeData, isBitcoinTrade, + isStellarTrade, isTronTrade, isEvmTxData, type Trade, diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index 0d37064a27..1af17f814e 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -45,6 +45,7 @@ import type { StepSchema, TokenFeatureSchema, QuoteStreamCompleteSchema, + StellarTradeDataSchema, TronTradeDataSchema, TxDataSchema, } from './utils/validators'; @@ -283,13 +284,15 @@ export type IntentOrderLike = Intent['order']; export type BitcoinTradeData = Infer; export type TronTradeData = Infer; + +export type StellarTradeData = Infer; /** * This is the type for the quote response from the bridge-api * TxDataType can be overriden to be a string when the quote is non-evm * ApprovalType can be overriden when you know the specific approval type (e.g., TxData for EVM-only contexts) */ export type QuoteResponse< - TxDataType = TxData | string | BitcoinTradeData | TronTradeData, + TxDataType = TxData | string | BitcoinTradeData | TronTradeData | StellarTradeData, ApprovalType = TxData | TronTradeData, > = Infer & { trade: TxDataType; @@ -317,6 +320,8 @@ export enum ChainId { LINEA = 59144, SOLANA = 1151111081099710, BTC = 20000000000001, + /** Internal bridge / token-list id for Stellar pubnet (Token API chain: stellar:pubnet). */ + 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..f24dea769a 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,24 @@ describe('Bridge utils', () => { }); }); + describe('isStellarChainId', () => { + it('returns true for Stellar CAIP-2 chain ids', () => { + expect(isStellarChainId(XlmScope.Pubnet)).toBe(true); + expect(isStellarChainId(XlmScope.Testnet)).toBe(true); + }); + + it('returns true for internal Stellar bridge chain id', () => { + expect(isStellarChainId(ChainId.STELLAR)).toBe(true); + expect(isStellarChainId(String(ChainId.STELLAR))).toBe(true); + }); + + it('returns false for other chainIds', () => { + expect(isStellarChainId(SolScope.Mainnet)).toBe(false); + expect(isStellarChainId('0x1')).toBe(false); + expect(isStellarChainId(1)).toBe(false); + }); + }); + describe('isNonEvmChainId', () => { it('returns true for Solana chainIds', () => { expect(isNonEvmChainId(ChainId.SOLANA)).toBe(true); @@ -198,6 +217,12 @@ describe('Bridge utils', () => { expect(isNonEvmChainId('20000000000001')).toBe(true); }); + it('returns true for Stellar chainIds', () => { + expect(isNonEvmChainId(XlmScope.Pubnet)).toBe(true); + expect(isNonEvmChainId(XlmScope.Testnet)).toBe(true); + expect(isNonEvmChainId(ChainId.STELLAR)).toBe(true); + }); + it('returns false for EVM chainIds', () => { expect(isNonEvmChainId('0x1')).toBe(false); expect(isNonEvmChainId(1)).toBe(false); @@ -268,6 +293,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: ChainId.STELLAR, + 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..9eea963a7f 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'; @@ -230,9 +230,27 @@ export const isTronChainId = (chainId: Hex | number | CaipChainId | string) => { return chainId.toString() === ChainId.TRON.toString(); }; +/** + * Checks whether the chainId matches Stellar pubnet or testnet (CAIP-2). + * + * @param chainId - The chainId to check + * @returns Whether the chainId is Stellar + */ +export const isStellarChainId = ( + chainId: Hex | number | CaipChainId | string, +): boolean => { + if (isCaipChainId(chainId)) { + return ( + chainId === XlmScope.Pubnet.toString() || + chainId === XlmScope.Testnet.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, Tron, and Stellar * * @param chainId - The chain ID to check * @returns True if the chain is a supported non-EVM chain, false otherwise @@ -243,7 +261,8 @@ export const isNonEvmChainId = ( return ( isSolanaChainId(chainId) || isBitcoinChainId(chainId) || - isTronChainId(chainId) + isTronChainId(chainId) || + isStellarChainId(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..3f91b91bfc 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,12 @@ describe('CAIP Formatters', () => { expect(formatChainIdToCaip(TrxScope.Mainnet)).toBe(TrxScope.Mainnet); }); + it('should convert Stellar chainId to XlmScope', () => { + expect(formatChainIdToCaip(ChainId.STELLAR)).toBe(XlmScope.Pubnet); + expect(formatChainIdToCaip(XlmScope.Pubnet)).toBe(XlmScope.Pubnet); + expect(formatChainIdToCaip(XlmScope.Testnet)).toBe(XlmScope.Testnet); + }); + it('should convert number to CAIP format', () => { expect(formatChainIdToCaip(1)).toBe('eip155:1'); }); @@ -68,6 +74,12 @@ describe('CAIP Formatters', () => { expect(formatChainIdToDec(TrxScope.Mainnet)).toBe(ChainId.TRON); }); + it('should handle Stellar mainnet', () => { + expect(formatChainIdToDec(XlmScope.Pubnet)).toBe(ChainId.STELLAR); + expect(formatChainIdToDec(XlmScope.Testnet)).toBe(ChainId.STELLAR); + expect(formatChainIdToDec(ChainId.STELLAR)).toBe(ChainId.STELLAR); + }); + it('should parse CAIP chainId to decimal', () => { expect(formatChainIdToDec('eip155:1')).toBe(1); }); diff --git a/packages/bridge-controller/src/utils/caip-formatters.ts b/packages/bridge-controller/src/utils/caip-formatters.ts index 450be976b0..eec21f2c74 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,6 +25,7 @@ import { isBitcoinChainId, isNativeAddress, isSolanaChainId, + isStellarChainId, isTronChainId, } from './bridge'; @@ -52,6 +53,12 @@ export const formatChainIdToCaip = ( if (isTronChainId(chainId)) { return TrxScope.Mainnet; } + if (isStellarChainId(chainId)) { + if (chainId === XlmScope.Testnet) { + return XlmScope.Testnet; + } + return XlmScope.Pubnet; + } return toEvmCaipChainId(numberToHex(Number(chainId))); }; @@ -76,6 +83,9 @@ export const formatChainIdToDec = ( if (chainId === TrxScope.Mainnet) { return ChainId.TRON; } + if (isStellarChainId(chainId)) { + return ChainId.STELLAR; + } if (isCaipChainId(chainId)) { return Number(chainId.split(':').at(-1)); } diff --git a/packages/bridge-controller/src/utils/trade-utils.test.ts b/packages/bridge-controller/src/utils/trade-utils.test.ts index 0d4a4cf74f..d238910208 100644 --- a/packages/bridge-controller/src/utils/trade-utils.test.ts +++ b/packages/bridge-controller/src/utils/trade-utils.test.ts @@ -3,6 +3,7 @@ import { extractTradeData, isEvmTxData, isBitcoinTrade, + isStellarTrade, isTronTrade, } from './trade-utils'; import type { Trade } from './trade-utils'; @@ -145,12 +146,50 @@ describe('Trade utils', () => { }); }); + describe('isStellarTrade', () => { + it('returns true for xdrBase64 object', () => { + expect( + isStellarTrade({ xdrBase64: 'AAAABg==' } as unknown as Trade), + ).toBe(true); + }); + + it('returns true for xdr object', () => { + expect(isStellarTrade({ xdr: 'AAAABg==' } as unknown as Trade)).toBe( + true, + ); + }); + + it('returns false for Tron trade', () => { + expect( + isStellarTrade({ + raw_data_hex: 'ab', + } as unknown as Trade), + ).toBe(false); + }); + }); + describe('extractTradeData', () => { it('returns string as-is for Solana trades', () => { const solanaTrade = 'base64EncodedSolanaTransaction'; expect(extractTradeData(solanaTrade)).toBe(solanaTrade); }); + it('returns xdrBase64 for Stellar trade object', () => { + expect( + extractTradeData({ + xdrBase64: 'stellarXdrPayload', + } as unknown as Trade), + ).toBe('stellarXdrPayload'); + }); + + it('returns xdr for Stellar trade object with xdr key', () => { + expect( + extractTradeData({ + xdr: 'stellarXdrAlt', + } as unknown as Trade), + ).toBe('stellarXdrAlt'); + }); + it('extracts data property from EVM TxData object', () => { const evmTxData: TxData = { chainId: 1, diff --git a/packages/bridge-controller/src/utils/trade-utils.ts b/packages/bridge-controller/src/utils/trade-utils.ts index 0e78b063da..568b115236 100644 --- a/packages/bridge-controller/src/utils/trade-utils.ts +++ b/packages/bridge-controller/src/utils/trade-utils.ts @@ -1,7 +1,17 @@ -import type { BitcoinTradeData, TronTradeData, TxData } from '../types'; +import type { + BitcoinTradeData, + StellarTradeData, + TronTradeData, + TxData, +} from '../types'; -// Union type representing all possible trade formats (EVM, Solana, Bitcoin, Tron) -export type Trade = TxData | string | BitcoinTradeData | TronTradeData; +// Union type representing all possible trade formats (EVM, Solana, Bitcoin, Tron, Stellar) +export type Trade = + | TxData + | string + | BitcoinTradeData + | TronTradeData + | StellarTradeData; /** * Type guard to check if a trade is an EVM TxData object @@ -41,6 +51,25 @@ export const isTronTrade = (trade: Trade): trade is TronTradeData => { return typeof trade === 'object' && trade !== null && 'raw_data_hex' in trade; }; +/** + * Type guard to check if a trade is a Stellar trade with XDR (base64) payload + */ +export const isStellarTrade = (trade: Trade): trade is StellarTradeData => { + if (typeof trade !== 'object' || trade === null) { + return false; + } + if ( + 'xdrBase64' in trade && + typeof (trade as { xdrBase64: unknown }).xdrBase64 === 'string' + ) { + return true; + } + if ('xdr' in trade && typeof (trade as { xdr: unknown }).xdr === 'string') { + return true; + } + return false; +}; + /** * Extracts the transaction data from different trade formats * @@ -59,6 +88,10 @@ export const extractTradeData = (trade: Trade): string => { return Buffer.from(trade.raw_data_hex, 'hex').toString('base64'); } + if (isStellarTrade(trade)) { + return 'xdrBase64' in trade ? trade.xdrBase64 : trade.xdr; + } + if (typeof trade === 'string') { // Solana txs - assuming already in correct format return trade; diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts index 73603b2ee9..893cb9c374 100644 --- a/packages/bridge-controller/src/utils/validators.ts +++ b/packages/bridge-controller/src/utils/validators.ts @@ -458,6 +458,14 @@ export const TronTradeDataSchema = type({ ), }); +/** + * Stellar bridge quote: unsigned transaction envelope as XDR (base64). + */ +export const StellarTradeDataSchema = union([ + type({ xdrBase64: string() }), + type({ xdr: string() }), +]); + export const QuoteResponseSchema = type({ quote: QuoteSchema, estimatedProcessingTimeInSeconds: number(), @@ -466,6 +474,7 @@ export const QuoteResponseSchema = type({ TxDataSchema, BitcoinTradeDataSchema, TronTradeDataSchema, + StellarTradeDataSchema, string(), ]), }); diff --git a/packages/chain-agnostic-permission/CHANGELOG.md b/packages/chain-agnostic-permission/CHANGELOG.md index a124e78c1b..f7b70087d1 100644 --- a/packages/chain-agnostic-permission/CHANGELOG.md +++ b/packages/chain-agnostic-permission/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `stellar` to CAIP-25 scope constants (RPC methods, notifications, session properties) ([#TODO](https://github.com/MetaMask/core/pull/TODO)) + ### Changed - Bump `@metamask/permission-controller` from `^12.2.1` to `^12.3.0` ([#8317](https://github.com/MetaMask/core/pull/8317)) diff --git a/packages/chain-agnostic-permission/src/scope/constants.test.ts b/packages/chain-agnostic-permission/src/scope/constants.test.ts index 9b3dafdc91..3af91b1d64 100644 --- a/packages/chain-agnostic-permission/src/scope/constants.test.ts +++ b/packages/chain-agnostic-permission/src/scope/constants.test.ts @@ -54,6 +54,7 @@ describe('KnownRpcMethods', () => { "eth_uninstallFilter", ], "solana": [], + "stellar": [], "tron": [], } `); @@ -66,6 +67,7 @@ describe('KnownSessionProperties', () => { { "Bip122AccountChangedNotifications": "bip122_accountChanged_notifications", "SolanaAccountChangedNotifications": "solana_accountChanged_notifications", + "StellarAccountChangedNotifications": "stellar_accountChanged_notifications", "TronAccountChangedNotifications": "tron_accountChanged_notifications", } `); @@ -77,6 +79,9 @@ describe('isKnownSessionPropertyValue', () => { expect( isKnownSessionPropertyValue('solana_accountChanged_notifications'), ).toBe(true); + expect( + isKnownSessionPropertyValue('stellar_accountChanged_notifications'), + ).toBe(true); expect( isKnownSessionPropertyValue('tron_accountChanged_notifications'), ).toBe(true); diff --git a/packages/chain-agnostic-permission/src/scope/constants.ts b/packages/chain-agnostic-permission/src/scope/constants.ts index bc31ebf963..41603532d4 100644 --- a/packages/chain-agnostic-permission/src/scope/constants.ts +++ b/packages/chain-agnostic-permission/src/scope/constants.ts @@ -17,6 +17,7 @@ export const CaipReferenceRegexes: Record = eip155: /^(0|[1-9][0-9]*)$/u, bip122: /.*/u, solana: /.*/u, + stellar: /.*/u, tron: /.*/u, }; @@ -67,6 +68,7 @@ export const KnownRpcMethods: Record = { eip155: Eip155Methods, bip122: [], solana: [], + stellar: [], tron: [], }; @@ -80,6 +82,7 @@ export const KnownWalletNamespaceRpcMethods: Record< eip155: WalletEip155Methods, bip122: [], solana: [], + stellar: [], tron: [], }; @@ -91,6 +94,7 @@ export const KnownNotifications: Record = eip155: ['eth_subscription'], bip122: [], solana: [], + stellar: [], tron: [], }; @@ -99,6 +103,7 @@ export const KnownNotifications: Record = */ export enum KnownSessionProperties { SolanaAccountChangedNotifications = 'solana_accountChanged_notifications', + StellarAccountChangedNotifications = 'stellar_accountChanged_notifications', TronAccountChangedNotifications = 'tron_accountChanged_notifications', Bip122AccountChangedNotifications = 'bip122_accountChanged_notifications', } diff --git a/packages/chain-agnostic-permission/src/scope/types.ts b/packages/chain-agnostic-permission/src/scope/types.ts index 24c2c036c2..2b7f052b3e 100644 --- a/packages/chain-agnostic-permission/src/scope/types.ts +++ b/packages/chain-agnostic-permission/src/scope/types.ts @@ -123,7 +123,8 @@ export type NonWalletKnownCaipNamespace = | KnownCaipNamespace.Eip155 | KnownCaipNamespace.Bip122 | KnownCaipNamespace.Solana - | KnownCaipNamespace.Tron; + | KnownCaipNamespace.Tron + | KnownCaipNamespace.Stellar; /** * Checks if a scope string is either a 'wallet' scope or a 'wallet:*' scope. diff --git a/packages/keyring-controller/src/KeyringController.ts b/packages/keyring-controller/src/KeyringController.ts index 415a912b9d..c6c81161e1 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -566,7 +566,7 @@ const hdKeyringV2Builder: KeyringV2Builder = Object.assign( const simpleKeyringV2Builder: KeyringV2Builder = Object.assign( (keyring: Keyring): KeyringV2 => - new SimpleKeyringV2({ + new (require('./SimpleKeyringV2').SimpleKeyringV2)({ legacyKeyring: keyring as SimpleKeyring, }), { type: KeyringTypes.simple as string }, diff --git a/packages/multichain-account-service/src/MultichainAccountService.test.ts b/packages/multichain-account-service/src/MultichainAccountService.test.ts index d8b3456d11..1d41e9bc90 100644 --- a/packages/multichain-account-service/src/MultichainAccountService.test.ts +++ b/packages/multichain-account-service/src/MultichainAccountService.test.ts @@ -10,6 +10,7 @@ import { EthAccountType, SolAccountType, TrxAccountType, + XlmAccountType, } from '@metamask/keyring-api'; import type { KeyringObject } from '@metamask/keyring-controller'; import type { EthKeyring } from '@metamask/keyring-internal-api'; @@ -37,6 +38,10 @@ import { TRX_ACCOUNT_PROVIDER_NAME, TrxAccountProvider, } from './providers/TrxAccountProvider'; +import { + XLM_ACCOUNT_PROVIDER_NAME, + XlmAccountProvider, +} from './providers/XlmAccountProvider'; import { SnapPlatformWatcher } from './snaps/SnapPlatformWatcher'; import type { RootMessenger, MockAccountProvider } from './tests'; import { @@ -91,6 +96,12 @@ jest.mock('./providers/TrxAccountProvider', () => { TrxAccountProvider: jest.fn(), }; }); +jest.mock('./providers/XlmAccountProvider', () => { + return { + ...jest.requireActual('./providers/XlmAccountProvider'), + XlmAccountProvider: jest.fn(), + }; +}); type Mocks = { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -124,6 +135,8 @@ type Mocks = { BtcAccountProvider: MockAccountProvider; // eslint-disable-next-line @typescript-eslint/naming-convention TrxAccountProvider: MockAccountProvider; + // eslint-disable-next-line @typescript-eslint/naming-convention + XlmAccountProvider: MockAccountProvider; }; type Spies = { @@ -172,6 +185,11 @@ function mockAccountProvider( mocks.isAccountCompatible?.mockImplementation( (account: KeyringAccount) => account.type === TrxAccountType.Eoa, ); + } else if (providerClass === (XlmAccountProvider as unknown)) { + mocks.getName.mockReturnValue(XLM_ACCOUNT_PROVIDER_NAME); + mocks.isAccountCompatible?.mockImplementation( + (account: KeyringAccount) => account.type === XlmAccountType.Account, + ); } } @@ -218,6 +236,7 @@ async function setup({ SolAccountProvider: makeMockAccountProvider(), BtcAccountProvider: makeMockAccountProvider(), TrxAccountProvider: makeMockAccountProvider(), + XlmAccountProvider: makeMockAccountProvider(), }; const spies: Spies = { @@ -292,6 +311,7 @@ async function setup({ SolAccountProvider.NAME = SOL_ACCOUNT_PROVIDER_NAME; BtcAccountProvider.NAME = BTC_ACCOUNT_PROVIDER_NAME; TrxAccountProvider.NAME = TRX_ACCOUNT_PROVIDER_NAME; + XlmAccountProvider.NAME = XLM_ACCOUNT_PROVIDER_NAME; mockAccountProvider( EvmAccountProvider, @@ -321,6 +341,13 @@ async function setup({ 3, TrxAccountType.Eoa, ); + mockAccountProvider( + XlmAccountProvider, + mocks.XlmAccountProvider, + accounts, + 4, + XlmAccountType.Account, + ); } const messenger = getMultichainAccountServiceMessenger(rootMessenger); diff --git a/packages/multichain-account-service/src/MultichainAccountService.ts b/packages/multichain-account-service/src/MultichainAccountService.ts index 283e747c46..032477bc21 100644 --- a/packages/multichain-account-service/src/MultichainAccountService.ts +++ b/packages/multichain-account-service/src/MultichainAccountService.ts @@ -25,10 +25,13 @@ import { EVM_ACCOUNT_PROVIDER_NAME, BtcAccountProviderConfig, TrxAccountProviderConfig, + XlmAccountProviderConfig, BTC_ACCOUNT_PROVIDER_NAME, TRX_ACCOUNT_PROVIDER_NAME, + XLM_ACCOUNT_PROVIDER_NAME, BtcAccountProvider, TrxAccountProvider, + XlmAccountProvider, } from './providers'; import { AccountProviderWrapper, @@ -59,6 +62,7 @@ export type MultichainAccountServiceOptions = { [SOL_ACCOUNT_PROVIDER_NAME]?: SolAccountProviderConfig; [BTC_ACCOUNT_PROVIDER_NAME]?: BtcAccountProviderConfig; [TRX_ACCOUNT_PROVIDER_NAME]?: TrxAccountProviderConfig; + [XLM_ACCOUNT_PROVIDER_NAME]?: XlmAccountProviderConfig; }; config?: MultichainAccountServiceConfig; /** @@ -206,6 +210,14 @@ export class MultichainAccountService { trace, ), ), + new AccountProviderWrapper( + this.#messenger, + new XlmAccountProvider( + this.#messenger, + providerConfigs?.[XLM_ACCOUNT_PROVIDER_NAME], + trace, + ), + ), // Custom account providers that can be provided by the MetaMask client. ...providers, ]; diff --git a/packages/multichain-account-service/src/MultichainAccountWallet.test.ts b/packages/multichain-account-service/src/MultichainAccountWallet.test.ts index 705ff8a9cd..57d3c09788 100644 --- a/packages/multichain-account-service/src/MultichainAccountWallet.test.ts +++ b/packages/multichain-account-service/src/MultichainAccountWallet.test.ts @@ -300,7 +300,7 @@ describe('MultichainAccountWallet', () => { ).rejects.toThrow('Unable to create accounts'); expect(captureExceptionSpy).toHaveBeenCalledWith( new Error( - 'Unable to create some accounts with provider "Mocked Provider 0"', + 'Unable to create accounts with provider "Mocked Provider 0" (group indices 1–1)', ), ); expect(captureExceptionSpy.mock.lastCall[0]).toHaveProperty( @@ -609,7 +609,7 @@ describe('MultichainAccountWallet', () => { ).rejects.toThrow(`Bad range, to (${badIndex}) must be >= 0`); }); - it('captures an error with batch mode message when EVM provider fails', async () => { + it('captures an error with group index range message when EVM provider fails', async () => { const { wallet, providers, messenger } = setup({ accounts: [[]], }); @@ -626,7 +626,7 @@ describe('MultichainAccountWallet', () => { expect(captureExceptionSpy).toHaveBeenCalledWith( new Error( - 'Unable to create some accounts (batch) with provider "Mocked Provider 0"', + 'Unable to create accounts with provider "Mocked Provider 0" (group indices 0–2)', ), ); expect(captureExceptionSpy.mock.lastCall[0]).toHaveProperty( diff --git a/packages/multichain-account-service/src/MultichainAccountWallet.ts b/packages/multichain-account-service/src/MultichainAccountWallet.ts index d38114eefe..0aca148d89 100644 --- a/packages/multichain-account-service/src/MultichainAccountWallet.ts +++ b/packages/multichain-account-service/src/MultichainAccountWallet.ts @@ -237,8 +237,6 @@ export class MultichainAccountWallet< from: number, to: number, ): Promise[]> { - const isBatching = to > from; - try { return await provider.createAccounts({ type: AccountCreationType.Bip44DeriveIndexRange, @@ -251,12 +249,12 @@ export class MultichainAccountWallet< } catch (error) { reportError( this.#messenger, - `Unable to create ${isBatching ? 'some accounts (batch)' : 'some accounts'} with provider "${provider.getName()}"`, + `Unable to create accounts with provider "${provider.getName()}" (group indices ${from}–${to})`, error, { range: { from, to }, provider: provider.getName(), - isBatching, + spansMultipleGroupIndices: to > from, }, ); throw error; diff --git a/packages/multichain-account-service/src/index.ts b/packages/multichain-account-service/src/index.ts index 802cbfbc97..440af75b7d 100644 --- a/packages/multichain-account-service/src/index.ts +++ b/packages/multichain-account-service/src/index.ts @@ -35,6 +35,8 @@ export { BtcAccountProvider, TRX_ACCOUNT_PROVIDER_NAME, TrxAccountProvider, + XLM_ACCOUNT_PROVIDER_NAME, + XlmAccountProvider, } from './providers'; export { MultichainAccountWallet } from './MultichainAccountWallet'; export { MultichainAccountGroup } from './MultichainAccountGroup'; diff --git a/packages/multichain-account-service/src/providers/EvmAccountProvider.ts b/packages/multichain-account-service/src/providers/EvmAccountProvider.ts index 147fe6a768..47f2031d36 100644 --- a/packages/multichain-account-service/src/providers/EvmAccountProvider.ts +++ b/packages/multichain-account-service/src/providers/EvmAccountProvider.ts @@ -8,6 +8,7 @@ import type { EntropySourceId, KeyringAccount, } from '@metamask/keyring-api'; +import type { KeyringCapabilities } from '@metamask/keyring-api/v2'; import { AccountCreationType, assertCreateAccountOptionIsSupported, diff --git a/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts b/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts index af33672f58..0ed4bd50ab 100644 --- a/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts +++ b/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts @@ -13,8 +13,8 @@ import type { CreateAccountOptions, DeleteAccountRequest, GetAccountRequest, - KeyringCapabilities, } from '@metamask/keyring-api'; +import type { KeyringCapabilities } from '@metamask/keyring-api/v2'; import type { EntropySourceId, KeyringAccount } from '@metamask/keyring-api'; import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { JsonRpcRequest, SnapId } from '@metamask/snaps-sdk'; diff --git a/packages/multichain-account-service/src/providers/XlmAccountProvider.test.ts b/packages/multichain-account-service/src/providers/XlmAccountProvider.test.ts new file mode 100644 index 0000000000..0c2d2942ef --- /dev/null +++ b/packages/multichain-account-service/src/providers/XlmAccountProvider.test.ts @@ -0,0 +1,286 @@ +import { isBip44Account } from '@metamask/account-api'; +import type { SnapKeyring } from '@metamask/eth-snap-keyring'; +import { AccountCreationType } from '@metamask/keyring-api'; +import type { KeyringMetadata } from '@metamask/keyring-controller'; +import type { + EthKeyring, + InternalAccount, +} from '@metamask/keyring-internal-api'; +import { SnapControllerState } from '@metamask/snaps-controllers'; +import type { Json } from '@metamask/utils'; +import deepmerge from 'deepmerge'; + +import { + getMultichainAccountServiceMessenger, + getRootMessenger, + MOCK_HD_KEYRING_2, + MOCK_XLM_ACCOUNT_1, + MockAccountBuilder, + toGroupIndexRangeArray, +} from '../tests'; +import type { RootMessenger, DeepPartial } from '../tests'; +import { AccountProviderWrapper } from './AccountProviderWrapper'; +import type { SnapAccountProviderConfig } from './SnapAccountProvider'; +import { + XLM_ACCOUNT_PROVIDER_DEFAULT_CONFIG, + XLM_ACCOUNT_PROVIDER_NAME, + XlmAccountProvider, +} from './XlmAccountProvider'; + +function asConfig( + partial: DeepPartial, +): SnapAccountProviderConfig { + return deepmerge( + XLM_ACCOUNT_PROVIDER_DEFAULT_CONFIG, + partial, + ) as SnapAccountProviderConfig; +} + +class MockStellarKeyring { + readonly type = 'MockStellarKeyring'; + + readonly metadata: KeyringMetadata = { + id: 'mock-stellar-keyring-id', + name: '', + }; + + readonly accounts: InternalAccount[]; + + constructor(accounts: InternalAccount[]) { + this.accounts = accounts; + } + + createAccount: SnapKeyring['createAccount'] = jest + .fn() + .mockImplementation((_, options: Record) => { + const { index } = options; + if (typeof index === 'number') { + const found = this.accounts.find( + (account) => + isBip44Account(account) && + account.options.entropy.groupIndex === index, + ); + + if (found) { + return found; + } + } + + const account = MockAccountBuilder.from(MOCK_XLM_ACCOUNT_1) + .withUuid() + .withAddressSuffix(`${this.accounts.length}`) + .withGroupIndex(typeof index === 'number' ? index : this.accounts.length) + .get(); + this.accounts.push(account); + + return account; + }); + + createAccounts: SnapKeyring['createAccounts'] = jest + .fn() + .mockImplementation((_, options) => { + const groupIndices = + options.type === 'bip44:derive-index' + ? [options.groupIndex] + : toGroupIndexRangeArray(options.range); + + return groupIndices.map((groupIndex) => { + const found = this.accounts.find( + (account) => + isBip44Account(account) && + account.options.entropy.groupIndex === groupIndex, + ); + + if (found) { + return found; + } + + const account = MockAccountBuilder.from(MOCK_XLM_ACCOUNT_1) + .withUuid() + .withAddressSuffix(`${groupIndex}`) + .withGroupIndex(groupIndex) + .get(); + this.accounts.push(account); + return account; + }); + }); +} + +class MockXlmAccountProvider extends XlmAccountProvider { + override async ensureCanUseSnapPlatform(): Promise { + // Override to avoid waiting during tests. + } +} + +function setup({ + messenger = getRootMessenger(), + accounts = [], + config, +}: { + messenger?: RootMessenger; + accounts?: InternalAccount[]; + config?: SnapAccountProviderConfig; +} = {}): { + provider: AccountProviderWrapper; + messenger: RootMessenger; + keyring: MockStellarKeyring; + mocks: { + handleRequest: jest.Mock; + keyring: { + createAccount: jest.Mock; + createAccounts: jest.Mock; + }; + trace: jest.Mock; + }; +} { + const keyring = new MockStellarKeyring(accounts); + + messenger.registerActionHandler( + 'AccountsController:getAccounts', + () => accounts, + ); + + messenger.registerActionHandler( + 'SnapController:getState', + () => ({ isReady: true }) as SnapControllerState, + ); + + messenger.registerActionHandler( + 'AccountsController:listMultichainAccounts', + () => accounts, + ); + + const mockGetAccount = jest.fn().mockImplementation((id) => { + return keyring.accounts.find((account) => account.id === id); + }); + messenger.registerActionHandler( + 'AccountsController:getAccount', + mockGetAccount, + ); + + const mockHandleRequest = jest + .fn() + .mockImplementation((address: string) => + keyring.accounts.find((account) => account.address === address), + ); + + const mockTrace = jest.fn().mockImplementation(async (_request, fn) => { + return await fn(); + }); + + messenger.registerActionHandler( + 'SnapController:handleRequest', + mockHandleRequest, + ); + + messenger.registerActionHandler( + 'KeyringController:withKeyring', + async (_, operation) => + operation({ + keyring: keyring as unknown as EthKeyring, + metadata: keyring.metadata, + }), + ); + + const multichainMessenger = getMultichainAccountServiceMessenger(messenger); + const xlmProvider = new MockXlmAccountProvider( + multichainMessenger, + config, + mockTrace, + ); + const accountIds = accounts.map((account) => account.id); + xlmProvider.init(accountIds); + const provider = new AccountProviderWrapper(multichainMessenger, xlmProvider); + + return { + provider, + messenger, + keyring, + mocks: { + handleRequest: mockHandleRequest, + keyring: { + createAccount: keyring.createAccount as jest.Mock, + createAccounts: keyring.createAccounts as jest.Mock, + }, + trace: mockTrace, + }, + }; +} + +describe('XlmAccountProvider', () => { + it('getName returns Stellar', () => { + const { provider } = setup({ accounts: [] }); + expect(provider.getName()).toBe(XLM_ACCOUNT_PROVIDER_NAME); + }); + + describe('v1', () => { + it('uses createAccount when batching is disabled', async () => { + const accounts = [MOCK_XLM_ACCOUNT_1]; + const { provider, mocks } = setup({ + accounts, + config: asConfig({ createAccounts: { batched: false } }), + }); + + await provider.createAccounts({ + type: AccountCreationType.Bip44DeriveIndex, + entropySource: MOCK_HD_KEYRING_2.metadata.id, + groupIndex: accounts.length, + }); + + expect(mocks.keyring.createAccount).toHaveBeenCalled(); + expect(mocks.keyring.createAccounts).not.toHaveBeenCalled(); + }); + }); + + describe('v2 - batched', () => { + it('creates one account via createAccounts', async () => { + const accounts = [MOCK_XLM_ACCOUNT_1]; + const { provider, mocks } = setup({ + accounts, + config: asConfig({ createAccounts: { batched: true } }), + }); + + const newGroupIndex = accounts.length; + const newAccounts = await provider.createAccounts({ + type: AccountCreationType.Bip44DeriveIndex, + entropySource: MOCK_HD_KEYRING_2.metadata.id, + groupIndex: newGroupIndex, + }); + + expect(newAccounts).toHaveLength(1); + expect(mocks.keyring.createAccounts).toHaveBeenCalledWith( + XlmAccountProvider.XLM_SNAP_ID, + { + type: AccountCreationType.Bip44DeriveIndex, + entropySource: MOCK_HD_KEYRING_2.metadata.id, + groupIndex: newGroupIndex, + }, + ); + expect(mocks.keyring.createAccount).not.toHaveBeenCalled(); + }); + + it('creates multiple accounts using Bip44DeriveIndexRange', async () => { + const accounts = [MOCK_XLM_ACCOUNT_1]; + const { provider, mocks } = setup({ + accounts, + config: asConfig({ createAccounts: { batched: true } }), + }); + + const from = 1; + const newAccounts = await provider.createAccounts({ + type: AccountCreationType.Bip44DeriveIndexRange, + entropySource: MOCK_HD_KEYRING_2.metadata.id, + range: { from, to: 3 }, + }); + + expect(newAccounts).toHaveLength(3); + expect(mocks.keyring.createAccounts).toHaveBeenCalledTimes(1); + expect(mocks.keyring.createAccount).not.toHaveBeenCalled(); + + for (const [index, account] of newAccounts.entries()) { + expect(isBip44Account(account)).toBe(true); + expect(account.options.entropy.groupIndex).toBe(from + index); + } + }); + }); +}); diff --git a/packages/multichain-account-service/src/providers/XlmAccountProvider.ts b/packages/multichain-account-service/src/providers/XlmAccountProvider.ts new file mode 100644 index 0000000000..55e109d5a3 --- /dev/null +++ b/packages/multichain-account-service/src/providers/XlmAccountProvider.ts @@ -0,0 +1,145 @@ +import type { Bip44Account } from '@metamask/account-api'; +import type { TraceCallback } from '@metamask/controller-utils'; +import type { + EntropySourceId, + KeyringAccount, +} from '@metamask/keyring-api'; +import type { KeyringCapabilities } from '@metamask/keyring-api/v2'; +import { + AccountCreationType, + XlmAccountType, + XlmScope, +} from '@metamask/keyring-api'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; +import type { SnapId } from '@metamask/snaps-sdk'; + +import { SnapAccountProvider } from './SnapAccountProvider'; +import type { + RestrictedSnapKeyring, + SnapAccountProviderConfig, +} from './SnapAccountProvider'; +import { withRetry, withTimeout } from './utils'; +import { traceFallback } from '../analytics'; +import { TraceName } from '../analytics/traces'; +import type { MultichainAccountServiceMessenger } from '../types'; + +export type XlmAccountProviderConfig = SnapAccountProviderConfig; + +export const XLM_ACCOUNT_PROVIDER_NAME = 'Stellar'; + +export const XLM_ACCOUNT_PROVIDER_DEFAULT_CONFIG: XlmAccountProviderConfig = { + maxConcurrency: 3, + discovery: { + enabled: true, + timeoutMs: 2000, + maxAttempts: 3, + backOffMs: 1000, + }, + createAccounts: { + batched: true, + timeoutMs: 10000, + }, + resyncAccounts: { + autoRemoveExtraSnapAccounts: true, + }, +}; + +export class XlmAccountProvider extends SnapAccountProvider { + static NAME = XLM_ACCOUNT_PROVIDER_NAME; + + static XLM_SNAP_ID = 'npm:@metamask/stellar-wallet-snap' as SnapId; + + readonly capabilities: KeyringCapabilities = { + scopes: [XlmScope.Pubnet, XlmScope.Testnet], + bip44: { + deriveIndex: true, + deriveIndexRange: true, + }, + }; + + constructor( + messenger: MultichainAccountServiceMessenger, + config: XlmAccountProviderConfig = XLM_ACCOUNT_PROVIDER_DEFAULT_CONFIG, + trace: TraceCallback = traceFallback, + ) { + super(XlmAccountProvider.XLM_SNAP_ID, messenger, config, trace); + } + + getName(): string { + return XlmAccountProvider.NAME; + } + + isAccountCompatible(account: Bip44Account): boolean { + return ( + account.type === XlmAccountType.Account && + account.metadata.keyring.type === (KeyringTypes.snap as string) + ); + } + + protected override createAccountV1( + keyring: RestrictedSnapKeyring, + { + entropySource, + groupIndex, + }: { entropySource: EntropySourceId; groupIndex: number }, + ): Promise { + return keyring.createAccount({ + entropySource, + index: groupIndex, + addressType: XlmAccountType.Account, + scope: XlmScope.Pubnet, + }); + } + + async discoverAccounts({ + entropySource, + groupIndex, + }: { + entropySource: EntropySourceId; + groupIndex: number; + }): Promise[]> { + return this.withSnap(async ({ client, keyring }) => { + return await super.trace( + { + name: TraceName.SnapDiscoverAccounts, + data: { + provider: this.getName(), + }, + }, + async () => { + if (!this.config.discovery.enabled) { + return []; + } + + const discoveredAccounts = await withRetry( + () => + withTimeout( + () => + client.discoverAccounts( + [XlmScope.Pubnet], + entropySource, + groupIndex, + ), + this.config.discovery.timeoutMs, + ), + { + maxAttempts: this.config.discovery.maxAttempts, + backOffMs: this.config.discovery.backOffMs, + }, + ); + + if (!discoveredAccounts.length) { + return []; + } + + return await this.createBip44Accounts(keyring, { + type: AccountCreationType.Bip44DeriveIndex, + entropySource, + groupIndex, + }); + }, + ); + }); + } +} diff --git a/packages/multichain-account-service/src/providers/index.ts b/packages/multichain-account-service/src/providers/index.ts index 0504f99fea..51f20af248 100644 --- a/packages/multichain-account-service/src/providers/index.ts +++ b/packages/multichain-account-service/src/providers/index.ts @@ -10,3 +10,4 @@ export * from './SolAccountProvider'; export * from './EvmAccountProvider'; export * from './BtcAccountProvider'; export * from './TrxAccountProvider'; +export * from './XlmAccountProvider'; diff --git a/packages/multichain-account-service/src/tests/accounts.ts b/packages/multichain-account-service/src/tests/accounts.ts index 4479a3000e..a403d8d37e 100644 --- a/packages/multichain-account-service/src/tests/accounts.ts +++ b/packages/multichain-account-service/src/tests/accounts.ts @@ -19,6 +19,9 @@ import { TrxAccountType, TrxMethod, TrxScope, + XlmAccountType, + XlmMethod, + XlmScope, } from '@metamask/keyring-api'; import { KeyringTypes } from '@metamask/keyring-controller'; import type { InternalAccount } from '@metamask/keyring-internal-api'; @@ -149,6 +152,31 @@ export const MOCK_SOL_ACCOUNT_1: Bip44Account = { }, }; +const XLM_METHODS = Object.values(XlmMethod); + +export const MOCK_XLM_ACCOUNT_1: Bip44Account = { + id: 'mock-snap-id-1', + address: `G${'A'.repeat(55)}`, + options: { + entropy: { + type: KeyringAccountEntropyTypeOption.Mnemonic, + id: MOCK_HD_KEYRING_2.metadata.id, + groupIndex: 0, + derivationPath: '', + }, + }, + methods: XLM_METHODS, + type: XlmAccountType.Account, + scopes: [XlmScope.Pubnet, XlmScope.Testnet], + metadata: { + name: 'Stellar Account 1', + keyring: { type: KeyringTypes.snap }, + snap: MOCK_SNAP_1, + importTime: 0, + lastSelected: 0, + }, +}; + export const MOCK_TRX_ACCOUNT_1: Bip44Account = { id: 'mock-snap-id-1', address: 'aabbccdd', @@ -185,6 +213,12 @@ export const MOCK_TRX_DISCOVERED_ACCOUNT_1: DiscoveredAccount = { derivationPath: `m/44'/195'/0'/0'`, }; +export const MOCK_XLM_DISCOVERED_ACCOUNT_1: DiscoveredAccount = { + type: 'bip44', + scopes: [XlmScope.Pubnet], + derivationPath: `m/44'/148'/0'`, +}; + export const MOCK_BTC_P2TR_DISCOVERED_ACCOUNT_1: DiscoveredAccount = { type: 'bip44', scopes: [BtcScope.Mainnet], diff --git a/packages/multichain-account-service/src/tests/providers.ts b/packages/multichain-account-service/src/tests/providers.ts index 2c92fca4ba..e88c66256d 100644 --- a/packages/multichain-account-service/src/tests/providers.ts +++ b/packages/multichain-account-service/src/tests/providers.ts @@ -1,5 +1,11 @@ import type { Bip44Account } from '@metamask/account-api'; -import { BtcScope, EthScope, SolScope, TrxScope } from '@metamask/keyring-api'; +import { + BtcScope, + EthScope, + SolScope, + TrxScope, + XlmScope, +} from '@metamask/keyring-api'; import type { KeyringAccount } from '@metamask/keyring-api'; import type { KeyringCapabilities } from '@metamask/keyring-api/v2'; @@ -37,6 +43,7 @@ export function makeMockAccountProvider( SolScope.Testnet, BtcScope.Testnet, TrxScope.Shasta, + XlmScope.Testnet, EthScope.Eoa, ], bip44: { deriveIndex: true }, diff --git a/packages/multichain-network-controller/src/MultichainNetworkController/MultichainNetworkController.test.ts b/packages/multichain-network-controller/src/MultichainNetworkController/MultichainNetworkController.test.ts index 240743acec..fefd952ca6 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController/MultichainNetworkController.test.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController/MultichainNetworkController.test.ts @@ -778,6 +778,18 @@ describe('MultichainNetworkController', () => { "name": "Solana Devnet", "nativeCurrency": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501", }, + "stellar:pubnet": { + "chainId": "stellar:pubnet", + "isEvm": false, + "name": "Stellar", + "nativeCurrency": "stellar:pubnet/slip44:148", + }, + "stellar:testnet": { + "chainId": "stellar:testnet", + "isEvm": false, + "name": "Stellar Testnet", + "nativeCurrency": "stellar:testnet/slip44:148", + }, "tron:2494104990": { "chainId": "tron:2494104990", "isEvm": false, @@ -864,6 +876,18 @@ describe('MultichainNetworkController', () => { "name": "Solana Devnet", "nativeCurrency": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501", }, + "stellar:pubnet": { + "chainId": "stellar:pubnet", + "isEvm": false, + "name": "Stellar", + "nativeCurrency": "stellar:pubnet/slip44:148", + }, + "stellar:testnet": { + "chainId": "stellar:testnet", + "isEvm": false, + "name": "Stellar Testnet", + "nativeCurrency": "stellar:testnet/slip44:148", + }, "tron:2494104990": { "chainId": "tron:2494104990", "isEvm": false, @@ -950,6 +974,18 @@ describe('MultichainNetworkController', () => { "name": "Solana Devnet", "nativeCurrency": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501", }, + "stellar:pubnet": { + "chainId": "stellar:pubnet", + "isEvm": false, + "name": "Stellar", + "nativeCurrency": "stellar:pubnet/slip44:148", + }, + "stellar:testnet": { + "chainId": "stellar:testnet", + "isEvm": false, + "name": "Stellar Testnet", + "nativeCurrency": "stellar:testnet/slip44:148", + }, "tron:2494104990": { "chainId": "tron:2494104990", "isEvm": false, @@ -1036,6 +1072,18 @@ describe('MultichainNetworkController', () => { "name": "Solana Devnet", "nativeCurrency": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501", }, + "stellar:pubnet": { + "chainId": "stellar:pubnet", + "isEvm": false, + "name": "Stellar", + "nativeCurrency": "stellar:pubnet/slip44:148", + }, + "stellar:testnet": { + "chainId": "stellar:testnet", + "isEvm": false, + "name": "Stellar Testnet", + "nativeCurrency": "stellar:testnet/slip44:148", + }, "tron:2494104990": { "chainId": "tron:2494104990", "isEvm": false, diff --git a/packages/multichain-network-controller/src/constants.ts b/packages/multichain-network-controller/src/constants.ts index 262e79aa6d..8fe8205881 100644 --- a/packages/multichain-network-controller/src/constants.ts +++ b/packages/multichain-network-controller/src/constants.ts @@ -1,5 +1,5 @@ import type { StateMetadata } from '@metamask/base-controller'; -import { BtcScope, SolScope, TrxScope } from '@metamask/keyring-api'; +import { BtcScope, SolScope, TrxScope, XlmScope } from '@metamask/keyring-api'; import type { CaipChainId } from '@metamask/keyring-api'; import { NetworkStatus } from '@metamask/network-controller'; @@ -21,6 +21,8 @@ export const SOL_DEVNET_NATIVE_ASSET = `${SolScope.Devnet}/slip44:501`; export const TRX_NATIVE_ASSET = `${TrxScope.Mainnet}/slip44:195`; export const TRX_NILE_NATIVE_ASSET = `${TrxScope.Nile}/slip44:195`; export const TRX_SHASTA_NATIVE_ASSET = `${TrxScope.Shasta}/slip44:195`; +export const XLM_NATIVE_ASSET = `${XlmScope.Pubnet}/slip44:148`; +export const XLM_TESTNET_NATIVE_ASSET = `${XlmScope.Testnet}/slip44:148`; /** * Supported networks by the MultichainNetworkController @@ -95,6 +97,18 @@ export const AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS: Record< nativeCurrency: TRX_SHASTA_NATIVE_ASSET, isEvm: false, }, + [XlmScope.Pubnet]: { + chainId: XlmScope.Pubnet, + name: 'Stellar', + nativeCurrency: XLM_NATIVE_ASSET, + isEvm: false, + }, + [XlmScope.Testnet]: { + chainId: XlmScope.Testnet, + name: 'Stellar Testnet', + nativeCurrency: XLM_TESTNET_NATIVE_ASSET, + isEvm: false, + }, }; /** @@ -111,6 +125,7 @@ export const NON_EVM_TESTNET_IDS: CaipChainId[] = [ SolScope.Devnet, TrxScope.Nile, TrxScope.Shasta, + XlmScope.Testnet, ]; /** @@ -129,6 +144,10 @@ export const NETWORKS_METADATA: Record = { features: [], status: NetworkStatus.Available, }, + [XlmScope.Pubnet]: { + features: [], + status: NetworkStatus.Available, + }, }; /** @@ -195,6 +214,8 @@ export const MULTICHAIN_NETWORK_TICKER: Record = { [TrxScope.Mainnet]: 'TRX', [TrxScope.Nile]: 'tTRX', [TrxScope.Shasta]: 'sTRX', + [XlmScope.Pubnet]: 'XLM', + [XlmScope.Testnet]: 'tXLM', } as const; /** @@ -213,4 +234,6 @@ export const MULTICHAIN_NETWORK_DECIMAL_PLACES: Record = { [TrxScope.Mainnet]: 6, [TrxScope.Nile]: 6, [TrxScope.Shasta]: 6, + [XlmScope.Pubnet]: 7, + [XlmScope.Testnet]: 7, } as const; diff --git a/packages/multichain-network-controller/src/types.ts b/packages/multichain-network-controller/src/types.ts index 6988307298..4b4b42b6d4 100644 --- a/packages/multichain-network-controller/src/types.ts +++ b/packages/multichain-network-controller/src/types.ts @@ -9,6 +9,7 @@ import type { CaipChainId, SolScope, TrxScope, + XlmScope, } from '@metamask/keyring-api'; import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { Messenger } from '@metamask/messenger'; @@ -43,7 +44,9 @@ export type SupportedCaipChainId = | SolScope.Devnet | TrxScope.Mainnet | TrxScope.Nile - | TrxScope.Shasta; + | TrxScope.Shasta + | XlmScope.Pubnet + | XlmScope.Testnet; export type CommonNetworkConfiguration = { /** diff --git a/packages/multichain-network-controller/src/utils.test.ts b/packages/multichain-network-controller/src/utils.test.ts index c865fcf610..83ceaacb05 100644 --- a/packages/multichain-network-controller/src/utils.test.ts +++ b/packages/multichain-network-controller/src/utils.test.ts @@ -1,4 +1,4 @@ -import { BtcScope, SolScope, EthScope } from '@metamask/keyring-api'; +import { BtcScope, SolScope, EthScope, XlmScope } from '@metamask/keyring-api'; import type { CaipChainId } from '@metamask/keyring-api'; import type { NetworkConfiguration } from '@metamask/network-controller'; import { KnownCaipNamespace } from '@metamask/utils'; @@ -29,6 +29,11 @@ describe('utils', () => { expect(getChainIdForNonEvm(scopes)).toBe(BtcScope.Testnet); }); + it('returns Stellar chain ID for Stellar scopes', () => { + const scopes = [XlmScope.Pubnet, XlmScope.Testnet]; + expect(getChainIdForNonEvm(scopes)).toBe(XlmScope.Pubnet); + }); + it('throws error if network is not found', () => { const scopes = ['unknown:scope' as CaipChainId]; expect(() => getChainIdForNonEvm(scopes)).toThrow( @@ -41,6 +46,8 @@ describe('utils', () => { it('returns true for supported CAIP chain IDs', () => { expect(checkIfSupportedCaipChainId(SolScope.Mainnet)).toBe(true); expect(checkIfSupportedCaipChainId(BtcScope.Mainnet)).toBe(true); + expect(checkIfSupportedCaipChainId(XlmScope.Pubnet)).toBe(true); + expect(checkIfSupportedCaipChainId(XlmScope.Testnet)).toBe(true); }); it('returns false for non-CAIP IDs', () => { diff --git a/packages/network-enablement-controller/package.json b/packages/network-enablement-controller/package.json index cb37237dc7..85455915f7 100644 --- a/packages/network-enablement-controller/package.json +++ b/packages/network-enablement-controller/package.json @@ -61,7 +61,7 @@ "@metamask/network-controller": "^30.0.1", "@metamask/slip44": "^4.3.0", "@metamask/transaction-controller": "^64.3.0", - "@metamask/utils": "^11.9.0", + "@metamask/utils": "^11.11.0", "reselect": "^5.1.1" }, "devDependencies": { diff --git a/packages/network-enablement-controller/src/NetworkEnablementController.test.ts b/packages/network-enablement-controller/src/NetworkEnablementController.test.ts index 8712deadaa..8ca349966c 100644 --- a/packages/network-enablement-controller/src/NetworkEnablementController.test.ts +++ b/packages/network-enablement-controller/src/NetworkEnablementController.test.ts @@ -1,6 +1,6 @@ import { deriveStateFromMetadata } from '@metamask/base-controller'; import { BuiltInNetworkName, ChainId } from '@metamask/controller-utils'; -import { BtcScope, SolScope, TrxScope } from '@metamask/keyring-api'; +import { BtcScope, SolScope, TrxScope, XlmScope } from '@metamask/keyring-api'; import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger'; import type { MessengerActions, @@ -83,6 +83,7 @@ const defaultMultichainGetState = (): MultichainGetStateReturn => ({ [BtcScope.Mainnet]: { chainId: BtcScope.Mainnet, name: 'Bitcoin' }, [SolScope.Mainnet]: { chainId: SolScope.Mainnet, name: 'Solana' }, [TrxScope.Mainnet]: { chainId: TrxScope.Mainnet, name: 'Tron' }, + [XlmScope.Pubnet]: { chainId: XlmScope.Pubnet, name: 'Stellar' }, }, selectedMultichainNetworkChainId: 'eip155:1', isEvmSelected: true, @@ -211,6 +212,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: true, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: getDefaultNativeAssetIdentifiers(), }); @@ -267,6 +272,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: true, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: { ...getDefaultNativeAssetIdentifiers(), @@ -330,6 +339,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: true, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: expectedNativeAssetIdentifiers, }); @@ -471,6 +484,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: true, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: expectedNativeAssetIdentifiersForFallback, }); @@ -571,6 +588,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: true, + [XlmScope.Testnet]: false, + }, }, // init() populates nativeAssetIdentifiers from NetworkController (EVM networks only) nativeAssetIdentifiers: { @@ -1172,6 +1193,10 @@ describe('NetworkEnablementController', () => { chainId: TrxScope.Mainnet, name: 'Tron Mainnet', }, + [XlmScope.Pubnet]: { + chainId: XlmScope.Pubnet, + name: 'Stellar Mainnet', + }, }, selectedMultichainNetworkChainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', @@ -1213,6 +1238,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: true, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: getDefaultNativeAssetIdentifiers(), }); @@ -1247,6 +1276,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: true, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: getDefaultNativeAssetIdentifiers(), }); @@ -1329,6 +1362,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: false, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: getDefaultNativeAssetIdentifiers(), }); @@ -1510,6 +1547,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: true, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: getDefaultNativeAssetIdentifiers(), }); @@ -1544,6 +1585,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: false, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: getDefaultNativeAssetIdentifiers(), }); @@ -1598,6 +1643,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: false, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: { ...getDefaultNativeAssetIdentifiers(), @@ -1636,6 +1685,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: false, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: { ...getDefaultNativeAssetIdentifiers(), @@ -1674,6 +1727,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: false, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: { ...getDefaultNativeAssetIdentifiers(), @@ -1723,6 +1780,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: false, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: getDefaultNativeAssetIdentifiers(), }); @@ -1764,6 +1825,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: false, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: getDefaultNativeAssetIdentifiers(), }); @@ -1818,6 +1883,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: false, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: { ...getDefaultNativeAssetIdentifiers(), @@ -1864,6 +1933,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: true, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: getDefaultNativeAssetIdentifiers(), }); @@ -1912,6 +1985,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: true, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: getDefaultNativeAssetIdentifiers(), }); @@ -2148,14 +2225,15 @@ describe('NetworkEnablementController', () => { const { controller } = setupController(); const result = controller.listPopularNetworks(); - // Default setup: 3 EVM (0x1, 0xe708, 0x2105) + 3 multichain (Btc, Sol, Trx) + // Default setup: 3 EVM (0x1, 0xe708, 0x2105) + 4 multichain (Btc, Sol, Trx, Stellar) expect(result).toContain('eip155:1'); expect(result).toContain('eip155:59144'); expect(result).toContain('eip155:8453'); expect(result).toContain(BtcScope.Mainnet); expect(result).toContain(SolScope.Mainnet); expect(result).toContain(TrxScope.Mainnet); - expect(result).toHaveLength(6); + expect(result).toContain(XlmScope.Pubnet); + expect(result).toHaveLength(7); }); it('excludes multichain mainnets when not in MultichainNetworkController state', () => { @@ -2175,6 +2253,7 @@ describe('NetworkEnablementController', () => { expect(result).not.toContain(BtcScope.Mainnet); expect(result).not.toContain(SolScope.Mainnet); expect(result).not.toContain(TrxScope.Mainnet); + expect(result).not.toContain(XlmScope.Pubnet); expect(result).toHaveLength(3); }); @@ -2222,14 +2301,15 @@ describe('NetworkEnablementController', () => { }); describe('listPopularMultichainNetworks', () => { - it('returns only Bitcoin, Solana, Tron mainnets that exist in MultichainNetworkController state', () => { + it('returns only Bitcoin, Solana, Tron, Stellar mainnets that exist in MultichainNetworkController state', () => { const { controller } = setupController(); const result = controller.listPopularMultichainNetworks(); expect(result).toContain(BtcScope.Mainnet); expect(result).toContain(SolScope.Mainnet); expect(result).toContain(TrxScope.Mainnet); - expect(result).toHaveLength(3); + expect(result).toContain(XlmScope.Pubnet); + expect(result).toHaveLength(4); }); it('returns empty when none of the multichain mainnets are configured', () => { diff --git a/packages/network-enablement-controller/src/NetworkEnablementController.ts b/packages/network-enablement-controller/src/NetworkEnablementController.ts index 8ea80bcc98..39e428e57b 100644 --- a/packages/network-enablement-controller/src/NetworkEnablementController.ts +++ b/packages/network-enablement-controller/src/NetworkEnablementController.ts @@ -4,7 +4,7 @@ import type { ControllerStateChangeEvent, } from '@metamask/base-controller'; import { BuiltInNetworkName, ChainId } from '@metamask/controller-utils'; -import { BtcScope, SolScope, TrxScope } from '@metamask/keyring-api'; +import { BtcScope, SolScope, TrxScope, XlmScope } from '@metamask/keyring-api'; import type { Messenger } from '@metamask/messenger'; import type { MultichainNetworkControllerGetStateAction } from '@metamask/multichain-network-controller'; import { toEvmCaipChainId } from '@metamask/multichain-network-controller'; @@ -183,6 +183,10 @@ const getDefaultNetworkEnablementControllerState = [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: true, + [XlmScope.Testnet]: false, + }, }, // nativeAssetIdentifiers is initialized as empty and should be populated // by the client using initNativeAssetIdentifiers() during controller init @@ -418,6 +422,18 @@ export class NetworkEnablementController extends BaseController< // Enable Tron mainnet state.enabledNetworkMap[tronKeys.namespace][tronKeys.storageKey] = true; } + + // Enable Stellar mainnet if it exists in MultichainNetworkController configurations + const stellarKeys = deriveKeys(XlmScope.Pubnet as CaipChainId); + if ( + multichainState.multichainNetworkConfigurationsByChainId[ + XlmScope.Pubnet + ] + ) { + this.#ensureNamespaceBucket(state, stellarKeys.namespace); + state.enabledNetworkMap[stellarKeys.namespace][stellarKeys.storageKey] = + true; + } }); } @@ -768,11 +784,11 @@ export class NetworkEnablementController extends BaseController< } /** - * Returns popular multichain (Bitcoin, Solana, Tron) mainnet chain IDs in + * Returns popular multichain (Bitcoin, Solana, Tron, Stellar) mainnet chain IDs in * CAIP-2 form, restricted to networks that exist in MultichainNetworkController * (multichainNetworkConfigurationsByChainId). * - * @returns CAIP-2 chain IDs for Bitcoin, Solana, and Tron mainnets that are configured. + * @returns CAIP-2 chain IDs for Bitcoin, Solana, Tron, and Stellar mainnets that are configured. */ listPopularMultichainNetworks(): CaipChainId[] { const multichainState = this.messenger.call( @@ -782,6 +798,7 @@ export class NetworkEnablementController extends BaseController< BtcScope.Mainnet, SolScope.Mainnet, TrxScope.Mainnet, + XlmScope.Pubnet, ] as const; return multichainMainnets.filter( (chainId) => @@ -794,7 +811,7 @@ export class NetworkEnablementController extends BaseController< * networks that exist in NetworkController (networkConfigurationsByChainId) and * MultichainNetworkController (multichainNetworkConfigurationsByChainId). EVM * popular networks come from POPULAR_NETWORKS; multichain popular are Bitcoin, - * Solana, and Tron mainnets. + * Solana, Tron, and Stellar mainnets. * * @returns CAIP-2 chain IDs for popular EVM networks and multichain mainnets that are configured. */