Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions packages/bridge-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
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 @@ -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];
Expand Down Expand Up @@ -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' },
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 @@ -59,6 +59,7 @@ const CURRENCY_SYMBOLS = {
MON: 'MON',
HYPE: 'HYPE',
MEGAETH: 'ETH',
XLM: 'XLM',
} as const;

const ETH_SWAPS_TOKEN_OBJECT = {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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 =
Expand All @@ -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',
};
3 changes: 3 additions & 0 deletions packages/bridge-controller/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export type {
Intent,
IntentOrderLike,
BitcoinTradeData,
StellarTradeData,
TronTradeData,
BridgeControllerState,
BridgeControllerAction,
Expand Down Expand Up @@ -138,6 +139,7 @@ export {
isSolanaChainId,
isBitcoinChainId,
isTronChainId,
isStellarChainId,
isNonEvmChainId,
getNativeAssetForChainId,
getDefaultBridgeControllerState,
Expand All @@ -164,6 +166,7 @@ export {
export {
extractTradeData,
isBitcoinTrade,
isStellarTrade,
isTronTrade,
isEvmTxData,
type Trade,
Expand Down
7 changes: 6 additions & 1 deletion packages/bridge-controller/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import type {
StepSchema,
TokenFeatureSchema,
QuoteStreamCompleteSchema,
StellarTradeDataSchema,
TronTradeDataSchema,
TxDataSchema,
} from './utils/validators';
Expand Down Expand Up @@ -283,13 +284,15 @@ export type IntentOrderLike = Intent['order'];
export type BitcoinTradeData = Infer<typeof BitcoinTradeDataSchema>;

export type TronTradeData = Infer<typeof TronTradeDataSchema>;

export type StellarTradeData = Infer<typeof StellarTradeDataSchema>;
/**
* 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<typeof QuoteResponseSchema> & {
trade: TxDataType;
Expand Down Expand Up @@ -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,
Expand Down
36 changes: 35 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,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);
Expand All @@ -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);
Expand Down Expand Up @@ -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',
Expand Down
25 changes: 22 additions & 3 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 @@ -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
Expand All @@ -243,7 +261,8 @@ export const isNonEvmChainId = (
return (
isSolanaChainId(chainId) ||
isBitcoinChainId(chainId) ||
isTronChainId(chainId)
isTronChainId(chainId) ||
isStellarChainId(chainId)
);
};

Expand Down
14 changes: 13 additions & 1 deletion packages/bridge-controller/src/utils/caip-formatters.test.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 { CHAIN_IDS } from '../constants/chains';
import { ChainId } from '../types';
Expand Down Expand Up @@ -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');
});
Expand Down Expand Up @@ -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);
});
Expand Down
12 changes: 11 additions & 1 deletion packages/bridge-controller/src/utils/caip-formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -25,6 +25,7 @@ import {
isBitcoinChainId,
isNativeAddress,
isSolanaChainId,
isStellarChainId,
isTronChainId,
} from './bridge';

Expand Down Expand Up @@ -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)));
};

Expand All @@ -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));
}
Expand Down
39 changes: 39 additions & 0 deletions packages/bridge-controller/src/utils/trade-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
extractTradeData,
isEvmTxData,
isBitcoinTrade,
isStellarTrade,
isTronTrade,
} from './trade-utils';
import type { Trade } from './trade-utils';
Expand Down Expand Up @@ -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,
Expand Down
Loading