@@ -34,11 +34,74 @@ import { CrossChainSwapFactory } from '../factory'
3434import { CrossChainSwapAdapterRegistry , Quote } from '../registry'
3535import { NEAR_STABLE_COINS , SOLANA_STABLE_COINS , isCanonicalPair } from '../utils'
3636
37+ // Mapping from ChainId/NonEvmChain to aggregator API chain names
38+ const CHAIN_TO_API_NAME : Partial < Record < Chain , string > > = {
39+ [ NonEvmChain . Bitcoin ] : 'bitcoin' ,
40+ [ NonEvmChain . Near ] : 'near' ,
41+ [ NonEvmChain . Solana ] : 'solana' ,
42+ [ ChainId . MAINNET ] : 'ethereum' ,
43+ [ ChainId . BASE ] : 'base' ,
44+ [ ChainId . ARBITRUM ] : 'arbitrum' ,
45+ [ ChainId . OPTIMISM ] : 'optimism' ,
46+ [ ChainId . MATIC ] : 'polygon' ,
47+ [ ChainId . BSCMAINNET ] : 'bsc' ,
48+ [ ChainId . AVAXMAINNET ] : 'avalanche' ,
49+ [ ChainId . LINEA ] : 'linea' ,
50+ [ ChainId . SCROLL ] : 'scroll' ,
51+ [ ChainId . BLAST ] : 'blast' ,
52+ [ ChainId . ZKSYNC ] : 'zksync' ,
53+ [ ChainId . SONIC ] : 'sonic' ,
54+ [ ChainId . FANTOM ] : 'fantom' ,
55+ [ ChainId . BERA ] : 'bera' ,
56+ [ ChainId . MANTLE ] : 'mantle' ,
57+ [ ChainId . RONIN ] : 'ronin' ,
58+ [ ChainId . UNICHAIN ] : 'unichain' ,
59+ [ ChainId . HYPEREVM ] : 'hyperevm' ,
60+ }
61+
62+ // Reverse mapping from API chain names to ChainId/NonEvmChain
63+ export const API_NAME_TO_CHAIN : Record < string , Chain > = {
64+ bitcoin : NonEvmChain . Bitcoin ,
65+ near : NonEvmChain . Near ,
66+ solana : NonEvmChain . Solana ,
67+ ethereum : ChainId . MAINNET ,
68+ base : ChainId . BASE ,
69+ arbitrum : ChainId . ARBITRUM ,
70+ optimism : ChainId . OPTIMISM ,
71+ polygon : ChainId . MATIC ,
72+ bsc : ChainId . BSCMAINNET ,
73+ avalanche : ChainId . AVAXMAINNET ,
74+ linea : ChainId . LINEA ,
75+ scroll : ChainId . SCROLL ,
76+ blast : ChainId . BLAST ,
77+ zksync : ChainId . ZKSYNC ,
78+ sonic : ChainId . SONIC ,
79+ fantom : ChainId . FANTOM ,
80+ bera : ChainId . BERA ,
81+ mantle : ChainId . MANTLE ,
82+ ronin : ChainId . RONIN ,
83+ unichain : ChainId . UNICHAIN ,
84+ hyperevm : ChainId . HYPEREVM ,
85+ }
86+
87+ const getChainName = ( chain : Chain ) : string => {
88+ return CHAIN_TO_API_NAME [ chain ] || chain . toString ( )
89+ }
90+
91+ export const getChainIdFromName = ( chainName : string ) : Chain => {
92+ return API_NAME_TO_CHAIN [ chainName . toLowerCase ( ) ] || chainName
93+ }
94+
3795export const registry = new CrossChainSwapAdapterRegistry ( )
3896CrossChainSwapFactory . getAllAdapters ( ) . forEach ( adapter => {
3997 registry . registerAdapter ( adapter )
4098} )
4199
100+ console . log (
101+ 'Registered adapters:' ,
102+ registry . getAllAdapters ( ) . map ( a => a . getName ( ) ) ,
103+ )
104+
42105// Helper function to create a timeout promise
43106const createTimeoutPromise = ( ms : number ) => {
44107 return new Promise < never > ( ( _ , reject ) => {
@@ -516,6 +579,7 @@ export const CrossChainSwapRegistryProvider = ({ children }: { children: React.R
516579
517580 setLoading ( true )
518581 setAllLoading ( true )
582+ setQuotes ( [ ] ) // Clear previous quotes
519583
520584 const getQuotesWithCancellation = async ( params : QuoteParams | NearQuoteParams ) => {
521585 let allAdapters = registry . getAllAdapters ( ) . filter ( a => ! excludedSources . includes ( a . getName ( ) ) )
@@ -525,8 +589,10 @@ export const CrossChainSwapRegistryProvider = ({ children }: { children: React.R
525589 allAdapters = registry . getAllAdapters ( )
526590 }
527591
528- // Create a modified version of getQuotes that can be cancelled
592+ // Build API URL with query parameters
529593 const quotes : Quote [ ] = [ ]
594+
595+ // Determine which adapters should be used (for filtering later)
530596 const adapters =
531597 params . fromChain === params . toChain &&
532598 isEvmChain ( params . fromChain ) &&
@@ -541,38 +607,169 @@ export const CrossChainSwapRegistryProvider = ({ children }: { children: React.R
541607 adapter . getSupportedChains ( ) . includes ( params . fromChain ) &&
542608 adapter . getSupportedChains ( ) . includes ( params . toChain ) ,
543609 )
544- // Map each adapter to a promise that can be cancelled
545- const quotePromises = adapters . map ( async adapter => {
546- try {
547- // Check for cancellation before starting
548- if ( signal . aborted ) throw new Error ( 'Cancelled' )
549-
550- // Race between the adapter quote and timeout
551- const quote = await Promise . race ( [ adapter . getQuote ( params ) , createTimeoutPromise ( 9_000 ) ] )
552-
553- // Check for cancellation after getting quote
554- if ( signal . aborted ) throw new Error ( 'Cancelled' )
555-
556- quotes . push ( { adapter, quote } )
557- const sortedQuotes = [ ...quotes ] . sort ( ( a , b ) => ( a . quote . outputAmount < b . quote . outputAmount ? 1 : - 1 ) )
558- setQuotes ( sortedQuotes )
559- setLoading ( false )
560- } catch ( err ) {
561- if ( err . message === 'Cancelled' || signal . aborted ) {
610+
611+ // Construct the API URL with parameters
612+ const fromToken = ( params . fromToken as any ) . isNative
613+ ? ZERO_ADDRESS
614+ : ( params . fromToken as any ) . wrapped ?. address ||
615+ ( params . fromToken as any ) . address ||
616+ ( params . fromToken as any ) . id ||
617+ ( params . fromToken as any ) . assetId
618+ const toToken = ( params . toToken as any ) . isNative
619+ ? ZERO_ADDRESS
620+ : ( params . toToken as any ) . wrapped ?. address ||
621+ ( params . toToken as any ) . address ||
622+ ( params . toToken as any ) . id ||
623+ ( params . toToken as any ) . assetId
624+ const fromTokenDecimals = params . fromToken . decimals
625+ const toTokenDecimals = params . toToken . decimals
626+
627+ const queryParams = new URLSearchParams ( {
628+ fromChain : getChainName ( params . fromChain ) ,
629+ fromToken,
630+ fromTokenDecimals : fromTokenDecimals . toString ( ) ,
631+ fromAddress : params . sender ,
632+ fromAmount : params . amount ,
633+ toChain : getChainName ( params . toChain ) ,
634+ toToken,
635+ toTokenDecimals : toTokenDecimals . toString ( ) ,
636+ toAddress : params . recipient ,
637+ fee : params . feeBps . toString ( ) ,
638+ integrator : 'kyberswap' ,
639+ stream : 'true' ,
640+ slippage : params . slippage . toString ( ) ,
641+ fromTokenUsd : ( params as any ) . tokenInUsd ?. toString ( ) || '0' ,
642+ toTokenUsd : ( params as any ) . tokenOutUsd ?. toString ( ) || '0' ,
643+ } )
644+
645+ const apiUrl = `https://pre-crosschain-aggregator.kyberengineering.io/api/v1/quotes?${ queryParams . toString ( ) } `
646+
647+ console . log ( 'Fetching quotes from streaming API:' , apiUrl )
648+
649+ try {
650+ // Check for cancellation before starting
651+ if ( signal . aborted ) throw new Error ( 'Cancelled' )
652+
653+ // Fetch with streaming
654+ const response = await fetch ( apiUrl , { signal } )
655+
656+ console . log ( 'Streaming API response status:' , response . status )
657+
658+ if ( ! response . ok ) {
659+ throw new Error ( `HTTP error! status: ${ response . status } ` )
660+ }
661+
662+ const reader = response . body ?. getReader ( )
663+ const decoder = new TextDecoder ( )
664+
665+ if ( ! reader ) {
666+ throw new Error ( 'No response body reader available' )
667+ }
668+
669+ let buffer = ''
670+
671+ // Read the stream
672+ while ( true ) {
673+ // Check for cancellation
674+ if ( signal . aborted ) {
675+ reader . cancel ( )
562676 throw new Error ( 'Cancelled' )
563677 }
564- console . error ( `Failed to get quote from ${ adapter . getName ( ) } :` , err )
678+
679+ const { done, value } = await reader . read ( )
680+
681+ if ( done ) break
682+
683+ buffer += decoder . decode ( value , { stream : true } )
684+
685+ // Process complete SSE messages
686+ const lines = buffer . split ( '\n' )
687+ buffer = lines . pop ( ) || '' // Keep incomplete line in buffer
688+
689+ for ( const line of lines ) {
690+ // Skip empty lines and event lines
691+ if ( ! line || line . startsWith ( 'event:' ) ) {
692+ continue
693+ }
694+
695+ if ( line . startsWith ( 'data:' ) ) {
696+ try {
697+ // Handle both "data:{json}" and "data: {json}" formats
698+ const jsonStr = line . startsWith ( 'data: ' ) ? line . slice ( 6 ) : line . slice ( 5 )
699+ const data = JSON . parse ( jsonStr )
700+
701+ // Skip complete messages
702+ if ( data . message === 'All quotes received' ) {
703+ console . log ( 'All quotes received from streaming API' )
704+ continue
705+ }
706+
707+ // Find the corresponding adapter
708+ console . log ( 'Processing quote from provider:' , data . provider )
709+ const adapter = registry . getAdapter ( data . provider )
710+
711+ if ( adapter ) {
712+ console . log ( 'Adapter found:' , adapter . getName ( ) , 'Output amount:' , data . outputAmount )
713+ // Convert the API response to NormalizedQuote format
714+ // Need to convert chain names back to Chain IDs for the adapters to work correctly
715+ const normalizedQuote = {
716+ quoteParams : {
717+ ...data . quoteParams ,
718+ fromChain : getChainIdFromName ( data . quoteParams . fromChain ) ,
719+ toChain : getChainIdFromName ( data . quoteParams . toChain ) ,
720+ } ,
721+ outputAmount : BigInt ( data . outputAmount ) ,
722+ formattedOutputAmount : data . formattedOutputAmount ,
723+ inputUsd : data . inputUsd ,
724+ outputUsd : data . outputUsd ,
725+ rate : data . rate ,
726+ timeEstimate : data . timeEstimate ,
727+ priceImpact : data . priceImpact ,
728+ gasFeeUsd : data . gasFeeUsd ,
729+ contractAddress : data . contractAddress ,
730+ rawQuote : data . rawQuote ,
731+ protocolFee : 0 , // Protocol fee is included in the quote
732+ protocolFeeString : data . protocolFeeString ,
733+ platformFeePercent : data . platformFeePercent ,
734+ }
735+
736+ quotes . push ( { adapter, quote : normalizedQuote } )
737+ console . log ( 'Total quotes so far:' , quotes . length )
738+ const sortedQuotes = [ ...quotes ] . sort ( ( a , b ) =>
739+ a . quote . outputAmount < b . quote . outputAmount ? 1 : - 1 ,
740+ )
741+ console . log ( 'Setting quotes state with' , sortedQuotes . length , 'quotes' )
742+ setQuotes ( sortedQuotes )
743+ setLoading ( false )
744+ } else {
745+ console . warn ( `Adapter not found in registry: ${ data . provider } ` )
746+ console . log (
747+ 'Available adapters:' ,
748+ registry . getAllAdapters ( ) . map ( a => a . getName ( ) ) ,
749+ )
750+ }
751+ } catch ( err ) {
752+ console . error ( 'Failed to parse SSE data:' , err )
753+ console . error ( 'Problematic line:' , line )
754+ console . error ( 'Line length:' , line . length )
755+ }
756+ }
757+ }
565758 }
566- } )
567759
568- await Promise . all ( quotePromises )
760+ if ( quotes . length === 0 ) {
761+ throw new Error ( 'No valid quotes found for the requested swap' )
762+ }
569763
570- if ( quotes . length === 0 ) {
571- throw new Error ( 'No valid quotes found for the requested swap' )
764+ quotes . sort ( ( a , b ) => ( a . quote . outputAmount < b . quote . outputAmount ? 1 : - 1 ) )
765+ return quotes
766+ } catch ( err ) {
767+ if ( ( err as Error ) . message === 'Cancelled' || signal . aborted ) {
768+ throw new Error ( 'Cancelled' )
769+ }
770+ console . error ( 'Failed to get quotes from streaming API:' , err )
771+ throw err
572772 }
573-
574- quotes . sort ( ( a , b ) => ( a . quote . outputAmount < b . quote . outputAmount ? 1 : - 1 ) )
575- return quotes
576773 }
577774
578775 const adaptedWallet = adaptSolanaWallet (
@@ -582,28 +779,32 @@ export const CrossChainSwapRegistryProvider = ({ children }: { children: React.R
582779 connection . sendTransaction as any ,
583780 )
584781
585- const q = await getQuotesWithCancellation ( {
586- feeBps,
587- tokenInUsd : tokenInUsd ,
588- tokenOutUsd : tokenOutUsd ,
589- fromChain : fromChainId ,
590- toChain : toChainId ,
591- fromToken : currencyIn ,
592- toToken : currencyOut ,
593- amount : inputAmount ,
594- slippage,
595- walletClient : fromChainId === 'solana' ? adaptedWallet : walletClient ?. data ,
596- sender,
597- recipient : receiver ,
598- nearTokens,
599- publicKey : btcPublicKey || '' ,
600- } ) . catch ( e => {
601- console . log ( e )
602- return [ ]
603- } )
604- setQuotes ( q )
605- setLoading ( false )
606- setAllLoading ( false )
782+ try {
783+ await getQuotesWithCancellation ( {
784+ feeBps,
785+ tokenInUsd : tokenInUsd ,
786+ tokenOutUsd : tokenOutUsd ,
787+ fromChain : fromChainId ,
788+ toChain : toChainId ,
789+ fromToken : currencyIn ,
790+ toToken : currencyOut ,
791+ amount : inputAmount ,
792+ slippage,
793+ walletClient : fromChainId === 'solana' ? adaptedWallet : walletClient ?. data ,
794+ sender,
795+ recipient : receiver ,
796+ nearTokens,
797+ publicKey : btcPublicKey || '' ,
798+ } )
799+ } catch ( e ) {
800+ console . error ( 'Error getting quotes:' , e )
801+ if ( ( e as Error ) . message !== 'Cancelled' ) {
802+ setQuotes ( [ ] )
803+ }
804+ } finally {
805+ setLoading ( false )
806+ setAllLoading ( false )
807+ }
607808 } , [
608809 sender ,
609810 receiver ,
0 commit comments