Skip to content

Commit 6af2e81

Browse files
committed
refactor: use cross-chain stream api
1 parent aa152bb commit 6af2e81

File tree

3 files changed

+252
-52
lines changed

3 files changed

+252
-52
lines changed

apps/kyberswap-interface/src/pages/CrossChainSwap/factory.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export class CrossChainSwapFactory {
124124
// CrossChainSwapFactory.getXyFinanceAdapter(),
125125
CrossChainSwapFactory.getNearIntentsAdapter(),
126126
CrossChainSwapFactory.getMayanAdapter(),
127-
// CrossChainSwapFactory.getSymbiosisAdapter(),
127+
CrossChainSwapFactory.getSymbiosisAdapter(),
128128
CrossChainSwapFactory.getDebridgeInstance(),
129129
CrossChainSwapFactory.getLifiInstance(),
130130
CrossChainSwapFactory.getOptimexAdapter(),

apps/kyberswap-interface/src/pages/CrossChainSwap/hooks/useCrossChainSwap.tsx

Lines changed: 249 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,74 @@ import { CrossChainSwapFactory } from '../factory'
3434
import { CrossChainSwapAdapterRegistry, Quote } from '../registry'
3535
import { 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+
3795
export const registry = new CrossChainSwapAdapterRegistry()
3896
CrossChainSwapFactory.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
43106
const createTimeoutPromise = (ms: number) => {
44107
return new Promise<never>((_, reject) => {
@@ -525,8 +588,10 @@ export const CrossChainSwapRegistryProvider = ({ children }: { children: React.R
525588
allAdapters = registry.getAllAdapters()
526589
}
527590

528-
// Create a modified version of getQuotes that can be cancelled
591+
// Build API URL with query parameters
529592
const quotes: Quote[] = []
593+
594+
// Determine which adapters should be used (for filtering later)
530595
const adapters =
531596
params.fromChain === params.toChain &&
532597
isEvmChain(params.fromChain) &&
@@ -541,38 +606,169 @@ export const CrossChainSwapRegistryProvider = ({ children }: { children: React.R
541606
adapter.getSupportedChains().includes(params.fromChain) &&
542607
adapter.getSupportedChains().includes(params.toChain),
543608
)
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) {
609+
610+
// Construct the API URL with parameters
611+
const fromToken = (params.fromToken as any).isNative
612+
? ZERO_ADDRESS
613+
: (params.fromToken as any).wrapped?.address ||
614+
(params.fromToken as any).address ||
615+
(params.fromToken as any).id ||
616+
(params.fromToken as any).assetId
617+
const toToken = (params.toToken as any).isNative
618+
? ZERO_ADDRESS
619+
: (params.toToken as any).wrapped?.address ||
620+
(params.toToken as any).address ||
621+
(params.toToken as any).id ||
622+
(params.toToken as any).assetId
623+
const fromTokenDecimals = params.fromToken.decimals
624+
const toTokenDecimals = params.toToken.decimals
625+
626+
const queryParams = new URLSearchParams({
627+
fromChain: getChainName(params.fromChain),
628+
fromToken,
629+
fromTokenDecimals: fromTokenDecimals.toString(),
630+
fromAddress: params.sender,
631+
fromAmount: params.amount,
632+
toChain: getChainName(params.toChain),
633+
toToken,
634+
toTokenDecimals: toTokenDecimals.toString(),
635+
toAddress: params.recipient,
636+
fee: params.feeBps.toString(),
637+
integrator: 'kyberswap',
638+
stream: 'true',
639+
slippage: params.slippage.toString(),
640+
fromTokenUsd: (params as any).tokenInUsd?.toString() || '0',
641+
toTokenUsd: (params as any).tokenOutUsd?.toString() || '0',
642+
})
643+
644+
const apiUrl = `https://pre-crosschain-aggregator.kyberengineering.io/api/v1/quotes?${queryParams.toString()}`
645+
646+
console.log('Fetching quotes from streaming API:', apiUrl)
647+
648+
try {
649+
// Check for cancellation before starting
650+
if (signal.aborted) throw new Error('Cancelled')
651+
652+
// Fetch with streaming
653+
const response = await fetch(apiUrl, { signal })
654+
655+
console.log('Streaming API response status:', response.status)
656+
657+
if (!response.ok) {
658+
throw new Error(`HTTP error! status: ${response.status}`)
659+
}
660+
661+
const reader = response.body?.getReader()
662+
const decoder = new TextDecoder()
663+
664+
if (!reader) {
665+
throw new Error('No response body reader available')
666+
}
667+
668+
let buffer = ''
669+
670+
// Read the stream
671+
while (true) {
672+
// Check for cancellation
673+
if (signal.aborted) {
674+
reader.cancel()
562675
throw new Error('Cancelled')
563676
}
564-
console.error(`Failed to get quote from ${adapter.getName()}:`, err)
677+
678+
const { done, value } = await reader.read()
679+
680+
if (done) break
681+
682+
buffer += decoder.decode(value, { stream: true })
683+
684+
// Process complete SSE messages
685+
const lines = buffer.split('\n')
686+
buffer = lines.pop() || '' // Keep incomplete line in buffer
687+
688+
for (const line of lines) {
689+
// Skip empty lines and event lines
690+
if (!line || line.startsWith('event:')) {
691+
continue
692+
}
693+
694+
if (line.startsWith('data:')) {
695+
try {
696+
// Handle both "data:{json}" and "data: {json}" formats
697+
const jsonStr = line.startsWith('data: ') ? line.slice(6) : line.slice(5)
698+
const data = JSON.parse(jsonStr)
699+
700+
// Skip complete messages
701+
if (data.message === 'All quotes received') {
702+
console.log('All quotes received from streaming API')
703+
continue
704+
}
705+
706+
// Find the corresponding adapter
707+
console.log('Processing quote from provider:', data.provider)
708+
const adapter = registry.getAdapter(data.provider)
709+
710+
if (adapter) {
711+
console.log('Adapter found:', adapter.getName(), 'Output amount:', data.outputAmount)
712+
// Convert the API response to NormalizedQuote format
713+
// Need to convert chain names back to Chain IDs for the adapters to work correctly
714+
const normalizedQuote = {
715+
quoteParams: {
716+
...data.quoteParams,
717+
fromChain: getChainIdFromName(data.quoteParams.fromChain),
718+
toChain: getChainIdFromName(data.quoteParams.toChain),
719+
},
720+
outputAmount: BigInt(data.outputAmount),
721+
formattedOutputAmount: data.formattedOutputAmount,
722+
inputUsd: data.inputUsd,
723+
outputUsd: data.outputUsd,
724+
rate: data.rate,
725+
timeEstimate: data.timeEstimate,
726+
priceImpact: data.priceImpact,
727+
gasFeeUsd: data.gasFeeUsd,
728+
contractAddress: data.contractAddress,
729+
rawQuote: data.rawQuote,
730+
protocolFee: data.protocolFee,
731+
protocolFeeString: data.protocolFeeString,
732+
platformFeePercent: data.platformFeePercent,
733+
}
734+
735+
quotes.push({ adapter, quote: normalizedQuote })
736+
console.log('Total quotes so far:', quotes.length)
737+
const sortedQuotes = [...quotes].sort((a, b) =>
738+
a.quote.outputAmount < b.quote.outputAmount ? 1 : -1,
739+
)
740+
console.log('Setting quotes state with', sortedQuotes.length, 'quotes')
741+
setQuotes(sortedQuotes)
742+
setLoading(false)
743+
} else {
744+
console.warn(`Adapter not found in registry: ${data.provider}`)
745+
console.log(
746+
'Available adapters:',
747+
registry.getAllAdapters().map(a => a.getName()),
748+
)
749+
}
750+
} catch (err) {
751+
console.error('Failed to parse SSE data:', err)
752+
console.error('Problematic line:', line)
753+
console.error('Line length:', line.length)
754+
}
755+
}
756+
}
565757
}
566-
})
567758

568-
await Promise.all(quotePromises)
759+
if (quotes.length === 0) {
760+
throw new Error('No valid quotes found for the requested swap')
761+
}
569762

570-
if (quotes.length === 0) {
571-
throw new Error('No valid quotes found for the requested swap')
763+
quotes.sort((a, b) => (a.quote.outputAmount < b.quote.outputAmount ? 1 : -1))
764+
return quotes
765+
} catch (err) {
766+
if ((err as Error).message === 'Cancelled' || signal.aborted) {
767+
throw new Error('Cancelled')
768+
}
769+
console.error('Failed to get quotes from streaming API:', err)
770+
throw err
572771
}
573-
574-
quotes.sort((a, b) => (a.quote.outputAmount < b.quote.outputAmount ? 1 : -1))
575-
return quotes
576772
}
577773

578774
const adaptedWallet = adaptSolanaWallet(
@@ -582,28 +778,32 @@ export const CrossChainSwapRegistryProvider = ({ children }: { children: React.R
582778
connection.sendTransaction as any,
583779
)
584780

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)
781+
try {
782+
await getQuotesWithCancellation({
783+
feeBps,
784+
tokenInUsd: tokenInUsd,
785+
tokenOutUsd: tokenOutUsd,
786+
fromChain: fromChainId,
787+
toChain: toChainId,
788+
fromToken: currencyIn,
789+
toToken: currencyOut,
790+
amount: inputAmount,
791+
slippage,
792+
walletClient: fromChainId === 'solana' ? adaptedWallet : walletClient?.data,
793+
sender,
794+
recipient: receiver,
795+
nearTokens,
796+
publicKey: btcPublicKey || '',
797+
})
798+
} catch (e) {
799+
console.error('Error getting quotes:', e)
800+
if ((e as Error).message !== 'Cancelled') {
801+
setQuotes([])
802+
}
803+
} finally {
804+
setLoading(false)
805+
setAllLoading(false)
806+
}
607807
}, [
608808
sender,
609809
receiver,

apps/kyberswap-interface/src/pages/CrossChainSwap/registry.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ export class CrossChainSwapAdapterRegistry {
1414
private adapters: Map<string, SwapProvider> = new Map()
1515

1616
registerAdapter(adapter: SwapProvider): void {
17-
this.adapters.set(adapter.getName().toLowerCase(), adapter)
17+
this.adapters.set(adapter.getName().toLowerCase().replace(/\s+/g, ''), adapter)
1818
}
1919

2020
getAdapter(name: string): SwapProvider | undefined {
21-
return this.adapters.get(name.toLowerCase())
21+
return this.adapters.get(name.toLowerCase().replace(/\s+/g, ''))
2222
}
2323

2424
getAllAdapters(): SwapProvider[] {

0 commit comments

Comments
 (0)