diff --git a/.eslintrc.js b/.eslintrc.js index 3398443860..58f501fb22 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -41,11 +41,10 @@ module.exports = { 'import/no-duplicates': 'warn' }, settings: { - 'import/resolver': { - 'node': { - 'extensions': ['.js', '.jsx', '.ts', '.tsx'] - } - } + 'import/resolver': [ + ['typescript', { alwaysTryTypes: true, project: './tsconfig.json' }], + ['node', { extensions: ['.js', '.jsx', '.ts', '.tsx'] }], + ], }, overrides: [ { diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000000..cb2c84d5c3 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm lint-staged diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..4a1d06adb6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,112 @@ +# AI Agent Instructions + +This file provides guidance to AI coding assistants when working with code in this repository. + +## Build & Command Reference +- Build: `pnpm build` +- Start server: `pnpm start --passphrase=` +- Start in dev mode: `pnpm start --passphrase= --dev` (HTTP mode, no SSL) +- Run all tests: `pnpm test` +- Run specific test file: `GATEWAY_TEST_MODE=dev jest --runInBand path/to/file.test.ts` +- Run tests with coverage: `pnpm test:cov` +- Lint code: `pnpm lint` +- Format code: `pnpm format` +- Type check: `pnpm typecheck` +- Initial setup: `pnpm setup` (creates configs and copies certificates) +- Clean install: `pnpm clean` (removes node_modules, coverage, logs, dist) + +## Architecture Overview + +### Gateway Pattern +- RESTful API gateway providing standardized endpoints for blockchain and DEX interactions +- Built with Fastify framework using TypeBox for schema validation +- Supports both HTTP (dev mode) and HTTPS (production) protocols +- Swagger documentation auto-generated at `/docs` (http://localhost:15888/docs in dev mode) + +### Module Organization +- **Chains**: Blockchain implementations (Ethereum, Solana) + - Each chain implements standard methods: balances, tokens, status, allowances + - Singleton pattern with network-specific instances via `getInstance()` + +- **Connectors**: DEX protocol implementations (Jupiter, Meteora, Raydium, Uniswap) + - Support for AMM (V2-style), CLMM (V3-style), and simple swap operations + - Each connector organized into operation-specific route files + - Standardized request/response schemas across all connectors + +### API Route Structure +- Chain routes: `/chains/{chain}/{operation}` +- Connector routes: `/connectors/{dex}/{type}/{operation}` +- Config routes: `/config/*` +- Wallet routes: `/wallet/*` + +## Coding Style Guidelines +- TypeScript with ESNext target and CommonJS modules +- 2-space indentation (no tabs) +- Single quotes for strings +- Semicolons required +- Arrow functions preferred over function declarations +- Explicit typing encouraged (TypeBox for API schemas) +- Unused variables prefixed with underscore (_variable) +- Error handling: Use Fastify's httpErrors for API errors + +## Project Structure +- `src/`: Source code + - `chains/`: Chain-specific implementations (ethereum, solana) + - `connectors/`: DEX and protocol connectors (jupiter, meteora, raydium, uniswap) + - `services/`: Core services and utilities + - `schemas/`: API schemas, interfaces and type definitions + - `json/`: JSON schema files + - `trading-types/`: Shared trading types (AMM, CLMM, swap) + - `config/`: Configuration-related routes and utils + - `wallet/`: Wallet management routes +- `test/`: Test files mirroring src structure + - `mocks/`: Mock data organized by module type +- `conf/`: Runtime configuration (created by setup) + - `lists/`: Token lists for each network + +## Best Practices +- Create tests for all new functionality (minimum 75% coverage for PRs) +- Use the logger for debug/errors (not console.log) +- Use Fastify's httpErrors for API error responses: + - `fastify.httpErrors.badRequest('Invalid input')` + - `fastify.httpErrors.notFound('Resource not found')` + - `fastify.httpErrors.internalServerError('Something went wrong')` +- Create route files in dedicated routes/ folders +- Define schemas using TypeBox +- Prefer async/await over promise chains +- Follow singleton pattern for chains/connectors + +## Adding New Features +- Follow existing patterns in chains/connectors directories +- Create corresponding test files with mock data +- Use TypeBox for all request/response schema definitions +- Register new routes in appropriate route files +- Update chain.routes.ts or connector.routes.ts to list new modules + +## Configuration +- Chain configs: `src/templates/{chain}.yml` +- Connector configs: `src/templates/{connector}.yml` +- Token lists: `src/templates/lists/{network}.json` +- All configs validated against JSON schemas in `src/templates/json/` + +## Supported Networks +### Ethereum Networks +- Mainnet, Sepolia, Arbitrum, Avalanche, Base, BSC, Celo, Optimism, Polygon, World Chain + +### Solana Networks +- Mainnet, Devnet + +## Supported DEX Connectors +- **Jupiter** (Solana): Token swaps via aggregator +- **Meteora** (Solana): CLMM operations +- **Raydium** (Solana): AMM and CLMM operations +- **Uniswap** (Ethereum/EVM): V2 AMM, V3 CLMM, and Universal Router swaps + +## Environment Variables +- `GATEWAY_PASSPHRASE`: Set passphrase for wallet encryption +- `GATEWAY_TEST_MODE=dev`: Run tests in development mode +- `START_SERVER=true`: Required to start the server +- `DEV=true`: Run in HTTP mode (Docker) + +## Hummingbot Gateway Endpoint Standardization +- This repo standardized DEX and chain endpoints that are used by Hummingbot strategies. See this branch for the matching code, especially the Gateway connector classes https://github.com/hummingbot/hummingbot/tree/development \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index d67c9fc168..0000000000 --- a/CLAUDE.md +++ /dev/null @@ -1,92 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Build & Command Reference -- Build: `pnpm build` -- Start server: `pnpm start` -- Start in dev mode: `pnpm start --dev` (HTTP mode, no SSL) -- Run all tests: `pnpm test` -- Run specific test file: `GATEWAY_TEST_MODE=dev jest --runInBand path/to/file.test.ts` -- Run tests with coverage: `pnpm test:cov` -- Lint code: `pnpm lint` -- Format code: `pnpm format` -- Type check: `pnpm typecheck` -- Initial setup: `pnpm setup` (creates configs and generates certificates) - -## Architecture Overview - -### Gateway Pattern -- RESTful API gateway providing standardized endpoints for blockchain and DEX interactions -- Built with Fastify framework using TypeBox for schema validation -- Supports both HTTP (dev mode) and HTTPS (production) protocols -- Swagger documentation auto-generated at `/docs` (http://localhost:15888/docs in dev mode) - -### Module Organization -- **Chains**: Blockchain implementations (Ethereum, Solana) - - Each chain implements standard methods: balances, tokens, status, allowances - - Singleton pattern with network-specific instances via `getInstance()` - -- **Connectors**: DEX protocol implementations (Jupiter, Meteora, Raydium, Uniswap) - - Support for AMM (V2-style), CLMM (V3-style), and simple swap operations - - Each connector organized into operation-specific route files - - Standardized request/response schemas across all connectors - -### API Route Structure -- Chain routes: `/chains/{chain}/{operation}` -- Connector routes: `/connectors/{dex}/{type}/{operation}` -- Config routes: `/config/*` -- Wallet routes: `/wallet/*` - -## Coding Style Guidelines -- TypeScript with ESNext target and CommonJS modules -- 2-space indentation (no tabs) -- Single quotes for strings -- Semicolons required -- Arrow functions preferred over function declarations -- Explicit typing encouraged (TypeBox for API schemas) -- Unused variables prefixed with underscore (_variable) -- Error handling: Use Fastify's httpErrors for API errors - -## Project Structure -- `src/`: Source code - - `chains/`: Chain-specific implementations (ethereum, solana) - - `connectors/`: DEX and protocol connectors (jupiter, meteora, raydium, uniswap) - - `services/`: Core services and utilities - - `schemas/`: API schemas, interfaces and type definitions - - `json/`: JSON schema files - - `trading-types/`: Shared trading types (AMM, CLMM, swap) - - `config/`: Configuration-related routes and utils - - `wallet/`: Wallet management routes -- `test/`: Test files mirroring src structure - - `mocks/`: Mock data organized by module type -- `conf/`: Runtime configuration (created by setup) - - `lists/`: Token lists for each network - -## Best Practices -- Create tests for all new functionality (minimum 75% coverage for PRs) -- Use the logger for debug/errors (not console.log) -- Use Fastify's httpErrors for API error responses: - - `fastify.httpErrors.badRequest('Invalid input')` - - `fastify.httpErrors.notFound('Resource not found')` - - `fastify.httpErrors.internalServerError('Something went wrong')` -- Create route files in dedicated routes/ folders -- Define schemas using TypeBox -- Prefer async/await over promise chains -- Follow singleton pattern for chains/connectors - -## Adding New Features -- Follow existing patterns in chains/connectors directories -- Create corresponding test files with mock data -- Use TypeBox for all request/response schema definitions -- Register new routes in appropriate route files -- Update chain.routes.ts or connector.routes.ts to list new modules - -## Configuration -- Chain configs: `src/templates/{chain}.yml` -- Connector configs: `src/templates/{connector}.yml` -- Token lists: `src/templates/lists/{network}.json` -- All configs validated against JSON schemas in `src/templates/json/` - -## Hummingbot Gateway Endpoint Standardization -- This repo standardized DEX and chain endpoints that are used by Hummingbot strategies. See this branch for the matching code, especially the Gateway connector classes https://github.com/hummingbot/hummingbot/tree/feat/gateway-2.6 \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000000..47dc3e3d86 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/GEMINI.md b/GEMINI.md new file mode 120000 index 0000000000..47dc3e3d86 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/README.md b/README.md index 796ecb63ed..49c482461e 100644 --- a/README.md +++ b/README.md @@ -4,27 +4,47 @@ ## Introduction -Hummingbot Gateway is an API/CLI client that exposes standardized REST endponts to perform actions and fetch data from **blockchain networks** (wallet, node & chain interaction) and their **decentralized exchanges (DEX)** (pricing, trading & liquidity provision). +Hummingbot Gateway is a REST API that provides a unified interface for interacting with **blockchain networks** (wallet, node & chain operations) and **decentralized exchanges (DEX)** (trading, liquidity provision, and market data). -### API Overview +Gateway abstracts the complexity of interacting with different blockchain protocols by providing standardized endpoints that work consistently across different chains and DEXs. Built with TypeScript to leverage native blockchain SDKs, it offers a language-agnostic API that can be integrated into any trading system. -- GET /chains - List all available blockchain networks and their supported networks -- GET /connectors - List all available DEX connectors and their supported networks -- GET /ethereum/... - Ethereum chain endpoints (balances, tokens, allowances) -- GET /solana/... - Solana chain endpoints (balances, tokens) -- GET /jupiter/... - Jupiter Aggregator swap endpoints -- GET /uniswap/... - Uniswap swap, AMM, and CLMM endpoints -- GET /uniswap/routes/quote-swap - Get price quote using Uniswap V3 Swap Router (recommended for token swapping) -- GET /uniswap/routes/execute-swap - Execute swap using Uniswap V3 Swap Router (recommended for token swapping) -- GET /raydium/amm/... - Raydium AMM endpoints -- GET /raydium/clmm/... - Raydium CLMM endpoints -- GET /meteora/clmm/... - Meteora CLMM endpoints +### Key Features -Gateway is written in Typescript in order to use Javascript-based SDKs provided by blockchains and DEX protocols. The advantage of using Gateway is it provides a standardized, language-agnostic approach to interacting with these protocols. +- **Multi-Chain Support**: Ethereum (and EVM-compatible chains) and Solana +- **DEX Integration**: Jupiter, Uniswap, Raydium, and Meteora +- **Trading Types**: Simple swaps, AMM (V2-style), and CLMM (V3-style concentrated liquidity) +- **Wallet Management**: Secure wallet storage and transaction signing +- **Swagger Documentation**: Auto-generated interactive API docs at `/docs` +- **TypeBox Validation**: Type-safe request/response schemas -Gateway may be used alongside the main [Hummingbot client](https://github.com/hummingbot/hummingbot) to enable trading and market making on DEXs, or as a standalone command line interface (CLI). +### Supported Networks -Gateway uses [Swagger](https://swagger.io/) for API documentation. When Gateway is started in HTTP mode, it automatically generates interactive Swagger API docs at: +#### Ethereum & EVM Networks +- Ethereum Mainnet +- Arbitrum +- Avalanche +- Base +- BSC (Binance Smart Chain) +- Celo +- Optimism +- Polygon +- World Chain +- Sepolia (testnet) + +#### Solana Networks +- Solana Mainnet +- Solana Devnet + +### Supported DEX Protocols + +| Protocol | Chain | Swap | AMM | CLMM | +|----------|-------|------|-----|------| +| Jupiter | Solana | ✅ | ❌ | ❌ | +| Meteora | Solana | ✅ | ❌ | ✅ | +| Raydium | Solana | ✅ | ✅ | ✅ | +| Uniswap | Ethereum/EVM | ✅ | ✅ | ✅ | + +Gateway uses [Swagger](https://swagger.io/) for API documentation. When running in development mode, access the interactive API documentation at: ## Installation from Source @@ -145,37 +165,58 @@ docker run --name gateway \ Afterwards, client may connect to Gateway at: and you can access the Swagger documentation UI at: -## CLI Commands +## API Endpoints Overview -When running Gateway from source, it provides a CLI interface for interacting with chains and DEXs. After installing from source, you can enable the `gateway` command by linking the CLI globally: -```bash -pnpm link --global -``` +### System Endpoints +- `GET /` - Health check +- `GET /chains` - List supported blockchains +- `GET /connectors` - List supported DEX connectors -Afterwards, you can use the `gateway` command to see available commands: -```bash -gateway -``` +### Configuration Management +- `GET /config` - Get configuration +- `POST /config/update` - Update configuration -Sample commands: -```bash -# Check wallet balances (requires running server) -gateway balance --chain solana --wallet +### Wallet Management +- `GET /wallet` - List all wallets +- `POST /wallet/add` - Add new wallet +- `DELETE /wallet/remove` - Remove wallet +- `POST /wallet/sign` - Sign message -# Build project from source (same as pnpm build) -gateway build +### Chain Operations -# Start the API server (same as pnpm start) -gateway start --passphrase= [--dev] +#### Ethereum/EVM (`/chains/ethereum`) +- `GET /status` - Chain connection status +- `GET /tokens` - Get token information +- `GET /balances` - Get wallet balances +- `GET /allowances` - Check token allowances +- `POST /approve` - Approve token spending +- `GET /poll` - Poll transaction status -# Get command help -gateway help [COMMAND] -``` +#### Solana (`/chains/solana`) +- `GET /status` - Chain connection status +- `GET /tokens` - Get token information +- `GET /balances` - Get wallet balances +- `GET /poll` - Poll transaction status -**Note:** Similar to the server, CLI commands require a `passphrase` argument used to encrypt and decrypt wallets used in executing transactions. Set the passphrase using the `--passphrase` argument when starting the server or by setting the `GATEWAY_PASSPHRASE` environment variable: -```bash -export GATEWAY_PASSPHRASE= -``` +### DEX Trading Endpoints + +#### Simple Swaps +- `GET /connectors/{dex}/quote-swap` - Get swap quote +- `POST /connectors/{dex}/execute-swap` - Execute swap + +#### AMM Operations (Uniswap V2, Raydium) +- `GET /connectors/{dex}/amm/pool-info` - Pool information +- `GET /connectors/{dex}/amm/position-info` - LP position details +- `POST /connectors/{dex}/amm/add-liquidity` - Add liquidity +- `POST /connectors/{dex}/amm/remove-liquidity` - Remove liquidity + +#### CLMM Operations (Uniswap V3, Raydium, Meteora) +- `GET /connectors/{dex}/clmm/pool-info` - Pool information +- `GET /connectors/{dex}/clmm/positions-owned` - List positions +- `POST /connectors/{dex}/clmm/open-position` - Open position +- `POST /connectors/{dex}/clmm/add-liquidity` - Add to position +- `POST /connectors/{dex}/clmm/remove-liquidity` - Remove from position +- `POST /connectors/{dex}/clmm/collect-fees` - Collect fees ## Contribution @@ -201,39 +242,65 @@ Here are some ways that you can contribute to Gateway: ## Architecture -Gateway follows a modular architecture with clear separation of concerns between chains, connectors, configuration, and wallet management: +Gateway follows a modular architecture with clear separation between chains, connectors, and core services: -- **Chains**: Blockchain network implementations - - [src/chains/chain.routes.ts](./src/chains/chain.routes.ts): List of supported chains and networks - - [src/chains/ethereum/ethereum.ts](./src/chains/ethereum/ethereum.ts): Core Ethereum chain operations - - [src/chains/solana/solana.ts](./src/chains/solana/solana.ts): Core Solana chain operations +### Directory Structure -- **Connectors**: DEX protocol implementations - - [src/connectors/connector.routes.ts](./src/connectors/connector.routes.ts): List of available DEX connectors - - [src/connectors/jupiter/jupiter.ts](./src/connectors/jupiter/jupiter.ts): Jupiter DEX connector for Solana - - [src/connectors/raydium/raydium.ts](./src/connectors/raydium/raydium.ts): Raydium DEX connector for Solana (AMM, CLMM) - - [src/connectors/uniswap/uniswap.ts](./src/connectors/uniswap/uniswap.ts): Uniswap DEX connector for Ethereum - - [src/connectors/uniswap/routes/quote-swap.ts](./src/connectors/uniswap/routes/quote-swap.ts): Uniswap V3 Swap Router for quote generation - - [src/connectors/uniswap/routes/execute-swap.ts](./src/connectors/uniswap/routes/execute-swap.ts): Uniswap V3 Swap Router for swap execution - - [src/connectors/uniswap/uniswap.contracts.ts](./src/connectors/uniswap/uniswap.contracts.ts): Contract addresses for Uniswap on all networks - -- **Configuration**: Configuration management - - [src/config/config.routes.ts](./src/config/config.routes.ts): Configuration endpoints - - [src/config/utils.ts](./src/config/utils.ts): Configuration utilities - -- **Wallet**: Wallet management - - [src/wallet/wallet.routes.ts](./src/wallet/wallet.routes.ts): Wallet endpoints - - [src/wallet/utils.ts](./src/wallet/utils.ts): Wallet utilities - -- **Schemas**: Common type definitions and schemas - - [src/schemas/trading-types/clmm-schema.ts](./src/schemas/trading-types/clmm-schema.ts): Standard schemas for CLMM operations - - [src/schemas/trading-types/amm-schema.ts](./src/schemas/trading-types/amm-schema.ts): Standard schemas for AMM operations - - [src/schemas/trading-types/swap-schema.ts](./src/schemas/trading-types/swap-schema.ts): Standard schemas for swap operations +``` +src/ +├── chains/ # Blockchain implementations +│ ├── ethereum/ # Ethereum and EVM chains +│ │ ├── ethereum.ts # Core chain class +│ │ ├── routes/ # Individual route handlers +│ │ └── ethereum.routes.ts +│ └── solana/ # Solana chain +│ ├── solana.ts +│ ├── routes/ +│ └── solana.routes.ts +├── connectors/ # DEX implementations +│ ├── jupiter/ # Jupiter aggregator (Solana) +│ ├── meteora/ # Meteora CLMM (Solana) +│ ├── raydium/ # Raydium AMM/CLMM (Solana) +│ └── uniswap/ # Uniswap V2/V3 (Ethereum) +├── schemas/ # TypeBox schemas +│ ├── trading-types/ # AMM, CLMM, Swap schemas +│ └── json/ # JSON schemas +├── services/ # Core services +├── wallet/ # Wallet management +└── config/ # Configuration management +``` -- **Services**: Core functionality and utilities - - [src/services/config-manager-v2.ts](./src/services/config-manager-v2.ts): Configuration management - - [src/services/logger.ts](./src/services/logger.ts): Logging utilities - - [src/services/base.ts](./src/services/base.ts): Base service functionality +### Design Patterns + +- **Singleton Pattern**: Chains and connectors use singleton instances per network +- **Route Organization**: Each module has a dedicated routes folder with operation-specific files +- **Schema Validation**: All API requests/responses validated with TypeBox schemas +- **Error Handling**: Consistent error responses using Fastify's httpErrors + +### Key Components + +#### Chains +Handle blockchain interactions including: +- Wallet balance queries +- Token information and lists +- Transaction submission and monitoring +- Gas estimation +- Token approvals (EVM only) + +#### Connectors +Implement DEX-specific logic for: +- Price quotes and routing +- Swap execution +- Liquidity pool operations +- Position management (CLMM) +- Fee collection + +#### Services +Provide shared functionality: +- Configuration management with YAML/JSON schemas +- Structured logging with Winston +- Database operations with LevelDB +- API server setup with Fastify ## Testing @@ -283,78 +350,77 @@ The test directory is organized as follows: For more details on the test setup and structure, see [Test README](./test/README.md). -## Adding a New Chain or Connector +## Development Guide ### Adding a New Chain -1. Create chain implementation files: - ```bash - mkdir -p src/chains/yourchain/routes - touch src/chains/yourchain/yourchain.ts - touch src/chains/yourchain/yourchain.config.ts - touch src/chains/yourchain/yourchain.routes.ts - touch src/chains/yourchain/yourchain.utils.ts +1. **Create chain implementation**: + ```typescript + // src/chains/mychain/mychain.ts + export class MyChain extends ChainBase { + private static instances: Record = {}; + + public static getInstance(network: string): MyChain { + if (!MyChain.instances[network]) { + MyChain.instances[network] = new MyChain(network); + } + return MyChain.instances[network]; + } + } ``` -2. Create test mock files: - ```bash - mkdir -p test/mocks/chains/yourchain - touch test/mocks/chains/yourchain/balance.json - touch test/mocks/chains/yourchain/status.json - touch test/mocks/chains/yourchain/tokens.json - ``` +2. **Implement required methods**: + - `getWallet(address: string)` + - `getBalance(address: string)` + - `getTokens(tokenSymbols: string[])` + - `getStatus()` -3. Create chain test file: - ```bash - touch test/chains/yourchain.test.js - ``` +3. **Create route handlers** in `src/chains/mychain/routes/` + +4. **Add configuration**: + - Create `src/templates/mychain.yml` + - Add JSON schema in `src/templates/json/mychain-schema.json` + +5. **Register the chain** in `src/chains/chain.routes.ts` ### Adding a New Connector -1. Create connector implementation files: - ```bash - mkdir -p src/connectors/yourconnector/routes - touch src/connectors/yourconnector/yourconnector.ts - touch src/connectors/yourconnector/yourconnector.config.ts - touch src/connectors/yourconnector/yourconnector.routes.ts +1. **Choose the appropriate base class**: + - For AMM: Extend from AMM base functionality + - For CLMM: Implement CLMM interface + - For simple swaps: Implement basic swap methods + +2. **Create connector class**: + ```typescript + // src/connectors/mydex/mydex.ts + export class MyDex { + private static instances: Record = {}; + + public static getInstance(chain: string, network: string): MyDex { + const key = `${chain}:${network}`; + if (!MyDex.instances[key]) { + MyDex.instances[key] = new MyDex(chain, network); + } + return MyDex.instances[key]; + } + } ``` -2. If the connector supports AMM, create these files: - ```bash - mkdir -p src/connectors/yourconnector/amm-routes - touch src/connectors/yourconnector/amm-routes/executeSwap.ts - touch src/connectors/yourconnector/amm-routes/poolInfo.ts - touch src/connectors/yourconnector/amm-routes/quoteSwap.ts - # Add other AMM operation files as needed - ``` +3. **Implement trading methods** based on supported operations -3. If the connector supports CLMM, create these files: - ```bash - mkdir -p src/connectors/yourconnector/clmm-routes - touch src/connectors/yourconnector/clmm-routes/executeSwap.ts - touch src/connectors/yourconnector/clmm-routes/poolInfo.ts - touch src/connectors/yourconnector/clmm-routes/quoteSwap.ts - touch src/connectors/yourconnector/clmm-routes/openPosition.ts - # Add other CLMM operation files as needed - ``` +4. **Create route files** following the pattern: + - Swap routes in `routes/` + - AMM routes in `amm-routes/` + - CLMM routes in `clmm-routes/` -4. Create test mock files: - ```bash - mkdir -p test/mocks/connectors/yourconnector - touch test/mocks/connectors/yourconnector/amm-pool-info.json - touch test/mocks/connectors/yourconnector/amm-quote-swap.json - touch test/mocks/connectors/yourconnector/clmm-pool-info.json - touch test/mocks/connectors/yourconnector/clmm-quote-swap.json - # Add other mock response files as needed - ``` +5. **Add configuration and register** in `src/connectors/connector.routes.ts` -5. Create connector test files: - ```bash - mkdir -p test/connectors/yourconnector - touch test/connectors/yourconnector/amm.test.js - touch test/connectors/yourconnector/clmm.test.js - touch test/connectors/yourconnector/swap.test.js - ``` +### Testing Requirements + +- Minimum 75% code coverage for new features +- Create mock responses in `test/mocks/` +- Write unit tests for all route handlers +- Test error cases and edge conditions ## Linting and Formatting diff --git a/jest.config.js b/jest.config.js index 18d914f8dc..c409deece5 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,6 @@ +const { pathsToModuleNameMapper } = require('ts-jest'); +const { compilerOptions } = require('./tsconfig.json'); + module.exports = { preset: 'ts-jest', testEnvironment: 'node', @@ -12,13 +15,21 @@ module.exports = { 'src/connectors/uniswap/uniswap.ts', 'src/connectors/uniswap/uniswap.lp.helper.ts', 'src/network/network.controllers.ts', - 'src/services/ethereum-base.ts', - 'src/services/telemetry-transport.ts', 'test/*', ], modulePathIgnorePatterns: ['/dist/'], setupFilesAfterEnv: ['/test/jest-setup.js'], - moduleNameMapper: { + testPathIgnorePatterns: [ + '/node_modules/', + 'test-helpers', + '/test-scripts/', + ], + testMatch: ['/test/**/*.test.ts', '/test/**/*.test.js'], + transform: { + '^.+\\.tsx?$': 'ts-jest', }, - testPathIgnorePatterns: ['/node_modules/', 'test-helpers'], + transformIgnorePatterns: ['/node_modules/(?!.*superjson)'], + moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { + prefix: '/', + }), }; diff --git a/package.json b/package.json index 000c30f6f9..0a013df193 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,23 @@ { "name": "gateway", - "version": "2.6.0", + "version": "2.7.0", "description": "Hummingbot Gateway is a CLI/API client that helps you interact with DEXs on various blockchains.", "main": "index.js", "license": "Apache-2.0", "repository": "https://github.com/hummingbot/gateway", + "engines": { + "node": ">=20.0.0" + }, + "imports": { + "#src": "./src/index.ts", + "#src/*": "./src/*", + "#test": "./test", + "#test/*": "./test/*" + }, "scripts": { "prebuild": "rimraf dist && mkdir dist", - "build": "tsc --skipLibCheck --sourceMap --project ./ && pnpm run copy-files", - "clean": "rm -rf ./node_modules && rm -rf ./coverage && rm -rf ./logs", + "build": "tsc --project tsconfig.build.json && tsc-alias -p tsconfig.build.json && pnpm run copy-files", + "clean": "rm -rf ./node_modules && rm -rf ./coverage && rm -rf ./logs && rm -rf ./dist", "clean:config": "find ./conf -maxdepth 1 -type f -delete", "format": "prettier . --write", "lint": "eslint src test --format table --fix", @@ -16,13 +25,15 @@ "start": "START_SERVER=true node dist/index.js", "copy-files": "copyfiles 'src/templates/json/*.json' 'src/templates/*.yml' 'src/templates/lists/*.json' dist", "test": "GATEWAY_TEST_MODE=dev jest --verbose", + "test:clear-cache": "jest --clearCache", "test:debug": "GATEWAY_TEST_MODE=dev jest --watch --runInBand", "test:unit": "GATEWAY_TEST_MODE=dev jest --runInBand ./test/", "test:cov": "GATEWAY_TEST_MODE=dev jest --runInBand --coverage ./test/", "test:scripts": "GATEWAY_TEST_MODE=dev jest --runInBand ./test-scripts/*.test.ts", "cli": "node dist/index.js", "typecheck": "tsc --noEmit", - "rebuild-bigint": "cd node_modules/bigint-buffer && npm run rebuild" + "rebuild-bigint": "cd node_modules/bigint-buffer && pnpm run rebuild", + "prepare": "husky install" }, "dependencies": { "@coral-xyz/anchor": "^0.29.0", @@ -62,6 +73,7 @@ "ajv": "^8.17.1", "app-root-path": "^3.1.0", "axios": "^1.8.4", + "bigint-buffer": "1.1.5", "bn.js": "5.2.1", "brotli": "1.3.2", "dayjs": "^1.11.13", @@ -116,6 +128,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-config-standard": "^17.1.0", "eslint-formatter-table": "^7.32.1", + "eslint-import-resolver-typescript": "4.4.3", "eslint-plugin-import": "^2.31.0", "eslint-plugin-n": "^16.6.2", "eslint-plugin-prettier": "^5.2.5", @@ -127,6 +140,7 @@ "jest": "^29.7.0", "jest-extended": "^0.11.5", "jsbi": "^3.2.5", + "lint-staged": "^16.1.2", "mock-ethers-provider": "^1.0.2", "node-gyp": "^11.2.0", "nodemon": "^2.0.22", @@ -138,7 +152,8 @@ "ts-jest": "^29.3.0", "ts-node": "^10.9.2", "typescript": "^5.8.2", - "viem": "^0.3.50" + "viem": "^0.3.50", + "tsc-alias": "^1.8.8" }, "resolutions": { "@types/bn.js": "5.1.6", @@ -153,6 +168,12 @@ "/dist", "/src/commands" ], + "lint-staged": { + "{src,test}/**/*.{ts,js}": "eslint --fix", + "**/*.json": [ + "prettier --write" + ] + }, "oclif": { "bin": "gateway", "dirname": "gateway", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ecebb7cf2..6ace51f266 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -128,6 +128,9 @@ importers: axios: specifier: ^1.8.4 version: 1.9.0 + bigint-buffer: + specifier: 1.1.5 + version: 1.1.5 bn.js: specifier: 5.2.1 version: 5.2.1 @@ -278,13 +281,16 @@ importers: version: 9.1.0(eslint@8.57.1) eslint-config-standard: specifier: ^17.1.0 - version: 17.1.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint-plugin-n@16.6.2(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1) + version: 17.1.0(eslint-plugin-import@2.31.0)(eslint-plugin-n@16.6.2(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1) eslint-formatter-table: specifier: ^7.32.1 version: 7.32.1 + eslint-import-resolver-typescript: + specifier: 4.4.3 + version: 4.4.3(eslint-plugin-import@2.31.0)(eslint@8.57.1) eslint-plugin-import: specifier: ^2.31.0 - version: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1) + version: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@4.4.3)(eslint@8.57.1) eslint-plugin-n: specifier: ^16.6.2 version: 16.6.2(eslint@8.57.1) @@ -315,6 +321,9 @@ importers: jsbi: specifier: ^3.2.5 version: 3.2.5 + lint-staged: + specifier: ^16.1.2 + version: 16.1.2 mock-ethers-provider: specifier: ^1.0.2 version: 1.0.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) @@ -345,6 +354,9 @@ importers: ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@15.14.9)(typescript@5.8.3) + tsc-alias: + specifier: ^1.8.8 + version: 1.8.16 typescript: specifier: ^5.8.2 version: 5.8.3 @@ -726,6 +738,15 @@ packages: '@dabh/diagnostics@2.0.3': resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + '@emnapi/core@1.4.3': + resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} + + '@emnapi/runtime@1.4.3': + resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} + + '@emnapi/wasi-threads@1.0.2': + resolution: {integrity: sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==} + '@eslint-community/eslint-utils@4.7.0': resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1120,6 +1141,9 @@ packages: resolution: {integrity: sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==} engines: {node: '>=4'} + '@napi-rs/wasm-runtime@0.2.11': + resolution: {integrity: sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==} + '@noble/curves@1.0.0': resolution: {integrity: sha512-2upgEu0iLiDVDZkNLeFV2+ht0BAVgQnEmCk6JsOch9Rp8xfkMCbvbAZlA2pBHQc73dbl+vFOXfqkf4uemdn0bw==} @@ -1805,6 +1829,9 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@tybys/wasm-util@0.9.0': + resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + '@types/abstract-leveldown@7.2.5': resolution: {integrity: sha512-/2B0nQF4UdupuxeKTJA2+Rj1D+uDemo6P4kMwKCpbfpnzeVaWSELTsAw4Lxn3VJD6APtRrZOCuYo+4nHUQfTfg==} @@ -2125,6 +2152,101 @@ packages: resolution: {integrity: sha512-so3c/CmaRmRSvgKFyrUWy6DCSogyzyVaoYCec/TJ4k2hXlJ8MK4vumcuxtmRr1oMnZ5KmaCPBS12Knb4FC3nsw==} engines: {node: '>=14'} + '@unrs/resolver-binding-android-arm-eabi@1.9.2': + resolution: {integrity: sha512-tS+lqTU3N0kkthU+rYp0spAYq15DU8ld9kXkaKg9sbQqJNF+WPMuNHZQGCgdxrUOEO0j22RKMwRVhF1HTl+X8A==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.9.2': + resolution: {integrity: sha512-MffGiZULa/KmkNjHeuuflLVqfhqLv1vZLm8lWIyeADvlElJ/GLSOkoUX+5jf4/EGtfwrNFcEaB8BRas03KT0/Q==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.9.2': + resolution: {integrity: sha512-dzJYK5rohS1sYl1DHdJ3mwfwClJj5BClQnQSyAgEfggbUwA9RlROQSSbKBLqrGfsiC/VyrDPtbO8hh56fnkbsQ==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.9.2': + resolution: {integrity: sha512-gaIMWK+CWtXcg9gUyznkdV54LzQ90S3X3dn8zlh+QR5Xy7Y+Efqw4Rs4im61K1juy4YNb67vmJsCDAGOnIeffQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.9.2': + resolution: {integrity: sha512-S7QpkMbVoVJb0xwHFwujnwCAEDe/596xqY603rpi/ioTn9VDgBHnCCxh+UFrr5yxuMH+dliHfjwCZJXOPJGPnw==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.9.2': + resolution: {integrity: sha512-+XPUMCuCCI80I46nCDFbGum0ZODP5NWGiwS3Pj8fOgsG5/ctz+/zzuBlq/WmGa+EjWZdue6CF0aWWNv84sE1uw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.9.2': + resolution: {integrity: sha512-sqvUyAd1JUpwbz33Ce2tuTLJKM+ucSsYpPGl2vuFwZnEIg0CmdxiZ01MHQ3j6ExuRqEDUCy8yvkDKvjYFPb8Zg==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.9.2': + resolution: {integrity: sha512-UYA0MA8ajkEDCFRQdng/FVx3F6szBvk3EPnkTTQuuO9lV1kPGuTB+V9TmbDxy5ikaEgyWKxa4CI3ySjklZ9lFA==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.9.2': + resolution: {integrity: sha512-P/CO3ODU9YJIHFqAkHbquKtFst0COxdphc8TKGL5yCX75GOiVpGqd1d15ahpqu8xXVsqP4MGFP2C3LRZnnL5MA==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.9.2': + resolution: {integrity: sha512-uKStFlOELBxBum2s1hODPtgJhY4NxYJE9pAeyBgNEzHgTqTiVBPjfTlPFJkfxyTjQEuxZbbJlJnMCrRgD7ubzw==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.9.2': + resolution: {integrity: sha512-LkbNnZlhINfY9gK30AHs26IIVEZ9PEl9qOScYdmY2o81imJYI4IMnJiW0vJVtXaDHvBvxeAgEy5CflwJFIl3tQ==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.9.2': + resolution: {integrity: sha512-vI+e6FzLyZHSLFNomPi+nT+qUWN4YSj8pFtQZSFTtmgFoxqB6NyjxSjAxEC1m93qn6hUXhIsh8WMp+fGgxCoRg==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.9.2': + resolution: {integrity: sha512-sSO4AlAYhSM2RAzBsRpahcJB1msc6uYLAtP6pesPbZtptF8OU/CbCPhSRW6cnYOGuVmEmWVW5xVboAqCnWTeHQ==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.9.2': + resolution: {integrity: sha512-jkSkwch0uPFva20Mdu8orbQjv2A3G88NExTN2oPTI1AJ+7mZfYW3cDCTyoH6OnctBKbBVeJCEqh0U02lTkqD5w==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.9.2': + resolution: {integrity: sha512-Uk64NoiTpQbkpl+bXsbeyOPRpUoMdcUqa+hDC1KhMW7aN1lfW8PBlBH4mJ3n3Y47dYE8qi0XTxy1mBACruYBaw==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-wasm32-wasi@1.9.2': + resolution: {integrity: sha512-EpBGwkcjDicjR/ybC0g8wO5adPNdVuMrNalVgYcWi+gYtC1XYNuxe3rufcO7dA76OHGeVabcO6cSkPJKVcbCXQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.9.2': + resolution: {integrity: sha512-EdFbGn7o1SxGmN6aZw9wAkehZJetFPao0VGZ9OMBwKx6TkvDuj6cNeLimF/Psi6ts9lMOe+Dt6z19fZQ9Ye2fw==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.9.2': + resolution: {integrity: sha512-JY9hi1p7AG+5c/dMU8o2kWemM8I6VZxfGwn1GCtf3c5i+IKcMo2NQ8OjZ4Z3/itvY/Si3K10jOBQn7qsD/whUA==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.9.2': + resolution: {integrity: sha512-ryoo+EB19lMxAd80ln9BVf8pdOAxLb97amrQ3SFN9OCRn/5M5wvwDgAe4i8ZjhpbiHoDeP8yavcTEnpKBo7lZg==} + cpu: [x64] + os: [win32] + '@wagmi/chains@1.0.0': resolution: {integrity: sha512-eNbqRWyHbivcMNq5tbXJks4NaOzVLHnNQauHPeE/EDT9AlpqzcrMc+v2T1/2Iw8zN4zgqB86NCsxeJHJs7+xng==} peerDependencies: @@ -2247,6 +2369,10 @@ packages: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} + ansi-escapes@7.0.0: + resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} + engines: {node: '>=18'} + ansi-regex@2.1.1: resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} engines: {node: '>=0.10.0'} @@ -2778,6 +2904,10 @@ packages: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + cli-progress@3.12.0: resolution: {integrity: sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==} engines: {node: '>=4'} @@ -2786,6 +2916,10 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} + cli-truncate@4.0.0: + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} + engines: {node: '>=18'} + cli-width@3.0.0: resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} engines: {node: '>= 10'} @@ -2866,6 +3000,10 @@ packages: resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} engines: {node: '>=18'} + commander@14.0.0: + resolution: {integrity: sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==} + engines: {node: '>=20'} + commander@2.11.0: resolution: {integrity: sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==} @@ -2879,6 +3017,10 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + complex.js@2.4.2: resolution: {integrity: sha512-qtx7HRhPGSCBtGiST4/WGHuW+zeaND/6Ld+db6PbrulIB1i2Ev/2UPiqcmpQNPSyfBKraC0EOvOKCB5dGZKt3g==} @@ -3036,6 +3178,15 @@ packages: supports-color: optional: true + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decamelize@1.2.0: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} @@ -3205,6 +3356,9 @@ packages: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -3236,6 +3390,10 @@ packages: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + err-code@2.0.3: resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} @@ -3326,9 +3484,31 @@ packages: resolution: {integrity: sha512-JYC49hAJMNjLfbgXVeQHU6ngP0M8ThgXCHLGrncYB+R/RHEhRPnLxHjolTJdb7RdQ8zcCt2F7Mrt6Ou3PwMOHw==} engines: {node: ^10.12.0 || >=12.0.0} + eslint-import-context@0.1.9: + resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + peerDependencies: + unrs-resolver: ^1.0.0 + peerDependenciesMeta: + unrs-resolver: + optional: true + eslint-import-resolver-node@0.3.9: resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + eslint-import-resolver-typescript@4.4.3: + resolution: {integrity: sha512-elVDn1eWKFrWlzxlWl9xMt8LltjKl161Ix50JFC50tHXI5/TRP32SNEqlJ/bo/HV+g7Rou/tlPQU2AcRtIhrOg==} + engines: {node: ^16.17.0 || >=18.6.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + eslint-module-utils@2.12.0: resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} engines: {node: '>=4'} @@ -3791,6 +3971,10 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.3.0: + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + engines: {node: '>=18'} + get-func-name@2.0.2: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} @@ -3825,6 +4009,9 @@ packages: get-tsconfig@4.10.0: resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + get-value@2.0.6: resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} engines: {node: '>=0.10.0'} @@ -4164,6 +4351,9 @@ packages: resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} engines: {node: '>=6'} + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} @@ -4225,6 +4415,14 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + + is-fullwidth-code-point@5.0.0: + resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} + engines: {node: '>=18'} + is-generator-fn@2.1.0: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} engines: {node: '>=6'} @@ -4728,6 +4926,15 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + lint-staged@16.1.2: + resolution: {integrity: sha512-sQKw2Si2g9KUZNY3XNvRuDq4UJqpHwF0/FQzZR2M7I5MvtpWvibikCjUVJzZdGE0ByurEl3KQNvsGetd1ty1/Q==} + engines: {node: '>=20.17'} + hasBin: true + + listr2@8.3.3: + resolution: {integrity: sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==} + engines: {node: '>=18.0.0'} + locate-path@3.0.0: resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} engines: {node: '>=6'} @@ -4759,6 +4966,10 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + logform@2.7.0: resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} engines: {node: '>= 12.0.0'} @@ -4894,6 +5105,10 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} @@ -5009,9 +5224,17 @@ packages: resolution: {integrity: sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==} engines: {node: '>=0.8.0'} + mylas@2.1.13: + resolution: {integrity: sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg==} + engines: {node: '>=12.0.0'} + nan@2.22.2: resolution: {integrity: sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==} + nano-spawn@1.0.2: + resolution: {integrity: sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==} + engines: {node: '>=20.17'} + nanomatch@1.2.13: resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==} engines: {node: '>=0.10.0'} @@ -5019,6 +5242,11 @@ packages: napi-macros@2.2.2: resolution: {integrity: sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g==} + napi-postinstall@0.2.5: + resolution: {integrity: sha512-kmsgUvCRIJohHjbZ3V8avP0I1Pekw329MVAMDzVxsrkjgdnqiwvMX5XwR+hWV66vsAtZ+iM+fVnq8RTQawUmCQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -5285,6 +5513,10 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + openapi-types@12.1.3: resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} @@ -5439,6 +5671,11 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + pino-abstract-transport@2.0.0: resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} @@ -5461,6 +5698,10 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + plimit-lit@1.6.1: + resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} + engines: {node: '>=12'} + pnpm@10.10.0: resolution: {integrity: sha512-1hXbJG/nDyXc/qbY1z3ueCziPiJF48T2+Igkn7VoFJMYY33Kc8LFyO8qTKDVZX+5VnGIv6tH9WbR7mzph4FcOQ==} engines: {node: '>=18.12'} @@ -5556,6 +5797,10 @@ packages: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} + queue-lit@1.5.2: + resolution: {integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==} + engines: {node: '>=12'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -5687,6 +5932,10 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + ret@0.1.15: resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==} engines: {node: '>=0.12'} @@ -5920,6 +6169,14 @@ packages: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} + slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + + slice-ansi@7.1.0: + resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} + engines: {node: '>=18'} + smart-buffer@4.2.0: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} @@ -6002,6 +6259,14 @@ packages: resolution: {integrity: sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==} engines: {node: ^18.17.0 || >=20.5.0} + stable-hash-x@0.1.1: + resolution: {integrity: sha512-l0x1D6vhnsNUGPFVDx45eif0y6eedVC8nm5uACTrVFJFtl2mLRW17aWtVyxFCpn5t94VUPkjU8vSLwIuwwqtJQ==} + engines: {node: '>=12.0.0'} + + stable-hash-x@0.2.0: + resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} + engines: {node: '>=12.0.0'} + stack-trace@0.0.10: resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} @@ -6053,6 +6318,10 @@ packages: stream-transform@3.3.3: resolution: {integrity: sha512-dALXrXe+uq4aO5oStdHKlfCM/b3NBdouigvxVPxCdrMRAU6oHh3KNss20VbTPQNQmjAHzZGKGe66vgwegFEIog==} + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} @@ -6073,6 +6342,10 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + string.prototype.trim@1.2.10: resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} engines: {node: '>= 0.4'} @@ -6231,6 +6504,10 @@ packages: resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + tmp-promise@3.0.3: resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} @@ -6337,6 +6614,11 @@ packages: '@swc/wasm': optional: true + tsc-alias@1.8.16: + resolution: {integrity: sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==} + engines: {node: '>=16.20.2'} + hasBin: true + tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} @@ -6447,6 +6729,9 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + unrs-resolver@1.9.2: + resolution: {integrity: sha512-VUyWiTNQD7itdiMuJy+EuLEErLj3uwX/EpHQF8EOf33Dq3Ju6VW1GXm+swk6+1h7a49uv9fKZ+dft9jU7esdLA==} + unset-value@1.0.0: resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==} engines: {node: '>=0.10.0'} @@ -6621,6 +6906,10 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrap-ansi@9.0.0: + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -6729,6 +7018,11 @@ packages: engines: {node: '>= 14'} hasBin: true + yaml@2.8.0: + resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@11.1.1: resolution: {integrity: sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==} @@ -7565,6 +7859,22 @@ snapshots: enabled: 2.0.0 kuler: 2.0.0 + '@emnapi/core@1.4.3': + dependencies: + '@emnapi/wasi-threads': 1.0.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.4.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.0.2': + dependencies: + tslib: 2.8.1 + optional: true + '@eslint-community/eslint-utils@4.7.0(eslint@8.57.1)': dependencies: eslint: 8.57.1 @@ -8507,6 +8817,13 @@ snapshots: call-me-maybe: 1.0.2 glob-to-regexp: 0.3.0 + '@napi-rs/wasm-runtime@0.2.11': + dependencies: + '@emnapi/core': 1.4.3 + '@emnapi/runtime': 1.4.3 + '@tybys/wasm-util': 0.9.0 + optional: true + '@noble/curves@1.0.0': dependencies: '@noble/hashes': 1.3.0 @@ -9574,6 +9891,11 @@ snapshots: '@tsconfig/node16@1.0.4': {} + '@tybys/wasm-util@0.9.0': + dependencies: + tslib: 2.8.1 + optional: true + '@types/abstract-leveldown@7.2.5': {} '@types/app-root-path@1.2.8': {} @@ -10055,6 +10377,65 @@ snapshots: transitivePeerDependencies: - hardhat + '@unrs/resolver-binding-android-arm-eabi@1.9.2': + optional: true + + '@unrs/resolver-binding-android-arm64@1.9.2': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.9.2': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.9.2': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.9.2': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.9.2': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.9.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.9.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.9.2': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.9.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.9.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.9.2': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.9.2': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.9.2': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.9.2': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.9.2': + dependencies: + '@napi-rs/wasm-runtime': 0.2.11 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.9.2': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.9.2': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.9.2': + optional: true + '@wagmi/chains@1.0.0(typescript@5.8.3)': optionalDependencies: typescript: 5.8.3 @@ -10174,6 +10555,10 @@ snapshots: dependencies: type-fest: 0.21.3 + ansi-escapes@7.0.0: + dependencies: + environment: 1.1.0 + ansi-regex@2.1.1: {} ansi-regex@3.0.1: {} @@ -10844,12 +11229,21 @@ snapshots: dependencies: restore-cursor: 3.1.0 + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + cli-progress@3.12.0: dependencies: string-width: 4.2.3 cli-spinners@2.9.2: {} + cli-truncate@4.0.0: + dependencies: + slice-ansi: 5.0.0 + string-width: 7.2.0 + cli-width@3.0.0: {} cliui@4.1.0: @@ -10929,6 +11323,8 @@ snapshots: commander@13.1.0: {} + commander@14.0.0: {} + commander@2.11.0: {} commander@2.15.1: {} @@ -10937,6 +11333,8 @@ snapshots: commander@8.3.0: {} + commander@9.5.0: {} + complex.js@2.4.2: {} component-emitter@1.3.1: {} @@ -11104,6 +11502,10 @@ snapshots: optionalDependencies: supports-color: 8.1.1 + debug@4.4.1: + dependencies: + ms: 2.1.3 + decamelize@1.2.0: {} decamelize@4.0.0: {} @@ -11240,6 +11642,8 @@ snapshots: emittery@0.13.1: {} + emoji-regex@10.4.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -11266,6 +11670,8 @@ snapshots: env-paths@2.2.1: {} + environment@1.1.0: {} + err-code@2.0.3: {} error-ex@1.3.2: @@ -11384,10 +11790,10 @@ snapshots: dependencies: eslint: 8.57.1 - eslint-config-standard@17.1.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint-plugin-n@16.6.2(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1): + eslint-config-standard@17.1.0(eslint-plugin-import@2.31.0)(eslint-plugin-n@16.6.2(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1): dependencies: eslint: 8.57.1 - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@4.4.3)(eslint@8.57.1) eslint-plugin-n: 16.6.2(eslint@8.57.1) eslint-plugin-promise: 6.6.0(eslint@8.57.1) @@ -11396,6 +11802,13 @@ snapshots: chalk: 4.1.2 table: 6.9.0 + eslint-import-context@0.1.9(unrs-resolver@1.9.2): + dependencies: + get-tsconfig: 4.10.1 + stable-hash-x: 0.2.0 + optionalDependencies: + unrs-resolver: 1.9.2 + eslint-import-resolver-node@0.3.9: dependencies: debug: 3.2.7(supports-color@5.5.0) @@ -11404,13 +11817,29 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1): + eslint-import-resolver-typescript@4.4.3(eslint-plugin-import@2.31.0)(eslint@8.57.1): + dependencies: + debug: 4.4.1 + eslint: 8.57.1 + eslint-import-context: 0.1.9(unrs-resolver@1.9.2) + get-tsconfig: 4.10.1 + is-bun-module: 2.0.0 + stable-hash-x: 0.1.1 + tinyglobby: 0.2.14 + unrs-resolver: 1.9.2 + optionalDependencies: + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@4.4.3)(eslint@8.57.1) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.3)(eslint@8.57.1): dependencies: debug: 3.2.7(supports-color@5.5.0) optionalDependencies: '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.8.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 4.4.3(eslint-plugin-import@2.31.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -11421,7 +11850,7 @@ snapshots: eslint: 8.57.1 eslint-compat-utils: 0.5.1(eslint@8.57.1) - eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@4.4.3)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -11432,7 +11861,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.3)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -12071,6 +12500,8 @@ snapshots: get-caller-file@2.0.5: {} + get-east-asian-width@1.3.0: {} + get-func-name@2.0.2: {} get-intrinsic@1.3.0: @@ -12111,6 +12542,10 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + get-value@2.0.6: {} glob-parent@3.1.0: @@ -12523,6 +12958,10 @@ snapshots: dependencies: builtin-modules: 3.3.0 + is-bun-module@2.0.0: + dependencies: + semver: 7.7.1 + is-callable@1.2.7: {} is-core-module@2.16.1: @@ -12576,6 +13015,12 @@ snapshots: is-fullwidth-code-point@3.0.0: {} + is-fullwidth-code-point@4.0.0: {} + + is-fullwidth-code-point@5.0.0: + dependencies: + get-east-asian-width: 1.3.0 + is-generator-fn@2.1.0: {} is-generator-function@1.1.0: @@ -13276,6 +13721,30 @@ snapshots: lines-and-columns@1.2.4: {} + lint-staged@16.1.2: + dependencies: + chalk: 5.4.1 + commander: 14.0.0 + debug: 4.4.1 + lilconfig: 3.1.3 + listr2: 8.3.3 + micromatch: 4.0.8 + nano-spawn: 1.0.2 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.8.0 + transitivePeerDependencies: + - supports-color + + listr2@8.3.3: + dependencies: + cli-truncate: 4.0.0 + colorette: 2.0.20 + eventemitter3: 5.0.1 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.0 + locate-path@3.0.0: dependencies: p-locate: 3.0.0 @@ -13304,6 +13773,14 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 + log-update@6.1.0: + dependencies: + ansi-escapes: 7.0.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.0 + strip-ansi: 7.1.0 + wrap-ansi: 9.0.0 + logform@2.7.0: dependencies: '@colors/colors': 1.6.0 @@ -13464,6 +13941,8 @@ snapshots: mimic-fn@2.1.0: {} + mimic-function@5.0.1: {} + minimalistic-assert@1.0.1: {} minimalistic-crypto-utils@1.0.1: {} @@ -13614,9 +14093,13 @@ snapshots: rimraf: 2.4.5 optional: true + mylas@2.1.13: {} + nan@2.22.2: optional: true + nano-spawn@1.0.2: {} + nanomatch@1.2.13: dependencies: arr-diff: 4.0.0 @@ -13635,6 +14118,8 @@ snapshots: napi-macros@2.2.2: {} + napi-postinstall@0.2.5: {} + natural-compare@1.4.0: {} natural-orderby@2.0.3: {} @@ -13855,6 +14340,10 @@ snapshots: dependencies: mimic-fn: 2.1.0 + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + openapi-types@12.1.3: {} optionator@0.9.4: @@ -13995,6 +14484,8 @@ snapshots: picomatch@4.0.2: {} + pidtree@0.6.0: {} + pino-abstract-transport@2.0.0: dependencies: split2: 4.2.0 @@ -14038,6 +14529,10 @@ snapshots: dependencies: find-up: 4.1.0 + plimit-lit@1.6.1: + dependencies: + queue-lit: 1.5.2 + pnpm@10.10.0: {} posix-character-classes@0.1.1: {} @@ -14118,6 +14613,8 @@ snapshots: dependencies: side-channel: 1.1.0 + queue-lit@1.5.2: {} + queue-microtask@1.2.3: {} quick-format-unescaped@4.0.4: {} @@ -14258,6 +14755,11 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + ret@0.1.15: {} ret@0.4.3: {} @@ -14520,6 +15022,16 @@ snapshots: astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 + slice-ansi@5.0.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 4.0.0 + + slice-ansi@7.1.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 5.0.0 + smart-buffer@4.2.0: {} snake-case@3.0.4: @@ -14625,6 +15137,10 @@ snapshots: dependencies: minipass: 7.1.2 + stable-hash-x@0.1.1: {} + + stable-hash-x@0.2.0: {} + stack-trace@0.0.10: {} stack-utils@1.0.5: @@ -14679,6 +15195,8 @@ snapshots: stream-transform@3.3.3: {} + string-argv@0.3.2: {} + string-length@4.0.2: dependencies: char-regex: 1.0.2 @@ -14707,6 +15225,12 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.0 + string-width@7.2.0: + dependencies: + emoji-regex: 10.4.0 + get-east-asian-width: 1.3.0 + strip-ansi: 7.1.0 + string.prototype.trim@1.2.10: dependencies: call-bind: 1.0.8 @@ -14884,6 +15408,11 @@ snapshots: fdir: 6.4.4(picomatch@4.0.2) picomatch: 4.0.2 + tinyglobby@0.2.14: + dependencies: + fdir: 6.4.4(picomatch@4.0.2) + picomatch: 4.0.2 + tmp-promise@3.0.3: dependencies: tmp: 0.2.3 @@ -14976,6 +15505,16 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + tsc-alias@1.8.16: + dependencies: + chokidar: 3.6.0 + commander: 9.5.0 + get-tsconfig: 4.10.1 + globby: 11.1.0 + mylas: 2.1.13 + normalize-path: 3.0.0 + plimit-lit: 1.6.1 + tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 @@ -15085,6 +15624,30 @@ snapshots: unpipe@1.0.0: {} + unrs-resolver@1.9.2: + dependencies: + napi-postinstall: 0.2.5 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.9.2 + '@unrs/resolver-binding-android-arm64': 1.9.2 + '@unrs/resolver-binding-darwin-arm64': 1.9.2 + '@unrs/resolver-binding-darwin-x64': 1.9.2 + '@unrs/resolver-binding-freebsd-x64': 1.9.2 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.9.2 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.9.2 + '@unrs/resolver-binding-linux-arm64-gnu': 1.9.2 + '@unrs/resolver-binding-linux-arm64-musl': 1.9.2 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.9.2 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.9.2 + '@unrs/resolver-binding-linux-riscv64-musl': 1.9.2 + '@unrs/resolver-binding-linux-s390x-gnu': 1.9.2 + '@unrs/resolver-binding-linux-x64-gnu': 1.9.2 + '@unrs/resolver-binding-linux-x64-musl': 1.9.2 + '@unrs/resolver-binding-wasm32-wasi': 1.9.2 + '@unrs/resolver-binding-win32-arm64-msvc': 1.9.2 + '@unrs/resolver-binding-win32-ia32-msvc': 1.9.2 + '@unrs/resolver-binding-win32-x64-msvc': 1.9.2 + unset-value@1.0.0: dependencies: has-value: 0.3.1 @@ -15315,6 +15878,12 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.0 + wrap-ansi@9.0.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 7.2.0 + strip-ansi: 7.1.0 + wrappy@1.0.2: {} write-file-atomic@4.0.2: @@ -15375,6 +15944,8 @@ snapshots: yaml@2.7.1: {} + yaml@2.8.0: {} + yargs-parser@11.1.1: dependencies: camelcase: 5.3.1 diff --git a/src/app.ts b/src/app.ts index 61ba6eba85..6f325d6834 100644 --- a/src/app.ts +++ b/src/app.ts @@ -29,7 +29,7 @@ import { walletRoutes } from './wallet/wallet.routes'; import { asciiLogo } from './index'; // Change version for each release -const GATEWAY_VERSION = '2.6.0'; +const GATEWAY_VERSION = '2.7.0'; // At the top level, define devMode once // When true, runs server in HTTP mode (less secure but useful for development) diff --git a/src/chains/ethereum/ethereum.ts b/src/chains/ethereum/ethereum.ts index 2abdce6dcf..9a8f5a4db3 100644 --- a/src/chains/ethereum/ethereum.ts +++ b/src/chains/ethereum/ethereum.ts @@ -7,6 +7,7 @@ import { Transaction, utils, Wallet, + ethers, } from 'ethers'; import { getAddress } from 'ethers/lib/utils'; import fse from 'fs-extra'; @@ -33,6 +34,7 @@ export type NewDebugMsgHandler = (msg: any) => void; export class Ethereum { private static _instances: { [name: string]: Ethereum }; + private static _walletAddressExample: string | null = null; public provider: providers.StaticJsonRpcProvider; public tokenList: TokenInfo[] = []; public tokenMap: Record = {}; @@ -285,9 +287,6 @@ export class Ethereum { return new Wallet(privateKey, this.provider); } - /** - * Get a wallet from stored encrypted key - */ /** * Validate Ethereum address format * @param address The address to validate @@ -333,8 +332,7 @@ export class Ethereum { /** * Get the first available Ethereum wallet address */ - public async getFirstWalletAddress(): Promise { - // Specifically look in the ethereum subdirectory, not in any other chain's directory + public static async getFirstWalletAddress(): Promise { const path = `${walletPath}/ethereum`; try { // Create directory if it doesn't exist @@ -348,20 +346,19 @@ export class Ethereum { return null; } - // Return first wallet address (without .json extension) + // Get the first wallet address (without .json extension) const walletAddress = walletFiles[0].slice(0, -5); - // Validate it looks like an Ethereum address (0x followed by 40 hex chars) - if (!walletAddress.startsWith('0x') || walletAddress.length !== 42) { + try { + // Attempt to validate the address + return Ethereum.validateAddress(walletAddress); + } catch (e) { logger.warn( `Invalid Ethereum address found in wallet directory: ${walletAddress}`, ); return null; } - - return walletAddress; - } catch (error) { - logger.error(`Error getting Ethereum wallet address: ${error.message}`); + } catch (err) { return null; } } @@ -636,4 +633,34 @@ export class Ethereum { logger.info(`Wrapping ${utils.formatEther(amountInWei)} ETH to WETH`); return await wrappedContract.deposit(params); } + + /** + * Get a wallet address example for schema documentation + */ + public static async getWalletAddressExample(): Promise { + if (Ethereum._walletAddressExample) { + return Ethereum._walletAddressExample; + } + const defaultAddress = '0x0000000000000000000000000000000000000000'; + try { + const foundWallet = await Ethereum.getFirstWalletAddress(); + if (foundWallet) { + Ethereum._walletAddressExample = foundWallet; + return foundWallet; + } + logger.debug('No wallets found for examples in schema, using default.'); + Ethereum._walletAddressExample = defaultAddress; + return defaultAddress; + } catch (error) { + logger.error( + `Error getting Ethereum wallet address for example: ${error.message}`, + ); + return defaultAddress; + } + } + + // Check if the address is a valid EVM address + public static isAddress(address: string): boolean { + return ethers.utils.isAddress(address); + } } diff --git a/src/chains/ethereum/routes/allowances.ts b/src/chains/ethereum/routes/allowances.ts index 0cdaf5fee9..9707fbd2d5 100644 --- a/src/chains/ethereum/routes/allowances.ts +++ b/src/chains/ethereum/routes/allowances.ts @@ -1,4 +1,5 @@ import { Type } from '@sinclair/typebox'; +import { utils } from 'ethers'; import { FastifyPluginAsync, FastifyInstance } from 'fastify'; import { getSpender } from '../../../connectors/uniswap/uniswap.contracts'; @@ -18,10 +19,58 @@ export async function getTokensToTokenInfo( for (let i = 0; i < tokens.length; i++) { const symbolOrAddress = tokens[i]; + + // First try to find the token in the list const tokenInfo = ethereum.getTokenBySymbol(symbolOrAddress); if (tokenInfo) { // Use the actual token symbol as the key, not the input which might be an address tokenInfoMap[tokenInfo.symbol] = tokenInfo; + } else { + // Check if the token string is a valid Ethereum address + try { + const normalizedAddress = utils.getAddress(symbolOrAddress); + // If it's a valid address but not in our token list, we create a basic contract + // and try to get its decimals, symbol, and name directly + try { + const contract = ethereum.getContract( + normalizedAddress, + ethereum.provider, + ); + logger.info( + `Token ${symbolOrAddress} not found in list but has valid address format. Fetching token info from chain...`, + ); + + // Try to fetch token information directly from the contract + const [decimals, symbol, name] = await Promise.all([ + contract.decimals(), + contract.symbol(), + contract.name(), + ]); + + // Create a token info object + const tokenInfoObj: TokenInfo = { + chainId: ethereum.chainId, + address: normalizedAddress, + name: name, + symbol: symbol, + decimals: decimals, + }; + + // Use the contract symbol as the key, or the address if symbol is empty + const key = symbol || normalizedAddress; + tokenInfoMap[key] = tokenInfoObj; + + logger.info( + `Successfully fetched token info for ${normalizedAddress}: ${symbol} (${name})`, + ); + } catch (contractError) { + logger.warn( + `Failed to fetch token info for address ${normalizedAddress}: ${contractError.message}`, + ); + } + } catch (addressError) { + logger.debug(`${symbolOrAddress} is not a valid Ethereum address`); + } } } @@ -41,25 +90,35 @@ export async function getEthereumAllowances( const wallet = await ethereum.getWallet(address); const tokenInfoMap = await getTokensToTokenInfo(ethereum, tokens); - // Check if any tokens were not found and create a helpful error message + // Check if any tokens were found const foundSymbols = Object.keys(tokenInfoMap); if (foundSymbols.length === 0) { - const errorMsg = `None of the provided tokens were found: ${tokens.join(', ')}`; + const errorMsg = `None of the provided tokens could be found or fetched: ${tokens.join(', ')}`; logger.error(errorMsg); throw fastify.httpErrors.badRequest(errorMsg); } - const missingTokens = tokens.filter( - (t) => - !Object.values(tokenInfoMap).some( - (token) => - token.symbol.toUpperCase() === t.toUpperCase() || - token.address.toLowerCase() === t.toLowerCase(), - ), - ); + // Log any tokens that couldn't be resolved + if (foundSymbols.length < tokens.length) { + const resolvedAddresses = Object.values(tokenInfoMap).map((t) => + t.address.toLowerCase(), + ); + const resolvedSymbols = Object.values(tokenInfoMap).map((t) => + t.symbol.toUpperCase(), + ); - if (missingTokens.length > 0) { - logger.warn(`Some tokens were not found: ${missingTokens.join(', ')}`); + const missingTokens = tokens.filter((t) => { + const tLower = t.toLowerCase(); + const tUpper = t.toUpperCase(); + return ( + !resolvedAddresses.includes(tLower) && + !resolvedSymbols.includes(tUpper) + ); + }); + + logger.warn( + `Some tokens could not be resolved: ${missingTokens.join(', ')}`, + ); } // Determine the spender address based on the input @@ -115,16 +174,7 @@ export async function getEthereumAllowances( } export const allowancesRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const ethereum = await Ethereum.getInstance('base'); - let firstWalletAddress = ''; - - try { - firstWalletAddress = - (await ethereum.getFirstWalletAddress()) || firstWalletAddress; - } catch (error) { - logger.warn('No wallets found for examples in schema'); - } + const walletAddressExample = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: AllowancesRequestType; @@ -152,7 +202,7 @@ export const allowancesRoute: FastifyPluginAsync = async (fastify) => { 'worldchain', ], }), - address: Type.String({ examples: [firstWalletAddress] }), + address: Type.String({ examples: [walletAddressExample] }), spender: Type.String({ examples: [ 'uniswap/clmm', @@ -162,7 +212,17 @@ export const allowancesRoute: FastifyPluginAsync = async (fastify) => { description: 'Spender can be a connector name (e.g., uniswap/clmm, uniswap/amm, uniswap) or a direct contract address', }), - tokens: Type.Array(Type.String(), { examples: [['USDC', 'DAI']] }), + tokens: Type.Array(Type.String(), { + examples: [ + ['USDC', 'DAI'], + [ + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + '0x6B175474E89094C44Da98b954EedeAC495271d0F', + ], + ['USDC', '0xd0b53D9277642d899DF5C87A3966A349A798F224'], + ], + description: 'Array of token symbols or addresses', + }), }), response: { 200: Type.Object({ diff --git a/src/chains/ethereum/routes/approve.ts b/src/chains/ethereum/routes/approve.ts index 81281c8b5f..0301f3b433 100644 --- a/src/chains/ethereum/routes/approve.ts +++ b/src/chains/ethereum/routes/approve.ts @@ -174,16 +174,7 @@ export async function approveEthereumToken( } export const approveRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const ethereum = await Ethereum.getInstance('base'); - let firstWalletAddress = ''; - - try { - firstWalletAddress = - (await ethereum.getFirstWalletAddress()) || firstWalletAddress; - } catch (error) { - logger.warn('No wallets found for examples in schema'); - } + const walletAddressExample = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: ApproveRequestType; @@ -211,7 +202,7 @@ export const approveRoute: FastifyPluginAsync = async (fastify) => { 'worldchain', ], }), - address: Type.String({ examples: [firstWalletAddress] }), + address: Type.String({ examples: [walletAddressExample] }), spender: Type.String({ examples: [ 'uniswap/clmm', diff --git a/src/chains/ethereum/routes/balances.ts b/src/chains/ethereum/routes/balances.ts index e26924c3df..7e80377699 100644 --- a/src/chains/ethereum/routes/balances.ts +++ b/src/chains/ethereum/routes/balances.ts @@ -162,31 +162,7 @@ export async function getEthereumBalances( } export const balancesRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const ethereum = await Ethereum.getInstance('base'); - - // Default Ethereum address for examples if no wallet is available - let firstWalletAddress = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'; - - try { - // Try to get user's first Ethereum wallet if available - // getFirstWalletAddress specifically looks in the /ethereum directory - const userWallet = await ethereum.getFirstWalletAddress(); - if (userWallet) { - // Make sure it's a valid Ethereum address (0x prefix and 42 chars) - const isValidEthAddress = /^0x[a-fA-F0-9]{40}$/i.test(userWallet); - if (isValidEthAddress) { - firstWalletAddress = userWallet; - logger.info( - `Using user's Ethereum wallet for examples: ${firstWalletAddress}`, - ); - } - } - } catch (error) { - logger.warn('No Ethereum wallets found for examples in schema'); - } - - BalanceRequestSchema.properties.address.examples = [firstWalletAddress]; + const walletAddressExample = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: BalanceRequestType; @@ -219,7 +195,7 @@ export const balancesRoute: FastifyPluginAsync = async (fastify) => { 'worldchain', ], }, - address: { type: 'string', examples: [firstWalletAddress] }, + address: { type: 'string', examples: [walletAddressExample] }, tokens: { type: 'array', items: { type: 'string' }, diff --git a/src/chains/ethereum/routes/wrap.ts b/src/chains/ethereum/routes/wrap.ts index b607d2b9fc..4ce86c860f 100644 --- a/src/chains/ethereum/routes/wrap.ts +++ b/src/chains/ethereum/routes/wrap.ts @@ -193,16 +193,7 @@ export async function wrapEthereum( } export const wrapRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const ethereum = await Ethereum.getInstance('mainnet'); - let firstWalletAddress = ''; - - try { - firstWalletAddress = - (await ethereum.getFirstWalletAddress()) || firstWalletAddress; - } catch (error) { - logger.warn('No wallets found for examples in schema'); - } + const walletAddressExample = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: WrapRequestType; @@ -231,7 +222,7 @@ export const wrapRoute: FastifyPluginAsync = async (fastify) => { 'worldchain', ], }), - address: Type.String({ examples: [firstWalletAddress] }), + address: Type.String({ examples: [walletAddressExample] }), amount: Type.String({ examples: ['0.1', '1.0'], description: diff --git a/src/chains/solana/routes/balances.ts b/src/chains/solana/routes/balances.ts index 67e1f0cf04..dd401b14b5 100644 --- a/src/chains/solana/routes/balances.ts +++ b/src/chains/solana/routes/balances.ts @@ -329,15 +329,7 @@ async function getOptimizedBalance( } export const balancesRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - use a known Solana address as fallback - const solana = await Solana.getInstance('mainnet-beta'); - let firstWalletAddress = ''; - - try { - firstWalletAddress = await solana.getFirstWalletAddress(); - } catch (error) { - logger.warn('No wallets found for examples in schema'); - } + const walletAddressExample = await Solana.getWalletAddressExample(); // Example address for Solana tokens const USDC_MINT_ADDRESS = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; // USDC on Solana @@ -358,7 +350,7 @@ export const balancesRoute: FastifyPluginAsync = async (fastify) => { properties: { ...BalanceRequestSchema.properties, network: { type: 'string', examples: ['mainnet-beta', 'devnet'] }, - address: { type: 'string', examples: [firstWalletAddress] }, + address: { type: 'string', examples: [walletAddressExample] }, tokens: { type: 'array', items: { type: 'string' }, diff --git a/src/chains/solana/solana.ts b/src/chains/solana/solana.ts index 967a3f883c..67113ae00c 100644 --- a/src/chains/solana/solana.ts +++ b/src/chains/solana/solana.ts @@ -92,6 +92,7 @@ export class Solana { private _tokenMap: Record = {}; private static _instances: { [name: string]: Solana }; + private static _walletAddressExample: string | null = null; private static lastPriorityFeeEstimate: { timestamp: number; @@ -1238,7 +1239,7 @@ export class Solana { } // Add new method to get first wallet address - public async getFirstWalletAddress(): Promise { + public static async getFirstWalletAddress(): Promise { // Specifically look in the solana subdirectory, not in any other chain's directory const safeChain = sanitizePathComponent('solana'); const path = `${walletPath}/${safeChain}`; @@ -1272,6 +1273,28 @@ export class Solana { } } + public static async getWalletAddressExample(): Promise { + if (Solana._walletAddressExample) { + return Solana._walletAddressExample; + } + const defaultAddress = '11111111111111111111111111111111'; + try { + const foundWallet = await Solana.getFirstWalletAddress(); + if (foundWallet) { + Solana._walletAddressExample = foundWallet; + return foundWallet; + } + logger.debug('No wallets found for examples in schema, using default.'); + Solana._walletAddressExample = defaultAddress; + return defaultAddress; + } catch (error) { + logger.error( + `Error getting Solana wallet address for example: ${error.message}`, + ); + return defaultAddress; + } + } + // Update getTokenBySymbol to use new getToken method public async getTokenBySymbol( tokenSymbol: string, diff --git a/src/commands/balance.ts b/src/commands/balance.ts index b995478325..74b26ddf75 100644 --- a/src/commands/balance.ts +++ b/src/commands/balance.ts @@ -60,7 +60,7 @@ export default class Balance extends Command { let keypair; let walletIdentifier = wallet; if (!walletIdentifier) { - walletIdentifier = await solana.getFirstWalletAddress(); + walletIdentifier = await Solana.getFirstWalletAddress(); if (!walletIdentifier) { this.error('No wallet provided and none found on file.'); } diff --git a/src/connectors/connector.routes.ts b/src/connectors/connector.routes.ts index b034e1994e..df516c46bd 100644 --- a/src/connectors/connector.routes.ts +++ b/src/connectors/connector.routes.ts @@ -6,12 +6,7 @@ import { logger } from '../services/logger'; import { JupiterConfig } from './jupiter/jupiter.config'; import { MeteoraConfig } from './meteora/meteora.config'; import { RaydiumConfig } from './raydium/raydium.config'; -import { - UniswapConfig, - uniswapNetworks, - uniswapAmmNetworks, - uniswapClmmNetworks, -} from './uniswap/uniswap.config'; +import { UniswapConfig } from './uniswap/uniswap.config'; // Define the schema using Typebox const ConnectorSchema = Type.Object({ @@ -73,20 +68,20 @@ export const connectorsRoutes: FastifyPluginAsync = async (fastify) => { { name: 'uniswap', trading_types: ['swap'], - chain: 'ethereum', - networks: uniswapNetworks, + chain: UniswapConfig.chain, + networks: UniswapConfig.networks, }, { name: 'uniswap/amm', trading_types: ['amm', 'swap'], - chain: 'ethereum', - networks: uniswapAmmNetworks, + chain: UniswapConfig.chain, + networks: UniswapConfig.networks, }, { name: 'uniswap/clmm', trading_types: ['clmm', 'swap'], - chain: 'ethereum', - networks: uniswapClmmNetworks, + chain: UniswapConfig.chain, + networks: UniswapConfig.networks, }, ]; diff --git a/src/connectors/jupiter/routes/executeSwap.ts b/src/connectors/jupiter/routes/executeSwap.ts index e0cb0c314a..50444c85ee 100644 --- a/src/connectors/jupiter/routes/executeSwap.ts +++ b/src/connectors/jupiter/routes/executeSwap.ts @@ -95,19 +95,7 @@ async function executeJupiterSwap( } export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const solana = await Solana.getInstance('mainnet-beta'); - let firstWalletAddress = ''; - - try { - firstWalletAddress = - (await solana.getFirstWalletAddress()) || firstWalletAddress; - } catch (error) { - logger.warn('No wallets found for examples in schema'); - } - - // Update schema example - ExecuteSwapRequest.properties.walletAddress.examples = [firstWalletAddress]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: ExecuteSwapRequestType; @@ -123,7 +111,7 @@ export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { properties: { ...ExecuteSwapRequest.properties, network: { type: 'string', default: 'mainnet-beta' }, - walletAddress: { type: 'string', examples: [firstWalletAddress] }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, baseToken: { type: 'string', examples: ['SOL'] }, quoteToken: { type: 'string', examples: ['USDC'] }, amount: { type: 'number', examples: [0.1] }, diff --git a/src/connectors/jupiter/routes/quoteSwap.ts b/src/connectors/jupiter/routes/quoteSwap.ts index 393eddd46a..6de4e057a5 100644 --- a/src/connectors/jupiter/routes/quoteSwap.ts +++ b/src/connectors/jupiter/routes/quoteSwap.ts @@ -102,13 +102,10 @@ export async function getJupiterQuote( : Number(quote.outAmount) / 10 ** quoteTokenInfo.decimals; // Getting quote return { - estimatedAmountIn: - tradeSide === 'BUY' ? estimatedAmountOut : estimatedAmountIn, // Always in base token - estimatedAmountOut: - tradeSide === 'BUY' ? estimatedAmountIn : estimatedAmountOut, // Always in quote token - minAmountOut: - tradeSide === 'BUY' ? estimatedAmountIn : estimatedAmountOut, - maxAmountIn: tradeSide === 'BUY' ? estimatedAmountOut : estimatedAmountIn, + estimatedAmountIn, // Amount of input token (quote for BUY, base for SELL) + estimatedAmountOut, // Amount of output token (base for BUY, quote for SELL) + minAmountOut: estimatedAmountOut, + maxAmountIn: estimatedAmountIn, baseToken: baseTokenInfo, quoteToken: quoteTokenInfo, expectedPrice: @@ -241,11 +238,9 @@ export const quoteSwapRoute: FastifyPluginAsync = async (fastify) => { minAmountOut: quote.minAmountOut, maxAmountIn: quote.maxAmountIn, baseTokenBalanceChange: - side === 'SELL' ? -quote.estimatedAmountIn : quote.estimatedAmountIn, + side === 'SELL' ? -quote.estimatedAmountIn : quote.estimatedAmountOut, quoteTokenBalanceChange: - side === 'SELL' - ? quote.estimatedAmountOut - : -quote.estimatedAmountOut, + side === 'SELL' ? quote.estimatedAmountOut : -quote.estimatedAmountIn, price: quote.expectedPrice, gasPrice: gasEstimation?.gasPrice, gasLimit: gasEstimation?.gasLimit, diff --git a/src/connectors/meteora/clmm-routes/addLiquidity.ts b/src/connectors/meteora/clmm-routes/addLiquidity.ts index b22c619730..86b15bcbc2 100644 --- a/src/connectors/meteora/clmm-routes/addLiquidity.ts +++ b/src/connectors/meteora/clmm-routes/addLiquidity.ts @@ -191,19 +191,7 @@ export type MeteoraAddLiquidityRequestType = Static< >; export const addLiquidityRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const solana = await Solana.getInstance('mainnet-beta'); - let firstWalletAddress = ''; - - const foundWallet = await solana.getFirstWalletAddress(); - if (foundWallet) { - firstWalletAddress = foundWallet; - } else { - logger.debug('No wallets found for examples in schema'); - } - - // Update schema example - AddLiquidityRequest.properties.walletAddress.examples = [firstWalletAddress]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: MeteoraAddLiquidityRequestType; @@ -219,6 +207,7 @@ export const addLiquidityRoute: FastifyPluginAsync = async (fastify) => { properties: { ...AddLiquidityRequest.properties, network: { type: 'string', default: 'mainnet-beta' }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, slippagePct: { type: 'number', examples: [1] }, strategyType: { type: 'number', diff --git a/src/connectors/meteora/clmm-routes/closePosition.ts b/src/connectors/meteora/clmm-routes/closePosition.ts index 4f491d3df2..99dae33287 100644 --- a/src/connectors/meteora/clmm-routes/closePosition.ts +++ b/src/connectors/meteora/clmm-routes/closePosition.ts @@ -118,19 +118,7 @@ async function closePosition( } export const closePositionRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const solana = await Solana.getInstance('mainnet-beta'); - let firstWalletAddress = ''; - - const foundWallet = await solana.getFirstWalletAddress(); - if (foundWallet) { - firstWalletAddress = foundWallet; - } else { - logger.info('No wallets found for examples in schema'); - } - - // Update schema example - ClosePositionRequest.properties.walletAddress.examples = [firstWalletAddress]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: ClosePositionRequestType; @@ -145,6 +133,7 @@ export const closePositionRoute: FastifyPluginAsync = async (fastify) => { ...ClosePositionRequest, properties: { ...ClosePositionRequest.properties, + walletAddress: { type: 'string', examples: [walletAddressExample] }, network: { type: 'string', default: 'mainnet-beta' }, positionAddress: { type: 'string' }, }, diff --git a/src/connectors/meteora/clmm-routes/collectFees.ts b/src/connectors/meteora/clmm-routes/collectFees.ts index e577a39bdd..ae7257d6e2 100644 --- a/src/connectors/meteora/clmm-routes/collectFees.ts +++ b/src/connectors/meteora/clmm-routes/collectFees.ts @@ -89,19 +89,7 @@ export async function collectFees( } export const collectFeesRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const solana = await Solana.getInstance('mainnet-beta'); - let firstWalletAddress = ''; - - const foundWallet = await solana.getFirstWalletAddress(); - if (foundWallet) { - firstWalletAddress = foundWallet; - } else { - logger.debug('No wallets found for examples in schema'); - } - - // Update schema example - CollectFeesRequest.properties.walletAddress.examples = [firstWalletAddress]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: CollectFeesRequestType; @@ -117,6 +105,7 @@ export const collectFeesRoute: FastifyPluginAsync = async (fastify) => { properties: { ...CollectFeesRequest.properties, network: { type: 'string', default: 'mainnet-beta' }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, }, }, response: { diff --git a/src/connectors/meteora/clmm-routes/executeSwap.ts b/src/connectors/meteora/clmm-routes/executeSwap.ts index 589e734eb2..26ac475382 100644 --- a/src/connectors/meteora/clmm-routes/executeSwap.ts +++ b/src/connectors/meteora/clmm-routes/executeSwap.ts @@ -100,15 +100,7 @@ async function executeSwap( } export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const solana = await Solana.getInstance('mainnet-beta'); - let firstWalletAddress = ''; - - try { - firstWalletAddress = await solana.getFirstWalletAddress(); - } catch (error) { - logger.warn('No wallets found for examples in schema'); - } + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: ExecuteSwapRequestType; @@ -124,7 +116,7 @@ export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { properties: { ...ExecuteSwapRequest.properties, network: { type: 'string', default: 'mainnet-beta' }, - walletAddress: { type: 'string', examples: [firstWalletAddress] }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, baseToken: { type: 'string', examples: ['SOL'] }, quoteToken: { type: 'string', examples: ['USDC'] }, amount: { type: 'number', examples: [0.01] }, diff --git a/src/connectors/meteora/clmm-routes/openPosition.ts b/src/connectors/meteora/clmm-routes/openPosition.ts index 628f59ea3e..7904e15a8f 100644 --- a/src/connectors/meteora/clmm-routes/openPosition.ts +++ b/src/connectors/meteora/clmm-routes/openPosition.ts @@ -257,18 +257,7 @@ export type MeteoraOpenPositionRequestType = Static< >; export const openPositionRoute: FastifyPluginAsync = async (fastify) => { - const solana = await Solana.getInstance('mainnet-beta'); - let firstWalletAddress = ''; - - const foundWallet = await solana.getFirstWalletAddress(); - if (foundWallet) { - firstWalletAddress = foundWallet; - } else { - logger.debug('No wallets found for examples in schema'); - } - - // Update schema example - OpenPositionRequest.properties.walletAddress.examples = [firstWalletAddress]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: MeteoraOpenPositionRequestType; @@ -284,6 +273,7 @@ export const openPositionRoute: FastifyPluginAsync = async (fastify) => { properties: { ...OpenPositionRequest.properties, network: { type: 'string', default: 'mainnet-beta' }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, lowerPrice: { type: 'number', examples: [100] }, upperPrice: { type: 'number', examples: [180] }, poolAddress: { diff --git a/src/connectors/meteora/clmm-routes/positionInfo.ts b/src/connectors/meteora/clmm-routes/positionInfo.ts index 7cbf02aa46..36bbbf3d40 100644 --- a/src/connectors/meteora/clmm-routes/positionInfo.ts +++ b/src/connectors/meteora/clmm-routes/positionInfo.ts @@ -12,21 +12,7 @@ import { logger } from '../../../services/logger'; import { Meteora } from '../meteora'; export const positionInfoRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const solana = await Solana.getInstance('mainnet-beta'); - let firstWalletAddress = ''; - - const foundWallet = await solana.getFirstWalletAddress(); - if (foundWallet) { - firstWalletAddress = foundWallet; - } else { - logger.debug('No wallets found for examples in schema'); - } - - // Update schema example - GetPositionInfoRequest.properties.walletAddress.examples = [ - firstWalletAddress, - ]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.get<{ Querystring: GetPositionInfoRequestType; @@ -41,15 +27,8 @@ export const positionInfoRoute: FastifyPluginAsync = async (fastify) => { ...GetPositionInfoRequest, properties: { network: { type: 'string', examples: ['mainnet-beta'] }, - walletAddress: { - type: 'string', - description: 'Will use first available wallet if not specified', - examples: [firstWalletAddress], - }, - positionAddress: { - type: 'string', - description: 'Meteora position', - }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, + positionAddress: { type: 'string' }, }, }, response: { diff --git a/src/connectors/meteora/clmm-routes/positionsOwned.ts b/src/connectors/meteora/clmm-routes/positionsOwned.ts index 4615c0ca97..20da4795b9 100644 --- a/src/connectors/meteora/clmm-routes/positionsOwned.ts +++ b/src/connectors/meteora/clmm-routes/positionsOwned.ts @@ -14,7 +14,6 @@ const INVALID_SOLANA_ADDRESS_MESSAGE = (address: string) => const GetPositionsOwnedRequest = Type.Object({ network: Type.Optional(Type.String({ default: 'mainnet-beta' })), walletAddress: Type.String({ - description: 'Will use first available wallet if not specified', examples: [], // Will be populated during route registration }), poolAddress: Type.String({ @@ -28,21 +27,7 @@ type GetPositionsOwnedRequestType = Static; type GetPositionsOwnedResponseType = Static; export const positionsOwnedRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const solana = await Solana.getInstance('mainnet-beta'); - let firstWalletAddress = ''; - - const foundWallet = await solana.getFirstWalletAddress(); - if (foundWallet) { - firstWalletAddress = foundWallet; - } else { - logger.debug('No wallets found for examples in schema'); - } - - // Update schema example - GetPositionsOwnedRequest.properties.walletAddress.examples = [ - firstWalletAddress, - ]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.get<{ Querystring: GetPositionsOwnedRequestType; @@ -54,7 +39,13 @@ export const positionsOwnedRoute: FastifyPluginAsync = async (fastify) => { description: "Retrieve a list of positions owned by a user's wallet in a specific Meteora pool", tags: ['meteora/clmm'], - querystring: GetPositionsOwnedRequest, + querystring: { + ...GetPositionsOwnedRequest, + properties: { + ...GetPositionsOwnedRequest.properties, + walletAddress: { type: 'string', examples: [walletAddressExample] }, + }, + }, response: { 200: GetPositionsOwnedResponse, }, diff --git a/src/connectors/meteora/clmm-routes/removeLiquidity.ts b/src/connectors/meteora/clmm-routes/removeLiquidity.ts index 852f731df5..aabfae1d33 100644 --- a/src/connectors/meteora/clmm-routes/removeLiquidity.ts +++ b/src/connectors/meteora/clmm-routes/removeLiquidity.ts @@ -103,21 +103,7 @@ export async function removeLiquidity( } export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const solana = await Solana.getInstance('mainnet-beta'); - let firstWalletAddress = ''; - - const foundWallet = await solana.getFirstWalletAddress(); - if (foundWallet) { - firstWalletAddress = foundWallet; - } else { - logger.debug('No wallets found for examples in schema'); - } - - // Update schema example - RemoveLiquidityRequest.properties.walletAddress.examples = [ - firstWalletAddress, - ]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: RemoveLiquidityRequestType; @@ -133,6 +119,7 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { properties: { ...RemoveLiquidityRequest.properties, network: { type: 'string', default: 'mainnet-beta' }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, percentageToRemove: { type: 'number', examples: [100] }, }, }, diff --git a/src/connectors/raydium/amm-routes/addLiquidity.ts b/src/connectors/raydium/amm-routes/addLiquidity.ts index 58850bd8ee..9bb482f282 100644 --- a/src/connectors/raydium/amm-routes/addLiquidity.ts +++ b/src/connectors/raydium/amm-routes/addLiquidity.ts @@ -216,19 +216,7 @@ async function addLiquidity( } export const addLiquidityRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const solana = await Solana.getInstance('mainnet-beta'); - let firstWalletAddress = ''; - - const foundWallet = await solana.getFirstWalletAddress(); - if (foundWallet) { - firstWalletAddress = foundWallet; - } else { - logger.debug('No wallets found for examples in schema'); - } - - // Update schema example - AddLiquidityRequest.properties.walletAddress.examples = [firstWalletAddress]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: AddLiquidityRequestType; @@ -244,6 +232,7 @@ export const addLiquidityRoute: FastifyPluginAsync = async (fastify) => { properties: { ...AddLiquidityRequest.properties, network: { type: 'string', default: 'mainnet-beta' }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, poolAddress: { type: 'string', examples: ['6UmmUiYoBjSrhakAobJw8BvkmJtDVxaeBtbt7rxWo1mg'], diff --git a/src/connectors/raydium/amm-routes/executeSwap.ts b/src/connectors/raydium/amm-routes/executeSwap.ts index f537969b92..3a3f39ce01 100644 --- a/src/connectors/raydium/amm-routes/executeSwap.ts +++ b/src/connectors/raydium/amm-routes/executeSwap.ts @@ -184,16 +184,7 @@ async function executeSwap( } export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const solana = await Solana.getInstance('mainnet-beta'); - let firstWalletAddress = ''; - - try { - firstWalletAddress = - (await solana.getFirstWalletAddress()) || firstWalletAddress; - } catch (error) { - logger.warn('No wallets found for examples in schema'); - } + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: ExecuteSwapRequestType; @@ -209,7 +200,7 @@ export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { properties: { ...ExecuteSwapRequest.properties, network: { type: 'string', default: 'mainnet-beta' }, - walletAddress: { type: 'string', examples: [firstWalletAddress] }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, baseToken: { type: 'string', examples: ['SOL'] }, quoteToken: { type: 'string', examples: ['USDC'] }, amount: { type: 'number', examples: [0.01] }, diff --git a/src/connectors/raydium/amm-routes/positionInfo.ts b/src/connectors/raydium/amm-routes/positionInfo.ts index d85ccc6ce0..0a0aa619fc 100644 --- a/src/connectors/raydium/amm-routes/positionInfo.ts +++ b/src/connectors/raydium/amm-routes/positionInfo.ts @@ -79,17 +79,7 @@ async function calculateLpAmount( } export const positionInfoRoute: FastifyPluginAsync = async (fastify) => { - // Populate wallet address example - let firstWalletAddress = ''; - try { - const solana = await Solana.getInstance('mainnet-beta'); - const walletAddress = await solana.getFirstWalletAddress(); - if (walletAddress) { - firstWalletAddress = walletAddress; - } - } catch (e) { - logger.warn('Could not populate wallet address example:', e); - } + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.get<{ Querystring: GetPositionInfoRequestType; @@ -104,16 +94,13 @@ export const positionInfoRoute: FastifyPluginAsync = async (fastify) => { ...GetPositionInfoRequest, properties: { network: { type: 'string', examples: ['mainnet-beta'] }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, poolAddress: { type: 'string', examples: ['AVs9TA4nWDzfPJE9gGVNJMVhcQy3V9PGazuz33BfG2RA'], }, baseToken: { type: 'string', examples: ['SOL'] }, quoteToken: { type: 'string', examples: ['USDC'] }, - walletAddress: { - type: 'string', - examples: [firstWalletAddress], - }, }, }, response: { diff --git a/src/connectors/raydium/amm-routes/removeLiquidity.ts b/src/connectors/raydium/amm-routes/removeLiquidity.ts index 50d4da4fa7..97094e03ca 100644 --- a/src/connectors/raydium/amm-routes/removeLiquidity.ts +++ b/src/connectors/raydium/amm-routes/removeLiquidity.ts @@ -257,21 +257,7 @@ async function removeLiquidity( } export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const solana = await Solana.getInstance('mainnet-beta'); - let firstWalletAddress = ''; - - const foundWallet = await solana.getFirstWalletAddress(); - if (foundWallet) { - firstWalletAddress = foundWallet; - } else { - logger.debug('No wallets found for examples in schema'); - } - - // Update schema example - RemoveLiquidityRequest.properties.walletAddress.examples = [ - firstWalletAddress, - ]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: RemoveLiquidityRequestType; @@ -287,6 +273,7 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { properties: { ...RemoveLiquidityRequest.properties, network: { type: 'string', default: 'mainnet-beta' }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, poolAddress: { type: 'string', examples: ['6UmmUiYoBjSrhakAobJw8BvkmJtDVxaeBtbt7rxWo1mg'], diff --git a/src/connectors/raydium/clmm-routes/closePosition.ts b/src/connectors/raydium/clmm-routes/closePosition.ts index 48e2da278a..857df9f164 100644 --- a/src/connectors/raydium/clmm-routes/closePosition.ts +++ b/src/connectors/raydium/clmm-routes/closePosition.ts @@ -99,19 +99,7 @@ async function closePosition( } export const closePositionRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const solana = await Solana.getInstance('mainnet-beta'); - let firstWalletAddress = ''; - - const foundWallet = await solana.getFirstWalletAddress(); - if (foundWallet) { - firstWalletAddress = foundWallet; - } else { - logger.debug('No wallets found for examples in schema'); - } - - // Update schema example - ClosePositionRequest.properties.walletAddress.examples = [firstWalletAddress]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: ClosePositionRequestType; @@ -127,6 +115,7 @@ export const closePositionRoute: FastifyPluginAsync = async (fastify) => { properties: { ...ClosePositionRequest.properties, network: { type: 'string', default: 'mainnet-beta' }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, positionAddress: { type: 'string' }, }, }, diff --git a/src/connectors/raydium/clmm-routes/collectFees.ts b/src/connectors/raydium/clmm-routes/collectFees.ts index 057ef80e67..4b40eafd37 100644 --- a/src/connectors/raydium/clmm-routes/collectFees.ts +++ b/src/connectors/raydium/clmm-routes/collectFees.ts @@ -95,17 +95,7 @@ export async function collectFees( } export const collectFeesRoute: FastifyPluginAsync = async (fastify) => { - const solana = await Solana.getInstance('mainnet-beta'); - let firstWalletAddress = ''; - - try { - firstWalletAddress = - (await solana.getFirstWalletAddress()) || firstWalletAddress; - } catch (error) { - logger.debug('No wallets found for examples in schema'); - } - - CollectFeesRequest.properties.walletAddress.examples = [firstWalletAddress]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: CollectFeesRequestType; @@ -121,6 +111,7 @@ export const collectFeesRoute: FastifyPluginAsync = async (fastify) => { properties: { ...CollectFeesRequest.properties, network: { type: 'string', default: 'mainnet-beta' }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, }, }, response: { 200: CollectFeesResponse }, diff --git a/src/connectors/raydium/clmm-routes/executeSwap.ts b/src/connectors/raydium/clmm-routes/executeSwap.ts index 1d4a6c32e7..75ed08d864 100644 --- a/src/connectors/raydium/clmm-routes/executeSwap.ts +++ b/src/connectors/raydium/clmm-routes/executeSwap.ts @@ -224,16 +224,7 @@ async function executeSwap( } export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const solana = await Solana.getInstance('mainnet-beta'); - let firstWalletAddress = ''; - - try { - firstWalletAddress = - (await solana.getFirstWalletAddress()) || firstWalletAddress; - } catch (error) { - logger.warn('No wallets found for examples in schema'); - } + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: ExecuteSwapRequestType; @@ -249,7 +240,7 @@ export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { properties: { ...ExecuteSwapRequest.properties, network: { type: 'string', default: 'mainnet-beta' }, - walletAddress: { type: 'string', examples: [firstWalletAddress] }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, baseToken: { type: 'string', examples: ['SOL'] }, quoteToken: { type: 'string', examples: ['USDC'] }, amount: { type: 'number', examples: [0.01] }, diff --git a/src/connectors/raydium/clmm-routes/openPosition.ts b/src/connectors/raydium/clmm-routes/openPosition.ts index c83b3175fc..ee75c2ef75 100644 --- a/src/connectors/raydium/clmm-routes/openPosition.ts +++ b/src/connectors/raydium/clmm-routes/openPosition.ts @@ -165,19 +165,7 @@ async function openPosition( } export const openPositionRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const solana = await Solana.getInstance('mainnet-beta'); - let firstWalletAddress = ''; - - const foundWallet = await solana.getFirstWalletAddress(); - if (foundWallet) { - firstWalletAddress = foundWallet; - } else { - logger.debug('No wallets found for examples in schema'); - } - - // Update schema example - OpenPositionRequest.properties.walletAddress.examples = [firstWalletAddress]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: OpenPositionRequestType; @@ -193,6 +181,7 @@ export const openPositionRoute: FastifyPluginAsync = async (fastify) => { properties: { ...OpenPositionRequest.properties, network: { type: 'string', default: 'mainnet-beta' }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, lowerPrice: { type: 'number', examples: [100] }, upperPrice: { type: 'number', examples: [180] }, poolAddress: { diff --git a/src/connectors/raydium/raydium.ts b/src/connectors/raydium/raydium.ts index 67713f7862..71878f6880 100644 --- a/src/connectors/raydium/raydium.ts +++ b/src/connectors/raydium/raydium.ts @@ -67,7 +67,7 @@ export class Raydium { this.solana = await Solana.getInstance(network); // Load first wallet if available - const walletAddress = await this.solana.getFirstWalletAddress(); + const walletAddress = await Solana.getFirstWalletAddress(); if (walletAddress) { this.owner = await this.solana.getWallet(walletAddress); } diff --git a/src/connectors/uniswap/amm-routes/addLiquidity.ts b/src/connectors/uniswap/amm-routes/addLiquidity.ts index c718e49414..c7dc859e2b 100644 --- a/src/connectors/uniswap/amm-routes/addLiquidity.ts +++ b/src/connectors/uniswap/amm-routes/addLiquidity.ts @@ -309,17 +309,7 @@ async function addLiquidity( export const addLiquidityRoute: FastifyPluginAsync = async (fastify) => { await fastify.register(require('@fastify/sensible')); - - // Get first wallet address for example - const ethereum = await Ethereum.getInstance('base'); - let firstWalletAddress = ''; - - try { - firstWalletAddress = - (await ethereum.getFirstWalletAddress()) || firstWalletAddress; - } catch (error) { - logger.warn('No wallets found for examples in schema'); - } + const walletAddressExample = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: AddLiquidityRequestType; @@ -335,7 +325,7 @@ export const addLiquidityRoute: FastifyPluginAsync = async (fastify) => { properties: { ...AddLiquidityRequest.properties, network: { type: 'string', default: 'base' }, - walletAddress: { type: 'string', examples: [firstWalletAddress] }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, poolAddress: { type: 'string', examples: [''], @@ -380,11 +370,10 @@ export const addLiquidityRoute: FastifyPluginAsync = async (fastify) => { // Get wallet address - either from request or first available let walletAddress = requestedWalletAddress; if (!walletAddress) { - const ethereum = await Ethereum.getInstance(networkToUse); - walletAddress = await ethereum.getFirstWalletAddress(); + walletAddress = await Ethereum.getFirstWalletAddress(); if (!walletAddress) { throw fastify.httpErrors.badRequest( - 'No wallet address provided and no default wallet found', + 'No wallet address provided and no wallets found.', ); } logger.info(`Using first available wallet address: ${walletAddress}`); diff --git a/src/connectors/uniswap/amm-routes/executeSwap.ts b/src/connectors/uniswap/amm-routes/executeSwap.ts index 10f5bb0957..849e849ebb 100644 --- a/src/connectors/uniswap/amm-routes/executeSwap.ts +++ b/src/connectors/uniswap/amm-routes/executeSwap.ts @@ -22,17 +22,7 @@ import { getUniswapAmmQuote } from './quoteSwap'; export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { // Import the httpErrors plugin to ensure it's available await fastify.register(require('@fastify/sensible')); - - // Get first wallet address for example - const ethereum = await Ethereum.getInstance('base'); - let firstWalletAddress = ''; - - try { - firstWalletAddress = - (await ethereum.getFirstWalletAddress()) || firstWalletAddress; - } catch (error) { - logger.warn('No wallets found for examples in schema'); - } + const walletAddressExample = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: ExecuteSwapRequestType; @@ -48,7 +38,7 @@ export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { properties: { ...ExecuteSwapRequest.properties, network: { type: 'string', default: 'base' }, - walletAddress: { type: 'string', examples: [firstWalletAddress] }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, baseToken: { type: 'string', examples: ['WETH'] }, quoteToken: { type: 'string', examples: ['USDC'] }, amount: { type: 'number', examples: [0.001] }, @@ -85,11 +75,10 @@ export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { // Get wallet address - either from request or first available let walletAddress = requestedWalletAddress; if (!walletAddress) { - const ethereum = await Ethereum.getInstance(networkToUse); - walletAddress = await ethereum.getFirstWalletAddress(); + walletAddress = await Ethereum.getFirstWalletAddress(); if (!walletAddress) { throw fastify.httpErrors.badRequest( - 'No wallet address provided and no default wallet found', + 'No wallet address provided and no wallets found.', ); } logger.info(`Using first available wallet address: ${walletAddress}`); diff --git a/src/connectors/uniswap/amm-routes/positionInfo.ts b/src/connectors/uniswap/amm-routes/positionInfo.ts index cad58c723e..a56de10702 100644 --- a/src/connectors/uniswap/amm-routes/positionInfo.ts +++ b/src/connectors/uniswap/amm-routes/positionInfo.ts @@ -31,22 +31,13 @@ export async function checkLPAllowance( const currentLpAllowance = BigNumber.from(lpAllowance.value); if (currentLpAllowance.lt(requiredAmount)) { throw new Error( - `Insufficient LP token allowance. Please approve at least ${formatTokenAmount(requiredAmount.toString(), 18)} LP tokens for the Uniswap router (${routerAddress})`, + `Insufficient LP token allowance. Please approve at least ${formatTokenAmount(requiredAmount.toString(), 18)} LP tokens (${poolAddress}) for the Uniswap router (${routerAddress})`, ); } } export const positionInfoRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const ethereum = await Ethereum.getInstance('base'); - let firstWalletAddress = ''; - - try { - firstWalletAddress = - (await ethereum.getFirstWalletAddress()) || firstWalletAddress; - } catch (error) { - logger.warn('No wallets found for examples in schema'); - } + const walletAddressExample = await Ethereum.getWalletAddressExample(); fastify.get<{ Querystring: GetPositionInfoRequestType; @@ -61,13 +52,13 @@ export const positionInfoRoute: FastifyPluginAsync = async (fastify) => { ...GetPositionInfoRequest, properties: { network: { type: 'string', default: 'base' }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, poolAddress: { type: 'string', examples: [''], }, baseToken: { type: 'string', examples: ['WETH'] }, quoteToken: { type: 'string', examples: ['USDC'] }, - walletAddress: { type: 'string', examples: [firstWalletAddress] }, }, }, response: { diff --git a/src/connectors/uniswap/amm-routes/removeLiquidity.ts b/src/connectors/uniswap/amm-routes/removeLiquidity.ts index fe9553b9b9..53966c93d4 100644 --- a/src/connectors/uniswap/amm-routes/removeLiquidity.ts +++ b/src/connectors/uniswap/amm-routes/removeLiquidity.ts @@ -22,16 +22,8 @@ import { formatTokenAmount } from '../uniswap.utils'; import { checkLPAllowance } from './positionInfo'; export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const ethereum = await Ethereum.getInstance('base'); - let firstWalletAddress = ''; - - try { - firstWalletAddress = - (await ethereum.getFirstWalletAddress()) || firstWalletAddress; - } catch (error) { - logger.warn('No wallets found for examples in schema'); - } + await fastify.register(require('@fastify/sensible')); + const walletAddressExample = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: RemoveLiquidityRequestType; @@ -47,7 +39,7 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { properties: { ...RemoveLiquidityRequest.properties, network: { type: 'string', default: 'base' }, - walletAddress: { type: 'string', examples: [firstWalletAddress] }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, poolAddress: { type: 'string', examples: [''], @@ -203,7 +195,7 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { routerAddress, liquidityToRemove, ); - } catch (error) { + } catch (error: any) { throw fastify.httpErrors.badRequest(error.message); } diff --git a/src/connectors/uniswap/clmm-routes/collectFees.ts b/src/connectors/uniswap/clmm-routes/collectFees.ts index 1f13f1f6a4..f72d42a1a8 100644 --- a/src/connectors/uniswap/clmm-routes/collectFees.ts +++ b/src/connectors/uniswap/clmm-routes/collectFees.ts @@ -1,4 +1,5 @@ import { Contract } from '@ethersproject/contracts'; +import { CurrencyAmount } from '@uniswap/sdk-core'; import { NonfungiblePositionManager } from '@uniswap/v3-sdk'; import { BigNumber } from 'ethers'; import { FastifyPluginAsync } from 'fastify'; @@ -12,41 +13,13 @@ import { } from '../../../schemas/clmm-schema'; import { logger } from '../../../services/logger'; import { Uniswap } from '../uniswap'; +import { POSITION_MANAGER_ABI } from '../uniswap.contracts'; import { formatTokenAmount } from '../uniswap.utils'; -// Define minimal ABI for the NonfungiblePositionManager -const POSITION_MANAGER_ABI = [ - { - inputs: [{ internalType: 'uint256', name: 'tokenId', type: 'uint256' }], - name: 'positions', - outputs: [ - { internalType: 'uint96', name: 'nonce', type: 'uint96' }, - { internalType: 'address', name: 'operator', type: 'address' }, - { internalType: 'address', name: 'token0', type: 'address' }, - { internalType: 'address', name: 'token1', type: 'address' }, - { internalType: 'uint24', name: 'fee', type: 'uint24' }, - { internalType: 'int24', name: 'tickLower', type: 'int24' }, - { internalType: 'int24', name: 'tickUpper', type: 'int24' }, - { internalType: 'uint128', name: 'liquidity', type: 'uint128' }, - { - internalType: 'uint256', - name: 'feeGrowthInside0LastX128', - type: 'uint256', - }, - { - internalType: 'uint256', - name: 'feeGrowthInside1LastX128', - type: 'uint256', - }, - { internalType: 'uint128', name: 'tokensOwed0', type: 'uint128' }, - { internalType: 'uint128', name: 'tokensOwed1', type: 'uint128' }, - ], - stateMutability: 'view', - type: 'function', - }, -]; - export const collectFeesRoute: FastifyPluginAsync = async (fastify) => { + await fastify.register(require('@fastify/sensible')); + const walletAddressExample = await Ethereum.getWalletAddressExample(); + fastify.post<{ Body: CollectFeesRequestType; Reply: CollectFeesResponseType; @@ -61,10 +34,11 @@ export const collectFeesRoute: FastifyPluginAsync = async (fastify) => { properties: { ...CollectFeesRequest.properties, network: { type: 'string', default: 'base' }, - walletAddress: { type: 'string', examples: ['0x...'] }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, positionAddress: { type: 'string', description: 'Position NFT token ID', + examples: ['1234'], }, }, }, @@ -115,6 +89,16 @@ export const collectFeesRoute: FastifyPluginAsync = async (fastify) => { const positionManagerAddress = uniswap.config.uniswapV3NftManagerAddress(networkToUse); + // Check NFT ownership + try { + await uniswap.checkNFTOwnership(positionAddress, walletAddress); + } catch (error: any) { + if (error.message.includes('is not owned by')) { + throw fastify.httpErrors.forbidden(error.message); + } + throw fastify.httpErrors.badRequest(error.message); + } + // Create position manager contract const positionManager = new Contract( positionManagerAddress, @@ -129,10 +113,11 @@ export const collectFeesRoute: FastifyPluginAsync = async (fastify) => { const token0 = uniswap.getTokenByAddress(position.token0); const token1 = uniswap.getTokenByAddress(position.token1); - // Determine base and quote tokens - const baseTokenSymbol = - token0.symbol === 'WETH' ? token0.symbol : token1.symbol; - const isBaseToken0 = token0.symbol === baseTokenSymbol; + // Determine base and quote tokens - WETH or lower address is base + const isBaseToken0 = + token0.symbol === 'WETH' || + (token1.symbol !== 'WETH' && + token0.address.toLowerCase() < token1.address.toLowerCase()); // Get fees owned const feeAmount0 = position.tokensOwed0; @@ -143,11 +128,21 @@ export const collectFeesRoute: FastifyPluginAsync = async (fastify) => { throw fastify.httpErrors.badRequest('No fees to collect'); } + // Create CurrencyAmount objects for fees + const expectedCurrencyOwed0 = CurrencyAmount.fromRawAmount( + token0, + feeAmount0.toString(), + ); + const expectedCurrencyOwed1 = CurrencyAmount.fromRawAmount( + token1, + feeAmount1.toString(), + ); + // Create parameters for collecting fees const collectParams = { tokenId: positionAddress, - expectedCurrencyOwed0: feeAmount0, - expectedCurrencyOwed1: feeAmount1, + expectedCurrencyOwed0, + expectedCurrencyOwed1, recipient: walletAddress, }; @@ -155,26 +150,11 @@ export const collectFeesRoute: FastifyPluginAsync = async (fastify) => { const { calldata, value } = NonfungiblePositionManager.collectCallParameters(collectParams); - // Initialize position manager with multicall interface - const positionManagerWithSigner = new Contract( - positionManagerAddress, - [ - { - inputs: [{ internalType: 'bytes', name: 'data', type: 'bytes' }], - name: 'multicall', - outputs: [ - { internalType: 'bytes[]', name: 'results', type: 'bytes[]' }, - ], - stateMutability: 'payable', - type: 'function', - }, - ], - wallet, - ); - // Execute the transaction to collect fees - const tx = await positionManagerWithSigner.multicall([calldata], { - value: BigNumber.from(value.toString()), + const tx = await wallet.sendTransaction({ + to: positionManagerAddress, + data: calldata, + value: BigNumber.from(value), gasLimit: 300000, }); diff --git a/src/connectors/uniswap/clmm-routes/executeSwap.ts b/src/connectors/uniswap/clmm-routes/executeSwap.ts index d7af827513..149f0ec8d0 100644 --- a/src/connectors/uniswap/clmm-routes/executeSwap.ts +++ b/src/connectors/uniswap/clmm-routes/executeSwap.ts @@ -23,16 +23,7 @@ export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { // Import the httpErrors plugin to ensure it's available await fastify.register(require('@fastify/sensible')); - // Get first wallet address for example - const ethereum = await Ethereum.getInstance('base'); - let firstWalletAddress = ''; - - try { - firstWalletAddress = - (await ethereum.getFirstWalletAddress()) || firstWalletAddress; - } catch (error) { - logger.warn('No wallets found for examples in schema'); - } + const walletAddressExample = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: ExecuteSwapRequestType; @@ -48,7 +39,7 @@ export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { properties: { ...ExecuteSwapRequest.properties, network: { type: 'string', default: 'base' }, - walletAddress: { type: 'string', examples: [firstWalletAddress] }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, baseToken: { type: 'string', examples: ['WETH'] }, quoteToken: { type: 'string', examples: ['USDC'] }, amount: { type: 'number', examples: [0.001] }, @@ -85,11 +76,10 @@ export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { // Get wallet address - either from request or first available let walletAddress = requestedWalletAddress; if (!walletAddress) { - const ethereum = await Ethereum.getInstance(networkToUse); - walletAddress = await ethereum.getFirstWalletAddress(); + walletAddress = await Ethereum.getFirstWalletAddress(); if (!walletAddress) { throw fastify.httpErrors.badRequest( - 'No wallet address provided and no default wallet found', + 'No wallet address provided and no wallets found.', ); } logger.info(`Using first available wallet address: ${walletAddress}`); @@ -187,6 +177,38 @@ export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { logger.info(`Side: ${side}`); logger.info(`Fee tier: ${quote.feeTier}`); + // Check allowance for input token (including WETH) + const tokenContract = ethereum.getContract(inputTokenAddress, wallet); + const allowance = await ethereum.getERC20Allowance( + tokenContract, + wallet, + routerAddress, + quote.inputToken.decimals, + ); + + const amountNeeded = + side === 'SELL' ? quote.rawAmountIn : quote.rawMaxAmountIn; + const currentAllowance = BigNumber.from(allowance.value); + + logger.info( + `Current allowance: ${formatTokenAmount(currentAllowance.toString(), quote.inputToken.decimals)} ${quote.inputToken.symbol}`, + ); + logger.info( + `Amount needed: ${formatTokenAmount(amountNeeded, quote.inputToken.decimals)} ${quote.inputToken.symbol}`, + ); + + // Check if allowance is sufficient + if (currentAllowance.lt(amountNeeded)) { + logger.error(`Insufficient allowance for ${quote.inputToken.symbol}`); + throw fastify.httpErrors.badRequest( + `Insufficient allowance for ${quote.inputToken.symbol}. Please approve at least ${formatTokenAmount(amountNeeded, quote.inputToken.decimals)} ${quote.inputToken.symbol} (${inputTokenAddress}) for the Uniswap SwapRouter02 (${routerAddress})`, + ); + } else { + logger.info( + `Sufficient allowance exists: ${formatTokenAmount(currentAllowance.toString(), quote.inputToken.decimals)} ${quote.inputToken.symbol}`, + ); + } + // Build swap parameters const swapParams = { tokenIn: inputTokenAddress, diff --git a/src/connectors/uniswap/clmm-routes/openPosition.ts b/src/connectors/uniswap/clmm-routes/openPosition.ts index ec85fd06a8..4aef2e5b5e 100644 --- a/src/connectors/uniswap/clmm-routes/openPosition.ts +++ b/src/connectors/uniswap/clmm-routes/openPosition.ts @@ -24,33 +24,10 @@ import { logger } from '../../../services/logger'; import { Uniswap } from '../uniswap'; import { formatTokenAmount, parseFeeTier } from '../uniswap.utils'; -// Define a minimal ABI for ERC20 tokens -const ERC20_ABI = [ - { - constant: false, - inputs: [ - { name: '_spender', type: 'address' }, - { name: '_value', type: 'uint256' }, - ], - name: 'approve', - outputs: [{ name: '', type: 'bool' }], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, -]; - export const openPositionRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const ethereum = await Ethereum.getInstance('base'); - let firstWalletAddress = ''; + await fastify.register(require('@fastify/sensible')); - try { - firstWalletAddress = - (await ethereum.getFirstWalletAddress()) || firstWalletAddress; - } catch (error) { - logger.warn('No wallets found for examples in schema'); - } + const walletAddressExample = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: OpenPositionRequestType; @@ -66,7 +43,7 @@ export const openPositionRoute: FastifyPluginAsync = async (fastify) => { properties: { ...OpenPositionRequest.properties, network: { type: 'string', default: 'base' }, - walletAddress: { type: 'string', examples: [firstWalletAddress] }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, lowerPrice: { type: 'number', examples: [1000] }, upperPrice: { type: 'number', examples: [4000] }, poolAddress: { type: 'string', examples: [''] }, @@ -297,30 +274,12 @@ export const openPositionRoute: FastifyPluginAsync = async (fastify) => { logger.info(` Recipient: ${walletAddress}`); logger.info(` Deadline: ${mintOptions.deadline}`); - // Check allowances instead of approving + // Get position manager address for allowance checks const positionManagerAddress = uniswap.config.uniswapV3NftManagerAddress(networkToUse); - // Check if we have enough ETH for WETH positions - if (value && value !== '0') { - const walletBalance = await wallet.getBalance(); - const requiredValue = BigNumber.from(value); - logger.info( - `Wallet ETH balance: ${formatTokenAmount(walletBalance.toString(), 18)}`, - ); - logger.info( - `Required ETH value: ${formatTokenAmount(requiredValue.toString(), 18)}`, - ); - - if (walletBalance.lt(requiredValue)) { - throw fastify.httpErrors.badRequest( - `Insufficient ETH balance. Required: ${formatTokenAmount(requiredValue.toString(), 18)} ETH`, - ); - } - } - - // Check token0 allowance if needed - if (!token0Amount.equalTo(0) && token0.symbol !== 'WETH') { + // Check token0 allowance if needed (including WETH) + if (!token0Amount.equalTo(0)) { const token0Contract = ethereum.getContract(token0.address, wallet); const allowance0 = await ethereum.getERC20Allowance( token0Contract, @@ -343,13 +302,13 @@ export const openPositionRoute: FastifyPluginAsync = async (fastify) => { if (currentAllowance0.lt(requiredAmount0)) { throw fastify.httpErrors.badRequest( - `Insufficient ${token0.symbol} allowance. Please approve at least ${formatTokenAmount(requiredAmount0.toString(), token0.decimals)} ${token0.symbol} for the Position Manager (${positionManagerAddress})`, + `Insufficient ${token0.symbol} allowance. Please approve at least ${formatTokenAmount(requiredAmount0.toString(), token0.decimals)} ${token0.symbol} (${token0.address}) for the Position Manager (${positionManagerAddress})`, ); } } - // Check token1 allowance if needed - if (!token1Amount.equalTo(0) && token1.symbol !== 'WETH') { + // Check token1 allowance if needed (including WETH) + if (!token1Amount.equalTo(0)) { const token1Contract = ethereum.getContract(token1.address, wallet); const allowance1 = await ethereum.getERC20Allowance( token1Contract, @@ -372,7 +331,7 @@ export const openPositionRoute: FastifyPluginAsync = async (fastify) => { if (currentAllowance1.lt(requiredAmount1)) { throw fastify.httpErrors.badRequest( - `Insufficient ${token1.symbol} allowance. Please approve at least ${formatTokenAmount(requiredAmount1.toString(), token1.decimals)} ${token1.symbol} for the Position Manager (${positionManagerAddress})`, + `Insufficient ${token1.symbol} allowance. Please approve at least ${formatTokenAmount(requiredAmount1.toString(), token1.decimals)} ${token1.symbol} (${token1.address}) for the Position Manager (${positionManagerAddress})`, ); } } diff --git a/src/connectors/uniswap/clmm-routes/positionInfo.ts b/src/connectors/uniswap/clmm-routes/positionInfo.ts index 79257bb150..9a42f676ce 100644 --- a/src/connectors/uniswap/clmm-routes/positionInfo.ts +++ b/src/connectors/uniswap/clmm-routes/positionInfo.ts @@ -19,51 +19,12 @@ import { } from '../../../schemas/clmm-schema'; import { logger } from '../../../services/logger'; import { Uniswap } from '../uniswap'; +import { POSITION_MANAGER_ABI } from '../uniswap.contracts'; import { formatTokenAmount } from '../uniswap.utils'; -// Define minimal ABI for the NonfungiblePositionManager -const POSITION_MANAGER_ABI = [ - { - inputs: [{ internalType: 'uint256', name: 'tokenId', type: 'uint256' }], - name: 'positions', - outputs: [ - { internalType: 'uint96', name: 'nonce', type: 'uint96' }, - { internalType: 'address', name: 'operator', type: 'address' }, - { internalType: 'address', name: 'token0', type: 'address' }, - { internalType: 'address', name: 'token1', type: 'address' }, - { internalType: 'uint24', name: 'fee', type: 'uint24' }, - { internalType: 'int24', name: 'tickLower', type: 'int24' }, - { internalType: 'int24', name: 'tickUpper', type: 'int24' }, - { internalType: 'uint128', name: 'liquidity', type: 'uint128' }, - { - internalType: 'uint256', - name: 'feeGrowthInside0LastX128', - type: 'uint256', - }, - { - internalType: 'uint256', - name: 'feeGrowthInside1LastX128', - type: 'uint256', - }, - { internalType: 'uint128', name: 'tokensOwed0', type: 'uint128' }, - { internalType: 'uint128', name: 'tokensOwed1', type: 'uint128' }, - ], - stateMutability: 'view', - type: 'function', - }, -]; - export const positionInfoRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const ethereum = await Ethereum.getInstance('base'); - let firstWalletAddress = ''; - - try { - firstWalletAddress = - (await ethereum.getFirstWalletAddress()) || firstWalletAddress; - } catch (error) { - logger.warn('No wallets found for examples in schema'); - } + await fastify.register(require('@fastify/sensible')); + const walletAddressExample = await Ethereum.getWalletAddressExample(); fastify.get<{ Querystring: GetPositionInfoRequestType; @@ -83,7 +44,7 @@ export const positionInfoRoute: FastifyPluginAsync = async (fastify) => { description: 'Position NFT token ID', examples: ['1234'], }, - walletAddress: { type: 'string', examples: [firstWalletAddress] }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, }, }, response: { diff --git a/src/connectors/uniswap/clmm-routes/positionsOwned.ts b/src/connectors/uniswap/clmm-routes/positionsOwned.ts index 5a44c33f96..453fd20a98 100644 --- a/src/connectors/uniswap/clmm-routes/positionsOwned.ts +++ b/src/connectors/uniswap/clmm-routes/positionsOwned.ts @@ -1,10 +1,14 @@ import { Contract } from '@ethersproject/contracts'; import { Type } from '@sinclair/typebox'; +import { Position, tickToPrice, computePoolAddress } from '@uniswap/v3-sdk'; import { FastifyPluginAsync } from 'fastify'; import { Ethereum } from '../../../chains/ethereum/ethereum'; +import { PositionInfoSchema } from '../../../schemas/clmm-schema'; import { logger } from '../../../services/logger'; import { Uniswap } from '../uniswap'; +import { POSITION_MANAGER_ABI } from '../uniswap.contracts'; +import { formatTokenAmount } from '../uniswap.utils'; // Define the request and response types const PositionsOwnedRequest = Type.Object({ @@ -12,20 +16,10 @@ const PositionsOwnedRequest = Type.Object({ walletAddress: Type.String({ examples: [''] }), }); -const PositionsOwnedResponse = Type.Array( - Type.Object({ - tokenId: Type.String(), - token0: Type.String(), - token1: Type.String(), - fee: Type.Number(), - tickLower: Type.Number(), - tickUpper: Type.Number(), - liquidity: Type.String(), - }), -); - -// Define minimal ABI for the NonfungiblePositionManager -const POSITION_MANAGER_ABI = [ +const PositionsOwnedResponse = Type.Array(PositionInfoSchema); + +// Additional ABI methods needed for enumerating positions +const ENUMERABLE_ABI = [ { inputs: [{ internalType: 'address', name: 'owner', type: 'address' }], name: 'balanceOf', @@ -43,51 +37,11 @@ const POSITION_MANAGER_ABI = [ stateMutability: 'view', type: 'function', }, - { - inputs: [{ internalType: 'uint256', name: 'tokenId', type: 'uint256' }], - name: 'positions', - outputs: [ - { internalType: 'uint96', name: 'nonce', type: 'uint96' }, - { internalType: 'address', name: 'operator', type: 'address' }, - { internalType: 'address', name: 'token0', type: 'address' }, - { internalType: 'address', name: 'token1', type: 'address' }, - { internalType: 'uint24', name: 'fee', type: 'uint24' }, - { internalType: 'int24', name: 'tickLower', type: 'int24' }, - { internalType: 'int24', name: 'tickUpper', type: 'int24' }, - { internalType: 'uint128', name: 'liquidity', type: 'uint128' }, - { - internalType: 'uint256', - name: 'feeGrowthInside0LastX128', - type: 'uint256', - }, - { - internalType: 'uint256', - name: 'feeGrowthInside1LastX128', - type: 'uint256', - }, - { internalType: 'uint128', name: 'tokensOwed0', type: 'uint128' }, - { internalType: 'uint128', name: 'tokensOwed1', type: 'uint128' }, - ], - stateMutability: 'view', - type: 'function', - }, ]; export const positionsOwnedRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const ethereum = await Ethereum.getInstance('base'); - let firstWalletAddress = ''; - - try { - firstWalletAddress = - (await ethereum.getFirstWalletAddress()) || firstWalletAddress; - // Update the example in the schema - PositionsOwnedRequest.properties.walletAddress.examples = [ - firstWalletAddress, - ]; - } catch (error) { - logger.warn('No wallets found for examples in schema'); - } + await fastify.register(require('@fastify/sensible')); + const walletAddressExample = await Ethereum.getWalletAddressExample(); fastify.get<{ Querystring: typeof PositionsOwnedRequest.static; @@ -98,7 +52,13 @@ export const positionsOwnedRoute: FastifyPluginAsync = async (fastify) => { schema: { description: 'Get all Uniswap V3 positions owned by a wallet', tags: ['uniswap/clmm'], - querystring: PositionsOwnedRequest, + querystring: { + ...PositionsOwnedRequest, + properties: { + ...PositionsOwnedRequest.properties, + walletAddress: { type: 'string', examples: [walletAddressExample] }, + }, + }, response: { 200: PositionsOwnedResponse, }, @@ -130,10 +90,10 @@ export const positionsOwnedRoute: FastifyPluginAsync = async (fastify) => { const positionManagerAddress = uniswap.config.uniswapV3NftManagerAddress(network); - // Create position manager contract + // Create position manager contract with both enumerable and position ABIs const positionManager = new Contract( positionManagerAddress, - POSITION_MANAGER_ABI, + [...ENUMERABLE_ABI, ...POSITION_MANAGER_ABI], ethereum.provider, ); @@ -145,7 +105,7 @@ export const positionsOwnedRoute: FastifyPluginAsync = async (fastify) => { return []; } - // Get all position token IDs + // Get all position token IDs and convert to PositionInfo format const positions = []; for (let i = 0; i < numPositions; i++) { try { @@ -155,20 +115,117 @@ export const positionsOwnedRoute: FastifyPluginAsync = async (fastify) => { ); // Get position details - const position = await positionManager.positions(tokenId); + const positionDetails = await positionManager.positions(tokenId); + + // Skip positions with no liquidity + if (positionDetails.liquidity.eq(0)) { + continue; + } + + // Get the token addresses from the position + const token0Address = positionDetails.token0; + const token1Address = positionDetails.token1; + + // Get the tokens from addresses + const token0 = uniswap.getTokenByAddress(token0Address); + const token1 = uniswap.getTokenByAddress(token1Address); + + // Get position ticks + const tickLower = positionDetails.tickLower; + const tickUpper = positionDetails.tickUpper; + const liquidity = positionDetails.liquidity; + const fee = positionDetails.fee; + + // Get collected fees + const feeAmount0 = formatTokenAmount( + positionDetails.tokensOwed0.toString(), + token0.decimals, + ); + const feeAmount1 = formatTokenAmount( + positionDetails.tokensOwed1.toString(), + token1.decimals, + ); - // Get tokens by address - const token0 = uniswap.getTokenByAddress(position.token0); - const token1 = uniswap.getTokenByAddress(position.token1); + // Get the pool associated with the position + const pool = await uniswap.getV3Pool(token0, token1, fee); + if (!pool) { + logger.warn(`Pool not found for position ${tokenId}`); + continue; + } + + // Calculate price range + const lowerPrice = tickToPrice( + token0, + token1, + tickLower, + ).toSignificant(6); + const upperPrice = tickToPrice( + token0, + token1, + tickUpper, + ).toSignificant(6); + + // Calculate current price + const price = pool.token0Price.toSignificant(6); + + // Create a Position instance to calculate token amounts + const position = new Position({ + pool, + tickLower, + tickUpper, + liquidity: liquidity.toString(), + }); + + // Get token amounts in the position + const token0Amount = formatTokenAmount( + position.amount0.quotient.toString(), + token0.decimals, + ); + const token1Amount = formatTokenAmount( + position.amount1.quotient.toString(), + token1.decimals, + ); + + // Determine which token is base and which is quote + const isBaseToken0 = + token0.symbol === 'WETH' || + (token1.symbol !== 'WETH' && + token0.address.toLowerCase() < token1.address.toLowerCase()); + + const [baseTokenAddress, quoteTokenAddress] = isBaseToken0 + ? [token0.address, token1.address] + : [token1.address, token0.address]; + + const [baseTokenAmount, quoteTokenAmount] = isBaseToken0 + ? [token0Amount, token1Amount] + : [token1Amount, token0Amount]; + + const [baseFeeAmount, quoteFeeAmount] = isBaseToken0 + ? [feeAmount0, feeAmount1] + : [feeAmount1, feeAmount0]; + + // Get the actual pool address using computePoolAddress + const poolAddress = computePoolAddress({ + factoryAddress: uniswap.config.uniswapV3FactoryAddress(network), + tokenA: token0, + tokenB: token1, + fee, + }); positions.push({ - tokenId: tokenId.toString(), - token0: token0.symbol, - token1: token1.symbol, - fee: position.fee / 10000, // Convert fee to percentage - tickLower: position.tickLower, - tickUpper: position.tickUpper, - liquidity: position.liquidity.toString(), + address: tokenId.toString(), + poolAddress, + baseTokenAddress, + quoteTokenAddress, + baseTokenAmount, + quoteTokenAmount, + baseFeeAmount, + quoteFeeAmount, + lowerBinId: tickLower, + upperBinId: tickUpper, + lowerPrice: parseFloat(lowerPrice), + upperPrice: parseFloat(upperPrice), + price: parseFloat(price), }); } catch (err) { logger.warn( @@ -180,6 +237,9 @@ export const positionsOwnedRoute: FastifyPluginAsync = async (fastify) => { return positions; } catch (e) { logger.error(e); + if (e.statusCode) { + throw e; + } throw fastify.httpErrors.internalServerError( 'Failed to fetch positions', ); diff --git a/src/connectors/uniswap/clmm-routes/removeLiquidity.ts b/src/connectors/uniswap/clmm-routes/removeLiquidity.ts index 14c390b2c6..12ed3aec57 100644 --- a/src/connectors/uniswap/clmm-routes/removeLiquidity.ts +++ b/src/connectors/uniswap/clmm-routes/removeLiquidity.ts @@ -1,6 +1,6 @@ import { Contract } from '@ethersproject/contracts'; -import { Percent } from '@uniswap/sdk-core'; -import { NonfungiblePositionManager } from '@uniswap/v3-sdk'; +import { Percent, CurrencyAmount } from '@uniswap/sdk-core'; +import { NonfungiblePositionManager, Position } from '@uniswap/v3-sdk'; import { BigNumber } from 'ethers'; import { FastifyPluginAsync } from 'fastify'; import JSBI from 'jsbi'; @@ -14,14 +14,14 @@ import { } from '../../../schemas/clmm-schema'; import { logger } from '../../../services/logger'; import { Uniswap } from '../uniswap'; -import { - getUniswapV3NftManagerAddress, - POSITION_MANAGER_ABI, - ERC20_ABI, -} from '../uniswap.contracts'; +import { POSITION_MANAGER_ABI } from '../uniswap.contracts'; import { formatTokenAmount } from '../uniswap.utils'; export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { + await fastify.register(require('@fastify/sensible')); + + const walletAddressExample = await Ethereum.getWalletAddressExample(); + fastify.post<{ Body: RemoveLiquidityRequestType; Reply: RemoveLiquidityResponseType; @@ -36,10 +36,11 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { properties: { ...RemoveLiquidityRequest.properties, network: { type: 'string', default: 'base' }, - walletAddress: { type: 'string', examples: ['0x...'] }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, positionAddress: { type: 'string', description: 'Position NFT token ID', + examples: ['1234'], }, percentageToRemove: { type: 'number', @@ -103,6 +104,16 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { const positionManagerAddress = uniswap.config.uniswapV3NftManagerAddress(networkToUse); + // Check NFT ownership + try { + await uniswap.checkNFTOwnership(positionAddress, walletAddress); + } catch (error: any) { + if (error.message.includes('is not owned by')) { + throw fastify.httpErrors.forbidden(error.message); + } + throw fastify.httpErrors.badRequest(error.message); + } + // Create position manager contract const positionManager = new Contract( positionManagerAddress, @@ -117,10 +128,11 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { const token0 = uniswap.getTokenByAddress(position.token0); const token1 = uniswap.getTokenByAddress(position.token1); - // Determine base and quote tokens - const baseTokenSymbol = - token0.symbol === 'WETH' ? token0.symbol : token1.symbol; - const isBaseToken0 = token0.symbol === baseTokenSymbol; + // Determine base and quote tokens - WETH or lower address is base + const isBaseToken0 = + token0.symbol === 'WETH' || + (token1.symbol !== 'WETH' && + token0.address.toLowerCase() < token1.address.toLowerCase()); // Get current liquidity const currentLiquidity = position.liquidity; @@ -136,110 +148,87 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { throw fastify.httpErrors.notFound('Pool not found for position'); } - // Calculate expected token amounts based on liquidity to remove - // This is a crude approximation - actual amounts will be calculated by the contract - const sqrtRatioX96 = BigNumber.from(pool.sqrtRatioX96.toString()); - const liquidity = BigNumber.from(liquidityToRemove.toString()); - - // Calculate token amounts using Uniswap V3 formulas (simplified) - const Q96 = BigNumber.from(2).pow(96); - let amount0, amount1; - - if ( - position.tickLower < pool.tickCurrent && - pool.tickCurrent < position.tickUpper - ) { - // Position straddles current tick - amount0 = liquidity - .mul(Q96) - .mul(BigNumber.from(Math.sqrt(2 ** 96)).sub(sqrtRatioX96)) - .div(sqrtRatioX96) - .div(Q96); - - amount1 = liquidity - .mul(sqrtRatioX96.sub(BigNumber.from(Math.sqrt(2 ** 96)))) - .div(Q96); - } else if (pool.tickCurrent <= position.tickLower) { - // Position is below current tick - amount0 = liquidity.mul(BigNumber.from(2).pow(96 / 2)).div(Q96); - amount1 = BigNumber.from(0); - } else { - // Position is above current tick - amount0 = BigNumber.from(0); - amount1 = liquidity.mul(BigNumber.from(2).pow(96 / 2)).div(Q96); - } + // Create a Position instance to calculate expected amounts + const positionSDK = new Position({ + pool, + tickLower: position.tickLower, + tickUpper: position.tickUpper, + liquidity: currentLiquidity.toString(), + }); + + // Calculate the amounts that will be withdrawn + const liquidityPercentage = new Percent( + Math.floor(percentageToRemove * 100), + 10000, + ); + const partialPosition = new Position({ + pool, + tickLower: position.tickLower, + tickUpper: position.tickUpper, + liquidity: JSBI.divide( + JSBI.multiply( + JSBI.BigInt(currentLiquidity.toString()), + JSBI.BigInt(liquidityPercentage.numerator.toString()), + ), + JSBI.BigInt(liquidityPercentage.denominator.toString()), + ), + }); + + // Get the expected amounts + const amount0 = partialPosition.amount0; + const amount1 = partialPosition.amount1; + + // Apply slippage tolerance + const slippageTolerance = new Percent(100, 10000); // 1% slippage + const amount0Min = amount0.multiply( + new Percent(1).subtract(slippageTolerance), + ).quotient; + const amount1Min = amount1.multiply( + new Percent(1).subtract(slippageTolerance), + ).quotient; + + // Also add any fees that have been collected to the expected amounts + const totalAmount0 = CurrencyAmount.fromRawAmount( + token0, + JSBI.add( + amount0.quotient, + JSBI.BigInt(position.tokensOwed0.toString()), + ), + ); + const totalAmount1 = CurrencyAmount.fromRawAmount( + token1, + JSBI.add( + amount1.quotient, + JSBI.BigInt(position.tokensOwed1.toString()), + ), + ); // Create parameters for removing liquidity const removeParams = { tokenId: positionAddress, - liquidityPercentage: new Percent( - Math.floor(percentageToRemove * 100), - 10000, - ), - slippageTolerance: new Percent(100, 10000), // 1% slippage tolerance + liquidityPercentage, + slippageTolerance, deadline: Math.floor(Date.now() / 1000) + 60 * 20, // 20 minutes from now burnToken: false, collectOptions: { - expectedCurrencyOwed0: amount0, - expectedCurrencyOwed1: amount1, + expectedCurrencyOwed0: totalAmount0, + expectedCurrencyOwed1: totalAmount1, recipient: walletAddress, }, }; - // For the sake of simplicity, we'll use a different approach - // We'd normally use NonfungiblePositionManager.removeCallParameters, but it may need custom parameters - // Here we'll construct a basic calldata for decreaseLiquidity and collect operations - - // Simplified approach to create calldata for removing some liquidity - const liquidityToRemoveStr = liquidityToRemove.toString(); - - const { calldata, value } = { - calldata: JSON.stringify([ - { - method: 'decreaseLiquidity', - params: { - tokenId: positionAddress, - liquidity: liquidityToRemoveStr, - amount0Min: amount0.toString(), - amount1Min: amount1.toString(), - deadline: Math.floor(Date.now() / 1000) + 60 * 20, - }, - }, - { - method: 'collect', - params: { - tokenId: positionAddress, - recipient: walletAddress, - amount0Max: amount0.toString(), - amount1Max: amount1.toString(), - }, - }, - ]), - value: '0', - }; - - // Initialize position manager with multicall interface - const positionManagerWithSigner = new Contract( - positionManagerAddress, - [ - { - inputs: [ - { internalType: 'bytes[]', name: 'data', type: 'bytes[]' }, - ], - name: 'multicall', - outputs: [ - { internalType: 'bytes[]', name: 'results', type: 'bytes[]' }, - ], - stateMutability: 'payable', - type: 'function', - }, - ], - wallet, - ); + // Get the calldata using the SDK + const { calldata, value } = + NonfungiblePositionManager.removeCallParameters( + positionSDK, + removeParams, + ); // Execute the transaction to remove liquidity - const tx = await positionManagerWithSigner.multicall([calldata], { - value: BigNumber.from(value.toString()), + const tx = await wallet.sendTransaction({ + to: positionManagerAddress, + data: calldata, + value: BigNumber.from(value), gasLimit: 500000, }); @@ -252,13 +241,13 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { 18, // ETH has 18 decimals ); - // Calculate token amounts removed (approximate) + // Calculate token amounts removed including fees const token0AmountRemoved = formatTokenAmount( - amount0.toString(), + totalAmount0.quotient.toString(), token0.decimals, ); const token1AmountRemoved = formatTokenAmount( - amount1.toString(), + totalAmount1.quotient.toString(), token1.decimals, ); diff --git a/src/connectors/uniswap/routes/execute-swap.ts b/src/connectors/uniswap/routes/execute-swap.ts index f8f2851dd4..f7787a7166 100644 --- a/src/connectors/uniswap/routes/execute-swap.ts +++ b/src/connectors/uniswap/routes/execute-swap.ts @@ -1,4 +1,3 @@ -import { Contract } from '@ethersproject/contracts'; import { BigNumber, ethers } from 'ethers'; import { FastifyPluginAsync } from 'fastify'; @@ -13,15 +12,6 @@ import { formatTokenAmount } from '../uniswap.utils'; import { getUniswapQuote } from './quote-swap'; -// Router02 ABI for executing swaps -const SwapRouter02ABI = { - inputs: [{ internalType: 'bytes', name: 'data', type: 'bytes' }], - name: 'multicall', - outputs: [{ internalType: 'bytes[]', name: 'results', type: 'bytes[]' }], - stateMutability: 'payable', - type: 'function', -}; - export const executeSwapRoute: FastifyPluginAsync = async ( fastify, _options, @@ -30,15 +20,13 @@ export const executeSwapRoute: FastifyPluginAsync = async ( await fastify.register(require('@fastify/sensible')); // Get first wallet address for example - const ethereum = await Ethereum.getInstance('mainnet'); - let firstWalletAddress = ''; + const walletAddressExample = await Ethereum.getWalletAddressExample(); - try { - firstWalletAddress = - (await ethereum.getFirstWalletAddress()) || firstWalletAddress; - } catch (error) { - logger.warn('No wallets found for examples in schema'); - } + // Get available networks from Ethereum configuration (same method as chain.routes.ts) + const { ConfigManagerV2 } = require('../../../services/config-manager-v2'); + const ethereumNetworks = Object.keys( + ConfigManagerV2.getInstance().get('ethereum.networks') || {}, + ); fastify.post<{ Body: ExecuteSwapRequestType; @@ -47,14 +35,17 @@ export const executeSwapRoute: FastifyPluginAsync = async ( '/execute-swap', { schema: { - description: - 'Execute a swap using Uniswap V3 Smart Order Router (mainnet only)', + description: 'Execute a swap using Uniswap V3 Smart Order Router', tags: ['uniswap'], body: { type: 'object', properties: { - network: { type: 'string', default: 'mainnet', enum: ['mainnet'] }, - walletAddress: { type: 'string', examples: [firstWalletAddress] }, + network: { + type: 'string', + default: 'mainnet', + enum: ethereumNetworks, + }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, baseToken: { type: 'string', examples: ['WETH'] }, quoteToken: { type: 'string', examples: ['USDC'] }, amount: { type: 'number', examples: [0.001] }, @@ -92,13 +83,6 @@ export const executeSwapRoute: FastifyPluginAsync = async ( const networkToUse = network || 'mainnet'; - // Only allow mainnet for quote swaps - if (networkToUse !== 'mainnet') { - throw fastify.httpErrors.badRequest( - `Uniswap router execution is only supported on mainnet. Current network: ${networkToUse}`, - ); - } - // Validate essential parameters if (!baseTokenSymbol || !quoteTokenSymbol || !amount || !side) { logger.error('Missing required parameters in request'); @@ -111,7 +95,7 @@ export const executeSwapRoute: FastifyPluginAsync = async ( // Get wallet address - either from request or first available let walletAddress = requestedWalletAddress; if (!walletAddress) { - walletAddress = await ethereum.getFirstWalletAddress(); + walletAddress = await Ethereum.getFirstWalletAddress(); if (!walletAddress) { return reply.badRequest( 'No wallet address provided and no default wallet found', @@ -147,16 +131,84 @@ export const executeSwapRoute: FastifyPluginAsync = async ( quoteToken, inputToken, outputToken, - inputAmount, + tradeAmount, slippageTolerance, exactIn, } = quoteResult; + // Log trade direction for clarity + logger.info( + `Trade direction: ${side} - ${exactIn ? 'EXACT_INPUT' : 'EXACT_OUTPUT'}`, + ); + logger.info( + `Input token: ${inputToken.symbol} (${inputToken.address})`, + ); + logger.info( + `Output token: ${outputToken.symbol} (${outputToken.address})`, + ); + logger.info( + `Estimated amounts: ${quoteResult.estimatedAmountIn} ${inputToken.symbol} -> ${quoteResult.estimatedAmountOut} ${outputToken.symbol}`, + ); + // Get the router address using getSpender from contracts const { getSpender } = require('../uniswap.contracts'); const routerAddress = getSpender(networkToUse, 'uniswap'); logger.info(`Using Swap Router address: ${routerAddress}`); + // Check balance of input token + logger.info( + `Checking balance of ${inputToken.symbol} for wallet ${walletAddress}`, + ); + let inputTokenBalance; + if (inputToken.symbol === 'ETH') { + // For native ETH, use getNativeBalance + inputTokenBalance = await ethereum.getNativeBalance(wallet); + } else { + // For ERC20 tokens (including WETH), use getERC20Balance + const contract = ethereum.getContract( + inputToken.address, + ethereum.provider, + ); + inputTokenBalance = await ethereum.getERC20Balance( + contract, + wallet, + inputToken.decimals, + 5000, // 5 second timeout + ); + } + const inputBalanceFormatted = Number( + formatTokenAmount( + inputTokenBalance.value.toString(), + inputToken.decimals, + ), + ); + logger.info(`${inputToken.symbol} balance: ${inputBalanceFormatted}`); + + // Calculate required input amount + const requiredInputAmount = exactIn + ? Number( + formatTokenAmount( + tradeAmount.quotient.toString(), + inputToken.decimals, + ), + ) + : Number( + formatTokenAmount( + route.quote.quotient.toString(), + inputToken.decimals, + ), + ); + + // Check if balance is sufficient + if (inputBalanceFormatted < requiredInputAmount) { + logger.error( + `Insufficient ${inputToken.symbol} balance: have ${inputBalanceFormatted}, need ${requiredInputAmount}`, + ); + throw fastify.httpErrors.badRequest( + `Insufficient ${inputToken.symbol} balance. You have ${inputBalanceFormatted} ${inputToken.symbol} but need ${requiredInputAmount} ${inputToken.symbol} to complete this swap.`, + ); + } + // If input token is not ETH, check allowance for the router if (inputToken.symbol !== 'ETH') { // Get token contract @@ -174,7 +226,9 @@ export const executeSwapRoute: FastifyPluginAsync = async ( ); // Calculate required amount - const amountNeeded = BigNumber.from(inputAmount.quotient.toString()); + const amountNeeded = exactIn + ? BigNumber.from(tradeAmount.quotient.toString()) + : BigNumber.from(route.quote.quotient.toString()); const currentAllowance = BigNumber.from(allowance.value); // Throw an error if allowance is insufficient @@ -204,29 +258,24 @@ export const executeSwapRoute: FastifyPluginAsync = async ( logger.info(`Calldata length: ${methodParameters.calldata.length}`); logger.info(`Value: ${methodParameters.value}`); - // Create the SwapRouter contract instance with the specific router address - const swapRouter = new Contract( - routerAddress, - [SwapRouter02ABI], - wallet, - ); - // Prepare transaction with gas settings from quote - const txOptions = { - value: methodParameters.value === '0x' ? '0' : methodParameters.value, + const txRequest = { + to: routerAddress, + data: methodParameters.calldata, + value: methodParameters.value, gasLimit: quoteResult.gasLimit || 350000, // Use estimated gas from quote gasPrice: ethers.utils.parseUnits( - quoteResult.gasPrice.toString(), + quoteResult.gasPrice.toFixed(9), // Limit to 9 decimal places for gwei 'gwei', ), // Convert the gas price from quote to wei }; - // Execute the swap using the multicall function - logger.info(`Executing swap via multicall to router: ${routerAddress}`); - const tx = await swapRouter.multicall( - methodParameters.calldata, - txOptions, + // Execute the swap by sending the transaction directly + logger.info(`Executing swap to router: ${routerAddress}`); + logger.info( + `Transaction data length: ${methodParameters.calldata.length}`, ); + const tx = await wallet.sendTransaction(txRequest); // Wait for transaction confirmation logger.info(`Transaction sent: ${tx.hash}`); @@ -240,7 +289,7 @@ export const executeSwapRoute: FastifyPluginAsync = async ( if (exactIn) { totalInputSwapped = Number( formatTokenAmount( - inputAmount.quotient.toString(), + tradeAmount.quotient.toString(), inputToken.decimals, ), ); @@ -256,7 +305,7 @@ export const executeSwapRoute: FastifyPluginAsync = async ( else { totalOutputSwapped = Number( formatTokenAmount( - inputAmount.quotient.toString(), + tradeAmount.quotient.toString(), outputToken.decimals, ), ); diff --git a/src/connectors/uniswap/routes/quote-swap.ts b/src/connectors/uniswap/routes/quote-swap.ts index bd6c83e015..b8371d5507 100644 --- a/src/connectors/uniswap/routes/quote-swap.ts +++ b/src/connectors/uniswap/routes/quote-swap.ts @@ -5,7 +5,7 @@ import { SwapRoute, SwapType, } from '@uniswap/smart-order-router'; -import { ethers } from 'ethers'; +import { ethers, BigNumber } from 'ethers'; import { FastifyPluginAsync, FastifyInstance } from 'fastify'; import { Ethereum } from '../../../chains/ethereum/ethereum'; @@ -14,6 +14,7 @@ import { GetSwapQuoteResponse, GetSwapQuoteRequestType, } from '../../../schemas/swap-schema'; +import { ConfigManagerV2 } from '../../../services/config-manager-v2'; import { logger } from '../../../services/logger'; import { formatTokenAmount } from '../uniswap.utils'; @@ -81,15 +82,42 @@ export async function getUniswapQuote( : [quoteToken, baseToken]; // Convert amount to token units with decimals - ensure proper precision - // For BUY orders with USDC as input, we need to ensure proper decimal handling - const scaleFactor = Math.pow(10, inputToken.decimals); - const scaledAmount = amount * scaleFactor; - const rawAmount = Math.floor(scaledAmount).toString(); - - logger.info( - `Amount conversion for ${inputToken.symbol} (decimals: ${inputToken.decimals}): ${amount} -> ${scaledAmount} -> ${rawAmount}`, - ); - const inputAmount = CurrencyAmount.fromRawAmount(inputToken, rawAmount); + // For BUY orders, amount represents the desired output (baseToken to buy) + // For SELL orders, amount represents the input (baseToken to sell) + let tradeAmount; + if (exactIn) { + // SELL: amount is the input amount (baseToken) + const scaleFactor = Math.pow(10, inputToken.decimals); + const scaledAmount = amount * scaleFactor; + const rawAmount = Math.floor(scaledAmount).toString(); + logger.info( + `SELL - Amount conversion for ${inputToken.symbol} (decimals: ${inputToken.decimals}): ${amount} -> ${scaledAmount} -> ${rawAmount}`, + ); + tradeAmount = CurrencyAmount.fromRawAmount(inputToken, rawAmount); + + // Debug: Verify the tradeAmount was created correctly + logger.info(`SELL - tradeAmount verification: + - toExact(): ${tradeAmount.toExact()} + - quotient: ${tradeAmount.quotient.toString()} + - currency.symbol: ${tradeAmount.currency.symbol} + - currency.address: ${tradeAmount.currency.address}`); + } else { + // BUY: amount is the desired output amount (baseToken to buy) + const scaleFactor = Math.pow(10, outputToken.decimals); + const scaledAmount = amount * scaleFactor; + const rawAmount = Math.floor(scaledAmount).toString(); + logger.info( + `BUY - Amount conversion for ${outputToken.symbol} (decimals: ${outputToken.decimals}): ${amount} -> ${scaledAmount} -> ${rawAmount}`, + ); + tradeAmount = CurrencyAmount.fromRawAmount(outputToken, rawAmount); + + // Debug: Verify the tradeAmount was created correctly + logger.info(`BUY - tradeAmount verification: + - toExact(): ${tradeAmount.toExact()} + - quotient: ${tradeAmount.quotient.toString()} + - currency.symbol: ${tradeAmount.currency.symbol} + - currency.address: ${tradeAmount.currency.address}`); + } // Calculate slippage tolerance const slippageTolerance = slippagePct @@ -107,14 +135,14 @@ export async function getUniswapQuote( type: SwapType.SWAP_ROUTER_02, // Explicitly use SwapRouter02 recipient, // Add recipient from parameter slippageTolerance, - deadline: Math.floor(Date.now() / 1000) + 1800, // 30 minutes + deadline: Math.floor(Date.now() / 1000) + 1800, // 30 minutes from now }; // Log the parameters being sent to the alpha router logger.info(`Alpha router params: - Input token: ${inputToken.symbol} (${inputToken.address}) - Output token: ${outputToken.symbol} (${outputToken.address}) - - Input amount: ${inputAmount.toExact()} (${rawAmount} in raw units) + - Trade amount: ${tradeAmount.toExact()} ${exactIn ? inputToken.symbol : outputToken.symbol} - Trade type: ${exactIn ? 'EXACT_INPUT' : 'EXACT_OUTPUT'} - Slippage tolerance: ${slippageTolerance.toFixed(2)}% - Chain ID: ${ethereum.chainId}`); @@ -123,7 +151,7 @@ export async function getUniswapQuote( // Add extra validation to ensure tokens are correctly formed // Simple logging, similar to v2.2.0 logger.info( - `Converting amount for ${inputToken.symbol} (decimals: ${inputToken.decimals}): ${amount} -> ${inputAmount.toExact()} -> ${rawAmount}`, + `Converting amount for ${exactIn ? inputToken.symbol : outputToken.symbol} (decimals: ${exactIn ? inputToken.decimals : outputToken.decimals}): ${amount} -> ${tradeAmount.toExact()}`, ); let route; @@ -133,25 +161,32 @@ export async function getUniswapQuote( `Fetching trade data for ${baseToken.address}-${quoteToken.address}`, ); - // Only support mainnet for alpha router routes - if (network !== 'mainnet') { - throw fastify.httpErrors.badRequest( - `Alpha router quotes are only supported on mainnet. Current network: ${network}`, - ); - } + // Log the network being used + logger.info(`Using AlphaRouter for network: ${network}`); + + // Let the AlphaRouter use its default configuration + // This will automatically select the best pools based on liquidity and price - // For mainnet, just eliminate splits which seems to be causing issues - const routingConfig = { - maxSplits: 0, // Disable splits for simplicity - distributionPercent: 100, // Use 100% for a single route - }; + // For EXACT_OUTPUT, we need to specify the currency we want to receive + // The tradeAmount should be the output currency amount + const currencyAmount = tradeAmount; + const otherCurrency = exactIn ? outputToken : inputToken; + + logger.info(`Calling alphaRouter.route with: + - currencyAmount: ${currencyAmount.toExact()} ${currencyAmount.currency.symbol} + - otherCurrency: ${otherCurrency.symbol} + - tradeType: ${exactIn ? 'EXACT_INPUT' : 'EXACT_OUTPUT'}`); + + // Debug the raw values being passed + logger.info( + `Debug currencyAmount raw: ${currencyAmount.quotient.toString()}`, + ); route = await alphaRouter.route( - inputAmount, - outputToken, + currencyAmount, + otherCurrency, exactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT, swapOptions, - routingConfig, ); } catch (routeError) { // Simple error logging like v2.2.0 @@ -177,18 +212,148 @@ export async function getUniswapQuote( `Route generation successful - has method parameters: ${!!route.methodParameters}`, ); + // Log pool selection details to debug fee tier issues + if (route.route && route.route.length > 0) { + route.route.forEach((pool, index) => { + if ('fee' in pool) { + logger.info( + `Route pool ${index + 1}: ${pool.token0.symbol}/${pool.token1.symbol} - Fee: ${pool.fee} (${pool.fee / 10000}%)`, + ); + } + }); + } + + // Debug: Log the methodParameters for all orders + if (route.methodParameters) { + logger.info(`${side} order methodParameters: + - calldata length: ${route.methodParameters.calldata.length} + - value: ${route.methodParameters.value} + - to: ${route.methodParameters.to}`); + + // Try to decode the calldata to see what function is being called + const calldataHex = route.methodParameters.calldata; + const functionSelector = calldataHex.slice(0, 10); + logger.info(`Function selector in calldata: ${functionSelector}`); + + // Common Uniswap V3 function selectors: + // 0x5ae401dc = multicall(uint256 deadline, bytes[] data) + // 0x5023b4df = exactInputSingle + // 0xdb3e2198 = exactOutputSingle + + // The amount should be somewhere in the calldata + if (calldataHex.length > 200 && functionSelector === '0x5ae401dc') { + // This is a multicall, need to decode the inner call + // For multicall(deadline, bytes[] data), the actual swap function is deeper in the calldata + // Let's trace through the calldata structure + + // Multicall parameters: + // 0x5ae401dc = multicall selector (4 bytes = 8 hex chars) + // deadline (32 bytes = 64 hex chars) starts at position 8 + // offset to data array (32 bytes = 64 hex chars) starts at position 72 + + const deadline = '0x' + calldataHex.slice(8, 72); + logger.info(`Deadline: ${parseInt(deadline, 16)}`); + + // The actual swap data starts later in the calldata + // Look for common swap function selectors in the data + const exactInputSingleSelector = '04e45aaf'; + const exactOutputSingleSelector = 'db3e2198'; + + const exactInputPos = calldataHex.indexOf(exactInputSingleSelector); + const exactOutputPos = calldataHex.indexOf(exactOutputSingleSelector); + + if (exactInputPos > -1) { + logger.info(`Found exactInputSingle at position ${exactInputPos}`); + const innerFunctionSelector = + '0x' + calldataHex.slice(exactInputPos, exactInputPos + 8); + logger.info(`Inner function selector: ${innerFunctionSelector}`); + + if (innerFunctionSelector === '0x04e45aaf') { + // exactInputSingle structure: + // tokenIn, tokenOut, fee, recipient, amountIn, amountOutMinimum, sqrtPriceLimitX96 + // Each param is 32 bytes (64 hex chars) + + const paramStart = exactInputPos + 8; // Skip function selector + let offset = paramStart; + + const tokenIn = '0x' + calldataHex.slice(offset + 24, offset + 64); + offset += 64; + const tokenOut = '0x' + calldataHex.slice(offset + 24, offset + 64); + offset += 64; + const fee = parseInt( + '0x' + calldataHex.slice(offset + 56, offset + 64), + 16, + ); + offset += 64; + const recipient = '0x' + calldataHex.slice(offset + 24, offset + 64); + offset += 64; + const amountInHex = '0x' + calldataHex.slice(offset, offset + 64); + offset += 64; + const minAmountOutHex = '0x' + calldataHex.slice(offset, offset + 64); + + logger.info(`exactInputSingle parameters: + - tokenIn: ${tokenIn} + - tokenOut: ${tokenOut} + - fee: ${fee} (${fee / 10000}%) + - recipient: ${recipient} + - amountIn: ${amountInHex} = ${BigNumber.from(amountInHex).toString()} + - minAmountOut: ${minAmountOutHex} = ${BigNumber.from(minAmountOutHex).toString()}`); + + // Check the actual amount being swapped + const amountInWei = BigNumber.from(amountInHex); + const amountInEther = Number( + formatTokenAmount(amountInWei.toString(), 18), + ); + logger.info(`Amount being swapped: ${amountInEther} WETH`); + + if (amountInEther === 0) { + logger.error(`CRITICAL: Swap amount is 0 WETH!`); + } + + if (exactIn) { + logger.info(`SELL order using exactInputSingle (expected)`); + } else { + logger.warn( + `BUY order is using exactInputSingle instead of exactOutputSingle!`, + ); + } + } + } else if (exactOutputPos > -1) { + logger.info(`Found exactOutputSingle at position ${exactOutputPos}`); + const innerFunctionSelector = + '0x' + calldataHex.slice(exactOutputPos, exactOutputPos + 8); + logger.info(`Inner function selector: ${innerFunctionSelector}`); + } + } + + // Log the trade details from the route + if (route.trade) { + logger.info(`Route trade details: + - inputAmount: ${route.trade.inputAmount.toExact()} ${route.trade.inputAmount.currency.symbol} + - outputAmount: ${route.trade.outputAmount.toExact()} ${route.trade.outputAmount.currency.symbol} + - tradeType: ${route.trade.tradeType}`); + } + } + // Simple route logging, similar to v2.2.0 logger.info( - `Best trade for ${baseToken.address}-${quoteToken.address}: ${route.quote.toExact()}${outputToken.symbol}.`, + `Best trade for ${baseToken.address}-${quoteToken.address}: ${route.quote.toExact()} ${exactIn ? outputToken.symbol : inputToken.symbol}`, ); + // Additional debug logging for BUY orders + if (!exactIn) { + logger.info( + `BUY order debug - tradeAmount: ${tradeAmount.toExact()} ${outputToken.symbol}, route.quote: ${route.quote.toExact()} ${inputToken.symbol}`, + ); + } + // Calculate amounts let estimatedAmountIn, estimatedAmountOut; // For SELL (exactIn), we know the exact input amount, output is estimated if (exactIn) { estimatedAmountIn = Number( - formatTokenAmount(inputAmount.quotient.toString(), inputToken.decimals), + formatTokenAmount(tradeAmount.quotient.toString(), inputToken.decimals), ); estimatedAmountOut = Number( @@ -198,7 +363,7 @@ export async function getUniswapQuote( // For BUY (exactOut), the output is exact, input is estimated else { estimatedAmountOut = Number( - formatTokenAmount(inputAmount.quotient.toString(), outputToken.decimals), + formatTokenAmount(tradeAmount.quotient.toString(), outputToken.decimals), ); estimatedAmountIn = Number( @@ -228,9 +393,13 @@ export async function getUniswapQuote( const quoteTokenBalanceChange = side === 'BUY' ? -estimatedAmountIn : estimatedAmountOut; - // Get gas estimate - const gasLimit = route.estimatedGasUsed?.toNumber() || 350000; + // Use fixed gas limit for Uniswap V3 swaps + const gasLimit = 300000; + logger.info(`Gas limit: using fixed ${gasLimit} for Uniswap V3 swap`); + const gasPrice = await ethereum.estimateGasPrice(); // Use ethereum's estimateGasPrice method + logger.info(`Gas price: ${gasPrice} GWEI from ethereum.estimateGasPrice()`); + const gasCost = gasPrice * gasLimit * 1e-9; // Convert to ETH return { @@ -239,7 +408,7 @@ export async function getUniswapQuote( quoteToken, inputToken, outputToken, - inputAmount, + tradeAmount, exactIn, estimatedAmountIn, estimatedAmountOut, @@ -260,15 +429,12 @@ export const quoteSwapRoute: FastifyPluginAsync = async (fastify, _options) => { await fastify.register(require('@fastify/sensible')); // Get first wallet address for example - const ethereum = await Ethereum.getInstance('mainnet'); - let firstWalletAddress = ''; + const walletAddressExample = await Ethereum.getWalletAddressExample(); - try { - firstWalletAddress = - (await ethereum.getFirstWalletAddress()) || firstWalletAddress; - } catch (error) { - logger.warn('No wallets found for examples in schema'); - } + // Get available networks from Ethereum configuration (same method as chain.routes.ts) + const ethereumNetworks = Object.keys( + ConfigManagerV2.getInstance().get('ethereum.networks') || {}, + ); fastify.get<{ Querystring: GetSwapQuoteRequestType; @@ -277,18 +443,22 @@ export const quoteSwapRoute: FastifyPluginAsync = async (fastify, _options) => { '/quote-swap', { schema: { - description: - 'Get a swap quote using Uniswap AlphaRouter (mainnet only)', + description: 'Get a swap quote using Uniswap AlphaRouter', tags: ['uniswap'], querystring: { type: 'object', properties: { - network: { type: 'string', default: 'mainnet', enum: ['mainnet'] }, + network: { + type: 'string', + default: 'mainnet', + enum: ethereumNetworks, + }, baseToken: { type: 'string', examples: ['WETH'] }, quoteToken: { type: 'string', examples: ['USDC'] }, amount: { type: 'number', examples: [0.001] }, side: { type: 'string', enum: ['BUY', 'SELL'], examples: ['SELL'] }, slippagePct: { type: 'number', examples: [0.5] }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, }, required: ['baseToken', 'quoteToken', 'amount', 'side'], }, diff --git a/src/connectors/uniswap/uniswap.config.ts b/src/connectors/uniswap/uniswap.config.ts index 95d11e4472..93706710f1 100644 --- a/src/connectors/uniswap/uniswap.config.ts +++ b/src/connectors/uniswap/uniswap.config.ts @@ -14,36 +14,8 @@ interface AvailableNetworks { networks: Array; } -// Export network arrays at the module level for direct import -export const uniswapNetworks = ['mainnet']; -export const uniswapAmmNetworks = [ - 'mainnet', - 'arbitrum', - 'optimism', - 'base', - 'sepolia', - 'bsc', - 'avalanche', - 'celo', - 'polygon', - 'blast', - 'zora', - 'worldchain', -]; -export const uniswapClmmNetworks = [ - 'mainnet', - 'arbitrum', - 'optimism', - 'base', - 'sepolia', - 'bsc', - 'avalanche', - 'celo', - 'polygon', - 'blast', - 'zora', - 'worldchain', -]; +// Networks are fetched directly from Ethereum configuration +// No need to maintain separate arrays export namespace UniswapConfig { export interface NetworkConfig { @@ -79,6 +51,11 @@ export namespace UniswapConfig { // Supported networks for the different Uniswap connectors export const chain = 'ethereum'; + // Get available networks from Ethereum configuration + export const networks: string[] = Object.keys( + ConfigManagerV2.getInstance().get('ethereum.networks') || {}, + ); + export const config: RootConfig = { // Global configuration allowedSlippage: ConfigManagerV2.getInstance().get( diff --git a/src/connectors/uniswap/uniswap.contracts.ts b/src/connectors/uniswap/uniswap.contracts.ts index 55da8c490a..73319a68ba 100644 --- a/src/connectors/uniswap/uniswap.contracts.ts +++ b/src/connectors/uniswap/uniswap.contracts.ts @@ -673,6 +673,26 @@ export const POSITION_MANAGER_ABI = [ }, ]; +/** + * Uniswap V2 Factory ABI for pair operations + */ +export const IUniswapV2FactoryABI = { + abi: [ + { + constant: true, + inputs: [ + { internalType: 'address', name: 'tokenA', type: 'address' }, + { internalType: 'address', name: 'tokenB', type: 'address' }, + ], + name: 'getPair', + outputs: [{ internalType: 'address', name: 'pair', type: 'address' }], + payable: false, + stateMutability: 'view', + type: 'function', + }, + ], +}; + /** * Standard ERC20 ABI for token operations */ diff --git a/src/connectors/uniswap/uniswap.ts b/src/connectors/uniswap/uniswap.ts index 8b7afec085..621f3d061a 100644 --- a/src/connectors/uniswap/uniswap.ts +++ b/src/connectors/uniswap/uniswap.ts @@ -1,122 +1,3 @@ -import { UniswapConfig } from './uniswap.config'; -import { - findPoolAddress, - isValidV2Pool, - isValidV3Pool, - isFractionString, -} from './uniswap.utils'; - -// V2 (AMM) imports - -// Define minimal ABIs for Uniswap V2 contracts -const IUniswapV2PairABI = { - abi: [ - { - constant: true, - inputs: [], - name: 'getReserves', - outputs: [ - { internalType: 'uint112', name: '_reserve0', type: 'uint112' }, - { internalType: 'uint112', name: '_reserve1', type: 'uint112' }, - { internalType: 'uint32', name: '_blockTimestampLast', type: 'uint32' }, - ], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: true, - inputs: [], - name: 'token0', - outputs: [{ internalType: 'address', name: '', type: 'address' }], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: true, - inputs: [], - name: 'token1', - outputs: [{ internalType: 'address', name: '', type: 'address' }], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: true, - inputs: [{ internalType: 'address', name: 'owner', type: 'address' }], - name: 'balanceOf', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: true, - inputs: [], - name: 'totalSupply', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: false, - inputs: [ - { internalType: 'address', name: 'spender', type: 'address' }, - { internalType: 'uint256', name: 'value', type: 'uint256' }, - ], - name: 'approve', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - ], -}; - -const IUniswapV2FactoryABI = { - abi: [ - { - constant: true, - inputs: [ - { internalType: 'address', name: 'tokenA', type: 'address' }, - { internalType: 'address', name: 'tokenB', type: 'address' }, - ], - name: 'getPair', - outputs: [{ internalType: 'address', name: 'pair', type: 'address' }], - payable: false, - stateMutability: 'view', - type: 'function', - }, - ], -}; - -const IUniswapV2RouterABI = { - abi: [ - { - inputs: [ - { internalType: 'address', name: 'tokenA', type: 'address' }, - { internalType: 'address', name: 'tokenB', type: 'address' }, - { internalType: 'uint256', name: 'amountADesired', type: 'uint256' }, - { internalType: 'uint256', name: 'amountBDesired', type: 'uint256' }, - { internalType: 'uint256', name: 'amountAMin', type: 'uint256' }, - { internalType: 'uint256', name: 'amountBMin', type: 'uint256' }, - { internalType: 'address', name: 'to', type: 'address' }, - { internalType: 'uint256', name: 'deadline', type: 'uint256' }, - ], - name: 'addLiquidity', - outputs: [ - { internalType: 'uint256', name: 'amountA', type: 'uint256' }, - { internalType: 'uint256', name: 'amountB', type: 'uint256' }, - { internalType: 'uint256', name: 'liquidity', type: 'uint256' }, - ], - stateMutability: 'nonpayable', - type: 'function', - }, - ], -}; - // V3 (CLMM) imports import { Token, CurrencyAmount, Percent } from '@uniswap/sdk-core'; import { AlphaRouter } from '@uniswap/smart-order-router'; @@ -132,6 +13,19 @@ import { Ethereum } from '../../chains/ethereum/ethereum'; import { percentRegexp } from '../../services/config-manager-v2'; import { logger } from '../../services/logger'; +import { UniswapConfig } from './uniswap.config'; +import { + IUniswapV2PairABI, + IUniswapV2FactoryABI, + IUniswapV2Router02ABI, +} from './uniswap.contracts'; +import { + findPoolAddress, + isValidV2Pool, + isValidV3Pool, + isFractionString, +} from './uniswap.utils'; + export class Uniswap { private static _instances: { [name: string]: Uniswap }; @@ -195,7 +89,7 @@ export class Uniswap { this.v2Router = new Contract( this.config.uniswapV2RouterAddress(this.networkName), - IUniswapV2RouterABI.abi, + IUniswapV2Router02ABI.abi, this.ethereum.provider, ); @@ -206,42 +100,40 @@ export class Uniswap { this.ethereum.provider, ); - // Define a minimal ABI for the NFT Manager - const NFTManagerABI = [ - { - inputs: [{ internalType: 'address', name: 'owner', type: 'address' }], - name: 'balanceOf', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - ]; - + // Initialize NFT Manager with minimal ABI this.v3NFTManager = new Contract( this.config.uniswapV3NftManagerAddress(this.networkName), - NFTManagerABI, + [ + { + inputs: [ + { internalType: 'address', name: 'owner', type: 'address' }, + ], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + ], this.ethereum.provider, ); - // Define a minimal ABI for the Quoter - const QuoterABI = [ - { - inputs: [ - { internalType: 'bytes', name: 'path', type: 'bytes' }, - { internalType: 'uint256', name: 'amountIn', type: 'uint256' }, - ], - name: 'quoteExactInput', - outputs: [ - { internalType: 'uint256', name: 'amountOut', type: 'uint256' }, - ], - stateMutability: 'nonpayable', - type: 'function', - }, - ]; - + // Initialize Quoter with minimal ABI this.v3Quoter = new Contract( this.config.quoterContractAddress(this.networkName), - QuoterABI, + [ + { + inputs: [ + { internalType: 'bytes', name: 'path', type: 'bytes' }, + { internalType: 'uint256', name: 'amountIn', type: 'uint256' }, + ], + name: 'quoteExactInput', + outputs: [ + { internalType: 'uint256', name: 'amountOut', type: 'uint256' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + ], this.ethereum.provider, ); @@ -617,13 +509,109 @@ export class Uniswap { */ public async getFirstWalletAddress(): Promise { try { - return await this.ethereum.getFirstWalletAddress(); + return await Ethereum.getFirstWalletAddress(); } catch (error) { logger.error(`Error getting first wallet address: ${error.message}`); return null; } } + /** + * Check NFT ownership for Uniswap V3 positions + * @param positionId The NFT position ID + * @param walletAddress The wallet address to check ownership for + * @throws Error if position is not owned by wallet or position ID is invalid + */ + public async checkNFTOwnership( + positionId: string, + walletAddress: string, + ): Promise { + const nftContract = new Contract( + this.config.uniswapV3NftManagerAddress(this.networkName), + [ + { + inputs: [ + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + ], + name: 'ownerOf', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + ], + this.ethereum.provider, + ); + + try { + const owner = await nftContract.ownerOf(positionId); + if (owner.toLowerCase() !== walletAddress.toLowerCase()) { + throw new Error( + `Position ${positionId} is not owned by wallet ${walletAddress}`, + ); + } + } catch (error: any) { + if (error.message.includes('is not owned by')) { + throw error; + } + throw new Error(`Invalid position ID ${positionId}`); + } + } + + /** + * Check NFT approval for Uniswap V3 positions + * @param positionId The NFT position ID + * @param walletAddress The wallet address that owns the NFT + * @param operatorAddress The address that needs approval (usually the position manager itself) + * @throws Error if NFT is not approved + */ + public async checkNFTApproval( + positionId: string, + walletAddress: string, + operatorAddress: string, + ): Promise { + const nftContract = new Contract( + this.config.uniswapV3NftManagerAddress(this.networkName), + [ + { + inputs: [ + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + ], + name: 'getApproved', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'address', name: 'operator', type: 'address' }, + ], + name: 'isApprovedForAll', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + ], + this.ethereum.provider, + ); + + // Check if the position manager itself is approved (it should be the operator) + const approvedAddress = await nftContract.getApproved(positionId); + const isApprovedForAll = await nftContract.isApprovedForAll( + walletAddress, + operatorAddress, + ); + + if ( + approvedAddress.toLowerCase() !== operatorAddress.toLowerCase() && + !isApprovedForAll + ) { + throw new Error( + `Insufficient NFT approval. Please approve the position NFT (${positionId}) for the Uniswap Position Manager (${operatorAddress})`, + ); + } + } + /** * Close the Uniswap instance and clean up resources */ diff --git a/src/services/config-manager-v2.ts b/src/services/config-manager-v2.ts index 3a7c770fc8..00dd1c0942 100644 --- a/src/services/config-manager-v2.ts +++ b/src/services/config-manager-v2.ts @@ -32,9 +32,9 @@ export const ConfigRootSchemaPath: string = path.join( 'configuration-root-schema.json', ); -// Always use conf directory for both configs and templates +// Use conf directory for configs and dist/src/templates for templates const ConfigDir: string = path.join(rootPath(), 'conf/'); -const ConfigTemplatesDir: string = ConfigDir; +const ConfigTemplatesDir: string = path.join(rootPath(), 'dist/src/templates/'); interface UnpackedConfigNamespace { namespace: ConfigurationNamespace; diff --git a/test/README.md b/test/README.md index 911086225f..cfb8e3f9f9 100644 --- a/test/README.md +++ b/test/README.md @@ -1,24 +1,49 @@ # Gateway Tests -This directory contains tests for the Gateway API. The test structure has been designed to be simple, maintainable, and easy to extend for community contributors. +This directory contains comprehensive test suites for the Gateway API. The test structure is designed to be modular, maintainable, and easy to extend. ## Test Structure ``` /test - /chains/ # Chain endpoint tests (ethereum.test.js, solana.test.js) + /chains/ # Chain endpoint tests + chain.test.js # Chain routes test + ethereum.test.js # Ethereum chain tests + solana.test.js # Solana chain tests /connectors/ # Connector endpoint tests by protocol /jupiter/ # Jupiter connector tests + swap.test.js # Swap operation tests /uniswap/ # Uniswap connector tests + amm.test.js # V2 AMM tests + clmm.test.js # V3 CLMM tests + swap.test.js # Universal Router tests /raydium/ # Raydium connector tests + amm.test.js # AMM operation tests + clmm.test.js # CLMM operation tests /meteora/ # Meteora connector tests + clmm.test.js # CLMM operation tests /mocks/ # Mock response data /chains/ # Chain mock responses + chains.json # Chain list response + /ethereum/ # Ethereum mock responses + balance.json + status.json + tokens.json + /solana/ # Solana mock responses + balance.json + status.json + tokens.json /connectors/ # Connector mock responses + connectors.json # Connector list response + /jupiter/ + /raydium/ + /meteora/ + /uniswap/ /services/ # Service tests /data/ # Test data files /wallet/ # Wallet tests - /utils/ # Test utilities + /config/ # Configuration tests + jest-setup.js # Test environment configuration ``` ## Running Tests @@ -27,18 +52,24 @@ This directory contains tests for the Gateway API. The test structure has been d # Run all tests pnpm test +# Run tests with coverage report +pnpm test:cov + +# Run tests in watch mode (for development) +pnpm test:debug + # Run chain tests only GATEWAY_TEST_MODE=dev jest --runInBand test/chains -# Run connector tests only -GATEWAY_TEST_MODE=dev jest --runInBand test/connectors +# Run specific connector tests +GATEWAY_TEST_MODE=dev jest --runInBand test/connectors/uniswap +GATEWAY_TEST_MODE=dev jest --runInBand test/connectors/raydium/amm.test.js -# Run services tests only -GATEWAY_TEST_MODE=dev jest --runInBand test/services - -# Run specific test +# Run a single test file GATEWAY_TEST_MODE=dev jest --runInBand test/chains/ethereum.test.js -GATEWAY_TEST_MODE=dev jest --runInBand test/connectors/jupiter/swap.test.js + +# Clear Jest cache if tests are behaving unexpectedly +pnpm test:clear-cache ``` ## Test Setup and Configuration @@ -66,55 +97,127 @@ The test environment is configured in `test/jest-setup.js`, which: ### Test Environment Variables -Tests use the following environment variables: +| Variable | Description | Required | +|----------|-------------|----------| +| `GATEWAY_TEST_MODE=dev` | Runs tests with mocked blockchain connections | Yes | +| `START_SERVER=true` | Required when starting the actual server | No (tests only) | -- `GATEWAY_TEST_MODE=dev` - Runs tests in development mode without requiring real blockchain connections -- Running tests without this variable will attempt to connect to real networks and may fail +**Note**: Always use `GATEWAY_TEST_MODE=dev` for unit tests to avoid real blockchain connections ## Mock Responses -All tests use mock responses stored in JSON files in the `test/mocks` directory, organized by chain and connector. These files can be easily updated to match current API responses for verification or to add new test cases. - -Directory structure for mocks: -``` -/mocks - /chains/ - /ethereum/ # Generic Ethereum mock responses - /solana/ # Generic Solana mock responses - /connectors/ - /jupiter/ # Jupiter connector mock responses - /raydium/ # Raydium connector mock responses - /meteora/ # Meteora connector mock responses - /uniswap/ # Uniswap connector mock responses +Tests use mock responses stored in JSON files in the `test/mocks` directory. This approach ensures: +- Tests run without blockchain connections +- Consistent test results +- Fast test execution +- CI/CD compatibility + +### Mock File Naming Convention + +| Operation | Mock File Name | +|-----------|----------------|| +| Chain status | `status.json` | +| Token balances | `balance.json` | +| Token info | `tokens.json` | +| Pool info | `{type}-pool-info.json` | +| Swap quote | `{type}-quote-swap.json` | +| Position info | `{type}-position-info.json` | + +Where `{type}` is either `amm` or `clmm`. + +### Updating Mock Responses + +1. **Start Gateway locally**: + ```bash + pnpm start --passphrase=test --dev + ``` + +2. **Make API calls** to get real responses: + ```bash + curl http://localhost:15888/chains/ethereum/status + ``` + +3. **Save responses** in the appropriate mock file: + ```bash + # Example: Save Ethereum status response + curl http://localhost:15888/chains/ethereum/status > test/mocks/chains/ethereum/status.json + ``` + +4. **Verify tests** pass with updated mocks: + ```bash + GATEWAY_TEST_MODE=dev jest --runInBand test/chains/ethereum.test.js + ``` + +## Writing Tests + +### Test Structure Example + +```javascript +// test/connectors/uniswap/amm.test.js +describe('Uniswap AMM Routes', () => { + const mockApp = { + inject: (options) => { + // Mock implementation + } + }; + + beforeEach(() => { + // Setup mocks + }); + + it('should return pool information', async () => { + const response = await mockApp.inject({ + method: 'GET', + url: '/connectors/uniswap/amm/pool-info', + query: { + chain: 'ethereum', + network: 'mainnet', + tokenA: 'USDC', + tokenB: 'WETH' + } + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toMatchObject({ + poolAddress: expect.any(String), + token0: expect.any(String), + token1: expect.any(String) + }); + }); +}); ``` -To update mock responses with live data: +### Testing Best Practices + +1. **Use descriptive test names** that explain what is being tested +2. **Test both success and error cases** +3. **Verify response structure** matches TypeBox schemas +4. **Mock external dependencies** (blockchain calls, API requests) +5. **Keep tests isolated** - each test should be independent +6. **Use beforeEach/afterEach** for setup and cleanup + +### Coverage Requirements -1. Run the Gateway API locally -2. Make the API calls you want to test -3. Save the responses as JSON files in the appropriate mock directory -4. Run the tests to verify they work with the updated mock data +- New features must have **minimum 75% code coverage** +- Run `pnpm test:cov` to check coverage +- Coverage reports are generated in `/coverage` directory -This approach ensures the tests run without requiring actual network connections, making them suitable for CI/CD environments. +## Troubleshooting Tests -## Supported Schema Types +### Common Issues -Gateway supports the following schema types for DEX connectors: +1. **Tests timing out** + - Increase timeout in specific test: `jest.setTimeout(30000)` + - Check for unresolved promises -1. **Swap Schema**: Basic token swap operations common to all DEXs - - Quote Swap: Get price quotes for token swaps - - Execute Swap: Execute token swaps between pairs +2. **Mock data mismatch** + - Update mock files with current API responses + - Verify mock file paths are correct -2. **AMM Schema**: Automated Market Maker operations - - Pool Info: Get information about liquidity pools - - Add Liquidity: Add liquidity to pools - - Remove Liquidity: Remove liquidity from pools - - Position Info: Get information about liquidity positions +3. **Module not found errors** + - Clear Jest cache: `pnpm test:clear-cache` + - Check import paths use correct aliases -3. **CLMM Schema**: Concentrated Liquidity Market Maker operations - - Pool Info: Get information about concentrated liquidity pools - - Open Position: Open a new concentrated liquidity position - - Close Position: Close a concentrated liquidity position - - Add Liquidity: Add liquidity to a position - - Remove Liquidity: Remove liquidity from a position - - Collect Fees: Collect fees from a position \ No newline at end of file +4. **Native module errors** + - These are handled by `jest-setup.js` + - If new errors appear, add mocks to setup file \ No newline at end of file diff --git a/test/connectors/jupiter/swap.test.js b/test/connectors/jupiter/swap.test.js index a35a842d10..cb01c2a7d0 100644 --- a/test/connectors/jupiter/swap.test.js +++ b/test/connectors/jupiter/swap.test.js @@ -6,8 +6,6 @@ const axios = require('axios'); // Constants for this test file const CONNECTOR = 'jupiter'; -const PROTOCOL = 'swap'; -const CHAIN = 'solana'; const NETWORK = 'mainnet-beta'; // Only test mainnet-beta const BASE_TOKEN = 'SOL'; const QUOTE_TOKEN = 'USDC'; diff --git a/test/connectors/meteora/clmm.test.js b/test/connectors/meteora/clmm.test.js index 6e9e79221f..0f68049fae 100644 --- a/test/connectors/meteora/clmm.test.js +++ b/test/connectors/meteora/clmm.test.js @@ -7,7 +7,6 @@ const axios = require('axios'); // Constants for this test file const CONNECTOR = 'meteora'; const PROTOCOL = 'clmm'; -const CHAIN = 'solana'; const NETWORK = 'mainnet-beta'; const BASE_TOKEN = 'SOL'; const QUOTE_TOKEN = 'USDC'; diff --git a/test/connectors/raydium/amm.test.js b/test/connectors/raydium/amm.test.js index 6974d083fb..fdf74312db 100644 --- a/test/connectors/raydium/amm.test.js +++ b/test/connectors/raydium/amm.test.js @@ -7,7 +7,6 @@ const axios = require('axios'); // Constants for this test file const CONNECTOR = 'raydium'; const PROTOCOL = 'amm'; -const CHAIN = 'solana'; const NETWORK = 'mainnet-beta'; const BASE_TOKEN = 'SOL'; const QUOTE_TOKEN = 'USDC'; diff --git a/test/connectors/raydium/clmm.test.js b/test/connectors/raydium/clmm.test.js index 210a8c0a22..3dc37e9c21 100644 --- a/test/connectors/raydium/clmm.test.js +++ b/test/connectors/raydium/clmm.test.js @@ -7,7 +7,6 @@ const axios = require('axios'); // Constants for this test file const CONNECTOR = 'raydium'; const PROTOCOL = 'clmm'; -const CHAIN = 'solana'; const NETWORK = 'mainnet-beta'; const BASE_TOKEN = 'SOL'; const QUOTE_TOKEN = 'USDC'; diff --git a/test/connectors/uniswap/amm.test.js b/test/connectors/uniswap/amm.test.js index 248d6c3cd0..e920e3bbb7 100644 --- a/test/connectors/uniswap/amm.test.js +++ b/test/connectors/uniswap/amm.test.js @@ -7,7 +7,6 @@ const axios = require('axios'); // Constants for this test file const CONNECTOR = 'uniswap'; const PROTOCOL = 'amm'; -const CHAIN = 'ethereum'; const NETWORK = 'base'; // Only test Base network const BASE_TOKEN = 'WETH'; const QUOTE_TOKEN = 'USDC'; @@ -212,7 +211,9 @@ describe('Uniswap AMM Tests (Base Network)', () => { baseTokenBalanceChange: 1.0, // Positive for BUY quoteTokenBalanceChange: -mockSellResponse.quoteTokenBalanceChange, // Negative for BUY // For BUY: price = quote needed / base received - price: mockSellResponse.estimatedAmountOut / mockSellResponse.estimatedAmountIn, + price: + mockSellResponse.estimatedAmountOut / + mockSellResponse.estimatedAmountIn, }; // Setup mock axios diff --git a/test/connectors/uniswap/clmm.test.js b/test/connectors/uniswap/clmm.test.js index d88ff3f17f..84c4450036 100644 --- a/test/connectors/uniswap/clmm.test.js +++ b/test/connectors/uniswap/clmm.test.js @@ -7,7 +7,6 @@ const axios = require('axios'); // Constants for this test file const CONNECTOR = 'uniswap'; const PROTOCOL = 'clmm'; -const CHAIN = 'ethereum'; const NETWORK = 'base'; // Only test Base network const BASE_TOKEN = 'WETH'; const QUOTE_TOKEN = 'USDC'; @@ -208,7 +207,9 @@ describe('Uniswap CLMM Tests (Base Network)', () => { baseTokenBalanceChange: 1.0, // Positive for BUY quoteTokenBalanceChange: -mockSellResponse.quoteTokenBalanceChange, // Negative for BUY // For BUY: price = quote needed / base received - price: mockSellResponse.estimatedAmountOut / mockSellResponse.estimatedAmountIn, + price: + mockSellResponse.estimatedAmountOut / + mockSellResponse.estimatedAmountIn, }; // Setup mock axios diff --git a/test/connectors/uniswap/mocks/quote-swap.json b/test/connectors/uniswap/mocks/quote-swap.json index 3fa95931c7..f66f74b7b4 100644 --- a/test/connectors/uniswap/mocks/quote-swap.json +++ b/test/connectors/uniswap/mocks/quote-swap.json @@ -7,6 +7,6 @@ "baseTokenBalanceChange": -1.0, "quoteTokenBalanceChange": 1800.0, "gasPrice": 5.0, - "gasLimit": 250000, - "gasCost": 0.00125 + "gasLimit": 300000, + "gasCost": 0.0015 } \ No newline at end of file diff --git a/test/connectors/uniswap/swap.test.js b/test/connectors/uniswap/swap.test.js index 06da0b3285..4b7bc989ae 100644 --- a/test/connectors/uniswap/swap.test.js +++ b/test/connectors/uniswap/swap.test.js @@ -7,7 +7,7 @@ const axios = require('axios'); // Constants for this test file const CONNECTOR = 'uniswap'; const CHAIN = 'ethereum'; -const NETWORK = 'base'; // Only test Base network +const NETWORK = 'base'; // Testing with Base network, but all Ethereum networks are supported const BASE_TOKEN = 'WETH'; const QUOTE_TOKEN = 'USDC'; const TEST_WALLET = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'; @@ -53,6 +53,19 @@ function validateSwapQuote(response) { ); } +// Function to validate gas parameters +function validateGasParameters(response) { + return ( + response && + typeof response.gasPrice === 'number' && + response.gasPrice > 0 && + response.gasLimit === 300000 && // Fixed gas limit per implementation + typeof response.gasCost === 'number' && + Math.abs(response.gasCost - response.gasPrice * response.gasLimit * 1e-9) < + 1e-10 // Floating point precision + ); +} + // Tests describe('Uniswap V3 Swap Router Tests (Base Network)', () => { beforeEach(() => { @@ -78,8 +91,8 @@ describe('Uniswap V3 Swap Router Tests (Base Network)', () => { baseTokenBalanceChange: -1.0, quoteTokenBalanceChange: 1800.0, gasPrice: 5.0, - gasLimit: 250000, - gasCost: 0.00125, + gasLimit: 300000, + gasCost: 0.0015, }; } @@ -106,6 +119,7 @@ describe('Uniswap V3 Swap Router Tests (Base Network)', () => { // Validate the response expect(response.status).toBe(200); expect(validateSwapQuote(response.data)).toBe(true); + expect(validateGasParameters(response.data)).toBe(true); // Check expected mock values for a SELL expect(response.data.baseTokenBalanceChange).toBeLessThan(0); // SELL means negative base token change @@ -154,8 +168,8 @@ describe('Uniswap V3 Swap Router Tests (Base Network)', () => { baseTokenBalanceChange: 1.0, quoteTokenBalanceChange: -1800.0, gasPrice: 5.0, - gasLimit: 250000, - gasCost: 0.00125, + gasLimit: 300000, + gasCost: 0.0015, }; } @@ -182,11 +196,96 @@ describe('Uniswap V3 Swap Router Tests (Base Network)', () => { // Validate the response expect(response.status).toBe(200); expect(validateSwapQuote(response.data)).toBe(true); + expect(validateGasParameters(response.data)).toBe(true); // Check expected mock values for a BUY expect(response.data.baseTokenBalanceChange).toBeGreaterThan(0); // BUY means positive base token change expect(response.data.quoteTokenBalanceChange).toBeLessThan(0); // BUY means negative quote token change }); + + test('validates gas parameters for swap quotes', async () => { + // Create a mock response with specific gas values + const mockResponse = { + estimatedAmountIn: 0.001, + estimatedAmountOut: 2.49, + minAmountOut: 2.47, + maxAmountIn: 0.001, + price: 2490.0, + baseTokenBalanceChange: -0.001, + quoteTokenBalanceChange: 2.49, + gasPrice: 0.8, // Testing realistic mainnet gas price + gasLimit: 300000, // Fixed gas limit as per implementation + gasCost: 0.00024, // 0.8 GWEI * 300000 / 1e9 + }; + + // Setup mock axios + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + // Make the request + const response = await axios.get( + `http://localhost:15888/connectors/${CONNECTOR}/routes/quote-swap`, + { + params: { + network: 'mainnet', + baseToken: BASE_TOKEN, + quoteToken: QUOTE_TOKEN, + side: 'SELL', + amount: 0.001, + }, + }, + ); + + // Validate gas parameters + expect(response.status).toBe(200); + expect(response.data.gasLimit).toBe(300000); // Fixed gas limit + expect(response.data.gasPrice).toBeGreaterThan(0); + expect(response.data.gasCost).toBe( + response.data.gasPrice * response.data.gasLimit * 1e-9, + ); + }); + + test('handles different networks correctly', async () => { + const networks = ['mainnet', 'arbitrum', 'optimism', 'base', 'polygon']; + + for (const network of networks) { + const mockResponse = { + estimatedAmountIn: 1.0, + estimatedAmountOut: 1800.0, + minAmountOut: 1782.0, + maxAmountIn: 1.0, + price: 1800.0, + baseTokenBalanceChange: -1.0, + quoteTokenBalanceChange: 1800.0, + gasPrice: 5.0, + gasLimit: 300000, + gasCost: 0.0015, + }; + + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + const response = await axios.get( + `http://localhost:15888/connectors/${CONNECTOR}/routes/quote-swap`, + { + params: { + network, + baseToken: BASE_TOKEN, + quoteToken: QUOTE_TOKEN, + side: 'SELL', + amount: 1.0, + }, + }, + ); + + expect(response.status).toBe(200); + expect(validateSwapQuote(response.data)).toBe(true); + } + }); }); describe('Execute Swap Endpoint', () => { @@ -261,5 +360,121 @@ describe('Uniswap V3 Swap Router Tests (Base Network)', () => { }), ); }); + + test('executes BUY swap successfully', async () => { + // Create a BUY quote response + const buyQuoteResponse = { + estimatedAmountIn: 2500.0, // USDC needed + estimatedAmountOut: 1.0, // WETH to receive + minAmountOut: 1.0, + maxAmountIn: 2525.0, // with slippage + price: 2500.0, + baseTokenBalanceChange: 1.0, + quoteTokenBalanceChange: -2500.0, + }; + + // Mock a successful BUY execution response + const executeBuyResponse = { + signature: + '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', + totalInputSwapped: buyQuoteResponse.estimatedAmountIn, + totalOutputSwapped: buyQuoteResponse.estimatedAmountOut, + fee: 0.003, + baseTokenBalanceChange: buyQuoteResponse.baseTokenBalanceChange, + quoteTokenBalanceChange: buyQuoteResponse.quoteTokenBalanceChange, + }; + + axios.post.mockResolvedValueOnce({ + status: 200, + data: executeBuyResponse, + }); + + const response = await axios.post( + `http://localhost:15888/connectors/${CONNECTOR}/routes/execute-swap`, + { + network: NETWORK, + baseToken: BASE_TOKEN, + quoteToken: QUOTE_TOKEN, + side: 'BUY', + amount: 1.0, // Want to buy 1 WETH + walletAddress: TEST_WALLET, + }, + ); + + expect(response.status).toBe(200); + expect(response.data.signature).toBeDefined(); + expect(response.data.totalInputSwapped).toBe(2500.0); // USDC spent + expect(response.data.totalOutputSwapped).toBe(1.0); // WETH received + expect(response.data.baseTokenBalanceChange).toBe(1.0); // +1 WETH + expect(response.data.quoteTokenBalanceChange).toBe(-2500.0); // -2500 USDC + }); + + test('validates slippage parameters', async () => { + const executeResponse = { + signature: '0x123...', + totalInputSwapped: 1.0, + totalOutputSwapped: 1790.0, + fee: 0.003, + baseTokenBalanceChange: -1.0, + quoteTokenBalanceChange: 1790.0, + }; + + axios.post.mockResolvedValueOnce({ + status: 200, + data: executeResponse, + }); + + const response = await axios.post( + `http://localhost:15888/connectors/${CONNECTOR}/routes/execute-swap`, + { + network: NETWORK, + baseToken: BASE_TOKEN, + quoteToken: QUOTE_TOKEN, + side: 'SELL', + amount: 1.0, + walletAddress: TEST_WALLET, + slippagePct: 1.0, // 1% slippage + }, + ); + + expect(response.status).toBe(200); + // With 1% slippage, the output should be at least 99% of expected + expect(response.data.totalOutputSwapped).toBeGreaterThanOrEqual(1782.0); + }); + + test('handles multiple networks for execution', async () => { + const networks = ['mainnet', 'arbitrum', 'optimism', 'base']; + + for (const network of networks) { + const executeResponse = { + signature: `0x${network}1234567890abcdef`, + totalInputSwapped: 1.0, + totalOutputSwapped: 1800.0, + fee: 0.003, + baseTokenBalanceChange: -1.0, + quoteTokenBalanceChange: 1800.0, + }; + + axios.post.mockResolvedValueOnce({ + status: 200, + data: executeResponse, + }); + + const response = await axios.post( + `http://localhost:15888/connectors/${CONNECTOR}/routes/execute-swap`, + { + network, + baseToken: BASE_TOKEN, + quoteToken: QUOTE_TOKEN, + side: 'SELL', + amount: 1.0, + walletAddress: TEST_WALLET, + }, + ); + + expect(response.status).toBe(200); + expect(response.data.signature).toContain(network); + } + }); }); }); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000000..18049c3487 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "src", + "incremental": true, + "composite": true, + "tsBuildInfoFile": "./dist/.tsbuildinfo", + "skipLibCheck": true, + "sourceMap": true, + "inlineSourceMap": false + }, + "exclude": ["node_modules", "dist", "coverage", "test", "test-scripts"] +} diff --git a/tsconfig.json b/tsconfig.json index 7785ece1af..2ce6b75bfa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,12 @@ { "compilerOptions": { "strict": false, - // "types": ["jest", "node"], "target": "ESNext", "module": "CommonJS", "allowJs": true, - // "lib": ["es6", "es2020", "esnext.asynciterable", "dom"], - // "sourceMap": true, - "outDir": "./dist", - // "moduleResolution": "node", + "inlineSourceMap": true, "removeComments": true, "noImplicitAny": false, - // "strictNullChecks": false, "strictFunctionTypes": true, "noImplicitThis": true, "noUnusedLocals": false, @@ -23,16 +18,25 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "resolveJsonModule": true, + "baseUrl": ".", + "paths": { + "#src": ["src/index.ts"], + "#src/*": ["src/*"], + "#test": ["test"], + "#test/*": ["test/*"] + }, "typeRoots": ["node_modules/@types", "src/@types"], "downlevelIteration": true, "skipLibCheck": true }, "exclude": [ "node_modules", - "test" + "dist", + "coverage" ], "include": [ "src/**/*.ts", - "src/**/*.js" + "test/**/*.ts", + "test-scripts/**/*.ts" ] }