diff --git a/eslint-suppressions.json b/eslint-suppressions.json index e69c43dc3b..bb20541c19 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -710,9 +710,6 @@ "packages/bridge-controller/src/utils/trade-utils.ts": { "no-restricted-globals": { "count": 1 - }, - "no-restricted-syntax": { - "count": 5 } }, "packages/bridge-controller/tests/mock-sse.ts": { diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index 51c6d26c27..484eae6b7f 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -38,6 +38,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - **BREAKING**: Add persisted input primary denomination state and `Unified SwapBridge Fiat Crypto Toggle Clicked` analytics event support ([#9147](https://github.com/MetaMask/core/pull/9147)) +- 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` ([#8829](https://github.com/MetaMask/core/pull/8829)) +- Add `StellarTradeDataSchema`, `StellarTradeData`, and `isStellarTrade`; extend `extractTradeData` to read Stellar XDR from `{ xdrBase64 }` or `{ xdr }` objects ([#8829](https://github.com/MetaMask/core/pull/8829)) ## [75.2.1] 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 9882186911..253eec98a3 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", ], "feature_id": "unified_swap_bridge", "location": "Unknown", diff --git a/packages/bridge-controller/src/bridge-controller.sse.test.ts b/packages/bridge-controller/src/bridge-controller.sse.test.ts index 95bbfd7858..cf4d62a102 100644 --- a/packages/bridge-controller/src/bridge-controller.sse.test.ts +++ b/packages/bridge-controller/src/bridge-controller.sse.test.ts @@ -857,13 +857,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); @@ -1236,22 +1236,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(); @@ -1266,21 +1268,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); @@ -1401,11 +1403,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 79d35f0f3e..537e72c265 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 { @@ -26,6 +26,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]; @@ -57,6 +58,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 ce522a8283..456ca97b5c 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', ARC: 'USDC', } as const; @@ -170,6 +171,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', @@ -221,6 +230,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 = @@ -245,6 +255,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', // It won't be displayed - hidden on UI client side diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index 3b3a311edd..d66ab330dd 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -54,6 +54,7 @@ export type { Intent, IntentOrderLike, BitcoinTradeData, + StellarTradeData, TronTradeData, BridgeControllerState, InputPrimaryDenomination, @@ -148,6 +149,7 @@ export { isSolanaChainId, isBitcoinChainId, isTronChainId, + isStellarChainId, isNonEvmChainId, getNativeAssetForChainId, getDefaultBridgeControllerState, @@ -175,6 +177,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 eecab10d8c..c83478fbb1 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -44,6 +44,7 @@ import type { StepSchema, TokenFeatureSchema, QuoteStreamCompleteSchema, + StellarTradeDataSchema, TronTradeDataSchema, TxDataSchema, BatchSellTradesResponseSchema, @@ -299,13 +300,20 @@ 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 QuoteResponseV1< - TxDataType = TxData | string | BitcoinTradeData | TronTradeData, + TxDataType = + | TxData + | string + | BitcoinTradeData + | TronTradeData + | StellarTradeData, ApprovalType = TxData | TronTradeData, > = Infer & { trade: TxDataType; @@ -367,6 +375,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 27671ed8f9..3dd103dda1 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..aae0def48f 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,59 @@ 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('falls back to xdr when xdrBase64 is present but not a string', () => { + expect( + extractTradeData({ + xdrBase64: null, + 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..e54c3c6b63 100644 --- a/packages/bridge-controller/src/utils/trade-utils.ts +++ b/packages/bridge-controller/src/utils/trade-utils.ts @@ -1,7 +1,20 @@ -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; + +const hasOwnProp = (obj: object, key: PropertyKey): boolean => + Object.prototype.hasOwnProperty.call(obj, key); /** * Type guard to check if a trade is an EVM TxData object @@ -13,9 +26,9 @@ export const isEvmTxData = (trade: Trade): trade is TxData => { return ( typeof trade === 'object' && trade !== null && - 'data' in trade && - 'chainId' in trade && - 'to' in trade + hasOwnProp(trade, 'data') && + hasOwnProp(trade, 'chainId') && + hasOwnProp(trade, 'to') ); }; @@ -27,7 +40,9 @@ export const isEvmTxData = (trade: Trade): trade is TxData => { */ export const isBitcoinTrade = (trade: Trade): trade is BitcoinTradeData => { return ( - typeof trade === 'object' && trade !== null && 'unsignedPsbtBase64' in trade + typeof trade === 'object' && + trade !== null && + hasOwnProp(trade, 'unsignedPsbtBase64') ); }; @@ -38,7 +53,36 @@ export const isBitcoinTrade = (trade: Trade): trade is BitcoinTradeData => { * @returns True if the trade is a Tron trade with raw_data_hex property */ export const isTronTrade = (trade: Trade): trade is TronTradeData => { - return typeof trade === 'object' && trade !== null && 'raw_data_hex' in trade; + return ( + typeof trade === 'object' && + trade !== null && + hasOwnProp(trade, 'raw_data_hex') + ); +}; + +/** + * Type guard to check if a trade is a Stellar trade with XDR (base64) payload + * + * @param trade - The trade object to check + * @returns True if the trade is a Stellar trade with xdrBase64 or xdr property + */ +export const isStellarTrade = (trade: Trade): trade is StellarTradeData => { + if (typeof trade !== 'object' || trade === null) { + return false; + } + if ( + hasOwnProp(trade, 'xdrBase64') && + typeof (trade as { xdrBase64: unknown }).xdrBase64 === 'string' + ) { + return true; + } + if ( + hasOwnProp(trade, 'xdr') && + typeof (trade as { xdr: unknown }).xdr === 'string' + ) { + return true; + } + return false; }; /** @@ -59,6 +103,16 @@ export const extractTradeData = (trade: Trade): string => { return Buffer.from(trade.raw_data_hex, 'hex').toString('base64'); } + if (isStellarTrade(trade)) { + if ( + hasOwnProp(trade, 'xdrBase64') && + typeof (trade as { xdrBase64: unknown }).xdrBase64 === 'string' + ) { + return (trade as { xdrBase64: string }).xdrBase64; + } + return (trade as { xdr: string }).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 433726518d..3bc1440f9b 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({ quoteId: optional(string()), quote: QuoteSchema, @@ -471,6 +479,7 @@ export const QuoteResponseSchema = type({ TxDataSchema, BitcoinTradeDataSchema, TronTradeDataSchema, + StellarTradeDataSchema, string(), ]), }); diff --git a/packages/bridge-status-controller/CHANGELOG.md b/packages/bridge-status-controller/CHANGELOG.md index 78a7706655..87a60d917c 100644 --- a/packages/bridge-status-controller/CHANGELOG.md +++ b/packages/bridge-status-controller/CHANGELOG.md @@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add input primary denomination to submitted bridge history and post-submit analytics ([#9147](https://github.com/MetaMask/core/pull/9147)) +- Add Stellar support to bridge transaction submission and status tracking: thread `StellarTradeData` through the non-EVM submit strategies and include source and destination asset IDs in the snap client transaction request ([#8829](https://github.com/MetaMask/core/pull/8829)) ### Changed diff --git a/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap b/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap index 37f96aa8f5..be45d33970 100644 --- a/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap +++ b/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap @@ -4884,6 +4884,10 @@ exports[`BridgeStatusController submitTx: Solana bridge should handle snap contr "method": "signAndSendTransaction", "params": { "accountId": "solana-account-1", + "options": { + "destAssetId": "eip155:1/slip44:60", + "sourceAssetId": "eip155:1399811149/slip44:501", + }, "scope": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "transaction": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAHDXLY8oVRIwA8ZdRSGjM5RIZJW8Wv+Twyw3NqU4Hov+OHoHp/dmeDvstKbICW3ezeGR69t3/PTAvdXgZVdJFJXaxkoKXUTWfEAyQyCCG9nwVoDsd10OFdnM9ldSi+9SLqHpqWVDV+zzkmftkF//DpbXxqeH8obNXHFR7pUlxG9uNVOn64oNsFdeUvD139j1M51iRmUY839Y25ET4jDRscT081oGb+rLnywLjLSrIQx6MkqNBhCFbxqY1YmoGZVORW/QMGRm/lIRcy/+ytunLDm+e8jOW7xfcSayxDmzpAAAAAjJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+FkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpBHnVW/IxwG7udMVuzmgVB/2xst6j9I5RArHNola8E4+0P/on9df2SnTAmx8pWHneSwmrNt/J3VFLMhqns4zl6JmXkZ+niuxMhAGrmKBaBo94uMv2Sl+Xh3i+VOO0m5BdNZ1ElenbwQylHQY+VW1ydG1MaUEeNpG+EVgswzPMwPoLBgAFAsBcFQAGAAkDQA0DAAAAAAAHBgABAhMICQAHBgADABYICQEBCAIAAwwCAAAAUEYVOwAAAAAJAQMBEQoUCQADBAETCgsKFw0ODxARAwQACRQj5RfLl3rjrSoBAAAAQ2QAAVBGFTsAAAAAyYZnBwAAAABkAAAJAwMAAAEJDAkAAAIBBBMVCQjGASBMKQwnooTbKNxdBwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUHTKomh4KXvNgA0ovYKS5F8GIOBgAAAAAAAAAAAAAAAAAQgAAAAAAAAAAAAAAAAAAAAAAAEIF7RFOAwAAAAAAAAAAAAAAaAIAAAAAAAC4CwAAAAAAAOAA2mcAAAAAAAAAAAAAAAAAAAAApapuIXG0FuHSfsU8qME9s/kaic0AAwGCsZdSuxV5eCm+Ria4LEQPgTg4bg65gNrTAefEzpAfPQgCABIMAgAAAAAAAAAAAAAACAIABQwCAAAAsIOFAAAAAAADWk6DVOZO8lMFQg2r0dgfltD6tRL/B1hH3u00UzZdgqkAAxEqIPdq2eRt/F6mHNmFe7iwZpdrtGmHNJMFlK7c6Bc6k6kjBezr6u/tAgvu3OGsJSwSElmcOHZ21imqH/rhJ2KgqDJdBPFH4SYIM1kBAAA=", }, @@ -4975,6 +4979,10 @@ exports[`BridgeStatusController submitTx: Solana bridge should successfully subm "method": "signAndSendTransaction", "params": { "accountId": "solana-account-1", + "options": { + "destAssetId": "eip155:1/slip44:60", + "sourceAssetId": "eip155:1399811149/slip44:501", + }, "scope": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "transaction": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAHDXLY8oVRIwA8ZdRSGjM5RIZJW8Wv+Twyw3NqU4Hov+OHoHp/dmeDvstKbICW3ezeGR69t3/PTAvdXgZVdJFJXaxkoKXUTWfEAyQyCCG9nwVoDsd10OFdnM9ldSi+9SLqHpqWVDV+zzkmftkF//DpbXxqeH8obNXHFR7pUlxG9uNVOn64oNsFdeUvD139j1M51iRmUY839Y25ET4jDRscT081oGb+rLnywLjLSrIQx6MkqNBhCFbxqY1YmoGZVORW/QMGRm/lIRcy/+ytunLDm+e8jOW7xfcSayxDmzpAAAAAjJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+FkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpBHnVW/IxwG7udMVuzmgVB/2xst6j9I5RArHNola8E4+0P/on9df2SnTAmx8pWHneSwmrNt/J3VFLMhqns4zl6JmXkZ+niuxMhAGrmKBaBo94uMv2Sl+Xh3i+VOO0m5BdNZ1ElenbwQylHQY+VW1ydG1MaUEeNpG+EVgswzPMwPoLBgAFAsBcFQAGAAkDQA0DAAAAAAAHBgABAhMICQAHBgADABYICQEBCAIAAwwCAAAAUEYVOwAAAAAJAQMBEQoUCQADBAETCgsKFw0ODxARAwQACRQj5RfLl3rjrSoBAAAAQ2QAAVBGFTsAAAAAyYZnBwAAAABkAAAJAwMAAAEJDAkAAAIBBBMVCQjGASBMKQwnooTbKNxdBwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUHTKomh4KXvNgA0ovYKS5F8GIOBgAAAAAAAAAAAAAAAAAQgAAAAAAAAAAAAAAAAAAAAAAAEIF7RFOAwAAAAAAAAAAAAAAaAIAAAAAAAC4CwAAAAAAAOAA2mcAAAAAAAAAAAAAAAAAAAAApapuIXG0FuHSfsU8qME9s/kaic0AAwGCsZdSuxV5eCm+Ria4LEQPgTg4bg65gNrTAefEzpAfPQgCABIMAgAAAAAAAAAAAAAACAIABQwCAAAAsIOFAAAAAAADWk6DVOZO8lMFQg2r0dgfltD6tRL/B1hH3u00UzZdgqkAAxEqIPdq2eRt/F6mHNmFe7iwZpdrtGmHNJMFlK7c6Bc6k6kjBezr6u/tAgvu3OGsJSwSElmcOHZ21imqH/rhJ2KgqDJdBPFH4SYIM1kBAAA=", }, @@ -5250,6 +5258,10 @@ exports[`BridgeStatusController submitTx: Solana swap should handle snap control "method": "signAndSendTransaction", "params": { "accountId": "solana-account-1", + "options": { + "destAssetId": "eip155:1399811149/slip44:501", + "sourceAssetId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501", + }, "scope": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "transaction": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAHDXLY8oVRIwA8ZdRSGjM5RIZJW8Wv+Twyw3NqU4Hov+OHoHp/dmeDvstKbICW3ezeGR69t3/PTAvdXgZVdJFJXaxkoKXUTWfEAyQyCCG9nwVoDsd10OFdnM9ldSi+9SLqHpqWVDV+zzkmftkF//DpbXxqeH8obNXHFR7pUlxG9uNVOn64oNsFdeUvD139j1M51iRmUY839Y25ET4jDRscT081oGb+rLnywLjLSrIQx6MkqNBhCFbxqY1YmoGZVORW/QMGRm/lIRcy/+ytunLDm+e8jOW7xfcSayxDmzpAAAAAjJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+FkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpBHnVW/IxwG7udMVuzmgVB/2xst6j9I5RArHNola8E4+0P/on9df2SnTAmx8pWHneSwmrNt/J3VFLMhqns4zl6JmXkZ+niuxMhAGrmKBaBo94uMv2Sl+Xh3i+VOO0m5BdNZ1ElenbwQylHQY+VW1ydG1MaUEeNpG+EVgswzPMwPoLBgAFAsBcFQAGAAkDQA0DAAAAAAAHBgABAhMICQAHBgADABYICQEBCAIAAwwCAAAAUEYVOwAAAAAJAQMBEQoUCQADBAETCgsKFw0ODxARAwQACRQj5RfLl3rjrSoBAAAAQ2QAAVBGFTsAAAAAyYZnBwAAAABkAAAJAwMAAAEJDAkAAAIBBBMVCQjGASBMKQwnooTbKNxdBwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUHTKomh4KXvNgA0ovYKS5F8GIOBgAAAAAAAAAAAAAAAAAQgAAAAAAAAAAAAAAAAAAAAAAAEIF7RFOAwAAAAAAAAAAAAAAaAIAAAAAAAC4CwAAAAAAAOAA2mcAAAAAAAAAAAAAAAAAAAAApapuIXG0FuHSfsU8qME9s/kaic0AAwGCsZdSuxV5eCm+Ria4LEQPgTg4bg65gNrTAefEzpAfPQgCABIMAgAAAAAAAAAAAAAACAIABQwCAAAAsIOFAAAAAAADWk6DVOZO8lMFQg2r0dgfltD6tRL/B1hH3u00UzZdgqkAAxEqIPdq2eRt/F6mHNmFe7iwZpdrtGmHNJMFlK7c6Bc6k6kjBezr6u/tAgvu3OGsJSwSElmcOHZ21imqH/rhJ2KgqDJdBPFH4SYIM1kBAAA=", }, @@ -5341,6 +5353,10 @@ exports[`BridgeStatusController submitTx: Solana swap should successfully submit "method": "signAndSendTransaction", "params": { "accountId": "solana-account-1", + "options": { + "destAssetId": "eip155:1399811149/slip44:501", + "sourceAssetId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501", + }, "scope": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "transaction": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAHDXLY8oVRIwA8ZdRSGjM5RIZJW8Wv+Twyw3NqU4Hov+OHoHp/dmeDvstKbICW3ezeGR69t3/PTAvdXgZVdJFJXaxkoKXUTWfEAyQyCCG9nwVoDsd10OFdnM9ldSi+9SLqHpqWVDV+zzkmftkF//DpbXxqeH8obNXHFR7pUlxG9uNVOn64oNsFdeUvD139j1M51iRmUY839Y25ET4jDRscT081oGb+rLnywLjLSrIQx6MkqNBhCFbxqY1YmoGZVORW/QMGRm/lIRcy/+ytunLDm+e8jOW7xfcSayxDmzpAAAAAjJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+FkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpBHnVW/IxwG7udMVuzmgVB/2xst6j9I5RArHNola8E4+0P/on9df2SnTAmx8pWHneSwmrNt/J3VFLMhqns4zl6JmXkZ+niuxMhAGrmKBaBo94uMv2Sl+Xh3i+VOO0m5BdNZ1ElenbwQylHQY+VW1ydG1MaUEeNpG+EVgswzPMwPoLBgAFAsBcFQAGAAkDQA0DAAAAAAAHBgABAhMICQAHBgADABYICQEBCAIAAwwCAAAAUEYVOwAAAAAJAQMBEQoUCQADBAETCgsKFw0ODxARAwQACRQj5RfLl3rjrSoBAAAAQ2QAAVBGFTsAAAAAyYZnBwAAAABkAAAJAwMAAAEJDAkAAAIBBBMVCQjGASBMKQwnooTbKNxdBwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUHTKomh4KXvNgA0ovYKS5F8GIOBgAAAAAAAAAAAAAAAAAQgAAAAAAAAAAAAAAAAAAAAAAAEIF7RFOAwAAAAAAAAAAAAAAaAIAAAAAAAC4CwAAAAAAAOAA2mcAAAAAAAAAAAAAAAAAAAAApapuIXG0FuHSfsU8qME9s/kaic0AAwGCsZdSuxV5eCm+Ria4LEQPgTg4bg65gNrTAefEzpAfPQgCABIMAgAAAAAAAAAAAAAACAIABQwCAAAAsIOFAAAAAAADWk6DVOZO8lMFQg2r0dgfltD6tRL/B1hH3u00UzZdgqkAAxEqIPdq2eRt/F6mHNmFe7iwZpdrtGmHNJMFlK7c6Bc6k6kjBezr6u/tAgvu3OGsJSwSElmcOHZ21imqH/rhJ2KgqDJdBPFH4SYIM1kBAAA=", }, diff --git a/packages/bridge-status-controller/src/strategy/index.ts b/packages/bridge-status-controller/src/strategy/index.ts index e029905587..53500b9aba 100644 --- a/packages/bridge-status-controller/src/strategy/index.ts +++ b/packages/bridge-status-controller/src/strategy/index.ts @@ -6,7 +6,9 @@ import { isBitcoinTrade, isEvmTxData, isNonEvmChainId, + isStellarTrade, isTronTrade, + StellarTradeData, Trade, TronTradeData, TxData, @@ -20,7 +22,12 @@ import { submitNonEvmHandler } from './non-evm-strategy'; import type { SubmitStrategyParams, SubmitStepResult } from './types'; const validateParams = < - TxDataType extends BitcoinTradeData | TronTradeData | string | TxData, + TxDataType extends + | BitcoinTradeData + | StellarTradeData + | TronTradeData + | string + | TxData, >( params: SubmitStrategyParams, ): params is SubmitStrategyParams => { @@ -38,6 +45,8 @@ const validateParams = < return txs.every((tx) => typeof tx === 'string'); case ChainId.BTC: return txs.every(isBitcoinTrade); + case ChainId.STELLAR: + return txs.every((tx) => typeof tx === 'string' || isStellarTrade(tx)); case ChainId.TRON: return txs.every(isTronTrade); default: diff --git a/packages/bridge-status-controller/src/strategy/non-evm-strategy.ts b/packages/bridge-status-controller/src/strategy/non-evm-strategy.ts index 1c57c21bf6..29a22dcfd6 100644 --- a/packages/bridge-status-controller/src/strategy/non-evm-strategy.ts +++ b/packages/bridge-status-controller/src/strategy/non-evm-strategy.ts @@ -2,6 +2,7 @@ import { isTronChainId } from '@metamask/bridge-controller'; import type { BitcoinTradeData, + StellarTradeData, TronTradeData, TxData, } from '@metamask/bridge-controller'; @@ -20,7 +21,7 @@ import type { SubmitStrategyParams, SubmitStepResult } from './types'; */ const handleTronApproval = async ( args: SubmitStrategyParams< - TronTradeData | BitcoinTradeData | string | TxData + TronTradeData | BitcoinTradeData | StellarTradeData | string | TxData >, ) => { const { @@ -65,7 +66,7 @@ const handleTronApproval = async ( */ export async function* submitNonEvmHandler( args: SubmitStrategyParams< - BitcoinTradeData | TronTradeData | string | TxData + BitcoinTradeData | StellarTradeData | TronTradeData | string | TxData >, ): AsyncGenerator { const { diff --git a/packages/bridge-status-controller/src/utils/snaps.ts b/packages/bridge-status-controller/src/utils/snaps.ts index 45fd83cd2a..58f0e2cb2d 100644 --- a/packages/bridge-status-controller/src/utils/snaps.ts +++ b/packages/bridge-status-controller/src/utils/snaps.ts @@ -19,7 +19,7 @@ import { TransactionType, } from '@metamask/transaction-controller'; import type { TransactionMeta } from '@metamask/transaction-controller'; -import type { CaipChainId, Hex } from '@metamask/utils'; +import type { CaipAssetType, CaipChainId, Hex } from '@metamask/utils'; import { v4 as uuid } from 'uuid'; import type { @@ -72,6 +72,8 @@ export const createClientTransactionRequest = ( * @param srcChainId - The source chain ID * @param accountId - The account ID * @param snapId - The snap ID + * @param sourceAssetId - The source asset ID + * @param destAssetId - The destination asset ID * @returns The snap request object for signing and sending transaction */ export const getClientRequest = ( @@ -79,18 +81,33 @@ export const getClientRequest = ( srcChainId: number, accountId: AccountsControllerState['internalAccounts']['accounts'][string]['id'], snapId: string, + sourceAssetId?: CaipAssetType, + destAssetId?: CaipAssetType, ): Parameters[0] => { const scope = formatChainIdToCaip(srcChainId); const transaction = extractTradeData(trade); - // Tron trades need the visible flag and contract type to be included in the request options - const options = isTronTrade(trade) - ? { - visible: trade.visible, - type: trade.raw_data?.contract?.[0]?.type, - } - : undefined; + let options: Record | undefined; + + if (sourceAssetId !== undefined || destAssetId !== undefined) { + options = { + ...(sourceAssetId !== undefined && { + sourceAssetId, + }), + ...(destAssetId !== undefined && { + destAssetId, + }), + }; + } + + if (isTronTrade(trade)) { + // Tron trades need the visible flag and contract type to be included in the request options + options = { + visible: trade.visible, + type: trade.raw_data?.contract?.[0]?.type, + }; + } return createClientTransactionRequest( snapId, @@ -250,6 +267,8 @@ export const handleNonEvmTx = async ( quoteResponse.quote.srcChainId, selectedAccount.id, selectedAccount.metadata?.snap?.id, + quoteResponse.quote.srcAsset.assetId, + quoteResponse.quote.destAsset.assetId, ); const requestResponse = (await messenger.call( 'SnapController:handleRequest', diff --git a/packages/bridge-status-controller/src/utils/transaction.test.ts b/packages/bridge-status-controller/src/utils/transaction.test.ts index 258c3f48e8..df5e906729 100644 --- a/packages/bridge-status-controller/src/utils/transaction.test.ts +++ b/packages/bridge-status-controller/src/utils/transaction.test.ts @@ -4,6 +4,7 @@ import { FeeType, formatChainIdToCaip, formatChainIdToHex, + getNativeAssetForChainId, } from '@metamask/bridge-controller'; import type { QuoteMetadata, @@ -1669,6 +1670,87 @@ describe('Bridge Status Controller Transaction Utils', () => { createClientRequestSpy.mockRestore(); }); + + it('should include Stellar source and destination asset IDs as options when trade is not Tron', () => { + const stellarTrade = { + xdrBase64: 'AAAABg==', + } as never; + + const mockAccount = { + id: 'test-account-id', + metadata: { + snap: { id: 'test-snap-id' }, + }, + }; + + const sourceAssetId = getNativeAssetForChainId(ChainId.STELLAR).assetId; + const destAssetId = getNativeAssetForChainId(ChainId.ETH).assetId; + + const result = snaps.getClientRequest( + stellarTrade, + ChainId.STELLAR, + mockAccount.id, + mockAccount.metadata.snap.id, + sourceAssetId, + destAssetId, + ); + + expect(result).toMatchObject({ + origin: 'metamask', + snapId: 'test-snap-id', + handler: 'onClientRequest', + request: { + id: expect.any(String), + jsonrpc: '2.0', + method: 'signAndSendTransaction', + params: { + transaction: 'AAAABg==', + scope: formatChainIdToCaip(ChainId.STELLAR), + accountId: 'test-account-id', + options: { + sourceAssetId, + destAssetId, + }, + }, + }, + }); + }); + + it('should omit destAssetId option for Stellar trades when destination asset ID is not provided', () => { + const stellarTrade = { + xdr: 'AAAABg==', + } as never; + + const mockAccount = { + id: 'test-account-id', + metadata: { + snap: { id: 'test-snap-id' }, + }, + }; + + const sourceAssetId = getNativeAssetForChainId(ChainId.STELLAR).assetId; + + const result = snaps.getClientRequest( + stellarTrade, + ChainId.STELLAR, + mockAccount.id, + mockAccount.metadata.snap.id, + sourceAssetId, + ); + + expect(result).toMatchObject({ + request: { + params: { + options: { + sourceAssetId, + }, + }, + }, + }); + expect( + (result.request.params as { options: Record }).options, + ).not.toHaveProperty('destAssetId'); + }); }); describe('getAddTransactionBatchParams', () => {