From b9051522595784aedc45c5f3b8435039847982b3 Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Fri, 15 May 2026 10:31:04 +0200 Subject: [PATCH] feat(bridge-controller): add Stellar bridge support --- packages/bridge-controller/CHANGELOG.md | 5 ++ packages/bridge-controller/package.json | 2 +- .../bridge-controller.sse.test.ts.snap | 2 + .../src/bridge-controller.sse.test.ts | 84 ++++++++++--------- .../bridge-controller/src/constants/bridge.ts | 4 +- .../bridge-controller/src/constants/tokens.ts | 13 ++- packages/bridge-controller/src/index.ts | 3 + packages/bridge-controller/src/types.ts | 7 +- .../src/utils/bridge.test.ts | 36 +++++++- .../bridge-controller/src/utils/bridge.ts | 25 +++++- .../src/utils/caip-formatters.test.ts | 14 +++- .../src/utils/caip-formatters.ts | 12 ++- .../src/utils/trade-utils.test.ts | 39 +++++++++ .../src/utils/trade-utils.ts | 39 ++++++++- .../bridge-controller/src/utils/validators.ts | 9 ++ 15 files changed, 240 insertions(+), 54 deletions(-) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index 0be7f50c94..9f66feb88f 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)) + ## [72.0.0] ### Added diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index ba58313029..b5a9a138b8 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -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", diff --git a/packages/bridge-controller/src/__snapshots__/bridge-controller.sse.test.ts.snap b/packages/bridge-controller/src/__snapshots__/bridge-controller.sse.test.ts.snap index 5dbe5e22f4..b7bbbbc609 100644 --- a/packages/bridge-controller/src/__snapshots__/bridge-controller.sse.test.ts.snap +++ b/packages/bridge-controller/src/__snapshots__/bridge-controller.sse.test.ts.snap @@ -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, diff --git a/packages/bridge-controller/src/bridge-controller.sse.test.ts b/packages/bridge-controller/src/bridge-controller.sse.test.ts index 0c10142524..b81bdffa3f 100644 --- a/packages/bridge-controller/src/bridge-controller.sse.test.ts +++ b/packages/bridge-controller/src/bridge-controller.sse.test.ts @@ -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); @@ -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(); @@ -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); @@ -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 diff --git a/packages/bridge-controller/src/constants/bridge.ts b/packages/bridge-controller/src/constants/bridge.ts index 6ee92f052a..ef632708bd 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 5af57fabf3..a9e82cb8af 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -50,6 +50,7 @@ export type { Intent, IntentOrderLike, BitcoinTradeData, + StellarTradeData, TronTradeData, BridgeControllerState, BridgeControllerAction, @@ -140,6 +141,7 @@ export { isSolanaChainId, isBitcoinChainId, isTronChainId, + isStellarChainId, isNonEvmChainId, getNativeAssetForChainId, getDefaultBridgeControllerState, @@ -167,6 +169,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 5f030bd45e..78ffd46ae3 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; @@ -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, 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 12cd1780b3..1307506182 100644 --- a/packages/bridge-controller/src/utils/validators.ts +++ b/packages/bridge-controller/src/utils/validators.ts @@ -462,6 +462,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(), @@ -470,6 +478,7 @@ export const QuoteResponseSchema = type({ TxDataSchema, BitcoinTradeDataSchema, TronTradeDataSchema, + StellarTradeDataSchema, string(), ]), });