From eef2ede41ef460e61e128cff60c776caeb2598fa Mon Sep 17 00:00:00 2001 From: rapcmia Date: Tue, 10 Jun 2025 11:23:22 +0800 Subject: [PATCH 01/31] bump version tyo 2.7 --- package.json | 2 +- src/app.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 14371ff352..ce0b63742f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gateway", - "version": "dev-2.6.0", + "version": "dev-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", diff --git a/src/app.ts b/src/app.ts index 37333b6b18..687c608164 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 = 'dev-2.6.0'; +const GATEWAY_VERSION = 'dev-2.7.0'; // At the top level, define devMode once // When true, runs server in HTTP mode (less secure but useful for development) From d8291f83ad6bf3f3bd089600e9123c37047b2045 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Mon, 23 Jun 2025 14:19:06 -0700 Subject: [PATCH 02/31] fix: correct ConfigTemplatesDir path for Docker container initialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Changed ConfigTemplatesDir from ConfigDir to 'dist/src/templates/' - Fixes ENOENT error when starting Docker container with empty volumes - Ensures template files can be properly copied from build directory to conf directory 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/services/config-manager-v2.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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; From 5a658f009b49bf3255226e9a7a3900baf715b02f Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Mon, 23 Jun 2025 17:38:45 -0700 Subject: [PATCH 03/31] fix: fix Uniswap AMM remove-liquidity error handling and improve error messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Register @fastify/sensible plugin to enable httpErrors functionality - Add LP token address to insufficient allowance error message for clarity - Fix error type annotation in catch block The remove-liquidity endpoint now properly returns BadRequest errors with helpful messages that include the LP token address (pool address) that needs approval. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/connectors/uniswap/amm-routes/positionInfo.ts | 2 +- src/connectors/uniswap/amm-routes/removeLiquidity.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/connectors/uniswap/amm-routes/positionInfo.ts b/src/connectors/uniswap/amm-routes/positionInfo.ts index cad58c723e..ffc406dd83 100644 --- a/src/connectors/uniswap/amm-routes/positionInfo.ts +++ b/src/connectors/uniswap/amm-routes/positionInfo.ts @@ -31,7 +31,7 @@ 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})`, ); } } diff --git a/src/connectors/uniswap/amm-routes/removeLiquidity.ts b/src/connectors/uniswap/amm-routes/removeLiquidity.ts index fe9553b9b9..0237fa6511 100644 --- a/src/connectors/uniswap/amm-routes/removeLiquidity.ts +++ b/src/connectors/uniswap/amm-routes/removeLiquidity.ts @@ -22,6 +22,8 @@ import { formatTokenAmount } from '../uniswap.utils'; import { checkLPAllowance } from './positionInfo'; export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { + await fastify.register(require('@fastify/sensible')); + // Get first wallet address for example const ethereum = await Ethereum.getInstance('base'); let firstWalletAddress = ''; @@ -203,7 +205,7 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { routerAddress, liquidityToRemove, ); - } catch (error) { + } catch (error: any) { throw fastify.httpErrors.badRequest(error.message); } From d85592d9364b22dcff1281fde1350375421352ed Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Mon, 23 Jun 2025 18:36:27 -0700 Subject: [PATCH 04/31] fix: remove unnecessary NFT approval checks from Uniswap V3 operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove NFT approval checks from removeLiquidity and collectFees endpoints - Delete unused approveNFT endpoint and route registration - Uniswap V3 Position Manager doesn't require approval for positions it minted - Fix WETH allowance checks to treat WETH like any other ERC20 token - Update positionsOwned to return standard PositionInfo objects - Enhance ethereum/allowances to accept token addresses directly - Add LP token address to error messages for clarity - Use Uniswap V3 SDK Position class for accurate calculations - Add firstWalletAddress logic to CLMM endpoints - Move shared functions and ABIs to appropriate locations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/chains/ethereum/routes/allowances.ts | 82 +++++- .../uniswap/clmm-routes/collectFees.ts | 107 ++++--- .../uniswap/clmm-routes/executeSwap.ts | 32 +++ .../uniswap/clmm-routes/openPosition.ts | 50 +--- .../uniswap/clmm-routes/positionInfo.ts | 34 +-- .../uniswap/clmm-routes/positionsOwned.ts | 172 +++++++---- .../uniswap/clmm-routes/removeLiquidity.ts | 207 +++++++------- src/connectors/uniswap/uniswap.contracts.ts | 20 ++ src/connectors/uniswap/uniswap.ts | 268 +++++++++--------- test/connectors/jupiter/swap.test.js | 2 - test/connectors/meteora/clmm.test.js | 1 - test/connectors/raydium/amm.test.js | 1 - test/connectors/raydium/clmm.test.js | 1 - test/connectors/uniswap/amm.test.js | 5 +- test/connectors/uniswap/clmm.test.js | 5 +- 15 files changed, 530 insertions(+), 457 deletions(-) diff --git a/src/chains/ethereum/routes/allowances.ts b/src/chains/ethereum/routes/allowances.ts index 0cdaf5fee9..ae930c3d7c 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,55 @@ 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 +87,26 @@ 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(), - ), - ); - - if (missingTokens.length > 0) { - logger.warn(`Some tokens were not found: ${missingTokens.join(', ')}`); + // 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()); + + 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 @@ -162,7 +209,14 @@ 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/connectors/uniswap/clmm-routes/collectFees.ts b/src/connectors/uniswap/clmm-routes/collectFees.ts index 1f13f1f6a4..0c18993a7b 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,23 @@ 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')); + + // 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'); + } + fastify.post<{ Body: CollectFeesRequestType; Reply: CollectFeesResponseType; @@ -61,10 +44,11 @@ export const collectFeesRoute: FastifyPluginAsync = async (fastify) => { properties: { ...CollectFeesRequest.properties, network: { type: 'string', default: 'base' }, - walletAddress: { type: 'string', examples: ['0x...'] }, + walletAddress: { type: 'string', examples: [firstWalletAddress] }, positionAddress: { type: 'string', description: 'Position NFT token ID', + examples: ['1234'], }, }, }, @@ -115,6 +99,17 @@ 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 +124,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 +139,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 +161,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..32357c3851 100644 --- a/src/connectors/uniswap/clmm-routes/executeSwap.ts +++ b/src/connectors/uniswap/clmm-routes/executeSwap.ts @@ -187,6 +187,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..30472cb0e7 100644 --- a/src/connectors/uniswap/clmm-routes/openPosition.ts +++ b/src/connectors/uniswap/clmm-routes/openPosition.ts @@ -24,23 +24,9 @@ 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) => { + await fastify.register(require('@fastify/sensible')); + // Get first wallet address for example const ethereum = await Ethereum.getInstance('base'); let firstWalletAddress = ''; @@ -297,30 +283,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 +311,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 +340,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..c0a748b0d5 100644 --- a/src/connectors/uniswap/clmm-routes/positionInfo.ts +++ b/src/connectors/uniswap/clmm-routes/positionInfo.ts @@ -19,41 +19,11 @@ 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) => { + await fastify.register(require('@fastify/sensible')); // Get first wallet address for example const ethereum = await Ethereum.getInstance('base'); let firstWalletAddress = ''; diff --git a/src/connectors/uniswap/clmm-routes/positionsOwned.ts b/src/connectors/uniswap/clmm-routes/positionsOwned.ts index 5a44c33f96..d3ef46140a 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,37 +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) => { + await fastify.register(require('@fastify/sensible')); + // Get first wallet address for example const ethereum = await Ethereum.getInstance('base'); let firstWalletAddress = ''; @@ -130,10 +98,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 +113,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 +123,109 @@ 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..ee3bd93926 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,23 @@ 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')); + + // 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'); + } + fastify.post<{ Body: RemoveLiquidityRequestType; Reply: RemoveLiquidityResponseType; @@ -36,10 +45,11 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { properties: { ...RemoveLiquidityRequest.properties, network: { type: 'string', default: 'base' }, - walletAddress: { type: 'string', examples: ['0x...'] }, + walletAddress: { type: 'string', examples: [firstWalletAddress] }, positionAddress: { type: 'string', description: 'Position NFT token ID', + examples: ['1234'], }, percentageToRemove: { type: 'number', @@ -103,6 +113,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 +137,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 +157,84 @@ 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 +247,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/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..a66c009305 100644 --- a/src/connectors/uniswap/uniswap.ts +++ b/src/connectors/uniswap/uniswap.ts @@ -5,117 +5,11 @@ import { 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', - }, - ], -}; +import { + IUniswapV2PairABI, + IUniswapV2FactoryABI, + IUniswapV2Router02ABI +} from './uniswap.contracts'; // V3 (CLMM) imports import { Token, CurrencyAmount, Percent } from '@uniswap/sdk-core'; @@ -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,38 @@ 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, ); @@ -624,6 +514,102 @@ export class Uniswap { } } + /** + * 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/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 From 064a65d6b55b299b157253dcb63169a6aadb2d83 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Mon, 30 Jun 2025 16:38:34 -0700 Subject: [PATCH 05/31] fix: fix Uniswap router BUY orders and improve gas handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed BUY order calculations to properly handle EXACT_OUTPUT trades - Added balance checks before swap execution with clear error messages - Set fixed gas limit of 300,000 for all Uniswap V3 swaps - Removed minimum gas price constraint to use market rates - Allow all Ethereum networks configured in ethereum.yml - Dynamically fetch networks from configuration instead of hardcoded lists - Added comprehensive tests for BUY/SELL operations and gas parameters 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/connectors/connector.routes.ts | 19 +- src/connectors/uniswap/routes/execute-swap.ts | 170 ++++++++--- src/connectors/uniswap/routes/quote-swap.ts | 287 +++++++++++++++--- src/connectors/uniswap/uniswap.config.ts | 37 +-- test/connectors/uniswap/README.md | 42 +++ test/connectors/uniswap/mocks/quote-swap.json | 4 +- test/connectors/uniswap/swap.test.js | 224 +++++++++++++- 7 files changed, 653 insertions(+), 130 deletions(-) create mode 100644 test/connectors/uniswap/README.md 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/uniswap/routes/execute-swap.ts b/src/connectors/uniswap/routes/execute-swap.ts index f8f2851dd4..edf7ca02f6 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, @@ -40,6 +30,12 @@ export const executeSwapRoute: FastifyPluginAsync = async ( 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; Reply: ExecuteSwapResponseType; @@ -48,12 +44,16 @@ export const executeSwapRoute: FastifyPluginAsync = async ( { schema: { description: - 'Execute a swap using Uniswap V3 Smart Order Router (mainnet only)', + 'Execute a swap using Uniswap V3 Smart Order Router', tags: ['uniswap'], body: { type: 'object', properties: { - network: { type: 'string', default: 'mainnet', enum: ['mainnet'] }, + network: { + type: 'string', + default: 'mainnet', + enum: ethereumNetworks, + }, walletAddress: { type: 'string', examples: [firstWalletAddress] }, baseToken: { type: 'string', examples: ['WETH'] }, quoteToken: { type: 'string', examples: ['USDC'] }, @@ -92,13 +92,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'); @@ -147,15 +140,56 @@ 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') { @@ -174,7 +208,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 @@ -203,30 +239,86 @@ export const executeSwapRoute: FastifyPluginAsync = async ( logger.info('Generated method parameters successfully'); 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, - ); + + // Safety check: Decode and validate the swap parameters before executing + try { + const calldataHex = methodParameters.calldata; + const functionSelector = calldataHex.slice(0, 10); + + if (functionSelector === '0x5ae401dc') { // multicall + const innerFunctionSelector = '0x' + calldataHex.slice(266, 274); + + if (innerFunctionSelector === '0x5023b4df') { // exactInputSingle + try { + // The function takes a struct, so after the selector (10 chars), there's an offset (64 chars) + // Then the actual struct data starts + const structOffset = parseInt('0x' + calldataHex.slice(10 + 256, 10 + 320), 16) * 2 + 10; + + // Skip to amountIn field (5th field in the struct) + // tokenIn (64) + tokenOut (64) + fee (64) + recipient (64) + deadline (64) = 320 + const amountInOffset = structOffset + 320; + + if (calldataHex.length > amountInOffset + 64) { + const amountInHex = '0x' + calldataHex.slice(amountInOffset, amountInOffset + 64); + const amountInBN = BigNumber.from(amountInHex); + const formattedAmountIn = Number(formatTokenAmount(amountInBN.toString(), inputToken.decimals)); + + logger.info(`Safety check - Amount in transaction: ${formattedAmountIn} ${inputToken.symbol}`); + + // Critical safety check: prevent 0 amount transactions + if (amountInBN.isZero() || formattedAmountIn === 0) { + logger.error(`CRITICAL: Transaction would swap 0 ${inputToken.symbol}!`); + return reply.badRequest( + `Safety check failed: Transaction contains 0 ${inputToken.symbol} as input. This indicates a quote generation error.`, + ); + } + + // For SELL orders, verify the amount matches what was requested + if (exactIn && Math.abs(formattedAmountIn - amount) > 0.0001) { + logger.error(`CRITICAL: Amount mismatch! Requested ${amount}, but transaction contains ${formattedAmountIn}`); + return reply.badRequest( + `Safety check failed: Requested to sell ${amount} ${baseToken.symbol}, but transaction would sell ${formattedAmountIn} ${inputToken.symbol}`, + ); + } + } + } catch (e) { + logger.warn(`Could not extract amount for safety check: ${e.message}`); + } + } + } + } catch (e) { + logger.warn(`Could not decode transaction for safety check: ${e.message}`); + // Continue with execution if we can't decode - the transaction will fail on-chain if invalid + } + + // Safety check: validate the amounts before executing + if (!exactIn) { + // For BUY orders, check if the input amount is reasonable + const maxReasonableInput = amount * 10000; // 10,000x the output amount + if (quoteResult.estimatedAmountIn > maxReasonableInput) { + logger.error(`Safety check failed: estimated input ${quoteResult.estimatedAmountIn} is too high for output ${amount}`); + return reply.badRequest( + `Safety check failed: Quote requires ${quoteResult.estimatedAmountIn} ${inputToken.symbol} which seems unreasonable for ${amount} ${outputToken.symbol}`, + ); + } + } // 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 +332,7 @@ export const executeSwapRoute: FastifyPluginAsync = async ( if (exactIn) { totalInputSwapped = Number( formatTokenAmount( - inputAmount.quotient.toString(), + tradeAmount.quotient.toString(), inputToken.decimals, ), ); @@ -256,7 +348,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..463824ab4c 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'; @@ -16,6 +16,7 @@ import { } from '../../../schemas/swap-schema'; import { logger } from '../../../services/logger'; import { formatTokenAmount } from '../uniswap.utils'; +import { ConfigManagerV2 } from '../../../services/config-manager-v2'; // Removing the Protocol enum as it's causing type issues @@ -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,30 @@ 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}`, - ); - } - - // 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 - }; - + // 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 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 @@ -176,11 +209,129 @@ export async function getUniswapQuote( logger.info( `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; @@ -188,7 +339,7 @@ export async function getUniswapQuote( // 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( @@ -197,13 +348,52 @@ export async function getUniswapQuote( } // For BUY (exactOut), the output is exact, input is estimated else { - estimatedAmountOut = Number( - formatTokenAmount(inputAmount.quotient.toString(), outputToken.decimals), - ); + // For EXACT_OUTPUT, check if the AlphaRouter is returning the correct trade type + if (route.trade && route.trade.tradeType === TradeType.EXACT_INPUT) { + logger.warn(`AlphaRouter returned EXACT_INPUT trade for a BUY order - this is a known issue`); + // The AlphaRouter might be using EXACT_INPUT internally even for EXACT_OUTPUT requests + // In this case, we need to recalculate based on the trade details + + // For BUY orders with EXACT_INPUT trades from AlphaRouter: + // - route.trade.outputAmount should be close to our requested amount (0.001 WETH) + // - route.trade.inputAmount is what we need to pay + estimatedAmountOut = Number( + formatTokenAmount(route.trade.outputAmount.quotient.toString(), outputToken.decimals), + ); + estimatedAmountIn = Number( + formatTokenAmount(route.trade.inputAmount.quotient.toString(), inputToken.decimals), + ); + + // Verify the output amount is close to what we requested + const outputDiff = Math.abs(estimatedAmountOut - amount) / amount; + if (outputDiff > 0.1) { // More than 10% difference + logger.error(`Output amount mismatch: requested ${amount}, got ${estimatedAmountOut} (${outputDiff * 100}% difference)`); + throw new Error(`Invalid route: output amount differs significantly from requested amount`); + } + } else if (route.trade) { + // Normal EXACT_OUTPUT trade + estimatedAmountIn = Number( + formatTokenAmount(route.trade.inputAmount.quotient.toString(), inputToken.decimals), + ); + estimatedAmountOut = Number( + formatTokenAmount(route.trade.outputAmount.quotient.toString(), outputToken.decimals), + ); + } else { + // Fallback to original logic + estimatedAmountOut = Number( + formatTokenAmount(tradeAmount.quotient.toString(), outputToken.decimals), + ); - estimatedAmountIn = Number( - formatTokenAmount(route.quote.quotient.toString(), inputToken.decimals), - ); + estimatedAmountIn = Number( + formatTokenAmount(route.quote.quotient.toString(), inputToken.decimals), + ); + } + + // Sanity check for BUY orders + if (estimatedAmountIn > amount * 10000) { + logger.error(`BUY order sanity check failed: estimatedAmountIn (${estimatedAmountIn}) is unreasonably high for amount ${amount}`); + throw new Error(`Invalid quote: estimated input amount is unreasonably high`); + } } // Calculate min/max values with slippage @@ -228,9 +418,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 +433,7 @@ export async function getUniswapQuote( quoteToken, inputToken, outputToken, - inputAmount, + tradeAmount, exactIn, estimatedAmountIn, estimatedAmountOut, @@ -270,6 +464,11 @@ export const quoteSwapRoute: FastifyPluginAsync = async (fastify, _options) => { 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; Reply: GetSwapQuoteResponseType; @@ -278,12 +477,16 @@ export const quoteSwapRoute: FastifyPluginAsync = async (fastify, _options) => { { schema: { description: - 'Get a swap quote using Uniswap AlphaRouter (mainnet only)', + '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] }, diff --git a/src/connectors/uniswap/uniswap.config.ts b/src/connectors/uniswap/uniswap.config.ts index 95d11e4472..c80aee315a 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 { @@ -78,6 +50,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 diff --git a/test/connectors/uniswap/README.md b/test/connectors/uniswap/README.md new file mode 100644 index 0000000000..4da8ba0f15 --- /dev/null +++ b/test/connectors/uniswap/README.md @@ -0,0 +1,42 @@ +# Uniswap Router Test Coverage + +This directory contains comprehensive tests for the Uniswap V3 Smart Order Router integration. + +## Test Coverage + +### Quote Swap Tests (`swap.test.js`) +- **SELL Orders**: Tests selling base tokens for quote tokens +- **BUY Orders**: Tests buying base tokens with quote tokens +- **Gas Parameters**: Validates gas limit (300,000) and gas price calculations +- **Multi-Network Support**: Tests all supported Ethereum networks (mainnet, arbitrum, optimism, base, polygon, etc.) + +### Execute Swap Tests (`swap.test.js`) +- **SELL Execution**: Tests execution of sell orders +- **BUY Execution**: Tests execution of buy orders +- **Slippage Validation**: Ensures slippage parameters are handled correctly +- **Multi-Network Execution**: Tests swap execution across different networks + +### Key Validations +1. **Gas Limit**: Fixed at 300,000 for all swaps +2. **Gas Price**: Uses dynamic gas price from ethereum.estimateGasPrice() +3. **Gas Cost**: Calculated as gasPrice * gasLimit * 1e-9 +4. **Balance Changes**: Validates correct positive/negative balance changes for BUY/SELL +5. **Price Calculations**: Validates price calculations for both trade directions + +### Mock Data +- Mock responses are stored in `mocks/` directory +- Gas parameters reflect the implementation's fixed 300,000 gas limit +- Realistic gas prices (e.g., 0.8 GWEI for mainnet) + +## Running Tests + +```bash +# Run all Uniswap tests +npm test -- test/connectors/uniswap/ + +# Run specific test file +npm test -- test/connectors/uniswap/swap.test.js + +# Run with coverage +npm test -- --coverage test/connectors/uniswap/ +``` \ No newline at end of file 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..c769db1815 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,18 @@ 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 +90,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 +118,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 +167,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 +195,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 +359,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); + } + }); }); }); From 43ac63b414f0c9a0b4a3aafdd85e48863ff61e04 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Mon, 30 Jun 2025 16:42:09 -0700 Subject: [PATCH 06/31] cleanup: remove AlphaRouter workaround and test README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removed EXACT_INPUT workaround for BUY orders as AlphaRouter now works correctly - Removed BUY order sanity check as it's no longer needed - Removed test README.md file 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/connectors/uniswap/routes/quote-swap.ts | 53 +++------------------ test/connectors/uniswap/README.md | 42 ---------------- 2 files changed, 7 insertions(+), 88 deletions(-) delete mode 100644 test/connectors/uniswap/README.md diff --git a/src/connectors/uniswap/routes/quote-swap.ts b/src/connectors/uniswap/routes/quote-swap.ts index 463824ab4c..60bbc8e9f4 100644 --- a/src/connectors/uniswap/routes/quote-swap.ts +++ b/src/connectors/uniswap/routes/quote-swap.ts @@ -348,52 +348,13 @@ export async function getUniswapQuote( } // For BUY (exactOut), the output is exact, input is estimated else { - // For EXACT_OUTPUT, check if the AlphaRouter is returning the correct trade type - if (route.trade && route.trade.tradeType === TradeType.EXACT_INPUT) { - logger.warn(`AlphaRouter returned EXACT_INPUT trade for a BUY order - this is a known issue`); - // The AlphaRouter might be using EXACT_INPUT internally even for EXACT_OUTPUT requests - // In this case, we need to recalculate based on the trade details - - // For BUY orders with EXACT_INPUT trades from AlphaRouter: - // - route.trade.outputAmount should be close to our requested amount (0.001 WETH) - // - route.trade.inputAmount is what we need to pay - estimatedAmountOut = Number( - formatTokenAmount(route.trade.outputAmount.quotient.toString(), outputToken.decimals), - ); - estimatedAmountIn = Number( - formatTokenAmount(route.trade.inputAmount.quotient.toString(), inputToken.decimals), - ); - - // Verify the output amount is close to what we requested - const outputDiff = Math.abs(estimatedAmountOut - amount) / amount; - if (outputDiff > 0.1) { // More than 10% difference - logger.error(`Output amount mismatch: requested ${amount}, got ${estimatedAmountOut} (${outputDiff * 100}% difference)`); - throw new Error(`Invalid route: output amount differs significantly from requested amount`); - } - } else if (route.trade) { - // Normal EXACT_OUTPUT trade - estimatedAmountIn = Number( - formatTokenAmount(route.trade.inputAmount.quotient.toString(), inputToken.decimals), - ); - estimatedAmountOut = Number( - formatTokenAmount(route.trade.outputAmount.quotient.toString(), outputToken.decimals), - ); - } else { - // Fallback to original logic - estimatedAmountOut = Number( - formatTokenAmount(tradeAmount.quotient.toString(), outputToken.decimals), - ); - - estimatedAmountIn = Number( - formatTokenAmount(route.quote.quotient.toString(), inputToken.decimals), - ); - } - - // Sanity check for BUY orders - if (estimatedAmountIn > amount * 10000) { - logger.error(`BUY order sanity check failed: estimatedAmountIn (${estimatedAmountIn}) is unreasonably high for amount ${amount}`); - throw new Error(`Invalid quote: estimated input amount is unreasonably high`); - } + estimatedAmountOut = Number( + formatTokenAmount(tradeAmount.quotient.toString(), outputToken.decimals), + ); + + estimatedAmountIn = Number( + formatTokenAmount(route.quote.quotient.toString(), inputToken.decimals), + ); } // Calculate min/max values with slippage diff --git a/test/connectors/uniswap/README.md b/test/connectors/uniswap/README.md deleted file mode 100644 index 4da8ba0f15..0000000000 --- a/test/connectors/uniswap/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# Uniswap Router Test Coverage - -This directory contains comprehensive tests for the Uniswap V3 Smart Order Router integration. - -## Test Coverage - -### Quote Swap Tests (`swap.test.js`) -- **SELL Orders**: Tests selling base tokens for quote tokens -- **BUY Orders**: Tests buying base tokens with quote tokens -- **Gas Parameters**: Validates gas limit (300,000) and gas price calculations -- **Multi-Network Support**: Tests all supported Ethereum networks (mainnet, arbitrum, optimism, base, polygon, etc.) - -### Execute Swap Tests (`swap.test.js`) -- **SELL Execution**: Tests execution of sell orders -- **BUY Execution**: Tests execution of buy orders -- **Slippage Validation**: Ensures slippage parameters are handled correctly -- **Multi-Network Execution**: Tests swap execution across different networks - -### Key Validations -1. **Gas Limit**: Fixed at 300,000 for all swaps -2. **Gas Price**: Uses dynamic gas price from ethereum.estimateGasPrice() -3. **Gas Cost**: Calculated as gasPrice * gasLimit * 1e-9 -4. **Balance Changes**: Validates correct positive/negative balance changes for BUY/SELL -5. **Price Calculations**: Validates price calculations for both trade directions - -### Mock Data -- Mock responses are stored in `mocks/` directory -- Gas parameters reflect the implementation's fixed 300,000 gas limit -- Realistic gas prices (e.g., 0.8 GWEI for mainnet) - -## Running Tests - -```bash -# Run all Uniswap tests -npm test -- test/connectors/uniswap/ - -# Run specific test file -npm test -- test/connectors/uniswap/swap.test.js - -# Run with coverage -npm test -- --coverage test/connectors/uniswap/ -``` \ No newline at end of file From f6aef698dbbba8191142d1919a1e5c2d47ddba85 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Mon, 30 Jun 2025 16:44:03 -0700 Subject: [PATCH 07/31] cleanup: remove safety checks from execute-swap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removed calldata decoding and validation logic - Removed amount validation checks for BUY orders - Trust AlphaRouter's output without additional verification 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/connectors/uniswap/routes/execute-swap.ts | 63 ------------------- 1 file changed, 63 deletions(-) diff --git a/src/connectors/uniswap/routes/execute-swap.ts b/src/connectors/uniswap/routes/execute-swap.ts index edf7ca02f6..6bd31b716f 100644 --- a/src/connectors/uniswap/routes/execute-swap.ts +++ b/src/connectors/uniswap/routes/execute-swap.ts @@ -239,69 +239,6 @@ export const executeSwapRoute: FastifyPluginAsync = async ( logger.info('Generated method parameters successfully'); logger.info(`Calldata length: ${methodParameters.calldata.length}`); logger.info(`Value: ${methodParameters.value}`); - - // Safety check: Decode and validate the swap parameters before executing - try { - const calldataHex = methodParameters.calldata; - const functionSelector = calldataHex.slice(0, 10); - - if (functionSelector === '0x5ae401dc') { // multicall - const innerFunctionSelector = '0x' + calldataHex.slice(266, 274); - - if (innerFunctionSelector === '0x5023b4df') { // exactInputSingle - try { - // The function takes a struct, so after the selector (10 chars), there's an offset (64 chars) - // Then the actual struct data starts - const structOffset = parseInt('0x' + calldataHex.slice(10 + 256, 10 + 320), 16) * 2 + 10; - - // Skip to amountIn field (5th field in the struct) - // tokenIn (64) + tokenOut (64) + fee (64) + recipient (64) + deadline (64) = 320 - const amountInOffset = structOffset + 320; - - if (calldataHex.length > amountInOffset + 64) { - const amountInHex = '0x' + calldataHex.slice(amountInOffset, amountInOffset + 64); - const amountInBN = BigNumber.from(amountInHex); - const formattedAmountIn = Number(formatTokenAmount(amountInBN.toString(), inputToken.decimals)); - - logger.info(`Safety check - Amount in transaction: ${formattedAmountIn} ${inputToken.symbol}`); - - // Critical safety check: prevent 0 amount transactions - if (amountInBN.isZero() || formattedAmountIn === 0) { - logger.error(`CRITICAL: Transaction would swap 0 ${inputToken.symbol}!`); - return reply.badRequest( - `Safety check failed: Transaction contains 0 ${inputToken.symbol} as input. This indicates a quote generation error.`, - ); - } - - // For SELL orders, verify the amount matches what was requested - if (exactIn && Math.abs(formattedAmountIn - amount) > 0.0001) { - logger.error(`CRITICAL: Amount mismatch! Requested ${amount}, but transaction contains ${formattedAmountIn}`); - return reply.badRequest( - `Safety check failed: Requested to sell ${amount} ${baseToken.symbol}, but transaction would sell ${formattedAmountIn} ${inputToken.symbol}`, - ); - } - } - } catch (e) { - logger.warn(`Could not extract amount for safety check: ${e.message}`); - } - } - } - } catch (e) { - logger.warn(`Could not decode transaction for safety check: ${e.message}`); - // Continue with execution if we can't decode - the transaction will fail on-chain if invalid - } - - // Safety check: validate the amounts before executing - if (!exactIn) { - // For BUY orders, check if the input amount is reasonable - const maxReasonableInput = amount * 10000; // 10,000x the output amount - if (quoteResult.estimatedAmountIn > maxReasonableInput) { - logger.error(`Safety check failed: estimated input ${quoteResult.estimatedAmountIn} is too high for output ${amount}`); - return reply.badRequest( - `Safety check failed: Quote requires ${quoteResult.estimatedAmountIn} ${inputToken.symbol} which seems unreasonable for ${amount} ${outputToken.symbol}`, - ); - } - } // Prepare transaction with gas settings from quote const txRequest = { From 3879fb3061d24e6384de4716ab1e38e4047e2f71 Mon Sep 17 00:00:00 2001 From: WuonParticle Date: Tue, 24 Jun 2025 15:39:00 -0600 Subject: [PATCH 08/31] Stop the startup spam from loading a full Solana instance for an example wallet --- src/chains/solana/routes/balances.ts | 11 ++--------- src/chains/solana/solana.ts | 19 ++++++++++++++++++- src/commands/balance.ts | 2 +- src/connectors/jupiter/routes/executeSwap.ts | 10 +--------- .../meteora/clmm-routes/addLiquidity.ts | 10 +--------- .../meteora/clmm-routes/closePosition.ts | 10 +--------- .../meteora/clmm-routes/collectFees.ts | 10 +--------- .../meteora/clmm-routes/executeSwap.ts | 9 +-------- .../meteora/clmm-routes/openPosition.ts | 10 +--------- .../meteora/clmm-routes/positionInfo.ts | 11 +---------- .../meteora/clmm-routes/positionsOwned.ts | 11 +---------- .../meteora/clmm-routes/removeLiquidity.ts | 10 +--------- .../raydium/amm-routes/addLiquidity.ts | 11 +---------- .../raydium/amm-routes/executeSwap.ts | 10 +--------- .../raydium/amm-routes/positionInfo.ts | 13 ++----------- .../raydium/amm-routes/removeLiquidity.ts | 10 +--------- .../raydium/clmm-routes/closePosition.ts | 10 +--------- .../raydium/clmm-routes/collectFees.ts | 12 +++--------- .../raydium/clmm-routes/executeSwap.ts | 10 +--------- .../raydium/clmm-routes/openPosition.ts | 11 +---------- src/connectors/raydium/raydium.ts | 2 +- 21 files changed, 42 insertions(+), 170 deletions(-) diff --git a/src/chains/solana/routes/balances.ts b/src/chains/solana/routes/balances.ts index 67e1f0cf04..8c3bc00727 100644 --- a/src/chains/solana/routes/balances.ts +++ b/src/chains/solana/routes/balances.ts @@ -329,15 +329,8 @@ 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'); - } + // Get first wallet address for example + const firstWalletAddress = await Solana.getWalletAddressExample(); // Example address for Solana tokens const USDC_MINT_ADDRESS = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; // USDC on Solana diff --git a/src/chains/solana/solana.ts b/src/chains/solana/solana.ts index 967a3f883c..c71c5f71ad 100644 --- a/src/chains/solana/solana.ts +++ b/src/chains/solana/solana.ts @@ -1238,7 +1238,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 +1272,23 @@ export class Solana { } } + public static async getWalletAddressExample(): Promise { + const defaultAddress = ''; + try { + const foundWallet = await this.getFirstWalletAddress(); + if (foundWallet) { + return foundWallet; + } + logger.debug('No wallets found for examples in schema, using default.'); + 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/jupiter/routes/executeSwap.ts b/src/connectors/jupiter/routes/executeSwap.ts index e0cb0c314a..579a5d79be 100644 --- a/src/connectors/jupiter/routes/executeSwap.ts +++ b/src/connectors/jupiter/routes/executeSwap.ts @@ -96,15 +96,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'); - } + const firstWalletAddress = await Solana.getWalletAddressExample(); // Update schema example ExecuteSwapRequest.properties.walletAddress.examples = [firstWalletAddress]; diff --git a/src/connectors/meteora/clmm-routes/addLiquidity.ts b/src/connectors/meteora/clmm-routes/addLiquidity.ts index b22c619730..f91eb6f501 100644 --- a/src/connectors/meteora/clmm-routes/addLiquidity.ts +++ b/src/connectors/meteora/clmm-routes/addLiquidity.ts @@ -192,15 +192,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'); - } + const firstWalletAddress = await Solana.getWalletAddressExample(); // Update schema example AddLiquidityRequest.properties.walletAddress.examples = [firstWalletAddress]; diff --git a/src/connectors/meteora/clmm-routes/closePosition.ts b/src/connectors/meteora/clmm-routes/closePosition.ts index 4f491d3df2..36f7a5d6ad 100644 --- a/src/connectors/meteora/clmm-routes/closePosition.ts +++ b/src/connectors/meteora/clmm-routes/closePosition.ts @@ -119,15 +119,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'); - } + const firstWalletAddress = await Solana.getWalletAddressExample(); // Update schema example ClosePositionRequest.properties.walletAddress.examples = [firstWalletAddress]; diff --git a/src/connectors/meteora/clmm-routes/collectFees.ts b/src/connectors/meteora/clmm-routes/collectFees.ts index e577a39bdd..89a35beaa8 100644 --- a/src/connectors/meteora/clmm-routes/collectFees.ts +++ b/src/connectors/meteora/clmm-routes/collectFees.ts @@ -90,15 +90,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'); - } + const firstWalletAddress = await Solana.getWalletAddressExample(); // Update schema example CollectFeesRequest.properties.walletAddress.examples = [firstWalletAddress]; diff --git a/src/connectors/meteora/clmm-routes/executeSwap.ts b/src/connectors/meteora/clmm-routes/executeSwap.ts index 589e734eb2..a810696ec3 100644 --- a/src/connectors/meteora/clmm-routes/executeSwap.ts +++ b/src/connectors/meteora/clmm-routes/executeSwap.ts @@ -101,14 +101,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 firstWalletAddress = await Solana.getWalletAddressExample(); fastify.post<{ Body: ExecuteSwapRequestType; diff --git a/src/connectors/meteora/clmm-routes/openPosition.ts b/src/connectors/meteora/clmm-routes/openPosition.ts index 628f59ea3e..767f2c1e5e 100644 --- a/src/connectors/meteora/clmm-routes/openPosition.ts +++ b/src/connectors/meteora/clmm-routes/openPosition.ts @@ -257,15 +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'); - } + const firstWalletAddress = await Solana.getWalletAddressExample(); // Update schema example OpenPositionRequest.properties.walletAddress.examples = [firstWalletAddress]; diff --git a/src/connectors/meteora/clmm-routes/positionInfo.ts b/src/connectors/meteora/clmm-routes/positionInfo.ts index 7cbf02aa46..ea4678d781 100644 --- a/src/connectors/meteora/clmm-routes/positionInfo.ts +++ b/src/connectors/meteora/clmm-routes/positionInfo.ts @@ -12,16 +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'); - } + const firstWalletAddress = await Solana.getWalletAddressExample(); // Update schema example GetPositionInfoRequest.properties.walletAddress.examples = [ diff --git a/src/connectors/meteora/clmm-routes/positionsOwned.ts b/src/connectors/meteora/clmm-routes/positionsOwned.ts index 4615c0ca97..87f35b68d3 100644 --- a/src/connectors/meteora/clmm-routes/positionsOwned.ts +++ b/src/connectors/meteora/clmm-routes/positionsOwned.ts @@ -28,16 +28,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'); - } + const firstWalletAddress = await Solana.getWalletAddressExample(); // Update schema example GetPositionsOwnedRequest.properties.walletAddress.examples = [ diff --git a/src/connectors/meteora/clmm-routes/removeLiquidity.ts b/src/connectors/meteora/clmm-routes/removeLiquidity.ts index 852f731df5..b89fbd16a0 100644 --- a/src/connectors/meteora/clmm-routes/removeLiquidity.ts +++ b/src/connectors/meteora/clmm-routes/removeLiquidity.ts @@ -104,15 +104,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'); - } + const firstWalletAddress = await Solana.getWalletAddressExample(); // Update schema example RemoveLiquidityRequest.properties.walletAddress.examples = [ diff --git a/src/connectors/raydium/amm-routes/addLiquidity.ts b/src/connectors/raydium/amm-routes/addLiquidity.ts index 58850bd8ee..93b34e7844 100644 --- a/src/connectors/raydium/amm-routes/addLiquidity.ts +++ b/src/connectors/raydium/amm-routes/addLiquidity.ts @@ -216,16 +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'); - } + const firstWalletAddress = await Solana.getWalletAddressExample(); // Update schema example AddLiquidityRequest.properties.walletAddress.examples = [firstWalletAddress]; diff --git a/src/connectors/raydium/amm-routes/executeSwap.ts b/src/connectors/raydium/amm-routes/executeSwap.ts index f537969b92..b7647d6199 100644 --- a/src/connectors/raydium/amm-routes/executeSwap.ts +++ b/src/connectors/raydium/amm-routes/executeSwap.ts @@ -185,15 +185,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 firstWalletAddress = await Solana.getWalletAddressExample(); fastify.post<{ Body: ExecuteSwapRequestType; diff --git a/src/connectors/raydium/amm-routes/positionInfo.ts b/src/connectors/raydium/amm-routes/positionInfo.ts index d85ccc6ce0..89d1427be4 100644 --- a/src/connectors/raydium/amm-routes/positionInfo.ts +++ b/src/connectors/raydium/amm-routes/positionInfo.ts @@ -79,17 +79,8 @@ 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); - } + // Get first wallet address for example + const firstWalletAddress = await Solana.getWalletAddressExample(); fastify.get<{ Querystring: GetPositionInfoRequestType; diff --git a/src/connectors/raydium/amm-routes/removeLiquidity.ts b/src/connectors/raydium/amm-routes/removeLiquidity.ts index 50d4da4fa7..ac0276b1f9 100644 --- a/src/connectors/raydium/amm-routes/removeLiquidity.ts +++ b/src/connectors/raydium/amm-routes/removeLiquidity.ts @@ -258,15 +258,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'); - } + const firstWalletAddress = await Solana.getWalletAddressExample(); // Update schema example RemoveLiquidityRequest.properties.walletAddress.examples = [ diff --git a/src/connectors/raydium/clmm-routes/closePosition.ts b/src/connectors/raydium/clmm-routes/closePosition.ts index 48e2da278a..2794246de0 100644 --- a/src/connectors/raydium/clmm-routes/closePosition.ts +++ b/src/connectors/raydium/clmm-routes/closePosition.ts @@ -100,15 +100,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'); - } + const firstWalletAddress = await Solana.getWalletAddressExample(); // Update schema example ClosePositionRequest.properties.walletAddress.examples = [firstWalletAddress]; diff --git a/src/connectors/raydium/clmm-routes/collectFees.ts b/src/connectors/raydium/clmm-routes/collectFees.ts index 057ef80e67..c8cc51a12e 100644 --- a/src/connectors/raydium/clmm-routes/collectFees.ts +++ b/src/connectors/raydium/clmm-routes/collectFees.ts @@ -95,16 +95,10 @@ 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'); - } + // Get first wallet address for example + const firstWalletAddress = await Solana.getWalletAddressExample(); + // Update schema example CollectFeesRequest.properties.walletAddress.examples = [firstWalletAddress]; fastify.post<{ diff --git a/src/connectors/raydium/clmm-routes/executeSwap.ts b/src/connectors/raydium/clmm-routes/executeSwap.ts index 1d4a6c32e7..595c409eee 100644 --- a/src/connectors/raydium/clmm-routes/executeSwap.ts +++ b/src/connectors/raydium/clmm-routes/executeSwap.ts @@ -225,15 +225,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 firstWalletAddress = await Solana.getWalletAddressExample(); fastify.post<{ Body: ExecuteSwapRequestType; diff --git a/src/connectors/raydium/clmm-routes/openPosition.ts b/src/connectors/raydium/clmm-routes/openPosition.ts index c83b3175fc..db451acf55 100644 --- a/src/connectors/raydium/clmm-routes/openPosition.ts +++ b/src/connectors/raydium/clmm-routes/openPosition.ts @@ -165,16 +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'); - } + const firstWalletAddress = await Solana.getWalletAddressExample(); // Update schema example OpenPositionRequest.properties.walletAddress.examples = [firstWalletAddress]; 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); } From eef8c5a62a2b9d45a3ffcec92645619950f68a68 Mon Sep 17 00:00:00 2001 From: WuonParticle Date: Tue, 24 Jun 2025 15:49:15 -0600 Subject: [PATCH 09/31] Stop the startup spam from loading multiple Ethereum instances to get an example address --- src/chains/ethereum/ethereum.ts | 39 ++++++++++++++----- src/chains/ethereum/routes/allowances.ts | 10 +---- src/chains/ethereum/routes/approve.ts | 10 +---- src/chains/ethereum/routes/balances.ts | 25 +----------- src/chains/ethereum/routes/wrap.ts | 10 +---- .../uniswap/amm-routes/addLiquidity.ts | 15 ++----- .../uniswap/amm-routes/executeSwap.ts | 15 ++----- .../uniswap/amm-routes/positionInfo.ts | 10 +---- .../uniswap/amm-routes/removeLiquidity.ts | 10 +---- .../uniswap/clmm-routes/executeSwap.ts | 15 ++----- .../uniswap/clmm-routes/openPosition.ts | 10 +---- .../uniswap/clmm-routes/positionInfo.ts | 10 +---- .../uniswap/clmm-routes/positionsOwned.ts | 18 +++------ src/connectors/uniswap/routes/execute-swap.ts | 12 +----- src/connectors/uniswap/routes/quote-swap.ts | 10 +---- src/connectors/uniswap/uniswap.ts | 2 +- 16 files changed, 56 insertions(+), 165 deletions(-) diff --git a/src/chains/ethereum/ethereum.ts b/src/chains/ethereum/ethereum.ts index 2abdce6dcf..f677882873 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'; @@ -333,8 +334,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 +348,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 +635,26 @@ export class Ethereum { logger.info(`Wrapping ${utils.formatEther(amountInWei)} ETH to WETH`); return await wrappedContract.deposit(params); } + + public static async getWalletAddressExample(): Promise { + const defaultAddress = ''; + try { + const foundWallet = await this.getFirstWalletAddress(); + if (foundWallet) { + return foundWallet; + } + logger.debug('No wallets found for examples in schema, using default.'); + 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 ae930c3d7c..9aa8392e99 100644 --- a/src/chains/ethereum/routes/allowances.ts +++ b/src/chains/ethereum/routes/allowances.ts @@ -163,15 +163,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 firstWalletAddress = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: AllowancesRequestType; diff --git a/src/chains/ethereum/routes/approve.ts b/src/chains/ethereum/routes/approve.ts index 81281c8b5f..eff463a0a5 100644 --- a/src/chains/ethereum/routes/approve.ts +++ b/src/chains/ethereum/routes/approve.ts @@ -175,15 +175,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 firstWalletAddress = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: ApproveRequestType; diff --git a/src/chains/ethereum/routes/balances.ts b/src/chains/ethereum/routes/balances.ts index e26924c3df..66998b2d1c 100644 --- a/src/chains/ethereum/routes/balances.ts +++ b/src/chains/ethereum/routes/balances.ts @@ -163,30 +163,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 firstWalletAddress = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: BalanceRequestType; diff --git a/src/chains/ethereum/routes/wrap.ts b/src/chains/ethereum/routes/wrap.ts index b607d2b9fc..b5aa45efe6 100644 --- a/src/chains/ethereum/routes/wrap.ts +++ b/src/chains/ethereum/routes/wrap.ts @@ -194,15 +194,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 firstWalletAddress = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: WrapRequestType; diff --git a/src/connectors/uniswap/amm-routes/addLiquidity.ts b/src/connectors/uniswap/amm-routes/addLiquidity.ts index c718e49414..321ff22e5e 100644 --- a/src/connectors/uniswap/amm-routes/addLiquidity.ts +++ b/src/connectors/uniswap/amm-routes/addLiquidity.ts @@ -311,15 +311,7 @@ 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 firstWalletAddress = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: AddLiquidityRequestType; @@ -380,11 +372,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..dc7f602545 100644 --- a/src/connectors/uniswap/amm-routes/executeSwap.ts +++ b/src/connectors/uniswap/amm-routes/executeSwap.ts @@ -24,15 +24,7 @@ export const executeSwapRoute: 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 firstWalletAddress = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: ExecuteSwapRequestType; @@ -85,11 +77,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 ffc406dd83..6fcd1a499e 100644 --- a/src/connectors/uniswap/amm-routes/positionInfo.ts +++ b/src/connectors/uniswap/amm-routes/positionInfo.ts @@ -38,15 +38,7 @@ export async function checkLPAllowance( 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 firstWalletAddress = await Ethereum.getWalletAddressExample(); fastify.get<{ Querystring: GetPositionInfoRequestType; diff --git a/src/connectors/uniswap/amm-routes/removeLiquidity.ts b/src/connectors/uniswap/amm-routes/removeLiquidity.ts index 0237fa6511..2f10b89590 100644 --- a/src/connectors/uniswap/amm-routes/removeLiquidity.ts +++ b/src/connectors/uniswap/amm-routes/removeLiquidity.ts @@ -25,15 +25,7 @@ export const removeLiquidityRoute: 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 firstWalletAddress = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: RemoveLiquidityRequestType; diff --git a/src/connectors/uniswap/clmm-routes/executeSwap.ts b/src/connectors/uniswap/clmm-routes/executeSwap.ts index 32357c3851..2ec06d8524 100644 --- a/src/connectors/uniswap/clmm-routes/executeSwap.ts +++ b/src/connectors/uniswap/clmm-routes/executeSwap.ts @@ -24,15 +24,7 @@ export const executeSwapRoute: 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 firstWalletAddress = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: ExecuteSwapRequestType; @@ -85,11 +77,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/clmm-routes/openPosition.ts b/src/connectors/uniswap/clmm-routes/openPosition.ts index 30472cb0e7..cf3b1fa654 100644 --- a/src/connectors/uniswap/clmm-routes/openPosition.ts +++ b/src/connectors/uniswap/clmm-routes/openPosition.ts @@ -28,15 +28,7 @@ export const openPositionRoute: 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 firstWalletAddress = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: OpenPositionRequestType; diff --git a/src/connectors/uniswap/clmm-routes/positionInfo.ts b/src/connectors/uniswap/clmm-routes/positionInfo.ts index c0a748b0d5..54963b3ff9 100644 --- a/src/connectors/uniswap/clmm-routes/positionInfo.ts +++ b/src/connectors/uniswap/clmm-routes/positionInfo.ts @@ -25,15 +25,7 @@ import { formatTokenAmount } from '../uniswap.utils'; export const positionInfoRoute: 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 firstWalletAddress = await Ethereum.getWalletAddressExample(); fastify.get<{ Querystring: GetPositionInfoRequestType; diff --git a/src/connectors/uniswap/clmm-routes/positionsOwned.ts b/src/connectors/uniswap/clmm-routes/positionsOwned.ts index d3ef46140a..f6deb7cc2b 100644 --- a/src/connectors/uniswap/clmm-routes/positionsOwned.ts +++ b/src/connectors/uniswap/clmm-routes/positionsOwned.ts @@ -43,19 +43,11 @@ export const positionsOwnedRoute: 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; - // Update the example in the schema - PositionsOwnedRequest.properties.walletAddress.examples = [ - firstWalletAddress, - ]; - } catch (error) { - logger.warn('No wallets found for examples in schema'); - } + const firstWalletAddress = await Ethereum.getWalletAddressExample(); + + PositionsOwnedRequest.properties.walletAddress.examples = [ + firstWalletAddress, + ]; fastify.get<{ Querystring: typeof PositionsOwnedRequest.static; diff --git a/src/connectors/uniswap/routes/execute-swap.ts b/src/connectors/uniswap/routes/execute-swap.ts index 6bd31b716f..470247bf42 100644 --- a/src/connectors/uniswap/routes/execute-swap.ts +++ b/src/connectors/uniswap/routes/execute-swap.ts @@ -20,15 +20,7 @@ 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 = ''; - - try { - firstWalletAddress = - (await ethereum.getFirstWalletAddress()) || firstWalletAddress; - } catch (error) { - logger.warn('No wallets found for examples in schema'); - } + const firstWalletAddress = await Ethereum.getWalletAddressExample(); // Get available networks from Ethereum configuration (same method as chain.routes.ts) const { ConfigManagerV2 } = require('../../../services/config-manager-v2'); @@ -104,7 +96,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', diff --git a/src/connectors/uniswap/routes/quote-swap.ts b/src/connectors/uniswap/routes/quote-swap.ts index 60bbc8e9f4..d87633498f 100644 --- a/src/connectors/uniswap/routes/quote-swap.ts +++ b/src/connectors/uniswap/routes/quote-swap.ts @@ -415,15 +415,7 @@ 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 = ''; - - try { - firstWalletAddress = - (await ethereum.getFirstWalletAddress()) || firstWalletAddress; - } catch (error) { - logger.warn('No wallets found for examples in schema'); - } + const firstWalletAddress = await Ethereum.getWalletAddressExample(); // Get available networks from Ethereum configuration (same method as chain.routes.ts) const ethereumNetworks = Object.keys( diff --git a/src/connectors/uniswap/uniswap.ts b/src/connectors/uniswap/uniswap.ts index a66c009305..84136cfccf 100644 --- a/src/connectors/uniswap/uniswap.ts +++ b/src/connectors/uniswap/uniswap.ts @@ -507,7 +507,7 @@ 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; From f3c58aaa5d57bd7d94c1f9a4754b3c19f4a3ea20 Mon Sep 17 00:00:00 2001 From: WuonParticle Date: Mon, 16 Jun 2025 13:10:20 -0600 Subject: [PATCH 10/31] refactor: run linter and apply fixes Fixes #44 --- test/connectors/uniswap/amm.test.js | 4 +++- test/connectors/uniswap/clmm.test.js | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/test/connectors/uniswap/amm.test.js b/test/connectors/uniswap/amm.test.js index 248d6c3cd0..71437b2e53 100644 --- a/test/connectors/uniswap/amm.test.js +++ b/test/connectors/uniswap/amm.test.js @@ -212,7 +212,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..41c8a2014c 100644 --- a/test/connectors/uniswap/clmm.test.js +++ b/test/connectors/uniswap/clmm.test.js @@ -208,7 +208,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 From 5db820063c432c226589d925dbe6c5edf04459fb Mon Sep 17 00:00:00 2001 From: WuonParticle Date: Wed, 11 Jun 2025 13:02:41 -0600 Subject: [PATCH 11/31] fix: reference bigint-buffer to avoid warning Reference bigint-buffer to avoid warning about falling back to js int. Fixes #44 --- package.json | 1 + pnpm-lock.yaml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/package.json b/package.json index 000c30f6f9..1c8d830093 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ecebb7cf2..43fc7a0a91 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 From e3e182e92fdc43ce6af87db17b417fc97b8a2fa8 Mon Sep 17 00:00:00 2001 From: WuonParticle Date: Tue, 17 Jun 2025 12:13:00 -0600 Subject: [PATCH 12/31] add auto linting --- .husky/pre-commit | 4 + package.json | 7 +- pnpm-lock.yaml | 210 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 .husky/pre-commit diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000000..c4a570bcc8 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +pnpm lint-staged diff --git a/package.json b/package.json index 1c8d830093..b901386a45 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "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", @@ -128,6 +129,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", @@ -154,6 +156,9 @@ "/dist", "/src/commands" ], + "lint-staged": { + "{src,test}/**/*.{ts,js}": "eslint --fix" + }, "oclif": { "bin": "gateway", "dirname": "gateway", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43fc7a0a91..d1cb166aa1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -318,6 +318,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) @@ -2250,6 +2253,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'} @@ -2781,6 +2788,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'} @@ -2789,6 +2800,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'} @@ -2869,6 +2884,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==} @@ -3039,6 +3058,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'} @@ -3208,6 +3236,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==} @@ -3239,6 +3270,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==} @@ -3794,6 +3829,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==} @@ -4228,6 +4267,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'} @@ -4731,6 +4778,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'} @@ -4762,6 +4818,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'} @@ -4897,6 +4957,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==} @@ -5015,6 +5079,10 @@ packages: 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'} @@ -5288,6 +5356,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==} @@ -5442,6 +5514,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==} @@ -5690,6 +5767,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'} @@ -5923,6 +6004,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'} @@ -6056,6 +6145,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'} @@ -6076,6 +6169,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'} @@ -6624,6 +6721,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==} @@ -6732,6 +6833,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==} @@ -10177,6 +10283,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: {} @@ -10847,12 +10957,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: @@ -10932,6 +11051,8 @@ snapshots: commander@13.1.0: {} + commander@14.0.0: {} + commander@2.11.0: {} commander@2.15.1: {} @@ -11107,6 +11228,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: {} @@ -11243,6 +11368,8 @@ snapshots: emittery@0.13.1: {} + emoji-regex@10.4.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -11269,6 +11396,8 @@ snapshots: env-paths@2.2.1: {} + environment@1.1.0: {} + err-code@2.0.3: {} error-ex@1.3.2: @@ -12074,6 +12203,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: @@ -12579,6 +12710,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: @@ -13279,6 +13416,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 @@ -13307,6 +13468,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 @@ -13467,6 +13636,8 @@ snapshots: mimic-fn@2.1.0: {} + mimic-function@5.0.1: {} + minimalistic-assert@1.0.1: {} minimalistic-crypto-utils@1.0.1: {} @@ -13620,6 +13791,8 @@ snapshots: nan@2.22.2: optional: true + nano-spawn@1.0.2: {} + nanomatch@1.2.13: dependencies: arr-diff: 4.0.0 @@ -13858,6 +14031,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: @@ -13998,6 +14175,8 @@ snapshots: picomatch@4.0.2: {} + pidtree@0.6.0: {} + pino-abstract-transport@2.0.0: dependencies: split2: 4.2.0 @@ -14261,6 +14440,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: {} @@ -14523,6 +14707,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: @@ -14682,6 +14876,8 @@ snapshots: stream-transform@3.3.3: {} + string-argv@0.3.2: {} + string-length@4.0.2: dependencies: char-regex: 1.0.2 @@ -14710,6 +14906,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 @@ -15318,6 +15520,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: @@ -15378,6 +15586,8 @@ snapshots: yaml@2.7.1: {} + yaml@2.8.0: {} + yargs-parser@11.1.1: dependencies: camelcase: 5.3.1 From 1e1d4e59372fcc8ff9bfd8ef447fc1b1d2db59fc Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 1 Jul 2025 16:21:33 -0700 Subject: [PATCH 13/31] fix: correct Jupiter quote-swap estimateAmountIn for BUY orders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, estimateAmountIn was incorrectly returning the base token amount for BUY orders instead of the quote token amount needed. This fix ensures: - BUY orders: estimateAmountIn = quote token amount needed - SELL orders: estimateAmountIn = base token amount to sell - Balance changes correctly reflect token movements 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/connectors/jupiter/routes/quoteSwap.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/connectors/jupiter/routes/quoteSwap.ts b/src/connectors/jupiter/routes/quoteSwap.ts index 393eddd46a..8cabcfd34e 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,11 @@ 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, + : -quote.estimatedAmountIn, price: quote.expectedPrice, gasPrice: gasEstimation?.gasPrice, gasLimit: gasEstimation?.gasLimit, From 0bf055715dee74cdc9b6f7835b130a24b8263089 Mon Sep 17 00:00:00 2001 From: WuonParticle Date: Wed, 18 Jun 2025 11:23:27 -0600 Subject: [PATCH 14/31] Add support for #src paths in src, test, and test-scripts .ts files. --- .eslintrc.js | 12 ++++++++---- jest.config.js | 17 +++++++++++++++-- package.json | 8 +++++++- tsconfig.build.json | 9 +++++++++ tsconfig.json | 22 ++++++++++++++-------- 5 files changed, 53 insertions(+), 15 deletions(-) create mode 100644 tsconfig.build.json diff --git a/.eslintrc.js b/.eslintrc.js index 3398443860..d29c5deb28 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -42,10 +42,14 @@ module.exports = { }, settings: { 'import/resolver': { - 'node': { - 'extensions': ['.js', '.jsx', '.ts', '.tsx'] - } - } + typescript: { + alwaysTryTypes: true, + project: './tsconfig.json', + }, + node: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + }, }, overrides: [ { diff --git a/jest.config.js b/jest.config.js index 18d914f8dc..2c5b0b3b36 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', @@ -18,7 +21,17 @@ module.exports = { ], modulePathIgnorePatterns: ['/dist/'], setupFilesAfterEnv: ['/test/jest-setup.js'], - moduleNameMapper: { + testPathIgnorePatterns: [ + '/node_modules/', + 'test-helpers', + '/test-scripts/', + ], + testMatch: ['/test/**/*.test.ts', '/test/**/*.test.js'], + transform: { + '^.+\\.(t|j)s$': '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..ff3602c6e1 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,13 @@ "main": "index.js", "license": "Apache-2.0", "repository": "https://github.com/hummingbot/gateway", + "imports": { + "#src/*": "./src/*", + "#test/*": "./test/*" + }, "scripts": { "prebuild": "rimraf dist && mkdir dist", - "build": "tsc --skipLibCheck --sourceMap --project ./ && pnpm run copy-files", + "build": "tsc --project tsconfig.build.json && pnpm run copy-files", "clean": "rm -rf ./node_modules && rm -rf ./coverage && rm -rf ./logs", "clean:config": "find ./conf -maxdepth 1 -type f -delete", "format": "prettier . --write", @@ -16,6 +20,7 @@ "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/", @@ -116,6 +121,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", diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000000..db5d0b562b --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "skipLibCheck": true, + "sourceMap": true, + "inlineSourceMap": false + }, + "exclude": ["node_modules", "test", "test-scripts"] +} diff --git a/tsconfig.json b/tsconfig.json index 7785ece1af..834e552c59 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,13 @@ { "compilerOptions": { "strict": false, - // "types": ["jest", "node"], "target": "ESNext", "module": "CommonJS", "allowJs": true, - // "lib": ["es6", "es2020", "esnext.asynciterable", "dom"], - // "sourceMap": true, + "inlineSourceMap": true, "outDir": "./dist", - // "moduleResolution": "node", "removeComments": true, "noImplicitAny": false, - // "strictNullChecks": false, "strictFunctionTypes": true, "noImplicitThis": true, "noUnusedLocals": false, @@ -23,16 +19,26 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "resolveJsonModule": true, + "baseUrl": ".", + "paths": { + "#src/*": [ + "src/*" + ], + "#test/*": [ + "test/*" + ] + }, "typeRoots": ["node_modules/@types", "src/@types"], "downlevelIteration": true, "skipLibCheck": true }, "exclude": [ - "node_modules", - "test" + "node_modules" ], "include": [ "src/**/*.ts", - "src/**/*.js" + "src/**/*.js", + "test/**/*.ts", + "test-scripts/**/*.ts" ] } From c504b65db50fed1e1ade25d50043f760fcfab94a Mon Sep 17 00:00:00 2001 From: WuonParticle Date: Tue, 1 Jul 2025 11:39:45 -0600 Subject: [PATCH 15/31] lock file changes for import paths --- pnpm-lock.yaml | 346 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 327 insertions(+), 19 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ecebb7cf2..84678cc3e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -278,13 +278,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.4(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.4)(eslint@8.57.1) eslint-plugin-n: specifier: ^16.6.2 version: 16.6.2(eslint@8.57.1) @@ -726,6 +729,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 +1132,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 +1820,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 +2143,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: @@ -3326,9 +3439,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.4: + resolution: {integrity: sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==} + 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'} @@ -3825,6 +3960,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 +4302,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'} @@ -5019,6 +5160,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==} @@ -6002,6 +6148,10 @@ packages: resolution: {integrity: sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==} engines: {node: ^18.17.0 || >=20.5.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==} @@ -6231,6 +6381,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==} @@ -6447,6 +6601,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'} @@ -7565,6 +7722,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 @@ -8339,7 +8512,7 @@ snapshots: '@metaplex-foundation/beet': 0.6.1 '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.8.3)(utf-8-validate@5.0.10) bs58: 5.0.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - bufferutil - encoding @@ -8352,7 +8525,7 @@ snapshots: '@metaplex-foundation/beet': 0.7.2 '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.8.3)(utf-8-validate@5.0.10) bs58: 5.0.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - bufferutil - encoding @@ -8364,7 +8537,7 @@ snapshots: dependencies: ansicolors: 0.3.2 bn.js: 5.2.1 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -8372,7 +8545,7 @@ snapshots: dependencies: ansicolors: 0.3.2 bn.js: 5.2.1 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -8380,7 +8553,7 @@ snapshots: dependencies: ansicolors: 0.3.2 bn.js: 5.2.1 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -8389,7 +8562,7 @@ snapshots: ansicolors: 0.3.2 assert: 2.1.0 bn.js: 5.2.1 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -8474,7 +8647,7 @@ snapshots: '@solana/spl-token': 0.3.11(@solana/web3.js@1.98.2(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.8.3)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.8.3)(utf-8-validate@5.0.10) bn.js: 5.2.1 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - bufferutil - encoding @@ -8507,6 +8680,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 +9754,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 +10240,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 @@ -10110,7 +10354,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -11384,10 +11628,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.4)(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 +11640,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 +11655,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.4(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.2.0 + 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.4)(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.4)(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.4(eslint-plugin-import@2.31.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -11421,7 +11688,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.4)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -11432,7 +11699,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.4)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -12111,6 +12378,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: @@ -12372,7 +12643,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.3 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -12386,7 +12657,7 @@ snapshots: https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.3 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -12523,6 +12794,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: @@ -12730,7 +13005,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -13635,6 +13910,8 @@ snapshots: napi-macros@2.2.2: {} + napi-postinstall@0.2.5: {} + natural-compare@1.4.0: {} natural-orderby@2.0.3: {} @@ -14557,7 +14834,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.3 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1 socks: 2.8.4 transitivePeerDependencies: - supports-color @@ -14625,6 +14902,8 @@ snapshots: dependencies: minipass: 7.1.2 + stable-hash-x@0.2.0: {} + stack-trace@0.0.10: {} stack-utils@1.0.5: @@ -14884,6 +15163,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 @@ -15085,6 +15369,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 From 052ac11f45e6d616336c25071d0b2f176bbed1b9 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 1 Jul 2025 16:38:25 -0700 Subject: [PATCH 16/31] Save current changes before updating PR --- src/chains/ethereum/routes/allowances.ts | 43 +++++--- src/connectors/jupiter/routes/quoteSwap.ts | 4 +- .../uniswap/clmm-routes/collectFees.ts | 3 +- .../uniswap/clmm-routes/positionsOwned.ts | 12 ++- .../uniswap/clmm-routes/removeLiquidity.ts | 7 +- src/connectors/uniswap/routes/execute-swap.ts | 70 +++++++++---- src/connectors/uniswap/routes/quote-swap.ts | 99 +++++++++++-------- src/connectors/uniswap/uniswap.config.ts | 2 +- src/connectors/uniswap/uniswap.ts | 30 +++--- test/connectors/uniswap/swap.test.js | 7 +- 10 files changed, 172 insertions(+), 105 deletions(-) diff --git a/src/chains/ethereum/routes/allowances.ts b/src/chains/ethereum/routes/allowances.ts index 9aa8392e99..17128e04f4 100644 --- a/src/chains/ethereum/routes/allowances.ts +++ b/src/chains/ethereum/routes/allowances.ts @@ -19,7 +19,7 @@ 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) { @@ -32,7 +32,10 @@ export async function getTokensToTokenInfo( // 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); + 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...`, ); @@ -56,7 +59,7 @@ export async function getTokensToTokenInfo( // 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})`, ); @@ -97,16 +100,25 @@ export async function getEthereumAllowances( // 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()); - - const missingTokens = tokens.filter(t => { + const resolvedAddresses = Object.values(tokenInfoMap).map((t) => + t.address.toLowerCase(), + ); + const resolvedSymbols = Object.values(tokenInfoMap).map((t) => + t.symbol.toUpperCase(), + ); + + const missingTokens = tokens.filter((t) => { const tLower = t.toLowerCase(); const tUpper = t.toUpperCase(); - return !resolvedAddresses.includes(tLower) && !resolvedSymbols.includes(tUpper); + return ( + !resolvedAddresses.includes(tLower) && + !resolvedSymbols.includes(tUpper) + ); }); - - logger.warn(`Some tokens could not be resolved: ${missingTokens.join(', ')}`); + + logger.warn( + `Some tokens could not be resolved: ${missingTokens.join(', ')}`, + ); } // Determine the spender address based on the input @@ -201,13 +213,16 @@ 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(), { + tokens: Type.Array(Type.String(), { examples: [ ['USDC', 'DAI'], - ['0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', '0x6B175474E89094C44Da98b954EedeAC495271d0F'], - ['USDC', '0xd0b53D9277642d899DF5C87A3966A349A798F224'] + [ + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + '0x6B175474E89094C44Da98b954EedeAC495271d0F', + ], + ['USDC', '0xd0b53D9277642d899DF5C87A3966A349A798F224'], ], - description: 'Array of token symbols or addresses' + description: 'Array of token symbols or addresses', }), }), response: { diff --git a/src/connectors/jupiter/routes/quoteSwap.ts b/src/connectors/jupiter/routes/quoteSwap.ts index 8cabcfd34e..6de4e057a5 100644 --- a/src/connectors/jupiter/routes/quoteSwap.ts +++ b/src/connectors/jupiter/routes/quoteSwap.ts @@ -240,9 +240,7 @@ export const quoteSwapRoute: FastifyPluginAsync = async (fastify) => { baseTokenBalanceChange: side === 'SELL' ? -quote.estimatedAmountIn : quote.estimatedAmountOut, quoteTokenBalanceChange: - side === 'SELL' - ? quote.estimatedAmountOut - : -quote.estimatedAmountIn, + side === 'SELL' ? quote.estimatedAmountOut : -quote.estimatedAmountIn, price: quote.expectedPrice, gasPrice: gasEstimation?.gasPrice, gasLimit: gasEstimation?.gasLimit, diff --git a/src/connectors/uniswap/clmm-routes/collectFees.ts b/src/connectors/uniswap/clmm-routes/collectFees.ts index 0c18993a7b..3ef342f02d 100644 --- a/src/connectors/uniswap/clmm-routes/collectFees.ts +++ b/src/connectors/uniswap/clmm-routes/collectFees.ts @@ -25,7 +25,7 @@ export const collectFeesRoute: FastifyPluginAsync = async (fastify) => { try { firstWalletAddress = - (await ethereum.getFirstWalletAddress()) || firstWalletAddress; + (await Ethereum.getFirstWalletAddress()) || firstWalletAddress; } catch (error) { logger.warn('No wallets found for examples in schema'); } @@ -109,7 +109,6 @@ export const collectFeesRoute: FastifyPluginAsync = async (fastify) => { throw fastify.httpErrors.badRequest(error.message); } - // Create position manager contract const positionManager = new Contract( positionManagerAddress, diff --git a/src/connectors/uniswap/clmm-routes/positionsOwned.ts b/src/connectors/uniswap/clmm-routes/positionsOwned.ts index f6deb7cc2b..35e10f2d59 100644 --- a/src/connectors/uniswap/clmm-routes/positionsOwned.ts +++ b/src/connectors/uniswap/clmm-routes/positionsOwned.ts @@ -154,8 +154,16 @@ export const positionsOwnedRoute: FastifyPluginAsync = async (fastify) => { } // Calculate price range - const lowerPrice = tickToPrice(token0, token1, tickLower).toSignificant(6); - const upperPrice = tickToPrice(token0, token1, tickUpper).toSignificant(6); + 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); diff --git a/src/connectors/uniswap/clmm-routes/removeLiquidity.ts b/src/connectors/uniswap/clmm-routes/removeLiquidity.ts index ee3bd93926..88e213a49d 100644 --- a/src/connectors/uniswap/clmm-routes/removeLiquidity.ts +++ b/src/connectors/uniswap/clmm-routes/removeLiquidity.ts @@ -26,7 +26,7 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { try { firstWalletAddress = - (await ethereum.getFirstWalletAddress()) || firstWalletAddress; + (await Ethereum.getFirstWalletAddress()) || firstWalletAddress; } catch (error) { logger.warn('No wallets found for examples in schema'); } @@ -228,7 +228,10 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { // Get the calldata using the SDK const { calldata, value } = - NonfungiblePositionManager.removeCallParameters(positionSDK, removeParams); + NonfungiblePositionManager.removeCallParameters( + positionSDK, + removeParams, + ); // Execute the transaction to remove liquidity const tx = await wallet.sendTransaction({ diff --git a/src/connectors/uniswap/routes/execute-swap.ts b/src/connectors/uniswap/routes/execute-swap.ts index 470247bf42..4767558e8e 100644 --- a/src/connectors/uniswap/routes/execute-swap.ts +++ b/src/connectors/uniswap/routes/execute-swap.ts @@ -35,15 +35,14 @@ export const executeSwapRoute: FastifyPluginAsync = async ( '/execute-swap', { schema: { - description: - 'Execute a swap using Uniswap V3 Smart Order Router', + description: 'Execute a swap using Uniswap V3 Smart Order Router', tags: ['uniswap'], body: { type: 'object', properties: { - network: { - type: 'string', - default: 'mainnet', + network: { + type: 'string', + default: 'mainnet', enum: ethereumNetworks, }, walletAddress: { type: 'string', examples: [firstWalletAddress] }, @@ -136,20 +135,30 @@ export const executeSwapRoute: FastifyPluginAsync = async ( 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}`); + 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}`); + logger.info( + `Checking balance of ${inputToken.symbol} for wallet ${walletAddress}`, + ); let inputTokenBalance; if (inputToken.symbol === 'ETH') { // For native ETH, use getNativeBalance @@ -167,19 +176,36 @@ export const executeSwapRoute: FastifyPluginAsync = async ( 5000, // 5 second timeout ); } - const inputBalanceFormatted = Number(formatTokenAmount(inputTokenBalance.value.toString(), inputToken.decimals)); + 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)); - + 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}`); + 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.` + `Insufficient ${inputToken.symbol} balance. You have ${inputBalanceFormatted} ${inputToken.symbol} but need ${requiredInputAmount} ${inputToken.symbol} to complete this swap.`, ); } @@ -246,7 +272,9 @@ export const executeSwapRoute: FastifyPluginAsync = async ( // Execute the swap by sending the transaction directly logger.info(`Executing swap to router: ${routerAddress}`); - logger.info(`Transaction data length: ${methodParameters.calldata.length}`); + logger.info( + `Transaction data length: ${methodParameters.calldata.length}`, + ); const tx = await wallet.sendTransaction(txRequest); // Wait for transaction confirmation diff --git a/src/connectors/uniswap/routes/quote-swap.ts b/src/connectors/uniswap/routes/quote-swap.ts index d87633498f..b37a1c44a6 100644 --- a/src/connectors/uniswap/routes/quote-swap.ts +++ b/src/connectors/uniswap/routes/quote-swap.ts @@ -14,9 +14,9 @@ import { GetSwapQuoteResponse, GetSwapQuoteRequestType, } from '../../../schemas/swap-schema'; +import { ConfigManagerV2 } from '../../../services/config-manager-v2'; import { logger } from '../../../services/logger'; import { formatTokenAmount } from '../uniswap.utils'; -import { ConfigManagerV2 } from '../../../services/config-manager-v2'; // Removing the Protocol enum as it's causing type issues @@ -94,7 +94,7 @@ export async function getUniswapQuote( `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()} @@ -110,7 +110,7 @@ export async function getUniswapQuote( `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()} @@ -171,15 +171,17 @@ export async function getUniswapQuote( // 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()}`); - + logger.info( + `Debug currencyAmount raw: ${currencyAmount.quotient.toString()}`, + ); + route = await alphaRouter.route( currencyAmount, otherCurrency, @@ -209,111 +211,121 @@ export async function getUniswapQuote( logger.info( `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}%)`); + 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); + const innerFunctionSelector = + '0x' + calldataHex.slice(exactInputPos, exactInputPos + 8); logger.info(`Inner function selector: ${innerFunctionSelector}`); - + if (innerFunctionSelector === '0x04e45aaf') { - // exactInputSingle structure: + // 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); + 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}%) + - 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)); + 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!`); + 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); + 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: @@ -327,10 +339,12 @@ export async function getUniswapQuote( logger.info( `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}`); + logger.info( + `BUY order debug - tradeAmount: ${tradeAmount.toExact()} ${outputToken.symbol}, route.quote: ${route.quote.toExact()} ${inputToken.symbol}`, + ); } // Calculate amounts @@ -382,10 +396,10 @@ export async function getUniswapQuote( // 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 { @@ -429,15 +443,14 @@ export const quoteSwapRoute: FastifyPluginAsync = async (fastify, _options) => { '/quote-swap', { schema: { - description: - 'Get a swap quote using Uniswap AlphaRouter', + description: 'Get a swap quote using Uniswap AlphaRouter', tags: ['uniswap'], querystring: { type: 'object', properties: { - network: { - type: 'string', - default: 'mainnet', + network: { + type: 'string', + default: 'mainnet', enum: ethereumNetworks, }, baseToken: { type: 'string', examples: ['WETH'] }, diff --git a/src/connectors/uniswap/uniswap.config.ts b/src/connectors/uniswap/uniswap.config.ts index c80aee315a..93706710f1 100644 --- a/src/connectors/uniswap/uniswap.config.ts +++ b/src/connectors/uniswap/uniswap.config.ts @@ -50,7 +50,7 @@ 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') || {}, diff --git a/src/connectors/uniswap/uniswap.ts b/src/connectors/uniswap/uniswap.ts index 84136cfccf..621f3d061a 100644 --- a/src/connectors/uniswap/uniswap.ts +++ b/src/connectors/uniswap/uniswap.ts @@ -1,16 +1,3 @@ -import { UniswapConfig } from './uniswap.config'; -import { - findPoolAddress, - isValidV2Pool, - isValidV3Pool, - isFractionString, -} from './uniswap.utils'; -import { - IUniswapV2PairABI, - IUniswapV2FactoryABI, - IUniswapV2Router02ABI -} from './uniswap.contracts'; - // V3 (CLMM) imports import { Token, CurrencyAmount, Percent } from '@uniswap/sdk-core'; import { AlphaRouter } from '@uniswap/smart-order-router'; @@ -26,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 }; @@ -105,7 +105,9 @@ export class Uniswap { this.config.uniswapV3NftManagerAddress(this.networkName), [ { - inputs: [{ internalType: 'address', name: 'owner', type: 'address' }], + inputs: [ + { internalType: 'address', name: 'owner', type: 'address' }, + ], name: 'balanceOf', outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], stateMutability: 'view', diff --git a/test/connectors/uniswap/swap.test.js b/test/connectors/uniswap/swap.test.js index c769db1815..4b7bc989ae 100644 --- a/test/connectors/uniswap/swap.test.js +++ b/test/connectors/uniswap/swap.test.js @@ -61,7 +61,8 @@ function validateGasParameters(response) { 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 + Math.abs(response.gasCost - response.gasPrice * response.gasLimit * 1e-9) < + 1e-10 // Floating point precision ); } @@ -248,7 +249,7 @@ describe('Uniswap V3 Swap Router Tests (Base Network)', () => { test('handles different networks correctly', async () => { const networks = ['mainnet', 'arbitrum', 'optimism', 'base', 'polygon']; - + for (const network of networks) { const mockResponse = { estimatedAmountIn: 1.0, @@ -443,7 +444,7 @@ describe('Uniswap V3 Swap Router Tests (Base Network)', () => { test('handles multiple networks for execution', async () => { const networks = ['mainnet', 'arbitrum', 'optimism', 'base']; - + for (const network of networks) { const executeResponse = { signature: `0x${network}1234567890abcdef`, From ca0872272db2b548680386f3ae0f115375de9be5 Mon Sep 17 00:00:00 2001 From: WuonParticle Date: Wed, 2 Jul 2025 10:39:12 -0600 Subject: [PATCH 17/31] Tweak address example handling for Ethereum and Solana classes - Introduced static variable `_walletAddressExample` to store example wallet addresses. - Updated `getWalletAddressExample` method to return stored example or default address if none found. --- src/chains/ethereum/ethereum.ts | 16 +++++++++++----- src/chains/solana/solana.ts | 10 ++++++++-- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/chains/ethereum/ethereum.ts b/src/chains/ethereum/ethereum.ts index f677882873..9a8f5a4db3 100644 --- a/src/chains/ethereum/ethereum.ts +++ b/src/chains/ethereum/ethereum.ts @@ -34,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 = {}; @@ -286,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 @@ -636,14 +634,22 @@ export class Ethereum { return await wrappedContract.deposit(params); } + /** + * Get a wallet address example for schema documentation + */ public static async getWalletAddressExample(): Promise { - const defaultAddress = ''; + if (Ethereum._walletAddressExample) { + return Ethereum._walletAddressExample; + } + const defaultAddress = '0x0000000000000000000000000000000000000000'; try { - const foundWallet = await this.getFirstWalletAddress(); + 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( diff --git a/src/chains/solana/solana.ts b/src/chains/solana/solana.ts index c71c5f71ad..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; @@ -1273,13 +1274,18 @@ export class Solana { } public static async getWalletAddressExample(): Promise { - const defaultAddress = ''; + if (Solana._walletAddressExample) { + return Solana._walletAddressExample; + } + const defaultAddress = '11111111111111111111111111111111'; try { - const foundWallet = await this.getFirstWalletAddress(); + 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( From 794e389f576dafaacb6b86404fdaa69febffb995 Mon Sep 17 00:00:00 2001 From: WuonParticle Date: Wed, 2 Jul 2025 10:01:11 -0600 Subject: [PATCH 18/31] make walletAddressExample usage consistent and remove dynamic schema modification --- src/chains/ethereum/routes/allowances.ts | 5 ++--- src/chains/ethereum/routes/approve.ts | 5 ++--- src/chains/ethereum/routes/balances.ts | 5 ++--- src/chains/ethereum/routes/wrap.ts | 5 ++--- src/chains/solana/routes/balances.ts | 5 ++--- src/connectors/jupiter/routes/executeSwap.ts | 8 ++------ .../meteora/clmm-routes/addLiquidity.ts | 7 ++----- .../meteora/clmm-routes/closePosition.ts | 7 ++----- .../meteora/clmm-routes/collectFees.ts | 7 ++----- .../meteora/clmm-routes/executeSwap.ts | 5 ++--- .../meteora/clmm-routes/openPosition.ts | 6 ++---- .../meteora/clmm-routes/positionInfo.ts | 18 +++--------------- .../meteora/clmm-routes/positionsOwned.ts | 16 ++++++++-------- .../meteora/clmm-routes/removeLiquidity.ts | 9 ++------- .../raydium/amm-routes/addLiquidity.ts | 6 ++---- .../raydium/amm-routes/executeSwap.ts | 5 ++--- .../raydium/amm-routes/positionInfo.ts | 8 ++------ .../raydium/amm-routes/removeLiquidity.ts | 9 ++------- .../raydium/clmm-routes/closePosition.ts | 7 ++----- .../raydium/clmm-routes/collectFees.ts | 7 ++----- .../raydium/clmm-routes/executeSwap.ts | 5 ++--- .../raydium/clmm-routes/openPosition.ts | 6 ++---- .../uniswap/amm-routes/addLiquidity.ts | 6 ++---- .../uniswap/amm-routes/executeSwap.ts | 6 ++---- .../uniswap/amm-routes/positionInfo.ts | 5 ++--- .../uniswap/amm-routes/removeLiquidity.ts | 6 ++---- .../uniswap/clmm-routes/collectFees.ts | 14 ++------------ .../uniswap/clmm-routes/executeSwap.ts | 5 ++--- .../uniswap/clmm-routes/openPosition.ts | 5 ++--- .../uniswap/clmm-routes/positionInfo.ts | 5 ++--- .../uniswap/clmm-routes/positionsOwned.ts | 16 ++++++++-------- .../uniswap/clmm-routes/removeLiquidity.ts | 13 ++----------- src/connectors/uniswap/routes/execute-swap.ts | 4 ++-- src/connectors/uniswap/routes/quote-swap.ts | 3 ++- 34 files changed, 81 insertions(+), 168 deletions(-) diff --git a/src/chains/ethereum/routes/allowances.ts b/src/chains/ethereum/routes/allowances.ts index 17128e04f4..9707fbd2d5 100644 --- a/src/chains/ethereum/routes/allowances.ts +++ b/src/chains/ethereum/routes/allowances.ts @@ -174,8 +174,7 @@ export async function getEthereumAllowances( } export const allowancesRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const firstWalletAddress = await Ethereum.getWalletAddressExample(); + const walletAddressExample = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: AllowancesRequestType; @@ -203,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', diff --git a/src/chains/ethereum/routes/approve.ts b/src/chains/ethereum/routes/approve.ts index eff463a0a5..0301f3b433 100644 --- a/src/chains/ethereum/routes/approve.ts +++ b/src/chains/ethereum/routes/approve.ts @@ -174,8 +174,7 @@ export async function approveEthereumToken( } export const approveRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const firstWalletAddress = await Ethereum.getWalletAddressExample(); + const walletAddressExample = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: ApproveRequestType; @@ -203,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 66998b2d1c..7e80377699 100644 --- a/src/chains/ethereum/routes/balances.ts +++ b/src/chains/ethereum/routes/balances.ts @@ -162,8 +162,7 @@ export async function getEthereumBalances( } export const balancesRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const firstWalletAddress = await Ethereum.getWalletAddressExample(); + const walletAddressExample = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: BalanceRequestType; @@ -196,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 b5aa45efe6..4ce86c860f 100644 --- a/src/chains/ethereum/routes/wrap.ts +++ b/src/chains/ethereum/routes/wrap.ts @@ -193,8 +193,7 @@ export async function wrapEthereum( } export const wrapRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const firstWalletAddress = await Ethereum.getWalletAddressExample(); + const walletAddressExample = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: WrapRequestType; @@ -223,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 8c3bc00727..dd401b14b5 100644 --- a/src/chains/solana/routes/balances.ts +++ b/src/chains/solana/routes/balances.ts @@ -329,8 +329,7 @@ async function getOptimizedBalance( } export const balancesRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const firstWalletAddress = await Solana.getWalletAddressExample(); + const walletAddressExample = await Solana.getWalletAddressExample(); // Example address for Solana tokens const USDC_MINT_ADDRESS = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; // USDC on Solana @@ -351,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/connectors/jupiter/routes/executeSwap.ts b/src/connectors/jupiter/routes/executeSwap.ts index 579a5d79be..50444c85ee 100644 --- a/src/connectors/jupiter/routes/executeSwap.ts +++ b/src/connectors/jupiter/routes/executeSwap.ts @@ -95,11 +95,7 @@ async function executeJupiterSwap( } export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const firstWalletAddress = await Solana.getWalletAddressExample(); - - // Update schema example - ExecuteSwapRequest.properties.walletAddress.examples = [firstWalletAddress]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: ExecuteSwapRequestType; @@ -115,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/meteora/clmm-routes/addLiquidity.ts b/src/connectors/meteora/clmm-routes/addLiquidity.ts index f91eb6f501..86b15bcbc2 100644 --- a/src/connectors/meteora/clmm-routes/addLiquidity.ts +++ b/src/connectors/meteora/clmm-routes/addLiquidity.ts @@ -191,11 +191,7 @@ export type MeteoraAddLiquidityRequestType = Static< >; export const addLiquidityRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const firstWalletAddress = await Solana.getWalletAddressExample(); - - // Update schema example - AddLiquidityRequest.properties.walletAddress.examples = [firstWalletAddress]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: MeteoraAddLiquidityRequestType; @@ -211,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 36f7a5d6ad..99dae33287 100644 --- a/src/connectors/meteora/clmm-routes/closePosition.ts +++ b/src/connectors/meteora/clmm-routes/closePosition.ts @@ -118,11 +118,7 @@ async function closePosition( } export const closePositionRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const firstWalletAddress = await Solana.getWalletAddressExample(); - - // Update schema example - ClosePositionRequest.properties.walletAddress.examples = [firstWalletAddress]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: ClosePositionRequestType; @@ -137,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 89a35beaa8..ae7257d6e2 100644 --- a/src/connectors/meteora/clmm-routes/collectFees.ts +++ b/src/connectors/meteora/clmm-routes/collectFees.ts @@ -89,11 +89,7 @@ export async function collectFees( } export const collectFeesRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const firstWalletAddress = await Solana.getWalletAddressExample(); - - // Update schema example - CollectFeesRequest.properties.walletAddress.examples = [firstWalletAddress]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: CollectFeesRequestType; @@ -109,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 a810696ec3..26ac475382 100644 --- a/src/connectors/meteora/clmm-routes/executeSwap.ts +++ b/src/connectors/meteora/clmm-routes/executeSwap.ts @@ -100,8 +100,7 @@ async function executeSwap( } export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const firstWalletAddress = await Solana.getWalletAddressExample(); + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: ExecuteSwapRequestType; @@ -117,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 767f2c1e5e..7904e15a8f 100644 --- a/src/connectors/meteora/clmm-routes/openPosition.ts +++ b/src/connectors/meteora/clmm-routes/openPosition.ts @@ -257,10 +257,7 @@ export type MeteoraOpenPositionRequestType = Static< >; export const openPositionRoute: FastifyPluginAsync = async (fastify) => { - const firstWalletAddress = await Solana.getWalletAddressExample(); - - // Update schema example - OpenPositionRequest.properties.walletAddress.examples = [firstWalletAddress]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: MeteoraOpenPositionRequestType; @@ -276,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 ea4678d781..36bbbf3d40 100644 --- a/src/connectors/meteora/clmm-routes/positionInfo.ts +++ b/src/connectors/meteora/clmm-routes/positionInfo.ts @@ -12,12 +12,7 @@ import { logger } from '../../../services/logger'; import { Meteora } from '../meteora'; export const positionInfoRoute: FastifyPluginAsync = async (fastify) => { - const firstWalletAddress = await Solana.getWalletAddressExample(); - - // Update schema example - GetPositionInfoRequest.properties.walletAddress.examples = [ - firstWalletAddress, - ]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.get<{ Querystring: GetPositionInfoRequestType; @@ -32,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 87f35b68d3..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,12 +27,7 @@ type GetPositionsOwnedRequestType = Static; type GetPositionsOwnedResponseType = Static; export const positionsOwnedRoute: FastifyPluginAsync = async (fastify) => { - const firstWalletAddress = await Solana.getWalletAddressExample(); - - // Update schema example - GetPositionsOwnedRequest.properties.walletAddress.examples = [ - firstWalletAddress, - ]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.get<{ Querystring: GetPositionsOwnedRequestType; @@ -45,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 b89fbd16a0..aabfae1d33 100644 --- a/src/connectors/meteora/clmm-routes/removeLiquidity.ts +++ b/src/connectors/meteora/clmm-routes/removeLiquidity.ts @@ -103,13 +103,7 @@ export async function removeLiquidity( } export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const firstWalletAddress = await Solana.getWalletAddressExample(); - - // Update schema example - RemoveLiquidityRequest.properties.walletAddress.examples = [ - firstWalletAddress, - ]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: RemoveLiquidityRequestType; @@ -125,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 93b34e7844..9bb482f282 100644 --- a/src/connectors/raydium/amm-routes/addLiquidity.ts +++ b/src/connectors/raydium/amm-routes/addLiquidity.ts @@ -216,10 +216,7 @@ async function addLiquidity( } export const addLiquidityRoute: FastifyPluginAsync = async (fastify) => { - const firstWalletAddress = await Solana.getWalletAddressExample(); - - // Update schema example - AddLiquidityRequest.properties.walletAddress.examples = [firstWalletAddress]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: AddLiquidityRequestType; @@ -235,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 b7647d6199..3a3f39ce01 100644 --- a/src/connectors/raydium/amm-routes/executeSwap.ts +++ b/src/connectors/raydium/amm-routes/executeSwap.ts @@ -184,8 +184,7 @@ async function executeSwap( } export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const firstWalletAddress = await Solana.getWalletAddressExample(); + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: ExecuteSwapRequestType; @@ -201,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 89d1427be4..0a0aa619fc 100644 --- a/src/connectors/raydium/amm-routes/positionInfo.ts +++ b/src/connectors/raydium/amm-routes/positionInfo.ts @@ -79,8 +79,7 @@ async function calculateLpAmount( } export const positionInfoRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const firstWalletAddress = await Solana.getWalletAddressExample(); + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.get<{ Querystring: GetPositionInfoRequestType; @@ -95,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 ac0276b1f9..97094e03ca 100644 --- a/src/connectors/raydium/amm-routes/removeLiquidity.ts +++ b/src/connectors/raydium/amm-routes/removeLiquidity.ts @@ -257,13 +257,7 @@ async function removeLiquidity( } export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const firstWalletAddress = await Solana.getWalletAddressExample(); - - // Update schema example - RemoveLiquidityRequest.properties.walletAddress.examples = [ - firstWalletAddress, - ]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: RemoveLiquidityRequestType; @@ -279,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 2794246de0..857df9f164 100644 --- a/src/connectors/raydium/clmm-routes/closePosition.ts +++ b/src/connectors/raydium/clmm-routes/closePosition.ts @@ -99,11 +99,7 @@ async function closePosition( } export const closePositionRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const firstWalletAddress = await Solana.getWalletAddressExample(); - - // Update schema example - ClosePositionRequest.properties.walletAddress.examples = [firstWalletAddress]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: ClosePositionRequestType; @@ -119,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 c8cc51a12e..4b40eafd37 100644 --- a/src/connectors/raydium/clmm-routes/collectFees.ts +++ b/src/connectors/raydium/clmm-routes/collectFees.ts @@ -95,11 +95,7 @@ export async function collectFees( } export const collectFeesRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const firstWalletAddress = await Solana.getWalletAddressExample(); - - // Update schema example - CollectFeesRequest.properties.walletAddress.examples = [firstWalletAddress]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: CollectFeesRequestType; @@ -115,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 595c409eee..75ed08d864 100644 --- a/src/connectors/raydium/clmm-routes/executeSwap.ts +++ b/src/connectors/raydium/clmm-routes/executeSwap.ts @@ -224,8 +224,7 @@ async function executeSwap( } export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const firstWalletAddress = await Solana.getWalletAddressExample(); + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: ExecuteSwapRequestType; @@ -241,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 db451acf55..ee75c2ef75 100644 --- a/src/connectors/raydium/clmm-routes/openPosition.ts +++ b/src/connectors/raydium/clmm-routes/openPosition.ts @@ -165,10 +165,7 @@ async function openPosition( } export const openPositionRoute: FastifyPluginAsync = async (fastify) => { - const firstWalletAddress = await Solana.getWalletAddressExample(); - - // Update schema example - OpenPositionRequest.properties.walletAddress.examples = [firstWalletAddress]; + const walletAddressExample = await Solana.getWalletAddressExample(); fastify.post<{ Body: OpenPositionRequestType; @@ -184,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/uniswap/amm-routes/addLiquidity.ts b/src/connectors/uniswap/amm-routes/addLiquidity.ts index 321ff22e5e..c7dc859e2b 100644 --- a/src/connectors/uniswap/amm-routes/addLiquidity.ts +++ b/src/connectors/uniswap/amm-routes/addLiquidity.ts @@ -309,9 +309,7 @@ async function addLiquidity( export const addLiquidityRoute: FastifyPluginAsync = async (fastify) => { await fastify.register(require('@fastify/sensible')); - - // Get first wallet address for example - const firstWalletAddress = await Ethereum.getWalletAddressExample(); + const walletAddressExample = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: AddLiquidityRequestType; @@ -327,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: [''], diff --git a/src/connectors/uniswap/amm-routes/executeSwap.ts b/src/connectors/uniswap/amm-routes/executeSwap.ts index dc7f602545..849e849ebb 100644 --- a/src/connectors/uniswap/amm-routes/executeSwap.ts +++ b/src/connectors/uniswap/amm-routes/executeSwap.ts @@ -22,9 +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 firstWalletAddress = await Ethereum.getWalletAddressExample(); + const walletAddressExample = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: ExecuteSwapRequestType; @@ -40,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] }, diff --git a/src/connectors/uniswap/amm-routes/positionInfo.ts b/src/connectors/uniswap/amm-routes/positionInfo.ts index 6fcd1a499e..a56de10702 100644 --- a/src/connectors/uniswap/amm-routes/positionInfo.ts +++ b/src/connectors/uniswap/amm-routes/positionInfo.ts @@ -37,8 +37,7 @@ export async function checkLPAllowance( } export const positionInfoRoute: FastifyPluginAsync = async (fastify) => { - // Get first wallet address for example - const firstWalletAddress = await Ethereum.getWalletAddressExample(); + const walletAddressExample = await Ethereum.getWalletAddressExample(); fastify.get<{ Querystring: GetPositionInfoRequestType; @@ -53,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 2f10b89590..53966c93d4 100644 --- a/src/connectors/uniswap/amm-routes/removeLiquidity.ts +++ b/src/connectors/uniswap/amm-routes/removeLiquidity.ts @@ -23,9 +23,7 @@ import { checkLPAllowance } from './positionInfo'; export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { await fastify.register(require('@fastify/sensible')); - - // Get first wallet address for example - const firstWalletAddress = await Ethereum.getWalletAddressExample(); + const walletAddressExample = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: RemoveLiquidityRequestType; @@ -41,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: [''], diff --git a/src/connectors/uniswap/clmm-routes/collectFees.ts b/src/connectors/uniswap/clmm-routes/collectFees.ts index 3ef342f02d..f72d42a1a8 100644 --- a/src/connectors/uniswap/clmm-routes/collectFees.ts +++ b/src/connectors/uniswap/clmm-routes/collectFees.ts @@ -18,17 +18,7 @@ import { formatTokenAmount } from '../uniswap.utils'; export const collectFeesRoute: 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: CollectFeesRequestType; @@ -44,7 +34,7 @@ export const collectFeesRoute: FastifyPluginAsync = async (fastify) => { properties: { ...CollectFeesRequest.properties, network: { type: 'string', default: 'base' }, - walletAddress: { type: 'string', examples: [firstWalletAddress] }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, positionAddress: { type: 'string', description: 'Position NFT token ID', diff --git a/src/connectors/uniswap/clmm-routes/executeSwap.ts b/src/connectors/uniswap/clmm-routes/executeSwap.ts index 2ec06d8524..149f0ec8d0 100644 --- a/src/connectors/uniswap/clmm-routes/executeSwap.ts +++ b/src/connectors/uniswap/clmm-routes/executeSwap.ts @@ -23,8 +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 firstWalletAddress = await Ethereum.getWalletAddressExample(); + const walletAddressExample = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: ExecuteSwapRequestType; @@ -40,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] }, diff --git a/src/connectors/uniswap/clmm-routes/openPosition.ts b/src/connectors/uniswap/clmm-routes/openPosition.ts index cf3b1fa654..4aef2e5b5e 100644 --- a/src/connectors/uniswap/clmm-routes/openPosition.ts +++ b/src/connectors/uniswap/clmm-routes/openPosition.ts @@ -27,8 +27,7 @@ import { formatTokenAmount, parseFeeTier } from '../uniswap.utils'; export const openPositionRoute: FastifyPluginAsync = async (fastify) => { await fastify.register(require('@fastify/sensible')); - // Get first wallet address for example - const firstWalletAddress = await Ethereum.getWalletAddressExample(); + const walletAddressExample = await Ethereum.getWalletAddressExample(); fastify.post<{ Body: OpenPositionRequestType; @@ -44,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: [''] }, diff --git a/src/connectors/uniswap/clmm-routes/positionInfo.ts b/src/connectors/uniswap/clmm-routes/positionInfo.ts index 54963b3ff9..9a42f676ce 100644 --- a/src/connectors/uniswap/clmm-routes/positionInfo.ts +++ b/src/connectors/uniswap/clmm-routes/positionInfo.ts @@ -24,8 +24,7 @@ import { formatTokenAmount } from '../uniswap.utils'; export const positionInfoRoute: FastifyPluginAsync = async (fastify) => { await fastify.register(require('@fastify/sensible')); - // Get first wallet address for example - const firstWalletAddress = await Ethereum.getWalletAddressExample(); + const walletAddressExample = await Ethereum.getWalletAddressExample(); fastify.get<{ Querystring: GetPositionInfoRequestType; @@ -45,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 35e10f2d59..453fd20a98 100644 --- a/src/connectors/uniswap/clmm-routes/positionsOwned.ts +++ b/src/connectors/uniswap/clmm-routes/positionsOwned.ts @@ -41,13 +41,7 @@ const ENUMERABLE_ABI = [ export const positionsOwnedRoute: FastifyPluginAsync = async (fastify) => { await fastify.register(require('@fastify/sensible')); - - // Get first wallet address for example - const firstWalletAddress = await Ethereum.getWalletAddressExample(); - - PositionsOwnedRequest.properties.walletAddress.examples = [ - firstWalletAddress, - ]; + const walletAddressExample = await Ethereum.getWalletAddressExample(); fastify.get<{ Querystring: typeof PositionsOwnedRequest.static; @@ -58,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, }, diff --git a/src/connectors/uniswap/clmm-routes/removeLiquidity.ts b/src/connectors/uniswap/clmm-routes/removeLiquidity.ts index 88e213a49d..12ed3aec57 100644 --- a/src/connectors/uniswap/clmm-routes/removeLiquidity.ts +++ b/src/connectors/uniswap/clmm-routes/removeLiquidity.ts @@ -20,16 +20,7 @@ import { formatTokenAmount } from '../uniswap.utils'; export const removeLiquidityRoute: 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: RemoveLiquidityRequestType; @@ -45,7 +36,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] }, positionAddress: { type: 'string', description: 'Position NFT token ID', diff --git a/src/connectors/uniswap/routes/execute-swap.ts b/src/connectors/uniswap/routes/execute-swap.ts index 4767558e8e..f7787a7166 100644 --- a/src/connectors/uniswap/routes/execute-swap.ts +++ b/src/connectors/uniswap/routes/execute-swap.ts @@ -20,7 +20,7 @@ export const executeSwapRoute: FastifyPluginAsync = async ( await fastify.register(require('@fastify/sensible')); // Get first wallet address for example - const firstWalletAddress = await Ethereum.getWalletAddressExample(); + const walletAddressExample = await Ethereum.getWalletAddressExample(); // Get available networks from Ethereum configuration (same method as chain.routes.ts) const { ConfigManagerV2 } = require('../../../services/config-manager-v2'); @@ -45,7 +45,7 @@ export const executeSwapRoute: FastifyPluginAsync = async ( default: 'mainnet', enum: ethereumNetworks, }, - 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] }, diff --git a/src/connectors/uniswap/routes/quote-swap.ts b/src/connectors/uniswap/routes/quote-swap.ts index b37a1c44a6..b8371d5507 100644 --- a/src/connectors/uniswap/routes/quote-swap.ts +++ b/src/connectors/uniswap/routes/quote-swap.ts @@ -429,7 +429,7 @@ export const quoteSwapRoute: FastifyPluginAsync = async (fastify, _options) => { await fastify.register(require('@fastify/sensible')); // Get first wallet address for example - const firstWalletAddress = await Ethereum.getWalletAddressExample(); + const walletAddressExample = await Ethereum.getWalletAddressExample(); // Get available networks from Ethereum configuration (same method as chain.routes.ts) const ethereumNetworks = Object.keys( @@ -458,6 +458,7 @@ export const quoteSwapRoute: FastifyPluginAsync = async (fastify, _options) => { 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'], }, From 1c8bb775ae154c88871e0414f674f69283729ea1 Mon Sep 17 00:00:00 2001 From: WuonParticle Date: Wed, 18 Jun 2025 11:23:27 -0600 Subject: [PATCH 19/31] Add base src dir support, Optimize transform pattern, reference specific import-resolver version, exclude dist and coverage dirs --- .eslintrc.js | 12 ++++-------- jest.config.js | 4 +--- package.json | 2 +- tsconfig.build.json | 4 +++- tsconfig.json | 10 ++++------ 5 files changed, 13 insertions(+), 19 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index d29c5deb28..615248a104 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -42,14 +42,10 @@ module.exports = { }, settings: { 'import/resolver': { - typescript: { - alwaysTryTypes: true, - project: './tsconfig.json', - }, - node: { - extensions: ['.js', '.jsx', '.ts', '.tsx'], - }, - }, + 'import/resolver': [ + ['typescript', { alwaysTryTypes: true, project: './tsconfig.json' }], + ['node', { extensions: ['.js', '.jsx', '.ts', '.tsx'] }], + ], }, overrides: [ { diff --git a/jest.config.js b/jest.config.js index 2c5b0b3b36..c409deece5 100644 --- a/jest.config.js +++ b/jest.config.js @@ -15,8 +15,6 @@ 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/'], @@ -28,7 +26,7 @@ module.exports = { ], testMatch: ['/test/**/*.test.ts', '/test/**/*.test.js'], transform: { - '^.+\\.(t|j)s$': 'ts-jest', + '^.+\\.tsx?$': 'ts-jest', }, transformIgnorePatterns: ['/node_modules/(?!.*superjson)'], moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { diff --git a/package.json b/package.json index fff2f7d3db..c941495840 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,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-import-resolver-typescript": "4.4.3", "eslint-plugin-import": "^2.31.0", "eslint-plugin-n": "^16.6.2", "eslint-plugin-prettier": "^5.2.5", diff --git a/tsconfig.build.json b/tsconfig.build.json index db5d0b562b..5f8ea8285d 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,9 +1,11 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "incremental": true, + "composite": true, "skipLibCheck": true, "sourceMap": true, "inlineSourceMap": false }, - "exclude": ["node_modules", "test", "test-scripts"] + "exclude": ["node_modules", "dist", "coverage", "test", "test-scripts"] } diff --git a/tsconfig.json b/tsconfig.json index 834e552c59..d7a0a8c437 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,12 +21,10 @@ "resolveJsonModule": true, "baseUrl": ".", "paths": { - "#src/*": [ - "src/*" - ], - "#test/*": [ - "test/*" - ] + "#src": ["src/index.ts"], + "#src/*": ["src/*"], + "#test": ["test"], + "#test/*": ["test/*"] }, "typeRoots": ["node_modules/@types", "src/@types"], "downlevelIteration": true, From fff10f2433bfe3431b448412e4eed39468bbd165 Mon Sep 17 00:00:00 2001 From: WuonParticle Date: Tue, 1 Jul 2025 17:52:26 -0600 Subject: [PATCH 20/31] add node version requirement to make sure imports works correctly. --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index c941495840..adeeafeed0 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,9 @@ "main": "index.js", "license": "Apache-2.0", "repository": "https://github.com/hummingbot/gateway", + "engines": { + "node": ">=20.0.0" + }, "imports": { "#src/*": "./src/*", "#test/*": "./test/*" From 6818a9cfcfe2b1589c7eecf59bcd96146adf4b03 Mon Sep 17 00:00:00 2001 From: WuonParticle Date: Wed, 2 Jul 2025 11:29:40 -0600 Subject: [PATCH 21/31] fix unescaped from LLM suggestion. fix nested import/resolver change from llm --- .eslintrc.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 615248a104..5edfb40f08 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -41,11 +41,10 @@ module.exports = { 'import/no-duplicates': 'warn' }, settings: { - 'import/resolver': { - 'import/resolver': [ - ['typescript', { alwaysTryTypes: true, project: './tsconfig.json' }], - ['node', { extensions: ['.js', '.jsx', '.ts', '.tsx'] }], - ], + 'import/resolver': [ + ['typescript', { alwaysTryTypes: true, project: './tsconfig.json' }], + ['node', { extensions: ['.js', '.jsx', '.ts', '.tsx'] }], + ], }, overrides: [ { From 23397aede293822c1ac8b768315a96b43ff4ae9a Mon Sep 17 00:00:00 2001 From: WuonParticle Date: Wed, 2 Jul 2025 11:35:49 -0600 Subject: [PATCH 22/31] Add base aliases so import '#src' works without a trailing slash --- .eslintrc.js | 2 +- package.json | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index 5edfb40f08..58f501fb22 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -41,7 +41,7 @@ module.exports = { 'import/no-duplicates': 'warn' }, settings: { - 'import/resolver': [ + 'import/resolver': [ ['typescript', { alwaysTryTypes: true, project: './tsconfig.json' }], ['node', { extensions: ['.js', '.jsx', '.ts', '.tsx'] }], ], diff --git a/package.json b/package.json index adeeafeed0..6c0614d70e 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ "node": ">=20.0.0" }, "imports": { + "#src": "./src", "#src/*": "./src/*", + "#test": "./test", "#test/*": "./test/*" }, "scripts": { From deb9f0cd3c16399bd26e20aacf997323a9877184 Mon Sep 17 00:00:00 2001 From: WuonParticle Date: Wed, 2 Jul 2025 11:37:23 -0600 Subject: [PATCH 23/31] Store the .tsbuildinfo outside the project root --- tsconfig.build.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.build.json b/tsconfig.build.json index 5f8ea8285d..937c775e2e 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -3,6 +3,7 @@ "compilerOptions": { "incremental": true, "composite": true, + "tsBuildInfoFile": "./dist/.tsbuildinfo", "skipLibCheck": true, "sourceMap": true, "inlineSourceMap": false From cb4283015787cc95e28d2d98dc87afe8003c8af9 Mon Sep 17 00:00:00 2001 From: WuonParticle Date: Wed, 2 Jul 2025 11:37:41 -0600 Subject: [PATCH 24/31] add dist and coverage to exclude. --- tsconfig.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index d7a0a8c437..171ab5f1cc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -31,11 +31,12 @@ "skipLibCheck": true }, "exclude": [ - "node_modules" + "node_modules", + "dist", + "coverage" ], "include": [ "src/**/*.ts", - "src/**/*.js", "test/**/*.ts", "test-scripts/**/*.ts" ] From 427532e9b71c1bfa592a318606f3d87b9a8b084c Mon Sep 17 00:00:00 2001 From: WuonParticle Date: Wed, 2 Jul 2025 12:19:34 -0600 Subject: [PATCH 25/31] fix the #src and #test pathing using tsc-alias --- package.json | 7 ++-- pnpm-lock.yaml | 100 +++++++++++++++++++++++++++++++++----------- tsconfig.build.json | 2 + tsconfig.json | 1 - 4 files changed, 81 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 6c0614d70e..c170ec6048 100644 --- a/package.json +++ b/package.json @@ -9,14 +9,14 @@ "node": ">=20.0.0" }, "imports": { - "#src": "./src", + "#src": "./src/index.ts", "#src/*": "./src/*", "#test": "./test", "#test/*": "./test/*" }, "scripts": { "prebuild": "rimraf dist && mkdir dist", - "build": "tsc --project tsconfig.build.json && pnpm run copy-files", + "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", "clean:config": "find ./conf -maxdepth 1 -type f -delete", "format": "prettier . --write", @@ -152,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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 658186710e..6ace51f266 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -286,11 +286,11 @@ importers: specifier: ^7.32.1 version: 7.32.1 eslint-import-resolver-typescript: - specifier: ^4.4.3 - version: 4.4.4(eslint-plugin-import@2.31.0)(eslint@8.57.1) + 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-import-resolver-typescript@4.4.4)(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) @@ -354,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 @@ -3014,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==} @@ -3489,8 +3496,8 @@ packages: eslint-import-resolver-node@0.3.9: resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} - eslint-import-resolver-typescript@4.4.4: - resolution: {integrity: sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==} + 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: '*' @@ -5217,6 +5224,10 @@ 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==} @@ -5687,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'} @@ -5782,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==} @@ -6240,6 +6259,10 @@ 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'} @@ -6591,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==} @@ -8621,7 +8649,7 @@ snapshots: '@metaplex-foundation/beet': 0.6.1 '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.8.3)(utf-8-validate@5.0.10) bs58: 5.0.0 - debug: 4.4.1 + debug: 4.4.0(supports-color@8.1.1) transitivePeerDependencies: - bufferutil - encoding @@ -8634,7 +8662,7 @@ snapshots: '@metaplex-foundation/beet': 0.7.2 '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.8.3)(utf-8-validate@5.0.10) bs58: 5.0.0 - debug: 4.4.1 + debug: 4.4.0(supports-color@8.1.1) transitivePeerDependencies: - bufferutil - encoding @@ -8646,7 +8674,7 @@ snapshots: dependencies: ansicolors: 0.3.2 bn.js: 5.2.1 - debug: 4.4.1 + debug: 4.4.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -8654,7 +8682,7 @@ snapshots: dependencies: ansicolors: 0.3.2 bn.js: 5.2.1 - debug: 4.4.1 + debug: 4.4.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -8662,7 +8690,7 @@ snapshots: dependencies: ansicolors: 0.3.2 bn.js: 5.2.1 - debug: 4.4.1 + debug: 4.4.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -8671,7 +8699,7 @@ snapshots: ansicolors: 0.3.2 assert: 2.1.0 bn.js: 5.2.1 - debug: 4.4.1 + debug: 4.4.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -8756,7 +8784,7 @@ snapshots: '@solana/spl-token': 0.3.11(@solana/web3.js@1.98.2(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.8.3)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.8.3)(utf-8-validate@5.0.10) bn.js: 5.2.1 - debug: 4.4.1 + debug: 4.4.0(supports-color@8.1.1) transitivePeerDependencies: - bufferutil - encoding @@ -10463,7 +10491,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.1 + debug: 4.4.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -11305,6 +11333,8 @@ snapshots: commander@8.3.0: {} + commander@9.5.0: {} + complex.js@2.4.2: {} component-emitter@1.3.1: {} @@ -11763,7 +11793,7 @@ snapshots: 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-import-resolver-typescript@4.4.4)(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) @@ -11787,29 +11817,29 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.31.0)(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.2.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.4)(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) 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.4)(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): 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.4(eslint-plugin-import@2.31.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 4.4.3(eslint-plugin-import@2.31.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -11820,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-import-resolver-typescript@4.4.4)(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 @@ -11831,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-import-resolver-typescript@4.4.4)(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 @@ -12777,7 +12807,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.3 - debug: 4.4.1 + debug: 4.4.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -12791,7 +12821,7 @@ snapshots: https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.3 - debug: 4.4.1 + debug: 4.4.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -13145,7 +13175,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.4.1 + debug: 4.4.0(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -14063,6 +14093,8 @@ snapshots: rimraf: 2.4.5 optional: true + mylas@2.1.13: {} + nan@2.22.2: optional: true @@ -14497,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: {} @@ -14577,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: {} @@ -15031,7 +15069,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.3 - debug: 4.4.1 + debug: 4.4.0(supports-color@8.1.1) socks: 2.8.4 transitivePeerDependencies: - supports-color @@ -15099,6 +15137,8 @@ snapshots: dependencies: minipass: 7.1.2 + stable-hash-x@0.1.1: {} + stable-hash-x@0.2.0: {} stack-trace@0.0.10: {} @@ -15465,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 diff --git a/tsconfig.build.json b/tsconfig.build.json index 937c775e2e..18049c3487 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,6 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "outDir": "./dist", + "rootDir": "src", "incremental": true, "composite": true, "tsBuildInfoFile": "./dist/.tsbuildinfo", diff --git a/tsconfig.json b/tsconfig.json index 171ab5f1cc..2ce6b75bfa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,6 @@ "module": "CommonJS", "allowJs": true, "inlineSourceMap": true, - "outDir": "./dist", "removeComments": true, "noImplicitAny": false, "strictFunctionTypes": true, From ba993804bee890b39dc2120057c37f90740ad9fa Mon Sep 17 00:00:00 2001 From: WuonParticle Date: Wed, 2 Jul 2025 12:34:30 -0600 Subject: [PATCH 26/31] add dist dir to clean script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c170ec6048..9a319e9638 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "scripts": { "prebuild": "rimraf dist && mkdir dist", "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", + "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", From 2d9b4bbece2ff554db61b973c8c5d4a74a2439a3 Mon Sep 17 00:00:00 2001 From: WuonParticle Date: Wed, 2 Jul 2025 12:50:27 -0600 Subject: [PATCH 27/31] add json files to auto lint. --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index fff2f7d3db..45fe8e5d9f 100644 --- a/package.json +++ b/package.json @@ -163,7 +163,10 @@ "/src/commands" ], "lint-staged": { - "{src,test}/**/*.{ts,js}": "eslint --fix" + "{src,test}/**/*.{ts,js}": "eslint --fix", + "**/*.json": [ + "prettier --write" + ] }, "oclif": { "bin": "gateway", From 7801f045e48f39b8e48baf87862c8579e5c540ee Mon Sep 17 00:00:00 2001 From: WuonParticle Date: Wed, 2 Jul 2025 14:51:05 -0600 Subject: [PATCH 28/31] fix deprecated lines in husky pre-commit file --- .husky/pre-commit | 3 --- 1 file changed, 3 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index c4a570bcc8..cb2c84d5c3 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - pnpm lint-staged From 5681a37a1c3d569acf58ed224ccbce0485116921 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Wed, 2 Jul 2025 15:28:08 -0700 Subject: [PATCH 29/31] docs: update documentation with current API endpoints and architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated CLAUDE.md with passphrase requirements, supported networks/DEXs, and environment variables - Restructured README.md with comprehensive API endpoints, improved architecture section, and development guide - Enhanced test/README.md with detailed test structure, mock file conventions, and troubleshooting guide - Removed deprecated CLI documentation as requested 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CLAUDE.md | 28 ++++- README.md | 322 +++++++++++++++++++++++++++++-------------------- test/README.md | 201 ++++++++++++++++++++++-------- 3 files changed, 370 insertions(+), 181 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d67c9fc168..179bb30aa1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,15 +4,16 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Build & Command Reference - Build: `pnpm build` -- Start server: `pnpm start` -- Start in dev mode: `pnpm start --dev` (HTTP mode, no SSL) +- 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 generates certificates) +- Initial setup: `pnpm setup` (creates configs and copies certificates) +- Clean install: `pnpm clean` (removes node_modules, coverage, logs, dist) ## Architecture Overview @@ -88,5 +89,24 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - 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/feat/gateway-2.6 \ No newline at end of file +- 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/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/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 From 1e411c4a39ccbb347db63623758f4e56795cc8b6 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Wed, 2 Jul 2025 15:39:09 -0700 Subject: [PATCH 30/31] refactor: create generic AI agent instructions file with symlinks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Created AGENTS.md as the main source of agent instructions - Changed agent-specific language to be generic (AI coding assistants) - Created symlinks CLAUDE.md and GEMINI.md pointing to AGENTS.md - Ensures compatibility with multiple AI coding assistants (Claude, Gemini, Cursor, etc.) - Single source of truth for maintenance 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- AGENTS.md | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 113 +----------------------------------------------------- GEMINI.md | 1 + 3 files changed, 114 insertions(+), 112 deletions(-) create mode 100644 AGENTS.md mode change 100644 => 120000 CLAUDE.md create mode 120000 GEMINI.md 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 179bb30aa1..0000000000 --- a/CLAUDE.md +++ /dev/null @@ -1,112 +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 --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 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 From 45878dd382e238d1f4a5996bd9e25c56f6087ac8 Mon Sep 17 00:00:00 2001 From: rapcmia Date: Thu, 3 Jul 2025 13:42:48 +0800 Subject: [PATCH 31/31] update ver to 2.7.0 --- package.json | 2 +- src/app.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 833a5825b9..0a013df193 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gateway", - "version": "dev-2.7.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", diff --git a/src/app.ts b/src/app.ts index 687c608164..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 = 'dev-2.7.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)