diff --git a/.changeset/forty-beers-carry.md b/.changeset/forty-beers-carry.md
new file mode 100644
index 0000000000000..408161d59f4e6
--- /dev/null
+++ b/.changeset/forty-beers-carry.md
@@ -0,0 +1,5 @@
+---
+'@pancakeswap/swap-sdk-core': minor
+---
+
+Add currency address getter
diff --git a/.changeset/seven-adults-switch.md b/.changeset/seven-adults-switch.md
new file mode 100644
index 0000000000000..1419c54fe03be
--- /dev/null
+++ b/.changeset/seven-adults-switch.md
@@ -0,0 +1,5 @@
+---
+'@pancakeswap/swap-sdk-core': minor
+---
+
+Support price wrapping
diff --git a/.changeset/tame-elephants-lick.md b/.changeset/tame-elephants-lick.md
new file mode 100644
index 0000000000000..1b1a9a68f662b
--- /dev/null
+++ b/.changeset/tame-elephants-lick.md
@@ -0,0 +1,6 @@
+---
+'@pancakeswap/price-api-sdk': minor
+'@pancakeswap/smart-router': minor
+---
+
+Add v4 pool types
diff --git a/.changeset/wicked-bobcats-play.md b/.changeset/wicked-bobcats-play.md
new file mode 100644
index 0000000000000..43e5bbcb5032b
--- /dev/null
+++ b/.changeset/wicked-bobcats-play.md
@@ -0,0 +1,6 @@
+---
+'@pancakeswap/price-api-sdk': minor
+'@pancakeswap/smart-router': minor
+---
+
+Introduce v4 liquidity pools
diff --git a/apps/web/src/hooks/useV4BinPools.ts b/apps/web/src/hooks/useV4BinPools.ts
new file mode 100644
index 0000000000000..77b377be4219e
--- /dev/null
+++ b/apps/web/src/hooks/useV4BinPools.ts
@@ -0,0 +1,73 @@
+import { Currency, getCurrencyAddress, sortCurrencies } from '@pancakeswap/swap-sdk-core'
+import { V4Router } from '@pancakeswap/smart-router'
+import { useQuery } from '@tanstack/react-query'
+import { useMemo } from 'react'
+
+import { POOLS_FAST_REVALIDATE } from 'config/pools'
+import { getViemClients } from 'utils/viem'
+
+export type V4PoolsParams = {
+ // Used for caching
+ key?: string
+ blockNumber?: number
+ enabled?: boolean
+}
+
+export function useV4BinCandidatePoolsWithoutBins(currencyA?: Currency, currencyB?: Currency, options?: V4PoolsParams) {
+ const key = useMemo(() => {
+ if (
+ !currencyA ||
+ !currencyB ||
+ currencyA.chainId !== currencyB.chainId ||
+ currencyA.wrapped.equals(currencyB.wrapped)
+ ) {
+ return ''
+ }
+ const [currency0, currency1] = sortCurrencies([currencyA, currencyB])
+ return [
+ currency0.isNative,
+ getCurrencyAddress(currency0),
+ currency1.isNative,
+ getCurrencyAddress(currency1),
+ currency0.chainId,
+ ].join('_')
+ }, [currencyA, currencyB])
+
+ const refetchInterval = useMemo(() => {
+ if (!currencyA?.chainId) {
+ return 0
+ }
+ return POOLS_FAST_REVALIDATE[currencyA.chainId] || 0
+ }, [currencyA?.chainId])
+
+ const { data, refetch, isPending, isFetching, error } = useQuery({
+ queryKey: ['v4_bin_candidate_pools_without_bins', key],
+ queryFn: async () => {
+ const pools = await V4Router.getV4BinCandidatePools({
+ currencyA,
+ currencyB,
+ clientProvider: getViemClients,
+ })
+ return {
+ key,
+ pools,
+ blockNumber: options?.blockNumber,
+ }
+ },
+ retry: 2,
+ staleTime: refetchInterval,
+ refetchInterval,
+ refetchOnWindowFocus: false,
+ enabled: Boolean(currencyA && currencyB && key && options?.enabled),
+ })
+
+ return {
+ refresh: refetch,
+ pools: data?.pools,
+ loading: isPending,
+ syncing: isFetching,
+ blockNumber: data?.blockNumber,
+ key: data?.key,
+ error,
+ }
+}
diff --git a/apps/web/src/hooks/useV4ClPools.ts b/apps/web/src/hooks/useV4ClPools.ts
new file mode 100644
index 0000000000000..20d2a111c3c1d
--- /dev/null
+++ b/apps/web/src/hooks/useV4ClPools.ts
@@ -0,0 +1,73 @@
+import { Currency, getCurrencyAddress, sortCurrencies } from '@pancakeswap/swap-sdk-core'
+import { V4Router } from '@pancakeswap/smart-router'
+import { useQuery } from '@tanstack/react-query'
+import { useMemo } from 'react'
+
+import { POOLS_FAST_REVALIDATE } from 'config/pools'
+import { getViemClients } from 'utils/viem'
+
+export type V4PoolsParams = {
+ // Used for caching
+ key?: string
+ blockNumber?: number
+ enabled?: boolean
+}
+
+export function useV4CandidateClPoolsWithoutTicks(currencyA?: Currency, currencyB?: Currency, options?: V4PoolsParams) {
+ const key = useMemo(() => {
+ if (
+ !currencyA ||
+ !currencyB ||
+ currencyA.chainId !== currencyB.chainId ||
+ currencyA.wrapped.equals(currencyB.wrapped)
+ ) {
+ return ''
+ }
+ const [currency0, currency1] = sortCurrencies([currencyA, currencyB])
+ return [
+ currency0.isNative,
+ getCurrencyAddress(currency0),
+ currency1.isNative,
+ getCurrencyAddress(currency1),
+ currency0.chainId,
+ ].join('_')
+ }, [currencyA, currencyB])
+
+ const refetchInterval = useMemo(() => {
+ if (!currencyA?.chainId) {
+ return 0
+ }
+ return POOLS_FAST_REVALIDATE[currencyA.chainId] || 0
+ }, [currencyA?.chainId])
+
+ const { data, refetch, isPending, isFetching, error } = useQuery({
+ queryKey: ['v4_cl_candidate_pools_without_ticks', key],
+ queryFn: async () => {
+ const pools = await V4Router.getV4ClCandidatePools({
+ currencyA,
+ currencyB,
+ clientProvider: getViemClients,
+ })
+ return {
+ key,
+ pools,
+ blockNumber: options?.blockNumber,
+ }
+ },
+ retry: 2,
+ staleTime: refetchInterval,
+ refetchInterval,
+ refetchOnWindowFocus: false,
+ enabled: Boolean(currencyA && currencyB && key && options?.enabled),
+ })
+
+ return {
+ refresh: refetch,
+ pools: data?.pools,
+ loading: isPending,
+ syncing: isFetching,
+ blockNumber: data?.blockNumber,
+ key: data?.key,
+ error,
+ }
+}
diff --git a/apps/web/src/utils/convertTrade.ts b/apps/web/src/utils/convertTrade.ts
index acfc3daba3019..c009896b2d496 100644
--- a/apps/web/src/utils/convertTrade.ts
+++ b/apps/web/src/utils/convertTrade.ts
@@ -18,7 +18,10 @@ export function toRoutingSDKPool(p: SmartRouterPool): Pool {
if (SmartRouter.isV2Pool(p)) {
return createV2Pool(p)
}
- return createStablePool(p)
+ if (SmartRouter.isStablePool(p)) {
+ return createStablePool(p)
+ }
+ throw new Error(`Unsupported pool type: ${p.type}`)
}
export function toSmartRouterPool(p: any): SmartRouterPool {
diff --git a/apps/web/src/views/Swap/V3Swap/components/RouteDisplayModal.tsx b/apps/web/src/views/Swap/V3Swap/components/RouteDisplayModal.tsx
index 27fb23dfd9fdb..ed1aa6e16c443 100644
--- a/apps/web/src/views/Swap/V3Swap/components/RouteDisplayModal.tsx
+++ b/apps/web/src/views/Swap/V3Swap/components/RouteDisplayModal.tsx
@@ -95,23 +95,36 @@ export const RouteDisplay = memo(function RouteDisplay({ route }: RouteDisplayPr
? pairs.map((p, index) => {
const [input, output] = p
const pool = pools[index]
+ const isV4ClPool = SmartRouter.isV4ClPool(pool)
+ const isV4BinPool = SmartRouter.isV4BinPool(pool)
+ const isV4Pool = isV4BinPool || isV4ClPool
const isV3Pool = SmartRouter.isV3Pool(pool)
const isV2Pool = SmartRouter.isV2Pool(pool)
- const key = isV2Pool ? `v2_${pool.reserve0.currency.symbol}_${pool.reserve1.currency.symbol}` : pool.address
+ const key = isV2Pool
+ ? `v2_${pool.reserve0.currency.symbol}_${pool.reserve1.currency.symbol}`
+ : SmartRouter.isStablePool(pool) || isV3Pool
+ ? pool.address
+ : isV4Pool
+ ? pool.id
+ : undefined
+ if (!key) return null
+ const feeDisplay = isV3Pool || isV4Pool ? v3FeeToPercent(pool.fee).toSignificant(6) : ''
const text = isV2Pool
? 'V2'
: isV3Pool
- ? `V3 (${v3FeeToPercent(pool.fee).toSignificant(6)}%)`
+ ? `V3 (${feeDisplay}%)`
+ : isV4ClPool
+ ? `V4CL (${feeDisplay}%)`
+ : isV4BinPool
+ ? `V4Bin (${feeDisplay}%)`
: t('StableSwap')
- const tooltipText = `${input.symbol}/${output.symbol}${
- isV3Pool ? ` (${v3FeeToPercent(pool.fee).toSignificant(6)}%)` : ''
- }`
+ const tooltipText = `${input.symbol}/${output.symbol}${isV3Pool || isV4Pool ? ` (${feeDisplay}%)` : ''}`
return (
)
diff --git a/apps/web/src/views/Swap/V3Swap/utils/exchange.ts b/apps/web/src/views/Swap/V3Swap/utils/exchange.ts
index a8abcfe0cf8e1..46f9513156790 100644
--- a/apps/web/src/views/Swap/V3Swap/utils/exchange.ts
+++ b/apps/web/src/views/Swap/V3Swap/utils/exchange.ts
@@ -6,7 +6,6 @@ import {
ONE_HUNDRED_PERCENT,
Percent,
Price,
- Token,
TradeType,
ZERO,
} from '@pancakeswap/sdk'
@@ -61,7 +60,7 @@ export function computeTradePriceBreakdown(trade?: TradeEssentialForPriceBreakdo
const { routes, outputAmount, inputAmount } = trade
let feePercent = new Percent(0)
- let outputAmountWithoutPriceImpact = CurrencyAmount.fromRawAmount(trade.outputAmount.wrapped.currency, 0)
+ let outputAmountWithoutPriceImpact = CurrencyAmount.fromRawAmount(trade.outputAmount.currency, 0)
for (const route of routes) {
const { inputAmount: routeInputAmount, pools, percent } = route
const routeFeePercent = ONE_HUNDRED_PERCENT.subtract(
@@ -85,7 +84,10 @@ export function computeTradePriceBreakdown(trade?: TradeEssentialForPriceBreakdo
const midPrice = SmartRouter.getMidPrice(route)
outputAmountWithoutPriceImpact = outputAmountWithoutPriceImpact.add(
- midPrice.quote(routeInputAmount.wrapped) as CurrencyAmount,
+ CurrencyAmount.fromRawAmount(
+ trade.outputAmount.currency,
+ midPrice.wrapped.quote(routeInputAmount.wrapped).quotient,
+ ),
)
}
@@ -96,9 +98,7 @@ export function computeTradePriceBreakdown(trade?: TradeEssentialForPriceBreakdo
}
}
- const priceImpactRaw = outputAmountWithoutPriceImpact
- .subtract(outputAmount.wrapped)
- .divide(outputAmountWithoutPriceImpact)
+ const priceImpactRaw = outputAmountWithoutPriceImpact.subtract(outputAmount).divide(outputAmountWithoutPriceImpact)
const priceImpactPercent = new Percent(priceImpactRaw.numerator, priceImpactRaw.denominator)
const priceImpactWithoutFee = priceImpactPercent.subtract(feePercent)
const lpFeeAmount = inputAmount.multiply(feePercent)
diff --git a/packages/price-api-sdk/src/types/amm.ts b/packages/price-api-sdk/src/types/amm.ts
index d1d71bb2359a2..452418be41048 100644
--- a/packages/price-api-sdk/src/types/amm.ts
+++ b/packages/price-api-sdk/src/types/amm.ts
@@ -28,7 +28,7 @@ export type AMMOrder = V4Router.Transformer.SerializedV4Trade & {
}
export type AMMRequestConfig = {
- protocols: ('V2' | 'V3' | 'STABLE')[]
+ protocols: PoolTypeKey[]
routingType: OrderType.PCS_CLASSIC
gasPriceWei?: string
maxHops?: number
diff --git a/packages/smart-router/evm/abis/IBinPoolManager.ts b/packages/smart-router/evm/abis/IBinPoolManager.ts
new file mode 100644
index 0000000000000..08fe441f13e6b
--- /dev/null
+++ b/packages/smart-router/evm/abis/IBinPoolManager.ts
@@ -0,0 +1,586 @@
+export const binPoolManagerAbi = [
+ {
+ inputs: [{ internalType: 'contract IVault', name: 'vault', type: 'address' }],
+ stateMutability: 'nonpayable',
+ type: 'constructor',
+ },
+ { inputs: [], name: 'AmountSpecifiedIsZero', type: 'error' },
+ {
+ inputs: [{ internalType: 'uint24', name: 'id', type: 'uint24' }],
+ name: 'BinHelper__CompositionFactorFlawed',
+ type: 'error',
+ },
+ { inputs: [{ internalType: 'uint24', name: 'id', type: 'uint24' }], name: 'BinPool__BurnZeroAmount', type: 'error' },
+ { inputs: [], name: 'BinPool__EmptyLiquidityConfigs', type: 'error' },
+ { inputs: [], name: 'BinPool__InsufficientAmountUnSpecified', type: 'error' },
+ { inputs: [], name: 'BinPool__InvalidBurnInput', type: 'error' },
+ { inputs: [], name: 'BinPool__NoLiquidityToReceiveFees', type: 'error' },
+ { inputs: [{ internalType: 'uint24', name: 'id', type: 'uint24' }], name: 'BinPool__ZeroAmountsOut', type: 'error' },
+ { inputs: [{ internalType: 'uint24', name: 'id', type: 'uint24' }], name: 'BinPool__ZeroShares', type: 'error' },
+ { inputs: [{ internalType: 'uint16', name: 'binStep', type: 'uint16' }], name: 'BinStepTooLarge', type: 'error' },
+ { inputs: [{ internalType: 'uint16', name: 'binStep', type: 'uint16' }], name: 'BinStepTooSmall', type: 'error' },
+ {
+ inputs: [
+ { internalType: 'address', name: 'currency0', type: 'address' },
+ { internalType: 'address', name: 'currency1', type: 'address' },
+ ],
+ name: 'CurrenciesInitializedOutOfOrder',
+ type: 'error',
+ },
+ { inputs: [], name: 'EnforcedPause', type: 'error' },
+ { inputs: [], name: 'HookConfigValidationError', type: 'error' },
+ { inputs: [], name: 'HookDeltaExceedsSwapAmount', type: 'error' },
+ { inputs: [], name: 'HookPermissionsValidationError', type: 'error' },
+ {
+ inputs: [{ internalType: 'uint256', name: 'shares', type: 'uint256' }],
+ name: 'InsufficientBinShareForDonate',
+ type: 'error',
+ },
+ { inputs: [], name: 'InvalidBips', type: 'error' },
+ { inputs: [], name: 'InvalidCaller', type: 'error' },
+ { inputs: [], name: 'InvalidHookResponse', type: 'error' },
+ { inputs: [{ internalType: 'uint24', name: 'fee', type: 'uint24' }], name: 'LPFeeTooLarge', type: 'error' },
+ { inputs: [], name: 'LiquidityConfigurations__InvalidConfig', type: 'error' },
+ {
+ inputs: [{ internalType: 'uint16', name: 'maxBinStep', type: 'uint16' }],
+ name: 'MaxBinStepTooSmall',
+ type: 'error',
+ },
+ { inputs: [], name: 'PackedUint128Math__AddOverflow', type: 'error' },
+ { inputs: [], name: 'PackedUint128Math__SubUnderflow', type: 'error' },
+ { inputs: [], name: 'PoolAlreadyInitialized', type: 'error' },
+ { inputs: [], name: 'PoolInvalidParameter', type: 'error' },
+ { inputs: [], name: 'PoolManagerMismatch', type: 'error' },
+ { inputs: [], name: 'PoolNotInitialized', type: 'error' },
+ { inputs: [], name: 'ProtocolFeeCannotBeFetched', type: 'error' },
+ { inputs: [{ internalType: 'uint24', name: 'fee', type: 'uint24' }], name: 'ProtocolFeeTooLarge', type: 'error' },
+ {
+ inputs: [
+ { internalType: 'uint256', name: 'x', type: 'uint256' },
+ { internalType: 'int256', name: 'y', type: 'int256' },
+ ],
+ name: 'Uint128x128Math__PowUnderflow',
+ type: 'error',
+ },
+ { inputs: [], name: 'Uint256x256Math__MulDivOverflow', type: 'error' },
+ { inputs: [], name: 'Uint256x256Math__MulShiftOverflow', type: 'error' },
+ { inputs: [], name: 'UnauthorizedDynamicLPFeeUpdate', type: 'error' },
+ { inputs: [], name: 'UnusedBitsNonZero', type: 'error' },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'PoolId', name: 'id', type: 'bytes32' },
+ { indexed: true, internalType: 'address', name: 'sender', type: 'address' },
+ { indexed: false, internalType: 'uint256[]', name: 'ids', type: 'uint256[]' },
+ { indexed: false, internalType: 'bytes32', name: 'salt', type: 'bytes32' },
+ { indexed: false, internalType: 'bytes32[]', name: 'amounts', type: 'bytes32[]' },
+ ],
+ name: 'Burn',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'PoolId', name: 'id', type: 'bytes32' },
+ { indexed: true, internalType: 'address', name: 'sender', type: 'address' },
+ { indexed: false, internalType: 'int128', name: 'amount0', type: 'int128' },
+ { indexed: false, internalType: 'int128', name: 'amount1', type: 'int128' },
+ { indexed: false, internalType: 'uint24', name: 'binId', type: 'uint24' },
+ ],
+ name: 'Donate',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'PoolId', name: 'id', type: 'bytes32' },
+ { indexed: false, internalType: 'uint24', name: 'dynamicLPFee', type: 'uint24' },
+ ],
+ name: 'DynamicLPFeeUpdated',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'PoolId', name: 'id', type: 'bytes32' },
+ { indexed: true, internalType: 'Currency', name: 'currency0', type: 'address' },
+ { indexed: true, internalType: 'Currency', name: 'currency1', type: 'address' },
+ { indexed: false, internalType: 'contract IHooks', name: 'hooks', type: 'address' },
+ { indexed: false, internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { indexed: false, internalType: 'bytes32', name: 'parameters', type: 'bytes32' },
+ { indexed: false, internalType: 'uint24', name: 'activeId', type: 'uint24' },
+ ],
+ name: 'Initialize',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'PoolId', name: 'id', type: 'bytes32' },
+ { indexed: true, internalType: 'address', name: 'sender', type: 'address' },
+ { indexed: false, internalType: 'uint256[]', name: 'ids', type: 'uint256[]' },
+ { indexed: false, internalType: 'bytes32', name: 'salt', type: 'bytes32' },
+ { indexed: false, internalType: 'bytes32[]', name: 'amounts', type: 'bytes32[]' },
+ { indexed: false, internalType: 'bytes32', name: 'compositionFeeAmount', type: 'bytes32' },
+ { indexed: false, internalType: 'bytes32', name: 'feeAmountToProtocol', type: 'bytes32' },
+ ],
+ name: 'Mint',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'address', name: 'previousOwner', type: 'address' },
+ { indexed: true, internalType: 'address', name: 'newOwner', type: 'address' },
+ ],
+ name: 'OwnershipTransferred',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [{ indexed: false, internalType: 'address', name: 'account', type: 'address' }],
+ name: 'Paused',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [{ indexed: true, internalType: 'address', name: 'protocolFeeController', type: 'address' }],
+ name: 'ProtocolFeeControllerUpdated',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'PoolId', name: 'id', type: 'bytes32' },
+ { indexed: false, internalType: 'uint24', name: 'protocolFee', type: 'uint24' },
+ ],
+ name: 'ProtocolFeeUpdated',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [{ indexed: false, internalType: 'uint16', name: 'maxBinStep', type: 'uint16' }],
+ name: 'SetMaxBinStep',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [{ indexed: false, internalType: 'uint256', name: 'minLiquidity', type: 'uint256' }],
+ name: 'SetMinBinSharesForDonate',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'PoolId', name: 'id', type: 'bytes32' },
+ { indexed: true, internalType: 'address', name: 'sender', type: 'address' },
+ { indexed: false, internalType: 'int128', name: 'amount0', type: 'int128' },
+ { indexed: false, internalType: 'int128', name: 'amount1', type: 'int128' },
+ { indexed: false, internalType: 'uint24', name: 'activeId', type: 'uint24' },
+ { indexed: false, internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { indexed: false, internalType: 'uint16', name: 'protocolFee', type: 'uint16' },
+ ],
+ name: 'Swap',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [{ indexed: false, internalType: 'address', name: 'account', type: 'address' }],
+ name: 'Unpaused',
+ type: 'event',
+ },
+ {
+ inputs: [],
+ name: 'MAX_BIN_STEP',
+ outputs: [{ internalType: 'uint16', name: '', type: 'uint16' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'MIN_BIN_SHARE_FOR_DONATE',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'MIN_BIN_STEP',
+ outputs: [{ internalType: 'uint16', name: '', type: 'uint16' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: 'Currency', name: 'currency0', type: 'address' },
+ { internalType: 'Currency', name: 'currency1', type: 'address' },
+ { internalType: 'contract IHooks', name: 'hooks', type: 'address' },
+ { internalType: 'contract IPoolManager', name: 'poolManager', type: 'address' },
+ { internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { internalType: 'bytes32', name: 'parameters', type: 'bytes32' },
+ ],
+ internalType: 'struct PoolKey',
+ name: 'key',
+ type: 'tuple',
+ },
+ {
+ components: [
+ { internalType: 'uint256[]', name: 'ids', type: 'uint256[]' },
+ { internalType: 'uint256[]', name: 'amountsToBurn', type: 'uint256[]' },
+ { internalType: 'bytes32', name: 'salt', type: 'bytes32' },
+ ],
+ internalType: 'struct IBinPoolManager.BurnParams',
+ name: 'params',
+ type: 'tuple',
+ },
+ { internalType: 'bytes', name: 'hookData', type: 'bytes' },
+ ],
+ name: 'burn',
+ outputs: [{ internalType: 'BalanceDelta', name: 'delta', type: 'int256' }],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: 'recipient', type: 'address' },
+ { internalType: 'Currency', name: 'currency', type: 'address' },
+ { internalType: 'uint256', name: 'amount', type: 'uint256' },
+ ],
+ name: 'collectProtocolFees',
+ outputs: [{ internalType: 'uint256', name: 'amountCollected', type: 'uint256' }],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: 'Currency', name: 'currency0', type: 'address' },
+ { internalType: 'Currency', name: 'currency1', type: 'address' },
+ { internalType: 'contract IHooks', name: 'hooks', type: 'address' },
+ { internalType: 'contract IPoolManager', name: 'poolManager', type: 'address' },
+ { internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { internalType: 'bytes32', name: 'parameters', type: 'bytes32' },
+ ],
+ internalType: 'struct PoolKey',
+ name: 'key',
+ type: 'tuple',
+ },
+ { internalType: 'uint128', name: 'amount0', type: 'uint128' },
+ { internalType: 'uint128', name: 'amount1', type: 'uint128' },
+ { internalType: 'bytes', name: 'hookData', type: 'bytes' },
+ ],
+ name: 'donate',
+ outputs: [
+ { internalType: 'BalanceDelta', name: 'delta', type: 'int256' },
+ { internalType: 'uint24', name: 'binId', type: 'uint24' },
+ ],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'bytes32', name: 'slot', type: 'bytes32' }],
+ name: 'extsload',
+ outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'bytes32[]', name: 'slots', type: 'bytes32[]' }],
+ name: 'extsload',
+ outputs: [{ internalType: 'bytes32[]', name: '', type: 'bytes32[]' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'PoolId', name: 'id', type: 'bytes32' },
+ { internalType: 'uint24', name: 'binId', type: 'uint24' },
+ ],
+ name: 'getBin',
+ outputs: [
+ { internalType: 'uint128', name: 'binReserveX', type: 'uint128' },
+ { internalType: 'uint128', name: 'binReserveY', type: 'uint128' },
+ { internalType: 'uint256', name: 'binLiquidity', type: 'uint256' },
+ { internalType: 'uint256', name: 'totalShares', type: 'uint256' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'PoolId', name: 'id', type: 'bytes32' },
+ { internalType: 'bool', name: 'swapForY', type: 'bool' },
+ { internalType: 'uint24', name: 'binId', type: 'uint24' },
+ ],
+ name: 'getNextNonEmptyBin',
+ outputs: [{ internalType: 'uint24', name: 'nextId', type: 'uint24' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'PoolId', name: 'id', type: 'bytes32' },
+ { internalType: 'address', name: 'owner', type: 'address' },
+ { internalType: 'uint24', name: 'binId', type: 'uint24' },
+ { internalType: 'bytes32', name: 'salt', type: 'bytes32' },
+ ],
+ name: 'getPosition',
+ outputs: [
+ {
+ components: [{ internalType: 'uint256', name: 'share', type: 'uint256' }],
+ internalType: 'struct BinPosition.Info',
+ name: 'position',
+ type: 'tuple',
+ },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'PoolId', name: 'id', type: 'bytes32' }],
+ name: 'getSlot0',
+ outputs: [
+ { internalType: 'uint24', name: 'activeId', type: 'uint24' },
+ { internalType: 'uint24', name: 'protocolFee', type: 'uint24' },
+ { internalType: 'uint24', name: 'lpFee', type: 'uint24' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: 'Currency', name: 'currency0', type: 'address' },
+ { internalType: 'Currency', name: 'currency1', type: 'address' },
+ { internalType: 'contract IHooks', name: 'hooks', type: 'address' },
+ { internalType: 'contract IPoolManager', name: 'poolManager', type: 'address' },
+ { internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { internalType: 'bytes32', name: 'parameters', type: 'bytes32' },
+ ],
+ internalType: 'struct PoolKey',
+ name: 'key',
+ type: 'tuple',
+ },
+ { internalType: 'uint24', name: 'activeId', type: 'uint24' },
+ { internalType: 'bytes', name: 'hookData', type: 'bytes' },
+ ],
+ name: 'initialize',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: 'Currency', name: 'currency0', type: 'address' },
+ { internalType: 'Currency', name: 'currency1', type: 'address' },
+ { internalType: 'contract IHooks', name: 'hooks', type: 'address' },
+ { internalType: 'contract IPoolManager', name: 'poolManager', type: 'address' },
+ { internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { internalType: 'bytes32', name: 'parameters', type: 'bytes32' },
+ ],
+ internalType: 'struct PoolKey',
+ name: 'key',
+ type: 'tuple',
+ },
+ {
+ components: [
+ { internalType: 'bytes32[]', name: 'liquidityConfigs', type: 'bytes32[]' },
+ { internalType: 'bytes32', name: 'amountIn', type: 'bytes32' },
+ { internalType: 'bytes32', name: 'salt', type: 'bytes32' },
+ ],
+ internalType: 'struct IBinPoolManager.MintParams',
+ name: 'params',
+ type: 'tuple',
+ },
+ { internalType: 'bytes', name: 'hookData', type: 'bytes' },
+ ],
+ name: 'mint',
+ outputs: [
+ { internalType: 'BalanceDelta', name: 'delta', type: 'int256' },
+ {
+ components: [
+ { internalType: 'uint256[]', name: 'ids', type: 'uint256[]' },
+ { internalType: 'bytes32[]', name: 'amounts', type: 'bytes32[]' },
+ { internalType: 'uint256[]', name: 'liquidityMinted', type: 'uint256[]' },
+ ],
+ internalType: 'struct BinPool.MintArrays',
+ name: 'mintArray',
+ type: 'tuple',
+ },
+ ],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'owner',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ { inputs: [], name: 'pause', outputs: [], stateMutability: 'nonpayable', type: 'function' },
+ {
+ inputs: [],
+ name: 'paused',
+ outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'PoolId', name: 'id', type: 'bytes32' }],
+ name: 'poolIdToPoolKey',
+ outputs: [
+ { internalType: 'Currency', name: 'currency0', type: 'address' },
+ { internalType: 'Currency', name: 'currency1', type: 'address' },
+ { internalType: 'contract IHooks', name: 'hooks', type: 'address' },
+ { internalType: 'contract IPoolManager', name: 'poolManager', type: 'address' },
+ { internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { internalType: 'bytes32', name: 'parameters', type: 'bytes32' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'PoolId', name: 'id', type: 'bytes32' }],
+ name: 'pools',
+ outputs: [
+ {
+ components: [
+ { internalType: 'uint24', name: 'activeId', type: 'uint24' },
+ { internalType: 'uint24', name: 'protocolFee', type: 'uint24' },
+ { internalType: 'uint24', name: 'lpFee', type: 'uint24' },
+ ],
+ internalType: 'struct BinPool.Slot0',
+ name: 'slot0',
+ type: 'tuple',
+ },
+ { internalType: 'bytes32', name: 'level0', type: 'bytes32' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'protocolFeeController',
+ outputs: [{ internalType: 'contract IProtocolFeeController', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'Currency', name: 'currency', type: 'address' }],
+ name: 'protocolFeesAccrued',
+ outputs: [{ internalType: 'uint256', name: 'amount', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'uint16', name: 'maxBinStep', type: 'uint16' }],
+ name: 'setMaxBinStep',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'uint256', name: 'minBinShare', type: 'uint256' }],
+ name: 'setMinBinSharesForDonate',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: 'Currency', name: 'currency0', type: 'address' },
+ { internalType: 'Currency', name: 'currency1', type: 'address' },
+ { internalType: 'contract IHooks', name: 'hooks', type: 'address' },
+ { internalType: 'contract IPoolManager', name: 'poolManager', type: 'address' },
+ { internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { internalType: 'bytes32', name: 'parameters', type: 'bytes32' },
+ ],
+ internalType: 'struct PoolKey',
+ name: 'key',
+ type: 'tuple',
+ },
+ { internalType: 'uint24', name: 'newProtocolFee', type: 'uint24' },
+ ],
+ name: 'setProtocolFee',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'contract IProtocolFeeController', name: 'controller', type: 'address' }],
+ name: 'setProtocolFeeController',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: 'Currency', name: 'currency0', type: 'address' },
+ { internalType: 'Currency', name: 'currency1', type: 'address' },
+ { internalType: 'contract IHooks', name: 'hooks', type: 'address' },
+ { internalType: 'contract IPoolManager', name: 'poolManager', type: 'address' },
+ { internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { internalType: 'bytes32', name: 'parameters', type: 'bytes32' },
+ ],
+ internalType: 'struct PoolKey',
+ name: 'key',
+ type: 'tuple',
+ },
+ { internalType: 'bool', name: 'swapForY', type: 'bool' },
+ { internalType: 'int128', name: 'amountSpecified', type: 'int128' },
+ { internalType: 'bytes', name: 'hookData', type: 'bytes' },
+ ],
+ name: 'swap',
+ outputs: [{ internalType: 'BalanceDelta', name: 'delta', type: 'int256' }],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }],
+ name: 'transferOwnership',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ { inputs: [], name: 'unpause', outputs: [], stateMutability: 'nonpayable', type: 'function' },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: 'Currency', name: 'currency0', type: 'address' },
+ { internalType: 'Currency', name: 'currency1', type: 'address' },
+ { internalType: 'contract IHooks', name: 'hooks', type: 'address' },
+ { internalType: 'contract IPoolManager', name: 'poolManager', type: 'address' },
+ { internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { internalType: 'bytes32', name: 'parameters', type: 'bytes32' },
+ ],
+ internalType: 'struct PoolKey',
+ name: 'key',
+ type: 'tuple',
+ },
+ { internalType: 'uint24', name: 'newDynamicLPFee', type: 'uint24' },
+ ],
+ name: 'updateDynamicLPFee',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'vault',
+ outputs: [{ internalType: 'contract IVault', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+] as const
diff --git a/packages/smart-router/evm/abis/IBinQuoter.ts b/packages/smart-router/evm/abis/IBinQuoter.ts
new file mode 100644
index 0000000000000..0c93bd84a856e
--- /dev/null
+++ b/packages/smart-router/evm/abis/IBinQuoter.ts
@@ -0,0 +1,294 @@
+export const binQuoterAbi = [
+ {
+ inputs: [{ internalType: 'address', name: '_poolManager', type: 'address' }],
+ stateMutability: 'nonpayable',
+ type: 'constructor',
+ },
+ { inputs: [{ internalType: 'PoolId', name: 'poolId', type: 'bytes32' }], name: 'NotEnoughLiquidity', type: 'error' },
+ { inputs: [], name: 'NotSelf', type: 'error' },
+ { inputs: [], name: 'NotVault', type: 'error' },
+ { inputs: [{ internalType: 'uint256', name: 'amount', type: 'uint256' }], name: 'QuoteSwap', type: 'error' },
+ { inputs: [], name: 'UnexpectedCallSuccess', type: 'error' },
+ {
+ inputs: [{ internalType: 'bytes', name: 'revertData', type: 'bytes' }],
+ name: 'UnexpectedRevertBytes',
+ type: 'error',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: 'Currency', name: 'exactCurrency', type: 'address' },
+ {
+ components: [
+ { internalType: 'Currency', name: 'intermediateCurrency', type: 'address' },
+ { internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { internalType: 'contract IHooks', name: 'hooks', type: 'address' },
+ { internalType: 'contract IPoolManager', name: 'poolManager', type: 'address' },
+ { internalType: 'bytes', name: 'hookData', type: 'bytes' },
+ { internalType: 'bytes32', name: 'parameters', type: 'bytes32' },
+ ],
+ internalType: 'struct PathKey[]',
+ name: 'path',
+ type: 'tuple[]',
+ },
+ { internalType: 'uint128', name: 'exactAmount', type: 'uint128' },
+ ],
+ internalType: 'struct IQuoter.QuoteExactParams',
+ name: 'params',
+ type: 'tuple',
+ },
+ ],
+ name: '_quoteExactInput',
+ outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ {
+ components: [
+ { internalType: 'Currency', name: 'currency0', type: 'address' },
+ { internalType: 'Currency', name: 'currency1', type: 'address' },
+ { internalType: 'contract IHooks', name: 'hooks', type: 'address' },
+ { internalType: 'contract IPoolManager', name: 'poolManager', type: 'address' },
+ { internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { internalType: 'bytes32', name: 'parameters', type: 'bytes32' },
+ ],
+ internalType: 'struct PoolKey',
+ name: 'poolKey',
+ type: 'tuple',
+ },
+ { internalType: 'bool', name: 'zeroForOne', type: 'bool' },
+ { internalType: 'uint128', name: 'exactAmount', type: 'uint128' },
+ { internalType: 'bytes', name: 'hookData', type: 'bytes' },
+ ],
+ internalType: 'struct IQuoter.QuoteExactSingleParams',
+ name: 'params',
+ type: 'tuple',
+ },
+ ],
+ name: '_quoteExactInputSingle',
+ outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: 'Currency', name: 'exactCurrency', type: 'address' },
+ {
+ components: [
+ { internalType: 'Currency', name: 'intermediateCurrency', type: 'address' },
+ { internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { internalType: 'contract IHooks', name: 'hooks', type: 'address' },
+ { internalType: 'contract IPoolManager', name: 'poolManager', type: 'address' },
+ { internalType: 'bytes', name: 'hookData', type: 'bytes' },
+ { internalType: 'bytes32', name: 'parameters', type: 'bytes32' },
+ ],
+ internalType: 'struct PathKey[]',
+ name: 'path',
+ type: 'tuple[]',
+ },
+ { internalType: 'uint128', name: 'exactAmount', type: 'uint128' },
+ ],
+ internalType: 'struct IQuoter.QuoteExactParams',
+ name: 'params',
+ type: 'tuple',
+ },
+ ],
+ name: '_quoteExactOutput',
+ outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ {
+ components: [
+ { internalType: 'Currency', name: 'currency0', type: 'address' },
+ { internalType: 'Currency', name: 'currency1', type: 'address' },
+ { internalType: 'contract IHooks', name: 'hooks', type: 'address' },
+ { internalType: 'contract IPoolManager', name: 'poolManager', type: 'address' },
+ { internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { internalType: 'bytes32', name: 'parameters', type: 'bytes32' },
+ ],
+ internalType: 'struct PoolKey',
+ name: 'poolKey',
+ type: 'tuple',
+ },
+ { internalType: 'bool', name: 'zeroForOne', type: 'bool' },
+ { internalType: 'uint128', name: 'exactAmount', type: 'uint128' },
+ { internalType: 'bytes', name: 'hookData', type: 'bytes' },
+ ],
+ internalType: 'struct IQuoter.QuoteExactSingleParams',
+ name: 'params',
+ type: 'tuple',
+ },
+ ],
+ name: '_quoteExactOutputSingle',
+ outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'bytes', name: 'data', type: 'bytes' }],
+ name: 'lockAcquired',
+ outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'poolManager',
+ outputs: [{ internalType: 'contract IBinPoolManager', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: 'Currency', name: 'exactCurrency', type: 'address' },
+ {
+ components: [
+ { internalType: 'Currency', name: 'intermediateCurrency', type: 'address' },
+ { internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { internalType: 'contract IHooks', name: 'hooks', type: 'address' },
+ { internalType: 'contract IPoolManager', name: 'poolManager', type: 'address' },
+ { internalType: 'bytes', name: 'hookData', type: 'bytes' },
+ { internalType: 'bytes32', name: 'parameters', type: 'bytes32' },
+ ],
+ internalType: 'struct PathKey[]',
+ name: 'path',
+ type: 'tuple[]',
+ },
+ { internalType: 'uint128', name: 'exactAmount', type: 'uint128' },
+ ],
+ internalType: 'struct IQuoter.QuoteExactParams',
+ name: 'params',
+ type: 'tuple',
+ },
+ ],
+ name: 'quoteExactInput',
+ outputs: [
+ { internalType: 'uint256', name: 'amountOut', type: 'uint256' },
+ { internalType: 'uint256', name: 'gasEstimate', type: 'uint256' },
+ ],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ {
+ components: [
+ { internalType: 'Currency', name: 'currency0', type: 'address' },
+ { internalType: 'Currency', name: 'currency1', type: 'address' },
+ { internalType: 'contract IHooks', name: 'hooks', type: 'address' },
+ { internalType: 'contract IPoolManager', name: 'poolManager', type: 'address' },
+ { internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { internalType: 'bytes32', name: 'parameters', type: 'bytes32' },
+ ],
+ internalType: 'struct PoolKey',
+ name: 'poolKey',
+ type: 'tuple',
+ },
+ { internalType: 'bool', name: 'zeroForOne', type: 'bool' },
+ { internalType: 'uint128', name: 'exactAmount', type: 'uint128' },
+ { internalType: 'bytes', name: 'hookData', type: 'bytes' },
+ ],
+ internalType: 'struct IQuoter.QuoteExactSingleParams',
+ name: 'params',
+ type: 'tuple',
+ },
+ ],
+ name: 'quoteExactInputSingle',
+ outputs: [
+ { internalType: 'uint256', name: 'amountOut', type: 'uint256' },
+ { internalType: 'uint256', name: 'gasEstimate', type: 'uint256' },
+ ],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: 'Currency', name: 'exactCurrency', type: 'address' },
+ {
+ components: [
+ { internalType: 'Currency', name: 'intermediateCurrency', type: 'address' },
+ { internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { internalType: 'contract IHooks', name: 'hooks', type: 'address' },
+ { internalType: 'contract IPoolManager', name: 'poolManager', type: 'address' },
+ { internalType: 'bytes', name: 'hookData', type: 'bytes' },
+ { internalType: 'bytes32', name: 'parameters', type: 'bytes32' },
+ ],
+ internalType: 'struct PathKey[]',
+ name: 'path',
+ type: 'tuple[]',
+ },
+ { internalType: 'uint128', name: 'exactAmount', type: 'uint128' },
+ ],
+ internalType: 'struct IQuoter.QuoteExactParams',
+ name: 'params',
+ type: 'tuple',
+ },
+ ],
+ name: 'quoteExactOutput',
+ outputs: [
+ { internalType: 'uint256', name: 'amountIn', type: 'uint256' },
+ { internalType: 'uint256', name: 'gasEstimate', type: 'uint256' },
+ ],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ {
+ components: [
+ { internalType: 'Currency', name: 'currency0', type: 'address' },
+ { internalType: 'Currency', name: 'currency1', type: 'address' },
+ { internalType: 'contract IHooks', name: 'hooks', type: 'address' },
+ { internalType: 'contract IPoolManager', name: 'poolManager', type: 'address' },
+ { internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { internalType: 'bytes32', name: 'parameters', type: 'bytes32' },
+ ],
+ internalType: 'struct PoolKey',
+ name: 'poolKey',
+ type: 'tuple',
+ },
+ { internalType: 'bool', name: 'zeroForOne', type: 'bool' },
+ { internalType: 'uint128', name: 'exactAmount', type: 'uint128' },
+ { internalType: 'bytes', name: 'hookData', type: 'bytes' },
+ ],
+ internalType: 'struct IQuoter.QuoteExactSingleParams',
+ name: 'params',
+ type: 'tuple',
+ },
+ ],
+ name: 'quoteExactOutputSingle',
+ outputs: [
+ { internalType: 'uint256', name: 'amountIn', type: 'uint256' },
+ { internalType: 'uint256', name: 'gasEstimate', type: 'uint256' },
+ ],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'vault',
+ outputs: [{ internalType: 'contract IVault', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+] as const
diff --git a/packages/smart-router/evm/abis/ICLQuoter.ts b/packages/smart-router/evm/abis/ICLQuoter.ts
new file mode 100644
index 0000000000000..031a400db453a
--- /dev/null
+++ b/packages/smart-router/evm/abis/ICLQuoter.ts
@@ -0,0 +1,294 @@
+export const clQuoterAbi = [
+ {
+ inputs: [{ internalType: 'address', name: '_poolManager', type: 'address' }],
+ stateMutability: 'nonpayable',
+ type: 'constructor',
+ },
+ { inputs: [{ internalType: 'PoolId', name: 'poolId', type: 'bytes32' }], name: 'NotEnoughLiquidity', type: 'error' },
+ { inputs: [], name: 'NotSelf', type: 'error' },
+ { inputs: [], name: 'NotVault', type: 'error' },
+ { inputs: [{ internalType: 'uint256', name: 'amount', type: 'uint256' }], name: 'QuoteSwap', type: 'error' },
+ { inputs: [], name: 'UnexpectedCallSuccess', type: 'error' },
+ {
+ inputs: [{ internalType: 'bytes', name: 'revertData', type: 'bytes' }],
+ name: 'UnexpectedRevertBytes',
+ type: 'error',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: 'Currency', name: 'exactCurrency', type: 'address' },
+ {
+ components: [
+ { internalType: 'Currency', name: 'intermediateCurrency', type: 'address' },
+ { internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { internalType: 'contract IHooks', name: 'hooks', type: 'address' },
+ { internalType: 'contract IPoolManager', name: 'poolManager', type: 'address' },
+ { internalType: 'bytes', name: 'hookData', type: 'bytes' },
+ { internalType: 'bytes32', name: 'parameters', type: 'bytes32' },
+ ],
+ internalType: 'struct PathKey[]',
+ name: 'path',
+ type: 'tuple[]',
+ },
+ { internalType: 'uint128', name: 'exactAmount', type: 'uint128' },
+ ],
+ internalType: 'struct IQuoter.QuoteExactParams',
+ name: 'params',
+ type: 'tuple',
+ },
+ ],
+ name: '_quoteExactInput',
+ outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ {
+ components: [
+ { internalType: 'Currency', name: 'currency0', type: 'address' },
+ { internalType: 'Currency', name: 'currency1', type: 'address' },
+ { internalType: 'contract IHooks', name: 'hooks', type: 'address' },
+ { internalType: 'contract IPoolManager', name: 'poolManager', type: 'address' },
+ { internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { internalType: 'bytes32', name: 'parameters', type: 'bytes32' },
+ ],
+ internalType: 'struct PoolKey',
+ name: 'poolKey',
+ type: 'tuple',
+ },
+ { internalType: 'bool', name: 'zeroForOne', type: 'bool' },
+ { internalType: 'uint128', name: 'exactAmount', type: 'uint128' },
+ { internalType: 'bytes', name: 'hookData', type: 'bytes' },
+ ],
+ internalType: 'struct IQuoter.QuoteExactSingleParams',
+ name: 'params',
+ type: 'tuple',
+ },
+ ],
+ name: '_quoteExactInputSingle',
+ outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: 'Currency', name: 'exactCurrency', type: 'address' },
+ {
+ components: [
+ { internalType: 'Currency', name: 'intermediateCurrency', type: 'address' },
+ { internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { internalType: 'contract IHooks', name: 'hooks', type: 'address' },
+ { internalType: 'contract IPoolManager', name: 'poolManager', type: 'address' },
+ { internalType: 'bytes', name: 'hookData', type: 'bytes' },
+ { internalType: 'bytes32', name: 'parameters', type: 'bytes32' },
+ ],
+ internalType: 'struct PathKey[]',
+ name: 'path',
+ type: 'tuple[]',
+ },
+ { internalType: 'uint128', name: 'exactAmount', type: 'uint128' },
+ ],
+ internalType: 'struct IQuoter.QuoteExactParams',
+ name: 'params',
+ type: 'tuple',
+ },
+ ],
+ name: '_quoteExactOutput',
+ outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ {
+ components: [
+ { internalType: 'Currency', name: 'currency0', type: 'address' },
+ { internalType: 'Currency', name: 'currency1', type: 'address' },
+ { internalType: 'contract IHooks', name: 'hooks', type: 'address' },
+ { internalType: 'contract IPoolManager', name: 'poolManager', type: 'address' },
+ { internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { internalType: 'bytes32', name: 'parameters', type: 'bytes32' },
+ ],
+ internalType: 'struct PoolKey',
+ name: 'poolKey',
+ type: 'tuple',
+ },
+ { internalType: 'bool', name: 'zeroForOne', type: 'bool' },
+ { internalType: 'uint128', name: 'exactAmount', type: 'uint128' },
+ { internalType: 'bytes', name: 'hookData', type: 'bytes' },
+ ],
+ internalType: 'struct IQuoter.QuoteExactSingleParams',
+ name: 'params',
+ type: 'tuple',
+ },
+ ],
+ name: '_quoteExactOutputSingle',
+ outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'bytes', name: 'data', type: 'bytes' }],
+ name: 'lockAcquired',
+ outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'poolManager',
+ outputs: [{ internalType: 'contract ICLPoolManager', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: 'Currency', name: 'exactCurrency', type: 'address' },
+ {
+ components: [
+ { internalType: 'Currency', name: 'intermediateCurrency', type: 'address' },
+ { internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { internalType: 'contract IHooks', name: 'hooks', type: 'address' },
+ { internalType: 'contract IPoolManager', name: 'poolManager', type: 'address' },
+ { internalType: 'bytes', name: 'hookData', type: 'bytes' },
+ { internalType: 'bytes32', name: 'parameters', type: 'bytes32' },
+ ],
+ internalType: 'struct PathKey[]',
+ name: 'path',
+ type: 'tuple[]',
+ },
+ { internalType: 'uint128', name: 'exactAmount', type: 'uint128' },
+ ],
+ internalType: 'struct IQuoter.QuoteExactParams',
+ name: 'params',
+ type: 'tuple',
+ },
+ ],
+ name: 'quoteExactInput',
+ outputs: [
+ { internalType: 'uint256', name: 'amountOut', type: 'uint256' },
+ { internalType: 'uint256', name: 'gasEstimate', type: 'uint256' },
+ ],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ {
+ components: [
+ { internalType: 'Currency', name: 'currency0', type: 'address' },
+ { internalType: 'Currency', name: 'currency1', type: 'address' },
+ { internalType: 'contract IHooks', name: 'hooks', type: 'address' },
+ { internalType: 'contract IPoolManager', name: 'poolManager', type: 'address' },
+ { internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { internalType: 'bytes32', name: 'parameters', type: 'bytes32' },
+ ],
+ internalType: 'struct PoolKey',
+ name: 'poolKey',
+ type: 'tuple',
+ },
+ { internalType: 'bool', name: 'zeroForOne', type: 'bool' },
+ { internalType: 'uint128', name: 'exactAmount', type: 'uint128' },
+ { internalType: 'bytes', name: 'hookData', type: 'bytes' },
+ ],
+ internalType: 'struct IQuoter.QuoteExactSingleParams',
+ name: 'params',
+ type: 'tuple',
+ },
+ ],
+ name: 'quoteExactInputSingle',
+ outputs: [
+ { internalType: 'uint256', name: 'amountOut', type: 'uint256' },
+ { internalType: 'uint256', name: 'gasEstimate', type: 'uint256' },
+ ],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: 'Currency', name: 'exactCurrency', type: 'address' },
+ {
+ components: [
+ { internalType: 'Currency', name: 'intermediateCurrency', type: 'address' },
+ { internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { internalType: 'contract IHooks', name: 'hooks', type: 'address' },
+ { internalType: 'contract IPoolManager', name: 'poolManager', type: 'address' },
+ { internalType: 'bytes', name: 'hookData', type: 'bytes' },
+ { internalType: 'bytes32', name: 'parameters', type: 'bytes32' },
+ ],
+ internalType: 'struct PathKey[]',
+ name: 'path',
+ type: 'tuple[]',
+ },
+ { internalType: 'uint128', name: 'exactAmount', type: 'uint128' },
+ ],
+ internalType: 'struct IQuoter.QuoteExactParams',
+ name: 'params',
+ type: 'tuple',
+ },
+ ],
+ name: 'quoteExactOutput',
+ outputs: [
+ { internalType: 'uint256', name: 'amountIn', type: 'uint256' },
+ { internalType: 'uint256', name: 'gasEstimate', type: 'uint256' },
+ ],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ {
+ components: [
+ { internalType: 'Currency', name: 'currency0', type: 'address' },
+ { internalType: 'Currency', name: 'currency1', type: 'address' },
+ { internalType: 'contract IHooks', name: 'hooks', type: 'address' },
+ { internalType: 'contract IPoolManager', name: 'poolManager', type: 'address' },
+ { internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { internalType: 'bytes32', name: 'parameters', type: 'bytes32' },
+ ],
+ internalType: 'struct PoolKey',
+ name: 'poolKey',
+ type: 'tuple',
+ },
+ { internalType: 'bool', name: 'zeroForOne', type: 'bool' },
+ { internalType: 'uint128', name: 'exactAmount', type: 'uint128' },
+ { internalType: 'bytes', name: 'hookData', type: 'bytes' },
+ ],
+ internalType: 'struct IQuoter.QuoteExactSingleParams',
+ name: 'params',
+ type: 'tuple',
+ },
+ ],
+ name: 'quoteExactOutputSingle',
+ outputs: [
+ { internalType: 'uint256', name: 'amountIn', type: 'uint256' },
+ { internalType: 'uint256', name: 'gasEstimate', type: 'uint256' },
+ ],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'vault',
+ outputs: [{ internalType: 'contract IVault', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+] as const
diff --git a/packages/smart-router/evm/abis/IV4MixedRouteQuoter.ts b/packages/smart-router/evm/abis/IV4MixedRouteQuoter.ts
new file mode 100644
index 0000000000000..9b0cba79684f2
--- /dev/null
+++ b/packages/smart-router/evm/abis/IV4MixedRouteQuoter.ts
@@ -0,0 +1,169 @@
+export const v4MixedRouteQuoterAbi = [
+ {
+ inputs: [
+ { internalType: 'address', name: '_factoryV3', type: 'address' },
+ { internalType: 'address', name: '_factoryV2', type: 'address' },
+ { internalType: 'address', name: '_factoryStable', type: 'address' },
+ { internalType: 'address', name: '_WETH9', type: 'address' },
+ { internalType: 'contract ICLQuoter', name: '_clQuoter', type: 'address' },
+ { internalType: 'contract IBinQuoter', name: '_binQuoter', type: 'address' },
+ ],
+ stateMutability: 'nonpayable',
+ type: 'constructor',
+ },
+ { inputs: [], name: 'INVALID_ADDRESS', type: 'error' },
+ { inputs: [], name: 'InputLengthMismatch', type: 'error' },
+ { inputs: [], name: 'InvalidPath', type: 'error' },
+ { inputs: [], name: 'InvalidPoolKeyCurrency', type: 'error' },
+ { inputs: [], name: 'NoActions', type: 'error' },
+ {
+ inputs: [
+ { internalType: 'uint8', name: 'bits', type: 'uint8' },
+ { internalType: 'uint256', name: 'value', type: 'uint256' },
+ ],
+ name: 'SafeCastOverflowedUintDowncast',
+ type: 'error',
+ },
+ {
+ inputs: [{ internalType: 'uint256', name: 'value', type: 'uint256' }],
+ name: 'SafeCastOverflowedUintToInt',
+ type: 'error',
+ },
+ { inputs: [{ internalType: 'uint256', name: 'action', type: 'uint256' }], name: 'UnsupportedAction', type: 'error' },
+ {
+ inputs: [],
+ name: 'WETH9',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'binQuoter',
+ outputs: [{ internalType: 'contract IBinQuoter', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'clQuoter',
+ outputs: [{ internalType: 'contract ICLQuoter', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'factoryStable',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'factoryV2',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'factoryV3',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'int256', name: 'amount0Delta', type: 'int256' },
+ { internalType: 'int256', name: 'amount1Delta', type: 'int256' },
+ { internalType: 'bytes', name: 'data', type: 'bytes' },
+ ],
+ name: 'pancakeV3SwapCallback',
+ outputs: [],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: 'address', name: 'tokenIn', type: 'address' },
+ { internalType: 'address', name: 'tokenOut', type: 'address' },
+ { internalType: 'uint256', name: 'amountIn', type: 'uint256' },
+ { internalType: 'uint256', name: 'flag', type: 'uint256' },
+ ],
+ internalType: 'struct IMixedQuoter.QuoteExactInputSingleStableParams',
+ name: 'params',
+ type: 'tuple',
+ },
+ ],
+ name: 'quoteExactInputSingleStable',
+ outputs: [
+ { internalType: 'uint256', name: 'amountOut', type: 'uint256' },
+ { internalType: 'uint256', name: 'gasEstimate', type: 'uint256' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: 'address', name: 'tokenIn', type: 'address' },
+ { internalType: 'address', name: 'tokenOut', type: 'address' },
+ { internalType: 'uint256', name: 'amountIn', type: 'uint256' },
+ ],
+ internalType: 'struct IMixedQuoter.QuoteExactInputSingleV2Params',
+ name: 'params',
+ type: 'tuple',
+ },
+ ],
+ name: 'quoteExactInputSingleV2',
+ outputs: [
+ { internalType: 'uint256', name: 'amountOut', type: 'uint256' },
+ { internalType: 'uint256', name: 'gasEstimate', type: 'uint256' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: 'address', name: 'tokenIn', type: 'address' },
+ { internalType: 'address', name: 'tokenOut', type: 'address' },
+ { internalType: 'uint256', name: 'amountIn', type: 'uint256' },
+ { internalType: 'uint24', name: 'fee', type: 'uint24' },
+ { internalType: 'uint160', name: 'sqrtPriceLimitX96', type: 'uint160' },
+ ],
+ internalType: 'struct IMixedQuoter.QuoteExactInputSingleV3Params',
+ name: 'params',
+ type: 'tuple',
+ },
+ ],
+ name: 'quoteExactInputSingleV3',
+ outputs: [
+ { internalType: 'uint256', name: 'amountOut', type: 'uint256' },
+ { internalType: 'uint160', name: 'sqrtPriceX96After', type: 'uint160' },
+ { internalType: 'uint32', name: 'initializedTicksCrossed', type: 'uint32' },
+ { internalType: 'uint256', name: 'gasEstimate', type: 'uint256' },
+ ],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'address[]', name: 'paths', type: 'address[]' },
+ { internalType: 'bytes', name: 'actions', type: 'bytes' },
+ { internalType: 'bytes[]', name: 'params', type: 'bytes[]' },
+ { internalType: 'uint256', name: 'amountIn', type: 'uint256' },
+ ],
+ name: 'quoteMixedExactInput',
+ outputs: [
+ { internalType: 'uint256', name: 'amountOut', type: 'uint256' },
+ { internalType: 'uint256', name: 'gasEstimate', type: 'uint256' },
+ ],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+] as const
diff --git a/packages/smart-router/evm/constants/v4.ts b/packages/smart-router/evm/constants/v4.ts
new file mode 100644
index 0000000000000..ffec8d762f4b5
--- /dev/null
+++ b/packages/smart-router/evm/constants/v4.ts
@@ -0,0 +1,15 @@
+import { ChainId } from '@pancakeswap/chains'
+
+export const V4_CL_QUOTER_ADDRESSES = {
+ [ChainId.BSC_TESTNET]: '0xD2a534e28efb7e44Da94ae942F892E71F7462142',
+}
+
+export const V4_BIN_QUOTER_ADDRESSES = {
+ [ChainId.BSC_TESTNET]: '0x740469475C0e268F2110444BB8Bc3114E5321B90',
+}
+
+export const V4_MIXED_ROUTE_QUOTER_ADDRESSES = {
+ [ChainId.BSC_TESTNET]: '0x5B5209dBFD9dB69Fcc14f6591fD23089c229E143',
+}
+
+export const V4_SUPPORTED_CHAINS = [ChainId.BSC_TESTNET]
diff --git a/packages/smart-router/evm/index.test.ts b/packages/smart-router/evm/index.test.ts
index 565975b739564..cff85d9f7a5b1 100644
--- a/packages/smart-router/evm/index.test.ts
+++ b/packages/smart-router/evm/index.test.ts
@@ -40,6 +40,7 @@ test('exports', () => {
"SwapRouter",
"PoolType",
"RouteType",
+ "V4MixedQuoterActions",
"V4Router",
]
`)
@@ -54,6 +55,7 @@ test('SmartRouter exports', () => {
"getV2PoolsOnChain",
"getStablePoolsOnChain",
"getV3PoolsWithoutTicksOnChain",
+ "createOnChainPoolFactory",
"getV3PoolSubgraph",
"getV2PoolSubgraph",
"getAllV3PoolsFromSubgraph",
@@ -94,6 +96,8 @@ test('SmartRouter exports', () => {
"isV2Pool",
"isV3Pool",
"isStablePool",
+ "isV4ClPool",
+ "isV4BinPool",
"getMidPrice",
"involvesCurrency",
"encodeMixedRouteToPath",
diff --git a/packages/smart-router/evm/v3-router/functions/getBestRouteCombinationByQuotes.ts b/packages/smart-router/evm/v3-router/functions/getBestRouteCombinationByQuotes.ts
index 3dcbff7a76541..ba7fbc4a40f3f 100644
--- a/packages/smart-router/evm/v3-router/functions/getBestRouteCombinationByQuotes.ts
+++ b/packages/smart-router/evm/v3-router/functions/getBestRouteCombinationByQuotes.ts
@@ -8,7 +8,7 @@ import Queue from 'mnemonist/queue.js'
import { usdGasTokensByChain } from '../../constants'
import { BestRoutes, L1ToL2GasCosts, RouteWithQuote } from '../types'
-import { getPoolAddress, isV2Pool, isV3Pool, logger } from '../utils'
+import { getPoolAddress, isStablePool, isV2Pool, isV3Pool, logger } from '../utils'
interface Config {
minSplits?: number
@@ -257,7 +257,10 @@ export function getBestSwapRouteBy(
if (isV3Pool(p)) {
return `V3 fee ${p.fee} ${p.token0.symbol}-${p.token1.symbol}`
}
- return `Stable ${p.balances.map((b) => b.currency).join('-')}`
+ if (isStablePool(p)) {
+ return `Stable ${p.balances.map((b) => b.currency).join('-')}`
+ }
+ return `Unsupported pool ${p}`
})
.join(', ')} ${r.quote.toExact()}`,
)
diff --git a/packages/smart-router/evm/v3-router/providers/onChainQuoteProvider.ts b/packages/smart-router/evm/v3-router/providers/onChainQuoteProvider.ts
index bc008dc921b38..a1bd03632d071 100644
--- a/packages/smart-router/evm/v3-router/providers/onChainQuoteProvider.ts
+++ b/packages/smart-router/evm/v3-router/providers/onChainQuoteProvider.ts
@@ -1,5 +1,5 @@
import { ChainId } from '@pancakeswap/chains'
-import { BigintIsh, Currency, CurrencyAmount } from '@pancakeswap/sdk'
+import { BigintIsh, Currency, CurrencyAmount, getCurrencyAddress } from '@pancakeswap/swap-sdk-core'
import { AbortControl, isAbortError } from '@pancakeswap/utils/abortControl'
import retry from 'async-retry'
import { Abi, Address } from 'viem'
@@ -21,6 +21,13 @@ import {
import { encodeMixedRouteToPath, getQuoteCurrency, isStablePool, isV2Pool, isV3Pool } from '../utils'
import { Result } from './multicallProvider'
import { PancakeMulticallProvider } from './multicallSwapProvider'
+import { V4_BIN_QUOTER_ADDRESSES, V4_CL_QUOTER_ADDRESSES, V4_MIXED_ROUTE_QUOTER_ADDRESSES } from '../../constants/v4'
+import { clQuoterAbi } from '../../abis/ICLQuoter'
+import { PathKey, encodeV4RouteToPath } from '../utils/encodeV4RouteToPath'
+import { v4MixedRouteQuoterAbi } from '../../abis/IV4MixedRouteQuoter'
+import { encodeV4MixedRouteActions } from '../utils/encodeV4MixedRouteActions'
+import { encodeV4MixedRouteParams } from '../utils/encodeV4MixedRouteParams'
+import { binQuoterAbi } from '../../abis/IBinQuoter'
const DEFAULT_BATCH_RETRIES = 2
@@ -47,9 +54,17 @@ const SUCCESS_RATE_CONFIG = {
[ChainId.BASE_SEPOLIA]: 0.1,
} as const satisfies Record
+type V4ClInputs = [
+ {
+ exactAmount: string
+ exactCurrency: string
+ path: PathKey[]
+ },
+]
type V3Inputs = [string, string]
type MixedInputs = [string, number[], string]
-type CallInputs = V3Inputs | MixedInputs
+type V4MixedInputs = [string[], string, string[], string]
+type CallInputs = V3Inputs | MixedInputs | V4ClInputs | V4MixedInputs
type AdjustQuoteForGasHandler = (params: {
isExactIn?: boolean
@@ -325,7 +340,7 @@ function validateSuccessRate(
// }
function processQuoteResults(
- quoteResults: (Result<[bigint, bigint[], number[], bigint]> | null)[],
+ quoteResults: (Result<[bigint, bigint[], number[], bigint] | [bigint, bigint]> | null)[],
routes: RouteWithoutQuote[],
gasModel: GasModel,
adjustQuoteForGas: AdjustQuoteForGasHandler,
@@ -358,14 +373,16 @@ function processQuoteResults(
continue
}
+ const [quoteRaw] = quoteResult.result
+ const initializedTickCrossedList = quoteResult.result.length === 4 ? quoteResult.result[2] : []
const quoteCurrency = getQuoteCurrency(route, route.amount.currency)
- const quote = CurrencyAmount.fromRawAmount(quoteCurrency.wrapped, quoteResult.result[0].toString())
+ const quote = CurrencyAmount.fromRawAmount(quoteCurrency, quoteRaw.toString())
const { gasEstimate, gasCostInToken, gasCostInUSD } = gasModel.estimateGasCost(
{
...route,
quote,
},
- { initializedTickCrossedList: quoteResult.result[2] },
+ { initializedTickCrossedList },
)
routesWithQuote.push({
@@ -439,3 +456,41 @@ export const createV3OnChainQuoteProvider = onChainQuoteProviderFactory({
`0x${route.amount.quotient.toString(16)}`,
],
})
+
+export const createV4ClOnChainQuoteProvider = onChainQuoteProviderFactory({
+ getQuoterAddress: (chainId) => (V4_CL_QUOTER_ADDRESSES as any)[chainId],
+ getQuoteFunctionName: (isExactIn) => (isExactIn ? 'quoteExactInput' : 'quoteExactOutput'),
+ abi: clQuoterAbi,
+ getCallInputs: (route, isExactIn) => [
+ {
+ exactCurrency: getCurrencyAddress(isExactIn ? route.input : route.output),
+ path: encodeV4RouteToPath(route, !isExactIn),
+ exactAmount: `0x${route.amount.quotient.toString(16)}`,
+ },
+ ],
+})
+
+export const createMixedRouteOnChainQuoteProviderV2 = onChainQuoteProviderFactory({
+ getQuoterAddress: (chainId) => (V4_MIXED_ROUTE_QUOTER_ADDRESSES as any)[chainId],
+ getQuoteFunctionName: () => 'quoteMixedExactInput',
+ abi: v4MixedRouteQuoterAbi,
+ getCallInputs: (route) => [
+ route.path.map((p) => getCurrencyAddress(p)),
+ encodeV4MixedRouteActions(route),
+ encodeV4MixedRouteParams(route),
+ `0x${route.amount.quotient.toString(16)}`,
+ ],
+})
+
+export const createV4BinOnChainQuoteProvider = onChainQuoteProviderFactory({
+ getQuoterAddress: (chainId) => (V4_BIN_QUOTER_ADDRESSES as any)[chainId],
+ getQuoteFunctionName: (isExactIn) => (isExactIn ? 'quoteExactInput' : 'quoteExactOutput'),
+ abi: binQuoterAbi,
+ getCallInputs: (route, isExactIn) => [
+ {
+ exactCurrency: getCurrencyAddress(isExactIn ? route.input : route.output),
+ path: encodeV4RouteToPath(route, !isExactIn),
+ exactAmount: `0x${route.amount.quotient.toString(16)}`,
+ },
+ ],
+})
diff --git a/packages/smart-router/evm/v3-router/providers/poolProviders/internalTypes.ts b/packages/smart-router/evm/v3-router/providers/poolProviders/internalTypes.ts
index 03661efd1306b..956f3e05569b8 100644
--- a/packages/smart-router/evm/v3-router/providers/poolProviders/internalTypes.ts
+++ b/packages/smart-router/evm/v3-router/providers/poolProviders/internalTypes.ts
@@ -1,12 +1,11 @@
import { Currency } from '@pancakeswap/sdk'
import { FeeAmount } from '@pancakeswap/v3-sdk'
-import { Address } from 'viem'
// Information used to identify a pool
export interface PoolMeta {
currencyA: Currency
currencyB: Currency
- address: Address
+ id: `0x${string}`
}
export interface V3PoolMeta extends PoolMeta {
diff --git a/packages/smart-router/evm/v3-router/providers/poolProviders/onChainPoolProviders.ts b/packages/smart-router/evm/v3-router/providers/poolProviders/onChainPoolProviders.ts
index 8c9c86d94c5d5..388d181e9be6b 100644
--- a/packages/smart-router/evm/v3-router/providers/poolProviders/onChainPoolProviders.ts
+++ b/packages/smart-router/evm/v3-router/providers/poolProviders/onChainPoolProviders.ts
@@ -14,9 +14,9 @@ import { PoolMeta, V3PoolMeta } from './internalTypes'
export const getV2PoolsOnChain = createOnChainPoolFactory({
abi: pancakePairABI,
getPossiblePoolMetas: ([currencyA, currencyB]) => [
- { address: computeV2PoolAddress(currencyA.wrapped, currencyB.wrapped), currencyA, currencyB },
+ { id: computeV2PoolAddress(currencyA.wrapped, currencyB.wrapped), currencyA, currencyB },
],
- buildPoolInfoCalls: ({ address }) => [
+ buildPoolInfoCalls: ({ id: address }) => [
{
address,
functionName: 'getReserves',
@@ -53,12 +53,12 @@ export const getStablePoolsOnChain = createOnChainPoolFactory ({
- address: stableSwapAddress,
+ id: stableSwapAddress,
currencyA,
currencyB,
}))
},
- buildPoolInfoCalls: ({ address }) => [
+ buildPoolInfoCalls: ({ id: address }) => [
{
address,
functionName: 'balances',
@@ -85,7 +85,7 @@ export const getStablePoolsOnChain = createOnChainPoolFactory {
+ buildPool: ({ currencyA, currencyB, id: address }, [balance0, balance1, a, fee, feeDenominator]) => {
if (!balance0 || !balance1 || !a || !fee || !feeDenominator) {
return null
}
@@ -113,7 +113,7 @@ export const getV3PoolsWithoutTicksOnChain = createOnChainPoolFactory ({
- address: computeV3PoolAddress({
+ id: computeV3PoolAddress({
deployerAddress,
tokenA: currencyA.wrapped,
tokenB: currencyB.wrapped,
@@ -124,7 +124,7 @@ export const getV3PoolsWithoutTicksOnChain = createOnChainPoolFactory [
+ buildPoolInfoCalls: ({ id: address, currencyA, currencyB }) => [
{
address,
functionName: 'liquidity',
@@ -146,7 +146,7 @@ export const getV3PoolsWithoutTicksOnChain = createOnChainPoolFactory {
+ buildPool: ({ currencyA, currencyB, fee, id: address }, [liquidity, slot0, balanceA, balanceB]) => {
if (!slot0) {
return null
}
@@ -187,7 +187,7 @@ interface OnChainPoolFactoryParams TPool | null
}
-function createOnChainPoolFactory<
+export function createOnChainPoolFactory<
TPool extends Pool,
TPoolMeta extends PoolMeta = PoolMeta,
TAbi extends Abi | unknown[] = Abi,
@@ -213,9 +213,9 @@ function createOnChainPoolFactory<
for (const pair of pairs) {
const possiblePoolMetas = getPossiblePoolMetas(pair)
for (const meta of possiblePoolMetas) {
- if (!poolAddressSet.has(meta.address)) {
+ if (!poolAddressSet.has(meta.id)) {
poolMetas.push(meta)
- poolAddressSet.add(meta.address)
+ poolAddressSet.add(meta.id)
}
}
}
diff --git a/packages/smart-router/evm/v3-router/providers/poolProviders/subgraphPoolProviders.ts b/packages/smart-router/evm/v3-router/providers/poolProviders/subgraphPoolProviders.ts
index 6b57177ecd8bd..3da6e5fcc15a7 100644
--- a/packages/smart-router/evm/v3-router/providers/poolProviders/subgraphPoolProviders.ts
+++ b/packages/smart-router/evm/v3-router/providers/poolProviders/subgraphPoolProviders.ts
@@ -56,7 +56,7 @@ function subgraphPoolProviderFactory({
for (const pair of pairs) {
const metas = getPoolMetas(pair)
for (const meta of metas) {
- metaMap.set(meta.address.toLocaleLowerCase() as Address, meta)
+ metaMap.set(meta.id.toLocaleLowerCase() as Address, meta)
}
}
const addresses = Array.from(metaMap.keys())
@@ -75,7 +75,7 @@ function subgraphPoolProviderFactory({
const getV3PoolMeta = memoize(
([currencyA, currencyB, feeAmount]: [Currency, Currency, FeeAmount]) => ({
- address: Pool.getAddress(currencyA.wrapped, currencyB.wrapped, feeAmount),
+ id: Pool.getAddress(currencyA.wrapped, currencyB.wrapped, feeAmount),
currencyA,
currencyB,
fee: feeAmount,
@@ -144,7 +144,7 @@ export const getV3PoolSubgraph = subgraphPoolProviderFactory {
@@ -205,7 +205,7 @@ export const getV2PoolSubgraph = subgraphPoolProviderFactory {
const { onChainProvider, multicallConfigs, gasLimit } = config
const offChainQuoteProvider = createOffChainQuoteProvider()
- const mixedRouteOnChainQuoteProvider = createMixedRouteOnChainQuoteProvider({
+ const mixedRouteOnChainQuoteProviderV1 = createMixedRouteOnChainQuoteProvider({
+ onChainProvider,
+ multicallConfigs,
+ gasLimit,
+ })
+ const mixedRouteOnChainQuoteProviderV2 = createMixedRouteOnChainQuoteProviderV2({
onChainProvider,
multicallConfigs,
gasLimit,
})
const v3OnChainQuoteProvider = createV3OnChainQuoteProvider({ onChainProvider, multicallConfigs, gasLimit })
+ const v4ClOnChainQuoteProvider = createV4ClOnChainQuoteProvider({ onChainProvider, multicallConfigs, gasLimit })
+ const v4BinOnChainQuoteProvider = createV4BinOnChainQuoteProvider({ onChainProvider, multicallConfigs, gasLimit })
const createGetRouteWithQuotes = (isExactIn = true) => {
const getOffChainQuotes = isExactIn
? offChainQuoteProvider.getRouteWithQuotesExactIn
: offChainQuoteProvider.getRouteWithQuotesExactOut
- const getMixedRouteQuotes = isExactIn
- ? mixedRouteOnChainQuoteProvider.getRouteWithQuotesExactIn
- : mixedRouteOnChainQuoteProvider.getRouteWithQuotesExactOut
const getV3Quotes = isExactIn
? v3OnChainQuoteProvider.getRouteWithQuotesExactIn
: v3OnChainQuoteProvider.getRouteWithQuotesExactOut
+ const getV4ClQuotes = isExactIn
+ ? v4ClOnChainQuoteProvider.getRouteWithQuotesExactIn
+ : v4ClOnChainQuoteProvider.getRouteWithQuotesExactOut
+ const getV4BinQuotes = isExactIn
+ ? v4BinOnChainQuoteProvider.getRouteWithQuotesExactIn
+ : v4BinOnChainQuoteProvider.getRouteWithQuotesExactOut
+ const createMixedRouteQuoteFetcher = (chainId: ChainId) => {
+ const mixedRouteOnChainQuoteProvider = V4_SUPPORTED_CHAINS.includes(chainId)
+ ? mixedRouteOnChainQuoteProviderV2
+ : mixedRouteOnChainQuoteProviderV1
+ return isExactIn
+ ? mixedRouteOnChainQuoteProvider.getRouteWithQuotesExactIn
+ : mixedRouteOnChainQuoteProvider.getRouteWithQuotesExactOut
+ }
return async function getRoutesWithQuotes(
routes: RouteWithoutQuote[],
{ blockNumber, gasModel, signal }: QuoterOptions,
): Promise {
+ const { chainId } = routes[0].input
+ const getMixedRouteQuotes = createMixedRouteQuoteFetcher(chainId)
+
+ const v4ClRoutes: RouteWithoutQuote[] = []
+ const v4BinRoutes: RouteWithoutQuote[] = []
const v3SingleHopRoutes: RouteWithoutQuote[] = []
const v3MultihopRoutes: RouteWithoutQuote[] = []
const mixedRoutesHaveV3Pool: RouteWithoutQuote[] = []
@@ -46,6 +77,14 @@ export function createQuoteProvider(config: QuoterConfig): QuoteProvider isV3Pool(pool))) {
mixedRoutesHaveV3Pool.push(route)
@@ -59,6 +98,8 @@ export function createQuoteProvider(config: QuoterConfig): QuoteProvider result.status === 'rejected')) {
throw new Error(results.map((result) => (result as PromiseRejectedResult).reason).join(','))
diff --git a/packages/smart-router/evm/v3-router/schema.ts b/packages/smart-router/evm/v3-router/schema.ts
index 652b71f0f3eb9..fd604bb2503ef 100644
--- a/packages/smart-router/evm/v3-router/schema.ts
+++ b/packages/smart-router/evm/v3-router/schema.ts
@@ -11,6 +11,7 @@ const zTradeType = z.nativeEnum(TradeType)
const zPoolType = z.nativeEnum(PoolType)
const zPoolTypes = z.array(zPoolType)
const zAddress = z.custom((val) => /^0x[a-fA-F0-9]{40}$/.test(val as string))
+const zHex = z.custom((val) => /^0x[a-fA-F0-9]*$/.test(val as string))
const zBigNumber = z.string().regex(/^[0-9]+$/)
const zCurrency = z
.object({
@@ -53,10 +54,43 @@ const zStablePool = z
balances: z.array(zCurrencyAmount),
amplifier: zBigNumber,
fee: z.string(),
+ address: zAddress,
+ })
+ .required()
+const zV4ClPool = z
+ .object({
+ type: z.literal(PoolType.V4CL),
+ currency0: zCurrency,
+ currency1: zCurrency,
+ fee: zFee,
+ liquidity: zBigNumber,
+ sqrtRatioX96: zBigNumber,
+ tick: z.number(),
+ tickSpacing: z.number(),
+ poolManager: zAddress,
+ id: zHex,
})
.required()
+ .extend({
+ hooks: zAddress.optional(),
+ })
+const zV4BinPool = z
+ .object({
+ type: z.literal(PoolType.V4BIN),
+ currency0: zCurrency,
+ currency1: zCurrency,
+ fee: zFee,
+ activeId: z.number(),
+ binStep: z.number(),
+ poolManager: zAddress,
+ id: zHex,
+ })
+ .required()
+ .extend({
+ hooks: zAddress.optional(),
+ })
-export const zPools = z.array(z.union([zV2Pool, zV3Pool, zStablePool]))
+export const zPools = z.array(z.union([zV2Pool, zV3Pool, zStablePool, zV4ClPool, zV4BinPool]))
export const zRouterGetParams = z
.object({
diff --git a/packages/smart-router/evm/v3-router/smartRouter.ts b/packages/smart-router/evm/v3-router/smartRouter.ts
index 1a7d3f7960084..65869cca02baf 100644
--- a/packages/smart-router/evm/v3-router/smartRouter.ts
+++ b/packages/smart-router/evm/v3-router/smartRouter.ts
@@ -11,6 +11,8 @@ export {
isV2Pool,
isV3Pool,
isStablePool,
+ isV4ClPool,
+ isV4BinPool,
getMidPrice,
involvesCurrency,
encodeMixedRouteToPath,
diff --git a/packages/smart-router/evm/v3-router/types/index.ts b/packages/smart-router/evm/v3-router/types/index.ts
index 1a7ab90943b19..87d708a977ad8 100644
--- a/packages/smart-router/evm/v3-router/types/index.ts
+++ b/packages/smart-router/evm/v3-router/types/index.ts
@@ -5,3 +5,4 @@ export * from './providers'
export * from './gasModel'
export * from './gasCost'
export * from './poolSelector'
+export * from './v4MixedQuoter'
diff --git a/packages/smart-router/evm/v3-router/types/pool.ts b/packages/smart-router/evm/v3-router/types/pool.ts
index 00d2db831e502..251604d6095f3 100644
--- a/packages/smart-router/evm/v3-router/types/pool.ts
+++ b/packages/smart-router/evm/v3-router/types/pool.ts
@@ -6,6 +6,8 @@ export enum PoolType {
V2,
V3,
STABLE,
+ V4CL,
+ V4BIN,
}
export interface BasePool {
@@ -48,7 +50,49 @@ export interface V3Pool extends BasePool {
reserve1?: CurrencyAmount
}
-export type Pool = V2Pool | V3Pool | StablePool
+export type V4ClPool = BasePool & {
+ id: `0x${string}`
+ type: PoolType.V4CL
+ currency0: Currency
+ currency1: Currency
+ fee: number
+ tickSpacing: number
+ liquidity: bigint
+ sqrtRatioX96: bigint
+ tick: number
+ hooks?: Address
+ poolManager: Address
+
+ // Allow pool with no ticks data provided
+ ticks?: Tick[]
+
+ reserve0?: CurrencyAmount
+ reserve1?: CurrencyAmount
+}
+
+type ActiveId = number
+type Reserve = {
+ reserveX: bigint
+ reserveY: bigint
+}
+export type V4BinPool = BasePool & {
+ id: `0x${string}`
+ type: PoolType.V4BIN
+ currency0: Currency
+ currency1: Currency
+ fee: number
+ binStep: number
+ activeId: ActiveId
+
+ hooks?: Address
+ poolManager: Address
+ reserveOfBin?: Record
+
+ reserve0?: CurrencyAmount
+ reserve1?: CurrencyAmount
+}
+
+export type Pool = V2Pool | V3Pool | StablePool | V4BinPool | V4ClPool
export interface WithTvl {
tvlUSD: bigint
diff --git a/packages/smart-router/evm/v3-router/types/route.ts b/packages/smart-router/evm/v3-router/types/route.ts
index 167ed0af5c431..570f641c40119 100644
--- a/packages/smart-router/evm/v3-router/types/route.ts
+++ b/packages/smart-router/evm/v3-router/types/route.ts
@@ -9,6 +9,8 @@ export enum RouteType {
STABLE,
MIXED,
MM,
+ V4CL,
+ V4BIN,
}
export interface BaseRoute {
diff --git a/packages/smart-router/evm/v3-router/types/v4MixedQuoter.ts b/packages/smart-router/evm/v3-router/types/v4MixedQuoter.ts
new file mode 100644
index 0000000000000..fd19df0df70b9
--- /dev/null
+++ b/packages/smart-router/evm/v3-router/types/v4MixedQuoter.ts
@@ -0,0 +1,8 @@
+export enum V4MixedQuoterActions {
+ SS_2_EXACT_INPUT_SINGLE = 0,
+ SS_3_EXACT_INPUT_SINGLE = 1,
+ V2_EXACT_INPUT_SINGLE = 2,
+ V3_EXACT_INPUT_SINGLE = 3,
+ V4_CL_EXACT_INPUT_SINGLE = 4,
+ V4_BIN_EXACT_INPUT_SINGLE = 5,
+}
diff --git a/packages/smart-router/evm/v3-router/utils/encodeV4MixedRouteActions.ts b/packages/smart-router/evm/v3-router/utils/encodeV4MixedRouteActions.ts
new file mode 100644
index 0000000000000..73950433885ca
--- /dev/null
+++ b/packages/smart-router/evm/v3-router/utils/encodeV4MixedRouteActions.ts
@@ -0,0 +1,29 @@
+import { Hex, bytesToHex } from 'viem'
+
+import { BaseRoute, V4MixedQuoterActions } from '../types'
+import { isStablePool, isV2Pool, isV3Pool, isV4BinPool, isV4ClPool } from './pool'
+
+export function encodeV4MixedRouteActions(route: BaseRoute): Hex {
+ return bytesToHex(
+ new Uint8Array(
+ route.pools.map((p) => {
+ if (isV2Pool(p)) {
+ return V4MixedQuoterActions.V2_EXACT_INPUT_SINGLE
+ }
+ if (isV3Pool(p)) {
+ return V4MixedQuoterActions.V3_EXACT_INPUT_SINGLE
+ }
+ if (isStablePool(p)) {
+ return V4MixedQuoterActions.SS_2_EXACT_INPUT_SINGLE
+ }
+ if (isV4ClPool(p)) {
+ return V4MixedQuoterActions.V4_CL_EXACT_INPUT_SINGLE
+ }
+ if (isV4BinPool(p)) {
+ return V4MixedQuoterActions.V4_BIN_EXACT_INPUT_SINGLE
+ }
+ throw new Error(`Unrecognized pool type ${p}`)
+ }),
+ ),
+ )
+}
diff --git a/packages/smart-router/evm/v3-router/utils/encodeV4MixedRouteParams.ts b/packages/smart-router/evm/v3-router/utils/encodeV4MixedRouteParams.ts
new file mode 100644
index 0000000000000..53d5c2e8274d7
--- /dev/null
+++ b/packages/smart-router/evm/v3-router/utils/encodeV4MixedRouteParams.ts
@@ -0,0 +1,76 @@
+import { PoolKey, encodePoolKey } from '@pancakeswap/v4-sdk'
+import { getCurrencyAddress } from '@pancakeswap/swap-sdk-core'
+import { Hex, encodeAbiParameters, parseAbiParameters } from 'viem'
+
+import { BaseRoute } from '../types'
+import { isStablePool, isV2Pool, isV3Pool, isV4BinPool, isV4ClPool } from './pool'
+
+const v4RouteParamsAbi = [
+ {
+ components: [
+ {
+ components: [
+ { name: 'currency0', type: 'address' },
+ { name: 'currency1', type: 'address' },
+ { name: 'hooks', type: 'address' },
+ { name: 'poolManager', type: 'address' },
+ { name: 'fee', type: 'uint24' },
+ { name: 'parameters', type: 'bytes32' },
+ ],
+ name: 'poolKey',
+ type: 'tuple',
+ },
+ { name: 'hookData', type: 'bytes' },
+ ],
+ name: 'params',
+ type: 'tuple',
+ },
+] as const
+
+export function encodeV4MixedRouteParams(route: BaseRoute): Hex[] {
+ return route.pools.map((p) => {
+ if (isV2Pool(p) || isStablePool(p)) {
+ return '0x'
+ }
+ if (isV3Pool(p)) {
+ return encodeAbiParameters(parseAbiParameters('uint24'), [p.fee])
+ }
+ if (isV4ClPool(p)) {
+ const poolKey: PoolKey<'CL'> = {
+ currency0: getCurrencyAddress(p.currency0),
+ currency1: getCurrencyAddress(p.currency1),
+ fee: p.fee,
+ hooks: p.hooks,
+ poolManager: p.poolManager,
+ parameters: {
+ tickSpacing: p.tickSpacing,
+ },
+ }
+ return encodeAbiParameters(v4RouteParamsAbi, [
+ {
+ poolKey: encodePoolKey(poolKey),
+ hookData: '0x',
+ },
+ ])
+ }
+ if (isV4BinPool(p)) {
+ const poolKey: PoolKey<'Bin'> = {
+ currency0: getCurrencyAddress(p.currency0),
+ currency1: getCurrencyAddress(p.currency1),
+ fee: p.fee,
+ hooks: p.hooks,
+ poolManager: p.poolManager,
+ parameters: {
+ binStep: Number(p.binStep),
+ },
+ }
+ return encodeAbiParameters(v4RouteParamsAbi, [
+ {
+ poolKey: encodePoolKey(poolKey),
+ hookData: '0x',
+ },
+ ])
+ }
+ throw new Error(`Invalid pool type ${p}`)
+ })
+}
diff --git a/packages/smart-router/evm/v3-router/utils/encodeV4RouteToPath.ts b/packages/smart-router/evm/v3-router/utils/encodeV4RouteToPath.ts
new file mode 100644
index 0000000000000..83e70aad73e0e
--- /dev/null
+++ b/packages/smart-router/evm/v3-router/utils/encodeV4RouteToPath.ts
@@ -0,0 +1,68 @@
+import { Address, zeroAddress } from 'viem'
+import { Currency, getCurrencyAddress } from '@pancakeswap/swap-sdk-core'
+import { encodePoolParameters } from '@pancakeswap/v4-sdk'
+
+import { BaseRoute, Pool } from '../types'
+import { getOutputCurrency, isV4BinPool, isV4ClPool } from './pool'
+
+export type PathKey = {
+ intermediateCurrency: Address
+ fee: number
+ hooks: Address
+ poolManager: Address
+ hookData: `0x${string}`
+ parameters: `0x${string}`
+}
+
+/**
+ * Converts a route to an array of path key
+ * @param route the mixed path to convert to an encoded path
+ * @returns the encoded path keys
+ */
+export function encodeV4RouteToPath(route: BaseRoute, exactOutput: boolean): PathKey[] {
+ if (route.pools.some((p) => !isV4ClPool(p) && !isV4BinPool(p))) {
+ throw new Error('Failed to encode path keys. Invalid v4 pool found in route.')
+ }
+
+ const currencyStart = exactOutput ? route.output : route.input
+ const pools = exactOutput ? [...route.pools].reverse() : route.pools
+
+ const { path } = pools.reduce(
+ (
+ // eslint-disable-next-line @typescript-eslint/no-shadow
+ { baseCurrency, path }: { baseCurrency: Currency; path: PathKey[] },
+ pool: Pool,
+ ): { baseCurrency: Currency; path: PathKey[] } => {
+ const isV4Cl = isV4ClPool(pool)
+ const isV4Bin = isV4BinPool(pool)
+ if (!isV4Cl && !isV4Bin) throw new Error(`Invalid v4 pool ${pool}`)
+ const quoteCurrency = getOutputCurrency(pool, baseCurrency)
+ const parameters = encodePoolParameters(
+ isV4Cl
+ ? {
+ tickSpacing: pool.tickSpacing,
+ }
+ : {
+ binStep: pool.binStep,
+ },
+ )
+ return {
+ baseCurrency: quoteCurrency,
+ path: [
+ ...path,
+ {
+ intermediateCurrency: getCurrencyAddress(quoteCurrency),
+ fee: pool.fee,
+ hooks: pool.hooks ?? zeroAddress,
+ poolManager: pool.poolManager,
+ hookData: '0x',
+ parameters,
+ },
+ ],
+ }
+ },
+ { baseCurrency: currencyStart, path: [] },
+ )
+
+ return exactOutput ? path.reverse() : path
+}
diff --git a/packages/smart-router/evm/v3-router/utils/getPriceImpact.ts b/packages/smart-router/evm/v3-router/utils/getPriceImpact.ts
index 280e4eddd4290..ee6f2ddcc2e9c 100644
--- a/packages/smart-router/evm/v3-router/utils/getPriceImpact.ts
+++ b/packages/smart-router/evm/v3-router/utils/getPriceImpact.ts
@@ -8,13 +8,15 @@ export function getPriceImpact(
routes: Pick[]
},
): Percent {
- let spotOutputAmount = CurrencyAmount.fromRawAmount(trade.outputAmount.currency.wrapped, 0)
+ let spotOutputAmount = CurrencyAmount.fromRawAmount(trade.outputAmount.currency, 0)
for (const route of trade.routes) {
const { inputAmount } = route
// FIXME typing
const midPrice: any = getMidPrice(route)
- spotOutputAmount = spotOutputAmount.add(midPrice.quote(inputAmount.wrapped))
+ spotOutputAmount = spotOutputAmount.add(
+ CurrencyAmount.fromRawAmount(trade.outputAmount.currency, midPrice.wrapped.quote(inputAmount.wrapped).quotient),
+ )
}
- const priceImpact = spotOutputAmount.subtract(trade.outputAmount.wrapped).divide(spotOutputAmount)
+ const priceImpact = spotOutputAmount.subtract(trade.outputAmount).divide(spotOutputAmount)
return new Percent(priceImpact.numerator, priceImpact.denominator)
}
diff --git a/packages/smart-router/evm/v3-router/utils/pool.ts b/packages/smart-router/evm/v3-router/utils/pool.ts
index 222ea07a5dc24..20455b8d422ac 100644
--- a/packages/smart-router/evm/v3-router/utils/pool.ts
+++ b/packages/smart-router/evm/v3-router/utils/pool.ts
@@ -1,3 +1,4 @@
+import { SCALE, getPriceFromId } from '@pancakeswap/v4-sdk'
import { Currency, Pair, Price } from '@pancakeswap/sdk'
import { Pool as SDKV3Pool, computePoolAddress } from '@pancakeswap/v3-sdk'
import tryParseAmount from '@pancakeswap/utils/tryParseAmount'
@@ -5,7 +6,7 @@ import { getSwapOutput } from '@pancakeswap/stable-swap-sdk'
import memoize from 'lodash/memoize.js'
import { Address } from 'viem'
-import { Pool, PoolType, StablePool, V2Pool, V3Pool } from '../types'
+import { Pool, PoolType, StablePool, V2Pool, V3Pool, V4BinPool, V4ClPool } from '../types'
export function isV2Pool(pool: Pool): pool is V2Pool {
return pool.type === PoolType.V2
@@ -19,6 +20,14 @@ export function isStablePool(pool: Pool): pool is StablePool {
return pool.type === PoolType.STABLE && pool.balances.length >= 2
}
+export function isV4BinPool(pool: Pool): pool is V4BinPool {
+ return pool.type === PoolType.V4BIN
+}
+
+export function isV4ClPool(pool: Pool): pool is V4ClPool {
+ return pool.type === PoolType.V4CL
+}
+
export function involvesCurrency(pool: Pool, currency: Currency) {
const token = currency.wrapped
if (isV2Pool(pool)) {
@@ -29,6 +38,15 @@ export function involvesCurrency(pool: Pool, currency: Currency) {
const { token0, token1 } = pool
return token0.equals(token) || token1.equals(token)
}
+ if (isV4ClPool(pool) || isV4BinPool(pool)) {
+ const { currency0, currency1 } = pool
+ return (
+ currency0.equals(currency) ||
+ currency1.equals(currency) ||
+ currency0.wrapped.equals(token) ||
+ currency1.wrapped.equals(token)
+ )
+ }
if (isStablePool(pool)) {
const { balances } = pool
return balances.some((b) => b.currency.equals(token))
@@ -36,7 +54,7 @@ export function involvesCurrency(pool: Pool, currency: Currency) {
return false
}
-// FIXME current version is not working with stable pools that have more than 2 tokens
+// FIXME: current version is not working with stable pools that have more than 2 tokens
export function getOutputCurrency(pool: Pool, currencyIn: Currency): Currency {
const tokenIn = currencyIn.wrapped
if (isV2Pool(pool)) {
@@ -51,6 +69,10 @@ export function getOutputCurrency(pool: Pool, currencyIn: Currency): Currency {
const { balances } = pool
return balances[0].currency.equals(tokenIn) ? balances[1].currency : balances[0].currency
}
+ if (isV4ClPool(pool) || isV4BinPool(pool)) {
+ const { currency0, currency1 } = pool
+ return currency0.wrapped.equals(tokenIn) ? currency1 : currency0
+ }
throw new Error('Cannot get output currency by invalid pool')
}
@@ -84,9 +106,11 @@ export const getPoolAddress = memoize(
}
const [token0, token1] = isV2Pool(pool)
? [pool.reserve0.currency.wrapped, pool.reserve1.currency.wrapped]
- : [pool.token0.wrapped, pool.token1.wrapped]
+ : isV3Pool(pool)
+ ? [pool.token0.wrapped, pool.token1.wrapped]
+ : [pool.currency0, pool.currency1]
const fee = isV3Pool(pool) ? pool.fee : 'V2_FEE'
- return `${pool.type}_${token0.chainId}_${token0.address}_${token1.address}_${fee}`
+ return `${pool.type}_${token0.chainId}_${token0.isNative}_${token0.wrapped.address}_${token1.isNative}_${token1.wrapped.address}_${fee}`
},
)
@@ -97,6 +121,28 @@ export function getTokenPrice(pool: Pool, base: Currency, quote: Currency): Pric
return v3Pool.priceOf(base.wrapped)
}
+ if (isV4ClPool(pool)) {
+ const { currency0, currency1, fee, liquidity, sqrtRatioX96, tick } = pool
+ const v3Pool = new SDKV3Pool(currency0.wrapped, currency1.wrapped, fee, sqrtRatioX96, liquidity, tick)
+ const tokenPrice = v3Pool.priceOf(base.wrapped)
+ const [baseCurrency, quoteCurrency] = base.wrapped.equals(currency0.wrapped)
+ ? [currency0, currency1]
+ : [currency1, currency0]
+ return new Price(baseCurrency, quoteCurrency, tokenPrice.denominator, tokenPrice.numerator)
+ }
+
+ if (isV4BinPool(pool)) {
+ const { activeId, binStep, currency0, currency1 } = pool
+ const rawPrice = getPriceFromId(BigInt(activeId), BigInt(binStep))
+ const price = new Price(
+ currency0,
+ currency1,
+ rawPrice * 10n ** BigInt(currency0.decimals),
+ SCALE * 10n ** BigInt(currency1.decimals),
+ )
+ return base.equals(price.baseCurrency) ? price : price.invert()
+ }
+
if (isV2Pool(pool)) {
const pair = new Pair(pool.reserve0.wrapped, pool.reserve1.wrapped)
return pair.priceOf(base.wrapped)
diff --git a/packages/smart-router/evm/v3-router/utils/route.ts b/packages/smart-router/evm/v3-router/utils/route.ts
index 0d11945fffe67..86cc4d3b9a90f 100644
--- a/packages/smart-router/evm/v3-router/utils/route.ts
+++ b/packages/smart-router/evm/v3-router/utils/route.ts
@@ -5,7 +5,7 @@ import { BaseRoute, Pool, RouteType, PoolType, Route } from '../types'
import { getOutputCurrency, getTokenPrice } from './pool'
export function buildBaseRoute(pools: Pool[], currencyIn: Currency, currencyOut: Currency): BaseRoute {
- const path: Currency[] = [currencyIn.wrapped]
+ const path: Currency[] = [currencyIn]
let prevIn = path[0]
let routeType: RouteType | null = null
const updateRouteType = (pool: Pool, currentRouteType: RouteType | null) => {
@@ -17,10 +17,15 @@ export function buildBaseRoute(pools: Pool[], currencyIn: Currency, currencyOut:
}
return currentRouteType
}
+ const lastPool = pools[pools.length - 1]
for (const pool of pools) {
+ routeType = updateRouteType(pool, routeType)
+ if (pool === lastPool) {
+ path.push(currencyOut)
+ continue
+ }
prevIn = getOutputCurrency(pool, prevIn)
path.push(prevIn)
- routeType = updateRouteType(pool, routeType)
}
if (routeType === null) {
@@ -44,6 +49,10 @@ function getRouteTypeFromPool(pool: Pick) {
return RouteType.V3
case PoolType.STABLE:
return RouteType.STABLE
+ case PoolType.V4CL:
+ return RouteType.V4CL
+ case PoolType.V4BIN:
+ return RouteType.V4BIN
default:
return RouteType.MIXED
}
@@ -69,13 +78,19 @@ export function getQuoteCurrency({ input, output }: BaseRoute, baseCurrency: Cur
return baseCurrency.equals(input) ? output : input
}
+function wrapPrice(price: Price): Price {
+ return new Price(price.baseCurrency.wrapped, price.quoteCurrency.wrapped, price.denominator, price.numerator)
+}
+
export function getMidPrice({ path, pools }: Pick) {
let i = 0
let price: Price | null = null
+ const currencyIn = path[0]
+ const currencyOut = path[path.length - 1]
for (const pool of pools) {
- const input = path[i].wrapped
- const output = path[i + 1].wrapped
- const poolPrice = getTokenPrice(pool, input, output)
+ const input = path[i]
+ const output = path[i + 1]
+ const poolPrice = wrapPrice(getTokenPrice(pool, input, output))
price = price ? price.multiply(poolPrice) : poolPrice
i += 1
@@ -84,5 +99,5 @@ export function getMidPrice({ path, pools }: Pick) {
if (!price) {
throw new Error('Get mid price failed')
}
- return price
+ return new Price(currencyIn, currencyOut, price.denominator, price.numerator)
}
diff --git a/packages/smart-router/evm/v3-router/utils/transformer.ts b/packages/smart-router/evm/v3-router/utils/transformer.ts
index 07677355cbacd..63846cab54cf5 100644
--- a/packages/smart-router/evm/v3-router/utils/transformer.ts
+++ b/packages/smart-router/evm/v3-router/utils/transformer.ts
@@ -2,8 +2,8 @@ import { ChainId } from '@pancakeswap/chains'
import { Currency, CurrencyAmount, ERC20Token, Native, Percent, TradeType } from '@pancakeswap/sdk'
import { ADDRESS_ZERO, Tick } from '@pancakeswap/v3-sdk'
import { Address } from 'viem'
-import { Pool, PoolType, Route, SmartRouterTrade, StablePool, V2Pool, V3Pool } from '../types'
-import { isStablePool, isV2Pool, isV3Pool } from './pool'
+import { Pool, PoolType, Route, SmartRouterTrade, StablePool, V2Pool, V3Pool, V4BinPool, V4ClPool } from '../types'
+import { isStablePool, isV2Pool, isV3Pool, isV4BinPool, isV4ClPool } from './pool'
const ONE_HUNDRED = 100n
@@ -53,13 +53,36 @@ export interface SerializedV3Pool
ticks?: SerializedTick[]
}
+export interface SerializedV4ClPool
+ extends Omit {
+ currency0: SerializedCurrency
+ currency1: SerializedCurrency
+ liquidity: string
+ sqrtRatioX96: string
+ reserve0?: SerializedCurrencyAmount
+ reserve1?: SerializedCurrencyAmount
+ ticks?: SerializedTick[]
+}
+
+export interface SerializedV4BinPool extends Omit {
+ currency0: SerializedCurrency
+ currency1: SerializedCurrency
+ reserve0?: SerializedCurrencyAmount
+ reserve1?: SerializedCurrencyAmount
+}
+
export interface SerializedStablePool extends Omit {
balances: SerializedCurrencyAmount[]
amplifier: string
fee: string
}
-export type SerializedPool = SerializedV2Pool | SerializedV3Pool | SerializedStablePool
+export type SerializedPool =
+ | SerializedV2Pool
+ | SerializedV3Pool
+ | SerializedStablePool
+ | SerializedV4ClPool
+ | SerializedV4BinPool
export interface SerializedRoute
extends Omit {
@@ -134,6 +157,27 @@ export function serializePool(pool: Pool): SerializedPool {
fee: pool.fee.toSignificant(6),
}
}
+ if (isV4ClPool(pool)) {
+ return {
+ ...pool,
+ currency0: serializeCurrency(pool.currency0),
+ currency1: serializeCurrency(pool.currency1),
+ liquidity: pool.liquidity.toString(),
+ sqrtRatioX96: pool.sqrtRatioX96.toString(),
+ ticks: pool.ticks?.map(serializeTick),
+ reserve0: pool.reserve0 && serializeCurrencyAmount(pool.reserve0),
+ reserve1: pool.reserve1 && serializeCurrencyAmount(pool.reserve1),
+ }
+ }
+ if (isV4BinPool(pool)) {
+ return {
+ ...pool,
+ currency0: serializeCurrency(pool.currency0),
+ currency1: serializeCurrency(pool.currency1),
+ reserve0: pool.reserve0 && serializeCurrencyAmount(pool.reserve0),
+ reserve1: pool.reserve1 && serializeCurrencyAmount(pool.reserve1),
+ }
+ }
throw new Error('Cannot serialize unsupoorted pool')
}
@@ -204,6 +248,27 @@ export function parsePool(chainId: ChainId, pool: SerializedPool): Pool {
fee: new Percent(parseFloat(pool.fee) * 1000000, ONE_HUNDRED * 1000000n),
}
}
+ if (pool.type === PoolType.V4CL) {
+ return {
+ ...pool,
+ currency0: parseCurrency(chainId, pool.currency0),
+ currency1: parseCurrency(chainId, pool.currency1),
+ liquidity: BigInt(pool.liquidity),
+ sqrtRatioX96: BigInt(pool.sqrtRatioX96),
+ ticks: pool.ticks?.map(parseTick),
+ reserve0: pool.reserve0 && parseCurrencyAmount(chainId, pool.reserve0),
+ reserve1: pool.reserve1 && parseCurrencyAmount(chainId, pool.reserve1),
+ }
+ }
+ if (pool.type === PoolType.V4BIN) {
+ return {
+ ...pool,
+ currency0: parseCurrency(chainId, pool.currency0),
+ currency1: parseCurrency(chainId, pool.currency1),
+ reserve0: pool.reserve0 && parseCurrencyAmount(chainId, pool.reserve0),
+ reserve1: pool.reserve1 && parseCurrencyAmount(chainId, pool.reserve1),
+ }
+ }
throw new Error('Cannot parse unsupoorted pool')
}
diff --git a/packages/smart-router/evm/v4-router/constants/index.ts b/packages/smart-router/evm/v4-router/constants/index.ts
index 560f1e57d1ce0..858f0c89642ff 100644
--- a/packages/smart-router/evm/v4-router/constants/index.ts
+++ b/packages/smart-router/evm/v4-router/constants/index.ts
@@ -1 +1,2 @@
export * from './v3PoolFetchGasLimit'
+export * from './v4Pools'
diff --git a/packages/smart-router/evm/v4-router/constants/v4Pools.ts b/packages/smart-router/evm/v4-router/constants/v4Pools.ts
new file mode 100644
index 0000000000000..297cdaaeb3e34
--- /dev/null
+++ b/packages/smart-router/evm/v4-router/constants/v4Pools.ts
@@ -0,0 +1,35 @@
+// TODO: move to sdk
+
+import { zeroAddress, type Address } from 'viem'
+
+export const CL_PRESETS: {
+ fee: number
+ tickSpacing: number
+}[] = [
+ {
+ fee: 500,
+ tickSpacing: 10,
+ },
+ {
+ fee: 100,
+ tickSpacing: 1000,
+ },
+]
+
+export const CL_HOOKS: Address[] = [zeroAddress]
+
+export const BIN_PRESETS: {
+ fee: number
+ binStep: number
+}[] = [
+ {
+ fee: 500,
+ binStep: 10,
+ },
+ {
+ fee: 100,
+ binStep: 10,
+ },
+]
+
+export const BIN_HOOKS: Address[] = [zeroAddress]
diff --git a/packages/smart-router/evm/v4-router/queries/getV4BinPools.ts b/packages/smart-router/evm/v4-router/queries/getV4BinPools.ts
new file mode 100644
index 0000000000000..1526cde206a50
--- /dev/null
+++ b/packages/smart-router/evm/v4-router/queries/getV4BinPools.ts
@@ -0,0 +1,101 @@
+import { Currency, getCurrencyAddress, sortCurrencies } from '@pancakeswap/swap-sdk-core'
+import { BinPoolManager, PoolKey, getPoolId } from '@pancakeswap/v4-sdk'
+import { Native } from '@pancakeswap/sdk'
+import { Address } from 'viem'
+
+import { getPairCombinations } from '../../v3-router/functions'
+import { PoolType, V4BinPool } from '../../v3-router/types'
+import { PoolMeta } from '../../v3-router/providers/poolProviders/internalTypes'
+import { createOnChainPoolFactory } from '../../v3-router/providers'
+import { BIN_HOOKS, BIN_PRESETS } from '../constants'
+import { GetV4CandidatePoolsParams } from '../types'
+
+export async function getV4BinCandidatePools({
+ currencyA,
+ currencyB,
+ clientProvider,
+ gasLimit,
+}: GetV4CandidatePoolsParams) {
+ if (!currencyA || !currencyB) {
+ throw new Error(`Invalid currencyA ${currencyA} or currencyB ${currencyB}`)
+ }
+ const native = Native.onChain(currencyA?.chainId)
+ const wnative = native.wrapped
+ const pairs = await getPairCombinations(currencyA, currencyB)
+ const pairsWithNative = [...pairs]
+ for (const pair of pairs) {
+ const index = pair.findIndex((c) => c.wrapped.equals(wnative))
+ if (index >= 0) {
+ const pairWithNative = [...pair]
+ pairWithNative[index] = native
+ pairsWithNative.push(pairWithNative as [Currency, Currency])
+ }
+ }
+ return getV4BinPoolsWithoutBins(pairsWithNative, clientProvider)
+}
+
+type V4BinPoolMeta = PoolMeta & {
+ fee: number
+ poolManager: Address
+ binStep: number
+ hooks: Address
+}
+
+export const getV4BinPoolsWithoutBins = createOnChainPoolFactory({
+ abi: BinPoolManager,
+ getPossiblePoolMetas: ([currencyA, currencyB]) => {
+ const [currency0, currency1] = sortCurrencies([currencyA, currencyB])
+ const metas: V4BinPoolMeta[] = []
+ for (const { fee, binStep } of BIN_PRESETS) {
+ for (const hooks of BIN_HOOKS) {
+ const poolKey: PoolKey<'Bin'> = {
+ currency0: getCurrencyAddress(currency0),
+ currency1: getCurrencyAddress(currency1),
+ fee,
+ parameters: {
+ binStep,
+ },
+ // TODO: use constant from v4 sdk
+ poolManager: '0x1DF0be383e9d17DA4448E57712849aBE5b3Fa33b' as const,
+ hooks,
+ }
+ const id = getPoolId(poolKey)
+ metas.push({
+ currencyA,
+ currencyB,
+ fee,
+ binStep,
+ hooks,
+ poolManager: poolKey.poolManager,
+ id,
+ })
+ }
+ }
+ return metas
+ },
+ buildPoolInfoCalls: ({ id, poolManager: address }) => [
+ {
+ address,
+ functionName: 'getSlot0',
+ args: [id],
+ },
+ ],
+ buildPool: ({ currencyA, currencyB, fee, id, binStep, poolManager, hooks }, [slot0]) => {
+ if (!slot0 || !slot0[0]) {
+ return null
+ }
+ const [activeId] = slot0
+ const [currency0, currency1] = sortCurrencies([currencyA, currencyB])
+ return {
+ id,
+ type: PoolType.V4BIN,
+ currency0,
+ currency1,
+ fee,
+ activeId,
+ binStep,
+ poolManager,
+ hooks,
+ }
+ },
+})
diff --git a/packages/smart-router/evm/v4-router/queries/getV4ClPools.ts b/packages/smart-router/evm/v4-router/queries/getV4ClPools.ts
new file mode 100644
index 0000000000000..6df5b21b79219
--- /dev/null
+++ b/packages/smart-router/evm/v4-router/queries/getV4ClPools.ts
@@ -0,0 +1,108 @@
+import { Currency, getCurrencyAddress, sortCurrencies } from '@pancakeswap/swap-sdk-core'
+import { PoolKey, getPoolId, CLPoolManager } from '@pancakeswap/v4-sdk'
+import { Native } from '@pancakeswap/sdk'
+import { Address } from 'viem'
+
+import { getPairCombinations } from '../../v3-router/functions'
+import { PoolType, V4ClPool } from '../../v3-router/types'
+import { PoolMeta } from '../../v3-router/providers/poolProviders/internalTypes'
+import { createOnChainPoolFactory } from '../../v3-router/providers'
+import { CL_HOOKS, CL_PRESETS } from '../constants'
+import { GetV4CandidatePoolsParams } from '../types'
+
+export async function getV4ClCandidatePools({
+ currencyA,
+ currencyB,
+ clientProvider,
+ gasLimit,
+}: GetV4CandidatePoolsParams) {
+ if (!currencyA || !currencyB) {
+ throw new Error(`Invalid currencyA ${currencyA} or currencyB ${currencyB}`)
+ }
+ const native = Native.onChain(currencyA?.chainId)
+ const wnative = native.wrapped
+ const pairs = await getPairCombinations(currencyA, currencyB)
+ const pairsWithNative = [...pairs]
+ for (const pair of pairs) {
+ const index = pair.findIndex((c) => c.wrapped.equals(wnative))
+ if (index >= 0) {
+ const pairWithNative = [...pair]
+ pairWithNative[index] = native
+ pairsWithNative.push(pairWithNative as [Currency, Currency])
+ }
+ }
+ return getV4ClPoolsWithoutTicks(pairsWithNative, clientProvider)
+}
+
+type V4ClPoolMeta = PoolMeta & {
+ fee: number
+ poolManager: Address
+ tickSpacing: number
+ hooks: Address
+}
+
+export const getV4ClPoolsWithoutTicks = createOnChainPoolFactory({
+ abi: CLPoolManager,
+ getPossiblePoolMetas: ([currencyA, currencyB]) => {
+ const [currency0, currency1] = sortCurrencies([currencyA, currencyB])
+ const metas: V4ClPoolMeta[] = []
+ for (const { fee, tickSpacing } of CL_PRESETS) {
+ for (const hooks of CL_HOOKS) {
+ const poolKey: PoolKey<'CL'> = {
+ currency0: getCurrencyAddress(currency0),
+ currency1: getCurrencyAddress(currency1),
+ fee,
+ parameters: {
+ tickSpacing,
+ },
+ // TODO: use constant from v4 sdk
+ poolManager: '0x26Ca53c8C5CE90E22aA1FadDA68AB9a08f7BA06f' as const,
+ hooks,
+ }
+ const id = getPoolId(poolKey)
+ metas.push({
+ currencyA,
+ currencyB,
+ fee,
+ tickSpacing,
+ hooks,
+ poolManager: poolKey.poolManager,
+ id,
+ })
+ }
+ }
+ return metas
+ },
+ buildPoolInfoCalls: ({ id, poolManager: address }) => [
+ {
+ address,
+ functionName: 'getLiquidity',
+ args: [id],
+ },
+ {
+ address,
+ functionName: 'getSlot0',
+ args: [id],
+ },
+ ],
+ buildPool: ({ currencyA, currencyB, fee, id, tickSpacing, poolManager, hooks }, [liquidity, slot0]) => {
+ if (!slot0 || !slot0[0]) {
+ return null
+ }
+ const [sqrtPriceX96, tick] = slot0
+ const [currency0, currency1] = sortCurrencies([currencyA, currencyB])
+ return {
+ id,
+ type: PoolType.V4CL,
+ currency0,
+ currency1,
+ fee,
+ liquidity: BigInt(liquidity.toString()),
+ sqrtRatioX96: BigInt(sqrtPriceX96.toString()),
+ tick: Number(tick),
+ tickSpacing,
+ poolManager,
+ hooks,
+ }
+ },
+})
diff --git a/packages/smart-router/evm/v4-router/queries/index.ts b/packages/smart-router/evm/v4-router/queries/index.ts
index 206d9dbe54939..d7a89f418634e 100644
--- a/packages/smart-router/evm/v4-router/queries/index.ts
+++ b/packages/smart-router/evm/v4-router/queries/index.ts
@@ -1 +1,3 @@
export * from './getV3Pools'
+export * from './getV4ClPools'
+export * from './getV4BinPools'
diff --git a/packages/smart-router/evm/v4-router/schema.ts b/packages/smart-router/evm/v4-router/schema.ts
index 8e670dbac9912..51813e424238f 100644
--- a/packages/smart-router/evm/v4-router/schema.ts
+++ b/packages/smart-router/evm/v4-router/schema.ts
@@ -12,6 +12,7 @@ const zTradeType = z.nativeEnum(TradeType)
const zAddress = z.custom((val) => /^0x[a-fA-F0-9]{40}$/.test(val as string))
const zBigNumber = z.string().regex(/^[0-9]+$/)
const zSignedBigInt = z.string().regex(/^-?[0-9]+$/)
+const zHex = z.custom((val) => /^0x[a-fA-F0-9]*$/.test(val as string))
const zCurrency = z
.object({
address: zAddress,
@@ -74,10 +75,50 @@ const zStablePool = z
balances: z.array(zCurrencyAmount),
amplifier: zBigNumber,
fee: z.string(),
+ address: zAddress,
})
.required()
-export const zPools = z.array(z.union([zV3Pool, zV2Pool, zStablePool]))
+const zV4ClPool = z
+ .object({
+ type: z.literal(PoolType.V4CL),
+ currency0: zCurrency,
+ currency1: zCurrency,
+ fee: zFee,
+ liquidity: zBigNumber,
+ sqrtRatioX96: zBigNumber,
+ tick: z.number(),
+ tickSpacing: z.number(),
+ poolManager: zAddress,
+ id: zHex,
+ })
+ .required()
+ .extend({
+ reserve0: zCurrencyAmountOptional,
+ reserve1: zCurrencyAmountOptional,
+ hooks: zAddress.optional(),
+ ticks: z.array(zTick).optional(),
+ })
+
+const zV4BinPool = z
+ .object({
+ type: z.literal(PoolType.V4BIN),
+ currency0: zCurrency,
+ currency1: zCurrency,
+ fee: zFee,
+ activeId: z.number(),
+ binStep: z.number(),
+ poolManager: zAddress,
+ id: zHex,
+ })
+ .required()
+ .extend({
+ reserve0: zCurrencyAmountOptional,
+ reserve1: zCurrencyAmountOptional,
+ hooks: zAddress.optional(),
+ })
+
+export const zPools = z.array(z.union([zV3Pool, zV2Pool, zStablePool, zV4ClPool, zV4BinPool]))
export const zRouterPostParams = z
.object({
diff --git a/packages/smart-router/evm/v4-router/types/index.ts b/packages/smart-router/evm/v4-router/types/index.ts
index 2b92c357aad5b..55a8c019ccd4f 100644
--- a/packages/smart-router/evm/v4-router/types/index.ts
+++ b/packages/smart-router/evm/v4-router/types/index.ts
@@ -1,2 +1,3 @@
export * from './trade'
export * from './graph'
+export * from './pool'
diff --git a/packages/smart-router/evm/v4-router/types/pool.ts b/packages/smart-router/evm/v4-router/types/pool.ts
new file mode 100644
index 0000000000000..962d331892416
--- /dev/null
+++ b/packages/smart-router/evm/v4-router/types/pool.ts
@@ -0,0 +1,17 @@
+import type { BigintIsh, Currency } from '@pancakeswap/swap-sdk-core'
+
+import type { OnChainProvider } from '../../v3-router/types'
+
+type WithMulticallGasLimit = {
+ gasLimit?: BigintIsh
+}
+
+type WithClientProvider = {
+ clientProvider?: OnChainProvider
+}
+
+export type GetV4CandidatePoolsParams = {
+ currencyA?: Currency
+ currencyB?: Currency
+} & WithClientProvider &
+ WithMulticallGasLimit
diff --git a/packages/smart-router/package.json b/packages/smart-router/package.json
index cda923e2326b8..d65e06cd1115a 100644
--- a/packages/smart-router/package.json
+++ b/packages/smart-router/package.json
@@ -32,6 +32,7 @@
"@pancakeswap/token-lists": "workspace:*",
"@pancakeswap/tokens": "workspace:*",
"@pancakeswap/v3-sdk": "workspace:*",
+ "@pancakeswap/v4-sdk": "workspace:*",
"async-retry": "^1.3.1",
"debug": "^4.3.4",
"graphql": "^16.8.1",
diff --git a/packages/swap-sdk-core/src/constants.ts b/packages/swap-sdk-core/src/constants.ts
index 375569d60a0bb..e2661038d0829 100644
--- a/packages/swap-sdk-core/src/constants.ts
+++ b/packages/swap-sdk-core/src/constants.ts
@@ -36,3 +36,5 @@ export const VM_TYPE_MAXIMA = {
[VMType.uint8]: BigInt('0xff'),
[VMType.uint256]: BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'),
}
+
+export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' as const
diff --git a/packages/swap-sdk-core/src/fractions/price.ts b/packages/swap-sdk-core/src/fractions/price.ts
index 572143d23f1a4..968e46d544209 100644
--- a/packages/swap-sdk-core/src/fractions/price.ts
+++ b/packages/swap-sdk-core/src/fractions/price.ts
@@ -4,6 +4,7 @@ import { BigintIsh, Rounding } from '../constants'
import { Currency } from '../currency'
import { Fraction } from './fraction'
import { CurrencyAmount } from './currencyAmount'
+import { Token } from '../token'
export class Price extends Fraction {
public readonly baseCurrency: TBase // input i.e. denominator
@@ -87,4 +88,8 @@ export class Price extends Frac
public toFixed(decimalPlaces = 4, format?: object, rounding?: Rounding): string {
return this.adjustedForDecimals.toFixed(decimalPlaces, format, rounding)
}
+
+ public get wrapped(): Price {
+ return new Price(this.baseCurrency.wrapped, this.quoteCurrency.wrapped, this.denominator, this.numerator)
+ }
}
diff --git a/packages/swap-sdk-core/src/index.test.ts b/packages/swap-sdk-core/src/index.test.ts
index f58ffd231bc1d..0bc00414456c8 100644
--- a/packages/swap-sdk-core/src/index.test.ts
+++ b/packages/swap-sdk-core/src/index.test.ts
@@ -19,6 +19,7 @@ test('exports', () => {
"MaxUint256",
"VMType",
"VM_TYPE_MAXIMA",
+ "ZERO_ADDRESS",
"BaseCurrency",
"Fraction",
"Percent",
@@ -34,6 +35,7 @@ test('exports', () => {
"computePriceImpact",
"getTokenComparator",
"sortCurrencies",
+ "getCurrencyAddress",
]
`)
})
diff --git a/packages/swap-sdk-core/src/utils.ts b/packages/swap-sdk-core/src/utils.ts
index 14d04e1ac87f6..41f2e1c0e4684 100644
--- a/packages/swap-sdk-core/src/utils.ts
+++ b/packages/swap-sdk-core/src/utils.ts
@@ -1,5 +1,5 @@
import invariant from 'tiny-invariant'
-import { ONE, THREE, TWO, VMType, VM_TYPE_MAXIMA, ZERO } from './constants'
+import { ONE, THREE, TWO, VMType, VM_TYPE_MAXIMA, ZERO, ZERO_ADDRESS } from './constants'
import { Currency } from './currency'
import { CurrencyAmount, Percent, Price } from './fractions'
import { Token } from './token'
@@ -128,3 +128,10 @@ export function sortCurrencies(currencies: T[]): T[] {
return a.sortsBefore(b) ? -1 : 1
})
}
+
+export function getCurrencyAddress(currency: Currency) {
+ if (currency.isNative) {
+ return ZERO_ADDRESS
+ }
+ return currency.address
+}
diff --git a/packages/universal-router-sdk/src/entities/protocols/pancakeswap.ts b/packages/universal-router-sdk/src/entities/protocols/pancakeswap.ts
index 64b73083e8c0e..dc781901e0156 100644
--- a/packages/universal-router-sdk/src/entities/protocols/pancakeswap.ts
+++ b/packages/universal-router-sdk/src/entities/protocols/pancakeswap.ts
@@ -73,6 +73,14 @@ export class PancakeSwapTrade implements Command {
addStableSwap(planner, singleRouteTrade, this.options, routerMustCustody, payerIsUser)
continue
}
+ if (route.type === RouteType.V4CL) {
+ // TODO: implementation
+ continue
+ }
+ if (route.type === RouteType.V4BIN) {
+ // TODO: implementation
+ continue
+ }
addMixedSwap(planner, singleRouteTrade, this.options, payerIsUser, routerMustCustody)
}
diff --git a/packages/v4-sdk/src/index.ts b/packages/v4-sdk/src/index.ts
index 8ec1cef061968..15a385b374483 100644
--- a/packages/v4-sdk/src/index.ts
+++ b/packages/v4-sdk/src/index.ts
@@ -6,3 +6,6 @@ export * from './functions/clamm'
export * from './types'
export * from './utils'
+
+export * from './abis/CLPoolManager'
+export * from './abis/BinPoolManager'
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9e24767c7d3d0..a75f144f504ff 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2111,6 +2111,9 @@ importers:
'@pancakeswap/v3-sdk':
specifier: workspace:*
version: link:../v3-sdk
+ '@pancakeswap/v4-sdk':
+ specifier: workspace:*
+ version: link:../v4-sdk
async-retry:
specifier: ^1.3.1
version: 1.3.3
@@ -10319,6 +10322,7 @@ packages:
eciesjs@0.3.18:
resolution: {integrity: sha512-RQhegEtLSyIiGJmFTZfvCTHER/fymipXFVx6OwSRYD6hOuy+6Kjpk0dGvIfP9kxn/smBpxQy71uxpGO406ITCw==}
+ deprecated: Please upgrade to v0.4+
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
@@ -15894,10 +15898,6 @@ packages:
undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
- undici@5.20.0:
- resolution: {integrity: sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g==}
- engines: {node: '>=12.18'}
-
undici@5.28.4:
resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==}
engines: {node: '>=14.0'}
@@ -18729,7 +18729,7 @@ snapshots:
dependencies:
'@binance/w3w-types': 1.1.4
'@binance/w3w-utils': 1.1.4
- ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+ ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
transitivePeerDependencies:
- bufferutil
- utf-8-validate
@@ -21017,7 +21017,7 @@ snapshots:
lodash: 4.17.21
semver: 6.3.1
table: 6.8.1
- undici: 5.20.0
+ undici: 5.28.4
transitivePeerDependencies:
- supports-color
optional: true
@@ -21517,9 +21517,9 @@ snapshots:
'@types/ws': 8.5.10
axios: 1.7.2
axios-retry: 3.9.1
- isomorphic-ws: 4.0.1(ws@8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10))
+ isomorphic-ws: 4.0.1(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))
ts-log: 2.2.5
- ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+ ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
transitivePeerDependencies:
- bufferutil
- debug
@@ -23299,7 +23299,7 @@ snapshots:
util: 0.12.5
util-deprecate: 1.0.2
watchpack: 2.4.0
- ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+ ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
transitivePeerDependencies:
- bufferutil
- encoding
@@ -25363,7 +25363,7 @@ snapshots:
'@walletconnect/jsonrpc-provider': 1.0.14
'@walletconnect/jsonrpc-types': 1.0.4
'@walletconnect/jsonrpc-utils': 1.0.8
- '@walletconnect/modal': 2.6.2(@types/react@18.2.37)(react@18.2.0)
+ '@walletconnect/modal': 2.7.0(@types/react@18.2.37)(react@18.2.0)
'@walletconnect/sign-client': 2.11.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
'@walletconnect/types': 2.11.0
'@walletconnect/universal-provider': 2.11.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
@@ -30287,7 +30287,7 @@ snapshots:
source-map-support: 0.5.21
stacktrace-parser: 0.1.10
tsort: 0.0.1
- undici: 5.20.0
+ undici: 5.28.4
uuid: 8.3.2
ws: 7.5.9(bufferutil@4.0.8)(utf-8-validate@5.0.10)
optionalDependencies:
@@ -30840,9 +30840,9 @@ snapshots:
dependencies:
ws: 7.5.9(bufferutil@4.0.8)(utf-8-validate@5.0.10)
- isomorphic-ws@4.0.1(ws@8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)):
+ isomorphic-ws@4.0.1(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)):
dependencies:
- ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+ ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
isows@1.0.3(ws@8.13.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)):
dependencies:
@@ -35710,11 +35710,6 @@ snapshots:
undici-types@5.26.5: {}
- undici@5.20.0:
- dependencies:
- busboy: 1.6.0
- optional: true
-
undici@5.28.4:
dependencies:
'@fastify/busboy': 2.1.1
@@ -36927,11 +36922,6 @@ snapshots:
bufferutil: 4.0.8
utf-8-validate: 5.0.10
- ws@8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10):
- optionalDependencies:
- bufferutil: 4.0.8
- utf-8-validate: 5.0.10
-
ws@8.17.1(bufferutil@4.0.8)(utf-8-validate@6.0.3):
optionalDependencies:
bufferutil: 4.0.8