From 421298b2b6c6a02746b2a52c9cdd0c89e60b8c64 Mon Sep 17 00:00:00 2001 From: Julien Fontanel Date: Wed, 17 Jun 2026 15:08:44 +0200 Subject: [PATCH 1/3] feat: add metadata to stellar trades tx for tx history in bridge status controller --- .../src/strategy/index.ts | 11 ++- .../src/strategy/non-evm-strategy.ts | 5 +- .../src/utils/snaps.ts | 27 +++++-- .../src/utils/transaction.test.ts | 74 +++++++++++++++++++ 4 files changed, 107 insertions(+), 10 deletions(-) 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..8c519b69d1 100644 --- a/packages/bridge-status-controller/src/utils/snaps.ts +++ b/packages/bridge-status-controller/src/utils/snaps.ts @@ -10,6 +10,7 @@ import { formatChainIdToCaip, formatChainIdToHex, isCrossChain, + isStellarTrade, isTronTrade, } from '@metamask/bridge-controller'; import { SnapController } from '@metamask/snaps-controllers'; @@ -72,6 +73,7 @@ export const createClientTransactionRequest = ( * @param srcChainId - The source chain ID * @param accountId - The account ID * @param snapId - The snap ID + * @param destChainId - The destination chain ID * @returns The snap request object for signing and sending transaction */ export const getClientRequest = ( @@ -79,18 +81,28 @@ export const getClientRequest = ( srcChainId: number, accountId: AccountsControllerState['internalAccounts']['accounts'][string]['id'], snapId: string, + destChainId?: number, ): 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 (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, + }; + } else if (isStellarTrade(trade)) { + // Stellar trades need the source and destination chain IDs to be included as metadata + options = { + sourceChainId: scope, + ...(destChainId !== undefined && { + destChainId: formatChainIdToCaip(destChainId), + }), + }; + } return createClientTransactionRequest( snapId, @@ -250,6 +262,7 @@ export const handleNonEvmTx = async ( quoteResponse.quote.srcChainId, selectedAccount.id, selectedAccount.metadata?.snap?.id, + quoteResponse.quote.destChainId, ); 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 1f8688d614..7912a6ab7c 100644 --- a/packages/bridge-status-controller/src/utils/transaction.test.ts +++ b/packages/bridge-status-controller/src/utils/transaction.test.ts @@ -1669,6 +1669,80 @@ describe('Bridge Status Controller Transaction Utils', () => { createClientRequestSpy.mockRestore(); }); + + it('should include Stellar source and destination chain IDs as options when trade is Stellar', () => { + const stellarTrade = { + xdrBase64: 'AAAABg==', + } as never; + + const mockAccount = { + id: 'test-account-id', + metadata: { + snap: { id: 'test-snap-id' }, + }, + }; + + const result = snaps.getClientRequest( + stellarTrade, + ChainId.STELLAR, + mockAccount.id, + mockAccount.metadata.snap.id, + ChainId.ETH, + ); + + 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: { + sourceChainId: formatChainIdToCaip(ChainId.STELLAR), + destChainId: formatChainIdToCaip(ChainId.ETH), + }, + }, + }, + }); + }); + + it('should omit destChainId option for Stellar trades when destination chain ID is not provided', () => { + const stellarTrade = { + xdr: 'AAAABg==', + } as never; + + const mockAccount = { + id: 'test-account-id', + metadata: { + snap: { id: 'test-snap-id' }, + }, + }; + + const result = snaps.getClientRequest( + stellarTrade, + ChainId.STELLAR, + mockAccount.id, + mockAccount.metadata.snap.id, + ); + + expect(result).toMatchObject({ + request: { + params: { + options: { + sourceChainId: formatChainIdToCaip(ChainId.STELLAR), + }, + }, + }, + }); + expect( + (result.request.params as { options: Record }).options, + ).not.toHaveProperty('destChainId'); + }); }); describe('getAddTransactionBatchParams', () => { From 11b3b9bf219d7321aac03467fb23374769f04e08 Mon Sep 17 00:00:00 2001 From: Julien Fontanel Date: Fri, 19 Jun 2026 10:13:54 +0200 Subject: [PATCH 2/3] chore: use asset Ids instead of chain Ids in options --- .../src/utils/snaps.ts | 29 ++++++++++--------- .../src/utils/transaction.test.ts | 22 +++++++++----- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/packages/bridge-status-controller/src/utils/snaps.ts b/packages/bridge-status-controller/src/utils/snaps.ts index 8c519b69d1..2a780bbaa8 100644 --- a/packages/bridge-status-controller/src/utils/snaps.ts +++ b/packages/bridge-status-controller/src/utils/snaps.ts @@ -10,7 +10,6 @@ import { formatChainIdToCaip, formatChainIdToHex, isCrossChain, - isStellarTrade, isTronTrade, } from '@metamask/bridge-controller'; import { SnapController } from '@metamask/snaps-controllers'; @@ -20,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 { @@ -73,7 +72,8 @@ export const createClientTransactionRequest = ( * @param srcChainId - The source chain ID * @param accountId - The account ID * @param snapId - The snap ID - * @param destChainId - The destination chain 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 = ( @@ -81,27 +81,27 @@ export const getClientRequest = ( srcChainId: number, accountId: AccountsControllerState['internalAccounts']['accounts'][string]['id'], snapId: string, - destChainId?: number, + sourceAssetId?: CaipAssetType, + destAssetId?: CaipAssetType, ): Parameters[0] => { const scope = formatChainIdToCaip(srcChainId); const transaction = extractTradeData(trade); - let options: Record | undefined; + let options: Record = { + ...(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, }; - } else if (isStellarTrade(trade)) { - // Stellar trades need the source and destination chain IDs to be included as metadata - options = { - sourceChainId: scope, - ...(destChainId !== undefined && { - destChainId: formatChainIdToCaip(destChainId), - }), - }; } return createClientTransactionRequest( @@ -262,7 +262,8 @@ export const handleNonEvmTx = async ( quoteResponse.quote.srcChainId, selectedAccount.id, selectedAccount.metadata?.snap?.id, - quoteResponse.quote.destChainId, + 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 7912a6ab7c..446d78ccce 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, @@ -1670,7 +1671,7 @@ describe('Bridge Status Controller Transaction Utils', () => { createClientRequestSpy.mockRestore(); }); - it('should include Stellar source and destination chain IDs as options when trade is Stellar', () => { + it('should include Stellar source and destination asset IDs as options when trade is not Tron', () => { const stellarTrade = { xdrBase64: 'AAAABg==', } as never; @@ -1682,12 +1683,16 @@ describe('Bridge Status Controller Transaction Utils', () => { }, }; + 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, - ChainId.ETH, + sourceAssetId, + destAssetId, ); expect(result).toMatchObject({ @@ -1703,15 +1708,15 @@ describe('Bridge Status Controller Transaction Utils', () => { scope: formatChainIdToCaip(ChainId.STELLAR), accountId: 'test-account-id', options: { - sourceChainId: formatChainIdToCaip(ChainId.STELLAR), - destChainId: formatChainIdToCaip(ChainId.ETH), + sourceAssetId, + destAssetId, }, }, }, }); }); - it('should omit destChainId option for Stellar trades when destination chain ID is not provided', () => { + it('should omit destAssetId option for Stellar trades when destination asset ID is not provided', () => { const stellarTrade = { xdr: 'AAAABg==', } as never; @@ -1723,25 +1728,28 @@ describe('Bridge Status Controller Transaction Utils', () => { }, }; + 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: { - sourceChainId: formatChainIdToCaip(ChainId.STELLAR), + sourceAssetId, }, }, }, }); expect( (result.request.params as { options: Record }).options, - ).not.toHaveProperty('destChainId'); + ).not.toHaveProperty('destAssetId'); }); }); From bd8bc27ed3d09639291fc9e1ac75fff93af1e7be Mon Sep 17 00:00:00 2001 From: Julien Fontanel Date: Fri, 19 Jun 2026 10:49:48 +0200 Subject: [PATCH 3/3] chore: leave options optional --- .../src/utils/snaps.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/bridge-status-controller/src/utils/snaps.ts b/packages/bridge-status-controller/src/utils/snaps.ts index 2a780bbaa8..58f0e2cb2d 100644 --- a/packages/bridge-status-controller/src/utils/snaps.ts +++ b/packages/bridge-status-controller/src/utils/snaps.ts @@ -88,14 +88,19 @@ export const getClientRequest = ( const transaction = extractTradeData(trade); - let options: Record = { - ...(sourceAssetId !== undefined && { - sourceAssetId, - }), - ...(destAssetId !== undefined && { - destAssetId, - }), - }; + 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 = {