Skip to content

Commit 9547c78

Browse files
authored
feat: enable stable pair for Symbiosis provider (#2850)
* feat: enable stable pair for Symbiosis provider * chore: add partner id * fix: canonical pair should be stable pair
1 parent 30440b9 commit 9547c78

File tree

4 files changed

+77
-8
lines changed

4 files changed

+77
-8
lines changed

apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/BaseSwapAdapter.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ export interface SwapProvider {
142142
connection?: Connection,
143143
): Promise<NormalizedTxResponse>
144144
getTransactionStatus(p: NormalizedTxResponse): Promise<SwapStatus>
145+
canSupport(category: string): boolean
145146
}
146147
export abstract class BaseSwapAdapter implements SwapProvider {
147148
abstract getName(): string
@@ -156,6 +157,11 @@ export abstract class BaseSwapAdapter implements SwapProvider {
156157
): Promise<NormalizedTxResponse>
157158
abstract getTransactionStatus(p: NormalizedTxResponse): Promise<SwapStatus>
158159

160+
canSupport(_category: string): boolean {
161+
// Default implementation - support all categories
162+
return true
163+
}
164+
159165
protected handleError(error: any): never {
160166
console.error(`[${this.getName()}] Error:`, error)
161167
throw new Error(`${this.getName()} provider error: ${error.message || 'Unknown error'}`)

apps/kyberswap-interface/src/pages/CrossChainSwap/adapters/SymbiosisAdapter.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ChainId, Currency } from '@kyberswap/ks-sdk-core'
22
import { WalletClient, formatUnits } from 'viem'
33

4-
import { ZERO_ADDRESS } from 'constants/index'
4+
import { CROSS_CHAIN_FEE_RECEIVER, ZERO_ADDRESS } from 'constants/index'
55

66
import { Quote } from '../registry'
77
import {
@@ -14,6 +14,7 @@ import {
1414
} from './BaseSwapAdapter'
1515

1616
const SYMBIOSIS_API = 'https://api.symbiosis.finance/crosschain/v1'
17+
const KYBERSWAP_PARTNER_ID = 'kyberswap'
1718

1819
export class SymbiosisAdapter extends BaseSwapAdapter {
1920
constructor() {
@@ -26,6 +27,12 @@ export class SymbiosisAdapter extends BaseSwapAdapter {
2627
getIcon(): string {
2728
return 'https://app.symbiosis.finance/images/favicon-32x32.png'
2829
}
30+
31+
canSupport(category: string): boolean {
32+
// Symbiosis should only be used for stablePair category
33+
return category === 'stablePair'
34+
}
35+
2936
getSupportedChains(): Chain[] {
3037
return [
3138
ChainId.MAINNET,
@@ -66,12 +73,14 @@ export class SymbiosisAdapter extends BaseSwapAdapter {
6673
from: params.sender,
6774
to: params.recipient,
6875
slippage: params.slippage,
76+
partnerAddress: CROSS_CHAIN_FEE_RECEIVER,
6977
}
7078

7179
const res = await fetch(`${SYMBIOSIS_API}/swap`, {
7280
method: 'POST',
7381
headers: {
7482
'Content-Type': 'application/json',
83+
'X-Partner-Id': KYBERSWAP_PARTNER_ID,
7584
},
7685
body: JSON.stringify(body),
7786
}).then(r => r.json())
@@ -87,6 +96,22 @@ export class SymbiosisAdapter extends BaseSwapAdapter {
8796
const inputUsd = tokenInUsd * +formattedInputAmount
8897
const outputUsd = tokenOutUsd * +formattedOutputAmount
8998

99+
// Calculate protocol fee from the fees array
100+
const protocolFee = (res.fees || []).reduce(
101+
(
102+
total: number,
103+
fee: {
104+
value: { amount: string; decimals: number; priceUsd: number }
105+
},
106+
) => {
107+
const { amount, decimals, priceUsd } = fee.value
108+
const feeAmount = Number(amount) / Math.pow(10, decimals)
109+
const feeUsd = feeAmount * priceUsd
110+
return total + feeUsd
111+
},
112+
0,
113+
)
114+
90115
return {
91116
quoteParams: params,
92117
outputAmount: BigInt(res.tokenAmountOut.amount),
@@ -99,10 +124,8 @@ export class SymbiosisAdapter extends BaseSwapAdapter {
99124
timeEstimate: res.estimatedTime,
100125
contractAddress: res.approveTo || ZERO_ADDRESS,
101126
rawQuote: res,
102-
103-
// TODO: add Fee
104-
protocolFee: 0,
105-
platformFeePercent: 0,
127+
protocolFee,
128+
platformFeePercent: (params.feeBps * 100) / 10_000,
106129
}
107130
}
108131

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: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
NOT_SUPPORTED_CHAINS_PRICE_SERVICE,
2828
NearQuoteParams,
2929
NonEvmChain,
30+
NormalizedQuote,
3031
QuoteParams,
3132
SwapProvider,
3233
} from '../adapters'
@@ -46,6 +47,28 @@ const createTimeoutPromise = (ms: number) => {
4647
})
4748
}
4849

50+
// Helper to calculate net output amount after protocol fees
51+
const getNetOutputAmount = (quote: NormalizedQuote): bigint => {
52+
const { outputAmount, protocolFee, quoteParams } = quote
53+
const { tokenOutUsd, toToken } = quoteParams
54+
55+
// Convert protocol fee from USD to token amount
56+
if (protocolFee && tokenOutUsd && tokenOutUsd > 0) {
57+
const decimals = toToken?.decimals || 18
58+
const protocolFeeInTokens = protocolFee / tokenOutUsd
59+
60+
// Use parseUnits to safely convert decimal to BigInt without precision loss
61+
try {
62+
const protocolFeeInSmallestUnit = parseUnits(protocolFeeInTokens.toFixed(decimals), decimals)
63+
return outputAmount - protocolFeeInSmallestUnit
64+
} catch (e) {
65+
console.error('Error converting protocol fee:', e)
66+
return outputAmount
67+
}
68+
}
69+
return outputAmount
70+
}
71+
4972
const RegistryContext = createContext<
5073
| {
5174
showPreview: boolean
@@ -446,6 +469,7 @@ export const CrossChainSwapRegistryProvider = ({ children }: { children: React.R
446469
(currencyOut as any).wrapped.address,
447470
)
448471
) {
472+
setCategory('stablePair')
449473
feeBps = 5
450474
} else {
451475
const [token0Cat, token1Cat] = await Promise.all([
@@ -547,14 +571,24 @@ export const CrossChainSwapRegistryProvider = ({ children }: { children: React.R
547571
// Check for cancellation before starting
548572
if (signal.aborted) throw new Error('Cancelled')
549573

574+
// Skip adapter if it does not support the category
575+
if (!adapter.canSupport(category)) {
576+
console.log(`Skipping ${adapter.getName()} because it does not support category ${category}`)
577+
return
578+
}
579+
550580
// Race between the adapter quote and timeout
551581
const quote = await Promise.race([adapter.getQuote(params), createTimeoutPromise(9_000)])
552582

553583
// Check for cancellation after getting quote
554584
if (signal.aborted) throw new Error('Cancelled')
555585

556586
quotes.push({ adapter, quote })
557-
const sortedQuotes = [...quotes].sort((a, b) => (a.quote.outputAmount < b.quote.outputAmount ? 1 : -1))
587+
const sortedQuotes = [...quotes].sort((a, b) => {
588+
const netA = getNetOutputAmount(a.quote)
589+
const netB = getNetOutputAmount(b.quote)
590+
return netA < netB ? 1 : -1
591+
})
558592
setQuotes(sortedQuotes)
559593
setLoading(false)
560594
} catch (err) {
@@ -571,7 +605,12 @@ export const CrossChainSwapRegistryProvider = ({ children }: { children: React.R
571605
throw new Error('No valid quotes found for the requested swap')
572606
}
573607

574-
quotes.sort((a, b) => (a.quote.outputAmount < b.quote.outputAmount ? 1 : -1))
608+
// Sort by net output amount (after protocol fees)
609+
quotes.sort((a, b) => {
610+
const netA = getNetOutputAmount(a.quote)
611+
const netB = getNetOutputAmount(b.quote)
612+
return netA < netB ? 1 : -1
613+
})
575614
return quotes
576615
}
577616

@@ -628,6 +667,7 @@ export const CrossChainSwapRegistryProvider = ({ children }: { children: React.R
628667
isToSolana,
629668
connection,
630669
excludedSources,
670+
category,
631671
])
632672

633673
return (

0 commit comments

Comments
 (0)