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
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))

## [72.0.0]

### Added
Expand Down
2 changes: 1 addition & 1 deletion packages/bridge-controller/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@metamask/bridge-controller",
"version": "72.0.0",
"version": "72.0.0-dev.2",
"description": "Manages bridge-related quote fetching functionality for MetaMask",
"keywords": [
"Ethereum",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ exports[`BridgeController SSE should publish validation failures 4`] = `
"lifi|trade.unsignedPsbtBase64",
"lifi|trade.inputsToSign",
"lifi|trade.raw_data_hex",
"lifi|trade.xdrBase64",
"lifi|trade.xdr",
],
"location": "Main View",
"refresh_count": 1,
Expand Down
84 changes: 43 additions & 41 deletions packages/bridge-controller/src/bridge-controller.sse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -848,13 +848,13 @@ describe('BridgeController SSE', function () {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
).toBeGreaterThan(t2!);
expect(consoleLogSpy.mock.calls).toMatchInlineSnapshot(`
[
[
"Failed to stream bridge quotes",
"Network error",
],
]
`);
[
[
"Failed to stream bridge quotes",
"Network error",
],
]
`);
expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1);
expect(getLayer1GasFeeMock).toHaveBeenCalledTimes(2);
expect(trackMetaMetricsFn).toHaveBeenCalledTimes(8);
Expand Down Expand Up @@ -1220,22 +1220,24 @@ describe('BridgeController SSE', function () {
t6!,
);
expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(`
[
"Quote validation failed",
[
"lifi|trade",
"lifi|trade.chainId",
"lifi|trade.to",
"lifi|trade.from",
"lifi|trade.value",
"lifi|trade.data",
"lifi|trade.gasLimit",
"lifi|trade.unsignedPsbtBase64",
"lifi|trade.inputsToSign",
"lifi|trade.raw_data_hex",
],
]
`);
"Quote validation failed",
[
"lifi|trade",
"lifi|trade.chainId",
"lifi|trade.to",
"lifi|trade.from",
"lifi|trade.value",
"lifi|trade.data",
"lifi|trade.gasLimit",
"lifi|trade.unsignedPsbtBase64",
"lifi|trade.inputsToSign",
"lifi|trade.raw_data_hex",
"lifi|trade.xdrBase64",
"lifi|trade.xdr",
],
]
`);
// Invalid quote
jest.advanceTimersByTime(FOURTH_FETCH_DELAY * 3 - 1000);
await flushPromises();
Expand All @@ -1250,21 +1252,21 @@ describe('BridgeController SSE', function () {
);
expect(consoleWarnSpy.mock.calls).toHaveLength(3);
expect(consoleWarnSpy.mock.calls[1]).toMatchInlineSnapshot(`
[
"Quote validation failed",
[
"unknown|unknown",
],
]
`);
[
"Quote validation failed",
[
"unknown|unknown",
],
]
`);
expect(consoleWarnSpy.mock.calls[2]).toMatchInlineSnapshot(`
[
"Quote validation failed",
[
"unknown|quote",
],
]
`);
[
"Quote validation failed",
[
"unknown|quote",
],
]
`);

expect(consoleLogSpy).toHaveBeenCalledTimes(1);
expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(5);
Expand Down Expand Up @@ -1384,11 +1386,11 @@ describe('BridgeController SSE', function () {
expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1);
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
expect(consoleLogSpy.mock.calls[0]).toMatchInlineSnapshot(`
[
"Failed to stream bridge quotes",
[Error: Bridge-api error: timeout from server],
]
`);
[
"Failed to stream bridge quotes",
[Error: Bridge-api error: timeout from server],
]
`);
expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1);
expect(getLayer1GasFeeMock).toHaveBeenCalledTimes(0);
// eslint-disable-next-line jest/no-restricted-matchers
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 @@ -50,6 +50,7 @@ export type {
Intent,
IntentOrderLike,
BitcoinTradeData,
StellarTradeData,
TronTradeData,
BridgeControllerState,
BridgeControllerAction,
Expand Down Expand Up @@ -140,6 +141,7 @@ export {
isSolanaChainId,
isBitcoinChainId,
isTronChainId,
isStellarChainId,
isNonEvmChainId,
getNativeAssetForChainId,
getDefaultBridgeControllerState,
Expand Down Expand Up @@ -167,6 +169,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 @@ -322,6 +325,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
Loading