import { Contract, Signer } from 'ethers'; import { SwapCalculationParams, SwapCalculationResult, EarlyReturnResult, } from './types/swap-calculator'; import { ProtocolDetector, AbiManager, ContractFactory, PositionExtractor, BalanceQuerier, PoolStateQuerier, VaultInput, getNetworkConfig, hasNetwork, NetworkConfig, } from './helpers'; import { CustomRouterFactory } from './factory'; import { NoopLogger } from './utils'; import { AlgebraCustomRouter } from './routers/algebra/algebra-router'; import { PoolSharkCustomRouter } from './routers/poolshark/poolshark-router'; import { ThickV2CustomRouter } from './routers/thickv2/thickv2-router'; import { UniswapCustomRouter } from './routers/uniswap/uniswap-v3-router'; import { AerodromeCustomRouter } from './routers/aerodrome/aerodrome-router'; import { ShadowCustomRouter } from './routers/shadow/shadow-router'; import { AlgebraIntegralCustomRouter } from './routers/algebra/algebra-integral-router'; import { AlgebraIntegralV2CustomRouter } from './routers/algebra/algebra-integral-v2-router'; import { UniswapCustomRouterV2 } from './routers/uniswap/uniswap-v4-router'; import { Chain } from '@steerprotocol/sdk'; /** * SwapCalculator orchestrates swap amount calculations * * High-level API that coordinates all helper modules to: * 1. Extract vault and pool data * 2. Calculate optimal swap amounts * 3. Handle protocol-specific logic * 4. Apply fallback strategies on errors * * Supports dynamic quoter address resolution when network name is provided. * * This is the main entry point for consuming applications. */ export class SwapCalculator { private static readonly ROUTER_STEP_TIMEOUT_MS = 20_000; private signer: Signer; private protocolDetector: ProtocolDetector; private abiManager: AbiManager; private contractFactory: ContractFactory; private positionExtractor: PositionExtractor; private balanceQuerier: BalanceQuerier | null; private poolStateQuerier: PoolStateQuerier; private customRouterFactory: CustomRouterFactory; private networkName: string; /** * Create a SwapCalculator instance * * @param signer - Ethers signer for contract interactions * @param networkName - Network name for dynamic address resolution and fallback strategies * (e.g., "arbitrum", "polygon", "optimism"). Defaults to "unknown". * @param steerPeripheryContract - Optional SteerPeriphery contract for balance queries * * @example * ```typescript * // With network name for dynamic quoter resolution * const calculator = new SwapCalculator(signer, 'arbitrum', peripheryContract); * * // Without network name (uses "unknown" default, no dynamic resolution) * const calculator = new SwapCalculator(signer); * ``` */ constructor( signer: Signer, networkName: Chain, steerPeripheryContract?: Contract, ) { this.signer = signer; this.networkName = networkName; // Load network config if network name is valid let networkConfig: NetworkConfig | undefined; try { console.log(`[SwapCalculator] Checking network: ${networkName}, type: ${typeof networkName}`); if (hasNetwork(networkName)) { console.log(`[SwapCalculator] Network found, loading config for: ${networkName}`); networkConfig = getNetworkConfig(networkName); console.log(`[SwapCalculator] Network config loaded successfully`); } else { console.warn(`[SwapCalculator] Network '${networkName}' not found in configuration`); } } catch (error) { // Network config loading failed - continue without it // Dynamic quoter resolution will not be available console.warn( `Failed to load network config for '${networkName}':`, error, ); } // Initialize helper modules this.protocolDetector = new ProtocolDetector(); this.abiManager = new AbiManager(); this.contractFactory = new ContractFactory( signer, this.abiManager, this.protocolDetector, networkConfig, // Pass network config for dynamic quoter resolution ); this.positionExtractor = new PositionExtractor(); this.balanceQuerier = steerPeripheryContract ? new BalanceQuerier(steerPeripheryContract, this.protocolDetector) : null; this.poolStateQuerier = new PoolStateQuerier(this.protocolDetector); this.customRouterFactory = new CustomRouterFactory(NoopLogger); } /** * Calculate swap amount for a vault * * Main orchestration method that: * 1. Checks for early returns (skipSwap, specifySwap) * 2. Creates pool and quoter contracts * 3. Calls protocol-specific router * 4. Applies slippage to result * 5. Handles errors with fallback strategies * * @param params - Swap calculation parameters * @returns Promise - Swap calculation result */ async calculateSwapForVault( params: SwapCalculationParams, ): Promise { // Check for early returns const earlyReturn = this.checkEarlyReturns(params); if (earlyReturn.shouldReturn && earlyReturn.result) { return earlyReturn.result; } let amount: bigint; let sqrtPrice: bigint; let isSwapRouterFailed = false; let resetPosition: any = null; try { // Create pool contract const poolContract = this.contractFactory.createPoolContract( params.poolAddress, params.beaconName, this.signer, ); // Create quoter contract with dynamic address resolution // Address is resolved via QuoterFactory using protocol detection + network config let quoterContract; try { quoterContract = this.contractFactory.createQuoterContract( params.beaconName, this.signer, ); console.log(`[SwapCalculator] Quoter contract created successfully`); } catch (error) { // If quoter creation fails, throw a more descriptive error const errorMessage = error instanceof Error ? error.message : String(error); throw new Error( `Failed to create quoter contract for beacon '${params.beaconName}' on network '${this.networkName}'. ` + `This usually means the network configuration is missing or invalid. ` + `Original error: ${errorMessage}` ); } // Create router via CustomRouterFactory const router = await this.customRouterFactory.createCustomRouter( params.beaconName, quoterContract, poolContract, ); const routerName = (router as any)?.constructor?.name ?? 'unknown'; console.log( `[SwapCalculator] Router created beacon=${params.beaconName} network=${this.networkName} router=${routerName}`, ); // Calculate max iterations based on protocol const maxIterations = params.maxIterations || this.getSwapMaxIterations(params.beaconName); // Call protocol-specific router with correct signature let swapResult; if ( router instanceof AlgebraCustomRouter || router instanceof AlgebraIntegralCustomRouter || router instanceof AlgebraIntegralV2CustomRouter || router instanceof PoolSharkCustomRouter ) { console.log( `[SwapCalculator] Calling router.getSwapAmount router=${routerName} branch=algebra-like beacon=${params.beaconName}`, ); // Algebra/PoolShark: no fee or tickSpacing parameter (7 params total) swapResult = await this.withRouterTimeout( `router=${routerName} branch=algebra-like beacon=${params.beaconName}`, router.getSwapAmount( poolContract, params.newPositions, params.bal0, params.bal1, params.token0, params.token1, maxIterations, ), ); } else if (router instanceof ThickV2CustomRouter) { // ThickV2: uses tickSpacing instead of fee (8 params total) const tickSpacing = await this.poolStateQuerier.getTickSpacing( params.poolAddress, this.signer, ); console.log( `[SwapCalculator] Calling router.getSwapAmount router=${routerName} branch=thickv2 beacon=${params.beaconName} tickSpacing=${tickSpacing || 60}`, ); swapResult = await this.withRouterTimeout( `router=${routerName} branch=thickv2 beacon=${params.beaconName}`, router.getSwapAmount( poolContract, params.newPositions, params.bal0, params.bal1, params.token0, params.token1, tickSpacing || 60, maxIterations, ), ); } else if ( router instanceof UniswapCustomRouter || router instanceof UniswapCustomRouterV2 || router instanceof AerodromeCustomRouter || router instanceof ShadowCustomRouter ) { console.log( `[SwapCalculator] Calling router.getSwapAmount router=${routerName} branch=uniswap-like beacon=${params.beaconName} poolFee=${params.poolFee}`, ); // Standard protocols: use poolFee (8 params total) swapResult = await this.withRouterTimeout( `router=${routerName} branch=uniswap-like beacon=${params.beaconName}`, router.getSwapAmount( poolContract, params.newPositions, params.bal0, params.bal1, params.token0, params.token1, params.poolFee, maxIterations, ), ); } console.log( `[SwapCalculator] router.getSwapAmount completed router=${routerName} zeroForOne=${String((swapResult as any).zeroForOne)} amountToSwap=${String((swapResult as any).amountToSwap)}`, ); // Convert to signed amount (positive for zeroForOne, negative for oneForZero) // Ensure amountToSwap is BigInt (ethers v6 compatibility) const amountToSwap = typeof swapResult.amountToSwap === 'bigint' ? swapResult.amountToSwap : BigInt(swapResult.amountToSwap); amount = amountToSwap * (swapResult.zeroForOne ? 1n : -1n); // Apply slippage to sqrtPrice const slippageBps = BigInt( swapResult.zeroForOne ? 10000 - Math.round(params.slippage * 100) : 10000 + Math.round(params.slippage * 100), ); // Ensure sqrtPriceX96 is BigInt (ethers v6 compatibility) const sqrtPriceX96 = typeof params.sqrtPriceX96 === 'bigint' ? params.sqrtPriceX96 : BigInt(params.sqrtPriceX96); sqrtPrice = (slippageBps * sqrtPriceX96) / 10000n; } catch (error) { // Handle swap calculation failure with fallback const fallbackResult = this.handleSwapCalculationFallback( params, error as Error, ); const errorMessage = error instanceof Error ? error.message : String(error); console.error( `[SwapCalculator] Swap calculation failed network=${this.networkName} beacon=${params.beaconName} pool=${params.poolAddress} error=${errorMessage}`, ); console.error( `[SwapCalculator] Fallback result network=${this.networkName} beacon=${params.beaconName} isSwapRouterFailed=${fallbackResult.isSwapRouterFailed} amount=${fallbackResult.amount.toString()} resetPosition=${JSON.stringify(fallbackResult.resetPosition)}`, ); amount = BigInt(fallbackResult.amount); sqrtPrice = fallbackResult.sqrtPrice; isSwapRouterFailed = fallbackResult.isSwapRouterFailed; resetPosition = fallbackResult.resetPosition; } return { amount, sqrtPrice, isSwapRouterFailed, resetPosition, }; } private async withRouterTimeout(label: string, promise: Promise): Promise { let timeoutId: ReturnType | undefined; try { return await Promise.race([ promise, new Promise((_, reject) => { timeoutId = setTimeout(() => { reject( new Error( `router step timed out after ${SwapCalculator.ROUTER_STEP_TIMEOUT_MS}ms ${label}`, ), ); }, SwapCalculator.ROUTER_STEP_TIMEOUT_MS); }), ]); } finally { if (timeoutId) { clearTimeout(timeoutId); } } } /** * Check for early return conditions * * Handles special cases: * - skipSwap: Return zero swap * - specifySwap: Return specified swap amount * * @param params - Swap calculation parameters * @returns EarlyReturnResult - Whether to return early and the result */ private checkEarlyReturns(params: SwapCalculationParams): EarlyReturnResult { const { executionResult } = params; // Check for skipSwap flag if (executionResult?.skipSwap === true) { return { shouldReturn: true, result: { amount: 0n, sqrtPrice: 0n, isSwapRouterFailed: false, resetPosition: null, }, }; } // Check for specifySwap with explicit amount if ( executionResult?.specifySwap?.amount !== undefined && executionResult?.specifySwap?.amount !== 0 ) { return { shouldReturn: true, result: { amount: executionResult.specifySwap.amount, sqrtPrice: BigInt(executionResult.specifySwap.sqrtPrice), isSwapRouterFailed: false, resetPosition: null, }, }; } // No early return return { shouldReturn: false, }; } /** * Handle swap calculation fallback * * Applies fallback strategies when router fails: * - For supported networks: Reset to current positions if in range * - For unsupported networks: Throw error * * @param params - Swap calculation parameters * @param error - The error that triggered fallback * @returns SwapCalculationResult - Fallback result */ private handleSwapCalculationFallback( params: SwapCalculationParams, error: Error, ): SwapCalculationResult { const supportedNetworks = [ 'kava', 'moonbeam', 'apechain', 'zircuit', 'filecoin', 'evmos', 'linea', 'manta', 'astar', 'andromeda', 'mode', 'modepreprod', 'telos', 'arbitrum', 'soneium', 'core', ]; if (!supportedNetworks.includes(this.networkName.toLowerCase())) { throw new Error( `Network ${this.networkName} not supported for fallback. Original error: ${error.message}`, ); } const amount = 0n; const sqrtPrice = params.sqrtPriceX96; // Handle multi-position fallback if (params.isMultiPosition && params.vaultCurrentMultiPositions) { return this.handleMultiPositionFallback( params.vaultCurrentMultiPositions, params.isUpgradedVault || false, params.currentSlot, amount, sqrtPrice, ); } // Handle single-position fallback if (params.vaultCurrentSinglePosition) { return this.handleSinglePositionFallback( params.vaultCurrentSinglePosition, params.currentSlot, amount, sqrtPrice, ); } // Default fallback: no reset return { amount, sqrtPrice, isSwapRouterFailed: false, resetPosition: null, }; } /** * Handle multi-position fallback * * Resets to current positions if current tick is in range * * @param vaultCurrentMultiPositions - Current multi-positions * @param isUpgradedVault - Whether vault is upgraded * @param currentSlot - Current slot data * @param amount - Swap amount (0 for fallback) * @param sqrtPrice - Current sqrt price * @returns SwapCalculationResult - Fallback result */ private handleMultiPositionFallback( vaultCurrentMultiPositions: any, isUpgradedVault: boolean, currentSlot: any, amount: bigint, sqrtPrice: bigint, ): SwapCalculationResult { // Convert old format to new format if needed let positions = vaultCurrentMultiPositions; if (!isUpgradedVault && Array.isArray(vaultCurrentMultiPositions[0])) { const newPositions: any[] = []; for (let i = 0; i < vaultCurrentMultiPositions[0].length; i++) { newPositions.push({ lowerTick: Number(vaultCurrentMultiPositions[0][i]), upperTick: Number(vaultCurrentMultiPositions[1][i]), relativeWeight: Number(vaultCurrentMultiPositions[2][i]), }); } positions = newPositions; } const lowerTicks = positions.map((p: any) => p.lowerTick); const upperTicks = positions.map((p: any) => p.upperTick); const currentTick = currentSlot?.pool?.tickToPrice || currentSlot?.tick; // Check if current tick is in range if ( currentTick >= Math.min(...lowerTicks) && currentTick <= Math.max(...upperTicks) ) { return { amount, sqrtPrice, isSwapRouterFailed: true, resetPosition: { position: [ lowerTicks, upperTicks, positions.map((p: any) => p.relativeWeight), ], isMultiVault: true, }, }; } // Out of range: no reset return { amount, sqrtPrice, isSwapRouterFailed: false, resetPosition: null, }; } /** * Handle single-position fallback * * Resets to current position if current tick is in range * * @param vaultCurrentSinglePosition - Current single position * @param currentSlot - Current slot data * @param amount - Swap amount (0 for fallback) * @param sqrtPrice - Current sqrt price * @returns SwapCalculationResult - Fallback result */ private handleSinglePositionFallback( vaultCurrentSinglePosition: any, currentSlot: any, amount: bigint, sqrtPrice: bigint, ): SwapCalculationResult { const currentTick = currentSlot?.pool?.tickToPrice || currentSlot?.tick; // Check if current tick is in range if ( currentTick >= vaultCurrentSinglePosition?.lowerTick && currentTick <= vaultCurrentSinglePosition?.upperTick ) { return { amount, sqrtPrice, isSwapRouterFailed: true, resetPosition: { position: vaultCurrentSinglePosition, isMultiVault: false, }, }; } // Out of range: no reset return { amount, sqrtPrice, isSwapRouterFailed: false, resetPosition: null, }; } /** * Get max iterations for swap calculation * * Different protocols require different iteration counts * * @param beaconName - Beacon name to determine protocol * @returns number - Max iterations */ private getSwapMaxIterations(beaconName: string): number { if (this.protocolDetector.isShadowVault(beaconName)) { return 40; } else if (this.protocolDetector.isAlgebraIntegral19Vault(beaconName)) { return 40; } else { return 28; } } /** * Extract positions from execution result * * @param executionResult - Execution result containing position data * @returns Position[] - Extracted positions */ extractPositions(executionResult: any) { return this.positionExtractor.extractNewPositions(executionResult); } /** * Get current vault positions * * @param vaultContract - Vault contract instance * @param isMultiPosition - Whether vault supports multiple positions * @returns Promise - Current vault positions */ async getCurrentPositions(vaultContract: Contract, isMultiPosition: boolean) { return this.positionExtractor.getCurrentVaultPositions( vaultContract, isMultiPosition, ); } /** * Get vault balances * * Requires SteerPeriphery contract to be provided in constructor * * @param vault - Vault input data * @returns Promise - Vault token balances * @throws Error if BalanceQuerier not initialized */ async getVaultBalances(vault: VaultInput) { if (!this.balanceQuerier) { throw new Error( 'BalanceQuerier not initialized. Provide steerPeripheryContract in constructor.', ); } return this.balanceQuerier.getVaultBalances(vault); } /** * Get pool slot0/globalState data * * @param beaconName - Beacon name to determine protocol * @param poolContract - Pool contract instance * @returns Promise - Slot0 or globalState data */ async getPoolSlot0(beaconName: string, poolContract: Contract) { return this.poolStateQuerier.getSlot0(beaconName, poolContract); } /** * Extract sqrtPriceX96 from slot data * * @param beaconName - Beacon name to determine protocol * @param slotData - Slot0 or globalState data * @returns any - The sqrtPriceX96 value */ extractSqrtPriceX96(beaconName: string, slotData: any) { return this.poolStateQuerier.extractSqrtPriceX96(beaconName, slotData); } /** * Get tick spacing from pool * * @param poolAddress - Pool contract address * @returns Promise - Tick spacing or undefined */ async getTickSpacing(poolAddress: string) { return this.poolStateQuerier.getTickSpacing(poolAddress, this.signer); } /** * Create a pool contract * * @param poolAddress - Pool contract address * @param beaconName - Beacon name to determine protocol * @returns Contract - Pool contract instance */ createPoolContract(poolAddress: string, beaconName: string) { return this.contractFactory.createPoolContract(poolAddress, beaconName); } /** * Create a vault contract * * @param vaultAddress - Vault contract address * @param beaconName - Beacon name to determine protocol * @param isUpgraded - Whether to use upgraded vault ABI * @returns Contract - Vault contract instance */ createVaultContract( vaultAddress: string, beaconName: string, isUpgraded = true, ) { return this.contractFactory.createVaultContract( vaultAddress, beaconName, isUpgraded, ); } /** * Detect if vault is upgraded * * @param vaultAddress - Vault contract address * @param beaconName - Beacon name to determine protocol * @returns Promise - Whether vault is upgraded */ async detectVaultUpgrade(vaultAddress: string, beaconName: string) { return this.contractFactory.detectVaultUpgrade(vaultAddress, beaconName); } /** * Check if vault is multi-position * * @param beaconName - Beacon name * @returns boolean - Whether vault supports multiple positions */ isMultiPositionVault(beaconName: string) { return this.contractFactory.isMultiPositionVault(beaconName); } }