From b345ab072f03e6fe8821b55d4fa3883ec837a069 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Wed, 3 Dec 2025 14:30:42 -0800 Subject: [PATCH 01/14] fix: simplify restart mechanism to prevent orphan processes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove detached process spawning from /restart endpoint. Instead: - Add PID file tracking (gateway.pid) for process management - Kill any existing gateway process on startup using PID file - On restart, simply exit - let process managers handle restart - Clean up PID file on graceful shutdown This matches the simpler approach used in gateway v2.2.0 and prevents orphan processes that were being created by the old spawn mechanism. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/app.ts | 92 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 7 deletions(-) diff --git a/src/app.ts b/src/app.ts index d87f53cb09..fb59ce7be5 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,6 +1,7 @@ // External dependencies -import { spawn } from 'child_process'; import { exec } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; import { promisify } from 'util'; import fastifyRateLimit from '@fastify/rate-limit'; @@ -47,6 +48,64 @@ const devMode = process.argv.includes('--dev') || process.env.GATEWAY_TEST_MODE // Promisify exec for async/await usage const execPromise = promisify(exec); +// PID file management for tracking the official Gateway process +const PID_FILE = path.join(process.cwd(), 'gateway.pid'); + +function writePidFile(): void { + try { + fs.writeFileSync(PID_FILE, process.pid.toString(), 'utf8'); + logger.info(`PID file written: ${PID_FILE} (PID: ${process.pid})`); + } catch (error) { + logger.warn(`Failed to write PID file: ${error}`); + } +} + +function readPidFile(): number | null { + try { + if (fs.existsSync(PID_FILE)) { + const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim(), 10); + return isNaN(pid) ? null : pid; + } + } catch (error) { + logger.warn(`Failed to read PID file: ${error}`); + } + return null; +} + +function removePidFile(): void { + try { + if (fs.existsSync(PID_FILE)) { + fs.unlinkSync(PID_FILE); + logger.info('PID file removed'); + } + } catch (error) { + logger.warn(`Failed to remove PID file: ${error}`); + } +} + +async function killExistingGateway(): Promise { + const existingPid = readPidFile(); + if (existingPid && existingPid !== process.pid) { + logger.info(`Found existing Gateway process (PID: ${existingPid}), killing...`); + try { + process.kill(existingPid, 'SIGTERM'); + // Wait a bit for graceful shutdown + await new Promise((resolve) => setTimeout(resolve, 1000)); + // Force kill if still running + try { + process.kill(existingPid, 0); // Check if process exists + process.kill(existingPid, 'SIGKILL'); + logger.info(`Force killed existing Gateway (PID: ${existingPid})`); + } catch { + // Process already dead, which is good + } + } catch (error) { + // Process doesn't exist or already dead + logger.info(`Existing Gateway process (PID: ${existingPid}) not running`); + } + } +} + const swaggerOptions = { openapi: { info: { @@ -335,13 +394,23 @@ const configureGatewayServer = () => { }); // Restart endpoint (outside registerRoutes, only on main server) + // This closes the server and exits. The caller is responsible for restarting. + // - If run from terminal: user needs to start again manually + // - If run via process manager (PM2, systemd, Docker): it will auto-restart server.post('/restart', async (_req, reply) => { await reply.status(200).send(); - // Spawn a new instance before exiting - spawn(process.argv[0], process.argv.slice(1), { - detached: true, - stdio: 'inherit', - }); + + logger.info('Restart requested - shutting down for restart...'); + + // Remove PID file + removePidFile(); + + // Close server to release the port + await server.close(); + + logger.info('Server closed. Exiting for restart...'); + + // Exit with code 0 - process manager will restart if configured process.exit(0); }); @@ -362,7 +431,10 @@ export const startGateway = async () => { logger.info(`🔧 Log level configured as: ${ConfigManagerV2.getInstance().get('server.logLevel') || 'info'}`); try { - // Kill any process using the gateway port + // Kill any existing Gateway process tracked by PID file + await killExistingGateway(); + + // Also kill any process using the gateway port (fallback) try { logger.info(`Checking for processes using port ${port}...`); @@ -415,6 +487,9 @@ export const startGateway = async () => { await gatewayApp.listen({ port, host: '0.0.0.0' }); } + // Write PID file after server successfully starts + writePidFile(); + // Single documentation log after server starts const docsUrl = docsPort ? `http://localhost:${docsPort}` : `${protocol}://localhost:${port}/docs`; @@ -427,6 +502,9 @@ export const startGateway = async () => { const shutdown = async () => { logger.info('Shutting down gracefully...'); + // Remove PID file + removePidFile(); + // Close server await gatewayApp.close(); From b33ead70e55f9202306b8088e4cf93dc2057decb Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Wed, 3 Dec 2025 14:37:05 -0800 Subject: [PATCH 02/14] feat: add gateway.sh lifecycle manager script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a shell script wrapper that manages the Gateway server lifecycle: - `./gateway.sh start [--dev]` - Start Gateway with auto-restart on config changes - `./gateway.sh stop` - Stop Gateway and wrapper - `./gateway.sh restart` - Request restart via API - `./gateway.sh status` - Show running status The wrapper process monitors exit codes: - Exit code 0: Restart requested (wrapper restarts Gateway) - Exit code 1: Shutdown requested (wrapper stops) This enables seamless restarts when clients update config values, as the wrapper automatically restarts Gateway after it exits. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .gitignore | 5 +- gateway.sh | 274 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 278 insertions(+), 1 deletion(-) create mode 100755 gateway.sh diff --git a/.gitignore b/.gitignore index e634695a5a..955a7910fc 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,7 @@ src/**/*.js.map *.log # MCP servers configuration -.mcp.json \ No newline at end of file +.mcp.json + +# PID files from gateway lifecycle manager +*.pid \ No newline at end of file diff --git a/gateway.sh b/gateway.sh new file mode 100755 index 0000000000..eb39b19f48 --- /dev/null +++ b/gateway.sh @@ -0,0 +1,274 @@ +#!/bin/bash +# +# Gateway lifecycle manager +# Usage: ./gateway.sh [start|stop|restart|status] +# +# This script manages the Gateway server process, handling restarts automatically +# when the server exits with code 0 (restart requested). +# + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PID_FILE="$SCRIPT_DIR/gateway.pid" +WRAPPER_PID_FILE="$SCRIPT_DIR/gateway-wrapper.pid" +LOG_FILE="$SCRIPT_DIR/logs/gateway.log" + +# Ensure logs directory exists +mkdir -p "$SCRIPT_DIR/logs" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +log() { + echo -e "${GREEN}[Gateway]${NC} $1" +} + +warn() { + echo -e "${YELLOW}[Gateway]${NC} $1" +} + +error() { + echo -e "${RED}[Gateway]${NC} $1" +} + +# Check if Gateway is running +is_running() { + if [ -f "$PID_FILE" ]; then + local pid=$(cat "$PID_FILE") + if kill -0 "$pid" 2>/dev/null; then + return 0 + fi + fi + return 1 +} + +# Check if wrapper is running +is_wrapper_running() { + if [ -f "$WRAPPER_PID_FILE" ]; then + local pid=$(cat "$WRAPPER_PID_FILE") + if kill -0 "$pid" 2>/dev/null; then + return 0 + fi + fi + return 1 +} + +# Start the Gateway server in a restart loop +start_gateway() { + if is_wrapper_running; then + error "Gateway wrapper is already running (PID: $(cat "$WRAPPER_PID_FILE"))" + return 1 + fi + + if is_running; then + error "Gateway is already running (PID: $(cat "$PID_FILE"))" + return 1 + fi + + log "Starting Gateway..." + + # Check for required environment variables + if [ -z "$GATEWAY_PASSPHRASE" ]; then + error "GATEWAY_PASSPHRASE environment variable is required" + error "Usage: GATEWAY_PASSPHRASE=yourpassphrase $0 start" + return 1 + fi + + # Parse additional arguments + local dev_mode="" + shift # Remove 'start' from args + while [[ $# -gt 0 ]]; do + case $1 in + --dev) + dev_mode="--dev" + shift + ;; + *) + shift + ;; + esac + done + + # Start the wrapper process in the background + ( + while true; do + log "Starting Gateway server..." + + # Run Gateway + cd "$SCRIPT_DIR" + START_SERVER=true node dist/index.js $dev_mode + exit_code=$? + + if [ $exit_code -eq 0 ]; then + log "Gateway requested restart (exit code 0). Restarting in 2 seconds..." + sleep 2 + else + log "Gateway stopped (exit code $exit_code). Not restarting." + rm -f "$WRAPPER_PID_FILE" + break + fi + done + ) >> "$LOG_FILE" 2>&1 & + + # Capture the background job PID immediately after & + echo "$!" > "$WRAPPER_PID_FILE" + + # Wait a moment for startup + sleep 3 + + if is_running; then + log "Gateway started successfully (PID: $(cat "$PID_FILE"))" + log "Wrapper PID: $(cat "$WRAPPER_PID_FILE")" + log "Logs: $LOG_FILE" + else + error "Gateway failed to start. Check logs: $LOG_FILE" + return 1 + fi +} + +# Stop the Gateway server +stop_gateway() { + local stopped=0 + + # First, stop the wrapper to prevent restart + if is_wrapper_running; then + local wrapper_pid=$(cat "$WRAPPER_PID_FILE") + log "Stopping Gateway wrapper (PID: $wrapper_pid)..." + kill "$wrapper_pid" 2>/dev/null + rm -f "$WRAPPER_PID_FILE" + stopped=1 + fi + + # Then stop the Gateway process + if is_running; then + local pid=$(cat "$PID_FILE") + log "Stopping Gateway server (PID: $pid)..." + kill "$pid" 2>/dev/null + + # Wait for process to stop + local count=0 + while kill -0 "$pid" 2>/dev/null && [ $count -lt 10 ]; do + sleep 1 + count=$((count + 1)) + done + + if kill -0 "$pid" 2>/dev/null; then + warn "Gateway didn't stop gracefully, force killing..." + kill -9 "$pid" 2>/dev/null + fi + + rm -f "$PID_FILE" + stopped=1 + fi + + if [ $stopped -eq 1 ]; then + log "Gateway stopped" + else + warn "Gateway is not running" + fi +} + +# Restart the Gateway server (via API) +restart_gateway() { + if ! is_running; then + warn "Gateway is not running. Starting..." + start_gateway "$@" + return + fi + + log "Requesting Gateway restart via API..." + + # Determine protocol based on dev mode + local protocol="https" + local curl_opts="-k" + + # Try to detect if running in dev mode by checking the process + if ps aux | grep -v grep | grep "node dist/index.js" | grep -q "\-\-dev"; then + protocol="http" + curl_opts="" + fi + + local port=$(grep -E "^port:" "$SCRIPT_DIR/conf/server.yml" 2>/dev/null | awk '{print $2}' || echo "15888") + + curl -s -X POST $curl_opts "$protocol://localhost:$port/restart" > /dev/null + + if [ $? -eq 0 ]; then + log "Restart request sent. Gateway will restart automatically." + else + error "Failed to send restart request. Is Gateway running?" + fi +} + +# Show Gateway status +status_gateway() { + echo "" + if is_wrapper_running; then + log "Wrapper: ${GREEN}running${NC} (PID: $(cat "$WRAPPER_PID_FILE"))" + else + log "Wrapper: ${RED}not running${NC}" + fi + + if is_running; then + local pid=$(cat "$PID_FILE") + log "Gateway: ${GREEN}running${NC} (PID: $pid)" + + # Try to get status from API + local port=$(grep -E "^port:" "$SCRIPT_DIR/conf/server.yml" 2>/dev/null | awk '{print $2}' || echo "15888") + local response=$(curl -s -k "https://localhost:$port/" 2>/dev/null || curl -s "http://localhost:$port/" 2>/dev/null) + + if [ -n "$response" ]; then + log "API: ${GREEN}responding${NC}" + else + warn "API: ${YELLOW}not responding${NC}" + fi + else + log "Gateway: ${RED}not running${NC}" + fi + echo "" +} + +# Show usage +usage() { + echo "" + echo "Gateway Lifecycle Manager" + echo "" + echo "Usage: $0 [options]" + echo "" + echo "Commands:" + echo " start [--dev] Start Gateway server (--dev for HTTP mode)" + echo " stop Stop Gateway server" + echo " restart Restart Gateway server" + echo " status Show Gateway status" + echo "" + echo "Environment variables:" + echo " GATEWAY_PASSPHRASE Required passphrase for wallet encryption" + echo "" + echo "Examples:" + echo " GATEWAY_PASSPHRASE=mypassword $0 start" + echo " GATEWAY_PASSPHRASE=mypassword $0 start --dev" + echo " $0 stop" + echo " $0 status" + echo "" +} + +# Main +case "${1:-}" in + start) + start_gateway "$@" + ;; + stop) + stop_gateway + ;; + restart) + restart_gateway "$@" + ;; + status) + status_gateway + ;; + *) + usage + exit 1 + ;; +esac From 7817a46985e359a360d18ecb46eff590febe5a33 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Wed, 3 Dec 2025 14:55:26 -0800 Subject: [PATCH 03/14] feat: update pnpm start to use lifecycle wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change start script from direct node invocation to gateway.sh - Add --passphrase argument support to gateway.sh - Users can now use: pnpm start --passphrase= [--dev] - Wrapper provides auto-restart on config changes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- gateway.sh | 40 +++++++++++++++++++++++++--------------- package.json | 2 +- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/gateway.sh b/gateway.sh index eb39b19f48..8a2ad9f641 100755 --- a/gateway.sh +++ b/gateway.sh @@ -69,13 +69,6 @@ start_gateway() { log "Starting Gateway..." - # Check for required environment variables - if [ -z "$GATEWAY_PASSPHRASE" ]; then - error "GATEWAY_PASSPHRASE environment variable is required" - error "Usage: GATEWAY_PASSPHRASE=yourpassphrase $0 start" - return 1 - fi - # Parse additional arguments local dev_mode="" shift # Remove 'start' from args @@ -85,12 +78,28 @@ start_gateway() { dev_mode="--dev" shift ;; + --passphrase=*) + export GATEWAY_PASSPHRASE="${1#*=}" + shift + ;; + --passphrase) + export GATEWAY_PASSPHRASE="$2" + shift 2 + ;; *) shift ;; esac done + # Check for required passphrase (from arg or environment) + if [ -z "$GATEWAY_PASSPHRASE" ]; then + error "Passphrase is required" + error "Usage: $0 start --passphrase=yourpassphrase [--dev]" + error " or: GATEWAY_PASSPHRASE=yourpassphrase $0 start [--dev]" + return 1 + fi + # Start the wrapper process in the background ( while true; do @@ -237,17 +246,18 @@ usage() { echo "Usage: $0 [options]" echo "" echo "Commands:" - echo " start [--dev] Start Gateway server (--dev for HTTP mode)" - echo " stop Stop Gateway server" - echo " restart Restart Gateway server" - echo " status Show Gateway status" + echo " start --passphrase= [--dev] Start Gateway server" + echo " stop Stop Gateway server" + echo " restart Restart Gateway server" + echo " status Show Gateway status" echo "" - echo "Environment variables:" - echo " GATEWAY_PASSPHRASE Required passphrase for wallet encryption" + echo "Options:" + echo " --passphrase= Passphrase for wallet encryption (required for start)" + echo " --dev Run in HTTP mode (development)" echo "" echo "Examples:" - echo " GATEWAY_PASSPHRASE=mypassword $0 start" - echo " GATEWAY_PASSPHRASE=mypassword $0 start --dev" + echo " $0 start --passphrase=mypassword" + echo " $0 start --passphrase=mypassword --dev" echo " $0 stop" echo " $0 status" echo "" diff --git a/package.json b/package.json index 34ec1d96dd..1f0f2214b0 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "lint": "eslint src test --format table --fix", "setup": "bash ./gateway-setup.sh", "setup:with-defaults": "bash ./gateway-setup.sh --with-defaults", - "start": "START_SERVER=true node dist/index.js", + "start": "bash ./gateway.sh start", "copy-files": "copyfiles 'src/templates/namespace/*.json' 'src/templates/*.yml' 'src/templates/chains/**/*.yml' 'src/templates/connectors/*.yml' 'src/templates/tokens/**/*.json' 'src/templates/pools/*.json' dist && copyfiles -u 1 'src/connectors/pancakeswap-sol/idl/*.json' dist", "test": "GATEWAY_TEST_MODE=dev jest --verbose", "test:clear-cache": "jest --clearCache", From dcf1e825fb5434e224f2101de701cd1f7bb9aecf Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Wed, 3 Dec 2025 15:13:59 -0800 Subject: [PATCH 04/14] feat: add server lifecycle endpoints and run in foreground MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add /status endpoint with version, uptime, mode, pid - Add /stop endpoint to shut down without restart - Move server routes to src/server/server.routes.ts for Swagger visibility - Update gateway.sh to run in foreground (logs visible in terminal) - Add pnpm stop and pnpm status commands 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- gateway.sh | 53 ++++++--------- package.json | 2 + src/app.ts | 32 ++------- src/server/server.routes.ts | 128 ++++++++++++++++++++++++++++++++++++ 4 files changed, 157 insertions(+), 58 deletions(-) create mode 100644 src/server/server.routes.ts diff --git a/gateway.sh b/gateway.sh index 8a2ad9f641..22152ece0d 100755 --- a/gateway.sh +++ b/gateway.sh @@ -100,41 +100,30 @@ start_gateway() { return 1 fi - # Start the wrapper process in the background - ( - while true; do - log "Starting Gateway server..." - - # Run Gateway - cd "$SCRIPT_DIR" - START_SERVER=true node dist/index.js $dev_mode - exit_code=$? - - if [ $exit_code -eq 0 ]; then - log "Gateway requested restart (exit code 0). Restarting in 2 seconds..." - sleep 2 - else - log "Gateway stopped (exit code $exit_code). Not restarting." - rm -f "$WRAPPER_PID_FILE" - break - fi - done - ) >> "$LOG_FILE" 2>&1 & + # Save wrapper PID + echo "$$" > "$WRAPPER_PID_FILE" - # Capture the background job PID immediately after & - echo "$!" > "$WRAPPER_PID_FILE" + # Handle Ctrl+C gracefully + trap 'log "Received interrupt signal. Stopping..."; rm -f "$WRAPPER_PID_FILE"; exit 130' INT TERM - # Wait a moment for startup - sleep 3 + # Run in foreground with restart loop + while true; do + log "Starting Gateway server..." - if is_running; then - log "Gateway started successfully (PID: $(cat "$PID_FILE"))" - log "Wrapper PID: $(cat "$WRAPPER_PID_FILE")" - log "Logs: $LOG_FILE" - else - error "Gateway failed to start. Check logs: $LOG_FILE" - return 1 - fi + # Run Gateway + cd "$SCRIPT_DIR" + START_SERVER=true node dist/index.js $dev_mode + exit_code=$? + + if [ $exit_code -eq 0 ]; then + log "Gateway requested restart (exit code 0). Restarting in 2 seconds..." + sleep 2 + else + log "Gateway stopped (exit code $exit_code). Not restarting." + rm -f "$WRAPPER_PID_FILE" + break + fi + done } # Stop the Gateway server diff --git a/package.json b/package.json index 1f0f2214b0..736b9f815e 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "setup": "bash ./gateway-setup.sh", "setup:with-defaults": "bash ./gateway-setup.sh --with-defaults", "start": "bash ./gateway.sh start", + "stop": "bash ./gateway.sh stop", + "status": "bash ./gateway.sh status", "copy-files": "copyfiles 'src/templates/namespace/*.json' 'src/templates/*.yml' 'src/templates/chains/**/*.yml' 'src/templates/connectors/*.yml' 'src/templates/tokens/**/*.json' 'src/templates/pools/*.json' dist && copyfiles -u 1 'src/connectors/pancakeswap-sol/idl/*.json' dist", "test": "GATEWAY_TEST_MODE=dev jest --verbose", "test:clear-cache": "jest --clearCache", diff --git a/src/app.ts b/src/app.ts index fb59ce7be5..437c46ce8c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -27,6 +27,7 @@ import { raydiumRoutes } from './connectors/raydium/raydium.routes'; import { uniswapRoutes } from './connectors/uniswap/uniswap.routes'; import { getHttpsOptions } from './https'; import { poolRoutes } from './pools/pools.routes'; +import { serverRoutes } from './server/server.routes'; import { ConfigManagerV2 } from './services/config-manager-v2'; import { logger } from './services/logger'; import { quoteCache } from './services/quote-cache'; @@ -119,6 +120,8 @@ const swaggerOptions = { }, ], tags: [ + // Server lifecycle + { name: 'server', description: 'Server lifecycle endpoints (health, restart, stop)' }, // Main categories { name: '/config', description: 'System configuration endpoints' }, { name: '/wallet', description: 'Wallet management endpoints' }, @@ -280,6 +283,9 @@ const configureGatewayServer = () => { // Register routes on both servers const registerRoutes = async (app: FastifyInstance) => { + // Register server lifecycle routes (health, status, restart, stop) + app.register(serverRoutes); + // Register system routes app.register(configRoutes, { prefix: '/config' }); @@ -388,32 +394,6 @@ const configureGatewayServer = () => { }); }); - // Health check route (outside registerRoutes, only on main server) - server.get('/', async () => { - return { status: 'ok' }; - }); - - // Restart endpoint (outside registerRoutes, only on main server) - // This closes the server and exits. The caller is responsible for restarting. - // - If run from terminal: user needs to start again manually - // - If run via process manager (PM2, systemd, Docker): it will auto-restart - server.post('/restart', async (_req, reply) => { - await reply.status(200).send(); - - logger.info('Restart requested - shutting down for restart...'); - - // Remove PID file - removePidFile(); - - // Close server to release the port - await server.close(); - - logger.info('Server closed. Exiting for restart...'); - - // Exit with code 0 - process manager will restart if configured - process.exit(0); - }); - return server; }; diff --git a/src/server/server.routes.ts b/src/server/server.routes.ts new file mode 100644 index 0000000000..f3d3a3a551 --- /dev/null +++ b/src/server/server.routes.ts @@ -0,0 +1,128 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +import { Type } from '@sinclair/typebox'; +import { FastifyInstance } from 'fastify'; + +import { logger } from '../services/logger'; +import { GATEWAY_VERSION } from '../version'; + +const PID_FILE = path.join(process.cwd(), 'gateway.pid'); + +function removePidFile(): void { + try { + if (fs.existsSync(PID_FILE)) { + fs.unlinkSync(PID_FILE); + logger.info('PID file removed'); + } + } catch (error) { + logger.warn(`Failed to remove PID file: ${error}`); + } +} + +// Detect dev mode +const devMode = process.argv.includes('--dev') || process.env.GATEWAY_TEST_MODE === 'dev'; + +export async function serverRoutes(fastify: FastifyInstance) { + // Health check route + fastify.get( + '/', + { + schema: { + description: 'Health check endpoint', + tags: ['server'], + response: { + 200: Type.Object({ + status: Type.String({ description: 'Server status' }), + }), + }, + }, + }, + async () => { + return { status: 'ok' }; + }, + ); + + // Status endpoint - returns server status info + fastify.get( + '/status', + { + schema: { + description: 'Get Gateway server status information including version, uptime, and mode.', + tags: ['server'], + response: { + 200: Type.Object({ + status: Type.String({ description: 'Server status' }), + version: Type.String({ description: 'Gateway version' }), + uptime: Type.Number({ description: 'Server uptime in seconds' }), + mode: Type.String({ description: 'Running mode (dev/production)' }), + pid: Type.Number({ description: 'Process ID' }), + }), + }, + }, + }, + async () => { + return { + status: 'ok', + version: GATEWAY_VERSION, + uptime: process.uptime(), + mode: devMode ? 'dev' : 'production', + pid: process.pid, + }; + }, + ); + + // Restart endpoint - exits with code 0 so wrapper will restart + fastify.post( + '/restart', + { + schema: { + description: 'Restart the Gateway server. The server will shut down and automatically restart.', + tags: ['server'], + response: { + 200: Type.Object({ + message: Type.String({ description: 'Confirmation message' }), + }), + }, + }, + }, + async (_req, reply) => { + await reply.status(200).send({ message: 'Restarting...' }); + + logger.info('Restart requested - shutting down for restart...'); + removePidFile(); + await fastify.close(); + logger.info('Server closed. Exiting for restart...'); + + // Exit with code 0 - wrapper will restart + process.exit(0); + }, + ); + + // Stop endpoint - exits with code 1 so wrapper will NOT restart + fastify.post( + '/stop', + { + schema: { + description: 'Stop the Gateway server completely. The server will shut down and NOT restart.', + tags: ['server'], + response: { + 200: Type.Object({ + message: Type.String({ description: 'Confirmation message' }), + }), + }, + }, + }, + async (_req, reply) => { + await reply.status(200).send({ message: 'Stopping...' }); + + logger.info('Stop requested - shutting down...'); + removePidFile(); + await fastify.close(); + logger.info('Server closed. Goodbye!'); + + // Exit with code 1 - wrapper will NOT restart + process.exit(1); + }, + ); +} From ea91744cac665086921a16a79a77b7932281c5dc Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Wed, 3 Dec 2025 15:48:02 -0800 Subject: [PATCH 05/14] fix: remove orphaned RPC provider namespace references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove helius and infura namespace references from root.yml since the rpc-provider-schema.json and rpc/ directory were previously removed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/templates/root.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/templates/root.yml b/src/templates/root.yml index 48e65517c8..d484ef78f4 100644 --- a/src/templates/root.yml +++ b/src/templates/root.yml @@ -92,15 +92,6 @@ configurations: configurationPath: connectors/pancakeswap-sol.yml schemaPath: pancakeswap-sol-schema.json - # RPC providers - $namespace helius: - configurationPath: rpc/helius.yml - schemaPath: rpc-provider-schema.json - - $namespace infura: - configurationPath: rpc/infura.yml - schemaPath: rpc-provider-schema.json - # API Keys (centralized) $namespace apiKeys: configurationPath: apiKeys.yml From 75cf8b8b24c75e6e9c9daa5a12dfa10f800c69eb Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Wed, 3 Dec 2025 17:06:39 -0800 Subject: [PATCH 06/14] feat: integrate AlphaRouter for split routing in Uniswap quote-swap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add AlphaRouterService for smart order routing across V2/V3 pools - Update quoteSwap to use AlphaRouter instead of UniversalRouterQuote - Pass slippagePct parameter through to AlphaRouter - Upgrade @uniswap/smart-order-router to v4.22.38 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- package.json | 2 +- pnpm-lock.yaml | 151 ++++++++++++------ src/connectors/uniswap/alpha-router.ts | 142 ++++++++++++++++ .../uniswap/router-routes/quoteSwap.ts | 31 ++-- src/connectors/uniswap/uniswap.ts | 64 ++++++++ 5 files changed, 322 insertions(+), 68 deletions(-) create mode 100644 src/connectors/uniswap/alpha-router.ts diff --git a/package.json b/package.json index 736b9f815e..f104781198 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "@uniswap/router-sdk": "^2.0.4", "@uniswap/sdk": "3.0.3", "@uniswap/sdk-core": "^5.9.0", - "@uniswap/smart-order-router": "^3.59.0", + "@uniswap/smart-order-router": "^4.22.38", "@uniswap/universal-router-sdk": "^4.19.6", "@uniswap/v2-sdk": "^4.15.2", "@uniswap/v3-core": "^1.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2521e4a7a1..87e684adf6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -152,8 +152,8 @@ importers: specifier: ^5.9.0 version: 5.9.0 '@uniswap/smart-order-router': - specifier: ^3.59.0 - version: 3.59.0(bufferutil@4.0.9)(encoding@0.1.13)(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))(jsbi@3.2.5)(utf-8-validate@5.0.10) + specifier: ^4.22.38 + version: 4.22.38(bufferutil@4.0.9)(encoding@0.1.13)(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))(jsbi@3.2.5)(utf-8-validate@5.0.10) '@uniswap/universal-router-sdk': specifier: ^4.19.6 version: 4.19.6(bufferutil@4.0.9)(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10) @@ -2699,12 +2699,12 @@ packages: '@uniswap/permit2-sdk@1.3.1': resolution: {integrity: sha512-Eq2by4zVEVSZL3PJ1Yuf5+AZ/yE1GOuksWzPXPoxr5WRm3hqh34jKEqtyTImHqwuPrdILG8i02xJmgGLTH1QfA==} - '@uniswap/router-sdk@1.23.0': - resolution: {integrity: sha512-KkHoMauTZh2N44sOU0ZuYseNNn9nAvaU57HwyCWjtwZdA7HaXtACfIRJbQvnkNNuALJfzHNkuv2aFyPSjNNmMw==} - '@uniswap/router-sdk@2.0.4': resolution: {integrity: sha512-MxCtD+g+2pzzd9rZ6HKTdv1ZK2mLjREoDRNAp9+F961zCCVhgJr9L1/6Hour27/xxCyljwmG83Zn1cSS054giw==} + '@uniswap/router-sdk@2.2.0': + resolution: {integrity: sha512-9xWepoISYXYyp9w2C1svegXsjqY0zO/qcheH1fizgHRJUJ3GQ5IbewOd9E6M0pTPlYOsIigOLIFXRCTDIbzu3w==} + '@uniswap/sdk-core@5.9.0': resolution: {integrity: sha512-OME7WR6+5QwQs45A2079r+/FS0zU944+JCQwUX9GyIriCxqw2pGu4F9IEqmlwD+zSIMml0+MJnJJ47pFgSyWDw==} engines: {node: '>=10'} @@ -2713,6 +2713,10 @@ packages: resolution: {integrity: sha512-0KqXw+y0opBo6eoPAEoLHEkNpOu0NG9gEk7GAYIGok+SHX89WlykWsRYeJKTg9tOwhLpcG9oHg8xZgQ390iOrA==} engines: {node: '>=10'} + '@uniswap/sdk-core@7.9.0': + resolution: {integrity: sha512-HHUFNK3LMi4KMQCAiHkdUyL62g/nrZLvNT44CY8RN4p8kWO6XYWzqdQt6OcjCsIbhMZ/Ifhe6Py5oOoccg/jUQ==} + engines: {node: '>=10'} + '@uniswap/sdk@3.0.3': resolution: {integrity: sha512-t4s8bvzaCFSiqD2qfXIm3rWhbdnXp+QjD3/mRaeVDHK7zWevs6RGEb1ohMiNgOCTZANvBayb4j8p+XFdnMBadQ==} engines: {node: '>=10'} @@ -2723,8 +2727,8 @@ packages: '@ethersproject/providers': ^5.0.0-beta '@ethersproject/solidity': ^5.0.0-beta - '@uniswap/smart-order-router@3.59.0': - resolution: {integrity: sha512-1le8eLk/zK6meWs2ky2QnGiU1poqWgxCzl2KxlukyWetfdxSeKkHFcsMmV4nk2o7/R6duuFU47yUTjUX3HYhKw==} + '@uniswap/smart-order-router@4.22.38': + resolution: {integrity: sha512-30l2ei0ZmdxOvwx5h0POT99R/GYxPrvTUb5sb+tiZ66Bv9w/AI8qL1YH0utTo9PGFFwu0FkLusTVAVdckN4rDw==} engines: {node: '>=10'} peerDependencies: jsbi: ^3.2.0 @@ -2737,26 +2741,26 @@ packages: resolution: {integrity: sha512-Hc3TfrFaupg0M84e/Zv7BoF+fmMWDV15mZ5s8ZQt2qZxUcNw2GQW+L6L/2k74who31G+p1m3GRYbJpAo7d1pqA==} engines: {node: '>=10'} - '@uniswap/universal-router-sdk@3.4.0': - resolution: {integrity: sha512-EB/NLIkuT2BCdKnh2wcXT0cmINjRoiskjibFclpheALHL49XSrB08H4k7KV3BP6+JNKLeTHekvTDdsMd9rs5TA==} - engines: {node: '>=14'} - '@uniswap/universal-router-sdk@4.19.6': resolution: {integrity: sha512-vBtHv4OzEn6Spkl1UgN/0TqO354w7RUdsE1uwAdqz2zfxhV48GOlKJWpe7LiI2ZukL/BMubLewtwC4q/RfjjJQ==} engines: {node: '>=14'} - '@uniswap/universal-router@1.6.0': - resolution: {integrity: sha512-Gt0b0rtMV1vSrgXY3vz5R1RCZENB+rOkbOidY9GvcXrK1MstSrQSOAc+FCr8FSgsDhmRAdft0lk5YUxtM9i9Lg==} + '@uniswap/universal-router-sdk@4.23.0': + resolution: {integrity: sha512-lSWXMoH4fMGHG1s00mR0ivIuBgdW/mR/Y+CuIpxOSDxgwtP86/7JHPfPWcH7EVU5dstSIyzprUwZ/a8v7vlaGg==} engines: {node: '>=14'} - '@uniswap/universal-router@2.0.0-beta.1': - resolution: {integrity: sha512-DdaMHaoDyJoCwpH+BiRKw/w2vjZtZS+ekpyrhmIeOBK1L2QEVFj977BNo6t24WzriZ9mSuIKF69RjHdXDUgHsQ==} + '@uniswap/universal-router@1.6.0': + resolution: {integrity: sha512-Gt0b0rtMV1vSrgXY3vz5R1RCZENB+rOkbOidY9GvcXrK1MstSrQSOAc+FCr8FSgsDhmRAdft0lk5YUxtM9i9Lg==} engines: {node: '>=14'} '@uniswap/universal-router@2.0.0-beta.2': resolution: {integrity: sha512-/USVkWZrOCjLeZluR7Yk8SpfWDUKG/MLcOyuxuwnqM1xCJj5ekguSYhct+Yfo/3t9fsZcnL8vSYgz0MKqAomGg==} engines: {node: '>=14'} + '@uniswap/universal-router@2.1.0': + resolution: {integrity: sha512-rt18RUsZd9xDfyVfIONJo+TEQ8w+olOYxu9+A1g4Thil1R7IMa+8mnyVQjdLPK2REhejScDwjYbOGpeaAce0hg==} + engines: {node: '>=14'} + '@uniswap/v2-core@1.0.1': resolution: {integrity: sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q==} engines: {node: '>=10'} @@ -2765,6 +2769,10 @@ packages: resolution: {integrity: sha512-EtROgWTdhHzw4EUj7SdK9wjppOG7psJ16c656cRuv69nWbD9QyDL2shVcQccEiY7ak9WlJ+bIv/VldybXYBDuw==} engines: {node: '>=10'} + '@uniswap/v2-sdk@4.16.0': + resolution: {integrity: sha512-USMm2qz1xhEX8R0dhd0mHzf6pz5aCLjbtud1ZyUBk+gshhUCFp6NW9UovH0L5hqrH03rTvmqQdfhHMW5m+Sosg==} + engines: {node: '>=10'} + '@uniswap/v3-core@1.0.0': resolution: {integrity: sha512-kSC4djMGKMHj7sLMYVnn61k9nu+lHjMIxgg9CDQT+s2QYLoA56GbSK9Oxr+qJXzzygbkrmuY6cwgP6cW2JXPFA==} engines: {node: '>=10'} @@ -2781,6 +2789,10 @@ packages: resolution: {integrity: sha512-0oiyJNGjUVbc958uZmAr+m4XBCjV7PfMs/OUeBv+XDl33MEYF/eH86oBhvqGDM8S/cYaK55tCXzoWkmRUByrHg==} engines: {node: '>=10'} + '@uniswap/v3-sdk@3.26.0': + resolution: {integrity: sha512-bcoWNE7ntNNTHMOnDPscIqtIN67fUyrbBKr6eswI2gD2wm5b0YYFBDeh+Qc5Q3117o9i8S7QdftqrU8YSMQUfQ==} + engines: {node: '>=10'} + '@uniswap/v3-staker@1.0.0': resolution: {integrity: sha512-JV0Qc46Px5alvg6YWd+UIaGH9lDuYG/Js7ngxPit1SPaIP30AlVer1UYB7BRYeUVVxE+byUyIeN5jeQ7LLDjIw==} engines: {node: '>=10'} @@ -2790,6 +2802,10 @@ packages: resolution: {integrity: sha512-so3c/CmaRmRSvgKFyrUWy6DCSogyzyVaoYCec/TJ4k2hXlJ8MK4vumcuxtmRr1oMnZ5KmaCPBS12Knb4FC3nsw==} engines: {node: '>=14'} + '@uniswap/v4-sdk@1.23.0': + resolution: {integrity: sha512-WpnkNacNTe/qL4kj3DVC2nHaivUeuzYsWIvon+olAWYZyy+Frsnzfon/ZlznDifMPoV+im+MqYFsNQke4Vz3LA==} + engines: {node: '>=14'} + '@unrs/resolver-binding-android-arm-eabi@1.9.2': resolution: {integrity: sha512-tS+lqTU3N0kkthU+rYp0spAYq15DU8ld9kXkaKg9sbQqJNF+WPMuNHZQGCgdxrUOEO0j22RKMwRVhF1HTl+X8A==} cpu: [arm] @@ -5263,10 +5279,6 @@ packages: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true @@ -11309,7 +11321,7 @@ snapshots: - bufferutil - utf-8-validate - '@uniswap/router-sdk@1.23.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))': + '@uniswap/router-sdk@2.0.4(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))': dependencies: '@ethersproject/abi': 5.8.0 '@uniswap/sdk-core': 7.7.2 @@ -11320,14 +11332,14 @@ snapshots: transitivePeerDependencies: - hardhat - '@uniswap/router-sdk@2.0.4(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))': + '@uniswap/router-sdk@2.2.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))': dependencies: '@ethersproject/abi': 5.8.0 - '@uniswap/sdk-core': 7.7.2 + '@uniswap/sdk-core': 7.9.0 '@uniswap/swap-router-contracts': 1.3.1(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@uniswap/v2-sdk': 4.15.2 - '@uniswap/v3-sdk': 3.25.2(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@uniswap/v4-sdk': 1.21.4(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@uniswap/v2-sdk': 4.16.0 + '@uniswap/v3-sdk': 3.26.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@uniswap/v4-sdk': 1.23.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) transitivePeerDependencies: - hardhat @@ -11355,6 +11367,18 @@ snapshots: tiny-invariant: 1.3.3 toformat: 2.0.0 + '@uniswap/sdk-core@7.9.0': + dependencies: + '@ethersproject/address': 5.7.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/strings': 5.7.0 + big.js: 5.2.2 + decimal.js-light: 2.5.1 + jsbi: 3.2.5 + tiny-invariant: 1.3.3 + toformat: 2.0.0 + '@uniswap/sdk@3.0.3(@ethersproject/address@5.7.0)(@ethersproject/contracts@5.7.0)(@ethersproject/networks@5.7.0)(@ethersproject/providers@5.7.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@ethersproject/solidity@5.7.0)': dependencies: '@ethersproject/address': 5.7.0 @@ -11370,21 +11394,21 @@ snapshots: tiny-warning: 1.0.3 toformat: 2.0.0 - '@uniswap/smart-order-router@3.59.0(bufferutil@4.0.9)(encoding@0.1.13)(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))(jsbi@3.2.5)(utf-8-validate@5.0.10)': + '@uniswap/smart-order-router@4.22.38(bufferutil@4.0.9)(encoding@0.1.13)(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))(jsbi@3.2.5)(utf-8-validate@5.0.10)': dependencies: '@eth-optimism/sdk': 3.3.3(bufferutil@4.0.9)(encoding@0.1.13)(ethers@5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10) '@types/brotli': 1.3.4 '@uniswap/default-token-list': 11.19.0 '@uniswap/permit2-sdk': 1.3.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@uniswap/router-sdk': 1.23.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@uniswap/sdk-core': 5.9.0 + '@uniswap/router-sdk': 2.2.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@uniswap/sdk-core': 7.9.0 '@uniswap/swap-router-contracts': 1.3.1(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) '@uniswap/token-lists': 1.0.0-beta.34 '@uniswap/universal-router': 1.6.0 - '@uniswap/universal-router-sdk': 3.4.0(bufferutil@4.0.9)(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10) - '@uniswap/v2-sdk': 4.15.2 - '@uniswap/v3-sdk': 3.25.2(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@uniswap/v4-sdk': 1.21.4(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@uniswap/universal-router-sdk': 4.23.0(bufferutil@4.0.9)(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10) + '@uniswap/v2-sdk': 4.16.0 + '@uniswap/v3-sdk': 3.26.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@uniswap/v4-sdk': 1.23.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) async-retry: 1.3.3 await-timeout: 1.1.1 axios: 1.12.0 @@ -11419,13 +11443,13 @@ snapshots: '@uniswap/token-lists@1.0.0-beta.34': {} - '@uniswap/universal-router-sdk@3.4.0(bufferutil@4.0.9)(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10)': + '@uniswap/universal-router-sdk@4.19.6(bufferutil@4.0.9)(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10)': dependencies: '@openzeppelin/contracts': 4.9.6 '@uniswap/permit2-sdk': 1.3.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@uniswap/router-sdk': 1.23.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@uniswap/sdk-core': 5.9.0 - '@uniswap/universal-router': 2.0.0-beta.1 + '@uniswap/router-sdk': 2.0.4(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@uniswap/sdk-core': 7.7.2 + '@uniswap/universal-router': 2.0.0-beta.2 '@uniswap/v2-core': 1.0.1 '@uniswap/v2-sdk': 4.15.2 '@uniswap/v3-core': 1.0.0 @@ -11438,18 +11462,18 @@ snapshots: - hardhat - utf-8-validate - '@uniswap/universal-router-sdk@4.19.6(bufferutil@4.0.9)(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10)': + '@uniswap/universal-router-sdk@4.23.0(bufferutil@4.0.9)(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10)': dependencies: '@openzeppelin/contracts': 4.9.6 '@uniswap/permit2-sdk': 1.3.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@uniswap/router-sdk': 2.0.4(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@uniswap/sdk-core': 7.7.2 - '@uniswap/universal-router': 2.0.0-beta.2 + '@uniswap/router-sdk': 2.2.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@uniswap/sdk-core': 7.9.0 + '@uniswap/universal-router': 2.1.0 '@uniswap/v2-core': 1.0.1 - '@uniswap/v2-sdk': 4.15.2 + '@uniswap/v2-sdk': 4.16.0 '@uniswap/v3-core': 1.0.0 - '@uniswap/v3-sdk': 3.25.2(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@uniswap/v4-sdk': 1.21.4(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@uniswap/v3-sdk': 3.26.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@uniswap/v4-sdk': 1.23.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) bignumber.js: 9.3.0 ethers: 5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) transitivePeerDependencies: @@ -11463,13 +11487,13 @@ snapshots: '@uniswap/v2-core': 1.0.1 '@uniswap/v3-core': 1.0.0 - '@uniswap/universal-router@2.0.0-beta.1': + '@uniswap/universal-router@2.0.0-beta.2': dependencies: '@openzeppelin/contracts': 4.9.6 '@uniswap/v2-core': 1.0.1 '@uniswap/v3-core': 1.0.0 - '@uniswap/universal-router@2.0.0-beta.2': + '@uniswap/universal-router@2.1.0': dependencies: '@openzeppelin/contracts': 4.9.6 '@uniswap/v2-core': 1.0.1 @@ -11485,6 +11509,14 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 + '@uniswap/v2-sdk@4.16.0': + dependencies: + '@ethersproject/address': 5.7.0 + '@ethersproject/solidity': 5.7.0 + '@uniswap/sdk-core': 7.9.0 + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + '@uniswap/v3-core@1.0.0': {} '@uniswap/v3-core@1.0.1': {} @@ -11510,6 +11542,19 @@ snapshots: transitivePeerDependencies: - hardhat + '@uniswap/v3-sdk@3.26.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))': + dependencies: + '@ethersproject/abi': 5.8.0 + '@ethersproject/solidity': 5.7.0 + '@uniswap/sdk-core': 7.9.0 + '@uniswap/swap-router-contracts': 1.3.1(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@uniswap/v3-periphery': 1.4.4 + '@uniswap/v3-staker': 1.0.0 + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + transitivePeerDependencies: + - hardhat + '@uniswap/v3-staker@1.0.0': dependencies: '@openzeppelin/contracts': 4.9.6 @@ -11526,6 +11571,16 @@ snapshots: transitivePeerDependencies: - hardhat + '@uniswap/v4-sdk@1.23.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))': + dependencies: + '@ethersproject/solidity': 5.7.0 + '@uniswap/sdk-core': 7.9.0 + '@uniswap/v3-sdk': 3.26.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + transitivePeerDependencies: + - hardhat + '@unrs/resolver-binding-android-arm-eabi@1.9.2': optional: true @@ -14602,10 +14657,6 @@ snapshots: argparse: 1.0.10 esprima: 4.0.1 - js-yaml@4.1.0: - dependencies: - argparse: 2.0.1 - js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -14951,7 +15002,7 @@ snapshots: find-up: 5.0.0 glob: 8.1.0 he: 1.2.0 - js-yaml: 4.1.0 + js-yaml: 4.1.1 log-symbols: 4.1.0 minimatch: 5.1.6 ms: 2.1.3 diff --git a/src/connectors/uniswap/alpha-router.ts b/src/connectors/uniswap/alpha-router.ts new file mode 100644 index 0000000000..e4111e6925 --- /dev/null +++ b/src/connectors/uniswap/alpha-router.ts @@ -0,0 +1,142 @@ +import { BaseProvider } from '@ethersproject/providers'; +import { CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'; +import { AlphaRouter, SwapRoute, SwapType } from '@uniswap/smart-order-router'; +import { UniversalRouterVersion } from '@uniswap/universal-router-sdk'; + +import { logger } from '../../services/logger'; + +// Chain IDs as numbers (matching @uniswap/sdk-core ChainId enum values) +const NETWORK_TO_CHAIN_ID: { [network: string]: number } = { + mainnet: 1, + goerli: 5, + sepolia: 11155111, + arbitrum: 42161, + optimism: 10, + polygon: 137, + base: 8453, + bsc: 56, + avalanche: 43114, + celo: 42220, +}; + +export interface AlphaRouterQuoteResult { + route: SwapRoute; + inputAmount: string; + outputAmount: string; + priceImpact: number; + routeString: string; + gasEstimate: string; + gasEstimateUSD: string; + methodParameters?: { + calldata: string; + value: string; + to: string; + }; +} + +export class AlphaRouterService { + private router: AlphaRouter; + private chainId: number; + private network: string; + + constructor(provider: BaseProvider, network: string) { + const chainId = NETWORK_TO_CHAIN_ID[network]; + if (!chainId) { + throw new Error(`Unsupported network for AlphaRouter: ${network}`); + } + + this.chainId = chainId; + this.network = network; + + // Initialize AlphaRouter with minimal config + // It will use default providers for pools, quotes, etc. + this.router = new AlphaRouter({ + chainId: this.chainId, + provider: provider, + }); + + logger.info(`[AlphaRouter] Initialized for network ${network} (chainId: ${chainId})`); + } + + /** + * Get an optimized quote using AlphaRouter's smart order routing + * This will automatically find the best route across V2, V3, and mixed pools + * with optimal split routing for better execution prices. + */ + async getQuote( + tokenIn: Token, + tokenOut: Token, + amount: CurrencyAmount, + tradeType: TradeType, + options: { + slippageTolerance: Percent; + deadline: number; + recipient: string; + }, + ): Promise { + logger.info(`[AlphaRouter] Starting quote generation`); + logger.info(`[AlphaRouter] Input: ${amount.toExact()} ${tokenIn.symbol} (${tokenIn.address})`); + logger.info(`[AlphaRouter] Output: ${tokenOut.symbol} (${tokenOut.address})`); + logger.info(`[AlphaRouter] Trade type: ${tradeType === TradeType.EXACT_INPUT ? 'EXACT_INPUT' : 'EXACT_OUTPUT'}`); + logger.info(`[AlphaRouter] Recipient: ${options.recipient}`); + logger.info(`[AlphaRouter] Slippage: ${options.slippageTolerance.toSignificant()}%`); + + const swapRoute = await this.router.route(amount, tokenOut, tradeType, { + type: SwapType.UNIVERSAL_ROUTER, + version: UniversalRouterVersion.V2_0, + slippageTolerance: options.slippageTolerance, + deadlineOrPreviousBlockhash: options.deadline, + recipient: options.recipient, + }); + + if (!swapRoute) { + throw new Error(`No route found for ${tokenIn.symbol} -> ${tokenOut.symbol}`); + } + + logger.info(`[AlphaRouter] Route found!`); + logger.info(`[AlphaRouter] Quote: ${swapRoute.quote.toExact()} ${swapRoute.quote.currency.symbol}`); + logger.info(`[AlphaRouter] Gas estimate: ${swapRoute.estimatedGasUsed.toString()}`); + logger.info(`[AlphaRouter] Gas estimate USD: $${swapRoute.estimatedGasUsedUSD.toExact()}`); + + // Log route details (split routing info) + const routeStrings: string[] = []; + for (const route of swapRoute.route) { + const routeStr = route.tokenPath.map((t) => t.symbol).join(' -> '); + const percent = route.percent; + routeStrings.push(`${percent}% via ${routeStr}`); + logger.info(`[AlphaRouter] Route: ${percent}% via ${routeStr}`); + } + + // Extract method parameters for Universal Router execution + let methodParameters: AlphaRouterQuoteResult['methodParameters']; + if (swapRoute.methodParameters) { + methodParameters = { + calldata: swapRoute.methodParameters.calldata, + value: swapRoute.methodParameters.value, + to: swapRoute.methodParameters.to, + }; + logger.info(`[AlphaRouter] Calldata length: ${methodParameters.calldata.length}`); + logger.info(`[AlphaRouter] Value: ${methodParameters.value}`); + logger.info(`[AlphaRouter] To: ${methodParameters.to}`); + } + + const result: AlphaRouterQuoteResult = { + route: swapRoute, + inputAmount: tradeType === TradeType.EXACT_INPUT ? amount.toExact() : swapRoute.quote.toExact(), + outputAmount: tradeType === TradeType.EXACT_INPUT ? swapRoute.quote.toExact() : amount.toExact(), + priceImpact: parseFloat(swapRoute.trade?.priceImpact?.toSignificant(4) || '0'), + routeString: routeStrings.join(' | '), + gasEstimate: swapRoute.estimatedGasUsed.toString(), + gasEstimateUSD: swapRoute.estimatedGasUsedUSD.toExact(), + methodParameters, + }; + + logger.info(`[AlphaRouter] Quote generation complete`); + logger.info(`[AlphaRouter] Input: ${result.inputAmount} ${tokenIn.symbol}`); + logger.info(`[AlphaRouter] Output: ${result.outputAmount} ${tokenOut.symbol}`); + logger.info(`[AlphaRouter] Price Impact: ${result.priceImpact}%`); + logger.info(`[AlphaRouter] Routes: ${result.routeString}`); + + return result; + } +} diff --git a/src/connectors/uniswap/router-routes/quoteSwap.ts b/src/connectors/uniswap/router-routes/quoteSwap.ts index cf33b887e8..33bdeefd4f 100644 --- a/src/connectors/uniswap/router-routes/quoteSwap.ts +++ b/src/connectors/uniswap/router-routes/quoteSwap.ts @@ -22,7 +22,7 @@ async function quoteSwap( side: 'BUY' | 'SELL', slippagePct: number = UniswapConfig.config.slippagePct, ): Promise> { - logger.info(`[quoteSwap] Starting quote generation`); + logger.info(`[quoteSwap] Starting quote generation using AlphaRouter`); logger.info(`[quoteSwap] Network: ${network}, Wallet: ${walletAddress || 'not provided'}`); logger.info(`[quoteSwap] Base: ${baseToken}, Quote: ${quoteToken}`); logger.info(`[quoteSwap] Amount: ${amount}, Side: ${side}, Slippage: ${slippagePct}%`); @@ -54,32 +54,27 @@ async function quoteSwap( logger.info(`[quoteSwap] Output token: ${outputToken.symbol} (${outputToken.address})`); logger.info(`[quoteSwap] Exact in: ${exactIn}`); - // Get quote from Universal Router - logger.info(`[quoteSwap] Calling getUniversalRouterQuote...`); - const quoteResult = await uniswap.getUniversalRouterQuote(inputToken, outputToken, amount, side, walletAddress); + // Get quote from AlphaRouter (smart order router with split routing) + // Use a placeholder address for quotes when no wallet is provided + const recipient = walletAddress || '0x0000000000000000000000000000000000000001'; + logger.info(`[quoteSwap] Calling getAlphaRouterQuote with slippage ${slippagePct}%...`); + const quoteResult = await uniswap.getAlphaRouterQuote(inputToken, outputToken, amount, side, recipient, slippagePct); logger.info(`[quoteSwap] Quote result received`); // Generate unique quote ID const quoteId = uuidv4(); logger.info(`[quoteSwap] Generated quote ID: ${quoteId}`); - // Extract route information from quoteResult - const routePath = quoteResult.routePath; + // Extract route information from AlphaRouter result + const routePath = quoteResult.routeString; logger.info(`[quoteSwap] Route path: ${routePath}`); - // Calculate amounts based on quote - let estimatedAmountIn: number; - let estimatedAmountOut: number; - - if (exactIn) { - estimatedAmountIn = amount; - estimatedAmountOut = parseFloat(quoteResult.quote.toExact()); - } else { - estimatedAmountIn = parseFloat(quoteResult.trade.inputAmount.toExact()); - estimatedAmountOut = amount; - } + // Get amounts from AlphaRouter result + const estimatedAmountIn = parseFloat(quoteResult.inputAmount); + const estimatedAmountOut = parseFloat(quoteResult.outputAmount); logger.info(`[quoteSwap] Estimated amounts - In: ${estimatedAmountIn}, Out: ${estimatedAmountOut}`); + logger.info(`[quoteSwap] Gas estimate: ${quoteResult.gasEstimate} (${quoteResult.gasEstimateUSD} USD)`); const minAmountOut = side === 'SELL' ? estimatedAmountOut * (1 - slippagePct / 100) : estimatedAmountOut; const maxAmountIn = side === 'BUY' ? estimatedAmountIn * (1 + slippagePct / 100) : estimatedAmountIn; @@ -93,9 +88,11 @@ async function quoteSwap( // Cache the quote for execution // Store both quote and request data in the quote object for Uniswap + // Include 'trade' at top level for compatibility with executeQuote const cachedQuote = { quote: { ...quoteResult, + trade: quoteResult.route.trade, // Extract trade from SwapRoute for executeQuote compatibility methodParameters: quoteResult.methodParameters, }, request: { diff --git a/src/connectors/uniswap/uniswap.ts b/src/connectors/uniswap/uniswap.ts index db8db345de..1eb8d2e02c 100644 --- a/src/connectors/uniswap/uniswap.ts +++ b/src/connectors/uniswap/uniswap.ts @@ -12,6 +12,7 @@ import JSBI from 'jsbi'; import { Ethereum, TokenInfo } from '../../chains/ethereum/ethereum'; import { logger } from '../../services/logger'; +import { AlphaRouterService, AlphaRouterQuoteResult } from './alpha-router'; import { UniswapConfig } from './uniswap.config'; import { IUniswapV2PairABI, @@ -48,6 +49,7 @@ export class Uniswap { private v3NFTManager: Contract; private v3Quoter: Contract; private universalRouter: UniversalRouterService; + private alphaRouter: AlphaRouterService; // Network information private networkName: string; @@ -139,6 +141,14 @@ export class Uniswap { // Initialize Universal Router service this.universalRouter = new UniversalRouterService(this.ethereum.provider, this.chainId, this.networkName); + // Initialize AlphaRouter service for split routing + try { + this.alphaRouter = new AlphaRouterService(this.ethereum.provider, this.networkName); + logger.info(`AlphaRouter initialized for network: ${this.networkName}`); + } catch (error) { + logger.warn(`AlphaRouter not available for network ${this.networkName}: ${error.message}`); + } + // Ensure ethereum is initialized if (!this.ethereum.ready()) { await this.ethereum.init(); @@ -226,6 +236,60 @@ export class Uniswap { return quoteResult; } + /** + * Get a quote using AlphaRouter's split routing for optimal execution + * This uses Uniswap's smart order router to find the best route across V2, V3, and mixed pools + * with optimal split percentages for better execution prices. + * + * @param inputToken The token being swapped from + * @param outputToken The token being swapped to + * @param amount The amount to swap + * @param side The trade direction (BUY or SELL) + * @param walletAddress The recipient wallet address + * @param slippagePct Optional slippage percentage (defaults to config value) + * @returns Quote result with split routing information + */ + public async getAlphaRouterQuote( + inputToken: Token, + outputToken: Token, + amount: number, + side: 'BUY' | 'SELL', + walletAddress: string, + slippagePct?: number, + ): Promise { + if (!this.alphaRouter) { + throw new Error(`AlphaRouter not available for network ${this.networkName}`); + } + + // Determine input/output based on side + const exactIn = side === 'SELL'; + const tokenForAmount = exactIn ? inputToken : outputToken; + + // Convert amount to token units using ethers parseUnits for proper decimal handling + const { parseUnits } = await import('ethers/lib/utils'); + const rawAmount = parseUnits(amount.toString(), tokenForAmount.decimals); + const tradeAmount = CurrencyAmount.fromRawAmount(tokenForAmount, rawAmount.toString()); + + // Use provided slippage or fall back to config + const slippage = slippagePct ?? this.config.slippagePct; + const slippageTolerance = new Percent(Math.floor(slippage * 100), 10000); + + // Get quote from AlphaRouter + const quoteResult = await this.alphaRouter.getQuote( + inputToken, + outputToken, + tradeAmount, + exactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT, + { + slippageTolerance, + deadline: Math.floor(Date.now() / 1000 + 1800), // 30 minutes + recipient: walletAddress, + }, + ); + + return quoteResult; + } + /** * Get a V2 pool (pair) by its address or by token symbols */ From 05467f7fe350a4d6e49b3fe91f2a8c8e6ecad3d4 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Wed, 3 Dec 2025 17:34:42 -0800 Subject: [PATCH 07/14] feat: add maxHops/maxSplits support to PancakeSwap Smart Router MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add maximumSplits config option to pancakeswap.yml - Pass maxHops and maxSplits from config to SmartRouter.getBestTrade() - Update Uniswap quoteSwap test to use AlphaRouterQuoteResult mock format 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../pancakeswap/pancakeswap.config.ts | 4 +- src/connectors/pancakeswap/pancakeswap.ts | 2 + .../pancakeswap/universal-router.ts | 10 ++++ src/templates/connectors/pancakeswap.yml | 7 ++- .../universal-router-quoteSwap.test.ts | 55 +++++++------------ 5 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/connectors/pancakeswap/pancakeswap.config.ts b/src/connectors/pancakeswap/pancakeswap.config.ts index c254f0f436..22e901de43 100644 --- a/src/connectors/pancakeswap/pancakeswap.config.ts +++ b/src/connectors/pancakeswap/pancakeswap.config.ts @@ -18,6 +18,7 @@ export namespace PancakeswapConfig { // Global configuration slippagePct: number; maximumHops: number; + maximumSplits: number; // Available networks availableNetworks: Array; @@ -25,7 +26,8 @@ export namespace PancakeswapConfig { export const config: RootConfig = { slippagePct: ConfigManagerV2.getInstance().get('pancakeswap.slippagePct'), - maximumHops: ConfigManagerV2.getInstance().get('pancakeswap.maximumHops') || 4, + maximumHops: ConfigManagerV2.getInstance().get('pancakeswap.maximumHops'), + maximumSplits: ConfigManagerV2.getInstance().get('pancakeswap.maximumSplits'), availableNetworks: [ { diff --git a/src/connectors/pancakeswap/pancakeswap.ts b/src/connectors/pancakeswap/pancakeswap.ts index 0e32da1c71..7ead39828d 100644 --- a/src/connectors/pancakeswap/pancakeswap.ts +++ b/src/connectors/pancakeswap/pancakeswap.ts @@ -222,6 +222,8 @@ export class Pancakeswap { deadline: Math.floor(Date.now() / 1000 + 1800), // 30 minutes recipient, protocols: protocolsToUse, + maxHops: this.config.maximumHops, + maxSplits: this.config.maximumSplits, }, ); diff --git a/src/connectors/pancakeswap/universal-router.ts b/src/connectors/pancakeswap/universal-router.ts index ed73e1c035..2a1d154b4d 100644 --- a/src/connectors/pancakeswap/universal-router.ts +++ b/src/connectors/pancakeswap/universal-router.ts @@ -84,6 +84,8 @@ export class UniversalRouterService { deadline: number; recipient: string; protocols?: PoolType[]; + maxHops?: number; + maxSplits?: number; }, ): Promise { logger.info(`[UniversalRouter] Starting quote generation`); @@ -94,6 +96,12 @@ export class UniversalRouterService { ); logger.info(`[UniversalRouter] Recipient: ${options.recipient}`); logger.info(`[UniversalRouter] Slippage: ${options.slippageTolerance.toSignificant()}%`); + if (options.maxHops !== undefined) { + logger.info(`[UniversalRouter] Max hops: ${options.maxHops}`); + } + if (options.maxSplits !== undefined) { + logger.info(`[UniversalRouter] Max splits: ${options.maxSplits}`); + } const protocols = options.protocols || [PoolType.V2, PoolType.V3]; logger.info(`[UniversalRouter] Protocols to check: ${protocols.join(', ')}`); @@ -166,6 +174,8 @@ export class UniversalRouterService { quoteProvider, quoterOptimization: true, gasPriceWei, + maxHops: options.maxHops, + maxSplits: options.maxSplits, }; // Create RouterTrade based on the best route diff --git a/src/templates/connectors/pancakeswap.yml b/src/templates/connectors/pancakeswap.yml index 886434ead7..6452264cd4 100644 --- a/src/templates/connectors/pancakeswap.yml +++ b/src/templates/connectors/pancakeswap.yml @@ -1,6 +1,9 @@ -# Global settings for Uniswap +# Global settings for PancakeSwap # Default slippage percentage for swaps (2%) slippagePct: 2 # For each swap, the maximum number of hops to consider -maximumHops: 4 \ No newline at end of file +maximumHops: 4 + +# Maximum number of split routes for optimal execution (e.g., 60% via V3, 40% via V2) +maximumSplits: 4 \ No newline at end of file diff --git a/test/connectors/uniswap/router-routes/universal-router-quoteSwap.test.ts b/test/connectors/uniswap/router-routes/universal-router-quoteSwap.test.ts index a24b083e10..3b2ae2dfc7 100644 --- a/test/connectors/uniswap/router-routes/universal-router-quoteSwap.test.ts +++ b/test/connectors/uniswap/router-routes/universal-router-quoteSwap.test.ts @@ -10,15 +10,7 @@ jest.mock('../../../../src/connectors/uniswap/uniswap'); jest.mock('uuid'); // Create a variable to store the mock implementation -const mockGetQuote = jest.fn(); -const mockUniversalRouterService = { - getQuote: mockGetQuote, -}; - -// Mock the UniversalRouterService -jest.mock('../../../../src/connectors/uniswap/universal-router', () => ({ - UniversalRouterService: jest.fn().mockImplementation(() => mockUniversalRouterService), -})); +const mockGetAlphaRouterQuote = jest.fn(); const buildApp = async () => { const server = fastifyWithTypeProvider(); @@ -59,20 +51,16 @@ describe('GET /quote-swap', () => { beforeEach(() => { jest.clearAllMocks(); - // Reset the UniversalRouterService mock to default behavior - mockGetQuote.mockResolvedValue({ - trade: { - inputAmount: { toExact: () => '1' }, - outputAmount: { toExact: () => '3000' }, - priceImpact: { toSignificant: () => '0.3' }, - }, - route: ['WETH', 'USDC'], - routePath: 'WETH -> USDC', + // Reset the AlphaRouter mock to default behavior + // This matches the AlphaRouterQuoteResult interface + mockGetAlphaRouterQuote.mockResolvedValue({ + route: { trade: { priceImpact: { toSignificant: () => '0.3' } } }, + inputAmount: '1', + outputAmount: '3000', priceImpact: 0.3, - estimatedGasUsed: { toString: () => '300000' }, - estimatedGasUsedQuoteToken: { toExact: () => '0.5' }, - quote: { toExact: () => '3000' }, - quoteGasAdjusted: { toExact: () => '2999.5' }, + routeString: 'WETH -> USDC', + gasEstimate: '300000', + gasEstimateUSD: '5.00', methodParameters: { calldata: '0x1234567890', value: '0x0', @@ -116,7 +104,7 @@ describe('GET /quote-swap', () => { mockUniswap = { router: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', getUniswapToken: jest.fn().mockImplementation((tokenInfo) => tokenInfo), - getUniversalRouterQuote: mockGetQuote, + getAlphaRouterQuote: mockGetAlphaRouterQuote, }; (Ethereum.getInstance as jest.Mock).mockReturnValue(mockEthereum); @@ -185,20 +173,15 @@ describe('GET /quote-swap', () => { }); it('should return a valid quote for BUY side', async () => { - // Update mock for BUY side - mockGetQuote.mockResolvedValue({ - trade: { - inputAmount: { toExact: () => '3000' }, - outputAmount: { toExact: () => '1' }, - priceImpact: { toSignificant: () => '0.3' }, - }, - route: ['USDC', 'WETH'], - routePath: 'USDC -> WETH', + // Update mock for BUY side - AlphaRouterQuoteResult format + mockGetAlphaRouterQuote.mockResolvedValue({ + route: { trade: { priceImpact: { toSignificant: () => '0.3' } } }, + inputAmount: '3000', + outputAmount: '1', priceImpact: 0.3, - estimatedGasUsed: { toString: () => '300000' }, - estimatedGasUsedQuoteToken: { toExact: () => '0.5' }, - quote: { toExact: () => '1' }, - quoteGasAdjusted: { toExact: () => '0.9995' }, + routeString: 'USDC -> WETH', + gasEstimate: '300000', + gasEstimateUSD: '5.00', methodParameters: { calldata: '0x1234567890', value: '0x0', From 9628f4e8c3ee64df934073a75173f46df21c5091 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Wed, 3 Dec 2025 17:48:25 -0800 Subject: [PATCH 08/14] fix: add maximumSplits to PancakeSwap JSON schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/templates/namespace/pancakeswap-schema.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/templates/namespace/pancakeswap-schema.json b/src/templates/namespace/pancakeswap-schema.json index 9974137371..99beef6e9b 100644 --- a/src/templates/namespace/pancakeswap-schema.json +++ b/src/templates/namespace/pancakeswap-schema.json @@ -9,8 +9,12 @@ "maximumHops": { "type": "integer", "description": "Maximum number of hops to consider for each swap" + }, + "maximumSplits": { + "type": "integer", + "description": "Maximum number of split routes for optimal execution (e.g., 60% via V3, 40% via V2)" } }, "additionalProperties": false, - "required": ["slippagePct", "maximumHops"] + "required": ["slippagePct", "maximumHops", "maximumSplits"] } From 09698d15132d1d217fdf05fb09574f691b1b264e Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Thu, 4 Dec 2025 06:28:05 -0800 Subject: [PATCH 09/14] fix: correct quoteCurrency parameter for EXACT_OUTPUT trades in AlphaRouter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For EXACT_OUTPUT trades, the AlphaRouter's route() method expects the quoteCurrency to be the input token (what we pay), not the output token. The previous code always passed tokenOut, which caused "Invariant failed: ADDRESSES" errors when getting BUY quotes because the V4 pool provider tried to compare a token with itself. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/connectors/uniswap/alpha-router.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/connectors/uniswap/alpha-router.ts b/src/connectors/uniswap/alpha-router.ts index e4111e6925..56836d6786 100644 --- a/src/connectors/uniswap/alpha-router.ts +++ b/src/connectors/uniswap/alpha-router.ts @@ -81,7 +81,12 @@ export class AlphaRouterService { logger.info(`[AlphaRouter] Recipient: ${options.recipient}`); logger.info(`[AlphaRouter] Slippage: ${options.slippageTolerance.toSignificant()}%`); - const swapRoute = await this.router.route(amount, tokenOut, tradeType, { + // For EXACT_INPUT: amount is the input, quoteCurrency is the output (what we're getting) + // For EXACT_OUTPUT: amount is the output, quoteCurrency is the input (what we're paying) + const quoteCurrency = tradeType === TradeType.EXACT_INPUT ? tokenOut : tokenIn; + logger.info(`[AlphaRouter] Quote currency: ${quoteCurrency.symbol} (${quoteCurrency.address})`); + + const swapRoute = await this.router.route(amount, quoteCurrency, tradeType, { type: SwapType.UNIVERSAL_ROUTER, version: UniversalRouterVersion.V2_0, slippageTolerance: options.slippageTolerance, From b7d6d459d6d21c37238a506cec57d98ca873b1b1 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Thu, 4 Dec 2025 06:51:43 -0800 Subject: [PATCH 10/14] fix: correct transaction status handling for pending Ethereum transactions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - handleTransactionExecution now returns null for pending transactions instead of a fake receipt with status:0 (which means reverted in ethers) - handleExecuteQuoteTransactionConfirmation now accepts txHash parameter to return the transaction hash for pending transactions - Fixed status code mapping to match Hummingbot's TransactionStatus: - CONFIRMED = 1 - PENDING = 0 - FAILED = -1 - Track txHash immediately after sending transaction in executeQuote Previously, pending transactions were incorrectly marked as failed because the fake receipt had status:0, which in Ethereum means the transaction reverted on-chain. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/chains/ethereum/ethereum.ts | 34 +++++++------------ .../uniswap/router-routes/executeQuote.ts | 15 +++++--- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/src/chains/ethereum/ethereum.ts b/src/chains/ethereum/ethereum.ts index e1916fb4c1..4b402a0a86 100644 --- a/src/chains/ethereum/ethereum.ts +++ b/src/chains/ethereum/ethereum.ts @@ -928,34 +928,23 @@ export class Ethereum { return ethers.utils.isAddress(address); } - public async handleTransactionExecution(tx: TransactionResponse): Promise { + public async handleTransactionExecution(tx: TransactionResponse): Promise { return await Promise.race([ tx.wait(1).then((receipt) => { - // Transaction confirmed + // Transaction confirmed (status: 1) or failed/reverted (status: 0) logger.info( `Transaction ${tx.hash} ${receipt.status === 1 ? 'confirmed' : 'failed'} in block ${receipt.blockNumber}`, ); return receipt; }), - new Promise((resolve) => + new Promise((resolve) => setTimeout(() => { - // Timeout reached, treat as pending + // Timeout reached, transaction is still pending + // Return null to indicate pending - the caller should check for null + // Note: Do NOT return a fake receipt with status: 0, because in Ethereum + // receipt.status === 0 means "reverted/failed", not "pending" logger.warn(`Transaction ${tx.hash} is still pending after timeout`); - resolve({ - transactionHash: tx.hash, - blockHash: '', - blockNumber: null, - transactionIndex: null, - from: tx.from, - to: tx.to || null, - cumulativeGasUsed: BigNumber.from(0), - gasUsed: BigNumber.from(0), - contractAddress: null, - logs: [], - logsBloom: '', - status: 0, // PENDING - effectiveGasPrice: BigNumber.from(0), - } as providers.TransactionReceipt); + resolve(null); }, this._transactionExecutionTimeoutMs), ), ]); @@ -979,6 +968,7 @@ export class Ethereum { expectedAmountIn: number, expectedAmountOut: number, side?: 'BUY' | 'SELL', + txHash?: string, // Optional tx hash for pending transactions ): { signature: string; status: number; @@ -996,8 +986,8 @@ export class Ethereum { // Transaction receipt not available - still pending logger.warn('Transaction pending, no receipt available yet'); return { - signature: '', - status: 0, // PENDING + signature: txHash || '', // Use provided txHash for pending transactions + status: 0, // PENDING (TransactionStatus.PENDING = 0) data: undefined, }; } @@ -1009,7 +999,7 @@ export class Ethereum { logger.error(`Transaction ${signature} failed on-chain`); return { signature, - status: -1, // FAILED + status: -1, // FAILED (TransactionStatus.FAILED = -1) data: { tokenIn: inputToken, tokenOut: outputToken, diff --git a/src/connectors/uniswap/router-routes/executeQuote.ts b/src/connectors/uniswap/router-routes/executeQuote.ts index 2cd0f87d17..4b14fb48a8 100644 --- a/src/connectors/uniswap/router-routes/executeQuote.ts +++ b/src/connectors/uniswap/router-routes/executeQuote.ts @@ -110,7 +110,8 @@ async function executeQuote(walletAddress: string, network: string, quoteId: str } // Execute the swap transaction - let txReceipt; + let txReceipt: Awaited> = null; + let txHash: string | undefined; try { if (isHardwareWallet) { @@ -139,6 +140,8 @@ async function executeQuote(walletAddress: string, network: string, quoteId: str // Send the signed transaction const txResponse = await ethereum.provider.sendTransaction(signedTx); + txHash = txResponse.hash; + logger.info(`Transaction sent: ${txHash}`); // Wait for confirmation with timeout txReceipt = await ethereum.handleTransactionExecution(txResponse); @@ -171,7 +174,8 @@ async function executeQuote(walletAddress: string, network: string, quoteId: str // Send transaction directly without relying on ethers' automatic gas estimation const txResponse = await wallet.sendTransaction(txData); - logger.info(`Transaction sent: ${txResponse.hash}`); + txHash = txResponse.hash; + logger.info(`Transaction sent: ${txHash}`); // Wait for transaction confirmation with timeout txReceipt = await ethereum.handleTransactionExecution(txResponse); @@ -262,6 +266,7 @@ async function executeQuote(walletAddress: string, network: string, quoteId: str const expectedAmountOut = parseFloat(quote.trade.outputAmount.toExact()); // Use the new handleExecuteQuoteTransactionConfirmation helper + // Pass txHash for pending transactions (when txReceipt is null) const result = ethereum.handleExecuteQuoteTransactionConfirmation( txReceipt, inputToken.address, @@ -269,10 +274,12 @@ async function executeQuote(walletAddress: string, network: string, quoteId: str expectedAmountIn, expectedAmountOut, side, + txHash, ); // Handle different transaction states - if (result.status === 0) { + // Status codes: 1 = CONFIRMED, 0 = PENDING, -1 = FAILED + if (result.status === -1) { // Transaction failed logger.error(`Transaction failed on-chain. Receipt: ${JSON.stringify(txReceipt)}`); throw httpErrors.internalServerError( @@ -280,7 +287,7 @@ async function executeQuote(walletAddress: string, network: string, quoteId: str ); } - if (result.status === -1) { + if (result.status === 0) { // Transaction is still pending logger.info(`Transaction ${result.signature || 'pending'} is still pending`); return result; From 3c39479bf4b17e89a46d927b486a87b9ff26d968 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Thu, 4 Dec 2025 15:19:23 -0800 Subject: [PATCH 11/14] refactor: reduce Uniswap router log verbosity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move detailed AlphaRouter and quoteSwap logs from info to debug level, keeping only essential messages at info for cleaner production logs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/connectors/uniswap/alpha-router.ts | 39 +++++++++---------- .../uniswap/router-routes/quoteSwap.ts | 35 +++++++---------- 2 files changed, 33 insertions(+), 41 deletions(-) diff --git a/src/connectors/uniswap/alpha-router.ts b/src/connectors/uniswap/alpha-router.ts index 56836d6786..209ec556c5 100644 --- a/src/connectors/uniswap/alpha-router.ts +++ b/src/connectors/uniswap/alpha-router.ts @@ -74,17 +74,19 @@ export class AlphaRouterService { recipient: string; }, ): Promise { - logger.info(`[AlphaRouter] Starting quote generation`); - logger.info(`[AlphaRouter] Input: ${amount.toExact()} ${tokenIn.symbol} (${tokenIn.address})`); - logger.info(`[AlphaRouter] Output: ${tokenOut.symbol} (${tokenOut.address})`); - logger.info(`[AlphaRouter] Trade type: ${tradeType === TradeType.EXACT_INPUT ? 'EXACT_INPUT' : 'EXACT_OUTPUT'}`); - logger.info(`[AlphaRouter] Recipient: ${options.recipient}`); - logger.info(`[AlphaRouter] Slippage: ${options.slippageTolerance.toSignificant()}%`); + const tradeTypeStr = tradeType === TradeType.EXACT_INPUT ? 'EXACT_INPUT' : 'EXACT_OUTPUT'; + logger.info( + `[AlphaRouter] Getting quote: ${amount.toExact()} ${tokenIn.symbol} -> ${tokenOut.symbol} (${tradeTypeStr})`, + ); + logger.debug(`[AlphaRouter] Input token: ${tokenIn.symbol} (${tokenIn.address})`); + logger.debug(`[AlphaRouter] Output token: ${tokenOut.symbol} (${tokenOut.address})`); + logger.debug(`[AlphaRouter] Recipient: ${options.recipient}`); + logger.debug(`[AlphaRouter] Slippage: ${options.slippageTolerance.toSignificant()}%`); // For EXACT_INPUT: amount is the input, quoteCurrency is the output (what we're getting) // For EXACT_OUTPUT: amount is the output, quoteCurrency is the input (what we're paying) const quoteCurrency = tradeType === TradeType.EXACT_INPUT ? tokenOut : tokenIn; - logger.info(`[AlphaRouter] Quote currency: ${quoteCurrency.symbol} (${quoteCurrency.address})`); + logger.debug(`[AlphaRouter] Quote currency: ${quoteCurrency.symbol} (${quoteCurrency.address})`); const swapRoute = await this.router.route(amount, quoteCurrency, tradeType, { type: SwapType.UNIVERSAL_ROUTER, @@ -98,10 +100,9 @@ export class AlphaRouterService { throw new Error(`No route found for ${tokenIn.symbol} -> ${tokenOut.symbol}`); } - logger.info(`[AlphaRouter] Route found!`); - logger.info(`[AlphaRouter] Quote: ${swapRoute.quote.toExact()} ${swapRoute.quote.currency.symbol}`); - logger.info(`[AlphaRouter] Gas estimate: ${swapRoute.estimatedGasUsed.toString()}`); - logger.info(`[AlphaRouter] Gas estimate USD: $${swapRoute.estimatedGasUsedUSD.toExact()}`); + logger.debug(`[AlphaRouter] Quote: ${swapRoute.quote.toExact()} ${swapRoute.quote.currency.symbol}`); + logger.debug(`[AlphaRouter] Gas estimate: ${swapRoute.estimatedGasUsed.toString()}`); + logger.debug(`[AlphaRouter] Gas estimate USD: $${swapRoute.estimatedGasUsedUSD.toExact()}`); // Log route details (split routing info) const routeStrings: string[] = []; @@ -109,7 +110,7 @@ export class AlphaRouterService { const routeStr = route.tokenPath.map((t) => t.symbol).join(' -> '); const percent = route.percent; routeStrings.push(`${percent}% via ${routeStr}`); - logger.info(`[AlphaRouter] Route: ${percent}% via ${routeStr}`); + logger.debug(`[AlphaRouter] Route: ${percent}% via ${routeStr}`); } // Extract method parameters for Universal Router execution @@ -120,9 +121,9 @@ export class AlphaRouterService { value: swapRoute.methodParameters.value, to: swapRoute.methodParameters.to, }; - logger.info(`[AlphaRouter] Calldata length: ${methodParameters.calldata.length}`); - logger.info(`[AlphaRouter] Value: ${methodParameters.value}`); - logger.info(`[AlphaRouter] To: ${methodParameters.to}`); + logger.debug(`[AlphaRouter] Calldata length: ${methodParameters.calldata.length}`); + logger.debug(`[AlphaRouter] Value: ${methodParameters.value}`); + logger.debug(`[AlphaRouter] To: ${methodParameters.to}`); } const result: AlphaRouterQuoteResult = { @@ -136,11 +137,9 @@ export class AlphaRouterService { methodParameters, }; - logger.info(`[AlphaRouter] Quote generation complete`); - logger.info(`[AlphaRouter] Input: ${result.inputAmount} ${tokenIn.symbol}`); - logger.info(`[AlphaRouter] Output: ${result.outputAmount} ${tokenOut.symbol}`); - logger.info(`[AlphaRouter] Price Impact: ${result.priceImpact}%`); - logger.info(`[AlphaRouter] Routes: ${result.routeString}`); + logger.info( + `[AlphaRouter] Quote: ${result.inputAmount} ${tokenIn.symbol} -> ${result.outputAmount} ${tokenOut.symbol} (impact: ${result.priceImpact}%, route: ${result.routeString})`, + ); return result; } diff --git a/src/connectors/uniswap/router-routes/quoteSwap.ts b/src/connectors/uniswap/router-routes/quoteSwap.ts index 33bdeefd4f..32693a64d0 100644 --- a/src/connectors/uniswap/router-routes/quoteSwap.ts +++ b/src/connectors/uniswap/router-routes/quoteSwap.ts @@ -22,10 +22,8 @@ async function quoteSwap( side: 'BUY' | 'SELL', slippagePct: number = UniswapConfig.config.slippagePct, ): Promise> { - logger.info(`[quoteSwap] Starting quote generation using AlphaRouter`); - logger.info(`[quoteSwap] Network: ${network}, Wallet: ${walletAddress || 'not provided'}`); - logger.info(`[quoteSwap] Base: ${baseToken}, Quote: ${quoteToken}`); - logger.info(`[quoteSwap] Amount: ${amount}, Side: ${side}, Slippage: ${slippagePct}%`); + logger.info(`[quoteSwap] ${baseToken}/${quoteToken} ${side} ${amount} on ${network}`); + logger.debug(`[quoteSwap] Wallet: ${walletAddress || 'not provided'}, Slippage: ${slippagePct}%`); const ethereum = await Ethereum.getInstance(network); const uniswap = await Uniswap.getInstance(network); @@ -39,8 +37,8 @@ async function quoteSwap( throw httpErrors.notFound(sanitizeErrorMessage('Token not found: {}', !baseTokenInfo ? baseToken : quoteToken)); } - logger.info(`[quoteSwap] Base token: ${baseTokenInfo.symbol} (${baseTokenInfo.address})`); - logger.info(`[quoteSwap] Quote token: ${quoteTokenInfo.symbol} (${quoteTokenInfo.address})`); + logger.debug(`[quoteSwap] Base token: ${baseTokenInfo.symbol} (${baseTokenInfo.address})`); + logger.debug(`[quoteSwap] Quote token: ${quoteTokenInfo.symbol} (${quoteTokenInfo.address})`); // Convert to Uniswap SDK Token objects const baseTokenObj = uniswap.getUniswapToken(baseTokenInfo); @@ -50,31 +48,26 @@ async function quoteSwap( const exactIn = side === 'SELL'; const [inputToken, outputToken] = exactIn ? [baseTokenObj, quoteTokenObj] : [quoteTokenObj, baseTokenObj]; - logger.info(`[quoteSwap] Input token: ${inputToken.symbol} (${inputToken.address})`); - logger.info(`[quoteSwap] Output token: ${outputToken.symbol} (${outputToken.address})`); - logger.info(`[quoteSwap] Exact in: ${exactIn}`); + logger.debug(`[quoteSwap] Input: ${inputToken.symbol}, Output: ${outputToken.symbol}, Exact in: ${exactIn}`); // Get quote from AlphaRouter (smart order router with split routing) // Use a placeholder address for quotes when no wallet is provided const recipient = walletAddress || '0x0000000000000000000000000000000000000001'; - logger.info(`[quoteSwap] Calling getAlphaRouterQuote with slippage ${slippagePct}%...`); const quoteResult = await uniswap.getAlphaRouterQuote(inputToken, outputToken, amount, side, recipient, slippagePct); - logger.info(`[quoteSwap] Quote result received`); // Generate unique quote ID const quoteId = uuidv4(); - logger.info(`[quoteSwap] Generated quote ID: ${quoteId}`); // Extract route information from AlphaRouter result const routePath = quoteResult.routeString; - logger.info(`[quoteSwap] Route path: ${routePath}`); // Get amounts from AlphaRouter result const estimatedAmountIn = parseFloat(quoteResult.inputAmount); const estimatedAmountOut = parseFloat(quoteResult.outputAmount); - logger.info(`[quoteSwap] Estimated amounts - In: ${estimatedAmountIn}, Out: ${estimatedAmountOut}`); - logger.info(`[quoteSwap] Gas estimate: ${quoteResult.gasEstimate} (${quoteResult.gasEstimateUSD} USD)`); + logger.debug( + `[quoteSwap] Quote ${quoteId}: ${estimatedAmountIn} -> ${estimatedAmountOut}, gas: ${quoteResult.gasEstimate}`, + ); const minAmountOut = side === 'SELL' ? estimatedAmountOut * (1 - slippagePct / 100) : estimatedAmountOut; const maxAmountIn = side === 'BUY' ? estimatedAmountIn * (1 + slippagePct / 100) : estimatedAmountIn; @@ -84,7 +77,7 @@ async function quoteSwap( side === 'SELL' ? estimatedAmountOut / estimatedAmountIn // SELL: USDC per HBOT : estimatedAmountIn / estimatedAmountOut; // BUY: USDC per HBOT - logger.info(`[quoteSwap] Price: ${price}, Min out: ${minAmountOut}, Max in: ${maxAmountIn}`); + logger.debug(`[quoteSwap] Price: ${price}, Min out: ${minAmountOut}, Max in: ${maxAmountIn}`); // Cache the quote for execution // Store both quote and request data in the quote object for Uniswap @@ -111,13 +104,13 @@ async function quoteSwap( quoteCache.set(quoteId, cachedQuote); logger.info( - `[quoteSwap] Cached quote ${quoteId}: ${estimatedAmountIn} ${inputToken.symbol} -> ${estimatedAmountOut} ${outputToken.symbol}`, + `[quoteSwap] Quote ${quoteId}: ${estimatedAmountIn} ${inputToken.symbol} -> ${estimatedAmountOut} ${outputToken.symbol}`, ); - logger.info(`[quoteSwap] Method parameters available: ${!!quoteResult.methodParameters}`); + logger.debug(`[quoteSwap] Method parameters available: ${!!quoteResult.methodParameters}`); if (quoteResult.methodParameters) { - logger.info(`[quoteSwap] Calldata length: ${quoteResult.methodParameters.calldata.length}`); - logger.info(`[quoteSwap] Value: ${quoteResult.methodParameters.value}`); - logger.info(`[quoteSwap] To: ${quoteResult.methodParameters.to}`); + logger.debug( + `[quoteSwap] Calldata length: ${quoteResult.methodParameters.calldata.length}, To: ${quoteResult.methodParameters.to}`, + ); } return { From f4e465cbcca8507741534c6ab227752ba02d6838 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 23 Dec 2025 11:40:28 -0800 Subject: [PATCH 12/14] fix: add multi-hop routing support for PancakeSwap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add intermediate token lists for each network (BSC, mainnet, arbitrum, base) - Refactor pool discovery to search for pools through intermediate tokens - Enable multi-hop routes like LINK -> WBNB -> DAI - maximumHops and maximumSplits config options now work correctly - Fixes "No routes found" error for token pairs without direct pools 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../pancakeswap/universal-router.ts | 214 ++++++++++++------ 1 file changed, 139 insertions(+), 75 deletions(-) diff --git a/src/connectors/pancakeswap/universal-router.ts b/src/connectors/pancakeswap/universal-router.ts index 2a1d154b4d..4d6e7c19e2 100644 --- a/src/connectors/pancakeswap/universal-router.ts +++ b/src/connectors/pancakeswap/universal-router.ts @@ -10,17 +10,9 @@ import { SwapRouter, TradeConfig, } from '@pancakeswap/smart-router'; -import { Pair as V2Pair, Route as V2Route, Trade as V2Trade, computePairAddress } from '@pancakeswap/v2-sdk'; +import { Pair as V2Pair, computePairAddress } from '@pancakeswap/v2-sdk'; import IPancakeswapV3Pool from '@pancakeswap/v3-core/artifacts/contracts/PancakeV3Pool.sol/PancakeV3Pool.json'; -import { - Pool as V3Pool, - Route as V3Route, - Trade as V3Trade, - FeeAmount, - computePoolAddress, - nearestUsableTick, - TICK_SPACINGS, -} from '@pancakeswap/v3-sdk'; +import { Pool as V3Pool, FeeAmount, computePoolAddress, nearestUsableTick, TICK_SPACINGS } from '@pancakeswap/v3-sdk'; import { BigNumber, Contract } from 'ethers'; import { Address } from 'viem'; @@ -36,6 +28,36 @@ import { // Common fee tiers for V3 const V3_FEE_TIERS = [FeeAmount.LOWEST, FeeAmount.LOW, FeeAmount.MEDIUM, FeeAmount.HIGH]; +// Common intermediate/base tokens for multi-hop routing per network +// These are the most liquid tokens that are commonly used as routing intermediates +const INTERMEDIATE_TOKENS: { [network: string]: { symbol: string; address: string; decimals: number }[] } = { + bsc: [ + { symbol: 'WBNB', address: '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c', decimals: 18 }, + { symbol: 'USDT', address: '0x55d398326f99059fF775485246999027B3197955', decimals: 18 }, + { symbol: 'USDC', address: '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', decimals: 18 }, + { symbol: 'BUSD', address: '0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56', decimals: 18 }, + { symbol: 'DAI', address: '0x1AF3F329e8BE154074D8769D1FFa4eE058B1DBc3', decimals: 18 }, + ], + mainnet: [ + { symbol: 'WETH', address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', decimals: 18 }, + { symbol: 'USDT', address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', decimals: 6 }, + { symbol: 'USDC', address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', decimals: 6 }, + { symbol: 'DAI', address: '0x6B175474E89094C44Da98b954EesdeC80D8D8Ac', decimals: 18 }, + { symbol: 'WBTC', address: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', decimals: 8 }, + ], + arbitrum: [ + { symbol: 'WETH', address: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', decimals: 18 }, + { symbol: 'USDT', address: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', decimals: 6 }, + { symbol: 'USDC', address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', decimals: 6 }, + { symbol: 'DAI', address: '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1', decimals: 18 }, + ], + base: [ + { symbol: 'WETH', address: '0x4200000000000000000000000000000000000006', decimals: 18 }, + { symbol: 'USDC', address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', decimals: 6 }, + { symbol: 'DAI', address: '0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb', decimals: 18 }, + ], +}; + export interface UniversalRouterQuoteResult { trade: SmartRouterTrade; route: string[]; @@ -105,53 +127,73 @@ export class UniversalRouterService { const protocols = options.protocols || [PoolType.V2, PoolType.V3]; logger.info(`[UniversalRouter] Protocols to check: ${protocols.join(', ')}`); - const allPools = []; + const allPools: Pool[] = []; + const poolAddresses = new Set(); // Track added pools to avoid duplicates + + // Get intermediate tokens for this network + const intermediateTokens = this.getIntermediateTokens(tokenIn, tokenOut); + logger.info( + `[UniversalRouter] Using ${intermediateTokens.length} intermediate tokens for multi-hop routing: ${intermediateTokens.map((t) => t.symbol).join(', ')}`, + ); - // Try to find routes through each protocol + // Collect all token pairs to search for pools + // Direct pair + pairs through intermediate tokens + const tokenPairs: [Token, Token][] = [[tokenIn, tokenOut]]; + + // Add pairs through each intermediate token for multi-hop routing + for (const intermediate of intermediateTokens) { + tokenPairs.push([tokenIn, intermediate]); + tokenPairs.push([intermediate, tokenOut]); + } + + logger.info(`[UniversalRouter] Searching ${tokenPairs.length} token pairs for pools...`); + + // Search for V3 pools if (protocols.includes(PoolType.V3)) { - logger.info(`[UniversalRouter] Searching for V3 routes...`); - try { - const v3Trade = await this.findV3Route(tokenIn, tokenOut, amount, tradeType); - if (v3Trade) { - logger.info( - `[UniversalRouter] Found V3 route: ${v3Trade.inputAmount.toExact()} -> ${v3Trade.outputAmount.toExact()}`, - ); - for (const swap of v3Trade.swaps) { - for (const pool of swap.route.pools as unknown as Pool[]) { - pool.type = PoolType.V3; - allPools.push(pool); + logger.info(`[UniversalRouter] Searching for V3 pools...`); + for (const [tokenA, tokenB] of tokenPairs) { + try { + const pool = await this.findV3Pool(tokenA, tokenB); + if (pool) { + const poolKey = `v3-${tokenA.address}-${tokenB.address}`; + if (!poolAddresses.has(poolKey)) { + poolAddresses.add(poolKey); + (pool as unknown as Pool).type = PoolType.V3; + allPools.push(pool as unknown as Pool); + logger.info(`[UniversalRouter] Found V3 pool: ${tokenA.symbol} <-> ${tokenB.symbol}`); } } - } else { - logger.info(`[UniversalRouter] No V3 route found`); + } catch (error) { + // Pool doesn't exist, continue } - } catch (error) { - logger.warn(`[UniversalRouter] Failed to find V3 route: ${error.message}`); } } + // Search for V2 pools if (protocols.includes(PoolType.V2)) { - logger.info(`[UniversalRouter] Searching for V2 routes...`); - try { - const v2Trade = await this.findV2Route(tokenIn, tokenOut, amount, tradeType); - if (v2Trade) { - logger.info( - `[UniversalRouter] Found V2 route: ${v2Trade.inputAmount.toExact()} -> ${v2Trade.outputAmount.toExact()}`, - ); - for (const pair of v2Trade.route.pairs as unknown as Pool[]) { - pair.type = PoolType.V2; - allPools.push(pair); + logger.info(`[UniversalRouter] Searching for V2 pools...`); + for (const [tokenA, tokenB] of tokenPairs) { + try { + const pair = await this.findV2Pool(tokenA, tokenB); + if (pair) { + const poolKey = `v2-${tokenA.address}-${tokenB.address}`; + if (!poolAddresses.has(poolKey)) { + poolAddresses.add(poolKey); + (pair as unknown as Pool).type = PoolType.V2; + allPools.push(pair as unknown as Pool); + logger.info(`[UniversalRouter] Found V2 pool: ${tokenA.symbol} <-> ${tokenB.symbol}`); + } } - } else { - logger.info(`[UniversalRouter] No V2 route found`); + } catch (error) { + // Pool doesn't exist, continue } - } catch (error) { - logger.warn(`[UniversalRouter] Failed to find V2 route: ${error.message}`); } } + logger.info(`[UniversalRouter] Total pools found: ${allPools.length}`); + if (allPools.length === 0) { - logger.error(`[UniversalRouter] No routes found for ${tokenIn.symbol} -> ${tokenOut.symbol}`); + logger.error(`[UniversalRouter] No pools found for ${tokenIn.symbol} -> ${tokenOut.symbol}`); throw new Error(`No routes found for ${tokenIn.symbol} -> ${tokenOut.symbol}`); } @@ -240,22 +282,43 @@ export class UniversalRouterService { } /** - * Find V3 route using pool address computation + * Get intermediate tokens for multi-hop routing + * Excludes the input and output tokens from the list */ - private async findV3Route( - tokenIn: Token, - tokenOut: Token, - amount: CurrencyAmount, - tradeType: TradeType, - ): Promise | null> { - // Try each fee tier + private getIntermediateTokens(tokenIn: Token, tokenOut: Token): Token[] { + const intermediates = INTERMEDIATE_TOKENS[this.network] || []; + const result: Token[] = []; + + for (const intermediate of intermediates) { + // Skip if this is the input or output token + if ( + intermediate.address.toLowerCase() === tokenIn.address.toLowerCase() || + intermediate.address.toLowerCase() === tokenOut.address.toLowerCase() + ) { + continue; + } + + result.push(new Token(this.chainId, intermediate.address as Address, intermediate.decimals, intermediate.symbol)); + } + + return result; + } + + /** + * Find a V3 pool for a token pair (returns the pool with best liquidity) + */ + private async findV3Pool(tokenA: Token, tokenB: Token): Promise { + let bestPool: V3Pool | null = null; + let bestLiquidity = BigNumber.from(0); + + // Try each fee tier and find the one with most liquidity for (const fee of V3_FEE_TIERS) { try { // Compute pool address const poolAddress = computePoolAddress({ deployerAddress: getPancakeswapV3PoolDeployerAddress(this.network), - tokenA: tokenIn, - tokenB: tokenOut, + tokenA, + tokenB, fee, }); @@ -289,54 +352,55 @@ export class UniversalRouterService { } // Create pool instance with tick data - const pool = new V3Pool(tokenIn, tokenOut, fee, sqrtPriceX96.toString(), liquidity.toString(), tick, ticks); + const pool = new V3Pool(tokenA, tokenB, fee, sqrtPriceX96.toString(), liquidity.toString(), tick, ticks); - // Create route and trade - const route = new V3Route([pool], tokenIn, tokenOut); - - return tradeType === TradeType.EXACT_INPUT ? V3Trade.exactIn(route, amount) : V3Trade.exactOut(route, amount); + // Keep track of pool with best liquidity + if (liquidity.gt(bestLiquidity)) { + bestLiquidity = liquidity; + bestPool = pool; + } } catch (error) { // Pool doesn't exist or other error, continue to next fee tier continue; } } - return null; + return bestPool; } /** - * Find V2 route for a token pair + * Find a V2 pool (pair) for a token pair */ - private async findV2Route( - tokenIn: Token, - tokenOut: Token, - amount: CurrencyAmount, - tradeType: TradeType, - ): Promise | null> { + private async findV2Pool(tokenA: Token, tokenB: Token): Promise { try { // Compute pair address const pairAddress = computePairAddress({ factoryAddress: getPancakeswapV2FactoryAddress(this.network), - tokenA: tokenIn, - tokenB: tokenOut, + tokenA, + tokenB, }); const pairContract = new Contract(pairAddress, IPancakeswapV2PairABI.abi, this.provider); const reserves = await pairContract.getReserves(); - const token0 = await pairContract.token0(); + const token0Address = await pairContract.token0(); const [reserve0, reserve1] = reserves; - const [reserveIn, reserveOut] = - tokenIn.address.toLowerCase() === token0.toLowerCase() ? [reserve0, reserve1] : [reserve1, reserve0]; + + // Check if pool has liquidity + if (reserve0.eq(0) || reserve1.eq(0)) { + return null; + } + + // Determine which token is token0 and which is token1 + const isTokenAToken0 = tokenA.address.toLowerCase() === token0Address.toLowerCase(); + const [tokenAReserve, tokenBReserve] = isTokenAToken0 ? [reserve0, reserve1] : [reserve1, reserve0]; const pair = new V2Pair( - CurrencyAmount.fromRawAmount(tokenIn, reserveIn.toString()), - CurrencyAmount.fromRawAmount(tokenOut, reserveOut.toString()), + CurrencyAmount.fromRawAmount(tokenA, tokenAReserve.toString()), + CurrencyAmount.fromRawAmount(tokenB, tokenBReserve.toString()), ); - const route = new V2Route([pair], tokenIn, tokenOut); - - return new V2Trade(route, amount, tradeType); + return pair; } catch (error) { return null; } From 530e7f15421eda5b1ed92fa450597b0862fa033d Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 30 Dec 2025 12:31:12 -0800 Subject: [PATCH 13/14] fix: display full route path with intermediate tokens in routePath MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix extractRoutePath to use route.path array which contains all tokens including intermediates (e.g., LINK -> WBNB -> DAI) - Add percentage display for split routes (e.g., "50% via LINK -> WBNB -> DAI") - Change route join separator from ' -> ' to ', ' for multiple split routes - Add tests to verify routePath format includes percentage and token path Before: "LINK -> DAI -> LINK -> DAI -> LINK -> DAI" (wrong) After: "100% via LINK -> WBNB -> DAI" (correct) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../pancakeswap/universal-router.ts | 34 ++++++++++++++--- src/connectors/uniswap/universal-router.ts | 38 +++++++++++++++---- .../pancakeswap/universal-router.test.ts | 33 ++++++++++++++++ .../uniswap/universal-router.test.ts | 33 ++++++++++++++++ 4 files changed, 125 insertions(+), 13 deletions(-) diff --git a/src/connectors/pancakeswap/universal-router.ts b/src/connectors/pancakeswap/universal-router.ts index 4d6e7c19e2..12ffa337ad 100644 --- a/src/connectors/pancakeswap/universal-router.ts +++ b/src/connectors/pancakeswap/universal-router.ts @@ -242,7 +242,7 @@ export class UniversalRouterService { // Calculate route path const route = this.extractRoutePath(bestTrade); - const routePath = route.join(' -> '); + const routePath = route.join(', '); logger.info(`[UniversalRouter] Route path: ${routePath}`); // Skip gas estimation during quote phase - it will be done during execution @@ -408,16 +408,40 @@ export class UniversalRouterService { /** * Extract route path from a trade + * Returns an array of route descriptions with percentages and full token paths */ private extractRoutePath(trade: SmartRouterTrade): string[] { - const path: string[] = []; + const routeDescriptions: string[] = []; for (const route of trade.routes) { - path.push(route.inputAmount.currency.symbol || (route.inputAmount.currency as Token).address); - path.push(route.outputAmount.currency.symbol || (route.outputAmount.currency as Token).address); + // Get the full path of tokens from the route + // route.path contains all tokens including intermediates (e.g., [LINK, WBNB, DAI]) + const routeWithPath = route as unknown as { path?: Currency[]; percent?: number }; + + let pathSymbols: string[]; + if (routeWithPath.path && routeWithPath.path.length > 0) { + // Use the full path from the route + pathSymbols = routeWithPath.path.map((currency: Currency) => { + const token = currency as Token; + return token.symbol || token.address; + }); + } else { + // Fallback to input/output if path not available + pathSymbols = [ + route.inputAmount.currency.symbol || (route.inputAmount.currency as Token).address, + route.outputAmount.currency.symbol || (route.outputAmount.currency as Token).address, + ]; + } + + // Get the percentage for this route (from RouteWithoutQuote) + const percent = routeWithPath.percent || 100; + + // Format as "X% via TOKEN1 -> TOKEN2 -> TOKEN3" + const pathStr = pathSymbols.join(' -> '); + routeDescriptions.push(`${percent}% via ${pathStr}`); } - return path; + return routeDescriptions; } /** diff --git a/src/connectors/uniswap/universal-router.ts b/src/connectors/uniswap/universal-router.ts index 124f84c06d..3df6324101 100644 --- a/src/connectors/uniswap/universal-router.ts +++ b/src/connectors/uniswap/universal-router.ts @@ -185,7 +185,7 @@ export class UniversalRouterService { // Calculate route path const route = this.extractRoutePath(bestTrade); - const routePath = route.join(' -> '); + const routePath = route.join(', '); logger.info(`[UniversalRouter] Route path: ${routePath}`); // Skip gas estimation during quote phase - it will be done during execution @@ -329,19 +329,41 @@ export class UniversalRouterService { /** * Extract route path from a trade + * Returns an array of route descriptions with percentages and full token paths */ private extractRoutePath(trade: RouterTrade): string[] { - const path: string[] = []; + const routeDescriptions: string[] = []; + const totalInput = trade.inputAmount; + + for (const swap of trade.swaps) { + const route = swap.route; + + // Get the full path of tokens from the route + // route.path contains all tokens including intermediates (e.g., [LINK, WETH, DAI]) + let pathSymbols: string[]; + if (route.path && route.path.length > 0) { + // Use the full path from the route + pathSymbols = route.path.map((currency: Currency) => { + const token = currency as Token; + return token.symbol || token.address; + }); + } else { + // Fallback to input/output if path not available + pathSymbols = [ + route.input.symbol || (route.input as Token).address, + route.output.symbol || (route.output as Token).address, + ]; + } - if (trade.swaps.length > 0) { - const firstSwap = trade.swaps[0]; - const route = firstSwap.route; + // Calculate the percentage for this swap + const percent = Math.round((parseFloat(swap.inputAmount.toExact()) / parseFloat(totalInput.toExact())) * 100); - path.push(route.input.symbol || (route.input as Token).address); - path.push(route.output.symbol || (route.output as Token).address); + // Format as "X% via TOKEN1 -> TOKEN2 -> TOKEN3" + const pathStr = pathSymbols.join(' -> '); + routeDescriptions.push(`${percent}% via ${pathStr}`); } - return path; + return routeDescriptions; } /** diff --git a/test/connectors/pancakeswap/universal-router.test.ts b/test/connectors/pancakeswap/universal-router.test.ts index d4db2ea626..6d02dfbf96 100644 --- a/test/connectors/pancakeswap/universal-router.test.ts +++ b/test/connectors/pancakeswap/universal-router.test.ts @@ -142,5 +142,38 @@ describe('UniversalRouterService', () => { expect(quote.methodParameters).toHaveProperty('to'); expect(quote.methodParameters.to).toBe('0x13f4EA83D0bd40E75C8222255bc855a974568Dd4'); }); + + it('should format routePath with percentage and token symbols', async () => { + const amount = CurrencyAmount.fromRawAmount(WBNB, '1000000000000000000'); + const options = { + slippageTolerance: new Percent(1, 100), + deadline: Math.floor(Date.now() / 1000) + 1800, + recipient: '0x0000000000000000000000000000000000000001', + protocols: [PoolType.V2], + }; + + // Mock a simple V2 pair + const mockContract = { + getReserves: jest + .fn() + .mockResolvedValue([ + BigNumber.from('1000000000000000000000'), + BigNumber.from('3000000000000'), + BigNumber.from('1234567890'), + ]), + token0: jest.fn().mockResolvedValue(WBNB.address), + token1: jest.fn().mockResolvedValue(USDC.address), + }; + + (Contract as any).mockImplementation(() => mockContract); + + const quote = await universalRouter.getQuote(WBNB, USDC, amount, TradeType.EXACT_INPUT, options); + + // Verify routePath format includes percentage and "via" + // Format should be like "100% via WBNB -> USDC" or for multi-hop "100% via LINK -> WBNB -> DAI" + expect(quote.routePath).toMatch(/^\d+% via .+$/); + expect(quote.routePath).toContain('% via'); + expect(quote.routePath).toContain('->'); + }); }); }); diff --git a/test/connectors/uniswap/universal-router.test.ts b/test/connectors/uniswap/universal-router.test.ts index c15ca02159..dd8732d6ab 100644 --- a/test/connectors/uniswap/universal-router.test.ts +++ b/test/connectors/uniswap/universal-router.test.ts @@ -142,5 +142,38 @@ describe('UniversalRouterService', () => { expect(quote.methodParameters).toHaveProperty('to'); expect(quote.methodParameters.to).toBe('0x66a9893cc07d91d95644aedd05d03f95e1dba8af'); }); + + it('should format routePath with percentage and token symbols', async () => { + const amount = CurrencyAmount.fromRawAmount(WETH, '1000000000000000000'); + const options = { + slippageTolerance: new Percent(1, 100), + deadline: Math.floor(Date.now() / 1000) + 1800, + recipient: '0x0000000000000000000000000000000000000001', + protocols: [Protocol.V2], + }; + + // Mock a simple V2 pair + const mockContract = { + getReserves: jest + .fn() + .mockResolvedValue([ + BigNumber.from('1000000000000000000000'), + BigNumber.from('3000000000000'), + BigNumber.from('1234567890'), + ]), + token0: jest.fn().mockResolvedValue(WETH.address), + token1: jest.fn().mockResolvedValue(USDC.address), + }; + + (Contract as any).mockImplementation(() => mockContract); + + const quote = await universalRouter.getQuote(WETH, USDC, amount, TradeType.EXACT_INPUT, options); + + // Verify routePath format includes percentage and "via" + // Format should be like "100% via WETH -> USDC" or for multi-hop "100% via LINK -> WETH -> DAI" + expect(quote.routePath).toMatch(/^\d+% via .+$/); + expect(quote.routePath).toContain('% via'); + expect(quote.routePath).toContain('->'); + }); }); }); From ff590c3d9e8b4bb211285506b33ec4a09eb5f59c Mon Sep 17 00:00:00 2001 From: Ralph Comia Date: Fri, 9 Jan 2026 17:31:25 +0800 Subject: [PATCH 14/14] Revert "feat: add split routing in Uniswap and Pancakeswap quote-swap" --- package.json | 2 +- pnpm-lock.yaml | 151 ++++------ src/chains/ethereum/ethereum.ts | 34 ++- .../pancakeswap/pancakeswap.config.ts | 4 +- src/connectors/pancakeswap/pancakeswap.ts | 2 - .../pancakeswap/universal-router.ts | 258 ++++++------------ src/connectors/uniswap/alpha-router.ts | 146 ---------- .../uniswap/router-routes/executeQuote.ts | 15 +- .../uniswap/router-routes/quoteSwap.ts | 64 +++-- src/connectors/uniswap/uniswap.ts | 64 ----- src/connectors/uniswap/universal-router.ts | 38 +-- src/templates/connectors/pancakeswap.yml | 7 +- .../namespace/pancakeswap-schema.json | 6 +- .../pancakeswap/universal-router.test.ts | 33 --- .../universal-router-quoteSwap.test.ts | 55 ++-- .../uniswap/universal-router.test.ts | 33 --- 16 files changed, 242 insertions(+), 670 deletions(-) delete mode 100644 src/connectors/uniswap/alpha-router.ts diff --git a/package.json b/package.json index 2c1c38a3a2..307df19f6f 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "@uniswap/router-sdk": "^2.0.4", "@uniswap/sdk": "3.0.3", "@uniswap/sdk-core": "^5.9.0", - "@uniswap/smart-order-router": "^4.22.38", + "@uniswap/smart-order-router": "^3.59.0", "@uniswap/universal-router-sdk": "^4.19.6", "@uniswap/v2-sdk": "^4.15.2", "@uniswap/v3-core": "^1.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 87e684adf6..2521e4a7a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -152,8 +152,8 @@ importers: specifier: ^5.9.0 version: 5.9.0 '@uniswap/smart-order-router': - specifier: ^4.22.38 - version: 4.22.38(bufferutil@4.0.9)(encoding@0.1.13)(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))(jsbi@3.2.5)(utf-8-validate@5.0.10) + specifier: ^3.59.0 + version: 3.59.0(bufferutil@4.0.9)(encoding@0.1.13)(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))(jsbi@3.2.5)(utf-8-validate@5.0.10) '@uniswap/universal-router-sdk': specifier: ^4.19.6 version: 4.19.6(bufferutil@4.0.9)(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10) @@ -2699,12 +2699,12 @@ packages: '@uniswap/permit2-sdk@1.3.1': resolution: {integrity: sha512-Eq2by4zVEVSZL3PJ1Yuf5+AZ/yE1GOuksWzPXPoxr5WRm3hqh34jKEqtyTImHqwuPrdILG8i02xJmgGLTH1QfA==} + '@uniswap/router-sdk@1.23.0': + resolution: {integrity: sha512-KkHoMauTZh2N44sOU0ZuYseNNn9nAvaU57HwyCWjtwZdA7HaXtACfIRJbQvnkNNuALJfzHNkuv2aFyPSjNNmMw==} + '@uniswap/router-sdk@2.0.4': resolution: {integrity: sha512-MxCtD+g+2pzzd9rZ6HKTdv1ZK2mLjREoDRNAp9+F961zCCVhgJr9L1/6Hour27/xxCyljwmG83Zn1cSS054giw==} - '@uniswap/router-sdk@2.2.0': - resolution: {integrity: sha512-9xWepoISYXYyp9w2C1svegXsjqY0zO/qcheH1fizgHRJUJ3GQ5IbewOd9E6M0pTPlYOsIigOLIFXRCTDIbzu3w==} - '@uniswap/sdk-core@5.9.0': resolution: {integrity: sha512-OME7WR6+5QwQs45A2079r+/FS0zU944+JCQwUX9GyIriCxqw2pGu4F9IEqmlwD+zSIMml0+MJnJJ47pFgSyWDw==} engines: {node: '>=10'} @@ -2713,10 +2713,6 @@ packages: resolution: {integrity: sha512-0KqXw+y0opBo6eoPAEoLHEkNpOu0NG9gEk7GAYIGok+SHX89WlykWsRYeJKTg9tOwhLpcG9oHg8xZgQ390iOrA==} engines: {node: '>=10'} - '@uniswap/sdk-core@7.9.0': - resolution: {integrity: sha512-HHUFNK3LMi4KMQCAiHkdUyL62g/nrZLvNT44CY8RN4p8kWO6XYWzqdQt6OcjCsIbhMZ/Ifhe6Py5oOoccg/jUQ==} - engines: {node: '>=10'} - '@uniswap/sdk@3.0.3': resolution: {integrity: sha512-t4s8bvzaCFSiqD2qfXIm3rWhbdnXp+QjD3/mRaeVDHK7zWevs6RGEb1ohMiNgOCTZANvBayb4j8p+XFdnMBadQ==} engines: {node: '>=10'} @@ -2727,8 +2723,8 @@ packages: '@ethersproject/providers': ^5.0.0-beta '@ethersproject/solidity': ^5.0.0-beta - '@uniswap/smart-order-router@4.22.38': - resolution: {integrity: sha512-30l2ei0ZmdxOvwx5h0POT99R/GYxPrvTUb5sb+tiZ66Bv9w/AI8qL1YH0utTo9PGFFwu0FkLusTVAVdckN4rDw==} + '@uniswap/smart-order-router@3.59.0': + resolution: {integrity: sha512-1le8eLk/zK6meWs2ky2QnGiU1poqWgxCzl2KxlukyWetfdxSeKkHFcsMmV4nk2o7/R6duuFU47yUTjUX3HYhKw==} engines: {node: '>=10'} peerDependencies: jsbi: ^3.2.0 @@ -2741,24 +2737,24 @@ packages: resolution: {integrity: sha512-Hc3TfrFaupg0M84e/Zv7BoF+fmMWDV15mZ5s8ZQt2qZxUcNw2GQW+L6L/2k74who31G+p1m3GRYbJpAo7d1pqA==} engines: {node: '>=10'} - '@uniswap/universal-router-sdk@4.19.6': - resolution: {integrity: sha512-vBtHv4OzEn6Spkl1UgN/0TqO354w7RUdsE1uwAdqz2zfxhV48GOlKJWpe7LiI2ZukL/BMubLewtwC4q/RfjjJQ==} + '@uniswap/universal-router-sdk@3.4.0': + resolution: {integrity: sha512-EB/NLIkuT2BCdKnh2wcXT0cmINjRoiskjibFclpheALHL49XSrB08H4k7KV3BP6+JNKLeTHekvTDdsMd9rs5TA==} engines: {node: '>=14'} - '@uniswap/universal-router-sdk@4.23.0': - resolution: {integrity: sha512-lSWXMoH4fMGHG1s00mR0ivIuBgdW/mR/Y+CuIpxOSDxgwtP86/7JHPfPWcH7EVU5dstSIyzprUwZ/a8v7vlaGg==} + '@uniswap/universal-router-sdk@4.19.6': + resolution: {integrity: sha512-vBtHv4OzEn6Spkl1UgN/0TqO354w7RUdsE1uwAdqz2zfxhV48GOlKJWpe7LiI2ZukL/BMubLewtwC4q/RfjjJQ==} engines: {node: '>=14'} '@uniswap/universal-router@1.6.0': resolution: {integrity: sha512-Gt0b0rtMV1vSrgXY3vz5R1RCZENB+rOkbOidY9GvcXrK1MstSrQSOAc+FCr8FSgsDhmRAdft0lk5YUxtM9i9Lg==} engines: {node: '>=14'} - '@uniswap/universal-router@2.0.0-beta.2': - resolution: {integrity: sha512-/USVkWZrOCjLeZluR7Yk8SpfWDUKG/MLcOyuxuwnqM1xCJj5ekguSYhct+Yfo/3t9fsZcnL8vSYgz0MKqAomGg==} + '@uniswap/universal-router@2.0.0-beta.1': + resolution: {integrity: sha512-DdaMHaoDyJoCwpH+BiRKw/w2vjZtZS+ekpyrhmIeOBK1L2QEVFj977BNo6t24WzriZ9mSuIKF69RjHdXDUgHsQ==} engines: {node: '>=14'} - '@uniswap/universal-router@2.1.0': - resolution: {integrity: sha512-rt18RUsZd9xDfyVfIONJo+TEQ8w+olOYxu9+A1g4Thil1R7IMa+8mnyVQjdLPK2REhejScDwjYbOGpeaAce0hg==} + '@uniswap/universal-router@2.0.0-beta.2': + resolution: {integrity: sha512-/USVkWZrOCjLeZluR7Yk8SpfWDUKG/MLcOyuxuwnqM1xCJj5ekguSYhct+Yfo/3t9fsZcnL8vSYgz0MKqAomGg==} engines: {node: '>=14'} '@uniswap/v2-core@1.0.1': @@ -2769,10 +2765,6 @@ packages: resolution: {integrity: sha512-EtROgWTdhHzw4EUj7SdK9wjppOG7psJ16c656cRuv69nWbD9QyDL2shVcQccEiY7ak9WlJ+bIv/VldybXYBDuw==} engines: {node: '>=10'} - '@uniswap/v2-sdk@4.16.0': - resolution: {integrity: sha512-USMm2qz1xhEX8R0dhd0mHzf6pz5aCLjbtud1ZyUBk+gshhUCFp6NW9UovH0L5hqrH03rTvmqQdfhHMW5m+Sosg==} - engines: {node: '>=10'} - '@uniswap/v3-core@1.0.0': resolution: {integrity: sha512-kSC4djMGKMHj7sLMYVnn61k9nu+lHjMIxgg9CDQT+s2QYLoA56GbSK9Oxr+qJXzzygbkrmuY6cwgP6cW2JXPFA==} engines: {node: '>=10'} @@ -2789,10 +2781,6 @@ packages: resolution: {integrity: sha512-0oiyJNGjUVbc958uZmAr+m4XBCjV7PfMs/OUeBv+XDl33MEYF/eH86oBhvqGDM8S/cYaK55tCXzoWkmRUByrHg==} engines: {node: '>=10'} - '@uniswap/v3-sdk@3.26.0': - resolution: {integrity: sha512-bcoWNE7ntNNTHMOnDPscIqtIN67fUyrbBKr6eswI2gD2wm5b0YYFBDeh+Qc5Q3117o9i8S7QdftqrU8YSMQUfQ==} - engines: {node: '>=10'} - '@uniswap/v3-staker@1.0.0': resolution: {integrity: sha512-JV0Qc46Px5alvg6YWd+UIaGH9lDuYG/Js7ngxPit1SPaIP30AlVer1UYB7BRYeUVVxE+byUyIeN5jeQ7LLDjIw==} engines: {node: '>=10'} @@ -2802,10 +2790,6 @@ packages: resolution: {integrity: sha512-so3c/CmaRmRSvgKFyrUWy6DCSogyzyVaoYCec/TJ4k2hXlJ8MK4vumcuxtmRr1oMnZ5KmaCPBS12Knb4FC3nsw==} engines: {node: '>=14'} - '@uniswap/v4-sdk@1.23.0': - resolution: {integrity: sha512-WpnkNacNTe/qL4kj3DVC2nHaivUeuzYsWIvon+olAWYZyy+Frsnzfon/ZlznDifMPoV+im+MqYFsNQke4Vz3LA==} - engines: {node: '>=14'} - '@unrs/resolver-binding-android-arm-eabi@1.9.2': resolution: {integrity: sha512-tS+lqTU3N0kkthU+rYp0spAYq15DU8ld9kXkaKg9sbQqJNF+WPMuNHZQGCgdxrUOEO0j22RKMwRVhF1HTl+X8A==} cpu: [arm] @@ -5279,6 +5263,10 @@ packages: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true @@ -11321,7 +11309,7 @@ snapshots: - bufferutil - utf-8-validate - '@uniswap/router-sdk@2.0.4(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))': + '@uniswap/router-sdk@1.23.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))': dependencies: '@ethersproject/abi': 5.8.0 '@uniswap/sdk-core': 7.7.2 @@ -11332,14 +11320,14 @@ snapshots: transitivePeerDependencies: - hardhat - '@uniswap/router-sdk@2.2.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))': + '@uniswap/router-sdk@2.0.4(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))': dependencies: '@ethersproject/abi': 5.8.0 - '@uniswap/sdk-core': 7.9.0 + '@uniswap/sdk-core': 7.7.2 '@uniswap/swap-router-contracts': 1.3.1(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@uniswap/v2-sdk': 4.16.0 - '@uniswap/v3-sdk': 3.26.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@uniswap/v4-sdk': 1.23.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@uniswap/v2-sdk': 4.15.2 + '@uniswap/v3-sdk': 3.25.2(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@uniswap/v4-sdk': 1.21.4(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) transitivePeerDependencies: - hardhat @@ -11367,18 +11355,6 @@ snapshots: tiny-invariant: 1.3.3 toformat: 2.0.0 - '@uniswap/sdk-core@7.9.0': - dependencies: - '@ethersproject/address': 5.7.0 - '@ethersproject/bytes': 5.8.0 - '@ethersproject/keccak256': 5.7.0 - '@ethersproject/strings': 5.7.0 - big.js: 5.2.2 - decimal.js-light: 2.5.1 - jsbi: 3.2.5 - tiny-invariant: 1.3.3 - toformat: 2.0.0 - '@uniswap/sdk@3.0.3(@ethersproject/address@5.7.0)(@ethersproject/contracts@5.7.0)(@ethersproject/networks@5.7.0)(@ethersproject/providers@5.7.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@ethersproject/solidity@5.7.0)': dependencies: '@ethersproject/address': 5.7.0 @@ -11394,21 +11370,21 @@ snapshots: tiny-warning: 1.0.3 toformat: 2.0.0 - '@uniswap/smart-order-router@4.22.38(bufferutil@4.0.9)(encoding@0.1.13)(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))(jsbi@3.2.5)(utf-8-validate@5.0.10)': + '@uniswap/smart-order-router@3.59.0(bufferutil@4.0.9)(encoding@0.1.13)(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))(jsbi@3.2.5)(utf-8-validate@5.0.10)': dependencies: '@eth-optimism/sdk': 3.3.3(bufferutil@4.0.9)(encoding@0.1.13)(ethers@5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10) '@types/brotli': 1.3.4 '@uniswap/default-token-list': 11.19.0 '@uniswap/permit2-sdk': 1.3.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@uniswap/router-sdk': 2.2.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@uniswap/sdk-core': 7.9.0 + '@uniswap/router-sdk': 1.23.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@uniswap/sdk-core': 5.9.0 '@uniswap/swap-router-contracts': 1.3.1(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) '@uniswap/token-lists': 1.0.0-beta.34 '@uniswap/universal-router': 1.6.0 - '@uniswap/universal-router-sdk': 4.23.0(bufferutil@4.0.9)(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10) - '@uniswap/v2-sdk': 4.16.0 - '@uniswap/v3-sdk': 3.26.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@uniswap/v4-sdk': 1.23.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@uniswap/universal-router-sdk': 3.4.0(bufferutil@4.0.9)(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10) + '@uniswap/v2-sdk': 4.15.2 + '@uniswap/v3-sdk': 3.25.2(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@uniswap/v4-sdk': 1.21.4(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) async-retry: 1.3.3 await-timeout: 1.1.1 axios: 1.12.0 @@ -11443,13 +11419,13 @@ snapshots: '@uniswap/token-lists@1.0.0-beta.34': {} - '@uniswap/universal-router-sdk@4.19.6(bufferutil@4.0.9)(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10)': + '@uniswap/universal-router-sdk@3.4.0(bufferutil@4.0.9)(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10)': dependencies: '@openzeppelin/contracts': 4.9.6 '@uniswap/permit2-sdk': 1.3.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@uniswap/router-sdk': 2.0.4(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@uniswap/sdk-core': 7.7.2 - '@uniswap/universal-router': 2.0.0-beta.2 + '@uniswap/router-sdk': 1.23.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@uniswap/sdk-core': 5.9.0 + '@uniswap/universal-router': 2.0.0-beta.1 '@uniswap/v2-core': 1.0.1 '@uniswap/v2-sdk': 4.15.2 '@uniswap/v3-core': 1.0.0 @@ -11462,18 +11438,18 @@ snapshots: - hardhat - utf-8-validate - '@uniswap/universal-router-sdk@4.23.0(bufferutil@4.0.9)(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10)': + '@uniswap/universal-router-sdk@4.19.6(bufferutil@4.0.9)(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10)': dependencies: '@openzeppelin/contracts': 4.9.6 '@uniswap/permit2-sdk': 1.3.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@uniswap/router-sdk': 2.2.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@uniswap/sdk-core': 7.9.0 - '@uniswap/universal-router': 2.1.0 + '@uniswap/router-sdk': 2.0.4(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@uniswap/sdk-core': 7.7.2 + '@uniswap/universal-router': 2.0.0-beta.2 '@uniswap/v2-core': 1.0.1 - '@uniswap/v2-sdk': 4.16.0 + '@uniswap/v2-sdk': 4.15.2 '@uniswap/v3-core': 1.0.0 - '@uniswap/v3-sdk': 3.26.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@uniswap/v4-sdk': 1.23.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@uniswap/v3-sdk': 3.25.2(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@uniswap/v4-sdk': 1.21.4(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) bignumber.js: 9.3.0 ethers: 5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) transitivePeerDependencies: @@ -11487,13 +11463,13 @@ snapshots: '@uniswap/v2-core': 1.0.1 '@uniswap/v3-core': 1.0.0 - '@uniswap/universal-router@2.0.0-beta.2': + '@uniswap/universal-router@2.0.0-beta.1': dependencies: '@openzeppelin/contracts': 4.9.6 '@uniswap/v2-core': 1.0.1 '@uniswap/v3-core': 1.0.0 - '@uniswap/universal-router@2.1.0': + '@uniswap/universal-router@2.0.0-beta.2': dependencies: '@openzeppelin/contracts': 4.9.6 '@uniswap/v2-core': 1.0.1 @@ -11509,14 +11485,6 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@uniswap/v2-sdk@4.16.0': - dependencies: - '@ethersproject/address': 5.7.0 - '@ethersproject/solidity': 5.7.0 - '@uniswap/sdk-core': 7.9.0 - tiny-invariant: 1.3.3 - tiny-warning: 1.0.3 - '@uniswap/v3-core@1.0.0': {} '@uniswap/v3-core@1.0.1': {} @@ -11542,19 +11510,6 @@ snapshots: transitivePeerDependencies: - hardhat - '@uniswap/v3-sdk@3.26.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))': - dependencies: - '@ethersproject/abi': 5.8.0 - '@ethersproject/solidity': 5.7.0 - '@uniswap/sdk-core': 7.9.0 - '@uniswap/swap-router-contracts': 1.3.1(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@uniswap/v3-periphery': 1.4.4 - '@uniswap/v3-staker': 1.0.0 - tiny-invariant: 1.3.3 - tiny-warning: 1.0.3 - transitivePeerDependencies: - - hardhat - '@uniswap/v3-staker@1.0.0': dependencies: '@openzeppelin/contracts': 4.9.6 @@ -11571,16 +11526,6 @@ snapshots: transitivePeerDependencies: - hardhat - '@uniswap/v4-sdk@1.23.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))': - dependencies: - '@ethersproject/solidity': 5.7.0 - '@uniswap/sdk-core': 7.9.0 - '@uniswap/v3-sdk': 3.26.0(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) - tiny-invariant: 1.3.3 - tiny-warning: 1.0.3 - transitivePeerDependencies: - - hardhat - '@unrs/resolver-binding-android-arm-eabi@1.9.2': optional: true @@ -14657,6 +14602,10 @@ snapshots: argparse: 1.0.10 esprima: 4.0.1 + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -15002,7 +14951,7 @@ snapshots: find-up: 5.0.0 glob: 8.1.0 he: 1.2.0 - js-yaml: 4.1.1 + js-yaml: 4.1.0 log-symbols: 4.1.0 minimatch: 5.1.6 ms: 2.1.3 diff --git a/src/chains/ethereum/ethereum.ts b/src/chains/ethereum/ethereum.ts index 4b402a0a86..e1916fb4c1 100644 --- a/src/chains/ethereum/ethereum.ts +++ b/src/chains/ethereum/ethereum.ts @@ -928,23 +928,34 @@ export class Ethereum { return ethers.utils.isAddress(address); } - public async handleTransactionExecution(tx: TransactionResponse): Promise { + public async handleTransactionExecution(tx: TransactionResponse): Promise { return await Promise.race([ tx.wait(1).then((receipt) => { - // Transaction confirmed (status: 1) or failed/reverted (status: 0) + // Transaction confirmed logger.info( `Transaction ${tx.hash} ${receipt.status === 1 ? 'confirmed' : 'failed'} in block ${receipt.blockNumber}`, ); return receipt; }), - new Promise((resolve) => + new Promise((resolve) => setTimeout(() => { - // Timeout reached, transaction is still pending - // Return null to indicate pending - the caller should check for null - // Note: Do NOT return a fake receipt with status: 0, because in Ethereum - // receipt.status === 0 means "reverted/failed", not "pending" + // Timeout reached, treat as pending logger.warn(`Transaction ${tx.hash} is still pending after timeout`); - resolve(null); + resolve({ + transactionHash: tx.hash, + blockHash: '', + blockNumber: null, + transactionIndex: null, + from: tx.from, + to: tx.to || null, + cumulativeGasUsed: BigNumber.from(0), + gasUsed: BigNumber.from(0), + contractAddress: null, + logs: [], + logsBloom: '', + status: 0, // PENDING + effectiveGasPrice: BigNumber.from(0), + } as providers.TransactionReceipt); }, this._transactionExecutionTimeoutMs), ), ]); @@ -968,7 +979,6 @@ export class Ethereum { expectedAmountIn: number, expectedAmountOut: number, side?: 'BUY' | 'SELL', - txHash?: string, // Optional tx hash for pending transactions ): { signature: string; status: number; @@ -986,8 +996,8 @@ export class Ethereum { // Transaction receipt not available - still pending logger.warn('Transaction pending, no receipt available yet'); return { - signature: txHash || '', // Use provided txHash for pending transactions - status: 0, // PENDING (TransactionStatus.PENDING = 0) + signature: '', + status: 0, // PENDING data: undefined, }; } @@ -999,7 +1009,7 @@ export class Ethereum { logger.error(`Transaction ${signature} failed on-chain`); return { signature, - status: -1, // FAILED (TransactionStatus.FAILED = -1) + status: -1, // FAILED data: { tokenIn: inputToken, tokenOut: outputToken, diff --git a/src/connectors/pancakeswap/pancakeswap.config.ts b/src/connectors/pancakeswap/pancakeswap.config.ts index 22e901de43..c254f0f436 100644 --- a/src/connectors/pancakeswap/pancakeswap.config.ts +++ b/src/connectors/pancakeswap/pancakeswap.config.ts @@ -18,7 +18,6 @@ export namespace PancakeswapConfig { // Global configuration slippagePct: number; maximumHops: number; - maximumSplits: number; // Available networks availableNetworks: Array; @@ -26,8 +25,7 @@ export namespace PancakeswapConfig { export const config: RootConfig = { slippagePct: ConfigManagerV2.getInstance().get('pancakeswap.slippagePct'), - maximumHops: ConfigManagerV2.getInstance().get('pancakeswap.maximumHops'), - maximumSplits: ConfigManagerV2.getInstance().get('pancakeswap.maximumSplits'), + maximumHops: ConfigManagerV2.getInstance().get('pancakeswap.maximumHops') || 4, availableNetworks: [ { diff --git a/src/connectors/pancakeswap/pancakeswap.ts b/src/connectors/pancakeswap/pancakeswap.ts index 7ead39828d..0e32da1c71 100644 --- a/src/connectors/pancakeswap/pancakeswap.ts +++ b/src/connectors/pancakeswap/pancakeswap.ts @@ -222,8 +222,6 @@ export class Pancakeswap { deadline: Math.floor(Date.now() / 1000 + 1800), // 30 minutes recipient, protocols: protocolsToUse, - maxHops: this.config.maximumHops, - maxSplits: this.config.maximumSplits, }, ); diff --git a/src/connectors/pancakeswap/universal-router.ts b/src/connectors/pancakeswap/universal-router.ts index 12ffa337ad..ed73e1c035 100644 --- a/src/connectors/pancakeswap/universal-router.ts +++ b/src/connectors/pancakeswap/universal-router.ts @@ -10,9 +10,17 @@ import { SwapRouter, TradeConfig, } from '@pancakeswap/smart-router'; -import { Pair as V2Pair, computePairAddress } from '@pancakeswap/v2-sdk'; +import { Pair as V2Pair, Route as V2Route, Trade as V2Trade, computePairAddress } from '@pancakeswap/v2-sdk'; import IPancakeswapV3Pool from '@pancakeswap/v3-core/artifacts/contracts/PancakeV3Pool.sol/PancakeV3Pool.json'; -import { Pool as V3Pool, FeeAmount, computePoolAddress, nearestUsableTick, TICK_SPACINGS } from '@pancakeswap/v3-sdk'; +import { + Pool as V3Pool, + Route as V3Route, + Trade as V3Trade, + FeeAmount, + computePoolAddress, + nearestUsableTick, + TICK_SPACINGS, +} from '@pancakeswap/v3-sdk'; import { BigNumber, Contract } from 'ethers'; import { Address } from 'viem'; @@ -28,36 +36,6 @@ import { // Common fee tiers for V3 const V3_FEE_TIERS = [FeeAmount.LOWEST, FeeAmount.LOW, FeeAmount.MEDIUM, FeeAmount.HIGH]; -// Common intermediate/base tokens for multi-hop routing per network -// These are the most liquid tokens that are commonly used as routing intermediates -const INTERMEDIATE_TOKENS: { [network: string]: { symbol: string; address: string; decimals: number }[] } = { - bsc: [ - { symbol: 'WBNB', address: '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c', decimals: 18 }, - { symbol: 'USDT', address: '0x55d398326f99059fF775485246999027B3197955', decimals: 18 }, - { symbol: 'USDC', address: '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', decimals: 18 }, - { symbol: 'BUSD', address: '0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56', decimals: 18 }, - { symbol: 'DAI', address: '0x1AF3F329e8BE154074D8769D1FFa4eE058B1DBc3', decimals: 18 }, - ], - mainnet: [ - { symbol: 'WETH', address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', decimals: 18 }, - { symbol: 'USDT', address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', decimals: 6 }, - { symbol: 'USDC', address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', decimals: 6 }, - { symbol: 'DAI', address: '0x6B175474E89094C44Da98b954EesdeC80D8D8Ac', decimals: 18 }, - { symbol: 'WBTC', address: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', decimals: 8 }, - ], - arbitrum: [ - { symbol: 'WETH', address: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', decimals: 18 }, - { symbol: 'USDT', address: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', decimals: 6 }, - { symbol: 'USDC', address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', decimals: 6 }, - { symbol: 'DAI', address: '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1', decimals: 18 }, - ], - base: [ - { symbol: 'WETH', address: '0x4200000000000000000000000000000000000006', decimals: 18 }, - { symbol: 'USDC', address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', decimals: 6 }, - { symbol: 'DAI', address: '0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb', decimals: 18 }, - ], -}; - export interface UniversalRouterQuoteResult { trade: SmartRouterTrade; route: string[]; @@ -106,8 +84,6 @@ export class UniversalRouterService { deadline: number; recipient: string; protocols?: PoolType[]; - maxHops?: number; - maxSplits?: number; }, ): Promise { logger.info(`[UniversalRouter] Starting quote generation`); @@ -118,82 +94,56 @@ export class UniversalRouterService { ); logger.info(`[UniversalRouter] Recipient: ${options.recipient}`); logger.info(`[UniversalRouter] Slippage: ${options.slippageTolerance.toSignificant()}%`); - if (options.maxHops !== undefined) { - logger.info(`[UniversalRouter] Max hops: ${options.maxHops}`); - } - if (options.maxSplits !== undefined) { - logger.info(`[UniversalRouter] Max splits: ${options.maxSplits}`); - } const protocols = options.protocols || [PoolType.V2, PoolType.V3]; logger.info(`[UniversalRouter] Protocols to check: ${protocols.join(', ')}`); - const allPools: Pool[] = []; - const poolAddresses = new Set(); // Track added pools to avoid duplicates - - // Get intermediate tokens for this network - const intermediateTokens = this.getIntermediateTokens(tokenIn, tokenOut); - logger.info( - `[UniversalRouter] Using ${intermediateTokens.length} intermediate tokens for multi-hop routing: ${intermediateTokens.map((t) => t.symbol).join(', ')}`, - ); - - // Collect all token pairs to search for pools - // Direct pair + pairs through intermediate tokens - const tokenPairs: [Token, Token][] = [[tokenIn, tokenOut]]; - - // Add pairs through each intermediate token for multi-hop routing - for (const intermediate of intermediateTokens) { - tokenPairs.push([tokenIn, intermediate]); - tokenPairs.push([intermediate, tokenOut]); - } - - logger.info(`[UniversalRouter] Searching ${tokenPairs.length} token pairs for pools...`); + const allPools = []; - // Search for V3 pools + // Try to find routes through each protocol if (protocols.includes(PoolType.V3)) { - logger.info(`[UniversalRouter] Searching for V3 pools...`); - for (const [tokenA, tokenB] of tokenPairs) { - try { - const pool = await this.findV3Pool(tokenA, tokenB); - if (pool) { - const poolKey = `v3-${tokenA.address}-${tokenB.address}`; - if (!poolAddresses.has(poolKey)) { - poolAddresses.add(poolKey); - (pool as unknown as Pool).type = PoolType.V3; - allPools.push(pool as unknown as Pool); - logger.info(`[UniversalRouter] Found V3 pool: ${tokenA.symbol} <-> ${tokenB.symbol}`); + logger.info(`[UniversalRouter] Searching for V3 routes...`); + try { + const v3Trade = await this.findV3Route(tokenIn, tokenOut, amount, tradeType); + if (v3Trade) { + logger.info( + `[UniversalRouter] Found V3 route: ${v3Trade.inputAmount.toExact()} -> ${v3Trade.outputAmount.toExact()}`, + ); + for (const swap of v3Trade.swaps) { + for (const pool of swap.route.pools as unknown as Pool[]) { + pool.type = PoolType.V3; + allPools.push(pool); } } - } catch (error) { - // Pool doesn't exist, continue + } else { + logger.info(`[UniversalRouter] No V3 route found`); } + } catch (error) { + logger.warn(`[UniversalRouter] Failed to find V3 route: ${error.message}`); } } - // Search for V2 pools if (protocols.includes(PoolType.V2)) { - logger.info(`[UniversalRouter] Searching for V2 pools...`); - for (const [tokenA, tokenB] of tokenPairs) { - try { - const pair = await this.findV2Pool(tokenA, tokenB); - if (pair) { - const poolKey = `v2-${tokenA.address}-${tokenB.address}`; - if (!poolAddresses.has(poolKey)) { - poolAddresses.add(poolKey); - (pair as unknown as Pool).type = PoolType.V2; - allPools.push(pair as unknown as Pool); - logger.info(`[UniversalRouter] Found V2 pool: ${tokenA.symbol} <-> ${tokenB.symbol}`); - } + logger.info(`[UniversalRouter] Searching for V2 routes...`); + try { + const v2Trade = await this.findV2Route(tokenIn, tokenOut, amount, tradeType); + if (v2Trade) { + logger.info( + `[UniversalRouter] Found V2 route: ${v2Trade.inputAmount.toExact()} -> ${v2Trade.outputAmount.toExact()}`, + ); + for (const pair of v2Trade.route.pairs as unknown as Pool[]) { + pair.type = PoolType.V2; + allPools.push(pair); } - } catch (error) { - // Pool doesn't exist, continue + } else { + logger.info(`[UniversalRouter] No V2 route found`); } + } catch (error) { + logger.warn(`[UniversalRouter] Failed to find V2 route: ${error.message}`); } } - logger.info(`[UniversalRouter] Total pools found: ${allPools.length}`); - if (allPools.length === 0) { - logger.error(`[UniversalRouter] No pools found for ${tokenIn.symbol} -> ${tokenOut.symbol}`); + logger.error(`[UniversalRouter] No routes found for ${tokenIn.symbol} -> ${tokenOut.symbol}`); throw new Error(`No routes found for ${tokenIn.symbol} -> ${tokenOut.symbol}`); } @@ -216,8 +166,6 @@ export class UniversalRouterService { quoteProvider, quoterOptimization: true, gasPriceWei, - maxHops: options.maxHops, - maxSplits: options.maxSplits, }; // Create RouterTrade based on the best route @@ -242,7 +190,7 @@ export class UniversalRouterService { // Calculate route path const route = this.extractRoutePath(bestTrade); - const routePath = route.join(', '); + const routePath = route.join(' -> '); logger.info(`[UniversalRouter] Route path: ${routePath}`); // Skip gas estimation during quote phase - it will be done during execution @@ -282,43 +230,22 @@ export class UniversalRouterService { } /** - * Get intermediate tokens for multi-hop routing - * Excludes the input and output tokens from the list + * Find V3 route using pool address computation */ - private getIntermediateTokens(tokenIn: Token, tokenOut: Token): Token[] { - const intermediates = INTERMEDIATE_TOKENS[this.network] || []; - const result: Token[] = []; - - for (const intermediate of intermediates) { - // Skip if this is the input or output token - if ( - intermediate.address.toLowerCase() === tokenIn.address.toLowerCase() || - intermediate.address.toLowerCase() === tokenOut.address.toLowerCase() - ) { - continue; - } - - result.push(new Token(this.chainId, intermediate.address as Address, intermediate.decimals, intermediate.symbol)); - } - - return result; - } - - /** - * Find a V3 pool for a token pair (returns the pool with best liquidity) - */ - private async findV3Pool(tokenA: Token, tokenB: Token): Promise { - let bestPool: V3Pool | null = null; - let bestLiquidity = BigNumber.from(0); - - // Try each fee tier and find the one with most liquidity + private async findV3Route( + tokenIn: Token, + tokenOut: Token, + amount: CurrencyAmount, + tradeType: TradeType, + ): Promise | null> { + // Try each fee tier for (const fee of V3_FEE_TIERS) { try { // Compute pool address const poolAddress = computePoolAddress({ deployerAddress: getPancakeswapV3PoolDeployerAddress(this.network), - tokenA, - tokenB, + tokenA: tokenIn, + tokenB: tokenOut, fee, }); @@ -352,55 +279,54 @@ export class UniversalRouterService { } // Create pool instance with tick data - const pool = new V3Pool(tokenA, tokenB, fee, sqrtPriceX96.toString(), liquidity.toString(), tick, ticks); + const pool = new V3Pool(tokenIn, tokenOut, fee, sqrtPriceX96.toString(), liquidity.toString(), tick, ticks); - // Keep track of pool with best liquidity - if (liquidity.gt(bestLiquidity)) { - bestLiquidity = liquidity; - bestPool = pool; - } + // Create route and trade + const route = new V3Route([pool], tokenIn, tokenOut); + + return tradeType === TradeType.EXACT_INPUT ? V3Trade.exactIn(route, amount) : V3Trade.exactOut(route, amount); } catch (error) { // Pool doesn't exist or other error, continue to next fee tier continue; } } - return bestPool; + return null; } /** - * Find a V2 pool (pair) for a token pair + * Find V2 route for a token pair */ - private async findV2Pool(tokenA: Token, tokenB: Token): Promise { + private async findV2Route( + tokenIn: Token, + tokenOut: Token, + amount: CurrencyAmount, + tradeType: TradeType, + ): Promise | null> { try { // Compute pair address const pairAddress = computePairAddress({ factoryAddress: getPancakeswapV2FactoryAddress(this.network), - tokenA, - tokenB, + tokenA: tokenIn, + tokenB: tokenOut, }); const pairContract = new Contract(pairAddress, IPancakeswapV2PairABI.abi, this.provider); const reserves = await pairContract.getReserves(); - const token0Address = await pairContract.token0(); + const token0 = await pairContract.token0(); const [reserve0, reserve1] = reserves; - - // Check if pool has liquidity - if (reserve0.eq(0) || reserve1.eq(0)) { - return null; - } - - // Determine which token is token0 and which is token1 - const isTokenAToken0 = tokenA.address.toLowerCase() === token0Address.toLowerCase(); - const [tokenAReserve, tokenBReserve] = isTokenAToken0 ? [reserve0, reserve1] : [reserve1, reserve0]; + const [reserveIn, reserveOut] = + tokenIn.address.toLowerCase() === token0.toLowerCase() ? [reserve0, reserve1] : [reserve1, reserve0]; const pair = new V2Pair( - CurrencyAmount.fromRawAmount(tokenA, tokenAReserve.toString()), - CurrencyAmount.fromRawAmount(tokenB, tokenBReserve.toString()), + CurrencyAmount.fromRawAmount(tokenIn, reserveIn.toString()), + CurrencyAmount.fromRawAmount(tokenOut, reserveOut.toString()), ); - return pair; + const route = new V2Route([pair], tokenIn, tokenOut); + + return new V2Trade(route, amount, tradeType); } catch (error) { return null; } @@ -408,40 +334,16 @@ export class UniversalRouterService { /** * Extract route path from a trade - * Returns an array of route descriptions with percentages and full token paths */ private extractRoutePath(trade: SmartRouterTrade): string[] { - const routeDescriptions: string[] = []; + const path: string[] = []; for (const route of trade.routes) { - // Get the full path of tokens from the route - // route.path contains all tokens including intermediates (e.g., [LINK, WBNB, DAI]) - const routeWithPath = route as unknown as { path?: Currency[]; percent?: number }; - - let pathSymbols: string[]; - if (routeWithPath.path && routeWithPath.path.length > 0) { - // Use the full path from the route - pathSymbols = routeWithPath.path.map((currency: Currency) => { - const token = currency as Token; - return token.symbol || token.address; - }); - } else { - // Fallback to input/output if path not available - pathSymbols = [ - route.inputAmount.currency.symbol || (route.inputAmount.currency as Token).address, - route.outputAmount.currency.symbol || (route.outputAmount.currency as Token).address, - ]; - } - - // Get the percentage for this route (from RouteWithoutQuote) - const percent = routeWithPath.percent || 100; - - // Format as "X% via TOKEN1 -> TOKEN2 -> TOKEN3" - const pathStr = pathSymbols.join(' -> '); - routeDescriptions.push(`${percent}% via ${pathStr}`); + path.push(route.inputAmount.currency.symbol || (route.inputAmount.currency as Token).address); + path.push(route.outputAmount.currency.symbol || (route.outputAmount.currency as Token).address); } - return routeDescriptions; + return path; } /** diff --git a/src/connectors/uniswap/alpha-router.ts b/src/connectors/uniswap/alpha-router.ts deleted file mode 100644 index 209ec556c5..0000000000 --- a/src/connectors/uniswap/alpha-router.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { BaseProvider } from '@ethersproject/providers'; -import { CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'; -import { AlphaRouter, SwapRoute, SwapType } from '@uniswap/smart-order-router'; -import { UniversalRouterVersion } from '@uniswap/universal-router-sdk'; - -import { logger } from '../../services/logger'; - -// Chain IDs as numbers (matching @uniswap/sdk-core ChainId enum values) -const NETWORK_TO_CHAIN_ID: { [network: string]: number } = { - mainnet: 1, - goerli: 5, - sepolia: 11155111, - arbitrum: 42161, - optimism: 10, - polygon: 137, - base: 8453, - bsc: 56, - avalanche: 43114, - celo: 42220, -}; - -export interface AlphaRouterQuoteResult { - route: SwapRoute; - inputAmount: string; - outputAmount: string; - priceImpact: number; - routeString: string; - gasEstimate: string; - gasEstimateUSD: string; - methodParameters?: { - calldata: string; - value: string; - to: string; - }; -} - -export class AlphaRouterService { - private router: AlphaRouter; - private chainId: number; - private network: string; - - constructor(provider: BaseProvider, network: string) { - const chainId = NETWORK_TO_CHAIN_ID[network]; - if (!chainId) { - throw new Error(`Unsupported network for AlphaRouter: ${network}`); - } - - this.chainId = chainId; - this.network = network; - - // Initialize AlphaRouter with minimal config - // It will use default providers for pools, quotes, etc. - this.router = new AlphaRouter({ - chainId: this.chainId, - provider: provider, - }); - - logger.info(`[AlphaRouter] Initialized for network ${network} (chainId: ${chainId})`); - } - - /** - * Get an optimized quote using AlphaRouter's smart order routing - * This will automatically find the best route across V2, V3, and mixed pools - * with optimal split routing for better execution prices. - */ - async getQuote( - tokenIn: Token, - tokenOut: Token, - amount: CurrencyAmount, - tradeType: TradeType, - options: { - slippageTolerance: Percent; - deadline: number; - recipient: string; - }, - ): Promise { - const tradeTypeStr = tradeType === TradeType.EXACT_INPUT ? 'EXACT_INPUT' : 'EXACT_OUTPUT'; - logger.info( - `[AlphaRouter] Getting quote: ${amount.toExact()} ${tokenIn.symbol} -> ${tokenOut.symbol} (${tradeTypeStr})`, - ); - logger.debug(`[AlphaRouter] Input token: ${tokenIn.symbol} (${tokenIn.address})`); - logger.debug(`[AlphaRouter] Output token: ${tokenOut.symbol} (${tokenOut.address})`); - logger.debug(`[AlphaRouter] Recipient: ${options.recipient}`); - logger.debug(`[AlphaRouter] Slippage: ${options.slippageTolerance.toSignificant()}%`); - - // For EXACT_INPUT: amount is the input, quoteCurrency is the output (what we're getting) - // For EXACT_OUTPUT: amount is the output, quoteCurrency is the input (what we're paying) - const quoteCurrency = tradeType === TradeType.EXACT_INPUT ? tokenOut : tokenIn; - logger.debug(`[AlphaRouter] Quote currency: ${quoteCurrency.symbol} (${quoteCurrency.address})`); - - const swapRoute = await this.router.route(amount, quoteCurrency, tradeType, { - type: SwapType.UNIVERSAL_ROUTER, - version: UniversalRouterVersion.V2_0, - slippageTolerance: options.slippageTolerance, - deadlineOrPreviousBlockhash: options.deadline, - recipient: options.recipient, - }); - - if (!swapRoute) { - throw new Error(`No route found for ${tokenIn.symbol} -> ${tokenOut.symbol}`); - } - - logger.debug(`[AlphaRouter] Quote: ${swapRoute.quote.toExact()} ${swapRoute.quote.currency.symbol}`); - logger.debug(`[AlphaRouter] Gas estimate: ${swapRoute.estimatedGasUsed.toString()}`); - logger.debug(`[AlphaRouter] Gas estimate USD: $${swapRoute.estimatedGasUsedUSD.toExact()}`); - - // Log route details (split routing info) - const routeStrings: string[] = []; - for (const route of swapRoute.route) { - const routeStr = route.tokenPath.map((t) => t.symbol).join(' -> '); - const percent = route.percent; - routeStrings.push(`${percent}% via ${routeStr}`); - logger.debug(`[AlphaRouter] Route: ${percent}% via ${routeStr}`); - } - - // Extract method parameters for Universal Router execution - let methodParameters: AlphaRouterQuoteResult['methodParameters']; - if (swapRoute.methodParameters) { - methodParameters = { - calldata: swapRoute.methodParameters.calldata, - value: swapRoute.methodParameters.value, - to: swapRoute.methodParameters.to, - }; - logger.debug(`[AlphaRouter] Calldata length: ${methodParameters.calldata.length}`); - logger.debug(`[AlphaRouter] Value: ${methodParameters.value}`); - logger.debug(`[AlphaRouter] To: ${methodParameters.to}`); - } - - const result: AlphaRouterQuoteResult = { - route: swapRoute, - inputAmount: tradeType === TradeType.EXACT_INPUT ? amount.toExact() : swapRoute.quote.toExact(), - outputAmount: tradeType === TradeType.EXACT_INPUT ? swapRoute.quote.toExact() : amount.toExact(), - priceImpact: parseFloat(swapRoute.trade?.priceImpact?.toSignificant(4) || '0'), - routeString: routeStrings.join(' | '), - gasEstimate: swapRoute.estimatedGasUsed.toString(), - gasEstimateUSD: swapRoute.estimatedGasUsedUSD.toExact(), - methodParameters, - }; - - logger.info( - `[AlphaRouter] Quote: ${result.inputAmount} ${tokenIn.symbol} -> ${result.outputAmount} ${tokenOut.symbol} (impact: ${result.priceImpact}%, route: ${result.routeString})`, - ); - - return result; - } -} diff --git a/src/connectors/uniswap/router-routes/executeQuote.ts b/src/connectors/uniswap/router-routes/executeQuote.ts index 4b14fb48a8..2cd0f87d17 100644 --- a/src/connectors/uniswap/router-routes/executeQuote.ts +++ b/src/connectors/uniswap/router-routes/executeQuote.ts @@ -110,8 +110,7 @@ async function executeQuote(walletAddress: string, network: string, quoteId: str } // Execute the swap transaction - let txReceipt: Awaited> = null; - let txHash: string | undefined; + let txReceipt; try { if (isHardwareWallet) { @@ -140,8 +139,6 @@ async function executeQuote(walletAddress: string, network: string, quoteId: str // Send the signed transaction const txResponse = await ethereum.provider.sendTransaction(signedTx); - txHash = txResponse.hash; - logger.info(`Transaction sent: ${txHash}`); // Wait for confirmation with timeout txReceipt = await ethereum.handleTransactionExecution(txResponse); @@ -174,8 +171,7 @@ async function executeQuote(walletAddress: string, network: string, quoteId: str // Send transaction directly without relying on ethers' automatic gas estimation const txResponse = await wallet.sendTransaction(txData); - txHash = txResponse.hash; - logger.info(`Transaction sent: ${txHash}`); + logger.info(`Transaction sent: ${txResponse.hash}`); // Wait for transaction confirmation with timeout txReceipt = await ethereum.handleTransactionExecution(txResponse); @@ -266,7 +262,6 @@ async function executeQuote(walletAddress: string, network: string, quoteId: str const expectedAmountOut = parseFloat(quote.trade.outputAmount.toExact()); // Use the new handleExecuteQuoteTransactionConfirmation helper - // Pass txHash for pending transactions (when txReceipt is null) const result = ethereum.handleExecuteQuoteTransactionConfirmation( txReceipt, inputToken.address, @@ -274,12 +269,10 @@ async function executeQuote(walletAddress: string, network: string, quoteId: str expectedAmountIn, expectedAmountOut, side, - txHash, ); // Handle different transaction states - // Status codes: 1 = CONFIRMED, 0 = PENDING, -1 = FAILED - if (result.status === -1) { + if (result.status === 0) { // Transaction failed logger.error(`Transaction failed on-chain. Receipt: ${JSON.stringify(txReceipt)}`); throw httpErrors.internalServerError( @@ -287,7 +280,7 @@ async function executeQuote(walletAddress: string, network: string, quoteId: str ); } - if (result.status === 0) { + if (result.status === -1) { // Transaction is still pending logger.info(`Transaction ${result.signature || 'pending'} is still pending`); return result; diff --git a/src/connectors/uniswap/router-routes/quoteSwap.ts b/src/connectors/uniswap/router-routes/quoteSwap.ts index 32693a64d0..cf33b887e8 100644 --- a/src/connectors/uniswap/router-routes/quoteSwap.ts +++ b/src/connectors/uniswap/router-routes/quoteSwap.ts @@ -22,8 +22,10 @@ async function quoteSwap( side: 'BUY' | 'SELL', slippagePct: number = UniswapConfig.config.slippagePct, ): Promise> { - logger.info(`[quoteSwap] ${baseToken}/${quoteToken} ${side} ${amount} on ${network}`); - logger.debug(`[quoteSwap] Wallet: ${walletAddress || 'not provided'}, Slippage: ${slippagePct}%`); + logger.info(`[quoteSwap] Starting quote generation`); + logger.info(`[quoteSwap] Network: ${network}, Wallet: ${walletAddress || 'not provided'}`); + logger.info(`[quoteSwap] Base: ${baseToken}, Quote: ${quoteToken}`); + logger.info(`[quoteSwap] Amount: ${amount}, Side: ${side}, Slippage: ${slippagePct}%`); const ethereum = await Ethereum.getInstance(network); const uniswap = await Uniswap.getInstance(network); @@ -37,8 +39,8 @@ async function quoteSwap( throw httpErrors.notFound(sanitizeErrorMessage('Token not found: {}', !baseTokenInfo ? baseToken : quoteToken)); } - logger.debug(`[quoteSwap] Base token: ${baseTokenInfo.symbol} (${baseTokenInfo.address})`); - logger.debug(`[quoteSwap] Quote token: ${quoteTokenInfo.symbol} (${quoteTokenInfo.address})`); + logger.info(`[quoteSwap] Base token: ${baseTokenInfo.symbol} (${baseTokenInfo.address})`); + logger.info(`[quoteSwap] Quote token: ${quoteTokenInfo.symbol} (${quoteTokenInfo.address})`); // Convert to Uniswap SDK Token objects const baseTokenObj = uniswap.getUniswapToken(baseTokenInfo); @@ -48,26 +50,36 @@ async function quoteSwap( const exactIn = side === 'SELL'; const [inputToken, outputToken] = exactIn ? [baseTokenObj, quoteTokenObj] : [quoteTokenObj, baseTokenObj]; - logger.debug(`[quoteSwap] Input: ${inputToken.symbol}, Output: ${outputToken.symbol}, Exact in: ${exactIn}`); + logger.info(`[quoteSwap] Input token: ${inputToken.symbol} (${inputToken.address})`); + logger.info(`[quoteSwap] Output token: ${outputToken.symbol} (${outputToken.address})`); + logger.info(`[quoteSwap] Exact in: ${exactIn}`); - // Get quote from AlphaRouter (smart order router with split routing) - // Use a placeholder address for quotes when no wallet is provided - const recipient = walletAddress || '0x0000000000000000000000000000000000000001'; - const quoteResult = await uniswap.getAlphaRouterQuote(inputToken, outputToken, amount, side, recipient, slippagePct); + // Get quote from Universal Router + logger.info(`[quoteSwap] Calling getUniversalRouterQuote...`); + const quoteResult = await uniswap.getUniversalRouterQuote(inputToken, outputToken, amount, side, walletAddress); + logger.info(`[quoteSwap] Quote result received`); // Generate unique quote ID const quoteId = uuidv4(); + logger.info(`[quoteSwap] Generated quote ID: ${quoteId}`); + + // Extract route information from quoteResult + const routePath = quoteResult.routePath; + logger.info(`[quoteSwap] Route path: ${routePath}`); + + // Calculate amounts based on quote + let estimatedAmountIn: number; + let estimatedAmountOut: number; + + if (exactIn) { + estimatedAmountIn = amount; + estimatedAmountOut = parseFloat(quoteResult.quote.toExact()); + } else { + estimatedAmountIn = parseFloat(quoteResult.trade.inputAmount.toExact()); + estimatedAmountOut = amount; + } - // Extract route information from AlphaRouter result - const routePath = quoteResult.routeString; - - // Get amounts from AlphaRouter result - const estimatedAmountIn = parseFloat(quoteResult.inputAmount); - const estimatedAmountOut = parseFloat(quoteResult.outputAmount); - - logger.debug( - `[quoteSwap] Quote ${quoteId}: ${estimatedAmountIn} -> ${estimatedAmountOut}, gas: ${quoteResult.gasEstimate}`, - ); + logger.info(`[quoteSwap] Estimated amounts - In: ${estimatedAmountIn}, Out: ${estimatedAmountOut}`); const minAmountOut = side === 'SELL' ? estimatedAmountOut * (1 - slippagePct / 100) : estimatedAmountOut; const maxAmountIn = side === 'BUY' ? estimatedAmountIn * (1 + slippagePct / 100) : estimatedAmountIn; @@ -77,15 +89,13 @@ async function quoteSwap( side === 'SELL' ? estimatedAmountOut / estimatedAmountIn // SELL: USDC per HBOT : estimatedAmountIn / estimatedAmountOut; // BUY: USDC per HBOT - logger.debug(`[quoteSwap] Price: ${price}, Min out: ${minAmountOut}, Max in: ${maxAmountIn}`); + logger.info(`[quoteSwap] Price: ${price}, Min out: ${minAmountOut}, Max in: ${maxAmountIn}`); // Cache the quote for execution // Store both quote and request data in the quote object for Uniswap - // Include 'trade' at top level for compatibility with executeQuote const cachedQuote = { quote: { ...quoteResult, - trade: quoteResult.route.trade, // Extract trade from SwapRoute for executeQuote compatibility methodParameters: quoteResult.methodParameters, }, request: { @@ -104,13 +114,13 @@ async function quoteSwap( quoteCache.set(quoteId, cachedQuote); logger.info( - `[quoteSwap] Quote ${quoteId}: ${estimatedAmountIn} ${inputToken.symbol} -> ${estimatedAmountOut} ${outputToken.symbol}`, + `[quoteSwap] Cached quote ${quoteId}: ${estimatedAmountIn} ${inputToken.symbol} -> ${estimatedAmountOut} ${outputToken.symbol}`, ); - logger.debug(`[quoteSwap] Method parameters available: ${!!quoteResult.methodParameters}`); + logger.info(`[quoteSwap] Method parameters available: ${!!quoteResult.methodParameters}`); if (quoteResult.methodParameters) { - logger.debug( - `[quoteSwap] Calldata length: ${quoteResult.methodParameters.calldata.length}, To: ${quoteResult.methodParameters.to}`, - ); + logger.info(`[quoteSwap] Calldata length: ${quoteResult.methodParameters.calldata.length}`); + logger.info(`[quoteSwap] Value: ${quoteResult.methodParameters.value}`); + logger.info(`[quoteSwap] To: ${quoteResult.methodParameters.to}`); } return { diff --git a/src/connectors/uniswap/uniswap.ts b/src/connectors/uniswap/uniswap.ts index 1eb8d2e02c..db8db345de 100644 --- a/src/connectors/uniswap/uniswap.ts +++ b/src/connectors/uniswap/uniswap.ts @@ -12,7 +12,6 @@ import JSBI from 'jsbi'; import { Ethereum, TokenInfo } from '../../chains/ethereum/ethereum'; import { logger } from '../../services/logger'; -import { AlphaRouterService, AlphaRouterQuoteResult } from './alpha-router'; import { UniswapConfig } from './uniswap.config'; import { IUniswapV2PairABI, @@ -49,7 +48,6 @@ export class Uniswap { private v3NFTManager: Contract; private v3Quoter: Contract; private universalRouter: UniversalRouterService; - private alphaRouter: AlphaRouterService; // Network information private networkName: string; @@ -141,14 +139,6 @@ export class Uniswap { // Initialize Universal Router service this.universalRouter = new UniversalRouterService(this.ethereum.provider, this.chainId, this.networkName); - // Initialize AlphaRouter service for split routing - try { - this.alphaRouter = new AlphaRouterService(this.ethereum.provider, this.networkName); - logger.info(`AlphaRouter initialized for network: ${this.networkName}`); - } catch (error) { - logger.warn(`AlphaRouter not available for network ${this.networkName}: ${error.message}`); - } - // Ensure ethereum is initialized if (!this.ethereum.ready()) { await this.ethereum.init(); @@ -236,60 +226,6 @@ export class Uniswap { return quoteResult; } - /** - * Get a quote using AlphaRouter's split routing for optimal execution - * This uses Uniswap's smart order router to find the best route across V2, V3, and mixed pools - * with optimal split percentages for better execution prices. - * - * @param inputToken The token being swapped from - * @param outputToken The token being swapped to - * @param amount The amount to swap - * @param side The trade direction (BUY or SELL) - * @param walletAddress The recipient wallet address - * @param slippagePct Optional slippage percentage (defaults to config value) - * @returns Quote result with split routing information - */ - public async getAlphaRouterQuote( - inputToken: Token, - outputToken: Token, - amount: number, - side: 'BUY' | 'SELL', - walletAddress: string, - slippagePct?: number, - ): Promise { - if (!this.alphaRouter) { - throw new Error(`AlphaRouter not available for network ${this.networkName}`); - } - - // Determine input/output based on side - const exactIn = side === 'SELL'; - const tokenForAmount = exactIn ? inputToken : outputToken; - - // Convert amount to token units using ethers parseUnits for proper decimal handling - const { parseUnits } = await import('ethers/lib/utils'); - const rawAmount = parseUnits(amount.toString(), tokenForAmount.decimals); - const tradeAmount = CurrencyAmount.fromRawAmount(tokenForAmount, rawAmount.toString()); - - // Use provided slippage or fall back to config - const slippage = slippagePct ?? this.config.slippagePct; - const slippageTolerance = new Percent(Math.floor(slippage * 100), 10000); - - // Get quote from AlphaRouter - const quoteResult = await this.alphaRouter.getQuote( - inputToken, - outputToken, - tradeAmount, - exactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT, - { - slippageTolerance, - deadline: Math.floor(Date.now() / 1000 + 1800), // 30 minutes - recipient: walletAddress, - }, - ); - - return quoteResult; - } - /** * Get a V2 pool (pair) by its address or by token symbols */ diff --git a/src/connectors/uniswap/universal-router.ts b/src/connectors/uniswap/universal-router.ts index 3df6324101..124f84c06d 100644 --- a/src/connectors/uniswap/universal-router.ts +++ b/src/connectors/uniswap/universal-router.ts @@ -185,7 +185,7 @@ export class UniversalRouterService { // Calculate route path const route = this.extractRoutePath(bestTrade); - const routePath = route.join(', '); + const routePath = route.join(' -> '); logger.info(`[UniversalRouter] Route path: ${routePath}`); // Skip gas estimation during quote phase - it will be done during execution @@ -329,41 +329,19 @@ export class UniversalRouterService { /** * Extract route path from a trade - * Returns an array of route descriptions with percentages and full token paths */ private extractRoutePath(trade: RouterTrade): string[] { - const routeDescriptions: string[] = []; - const totalInput = trade.inputAmount; - - for (const swap of trade.swaps) { - const route = swap.route; - - // Get the full path of tokens from the route - // route.path contains all tokens including intermediates (e.g., [LINK, WETH, DAI]) - let pathSymbols: string[]; - if (route.path && route.path.length > 0) { - // Use the full path from the route - pathSymbols = route.path.map((currency: Currency) => { - const token = currency as Token; - return token.symbol || token.address; - }); - } else { - // Fallback to input/output if path not available - pathSymbols = [ - route.input.symbol || (route.input as Token).address, - route.output.symbol || (route.output as Token).address, - ]; - } + const path: string[] = []; - // Calculate the percentage for this swap - const percent = Math.round((parseFloat(swap.inputAmount.toExact()) / parseFloat(totalInput.toExact())) * 100); + if (trade.swaps.length > 0) { + const firstSwap = trade.swaps[0]; + const route = firstSwap.route; - // Format as "X% via TOKEN1 -> TOKEN2 -> TOKEN3" - const pathStr = pathSymbols.join(' -> '); - routeDescriptions.push(`${percent}% via ${pathStr}`); + path.push(route.input.symbol || (route.input as Token).address); + path.push(route.output.symbol || (route.output as Token).address); } - return routeDescriptions; + return path; } /** diff --git a/src/templates/connectors/pancakeswap.yml b/src/templates/connectors/pancakeswap.yml index 6452264cd4..886434ead7 100644 --- a/src/templates/connectors/pancakeswap.yml +++ b/src/templates/connectors/pancakeswap.yml @@ -1,9 +1,6 @@ -# Global settings for PancakeSwap +# Global settings for Uniswap # Default slippage percentage for swaps (2%) slippagePct: 2 # For each swap, the maximum number of hops to consider -maximumHops: 4 - -# Maximum number of split routes for optimal execution (e.g., 60% via V3, 40% via V2) -maximumSplits: 4 \ No newline at end of file +maximumHops: 4 \ No newline at end of file diff --git a/src/templates/namespace/pancakeswap-schema.json b/src/templates/namespace/pancakeswap-schema.json index 99beef6e9b..9974137371 100644 --- a/src/templates/namespace/pancakeswap-schema.json +++ b/src/templates/namespace/pancakeswap-schema.json @@ -9,12 +9,8 @@ "maximumHops": { "type": "integer", "description": "Maximum number of hops to consider for each swap" - }, - "maximumSplits": { - "type": "integer", - "description": "Maximum number of split routes for optimal execution (e.g., 60% via V3, 40% via V2)" } }, "additionalProperties": false, - "required": ["slippagePct", "maximumHops", "maximumSplits"] + "required": ["slippagePct", "maximumHops"] } diff --git a/test/connectors/pancakeswap/universal-router.test.ts b/test/connectors/pancakeswap/universal-router.test.ts index 6d02dfbf96..d4db2ea626 100644 --- a/test/connectors/pancakeswap/universal-router.test.ts +++ b/test/connectors/pancakeswap/universal-router.test.ts @@ -142,38 +142,5 @@ describe('UniversalRouterService', () => { expect(quote.methodParameters).toHaveProperty('to'); expect(quote.methodParameters.to).toBe('0x13f4EA83D0bd40E75C8222255bc855a974568Dd4'); }); - - it('should format routePath with percentage and token symbols', async () => { - const amount = CurrencyAmount.fromRawAmount(WBNB, '1000000000000000000'); - const options = { - slippageTolerance: new Percent(1, 100), - deadline: Math.floor(Date.now() / 1000) + 1800, - recipient: '0x0000000000000000000000000000000000000001', - protocols: [PoolType.V2], - }; - - // Mock a simple V2 pair - const mockContract = { - getReserves: jest - .fn() - .mockResolvedValue([ - BigNumber.from('1000000000000000000000'), - BigNumber.from('3000000000000'), - BigNumber.from('1234567890'), - ]), - token0: jest.fn().mockResolvedValue(WBNB.address), - token1: jest.fn().mockResolvedValue(USDC.address), - }; - - (Contract as any).mockImplementation(() => mockContract); - - const quote = await universalRouter.getQuote(WBNB, USDC, amount, TradeType.EXACT_INPUT, options); - - // Verify routePath format includes percentage and "via" - // Format should be like "100% via WBNB -> USDC" or for multi-hop "100% via LINK -> WBNB -> DAI" - expect(quote.routePath).toMatch(/^\d+% via .+$/); - expect(quote.routePath).toContain('% via'); - expect(quote.routePath).toContain('->'); - }); }); }); diff --git a/test/connectors/uniswap/router-routes/universal-router-quoteSwap.test.ts b/test/connectors/uniswap/router-routes/universal-router-quoteSwap.test.ts index 3b2ae2dfc7..a24b083e10 100644 --- a/test/connectors/uniswap/router-routes/universal-router-quoteSwap.test.ts +++ b/test/connectors/uniswap/router-routes/universal-router-quoteSwap.test.ts @@ -10,7 +10,15 @@ jest.mock('../../../../src/connectors/uniswap/uniswap'); jest.mock('uuid'); // Create a variable to store the mock implementation -const mockGetAlphaRouterQuote = jest.fn(); +const mockGetQuote = jest.fn(); +const mockUniversalRouterService = { + getQuote: mockGetQuote, +}; + +// Mock the UniversalRouterService +jest.mock('../../../../src/connectors/uniswap/universal-router', () => ({ + UniversalRouterService: jest.fn().mockImplementation(() => mockUniversalRouterService), +})); const buildApp = async () => { const server = fastifyWithTypeProvider(); @@ -51,16 +59,20 @@ describe('GET /quote-swap', () => { beforeEach(() => { jest.clearAllMocks(); - // Reset the AlphaRouter mock to default behavior - // This matches the AlphaRouterQuoteResult interface - mockGetAlphaRouterQuote.mockResolvedValue({ - route: { trade: { priceImpact: { toSignificant: () => '0.3' } } }, - inputAmount: '1', - outputAmount: '3000', + // Reset the UniversalRouterService mock to default behavior + mockGetQuote.mockResolvedValue({ + trade: { + inputAmount: { toExact: () => '1' }, + outputAmount: { toExact: () => '3000' }, + priceImpact: { toSignificant: () => '0.3' }, + }, + route: ['WETH', 'USDC'], + routePath: 'WETH -> USDC', priceImpact: 0.3, - routeString: 'WETH -> USDC', - gasEstimate: '300000', - gasEstimateUSD: '5.00', + estimatedGasUsed: { toString: () => '300000' }, + estimatedGasUsedQuoteToken: { toExact: () => '0.5' }, + quote: { toExact: () => '3000' }, + quoteGasAdjusted: { toExact: () => '2999.5' }, methodParameters: { calldata: '0x1234567890', value: '0x0', @@ -104,7 +116,7 @@ describe('GET /quote-swap', () => { mockUniswap = { router: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', getUniswapToken: jest.fn().mockImplementation((tokenInfo) => tokenInfo), - getAlphaRouterQuote: mockGetAlphaRouterQuote, + getUniversalRouterQuote: mockGetQuote, }; (Ethereum.getInstance as jest.Mock).mockReturnValue(mockEthereum); @@ -173,15 +185,20 @@ describe('GET /quote-swap', () => { }); it('should return a valid quote for BUY side', async () => { - // Update mock for BUY side - AlphaRouterQuoteResult format - mockGetAlphaRouterQuote.mockResolvedValue({ - route: { trade: { priceImpact: { toSignificant: () => '0.3' } } }, - inputAmount: '3000', - outputAmount: '1', + // Update mock for BUY side + mockGetQuote.mockResolvedValue({ + trade: { + inputAmount: { toExact: () => '3000' }, + outputAmount: { toExact: () => '1' }, + priceImpact: { toSignificant: () => '0.3' }, + }, + route: ['USDC', 'WETH'], + routePath: 'USDC -> WETH', priceImpact: 0.3, - routeString: 'USDC -> WETH', - gasEstimate: '300000', - gasEstimateUSD: '5.00', + estimatedGasUsed: { toString: () => '300000' }, + estimatedGasUsedQuoteToken: { toExact: () => '0.5' }, + quote: { toExact: () => '1' }, + quoteGasAdjusted: { toExact: () => '0.9995' }, methodParameters: { calldata: '0x1234567890', value: '0x0', diff --git a/test/connectors/uniswap/universal-router.test.ts b/test/connectors/uniswap/universal-router.test.ts index dd8732d6ab..c15ca02159 100644 --- a/test/connectors/uniswap/universal-router.test.ts +++ b/test/connectors/uniswap/universal-router.test.ts @@ -142,38 +142,5 @@ describe('UniversalRouterService', () => { expect(quote.methodParameters).toHaveProperty('to'); expect(quote.methodParameters.to).toBe('0x66a9893cc07d91d95644aedd05d03f95e1dba8af'); }); - - it('should format routePath with percentage and token symbols', async () => { - const amount = CurrencyAmount.fromRawAmount(WETH, '1000000000000000000'); - const options = { - slippageTolerance: new Percent(1, 100), - deadline: Math.floor(Date.now() / 1000) + 1800, - recipient: '0x0000000000000000000000000000000000000001', - protocols: [Protocol.V2], - }; - - // Mock a simple V2 pair - const mockContract = { - getReserves: jest - .fn() - .mockResolvedValue([ - BigNumber.from('1000000000000000000000'), - BigNumber.from('3000000000000'), - BigNumber.from('1234567890'), - ]), - token0: jest.fn().mockResolvedValue(WETH.address), - token1: jest.fn().mockResolvedValue(USDC.address), - }; - - (Contract as any).mockImplementation(() => mockContract); - - const quote = await universalRouter.getQuote(WETH, USDC, amount, TradeType.EXACT_INPUT, options); - - // Verify routePath format includes percentage and "via" - // Format should be like "100% via WETH -> USDC" or for multi-hop "100% via LINK -> WETH -> DAI" - expect(quote.routePath).toMatch(/^\d+% via .+$/); - expect(quote.routePath).toContain('% via'); - expect(quote.routePath).toContain('->'); - }); }); });