Skip to content
25 changes: 21 additions & 4 deletions packages/swapper/src/swappers/CetusSwapper/endpoints.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Transaction } from '@cetusprotocol/aggregator-sdk/node_modules/@mysten/sui/transactions'
import { getProvidersExcluding } from '@cetusprotocol/aggregator-sdk'
import { TxStatus } from '@shapeshiftoss/unchained-client'
import { bnOrZero } from '@shapeshiftoss/utils'
import type { Result } from '@sniptt/monads'

import { getDefaultSlippageDecimalPercentageForSwapper } from '../../constants'
import type {
CommonTradeQuoteInput,
GetTradeRateInput,
Expand All @@ -14,11 +16,22 @@ import type {
TradeRate,
TradeStatus,
} from '../../types'
import { SwapperName } from '../../types'
import { checkSuiSwapStatus, getExecutableTradeStep, isExecutableTradeQuote } from '../../utils'
import { getTradeQuote } from './swapperApi/getTradeQuote'
import { getTradeRate } from './swapperApi/getTradeRate'
import { findBestRoute, getAggregatorClient, getCoinType, getSuiClient } from './utils/helpers'

// DEX providers that require Pyth oracle price feeds for transaction construction
// These are excluded to avoid failures when Pyth's public endpoint is unavailable
// See: https://cetus-1.gitbook.io/cetus-developer-docs/developer/cetus-aggregator/features-available
// Docs: "Some providers, such as Headalpmm and Metastable, rely on Pyth oracle prices"
const PYTH_DEPENDENT_PROVIDERS = [
'HAEDALPMM', // Haedal PMM (explicitly mentioned in docs)
'HAEDALHMMV2', // Haedal HMM V2 (variant of HAEDALPMM)
'METASTABLE', // Metastable pools (explicitly mentioned in docs)
]

export const cetusApi: SwapperApi = {
getTradeQuote: (
input: CommonTradeQuoteInput,
Expand Down Expand Up @@ -56,21 +69,25 @@ export const cetusApi: SwapperApi = {
const sellCoinType = getCoinType(sellAsset)
const buyCoinType = getCoinType(buyAsset)

// Exclude Pyth-dependent providers to avoid oracle failures
const providersWithoutPyth = getProvidersExcluding(PYTH_DEPENDENT_PROVIDERS)

const routerData = await findBestRoute(
client,
sellCoinType,
buyCoinType,
sellAmountIncludingProtocolFeesCryptoBaseUnit,
providersWithoutPyth,
)

if (!routerData) {
throw new Error(`No route found for ${sellAsset.symbol}/${buyAsset.symbol}`)
}

const slippage =
tradeQuote.slippageTolerancePercentageDecimal !== undefined
? bnOrZero(tradeQuote.slippageTolerancePercentageDecimal).toNumber()
: 0.01
const slippage = bnOrZero(
tradeQuote.slippageTolerancePercentageDecimal ??
getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Cetus),
).toNumber()

const txb = new Transaction()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { RouterDataV3 } from '@cetusprotocol/aggregator-sdk'
import { getProvidersExcluding } from '@cetusprotocol/aggregator-sdk'
Comment thread
coderabbitai[bot] marked this conversation as resolved.
import type { AssetId } from '@shapeshiftoss/caip'
import { suiAssetId } from '@shapeshiftoss/caip'
import type { Asset } from '@shapeshiftoss/types'
Expand All @@ -10,6 +12,16 @@ import { getInputOutputRate, makeSwapErrorRight } from '../../../utils'
import { isSupportedChainId } from '../utils/constants'
import { findBestRoute, getAggregatorClient, getCoinType } from '../utils/helpers'

// DEX providers that require Pyth oracle price feeds for transaction construction
// These are excluded to avoid failures when Pyth's public endpoint is unavailable
// See: https://cetus-1.gitbook.io/cetus-developer-docs/developer/cetus-aggregator/features-available
// Docs: "Some providers, such as Headalpmm and Metastable, rely on Pyth oracle prices"
const PYTH_DEPENDENT_PROVIDERS = [
'HAEDALPMM', // Haedal PMM (explicitly mentioned in docs)
'HAEDALHMMV2', // Haedal HMM V2 (variant of HAEDALPMM)
'METASTABLE', // Metastable pools (explicitly mentioned in docs)
]

type CetusTradeDataInput = {
sellAsset: Asset
buyAsset: Asset
Expand All @@ -22,8 +34,11 @@ type CetusTradeData = {
rate: string
addressForFeeEstimate: string
sellCoinType: string
buyCoinType: string
routerData: RouterDataV3
protocolFees: Record<AssetId, ProtocolFee>
adapter: ReturnType<SwapperDeps['assertGetSuiChainAdapter']>
rpcUrl: string
}

export const getCetusTradeData = async (
Expand Down Expand Up @@ -74,11 +89,15 @@ export const getCetusTradeData = async (
const sellCoinType = getCoinType(sellAsset)
const buyCoinType = getCoinType(buyAsset)

// Exclude Pyth-dependent providers to avoid oracle failures
const providersWithoutPyth = getProvidersExcluding(PYTH_DEPENDENT_PROVIDERS)

const routerData = await findBestRoute(
client,
sellCoinType,
buyCoinType,
sellAmountIncludingProtocolFeesCryptoBaseUnit,
providersWithoutPyth,
)

if (!routerData) {
Expand Down Expand Up @@ -109,8 +128,11 @@ export const getCetusTradeData = async (
rate,
addressForFeeEstimate,
sellCoinType,
buyCoinType,
routerData,
protocolFees,
adapter,
rpcUrl,
})
} catch (error) {
return Err(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { GetFeeDataInput } from '@shapeshiftoss/chain-adapters'
import type { KnownChainIds } from '@shapeshiftoss/types'
import { Transaction } from '@cetusprotocol/aggregator-sdk/node_modules/@mysten/sui/transactions'
import { bnOrZero } from '@shapeshiftoss/utils'
import type { Result } from '@sniptt/monads'
import { Err } from '@sniptt/monads'
import { v4 as uuid } from 'uuid'

import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants'
import type { CommonTradeQuoteInput, SwapErrorRight, SwapperDeps, TradeQuote } from '../../../types'
import { SwapperName, TradeQuoteError } from '../../../types'
import { makeSwapErrorRight } from '../../../utils'
import { getAggregatorClient, getSuiClient } from '../utils/helpers'
import { getCetusTradeData } from './getCetusTradeData'

export const getTradeQuote = async (
Expand Down Expand Up @@ -48,22 +50,48 @@ export const getTradeQuote = async (
buyAmountAfterFeesCryptoBaseUnit,
rate,
addressForFeeEstimate,
sellCoinType,
routerData,
protocolFees,
adapter,
rpcUrl,
} = tradeDataResult.unwrap()

try {
const getFeeDataInput: GetFeeDataInput<KnownChainIds.SuiMainnet> = {
to: addressForFeeEstimate,
value: sellAmount,
chainSpecific: {
from: addressForFeeEstimate,
tokenId: sellCoinType,
},
}
// Build the actual Cetus swap transaction to get accurate gas estimation
const client = getAggregatorClient(rpcUrl)
const suiClient = getSuiClient(rpcUrl)

const slippage = bnOrZero(
slippageTolerancePercentageDecimal ??
getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Cetus),
).toNumber()

const txb = new Transaction()
txb.setSender(addressForFeeEstimate)

await client.fastRouterSwap({
router: routerData,
slippage,
txb,
refreshAllCoins: true,
})

const transactionBytes = await txb.build({ client: suiClient })

const dryRunResult = await suiClient.dryRunTransactionBlock({
transactionBlock: transactionBytes,
})

const computationCost = BigInt(dryRunResult.effects.gasUsed.computationCost)
const storageCost = BigInt(dryRunResult.effects.gasUsed.storageCost)
const storageRebate = BigInt(dryRunResult.effects.gasUsed.storageRebate)

const netStorageCost = storageCost > storageRebate ? storageCost - storageRebate : 0n
const estimatedGas = computationCost + netStorageCost

const txFee = estimatedGas.toString()
const gasBudget = ((estimatedGas * 120n) / 100n).toString()

const feeData = await adapter.getFeeData(getFeeDataInput)
const gasPrice = await suiClient.getReferenceGasPrice()

const tradeQuote: TradeQuote = {
id: uuid(),
Expand All @@ -81,10 +109,10 @@ export const getTradeQuote = async (
sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmount,
feeData: {
protocolFees,
networkFeeCryptoBaseUnit: feeData.fast.txFee,
networkFeeCryptoBaseUnit: txFee,
chainSpecific: {
gasBudget: feeData.fast.chainSpecific.gasBudget,
gasPrice: feeData.fast.chainSpecific.gasPrice,
gasBudget,
gasPrice: gasPrice.toString(),
},
},
rate,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { Transaction } from '@cetusprotocol/aggregator-sdk/node_modules/@mysten/sui/transactions'
import { bnOrZero } from '@shapeshiftoss/utils'
import type { Result } from '@sniptt/monads'
import { Err } from '@sniptt/monads'
import { v4 as uuid } from 'uuid'

import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants'
import type { GetTradeRateInput, SwapErrorRight, SwapperDeps, TradeRate } from '../../../types'
import { SwapperName, TradeQuoteError } from '../../../types'
import { makeSwapErrorRight } from '../../../utils'
import { getAggregatorClient, getSuiClient } from '../utils/helpers'
import { getCetusTradeData } from './getCetusTradeData'

export const getTradeRate = async (
Expand Down Expand Up @@ -36,21 +40,47 @@ export const getTradeRate = async (
buyAmountAfterFeesCryptoBaseUnit,
rate,
addressForFeeEstimate,
sellCoinType,
routerData,
protocolFees,
adapter,
rpcUrl,
} = tradeDataResult.unwrap()

try {
const { fast: feeDataFast } = await adapter.getFeeData({
to: addressForFeeEstimate,
value: sellAmount,
chainSpecific: {
from: addressForFeeEstimate,
tokenId: sellCoinType,
},
// Build the actual Cetus swap transaction to get accurate gas estimation
const client = getAggregatorClient(rpcUrl)
const suiClient = getSuiClient(rpcUrl)

const slippage = bnOrZero(
slippageTolerancePercentageDecimal ??
getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Cetus),
).toNumber()

const txb = new Transaction()
txb.setSender(addressForFeeEstimate)

await client.fastRouterSwap({
router: routerData,
slippage,
txb,
refreshAllCoins: true,
})

const transactionBytes = await txb.build({ client: suiClient })

const dryRunResult = await suiClient.dryRunTransactionBlock({
transactionBlock: transactionBytes,
})

const computationCost = BigInt(dryRunResult.effects.gasUsed.computationCost)
const storageCost = BigInt(dryRunResult.effects.gasUsed.storageCost)
const storageRebate = BigInt(dryRunResult.effects.gasUsed.storageRebate)

const netStorageCost = storageCost > storageRebate ? storageCost - storageRebate : 0n

const estimatedGas = computationCost + netStorageCost

const txFee = estimatedGas.toString()

const tradeRate: TradeRate = {
id: uuid(),
quoteOrRate: 'rate',
Expand All @@ -67,7 +97,7 @@ export const getTradeRate = async (
sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmount,
feeData: {
protocolFees,
networkFeeCryptoBaseUnit: feeDataFast.txFee,
networkFeeCryptoBaseUnit: txFee,
},
rate,
source: SwapperName.Cetus,
Expand Down
2 changes: 2 additions & 0 deletions packages/swapper/src/swappers/CetusSwapper/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,14 @@ export const findBestRoute = async (
sellCoinType: string,
buyCoinType: string,
sellAmountCryptoBaseUnit: string,
providers?: string[],
): Promise<RouterDataV3 | undefined> => {
const routerData = await client.findRouters({
from: sellCoinType,
target: buyCoinType,
amount: sellAmountCryptoBaseUnit,
byAmountIn: true,
...(providers && { providers }),
})

if (!routerData) {
Expand Down